diff --git a/rfcs/0019-managed-configuration.md b/rfcs/0019-managed-configuration.md new file mode 100644 index 00000000..52cef22e --- /dev/null +++ b/rfcs/0019-managed-configuration.md @@ -0,0 +1,596 @@ +--- +title: Managed Configuration +authors: + - Gio Lodi +created: 2026-07-10 +last_updated: 2026-07-28 +status: draft +issue: +rfc_pr: https://github.com/openclaw/rfcs/pull/34 +--- + +# Proposal: Managed Configuration + +## Summary + +Add an opt-in way to start OpenClaw from an ordered list of ordinary +configuration documents. + +Each document is independently parsed, include-resolved, environment-resolved, +and checked for a valid object root. OpenClaw then folds the documents in +declared order and applies schema defaults and plugin-aware validation once to +the composed source. The first layer +to declare an exact path controls it; later layers may omit it or repeat the +same value, but may not replace it. A small closed set of fields can use +OpenClaw-owned monotonic rules instead. + +The first version is deliberately startup-only and read-only. It does not add a +configuration control plane, layer roles, write routing, live reload, or a +provenance API. + +The implementer-facing v1 contract is defined in +[Managed Configuration v1 Core Specification](0019/managed-configuration-v1-spec.md). +That sidecar is normative for V1; if this explanatory RFC and the sidecar +conflict, the sidecar controls. + +## Motivation + +Lobster currently needs to combine three kinds of OpenClaw configuration: + +- Scout-wide security and Gateway defaults; +- tenant-specific URLs and private-network facts; +- operator-local customization. + +Without an upstream composition seam, Lobster must bake those inputs into one +config file and maintain overlay application, stale-value cleanup, and +OpenClaw-specific validation behavior outside OpenClaw. OpenClaw sees only the +result and cannot reject a later overlay that weakens an earlier boundary. + +The problem is not unique to Lobster and does not require a new `hosting` +section. The values already belong in the ordinary OpenClaw schema. What is +missing is a generic way to compose multiple ordinary documents while +preserving declared order and rejecting conflicts. + +### Why OpenClaw owns composition semantics + +OpenClaw must own the merge semantics because OpenClaw owns the configuration +schema, plugin-aware validation, tool-policy interpretation, runtime snapshot, +and mutation boundary. Authority and monotonic-tightening decisions depend on +those semantics; a launcher cannot reproduce them reliably without becoming a +second OpenClaw configuration implementation. + +Hosts remain responsible for materializing values and declaring source order. +OpenClaw is responsible for resolving, composing, validating, and enforcing +those sources. This boundary keeps deployment vocabulary outside core while +ensuring every host receives the same conflict and security behavior. + +### Ownership and authority + +This RFC uses **ownership** for responsibility over semantics and lifecycle, +and **authority** for the value-control relationship derived from ordered +layers. A layer id is a descriptive label, not an authenticated principal or a +durable owner identity. + +| Concern | Responsible owner | Lifetime | +| --- | --- | --- | +| Field meaning, defaults, validation, and bounded comparison | The OpenClaw subsystem that owns the existing config field | The OpenClaw contract version | +| Source contents, materialization, permissions, and declared order | The invoking operator, host, Fleet, or future control plane | One process invocation | +| Authority over an authored path | The earliest declaring layer, as derived and enforced by OpenClaw | One candidate evaluation and, after acceptance, its layered activation | +| Candidate composition and admission | OpenClaw config bootstrap | One startup evaluation | +| Effective runtime snapshot | The existing Gateway config lifecycle | One full process activation, including in-process Gateway restarts | +| Canonical-path mutation exclusion | Each layered Gateway server lifecycle | Registration through server close or failed startup cleanup | +| Runtime behavior | The existing Gateway, plugin, channel, tool, and agent owners | The accepted snapshot they consume | + +The source producer chooses desired values and order but does not define what a +field means or whether one value tightens another. OpenClaw derives authority, +rejects invalid candidates, and publishes the only effective snapshot. Runtime +consumers use that snapshot; they do not recompute layering or infer roles from +layer ids. + +A full process restart creates a new layered activation and recomputes +authority from the then-current ordered sources. An in-process Gateway restart +does not create a new activation and must reuse the accepted snapshot. V1 does +not persist authority as a separate lease, generation, or configuration source. + +This also defines the future OCC boundary. OCC may own desired state, +admission, rollout, and source materialization, but it remains a caller of this +contract. It does not replace OpenClaw's ownership of field semantics, +composition, validation, or the active runtime snapshot. + +`$include` remains appropriate for structuring one authored configuration +document. It does not preserve authority between independent sources, enforce +monotonic policy bounds, or make the resulting runtime immutable. External +flattening also erases source boundaries before OpenClaw can enforce them. + +## Goals + +- Accept any positive number of explicitly ordered config documents. +- Keep layer names descriptive, with no built-in host, tenant, or operator roles. +- Apply the same operation recursively for every layer. +- Reuse JSON5, include, environment, schema, and plugin validation behavior. +- Reject exact conflicts instead of silently choosing a winner. +- Permit only proven tightening for the initial bounded tool-policy fields. +- Publish one effective startup snapshot to existing runtime consumers. +- Make the layered runtime immutable for its lifetime. +- Leave ordinary single-config startup unchanged and unaware of the feature. +- Give Lobster a supported seam that can replace baked overlay generation. + +## Non-goals + +- Implicit source discovery or numeric priorities. +- A fixed two-stage managed/operator model. +- Host-defined comparators or a policy expression language. +- Writable layers or routing config mutations back to source documents. +- Live layer reload, rollback generations, or transactional reconciliation. +- A per-path provenance or explanation API in V1. +- Secret delivery, identity, state synchronization, or plugin installation. +- Moving canonical settings into a parallel hosted-config schema. +- Multi-tenant isolation inside one Gateway. Mutually untrusted tenants still + require separate Gateway processes and state boundaries. + +## Interface + +The Gateway CLI accepts a repeatable option: + +```bash +openclaw gateway run \ + --config-layer global=./global.json5 \ + --config-layer tenant=./tenant.json5 \ + --config-layer operator=./operator.json5 +``` + +The syntax is: + +```text +--config-layer +``` + +Order is the command-line declaration order. IDs must be non-empty and unique, +but otherwise have no semantics. The examples use deployment vocabulary only +to make the source of each document understandable. + +When no `--config-layer` option is present, OpenClaw follows its existing +single-config path with no behavior change. + +Layered startup is incompatible with `--dev`, because dev startup creates and +mutates configuration. + +## Composition model + +Each source is an ordinary sparse OpenClaw config document. For each source, +OpenClaw: + +1. reads JSON5; +2. resolves includes using the normal include roots and the source file location; +3. resolves environment references; +4. requires a plain-object root; +5. rejects bootstrap-owned `meta` and `env` root keys. + +After every source resolves, OpenClaw folds them in declared order: + +```text +state[0] = empty +state[i + 1] = compose(state[i], layer[i]) +effective = validate(state[layerCount]) +``` + +```mermaid +flowchart LR + A[Ordered id=path arguments] --> B[Resolve each ordinary config document] + B --> C[Recursive authority fold] + C --> D[Validate schema and plugins] + D --> E[Publish one startup snapshot] + B -. parse or include failure .-> X[Reject startup] + C -. authority conflict .-> X + D -. invalid config .-> X +``` + +```mermaid +flowchart TD + S0[Empty state] --> L0[Compose layer 0] + L0 --> S1[Authority state 1] + S1 --> L1[Compose layer 1] + L1 --> S2[Authority state 2] + S2 --> LN[Repeat for layer n] + LN --> E[Effective sparse source] +``` + +Objects compose recursively. Declaring one child does not claim unrelated +siblings. Arrays are whole-field values unless the field has a built-in bounded +rule. Empty objects preserve ownership of that object boundary. + +The composed source is validated once through the ordinary OpenClaw schema and +plugin-aware validator. Gateway, plugins, tools, and other consumers receive +the normal runtime config shape; they do not implement layer-specific logic. + +## Authority rules + +### Exact authority + +Exact authority is the default. + +The earliest layer declaring a path controls that path for the current +candidate and accepted activation. A later layer may: + +- omit the path; +- repeat the same authored value. + +A later layer may not provide a different value. OpenClaw rejects the complete +candidate with a `ControlledByEarlierLayer` finding. + +```json +{ + "reason": "ControlledByEarlierLayer", + "layer": "operator", + "path": "gateway.controlUi.allowedOrigins", + "controllingLayer": "tenant" +} +``` + +There is no silent managed-wins or last-writer-wins behavior. + +### Bounded tool policy + +V1 has two built-in bounded paths: + +| Path | Rule | +| --- | --- | +| `tools.allow` | A later layer may only narrow the effective allow policy | +| `tools.deny` | A later layer may only broaden the effective deny policy | + +The comparison uses OpenClaw's runtime tool-policy semantics, including groups, +wildcards, and the meaning of an empty allow list. Ambiguous +expression-to-expression comparisons fail closed unless containment is proven. + +A weakening attempt produces `WouldWeakenEarlierLayer`. + +No other field receives a comparator in V1. Additional comparators require +field-specific semantics, tests, and demonstrated demand. + +## Runtime lifecycle + +Layered configuration is a startup input, not a second live config store. + +When layered mode is active: + +- the composed snapshot is reused for the server lifetime; +- config hot reload is disabled; +- config-mutating RPCs are rejected; +- agent create, update, and delete are rejected before workspace side effects; +- config persistence targeting the layered Gateway's canonical path rejects + writes; +- a source change takes effect only after a full Gateway process restart or + replacement. + +Read surfaces use the composed snapshot where they would otherwise reread the +canonical config file. + +The write guard is owned by the Gateway server lifecycle and scoped to the +canonical config path. Overlapping owners for that path compose safely, while +an unrelated config path remains writable. Closing one layered server removes +only its own guard. Pathless mutation preflights resolve to the canonical path +so plugin or repair side effects cannot occur before persistence is rejected. + +This read-only boundary avoids partial write semantics and keeps V1 small. A +future writable-layer design would need explicit source ownership, conflict +detection, atomic persistence, reload, and recovery guarantees and is not +implied by this RFC. + +## Lobster example + +The following is a realistic three-file Scout deployment. The names are not +special to OpenClaw. + +### 1. Scout global config + +```json5 +// scout-global.json5 +{ + gateway: { + mode: "local", + auth: { + mode: "trusted-proxy", + trustedProxy: { + userHeader: "x-scout-user", + requiredHeaders: ["x-scout-tenant"], + }, + }, + controlUi: { + dangerouslyAllowHostHeaderOriginFallback: false, + allowInsecureAuth: false, + dangerouslyDisableDeviceAuth: false, + }, + }, + tools: { + deny: ["exec"], + }, +} +``` + +### 2. Tenant network config + +```json5 +// tenant-network.json5 +{ + gateway: { + bind: "tailnet", + trustedProxies: ["100.96.0.0/12"], + tailscale: { + mode: "serve", + serviceName: "svc:openclaw-acme", + }, + controlUi: { + allowedOrigins: ["https://openclaw.acme.internal"], + }, + }, + tools: { + deny: ["exec", "web"], + }, +} +``` + +### 3. Operator config + +```json5 +// operator.json5 +{ + gateway: { + controlUi: { + enabled: true, + }, + }, + logging: { + level: "info", + }, +} +``` + +Lobster starts OpenClaw with the three explicit sources: + +```bash +openclaw gateway run \ + --config-layer scout=./scout-global.json5 \ + --config-layer tenant=./tenant-network.json5 \ + --config-layer operator=./operator.json5 +``` + +```mermaid +flowchart LR + S[Scout global
security baseline] --> F[Ordered recursive fold] + T[Tenant network
URL, proxy, Tailnet] --> F + O[Operator
local customization] --> F + F --> V[OpenClaw validation] + V --> R[One immutable runtime config] +``` + +The effective runtime shape is the same ordinary config shape OpenClaw already +consumes: + +```json5 +{ + gateway: { + mode: "local", + bind: "tailnet", + auth: { + mode: "trusted-proxy", + trustedProxy: { + userHeader: "x-scout-user", + requiredHeaders: ["x-scout-tenant"], + }, + }, + trustedProxies: ["100.96.0.0/12"], + tailscale: { + mode: "serve", + serviceName: "svc:openclaw-acme", + }, + controlUi: { + enabled: true, + allowedOrigins: ["https://openclaw.acme.internal"], + dangerouslyAllowHostHeaderOriginFallback: false, + allowInsecureAuth: false, + dangerouslyDisableDeviceAuth: false, + }, + }, + tools: { + deny: ["exec", "web"], + }, + logging: { + level: "info", + }, +} +``` + +If the operator document also declares a different +`gateway.controlUi.allowedOrigins`, startup is rejected because the tenant +layer declared that exact path first. If it removes `web` from +`tools.deny`, startup is rejected because that would weaken the inherited +deny floor. + +This lets Lobster materialize three source files without teaching OpenClaw +about Scout, tenants, or operators. After an OpenClaw release contains the +feature, Lobster can remove the baked effective-config overlay and its +stale-value cleanup. + +## Fleet and multi-tenant hosting + +Managed configuration complements OpenClaw Fleet's cell isolation model. Fleet +continues to run one complete Gateway cell per tenant trust boundary, with +separate state, credentials, workspace, network, and Gateway token. Layers do +not turn one shared Gateway into a tenant authorization boundary. + +For each cell, the host can reuse one shared baseline and append only the +documents applicable to that tenant and cell: + +```mermaid +flowchart LR + G[Shared global baseline] --> A[Compose Acme cell] + TA[Acme tenant config] --> A + OA[Acme operator config] --> A + A --> CA[Acme Gateway cell] + + G --> B[Compose Contoso cell] + TB[Contoso tenant config] --> B + OB[Contoso operator config] --> B + B --> CB[Contoso Gateway cell] +``` + +Conceptually, Fleet or another host supervisor starts each isolated cell with +its own ordered arguments: + +```text +Acme: global.json5, tenant-acme.json5, operator-acme.json5 +Contoso: global.json5, tenant-contoso.json5, operator-contoso.json5 +``` + +The global document can establish security and operational boundaries. The +tenant document can add that cell's URLs, private-network settings, channel or +provider configuration, and other tenant facts. An optional operator document +can add local choices or tighten bounded policy. OpenClaw applies the same +generic composition rules independently inside each cell. + +The V1 Gateway feature does not add or change Fleet commands. A later Fleet +integration only needs to mount the applicable documents into each cell, pass +the repeated `--config-layer` arguments, and replace or fully restart the +Gateway process when those inputs change. Cells that rely on interactive in-cell configuration +should continue using ordinary mutable config instead of opting into layered +mode. + +This preserves the ownership boundary: Fleet owns cell lifecycle, isolation, +mounts, source materialization, and source order; the Gateway owns config +resolution, composition, validation, authority, and runtime enforcement. A +host must not place multiple mutually untrusted tenant documents into one +Gateway stack. + +## Evidence + +The design was developed through the FACES loop: + +- frame the host problem and deletion target; +- audit the existing OpenClaw config and lifecycle boundaries; +- compare recursive and layered patterns inside OpenClaw; +- evaluate the contract with maintainer, security, host, operator, and testing + roles; +- build a broad fork prototype, then reduce it to the smallest supported slice. + +Evidence available during RFC review: + +- broad fork prototype: https://github.com/giodl73-repo/openclaw/pull/33 +- simplified upstream draft implementation: + https://github.com/openclaw/openclaw/pull/107026 +- a Lobster fork adapter demonstrates materializing Scout, tenant, and operator + documents and passing them as repeated flags; its production lifecycle and + rollout proof remain adoption work; +- 63 focused tests on the recorded implementation heads cover recursive composition, exact conflicts, + bounded tool policies, config loading, immutable write ownership, and early + agent-mutation rejection; +- a foreground lifecycle proof demonstrates successful three-layer startup, + composed config reads, rejected runtime mutation, rejected conflicting + startup, and no canonical config write; +- fresh-state and existing-state Gateway proofs demonstrate that layered startup + neither creates a missing canonical config nor changes an existing canonical + config or its legacy metadata; +- exact-head core conformance, upstream CI, and host-supervisor integration + evidence remain required before either implementation is called complete. + +The broad prototype was useful evidence, not the proposed V1. It showed that +writable layers, provenance, reload, and rollback substantially expand the +contract. Those features were removed from the upstream implementation rather +than carried as speculative framework. + +## Delivery plan + +### PR 1: OpenClaw V1 + +The implementation draft at +https://github.com/openclaw/openclaw/pull/107026 implements the proposed V1 core +slice in one reviewable change: + +- pure recursive composition; +- exact authority and bounded tool-policy checks; +- repeatable Gateway CLI loading; +- ordinary config and plugin validation; +- immutable server lifecycle; +- focused tests and user documentation. + +The implementation PR records a foreground Gateway proof showing successful +three-layer startup, composed reads, rejected conflict startup, and rejected +runtime mutation. Before it can claim V1 core conformance, it must be rebased to +current main, have green exact-head upstream CI, and map every core conformance +case below to an automated test or named proof. + +### PR 2: Lobster adoption and deletion + +After an OpenClaw release contains PR 1, Lobster can: + +- materialize the three ordinary documents; +- launch OpenClaw with repeated `--config-layer` flags; +- compare the resulting effective config with representative existing + deployments; +- remove the baked overlay writer, stale-value cleanup, and exact generated-blob + tests. + +The Lobster PR should depend on the released OpenClaw version, not an +unpublished branch. No upstream Lobster PR is required before that release. + +## Conformance + +V1 acceptance requires: + +- no-flag startup remains behaviorally unchanged; +- one or more layers compose in declared order; +- duplicate or malformed descriptors fail; +- JSON5, includes, env references, schema, and plugin validation work; +- exact conflicts fail before readiness; +- tool-policy tightening succeeds and weakening fails closed; +- the Gateway uses the composed snapshot for reads; +- reload is disabled for layered mode; +- config and agent config mutations fail before persistence or workspace side + effects; +- closing layered runtimes releases only their own write guards; +- documentation shows the generic model and the Lobster three-file example. + +## Rationale + +### Why an opt-in CLI seam? + +It is explicit, easy for hosts to generate, invisible to ordinary users, and +does not add a second persistent config format. + +### Why reject conflicts? + +Silent precedence hides operator intent and can weaken deployment posture. +Rejecting the whole candidate makes the boundary visible and keeps the last +running configuration unchanged. + +### Why only two bounded fields? + +Exact authority is generic. Monotonic comparison is field-specific. The two +tool-policy fields already have runtime semantics OpenClaw can reuse and test. +Adding an empty comparator framework would increase surface area without +delivering behavior. + +### Why startup-only and read-only? + +Write-through and reload require source selection, concurrency, recovery, and +partial-failure semantics. Hosts can solve the immediate overlay problem by +regenerating source files and restarting. The smaller lifecycle is easier to +reason about and ship. + +### Why core rather than a plugin? + +Composition and authority must run before plugin-aware validation and plugin +activation. A plugin cannot safely enforce the configuration that controls its +own loading. + +## Future work + +The following require separate evidence and design review: + +- redacted per-path provenance and explanation; +- writable source routing; +- live reload with atomic candidate publication; +- rollback generations; +- additional field-specific bounded comparators; +- non-file source adapters. + +None is required for Lobster to replace its baked startup overlays. diff --git a/rfcs/0019/managed-configuration-v1-spec.md b/rfcs/0019/managed-configuration-v1-spec.md new file mode 100644 index 00000000..818bcb3e --- /dev/null +++ b/rfcs/0019/managed-configuration-v1-spec.md @@ -0,0 +1,484 @@ +# Managed Configuration v1 Core Specification + +This document is the implementer-facing core specification for RFC 0019, +Managed Configuration. The RFC explains the motivation, ownership boundary, +and rollout plan. This file defines the v1 invocation, source preparation, +composition, validation, findings, runtime lifecycle, hosting, and conformance +contract that OpenClaw and host supervisors can build against. + +Status: draft, tied to RFC 0019. + +## Scope + +This specification defines: + +- repeated ordered local-file inputs at Gateway startup; +- generic layer identifiers with no built-in deployment roles; +- JSON5, include, and environment preparation for each source; +- recursive first-declaration authority; +- bounded tightening for `tools.allow` and `tools.deny`; +- final schema and plugin-aware validation; +- one immutable startup snapshot; +- path-scoped mutation rejection; +- ordinary single-config compatibility; +- per-cell use by Fleet and other host supervisors; +- a minimum conformance suite. + +This specification does not define: + +- live layer reload or reconciliation; +- writable layers or mutation routing; +- provenance or status APIs; +- generation, rollback, or transaction controllers; +- implicit source discovery, priorities, or inheritance; +- special host, tenant, operator, or Fleet roles; +- secret delivery or credential storage; +- tenant isolation inside one Gateway; +- remote layer URLs or signed layer envelopes. + +## Normative Language + +The terms **must**, **must not**, **should**, and **may** describe requirements +for a conforming v1 implementation. Examples use deployment-oriented labels +only for clarity; those labels have no core semantics. + +## Terminology + +- **Layer**: one explicitly ordered ordinary OpenClaw config document. +- **Layer id**: a unique descriptive label supplied with a layer path. +- **Authored value**: a value present after JSON5 parsing, include resolution, + and environment substitution, before schema defaults are applied. +- **Exact path**: a recursively addressed config path such as + `gateway.controlUi.allowedOrigins`. +- **Controlling layer**: the earliest layer that declares an exact path. +- **Bounded path**: a path with a field-specific rule that may accept a later + value only when it provably tightens the effective value. +- **Composed source**: the sparse authored document produced by the ordered + fold. +- **Runtime snapshot**: the validated config published to ordinary Gateway + consumers for the process lifetime. +- **Candidate evaluation**: one attempt to resolve, compose, and validate the + complete ordered source list before publication. +- **Layered activation**: the lifetime beginning when a candidate is accepted + and ending when the OpenClaw process exits. An in-process Gateway restart is + part of the same activation. +- **Canonical config path**: the `openclaw.json` path that ordinary config + persistence would target for the process. + +## Ownership And Lifetime Contract + +Conforming implementations must preserve these responsibility boundaries: + +| Concern | Owner | Required behavior | +| --- | --- | --- | +| Config field semantics | The existing OpenClaw field owner | Defines defaults, validation, runtime meaning, and any bounded comparator | +| Source selection | The invoker or host supervisor | Supplies complete local files, permissions, and deterministic order | +| Composition and authority admission | OpenClaw config bootstrap | Derives first-declaration authority and rejects the complete candidate on failure | +| Active effective config | The existing Gateway config lifecycle | Publishes exactly one accepted snapshot for the layered activation | +| Mutation exclusion | Each layered Gateway server lifecycle | Registers and releases only its own canonical-path write block | +| Runtime consumption | Existing Gateway, plugin, channel, tool, and agent owners | Consumes the accepted ordinary config without interpreting layer ids or roles | + +Layer ids are diagnostic labels. They must not be treated as identities, +credentials, authorization principals, built-in roles, or durable ownership +records. + +First-declaration authority is derived state scoped to one candidate +evaluation. Once accepted, that authority and its effective snapshot remain +fixed for the layered activation. A full process restart creates a new +activation and must recompute both from the then-current ordered sources. An +in-process Gateway restart remains in the current activation and must reuse the +accepted snapshot. + +The implementation must not persist derived authority as another config +source, silently transfer authority during an activation, or allow a runtime +consumer to reinterpret composition. V1 defines no authority generation, +ownership lease, or write-through owner. + +A control plane or host may own desired state, rollout, source materialization, +and process replacement. Those responsibilities do not transfer ownership of +OpenClaw field semantics, admission, or the effective runtime snapshot. + +## Invocation Contract + +The Gateway CLI accepts a repeatable option: + +```text +--config-layer +``` + +Example: + +```bash +openclaw gateway run \ + --config-layer global=./global.json5 \ + --config-layer tenant=./tenant.json5 \ + --config-layer operator=./operator.json5 +``` + +Requirements: + +- option occurrence order is layer order; +- at least one occurrence enables layered mode; +- the first `=` separates id from path; +- ids and paths must be non-empty after trimming; +- ids must be unique within one invocation; +- paths resolve through normal user-path expansion to absolute local paths; +- a missing, unreadable, or invalid file rejects startup; +- `--dev` and layered mode must not be combined; +- no flag means the ordinary single-config path with no layered behavior. + +The option may be registered on `gateway` and remain available to its `run` +subcommand. The contract is the repeated ordered sequence, not a particular +argument-parser implementation. + +## Source Preparation + +Each layer is prepared independently in declared order. + +For each layer, OpenClaw must: + +1. read the selected local file as UTF-8; +2. parse it as JSON5; +3. resolve `$include` using the layer file as the relative source location and + the normal configured include roots; +4. resolve `${ENV_NAME}` references using the startup environment and preserve + normal missing-variable warnings; +5. require a plain-object root; +6. reject authored root keys `meta` and `env`. + +`meta` and `env` are rejected because they participate in process bootstrap +semantics that precede layered composition. Launchers must supply process +environment through the process boundary instead. + +Preparation does not apply schema defaults to individual layers. Defaults and +plugin-aware validation apply once to the complete composed source so defaults +cannot accidentally claim authority on behalf of an earlier sparse layer. + +## Ordered Composition + +Composition starts from an empty object and folds prepared layers in declared +order: + +```text +state[0] = {} +state[i + 1] = compose(state[i], layer[i]) +composedSource = state[layerCount] +``` + +The operation must be deterministic for the same ordered authored inputs. + +### Recursive Object Semantics + +Plain objects compose recursively. Declaring one child does not claim sibling +paths. For example, an earlier declaration of `gateway.mode` does not control +`gateway.controlUi.allowedOrigins`. + +An empty object is an authored value and controls that object path. It must not +disappear during composition. + +Arrays, scalars, and non-plain-object values are whole-path values unless the +path has a bounded rule. + +### Exact Authority + +Exact authority is the default for every path. + +The earliest layer declaring a path becomes its controlling layer. A later +layer may: + +- omit the path; +- repeat a deeply equal authored value. + +A later layer must not replace the path with a different value. One conflict +invalidates the complete candidate; OpenClaw must not publish a partial +composition. + +### Bounded Tool Policy + +V1 defines bounded rules only for these exact paths: + +| Path | Accepted later value | +| --- | --- | +| `tools.allow` | Proven to narrow the effective allow policy | +| `tools.deny` | Proven to broaden the effective deny policy | + +Comparisons must use OpenClaw's runtime tool-policy meaning, including exact +tool names, groups, wildcard patterns, and the meaning of an empty allow list. + +The following vectors are normative for V1. `accept` means the later value is +proven monotonic; `reject` includes indeterminate containment. + +| Path | Earlier value | Later value | Result | Reason | +| --- | --- | --- | --- | --- | +| `tools.allow` | `[]` | `["read"]` | accept | Empty allow is unrestricted; a non-empty allow narrows it | +| `tools.allow` | `["read", "write"]` | `["read"]` | accept | Exact subset | +| `tools.allow` | `["write"]` | `["apply_patch"]` | accept | The runtime `write` alias includes `apply_patch` | +| `tools.allow` | `["read"]` | `["write"]` | reject | Adds authority | +| `tools.allow` | `["read*"]` | `["read_file"]` | accept | The earlier wildcard matches the later exact name | +| `tools.allow` | `["read*"]` | `["read?"]` | reject | Expression-to-expression containment is not proven | +| `tools.allow` | `["group:fs"]` | `["group:fs"]` | accept | Identical group expression | +| `tools.allow` | `["group:fs"]` | `["group:web"]` | reject | Different group containment is not proven | +| `tools.deny` | `[]` | `["exec"]` | accept | Adds a denial | +| `tools.deny` | `["exec"]` | `["exec", "browser"]` | accept | Exact superset | +| `tools.deny` | `["apply_patch"]` | `["write"]` | accept | The runtime `write` alias continues denying `apply_patch` | +| `tools.deny` | `["write"]` | `["apply_patch"]` | reject | Would stop denying the distinct `write` name | + +Runtime matcher evolution must preserve these results. A matcher change that +changes whether an existing layered input is accepted requires a contract +revision, even when the matcher change is otherwise compatible for ordinary +single-config use. + +A comparator must fail closed. If containment between expressions cannot be +proven, the later declaration is rejected. Syntactic difference alone is not +proof of either tightening or weakening. + +No other path receives a comparator in v1. A future bounded path requires +field-specific semantics, positive and negative vectors, and an RFC/spec +update. Hosts must not inject custom comparators into core composition. + +## Findings And Startup Rejection + +Composition findings are structured and identify enough context to repair the +source without exposing unrelated config values. + +An exact conflict has this minimum shape: + +```json +{ + "reason": "ControlledByEarlierLayer", + "layer": "operator", + "path": "gateway.controlUi.allowedOrigins", + "controllingLayer": "tenant" +} +``` + +A bounded weakening has this minimum shape: + +```json +{ + "reason": "WouldWeakenEarlierLayer", + "layer": "operator", + "path": "tools.deny", + "controllingLayer": "global" +} +``` + +Implementations may attach bounded diagnostic metadata, but must not include +credentials, raw secret values, or complete unredacted documents in findings. + +Any parse, include, root-shape, bootstrap-key, composition, schema, or plugin +validation error rejects Gateway startup before a runtime snapshot is +published. + +## Final Validation And Publication + +After successful composition, OpenClaw must: + +1. resolve plugin metadata for the composed source and effective workspace; +2. validate the composed source through the ordinary OpenClaw schema and + plugin-aware validator; +3. retain ordinary validation warnings; +4. publish the validated config through the existing Gateway startup-snapshot + path. + +Gateway, plugin, channel, tool, and agent consumers receive the ordinary +runtime config shape. They must not implement layer-name or layer-role logic. + +The canonical `openclaw.json` is not an implicit final layer and must not be +created, repaired, migrated, or merged into the layered candidate. + +## Runtime Lifecycle + +Layered v1 is startup-only and read-only. + +While layered mode is active: + +- `config.get` reports the composed source and effective runtime config; +- config mutation RPCs reject before persistence; +- agent create, update, and delete reject before workspace side effects; +- plugin/runtime mutation preflights reject before installation or other + persistent side effects; +- config persistence targeting the canonical config path is blocked; +- an unrelated config path in the same process remains writable; +- canonical file watching and last-known-good promotion are disabled; +- source file changes do not alter the active snapshot; +- an in-process Gateway restart reuses the validated startup snapshot; +- a full process restart rereads the declared layer files. + +Pathless mutation preflights are interpreted as targeting the process's +canonical config path. Runtime write blocks are path-scoped, support +overlapping owners, and release independently when their owning server closes +or startup fails. + +A separate process may edit canonical config while a layered Gateway runs. The +layered Gateway does not consume that file, and such edits do not change its +active snapshot. + +## Ordinary Config Compatibility + +When no `--config-layer` option is supplied: + +- startup reads ordinary canonical config exactly as before; +- normal config watching, migration, mutation, backup, and reload behavior is + unchanged; +- no layered write block is registered; +- users do not need to know the feature exists. + +Layered files use the existing OpenClaw schema. This specification does not +create a parallel hosted or managed config schema. + +## Fleet And Host Supervisor Contract + +One ordered stack configures one Gateway trust domain. Managed configuration +does not permit mutually untrusted tenants to share a Gateway. + +Fleet or another host supervisor may reuse a global document while selecting +cell-specific later documents: + +```text +global + tenant-acme + operator-acme -> Acme Gateway cell +global + tenant-contoso + operator-contoso -> Contoso Gateway cell +``` + +The supervisor owns: + +- cell/process isolation; +- state, credential, workspace, and network boundaries; +- source generation, permissions, mounts, and order; +- rollout and full-process replacement or restart. + +OpenClaw owns: + +- source resolution; +- recursive composition; +- exact and bounded authority; +- schema and plugin validation; +- runtime snapshot publication; +- mutation enforcement. + +V1 does not require a Fleet command change. A Fleet integration may mount the +applicable documents read-only and pass repeated Gateway flags. Cells that +depend on interactive in-cell configuration should use ordinary mutable config +instead of layered mode. + +## Security Requirements + +- Layer files are trusted startup inputs from the launcher or operator. +- File permissions and secure materialization are host responsibilities. +- Layer ids and diagnostics are not authorization boundaries. +- Layering does not isolate sessions, users, tenants, credentials, channels, + workspaces, tools, or state inside one Gateway. +- Mutually untrusted tenants require separate Gateway cells or stronger host + isolation. +- Secrets remain subject to ordinary OpenClaw secret handling and must not be + exposed in findings or logs. +- A later layer must never weaken an earlier bounded policy when containment is + uncertain. + +## Compatibility And Evolution + +The v1 compatibility surface is the CLI spelling, declared order, source +preparation, composition rules, finding reasons, and immutable lifecycle. + +Compatible changes may add clearer diagnostics or optimize implementation +without changing accepted/rejected inputs or runtime behavior. + +The following require an explicit contract revision: + +- accepting remote or implicit sources; +- changing first-declaration authority; +- adding a bounded comparator; +- making layers writable or reloadable; +- adding semantic layer roles; +- changing full-process versus in-process restart behavior; +- treating canonical config as part of the layered stack. + +## Minimum Conformance Suites + +A conforming OpenClaw core implementation must cover the invocation, +composition, validation, and lifecycle cases below. Host-supervisor +conformance is separate because process, state, credential, mount, and network +isolation are host responsibilities rather than OpenClaw core behavior. + +### Invocation And Preparation + +- no flag follows ordinary startup; +- one valid layer starts successfully; +- repeated flags preserve declaration order; +- duplicate or empty ids reject; +- changing only layer ids does not change composition or runtime behavior; +- missing files and invalid JSON5 reject; +- includes resolve relative to each source; +- missing environment variables preserve normal warnings; +- non-object roots and authored `meta` or `env` reject; +- `--dev` plus a layer rejects. + +### Composition + +- unrelated object siblings compose; +- the same value may be repeated; +- a different controlled value rejects with `ControlledByEarlierLayer`; +- arrays behave as whole-path values; +- empty objects preserve authority; +- later `tools.allow` narrowing succeeds; +- later `tools.deny` broadening succeeds; +- allow or deny weakening rejects with `WouldWeakenEarlierLayer`; +- ambiguous wildcard containment fails closed. + +### Validation And Lifecycle + +- final schema and plugin validation use the composed source; +- `config.get` returns the composed snapshot; +- config mutations reject with no canonical write; +- agent mutations reject before workspace side effects; +- pathless plugin mutation preflights reject before installation side effects; +- an unrelated config path remains writable; +- overlapping runtime owners release independently; +- startup failure releases its write owner; +- a fresh state directory does not gain canonical config; +- an existing canonical config remains byte-for-byte unchanged; +- a full restart rereads sources while an in-process restart reuses the + validated snapshot. +- a full restart recomputes authority from the complete current source list; +- no accepted activation can silently transfer authority or publish a partial + candidate. + +### Hosted Cell + +A conforming host integration must cover these cases. They are not prerequisites +for OpenClaw core conformance. + +- two cells may reuse one global source with different tenant sources; +- each cell receives only its own effective config and state; +- the global bounded policy cannot be weakened by either tenant source; +- no test treats layering as cross-tenant process isolation. + +## Implementer Checklist + +An OpenClaw implementation is v1 conformant when it: + +- exposes the repeated Gateway flag without changing no-flag behavior; +- prepares sources through native config primitives; +- composes sparse authored values before applying defaults; +- implements generic recursive exact authority; +- implements only the two specified bounded tool-policy paths; +- rejects the complete startup candidate on any finding or validation error; +- publishes one ordinary runtime snapshot; +- scopes immutability to the canonical config path and server lifetime; +- treats layer ids as diagnostic labels rather than identities or roles; +- keeps field semantics and bounded comparators with their existing OpenClaw + owners; +- prevents persistent side effects before mutation rejection; +- documents restart-to-apply behavior and the lack of tenant isolation; +- passes the OpenClaw core portions of the minimum conformance suites. + +A host supervisor is v1 compatible when it: + +- materializes complete local source files before process start; +- passes a deterministic explicit order; +- protects file paths and permissions; +- uses one stack per Gateway trust domain; +- replaces or fully restarts the process to activate changes; +- does not depend on write-through, reload, provenance, or special role + semantics; +- passes the hosted-cell conformance suite.