Skip to content

feat(gateway): let an agent in a sandbox create child sandboxes within a user-set grant (gateway MCP + API) #4574

Open

Description

@patkraw

User Story

I use OpenShell to run agent harnesses (Pi and OpenWorker) where a lead agent delegates work to sub-agents, and I want each agent in its own sandbox with only the access its task needs.
I directly need an agent inside a sandbox to create child sandboxes, for sub-agents and for tool sandboxes that run commands, without holding a user credential and without being able to exceed limits I set.
This affects whether multi-agent harnesses can run on OpenShell with least privilege, instead of on host services outside it.

Problem Statement

A sandbox cannot create another sandbox. CreateSandbox is user-only, and sandbox principals are refused at the router. OpenShell also has no notion of a sandbox tree: no record of which sandbox created which, no limit on how many sandboxes one delegated task may create, no cascade stop, and no check that a child gets no more than its parent. The containment check that would prove "child within parent" exists in openshell-prover but nothing in the gateway calls it.

This is the concrete workflow behind the delegation model in #2025 (child_policy <= parent boundary <= human/admin maximum).

Impact / Why This Matters

Without this, harnesses that want sandboxed sub-agents run a service on the host that holds the user's credential and creates sandboxes when an agent asks. Each harness builds its own, the checks live outside OpenShell, and there are two sources of truth for who may create what.

Limits such as maximum depth or concurrency are enforced only inside the harness, which runs in the sandbox next to the agent; a prompt-injected agent can call the creation path directly and ignore them. A crashed lead leaves its workers running. Operators cannot answer "which sandbox created this one, for whom, and was it allowed".

The same need appears outside sub-agents: RL environment runners (for example OpenEnv or NeMo Gym) start environments from inside an environment, and coding agents on a developer's machine want to ask the gateway for a sandbox directly.

Proposed Design

The user sets one grant when starting the lead, or attaches it later. The grant is the only approval; inside it, no prompts.

openshell sandbox create --name lead --from pi-agent:local --policy lead.yaml \
  --grant-max-depth 1 --grant-max-breadth 2 --grant-max-total 3 \
  --grant-expires 4h --grant-program /usr/local/bin/node \
  -- pi
  • Every child's policy must provably fit inside its parent's current policy (the lead's policy for the lead's children), so each level can only narrow: child <= parent <= global maximum.
  • max_depth, max_breadth and max_total bound the whole tree; children and grandchildren share the grant. Tool sandboxes ignore depth and count toward breadth and total.
  • A sandbox without a grant cannot create children. Only the user can attach, widen, shrink or revoke a grant (openshell grant create|get|list|update|revoke; creating a new grant replaces a revoked or expired one), and changes apply from the next call.

An agent in the sandbox creates children without a credential. It calls the gateway's MCP endpoint at http://openshell.local/mcp, served by its own supervisor in every sandbox, which attaches the sandbox's identity and the calling program; the gateway checks the program against program in the grant. Agents on the host call the same endpoint, or the CLI and SDKs, with the user's login.

{"method": "tools/call", "params": {"name": "create_sandbox",
  "arguments": {"name": "worker", "command": ["pi", "-p", "Review PR #12"],
                "policy": {"network_policies": {"...": "..."}}, "request_id": "r1"}}}

Observable behavior:

  • The child runs the parent's template (image, CPU, memory; no GPU). The agent may narrow every section the prover checks (network, filesystem, process, Landlock); the whole policy is proved against the parent's. A wider policy is refused with the prover's counterexample and a hint; a policy the prover cannot check is refused. Rules the prover cannot model in the parent (such as MCP rules) are dropped from the boundary before the proof, so they never block other children.
  • A parent that needs to give more than it has asks for it for itself through the existing policy.local proposal flow, then retries. Later writes to a child's policy are proved against its parent's policy too.
  • Tools are always listed; a sandbox with no grant, depth or budget left is refused at call time (LIMIT_REACHED, NOT_ALLOWED); when the user raises the limit, its next call succeeds with no policy change.
  • kind: tool children run commands for their parent (exec, through a sandbox-only ExecChildSandbox RPC) and stop when idle; agent children run their own task, and the parent reads their files with get_file while they run.
  • Stopping or deleting a parent stops its children first. A repeated request_id returns the first result.
  • The same operations exist as gRPC RPCs for the CLI and SDKs; MCP is one surface over the same API. In v1 the in-sandbox endpoint carries MCP only; gRPC through it comes later.

