@scotthuang/agent-knock-knock 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +19 -2
  3. package/dist/src/claude-local-transcript-provider.d.ts +16 -0
  4. package/dist/src/claude-local-transcript-provider.js +111 -47
  5. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  6. package/dist/src/cli.js +3738 -812
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/codex-lifecycle-compatibility.d.ts +2 -0
  9. package/dist/src/codex-lifecycle-compatibility.js +13 -0
  10. package/dist/src/codex-lifecycle-compatibility.js.map +1 -0
  11. package/dist/src/codex-store-adapter.js +8 -7
  12. package/dist/src/codex-store-adapter.js.map +1 -1
  13. package/dist/src/codex-terminal-agent-adapter.js +9 -8
  14. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  15. package/dist/src/live-lifecycle-evidence.d.ts +232 -0
  16. package/dist/src/live-lifecycle-evidence.js +1157 -0
  17. package/dist/src/live-lifecycle-evidence.js.map +1 -0
  18. package/dist/src/live-lifecycle-smoke-evidence.d.ts +13 -0
  19. package/dist/src/live-lifecycle-smoke-evidence.js +167 -0
  20. package/dist/src/live-lifecycle-smoke-evidence.js.map +1 -0
  21. package/dist/src/live-lifecycle-smoke.d.ts +123 -0
  22. package/dist/src/live-lifecycle-smoke.js +937 -0
  23. package/dist/src/live-lifecycle-smoke.js.map +1 -0
  24. package/dist/src/openclaw-plugin-helpers.d.ts +1 -0
  25. package/dist/src/openclaw-plugin-helpers.js +30 -2
  26. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  27. package/dist/src/openclaw-plugin.js +193 -31
  28. package/dist/src/openclaw-plugin.js.map +1 -1
  29. package/dist/src/protocol.d.ts +14 -0
  30. package/dist/src/protocol.js +87 -0
  31. package/dist/src/protocol.js.map +1 -1
  32. package/dist/src/store.js +17 -2
  33. package/dist/src/store.js.map +1 -1
  34. package/dist/src/terminal-agent-bridge.d.ts +31 -2
  35. package/dist/src/terminal-agent-bridge.js +231 -11
  36. package/dist/src/terminal-agent-bridge.js.map +1 -1
  37. package/dist/src/terminal-submission-acceptance.d.ts +78 -0
  38. package/dist/src/terminal-submission-acceptance.js +454 -0
  39. package/dist/src/terminal-submission-acceptance.js.map +1 -0
  40. package/package.json +3 -1
