@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.
- package/CHANGELOG.md +27 -0
- package/README.md +19 -2
- package/dist/src/claude-local-transcript-provider.d.ts +16 -0
- package/dist/src/claude-local-transcript-provider.js +111 -47
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/cli.js +3738 -812
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-lifecycle-compatibility.d.ts +2 -0
- package/dist/src/codex-lifecycle-compatibility.js +13 -0
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -0
- package/dist/src/codex-store-adapter.js +8 -7
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.js +9 -8
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/live-lifecycle-evidence.d.ts +232 -0
- package/dist/src/live-lifecycle-evidence.js +1157 -0
- package/dist/src/live-lifecycle-evidence.js.map +1 -0
- package/dist/src/live-lifecycle-smoke-evidence.d.ts +13 -0
- package/dist/src/live-lifecycle-smoke-evidence.js +167 -0
- package/dist/src/live-lifecycle-smoke-evidence.js.map +1 -0
- package/dist/src/live-lifecycle-smoke.d.ts +123 -0
- package/dist/src/live-lifecycle-smoke.js +937 -0
- package/dist/src/live-lifecycle-smoke.js.map +1 -0
- package/dist/src/openclaw-plugin-helpers.d.ts +1 -0
- package/dist/src/openclaw-plugin-helpers.js +30 -2
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.js +193 -31
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/protocol.d.ts +14 -0
- package/dist/src/protocol.js +87 -0
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/store.js +17 -2
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +31 -2
- package/dist/src/terminal-agent-bridge.js +231 -11
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.d.ts +78 -0
- package/dist/src/terminal-submission-acceptance.js +454 -0
- package/dist/src/terminal-submission-acceptance.js.map +1 -0
- 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
|