From 73104eae7df0ce6735695139241c66c90e355fb9 Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Tue, 18 Aug 2026 12:13:30 -0700 Subject: [PATCH 01/15] docs: propose gateway containment via Windows Isolation Sessions Adds RFC 0032, which argues that the OpenClaw Gateway process itself should be containable rather than running with the signed-in user's full identity, and proposes a platform-agnostic GatewayContainmentProvider seam with Windows Isolation Sessions as the first provider. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 407 ++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 rfcs/0032-gateway-containment-windows-isolation-session.md diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md new file mode 100644 index 00000000..f6ec91c9 --- /dev/null +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -0,0 +1,407 @@ +--- +title: Gateway Containment and Windows Isolation Sessions +authors: + - Paul Campbell +created: 2026-08-18 +last_updated: 2026-08-18 +status: draft +issue: +rfc_pr: +--- + +# Proposal: Gateway Containment and Windows Isolation Sessions + +## Summary + +OpenClaw contains the work an agent does, but not the process that decides to do +it. On Windows the Gateway runs as the signed-in user and inherits that user's +full filesystem, registry, and token reach, so every credential, plugin, and +model-directed decision executes with the operator's identity. This RFC argues +that the Gateway process itself should be containable, and proposes a +platform-agnostic `GatewayContainmentProvider` seam that lets a deployment run +the Gateway inside an OS-managed containment boundary instead of directly as the +user. Windows Isolation Sessions are proposed as the first provider: the +operating system provisions a fresh, OS-assigned agent account, runs the Gateway +in a dedicated session bound to that account, and tears the session and account +down when the owning process exits. The seam and the argument are the proposal; +the Windows provider is currently preview-quality, so this RFC defines the +readiness bar that adoption must clear rather than asking for immediate default +adoption. + +## Motivation + +### The Gateway is the largest uncontained surface + +OpenClaw already has containment seams, and they are all narrower than the +Gateway. `SandboxBackend` (`src/agents/sandbox/`, with Docker and SSH backends) +is created per session and scoped to a session key and workspace directory. It +contains the commands an agent runs. It does not contain the process that loads +plugins, holds channel and provider credentials, runs the scheduler, accepts +node connections, and decides which commands to run in the first place. + +That leaves a straightforward asymmetry. A tool call can be confined to a +container, while the component that chose the tool call, holds the tokens that +authorize it, and can rewrite its own configuration runs with the operator's +full identity. On a Windows workstation the practical blast radius of a +prompt-injection, a hostile skill, or a compromised plugin is therefore the +user's entire profile: documents, browser and credential stores, `HKCU`, startup +entries, SSH keys, and any resource that authenticates the user by token rather +than by password. + +### ACLs are not a boundary against your own code + +The usual mitigations do not close this. File and registry ACLs do not separate +code running as the user from data owned by the user, because that is exactly +the grant they encode. Neither does running the Gateway as a service, or in a +different working directory, or under a restricted shell. Any control that +depends on the Gateway voluntarily declining access is a policy, not a boundary, +and agent software is precisely the category where the process can be talked +into changing its mind. + +The only durable fix is an identity the operating system enforces: run the +Gateway as a principal that is not the user and never had the user's grants. + +### Existing options force an unattractive trade + +Windows deployments today choose between reach and containment: + +- **Run natively as the user.** Full reach, full blast radius. This is the + default and it is what most users run. +- **Run in WSL.** The current recommended local-Gateway path on Windows. It + provides a real boundary, but it is a second operating system with its own + filesystem, package management, credential storage, update cadence, and + support burden, and it moves the Gateway away from the Windows environment the + user actually works in. +- **Run in a VM or Windows Sandbox.** A stronger boundary at a much higher + resource and lifecycle cost, and an awkward fit for a long-lived daemon that + is expected to be running whenever the user is. + +None of these is a good default. The first is not contained, and the others are +heavy enough that most users will not adopt them for an always-on background +process. + +### The operating system now offers a better-shaped primitive + +Windows has begun exposing a containment primitive built around per-instance +identity rather than a whole guest OS. As documented publicly by the +[`microsoft/mxc`](https://github.com/microsoft/mxc) project, its +`isolation_session` containment backend calls a Windows service that provisions +a fresh agent user account with an opaque, OS-assigned name, starts a dedicated +session for it, hosts processes inside that session, and then stops the session +and deprovisions the account. MXC states the requirements this backend exists to +meet as "per-execution OS-isolated identity so the workload's actions cannot +pollute the calling user's NTFS / registry / token state", with an "OS-managed +session lifecycle that the OS-side service tears down cleanly when the calling +process exits." + +That shape matters. The unit of containment is a session and an account, not a +guest operating system, so it avoids a guest's memory footprint and boot time, +and the boundary is an OS-enforced identity rather than a cooperative policy. +Provisioning and session start are not free — MXC's state-aware lifecycle exists +precisely so one provisioned session can host multiple executions "without +re-paying the provisioning / session-start cost each time" — but that is a cost +paid once for a daemon that then stays up. For a long-lived agent process, that +is the trade OpenClaw has been missing. + +### This composes with packaging, and does not duplicate it + +[RFC PR #58](https://github.com/openclaw/rfcs/pull/58) proposes MSIX packaging +for Windows and explicitly lists "Defining runtime isolation. A separate RFC may +define a session-based runtime model" among its non-goals. The two proposals +answer different questions. Packaging gives the installation a reviewable +identity so an administrator can inventory, approve, update, and remove it. +Containment reduces what that installation can reach while it runs. An approved, +signed, inventoried Gateway with the user's full token is still an +unbounded-blast-radius component. + +## Goals + +- Define a platform-agnostic `GatewayContainmentProvider` seam so Gateway + containment is a deployment choice with a stable contract, not a + Windows-specific fork of the startup path. +- Make the containment boundary and its limits explicit and machine-readable, so + a provider that cannot enforce something says so instead of implying it. +- Establish Windows Isolation Sessions as the first provider, contributed behind + the same seam as any future provider. +- Preserve the Gateway's existing contract across the boundary: endpoints, + discovery, device authorization, pairing, node capability approval, and + protocol behavior are unchanged by containment. +- Define explicit selection, fallback, and diagnostic behavior, including what + happens when a requested provider is unavailable on the host. +- Define the readiness bar that a provider must clear before contained execution + can be recommended, and later defaulted, for a class of deployment. + +## Non-Goals + +- **Defining the packaging or launch mechanism.** How a Gateway build is + packaged, signed, distributed, and started on Windows belongs to + [PR #58](https://github.com/openclaw/rfcs/pull/58) and its implementation. + This RFC describes the containment contract and deliberately does not specify + the process that establishes it. +- **Replacing per-session sandboxes.** `SandboxBackend` and the worker-provider + direction in [PR #55](https://github.com/openclaw/rfcs/pull/55) contain a + session's work. This RFC contains the Gateway. They compose; neither + substitutes for the other. +- **Specifying the Windows API surface.** The OS API is owned by Windows and is + consumed, not defined, here. +- **Requiring containment, or making it the default, in this RFC.** The Windows + provider is preview-quality today. This RFC asks for the seam, the argument, + and the readiness bar. +- **Changing authentication, pairing, discovery, or the wire protocol.** No new + endpoint, credential type, or trust relationship is introduced. +- **Defining enterprise policy.** Which deployments must run contained is a + policy question for the administrator and for OpenClaw's enterprise surface, + not for this seam. + +## Proposal + +### What moves, and what does not + +Containment applies to the Gateway process and everything it hosts in-process: +the agent loop, plugin code, the scheduler, and the Gateway's own working state. +The following explicitly stay outside the boundary and are unchanged: + +- Client, channel, and node connections, which continue to reach the Gateway + through its existing endpoints. +- Per-session sandbox backends, which continue to contain agent work. A + contained Gateway may still place a session in a sandbox; containment nests. +- Configuration and credential *ownership*. The Gateway continues to own its + configuration, credentials, and pairing records. The containment provider + provisions and tears down an execution environment; it does not read, copy, + broker, or authorize Gateway credentials. + +### The `GatewayContainmentProvider` seam + +A containment provider answers two things: what boundary can this host actually +provide, and how is a Gateway started and stopped inside it. + +**Capability descriptor.** A provider must declare its boundary honestly, +because an overstated boundary is worse than no boundary. At minimum: + +| Capability | Meaning | +|---|---| +| `identityIsolation` | Whether the contained Gateway runs as a principal distinct from the invoking user, and whether that principal is per-instance or shared. | +| `statePersistence` | Whether Gateway state survives the containment lifecycle, and across what unit — instance, host, or not at all. | +| `hostPathSharing` | Whether, and how, host paths can be projected into the boundary. | +| `networkPosture` | What the provider can enforce on inbound and outbound network access. Explicitly includes "nothing". | +| `hostUiReach` | Whether contained code can observe or drive the user's desktop, clipboard, and input. | +| `lifecycleOwner` | Whether teardown is guaranteed by the OS or must be driven by OpenClaw. | + +A provider must decline configuration it cannot enforce rather than accepting it +silently. This mirrors the disposition MXC already takes for its own backends, +where unenforceable policy is rejected rather than quietly dropped, and it is +the property that makes the descriptor trustworthy. + +**Lifecycle.** Providers implement a lifecycle that a long-lived daemon can use: + +``` +probe -> provision -> start -> attach -> stop -> deprovision +``` + +`probe` reports availability and the capability descriptor for the current host, +without side effects. `provision` creates the isolated principal and +environment. `start` launches the Gateway inside it. `attach` reconnects to an +already-running contained Gateway across separate CLI invocations, so ordinary +commands do not each pay provisioning cost. `stop` and `deprovision` end the +Gateway and release the environment. + +**Selection and fallback.** Containment is selected explicitly by configuration, +never inferred. When a selected provider's `probe` fails, the default is +**fail-closed**: the Gateway does not start, and the failure is reported with +the reason. A deployment may opt into falling back to uncontained execution, but +that must be a stated choice, because a security control that silently degrades +to no control is the failure mode worth avoiding. Whichever path is taken, the +resulting posture must be visible in Gateway status and diagnostics: an operator +should never have to guess whether the Gateway they are talking to is contained. + +### The Windows Isolation Session provider + +The Windows provider maps this seam onto the OS-managed session primitive +described publicly by `microsoft/mxc`. `provision` asks the OS-side service for +a fresh agent account; `start` boots a session bound to that account and +launches the Gateway inside it; `stop` and `deprovision` end the session and +remove the account. Because each provisioned instance is a distinct OS account +with no shared registration, two contained Gateways on one host are independent. + +Every claim this RFC makes about Windows behavior is drawn from that public +documentation rather than from any particular OpenClaw implementation, and the +feature it describes is preview-gated today: MXC lists `isolation_session` as an +experimental backend, gated behind an explicit experimental flag and an OS +feature flag, and available only on recent Windows Insider builds. + +```mermaid +flowchart TB + subgraph host["Windows host"] + user["User profile
files, HKCU, tokens"] + client["Clients, channels, nodes"] + launcher["OpenClaw launcher
selects provider"] + end + + svc["Isolation Session
service, SYSTEM
owns lifecycle"] + + subgraph iso["Isolation session"] + gw["OpenClaw Gateway"] + plugins["Plugins, scheduler,
agent loop"] + sb["Per-session sandboxes"] + gw --- plugins + gw --- sb + end + + launcher -->|"provision and start"| svc + svc -->|"OS-assigned account"| iso + client -->|"existing endpoints,
unchanged protocol"| gw + user -.->|"no inherited grants"| iso +``` + +**Figure 1.** The Gateway runs under an OS-assigned account inside an OS-managed +session. Clients reach it through its existing endpoints; it does not inherit +the signed-in user's grants. + +The boundary this provider delivers is an **identity** boundary, and the +capability descriptor must say exactly that. Per the public MXC documentation +for this backend, it has no host-folder-sharing primitive and rejects filesystem +policy outright; its network is unrestricted, with a process inside able to +listen on a port reachable via localhost; and it exposes no UI-restriction +primitive, though contained code cannot reach the host's desktop or clipboard. +So the honest descriptor is per-instance identity isolation and OS-owned +teardown, with `hostPathSharing` and `networkPosture` reported as unsupported. + +### Obligations this creates + +Naming the boundary honestly surfaces three obligations that the seam must +account for and that the Windows provider does not yet satisfy. + +**State must outlive the environment.** A Gateway is not a one-shot workload. +Its configuration, credentials, pairing records, and session history must +survive restarts. If deprovisioning removes the account that owns that state, +then either state lives outside the boundary through a mechanism the provider +declares, or contained execution is limited to deployments that can tolerate +losing it. The seam must therefore treat `statePersistence` as a first-class +capability rather than an implementation footnote, and OpenClaw must not assume +state survives unless the provider says it does. + +**Reachability must be explicit.** Clients, channels, and nodes must continue to +reach the contained Gateway with no protocol change. Where the provider's +network posture is unrestricted this is straightforward, but it also means +containment buys nothing at the network layer, and the RFC should not let the +word "contained" imply otherwise. + +**Host reach is genuinely lost.** A contained Gateway cannot see the user's +desktop or, on this provider, the user's files. For an agent expected to work on +the user's behalf in the user's environment, that is a real capability +regression, not a detail. It is the central unresolved question below. + +### Readiness bar + +Contained execution should be recommended for a class of deployment only once: + +- The provider's capability descriptor is accurate, and OpenClaw refuses + configuration the provider cannot enforce. +- Gateway state has defined, tested persistence across + `provision`/`deprovision`, including across host restarts. +- Clients, channels, and nodes connect to a contained Gateway with no protocol + change and no additional user step. +- `attach` makes ordinary CLI use cheap, rather than paying provisioning cost + per command. +- Startup, crash, restart, and teardown paths are covered, including orphaned + environments after an ungraceful host shutdown. +- Containment status is reported in Gateway status and diagnostics, and a + fallback to uncontained execution is loud rather than silent. +- The platform primitive is generally available rather than preview-gated, and + its owner is prepared to describe it as a security boundary. The + `microsoft/mxc` README currently states the opposite for its preview backends, + and that alone blocks any claim that containment is a defense OpenClaw can + rely on today. + +Until then, contained execution should ship as an explicitly experimental, +opt-in posture for deployments that want to exercise it. + +## Rationale + +**Why contain the Gateway rather than only the session.** Per-session sandboxes +were designed for the case where the agent's *work* is untrusted. The case that +motivates this RFC is where the agent's *judgment* is untrusted, because +judgment is what prompt injection attacks. Once that is the threat, the +component holding the credentials and choosing the actions has to be inside a +boundary too. + +**Why a provider seam rather than a Windows code path.** The problem is not +Windows-specific; only this particular primitive is. macOS and Linux have +different mechanisms with different capability profiles, and the industry is +actively producing more of them. A seam with an explicit capability descriptor +lets those arrive as contributions, keeps Windows-specific lifecycle code out of +the Gateway's startup path, and forces each provider to state its limits in a +form OpenClaw can act on. Absent a seam, the first implementation becomes the de +facto contract, and its unstated assumptions get baked in. + +**Why Isolation Sessions rather than WSL.** WSL is the current recommended local +Gateway path on Windows and works, but it contains by moving the Gateway into a +second operating system. That brings its own filesystem, package management, +credential storage, update cadence, and failure modes, and it distances the +Gateway from the Windows environment the user works in. An isolation session +keeps the Gateway on Windows and changes only the principal it runs as, which is +the narrower change and the one that matches the actual threat. + +**Why Isolation Sessions rather than a VM or Windows Sandbox.** Both provide a +stronger boundary than an identity boundary, and for some deployments that is +the right answer. They are a poor default for an always-on daemon: the resource +cost is charged continuously, startup is slow enough to be noticed, and their +lifecycle model is oriented toward disposable sessions rather than a service the +user expects to already be running. The value of the isolation-session primitive +is that its cost profile makes contained execution plausible as an eventual +default rather than an expert-only mode. + +**Why not AppContainer or a process-level container.** Process-level containment +on Windows restricts a process that still runs as the user. It is useful, and +MXC's default Windows backend uses it, but the principal is unchanged, so the +per-user token reach that motivates this RFC is not removed. Isolation sessions +change the principal, which is the property being argued for. + +**Why fail-closed by default.** A containment control that silently degrades to +no containment produces the worst outcome: an operator who believes they are +protected and is not. Failing closed makes the absence of the boundary a visible +event. Deployments that prefer availability can opt out, having said so. + +**Why argue the direction before the primitive is ready.** The seam, the +capability descriptor, and the readiness bar are useful independently of any one +provider, and they are what determine whether contained execution is adopted +honestly. Designing them while the first provider is still preview-quality is +cheaper than retrofitting them around a shipped Windows-specific implementation. + +## Unresolved questions + +- **How does a contained Gateway reach the user's files?** This provider has no + host-folder-sharing primitive. An agent that cannot open the user's working + files is not useful for most of what OpenClaw is used for. Is the answer a + future OS sharing primitive, an explicit user-mediated projection of selected + paths, a filesystem bridge over the existing protocol similar to the remote + filesystem bridge already used for sandboxes, or an accepted restriction to + deployments where the Gateway operates on its own workspace? +- **Where does Gateway state live?** If the OS-assigned account is removed at + deprovision, what persists configuration, credentials, and pairing records + across restarts, and what is the migration path for an existing uncontained + installation adopting containment? +- **What is the correct network posture?** The provider enforces nothing today + and the Gateway's endpoints are reachable via localhost. Should OpenClaw + require a network-capable provider before recommending containment, or is an + identity-only boundary sufficient for the stated threat? +- **How do Windows node capabilities survive?** Computer use, screen capture, + and input injection (cf. [RFC 0025](0025-default-pluggable-computer-use.md)) + require the user's desktop, which contained code cannot reach. Does the + Windows node remain outside the boundary and connect to the contained Gateway + as a node, and what does that imply for the boundary's value? +- **What are the availability and gating rules?** The primitive is preview-gated + and requires a recent Windows build. How does OpenClaw express a containment + posture that is unavailable on most hosts without fragmenting the Windows + experience? +- **How is autostart handled?** A Gateway expected to run in the background must + start without an interactive logon. What does contained startup look like + before or without a signed-in user? +- **Who owns the provider implementation?** Does it live in the OpenClaw + repository, alongside the Windows packaging work, or in a separate + platform-integration component, and what is the dependency and support + boundary? +- **What is the acceptance criterion for calling this a security boundary?** The + platform currently declines to make that claim for preview profiles. OpenClaw + should state in advance what evidence it requires before describing contained + execution as a defense to users. From e5584d393814623318b8c7a7000c46722caabf06 Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Tue, 18 Aug 2026 12:14:33 -0700 Subject: [PATCH 02/15] docs: set rfc_pr for RFC 0032 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- rfcs/0032-gateway-containment-windows-isolation-session.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index f6ec91c9..28e3d84e 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -6,7 +6,7 @@ created: 2026-08-18 last_updated: 2026-08-18 status: draft issue: -rfc_pr: +rfc_pr: https://github.com/openclaw/rfcs/pull/61 --- # Proposal: Gateway Containment and Windows Isolation Sessions From 6aa8783f6b32ac3f1c324ab002f2cc17bb3fd9b8 Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Tue, 18 Aug 2026 14:11:31 -0700 Subject: [PATCH 03/15] docs: normalize RFC 0032 to LF line endings Addresses the ClawSweeper P2 finding: the file was committed with CRLF, unlike every other RFC in the repository. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 814 +++++++++--------- 1 file changed, 407 insertions(+), 407 deletions(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index 28e3d84e..d6aa61c8 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -1,407 +1,407 @@ ---- -title: Gateway Containment and Windows Isolation Sessions -authors: - - Paul Campbell -created: 2026-08-18 -last_updated: 2026-08-18 -status: draft -issue: -rfc_pr: https://github.com/openclaw/rfcs/pull/61 ---- - -# Proposal: Gateway Containment and Windows Isolation Sessions - -## Summary - -OpenClaw contains the work an agent does, but not the process that decides to do -it. On Windows the Gateway runs as the signed-in user and inherits that user's -full filesystem, registry, and token reach, so every credential, plugin, and -model-directed decision executes with the operator's identity. This RFC argues -that the Gateway process itself should be containable, and proposes a -platform-agnostic `GatewayContainmentProvider` seam that lets a deployment run -the Gateway inside an OS-managed containment boundary instead of directly as the -user. Windows Isolation Sessions are proposed as the first provider: the -operating system provisions a fresh, OS-assigned agent account, runs the Gateway -in a dedicated session bound to that account, and tears the session and account -down when the owning process exits. The seam and the argument are the proposal; -the Windows provider is currently preview-quality, so this RFC defines the -readiness bar that adoption must clear rather than asking for immediate default -adoption. - -## Motivation - -### The Gateway is the largest uncontained surface - -OpenClaw already has containment seams, and they are all narrower than the -Gateway. `SandboxBackend` (`src/agents/sandbox/`, with Docker and SSH backends) -is created per session and scoped to a session key and workspace directory. It -contains the commands an agent runs. It does not contain the process that loads -plugins, holds channel and provider credentials, runs the scheduler, accepts -node connections, and decides which commands to run in the first place. - -That leaves a straightforward asymmetry. A tool call can be confined to a -container, while the component that chose the tool call, holds the tokens that -authorize it, and can rewrite its own configuration runs with the operator's -full identity. On a Windows workstation the practical blast radius of a -prompt-injection, a hostile skill, or a compromised plugin is therefore the -user's entire profile: documents, browser and credential stores, `HKCU`, startup -entries, SSH keys, and any resource that authenticates the user by token rather -than by password. - -### ACLs are not a boundary against your own code - -The usual mitigations do not close this. File and registry ACLs do not separate -code running as the user from data owned by the user, because that is exactly -the grant they encode. Neither does running the Gateway as a service, or in a -different working directory, or under a restricted shell. Any control that -depends on the Gateway voluntarily declining access is a policy, not a boundary, -and agent software is precisely the category where the process can be talked -into changing its mind. - -The only durable fix is an identity the operating system enforces: run the -Gateway as a principal that is not the user and never had the user's grants. - -### Existing options force an unattractive trade - -Windows deployments today choose between reach and containment: - -- **Run natively as the user.** Full reach, full blast radius. This is the - default and it is what most users run. -- **Run in WSL.** The current recommended local-Gateway path on Windows. It - provides a real boundary, but it is a second operating system with its own - filesystem, package management, credential storage, update cadence, and - support burden, and it moves the Gateway away from the Windows environment the - user actually works in. -- **Run in a VM or Windows Sandbox.** A stronger boundary at a much higher - resource and lifecycle cost, and an awkward fit for a long-lived daemon that - is expected to be running whenever the user is. - -None of these is a good default. The first is not contained, and the others are -heavy enough that most users will not adopt them for an always-on background -process. - -### The operating system now offers a better-shaped primitive - -Windows has begun exposing a containment primitive built around per-instance -identity rather than a whole guest OS. As documented publicly by the -[`microsoft/mxc`](https://github.com/microsoft/mxc) project, its -`isolation_session` containment backend calls a Windows service that provisions -a fresh agent user account with an opaque, OS-assigned name, starts a dedicated -session for it, hosts processes inside that session, and then stops the session -and deprovisions the account. MXC states the requirements this backend exists to -meet as "per-execution OS-isolated identity so the workload's actions cannot -pollute the calling user's NTFS / registry / token state", with an "OS-managed -session lifecycle that the OS-side service tears down cleanly when the calling -process exits." - -That shape matters. The unit of containment is a session and an account, not a -guest operating system, so it avoids a guest's memory footprint and boot time, -and the boundary is an OS-enforced identity rather than a cooperative policy. -Provisioning and session start are not free — MXC's state-aware lifecycle exists -precisely so one provisioned session can host multiple executions "without -re-paying the provisioning / session-start cost each time" — but that is a cost -paid once for a daemon that then stays up. For a long-lived agent process, that -is the trade OpenClaw has been missing. - -### This composes with packaging, and does not duplicate it - -[RFC PR #58](https://github.com/openclaw/rfcs/pull/58) proposes MSIX packaging -for Windows and explicitly lists "Defining runtime isolation. A separate RFC may -define a session-based runtime model" among its non-goals. The two proposals -answer different questions. Packaging gives the installation a reviewable -identity so an administrator can inventory, approve, update, and remove it. -Containment reduces what that installation can reach while it runs. An approved, -signed, inventoried Gateway with the user's full token is still an -unbounded-blast-radius component. - -## Goals - -- Define a platform-agnostic `GatewayContainmentProvider` seam so Gateway - containment is a deployment choice with a stable contract, not a - Windows-specific fork of the startup path. -- Make the containment boundary and its limits explicit and machine-readable, so - a provider that cannot enforce something says so instead of implying it. -- Establish Windows Isolation Sessions as the first provider, contributed behind - the same seam as any future provider. -- Preserve the Gateway's existing contract across the boundary: endpoints, - discovery, device authorization, pairing, node capability approval, and - protocol behavior are unchanged by containment. -- Define explicit selection, fallback, and diagnostic behavior, including what - happens when a requested provider is unavailable on the host. -- Define the readiness bar that a provider must clear before contained execution - can be recommended, and later defaulted, for a class of deployment. - -## Non-Goals - -- **Defining the packaging or launch mechanism.** How a Gateway build is - packaged, signed, distributed, and started on Windows belongs to - [PR #58](https://github.com/openclaw/rfcs/pull/58) and its implementation. - This RFC describes the containment contract and deliberately does not specify - the process that establishes it. -- **Replacing per-session sandboxes.** `SandboxBackend` and the worker-provider - direction in [PR #55](https://github.com/openclaw/rfcs/pull/55) contain a - session's work. This RFC contains the Gateway. They compose; neither - substitutes for the other. -- **Specifying the Windows API surface.** The OS API is owned by Windows and is - consumed, not defined, here. -- **Requiring containment, or making it the default, in this RFC.** The Windows - provider is preview-quality today. This RFC asks for the seam, the argument, - and the readiness bar. -- **Changing authentication, pairing, discovery, or the wire protocol.** No new - endpoint, credential type, or trust relationship is introduced. -- **Defining enterprise policy.** Which deployments must run contained is a - policy question for the administrator and for OpenClaw's enterprise surface, - not for this seam. - -## Proposal - -### What moves, and what does not - -Containment applies to the Gateway process and everything it hosts in-process: -the agent loop, plugin code, the scheduler, and the Gateway's own working state. -The following explicitly stay outside the boundary and are unchanged: - -- Client, channel, and node connections, which continue to reach the Gateway - through its existing endpoints. -- Per-session sandbox backends, which continue to contain agent work. A - contained Gateway may still place a session in a sandbox; containment nests. -- Configuration and credential *ownership*. The Gateway continues to own its - configuration, credentials, and pairing records. The containment provider - provisions and tears down an execution environment; it does not read, copy, - broker, or authorize Gateway credentials. - -### The `GatewayContainmentProvider` seam - -A containment provider answers two things: what boundary can this host actually -provide, and how is a Gateway started and stopped inside it. - -**Capability descriptor.** A provider must declare its boundary honestly, -because an overstated boundary is worse than no boundary. At minimum: - -| Capability | Meaning | -|---|---| -| `identityIsolation` | Whether the contained Gateway runs as a principal distinct from the invoking user, and whether that principal is per-instance or shared. | -| `statePersistence` | Whether Gateway state survives the containment lifecycle, and across what unit — instance, host, or not at all. | -| `hostPathSharing` | Whether, and how, host paths can be projected into the boundary. | -| `networkPosture` | What the provider can enforce on inbound and outbound network access. Explicitly includes "nothing". | -| `hostUiReach` | Whether contained code can observe or drive the user's desktop, clipboard, and input. | -| `lifecycleOwner` | Whether teardown is guaranteed by the OS or must be driven by OpenClaw. | - -A provider must decline configuration it cannot enforce rather than accepting it -silently. This mirrors the disposition MXC already takes for its own backends, -where unenforceable policy is rejected rather than quietly dropped, and it is -the property that makes the descriptor trustworthy. - -**Lifecycle.** Providers implement a lifecycle that a long-lived daemon can use: - -``` -probe -> provision -> start -> attach -> stop -> deprovision -``` - -`probe` reports availability and the capability descriptor for the current host, -without side effects. `provision` creates the isolated principal and -environment. `start` launches the Gateway inside it. `attach` reconnects to an -already-running contained Gateway across separate CLI invocations, so ordinary -commands do not each pay provisioning cost. `stop` and `deprovision` end the -Gateway and release the environment. - -**Selection and fallback.** Containment is selected explicitly by configuration, -never inferred. When a selected provider's `probe` fails, the default is -**fail-closed**: the Gateway does not start, and the failure is reported with -the reason. A deployment may opt into falling back to uncontained execution, but -that must be a stated choice, because a security control that silently degrades -to no control is the failure mode worth avoiding. Whichever path is taken, the -resulting posture must be visible in Gateway status and diagnostics: an operator -should never have to guess whether the Gateway they are talking to is contained. - -### The Windows Isolation Session provider - -The Windows provider maps this seam onto the OS-managed session primitive -described publicly by `microsoft/mxc`. `provision` asks the OS-side service for -a fresh agent account; `start` boots a session bound to that account and -launches the Gateway inside it; `stop` and `deprovision` end the session and -remove the account. Because each provisioned instance is a distinct OS account -with no shared registration, two contained Gateways on one host are independent. - -Every claim this RFC makes about Windows behavior is drawn from that public -documentation rather than from any particular OpenClaw implementation, and the -feature it describes is preview-gated today: MXC lists `isolation_session` as an -experimental backend, gated behind an explicit experimental flag and an OS -feature flag, and available only on recent Windows Insider builds. - -```mermaid -flowchart TB - subgraph host["Windows host"] - user["User profile
files, HKCU, tokens"] - client["Clients, channels, nodes"] - launcher["OpenClaw launcher
selects provider"] - end - - svc["Isolation Session
service, SYSTEM
owns lifecycle"] - - subgraph iso["Isolation session"] - gw["OpenClaw Gateway"] - plugins["Plugins, scheduler,
agent loop"] - sb["Per-session sandboxes"] - gw --- plugins - gw --- sb - end - - launcher -->|"provision and start"| svc - svc -->|"OS-assigned account"| iso - client -->|"existing endpoints,
unchanged protocol"| gw - user -.->|"no inherited grants"| iso -``` - -**Figure 1.** The Gateway runs under an OS-assigned account inside an OS-managed -session. Clients reach it through its existing endpoints; it does not inherit -the signed-in user's grants. - -The boundary this provider delivers is an **identity** boundary, and the -capability descriptor must say exactly that. Per the public MXC documentation -for this backend, it has no host-folder-sharing primitive and rejects filesystem -policy outright; its network is unrestricted, with a process inside able to -listen on a port reachable via localhost; and it exposes no UI-restriction -primitive, though contained code cannot reach the host's desktop or clipboard. -So the honest descriptor is per-instance identity isolation and OS-owned -teardown, with `hostPathSharing` and `networkPosture` reported as unsupported. - -### Obligations this creates - -Naming the boundary honestly surfaces three obligations that the seam must -account for and that the Windows provider does not yet satisfy. - -**State must outlive the environment.** A Gateway is not a one-shot workload. -Its configuration, credentials, pairing records, and session history must -survive restarts. If deprovisioning removes the account that owns that state, -then either state lives outside the boundary through a mechanism the provider -declares, or contained execution is limited to deployments that can tolerate -losing it. The seam must therefore treat `statePersistence` as a first-class -capability rather than an implementation footnote, and OpenClaw must not assume -state survives unless the provider says it does. - -**Reachability must be explicit.** Clients, channels, and nodes must continue to -reach the contained Gateway with no protocol change. Where the provider's -network posture is unrestricted this is straightforward, but it also means -containment buys nothing at the network layer, and the RFC should not let the -word "contained" imply otherwise. - -**Host reach is genuinely lost.** A contained Gateway cannot see the user's -desktop or, on this provider, the user's files. For an agent expected to work on -the user's behalf in the user's environment, that is a real capability -regression, not a detail. It is the central unresolved question below. - -### Readiness bar - -Contained execution should be recommended for a class of deployment only once: - -- The provider's capability descriptor is accurate, and OpenClaw refuses - configuration the provider cannot enforce. -- Gateway state has defined, tested persistence across - `provision`/`deprovision`, including across host restarts. -- Clients, channels, and nodes connect to a contained Gateway with no protocol - change and no additional user step. -- `attach` makes ordinary CLI use cheap, rather than paying provisioning cost - per command. -- Startup, crash, restart, and teardown paths are covered, including orphaned - environments after an ungraceful host shutdown. -- Containment status is reported in Gateway status and diagnostics, and a - fallback to uncontained execution is loud rather than silent. -- The platform primitive is generally available rather than preview-gated, and - its owner is prepared to describe it as a security boundary. The - `microsoft/mxc` README currently states the opposite for its preview backends, - and that alone blocks any claim that containment is a defense OpenClaw can - rely on today. - -Until then, contained execution should ship as an explicitly experimental, -opt-in posture for deployments that want to exercise it. - -## Rationale - -**Why contain the Gateway rather than only the session.** Per-session sandboxes -were designed for the case where the agent's *work* is untrusted. The case that -motivates this RFC is where the agent's *judgment* is untrusted, because -judgment is what prompt injection attacks. Once that is the threat, the -component holding the credentials and choosing the actions has to be inside a -boundary too. - -**Why a provider seam rather than a Windows code path.** The problem is not -Windows-specific; only this particular primitive is. macOS and Linux have -different mechanisms with different capability profiles, and the industry is -actively producing more of them. A seam with an explicit capability descriptor -lets those arrive as contributions, keeps Windows-specific lifecycle code out of -the Gateway's startup path, and forces each provider to state its limits in a -form OpenClaw can act on. Absent a seam, the first implementation becomes the de -facto contract, and its unstated assumptions get baked in. - -**Why Isolation Sessions rather than WSL.** WSL is the current recommended local -Gateway path on Windows and works, but it contains by moving the Gateway into a -second operating system. That brings its own filesystem, package management, -credential storage, update cadence, and failure modes, and it distances the -Gateway from the Windows environment the user works in. An isolation session -keeps the Gateway on Windows and changes only the principal it runs as, which is -the narrower change and the one that matches the actual threat. - -**Why Isolation Sessions rather than a VM or Windows Sandbox.** Both provide a -stronger boundary than an identity boundary, and for some deployments that is -the right answer. They are a poor default for an always-on daemon: the resource -cost is charged continuously, startup is slow enough to be noticed, and their -lifecycle model is oriented toward disposable sessions rather than a service the -user expects to already be running. The value of the isolation-session primitive -is that its cost profile makes contained execution plausible as an eventual -default rather than an expert-only mode. - -**Why not AppContainer or a process-level container.** Process-level containment -on Windows restricts a process that still runs as the user. It is useful, and -MXC's default Windows backend uses it, but the principal is unchanged, so the -per-user token reach that motivates this RFC is not removed. Isolation sessions -change the principal, which is the property being argued for. - -**Why fail-closed by default.** A containment control that silently degrades to -no containment produces the worst outcome: an operator who believes they are -protected and is not. Failing closed makes the absence of the boundary a visible -event. Deployments that prefer availability can opt out, having said so. - -**Why argue the direction before the primitive is ready.** The seam, the -capability descriptor, and the readiness bar are useful independently of any one -provider, and they are what determine whether contained execution is adopted -honestly. Designing them while the first provider is still preview-quality is -cheaper than retrofitting them around a shipped Windows-specific implementation. - -## Unresolved questions - -- **How does a contained Gateway reach the user's files?** This provider has no - host-folder-sharing primitive. An agent that cannot open the user's working - files is not useful for most of what OpenClaw is used for. Is the answer a - future OS sharing primitive, an explicit user-mediated projection of selected - paths, a filesystem bridge over the existing protocol similar to the remote - filesystem bridge already used for sandboxes, or an accepted restriction to - deployments where the Gateway operates on its own workspace? -- **Where does Gateway state live?** If the OS-assigned account is removed at - deprovision, what persists configuration, credentials, and pairing records - across restarts, and what is the migration path for an existing uncontained - installation adopting containment? -- **What is the correct network posture?** The provider enforces nothing today - and the Gateway's endpoints are reachable via localhost. Should OpenClaw - require a network-capable provider before recommending containment, or is an - identity-only boundary sufficient for the stated threat? -- **How do Windows node capabilities survive?** Computer use, screen capture, - and input injection (cf. [RFC 0025](0025-default-pluggable-computer-use.md)) - require the user's desktop, which contained code cannot reach. Does the - Windows node remain outside the boundary and connect to the contained Gateway - as a node, and what does that imply for the boundary's value? -- **What are the availability and gating rules?** The primitive is preview-gated - and requires a recent Windows build. How does OpenClaw express a containment - posture that is unavailable on most hosts without fragmenting the Windows - experience? -- **How is autostart handled?** A Gateway expected to run in the background must - start without an interactive logon. What does contained startup look like - before or without a signed-in user? -- **Who owns the provider implementation?** Does it live in the OpenClaw - repository, alongside the Windows packaging work, or in a separate - platform-integration component, and what is the dependency and support - boundary? -- **What is the acceptance criterion for calling this a security boundary?** The - platform currently declines to make that claim for preview profiles. OpenClaw - should state in advance what evidence it requires before describing contained - execution as a defense to users. +--- +title: Gateway Containment and Windows Isolation Sessions +authors: + - Paul Campbell +created: 2026-08-18 +last_updated: 2026-08-18 +status: draft +issue: +rfc_pr: https://github.com/openclaw/rfcs/pull/61 +--- + +# Proposal: Gateway Containment and Windows Isolation Sessions + +## Summary + +OpenClaw contains the work an agent does, but not the process that decides to do +it. On Windows the Gateway runs as the signed-in user and inherits that user's +full filesystem, registry, and token reach, so every credential, plugin, and +model-directed decision executes with the operator's identity. This RFC argues +that the Gateway process itself should be containable, and proposes a +platform-agnostic `GatewayContainmentProvider` seam that lets a deployment run +the Gateway inside an OS-managed containment boundary instead of directly as the +user. Windows Isolation Sessions are proposed as the first provider: the +operating system provisions a fresh, OS-assigned agent account, runs the Gateway +in a dedicated session bound to that account, and tears the session and account +down when the owning process exits. The seam and the argument are the proposal; +the Windows provider is currently preview-quality, so this RFC defines the +readiness bar that adoption must clear rather than asking for immediate default +adoption. + +## Motivation + +### The Gateway is the largest uncontained surface + +OpenClaw already has containment seams, and they are all narrower than the +Gateway. `SandboxBackend` (`src/agents/sandbox/`, with Docker and SSH backends) +is created per session and scoped to a session key and workspace directory. It +contains the commands an agent runs. It does not contain the process that loads +plugins, holds channel and provider credentials, runs the scheduler, accepts +node connections, and decides which commands to run in the first place. + +That leaves a straightforward asymmetry. A tool call can be confined to a +container, while the component that chose the tool call, holds the tokens that +authorize it, and can rewrite its own configuration runs with the operator's +full identity. On a Windows workstation the practical blast radius of a +prompt-injection, a hostile skill, or a compromised plugin is therefore the +user's entire profile: documents, browser and credential stores, `HKCU`, startup +entries, SSH keys, and any resource that authenticates the user by token rather +than by password. + +### ACLs are not a boundary against your own code + +The usual mitigations do not close this. File and registry ACLs do not separate +code running as the user from data owned by the user, because that is exactly +the grant they encode. Neither does running the Gateway as a service, or in a +different working directory, or under a restricted shell. Any control that +depends on the Gateway voluntarily declining access is a policy, not a boundary, +and agent software is precisely the category where the process can be talked +into changing its mind. + +The only durable fix is an identity the operating system enforces: run the +Gateway as a principal that is not the user and never had the user's grants. + +### Existing options force an unattractive trade + +Windows deployments today choose between reach and containment: + +- **Run natively as the user.** Full reach, full blast radius. This is the + default and it is what most users run. +- **Run in WSL.** The current recommended local-Gateway path on Windows. It + provides a real boundary, but it is a second operating system with its own + filesystem, package management, credential storage, update cadence, and + support burden, and it moves the Gateway away from the Windows environment the + user actually works in. +- **Run in a VM or Windows Sandbox.** A stronger boundary at a much higher + resource and lifecycle cost, and an awkward fit for a long-lived daemon that + is expected to be running whenever the user is. + +None of these is a good default. The first is not contained, and the others are +heavy enough that most users will not adopt them for an always-on background +process. + +### The operating system now offers a better-shaped primitive + +Windows has begun exposing a containment primitive built around per-instance +identity rather than a whole guest OS. As documented publicly by the +[`microsoft/mxc`](https://github.com/microsoft/mxc) project, its +`isolation_session` containment backend calls a Windows service that provisions +a fresh agent user account with an opaque, OS-assigned name, starts a dedicated +session for it, hosts processes inside that session, and then stops the session +and deprovisions the account. MXC states the requirements this backend exists to +meet as "per-execution OS-isolated identity so the workload's actions cannot +pollute the calling user's NTFS / registry / token state", with an "OS-managed +session lifecycle that the OS-side service tears down cleanly when the calling +process exits." + +That shape matters. The unit of containment is a session and an account, not a +guest operating system, so it avoids a guest's memory footprint and boot time, +and the boundary is an OS-enforced identity rather than a cooperative policy. +Provisioning and session start are not free — MXC's state-aware lifecycle exists +precisely so one provisioned session can host multiple executions "without +re-paying the provisioning / session-start cost each time" — but that is a cost +paid once for a daemon that then stays up. For a long-lived agent process, that +is the trade OpenClaw has been missing. + +### This composes with packaging, and does not duplicate it + +[RFC PR #58](https://github.com/openclaw/rfcs/pull/58) proposes MSIX packaging +for Windows and explicitly lists "Defining runtime isolation. A separate RFC may +define a session-based runtime model" among its non-goals. The two proposals +answer different questions. Packaging gives the installation a reviewable +identity so an administrator can inventory, approve, update, and remove it. +Containment reduces what that installation can reach while it runs. An approved, +signed, inventoried Gateway with the user's full token is still an +unbounded-blast-radius component. + +## Goals + +- Define a platform-agnostic `GatewayContainmentProvider` seam so Gateway + containment is a deployment choice with a stable contract, not a + Windows-specific fork of the startup path. +- Make the containment boundary and its limits explicit and machine-readable, so + a provider that cannot enforce something says so instead of implying it. +- Establish Windows Isolation Sessions as the first provider, contributed behind + the same seam as any future provider. +- Preserve the Gateway's existing contract across the boundary: endpoints, + discovery, device authorization, pairing, node capability approval, and + protocol behavior are unchanged by containment. +- Define explicit selection, fallback, and diagnostic behavior, including what + happens when a requested provider is unavailable on the host. +- Define the readiness bar that a provider must clear before contained execution + can be recommended, and later defaulted, for a class of deployment. + +## Non-Goals + +- **Defining the packaging or launch mechanism.** How a Gateway build is + packaged, signed, distributed, and started on Windows belongs to + [PR #58](https://github.com/openclaw/rfcs/pull/58) and its implementation. + This RFC describes the containment contract and deliberately does not specify + the process that establishes it. +- **Replacing per-session sandboxes.** `SandboxBackend` and the worker-provider + direction in [PR #55](https://github.com/openclaw/rfcs/pull/55) contain a + session's work. This RFC contains the Gateway. They compose; neither + substitutes for the other. +- **Specifying the Windows API surface.** The OS API is owned by Windows and is + consumed, not defined, here. +- **Requiring containment, or making it the default, in this RFC.** The Windows + provider is preview-quality today. This RFC asks for the seam, the argument, + and the readiness bar. +- **Changing authentication, pairing, discovery, or the wire protocol.** No new + endpoint, credential type, or trust relationship is introduced. +- **Defining enterprise policy.** Which deployments must run contained is a + policy question for the administrator and for OpenClaw's enterprise surface, + not for this seam. + +## Proposal + +### What moves, and what does not + +Containment applies to the Gateway process and everything it hosts in-process: +the agent loop, plugin code, the scheduler, and the Gateway's own working state. +The following explicitly stay outside the boundary and are unchanged: + +- Client, channel, and node connections, which continue to reach the Gateway + through its existing endpoints. +- Per-session sandbox backends, which continue to contain agent work. A + contained Gateway may still place a session in a sandbox; containment nests. +- Configuration and credential *ownership*. The Gateway continues to own its + configuration, credentials, and pairing records. The containment provider + provisions and tears down an execution environment; it does not read, copy, + broker, or authorize Gateway credentials. + +### The `GatewayContainmentProvider` seam + +A containment provider answers two things: what boundary can this host actually +provide, and how is a Gateway started and stopped inside it. + +**Capability descriptor.** A provider must declare its boundary honestly, +because an overstated boundary is worse than no boundary. At minimum: + +| Capability | Meaning | +|---|---| +| `identityIsolation` | Whether the contained Gateway runs as a principal distinct from the invoking user, and whether that principal is per-instance or shared. | +| `statePersistence` | Whether Gateway state survives the containment lifecycle, and across what unit — instance, host, or not at all. | +| `hostPathSharing` | Whether, and how, host paths can be projected into the boundary. | +| `networkPosture` | What the provider can enforce on inbound and outbound network access. Explicitly includes "nothing". | +| `hostUiReach` | Whether contained code can observe or drive the user's desktop, clipboard, and input. | +| `lifecycleOwner` | Whether teardown is guaranteed by the OS or must be driven by OpenClaw. | + +A provider must decline configuration it cannot enforce rather than accepting it +silently. This mirrors the disposition MXC already takes for its own backends, +where unenforceable policy is rejected rather than quietly dropped, and it is +the property that makes the descriptor trustworthy. + +**Lifecycle.** Providers implement a lifecycle that a long-lived daemon can use: + +``` +probe -> provision -> start -> attach -> stop -> deprovision +``` + +`probe` reports availability and the capability descriptor for the current host, +without side effects. `provision` creates the isolated principal and +environment. `start` launches the Gateway inside it. `attach` reconnects to an +already-running contained Gateway across separate CLI invocations, so ordinary +commands do not each pay provisioning cost. `stop` and `deprovision` end the +Gateway and release the environment. + +**Selection and fallback.** Containment is selected explicitly by configuration, +never inferred. When a selected provider's `probe` fails, the default is +**fail-closed**: the Gateway does not start, and the failure is reported with +the reason. A deployment may opt into falling back to uncontained execution, but +that must be a stated choice, because a security control that silently degrades +to no control is the failure mode worth avoiding. Whichever path is taken, the +resulting posture must be visible in Gateway status and diagnostics: an operator +should never have to guess whether the Gateway they are talking to is contained. + +### The Windows Isolation Session provider + +The Windows provider maps this seam onto the OS-managed session primitive +described publicly by `microsoft/mxc`. `provision` asks the OS-side service for +a fresh agent account; `start` boots a session bound to that account and +launches the Gateway inside it; `stop` and `deprovision` end the session and +remove the account. Because each provisioned instance is a distinct OS account +with no shared registration, two contained Gateways on one host are independent. + +Every claim this RFC makes about Windows behavior is drawn from that public +documentation rather than from any particular OpenClaw implementation, and the +feature it describes is preview-gated today: MXC lists `isolation_session` as an +experimental backend, gated behind an explicit experimental flag and an OS +feature flag, and available only on recent Windows Insider builds. + +```mermaid +flowchart TB + subgraph host["Windows host"] + user["User profile
files, HKCU, tokens"] + client["Clients, channels, nodes"] + launcher["OpenClaw launcher
selects provider"] + end + + svc["Isolation Session
service, SYSTEM
owns lifecycle"] + + subgraph iso["Isolation session"] + gw["OpenClaw Gateway"] + plugins["Plugins, scheduler,
agent loop"] + sb["Per-session sandboxes"] + gw --- plugins + gw --- sb + end + + launcher -->|"provision and start"| svc + svc -->|"OS-assigned account"| iso + client -->|"existing endpoints,
unchanged protocol"| gw + user -.->|"no inherited grants"| iso +``` + +**Figure 1.** The Gateway runs under an OS-assigned account inside an OS-managed +session. Clients reach it through its existing endpoints; it does not inherit +the signed-in user's grants. + +The boundary this provider delivers is an **identity** boundary, and the +capability descriptor must say exactly that. Per the public MXC documentation +for this backend, it has no host-folder-sharing primitive and rejects filesystem +policy outright; its network is unrestricted, with a process inside able to +listen on a port reachable via localhost; and it exposes no UI-restriction +primitive, though contained code cannot reach the host's desktop or clipboard. +So the honest descriptor is per-instance identity isolation and OS-owned +teardown, with `hostPathSharing` and `networkPosture` reported as unsupported. + +### Obligations this creates + +Naming the boundary honestly surfaces three obligations that the seam must +account for and that the Windows provider does not yet satisfy. + +**State must outlive the environment.** A Gateway is not a one-shot workload. +Its configuration, credentials, pairing records, and session history must +survive restarts. If deprovisioning removes the account that owns that state, +then either state lives outside the boundary through a mechanism the provider +declares, or contained execution is limited to deployments that can tolerate +losing it. The seam must therefore treat `statePersistence` as a first-class +capability rather than an implementation footnote, and OpenClaw must not assume +state survives unless the provider says it does. + +**Reachability must be explicit.** Clients, channels, and nodes must continue to +reach the contained Gateway with no protocol change. Where the provider's +network posture is unrestricted this is straightforward, but it also means +containment buys nothing at the network layer, and the RFC should not let the +word "contained" imply otherwise. + +**Host reach is genuinely lost.** A contained Gateway cannot see the user's +desktop or, on this provider, the user's files. For an agent expected to work on +the user's behalf in the user's environment, that is a real capability +regression, not a detail. It is the central unresolved question below. + +### Readiness bar + +Contained execution should be recommended for a class of deployment only once: + +- The provider's capability descriptor is accurate, and OpenClaw refuses + configuration the provider cannot enforce. +- Gateway state has defined, tested persistence across + `provision`/`deprovision`, including across host restarts. +- Clients, channels, and nodes connect to a contained Gateway with no protocol + change and no additional user step. +- `attach` makes ordinary CLI use cheap, rather than paying provisioning cost + per command. +- Startup, crash, restart, and teardown paths are covered, including orphaned + environments after an ungraceful host shutdown. +- Containment status is reported in Gateway status and diagnostics, and a + fallback to uncontained execution is loud rather than silent. +- The platform primitive is generally available rather than preview-gated, and + its owner is prepared to describe it as a security boundary. The + `microsoft/mxc` README currently states the opposite for its preview backends, + and that alone blocks any claim that containment is a defense OpenClaw can + rely on today. + +Until then, contained execution should ship as an explicitly experimental, +opt-in posture for deployments that want to exercise it. + +## Rationale + +**Why contain the Gateway rather than only the session.** Per-session sandboxes +were designed for the case where the agent's *work* is untrusted. The case that +motivates this RFC is where the agent's *judgment* is untrusted, because +judgment is what prompt injection attacks. Once that is the threat, the +component holding the credentials and choosing the actions has to be inside a +boundary too. + +**Why a provider seam rather than a Windows code path.** The problem is not +Windows-specific; only this particular primitive is. macOS and Linux have +different mechanisms with different capability profiles, and the industry is +actively producing more of them. A seam with an explicit capability descriptor +lets those arrive as contributions, keeps Windows-specific lifecycle code out of +the Gateway's startup path, and forces each provider to state its limits in a +form OpenClaw can act on. Absent a seam, the first implementation becomes the de +facto contract, and its unstated assumptions get baked in. + +**Why Isolation Sessions rather than WSL.** WSL is the current recommended local +Gateway path on Windows and works, but it contains by moving the Gateway into a +second operating system. That brings its own filesystem, package management, +credential storage, update cadence, and failure modes, and it distances the +Gateway from the Windows environment the user works in. An isolation session +keeps the Gateway on Windows and changes only the principal it runs as, which is +the narrower change and the one that matches the actual threat. + +**Why Isolation Sessions rather than a VM or Windows Sandbox.** Both provide a +stronger boundary than an identity boundary, and for some deployments that is +the right answer. They are a poor default for an always-on daemon: the resource +cost is charged continuously, startup is slow enough to be noticed, and their +lifecycle model is oriented toward disposable sessions rather than a service the +user expects to already be running. The value of the isolation-session primitive +is that its cost profile makes contained execution plausible as an eventual +default rather than an expert-only mode. + +**Why not AppContainer or a process-level container.** Process-level containment +on Windows restricts a process that still runs as the user. It is useful, and +MXC's default Windows backend uses it, but the principal is unchanged, so the +per-user token reach that motivates this RFC is not removed. Isolation sessions +change the principal, which is the property being argued for. + +**Why fail-closed by default.** A containment control that silently degrades to +no containment produces the worst outcome: an operator who believes they are +protected and is not. Failing closed makes the absence of the boundary a visible +event. Deployments that prefer availability can opt out, having said so. + +**Why argue the direction before the primitive is ready.** The seam, the +capability descriptor, and the readiness bar are useful independently of any one +provider, and they are what determine whether contained execution is adopted +honestly. Designing them while the first provider is still preview-quality is +cheaper than retrofitting them around a shipped Windows-specific implementation. + +## Unresolved questions + +- **How does a contained Gateway reach the user's files?** This provider has no + host-folder-sharing primitive. An agent that cannot open the user's working + files is not useful for most of what OpenClaw is used for. Is the answer a + future OS sharing primitive, an explicit user-mediated projection of selected + paths, a filesystem bridge over the existing protocol similar to the remote + filesystem bridge already used for sandboxes, or an accepted restriction to + deployments where the Gateway operates on its own workspace? +- **Where does Gateway state live?** If the OS-assigned account is removed at + deprovision, what persists configuration, credentials, and pairing records + across restarts, and what is the migration path for an existing uncontained + installation adopting containment? +- **What is the correct network posture?** The provider enforces nothing today + and the Gateway's endpoints are reachable via localhost. Should OpenClaw + require a network-capable provider before recommending containment, or is an + identity-only boundary sufficient for the stated threat? +- **How do Windows node capabilities survive?** Computer use, screen capture, + and input injection (cf. [RFC 0025](0025-default-pluggable-computer-use.md)) + require the user's desktop, which contained code cannot reach. Does the + Windows node remain outside the boundary and connect to the contained Gateway + as a node, and what does that imply for the boundary's value? +- **What are the availability and gating rules?** The primitive is preview-gated + and requires a recent Windows build. How does OpenClaw express a containment + posture that is unavailable on most hosts without fragmenting the Windows + experience? +- **How is autostart handled?** A Gateway expected to run in the background must + start without an interactive logon. What does contained startup look like + before or without a signed-in user? +- **Who owns the provider implementation?** Does it live in the OpenClaw + repository, alongside the Windows packaging work, or in a separate + platform-integration component, and what is the dependency and support + boundary? +- **What is the acceptance criterion for calling this a security boundary?** The + platform currently declines to make that claim for preview profiles. OpenClaw + should state in advance what evidence it requires before describing contained + execution as a defense to users. From 53ee5ece4e070aed8ce14f93156d1194f15a261e Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Tue, 18 Aug 2026 15:12:25 -0700 Subject: [PATCH 04/15] docs: address RFC review findings for 0032 Reconciles the proposal with openclaw/openclaw#42026 (control plane and per-agent runtime split), which occupies the same decision space: that issue partitions which component holds which secrets, while this RFC changes what principal a component runs as. The seam is now written against a unit of containment so it survives that split. Adds the sections the openclaw-rfc readiness gates required: - Threat model and residual risk, including the loopback reachability that survives containment and was previously stated as a fact without drawing the conclusion. - Compatibility, migration, and rollback. - A staged implementation plan. - An explicit decision requested from maintainers. - Ownership and the core-versus-plugin justification, replacing the unresolved question about who owns the provider. - Capability descriptor versioning and a closed failure taxonomy, so policy rejection can never fall back to a weaker boundary. Unresolved questions are now split into blocking and non-blocking, and record that the RFC number needs maintainer confirmation because the repository's allocation is not sequential in practice. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 308 ++++++++++++++++-- 1 file changed, 275 insertions(+), 33 deletions(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index d6aa61c8..6a0693da 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -23,10 +23,12 @@ the Gateway inside an OS-managed containment boundary instead of directly as the user. Windows Isolation Sessions are proposed as the first provider: the operating system provisions a fresh, OS-assigned agent account, runs the Gateway in a dedicated session bound to that account, and tears the session and account -down when the owning process exits. The seam and the argument are the proposal; -the Windows provider is currently preview-quality, so this RFC defines the -readiness bar that adoption must clear rather than asking for immediate default -adoption. +down when the owning process exits. The seam is written against a *unit of +containment* rather than a monolithic Gateway, so it survives a future split of +the Gateway into a control plane and per-agent runtimes. The seam and the +argument are the proposal; the Windows provider is currently preview-quality, so +this RFC defines the readiness bar that adoption must clear rather than asking +for immediate default adoption. ## Motivation @@ -103,16 +105,35 @@ re-paying the provisioning / session-start cost each time" — but that is a cos paid once for a daemon that then stays up. For a long-lived agent process, that is the trade OpenClaw has been missing. -### This composes with packaging, and does not duplicate it +### This composes with neighbouring proposals rather than duplicating them + +Two active proposals touch the same area, and neither makes this one redundant. [RFC PR #58](https://github.com/openclaw/rfcs/pull/58) proposes MSIX packaging for Windows and explicitly lists "Defining runtime isolation. A separate RFC may -define a session-based runtime model" among its non-goals. The two proposals -answer different questions. Packaging gives the installation a reviewable -identity so an administrator can inventory, approve, update, and remove it. -Containment reduces what that installation can reach while it runs. An approved, -signed, inventoried Gateway with the user's full token is still an -unbounded-blast-radius component. +define a session-based runtime model" among its non-goals. Packaging gives the +installation a reviewable identity so an administrator can inventory, approve, +update, and remove it. Containment reduces what that installation can reach +while it runs. An approved, signed, inventoried Gateway holding the user's full +token is still an unbounded-blast-radius component. + +[`openclaw/openclaw#42026`](https://github.com/openclaw/openclaw/issues/42026) +proposes splitting the gateway into a control plane and per-agent runtimes so +each agent can run in its own container, VM, or process, and argues for "true +secret isolation" in which one agent's credentials never coexist with another's. +That is a real reduction in blast radius, and it is a different axis from this +proposal. Decomposition partitions *which component holds which secrets*; +containment changes *what principal a component runs as*. A fully decomposed +deployment on Windows still runs every control plane and every runtime as the +signed-in user, so each piece keeps the same reach into that user's files, +registry, and tokens — the partition is horizontal, between agents, and does not +cross the user boundary. Conversely, containment without decomposition places +all agents behind one contained identity. + +The two therefore compose, and the seam proposed here is deliberately written +against a *unit of containment* rather than against a monolithic Gateway. If +#42026 lands, the unit becomes the runtime, optionally the control plane, and +the contract below is unchanged. ## Goals @@ -128,20 +149,29 @@ unbounded-blast-radius component. protocol behavior are unchanged by containment. - Define explicit selection, fallback, and diagnostic behavior, including what happens when a requested provider is unavailable on the host. +- Keep the seam neutral about the unit of containment so it remains valid if the + Gateway is later split into a control plane and per-agent runtimes. +- Preserve existing uncontained deployments unchanged, with a defined migration + and rollback path for those that opt in. - Define the readiness bar that a provider must clear before contained execution can be recommended, and later defaulted, for a class of deployment. ## Non-Goals - **Defining the packaging or launch mechanism.** How a Gateway build is - packaged, signed, distributed, and started on Windows belongs to - [PR #58](https://github.com/openclaw/rfcs/pull/58) and its implementation. - This RFC describes the containment contract and deliberately does not specify - the process that establishes it. + packaged, signed, distributed, and started on Windows belongs to [PR + #58](https://github.com/openclaw/rfcs/pull/58) and its implementation. This + RFC describes the containment contract and deliberately does not specify the + process that establishes it. - **Replacing per-session sandboxes.** `SandboxBackend` and the worker-provider direction in [PR #55](https://github.com/openclaw/rfcs/pull/55) contain a session's work. This RFC contains the Gateway. They compose; neither substitutes for the other. +- **Deciding how the Gateway is decomposed.** Whether the Gateway splits into a + control plane and per-agent runtimes is + [`openclaw/openclaw#42026`](https://github.com/openclaw/openclaw/issues/42026) + to settle. This RFC defines a contract that applies to whatever the resulting + deployable unit is. - **Specifying the Windows API surface.** The OS API is owned by Windows and is consumed, not defined, here. - **Requiring containment, or making it the default, in this RFC.** The Windows @@ -157,9 +187,14 @@ unbounded-blast-radius component. ### What moves, and what does not -Containment applies to the Gateway process and everything it hosts in-process: -the agent loop, plugin code, the scheduler, and the Gateway's own working state. -The following explicitly stay outside the boundary and are unchanged: +The unit of containment is the deployable process that holds credentials and +runs the agent loop. Today that is the Gateway. If +[#42026](https://github.com/openclaw/openclaw/issues/42026) splits it, the unit +becomes each per-agent runtime, and optionally the control plane, without +changing the contract below. Containment applies to that unit and everything it +hosts in-process: the agent loop, plugin code, the scheduler, and its own +working state. The following explicitly stay outside the boundary and are +unchanged: - Client, channel, and node connections, which continue to reach the Gateway through its existing endpoints. @@ -192,6 +227,13 @@ silently. This mirrors the disposition MXC already takes for its own backends, where unenforceable policy is rejected rather than quietly dropped, and it is the property that makes the descriptor trustworthy. +The descriptor is versioned. A provider reports the descriptor schema version it +implements, and OpenClaw refuses to run against a descriptor whose version it +does not understand rather than assuming absent fields mean "unsupported" — an +unrecognised capability must never be silently downgraded into a claim about the +boundary. Adding a capability is a minor version change; changing the meaning of +an existing one is a major version change and requires a provider update. + **Lifecycle.** Providers implement a lifecycle that a long-lived daemon can use: ``` @@ -200,10 +242,31 @@ probe -> provision -> start -> attach -> stop -> deprovision `probe` reports availability and the capability descriptor for the current host, without side effects. `provision` creates the isolated principal and -environment. `start` launches the Gateway inside it. `attach` reconnects to an -already-running contained Gateway across separate CLI invocations, so ordinary -commands do not each pay provisioning cost. `stop` and `deprovision` end the -Gateway and release the environment. +environment. `start` launches the unit of containment inside it. `attach` +reconnects to an already-running contained unit across separate CLI invocations, +so ordinary commands do not each pay provisioning cost. `stop` and `deprovision` +end the process and release the environment. + +Two invariants make the lifecycle safe to drive from a supervisor. Every +operation is idempotent: `provision` on an already-provisioned environment +returns the existing one, and `stop` or `deprovision` on an absent one succeeds. +And `deprovision` is the only destructive step, so a supervisor may retry any +other operation without risking state. + +**Failure semantics.** Providers report a small closed set of outcomes so +OpenClaw can act on them without parsing provider text: + +| Outcome | Meaning | OpenClaw behavior | +|---|---|---| +| `unavailable` | The provider cannot run on this host: absent, feature-gated off, or unsupported build. | Fail closed unless fallback is configured. Report the reason. | +| `policy_rejected` | Configuration was supplied that this provider cannot enforce. | Fail closed always. Never downgrade to a weaker boundary. | +| `stale` | The referenced environment no longer exists. | Re-provision if the caller asked to start; otherwise surface. | +| `lifecycle_failed` | A lifecycle operation failed for an operational reason. | Retry per policy, then fail closed. | + +`policy_rejected` never falls back, even when fallback is configured. A +deployment that asked for a boundary the provider cannot deliver has a +configuration error, not an availability problem, and silently running it with a +weaker boundary is the outcome this contract exists to prevent. **Selection and fallback.** Containment is selected explicitly by configuration, never inferred. When a selected provider's `probe` fails, the default is @@ -291,6 +354,32 @@ desktop or, on this provider, the user's files. For an agent expected to work on the user's behalf in the user's environment, that is a real capability regression, not a detail. It is the central unresolved question below. +### Ownership and why this belongs in core + +The seam belongs in core; individual providers do not. + +Core owns the `GatewayContainmentProvider` interface, the capability descriptor +and its schema version, the failure taxonomy, provider selection, the +fail-closed rule, and the reporting of containment posture in status and +diagnostics. This is the minimum that cannot live outside core: it decides +whether the Gateway starts at all, it is the thing that must refuse to downgrade +a security posture silently, and its guarantees are only as good as the +component that enforces them. A containment boundary implemented by an optional, +independently versioned plugin would be a boundary the Gateway could be +persuaded to skip, which defeats the argument in the Motivation. + +Individual providers are platform integrations and should live wherever their +platform dependency is maintainable — for the Windows provider, alongside the +Windows packaging work rather than in the cross-platform Gateway, so that a +preview OS dependency and its binding regeneration do not become a build +requirement for every OpenClaw contributor. The seam must therefore support an +out-of-tree provider without special-casing it. + +This satisfies the usual test for core ownership: the behavior is mandatory for +the security and trust model rather than optional, it is a public contract with +more than one credible implementer (Windows now, macOS and Linux primitives +later), and it is not a one-off hook added for a single consumer. + ### Readiness bar Contained execution should be recommended for a class of deployment only once: @@ -316,6 +405,138 @@ Contained execution should be recommended for a class of deployment only once: Until then, contained execution should ship as an explicitly experimental, opt-in posture for deployments that want to exercise it. +## Threat model and residual risk + +The threat this addresses is a Gateway that is induced to act against the user: +prompt injection reaching the agent loop, a hostile or compromised plugin or +skill, or a supply-chain compromise of a dependency loaded in-process. The +attacker is assumed to achieve arbitrary code execution inside the Gateway +process. Attackers who already hold the user's credentials, local administrator, +or kernel-level access are out of scope, as is a malicious OpenClaw build. + +### What the identity boundary removes + +Contained, the attacker no longer runs as the signed-in user. Reads and writes +against the user's profile, `HKCU`, browser and credential stores, SSH keys, +startup entries, and any resource whose ACL grants that user are refused by the +operating system rather than by OpenClaw's own policy. On this provider the +attacker also cannot observe or drive the user's desktop, clipboard, or input, +which removes screen-scraping and synthetic-input paths to the user's other +applications. When the session is deprovisioned, the account and its session +state go with it, so filesystem persistence inside the boundary is not durable. + +### What the attacker still has + +This is an identity boundary and nothing more. The following survive it: + +- **Everything the Gateway legitimately holds.** Provider API keys, channel + credentials, pairing records, and conversation history are inside the boundary + by construction. Containment limits reach into the *user's* assets; it does + not protect the Gateway's own secrets from code running as the Gateway. + Credential exfiltration remains fully available. +- **The network, including loopback.** The provider enforces nothing on the + network, and a process inside can reach and be reached over localhost. Host + services that authenticate by origin rather than by credential — development + servers, local databases, metadata or agent endpoints, other OpenClaw + Gateways, and local MCP servers — remain reachable from inside the boundary. + For a workstation with local services listening on loopback, this is the + largest surviving path, and it means containment must not be described as a + network control. +- **Outbound egress.** Exfiltration to the internet is unrestricted. +- **Whatever is deliberately shared in.** Any future host-path sharing + reintroduces exactly the reach it projects, and should be treated as reopening + the boundary for those paths rather than as a convenience feature. +- **The user's own actions.** A contained agent can still return output that + induces the user to run something themselves. + +### Consequences + +Two follow directly. First, containment reduces blast radius but does not reduce +the value of the credentials the Gateway holds, so credential hygiene, scoping, +and rotation remain as important as before. Second, because loopback survives, a +deployment that relies on local services trusting their callers gets materially +less protection than the headline claim suggests, and should be told so rather +than left to infer it. + +Finally, the platform primitive is preview-gated and its own documentation +declines to describe its profiles as security boundaries today. Until that +changes, contained execution should be presented as defense in depth and a +direction of travel, not as a control a deployment may rely on. + +## Compatibility, migration, and rollback + +**Existing deployments are unaffected.** Containment is opt-in and off by +default. A deployment that does not configure a provider behaves exactly as it +does today, on every platform, with no new dependency and no change to startup. + +**Adoption is a migration, not a flag.** Turning containment on moves the +Gateway to a new principal, so state that lived under the user's profile is not +automatically visible to it. Before this can be recommended, the implementation +must provide a supported path that relocates or re-establishes configuration, +credentials, and pairing records for the contained identity, and must report +clearly when it cannot. Because the Windows provider's `statePersistence` is +unresolved, this is a blocking dependency rather than a detail — a migration +that silently produces an empty Gateway would look identical to a working one +until the first channel fails to authenticate. + +**Rollback must be routine.** Disabling containment must return the deployment +to its previous uncontained behavior without data loss, which requires that +adoption never destroys the pre-migration state as part of moving it. Rollback +should be a supported operation exercised in testing, not a recovery procedure +discovered during an incident. + +**Downgrade.** An OpenClaw version that predates the seam ignores the +configuration and starts uncontained. That is a silent posture change of exactly +the kind this proposal argues against, so the configuration must be rejected as +unknown rather than ignored on versions that cannot honor it, and the readiness +bar should not be considered met until that behavior exists. + +**No protocol or wire compatibility impact.** Clients, channels, and nodes are +unchanged, so a contained and an uncontained Gateway are indistinguishable to +them. + +## Implementation plan + +Staged so that each phase produces something reviewable and none of them +requires the preview OS dependency to be present. + +1. **Contract only.** Land the provider interface, versioned capability + descriptor, failure taxonomy, selection and fail-closed logic, and posture + reporting in status and diagnostics, with an in-tree null provider. Proof: + unit coverage for selection, refusal, and reporting, exercised through the + real interface rather than through source inspection. +2. **Windows provider, behind experimental opt-in.** Implement `probe`, + `provision`, `start`, `attach`, `stop`, and `deprovision` against the OS + primitive, reporting the honest descriptor. Proof: a Gateway that starts + contained on a capable build, is reachable from a client, and reports its + posture; and a capable-build-absent host that fails closed with the reason. +3. **State and migration.** Resolve persistence, then implement adoption and + rollback with tested restart, upgrade, and downgrade behavior. Proof: a + contained Gateway that survives host restart with its pairing intact, and a + rollback that restores the prior deployment. +4. **Readiness review.** Re-evaluate against the readiness bar and decide + whether contained execution can be recommended for any deployment class. + +Phase 1 is owned by whoever owns Gateway startup; phases 2 and 3 need a Windows +owner with access to the platform primitive. Phase 3 should not begin while the +persistence question is open, and phase 4 is a maintainer decision rather than +an implementation task. + +## Decision requested + +This RFC asks maintainers for one decision: **is an opt-in, capability-honest +Gateway containment seam a direction OpenClaw wants to own in core, given that +the first available provider offers identity isolation only and is +preview-gated?** + +A "yes" authorizes phase 1 — the contract, with no provider and no default +change. It does not commit OpenClaw to the Windows provider, to a default +posture, or to a timeline. + +Two secondary decisions follow only from a "yes": whether providers may live +out-of-tree, and whether this RFC's number is correct given the repository's +current numbering practice. Both are noted rather than assumed. + ## Rationale **Why contain the Gateway rather than only the session.** Per-session sandboxes @@ -357,6 +578,17 @@ MXC's default Windows backend uses it, but the principal is unchanged, so the per-user token reach that motivates this RFC is not removed. Isolation sessions change the principal, which is the property being argued for. +**Why not rely on splitting the Gateway instead.** +[#42026](https://github.com/openclaw/openclaw/issues/42026) would give each +agent its own runtime and its own secrets, which is a genuine reduction in blast +radius and independently worth doing. It is not a substitute, because it +partitions between agents while leaving every resulting process running as the +signed-in user. Decomposition means a compromised agent cannot reach another +agent's secrets; containment means it cannot reach the user's. A deployment +wants both, and adopting either does not foreclose the other. Writing the seam +against a unit of containment rather than against "the Gateway" is what keeps +that true. + **Why fail-closed by default.** A containment control that silently degrades to no containment produces the worst outcome: an operator who believes they are protected and is not. Failing closed makes the absence of the boundary a visible @@ -370,21 +602,27 @@ cheaper than retrofitting them around a shipped Windows-specific implementation. ## Unresolved questions +### Blocking — these gate the readiness bar + - **How does a contained Gateway reach the user's files?** This provider has no host-folder-sharing primitive. An agent that cannot open the user's working files is not useful for most of what OpenClaw is used for. Is the answer a future OS sharing primitive, an explicit user-mediated projection of selected paths, a filesystem bridge over the existing protocol similar to the remote filesystem bridge already used for sandboxes, or an accepted restriction to - deployments where the Gateway operates on its own workspace? + deployments where the Gateway operates on its own workspace? Whichever is + chosen, it reopens the boundary for whatever it projects. - **Where does Gateway state live?** If the OS-assigned account is removed at deprovision, what persists configuration, credentials, and pairing records - across restarts, and what is the migration path for an existing uncontained - installation adopting containment? -- **What is the correct network posture?** The provider enforces nothing today - and the Gateway's endpoints are reachable via localhost. Should OpenClaw - require a network-capable provider before recommending containment, or is an - identity-only boundary sufficient for the stated threat? + across restarts? The migration and rollback design above depends on this + answer, so it blocks implementation phase 3. +- **Is an identity-only boundary worth adopting?** The provider enforces nothing + on the network and loopback survives containment. Should OpenClaw require a + network-capable provider before recommending containment for any deployment + class, or is the reduction in user-asset reach sufficient on its own? + +### Non-blocking — these can be resolved during implementation + - **How do Windows node capabilities survive?** Computer use, screen capture, and input injection (cf. [RFC 0025](0025-default-pluggable-computer-use.md)) require the user's desktop, which contained code cannot reach. Does the @@ -397,11 +635,15 @@ cheaper than retrofitting them around a shipped Windows-specific implementation. - **How is autostart handled?** A Gateway expected to run in the background must start without an interactive logon. What does contained startup look like before or without a signed-in user? -- **Who owns the provider implementation?** Does it live in the OpenClaw - repository, alongside the Windows packaging work, or in a separate - platform-integration component, and what is the dependency and support - boundary? +- **Should providers live out-of-tree?** The Ownership section proposes that + platform providers live with their platform dependency rather than in the + cross-platform Gateway. Maintainers should confirm that split and the support + boundary it implies. - **What is the acceptance criterion for calling this a security boundary?** The platform currently declines to make that claim for preview profiles. OpenClaw should state in advance what evidence it requires before describing contained execution as a defense to users. +- **Is this RFC number correct?** It was chosen as the lowest unclaimed integer, + but the repository currently has several open proposals sharing a number, so + the allocation rule is not obviously sequential. A maintainer should confirm + or reassign it. From 63b6402c1a849a273522fdd90e4071e833a0b660 Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Tue, 18 Aug 2026 15:21:47 -0700 Subject: [PATCH 05/15] docs: ground RFC 0032 in how containment already works Corrects the RFC's central factual claim. It said the provider has "no host-folder-sharing primitive"; the public MXC documentation actually describes a per-sandbox staging directory shared between the calling user and the isolated user, with asymmetric visibility, created at provision and deleted at deprovision. That distinction drives several design changes: - Split the capability descriptor's host-path capability into hostPathProjection and stagingChannel, so a provider that can hand a file across the boundary is not mistaken for one that can give an agent the user's working tree. - Settle where durable state lives: outside the boundary, host-side, because both the account and the staging directory are destroyed at deprovision. The host-side store then holds the credentials containment is meant to protect, which the threat model now says plainly. - Treat anything the host reads back from the staging channel as untrusted input, and add that to the readiness bar. - Require that the Gateway is materialized inside the boundary rather than run from the ephemeral, caller-writable staging directory. - Document addressing and reuse: provision returns an identifier the caller persists, reports whether it reused an identity, and the environment outlives the process that started it, so orphan reconciliation is an obligation rather than an edge case. - Make the link to the MSIX packaging RFC mechanical rather than thematic: package identity is an input to provisioning, accepted so a future OS contract can act on the calling application's identity. - Reserve workloadIdentity, since the provider can give the contained unit an identity of its own instead of the caller's. All claims remain sourced to the public microsoft/mxc documentation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 173 ++++++++++++++---- 1 file changed, 136 insertions(+), 37 deletions(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index 6a0693da..4b0ce747 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -115,7 +115,11 @@ define a session-based runtime model" among its non-goals. Packaging gives the installation a reviewable identity so an administrator can inventory, approve, update, and remove it. Containment reduces what that installation can reach while it runs. An approved, signed, inventoried Gateway holding the user's full -token is still an unbounded-blast-radius component. +token is still an unbounded-blast-radius component. The two are also joined +mechanically rather than only thematically: the containment provider accepts the +calling application's package identity at provision time, specifically so a +future OS contract can act on it, which makes a stable package identity an input +to containment rather than a parallel concern. [`openclaw/openclaw#42026`](https://github.com/openclaw/openclaw/issues/42026) proposes splitting the gateway into a control plane and per-agent runtimes so @@ -161,8 +165,8 @@ the contract below is unchanged. - **Defining the packaging or launch mechanism.** How a Gateway build is packaged, signed, distributed, and started on Windows belongs to [PR #58](https://github.com/openclaw/rfcs/pull/58) and its implementation. This - RFC describes the containment contract and deliberately does not specify the - process that establishes it. +RFC describes the containment contract and deliberately does not specify the +process that establishes it. - **Replacing per-session sandboxes.** `SandboxBackend` and the worker-provider direction in [PR #55](https://github.com/openclaw/rfcs/pull/55) contain a session's work. This RFC contains the Gateway. They compose; neither @@ -216,12 +220,20 @@ because an overstated boundary is worse than no boundary. At minimum: | Capability | Meaning | |---|---| | `identityIsolation` | Whether the contained Gateway runs as a principal distinct from the invoking user, and whether that principal is per-instance or shared. | -| `statePersistence` | Whether Gateway state survives the containment lifecycle, and across what unit — instance, host, or not at all. | -| `hostPathSharing` | Whether, and how, host paths can be projected into the boundary. | +| `statePersistence` | Whether state inside the boundary survives the containment lifecycle, and across what unit — instance, host, or not at all. | +| `hostPathProjection` | Whether arbitrary host paths can be mapped into the boundary. | +| `stagingChannel` | Whether the provider offers a directory for moving files across the boundary, its lifetime, and which side can see it. | | `networkPosture` | What the provider can enforce on inbound and outbound network access. Explicitly includes "nothing". | | `hostUiReach` | Whether contained code can observe or drive the user's desktop, clipboard, and input. | +| `workloadIdentity` | Whether the contained unit can be given an identity of its own rather than borrowing the caller's. | | `lifecycleOwner` | Whether teardown is guaranteed by the OS or must be driven by OpenClaw. | +`hostPathProjection` and `stagingChannel` are deliberately separate. A provider +may offer a way to hand a file across the boundary without being able to give +the contained unit access to the user's working tree, and treating those as one +capability is how a deployment ends up believing it has the second when it only +has the first. + A provider must decline configuration it cannot enforce rather than accepting it silently. This mirrors the disposition MXC already takes for its own backends, where unenforceable policy is rejected rather than quietly dropped, and it is @@ -253,6 +265,22 @@ returns the existing one, and `stop` or `deprovision` on an absent one succeeds. And `deprovision` is the only destructive step, so a supervisor may retry any other operation without risking state. +**Addressing and reuse.** `provision` returns an opaque identifier that the +caller persists and uses to address the environment in every later phase, and it +reports whether it created a new isolated identity or reused an existing one. +This is what makes `attach` work across separate CLI invocations, and it is not +theoretical: the Windows provider's underlying lifecycle is explicitly designed +so a provisioned session can host multiple executions across separate caller +processes rather than re-paying provisioning cost per command. A contained +Gateway is therefore a durable environment that outlives the process that +started it, and the supervisor's job is to re-address it, not to recreate it. + +The corollary is that the environment does not go away on its own. Because +`deprovision` is explicit and destructive, a crashed or forgotten supervisor +leaves a provisioned environment behind. Reconciling those orphans is an +implementation obligation, not an edge case, and is called out in the readiness +bar. + **Failure semantics.** Providers report a small closed set of outcomes so OpenClaw can act on them without parsing provider text: @@ -322,26 +350,67 @@ the signed-in user's grants. The boundary this provider delivers is an **identity** boundary, and the capability descriptor must say exactly that. Per the public MXC documentation -for this backend, it has no host-folder-sharing primitive and rejects filesystem -policy outright; its network is unrestricted, with a process inside able to -listen on a port reachable via localhost; and it exposes no UI-restriction +for this backend, its network is unrestricted, with a process inside able to +listen on a port reachable via localhost, and it exposes no UI-restriction primitive, though contained code cannot reach the host's desktop or clipboard. So the honest descriptor is per-instance identity isolation and OS-owned -teardown, with `hostPathSharing` and `networkPosture` reported as unsupported. +teardown, with `networkPosture` reported as unsupported. + +`hostPathSharing` is more nuanced than "unsupported", and the distinction drives +much of the design below. The backend rejects every filesystem policy field, so +a caller cannot project an arbitrary host path — the user's documents folder +cannot be mapped in. What it does provide is a single OS-created staging +directory per sandbox, documented as "a directory shared between the calling +user and this isolated agent user, through which the caller can stage files into +the session", with three properties that matter: + +- **Asymmetric visibility.** Each isolated user can access only its own + workspace, while the caller can access every concurrent sandbox's workspace. + The caller is the more privileged side. +- **Ephemeral.** It is created at provision and deleted at deprovision, so it is + a staging channel, not storage. +- **Not the working directory.** It does not change where the workload runs. + +So the descriptor should report `hostPathSharing` as a *staging channel* rather +than as path projection, and the seam must model those as different +capabilities. A provider that can stage a file in is not a provider that can +give an agent access to the user's working tree, and conflating them would let a +deployment believe the second was available when only the first is. + +Two further provider inputs are worth recording because they shape the contract. +Provisioning accepts an optional application identifier, documented as "the +Package Family Name for a packaged app", carried verbatim so that "a future OS +contract acting on the calling application's identity needs no breaking change" +— which is a direct, concrete link to the package identity proposed in +[#58](https://github.com/openclaw/rfcs/pull/58) rather than a thematic one. +Provisioning also accepts an optional user-identity bundle, which is the hook by +which a contained unit could carry an identity of its own instead of borrowing +the caller's. ### Obligations this creates Naming the boundary honestly surfaces three obligations that the seam must account for and that the Windows provider does not yet satisfy. -**State must outlive the environment.** A Gateway is not a one-shot workload. -Its configuration, credentials, pairing records, and session history must -survive restarts. If deprovisioning removes the account that owns that state, -then either state lives outside the boundary through a mechanism the provider -declares, or contained execution is limited to deployments that can tolerate -losing it. The seam must therefore treat `statePersistence` as a first-class -capability rather than an implementation footnote, and OpenClaw must not assume -state survives unless the provider says it does. +**State must outlive the environment, so it has to live outside it.** A Gateway +is not a one-shot workload: its configuration, credentials, pairing records, and +session history must survive restarts. On this provider, both the isolated +account and the staging directory are destroyed at deprovision, so nothing kept +inside the boundary is durable. That settles the design rather than leaving it +open — durable state belongs to the host side, outside the boundary, and is +staged in when the environment is created. Two consequences follow. The store +becomes a boundary-crossing asset that must be protected as carefully as the +Gateway itself, since it holds exactly the credentials the containment is +supposed to be worth protecting. And the seam must treat `statePersistence` as a +declared capability rather than an assumption, because a provider that *does* +offer durable in-boundary state should not be forced through host-side staging. + +**Do not run the Gateway from the staging channel.** The staging directory is +ephemeral, visible to the more privileged caller, and explicitly not the +workload's working directory. Application content should be staged through it +and then materialized inside the boundary, so that the running Gateway does not +depend at runtime on a directory the host can rewrite underneath it and that +disappears at deprovision. **Reachability must be explicit.** Clients, channels, and nodes must continue to reach the contained Gateway with no protocol change. Where the provider's @@ -349,10 +418,15 @@ network posture is unrestricted this is straightforward, but it also means containment buys nothing at the network layer, and the RFC should not let the word "contained" imply otherwise. -**Host reach is genuinely lost.** A contained Gateway cannot see the user's -desktop or, on this provider, the user's files. For an agent expected to work on -the user's behalf in the user's environment, that is a real capability -regression, not a detail. It is the central unresolved question below. +**Host reach is reduced to a staging channel.** A contained Gateway cannot see +the user's desktop, and it cannot open the user's working tree, because +arbitrary host paths cannot be projected in. What remains is a directory through +which the host can hand it files. For an agent expected to work on the user's +behalf in the user's environment, that is a real capability regression: "the +user asks the agent to fix a file in their repository" is not expressible unless +something explicitly stages that content across, which is a product decision +about mediated access rather than a transparent capability. It remains the +central unresolved question below. ### Ownership and why this belongs in core @@ -387,7 +461,13 @@ Contained execution should be recommended for a class of deployment only once: - The provider's capability descriptor is accurate, and OpenClaw refuses configuration the provider cannot enforce. - Gateway state has defined, tested persistence across - `provision`/`deprovision`, including across host restarts. + `provision`/`deprovision`, including across host restarts, and the host-side + store holding it is protected commensurately with the credentials it contains. +- Orphaned environments left by a crashed or replaced supervisor are reconciled + rather than accumulating, since `deprovision` is explicit and no other party + performs it. +- Anything the host reads back from the staging channel is treated as untrusted + input, with the parsing boundary identified and tested. - Clients, channels, and nodes connect to a contained Gateway with no protocol change and no additional user step. - `attach` makes ordinary CLI use cheap, rather than paying provisioning cost @@ -443,9 +523,18 @@ This is an identity boundary and nothing more. The following survive it: largest surviving path, and it means containment must not be described as a network control. - **Outbound egress.** Exfiltration to the internet is unrestricted. -- **Whatever is deliberately shared in.** Any future host-path sharing - reintroduces exactly the reach it projects, and should be treated as reopening - the boundary for those paths rather than as a convenience feature. +- **The staging channel, in both directions.** The shared directory crosses the + boundary, and its visibility is asymmetric in the caller's favour: each + contained unit sees only its own, while the caller sees every concurrent one. + Contained code can therefore write into a directory that a host-side process + running as the user will later read, so anything the host parses from it is + untrusted input — the most likely place for a boundary-crossing bug. Any + future arbitrary path projection reopens the boundary for exactly what it + projects and should be treated as such rather than as a convenience. +- **The host-side state store.** Because durable state cannot live inside the + boundary, it lives on the host and holds the credentials containment is meant + to make less valuable to steal. Compromising the host side recovers them, so + containment moves the crown jewels rather than eliminating them. - **The user's own actions.** A contained agent can still return output that induces the user to run something themselves. @@ -604,18 +693,20 @@ cheaper than retrofitting them around a shipped Windows-specific implementation. ### Blocking — these gate the readiness bar -- **How does a contained Gateway reach the user's files?** This provider has no - host-folder-sharing primitive. An agent that cannot open the user's working - files is not useful for most of what OpenClaw is used for. Is the answer a - future OS sharing primitive, an explicit user-mediated projection of selected - paths, a filesystem bridge over the existing protocol similar to the remote - filesystem bridge already used for sandboxes, or an accepted restriction to - deployments where the Gateway operates on its own workspace? Whichever is - chosen, it reopens the boundary for whatever it projects. -- **Where does Gateway state live?** If the OS-assigned account is removed at - deprovision, what persists configuration, credentials, and pairing records - across restarts? The migration and rollback design above depends on this - answer, so it blocks implementation phase 3. +- **How does a contained Gateway reach the user's files?** Arbitrary host paths + cannot be projected in; what exists is a staging directory the host can write + into. So the question is not whether sharing is possible but what the product + should do with a mediated channel: stage an explicitly selected working set in + and results out, run a filesystem bridge over the existing protocol similar to + the remote filesystem bridge already used for sandboxes, wait for a future OS + projection primitive, or accept that a contained Gateway operates only on its + own workspace. Each reopens the boundary to a different degree, and the choice + determines how useful contained execution actually is. +- **What holds durable state, and how is it protected?** State must live + host-side because the account and its staging directory are destroyed at + deprovision. What is that store, how is it protected given that it holds the + Gateway's credentials, and how does an existing uncontained installation + migrate into it? This blocks implementation phase 3. - **Is an identity-only boundary worth adopting?** The provider enforces nothing on the network and loopback survives containment. Should OpenClaw require a network-capable provider before recommending containment for any deployment @@ -632,6 +723,14 @@ cheaper than retrofitting them around a shipped Windows-specific implementation. and requires a recent Windows build. How does OpenClaw express a containment posture that is unavailable on most hosts without fragmenting the Windows experience? +- **Should the contained Gateway carry its own identity?** The provider accepts + an optional identity bundle at provision, so a contained unit could + authenticate as itself rather than inheriting whatever the caller holds. That + would narrow the credential blast radius the threat model calls out, and it + interacts with the per-agent secret isolation proposed in + [#42026](https://github.com/openclaw/openclaw/issues/42026). Out of scope for + phase 1, but the descriptor reserves `workloadIdentity` so it can be answered + without a breaking change. - **How is autostart handled?** A Gateway expected to run in the background must start without an interactive logon. What does contained startup look like before or without a signed-in user? From bc2fc67a09f1831c7efff3948236a4c4b50750c8 Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Tue, 18 Aug 2026 17:36:46 -0700 Subject: [PATCH 06/15] docs: move containment out of the Gateway and into a launcher The previous revision had OpenClaw core owning the seam that contains the Gateway, which contradicts the RFC's own threat model: if the Gateway is the component treated as compromised, a containment decision it makes is a decision the attacker makes. A check inside the process can be patched, configured away, or never reached, and by the time it would run the untrusted code is already executing with the identity the check was supposed to remove. The same applies to plugins, which load into the process being contained. The boundary is now established before the Gateway exists, by a small trusted launcher outside it: - The launcher owns the ContainmentProvider seam, provider selection, the fail-closed rule, and posture reporting. It probes, provisions, stages, and starts the Gateway inside the boundary. - The Gateway is unchanged and unaware. It has no uncontained mode to reach because it is never started outside the boundary, so phase 1 requires no change to OpenClaw core at all. - On Windows the launcher is the packaged host app already proposed in openclaw/rfcs#58 rather than a new component: #58 defines it as the packaged entry point behind an openclaw.exe execution alias whose job is launching or stopping the packaged Gateway. - Adds bypass resistance. #58 already requires that native `openclaw gateway` commands cannot bypass staged-payload activation; the same requirement extends to containment. Because the Gateway cannot be trusted to refuse to start, this comes from the deployment shape rather than a check inside it. - The threat model now names the launcher as the trusted computing base, so anything added to it is added to the TCB, and package integrity becomes a dependency of this design. - Posture reported by the Gateway is explicitly not evidence, since a compromised Gateway can claim to be contained. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 296 ++++++++++++------ 1 file changed, 205 insertions(+), 91 deletions(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index 4b0ce747..133af2c5 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -17,13 +17,16 @@ OpenClaw contains the work an agent does, but not the process that decides to do it. On Windows the Gateway runs as the signed-in user and inherits that user's full filesystem, registry, and token reach, so every credential, plugin, and model-directed decision executes with the operator's identity. This RFC argues -that the Gateway process itself should be containable, and proposes a -platform-agnostic `GatewayContainmentProvider` seam that lets a deployment run -the Gateway inside an OS-managed containment boundary instead of directly as the -user. Windows Isolation Sessions are proposed as the first provider: the -operating system provisions a fresh, OS-assigned agent account, runs the Gateway -in a dedicated session bound to that account, and tears the session and account -down when the owning process exits. The seam is written against a *unit of +that the Gateway process itself should be contained, and that the component +which establishes that containment must sit outside it. It proposes a small, +trusted **launcher** that owns a platform-agnostic `ContainmentProvider` seam: +the launcher probes the host, provisions an OS-managed boundary, stages the +payload, and starts the Gateway inside — so the Gateway never runs uncontained +and is never asked to contain itself. Windows Isolation Sessions are proposed as +the first provider: the operating system provisions a fresh, OS-assigned agent +account, runs the Gateway in a dedicated session bound to that account, and +tears the session and account down when the owning process exits. The Gateway +itself is unchanged and unaware. The seam is written against a *unit of containment* rather than a monolithic Gateway, so it survives a future split of the Gateway into a control plane and per-agent runtimes. The seam and the argument are the proposal; the Windows provider is currently preview-quality, so @@ -105,21 +108,58 @@ re-paying the provisioning / session-start cost each time" — but that is a cos paid once for a daemon that then stays up. For a long-lived agent process, that is the trade OpenClaw has been missing. +### A compromised Gateway cannot contain itself + +This follows directly from the threat, and it constrains the design more than +anything else in this proposal. + +If the Gateway is the component we are treating as potentially compromised, it +cannot also be the component that decides whether to be contained. A containment +check that runs inside the Gateway is code the attacker already controls: it can +be patched, configured away, exception-handled, or simply never reached, and by +the time it would run the untrusted code is already executing with the identity +the check was supposed to remove. The same applies to a plugin, since plugins +load into the process being contained. + +So the boundary has to be established *before* the Gateway exists, by something +outside it. That something is a launcher: a separate, minimal executable that +runs as the user, provisions the boundary, and starts the Gateway inside it. The +Gateway then has no uncontained mode to fall back to, not because it declines to +use one, but because it is never started outside the boundary in the first +place. Correspondingly, the Gateway needs no containment code, no containment +configuration, and no awareness that it is contained. + +This also sets the standard the launcher must meet. It is the trusted computing +base for this design, so it should stay small, load no plugins, execute no model +output, and do nothing on behalf of the agent. Its job is to establish the +boundary and get out of the way. + ### This composes with neighbouring proposals rather than duplicating them Two active proposals touch the same area, and neither makes this one redundant. [RFC PR #58](https://github.com/openclaw/rfcs/pull/58) proposes MSIX packaging for Windows and explicitly lists "Defining runtime isolation. A separate RFC may -define a session-based runtime model" among its non-goals. Packaging gives the -installation a reviewable identity so an administrator can inventory, approve, -update, and remove it. Containment reduces what that installation can reach -while it runs. An approved, signed, inventoried Gateway holding the user's full -token is still an unbounded-blast-radius component. The two are also joined -mechanically rather than only thematically: the containment provider accepts the -calling application's package identity at provision time, specifically so a -future OS contract can act on it, which makes a stable package identity an input -to containment rather than a parallel concern. +define a session-based runtime model" among its non-goals. It also already +describes the component this proposal needs: a package-specific host app that is +"the packaged entry point", registered behind an `openclaw.exe` execution alias, +whose responsibilities are "package activation, payload verification and +staging, and launching or stopping the packaged Gateway", and which explicitly +"does not proxy Gateway traffic, distribute Gateway credentials, or approve +clients, nodes, or channel users". + +That host app is the launcher this design requires. This RFC does not propose a +second one, and it does not depend on MSIX: it proposes that whatever launches +the Gateway on a platform also establish the containment boundary first, and it +defines the contract for doing that portably. Packaging gives the installation a +reviewable identity so an administrator can inventory, approve, update, and +remove it. Containment reduces what that installation can reach while it runs. +An approved, signed, inventoried Gateway holding the user's full token is still +an unbounded-blast-radius component. The two are also joined mechanically rather +than only thematically: the containment provider accepts the calling +application's package identity at provision time, specifically so a future OS +contract can act on it, which makes a stable package identity an input to +containment rather than a parallel concern. [`openclaw/openclaw#42026`](https://github.com/openclaw/openclaw/issues/42026) proposes splitting the gateway into a control plane and per-agent runtimes so @@ -141,9 +181,14 @@ the contract below is unchanged. ## Goals -- Define a platform-agnostic `GatewayContainmentProvider` seam so Gateway - containment is a deployment choice with a stable contract, not a - Windows-specific fork of the startup path. +- Establish the containment boundary in a launcher outside the Gateway, so a + compromised Gateway has no uncontained mode to reach and needs no containment + code of its own. +- Define a platform-agnostic `ContainmentProvider` seam so containment is a + deployment choice with a stable contract, not a Windows-specific fork of the + startup path. +- Keep the Gateway unchanged and unaware, so containment adds no configuration, + no runtime branch, and no protocol change inside it. - Make the containment boundary and its limits explicit and machine-readable, so a provider that cannot enforce something says so instead of implying it. - Establish Windows Isolation Sessions as the first provider, contributed behind @@ -189,30 +234,44 @@ process that establishes it. ## Proposal -### What moves, and what does not +### What sits where -The unit of containment is the deployable process that holds credentials and -runs the agent loop. Today that is the Gateway. If +The design has three parts, and which side of the boundary each falls on is the +whole point. + +**The launcher runs outside the boundary, as the user.** It is the entry point +the user or the OS invokes. It selects and drives a containment provider, +provisions the boundary, stages the payload, starts the unit of containment +inside it, and reports posture. It is the trusted computing base: it must stay +minimal, load no plugins, and never execute agent-directed work. On Windows it +is the packaged host app from [#58](https://github.com/openclaw/rfcs/pull/58) +rather than a new component. + +**The unit of containment runs inside.** Today that is the Gateway. If [#42026](https://github.com/openclaw/openclaw/issues/42026) splits it, the unit becomes each per-agent runtime, and optionally the control plane, without changing the contract below. Containment applies to that unit and everything it hosts in-process: the agent loop, plugin code, the scheduler, and its own -working state. The following explicitly stay outside the boundary and are -unchanged: +working state. It is unchanged by this proposal and holds no containment logic. + +**Everything else is untouched.** The following explicitly stay outside the +boundary and keep their current behavior: - Client, channel, and node connections, which continue to reach the Gateway through its existing endpoints. - Per-session sandbox backends, which continue to contain agent work. A contained Gateway may still place a session in a sandbox; containment nests. - Configuration and credential *ownership*. The Gateway continues to own its - configuration, credentials, and pairing records. The containment provider - provisions and tears down an execution environment; it does not read, copy, - broker, or authorize Gateway credentials. + configuration, credentials, and pairing records. The launcher provisions and + tears down an execution environment; consistent with #58's boundary for the + host app, it does not read, copy, broker, or authorize Gateway credentials, + and it does not proxy Gateway traffic. -### The `GatewayContainmentProvider` seam +### The `ContainmentProvider` seam -A containment provider answers two things: what boundary can this host actually -provide, and how is a Gateway started and stopped inside it. +The seam is implemented and consumed by the launcher. A containment provider +answers two things: what boundary can this host actually provide, and how is a +unit of containment started and stopped inside it. **Capability descriptor.** A provider must declare its boundary honestly, because an overstated boundary is worse than no boundary. At minimum: @@ -240,11 +299,12 @@ where unenforceable policy is rejected rather than quietly dropped, and it is the property that makes the descriptor trustworthy. The descriptor is versioned. A provider reports the descriptor schema version it -implements, and OpenClaw refuses to run against a descriptor whose version it -does not understand rather than assuming absent fields mean "unsupported" — an -unrecognised capability must never be silently downgraded into a claim about the -boundary. Adding a capability is a minor version change; changing the meaning of -an existing one is a major version change and requires a provider update. +implements, and the launcher refuses to run against a descriptor whose version +it does not understand rather than assuming absent fields mean "unsupported" — +an unrecognised capability must never be silently downgraded into a claim about +the boundary. Adding a capability is a minor version change; changing the +meaning of an existing one is a major version change and requires a provider +update. **Lifecycle.** Providers implement a lifecycle that a long-lived daemon can use: @@ -325,28 +385,29 @@ flowchart TB subgraph host["Windows host"] user["User profile
files, HKCU, tokens"] client["Clients, channels, nodes"] - launcher["OpenClaw launcher
selects provider"] + launcher["Launcher (trusted)
packaged entry point
no plugins"] end svc["Isolation Session
service, SYSTEM
owns lifecycle"] subgraph iso["Isolation session"] - gw["OpenClaw Gateway"] + gw["OpenClaw Gateway
unchanged, unaware"] plugins["Plugins, scheduler,
agent loop"] sb["Per-session sandboxes"] gw --- plugins gw --- sb end - launcher -->|"provision and start"| svc + launcher -->|"provision, stage, start"| svc svc -->|"OS-assigned account"| iso client -->|"existing endpoints,
unchanged protocol"| gw user -.->|"no inherited grants"| iso ``` -**Figure 1.** The Gateway runs under an OS-assigned account inside an OS-managed -session. Clients reach it through its existing endpoints; it does not inherit -the signed-in user's grants. +**Figure 1.** The launcher runs as the user and establishes the boundary; the +Gateway is started inside it and never runs outside it. Clients reach the +Gateway through its existing endpoints, and it does not inherit the signed-in +user's grants. The boundary this provider delivers is an **identity** boundary, and the capability descriptor must say exactly that. Per the public MXC documentation @@ -428,37 +489,61 @@ something explicitly stages that content across, which is a product decision about mediated access rather than a transparent capability. It remains the central unresolved question below. -### Ownership and why this belongs in core +### Ownership, and why this does not belong in the Gateway -The seam belongs in core; individual providers do not. +The seam belongs to the launcher. It must not live in OpenClaw core, and the +reason is the argument from the Motivation rather than a packaging preference: a +containment decision made by the process being contained is made by code the +attacker already controls. -Core owns the `GatewayContainmentProvider` interface, the capability descriptor -and its schema version, the failure taxonomy, provider selection, the -fail-closed rule, and the reporting of containment posture in status and -diagnostics. This is the minimum that cannot live outside core: it decides -whether the Gateway starts at all, it is the thing that must refuse to downgrade -a security posture silently, and its guarantees are only as good as the -component that enforces them. A containment boundary implemented by an optional, -independently versioned plugin would be a boundary the Gateway could be -persuaded to skip, which defeats the argument in the Motivation. +The launcher therefore owns the `ContainmentProvider` interface, the capability +descriptor and its schema version, the failure taxonomy, provider selection, the +fail-closed rule, and the reporting of containment posture. OpenClaw core owns +none of it and gains no containment configuration, which is what keeps the +Gateway unchanged and keeps a preview OS dependency out of the cross-platform +build. Individual providers are platform integrations and should live wherever their -platform dependency is maintainable — for the Windows provider, alongside the -Windows packaging work rather than in the cross-platform Gateway, so that a -preview OS dependency and its binding regeneration do not become a build -requirement for every OpenClaw contributor. The seam must therefore support an -out-of-tree provider without special-casing it. - -This satisfies the usual test for core ownership: the behavior is mandatory for -the security and trust model rather than optional, it is a public contract with -more than one credible implementer (Windows now, macOS and Linux primitives -later), and it is not a one-off hook added for a single consumer. +platform dependency is maintainable, so the seam must support an out-of-tree +provider without special-casing it. On Windows the launcher and the provider +naturally live with the packaging work described in +[#58](https://github.com/openclaw/rfcs/pull/58). + +One consequence deserves stating plainly: **posture reported by the Gateway is +not evidence.** A compromised Gateway can claim to be contained. Containment +status is therefore a launcher-side assertion, and any check that needs to be +trustworthy — an administrator confirming a managed device is compliant — must +observe the boundary from outside the Gateway rather than ask it. + +### Bypass resistance + +Containment that the user can accidentally skip is a default, not a boundary. +Two paths must be closed for a deployment that has selected containment: + +- **A second entry point.** If the packaged launcher is the supported entry + point but a source checkout, a stale shortcut, or a copied binary can still + start a Gateway directly, the boundary is optional in practice. The launcher + must be the only supported way to start a managed Gateway, and the others must + fail rather than silently starting uncontained. +- **A native start command.** `openclaw gateway` starts a Gateway by design. + [#58](https://github.com/openclaw/rfcs/pull/58) already makes this a release + requirement for its own payload-activation contract — that host lifecycle + operations and native `openclaw gateway` commands "cannot create conflicting + managed Gateway instances or bypass staged-payload activation" — and the same + requirement extends to containment. + +Because the Gateway cannot be trusted to refuse to start, this cannot be +implemented as a check inside it. It has to come from the deployment shape: the +managed installation exposes the launcher as its entry point, and a Gateway +started another way is a different, unmanaged installation rather than a +containment failure of the managed one. Making that distinction observable — so +an administrator can tell the two apart — is part of the readiness bar. ### Readiness bar Contained execution should be recommended for a class of deployment only once: -- The provider's capability descriptor is accurate, and OpenClaw refuses +- The provider's capability descriptor is accurate, and the launcher refuses configuration the provider cannot enforce. - Gateway state has defined, tested persistence across `provision`/`deprovision`, including across host restarts, and the host-side @@ -474,8 +559,9 @@ Contained execution should be recommended for a class of deployment only once: per command. - Startup, crash, restart, and teardown paths are covered, including orphaned environments after an ungraceful host shutdown. -- Containment status is reported in Gateway status and diagnostics, and a - fallback to uncontained execution is loud rather than silent. +- Containment posture is reported by the launcher rather than by the Gateway, a + fallback to uncontained execution is loud rather than silent, and a managed + installation is distinguishable from a Gateway started outside it. - The platform primitive is generally available rather than preview-gated, and its owner is prepared to describe it as a security boundary. The `microsoft/mxc` README currently states the opposite for its preview backends, @@ -535,6 +621,13 @@ This is an identity boundary and nothing more. The following survive it: boundary, it lives on the host and holds the credentials containment is meant to make less valuable to steal. Compromising the host side recovers them, so containment moves the crown jewels rather than eliminating them. +- **The launcher itself.** It runs as the user and is by construction able to + start, stop, and stage into the boundary, so compromising it defeats the whole + design. That is why it must stay small, load nothing extensible, and never + execute agent-directed work: everything added to the launcher is added to the + trusted computing base. An attacker who can replace the launcher binary or its + entry-point registration has already won, which makes package integrity and + signing a dependency of this design rather than an adjacent concern. - **The user's own actions.** A contained agent can still return output that induces the user to run something themselves. @@ -591,40 +684,48 @@ requires the preview OS dependency to be present. 1. **Contract only.** Land the provider interface, versioned capability descriptor, failure taxonomy, selection and fail-closed logic, and posture - reporting in status and diagnostics, with an in-tree null provider. Proof: - unit coverage for selection, refusal, and reporting, exercised through the - real interface rather than through source inspection. + reporting in the launcher, with a null provider. No change to OpenClaw core. + Proof: unit coverage for selection, refusal, and reporting, exercised through + the real interface rather than through source inspection. 2. **Windows provider, behind experimental opt-in.** Implement `probe`, `provision`, `start`, `attach`, `stop`, and `deprovision` against the OS primitive, reporting the honest descriptor. Proof: a Gateway that starts - contained on a capable build, is reachable from a client, and reports its - posture; and a capable-build-absent host that fails closed with the reason. + contained on a capable build, is reachable from a client, and runs under an + account that is demonstrably not the signed-in user; and a + capable-build-absent host that fails closed with the reason. 3. **State and migration.** Resolve persistence, then implement adoption and rollback with tested restart, upgrade, and downgrade behavior. Proof: a contained Gateway that survives host restart with its pairing intact, and a rollback that restores the prior deployment. -4. **Readiness review.** Re-evaluate against the readiness bar and decide +4. **Bypass resistance.** Make the launcher the entry point for a managed + installation and make an unmanaged Gateway distinguishable from a managed + one. Proof: an administrator-observable signal that does not depend on asking + the Gateway. +5. **Readiness review.** Re-evaluate against the readiness bar and decide whether contained execution can be recommended for any deployment class. -Phase 1 is owned by whoever owns Gateway startup; phases 2 and 3 need a Windows -owner with access to the platform primitive. Phase 3 should not begin while the -persistence question is open, and phase 4 is a maintainer decision rather than -an implementation task. +Phases 1 and 2 are owned by whoever owns the launcher — on Windows, the +packaging work in [#58](https://github.com/openclaw/rfcs/pull/58). Phase 3 +should not begin while the persistence question is open. Phase 4 depends on +#58's own entry-point and activation work rather than duplicating it, and +phase 5 is a maintainer decision rather than an implementation task. ## Decision requested -This RFC asks maintainers for one decision: **is an opt-in, capability-honest -Gateway containment seam a direction OpenClaw wants to own in core, given that -the first available provider offers identity isolation only and is -preview-gated?** +This RFC asks maintainers for one decision: **is launcher-established, opt-in +Gateway containment a direction OpenClaw wants to support, given that the first +available provider offers identity isolation only and is preview-gated?** -A "yes" authorizes phase 1 — the contract, with no provider and no default -change. It does not commit OpenClaw to the Windows provider, to a default -posture, or to a timeline. +A "yes" authorizes phase 1 — the contract, implemented in the launcher, with no +provider and no default change. Notably it requires no change to OpenClaw core +at all, since the Gateway is unchanged and unaware. It does not commit OpenClaw +to the Windows provider, to a default posture, or to a timeline. -Two secondary decisions follow only from a "yes": whether providers may live -out-of-tree, and whether this RFC's number is correct given the repository's -current numbering practice. Both are noted rather than assumed. +Two secondary decisions follow only from a "yes": whether the launcher-side +contract should be specified here or folded into +[#58](https://github.com/openclaw/rfcs/pull/58), which already owns the host app +it would live in; and whether this RFC's number is correct given the +repository's current numbering practice. Both are noted rather than assumed. ## Rationale @@ -678,6 +779,17 @@ wants both, and adopting either does not foreclose the other. Writing the seam against a unit of containment rather than against "the Gateway" is what keeps that true. +**Why the seam is not a hook inside the Gateway.** A containment seam inside +OpenClaw core would be easier to ship and would put the capability where the +rest of the runtime configuration lives. It is the wrong place for exactly one +reason, and it is decisive: the Gateway is the component the threat model treats +as compromised, so a decision it makes about its own containment is a decision +the attacker makes. Establishing the boundary in a launcher costs a separate +executable and an entry-point contract, and buys a boundary that exists before +any untrusted code runs. The corollary — that OpenClaw core changes not at all — +is a benefit rather than a consolation, since it keeps a preview OS dependency +out of the cross-platform build and leaves nothing to configure incorrectly. + **Why fail-closed by default.** A containment control that silently degrades to no containment produces the worst outcome: an operator who believes they are protected and is not. Failing closed makes the absence of the boundary a visible @@ -732,12 +844,14 @@ cheaper than retrofitting them around a shipped Windows-specific implementation. phase 1, but the descriptor reserves `workloadIdentity` so it can be answered without a breaking change. - **How is autostart handled?** A Gateway expected to run in the background must - start without an interactive logon. What does contained startup look like - before or without a signed-in user? -- **Should providers live out-of-tree?** The Ownership section proposes that - platform providers live with their platform dependency rather than in the - cross-platform Gateway. Maintainers should confirm that split and the support - boundary it implies. + start without an interactive logon, and the launcher is what would start it. + What does contained startup look like before or without a signed-in user? +- **Should this contract live here or in #58?** The launcher it belongs to is + already proposed by [#58](https://github.com/openclaw/rfcs/pull/58). Keeping + the contract in a separate RFC keeps it platform-neutral and reviewable on its + own; folding it into #58 keeps the host app's responsibilities in one + document. Maintainers should pick one rather than letting both describe the + launcher. - **What is the acceptance criterion for calling this a security boundary?** The platform currently declines to make that claim for preview profiles. OpenClaw should state in advance what evidence it requires before describing contained From 8b0b942d7aff12d4a719cf96927aea496e876698 Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Tue, 18 Aug 2026 17:53:49 -0700 Subject: [PATCH 07/15] docs: rewrite RFC 0032 tighter and more direct Full rewrite. Same argument and same design, ~35% shorter: drops the repetition and hedging that accumulated across revisions, uses plainer language, and merges the migration, phasing, and rollback material into a single Rollout section instead of three. Adds crosslinks throughout to #58, #42026, #55, RFC 0025, the public microsoft/mxc backend docs, and the Windows Insider build that gates the primitive. Fixes two defects found while rewriting: a typo in the summary, and a capability named hostPathSharing in the Windows section that no longer matched the descriptor table after it was split into hostPathProjection and stagingChannel. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 1231 +++++++---------- 1 file changed, 467 insertions(+), 764 deletions(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index 133af2c5..e764dc56 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -6,379 +6,250 @@ created: 2026-08-18 last_updated: 2026-08-18 status: draft issue: -rfc_pr: https://github.com/openclaw/rfcs/pull/61 +rfc_pr: https://github.com/paulcam206/rfcs/pull/1 --- # Proposal: Gateway Containment and Windows Isolation Sessions ## Summary -OpenClaw contains the work an agent does, but not the process that decides to do -it. On Windows the Gateway runs as the signed-in user and inherits that user's -full filesystem, registry, and token reach, so every credential, plugin, and -model-directed decision executes with the operator's identity. This RFC argues -that the Gateway process itself should be contained, and that the component -which establishes that containment must sit outside it. It proposes a small, -trusted **launcher** that owns a platform-agnostic `ContainmentProvider` seam: -the launcher probes the host, provisions an OS-managed boundary, stages the -payload, and starts the Gateway inside — so the Gateway never runs uncontained -and is never asked to contain itself. Windows Isolation Sessions are proposed as -the first provider: the operating system provisions a fresh, OS-assigned agent -account, runs the Gateway in a dedicated session bound to that account, and -tears the session and account down when the owning process exits. The Gateway -itself is unchanged and unaware. The seam is written against a *unit of -containment* rather than a monolithic Gateway, so it survives a future split of -the Gateway into a control plane and per-agent runtimes. The seam and the -argument are the proposal; the Windows provider is currently preview-quality, so -this RFC defines the readiness bar that adoption must clear rather than asking -for immediate default adoption. +We contain the work an agent does, but not the process that decides to do it. On +Windows the Gateway runs as the signed-in user, so every plugin, credential, and +model-directed decision carries that user's full reach over their files, +registry, and tokens. + +This proposes containing the Gateway itself — and, importantly, doing it from +*outside* the Gateway. A small trusted launcher provisions an OS-managed +boundary and starts the Gateway inside it. The Gateway is unchanged and unaware; +it simply never runs anywhere else. + +Windows Isolation Sessions are the first provider: the OS mints a fresh +throwaway account, runs the Gateway in a session bound to it, and tears both +down afterward. That provider is preview-quality today, so this asks for the +contract and the direction, plus a readiness bar to clear before anyone +recommends it. ## Motivation -### The Gateway is the largest uncontained surface - -OpenClaw already has containment seams, and they are all narrower than the -Gateway. `SandboxBackend` (`src/agents/sandbox/`, with Docker and SSH backends) -is created per session and scoped to a session key and workspace directory. It -contains the commands an agent runs. It does not contain the process that loads -plugins, holds channel and provider credentials, runs the scheduler, accepts -node connections, and decides which commands to run in the first place. - -That leaves a straightforward asymmetry. A tool call can be confined to a -container, while the component that chose the tool call, holds the tokens that -authorize it, and can rewrite its own configuration runs with the operator's -full identity. On a Windows workstation the practical blast radius of a -prompt-injection, a hostile skill, or a compromised plugin is therefore the -user's entire profile: documents, browser and credential stores, `HKCU`, startup -entries, SSH keys, and any resource that authenticates the user by token rather -than by password. - -### ACLs are not a boundary against your own code - -The usual mitigations do not close this. File and registry ACLs do not separate -code running as the user from data owned by the user, because that is exactly -the grant they encode. Neither does running the Gateway as a service, or in a -different working directory, or under a restricted shell. Any control that -depends on the Gateway voluntarily declining access is a policy, not a boundary, -and agent software is precisely the category where the process can be talked -into changing its mind. - -The only durable fix is an identity the operating system enforces: run the -Gateway as a principal that is not the user and never had the user's grants. - -### Existing options force an unattractive trade - -Windows deployments today choose between reach and containment: - -- **Run natively as the user.** Full reach, full blast radius. This is the - default and it is what most users run. -- **Run in WSL.** The current recommended local-Gateway path on Windows. It - provides a real boundary, but it is a second operating system with its own - filesystem, package management, credential storage, update cadence, and - support burden, and it moves the Gateway away from the Windows environment the - user actually works in. -- **Run in a VM or Windows Sandbox.** A stronger boundary at a much higher - resource and lifecycle cost, and an awkward fit for a long-lived daemon that - is expected to be running whenever the user is. - -None of these is a good default. The first is not contained, and the others are -heavy enough that most users will not adopt them for an always-on background -process. - -### The operating system now offers a better-shaped primitive - -Windows has begun exposing a containment primitive built around per-instance -identity rather than a whole guest OS. As documented publicly by the +### The Gateway is our biggest uncontained surface + +We already have containment seams, and they're all narrower than the Gateway. +`SandboxBackend` (`src/agents/sandbox/`, with Docker and SSH backends) is +per-session and scoped to a workspace. It contains the commands an agent runs. + +It doesn't contain the process that loads plugins, holds channel and provider +credentials, runs the scheduler, and picks the commands in the first place. + +That's backwards. A tool call gets a container; the thing that chose the tool +call, and holds the tokens authorizing it, runs as you. On a Windows +workstation, the blast radius of a prompt injection or a hostile skill is your +whole profile: documents, browser and credential stores, `HKCU`, startup +entries, SSH keys. + +ACLs don't help here. They don't separate code running as you from data owned by +you — that's exactly the grant they encode. Neither does a different working +directory or a restricted shell. Any control the Gateway can decline to apply is +a policy, not a boundary, and agent software is the one category where the +process can be talked into changing its mind. + +### A compromised Gateway can't contain itself + +This constrains the design more than anything else here. + +If the Gateway is what we're treating as compromised, it can't also be what +decides whether to be contained. A containment check inside the Gateway is code +the attacker already controls — patch it, configure it away, or just never reach +it. By the time it would run, untrusted code is already executing with the +identity the check was supposed to remove. Plugins are no better, since they +load into the process being contained. + +So the boundary has to exist *before* the Gateway does, established by something +outside it. That something is a launcher: a separate, minimal executable that +runs as the user, provisions the boundary, and starts the Gateway inside. The +Gateway then has no uncontained mode to reach — not because it declines one, but +because it's never started outside the boundary at all. It needs no containment +code and no containment config. + +That also sets the bar for the launcher. It's the trusted computing base here, +so it stays small, loads no plugins, and never runs agent-directed work. + +### Today's Windows options are all bad trades + +| Option | Trade | +|---|---| +| Run as the user (default) | Full reach, full blast radius | +| WSL (today's recommendation, per [#58](https://github.com/openclaw/rfcs/pull/58)) | Real boundary, but a second OS with its own filesystem, packages, credentials, and update cadence | +| VM or Windows Sandbox | Stronger boundary, but continuous resource cost and a lifecycle built for disposable sessions, not an always-on daemon | + +None works as a default. The first isn't contained; the others are heavy enough +that nobody runs them for a background process. + +### The OS now offers something better shaped + +Windows is exposing containment built around per-instance *identity* rather than +a whole guest OS. Per the public [`microsoft/mxc`](https://github.com/microsoft/mxc) project, its -`isolation_session` containment backend calls a Windows service that provisions -a fresh agent user account with an opaque, OS-assigned name, starts a dedicated -session for it, hosts processes inside that session, and then stops the session -and deprovisions the account. MXC states the requirements this backend exists to -meet as "per-execution OS-isolated identity so the workload's actions cannot +`isolation_session` backend asks a Windows service to mint a fresh agent account +with an opaque OS-assigned name, start a session for it, run processes inside, +then stop the session and delete the account. + +[MXC states the +requirements](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/oneshot.md) +plainly: "per-execution OS-isolated identity so the workload's actions cannot pollute the calling user's NTFS / registry / token state", with an "OS-managed session lifecycle that the OS-side service tears down cleanly when the calling process exits." -That shape matters. The unit of containment is a session and an account, not a -guest operating system, so it avoids a guest's memory footprint and boot time, -and the boundary is an OS-enforced identity rather than a cooperative policy. -Provisioning and session start are not free — MXC's state-aware lifecycle exists -precisely so one provisioned session can host multiple executions "without -re-paying the provisioning / session-start cost each time" — but that is a cost -paid once for a daemon that then stays up. For a long-lived agent process, that -is the trade OpenClaw has been missing. - -### A compromised Gateway cannot contain itself - -This follows directly from the threat, and it constrains the design more than -anything else in this proposal. - -If the Gateway is the component we are treating as potentially compromised, it -cannot also be the component that decides whether to be contained. A containment -check that runs inside the Gateway is code the attacker already controls: it can -be patched, configured away, exception-handled, or simply never reached, and by -the time it would run the untrusted code is already executing with the identity -the check was supposed to remove. The same applies to a plugin, since plugins -load into the process being contained. - -So the boundary has to be established *before* the Gateway exists, by something -outside it. That something is a launcher: a separate, minimal executable that -runs as the user, provisions the boundary, and starts the Gateway inside it. The -Gateway then has no uncontained mode to fall back to, not because it declines to -use one, but because it is never started outside the boundary in the first -place. Correspondingly, the Gateway needs no containment code, no containment -configuration, and no awareness that it is contained. - -This also sets the standard the launcher must meet. It is the trusted computing -base for this design, so it should stay small, load no plugins, execute no model -output, and do nothing on behalf of the agent. Its job is to establish the -boundary and get out of the way. - -### This composes with neighbouring proposals rather than duplicating them - -Two active proposals touch the same area, and neither makes this one redundant. - -[RFC PR #58](https://github.com/openclaw/rfcs/pull/58) proposes MSIX packaging -for Windows and explicitly lists "Defining runtime isolation. A separate RFC may -define a session-based runtime model" among its non-goals. It also already -describes the component this proposal needs: a package-specific host app that is -"the packaged entry point", registered behind an `openclaw.exe` execution alias, -whose responsibilities are "package activation, payload verification and -staging, and launching or stopping the packaged Gateway", and which explicitly -"does not proxy Gateway traffic, distribute Gateway credentials, or approve -clients, nodes, or channel users". - -That host app is the launcher this design requires. This RFC does not propose a -second one, and it does not depend on MSIX: it proposes that whatever launches -the Gateway on a platform also establish the containment boundary first, and it -defines the contract for doing that portably. Packaging gives the installation a -reviewable identity so an administrator can inventory, approve, update, and -remove it. Containment reduces what that installation can reach while it runs. -An approved, signed, inventoried Gateway holding the user's full token is still -an unbounded-blast-radius component. The two are also joined mechanically rather -than only thematically: the containment provider accepts the calling -application's package identity at provision time, specifically so a future OS -contract can act on it, which makes a stable package identity an input to -containment rather than a parallel concern. - -[`openclaw/openclaw#42026`](https://github.com/openclaw/openclaw/issues/42026) -proposes splitting the gateway into a control plane and per-agent runtimes so -each agent can run in its own container, VM, or process, and argues for "true -secret isolation" in which one agent's credentials never coexist with another's. -That is a real reduction in blast radius, and it is a different axis from this -proposal. Decomposition partitions *which component holds which secrets*; -containment changes *what principal a component runs as*. A fully decomposed -deployment on Windows still runs every control plane and every runtime as the -signed-in user, so each piece keeps the same reach into that user's files, -registry, and tokens — the partition is horizontal, between agents, and does not -cross the user boundary. Conversely, containment without decomposition places -all agents behind one contained identity. - -The two therefore compose, and the seam proposed here is deliberately written -against a *unit of containment* rather than against a monolithic Gateway. If -#42026 lands, the unit becomes the runtime, optionally the control plane, and -the contract below is unchanged. +The unit is a session and an account, not a guest OS — no guest memory footprint +or boot time. Provisioning isn't free, which is exactly why MXC's state-aware +lifecycle exists: one session hosts many executions "without re-paying the +provisioning / session-start cost each time." For a daemon that starts once and +stays up, that's the right shape. + +### How this fits with work already in flight + +- [**#58 (MSIX packaging)**](https://github.com/openclaw/rfcs/pull/58) lists + "Defining runtime isolation" as a non-goal and says a separate RFC may define + a session-based runtime model. This is that RFC. #58 also already describes + the launcher we need — see below. +- [**#42026 (control plane / per-agent + runtimes)**](https://github.com/openclaw/openclaw/issues/42026) splits the + gateway so each agent runs in its own container or process, giving "true + secret isolation" between agents. Different axis. It partitions *which + component holds which secrets*; this changes *what principal a component runs + as*. Fully decomposed on Windows, every runtime still runs as you. Both are + worth having, so the contract below targets a *unit of containment* rather + than a monolithic Gateway. +- [**#55 (OpenShell worker + provider)**](https://github.com/openclaw/rfcs/pull/55) contains a session's + worker. This contains the Gateway. They nest. ## Goals -- Establish the containment boundary in a launcher outside the Gateway, so a - compromised Gateway has no uncontained mode to reach and needs no containment - code of its own. -- Define a platform-agnostic `ContainmentProvider` seam so containment is a - deployment choice with a stable contract, not a Windows-specific fork of the - startup path. -- Keep the Gateway unchanged and unaware, so containment adds no configuration, - no runtime branch, and no protocol change inside it. -- Make the containment boundary and its limits explicit and machine-readable, so - a provider that cannot enforce something says so instead of implying it. -- Establish Windows Isolation Sessions as the first provider, contributed behind - the same seam as any future provider. -- Preserve the Gateway's existing contract across the boundary: endpoints, - discovery, device authorization, pairing, node capability approval, and - protocol behavior are unchanged by containment. -- Define explicit selection, fallback, and diagnostic behavior, including what - happens when a requested provider is unavailable on the host. -- Keep the seam neutral about the unit of containment so it remains valid if the - Gateway is later split into a control plane and per-agent runtimes. -- Preserve existing uncontained deployments unchanged, with a defined migration - and rollback path for those that opt in. -- Define the readiness bar that a provider must clear before contained execution - can be recommended, and later defaulted, for a class of deployment. +- Establish the boundary in a launcher outside the Gateway, so a compromised + Gateway has no uncontained mode to reach. +- Keep the Gateway unchanged and unaware — no new config, no runtime branch, no + protocol change. +- Define a platform-agnostic `ContainmentProvider` contract so this isn't a + Windows fork of the startup path. +- Make each provider's limits explicit and machine-readable, so one that can't + enforce something says so instead of implying it. +- Fail closed, loudly, when a selected provider isn't available. +- Leave existing uncontained deployments alone, with a real migration and + rollback path for anyone opting in. +- Define the readiness bar to clear before recommending — later, defaulting — + contained execution. ## Non-Goals -- **Defining the packaging or launch mechanism.** How a Gateway build is - packaged, signed, distributed, and started on Windows belongs to [PR - #58](https://github.com/openclaw/rfcs/pull/58) and its implementation. This -RFC describes the containment contract and deliberately does not specify the -process that establishes it. -- **Replacing per-session sandboxes.** `SandboxBackend` and the worker-provider - direction in [PR #55](https://github.com/openclaw/rfcs/pull/55) contain a - session's work. This RFC contains the Gateway. They compose; neither - substitutes for the other. -- **Deciding how the Gateway is decomposed.** Whether the Gateway splits into a - control plane and per-agent runtimes is - [`openclaw/openclaw#42026`](https://github.com/openclaw/openclaw/issues/42026) - to settle. This RFC defines a contract that applies to whatever the resulting - deployable unit is. -- **Specifying the Windows API surface.** The OS API is owned by Windows and is - consumed, not defined, here. -- **Requiring containment, or making it the default, in this RFC.** The Windows - provider is preview-quality today. This RFC asks for the seam, the argument, - and the readiness bar. -- **Changing authentication, pairing, discovery, or the wire protocol.** No new - endpoint, credential type, or trust relationship is introduced. -- **Defining enterprise policy.** Which deployments must run contained is a - policy question for the administrator and for OpenClaw's enterprise surface, - not for this seam. +- **Packaging and distribution.** That's + [#58](https://github.com/openclaw/rfcs/pull/58). This RFC doesn't depend on + MSIX; it needs *a* launcher, and on Windows #58 already builds one. +- **Replacing per-session sandboxes.** `SandboxBackend` and + [#55](https://github.com/openclaw/rfcs/pull/55) contain a session's work. This + contains the Gateway. +- **Deciding how the Gateway is decomposed.** That's + [#42026](https://github.com/openclaw/openclaw/issues/42026). +- **Specifying the Windows API.** Owned by Windows; consumed, not defined, here. +- **Making containment the default,** or requiring it, now. +- **Changing auth, pairing, discovery, or the wire protocol.** ## Proposal -### What sits where +### Where things run -The design has three parts, and which side of the boundary each falls on is the -whole point. +Three parts, and which side of the boundary each lands on is the whole point. -**The launcher runs outside the boundary, as the user.** It is the entry point -the user or the OS invokes. It selects and drives a containment provider, -provisions the boundary, stages the payload, starts the unit of containment -inside it, and reports posture. It is the trusted computing base: it must stay -minimal, load no plugins, and never execute agent-directed work. On Windows it -is the packaged host app from [#58](https://github.com/openclaw/rfcs/pull/58) -rather than a new component. +**The launcher runs outside, as the user.** It's the entry point. It selects a +provider, provisions the boundary, stages the payload, starts the unit inside, +and reports posture. It's the trusted computing base, so it stays minimal, loads +no plugins, and never executes agent-directed work. -**The unit of containment runs inside.** Today that is the Gateway. If -[#42026](https://github.com/openclaw/openclaw/issues/42026) splits it, the unit -becomes each per-agent runtime, and optionally the control plane, without -changing the contract below. Containment applies to that unit and everything it -hosts in-process: the agent loop, plugin code, the scheduler, and its own -working state. It is unchanged by this proposal and holds no containment logic. +On Windows this is #58's host app, not a new component. #58 defines it as "the +packaged entry point" behind an `openclaw.exe` execution alias, whose job is +"package activation, payload verification and staging, and launching or stopping +the packaged Gateway", and which explicitly "does not proxy Gateway traffic, +distribute Gateway credentials, or approve clients, nodes, or channel users." +This RFC adds one responsibility: establish the boundary first. -**Everything else is untouched.** The following explicitly stay outside the -boundary and keep their current behavior: +**The unit of containment runs inside.** Today that's the Gateway, along with +the agent loop, plugins, scheduler, and its working state. If +[#42026](https://github.com/openclaw/openclaw/issues/42026) lands, the unit +becomes each runtime instead, and nothing below changes. -- Client, channel, and node connections, which continue to reach the Gateway - through its existing endpoints. -- Per-session sandbox backends, which continue to contain agent work. A - contained Gateway may still place a session in a sandbox; containment nests. -- Configuration and credential *ownership*. The Gateway continues to own its - configuration, credentials, and pairing records. The launcher provisions and - tears down an execution environment; consistent with #58's boundary for the - host app, it does not read, copy, broker, or authorize Gateway credentials, - and it does not proxy Gateway traffic. +**Everything else is untouched.** Clients, channels, and nodes reach the Gateway +through existing endpoints. Per-session sandboxes still work; containment nests. +The Gateway still owns its own config, credentials, and pairing records — the +launcher provisions an environment, it doesn't broker credentials. -### The `ContainmentProvider` seam +### The `ContainmentProvider` contract -The seam is implemented and consumed by the launcher. A containment provider -answers two things: what boundary can this host actually provide, and how is a -unit of containment started and stopped inside it. +The launcher implements and consumes this. A provider answers two questions: +what boundary can this host actually give me, and how do I start and stop +something inside it. -**Capability descriptor.** A provider must declare its boundary honestly, -because an overstated boundary is worse than no boundary. At minimum: +**Capabilities.** A provider declares its boundary honestly, because an +overstated boundary is worse than none: | Capability | Meaning | |---|---| -| `identityIsolation` | Whether the contained Gateway runs as a principal distinct from the invoking user, and whether that principal is per-instance or shared. | -| `statePersistence` | Whether state inside the boundary survives the containment lifecycle, and across what unit — instance, host, or not at all. | -| `hostPathProjection` | Whether arbitrary host paths can be mapped into the boundary. | -| `stagingChannel` | Whether the provider offers a directory for moving files across the boundary, its lifetime, and which side can see it. | -| `networkPosture` | What the provider can enforce on inbound and outbound network access. Explicitly includes "nothing". | -| `hostUiReach` | Whether contained code can observe or drive the user's desktop, clipboard, and input. | -| `workloadIdentity` | Whether the contained unit can be given an identity of its own rather than borrowing the caller's. | -| `lifecycleOwner` | Whether teardown is guaranteed by the OS or must be driven by OpenClaw. | - -`hostPathProjection` and `stagingChannel` are deliberately separate. A provider -may offer a way to hand a file across the boundary without being able to give -the contained unit access to the user's working tree, and treating those as one -capability is how a deployment ends up believing it has the second when it only -has the first. - -A provider must decline configuration it cannot enforce rather than accepting it -silently. This mirrors the disposition MXC already takes for its own backends, -where unenforceable policy is rejected rather than quietly dropped, and it is -the property that makes the descriptor trustworthy. - -The descriptor is versioned. A provider reports the descriptor schema version it -implements, and the launcher refuses to run against a descriptor whose version -it does not understand rather than assuming absent fields mean "unsupported" — -an unrecognised capability must never be silently downgraded into a claim about -the boundary. Adding a capability is a minor version change; changing the -meaning of an existing one is a major version change and requires a provider -update. - -**Lifecycle.** Providers implement a lifecycle that a long-lived daemon can use: +| `identityIsolation` | Does the unit run as a principal distinct from the user, and is it per-instance? | +| `statePersistence` | Does state inside survive the lifecycle, and across what — instance, host, nothing? | +| `hostPathProjection` | Can arbitrary host paths be mapped in? | +| `stagingChannel` | Is there a directory for moving files across, and what's its lifetime and visibility? | +| `networkPosture` | What can be enforced on the network. Includes "nothing". | +| `hostUiReach` | Can contained code see or drive the user's desktop, clipboard, input? | +| `workloadIdentity` | Can the unit carry its own identity instead of borrowing the caller's? | +| `lifecycleOwner` | Is teardown guaranteed by the OS, or do we have to drive it? | + +`hostPathProjection` and `stagingChannel` are deliberately separate. Being able +to hand a file across is not the same as giving an agent the user's working +tree, and merging them is how someone ends up believing they have the second. + +The descriptor is versioned. The launcher refuses a version it doesn't +understand rather than treating absent fields as "unsupported" — an unrecognised +capability must never quietly become a claim about the boundary. + +**Lifecycle:** ``` probe -> provision -> start -> attach -> stop -> deprovision ``` -`probe` reports availability and the capability descriptor for the current host, -without side effects. `provision` creates the isolated principal and -environment. `start` launches the unit of containment inside it. `attach` -reconnects to an already-running contained unit across separate CLI invocations, -so ordinary commands do not each pay provisioning cost. `stop` and `deprovision` -end the process and release the environment. - -Two invariants make the lifecycle safe to drive from a supervisor. Every -operation is idempotent: `provision` on an already-provisioned environment -returns the existing one, and `stop` or `deprovision` on an absent one succeeds. -And `deprovision` is the only destructive step, so a supervisor may retry any -other operation without risking state. - -**Addressing and reuse.** `provision` returns an opaque identifier that the -caller persists and uses to address the environment in every later phase, and it -reports whether it created a new isolated identity or reused an existing one. -This is what makes `attach` work across separate CLI invocations, and it is not -theoretical: the Windows provider's underlying lifecycle is explicitly designed -so a provisioned session can host multiple executions across separate caller -processes rather than re-paying provisioning cost per command. A contained -Gateway is therefore a durable environment that outlives the process that -started it, and the supervisor's job is to re-address it, not to recreate it. - -The corollary is that the environment does not go away on its own. Because -`deprovision` is explicit and destructive, a crashed or forgotten supervisor -leaves a provisioned environment behind. Reconciling those orphans is an -implementation obligation, not an edge case, and is called out in the readiness -bar. +`probe` reports availability and capabilities with no side effects. `attach` +reconnects to an already-running unit across separate invocations, so ordinary +commands don't each pay provisioning cost. -**Failure semantics.** Providers report a small closed set of outcomes so -OpenClaw can act on them without parsing provider text: +Every operation is idempotent, and `deprovision` is the only destructive one, so +a supervisor can retry anything else safely. `provision` returns an opaque +identifier the launcher persists and reuses to address the environment later, +and reports whether it reused an existing identity. -| Outcome | Meaning | OpenClaw behavior | +The environment outlives the process that started it. That's what makes `attach` +work, and it means a crashed launcher leaves an environment behind — reconciling +orphans is an obligation, not an edge case. + +**Failure outcomes** are a closed set, so the launcher can act without parsing +provider text: + +| Outcome | Meaning | Behavior | |---|---|---| -| `unavailable` | The provider cannot run on this host: absent, feature-gated off, or unsupported build. | Fail closed unless fallback is configured. Report the reason. | -| `policy_rejected` | Configuration was supplied that this provider cannot enforce. | Fail closed always. Never downgrade to a weaker boundary. | -| `stale` | The referenced environment no longer exists. | Re-provision if the caller asked to start; otherwise surface. | -| `lifecycle_failed` | A lifecycle operation failed for an operational reason. | Retry per policy, then fail closed. | - -`policy_rejected` never falls back, even when fallback is configured. A -deployment that asked for a boundary the provider cannot deliver has a -configuration error, not an availability problem, and silently running it with a -weaker boundary is the outcome this contract exists to prevent. - -**Selection and fallback.** Containment is selected explicitly by configuration, -never inferred. When a selected provider's `probe` fails, the default is -**fail-closed**: the Gateway does not start, and the failure is reported with -the reason. A deployment may opt into falling back to uncontained execution, but -that must be a stated choice, because a security control that silently degrades -to no control is the failure mode worth avoiding. Whichever path is taken, the -resulting posture must be visible in Gateway status and diagnostics: an operator -should never have to guess whether the Gateway they are talking to is contained. - -### The Windows Isolation Session provider - -The Windows provider maps this seam onto the OS-managed session primitive -described publicly by `microsoft/mxc`. `provision` asks the OS-side service for -a fresh agent account; `start` boots a session bound to that account and -launches the Gateway inside it; `stop` and `deprovision` end the session and -remove the account. Because each provisioned instance is a distinct OS account -with no shared registration, two contained Gateways on one host are independent. - -Every claim this RFC makes about Windows behavior is drawn from that public -documentation rather than from any particular OpenClaw implementation, and the -feature it describes is preview-gated today: MXC lists `isolation_session` as an -experimental backend, gated behind an explicit experimental flag and an OS -feature flag, and available only on recent Windows Insider builds. +| `unavailable` | Provider can't run here — absent, gated off, unsupported build | Fail closed unless fallback is configured | +| `policy_rejected` | Config was supplied that this provider can't enforce | Fail closed, always | +| `stale` | Referenced environment is gone | Re-provision if starting, else surface | +| `lifecycle_failed` | Operational failure | Retry per policy, then fail closed | + +`policy_rejected` never falls back, even when fallback is on. Asking for a +boundary the provider can't deliver is a config error, not an availability +problem, and quietly running with a weaker boundary is the exact outcome this +contract exists to prevent. + +### The Windows provider + +`provision` asks the OS service for a fresh account, `start` boots a session +bound to it, `stop` and `deprovision` tear both down. Each instance is a +distinct account with no shared registration, so concurrent units are +independent. ```mermaid flowchart TB @@ -404,459 +275,291 @@ flowchart TB user -.->|"no inherited grants"| iso ``` -**Figure 1.** The launcher runs as the user and establishes the boundary; the -Gateway is started inside it and never runs outside it. Clients reach the -Gateway through its existing endpoints, and it does not inherit the signed-in -user's grants. - -The boundary this provider delivers is an **identity** boundary, and the -capability descriptor must say exactly that. Per the public MXC documentation -for this backend, its network is unrestricted, with a process inside able to -listen on a port reachable via localhost, and it exposes no UI-restriction -primitive, though contained code cannot reach the host's desktop or clipboard. -So the honest descriptor is per-instance identity isolation and OS-owned -teardown, with `networkPosture` reported as unsupported. - -`hostPathSharing` is more nuanced than "unsupported", and the distinction drives -much of the design below. The backend rejects every filesystem policy field, so -a caller cannot project an arbitrary host path — the user's documents folder -cannot be mapped in. What it does provide is a single OS-created staging -directory per sandbox, documented as "a directory shared between the calling -user and this isolated agent user, through which the caller can stage files into -the session", with three properties that matter: - -- **Asymmetric visibility.** Each isolated user can access only its own - workspace, while the caller can access every concurrent sandbox's workspace. - The caller is the more privileged side. -- **Ephemeral.** It is created at provision and deleted at deprovision, so it is - a staging channel, not storage. -- **Not the working directory.** It does not change where the workload runs. - -So the descriptor should report `hostPathSharing` as a *staging channel* rather -than as path projection, and the seam must model those as different -capabilities. A provider that can stage a file in is not a provider that can -give an agent access to the user's working tree, and conflating them would let a -deployment believe the second was available when only the first is. - -Two further provider inputs are worth recording because they shape the contract. -Provisioning accepts an optional application identifier, documented as "the -Package Family Name for a packaged app", carried verbatim so that "a future OS -contract acting on the calling application's identity needs no breaking change" -— which is a direct, concrete link to the package identity proposed in -[#58](https://github.com/openclaw/rfcs/pull/58) rather than a thematic one. -Provisioning also accepts an optional user-identity bundle, which is the hook by -which a contained unit could carry an identity of its own instead of borrowing -the caller's. - -### Obligations this creates - -Naming the boundary honestly surfaces three obligations that the seam must -account for and that the Windows provider does not yet satisfy. - -**State must outlive the environment, so it has to live outside it.** A Gateway -is not a one-shot workload: its configuration, credentials, pairing records, and -session history must survive restarts. On this provider, both the isolated -account and the staging directory are destroyed at deprovision, so nothing kept -inside the boundary is durable. That settles the design rather than leaving it -open — durable state belongs to the host side, outside the boundary, and is -staged in when the environment is created. Two consequences follow. The store -becomes a boundary-crossing asset that must be protected as carefully as the -Gateway itself, since it holds exactly the credentials the containment is -supposed to be worth protecting. And the seam must treat `statePersistence` as a -declared capability rather than an assumption, because a provider that *does* -offer durable in-boundary state should not be forced through host-side staging. - -**Do not run the Gateway from the staging channel.** The staging directory is -ephemeral, visible to the more privileged caller, and explicitly not the -workload's working directory. Application content should be staged through it -and then materialized inside the boundary, so that the running Gateway does not -depend at runtime on a directory the host can rewrite underneath it and that -disappears at deprovision. - -**Reachability must be explicit.** Clients, channels, and nodes must continue to -reach the contained Gateway with no protocol change. Where the provider's -network posture is unrestricted this is straightforward, but it also means -containment buys nothing at the network layer, and the RFC should not let the -word "contained" imply otherwise. - -**Host reach is reduced to a staging channel.** A contained Gateway cannot see -the user's desktop, and it cannot open the user's working tree, because -arbitrary host paths cannot be projected in. What remains is a directory through -which the host can hand it files. For an agent expected to work on the user's -behalf in the user's environment, that is a real capability regression: "the -user asks the agent to fix a file in their repository" is not expressible unless -something explicitly stages that content across, which is a product decision -about mediated access rather than a transparent capability. It remains the -central unresolved question below. - -### Ownership, and why this does not belong in the Gateway - -The seam belongs to the launcher. It must not live in OpenClaw core, and the -reason is the argument from the Motivation rather than a packaging preference: a -containment decision made by the process being contained is made by code the -attacker already controls. - -The launcher therefore owns the `ContainmentProvider` interface, the capability -descriptor and its schema version, the failure taxonomy, provider selection, the -fail-closed rule, and the reporting of containment posture. OpenClaw core owns -none of it and gains no containment configuration, which is what keeps the -Gateway unchanged and keeps a preview OS dependency out of the cross-platform -build. - -Individual providers are platform integrations and should live wherever their -platform dependency is maintainable, so the seam must support an out-of-tree -provider without special-casing it. On Windows the launcher and the provider -naturally live with the packaging work described in -[#58](https://github.com/openclaw/rfcs/pull/58). - -One consequence deserves stating plainly: **posture reported by the Gateway is -not evidence.** A compromised Gateway can claim to be contained. Containment -status is therefore a launcher-side assertion, and any check that needs to be -trustworthy — an administrator confirming a managed device is compliant — must -observe the boundary from outside the Gateway rather than ask it. +**Figure 1.** The launcher establishes the boundary; the Gateway starts inside +it and never runs outside it. + +This is an **identity** boundary and the descriptor has to say so. Per [MXC's +backend +docs](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/state-aware-rust.md), +the network is unrestricted — a process inside can listen on a port reachable +over localhost — so `networkPosture` is unsupported. There's no UI-restriction +primitive either, though contained code can't reach the host's desktop or +clipboard. + +Host paths are the interesting part, and they drive much of the design. +Arbitrary path projection is rejected outright — you can't map in the user's +documents folder. What exists instead is one OS-created directory per sandbox, +documented as "a directory shared between the calling user and this isolated +agent user, through which the caller can stage files into the session", with +three properties that matter: + +- **Asymmetric.** Each isolated user sees only its own; the caller sees every + concurrent sandbox's. The caller is the privileged side. +- **Ephemeral.** Created at provision, deleted at deprovision. A channel, not + storage. +- **Not the working directory.** It doesn't change where the workload runs. + +Two other provider inputs shape things. Provisioning takes an optional +application identifier — documented as "the Package Family Name for a packaged +app", carried so "a future OS contract acting on the calling application's +identity needs no breaking change." That's a concrete, mechanical link to +[#58](https://github.com/openclaw/rfcs/pull/58): package identity is an *input* +to containment. It also takes an optional user-identity bundle, which is the +hook for giving a contained unit an identity of its own. + +Availability: the backend is experimental, gated behind both an explicit flag +and an OS feature flag, and needs a recent [Insider +build](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimental/preview-build-26300-8553). + +### What this costs + +**Durable state has to live outside.** Both the account and the staging +directory die at deprovision, so nothing kept inside is durable. That settles it +rather than leaving it open: durable state is host-side and staged in. The catch +is that this store then holds exactly the credentials containment was supposed +to make less valuable, so it needs protecting accordingly. + +**Don't run the Gateway from the staging directory.** It's ephemeral, +caller-writable, and explicitly not the working directory. Stage through it, +then materialize inside. + +**Host reach drops to a staging channel.** No desktop, and no user working tree. +"Fix a file in my repo" isn't expressible unless something explicitly stages +that content across — a product decision about mediated access, not a +transparent capability. See [Unresolved questions](#unresolved-questions). ### Bypass resistance -Containment that the user can accidentally skip is a default, not a boundary. -Two paths must be closed for a deployment that has selected containment: +Containment you can skip by accident is a default, not a boundary. Two paths +need closing: -- **A second entry point.** If the packaged launcher is the supported entry - point but a source checkout, a stale shortcut, or a copied binary can still - start a Gateway directly, the boundary is optional in practice. The launcher - must be the only supported way to start a managed Gateway, and the others must - fail rather than silently starting uncontained. -- **A native start command.** `openclaw gateway` starts a Gateway by design. +- **A second entry point.** If a source checkout, stale shortcut, or copied + binary still starts a Gateway directly, the boundary is optional in practice. +- **`openclaw gateway`.** It starts a Gateway by design. [#58](https://github.com/openclaw/rfcs/pull/58) already makes this a release - requirement for its own payload-activation contract — that host lifecycle - operations and native `openclaw gateway` commands "cannot create conflicting - managed Gateway instances or bypass staged-payload activation" — and the same - requirement extends to containment. - -Because the Gateway cannot be trusted to refuse to start, this cannot be -implemented as a check inside it. It has to come from the deployment shape: the -managed installation exposes the launcher as its entry point, and a Gateway -started another way is a different, unmanaged installation rather than a -containment failure of the managed one. Making that distinction observable — so -an administrator can tell the two apart — is part of the readiness bar. + requirement for its own payload activation — that host operations and native + `openclaw gateway` commands "cannot create conflicting managed Gateway + instances or bypass staged-payload activation" — and the same applies here. + +Since the Gateway can't be trusted to refuse to start, this can't be a check +inside it. It comes from deployment shape: the managed install exposes the +launcher as its entry point, and a Gateway started some other way is a +*different, unmanaged install* rather than a containment failure of the managed +one. Making that difference observable is part of the readiness bar. ### Readiness bar -Contained execution should be recommended for a class of deployment only once: +Recommend contained execution for a deployment class only once: -- The provider's capability descriptor is accurate, and the launcher refuses - configuration the provider cannot enforce. -- Gateway state has defined, tested persistence across - `provision`/`deprovision`, including across host restarts, and the host-side - store holding it is protected commensurately with the credentials it contains. -- Orphaned environments left by a crashed or replaced supervisor are reconciled - rather than accumulating, since `deprovision` is explicit and no other party - performs it. +- The capability descriptor is accurate and the launcher refuses what a provider + can't enforce. +- State persists across `provision`/`deprovision` and host restarts, and the + host-side store is protected like the credentials it holds. +- Orphaned environments get reconciled instead of accumulating. - Anything the host reads back from the staging channel is treated as untrusted input, with the parsing boundary identified and tested. -- Clients, channels, and nodes connect to a contained Gateway with no protocol - change and no additional user step. -- `attach` makes ordinary CLI use cheap, rather than paying provisioning cost - per command. -- Startup, crash, restart, and teardown paths are covered, including orphaned - environments after an ungraceful host shutdown. -- Containment posture is reported by the launcher rather than by the Gateway, a - fallback to uncontained execution is loud rather than silent, and a managed - installation is distinguishable from a Gateway started outside it. -- The platform primitive is generally available rather than preview-gated, and - its owner is prepared to describe it as a security boundary. The - `microsoft/mxc` README currently states the opposite for its preview backends, - and that alone blocks any claim that containment is a defense OpenClaw can - rely on today. - -Until then, contained execution should ship as an explicitly experimental, -opt-in posture for deployments that want to exercise it. - -## Threat model and residual risk - -The threat this addresses is a Gateway that is induced to act against the user: -prompt injection reaching the agent loop, a hostile or compromised plugin or -skill, or a supply-chain compromise of a dependency loaded in-process. The -attacker is assumed to achieve arbitrary code execution inside the Gateway -process. Attackers who already hold the user's credentials, local administrator, -or kernel-level access are out of scope, as is a malicious OpenClaw build. - -### What the identity boundary removes - -Contained, the attacker no longer runs as the signed-in user. Reads and writes -against the user's profile, `HKCU`, browser and credential stores, SSH keys, -startup entries, and any resource whose ACL grants that user are refused by the -operating system rather than by OpenClaw's own policy. On this provider the -attacker also cannot observe or drive the user's desktop, clipboard, or input, -which removes screen-scraping and synthetic-input paths to the user's other -applications. When the session is deprovisioned, the account and its session -state go with it, so filesystem persistence inside the boundary is not durable. - -### What the attacker still has - -This is an identity boundary and nothing more. The following survive it: - -- **Everything the Gateway legitimately holds.** Provider API keys, channel - credentials, pairing records, and conversation history are inside the boundary - by construction. Containment limits reach into the *user's* assets; it does - not protect the Gateway's own secrets from code running as the Gateway. - Credential exfiltration remains fully available. -- **The network, including loopback.** The provider enforces nothing on the - network, and a process inside can reach and be reached over localhost. Host - services that authenticate by origin rather than by credential — development - servers, local databases, metadata or agent endpoints, other OpenClaw - Gateways, and local MCP servers — remain reachable from inside the boundary. - For a workstation with local services listening on loopback, this is the - largest surviving path, and it means containment must not be described as a - network control. -- **Outbound egress.** Exfiltration to the internet is unrestricted. -- **The staging channel, in both directions.** The shared directory crosses the - boundary, and its visibility is asymmetric in the caller's favour: each - contained unit sees only its own, while the caller sees every concurrent one. - Contained code can therefore write into a directory that a host-side process - running as the user will later read, so anything the host parses from it is - untrusted input — the most likely place for a boundary-crossing bug. Any - future arbitrary path projection reopens the boundary for exactly what it - projects and should be treated as such rather than as a convenience. -- **The host-side state store.** Because durable state cannot live inside the - boundary, it lives on the host and holds the credentials containment is meant - to make less valuable to steal. Compromising the host side recovers them, so - containment moves the crown jewels rather than eliminating them. -- **The launcher itself.** It runs as the user and is by construction able to - start, stop, and stage into the boundary, so compromising it defeats the whole - design. That is why it must stay small, load nothing extensible, and never - execute agent-directed work: everything added to the launcher is added to the - trusted computing base. An attacker who can replace the launcher binary or its - entry-point registration has already won, which makes package integrity and - signing a dependency of this design rather than an adjacent concern. -- **The user's own actions.** A contained agent can still return output that - induces the user to run something themselves. - -### Consequences - -Two follow directly. First, containment reduces blast radius but does not reduce -the value of the credentials the Gateway holds, so credential hygiene, scoping, -and rotation remain as important as before. Second, because loopback survives, a -deployment that relies on local services trusting their callers gets materially -less protection than the headline claim suggests, and should be told so rather -than left to infer it. - -Finally, the platform primitive is preview-gated and its own documentation -declines to describe its profiles as security boundaries today. Until that -changes, contained execution should be presented as defense in depth and a -direction of travel, not as a control a deployment may rely on. - -## Compatibility, migration, and rollback - -**Existing deployments are unaffected.** Containment is opt-in and off by -default. A deployment that does not configure a provider behaves exactly as it -does today, on every platform, with no new dependency and no change to startup. - -**Adoption is a migration, not a flag.** Turning containment on moves the -Gateway to a new principal, so state that lived under the user's profile is not -automatically visible to it. Before this can be recommended, the implementation -must provide a supported path that relocates or re-establishes configuration, -credentials, and pairing records for the contained identity, and must report -clearly when it cannot. Because the Windows provider's `statePersistence` is -unresolved, this is a blocking dependency rather than a detail — a migration -that silently produces an empty Gateway would look identical to a working one -until the first channel fails to authenticate. - -**Rollback must be routine.** Disabling containment must return the deployment -to its previous uncontained behavior without data loss, which requires that -adoption never destroys the pre-migration state as part of moving it. Rollback -should be a supported operation exercised in testing, not a recovery procedure -discovered during an incident. - -**Downgrade.** An OpenClaw version that predates the seam ignores the -configuration and starts uncontained. That is a silent posture change of exactly -the kind this proposal argues against, so the configuration must be rejected as -unknown rather than ignored on versions that cannot honor it, and the readiness -bar should not be considered met until that behavior exists. - -**No protocol or wire compatibility impact.** Clients, channels, and nodes are -unchanged, so a contained and an uncontained Gateway are indistinguishable to -them. - -## Implementation plan - -Staged so that each phase produces something reviewable and none of them -requires the preview OS dependency to be present. - -1. **Contract only.** Land the provider interface, versioned capability - descriptor, failure taxonomy, selection and fail-closed logic, and posture - reporting in the launcher, with a null provider. No change to OpenClaw core. - Proof: unit coverage for selection, refusal, and reporting, exercised through - the real interface rather than through source inspection. -2. **Windows provider, behind experimental opt-in.** Implement `probe`, - `provision`, `start`, `attach`, `stop`, and `deprovision` against the OS - primitive, reporting the honest descriptor. Proof: a Gateway that starts - contained on a capable build, is reachable from a client, and runs under an - account that is demonstrably not the signed-in user; and a - capable-build-absent host that fails closed with the reason. -3. **State and migration.** Resolve persistence, then implement adoption and - rollback with tested restart, upgrade, and downgrade behavior. Proof: a - contained Gateway that survives host restart with its pairing intact, and a - rollback that restores the prior deployment. -4. **Bypass resistance.** Make the launcher the entry point for a managed - installation and make an unmanaged Gateway distinguishable from a managed - one. Proof: an administrator-observable signal that does not depend on asking - the Gateway. -5. **Readiness review.** Re-evaluate against the readiness bar and decide - whether contained execution can be recommended for any deployment class. - -Phases 1 and 2 are owned by whoever owns the launcher — on Windows, the -packaging work in [#58](https://github.com/openclaw/rfcs/pull/58). Phase 3 -should not begin while the persistence question is open. Phase 4 depends on -#58's own entry-point and activation work rather than duplicating it, and -phase 5 is a maintainer decision rather than an implementation task. +- Clients, channels, and nodes connect with no protocol change and no extra user + step. +- `attach` keeps ordinary CLI use cheap. +- Startup, crash, restart, and teardown are covered, including ungraceful + shutdown. +- Posture is reported by the launcher, fallback is loud, and a managed install + is distinguishable from one started outside it. +- The platform primitive is generally available and its owner is willing to call + it a security boundary. The `microsoft/mxc` README currently says the opposite + about its preview backends, which alone blocks claiming this as a defense. + +Until then it ships experimental and opt-in. + +## Threat model + +The threat is a Gateway induced to act against you: prompt injection reaching +the agent loop, a hostile plugin or skill, a compromised dependency. Assume +arbitrary code execution inside the Gateway. Out of scope: an attacker who +already has your credentials, local admin, or kernel access, and a malicious +OpenClaw build. + +**What the boundary removes.** The attacker no longer runs as you. Your profile, +`HKCU`, browser and credential stores, SSH keys, and startup entries are refused +by the OS rather than by our own policy. They also can't watch your screen or +inject input, which kills the screen-scraping and synthetic-input paths into +your other apps. Filesystem persistence inside the boundary dies at deprovision. + +**What survives.** This is an identity boundary and nothing more: + +- **Every credential the Gateway holds.** Provider keys, channel credentials, + pairing records, conversation history — all inside by construction. + Containment limits reach into *your* assets; it does nothing to protect the + Gateway's own secrets from code running as the Gateway. Exfiltration is fully + available. +- **The network, including loopback.** Nothing is enforced, and a process inside + can reach and be reached over localhost. Host services that trust their caller + by origin — dev servers, local databases, other Gateways, local MCP servers — + stay reachable. On a workstation with local services listening, this is the + biggest surviving path, and it's why this must never be described as a network + control. +- **The staging channel, both ways.** Contained code can write into a directory + a host-side process running as you will later read, so anything the host + parses from it is untrusted input. This is the likeliest place for a + boundary-crossing bug. +- **The host-side state store,** which holds the credentials above. Containment + moves the crown jewels; it doesn't eliminate them. +- **The launcher itself.** It runs as you and can start, stop, and stage into + the boundary, so compromising it defeats everything. Anything added to the + launcher is added to the TCB, which makes package integrity and signing a + dependency of this design rather than an adjacent concern. +- **You.** A contained agent can still talk you into running something yourself. + +One consequence worth stating: **posture reported by the Gateway is not +evidence.** A compromised Gateway can claim to be contained. Containment status +is a launcher-side assertion, and anything that needs to be trustworthy — an +admin verifying a managed device — has to observe from outside. + +## Rollout + +**Nothing changes by default.** Containment is opt-in and off. Deployments that +don't configure a provider behave exactly as they do now, everywhere. + +**Turning it on is a migration.** The Gateway moves to a new principal, so state +under your profile isn't automatically visible to it. Adoption has to relocate +or re-establish config, credentials, and pairing records, and say clearly when +it can't — a migration that silently produces an empty Gateway looks identical +to a working one until the first channel fails to authenticate. This depends on +the unresolved persistence question. + +**Rollback is routine, not recovery.** Turning containment off returns to the +previous behavior without data loss, which means adoption must never destroy +pre-migration state while moving it. + +**Downgrade.** A version predating the launcher contract would ignore the config +and start uncontained — the silent posture change this whole RFC argues against. +It must reject unknown config instead, and that behavior gates the readiness +bar. -## Decision requested +**Phases.** None of the first three require the preview OS dependency to begin. + +1. **Contract only.** Provider interface, versioned descriptor, failure + taxonomy, selection, fail-closed, posture reporting — in the launcher, with a + null provider. No change to OpenClaw core. Proof: tests through the real + interface for selection, refusal, and reporting. +2. **Windows provider, experimental.** Implement the lifecycle against the OS + primitive with an honest descriptor. Proof: a Gateway that starts contained, + is reachable from a client, and runs under an account that demonstrably isn't + the signed-in user; plus a host without the capability failing closed with a + reason. +3. **State and migration.** Resolve persistence, then adoption and rollback with + tested restart, upgrade, and downgrade. +4. **Bypass resistance.** Make the launcher the managed entry point and make an + unmanaged Gateway distinguishable. Proof: an admin-observable signal that + doesn't involve asking the Gateway. +5. **Readiness review.** Maintainer decision against the bar. + +Phases 1–2 belong to whoever owns the launcher — on Windows, the +[#58](https://github.com/openclaw/rfcs/pull/58) work. Phase 3 shouldn't start +while persistence is open, and phase 4 builds on #58's entry-point work rather +than duplicating it. -This RFC asks maintainers for one decision: **is launcher-established, opt-in -Gateway containment a direction OpenClaw wants to support, given that the first -available provider offers identity isolation only and is preview-gated?** +## Decision requested -A "yes" authorizes phase 1 — the contract, implemented in the launcher, with no -provider and no default change. Notably it requires no change to OpenClaw core -at all, since the Gateway is unchanged and unaware. It does not commit OpenClaw -to the Windows provider, to a default posture, or to a timeline. +**Is launcher-established, opt-in Gateway containment a direction we want to +support, given that the first provider offers identity isolation only and is +preview-gated?** -Two secondary decisions follow only from a "yes": whether the launcher-side -contract should be specified here or folded into -[#58](https://github.com/openclaw/rfcs/pull/58), which already owns the host app -it would live in; and whether this RFC's number is correct given the -repository's current numbering practice. Both are noted rather than assumed. +A yes authorizes phase 1: the contract, in the launcher, no provider, no default +change — and notably no change to OpenClaw core, since the Gateway is unchanged +and unaware. It doesn't commit us to the Windows provider, a default posture, or +a timeline. ## Rationale -**Why contain the Gateway rather than only the session.** Per-session sandboxes -were designed for the case where the agent's *work* is untrusted. The case that -motivates this RFC is where the agent's *judgment* is untrusted, because -judgment is what prompt injection attacks. Once that is the threat, the -component holding the credentials and choosing the actions has to be inside a -boundary too. - -**Why a provider seam rather than a Windows code path.** The problem is not -Windows-specific; only this particular primitive is. macOS and Linux have -different mechanisms with different capability profiles, and the industry is -actively producing more of them. A seam with an explicit capability descriptor -lets those arrive as contributions, keeps Windows-specific lifecycle code out of -the Gateway's startup path, and forces each provider to state its limits in a -form OpenClaw can act on. Absent a seam, the first implementation becomes the de -facto contract, and its unstated assumptions get baked in. - -**Why Isolation Sessions rather than WSL.** WSL is the current recommended local -Gateway path on Windows and works, but it contains by moving the Gateway into a -second operating system. That brings its own filesystem, package management, -credential storage, update cadence, and failure modes, and it distances the -Gateway from the Windows environment the user works in. An isolation session -keeps the Gateway on Windows and changes only the principal it runs as, which is -the narrower change and the one that matches the actual threat. - -**Why Isolation Sessions rather than a VM or Windows Sandbox.** Both provide a -stronger boundary than an identity boundary, and for some deployments that is -the right answer. They are a poor default for an always-on daemon: the resource -cost is charged continuously, startup is slow enough to be noticed, and their -lifecycle model is oriented toward disposable sessions rather than a service the -user expects to already be running. The value of the isolation-session primitive -is that its cost profile makes contained execution plausible as an eventual -default rather than an expert-only mode. - -**Why not AppContainer or a process-level container.** Process-level containment -on Windows restricts a process that still runs as the user. It is useful, and -MXC's default Windows backend uses it, but the principal is unchanged, so the -per-user token reach that motivates this RFC is not removed. Isolation sessions -change the principal, which is the property being argued for. - -**Why not rely on splitting the Gateway instead.** -[#42026](https://github.com/openclaw/openclaw/issues/42026) would give each -agent its own runtime and its own secrets, which is a genuine reduction in blast -radius and independently worth doing. It is not a substitute, because it -partitions between agents while leaving every resulting process running as the -signed-in user. Decomposition means a compromised agent cannot reach another -agent's secrets; containment means it cannot reach the user's. A deployment -wants both, and adopting either does not foreclose the other. Writing the seam -against a unit of containment rather than against "the Gateway" is what keeps -that true. - -**Why the seam is not a hook inside the Gateway.** A containment seam inside -OpenClaw core would be easier to ship and would put the capability where the -rest of the runtime configuration lives. It is the wrong place for exactly one -reason, and it is decisive: the Gateway is the component the threat model treats -as compromised, so a decision it makes about its own containment is a decision -the attacker makes. Establishing the boundary in a launcher costs a separate -executable and an entry-point contract, and buys a boundary that exists before -any untrusted code runs. The corollary — that OpenClaw core changes not at all — -is a benefit rather than a consolation, since it keeps a preview OS dependency -out of the cross-platform build and leaves nothing to configure incorrectly. - -**Why fail-closed by default.** A containment control that silently degrades to -no containment produces the worst outcome: an operator who believes they are -protected and is not. Failing closed makes the absence of the boundary a visible -event. Deployments that prefer availability can opt out, having said so. - -**Why argue the direction before the primitive is ready.** The seam, the -capability descriptor, and the readiness bar are useful independently of any one -provider, and they are what determine whether contained execution is adopted -honestly. Designing them while the first provider is still preview-quality is -cheaper than retrofitting them around a shipped Windows-specific implementation. +**Why contain the Gateway, not just the session.** Per-session sandboxes assume +the agent's *work* is untrusted. Prompt injection attacks its *judgment*. Once +that's the threat, the thing holding the credentials and choosing the actions +has to be inside a boundary too. + +**Why the launcher, not a hook in the Gateway.** A seam in OpenClaw core would +be easier to ship and would sit where the rest of the runtime config lives. It's +wrong for one decisive reason: the Gateway is what the threat model treats as +compromised, so a decision it makes about its own containment is a decision the +attacker makes. A separate executable costs an entry-point contract and buys a +boundary that exists before any untrusted code runs. That OpenClaw core changes +not at all is a bonus — nothing to misconfigure, and no preview OS dependency in +the cross-platform build. + +**Why a provider contract, not Windows-specific code.** The problem isn't +Windows-specific; only this primitive is. macOS and Linux have different +mechanisms with different capability profiles. An explicit descriptor lets those +arrive later and forces each to state its limits in a form we can act on. +Without it, the first implementation becomes the de facto contract and its +unstated assumptions get baked in. + +**Why not just split the Gateway.** +[#42026](https://github.com/openclaw/openclaw/issues/42026) gives each agent its +own runtime and secrets — genuinely worth doing, and not a substitute. It +partitions between agents while every resulting process still runs as you. +Decomposition means a compromised agent can't reach another agent's secrets; +containment means it can't reach yours. + +**Why not WSL.** It contains by moving the Gateway into a second OS, with its +own filesystem, packages, credentials, update cadence, and failure modes, and it +distances the Gateway from the environment you actually work in. An isolation +session keeps it on Windows and changes only the principal. + +**Why not a VM or Windows Sandbox.** Stronger boundary, and right for some +deployments. Poor default for an always-on daemon: continuous resource cost, +noticeable startup, and a lifecycle built for disposable sessions. The +isolation-session cost profile is what makes contained execution plausible as an +eventual default rather than an expert mode. + +**Why not AppContainer.** Process-level containment restricts a process that +still runs as you — it's MXC's default Windows backend, and useful, but the +principal is unchanged, so the token reach motivating this RFC survives. + +**Why fail closed.** A control that silently degrades to no control produces the +worst outcome: someone who believes they're protected and isn't. ## Unresolved questions -### Blocking — these gate the readiness bar - -- **How does a contained Gateway reach the user's files?** Arbitrary host paths - cannot be projected in; what exists is a staging directory the host can write - into. So the question is not whether sharing is possible but what the product - should do with a mediated channel: stage an explicitly selected working set in - and results out, run a filesystem bridge over the existing protocol similar to - the remote filesystem bridge already used for sandboxes, wait for a future OS - projection primitive, or accept that a contained Gateway operates only on its - own workspace. Each reopens the boundary to a different degree, and the choice - determines how useful contained execution actually is. -- **What holds durable state, and how is it protected?** State must live - host-side because the account and its staging directory are destroyed at - deprovision. What is that store, how is it protected given that it holds the - Gateway's credentials, and how does an existing uncontained installation - migrate into it? This blocks implementation phase 3. -- **Is an identity-only boundary worth adopting?** The provider enforces nothing - on the network and loopback survives containment. Should OpenClaw require a - network-capable provider before recommending containment for any deployment - class, or is the reduction in user-asset reach sufficient on its own? - -### Non-blocking — these can be resolved during implementation - -- **How do Windows node capabilities survive?** Computer use, screen capture, - and input injection (cf. [RFC 0025](0025-default-pluggable-computer-use.md)) - require the user's desktop, which contained code cannot reach. Does the - Windows node remain outside the boundary and connect to the contained Gateway - as a node, and what does that imply for the boundary's value? -- **What are the availability and gating rules?** The primitive is preview-gated - and requires a recent Windows build. How does OpenClaw express a containment - posture that is unavailable on most hosts without fragmenting the Windows - experience? +### Blocking + +- **How does a contained Gateway reach the user's files?** Arbitrary paths can't + be projected; there's a staging directory. So the question isn't whether + sharing is possible but what to do with a mediated channel: stage an + explicitly selected working set in and results out, run a filesystem bridge + over the existing protocol like the one already used for sandboxes, wait for + an OS projection primitive, or accept that a contained Gateway works only on + its own workspace. Each reopens the boundary differently, and the choice + decides how useful this actually is. +- **What holds durable state, and how is it protected?** It has to be host-side. + What is it, how is it protected given it holds the Gateway's credentials, and + how does an existing install migrate into it? Blocks phase 3. +- **Is an identity-only boundary worth adopting?** Nothing is enforced on the + network and loopback survives. Do we require a network-capable provider before + recommending this, or is reduced reach into user assets enough on its own? + +### Non-blocking + +- **Do Windows node capabilities survive?** Computer use, screen capture, and + input injection (see [RFC 0025](0025-default-pluggable-computer-use.md)) need + the user's desktop, which contained code can't reach. Does the node stay + outside and connect in, and what does that do to the boundary's value? - **Should the contained Gateway carry its own identity?** The provider accepts - an optional identity bundle at provision, so a contained unit could - authenticate as itself rather than inheriting whatever the caller holds. That - would narrow the credential blast radius the threat model calls out, and it - interacts with the per-agent secret isolation proposed in + an identity bundle at provision, which would narrow the credential blast + radius above and interacts with per-agent secret isolation in [#42026](https://github.com/openclaw/openclaw/issues/42026). Out of scope for - phase 1, but the descriptor reserves `workloadIdentity` so it can be answered - without a breaking change. -- **How is autostart handled?** A Gateway expected to run in the background must - start without an interactive logon, and the launcher is what would start it. - What does contained startup look like before or without a signed-in user? -- **Should this contract live here or in #58?** The launcher it belongs to is - already proposed by [#58](https://github.com/openclaw/rfcs/pull/58). Keeping - the contract in a separate RFC keeps it platform-neutral and reviewable on its - own; folding it into #58 keeps the host app's responsibilities in one - document. Maintainers should pick one rather than letting both describe the - launcher. -- **What is the acceptance criterion for calling this a security boundary?** The - platform currently declines to make that claim for preview profiles. OpenClaw - should state in advance what evidence it requires before describing contained - execution as a defense to users. -- **Is this RFC number correct?** It was chosen as the lowest unclaimed integer, - but the repository currently has several open proposals sharing a number, so - the allocation rule is not obviously sequential. A maintainer should confirm - or reassign it. + phase 1; `workloadIdentity` is reserved so it can be answered later. +- **How does autostart work?** A background Gateway has to start without an + interactive logon, and the launcher is what starts it. +- **How do we express a posture most hosts can't support** without fragmenting + the Windows experience? +- **Should this contract live here or in + [#58](https://github.com/openclaw/rfcs/pull/58)?** It belongs to a launcher + #58 already owns. Separate keeps it platform-neutral; folded in keeps the host +app's responsibilities in one document. Pick one rather than letting both +describe the launcher. +- **What would let us call this a security boundary?** The platform won't make + that claim for preview profiles. We should say in advance what evidence we'd + need before describing this as a defense to users. +- **Is `0032` the right number?** It was the lowest unclaimed integer, but + several open proposals currently share numbers, so allocation clearly isn't + sequential. A maintainer should confirm or reassign. From b5df9aca318b7d8280d341c0ff9b8a7ea8ba4e1c Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Tue, 18 Aug 2026 18:26:52 -0700 Subject: [PATCH 08/15] docs: address hostile review of RFC 0032 Fixes several factual errors, verified against microsoft/mxc at 0aaa2afa (2026-08-18). The earlier draft was written against a clone that was eight days stale, and the preview API had changed underneath it. Corrections: - Lifecycle was wrong. The real phases are provision, start, exec, stop, deprovision. The draft omitted exec and invented an attach phase the provider explicitly defers. - "Every operation is idempotent" was false and dangerous: provision mints a fresh identity on every call, so a naive retry leaks accounts. Replaced with per-operation semantics and a durable pre-provision record. - Detached execution is deferred upstream and a spawned process stays tethered to its caller, so the launcher cannot exit. It is a long-lived supervisor, not a bootstrapper, and that cost is now stated. - The caller-supplied identity bundle was removed upstream. Dropped the workloadIdentity capability and the claim built on it; appId is now described as inert forward-compatibility metadata rather than a mechanical link to package identity. - PR #55 was mischaracterized. It also contains the long-lived Gateway, and brokers model credentials so neither Gateway nor worker holds provider values, which solves a residual risk this RFC cannot. Now positioned as complementary and the better answer where available, differing by deployment context rather than by scope. - #58 proposes a launcher, it does not already build one. #42026 would make the unit of containment a live question, not a drop-in swap. - AppContainer dismissal was too glib. It is MXC's default Windows backend; the real objection is that it is an allowlist over the same token, and its universally available tier enforces filesystem policy by mutating DACLs on host paths. - The downgrade requirement was impossible as written. An older launcher cannot be taught to reject config it never knew; this needs a minimum version enforced by packaging. - Fallback no longer contradicts fail-closed: it means another provider of equal or greater strength, never uncontained. Threat model restructured around the right baseline. Several residual risks are unchanged from an uncontained Gateway rather than regressions - shared-ACL reach, local IPC, egress, and persistence, which is now confined to a throwaway account instead of the user's profile. Separated from the risks containment actually shifts or introduces: the host-side state store, the staging channel, and the launcher. Added a TCB table covering the SDK, the SYSTEM service, and the kernel, and noted that a compromised Gateway commanding an authorized desktop node is not contained by this boundary. Also drops the claim that the Gateway is unaffected. It needs no containment code, but a new principal means a different profile, registry hive, and credential store, and whether the Docker and SSH sandbox backends survive that is now an explicit phase 2 question. All MXC citations pinned to a commit with a verification date, since a load-bearing claim went stale within a week. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 337 +++++++++++++----- 1 file changed, 239 insertions(+), 98 deletions(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index e764dc56..0688c489 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -20,8 +20,8 @@ registry, and tokens. This proposes containing the Gateway itself — and, importantly, doing it from *outside* the Gateway. A small trusted launcher provisions an OS-managed -boundary and starts the Gateway inside it. The Gateway is unchanged and unaware; -it simply never runs anywhere else. +boundary and starts the Gateway inside it. The Gateway needs no containment code +of its own; it simply never runs anywhere else. Windows Isolation Sessions are the first provider: the OS mints a fresh throwaway account, runs the Gateway in a session bound to it, and tears both @@ -94,7 +94,7 @@ with an opaque OS-assigned name, start a session for it, run processes inside, then stop the session and delete the account. [MXC states the -requirements](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/oneshot.md) +requirements](https://github.com/microsoft/mxc/blob/0aaa2afa6588d4aee34b35efb290308bb6f84fa1/docs/isolation-session/oneshot.md) plainly: "per-execution OS-isolated identity so the workload's actions cannot pollute the calling user's NTFS / registry / token state", with an "OS-managed session lifecycle that the OS-side service tears down cleanly when the calling @@ -121,15 +121,26 @@ stays up, that's the right shape. worth having, so the contract below targets a *unit of containment* rather than a monolithic Gateway. - [**#55 (OpenShell worker - provider)**](https://github.com/openclaw/rfcs/pull/55) contains a session's - worker. This contains the Gateway. They nest. + provider)**](https://github.com/openclaw/rfcs/pull/55) overlaps most and + deserves care: it also runs the long-lived Gateway inside a sandbox, and goes + further by brokering model credentials through `inference.local` so neither + Gateway nor worker holds provider credential values. That directly fixes the + residual risk this RFC can't — a contained Gateway still holds its own + secrets. The difference is deployment context. #55's Gateway sandbox is + *operator-created* on a container platform, which suits cloud and server + deployments; it doesn't help a personal Windows machine where there is no + operator and no OpenShell. The two are complementary: #55 is the better answer + wherever it's available, and its credential brokering is the direction this + RFC should follow rather than compete with. ## Goals - Establish the boundary in a launcher outside the Gateway, so a compromised Gateway has no uncontained mode to reach. -- Keep the Gateway unchanged and unaware — no new config, no runtime branch, no - protocol change. +- Add no containment code, config, or runtime branch to the Gateway, and no + protocol change. This is *not* a claim that the Gateway is unaffected — a new + principal means a different profile, registry hive, and credential store, and + what that breaks is tracked below. - Define a platform-agnostic `ContainmentProvider` contract so this isn't a Windows fork of the startup path. - Make each provider's limits explicit and machine-readable, so one that can't @@ -144,7 +155,11 @@ stays up, that's the right shape. - **Packaging and distribution.** That's [#58](https://github.com/openclaw/rfcs/pull/58). This RFC doesn't depend on - MSIX; it needs *a* launcher, and on Windows #58 already builds one. + MSIX; it needs *a* launcher, and #58 proposes one for Windows. #58 is itself + an open draft, so this RFC depends on a proposal rather than on shipped code. +- **Containing what the Gateway can command.** Authorized desktop nodes and + other action fulfillers sit outside this boundary and need their own + containment story. Out of scope here. - **Replacing per-session sandboxes.** `SandboxBackend` and [#55](https://github.com/openclaw/rfcs/pull/55) contain a session's work. This contains the Gateway. @@ -174,14 +189,27 @@ This RFC adds one responsibility: establish the boundary first. **The unit of containment runs inside.** Today that's the Gateway, along with the agent loop, plugins, scheduler, and its working state. If -[#42026](https://github.com/openclaw/openclaw/issues/42026) lands, the unit -becomes each runtime instead, and nothing below changes. - -**Everything else is untouched.** Clients, channels, and nodes reach the Gateway -through existing endpoints. Per-session sandboxes still work; containment nests. -The Gateway still owns its own config, credentials, and pairing records — the +[#42026](https://github.com/openclaw/openclaw/issues/42026) lands, the unit is +whichever process holds the credentials and runs the agent loop — likely each +runtime, and possibly the control plane too, since it keeps channels, routing, +and cron. The provider contract below still applies, but which unit to contain, +and how many, becomes a live question rather than a given. + +**The protocol surface is untouched, but the environment isn't.** Clients, +channels, and nodes reach the Gateway through existing endpoints, and the +Gateway still owns its own config, credentials, and pairing records — the launcher provisions an environment, it doesn't broker credentials. +What does change is everything a process inherits from its principal: profile +path, registry hive, and credential store. Two consequences we should not gloss +over. Migration has to move or re-establish that state (see +[Rollout](#rollout)). And per-session sandbox backends assume things about their +environment — the Docker backend needs to reach a daemon, the SSH backend needs +keys and known hosts — none of which is guaranteed for a throwaway account. +Whether those keep working under containment is an implementation question phase +2 has to answer, and if the answer is no for some backend, that is a real +capability loss rather than a detail. + ### The `ContainmentProvider` contract The launcher implements and consumes this. A provider answers two questions: @@ -189,18 +217,19 @@ what boundary can this host actually give me, and how do I start and stop something inside it. **Capabilities.** A provider declares its boundary honestly, because an -overstated boundary is worse than none: +overstated boundary is worse than none. These are enumerated values, not free +text, so the launcher can compare two providers and refuse a weaker one: -| Capability | Meaning | +| Capability | Values | |---|---| -| `identityIsolation` | Does the unit run as a principal distinct from the user, and is it per-instance? | -| `statePersistence` | Does state inside survive the lifecycle, and across what — instance, host, nothing? | -| `hostPathProjection` | Can arbitrary host paths be mapped in? | -| `stagingChannel` | Is there a directory for moving files across, and what's its lifetime and visibility? | -| `networkPosture` | What can be enforced on the network. Includes "nothing". | -| `hostUiReach` | Can contained code see or drive the user's desktop, clipboard, input? | -| `workloadIdentity` | Can the unit carry its own identity instead of borrowing the caller's? | -| `lifecycleOwner` | Is teardown guaranteed by the OS, or do we have to drive it? | +| `identityIsolation` | `none` (same principal), `shared` (one distinct principal), `perInstance` (fresh principal per environment) | +| `statePersistence` | `none`, `perInstance`, `perHost` | +| `hostPathProjection` | `none`, `explicitPaths` | +| `stagingChannel` | `none`, or a directory with a declared lifetime (`ephemeral`, `durable`) and visibility (`callerSeesAll`, `mutual`) | +| `networkPosture` | `unrestricted`, `egressFiltered`, `isolated` | +| `hostUiReach` | `full`, `none` | +| `lifecycleOwner` | `os` (teardown guaranteed), `caller` (we must drive it) | +| `detachedExecution` | `supported`, `deferred` — whether the launcher may exit after starting the unit | `hostPathProjection` and `stagingChannel` are deliberately separate. Being able to hand a file across is not the same as giving an agent the user's working @@ -210,39 +239,65 @@ The descriptor is versioned. The launcher refuses a version it doesn't understand rather than treating absent fields as "unsupported" — an unrecognised capability must never quietly become a claim about the boundary. -**Lifecycle:** +**Lifecycle.** This mirrors the provider's own phases rather than inventing new +ones: ``` -probe -> provision -> start -> attach -> stop -> deprovision +probe -> provision -> start -> exec -> stop -> deprovision ``` -`probe` reports availability and capabilities with no side effects. `attach` -reconnects to an already-running unit across separate invocations, so ordinary -commands don't each pay provisioning cost. +`probe` reports availability and capabilities with no side effects. `exec` is +what actually launches the Gateway inside the environment; the earlier phases +only build the place it runs. + +**The launcher stays alive.** On the Windows provider, detached execution — a +process outliving the call that started it — is explicitly deferred, and a +spawned process stays tethered to the caller's lifetime. So the launcher is not +a fire-and-forget bootstrapper. It remains running as the Gateway's supervisor +for as long as the Gateway runs, which means it owns crash detection, restart, +and teardown. That is a real cost: it is a second always-on process, and it must +stay in the trusted computing base for the whole session rather than just at +startup. If the provider later supports detached execution, the launcher can +shrink to a bootstrapper and reattach instead; the contract should not assume +either. + +**Idempotency is per-operation, not universal.** The provider's phases behave +differently on repeat, and a supervisor that assumes otherwise leaks accounts: + +| Operation | On repeat | What the launcher must do | +|---|---|---| +| `provision` | **Not idempotent** — mints a fresh identity every call | Write a durable operation record *before* calling, so a crash between call and persist is recoverable rather than orphaning an account | +| `start` / `stop` | Provider-dependent failure on repeat | Track state; don't retry blindly | +| `exec` | Runs the command again, no deduplication | Guard with the launcher's own supervision state | +| `deprovision` | Returns "stale" once the identity is gone | Treat stale as success | -Every operation is idempotent, and `deprovision` is the only destructive one, so -a supervisor can retry anything else safely. `provision` returns an opaque -identifier the launcher persists and reuses to address the environment later, -and reports whether it reused an existing identity. +Only `deprovision` is destructive, but only `deprovision` is safely repeatable, +which is the opposite of the convenient case. `provision` returns an opaque +identifier the launcher persists and uses to address the environment later. -The environment outlives the process that started it. That's what makes `attach` -work, and it means a crashed launcher leaves an environment behind — reconciling -orphans is an obligation, not an edge case. +Because the environment survives until something explicitly deprovisions it, a +crashed or replaced launcher leaves one behind. The launcher therefore needs to +enumerate and reconcile environments it owns at startup. This is a required +operation, not an edge case. **Failure outcomes** are a closed set, so the launcher can act without parsing provider text: | Outcome | Meaning | Behavior | |---|---|---| -| `unavailable` | Provider can't run here — absent, gated off, unsupported build | Fail closed unless fallback is configured | +| `unavailable` | Provider can't run here — absent, gated off, unsupported build | Fail closed, unless a fallback provider of equal or greater strength is configured | | `policy_rejected` | Config was supplied that this provider can't enforce | Fail closed, always | | `stale` | Referenced environment is gone | Re-provision if starting, else surface | | `lifecycle_failed` | Operational failure | Retry per policy, then fail closed | -`policy_rejected` never falls back, even when fallback is on. Asking for a -boundary the provider can't deliver is a config error, not an availability -problem, and quietly running with a weaker boundary is the exact outcome this -contract exists to prevent. +Fallback never means "run uncontained." It means "try the next provider that +still satisfies the requested capabilities." A deployment that wants uncontained +execution turns containment off, which is a visible configuration change rather +than a silent runtime degradation. + +`policy_rejected` never falls back at all, even to a stronger provider. Asking +for a boundary that can't be delivered is a config error, not an availability +problem. ### The Windows provider @@ -280,11 +335,11 @@ it and never runs outside it. This is an **identity** boundary and the descriptor has to say so. Per [MXC's backend -docs](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/state-aware-rust.md), +docs](https://github.com/microsoft/mxc/blob/0aaa2afa6588d4aee34b35efb290308bb6f84fa1/docs/isolation-session/state-aware-rust.md), the network is unrestricted — a process inside can listen on a port reachable -over localhost — so `networkPosture` is unsupported. There's no UI-restriction -primitive either, though contained code can't reach the host's desktop or -clipboard. +over localhost — so `networkPosture` is `unrestricted`. There's no +UI-restriction primitive either, though contained code can't reach the host's +desktop or clipboard. Host paths are the interesting part, and they drive much of the design. Arbitrary path projection is rejected outright — you can't map in the user's @@ -299,13 +354,25 @@ three properties that matter: storage. - **Not the working directory.** It doesn't change where the workload runs. -Two other provider inputs shape things. Provisioning takes an optional -application identifier — documented as "the Package Family Name for a packaged -app", carried so "a future OS contract acting on the calling application's -identity needs no breaking change." That's a concrete, mechanical link to -[#58](https://github.com/openclaw/rfcs/pull/58): package identity is an *input* -to containment. It also takes an optional user-identity bundle, which is the -hook for giving a contained unit an identity of its own. +Provisioning also takes an optional application identifier — documented as "the +Package Family Name for a packaged app", which MXC "neither interprets nor +verifies" and which nothing consumes yet. It is carried verbatim so "a future OS +contract acting on the calling application's identity needs no breaking change." +Today that is inert forward-compatibility metadata, not an enforced link. Worth +noting because it is where package identity from +[#58](https://github.com/openclaw/rfcs/pull/58) would plug in if that OS +contract ever lands — but this RFC should not claim a mechanical link that does +not exist yet. + +**These facts are pinned deliberately.** Every claim here is cited against +`microsoft/mxc` commit +[`0aaa2af`](https://github.com/microsoft/mxc/commit/0aaa2afa6588d4aee34b35efb290308bb6f84fa1), +verified 2026-08-18. This is not pedantry: an earlier draft of this RFC +described an optional caller-supplied identity bundle that the backend accepted +at provision. That surface was removed upstream within about a week, and the +current docs state that `appId` is the only caller knob beyond the network +acknowledgment. Anything preview-gated should be re-verified, not trusted from +memory. Availability: the backend is experimental, gated behind both an explicit flag and an OS feature flag, and needs a recent [Insider @@ -360,7 +427,8 @@ Recommend contained execution for a deployment class only once: input, with the parsing boundary identified and tested. - Clients, channels, and nodes connect with no protocol change and no extra user step. -- `attach` keeps ordinary CLI use cheap. +- Ordinary CLI use doesn't pay provisioning cost per command. +- Orphaned environments are enumerated and reconciled at launcher startup. - Startup, crash, restart, and teardown are covered, including ungraceful shutdown. - Posture is reported by the launcher, fallback is loud, and a managed install @@ -379,41 +447,97 @@ arbitrary code execution inside the Gateway. Out of scope: an attacker who already has your credentials, local admin, or kernel access, and a malicious OpenClaw build. -**What the boundary removes.** The attacker no longer runs as you. Your profile, -`HKCU`, browser and credential stores, SSH keys, and startup entries are refused -by the OS rather than by our own policy. They also can't watch your screen or -inject input, which kills the screen-scraping and synthetic-input paths into -your other apps. Filesystem persistence inside the boundary dies at deprovision. - -**What survives.** This is an identity boundary and nothing more: - -- **Every credential the Gateway holds.** Provider keys, channel credentials, - pairing records, conversation history — all inside by construction. - Containment limits reach into *your* assets; it does nothing to protect the - Gateway's own secrets from code running as the Gateway. Exfiltration is fully - available. -- **The network, including loopback.** Nothing is enforced, and a process inside - can reach and be reached over localhost. Host services that trust their caller - by origin — dev servers, local databases, other Gateways, local MCP servers — - stay reachable. On a workstation with local services listening, this is the - biggest surviving path, and it's why this must never be described as a network - control. -- **The staging channel, both ways.** Contained code can write into a directory - a host-side process running as you will later read, so anything the host - parses from it is untrusted input. This is the likeliest place for a - boundary-crossing bug. -- **The host-side state store,** which holds the credentials above. Containment - moves the crown jewels; it doesn't eliminate them. -- **The launcher itself.** It runs as you and can start, stop, and stage into - the boundary, so compromising it defeats everything. Anything added to the - launcher is added to the TCB, which makes package integrity and signing a - dependency of this design rather than an adjacent concern. +The right comparison throughout is **against an uncontained Gateway today**, not +against a perfect sandbox. Several things below survive containment, and it +matters a great deal whether they survive *worse*, *the same*, or *newly*. + +### What containment removes + +The attacker no longer runs as you. Your profile, `HKCU`, browser and credential +stores, SSH keys, and per-user startup entries are refused by the OS rather than +by our own policy. It can't drive your desktop directly — no screen capture off +your session, no synthetic input into your other apps. Anything it writes into +its own profile disappears when the account is deprovisioned. + +That is the whole claim. It is narrower than "the Gateway is sandboxed." + +### What is unchanged from today + +These survive containment, but a contained Gateway is **no worse than the +uncontained one you're running now**. They are not regressions, and none of them +is an argument against adopting containment — they're an argument against +overselling it. + +- **Anything readable by `Users`, `Authenticated Users`, or `Everyone`.** The + contained account is a normal Windows account, so machine-wide files, `HKLM`, + and world-readable content stay reachable. Containment removes *your* grants, + not the machine's public surface. +- **Local IPC and host services.** Named pipes, RPC/COM, ALPC, shared memory, + and anything on loopback. Services that trust a caller by origin rather than + by credential — dev servers, local databases, other Gateways, local MCP + servers — remain reachable. This is why containment must never be described as + a network control. +- **Outbound egress,** which is unrestricted. +- **Resource exhaustion** of host CPU, disk, and memory. +- **Persistence — but scoped.** The account and its profile survive restarts + until something explicitly deprovisions, so an attacker can persist across + Gateway restarts. Uncontained, that persistence lands in *your* profile and + startup; contained, it is confined to a throwaway account that deprovisioning + destroys. Strictly better, and it makes deprovision cadence a real lever. +- **Anything a compromised Gateway can order someone else to do.** Most + concretely an authorized desktop node (see [RFC + 0025](0025-default-pluggable-computer-use.md)): the Gateway doesn't need + desktop access if it can command a node that has it. Containing the Gateway + does not contain what the Gateway is allowed to drive. Nodes need their own + containment story; that's out of scope here, and this RFC should not be read + as claiming model-directed action is fully contained. - **You.** A contained agent can still talk you into running something yourself. -One consequence worth stating: **posture reported by the Gateway is not -evidence.** A compromised Gateway can claim to be contained. Containment status -is a launcher-side assertion, and anything that needs to be trustworthy — an -admin verifying a managed device — has to observe from outside. +### What containment shifts or newly introduces + +This is the list that deserves scrutiny, because these are the costs rather than +the leftovers. + +- **The Gateway's own credentials stay with it.** Provider keys, channel + credentials, and pairing records live inside by construction. Containment + limits reach into your assets; it does nothing to stop code running as the + Gateway from exfiltrating what the Gateway legitimately holds. Fixing that + needs credential brokering, not containment — see + [#55](https://github.com/openclaw/rfcs/pull/55), which does exactly that. +- **A host-side state store,** new to this design, holding those credentials + outside the boundary. Containment moves the crown jewels rather than + eliminating them, and this store now needs protecting like the Gateway did. +- **The staging channel, in both directions.** Contained code writes into a + directory a host-side process running as you later reads, so anything the host + parses from it is untrusted input. This is a genuinely new boundary-crossing + surface and the likeliest place for a bug. +- **The launcher,** which runs as you, supervises the Gateway for its whole + lifetime, and can start, stop, and stage into the boundary. Compromising it + defeats everything. + +### The trusted computing base + +Bigger than the launcher, and worth naming honestly: + +| Component | Why it's trusted | +|---|---| +| Launcher and its provider implementation | Establishes and supervises the boundary | +| The provider SDK and native bindings | Everything reaches the OS through them | +| The SYSTEM-hosted OS session service | Owns account and session lifecycle | +| Windows kernel and session separation | The enforcement itself | +| Host-side state store and staging parser | Cross-boundary data path | +| Package integrity and signing | An attacker who replaces the launcher has won | + +Two consequences. "The provider declares its capabilities honestly" is a +contract, not a security mechanism — providers must be trusted built-ins or +independently verified, not arbitrary third-party code. And **posture reported +by the Gateway is not evidence**: a compromised Gateway can claim to be +contained, so anything that must be trustworthy has to observe from outside. + +Finally, the platform's own documentation declines to call these profiles +security boundaries today, and session separation does not by itself stop a +kernel-level escape. Contained execution is defense in depth and a direction of +travel, not a control to rely on. ## Rollout @@ -431,10 +555,14 @@ the unresolved persistence question. previous behavior without data loss, which means adoption must never destroy pre-migration state while moving it. -**Downgrade.** A version predating the launcher contract would ignore the config -and start uncontained — the silent posture change this whole RFC argues against. -It must reject unknown config instead, and that behavior gates the readiness -bar. +**Downgrade is a real gap, not a requirement we can write our way out of.** A +launcher version predating this contract ignores the containment config and +starts uncontained — the silent posture change this RFC argues against. We can't +retroactively teach an older build to reject config it has never heard of, so +this has to be handled outside the config: a minimum launcher version enforced +by the deployment, or an install that refuses to downgrade below the version +that introduced containment. Whichever, it belongs to whoever owns packaging, +and the readiness bar depends on it existing. **Phases.** None of the first three require the preview OS dependency to begin. @@ -511,9 +639,20 @@ noticeable startup, and a lifecycle built for disposable sessions. The isolation-session cost profile is what makes contained execution plausible as an eventual default rather than an expert mode. -**Why not AppContainer.** Process-level containment restricts a process that -still runs as you — it's MXC's default Windows backend, and useful, but the -principal is unchanged, so the token reach motivating this RFC survives. +**Why not AppContainer.** MXC's default Windows backend is AppContainer-based, +so this is a real option rather than a strawman — it's just the wrong shape for +a Gateway. AppContainer is a *restriction* model: it keeps your token and takes +access away, so every resource the workload needs has to be enumerated up front. +A Gateway that loads arbitrary plugins, spawns a Node runtime, and starts MCP +servers has an open-ended and unpredictable resource set, which is exactly the +case an allowlist handles worst. It also cuts the other way: the tier available +on every current Windows release enforces filesystem policy by adding ACEs to +host paths, so broadening access means mutating DACLs on the user's real +directories — persistent host-side side effects, granting access to paths that +still belong to the user. Too restrictive to run the workload, too invasive when +you loosen it. An isolation session gives a separate principal with an ordinary +account instead, so nothing has to be enumerated and nothing on the host is +rewritten. **Why fail closed.** A control that silently degrades to no control produces the worst outcome: someone who believes they're protected and isn't. @@ -543,11 +682,13 @@ worst outcome: someone who believes they're protected and isn't. input injection (see [RFC 0025](0025-default-pluggable-computer-use.md)) need the user's desktop, which contained code can't reach. Does the node stay outside and connect in, and what does that do to the boundary's value? -- **Should the contained Gateway carry its own identity?** The provider accepts - an identity bundle at provision, which would narrow the credential blast - radius above and interacts with per-agent secret isolation in - [#42026](https://github.com/openclaw/openclaw/issues/42026). Out of scope for - phase 1; `workloadIdentity` is reserved so it can be answered later. +- **Should the contained Gateway carry its own identity?** Giving the unit its + own credentials rather than the Gateway holding provider keys would fix the + biggest residual risk above. The provider exposed a caller-supplied identity + bundle recently and no longer does, so there's nothing to build on today — but + [#55](https://github.com/openclaw/rfcs/pull/55) already solves this with + credential brokering, and that's the more promising direction regardless of + what the OS offers. - **How does autostart work?** A background Gateway has to start without an interactive logon, and the launcher is what starts it. - **How do we express a posture most hosts can't support** without fragmenting From f483539f8adf999dabdd23e7d94153082fbb85dc Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Tue, 18 Aug 2026 18:39:13 -0700 Subject: [PATCH 09/15] docs: close out remaining review findings for RFC 0032 Follow-up pass on the re-check: - Clarify that probe is the launcher's own step, not a provider phase, and drop the suggestion that the launcher could reattach later, since no such API exists. - Admit the orphan case a write-ahead record cannot fix: a crash during provisioning can leave an identity that no launcher can address, and reclaiming it is an out-of-band operator task. Added to the readiness bar. - Fix a stale Non-Goals line that still described #55 as containing only a session's work, and restate the boundary as deployment context. - Correct the AppContainer argument again: the ACEs it needs are removable by a matching unprepare step, so the real objection is the elevated, machine-wide host-prep required before a per-user agent can run, not permanent damage. - Stop calling the launcher "the trusted computing base" where the TCB table already shows it is only the part we own. - Pin the README citation, show exec and supervision in Figure 1, and add readiness items for sandbox-backend survival and downgrade prevention. - Decision requested now states what a yes actually accepts: a second always-on process, a larger TCB, and an identity-only boundary. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 100 ++++++++++++------ 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index 0688c489..d359bbed 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -70,8 +70,9 @@ Gateway then has no uncontained mode to reach — not because it declines one, b because it's never started outside the boundary at all. It needs no containment code and no containment config. -That also sets the bar for the launcher. It's the trusted computing base here, -so it stays small, loads no plugins, and never runs agent-directed work. +That also sets the bar for the launcher. It's the part of the trusted computing +base we actually own, so it stays small, loads no plugins, and never runs +agent-directed work. ### Today's Windows options are all bad trades @@ -160,9 +161,12 @@ stays up, that's the right shape. - **Containing what the Gateway can command.** Authorized desktop nodes and other action fulfillers sit outside this boundary and need their own containment story. Out of scope here. -- **Replacing per-session sandboxes.** `SandboxBackend` and - [#55](https://github.com/openclaw/rfcs/pull/55) contain a session's work. This - contains the Gateway. +- **Replacing per-session sandboxes.** `SandboxBackend` contains a session's + work; this contains the process running it. They nest. +- **Competing with [#55](https://github.com/openclaw/rfcs/pull/55).** Where an + operator-managed container platform is available, #55 is the better answer and + its credential brokering is stronger. This targets machines where that isn't + an option. - **Deciding how the Gateway is decomposed.** That's [#42026](https://github.com/openclaw/openclaw/issues/42026). - **Specifying the Windows API.** Owned by Windows; consumed, not defined, here. @@ -177,8 +181,9 @@ Three parts, and which side of the boundary each lands on is the whole point. **The launcher runs outside, as the user.** It's the entry point. It selects a provider, provisions the boundary, stages the payload, starts the unit inside, -and reports posture. It's the trusted computing base, so it stays minimal, loads -no plugins, and never executes agent-directed work. +and reports posture, and it stays alive supervising the Gateway. It's the part +of the trusted computing base we own, so it stays minimal, loads no plugins, and +never executes agent-directed work. On Windows this is #58's host app, not a new component. #58 defines it as "the packaged entry point" behind an `openclaw.exe` execution alias, whose job is @@ -239,8 +244,9 @@ The descriptor is versioned. The launcher refuses a version it doesn't understand rather than treating absent fields as "unsupported" — an unrecognised capability must never quietly become a claim about the boundary. -**Lifecycle.** This mirrors the provider's own phases rather than inventing new -ones: +**Lifecycle.** The middle phases mirror the provider's own rather than inventing +new ones; `probe` is the launcher's own step, not something the provider +defines: ``` probe -> provision -> start -> exec -> stop -> deprovision @@ -257,16 +263,16 @@ a fire-and-forget bootstrapper. It remains running as the Gateway's supervisor for as long as the Gateway runs, which means it owns crash detection, restart, and teardown. That is a real cost: it is a second always-on process, and it must stay in the trusted computing base for the whole session rather than just at -startup. If the provider later supports detached execution, the launcher can -shrink to a bootstrapper and reattach instead; the contract should not assume -either. +startup. Should the provider gain detached execution later, the launcher could +shrink to a bootstrapper — but no such capability exists today, so the contract +assumes supervision and treats anything better as a bonus. **Idempotency is per-operation, not universal.** The provider's phases behave differently on repeat, and a supervisor that assumes otherwise leaks accounts: | Operation | On repeat | What the launcher must do | |---|---|---| -| `provision` | **Not idempotent** — mints a fresh identity every call | Write a durable operation record *before* calling, so a crash between call and persist is recoverable rather than orphaning an account | +| `provision` | **Not idempotent** — mints a fresh identity every call | Write a durable record *before* calling, so a retry after a lost response doesn't blindly provision again | | `start` / `stop` | Provider-dependent failure on repeat | Track state; don't retry blindly | | `exec` | Runs the command again, no deduplication | Guard with the launcher's own supervision state | | `deprovision` | Returns "stale" once the identity is gone | Treat stale as success | @@ -275,6 +281,15 @@ Only `deprovision` is destructive, but only `deprovision` is safely repeatable, which is the opposite of the convenient case. `provision` returns an opaque identifier the launcher persists and uses to address the environment later. +One failure the launcher genuinely cannot fix on its own: if it dies *during* +provisioning, the identity may exist while the identifier needed to address it +does not. That environment is unreachable through the normal contract, and the +provider treats cleaning it up as an out-of-band operator task. So a write-ahead +record narrows the window but doesn't close it, and shipping this needs an +out-of-band reclamation path for environments no launcher can name. That belongs +in the readiness bar, and it is one of the costs of the provider being +preview-quality. + Because the environment survives until something explicitly deprovisions it, a crashed or replaced launcher leaves one behind. The launcher therefore needs to enumerate and reconcile environments it owns at startup. This is a required @@ -324,14 +339,15 @@ flowchart TB gw --- sb end - launcher -->|"provision, stage, start"| svc + launcher -->|"provision, stage"| svc svc -->|"OS-assigned account"| iso + launcher -->|"exec, then supervise"| gw client -->|"existing endpoints,
unchanged protocol"| gw user -.->|"no inherited grants"| iso ``` -**Figure 1.** The launcher establishes the boundary; the Gateway starts inside -it and never runs outside it. +**Figure 1.** The launcher establishes the boundary, `exec`s the Gateway inside +it, and stays alive supervising it. The Gateway never runs outside the boundary. This is an **identity** boundary and the descriptor has to say so. Per [MXC's backend @@ -428,14 +444,20 @@ Recommend contained execution for a deployment class only once: - Clients, channels, and nodes connect with no protocol change and no extra user step. - Ordinary CLI use doesn't pay provisioning cost per command. -- Orphaned environments are enumerated and reconciled at launcher startup. +- Orphaned environments are enumerated and reconciled at launcher startup, and + there's an out-of-band path for ones no launcher can address. - Startup, crash, restart, and teardown are covered, including ungraceful shutdown. +- The Docker and SSH sandbox backends either work under containment or their + loss is a documented, accepted tradeoff. +- Downgrade below the containment-capable launcher is prevented by packaging. - Posture is reported by the launcher, fallback is loud, and a managed install is distinguishable from one started outside it. - The platform primitive is generally available and its owner is willing to call - it a security boundary. The `microsoft/mxc` README currently says the opposite - about its preview backends, which alone blocks claiming this as a defense. + it a security boundary. The [`microsoft/mxc` + README](https://github.com/microsoft/mxc/blob/0aaa2afa6588d4aee34b35efb290308bb6f84fa1/README.md) + currently says the opposite about its preview backends, which alone blocks + claiming this as a defense. Until then it ships experimental and opt-in. @@ -590,13 +612,26 @@ than duplicating it. ## Decision requested **Is launcher-established, opt-in Gateway containment a direction we want to -support, given that the first provider offers identity isolation only and is -preview-gated?** +support, given that the first provider offers identity isolation only, is +preview-gated, and requires a permanently running supervisor process?** A yes authorizes phase 1: the contract, in the launcher, no provider, no default -change — and notably no change to OpenClaw core, since the Gateway is unchanged -and unaware. It doesn't commit us to the Windows provider, a default posture, or -a timeline. +change — and no change to OpenClaw core, since the Gateway gains no containment +code. It doesn't commit us to the Windows provider, a default posture, or a +timeline. + +Be clear about what a yes accepts in principle, though, because these are +structural rather than incidental: + +- **A second always-on process.** Because detached execution isn't available, + the launcher supervises the Gateway for its whole lifetime. Contained + deployments run two processes where they ran one. +- **A larger trusted computing base,** including the provider SDK, a + SYSTEM-hosted OS service, and package integrity. +- **A boundary that is identity-only.** It doesn't constrain the network, and it + doesn't contain what the Gateway is authorized to command. + +If those are unacceptable, it's better to say so now than after phase 1. ## Rationale @@ -645,14 +680,15 @@ a Gateway. AppContainer is a *restriction* model: it keeps your token and takes access away, so every resource the workload needs has to be enumerated up front. A Gateway that loads arbitrary plugins, spawns a Node runtime, and starts MCP servers has an open-ended and unpredictable resource set, which is exactly the -case an allowlist handles worst. It also cuts the other way: the tier available -on every current Windows release enforces filesystem policy by adding ACEs to -host paths, so broadening access means mutating DACLs on the user's real -directories — persistent host-side side effects, granting access to paths that -still belong to the user. Too restrictive to run the workload, too invasive when -you loosen it. An isolation session gives a separate principal with an ordinary -account instead, so nothing has to be enumerated and nothing on the host is -rewritten. +case an allowlist handles worst. There's a deployment cost too: the tier +available on every current Windows release enforces filesystem policy through +host path ACEs, and standing that up needs a separate **elevated** host-prep +step that adds ACEs for AppContainer SIDs to the system-drive root. Those ACEs +are removable again by a matching unprepare step, so this isn't permanent damage +— but requiring an administrator to modify machine-wide ACLs before a per-user +agent can run is a poor fit for a personal machine. An isolation session gives a +separate principal with an ordinary account instead, so nothing has to be +enumerated and nothing machine-wide is touched. **Why fail closed.** A control that silently degrades to no control produces the worst outcome: someone who believes they're protected and isn't. From 1619981c2ee3590450f1d364cbd5358c99ba62a4 Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Wed, 19 Aug 2026 10:21:29 -0700 Subject: [PATCH 10/15] docs: adopt Agent User terminology and a persistent account model Two clarifications that change the design rather than just the wording. Name it. The OS-minted account is the Agent User, matching the provider's own agentUserName/agentUserSid vocabulary. Used consistently now instead of "fresh account" or "throwaway account". More importantly, the account is not per-run. It is explicitly provisioned and explicitly deprovisioned, and it survives everything short of that -- including the launcher exiting and the launcher binary being replaced. Keeping it between runs is the intended shape, because that is what lets multi-session OpenClaw state, memory, and pairing persist across runs and reboots. That removes an entire invented problem. Earlier drafts concluded that nothing inside the boundary was durable and therefore state had to live in a host-side store staged in at startup, which then held the very credentials containment was supposed to protect. There is no such store. State lives in the Agent User profile, inside the boundary, where the user's own token cannot reach it, and the staging channel goes back to being setup-time payload delivery rather than a runtime data plane. New costs that come with a long-lived account, now stated: - Deprovision is the reset button and it is expensive: remediating a suspected compromise and destroying the agent's accumulated memory are the same operation, so export has to exist before deprovision can be recommended. - The sandbox identifier is what addresses the Agent User, and an identifier a newer build cannot decode strands the account along with all persisted state. Identifier compatibility across launcher upgrades is now a hard requirement. - The profile accrues months of credentials and history and is not covered by the user's own backup or credential protection. - Migration is one-time rather than per-run, and DPAPI-protected secrets must be re-established under the new principal rather than copied. Threat model updated accordingly: persistence is relocated rather than removed, and better scoped, but it is no longer described as dying at teardown by default. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 193 +++++++++++------- 1 file changed, 123 insertions(+), 70 deletions(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index d359bbed..63f807a9 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -23,11 +23,12 @@ This proposes containing the Gateway itself — and, importantly, doing it from boundary and starts the Gateway inside it. The Gateway needs no containment code of its own; it simply never runs anywhere else. -Windows Isolation Sessions are the first provider: the OS mints a fresh -throwaway account, runs the Gateway in a session bound to it, and tears both -down afterward. That provider is preview-quality today, so this asks for the -contract and the direction, plus a readiness bar to clear before anyone -recommends it. +Windows Isolation Sessions are the first provider: the OS mints a dedicated +**Agent User** account, runs the Gateway in a session bound to it, and can tear +both down on request. Crucially the account is not per-run — it's provisioned +once and kept, so agent memory and state persist across runs and reboots. That +provider is preview-quality today, so this asks for the contract and the +direction, plus a readiness bar to clear before anyone recommends it. ## Motivation @@ -87,19 +88,19 @@ that nobody runs them for a background process. ### The OS now offers something better shaped -Windows is exposing containment built around per-instance *identity* rather than -a whole guest OS. Per the public +Windows is exposing containment built around a distinct *identity* rather than a +whole guest OS. Per the public [`microsoft/mxc`](https://github.com/microsoft/mxc) project, its -`isolation_session` backend asks a Windows service to mint a fresh agent account -with an opaque OS-assigned name, start a session for it, run processes inside, -then stop the session and delete the account. +`isolation_session` backend asks a Windows service to mint an **Agent User** — +an account with an opaque OS-assigned name — start a session bound to it, and +run processes inside. Both the account and the session are explicitly +provisioned and explicitly removed; neither is torn down implicitly. [MXC states the requirements](https://github.com/microsoft/mxc/blob/0aaa2afa6588d4aee34b35efb290308bb6f84fa1/docs/isolation-session/oneshot.md) -plainly: "per-execution OS-isolated identity so the workload's actions cannot -pollute the calling user's NTFS / registry / token state", with an "OS-managed -session lifecycle that the OS-side service tears down cleanly when the calling -process exits." +plainly: "OS-isolated identity so the workload's actions cannot pollute the +calling user's NTFS / registry / token state", with an OS-managed session +lifecycle. The unit is a session and an account, not a guest OS — no guest memory footprint or boot time. Provisioning isn't free, which is exactly why MXC's state-aware @@ -210,10 +211,10 @@ path, registry hive, and credential store. Two consequences we should not gloss over. Migration has to move or re-establish that state (see [Rollout](#rollout)). And per-session sandbox backends assume things about their environment — the Docker backend needs to reach a daemon, the SSH backend needs -keys and known hosts — none of which is guaranteed for a throwaway account. -Whether those keep working under containment is an implementation question phase -2 has to answer, and if the answer is no for some backend, that is a real -capability loss rather than a detail. +keys and known hosts — none of which is guaranteed for the Agent User. Whether +those keep working under containment is an implementation question phase 2 has +to answer, and if the answer is no for some backend, that is a real capability +loss rather than a detail. ### The `ContainmentProvider` contract @@ -228,7 +229,7 @@ text, so the launcher can compare two providers and refuse a weaker one: | Capability | Values | |---|---| | `identityIsolation` | `none` (same principal), `shared` (one distinct principal), `perInstance` (fresh principal per environment) | -| `statePersistence` | `none`, `perInstance`, `perHost` | +| `statePersistence` | `none`, `acrossRuns` (survives restart, dies on teardown), `external` (provider keeps no state) | | `hostPathProjection` | `none`, `explicitPaths` | | `stagingChannel` | `none`, or a directory with a declared lifetime (`ephemeral`, `durable`) and visibility (`callerSeesAll`, `mutual`) | | `networkPosture` | `unrestricted`, `egressFiltered`, `isolated` | @@ -316,10 +317,14 @@ problem. ### The Windows provider -`provision` asks the OS service for a fresh account, `start` boots a session -bound to it, `stop` and `deprovision` tear both down. Each instance is a -distinct account with no shared registration, so concurrent units are -independent. +`provision` mints the Agent User, `start` boots a session bound to it, `exec` +runs the Gateway, `stop` ends the session, and `deprovision` removes the +account. Each Agent User is distinct with no shared registration, so concurrent +units are independent. + +In normal operation only `start`/`exec`/`stop` recur. `provision` happens once +at setup and `deprovision` only when the deployment is being decommissioned or +deliberately reset — which is what lets Gateway state persist across runs. ```mermaid flowchart TB @@ -332,7 +337,7 @@ flowchart TB svc["Isolation Session
service, SYSTEM
owns lifecycle"] subgraph iso["Isolation session"] - gw["OpenClaw Gateway
unchanged, unaware"] + gw["OpenClaw Gateway
state in Agent User
profile, persists"] plugins["Plugins, scheduler,
agent loop"] sb["Per-session sandboxes"] gw --- plugins @@ -340,7 +345,7 @@ flowchart TB end launcher -->|"provision, stage"| svc - svc -->|"OS-assigned account"| iso + svc -->|"mints Agent User"| iso launcher -->|"exec, then supervise"| gw client -->|"existing endpoints,
unchanged protocol"| gw user -.->|"no inherited grants"| iso @@ -364,10 +369,11 @@ documented as "a directory shared between the calling user and this isolated agent user, through which the caller can stage files into the session", with three properties that matter: -- **Asymmetric.** Each isolated user sees only its own; the caller sees every +- **Asymmetric.** Each Agent User sees only its own; the caller sees every concurrent sandbox's. The caller is the privileged side. -- **Ephemeral.** Created at provision, deleted at deprovision. A channel, not - storage. +- **Ephemeral.** Created at provision, deleted at deprovision — so it tracks the + Agent User's lifetime, not each run. Still a channel, not storage: the + Gateway's durable state belongs in the profile, not here. - **Not the working directory.** It doesn't change where the workload runs. Provisioning also takes an optional application identifier — documented as "the @@ -394,17 +400,43 @@ Availability: the backend is experimental, gated behind both an explicit flag and an OS feature flag, and needs a recent [Insider build](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimental/preview-build-26300-8553). -### What this costs - -**Durable state has to live outside.** Both the account and the staging -directory die at deprovision, so nothing kept inside is durable. That settles it -rather than leaving it open: durable state is host-side and staged in. The catch -is that this store then holds exactly the credentials containment was supposed -to make less valuable, so it needs protecting accordingly. +### State lives in the Agent User profile + +The Agent User is provisioned, not rented per run. Deprovisioning is an explicit +act, and the account survives everything short of it — including the launcher +exiting and the launcher binary being upgraded, since outliving the calling +process is the premise of the state-aware lifecycle. + +So the intended shape is: **provision once, keep it.** The Gateway's +configuration, credentials, pairing records, conversation history, and memory +live in that account's own profile and persist across runs and reboots. There is +no host-side shadow copy of Gateway state, and no need for one. + +That is a better outcome than it first appears. Durable state stays *inside* the +boundary rather than being parked on the host where the user's own token could +reach it, which means containment isn't just relocating the credentials it was +supposed to protect. The staging channel goes back to being what its name says — +a way to hand payload in at setup — rather than a data plane the Gateway depends +on at runtime. + +Two consequences to design for: + +- **Deprovision is the reset button, and it is expensive.** Wiping a possibly + compromised environment also destroys the agent's accumulated memory and its + pairing records. Recovery-after-compromise and losing your agent's history are + the same operation, so a supported export — or an explicitly accepted loss — + has to exist before deprovision can be recommended as a remediation. +- **Don't strand the account.** The provider's sandbox identifier is what + addresses the Agent User, and an identifier a newer build cannot decode is + refused, leaving a sandbox that "cannot be addressed through MXC afterwards". + For a long-lived launcher that will be upgraded many times over the life of + one Agent User, that is a live hazard: a stranded account takes all the + persisted OpenClaw state with it. Identifier compatibility across launcher + versions is a hard requirement, not a nice-to-have. **Don't run the Gateway from the staging directory.** It's ephemeral, caller-writable, and explicitly not the working directory. Stage through it, -then materialize inside. +then materialize into the profile. **Host reach drops to a staging channel.** No desktop, and no user working tree. "Fix a file in my repo" isn't expressible unless something explicitly stages @@ -436,8 +468,10 @@ Recommend contained execution for a deployment class only once: - The capability descriptor is accurate and the launcher refuses what a provider can't enforce. -- State persists across `provision`/`deprovision` and host restarts, and the - host-side store is protected like the credentials it holds. +- Gateway state survives restart and reboot in the Agent User profile, and + survives a launcher upgrade without stranding the account. +- There's a supported way to export state before `deprovision`, so remediation + and rollback don't mean losing the agent's memory. - Orphaned environments get reconciled instead of accumulating. - Anything the host reads back from the staging channel is treated as untrusted input, with the parsing boundary identified and tested. @@ -478,8 +512,8 @@ matters a great deal whether they survive *worse*, *the same*, or *newly*. The attacker no longer runs as you. Your profile, `HKCU`, browser and credential stores, SSH keys, and per-user startup entries are refused by the OS rather than by our own policy. It can't drive your desktop directly — no screen capture off -your session, no synthetic input into your other apps. Anything it writes into -its own profile disappears when the account is deprovisioned. +your session, no synthetic input into your other apps. What it writes lands in +the Agent User's profile, not yours. That is the whole claim. It is narrower than "the Gateway is sandboxed." @@ -501,11 +535,14 @@ overselling it. a network control. - **Outbound egress,** which is unrestricted. - **Resource exhaustion** of host CPU, disk, and memory. -- **Persistence — but scoped.** The account and its profile survive restarts - until something explicitly deprovisions, so an attacker can persist across - Gateway restarts. Uncontained, that persistence lands in *your* profile and - startup; contained, it is confined to a throwaway account that deprovisioning - destroys. Strictly better, and it makes deprovision cadence a real lever. +- **Persistence — relocated, not removed.** Because the Agent User is meant to + be kept between runs, an attacker who establishes persistence in that profile + keeps it across restarts and reboots, exactly as they would today. The + difference is *where* it lands: uncontained it goes in your profile, your + `HKCU`, and your startup; contained it is confined to the Agent User. Better + scoped and cleanly removable — but note the removal is `deprovision`, which + also destroys the agent's memory, so treat it as a real remediation cost + rather than a free reset. - **Anything a compromised Gateway can order someone else to do.** Most concretely an authorized desktop node (see [RFC 0025](0025-default-pluggable-computer-use.md)): the Gateway doesn't need @@ -521,18 +558,24 @@ This is the list that deserves scrutiny, because these are the costs rather than the leftovers. - **The Gateway's own credentials stay with it.** Provider keys, channel - credentials, and pairing records live inside by construction. Containment - limits reach into your assets; it does nothing to stop code running as the - Gateway from exfiltrating what the Gateway legitimately holds. Fixing that - needs credential brokering, not containment — see - [#55](https://github.com/openclaw/rfcs/pull/55), which does exactly that. -- **A host-side state store,** new to this design, holding those credentials - outside the boundary. Containment moves the crown jewels rather than - eliminating them, and this store now needs protecting like the Gateway did. + credentials, and pairing records live inside the Agent User profile by + construction. Containment limits reach into your assets; it does nothing to + stop code running as the Gateway from exfiltrating what the Gateway + legitimately holds. Fixing that needs credential brokering, not containment — + see [#55](https://github.com/openclaw/rfcs/pull/55), which does exactly that. + Keeping state inside the boundary at least avoids the worse alternative of + parking those credentials on the host where your own token could reach them. - **The staging channel, in both directions.** Contained code writes into a directory a host-side process running as you later reads, so anything the host parses from it is untrusted input. This is a genuinely new boundary-crossing - surface and the likeliest place for a bug. + surface and the likeliest place for a bug. Keeping it to setup-time staging, + rather than a runtime data plane, keeps that surface small. +- **A long-lived account holding accumulated secrets.** Persisting across runs + is the point, but it means the Agent User profile accrues credentials, + history, and memory over months. It becomes a target worth attacking in its + own right, and it is not protected by your credentials — so how it is backed + up, and whether anything can read it from outside the boundary, are real + questions rather than deployment details. - **The launcher,** which runs as you, supervises the Gateway for its whole lifetime, and can start, stop, and stage into the boundary. Compromising it defeats everything. @@ -547,7 +590,7 @@ Bigger than the launcher, and worth naming honestly: | The provider SDK and native bindings | Everything reaches the OS through them | | The SYSTEM-hosted OS session service | Owns account and session lifecycle | | Windows kernel and session separation | The enforcement itself | -| Host-side state store and staging parser | Cross-boundary data path | +| Staging channel parser | Cross-boundary data path | | Package integrity and signing | An attacker who replaces the launcher has won | Two consequences. "The provider declares its capabilities honestly" is a @@ -566,16 +609,24 @@ travel, not a control to rely on. **Nothing changes by default.** Containment is opt-in and off. Deployments that don't configure a provider behave exactly as they do now, everywhere. -**Turning it on is a migration.** The Gateway moves to a new principal, so state -under your profile isn't automatically visible to it. Adoption has to relocate -or re-establish config, credentials, and pairing records, and say clearly when -it can't — a migration that silently produces an empty Gateway looks identical -to a working one until the first channel fails to authenticate. This depends on -the unresolved persistence question. +**Turning it on is a one-time migration.** The Gateway moves to the Agent User, +so state under your profile isn't automatically visible to it. Adoption stages +config, credentials, and pairing records across into the Agent User profile +once, and says clearly when it can't — a migration that silently produces an +empty Gateway looks identical to a working one until the first channel fails to +authenticate. After that the Agent User profile is the Gateway's home and +nothing needs re-staging on subsequent runs. + +Credentials protected by the old account's DPAPI keys won't decrypt under the +new principal, so migration means re-establishing those secrets rather than +copying ciphertext. Anything that can't be re-established without user action +has to say so rather than failing at runtime. **Rollback is routine, not recovery.** Turning containment off returns to the previous behavior without data loss, which means adoption must never destroy -pre-migration state while moving it. +pre-migration state while moving it — and it means a supported way to get state +*out* of the Agent User profile before deprovisioning, since deprovision +destroys it irreversibly. **Downgrade is a real gap, not a requirement we can write our way out of.** A launcher version predating this contract ignores the containment config and @@ -597,17 +648,17 @@ and the readiness bar depends on it existing. is reachable from a client, and runs under an account that demonstrably isn't the signed-in user; plus a host without the capability failing closed with a reason. -3. **State and migration.** Resolve persistence, then adoption and rollback with - tested restart, upgrade, and downgrade. +3. **State and migration.** One-time migration into the Agent User profile, + export before deprovision, and tested restart, reboot, upgrade, and + downgrade. 4. **Bypass resistance.** Make the launcher the managed entry point and make an unmanaged Gateway distinguishable. Proof: an admin-observable signal that doesn't involve asking the Gateway. 5. **Readiness review.** Maintainer decision against the bar. Phases 1–2 belong to whoever owns the launcher — on Windows, the -[#58](https://github.com/openclaw/rfcs/pull/58) work. Phase 3 shouldn't start -while persistence is open, and phase 4 builds on #58's entry-point work rather -than duplicating it. +[#58](https://github.com/openclaw/rfcs/pull/58) work. Phase 4 builds on #58's +entry-point work rather than duplicating it. ## Decision requested @@ -705,9 +756,11 @@ worst outcome: someone who believes they're protected and isn't. an OS projection primitive, or accept that a contained Gateway works only on its own workspace. Each reopens the boundary differently, and the choice decides how useful this actually is. -- **What holds durable state, and how is it protected?** It has to be host-side. - What is it, how is it protected given it holds the Gateway's credentials, and - how does an existing install migrate into it? Blocks phase 3. +- **How is the Agent User profile protected and backed up?** State living inside + the boundary is the right answer, but it creates a long-lived account holding + months of credentials, history, and memory that isn't covered by the user's + own backup or credential protection. What backs it up, what can read it from + outside, and what happens to it when the machine is reimaged? - **Is an identity-only boundary worth adopting?** Nothing is enforced on the network and loopback survives. Do we require a network-capable provider before recommending this, or is reduced reach into user assets enough on its own? From a7d61c6d6bf9583d3a74a67dfe5c6d8541a097bc Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Wed, 19 Aug 2026 10:38:09 -0700 Subject: [PATCH 11/15] docs: right-size the Agent User persistence caveats Three corrections, all about not overstating. The provider does support per-run provisioning; this design just doesn't use it. Say that instead of claiming the account is never per-run. Backup and restore is not an open problem. The launcher can expose it through the staging directory, so exporting before a reset and reinstating afterwards is a straightforward implementation rather than a blocking unknown. Downgraded from a scary caveat to a phase 3 work item, and the residual question narrowed to protecting an export once it is outside the boundary. Dropped the sandbox-identifier stranding warning entirely. Keeping an identifier addressable across provider versions is MXC's problem, not something this RFC should impose on the launcher as a hard requirement. Also removes a duplicated orphan-reconciliation bullet in the readiness bar. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 99 ++++++++----------- 1 file changed, 42 insertions(+), 57 deletions(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index 63f807a9..cfc975f2 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -24,11 +24,11 @@ boundary and starts the Gateway inside it. The Gateway needs no containment code of its own; it simply never runs anywhere else. Windows Isolation Sessions are the first provider: the OS mints a dedicated -**Agent User** account, runs the Gateway in a session bound to it, and can tear -both down on request. Crucially the account is not per-run — it's provisioned -once and kept, so agent memory and state persist across runs and reboots. That -provider is preview-quality today, so this asks for the contract and the -direction, plus a readiness bar to clear before anyone recommends it. +**Agent User** account and runs the Gateway in a session bound to it. The +account is provisioned and deprovisioned explicitly, so we keep it between runs +and agent memory and state persist across reboots. That provider is +preview-quality today, so this asks for the contract and the direction, plus a +readiness bar to clear before anyone recommends it. ## Motivation @@ -402,37 +402,26 @@ build](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimen ### State lives in the Agent User profile -The Agent User is provisioned, not rented per run. Deprovisioning is an explicit -act, and the account survives everything short of it — including the launcher -exiting and the launcher binary being upgraded, since outliving the calling -process is the premise of the state-aware lifecycle. - -So the intended shape is: **provision once, keep it.** The Gateway's -configuration, credentials, pairing records, conversation history, and memory -live in that account's own profile and persist across runs and reboots. There is -no host-side shadow copy of Gateway state, and no need for one. - -That is a better outcome than it first appears. Durable state stays *inside* the -boundary rather than being parked on the host where the user's own token could -reach it, which means containment isn't just relocating the credentials it was -supposed to protect. The staging channel goes back to being what its name says — -a way to hand payload in at setup — rather than a data plane the Gateway depends -on at runtime. - -Two consequences to design for: - -- **Deprovision is the reset button, and it is expensive.** Wiping a possibly - compromised environment also destroys the agent's accumulated memory and its - pairing records. Recovery-after-compromise and losing your agent's history are - the same operation, so a supported export — or an explicitly accepted loss — - has to exist before deprovision can be recommended as a remediation. -- **Don't strand the account.** The provider's sandbox identifier is what - addresses the Agent User, and an identifier a newer build cannot decode is - refused, leaving a sandbox that "cannot be addressed through MXC afterwards". - For a long-lived launcher that will be upgraded many times over the life of - one Agent User, that is a live hazard: a stranded account takes all the - persisted OpenClaw state with it. Identifier compatibility across launcher - versions is a hard requirement, not a nice-to-have. +The provider supports provisioning an Agent User per run, but this design +deliberately doesn't. Deprovisioning is an explicit act, and the account +survives everything short of it, so the shape we want is **provision once, keep +it**. The Gateway's configuration, credentials, pairing records, conversation +history, and memory live in that account's profile and persist across runs and +reboots. There's no host-side shadow copy of Gateway state, and no need for one. + +That's a better outcome than it first appears. Durable state stays *inside* the +boundary rather than parked on the host where the user's own token could reach +it, so containment isn't just relocating the credentials it was supposed to +protect. The staging channel goes back to being what its name says — a way to +hand payload in at setup — rather than a data plane the Gateway depends on at +runtime. + +One consequence to design for: **deprovision destroys the agent's memory along +with the environment**, so wiping a possibly compromised Agent User and losing +its accumulated history are the same operation. The fix is straightforward — the +launcher exposes backup and restore through the staging directory, so state can +be exported before a reset and reinstated after — but it has to exist before +deprovision can be recommended as remediation. **Don't run the Gateway from the staging directory.** It's ephemeral, caller-writable, and explicitly not the working directory. Stage through it, @@ -468,11 +457,9 @@ Recommend contained execution for a deployment class only once: - The capability descriptor is accurate and the launcher refuses what a provider can't enforce. -- Gateway state survives restart and reboot in the Agent User profile, and - survives a launcher upgrade without stranding the account. -- There's a supported way to export state before `deprovision`, so remediation - and rollback don't mean losing the agent's memory. -- Orphaned environments get reconciled instead of accumulating. +- Gateway state survives restart and reboot in the Agent User profile. +- The launcher can back up and restore that state through the staging directory, + so remediation and rollback don't mean losing the agent's memory. - Anything the host reads back from the staging channel is treated as untrusted input, with the parsing boundary identified and tested. - Clients, channels, and nodes connect with no protocol change and no extra user @@ -571,11 +558,10 @@ the leftovers. surface and the likeliest place for a bug. Keeping it to setup-time staging, rather than a runtime data plane, keeps that surface small. - **A long-lived account holding accumulated secrets.** Persisting across runs - is the point, but it means the Agent User profile accrues credentials, - history, and memory over months. It becomes a target worth attacking in its - own right, and it is not protected by your credentials — so how it is backed - up, and whether anything can read it from outside the boundary, are real - questions rather than deployment details. + is the point, but the Agent User profile accrues credentials, history, and + memory over months, and it isn't covered by the user's own backup. The + launcher's backup and restore path covers that; the residual question is who + else can read the backup once it's outside the boundary. - **The launcher,** which runs as you, supervises the Gateway for its whole lifetime, and can start, stop, and stage into the boundary. Compromising it defeats everything. @@ -624,9 +610,9 @@ has to say so rather than failing at runtime. **Rollback is routine, not recovery.** Turning containment off returns to the previous behavior without data loss, which means adoption must never destroy -pre-migration state while moving it — and it means a supported way to get state -*out* of the Agent User profile before deprovisioning, since deprovision -destroys it irreversibly. +pre-migration state while moving it — and it's why the launcher's backup and +restore path matters: state has to come *out* of the Agent User profile before +deprovisioning, which destroys it irreversibly. **Downgrade is a real gap, not a requirement we can write our way out of.** A launcher version predating this contract ignores the containment config and @@ -648,9 +634,9 @@ and the readiness bar depends on it existing. is reachable from a client, and runs under an account that demonstrably isn't the signed-in user; plus a host without the capability failing closed with a reason. -3. **State and migration.** One-time migration into the Agent User profile, - export before deprovision, and tested restart, reboot, upgrade, and - downgrade. +3. **State, migration, and backup.** One-time migration into the Agent User + profile, backup and restore through the staging directory, and tested + restart, reboot, upgrade, and downgrade. 4. **Bypass resistance.** Make the launcher the managed entry point and make an unmanaged Gateway distinguishable. Proof: an admin-observable signal that doesn't involve asking the Gateway. @@ -756,11 +742,10 @@ worst outcome: someone who believes they're protected and isn't. an OS projection primitive, or accept that a contained Gateway works only on its own workspace. Each reopens the boundary differently, and the choice decides how useful this actually is. -- **How is the Agent User profile protected and backed up?** State living inside - the boundary is the right answer, but it creates a long-lived account holding - months of credentials, history, and memory that isn't covered by the user's - own backup or credential protection. What backs it up, what can read it from - outside, and what happens to it when the machine is reimaged? +- **How is exported state protected?** Backup and restore through the staging + directory is the mechanism, but an export leaves the boundary carrying the + Gateway's credentials and history. What protects it at rest, and what happens + to the Agent User profile when the machine is reimaged? - **Is an identity-only boundary worth adopting?** Nothing is enforced on the network and loopback survives. Do we require a network-capable provider before recommending this, or is reduced reach into user assets enough on its own? From 8373e7dfa2393e132a907abcc5d031297b547e4f Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Wed, 19 Aug 2026 10:42:51 -0700 Subject: [PATCH 12/15] docs: credit the tool sandbox and name the three uncontained routes The motivation implied the per-session sandbox does nothing, which is unfair and weakens the argument. It does real work: a sandboxed tool call reaching for SSH keys or a startup entry doesn't get there. The full profile blast radius is still reachable, but by three specific routes rather than by the sandbox being useless: a tool call that isn't sandboxed because the session selected no backend or the operation doesn't route through one, a container escape, and the Gateway process itself, which is never sandboxed. Tool sandboxing narrows the first and does nothing about the third, which is what this RFC addresses. Naming escape as a route also surfaces a benefit worth claiming: with the Gateway contained, escaping a per-session sandbox lands the attacker in the Agent User rather than on the host as the user, so the two seams compose instead of overlapping. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index cfc975f2..c14ea2b3 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -36,16 +36,26 @@ readiness bar to clear before anyone recommends it. We already have containment seams, and they're all narrower than the Gateway. `SandboxBackend` (`src/agents/sandbox/`, with Docker and SSH backends) is -per-session and scoped to a workspace. It contains the commands an agent runs. - -It doesn't contain the process that loads plugins, holds channel and provider -credentials, runs the scheduler, and picks the commands in the first place. - -That's backwards. A tool call gets a container; the thing that chose the tool -call, and holds the tokens authorizing it, runs as you. On a Windows -workstation, the blast radius of a prompt injection or a hostile skill is your -whole profile: documents, browser and credential stores, `HKCU`, startup -entries, SSH keys. +per-session and scoped to a workspace. It contains the commands an agent runs, +and it does real work: a sandboxed tool call that tries to read your SSH keys or +write a startup entry doesn't reach them. + +What it doesn't contain is the process that loads plugins, holds channel and +provider credentials, runs the scheduler, and picks the commands in the first +place. + +That's backwards, and it leaves the full blast radius reachable by three +distinct routes. A tool call that isn't sandboxed — because the session didn't +select a backend, or the operation doesn't route through one — runs with your +identity. A container escape puts the attacker back on the host as you. And the +Gateway process is never sandboxed at all, so anything the agent loop, a plugin, +or the scheduler does directly skips the boundary rather than escaping it. + +Down any of those routes, on a Windows workstation, the blast radius of a prompt +injection or a hostile skill is your whole profile: documents, browser and +credential stores, `HKCU`, startup entries, SSH keys. Tool sandboxing narrows +the first route and does nothing about the third, which is the one this RFC is +about. ACLs don't help here. They don't separate code running as you from data owned by you — that's exactly the grant they encode. Neither does a different working @@ -502,6 +512,12 @@ by our own policy. It can't drive your desktop directly — no screen capture of your session, no synthetic input into your other apps. What it writes lands in the Agent User's profile, not yours. +It also devalues a tool-sandbox escape. Today, breaking out of a per-session +sandbox lands the attacker on the host as you. With the Gateway contained, the +same escape lands them in the Agent User — still bad, but bounded by the same +boundary as the Gateway itself. The two seams genuinely compose rather than +overlapping. + That is the whole claim. It is narrower than "the Gateway is sandboxed." ### What is unchanged from today From 80b55005585721d06986aecf6ea3f5e6a1d9a664 Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Wed, 19 Aug 2026 11:21:47 -0700 Subject: [PATCH 13/15] docs: correct the launcher lifetime -- it is transient, not resident The RFC claimed the launcher must stay alive supervising the Gateway because detached execution is deferred upstream. Checking the existing implementation shows that is wrong, and the real design is better. MXC's deferral is about the SDK exec call handing back a process that outlives it. It does not prevent the workload from detaching on the inside: the script the host execs starts the gateway as a detached child so it outlives the setup script, then returns. The host verifies liveness in a bounded settle window and exits. The pieces that make it work: - The MXC session persists across host invocations and is only stopped explicitly, so the environment is still there next time. - Supervision lives inside the session -- a small in-session script owns the gateway process, records its PID and status, and captures its exit. - Persistence across logoff and reboot comes from a scheduled task that re-runs the host at logon, not from a resident process. - Health is observational: a later invocation reads the status file and probes the port rather than holding a handle. Consequences for the RFC: - No second always-on process. Removed from the Decision requested, which no longer asks maintainers to accept a permanent supervisor. - Smaller TCB. The launcher is short-lived, so the standing targets are its binary and its autostart registration rather than a resident daemon. The autostart registration is now named in the TCB. - Orphan reconciliation reframed: rehydrating and adopting the existing environment is the normal path that makes the transient model work, not crash-recovery machinery. - detachedExecution capability replaced with sessionLifetime, which is the property that actually matters -- whether the environment dies with the calling process or persists until stopped. - Figure 1 shows the logon task, the in-session supervisor, and the launcher exiting after exec. - The autostart unresolved question narrows to headless operation, since the logon case is answered. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 107 +++++++++++------- 1 file changed, 68 insertions(+), 39 deletions(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index c14ea2b3..524df2cb 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -192,9 +192,8 @@ Three parts, and which side of the boundary each lands on is the whole point. **The launcher runs outside, as the user.** It's the entry point. It selects a provider, provisions the boundary, stages the payload, starts the unit inside, -and reports posture, and it stays alive supervising the Gateway. It's the part -of the trusted computing base we own, so it stays minimal, loads no plugins, and -never executes agent-directed work. +reports posture, and exits. It's the part of the trusted computing base we own, +so it stays minimal, loads no plugins, and never executes agent-directed work. On Windows this is #58's host app, not a new component. #58 defines it as "the packaged entry point" behind an `openclaw.exe` execution alias, whose job is @@ -245,7 +244,7 @@ text, so the launcher can compare two providers and refuse a weaker one: | `networkPosture` | `unrestricted`, `egressFiltered`, `isolated` | | `hostUiReach` | `full`, `none` | | `lifecycleOwner` | `os` (teardown guaranteed), `caller` (we must drive it) | -| `detachedExecution` | `supported`, `deferred` — whether the launcher may exit after starting the unit | +| `sessionLifetime` | `perCall` (environment dies with the calling process), `persistent` (survives until explicitly stopped) | `hostPathProjection` and `stagingChannel` are deliberately separate. Being able to hand a file across is not the same as giving an agent the user's working @@ -267,19 +266,32 @@ probe -> provision -> start -> exec -> stop -> deprovision what actually launches the Gateway inside the environment; the earlier phases only build the place it runs. -**The launcher stays alive.** On the Windows provider, detached execution — a -process outliving the call that started it — is explicitly deferred, and a -spawned process stays tethered to the caller's lifetime. So the launcher is not -a fire-and-forget bootstrapper. It remains running as the Gateway's supervisor -for as long as the Gateway runs, which means it owns crash detection, restart, -and teardown. That is a real cost: it is a second always-on process, and it must -stay in the trusted computing base for the whole session rather than just at -startup. Should the provider gain detached execution later, the launcher could -shrink to a bootstrapper — but no such capability exists today, so the contract -assumes supervision and treats anything better as a bonus. +**The launcher is transient.** It runs, provisions or rehydrates the +environment, `exec`s, confirms the Gateway came up, and exits. It is not a +resident supervisor, and containment does not add a second always-on process. + +That works because of two things. The session outlives the launcher — it is +stopped explicitly, not when the calling process goes away — so the environment +is still there next time. And while the provider's own `exec` call is +synchronous, the workload can detach *inside* the boundary: the script the +launcher execs starts the Gateway as a detached child and returns, so the +Gateway outlives the call that created it. Nothing about the provider prevents a +long-lived daemon; the detachment just happens on the inside rather than being +handed out by the API. + +Supervision lives inside the boundary too — a small in-session launcher script +owns the Gateway process, records its PID and status, and captures its exit — +which keeps the host-side launcher out of the steady-state picture entirely. + +**So what starts it?** Not a resident process. On Windows the packaged flow +registers a scheduled task at logon that re-runs the launcher, which rehydrates +the existing session and starts the Gateway if it isn't already up. That's what +makes the Gateway survive logoff and reboot, and it keeps each launcher +invocation short-lived. Health and status are observational: a later invocation +reads the status file and probes the port rather than holding a live handle. **Idempotency is per-operation, not universal.** The provider's phases behave -differently on repeat, and a supervisor that assumes otherwise leaks accounts: +differently on repeat, and a launcher that assumes otherwise leaks accounts: | Operation | On repeat | What the launcher must do | |---|---|---| @@ -301,10 +313,12 @@ out-of-band reclamation path for environments no launcher can name. That belongs in the readiness bar, and it is one of the costs of the provider being preview-quality. -Because the environment survives until something explicitly deprovisions it, a -crashed or replaced launcher leaves one behind. The launcher therefore needs to -enumerate and reconcile environments it owns at startup. This is a required -operation, not an edge case. +Because the environment survives until something explicitly deprovisions it — +which is what we want — the launcher has to reconcile against what already +exists rather than assume a clean slate. Every invocation rehydrates persisted +state and adopts the running environment instead of provisioning a second one. +That's the same mechanism that makes the transient-launcher model work, so it +isn't extra machinery; it just has to be right. **Failure outcomes** are a closed set, so the launcher can act without parsing provider text: @@ -341,28 +355,33 @@ flowchart TB subgraph host["Windows host"] user["User profile
files, HKCU, tokens"] client["Clients, channels, nodes"] - launcher["Launcher (trusted)
packaged entry point
no plugins"] + task["Logon task"] + launcher["Launcher (trusted)
transient, no plugins"] end svc["Isolation Session
service, SYSTEM
owns lifecycle"] subgraph iso["Isolation session"] + sup["In-session supervisor"] gw["OpenClaw Gateway
state in Agent User
profile, persists"] plugins["Plugins, scheduler,
agent loop"] sb["Per-session sandboxes"] + sup --> gw gw --- plugins gw --- sb end + task -->|"at logon"| launcher launcher -->|"provision, stage"| svc svc -->|"mints Agent User"| iso - launcher -->|"exec, then supervise"| gw + launcher -->|"exec, then exit"| sup client -->|"existing endpoints,
unchanged protocol"| gw user -.->|"no inherited grants"| iso ``` -**Figure 1.** The launcher establishes the boundary, `exec`s the Gateway inside -it, and stays alive supervising it. The Gateway never runs outside the boundary. +**Figure 1.** A logon task runs the launcher, which establishes the boundary, +`exec`s a supervisor inside it, and exits. The session and the Gateway outlive +it; the Gateway never runs outside the boundary. This is an **identity** boundary and the descriptor has to say so. Per [MXC's backend @@ -475,10 +494,11 @@ Recommend contained execution for a deployment class only once: - Clients, channels, and nodes connect with no protocol change and no extra user step. - Ordinary CLI use doesn't pay provisioning cost per command. -- Orphaned environments are enumerated and reconciled at launcher startup, and - there's an out-of-band path for ones no launcher can address. -- Startup, crash, restart, and teardown are covered, including ungraceful - shutdown. +- Repeat invocations adopt the existing environment rather than provisioning a + second one, and there's an out-of-band path for environments no launcher can + address. +- The Gateway survives the launcher exiting, and comes back after logoff, + reboot, and its own crash. - The Docker and SSH sandbox backends either work under containment or their loss is a documented, accepted tradeoff. - Downgrade below the containment-capable launcher is prevented by packaging. @@ -578,9 +598,14 @@ the leftovers. memory over months, and it isn't covered by the user's own backup. The launcher's backup and restore path covers that; the residual question is who else can read the backup once it's outside the boundary. -- **The launcher,** which runs as you, supervises the Gateway for its whole - lifetime, and can start, stop, and stage into the boundary. Compromising it - defeats everything. +- **The launcher,** which runs as you and can provision, start, stop, and stage + into the boundary. It's short-lived, so it isn't a standing target the way a + resident daemon would be — but its binary and its registered entry point are, + since replacing either means owning every subsequent start. +- **The autostart registration.** Something has to start the Gateway at logon, + and whatever holds that registration can change what gets started. On Windows + that's a scheduled task, which is a well-understood thing to protect but is + now part of the design rather than an incidental detail. ### The trusted computing base @@ -588,12 +613,13 @@ Bigger than the launcher, and worth naming honestly: | Component | Why it's trusted | |---|---| -| Launcher and its provider implementation | Establishes and supervises the boundary | +| Launcher and its provider implementation | Establishes the boundary and stages into it | +| Its binary and autostart registration | Replacing either owns every subsequent start | | The provider SDK and native bindings | Everything reaches the OS through them | | The SYSTEM-hosted OS session service | Owns account and session lifecycle | | Windows kernel and session separation | The enforcement itself | | Staging channel parser | Cross-boundary data path | -| Package integrity and signing | An attacker who replaces the launcher has won | +| Package integrity and signing | Protects the above | Two consequences. "The provider declares its capabilities honestly" is a contract, not a security mechanism — providers must be trusted built-ins or @@ -665,8 +691,8 @@ entry-point work rather than duplicating it. ## Decision requested **Is launcher-established, opt-in Gateway containment a direction we want to -support, given that the first provider offers identity isolation only, is -preview-gated, and requires a permanently running supervisor process?** +support, given that the first provider offers identity isolation only and is +preview-gated?** A yes authorizes phase 1: the contract, in the launcher, no provider, no default change — and no change to OpenClaw core, since the Gateway gains no containment @@ -676,9 +702,10 @@ timeline. Be clear about what a yes accepts in principle, though, because these are structural rather than incidental: -- **A second always-on process.** Because detached execution isn't available, - the launcher supervises the Gateway for its whole lifetime. Contained - deployments run two processes where they ran one. +- **A launcher as the supported entry point.** Starting the Gateway goes through + it, including at logon, so the deployment owns an entry point it didn't have + before. The launcher is short-lived, so this isn't a resident process — but it + is a new required step in front of the Gateway. - **A larger trusted computing base,** including the provider SDK, a SYSTEM-hosted OS service, and package integrity. - **A boundary that is identity-only.** It doesn't constrain the network, and it @@ -779,8 +806,10 @@ worst outcome: someone who believes they're protected and isn't. [#55](https://github.com/openclaw/rfcs/pull/55) already solves this with credential brokering, and that's the more promising direction regardless of what the OS offers. -- **How does autostart work?** A background Gateway has to start without an - interactive logon, and the launcher is what starts it. +- **Can the Gateway start without a logon?** A logon task covers the normal + case, but a Gateway expected to be reachable on a machine nobody has signed + into needs something else, and registering that something generally needs + elevation. Whether headless operation is in scope is a product question. - **How do we express a posture most hosts can't support** without fragmenting the Windows experience? - **Should this contract live here or in From a414188e6fd6333bf6e80a670e5f743873c414c8 Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Wed, 19 Aug 2026 11:34:33 -0700 Subject: [PATCH 14/15] docs: align RFC 0032 with accepted RFC 0027 and prior review patterns Compared against the accepted RFCs on main and the review comments on prior RFC PRs. Three gaps worth closing. Accepted RFC 0027 already defines a SandboxDriver with close to the same invariants: verify the driver supports the complete policy and reject when enforcement is unsupported or ambiguous, establish and verify containment before the Harness starts, and treat an implementation that cannot verify enforcement as ineligible rather than substituting another. Not citing it was a real omission -- reviewers on prior PRs have asked for exactly this kind of alignment by number. It also strengthens the motivation: 0027's driver explicitly does not manage the Namespace's OpenClaw gateway, so even in the accepted enterprise design the Gateway is the component left outside the boundary. Named the configuration owner. The RFC referred to configuring a provider without ever saying where that configuration lives. It lives with the launcher, not in openclaw.json, because Gateway-side settings governing Gateway containment is the same inversion the Motivation rejects -- and it keeps the promise that core gains no containment surface. Added a Security properties section, following accepted RFC 0011, which states its invariants as a checkable list rather than leaving them implied by prose. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- ...y-containment-windows-isolation-session.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index 524df2cb..cdcecac8 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -144,6 +144,19 @@ stays up, that's the right shape. operator and no OpenShell. The two are complementary: #55 is the better answer wherever it's available, and its credential brokering is the direction this RFC should follow rather than compete with. +- [**RFC 0027 (OpenClaw Enterprise)**](0027-openclaw-enterprise.md), accepted, + already defines a `SandboxDriver` whose invariants are close to the ones + below: the platform verifies the selected driver "supports the complete + policy" and rejects deployment when enforcement is "unsupported, unavailable, + or ambiguous"; the driver "establishes and verifies containment before the + Harness starts"; and "an implementation that cannot verify enforcement is + ineligible for the operation", with no substitution of an alternate. That's + capability honesty, boundary-before-start, and fail-closed — so treat what + follows as applying an accepted pattern in a new place rather than inventing + one. Two differences matter: 0027 assumes a control plane a personal machine + doesn't have, and its driver explicitly "does not manage the Namespace's + OpenClaw gateway". Even in the accepted enterprise design, the Gateway is the + piece left outside the boundary — which is the gap this RFC closes. ## Goals @@ -339,6 +352,15 @@ than a silent runtime degradation. for a boundary that can't be delivered is a config error, not an availability problem. +**Where the configuration lives.** With the launcher, not in `openclaw.json`. +Provider selection, the requested capabilities, and the fallback policy are read +by the launcher before the Gateway exists, so putting them in Gateway config +would mean the contained process owned the settings governing its own +containment — the same inversion the Motivation rejects. It also keeps the +promise that OpenClaw core gains no containment surface: no new config keys, no +new schema, nothing for a deployment to set in the wrong place. The Gateway +never reads or writes these values, and can't. + ### The Windows provider `provision` mints the Agent User, `start` boots a session bound to it, `exec` @@ -512,6 +534,26 @@ Recommend contained execution for a deployment class only once: Until then it ships experimental and opt-in. +### Security properties + +The invariants a correct implementation has to hold, stated so they can be +checked rather than assumed: + +- The boundary exists before any Gateway code runs; there is no window in which + the Gateway executes uncontained and is contained afterwards. +- The Gateway holds no containment code, configuration, or decision. Nothing it + can do to itself removes the boundary. +- A provider that cannot enforce a requested capability refuses, rather than + accepting it and enforcing something weaker. +- No failure path silently produces uncontained execution. Fallback selects + another provider satisfying the same capabilities, or the start fails. +- The contained unit runs as a principal that never held the user's grants, + rather than as the user with restrictions applied. +- Containment posture is asserted by the launcher. A statement from the Gateway + about its own containment is not evidence. +- Data crossing the boundary is one-directional in trust: anything the host + reads back from the staging channel is untrusted input. + ## Threat model The threat is a Gateway induced to act against you: prompt injection reaching From ae9f4a2ed27468a4a6a86edfa183674ccf4d63c0 Mon Sep 17 00:00:00 2001 From: "Paul Campbell (AgOS)" Date: Wed, 19 Aug 2026 11:41:18 -0700 Subject: [PATCH 15/15] docs: point rfc_pr back at the upstream RFC PR The RFC is returning to openclaw/rfcs#61 after iterating in the fork. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b --- rfcs/0032-gateway-containment-windows-isolation-session.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0032-gateway-containment-windows-isolation-session.md b/rfcs/0032-gateway-containment-windows-isolation-session.md index cdcecac8..a111ce0c 100644 --- a/rfcs/0032-gateway-containment-windows-isolation-session.md +++ b/rfcs/0032-gateway-containment-windows-isolation-session.md @@ -6,7 +6,7 @@ created: 2026-08-18 last_updated: 2026-08-18 status: draft issue: -rfc_pr: https://github.com/paulcam206/rfcs/pull/1 +rfc_pr: https://github.com/openclaw/rfcs/pull/61 --- # Proposal: Gateway Containment and Windows Isolation Sessions