pi-background-tasks 2.1.4 → 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.
- package/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
- package/PUBLISHING.md +2 -0
- package/README.md +15 -8
- package/TESTING.md +15 -10
- package/TEST_PLAN.md +10 -9
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/docs/INDEX.md +8 -4
- package/docs/choose-a-workflow.md +5 -2
- package/docs/commands/claude-cache.md +50 -0
- package/docs/getting-started.md +3 -0
- package/docs/manifest.json +84 -19
- package/docs/operations/configuration.md +15 -1
- package/docs/operations/releasing.md +6 -3
- package/docs/read-before-edit.md +4 -1
- package/docs/reference/runtime-contracts.md +52 -52
- package/docs/subsystems/anthropic-attribution.md +63 -0
- package/docs/subsystems/attested-pi-runs.md +2 -2
- package/docs/subsystems/child-launch-durability-and-safety.md +3 -3
- package/docs/subsystems/delegation.md +12 -4
- package/docs/subsystems/docs-freshness-gate.md +5 -5
- package/docs/subsystems/fusion.md +2 -2
- package/docs/tools/bg_delegate.md +21 -9
- package/docs/tools/bg_result.md +1 -1
- package/docs/tools/bg_run.md +5 -0
- package/extensions/anthropic-attribution.ts +1 -0
- package/package.json +4 -2
- package/src/core/anthropic-attribution-path.ts +26 -0
- package/src/core/{fusion/anthropic-attribution.ts → anthropic-attribution.ts} +61 -8
- package/src/core/attested-pi-run.ts +10 -1
- package/src/core/common.ts +2 -1
- package/src/core/delegate/artifacts.ts +4 -0
- package/src/core/delegate/launch.ts +44 -14
- package/src/core/delegate/runner.ts +24 -0
- package/src/core/delegate/seed.ts +12 -0
- package/src/core/delegate/types.ts +10 -2
- package/src/core/fusion/config.ts +1 -1
- package/src/core/fusion/pi-child.ts +7 -124
- package/src/core/registry.ts +4 -1
- package/src/delegate-child-extension.ts +9 -2
- package/src/delegate-extension.ts +53 -4
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { canonicalJson } from '../attested-pi-run.js';
|
|
4
4
|
import { replaceFileDurable } from '../durable-fs.js';
|
|
5
|
+
import { resolveAnthropicAttributionExtensionPath } from '../anthropic-attribution-path.js';
|
|
5
6
|
import { join } from 'node:path';
|
|
6
7
|
import { DelegateArtifactStore, discardDelegateArtifactRoot } from './artifacts.js';
|
|
7
8
|
import {
|
|
@@ -20,6 +21,7 @@ import { verifyDelegateResultPackage, type VerifiedDelegateResult } from './resu
|
|
|
20
21
|
import {
|
|
21
22
|
DelegateError,
|
|
22
23
|
type DelegateAutoDeliverMode,
|
|
24
|
+
type DelegateExtensionMode,
|
|
23
25
|
type DelegateResultPackageV1,
|
|
24
26
|
} from './types.js';
|
|
25
27
|
import type { DelegateTaskFacts, DelegateTaskOutcome } from '../common.js';
|
|
@@ -48,7 +50,9 @@ export interface PrepareDelegateLaunchInput extends DelegatePreflightInput {
|
|
|
48
50
|
cwd: string;
|
|
49
51
|
sessionId: string | undefined;
|
|
50
52
|
autoDeliver: DelegateAutoDeliverMode;
|
|
53
|
+
extensionMode: DelegateExtensionMode;
|
|
51
54
|
childExtensionPath?: string | undefined;
|
|
55
|
+
attributionExtensionPath?: string | undefined;
|
|
52
56
|
env?: NodeJS.ProcessEnv | undefined;
|
|
53
57
|
now?: (() => Date) | undefined;
|
|
54
58
|
}
|
|
@@ -68,6 +72,22 @@ export async function prepareDelegateLaunch(
|
|
|
68
72
|
// its child guard must refuse rather than spawn an unguarded child.
|
|
69
73
|
const childExtensionPath =
|
|
70
74
|
input.childExtensionPath ?? resolveDelegateChildExtensionPath();
|
|
75
|
+
let attributionExtensionPath: string | undefined;
|
|
76
|
+
if (input.route.provider === 'anthropic') {
|
|
77
|
+
try {
|
|
78
|
+
attributionExtensionPath =
|
|
79
|
+
input.attributionExtensionPath ?? resolveAnthropicAttributionExtensionPath();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new DelegateError(
|
|
82
|
+
`Anthropic delegate attribution extension could not be resolved: ${error instanceof Error ? error.message : String(error)}`,
|
|
83
|
+
{
|
|
84
|
+
code: 'delegate_isolation_unsupported',
|
|
85
|
+
childCreated: false,
|
|
86
|
+
remediation: ['Reinstall the package; Anthropic delegates require attribution.'],
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
71
91
|
|
|
72
92
|
const preflight = preflightDelegateLaunch(input);
|
|
73
93
|
|
|
@@ -78,6 +98,7 @@ export async function prepareDelegateLaunch(
|
|
|
78
98
|
sessionId: input.sessionId,
|
|
79
99
|
childSessionId: preflight.childSessionId,
|
|
80
100
|
childSessionDir: '',
|
|
101
|
+
extensionMode: input.extensionMode,
|
|
81
102
|
route: input.route,
|
|
82
103
|
limits: preflight.limits,
|
|
83
104
|
seedSha256: preflight.seed.sha256,
|
|
@@ -107,9 +128,11 @@ export async function prepareDelegateLaunch(
|
|
|
107
128
|
const argv = buildDelegateChildArgv({
|
|
108
129
|
route: input.route,
|
|
109
130
|
capability: input.capability,
|
|
131
|
+
extensionMode: input.extensionMode,
|
|
110
132
|
childSessionId: preflight.childSessionId,
|
|
111
133
|
childSessionDir: childSessionDirAbs,
|
|
112
134
|
childExtensionPath,
|
|
135
|
+
attributionExtensionPath,
|
|
113
136
|
systemPrompt: preflight.childSystemPrompt,
|
|
114
137
|
});
|
|
115
138
|
const env = delegateChildEnv(
|
|
@@ -138,6 +161,7 @@ export async function prepareDelegateLaunch(
|
|
|
138
161
|
family: preflight.plan.route.family,
|
|
139
162
|
rate_source: preflight.plan.route.rate_source,
|
|
140
163
|
},
|
|
164
|
+
extensionMode: input.extensionMode,
|
|
141
165
|
autoDeliver: input.autoDeliver,
|
|
142
166
|
};
|
|
143
167
|
const stdinBytes = Buffer.from(preflight.childPrompt, 'utf8');
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type DelegateContextPolicyDescriptor,
|
|
23
23
|
type DelegateConversationProjection,
|
|
24
24
|
type DelegateLedgerV1,
|
|
25
|
+
type DelegateExtensionMode,
|
|
25
26
|
type DelegateLimits,
|
|
26
27
|
type DelegatePinnedRoute,
|
|
27
28
|
type DelegateSeedV1,
|
|
@@ -34,6 +35,7 @@ export interface BuildDelegateSeedOptions {
|
|
|
34
35
|
toolCallId: string | undefined;
|
|
35
36
|
directive: string;
|
|
36
37
|
capability: DelegateCapability;
|
|
38
|
+
extensionMode: DelegateExtensionMode;
|
|
37
39
|
route: DelegatePinnedRoute;
|
|
38
40
|
limits: DelegateLimits;
|
|
39
41
|
}
|
|
@@ -167,6 +169,7 @@ export function buildDelegateSeed(
|
|
|
167
169
|
launch_nonce: options.launchNonce,
|
|
168
170
|
cwd: ctx.cwd,
|
|
169
171
|
capability: options.capability,
|
|
172
|
+
extension_mode: options.extensionMode,
|
|
170
173
|
route: options.route,
|
|
171
174
|
parent_system_prompt: ctx.getSystemPrompt(),
|
|
172
175
|
parent_leaf_id: snapshot.leafId,
|
|
@@ -296,6 +299,14 @@ function rebuildSeed(record: Record<PropertyKey, unknown>, taskId: string): Dele
|
|
|
296
299
|
taskId,
|
|
297
300
|
});
|
|
298
301
|
}
|
|
302
|
+
const extensionMode = requireString(record, 'extension_mode', taskId);
|
|
303
|
+
if (extensionMode !== 'isolated' && extensionMode !== 'ambient') {
|
|
304
|
+
throw new DelegateError(`delegate seed extension mode ${extensionMode} is not recognised`, {
|
|
305
|
+
code: 'seed_hash_mismatch',
|
|
306
|
+
childCreated: true,
|
|
307
|
+
taskId,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
299
310
|
const routeRecord = requireRecord(record, 'route', taskId);
|
|
300
311
|
const routeOrigin = requireString(routeRecord, 'origin', taskId);
|
|
301
312
|
if (routeOrigin !== 'parent_current' && routeOrigin !== 'explicit') {
|
|
@@ -378,6 +389,7 @@ function rebuildSeed(record: Record<PropertyKey, unknown>, taskId: string): Dele
|
|
|
378
389
|
launch_nonce: requireString(record, 'launch_nonce', taskId),
|
|
379
390
|
cwd: requireString(record, 'cwd', taskId),
|
|
380
391
|
capability,
|
|
392
|
+
extension_mode: extensionMode,
|
|
381
393
|
route,
|
|
382
394
|
parent_system_prompt: requireString(record, 'parent_system_prompt', taskId),
|
|
383
395
|
parent_leaf_id: parentLeafId,
|
|
@@ -12,14 +12,14 @@ import type {
|
|
|
12
12
|
ProjectionEntry,
|
|
13
13
|
} from '../context/visible-conversation-v2.js';
|
|
14
14
|
|
|
15
|
-
export const DELEGATE_SEED_SCHEMA_VERSION = 'pi-background-tasks.delegate-seed.
|
|
15
|
+
export const DELEGATE_SEED_SCHEMA_VERSION = 'pi-background-tasks.delegate-seed.v2' as const;
|
|
16
16
|
export const DELEGATE_LEDGER_SCHEMA_VERSION = 'pi-background-tasks.delegate-ledger.v1' as const;
|
|
17
17
|
export const DELEGATE_RESULT_PACKAGE_SCHEMA_VERSION =
|
|
18
18
|
'pi-background-tasks.delegate-result.v1' as const;
|
|
19
19
|
export const DELEGATE_RECEIPT_SCHEMA_VERSION = 'pi-background-tasks.delegate-receipt.v1' as const;
|
|
20
20
|
export const DELEGATE_BUDGET_PLAN_SCHEMA_VERSION =
|
|
21
21
|
'pi-background-tasks.delegate-budget-plan.v2' as const;
|
|
22
|
-
export const DELEGATE_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.delegate-manifest.
|
|
22
|
+
export const DELEGATE_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.delegate-manifest.v2' as const;
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Delegate's own context policy id. It shares the frozen
|
|
@@ -35,6 +35,13 @@ export const DELEGATE_RESULT_TOOL_NAME = 'bg_result';
|
|
|
35
35
|
export const DELEGATE_CAPABILITIES = ['inspect'] as const;
|
|
36
36
|
export type DelegateCapability = (typeof DELEGATE_CAPABILITIES)[number];
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Controls only Pi's ambient extension discovery for delegate children.
|
|
40
|
+
* Tool and project-resource restrictions remain independently enforced.
|
|
41
|
+
*/
|
|
42
|
+
export const DELEGATE_EXTENSION_MODES = ['isolated', 'ambient'] as const;
|
|
43
|
+
export type DelegateExtensionMode = (typeof DELEGATE_EXTENSION_MODES)[number];
|
|
44
|
+
|
|
38
45
|
export const DELEGATE_AUTO_DELIVER_MODES = ['never', 'when_small', 'always'] as const;
|
|
39
46
|
export type DelegateAutoDeliverMode = (typeof DELEGATE_AUTO_DELIVER_MODES)[number];
|
|
40
47
|
|
|
@@ -110,6 +117,7 @@ export interface DelegateSeedV1 {
|
|
|
110
117
|
launch_nonce: string;
|
|
111
118
|
cwd: string;
|
|
112
119
|
capability: DelegateCapability;
|
|
120
|
+
extension_mode: DelegateExtensionMode;
|
|
113
121
|
route: DelegatePinnedRoute;
|
|
114
122
|
parent_system_prompt: string;
|
|
115
123
|
parent_leaf_id: string | null;
|
|
@@ -5,7 +5,7 @@ import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
|
5
5
|
import type { Api, Model } from '@earendil-works/pi-ai';
|
|
6
6
|
import { isJsonObject, parseJsonText, type JsonObject } from '../common.js';
|
|
7
7
|
import { replaceFileDurable } from '../durable-fs.js';
|
|
8
|
-
import { CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW } from '
|
|
8
|
+
import { CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW } from '../anthropic-attribution.js';
|
|
9
9
|
import {
|
|
10
10
|
FUSION_MODEL_CONFIG_SCHEMA_VERSION,
|
|
11
11
|
FusionError,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { spawn as nodeSpawn, type SpawnOptions } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { constants, existsSync
|
|
3
|
+
import { constants, existsSync } from 'node:fs';
|
|
4
4
|
import { open } from 'node:fs/promises';
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
5
|
import { dirname, resolve } from 'node:path';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
8
7
|
import {
|
|
@@ -73,6 +72,7 @@ import {
|
|
|
73
72
|
resolvePiLaunch,
|
|
74
73
|
type PiLaunchDependencies,
|
|
75
74
|
} from '../pi-launch.js';
|
|
75
|
+
import { resolveAnthropicAttributionExtensionPath } from '../anthropic-attribution-path.js';
|
|
76
76
|
|
|
77
77
|
// The response cap now applies to one final full answer, not cumulative Pi JSON events.
|
|
78
78
|
export const FUSION_CHILD_STDOUT_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
@@ -274,21 +274,6 @@ export function fusionPiChildEnv(
|
|
|
274
274
|
return out;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
export function resolveFusionAnthropicAttributionExtensionPath(
|
|
278
|
-
moduleUrl = import.meta.url,
|
|
279
|
-
pathExists: (path: string) => boolean = existsSync,
|
|
280
|
-
): string {
|
|
281
|
-
const modulePath = fileURLToPath(moduleUrl);
|
|
282
|
-
const extension = modulePath.endsWith('.ts')
|
|
283
|
-
? 'anthropic-attribution.ts'
|
|
284
|
-
: 'anthropic-attribution.js';
|
|
285
|
-
const candidate = resolve(dirname(modulePath), extension);
|
|
286
|
-
if (!pathExists(candidate)) {
|
|
287
|
-
throw new Error(`Fusion Anthropic attribution extension is missing: ${candidate}`);
|
|
288
|
-
}
|
|
289
|
-
return candidate;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
277
|
export function resolveFusionChildExtensionPath(
|
|
293
278
|
moduleUrl = import.meta.url,
|
|
294
279
|
pathExists: (path: string) => boolean = existsSync,
|
|
@@ -303,109 +288,10 @@ export function resolveFusionChildExtensionPath(
|
|
|
303
288
|
}
|
|
304
289
|
|
|
305
290
|
/**
|
|
306
|
-
* Provider whose children require the
|
|
307
|
-
*
|
|
308
|
-
* Pi's own system prompt contains documentation lines that Anthropic rejects, so a
|
|
309
|
-
* Claude child launched without the sanitizer fails at the provider rather than
|
|
310
|
-
* producing an answer. The parent session loads the sanitizer through ordinary
|
|
311
|
-
* extension discovery, but Fusion children run with `--no-extensions` for
|
|
312
|
-
* isolation and therefore inherit nothing; the sanitizer must be re-supplied
|
|
313
|
-
* explicitly per child.
|
|
291
|
+
* Provider whose isolated children require the package-owned attribution and
|
|
292
|
+
* exact-match system-prompt sanitization extension.
|
|
314
293
|
*/
|
|
315
294
|
export const FUSION_SANITIZED_PROVIDER = 'anthropic';
|
|
316
|
-
export const FUSION_ANTHROPIC_SANITIZER_PACKAGE = '@ravshansbox/pi-anthropic-sps';
|
|
317
|
-
const FUSION_ANTHROPIC_SANITIZER_MANIFEST = `${FUSION_ANTHROPIC_SANITIZER_PACKAGE}/package.json`;
|
|
318
|
-
|
|
319
|
-
export interface FusionSanitizerDependencies {
|
|
320
|
-
resolvePackageJson?: ((specifier: string) => string) | undefined;
|
|
321
|
-
readManifest?: ((path: string) => string) | undefined;
|
|
322
|
-
pathExists?: ((path: string) => boolean) | undefined;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function manifestExtensionEntry(manifestText: string, manifestPath: string): string {
|
|
326
|
-
let parsed: unknown;
|
|
327
|
-
try {
|
|
328
|
-
parsed = parseJsonText(manifestText);
|
|
329
|
-
} catch (error) {
|
|
330
|
-
throw new FusionError(
|
|
331
|
-
`Anthropic sanitizer manifest ${manifestPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
332
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
if (!isJsonObject(parsed)) {
|
|
336
|
-
throw new FusionError(`Anthropic sanitizer manifest ${manifestPath} must be an object`, {
|
|
337
|
-
code: 'orchestration_failed',
|
|
338
|
-
childCreated: false,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
const pi = parsed['pi'];
|
|
342
|
-
if (!isJsonObject(pi)) {
|
|
343
|
-
throw new FusionError(
|
|
344
|
-
`Anthropic sanitizer manifest ${manifestPath} has no "pi" section declaring its extension`,
|
|
345
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
const extensions = pi['extensions'];
|
|
349
|
-
if (!Array.isArray(extensions) || extensions.length === 0) {
|
|
350
|
-
throw new FusionError(
|
|
351
|
-
`Anthropic sanitizer manifest ${manifestPath} declares no pi.extensions entries`,
|
|
352
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
const [entry] = extensions;
|
|
356
|
-
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
357
|
-
throw new FusionError(
|
|
358
|
-
`Anthropic sanitizer manifest ${manifestPath} pi.extensions[0] must be a non-blank string`,
|
|
359
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
return entry;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Resolve the sanitizer extension file shipped by the sanitizer package.
|
|
367
|
-
*
|
|
368
|
-
* The package intentionally publishes no `main`/`exports`, so the entry cannot be
|
|
369
|
-
* required directly; its manifest is resolved and the declared `pi.extensions[0]`
|
|
370
|
-
* path is joined against the package root. Every failure is loud: a Claude child
|
|
371
|
-
* launched without the sanitizer would fail at the provider with a far less
|
|
372
|
-
* actionable error, so silently omitting it is never correct.
|
|
373
|
-
*/
|
|
374
|
-
export function resolveAnthropicSanitizerExtensionPath(
|
|
375
|
-
dependencies: FusionSanitizerDependencies = {},
|
|
376
|
-
): string {
|
|
377
|
-
const resolvePackageJson =
|
|
378
|
-
dependencies.resolvePackageJson ?? createRequire(import.meta.url).resolve;
|
|
379
|
-
const readManifest = dependencies.readManifest ?? ((path: string) => readFileSync(path, 'utf8'));
|
|
380
|
-
const pathExists = dependencies.pathExists ?? existsSync;
|
|
381
|
-
let manifestPath: string;
|
|
382
|
-
try {
|
|
383
|
-
manifestPath = resolvePackageJson(FUSION_ANTHROPIC_SANITIZER_MANIFEST);
|
|
384
|
-
} catch (error) {
|
|
385
|
-
throw new FusionError(
|
|
386
|
-
`Anthropic sanitizer package ${FUSION_ANTHROPIC_SANITIZER_PACKAGE} could not be resolved: ${error instanceof Error ? error.message : String(error)}. Claude children cannot be launched without it.`,
|
|
387
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
let manifestText: string;
|
|
391
|
-
try {
|
|
392
|
-
manifestText = readManifest(manifestPath);
|
|
393
|
-
} catch (error) {
|
|
394
|
-
throw new FusionError(
|
|
395
|
-
`Anthropic sanitizer manifest ${manifestPath} could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
396
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
const entry = manifestExtensionEntry(manifestText, manifestPath);
|
|
400
|
-
const extensionPath = resolve(dirname(manifestPath), entry);
|
|
401
|
-
if (!pathExists(extensionPath)) {
|
|
402
|
-
throw new FusionError(
|
|
403
|
-
`Anthropic sanitizer extension is missing: ${extensionPath} (declared by ${manifestPath})`,
|
|
404
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
return extensionPath;
|
|
408
|
-
}
|
|
409
295
|
|
|
410
296
|
export function assertFusionToolPolicyDisjoint(
|
|
411
297
|
allowlist: readonly string[] = FUSION_INSPECT_TOOLS,
|
|
@@ -468,11 +354,10 @@ function fusionToolArgv(capability: FusionCapability): string[] {
|
|
|
468
354
|
export function fusionChildExtensionPaths(
|
|
469
355
|
model: ResolvedFusionModel,
|
|
470
356
|
childExtensionPath: string,
|
|
471
|
-
|
|
472
|
-
resolveAttribution: () => string = resolveFusionAnthropicAttributionExtensionPath,
|
|
357
|
+
resolveAttribution: () => string = resolveAnthropicAttributionExtensionPath,
|
|
473
358
|
): readonly string[] {
|
|
474
359
|
if (model.provider !== FUSION_SANITIZED_PROVIDER) return [childExtensionPath];
|
|
475
|
-
return [resolveAttribution(),
|
|
360
|
+
return [resolveAttribution(), childExtensionPath];
|
|
476
361
|
}
|
|
477
362
|
|
|
478
363
|
export function buildFusionPiChildArgv(
|
|
@@ -480,13 +365,11 @@ export function buildFusionPiChildArgv(
|
|
|
480
365
|
systemPrompt: string,
|
|
481
366
|
childExtensionPath = resolveFusionChildExtensionPath(),
|
|
482
367
|
capability: FusionCapability = FUSION_NO_TOOLS_CAPABILITY,
|
|
483
|
-
|
|
484
|
-
resolveAttribution: () => string = resolveFusionAnthropicAttributionExtensionPath,
|
|
368
|
+
resolveAttribution: () => string = resolveAnthropicAttributionExtensionPath,
|
|
485
369
|
): string[] {
|
|
486
370
|
const extensionArgs = fusionChildExtensionPaths(
|
|
487
371
|
model,
|
|
488
372
|
childExtensionPath,
|
|
489
|
-
resolveSanitizer,
|
|
490
373
|
resolveAttribution,
|
|
491
374
|
).flatMap((path) => ['--extension', path]);
|
|
492
375
|
return [
|
package/src/core/registry.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
resolvePiLaunch,
|
|
59
59
|
type PiLaunchSpec,
|
|
60
60
|
} from './pi-launch.js';
|
|
61
|
+
import { resolveAnthropicAttributionExtensionPath } from './anthropic-attribution-path.js';
|
|
61
62
|
import {
|
|
62
63
|
runWindowsTaskkill,
|
|
63
64
|
type TaskkillOutcome,
|
|
@@ -1234,7 +1235,9 @@ export class BackgroundTaskRegistry {
|
|
|
1234
1235
|
if (this.shuttingDown)
|
|
1235
1236
|
throw new Error('Cannot start an attested Pi task while Pi is shutting down');
|
|
1236
1237
|
|
|
1237
|
-
const
|
|
1238
|
+
const attributionExtensionPath =
|
|
1239
|
+
request.provider === 'anthropic' ? resolveAnthropicAttributionExtensionPath() : undefined;
|
|
1240
|
+
const argv = buildAttestedPiArgv(request, attributionExtensionPath);
|
|
1238
1241
|
const attestedPiLaunch = resolvePiLaunch({ platform: this.platform });
|
|
1239
1242
|
assertWindowsCommandLineWithinLimit(
|
|
1240
1243
|
attestedPiLaunch,
|
|
@@ -7,6 +7,7 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
|
7
7
|
import type { Usage } from '@earendil-works/pi-ai';
|
|
8
8
|
import {
|
|
9
9
|
DELEGATE_RECEIPT_SCHEMA_VERSION,
|
|
10
|
+
DELEGATE_CAPABILITIES,
|
|
10
11
|
type DelegateRouteAttestation,
|
|
11
12
|
type DelegateSeedV1,
|
|
12
13
|
type DelegateSpillReceipt,
|
|
@@ -23,8 +24,10 @@ import {
|
|
|
23
24
|
/**
|
|
24
25
|
* Package-owned delegate child extension.
|
|
25
26
|
*
|
|
26
|
-
* This runs inside
|
|
27
|
-
*
|
|
27
|
+
* This runs inside every delegate child Pi process and is the package-owned
|
|
28
|
+
* child guard. Anthropic routes load attribution first, and ambient mode may
|
|
29
|
+
* also execute discovered extensions; this guard remains
|
|
30
|
+
* responsible for every isolation guarantee that cannot be enforced from the
|
|
28
31
|
* parent:
|
|
29
32
|
*
|
|
30
33
|
* - verifying the frozen seed bytes before the first model call;
|
|
@@ -289,6 +292,10 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
289
292
|
launchNonce: expectedNonce,
|
|
290
293
|
});
|
|
291
294
|
|
|
295
|
+
if (!DELEGATE_CAPABILITIES.includes(seed.capability)) {
|
|
296
|
+
throw new Error(`delegate child cannot enforce capability ${seed.capability}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
292
299
|
const state: GuardState = {
|
|
293
300
|
seed,
|
|
294
301
|
artifactDirAbs,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
DELEGATE_AUTO_DELIVER_MODES,
|
|
23
23
|
DELEGATE_CAPABILITIES,
|
|
24
|
+
DELEGATE_EXTENSION_MODES,
|
|
24
25
|
DELEGATE_RESULT_TOOL_NAME,
|
|
25
26
|
DELEGATE_TOOL_NAME,
|
|
26
27
|
DelegateError,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
type DelegateCapability,
|
|
29
30
|
type DelegateDeliveryMode,
|
|
30
31
|
type DelegateBudgetRouteSource,
|
|
32
|
+
type DelegateExtensionMode,
|
|
31
33
|
type DelegateRoute,
|
|
32
34
|
} from './core/delegate/types.js';
|
|
33
35
|
import {
|
|
@@ -67,7 +69,7 @@ const HOOK_EVIDENCE_PATH = fileURLToPath(
|
|
|
67
69
|
new URL('./core/delegate/hook-contract-evidence.json', import.meta.url),
|
|
68
70
|
);
|
|
69
71
|
|
|
70
|
-
const DelegateParams = Type.Object(
|
|
72
|
+
export const DelegateParams = Type.Object(
|
|
71
73
|
{
|
|
72
74
|
name: Type.String({
|
|
73
75
|
description: 'Short human-readable task name shown in the bg footer dock. Use 2-6 words.',
|
|
@@ -93,6 +95,12 @@ const DelegateParams = Type.Object(
|
|
|
93
95
|
description: `Capability profile. Only "inspect" (read/search/list, no shell, no writes, no network, no recursion) is supported.`,
|
|
94
96
|
}),
|
|
95
97
|
),
|
|
98
|
+
extensionMode: Type.Optional(
|
|
99
|
+
Type.String({
|
|
100
|
+
description:
|
|
101
|
+
'Extension discovery: isolated | ambient. Default isolated. Ambient is for extension-registered providers and executes arbitrary discovered extension code, weakening process isolation.',
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
96
104
|
maxTurns: Type.Optional(
|
|
97
105
|
Type.Number({
|
|
98
106
|
description: `Maximum agent turns. Default ${String(DELEGATE_DEFAULT_MAX_TURNS)}.`,
|
|
@@ -142,6 +150,20 @@ const ResultParams = Type.Object(
|
|
|
142
150
|
type DelegateParamsValue = Static<typeof DelegateParams>;
|
|
143
151
|
type ResultParamsValue = Static<typeof ResultParams>;
|
|
144
152
|
|
|
153
|
+
const DELEGATE_PARAM_KEYS = new Set([
|
|
154
|
+
'name',
|
|
155
|
+
'prompt',
|
|
156
|
+
'route',
|
|
157
|
+
'capability',
|
|
158
|
+
'extensionMode',
|
|
159
|
+
'maxTurns',
|
|
160
|
+
'maxToolCalls',
|
|
161
|
+
'timeoutSeconds',
|
|
162
|
+
'autoDeliver',
|
|
163
|
+
'notifyOnCompletion',
|
|
164
|
+
'triggerOnCompletion',
|
|
165
|
+
]);
|
|
166
|
+
|
|
145
167
|
export interface DelegateLaunchDetails {
|
|
146
168
|
schema_version: 'pi-background-tasks.delegate-launch.v1';
|
|
147
169
|
task: BgTaskSnapshot;
|
|
@@ -151,6 +173,7 @@ export interface DelegateLaunchDetails {
|
|
|
151
173
|
seed_sha256: string;
|
|
152
174
|
seed_utf8_bytes: number;
|
|
153
175
|
budget: DelegateBudgetRouteSource;
|
|
176
|
+
extension_mode: DelegateExtensionMode;
|
|
154
177
|
auto_deliver: DelegateAutoDeliverMode;
|
|
155
178
|
notify_on_completion: boolean;
|
|
156
179
|
trigger_on_completion: boolean;
|
|
@@ -187,6 +210,7 @@ export interface DelegateResultDetails {
|
|
|
187
210
|
delivery: DelegateDeliveryMode | 'none';
|
|
188
211
|
route?: { provider: string; model: string } | undefined;
|
|
189
212
|
budget?: DelegateBudgetRouteSource | undefined;
|
|
213
|
+
extension_mode?: DelegateExtensionMode | undefined;
|
|
190
214
|
answer_bytes?: number | undefined;
|
|
191
215
|
answer_sha256?: string | undefined;
|
|
192
216
|
turns?: number | undefined;
|
|
@@ -213,6 +237,15 @@ function requireCapability(value: unknown): DelegateCapability {
|
|
|
213
237
|
);
|
|
214
238
|
}
|
|
215
239
|
|
|
240
|
+
function requireExtensionMode(value: unknown): DelegateExtensionMode {
|
|
241
|
+
if (value === undefined) return 'isolated';
|
|
242
|
+
if (value === 'isolated' || value === 'ambient') return value;
|
|
243
|
+
throw new DelegateError(
|
|
244
|
+
`bg_delegate extensionMode must be one of ${DELEGATE_EXTENSION_MODES.join(', ')}`,
|
|
245
|
+
{ code: 'invalid_arguments', childCreated: false },
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
216
249
|
function requireAutoDeliver(value: unknown): DelegateAutoDeliverMode {
|
|
217
250
|
if (value === undefined) return 'never';
|
|
218
251
|
if (value === 'never' || value === 'when_small' || value === 'always') return value;
|
|
@@ -308,13 +341,15 @@ export function registerDelegateExtension(
|
|
|
308
341
|
name: DELEGATE_TOOL_NAME,
|
|
309
342
|
label: 'Background Delegate',
|
|
310
343
|
description:
|
|
311
|
-
'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.',
|
|
344
|
+
'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.',
|
|
312
345
|
promptSnippet:
|
|
313
346
|
'Delegate an investigation to a background agent that already has this conversation as context',
|
|
314
347
|
promptGuidelines: [
|
|
315
348
|
'Use bg_delegate when work should continue in the background and the worker needs what you already know: it is seeded with a projection of this conversation.',
|
|
316
349
|
'The prompt is authoritative. State exactly what you want investigated and what the answer should contain.',
|
|
317
|
-
'The delegate is inspect-only: it can read, search, and list files, but cannot run shell commands, edit or write files, use the network, or delegate further.',
|
|
350
|
+
'The delegate is inspect-only at the model-visible tool boundary: it can read, search, and list files, but cannot run shell commands, edit or write files, use the network, or delegate further.',
|
|
351
|
+
'Extension discovery is isolated by default. Use extensionMode:"ambient" only when the pinned provider is registered by an ambient user/project extension.',
|
|
352
|
+
'Ambient mode executes arbitrary discovered extension code in the child process. Tool allowlists do not sandbox extension code, so ambient mode weakens inspect-only process isolation.',
|
|
318
353
|
'Facts that exist only inside omitted tool output are not available to the delegate. Restate such findings in the prompt.',
|
|
319
354
|
'bg_delegate returns immediately. Do not poll; retrieve the answer with bg_result after the terminal notification arrives.',
|
|
320
355
|
],
|
|
@@ -325,6 +360,13 @@ export function registerDelegateExtension(
|
|
|
325
360
|
code: 'invalid_arguments',
|
|
326
361
|
childCreated: false,
|
|
327
362
|
});
|
|
363
|
+
const unknownKeys = Object.keys(args).filter((key) => !DELEGATE_PARAM_KEYS.has(key));
|
|
364
|
+
if (unknownKeys.length > 0) {
|
|
365
|
+
throw new DelegateError(
|
|
366
|
+
`bg_delegate contains unsupported key(s): ${unknownKeys.sort().join(', ')}`,
|
|
367
|
+
{ code: 'invalid_arguments', childCreated: false },
|
|
368
|
+
);
|
|
369
|
+
}
|
|
328
370
|
const name = args['name'];
|
|
329
371
|
const prompt = args['prompt'];
|
|
330
372
|
if (typeof name !== 'string' || name.trim().length === 0)
|
|
@@ -341,6 +383,7 @@ export function registerDelegateExtension(
|
|
|
341
383
|
const route = requireRoute(args['route']);
|
|
342
384
|
if (route !== undefined) prepared.route = route;
|
|
343
385
|
prepared.capability = requireCapability(args['capability']);
|
|
386
|
+
prepared.extensionMode = requireExtensionMode(args['extensionMode']);
|
|
344
387
|
prepared.autoDeliver = requireAutoDeliver(args['autoDeliver']);
|
|
345
388
|
const maxTurns = optionalPositiveInteger(args['maxTurns'], 'maxTurns');
|
|
346
389
|
if (maxTurns !== undefined) prepared.maxTurns = maxTurns;
|
|
@@ -356,6 +399,7 @@ export function registerDelegateExtension(
|
|
|
356
399
|
},
|
|
357
400
|
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
358
401
|
const capability = requireCapability(params.capability);
|
|
402
|
+
const extensionMode = requireExtensionMode(params.extensionMode);
|
|
359
403
|
const autoDeliver = requireAutoDeliver(params.autoDeliver);
|
|
360
404
|
const hookEvidence = await loadEvidence();
|
|
361
405
|
const route = resolveDelegateRoute({
|
|
@@ -385,6 +429,7 @@ export function registerDelegateExtension(
|
|
|
385
429
|
toolCallId,
|
|
386
430
|
prompt: params.prompt,
|
|
387
431
|
capability,
|
|
432
|
+
extensionMode,
|
|
388
433
|
route,
|
|
389
434
|
limitOverrides: {
|
|
390
435
|
maxTurns: params.maxTurns,
|
|
@@ -423,6 +468,7 @@ export function registerDelegateExtension(
|
|
|
423
468
|
seed_sha256: prepared.facts.seedSha256,
|
|
424
469
|
seed_utf8_bytes: prepared.preflight.plan.seed_utf8_bytes,
|
|
425
470
|
budget: prepared.facts.budget,
|
|
471
|
+
extension_mode: extensionMode,
|
|
426
472
|
auto_deliver: autoDeliver,
|
|
427
473
|
notify_on_completion: launchOptions.notifyOnCompletion,
|
|
428
474
|
trigger_on_completion: launchOptions.triggerOnCompletion,
|
|
@@ -437,6 +483,7 @@ export function registerDelegateExtension(
|
|
|
437
483
|
`Seed: ${String(prepared.preflight.plan.seed_utf8_bytes)} bytes, sha256 ${prepared.facts.seedSha256}`,
|
|
438
484
|
`Estimator: family ${prepared.facts.budget.family}, source ${prepared.facts.budget.rate_source.source}, rate ${String(prepared.facts.budget.rate_source.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(prepared.facts.budget.rate_source.affine_f_tokens)} tokens${prepared.facts.budget.rate_source.warning === null ? '' : `; warning: ${prepared.facts.budget.rate_source.warning}`}`,
|
|
439
485
|
`Capability: ${capability} (read/search/list only)`,
|
|
486
|
+
`Extension mode: ${extensionMode}${extensionMode === 'ambient' ? ' — WARNING: arbitrary discovered extension code executes in the child; the tool allowlist does not sandbox it, so inspect-only process isolation is weakened.' : ' (ambient extension discovery disabled)'}`,
|
|
440
487
|
`Limits: ${String(prepared.preflight.limits.max_turns)} turns, ${String(prepared.preflight.limits.max_tool_calls)} tool calls, ${String(prepared.preflight.limits.timeout_seconds)}s`,
|
|
441
488
|
`Auto-deliver: ${autoDeliver}`,
|
|
442
489
|
launchOptions.notifyOnCompletion
|
|
@@ -458,7 +505,7 @@ export function registerDelegateExtension(
|
|
|
458
505
|
renderResult(result, _options, theme) {
|
|
459
506
|
const details = result.details;
|
|
460
507
|
return new Text(
|
|
461
|
-
`${theme.fg('success', '✓ delegated')} ${theme.fg('accent', details.task.id)}\n${theme.fg('dim', `route ${details.route.qualified_id} · seed ${String(details.seed_utf8_bytes)}B · ${details.artifact_dir}`)}`,
|
|
508
|
+
`${theme.fg('success', '✓ delegated')} ${theme.fg('accent', details.task.id)}\n${theme.fg('dim', `route ${details.route.qualified_id} · extensions ${details.extension_mode} · seed ${String(details.seed_utf8_bytes)}B · ${details.artifact_dir}`)}`,
|
|
462
509
|
0,
|
|
463
510
|
0,
|
|
464
511
|
);
|
|
@@ -633,6 +680,7 @@ export function registerDelegateExtension(
|
|
|
633
680
|
delivery: 'none',
|
|
634
681
|
artifact_dir: facts.artifactDir,
|
|
635
682
|
budget: facts.budget,
|
|
683
|
+
extension_mode: facts.extensionMode,
|
|
636
684
|
};
|
|
637
685
|
return {
|
|
638
686
|
content: textContent(
|
|
@@ -686,6 +734,7 @@ export function registerDelegateExtension(
|
|
|
686
734
|
delivery: decision.mode,
|
|
687
735
|
route: verified.package.route,
|
|
688
736
|
budget: facts.budget,
|
|
737
|
+
extension_mode: facts.extensionMode,
|
|
689
738
|
answer_bytes: verified.package.answer.byte_length,
|
|
690
739
|
answer_sha256: verified.package.answer.sha256,
|
|
691
740
|
turns: verified.package.turns,
|