Environments¶
An environment is where file and shell tools run. That choice is
independent of isolation (run mode), which is where Pi
itself runs. When the computer is local, Pi and the files share the same
isolation boundary. A remote runner is valid with none and
microvm. That is the only supported split.
Types¶
environment.type |
When |
|---|---|
openai_hosted |
Default. Session directory next to Pi. |
hosted |
Alias for openai_hosted. Stored and returned as openai_hosted. |
none |
No filesystem, no shell. |
self_hosted |
External runner. Tools go over a socket. Not an ApiPi sandbox worker. |
openai_hosted is OpenAI's field name for a local session directory.
It is not OpenAI's cloud VM. hosted means the same folder. You
can override the type per session. If you omit environment on
create, the gateway uses openai_hosted.
Search and browser are not environments. Attach them as MCP. See tools.
Local directory (openai_hosted)¶
One directory per session. Pi and this folder share the same
run mode isolation. There is no mode that puts Pi in
one sandbox and the local files in another. The path is
{APIPI_SESSIONS_DIR}/{tenant_id}/{session_id}. When
APIPI_SESSIONS_DIR is unset, that root is .apipi/sessions under the
gateway's working directory.
In isolation none this is a folder on the host. In microvm, that
folder is packed into a workspace drive
at boot and unpacked onto a guest tmpfs at /workspace. Files under
outputs/ are harvested when a turn completes. Scratch files do not
survive sandbox stop.
Session rows live in the store. The openai_hosted workspace is
ephemeral: after APIPI_SANDBOX_TTL_OPENAI_HOSTED (default 1 hour)
with no activity, Pi stops and the directory is deleted. Transcript,
published artifacts, and the harness session cache stay (the session
row stores a file:// or s3:// URI for that cache). The next turn
creates an empty /workspace, re-applies skills, packages, setup
commands, files, env, and network policy, and reloads the cached
session file so Pi continues the
conversation. Published files are not copied back into /workspace.
The directory is bounded by APIPI_MAX_WORKSPACE_BYTES (default 1GiB).
Artifact bytes are copied to the gateway host when a turn completes,
up to APIPI_MAX_ARTIFACT_BYTES (default 512MiB) per session. See
run modes and config.
There is no runner socket. The directory is created when the session is created. File tools (read, write, edit, bash) run against that folder.
Sandbox size¶
Session create may include environment.sandbox_size with value S,
M, or L. That field is an ApiPi extension. Official OpenAI clients
that reject unknown environment keys can set
metadata["apipi.sandbox_size"] instead. Other metadata keys stay
opaque tags. The apipi. prefix is reserved. The gateway reads
apipi.sandbox_size (this page) and apipi.session_kind (placement).
It writes apipi.title and apipi.title_status when automatic titles
are on. Extenders may set apipi.actor_type, apipi.schedule_id, and
apipi.source. The gateway stores those keys and does not schedule
from them. The full list is in
reserved metadata.
Resolution, highest wins:
environment.sandbox_size- Session create
metadata["apipi.sandbox_size"] - Agent
metadata["apipi.sandbox_size"] - Gateway
APIPI_SANDBOX_DEFAULT_SIZE/[sandbox].default_size(shipped defaultS)
The resolved size is stored on the session environment and is fixed
for the life of the live guest. Updating session metadata later does
not resize or reimage an already chosen size.
Sandbox image¶
environment.sandbox_image chooses the guest image by id. It is
separate from size. Size is RAM. Official clients can set
metadata["apipi.sandbox_image"] instead. Resolution, highest wins:
environment.sandbox_image- Session create
metadata["apipi.sandbox_image"] - Agent
metadata["apipi.sandbox_image"] - Size
Lselectsbrowser. Other sizes fall through. APIPI_SANDBOX_DEFAULT_IMAGE/[sandbox].default_image(shipped defaultdefault)
The resolved id is stored on the session environment as
sandbox_image. A later metadata update does not reimage a live guest.
An id must match ^[a-z0-9][a-z0-9-]{0,31}$. An unknown id is 400.
Each image has a minimum size. browser needs M or larger. A smaller
size is 400. Isolation none stores the field and does not apply it.
Playwright MCP is injected when the resolved image is browser and
auto-inject is on, not because the size is L. The shipped default
still maps L to browser, so existing L sessions keep the tools.
| Size | Guest RAM | Rootfs | When |
|---|---|---|---|
S |
[sandbox.resources].mem_mib (512) |
default |
Pi and light tools |
M |
APIPI_SANDBOX_M_MEM_MIB (1024) |
default |
Heavier non-browser work |
L |
APIPI_SANDBOX_L_MEM_MIB (2048) |
browser |
Chromium in the guest plus Playwright MCP tools (unless you already attached them). Install the browser rootfs. |
Isolation none accepts the field and does not apply RAM or rootfs.
Isolation microvm applies both, including when environment.type is
none (Pi still runs in a guest). Each live lease consumes that
size's RAM against worker memory_mb and still counts as one session.
On microvm, size L injects a Playwright stdio MCP server
(npx @playwright/mcp, headless, isolated, system Chromium at
/usr/bin/chromium-browser) so the browser just works without
examples/playwright.yaml. If the agent already has a Playwright MCP
tool (server_label playwright or the same package), that tool is
kept and nothing is duplicated. Set [sandbox.browser].auto_playwright
= false to keep L RAM and rootfs but attach MCP yourself. A Playwright
process that cannot start makes Pi exit instead of running L without
browser tools. The platform prompt always names the sandbox size. It
mentions Chromium and MCP tool names only when those tools are
attached, and it tells the model not to install Playwright or browsers.
Packages, files, env, network, and setup commands¶
Session create may include environment.packages,
environment.setup_commands, environment.env,
environment.files, and environment.network on openai_hosted.
Those fields are stored on the session. Prep runs before the first
agent turn that needs the computer:
- Write
filesinto the session directory. Paths use the same/workspaceand/tmp/workspacemapping as setupcwd. Other absolute paths,..escapes, and writes under.apipi/are rejected.type: "inline"uses standard base64data.type: "file_id"copies bytes from a Files API upload owned by this tenant. The decoded total must fitAPIPI_MAX_WORKSPACE_BYTES. At most 50 files per create. A missing or foreignfile_idis404. - Apply
env(string keys and values) to that session's Pi process and to prep. Reserved names are rejected:PATH,HOME,USER,SHELL,PWD,LD_LIBRARY_PATH,LD_PRELOAD,OPENAI_API_KEY,OPENAI_BASE_URL,DATABASE_URL,PI_CODING_AGENT_DIR, and any name starting withAPIPI_,CODEX_, orPI_. - Install
packages.python, thenpackages.system, thenpackages.npm. - Run
setup_commandsin order. Each item is an object withcommandand optionalcwd.cwddefaults to the session directory. Absolute OpenAI paths/workspaceand/tmp/workspacemap to that directory. Other absolute paths are rejected.
network.access is enabled, disabled, or restricted.
restricted requires allowed_domains (1–100 exact hostnames).
enabled allows outbound traffic to the public internet. Private and
special-use IPv4 ranges are always rejected. If the process-wide TAP
allowlist is on, that list still wins for public hosts. disabled
blocks guest TAP egress (DNS and the host broker on the TAP subnet
still work). restricted allows only those hostnames, plus package
registries when packages is set so install can run. A session cannot
add a host that [sandbox.network] forbids. Model and HTTP MCP calls
go through the host broker, so they still work when TAP is locked.
After a sandbox TTL wipe, the next turn recreates /workspace and
re-applies the stored files (inline and Files API ids), env, packages,
setup commands, and network policy.
Isolation none runs that script in the session directory on the host
(uv pip or python3 -m pip, apk or apt-get if present, npm).
Missing tools fail the session. Isolation none cannot enforce TAP
policy: disabled and restricted fail the environment with a clear
error; enabled is a no-op. Isolation microvm packs the same
script into the guest and runs it after unpack, before Pi, in the same
guest, and applies network on that guest TAP. When the optional TAP
allowlist is on, install hosts (PyPI, npm, Alpine) are added for that
session if the matching package list is set.
A nonzero exit emits agent.session.environment.failed and
agent.session.failed. Pi does not start. Successful prep is visible
in the workspace before the turn. none and self_hosted environment
types reject packages, setup commands, env, and files. self_hosted
also rejects network. Environment type none ignores network.
none¶
No computer. Pi still runs the loop. Function tools and MCP still
work. There is no session directory and no shell. This type is an
Agents API field. /v1/chat never asks clients to set it and never
returns environment. Chat sessions still store type=none internally
so placement can use chat workers. See chat fleets.
self_hosted¶
Pi stays in the run mode (none or microvm). Production
SaaS and enterprise still run that Pi under microvm. The computer
is elsewhere. You must sandbox the runner. The gateway does not nest
the remote runner in a microvm. This socket is not the trusted
sandbox worker protocol.
- Create the session with
environment.typeself_hosted. - The create response includes
environment.idon the environment object, a top-levelenvironment_id, and a one-timekey. The key is not stored in plaintext and is not returned again. - The gateway emits
environment.pending. When the runner connects, it emitsenvironment.connected. If the socket drops, it emitsenvironment.disconnected. read/write/edit/bashgo over the socket, not through the Pi process's local filesystem.
required_actions may include environment_connection until the
runner is connected. Session status stays idle so turns that do not
need files can still run. If nothing connects, file tools stay off.
The runner opens /v1/environments/{environment_id} as a WebSocket and
sends hello with the key. Wrong id or key is not found. There is no
/v1/runners resource. One key is one workspace. That socket must
reach the same gateway process that created the session. See
multiple nodes.
Messages are JSON objects. hello is first:
The gateway replies {"type": "hello", "ok": true}. Later requests
have id. Replies: {"id": "...", "ok": true, ...} or
{"id": "...", "ok": false, "error": "..."}.
| Verb | Direction | Job |
| --- | --- |
| hello | runner → gateway | Auth, capabilities |
| exec | gateway → runner | Command in workspace cwd |
| read / write / edit / list | gateway → runner | Files |
| artifact | gateway → runner | Publish an output |
| ping | either | Keepalive (pong back) |
| close | either | Shutdown |
{"id": "...", "type": "exec", "command": "ls"}
{"id": "...", "type": "read", "path": "a.txt"}
{"id": "...", "type": "write", "path": "a.txt", "content": "..."}
{"id": "...", "type": "edit", "path": "a.txt", "old_text": "...", "new_text": "..."}
{"id": "...", "type": "list", "path": "."}
{"id": "...", "type": "artifact", "path": "out.bin"}
{"id": "...", "type": "ping"}
{"id": "...", "type": "close"}
Artifact bytes are read while the socket is up. 410 if the file is
gone or the runner is disconnected.
A runnable example that attaches a local directory as that computer is
examples/self_hosted_runner.py. It speaks this protocol. Pass the
one-time key and environment_id from session create in the
environment, not in the file. Setup is in examples/README.md.
Skills¶
environment.capability_directories lists paths on this computer that
contain skill directories (SKILL.md). They are discovered when the
session starts. Hosted packs upload at /v1/skills and attach with
environment.skills { "type": "skill_reference", "skill_id": "…" }.
Those zips unpack under .agents/skills/ in the session workspace.
See tools.
Stdio MCP (for example Playwright) follows Pi, not the remote runner. HTTP MCP is reached from the gateway and handed to Pi.