# Security model

Pi packages run with full system access. The subagent extension spawns child `pi`
processes that inherit the parent environment (including provider credentials)
and can use tools according to their capability profile. The separate command
`task` extension uses the current callable `bash` with the same system access.

## Managed shell command tasks

`task` is execution-capable, not read-only. It preserves Pi's current nested
`bash` validation, permission/input/result hooks, effective shell and command
prefix. Run declares `reachesTools: ["bash"]`; management reaches no new command.
An unavailable or denied command capability starts nothing. There is no raw
spawn fallback, independently constructed bash tool or model/Jev call. A shell
command can itself access the network, secrets or paid services, just as native
bash can; task management grants no sandbox or new permission.

`timeout_ms` limits waiting only. It does not replace native `bash.timeout` or
an independent policy/wrapper deadline. Foreground abort requests cancellation
before transfer; old-caller and wait aborts cannot cancel background ownership.
Only explicit cancel and orderly lifecycle cleanup abort the owned invocation.
Process-tree termination is delegated to bash, not inferred from a saved PID.
A wrapper that ignores cancellation stays `cancelling`/`cleanup_pending` until
it settles; detached descendants and process crashes retain the underlying
tool's cleanup limitations. Command tasks are not persistent daemons.

Logs are retained below the sanitized `sessionDir` at
`shell-tasks/<task-id>/output.log`, with an immutable session `owner` marker.
Creation is exclusive, with directory/file modes `0700`/`0600` where supported.
Session entries hold bounded versioned metadata, never caller-supplied paths;
restored reads derive paths from exact IDs, verify owner and reject symlinked or
foreign artifacts. Only live tool-provided full-output pointers feed capture;
there is no search for withheld temp output. A missing/truncated custom source
or capture error is reported, and available fragments can remain incomplete.
Logs may contain credentials or other private command output: do not upload or
share them, protect the storage root, and remove old logs manually when safe.
No automatic expiry is added. Same-user processes can still alter this storage.

Live management and completion messages belong to the current session branch.
Exit/reload/navigation requests cancellation with an 8-second bounded wait;
late callbacks cannot notify a new owner. Reloaded active history is
lost/uncontrolled, not resumed or independently signalled. Existing notification
settings apply, but subagent model/depth/budget/process-slot settings are not
command-task controls.

## What subagents can do

| Profile | Finalized tools | Writes? |
|---------|-----------------|---------|
| `explore` | All locally permitted active read-only tools | No project-file writes |
| `review` | Same as explore | No project-file writes |
| `general` | All active locally permitted ordinary tools after explicit-tool policy | Yes if the local set includes write-capable tools |

Parallel mode defaults to `explore` to avoid concurrent shared writes. Tool exposure follows Pi's official metadata: native non-direct definitions are carried for registration without being treated as ordinary profile candidates, while ordinary direct-tool safety remains fail-closed.

## Hard rules

1. **Read-only means no project-file mutation.** `bash` can rewrite the disk and is never part of
   an explore/review profile. The finalized tools reach the child as Pi's `--tools`
   allowlist (`--no-tools` only when the locally resolved ordinary/native set is empty).
   Local policy, not a selector tool answer, determines the ordinary set: active availability,
   profile restrictions and an explicit `tools` ceiling are applied before launch. Pi 0.86.0
   is the verified baseline for built-in, extension and late-registered tool enforcement;
   a host that cannot honor the allowlist is refused rather than silently weakened.
   Before the real task prompt, a package-local startup check verifies the routing bootstrap
   command source, exact selected model and bounded child capability evidence. The effective
   set is the intersection of the finalized candidate allowlist with observed ordinary
   activity and native registration; native definitions may be host-inactive. Unforced
   candidate omissions are recorded as diagnostics and do not abort, while a missing
   explicitly requested tool fails closed. Active names outside the finalized allowlist,
   malformed evidence or a provenance/model/nonce mismatch aborts as a capability diagnostic
   and is never fixed by widening tools or switching models.
2. **Parallel writers** require `isolation: "worktree"`, distinct `cwd` values,
   or an explicit `allow_shared_writes: true` opt-in.
