# Child extension control and blocked-tool exclusion via two config fields (derived `childExtensions`, always-expanded `childBlockedTools`)

Children spawned by `delegate` inherit every globally installed extension (ADR-0005), and pi offers no env var or handler-level way for a child to suppress extension loading — only spawn-time flags: `--no-extensions` disables extension discovery entirely, and `-e <source>` re-adds a specific extension (documented combo: `pi --no-extensions -e ./my-extension.ts`). There is no per-extension exclusion flag: a child either loads all discovered extensions or, with `--no-extensions`, only the `-e` sources it is given. Separately, pi's `--exclude-tools` spawn flag (per the usage docs: a comma-separated denylist, not a repeatable flag) accepts concrete tool names only — its glob/`ext:` semantics are undocumented. That matters because some extensions misbehave in short-lived RPC children: pi-workspace-history's 60s cleanup deletes other sessions' shadow repos (`repo.git`) when more than three sessions share a workspace, and its cached validation never re-checks, so the parent TUI spams `fatal: not a git repository` banners on every `turn_end`/`agent_settled`. We decided on a two-field design: `childExtensions` carries derived semantics — non-empty spawns children with `--no-extensions` plus one `-e` per entry (selective loading), empty means no extension flags at all (inherit-all, ADR-0005 unchanged) — replacing the earlier `childNoExtensions` boolean, whose job emptiness of `childExtensions` now does. And `childBlockedTools` is always enforced two ways: every matcher (exact, glob, `ext:<id>`) is expanded parent-side to concrete tool names against the parent's tool registry at spawn time and unregistered via `--exclude-tools`, while the always-on ADR-0007 interception gate stays as backstop. Both fields are plumbed live from orchestrator.json through `DelegateDeps` (`getChildExtensions`, `getChildBlockedTools`).

## Considered options

- Handler-level suppression (rejected): impossible — pi has no env var or API for a child to refuse extensions; by the time any handler runs, discovery has already happened. The only lever is the spawn line.
- Patching third-party extensions (rejected): the workspace-history cleanup bug is upstream; patching or vendoring a third-party package from the orchestrator would break on every update and does not generalize to the next misbehaving extension.
- Keeping the `childNoExtensions` boolean (rejected): three knobs for two behaviors — the boolean is fully derivable from whether `childExtensions` is non-empty, and a separate switch invites nonsensical states (`childNoExtensions: true` with no entries, or `false` with entries that double-load). One list, one meaning.
- `--exclude-tools` behind an opt-in flag for exact names only (rejected): splitting matchers into "safe for the flag" and "gate-only" halves was arbitrary — the flag needs concrete names, so resolve every matcher to names instead of filtering which matchers qualify. The gate (ADR-0007) remains the universal backstop for all matcher types.
- Passing matchers directly to `--exclude-tools` (rejected): the flag's glob/`ext:` semantics are undocumented; only concrete-name behavior is documented, so the parent expands matchers itself and passes names only.

## Consequences

- Default unchanged: with an empty `childExtensions`, children keep inheriting every global extension exactly as before (ADR-0005).
- With a non-empty `childExtensions`, children load nothing by default — including pi-orchestrator itself — so the ADR-0007 tool gate, the child-side orphan watchdog, and the gate's fail-soft enforcement are absent unless the package is re-added via `childExtensions` (e.g. `["npm:@saifymatteo/pi-orchestrator"]`). The parent-written `# Tool policy` system-prompt hint still ships, and parent-side reaping (turn budget, stall watchdog, session_shutdown/abort kills) still applies.
- Expansion is bounded by the parent's registry: glob and `ext:<id>` matchers are resolved against `pi.getAllTools()` at spawn time, so a child that loads different extensions (e.g. via a per-task `cwd` with extra project extensions) may register tools the parent never saw and therefore never excluded. The interception gate (ADR-0007) backstops those — it matches on the child side, where the tool actually exists.
- Matchers that expand to nothing are skipped silently: a matcher naming an absent package or a typo excludes nothing at spawn, and only the gate would ever see a match (it also matches nothing). No error is surfaced — the block list is a policy, not a validation of the installed set.
- Deduplicated concrete names in first-seen order feed a single `--exclude-tools` occurrence (comma-separated denylist), and exact-name matching keeps its case-insensitive semantics consistent with the gate.
- Version floor: `--exclude-tools` requires pi >= 0.84.0, which package.json already enforces, so the spawn-time unregistration is not version-gated at runtime; the gate remains as backstop regardless. (Superseded by ADR-0017, which raises the floor to pi >= 1.0.4 for pattern pass-through.)
- Document-only stance on the workspace-history bug: users isolate children via a non-empty `childExtensions` (re-adding this package to keep the gate/watchdog alive) or pin a fixed upstream version until the cleanup behavior is fixed upstream; no workaround is attempted in this extension.
- Later refinement (ADR-0017): pi >= 1.0.4 documents `*` patterns for `--exclude-tools`, so pi-accepted matchers (exact, `*` glob) now pass through verbatim — the child's own registry is filtered, closing the parent-registry-bound gap above for those matcher kinds — while expansion is retained for case normalization and for `ext:<id>`/`?` matchers; the gate remains the universal backstop and the peer floor moved to pi >= 1.0.4.
