engineering-behavior-observatory 0.2.1
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/LICENSE +202 -0
- package/README.md +151 -0
- package/contracts/codex-app-server-0.150.1/manifest.json +25 -0
- package/contracts/codex-app-server-0.150.1/schema/InitializeParams.json +84 -0
- package/contracts/codex-app-server-0.150.1/schema/JSONRPCMessage.json +137 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/ThreadReadParams.json +17 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/TurnInterruptParams.json +17 -0
- package/contracts/codex-app-server-0.150.1/types/AskForApproval.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/ClientNotification.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/SandboxMode.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/ThreadReadParams.ts +9 -0
- package/contracts/codex-app-server-0.150.1/types/TokenUsageBreakdown.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/TurnInterruptParams.ts +5 -0
- package/contracts/codex-app-server-0.153.4/manifest.json +27 -0
- package/contracts/codex-app-server-0.153.4/schema/JSONRPCMessage.json +137 -0
- package/contracts/codex-app-server-0.153.4/schema/v1/InitializeParams.json +84 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadReadParams.json +17 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadStartParams.json +515 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/TurnInterruptParams.json +17 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/TurnStartParams.json +921 -0
- package/contracts/codex-app-server-0.153.4/types/AskForApproval.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/ClientNotification.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/SandboxMode.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/ThreadReadParams.ts +12 -0
- package/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.ts +5 -0
- package/contracts/openhands-agent-server-v1.44.1.json +65 -0
- package/contracts/openhands-agent-server-v1.46.0.json +46 -0
- package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.d.ts +9 -0
- package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.d.ts +3 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.d.ts +1 -0
- package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.d.ts +10 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.d.ts +8 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.d.ts +4 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.js +2 -0
- package/dist/src/agent-sdk-normalizer.d.ts +22 -0
- package/dist/src/agent-sdk-normalizer.js +995 -0
- package/dist/src/agent-sdk-run.d.ts +31 -0
- package/dist/src/agent-sdk-run.js +242 -0
- package/dist/src/agent-sdk-runner.d.ts +96 -0
- package/dist/src/agent-sdk-runner.js +407 -0
- package/dist/src/agent-sdk.d.ts +294 -0
- package/dist/src/agent-sdk.js +644 -0
- package/dist/src/aggregation.d.ts +183 -0
- package/dist/src/aggregation.js +513 -0
- package/dist/src/artifacts.d.ts +36 -0
- package/dist/src/artifacts.js +1372 -0
- package/dist/src/atlas-grafana.d.ts +377 -0
- package/dist/src/atlas-grafana.js +47 -0
- package/dist/src/atlas-html.d.ts +2 -0
- package/dist/src/atlas-html.js +34 -0
- package/dist/src/atlas.d.ts +130 -0
- package/dist/src/atlas.js +235 -0
- package/dist/src/behavior-assertions.d.ts +95 -0
- package/dist/src/behavior-assertions.js +154 -0
- package/dist/src/cli.d.ts +5 -0
- package/dist/src/cli.js +957 -0
- package/dist/src/codex-judge.d.ts +44 -0
- package/dist/src/codex-judge.js +214 -0
- package/dist/src/codex-run.d.ts +114 -0
- package/dist/src/codex-run.js +519 -0
- package/dist/src/codex.d.ts +187 -0
- package/dist/src/codex.js +1028 -0
- package/dist/src/contracts.d.ts +155 -0
- package/dist/src/contracts.js +866 -0
- package/dist/src/corpus.d.ts +67 -0
- package/dist/src/corpus.js +540 -0
- package/dist/src/cursor-sdk-runner.d.ts +81 -0
- package/dist/src/cursor-sdk-runner.js +362 -0
- package/dist/src/cursor-sdk.d.ts +81 -0
- package/dist/src/cursor-sdk.js +1078 -0
- package/dist/src/deepseek-adapter.d.ts +152 -0
- package/dist/src/deepseek-adapter.js +777 -0
- package/dist/src/exports.d.ts +69 -0
- package/dist/src/exports.js +966 -0
- package/dist/src/human-calibration.d.ts +167 -0
- package/dist/src/human-calibration.js +618 -0
- package/dist/src/index.d.ts +71 -0
- package/dist/src/index.js +38 -0
- package/dist/src/lifecycle.d.ts +196 -0
- package/dist/src/lifecycle.js +1889 -0
- package/dist/src/normalization-integrity.d.ts +128 -0
- package/dist/src/normalization-integrity.js +429 -0
- package/dist/src/openhands-run.d.ts +36 -0
- package/dist/src/openhands-run.js +415 -0
- package/dist/src/openhands.d.ts +130 -0
- package/dist/src/openhands.js +859 -0
- package/dist/src/pi.d.ts +203 -0
- package/dist/src/pi.js +1345 -0
- package/dist/src/process-protocol.d.ts +245 -0
- package/dist/src/process-protocol.js +1261 -0
- package/dist/src/retained-evidence.d.ts +12 -0
- package/dist/src/retained-evidence.js +201 -0
- package/dist/src/run-bundles.d.ts +193 -0
- package/dist/src/run-bundles.js +993 -0
- package/dist/src/scheduler.d.ts +109 -0
- package/dist/src/scheduler.js +1080 -0
- package/dist/src/semantic-judge.d.ts +194 -0
- package/dist/src/semantic-judge.js +875 -0
- package/dist/src/structural-observations.d.ts +124 -0
- package/dist/src/structural-observations.js +671 -0
- package/dist/src/task-packets.d.ts +115 -0
- package/dist/src/task-packets.js +683 -0
- package/dist/src/uniform-events.d.ts +120 -0
- package/dist/src/uniform-events.js +158 -0
- package/dist/src/verifiers.d.ts +116 -0
- package/dist/src/verifiers.js +819 -0
- package/dist/src/workspaces.d.ts +56 -0
- package/dist/src/workspaces.js +1314 -0
- package/docs/README.md +60 -0
- package/docs/development/README.md +71 -0
- package/docs/development/documentation-sync.md +58 -0
- package/docs/development/extension-contracts.md +307 -0
- package/docs/evaluation/README.md +48 -0
- package/docs/evaluation/aggregation.md +112 -0
- package/docs/evaluation/behavior-assertions.md +45 -0
- package/docs/evaluation/human-calibration.md +170 -0
- package/docs/evaluation/normalization-integrity.md +80 -0
- package/docs/evaluation/semantic-judge.md +188 -0
- package/docs/evaluation/structural-observations.md +84 -0
- package/docs/evaluation/uniform-events.md +131 -0
- package/docs/guides/atlas.md +144 -0
- package/docs/guides/evidence-and-sharing.md +100 -0
- package/docs/guides/operator-guide.md +424 -0
- package/docs/guides/quickstart.md +152 -0
- package/docs/guides/telemetry.md +66 -0
- package/docs/harnesses/README.md +31 -0
- package/docs/harnesses/claude-agent-sdk.md +61 -0
- package/docs/harnesses/codex-harness.md +174 -0
- package/docs/harnesses/cursor-sdk.md +149 -0
- package/docs/harnesses/deepseek-harness.md +134 -0
- package/docs/harnesses/openhands-agent-server.md +153 -0
- package/docs/harnesses/pi-sdk.md +66 -0
- package/docs/reference/README.md +19 -0
- package/docs/reference/agent-sdk-operational-runner.md +118 -0
- package/docs/reference/cli.md +114 -0
- package/docs/reference/contracts.md +222 -0
- package/docs/reference/run-bundle-contract.md +354 -0
- package/docs/reference/run-lifecycle.md +54 -0
- package/examples/cursor-sdk/README.md +11 -0
- package/examples/cursor-sdk/capture-profile.json +1 -0
- package/examples/cursor-sdk/harness.json +1 -0
- package/examples/cursor-sdk/model.json +1 -0
- package/examples/cursor-sdk/native-limits.json +1 -0
- package/examples/cursor-sdk/native-tool-policy.json +1 -0
- package/ontology/behavior-categories.v1.json +46 -0
- package/package.json +66 -0
- package/release/0.1.0/KNOWN_LIMITATIONS.md +35 -0
- package/release/0.1.0/README.md +46 -0
- package/release/0.1.0/reproducibility.json +61 -0
- package/release/0.2.0/KNOWN_LIMITATIONS.md +40 -0
- package/release/0.2.0/README.md +42 -0
- package/release/0.2.0/reproducibility.json +73 -0
- package/release/0.2.1/KNOWN_LIMITATIONS.md +18 -0
- package/release/0.2.1/README.md +63 -0
- package/release/0.2.1/reproducibility.json +73 -0
- package/release/README.md +13 -0
- package/schemas/aggregation.v1.json +293 -0
- package/schemas/behavior-assertions.v1.json +220 -0
- package/schemas/deepseek-runtime-composition.v1.schema.json +122 -0
- package/schemas/experiment.v1.schema.json +188 -0
- package/schemas/human-calibration.v1.json +336 -0
- package/schemas/normalization-integrity.v1.json +302 -0
- package/schemas/run-bundles/v1.json +1040 -0
- package/schemas/run-queue.v1.schema.json +206 -0
- package/schemas/semantic-judge.v1.json +380 -0
- package/schemas/structural-observations.v1.json +131 -0
- package/schemas/task-packet-freeze.v1.schema.json +106 -0
- package/schemas/task-packet.v1.schema.json +234 -0
- package/schemas/uniform-events/v1.json +289 -0
- package/scripts/atlas-grafana.sh +18 -0
|
@@ -0,0 +1,1372 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { link, lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
7
|
+
import * as formats from "ajv-formats";
|
|
8
|
+
import { closeBundleRoot, isSafeArtifactRelativePath, openBundleRoot, removeInterruptedPublicationLink, resolveBundleArtifact, } from "./contracts.js";
|
|
9
|
+
const addFormats = formats.default;
|
|
10
|
+
const schemaDirectory = fileURLToPath(new URL("../../schemas/", import.meta.url));
|
|
11
|
+
const runBundleSchemaId = "urn:ebo:schema:run-bundle:v1";
|
|
12
|
+
const validators = loadValidators();
|
|
13
|
+
const RESERVED_MANIFEST_PATHS = new Set(["manifest.json"]);
|
|
14
|
+
export const SUPPORTED_ARTIFACT_SCHEMA_VERSIONS = [...validators.keys()];
|
|
15
|
+
export function canonicalizeMetadata(value) {
|
|
16
|
+
return canonicalJson(value, new Set());
|
|
17
|
+
}
|
|
18
|
+
export function digestBytes(bytes) {
|
|
19
|
+
return { algorithm: "sha256", value: createHash("sha256").update(bytes).digest("hex") };
|
|
20
|
+
}
|
|
21
|
+
export function digestMetadata(metadata) {
|
|
22
|
+
return digestBytes(Buffer.from(canonicalizeMetadata(metadata)));
|
|
23
|
+
}
|
|
24
|
+
export function verifyDigest(bytes, expected) {
|
|
25
|
+
const actual = digestBytes(bytes);
|
|
26
|
+
return expected.algorithm === actual.algorithm && expected.value === actual.value;
|
|
27
|
+
}
|
|
28
|
+
export function assertNoDuplicateJsonKeys(text) {
|
|
29
|
+
let index = 0;
|
|
30
|
+
const skipWhitespace = () => {
|
|
31
|
+
while (/\s/.test(text[index] ?? ""))
|
|
32
|
+
index += 1;
|
|
33
|
+
};
|
|
34
|
+
const readString = () => {
|
|
35
|
+
const start = index;
|
|
36
|
+
index += 1;
|
|
37
|
+
while (index < text.length) {
|
|
38
|
+
if (text[index] === "\\")
|
|
39
|
+
index += 2;
|
|
40
|
+
else if (text[index++] === '"')
|
|
41
|
+
return text.slice(start, index);
|
|
42
|
+
}
|
|
43
|
+
throw new Error("Verifier output contains an unterminated JSON string.");
|
|
44
|
+
};
|
|
45
|
+
const readValue = () => {
|
|
46
|
+
skipWhitespace();
|
|
47
|
+
if (text[index] === "{")
|
|
48
|
+
readObject();
|
|
49
|
+
else if (text[index] === "[")
|
|
50
|
+
readArray();
|
|
51
|
+
else if (text[index] === '"')
|
|
52
|
+
readString();
|
|
53
|
+
else
|
|
54
|
+
while (index < text.length && !/[\s,\]}]/.test(text[index]))
|
|
55
|
+
index += 1;
|
|
56
|
+
};
|
|
57
|
+
const readObject = () => {
|
|
58
|
+
index += 1;
|
|
59
|
+
skipWhitespace();
|
|
60
|
+
const keys = new Set();
|
|
61
|
+
if (text[index] === "}") {
|
|
62
|
+
index += 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
while (index < text.length) {
|
|
66
|
+
skipWhitespace();
|
|
67
|
+
const key = JSON.parse(readString());
|
|
68
|
+
if (keys.has(key))
|
|
69
|
+
throw new Error("Verifier output contains duplicate JSON object keys.");
|
|
70
|
+
keys.add(key);
|
|
71
|
+
skipWhitespace();
|
|
72
|
+
index += 1;
|
|
73
|
+
readValue();
|
|
74
|
+
skipWhitespace();
|
|
75
|
+
if (text[index] === "}") {
|
|
76
|
+
index += 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
index += 1;
|
|
80
|
+
}
|
|
81
|
+
throw new Error("Verifier output contains an unterminated JSON object.");
|
|
82
|
+
};
|
|
83
|
+
const readArray = () => {
|
|
84
|
+
index += 1;
|
|
85
|
+
skipWhitespace();
|
|
86
|
+
if (text[index] === "]") {
|
|
87
|
+
index += 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
while (index < text.length) {
|
|
91
|
+
readValue();
|
|
92
|
+
skipWhitespace();
|
|
93
|
+
if (text[index] === "]") {
|
|
94
|
+
index += 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
index += 1;
|
|
98
|
+
}
|
|
99
|
+
throw new Error("Verifier output contains an unterminated JSON array.");
|
|
100
|
+
};
|
|
101
|
+
readValue();
|
|
102
|
+
skipWhitespace();
|
|
103
|
+
}
|
|
104
|
+
export function assertUniqueArtifactIdentities(identities) {
|
|
105
|
+
const ids = new Set();
|
|
106
|
+
const paths = new Set();
|
|
107
|
+
const ancestorPaths = new Set();
|
|
108
|
+
for (const identity of identities) {
|
|
109
|
+
if (identity.id.trim() === "" || ids.has(identity.id)) {
|
|
110
|
+
throw new Error(`Duplicate artifact identity "${identity.id}".`);
|
|
111
|
+
}
|
|
112
|
+
if (!isSafeArtifactRelativePath(identity.relativePath)) {
|
|
113
|
+
throw new Error(`Artifact path "${identity.relativePath}" is unsafe.`);
|
|
114
|
+
}
|
|
115
|
+
const normalizedPath = identity.relativePath.toLowerCase();
|
|
116
|
+
if (paths.has(normalizedPath) || ancestorPaths.has(normalizedPath)) {
|
|
117
|
+
throw new Error(`Artifact path "${identity.relativePath}" collides with another artifact path.`);
|
|
118
|
+
}
|
|
119
|
+
for (let boundary = normalizedPath.lastIndexOf("/"); boundary > 0; boundary = normalizedPath.lastIndexOf("/", boundary - 1)) {
|
|
120
|
+
const ancestor = normalizedPath.slice(0, boundary);
|
|
121
|
+
if (paths.has(ancestor)) {
|
|
122
|
+
throw new Error(`Artifact path "${identity.relativePath}" collides with another artifact path.`);
|
|
123
|
+
}
|
|
124
|
+
ancestorPaths.add(ancestor);
|
|
125
|
+
}
|
|
126
|
+
ids.add(identity.id);
|
|
127
|
+
paths.add(normalizedPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function validateArtifact(artifact, document) {
|
|
131
|
+
const schemaVersion = schemaVersionOf(document);
|
|
132
|
+
const validate = validators.get(schemaVersion);
|
|
133
|
+
if (validate === undefined) {
|
|
134
|
+
return [{ artifact, schemaVersion, field: "/schemaVersion", message: "Unsupported schema version." }];
|
|
135
|
+
}
|
|
136
|
+
const errors = validate(document)
|
|
137
|
+
? []
|
|
138
|
+
: (validate.errors ?? []).map((error) => validationError(artifact, schemaVersion, error));
|
|
139
|
+
if (schemaVersion === "run-manifest/v1" && isRecord(document) && Array.isArray(document.evidence)) {
|
|
140
|
+
errors.push(...runManifestIntegrityErrors(artifact, schemaVersion, document));
|
|
141
|
+
}
|
|
142
|
+
if (schemaVersion === "verifier-result/v1" && isRecord(document) && Array.isArray(document.assertions)) {
|
|
143
|
+
const assertionIds = document.assertions
|
|
144
|
+
.filter((assertion) => isRecord(assertion) && typeof assertion.id === "string")
|
|
145
|
+
.map((assertion) => assertion.id);
|
|
146
|
+
if (new Set(assertionIds).size !== assertionIds.length) {
|
|
147
|
+
errors.push({ artifact, schemaVersion, field: "/assertions", message: "Verifier assertion IDs must be unique." });
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(document.diagnostics)) {
|
|
150
|
+
const diagnosticLocators = document.diagnostics
|
|
151
|
+
.filter((diagnostic) => isRecord(diagnostic) && typeof diagnostic.locator === "string")
|
|
152
|
+
.map((diagnostic) => diagnostic.locator.toLowerCase());
|
|
153
|
+
const diagnosticStreams = document.diagnostics
|
|
154
|
+
.filter((diagnostic) => isRecord(diagnostic) && typeof diagnostic.stream === "string")
|
|
155
|
+
.map((diagnostic) => diagnostic.stream);
|
|
156
|
+
if (new Set(diagnosticStreams).size !== diagnosticStreams.length) {
|
|
157
|
+
errors.push({ artifact, schemaVersion, field: "/diagnostics", message: "Verifier diagnostic streams must be unique." });
|
|
158
|
+
}
|
|
159
|
+
const seenDiagnosticLocators = new Set();
|
|
160
|
+
if (diagnosticLocators.some((locator) => {
|
|
161
|
+
const collision = hasPortablePathCollision(locator, seenDiagnosticLocators);
|
|
162
|
+
seenDiagnosticLocators.add(locator);
|
|
163
|
+
return collision;
|
|
164
|
+
})) {
|
|
165
|
+
errors.push({ artifact, schemaVersion, field: "/diagnostics", message: "Verifier diagnostic locators must be unique and non-overlapping." });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return errors;
|
|
170
|
+
}
|
|
171
|
+
export function validateRunManifestEvidence(artifact, manifest, bundleRoot) {
|
|
172
|
+
if (!isRecord(manifest) || manifest.schemaVersion !== "run-manifest/v1" || !Array.isArray(manifest.evidence))
|
|
173
|
+
return [];
|
|
174
|
+
const errors = [];
|
|
175
|
+
const expectedBundleId = typeof manifest.bundleId === "string" ? manifest.bundleId : undefined;
|
|
176
|
+
const expectedVerifier = isRecord(manifest.run) && isRecord(manifest.run.verifier)
|
|
177
|
+
? manifest.run.verifier
|
|
178
|
+
: undefined;
|
|
179
|
+
const workspaceEvidence = new Map(manifest.evidence
|
|
180
|
+
.filter((entry) => isRecord(entry)
|
|
181
|
+
&& typeof entry.id === "string" && entry.kind === "workspace")
|
|
182
|
+
.map((entry) => [entry.id, entry]));
|
|
183
|
+
const verifierOutcomes = [];
|
|
184
|
+
const verifierResults = new Map();
|
|
185
|
+
const evidenceByPath = new Map(manifest.evidence
|
|
186
|
+
.filter((entry) => isRecord(entry)
|
|
187
|
+
&& typeof entry.relativePath === "string")
|
|
188
|
+
.map((entry) => [entry.relativePath.toLowerCase(), entry]));
|
|
189
|
+
const evidencePaths = new Set(manifest.evidence
|
|
190
|
+
.filter((entry) => isRecord(entry) && typeof entry.relativePath === "string")
|
|
191
|
+
.map((entry) => entry.relativePath.toLowerCase()));
|
|
192
|
+
const nestedDiagnosticPaths = new Set();
|
|
193
|
+
for (const descriptor of manifest.evidence) {
|
|
194
|
+
if (!isRecord(descriptor) || typeof descriptor.id !== "string" || typeof descriptor.relativePath !== "string"
|
|
195
|
+
|| typeof descriptor.digest !== "string" || typeof descriptor.sizeBytes !== "number")
|
|
196
|
+
continue;
|
|
197
|
+
try {
|
|
198
|
+
const bytes = resolveBundleArtifact(bundleRoot, {
|
|
199
|
+
locator: descriptor.relativePath,
|
|
200
|
+
digest: runDigest(descriptor.digest),
|
|
201
|
+
});
|
|
202
|
+
if (bytes.length !== descriptor.sizeBytes)
|
|
203
|
+
throw new Error("Artifact size does not match its manifest descriptor.");
|
|
204
|
+
if (descriptor.kind === "verifier") {
|
|
205
|
+
const verifierErrors = nestedVerifierDiagnosticErrors(artifact, descriptor.id, bytes, bundleRoot, expectedBundleId, expectedVerifier, workspaceEvidence, descriptor.sanitizedFrom !== undefined, isRecord(descriptor.sanitizedFrom) && typeof descriptor.sanitizedFrom.artifactId === "string"
|
|
206
|
+
? descriptor.sanitizedFrom.artifactId
|
|
207
|
+
: undefined, evidenceByPath, evidencePaths, nestedDiagnosticPaths);
|
|
208
|
+
errors.push(...verifierErrors);
|
|
209
|
+
if (verifierErrors.length === 0) {
|
|
210
|
+
const outcome = nestedVerifierOutcome(bytes);
|
|
211
|
+
if (outcome !== undefined) {
|
|
212
|
+
verifierResults.set(descriptor.id, outcome);
|
|
213
|
+
if (descriptor.sanitizedFrom === undefined)
|
|
214
|
+
verifierOutcomes.push(outcome);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
errors.push({
|
|
221
|
+
artifact,
|
|
222
|
+
schemaVersion: "run-manifest/v1",
|
|
223
|
+
field: `/evidence/${escapeJsonPointer(descriptor.id)}`,
|
|
224
|
+
message: error instanceof Error ? error.message : "Evidence could not be verified.",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const descriptor of manifest.evidence) {
|
|
229
|
+
if (!isRecord(descriptor) || descriptor.kind !== "verifier" || !isRecord(descriptor.sanitizedFrom)
|
|
230
|
+
|| typeof descriptor.id !== "string" || typeof descriptor.sanitizedFrom.artifactId !== "string")
|
|
231
|
+
continue;
|
|
232
|
+
const source = verifierResults.get(descriptor.sanitizedFrom.artifactId);
|
|
233
|
+
const derivative = verifierResults.get(descriptor.id);
|
|
234
|
+
if (source === undefined || derivative === undefined)
|
|
235
|
+
continue;
|
|
236
|
+
if (source.status !== derivative.status) {
|
|
237
|
+
errors.push({
|
|
238
|
+
artifact,
|
|
239
|
+
schemaVersion: "run-manifest/v1",
|
|
240
|
+
field: `/evidence/${escapeJsonPointer(descriptor.id)}/status`,
|
|
241
|
+
message: "Sanitized verifier must preserve its source outcome status.",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const sourceAssertions = new Map(source.assertions.map((assertion) => [assertion.id, assertion.status]));
|
|
245
|
+
const derivativeAssertions = new Map(derivative.assertions.map((assertion) => [assertion.id, assertion.status]));
|
|
246
|
+
if (sourceAssertions.size !== derivativeAssertions.size
|
|
247
|
+
|| [...sourceAssertions].some(([id, status]) => derivativeAssertions.get(id) !== status)) {
|
|
248
|
+
errors.push({
|
|
249
|
+
artifact,
|
|
250
|
+
schemaVersion: "run-manifest/v1",
|
|
251
|
+
field: `/evidence/${escapeJsonPointer(descriptor.id)}/assertions`,
|
|
252
|
+
message: "Sanitized verifier must preserve source assertion outcomes.",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (derivative.exitCode !== source.exitCode) {
|
|
256
|
+
errors.push({
|
|
257
|
+
artifact,
|
|
258
|
+
schemaVersion: "run-manifest/v1",
|
|
259
|
+
field: `/evidence/${escapeJsonPointer(descriptor.id)}/exitCode`,
|
|
260
|
+
message: "Sanitized verifier must preserve its source exit code.",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const sourceVerifier = source.verifier;
|
|
264
|
+
const derivativeVerifier = derivative.verifier;
|
|
265
|
+
const sameVerifier = sourceVerifier === undefined
|
|
266
|
+
? derivativeVerifier === undefined
|
|
267
|
+
: derivativeVerifier !== undefined
|
|
268
|
+
&& derivativeVerifier.locator === sourceVerifier.locator
|
|
269
|
+
&& derivativeVerifier.digest === sourceVerifier.digest
|
|
270
|
+
&& derivativeVerifier.format === sourceVerifier.format;
|
|
271
|
+
if (!sameVerifier) {
|
|
272
|
+
errors.push({
|
|
273
|
+
artifact,
|
|
274
|
+
schemaVersion: "run-manifest/v1",
|
|
275
|
+
field: `/evidence/${escapeJsonPointer(descriptor.id)}/verifier`,
|
|
276
|
+
message: "Sanitized verifier must preserve its source verifier binding.",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (source.durationMs !== derivative.durationMs) {
|
|
280
|
+
errors.push({
|
|
281
|
+
artifact,
|
|
282
|
+
schemaVersion: "run-manifest/v1",
|
|
283
|
+
field: `/evidence/${escapeJsonPointer(descriptor.id)}/durationMs`,
|
|
284
|
+
message: "Sanitized verifier must preserve its source duration.",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const sameError = derivative.errorRedacted === true
|
|
288
|
+
? source.status === "error" && derivative.error === "[redacted]"
|
|
289
|
+
: source.error === derivative.error && source.errorRedacted === derivative.errorRedacted;
|
|
290
|
+
if (!sameError) {
|
|
291
|
+
errors.push({
|
|
292
|
+
artifact,
|
|
293
|
+
schemaVersion: "run-manifest/v1",
|
|
294
|
+
field: `/evidence/${escapeJsonPointer(descriptor.id)}/error`,
|
|
295
|
+
message: "Sanitized verifier must preserve or explicitly redact its source error.",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const sourceWorkspace = source.workspace;
|
|
299
|
+
const derivativeWorkspace = derivative.workspace;
|
|
300
|
+
const sameWorkspace = sourceWorkspace === undefined
|
|
301
|
+
? derivativeWorkspace === undefined
|
|
302
|
+
: derivativeWorkspace !== undefined
|
|
303
|
+
&& derivativeWorkspace.artifactId === sourceWorkspace.artifactId
|
|
304
|
+
&& derivativeWorkspace.digest === sourceWorkspace.digest
|
|
305
|
+
&& derivativeWorkspace.fingerprint === sourceWorkspace.fingerprint;
|
|
306
|
+
if (!sameWorkspace) {
|
|
307
|
+
errors.push({
|
|
308
|
+
artifact,
|
|
309
|
+
schemaVersion: "run-manifest/v1",
|
|
310
|
+
field: `/evidence/${escapeJsonPointer(descriptor.id)}/workspace`,
|
|
311
|
+
message: "Sanitized verifier must preserve its source workspace binding.",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (derivative.diagnostics.some((diagnostic) => diagnostic.source === undefined
|
|
315
|
+
|| !source.diagnostics.some((sourceDiagnostic) => sameDiagnosticOrigin(sourceDiagnostic, diagnostic.source)))) {
|
|
316
|
+
errors.push({
|
|
317
|
+
artifact,
|
|
318
|
+
schemaVersion: "run-manifest/v1",
|
|
319
|
+
field: `/evidence/${escapeJsonPointer(descriptor.id)}/diagnostics`,
|
|
320
|
+
message: "Sanitized verifier diagnostics must identify source diagnostics.",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const terminal = isRecord(manifest.terminal) ? manifest.terminal : undefined;
|
|
325
|
+
const assessmentMode = isRecord(manifest.run) ? manifest.run.assessmentMode ?? "verified" : undefined;
|
|
326
|
+
const terminalWorkspace = typeof terminal?.workspaceArtifactId === "string"
|
|
327
|
+
? workspaceEvidence.get(terminal.workspaceArtifactId)
|
|
328
|
+
: undefined;
|
|
329
|
+
const matchesTerminalWorkspace = (outcome, status) => outcome.status === status
|
|
330
|
+
&& outcome.workspace !== undefined
|
|
331
|
+
&& terminalWorkspace !== undefined
|
|
332
|
+
&& outcome.workspace.artifactId === terminalWorkspace.id
|
|
333
|
+
&& outcome.workspace.digest === terminalWorkspace.digest
|
|
334
|
+
&& (outcome.workspace.fingerprint === undefined
|
|
335
|
+
? terminalWorkspace.fingerprint === undefined
|
|
336
|
+
: terminalWorkspace.fingerprint !== undefined && outcome.workspace.fingerprint === terminalWorkspace.fingerprint);
|
|
337
|
+
if (assessmentMode === "verified" && terminal?.state === "completed"
|
|
338
|
+
&& !verifierOutcomes.some((outcome) => matchesTerminalWorkspace(outcome, "passed"))) {
|
|
339
|
+
errors.push({
|
|
340
|
+
artifact,
|
|
341
|
+
schemaVersion: "run-manifest/v1",
|
|
342
|
+
field: "/terminal/state",
|
|
343
|
+
message: "Completed runs require a passed verifier bound to the terminal workspace.",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (terminal?.state === "failed" && terminal.failureClass === "task"
|
|
347
|
+
&& !verifierOutcomes.some((outcome) => matchesTerminalWorkspace(outcome, "failed"))) {
|
|
348
|
+
errors.push({
|
|
349
|
+
artifact,
|
|
350
|
+
schemaVersion: "run-manifest/v1",
|
|
351
|
+
field: "/terminal/state",
|
|
352
|
+
message: "Task-failed runs require a failed verifier bound to the terminal workspace.",
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (assessmentMode === "observational" && terminal?.failureClass === "task") {
|
|
356
|
+
errors.push({
|
|
357
|
+
artifact,
|
|
358
|
+
schemaVersion: "run-manifest/v1",
|
|
359
|
+
field: "/terminal/failureClass",
|
|
360
|
+
message: "Observational runs cannot claim task failure.",
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
if (assessmentMode === "observational" && verifierOutcomes.length > 0) {
|
|
364
|
+
errors.push({
|
|
365
|
+
artifact,
|
|
366
|
+
schemaVersion: "run-manifest/v1",
|
|
367
|
+
field: "/evidence",
|
|
368
|
+
message: "Observational runs cannot retain verifier outcome evidence.",
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return errors;
|
|
372
|
+
}
|
|
373
|
+
function isNestedVerifierDiagnostic(value) {
|
|
374
|
+
if (!isRecord(value) || ![5, 6].includes(Object.keys(value).length)
|
|
375
|
+
|| typeof value.stream !== "string" || !["stdout", "stderr"].includes(value.stream)
|
|
376
|
+
|| typeof value.locator !== "string" || typeof value.digest !== "string"
|
|
377
|
+
|| !/^sha256:[a-f0-9]{64}$/.test(value.digest) || !Number.isSafeInteger(value.sizeBytes)
|
|
378
|
+
|| value.sizeBytes < 0 || typeof value.truncated !== "boolean")
|
|
379
|
+
return false;
|
|
380
|
+
return !Object.hasOwn(value, "source") || isNestedVerifierDiagnosticOrigin(value.source);
|
|
381
|
+
}
|
|
382
|
+
function isNestedVerifierDiagnosticOrigin(value) {
|
|
383
|
+
return isRecord(value) && Object.keys(value).length === 5
|
|
384
|
+
&& typeof value.stream === "string" && ["stdout", "stderr"].includes(value.stream)
|
|
385
|
+
&& typeof value.locator === "string" && typeof value.digest === "string"
|
|
386
|
+
&& /^sha256:[a-f0-9]{64}$/.test(value.digest) && Number.isSafeInteger(value.sizeBytes)
|
|
387
|
+
&& value.sizeBytes >= 0 && typeof value.truncated === "boolean";
|
|
388
|
+
}
|
|
389
|
+
function isDiagnosticSourceBinding(value) {
|
|
390
|
+
return isRecord(value) && Object.keys(value).length === 6
|
|
391
|
+
&& typeof value.verifierId === "string"
|
|
392
|
+
&& isNestedVerifierDiagnosticOrigin(Object.fromEntries(Object.entries(value).filter(([key]) => key !== "verifierId")));
|
|
393
|
+
}
|
|
394
|
+
function sameDiagnosticOrigin(left, right) {
|
|
395
|
+
return left.stream === right.stream
|
|
396
|
+
&& left.locator === right.locator
|
|
397
|
+
&& left.digest === right.digest
|
|
398
|
+
&& left.sizeBytes === right.sizeBytes
|
|
399
|
+
&& left.truncated === right.truncated;
|
|
400
|
+
}
|
|
401
|
+
function nestedVerifierOutcome(bytes) {
|
|
402
|
+
try {
|
|
403
|
+
const result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
404
|
+
if (!isRecord(result) || typeof result.status !== "string")
|
|
405
|
+
return undefined;
|
|
406
|
+
const verifier = isRecord(result.verifier)
|
|
407
|
+
&& typeof result.verifier.locator === "string"
|
|
408
|
+
&& typeof result.verifier.digest === "string"
|
|
409
|
+
? {
|
|
410
|
+
locator: result.verifier.locator,
|
|
411
|
+
digest: result.verifier.digest,
|
|
412
|
+
...(typeof result.verifier.format === "string" ? { format: result.verifier.format } : {}),
|
|
413
|
+
}
|
|
414
|
+
: undefined;
|
|
415
|
+
const workspace = isRecord(result.workspace)
|
|
416
|
+
&& typeof result.workspace.artifactId === "string"
|
|
417
|
+
&& typeof result.workspace.digest === "string"
|
|
418
|
+
? {
|
|
419
|
+
artifactId: result.workspace.artifactId,
|
|
420
|
+
digest: result.workspace.digest,
|
|
421
|
+
...(typeof result.workspace.fingerprint === "string" ? { fingerprint: result.workspace.fingerprint } : {}),
|
|
422
|
+
}
|
|
423
|
+
: undefined;
|
|
424
|
+
const assertions = Array.isArray(result.assertions)
|
|
425
|
+
? result.assertions.flatMap((assertion) => isRecord(assertion)
|
|
426
|
+
&& typeof assertion.id === "string" && typeof assertion.status === "string"
|
|
427
|
+
? [{ id: assertion.id, status: assertion.status }]
|
|
428
|
+
: [])
|
|
429
|
+
: [];
|
|
430
|
+
const diagnostics = Array.isArray(result.diagnostics)
|
|
431
|
+
? result.diagnostics.flatMap((diagnostic) => {
|
|
432
|
+
if (!isRecord(diagnostic) || typeof diagnostic.stream !== "string" || typeof diagnostic.locator !== "string"
|
|
433
|
+
|| typeof diagnostic.digest !== "string" || typeof diagnostic.sizeBytes !== "number"
|
|
434
|
+
|| typeof diagnostic.truncated !== "boolean")
|
|
435
|
+
return [];
|
|
436
|
+
const source = isRecord(diagnostic.source)
|
|
437
|
+
&& typeof diagnostic.source.stream === "string"
|
|
438
|
+
&& typeof diagnostic.source.locator === "string"
|
|
439
|
+
&& typeof diagnostic.source.digest === "string"
|
|
440
|
+
&& typeof diagnostic.source.sizeBytes === "number"
|
|
441
|
+
&& typeof diagnostic.source.truncated === "boolean"
|
|
442
|
+
? {
|
|
443
|
+
stream: diagnostic.source.stream,
|
|
444
|
+
locator: diagnostic.source.locator,
|
|
445
|
+
digest: diagnostic.source.digest,
|
|
446
|
+
sizeBytes: diagnostic.source.sizeBytes,
|
|
447
|
+
truncated: diagnostic.source.truncated,
|
|
448
|
+
}
|
|
449
|
+
: undefined;
|
|
450
|
+
return [{
|
|
451
|
+
stream: diagnostic.stream,
|
|
452
|
+
locator: diagnostic.locator,
|
|
453
|
+
digest: diagnostic.digest,
|
|
454
|
+
sizeBytes: diagnostic.sizeBytes,
|
|
455
|
+
truncated: diagnostic.truncated,
|
|
456
|
+
...(source === undefined ? {} : { source }),
|
|
457
|
+
}];
|
|
458
|
+
})
|
|
459
|
+
: [];
|
|
460
|
+
const exitCode = typeof result.exitCode === "number" ? result.exitCode : undefined;
|
|
461
|
+
const durationMs = typeof result.durationMs === "number" ? result.durationMs : undefined;
|
|
462
|
+
const error = typeof result.error === "string" ? result.error : undefined;
|
|
463
|
+
const errorRedacted = typeof result.errorRedacted === "boolean" ? result.errorRedacted : undefined;
|
|
464
|
+
return {
|
|
465
|
+
status: result.status,
|
|
466
|
+
...(durationMs === undefined ? {} : { durationMs }),
|
|
467
|
+
...(error === undefined ? {} : { error }),
|
|
468
|
+
...(errorRedacted === undefined ? {} : { errorRedacted }),
|
|
469
|
+
assertions,
|
|
470
|
+
diagnostics,
|
|
471
|
+
...(verifier === undefined ? {} : { verifier }),
|
|
472
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
473
|
+
...(exitCode === undefined ? {} : { exitCode }),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function nestedVerifierDiagnosticErrors(artifact, verifierId, bytes, bundleRoot, expectedBundleId, expectedVerifier, workspaceEvidence, sanitized, sanitizedSourceVerifierId, evidenceByPath, evidencePaths, nestedDiagnosticPaths) {
|
|
481
|
+
const scope = `/evidence/${escapeJsonPointer(verifierId)}`;
|
|
482
|
+
let result;
|
|
483
|
+
try {
|
|
484
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
485
|
+
result = JSON.parse(text);
|
|
486
|
+
assertNoDuplicateJsonKeys(text);
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
return [{ artifact, schemaVersion: "run-manifest/v1", field: scope, message: `Verifier artifact could not be parsed: ${error instanceof Error ? error.message : "invalid UTF-8 or JSON."}` }];
|
|
490
|
+
}
|
|
491
|
+
const schemaErrors = validateArtifact(`${artifact}/${verifierId}`, result);
|
|
492
|
+
if (schemaErrors.length > 0) {
|
|
493
|
+
return schemaErrors.map((error) => ({ ...error, artifact, schemaVersion: "run-manifest/v1", field: `${scope}${error.field}` }));
|
|
494
|
+
}
|
|
495
|
+
if (!isRecord(result))
|
|
496
|
+
return [];
|
|
497
|
+
const bindingErrors = [];
|
|
498
|
+
if (expectedBundleId !== undefined && result.bundleId !== expectedBundleId) {
|
|
499
|
+
bindingErrors.push({
|
|
500
|
+
artifact,
|
|
501
|
+
schemaVersion: "run-manifest/v1",
|
|
502
|
+
field: `${scope}/bundleId`,
|
|
503
|
+
message: "Verifier bundleId must match its containing run manifest.",
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (expectedVerifier !== undefined) {
|
|
507
|
+
const actualVerifier = isRecord(result.verifier) ? result.verifier : undefined;
|
|
508
|
+
const expectedFormat = typeof expectedVerifier.format === "string"
|
|
509
|
+
? expectedVerifier.format
|
|
510
|
+
: typeof expectedVerifier.locator === "string" && expectedVerifier.locator.toLowerCase().endsWith(".mjs")
|
|
511
|
+
? "module"
|
|
512
|
+
: "commonjs";
|
|
513
|
+
if (actualVerifier === undefined
|
|
514
|
+
|| actualVerifier.locator !== expectedVerifier.locator
|
|
515
|
+
|| actualVerifier.digest !== expectedVerifier.digest
|
|
516
|
+
|| actualVerifier.format !== expectedFormat) {
|
|
517
|
+
bindingErrors.push({
|
|
518
|
+
artifact,
|
|
519
|
+
schemaVersion: "run-manifest/v1",
|
|
520
|
+
field: `${scope}/verifier`,
|
|
521
|
+
message: "Verifier execution reference must match the run configuration.",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (isRecord(result.workspace)) {
|
|
526
|
+
const workspaceArtifactId = result.workspace.artifactId;
|
|
527
|
+
const retainedWorkspace = typeof workspaceArtifactId === "string" ? workspaceEvidence.get(workspaceArtifactId) : undefined;
|
|
528
|
+
if (retainedWorkspace === undefined) {
|
|
529
|
+
bindingErrors.push({
|
|
530
|
+
artifact,
|
|
531
|
+
schemaVersion: "run-manifest/v1",
|
|
532
|
+
field: `${scope}/workspace/artifactId`,
|
|
533
|
+
message: "Verifier workspace must reference retained workspace evidence.",
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
else if (retainedWorkspace.digest !== result.workspace.digest
|
|
537
|
+
|| (result.workspace.fingerprint === undefined
|
|
538
|
+
? retainedWorkspace.fingerprint !== undefined
|
|
539
|
+
: retainedWorkspace.fingerprint === undefined || retainedWorkspace.fingerprint !== result.workspace.fingerprint)) {
|
|
540
|
+
bindingErrors.push({
|
|
541
|
+
artifact,
|
|
542
|
+
schemaVersion: "run-manifest/v1",
|
|
543
|
+
field: `${scope}/workspace/digest`,
|
|
544
|
+
message: "Verifier workspace digest must match retained workspace evidence.",
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (result.diagnostics === undefined)
|
|
549
|
+
return bindingErrors;
|
|
550
|
+
if (!Array.isArray(result.diagnostics)) {
|
|
551
|
+
return [
|
|
552
|
+
...bindingErrors,
|
|
553
|
+
{ artifact, schemaVersion: "run-manifest/v1", field: `${scope}/diagnostics`, message: "Verifier diagnostics must be an array." },
|
|
554
|
+
];
|
|
555
|
+
}
|
|
556
|
+
const errors = [...bindingErrors];
|
|
557
|
+
for (const [index, diagnostic] of result.diagnostics.entries()) {
|
|
558
|
+
if (!isNestedVerifierDiagnostic(diagnostic)) {
|
|
559
|
+
errors.push({ artifact, schemaVersion: "run-manifest/v1", field: `${scope}/diagnostics/${index}`, message: "Verifier diagnostic reference is malformed." });
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const normalizedLocator = diagnostic.locator.toLowerCase();
|
|
563
|
+
const retainedEvidence = evidenceByPath.get(normalizedLocator);
|
|
564
|
+
const exactSidecar = retainedEvidence !== undefined
|
|
565
|
+
&& retainedEvidence.relativePath === diagnostic.locator
|
|
566
|
+
&& retainedEvidence.digest === diagnostic.digest
|
|
567
|
+
&& retainedEvidence.sizeBytes === diagnostic.sizeBytes;
|
|
568
|
+
const invalidUnsanitizedReference = !sanitized && diagnostic.source !== undefined;
|
|
569
|
+
const sourceOrigin = diagnostic.source;
|
|
570
|
+
const sourceBinding = retainedEvidence !== undefined && isDiagnosticSourceBinding(retainedEvidence.diagnosticSource)
|
|
571
|
+
? retainedEvidence.diagnosticSource
|
|
572
|
+
: undefined;
|
|
573
|
+
const sourceBindingMatches = sourceBinding !== undefined
|
|
574
|
+
&& sourceOrigin !== undefined
|
|
575
|
+
&& sourceBinding.verifierId === sanitizedSourceVerifierId
|
|
576
|
+
&& sourceBinding.stream === diagnostic.stream
|
|
577
|
+
&& sourceOrigin.stream === diagnostic.stream
|
|
578
|
+
&& sameDiagnosticOrigin(sourceBinding, sourceOrigin);
|
|
579
|
+
const byteIdentical = sanitized && sourceOrigin !== undefined && diagnostic.digest === sourceOrigin.digest;
|
|
580
|
+
const isSanitizedSidecar = exactSidecar
|
|
581
|
+
&& retainedEvidence.kind === "diagnostic"
|
|
582
|
+
&& retainedEvidence.authority === "outcome"
|
|
583
|
+
&& (retainedEvidence.mediaType === "text/plain" || retainedEvidence.mediaType === "application/octet-stream")
|
|
584
|
+
&& (retainedEvidence.sharingClass === "partner" || retainedEvidence.sharingClass === "public")
|
|
585
|
+
&& isRecord(retainedEvidence.sanitizedFrom)
|
|
586
|
+
&& retainedEvidence.sanitizedFrom.artifactId === sanitizedSourceVerifierId
|
|
587
|
+
&& sourceBindingMatches
|
|
588
|
+
&& !byteIdentical;
|
|
589
|
+
const invalidSanitizedReference = sanitized && !isSanitizedSidecar;
|
|
590
|
+
const aliasesRetainedEvidence = !sanitized && hasPortablePathCollision(normalizedLocator, evidencePaths);
|
|
591
|
+
if (invalidUnsanitizedReference
|
|
592
|
+
|| hasPortablePathCollision(normalizedLocator, RESERVED_MANIFEST_PATHS)
|
|
593
|
+
|| invalidSanitizedReference
|
|
594
|
+
|| aliasesRetainedEvidence
|
|
595
|
+
|| hasPortablePathCollision(normalizedLocator, nestedDiagnosticPaths)) {
|
|
596
|
+
errors.push({
|
|
597
|
+
artifact,
|
|
598
|
+
schemaVersion: "run-manifest/v1",
|
|
599
|
+
field: `${scope}/diagnostics`,
|
|
600
|
+
message: invalidUnsanitizedReference
|
|
601
|
+
? "Unsanitized verifier diagnostics cannot carry source provenance."
|
|
602
|
+
: invalidSanitizedReference
|
|
603
|
+
? "Sanitized verifier diagnostics require classified, source-bound sidecars with changed bytes."
|
|
604
|
+
: "Verifier diagnostics cannot alias retained evidence paths.",
|
|
605
|
+
});
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
nestedDiagnosticPaths.add(normalizedLocator);
|
|
609
|
+
try {
|
|
610
|
+
const diagnosticBytes = resolveBundleArtifact(bundleRoot, {
|
|
611
|
+
locator: diagnostic.locator,
|
|
612
|
+
digest: runDigest(diagnostic.digest),
|
|
613
|
+
});
|
|
614
|
+
if (diagnosticBytes.length !== diagnostic.sizeBytes) {
|
|
615
|
+
throw new Error("Diagnostic size does not match its result reference.");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
errors.push({
|
|
620
|
+
artifact,
|
|
621
|
+
schemaVersion: "run-manifest/v1",
|
|
622
|
+
field: `${scope}/diagnostics`,
|
|
623
|
+
message: error instanceof Error ? error.message : "Verifier diagnostic could not be verified.",
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return errors;
|
|
628
|
+
}
|
|
629
|
+
function hasPortablePathCollision(path, existingPaths) {
|
|
630
|
+
return [...existingPaths].some((existing) => existing === path || existing.startsWith(`${path}/`) || path.startsWith(`${existing}/`));
|
|
631
|
+
}
|
|
632
|
+
export function validateExportManifest(artifact, exportManifest, containingManifest, bundleRoot) {
|
|
633
|
+
if (!isRecord(exportManifest) || exportManifest.schemaVersion !== "export-manifest/v1"
|
|
634
|
+
|| !["ready", "exported"].includes(String(exportManifest.status)))
|
|
635
|
+
return [];
|
|
636
|
+
if (!isRecord(containingManifest) || containingManifest.schemaVersion !== "run-manifest/v1" || !Array.isArray(containingManifest.evidence)) {
|
|
637
|
+
return [{ artifact, schemaVersion: "export-manifest/v1", field: "/bundleId", message: "Ready exports require their containing run manifest." }];
|
|
638
|
+
}
|
|
639
|
+
if (exportManifest.bundleId !== containingManifest.bundleId) {
|
|
640
|
+
return [{ artifact, schemaVersion: "export-manifest/v1", field: "/bundleId", message: "Export bundle ID does not match its containing run manifest." }];
|
|
641
|
+
}
|
|
642
|
+
if (!isRecord(containingManifest.run)
|
|
643
|
+
|| (exportManifest.assessmentMode ?? "verified") !== (containingManifest.run.assessmentMode ?? "verified")) {
|
|
644
|
+
return [{ artifact, schemaVersion: "export-manifest/v1", field: "/assessmentMode", message: "Export assessment mode does not match its containing run manifest." }];
|
|
645
|
+
}
|
|
646
|
+
const evidenceById = new Map();
|
|
647
|
+
for (const descriptor of containingManifest.evidence) {
|
|
648
|
+
if (isRecord(descriptor) && typeof descriptor.id === "string")
|
|
649
|
+
evidenceById.set(descriptor.id, descriptor);
|
|
650
|
+
}
|
|
651
|
+
const errors = [];
|
|
652
|
+
const expectedBundleId = typeof containingManifest.bundleId === "string" ? containingManifest.bundleId : undefined;
|
|
653
|
+
const expectedVerifier = isRecord(containingManifest.run) && isRecord(containingManifest.run.verifier)
|
|
654
|
+
? containingManifest.run.verifier
|
|
655
|
+
: undefined;
|
|
656
|
+
const workspaceEvidence = new Map(containingManifest.evidence
|
|
657
|
+
.filter((entry) => isRecord(entry)
|
|
658
|
+
&& typeof entry.id === "string" && entry.kind === "workspace")
|
|
659
|
+
.map((entry) => [entry.id, entry]));
|
|
660
|
+
const evidenceByPath = new Map(containingManifest.evidence
|
|
661
|
+
.filter((entry) => isRecord(entry)
|
|
662
|
+
&& typeof entry.relativePath === "string")
|
|
663
|
+
.map((entry) => [entry.relativePath.toLowerCase(), entry]));
|
|
664
|
+
const exportedIds = new Set(Array.isArray(exportManifest.artifactIds)
|
|
665
|
+
? exportManifest.artifactIds.filter((id) => typeof id === "string")
|
|
666
|
+
: []);
|
|
667
|
+
if (bundleRoot !== undefined) {
|
|
668
|
+
errors.push(...validateRunManifestEvidence(artifact, containingManifest, bundleRoot));
|
|
669
|
+
}
|
|
670
|
+
const referencedDiagnosticPaths = new Set();
|
|
671
|
+
const exportedDiagnosticDescriptors = [];
|
|
672
|
+
let hasNonExportArtifact = false;
|
|
673
|
+
for (const id of Array.isArray(exportManifest.artifactIds) ? exportManifest.artifactIds : []) {
|
|
674
|
+
const descriptor = typeof id === "string" ? evidenceById.get(id) : undefined;
|
|
675
|
+
if (descriptor === undefined || descriptor.sharingClass === "unknown" || descriptor.sharingClass !== exportManifest.sharingClass) {
|
|
676
|
+
errors.push({ artifact, schemaVersion: "export-manifest/v1", field: "/artifactIds", message: `Export cannot include artifact ${String(id)}.` });
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (exportManifest.sharingClass === "partner" || exportManifest.sharingClass === "public") {
|
|
680
|
+
validateSanitizedProvenance(artifact, "export-manifest/v1", descriptor, evidenceById, errors);
|
|
681
|
+
if (descriptor.kind === "verifier" && descriptor.sanitizedFrom !== undefined) {
|
|
682
|
+
errors.push(...validateExportedVerifierDiagnostics(artifact, descriptor, bundleRoot, expectedBundleId, expectedVerifier, workspaceEvidence, evidenceByPath, exportedIds));
|
|
683
|
+
for (const locator of exportedVerifierDiagnosticLocators(bundleRoot, descriptor)) {
|
|
684
|
+
referencedDiagnosticPaths.add(locator);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (descriptor.kind === "diagnostic" && descriptor.sanitizedFrom !== undefined) {
|
|
688
|
+
errors.push(...validateExportedDiagnosticSidecar(artifact, descriptor, bundleRoot, evidenceById));
|
|
689
|
+
exportedDiagnosticDescriptors.push(descriptor);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
hasNonExportArtifact ||= descriptor.kind !== "export-manifest";
|
|
693
|
+
}
|
|
694
|
+
for (const descriptor of exportedDiagnosticDescriptors) {
|
|
695
|
+
if (typeof descriptor.relativePath === "string" && !referencedDiagnosticPaths.has(descriptor.relativePath.toLowerCase())) {
|
|
696
|
+
errors.push({
|
|
697
|
+
artifact,
|
|
698
|
+
schemaVersion: "export-manifest/v1",
|
|
699
|
+
field: `/artifactIds/${escapeJsonPointer(String(descriptor.id))}`,
|
|
700
|
+
message: "Exported diagnostic sidecars must be referenced by an exported sanitized verifier.",
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (!hasNonExportArtifact)
|
|
705
|
+
errors.push({ artifact, schemaVersion: "export-manifest/v1", field: "/artifactIds", message: "Ready exports must include non-export evidence." });
|
|
706
|
+
return errors;
|
|
707
|
+
}
|
|
708
|
+
function exportedVerifierDiagnosticLocators(bundleRoot, descriptor) {
|
|
709
|
+
if (bundleRoot === undefined || typeof descriptor.relativePath !== "string" || typeof descriptor.digest !== "string")
|
|
710
|
+
return [];
|
|
711
|
+
try {
|
|
712
|
+
const bytes = resolveBundleArtifact(bundleRoot, {
|
|
713
|
+
locator: descriptor.relativePath,
|
|
714
|
+
digest: runDigest(descriptor.digest),
|
|
715
|
+
});
|
|
716
|
+
const result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
717
|
+
return isRecord(result) && Array.isArray(result.diagnostics)
|
|
718
|
+
? result.diagnostics.flatMap((diagnostic) => isRecord(diagnostic) && typeof diagnostic.locator === "string"
|
|
719
|
+
? [diagnostic.locator.toLowerCase()]
|
|
720
|
+
: [])
|
|
721
|
+
: [];
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return [];
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function validateExportedDiagnosticSidecar(artifact, descriptor, bundleRoot, evidenceById) {
|
|
728
|
+
const scope = `/artifactIds/${escapeJsonPointer(String(descriptor.id))}`;
|
|
729
|
+
if (bundleRoot === undefined) {
|
|
730
|
+
return [{ artifact, schemaVersion: "export-manifest/v1", field: scope, message: "Exported diagnostic sidecars require a bundle root." }];
|
|
731
|
+
}
|
|
732
|
+
const sourceBinding = isDiagnosticSourceBinding(descriptor.diagnosticSource) ? descriptor.diagnosticSource : undefined;
|
|
733
|
+
if (sourceBinding === undefined) {
|
|
734
|
+
return [{ artifact, schemaVersion: "export-manifest/v1", field: `${scope}/diagnosticSource`, message: "Exported diagnostic sidecars require source-specific provenance." }];
|
|
735
|
+
}
|
|
736
|
+
if (!isRecord(descriptor.sanitizedFrom) || descriptor.sanitizedFrom.artifactId !== sourceBinding.verifierId) {
|
|
737
|
+
return [{ artifact, schemaVersion: "export-manifest/v1", field: `${scope}/sanitizedFrom`, message: "Diagnostic sidecar provenance must name its diagnostic source verifier." }];
|
|
738
|
+
}
|
|
739
|
+
const sourceDescriptor = evidenceById.get(sourceBinding.verifierId);
|
|
740
|
+
if (sourceDescriptor === undefined || sourceDescriptor.kind !== "verifier"
|
|
741
|
+
|| typeof sourceDescriptor.relativePath !== "string" || typeof sourceDescriptor.digest !== "string") {
|
|
742
|
+
return [{ artifact, schemaVersion: "export-manifest/v1", field: `${scope}/diagnosticSource/verifierId`, message: "Diagnostic source provenance must reference a retained verifier." }];
|
|
743
|
+
}
|
|
744
|
+
let sourceOutcome;
|
|
745
|
+
try {
|
|
746
|
+
const sourceBytes = resolveBundleArtifact(bundleRoot, {
|
|
747
|
+
locator: sourceDescriptor.relativePath,
|
|
748
|
+
digest: runDigest(sourceDescriptor.digest),
|
|
749
|
+
});
|
|
750
|
+
sourceOutcome = nestedVerifierOutcome(sourceBytes);
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
sourceOutcome = undefined;
|
|
754
|
+
}
|
|
755
|
+
if (sourceOutcome === undefined || !sourceOutcome.diagnostics.some((diagnostic) => sameDiagnosticOrigin(diagnostic, sourceBinding))) {
|
|
756
|
+
return [{ artifact, schemaVersion: "export-manifest/v1", field: `${scope}/diagnosticSource`, message: "Diagnostic source provenance must match a retained source diagnostic." }];
|
|
757
|
+
}
|
|
758
|
+
const errors = [];
|
|
759
|
+
if (typeof descriptor.relativePath !== "string" || typeof descriptor.digest !== "string" || typeof descriptor.sizeBytes !== "number")
|
|
760
|
+
return errors;
|
|
761
|
+
try {
|
|
762
|
+
const sidecarBytes = resolveBundleArtifact(bundleRoot, {
|
|
763
|
+
locator: descriptor.relativePath,
|
|
764
|
+
digest: runDigest(descriptor.digest),
|
|
765
|
+
});
|
|
766
|
+
if (sidecarBytes.length !== descriptor.sizeBytes)
|
|
767
|
+
throw new Error("Diagnostic sidecar size does not match its descriptor.");
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
errors.push({ artifact, schemaVersion: "export-manifest/v1", field: scope, message: error instanceof Error ? error.message : "Diagnostic sidecar could not be verified." });
|
|
771
|
+
}
|
|
772
|
+
if (descriptor.digest === sourceBinding.digest) {
|
|
773
|
+
errors.push({ artifact, schemaVersion: "export-manifest/v1", field: `${scope}/digest`, message: "Diagnostic sidecar bytes must differ from the source diagnostic." });
|
|
774
|
+
}
|
|
775
|
+
return errors;
|
|
776
|
+
}
|
|
777
|
+
function validateExportedVerifierDiagnostics(artifact, descriptor, bundleRoot, expectedBundleId, expectedVerifier, workspaceEvidence, evidenceByPath, exportedIds) {
|
|
778
|
+
const scope = `/artifactIds/${escapeJsonPointer(String(descriptor.id))}`;
|
|
779
|
+
if (bundleRoot === undefined) {
|
|
780
|
+
return [{ artifact, schemaVersion: "export-manifest/v1", field: scope, message: "Sanitized verifier exports require a bundle root to validate diagnostics." }];
|
|
781
|
+
}
|
|
782
|
+
let bytes;
|
|
783
|
+
try {
|
|
784
|
+
bytes = resolveBundleArtifact(bundleRoot, {
|
|
785
|
+
locator: descriptor.relativePath,
|
|
786
|
+
digest: runDigest(descriptor.digest),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
return [{ artifact, schemaVersion: "export-manifest/v1", field: scope, message: error instanceof Error ? error.message : "Sanitized verifier could not be read." }];
|
|
791
|
+
}
|
|
792
|
+
const evidencePaths = new Set([...evidenceByPath.keys()]);
|
|
793
|
+
const errors = nestedVerifierDiagnosticErrors(artifact, String(descriptor.id), bytes, bundleRoot, expectedBundleId, expectedVerifier, workspaceEvidence, true, isRecord(descriptor.sanitizedFrom) && typeof descriptor.sanitizedFrom.artifactId === "string"
|
|
794
|
+
? descriptor.sanitizedFrom.artifactId
|
|
795
|
+
: undefined, evidenceByPath, evidencePaths, new Set());
|
|
796
|
+
if (errors.length > 0)
|
|
797
|
+
return errors.map((error) => ({ ...error, schemaVersion: "export-manifest/v1", field: `${scope}${error.field.replace(/^\/evidence\/[^/]*/, "")}` }));
|
|
798
|
+
let result;
|
|
799
|
+
try {
|
|
800
|
+
result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
801
|
+
}
|
|
802
|
+
catch {
|
|
803
|
+
return [];
|
|
804
|
+
}
|
|
805
|
+
if (!isRecord(result) || !Array.isArray(result.diagnostics))
|
|
806
|
+
return [];
|
|
807
|
+
for (const [index, diagnostic] of result.diagnostics.entries()) {
|
|
808
|
+
if (!isRecord(diagnostic) || typeof diagnostic.locator !== "string")
|
|
809
|
+
continue;
|
|
810
|
+
const sidecar = evidenceByPath.get(diagnostic.locator.toLowerCase());
|
|
811
|
+
if (sidecar !== undefined && typeof sidecar.id === "string" && !exportedIds.has(sidecar.id)) {
|
|
812
|
+
errors.push({
|
|
813
|
+
artifact,
|
|
814
|
+
schemaVersion: "export-manifest/v1",
|
|
815
|
+
field: `${scope}/diagnostics/${index}/locator`,
|
|
816
|
+
message: "Sanitized verifier diagnostic sidecars must be included in the export.",
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return errors;
|
|
821
|
+
}
|
|
822
|
+
export async function readVerifiedArtifact(artifactRoot, relativePath, expectedDigest, maxBytes) {
|
|
823
|
+
if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes < 0)) {
|
|
824
|
+
throw new Error("Artifact byte limit must be a nonnegative safe integer.");
|
|
825
|
+
}
|
|
826
|
+
const { root, path } = await resolveExistingArtifactPath(artifactRoot, relativePath);
|
|
827
|
+
removeInterruptedPublicationLink(path, lstatSync(path));
|
|
828
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
829
|
+
try {
|
|
830
|
+
const opened = await handle.stat();
|
|
831
|
+
const current = await lstat(path);
|
|
832
|
+
if (!opened.isFile() || !(await isReadablePublishedArtifact(path, opened))
|
|
833
|
+
|| current.isSymbolicLink() || opened.dev !== current.dev || opened.ino !== current.ino) {
|
|
834
|
+
throw new Error(`Artifact path "${relativePath}" is not an isolated regular file.`);
|
|
835
|
+
}
|
|
836
|
+
if (maxBytes !== undefined && opened.size > maxBytes) {
|
|
837
|
+
throw new Error(`Artifact "${relativePath}" exceeds the qualification byte limit of ${maxBytes}.`);
|
|
838
|
+
}
|
|
839
|
+
await assertExistingArtifactPath(root, relativePath);
|
|
840
|
+
const bytes = await handle.readFile();
|
|
841
|
+
const completed = await handle.stat();
|
|
842
|
+
if (!completed.isFile() || !(await isReadablePublishedArtifact(path, completed))
|
|
843
|
+
|| completed.dev !== opened.dev || completed.ino !== opened.ino || completed.size !== opened.size) {
|
|
844
|
+
throw new Error(`Artifact path "${relativePath}" changed while it was being read.`);
|
|
845
|
+
}
|
|
846
|
+
if (!verifyDigest(bytes, expectedDigest)) {
|
|
847
|
+
throw new Error(`Artifact "${relativePath}" digest does not match its source reference.`);
|
|
848
|
+
}
|
|
849
|
+
return bytes;
|
|
850
|
+
}
|
|
851
|
+
finally {
|
|
852
|
+
await handle.close();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
export async function inspectRetainedArtifact(artifactRoot, relativePath) {
|
|
856
|
+
const { path } = await resolveExistingArtifactPath(artifactRoot, relativePath);
|
|
857
|
+
return inspectExistingPath(path, relativePath);
|
|
858
|
+
}
|
|
859
|
+
export async function writeMetadataAtomically(artifactRoot, relativePath, metadata, signal, options = {}) {
|
|
860
|
+
const bytes = Buffer.from(canonicalizeMetadata(metadata));
|
|
861
|
+
return writeArtifactAtomically(artifactRoot, relativePath, bytes, signal, options);
|
|
862
|
+
}
|
|
863
|
+
export async function writeArtifactAtomically(artifactRoot, relativePath, content, signal, options = {}) {
|
|
864
|
+
const bytes = Buffer.from(content);
|
|
865
|
+
const { parent, path } = await prepareArtifactPath(artifactRoot, relativePath);
|
|
866
|
+
const temporaryPath = resolve(parent, `.${randomUUID()}.tmp`);
|
|
867
|
+
let handle;
|
|
868
|
+
try {
|
|
869
|
+
handle = await open(temporaryPath, "wx", 0o600);
|
|
870
|
+
await handle.writeFile(bytes);
|
|
871
|
+
await handle.sync();
|
|
872
|
+
await handle.close();
|
|
873
|
+
handle = undefined;
|
|
874
|
+
if (signal?.aborted)
|
|
875
|
+
throw new Error("Artifact metadata write interrupted.");
|
|
876
|
+
if (options.overwrite === false) {
|
|
877
|
+
try {
|
|
878
|
+
await link(temporaryPath, path);
|
|
879
|
+
}
|
|
880
|
+
catch (error) {
|
|
881
|
+
if (error.code === "EEXIST") {
|
|
882
|
+
throw new Error(`Artifact metadata path "${relativePath}" already exists.`);
|
|
883
|
+
}
|
|
884
|
+
throw error;
|
|
885
|
+
}
|
|
886
|
+
await rm(temporaryPath, { force: true });
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
await rename(temporaryPath, path);
|
|
890
|
+
}
|
|
891
|
+
await syncDirectory(parent);
|
|
892
|
+
}
|
|
893
|
+
finally {
|
|
894
|
+
await handle?.close();
|
|
895
|
+
await rm(temporaryPath, { force: true });
|
|
896
|
+
}
|
|
897
|
+
const digest = digestBytes(bytes);
|
|
898
|
+
await readVerifiedArtifact(artifactRoot, relativePath, digest);
|
|
899
|
+
return digest;
|
|
900
|
+
}
|
|
901
|
+
export function writeMetadataAtomicallyIfAbsentSync(artifactRoot, relativePath, metadata, rootHandle, beforeDestinationPublish, afterDestinationPublish) {
|
|
902
|
+
const bytes = Buffer.from(canonicalizeMetadata(metadata));
|
|
903
|
+
const rootIdentity = rootHandle ?? openBundleRoot(artifactRoot);
|
|
904
|
+
const ownsRoot = rootHandle === undefined;
|
|
905
|
+
let descriptor;
|
|
906
|
+
let temporaryPath;
|
|
907
|
+
try {
|
|
908
|
+
const { createdDirectories, parent, path } = prepareArtifactPathSync(rootIdentity.path, relativePath);
|
|
909
|
+
temporaryPath = resolve(parent, `.${randomUUID()}.tmp`);
|
|
910
|
+
descriptor = openSync(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
911
|
+
writeFileSync(descriptor, bytes);
|
|
912
|
+
fsyncSync(descriptor);
|
|
913
|
+
beforeDestinationPublish?.();
|
|
914
|
+
let created = false;
|
|
915
|
+
try {
|
|
916
|
+
linkSync(temporaryPath, path);
|
|
917
|
+
created = true;
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
if (error.code !== "EEXIST")
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
if (created) {
|
|
924
|
+
try {
|
|
925
|
+
unlinkSync(temporaryPath);
|
|
926
|
+
}
|
|
927
|
+
catch (error) {
|
|
928
|
+
if (error.code !== "ENOENT")
|
|
929
|
+
throw error;
|
|
930
|
+
}
|
|
931
|
+
temporaryPath = undefined;
|
|
932
|
+
}
|
|
933
|
+
syncPublicationDirectories(rootIdentity.path, createdDirectories, parent);
|
|
934
|
+
if (created)
|
|
935
|
+
afterDestinationPublish?.();
|
|
936
|
+
return { created, digest: digestExistingPath(path, relativePath) };
|
|
937
|
+
}
|
|
938
|
+
finally {
|
|
939
|
+
if (descriptor !== undefined)
|
|
940
|
+
closeSync(descriptor);
|
|
941
|
+
if (temporaryPath !== undefined) {
|
|
942
|
+
try {
|
|
943
|
+
unlinkSync(temporaryPath);
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
if (error.code !== "ENOENT") {
|
|
947
|
+
// ponytail: a unique stale temp is harmless; add managed cleanup only if accumulation is observed.
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (ownsRoot)
|
|
952
|
+
closeBundleRoot(rootIdentity);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function loadValidators() {
|
|
956
|
+
const readSchema = (name) => JSON.parse(readFileSync(`${schemaDirectory}${name}`, "utf8"));
|
|
957
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
958
|
+
addFormats(ajv);
|
|
959
|
+
ajv.addSchema(readSchema("task-packet.v1.schema.json"));
|
|
960
|
+
ajv.addSchema(readSchema("task-packet-freeze.v1.schema.json"));
|
|
961
|
+
ajv.addSchema(readSchema("experiment.v1.schema.json"));
|
|
962
|
+
ajv.addSchema(readSchema("run-queue.v1.schema.json"));
|
|
963
|
+
ajv.addSchema(readSchema("run-bundles/v1.json"));
|
|
964
|
+
ajv.addSchema(readSchema("uniform-events/v1.json"));
|
|
965
|
+
ajv.addSchema(readSchema("normalization-integrity.v1.json"));
|
|
966
|
+
ajv.addSchema(readSchema("structural-observations.v1.json"));
|
|
967
|
+
ajv.addSchema(readSchema("behavior-assertions.v1.json"));
|
|
968
|
+
ajv.addSchema(readSchema("semantic-judge.v1.json"));
|
|
969
|
+
ajv.addSchema(readSchema("human-calibration.v1.json"));
|
|
970
|
+
ajv.addSchema(readSchema("aggregation.v1.json"));
|
|
971
|
+
ajv.addSchema(readSchema("deepseek-runtime-composition.v1.schema.json"));
|
|
972
|
+
return new Map([
|
|
973
|
+
["ebo.task-packet/v1", requiredValidator(ajv, "https://ebo.dev/schemas/task-packet.v1.schema.json")],
|
|
974
|
+
["ebo.task-packet-freeze/v1", requiredValidator(ajv, "https://ebo.dev/schemas/task-packet-freeze.v1.schema.json")],
|
|
975
|
+
["ebo.experiment/v1", requiredValidator(ajv, "https://ebo.dev/schemas/experiment.v1.schema.json")],
|
|
976
|
+
["ebo.run-queue/v1", requiredValidator(ajv, "https://ebo.dev/schemas/run-queue.v1.schema.json")],
|
|
977
|
+
["run-manifest/v1", requiredValidator(ajv, runBundleSchemaId)],
|
|
978
|
+
["verifier-result/v1", requiredValidator(ajv, `${runBundleSchemaId}#/$defs/verifierResult`)],
|
|
979
|
+
["capture-report/v1", requiredValidator(ajv, `${runBundleSchemaId}#/$defs/captureReport`)],
|
|
980
|
+
["export-manifest/v1", requiredValidator(ajv, `${runBundleSchemaId}#/$defs/exportManifest`)],
|
|
981
|
+
["ebo.uniform-event/v1", requiredValidator(ajv, "urn:ebo:schema:uniform-events:v1#/$defs/uniformEvent")],
|
|
982
|
+
["ebo.adapter-capability-profile/v1", requiredValidator(ajv, "urn:ebo:schema:uniform-events:v1#/$defs/capabilityProfile")],
|
|
983
|
+
["ebo.normalized-dataset/v1", requiredValidator(ajv, "urn:ebo:schema:normalization-integrity:v1#/$defs/normalizedDataset")],
|
|
984
|
+
["ebo.adapter-coverage-report/v1", requiredValidator(ajv, "urn:ebo:schema:normalization-integrity:v1#/$defs/coverageReport")],
|
|
985
|
+
["ebo.comparison-request/v1", requiredValidator(ajv, "urn:ebo:schema:normalization-integrity:v1#/$defs/comparisonRequest")],
|
|
986
|
+
["ebo.comparison-report/v1", requiredValidator(ajv, "urn:ebo:schema:normalization-integrity:v1#/$defs/comparisonReport")],
|
|
987
|
+
["ebo.comparison-request/v2", requiredValidator(ajv, "urn:ebo:schema:normalization-integrity:v1#/$defs/comparisonRequestV2")],
|
|
988
|
+
["ebo.comparison-report/v2", requiredValidator(ajv, "urn:ebo:schema:normalization-integrity:v1#/$defs/comparisonReportV2")],
|
|
989
|
+
["ebo.structural-observation/v1", requiredValidator(ajv, "urn:ebo:schema:structural-observations:v1#/$defs/observation")],
|
|
990
|
+
["ebo.structural-observation-set/v1", requiredValidator(ajv, "urn:ebo:schema:structural-observations:v1#/$defs/set")],
|
|
991
|
+
["ebo.behavior-vocabulary/v1", requiredValidator(ajv, "urn:ebo:schema:behavior-assertions:v1#/$defs/vocabulary")],
|
|
992
|
+
["ebo.behavior-assertion/v1", requiredValidator(ajv, "urn:ebo:schema:behavior-assertions:v1#/$defs/assertion")],
|
|
993
|
+
["ebo.behavior-review/v1", requiredValidator(ajv, "urn:ebo:schema:behavior-assertions:v1#/$defs/review")],
|
|
994
|
+
["ebo.semantic-judge-request/v1", requiredValidator(ajv, "urn:ebo:schema:semantic-judge:v1#/$defs/request")],
|
|
995
|
+
["ebo.semantic-judge-input/v1", requiredValidator(ajv, "urn:ebo:schema:semantic-judge:v1#/$defs/input")],
|
|
996
|
+
["ebo.semantic-judgment/v1", requiredValidator(ajv, "urn:ebo:schema:semantic-judge:v1#/$defs/judgment")],
|
|
997
|
+
["ebo.review-source-set/v1", requiredValidator(ajv, "urn:ebo:schema:human-calibration:v1#/$defs/sourceSet")],
|
|
998
|
+
["ebo.review-sample-criteria/v1", requiredValidator(ajv, "urn:ebo:schema:human-calibration:v1#/$defs/criteria")],
|
|
999
|
+
["ebo.review-sample/v1", requiredValidator(ajv, "urn:ebo:schema:human-calibration:v1#/$defs/sample")],
|
|
1000
|
+
["ebo.review-packet/v1", requiredValidator(ajv, "urn:ebo:schema:human-calibration:v1#/$defs/packet")],
|
|
1001
|
+
["ebo.human-review-decision/v1", requiredValidator(ajv, "urn:ebo:schema:human-calibration:v1#/$defs/decision")],
|
|
1002
|
+
["ebo.review-history/v1", requiredValidator(ajv, "urn:ebo:schema:human-calibration:v1#/$defs/history")],
|
|
1003
|
+
["ebo.calibration-summary/v1", requiredValidator(ajv, "urn:ebo:schema:human-calibration:v1#/$defs/summary")],
|
|
1004
|
+
["ebo.aggregation-request/v1", requiredValidator(ajv, "urn:ebo:schema:aggregation:v1#/$defs/request")],
|
|
1005
|
+
["ebo.aggregation-report/v1", requiredValidator(ajv, "urn:ebo:schema:aggregation:v1#/$defs/report")],
|
|
1006
|
+
["ebo.deepseek-runtime-composition/v1", requiredValidator(ajv, "https://ebo.dev/schemas/deepseek-runtime-composition.v1.schema.json")],
|
|
1007
|
+
]);
|
|
1008
|
+
}
|
|
1009
|
+
function requiredValidator(ajv, schemaId) {
|
|
1010
|
+
const validate = ajv.getSchema(schemaId);
|
|
1011
|
+
if (validate === undefined)
|
|
1012
|
+
throw new Error(`Missing bundled schema ${schemaId}.`);
|
|
1013
|
+
return validate;
|
|
1014
|
+
}
|
|
1015
|
+
function canonicalJson(value, ancestors) {
|
|
1016
|
+
if (value === null)
|
|
1017
|
+
return "null";
|
|
1018
|
+
if (typeof value === "string" || typeof value === "boolean")
|
|
1019
|
+
return JSON.stringify(value);
|
|
1020
|
+
if (typeof value === "number") {
|
|
1021
|
+
if (!Number.isFinite(value))
|
|
1022
|
+
throw new Error("Metadata must contain only finite JSON numbers.");
|
|
1023
|
+
return JSON.stringify(value);
|
|
1024
|
+
}
|
|
1025
|
+
if (Array.isArray(value)) {
|
|
1026
|
+
if (ancestors.has(value))
|
|
1027
|
+
throw new Error("Metadata must not contain cycles.");
|
|
1028
|
+
if (Object.keys(value).some((key) => !/^(?:0|[1-9][0-9]*)$/.test(key) || Number(key) >= value.length)) {
|
|
1029
|
+
throw new Error("Metadata arrays must not contain non-index properties.");
|
|
1030
|
+
}
|
|
1031
|
+
ancestors.add(value);
|
|
1032
|
+
try {
|
|
1033
|
+
const entries = [];
|
|
1034
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1035
|
+
if (!Object.hasOwn(value, index))
|
|
1036
|
+
throw new Error("Metadata arrays must not contain holes.");
|
|
1037
|
+
entries.push(canonicalJson(value[index], ancestors));
|
|
1038
|
+
}
|
|
1039
|
+
return `[${entries.join(",")}]`;
|
|
1040
|
+
}
|
|
1041
|
+
finally {
|
|
1042
|
+
ancestors.delete(value);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (!isRecord(value) || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
1046
|
+
|| Object.getOwnPropertySymbols(value).length > 0) {
|
|
1047
|
+
throw new Error("Metadata must contain only JSON values.");
|
|
1048
|
+
}
|
|
1049
|
+
if (ancestors.has(value))
|
|
1050
|
+
throw new Error("Metadata must not contain cycles.");
|
|
1051
|
+
ancestors.add(value);
|
|
1052
|
+
try {
|
|
1053
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key], ancestors)}`).join(",")}}`;
|
|
1054
|
+
}
|
|
1055
|
+
finally {
|
|
1056
|
+
ancestors.delete(value);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function schemaVersionOf(document) {
|
|
1060
|
+
return isRecord(document) && typeof document.schemaVersion === "string" ? document.schemaVersion : "unknown";
|
|
1061
|
+
}
|
|
1062
|
+
function validationError(artifact, schemaVersion, error) {
|
|
1063
|
+
const missing = error.keyword === "required" && typeof error.params.missingProperty === "string"
|
|
1064
|
+
? `/${escapeJsonPointer(error.params.missingProperty)}`
|
|
1065
|
+
: "";
|
|
1066
|
+
return {
|
|
1067
|
+
artifact,
|
|
1068
|
+
schemaVersion,
|
|
1069
|
+
field: `${error.instancePath}${missing}` || "/",
|
|
1070
|
+
message: error.message ?? error.keyword,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function runManifestIntegrityErrors(artifact, schemaVersion, manifest) {
|
|
1074
|
+
const errors = [];
|
|
1075
|
+
const evidence = manifest.evidence;
|
|
1076
|
+
try {
|
|
1077
|
+
assertUniqueArtifactIdentities(evidence.map((entry) => ({
|
|
1078
|
+
id: isRecord(entry) && typeof entry.id === "string" ? entry.id : "",
|
|
1079
|
+
relativePath: isRecord(entry) && typeof entry.relativePath === "string" ? entry.relativePath : "",
|
|
1080
|
+
})));
|
|
1081
|
+
}
|
|
1082
|
+
catch (error) {
|
|
1083
|
+
errors.push({ artifact, schemaVersion, field: "/evidence", message: error instanceof Error ? error.message : "Duplicate evidence identity." });
|
|
1084
|
+
}
|
|
1085
|
+
const evidenceClasses = new Map();
|
|
1086
|
+
const evidenceById = new Map();
|
|
1087
|
+
for (const descriptor of evidence) {
|
|
1088
|
+
if (!isRecord(descriptor))
|
|
1089
|
+
continue;
|
|
1090
|
+
const { digest, id, kind, authority, relativePath } = descriptor;
|
|
1091
|
+
if (typeof id === "string")
|
|
1092
|
+
evidenceById.set(id, descriptor);
|
|
1093
|
+
if (typeof relativePath === "string" && (relativePath.toLowerCase() === "manifest.json" || relativePath.toLowerCase().startsWith("manifest.json/"))) {
|
|
1094
|
+
errors.push({ artifact, schemaVersion, field: "/evidence", message: "Evidence cannot reuse the containing manifest path." });
|
|
1095
|
+
}
|
|
1096
|
+
if (typeof digest !== "string" || typeof kind !== "string" || typeof authority !== "string")
|
|
1097
|
+
continue;
|
|
1098
|
+
const evidenceClass = `${kind}:${authority}`;
|
|
1099
|
+
const existing = evidenceClasses.get(digest);
|
|
1100
|
+
if (existing !== undefined && existing !== evidenceClass) {
|
|
1101
|
+
errors.push({ artifact, schemaVersion, field: "/evidence", message: `Evidence reuses ${digest} across evidence classes.` });
|
|
1102
|
+
}
|
|
1103
|
+
evidenceClasses.set(digest, evidenceClass);
|
|
1104
|
+
}
|
|
1105
|
+
for (const descriptor of evidenceById.values()) {
|
|
1106
|
+
validateSanitizedProvenance(artifact, schemaVersion, descriptor, evidenceById, errors);
|
|
1107
|
+
}
|
|
1108
|
+
if (isRecord(manifest.attempt) && manifest.attempt.retryOf === manifest.attempt.id) {
|
|
1109
|
+
errors.push({ artifact, schemaVersion, field: "/attempt/retryOf", message: "Retry lineage cannot reference the current attempt." });
|
|
1110
|
+
}
|
|
1111
|
+
const sourceEvidence = [...evidenceById.values()].filter((descriptor) => descriptor.sanitizedFrom === undefined);
|
|
1112
|
+
const run = isRecord(manifest.run) ? manifest.run : undefined;
|
|
1113
|
+
if (run !== undefined && isRecord(run.native)) {
|
|
1114
|
+
validateNativeReference(artifact, schemaVersion, "sessionId", "session", "session", run.native, sourceEvidence, errors);
|
|
1115
|
+
validateNativeReference(artifact, schemaVersion, "traceId", "telemetry", "trace", run.native, sourceEvidence, errors);
|
|
1116
|
+
}
|
|
1117
|
+
const harness = run !== undefined && isRecord(run.harness) ? run.harness : undefined;
|
|
1118
|
+
if (run !== undefined && harness !== undefined && Array.isArray(run.runtime)
|
|
1119
|
+
&& !run.runtime.some((component) => isRecord(component)
|
|
1120
|
+
&& component.version === harness.version
|
|
1121
|
+
&& (component.name === harness.id || component.source === harness.id))) {
|
|
1122
|
+
errors.push({ artifact, schemaVersion, field: "/run/runtime", message: "Declared harness is not represented by its runtime composition." });
|
|
1123
|
+
}
|
|
1124
|
+
const terminal = isRecord(manifest.terminal) ? manifest.terminal : undefined;
|
|
1125
|
+
if (terminal !== undefined && typeof terminal.workspaceArtifactId === "string"
|
|
1126
|
+
&& !sourceEvidence.some((descriptor) => descriptor.id === terminal.workspaceArtifactId && descriptor.kind === "workspace")) {
|
|
1127
|
+
errors.push({ artifact, schemaVersion, field: "/terminal/workspaceArtifactId", message: "Terminal workspace must reference retained workspace evidence." });
|
|
1128
|
+
}
|
|
1129
|
+
return errors;
|
|
1130
|
+
}
|
|
1131
|
+
function validateSanitizedProvenance(artifact, schemaVersion, descriptor, evidenceById, errors) {
|
|
1132
|
+
if (!isRecord(descriptor.sanitizedFrom) || typeof descriptor.id !== "string" || typeof descriptor.digest !== "string")
|
|
1133
|
+
return;
|
|
1134
|
+
const field = `/evidence/${escapeJsonPointer(descriptor.id)}/sanitizedFrom`;
|
|
1135
|
+
const shared = descriptor.sharingClass === "partner" || descriptor.sharingClass === "public";
|
|
1136
|
+
const seen = new Set([descriptor.id]);
|
|
1137
|
+
const sharedDigest = descriptor.digest;
|
|
1138
|
+
let current = descriptor;
|
|
1139
|
+
while (isRecord(current.sanitizedFrom)) {
|
|
1140
|
+
const { artifactId, digest } = current.sanitizedFrom;
|
|
1141
|
+
if (typeof artifactId !== "string" || typeof digest !== "string" || seen.has(artifactId)) {
|
|
1142
|
+
errors.push({ artifact, schemaVersion, field, message: "Sanitized provenance cannot contain a cycle." });
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const source = evidenceById.get(artifactId);
|
|
1146
|
+
if (source === undefined) {
|
|
1147
|
+
errors.push({ artifact, schemaVersion, field, message: "Sanitized provenance must reference retained source evidence." });
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const currentShared = current.sharingClass === "partner" || current.sharingClass === "public";
|
|
1151
|
+
const diagnosticDerivative = current.kind === "diagnostic" && source.kind === "verifier";
|
|
1152
|
+
if (["source", "authority"].some((key) => source[key] !== current[key])
|
|
1153
|
+
|| (!diagnosticDerivative && source.kind !== current.kind)
|
|
1154
|
+
|| (currentShared ? current.nativeReference !== undefined : !sameNativeReference(source.nativeReference, current.nativeReference))
|
|
1155
|
+
|| source.digest !== digest || source.digest === current.digest || source.digest === sharedDigest || source.relativePath === current.relativePath) {
|
|
1156
|
+
errors.push({ artifact, schemaVersion, field, message: "Sanitized provenance does not preserve its source evidence." });
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
seen.add(artifactId);
|
|
1160
|
+
current = source;
|
|
1161
|
+
}
|
|
1162
|
+
if (shared && [...evidenceById.values()].some((candidate) => !seen.has(String(candidate.id)) && candidate.digest === sharedDigest)) {
|
|
1163
|
+
errors.push({ artifact, schemaVersion, field, message: "Sanitized shared evidence cannot reuse unrelated retained bytes." });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function validateNativeReference(artifact, schemaVersion, field, kind, nativeType, native, sourceEvidence, errors) {
|
|
1167
|
+
if (typeof native[field] === "string" && !sourceEvidence.some((descriptor) => descriptor.kind === kind && isRecord(descriptor.nativeReference)
|
|
1168
|
+
&& descriptor.nativeReference.type === nativeType && descriptor.nativeReference.id === native[field])) {
|
|
1169
|
+
errors.push({ artifact, schemaVersion, field: `/run/native/${field}`, message: `Declared ${field} does not match retained ${kind} evidence.` });
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function sameNativeReference(left, right) {
|
|
1173
|
+
if (left == null || right == null)
|
|
1174
|
+
return left == null && right == null;
|
|
1175
|
+
return isRecord(left) && isRecord(right) && left.type === right.type && left.id === right.id;
|
|
1176
|
+
}
|
|
1177
|
+
function runDigest(value) {
|
|
1178
|
+
return { algorithm: "sha256", value: value.slice("sha256:".length) };
|
|
1179
|
+
}
|
|
1180
|
+
function escapeJsonPointer(value) {
|
|
1181
|
+
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
1182
|
+
}
|
|
1183
|
+
async function resolveExistingArtifactPath(artifactRoot, relativePath) {
|
|
1184
|
+
const root = await realpath(artifactRoot);
|
|
1185
|
+
return { root, path: await assertExistingArtifactPath(root, relativePath) };
|
|
1186
|
+
}
|
|
1187
|
+
async function assertExistingArtifactPath(root, relativePath) {
|
|
1188
|
+
assertSafeArtifactPath(relativePath);
|
|
1189
|
+
let path = root;
|
|
1190
|
+
const segments = relativePath.split("/");
|
|
1191
|
+
for (const [index, segment] of segments.entries()) {
|
|
1192
|
+
path = resolve(path, segment);
|
|
1193
|
+
const entry = await lstat(path);
|
|
1194
|
+
if (!isContained(root, path) || entry.isSymbolicLink()) {
|
|
1195
|
+
throw new Error(`Artifact path "${relativePath}" escapes its declared root.`);
|
|
1196
|
+
}
|
|
1197
|
+
if (index === segments.length - 1 && !entry.isFile()) {
|
|
1198
|
+
throw new Error(`Artifact path "${relativePath}" is not an isolated regular file.`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return path;
|
|
1202
|
+
}
|
|
1203
|
+
async function prepareArtifactPath(artifactRoot, relativePath) {
|
|
1204
|
+
assertSafeArtifactPath(relativePath);
|
|
1205
|
+
const root = await realpath(artifactRoot);
|
|
1206
|
+
const segments = relativePath.split("/");
|
|
1207
|
+
let parent = root;
|
|
1208
|
+
for (const segment of segments.slice(0, -1)) {
|
|
1209
|
+
parent = resolve(parent, segment);
|
|
1210
|
+
if (!isContained(root, parent))
|
|
1211
|
+
throw new Error(`Artifact path "${relativePath}" escapes its declared root.`);
|
|
1212
|
+
await mkdir(parent).catch((error) => {
|
|
1213
|
+
if (error.code !== "EEXIST")
|
|
1214
|
+
throw error;
|
|
1215
|
+
});
|
|
1216
|
+
const entry = await lstat(parent);
|
|
1217
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
1218
|
+
throw new Error(`Artifact path "${relativePath}" crosses a symbolic link.`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const path = resolve(parent, segments.at(-1));
|
|
1222
|
+
try {
|
|
1223
|
+
const entry = await lstat(path);
|
|
1224
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
1225
|
+
throw new Error(`Artifact path "${relativePath}" is not an isolated regular file.`);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
catch (error) {
|
|
1229
|
+
if (error.code !== "ENOENT")
|
|
1230
|
+
throw error;
|
|
1231
|
+
}
|
|
1232
|
+
return { parent, path };
|
|
1233
|
+
}
|
|
1234
|
+
function prepareArtifactPathSync(artifactRoot, relativePath) {
|
|
1235
|
+
assertSafeArtifactPath(relativePath);
|
|
1236
|
+
const root = realpathSync(artifactRoot);
|
|
1237
|
+
const segments = relativePath.split("/");
|
|
1238
|
+
const createdDirectories = [];
|
|
1239
|
+
let parent = root;
|
|
1240
|
+
for (const segment of segments.slice(0, -1)) {
|
|
1241
|
+
parent = resolve(parent, segment);
|
|
1242
|
+
if (!isContained(root, parent))
|
|
1243
|
+
throw new Error(`Artifact path "${relativePath}" escapes its declared root.`);
|
|
1244
|
+
try {
|
|
1245
|
+
mkdirSync(parent, { mode: 0o700 });
|
|
1246
|
+
createdDirectories.push(parent);
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
if (error.code !== "EEXIST")
|
|
1250
|
+
throw error;
|
|
1251
|
+
}
|
|
1252
|
+
const entry = lstatSync(parent);
|
|
1253
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
1254
|
+
throw new Error(`Artifact path "${relativePath}" crosses a symbolic link.`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const path = resolve(parent, segments.at(-1));
|
|
1258
|
+
try {
|
|
1259
|
+
const entry = lstatSync(path);
|
|
1260
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
1261
|
+
throw new Error(`Artifact path "${relativePath}" is not an isolated regular file.`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
catch (error) {
|
|
1265
|
+
if (error.code !== "ENOENT")
|
|
1266
|
+
throw error;
|
|
1267
|
+
}
|
|
1268
|
+
return { createdDirectories, parent, path };
|
|
1269
|
+
}
|
|
1270
|
+
function digestExistingPath(path, relativePath) {
|
|
1271
|
+
return inspectExistingPath(path, relativePath).digest;
|
|
1272
|
+
}
|
|
1273
|
+
function inspectExistingPath(path, relativePath) {
|
|
1274
|
+
const descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
1275
|
+
try {
|
|
1276
|
+
let opened = fstatSync(descriptor);
|
|
1277
|
+
if (opened.nlink > 1) {
|
|
1278
|
+
removeInterruptedPublicationLink(path, opened);
|
|
1279
|
+
opened = fstatSync(descriptor);
|
|
1280
|
+
}
|
|
1281
|
+
const openedTimes = fstatSync(descriptor, { bigint: true });
|
|
1282
|
+
if (!opened.isFile() || !isReadablePublishedFile(path, opened)
|
|
1283
|
+
|| !Number.isSafeInteger(opened.size) || opened.size < 0) {
|
|
1284
|
+
throw new Error(`Artifact path "${relativePath}" is not an isolated regular file.`);
|
|
1285
|
+
}
|
|
1286
|
+
const hash = createHash("sha256");
|
|
1287
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
1288
|
+
for (let offset = 0; offset < opened.size;) {
|
|
1289
|
+
const read = readSync(descriptor, chunk, 0, Math.min(chunk.length, opened.size - offset), offset);
|
|
1290
|
+
if (read === 0)
|
|
1291
|
+
throw new Error(`Artifact path "${relativePath}" changed while it was being read.`);
|
|
1292
|
+
hash.update(chunk.subarray(0, read));
|
|
1293
|
+
offset += read;
|
|
1294
|
+
}
|
|
1295
|
+
const trailing = Buffer.allocUnsafe(1);
|
|
1296
|
+
if (readSync(descriptor, trailing, 0, 1, opened.size) !== 0) {
|
|
1297
|
+
throw new Error(`Artifact path "${relativePath}" changed while it was being read.`);
|
|
1298
|
+
}
|
|
1299
|
+
const completed = fstatSync(descriptor);
|
|
1300
|
+
const completedTimes = fstatSync(descriptor, { bigint: true });
|
|
1301
|
+
if (!completed.isFile() || !isReadablePublishedFile(path, completed)
|
|
1302
|
+
|| !sameFileIdentity(opened, completed) || completed.size !== opened.size
|
|
1303
|
+
|| completedTimes.mtimeNs !== openedTimes.mtimeNs || completedTimes.ctimeNs !== openedTimes.ctimeNs) {
|
|
1304
|
+
throw new Error(`Artifact path "${relativePath}" changed while it was being read.`);
|
|
1305
|
+
}
|
|
1306
|
+
return {
|
|
1307
|
+
digest: { algorithm: "sha256", value: hash.digest("hex") },
|
|
1308
|
+
sizeBytes: opened.size,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
finally {
|
|
1312
|
+
closeSync(descriptor);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
function isReadablePublishedFile(path, target) {
|
|
1316
|
+
try {
|
|
1317
|
+
const current = lstatSync(path);
|
|
1318
|
+
return target.nlink === 1 && current.nlink === 1
|
|
1319
|
+
&& current.isFile() && !current.isSymbolicLink() && sameFileIdentity(current, target);
|
|
1320
|
+
}
|
|
1321
|
+
catch {
|
|
1322
|
+
return false;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
async function isReadablePublishedArtifact(path, target) {
|
|
1326
|
+
try {
|
|
1327
|
+
const current = await lstat(path);
|
|
1328
|
+
return target.nlink === 1 && current.nlink === 1
|
|
1329
|
+
&& current.isFile() && !current.isSymbolicLink() && sameFileIdentity(current, target);
|
|
1330
|
+
}
|
|
1331
|
+
catch {
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
function sameFileIdentity(left, right) {
|
|
1336
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
1337
|
+
}
|
|
1338
|
+
function syncDirectorySync(path) {
|
|
1339
|
+
const descriptor = openSync(path, constants.O_RDONLY);
|
|
1340
|
+
try {
|
|
1341
|
+
fsyncSync(descriptor);
|
|
1342
|
+
}
|
|
1343
|
+
finally {
|
|
1344
|
+
closeSync(descriptor);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function syncPublicationDirectories(root, createdDirectories, parent) {
|
|
1348
|
+
const directories = new Set([parent, ...createdDirectories.slice().reverse(), ...createdDirectories.map(dirname), root]);
|
|
1349
|
+
for (const directory of directories)
|
|
1350
|
+
syncDirectorySync(directory);
|
|
1351
|
+
}
|
|
1352
|
+
async function syncDirectory(path) {
|
|
1353
|
+
const handle = await open(path, constants.O_RDONLY);
|
|
1354
|
+
try {
|
|
1355
|
+
await handle.sync();
|
|
1356
|
+
}
|
|
1357
|
+
finally {
|
|
1358
|
+
await handle.close();
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function assertSafeArtifactPath(relativePath) {
|
|
1362
|
+
if (!isSafeArtifactRelativePath(relativePath)) {
|
|
1363
|
+
throw new Error(`Artifact path "${relativePath}" is unsafe.`);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function isContained(root, path) {
|
|
1367
|
+
const fromRoot = relative(root, path);
|
|
1368
|
+
return fromRoot === "" || (!isAbsolute(fromRoot) && fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`));
|
|
1369
|
+
}
|
|
1370
|
+
function isRecord(value) {
|
|
1371
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1372
|
+
}
|