Security model
pi has no permission prompts by design. The container is the security boundary (constitution Principle I). This page is the threat model (spec FR-023), the hardening that implements it, the network endpoints (FR-011) and every accepted risk.
Threat model#
In scope — what the sandbox protects the host against:
| Threat | Mitigation |
|---|---|
| Mistakes by the agent (deleting files, running destructive commands) | Only the selected project, the memory stores and the agent state are writable; the image and config are read-only |
| Prompt injection through project files, web content or memory | Same boundary: an injected instruction can only act inside the container and its mounts |
| A malicious or compromised pi package or extension | Same boundary; packages are pinned and reviewed (pi-config/docs/migration.md) — but see “Packages” below |
| Reaching host secrets | No host home, ~/.ssh, SSH agent, host git config, host ~/.pi/agent, Keychain or docker.sock in the container; tested by pi-sandbox selftest |
| Pushing code or memory without review | Memory credentials exist only in the separate sync container. Code can be pushed from a session only when the owner configured a push credential for the domain and git host (credentials.md): default and protected branches, force pushes and ref deletion are refused by an in-session guard (defence in depth) and by the forge’s branch protection (the guarantee) |
Out of scope for v1:
- escape from the container runtime or the Docker Desktop VM, and kernel exploits;
- a compromised host (the Mac itself);
- exfiltration of data over the network (outbound access is unrestricted, see below).
Accepted risks, each tied to its requirement:
| Risk | Requirement | Where documented |
|---|---|---|
| Session history is shared across domains, kept without a time limit and may contain customer data seen in work sessions | FR-008a | state.md |
| An injected API key is readable by the agent during the session | FR-008b | credentials.md |
| Unrestricted outbound network: the agent can reach the LAN/homelab and send project code, memory or session content to any internet host | FR-011 | below |
| Memory “never store” rules are behavioural (instructions + owner review), not technical | FR-016a | memory.md |
| The sync container runs the same image as the agent | FR-019a | memory.md |
| The Codeberg token for core/private memory can write to every repo of the owner’s account (Codeberg has no per-repo deploy tokens) | FR-019a | memory.md |
| Project trust decisions are shared across domains | Edge Cases | state.md |
| Packages and extensions run with the agent’s full access | FR-024 | below |
| A C/C++ toolchain is available in the image (for native package builds) | FR-024 | Hardening table |
kubeconfig (without a level kube.json; with one, only the kube-proxy sidecar holds it and the agent can only read (pod logs included, secrets excluded) and rollout restart through it) / talosconfig, STACKIT, Hetzner Cloud and GitHub sign-ins in the domain volume are readable by the agent in that domain’s sessions and allow what their token allows | FR-008b | credentials.md |
| A forge token for workspace cloning is readable inside the workspace container during the run (not in sessions); it has whatever access the token grants | FR-008b | workspace.md |
SSH keys of the ssh.json hosts: only the ssh-agent sidecar holds them, but the agent can use them during the session to log in to any server that accepts them (keys are not bound to their host inside the agent) and can read what those servers hold | FR-008b | config.md |
The push credential of a domain and host (SSH key or HTTPS token) is readable by the agent during the session (the entrypoint’s /proc/<pid>/environ holds it; the ssh-agent socket can sign) and allows whatever it is allowed to do on the forge; the pre-push guard is bypassable by the agent and only the forge’s branch protection is a real limit. Mitigation: a dedicated credential per domain and host, a machine user or deploy key with least access, branch protection | FR-008b | credentials.md |
An autonomous /yolo run (pi-config spec 012, constitution v1.3.0) can merge its own PR and deploy, production included, with the push credential and forge access the session already has; a wrong change can reach production in ways rollback does not undo. The gates are behavioural; the real limits are the forge’s branch protection with required CI and the credential’s scope. Revocation: revoke the push key and forge token, remove the project’s ## Yolo block, or delete agent/prompts/yolo.md in pi-config. No sandbox change | FR-008b | pi-config docs/config-reference.md (agent/prompts/yolo.md) |
AWS credentials passed to an aws-api MCP server are readable by the agent during the session and allow what the agent’s permission set allows (read and write where granted) | FR-008b | config.md |
Hardening (compose.yaml)#
| Setting | Why |
|---|---|
user: "1000:1000" | pi runs as the unprivileged user pi |
cap_drop: [ALL] | No Linux capabilities; none are added back |
security_opt: no-new-privileges:true | No privilege gain through setuid binaries |
read_only: true | The image filesystem cannot be changed; only declared mounts and tmpfs are writable |
tmpfs: /tmp, /run/pi-session | In-memory scratch space, gone on exit |
init: true | Docker’s minimal init (mounted read-only at /usr/sbin/docker-init) reaps processes and forwards signals |
No ports:, default bridge network | Outbound only; nothing can connect in; no host network or shared namespaces |
No privileged, no docker.sock, no host home or SSH mounts | See the threat model |
openssh-client installed, but inert without a key | Push is opt-in per domain and host: no credential means no agent, no token file, no GIT_SSH_COMMAND, no hook path; the selftest checks both states |
python3, make, g++ installed | Needed so node-gyp can compile native addons of pinned packages without a linux-arm64 prebuilt (node-pty for plannotator). Accepted risk: the agent can compile and run native code — inside the same boundary |
scripts/check-sandbox-definition.sh (run by just check) fails if any of these is weakened,
if a mount outside path-contract.md appears, or if the base image or pi
version is unpinned (spec SC-008).
Resource limits#
| Limit | Default | Change in |
|---|---|---|
| CPUs | 4 | compose.yaml → cpus |
| Memory | 8 GiB | compose.yaml → mem_limit |
| Processes | 512 | compose.yaml → pids_limit |
Generous enough for LSP-heavy extensions; adjust deliberately and note it here.
Network (FR-011)#
Outbound access is unrestricted in v1. The agent relies on:
| Endpoint | Used for |
|---|---|
api.anthropic.com (and any other configured model provider’s API host) | Model requests |
Your model proxy proxy host (configured with /login model-proxy) | Model requests via pi-provider-model-proxy |
registry.npmjs.org | Installing pinned pi packages into the agent state |
*.api.stackit.cloud, api.hetzner.cloud, api.github.com | STACKIT CLI, Hetzner Cloud CLI and GitHub CLI after a sign-in in the domain (credentials.md) |
| Hermit package sources (GitHub releases and the package hosts the manifests name) | Toolsets of tools.json and the repos’ own Hermit pins, downloaded into the domain’s cache. The Hermit binary and the package index are pinned in the image; the packages are verified by the checksums of their manifests. A repo’s own Hermit pins are not verified by the image: trust a Hermit-managed repo like its scripts (credentials.md) |
| The API servers in your kubeconfig / talosconfig | kubectl and talosctl after you put a config into the domain (credentials.md) |
aws-mcp.us-east-1.api.aws | AWS MCP Server: documentation, skills and — with an aws-api server and credentials — AWS API calls (config.md) |
The forge hosts of pi-sandbox workspace (API and HTTPS clones) | Workspace container only (workspace.md) |
gitlab.com, github.com, codeberg.org | Clones, fetches and pulls of public dependencies; memory sync pushes (sync container only) |
| Arbitrary hosts | Web search and fetch tools (for example pi-web-access) |
Residual risk: the agent can reach every host the Mac can reach, including the LAN and homelab services, and it could send project code, memory or session content anywhere. This list exists so that an egress allow-list can be added on purpose later.
Inbound: nothing. No published ports, no host network mode, no shared network namespace.
Released image (supply chain)#
Sessions run codeberg.org/black0ut-pi-setup/pi-sandbox:<VERSION>, built and pushed by the
release pipeline on the self-hosted runner from this repo’s pinned Dockerfile. Whoever controls
that runner or the release token can publish an image the sandbox will pull. Keep the runner
dedicated to these repos and the token limited to write:repository + write:package
(release.md). pi-sandbox rebuild builds the same tag locally from source if you
prefer not to trust the registry.
Memory check#
The memory check guards what leaves the machine through memory-sync: secrets block a store,
possible personal data must be confirmed per finding (memory.md). It is
a safety net, not a guarantee — detection is heuristic. The on-demand pi-sandbox memory-check
container has no network, no credentials and read-only stores. gitleaks comes from the official
image pinned by digest (same supply-chain rules as the base image); reports never contain the
matched values.
Packages and extensions (FR-024)#
pi packages and extensions run inside the pi process with the agent’s full access in the
container, including the agent state and stored credentials. Only packages kept in the
migration review, or added later with a documented source, exact pinned version and reason in
pi-config/docs/config-reference.md, may be installed.