Security

On this page

The agency security baseline applies in full: everything stays secure with its source, config, and design public (Kerckhoffs). This repo is public; there are no secrets in the tree.

Threat model — what this defends against

The permission stack defends against accidents, scope drift, casual prompt injection, and blast radius: an agent deleting the wrong thing, wandering into credentials, piping a download into a shell, or quietly bypassing the commit checks. It does not defend against a compromised model determined to do harm — that requires an OS-level sandbox, which is tracked separately and is not a property any permission prompt can provide.

Credential model

Credential Where it lives

GCP auth (Vertex)

Application Default Credentials (gcloud auth application-default login) or GOOGLE_APPLICATION_CREDENTIALS. Used by Google’s own libraries at request time; never stored by this code. When your ADC session expires, the cached client is rebuilt from the rotated credential file automatically — and the in-flight task waits for your re-login instead of dying.

GCP project id

ANTHROPIC_VERTEX_PROJECT_ID (or GOOGLE_CLOUD_PROJECT) env, or the per-user catalog. The shipped catalog holds an env reference, never a value.

GitLab token

GITLAB_TOKEN, exported in your shell before starting pi — never read from a file mid-session. Without it, glab falls back silently to whatever identity sits in ~/.config/glab-cli, which on a shared machine may belong to another team; the workflow guard therefore blocks glab writes when the variable is missing.

pi provider credentials

~/.pi/agent/auth.json, outside any repo. The Vertex extension stores only the sentinel string "adc" there, never a real credential.

