Configuration persistence and Agent revision snapshots
Trace how OCC reads a Configuration and saves its values in an Agent revision. See the parent flow for its context and overall sequence.
Execution trace
4. Lock metadata and invoke the selected Configuration Driver
apps/controller/src/drivers/configuration/kubernetes/index.ts:KubernetesConfigurationDriver.create
Configuration ownership, immutable kind: "agent", generation, creation time,
normalized Secret bindings, and server-generated cfg_ identity are persisted by
PostgreSQL platform state;
the live configuration document is not duplicated into Configuration metadata.
OCC owns API-level native Configuration validation before it calls the selected
bundled or installed Configuration Driver: model provider settings,
requireDeployableRoster in
the roster rules, which refuses
an agents roster, a model or fallback that is not <provider>/<model>
(OpenClaw's openrouter:auto included), a fallback on another provider than
its primary's, a models.providers.<id>.models entry that names no selected
model, or model and runtime settings that conflict (requireConsistentModelPolicy,
which runs the deployment resolver itself, in its order, and leaves a draft
without a model or runtime to deployment), all of which every deployment refuses,
with deployment's own text, and the settings rules.
These rules reject unsupported Codex approval policies, a literal
gateway.auth.password or a password SecretRef OpenClaw cannot read
(gatewayPasswordRequirement), and retired settings such as security.preset.
OCC also calls the selected Compute Driver's optional
validateConfiguration. On
Kubernetes, this check refuses unsupported channels, model credentials outside the Harness
authentication binding, and model selections or provider apiKeys off the
primary model's provider. Configuration create and update, Agent provisioning and
Preset save run this one set (validateConfigurationContent in
OCC); a Preset value that waits on a
variable is checked as far as its written text goes, and in full once filled.
Preset save skips the catalog and runtime checks; its launch's Configuration runs them.
Create, update and provisioning first ask the Driver's optional
validateSize
to refuse a document too large to store, so no content rule walks it. Its
ConfigurationSizeError returns 400 INVALID_REQUEST naming the limit, not the
generic invalid-configuration text.
A stored row that predates a rule still reads and an edit can replace it: a
bundled Driver checks a stored document only against
the credential-reference rules in force before 2026-10-12, and every write and
deployment against all of them. Those include a ${NAME} reference or SecretRef
in every slot OpenClaw marks sensitive, channel and plugin secrets included
(list), and in a
plugin secret input also OpenClaw's whole $NAME shorthand, checked in
the credential rules,
which a Preset save runs too. The Driver owns backing document
storage and checks storage-specific identity and ownership. When the bundled
Kubernetes implementation is selected, KubernetesConfigurationDriver in
the Kubernetes Configuration implementation
validates its storage envelope, discovers its exact tenant-labeled Kubernetes
namespace using the existing cluster-scoped namespace-observer get/list
grant, and derives its deterministic ConfigMap name. For an externally owned
namespace explicitly selected by existingNamespace, the Compute worker first
binds its tenant identity during provisioning; Configuration creation returns
409 until that explicitly external platform Namespace is ready. It then
checks object
ownership labels, kind/generation annotations, and creation time. Each ConfigMap
has exactly one data entry: openclaw.json, containing the serialized native
configuration document and its unresolved inline SecretRefs. Kubernetes driver
reads reject malformed stored objects, extra entries, and oversized documents.
Create calls
createNamespacedConfigMap, read calls readNamespacedConfigMap, replace calls
replaceNamespacedConfigMap with the observed resourceVersion, and delete
calls deleteNamespacedConfigMap with the observed object identity when
available. Creation starts at generation 1; each successful PATCH replaces
the entire native document and advances the generation exactly once. Metadata
and ConfigMap updates share OCC's existing transactional/compensating boundary;
create, update and delete register their compensation before the write, so a write
that applied but answered an error is undone as well. A failed create deletes only
the exact Configuration inspectExact finds, a failed delete recreates the previous
one only when it is gone, and a failed update restores the stored generation. When
that check cannot read the backend, the request fails as a rollback failure (503).
A Driver without inspectExact, such as the filesystem development Driver, compensates
only a write it saw succeed. Compensations run after the failed transaction released
its row locks, so delete and update compensations first re-read the metadata row and
act only while it is unchanged: a Configuration another request deleted or updated
in the meantime is left as that request committed it. If that re-read fails, the
compensation proceeds as before. A write still in flight, or
another change that lands between that re-read and the compensation, is not covered.
Each Kubernetes call has its own 10-second limit. A Configuration or Secret create,
update or delete gives all of its Kubernetes calls 12 seconds from the start of its API
handler (apps/controller/src/admission/mutation-deadline.ts:withMutationDeadline;
finding 3106). A call gets only the time left. A write is sent only with at least 2 seconds
left, and a read is retried only when its pause and 2 seconds fit. Otherwise nothing more is
sent: the request fails with 503 DEPENDENCY_UNAVAILABLE, or with the error a sent read
already got. The audit append and the commit run after the budget and are not bounded by
it. Compensations, and the cleanup of a Secret create that was sent, also run after it,
each call with its own 10 seconds.
A referenced Configuration cannot be deleted. Tenant child-data access remains
limited to namespaced ConfigMap create, get, update, and delete;
Kubernetes cannot restrict create by resourceNames, so that verb must use
its own namespaced Role rule. ConfigMap list/watch, Secrets, Pods, and
cluster-wide tenant-resource access remain unavailable.
Startup validates the bundled Kubernetes Configuration Driver's authentication
configuration but does not probe tenant ConfigMaps or ConfigMap RBAC. Its first
exact CRUD call checks actual tenant namespace existence and authorization; a
driver-managed provisioning Namespace can return 503 until its Kubernetes
namespace and API RoleBinding are ready. Explicitly selected external Namespaces
instead reject Configuration creation with 409 before readiness; after
readiness, a missing API RoleBinding returns 503. The development filesystem
Driver persists OCC-validated
documents under the configured root and checks only server-generated file
ownership. Installed implementations validate and use their own backing-storage
prerequisites.
5. Resolve the Agent reference and save the deployment configuration
packages/occ/src/index.ts:OpenClawController.deployAgent
createAgent and updateAgent in
OCC separately authorize and verify the
Agent's exact same-Namespace configurationId and ensure its Configuration kind
is "agent". They also run deployment's checks of the Agent against that
Configuration (Agent save):
the Sandbox execution mode, the Harness and its mode, and the Compute checks.
Agent save and deployment also refuse a stored Configuration that still contains
security.preset. The operator can read it and replace the field with
explicit settings.
With a Compute Driver and a Harness resolver selected, every Agent save reads
its Configuration once from the Configuration Driver, inside the save's
transaction; a Driver outage then fails the save with 503. deployAgent then locks the Agent, authorizes a read of the
referenced Configuration, and locks its ownership metadata. It authorizes the
caller's selected Secret bindings and the Agent service principal's consumption,
checks topology/model-source compatibility, and asks the selected Secret Driver
to verify each backend reference. This resolves backend identity, not credential
bytes or inline SecretRefs.
Next OCC reads the selected Configuration Driver's stored document. For the
bundled Kubernetes Driver, that document is the openclaw.json ConfigMap entry.
When the selected Sandbox Driver exposes configureAgent, OCC transforms a
frozen copy before Configuration Driver validation and Harness selection. The
stored reusable Configuration and its generation remain unchanged.
admitGatewaySecurityDefaults
then fills the Gateway security defaults
on the copy. It preserves explicit settings. OCE refuses an explicit OpenClaw
sandbox mode other than "off" or a tools.exec.host of "sandbox" at save and
deployment because OCE provides the Agent's sandbox. Compute then
checks the Harness authentication binding (Kubernetes Compute also runs the shared
requireOpenClawRoster, the dedicated OpenClaw and Codex main Agent rules, and
the rules
preparation and the Gateway entrypoint apply after a dedicated deployment stops its
predecessors: enabled channels need a dedicated workload and runtime.channels, a
Gateway hosting a workspace node needs the file-transfer plugin, and a Dedicated Codex
Gateway needs its codex plugin left on) and, through optional
validateGatewaySettings, the native gateway settings: Kubernetes and Docker Compute
refuse a setting every preparation would refuse with 409, naming the setting and never
its value, before an Agent revision exists. Kubernetes and Docker Compute also refuse
listeners that routed traffic cannot reach, which the local readiness check would
pass: a gateway.bind other than auto, lan or custom, custom unless
customBindHost is 0.0.0.0, and any gateway.tailscale.mode but off.
Kubernetes renders lan for an omitted or auto bind; an Agent revision prepared before
that keeps its immutable gateway document, and maintenance accepts exactly that
earlier rendering, until the Agent is deployed again. Every Compute Driver refuses native TLS and
a Codex approval policy outside never, on-request and on-failure this way;
Kubernetes also refuses a gateway.auth.password other than the
OPENCLAW_GATEWAY_PASSWORD reference. The selected PluginDriver's
optional validateAgentConfiguration then checks plugin selections against the
admitted values. OCC then applies
withAgentIdentityName, which
sets the Agent name as the sole roster entry's identity.name unless the
Configuration names it (rules).
No check above reads that setting. OCC freezes
the result, including any remaining inline unresolved SecretRefs, into
AgentRevision.configuration; separate configurationId, configurationKind,
and configurationGeneration fields pin the selected Configuration metadata.
The Agent revision also pins the selected OpenClaw or Codex Harness descriptor,
execution mode, Compute identity, optional Sandbox identity, Agent service
principal, and any selected Secret Driver identity and normalized bindings.
Backend locators and value bytes are not stored in the Agent revision. Subsequent nested
Configuration edits increment its generation but affect only later explicit
deployments; historical Agent revisions and their admitted snapshots remain unchanged.
PostgreSQL enforces the supported kind, positive generation, exact admitted
snapshot shape, ownership, and Agent revision immutability. OCC records an
attributable sanitized admission event and hands the Agent revision to
Compute-owned reconciliation without resolving credential bytes or starting a
gateway during HTTP admission. A development worker can subsequently create or
reuse exactly one gateway for the owning Agent during Agent revision preparation.
When Kubernetes Compute owns that gateway, it creates a distinct immutable,
Agent-owned ConfigMap from the admitted configuration and mounts it
read-only at OPENCLAW_CONFIG_PATH=/etc/openclaw/openclaw.json. The gateway
wrapper (GATEWAY_RUNTIME_ENTRYPOINT in kubernetes/runtime-entrypoints.ts)
starts OpenClaw with OPENCLAW_CONFIG_READONLY=1 whenever that file's directory
is not writable, so OpenClaw skips its last-known-good backup. It never mounts
the mutable Configuration Driver ConfigMap; each changed admitted generation
selects a new immutable snapshot and rolls the stable Agent gateway. Previous
snapshots remain until Namespace deletion because earlier Pods may still mount
them; safe earlier cleanup is deferred. Production workers also reconcile
embedded OpenClaw and dedicated Codex Agent revisions, connecting each
Agent-owned gateway only to its own active workload.
Guarded first admission
apps/controller/src/http/agents.ts:createAgentHandlers handles deployAgentFirst and
recoverAgentFirstDeployment. The POST accepts an intent; the GET carries the same
key, reviewed Configuration ID, and generation in path parameters. Both use the
authenticated caller. The deployment reference
owns the contract; the Agent revisions guide
owns operator commands.
graph TD
A["Capture original request<br/>and authorize receipt access"] --> B{"Receipt exists?"}
B -->|yes| R["Return original receipt<br/>without new work"]
B -->|no, recovery GET| N["Return null<br/>outcome remains uncertain"]
B -->|no, deployment POST| C{"Active and stopped Agent,<br/>no history, reviewed Configuration?"}
C -->|no| X["Refuse fresh admission"]
C -->|yes| D["Prepare channels<br/>outside transaction"]
D --> E["Lock Agent, reauthorize,<br/>look up receipt again"]
E -->|match| R
E -->|miss| F{"Preparation and current<br/>admission checks pass?"}
F -->|no| X
F -->|yes| G["Commit revision 1,<br/>receipt digest, and work"]packages/occ/src/index.ts:OpenClawController.deployAgentFirst and
OpenClawController.recoverAgentFirstDeployment capture the Namespace, Agent, key, Configuration ID, and
generation on entry. They use those values for the digest and each permission check.
The first admission read checks exact receipt authority through
authorizeFirstDeploymentReceipt, then reads
the receipt, matching draft metadata, and history in one snapshot. PostgreSQL uses
REPEATABLE READ. On a receipt miss, an Agent without active lifecycle status
returns 409 AGENT_DELETING before channel preparation. A changed Agent binding
refuses fresh admission without reading or
authorizing a different Configuration. A stale tuple, existing history, or read error
ends the request from that snapshot. Only preparation after a valid fresh tuple can
fall back to locked recovery.
prepareDeploymentChannels shares ordinary deployment's Configuration and Channel
validation outside the transaction. It captures the Namespace, Agent, and reviewed
Configuration IDs before asynchronous work. validateChannelCredentials enforces
exact Secret scope. Before withValue reads the backend and again before its value
callback dispatches to the provider, OCC rechecks exact Agent deploy, reviewed
Configuration read, and Secret operate. Revocation at either boundary prevents
the next credential operation. Ordinary deployment uses the same checks and
credential reader. Concurrent fresh submissions can repeat preparation.
The private admitAgentDeployment reuses ordinary admission's Namespace and Agent
locks. Under the Agent lock it rechecks current exact receipt permissions and performs
one receipt lookup, before fresh-only dependencies or a saved preparation error can
refuse the request. A match returns the minimal receipt without decoding the revision
or calling Secrets, providers, models, or work. A contender may already have performed
preparation before this lookup; observing a match adds no fresh work. A different actor
cannot match. A match remains recoverable after Agent deletion starts.
On a locked miss, admission checks active lifecycle status before propagating a
saved preparation error; deletion returns 409 AGENT_DELETING. It then checks stopped desired state, empty history, binding,
generation, and equality with the validated Configuration. It rechecks Secret and
other ordinary admission permissions. assertPreparedDeploymentDrivers checks the
actual selected Configuration and Channel instances after locking and immediately
before revision insertion: database locks do not freeze process-local selections.
Any drift refuses admission and queues no work.
packages/occ/src/state/postgres-state.ts stores a SHA-256 digest of the versioned operation,
Installation, actor, Namespace, Agent, key, and expected Configuration tuple in
agent_revisions.first_deployment_digest. The digest commits with revision 1 and
its reconcile work. It is separate from the admitted snapshot and is never projected
to workers or normal revision responses. The GET reads the original minimal receipt
without consulting the Agent's current Configuration binding or admitting work.
Absence is an observation, not a cancellation or proof of failure. Conflict
responses direct the operator to recover the original receipt after uncertainty,
then inspect current binding, Configuration, or deployment state. Existing history
requires a deliberate later deployment through ordinary deploy, not a blind retry.
internal/occcli/cli.go:application.firstDeploymentRecoveryNotice preserves the
original POST error and prints a read-only recovery command for an uncertain
transport result, invalid receipt, HTTP 408, or 5xx. A typed network dial
failure omits this hint. internal/occclient/client.go:validateFirstDeploymentReceipt
validates the receipt's
six fields and matches its Namespace, Agent, Configuration, and generation to the
request for POST and non-null recovery GET replies. A null GET remains a receipt
miss. The command retains the original request and connection options; the
service-key file must still identify the original caller. The helper sends no
request.
