Sandbox Subsystem — Conceptual Deep Dive
What the sandbox is protecting against
Agentweaver lets an AI agent inspect files, edit a workspace, search source, and optionally run shell commands. Those capabilities are useful only if the agent can act like an engineer, but they also create an escape problem: a model can be mistaken, prompt-injected, or asked to run commands whose side effects are broader than the current project.
The sandbox is therefore built around one rule: agent actions must be useful inside the assigned workspace and boring everywhere else. A well-behaved agent should barely notice the sandbox. A malicious or confused agent should be unable to:
- read or write files outside the run workspace;
- escape through
.., absolute paths, Windows drive tricks, UNC/device paths, symlinks, or junctions; - run host-level shell commands when no real process isolation exists;
- reach arbitrary internal services from a production sandbox pod;
- leave long-lived compute, network listeners, or preview tunnels after the run ends;
- exfiltrate obvious secrets through large command output.
This is not one mechanism. It is a layered isolation model:
- Governance decides whether a tool call is allowed. Unknown tools and suspicious paths fail closed before tool code runs.
- Filesystem tools validate again at the point of use. The implementation assumes governance can be bypassed or fed malformed arguments.
- Shell commands run through an executor abstraction. The runtime can choose a local isolation backend for development or a Kubernetes-backed sandbox for production.
- Production sandboxes are isolated pods. They use hardened pod settings, a shared workspace mount, bounded lifetime claims, and network policy egress controls.
- Output is bounded and redacted. The sandbox assumes command output itself can become an exfiltration channel.
The result is defense in depth rather than a single perfect wall.
Where this lives: packages/Agentweaver.AgentRuntime, packages/Agentweaver.AgentTools, packages/Agentweaver.SandboxFs, packages/Agentweaver.SandboxExec, apps/Agentweaver.Api/Sandbox, k8s
The core mental model
Think of every agent action as passing through three concentric boundaries:
Each boundary has a different job:
- Governance boundary: answers “is this kind of action allowed for this run?” It is intentionally deny-by-default. A tool name must be recognized, and path-bearing arguments must resolve inside the sandbox root.
- Tool boundary: answers “is this exact file operation safe right now?” It re-validates paths, rejects reparse-point escapes, and returns controlled failures to the agent loop.
- Execution boundary: answers “where does this process actually run?” It hides the host behind a container, namespace, VM, or — only in explicit non-production cases — direct passthrough.
- Network boundary: answers “what can this isolated process talk to?” In production, this is handled by Kubernetes/Cilium policy, not by trusting shell command text.
The important design choice is that boundaries are redundant. Governance is not trusted as the only check, path validation is not trusted as process isolation, and process isolation is not trusted as network isolation.
Why command execution needs stronger isolation than file tools
File tools are narrow: read this path, write this file, search this tree. Shell commands are broad: a single command can spawn processes, run interpreters, traverse the filesystem, open sockets, install packages, fork children, or encode data into output.
For that reason, run_command is treated as a privileged capability:
- The shell tool is only registered when shell execution is enabled and the selected executor is acceptable for the current mode.
- Destructive command patterns, or policies that require approval for all shell commands, trigger a human-in-the-loop approval gate before execution.
- The command validator rejects malformed shell requests such as missing/invalid working directories, null bytes, or excessive command length.
run_commandis for finite commands. Its default execution budget is 30 minutes and can be overridden withAGENTWEAVER_RUN_COMMAND_DEFAULT_TIMEOUT_SECONDSor a tool-calltimeout_ms. If the budget expires, Agentweaver cancels the sandbox process and returns a visibletimed_out: truefailure that tells the model to usestart_preview_processfor long-lived preview/dev servers.- The command is packaged with the run workspace, timeout, filesystem policy, network flag, and optional run ID.
- The selected executor runs it and returns only bounded, redacted stdout/stderr plus an exit code.
This design does not try to parse every shell command into safe and unsafe subcommands. That would be brittle. Instead, the system validates the shell envelope, requires approval for dangerous patterns, and relies on the executor boundary to contain whatever the shell actually does. The executor does use a narrow, non-security-critical package-manager hint to decide whether to prepare an optional writable system root; a miss only makes /usr, /etc, and /var read-only for that command.
Where this lives: packages/Agentweaver.AgentTools/Tools/RunCommandTool.cs, packages/Agentweaver.SandboxExec
Filesystem containment: make the workspace the only universe
The filesystem sandbox exists because path strings are adversarial input. An agent may ask for ../../secrets, an absolute host path, a Windows device path, a UNC share, or a benign-looking path whose parent is a symlink to somewhere else. The containment rule is simple: all file effects must resolve to the sandbox root or one of its children.
A rebuild should implement containment in two phases.
Phase 1: lexical rejection before touching the filesystem
Before opening anything, reject inputs that are obviously outside the contract:
- empty paths;
- absolute paths when the tool expects a relative workspace path;
..path segments;- Windows device paths such as
\\?\or\\.\; - UNC paths such as
\\server\share; - drive-relative paths such as
C:foo; - normalized paths whose prefix is not the normalized sandbox root.
This catches cheap escape attempts without giving the filesystem a chance to resolve links or special names.
Phase 2: real-path verification at the point of use
Lexical checks are necessary but insufficient. A path can look safe and still escape through a symlink or junction. Agentweaver therefore treats symlink/junction ancestors as untrusted and verifies the final opened handle where possible.
The invariant is:
The path must be inside the workspace both before opening and after the OS resolves the object that was opened.
That second check matters because it narrows time-of-check/time-of-use races. If an attacker swaps a path between validation and open, the final handle resolution can still detect that the opened object is outside the sandbox.
Why tools return structured failures
Sandbox violations are not treated as fatal runtime crashes. File tools return clear, structured failures to the agent. This keeps the run alive while making the boundary visible: the agent can choose a safe path and continue, but it cannot pressure the runtime into ignoring the violation.
Search is constrained enumeration
Search tools do not accept arbitrary host roots. They enumerate the sandbox root, avoid reparse points, skip high-noise/generated directories such as .git, node_modules, bin, obj, and .vs, and cap results. This is partly security and partly agent ergonomics: bounded search prevents accidental huge responses and reduces the chance of leaking irrelevant data.
Where this lives: packages/Agentweaver.SandboxFs
Governance: fail closed before side effects
The governance layer is the first policy checkpoint for model-selected tools. Its conceptual contract is:
- every run has a credential-free effective permission binding with a schema version, binding ID, policy digest, source, run ID, lifecycle attempt, and scope;
- child bindings are intersected with their parent bindings, and refreshed bindings are intersected with the launch binding, so delegation, recovery, or a stale warm pod cannot widen authority;
- default action is deny;
- unknown tool names are denied;
- known file tools must provide a recognized path argument;
- search tools are allowed only because they are implemented as sandbox-root enumeration;
- shell tools must provide a working directory inside the sandbox root;
- internal governance exceptions deny the call rather than allowing it.
The same binding is used by in-process execution and AgentHost. The API sends it in the one-time AgentHost /configure request, and the worker sends the current narrowed binding again with every A2A turn. A policy change can therefore revoke or narrow later work in a warm pod. Automatic approval is evaluated only after the binding allows the operation, so it can remove a prompt but cannot turn a denial into an allow.
The optional sandbox.allowed_operations list in .agentweaver/settings.yml narrows the legacy sandbox profile. Omit it (or leave it empty) to preserve existing default and restricted behavior. Supported operation values and enforcement gates are:
| Operation | Tool family | Enforcement gate |
|---|---|---|
observe | report_intent, report_outcome | runtime permission handler |
workspace.read | file and directory reads | binding, then path containment |
workspace.search | workspace search/glob | binding, then bounded root enumeration |
workspace.write | create, edit, replace, patch | binding, then path containment and tool validation |
shell.execute | run_command | binding, isolation check, command policy, executor |
network.access | web_fetch | binding before approval or auto-approval |
agentweaver.read | read-only Agentweaver API tools | binding before authenticated API call |
agentweaver.write | mutating Agentweaver API tools | binding before authenticated API call |
preview.manage | preview process/session tools | binding, then preview runner gates |
human.interaction | ask_question | binding, then the run-scoped question gate |
An unclassified operation is denied. A malformed policy or a missing, unsupported, or run-mismatched binding also denies execution explicitly. Denials use existing tool.error and run.degraded events; the degraded event includes the binding ID, version, and source without credentials. GET /api/runs/{id}/effective-permissions returns an authorization-filtered inspection projection for authorized operators or the matching run capability. The projection distinguishes the current configured policy, the effective narrowed policy, restrictions imposed by the durable launch ceiling or a parent, permissions revoked since launch, fixed operation-family enforcement coverage, and the latest permission denial. Denial evidence is normalized to a reason code, operation, safe tool name, binding provenance, sequence, and timestamp; tool arguments, commands, URLs, arbitrary event payloads, and credentials are not returned. Inspection is read-only: viewing an unstarted or legacy run does not create its durable launch ceiling. The first binding for each run lifecycle is also recorded as permission.binding.bound; current policy is always intersected with that durable launch ceiling. This keeps restored executions and newly delegated children from recovering authority that their parent did not have at launch.
The operator assistant's MCP tools use the same binding classifier. The permission check wraps the approval gate, so even an approved or normally ungated MCP mutation cannot bypass a read-only assignment. Operators can pass run_id to sandbox_policy_get to receive the same effective-permission projection as REST. In the web app, the Permissions action on an orchestration run opens the same projection, including configured/effective differences, revocation state, coverage, and safe denial provenance.
Agentweaver also performs a direct sandbox-backend evaluation in addition to the governance kernel evaluation. That redundancy is deliberate: even if one policy integration changes behavior, the dedicated containment backend still has to approve the call.
For shell specifically, governance adds a capability check: if the selected executor does not represent acceptable isolation for shell mode, shell execution is denied. The model should not be able to obtain a host shell merely because a tool name exists.
Where this lives: packages/Agentweaver.AgentRuntime/SandboxGovernance.cs, packages/Agentweaver.SandboxFs/SandboxPolicyBackend.cs
Executor abstraction: one command contract, many isolation backends
The executor abstraction separates “what the agent wants to run” from “where and how it runs.” The runtime passes a command object containing:
- command line;
- working directory;
- environment variables;
- filesystem policy;
- timeout;
- network-enabled flag;
- optional Agentweaver run ID.
Every executor returns the same shape: exit code, stdout, stderr, timeout flag, and output-truncated flag. This uniform contract lets the agent runtime stay stable while deployments choose different isolation implementations.
Backend selection logic
Executor selection is environment-aware:
Selected Kubernetes initialization fails closed. The API router first chooses Kubernetes versus local using the explicit backend and cluster detection; Sandbox:Backend=local still chooses the local factory inside a cluster.
Local backends and their trade-offs
Local execution exists for development, tests, and non-cluster deployments. It is intentionally best-effort and transparent about gaps:
| Backend family | Conceptual role | Trade-off |
|---|---|---|
Windows process container (mxc) | Use OS/container support to isolate a process from the host. | Network allowlisting is not equivalent to Kubernetes policy, so warnings are surfaced. |
WSL2: wsl-bwrap / wsl-unshare | Bubblewrap reports real isolation; unshare does not. | Controlled shell is not registered for wsl-unshare; the two backends are not equivalent. |
| Native Linux bubblewrap | Bind the workspace as /workspace, mount only selected runtime paths read-only, create tmpfs homes/temp, and unshare PID/user/network namespaces unless network is enabled. | Useful local isolation, but still not the same operational boundary as a production Kata pod plus cluster policy. |
Native Linux lxc-exec | Fallback Linux isolation when bubblewrap is unavailable. | Depends on host LXC availability and configuration. |
| Direct passthrough | Last-resort host shell. | Not isolation. Use only when the surrounding environment is already disposable or explicitly trusted. |
Where this lives: packages/Agentweaver.SandboxExec, apps/Agentweaver.Api/Sandbox/SandboxExecutorRouter.cs
Kubernetes sandbox lifecycle: retained utility-command contract
The retained Kubernetes utility-command API uses claims and pod exec. It is not the deployed AgentHost turn loop: model-controlled commands there use pod-private PodExec and the executor sidecar, not a new claim for every shell invocation. The retained contract is:
- A SandboxTemplate defines the pod shape and hardening policy.
- A SandboxWarmPool keeps ready sandboxes available from that template.
- A SandboxClaim asks the sandbox controller for one sandbox instance for a bounded TTL.
- The executor waits until the claim is bound to a concrete pod, then uses Kubernetes pod exec to run the command.
The agent-sandbox controller (and where MXC fits)
The "Sandbox controller" above is the upstream kubernetes-sigs/agent-sandbox controller — not MXC. The two are different runtimes for different tiers, and the names are easy to conflate:
- MXC (
Sabbour.Mxc.Sdk/wxc-exec.exe) is the local-host command isolation runtime behind the Windowsprocesscontainer, WSL, and Linuxlxc-execexecutors. It runs on a developer or non-cluster host and has no Kubernetes presence. - The agent-sandbox controller is the in-cluster runtime that turns a
SandboxClaiminto a bound, Kata-isolated pod.KubernetesSandboxExecutortalks only to this controller's CRDs; no MXC binary exists in the cluster.
Agentweaver installs the controller and its three CRDs (API group extensions.agents.x-k8s.io) in scripts/azure/steps/10-create-cluster.mjs (install default SANDBOX_CONTROLLER_VERSION=v0.5.3 — production clusters run agent-sandbox v0.5.3, #487). The installed controller serves both v1beta1 (the storage version) and the deprecated-but-served v1alpha1; KubernetesSandboxExecutor targets v1beta1 (SandboxClaimConventions.cs:23):
SandboxTemplate(k8s/base/sandbox-template-agenthost.yaml,agentweaver-agent-host) defines the live AgentHost pod shape:kata-vm-isolationruntime class, non-root UID/GID 1000, dropped capabilities,/workspacePVC, A2A listener port8088, workload identity, and theagentweaver-execexecutor sidecar — a second container from the same image that owns every model-controlled process in its own PID namespace (see sandbox pod execution).SandboxWarmPoolkeeps AgentHost pods pre-built from that template so claims bind without a cold pod start. The live pool isagentweaver-agent-host(k8s/base/sandbox-warmpool-agenthost.yaml,replicas: 2). AgentHost warm pods boot withoutRunId, enter standby, and are configured after binding byPOST /configure, so the .NET process and Copilot SDK are pre-warmed without per-run env. Configuration carries the effective permission binding, and every later A2A turn must carry a current valid binding or the pod rejects the turn before execution.SandboxClaim(created per run byKubernetesSandboxExecutor; shape ink8s/reference/sandbox-claim-template.yaml) carriesspec.warmPoolRef.name(agentweaver-agent-hoston the live path) andspec.lifecycle.{ttlSecondsAfterFinished, shutdownPolicy: Delete}. The AgentHost claim omitsspec.env; static values belong to the template/config map. Per-run identity, workspace, credentials, turn authentication, purpose, and approval values arrive later via/configure. The controller adopts a warm pod, then signals readiness with aReadycondition (status.conditions[type=Ready].status == "True") and writes the bound pod name intostatus.sandbox.name. There is nostatus.phasefield.- Model-controlled
run_commandcalls do not create a Kubernetes claim or pod exec session. After the AgentHost pod is configured, the tool uses the pod-privateagentweaver-execsidecar over authenticated IPC. The sidecar only starts the optional per-run writable system root for package-manager commands (apt,apt-get,dpkg, etc.); ordinary commands run directly in the read-only system-root bubblewrap view so they do not pay package-manager setup latency on the happy path.
The executor's provisioning loop is the concrete contract with the controller:
CreateClaimAsync/CreateAgentHostClaimAsyncPOSTs the v1beta1SandboxClaimcustom object into the namespace (KubernetesSandboxExecutor.cs:354,:294).WaitForBoundAsyncpolls the claim every 2 s until itsReadycondition isTrue, then returnsstatus.sandbox.name(SandboxClaimConventions.cs:53).- For AgentHost, resolve pod IP, wait for the HTTP 200 standby listener, configure once and complete setup, then register the effective workspace and A2A endpoint.
- On claim deletion (ad-hoc command) or TTL expiry, the controller garbage-collects the pod and its service — Agentweaver never deletes pods directly.
Why claims have TTLs
A shell command can hang, or a client can disconnect. The claim TTL gives the controller an independent cleanup clock. Agentweaver also clamps the command timeout below the claim TTL so the controller should not delete the sandbox while the executor is still expecting a result.
Why run IDs map to pod names
Run-scoped utility commands derive a stable claim name and record the pod. The local kubectl fallback may use that mapping. Production preview resolves claims and HTTPRoute annotations from cluster state, not a replica-local pod registry; see sandbox browser preview.
Why ad-hoc and run-scoped cleanup differ
Ad-hoc commands have no reason to keep a sandbox alive after the command returns, so the claim is deleted immediately. Run-scoped commands may keep the claim until cleanup or TTL so preview remains available. That improves developer experience but consumes warm-pool/quota capacity for longer, so operators must size quotas and warm pools accordingly.
Where this lives: apps/Agentweaver.Api/Sandbox/KubernetesSandboxExecutor.cs, apps/Agentweaver.Api/Sandbox/SandboxClaimConventions.cs, apps/Agentweaver.Api/Sandbox/PortForwardService.cs, apps/Agentweaver.Api/Endpoints/SandboxEndpoints.cs, apps/Agentweaver.Api/Runs/RunWatchLoopService.cs, k8s/base/sandbox-template-agenthost.yaml, k8s/base/sandbox-warmpool-agenthost.yaml, k8s/reference/sandbox-claim-template.yaml
AgentHost warm-pool configure contract
AgentHost uses two standby warm pods. Shipped claims omit spec.env; static configuration belongs to the template/config map. Setup waits for one-time /configure. See the claim/configure sequence.
Per-run values are delivered by POST /configure after the claim binds:
/configure field | Purpose |
|---|---|
runId | The Agentweaver run this pod executes; missing values return 400. |
userId | The submitting user; drives RuntimeUserScopeProvider. |
turnBearerToken | Production supplies a 256-bit per-run bearer token; turn middleware enforces equality when the configured token is nonempty. |
copilotCredential / byokProviderConfiguration | Alternative run-scoped model-provider payloads. |
workingDirectory | Shared coordinate; local modes resolve a verified pod-local effective execution directory. |
effectivePermissionBinding | Credential-free run/attempt binding and policy ceiling. Missing or invalid values return 400. |
TryConfigure is one-shot; repeat configuration returns 409. Setup resolves a valid Shared workspace, a verified local checkout, or a pod-private fallback when Shared has no supplied path. The effective directory is not universally Run.WorktreePath. /healthz is already 200 in standby. Production supplies a turn token, enforced when nonempty. /configure delivers that token and instead relies on configured transport controls and network policy; the additive preview range includes 8088, so policy alone is not API/worker-exclusive.
The executor does not create per-run SecretProviderClass objects, cloned SandboxTemplates, or per-run warm pools for AgentHost. It sends the required run-scoped provider and repository capability data through /configure. The sandbox identity has no Key Vault access and cannot retrieve ambient user credentials.
AgentHost claims carry the active run-lease fencing token. A recovery or dispatch takeover that acquires a newer token must replace the older claim before sending a turn, so a stale pod cannot continue editing or publish a competing write-back under the same run ID.
Where this lives:
| Source | Role |
|---|---|
apps/Agentweaver.Api/Sandbox/IRunSubmittingUserResolver.cs | Resolves the submitting user and the run's WorktreePath, stripping coordinator sub-run suffixes so child stages inherit the parent's shared worktree. |
apps/Agentweaver.Api/Sandbox/KubernetesSandboxExecutor.cs | Resolves workingDirectory without failing launch on lookup errors and includes it in the one-time /configure body. |
apps/Agentweaver.AgentHost/Program.cs | Defines the /configure request body, including workingDirectory, and passes it to AgentHost startup. |
apps/Agentweaver.AgentHost/AgentHostRuntimeState.cs | Stores the one-time run/user/token configuration. |
apps/Agentweaver.AgentHost/AgentHostStartupService.cs | Runs SetupAsync after /configure, using the per-run working directory when present and preserving env-var launch behavior otherwise. |
packages/Agentweaver.AgentRuntime/CopilotAIAgent.cs | Receives the configured working directory as the agent setup/file-tool root. |
Production pod isolation and hardening
The AgentHost sandbox pod contains the runtime needed for live agent turns, but it is not privileged. Because it executes untrusted shell/tool code, it runs as a dedicated managed identity with no Key Vault role assignments. It receives only run-scoped capability data through /configure.
The production template applies several important constraints:
- Kata runtime: the pod uses
kata-vm-isolation, adding a VM boundary around the container workload. - Non-root identity: the container runs as UID/GID 1000.
- No privilege escalation and no Linux capabilities: the process should not be able to acquire broader kernel privileges.
- Separate platform and child views: platform containers have writable roots and workspace/scratch/HOME mounts. The model-controlled child has a narrower allowlist without the PVC root, siblings or IPC secrets.
- Never restart: failed or completed sandbox pods are not automatically restarted as hidden long-lived state.
The image carries the AgentHost .NET/Copilot runtime. The security posture comes from pod/runtime policy, workload identity scoping, and network policy, not from making the image empty.
Important boundary: the shared workspace PVC is an execution workspace, not a secrecy boundary between every workload that can mount it. The sandbox limits process and network blast radius; it does not make shared storage private from other principals with access to the same volume.
Where this lives: apps/Agentweaver.AgentHost/Dockerfile, k8s/base/sandbox-template-agenthost.yaml
Network isolation and egress allowlisting
Network access is an exfiltration and lateral-movement channel. A sandboxed command that can reach arbitrary addresses can probe cluster services, call metadata endpoints, or send data to the internet. Production network policy therefore follows an allowlist model.
Current policies select app: agentweaver-agent-host: default deny plus API/worker control ingress, same-namespace preview-Gateway ingress, DNS, API/MCP egress and public-IP HTTPS with explicit CIDR exclusions. They are not a GitHub-only or FQDN-only allowlist. AgentHost has a service-account token; the executor and its child view exclude that identity. See actual rules and manifest gaps.
The service-CIDR warning is important. If sandbox egress accidentally includes the cluster service CIDR, a sandbox pod may be able to reach internal Kubernetes services even if internet egress looks restricted. Agentweaver checks configured service CIDR exclusions and logs a warning when the cluster service CIDR is not excluded.
Local backends cannot all enforce the same network model. Where network allowlisting is unavailable or weaker, executors report warnings, and runners emit sandbox warning events. Treat those warnings as a deployment property, not as an agent-visible suggestion.
Where this lives: k8s/base/networkpolicy-sandbox.yaml, k8s/base/cilium-network-policy-sandbox.yaml, apps/Agentweaver.Api/Sandbox/SandboxExecutorRouter.cs, packages/Agentweaver.SandboxExec
Rebuild blueprint
If rebuilding this subsystem from scratch, implement it in this order:
- Define the workspace invariant. Pick one sandbox root per run. Every file tool, search tool, and shell working directory must resolve inside it.
- Create a path validator. Reject obvious path escapes lexically, reject symlink/junction ancestors, and verify opened handles against the sandbox root.
- Wrap all file/search tools. Do not expose raw host file APIs to the model. Return structured errors for violations.
- Add deny-by-default governance. Allow only known tools and known argument shapes. Fail closed on unknown tools and internal policy errors.
- Define a command executor interface. Keep command input/output stable so the runtime is independent of the isolation backend.
- Gate shell execution. Require shell enablement, working-directory containment, destructive-command approval, timeout bounds, output caps, and redaction.
- Provide local executors. Prefer real local isolation where available; mark direct passthrough as non-production and warn loudly.
- Provide a production Kubernetes executor. Use claims, templates, warm pools, TTLs, pod exec, and fail-closed backend selection.
- Harden platform and child separately. Preserve Kata, non-root execution, dropped capabilities, distinct container PID namespaces and the per-child mount allowlist. Current platform roots are writable; executor children must not inherit AgentHost identity or IPC secrets.
- Add network policy. Default-deny with explicit control/preview ingress and the actual egress exceptions; assess the union rather than asserting domain-only access.
- Plan cleanup. Delete ad-hoc claims, retain run-scoped claims only while previews need them, and enforce TTL/quota as independent backstops.
- Surface warnings. If a backend cannot enforce a promised boundary, emit an explicit warning rather than silently weakening isolation.
Security invariants and gotchas
- Default deny is an invariant. A new tool should do nothing until governance and tool-level validation know how to constrain it.
- Path containment is checked more than once. This is intentional defense in depth, not duplication to remove.
- Shell parsing is not the security boundary. The executor and OS/container boundary must contain arbitrary shell behavior.
- Kubernetes fallback must fail closed. In production, silently downgrading to local or direct execution is worse than failing the run.
- Direct passthrough is not a sandbox. It is useful only when the host environment is already disposable/trusted.
- Run-scoped sandboxes consume capacity while retained. Preview support trades resource usage for debuggability.
- Shared
/workspaceis shared storage. Do not treat the PVC as a per-tenant secrecy boundary unless the surrounding storage model enforces that. - Output can leak data. Keep caps and redaction even when process isolation is strong.
- Directory listing has a narrow residual race. The code documents a filename-only TOCTOU residual risk for listing; file reads/writes use stronger open-and-verify handling.
- Output caps apply to command and tool results. The command/tool output cap in this subsystem is 4 MiB; keep that limit in place so a single large result cannot exhaust memory or flood the event stream. Image or attachment upload paths belong to other components and are out of scope for this repo-owned sandbox subsystem.
Visual model
Sandbox boundary
Structured source · Editable draw.io
See also
- Sandbox pod execution - pod-local scratch workspaces, Git write-back, nested-repository flattening, and the HOME/XDG cache contract.
- Sandbox browser preview - exposing a server running inside a run's sandbox pod to the user over a public HTTPS reverse proxy (per-preview HTTPRoute -> per-run ClusterIP Service -> pod).
Diagram details and constraints
| Element | Contract |
|---|---|
| title | Several checks contain each action |
| takeaway | Native shell is denied; governed tools combine AGT policy, direct containment and execution isolation. |
| group-title0 | TOOL SELECTION / POLICY |
| group-title1 | POINT-OF-USE CONTAINMENT |
| Model tool request | Model tool request |
| Model tool request | Permission dispatch |
| Model tool request | Native shell: always denied |
| Model tool request | URL approvals handled apart |
| Model tool request | Custom reporting bypass |
| Governance | Governance |
| Governance | Deny-by-default policy |
| Governance | AGT policy must allow |
| Governance | Direct backend must allow |
| Governance | Both checks, not either |
| Registered tools | Registered tools |
| Registered tools | Explicit capability surface |
| Registered tools | Files revalidate at use |
| Registered tools | run_command gates shell |
| Registered tools | Unknown tools denied |
| Workspace boundary | Workspace boundary |
| Workspace boundary | Sandbox filesystem |
| Workspace boundary | Lexical + real-path checks |
| Workspace boundary | Reject symlink escapes |
| Workspace boundary | Bounded / redacted output |
| Execution boundary | Execution boundary |
| Execution boundary | Selected isolation backend |
| Execution boundary | Shell policy + approval |
| Execution boundary | Kata pod in AKS |
| Execution boundary | Direct mode is opt-in |
| Credential handling | Credential handling |
| Credential handling | Current implementation |
| Credential handling | Host + tool options hold token |
| Credential handling | Direct git status / allowed gh |
| Credential handling | No blanket shell injection |
| relation-0 | 1 governed calls |
| relation-1 | 2 both allow |
| relation-2 | 3 file operation |
| relation-3 | 4 run_command |
| relation-4 | 5 eligible git / gh |
| assurance | Current code delivers repository credentials into Host/tool options; the normative no-credential contract is NOT met. |
| assurance-0-label | Dispatch exceptions |
| assurance-0-fact | Native shell denied; URL path separate. |
| assurance-0-source | CopilotAIAgent.cs |
| assurance-1-label | Execution isolation |
| assurance-1-fact | Sidecar: separate PID namespace. |
| assurance-1-source | sandbox-template-agenthost.yaml |
| assurance-2-label | Credential reality |
| assurance-2-fact | No blanket shell credential inheritance. |
| assurance-2-source | RunCommandTool.cs |
| n0 | Native shell: always denied; URL approvals handled apart |
| n1 | AGT policy must allow; Direct backend must allow |
| n2 | Files revalidate at use; run_command gates shell |
| n3 | Lexical + real-path checks; Reject symlink escapes |
| n4 | Shell policy + approval; Kata pod in AKS |
| n5 | Host + tool options hold token; Direct git status / allowed gh |
| groups | TOOL SELECTION / POLICY; POINT-OF-USE CONTAINMENT |
Diagram details and constraints
| Element | Contract |
|---|---|
| title | Choose Kubernetes before local probing |
| takeaway | The API router owns cluster selection; the local factory can fall back to direct. |
| API executor router | API executor router |
| API executor router | Backend override / cluster detect |
| API executor router | Explicit local bypasses cluster |
| Kubernetes selected | Kubernetes selected |
| Kubernetes selected | Initialize claim executor |
| Kubernetes selected | Failure throws; no local fallback |
| Claim backend | Claim backend |
| Claim backend | Bound pod command contract |
| Claim backend | Not the in-pod PodExec client |
| Local factory | Local factory |
| Local factory | Only when router selects local |
| Local factory | Probe host-supported backends |
| Windows ladder | Windows ladder |
| Windows ladder | processcontainer -> WSL |
| Windows ladder | wsl-bwrap real; unshare not real |
| Linux ladder | Linux ladder |
| Linux ladder | bubblewrap -> LXC |
| Linux ladder | Host tools must be available |
| No usable isolation | No usable isolation |
| No usable isolation | Automatic local fallback |
| No usable isolation | Also explicit direct option |
| Direct passthrough | Direct passthrough |
| Direct passthrough | IsRealIsolation = false |
| Direct passthrough | Trusted/disposable host only |
| Command registration | Command registration |
| Command registration | ShellEnabled AND real or direct |
| Command registration | wsl-unshare: no controlled shell |
| arrow-1 | select |
| arrow-2 | ready |
| arrow-3 | local |
| arrow-4 | Windows |
| arrow-5 | Linux |
| arrow-6 | unavailable |
| arrow-8 | warn |
| arrow-9 | gate |
| note-0 | The local platform ladders are alternatives, not a Windows-to-Linux chain. |
| note-1 | Kubernetes failure never silently descends into the local ladder. |
| note-2 | Runtime emits sandbox.selected; factory choice is not an isolation guarantee. |
| notes | The local platform ladders are alternatives, not a Windows-to-Linux chain.; Kubernetes failure never silently descends into the local ladder.; Runtime emits sandbox.selected; factory choice is not an isolation guarantee. |
Diagram details and constraints
| Element | Contract |
|---|---|
| title | Retained utility-command claim lifecycle |
| takeaway | This API path is not the AgentHost model-controlled command loop. |
| Utility command | Utility command |
| Utility command | Workspace and bounded timeout |
| Utility command | Optional Agentweaver run ID |
| Kubernetes executor | Kubernetes executor |
| Kubernetes executor | Create or reuse claim |
| Kubernetes executor | Stable name for run-scoped work |
| Sandbox controller | Sandbox controller |
| Sandbox controller | Bind warm-pool pod |
| Sandbox controller | Ready condition + sandbox name |
| Command result | Command result |
| Command result | stdout / stderr / exit status |
| Command result | Timeout and output flags |
| Kubernetes pod exec | Kubernetes pod exec |
| Kubernetes pod exec | Execute retained API command |
| Kubernetes pod exec | No generic sleep pool implied |
| Bound pod | Bound pod |
| Bound pod | Current template is AgentHost |
| Bound pod | Not a separate generic deployment |
| New ad-hoc claim | New ad-hoc claim |
| New ad-hoc claim | Delete in command finally |
| New ad-hoc claim | No run-scoped retention needed |
| Run-scoped claim | Run-scoped claim |
| Run-scoped claim | Keep for cleanup / TTL |
| Run-scoped claim | Not deleted after every command |
| Production preview lookup | Production preview lookup |
| Production preview lookup | Uses cluster claim/route state |
| Production preview lookup | Not replica-local registry |
| arrow-1 | submit |
| arrow-2 | claim |
| arrow-3 | bind |
| arrow-4 | exec |
| arrow-5 | return |
| note-0 | Cleanup cards distinguish newly created ad-hoc and run-scoped claims. |
| note-1 | AgentHost controlled tools instead use authenticated pod-private PodExec. |
| note-2 | SQL run ownership leases are separate from this Kubernetes claim lifecycle. |
| notes | Cleanup cards distinguish newly created ad-hoc and run-scoped claims.; AgentHost controlled tools instead use authenticated pod-private PodExec.; SQL run ownership leases are separate from this Kubernetes claim lifecycle. |
Diagram details and constraints
| Element | Contract |
|---|---|
| title | Bind, reach standby, configure once |
| takeaway | Liveness precedes configuration; production delivers run credentials out of pod specs. |
| Prepare launch | Prepare launch |
| Prepare launch | Resolve provider and run context |
| Prepare launch | Mint fresh turn token |
| Claim warm pod | Claim warm pod |
| Claim warm pod | Create/adopt; omit spec.env |
| Claim warm pod | Wait Ready + bound pod name |
| Resolve and register | Resolve and register |
| Resolve and register | Pod mapping / token registry |
| Resolve and register | Resolve actual pod IP |
| GET /healthz | GET /healthz |
| GET /healthz | HTTP 200 standby |
| GET /healthz | Listener liveness, not turn ready |
| POST /configure | POST /configure |
| POST /configure | Identity / workspace / approvals |
| POST /configure | Copilot capability OR BYOK |
| AgentHost setup | AgentHost setup |
| AgentHost setup | One-time configuration |
| AgentHost setup | Effective workspace + HOME |
| Configuration guards | Configuration guards |
| Configuration guards | Second configure: 409 |
| Configuration guards | Other routes: 503 before ready |
| Register effective endpoint | Register effective endpoint |
| Register effective endpoint | Return effective working directory |
| Register effective endpoint | Shared/local/private fallback |
| First A2A turn | First A2A turn |
| First A2A turn | Production sends turn bearer |
| First A2A turn | Equality guard when nonempty |
| arrow-1 | launch |
| arrow-2 | bound |
| arrow-3 | poll |
| arrow-4 | reachable |
| arrow-5 | setup |
| arrow-6 | ready |
| arrow-7 | invoke |
| note-0 | Top, middle and bottom rows are successive launch stages. |
| note-1 | Repository / preview / broker credentials have separate purposes. |
| note-2 | Optional schema fields do not imply unconditional endpoint enforcement. |
| notes | Top, middle and bottom rows are successive launch stages.; Repository / preview / broker credentials have separate purposes.; Optional schema fields do not imply unconditional endpoint enforcement. |