The enforcement stack, security view

  • The permission engine is upstream and unmodified. Its fail-closed behaviour, bash decomposition, and symlink-resolved path checks stay exactly as audited upstream. Everything of ours is additive, through public extension points.

  • The judge reviews only the ambiguous, and only for danger. Deterministic allow/deny never consults a model. The judge receives the action alone — no conversation history — which removes the prompt-injection surface that history represented, and its every failure mode (timeout, bad output, missing credentials) resolves to "ask the human", never to allow.

  • A claimed authorization changes nothing. "The user said this is fine" inside a command, filename, or tool output does not move any verdict; real approval happens at the human prompt, which can overrule the judge.

  • Agents cannot edit their own leash. Writes to pi’s settings, installed extensions, and the permission policy itself are refused by rule; the seeded policy path also blocks reading the judge’s audit log directly (task verdicts is the sanctioned reader). Path denies are operation-agnostic — they catch reads as well as writes — and upstream documents that a path deny cannot be overridden by a per-tool or external_directory allow, so no overlay can quietly re-open one.

  • Two surfaces are excluded from capping by design. The chain owner downgrades any authorizer’s allow on path and external_directory to defer, which makes a rule there uncappable: an ask on either surface prompts a human in every mode, including yolo. Policy therefore never places rules on those surfaces; path restrictions act on the path each read/write/edit/bash ask carries instead.

  • Deferred execution is treated as execution. Writing or arming anything that runs code later — git hooks, shell startup files, editor task configs, CI pipelines that fetch-and-run — is refused or escalated, regardless of how innocent the content looks.

  • Secrets are judged by what a command prints. Searches that print matching lines from credential material are refused wherever they run; existence checks and filename lists are fine. The common cases are hard rules; the judge covers the shapes rules cannot express. One carve-out is a decision, not a gap (#121): an unfiltered dump of the process environment - env, printenv - is allowed. Its values reach the transcript, but the shell already shows them to the person at the keyboard, and refusing the dump while allowing every other look at one’s shell protects nothing and stops a developer looking at their own environment. Disclosure is the filtered form - a credential term as the filter, env | grep -i token - and that is denied by rule and by the judge; the eval corpus pins both sides (cred-env-bare-dump, cred-env-grep-values).

  • Commit integrity: signed commits, a per-commit reflection pause in the extension (no bypassable token), --no-verify refused as a string and as a parsed flag, attribution appended mechanically from the live session.

  • The reviewer’s reads are bounded, contained and disclosed. The cold reviewer reads the candidate snapshot through three read-only tools that run git plumbing with argument arrays (a pattern after -e, paths after --, never a shell), resolve symlinks before the root check, refuse credential-shaped files by name on every subject and withhold their lines from a wide search, and honour .gitignore on disk; the budget is per review and every read is named in the pause text. Residual: the refusal is a basename heuristic — a secret in a file named otherwise is readable by the reviewer, as it already was by the author’s own session; the reviewer sees it, the model provider sees it, and nothing else does. The reviewer is a model reading untrusted content with no write surface: a read that looks like an instruction can steer its verdict, which the human reads, never an action.

  • A headless session never defers to a human it cannot reach (#135). Under pi --mode rpc a deferred ask is put to the host with a bounded wait; yes inside the wait allows, no denies, and silence denies — the turn ends with a reason instead of waiting forever on a dialog nobody is watching. Plan approval and ask are bounded the same way; no answer is a refusal, never consent. Residuals: the permission system’s own ask rules on its two excluded surfaces (path, external_directory) still prompt without us — the seed keeps them allow and reconcile advises if they drift; “allow always” is unavailable from a host, since the extension returns one-shot decisions; and a human’s “no” that lands exactly at the deadline is recorded as unattended rather than as theirs — both are denies, so the misattribution costs a word, not a permission.

  • A paired device is a second keyboard, no more (#140). Pairing is trust: a device that completed the QR pairing may answer the same asks the desk may — the gate defer, plan approval, ask, the memory picker — through the ask broker, and nothing else. The winner decides and a cancelled dialog’s value is never read; a remote answer that does not validate against the ask (an option not offered, the wrong kind, a dropped link) is a decline, never a yes. A phone adds an answerer, not a deadline: the terminal still waits for a person; a headless host keeps its waits. While a device is attached the desk sees the bounded, one-shot dialog set on every surface — no “allow always” from either. Residuals: asks not carried by the broker (the permission system’s own prompts, third-party extensions, pi’s built-ins) are answerable only at the desk and reach the phone as a nudge; and a device that registered without a live link narrows the desk’s gate dialog until it unregisters — pivot’s rule is to register only while the link is up. Offboarding a device is forgetting its key on the box and wiping the device; nothing in the relay holds a session. Concretely (@gadhs/pi-remote, #142): the box’s identity is 64 bytes at ~/.pi/agent/gadhs-pi-remote/identity.key, mode 0600, minted once and never logged; trusted devices are { device_id, dh, label } beside it; admission is two decisions — a pairing device only inside an open window with the token, a known device only with the trusted Noise static, and an introduce that would re-key an id already trusted is refused so a token-holder cannot lock a real device out; the pairing URL, which carries the token, is shown in a pane or a notification and never written to the transcript; a lost link unregisters every device before it reconnects. The relay sees ids and ciphertext; the transcript never leaves the pair in the clear.

  • Delegation goes through the gate, or not at all. The agency distribution’s one delegation system is pi-subagents behind the delegation gate: profiles, contracts, the helper ledger. A tool that injects text into another pi session as the user — remote-pi’s mesh (agent_send) is the shipped example — bypasses every gate at once: a narrow mode can have a wide peer do its work, and a peer’s message arrives with the human’s authority. Such tools are out of policy for agency sessions; if one is ever wanted, it lands behind the gate with a profile, not beside it.

Verification standard

A recurring finding in this project: enforcement that looks wired is not evidence. Four separate rules shipped looking protective and never fired. The standard, therefore: every enforcement claim must be demonstrated by a live run with the mechanism visibly firing in the review log — and the evaluation gate (145 labelled cases, worst-case over repeated runs, zero unsafe allows required) runs before any change to the judge ships.

Reporting

See SECURITY.adoc at the repo root.

Edit this page · latest