pi-background-tasks 2.1.3 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
  2. package/PUBLISHING.md +2 -0
  3. package/README.md +16 -9
  4. package/TESTING.md +15 -10
  5. package/TEST_PLAN.md +12 -11
  6. package/THIRD_PARTY_NOTICES.md +30 -0
  7. package/docs/INDEX.md +8 -4
  8. package/docs/choose-a-workflow.md +5 -2
  9. package/docs/commands/claude-cache.md +50 -0
  10. package/docs/getting-started.md +3 -0
  11. package/docs/manifest.json +84 -19
  12. package/docs/operations/configuration.md +15 -1
  13. package/docs/operations/releasing.md +6 -3
  14. package/docs/read-before-edit.md +4 -1
  15. package/docs/reference/runtime-contracts.md +72 -71
  16. package/docs/subsystems/anthropic-attribution.md +63 -0
  17. package/docs/subsystems/attested-pi-runs.md +2 -2
  18. package/docs/subsystems/child-launch-durability-and-safety.md +3 -3
  19. package/docs/subsystems/delegation.md +12 -4
  20. package/docs/subsystems/docs-freshness-gate.md +6 -6
  21. package/docs/subsystems/fusion.md +6 -2
  22. package/docs/tools/bg_delegate.md +21 -9
  23. package/docs/tools/bg_result.md +8 -1
  24. package/docs/tools/bg_run.md +5 -0
  25. package/extensions/anthropic-attribution.ts +1 -0
  26. package/package.json +4 -2
  27. package/src/core/anthropic-attribution-path.ts +26 -0
  28. package/src/core/{fusion/anthropic-attribution.ts → anthropic-attribution.ts} +61 -8
  29. package/src/core/attested-pi-run.ts +10 -1
  30. package/src/core/common.ts +2 -1
  31. package/src/core/delegate/artifacts.ts +4 -0
  32. package/src/core/delegate/launch.ts +44 -14
  33. package/src/core/delegate/runner.ts +24 -0
  34. package/src/core/delegate/seed.ts +12 -0
  35. package/src/core/delegate/types.ts +10 -2
  36. package/src/core/fusion/artifacts.ts +265 -3
  37. package/src/core/fusion/config.ts +1 -1
  38. package/src/core/fusion/orchestrator.ts +47 -57
  39. package/src/core/fusion/pi-child.ts +7 -124
  40. package/src/core/fusion/result-package.ts +550 -3
  41. package/src/core/fusion/types.ts +87 -0
  42. package/src/core/registry.ts +4 -1
  43. package/src/delegate-child-extension.ts +9 -2
  44. package/src/delegate-extension.ts +119 -9
@@ -40,9 +40,9 @@ The logical argv always begins:
40
40
  pi --mode json --provider <provider> --model <model>
