Shell and Sandbox
Persistent Workspace Sandboxes, Shell Sessions, host execution, and approvals
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.
Paths and the Workspace mount
File and search tools run in the host process, while Shell commands run inside the Sandbox. Both accept a sandbox-internal path such as /workspace/src/main.ts: the sandbox path is mapped back to the project root before the path policy runs, so /workspace/src/main.ts and src/main.ts address the same file. Results always report host-side paths. Paths outside the mount, such as /etc/hosts, are still rejected.
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.
Failure messages
When a command exits with a nonzero code or its Shell status is failed, the tool returns success: false and an explicit error containing the exit code, when available, and up to 2000 characters from the end of its output. The returned output remains available in output. If an interactive session has no output in the current chunk, the error uses its latest output preview.
An unapproved host request returns Host execution was not approved. A missing approval gateway or missing call identity can also produce this result; the message does not imply that the user explicitly rejected the request.
Local Agent Session IDs are case insensitive; creation and lookup normalize them to lowercase. New and forked Sessions use the same 12-character random format, such as k7m2x9p4w6rt, excluding ambiguous 0/o and 1/i/l. Generated IDs are used directly as directory names. Storage still handles special path characters and Windows reserved names in custom IDs.
Existing Sessions require offline migration before using the new rules. The script previews changes by default:
node public/packages/agent/scripts/migrate-session-ids.mjs /path/to/agent-root
# Stop Downcity and review the preview and external references before applying:
node public/packages/agent/scripts/migrate-session-ids.mjs /path/to/agent-root --apply --offlineThe script checks for case collisions within each origin partition, backs up active and archived trees, updates database identities, message origins and structured attachment paths, then renames directories. Existing IDs are lowercased rather than shortened. The backup's manifest.json supplies ID and path mappings. Owners of Task, Messenger, Power and custom Composer references must update their external data using that mapping before restarting Downcity. If migration fails, keep Downcity offline and restore both Session trees from the backup.
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.