3. **Depth is capped** (`maxDepth`, default 2). Nested children at the ceiling do
   not re-register the subagent tool. Depth is scheduling metadata — `bash` or an
   env-scrubbing wrapper can still invoke `pi` directly, so treat it as an
   accidental-recursion guard, not a security boundary.
   **Spawn allowlists** (`spawns:` in agent frontmatter, env `PI_SUBAGENT_SPAWNS`)
   refine that same guard: a child may be limited to named personas, or to none
   (tool not registered). Like depth, this is not a sandbox — children can still
   shell out to `pi`.
4. **Process caps** limit concurrency both per parent session (`maxActiveProcesses`)
   and machine-wide (`maxGlobalActive`, default 16).
5. **Transcripts** under `~/.pi/subagent-sessions` may contain task content, tool
   output, and secrets that appeared in context. Protect that directory. Task text
   is delivered via stdin (not argv) so it stays out of `ps` listings, but it is
   still written into the child session log.
6. **Background permission prompts** are limited because children run headless
   (RPC mode). Extension UI dialogs raised inside a child are auto-cancelled so
   they can never hang a run — which also means a child can never obtain
   interactive consent. Prefer restricted tools for async/background runs.
7. **Steering messages** (`action: "steer"` and the overlay `s` key) inject text
   into a running child's conversation with user-level authority. Anything that
   can call the subagent tool can steer any live run in the same session.
8. **Process cleanup.** On POSIX, children run in their own process group so tree
   kills work for ordinary descendants. Parent (re)start reaps orphans recorded
   under `~/.pi/subagent-locks/runs/` so resume cannot race a still-alive writer.
   Grandchildren that call `setsid()` can still escape a simple process-group kill.
9. **Resume exclusivity.** Direct resume takes a durable per-session file lock;
   concurrent parents cannot append to the same child session.
10. **Profiles are tool-selection policy, not a sandbox.** Children inherit
   `$HOME`, SSH/cloud credentials, network access, and the parent filesystem.
   Git worktrees only isolate the checkout. For untrusted tasks, use an outer
   container/cgroup/network policy.
11. **`max_cost` is accounting, not a hard provider gate.** Usage arrives after a
    turn; orphans may spend money the ledger never sees. It caps provider-reported
    execution cost only: TypeSafe reports routing tokens, not currency, so selector
    cost is unreported and outside `max_cost`. Combine with provider account
    budgets for hard spend limits.

## Official tool exposure boundary

Pi 0.99.0+ `getAllTools()` metadata is the only classification source for the native boundary. Active `direct` definitions in the same snapshot form the ordinary local capability set, including direct SDK/custom tools; local profile and explicit-tool policy decide which of that set reaches the child. `model-only`, `codemode` and `deferred` definitions are native managed tools: the parent carries their registered names automatically, child registration is observed as bounded capability evidence, and an unforced missing definition is omitted while Pi controls whether each is active. `hidden` definitions are excluded from both paths. If an older host omits `exposure`, the Pi default `direct` behavior is used; an unknown present exposure value is dropped rather than guessed.

`sourceInfo` is retained for provenance and nested-extension attestation only. `annotations`, names and extension source labels never prove that a tool is read-only. Ordinary unknown/custom direct tools remain writer-capable in `general` and are rejected by `explore`/`review` unless they are in the existing conservative read-only set. Native managed tools do not alter ordinary writer classification, and nested `subagent`/`subagent_wait` names remain subject to package depth/spawn policy.

The startup proof checks the exact model and negotiates the child-effective set from ordinary-active and native-registration evidence. A candidate absent from the child is recorded as `omittedTools` without aborting by default; an explicitly requested (`forcedTools`) name must be effective or startup fails closed. It rejects active names outside the finalized allowlist and retains source/model/nonce/host checks plus nested-tool provenance checks. No user-owned whitelist, source-name preset, forced activation beyond the explicit request contract, foreign config discovery or annotation-based sandbox exists.

## Routing disclosure and credentials

Jev routing sends a minimal projection to TypeSafe: the current delegated task
text, the configured candidate model IDs and your per-model descriptions, and the
necessary constraints (profile, the resolved thinking level, whether structured output
is needed). It does not send tool names or descriptions, upload repository files,
conversation history, full system prompts, persona text or tool parameter schemas, and
does not read them in the background. Resume, fork and synthesis select from the new
task instruction rather than the assembled transcript. Task text and model descriptions
are user content and can themselves contain secrets; there is no guaranteed redaction.