41
41
  ```
42
42
 
43
- Then optional `--thinking <thinking>`, then literal `extraPiArgs`, then the prompt as the final user prompt argument. Forbidden extra args are direct auth (`--api-key`, `--auth-file`), mode/print (`-p`, `--print`, `--mode`), and duplicate structured fields (`--provider`, `--model`, `--thinking`).
43
+ For an Anthropic request, the package then adds `--extension <package-owned-anthropic-attribution>` before optional thinking. Next come optional `--thinking <thinking>`, literal `extraPiArgs`, and the prompt as the final user prompt argument. Forbidden extra args are direct auth (`--api-key`, `--auth-file`), mode/print (`-p`, `--print`, `--mode`), and duplicate structured fields (`--provider`, `--model`, `--thinking`). Missing attribution bytes refuse an Anthropic launch before task creation.
44
44
 
45
- The registry launches exactly one child through the resolved Pi executable with `shell:false`. The attestation records the stable logical argv (`['pi', ...]`), not platform-specific Windows Node/CLI shims. Attested tasks are created with generic background completion notification/wake disabled; terminal snapshots are still published through the task system.
45
+ The registry launches exactly one child through the resolved Pi executable with `shell:false`. The attestation records the stable logical argv (`['pi', ...]`), including any package-owned attribution extension, not platform-specific Windows Node/CLI shims. Attested tasks are created with generic background completion notification/wake disabled; terminal snapshots are still published through the task system.
46
46
 
47
47
  ## Auth and environment boundary
48
48
 
@@ -46,9 +46,9 @@ After atomic replace, POSIX-like platforms open and sync the parent directory to
46
46
  ## Process trust boundaries
47
47
 
48
48
  - Background shell tasks run the operator-provided shell command in the project cwd and are not sandboxed.
49
- - Delegate children are direct `pi` spawns, not shell commands. They use a task-owned session id and session dir, stripped parent session environment, disabled discovery, and an explicit child guard extension.
50
- - Fusion children are direct `pi --mode text` spawns with private metadata/tool-call audit extensions and workflow-specific tool policy.
51
- - Attested Pi tasks are direct `pi --mode json` spawns and produce evidence sidecars after successful parsing and durability.
49
+ - Delegate children are direct `pi` spawns, not shell commands. They use a task-owned session id and session dir, stripped parent session environment, disabled discovery, and an explicit child guard extension; Anthropic delegates first load the package attribution extension.
50
+ - Fusion children are direct `pi --mode text` spawns with private metadata/tool-call audit extensions and workflow-specific tool policy; Anthropic children first load the package attribution extension.
51
+ - Attested Pi tasks are direct `pi --mode json` spawns and produce evidence sidecars after successful parsing and durability; Anthropic tasks receive the package attribution extension explicitly.
52
52
 
53
53
  Never blur parent and child authority: parent tools can start/inspect/kill tasks, but child tools must stay within their explicit argv tool set.
54
54
 
@@ -32,7 +32,7 @@ The design deliberately separates:
32
32
 
33
33
  ## Seed and context policy
34
34
 
35
- The seed schema is `pi-background-tasks.delegate-seed.v1`. It wraps the frozen `visible-conversation-ledger-v2` projection under delegate policy id `delegate-inspect-v1`; it never emits Fusion input schemas or claims Fusion provenance.
35
+ The seed schema is `pi-background-tasks.delegate-seed.v2`. It wraps the frozen `visible-conversation-ledger-v2` projection under delegate policy id `delegate-inspect-v1`; it never emits Fusion input schemas or claims Fusion provenance. The exact selected `extension_mode` is hash-bound into the seed, and is also recorded in launch details, task facts, and `manifest.json`.
36
36
 
37
37
  Projection behavior:
38
38
 
@@ -62,16 +62,22 @@ The child launch:
62
62
  - separate random `--session-id`;
63
63
  - task-owned `--session-dir` under the artifact directory;
64
64
  - parent session/provider/model/reasoning env keys stripped;
65
- - only package-owned child guard extension explicitly loaded;
66
- - ambient extension/skill/template/theme/context discovery disabled.
65
+ - skill/template/theme/context discovery always disabled;
66
+ - extension discovery disabled by default in `extensionMode:"isolated"`;
67
+ - extension discovery deliberately enabled only by `extensionMode:"ambient"`;
68
+ - non-Anthropic children explicitly load the package-owned child guard in both modes;
69
+ - Anthropic children explicitly load package attribution/sanitization first, then the child guard, in both modes.
67
70
 
68
- The only v1 capability is `inspect`. Allowed tools are exactly `read`, `grep`, `find`, `ls`, and `delegate_read_artifact`; forbidden tools deny shell, writes, background task controls, recursive delegation, attested Pi launch, and Fusion. The boundary is argv/tool-registry enforced.
71
+ Ambient mode exists for providers registered by user/project Pi extensions. It omits only `--no-extensions`; it accepts no caller-supplied extension paths and performs no provider fallback or route substitution. Ambient discovery executes arbitrary trusted-location extension code in the child process. That code has Node process privileges and is not sandboxed by Pi's model-visible tool allowlist, so ambient mode deliberately weakens the inspect-only process-isolation guarantee. It must not be described as safe or equivalent to isolated mode.
72
+
73
+ The only v1 capability is `inspect`. Allowed model-visible tools are exactly `read`, `grep`, `find`, `ls`, and `delegate_read_artifact`; forbidden tools deny shell, writes, background task controls, recursive delegation, attested Pi launch, and Fusion. This tool boundary remains argv/tool-registry enforced in both extension modes, but it is not a sandbox for ambient extension initialization or handlers. Missing Anthropic attribution bytes are a pre-artifact `delegate_isolation_unsupported` refusal; no un-attributed child or alternate route is launched.
69
74
 
70
75
  ## Route and budget
71
76
 
72
77
  Routes are pinned once:
73
78
 
74
79
  - omitted route → parent current model;
80
+ - extension-only routes still must be visible in the parent registry and require explicit `extensionMode:"ambient"` so the fresh child can load their implementing extension;
75
81
  - explicit route → exact registry entry;
76
82
  - unavailable/unknown-capacity routes fail;
77
83
  - no substitution, fallback, or retry on a different route.
@@ -130,6 +136,8 @@ Default delivery inlines answers up to `48 KiB`; larger answers return artifact
130
136
 
131
137
  Current `autoDeliver` status: `bg_delegate` accepts and records `never | when_small | always` and includes it in launch facts/details. The registry's generic terminal notification currently does not evaluate delegate results or include answer text, so `bg_result` remains the retrieval path.
132
138
 
139
+ `extensionMode` accepts only `isolated | ambient`, defaults to `isolated`, and is surfaced in receipt text and durable metadata. Ambient receipts include an explicit arbitrary-code/isolation warning.
140
+
133
141
  ## User-oriented failure taxonomy
134
142
 
135
143
  Admission / no child:
@@ -12,14 +12,14 @@ covers_sources: []
12
12
  This authored section defines the boundary: documentation facts are extracted from package metadata and TypeScript ASTs, then generated into docs and the manifest. Unsupported syntax fails the gate rather than falling back to regex or stale hand-maintained inventories. Public registrations must remain unconditional top-level direct calls or use the one validated local tool-wrapper shape; host/method aliases, computed access, nested or conditional registration, wrapper chaining/passing, constructor helpers, ambiguous public metadata, destructured Pi parameters, and repeated imported registrars are rejected.
13
13
 
14
14
  <!-- pi-docs:begin name="docs-freshness-gate" generator="scripts/docs/generate.mjs" -->
15
- - Canonical package version: `2.1.3`
16
- - Governed markdown docs: 40
17
- - Public surfaces extracted: 30
18
- - Governed production sources: 48
15
+ - Canonical package version: `2.3.0`
16
+ - Governed markdown docs: 42
17
+ - Public surfaces extracted: 31
18
+ - Governed production sources: 50
19
19
  - Tool contracts extracted: 11
20
- - Schema IDs extracted: 43
20
+ - Schema IDs extracted: 44
21
21
  - Environment variable references extracted: 49
22
- - Behavioral attestation receipts not passing: 4
22
+ - Behavioral attestation receipts not passing: 7
23
23
  - Receipt store: `docs/attestations.json`
24
24
 
25
25
  `npm run docs:verify` is read-only: it renders generated files twice in memory and compares them with committed bytes. `npm run docs:generate` is the only docs writer.
@@ -5,7 +5,7 @@ mode: mixed
5
5
  review_policy: behavioral
6
6
  stability: stable
7
7
  covers_surfaces: [renderer:fusion-result, workflow:investigate, workflow:reason, workflow:research, workflow:validate]
8
- covers_sources: [extensions/fusion-child.ts, src/core/fusion/anthropic-attribution.ts, src/core/fusion/artifacts.ts, src/core/fusion/budget.ts, src/core/fusion/child-protocol.ts, src/core/fusion/claude-cache.ts, src/core/fusion/clean-context.ts, src/core/fusion/config.ts, src/core/fusion/context.ts, src/core/fusion/evaluation.ts, src/core/fusion/orchestrator.ts, src/core/fusion/output-contract.ts, src/core/fusion/pi-child.ts, src/core/fusion/prompts.ts, src/core/fusion/result-package.ts, src/core/fusion/source-policy.ts, src/core/fusion/types.ts, src/core/fusion/web-fetch.ts, src/core/fusion/workflows.ts, src/fusion-child-extension.ts, src/fusion-extension.ts, src/ui/fusion-model-selector.ts]
8
+ covers_sources: [extensions/fusion-child.ts, src/core/fusion/artifacts.ts, src/core/fusion/budget.ts, src/core/fusion/child-protocol.ts, src/core/fusion/claude-cache.ts, src/core/fusion/clean-context.ts, src/core/fusion/config.ts, src/core/fusion/context.ts, src/core/fusion/evaluation.ts, src/core/fusion/orchestrator.ts, src/core/fusion/output-contract.ts, src/core/fusion/pi-child.ts, src/core/fusion/prompts.ts, src/core/fusion/result-package.ts, src/core/fusion/source-policy.ts, src/core/fusion/types.ts, src/core/fusion/web-fetch.ts, src/core/fusion/workflows.ts, src/fusion-child-extension.ts, src/fusion-extension.ts, src/ui/fusion-model-selector.ts]
9
9
  ---
10
10
 
11
11
  # Fusion subsystem
@@ -88,7 +88,7 @@ Inspect/research candidates write sealed tool-call audit logs. The log contains
88
88
 
89
89
  ## Child process isolation
90
90
 
91
- Fusion never calls direct completion APIs. It launches direct child `pi --mode text` processes and writes the prompt over stdin. Child argv includes `--no-session`, `--no-extensions`, `--no-skills`, `--no-prompt-templates`, `--no-themes`, and `--no-context-files`; explicit extensions still load. Non-Anthropic children receive only the package-owned compact metadata/runtime-governor extension. Anthropic children receive, in fixed order, the package-owned Claude Code attribution provider, `@ravshansbox/pi-anthropic-sps`, and the runtime governor. The repo-local `spawn-anthropic-attribution` entrypoint re-exports that same package-owned implementation, so normal agent spawns and Fusion cannot drift into different OAuth/cache request shapes. Attribution adds the Claude Code OAuth session header, linked account/device/session metadata, model-policy beta headers, system identity, beta-resource transport, cache surfaces, and model-aware cache usage pricing. It reads `userID` and `oauthAccount.accountUuid` from `~/.claude.json` without writing the file and fails loudly when required attribution data is absent or malformed. The sanitizer then removes only known rejected prompt lines while preserving attribution and cache controls.
91
+ Fusion never calls direct completion APIs. It launches direct child `pi --mode text` processes and writes the prompt over stdin. Child argv includes `--no-session`, `--no-extensions`, `--no-skills`, `--no-prompt-templates`, `--no-themes`, and `--no-context-files`; explicit extensions still load. Non-Anthropic children receive only the package-owned compact metadata/runtime-governor extension. Anthropic children receive, in fixed order, the package-wide attribution/sanitization extension and the runtime governor. The same attribution implementation is globally loaded for ordinary package sessions; Fusion supplies its public extension entrypoint explicitly because ambient discovery is disabled. Attribution adds the Claude Code OAuth session header, linked account/device/session metadata, model-policy beta headers, system identity, beta-resource transport, cache surfaces, and model-aware cache usage pricing. It reads `userID` and `oauthAccount.accountUuid` from `~/.claude.json` without writing the file and fails loudly when required attribution data is absent or malformed. Its internal sanitizer removes all reviewed exact-match rejected prompt lines while preserving unrelated text and cache controls; no external sanitizer package is resolved.
92
92
 
93
93
  Child text mode writes the final full answer to stdout. The private child extension emits compact reasoning-free metadata frames to stderr for finalized assistant messages: provider/model, stop reason, text block byte counts and hashes, aggregate text hash, the complete Pi `Usage` object (including Anthropic `cacheWrite1h` and provider-reported reasoning subsets), and a closed cache-policy observation. It governs every final `before_provider_request` payload after attribution and sanitization. For Anthropic routes, the child environment defaults `PI_CACHE_RETENTION` to `long` before provider serialization, so the attribution/Pi adapter creates system, final-tool, and final-conversation breakpoints with `ttl: "1h"`; inherited `PI_CACHE_RETENTION=short|none|long` remains explicit, and call-level `cacheRetention="none"` still wins for compaction. The final governor validates and normalizes those upstream-selected breakpoints, falls back to short when model compatibility rejects long retention, preserves no-marker compaction payloads, enforces Anthropic's four-breakpoint ceiling, and appends the subscription prompt-caching-scope beta idempotently. Its `effective_retention` field describes the final payload, not provider acceptance. Provider usage is preserved verbatim: `cacheWrite1h > 0` proves a one-hour write, but zero is inconclusive on subscription OAuth. Live normal-spawn and exact Fusion-child controls each observed a unique cache read after 370 idle seconds despite `cacheWrite1h = 0`; therefore payload observations prove request intent and `cacheRead` proves reuse, while neither zero telemetry nor a six-minute hit alone proves the full one-hour lifetime. Malformed controls or policy values abort before transport. Non-Anthropic payloads and child environments remain unchanged apart from the governor's existing JSON normalization.
94
94
 
@@ -110,6 +110,10 @@ When a candidate's first complete `stop` response exceeds 48 KiB, the private ch
110
110
 
111
111
  Run artifacts are private local evidence under `.pi/fusion/<session-id>-<pid>/<run-id>/`. They include `manifest.json`, `canonical-input.json`, `budget-plan.json`, per-attempt prompts/events/stderr/responses, optional partial responses for failed attempts, optional tool-call logs/seals, `blind-candidates.json`, `evaluation.json`, `merged.md`, manifest-bound `result.json`, `error.json`, and workflow-specific context/source-policy artifacts. `bg_result` verifies manifest state, fixed artifact references, byte lengths, SHA-256 values, UTF-8, run/workflow identity, and result details before returning merged bytes.
112
112
 
113
+ Failed and cancelled stored runs additionally write the canonical `pi-background-tasks.fusion-failure-summary.v1` `failure-summary.json` after `error.json` and the terminal manifest transition. The manifest binds that summary's exact basename, byte length, and SHA-256; the summary never hashes `manifest.json`. It is evidence metadata, not an answer: it contains a closed no-answer assertion, bounded terminal-error metadata, durable progress and usage, capped attempt metadata, manifest-bound evidence refs, classifications, remediation identifiers, and explicit omission counts. It contains no candidate/evaluator/merger response text, partial response text, tool-result payload, or fetched content. Completion never writes this artifact.
114
+
115
+ Summary persistence is subordinate: Fusion writes terminal usage, error evidence, and the failed/cancelled manifest first, then attempts the summary exactly once from a fresh terminal manifest snapshot. A summary-write failure does not retry or suppress terminal publication; the failure channel records one bounded summary-unavailable note. A pre-store refusal has no summary, and an after-store/pre-registration refusal may leave its run directory without inventing a task index.
116
+
113
117
  Artifact writes use durable private temp-file/fsync/rename. Manifests enforce legal state transitions and record config, resolved models, fixed capabilities, context policy, tool policy, anonymous map, attempts, artifact refs, cumulative usage, and errors. Successful, failed, and cancelled observed attempts preserve complete Pi usage/cost components, including optional `cacheWrite1h` and `reasoning` subsets; same-session compression includes both provider turns in that one attempt's aggregate; public tool results clone the same `Usage` shape without counting either subset as additional tokens. Terminal failures enrich their stage-local cause from the durable manifest after usage persistence: candidate/evaluator/merger progress reports completed, failed, cancelled, and not-started child facts plus exact usage so far. A late evaluator or merger budget refusal never claims that no child anywhere in the run was created.
114
118
 
115
119
  For tool-enabled children, the private audit journal remains open across every low-level `agent_end`, because Pi may still retry, compact and retry, or process a queued continuation. Only terminal `agent_settled` can exclusively publish the complete hash/count/byte seal. Runtime-guard refusal latches process failure, makes that seal incomplete, and forces the result settlement to failed. The child emits one closed `pi-background-tasks.fusion-runtime-guard.v2` stderr frame for malformed provider payloads, malformed Claude cache policy, provider-request loops, or tool-call loops. The frame contains the refusal code, route, request/tool ordinals, bounded payload byte/hash evidence where applicable, and a bounded message; it never emits the payload itself. The parent validates this frame and reports typed `child_runtime_limit_exceeded`, `child_runtime_payload_invalid`, or `child_cache_policy_invalid` instead of accepting a later clean-looking result or reducing it to an unexplained exit code. Tool activity after finalization, duplicate settlement, pre-settlement shutdown, extension diagnostics, malformed/duplicate runtime-guard frames, and missing/failed/stale seals are fatal. This lifecycle requires Pi 0.81.1 or newer; older Pi lines do not expose the required terminal event and are not claimed as compatible.
@@ -11,14 +11,15 @@ covers_sources: []
11
11
 
12
12
  <!-- pi-docs:begin name="tool-contract-bg_delegate" generator="scripts/docs/generate.mjs" -->
13
13
  - Label: **Background Delegate**
14
- - Source: `src/delegate-extension.ts:292`
15
- - Description: Launch one background Pi agent seeded with a frozen projection of the current conversation, then return a launch receipt immediately. The child has its own session, a route pinned at launch that is never substituted, and read-only tools. Retrieve its verified answer with bg_result.
14
+ - Source: `src/delegate-extension.ts:340`
15
+ - Description: Launch one background Pi agent seeded with a frozen projection of the current conversation, then return a launch receipt immediately. The child has its own session, a route pinned at launch that is never substituted, and read-only tools. Extension discovery is isolated by default; ambient mode supports extension-registered providers but executes arbitrary discovered extension code. Retrieve its verified answer with bg_result.
16
16
  - Root schema: `object`; additionalProperties: `false`
17
17
 
18
18
  | Field | Required | Type | Description | Constraints |
19
19
  | --- | --- | --- | --- | --- |
20
20
  | `autoDeliver` | no | `string` | Whether the completion notification carries the answer: never \| when_small \| always. Default never; retrieve with bg_result. | |
21
21
  | `capability` | no | `string` | Capability profile. Only "inspect" (read/search/list, no shell, no writes, no network, no recursion) is supported. | |
22
+ | `extensionMode` | no | `string` | Extension discovery: isolated \| ambient. Default isolated. Ambient is for extension-registered providers and executes arbitrary discovered extension code, weakening process isolation. | |
22
23
  | `maxToolCalls` | no | `number` | Maximum tool calls. Default 120. | |
23
24
  | `maxTurns` | no | `number` | Maximum agent turns. Default 24. | |
24
25
  | `name` | yes | `string` | Short human-readable task name shown in the bg footer dock. Use 2-6 words. | |
@@ -46,6 +47,10 @@ covers_sources: []
46
47
  "description": "Capability profile. Only \"inspect\" (read/search/list, no shell, no writes, no network, no recursion) is supported.",
47
48
  "type": "string"
48
49
  },
50
+ "extensionMode": {
51
+ "description": "Extension discovery: isolated | ambient. Default isolated. Ambient is for extension-registered providers and executes arbitrary discovered extension code, weakening process isolation.",
52
+ "type": "string"
53
+ },
49
54
  "maxToolCalls": {
50
55
  "description": "Maximum tool calls. Default 120.",
51
56
  "type": "number"
@@ -118,6 +123,7 @@ Optional:
118
123
 
119
124
  - `route: {provider: string, model: string}` — exact route pin. If omitted, the parent session's current `ctx.model.provider` and `ctx.model.id` are used.
120
125
  - `capability: "inspect"` — default `"inspect"`; this is the only v1 capability.
126
+ - `extensionMode: "isolated" | "ambient"` — default `"isolated"`. Use `"ambient"` only when the pinned provider is implemented by an auto-discovered user/project extension. Ambient mode executes arbitrary discovered extension code and weakens process isolation.
121
127
  - `maxTurns: positive integer` — default `24`.
122
128
  - `maxToolCalls: positive integer` — default `120`.
123
129
  - `timeoutSeconds: positive integer` — default `1200`.
@@ -153,21 +159,27 @@ Route resolution is pin-only:
153
159
  - routes with no declared context window are refused before child creation;
154
160
  - the child records provider/model attestations for assistant messages, and a mismatch prevents a successful result commit.
155
161
 
156
- ## Inspect-only tool boundary
162
+ ## Inspect-only tool boundary and extension modes
157
163
 
158
- The v1 capability is enforced by child argv and Pi's tool registry, not merely by prompt text:
164
+ The v1 model-visible capability is enforced by child argv and Pi's tool registry, not merely by prompt text:
159
165
 
160
166
  - enabled tools: `read`, `grep`, `find`, `ls`, `delegate_read_artifact`;
161
167
  - `--no-builtin-tools` is used with the explicit allowlist;
162
168
  - forbidden tools include shell/write/background/delegate/Fusion surfaces (`bash`, `edit`, `write`, `bg_run`, `bg_delegate`, `bg_result`, `bg_run_pi_attested`, Fusion tools, etc.);
163
- - ambient discovery is disabled with `--no-extensions`, `--no-skills`, `--no-prompt-templates`, `--no-themes`, `--no-context-files`;
164
- - only the package-owned delegate child extension is loaded explicitly.
169
+ - skills, prompt templates, themes, and context files remain disabled in both extension modes;
170
+ - the package-owned delegate guard is loaded explicitly in both modes; Anthropic routes first load the package attribution/sanitization extension.
171
+
172
+ `extensionMode:"isolated"` adds `--no-extensions` and is the default. Use it for built-in providers and whenever ambient provider code is unnecessary.
173
+
174
+ `extensionMode:"ambient"` omits only `--no-extensions`, allowing Pi to discover trusted-location user/project extensions so a fresh child can resolve an extension-registered provider. It does **not** accept extension paths from the tool call, alter the pinned route, or provide fallback/substitution.
175
+
176
+ Ambient extensions execute arbitrary code in the child process with Node privileges. The read-only tool allowlist constrains tools exposed to the model; it does not sandbox extension initialization or event handlers. Therefore ambient mode weakens the inspect-only process-isolation guarantee even though the model-visible tool registry remains inspect-only.
165
177
 
166
- There is no shell, edit/write, network tool, recursive delegation, Fusion, or ambient project resource loading in the child tool set.
178
+ There is no shell, edit/write, network tool, recursive delegation, or Fusion in the child tool set. That claim applies to registered model tools, not to arbitrary code loaded by ambient extensions.
167
179
 
168
180
  ## Admission, budgets, and artifacts
169
181
 
170
- Public admission resolves the route and package-owned child guard extension before entering `preflightDelegateLaunch()`. Within that preflight, the hook contract is checked before capability/limit/seed/budget admission. Every refusal still occurs before child process, child session directory, or artifact root creation, leaving zero child processes and zero delegate artifacts; callers should not depend on a single absolute error-precedence order across route, guard-extension, and hook checks.
182
+ Public admission resolves the route, package-owned child guard, and—for Anthropic routes—the package attribution extension before entering `preflightDelegateLaunch()`. Within that preflight, the hook contract is checked before capability/limit/seed/budget admission. Every refusal still occurs before child process, child session directory, or artifact root creation, leaving zero child processes and zero delegate artifacts; callers should not depend on a single absolute error-precedence order across route, guard-extension, and hook checks.
171
183
 
172
184
  Budgets and limits:
173
185
 
@@ -190,4 +202,4 @@ Inside the child, `delegate_read_artifact({artifact, offset, length})` reads an
190
202
 
191
203
  ## Completion
192
204
 
193
- `bg_delegate` returns a receipt with task id, route, child session id, artifact dir, seed hash/size, budget source, limits, auto-deliver setting, and notification/wake settings. With default notification settings, the parent receives the generic durable `background-task-notification` after terminal state and may then call `bg_result`. Do not poll solely to wait.
205
+ `bg_delegate` returns a receipt with task id, route, child session id, artifact dir, seed hash/size, budget source, extension mode, limits, auto-deliver setting, and notification/wake settings. Ambient receipts include an explicit arbitrary-code/isolation warning. The mode is also hash-bound in `seed.json` and persisted in task facts and `manifest.json`. With default notification settings, the parent receives the generic durable `background-task-notification` after terminal state and may then call `bg_result`. Do not poll solely to wait.
@@ -11,7 +11,7 @@ covers_sources: []
11
11
 
12
12
  <!-- pi-docs:begin name="tool-contract-bg_result" generator="scripts/docs/generate.mjs" -->
13
13
  - Label: **Background Result**
14
- - Source: `src/delegate-extension.ts:453`
14
+ - Source: `src/delegate-extension.ts:515`
15
15
  - Description: Retrieve a hash-verified result from a bg_delegate or background Fusion task. Never blocks: a running task returns a typed not-ready result. Oversized answers are never truncated.
16
16
  - Root schema: `object`; additionalProperties: `false`
17
17
 
@@ -94,6 +94,12 @@ The returned text is decoded from the same aggregate buffer that was hashed. Cor
94
94
 
95
95
  A completed Fusion task is accepted only when `manifest.json` is terminal `completed`, its `result.json` and `merged.md` fixed references match, both files match manifest-bound byte lengths and SHA-256 values, run/workflow/artifact identity matches the task, result details carry the current schema, usage is complete, and merged bytes are well-formed UTF-8. The first successful retrieval attaches complete Fusion usage exactly once; later retrievals omit usage to prevent double-counting.
96
96
 
97
+ ## Fusion failed/cancelled terminal view
98
+
99
+ A failed or cancelled Fusion task returns successfully as an answer-free typed terminal view rather than exposing partial output or throwing a plain failure string. It always has `state:"failed" | "cancelled"`, `delivery:"none"`, and `answer:{present:false,reason:"run_did_not_commit"}`; a requested inline or artifact delivery cannot override this. It includes workflow/artifact location where known, bounded progress/failure/count metadata, and manifest-bound evidence references only. It never includes merged text, partial response text, answer bytes/hash, or delivered-answer usage, and it never claims Fusion usage for these views.
100
+
101
+ For current runs, `summary_status:"verified"` means `failure-summary.json` was manifest-bound and its exact bytes/hash, UTF-8, closed schema, identity/state, no-answer assertion, and surfaced evidence refs were checked. Referenced stage-output bodies are never read; refs are honestly manifest-bound rather than freshly rehashed. `legacy_manifest_only` describes a validated historical terminal manifest with no summary and never backfills it. `integrity_failed` exposes no summary-derived metadata; `unavailable` exposes no untrusted refs. Failure rendering is bounded to the diagnostics-scale 8 KiB budget by deterministically dropping whole optional rows with exact omission counts, never cutting strings.
102
+
97
103
  ## Inline/artifact delivery and no truncation
98
104
 
99
105
  `bg_result` never truncates an answer.
@@ -116,6 +122,7 @@ Common delegate retrieval outcomes:
116
122
  - `seed_hash_mismatch`, `answer_hash_mismatch`, `child_result_invalid`, `child_result_encoding_invalid` — integrity or encoding failure.
117
123
  - `artifact_read_failed`, `artifact_spill_failed`, `artifact_error` — artifact I/O failure.
118
124
  - `result_too_large_for_inline` — explicit inline request exceeded the inline cap.
125
+ - Fusion `summary_status:"integrity_failed"` — a terminal summary or its manifest binding failed verification; no summary metadata is trusted.
119
126
 
120
127
  Delegate errors include whether a child process was created, preserved artifact hints when known, and remediation text. Usage missing from the provider is reported as `unavailable`, not synthesized as zero. Fusion retrieval additionally fails on non-completed manifests, identity/schema drift, malformed usage/details, invalid UTF-8, or any manifest/result/merged hash or byte-length mismatch; failed/cancelled runs return their preserved terminal error rather than partial output.
121
128
 
@@ -97,6 +97,8 @@ Legacy argument preparation can derive a missing `name` from `description` or `c
97
97
 
