A Pi Daemon instance is one operator trust domain, not a sandbox. The server process holds no agent code: tools, MCP servers and extension modules run in supervised per-profile worker processes, and each worker is one trust domain. Real isolation between mutually untrusted workloads needs a separate uid, container or remote execution environment.
All clients of an instance share its bearer, so each can see and drive every session of that instance. Run a separate instance (its own state directory, port and bearer) for a client that must not.
Enforced defaults
- exactly one effective service bearer source: when no external file,
fd, or environment source is configured, first launch atomically
generates
STATE_DIR/api-token - generated bearer files are random, owner-only, complete before publication, stable across restart, and never overwrite an existing path
- configured secret-manager bearer symlinks, including relative and nested links, are resolved to a canonical target that is opened with no-follow protection against a second traversal; the opened inode must be regular, current-user-owned, owner-only, and bounded
- bearer material is reduced to a one-way digest after loading and never logged or returned
- every HTTP request and WebSocket upgrade except
/healthzand/readyzmust present the bearer asAuthorization: Bearer <token>; missing or wrong bearers are refused with 401 before routing - state and Pi agent directories are created owner-only; foreign-owned, symlinked or group/world-accessible daemon paths are refused
auth.jsonis seeded at most once, from an owner-only regular non-symlink JSON file, and never overwrites an existing file- instance YAML must be current-user- or root-owned, not group/world writable, bounded, alias-free and free of unknown fields
- the listener binds loopback by default and speaks plain HTTP;
binding another address (
api.bind) is an explicit choice, so use a trusted network such as a tailnet, or a TLS reverse proxy, for remote clients - extension modules load only from the files a profile names, and a session may only leave some of them out; a built-in tool a profile does not list is never registered for its sessions
- a session's cwd must be under its profile's
cwdRoots(symlinks resolved), and clients may set only the nonsecret variables the profile'ssessionEnvallows (aPREFIX*pattern never matchesPATH,HOME,LD_PRELOADor the other protected names) - a session's own MCP servers that run commands are allowed only for a
session that has
bash; skills, prompt templates and context files a client names must be within the profile'scwdRootsoragentDir; a session's variables, prompt and server definitions are never logged or sent to clients - clients never supply secrets: keep MCP server and tool credentials
in a profile's
envFiles, outside the Nix store, rather than in YAML; per-session credentials go in files the profile'ssessionEnvFilesfind from each session's cwd, read only in the profile's worker - release self-update adopts only the exact npm-global launcher
symlink produced by
just install; regular files, arbitrary/mismatched links, and foreign targets remain collisions - logs never contain prompts, model output, credentials, environment values or tokens
See also the repository SECURITY.md.