@@ -0,0 +1,1157 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ export const LIVE_LIFECYCLE_EVIDENCE_KIND = "agent-knock-knock/native-lifecycle-live-smoke";
3
+ export const LIVE_LIFECYCLE_EVIDENCE_SCHEMA_VERSION = 1;
4
+ export const LIVE_LIFECYCLE_ATTESTATION_BEGIN = "-----BEGIN AKK NATIVE LIFECYCLE LIVE SMOKE V1-----";
5
+ export const LIVE_LIFECYCLE_ATTESTATION_END = "-----END AKK NATIVE LIFECYCLE LIVE SMOKE V1-----";
6
+ const DEFAULT_MAX_AGE_HOURS = 72;
7
+ const CLOCK_SKEW_MS = 5 * 60 * 1_000;
8
+ const MAX_RUN_DURATION_MS = 6 * 60 * 60 * 1_000;
9
+ const DIGEST_DOMAIN = "agent-knock-knock/live-lifecycle-evidence/v1";
10
+ const FINGERPRINT_DOMAIN = "agent-knock-knock/live-lifecycle-evidence/fingerprint/v1";
11
+ const AGENTS = ["codex", "claude"];
12
+ const STEP_NAMES = [
13
+ "preflight",
14
+ "new_thread",
15
+ "send",
16
+ "wait_completion",
17
+ "list_resumable_threads",
18
+ "resume_thread",
19
+ "final_verify"
20
+ ];
21
+ export class LiveLifecycleEvidenceValidationError extends Error {
22
+ code;
23
+ constructor(code, message) {
24
+ super(message);
25
+ this.code = code;
26
+ this.name = "LiveLifecycleEvidenceValidationError";
27
+ }
28
+ }
29
+ /**
30
+ * Build an allowlisted evidence document from raw runtime results. Raw IDs are
31
+ * used only to calculate per-run, domain-separated fingerprints and relationship
32
+ * assertions; they are never copied into the returned document.
33
+ */
34
+ export function create(input) {
35
+ assertRawEnvelope(input);
36
+ const runId = input.runId ?? randomBytes(16).toString("hex");
37
+ const fingerprintSalt = input.fingerprintSalt ?? randomBytes(32).toString("hex");
38
+ assertPattern(runId, /^[0-9a-f]{32}$/u, "invalid_run_id", "runId");
39
+ assertPattern(fingerprintSalt, /^[0-9a-f]{64}$/u, "invalid_fingerprint_salt", "fingerprintSalt");
40
+ const matrix = {};
41
+ for (const agent of AGENTS) {
42
+ const scenario = Object.prototype.hasOwnProperty.call(input.scenarios, agent)
43
+ ? input.scenarios[agent]
44
+ : undefined;
45
+ if (scenario !== undefined) {
46
+ matrix[agent] = createScenarioEvidence(scenario, fingerprintSalt);
47
+ }
48
+ }
49
+ if (Object.keys(matrix).length === 0) {
50
+ fail("matrix_empty", "At least one Codex or Claude scenario is required.");
51
+ }
52
+ const statuses = Object.values(matrix).map((scenario) => scenario.status);
53
+ const derivedStatus = deriveResultStatus(statuses);
54
+ const unsigned = {
55
+ schema_version: LIVE_LIFECYCLE_EVIDENCE_SCHEMA_VERSION,
56
+ kind: LIVE_LIFECYCLE_EVIDENCE_KIND,
57
+ run_id: runId,
58
+ package: {
59
+ name: input.packageName,
60
+ version: input.packageVersion
61
+ },
62
+ source: {
63
+ commit: input.commit,
64
+ worktree_clean: input.worktreeClean
65
+ },
66
+ started_at: input.startedAt,
67
+ completed_at: input.completedAt,
68
+ overall_status: Object.keys(matrix).length === AGENTS.length ? derivedStatus :
69
+ derivedStatus === "uncertain" ? "uncertain" : "failed",
70
+ matrix
71
+ };
72
+ // Creation is also a redaction boundary: even failed/uncertain evidence may
73
+ // be written as machine-readable diagnostics, so unsafe plaintext must not
74
+ // survive until release validation.
75
+ scanForSensitiveMaterial(unsigned, "$", new Set());
76
+ const evidence = {
77
+ ...unsigned,
78
+ digest: digestUnsignedEvidence(unsigned)
79
+ };
80
+ return assertEvidence(evidence, [], {
81
+ allowNonPassing: true,
82
+ allowDirtyWorktree: true
83
+ });
84
+ }
85
+ export const createLiveLifecycleEvidence = create;
86
+ /**
87
+ * Validate an evidence document for release use. The default release matrix is
88
+ * Codex plus Claude, and freshness defaults to 72 hours with five minutes of
89
+ * allowed forward clock skew.
90
+ */
91
+ export function validate(value, options) {
92
+ const evidence = assertEvidence(value, options.requireAgents ?? AGENTS);
93
+ assertExpectedValue(evidence.package.name, options.expectedPackageName, "package_name_mismatch", "package.name");
94
+ assertExpectedValue(evidence.package.version, options.expectedPackageVersion, "package_version_mismatch", "package.version");
95
+ assertPattern(options.expectedCommit, /^[0-9a-f]{40}$/u, "invalid_expected_commit", "expectedCommit");
96
+ assertExpectedValue(evidence.source.commit, options.expectedCommit, "commit_mismatch", "source.commit");
97
+ const maxAgeHours = options.maxAgeHours ?? DEFAULT_MAX_AGE_HOURS;
98
+ if (!Number.isFinite(maxAgeHours) || maxAgeHours <= 0) {
99
+ fail("invalid_max_age", "maxAgeHours must be a positive finite number.");
100
+ }
101
+ const now = parseNow(options.now);
102
+ const startedAt = parseTimestamp(evidence.started_at, "started_at");
103
+ const completedAt = parseTimestamp(evidence.completed_at, "completed_at");
104
+ if (completedAt < startedAt) {
105
+ fail("invalid_time_order", "Evidence completed_at precedes started_at.");
106
+ }
107
+ if (completedAt - startedAt > MAX_RUN_DURATION_MS) {
108
+ fail("run_duration_exceeded", "Evidence run duration exceeds six hours.");
109
+ }
110
+ if (completedAt > now + CLOCK_SKEW_MS) {
111
+ fail("future_evidence", "Evidence completed_at is too far in the future.");
112
+ }
113
+ if (now - completedAt > maxAgeHours * 60 * 60 * 1_000) {
114
+ fail("stale_evidence", `Evidence is older than ${maxAgeHours} hours.`);
115
+ }
116
+ for (const agent of AGENTS) {
117
+ const scenario = evidence.matrix[agent];
118
+ if (scenario === undefined) {
119
+ continue;
120
+ }
121
+ const scenarioStartedAt = parseTimestamp(scenario.started_at, `matrix.${agent}.started_at`);
122
+ const scenarioCompletedAt = parseTimestamp(scenario.completed_at, `matrix.${agent}.completed_at`);
123
+ if (scenarioCompletedAt < scenarioStartedAt ||
124
+ scenarioStartedAt < startedAt ||
125
+ scenarioCompletedAt > completedAt) {
126
+ fail("invalid_scenario_time_order", `${agent} scenario timestamps fall outside the evidence interval.`);
127
+ }
128
+ if (scenarioCompletedAt - scenarioStartedAt > MAX_RUN_DURATION_MS) {
129
+ fail("run_duration_exceeded", `${agent} scenario duration exceeds six hours.`);
130
+ }
131
+ if (scenarioCompletedAt > now + CLOCK_SKEW_MS) {
132
+ fail("future_evidence", `${agent} scenario is too far in the future.`);
133
+ }
134
+ if (now - scenarioCompletedAt > maxAgeHours * 60 * 60 * 1_000) {
135
+ fail("stale_scenario", `${agent} scenario is older than ${maxAgeHours} hours.`);
136
+ }
137
+ }
138
+ return evidence;
139
+ }
140
+ export const validateLiveLifecycleEvidence = validate;
141
+ /** Serialize canonical evidence into a block suitable for an annotated tag body. */
142
+ export function serializeAttestation(evidence) {
143
+ const verified = assertEvidence(evidence, AGENTS);
144
+ const canonical = canonicalJson(verified);
145
+ const payload = Buffer.from(canonical, "utf8").toString("base64url");
146
+ return [
147
+ LIVE_LIFECYCLE_ATTESTATION_BEGIN,
148
+ payload,
149
+ LIVE_LIFECYCLE_ATTESTATION_END
150
+ ].join("\n");
151
+ }
152
+ /** Extract and structurally verify one attestation block from an annotated tag. */
153
+ export function parseAttestation(message) {
154
+ if (typeof message !== "string") {
155
+ fail("invalid_attestation", "Attestation message must be a string.");
156
+ }
157
+ const normalized = message.replace(/\r\n?/gu, "\n");
158
+ const beginMatches = countOccurrences(normalized, LIVE_LIFECYCLE_ATTESTATION_BEGIN);
159
+ const endMatches = countOccurrences(normalized, LIVE_LIFECYCLE_ATTESTATION_END);
160
+ if (beginMatches !== 1 || endMatches !== 1) {
161
+ fail("invalid_attestation_markers", "Annotated tag must contain exactly one complete live-smoke attestation block.");
162
+ }
163
+ const beginIndex = normalized.indexOf(LIVE_LIFECYCLE_ATTESTATION_BEGIN);
164
+ const payloadStart = beginIndex + LIVE_LIFECYCLE_ATTESTATION_BEGIN.length;
165
+ const endIndex = normalized.indexOf(LIVE_LIFECYCLE_ATTESTATION_END, payloadStart);
166
+ if (endIndex < payloadStart) {
167
+ fail("invalid_attestation_markers", "Attestation markers are out of order.");
168
+ }
169
+ const payload = normalized.slice(payloadStart, endIndex).trim();
170
+ if (!/^[A-Za-z0-9_-]+$/u.test(payload)) {
171
+ fail("invalid_attestation_payload", "Attestation payload is not canonical base64url.");
172
+ }
173
+ let decoded;
174
+ try {
175
+ const bytes = Buffer.from(payload, "base64url");
176
+ if (bytes.toString("base64url") !== payload) {
177
+ fail("invalid_attestation_payload", "Attestation base64url is not canonical.");
178
+ }
179
+ decoded = bytes.toString("utf8");
180
+ }
181
+ catch (error) {
182
+ if (error instanceof LiveLifecycleEvidenceValidationError) {
183
+ throw error;
184
+ }
185
+ fail("invalid_attestation_payload", "Attestation payload cannot be decoded.");
186
+ }
187
+ let parsed;
188
+ try {
189
+ parsed = JSON.parse(decoded);
190
+ }
191
+ catch {
192
+ fail("invalid_attestation_json", "Attestation payload is not valid JSON.");
193
+ }
194
+ if (canonicalJson(parsed) !== decoded) {
195
+ fail("noncanonical_attestation", "Attestation JSON is not canonical.");
196
+ }
197
+ return assertEvidence(parsed, AGENTS);
198
+ }
199
+ function createScenarioEvidence(raw, salt) {
200
+ assertRawScenario(raw);
201
+ const before = raw.before === undefined ? undefined :
202
+ createSnapshotEvidence(raw.before, salt);
203
+ const afterNew = raw.afterNew === undefined ? undefined :
204
+ createSnapshotEvidence(raw.afterNew, salt);
205
+ const afterResume = raw.afterResume === undefined ? undefined :
206
+ createSnapshotEvidence(raw.afterResume, salt);
207
+ const fingerprint = (domain, value) => fingerprintValue(salt, domain, requireRawIdentifier(value, domain));
208
+ const send = raw.send === undefined
209
+ ? undefined
210
+ : {
211
+ status: raw.send.status,
212
+ turn_fingerprint: fingerprint("turn", raw.send.turnId),
213
+ session_fingerprint: fingerprint("session", raw.send.sessionId),
214
+ binding_fingerprint: fingerprint("binding", raw.send.bindingId),
215
+ binding_generation: raw.send.bindingGeneration
216
+ };
217
+ const newTransition = raw.transitions?.newThread === undefined ? undefined :
218
+ createTransitionEvidence(raw.transitions.newThread, salt);
219
+ const resumeTransition = raw.transitions?.resumeThread === undefined ? undefined :
220
+ createTransitionEvidence(raw.transitions.resumeThread, salt);
221
+ const resumeCandidate = raw.resumeCandidate === undefined
222
+ ? undefined
223
+ : {
224
+ native_thread_fingerprint: fingerprint("native_thread", raw.resumeCandidate.nativeThreadId),
225
+ managed_session_fingerprint: raw.resumeCandidate.managedSessionId === null
226
+ ? null
227
+ : fingerprint("session", raw.resumeCandidate.managedSessionId),
228
+ exact_candidate_count: raw.resumeCandidate.exactCandidateCount,
229
+ resumable: raw.resumeCandidate.resumable,
230
+ active_elsewhere: raw.resumeCandidate.activeElsewhere,
231
+ fresh_candidate_token_present: raw.resumeCandidate.freshCandidateTokenPresent
232
+ };
233
+ const steps = raw.steps.map((step) => ({
234
+ name: step.name,
235
+ status: step.status,
236
+ duration_ms: step.durationMs
237
+ }));
238
+ const status = deriveResultStatus([
239
+ raw.status,
240
+ ...steps.map((step) => step.status),
241
+ ...(raw.send === undefined
242
+ ? []
243
+ : [raw.send.status === "completed" ? "passed" : raw.send.status]),
244
+ ...(raw.transitions?.newThread === undefined
245
+ ? []
246
+ : [raw.transitions.newThread.status === "committed"
247
+ ? "passed"
248
+ : raw.transitions.newThread.status]),
249
+ ...(raw.transitions?.resumeThread === undefined
250
+ ? []
251
+ : [raw.transitions.resumeThread.status === "committed"
252
+ ? "passed"
253
+ : raw.transitions.resumeThread.status])
254
+ ]);
255
+ if (status === "passed") {
256
+ if (raw.failureStage !== undefined ||
257
+ raw.reasonCode !== undefined ||
258
+ raw.doNotRetry !== undefined) {
259
+ fail("unexpected_failure_detail", "Passing scenarios cannot carry failure details.");
260
+ }
261
+ }
262
+ else {
263
+ if (raw.failureStage === undefined) {
264
+ fail("missing_failure_stage", "Non-passing scenarios require failureStage.");
265
+ }
266
+ requireReasonCode(raw.reasonCode, "reasonCode");
267
+ if (status === "uncertain" && raw.doNotRetry !== true) {
268
+ fail("retry_guard_missing", "An uncertain lifecycle run must record doNotRetry=true.");
269
+ }
270
+ }
271
+ const assertions = deriveRelationshipAssertions({
272
+ before,
273
+ afterNew,
274
+ afterResume,
275
+ resumeCandidate,
276
+ send,
277
+ newTransition,
278
+ resumeTransition,
279
+ tmuxTarget: raw.tmuxTarget,
280
+ panePid: raw.panePid
281
+ });
282
+ return {
283
+ status,
284
+ ...(status === "passed" ? {} : {
285
+ failure_stage: raw.failureStage,
286
+ reason_code: raw.reasonCode,
287
+ ...(raw.doNotRetry === true ? { do_not_retry: true } : {})
288
+ }),
289
+ ...(raw.agentVersion === undefined ? {} : { agent_version: raw.agentVersion }),
290
+ tmux_target: raw.tmuxTarget,
291
+ pane_pid: raw.panePid,
292
+ started_at: raw.startedAt,
293
+ completed_at: raw.completedAt,
294
+ snapshots: {
295
+ ...(before === undefined ? {} : { before }),
296
+ ...(afterNew === undefined ? {} : { after_new: afterNew }),
297
+ ...(afterResume === undefined ? {} : { after_resume: afterResume })
298
+ },
299
+ ...(resumeCandidate === undefined ? {} : {
300
+ resume_candidate: resumeCandidate
301
+ }),
302
+ ...(send === undefined ? {} : { send }),
303
+ transitions: {
304
+ ...(newTransition === undefined ? {} : { new_thread: newTransition }),
305
+ ...(resumeTransition === undefined ? {} : { resume_thread: resumeTransition })
306
+ },
307
+ turn_deltas: {
308
+ new_thread: raw.turnDeltas?.newThread ?? null,
309
+ send: raw.turnDeltas?.send ?? null,
310
+ resume_thread: raw.turnDeltas?.resumeThread ?? null
311
+ },
312
+ assertions,
313
+ steps
314
+ };
315
+ }
316
+ function createSnapshotEvidence(raw, salt) {
317
+ assertSafeInteger(raw.panePid, "invalid_pane_pid", "panePid", 2);
318
+ assertSafeInteger(raw.agentPid, "invalid_agent_pid", "agentPid", 2);
319
+ if (typeof raw.sessionMaterialized !== "boolean") {
320
+ fail("invalid_boolean", "sessionMaterialized must be boolean.");
321
+ }
322
+ if (raw.sessionMaterialized) {
323
+ requireRawIdentifier(raw.sessionId, "session");
324
+ requireRawIdentifier(raw.bindingId, "binding");
325
+ assertSafeInteger(raw.bindingGeneration, "invalid_binding_generation", "bindingGeneration", 1);
326
+ }
327
+ else if (raw.sessionId !== null ||
328
+ raw.bindingId !== null ||
329
+ raw.bindingGeneration !== null) {
330
+ fail("session_materialization_invalid", "An unmanaged snapshot cannot claim a Session, binding, or generation.");
331
+ }
332
+ return {
333
+ tmux_target: requireBoundedString(raw.tmuxTarget, "tmuxTarget", 512),
334
+ pane_pid: raw.panePid,
335
+ agent_pid: raw.agentPid,
336
+ process_uuid_fingerprint: fingerprintValue(salt, "process_uuid", requireRawIdentifier(raw.processUuid, "process_uuid")),
337
+ ...(raw.processBirth === undefined ? {} : {
338
+ process_birth_fingerprint: fingerprintValue(salt, "process_birth", requireRawIdentifier(raw.processBirth, "process_birth"))
339
+ }),
340
+ workspace_fingerprint: fingerprintValue(salt, "workspace", requireRawIdentifier(raw.workspace, "workspace")),
341
+ native_thread_fingerprint: fingerprintValue(salt, "native_thread", requireRawIdentifier(raw.nativeThreadId, "native_thread")),
342
+ session_materialized: raw.sessionMaterialized,
343
+ session_fingerprint: raw.sessionId === null
344
+ ? null
345
+ : fingerprintValue(salt, "session", requireRawIdentifier(raw.sessionId, "session")),
346
+ binding_fingerprint: raw.bindingId === null
347
+ ? null
348
+ : fingerprintValue(salt, "binding", requireRawIdentifier(raw.bindingId, "binding")),
349
+ binding_generation: raw.bindingGeneration,
350
+ idle: raw.idle === true
351
+ };
352
+ }
353
+ function createTransitionEvidence(raw, salt) {
354
+ return {
355
+ status: raw.status,
356
+ transition_fingerprint: fingerprintValue(salt, "transition", requireRawIdentifier(raw.transitionId, "transition")),
357
+ source_session_fingerprint: raw.sourceSessionId === null
358
+ ? null
359
+ : fingerprintValue(salt, "session", requireRawIdentifier(raw.sourceSessionId, "session")),
360
+ target_session_fingerprint: fingerprintValue(salt, "session", requireRawIdentifier(raw.targetSessionId, "session")),
361
+ source_binding_fingerprint: raw.sourceBindingId === null
362
+ ? null
363
+ : fingerprintValue(salt, "binding", requireRawIdentifier(raw.sourceBindingId, "binding")),
364
+ target_binding_fingerprint: fingerprintValue(salt, "binding", requireRawIdentifier(raw.targetBindingId, "binding"))
365
+ };
366
+ }
367
+ function deriveRelationshipAssertions({ before, afterNew, afterResume, resumeCandidate, send, newTransition, resumeTransition, tmuxTarget, panePid }) {
368
+ const hasAllSnapshots = before !== undefined && afterNew !== undefined && afterResume !== undefined;
369
+ const managedStart = before?.session_materialized === true;
370
+ const resumeCandidateSessionMatchesStart = before !== undefined && resumeCandidate !== undefined &&
371
+ (managedStart
372
+ ? resumeCandidate.managed_session_fingerprint === before.session_fingerprint
373
+ : before.session_fingerprint === null &&
374
+ before.binding_fingerprint === null &&
375
+ before.binding_generation === null &&
376
+ resumeCandidate.managed_session_fingerprint === null);
377
+ const resumeSessionRelationshipValid = hasAllSnapshots &&
378
+ afterNew.session_materialized &&
379
+ afterResume.session_materialized &&
380
+ (managedStart
381
+ ? afterResume.session_fingerprint === before.session_fingerprint
382
+ : before.session_fingerprint === null &&
383
+ before.binding_fingerprint === null &&
384
+ before.binding_generation === null &&
385
+ afterResume.session_fingerprint !== afterNew.session_fingerprint);
386
+ const bindingGenerationsExact = hasAllSnapshots &&
387
+ afterNew.session_materialized &&
388
+ afterNew.binding_generation === 1 &&
389
+ afterResume.session_materialized &&
390
+ (managedStart
391
+ ? before.binding_generation !== null &&
392
+ afterResume.binding_generation === before.binding_generation + 1
393
+ : afterResume.binding_generation === 1);
394
+ const materializedBindingFingerprints = [before, afterNew, afterResume]
395
+ .filter((snapshot) => snapshot?.session_materialized === true)
396
+ .map((snapshot) => snapshot.binding_fingerprint);
397
+ const bindingFingerprintsDistinct = hasAllSnapshots &&
398
+ materializedBindingFingerprints.every((value) => value !== null) &&
399
+ new Set(materializedBindingFingerprints).size ===
400
+ materializedBindingFingerprints.length;
401
+ const sameTmuxPane = hasAllSnapshots &&
402
+ [before, afterNew, afterResume].every((snapshot) => snapshot.tmux_target === tmuxTarget && snapshot.pane_pid === panePid);
403
+ const sameProcess = hasAllSnapshots &&
404
+ [before, afterNew, afterResume].every((snapshot) => snapshot.agent_pid === before.agent_pid &&
405
+ snapshot.process_uuid_fingerprint === before.process_uuid_fingerprint &&
406
+ snapshot.process_birth_fingerprint === before.process_birth_fingerprint);
407
+ const sameWorkspace = hasAllSnapshots &&
408
+ [before, afterNew, afterResume].every((snapshot) => snapshot.workspace_fingerprint === before.workspace_fingerprint);
409
+ return {
410
+ start_idle: before?.idle === true,
411
+ new_idle: afterNew?.idle === true,
412
+ final_idle: afterResume?.idle === true,
413
+ new_native_thread_differs: before !== undefined && afterNew !== undefined &&
414
+ afterNew.native_thread_fingerprint !== before.native_thread_fingerprint,
415
+ final_native_thread_matches_start: before !== undefined && afterResume !== undefined &&
416
+ afterResume.native_thread_fingerprint === before.native_thread_fingerprint,
417
+ resume_candidate_matches_start: before !== undefined && resumeCandidate !== undefined &&
418
+ resumeCandidate.native_thread_fingerprint === before.native_thread_fingerprint,
419
+ new_session_differs: before !== undefined && afterNew !== undefined &&
420
+ afterNew.session_materialized &&
421
+ afterNew.session_fingerprint !== before.session_fingerprint,
422
+ resume_session_relationship_valid: resumeSessionRelationshipValid,
423
+ binding_generation_relationship_valid: bindingGenerationsExact,
424
+ binding_fingerprints_distinct: bindingFingerprintsDistinct,
425
+ send_bound_to_new_session: send !== undefined && afterNew !== undefined &&
426
+ send.status === "completed" &&
427
+ send.session_fingerprint === afterNew.session_fingerprint &&
428
+ send.binding_fingerprint === afterNew.binding_fingerprint &&
429
+ send.binding_generation === afterNew.binding_generation,
430
+ transitions_match_sessions: hasAllSnapshots &&
431
+ newTransition !== undefined &&
432
+ resumeTransition !== undefined &&
433
+ newTransition.source_session_fingerprint === before.session_fingerprint &&
434
+ newTransition.target_session_fingerprint === afterNew.session_fingerprint &&
435
+ resumeTransition.source_session_fingerprint === afterNew.session_fingerprint &&
436
+ resumeTransition.target_session_fingerprint === afterResume.session_fingerprint,
437
+ transitions_match_bindings: hasAllSnapshots &&
438
+ newTransition !== undefined &&
439
+ resumeTransition !== undefined &&
440
+ newTransition.source_binding_fingerprint === before.binding_fingerprint &&
441
+ newTransition.target_binding_fingerprint === afterNew.binding_fingerprint &&
442
+ resumeTransition.source_binding_fingerprint === afterNew.binding_fingerprint &&
443
+ resumeTransition.target_binding_fingerprint === afterResume.binding_fingerprint,
444
+ transitions_distinct: newTransition !== undefined &&
445
+ resumeTransition !== undefined &&
446
+ newTransition.transition_fingerprint !== resumeTransition.transition_fingerprint,
447
+ resume_candidate_safe: resumeCandidate !== undefined &&
448
+ resumeCandidate.exact_candidate_count === 1 &&
449
+ resumeCandidate.resumable === true &&
450
+ resumeCandidate.active_elsewhere === false &&
451
+ resumeCandidate.fresh_candidate_token_present === true &&
452
+ resumeCandidateSessionMatchesStart,
453
+ same_tmux_pane: sameTmuxPane,
454
+ same_process_incarnation: sameProcess,
455
+ same_workspace: sameWorkspace
456
+ };
457
+ }
458
+ function fingerprintValue(salt, domain, value) {
459
+ const digest = createHash("sha256")
460
+ .update(FINGERPRINT_DOMAIN, "utf8")
461
+ .update("\0", "utf8")
462
+ .update(domain, "utf8")
463
+ .update("\0", "utf8")
464
+ .update(salt, "utf8")
465
+ .update("\0", "utf8")
466
+ .update(value, "utf8")
467
+ .digest("hex");
468
+ return `sha256:${digest}`;
469
+ }
470
+ function digestUnsignedEvidence(unsigned) {
471
+ const digest = createHash("sha256")
472
+ .update(DIGEST_DOMAIN, "utf8")
473
+ .update("\0", "utf8")
474
+ .update(canonicalJson(unsigned), "utf8")
475
+ .digest("hex");
476
+ return `sha256:${digest}`;
477
+ }
478
+ function assertEvidence(value, requireAgents, policy = {}) {
479
+ scanForSensitiveMaterial(value, "$", new Set());
480
+ const root = requireRecord(value, "$", [
481
+ "schema_version",
482
+ "kind",
483
+ "run_id",
484
+ "package",
485
+ "source",
486
+ "started_at",
487
+ "completed_at",
488
+ "overall_status",
489
+ "matrix",
490
+ "digest"
491
+ ]);
492
+ assertLiteral(root.schema_version, LIVE_LIFECYCLE_EVIDENCE_SCHEMA_VERSION, "unsupported_schema", "schema_version");
493
+ assertLiteral(root.kind, LIVE_LIFECYCLE_EVIDENCE_KIND, "invalid_kind", "kind");
494
+ assertPattern(root.run_id, /^[0-9a-f]{32}$/u, "invalid_run_id", "run_id");
495
+ const packageRecord = requireRecord(root.package, "package", ["name", "version"]);
496
+ requireBoundedString(packageRecord.name, "package.name", 256);
497
+ requireBoundedString(packageRecord.version, "package.version", 128);
498
+ const source = requireRecord(root.source, "source", [
499
+ "commit",
500
+ "worktree_clean"
501
+ ]);
502
+ assertPattern(source.commit, /^[0-9a-f]{40}$/u, "invalid_commit", "source.commit");
503
+ if (typeof source.worktree_clean !== "boolean") {
504
+ fail("invalid_boolean", "source.worktree_clean must be boolean.");
505
+ }
506
+ if (source.worktree_clean !== true && policy.allowDirtyWorktree !== true) {
507
+ fail("dirty_worktree", "Evidence source.worktree_clean must be true.");
508
+ }
509
+ const evidenceStartedAt = parseTimestamp(root.started_at, "started_at");
510
+ const evidenceCompletedAt = parseTimestamp(root.completed_at, "completed_at");
511
+ if (evidenceCompletedAt < evidenceStartedAt) {
512
+ fail("invalid_time_order", "Evidence completed_at precedes started_at.");
513
+ }
514
+ if (evidenceCompletedAt - evidenceStartedAt > MAX_RUN_DURATION_MS) {
515
+ fail("run_duration_exceeded", "Evidence run duration exceeds six hours.");
516
+ }
517
+ assertStatus(root.overall_status, "overall_status");
518
+ assertPattern(root.digest, /^sha256:[0-9a-f]{64}$/u, "invalid_digest", "digest");
519
+ const matrix = requireRecord(root.matrix, "matrix", AGENTS, [], true);
520
+ if (Object.keys(matrix).length === 0) {
521
+ fail("matrix_empty", "Evidence matrix must contain at least one agent.");
522
+ }
523
+ const typedMatrix = {};
524
+ for (const agent of AGENTS) {
525
+ if (matrix[agent] !== undefined) {
526
+ typedMatrix[agent] = assertScenarioEvidence(matrix[agent], agent);
527
+ const scenarioStartedAt = parseTimestamp(typedMatrix[agent].started_at, `matrix.${agent}.started_at`);
528
+ const scenarioCompletedAt = parseTimestamp(typedMatrix[agent].completed_at, `matrix.${agent}.completed_at`);
529
+ if (scenarioCompletedAt < scenarioStartedAt ||
530
+ scenarioStartedAt < evidenceStartedAt ||
531
+ scenarioCompletedAt > evidenceCompletedAt) {
532
+ fail("invalid_scenario_time_order", `${agent} scenario timestamps fall outside the evidence interval.`);
533
+ }
534
+ if (scenarioCompletedAt - scenarioStartedAt > MAX_RUN_DURATION_MS) {
535
+ fail("run_duration_exceeded", `${agent} scenario duration exceeds six hours.`);
536
+ }
537
+ }
538
+ }
539
+ const evidence = root;
540
+ const statuses = Object.values(typedMatrix).map((scenario) => scenario.status);
541
+ const derivedStatus = deriveResultStatus(statuses);
542
+ const expectedOverall = Object.keys(typedMatrix).length === AGENTS.length
543
+ ? derivedStatus
544
+ : derivedStatus === "uncertain" ? "uncertain" : "failed";
545
+ if (expectedOverall !== evidence.overall_status) {
546
+ fail("overall_status_mismatch", "Overall status does not match the matrix.");
547
+ }
548
+ const { digest, ...unsigned } = evidence;
549
+ const expectedDigest = digestUnsignedEvidence(unsigned);
550
+ if (digest !== expectedDigest) {
551
+ fail("digest_mismatch", "Evidence canonical digest does not match its content.");
552
+ }
553
+ if (evidence.overall_status !== "passed" &&
554
+ policy.allowNonPassing !== true) {
555
+ fail("status_not_passed", "Overall live-smoke status must be passed.");
556
+ }
557
+ for (const agent of requireAgents) {
558
+ if (!AGENTS.includes(agent)) {
559
+ fail("invalid_required_agent", `Unsupported required agent: ${agent}`);
560
+ }
561
+ if (typedMatrix[agent] === undefined) {
562
+ fail("matrix_incomplete", `Evidence matrix is missing ${agent}.`);
563
+ }
564
+ }
565
+ return evidence;
566
+ }
567
+ function assertScenarioEvidence(value, agent) {
568
+ const path = `matrix.${agent}`;
569
+ const scenario = requireRecord(value, path, [
570
+ "status",
571
+ "failure_stage",
572
+ "reason_code",
573
+ "do_not_retry",
574
+ "agent_version",
575
+ "tmux_target",
576
+ "pane_pid",
577
+ "started_at",
578
+ "completed_at",
579
+ "snapshots",
580
+ "resume_candidate",
581
+ "send",
582
+ "transitions",
583
+ "turn_deltas",
584
+ "assertions",
585
+ "steps"
586
+ ], [
587
+ "status",
588
+ "tmux_target",
589
+ "pane_pid",
590
+ "started_at",
591
+ "completed_at",
592
+ "snapshots",
593
+ "transitions",
594
+ "turn_deltas",
595
+ "assertions",
596
+ "steps"
597
+ ]);
598
+ assertStatus(scenario.status, `${path}.status`);
599
+ requireBoundedString(scenario.tmux_target, `${path}.tmux_target`, 512);
600
+ assertSafeInteger(scenario.pane_pid, "invalid_pane_pid", `${path}.pane_pid`, 2);
601
+ if (scenario.agent_version !== undefined) {
602
+ requireBoundedString(scenario.agent_version, `${path}.agent_version`, 128);
603
+ }
604
+ parseTimestamp(scenario.started_at, `${path}.started_at`);
605
+ parseTimestamp(scenario.completed_at, `${path}.completed_at`);
606
+ if (scenario.status === "passed") {
607
+ if (scenario.failure_stage !== undefined ||
608
+ scenario.reason_code !== undefined ||
609
+ scenario.do_not_retry !== undefined) {
610
+ fail("unexpected_failure_detail", `${path} cannot include failure details when passed.`);
611
+ }
612
+ if (scenario.agent_version === undefined) {
613
+ fail("missing_key", `${path}.agent_version is required when passed.`);
614
+ }
615
+ }
616
+ else {
617
+ if (!STEP_NAMES.includes(scenario.failure_stage)) {
618
+ fail("missing_failure_stage", `${path}.failure_stage is required.`);
619
+ }
620
+ requireReasonCode(scenario.reason_code, `${path}.reason_code`);
621
+ if (scenario.do_not_retry !== undefined && scenario.do_not_retry !== true) {
622
+ fail("invalid_boolean", `${path}.do_not_retry must be true when present.`);
623
+ }
624
+ if (scenario.status === "uncertain" && scenario.do_not_retry !== true) {
625
+ fail("retry_guard_missing", `${path} must record do_not_retry=true when uncertain.`);
626
+ }
627
+ }
628
+ const snapshots = requireRecord(scenario.snapshots, `${path}.snapshots`, [
629
+ "before",
630
+ "after_new",
631
+ "after_resume"
632
+ ], [], true);
633
+ const before = snapshots.before === undefined ? undefined :
634
+ assertSnapshot(snapshots.before, `${path}.snapshots.before`);
635
+ const afterNew = snapshots.after_new === undefined ? undefined :
636
+ assertSnapshot(snapshots.after_new, `${path}.snapshots.after_new`);
637
+ const afterResume = snapshots.after_resume === undefined ? undefined :
638
+ assertSnapshot(snapshots.after_resume, `${path}.snapshots.after_resume`);
639
+ if (afterNew !== undefined && !afterNew.session_materialized) {
640
+ fail("session_materialization_invalid", `${path}.snapshots.after_new must contain a materialized Session.`);
641
+ }
642
+ if (afterResume !== undefined && !afterResume.session_materialized) {
643
+ fail("session_materialization_invalid", `${path}.snapshots.after_resume must contain a materialized Session.`);
644
+ }
645
+ const resumeCandidate = scenario.resume_candidate === undefined ? undefined :
646
+ assertResumeCandidate(scenario.resume_candidate, `${path}.resume_candidate`);
647
+ const send = scenario.send === undefined ? undefined :
648
+ assertSend(scenario.send, `${path}.send`);
649
+ const transitions = requireRecord(scenario.transitions, `${path}.transitions`, [
650
+ "new_thread",
651
+ "resume_thread"
652
+ ], [], true);
653
+ const newTransition = transitions.new_thread === undefined ? undefined :
654
+ assertTransition(transitions.new_thread, `${path}.transitions.new_thread`);
655
+ const resumeTransition = transitions.resume_thread === undefined ? undefined :
656
+ assertTransition(transitions.resume_thread, `${path}.transitions.resume_thread`);
657
+ const turnDeltas = requireRecord(scenario.turn_deltas, `${path}.turn_deltas`, [
658
+ "new_thread",
659
+ "send",
660
+ "resume_thread"
661
+ ]);
662
+ for (const key of ["new_thread", "send", "resume_thread"]) {
663
+ if (turnDeltas[key] !== null) {
664
+ assertSafeInteger(turnDeltas[key], "turn_delta_invalid", `${path}.turn_deltas.${key}`, 0);
665
+ }
666
+ }
667
+ const assertions = assertAssertions(scenario.assertions, `${path}.assertions`);
668
+ const steps = assertSteps(scenario.steps, `${path}.steps`, scenario.status, scenario.failure_stage);
669
+ const typed = scenario;
670
+ if (scenario.status !== "passed") {
671
+ return typed;
672
+ }
673
+ if (before === undefined ||
674
+ afterNew === undefined ||
675
+ afterResume === undefined ||
676
+ resumeCandidate === undefined ||
677
+ send === undefined ||
678
+ newTransition === undefined ||
679
+ resumeTransition === undefined) {
680
+ fail("missing_pass_evidence", `${path} lacks complete passing lifecycle evidence.`);
681
+ }
682
+ assertExactInteger(turnDeltas.new_thread, 0, `${path}.turn_deltas.new_thread`);
683
+ assertExactInteger(turnDeltas.send, 1, `${path}.turn_deltas.send`);
684
+ assertExactInteger(turnDeltas.resume_thread, 0, `${path}.turn_deltas.resume_thread`);
685
+ const recomputed = deriveRelationshipAssertions({
686
+ before,
687
+ afterNew,
688
+ afterResume,
689
+ resumeCandidate,
690
+ send,
691
+ newTransition,
692
+ resumeTransition,
693
+ tmuxTarget: scenario.tmux_target,
694
+ panePid: scenario.pane_pid
695
+ });
696
+ for (const key of Object.keys(recomputed)) {
697
+ if (assertions[key] !== recomputed[key] || recomputed[key] !== true) {
698
+ fail("relationship_invalid", `${path}.assertions.${key} is not proven by the evidence.`);
699
+ }
700
+ }
701
+ if (send.status !== "completed") {
702
+ fail("turn_not_completed", `${agent} sentinel Turn did not complete.`);
703
+ }
704
+ if (newTransition.status !== "committed" ||
705
+ resumeTransition.status !== "committed") {
706
+ fail("transition_not_committed", `${agent} lifecycle transition was not committed.`);
707
+ }
708
+ return {
709
+ status: scenario.status,
710
+ agent_version: scenario.agent_version,
711
+ tmux_target: scenario.tmux_target,
712
+ pane_pid: scenario.pane_pid,
713
+ started_at: scenario.started_at,
714
+ completed_at: scenario.completed_at,
715
+ snapshots: { before, after_new: afterNew, after_resume: afterResume },
716
+ resume_candidate: resumeCandidate,
717
+ send,
718
+ transitions: { new_thread: newTransition, resume_thread: resumeTransition },
719
+ turn_deltas: { new_thread: 0, send: 1, resume_thread: 0 },
720
+ assertions,
721
+ steps
722
+ };
723
+ }
724
+ function assertSnapshot(value, path) {
725
+ const snapshot = requireRecord(value, path, [
726
+ "tmux_target",
727
+ "pane_pid",
728
+ "agent_pid",
729
+ "process_uuid_fingerprint",
730
+ "process_birth_fingerprint",
731
+ "workspace_fingerprint",
732
+ "native_thread_fingerprint",
733
+ "session_materialized",
734
+ "session_fingerprint",
735
+ "binding_fingerprint",
736
+ "binding_generation",
737
+ "idle"
738
+ ], [
739
+ "tmux_target",
740
+ "pane_pid",
741
+ "agent_pid",
742
+ "process_uuid_fingerprint",
743
+ "workspace_fingerprint",
744
+ "native_thread_fingerprint",
745
+ "session_materialized",
746
+ "session_fingerprint",
747
+ "binding_fingerprint",
748
+ "binding_generation",
749
+ "idle"
750
+ ]);
751
+ requireBoundedString(snapshot.tmux_target, `${path}.tmux_target`, 512);
752
+ assertSafeInteger(snapshot.pane_pid, "invalid_pane_pid", `${path}.pane_pid`, 2);
753
+ assertSafeInteger(snapshot.agent_pid, "invalid_agent_pid", `${path}.agent_pid`, 2);
754
+ assertFingerprint(snapshot.process_uuid_fingerprint, `${path}.process_uuid_fingerprint`);
755
+ if (snapshot.process_birth_fingerprint !== undefined) {
756
+ assertFingerprint(snapshot.process_birth_fingerprint, `${path}.process_birth_fingerprint`);
757
+ }
758
+ assertFingerprint(snapshot.workspace_fingerprint, `${path}.workspace_fingerprint`);
759
+ assertFingerprint(snapshot.native_thread_fingerprint, `${path}.native_thread_fingerprint`);
760
+ if (typeof snapshot.session_materialized !== "boolean") {
761
+ fail("invalid_boolean", `${path}.session_materialized must be boolean.`);
762
+ }
763
+ if (snapshot.session_materialized) {
764
+ assertFingerprint(snapshot.session_fingerprint, `${path}.session_fingerprint`);
765
+ assertFingerprint(snapshot.binding_fingerprint, `${path}.binding_fingerprint`);
766
+ assertSafeInteger(snapshot.binding_generation, "invalid_binding_generation", `${path}.binding_generation`, 1);
767
+ }
768
+ else if (snapshot.session_fingerprint !== null ||
769
+ snapshot.binding_fingerprint !== null ||
770
+ snapshot.binding_generation !== null) {
771
+ fail("session_materialization_invalid", `${path} cannot claim Session data when session_materialized=false.`);
772
+ }
773
+ if (typeof snapshot.idle !== "boolean") {
774
+ fail("invalid_boolean", `${path}.idle must be boolean.`);
775
+ }
776
+ return snapshot;
777
+ }
778
+ function assertResumeCandidate(value, path) {
779
+ const candidate = requireRecord(value, path, [
780
+ "native_thread_fingerprint",
781
+ "managed_session_fingerprint",
782
+ "exact_candidate_count",
783
+ "resumable",
784
+ "active_elsewhere",
785
+ "fresh_candidate_token_present"
786
+ ]);
787
+ assertFingerprint(candidate.native_thread_fingerprint, `${path}.native_thread_fingerprint`);
788
+ assertFingerprintOrNull(candidate.managed_session_fingerprint, `${path}.managed_session_fingerprint`);
789
+ assertSafeInteger(candidate.exact_candidate_count, "invalid_candidate_count", `${path}.exact_candidate_count`, 0);
790
+ for (const key of [
791
+ "resumable",
792
+ "active_elsewhere",
793
+ "fresh_candidate_token_present"
794
+ ]) {
795
+ if (typeof candidate[key] !== "boolean") {
796
+ fail("invalid_boolean", `${path}.${key} must be boolean.`);
797
+ }
798
+ }
799
+ return candidate;
800
+ }
801
+ function assertTransition(value, path) {
802
+ const transition = requireRecord(value, path, [
803
+ "status",
804
+ "transition_fingerprint",
805
+ "source_session_fingerprint",
806
+ "target_session_fingerprint",
807
+ "source_binding_fingerprint",
808
+ "target_binding_fingerprint"
809
+ ]);
810
+ if (!["committed", "failed", "uncertain"].includes(transition.status)) {
811
+ fail("invalid_transition_status", `${path}.status is invalid.`);
812
+ }
813
+ assertFingerprint(transition.transition_fingerprint, `${path}.transition_fingerprint`);
814
+ assertFingerprintOrNull(transition.source_session_fingerprint, `${path}.source_session_fingerprint`);
815
+ assertFingerprint(transition.target_session_fingerprint, `${path}.target_session_fingerprint`);
816
+ assertFingerprintOrNull(transition.source_binding_fingerprint, `${path}.source_binding_fingerprint`);
817
+ assertFingerprint(transition.target_binding_fingerprint, `${path}.target_binding_fingerprint`);
818
+ return transition;
819
+ }
820
+ function assertSend(value, path) {
821
+ const send = requireRecord(value, path, [
822
+ "status",
823
+ "turn_fingerprint",
824
+ "session_fingerprint",
825
+ "binding_fingerprint",
826
+ "binding_generation"
827
+ ]);
828
+ if (!["completed", "failed", "uncertain"].includes(send.status)) {
829
+ fail("invalid_turn_status", `${path}.status is invalid.`);
830
+ }
831
+ assertFingerprint(send.turn_fingerprint, `${path}.turn_fingerprint`);
832
+ assertFingerprint(send.session_fingerprint, `${path}.session_fingerprint`);
833
+ assertFingerprint(send.binding_fingerprint, `${path}.binding_fingerprint`);
834
+ assertSafeInteger(send.binding_generation, "invalid_binding_generation", `${path}.binding_generation`, 0);
835
+ return send;
836
+ }
837
+ function assertAssertions(value, path) {
838
+ const keys = [
839
+ "start_idle",
840
+ "new_idle",
841
+ "final_idle",
842
+ "new_native_thread_differs",
843
+ "final_native_thread_matches_start",
844
+ "resume_candidate_matches_start",
845
+ "new_session_differs",
846
+ "resume_session_relationship_valid",
847
+ "binding_generation_relationship_valid",
848
+ "binding_fingerprints_distinct",
849
+ "send_bound_to_new_session",
850
+ "transitions_match_sessions",
851
+ "transitions_match_bindings",
852
+ "transitions_distinct",
853
+ "resume_candidate_safe",
854
+ "same_tmux_pane",
855
+ "same_process_incarnation",
856
+ "same_workspace"
857
+ ];
858
+ const assertions = requireRecord(value, path, keys);
859
+ for (const key of keys) {
860
+ if (typeof assertions[key] !== "boolean") {
861
+ fail("invalid_boolean", `${path}.${key} must be boolean.`);
862
+ }
863
+ }
864
+ return assertions;
865
+ }
866
+ function assertSteps(value, path, scenarioStatus, failureStage) {
867
+ if (!Array.isArray(value) ||
868
+ value.length === 0 ||
869
+ value.length > STEP_NAMES.length ||
870
+ (scenarioStatus === "passed" && value.length !== STEP_NAMES.length)) {
871
+ fail("steps_invalid", `${path} must contain a non-empty prefix of the fixed ${STEP_NAMES.length}-step sequence.`);
872
+ }
873
+ const steps = value.map((entry, index) => {
874
+ const stepPath = `${path}[${index}]`;
875
+ const step = requireRecord(entry, stepPath, ["name", "status", "duration_ms"]);
876
+ assertLiteral(step.name, STEP_NAMES[index], "steps_invalid", `${stepPath}.name`);
877
+ assertStatus(step.status, `${stepPath}.status`);
878
+ assertSafeInteger(step.duration_ms, "invalid_step_duration", `${stepPath}.duration_ms`, 0);
879
+ return step;
880
+ });
881
+ if (scenarioStatus === "passed") {
882
+ if (steps.some((step) => step.status !== "passed")) {
883
+ fail("status_not_passed", `${path} contains a non-passing step.`);
884
+ }
885
+ }
886
+ else {
887
+ const last = steps.at(-1);
888
+ if (last.status !== scenarioStatus ||
889
+ last.name !== failureStage ||
890
+ steps.slice(0, -1).some((step) => step.status !== "passed")) {
891
+ fail("failure_step_mismatch", `${path} must end at failure_stage with the scenario status.`);
892
+ }
893
+ }
894
+ return steps;
895
+ }
896
+ function assertRawEnvelope(input) {
897
+ if (!isPlainRecord(input)) {
898
+ fail("invalid_input", "Raw evidence input must be a plain object.");
899
+ }
900
+ requireBoundedString(input.packageName, "packageName", 256);
901
+ requireBoundedString(input.packageVersion, "packageVersion", 128);
902
+ assertPattern(input.commit, /^[0-9a-f]{40}$/u, "invalid_commit", "commit");
903
+ if (typeof input.worktreeClean !== "boolean") {
904
+ fail("invalid_boolean", "worktreeClean must be boolean.");
905
+ }
906
+ parseTimestamp(input.startedAt, "startedAt");
907
+ parseTimestamp(input.completedAt, "completedAt");
908
+ if (!isPlainRecord(input.scenarios)) {
909
+ fail("invalid_input", "scenarios must be a plain object.");
910
+ }
911
+ for (const key of Object.keys(input.scenarios)) {
912
+ if (!AGENTS.includes(key)) {
913
+ fail("unknown_agent", `Unsupported live-smoke agent: ${key}`);
914
+ }
915
+ }
916
+ }
917
+ function assertRawScenario(raw) {
918
+ if (!isPlainRecord(raw)) {
919
+ fail("invalid_input", "Raw scenario must be a plain object.");
920
+ }
921
+ assertStatus(raw.status, "scenario.status", false);
922
+ if (raw.agentVersion !== undefined) {
923
+ requireBoundedString(raw.agentVersion, "agentVersion", 128);
924
+ }
925
+ if (raw.doNotRetry !== undefined && raw.doNotRetry !== true) {
926
+ fail("invalid_boolean", "doNotRetry must be true when present.");
927
+ }
928
+ requireBoundedString(raw.tmuxTarget, "tmuxTarget", 512);
929
+ assertSafeInteger(raw.panePid, "invalid_pane_pid", "panePid", 2);
930
+ parseTimestamp(raw.startedAt, "scenario.startedAt");
931
+ parseTimestamp(raw.completedAt, "scenario.completedAt");
932
+ if (!Array.isArray(raw.steps)) {
933
+ fail("steps_invalid", "Raw scenario steps must be an array.");
934
+ }
935
+ for (const step of raw.steps) {
936
+ if (!isPlainRecord(step)) {
937
+ fail("steps_invalid", "Raw scenario step must be an object.");
938
+ }
939
+ assertStatus(step.status, "scenario.step.status", false);
940
+ assertSafeInteger(step.durationMs, "invalid_step_duration", "durationMs", 0);
941
+ }
942
+ for (const generation of [
943
+ raw.before?.bindingGeneration,
944
+ raw.afterNew?.bindingGeneration,
945
+ raw.afterResume?.bindingGeneration,
946
+ raw.send?.bindingGeneration
947
+ ]) {
948
+ if (generation === undefined || generation === null) {
949
+ continue;
950
+ }
951
+ assertSafeInteger(generation, "invalid_binding_generation", "bindingGeneration", 0);
952
+ }
953
+ if (raw.resumeCandidate !== undefined) {
954
+ requireRawIdentifier(raw.resumeCandidate.nativeThreadId, "resume_candidate_native_thread");
955
+ if (raw.resumeCandidate.managedSessionId !== null) {
956
+ requireRawIdentifier(raw.resumeCandidate.managedSessionId, "resume_candidate_managed_session");
957
+ }
958
+ assertSafeInteger(raw.resumeCandidate.exactCandidateCount, "invalid_candidate_count", "exactCandidateCount", 0);
959
+ for (const value of [
960
+ raw.resumeCandidate.resumable,
961
+ raw.resumeCandidate.activeElsewhere,
962
+ raw.resumeCandidate.freshCandidateTokenPresent
963
+ ]) {
964
+ if (typeof value !== "boolean") {
965
+ fail("invalid_boolean", "Resume candidate flags must be boolean.");
966
+ }
967
+ }
968
+ }
969
+ }
970
+ function requireReasonCode(value, path) {
971
+ if (typeof value !== "string" ||
972
+ !/^[a-z][a-z0-9_]{0,63}$/u.test(value)) {
973
+ fail("invalid_reason_code", `${path} must be a safe lowercase reason code, not a raw error message.`);
974
+ }
975
+ return value;
976
+ }
977
+ function deriveResultStatus(statuses) {
978
+ if (statuses.includes("uncertain")) {
979
+ return "uncertain";
980
+ }
981
+ if (statuses.includes("failed")) {
982
+ return "failed";
983
+ }
984
+ return "passed";
985
+ }
986
+ function scanForSensitiveMaterial(value, path, seen) {
987
+ if (typeof value === "string") {
988
+ if (/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/iu.test(value)) {
989
+ fail("raw_uuid", `Raw UUID-like value is forbidden at ${path}.`);
990
+ }
991
+ if (/(?:^|\b)(?:sk-[A-Za-z0-9_-]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|npm_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})/u.test(value) ||
992
+ /(?:bearer\s+[A-Za-z0-9._~+\/-]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY-----)/iu.test(value)) {
993
+ fail("secret_material", `Secret-like value is forbidden at ${path}.`);
994
+ }
995
+ if (/^(?:\/(?:Users|home|private|var|tmp)\/|[A-Za-z]:\\)/u.test(value)) {
996
+ fail("raw_path", `Raw local path is forbidden at ${path}.`);
997
+ }
998
+ return;
999
+ }
1000
+ if (value === null || typeof value !== "object") {
1001
+ return;
1002
+ }
1003
+ if (seen.has(value)) {
1004
+ fail("cyclic_value", `Cyclic value is forbidden at ${path}.`);
1005
+ }
1006
+ seen.add(value);
1007
+ if (Array.isArray(value)) {
1008
+ value.forEach((entry, index) => scanForSensitiveMaterial(entry, `${path}[${index}]`, seen));
1009
+ }
1010
+ else {
1011
+ for (const [key, entry] of Object.entries(value)) {
1012
+ if (/^(?:prompt|nonce|stdout|stderr|screen|callback|callback_payload|token|tokens|api_key|secret|password|raw|raw_.+|workspace|native_thread_id|session_id|turn_id|transition_id|binding_id|process_id)$/iu.test(key)) {
1013
+ fail("sensitive_key", `Sensitive evidence key is forbidden at ${path}.${key}.`);
1014
+ }
1015
+ scanForSensitiveMaterial(entry, `${path}.${key}`, seen);
1016
+ }
1017
+ }
1018
+ seen.delete(value);
1019
+ }
1020
+ function requireRecord(value, path, allowedKeys, requiredKeys = allowedKeys, allowMissingAllowed = false) {
1021
+ if (!isPlainRecord(value)) {
1022
+ fail("invalid_shape", `${path} must be a plain object.`);
1023
+ }
1024
+ const record = value;
1025
+ for (const key of Object.keys(record)) {
1026
+ if (!allowedKeys.includes(key)) {
1027
+ fail("unknown_key", `Unknown evidence key: ${path}.${key}.`);
1028
+ }
1029
+ }
1030
+ if (!allowMissingAllowed) {
1031
+ for (const key of requiredKeys) {
1032
+ if (!Object.prototype.hasOwnProperty.call(record, key)) {
1033
+ fail("missing_key", `Missing evidence key: ${path}.${key}.`);
1034
+ }
1035
+ }
1036
+ }
1037
+ return record;
1038
+ }
1039
+ function isPlainRecord(value) {
1040
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1041
+ return false;
1042
+ }
1043
+ const prototype = Object.getPrototypeOf(value);
1044
+ return prototype === Object.prototype || prototype === null;
1045
+ }
1046
+ function assertStatus(value, path, requirePassed = false) {
1047
+ if (!(value === "passed" || value === "failed" || value === "uncertain")) {
1048
+ fail("invalid_status", `${path} has an invalid status.`);
1049
+ }
1050
+ if (requirePassed && value !== "passed") {
1051
+ fail("status_not_passed", `${path} must be passed.`);
1052
+ }
1053
+ }
1054
+ function assertFingerprint(value, path) {
1055
+ assertPattern(value, /^sha256:[0-9a-f]{64}$/u, "invalid_fingerprint", path);
1056
+ }
1057
+ function assertFingerprintOrNull(value, path) {
1058
+ if (value !== null) {
1059
+ assertFingerprint(value, path);
1060
+ }
1061
+ }
1062
+ function assertPattern(value, pattern, code, path) {
1063
+ if (typeof value !== "string" || !pattern.test(value)) {
1064
+ fail(code, `${path} has an invalid format.`);
1065
+ }
1066
+ }
1067
+ function assertLiteral(value, expected, code, path) {
1068
+ if (value !== expected) {
1069
+ fail(code, `${path} must equal ${String(expected)}.`);
1070
+ }
1071
+ }
1072
+ function assertExpectedValue(actual, expected, code, path) {
1073
+ if (actual !== expected) {
1074
+ fail(code, `${path} does not match the expected release value.`);
1075
+ }
1076
+ }
1077
+ function assertSafeInteger(value, code, path, minimum) {
1078
+ if (!Number.isSafeInteger(value) || value < minimum) {
1079
+ fail(code, `${path} must be a safe integer >= ${minimum}.`);
1080
+ }
1081
+ }
1082
+ function assertExactInteger(value, expected, path) {
1083
+ if (value !== expected) {
1084
+ fail("turn_delta_invalid", `${path} must equal ${expected}.`);
1085
+ }
1086
+ }
1087
+ function requireBoundedString(value, path, max) {
1088
+ if (typeof value !== "string" || value.length === 0 || value.length > max) {
1089
+ fail("invalid_string", `${path} must be a non-empty string <= ${max} characters.`);
1090
+ }
1091
+ return value;
1092
+ }
1093
+ function requireRawIdentifier(value, path) {
1094
+ return requireBoundedString(value, `raw.${path}`, 4_096);
1095
+ }
1096
+ function parseTimestamp(value, path) {
1097
+ if (typeof value !== "string" ||
1098
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/u.test(value)) {
1099
+ fail("invalid_timestamp", `${path} must be an RFC3339 timestamp.`);
1100
+ }
1101
+ const parsed = Date.parse(value);
1102
+ if (!Number.isFinite(parsed)) {
1103
+ fail("invalid_timestamp", `${path} must be an RFC3339 timestamp.`);
1104
+ }
1105
+ return parsed;
1106
+ }
1107
+ function parseNow(value) {
1108
+ if (value === undefined) {
1109
+ return Date.now();
1110
+ }
1111
+ const parsed = value instanceof Date ? value.getTime() :
1112
+ typeof value === "number" ? value : Date.parse(value);
1113
+ if (!Number.isFinite(parsed)) {
1114
+ fail("invalid_now", "Validation option now must be a valid date or epoch milliseconds.");
1115
+ }
1116
+ return parsed;
1117
+ }
1118
+ function canonicalJson(value) {
1119
+ if (value === null) {
1120
+ return "null";
1121
+ }
1122
+ if (typeof value === "string" || typeof value === "boolean") {
1123
+ return JSON.stringify(value);
1124
+ }
1125
+ if (typeof value === "number") {
1126
+ if (!Number.isFinite(value)) {
1127
+ fail("noncanonical_value", "Canonical evidence cannot contain a non-finite number.");
1128
+ }
1129
+ return JSON.stringify(value);
1130
+ }
1131
+ if (Array.isArray(value)) {
1132
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
1133
+ }
1134
+ if (isPlainRecord(value)) {
1135
+ const entries = Object.keys(value)
1136
+ .sort()
1137
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`);
1138
+ return `{${entries.join(",")}}`;
1139
+ }
1140
+ fail("noncanonical_value", "Canonical evidence contains an unsupported value.");
1141
+ }
1142
+ function countOccurrences(haystack, needle) {
1143
+ let count = 0;
1144
+ let offset = 0;
1145
+ while (true) {
1146
+ const index = haystack.indexOf(needle, offset);
1147
+ if (index < 0) {
1148
+ return count;
1149
+ }
1150
+ count += 1;
1151
+ offset = index + needle.length;
1152
+ }
1153
+ }
1154
+ function fail(code, message) {
1155
+ throw new LiveLifecycleEvidenceValidationError(code, message);
1156
+ }
1157
+ //# sourceMappingURL=live-lifecycle-evidence.js.map