98
98
  Use for long-running tests, builds, servers, watchers, sleeps, and child agent work. Do not use normal foreground shell tools for commands expected to outlive the current turn.
99
99
 
100
+ For an Anthropic child `pi`, keep normal extension discovery enabled. Do not pass `--no-extensions` unless the command also explicitly loads this package's `extensions/anthropic-attribution.ts` with `-e`/`--extension`.
101
+
100
102
  ## Defaults
101
103
 
102
104
  - `notifyOnCompletion`: `true`.
@@ -154,6 +156,8 @@ Creates `.pi/tasks/<session-id>-<pid>/<task-id>.output` and `.json`. If `isAgent
154
156
 
155
157
  The command runs through the platform shell and is not sandboxed. Use `isAgent:true` only to request Pi-agent telemetry wrapping; setting it does not make execution safer. Model-visible logs are bounded and point to the full output path.
156
158
 
159
+ `bg_run` does not parse or repair arbitrary child `pi` argv. An Anthropic command that uses `--no-extensions` without explicitly loading the attribution extension bypasses the package's attribution/sanitization contract and is unsupported.
160
+
157
161
  ## Related docs
158
162
 
159
163
  - [Completion delivery](../concepts/completion-delivery.md)
@@ -162,6 +166,7 @@ The command runs through the platform shell and is not sandboxed. Use `isAgent:t
162
166
  - [`bg_kill`](bg_kill.md)
