Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/features/sandbox/standard-sizes.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The values must stay within the bounds that Core and `validSandboxResources` acc

- The Web setup wizard, through `defaultSandboxResources` in `deployment-specification.ts`.
- The release bundle: `scripts/build-core-distribution.sh` copies this file to `<bundle>/standard-sizes.json`.
- The Core installer: `deploy/install/sandbox_setup.py` reads the bundled copy when `install.sh` saves the initial Docker or microsandbox deployment (`--sandbox`, microsandbox by default).
- The Core installer: `deploy/install/sandbox_setup.py` reads the bundled copy when `install.sh` saves the initial microsandbox deployment at the Standard size.

## Contract

Expand Down
21 changes: 9 additions & 12 deletions deploy/install/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ This directory holds the Core/Web installer, the `oac` command and the node inst

- Install only into an empty directory or over an [incomplete installation](#new-installations), or repair a complete installation of the same source revision. Require the current state format before changing installation files or services, and a matching source revision when repairing a complete installation. An incomplete installation of that state format is removed before installing the selected release, even when its source revision differs. Preserve data when those checks fail.
- The packaged `oac.pyz` embeds its build revision and refuses a `state.json` whose `source_commit` differs.
- The installer and every mutating `oac` command share `.oac.lock`. The installer holds it across creation, payload, native service and launcher repair, and apply, calling the already-locked apply implementation without locking again. Never replace the lock file; its inode must stay stable. Only the cleanup of a new installation, with the directory the installer created, and `oac uninstall` unlink it, last and while holding it. `locked` refuses a lock whose path no longer names the file it locked.
- The installer and every mutating `oac` command share `.oac.lock`. The installer holds it across creation, payload and launcher repair, and apply, calling the already-locked apply implementation without locking again. Never replace the lock file; its inode must stay stable. Only the cleanup of a new installation, with the directory the installer created, and `oac uninstall` unlink it, last and while holding it. `locked` refuses a lock whose path no longer names the file it locked.

## New installations

- An installation is complete after its first start: apply converged, the services are healthy and, for Web-only, its Core accepts the Core key. `create()` writes `state.json` before anything else, atomically, with the project name and `"complete": false`, and the installer sets it to `true` at that moment. A later failure, such as a refused sandbox selection, keeps the installation.
- An installation is complete after its first start: apply converged and the services are healthy. `create()` writes `state.json` before anything else, atomically, with the project name and `"complete": false`, and the installer sets it to `true` at that moment. A later failure, such as a refused sandbox selection, keeps the installation.
- Until then, `oac apply`, `start`, `domain` and `rotate-core-key` and Web's domain setup refuse the installation and point to the installer and `oac uninstall`; `oac status` reports it. Only the installer starts an incomplete installation; the installer or `oac uninstall` removes it.
- Any failure or interrupt (Ctrl-C, SIGTERM, SIGHUP) of an installation that is not complete runs `oac_cli.remove` before anything is printed, so a closed terminal can't stop it. The original error then goes on, and the installer prints it followed by `Nothing was kept; fix the problem and rerun the same command.` `remove` disables native Core's unit, runs `docker compose -p <project> down --volumes --remove-orphans` from `/` without `COMPOSE_*` variables and with its output discarded, and deletes every file in the installation directory. The directory goes too when the installer created it; otherwise it stays with its lock. Loaded images stay.
- Any failure or interrupt (Ctrl-C, SIGTERM, SIGHUP) of an installation that is not complete runs `oac_cli.remove` before anything is printed, so a closed terminal can't stop it. The original error then goes on, and the installer prints it followed by `Nothing was kept; fix the problem and rerun the same command.` `remove` runs `docker compose -p <project> down --volumes --remove-orphans` from `/` without `COMPOSE_*` variables and with its output discarded, and deletes every file in the installation directory. The directory goes too when the installer created it; otherwise it stays with its lock. Loaded images stay.
- The release downloader waits for the bundled installer in a separate process group. It forwards the first Ctrl-C, SIGTERM or SIGHUP and waits for cleanup before removing the temporary bundle; repeated signals do not interrupt that wait. The child inherits the download lock, so killing the downloader alone cannot expose its active files to another download.
- `remove` touches only the `oac-<10 hex digits>` project and its `<project>-core.service` named in `state.json`, and never follows a link. `state.json`, then the `oac` command, then `.oac.lock` are removed last, and the files stay when a service can't be removed, so the next run still recognizes the installation and no other command locks it afresh mid-removal. Those final unlinks ignore SIGINT, SIGTERM and SIGHUP; if the command cannot be unlinked, cleanup restores `state.json` for a retry. SIGKILL or power loss during those final unlinks can leave files that need manual removal. The error lists what is left and the commands that remove it; the printed Compose command uses the same directory and environment isolation as automatic cleanup.
- `remove` touches only the `oac-<10 hex digits>` project named in `state.json`, and never follows a link. `state.json`, then the `oac` command, then `.oac.lock` are removed last, and the files stay when a service can't be removed, so the next run still recognizes the installation and no other command locks it afresh mid-removal. Those final unlinks ignore SIGINT, SIGTERM and SIGHUP; if the command cannot be unlinked, cleanup restores `state.json` for a retry. SIGKILL or power loss during those final unlinks can leave files that need manual removal. The error lists what is left and the commands that remove it; the printed Compose command uses the same directory and environment isolation as automatic cleanup.
- A rerun over an incomplete installation, a `state.json` without `"complete": true`, removes it the same way under the lock and then checks the settings and ports and installs with the flags given now. Only that explicit marker proves completion. A directory without `state.json` is refused whatever it holds, and nothing in it is removed.

## Uninstall
Expand All @@ -62,23 +62,20 @@ This directory holds the Core/Web installer, the `oac` command and the node inst

## Install-time sandbox selection

`--sandbox docker|microsandbox|e2b|none` (default `microsandbox`; only `none` with `--web-only`) is a one-time action. After the services are healthy, the installer posts `/core/v1/sandbox/deployment` once, as Web's setup would, and never on a repair. The choice is not written to `config.json`; PostgreSQL owns it, and an existing database selection is never overwritten.
After the services are healthy, the installer posts `/core/v1/sandbox/deployment` once for microsandbox at Web's Standard size, as Web's setup would, and never on a repair. The choice is not written to `config.json`; PostgreSQL owns it, and an existing database selection is never overwritten.

- Docker and microsandbox use Web's Standard size from `apps/web/src/features/sandbox/standard-sizes.json`, which the distribution build copies into the bundle. Keep no other copy of those values.
- `docker` prints its weaker isolation and needs a y/N confirmation or `--accept-docker-risks` before anything is created.
- `e2b` needs a non-loopback HTTPS `public_url`, `--e2b-api-key-file` and `--e2b-template`; otherwise the installer refuses before installing anything.
- A Docker or microsandbox selection with a loopback `public_url` is saved, but no node can serve it until `public_url` is guest-reachable HTTPS.
- microsandbox uses Web's Standard size from `apps/web/src/features/sandbox/standard-sizes.json`, which the distribution build copies into the bundle. Keep no other copy of those values.
- A selection with a loopback `public_url` is saved, but no node can serve it until `public_url` is guest-reachable HTTPS.

## Accounts and permissions

- The Core/Web installer runs as the launching account, root included, in a writable installation directory. It never invokes sudo, switches accounts or changes Docker permissions. Check the actual platform, Docker and directory prerequisites; root alone is no reason to refuse.
- `--native-core` runs Core as a systemd user service of that account, with lingering, and keeps PostgreSQL and Web in Compose with a private loopback database port. Native Core needs no KVM or node assets.
- Installation state and secrets are private under `~/.oac/`. No credential enters build arguments, image layers, browser bundles or diagnostic output. The Compose file is confidential.
- The distribution build uses umask 022 so non-root service users can read the payload; installation credentials and state keep their private modes.

## Managed HTTPS

A default combined Docker installation adds two Compose services from one pinned image: `gateway` runs Caddy, and `installation` runs the packaged `oac domain-server`. The latter runs with the installing account's UID and its Docker socket access and calls the same locked apply implementation. Its only request surface is the private `ingress/api/api.sock`, with Core-key authentication and one typed domain action. Core and Web get no Docker socket, host process authority or writable installation configuration. Web gets only the private API socket directory, never Caddy's admin socket.
A default installation adds two Compose services from one pinned image: `gateway` runs Caddy, and `installation` runs the packaged `oac domain-server`. The latter runs with the installing account's UID and its Docker socket access and calls the same locked apply implementation. Its only request surface is the private `ingress/api/api.sock`, with Core-key authentication and one typed domain action. Core and Web get no Docker socket, host process authority or writable installation configuration. Web gets only the private API socket directory, never Caddy's admin socket.

- The gateway publishes the initial Web port, and ports 80 and 443 only for HTTPS (`ingress_config.published`). Caddy issues and renews certificates and keeps its private data in `ingress/data`. `generated/Caddyfile` is derived from `config.json`, and apply reloads it through the private Caddy socket even when container inputs already match.
- A domain change first checks that the hostname resolves and that no other program holds port 80 or 443; ports the gateway publishes while it runs as written are its own. It confirms a public URL change from the applied address, because it returns Core there before switching. It keeps the old entry point while the common apply renders the candidate address: the gateway publishes 80 and 443 and serves the candidate, and the verification needs a trusted certificate and an installation-specific response over HTTPS. Only then does it set `public_url` and apply again. Failure restores the previous configuration, including the gateway without 80 and 443 when HTTPS was off, and reports incomplete recovery; failed retries restore through the common apply even after a partial change.
Expand Down Expand Up @@ -140,7 +137,7 @@ The Core and node installers share one resolver for these identities. It confirm
- Interactive selection and CLI-only installation share one options and validation path. There is no installation-options file. The saved installation state and explicitly supplied credential and tool-variable files serve runtime operation, not a second configuration language.
- Each release bundles pinned Node.js and npm, the native Harnesses and their adapter assets. Registration lives in the CLI, and native activation and readiness in each adapter's optional `agent.Installation` descriptor. Core never selects native paths or OS-specific steps.
- Bootstrap scripts only download and extract the current platform's archive, after verifying the checksum Core provides. Installation, startup, connection verification and execution stay common. Native bundles must match Core's source revision and Runtime wire version.
- Neither Core installation nor repair downloads native payloads. The Core installer keeps the catalog and any offline archives in the installation's private `native-installers/` directory, mounts it read-only into container Core and points native Core at the same files. Core serves the local offline archives or redirects to the catalog URL without proxying or caching; it verifies local archives when it starts, and a corrupt local archive stops Core from starting.
- Neither Core installation nor repair downloads native payloads. The Core installer keeps the catalog and any offline archives in the installation's private `native-installers/` directory and mounts it read-only into Core. Core serves the local offline archives or redirects to the catalog URL without proxying or caching; it verifies local archives when it starts, and a corrupt local archive stops Core from starting.
- Every mutation holds the installation directory lock. Publish complete, checksum-verified components from staging, then commit the configuration after native readiness passes. A rerun with the same connection settings adds the selected Harnesses and validates existing contents. Never overwrite, upgrade, repair or migrate installed components; missing, modified, wrong-platform or incompatible content is an explicit error. A partial addition keeps the old configuration and reusable complete components and removes nothing.
- Serialize background PID inspection and publication so concurrent starts cannot create two daemons. An installed daemon registers only the adapter kinds its verified installation manifest names; other Harness executables on `PATH` cannot extend it. Direct `connect` refuses an installed Runtime and points to `start`.
- The installer runs as the current user in writable directories and never elevates. Subprocess diagnostics never expose sensitive parameters or environment values. Readiness checks take the installer's cancellation context and reap their processes before returning.
Expand Down
56 changes: 6 additions & 50 deletions deploy/install/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Process settings of one installation. Edit config.json, then run oac apply.",
"type": "object",
"additionalProperties": false,
"required": ["format", "mode"],
"required": ["format"],
"properties": {
"$schema": {
"type": "string",
Expand All @@ -16,18 +16,6 @@
"description": "Configuration format for this release. Fixed after installation.",
"x-oac": {"setting": false, "changeable": false}
},
"mode": {
"enum": ["all", "core-only", "web-only"],
"default": "all",
"description": "Which services this installation runs.",
"x-oac": {"changeable": false, "install_flag": "--core-only or --web-only"}
},
"native_core": {
"type": "boolean",
"default": false,
"description": "Run Core as a systemd user service instead of a container.",
"x-oac": {"changeable": false, "modes": ["all", "core-only"], "install_flag": "--native-core"}
},
"public_url": {
"type": ["string", "null"],
"default": null,
Expand All @@ -42,7 +30,7 @@
"host": {
"type": "string",
"default": "127.0.0.1",
"description": "Listener IP. With managed ingress only the gateway is public; Core stays on loopback. The default combined installer listens on all IPv4 interfaces.",
"description": "Listener IP. With managed ingress only the gateway is public; Core stays on loopback. The default installer listens on all IPv4 interfaces.",
"x-oac": {
"check": "listen_host",
"restarts": [
Expand All @@ -64,12 +52,10 @@
"minimum": 1024,
"maximum": 65535,
"default": 8091,
"description": "Host port of the Core API. With native Core, Web follows it.",
"description": "Host port of the Core API.",
"x-oac": {
"modes": ["all", "core-only"],
"restarts": ["core"],
"native_restarts": ["core", "web"],
"derives": ["Core port mapping or OAC_ADDR"],
"derives": ["Core port mapping"],
"install_flag": "--core-port"
}
},
Expand All @@ -80,39 +66,10 @@
"default": 8080,
"description": "Host port of Web.",
"x-oac": {
"modes": ["all", "web-only"],
"restarts": ["web"],
"derives": ["Web port mapping or OAC_WEB_ADDR"],
"derives": ["Web or gateway port mapping"],
"install_flag": "--web-port"
}
},
"database": {
"type": "integer",
"minimum": 1024,
"maximum": 65535,
"description": "Loopback port of PostgreSQL. Present exactly when native_core is true; the installer picks a free port.",
"x-oac": {
"modes": ["all", "core-only"],
"restarts": ["database", "core"],
"derives": ["database port mapping", "OAC_DATABASE_URL"]
}
}
}
},
"web": {
"type": "object",
"additionalProperties": false,
"x-oac": {"modes": ["web-only"]},
"properties": {
"core_url": {
"type": "string",
"description": "Origin of the Core that this Web connects to: HTTPS, or HTTP on a loopback host.",
"x-oac": {
"check": "origin",
"restarts": ["web"],
"derives": ["OAC_WEB_UPSTREAM"],
"install_flag": "--core-url"
}
}
}
},
Expand Down Expand Up @@ -143,7 +100,6 @@
"core": {
"type": "object",
"additionalProperties": false,
"x-oac": {"modes": ["all", "core-only"]},
"properties": {
"execution_concurrency": {
"type": "integer",
Expand Down Expand Up @@ -270,7 +226,7 @@
"ingress": {
"enum": ["managed", "external"],
"default": "external",
"description": "managed provides automatic HTTPS and Web domain setup for a combined Docker installation; install.sh selects it by default. external uses your existing proxy. Fixed after installation.",
"description": "managed provides automatic HTTPS and Web domain setup; install.sh selects it by default. external uses your existing proxy. Fixed after installation.",
"x-oac": {"changeable": false, "install_flag": "--ingress"}
}
}
Expand Down
Loading
Loading