pi-fold 0.0.1 → 0.1.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.
@@ -0,0 +1,673 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import {
4
+ chmod,
5
+ link,
6
+ mkdir,
7
+ open,
8
+ realpath,
9
+ rm,
10
+ stat,
11
+ } from "node:fs/promises";
12
+ import { dirname, isAbsolute, join } from "node:path";
13
+ import { denseOwnArrayValues, evidenceValue, isPlainRecord, ownDataProperty, stableStringify } from "./json.ts";
14
+ import { DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX, entryTypeNamespace } from "./lib/policy.ts";
15
+
16
+ export const EVIDENCE_REFERENCE_VERSION = 1;
17
+ export const TOOL_RESULT_PROJECTION_BYTES = 16 * 1024;
18
+ export const AGENT_RESULT_PROJECTION_BYTES = 24 * 1024;
19
+ export const MAX_EVIDENCE_SOURCE_BYTES = 512 * 1024 * 1024;
20
+
21
+ const AGENT_TEXT_PROJECTION_BYTES = 4 * 1024;
22
+ const AGENT_DETAILS_PROJECTION_BYTES = 12 * 1024;
23
+ const MAX_AGENT_JOBS = 8;
24
+ const MAX_AGENT_NODES_PER_JOB = 4;
25
+ const MAX_AGENT_PROFILES = 16;
26
+ const OWNER_KINDS = new Set(["pi-session", "pi-subagents", "pi-fold-mcp"]);
27
+ const DEFAULT_EVIDENCE_DIRECTORY = "pi-fold-evidence";
28
+ const evidenceProjectors = new Map();
29
+
30
+ export function registerEvidenceProjector(toolName, projector) {
31
+ if (typeof toolName !== "string" || !toolName.trim()) {
32
+ throw new Error("Evidence projector toolName must be a non-empty string");
33
+ }
34
+ if (typeof projector !== "function") {
35
+ throw new Error("Evidence projector must be a function");
36
+ }
37
+ evidenceProjectors.set(toolName, projector);
38
+ }
39
+
40
+ const SOURCE_CHUNK_BYTES = 1024 * 1024;
41
+ const defineProperty = Object.defineProperty;
42
+ const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
43
+ const numberIsSafeInteger = Number.isSafeInteger;
44
+ const ownKeys = Reflect.ownKeys;
45
+
46
+ function appendValue(values, value) {
47
+ defineProperty(values, String(values.length), {
48
+ value,
49
+ enumerable: true,
50
+ configurable: true,
51
+ writable: true,
52
+ });
53
+ }
54
+
55
+ function shortenString(value, maximumBytes) {
56
+ return typeof value === "string" ? utf8Head(value, maximumBytes) : undefined;
57
+ }
58
+
59
+ function utf8Bytes(value) {
60
+ return Buffer.byteLength(value, "utf8");
61
+ }
62
+
63
+ export function utf8Head(value, maximumBytes) {
64
+ const source = Buffer.from(value, "utf8");
65
+ if (source.length <= maximumBytes) return value;
66
+ let end = maximumBytes;
67
+ while (end > 0 && (source[end] & 0xc0) === 0x80) end -= 1;
68
+ return source.subarray(0, end).toString("utf8");
69
+ }
70
+
71
+ export function utf8Tail(value, maximumBytes) {
72
+ const source = Buffer.from(value, "utf8");
73
+ if (source.length <= maximumBytes) return value;
74
+ let start = source.length - maximumBytes;
75
+ while (start < source.length && (source[start] & 0xc0) === 0x80) start += 1;
76
+ return source.subarray(start).toString("utf8");
77
+ }
78
+
79
+ function safeSegment(value, label) {
80
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a nonempty string`);
81
+ return value.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 120);
82
+ }
83
+
84
+ function extensionFor(mediaType) {
85
+ if (mediaType === "application/json") return ".json";
86
+ if (mediaType?.startsWith("text/")) return ".txt";
87
+ return ".bin";
88
+ }
89
+
90
+ async function verifyArtifact(path, sha256, bytes) {
91
+ const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
92
+ try {
93
+ const file = await handle.stat({ bigint: true });
94
+ if (!file.isFile() || file.size !== BigInt(bytes)) throw new Error(`Evidence artifact size drift at ${path}`);
95
+ const observedArtifact = await hashOpenFile(handle, bytes);
96
+ if (observedArtifact.bytes !== bytes || observedArtifact.sha256 !== sha256) {
97
+ throw new Error(`Evidence artifact digest drift at ${path}`);
98
+ }
99
+ } finally {
100
+ await handle.close();
101
+ }
102
+ }
103
+
104
+ async function fsyncDirectory(path) {
105
+ const handle = await open(path, fsConstants.O_RDONLY);
106
+ try { await handle.sync(); } finally { await handle.close(); }
107
+ }
108
+
109
+ async function installTemporary(root, temporary, sha256, bytes, mediaType, ownerKind) {
110
+ const directory = join(root, safeSegment(ownerKind, "owner kind"), "sha256", sha256.slice(0, 2));
111
+ await mkdir(directory, { recursive: true, mode: 0o700 });
112
+ const path = join(directory, `${sha256}${extensionFor(mediaType)}`);
113
+ try {
114
+ await link(temporary, path);
115
+ await chmod(path, 0o444);
116
+ await fsyncDirectory(directory);
117
+ } catch (error) {
118
+ if (error?.code !== "EEXIST") throw error;
119
+ await verifyArtifact(path, sha256, bytes);
120
+ } finally {
121
+ await rm(temporary, { force: true }).catch(() => undefined);
122
+ }
123
+ return path;
124
+ }
125
+
126
+ async function temporaryPath(root) {
127
+ const directory = join(root, ".tmp");
128
+ await mkdir(directory, { recursive: true, mode: 0o700 });
129
+ return join(directory, `.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
130
+ }
131
+
132
+ function ownerRoot(
133
+ sessionFile,
134
+ ownerKind,
135
+ evidenceDirectory = DEFAULT_EVIDENCE_DIRECTORY,
136
+ additionalOwnerKind = undefined,
137
+ ) {
138
+ if (typeof sessionFile !== "string" || !isAbsolute(sessionFile)) {
139
+ throw new Error("Evidence ingestion requires an absolute Pi session file");
140
+ }
141
+ if (!OWNER_KINDS.has(ownerKind) && ownerKind !== additionalOwnerKind) {
142
+ throw new Error(`Unsupported evidence owner kind: ${ownerKind}`);
143
+ }
144
+ if (safeSegment(evidenceDirectory, "evidence directory") !== evidenceDirectory) {
145
+ throw new Error("Evidence directory must be one safe path segment");
146
+ }
147
+ return join(
148
+ dirname(sessionFile),
149
+ ownerKind === "pi-subagents" ? "subagent-artifacts/.evidence" : evidenceDirectory,
150
+ );
151
+ }
152
+
153
+ function baseDescriptor({ ownerKind, ownerId, path, sha256, bytes, mediaType, encoding, source }) {
154
+ return {
155
+ version: EVIDENCE_REFERENCE_VERSION,
156
+ owner: { kind: ownerKind, id: ownerId },
157
+ artifactId: `sha256:${sha256}`,
158
+ path,
159
+ sha256,
160
+ bytes,
161
+ mediaType,
162
+ encoding,
163
+ range: null,
164
+ source,
165
+ projection: {
166
+ strategy: "none",
167
+ sourceBytes: bytes,
168
+ replacementBytes: bytes,
169
+ },
170
+ };
171
+ }
172
+
173
+ export async function writeEvidenceArtifact({
174
+ sessionFile,
175
+ ownerKind,
176
+ ownerId,
177
+ bytes,
178
+ mediaType = "application/octet-stream",
179
+ encoding = "binary",
180
+ source,
181
+ evidenceDirectory = DEFAULT_EVIDENCE_DIRECTORY,
182
+ additionalOwnerKind,
183
+ }) {
184
+ const value = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
185
+ if (value.length > MAX_EVIDENCE_SOURCE_BYTES) throw new Error(`Evidence exceeds ${MAX_EVIDENCE_SOURCE_BYTES} bytes`);
186
+ const root = ownerRoot(sessionFile, ownerKind, evidenceDirectory, additionalOwnerKind);
187
+ const temporary = await temporaryPath(root);
188
+ const handle = await open(temporary, "wx", 0o600);
189
+ try {
190
+ await handle.writeFile(value);
191
+ await handle.sync();
192
+ } finally {
193
+ await handle.close();
194
+ }
195
+ const sha256 = createHash("sha256").update(value).digest("hex");
196
+ const path = await installTemporary(root, temporary, sha256, value.length, mediaType, ownerKind);
197
+ return baseDescriptor({ ownerKind, ownerId, path, sha256, bytes: value.length, mediaType, encoding, source });
198
+ }
199
+
200
+ function sameIdentity(left, right) {
201
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size &&
202
+ left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
203
+ }
204
+
205
+ async function hashOpenFile(handle, maximumBytes = MAX_EVIDENCE_SOURCE_BYTES) {
206
+ const hash = createHash("sha256");
207
+ let position = 0;
208
+ while (true) {
209
+ const buffer = Buffer.allocUnsafe(Math.min(SOURCE_CHUNK_BYTES, maximumBytes - position + 1));
210
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
211
+ if (bytesRead === 0) break;
212
+ position += bytesRead;
213
+ if (position > maximumBytes) throw new Error(`Evidence source exceeds ${maximumBytes} bytes`);
214
+ hash.update(buffer.subarray(0, bytesRead));
215
+ }
216
+ return { sha256: hash.digest("hex"), bytes: position };
217
+ }
218
+
219
+ async function copyAndHashOpenFile(sourceHandle, destinationHandle, maximumBytes = MAX_EVIDENCE_SOURCE_BYTES) {
220
+ const hash = createHash("sha256");
221
+ let position = 0;
222
+ while (true) {
223
+ const buffer = Buffer.allocUnsafe(Math.min(SOURCE_CHUNK_BYTES, maximumBytes - position + 1));
224
+ const { bytesRead } = await sourceHandle.read(buffer, 0, buffer.length, position);
225
+ if (bytesRead === 0) break;
226
+ if (position + bytesRead > maximumBytes) throw new Error(`Evidence source exceeds ${maximumBytes} bytes`);
227
+ let written = 0;
228
+ while (written < bytesRead) {
229
+ const result = await destinationHandle.write(buffer, written, bytesRead - written, position + written);
230
+ if (!result.bytesWritten) throw new Error("Evidence snapshot write made no progress");
231
+ written += result.bytesWritten;
232
+ }
233
+ hash.update(buffer.subarray(0, bytesRead));
234
+ position += bytesRead;
235
+ }
236
+ return { sha256: hash.digest("hex"), bytes: position };
237
+ }
238
+
239
+ export async function pinEvidenceFile({
240
+ sessionFile,
241
+ ownerKind,
242
+ ownerId,
243
+ sourcePath,
244
+ mediaType = "text/plain",
245
+ encoding = "utf-8",
246
+ source,
247
+ evidenceDirectory = DEFAULT_EVIDENCE_DIRECTORY,
248
+ additionalOwnerKind,
249
+ }) {
250
+ if (typeof sourcePath !== "string" || !isAbsolute(sourcePath)) throw new Error("Evidence source path must be absolute");
251
+ const canonicalSource = await realpath(sourcePath);
252
+ const root = ownerRoot(sessionFile, ownerKind, evidenceDirectory, additionalOwnerKind);
253
+ const temporary = await temporaryPath(root);
254
+ let sourceHandle;
255
+ let destinationHandle;
256
+ let snapshot;
257
+ let failure;
258
+ try {
259
+ sourceHandle = await open(canonicalSource, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
260
+ destinationHandle = await open(temporary, "wx", 0o600);
261
+ const sourceBeforeCopy = await sourceHandle.stat({ bigint: true });
262
+ if (!sourceBeforeCopy.isFile()) throw new Error(`Evidence source is not a regular file: ${canonicalSource}`);
263
+ snapshot = await copyAndHashOpenFile(sourceHandle, destinationHandle);
264
+ await destinationHandle.sync();
265
+ const sourceAfterCopy = await sourceHandle.stat({ bigint: true });
266
+ const namedSource = await stat(canonicalSource, { bigint: true });
267
+ if (!sameIdentity(sourceBeforeCopy, sourceAfterCopy) || !sameIdentity(sourceAfterCopy, namedSource) ||
268
+ BigInt(snapshot.bytes) !== namedSource.size) {
269
+ throw new Error(`Evidence source changed while snapshotting: ${canonicalSource}`);
270
+ }
271
+ } catch (error) {
272
+ failure = error;
273
+ } finally {
274
+ await sourceHandle?.close().catch((error) => { failure ??= error; });
275
+ await destinationHandle?.close().catch((error) => { failure ??= error; });
276
+ }
277
+ if (failure) {
278
+ await rm(temporary, { force: true }).catch(() => undefined);
279
+ throw failure;
280
+ }
281
+ const path = await installTemporary(root, temporary, snapshot.sha256, snapshot.bytes, mediaType, ownerKind);
282
+ return baseDescriptor({
283
+ ownerKind,
284
+ ownerId,
285
+ path,
286
+ sha256: snapshot.sha256,
287
+ bytes: snapshot.bytes,
288
+ mediaType,
289
+ encoding,
290
+ source,
291
+ });
292
+ }
293
+
294
+ function textContent(content) {
295
+ const parts = denseOwnArrayValues(content);
296
+ if (!parts) return "";
297
+ let text = "";
298
+ for (let index = 0; index < parts.length; index += 1) {
299
+ const part = parts[index];
300
+ const value = ownDataProperty(part, "text");
301
+ if (ownDataProperty(part, "type") !== "text" || typeof value !== "string") continue;
302
+ if (text) text += "\n";
303
+ text += value;
304
+ }
305
+ return text;
306
+ }
307
+
308
+ function hasImage(content) {
309
+ const parts = denseOwnArrayValues(content);
310
+ if (!parts) return false;
311
+ for (let index = 0; index < parts.length; index += 1) {
312
+ if (ownDataProperty(parts[index], "type") === "image") return true;
313
+ }
314
+ return false;
315
+ }
316
+
317
+ function referenceLine(descriptor) {
318
+ return `[Exact evidence: ${descriptor.path}; sha256=${descriptor.sha256}; bytes=${descriptor.bytes}]`;
319
+ }
320
+
321
+ function replacementBytes(content, details) {
322
+ return utf8Bytes(stableStringify({ content: evidenceValue(content), details: evidenceValue(details) }));
323
+ }
324
+
325
+ function copyOwnDataRecord(value) {
326
+ const copied = {};
327
+ if (!isPlainRecord(value)) return copied;
328
+ const keys = ownKeys(value);
329
+ for (let index = 0; index < keys.length && index < 64; index += 1) {
330
+ const key = keys[index];
331
+ if (typeof key !== "string") continue;
332
+ const descriptor = getOwnPropertyDescriptor(value, key);
333
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) continue;
334
+ defineProperty(copied, key, {
335
+ value: descriptor.value,
336
+ enumerable: true,
337
+ configurable: true,
338
+ writable: true,
339
+ });
340
+ }
341
+ return copied;
342
+ }
343
+
344
+ export function projectEvidenceText(
345
+ text,
346
+ descriptor,
347
+ strategy,
348
+ originalDetails = undefined,
349
+ tail = false,
350
+ maximumPreviewBytes = TOOL_RESULT_PROJECTION_BYTES,
351
+ ) {
352
+ const preview = tail ? utf8Tail(text, maximumPreviewBytes) : utf8Head(text, maximumPreviewBytes);
353
+ const content = [{ type: "text", text: `${preview}\n\n${referenceLine(descriptor)}` }];
354
+ const details = copyOwnDataRecord(originalDetails);
355
+ details.evidence = descriptor;
356
+ descriptor.projection.strategy = strategy;
357
+ for (let index = 0; index < 4; index += 1) {
358
+ const next = replacementBytes(content, details);
359
+ if (next === descriptor.projection.replacementBytes) break;
360
+ descriptor.projection.replacementBytes = next;
361
+ }
362
+ return { content, details };
363
+ }
364
+
365
+ function shrinkArray(values) {
366
+ if (values.length > 0) defineProperty(values, "length", { value: values.length - 1, writable: true });
367
+ }
368
+
369
+ function boundedNode(node) {
370
+ if (!isPlainRecord(node)) return undefined;
371
+ const response = ownDataProperty(node, "response");
372
+ return {
373
+ id: shortenString(ownDataProperty(node, "id"), 128),
374
+ profile: shortenString(ownDataProperty(node, "profile"), 96),
375
+ mode: shortenString(ownDataProperty(node, "mode"), 48),
376
+ status: shortenString(ownDataProperty(node, "status"), 64),
377
+ provider: shortenString(ownDataProperty(node, "provider"), 96),
378
+ model: shortenString(ownDataProperty(node, "model"), 160),
379
+ thinking: shortenString(ownDataProperty(node, "thinking"), 48),
380
+ tier: shortenString(ownDataProperty(node, "tier"), 48),
381
+ contractDigest: shortenString(ownDataProperty(node, "contractDigest"), 64),
382
+ launchContractDigest: shortenString(ownDataProperty(node, "launchContractDigest"), 64),
383
+ runId: shortenString(ownDataProperty(response, "runId") ?? ownDataProperty(node, "packageRunId"), 128),
384
+ outputPath: shortenString(ownDataProperty(node, "outputPath"), 512),
385
+ error: shortenString(ownDataProperty(node, "error"), 512),
386
+ };
387
+ }
388
+
389
+ function boundedJob(job) {
390
+ if (!isPlainRecord(job)) return undefined;
391
+ const sourceNodes = denseOwnArrayValues(ownDataProperty(job, "nodes"));
392
+ const nodes = [];
393
+ if (sourceNodes) {
394
+ for (let index = 0; index < sourceNodes.length && nodes.length < MAX_AGENT_NODES_PER_JOB; index += 1) {
395
+ const node = boundedNode(sourceNodes[index]);
396
+ if (node) appendValue(nodes, node);
397
+ }
398
+ }
399
+ return {
400
+ schemaVersion: numberIsSafeInteger(ownDataProperty(job, "schemaVersion")) ? ownDataProperty(job, "schemaVersion") : undefined,
401
+ jobId: shortenString(ownDataProperty(job, "jobId"), 128),
402
+ status: shortenString(ownDataProperty(job, "status"), 64),
403
+ mode: shortenString(ownDataProperty(job, "mode"), 48),
404
+ provider: shortenString(ownDataProperty(job, "provider"), 96),
405
+ parentSessionId: shortenString(ownDataProperty(job, "parentSessionId"), 128),
406
+ createdAt: shortenString(ownDataProperty(job, "createdAt"), 64),
407
+ updatedAt: shortenString(ownDataProperty(job, "updatedAt"), 64),
408
+ packageRunId: shortenString(ownDataProperty(job, "packageRunId"), 128),
409
+ packageAsyncDir: shortenString(ownDataProperty(job, "packageAsyncDir"), 512),
410
+ error: shortenString(ownDataProperty(job, "error"), 512),
411
+ nodesTotal: sourceNodes?.length ?? 0,
412
+ nodes,
413
+ };
414
+ }
415
+
416
+ function boundedAgentDetails(details) {
417
+ if (!isPlainRecord(details)) return {};
418
+ const result = {};
419
+ const job = boundedJob(ownDataProperty(details, "job"));
420
+ if (job) result.job = job;
421
+
422
+ const sourceJobs = denseOwnArrayValues(ownDataProperty(details, "jobs"));
423
+ const jobs = [];
424
+ if (sourceJobs) {
425
+ for (let index = 0; index < sourceJobs.length && jobs.length < MAX_AGENT_JOBS; index += 1) {
426
+ const projected = boundedJob(sourceJobs[index]);
427
+ if (projected) appendValue(jobs, projected);
428
+ }
429
+ result.jobs = jobs;
430
+ result.jobsTotal = sourceJobs.length;
431
+ result.jobsShown = jobs.length;
432
+ }
433
+
434
+ const sourceProfiles = denseOwnArrayValues(ownDataProperty(details, "profiles"));
435
+ const profiles = [];
436
+ if (sourceProfiles) {
437
+ for (let index = 0; index < sourceProfiles.length && profiles.length < MAX_AGENT_PROFILES; index += 1) {
438
+ const profile = sourceProfiles[index];
439
+ if (!isPlainRecord(profile)) continue;
440
+ appendValue(profiles, {
441
+ name: shortenString(ownDataProperty(profile, "name"), 96),
442
+ description: shortenString(ownDataProperty(profile, "description"), 256),
443
+ mode: shortenString(ownDataProperty(profile, "mode"), 48),
444
+ defaultTier: shortenString(ownDataProperty(profile, "defaultTier"), 48),
445
+ });
446
+ }
447
+ result.profiles = profiles;
448
+ result.profilesTotal = sourceProfiles.length;
449
+ result.profilesShown = profiles.length;
450
+ }
451
+
452
+ const workflow = ownDataProperty(details, "workflow");
453
+ if (isPlainRecord(workflow)) {
454
+ result.workflow = {
455
+ mode: shortenString(ownDataProperty(workflow, "mode"), 48),
456
+ packageRunId: shortenString(ownDataProperty(workflow, "packageRunId"), 128),
457
+ packageAsyncDir: shortenString(ownDataProperty(workflow, "packageAsyncDir"), 512),
458
+ status: shortenString(ownDataProperty(workflow, "status"), 64),
459
+ };
460
+ }
461
+ result.packageStatus = shortenString(ownDataProperty(details, "packageStatus"), 512);
462
+ result.jobId = shortenString(ownDataProperty(details, "jobId"), 128);
463
+
464
+ while (utf8Bytes(stableStringify(result)) > AGENT_DETAILS_PROJECTION_BYTES) {
465
+ if (jobs.length > 1) {
466
+ shrinkArray(jobs);
467
+ result.jobsShown = jobs.length;
468
+ continue;
469
+ }
470
+ if (job?.nodes?.length > 0) {
471
+ shrinkArray(job.nodes);
472
+ continue;
473
+ }
474
+ if (jobs[0]?.nodes?.length > 0) {
475
+ shrinkArray(jobs[0].nodes);
476
+ continue;
477
+ }
478
+ if (jobs.length > 0) {
479
+ shrinkArray(jobs);
480
+ result.jobsShown = jobs.length;
481
+ continue;
482
+ }
483
+ if (profiles.length > 0) {
484
+ shrinkArray(profiles);
485
+ result.profilesShown = profiles.length;
486
+ continue;
487
+ }
488
+ if (result.packageStatus !== undefined) {
489
+ delete result.packageStatus;
490
+ continue;
491
+ }
492
+ if (result.workflow !== undefined) {
493
+ delete result.workflow;
494
+ continue;
495
+ }
496
+ if (result.job !== undefined) {
497
+ delete result.job;
498
+ continue;
499
+ }
500
+ break;
501
+ }
502
+ return result;
503
+ }
504
+
505
+ function boundedTruncation(value) {
506
+ if (!isPlainRecord(value)) return undefined;
507
+ const bounded = {};
508
+ const stringFields = ["truncatedBy"];
509
+ const integerFields = ["totalLines", "totalBytes", "outputLines", "outputBytes", "maxLines", "maxBytes"];
510
+ const booleanFields = ["truncated", "lastLinePartial", "firstLineExceedsLimit"];
511
+ for (let index = 0; index < stringFields.length; index += 1) {
512
+ const field = stringFields[index];
513
+ const fieldValue = shortenString(ownDataProperty(value, field), 32);
514
+ if (fieldValue !== undefined) bounded[field] = fieldValue;
515
+ }
516
+ for (let index = 0; index < integerFields.length; index += 1) {
517
+ const field = integerFields[index];
518
+ const fieldValue = ownDataProperty(value, field);
519
+ if (numberIsSafeInteger(fieldValue) && fieldValue >= 0) bounded[field] = fieldValue;
520
+ }
521
+ for (let index = 0; index < booleanFields.length; index += 1) {
522
+ const field = booleanFields[index];
523
+ const fieldValue = ownDataProperty(value, field);
524
+ if (typeof fieldValue === "boolean") bounded[field] = fieldValue;
525
+ }
526
+ return bounded;
527
+ }
528
+
529
+ function agentEvidenceOwnerId(details, toolCallId) {
530
+ const job = ownDataProperty(details, "job");
531
+ const workflow = ownDataProperty(details, "workflow");
532
+ const candidates = [
533
+ ownDataProperty(job, "packageRunId"),
534
+ ownDataProperty(workflow, "packageRunId"),
535
+ ownDataProperty(job, "jobId"),
536
+ ownDataProperty(details, "jobId"),
537
+ ];
538
+ for (let index = 0; index < candidates.length; index += 1) {
539
+ const value = shortenString(candidates[index], 128);
540
+ if (value) return value;
541
+ }
542
+ return `tool-call:${safeSegment(toolCallId, "Agent tool call id")}`;
543
+ }
544
+
545
+ function eventEnvelope(event) {
546
+ return {
547
+ version: 1,
548
+ toolName: ownDataProperty(event, "toolName"),
549
+ toolCallId: ownDataProperty(event, "toolCallId"),
550
+ isError: ownDataProperty(event, "isError") === true,
551
+ content: evidenceValue(ownDataProperty(event, "content")),
552
+ details: evidenceValue(ownDataProperty(event, "details")),
553
+ };
554
+ }
555
+
556
+ function mcpServerForTool(toolName, fallback) {
557
+ if (!toolName.startsWith("mcp__")) return fallback;
558
+ const remainder = toolName.slice("mcp__".length);
559
+ const separator = remainder.indexOf("__");
560
+ return separator > 0 && separator < remainder.length - 2
561
+ ? remainder.slice(0, separator)
562
+ : fallback;
563
+ }
564
+
565
+ export function registerEvidenceIngestion(pi, {
566
+ isMcpTool = () => false,
567
+ entryTypePrefix = DEFAULT_ACTIVE_CONTEXT_ENTRY_TYPE_PREFIX,
568
+ } = {}) {
569
+ const evidenceNamespace = safeSegment(
570
+ entryTypeNamespace(entryTypePrefix),
571
+ "entry type namespace",
572
+ );
573
+ const mcpOwnerKind = `${evidenceNamespace}-mcp`;
574
+ const evidenceDirectory = `${evidenceNamespace}-evidence`;
575
+ pi.on("tool_result", async (event, ctx) => {
576
+ if (ownDataProperty(event, "isError") === true) return;
577
+ const toolName = ownDataProperty(event, "toolName");
578
+ const toolCallId = ownDataProperty(event, "toolCallId");
579
+ if (typeof toolName !== "string" || typeof toolCallId !== "string") return;
580
+ if (hasImage(ownDataProperty(event, "content"))) return;
581
+ const sessionFile = ctx.sessionManager.getSessionFile();
582
+ const sessionId = ctx.sessionManager.getSessionId();
583
+ const details = ownDataProperty(event, "details");
584
+ if (ownDataProperty(details, "evidence") !== undefined) return;
585
+ const fullOutputPath = ownDataProperty(details, "fullOutputPath");
586
+
587
+ const projector = evidenceProjectors.get(toolName);
588
+ if (projector) {
589
+ return await projector({ event, ctx, sessionFile, sessionId, details, toolCallId });
590
+ }
591
+
592
+ if (toolName === "bash" && typeof fullOutputPath === "string" && isAbsolute(fullOutputPath)) {
593
+ const descriptor = await pinEvidenceFile({
594
+ sessionFile,
595
+ ownerKind: "pi-session",
596
+ ownerId: sessionId,
597
+ sourcePath: fullOutputPath,
598
+ source: { toolName, toolCallId, artifactKind: "bash-full-output" },
599
+ evidenceDirectory,
600
+ });
601
+ const text = textContent(ownDataProperty(event, "content"));
602
+ const truncation = boundedTruncation(ownDataProperty(details, "truncation"));
603
+ const safeDetails = {
604
+ ...(truncation ? { truncation } : {}),
605
+ fullOutputPath: descriptor.path,
606
+ };
607
+ return projectEvidenceText(text, descriptor, "utf8-tail", safeDetails, true);
608
+ }
609
+
610
+ const text = textContent(ownDataProperty(event, "content"));
611
+ if (toolName === "read" && utf8Bytes(text) > TOOL_RESULT_PROJECTION_BYTES) {
612
+ const descriptor = await writeEvidenceArtifact({
613
+ sessionFile,
614
+ ownerKind: "pi-session",
615
+ ownerId: sessionId,
616
+ bytes: Buffer.from(text, "utf8"),
617
+ mediaType: "text/plain",
618
+ encoding: "utf-8",
619
+ source: { toolName, toolCallId, artifactKind: "read-result" },
620
+ evidenceDirectory,
621
+ });
622
+ const truncation = boundedTruncation(ownDataProperty(details, "truncation"));
623
+ return projectEvidenceText(text, descriptor, "utf8-head", truncation ? { truncation } : undefined);
624
+ }
625
+
626
+ if (isMcpTool(toolName)) {
627
+ const envelope = eventEnvelope(event);
628
+ const serialized = `${stableStringify(envelope)}\n`;
629
+ if (utf8Bytes(serialized) <= TOOL_RESULT_PROJECTION_BYTES) return;
630
+ const descriptor = await writeEvidenceArtifact({
631
+ sessionFile,
632
+ ownerKind: mcpOwnerKind,
633
+ ownerId: `${evidenceNamespace}:${sessionId}`,
634
+ bytes: Buffer.from(serialized, "utf8"),
635
+ mediaType: "application/json",
636
+ encoding: "utf-8",
637
+ source: { toolName, toolCallId, artifactKind: "mcp-tool-result" },
638
+ evidenceDirectory,
639
+ additionalOwnerKind: mcpOwnerKind,
640
+ });
641
+ return projectEvidenceText(text || "Oversized structured MCP result", descriptor, "utf8-head", {
642
+ mcpServer: mcpServerForTool(toolName, evidenceNamespace),
643
+ });
644
+ }
645
+ });
646
+ }
647
+
648
+ registerEvidenceProjector("Agent", async ({ event, ctx, sessionFile, sessionId, details, toolCallId }) => {
649
+ const envelope = eventEnvelope(event);
650
+ const serialized = `${stableStringify(envelope)}\n`;
651
+ if (utf8Bytes(serialized) <= TOOL_RESULT_PROJECTION_BYTES) return;
652
+ const descriptor = await writeEvidenceArtifact({
653
+ sessionFile,
654
+ ownerKind: "pi-subagents",
655
+ ownerId: agentEvidenceOwnerId(details, toolCallId),
656
+ bytes: Buffer.from(serialized, "utf8"),
657
+ mediaType: "application/json",
658
+ encoding: "utf-8",
659
+ source: { toolName: "Agent", toolCallId, artifactKind: "governed-agent-result" },
660
+ });
661
+ const projection = projectEvidenceText(
662
+ textContent(ownDataProperty(event, "content")) || "Governed Agent result",
663
+ descriptor,
664
+ "utf8-head",
665
+ boundedAgentDetails(details),
666
+ false,
667
+ AGENT_TEXT_PROJECTION_BYTES,
668
+ );
669
+ if (utf8Bytes(stableStringify(projection)) > AGENT_RESULT_PROJECTION_BYTES) {
670
+ throw new Error(`Governed Agent evidence projection exceeds ${AGENT_RESULT_PROJECTION_BYTES} bytes`);
671
+ }
672
+ return projection;
673
+ });
@@ -0,0 +1,32 @@
1
+ import { registerActiveContext } from "./active-context.ts";
2
+ import { registerEvidenceIngestion } from "./evidence.js";
3
+ import { createSummarizeContextSpan } from "./summarizer.js";
4
+
5
+ export function registerPiFold(pi, options = {}, loadHostModule) {
6
+ const {
7
+ evidenceIngestion = true,
8
+ isMcpTool,
9
+ summarizer = "session",
10
+ summarizeContextSpan,
11
+ ...activeContextOptions
12
+ } = options;
13
+ if (Object.hasOwn(options, "summarizer") && summarizeContextSpan !== undefined) {
14
+ throw new Error("summarizer and summarizeContextSpan cannot be configured together");
15
+ }
16
+ const configuredSummarizer = summarizeContextSpan ??
17
+ createSummarizeContextSpan(summarizer, loadHostModule);
18
+ if (evidenceIngestion) {
19
+ registerEvidenceIngestion(pi, {
20
+ isMcpTool,
21
+ entryTypePrefix: activeContextOptions.entryTypePrefix,
22
+ });
23
+ }
24
+ return registerActiveContext(pi, {
25
+ ...activeContextOptions,
26
+ ...(configuredSummarizer ? { summarizeContextSpan: configuredSummarizer } : {}),
27
+ });
28
+ }
29
+
30
+ export default function piFold(pi, options) {
31
+ return registerPiFold(pi, options);
32
+ }