163
167
  - [`/bg`](../commands/bg.md)
164
168
  - [Background task runtime](../subsystems/background-task-runtime.md)
169
+ - [Anthropic attribution](../subsystems/anthropic-attribution.md)
165
170
 
166
171
  ## Source ownership/reference
167
172
 
@@ -0,0 +1 @@
1
+ export { default } from '../src/core/anthropic-attribution.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-background-tasks",
3
- "version": "2.1.3",
3
+ "version": "2.3.0",
4
4
  "description": "Pi extension for durable background shell tasks, read-only delegated agents, local attested Pi runs, and fixed-purpose Fusion workflows through child Pi processes.",
5
5
  "type": "module",
6
6
  "license": "ISC",
@@ -42,6 +42,7 @@
42
42
  "TEST_PLAN.md",
43
43
  "PUBLISHING.md",
44
44
  "LICENSE",
45
+ "THIRD_PARTY_NOTICES.md",
45
46
  "docs/",
46
47
  "BACKGROUND-TASKS-INSTRUCTIONS.md",
47
48
  "logo.png"
@@ -65,6 +66,7 @@
65
66
  "lint": "cd ../.. && npm run quality:ts:lint",
66
67
  "format:check": "cd ../.. && npm run quality:ts:format",
67
68
  "test:compat": "tsx scripts/test-compat.ts",
