Shell and Sandbox
Persistent Workspace Sandboxes, Shell Sessions, host execution, and approvals
Shell and Sandbox
Downcity separates four responsibilities:
City → Workspace
Workspace → Shell
Shell → SandboxProvider + persistent Workspace Sandbox + Shell Sessions
Chat Session → caller onlyA Workspace gets one isolated, persistent Sandbox. Every Chat Session using that Workspace shares its Sandbox HOME, installed tools, and caches. Different Workspaces never share writable Sandbox state. Shell Sessions are independent resources identified by shell_id; they are not Chat Sessions.
Install and setup
pnpm add @downcity/agent @downcity/city @downcity/sandbox-microsandbox
npx microsandbox setupThe default Provider uses microsandbox microVMs and OCI images on supported macOS, Linux, and Windows hosts.
import { Agent } from "@downcity/agent";
import { City, Shell, Workspace } from "@downcity/city";
import { MicrosandboxProvider } from "@downcity/sandbox-microsandbox";
const workspace = new Workspace({
id: "project",
path: process.cwd(),
shell: new Shell({
sandbox_provider: new MicrosandboxProvider(),
}),
});
const city = new City({
workspaces: [workspace],
});
const agent = new Agent({ id: "repo-helper", model });
city.agents.add(agent);An embedded host that does not use City passes sandbox_provider to Shell and runtime_path to Workspace. City only supplies the runtime path automatically; it never selects or owns the Provider.
Execution targets
Shell has only two targets:
sandboxis the default. Commands run in the Workspace microVM, with the project mounted at/workspace.hostruns with full host permissions and requires a non-empty reason plus host approval.
shell_exec({
cmd: "open ./report.pdf",
target: "host",
reason: "Open the generated report in the host desktop application.",
});There is no partial host-path allowlist, intermediate permission mode, or silent fallback. If the Sandbox is unavailable, execution fails instead of switching to the host.
Tools and lifecycle
Shell provides shell_exec for short non-interactive commands and shell_session for long-running or interactive processes. shell_session supports start, send, read, wait, list, and stop.
Closing a Workspace disposes its Shell. Shell first closes its Shell Sessions and then stops Sandbox compute, preserving the Sandbox filesystem. Recreating the same Workspace under the same City storage reconnects to that environment. Only an explicit shell.reset_sandbox() deletes HOME and installed tools.
The Workspace mount is fixed when the Sandbox is created. If a stable Workspace ID is later bound to a different host path or City runtime path, Downcity rejects the connection and requires an explicit reset rather than silently mounting unexpected files.
Environment
Sandbox commands receive the explicit Workspace env and Downcity execution identifiers; they do not inherit the complete host process.env. Host commands start from the host environment and then apply Workspace env. Secret Downcity authentication variables are removed in both cases.
workspace.set_env() and workspace.patch_env() affect subsequent commands. Existing processes keep the environment captured when they started.
Approval
Every host start and every stdin write to a host Shell Session goes through the calling Chat Session's approval gateway. Approval belongs to the tool call, while the resulting Shell Session remains a Workspace Shell resource.
const pending = await session.interactions();
await session.respond({
interaction_id: pending[0].request.interaction_id,
response: {
type: "approval",
outcome: "resolved",
payload: { decision: "approved" },
},
});ask is the default approval mode. always-allow affects new host approval requests in that Chat Session only; it does not change the Sandbox boundary.