Sandbox provisioning in QM creates isolated execution environments for agent turns by bundling actor credentials, tool access, and configuration into three tracked sandbox handles (box, scratchBox, ownerAuthBox), provisioning them on-demand with deduplication and lazy initialization. Device-flow credentials are restored selectively into each sandbox's keychain based on origin and isolation rules, with failed restores logged but non-fatal, and broker-vended tools are shimmed into owner-auth commands only when detected as word-boundary tokens.
src/core/orchestrator/sandboxes.ts defines the TurnSandboxContext interface — the complete set of dependencies and configuration passed into createTurnSandboxes for a single agent turn, including the actor principal, session, resolution, scope IDs, credential environment, broker-vended tools, skill resolutions, and performance tracking.[1] Three separate sandbox handles are tracked per turn: box (the primary turn sandbox), scratchBox (a scratch sandbox), and ownerAuthBox (an isolated owner-auth sandbox), each with independent provision and pending-handle state.[1]
Sandboxes are provisioned lazily: provision(eager = false) records box.used = true only on non-eager calls and de-duplicates concurrent provisions via provisionInFlight ??= doProvision(...). A failed provision nulls the in-flight reference so it can be retried.[1] After provisioning, doProvision sets the sandbox environment variable AGENT_OUTBOX to ${handle.rootDir}/${turnOutboxDir}, making it available to all tool executions in that sandbox.[1] Sandbox status events are streamed to deps.runActivity (if present) as records with type: "sandbox_status", using a monotonically increasing sequence number starting at 2_000_000 to avoid colliding with other activity record ranges.[1]
During provisioning, device-flow (keychain) credentials are restored to the sandbox. For automation-origin turns with useOwnerKeychain: true and no isolation, the actor's own ID is used as the restore owner; otherwise a derived deviceFlowCredOwner(memoryScopeId, actorId) key is used.[1] Services listed in quarantinedServices or marked for resident reset are removed from the sandbox keychain before credentials are restored, and the removal also clears canonical filesystem roots associated with each tool.[1] For every device-flow service successfully restored, a credential usage record is emitted with status "legacy_retained" when cutoverModeOf(service) === "prefer_ephemeral", and "legacy_restored" otherwise.[1] Device-flow restore errors are swallowed and recorded via deps.errors (category "keychain", code "device_flow_restore_failed") rather than failing the turn, so a keychain restore failure is non-fatal.[1]
The ownerAuthCommand wrapper injects owner-auth environment variables and creates shell function shims for any broker-vended tool binaries detected in the command string. Audit log entries are emitted for both keychain materialize and credential materialize events.[1] Broker-vended tool binaries are detected using the regex (^|[\s;&|()])${tool.binary}(?=$|[\s;&|()]), so credentials are injected only when the specific binary appears as a word-boundary token in the command.[1]
destroyOwnerAuthHandle retries teardown up to 3 times with exponential-style back-off (sleep(50 * attempt) ms between attempts) before re-throwing the last error.[1] Turn files are swept by age: the constant TURN_FILES_MAX_AGE_MS sets the stale threshold to 24 hours (24 * 60 * 60_000 ms), and files older than this are removed during sandbox provisioning via sweepStaleTurnFiles.[1]
Sources