# Timeout-safety audit — 2026-07-25

## Scope

Audited the live mitigation under `~/.pi/agent/extensions/subagent-timeout-safety`,
Pi `0.82.0`, and `pi-subagents` `0.35.1`; then created the standalone,
credential-free package in this directory.

## Verified facts

1. Pi's `tool_call` event permits in-place mutation of validated arguments before
   execution. Evidence: Pi `dist/core/extensions/types.d.ts:678-683` and
   `dist/core/agent-session.js:214-233`.
2. In `pi-subagents 0.35.1`, `timeoutMs` and `maxRuntimeMs` result in an
   elapsed deadline; the background runner's timeout callback sends `SIGTERM`
   and escalates to `SIGKILL`. Evidence:
   `src/runs/background/subagent-runner.ts:626-636,2496-2528,2569-2573`.
3. A turn budget can also become process termination. Evidence:
   `src/runs/background/subagent-runner.ts:648-663` and
   `src/runs/foreground/execution.ts:558-604`.
4. Custom-agent `timeoutMs`/`turnBudget` defaults are applied after the tool
   hook for single-agent runs. Evidence:
   `src/runs/foreground/subagent-executor.ts:1585-1602,3512-3528`.
5. The initial live extension only intercepted normal model-issued tool calls.
   Direct `pi-subagents` routes exist for slash commands, delegation, RPC,
   schedules, and fanout children. Evidence:
   `src/extension/index.ts:274-285,363-411`,
   `src/slash/slash-bridge.ts:76-158`, `src/slash/prompt-template-bridge.ts:93-224`,
   `src/extension/rpc.ts:188-206`, and `src/runs/background/scheduled-runs.ts:221-249,466-484`.

## Findings and disposition

### High — capability vs. prompt mismatch

The previous extension treated a writer as safe for a hard deadline when task
text said `read-only` or used `acceptanceRole: read-only`. Those declarations
do not revoke `write`, `edit`, or `bash`; a model can still mutate.

**Disposition in standalone package:** classification is capability-based. A
limit is retained only for a resolved custom agent whose declared tools lack
mutation capability. All bundled roles and unknown/package agents fail closed.

### High — launch-path coverage was incomplete

A Pi `tool_call` hook does not observe direct executor bridges. These include
slash, prompt/delegation, RPC, scheduled fire, and fanout-child execution.

**Disposition:** documented as a real limitation. The standalone package keeps
its use of Pi's supported `tool_call` contract narrowly scoped; it does not
make unverified assumptions about event-listener ordering to claim coverage of
direct bridges. Scheduled jobs are protected when created through the model
`subagent` tool because their sanitized params are persisted.

### Medium — discovery did not exactly mirror `agentScope`

The earlier implementation always scanned both user and project directories.

**Disposition in standalone package:** local capability inventory now honors
`agentScope: user | project | both`, follows the nearest project root, and uses
Pi's relevant directory precedence. Package-provided agents remain unresolved
and therefore fail closed.

### Medium — dynamic fanout was not checkpointed

The earlier extension removed the shared limit but did not append its
checkpoint hint to a dynamic fanout template.

**Disposition in standalone package:** the template receives the checkpoint
hint before the plugin materializes concrete children.

### High — global `turnBudget` configuration remains outside this package

The extension removes a caller-provided `turnBudget`, but `pi-subagents` then
falls back to its extension configuration with
`effectiveParams.turnBudget ?? deps.config.turnBudget`. A non-empty global
`turnBudget` can therefore still terminate a mutation-capable run. The current
audited local config has no such field, but a package cannot enforce that
across all installations.

**Disposition:** documented as a residual configuration constraint. Do not set
a global `turnBudget` while mutation-capable agents are enabled; the upstream
executor needs a capability-aware checkpoint-first policy to close this gap.

### Medium — local agent inventory initially skipped nested definitions

`pi-subagents` discovers agent Markdown files recursively, while the first
standalone inventory scanned only direct children of each agent directory. A
nested read-only custom agent could therefore be classified as unknown and
conservatively have its limits removed. This was fail-closed but inconsistent
with Pi's actual discovery.

**Disposition in standalone package:** local discovery is now recursive,
honors `PI_SUBAGENT_EXTRA_AGENT_DIRS`, excludes chain files, preserves
deterministic lexical ordering, and includes scope coverage tests.

### Medium — backup portability issues

The configuration backup is deliberately broader than a plugin package and
contained global config behavior unrelated to timeout safety. Its snapshot
checksum manifest used source-machine absolute paths, and `restore.sh` is not
an atomic staging restore.

**Disposition:** this package is separate from the global backup, includes no
credentials or model settings, and does not depend on that restore mechanism.
Those backup-hardening issues remain independent follow-up work.

## Verification performed

```text
npm run check
npm test
pi --offline --no-session --no-builtin-tools \
  --extension /home/xwzy/workspace/pi-subagent-timeout-safety \
  --tools '' --mode json -p 'Reply with exactly: package-load-ok'
```

Results: syntax checks passed; all 12 policy/extension tests passed; the local
package loader smoke test passed; source scan found no provider key, private
key, or live Pi credential/model/settings reference.

## Residual risk

No injection extension can make OS process termination universally safe.
Explicit `stop`, parent/session abort, provider failure, protocol limits, and
system failure still terminate work. The upstream fix remains a checkpoint-first
deadline policy at the `pi-subagents` executor/runner boundary.