Full design: RFC draft at https://github.com/patkraw/OpenShell/blob/docs/gateway-mcp-rfc/patkraw/rfc/NNNN-gateway-mcp-sandbox-creation/README.md

Acceptance Criteria

  • A sandbox started with a grant can create a child through MCP from inside the sandbox, with no credential inside it, and a user on the host can do the same through MCP, the CLI and the gRPC API; in-sandbox gRPC is not part of v1.
  • A sandbox started without a grant cannot create children; a grant attached later lets it.
  • A child policy wider than its parent's policy is refused, at every depth, with a counterexample; one the prover cannot check is refused; a later write that would widen a child past its parent is refused.
  • Only the grant's program can call the endpoint for a tree; other programs in the sandbox are refused.
  • Depth, breadth and total limits hold across the whole tree, including under concurrent creates and retries.
  • Only the user can change a grant; an agent cannot, through any surface.
  • Deleting a parent stops its descendants first.
  • Every create, refusal and grant change is attributed to the caller, the parent and the user in the audit log.

Alternatives Considered

  • Host creation service (status quo). Works, but each harness builds its own, it holds a user credential, and its checks sit outside OpenShell.
  • Agent holds a gateway token. A prompt-injected agent could exfiltrate it and create sandboxes from anywhere.
  • MCP server in each supervisor (like policy.local). Agents on the host have no supervisor, and every supervisor would carry and upgrade an MCP server; the decisions still belong in the gateway.
  • Supervisor middleware or a gateway interceptor. These shape requests on existing paths; they do not give a sandbox principal a way to create sandboxes, nor hold tree state (counters, lineage) across requests.
  • Global policy as the limit. It replaces every sandbox's policy, so no child could have less access than another. The design instead proposes a global policy that acts as a maximum.

Related: #2025 (spike: agents setting maximum policies for spawned agents), #3808 (revalidate running subagents when ancestor authority changes; this proposal checks children at create time and on later writes to their own policy, and leaves revalidating running children after an ancestor shrinks to that issue).

Non-goals: a results channel that keeps a child's result after it exits; a single interactive prompt for requests outside a grant; per-tool rules for other MCP servers (children get those servers whole, as REST rules). All are future work in the RFC.

Agent Investigation

A proof of concept is on https://github.com/patkraw/OpenShell/tree/gateway-mcp-poc (examples/gateway-mcp-poc/README.md):

  • crates/openshell-gateway-mcp: MCP protocol only, behind a SandboxApi trait, tested with a fake backend.
  • crates/openshell-server/src/children.rs: grants, lineage, prover check (check_within_boundary), limits, retries, tool sandboxes, cascade stop; gRPC handlers and MCP share these rules.
  • crates/openshell-supervisor-network/src/gateway_mcp_door.rs: the supervisor endpoint at openshell.local, open in every sandbox, reusing the policy.local mechanism; it forwards the caller's kernel-resolved program path.
  • CLI --grant-* flags, openshell grant create|get|list|update|revoke, openshell sandbox children; Python SDK methods; tutorials for the CLI, SDK and MCP in examples/gateway-mcp-poc/tutorials/.
  • run-checks.sh runs the end-to-end checks against a dev gateway with the Docker driver, including the proposal flow, grant replacement and concurrent retries: 74/74 pass at commit a1b94e583 (see examples/gateway-mcp-poc/README.md, Results). The official Python and TypeScript MCP SDKs work against the endpoint unchanged.
  • Gaps: grants and lineage are in memory; the supervisor forwards its session token rather than the dedicated urn:openshell:gateway-mcp token; no per-sandbox rate limit; global policy maximum mode is not built.

Checklist

  • I've reviewed existing issues and the published docs
  • This is a design proposal, not a "please build this" request

Activity

  1. added and removed
    state:triage-neededOpened without agent diagnostics and needs triage
    on Oct 19, 2026
  2. patkraw commented on Oct 19, 2026

    @patkraw
    Author

    The RFC draft and a working POC are ready:

    Could a maintainer triage this and, if an RFC is wanted, accept it and assign an RFC number? 0021 looks free. Line comments are welcome on my fork in the meantime: patkraw#1

  3. drew commented on Oct 19, 2026

    @drew
    Collaborator

    Accepted. Please grab RFC-0021.

  4. added
    state:acceptedA maintainer decided OpenShell should pursue this issue
    on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-rfcstate:acceptedA maintainer decided OpenShell should pursue this issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions