Compose development flow
Overview
./bin/occ dev up starts local OpenClaw Enterprise development from a checkout.
The scripts/dev-up entry point selects the same profile. By default it starts the
local k3d-only profile:
Kubernetes Compute with the Kubernetes control plane
(internal/occdev/profile.go:ResolveProfile). Operators can select the Compose
control plane, or Docker Compute with OCC and PostgreSQL in Compose. The
Kubernetes profile can select the OpenShell Sandbox Driver for its fail-closed
path; it prepares pinned OpenShell infrastructure and reconciles rendered
workspace-chart resources before reporting readiness.
Compose-backed profiles perform host preflight, select Docker Engine or Podman,
prepare runtime images, start Compose, and wait for PostgreSQL migration,
Installation bootstrap, API health, and worker readiness. The Kubernetes-only
profile performs the equivalent readiness checks inside its owned cluster.
All profiles prepare HTTPS Console and Agent DNS hosts and use the existing
loopback TLS proxy, even when Agent native access is disabled. Session cookie
scope derives from those hosts; cookie-domain settings are refused.
For Docker Compute, scripts/dev-up calls the hidden CLI adapter backed by
internal/occdev/docker_browser.go:DockerBrowserEnvironment. It reuses the
TLS generator and keeps private per-checkout certificates in the system
temporary directory across stop/start. The launcher prints the Console
URL and CA certificate path. occ dev down reloads the same proxy settings.
Startup proves authenticated Installation access with a protected local
bootstrap service key; it does not create an Agent or prove model execution.
For Docker Compute, the worker can reconcile Namespace infrastructure, but Agent deployment stops at harness authentication admission because Docker Compute rejects bindings. Kubernetes Agent execution continues through the selected Kubernetes Compute Driver and the authenticated Agent deployment procedure.
Entry Points
- Trigger:
./bin/occ dev up [--key-output PATH] [-- COMPOSE_GLOBAL_OPTIONS...]from the repository root, followed by authenticated Namespace operations. - Source:
scripts/dev-up:require_commandandinternal/occdev/up.go:Up. - Assumptions: Docker Engine with Compose, or Podman with
docker-composeorpodman-compose(Docker Compute requirespodman-compose); Bash, curl, Python 3, andyqv4 for the Docker profile; writable PostgreSQL and Configuration volumes; loopback API publication; executablebin/occbuilt withpnpm cli:build. Startup needs no model credential.
The Kubernetes profile additionally uses compose.kubernetes.yaml,
internal/occdev, k3d, and kubectl. ./bin/occ dev down owns profile cleanup;
scripts/dev-down dispatches to it. Without a Compute Driver selection,
internal/occdev/profile.go:chooseDownStack sends cleanup to the stack
recorded in the state directory, and to Docker only when that directory is
absent or Compose options follow --; it refuses another checkout's stack.
The local Kubernetes development guide owns the operator procedure and
destructive cleanup boundary.
Flow
graph TD
A["./bin/occ dev up"] --> Checkout{"Valid checkout markers<br/>and LF module?"}
Checkout -->|No| Refuse["Refuse before startup"]
Checkout -->|Yes| Guard{"Development settings valid?"}
Guard -->|No| Refuse
Guard -->|Yes| Profile{"Compute profile"}
Profile -->|Docker| B["Preflight host tools, resolve Podman machine connection,<br/>and inspect Compose config"]
Profile -->|Kubernetes| KPre["Pin local engine endpoint<br/>and reject existing resources"]
KPre --> KControl{"Control-plane profile"}
KControl -->|Kubernetes| KOwned["Create owned cluster,<br/>verify DNS/API and select kubeconfig"]
KOwned --> KDriver{"Sandbox Driver"}
KDriver -->|none| KCodex["Import runtime and verify<br/>the normal Codex sandbox"]
KCodex -->|success| KRoutingFirst["Install routing controllers<br/>and remaining images"]
KCodex -->|failure| KRollback
KDriver -->|OpenShell| KOpenFirst["Prepare OpenShell assets,<br/>install routing, then import runtime"]
KRoutingFirst --> KOnly["Install selected control-plane profile<br/>and retain all readiness gates"]
KOpenFirst --> KGateway["Install authenticated Gateway<br/>and wait for readiness"]
KGateway --> KOnly
KOnly --> KWait["Complete control-plane readiness gates"]
KWait --> KWorkspace
KControl -->|Compose| KConfig["Validate Compose and claim<br/>private state with snapshot"]
KConfig --> KStart["Bootstrap OCC and create<br/>the owned k3d cluster"]
KStart --> KReady["Import runtime and start<br/>API and Kubernetes worker"]
KReady --> KSandbox{"Sandbox profile"}
KSandbox -->|none| KProof
KSandbox -->|OpenShell| KOpenShell["Install pinned Agent Sandbox, RuntimeClass,<br/>Gateway, and render workspace resources"]
KOpenShell -->|Compose| KRouting["Install private Envoy route and project<br/>its service key and public CA"]
KRouting --> KWorkspace["Driver applies workspace resources and creates<br/>the default Namespace's owned Workspace"]
KOpenShell -->|Kubernetes| KWorkspace
KWorkspace --> KFailClosed["Default Namespace ready;<br/>Agent projection remains fail closed"]
KFailClosed --> KProof
KProof["Prove authenticated<br/>Installation access"]
KProof --> KDown["./bin/occ dev down reuses<br/>recorded endpoint and project"]
KStart -->|failure| KRollback["Roll back owned resources<br/>retain state if cleanup fails"]
KReady -->|failure| KRollback
KProof -->|failure| KRollback
KDown --> KRemove["Stop reconcilers and delete<br/>owned cluster and volumes"]
KRemove -->|success| KDone["Remove private state"]
KRemove -->|failure| KRetain["Keep state for recovery"]
B --> C["Select quickstart runtime image or validate custom images"]
C --> D["Selected Compose starts PostgreSQL, migrate, bootstrap, API, and worker"]
D --> E["Copy bootstrap service-key response to private local file"]
E --> F["./bin/occ installation get proves authenticated access"]
F --> G["Operator creates Namespace through authenticated API"]
G --> H["Worker claims durable Namespace operation"]
H --> I["Docker Driver ensures owned tenant network"]
I --> J["Namespace becomes ready"]
F --> K["Operator requests Agent deployment on Docker Compute"]
K --> L["Admission rejects missing or unsupported harness binding"]
J --> M["Authorized deletion removes owned Namespace resources"]
D -->|operator cleanup| CDown["occ dev down keeps<br/>selected engine connection"]
M --> CDown
CDown --> CVolumes{"--volumes?"}
CVolumes -->|No| CKeep["Remove Compose containers and network;<br/>retain named volumes"]
CVolumes -->|Yes| CDelete["Remove Compose containers,<br/>network and named volumes"]Execution Trace
1. Start and initialize the local stack
internal/occcli/dev.go:developmentCommand,
internal/occdev/egress_k3d.go:CheckDevelopmentProfile, scripts/dev-up
internal/occcli/dev.go:developmentRepository selects the nearest enclosing
checkout with an OCE module declaration and a regular compose.yaml file,
including a symlink to one. An absent Compose marker allows the ancestor search
to continue. A present marker that is not a regular file or cannot be resolved
stops both up and down with its exact path before profile dispatch. CRLF in
go.mod also stops both commands with an LF repair error. These invalid markers
cannot fall through to an outer checkout and target its stack or volumes.
Before Compute dispatch, the CLI checks protected-egress, database, and host-gateway
selectors through the existing development validators. Direct Docker script startup
calls the same check through occ dev check-profile before temporary-state creation
or engine probes; Kubernetes script startup dispatches to the guarded CLI.
Invalid or incompatible selections stop startup. Unset or 0 protected egress
preserves ordinary startup when its related selectors are absent.
Both shell wrappers locate the checkout with command-local CDPATH="" cd,
so shell search paths cannot change the selected repository or contaminate the
captured path; the delegated CLI retains the caller's original CDPATH.
Docker or Podman Compose startup owns engine selection, database initialization, local API admission and worker startup.
2. Prepare Namespace infrastructure and enforce Agent admission
apps/controller/src/drivers/compute/docker/index.ts:DockerComputeDriver
Namespace execution and Agent admission
traces API authorization, queued controller operations, tenant network ownership, unsupported
Agent authentication and exact resource removal.
The Driver rejects Docker API responses above 4 MiB, by declared length or
bytes received, before buffering the body.
For in-flight aborts, DockerComputeDriver.request preserves the error and adds
request diagnostics
to its message. Preflight cancellation remains unchanged.
3. Clean up Docker or Podman Compose
internal/occdev/down.go:Down, internal/occdev/down.go:podmanComposeArgs
occ dev down uses the selected engine and forwarded Compose project and file
options, including after partial startup. Podman keeps the caller's connection;
its reported API socket sets OCC_CONTAINER_ENGINE_SOCKET only to resolve the
worker mount, never replacing the host connection with a macOS VM socket. An
unavailable engine or invalid socket fails cleanup.
Compose removes its project containers and network. With --volumes, it also
removes project volumes. Down leaves local service-key copies and Docker Compute Namespace
networks in place because the Compose project does not own them. See
safe development shutdown.
4. Start and clean up Kubernetes development
internal/occdev/up.go:Up, internal/occdev/down.go:Down.
The Kubernetes startup and cleanup trace follows profile selection, the private Compose snapshot, k3d creation, runtime import, authenticated readiness, and cleanup through the recorded engine.
For the Kubernetes-only profile, internal/occdev/openshell_k3d.go:upK3d
creates the owned cluster, checks node DNS, and writes and selects its kubeconfig.
With Sandbox Driver none, it then imports the runtime image and runs
prepareDevelopmentCodexSandbox before installing private routing controllers.
Meanwhile, internal/occdev/concurrent.go:concurrently imports the controller
image, then checks network isolation while importing PostgreSQL and, when
configured, repository images. The first independent failure cancels the others
and is reported. A sandbox refusal or failed check rolls back the owned cluster
before Installation setup or credential delivery. Both paths install
the platform only after the check, then gate on authenticated readiness.
internal/occdev/kubernetes.go:importDevelopmentImage exports each selected
image to its own temporary archive, with an explicit Linux platform on Docker.
internal/occdev/k3d_import.go:importArchiveDirect streams it into
k3d, retrying only its known closed-stream race, three times at most. The launcher
resolves the imported digest from containerd, rejects missing or ambiguous
results, and removes the archive. Tagged builds share this path.
internal/occdev/registry_k3d.go:pullDevelopmentImage pulls digest-pinned
OCC_DEVELOPMENT_IMAGE_REGISTRY_CONTAINER images instead; state.json records
the registry attachment for occ dev down to detach. Kubernetes-only uses its
owned k3d network; Compose uses its exact recorded Compose network and selects
only a runtime image with the current checkout label. Both detach after imports
and on rollback, including recovery after interrupted startup.
State rewrites set mode 0600 before writing so recovery can read
state.json under a restrictive umask.
For Kubernetes-only OpenShell, runtime import follows PostgreSQL/repository imports
in their existing concurrent branch. OpenShell asset preparation follows those
imports while network probes finish. This installs the Agent Sandbox controller
and CRDs and imports images; platform installation and credential delivery still
wait for every branch, including successful network verification. Image builds
remain serial, and the large runtime import precedes smaller OpenShell assets.
5. Prepare the optional OpenShell development profile
internal/occdev/openshell_k3d.go:upK3d,
internal/occdev/openshell.go:prepareOpenShell,
apps/controller/src/drivers/sandbox/openshell.ts:ensureNamespace
With OCC_DEVELOPMENT_CONTROL_PLANE=kubernetes, OpenShell uses the
Kubernetes-only lifecycle before Compose rendering. It pins K3s and OpenShell
inputs, installs PostgreSQL, OCE, and one central Gateway in oce-system, and
publishes only an admitted API proxy on host loopback. The source archive and all three
OpenShell images select 0.1.8 together, including the paired sandbox/supervisor
control protocol and compromised-key rejection. The observed-hook repository
credential preview admits 0.1.6 through its existing opt-in, held-launcher and
recipient checks. Repository-scoped access uses the separate
protected Git enrollment.
With OCC_DEVELOPMENT_CONTROL_PLANE=compose, the Kubernetes lifecycle starts PostgreSQL, migration, bootstrap,
the API, and the worker in Compose. It installs the central Gateway in
openshell-system and exposes its fixed NodePort only to the owned container
network. Before starting the API and worker, the launcher installs pinned
cert-manager and Envoy Gateway controllers, renders only the Helm-owned private
routing resources, and copies the generated public CA and a dedicated service
key into the private state directory. The Installation uses the k3d node name
and Envoy HTTPS NodePort. After Compose starts, the launcher reapplies Envoy
ingress with the controller, Kubernetes worker, and k3d node /32 addresses.
In both modes, the worker
creates the bootstrap Namespace through the regular Compute workflow. The
Sandbox Driver applies rendered workspace resources and the operator label
before creating the corresponding Gateway Workspace. Startup waits until the
OCC Namespace becomes ready. See the
OpenShell flow.
6. Start authenticated OpenShell transport
internal/occdev/egress_k3d.go:protectedEgressSelection,
internal/occdev/openshell_k3d.go:installDevelopmentPlatform,
apps/controller/src/drivers/sandbox/openshell-gateway-client.ts:createClient
internal/occdev/openshell.go:installOpenShellGateway enables native PKI,
TLS, mTLS and authentication for every Kubernetes-only OpenShell installation.
openShellBackendConfiguration selects HTTPS and client certificates;
installKubernetesControlPlane projects them into the API and worker through Helm.
The default profile registers no static-inference middleware and waits for the
Gateway before installing OCE. It needs no corrective Gateway upgrade or Pod deletion.
New protected static-inference launches are retired.
CheckDevelopmentProfile rejects OCC_DEVELOPMENT_PROTECTED_EGRESS=1 before
Compute dispatch, including the former Kubernetes/OpenShell selection. Direct
Kubernetes startup also checks protectedEgressSelection before provisioning.
Any presence of OCC_DEVELOPMENT_OPENSHELL_HOST_GATEWAY_IP, including an empty
value, is rejected. Unset, empty and 0 protected-egress selections retain ordinary
startup; malformed values still fail validation. Refusal does not select a fallback.
For an existing installation, keep the recorded state and ownership evidence for cleanup. Legacy protected fields remain readable in cleanup receipts but do not configure startup.
The chart projects the administrative client certificate into API and worker only. The Backend supplies it to verified gRPC TLS for DNS or exact-IP server identities. Service traffic keeps its separate scoped bearer identity. For separately prepared production installations with an authority, the API alone receives authority TLS and the issuer public key; selected Gateway and supervisor Pods can reach that listener through narrow NetworkPolicies. See the Backend authentication boundary.
selectedDevelopmentDatabase validates an optional disposable database name
before effects. installKubernetesControlPlane uses that name in creation, readiness,
migrator and application URLs. Bootstrap, API and worker share the limited
application-role URL. PostgreSQL starts while the bootstrap volume is prepared;
Helm waits for both. Unselected profiles retain the existing database and
transport defaults. The retirement notice
describes accepted selectors.
Debugging and Verification
./scripts/dev-upshould show PostgreSQL readiness, migration completion, API listening on127.0.0.1:${OPENCLAW_DEV_PORT:-3000}, fresh-database initialization,worker.startedwithcomputeDriverIdset tocompute-docker-development, a private copied service-key path, and a successful authenticated/installationproof.- With
OCC_DEVELOPMENT_COMPUTE_DRIVER=kubernetes, startup should instead report Kubernetes Compute, a private kubeconfig, and the disposable k3d context; it does not mount the engine socket into the Kubernetes worker. - With
OCC_DEVELOPMENT_SANDBOX_DRIVER=openshellandOCC_DEVELOPMENT_CONTROL_PLANE=kubernetes, startup should also report Kubernetes-only deployment, one readyoce-system/openshell-gatewayService, theopenshell-sandboxRuntimeClass, the Agent Sandbox CRD, and workspace resources in the bootstrap Namespace. The selected real dev-up case also reads the owned Workspace through the Gateway API. This proves infrastructure readiness, not a model turn. The expected Agent result is the explicit unsupportedsecretKeyRefprojection failure with no Sandbox or Agent Pod. - With
OCC_DEVELOPMENT_CONTROL_PLANE=compose, OpenShell startup should reportControl plane: Compose, retain a private Compose snapshot, install the Gateway inopenshell-system, programoce-system/openclaw-enterprise-agent-gateways, and create the bootstrap Namespace's operator-mode Workspace.installation.yamlmust contain the Envoy NodePort endpoint, while only the controller andworker-kubernetesreceivegateway-api-keyandgateway-ca.crtmounts. - Docker Compute on Podman startup verification should show Podman as the
selected engine, mount only its reported API socket into the worker, and
complete the same authenticated Installation proof without a
dockeralias. The startup flow documents the macOS prerequisite. <engine> network ls --filter label=org.openclaw.enterprise.compute-driver=dockershould show the owned network for a ready development Namespace.- Docker Compute Agent deployment must reject a missing or unsupported harness binding before
workload creation. A worker
OPENAI_API_KEYcannot make it supported. - Retained Docker/Podman model suites currently cannot pass through this admission boundary. See Docker test status; old model-turn evidence does not establish current support.
- Namespace deletion removes its owned resources while preserving unrelated ones.
- The real Compose cleanup case verifies the compiled CLI against a partially started project, including volume retention and explicit deletion on the selected engine connection.