Version `0.10.0` reads the TypeSafe credential from `jevRouting.apiKey` in the private user-level `~/.pi/subagent.json`. This is plaintext storage: restrict file access and protect editor backups and synchronized copies. Same-user processes, including children with filesystem access, may read it. Profiles and worktrees do not protect this file from those processes.

The transport sends the key only as an `Authorization` header to the normalized per-invocation `jevRouting.baseUrl` destination, with `redirect: "error"`. When the field is omitted, the exact official HTTPS endpoint remains the default. A configured destination must be a complete absolute HTTPS URL with a hostname, at most 2048 characters, and no username/password, query, fragment, whitespace or control characters; HTTP and other schemes are rejected before any selector request. A custom destination intentionally changes which service receives the minimal task/model routing disclosure, so it must be trusted accordingly. The URL is configuration-only and is not copied into prompts, selector JSON bodies, argv, child manifests, logs, receipts or results. The key itself is never copied into prompts, selector JSON bodies, argv, child manifests, logs, receipts or results. Never serialize or log the complete routing configuration. Rotate any credential pasted into a transcript or shared in conversation.

Published npm `0.9.0` uses the older environment-based mechanism. In `0.10.0`, `apiKeyEnv` is rejected with manual migration guidance and no environment fallback. Unrelated `PI_SUBAGENT_*` runtime settings remain supported.

## Trust and project cwd

If `cwd` points outside the parent project, the child inherits whatever local
project config/trust applies to that path. Treat external `cwd` as elevated risk
and prefer read-only profiles when exploring third-party trees.

## Output artifacts

`output` files are written by the orchestrator from the delivered result, not
by the worker process that produced it (for extension-managed runs, the parent
process on the dispatching side writes them). Resolve paths carefully and reject
duplicate output paths across parallel workers.

## Delegated testing boundary

Every Pi child launched through this package is always given the same package-owned mandatory instruction in its appended system prompt, composed after any persona/caller text and before the structured-output contract; no supported request field, config value or persona removes it, and an anonymous task receives it too.

The instruction forbids running tests or test commands, ad-hoc smoke/reproduction/assertion execution, creating or modifying temporary or permanent test code/scripts/fixtures/harnesses, and evading the rule through inline or in-memory execution, temporary directories, indirect shell wrappers or delegation to another subagent. Children may implement production/source/documentation changes and statically review source, diffs and existing tests. Executable testing belongs only to the TOP-LEVEL main agent, never to a delegated child: a child that delegates further is still a child, so an intermediate `subagent` parent owns no tests. When a task asks a child to test, the child reports that and states what remains untested instead of claiming results.

Logs and backups remain allowed within the existing task scope, capability profile and normal credential/privacy rules; the instruction neither requires nor creates them and grants no tool or permission. Package-owned automatic startup capability checks, sessions, locks, worktrees and parent-written result files are unaffected runtime mechanics, not delegated testing.

This is instruction inclusion only. It does not constrain the operating system, does not disable `bash`, native or custom tools, and cannot prevent a noncompliant model or extension from executing tests; profiles and worktrees remain the existing non-sandbox tool-selection policy. It reaches launches made after the updated package code is loaded. Already-running children and independently launched or custom-backend processes are outside it. Reload or restart Pi before relying on it for new dispatches.

## Named agent files

Agent files (`.pi/agents/`, `.agents/agents/`, global agent dir) inject their
body into the child's system prompt and set persona, thinking and budget
defaults. Model selection comes from Jev routing; local active/profile/explicit-tool policy resolves the complete ordinary tool set, while Pi native tool activity remains host-owned. A legacy
`model`/`fallback_models` in frontmatter is ignored. A
project-level agent file shapes subagent behavior the same way project
extensions and skills do — review them like code when working in untrusted
repositories. Mitigations: capability profiles still fail closed (an agent
cannot grant write tools under `explore`/`review`), symlinked agent files are
skipped, names are validated against traversal characters, and files over
64KB are ignored.

## Machine-wide state

`~/.pi/subagent-locks/` holds session locks, global concurrency slots, and run
process identity records. It is per-user (under `$HOME`) and must not be shared
across untrusted users/containers without care — a compromised client could
interfere with lock reclaim on the same account.
