> ## Documentation Index
> Fetch the complete documentation index at: https://user-docs.olyrosstudios.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandboxes

> A sandbox is a persistent agent: it keeps its own workspace, your cloned repositories, the tools it installed and your conversation history between sessions.

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](https://user-docs.olyrosstudios.com/oho-and-environments.md), one-shot
tasks to [Cloud tasks and executors](https://user-docs.olyrosstudios.com/cloud-tasks-and-executors.md).

## 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](https://user-docs.olyrosstudios.com/agents.md)) 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](https://user-docs.olyrosstudios.com/slack.md)). A
[router](https://user-docs.olyrosstudios.com/routers.md)'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](https://user-docs.olyrosstudios.com/github.md)) 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](https://user-docs.olyrosstudios.com/mcp-servers.md)). 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](https://user-docs.olyrosstudios.com/agents.md).
* 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](https://user-docs.olyrosstudios.com/github.md)).
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](https://user-docs.olyrosstudios.com/slack.md).
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

| Setting | Default | Where enforced |
| - | - | - |
| `sandbox_idle_timeout_minutes` | 15 (1 to 1440) | `setSandboxIdleTimeout`; OHO idle watcher and cloud idle sweep tick every 60 s |
| `sandbox_size_class` | `standard` (`standard`, `large`) | `setSandboxResources`; machine type per size class |
| `sandbox_disk_size_gb` | 10 (10 to 500) | `setSandboxResources`; cannot shrink once a cloud disk exists |
| onCreate / postStart script size | 100 KB each | overlay mutations |
| Overlay key format | `^[A-Z][A-Z0-9_]*$`, no env/secret overlap | overlay mutations |
| `agentMessagesInThread` limit | 200 (1 to 500) | query |
| Lifecycle event page limit | 50 (1 to 1000) | query |
| Lazy backfill from OHO | 1000 events, once per sandbox | lifecycle event backfill |
| Interrupt round-trip timeout | 5 s | interrupt relay |
| `ask_user` wait | 600 s (30 to 600) | HITL `clamp_timeout` for `sandbox_agent` |
| OAuth relay | 10 min code TTL, 60 requests/min per IP | OAuth relay endpoint |
| Cloud start grace | 300 s to dial in, 240 s to build the container | `CloudSandboxSupervisor` |
| GitHub installation token | about 1 h, minted per provision or rebuild | dispatch |
| Interrupt mode | `HARD` only (`SOFT` defined, unreachable) | `interruptSandboxInvocation` |

## 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.