@skill-harness/adapters 0.9.0 → 0.11.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.
@@ -1,7 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFileSync, readdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { TRAJECTORY_EVENT_VERSION, deserializeTrajectoryEvents, matchesGlob, redactArgs } from "@skill-harness/core";
4
+ import { TRAJECTORY_EVENT_VERSION, deserializeTrajectoryEvents, matchesGlob, redactArgs, redactText } from "@skill-harness/core";
5
+ import { assertSupportedSchema, declaredPropertyNames, validateClosedSchema } from "./closed-schema.js";
6
+ import { PI_DADDY_CONTRACT_COMMIT, PI_DADDY_LEDGER_V2_SCHEMA } from "./pi-daddy-ledger-v2.js";
5
7
  /** Read and normalize declared workspace-local native ledger files. */
6
8
  export function collectTrajectorySources(cwd, sources) {
7
9
  const files = walkFiles(cwd);
@@ -32,13 +34,21 @@ export function collectTrajectorySources(cwd, sources) {
32
34
  if (!normalized)
33
35
  throw new Error("normalized-v1 source is empty, malformed, or unsupported");
34
36
  const times = normalized.map((event) => validTime(event.at) ? Date.parse(event.at) : null);
35
- if (times.every((time) => time !== null) && times.some((time, index) => index > 0 && time < times[index - 1])) {
36
- throw new Error("native event timestamps move backwards relative to the source's recorded sequence");
37
+ if (times.every((time) => time !== null)) {
38
+ const highWaterByStream = new Map();
39
+ for (let index = 0; index < times.length; index += 1) {
40
+ const stream = source.adapter === "pi-daddy-v1" ? normalizedPiDaddyStreamKey(normalized[index], index) : "source";
41
+ const highWater = highWaterByStream.get(stream);
42
+ if (highWater !== undefined && times[index] < highWater && !isAllowedPiDaddyReceiptInversion(source.adapter, normalized, index)) {
43
+ throw new Error("native event timestamps move backwards relative to the source's recorded sequence");
44
+ }
45
+ highWaterByStream.set(stream, Math.max(highWater ?? times[index], times[index]));
46
+ }
37
47
  }
38
48
  streams.push({ file, adapter: source.adapter, events: normalized });
39
49
  }
40
50
  catch (error) {
41
- errors.push(`${source.adapter}:${file}: ${error instanceof Error ? error.message : String(error)}`);
51
+ errors.push(`${source.adapter}:${file}: ${sanitizePersistedError(error)}`);
42
52
  }
43
53
  }
44
54
  }
