Skip to main content
A sandbox is a persistent agent: one identity with its own data store, working directory, cloned repositories, installed tooling and conversation history, all kept between conversations. People reach it from Slack and the web chat, and every thread resumes where it stopped. Whether it runs on a machine the company connects through OHO or on cloud execution Olyros manages is a venue choice, not what a sandbox is. Sandboxes live under the Agents sidebar group as Sandboxes (/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 whose sandbox_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 deprecated setSandboxAllowlist: editor or admin (ability agent.manage).
  • deleteSandboxedAgent: company admin (ability sandbox.delete).
  • Moving a sandbox to another host or pool (moveSandboxedAgent): editor or admin (ability agent.manage); it is not offered on Slack App Home.
  • Who may talk to a sandbox is the object-scoped agent.chat ability, 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_invoke billing gate and the max_agents structural gate are platform settings that default to off.

How to configure it

  1. Prerequisites (OHO and environments page): a connected OHO host or an entitled cloud pool with sandbox_hosting, an Environment of type docker, an Executor of type claude_agent_sdk, codex_cli or gemini_cli, and an LLM integration with credentials whose provider matches the engine.
  2. 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.
  3. Attach repositories on the agent (default repositories, from GitHub).
  4. Setup & secrets tab: setSandboxOverlayEnvVars, setSandboxOverlaySecrets (value omitted keeps the stored value, a key left out is deleted, empty values are rejected), setSandboxOncreateScript, setSandboxPoststartScript. Keys must match ^[A-Z][A-Z0-9_]*$.
  5. 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.
  6. Behaviour: setSandboxIdleTimeout(agentId, minutes); for cloud sandboxes setSandboxResources(agentId, sizeClass, diskSizeGb).
  7. Slack tab to connect a Slack app and choose reply options; see Slack integration.
  8. 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.
  9. Placement changes through updateAgent (sandboxOhoHostId or backendId) while the sandbox is NEVER_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 on RUNNING, STOPPED and FAILED; 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_pd sandboxes have no MCP bridge, no ask_user and no GitHub token refresh after provision; the cloud driver is behind the platform flag gcp_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 BUSY gets rejected_busy; the Wait / Stop & send prompt exists on Slack only.
  • SOFT interrupt is defined but every client sends HARD.
  • The sandbox_invoke billing gate defaults to off, so unpaid accounts are not stopped.
  • agentSessions always reports messageCount 0; only agentChatThreads computes it.
  • setSandboxAllowlist (old clients) cannot set Slack groups; use setAgentAccess.
  • 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.

Customer-safe summary

A sandbox is a persistent agent: it keeps its own workspace, your cloned repositories, the tools it installed and your conversation history between sessions. You talk to it from Slack or the web chat, each thread picks up where it left off, and you choose who may talk to it. It wakes on the first message, pauses after a configurable idle period, and can be started, stopped, rebuilt or deleted from the app or from Slack; only a rebuild or delete clears its state. You layer your own settings, secrets and setup scripts on the team’s base image, and run it on a machine your team connects or on cloud capacity Olyros manages. Inside a turn it can call your connected tools, ask you to sign in to a service, or ask you a question and wait. Every state change, turn and cost is recorded in its activity and your usage reports.