Security
The additive HTTP/WebSocket session API uses one opaque server-wide
bearer for the complete API trust domain; it has no per-session
authorization in v1. The bearer is authenticated before bodies,
upgrades, or session existence are observed and is never accepted in
process-visible CLI arguments or emitted in logs, status, tickets,
manifests, journals, or metrics. See the Session API
contract.
A Pi Daemon process is one operator trust domain, not a sandbox.
Logical sessions isolate state and scheduling; they do not isolate
malicious JavaScript loaded into the same Node process. The initial
service therefore loads no project code or tools.
Enforced defaults
- authenticated JSON listener disabled unless
--api-port
is explicit
- JSON listener defaults to literal loopback and refuses implicit
remote plaintext
- 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
- bearer authorization is checked before JSON bodies, route/session
disclosure, and RPC/ACP stream upgrades
- bearer material is reduced to a one-way digest after startup loading
and never logged or returned
- owner-only Unix socket in a non-group/world-writable real
directory
- absent daemon-owned state, socket-parent, and Pi agent directories
are created as owner-only; permissive or overlapping state/credential
roots fail closed
- owner-only state directories, manifests, and journals
- symlink and traversal refusal for durable state/session paths
- required canonical
--allow-root for every logical
cwd
- cwd must not overlap daemon state or Pi credential roots unless the
operator explicitly enables the high-trust
security.allowAuthorityRootOverlap policy; with it enabled,
session tools can reach protected paths beneath the cwd
- newly managed Pi session files remain inside that logical session's
owner-private state directory
- explicitly confirmed direct co-opt may retain an existing source
under a configured inventory root when its directory/file are
current-user-owned, real, and not group/world writable; conventional
read/execute bits are preserved rather than silently chmodded
- newly persistent Pi managers are materialized as owner-only JSONL
before durable acceptance
- restart/eviction replay requires the exact recorded Pi session ID
and canonical file
- memory-only sessions never write replayable wake journals or
masquerade as durable
- ACP is route/cwd/generation scoped, bounded per peer/hub, and never
launches an adapter subprocess
- default Pi auth file must be an owner-only regular file
- an absent custom-agent
auth.json may be seeded once
from the bounded owner-only normal Pi auth file or an explicit required
seed; it is never synchronized, overwritten, logged, or returned
- empty built-in/custom tool allowlist
- empty extensions, skills, templates, themes, context files, and
append prompt unless an owner-private
web.runtimePolicy
grants them; explicit lists exclude ambient discovery, while
inheritInstalledPackages resolves only bounded global
packages already installed by the Pi CLI and never
installs/updates/spawns/uses network; missing or invalid resources abort
activation
- no Cacophony node token, CA key, daemon state, or orchestration
authority
- structured logs redact prompts, output/content, environment, and
credentials
- accepted wake requests are never blindly replayed after a crash
- neutral blob/file routes authenticate before existence, bind every
resource to exact canonical session/generation, accept no client path,
and never grant cwd/tool authority
- uploaded bytes are incrementally size/hash verified into
owner-private immutable SHA-256 objects; aliases deduplicate storage
without sharing authorization, and startup rehashes settled objects
before admission
- blob names/MIME remain explicitly untrusted; downloads are
attachment-only
application/octet-stream with
nosniff, archive/active defaults quarantine, and scanner
failures fail closed without content execution
- blob reservations, objects, references, recovery, queues, and TTL
cleanup are bounded; accepted cleanup/materialization tickets and
settled content are never blindly replayed
- queued wake requests are never replayed into a missing, corrupt, or
fresh replacement conversation
- Unix event subscriptions require explicit generation-bound
attach and detach
- WebSocket RPC upgrades authenticate before subprotocol, session,
cursor, or controller disclosure
- RPC readers have independent message/queue bounds; cursors are
scoped to host, canonical session, and generation
- one explicit controller may mutate or answer extension UI; observers
retain read-only state access
- the remote stdio client accepts bearer material only through a
bounded private file, inherited fd, or environment
- client bearer bytes exist only for the authenticated handshake and
never enter URL, argv, stdout, stderr, or reconnect status
- remote WebSocket use requires TLS or an operator-owned authenticated
loopback proxy
- Dash uses a separate owner-only web credential; the daemon service
bearer is server-to-server and never enters browser state
- Dash browser sessions are bounded, revocable, server-side records
addressed by an HMAC-signed opaque
HttpOnly,
SameSite=Strict cookie; their server-side record now binds
a validated principal while the cookie remains identity-free
- the multi-user foundation compares every bounded static credential
digest, preserves
local-owner compatibility, and persists
resource roles only in one owner-private central policy/audit ledger
that fails closed and rolls back memory on publication failure
- every browser HTTP, principal-bound inventory cursor, direct
resource/ticket lookup, and Rich/TUI frame/event now enforces that
ledger with content-free absent/unauthorized parity; the dedicated
backend still receives only the machine bearer
- authorization administration requires resource admin, CSRF, exact
policy/controller ETags and durable bounded idempotency; revocation
closes matching workspaces/channels, and controller handoff waits for
old-controller release before target grant and content-free audit
- multi-user activation requires an explicit bounded static provider;
identity metadata may enter strict YAML/CLI/Home Manager configuration,
but credential bytes come only from owner-only files/inherited
descriptors and never enter YAML, argv, Nix store values, status, logs,
cookies, or browser persistence; absent provider preserves exact
local-owner compatibility
- a cookie-authenticated same-origin bootstrap reproduces the
session-bound HMAC CSRF token in a no-store response header, so an
ordinary browser reload restores mutation authority without putting the
owner credential, cookie, or CSRF token in browser storage
- Dash private routes authenticate before route matching; mutations
require exact Host, Origin, and per-session CSRF validation
- Dash plaintext listeners default to loopback; an explicit
non-loopback bind requires a validated non-loopback public origin plus
allowInsecureHttp: true, emits a content-free advisory, and
retains mandatory authentication/Host/Origin/CSRF enforcement; loopback
TLS proxy or native HTTPS/WSS remains preferred
- native TLS requires an exact HTTPS public origin, rejects mismatched
SNI/Host/Origin, atomically rotates only a fully valid certificate/key
pair, and retains the last good context on reload failure
- forwarded authority is never inferred: RFC
Forwarded is
rejected, and exact
X-Forwarded-Host/Proto/Port
evidence is accepted only from loopback after explicit trust
- HTTPS public origins emit HSTS and use a
Secure
__Host- browser cookie; explicit non-loopback plaintext
uses no HSTS, keeps the ordinary HttpOnly SameSite cookie, and never
relaxes authorization or resource bounds
- certificate and private-key bytes are bounded, never enter
YAML/argv/Nix store/status/logs, and file targets are owner-controlled
with owner-only private-key mode
- content-free
/dash/healthz and fresh-backend
/dash/readyz still enforce Host/proxy authority and reveal
no session, credential, path, certificate, or backend state; readiness
is expressed only as 204/503
- packaged Dash assets are hash-named and regular/non-writable, with
traversal/symlink rejection and a deny-by-default CSP
- Dash workspace/settings files are owner-only, atomic, bounded,
revision/ETag checked, and UI overlays cannot mutate service
authority
- lazy Dash session drafts use a separate owner-private atomic store;
create/get/cancel and owner-default resolution perform no
runtime/model/tool work, private first-message content never enters
browser resources, and prompt-submitting crash/cancel races become
indeterminate rather than replayed
- optional New Session defaults expose only a canonical cwd, effective
model/thinking and visible policy flags; Pi settings paths/package
values and runtime-policy resource paths remain server-side, and
elevated drafts are checked against owner policy both on create and
restart-time materialization
- administrator diagnostics expose only a 128-event in-memory ring of
fixed policy booleans/counts and normalized route/error codes; raw logs,
identifier-bearing paths, prompts, model output, request bodies,
credentials, bearer material, environment values, and arbitrary files
are never exposed
- shadow-TUI frames come from a bounded in-process cell grid; raw
ANSI, OSC 52 clipboard access, image/device payloads, unsafe links,
terminal queries, and unsupported controls never reach the browser
- declarative extension views are validated and normalized before
replay/browser delivery; only inert allowlisted nodes, opaque
authorized-blob image references, exact view-scoped actions, and bounded
string/boolean form responses are accepted—never extension JavaScript,
HTML, CSS, callbacks, ambient links, or arbitrary fetches
- full session-tree reads are count/depth/text bounded and
observer-safe; fork/clone/navigate/summarize require the exact channel
controller and generation, while the private in-place navigation result
is correlated, non-replayed, capacity-limited, and scheduled with other
model work
- a TUI view shares one resident runtime, extension instance, and
JSONL writer; a child Pi/PTY is not a supported rendering path
Prompts and terminal results are necessarily retained in the private
durable request journal so a queued request can be replayed and a
duplicate terminal request can receive its prior result. They are not
emitted in status or logs. Protect the state directory as sensitive
application data.
Authenticated clients may explicitly create an
unisolated configured session. The admission parser
separates a secret-free persisted spec and sorted environment-key
summary from the memory-only raw overlay. Raw values never enter the
catalog or journal; after restart an env-dependent operation fails
credentials_required until re-provisioned. The daemon never
swaps global process.env or cwd. Known provider API keys
use a session-scoped in-memory auth store and the bash overlay applies
only through a child-process spawn hook.
Automatic extensions, packages, skills, prompts, themes, and context
discovery remain disabled unless project trust is explicitly approved.
Explicit resource paths are themselves an authority grant.
Extension/package JavaScript still shares process memory, globals,
ambient daemon environment, and provider registries with every other
in-process session.
The additive protocol-v2 host
tool-adapter is a narrower alternative to loading project code in
the daemon. Its closed descriptor grants a subset of six fixed
filesystem operations through an owner-private Unix socket. Adapter
ID/version, host incarnation, logical session ID, and generation are
checked on every frame. The session cwd is the implicit root; paths are
root-relative and the runtime must fail closed on traversal, symlinks,
stale identity, oversized records, queue/time limits, and response
mismatch.
The opaque capability handle is memory-only and appears on the
private wire only in bind. It is never an HTTP/daemon
bearer, certificate, environment value, or persisted credential, and it
must not enter logs, errors, status, events, manifests, journals,
tickets, or acknowledgements. Restart changes host identity and requires
reprovisioning rather than replay. Per-invocation abort and
best-effort generation/session revoke prevent one stuck
tool from requiring a whole shared socket teardown. V2 configured opens
may select a per-session agentDir and
sessionDir, but only through the strict prepared-session
path; private auth mode/ownership, allowed cwd roots, canonical
<agentDir>/sessions confinement, and
credential/state/workload overlap checks remain mandatory. The exact
resolved Pi conversation identity is persisted and used for recovery
instead of regex/name search. V1 and every v2 tools: "none"
open retain no built-in or ambient project tools; configured storage
does not widen tool authority.
Separate inhabitants
Configured process/filesystem tools and reviewed extensions may share
one unisolated daemon only when all sessions are mutually
trusted. A workload requiring unreviewed project JavaScript or
shell-grade environment, filesystem, process, network, or credential
isolation must run in a separate Pi Daemon process/security domain until
a stronger advertised backend exists.
Report vulnerabilities privately to the repository owner; do not
include live credentials, prompts, output, or private paths in a public
issue.