69
+ "test:pnpm-pack": "node scripts/test-pnpm-pack-install.mjs",
68
70
  "test:hook-contract": "tsx --test --test-concurrency=1 tests/scripted-provider/pi-hook-contract.test.ts",
69
71
  "docs:generate": "node scripts/docs/generate.mjs",
70
72
  "docs:verify": "node scripts/docs/verify.mjs",
@@ -79,6 +81,7 @@
79
81
  "pi": {
80
82
  "image": "https://raw.githubusercontent.com/ismailsaleekh/pi-background-tasks/main/logo.png",
81
83
  "extensions": [
84
+ "./extensions/anthropic-attribution.ts",
82
85
  "./extensions/background-tasks.ts"
83
86
  ]
84
87
  },
@@ -101,7 +104,6 @@
101
104
  "node": ">=22.19.0"
102
105
  },
103
106
  "dependencies": {
104
- "@ravshansbox/pi-anthropic-sps": "https://codeload.github.com/ravshansbox/pi-anthropic-sps/tar.gz/17409b5615f0ec0625776bc5434f92f2c55e3fd0",
105
107
  "turndown": "7.2.4"
106
108
  }
107
109
  }
@@ -0,0 +1,26 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ /**
6
+ * Resolve the package-owned global Anthropic attribution extension.
7
+ *
8
+ * Package-owned child Pi processes disable ambient extension discovery, so they
9
+ * must explicitly load this public extension entrypoint. Keeping path resolution
10
+ * in one module prevents Fusion, delegation, and attested runs from deriving
11
+ * different package paths.
12
+ */
13
+ export function resolveAnthropicAttributionExtensionPath(
14
+ moduleUrl = import.meta.url,
15
+ pathExists: (path: string) => boolean = existsSync,
16
+ ): string {
17
+ const modulePath = fileURLToPath(moduleUrl);
18
+ const extension = modulePath.endsWith('.ts')
19
+ ? 'anthropic-attribution.ts'
20
+ : 'anthropic-attribution.js';
21
+ const candidate = resolve(dirname(modulePath), '../../extensions', extension);
22
+ if (!pathExists(candidate)) {
23
+ throw new Error(`Anthropic attribution extension is missing: ${candidate}`);
24
+ }
25
+ return candidate;
26
+ }
@@ -64,13 +64,17 @@ const AUDIT_ENV = 'PIPELINE_ANTHROPIC_ATTRIBUTION_AUDIT_PATH';
64
64
  const CACHE_RETENTION_ENV = 'PI_CACHE_RETENTION';
