Skip to content

docs: propose gateway containment via Windows Isolation Sessions - #1

Closed
paulcam206 wants to merge 15 commits into
mainfrom
paulcam206-isolation-session-rfc
Closed

docs: propose gateway containment via Windows Isolation Sessions#1
paulcam206 wants to merge 15 commits into
mainfrom
paulcam206-isolation-session-rfc

Conversation

@paulcam206

@paulcam206 paulcam206 commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Iterating on this RFC here in the fork before proposing it upstream. The
upstream draft (openclaw#61) is closed.

RFC file: rfcs/0032-gateway-containment-windows-isolation-session.md
Lifecycle: status: draft, issue intentionally blank until acceptance.

What Problem This Solves

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 a prompt-injection, a hostile
skill, or a compromised plugin acts with the operator's identity. The existing
SandboxBackend seam is per-session and workspace-scoped: it contains the
commands an agent runs, not the process that holds the credentials and chooses
the commands.

Why This Change Was Made

Adds RFC 0032, an argument-first proposal for a platform-agnostic
GatewayContainmentProvider seam — versioned capability descriptor, a
probe -> provision -> start -> attach -> stop -> deprovision lifecycle, a
closed failure taxonomy, and fail-closed selection — with Windows Isolation
Sessions as the first provider.

It is deliberately capability-honest. The Windows primitive is preview-quality,
so the RFC defines a readiness bar rather than asking for adoption, and states
the provider's limits plainly: no host-folder-sharing primitive, an unrestricted
localhost-reachable network, and no reach to the user's desktop. The boundary on
offer is an identity boundary, and the RFC says so.

Decision Requested

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 only — 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.

Related Work

Proposal Relationship
openclaw#58 (MSIX packaging) Packaging gives the install a reviewable identity; this reduces what it can reach while running. openclaw#58 lists runtime isolation as a non-goal, so this fills that slot.
openclaw/openclaw#42026 (control plane / per-agent runtime split) Same decision space, different axis. #42026 partitions which component holds which secrets; this changes what principal a component runs as. Reconciled in Motivation and Rationale; the seam is written against a "unit of containment" so it survives that split.
openclaw#55 (OpenShell worker provider) Contains a session's worker; this contains the Gateway. They compose.

Current Code and Contracts Inspected

  • src/agents/sandbox/ (SandboxBackend, Docker and SSH backends) to establish
    what OpenClaw already contains and at what scope.
  • The live RFC template contract and lifecycle, refreshed from openclaw/rfcs
    at af708f0ddb6f.
  • Public microsoft/mxc README and
    docs/isolation-session/ for every Windows behavioral claim.

Evidence

Design-only change, so the proof is conformance and sourcing rather than runtime
behavior:

  • All seven required headings present; frontmatter keys match the live template.
  • Every claim about Windows behavior traces to a public source, including the
    constraints that weaken the case
    , which are quoted rather than omitted.
  • Mermaid validated for GitHub strict mode: no <br/> segment over the clip
    budget, subgraph titles narrower than contents, and mmdc parses the block
    under securityLevel: strict / htmlLabels: false.
  • LF line endings and 80-column prose, matching the other RFCs;
    git diff --check clean.

Review History

Reviewed against the openclaw-rfc readiness gates; the first draft passed 3 of
10. Addressed since:

  • Reconciliation with #42026 (was entirely uncited).
  • Threat model and residual risk — the first draft argued for a security
    boundary without analysing what an attacker retains. Loopback reachability was
    stated as a fact but the conclusion was never drawn.
  • Compatibility, migration, rollback, and downgrade.
  • Staged implementation plan.
  • Ownership and core-versus-plugin justification, replacing an unresolved
    question.
  • Descriptor versioning and failure taxonomy; policy_rejected never falls back.
  • ClawSweeper's P2 on upstream docs: propose gateway containment via Windows Isolation Sessions openclaw/rfcs#61: the file was committed CRLF. Fixed.

Known Gaps

  • RFC number needs confirmation. 0032 was the lowest unclaimed integer, but
    the repository has four open PRs sharing 0009 and four sharing 0010, so
    allocation is not sequential in practice. Recorded as an unresolved question.
  • No maintainer-discussion thread yet — required by the RFC lifecycle before
    acceptance.
  • No feature-intake issue in openclaw/openclaw, which the RFC routing
    guidance expects to precede an architecture RFC.

paulcam206 and others added 15 commits August 18, 2026 12:13
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
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b
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
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
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
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#58 rather than a new component: openclaw#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. openclaw#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
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 openclaw#58, #42026, openclaw#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
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 openclaw#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.
- openclaw#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
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 openclaw#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
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
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
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
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
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
The RFC is returning to openclaw#61 after iterating in the fork.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ccc616c5-8e0a-4098-b8ef-c19695e5c27b
@paulcam206

Copy link
Copy Markdown
Owner Author

Superseded by the upstream RFC PR: openclaw#61 — same branch, so the work carried over. Closing to keep one live PR.

@paulcam206 paulcam206 closed this Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant