@skill-harness/core 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/adapters/types.d.ts +0 -12
- package/dist/capture-trace-types.d.ts +0 -96
- package/dist/capture-trace-types.js +0 -4
- package/dist/command-cost.d.ts +1 -1
- package/dist/command-cost.js +0 -5
- package/dist/execution-trace.js +1 -1
- package/dist/index.d.ts +3 -23
- package/dist/index.js +3 -23
- package/dist/instruction-coverage.d.ts +0 -7
- package/dist/instruction-coverage.js +5 -7
- package/dist/journal.d.ts +0 -8
- package/dist/metrics.d.ts +18 -0
- package/dist/metrics.js +28 -0
- package/dist/redaction.d.ts +6 -0
- package/dist/redaction.js +55 -0
- package/dist/regate.js +6 -71
- package/dist/regrade.d.ts +1 -3
- package/dist/regrade.js +1 -21
- package/dist/report.d.ts +1 -15
- package/dist/report.js +1 -13
- package/dist/reps.d.ts +1 -2
- package/dist/reps.js +0 -2
- package/dist/results.d.ts +0 -9
- package/dist/results.js +2 -19
- package/dist/run.js +11 -72
- package/dist/seeded.d.ts +1 -7
- package/dist/seeded.js +5 -11
- package/dist/sources.js +4 -9
- package/dist/spec.d.ts +0 -13
- package/dist/spec.js +6 -48
- package/dist/trajectory-events.d.ts +58 -0
- package/dist/trajectory-events.js +119 -0
- package/package.json +1 -1
- package/schemas/results-v2.schema.json +376 -75
- package/schemas/specification-v1.schema.json +128 -174
- package/dist/adjudication.d.ts +0 -219
- package/dist/adjudication.js +0 -422
- package/dist/adoption.d.ts +0 -128
- package/dist/adoption.js +0 -102
- package/dist/affected.d.ts +0 -88
- package/dist/affected.js +0 -222
- package/dist/capture.d.ts +0 -193
- package/dist/capture.js +0 -344
- package/dist/comparison.d.ts +0 -97
- package/dist/comparison.js +0 -189
- package/dist/factory-calibration.d.ts +0 -63
- package/dist/factory-calibration.js +0 -115
- package/dist/intervention-results.d.ts +0 -6
- package/dist/intervention-results.js +0 -52
- package/dist/intervention.d.ts +0 -123
- package/dist/intervention.js +0 -218
- package/dist/investigation.d.ts +0 -94
- package/dist/investigation.js +0 -148
- package/dist/judge-agreement.d.ts +0 -19
- package/dist/judge-agreement.js +0 -31
- package/dist/qualification-capture.d.ts +0 -44
- package/dist/qualification-capture.js +0 -163
- package/dist/qualification-config.d.ts +0 -233
- package/dist/qualification-config.js +0 -694
- package/dist/qualification-lock.d.ts +0 -15
- package/dist/qualification-lock.js +0 -87
- package/dist/qualification-oauth-directory.d.ts +0 -79
- package/dist/qualification-oauth-directory.js +0 -444
- package/dist/qualification-panel-store.d.ts +0 -14
- package/dist/qualification-panel-store.js +0 -188
- package/dist/qualification-panels.d.ts +0 -99
- package/dist/qualification-panels.js +0 -91
- package/dist/qualification-process.d.ts +0 -13
- package/dist/qualification-process.js +0 -190
- package/dist/qualification-runner.d.ts +0 -203
- package/dist/qualification-runner.js +0 -2037
- package/dist/qualification-store.d.ts +0 -187
- package/dist/qualification-store.js +0 -833
- package/dist/qualification-validation.d.ts +0 -12
- package/dist/qualification-validation.js +0 -8
- package/dist/screen.d.ts +0 -31
- package/dist/screen.js +0 -105
- package/dist/trajectory-gates.d.ts +0 -137
- package/dist/trajectory-gates.js +0 -576
- package/dist/trusted-host-supervision.d.ts +0 -25
- package/dist/trusted-host-supervision.js +0 -94
- package/dist/work-capture-schema.d.ts +0 -38
- package/dist/work-capture-schema.js +0 -33
- package/dist/work-capture.d.ts +0 -43
- package/dist/work-capture.js +0 -81
- package/dist/work-signals.d.ts +0 -55
- package/dist/work-signals.js +0 -71
- package/schemas/qualification-cell-result-v1.schema.json +0 -32
- package/schemas/qualification-config-v1.schema.json +0 -244
- package/schemas/qualification-invocation-request-v1.schema.json +0 -59
- package/schemas/qualification-judge-panel-result-v1.schema.json +0 -42
- package/schemas/trajectory-event-v1.schema.json +0 -80
|
@@ -1,2037 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, } from "node:fs";
|
|
4
|
-
import { isAbsolute, join } from "node:path";
|
|
5
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
|
-
import { detectMisfire, parseVerdict } from "./grade.js";
|
|
7
|
-
import { collapseVotePanel } from "./vote-panel.js";
|
|
8
|
-
import { attestQualificationPiJsonl as attestPiJsonl, openQualificationBoundedCapture as openBoundedCapture, qualificationOutputReceipt as outputReceipt, redactQualificationOutput, spawnQualificationCapture as spawnCapture, verifyQualificationOutputReceipt as verifyOutputReceipt, } from "./qualification-capture.js";
|
|
9
|
-
export { redactQualificationOutput } from "./qualification-capture.js";
|
|
10
|
-
import { cleanupQualificationProcessGroupAfterLeaderExit, qualificationProcessIdentity, qualificationProcessMatches, terminateInterruptedQualificationProcessGroup, terminateQualificationByIdentity, terminateQualificationProcess, } from "./qualification-process.js";
|
|
11
|
-
export { qualificationProcessIdentity, qualificationProcessMatches } from "./qualification-process.js";
|
|
12
|
-
import { qualificationLockOwnerIsLive, readQualificationLockOwner, reclaimQualificationLock, releaseQualificationLock, tryPublishQualificationLock, } from "./qualification-lock.js";
|
|
13
|
-
import { QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2, QUALIFICATION_TERMINAL_RECEIPT_VERSION_V3, qualificationOAuthDirectoryPolicy, sanitizeQualificationEnvironment, qualificationCanonicalJson, qualificationSha256, verifyQualificationExecutable, verifyQualificationPins, verifyQualificationResource, } from "./qualification-config.js";
|
|
14
|
-
import { assertQualificationOAuthDirectoryContinuityV2, assertQualificationOAuthDirectoryPolicyV2, inspectQualificationOAuthDirectoryPolicyV2, invalidateQualificationOAuthDirectoryInventoryV2, validateQualificationOAuthDirectoryInventoryV2, } from "./qualification-oauth-directory.js";
|
|
15
|
-
import { appendQualificationAccountingEvent, atomicWriteBytes, atomicWriteCanonical, fsyncQualificationDirectory, qualificationSpoolPaths, readCanonicalJson, readQualificationAccounting, readQualificationInvocation, readQualificationLifecycle, readQualificationSpoolConfig, transitionQualificationLifecycle, validateQualificationSpool, writeQualificationAccounting, writeQualificationLifecycle, verifyQualificationInvocationInput, QUALIFICATION_INVOCATION_VERSION_V3, QUALIFICATION_INVOCATION_VERSION_V4, } from "./qualification-store.js";
|
|
16
|
-
export const QUALIFICATION_AUTH_EVIDENCE_VERSION = "qualification-auth-evidence-v1";
|
|
17
|
-
export const QUALIFICATION_AUTH_EVIDENCE_VERSION_V2 = "qualification-auth-evidence-v2";
|
|
18
|
-
export const QUALIFICATION_TERMINAL_RECEIPT_VERSION = "qualification-terminal-receipt-v1";
|
|
19
|
-
export const QUALIFICATION_TERMINAL_RECEIPT_VERSION_V2 = "qualification-terminal-receipt-v2";
|
|
20
|
-
export const QUALIFICATION_LAUNCH_ATTEMPT_VERSION = "qualification-launch-attempt-v1";
|
|
21
|
-
export const QUALIFICATION_LAUNCH_ATTEMPT_VERSION_V2 = "qualification-launch-attempt-v2";
|
|
22
|
-
export const QUALIFICATION_INPUT_LAUNCH_CLAIM_VERSION = "qualification-input-launch-claim-v1";
|
|
23
|
-
/**
|
|
24
|
-
* Verify that the Pi agent directory exposed to a production child contains an
|
|
25
|
-
* OAuth credential for openai-codex, no stored API-key credentials, and no
|
|
26
|
-
* models.json override capable of redirecting that provider. No credential
|
|
27
|
-
* value or digest leaves this function.
|
|
28
|
-
*/
|
|
29
|
-
export function assertQualificationOAuthCredentialBoundary(env) {
|
|
30
|
-
const agentDirectory = env.PI_CODING_AGENT_DIR
|
|
31
|
-
?? (env.HOME ? join(env.HOME, ".pi", "agent") : undefined);
|
|
32
|
-
if (!agentDirectory || !isAbsolute(agentDirectory))
|
|
33
|
-
throw new Error("qualification OAuth boundary requires an absolute PI_CODING_AGENT_DIR or HOME");
|
|
34
|
-
let agentStat;
|
|
35
|
-
try {
|
|
36
|
-
agentStat = lstatSync(agentDirectory);
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
throw new Error("qualification OAuth agent directory is missing");
|
|
40
|
-
}
|
|
41
|
-
if (agentStat.isSymbolicLink() || !agentStat.isDirectory())
|
|
42
|
-
throw new Error("qualification OAuth agent directory must be a regular non-symlink directory");
|
|
43
|
-
if (process.platform !== "win32" && ((agentStat.mode & 0o077) !== 0 || agentStat.uid !== process.getuid?.())) {
|
|
44
|
-
throw new Error("qualification OAuth agent directory must be private and owned by the qualification user");
|
|
45
|
-
}
|
|
46
|
-
const directoryEntries = readdirSync(agentDirectory, { withFileTypes: true }).map((entry) => entry.name).sort();
|
|
47
|
-
if (directoryEntries.some((entry) => entry !== "auth.json" && entry !== "models.json")) {
|
|
48
|
-
throw new Error("qualification dedicated OAuth agent directory contains undeclared entries");
|
|
49
|
-
}
|
|
50
|
-
const authPath = join(agentDirectory, "auth.json");
|
|
51
|
-
const openedAuth = readPrivateRegularFileOccurrence(authPath, "qualification OAuth auth.json");
|
|
52
|
-
let auth;
|
|
53
|
-
try {
|
|
54
|
-
auth = JSON.parse(openedAuth.bytes.toString("utf8"));
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
if (error instanceof SyntaxError)
|
|
58
|
-
throw new Error("qualification OAuth auth.json is invalid JSON");
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
61
|
-
if (!plainObject(auth))
|
|
62
|
-
throw new Error("qualification OAuth auth.json must be an object");
|
|
63
|
-
if (qualificationCanonicalJson(Object.keys(auth).sort()) !== qualificationCanonicalJson(["openai-codex"])) {
|
|
64
|
-
throw new Error("qualification dedicated OAuth credential store must contain exactly the openai-codex OAuth credential");
|
|
65
|
-
}
|
|
66
|
-
const codex = auth["openai-codex"];
|
|
67
|
-
if (!plainObject(codex) || codex.type !== "oauth")
|
|
68
|
-
throw new Error("qualification openai-codex credential is missing or is not OAuth");
|
|
69
|
-
const modelsPath = join(agentDirectory, "models.json");
|
|
70
|
-
let modelsIdentity = null;
|
|
71
|
-
if (existsSync(modelsPath)) {
|
|
72
|
-
const openedModels = readPrivateRegularFileOccurrence(modelsPath, "qualification Pi models.json");
|
|
73
|
-
modelsIdentity = openedModels.identity;
|
|
74
|
-
let models;
|
|
75
|
-
try {
|
|
76
|
-
models = JSON.parse(openedModels.bytes.toString("utf8"));
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
throw new Error("qualification Pi models.json is invalid JSON");
|
|
80
|
-
}
|
|
81
|
-
if (!plainObject(models))
|
|
82
|
-
throw new Error("qualification Pi models.json must be an object");
|
|
83
|
-
if (Object.keys(models).length > 0) {
|
|
84
|
-
throw new Error("qualification dedicated OAuth agent directory must not carry models.json overrides or embedded provider credentials");
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const finalAgentStat = lstatSync(agentDirectory);
|
|
88
|
-
if (finalAgentStat.dev !== agentStat.dev || finalAgentStat.ino !== agentStat.ino || finalAgentStat.mtimeMs !== agentStat.mtimeMs) {
|
|
89
|
-
throw new Error("qualification OAuth agent directory changed while it was verified");
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
agent_directory: agentDirectory,
|
|
93
|
-
provider: "openai-codex",
|
|
94
|
-
auth_type: "oauth",
|
|
95
|
-
agent_directory_identity: filesystemOccurrence(agentDirectory, agentStat),
|
|
96
|
-
auth_file_identity: openedAuth.identity,
|
|
97
|
-
models_file_identity: modelsIdentity,
|
|
98
|
-
directory_entries: directoryEntries,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
function assertConfiguredOAuthBoundary(mode, policy, env, validationPoint, now) {
|
|
102
|
-
if (policy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
103
|
-
return assertQualificationOAuthDirectoryPolicyV2(env, { validation_point: validationPoint, now });
|
|
104
|
-
}
|
|
105
|
-
return mode === "production" ? assertQualificationOAuthCredentialBoundary(env) : null;
|
|
106
|
-
}
|
|
107
|
-
function isOAuthBoundaryV2(value) {
|
|
108
|
-
return value !== null && "policy" in value && value.policy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2;
|
|
109
|
-
}
|
|
110
|
-
export async function checkQualificationAuthentication(options) {
|
|
111
|
-
const invocation = readQualificationInvocation(options.spool_dir, options.invocation_id);
|
|
112
|
-
const config = readQualificationSpoolConfig(options.spool_dir);
|
|
113
|
-
const arm = config.arms.find((candidate) => candidate.id === invocation.arms.selected);
|
|
114
|
-
if (!arm)
|
|
115
|
-
throw new Error(`qualification selected arm ${invocation.arms.selected} disappeared from configuration`);
|
|
116
|
-
assertQualificationPanelMemberLaunchAuthorized(options.spool_dir, invocation);
|
|
117
|
-
const paths = qualificationSpoolPaths(options.spool_dir, options.invocation_id);
|
|
118
|
-
const initialLifecycle = readQualificationLifecycle(paths.root, invocation.invocation_id);
|
|
119
|
-
if (initialLifecycle.phase === "terminal" || existsSync(paths.terminal)) {
|
|
120
|
-
throw new Error(`qualification authentication cannot be refreshed after invocation ${invocation.invocation_id} is terminal`);
|
|
121
|
-
}
|
|
122
|
-
const initialSupervisor = latestContinuationSupervisor(paths.invocation, invocation) ?? latestProcessIdentity(initialLifecycle, "supervisor");
|
|
123
|
-
if (initialLifecycle.phase !== "prepared" && initialSupervisor && qualificationProcessMatches(initialSupervisor)) {
|
|
124
|
-
throw new Error(`qualification authentication cannot be refreshed while invocation ${invocation.invocation_id} has a live supervisor`);
|
|
125
|
-
}
|
|
126
|
-
verifyQualificationPins(config);
|
|
127
|
-
assertRunningQualificationRunner(config.mode, config.runner.executable);
|
|
128
|
-
verifyQualificationExecutable(config.runner.executable);
|
|
129
|
-
const executable = verifyQualificationExecutable(arm.executable);
|
|
130
|
-
assertQualificationExecutableIdentity(invocation, executable);
|
|
131
|
-
for (const resource of invocation.execution.resources)
|
|
132
|
-
verifyQualificationResource(resource);
|
|
133
|
-
assertQualificationWorkingDirectory(invocation);
|
|
134
|
-
const sanitized = sanitizeQualificationEnvironment(options.parent_env ?? process.env, arm.allowed_environment_names, config.runner.conflicting_parent_environment);
|
|
135
|
-
const oauthPolicy = qualificationOAuthDirectoryPolicy(config);
|
|
136
|
-
const beforeOAuthBoundary = assertConfiguredOAuthBoundary(config.mode, oauthPolicy, sanitized.env, "before-oauth-readiness", options.now);
|
|
137
|
-
const result = await spawnCapture(arm.executable.path, ["auth", "check", "--provider", arm.provider, "--model", arm.model, "--json"], { cwd: invocation.scenario.working_directory, env: sanitized.env, timeout_ms: Math.min(30_000, arm.timeout_ms), output_limit_bytes: 64 * 1024 });
|
|
138
|
-
if (result.timed_out)
|
|
139
|
-
throw new Error("qualification OAuth readiness check timed out");
|
|
140
|
-
if (result.code !== 0)
|
|
141
|
-
throw new Error(`qualification auth check exited ${String(result.code)}`);
|
|
142
|
-
let parsed;
|
|
143
|
-
try {
|
|
144
|
-
parsed = JSON.parse(result.stdout.trim());
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
throw new Error("qualification auth check returned invalid JSON");
|
|
148
|
-
}
|
|
149
|
-
if (!plainObject(parsed))
|
|
150
|
-
throw new Error("qualification auth check returned a non-object response");
|
|
151
|
-
const unknownAuthField = Object.keys(parsed).find((key) => !["status", "provider", "reason", "authType", "model"].includes(key));
|
|
152
|
-
if (unknownAuthField)
|
|
153
|
-
throw new Error(`qualification auth check returned unknown field ${unknownAuthField}`);
|
|
154
|
-
if (parsed.status !== "ready")
|
|
155
|
-
throw new Error(`qualification OAuth readiness is not ready for ${arm.provider}:${arm.model}`);
|
|
156
|
-
if (parsed.provider !== arm.provider)
|
|
157
|
-
throw new Error(`qualification auth check provider substitution: requested ${arm.provider}, reported ${String(parsed.provider)}`);
|
|
158
|
-
if (parsed.model !== undefined && parsed.model !== arm.model)
|
|
159
|
-
throw new Error(`qualification auth check model substitution: requested ${arm.model}, reported ${String(parsed.model)}`);
|
|
160
|
-
if (parsed.authType !== "oauth")
|
|
161
|
-
throw new Error(`qualification requires OAuth readiness; Pi reported ${String(parsed.authType ?? "missing auth type")}`);
|
|
162
|
-
const afterOAuthBoundary = assertConfiguredOAuthBoundary(config.mode, oauthPolicy, sanitized.env, "after-oauth-readiness", options.now);
|
|
163
|
-
if (oauthPolicy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
164
|
-
if (!isOAuthBoundaryV2(beforeOAuthBoundary) || !isOAuthBoundaryV2(afterOAuthBoundary)) {
|
|
165
|
-
throw new Error("qualification OAuth directory policy v2 did not produce both readiness inventories");
|
|
166
|
-
}
|
|
167
|
-
// The auth metadata subprocess is an authorized Pi execution. It may create
|
|
168
|
-
// or atomically replace Pi's own runtime-state file, but not auth/models.
|
|
169
|
-
assertQualificationOAuthDirectoryContinuityV2(beforeOAuthBoundary.inventory, afterOAuthBoundary.inventory, { allow_models_store_change: true });
|
|
170
|
-
}
|
|
171
|
-
const launchAuthority = randomBytes(32).toString("base64url");
|
|
172
|
-
const commonEvidence = {
|
|
173
|
-
checked_at: timestamp((options.now ?? (() => new Date().toISOString()))(), "qualification auth check time"),
|
|
174
|
-
provider: arm.provider,
|
|
175
|
-
requested_model: arm.model,
|
|
176
|
-
model_identity_observed: false,
|
|
177
|
-
status: "ready",
|
|
178
|
-
auth_type: "oauth",
|
|
179
|
-
source: "pi-auth-check-json",
|
|
180
|
-
credentials_included: false,
|
|
181
|
-
readiness_only: true,
|
|
182
|
-
removed_parent_environment_names: sanitized.removed_names,
|
|
183
|
-
child_environment_names: Object.keys(sanitized.env).sort(),
|
|
184
|
-
executable_sha256: executable.sha256,
|
|
185
|
-
launch_authority_sha256: qualificationSha256(launchAuthority),
|
|
186
|
-
};
|
|
187
|
-
let evidence;
|
|
188
|
-
if (oauthPolicy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
189
|
-
if (!isOAuthBoundaryV2(beforeOAuthBoundary) || !isOAuthBoundaryV2(afterOAuthBoundary))
|
|
190
|
-
throw new Error("qualification OAuth directory policy v2 evidence is incomplete");
|
|
191
|
-
const evidenceBase = {
|
|
192
|
-
schema_version: QUALIFICATION_AUTH_EVIDENCE_VERSION_V2,
|
|
193
|
-
...commonEvidence,
|
|
194
|
-
oauth_directory_policy: QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2,
|
|
195
|
-
oauth_directory_validations: {
|
|
196
|
-
before_oauth_readiness: beforeOAuthBoundary.inventory,
|
|
197
|
-
after_oauth_readiness: afterOAuthBoundary.inventory,
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
evidence = { ...evidenceBase, evidence_sha256: qualificationSha256(qualificationCanonicalJson(evidenceBase)) };
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
const oauthBoundary = afterOAuthBoundary && !isOAuthBoundaryV2(afterOAuthBoundary) ? afterOAuthBoundary : null;
|
|
204
|
-
const evidenceBase = {
|
|
205
|
-
schema_version: QUALIFICATION_AUTH_EVIDENCE_VERSION,
|
|
206
|
-
...commonEvidence,
|
|
207
|
-
oauth_agent_directory_identity: oauthBoundary?.agent_directory_identity ?? null,
|
|
208
|
-
oauth_auth_file_identity: oauthBoundary?.auth_file_identity ?? null,
|
|
209
|
-
oauth_models_file_identity: oauthBoundary?.models_file_identity ?? null,
|
|
210
|
-
oauth_directory_entries: oauthBoundary?.directory_entries ?? [],
|
|
211
|
-
};
|
|
212
|
-
evidence = { ...evidenceBase, evidence_sha256: qualificationSha256(qualificationCanonicalJson(evidenceBase)) };
|
|
213
|
-
}
|
|
214
|
-
const authPath = join(paths.invocation, "auth-evidence.json");
|
|
215
|
-
const published = await withAsyncLock(paths.invocationLock, async () => {
|
|
216
|
-
const lifecycle = readQualificationLifecycle(paths.root, invocation.invocation_id);
|
|
217
|
-
if (lifecycle.phase === "terminal" || existsSync(paths.terminal)) {
|
|
218
|
-
throw new Error(`qualification authentication cannot be refreshed after invocation ${invocation.invocation_id} is terminal`);
|
|
219
|
-
}
|
|
220
|
-
const activeSupervisor = latestContinuationSupervisor(paths.invocation, invocation) ?? latestProcessIdentity(lifecycle, "supervisor");
|
|
221
|
-
if (lifecycle.phase !== "prepared" && activeSupervisor && qualificationProcessMatches(activeSupervisor)) {
|
|
222
|
-
throw new Error(`qualification authentication cannot be refreshed while invocation ${invocation.invocation_id} has a live supervisor`);
|
|
223
|
-
}
|
|
224
|
-
if (existsSync(authPath)) {
|
|
225
|
-
const existing = readCanonicalJson(authPath, `qualification auth evidence ${options.invocation_id}`);
|
|
226
|
-
try {
|
|
227
|
-
validateAuthEvidence(existing, invocation, true);
|
|
228
|
-
if (existing.provider !== evidence.provider || existing.requested_model !== evidence.requested_model || existing.executable_sha256 !== evidence.executable_sha256) {
|
|
229
|
-
throw new Error("qualification auth evidence contradicts the current invocation");
|
|
230
|
-
}
|
|
231
|
-
// A separately computed child environment must never reuse another
|
|
232
|
-
// start contender's evidence. Validation above is corruption checking;
|
|
233
|
-
// this contender always publishes its own opaque launch binding.
|
|
234
|
-
}
|
|
235
|
-
catch (error) {
|
|
236
|
-
if (!(error instanceof Error) || !/auth evidence.*stale/i.test(error.message))
|
|
237
|
-
throw error;
|
|
238
|
-
}
|
|
239
|
-
atomicWriteCanonical(authPath, evidence, false);
|
|
240
|
-
return evidence;
|
|
241
|
-
}
|
|
242
|
-
atomicWriteCanonical(authPath, evidence, true);
|
|
243
|
-
return evidence;
|
|
244
|
-
});
|
|
245
|
-
return { evidence: published, child_env: sanitized.env, launch_authority: launchAuthority };
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Execute one previously prepared invocation. There is deliberately no retry
|
|
249
|
-
* parameter and exactly one model-process spawn site in this function.
|
|
250
|
-
*/
|
|
251
|
-
export async function superviseQualificationInvocation(options) {
|
|
252
|
-
const now = options.now ?? (() => new Date().toISOString());
|
|
253
|
-
const invocation = readQualificationInvocation(options.spool_dir, options.invocation_id);
|
|
254
|
-
const config = readQualificationSpoolConfig(options.spool_dir);
|
|
255
|
-
const oauthPolicy = qualificationOAuthDirectoryPolicy(config);
|
|
256
|
-
if (options.test_hooks && config.mode !== "test")
|
|
257
|
-
throw new Error("qualification runner test hooks are prohibited in production");
|
|
258
|
-
const arm = config.arms.find((candidate) => candidate.id === invocation.arms.selected);
|
|
259
|
-
if (!arm)
|
|
260
|
-
throw new Error(`qualification selected arm ${invocation.arms.selected} disappeared from configuration`);
|
|
261
|
-
const paths = qualificationSpoolPaths(options.spool_dir, options.invocation_id);
|
|
262
|
-
if (existsSync(paths.terminal)) {
|
|
263
|
-
reconcileTerminalLifecycle(paths.root, invocation);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
verifyQualificationPins(config);
|
|
267
|
-
assertRunningQualificationRunner(config.mode, config.runner.executable);
|
|
268
|
-
verifyQualificationExecutable(config.runner.executable);
|
|
269
|
-
assertQualificationExecutableIdentity(invocation, verifyQualificationExecutable(arm.executable));
|
|
270
|
-
for (const resource of invocation.execution.resources)
|
|
271
|
-
verifyQualificationResource(resource);
|
|
272
|
-
assertQualificationWorkingDirectory(invocation);
|
|
273
|
-
assertInputUnchanged(invocation);
|
|
274
|
-
assertChildEnvironment(options.child_env, arm.allowed_environment_names);
|
|
275
|
-
if (config.mode === "production" && oauthPolicy !== QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
276
|
-
assertQualificationOAuthCredentialBoundary(options.child_env);
|
|
277
|
-
}
|
|
278
|
-
let auth;
|
|
279
|
-
const supervisorIdentity = qualificationProcessIdentity(process.pid);
|
|
280
|
-
let ownsLaunch = false;
|
|
281
|
-
let reconcileExistingAttempt = false;
|
|
282
|
-
let accountingEventSha = "";
|
|
283
|
-
let continuationSha = null;
|
|
284
|
-
let claimedInputBinding = null;
|
|
285
|
-
await withAsyncLock(paths.lock, async () => {
|
|
286
|
-
await withAsyncLock(paths.invocationLock, async () => {
|
|
287
|
-
let lifecycle = readQualificationLifecycle(paths.root, invocation.invocation_id);
|
|
288
|
-
if (lifecycle.phase === "terminal" || existsSync(paths.terminal)) {
|
|
289
|
-
if (existsSync(paths.terminal))
|
|
290
|
-
reconcileTerminalLifecycle(paths.root, invocation);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
const currentAuth = readAuthEvidence(options.spool_dir, invocation);
|
|
294
|
-
const claimInputBinding = isInputBoundQualificationInvocation(invocation)
|
|
295
|
-
? verifyQualificationInvocationInput(invocation)
|
|
296
|
-
: null;
|
|
297
|
-
const claimBoundary = assertConfiguredOAuthBoundary(config.mode, oauthPolicy, options.child_env, "before-launch-claim", options.now);
|
|
298
|
-
assertConfiguredOAuthBoundaryMatchesEvidence(currentAuth, claimBoundary, false);
|
|
299
|
-
if (config.mode === "production" && !options.authentication_authority) {
|
|
300
|
-
throw new Error("qualification production launch requires the private authentication authority from its auth check");
|
|
301
|
-
}
|
|
302
|
-
if (options.authentication_authority && qualificationSha256(options.authentication_authority) !== currentAuth.launch_authority_sha256) {
|
|
303
|
-
throw new Error("qualification authentication authority does not bind the current auth evidence");
|
|
304
|
-
}
|
|
305
|
-
auth = currentAuth;
|
|
306
|
-
const ledger = readQualificationAccounting(paths.root);
|
|
307
|
-
const existingClaim = ledger.events.find((event) => event.invocation_id === invocation.invocation_id);
|
|
308
|
-
if (oauthPolicy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
309
|
-
if (!isOAuthBoundaryV2(claimBoundary))
|
|
310
|
-
throw new Error("qualification OAuth directory v2 launch-claim inventory is missing");
|
|
311
|
-
if (existingClaim) {
|
|
312
|
-
readOAuthDirectoryCheckpointV2(paths.invocation, invocation, "before-launch-claim");
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
// Published before the accounting append. A crash before the append may
|
|
316
|
-
// replace this unclaimed checkpoint; once an event exists it is immutable.
|
|
317
|
-
writeOAuthDirectoryCheckpointV2(paths.invocation, invocation, "before-launch-claim", claimBoundary.inventory, false);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
if (existingClaim) {
|
|
321
|
-
accountingEventSha = existingClaim.event_sha256;
|
|
322
|
-
claimedInputBinding = isInputBoundQualificationInvocation(invocation)
|
|
323
|
-
? readInputLaunchClaim(paths.invocation, invocation, accountingEventSha)
|
|
324
|
-
: null;
|
|
325
|
-
const launchAttemptExists = existsSync(join(paths.invocation, "launch-attempt.json"));
|
|
326
|
-
const priorSupervisor = latestContinuationSupervisor(paths.invocation, invocation) ?? latestProcessIdentity(lifecycle, "supervisor");
|
|
327
|
-
if (lifecycle.phase !== "prepared" && priorSupervisor && qualificationProcessMatches(priorSupervisor))
|
|
328
|
-
return;
|
|
329
|
-
if (!options.continuation_authority) {
|
|
330
|
-
throw new Error("qualification launch claim was interrupted; explicit continuation authority is required and no relaunch is permitted automatically");
|
|
331
|
-
}
|
|
332
|
-
continuationSha = recordContinuationAuthority(paths.root, invocation, options.continuation_authority, priorSupervisor, supervisorIdentity, now());
|
|
333
|
-
if (launchAttemptExists || lifecycle.phase === "running") {
|
|
334
|
-
reconcileExistingAttempt = true;
|
|
335
|
-
ownsLaunch = true;
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (lifecycle.phase !== "prepared" && lifecycle.phase !== "launch-claimed") {
|
|
339
|
-
throw new Error(`qualification interrupted lifecycle ${lifecycle.phase} cannot be continued`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
if (lifecycle.phase !== "prepared")
|
|
344
|
-
throw new Error("qualification lifecycle claims launch without an accounting event");
|
|
345
|
-
if (existsSync(join(paths.invocation, "abort-request.json"))) {
|
|
346
|
-
await finalizeWithoutChild({ invocation, spoolDir: paths.root, status: "aborted", supervisor: supervisorIdentity, auth, now: now(), error: "aborted before launch claim" });
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
const next = appendQualificationAccountingEvent(ledger, {
|
|
350
|
-
invocation_id: invocation.invocation_id,
|
|
351
|
-
role: invocation.role,
|
|
352
|
-
call_class: arm.kind,
|
|
353
|
-
counts_as_measurement: invocation.counts_as_measurement,
|
|
354
|
-
launched_at: timestamp(now(), "qualification launch claim time"),
|
|
355
|
-
});
|
|
356
|
-
writeQualificationAccounting(paths.root, next);
|
|
357
|
-
accountingEventSha = next.chain_head;
|
|
358
|
-
if (claimInputBinding) {
|
|
359
|
-
writeInputLaunchClaim(paths.invocation, invocation, accountingEventSha, claimInputBinding, supervisorIdentity, timestamp(now(), "qualification input launch claim time"));
|
|
360
|
-
claimedInputBinding = claimInputBinding;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
if (lifecycle.phase === "prepared") {
|
|
364
|
-
lifecycle = transitionQualificationLifecycle(lifecycle, "launch-claimed", {
|
|
365
|
-
at: timestamp(now(), "qualification launch claim time"),
|
|
366
|
-
detail: {
|
|
367
|
-
attempt: 1,
|
|
368
|
-
accounting_event_sha256: accountingEventSha,
|
|
369
|
-
supervisor: supervisorIdentity,
|
|
370
|
-
continuation_authority_sha256: continuationSha,
|
|
371
|
-
...(claimedInputBinding ? { invocation_input: claimedInputBinding } : {}),
|
|
372
|
-
},
|
|
373
|
-
});
|
|
374
|
-
writeQualificationLifecycle(paths.root, lifecycle);
|
|
375
|
-
}
|
|
376
|
-
ownsLaunch = true;
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
if (!ownsLaunch)
|
|
380
|
-
return;
|
|
381
|
-
if (!auth)
|
|
382
|
-
throw new Error("qualification launch ownership lacks authentication evidence");
|
|
383
|
-
if (reconcileExistingAttempt) {
|
|
384
|
-
await reconcileInterruptedAttempt({
|
|
385
|
-
invocation,
|
|
386
|
-
spoolDir: paths.root,
|
|
387
|
-
supervisor: supervisorIdentity,
|
|
388
|
-
auth,
|
|
389
|
-
accountingEventSha,
|
|
390
|
-
continuationSha,
|
|
391
|
-
now: now(),
|
|
392
|
-
status: existsSync(join(paths.invocation, "abort-request.json")) ? "aborted" : "failed",
|
|
393
|
-
error: existsSync(join(paths.invocation, "abort-request.json")) ? "aborted during interrupted launch" : "interrupted launch reconciled without replay",
|
|
394
|
-
});
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const launchAttemptPath = join(paths.invocation, "launch-attempt.json");
|
|
398
|
-
if (existsSync(launchAttemptPath))
|
|
399
|
-
throw new Error("qualification launch attempt already exists; automatic retry is prohibited");
|
|
400
|
-
if (existsSync(join(paths.invocation, "abort-request.json"))) {
|
|
401
|
-
await finalizeWithoutChild({ invocation, spoolDir: paths.root, status: "aborted", supervisor: supervisorIdentity, auth, now: now(), error: "aborted after launch claim", accountingEventSha, continuationSha });
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const artifactAbsolute = join(paths.root, invocation.expected_artifact.path);
|
|
405
|
-
const argv = buildQualificationArgv(invocation, artifactAbsolute);
|
|
406
|
-
const attemptAt = timestamp(now(), "qualification launch attempt time");
|
|
407
|
-
try {
|
|
408
|
-
atomicWriteCanonical(launchAttemptPath, {
|
|
409
|
-
schema_version: claimedInputBinding ? QUALIFICATION_LAUNCH_ATTEMPT_VERSION_V2 : QUALIFICATION_LAUNCH_ATTEMPT_VERSION,
|
|
410
|
-
invocation_id: invocation.invocation_id,
|
|
411
|
-
attempt: 1,
|
|
412
|
-
at: attemptAt,
|
|
413
|
-
executable: arm.executable,
|
|
414
|
-
argv,
|
|
415
|
-
environment_names: Object.keys(options.child_env).sort(),
|
|
416
|
-
automatic_retry: false,
|
|
417
|
-
...(claimedInputBinding ? { invocation_input: claimedInputBinding, accounting_event_sha256: accountingEventSha } : {}),
|
|
418
|
-
}, true);
|
|
419
|
-
}
|
|
420
|
-
catch (error) {
|
|
421
|
-
await finalizeWithoutChild({ invocation, spoolDir: paths.root, status: "failed", supervisor: supervisorIdentity, auth, now: now(), error: `launch-attempt publication failed: ${safeError(error)}`, accountingEventSha, continuationSha });
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
const stdoutPartial = join(paths.invocation, "stdout.partial");
|
|
425
|
-
const stderrPartial = join(paths.invocation, "stderr.partial");
|
|
426
|
-
const credentialValues = config.mode === "production" ? qualificationCredentialRedactionValues(options.child_env) : [];
|
|
427
|
-
const outputRedactor = (text) => redactQualificationOutput(text, credentialValues);
|
|
428
|
-
let stdoutCapture = null;
|
|
429
|
-
let stderrCapture = null;
|
|
430
|
-
try {
|
|
431
|
-
// The claim and immutable attempt already exist. Revalidate every external
|
|
432
|
-
// production pin and every materialized launch input immediately before spawn.
|
|
433
|
-
verifyQualificationPins(config);
|
|
434
|
-
const prelaunchBoundary = assertConfiguredOAuthBoundary(config.mode, oauthPolicy, options.child_env, "immediately-before-pi-launch", options.now);
|
|
435
|
-
assertConfiguredOAuthBoundaryMatchesEvidence(auth, prelaunchBoundary, false);
|
|
436
|
-
if (oauthPolicy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
437
|
-
if (!isOAuthBoundaryV2(prelaunchBoundary))
|
|
438
|
-
throw new Error("qualification OAuth directory v2 prelaunch inventory is missing");
|
|
439
|
-
const recordedClaim = readOAuthDirectoryCheckpointV2(paths.invocation, invocation, "before-launch-claim");
|
|
440
|
-
assertQualificationOAuthDirectoryContinuityV2(recordedClaim, prelaunchBoundary.inventory, {
|
|
441
|
-
// A continuation performs a fresh authorized Pi auth check after the
|
|
442
|
-
// original claim. That subprocess may legitimately update runtime state.
|
|
443
|
-
allow_models_store_change: continuationSha !== null,
|
|
444
|
-
});
|
|
445
|
-
writeOAuthDirectoryCheckpointV2(paths.invocation, invocation, "immediately-before-pi-launch", prelaunchBoundary.inventory, true);
|
|
446
|
-
}
|
|
447
|
-
assertQualificationExecutableIdentity(invocation, verifyQualificationExecutable(arm.executable));
|
|
448
|
-
for (const resource of invocation.execution.resources)
|
|
449
|
-
verifyQualificationResource(resource);
|
|
450
|
-
assertQualificationWorkingDirectory(invocation);
|
|
451
|
-
assertInputUnchanged(invocation);
|
|
452
|
-
stdoutCapture = openBoundedCapture(stdoutPartial, arm.output_limit_bytes, outputRedactor);
|
|
453
|
-
stderrCapture = openBoundedCapture(stderrPartial, arm.output_limit_bytes, outputRedactor);
|
|
454
|
-
}
|
|
455
|
-
catch (error) {
|
|
456
|
-
stdoutCapture?.close();
|
|
457
|
-
stderrCapture?.close();
|
|
458
|
-
await finalizeWithoutChild({ invocation, spoolDir: paths.root, status: "failed", supervisor: supervisorIdentity, auth, now: now(), error: `post-claim setup failed: ${safeError(error)}`, accountingEventSha, continuationSha });
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
if (!stdoutCapture || !stderrCapture)
|
|
462
|
-
throw new Error("qualification output capture setup did not complete");
|
|
463
|
-
let child = null;
|
|
464
|
-
let closed = null;
|
|
465
|
-
let childIdentity = null;
|
|
466
|
-
let childIdentityError = null;
|
|
467
|
-
let startedAt = attemptAt;
|
|
468
|
-
let deadlineAt = new Date(Date.parse(attemptAt) + arm.timeout_ms).toISOString();
|
|
469
|
-
try {
|
|
470
|
-
await withAsyncLock(paths.invocationLock, async () => {
|
|
471
|
-
if (existsSync(join(paths.invocation, "abort-request.json")))
|
|
472
|
-
throw new Error("aborted before process spawn");
|
|
473
|
-
options.test_hooks?.immediately_before_child_input_validation?.();
|
|
474
|
-
const spawnInputBinding = isInputBoundQualificationInvocation(invocation)
|
|
475
|
-
? verifyQualificationInvocationInput(invocation)
|
|
476
|
-
: null;
|
|
477
|
-
if (qualificationCanonicalJson(spawnInputBinding) !== qualificationCanonicalJson(claimedInputBinding))
|
|
478
|
-
throw new Error("qualification invocation input changed between launch claim and child spawn");
|
|
479
|
-
const lifecycle = readQualificationLifecycle(paths.root, invocation.invocation_id);
|
|
480
|
-
if (lifecycle.phase !== "launch-claimed")
|
|
481
|
-
throw new Error(`qualification invocation entered ${lifecycle.phase} before process start could be recorded`);
|
|
482
|
-
child = spawn(arm.executable.path, argv, {
|
|
483
|
-
cwd: invocation.scenario.working_directory,
|
|
484
|
-
env: options.child_env,
|
|
485
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
486
|
-
detached: process.platform !== "win32",
|
|
487
|
-
});
|
|
488
|
-
const launchedChild = child;
|
|
489
|
-
closed = new Promise((resolve) => {
|
|
490
|
-
let settled = false;
|
|
491
|
-
const finish = (value) => {
|
|
492
|
-
if (settled)
|
|
493
|
-
return;
|
|
494
|
-
settled = true;
|
|
495
|
-
resolve(value);
|
|
496
|
-
};
|
|
497
|
-
launchedChild.once("error", (error) => finish({ code: null, signal: null, error: safeError(error) }));
|
|
498
|
-
launchedChild.once("close", (code, signal) => finish({ code, signal }));
|
|
499
|
-
});
|
|
500
|
-
launchedChild.stdout?.on("data", (chunk) => stdoutCapture.write(chunk));
|
|
501
|
-
launchedChild.stderr?.on("data", (chunk) => stderrCapture.write(chunk));
|
|
502
|
-
if (launchedChild.pid) {
|
|
503
|
-
try {
|
|
504
|
-
childIdentity = qualificationProcessIdentity(launchedChild.pid);
|
|
505
|
-
}
|
|
506
|
-
catch (error) {
|
|
507
|
-
childIdentityError = safeError(error);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
else {
|
|
511
|
-
childIdentityError = "child process did not expose a PID occurrence identity";
|
|
512
|
-
}
|
|
513
|
-
startedAt = timestamp(now(), "qualification process start time");
|
|
514
|
-
deadlineAt = new Date(Date.parse(startedAt) + arm.timeout_ms).toISOString();
|
|
515
|
-
if (!childIdentity)
|
|
516
|
-
throw new Error(childIdentityError ?? "child process occurrence identity is unavailable");
|
|
517
|
-
atomicWriteCanonical(join(paths.invocation, "child-occurrence.json"), {
|
|
518
|
-
schema_version: claimedInputBinding ? "qualification-child-occurrence-v2" : "qualification-child-occurrence-v1",
|
|
519
|
-
invocation_id: invocation.invocation_id,
|
|
520
|
-
attempt: 1,
|
|
521
|
-
started_at: startedAt,
|
|
522
|
-
deadline_at: deadlineAt,
|
|
523
|
-
child: childIdentity,
|
|
524
|
-
...(claimedInputBinding ? { invocation_input: claimedInputBinding, accounting_event_sha256: accountingEventSha } : {}),
|
|
525
|
-
}, true);
|
|
526
|
-
writeQualificationLifecycle(paths.root, transitionQualificationLifecycle(lifecycle, "running", {
|
|
527
|
-
at: startedAt,
|
|
528
|
-
detail: { attempt: 1, supervisor: supervisorIdentity, child: childIdentity, deadline_at: deadlineAt, continuation_authority_sha256: continuationSha, ...(claimedInputBinding ? { invocation_input: claimedInputBinding } : {}) },
|
|
529
|
-
}));
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
catch (error) {
|
|
533
|
-
const aborted = existsSync(join(paths.invocation, "abort-request.json"));
|
|
534
|
-
const failedChild = child;
|
|
535
|
-
const failedClosed = closed;
|
|
536
|
-
if (!failedChild || !failedClosed) {
|
|
537
|
-
stdoutCapture.close();
|
|
538
|
-
stderrCapture.close();
|
|
539
|
-
await finalizeWithoutChild({ invocation, spoolDir: paths.root, status: aborted ? "aborted" : "failed", supervisor: supervisorIdentity, auth, now: now(), error: safeError(error), accountingEventSha, continuationSha });
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
terminateQualificationProcess(failedChild, childIdentity);
|
|
543
|
-
const outcome = await failedClosed;
|
|
544
|
-
await cleanupQualificationProcessGroupAfterLeaderExit(failedChild.pid, childIdentity);
|
|
545
|
-
stdoutCapture.close();
|
|
546
|
-
stderrCapture.close();
|
|
547
|
-
await finalizeAfterCapture({ invocation, spoolDir: paths.root, terminalStatus: aborted ? "aborted" : "failed", supervisor: supervisorIdentity, child: childIdentity, auth, accountingEventSha, continuationSha, startedAt, deadlineAt, exitCode: outcome.code, signal: outcome.signal, stdoutCapture, stderrCapture, now: now(), error: `process occurrence publication failed: ${safeError(error)}` });
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
const runningChild = child;
|
|
551
|
-
const runningClosed = closed;
|
|
552
|
-
if (!runningChild || !runningClosed)
|
|
553
|
-
throw new Error("qualification process launch did not publish a child occurrence");
|
|
554
|
-
let termination = "exit";
|
|
555
|
-
const timeoutTimer = setTimeout(() => {
|
|
556
|
-
if (termination !== "exit")
|
|
557
|
-
return;
|
|
558
|
-
termination = "timeout";
|
|
559
|
-
terminateQualificationProcess(runningChild, childIdentity);
|
|
560
|
-
}, arm.timeout_ms);
|
|
561
|
-
const abortTimer = setInterval(() => {
|
|
562
|
-
if (termination !== "exit")
|
|
563
|
-
return;
|
|
564
|
-
if (existsSync(join(paths.invocation, "abort-request.json"))) {
|
|
565
|
-
termination = "abort";
|
|
566
|
-
terminateQualificationProcess(runningChild, childIdentity);
|
|
567
|
-
}
|
|
568
|
-
}, 20);
|
|
569
|
-
const outcome = await runningClosed;
|
|
570
|
-
clearTimeout(timeoutTimer);
|
|
571
|
-
clearInterval(abortTimer);
|
|
572
|
-
await cleanupQualificationProcessGroupAfterLeaderExit(runningChild.pid, childIdentity);
|
|
573
|
-
// An operator can write the durable abort request and signal the process before
|
|
574
|
-
// the supervisor's 20ms watcher observes it. The request, not which callback won
|
|
575
|
-
// that race, is authoritative for terminal classification.
|
|
576
|
-
if (termination === "exit" && existsSync(join(paths.invocation, "abort-request.json")))
|
|
577
|
-
termination = "abort";
|
|
578
|
-
stdoutCapture.close();
|
|
579
|
-
stderrCapture.close();
|
|
580
|
-
const terminationResult = termination;
|
|
581
|
-
const terminalStatus = terminationResult === "timeout" ? "timed-out" : terminationResult === "abort" ? "aborted" : undefined;
|
|
582
|
-
await finalizeAfterCapture({
|
|
583
|
-
invocation,
|
|
584
|
-
spoolDir: paths.root,
|
|
585
|
-
terminalStatus: childIdentityError ? "failed" : terminalStatus,
|
|
586
|
-
supervisor: supervisorIdentity,
|
|
587
|
-
child: childIdentity,
|
|
588
|
-
auth,
|
|
589
|
-
accountingEventSha,
|
|
590
|
-
continuationSha,
|
|
591
|
-
startedAt,
|
|
592
|
-
deadlineAt,
|
|
593
|
-
exitCode: outcome.code,
|
|
594
|
-
signal: outcome.signal,
|
|
595
|
-
stdoutCapture,
|
|
596
|
-
stderrCapture,
|
|
597
|
-
now: now(),
|
|
598
|
-
error: outcome.error ?? childIdentityError,
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
function assertQualificationPanelMemberLaunchAuthorized(spoolDir, invocation) {
|
|
602
|
-
if (!invocation.panel || invocation.panel.member_ordinal !== 3)
|
|
603
|
-
return;
|
|
604
|
-
const entries = readdirSync(qualificationSpoolPaths(spoolDir).invocations, { withFileTypes: true });
|
|
605
|
-
const prior = entries
|
|
606
|
-
.filter((entry) => entry.isDirectory())
|
|
607
|
-
.map((entry) => readQualificationInvocation(spoolDir, entry.name))
|
|
608
|
-
.filter((candidate) => candidate.panel?.id === invocation.panel.id && candidate.panel.member_ordinal <= 2)
|
|
609
|
-
.sort((left, right) => left.panel.member_ordinal - right.panel.member_ordinal);
|
|
610
|
-
if (prior.length !== 2 || prior[0].panel?.member_ordinal !== 1 || prior[1].panel?.member_ordinal !== 2) {
|
|
611
|
-
throw new Error(`qualification panel ${invocation.panel.id} tie-break requires terminal members 1 and 2`);
|
|
612
|
-
}
|
|
613
|
-
const votes = prior.map((candidate) => readQualificationJudgePanelMember(spoolDir, candidate.invocation_id));
|
|
614
|
-
if (!collapseVotePanel(votes).split)
|
|
615
|
-
throw new Error(`qualification panel ${invocation.panel.id} tie-break is unauthorized because the first two clean votes did not split`);
|
|
616
|
-
}
|
|
617
|
-
export function readValidatedQualificationTerminalReceipt(spoolDir, invocationId) {
|
|
618
|
-
const invocation = readQualificationInvocation(spoolDir, invocationId);
|
|
619
|
-
const lifecycle = readQualificationLifecycle(spoolDir, invocationId);
|
|
620
|
-
if (lifecycle.phase !== "terminal")
|
|
621
|
-
throw new Error(`qualification invocation ${invocationId} is not terminal`);
|
|
622
|
-
const receipt = readTerminalReceipt(spoolDir, invocation);
|
|
623
|
-
assertTerminalReceiptCrossBindings(spoolDir, invocation, receipt);
|
|
624
|
-
if (receipt.terminal_status !== lifecycle.terminal_status)
|
|
625
|
-
throw new Error(`qualification terminal receipt for ${invocationId} contradicts lifecycle`);
|
|
626
|
-
return receipt;
|
|
627
|
-
}
|
|
628
|
-
/** Convert one fully validated judge invocation into the shared panel vote shape. */
|
|
629
|
-
export function readQualificationJudgePanelMember(spoolDir, invocationId) {
|
|
630
|
-
const invocation = readQualificationInvocation(spoolDir, invocationId);
|
|
631
|
-
const config = readQualificationSpoolConfig(spoolDir);
|
|
632
|
-
if (!config.judge_panel || invocation.role !== "judge" || !invocation.panel)
|
|
633
|
-
throw new Error(`qualification invocation ${invocationId} is not a configured judge-panel member`);
|
|
634
|
-
const receipt = readValidatedQualificationTerminalReceipt(spoolDir, invocationId);
|
|
635
|
-
if (receipt.attempt !== 1 || receipt.accounting_event_sha256 === null || receipt.authentication === null) {
|
|
636
|
-
throw new Error(`qualification judge-panel member ${invocationId} has no consumed OAuth-authenticated launch`);
|
|
637
|
-
}
|
|
638
|
-
const subject = readQualificationInvocation(spoolDir, invocation.panel.subject_invocation_id);
|
|
639
|
-
const subjectReceipt = readValidatedQualificationTerminalReceipt(spoolDir, subject.invocation_id);
|
|
640
|
-
if (subject.role !== "subject" || subjectReceipt.terminal_status !== "completed" || !subjectReceipt.artifact ||
|
|
641
|
-
subjectReceipt.artifact.sha256 !== invocation.panel.subject_artifact_sha256 ||
|
|
642
|
-
subject.measurement_identity_sha256 !== invocation.measurement_identity_sha256 ||
|
|
643
|
-
subject.scenario.id !== invocation.scenario.id || subject.scenario.version !== invocation.scenario.version ||
|
|
644
|
-
subject.scenario.stimulus_sha256 !== invocation.scenario.stimulus_sha256 || subject.scenario.rubric_sha256 !== invocation.scenario.rubric_sha256 ||
|
|
645
|
-
subject.repetition !== invocation.repetition || subject.arms.selected !== invocation.arms.subject || subject.arms.judge !== invocation.arms.judge) {
|
|
646
|
-
throw new Error(`qualification judge-panel member ${invocationId} subject artifact binding mismatch`);
|
|
647
|
-
}
|
|
648
|
-
const common = {
|
|
649
|
-
invocation_id: invocationId,
|
|
650
|
-
ordinal: invocation.panel.member_ordinal,
|
|
651
|
-
judge: { ...receipt.requested },
|
|
652
|
-
terminal_receipt_sha256: qualificationSha256(`${qualificationCanonicalJson(receipt)}\n`),
|
|
653
|
-
};
|
|
654
|
-
if (receipt.terminal_status !== "completed" || !receipt.artifact) {
|
|
655
|
-
return {
|
|
656
|
-
...common,
|
|
657
|
-
verdict: "ERROR",
|
|
658
|
-
reason: `judge invocation ${receipt.terminal_status}${receipt.error ? `: ${receipt.error}` : ""}`,
|
|
659
|
-
suspect: false,
|
|
660
|
-
artifact: null,
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
const artifactBytes = readFileSync(join(qualificationSpoolPaths(spoolDir).root, receipt.artifact.path));
|
|
664
|
-
const raw = qualificationAssistantText(artifactBytes.toString("utf8"));
|
|
665
|
-
const parsed = parseVerdict(raw);
|
|
666
|
-
return {
|
|
667
|
-
...common,
|
|
668
|
-
verdict: parsed.verdict,
|
|
669
|
-
reason: parsed.reason,
|
|
670
|
-
suspect: detectMisfire(raw, parsed.verdict),
|
|
671
|
-
artifact: { sha256: receipt.artifact.sha256, bytes: receipt.artifact.bytes },
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
function qualificationAssistantText(jsonl) {
|
|
675
|
-
const texts = [];
|
|
676
|
-
for (const line of jsonl.split("\n")) {
|
|
677
|
-
if (!line.trim())
|
|
678
|
-
continue;
|
|
679
|
-
const event = JSON.parse(line);
|
|
680
|
-
if (!plainObject(event) || event.type !== "message_end" || !plainObject(event.message) || event.message.role !== "assistant" || !Array.isArray(event.message.content))
|
|
681
|
-
continue;
|
|
682
|
-
for (const block of event.message.content)
|
|
683
|
-
if (plainObject(block) && block.type === "text" && typeof block.text === "string")
|
|
684
|
-
texts.push(block.text);
|
|
685
|
-
}
|
|
686
|
-
return texts.at(-1) ?? "";
|
|
687
|
-
}
|
|
688
|
-
export function qualificationInvocationStatus(spoolDir, invocationId) {
|
|
689
|
-
const invocation = readQualificationInvocation(spoolDir, invocationId);
|
|
690
|
-
const paths = qualificationSpoolPaths(spoolDir, invocationId);
|
|
691
|
-
if (existsSync(join(paths.terminal, "receipt.json")))
|
|
692
|
-
reconcileTerminalLifecycle(paths.root, invocation);
|
|
693
|
-
const lifecycle = readQualificationLifecycle(spoolDir, invocationId);
|
|
694
|
-
const receipt = existsSync(join(paths.terminal, "receipt.json")) ? readTerminalReceipt(spoolDir, invocation) : null;
|
|
695
|
-
if (receipt && lifecycle.phase === "terminal" && lifecycle.terminal_status !== receipt.terminal_status) {
|
|
696
|
-
throw new Error(`qualification invocation ${invocationId} terminal receipt contradicts lifecycle state`);
|
|
697
|
-
}
|
|
698
|
-
const latest = lifecycle.events[lifecycle.events.length - 1]?.detail ?? {};
|
|
699
|
-
const supervisor = receipt?.supervisor ?? latestContinuationSupervisor(paths.invocation, invocation) ?? latestProcessIdentity(lifecycle, "supervisor");
|
|
700
|
-
const child = receipt?.child ?? processIdentityFrom(latest.child);
|
|
701
|
-
return {
|
|
702
|
-
invocation_id: invocationId,
|
|
703
|
-
phase: receipt ? "terminal" : lifecycle.phase,
|
|
704
|
-
terminal_status: receipt?.terminal_status ?? lifecycle.terminal_status,
|
|
705
|
-
attempt: (readQualificationAccounting(spoolDir).events.some((event) => event.invocation_id === invocationId) ? 1 : 0),
|
|
706
|
-
requested: invocation.requested,
|
|
707
|
-
actual: receipt?.actual ?? null,
|
|
708
|
-
deadline_at: receipt?.deadline_at ?? stringValue(latest.deadline_at) ?? null,
|
|
709
|
-
effective_timeout_ms: receipt?.effective_timeout_ms ?? null,
|
|
710
|
-
output: receipt ? { stdout: receipt.stdout, stderr: receipt.stderr } : null,
|
|
711
|
-
supervisor,
|
|
712
|
-
child,
|
|
713
|
-
supervisor_alive: supervisor ? qualificationProcessMatches(supervisor) : null,
|
|
714
|
-
child_alive: child ? qualificationProcessMatches(child) : null,
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
export async function pollQualificationInvocation(options) {
|
|
718
|
-
const waitMs = options.wait_ms ?? 0;
|
|
719
|
-
const interval = Math.max(5, options.interval_ms ?? 100);
|
|
720
|
-
const deadline = Date.now() + waitMs;
|
|
721
|
-
while (true) {
|
|
722
|
-
const status = qualificationInvocationStatus(options.spool_dir, options.invocation_id);
|
|
723
|
-
if (status.phase === "terminal" || Date.now() >= deadline)
|
|
724
|
-
return status;
|
|
725
|
-
if (status.supervisor && status.supervisor_alive === false) {
|
|
726
|
-
throw new Error(`qualification invocation ${options.invocation_id} has a stale supervisor identity; it will not be relaunched without explicit continuation authority`);
|
|
727
|
-
}
|
|
728
|
-
await sleep(Math.min(interval, Math.max(1, deadline - Date.now())));
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
export async function abortQualificationInvocation(options) {
|
|
732
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(options.reason))
|
|
733
|
-
throw new Error("qualification abort reason must be a bounded identifier");
|
|
734
|
-
const paths = qualificationSpoolPaths(options.spool_dir, options.invocation_id);
|
|
735
|
-
const requestedAt = timestamp((options.now ?? (() => new Date().toISOString()))(), "qualification abort time");
|
|
736
|
-
let childToTerminate = null;
|
|
737
|
-
await withAsyncLock(paths.lock, async () => {
|
|
738
|
-
await withAsyncLock(paths.invocationLock, async () => {
|
|
739
|
-
const invocation = readQualificationInvocation(paths.root, options.invocation_id);
|
|
740
|
-
const lifecycle = readQualificationLifecycle(paths.root, options.invocation_id);
|
|
741
|
-
if (lifecycle.phase === "terminal" || existsSync(paths.terminal)) {
|
|
742
|
-
if (existsSync(paths.terminal))
|
|
743
|
-
reconcileTerminalLifecycle(paths.root, invocation);
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
const request = { schema_version: "qualification-abort-request-v1", invocation_id: invocation.invocation_id, requested_at: requestedAt, reason: options.reason };
|
|
747
|
-
const abortPath = join(paths.invocation, "abort-request.json");
|
|
748
|
-
if (!existsSync(abortPath))
|
|
749
|
-
atomicWriteCanonical(abortPath, request, true);
|
|
750
|
-
const accountingEvent = readQualificationAccounting(paths.root).events.find((event) => event.invocation_id === invocation.invocation_id);
|
|
751
|
-
const authPath = join(paths.invocation, "auth-evidence.json");
|
|
752
|
-
const auth = existsSync(authPath) ? readAuthEvidence(paths.root, invocation, false) : null;
|
|
753
|
-
if (lifecycle.phase === "prepared") {
|
|
754
|
-
await finalizeWithoutChild({
|
|
755
|
-
invocation,
|
|
756
|
-
spoolDir: paths.root,
|
|
757
|
-
status: "aborted",
|
|
758
|
-
supervisor: qualificationProcessIdentity(process.pid),
|
|
759
|
-
auth,
|
|
760
|
-
now: requestedAt,
|
|
761
|
-
error: options.reason,
|
|
762
|
-
accountingEventSha: accountingEvent?.event_sha256,
|
|
763
|
-
});
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
if (!accountingEvent)
|
|
767
|
-
throw new Error(`qualification nonterminal invocation ${invocation.invocation_id} has no accounting claim`);
|
|
768
|
-
const priorSupervisor = latestContinuationSupervisor(paths.invocation, invocation) ?? latestProcessIdentity(lifecycle, "supervisor");
|
|
769
|
-
const child = readChildOccurrence(paths.invocation, invocation, false, accountingEvent.event_sha256)?.child ?? latestProcessIdentity(lifecycle, "child");
|
|
770
|
-
if (priorSupervisor && qualificationProcessMatches(priorSupervisor)) {
|
|
771
|
-
if (child) {
|
|
772
|
-
childToTerminate = child;
|
|
773
|
-
return;
|
|
774
|
-
}
|
|
775
|
-
if (priorSupervisor.pid === process.pid)
|
|
776
|
-
return;
|
|
777
|
-
await terminateInterruptedQualificationProcessGroup(priorSupervisor);
|
|
778
|
-
if (qualificationProcessMatches(priorSupervisor))
|
|
779
|
-
throw new Error(`qualification live supervisor ${priorSupervisor.pid} could not be terminated for abort reconciliation`);
|
|
780
|
-
const continuationRecord = readContinuationRecord(paths.invocation);
|
|
781
|
-
await reconcileInterruptedAttempt({
|
|
782
|
-
invocation,
|
|
783
|
-
spoolDir: paths.root,
|
|
784
|
-
supervisor: qualificationProcessIdentity(process.pid),
|
|
785
|
-
auth,
|
|
786
|
-
accountingEventSha: accountingEvent.event_sha256,
|
|
787
|
-
continuationSha: continuationRecord ? String(continuationRecord.authority_sha256) : null,
|
|
788
|
-
now: requestedAt,
|
|
789
|
-
status: "aborted",
|
|
790
|
-
error: options.reason,
|
|
791
|
-
child: null,
|
|
792
|
-
});
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
const continuationRecord = readContinuationRecord(paths.invocation);
|
|
796
|
-
await reconcileInterruptedAttempt({
|
|
797
|
-
invocation,
|
|
798
|
-
spoolDir: paths.root,
|
|
799
|
-
supervisor: qualificationProcessIdentity(process.pid),
|
|
800
|
-
auth,
|
|
801
|
-
accountingEventSha: accountingEvent.event_sha256,
|
|
802
|
-
continuationSha: continuationRecord ? String(continuationRecord.authority_sha256) : null,
|
|
803
|
-
now: requestedAt,
|
|
804
|
-
status: "aborted",
|
|
805
|
-
error: options.reason,
|
|
806
|
-
child,
|
|
807
|
-
});
|
|
808
|
-
});
|
|
809
|
-
});
|
|
810
|
-
if (childToTerminate && qualificationProcessMatches(childToTerminate))
|
|
811
|
-
terminateQualificationByIdentity(childToTerminate);
|
|
812
|
-
return qualificationInvocationStatus(paths.root, options.invocation_id);
|
|
813
|
-
}
|
|
814
|
-
export function validateQualificationBaseSpool(spoolDir) {
|
|
815
|
-
const report = validateQualificationSpool(spoolDir);
|
|
816
|
-
const paths = qualificationSpoolPaths(spoolDir);
|
|
817
|
-
const invocationIds = readdirSync(paths.invocations, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
818
|
-
const accountingLedger = readQualificationAccounting(paths.root);
|
|
819
|
-
const expectedArtifacts = new Set();
|
|
820
|
-
let terminal = 0;
|
|
821
|
-
for (const id of invocationIds) {
|
|
822
|
-
const invocation = readQualificationInvocation(paths.root, id);
|
|
823
|
-
assertInputUnchanged(invocation);
|
|
824
|
-
for (const resource of invocation.execution.resources)
|
|
825
|
-
verifyQualificationResource(resource);
|
|
826
|
-
const invocationPaths = qualificationSpoolPaths(paths.root, id);
|
|
827
|
-
if (existsSync(join(invocationPaths.terminal, "receipt.json")))
|
|
828
|
-
reconcileTerminalLifecycle(paths.root, invocation);
|
|
829
|
-
const lifecycle = readQualificationLifecycle(paths.root, id);
|
|
830
|
-
const accountingEvent = accountingLedger.events.find((event) => event.invocation_id === id);
|
|
831
|
-
const launchAttemptPath = join(invocationPaths.invocation, "launch-attempt.json");
|
|
832
|
-
const hasLaunchAttempt = existsSync(launchAttemptPath);
|
|
833
|
-
if (hasLaunchAttempt) {
|
|
834
|
-
const attempt = readCanonicalJson(launchAttemptPath, `qualification launch attempt ${id}`);
|
|
835
|
-
if (!accountingEvent)
|
|
836
|
-
throw new Error(`qualification launch attempt ${id} has no accounting claim`);
|
|
837
|
-
validateLaunchAttempt(attempt, invocation, paths.root, accountingEvent.event_sha256);
|
|
838
|
-
}
|
|
839
|
-
if ((lifecycle.phase === "launch-claimed" || lifecycle.phase === "running") && !accountingEvent) {
|
|
840
|
-
throw new Error(`qualification lifecycle ${id} advanced without an accounting claim`);
|
|
841
|
-
}
|
|
842
|
-
if (isInputBoundQualificationInvocation(invocation)) {
|
|
843
|
-
const inputClaimPath = join(invocationPaths.invocation, "input-launch-claim.json");
|
|
844
|
-
if (accountingEvent)
|
|
845
|
-
readInputLaunchClaim(invocationPaths.invocation, invocation, accountingEvent.event_sha256);
|
|
846
|
-
else if (existsSync(inputClaimPath))
|
|
847
|
-
throw new Error(`qualification invocation ${id} has an input launch claim without accounting`);
|
|
848
|
-
}
|
|
849
|
-
const continuationRecord = readContinuationRecord(invocationPaths.invocation);
|
|
850
|
-
if (continuationRecord)
|
|
851
|
-
assertContinuationRecordBound(continuationRecord, invocation);
|
|
852
|
-
const childOccurrence = readChildOccurrence(invocationPaths.invocation, invocation, false, accountingEvent?.event_sha256);
|
|
853
|
-
if (childOccurrence && (!hasLaunchAttempt || !accountingEvent))
|
|
854
|
-
throw new Error(`qualification child occurrence ${id} has no launch/accounting claim`);
|
|
855
|
-
if (invocation.oauth_directory_policy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
856
|
-
const claimPath = oauthCheckpointPath(invocationPaths.invocation, "before-launch-claim");
|
|
857
|
-
const prelaunchPath = oauthCheckpointPath(invocationPaths.invocation, "immediately-before-pi-launch");
|
|
858
|
-
if (accountingEvent && !existsSync(claimPath) && !existsSync(join(invocationPaths.terminal, "receipt.json"))) {
|
|
859
|
-
throw new Error(`qualification invocation ${id} launch-claim OAuth checkpoint is missing`);
|
|
860
|
-
}
|
|
861
|
-
if (existsSync(claimPath))
|
|
862
|
-
readOAuthDirectoryCheckpointV2(invocationPaths.invocation, invocation, "before-launch-claim");
|
|
863
|
-
if (existsSync(prelaunchPath))
|
|
864
|
-
readOAuthDirectoryCheckpointV2(invocationPaths.invocation, invocation, "immediately-before-pi-launch");
|
|
865
|
-
if (childOccurrence && !existsSync(prelaunchPath) && !existsSync(join(invocationPaths.terminal, "receipt.json"))) {
|
|
866
|
-
throw new Error(`qualification invocation ${id} child occurrence lacks its prelaunch OAuth checkpoint`);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
if (lifecycle.phase === "running" && (!childOccurrence || qualificationCanonicalJson(latestProcessIdentity(lifecycle, "child")) !== qualificationCanonicalJson(childOccurrence.child))) {
|
|
870
|
-
throw new Error(`qualification running lifecycle ${id} is not bound to its child occurrence`);
|
|
871
|
-
}
|
|
872
|
-
const receiptPath = join(invocationPaths.terminal, "receipt.json");
|
|
873
|
-
if (existsSync(receiptPath)) {
|
|
874
|
-
terminal += 1;
|
|
875
|
-
const receipt = readTerminalReceipt(paths.root, invocation);
|
|
876
|
-
if (invocation.oauth_directory_policy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2 && accountingEvent &&
|
|
877
|
-
!existsSync(oauthCheckpointPath(invocationPaths.invocation, "before-launch-claim"))) {
|
|
878
|
-
const handledMissingClaim = receipt.schema_version !== QUALIFICATION_TERMINAL_RECEIPT_VERSION &&
|
|
879
|
-
receipt.oauth_directory_validations.before_launch_claim === null && receipt.artifact === null &&
|
|
880
|
-
receipt.terminal_status !== "completed" && receipt.terminal_status !== "refused";
|
|
881
|
-
if (!handledMissingClaim)
|
|
882
|
-
throw new Error(`qualification terminal receipt for ${id} did not fail closed over its missing launch-claim OAuth checkpoint`);
|
|
883
|
-
}
|
|
884
|
-
if (invocation.oauth_directory_policy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2 && childOccurrence &&
|
|
885
|
-
!existsSync(oauthCheckpointPath(invocationPaths.invocation, "immediately-before-pi-launch"))) {
|
|
886
|
-
const handledMissingCheckpoint = receipt.schema_version !== QUALIFICATION_TERMINAL_RECEIPT_VERSION &&
|
|
887
|
-
receipt.oauth_directory_validations.immediately_before_pi_launch === null && receipt.artifact === null &&
|
|
888
|
-
receipt.terminal_status !== "completed" && receipt.terminal_status !== "refused";
|
|
889
|
-
if (!handledMissingCheckpoint)
|
|
890
|
-
throw new Error(`qualification terminal receipt for ${id} did not fail closed over its missing prelaunch OAuth checkpoint`);
|
|
891
|
-
}
|
|
892
|
-
if (lifecycle.phase !== "terminal" || lifecycle.terminal_status !== receipt.terminal_status)
|
|
893
|
-
throw new Error(`qualification terminal receipt for ${id} contradicts lifecycle`);
|
|
894
|
-
const receiptDigest = qualificationSha256(`${qualificationCanonicalJson(receipt)}\n`);
|
|
895
|
-
if (lifecycle.events.at(-1)?.detail.receipt_sha256 !== receiptDigest)
|
|
896
|
-
throw new Error(`qualification terminal receipt for ${id} is not bound by the lifecycle chain`);
|
|
897
|
-
const expectedAttempt = accountingEvent ? 1 : 0;
|
|
898
|
-
if (receipt.attempt !== expectedAttempt)
|
|
899
|
-
throw new Error(`qualification terminal receipt for ${id} attempt contradicts accounting`);
|
|
900
|
-
if (receipt.accounting_event_sha256 !== (accountingEvent?.event_sha256 ?? null))
|
|
901
|
-
throw new Error(`qualification terminal receipt for ${id} accounting digest mismatch`);
|
|
902
|
-
if (receipt.started_at !== null && !hasLaunchAttempt)
|
|
903
|
-
throw new Error(`qualification started terminal receipt for ${id} has no immutable launch attempt`);
|
|
904
|
-
if (receipt.attempt === 0 && hasLaunchAttempt)
|
|
905
|
-
throw new Error(`qualification attempt-zero receipt for ${id} has a launch-attempt record`);
|
|
906
|
-
if (childOccurrence && (qualificationCanonicalJson(receipt.child) !== qualificationCanonicalJson(childOccurrence.child) || receipt.started_at !== childOccurrence.started_at || receipt.deadline_at !== childOccurrence.deadline_at)) {
|
|
907
|
-
throw new Error(`qualification terminal receipt for ${id} contradicts child occurrence evidence`);
|
|
908
|
-
}
|
|
909
|
-
const expectedContinuationSha = continuationRecord ? String(continuationRecord.authority_sha256) : null;
|
|
910
|
-
if (receipt.continuation_authority_sha256 !== expectedContinuationSha)
|
|
911
|
-
throw new Error(`qualification terminal receipt for ${id} continuation authority mismatch`);
|
|
912
|
-
if (receipt.authentication) {
|
|
913
|
-
const authEvidence = readAuthEvidence(paths.root, invocation, false);
|
|
914
|
-
const authDigest = qualificationSha256(`${qualificationCanonicalJson(authEvidence)}\n`);
|
|
915
|
-
if (receipt.authentication.evidence_sha256 !== authDigest)
|
|
916
|
-
throw new Error(`qualification terminal receipt for ${id} authentication evidence mismatch`);
|
|
917
|
-
}
|
|
918
|
-
else if (receipt.attempt !== 0 || receipt.terminal_status !== "aborted") {
|
|
919
|
-
throw new Error(`qualification terminal receipt for ${id} is missing authentication evidence`);
|
|
920
|
-
}
|
|
921
|
-
verifyOutputReceipt(paths.root, receipt.stdout);
|
|
922
|
-
verifyOutputReceipt(paths.root, receipt.stderr);
|
|
923
|
-
if (receipt.artifact) {
|
|
924
|
-
expectedArtifacts.add(receipt.artifact.path.split("/").at(-1));
|
|
925
|
-
const artifactPath = join(paths.root, receipt.artifact.path);
|
|
926
|
-
assertRegularFile(artifactPath, `qualification artifact ${id}`);
|
|
927
|
-
const bytes = readFileSync(artifactPath);
|
|
928
|
-
if (bytes.length !== receipt.artifact.bytes || qualificationSha256(bytes) !== receipt.artifact.sha256)
|
|
929
|
-
throw new Error(`qualification artifact ${id} digest/size mismatch`);
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
else if (lifecycle.phase === "terminal") {
|
|
933
|
-
throw new Error(`qualification lifecycle ${id} is terminal without an atomic receipt`);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
if (existsSync(paths.artifacts)) {
|
|
937
|
-
for (const entry of readdirSync(paths.artifacts, { withFileTypes: true })) {
|
|
938
|
-
if (!entry.isFile() || !expectedArtifacts.has(entry.name))
|
|
939
|
-
throw new Error(`qualification artifacts contain undeclared entry ${entry.name}`);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
return { ...report, terminal };
|
|
943
|
-
}
|
|
944
|
-
const QUALIFICATION_OAUTH_DIRECTORY_CHECKPOINT_VERSION = "qualification-oauth-directory-checkpoint-v2";
|
|
945
|
-
function oauthCheckpointPath(invocationDir, point) {
|
|
946
|
-
return join(invocationDir, point === "before-launch-claim" ? "oauth-directory-launch-claim.json" : "oauth-directory-prelaunch.json");
|
|
947
|
-
}
|
|
948
|
-
function writeOAuthDirectoryCheckpointV2(invocationDir, invocation, point, inventory, exclusive) {
|
|
949
|
-
if (invocation.oauth_directory_policy !== QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2)
|
|
950
|
-
throw new Error("qualification OAuth directory checkpoint requires a v2 invocation");
|
|
951
|
-
validateQualificationOAuthDirectoryInventoryV2(inventory, point);
|
|
952
|
-
if (!inventory.valid)
|
|
953
|
-
throw new Error(`qualification OAuth directory ${point} checkpoint cannot bind an invalid inventory`);
|
|
954
|
-
const base = {
|
|
955
|
-
schema_version: QUALIFICATION_OAUTH_DIRECTORY_CHECKPOINT_VERSION,
|
|
956
|
-
invocation_id: invocation.invocation_id,
|
|
957
|
-
oauth_directory_policy: QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2,
|
|
958
|
-
validation_point: point,
|
|
959
|
-
inventory,
|
|
960
|
-
};
|
|
961
|
-
atomicWriteCanonical(oauthCheckpointPath(invocationDir, point), { ...base, checkpoint_sha256: qualificationSha256(qualificationCanonicalJson(base)) }, exclusive);
|
|
962
|
-
}
|
|
963
|
-
function readOAuthDirectoryCheckpointV2(invocationDir, invocation, point) {
|
|
964
|
-
const value = readCanonicalJson(oauthCheckpointPath(invocationDir, point), `qualification OAuth directory ${point} checkpoint ${invocation.invocation_id}`);
|
|
965
|
-
if (!plainObject(value))
|
|
966
|
-
throw new Error(`qualification OAuth directory ${point} checkpoint must be an object`);
|
|
967
|
-
exactKeys(value, ["schema_version", "invocation_id", "oauth_directory_policy", "validation_point", "inventory", "checkpoint_sha256"], `qualification OAuth directory ${point} checkpoint`);
|
|
968
|
-
const { checkpoint_sha256: recorded, ...base } = value;
|
|
969
|
-
if (value.schema_version !== QUALIFICATION_OAUTH_DIRECTORY_CHECKPOINT_VERSION || value.invocation_id !== invocation.invocation_id ||
|
|
970
|
-
invocation.oauth_directory_policy !== QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2 || value.oauth_directory_policy !== QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2 ||
|
|
971
|
-
value.validation_point !== point || typeof recorded !== "string" || qualificationSha256(qualificationCanonicalJson(base)) !== recorded) {
|
|
972
|
-
throw new Error(`qualification OAuth directory ${point} checkpoint identity/digest mismatch`);
|
|
973
|
-
}
|
|
974
|
-
validateQualificationOAuthDirectoryInventoryV2(value.inventory, point);
|
|
975
|
-
if (!value.inventory.valid)
|
|
976
|
-
throw new Error(`qualification OAuth directory ${point} checkpoint is invalid`);
|
|
977
|
-
return value.inventory;
|
|
978
|
-
}
|
|
979
|
-
function isInputBoundQualificationInvocation(invocation) {
|
|
980
|
-
return invocation.schema_version === QUALIFICATION_INVOCATION_VERSION_V3 || invocation.schema_version === QUALIFICATION_INVOCATION_VERSION_V4;
|
|
981
|
-
}
|
|
982
|
-
function buildQualificationArgv(invocation, artifactPath) {
|
|
983
|
-
const replacements = {
|
|
984
|
-
input_path: invocation.scenario.input_path,
|
|
985
|
-
scenario_id: invocation.scenario.id,
|
|
986
|
-
invocation_id: invocation.invocation_id,
|
|
987
|
-
artifact_path: artifactPath,
|
|
988
|
-
};
|
|
989
|
-
const configured = invocation.execution.arguments.map((argument) => argument.replace(/\{([a-z_]+)\}/g, (_match, name) => replacements[name] ?? `{${name}}`));
|
|
990
|
-
const resources = invocation.execution.resources.flatMap((resource) => resource.kind === "extension"
|
|
991
|
-
? ["--extension", resource.path]
|
|
992
|
-
: resource.kind === "skill"
|
|
993
|
-
? ["--skill", resource.path]
|
|
994
|
-
: ["--append-system-prompt", resource.path]);
|
|
995
|
-
return [
|
|
996
|
-
"--provider", invocation.requested.provider,
|
|
997
|
-
"--model", invocation.requested.model,
|
|
998
|
-
"--mode", "json",
|
|
999
|
-
"--print",
|
|
1000
|
-
"--no-session",
|
|
1001
|
-
"--no-context-files",
|
|
1002
|
-
"--no-extensions",
|
|
1003
|
-
"--no-skills",
|
|
1004
|
-
...resources,
|
|
1005
|
-
...configured,
|
|
1006
|
-
];
|
|
1007
|
-
}
|
|
1008
|
-
function writeInputLaunchClaim(invocationDir, invocation, accountingSha, input, supervisor, claimedAt) {
|
|
1009
|
-
if (!isInputBoundQualificationInvocation(invocation) || !invocation.invocation_input_occurrence)
|
|
1010
|
-
throw new Error("qualification input launch claim requires an input-bound invocation");
|
|
1011
|
-
atomicWriteCanonical(join(invocationDir, "input-launch-claim.json"), {
|
|
1012
|
-
schema_version: QUALIFICATION_INPUT_LAUNCH_CLAIM_VERSION,
|
|
1013
|
-
invocation_id: invocation.invocation_id,
|
|
1014
|
-
attempt: 1,
|
|
1015
|
-
accounting_event_sha256: accountingSha,
|
|
1016
|
-
claimed_at: claimedAt,
|
|
1017
|
-
supervisor,
|
|
1018
|
-
invocation_input: input,
|
|
1019
|
-
invocation_input_occurrence: invocation.invocation_input_occurrence,
|
|
1020
|
-
}, true);
|
|
1021
|
-
}
|
|
1022
|
-
function readInputLaunchClaim(invocationDir, invocation, accountingSha) {
|
|
1023
|
-
const value = readCanonicalJson(join(invocationDir, "input-launch-claim.json"), `qualification input launch claim ${invocation.invocation_id}`);
|
|
1024
|
-
if (!plainObject(value))
|
|
1025
|
-
throw new Error("qualification input launch claim must be an object");
|
|
1026
|
-
exactKeys(value, ["schema_version", "invocation_id", "attempt", "accounting_event_sha256", "claimed_at", "supervisor", "invocation_input", "invocation_input_occurrence"], "qualification input launch claim");
|
|
1027
|
-
if (value.schema_version !== QUALIFICATION_INPUT_LAUNCH_CLAIM_VERSION || value.invocation_id !== invocation.invocation_id || value.attempt !== 1 || value.accounting_event_sha256 !== accountingSha || !processIdentityFrom(value.supervisor))
|
|
1028
|
-
throw new Error("qualification input launch claim identity mismatch");
|
|
1029
|
-
timestamp(String(value.claimed_at), "qualification input launch claim time");
|
|
1030
|
-
if (qualificationCanonicalJson(value.invocation_input) !== qualificationCanonicalJson(invocation.invocation_input) || qualificationCanonicalJson(value.invocation_input_occurrence) !== qualificationCanonicalJson(invocation.invocation_input_occurrence))
|
|
1031
|
-
throw new Error("qualification input launch claim binding mismatch");
|
|
1032
|
-
return invocation.invocation_input;
|
|
1033
|
-
}
|
|
1034
|
-
function readChildOccurrence(invocationDir, invocation, required, expectedAccountingSha) {
|
|
1035
|
-
const path = join(invocationDir, "child-occurrence.json");
|
|
1036
|
-
if (!existsSync(path)) {
|
|
1037
|
-
if (required)
|
|
1038
|
-
throw new Error(`qualification child occurrence for ${invocation.invocation_id} is missing`);
|
|
1039
|
-
return null;
|
|
1040
|
-
}
|
|
1041
|
-
const value = readCanonicalJson(path, `qualification child occurrence ${invocation.invocation_id}`);
|
|
1042
|
-
if (!plainObject(value))
|
|
1043
|
-
throw new Error(`qualification child occurrence ${invocation.invocation_id} must be an object`);
|
|
1044
|
-
const v3 = isInputBoundQualificationInvocation(invocation);
|
|
1045
|
-
exactKeys(value, v3 ? ["schema_version", "invocation_id", "attempt", "started_at", "deadline_at", "child", "invocation_input", "accounting_event_sha256"] : ["schema_version", "invocation_id", "attempt", "started_at", "deadline_at", "child"], `qualification child occurrence ${invocation.invocation_id}`);
|
|
1046
|
-
const child = processIdentityFrom(value.child);
|
|
1047
|
-
if (value.schema_version !== (v3 ? "qualification-child-occurrence-v2" : "qualification-child-occurrence-v1") || value.invocation_id !== invocation.invocation_id || value.attempt !== 1 || !child) {
|
|
1048
|
-
throw new Error(`qualification child occurrence ${invocation.invocation_id} identity/version mismatch`);
|
|
1049
|
-
}
|
|
1050
|
-
if (v3 && (qualificationCanonicalJson(value.invocation_input) !== qualificationCanonicalJson(invocation.invocation_input) || typeof value.accounting_event_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.accounting_event_sha256) || value.accounting_event_sha256 !== expectedAccountingSha))
|
|
1051
|
-
throw new Error(`qualification child occurrence ${invocation.invocation_id} input binding mismatch`);
|
|
1052
|
-
const startedAt = timestamp(String(value.started_at), `qualification child occurrence ${invocation.invocation_id} start time`);
|
|
1053
|
-
const deadlineAt = timestamp(String(value.deadline_at), `qualification child occurrence ${invocation.invocation_id} deadline`);
|
|
1054
|
-
if (Date.parse(deadlineAt) < Date.parse(startedAt))
|
|
1055
|
-
throw new Error(`qualification child occurrence ${invocation.invocation_id} deadline precedes start`);
|
|
1056
|
-
return { schema_version: v3 ? "qualification-child-occurrence-v2" : "qualification-child-occurrence-v1", invocation_id: invocation.invocation_id, attempt: 1, started_at: startedAt, deadline_at: deadlineAt, child, ...(v3 ? { invocation_input: invocation.invocation_input, accounting_event_sha256: String(value.accounting_event_sha256) } : {}) };
|
|
1057
|
-
}
|
|
1058
|
-
function latestProcessIdentity(lifecycle, field) {
|
|
1059
|
-
for (let index = lifecycle.events.length - 1; index >= 0; index -= 1) {
|
|
1060
|
-
const identity = processIdentityFrom(lifecycle.events[index].detail[field]);
|
|
1061
|
-
if (identity)
|
|
1062
|
-
return identity;
|
|
1063
|
-
}
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
function readContinuationRecord(invocationDir) {
|
|
1067
|
-
const directory = join(invocationDir, "continuations");
|
|
1068
|
-
if (!existsSync(directory))
|
|
1069
|
-
return null;
|
|
1070
|
-
const stat = lstatSync(directory);
|
|
1071
|
-
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
1072
|
-
throw new Error("qualification continuation authority directory is invalid");
|
|
1073
|
-
const entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
1074
|
-
if (entries.length > 1)
|
|
1075
|
-
throw new Error("qualification continuation authority is one-time but multiple records exist");
|
|
1076
|
-
let previousSha = null;
|
|
1077
|
-
let latest = null;
|
|
1078
|
-
entries.forEach((entry, index) => {
|
|
1079
|
-
const expectedName = `${String(index + 1).padStart(6, "0")}.json`;
|
|
1080
|
-
if (!entry.isFile() || entry.name !== expectedName)
|
|
1081
|
-
throw new Error("qualification continuation authority sequence is non-contiguous");
|
|
1082
|
-
const value = readCanonicalJson(join(directory, entry.name), `qualification continuation authority ${index + 1}`);
|
|
1083
|
-
if (!plainObject(value))
|
|
1084
|
-
throw new Error("qualification continuation authority must be an object");
|
|
1085
|
-
exactKeys(value, ["schema_version", "invocation_id", "seq", "authorized_at", "authority_sha256", "previous_record_sha256", "prior_supervisor", "new_supervisor", "record_sha256"], "qualification continuation authority");
|
|
1086
|
-
const { record_sha256: digest, ...base } = value;
|
|
1087
|
-
if (value.schema_version !== "qualification-continuation-authority-v1" || value.seq !== index + 1 || value.previous_record_sha256 !== previousSha ||
|
|
1088
|
-
typeof digest !== "string" || qualificationSha256(qualificationCanonicalJson(base)) !== digest ||
|
|
1089
|
-
!/^[a-f0-9]{64}$/.test(String(value.authority_sha256)) || !processIdentityFrom(value.new_supervisor)) {
|
|
1090
|
-
throw new Error("qualification continuation authority is corrupt");
|
|
1091
|
-
}
|
|
1092
|
-
timestamp(String(value.authorized_at), "qualification continuation authority time");
|
|
1093
|
-
previousSha = digest;
|
|
1094
|
-
latest = value;
|
|
1095
|
-
});
|
|
1096
|
-
return latest;
|
|
1097
|
-
}
|
|
1098
|
-
function assertContinuationRecordBound(record, invocation) {
|
|
1099
|
-
if (record.invocation_id !== invocation.invocation_id || record.authority_sha256 !== invocation.continuation_authority_sha256 ||
|
|
1100
|
-
Date.parse(String(record.authorized_at)) > Date.parse(invocation.continuation_authority_expires_at)) {
|
|
1101
|
-
throw new Error(`qualification continuation authority for ${invocation.invocation_id} is not bound to its immutable capability/expiry`);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
function latestContinuationSupervisor(invocationDir, invocation) {
|
|
1105
|
-
const record = readContinuationRecord(invocationDir);
|
|
1106
|
-
if (!record)
|
|
1107
|
-
return null;
|
|
1108
|
-
assertContinuationRecordBound(record, invocation);
|
|
1109
|
-
return processIdentityFrom(record.new_supervisor);
|
|
1110
|
-
}
|
|
1111
|
-
function recordContinuationAuthority(spoolDir, invocation, authority, priorSupervisor, newSupervisor, authorizedAt) {
|
|
1112
|
-
if (!authority.trim() || Buffer.byteLength(authority, "utf8") < 32 || Buffer.byteLength(authority, "utf8") > 4096)
|
|
1113
|
-
throw new Error("qualification continuation authority must be a 32..4096 byte one-time capability");
|
|
1114
|
-
const paths = qualificationSpoolPaths(spoolDir, invocation.invocation_id);
|
|
1115
|
-
const directory = join(paths.invocation, "continuations");
|
|
1116
|
-
const previous = readContinuationRecord(paths.invocation);
|
|
1117
|
-
if (previous)
|
|
1118
|
-
throw new Error("qualification continuation authority was already consumed");
|
|
1119
|
-
const authoritySha = qualificationSha256(authority);
|
|
1120
|
-
if (authoritySha !== invocation.continuation_authority_sha256)
|
|
1121
|
-
throw new Error("qualification continuation authority does not match the immutable invocation capability");
|
|
1122
|
-
if (Date.parse(authorizedAt) > Date.parse(invocation.continuation_authority_expires_at))
|
|
1123
|
-
throw new Error("qualification continuation authority is expired");
|
|
1124
|
-
const base = {
|
|
1125
|
-
schema_version: "qualification-continuation-authority-v1",
|
|
1126
|
-
invocation_id: invocation.invocation_id,
|
|
1127
|
-
seq: 1,
|
|
1128
|
-
authorized_at: timestamp(authorizedAt, "qualification continuation authority time"),
|
|
1129
|
-
authority_sha256: authoritySha,
|
|
1130
|
-
previous_record_sha256: null,
|
|
1131
|
-
prior_supervisor: priorSupervisor,
|
|
1132
|
-
new_supervisor: newSupervisor,
|
|
1133
|
-
};
|
|
1134
|
-
const nextPath = join(directory, `${String(base.seq).padStart(6, "0")}.json`);
|
|
1135
|
-
atomicWriteCanonical(nextPath, { ...base, record_sha256: qualificationSha256(qualificationCanonicalJson(base)) }, true);
|
|
1136
|
-
return authoritySha;
|
|
1137
|
-
}
|
|
1138
|
-
async function reconcileInterruptedAttempt(options) {
|
|
1139
|
-
if (!options.accountingEventSha)
|
|
1140
|
-
throw new Error(`qualification interrupted invocation ${options.invocation.invocation_id} has no accounting claim`);
|
|
1141
|
-
const paths = qualificationSpoolPaths(options.spoolDir, options.invocation.invocation_id);
|
|
1142
|
-
const occurrence = readChildOccurrence(paths.invocation, options.invocation, false, options.accountingEventSha);
|
|
1143
|
-
const child = options.child ?? occurrence?.child ?? latestProcessIdentity(readQualificationLifecycle(paths.root, options.invocation.invocation_id), "child");
|
|
1144
|
-
if (child) {
|
|
1145
|
-
await terminateInterruptedQualificationProcessGroup(child);
|
|
1146
|
-
if (qualificationProcessMatches(child))
|
|
1147
|
-
throw new Error(`qualification interrupted child ${child.pid} could not be terminated safely`);
|
|
1148
|
-
}
|
|
1149
|
-
await finalizeWithoutChild({
|
|
1150
|
-
invocation: options.invocation,
|
|
1151
|
-
spoolDir: paths.root,
|
|
1152
|
-
status: options.status,
|
|
1153
|
-
supervisor: options.supervisor,
|
|
1154
|
-
auth: options.auth,
|
|
1155
|
-
now: options.now,
|
|
1156
|
-
error: options.error,
|
|
1157
|
-
accountingEventSha: options.accountingEventSha,
|
|
1158
|
-
continuationSha: options.continuationSha,
|
|
1159
|
-
child,
|
|
1160
|
-
startedAt: occurrence?.started_at ?? null,
|
|
1161
|
-
deadlineAt: occurrence?.deadline_at ?? null,
|
|
1162
|
-
outputIncomplete: Boolean(occurrence) || existsSync(join(paths.invocation, "launch-attempt.json")),
|
|
1163
|
-
});
|
|
1164
|
-
}
|
|
1165
|
-
function collectOAuthDirectoryValidationsV2(invocation, spoolDir, auth, terminalTime, validateTerminal) {
|
|
1166
|
-
if (invocation.oauth_directory_policy !== QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2)
|
|
1167
|
-
return null;
|
|
1168
|
-
const validations = {
|
|
1169
|
-
before_oauth_readiness: null,
|
|
1170
|
-
after_oauth_readiness: null,
|
|
1171
|
-
before_launch_claim: null,
|
|
1172
|
-
immediately_before_pi_launch: null,
|
|
1173
|
-
after_child_termination: null,
|
|
1174
|
-
};
|
|
1175
|
-
const errors = [];
|
|
1176
|
-
if (auth?.schema_version === QUALIFICATION_AUTH_EVIDENCE_VERSION_V2 && auth.oauth_directory_policy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
1177
|
-
validations.before_oauth_readiness = auth.oauth_directory_validations.before_oauth_readiness;
|
|
1178
|
-
validations.after_oauth_readiness = auth.oauth_directory_validations.after_oauth_readiness;
|
|
1179
|
-
}
|
|
1180
|
-
else if (auth !== null) {
|
|
1181
|
-
errors.push("v2 authentication evidence is missing or historical");
|
|
1182
|
-
}
|
|
1183
|
-
const invocationDir = qualificationSpoolPaths(spoolDir, invocation.invocation_id).invocation;
|
|
1184
|
-
for (const point of ["before-launch-claim", "immediately-before-pi-launch"]) {
|
|
1185
|
-
const path = oauthCheckpointPath(invocationDir, point);
|
|
1186
|
-
if (!existsSync(path))
|
|
1187
|
-
continue;
|
|
1188
|
-
try {
|
|
1189
|
-
const inventory = readOAuthDirectoryCheckpointV2(invocationDir, invocation, point);
|
|
1190
|
-
if (point === "before-launch-claim")
|
|
1191
|
-
validations.before_launch_claim = inventory;
|
|
1192
|
-
else
|
|
1193
|
-
validations.immediately_before_pi_launch = inventory;
|
|
1194
|
-
}
|
|
1195
|
-
catch (error) {
|
|
1196
|
-
errors.push(safeError(error));
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
if (validateTerminal) {
|
|
1200
|
-
if (validations.before_launch_claim === null)
|
|
1201
|
-
errors.push("launch-claim OAuth directory checkpoint is missing after accounting");
|
|
1202
|
-
if (validations.immediately_before_pi_launch === null)
|
|
1203
|
-
errors.push("prelaunch OAuth directory checkpoint is missing after child start");
|
|
1204
|
-
const source = validations.immediately_before_pi_launch ?? validations.before_launch_claim ?? validations.after_oauth_readiness;
|
|
1205
|
-
if (!source) {
|
|
1206
|
-
errors.push("no earlier v2 OAuth directory occurrence exists for terminal continuity");
|
|
1207
|
-
}
|
|
1208
|
-
else {
|
|
1209
|
-
try {
|
|
1210
|
-
let terminal = inspectQualificationOAuthDirectoryPolicyV2({ PI_CODING_AGENT_DIR: source.directory.path }, { validation_point: "after-child-termination", now: () => terminalTime }).inventory;
|
|
1211
|
-
if (terminal.valid) {
|
|
1212
|
-
try {
|
|
1213
|
-
// Pi may create or atomically replace only models-store.json during
|
|
1214
|
-
// the authorized child. Auth and models occurrences remain bound.
|
|
1215
|
-
assertQualificationOAuthDirectoryContinuityV2(source, terminal, { allow_models_store_change: true });
|
|
1216
|
-
}
|
|
1217
|
-
catch (error) {
|
|
1218
|
-
terminal = invalidateQualificationOAuthDirectoryInventoryV2(terminal, safeError(error));
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
validations.after_child_termination = terminal;
|
|
1222
|
-
if (!terminal.valid)
|
|
1223
|
-
errors.push(...terminal.errors);
|
|
1224
|
-
}
|
|
1225
|
-
catch (error) {
|
|
1226
|
-
errors.push(`terminal OAuth directory occurrence could not be inspected: ${safeError(error)}`);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
return {
|
|
1231
|
-
validations,
|
|
1232
|
-
artifact_eligible: errors.length === 0,
|
|
1233
|
-
error: errors.length > 0 ? `qualification OAuth terminal inventory validation failed: ${errors[0]}` : null,
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
function terminalReceiptForPolicy(invocation, spoolDir, base, oauth) {
|
|
1237
|
-
if (isInputBoundQualificationInvocation(invocation)) {
|
|
1238
|
-
if (!oauth || !invocation.invocation_input)
|
|
1239
|
-
throw new Error("qualification terminal receipt v3 lacks OAuth/input evidence");
|
|
1240
|
-
const dir = qualificationSpoolPaths(spoolDir, invocation.invocation_id).invocation;
|
|
1241
|
-
const evidenceDigest = (name) => {
|
|
1242
|
-
const path = join(dir, name);
|
|
1243
|
-
return existsSync(path) ? qualificationSha256(readExactRegularBytes(path, `qualification ${name}`)) : null;
|
|
1244
|
-
};
|
|
1245
|
-
return {
|
|
1246
|
-
schema_version: QUALIFICATION_TERMINAL_RECEIPT_VERSION_V3,
|
|
1247
|
-
...base,
|
|
1248
|
-
oauth_directory_policy: QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2,
|
|
1249
|
-
oauth_directory_validations: oauth.validations,
|
|
1250
|
-
configuration_sha256: invocation.configuration_sha256,
|
|
1251
|
-
invocation_sha256: invocation.invocation_sha256,
|
|
1252
|
-
invocation_input: invocation.invocation_input,
|
|
1253
|
-
input_launch_claim_sha256: evidenceDigest("input-launch-claim.json"),
|
|
1254
|
-
launch_attempt_sha256: evidenceDigest("launch-attempt.json"),
|
|
1255
|
-
child_occurrence_sha256: evidenceDigest("child-occurrence.json"),
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1258
|
-
if (invocation.oauth_directory_policy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
1259
|
-
if (!oauth)
|
|
1260
|
-
throw new Error("qualification terminal receipt v2 lacks OAuth directory evidence");
|
|
1261
|
-
return {
|
|
1262
|
-
schema_version: QUALIFICATION_TERMINAL_RECEIPT_VERSION_V2,
|
|
1263
|
-
...base,
|
|
1264
|
-
oauth_directory_policy: QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2,
|
|
1265
|
-
oauth_directory_validations: oauth.validations,
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
return { schema_version: QUALIFICATION_TERMINAL_RECEIPT_VERSION, ...base };
|
|
1269
|
-
}
|
|
1270
|
-
async function finalizeAfterCapture(options) {
|
|
1271
|
-
const paths = qualificationSpoolPaths(options.spoolDir, options.invocation.invocation_id);
|
|
1272
|
-
const stdoutFinal = join(paths.invocation, "stdout.partial");
|
|
1273
|
-
const stderrFinal = join(paths.invocation, "stderr.partial");
|
|
1274
|
-
if (options.stdoutCapture.path !== stdoutFinal || options.stderrCapture.path !== stderrFinal) {
|
|
1275
|
-
throw new Error("qualification output capture paths do not match the durable partial-output contract");
|
|
1276
|
-
}
|
|
1277
|
-
const stdout = outputReceipt(paths.root, stdoutFinal, options.stdoutCapture);
|
|
1278
|
-
const stderr = outputReceipt(paths.root, stderrFinal, options.stderrCapture);
|
|
1279
|
-
const finishedAt = timestamp(options.now, "qualification terminal time");
|
|
1280
|
-
const oauth = collectOAuthDirectoryValidationsV2(options.invocation, paths.root, options.auth, finishedAt, true);
|
|
1281
|
-
const artifactEligible = oauth?.artifact_eligible ?? true;
|
|
1282
|
-
let artifact = null;
|
|
1283
|
-
let attestation = { ok: false, actual: null, fallback: false, refused: false, error: "artifact was not validated" };
|
|
1284
|
-
if (!stdout.truncated && artifactEligible) {
|
|
1285
|
-
const artifactPath = join(paths.root, options.invocation.expected_artifact.path);
|
|
1286
|
-
try {
|
|
1287
|
-
atomicWriteBytes(artifactPath, readFileSync(stdoutFinal), true);
|
|
1288
|
-
const bytes = readFileSync(artifactPath);
|
|
1289
|
-
artifact = { path: options.invocation.expected_artifact.path, type: "pi-jsonl", bytes: bytes.length, sha256: qualificationSha256(bytes) };
|
|
1290
|
-
attestation = attestPiJsonl(bytes.toString("utf8"), options.invocation.requested);
|
|
1291
|
-
}
|
|
1292
|
-
catch (error) {
|
|
1293
|
-
attestation = { ok: false, actual: null, fallback: false, refused: false, error: safeError(error) };
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
else if (!artifactEligible) {
|
|
1297
|
-
attestation = { ok: false, actual: null, fallback: false, refused: false, error: oauth?.error ?? "OAuth directory terminal inventory is invalid" };
|
|
1298
|
-
}
|
|
1299
|
-
else {
|
|
1300
|
-
attestation = { ok: false, actual: null, fallback: false, refused: false, error: "stdout exceeded the configured output limit" };
|
|
1301
|
-
}
|
|
1302
|
-
const status = options.terminalStatus
|
|
1303
|
-
?? (options.exitCode !== 0 ? "failed"
|
|
1304
|
-
: !artifactEligible ? "invalid-artifact"
|
|
1305
|
-
: attestation.refused ? "refused"
|
|
1306
|
-
: attestation.ok ? "completed" : "invalid-artifact");
|
|
1307
|
-
const error = options.error ?? (status === "completed" ? null : oauth?.error ?? attestation.error ?? `child exited ${String(options.exitCode)}`);
|
|
1308
|
-
const receipt = terminalReceiptForPolicy(options.invocation, paths.root, {
|
|
1309
|
-
invocation_id: options.invocation.invocation_id,
|
|
1310
|
-
terminal_status: status,
|
|
1311
|
-
attempt: 1,
|
|
1312
|
-
started_at: options.startedAt,
|
|
1313
|
-
finished_at: finishedAt,
|
|
1314
|
-
deadline_at: options.deadlineAt,
|
|
1315
|
-
requested_timeout_ms: options.invocation.execution.timeout_ms,
|
|
1316
|
-
effective_timeout_ms: options.invocation.execution.timeout_ms,
|
|
1317
|
-
supervisor: options.supervisor,
|
|
1318
|
-
child: options.child,
|
|
1319
|
-
exit_code: options.exitCode,
|
|
1320
|
-
signal: options.signal,
|
|
1321
|
-
requested: options.invocation.requested,
|
|
1322
|
-
actual: attestation.actual,
|
|
1323
|
-
provider_model_identity_observed: attestation.ok,
|
|
1324
|
-
successful_execution: status === "completed",
|
|
1325
|
-
fallback_detected: attestation.fallback,
|
|
1326
|
-
authentication: {
|
|
1327
|
-
evidence_sha256: qualificationSha256(`${qualificationCanonicalJson(options.auth)}\n`),
|
|
1328
|
-
status: "ready",
|
|
1329
|
-
auth_type: "oauth",
|
|
1330
|
-
readiness_only: true,
|
|
1331
|
-
},
|
|
1332
|
-
accounting_event_sha256: options.accountingEventSha,
|
|
1333
|
-
continuation_authority_sha256: options.continuationSha,
|
|
1334
|
-
stdout,
|
|
1335
|
-
stderr,
|
|
1336
|
-
artifact,
|
|
1337
|
-
error,
|
|
1338
|
-
}, oauth);
|
|
1339
|
-
commitTerminalReceipt(paths.root, options.invocation, receipt);
|
|
1340
|
-
}
|
|
1341
|
-
async function finalizeWithoutChild(options) {
|
|
1342
|
-
const paths = qualificationSpoolPaths(options.spoolDir, options.invocation.invocation_id);
|
|
1343
|
-
const stdoutPath = join(paths.invocation, "stdout.partial");
|
|
1344
|
-
const stderrPath = join(paths.invocation, "stderr.partial");
|
|
1345
|
-
if (!existsSync(stdoutPath))
|
|
1346
|
-
atomicWriteBytes(stdoutPath, Buffer.alloc(0), true);
|
|
1347
|
-
if (!existsSync(stderrPath))
|
|
1348
|
-
atomicWriteBytes(stderrPath, Buffer.alloc(0), true);
|
|
1349
|
-
const existingCapture = (path) => {
|
|
1350
|
-
assertRegularFile(path, `qualification interrupted output ${path}`);
|
|
1351
|
-
const bytes = readFileSync(path).length;
|
|
1352
|
-
return { path, total_bytes: bytes, captured_bytes: bytes, truncated: options.outputIncomplete === true, write: () => { }, close: () => { } };
|
|
1353
|
-
};
|
|
1354
|
-
const finishedAt = timestamp(options.now, "qualification terminal time");
|
|
1355
|
-
const oauth = collectOAuthDirectoryValidationsV2(options.invocation, paths.root, options.auth, finishedAt, Boolean(options.child || options.startedAt));
|
|
1356
|
-
const interruptedArtifactPath = join(paths.root, options.invocation.expected_artifact.path);
|
|
1357
|
-
let interruptedArtifact = null;
|
|
1358
|
-
let interruptedAttestation = { ok: false, actual: null, fallback: false, refused: false, error: null };
|
|
1359
|
-
if (existsSync(interruptedArtifactPath) && options.invocation.oauth_directory_policy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
1360
|
-
// This path did not complete the normal post-child terminal validation,
|
|
1361
|
-
// so no pre-existing artifact can become eligible under policy v2.
|
|
1362
|
-
rmSync(interruptedArtifactPath, { force: true });
|
|
1363
|
-
}
|
|
1364
|
-
else if (existsSync(interruptedArtifactPath) && (oauth?.artifact_eligible ?? true)) {
|
|
1365
|
-
assertRegularFile(interruptedArtifactPath, `qualification interrupted artifact ${options.invocation.invocation_id}`);
|
|
1366
|
-
const bytes = readFileSync(interruptedArtifactPath);
|
|
1367
|
-
interruptedArtifact = { path: options.invocation.expected_artifact.path, type: "pi-jsonl", bytes: bytes.length, sha256: qualificationSha256(bytes) };
|
|
1368
|
-
interruptedAttestation = attestPiJsonl(bytes.toString("utf8"), options.invocation.requested);
|
|
1369
|
-
}
|
|
1370
|
-
else if (existsSync(interruptedArtifactPath)) {
|
|
1371
|
-
rmSync(interruptedArtifactPath, { force: true });
|
|
1372
|
-
}
|
|
1373
|
-
const receipt = terminalReceiptForPolicy(options.invocation, paths.root, {
|
|
1374
|
-
invocation_id: options.invocation.invocation_id,
|
|
1375
|
-
terminal_status: options.status,
|
|
1376
|
-
attempt: options.accountingEventSha ? 1 : 0,
|
|
1377
|
-
started_at: options.startedAt ?? null,
|
|
1378
|
-
finished_at: finishedAt,
|
|
1379
|
-
deadline_at: options.deadlineAt ?? null,
|
|
1380
|
-
requested_timeout_ms: options.invocation.execution.timeout_ms,
|
|
1381
|
-
effective_timeout_ms: options.invocation.execution.timeout_ms,
|
|
1382
|
-
supervisor: options.supervisor,
|
|
1383
|
-
child: options.child ?? null,
|
|
1384
|
-
exit_code: null,
|
|
1385
|
-
signal: null,
|
|
1386
|
-
requested: options.invocation.requested,
|
|
1387
|
-
actual: interruptedAttestation.actual,
|
|
1388
|
-
provider_model_identity_observed: interruptedAttestation.ok,
|
|
1389
|
-
successful_execution: false,
|
|
1390
|
-
fallback_detected: interruptedAttestation.fallback,
|
|
1391
|
-
authentication: options.accountingEventSha && options.auth ? {
|
|
1392
|
-
evidence_sha256: qualificationSha256(`${qualificationCanonicalJson(options.auth)}\n`),
|
|
1393
|
-
status: "ready",
|
|
1394
|
-
auth_type: "oauth",
|
|
1395
|
-
readiness_only: true,
|
|
1396
|
-
} : null,
|
|
1397
|
-
accounting_event_sha256: options.accountingEventSha ?? null,
|
|
1398
|
-
continuation_authority_sha256: options.continuationSha ?? null,
|
|
1399
|
-
stdout: outputReceipt(paths.root, stdoutPath, existingCapture(stdoutPath)),
|
|
1400
|
-
stderr: outputReceipt(paths.root, stderrPath, existingCapture(stderrPath)),
|
|
1401
|
-
artifact: interruptedArtifact,
|
|
1402
|
-
error: oauth?.error ?? options.error,
|
|
1403
|
-
}, oauth);
|
|
1404
|
-
commitTerminalReceipt(paths.root, options.invocation, receipt);
|
|
1405
|
-
}
|
|
1406
|
-
function readExactRegularBytes(path, ctx) {
|
|
1407
|
-
let fd;
|
|
1408
|
-
try {
|
|
1409
|
-
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
1410
|
-
const stat = fstatSync(fd);
|
|
1411
|
-
if (!stat.isFile() || stat.nlink !== 1)
|
|
1412
|
-
throw new Error(`${ctx} is not a single-link regular file`);
|
|
1413
|
-
return readFileSync(fd);
|
|
1414
|
-
}
|
|
1415
|
-
catch (error) {
|
|
1416
|
-
if (error instanceof Error && error.message.includes("single-link"))
|
|
1417
|
-
throw error;
|
|
1418
|
-
throw new Error(`${ctx} is missing, replaced, or unreadable`);
|
|
1419
|
-
}
|
|
1420
|
-
finally {
|
|
1421
|
-
if (fd !== undefined)
|
|
1422
|
-
closeSync(fd);
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
function parseCanonicalReceiptBytes(bytes, invocation) {
|
|
1426
|
-
let value;
|
|
1427
|
-
try {
|
|
1428
|
-
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
1429
|
-
}
|
|
1430
|
-
catch {
|
|
1431
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} contains invalid JSON/UTF-8`);
|
|
1432
|
-
}
|
|
1433
|
-
if (!bytes.equals(Buffer.from(`${qualificationCanonicalJson(value)}\n`)))
|
|
1434
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} is not canonical JSON`);
|
|
1435
|
-
validateTerminalReceipt(value, invocation);
|
|
1436
|
-
return value;
|
|
1437
|
-
}
|
|
1438
|
-
function commitTerminalReceipt(spoolDir, invocation, receipt) {
|
|
1439
|
-
const paths = qualificationSpoolPaths(spoolDir, invocation.invocation_id);
|
|
1440
|
-
if (existsSync(paths.terminal)) {
|
|
1441
|
-
const existing = readTerminalReceipt(spoolDir, invocation);
|
|
1442
|
-
if (qualificationCanonicalJson(existing) !== qualificationCanonicalJson(receipt))
|
|
1443
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} is immutable`);
|
|
1444
|
-
reconcileTerminalLifecycle(spoolDir, invocation);
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
const temp = join(paths.invocation, `.terminal-${randomBytes(10).toString("hex")}`);
|
|
1448
|
-
mkdirSync(temp, { mode: 0o700 });
|
|
1449
|
-
try {
|
|
1450
|
-
const temporaryReceiptPath = join(temp, "receipt.json");
|
|
1451
|
-
atomicWriteCanonical(temporaryReceiptPath, receipt, true);
|
|
1452
|
-
const exactReceiptBytes = readExactRegularBytes(temporaryReceiptPath, `qualification terminal receipt ${invocation.invocation_id}`);
|
|
1453
|
-
parseCanonicalReceiptBytes(exactReceiptBytes, invocation);
|
|
1454
|
-
if (receipt.schema_version === QUALIFICATION_TERMINAL_RECEIPT_VERSION_V3) {
|
|
1455
|
-
atomicWriteCanonical(join(temp, "receipt-identity.json"), {
|
|
1456
|
-
schema_version: "qualification-terminal-receipt-byte-identity-v1",
|
|
1457
|
-
invocation_id: invocation.invocation_id,
|
|
1458
|
-
bytes: exactReceiptBytes.length,
|
|
1459
|
-
sha256: qualificationSha256(exactReceiptBytes),
|
|
1460
|
-
}, true);
|
|
1461
|
-
}
|
|
1462
|
-
renameSync(temp, paths.terminal);
|
|
1463
|
-
fsyncQualificationDirectory(paths.invocation);
|
|
1464
|
-
}
|
|
1465
|
-
finally {
|
|
1466
|
-
rmSync(temp, { recursive: true, force: true });
|
|
1467
|
-
}
|
|
1468
|
-
reconcileTerminalLifecycle(spoolDir, invocation);
|
|
1469
|
-
}
|
|
1470
|
-
function reconcileTerminalLifecycle(spoolDir, invocation) {
|
|
1471
|
-
const paths = qualificationSpoolPaths(spoolDir, invocation.invocation_id);
|
|
1472
|
-
if (!existsSync(join(paths.terminal, "receipt.json")))
|
|
1473
|
-
return;
|
|
1474
|
-
const receipt = readTerminalReceipt(spoolDir, invocation);
|
|
1475
|
-
assertTerminalReceiptCrossBindings(spoolDir, invocation, receipt);
|
|
1476
|
-
const lifecycle = readQualificationLifecycle(spoolDir, invocation.invocation_id);
|
|
1477
|
-
if (lifecycle.phase === "terminal") {
|
|
1478
|
-
if (lifecycle.terminal_status !== receipt.terminal_status || lifecycle.events.at(-1)?.detail.receipt_sha256 !== qualificationSha256(`${qualificationCanonicalJson(receipt)}\n`)) {
|
|
1479
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} contradicts lifecycle`);
|
|
1480
|
-
}
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
writeQualificationLifecycle(spoolDir, transitionQualificationLifecycle(lifecycle, "terminal", {
|
|
1484
|
-
at: receipt.finished_at,
|
|
1485
|
-
terminal_status: receipt.terminal_status,
|
|
1486
|
-
detail: { receipt_sha256: qualificationSha256(`${qualificationCanonicalJson(receipt)}\n`) },
|
|
1487
|
-
}));
|
|
1488
|
-
}
|
|
1489
|
-
function validateLaunchAttempt(value, invocation, spoolRoot, expectedAccountingSha) {
|
|
1490
|
-
if (!plainObject(value))
|
|
1491
|
-
throw new Error(`qualification launch attempt ${invocation.invocation_id} must be an object`);
|
|
1492
|
-
const v3 = isInputBoundQualificationInvocation(invocation);
|
|
1493
|
-
exactKeys(value, v3 ? ["schema_version", "invocation_id", "attempt", "at", "executable", "argv", "environment_names", "automatic_retry", "invocation_input", "accounting_event_sha256"] : ["schema_version", "invocation_id", "attempt", "at", "executable", "argv", "environment_names", "automatic_retry"], `qualification launch attempt ${invocation.invocation_id}`);
|
|
1494
|
-
if (value.schema_version !== (v3 ? QUALIFICATION_LAUNCH_ATTEMPT_VERSION_V2 : QUALIFICATION_LAUNCH_ATTEMPT_VERSION) || value.invocation_id !== invocation.invocation_id || value.attempt !== 1 || value.automatic_retry !== false) {
|
|
1495
|
-
throw new Error(`qualification launch attempt ${invocation.invocation_id} identity/version/retry policy mismatch`);
|
|
1496
|
-
}
|
|
1497
|
-
timestamp(String(value.at), `qualification launch attempt ${invocation.invocation_id} time`);
|
|
1498
|
-
if (v3 && (qualificationCanonicalJson(value.invocation_input) !== qualificationCanonicalJson(invocation.invocation_input) || typeof value.accounting_event_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.accounting_event_sha256) || value.accounting_event_sha256 !== expectedAccountingSha))
|
|
1499
|
-
throw new Error(`qualification launch attempt ${invocation.invocation_id} input binding mismatch`);
|
|
1500
|
-
if (qualificationCanonicalJson(value.executable) !== qualificationCanonicalJson(invocation.execution.executable))
|
|
1501
|
-
throw new Error(`qualification launch attempt ${invocation.invocation_id} executable mismatch`);
|
|
1502
|
-
const expectedArgv = buildQualificationArgv(invocation, join(spoolRoot, invocation.expected_artifact.path));
|
|
1503
|
-
if (qualificationCanonicalJson(value.argv) !== qualificationCanonicalJson(expectedArgv))
|
|
1504
|
-
throw new Error(`qualification launch attempt ${invocation.invocation_id} argv mismatch`);
|
|
1505
|
-
if (!Array.isArray(value.environment_names) || value.environment_names.some((name) => typeof name !== "string") ||
|
|
1506
|
-
new Set(value.environment_names).size !== value.environment_names.length ||
|
|
1507
|
-
value.environment_names.some((name) => !invocation.execution.allowed_environment_names.includes(name))) {
|
|
1508
|
-
throw new Error(`qualification launch attempt ${invocation.invocation_id} environment-name set is invalid`);
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
function readTerminalReceipt(spoolDir, invocation) {
|
|
1512
|
-
const terminal = qualificationSpoolPaths(spoolDir, invocation.invocation_id).terminal;
|
|
1513
|
-
const path = join(terminal, "receipt.json");
|
|
1514
|
-
const exactBytes = readExactRegularBytes(path, `qualification terminal receipt ${invocation.invocation_id}`);
|
|
1515
|
-
const receipt = parseCanonicalReceiptBytes(exactBytes, invocation);
|
|
1516
|
-
if (isInputBoundQualificationInvocation(invocation)) {
|
|
1517
|
-
const identity = readCanonicalJson(join(terminal, "receipt-identity.json"), `qualification terminal receipt byte identity ${invocation.invocation_id}`);
|
|
1518
|
-
if (!plainObject(identity))
|
|
1519
|
-
throw new Error(`qualification terminal receipt byte identity ${invocation.invocation_id} must be an object`);
|
|
1520
|
-
exactKeys(identity, ["schema_version", "invocation_id", "bytes", "sha256"], `qualification terminal receipt byte identity ${invocation.invocation_id}`);
|
|
1521
|
-
if (identity.schema_version !== "qualification-terminal-receipt-byte-identity-v1" || identity.invocation_id !== invocation.invocation_id || identity.bytes !== exactBytes.length || identity.sha256 !== qualificationSha256(exactBytes))
|
|
1522
|
-
throw new Error(`qualification terminal receipt byte identity ${invocation.invocation_id} mismatch`);
|
|
1523
|
-
}
|
|
1524
|
-
return receipt;
|
|
1525
|
-
}
|
|
1526
|
-
function assertTerminalReceiptCrossBindings(spoolDir, invocation, receipt) {
|
|
1527
|
-
const paths = qualificationSpoolPaths(spoolDir, invocation.invocation_id);
|
|
1528
|
-
const accountingEvent = readQualificationAccounting(paths.root).events.find((event) => event.invocation_id === invocation.invocation_id);
|
|
1529
|
-
const expectedAttempt = accountingEvent ? 1 : 0;
|
|
1530
|
-
if (receipt.attempt !== expectedAttempt || receipt.accounting_event_sha256 !== (accountingEvent?.event_sha256 ?? null)) {
|
|
1531
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} contradicts accounting`);
|
|
1532
|
-
}
|
|
1533
|
-
const launchAttemptPath = join(paths.invocation, "launch-attempt.json");
|
|
1534
|
-
const hasLaunchAttempt = existsSync(launchAttemptPath);
|
|
1535
|
-
if (hasLaunchAttempt)
|
|
1536
|
-
validateLaunchAttempt(readCanonicalJson(launchAttemptPath, `qualification launch attempt ${invocation.invocation_id}`), invocation, paths.root, accountingEvent?.event_sha256);
|
|
1537
|
-
if (receipt.started_at !== null && !hasLaunchAttempt)
|
|
1538
|
-
throw new Error(`qualification started terminal receipt for ${invocation.invocation_id} has no launch attempt`);
|
|
1539
|
-
if (receipt.attempt === 0 && hasLaunchAttempt)
|
|
1540
|
-
throw new Error(`qualification attempt-zero receipt for ${invocation.invocation_id} has a launch attempt`);
|
|
1541
|
-
const occurrence = readChildOccurrence(paths.invocation, invocation, false, accountingEvent?.event_sha256);
|
|
1542
|
-
if (receipt.schema_version === QUALIFICATION_TERMINAL_RECEIPT_VERSION_V3) {
|
|
1543
|
-
const digestEvidence = (name) => {
|
|
1544
|
-
const path = join(paths.invocation, name);
|
|
1545
|
-
return existsSync(path) ? qualificationSha256(readExactRegularBytes(path, `qualification ${name}`)) : null;
|
|
1546
|
-
};
|
|
1547
|
-
const claimSha = digestEvidence("input-launch-claim.json");
|
|
1548
|
-
const attemptSha = digestEvidence("launch-attempt.json");
|
|
1549
|
-
const childSha = digestEvidence("child-occurrence.json");
|
|
1550
|
-
if (receipt.configuration_sha256 !== invocation.configuration_sha256 || receipt.invocation_sha256 !== invocation.invocation_sha256 || qualificationCanonicalJson(receipt.invocation_input) !== qualificationCanonicalJson(invocation.invocation_input) || receipt.input_launch_claim_sha256 !== claimSha || receipt.launch_attempt_sha256 !== attemptSha || receipt.child_occurrence_sha256 !== childSha)
|
|
1551
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} v3 evidence digest mismatch`);
|
|
1552
|
-
if (accountingEvent)
|
|
1553
|
-
readInputLaunchClaim(paths.invocation, invocation, accountingEvent.event_sha256);
|
|
1554
|
-
}
|
|
1555
|
-
if (occurrence && (qualificationCanonicalJson(receipt.child) !== qualificationCanonicalJson(occurrence.child) || receipt.started_at !== occurrence.started_at || receipt.deadline_at !== occurrence.deadline_at)) {
|
|
1556
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} contradicts child occurrence evidence`);
|
|
1557
|
-
}
|
|
1558
|
-
const continuation = readContinuationRecord(paths.invocation);
|
|
1559
|
-
if (continuation)
|
|
1560
|
-
assertContinuationRecordBound(continuation, invocation);
|
|
1561
|
-
if (receipt.continuation_authority_sha256 !== (continuation ? String(continuation.authority_sha256) : null)) {
|
|
1562
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} continuation authority mismatch`);
|
|
1563
|
-
}
|
|
1564
|
-
let authEvidence = null;
|
|
1565
|
-
if (receipt.authentication) {
|
|
1566
|
-
authEvidence = readAuthEvidence(paths.root, invocation, false);
|
|
1567
|
-
if (receipt.authentication.evidence_sha256 !== qualificationSha256(`${qualificationCanonicalJson(authEvidence)}\n`)) {
|
|
1568
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} authentication evidence mismatch`);
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
if (receipt.schema_version === QUALIFICATION_TERMINAL_RECEIPT_VERSION_V2 || receipt.schema_version === QUALIFICATION_TERMINAL_RECEIPT_VERSION_V3) {
|
|
1572
|
-
if (invocation.oauth_directory_policy !== QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2)
|
|
1573
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} reinterprets historical policy as v2`);
|
|
1574
|
-
const validations = receipt.oauth_directory_validations;
|
|
1575
|
-
if (authEvidence?.schema_version === QUALIFICATION_AUTH_EVIDENCE_VERSION_V2) {
|
|
1576
|
-
if (qualificationCanonicalJson(validations.before_oauth_readiness) !== qualificationCanonicalJson(authEvidence.oauth_directory_validations.before_oauth_readiness) ||
|
|
1577
|
-
qualificationCanonicalJson(validations.after_oauth_readiness) !== qualificationCanonicalJson(authEvidence.oauth_directory_validations.after_oauth_readiness)) {
|
|
1578
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} OAuth readiness occurrence mismatch`);
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
else if (receipt.attempt === 1) {
|
|
1582
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} lacks v2 auth occurrence evidence`);
|
|
1583
|
-
}
|
|
1584
|
-
if (receipt.attempt === 1) {
|
|
1585
|
-
const claimPath = oauthCheckpointPath(paths.invocation, "before-launch-claim");
|
|
1586
|
-
if (existsSync(claimPath)) {
|
|
1587
|
-
const claim = readOAuthDirectoryCheckpointV2(paths.invocation, invocation, "before-launch-claim");
|
|
1588
|
-
if (qualificationCanonicalJson(validations.before_launch_claim) !== qualificationCanonicalJson(claim)) {
|
|
1589
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} launch-claim OAuth occurrence mismatch`);
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
else if (validations.before_launch_claim !== null || receipt.artifact !== null ||
|
|
1593
|
-
receipt.terminal_status === "completed" || receipt.terminal_status === "refused") {
|
|
1594
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} did not fail closed over missing launch-claim OAuth evidence`);
|
|
1595
|
-
}
|
|
1596
|
-
if (existsSync(oauthCheckpointPath(paths.invocation, "immediately-before-pi-launch"))) {
|
|
1597
|
-
const prelaunch = readOAuthDirectoryCheckpointV2(paths.invocation, invocation, "immediately-before-pi-launch");
|
|
1598
|
-
if (qualificationCanonicalJson(validations.immediately_before_pi_launch) !== qualificationCanonicalJson(prelaunch)) {
|
|
1599
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} prelaunch OAuth occurrence mismatch`);
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
else if (validations.immediately_before_pi_launch !== null) {
|
|
1603
|
-
throw new Error(`qualification terminal receipt for ${invocation.invocation_id} invents missing prelaunch OAuth occurrence evidence`);
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
verifyOutputReceipt(paths.root, receipt.stdout);
|
|
1608
|
-
verifyOutputReceipt(paths.root, receipt.stderr);
|
|
1609
|
-
if (receipt.artifact) {
|
|
1610
|
-
const artifactPath = join(paths.root, receipt.artifact.path);
|
|
1611
|
-
assertRegularFile(artifactPath, `qualification artifact ${invocation.invocation_id}`);
|
|
1612
|
-
const bytes = readFileSync(artifactPath);
|
|
1613
|
-
if (bytes.length !== receipt.artifact.bytes || qualificationSha256(bytes) !== receipt.artifact.sha256) {
|
|
1614
|
-
throw new Error(`qualification artifact ${invocation.invocation_id} digest/size mismatch`);
|
|
1615
|
-
}
|
|
1616
|
-
const stdoutBytes = readFileSync(join(paths.root, receipt.stdout.path));
|
|
1617
|
-
if (!bytes.equals(stdoutBytes))
|
|
1618
|
-
throw new Error(`qualification artifact ${invocation.invocation_id} does not equal retained stdout evidence`);
|
|
1619
|
-
const attestation = attestPiJsonl(bytes.toString("utf8"), invocation.requested);
|
|
1620
|
-
if (qualificationCanonicalJson(receipt.actual) !== qualificationCanonicalJson(attestation.actual) ||
|
|
1621
|
-
receipt.provider_model_identity_observed !== attestation.ok || receipt.fallback_detected !== attestation.fallback) {
|
|
1622
|
-
throw new Error(`qualification artifact ${invocation.invocation_id} semantic identity contradicts terminal receipt`);
|
|
1623
|
-
}
|
|
1624
|
-
if (receipt.terminal_status === "completed" && (receipt.exit_code !== 0 || receipt.stdout.truncated || !attestation.ok || attestation.refused)) {
|
|
1625
|
-
throw new Error(`qualification artifact ${invocation.invocation_id} cannot substantiate completed execution`);
|
|
1626
|
-
}
|
|
1627
|
-
if (receipt.terminal_status === "refused" && (receipt.exit_code !== 0 || receipt.stdout.truncated || !attestation.ok || !attestation.refused)) {
|
|
1628
|
-
throw new Error(`qualification artifact ${invocation.invocation_id} cannot substantiate refusal`);
|
|
1629
|
-
}
|
|
1630
|
-
if (receipt.terminal_status === "invalid-artifact" && receipt.exit_code === 0 && !receipt.stdout.truncated && attestation.ok) {
|
|
1631
|
-
throw new Error(`qualification artifact ${invocation.invocation_id} contradicts invalid-artifact classification`);
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
else if (receipt.terminal_status === "completed" || receipt.terminal_status === "refused") {
|
|
1635
|
-
throw new Error(`qualification terminal ${receipt.terminal_status} for ${invocation.invocation_id} has no semantic artifact evidence`);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
function validateTerminalReceipt(value, invocation) {
|
|
1639
|
-
if (!plainObject(value))
|
|
1640
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} must be an object`);
|
|
1641
|
-
const historical = ["schema_version", "invocation_id", "terminal_status", "attempt", "started_at", "finished_at", "deadline_at", "requested_timeout_ms", "effective_timeout_ms", "supervisor", "child", "exit_code", "signal", "requested", "actual", "provider_model_identity_observed", "successful_execution", "fallback_detected", "authentication", "accounting_event_sha256", "continuation_authority_sha256", "stdout", "stderr", "artifact", "error"];
|
|
1642
|
-
const policyV2 = invocation.oauth_directory_policy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2;
|
|
1643
|
-
const receiptV3 = isInputBoundQualificationInvocation(invocation);
|
|
1644
|
-
if (policyV2) {
|
|
1645
|
-
const expectedVersion = receiptV3 ? QUALIFICATION_TERMINAL_RECEIPT_VERSION_V3 : QUALIFICATION_TERMINAL_RECEIPT_VERSION_V2;
|
|
1646
|
-
if (value.schema_version !== expectedVersion) {
|
|
1647
|
-
throw new Error(`qualification ${receiptV3 ? "v3" : "v2"} terminal receipt ${invocation.invocation_id} identity/policy mismatch; historical receipts are not reinterpreted`);
|
|
1648
|
-
}
|
|
1649
|
-
exactKeys(value, receiptV3 ? [...historical, "oauth_directory_policy", "oauth_directory_validations", "configuration_sha256", "invocation_sha256", "invocation_input", "input_launch_claim_sha256", "launch_attempt_sha256", "child_occurrence_sha256"] : [...historical, "oauth_directory_policy", "oauth_directory_validations"], `qualification terminal receipt ${invocation.invocation_id}`);
|
|
1650
|
-
if (value.oauth_directory_policy !== QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
1651
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} identity/policy mismatch; historical receipts are not reinterpreted`);
|
|
1652
|
-
}
|
|
1653
|
-
validateTerminalOAuthDirectoryValidationsV2(value.oauth_directory_validations, invocation.invocation_id);
|
|
1654
|
-
if (receiptV3) {
|
|
1655
|
-
if (value.configuration_sha256 !== invocation.configuration_sha256 || value.invocation_sha256 !== invocation.invocation_sha256 || qualificationCanonicalJson(value.invocation_input) !== qualificationCanonicalJson(invocation.invocation_input))
|
|
1656
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} v3 immutable binding mismatch`);
|
|
1657
|
-
for (const key of ["input_launch_claim_sha256", "launch_attempt_sha256", "child_occurrence_sha256"])
|
|
1658
|
-
if (value[key] !== null && !/^[a-f0-9]{64}$/.test(String(value[key])))
|
|
1659
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} ${key} is invalid`);
|
|
1660
|
-
if (value.attempt === 1 && value.input_launch_claim_sha256 === null)
|
|
1661
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} lacks its v3 input launch claim`);
|
|
1662
|
-
if ((value.started_at !== null) !== (value.child_occurrence_sha256 !== null))
|
|
1663
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} child occurrence digest mismatch`);
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
else {
|
|
1667
|
-
exactKeys(value, historical, `qualification terminal receipt ${invocation.invocation_id}`);
|
|
1668
|
-
if (value.schema_version !== QUALIFICATION_TERMINAL_RECEIPT_VERSION)
|
|
1669
|
-
throw new Error(`qualification historical terminal receipt ${invocation.invocation_id} identity/version mismatch`);
|
|
1670
|
-
}
|
|
1671
|
-
if (value.invocation_id !== invocation.invocation_id || (value.attempt !== 0 && value.attempt !== 1))
|
|
1672
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} identity/version mismatch`);
|
|
1673
|
-
if (!["completed", "failed", "timed-out", "aborted", "refused", "invalid-artifact"].includes(String(value.terminal_status)))
|
|
1674
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} has unknown status`);
|
|
1675
|
-
timestamp(String(value.finished_at), `qualification terminal receipt ${invocation.invocation_id} finish time`);
|
|
1676
|
-
if (value.started_at !== null)
|
|
1677
|
-
timestamp(String(value.started_at), `qualification terminal receipt ${invocation.invocation_id} start time`);
|
|
1678
|
-
if (value.deadline_at !== null)
|
|
1679
|
-
timestamp(String(value.deadline_at), `qualification terminal receipt ${invocation.invocation_id} deadline`);
|
|
1680
|
-
if ((value.started_at === null) !== (value.deadline_at === null))
|
|
1681
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} start/deadline mismatch`);
|
|
1682
|
-
if (value.requested_timeout_ms !== invocation.execution.timeout_ms || value.effective_timeout_ms !== invocation.execution.timeout_ms)
|
|
1683
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} timeout policy mismatch`);
|
|
1684
|
-
if (!processIdentityFrom(value.supervisor) || (value.child !== null && !processIdentityFrom(value.child)))
|
|
1685
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} process identity is invalid`);
|
|
1686
|
-
if (qualificationCanonicalJson(value.requested) !== qualificationCanonicalJson(invocation.requested))
|
|
1687
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} requested identity mismatch`);
|
|
1688
|
-
if (value.actual !== null && (!plainObject(value.actual) || Object.keys(value.actual).sort().join(",") !== "model,provider" || typeof value.actual.provider !== "string" || typeof value.actual.model !== "string")) {
|
|
1689
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} actual identity is invalid`);
|
|
1690
|
-
}
|
|
1691
|
-
if (value.exit_code !== null && !Number.isInteger(value.exit_code))
|
|
1692
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} exit code is invalid`);
|
|
1693
|
-
if (value.signal !== null && typeof value.signal !== "string")
|
|
1694
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} signal is invalid`);
|
|
1695
|
-
if (value.successful_execution !== (value.terminal_status === "completed"))
|
|
1696
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} successful-execution flag contradicts terminal status`);
|
|
1697
|
-
if (typeof value.provider_model_identity_observed !== "boolean" || typeof value.fallback_detected !== "boolean")
|
|
1698
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} identity/fallback flags are invalid`);
|
|
1699
|
-
if (value.provider_model_identity_observed && qualificationCanonicalJson(value.actual) !== qualificationCanonicalJson(invocation.requested)) {
|
|
1700
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} observed identity does not match the request`);
|
|
1701
|
-
}
|
|
1702
|
-
if (value.terminal_status === "completed" && (!value.provider_model_identity_observed || value.fallback_detected || value.actual === null || value.artifact === null)) {
|
|
1703
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} completed without exact identity/artifact evidence`);
|
|
1704
|
-
}
|
|
1705
|
-
if (value.attempt === 0 && (value.terminal_status !== "aborted" || value.started_at !== null || value.child !== null || value.authentication !== null || value.accounting_event_sha256 !== null)) {
|
|
1706
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} attempt-zero evidence is contradictory`);
|
|
1707
|
-
}
|
|
1708
|
-
if (value.attempt === 1 && (!/^[a-f0-9]{64}$/.test(String(value.accounting_event_sha256)) || !plainObject(value.authentication))) {
|
|
1709
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} attempt-one evidence is incomplete`);
|
|
1710
|
-
}
|
|
1711
|
-
if (value.authentication !== null) {
|
|
1712
|
-
if (!plainObject(value.authentication))
|
|
1713
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} authentication evidence is invalid`);
|
|
1714
|
-
exactKeys(value.authentication, ["evidence_sha256", "status", "auth_type", "readiness_only"], `qualification terminal receipt ${invocation.invocation_id} authentication`);
|
|
1715
|
-
if (!/^[a-f0-9]{64}$/.test(String(value.authentication.evidence_sha256)) || value.authentication.status !== "ready" || value.authentication.auth_type !== "oauth" || value.authentication.readiness_only !== true) {
|
|
1716
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} authentication evidence is invalid`);
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
if (value.continuation_authority_sha256 !== null && !/^[a-f0-9]{64}$/.test(String(value.continuation_authority_sha256)))
|
|
1720
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} continuation digest is invalid`);
|
|
1721
|
-
validateOutputReceiptShape(value.stdout, invocation.invocation_id, "stdout.partial");
|
|
1722
|
-
validateOutputReceiptShape(value.stderr, invocation.invocation_id, "stderr.partial");
|
|
1723
|
-
if (value.artifact !== null) {
|
|
1724
|
-
if (!plainObject(value.artifact) || value.artifact.path !== invocation.expected_artifact.path || value.artifact.type !== "pi-jsonl" || !Number.isSafeInteger(value.artifact.bytes) || Number(value.artifact.bytes) < 0 || !/^[a-f0-9]{64}$/.test(String(value.artifact.sha256))) {
|
|
1725
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} artifact evidence is invalid`);
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
if (value.error !== null && typeof value.error !== "string")
|
|
1729
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} error field is invalid`);
|
|
1730
|
-
if (policyV2) {
|
|
1731
|
-
const validations = value.oauth_directory_validations;
|
|
1732
|
-
const terminal = validations.after_child_termination;
|
|
1733
|
-
if (value.started_at !== null && terminal === null)
|
|
1734
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} lacks post-child OAuth inventory evidence`);
|
|
1735
|
-
if (value.attempt === 1 && validations.before_launch_claim === null &&
|
|
1736
|
-
(value.terminal_status === "completed" || value.terminal_status === "refused" || value.artifact !== null)) {
|
|
1737
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} accepts an artifact without launch-claim OAuth checkpoint evidence`);
|
|
1738
|
-
}
|
|
1739
|
-
if (value.started_at !== null && validations.immediately_before_pi_launch === null &&
|
|
1740
|
-
(value.terminal_status === "completed" || value.terminal_status === "refused" || value.artifact !== null)) {
|
|
1741
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} accepts an artifact without prelaunch OAuth checkpoint evidence`);
|
|
1742
|
-
}
|
|
1743
|
-
if (terminal?.valid === false && value.artifact !== null)
|
|
1744
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} accepts an artifact after invalid OAuth inventory`);
|
|
1745
|
-
if ((value.terminal_status === "completed" || value.terminal_status === "refused") && terminal?.valid !== true) {
|
|
1746
|
-
throw new Error(`qualification terminal receipt ${invocation.invocation_id} successful/refused artifact lacks valid terminal OAuth inventory`);
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
function validateTerminalOAuthDirectoryValidationsV2(value, invocationId) {
|
|
1751
|
-
if (!plainObject(value))
|
|
1752
|
-
throw new Error(`qualification terminal receipt ${invocationId} OAuth directory validations are invalid`);
|
|
1753
|
-
const fields = [
|
|
1754
|
-
["before_oauth_readiness", "before-oauth-readiness"],
|
|
1755
|
-
["after_oauth_readiness", "after-oauth-readiness"],
|
|
1756
|
-
["before_launch_claim", "before-launch-claim"],
|
|
1757
|
-
["immediately_before_pi_launch", "immediately-before-pi-launch"],
|
|
1758
|
-
["after_child_termination", "after-child-termination"],
|
|
1759
|
-
];
|
|
1760
|
-
exactKeys(value, fields.map(([name]) => name), `qualification terminal receipt ${invocationId} OAuth directory validations`);
|
|
1761
|
-
for (const [name, point] of fields) {
|
|
1762
|
-
const inventory = value[name];
|
|
1763
|
-
if (inventory !== null)
|
|
1764
|
-
validateQualificationOAuthDirectoryInventoryV2(inventory, point);
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
function validateOutputReceiptShape(value, invocationId, fileName) {
|
|
1768
|
-
if (!plainObject(value))
|
|
1769
|
-
throw new Error(`qualification terminal receipt ${invocationId} output evidence is invalid`);
|
|
1770
|
-
exactKeys(value, ["path", "total_bytes", "captured_bytes", "truncated", "sha256"], `qualification terminal receipt ${invocationId} output`);
|
|
1771
|
-
if (value.path !== `invocations/${invocationId}/${fileName}` || !Number.isSafeInteger(value.total_bytes) || !Number.isSafeInteger(value.captured_bytes) ||
|
|
1772
|
-
Number(value.total_bytes) < Number(value.captured_bytes) || Number(value.captured_bytes) < 0 || typeof value.truncated !== "boolean" || !/^[a-f0-9]{64}$/.test(String(value.sha256))) {
|
|
1773
|
-
throw new Error(`qualification terminal receipt ${invocationId} output evidence is invalid`);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
function readAuthEvidence(spoolDir, invocation, requireFresh = true) {
|
|
1777
|
-
const path = join(qualificationSpoolPaths(spoolDir, invocation.invocation_id).invocation, "auth-evidence.json");
|
|
1778
|
-
const evidence = readCanonicalJson(path, `qualification auth evidence ${invocation.invocation_id}`);
|
|
1779
|
-
validateAuthEvidence(evidence, invocation, requireFresh);
|
|
1780
|
-
return evidence;
|
|
1781
|
-
}
|
|
1782
|
-
function validateAuthEvidence(evidence, invocation, requireFresh = true) {
|
|
1783
|
-
if (!plainObject(evidence))
|
|
1784
|
-
throw new Error("qualification auth evidence must be an object");
|
|
1785
|
-
const commonKeys = ["schema_version", "checked_at", "provider", "requested_model", "model_identity_observed", "status", "auth_type", "source", "credentials_included", "readiness_only", "removed_parent_environment_names", "child_environment_names", "executable_sha256", "launch_authority_sha256", "evidence_sha256"];
|
|
1786
|
-
const policyV2 = invocation.oauth_directory_policy === QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2;
|
|
1787
|
-
if (policyV2) {
|
|
1788
|
-
exactKeys(evidence, [...commonKeys, "oauth_directory_policy", "oauth_directory_validations"], `qualification auth evidence ${invocation.invocation_id}`);
|
|
1789
|
-
if (evidence.schema_version !== QUALIFICATION_AUTH_EVIDENCE_VERSION_V2 || evidence.oauth_directory_policy !== QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
1790
|
-
throw new Error(`qualification auth evidence v2 policy contradicts invocation ${invocation.invocation_id}`);
|
|
1791
|
-
}
|
|
1792
|
-
if (!plainObject(evidence.oauth_directory_validations))
|
|
1793
|
-
throw new Error(`qualification auth evidence v2 inventories are missing for ${invocation.invocation_id}`);
|
|
1794
|
-
exactKeys(evidence.oauth_directory_validations, ["before_oauth_readiness", "after_oauth_readiness"], `qualification auth evidence v2 inventories ${invocation.invocation_id}`);
|
|
1795
|
-
validateQualificationOAuthDirectoryInventoryV2(evidence.oauth_directory_validations.before_oauth_readiness, "before-oauth-readiness");
|
|
1796
|
-
validateQualificationOAuthDirectoryInventoryV2(evidence.oauth_directory_validations.after_oauth_readiness, "after-oauth-readiness");
|
|
1797
|
-
if (!evidence.oauth_directory_validations.before_oauth_readiness.valid || !evidence.oauth_directory_validations.after_oauth_readiness.valid) {
|
|
1798
|
-
throw new Error(`qualification auth evidence v2 inventory is invalid for ${invocation.invocation_id}`);
|
|
1799
|
-
}
|
|
1800
|
-
assertQualificationOAuthDirectoryContinuityV2(evidence.oauth_directory_validations.before_oauth_readiness, evidence.oauth_directory_validations.after_oauth_readiness, { allow_models_store_change: true });
|
|
1801
|
-
}
|
|
1802
|
-
else {
|
|
1803
|
-
exactKeys(evidence, [...commonKeys, "oauth_agent_directory_identity", "oauth_auth_file_identity", "oauth_models_file_identity", "oauth_directory_entries"], `qualification auth evidence ${invocation.invocation_id}`);
|
|
1804
|
-
if (evidence.schema_version !== QUALIFICATION_AUTH_EVIDENCE_VERSION) {
|
|
1805
|
-
throw new Error(`qualification historical auth evidence cannot be reinterpreted for invocation ${invocation.invocation_id}`);
|
|
1806
|
-
}
|
|
1807
|
-
const productionAuth = invocation.authentication === "chatgpt-oauth";
|
|
1808
|
-
if (productionAuth) {
|
|
1809
|
-
if (!isFilesystemOccurrence(evidence.oauth_agent_directory_identity) || !isFilesystemOccurrence(evidence.oauth_auth_file_identity) ||
|
|
1810
|
-
(evidence.oauth_models_file_identity !== null && !isFilesystemOccurrence(evidence.oauth_models_file_identity)) ||
|
|
1811
|
-
!Array.isArray(evidence.oauth_directory_entries) || evidence.oauth_directory_entries.some((entry) => typeof entry !== "string")) {
|
|
1812
|
-
throw new Error(`qualification auth evidence OAuth filesystem occurrence is invalid for ${invocation.invocation_id}`);
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
else if (evidence.oauth_agent_directory_identity !== null || evidence.oauth_auth_file_identity !== null || evidence.oauth_models_file_identity !== null ||
|
|
1816
|
-
!Array.isArray(evidence.oauth_directory_entries) || evidence.oauth_directory_entries.length !== 0) {
|
|
1817
|
-
throw new Error(`qualification test auth evidence must not claim a production OAuth filesystem occurrence`);
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
if (evidence.provider !== invocation.requested.provider || evidence.requested_model !== invocation.requested.model || evidence.model_identity_observed !== false ||
|
|
1821
|
-
evidence.status !== "ready" || evidence.auth_type !== "oauth" || evidence.credentials_included !== false || evidence.readiness_only !== true ||
|
|
1822
|
-
!/^[a-f0-9]{64}$/.test(evidence.launch_authority_sha256)) {
|
|
1823
|
-
throw new Error(`qualification auth evidence contradicts invocation ${invocation.invocation_id}`);
|
|
1824
|
-
}
|
|
1825
|
-
const { evidence_sha256: recorded, ...digestInput } = evidence;
|
|
1826
|
-
if (!/^[a-f0-9]{64}$/.test(recorded) || qualificationSha256(qualificationCanonicalJson(digestInput)) !== recorded) {
|
|
1827
|
-
throw new Error(`qualification auth evidence digest mismatch for ${invocation.invocation_id}`);
|
|
1828
|
-
}
|
|
1829
|
-
const checkedAt = Date.parse(timestamp(evidence.checked_at, "qualification auth evidence time"));
|
|
1830
|
-
if (requireFresh && (checkedAt > Date.now() + 60_000 || Date.now() - checkedAt > 5 * 60_000)) {
|
|
1831
|
-
throw new Error(`qualification auth evidence for ${invocation.invocation_id} is stale; run start to perform a fresh Pi auth check`);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
function isFilesystemOccurrence(value) {
|
|
1835
|
-
return plainObject(value) && typeof value.realpath === "string" && Number.isSafeInteger(value.device) && Number.isSafeInteger(value.inode) &&
|
|
1836
|
-
typeof value.mtime_ms === "number" && Number.isFinite(value.mtime_ms) && Number.isSafeInteger(value.bytes) && Number(value.bytes) >= 0;
|
|
1837
|
-
}
|
|
1838
|
-
function assertConfiguredOAuthBoundaryMatchesEvidence(evidence, current, allowModelsStoreChange) {
|
|
1839
|
-
if (evidence.schema_version === QUALIFICATION_AUTH_EVIDENCE_VERSION_V2) {
|
|
1840
|
-
if (!isOAuthBoundaryV2(current) || evidence.oauth_directory_policy !== QUALIFICATION_OAUTH_DIRECTORY_POLICY_V2) {
|
|
1841
|
-
throw new Error("qualification OAuth directory policy v2 evidence cannot be matched to a historical boundary");
|
|
1842
|
-
}
|
|
1843
|
-
assertQualificationOAuthDirectoryContinuityV2(evidence.oauth_directory_validations.after_oauth_readiness, current.inventory, { allow_models_store_change: allowModelsStoreChange });
|
|
1844
|
-
return;
|
|
1845
|
-
}
|
|
1846
|
-
if (current === null)
|
|
1847
|
-
return;
|
|
1848
|
-
if (isOAuthBoundaryV2(current))
|
|
1849
|
-
throw new Error("qualification historical OAuth evidence cannot be reinterpreted under policy v2");
|
|
1850
|
-
assertOAuthBoundaryMatchesEvidence(evidence, current);
|
|
1851
|
-
}
|
|
1852
|
-
function assertOAuthBoundaryMatchesEvidence(evidence, current) {
|
|
1853
|
-
const expected = {
|
|
1854
|
-
agent: evidence.oauth_agent_directory_identity,
|
|
1855
|
-
auth: evidence.oauth_auth_file_identity,
|
|
1856
|
-
models: evidence.oauth_models_file_identity,
|
|
1857
|
-
entries: evidence.oauth_directory_entries,
|
|
1858
|
-
};
|
|
1859
|
-
const observed = {
|
|
1860
|
-
agent: current.agent_directory_identity,
|
|
1861
|
-
auth: current.auth_file_identity,
|
|
1862
|
-
models: current.models_file_identity,
|
|
1863
|
-
entries: current.directory_entries,
|
|
1864
|
-
};
|
|
1865
|
-
if (qualificationCanonicalJson(expected) !== qualificationCanonicalJson(observed)) {
|
|
1866
|
-
throw new Error("qualification OAuth credential occurrence changed between auth check and launch");
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
function qualificationCredentialRedactionValues(env) {
|
|
1870
|
-
const agentDirectory = env.PI_CODING_AGENT_DIR ?? (env.HOME ? join(env.HOME, ".pi", "agent") : undefined);
|
|
1871
|
-
if (!agentDirectory)
|
|
1872
|
-
return [];
|
|
1873
|
-
let auth;
|
|
1874
|
-
try {
|
|
1875
|
-
auth = JSON.parse(readPrivateRegularFile(join(agentDirectory, "auth.json"), "qualification OAuth auth.json").toString("utf8"));
|
|
1876
|
-
}
|
|
1877
|
-
catch {
|
|
1878
|
-
throw new Error("qualification OAuth credential values could not be reopened for output redaction");
|
|
1879
|
-
}
|
|
1880
|
-
const credential = plainObject(auth) ? auth["openai-codex"] : undefined;
|
|
1881
|
-
const values = [];
|
|
1882
|
-
const visit = (value) => {
|
|
1883
|
-
if (typeof value === "string" && value.length >= 8)
|
|
1884
|
-
values.push(value);
|
|
1885
|
-
else if (Array.isArray(value))
|
|
1886
|
-
value.forEach(visit);
|
|
1887
|
-
else if (plainObject(value))
|
|
1888
|
-
Object.values(value).forEach(visit);
|
|
1889
|
-
};
|
|
1890
|
-
visit(credential);
|
|
1891
|
-
return values;
|
|
1892
|
-
}
|
|
1893
|
-
function assertRunningQualificationRunner(mode, pin) {
|
|
1894
|
-
if (mode !== "production")
|
|
1895
|
-
return;
|
|
1896
|
-
const script = process.argv[1];
|
|
1897
|
-
if (!script || realpathSync(script) !== verifyQualificationExecutable(pin).realpath) {
|
|
1898
|
-
throw new Error("qualification production core must run through the pinned source-built runner executable");
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
function assertQualificationExecutableIdentity(invocation, current) {
|
|
1902
|
-
if (qualificationCanonicalJson(current) !== qualificationCanonicalJson(invocation.execution.executable_identity)) {
|
|
1903
|
-
throw new Error(`qualification executable filesystem identity changed after preparation for ${invocation.invocation_id}`);
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
function assertQualificationWorkingDirectory(invocation) {
|
|
1907
|
-
let stat;
|
|
1908
|
-
try {
|
|
1909
|
-
stat = lstatSync(invocation.scenario.working_directory);
|
|
1910
|
-
}
|
|
1911
|
-
catch {
|
|
1912
|
-
throw new Error(`qualification working directory disappeared after preparation for ${invocation.invocation_id}`);
|
|
1913
|
-
}
|
|
1914
|
-
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
1915
|
-
throw new Error(`qualification working directory changed type after preparation for ${invocation.invocation_id}`);
|
|
1916
|
-
const current = { realpath: realpathSync(invocation.scenario.working_directory), device: stat.dev, inode: stat.ino };
|
|
1917
|
-
if (qualificationCanonicalJson(current) !== qualificationCanonicalJson(invocation.execution.working_directory_identity)) {
|
|
1918
|
-
throw new Error(`qualification working directory identity changed after preparation for ${invocation.invocation_id}`);
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
function assertInputUnchanged(invocation) {
|
|
1922
|
-
assertRegularFile(invocation.scenario.input_path, `qualification input ${invocation.invocation_id}`);
|
|
1923
|
-
const digest = qualificationSha256(readFileSync(invocation.scenario.input_path));
|
|
1924
|
-
if (digest !== invocation.scenario.input_sha256)
|
|
1925
|
-
throw new Error(`qualification input changed after preparation for ${invocation.invocation_id}`);
|
|
1926
|
-
}
|
|
1927
|
-
function assertChildEnvironment(env, allowedNames) {
|
|
1928
|
-
const allowed = new Set(allowedNames);
|
|
1929
|
-
const extra = Object.keys(env).find((name) => !allowed.has(name));
|
|
1930
|
-
if (extra)
|
|
1931
|
-
throw new Error(`qualification child environment contains undeclared name ${extra}`);
|
|
1932
|
-
// Re-run the policy over the already-sanitized map so a caller cannot bypass
|
|
1933
|
-
// the parent-boundary function by invoking the supervisor directly.
|
|
1934
|
-
sanitizeQualificationEnvironment(env, allowedNames, "refuse");
|
|
1935
|
-
}
|
|
1936
|
-
async function withAsyncLock(path, action, timeoutMs = 5000) {
|
|
1937
|
-
const deadline = Date.now() + timeoutMs;
|
|
1938
|
-
const token = randomBytes(16).toString("hex");
|
|
1939
|
-
const io = { read: readCanonicalJson, write: atomicWriteCanonical };
|
|
1940
|
-
while (true) {
|
|
1941
|
-
if (tryPublishQualificationLock(path, token, io))
|
|
1942
|
-
break;
|
|
1943
|
-
const owner = readQualificationLockOwner(path, io);
|
|
1944
|
-
if (!owner)
|
|
1945
|
-
continue;
|
|
1946
|
-
if (!qualificationLockOwnerIsLive(owner)) {
|
|
1947
|
-
reclaimQualificationLock(path, owner.token, token, io);
|
|
1948
|
-
continue;
|
|
1949
|
-
}
|
|
1950
|
-
if (Date.now() >= deadline)
|
|
1951
|
-
throw new Error(`qualification state lock timed out: ${path}`);
|
|
1952
|
-
await sleep(10);
|
|
1953
|
-
}
|
|
1954
|
-
try {
|
|
1955
|
-
return await action();
|
|
1956
|
-
}
|
|
1957
|
-
finally {
|
|
1958
|
-
releaseQualificationLock(path, token, io);
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
function filesystemOccurrence(path, stat) {
|
|
1962
|
-
return { realpath: realpathSync(path), device: stat.dev, inode: stat.ino, mtime_ms: stat.mtimeMs, bytes: stat.size };
|
|
1963
|
-
}
|
|
1964
|
-
function readPrivateRegularFileOccurrence(path, ctx) {
|
|
1965
|
-
let fd;
|
|
1966
|
-
try {
|
|
1967
|
-
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1968
|
-
const stat = fstatSync(fd);
|
|
1969
|
-
const pathStat = lstatSync(path);
|
|
1970
|
-
if (!stat.isFile() || pathStat.isSymbolicLink() || stat.dev !== pathStat.dev || stat.ino !== pathStat.ino)
|
|
1971
|
-
throw new Error(`${ctx} must be a stable regular non-symlink file`);
|
|
1972
|
-
if (process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())) {
|
|
1973
|
-
throw new Error(`${ctx} must be private and owned by the qualification user`);
|
|
1974
|
-
}
|
|
1975
|
-
const bytes = readFileSync(fd);
|
|
1976
|
-
const finalStat = lstatSync(path);
|
|
1977
|
-
if (finalStat.isSymbolicLink() || finalStat.dev !== stat.dev || finalStat.ino !== stat.ino || finalStat.mtimeMs !== stat.mtimeMs || finalStat.size !== stat.size) {
|
|
1978
|
-
throw new Error(`${ctx} changed while it was opened`);
|
|
1979
|
-
}
|
|
1980
|
-
return { bytes, identity: filesystemOccurrence(path, stat) };
|
|
1981
|
-
}
|
|
1982
|
-
catch (error) {
|
|
1983
|
-
if (error instanceof Error && error.message.startsWith(ctx))
|
|
1984
|
-
throw error;
|
|
1985
|
-
throw new Error(`${ctx} is missing, unreadable, or a symlink`);
|
|
1986
|
-
}
|
|
1987
|
-
finally {
|
|
1988
|
-
if (fd !== undefined)
|
|
1989
|
-
closeSync(fd);
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
function readPrivateRegularFile(path, ctx) {
|
|
1993
|
-
return readPrivateRegularFileOccurrence(path, ctx).bytes;
|
|
1994
|
-
}
|
|
1995
|
-
function assertRegularFile(path, ctx) {
|
|
1996
|
-
let stat;
|
|
1997
|
-
try {
|
|
1998
|
-
stat = lstatSync(path);
|
|
1999
|
-
}
|
|
2000
|
-
catch {
|
|
2001
|
-
throw new Error(`${ctx} is missing`);
|
|
2002
|
-
}
|
|
2003
|
-
if (stat.isSymbolicLink() || !stat.isFile())
|
|
2004
|
-
throw new Error(`${ctx} must be a regular non-symlink file`);
|
|
2005
|
-
}
|
|
2006
|
-
function processIdentityFrom(value) {
|
|
2007
|
-
if (!plainObject(value) || !Number.isInteger(value.pid) || typeof value.platform !== "string" ||
|
|
2008
|
-
(value.boot_id !== null && typeof value.boot_id !== "string") || (value.start_ticks !== null && typeof value.start_ticks !== "string"))
|
|
2009
|
-
return null;
|
|
2010
|
-
return { pid: Number(value.pid), platform: value.platform, boot_id: value.boot_id, start_ticks: value.start_ticks };
|
|
2011
|
-
}
|
|
2012
|
-
function plainObject(value) {
|
|
2013
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2014
|
-
return false;
|
|
2015
|
-
const prototype = Object.getPrototypeOf(value);
|
|
2016
|
-
return prototype === Object.prototype || prototype === null;
|
|
2017
|
-
}
|
|
2018
|
-
function exactKeys(value, keys, ctx) {
|
|
2019
|
-
const allowed = new Set(keys);
|
|
2020
|
-
const unknown = Object.keys(value).find((key) => !allowed.has(key));
|
|
2021
|
-
if (unknown)
|
|
2022
|
-
throw new Error(`${ctx} contains unknown field ${unknown}`);
|
|
2023
|
-
const missing = keys.find((key) => !Object.hasOwn(value, key));
|
|
2024
|
-
if (missing)
|
|
2025
|
-
throw new Error(`${ctx} is missing field ${missing}`);
|
|
2026
|
-
}
|
|
2027
|
-
function stringValue(value) { return typeof value === "string" ? value : undefined; }
|
|
2028
|
-
function timestamp(value, ctx) {
|
|
2029
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(value) || !Number.isFinite(Date.parse(value)))
|
|
2030
|
-
throw new Error(`${ctx} is not an RFC 3339 UTC timestamp`);
|
|
2031
|
-
return value;
|
|
2032
|
-
}
|
|
2033
|
-
function safeError(error) {
|
|
2034
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2035
|
-
return message.replace(/(?:sk|key|token)-[A-Za-z0-9._-]+/gi, "[REDACTED]").slice(0, 2048);
|
|
2036
|
-
}
|
|
2037
|
-
//# sourceMappingURL=qualification-runner.js.map
|