@skill-harness/adapters 0.8.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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pi-daddy-ledger-v2.d.ts +460 -0
- package/dist/pi-daddy-ledger-v2.js +649 -0
- package/dist/pi-json.d.ts +21 -0
- package/dist/pi-json.js +1 -1
- package/dist/pi.js +30 -2
- package/dist/trajectory.d.ts +56 -0
- package/dist/trajectory.js +1104 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
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";
|
|
7
|
+
/** Read and normalize declared workspace-local native ledger files. */
|
|
8
|
+
export function collectTrajectorySources(cwd, sources) {
|
|
9
|
+
const files = walkFiles(cwd);
|
|
10
|
+
const streams = [];
|
|
11
|
+
const errors = [];
|
|
12
|
+
const seenFiles = new Set();
|
|
13
|
+
for (const source of sources) {
|
|
14
|
+
const matched = files.filter((file) => matchesGlob(source.path, file));
|
|
15
|
+
if (matched.length === 0) {
|
|
16
|
+
if (source.required)
|
|
17
|
+
errors.push(`required event source ${source.adapter}:${source.path} is missing`);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
for (const file of matched.sort()) {
|
|
21
|
+
const sourceFile = `${source.adapter}:${file}`;
|
|
22
|
+
if (seenFiles.has(sourceFile)) {
|
|
23
|
+
errors.push(`event source ${sourceFile} was declared more than once`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
seenFiles.add(sourceFile);
|
|
27
|
+
try {
|
|
28
|
+
const text = readFileSync(join(cwd, file), "utf8");
|
|
29
|
+
const normalized = source.adapter === "principal-assurance-v1"
|
|
30
|
+
? normalizePrincipalAssuranceLedger(text)
|
|
31
|
+
: source.adapter === "pi-daddy-v1"
|
|
32
|
+
? normalizePiDaddyLedger(text)
|
|
33
|
+
: deserializeTrajectoryEvents(text);
|
|
34
|
+
if (!normalized)
|
|
35
|
+
throw new Error("normalized-v1 source is empty, malformed, or unsupported");
|
|
36
|
+
const times = normalized.map((event) => validTime(event.at) ? Date.parse(event.at) : null);
|
|
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
|
+
}
|
|
47
|
+
}
|
|
48
|
+
streams.push({ file, adapter: source.adapter, events: normalized });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
errors.push(`${source.adapter}:${file}: ${sanitizePersistedError(error)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (streams.length > 1) {
|
|
56
|
+
if (streams.some((stream) => stream.events.some((event) => !validTime(event.at)))) {
|
|
57
|
+
errors.push("multiple native event files cannot be globally ordered because at least one event has no valid `at` timestamp");
|
|
58
|
+
}
|
|
59
|
+
const owners = new Map();
|
|
60
|
+
for (const stream of streams)
|
|
61
|
+
for (const event of stream.events) {
|
|
62
|
+
if (!event.at)
|
|
63
|
+
continue;
|
|
64
|
+
const instant = String(Date.parse(event.at));
|
|
65
|
+
const filesAtTime = owners.get(instant) ?? new Set();
|
|
66
|
+
filesAtTime.add(stream.file);
|
|
67
|
+
owners.set(instant, filesAtTime);
|
|
68
|
+
}
|
|
69
|
+
if ([...owners.values()].some((filesAtTime) => filesAtTime.size > 1)) {
|
|
70
|
+
errors.push("native event files contain equal timestamps, so strict cross-source order is ambiguous");
|
|
71
|
+
}
|
|
72
|
+
const principalRuns = new Map();
|
|
73
|
+
for (const stream of streams.filter((entry) => entry.adapter === "principal-assurance-v1")) {
|
|
74
|
+
for (const runId of new Set(stream.events.map((event) => event.run_id).filter((value) => Boolean(value)))) {
|
|
75
|
+
const prior = principalRuns.get(runId);
|
|
76
|
+
if (prior && prior !== stream.file)
|
|
77
|
+
errors.push(`principal assurance run ${runId} appears in multiple ledger files (${prior}, ${stream.file})`);
|
|
78
|
+
else
|
|
79
|
+
principalRuns.set(runId, stream.file);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { events: resequence(streams.flatMap((stream) => stream.events)), errors };
|
|
84
|
+
}
|
|
85
|
+
/** Combine independently sequenced sources by recorded time while retaining each native sequence. */
|
|
86
|
+
export function resequence(events) {
|
|
87
|
+
const native = events.map((event, index) => ({
|
|
88
|
+
event,
|
|
89
|
+
index,
|
|
90
|
+
at: validTime(event.at) ? Date.parse(event.at) : null,
|
|
91
|
+
}));
|
|
92
|
+
if (native.every((entry) => entry.at !== null))
|
|
93
|
+
native.sort((a, b) => a.at - b.at || a.index - b.index);
|
|
94
|
+
return native.map(({ event }, index) => ({
|
|
95
|
+
...event,
|
|
96
|
+
seq: index + 1,
|
|
97
|
+
attributes: { native_seq: event.seq, ...(event.attributes ?? {}) },
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
/** Normalize pi's structured calls into adapter-neutral start/completion events. */
|
|
101
|
+
export function normalizePiTraces(traces) {
|
|
102
|
+
const events = [];
|
|
103
|
+
let seq = 1;
|
|
104
|
+
for (const trace of [...traces].sort((a, b) => a.turn - b.turn)) {
|
|
105
|
+
const base = { scenario_id: trace.scenario_id, rep: trace.rep, turn: trace.turn };
|
|
106
|
+
const calls = [...trace.tool_calls].sort((a, b) => a.issueIndex - b.issueIndex);
|
|
107
|
+
for (const call of calls) {
|
|
108
|
+
events.push({
|
|
109
|
+
event_version: TRAJECTORY_EVENT_VERSION,
|
|
110
|
+
seq: seq++,
|
|
111
|
+
type: "tool_started",
|
|
112
|
+
source: "pi",
|
|
113
|
+
at: call.started_at,
|
|
114
|
+
tool: call.name,
|
|
115
|
+
attributes: { ...base, tool_call_id: call.id, args: call.args, issue_index: call.issueIndex },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
for (const call of calls.filter((item) => item.completionIndex >= 0).sort((a, b) => a.completionIndex - b.completionIndex)) {
|
|
119
|
+
events.push({
|
|
120
|
+
event_version: TRAJECTORY_EVENT_VERSION,
|
|
121
|
+
seq: seq++,
|
|
122
|
+
type: "tool_completed",
|
|
123
|
+
source: "pi",
|
|
124
|
+
at: call.completed_at,
|
|
125
|
+
tool: call.name,
|
|
126
|
+
attributes: {
|
|
127
|
+
...base,
|
|
128
|
+
tool_call_id: call.id,
|
|
129
|
+
success: !call.isError,
|
|
130
|
+
issue_index: call.issueIndex,
|
|
131
|
+
completion_index: call.completionIndex,
|
|
132
|
+
result_sha256: call.result.sha256,
|
|
133
|
+
...(call.result.details ? { details: call.result.details } : {}),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return events;
|
|
139
|
+
}
|
|
140
|
+
/** Normalize principal-pi-skills' immutable assurance event schema v1.0. */
|
|
141
|
+
export function normalizePrincipalAssuranceLedger(text) {
|
|
142
|
+
const records = parseJsonl(text, "principal assurance");
|
|
143
|
+
validatePrincipalIntegrity(records);
|
|
144
|
+
return records.map((record, index) => {
|
|
145
|
+
if (record.schema_version !== "1.0") {
|
|
146
|
+
throw new Error(`unsupported principal assurance schema version ${safeDiagnosticValue(record.schema_version)} at line ${index + 1}; expected \"1.0\"`);
|
|
147
|
+
}
|
|
148
|
+
if (!Number.isInteger(record.seq) || Number(record.seq) < 1 || typeof record.type !== "string" || typeof record.run_id !== "string") {
|
|
149
|
+
throw new Error(`invalid principal assurance v1 event at line ${index + 1}: seq, type, and run_id are required`);
|
|
150
|
+
}
|
|
151
|
+
const packet = object(record.packet);
|
|
152
|
+
const definitionDigests = object(packet?.definition_digests);
|
|
153
|
+
const definition = typeof record.definition_digest === "string"
|
|
154
|
+
? record.definition_digest
|
|
155
|
+
: typeof definitionDigests?.["skill:build"] === "string"
|
|
156
|
+
? definitionDigests["skill:build"]
|
|
157
|
+
: undefined;
|
|
158
|
+
const taskId = string(record.task_id) ?? string(packet?.task_id);
|
|
159
|
+
const workspaceId = string(record.workspace_id) ?? string(packet?.workspace_id);
|
|
160
|
+
const plan = string(record.plan_digest) ?? string(packet?.plan_digest);
|
|
161
|
+
const head = string(record.head_sha);
|
|
162
|
+
const tree = string(record.tree_sha);
|
|
163
|
+
const attributes = without(record, [
|
|
164
|
+
"schema_version", "seq", "type", "at", "run_id", "task_id", "workspace_id", "context_id",
|
|
165
|
+
"finding_id", "phase", "plan_digest", "definition_digest", "head_sha", "tree_sha", "exit_code",
|
|
166
|
+
]);
|
|
167
|
+
return cleanEvent({
|
|
168
|
+
event_version: TRAJECTORY_EVENT_VERSION,
|
|
169
|
+
seq: Number(record.seq),
|
|
170
|
+
type: record.type,
|
|
171
|
+
source: "principal-assurance-v1",
|
|
172
|
+
at: string(record.at),
|
|
173
|
+
run_id: record.run_id,
|
|
174
|
+
task_id: taskId,
|
|
175
|
+
workspace_id: workspaceId,
|
|
176
|
+
context_id: string(record.context_id),
|
|
177
|
+
finding_id: string(record.finding_id),
|
|
178
|
+
phase: string(record.phase),
|
|
179
|
+
exit_code: Number.isInteger(record.exit_code) ? Number(record.exit_code) : undefined,
|
|
180
|
+
digests: anyDefined({ plan, definition, head, tree }),
|
|
181
|
+
requirements: stringArray(record.requirements),
|
|
182
|
+
attributes: sanitizeAttributes(attributes),
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
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.
|
|
190
|
+
*/
|
|
191
|
+
export function normalizePiDaddyLedger(text) {
|
|
192
|
+
const records = parseJsonl(text, "pi-daddy");
|
|
193
|
+
validatePiDaddyTimestampOrder(records);
|
|
194
|
+
const out = [];
|
|
195
|
+
let seq = 1;
|
|
196
|
+
records.forEach((record, 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++ });
|
|
208
|
+
return;
|
|
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
|
+
}
|
|
216
|
+
for (const event of normalizeLegacyGrant(record, index))
|
|
217
|
+
out.push({ ...event, seq: seq++ });
|
|
218
|
+
});
|
|
219
|
+
return out;
|
|
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
|
+
}
|
|
864
|
+
function normalizeLegacyGrant(record, index) {
|
|
865
|
+
const requiredArrays = ["requested", "parentGrant", "effective", "denied", "clipped", "gatedBlocked"];
|
|
866
|
+
if (typeof record.ts !== "string" || typeof record.parentId !== "string" || typeof record.childId !== "string" ||
|
|
867
|
+
!Number.isInteger(record.depth) || typeof record.blocked !== "boolean" || typeof record.executor !== "string" ||
|
|
868
|
+
requiredArrays.some((field) => !Array.isArray(record[field]) || !record[field].every((value) => typeof value === "string"))) {
|
|
869
|
+
throw new Error(`invalid unversioned pi-daddy grant record at line ${index + 1}; expected the 0.17 GrantRecord shape`);
|
|
870
|
+
}
|
|
871
|
+
const requested = record.requested;
|
|
872
|
+
const effective = record.effective;
|
|
873
|
+
const denied = record.denied;
|
|
874
|
+
const gated = record.gatedBlocked;
|
|
875
|
+
const digest = object(record.definitionDigest);
|
|
876
|
+
const common = {
|
|
877
|
+
event_version: TRAJECTORY_EVENT_VERSION,
|
|
878
|
+
source: "pi-daddy-0.17",
|
|
879
|
+
at: record.ts,
|
|
880
|
+
parent_id: record.parentId,
|
|
881
|
+
child_id: record.childId,
|
|
882
|
+
};
|
|
883
|
+
const attributes = sanitizeAttributes({
|
|
884
|
+
native_record: index + 1,
|
|
885
|
+
depth: record.depth,
|
|
886
|
+
agent_type: record.agentType,
|
|
887
|
+
executor: record.executor,
|
|
888
|
+
parent_grant: record.parentGrant,
|
|
889
|
+
clipped: record.clipped,
|
|
890
|
+
gated_blocked: gated,
|
|
891
|
+
gate_outcome: record.gateOutcome,
|
|
892
|
+
human_denied: record.humanDenied === true,
|
|
893
|
+
reason: record.reason,
|
|
894
|
+
definition_name: digest?.name,
|
|
895
|
+
legacy_schema: "pi-daddy-grant-ledger/0.17",
|
|
896
|
+
});
|
|
897
|
+
const refusal = record.blocked ? legacyRefusalCode(record) : undefined;
|
|
898
|
+
const spawn = cleanEvent({
|
|
899
|
+
...common,
|
|
900
|
+
type: record.blocked ? "child_spawn_refused" : "child_started",
|
|
901
|
+
requested_capabilities: requested,
|
|
902
|
+
effective_capabilities: effective,
|
|
903
|
+
refusal_code: refusal,
|
|
904
|
+
digests: anyDefined({ definition: string(digest?.sha256) }),
|
|
905
|
+
attributes,
|
|
906
|
+
});
|
|
907
|
+
const events = [
|
|
908
|
+
...requested.map((capability) => ({ ...common, type: "capability_requested", capability, requested_capabilities: requested, effective_capabilities: effective, attributes })),
|
|
909
|
+
...effective.map((capability) => ({ ...common, type: "capability_granted", capability, requested_capabilities: requested, effective_capabilities: effective, attributes })),
|
|
910
|
+
...[...new Set([...denied, ...gated])].map((capability) => ({ ...common, type: "capability_refused", capability, requested_capabilities: requested, effective_capabilities: effective, refusal_code: denied.includes(capability) ? "CAPABILITY_ESCALATION" : refusal, attributes })),
|
|
911
|
+
];
|
|
912
|
+
const sources = object(record.approvalSources);
|
|
913
|
+
const scopes = object(record.approvalScopes);
|
|
914
|
+
for (const capability of stringArray(record.approved) ?? []) {
|
|
915
|
+
events.push(cleanEvent({
|
|
916
|
+
...common,
|
|
917
|
+
type: "approval_used",
|
|
918
|
+
capability,
|
|
919
|
+
approval: {
|
|
920
|
+
capability,
|
|
921
|
+
source: string(sources?.[capability]) ?? string(record.approvalSource),
|
|
922
|
+
scope: string(scopes?.[capability]) ?? string(record.approvalScope),
|
|
923
|
+
used_at: record.ts,
|
|
924
|
+
},
|
|
925
|
+
attributes,
|
|
926
|
+
}));
|
|
927
|
+
}
|
|
928
|
+
events.push(spawn);
|
|
929
|
+
return events;
|
|
930
|
+
}
|
|
931
|
+
function legacyRefusalCode(record) {
|
|
932
|
+
const denied = stringArray(record.denied) ?? [];
|
|
933
|
+
const gated = stringArray(record.gatedBlocked) ?? [];
|
|
934
|
+
const reason = string(record.reason) ?? "";
|
|
935
|
+
if (denied.length)
|
|
936
|
+
return "CAPABILITY_ESCALATION";
|
|
937
|
+
if (/declares no `allowed-tools`/i.test(reason))
|
|
938
|
+
return "UNDECLARED_CAPABILITIES";
|
|
939
|
+
if (/unknown capabilit/i.test(reason))
|
|
940
|
+
return "UNKNOWN_CAPABILITY";
|
|
941
|
+
if (/depth limit/i.test(reason))
|
|
942
|
+
return "DEPTH_LIMIT";
|
|
943
|
+
if (/needs a task/i.test(reason))
|
|
944
|
+
return "MISSING_TASK";
|
|
945
|
+
if (/universal capability|cannot narrow/i.test(reason))
|
|
946
|
+
return "NON_NARROWING_GRANT";
|
|
947
|
+
if (gated.length) {
|
|
948
|
+
if (record.humanDenied === true || record.gateOutcome === "declined")
|
|
949
|
+
return "APPROVAL_DECLINED";
|
|
950
|
+
if (record.gateOutcome === "no-ui")
|
|
951
|
+
return "APPROVAL_NO_UI";
|
|
952
|
+
if (record.gateOutcome === "dismissed")
|
|
953
|
+
return "APPROVAL_DISMISSED";
|
|
954
|
+
if (record.gateOutcome === "error")
|
|
955
|
+
return "APPROVAL_ERROR";
|
|
956
|
+
return "APPROVAL_REQUIRED";
|
|
957
|
+
}
|
|
958
|
+
return "LEGACY_UNCLASSIFIED";
|
|
959
|
+
}
|
|
960
|
+
function validatePrincipalIntegrity(records) {
|
|
961
|
+
let previous = null;
|
|
962
|
+
let previousTime = null;
|
|
963
|
+
let runId = null;
|
|
964
|
+
records.forEach((record, index) => {
|
|
965
|
+
const line = index + 1;
|
|
966
|
+
if (record.schema_version !== "1.0") {
|
|
967
|
+
throw new Error(`unsupported principal assurance schema version ${safeDiagnosticValue(record.schema_version)} at line ${line}; expected "1.0"`);
|
|
968
|
+
}
|
|
969
|
+
if (record.seq !== line)
|
|
970
|
+
throw new Error(`principal assurance integrity failure at line ${line}: sequence mismatch`);
|
|
971
|
+
if (index === 0 && record.type !== "run_initialized")
|
|
972
|
+
throw new Error("principal assurance integrity failure: first event must initialize the run");
|
|
973
|
+
if (typeof record.run_id !== "string" || !record.run_id)
|
|
974
|
+
throw new Error(`principal assurance integrity failure at line ${line}: run_id is missing`);
|
|
975
|
+
if (runId === null)
|
|
976
|
+
runId = record.run_id;
|
|
977
|
+
else if (record.run_id !== runId)
|
|
978
|
+
throw new Error(`principal assurance integrity failure at line ${line}: run_id changed`);
|
|
979
|
+
if (record.prev_digest !== previous)
|
|
980
|
+
throw new Error(`principal assurance integrity failure at line ${line}: previous digest mismatch`);
|
|
981
|
+
if (typeof record.event_digest !== "string" || !/^[a-f0-9]{64}$/i.test(record.event_digest)) {
|
|
982
|
+
throw new Error(`principal assurance integrity failure at line ${line}: event_digest is invalid`);
|
|
983
|
+
}
|
|
984
|
+
const copy = { ...record };
|
|
985
|
+
delete copy.event_digest;
|
|
986
|
+
const expected = createHash("sha256").update(canonicalJson(copy)).digest("hex");
|
|
987
|
+
if (record.event_digest !== expected)
|
|
988
|
+
throw new Error(`principal assurance integrity failure at line ${line}: event digest mismatch`);
|
|
989
|
+
if (!validTime(typeof record.at === "string" ? record.at : undefined))
|
|
990
|
+
throw new Error(`invalid principal assurance v1 event at line ${line}: at must be a date-time`);
|
|
991
|
+
const at = Date.parse(record.at);
|
|
992
|
+
if (previousTime !== null && at < previousTime)
|
|
993
|
+
throw new Error(`principal assurance integrity failure at line ${line}: timestamp moves backwards`);
|
|
994
|
+
previousTime = at;
|
|
995
|
+
previous = record.event_digest;
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
function canonicalJson(value) {
|
|
999
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
1000
|
+
return JSON.stringify(value);
|
|
1001
|
+
if (typeof value === "number") {
|
|
1002
|
+
if (!Number.isFinite(value))
|
|
1003
|
+
throw new Error("principal assurance event contains a non-finite number");
|
|
1004
|
+
return JSON.stringify(value);
|
|
1005
|
+
}
|
|
1006
|
+
if (Array.isArray(value))
|
|
1007
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
1008
|
+
if (!value || typeof value !== "object")
|
|
1009
|
+
throw new Error("principal assurance event contains a non-JSON value");
|
|
1010
|
+
return `{${Object.entries(value)
|
|
1011
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
1012
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
|
|
1013
|
+
.join(",")}}`;
|
|
1014
|
+
}
|
|
1015
|
+
function validTime(value) {
|
|
1016
|
+
if (typeof value !== "string")
|
|
1017
|
+
return false;
|
|
1018
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
1019
|
+
if (!match || !Number.isFinite(Date.parse(value)))
|
|
1020
|
+
return false;
|
|
1021
|
+
const [, year, month, day, hour, minute, second] = match.map(Number);
|
|
1022
|
+
if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59)
|
|
1023
|
+
return false;
|
|
1024
|
+
return day >= 1 && day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
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
|
+
}
|
|
1039
|
+
function sanitizeAttributes(value) {
|
|
1040
|
+
const redacted = redactArgs(value);
|
|
1041
|
+
const sensitiveKey = /(secret|token|password|passphrase|api[_-]?key|authorization|cookie|credential)/i;
|
|
1042
|
+
const freeTextKey = /^(request|command|stdout|stderr|output|prompt|content|reason|message|release_reason|diagnostic)$/i;
|
|
1043
|
+
const walk = (current, key = "") => {
|
|
1044
|
+
if (sensitiveKey.test(key))
|
|
1045
|
+
return "[REDACTED]";
|
|
1046
|
+
if (typeof current === "string" && freeTextKey.test(key)) {
|
|
1047
|
+
return `[REDACTED sha256:${createHash("sha256").update(current).digest("hex")}]`;
|
|
1048
|
+
}
|
|
1049
|
+
if (Array.isArray(current))
|
|
1050
|
+
return current.map((entry) => walk(entry));
|
|
1051
|
+
if (current && typeof current === "object")
|
|
1052
|
+
return Object.fromEntries(Object.entries(current).map(([childKey, entry]) => [childKey, walk(entry, childKey)]));
|
|
1053
|
+
return current;
|
|
1054
|
+
};
|
|
1055
|
+
return walk(redacted);
|
|
1056
|
+
}
|
|
1057
|
+
function parseJsonl(text, label) {
|
|
1058
|
+
const lines = text.split("\n").filter((line) => line.trim());
|
|
1059
|
+
if (!lines.length)
|
|
1060
|
+
throw new Error(`${label} ledger is empty`);
|
|
1061
|
+
return lines.map((line, index) => {
|
|
1062
|
+
try {
|
|
1063
|
+
const value = JSON.parse(line);
|
|
1064
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1065
|
+
throw new Error("record is not an object");
|
|
1066
|
+
return value;
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
throw new Error(`${label} ledger line ${index + 1} is invalid JSON [REDACTED parser detail]`);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
function object(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : undefined; }
|
|
1074
|
+
function string(value) { return typeof value === "string" && value.length ? value : undefined; }
|
|
1075
|
+
function stringArray(value) { return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : undefined; }
|
|
1076
|
+
function anyDefined(value) {
|
|
1077
|
+
const defined = cleanObject(value);
|
|
1078
|
+
return Object.keys(defined).length > 0 ? defined : undefined;
|
|
1079
|
+
}
|
|
1080
|
+
function cleanObject(value) { return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)); }
|
|
1081
|
+
function safeAttributes(value) { return sanitizeAttributes(cleanObject(value)); }
|
|
1082
|
+
function cleanEvent(event) { return cleanObject(event); }
|
|
1083
|
+
function without(record, keys) { const omitted = new Set(keys); return Object.fromEntries(Object.entries(record).filter(([key, value]) => !omitted.has(key) && value !== undefined)); }
|
|
1084
|
+
function walkFiles(root, relative = "") {
|
|
1085
|
+
const out = [];
|
|
1086
|
+
let entries;
|
|
1087
|
+
try {
|
|
1088
|
+
entries = readdirSync(join(root, relative), { withFileTypes: true });
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
return out;
|
|
1092
|
+
}
|
|
1093
|
+
for (const entry of entries) {
|
|
1094
|
+
const path = relative ? `${relative}/${entry.name}` : entry.name;
|
|
1095
|
+
if (entry.isDirectory())
|
|
1096
|
+
out.push(...walkFiles(root, path));
|
|
1097
|
+
else if (entry.isFile())
|
|
1098
|
+
out.push(path);
|
|
1099
|
+
// Symlinks are not followed: an event source is workspace-local evidence,
|
|
1100
|
+
// not a route for a spec to read an arbitrary host path.
|
|
1101
|
+
}
|
|
1102
|
+
return out;
|
|
1103
|
+
}
|
|
1104
|
+
//# sourceMappingURL=trajectory.js.map
|