@skill-harness/adapters 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/closed-schema.d.ts +50 -0
- package/dist/closed-schema.js +353 -0
- package/dist/pi-daddy-ledger-v2.d.ts +460 -0
- package/dist/pi-daddy-ledger-v2.js +649 -0
- package/dist/trajectory.d.ts +40 -3
- package/dist/trajectory.js +700 -76
- package/package.json +2 -2
package/dist/trajectory.js
CHANGED
|
@@ -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)
|
|
36
|
-
|
|
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}: ${
|
|
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 ${
|
|
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
|
|
178
|
-
*
|
|
179
|
-
*
|
|
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.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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 ${
|
|
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
|
|
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) {
|
|
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 = "") {
|