65
65
  export const ANTHROPIC_CACHE_RETENTION_ENTRY = 'pipeline-anthropic-cache-retention';
66
66
  const ANTHROPIC_CACHE_RETENTION_SCHEMA = 'pipeline.anthropic_cache_retention.v1';
67
+ export const ANTHROPIC_ATTRIBUTION_CLAIM_CHANNEL = 'pi-anthropic-attribution:claim:v1';
68
+ const ANTHROPIC_ATTRIBUTION_CLAIM_SCHEMA = 'pi-anthropic-attribution.claim.v1';
67
69
  const NATIVE_ATTESTATION_PLACEHOLDER = '00000';
68
70
  const ANTHROPIC_CACHE_CONTROL_BREAKPOINT_LIMIT = 4;
69
71
 
70
- // Source: ravshansbox/pi-anthropic-sps. Keep exact-match semantics so the
71
- // repo-local attribution shim is idempotent with the global sanitizer.
72
+ // Sanitization behavior derived from the MIT-licensed ravshansbox/pi-anthropic-sps
73
+ // extension at commit 17409b5615f0ec0625776bc5434f92f2c55e3fd0. Keep exact-match
74
+ // semantics and all known Pi prompt variants; unrelated system text is preserved.
72
75
  const ANTHROPIC_SYSTEM_PROMPT_BAD_LINES = new Set([
73
76
  '- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md)',
77
+ '- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md), environment variables (docs/environment-variables.md)',
74
78
  '- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing',
75
79
  ]);
