@pi-unipi/background-tasks 2.6.2 → 2.6.4

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 (59) hide show
  1. package/package.json +3 -2
  2. package/src/tools.ts +3 -1
  3. package/src/__tests__/anthropic-attribution.test.ts +0 -195
  4. package/src/__tests__/config.test.ts +0 -137
  5. package/src/__tests__/core.test.ts +0 -493
  6. package/src/__tests__/delegate-artifacts.test.ts +0 -528
  7. package/src/__tests__/delegate-budget.test.ts +0 -456
  8. package/src/__tests__/delegate-launch.test.ts +0 -676
  9. package/src/__tests__/delegate-result-package.test.ts +0 -350
  10. package/src/__tests__/delegate-seed.test.ts +0 -392
  11. package/src/__tests__/durable-fs.test.ts +0 -559
  12. package/src/__tests__/extension-api.test.ts +0 -579
  13. package/src/__tests__/fusion-artifacts.test.ts +0 -1039
  14. package/src/__tests__/fusion-budget.test.ts +0 -1356
  15. package/src/__tests__/fusion-claude-cache.test.ts +0 -320
  16. package/src/__tests__/fusion-config.test.ts +0 -335
  17. package/src/__tests__/fusion-context-prompts.test.ts +0 -670
  18. package/src/__tests__/fusion-evaluation.test.ts +0 -315
  19. package/src/__tests__/fusion-extraction-equivalence.test.ts +0 -58
  20. package/src/__tests__/fusion-golden-bytes.test.ts +0 -35
  21. package/src/__tests__/fusion-high-cardinality.test.ts +0 -192
  22. package/src/__tests__/fusion-model-selector.test.ts +0 -205
  23. package/src/__tests__/fusion-orchestrator.test.ts +0 -1194
  24. package/src/__tests__/fusion-rpc.test.ts +0 -369
  25. package/src/__tests__/fusion-sdk.test.ts +0 -1226
  26. package/src/__tests__/fusion-v5-core.test.ts +0 -219
  27. package/src/__tests__/fusion-validate-orchestrator.test.ts +0 -240
  28. package/src/__tests__/fusion-web-fetch.test.ts +0 -485
  29. package/src/__tests__/fusion-workflows.test.ts +0 -59
  30. package/src/__tests__/helpers/delegate-deterministic-seed.ts +0 -109
  31. package/src/__tests__/helpers/delegate-seed-subprocess.ts +0 -10
  32. package/src/__tests__/helpers/fusion-canonical-subprocess.ts +0 -21
  33. package/src/__tests__/helpers/fusion-canonical.ts +0 -140
  34. package/src/__tests__/helpers/fusion-fake-pi.ts +0 -279
  35. package/src/__tests__/helpers/fusion-golden-corpus.ts +0 -500
  36. package/src/__tests__/helpers/fusion-high-cardinality.ts +0 -140
  37. package/src/__tests__/helpers/normalize.ts +0 -22
  38. package/src/__tests__/helpers/pi-hook-contract-evidence.json +0 -18
  39. package/src/__tests__/pi-launch.test.ts +0 -202
  40. package/src/__tests__/registry.test.ts +0 -1580
  41. package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +0 -130
  42. package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +0 -631
  43. package/src/__tests__/scripted-provider/delegate-guard-provider.ts +0 -403
  44. package/src/__tests__/scripted-provider/follow-up.test.ts +0 -448
  45. package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +0 -132
  46. package/src/__tests__/scripted-provider/fusion-reason.test.ts +0 -310
  47. package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +0 -163
  48. package/src/__tests__/scripted-provider/hook-contract-provider.ts +0 -179
  49. package/src/__tests__/scripted-provider/hook-probe-a.ts +0 -3
  50. package/src/__tests__/scripted-provider/hook-probe-b.ts +0 -3
  51. package/src/__tests__/scripted-provider/hook-probe-extension.ts +0 -126
  52. package/src/__tests__/scripted-provider/output-recovery-provider.ts +0 -153
  53. package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +0 -18
  54. package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +0 -477
  55. package/src/__tests__/scripted-provider/runtime-guard-probe.ts +0 -28
  56. package/src/__tests__/scripted-provider/runtime-guard-provider.ts +0 -49
  57. package/src/__tests__/scripted-provider/scripted-provider-extension.ts +0 -408
  58. package/src/__tests__/task-manager.test.ts +0 -479
  59. package/src/__tests__/windows-taskkill.test.ts +0 -161