@@ -133,7 +143,7 @@ export function normalizePrincipalAssuranceLedger(text) {
133
143
  validatePrincipalIntegrity(records);
134
144
  return records.map((record, index) => {
135
145
  if (record.schema_version !== "1.0") {
136
- throw new Error(`unsupported principal assurance schema version ${JSON.stringify(record.schema_version)} at line ${index + 1}; expected \"1.0\"`);
146
+ throw new Error(`unsupported principal assurance schema version ${safeDiagnosticValue(record.schema_version)} at line ${index + 1}; expected \"1.0\"`);
137
147
  }
138
148
  if (!Number.isInteger(record.seq) || Number(record.seq) < 1 || typeof record.type !== "string" || typeof record.run_id !== "string") {
139
149
  throw new Error(`invalid principal assurance v1 event at line ${index + 1}: seq, type, and run_id are required`);
@@ -174,27 +184,683 @@ export function normalizePrincipalAssuranceLedger(text) {
174
184
  });
175
185
  }
176
186
  /**
177
- * Normalize both the current unversioned pi-daddy 0.17 grant ledger and the
178
- * explicit v1 governance supplement. Legacy omissions remain omissions: no
179
- * task/workspace/expiry field is ever inferred as successful governance.
187
+ * Normalize pi-daddy's public ledgers: unversioned 0.17 GrantRecord lines and
188
+ * ledgerVersion 2 runtime events emitted by 0.18.0. Version detection precedes
189
+ * the legacy fallback so a new event can never be misdiagnosed as an old grant.
180
190
  */
181
191
  export function normalizePiDaddyLedger(text) {
182
192
  const records = parseJsonl(text, "pi-daddy");
193
+ validatePiDaddyTimestampOrder(records);
183
194
  const out = [];
184
195
  let seq = 1;
185
196
  records.forEach((record, index) => {
186
- if (record.schema_version !== undefined && record.schema_version !== "1.0") {
187
- throw new Error(`unsupported pi-daddy ledger schema version ${JSON.stringify(record.schema_version)} at line ${index + 1}; expected unversioned 0.17 grant records or \"1.0\" governance records`);
188
- }
189
- if (record.schema_version === "1.0") {
190
- out.push(normalizePiDaddyV1(record, seq++, index));
197
+ if (record.ledgerVersion !== undefined) {
198
+ if (record.ledgerVersion !== 2) {
199
+ throw new Error(`unsupported pi-daddy ledgerVersion ${safeDiagnosticValue(record.ledgerVersion)} at line ${index + 1}; expected 2 or an unversioned 0.17 GrantRecord`);
200
+ }
201
+ // The discriminator first (it names the four public variants), then the
202
+ // producer's own closed schema, then semantic normalization. Nothing
203
+ // downstream may assume a field the pinned contract has not admitted.
204
+ requireV2Discriminator(record, index + 1);
205
+ assertPinnedV2Contract(record, index + 1);
206
+ for (const event of normalizePiDaddyV2(record, index))
207
+ out.push({ ...event, seq: seq++ });
191
208
  return;
192
209
  }
210
+ if (record.schema_version !== undefined) {
211
+ throw new Error(`pi-daddy schema_version/record_type at line ${index + 1} is not a public pi-daddy ledger format; expected ledgerVersion 2 or an unversioned 0.17 GrantRecord`);
212
+ }
213
+ if (record.event !== undefined) {
214
+ throw new Error(`pi-daddy event [REDACTED invalid value] at line ${index + 1} is missing explicit ledgerVersion 2`);
215
+ }
193
216
  for (const event of normalizeLegacyGrant(record, index))
194
217
  out.push({ ...event, seq: seq++ });
195
218
  });
196
219
  return out;
197
220
  }
221
+ /** The four public `ledgerVersion: 2` event discriminators. */
222
+ const V2_EVENTS = new Set(["capability_decision", "workspace_lease", "child_lifecycle", "check_receipt"]);
223
+ function requireV2Discriminator(record, line) {
224
+ const nativeEvent = string(record.event);
225
+ if (!nativeEvent || !V2_EVENTS.has(nativeEvent)) {
226
+ throw new Error(`invalid pi-daddy v2 event at line ${line}: event must be capability_decision, workspace_lease, child_lifecycle, or check_receipt`);
227
+ }
228
+ return nativeEvent;
229
+ }
230
+ // Memoized once per process: the pinned document does not change at runtime, and
231
+ // walking it for every ledger line would be pure waste.
232
+ let pinnedContractChecked = false;
233
+ let pinnedContractFieldNames;
234
+ /**
235
+ * Validate one explicit `ledgerVersion: 2` record against pi-daddy's *own* closed
236
+ * schema before anything reads a field out of it.
237
+ *
238
+ * The harness used to reimplement the contract field by field, which is how an
239
+ * undeclared top-level field could ride along unnoticed: a check that is not
240
+ * written cannot fail. Driving the check from the producer's pinned bytes makes
241
+ * unknown fields, enum members, nullability and requiredness fail closed without
242
+ * a second vocabulary to keep in step.
243
+ */
244
+ function assertPinnedV2Contract(record, line) {
245
+ if (!pinnedContractChecked) {
246
+ assertSupportedSchema(PI_DADDY_LEDGER_V2_SCHEMA, "pinned pi-daddy ledger v2 schema");
247
+ pinnedContractFieldNames = declaredPropertyNames(PI_DADDY_LEDGER_V2_SCHEMA);
248
+ pinnedContractChecked = true;
249
+ }
250
+ const violations = validateClosedSchema(PI_DADDY_LEDGER_V2_SCHEMA, record, { knownFieldNames: pinnedContractFieldNames });
251
+ if (violations.length === 0)
252
+ return;
253
+ const nativeEvent = string(record.event);
254
+ const label = nativeEvent && V2_EVENTS.has(nativeEvent) ? nativeEvent : "record";
255
+ const [first] = violations;
256
+ const extra = violations.length > 1 ? ` (+${violations.length - 1} more contract violation${violations.length > 2 ? "s" : ""})` : "";
257
+ throw new Error(`invalid pi-daddy v2 ${label} at line ${line}: closed contract violation — ${first.path ? `${first.path} ` : ""}${first.message}${extra}` +
258
+ ` [pi-daddy ${PI_DADDY_CONTRACT_COMMIT.slice(0, 12)}]`);
259
+ }
260
+ const V2_LEASE_OUTCOMES = new Set([
261
+ "acquired", "uncontended", "refused", "released", "released-unrecorded", "lost", "retained", "timeout", "recovered",
262
+ ]);
263
+ const V2_LEASE_ACCESS = new Set(["read", "write"]);
264
+ /** Lease outcomes that can precede the one accepted append-after-release receipt inversion. */
265
+ const V2_RECEIPT_PRIOR_LEASE_OUTCOMES = new Set(["acquired", "recovered"]);
266
+ const V2_REFUSAL_FIELDS = new Set(["code", "message", "details"]);
267
+ const V2_REFUSAL_DETAIL_TYPES = new Set(["string", "number", "boolean", "null"]);
268
+ const V2_LIFECYCLE_STATES = new Set(["starting", "completed", "failed"]);
269
+ const V2_EXECUTORS = new Set(["process", "herdr"]);
270
+ const V2_RECEIPT_RELEASE_OUTCOMES = new Set(["released", "released-unrecorded", "lost", "timeout"]);
271
+ const NORMALIZED_RECEIPT_RELEASE_EVENTS = new Set([
272
+ "writer_lease_released", "writer_lease_released_unrecorded", "writer_lease_lost", "writer_lease_timeout",
273
+ ]);
274
+ const V2_CORRELATION_FIELDS = new Set([
275
+ "schema_version", "run_id", "task_id", "workspace_id", "context_id", "phase", "assurance",
276
+ "assurance_effective", "policy_label", "assurance_source", "assurance_scope", "activated_at",
277
+ "plan_digest", "definition_digest", "task_digest", "base_sha", "head_sha", "tree_sha",
278
+ "event_seq", "last_change_seq", "last_authority_seq", "check_receipt_id",
279
+ ]);
280
+ const V2_CORRELATION_NUMERIC_FIELDS = new Set(["event_seq", "last_change_seq", "last_authority_seq"]);
281
+ const V2_APPROVAL_SOURCES = new Set(["prompt", "session", "persisted", "inherited"]);
282
+ const V2_APPROVAL_SCOPES = new Set(["once", "session", "always"]);
283
+ /**
284
+ * pi-daddy's canonical refusal vocabulary, in the pinned contract's own order.
285
+ *
286
+ * This is a copy of `#/$defs/refusalCode` from the pinned schema and is exported
287
+ * so `pi-daddy-contract.test.ts` can assert set equality against the producer
288
+ * artifact — a hand-maintained second vocabulary without that drift assertion is
289
+ * exactly how `GRANT_ID_MALFORMED` came to be rejected as "unsupported".
290
+ */
291
+ export const V2_REFUSAL_CODES = new Set([
292
+ "CAPABILITY_ESCALATION", "GRANT_ID_MALFORMED", "DEFINITION_NOT_AUTHORIZED", "UNDECLARED_TOOLS", "UNKNOWN_TOOL",
293
+ "GATED_UNAPPROVED", "APPROVAL_EXPIRED", "APPROVAL_SCOPE_MISMATCH", "APPROVAL_FLOW_FAILED",
294
+ "DEPTH_EXCEEDED", "FANOUT_EXCEEDED", "EXECUTOR_UNAVAILABLE", "CHILD_TIMED_OUT", "CHILD_CANCELLED",
295
+ "CHILD_EXIT_NONZERO", "TASK_MISSING", "UNKNOWN_DEFINITION", "CEILING_PATTERNS_UNRESOLVED",
296
+ "NARROWING_VIOLATED", "DEFINITION_UNREADABLE", "CORRELATION_TOO_LARGE", "CORRELATION_INVALID",
297
+ "LEDGER_WRITE_FAILED", "FANOUT_FAILED", "WORKSPACE_NOT_REGISTERED", "WORKSPACE_WRITE_CONFLICT",
298
+ "WORKSPACE_LEASE_STALE", "CHECK_NOT_CONFIGURED", "CHECK_CONFIGURATION_INVALID",
299
+ "CHECK_IDENTITY_UNAVAILABLE", "CHECK_IDENTITY_MISMATCH",
300
+ ]);
301
+ /**
302
+ * Every vocabulary the adapter restates from the pinned contract, paired with the
303
+ * place in the schema it must equal.
304
+ *
305
+ * The closed schema gates first, so a semantic check that has drifted *narrower*
306
+ * than the contract no longer opens a hole — it produces the opposite failure:
307
+ * a contract-valid record admitted by the schema and then thrown out by a stale
308
+ * harness set, which is precisely what `GRANT_ID_MALFORMED` did. One manifest, one
309
+ * test over all of it, so re-pinning cannot quietly leave a set behind.
310
+ */
311
+ export const V2_RESTATED_VOCABULARIES = [
312
+ { name: "V2_EVENTS", kind: "discriminators", pointer: "#/oneOf", values: V2_EVENTS },
313
+ { name: "V2_REFUSAL_CODES", kind: "enum", pointer: "#/$defs/refusalCode", values: V2_REFUSAL_CODES },
314
+ { name: "V2_APPROVAL_SOURCES", kind: "enum", pointer: "#/$defs/approvalSource", values: V2_APPROVAL_SOURCES },
315
+ { name: "V2_APPROVAL_SCOPES", kind: "enum", pointer: "#/$defs/approvalScope", values: V2_APPROVAL_SCOPES },
316
+ { name: "V2_LEASE_OUTCOMES", kind: "enum", pointer: "#/$defs/workspaceLease/properties/outcome", values: V2_LEASE_OUTCOMES },
317
+ { name: "V2_LEASE_ACCESS", kind: "enum", pointer: "#/$defs/workspaceLease/properties/access", values: V2_LEASE_ACCESS },
318
+ { name: "V2_LIFECYCLE_STATES", kind: "enum", pointer: "#/$defs/childLifecycle/properties/state", values: V2_LIFECYCLE_STATES },
319
+ { name: "V2_EXECUTORS (lifecycle)", kind: "enum", pointer: "#/$defs/childLifecycle/properties/executor", values: V2_EXECUTORS },
320
+ { name: "V2_EXECUTORS (decision)", kind: "enum", pointer: "#/$defs/capabilityDecision/properties/executor", values: V2_EXECUTORS },
321
+ { name: "V2_CORRELATION_FIELDS", kind: "propertyNames", pointer: "#/$defs/correlation", values: V2_CORRELATION_FIELDS },
322
+ { name: "V2_CORRELATION_NUMERIC_FIELDS", kind: "numericPropertyNames", pointer: "#/$defs/correlation", values: V2_CORRELATION_NUMERIC_FIELDS },
323
+ { name: "V2_REFUSAL_FIELDS", kind: "propertyNames", pointer: "#/$defs/refusal", values: V2_REFUSAL_FIELDS },
324
+ { name: "V2_REFUSAL_DETAIL_TYPES", kind: "typeNames", pointer: "#/$defs/refusal/properties/details/additionalProperties", values: V2_REFUSAL_DETAIL_TYPES },
325
+ ];
326
+ /**
327
+ * Harness-side subsets of a contract vocabulary, not restatements of one. They encode
328
+ * the harness's own semantics — which lease outcomes a receipt may be appended after,
329
+ * and which may precede that release — so the assertion on them is containment, not
330
+ * equality. Anything the test *equality*-asserts belongs in the manifest above
331
+ * instead; membership here is a claim that the harness deliberately holds a subset.
332
+ */
333
+ export const V2_VOCABULARY_SUBSETS = [
334
+ { name: "V2_RECEIPT_RELEASE_OUTCOMES", pointer: "#/$defs/workspaceLease/properties/outcome", values: V2_RECEIPT_RELEASE_OUTCOMES },
335
+ { name: "V2_RECEIPT_PRIOR_LEASE_OUTCOMES", pointer: "#/$defs/workspaceLease/properties/outcome", values: V2_RECEIPT_PRIOR_LEASE_OUTCOMES },
336
+ ];
337
+ const V2_CORRELATION_MAX_BYTES = 32 * 1024;
338
+ const V2_CORRELATION_MAX_FIELD_CHARS = 512;
339
+ const V2_CORRELATION_MAX_SCOPE_BYTES = 4 * 1024;
340
+ function piDaddyStreamKey(record, index) {
341
+ if (record.ledgerVersion === undefined)
342
+ return JSON.stringify(["legacy", string(record.childId) ?? `missing-child:${index}`]);
343
+ const correlation = object(record.correlation);
344
+ return JSON.stringify([
345
+ string(correlation?.run_id) ?? `missing-run:${index}`,
346
+ string(correlation?.task_id) ?? `missing-task:${index}`,
347
+ string(record.workspaceId) ?? string(correlation?.workspace_id) ?? "",
348
+ string(record.childId) ?? `missing-child:${index}`,
349
+ ]);
350
+ }
351
+ function normalizedPiDaddyStreamKey(event, index) {
352
+ if (event.source === "pi-daddy-0.17")
353
+ return JSON.stringify(["legacy", event.child_id ?? `missing-child:${index}`]);
354
+ const correlation = object(event.attributes?.correlation);
355
+ return JSON.stringify([
356
+ event.run_id ?? `missing-run:${index}`,
357
+ event.task_id ?? `missing-task:${index}`,
358
+ event.workspace_id ?? string(correlation?.workspace_id) ?? "",
359
+ event.child_id ?? `missing-child:${index}`,
360
+ ]);
361
+ }
362
+ function sameRawCorrelationIdentity(left, right) {
363
+ const leftCorrelation = object(left.correlation);
364
+ const rightCorrelation = object(right.correlation);
365
+ return string(leftCorrelation?.run_id) === string(rightCorrelation?.run_id) &&
366
+ string(leftCorrelation?.task_id) === string(rightCorrelation?.task_id);
367
+ }
368
+ function validatePiDaddyTimestampOrder(records) {
369
+ const highWaterByChild = new Map();
370
+ records.forEach((record, index) => {
371
+ const supportedV2 = record.ledgerVersion === 2;
372
+ const legacy = record.ledgerVersion === undefined && record.schema_version === undefined && record.event === undefined;
373
+ if (!supportedV2 && !legacy)
374
+ return;
375
+ const at = string(record.ts);
376
+ if (!validTime(at))
377
+ throw new Error(`invalid pi-daddy ledger timestamp at line ${index + 1}: ts must be a date-time`);
378
+ const time = Date.parse(at);
379
+ const child = piDaddyStreamKey(record, index);
380
+ const highWater = highWaterByChild.get(child);
381
+ if (highWater !== undefined && time < highWater && !isRawPiDaddyReceiptInversion(records, index, time)) {
382
+ throw new Error(`pi-daddy ledger timestamp moves backwards at line ${index + 1}`);
383
+ }
384
+ highWaterByChild.set(child, Math.max(highWater ?? time, time));
385
+ });
386
+ }
387
+ function isRawPiDaddyReceiptInversion(records, index, receiptTime) {
388
+ const receipt = records[index];
389
+ const release = records[index - 1];
390
+ if (receipt?.ledgerVersion !== 2 || receipt.event !== "check_receipt" || release?.ledgerVersion !== 2 || release.event !== "workspace_lease")
391
+ return false;
392
+ if (receipt.childId !== release.childId || receipt.workspaceId !== release.workspaceId || !sameRawCorrelationIdentity(receipt, release) || !V2_RECEIPT_RELEASE_OUTCOMES.has(string(release.outcome) ?? ""))
393
+ return false;
394
+ const previousLease = records.slice(0, index - 1).reverse().find((record) => record.ledgerVersion === 2 && record.event === "workspace_lease" && record.childId === receipt.childId &&
395
+ record.workspaceId === receipt.workspaceId && sameRawCorrelationIdentity(receipt, record));
396
+ return Boolean(previousLease && V2_RECEIPT_PRIOR_LEASE_OUTCOMES.has(string(previousLease.outcome) ?? "") &&
397
+ validTime(string(previousLease.ts)) && Date.parse(string(previousLease.ts)) <= receiptTime);
398
+ }
399
+ function isAllowedPiDaddyReceiptInversion(adapter, events, index) {
400
+ if (adapter !== "pi-daddy-v1")
401
+ return false;
402
+ const receipt = events[index];
403
+ const release = events[index - 1];
404
+ if (receipt?.type !== "check_receipt_recorded" || !NORMALIZED_RECEIPT_RELEASE_EVENTS.has(release?.type))
405
+ return false;
406
+ if (receipt.child_id !== release.child_id || receipt.workspace_id !== release.workspace_id || receipt.run_id !== release.run_id || receipt.task_id !== release.task_id || !validTime(receipt.at))
407
+ return false;
408
+ const receiptTime = Date.parse(receipt.at);
409
+ const previousLease = events.slice(0, index - 1).reverse().find((event) => event.attributes?.native_event === "workspace_lease" && event.child_id === receipt.child_id &&
410
+ event.workspace_id === receipt.workspace_id && event.run_id === receipt.run_id && event.task_id === receipt.task_id);
411
+ return Boolean(previousLease && new Set(["writer_lease_acquired", "writer_lease_recovered"]).has(previousLease.type) &&
412
+ validTime(previousLease.at) && Date.parse(previousLease.at) <= receiptTime);
413
+ }
414
+ function normalizePiDaddyV2(record, index) {
415
+ const line = index + 1;
416
+ const nativeEvent = requireV2Discriminator(record, line);
417
+ const at = requireV2String(record, "ts", nativeEvent, line);
418
+ const childId = requireV2String(record, "childId", nativeEvent, line);
419
+ const correlation = requireV2Correlation(record, nativeEvent, line);
420
+ const carriesTopWorkspace = nativeEvent === "workspace_lease" || nativeEvent === "check_receipt";
421
+ if (!carriesTopWorkspace && record.workspaceId !== undefined) {
422
+ throw new Error(`invalid pi-daddy v2 ${nativeEvent} at line ${line}: workspaceId is not part of the public variant`);
423
+ }
424
+ const topWorkspace = carriesTopWorkspace ? string(record.workspaceId) : undefined;
425
+ const correlationWorkspace = string(correlation.workspace_id);
426
+ if (topWorkspace && correlationWorkspace && topWorkspace !== correlationWorkspace) {
427
+ throw new Error(`invalid pi-daddy v2 ${nativeEvent} at line ${line}: workspaceId disagrees with correlation.workspace_id`);
428
+ }
429
+ if (nativeEvent !== "capability_decision" && (record.taskDigest !== undefined || record.definitionDigest !== undefined)) {
430
+ throw new Error(`invalid pi-daddy v2 ${nativeEvent} at line ${line}: taskDigest and definitionDigest belong only to capability_decision`);
431
+ }
432
+ const definition = nativeEvent === "capability_decision" ? object(record.definitionDigest) : undefined;
433
+ const trustedTask = nativeEvent === "capability_decision" ? string(record.taskDigest) : undefined;
434
+ const trustedDefinition = nativeEvent === "capability_decision" ? string(definition?.sha256) : undefined;
435
+ const common = {
436
+ event_version: TRAJECTORY_EVENT_VERSION,
437
+ source: "pi-daddy-v2",
438
+ at,
439
+ run_id: string(correlation.run_id),
440
+ task_id: string(correlation.task_id),
441
+ // correlation.workspace_id is a controller-supplied join label, not proof that
442
+ // pi-daddy resolved or leased that workspace. Only a top-level runtime identity
443
+ // is promoted into the adapter-neutral authoritative-looking field.
444
+ workspace_id: topWorkspace,
445
+ context_id: string(correlation.context_id),
446
+ child_id: childId,
447
+ phase: string(correlation.phase),
448
+ digests: anyDefined({
449
+ task: trustedTask,
450
+ definition: trustedDefinition,
451
+ correlation_plan: string(correlation.plan_digest),
452
+ correlation_task: string(correlation.task_digest),
453
+ correlation_definition: string(correlation.definition_digest),
454
+ correlation_base: string(correlation.base_sha),
455
+ correlation_head: string(correlation.head_sha),
456
+ correlation_tree: string(correlation.tree_sha),
457
+ }),
458
+ };
459
+ const commonAttributes = safeAttributes({
460
+ ledger_version: 2,
461
+ native_event: nativeEvent,
462
+ correlation: sanitizeAttributes(correlation),
463
+ event_seq: finiteNumber(correlation.event_seq),
464
+ last_change_seq: finiteNumber(correlation.last_change_seq),
465
+ last_authority_seq: finiteNumber(correlation.last_authority_seq),
466
+ check_receipt_id: string(correlation.check_receipt_id),
467
+ assurance: string(correlation.assurance),
468
+ assurance_effective: string(correlation.assurance_effective),
469
+ policy_label: string(correlation.policy_label),
470
+ assurance_source: string(correlation.assurance_source),
471
+ assurance_scope: correlation.assurance_scope,
472
+ activated_at: string(correlation.activated_at),
473
+ });
474
+ if (nativeEvent === "capability_decision") {
475
+ if (record.definitionDigest !== undefined && (!definition || !string(definition.name) || !string(definition.source) || !trustedDefinition || !/^[a-fA-F0-9]{64}$/.test(trustedDefinition))) {
476
+ throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: definitionDigest requires non-empty name, source, and sha256`);
477
+ }
478
+ const parentId = requireV2String(record, "parentId", nativeEvent, line);
479
+ const executor = requireV2Executor(record, nativeEvent, line);
480
+ const taskDigest = requireV2String(record, "taskDigest", nativeEvent, line);
481
+ if (!/^[a-fA-F0-9]{64}$/.test(taskDigest))
482
+ throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: taskDigest must be sha256`);
483
+ if (!Number.isInteger(record.depth) || typeof record.blocked !== "boolean") {
484
+ throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: depth and blocked are required`);
485
+ }
486
+ const requested = requireV2StringArray(record, "requested", nativeEvent, line);
487
+ const parentGrant = requireV2StringArray(record, "parentGrant", nativeEvent, line);
488
+ const effective = requireV2StringArray(record, "effective", nativeEvent, line);
489
+ const denied = requireV2StringArray(record, "denied", nativeEvent, line);
490
+ const clipped = requireV2StringArray(record, "clipped", nativeEvent, line);
491
+ const gated = requireV2StringArray(record, "gatedBlocked", nativeEvent, line);
492
+ const approved = optionalV2StringArray(record, "approved", nativeEvent, line);
493
+ const agentType = optionalV2SafeString(record.agentType, "agentType", nativeEvent, line);
494
+ const humanDenied = optionalV2Boolean(record, "humanDenied", nativeEvent, line);
495
+ const refusal = structuredRefusal(record.refusal, nativeEvent, line);
496
+ if (!record.blocked && refusal)
497
+ throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: an allowed decision cannot carry a refusal`);
498
+ validateCapabilityPartition(requested, effective, denied, clipped, gated, approved, Boolean(record.blocked), line);
499
+ const approvalSource = optionalV2Enum(record.approvalSource, "approvalSource", V2_APPROVAL_SOURCES, nativeEvent, line);
500
+ const approvalSources = optionalV2EnumMap(record.approvalSources, "approvalSources", V2_APPROVAL_SOURCES, nativeEvent, line);
501
+ const approvalScope = optionalV2Enum(record.approvalScope, "approvalScope", V2_APPROVAL_SCOPES, nativeEvent, line);
502
+ const approvalScopes = optionalV2EnumMap(record.approvalScopes, "approvalScopes", V2_APPROVAL_SCOPES, nativeEvent, line);
503
+ const approvalExpiresAt = optionalV2StringMap(record.approvalExpiresAt, "approvalExpiresAt", nativeEvent, line, validTime);
504
+ const approvalUses = optionalV2ApprovalUses(record.approvalUses, nativeEvent, line);
505
+ validateApprovalEvidence(approved ?? [], approvalSource, approvalSources, approvalScopes, approvalExpiresAt, approvalUses, line);
506
+ const normalizedRequested = [...new Set(requested)];
507
+ const attributes = safeAttributes({
508
+ ...commonAttributes,
509
+ depth: record.depth,
510
+ agent_type: agentType,
511
+ native_requested: normalizedRequested.length === requested.length ? undefined : requested,
512
+ executor,
513
+ task_from: string(record.taskFrom),
514
+ parent_grant: parentGrant,
515
+ denied,
516
+ clipped,
517
+ gated_blocked: gated,
518
+ blocked: record.blocked,
519
+ reason: string(record.reason),
520
+ approved,
521
+ approval_source: approvalSource,
522
+ approval_sources: approvalSources,
523
+ approval_scope: approvalScope,
524
+ approval_scopes: approvalScopes,
525
+ approval_expires_at: approvalExpiresAt,
526
+ approval_uses: approvalUses,
527
+ human_denied: humanDenied,
528
+ gate_outcome: string(record.gateOutcome),
529
+ definition_name: string(definition?.name),
530
+ definition_source: string(definition?.source),
531
+ structured_refusal: refusal,
532
+ });
533
+ const base = { ...common, parent_id: parentId, requested_capabilities: normalizedRequested, effective_capabilities: effective, attributes };
534
+ const refusalCode = string(refusal?.code);
535
+ const events = [
536
+ ...normalizedRequested.map((capability) => ({ ...base, type: "capability_requested", capability })),
537
+ ];
538
+ const sources = approvalSources;
539
+ const scopes = approvalScopes;
540
+ const expiries = approvalExpiresAt;
541
+ const uses = approvalUses;
542
+ for (const capability of approved ?? []) {
543
+ events.push(cleanEvent({
544
+ ...base,
545
+ type: "approval_used",
546
+ capability,
547
+ approval: cleanObject({
548
+ capability,
549
+ subject: approvalSubject(agentType),
550
+ source: string(sources?.[capability]) ?? string(record.approvalSource),
551
+ scope: string(scopes?.[capability]) ?? string(record.approvalScope),
552
+ expires_at: string(expiries?.[capability]),
553
+ used_at: at,
554
+ }),
555
+ attributes: safeAttributes({ ...attributes, approval_uses: object(uses?.[capability]) }),
556
+ }));
557
+ }
558
+ const approvedSet = new Set(approved ?? []);
559
+ events.push(...(record.blocked ? [] : effective.map((capability) => ({ ...base, type: "capability_granted", capability }))), ...[...new Set([...denied, ...gated.filter((capability) => !approvedSet.has(capability))])].map((capability) => ({
560
+ ...base,
561
+ type: "capability_refused",
562
+ capability,
563
+ refusal_code: denied.includes(capability) ? "CAPABILITY_ESCALATION" : refusalCode,
564
+ })));
565
+ events.push(cleanEvent({
566
+ ...base,
567
+ type: record.blocked ? "child_spawn_refused" : "capability_decision",
568
+ refusal_code: refusalCode,
569
+ }));
570
+ return events;
571
+ }
572
+ if (nativeEvent === "workspace_lease") {
573
+ const workspaceId = requireV2String(record, "workspaceId", nativeEvent, line);
574
+ requireV2String(record, "root", nativeEvent, line);
575
+ const access = requireV2String(record, "access", nativeEvent, line);
576
+ const outcome = requireV2String(record, "outcome", nativeEvent, line);
577
+ if (!V2_LEASE_ACCESS.has(access) || !V2_LEASE_OUTCOMES.has(outcome)) {
578
+ throw new Error(`invalid pi-daddy v2 workspace_lease at line ${line}: access or outcome is unsupported`);
579
+ }
580
+ if (record.recovered !== undefined && typeof record.recovered !== "boolean" && record.recovered !== "unknown") {
581
+ throw new Error(`invalid pi-daddy v2 workspace_lease at line ${line}: recovered must be boolean or \"unknown\"`);
582
+ }
583
+ const refusal = structuredRefusal(record.refusal, nativeEvent, line);
584
+ const type = access === "read"
585
+ ? `workspace_read_${outcome.replaceAll("-", "_")}`
586
+ : outcome === "refused" && refusal?.code === "WORKSPACE_WRITE_CONFLICT"
587
+ ? "writer_lease_conflict"
588
+ : `writer_lease_${outcome.replaceAll("-", "_")}`;
589
+ return [cleanEvent({
590
+ ...common,
591
+ workspace_id: workspaceId,
592
+ type,
593
+ refusal_code: string(refusal?.code),
594
+ attributes: safeAttributes({
595
+ ...commonAttributes,
596
+ root: string(record.root),
597
+ access,
598
+ outcome,
599
+ recovered: record.recovered,
600
+ release_reason: string(record.releaseReason),
601
+ structured_refusal: refusal,
602
+ }),
603
+ })];
604
+ }
605
+ if (nativeEvent === "child_lifecycle") {
606
+ const state = requireV2String(record, "state", nativeEvent, line);
607
+ const executor = requireV2Executor(record, nativeEvent, line);
608
+ if (!V2_LIFECYCLE_STATES.has(state)) {
609
+ throw new Error(`invalid pi-daddy v2 child_lifecycle at line ${line}: state is unsupported`);
610
+ }
611
+ if (record.exitCode !== undefined && record.exitCode !== null && !Number.isInteger(record.exitCode)) {
612
+ throw new Error(`invalid pi-daddy v2 child_lifecycle at line ${line}: exitCode must be an integer or null`);
613
+ }
614
+ const timedOut = optionalV2Boolean(record, "timedOut", nativeEvent, line);
615
+ const aborted = optionalV2Boolean(record, "aborted", nativeEvent, line);
616
+ const truncated = optionalV2Boolean(record, "truncated", nativeEvent, line);
617
+ const type = state === "starting" ? "child_started" : state === "completed" ? "child_completed" : "child_failed";
618
+ return [cleanEvent({
619
+ ...common,
620
+ type,
621
+ exit_code: Number.isInteger(record.exitCode) ? Number(record.exitCode) : undefined,
622
+ attributes: safeAttributes({
623
+ ...commonAttributes,
624
+ state,
625
+ executor,
626
+ exit_code: record.exitCode,
627
+ signal: record.signal,
628
+ timed_out: timedOut,
629
+ aborted,
630
+ truncated,
631
+ reason: string(record.reason),
632
+ }),
633
+ })];
634
+ }
635
+ const workspaceId = requireV2String(record, "workspaceId", nativeEvent, line);
636
+ const receiptId = requireV2String(record, "receiptId", nativeEvent, line);
637
+ if (!/^[a-fA-F0-9]{64}$/.test(receiptId)) {
638
+ throw new Error(`invalid pi-daddy v2 check_receipt at line ${line}: receiptId must be sha256`);
639
+ }
640
+ const checkId = requireV2String(record, "checkId", nativeEvent, line);
641
+ const treeSha = requireV2String(record, "treeSha", nativeEvent, line);
642
+ if (!/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/.test(treeSha)) {
643
+ throw new Error(`invalid pi-daddy v2 check_receipt at line ${line}: treeSha must be a git object id`);
644
+ }
645
+ // The receipt's top-level `treeSha` is the candidate identity pi-daddy measured;
646
+ // `correlation.tree_sha` is a controller label the producer documents as opaque
647
+ // and non-authoritative, and its own builders emit the two independently. The
648
+ // adapter therefore promotes only the measured value into `digests.tree` and
649
+ // keeps the correlation copy in `digests.correlation_tree`. Requiring the two to
650
+ // agree rejected pi-daddy's own canonical receipt and, worse, let a controller
651
+ // string vouch for a measured one.
652
+ return [cleanEvent({
653
+ ...common,
654
+ workspace_id: workspaceId,
655
+ type: "check_receipt_recorded",
656
+ digests: { ...(common.digests ?? {}), tree: treeSha },
657
+ attributes: safeAttributes({
658
+ ...commonAttributes,
659
+ receipt_id: receiptId,
660
+ check_id: checkId,
661
+ check_receipt_id: string(correlation.check_receipt_id),
662
+ }),
663
+ })];
664
+ }
665
+ function requireV2Correlation(record, event, line) {
666
+ const correlation = object(record.correlation);
667
+ if (!correlation) {
668
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: correlation.run_id and correlation.task_id are required for workflow joins`);
669
+ }
670
+ const encoded = JSON.stringify(correlation);
671
+ if (Buffer.byteLength(encoded) > V2_CORRELATION_MAX_BYTES) {
672
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: correlation exceeds ${V2_CORRELATION_MAX_BYTES} bytes`);
673
+ }
674
+ const undeclared = Object.keys(correlation).filter((key) => !V2_CORRELATION_FIELDS.has(key));
675
+ if (undeclared.length > 0) {
676
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: correlation carries fields outside the pinned schema 1.0 contract [REDACTED field names]`);
677
+ }
678
+ for (const [key, value] of Object.entries(correlation)) {
679
+ if (value === undefined || value === null)
680
+ continue;
681
+ if (key === "assurance_scope") {
682
+ const size = Buffer.byteLength(JSON.stringify(value));
683
+ if (size > V2_CORRELATION_MAX_SCOPE_BYTES) {
684
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: correlation assurance_scope exceeds ${V2_CORRELATION_MAX_SCOPE_BYTES} bytes`);
685
+ }
686
+ continue;
687
+ }
688
+ if (V2_CORRELATION_NUMERIC_FIELDS.has(key)) {
689
+ if (typeof value !== "number" || !Number.isFinite(value)) {
690
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: correlation ${key} must be a finite number`);
691
+ }
692
+ continue;
693
+ }
694
+ if (typeof value !== "string") {
695
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: correlation ${key} must be a string`);
696
+ }
697
+ if (value.length > V2_CORRELATION_MAX_FIELD_CHARS) {
698
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: correlation ${key} exceeds ${V2_CORRELATION_MAX_FIELD_CHARS} characters`);
699
+ }
700
+ if (redactText(value) !== value) {
701
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: correlation ${key} contains a sensitive value`);
702
+ }
703
+ }
704
+ if (!string(correlation.run_id) || !string(correlation.task_id)) {
705
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: correlation.run_id and correlation.task_id are required for workflow joins`);
706
+ }
707
+ return Object.fromEntries(Object.entries(correlation).filter(([, value]) => value !== undefined && value !== null));
708
+ }
709
+ function requireV2String(record, field, event, line) {
710
+ const value = string(record[field]);
711
+ if (!value)
712
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} is required`);
713
+ if (redactText(value) !== value)
714
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} contains a sensitive value`);
715
+ return value;
716
+ }
717
+ function requireV2Executor(record, event, line) {
718
+ const executor = requireV2String(record, "executor", event, line);
719
+ if (!V2_EXECUTORS.has(executor)) {
720
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: executor must be process or herdr`);
721
+ }
722
+ return executor;
723
+ }
724
+ function requireV2StringArray(record, field, event, line) {
725
+ const value = stringArray(record[field]);
726
+ if (!value)
727
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} must be an array of strings`);
728
+ if (value.some((entry) => redactText(entry) !== entry)) {
729
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} contains a sensitive value`);
730
+ }
731
+ return value;
732
+ }
733
+ function optionalV2StringArray(record, field, event, line) {
734
+ if (record[field] === undefined)
735
+ return undefined;
736
+ return requireV2StringArray(record, field, event, line);
737
+ }
738
+ function optionalV2SafeString(value, field, event, line) {
739
+ if (value === undefined)
740
+ return undefined;
741
+ const parsed = string(value);
742
+ if (!parsed)
743
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} must be a non-empty string`);
744
+ if (redactText(parsed) !== parsed)
745
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} contains a sensitive value`);
746
+ return parsed;
747
+ }
748
+ function optionalV2Enum(value, field, allowed, event, line) {
749
+ if (value === undefined)
750
+ return undefined;
751
+ const parsed = string(value);
752
+ if (!parsed || !allowed.has(parsed)) {
753
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} must be one of ${[...allowed].join(", ")}`);
754
+ }
755
+ return parsed;
756
+ }
757
+ function optionalV2EnumMap(value, field, allowed, event, line) {
758
+ if (value === undefined)
759
+ return undefined;
760
+ const parsed = object(value);
761
+ if (!parsed)
762
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} must be an object`);
763
+ const entries = Object.entries(parsed);
764
+ if (entries.some(([key, entry]) => !key || typeof entry !== "string" || !allowed.has(entry))) {
765
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} values must be one of ${[...allowed].join(", ")}`);
766
+ }
767
+ return Object.fromEntries(entries);
768
+ }
769
+ function optionalV2StringMap(value, field, event, line, validate = () => true) {
770
+ if (value === undefined)
771
+ return undefined;
772
+ const parsed = object(value);
773
+ if (!parsed)
774
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} must be an object`);
775
+ const entries = Object.entries(parsed);
776
+ if (entries.some(([key, entry]) => !key || typeof entry !== "string" || !validate(entry))) {
777
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} must map capabilities to valid strings`);
778
+ }
779
+ return Object.fromEntries(entries);
780
+ }
781
+ function optionalV2ApprovalUses(value, event, line) {
782
+ if (value === undefined)
783
+ return undefined;
784
+ const parsed = object(value);
785
+ if (!parsed)
786
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: approvalUses must be an object`);
787
+ const output = {};
788
+ for (const [capability, boundsValue] of Object.entries(parsed)) {
789
+ const bounds = object(boundsValue);
790
+ if (!capability || !bounds || !Number.isInteger(bounds.max) || !Number.isInteger(bounds.remaining) ||
791
+ Number(bounds.max) < 0 || Number(bounds.remaining) < 0 || Number(bounds.remaining) > Number(bounds.max)) {
792
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: approvalUses requires integer max/remaining bounds`);
793
+ }
794
+ output[capability] = { max: Number(bounds.max), remaining: Number(bounds.remaining) };
795
+ }
796
+ return output;
797
+ }
798
+ function validateCapabilityPartition(requested, effective, denied, clipped, gated, approved, blocked, line) {
799
+ const groups = [effective, denied, clipped, gated];
800
+ if (groups.some((values) => new Set(values).size !== values.length)) {
801
+ throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: result capability arrays must not contain duplicates`);
802
+ }
803
+ const requestedSet = new Set(requested);
804
+ if (groups.some((values) => values.some((capability) => !requestedSet.has(capability)))) {
805
+ throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: effective, denied, clipped, and gatedBlocked must partition requested`);
806
+ }
807
+ const flattened = groups.flat();
808
+ if (new Set(flattened).size !== flattened.length) {
809
+ throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: effective, denied, clipped, and gatedBlocked must be disjoint subsets of requested`);
810
+ }
811
+ if ((approved ?? []).some((capability) => !requestedSet.has(capability) || (blocked ? !effective.includes(capability) && !gated.includes(capability) : !effective.includes(capability)))) {
812
+ throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: approved capabilities must be requested and reflected in the resolved decision`);
813
+ }
814
+ }
815
+ function validateApprovalEvidence(approved, scalarSource, sources, scopes, expiries, uses, line) {
816
+ const approvedSet = new Set(approved);
817
+ for (const [field, map] of [["approvalSources", sources], ["approvalScopes", scopes], ["approvalExpiresAt", expiries], ["approvalUses", uses]]) {
818
+ if (map && Object.keys(map).some((capability) => !approvedSet.has(capability))) {
819
+ throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: ${field} keys must be approved capabilities`);
820
+ }
821
+ }
822
+ if (approved.some((capability) => !sources?.[capability] && !scalarSource)) {
823
+ throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: each approved capability requires an approval source`);
824
+ }
825
+ if (approved.length === 0 && (scalarSource || sources || scopes || expiries || uses)) {
826
+ throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: approval evidence requires approved capabilities`);
827
+ }
828
+ }
829
+ function optionalV2Boolean(record, field, event, line) {
830
+ const value = record[field];
831
+ if (value === undefined)
832
+ return undefined;
833
+ if (typeof value !== "boolean")
834
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: ${field} must be boolean`);
835
+ return value;
836
+ }
837
+ function approvalSubject(value) {
838
+ const agentType = string(value);
839
+ return agentType === undefined || agentType === "delegate" ? "<delegate>" : agentType;
840
+ }
841
+ function structuredRefusal(value, event, line) {
842
+ if (value === undefined)
843
+ return undefined;
844
+ const parsed = object(value);
845
+ const code = string(parsed?.code);
846
+ if (!parsed || !code || !string(parsed.message)) {
847
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: refusal requires code and message`);
848
+ }
849
+ if (!V2_REFUSAL_CODES.has(code)) {
850
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: refusal has unsupported code ${safeDiagnosticValue(code)}`);
851
+ }
852
+ const unknown = Object.keys(parsed).filter((key) => !V2_REFUSAL_FIELDS.has(key));
853
+ if (unknown.length > 0)
854
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: refusal carries unsupported fields`);
855
+ const details = parsed.details === undefined ? undefined : object(parsed.details);
856
+ if (parsed.details !== undefined && (!details || Object.values(details).some((entry) => !V2_REFUSAL_DETAIL_TYPES.has(entry === null ? "null" : typeof entry)))) {
857
+ throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: refusal.details must contain scalar values`);
858
+ }
859
+ return parsed;
860
+ }
861
+ function finiteNumber(value) {
862
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
863
+ }
198
864
  function normalizeLegacyGrant(record, index) {
199
865
  const requiredArrays = ["requested", "parentGrant", "effective", "denied", "clipped", "gatedBlocked"];
200
866
  if (typeof record.ts !== "string" || typeof record.parentId !== "string" || typeof record.childId !== "string" ||
@@ -262,65 +928,6 @@ function normalizeLegacyGrant(record, index) {
262
928
  events.push(spawn);
263
929
  return events;
264
930
  }
265
- function normalizePiDaddyV1(record, seq, index) {
266
- const recordType = string(record.record_type);
267
- const action = string(record.action);
268
- if (!recordType || !action || !string(record.ts))
269
- throw new Error(`invalid pi-daddy governance v1 record at line ${index + 1}: record_type, action, and ts are required`);
270
- let type;
271
- if (recordType === "writer_lease") {
272
- if (!new Set(["acquired", "refused", "conflict", "released"]).has(action))
273
- throw new Error(`invalid writer_lease action ${JSON.stringify(action)} at line ${index + 1}`);
274
- type = action === "conflict" ? "writer_lease_conflict" : `writer_lease_${action}`;
275
- }
276
- else if (recordType === "approval") {
277
- if (!new Set(["granted", "used", "refused"]).has(action))
278
- throw new Error(`invalid approval action ${JSON.stringify(action)} at line ${index + 1}`);
279
- type = `approval_${action}`;
280
- }
281
- else if (recordType === "child_lifecycle") {
282
- if (!new Set(["started", "completed", "refused"]).has(action))
283
- throw new Error(`invalid child_lifecycle action ${JSON.stringify(action)} at line ${index + 1}`);
284
- type = action === "refused" ? "child_spawn_refused" : `child_${action}`;
285
- }
286
- else if (recordType === "capability") {
287
- if (!new Set(["requested", "granted", "refused"]).has(action))
288
- throw new Error(`invalid capability action ${JSON.stringify(action)} at line ${index + 1}`);
289
- type = `capability_${action}`;
290
- }
291
- else {
292
- throw new Error(`unsupported pi-daddy governance v1 record_type ${JSON.stringify(recordType)} at line ${index + 1}`);
293
- }
294
- return cleanEvent({
295
- event_version: TRAJECTORY_EVENT_VERSION,
296
- seq,
297
- type,
298
- source: "pi-daddy-v1",
299
- at: string(record.ts),
300
- run_id: string(record.run_id),
301
- task_id: string(record.task_id),
302
- workspace_id: string(record.workspace_id),
303
- context_id: string(record.context_id),
304
- parent_id: string(record.parent_id),
305
- child_id: string(record.child_id),
306
- capability: string(record.capability),
307
- requested_capabilities: stringArray(record.requested),
308
- effective_capabilities: stringArray(record.effective),
309
- refusal_code: string(record.refusal_code),
310
- digests: anyDefined({ task: string(record.task_digest), definition: string(record.definition_digest) }),
311
- approval: recordType === "approval" ? cleanObject({
312
- id: string(record.approval_id),
313
- capability: string(record.capability),
314
- subject: string(record.subject),
315
- source: string(record.source),
316
- scope: string(record.scope),
317
- approved_at: string(record.approved_at),
318
- expires_at: string(record.expires_at),
319
- used_at: string(record.used_at),
320
- }) : undefined,
321
- attributes: sanitizeAttributes(without(record, ["schema_version", "record_type", "ts", "action", "run_id", "task_id", "workspace_id", "context_id", "parent_id", "child_id", "capability", "requested", "effective", "refusal_code", "task_digest", "definition_digest", "approval_id", "subject", "source", "scope", "approved_at", "expires_at", "used_at"])),
322
- });
323
- }
324
931
  function legacyRefusalCode(record) {
325
932
  const denied = stringArray(record.denied) ?? [];
326
933
  const gated = stringArray(record.gatedBlocked) ?? [];
@@ -357,7 +964,7 @@ function validatePrincipalIntegrity(records) {
357
964
  records.forEach((record, index) => {
358
965
  const line = index + 1;
359
966
  if (record.schema_version !== "1.0") {
360
- throw new Error(`unsupported principal assurance schema version ${JSON.stringify(record.schema_version)} at line ${line}; expected "1.0"`);
967
+ throw new Error(`unsupported principal assurance schema version ${safeDiagnosticValue(record.schema_version)} at line ${line}; expected "1.0"`);
361
968
  }
362
969
  if (record.seq !== line)
363
970
  throw new Error(`principal assurance integrity failure at line ${line}: sequence mismatch`);
@@ -416,10 +1023,23 @@ function validTime(value) {
416
1023
  return false;
417
1024
  return day >= 1 && day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
418
1025
  }
1026
+ function safeDiagnosticValue(value) {
1027
+ if (typeof value === "number" && Number.isFinite(value))
1028
+ return String(value);
1029
+ if (typeof value === "string" && /^[A-Za-z0-9_.-]{1,64}$/.test(value) && redactText(value) === value)
1030
+ return JSON.stringify(value);
1031
+ return "[REDACTED invalid value]";
1032
+ }
1033
+ function sanitizePersistedError(error) {
1034
+ const raw = error instanceof Error ? error.message : String(error);
1035
+ return redactText(raw)
1036
+ .replace(/("?(?:password|passwd|secret|token|api[-_]?key|authorization|credential)"?\s*[:=]\s*"?)[^\s,}"']+/gi, "$1[REDACTED]")
1037
+ .slice(0, 1_000);
1038
+ }
419
1039
  function sanitizeAttributes(value) {
420
1040
  const redacted = redactArgs(value);
421
1041
  const sensitiveKey = /(secret|token|password|passphrase|api[_-]?key|authorization|cookie|credential)/i;
422
- const freeTextKey = /^(request|command|stdout|stderr|output|prompt|content)$/i;
1042
+ const freeTextKey = /^(request|command|stdout|stderr|output|prompt|content|reason|message|release_reason|diagnostic)$/i;
423
1043
  const walk = (current, key = "") => {
424
1044
  if (sensitiveKey.test(key))
425
1045
  return "[REDACTED]";
@@ -446,15 +1066,19 @@ function parseJsonl(text, label) {
446
1066
  return value;
447
1067
  }
448
1068
  catch (error) {
449
- throw new Error(`${label} ledger line ${index + 1} is invalid JSON: ${error instanceof Error ? error.message : error}`);
1069
+ throw new Error(`${label} ledger line ${index + 1} is invalid JSON [REDACTED parser detail]`);
450
1070
  }
451
1071
  });
452
1072
  }
453
1073
  function object(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : undefined; }
454
1074
  function string(value) { return typeof value === "string" && value.length ? value : undefined; }
455
1075
  function stringArray(value) { return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : undefined; }
456
- function anyDefined(value) { return Object.values(value).some((entry) => entry !== undefined) ? value : undefined; }
1076
+ function anyDefined(value) {
1077
+ const defined = cleanObject(value);
1078
+ return Object.keys(defined).length > 0 ? defined : undefined;
1079
+ }
457
1080
  function cleanObject(value) { return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)); }
1081
+ function safeAttributes(value) { return sanitizeAttributes(cleanObject(value)); }
458
1082
  function cleanEvent(event) { return cleanObject(event); }
459
1083
  function without(record, keys) { const omitted = new Set(keys); return Object.fromEntries(Object.entries(record).filter(([key, value]) => !omitted.has(key) && value !== undefined)); }
460
1084
  function walkFiles(root, relative = "") {