76
80
 
@@ -329,7 +333,13 @@ interface PiCommandConfigLike {
329
333
  readonly handler: (args: string, ctx: PiContextLike) => Promise<void> | void;
330
334
  }
331
335
 
336
+ interface PiEventBusLike {
337
+ emit(channel: string, data: unknown): void;
338
+ on(channel: string, handler: (data: unknown) => void): () => void;
339
+ }
340
+
332
341
  export interface PiExtensionHost extends PiProviderRegistrationHost {
342
+ readonly events: PiEventBusLike;
333
343
  on(
334
344
  eventName: 'session_start' | 'session_shutdown' | 'session_tree' | 'before_agent_start',
335
345
  handler: (event: unknown, ctx: PiContextLike) => void,
@@ -1867,11 +1877,54 @@ function cacheRetentionLabel(retention: CacheRetention): string {
1867
1877
  }
1868
1878
  }
1869
1879
 
1880
+ interface AnthropicAttributionClaimProbe {
1881
+ readonly schema_version: typeof ANTHROPIC_ATTRIBUTION_CLAIM_SCHEMA;
1882
+ readonly acknowledge: () => void;
1883
+ }
1884
+
1885
+ function isAnthropicAttributionClaimProbe(value: unknown): value is AnthropicAttributionClaimProbe {
1886
+ return (
1887
+ isPlainObject(value) &&
1888
+ value['schema_version'] === ANTHROPIC_ATTRIBUTION_CLAIM_SCHEMA &&
1889
+ typeof value['acknowledge'] === 'function'
1890
+ );
1891
+ }
1892
+
1893
+ /**
1894
+ * Prevent two independently installed copies from registering duplicate provider
1895
+ * hooks and `/claude-cache` commands in one Pi runtime. Pi loads extension factories
1896
+ * sequentially and its EventBus dispatches listeners synchronously, so an existing
1897
+ * owner acknowledges this probe before emit() returns. The winning extension only
1898
+ * publishes ownership after every registration below succeeds; a factory that throws
1899
+ * cannot strand a false claim that suppresses a healthy later copy.
1900
+ */
1870
1901
  export default function spawnAnthropicAttribution(pi: PiExtensionHost): void {
1902
+ const acknowledgements: true[] = [];
1903
+ const probe: AnthropicAttributionClaimProbe = {
1904
+ schema_version: ANTHROPIC_ATTRIBUTION_CLAIM_SCHEMA,
1905
+ acknowledge: () => {
1906
+ acknowledgements.push(true);
1907
+ },
1908
+ };
1909
+ pi.events.emit(ANTHROPIC_ATTRIBUTION_CLAIM_CHANNEL, probe);
1910
+ if (acknowledgements.length > 0) return;
1911
+
1871
1912
  let sessionCacheRetention: Exclude<CacheRetention, 'none'> | undefined;
1872
1913
  const getSessionOverride = (): Exclude<CacheRetention, 'none'> | undefined =>
1873
1914
  sessionCacheRetention;
1874
1915
 
1916
+ // Registration is global but route-scoped by provider name. Keeping it at
1917
+ // factory scope avoids lifecycle-dependent provider availability; the custom
1918
+ // transport derives session/model headers from the attributed payload.
1919
+ pi.registerProvider('anthropic', {
1920
+ api: 'anthropic-messages',
1921
+ streamSimple: (model, context, options) =>
1922
+ streamAnthropicViaBetaMessages(model, context, {
1923
+ ...(options ?? {}),
1924
+ cacheRetention: resolveCacheRetentionPreference(options, getSessionOverride()),
1925
+ }),
1926
+ });
1927
+
1875
1928
  pi.registerCommand('claude-cache', {
1876
1929
  description: 'Show or set Claude cache retention for this session (short, long, default)',
1877
1930
  handler: (args, ctx) => {
@@ -1902,7 +1955,6 @@ export default function spawnAnthropicAttribution(pi: PiExtensionHost): void {
1902
1955
 
1903
1956
  pi.on('session_start', (_event, ctx) => {
1904
1957
  sessionCacheRetention = restoreAnthropicSessionCacheRetention(ctx.sessionManager.getBranch());
1905
- registerAnthropicAttributionProvider(pi, ctx, getSessionOverride);
1906
1958
  });
1907
1959
 
1908
1960
  pi.on('session_shutdown', () => {
@@ -1913,13 +1965,8 @@ export default function spawnAnthropicAttribution(pi: PiExtensionHost): void {
1913
1965
  sessionCacheRetention = restoreAnthropicSessionCacheRetention(ctx.sessionManager.getBranch());
1914
1966
  });
1915
1967
 
1916
- pi.on('before_agent_start', (_event, ctx) => {
1917
- registerAnthropicAttributionProvider(pi, ctx, getSessionOverride);
1918
- });
1919
-
1920
1968
  pi.on('before_provider_request', (event, ctx) => {
1921
1969
  if (!isAnthropicContext(ctx)) return undefined;
1922
- registerAnthropicAttributionProvider(pi, ctx, getSessionOverride);
1923
1970
  return rewriteAnthropicRequestPayload({
1924
1971
  payload: event.payload,
1925
1972
  ctx,
@@ -1927,4 +1974,10 @@ export default function spawnAnthropicAttribution(pi: PiExtensionHost): void {
1927
1974
  headerRegistered: true,
1928
1975
  });
1929
1976
  });
1977
+
1978
+ // Publish ownership last. Extension loading is sequential, so later independent
1979
+ // copies probe this responder and become inert instead of registering duplicates.
1980
+ pi.events.on(ANTHROPIC_ATTRIBUTION_CLAIM_CHANNEL, (value) => {
1981
+ if (isAnthropicAttributionClaimProbe(value)) value.acknowledge();
1982
+ });
1930
1983
  }
@@ -150,9 +150,18 @@ export function attestedPiChildEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
150
150
  return out;
151
151
  }
152
152
 
153
- export function buildAttestedPiArgv(input: StructuredPiLaunchRequest): string[] {
153
+ export function buildAttestedPiArgv(
154
+ input: StructuredPiLaunchRequest,
155
+ attributionExtensionPath?: string,
156
+ ): string[] {
154
157
  validateStructuredPiLaunchRequest(input);
155
158
  const args = ['pi', '--mode', 'json', '--provider', input.provider, '--model', input.model];
159
+ if (input.provider === 'anthropic') {
160
+ if (!attributionExtensionPath?.trim()) {
161
+ throw new Error('Anthropic attested Pi tasks require the package attribution extension');
162
+ }
163
+ args.push('--extension', attributionExtensionPath);
164
+ }
156
165
  if (input.thinking?.trim()) args.push('--thinking', input.thinking.trim());
157
166
  args.push(...(input.extraPiArgs ?? []), input.prompt);
158
167
  return args;
@@ -3,7 +3,7 @@ import { open } from 'node:fs/promises';
3
3
  import { extname, isAbsolute, join, win32 } from 'node:path';
4
4
  import { DEFAULT_MAX_BYTES } from '@earendil-works/pi-coding-agent';
5
5
  import type { BackgroundTaskChildProcess } from './registry.js';
6
- import type { DelegateBudgetRouteSource } from './delegate/types.js';
6
+ import type { DelegateBudgetRouteSource, DelegateExtensionMode } from './delegate/types.js';
7
7
  import type { FusionResultDetails, FusionUsage, FusionWorkflowId } from './fusion/types.js';
8
8
 
9
9
  export const TASK_STATUS_VALUES = ['running', 'completed', 'failed', 'killed'] as const;
@@ -87,6 +87,7 @@ export interface DelegateTaskFacts {
87
87
  childSessionId: string;
88
88
  route: { provider: string; model: string; qualifiedId: string };
89
89
  budget: DelegateBudgetRouteSource;
90
+ extensionMode: DelegateExtensionMode;
90
91
  autoDeliver: 'never' | 'when_small' | 'always';
91
92
  /** Set once the run reaches a terminal state and its result has been evaluated. */
92
93
  outcome?: DelegateTaskOutcome | undefined;
@@ -8,6 +8,7 @@ import {
8
8
  DELEGATE_MANIFEST_SCHEMA_VERSION,
9
9
  DELEGATE_RECEIPT_SCHEMA_VERSION,
10
10
  DelegateError,
11
+ type DelegateExtensionMode,
11
12
  type DelegateLimits,
12
13
  type DelegatePinnedRoute,
13
14
  type DelegateSpillReceipt,
@@ -67,6 +68,7 @@ export interface DelegateManifestV1 {
67
68
  cwd: string;
68
69
  child_session_id: string;
69
70
  child_session_dir: string;
71
+ extension_mode: DelegateExtensionMode;
70
72
  route: DelegatePinnedRoute;
71
73
  limits: DelegateLimits;
72
74
  seed_sha256: string;
@@ -111,6 +113,7 @@ export interface CreateDelegateArtifactStoreOptions {
111
113
  sessionId?: string | undefined;
112
114
  childSessionId: string;
113
115
  childSessionDir: string;
116
+ extensionMode: DelegateExtensionMode;
114
117
  route: DelegatePinnedRoute;
115
118
  limits: DelegateLimits;
116
119
  seedSha256: string;
@@ -175,6 +178,7 @@ export class DelegateArtifactStore {
175
178
  cwd: options.cwd,
176
179
  child_session_id: options.childSessionId,
177
180
  child_session_dir: options.childSessionDir,
181
+ extension_mode: options.extensionMode,
178
182
  route: options.route,
179
183
  limits: options.limits,
180
184
  seed_sha256: options.seedSha256,