@@ -1,477 +0,0 @@
1
- import { describe, it, afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { existsSync } from 'node:fs';
4
- import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
5
- import { tmpdir } from 'node:os';
6
- import { join, resolve } from 'node:path';
7
- import {
8
- AuthStorage,
9
- createAgentSession,
10
- DefaultResourceLoader,
11
- ModelRegistry,
12
- SessionManager,
13
- SettingsManager,
14
- } from '@earendil-works/pi-coding-agent';
15
- import { parseJsonText } from '../../types.js';
16
- import { isolatedTestEnv } from '../helpers/normalize.js';
17
- import {
18
- HOOK_PROBE_INJECTED_TEXT,
19
- HOOK_PROBE_REPLACED_TOOL_TEXT,
20
- type HookProbeMode,
21
- } from './hook-probe-extension.js';
22
- import {
23
- DELEGATE_HOOK_CONTRACT_ID,
24
- DELEGATE_REQUIRED_HOOK_GUARANTEES,
25
- type DelegateHookContractEvidence,
26
- } from '../../delegate/hook-contract.js';
27
-
28
- /**
29
- * Pi hook-contract characterisation gate.
30
- *
31
- * The delegate child-side guard depends on documented Pi behaviour that must be
32
- * proven by execution rather than read from type declarations. This gate drives
33
- * a real Pi agent loop against a deterministic provider and records the observed
34
- * guarantees as durable evidence consumed by the delegate launch preflight.
35
- */
36
- const providerPath = resolve('tests/scripted-provider/hook-contract-provider.ts');
37
- const probeAPath = resolve('tests/scripted-provider/hook-probe-a.ts');
38
- const probeBPath = resolve('tests/scripted-provider/hook-probe-b.ts');
39
- const evidencePath = resolve('tests/scripted-provider/pi-hook-contract-evidence.json');
40
-
41
- const roots: string[] = [];
42
-
43
- type JsonObject = Record<PropertyKey, unknown>;
44
-
45
- interface ProbeRecord extends JsonObject {
46
- hook?: unknown;
47
- probeId?: unknown;
48
- providerCalls?: unknown;
49
- texts?: unknown;
50
- roles?: unknown;
51
- toolName?: unknown;
52
- toolCallId?: unknown;
53
- isError?: unknown;
54
- role?: unknown;
55
- }
56
-
57
- function isJsonObject(value: unknown): value is JsonObject {
58
- return typeof value === 'object' && value !== null && !Array.isArray(value);
59
- }
60
-
61
- function parseRecord(line: string): ProbeRecord {
62
- const parsed = parseJsonText(line);
63
- assert.ok(isJsonObject(parsed), 'hook probe record must be a JSON object');
64
- return parsed;
65
- }
66
-
67
- function stringArray(value: unknown): string[] {
68
- if (!Array.isArray(value)) return [];
69
- return value.filter((entry): entry is string => typeof entry === 'string');
70
- }
71
-
72
- interface Harness {
73
- session: Awaited<ReturnType<typeof createAgentSession>>['session'];
74
- logPath: string;
75
- restore: () => void;
76
- }
77
-
78
- interface HarnessOptions {
79
- mode: HookProbeMode;
80
- withTool?: boolean;
81
- }
82
-
83
- function restoreEnvValue(key: string, value: string | undefined): void {
84
- if (value === undefined) {
85
- Reflect.deleteProperty(process.env, key);
86
- return;
87
- }
88
- process.env[key] = value;
89
- }
90
-
91
- async function harness(options: HarnessOptions): Promise<Harness> {
92
- const root = await mkdtemp(join(tmpdir(), 'pi-bg-hook-contract-'));
93
- roots.push(root);
94
- const cwd = join(root, 'project');
95
- const agentDir = join(root, 'agent');
96
- const logPath = join(root, 'hook-probe.jsonl');
97
- await mkdir(cwd, { recursive: true });
98
- await mkdir(agentDir, { recursive: true });
99
- await writeFile(logPath, '', 'utf8');
100
-
101
- const previous = {
102
- mode: process.env['UNIPI_BG_HOOK_PROBE_MODE'],
103
- log: process.env['UNIPI_BG_HOOK_PROBE_LOG'],
104
- tool: process.env['UNIPI_BG_HOOK_PROBE_TOOL'],
105
- apiKey: process.env['UNIPI_BG_HOOK_CONTRACT_API_KEY'],
106
- };
107
- Object.assign(process.env, isolatedTestEnv, {
108
- UNIPI_BG_HOOK_PROBE_MODE: options.mode,
109
- UNIPI_BG_HOOK_PROBE_LOG: logPath,
110
- UNIPI_BG_HOOK_PROBE_TOOL: options.withTool === true ? '1' : '0',
111
- UNIPI_BG_HOOK_CONTRACT_API_KEY: 'hook-contract-api-key',
112
- });
113
-
114
- const settingsManager = SettingsManager.inMemory({
115
- defaultProvider: 'pi-bg-hook-contract',
116
- defaultModel: 'hook-contract-model',
117
- });
118
- const loader = new DefaultResourceLoader({
119
- cwd,
120
- agentDir,
121
- settingsManager,
122
- // Load order fixes handler ordering: provider, then probe-a, then probe-b.
123
- additionalExtensionPaths: [providerPath, probeAPath, probeBPath],
124
- noExtensions: true,
125
- noSkills: true,
126
- noPromptTemplates: true,
127
- noContextFiles: true,
128
- noThemes: true,
129
- });
130
- await loader.reload();
131
- const authStorage = AuthStorage.create(join(agentDir, 'auth.json'));
132
- const modelRegistry = ModelRegistry.create(authStorage);
133
- const { session } = await createAgentSession({
134
- cwd,
135
- agentDir,
136
- resourceLoader: loader,
137
- sessionManager: SessionManager.inMemory(cwd),
138
- settingsManager,
139
- authStorage,
140
- modelRegistry,
141
- noTools: 'builtin',
142
- });
143
- const model = modelRegistry.find('pi-bg-hook-contract', 'hook-contract-model');
144
- assert.ok(model, 'hook-contract provider model should be registered');
145
- await session.setModel(model);
146
- return {
147
- session,
148
- logPath,
149
- restore: () => {
150
- restoreEnvValue('UNIPI_BG_HOOK_PROBE_MODE', previous.mode);
151
- restoreEnvValue('UNIPI_BG_HOOK_PROBE_LOG', previous.log);
152
- restoreEnvValue('UNIPI_BG_HOOK_PROBE_TOOL', previous.tool);
153
- restoreEnvValue('UNIPI_BG_HOOK_CONTRACT_API_KEY', previous.apiKey);
154
- },
155
- };
156
- }
157
-
158
- async function dispose(h: Harness): Promise<void> {
159
- try {
160
- await h.session.extensionRunner.emit({ type: 'session_shutdown', reason: 'quit' });
161
- } finally {
162
- h.session.dispose();
163
- h.restore();
164
- }
165
- }
166
-
167
- async function records(logPath: string): Promise<ProbeRecord[]> {
168
- if (!existsSync(logPath)) return [];
169
- const raw = await readFile(logPath, 'utf8');
170
- return raw.trim().length === 0 ? [] : raw.trim().split('\n').map(parseRecord);
171
- }
172
-
173
- function hooksOf(rows: readonly ProbeRecord[], hook: string): ProbeRecord[] {
174
- return rows.filter((row) => row['hook'] === hook);
175
- }
176
-
177
- const observed = new Map<string, boolean>();
178
-
179
- function note(guarantee: string, value: boolean): void {
180
- observed.set(guarantee, value);
181
- }
182
-
183
- afterEach(async () => {
184
- await new Promise((resolve) => setTimeout(resolve, 100));
185
- for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true });
186
- });
187
-
188
- void describe('Pi hook contract characterisation', { concurrency: false }, () => {
189
- void it(
190
- 'fires context before every model call and delivers returned messages to the provider',
191
- { timeout: 20_000 },
192
- async () => {
193
- const h = await harness({ mode: 'context-replace', withTool: true });
194
- try {
195
- await h.session.prompt('probe the context hook');
196
- await h.session.agent.waitForIdle();
197
- const rows = await records(h.logPath);
198
- const providerCalls = hooksOf(rows, 'provider_call');
199
- const contextFires = hooksOf(rows, 'context').filter((row) => row['probeId'] === 'probe-a');
200
- assert.equal(
201
- providerCalls.length,
202
- 2,
203
- 'a tool-using turn should produce exactly two provider calls',
204
- );
205
- assert.equal(
206
- contextFires.length,
207
- providerCalls.length,
208
- 'context must fire exactly once before every model call',
209
- );
210
- // Ordering: each context fire strictly precedes its provider call.
211
- const ordered = rows.filter(
212
- (row) =>
213
- (row['hook'] === 'context' && row['probeId'] === 'probe-a') ||
214
- row['hook'] === 'provider_call',
215
- );
216
- for (let index = 0; index < ordered.length; index += 2) {
217
- assert.equal(ordered[index]?.['hook'], 'context');
218
- assert.equal(ordered[index + 1]?.['hook'], 'provider_call');
219
- }
220
- note('context_fires_before_every_model_call', true);
221
-
222
- for (const call of providerCalls) {
223
- assert.ok(
224
- stringArray(call['texts']).some((text) => text.includes(HOOK_PROBE_INJECTED_TEXT)),
225
- 'messages returned from a context handler must reach the provider',
226
- );
227
- }
228
- note('context_result_messages_reach_provider', true);
229
- } finally {
230
- await dispose(h);
231
- }
232
- },
233
- );
234
-
235
- void it(
236
- 'records whether throwing inside a context handler prevents the provider call',
237
- { timeout: 20_000 },
238
- async () => {
239
- const h = await harness({ mode: 'context-throw' });
240
- try {
241
- await h.session.prompt('probe the context throw path');
242
- await h.session.agent.waitForIdle();
243
- const rows = await records(h.logPath);
244
- const threw = hooksOf(rows, 'context_throwing').length > 0;
245
- assert.equal(threw, true, 'probe-a must have thrown from its context handler');
246
- const providerCalls = hooksOf(rows, 'provider_call');
247
- const blocked = providerCalls.length === 0;
248
- note('context_throw_blocks_provider_call', blocked);
249
- // Pi 0.83 catches context-handler exceptions and continues, so a throw is
250
- // NOT a dispatch barrier. Recorded, not assumed.
251
- assert.equal(
252
- blocked,
253
- false,
254
- 'Pi 0.83 is expected to swallow context-handler throws; if this changes the recorded evidence must be regenerated',
255
- );
256
- const laterProbeRan = hooksOf(rows, 'context').some((row) => row['probeId'] === 'probe-b');
257
- assert.equal(
258
- laterProbeRan,
259
- true,
260
- 'a throwing handler must not prevent later extensions from running',
261
- );
262
- note('context_throw_isolated_to_throwing_handler', laterProbeRan);
263
- } finally {
264
- await dispose(h);
265
- }
266
- },
267
- );
268
-
269
- void it(
270
- 'records whether ctx.abort() inside a context handler prevents the provider request',
271
- { timeout: 20_000 },
272
- async () => {
273
- const h = await harness({ mode: 'context-abort' });
274
- try {
275
- await h.session.prompt('probe the context abort path');
276
- await h.session.agent.waitForIdle();
277
- const rows = await records(h.logPath);
278
- assert.equal(hooksOf(rows, 'context_aborting').length > 0, true);
279
- const providerCalls = hooksOf(rows, 'provider_call');
280
- // Pi 0.81.1-0.83.0 invoke streamSimple with an already-aborted signal;
281
- // Pi 0.84.0 propagates the signal through auth resolution and skips the
282
- // provider entry point entirely. Both modes block transport. The exact
283
- // per-version behavior is pinned again by scripts/test-compat.ts.
284
- const skippedStreamInvocation = providerCalls.length === 0;
285
- note('context_abort_skips_stream_invocation', skippedStreamInvocation);
286
- const everyDispatchedCallAborted = providerCalls.every(
287
- (call) => call['signalAborted'] === true,
288
- );
289
- assert.equal(
290
- skippedStreamInvocation || everyDispatchedCallAborted,
291
- true,
292
- 'ctx.abort() must either skip stream invocation or hand every dispatched call an already-aborted signal',
293
- );
294
- note('context_abort_blocks_provider_call', true);
295
-
296
- // The run must also terminate rather than continuing to further turns.
297
- assert.ok(
298
- providerCalls.length <= 1,
299
- 'an aborted context handler must not allow the agent loop to keep issuing model calls',
300
- );
301
- note('context_abort_terminates_run', true);
302
- } finally {
303
- await dispose(h);
304
- }
305
- },
306
- );
307
-
308
- void it(
309
- 'fires tool_result before the result enters the transcript and honours content replacement',
310
- { timeout: 20_000 },
311
- async () => {
312
- const h = await harness({ mode: 'tool-result-replace', withTool: true });
313
- try {
314
- await h.session.prompt('probe the tool_result hook');
315
- await h.session.agent.waitForIdle();
316
- const rows = await records(h.logPath);
317
- const toolResults = hooksOf(rows, 'tool_result');
318
- assert.equal(toolResults.length, 2, 'both probes must observe the tool result');
319
- const first = toolResults[0];
320
- const second = toolResults[1];
321
- assert.ok(first && second);
322
- assert.equal(first['probeId'], 'probe-a');
323
- assert.equal(second['probeId'], 'probe-b');
324
- assert.ok(
325
- stringArray(first['texts']).some((text) => text.includes('ORIGINAL_TOOL_PAYLOAD:seed')),
326
- 'the first handler must see the original tool payload',
327
- );
328
- assert.ok(
329
- stringArray(second['texts']).some((text) => text === HOOK_PROBE_REPLACED_TOOL_TEXT),
330
- 'later handlers must observe the earlier handler replacement (middleware chaining)',
331
- );
332
- note('tool_result_chains_in_load_order', true);
333
-
334
- // tool_result must run before tool_execution_end and before the result
335
- // reaches the provider transcript.
336
- const executionEndIndex = rows.findIndex((row) => row['hook'] === 'tool_execution_end');
337
- const lastToolResultIndex = rows.reduce(
338
- (last, row, index) => (row['hook'] === 'tool_result' ? index : last),
339
- -1,
340
- );
341
- assert.ok(
342
- lastToolResultIndex >= 0 && lastToolResultIndex < executionEndIndex,
343
- 'tool_result must fire before tool_execution_end',
344
- );
345
- note('tool_result_fires_before_transcript_entry', true);
346
-
347
- const followUpCall = hooksOf(rows, 'provider_call')[1];
348
- assert.ok(followUpCall, 'the tool turn must produce a follow-up provider call');
349
- const followUpTexts = stringArray(followUpCall['texts']);
350
- assert.ok(
351
- followUpTexts.some((text) => text.includes(HOOK_PROBE_REPLACED_TOOL_TEXT)),
352
- 'the replaced tool-result content must be what reaches the provider',
353
- );
354
- assert.ok(
355
- followUpTexts.every((text) => !text.includes('ORIGINAL_TOOL_PAYLOAD:seed')),
356
- 'the original tool payload must never reach the provider after replacement',
357
- );
358
- note('tool_result_replacement_reaches_provider', true);
359
- } finally {
360
- await dispose(h);
361
- }
362
- },
363
- );
364
-
365
- void it(
366
- 'preserves tool-call identity, role, and error flag after replacement',
367
- { timeout: 20_000 },
368
- async () => {
369
- const h = await harness({ mode: 'tool-result-replace', withTool: true });
370
- try {
371
- await h.session.prompt('probe tool result identity');
372
- await h.session.agent.waitForIdle();
373
- const rows = await records(h.logPath);
374
- const toolResults = hooksOf(rows, 'tool_result');
375
- const executionEnd = hooksOf(rows, 'tool_execution_end');
376
- const toolCallIds = new Set(toolResults.map((row) => row['toolCallId']));
377
- assert.equal(toolCallIds.size, 1, 'the tool call id must be stable across handlers');
378
- assert.equal([...toolCallIds][0], 'probe-call-1');
379
- for (const row of toolResults) assert.equal(row['isError'], false);
380
- for (const row of executionEnd) {
381
- assert.equal(row['toolCallId'], 'probe-call-1');
382
- assert.equal(row['isError'], false);
383
- }
384
- const entries: readonly unknown[] = h.session.sessionManager.getEntries();
385
- const toolResultRoles = entries.flatMap((entry) => {
386
- if (!isJsonObject(entry) || entry['type'] !== 'message') return [];
387
- const message = entry['message'];
388
- if (!isJsonObject(message) || message['role'] !== 'toolResult') return [];
389
- return [message];
390
- });
391
- assert.equal(toolResultRoles.length, 1);
392
- const toolResultMessage = toolResultRoles[0];
393
- assert.ok(toolResultMessage);
394
- assert.equal(toolResultMessage['role'], 'toolResult');
395
- assert.equal(toolResultMessage['toolCallId'], 'probe-call-1');
396
- assert.equal(toolResultMessage['toolName'], 'probe_echo');
397
- assert.equal(toolResultMessage['isError'], false);
398
- const content = toolResultMessage['content'];
399
- assert.ok(Array.isArray(content));
400
- const contentTexts = content.flatMap((part) =>
401
- isJsonObject(part) && typeof part['text'] === 'string' ? [part['text']] : [],
402
- );
403
- assert.deepEqual(contentTexts, [HOOK_PROBE_REPLACED_TOOL_TEXT]);
404
- note('tool_result_replacement_preserves_identity', true);
405
- } finally {
406
- await dispose(h);
407
- }
408
- },
409
- );
410
-
411
- void it(
412
- 'fires handlers from separate extensions in deterministic load order',
413
- { timeout: 20_000 },
414
- async () => {
415
- const h = await harness({ mode: 'observe', withTool: true });
416
- try {
417
- await h.session.prompt('probe handler ordering');
418
- await h.session.agent.waitForIdle();
419
- const rows = await records(h.logPath);
420
- const contextProbes = hooksOf(rows, 'context').map((row) => row['probeId']);
421
- assert.deepEqual(
422
- contextProbes,
423
- ['probe-a', 'probe-b', 'probe-a', 'probe-b'],
424
- 'context handlers must run in extension load order for every model call',
425
- );
426
- const toolProbes = hooksOf(rows, 'tool_result').map((row) => row['probeId']);
427
- assert.deepEqual(toolProbes, ['probe-a', 'probe-b']);
428
- note('handlers_run_in_extension_load_order', true);
429
- } finally {
430
- await dispose(h);
431
- }
432
- },
433
- );
434
-
435
- void it('matches the supported-range hook-contract evidence consumed by delegate preflight', async () => {
436
- for (const guarantee of DELEGATE_REQUIRED_HOOK_GUARANTEES) {
437
- assert.equal(
438
- observed.get(guarantee),
439
- true,
440
- `required delegate hook guarantee "${guarantee}" was not observed by this gate`,
441
- );
442
- }
443
- const throwBlocks = observed.get('context_throw_blocks_provider_call');
444
- assert.equal(typeof throwBlocks, 'boolean', 'throw behaviour must have been observed');
445
- const evidence: DelegateHookContractEvidence = {
446
- schema_version: 'pi-background-tasks.delegate-hook-contract.v1',
447
- contract_id: DELEGATE_HOOK_CONTRACT_ID,
448
- guarantees: {
449
- context_fires_before_every_model_call: true,
450
- context_result_messages_reach_provider: true,
451
- context_abort_blocks_provider_call: true,
452
- // The shipped evidence is the conservative contract shared by every
453
- // supported Pi line. Pi 0.84 skips the call, but 0.81.1-0.83.0 do not.
454
- context_abort_skips_stream_invocation: false,
455
- context_abort_terminates_run: true,
456
- context_throw_blocks_provider_call: throwBlocks === true,
457
- context_throw_isolated_to_throwing_handler: true,
458
- tool_result_fires_before_transcript_entry: true,
459
- tool_result_replacement_reaches_provider: true,
460
- tool_result_replacement_preserves_identity: true,
461
- tool_result_chains_in_load_order: true,
462
- handlers_run_in_extension_load_order: true,
463
- },
464
- };
465
- const existing = existsSync(evidencePath) ? await readFile(evidencePath, 'utf8') : undefined;
466
- const serialized = `${JSON.stringify(evidence, null, 2)}\n`;
467
- if (existing === undefined) {
468
- await writeFile(evidencePath, serialized, 'utf8');
469
- return;
470
- }
471
- assert.equal(
472
- existing,
473
- serialized,
474
- 'the recorded Pi hook-contract evidence no longer matches observed Pi behaviour; regenerate it deliberately and re-review the delegate child guard',
475
- );
476
- });
477
- });
@@ -1,28 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { appendFileSync } from 'node:fs';
3
- import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
4
- import { RUNTIME_GUARD_MARKER } from './runtime-guard-provider.js';
5
-
6
- function record(value: Record<string, unknown>): void {
7
- const path = process.env['UNIPI_BG_RUNTIME_GUARD_LOG'];
8
- if (path) appendFileSync(path, `${JSON.stringify(value)}\n`, 'utf8');
9
- }
10
-
11
- export default function runtimeGuardProbe(pi: ExtensionAPI): void {
12
- pi.on('before_provider_request', (event, ctx) => {
13
- const serialized = JSON.stringify(event.payload);
14
- if (serialized === undefined)
15
- throw new Error('runtime guard probe payload serialized to undefined');
16
- const payload = event.payload as Record<string, unknown>;
17
- record({
18
- hook: 'governor',
19
- marker_seen: payload['pi_bg_runtime_guard_marker'] === RUNTIME_GUARD_MARKER,
20
- payload_bytes: Buffer.byteLength(serialized, 'utf8'),
21
- payload_sha256: createHash('sha256').update(serialized, 'utf8').digest('hex'),
22
- provider: ctx.model?.provider,
23
- model: ctx.model?.id,
24
- });
25
- ctx.abort();
26
- return event.payload;
27
- });
28
- }
@@ -1,49 +0,0 @@
1
- import { appendFileSync } from 'node:fs';
2
- import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
3
-
4
- export const RUNTIME_GUARD_PROVIDER = 'pi-bg-runtime-guard';
5
- export const RUNTIME_GUARD_MODEL = 'runtime-guard-model';
6
- export const RUNTIME_GUARD_MARKER = 'MUTATOR_RAN_BEFORE_GOVERNOR';
7
-
8
- function record(value: Record<string, unknown>): void {
9
- const path = process.env['UNIPI_BG_RUNTIME_GUARD_LOG'];
10
- if (path) appendFileSync(path, `${JSON.stringify(value)}\n`, 'utf8');
11
- }
12
-
13
- export default function runtimeGuardProvider(pi: ExtensionAPI): void {
14
- const baseUrl = process.env['UNIPI_BG_RUNTIME_GUARD_BASE_URL'];
15
- if (!baseUrl) throw new Error('UNIPI_BG_RUNTIME_GUARD_BASE_URL is required');
16
-
17
- pi.registerProvider(RUNTIME_GUARD_PROVIDER, {
18
- name: 'Pi Background Tasks Runtime Guard Probe',
19
- baseUrl,
20
- apiKey: '$UNIPI_BG_RUNTIME_GUARD_API_KEY',
21
- // Exercise the same Pi transport adapter used by Fusion's subscription Codex routes.
22
- api: 'openai-codex-responses',
23
- models: [
24
- {
25
- id: RUNTIME_GUARD_MODEL,
26
- name: 'Runtime Guard Model',
27
- reasoning: false,
28
- input: ['text'],
29
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
30
- contextWindow: 200_000,
31
- maxTokens: 4_096,
32
- },
33
- ],
34
- });
35
-
36
- // This extension deliberately loads before the governor probe. The test proves
37
- // before_provider_request handlers form a payload-transform chain in load order.
38
- pi.on('before_provider_request', (event) => {
39
- record({ hook: 'mutator', payload_type: typeof event.payload });
40
- if (
41
- typeof event.payload !== 'object' ||
42
- event.payload === null ||
43
- Array.isArray(event.payload)
44
- ) {
45
- throw new Error('runtime guard provider payload must be an object');
46
- }
47
- return { ...event.payload, pi_bg_runtime_guard_marker: RUNTIME_GUARD_MARKER };
48
- });
49
- }