Installed CLI Architecture
Technical contract for the portable Oblive operator CLI, deployment bundle, runtime generation, updates, and compatibility tests.
The installed oblive CLI is host operator tooling for a portable Oblive deployment. It owns the
installation lifecycle around the complete Docker Compose stack: configuration generation,
authentication preflight, image selection, service startup, inspection, coordinated updates, and
explicit data removal.
It is not an application control-plane client. oblivectl is the authenticated, agent-only CLI for
durable product transitions, while contextctl verify validates local Context and contextctl integration accesses live integration context. Keep deployment
operations in oblive; do not add application mutations or agent capabilities to it.
Source and Artifact Ownership
| Concern | Canonical source | Released artifact |
|---|---|---|
| Operator command implementation | scripts/release/oblive | oblive |
| Typed config/runtime generator | scripts/portable-stack.ts | lib/portable-stack.js |
| Base topology | compose.yaml | compose.yaml |
| Local Codex auth topology | compose.codex-local.yaml | compose.codex-local.yaml |
| Configuration contract | config/local-stack.schema.json | local-stack.schema.json |
| Bundle assembly | .github/scripts/release/build-deployment-bundle.sh | oblive-deployment.tar.gz |
| Public bootstrap | sites/landing/public/install.sh | https://oblive.dev/install.sh |
The CLI is POSIX sh so the installed deployment does not require a host Bun or a repository
checkout. Bundle construction still requires Bun because it compiles the TypeScript generator into
one dependency-bundled JavaScript file.
The release archive has a deliberately small, exact file set:
IMAGE_REPOSITORY
VERSION
compose.cloudflare.yaml
compose.codex-local.yaml
compose.yaml
docker/garage.toml
docker/postgres/001-vector.sql
lib/portable-stack.js
local-stack.schema.json
obliveDo not add source-only files, package-manager state, build directives, or floating image references to the portable artifact.
Installation Layout and Root Resolution
The default bootstrap creates:
~/.local/bin/oblive -> ~/.oblive/oblive
~/.config/oblive/install-root
~/.oblive/XDG_CONFIG_HOME changes the pointer directory, OBLIVE_BIN_DIR changes the bootstrap symlink
directory, and --install-dir changes the installed root. A named installation defaults to
~/.oblive-instances/<name> and registers its root at $XDG_CONFIG_HOME/oblive/instances/<name>
(using ~/.config when XDG is unset). Named roots must not overlap other registered roots. The
CLI resolves its root in this order:
- An explicit leading
--instance <name>, resolving its registration;defaultuses the legacy pointer. OBLIVE_HOME, when set.- The first line of
$XDG_CONFIG_HOME/oblive/install-root. ~/.oblive.
An unknown named instance fails without falling back to default. The shared PATH symlink may
point to any instance-aware release: the command dispatches to the selected installation’s own
CLI before running it. Installing a named release never replaces the default pointer. Purging
the instance owning that symlink repoints it to a surviving installation. oblive instances reads
the registrations and version files without invoking Docker.
The root must not be empty, /, the user’s home directory, or contain :. OBLIVE_HOME is useful
for tests and deliberate custom invocations, but the install pointer is the normal runtime contract
for a non-default installation.
Release-owned and machine-owned files remain separate:
| File or directory | Owner | Update behavior |
|---|---|---|
VERSION | Release | Replaced with the coordinated semantic version |
IMAGE_REPOSITORY | Release | Replaced with the release repository |
Compose and lib/ | Release | Replaced as one bundle |
.channel | Installer/CLI | Tracks latest or pinned |
.download-base-url | Installer/CLI | Preserved into the staged release |
.instance | Installer/CLI | Stable name; preserved unchanged across updates |
.cli-link | Installer/CLI | Shared command location; preserved across updates |
local-stack.json | Human operator | Copied unchanged into a staged update |
.runtime/ | Generator output | Regenerated from config and the installed release |
local-stack.json and generated environment files are mode 0600. The generator runs as the
invoking host UID and GID so mounted output does not become root-owned on Linux.
Bootstrap Boundary
The public installer performs the one-time host bootstrap:
- Validate macOS or Linux on AMD64 or ARM64 and require
curl,tar,awk,sed, andmktemp. - Verify Docker, the Compose plugin, and the selected Codex or API-key authentication prerequisite.
- Resolve
latestto an exact stablevX.Y.Z, or normalize the explicitly selected version. - Download the versioned archive and checksum over HTTPS.
- Verify the SHA-256 digest, reject traversal paths, and reject links or special archive entries.
- Extract into the installation root, write release-channel metadata, create the CLI symlink and install pointer, and initialize the machine-local configuration.
- Start the stack unless
--no-startwas selected.
Re-running the installer against a valid existing root delegates release movement to oblive update, then reinitializes without replacing the existing configuration. A directory that exists
but is not a valid Oblive installation fails closed.
A new named instance requires --port-base, an integer from 1024 through 65530. The typed
generator initializes six consecutive ports, ordered PostgreSQL, Redis, Garage S3, Garage admin,
backend, frontend. The operator chooses non-overlapping ranges. Reinitialization preserves the
existing config and rejects a supplied base that differs from its saved ports.
Instance-aware bundles answer the internal --instance-format capability probe with 1 before
root resolution. The installer and updater reject a named-instance target without that capability
before invoking its lifecycle commands. Legacy bundles remain usable as the default installation.
Runtime Generation and Compose Invocation
The portable generator is bundled with the release, but it executes with the pinned Bun binary in the release’s semantic backend image:
host oblive script
-> pull <repository>:backend-vX.Y.Z
-> mount installation root at /oblive
-> run bun /oblive/lib/portable-stack.js as host UID:GID
-> write validated .runtime files
-> invoke Docker ComposeThis keeps the generator coordinated with the bundle without requiring a host runtime or making the backend image the owner of generator source. The CLI always invokes Compose with:
- project name
oblivefor the default instance oroblive-<name>for a named instance; - the installation root as the project directory;
.runtime/compose.envas the environment file;compose.yaml; andcompose.codex-local.yamlonly whenauthModeislocal.
The instance’s fixed project name stabilizes container and named-volume identity across bundle
replacement, including update staging. It never includes the release version or temporary directory.
Existing installations without .instance metadata retain project oblive. PostgreSQL, Redis,
Garage, agent workspaces, and container-owned
Codex state use named volumes. Normal stop, restart, update, and uninstall operations omit
--volumes and therefore preserve that state.
prepare_runtime pulls the semantic backend image, initializes missing default-instance configuration, validates the
configured authentication source, and generates release-specific environment files. start then
pulls every selected image and runs compose up --detach --wait --remove-orphans. The migration
container is a one-shot dependency; PostgreSQL remains the durable owner of operational state.
Missing named-instance configuration fails with instructions to rerun the installer with a port base.
The instance work changes only host deployment tooling. oblivectl, application HTTP contracts,
OpenAPI, and database schemas are unaffected. The regression suite exercises three installations,
release dispatch, isolated update/reset/purge, preserved default data/configuration, legacy-bundle
rejection, and Docker-rendered network, volume, and port separation.
Authentication Boundary
Local mode reads the configured host Codex auth.json only as an input to the dedicated
codex-auth-sync service. Chat and worker services mount container-owned Codex state; they do not
mount the host Codex home.
API-key mode resolves the configured host environment-variable name at invocation time. The CLI
passes that variable only to the generator container. The secret is absent from local-stack.json,
release metadata, and Compose interpolation output, and is materialized only in private generated
agent environment files.
oblive auth login is valid only for local mode. API-key mode supports status validation but never
owns a provider login flow.
Coordinated Update Transaction
start is intentionally version-stable. Only update can change the installed release.
Plain oblive update advances an installation whose .channel is latest. A pinned installation
requires --version vX.Y.Z or --latest. The update transaction is:
- Create a private staging directory next to the installed root.
- Resolve and download the requested stable bundle and checksum.
- Verify checksum, archive safety, embedded version, and semantic ordering.
- Reject a downgrade before touching the running installation.
- Copy
local-stack.json, channel metadata, and the download base URL into the staged root. - Run generation and
compose config --quietagainst the staged root. - If the old stack is running, stop it without removing volumes.
- Move the old root to a private backup and atomically move the staged root into its place.
- Restart only when the old stack had been running. Retain the private previous installation and print its location after success.
Validation happens before downtime. If startup fails after replacement, the newly installed release remains active and the previous files remain in the printed backup directory. The CLI does not automatically roll back: the new startup may already have applied irreversible migrations. Docker volumes remain intact throughout the transaction.
An update to the already installed version changes the channel metadata when requested, but does not replace release files or restart services.
Command and Error Contract
The CLI uses three process-status classes:
| Status | Meaning |
|---|---|
0 | Help, a successful operation, or an intentional no-op |
1 | Runtime, dependency, authentication, validation, or I/O failure |
2 | Unknown command, invalid option, or invalid command usage |
Help is available through oblive, oblive --help, oblive help <command>, and <command> --help. Human-readable color appears only on a terminal and is disabled when NO_COLOR is set.
Keep errors actionable and send them to standard error. Preserve the service allowlist in logs so
user input cannot become an arbitrary Compose service or option.
Destructive Boundaries
Data removal must stay syntactically explicit:
reset --confirmstops the stack and removes named volumes but retains configuration and the CLI;- plain
uninstallstops services and removes the recognized CLI symlink without removing volumes or the installation root; and uninstall --purge --confirmremoves volumes, the recognized CLI symlink, the installation root, and its pointer.
Never make stop, restart, update, reinstall, or plain uninstall imply volume deletion. Never widen a recursive deletion target without resolving and validating the exact installation root first.
Change and Test Contract
Treat the bootstrap, archive, CLI, generator, Compose topology, semantic image tags, and release workflow as one compatibility boundary. A change to one may require coordinated updates to all of them.
Run these focused suites after changing the installed CLI:
.github/scripts/tests/deployment-bundle.test.sh
.github/scripts/tests/deployment-bundle-container.test.sh
.github/scripts/tests/portable-install.test.sh
bun test scripts/tests/portable-stack.test.ts
bun run --cwd sites/docs checkThe tests cover the exact archive contents and checksum, build-free Compose topology, POSIX shell syntax, generated-file ownership and permissions, install and PATH behavior, configuration preservation, help and exit statuses, authentication modes, semantic updates, downgrade rejection, and corrupt-download handling.
Use a bundle-only release only when existing application images remain compatible with the CLI,
generator, schema, and Compose change. Otherwise publish a full coordinated release. See
Container Releases for the publication and smoke-test boundary.
Upgrade acceptance and recovery
Before upgrading, record the installed version, image IDs/digests, effective non-secret configuration,
active work IDs, schedules and profile catalog pins. Run oblive doctor and validate the staged
configuration. Download and verify the target bundle and images before downtime. Dispatch GitHub CD
with expected_sha set to the exact commit whose CI passed; a different source fails before release.
For a release without typed deployment shutdown, first stop new work dispatch through the supported organization setting and let existing runs finish. Preserve the previous dispatch flags and restore them after acceptance. This avoids interrupting old processes that cannot classify a restart.
Stop agent containers first with a 60-second timeout, keeping backend, PostgreSQL, Redis and object
storage available while partial files and receipts synchronize. New bundles set both agent services’
stop_grace_period to 60 seconds. For an older bundle, explicitly use docker stop --time 60 with the
agent container IDs discovered from the oblive Compose project. Do not remove volumes.
After agent shutdown, stop the remaining services and create one private, consistent recovery set:
- The complete installed directory, including
local-stack.jsonand its credential encryption key. - Every named volume mounted by the installation: PostgreSQL, Redis, Garage metadata/data, agent workspaces and Codex state. Discover mounts from the actual containers, rather than assuming names.
- The image identities and installed release/bundle checksum. Keep the previous images available.
Treat the recovery set as credentials: restrict its directory to the owner, keep it outside Git, and verify archive checksums. A configuration copy alone is not a data backup. Restore tests must use a disposable Compose project and matching database/object-store volumes; never overwrite live volumes as a test. A rollback restores the previous installation and its matching pre-migration durable state. Starting old images against a migrated database is unsupported. Retain the recovery set until post-upgrade acceptance finishes and according to the owner’s backup policy.
After upgrading, verify service health, migration completion, immutable image revisions and automatic built-in instruction pins. Preserve explicit/custom pins, existing task identities, schedules and action ownership. Deployment interruption uses the existing bounded retry path; it does not replay an uncertain external write. Check action receipts before recovery. Confirm retained files and at least one real permitted provider read separately from mocked platform tests. Reconnect expired provider grants only when the provider requires consent.