/sandboxes): list,
builder (/sandboxes/new, /sandboxes/:agentId/edit), chat
(/sandboxes/:agentId/chat) and a detail shell with tabs overview, setup, access,
routing, knowledge, memory, tools, mcp-servers, activity, slack,
versions. Machines belong to OHO and environments, one-shot
tasks to Cloud tasks and executors.
What it does
A persistent agent with a persistent data store. A sandbox is an agent whosesandbox_type is set. It keeps one identity (name, persona, system prompt, model,
knowledge, memory, tools, MCP servers, versions and access, as for a regular agent, see
Agents) and one working state: a /workspace with the cloned repositories,
whatever the agent installed or wrote, its credentials, and the executor session behind
each thread. Stopping keeps that state; only a rebuild or delete discards it. Channels
are slack and olyros_chat; each thread maps to one resumable session. New sandboxes
start active with version 1 published to production.
Seven sandbox states. NEVER_BUILT (created, nothing provisioned yet),
PROVISIONING, RUNNING, STOPPED, REBUILDING, FAILED, DESTROYED. Alongside it
an agent state: READY, BUSY (a turn is running), UNAVAILABLE. The venue’s
supervisor is authoritative and pushes sandbox.state_change events; the state field
on the agent exposes the mirror (state, reason, last activity, provisioned/stopped times,
rebuild count, failure reason, last active user, workspace and credential disk bytes).
Lifecycle actions. startSandboxedAgent brings a STOPPED sandbox back and
provisions a NEVER_BUILT or DESTROYED one; stopSandboxedAgent stops the container
(the workspace is kept); rebuildSandboxedAgent destroys and re-provisions from the
current Environment, repositories and overlay, expiring every active thread session
first; reconcileSandbox makes the host re-read the real container or VM state and
correct drift; deleteSandboxedAgent destroys the container (or VM, disk and service
account) and removes the agent with its history. All of these return once the host has
scheduled the work; progress arrives through state and lifecycle events. The Slack App
Home tab offers Start, Stop and Rebuild buttons that call the same cores, to people who
may chat with the sandbox (see Slack integration). A
router’s App Home shows the sandboxes the viewer is routed to with the
same buttons, and the routing tab lists the routes that land on this sandbox. A message to a NEVER_BUILT, DESTROYED or STOPPED
sandbox provisions or wakes it as part of the turn (cloud sandboxes boot the VM first).
Move (moveSandboxedAgent(agentId, backendId, force), from the list row’s menu, the
builder’s “Runs on” box in edit mode or the Overview tab’s “Runs on” row) rebuilds a built OHO sandbox on another OHO host or cloud pool
while keeping the agent id and everything the server holds for it: Slack binding, access
grants, routes, threads, chat history, activity and usage. The container is destroyed on the
old host, the agent is repointed in one transaction and started on the new host, so it comes
back RUNNING there with a fresh workspace; executor sessions are discarded, so the next
message in an old thread starts a new session, as after Rebuild. The target is checked first
(placement rule, engine, tenant-MCP and minimum-version gates), so a refused move leaves the
source untouched. When the old host is unreachable the move reports OHO_UNAVAILABLE; with
force it proceeds and queues an eviction that destroys the stale container and its
credentials when that host reconnects (force delete queues the same). The overview shows
“Moving to …” while a move is in flight and “Moved from … on …” afterwards; a failed move
leaves the sandbox NEVER_BUILT on the target with the reason on the move record. A cloud
(gce_pd) sandbox cannot be moved today.
Messages run through one pipeline for every channel. sendAgentChatMessage (web)
and the Slack adapter both call the same pipeline, which in order: checks the
agent.chat grants (see Who can use it); applies the sandbox_invoke billing gate; rejects when the
sandbox is FAILED (“Rebuild it to continue”) or REBUILDING; rejects when the agent
is BUSY (on Slack a second message instead stashes the text behind a Wait /
Stop & send prompt); builds the spec and dispatch, refusing a misconfigured agent
(no default model, wrong Environment or Executor type, integration without credentials)
or an OHO too old for the engine or for tenant MCP; resolves the thread session; records
the invocation and the user_message frame; dispatches sandbox.invoke. Every refusal
is persisted as a rejection frame so the chat and the Slack thread show the reason.
Outcomes: dispatched, rejected_unauthorized, rejected_billing,
rejected_unavailable, rejected_busy, pending_prompt (Slack only), hitl_answered,
hitl_stale.
Threads resume executor sessions. A session row maps (agent, channel, thread_key)
to the host’s executor session; a web key is minted on the first message, a Slack key is
channel plus thread timestamp. An active session resumes (resume_session); one expired
by a rebuild starts fresh under the same key (start_session). agentChatThreads lists
threads with message counts and Slack deep links, agentSessions the raw rows,
agentCurrentInvocation the running turn for the Stop button.
Frames are the transcript. The host streams typed frames: container_notice,
session_metadata, thinking, tool_call_summary, tool_call_arguments,
tool_call_result, final_reply, auth_request, hitl_request, mcp_auth_required,
rejection, user_message, error_detail, invocation_metrics. The web chat
subscribes with agentMessageFrames and rehydrates with agentMessagesInThread, both
filtered by the agent’s reply options (final_replies_only, status_and_replies,
verbose plus overrides, shared with Slack via setSlackReplyOptions). Frames with
tenant MCP data or Connect cards reach only the person who started the turn.
Stop a running turn. interruptSandboxInvocation(invocationId, mode: HARD) cancels
the host’s invoke task, as do the Slack prompt and the App Home Stop button. A missing,
finished or foreign invocation is a no-op (null); the row keeps who cancelled and why.
Idle timeout. Every 60 s the host stops a RUNNING sandbox idle longer than
idle_timeout_minutes (cause idle_timeout); setSandboxIdleTimeout applies live.
Repositories are cloned with short-lived tokens. The agent’s default repositories
(repo, read or read_write, branch, mount path, one marked as working directory,
from GitHub) are cloned at provision and rebuild. One GitHub App
installation token is minted per installation, scoped to contents: read or write
by the widest access level needed, and sent with the dispatch; a wake of an existing
container does not mint again. A default repository the installation no longer grants
is left out of the spec and not cloned.
Per-sandbox overlay on top of the Environment. Plain env vars
(setSandboxOverlayEnvVars), write-only secrets (setSandboxOverlaySecrets, key
names visible as overlaySecretKeys, values only through
revealSandboxOverlaySecret), an onCreate script that runs after each provision or
rebuild and a postStart script that runs on every container start
(setSandboxOncreateScript, setSandboxPoststartScript; both as the agent user in
/workspace). The overlay is applied last, so it wins over Environment values. A key
may be an env var or a secret, never both. Overlay, repository, Environment, Executor,
model and resource changes change the spec hash; sandboxSpecStatus.rebuildNeeded drives
the “rebuild required” banner until the next rebuild or start applies them.
Where it runs (execution venue). createAgent with sandboxType,
sandboxEnvironmentId, sandboxExecutorId and exactly one of sandboxOhoHostId or
backendId places the sandbox. An OHO-provider backend resolves to its OHO host and
yields an oho_docker sandbox (a Docker container on a machine the company connects);
a managed-cloud backend must carry the sandbox_hosting capability and yields a
gce_pd sandbox (one Google Cloud VM per sandbox with the workspace on a persistent
disk, driven by a server-side supervisor). The Environment must be of type docker
without volumes; the Executor type must be claude_agent_sdk, codex_cli or
gemini_cli; the LLM integration provider must match the engine (anthropic,
claude, bedrock for Claude; openai for Codex; google, gemini for Gemini).
setSandboxResources sets the cloud size class (standard = e2-standard-2, large
= e2-standard-4) and workspace disk size; OHO sandboxes ignore both. Hosts, backends,
pools and Environments are described on the OHO and environments page.
MCP bridge and bridge tools (OHO sandboxes). MCP servers bound to the agent with
runtime sandbox are mounted inside the container through the OHO host’s HTTP bridge:
the host mints a per-container bearer token at provision and a per-turn token for each
invocation, forwards tool calls over the cloud link, and the server resolves the
caller’s credentials; a missing or expired per-user credential returns a Connect card
(mcp_auth_required) to the person who asked (see MCP servers). The
same bridge carries two built-in tools: request_authentication, which pauses the turn
while the user completes a third-party OAuth consent that lands on the public
GET /auth/callback relay (states pending, code_received, used, denied,
expired, error), and ask_user, which posts a structured question
(hitl_request frame, Slack card with buttons) and blocks the turn until the person who
started it answers by button, by typing in the same thread, or with
answerHitlRequest on the web.
Lifecycle events are pushed, not pulled. Every transition is an event of type
created, started, stopped, rebuilt, failed, reconciled, destroyed or
setup_script, with cause user_action, idle_timeout, crash, reconciliation,
message_triggered or system, from and to state, and a detail payload (setup-script
results land here). OHO pushes each event over its WebSocket, a reconnect replays the gap
from a per-sandbox cursor, and an hourly job re-walks RUNNING and REBUILDING
sandboxes. sandboxLifecycleEvents, sandboxLifecycleEventsPage and the agent’s
lifecycleEvents read the local table; the only host round-trip is a one-time backfill
of up to 1000 events for a pre-existing OHO sandbox.
Duplicate. duplicateSandboxAgent(agentId, name, slug, backendId) deep-copies a
sandbox server-side: configuration, idle timeout, resources, Chat grants, reply
options, overlay env vars, the encrypted secrets blob verbatim, both scripts and the
default repositories. The copy is NEVER_BUILT, named Copy of <name> by default with
a unique slug, possibly on another backend. Knowledge, memory, threads, chat history and
lifecycle events are not copied.
Cost and usage. The terminal invocation_metrics frame of each turn becomes an
agent interaction with tokens and cost, attributed to the Slack or Olyros user who sent
the message and tagged sandbox_oho_docker or sandbox_gce_pd for usage and cost
reports. sandboxExecutionUsage gives 30 days of compute hours and invocations;
environmentSandboxes lists an Environment’s sandboxes with state, size and disk.
Who can use it
- Creating, editing, starting, stopping, rebuilding, reconciling and duplicating a sandbox, stopping a turn and setting the idle timeout in the app: any company member (membership only).
- Overlay env vars, secrets and scripts, revealing a secret value, cloud size and disk,
the Access tab (
setAgentAccess) and the deprecatedsetSandboxAllowlist: editor or admin (abilityagent.manage). deleteSandboxedAgent: company admin (abilitysandbox.delete).- Moving a sandbox to another host or pool (
moveSandboxedAgent): editor or admin (abilityagent.manage); it is not offered on Slack App Home. - Who may talk to a sandbox is the object-scoped
agent.chatability, decided by the grants in the sandbox’s Access table, the same table agents use: a sandbox with no grant is open to every company member and Slack user; once it has any grant, only the listed Olyros users, Slack users and Slack groups (and their linked identities) pass, on web chat and Slack alike. Admins and editors are subject to it too. The check runs on every message and on Slack App Home buttons; through a router, the router’s own grants are checked first. How grants are matched is described on Agents. - The live frame subscription requires an active company and a sandbox in it; initiator-private frames go only to the person who started the turn.
- No plan or tier gate exists. The
sandbox_invokebilling gate and themax_agentsstructural gate are platform settings that default tooff.
How to configure it
- Prerequisites (OHO and environments page): a connected OHO host or an entitled
cloud pool with
sandbox_hosting, an Environment of typedocker, an Executor of typeclaude_agent_sdk,codex_cliorgemini_cli, and an LLM integration with credentials whose provider matches the engine. - Create (Sandboxes -> New):
createAgent(name, sandboxType, sandboxEnvironmentId, sandboxExecutorId, sandboxOhoHostId | backendId, defaultProvider, defaultModel, defaultIntegrationId, systemPrompt, ...). Supplying both or neither placement is rejected; a sandbox with no default model is refused at dispatch time. - Attach repositories on the agent (default repositories, from GitHub).
- Setup & secrets tab:
setSandboxOverlayEnvVars,setSandboxOverlaySecrets(valueomitted keeps the stored value, a key left out is deleted, empty values are rejected),setSandboxOncreateScript,setSandboxPoststartScript. Keys must match^[A-Z][A-Z0-9_]*$. - Access tab (editor or admin): one Chat row in the Access table,
setAgentAccess(agentId, userIds, slackUserIds, slackGroupIds); no grants at all keeps the sandbox open to everyone. - Behaviour:
setSandboxIdleTimeout(agentId, minutes); for cloud sandboxessetSandboxResources(agentId, sizeClass, diskSizeGb). - Slack tab to connect a Slack app and choose reply options; see Slack integration.
- Start (
startSandboxedAgent) or simply send the first message; watch the Activity tab (sandboxLifecycleEventsPage) and the state pills. When the banner says a rebuild is needed,rebuildSandboxedAgent. - Placement changes through
updateAgent(sandboxOhoHostIdorbackendId) while the sandbox isNEVER_BUILT; once built, Move… (moveSandboxedAgent) rebuilds it on the chosen OHO host or pool with the same agent id, or duplicate onto the new backend to keep both.
Limits and defaults
What it emits
- State changes mirrored into the agent’s
state; Slack lifecycle notices onRUNNING,STOPPEDandFAILED; an App Home republish. - Lifecycle event rows (type, cause, from/to state, detail) per transition.
- One invocation row per turn (
running,completed,failed,cancelled, with channel, thread, initiator and cancellation metadata) and one frame row per streamed frame, replayable in the web chat; session rows per thread, expired on rebuild. - An agent interaction with tokens, cost, actor and
sandbox_<type>environment type per completed turn, feeding usage and cost attribution. - Slack outbound messages for visible frames, rejections, HITL and OAuth cards; Slack lifecycle action audit rows for App Home clicks; pending OAuth and HITL request rows.
Known gaps
- No workflow node, schedule target or API trigger can message a sandbox: those create one-shot cloud tasks; only web chat and Slack reach a sandbox.
gce_pdsandboxes have no MCP bridge, noask_userand no GitHub token refresh after provision; the cloud driver is behind the platform flaggcp_sandbox_driver_enabled.- Overlay and resource edits need a rebuild; only the idle timeout is pushed live.
- A web user who writes while the agent is
BUSYgetsrejected_busy; the Wait / Stop & send prompt exists on Slack only. SOFTinterrupt is defined but every client sendsHARD.- The
sandbox_invokebilling gate defaults tooff, so unpaid accounts are not stopped. agentSessionsalways reportsmessageCount0; onlyagentChatThreadscomputes it.setSandboxAllowlist(old clients) cannot set Slack groups; usesetAgentAccess.- There is no per-sandbox cost cap or budget.
- A move does not carry the workspace or the executor sessions; moving with contents is a
backlog concept (
documentation/projects/2026-11/sandbox_move/with_contents_concept.md). Cloud (gce_pd) sandboxes cannot be moved, and there is no “move everything off this host” action.