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,683 @@
|
|
|
1
|
+
import { closeSync, fstatSync, lstatSync, readSync, } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { assertNoDuplicateJsonKeys, digestBytes, digestMetadata, validateArtifact, writeMetadataAtomicallyIfAbsentSync, } from "./artifacts.js";
|
|
4
|
+
import { isSafeArtifactRelativePath, openBundleRegularFile, MAX_CONFIGURATION_BYTES, resolveBundleArtifact, resolveBundleArtifactDigest, resolveTaskArchive, validateTaskArchive, closeBundleRoot, openBundleRoot, } from "./contracts.js";
|
|
5
|
+
export const TASK_PACKET_FREEZE_SCHEMA_VERSION = "ebo.task-packet-freeze/v1";
|
|
6
|
+
export const MAX_TASK_PACKET_METADATA_BYTES = MAX_CONFIGURATION_BYTES;
|
|
7
|
+
const TASK_PACKET_SCHEMA_VERSION = "ebo.task-packet/v1";
|
|
8
|
+
const FREEZE_LOCATOR_SUFFIX = ".freeze.json";
|
|
9
|
+
export function inspectTaskPacket(bundleRoot, packetLocator) {
|
|
10
|
+
let root;
|
|
11
|
+
try {
|
|
12
|
+
root = openBundleRoot(bundleRoot);
|
|
13
|
+
return inspectTaskPacketWithRoot(bundleRoot, packetLocator, root);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
return {
|
|
17
|
+
packetLocator,
|
|
18
|
+
packet: null,
|
|
19
|
+
preAdmissionDigest: null,
|
|
20
|
+
packetDigest: null,
|
|
21
|
+
components: null,
|
|
22
|
+
modelVisible: null,
|
|
23
|
+
errors: [packetError(packetLocator, "/", errorMessage(error))],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
if (root !== undefined)
|
|
28
|
+
closeBundleRoot(root);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function inspectTaskPacketWithRoot(bundleRoot, packetLocator, root) {
|
|
32
|
+
let document;
|
|
33
|
+
try {
|
|
34
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(readBundleFile(bundleRoot, packetLocator, root));
|
|
35
|
+
assertNoDuplicateJsonKeys(text);
|
|
36
|
+
document = JSON.parse(text);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return {
|
|
40
|
+
packetLocator,
|
|
41
|
+
packet: null,
|
|
42
|
+
preAdmissionDigest: null,
|
|
43
|
+
packetDigest: null,
|
|
44
|
+
components: null,
|
|
45
|
+
modelVisible: null,
|
|
46
|
+
errors: [packetError(packetLocator, "/", errorMessage(error))],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const errors = validateTaskPacket(packetLocator, document);
|
|
50
|
+
if (errors.length > 0) {
|
|
51
|
+
return { packetLocator, packet: null, preAdmissionDigest: null, packetDigest: null, components: null, modelVisible: null, errors };
|
|
52
|
+
}
|
|
53
|
+
const serializedPacket = document;
|
|
54
|
+
const packet = {
|
|
55
|
+
...structuredClone(serializedPacket),
|
|
56
|
+
assessmentMode: "assessmentMode" in serializedPacket ? serializedPacket.assessmentMode : "verified",
|
|
57
|
+
};
|
|
58
|
+
const preAdmissionPacket = structuredClone(serializedPacket);
|
|
59
|
+
delete preAdmissionPacket.admission;
|
|
60
|
+
const preAdmissionDigest = digestMetadata(preAdmissionPacket);
|
|
61
|
+
const components = {
|
|
62
|
+
prompt: digestBytes(Buffer.from(packet.agentInput.prompt, "utf8")),
|
|
63
|
+
fixture: resolveComponent(bundleRoot, packet.agentInput.fixture.source, "/agentInput/fixture/source", packetLocator, errors, root, packet.agentInput.fixture.source.limits.maxCompressedBytes, {
|
|
64
|
+
limits: packet.agentInput.fixture.source.limits,
|
|
65
|
+
includePaths: packet.agentInput.fixture.materializer.includePaths,
|
|
66
|
+
}),
|
|
67
|
+
reference: packet.assessmentMode === "verified"
|
|
68
|
+
? "locator" in packet.restricted.referenceSolution
|
|
69
|
+
? { status: "referenced", digest: resolveComponent(bundleRoot, packet.restricted.referenceSolution, "/restricted/referenceSolution", packetLocator, errors, root) }
|
|
70
|
+
: { status: packet.restricted.referenceSolution.status }
|
|
71
|
+
: { status: "not-provided" },
|
|
72
|
+
verifier: packet.assessmentMode === "verified"
|
|
73
|
+
? resolveComponent(bundleRoot, packet.restricted.verifier, "/restricted/verifier", packetLocator, errors, root)
|
|
74
|
+
: null,
|
|
75
|
+
reviewRecord: packet.admission.review === null
|
|
76
|
+
? null
|
|
77
|
+
: resolveReviewRecord(bundleRoot, packet.admission.review.reviewRecord, preAdmissionDigest, packet.admission, "/admission/review/reviewRecord", packetLocator, errors, root),
|
|
78
|
+
controlledPerturbation: "reference" in packet.controlledPerturbation
|
|
79
|
+
? { status: "referenced", digest: resolveComponent(bundleRoot, packet.controlledPerturbation.reference, "/controlledPerturbation/reference", packetLocator, errors, root) }
|
|
80
|
+
: { status: packet.controlledPerturbation.status },
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
packetLocator,
|
|
84
|
+
packet,
|
|
85
|
+
preAdmissionDigest,
|
|
86
|
+
packetDigest: digestMetadata(serializedPacket),
|
|
87
|
+
components,
|
|
88
|
+
modelVisible: modelVisibleTaskPacket(packet),
|
|
89
|
+
errors,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function admitTaskPacket(bundleRoot, packetLocator) {
|
|
93
|
+
let root;
|
|
94
|
+
try {
|
|
95
|
+
root = openBundleRoot(bundleRoot);
|
|
96
|
+
return admitTaskPacketWithRoot(bundleRoot, packetLocator, root);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
packetLocator,
|
|
101
|
+
packet: null,
|
|
102
|
+
preAdmissionDigest: null,
|
|
103
|
+
packetDigest: null,
|
|
104
|
+
components: null,
|
|
105
|
+
modelVisible: null,
|
|
106
|
+
errors: [packetError(packetLocator, "/", errorMessage(error))],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
if (root !== undefined)
|
|
111
|
+
closeBundleRoot(root);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function admitTaskPacketWithRoot(bundleRoot, packetLocator, root) {
|
|
115
|
+
const inspection = inspectTaskPacketWithRoot(bundleRoot, packetLocator, root);
|
|
116
|
+
if (inspection.packet === null)
|
|
117
|
+
return inspection;
|
|
118
|
+
if (inspection.packet.admission.status === "proposed") {
|
|
119
|
+
inspection.errors.push(packetError(packetLocator, "/admission/status", "Packet has no passing human admission decision."));
|
|
120
|
+
}
|
|
121
|
+
else if (inspection.packet.admission.status === "rejected") {
|
|
122
|
+
inspection.errors.push(packetError(packetLocator, "/admission/status", "Packet admission review is rejected."));
|
|
123
|
+
}
|
|
124
|
+
if (inspection.packet.admission.review === null) {
|
|
125
|
+
inspection.errors.push(packetError(packetLocator, "/admission/review", "Packet is missing its human admission review."));
|
|
126
|
+
}
|
|
127
|
+
return inspection;
|
|
128
|
+
}
|
|
129
|
+
export function assertTaskPacketAdmitted(bundleRoot, packetLocator) {
|
|
130
|
+
const inspection = admitTaskPacket(bundleRoot, packetLocator);
|
|
131
|
+
if (inspection.errors.length > 0) {
|
|
132
|
+
throw new Error(formatErrors(inspection.errors));
|
|
133
|
+
}
|
|
134
|
+
return inspection;
|
|
135
|
+
}
|
|
136
|
+
function assertTaskPacketAdmittedWithRoot(bundleRoot, packetLocator, root) {
|
|
137
|
+
const inspection = admitTaskPacketWithRoot(bundleRoot, packetLocator, root);
|
|
138
|
+
if (inspection.errors.length > 0) {
|
|
139
|
+
throw new Error(formatErrors(inspection.errors));
|
|
140
|
+
}
|
|
141
|
+
return inspection;
|
|
142
|
+
}
|
|
143
|
+
export function modelVisibleTaskPacket(packet) {
|
|
144
|
+
return structuredClone(packet.agentInput);
|
|
145
|
+
}
|
|
146
|
+
export function defaultFreezeLocator(packetLocator) {
|
|
147
|
+
const freezeLocator = `${packetLocator}${FREEZE_LOCATOR_SUFFIX}`;
|
|
148
|
+
assertFreezeLocatorPathWithinLimits(freezeLocator, packetLocator);
|
|
149
|
+
return freezeLocator;
|
|
150
|
+
}
|
|
151
|
+
function readRetainedFreezeRecord(root, freezeLocator) {
|
|
152
|
+
let value;
|
|
153
|
+
try {
|
|
154
|
+
value = readOptionalJson(root.path, freezeLocator, root);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (["ELOOP", "ENOTDIR"].some((code) => isErrno(error, code)))
|
|
158
|
+
return undefined;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
if (value === undefined)
|
|
162
|
+
return undefined;
|
|
163
|
+
const errors = validateFreezeRecord(freezeLocator, value);
|
|
164
|
+
return errors.length === 0 && isRecord(value) ? value : undefined;
|
|
165
|
+
}
|
|
166
|
+
export function freezeTaskPacket(bundleRoot, packetLocator, freezeLocator = defaultFreezeLocator(packetLocator)) {
|
|
167
|
+
assertFreezeLocatorPathWithinLimits(freezeLocator, packetLocator);
|
|
168
|
+
if (packetLocator === freezeLocator) {
|
|
169
|
+
throw new Error("Freeze record must use a path distinct from the task packet.");
|
|
170
|
+
}
|
|
171
|
+
const root = openBundleRoot(bundleRoot);
|
|
172
|
+
try {
|
|
173
|
+
const inspection = assertTaskPacketAdmittedWithRoot(bundleRoot, packetLocator, root);
|
|
174
|
+
const confirmedInspection = assertTaskPacketAdmittedWithRoot(bundleRoot, packetLocator, root);
|
|
175
|
+
const beforePublication = freezeCandidate(packetLocator, inspection);
|
|
176
|
+
const prePublicationMismatches = compareFreezeRecord(beforePublication, confirmedInspection);
|
|
177
|
+
if (prePublicationMismatches.length > 0) {
|
|
178
|
+
throw new Error(`Task packet changed before freeze publication: ${prePublicationMismatches.join(", ")}.`);
|
|
179
|
+
}
|
|
180
|
+
const retained = readRetainedFreezeRecord(root, freezeLocator);
|
|
181
|
+
// Restore any validated retained publication first. The final inspection
|
|
182
|
+
// below is the authority for whether current inputs still match it.
|
|
183
|
+
const candidate = retained ?? freezeCandidate(packetLocator, confirmedInspection);
|
|
184
|
+
const candidateErrors = validateArtifact(freezeLocator, candidate);
|
|
185
|
+
if (candidateErrors.length > 0)
|
|
186
|
+
throw new Error(formatErrors(candidateErrors));
|
|
187
|
+
let validatedInspection;
|
|
188
|
+
let validatedPublished;
|
|
189
|
+
const validatePublication = () => {
|
|
190
|
+
const inspection = assertTaskPacketAdmittedWithRoot(root.path, packetLocator, root);
|
|
191
|
+
const mismatches = compareFreezeRecord(candidate, inspection);
|
|
192
|
+
if (mismatches.length > 0) {
|
|
193
|
+
throw new Error(`Task packet changed before freeze publication: ${mismatches.join(", ")}.`);
|
|
194
|
+
}
|
|
195
|
+
validatedInspection = inspection;
|
|
196
|
+
const published = readOptionalJson(root.path, freezeLocator, root);
|
|
197
|
+
if (published === undefined)
|
|
198
|
+
return;
|
|
199
|
+
const publishedErrors = validateFreezeRecord(freezeLocator, published);
|
|
200
|
+
if (publishedErrors.length > 0)
|
|
201
|
+
throw new Error(formatErrors(publishedErrors));
|
|
202
|
+
const publishedMismatches = compareFreezeRecord(published, inspection);
|
|
203
|
+
if (publishedMismatches.length > 0) {
|
|
204
|
+
throw new Error(`Published freeze record changed: ${publishedMismatches.join(", ")}.`);
|
|
205
|
+
}
|
|
206
|
+
if (!sameDigest(digestMetadata(published), digestMetadata(candidate))) {
|
|
207
|
+
throw new Error("Published freeze record bytes do not match the candidate.");
|
|
208
|
+
}
|
|
209
|
+
validatedPublished = published;
|
|
210
|
+
};
|
|
211
|
+
const write = writeMetadataAtomicallyIfAbsentSync(bundleRoot, freezeLocator, candidate, root, validatePublication, validatePublication);
|
|
212
|
+
if (!write.created) {
|
|
213
|
+
const winner = readOptionalJson(bundleRoot, freezeLocator, root);
|
|
214
|
+
if (winner === undefined)
|
|
215
|
+
throw new Error("Freeze record disappeared after concurrent creation.");
|
|
216
|
+
const errors = validateFreezeRecord(freezeLocator, winner);
|
|
217
|
+
if (errors.length > 0)
|
|
218
|
+
throw new Error(formatErrors(errors));
|
|
219
|
+
const winnerDigest = digestMetadata(winner);
|
|
220
|
+
const finalInspection = assertTaskPacketAdmittedWithRoot(bundleRoot, packetLocator, root);
|
|
221
|
+
const finalWinner = readOptionalJson(bundleRoot, freezeLocator, root);
|
|
222
|
+
if (finalWinner === undefined)
|
|
223
|
+
throw new Error("Freeze record disappeared after final inspection.");
|
|
224
|
+
const finalWinnerErrors = validateFreezeRecord(freezeLocator, finalWinner);
|
|
225
|
+
if (finalWinnerErrors.length > 0)
|
|
226
|
+
throw new Error(formatErrors(finalWinnerErrors));
|
|
227
|
+
if (!sameDigest(winnerDigest, digestMetadata(finalWinner))) {
|
|
228
|
+
throw new Error("Freeze record changed after final inspection.");
|
|
229
|
+
}
|
|
230
|
+
const mismatches = compareFreezeRecord(finalWinner, finalInspection);
|
|
231
|
+
if (mismatches.length > 0)
|
|
232
|
+
throw new Error(`Frozen task packet changed: ${mismatches.join(", ")}.`);
|
|
233
|
+
return finalWinner;
|
|
234
|
+
}
|
|
235
|
+
if (validatedInspection === undefined || validatedPublished === undefined) {
|
|
236
|
+
throw new Error("Freeze publication did not complete its final validation.");
|
|
237
|
+
}
|
|
238
|
+
return validatedPublished;
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
closeBundleRoot(root);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export function statusTaskPacket(bundleRoot, packetLocator, freezeLocator = defaultFreezeLocator(packetLocator)) {
|
|
245
|
+
const root = openBundleRoot(bundleRoot);
|
|
246
|
+
try {
|
|
247
|
+
return statusTaskPacketWithRoot(bundleRoot, packetLocator, freezeLocator, root);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
closeBundleRoot(root);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function statusTaskPacketWithRoot(bundleRoot, packetLocator, freezeLocator, root) {
|
|
254
|
+
const inspection = inspectTaskPacketWithRoot(bundleRoot, packetLocator, root);
|
|
255
|
+
const packetId = inspection.packet?.id ?? null;
|
|
256
|
+
const packetDigest = inspection.packetDigest;
|
|
257
|
+
let freeze;
|
|
258
|
+
try {
|
|
259
|
+
freeze = readOptionalJson(bundleRoot, freezeLocator, root);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return {
|
|
263
|
+
status: "invalid",
|
|
264
|
+
packetId,
|
|
265
|
+
packetDigest,
|
|
266
|
+
aggregateDigest: null,
|
|
267
|
+
freezeLocator,
|
|
268
|
+
mismatches: ["freeze-record"],
|
|
269
|
+
errors: [...inspection.errors, freezeError(freezeLocator, errorMessage(error))],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (freeze === undefined) {
|
|
273
|
+
return {
|
|
274
|
+
status: inspection.errors.length > 0
|
|
275
|
+
? "invalid"
|
|
276
|
+
: inspection.packet?.admission.status === "admitted" ? "unfrozen" : "unadmitted",
|
|
277
|
+
packetId,
|
|
278
|
+
packetDigest,
|
|
279
|
+
aggregateDigest: null,
|
|
280
|
+
freezeLocator,
|
|
281
|
+
mismatches: [],
|
|
282
|
+
errors: inspection.errors,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const freezeErrors = validateFreezeRecord(freezeLocator, freeze);
|
|
286
|
+
if (freezeErrors.length > 0) {
|
|
287
|
+
return {
|
|
288
|
+
status: "invalid",
|
|
289
|
+
packetId,
|
|
290
|
+
packetDigest,
|
|
291
|
+
aggregateDigest: null,
|
|
292
|
+
freezeLocator,
|
|
293
|
+
mismatches: ["freeze-record"],
|
|
294
|
+
errors: [...inspection.errors, ...freezeErrors],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
let record = freeze;
|
|
298
|
+
if (inspection.errors.length === 0 && inspection.packet?.admission.status !== "admitted") {
|
|
299
|
+
return {
|
|
300
|
+
status: "unadmitted",
|
|
301
|
+
packetId,
|
|
302
|
+
packetDigest,
|
|
303
|
+
aggregateDigest: null,
|
|
304
|
+
freezeLocator,
|
|
305
|
+
mismatches: ["admission"],
|
|
306
|
+
errors: inspection.errors,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
let currentInspection = inspection;
|
|
310
|
+
let mismatches = compareFreezeRecord(record, currentInspection);
|
|
311
|
+
if (inspection.errors.length === 0 && mismatches.length === 0) {
|
|
312
|
+
let previousInspection = inspection;
|
|
313
|
+
let previousRecord = record;
|
|
314
|
+
let previousFreeze = freeze;
|
|
315
|
+
let settled = false;
|
|
316
|
+
let lastMismatches = ["snapshot"];
|
|
317
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
318
|
+
const nextInspection = admitTaskPacketWithRoot(bundleRoot, packetLocator, root);
|
|
319
|
+
if (nextInspection.errors.length > 0 || nextInspection.packet?.admission.status !== "admitted") {
|
|
320
|
+
return {
|
|
321
|
+
status: nextInspection.errors.length > 0 ? "invalid" : "unadmitted",
|
|
322
|
+
packetId: nextInspection.packet?.id ?? packetId,
|
|
323
|
+
packetDigest: nextInspection.packetDigest,
|
|
324
|
+
aggregateDigest: previousRecord.aggregateDigest,
|
|
325
|
+
freezeLocator,
|
|
326
|
+
mismatches: ["admission"],
|
|
327
|
+
errors: nextInspection.errors,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
let nextFreeze;
|
|
331
|
+
try {
|
|
332
|
+
nextFreeze = readOptionalJson(bundleRoot, freezeLocator, root);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
return {
|
|
336
|
+
status: "invalid",
|
|
337
|
+
packetId: nextInspection.packet?.id ?? packetId,
|
|
338
|
+
packetDigest: nextInspection.packetDigest,
|
|
339
|
+
aggregateDigest: null,
|
|
340
|
+
freezeLocator,
|
|
341
|
+
mismatches: ["freeze-record"],
|
|
342
|
+
errors: [...nextInspection.errors, freezeError(freezeLocator, errorMessage(error))],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (nextFreeze === undefined) {
|
|
346
|
+
return {
|
|
347
|
+
status: "invalid",
|
|
348
|
+
packetId: nextInspection.packet?.id ?? packetId,
|
|
349
|
+
packetDigest: nextInspection.packetDigest,
|
|
350
|
+
aggregateDigest: null,
|
|
351
|
+
freezeLocator,
|
|
352
|
+
mismatches: ["freeze-record"],
|
|
353
|
+
errors: [...nextInspection.errors, freezeError(freezeLocator, "Freeze record disappeared during snapshot.")],
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const nextErrors = validateFreezeRecord(freezeLocator, nextFreeze);
|
|
357
|
+
if (nextErrors.length > 0) {
|
|
358
|
+
return {
|
|
359
|
+
status: "invalid",
|
|
360
|
+
packetId: nextInspection.packet?.id ?? packetId,
|
|
361
|
+
packetDigest: nextInspection.packetDigest,
|
|
362
|
+
aggregateDigest: null,
|
|
363
|
+
freezeLocator,
|
|
364
|
+
mismatches: ["freeze-record"],
|
|
365
|
+
errors: [...nextInspection.errors, ...nextErrors],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const nextRecord = nextFreeze;
|
|
369
|
+
const packetDrift = compareFreezeRecord(previousRecord, nextInspection);
|
|
370
|
+
const freezeDrift = !sameDigest(digestMetadata(previousFreeze), digestMetadata(nextFreeze));
|
|
371
|
+
const nextMismatches = compareFreezeRecord(nextRecord, nextInspection);
|
|
372
|
+
const finalInspection = admitTaskPacketWithRoot(bundleRoot, packetLocator, root);
|
|
373
|
+
if (finalInspection.errors.length > 0 || finalInspection.packet?.admission.status !== "admitted") {
|
|
374
|
+
return {
|
|
375
|
+
status: finalInspection.errors.length > 0 ? "invalid" : "unadmitted",
|
|
376
|
+
packetId: finalInspection.packet?.id ?? packetId,
|
|
377
|
+
packetDigest: finalInspection.packetDigest,
|
|
378
|
+
aggregateDigest: nextRecord.aggregateDigest,
|
|
379
|
+
freezeLocator,
|
|
380
|
+
mismatches: ["admission"],
|
|
381
|
+
errors: finalInspection.errors,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const finalMismatches = compareFreezeRecord(nextRecord, finalInspection);
|
|
385
|
+
lastMismatches = [
|
|
386
|
+
...packetDrift,
|
|
387
|
+
...(freezeDrift ? ["freeze-record"] : []),
|
|
388
|
+
...nextMismatches,
|
|
389
|
+
...finalMismatches,
|
|
390
|
+
];
|
|
391
|
+
if (lastMismatches.length === 0) {
|
|
392
|
+
currentInspection = finalInspection;
|
|
393
|
+
record = nextRecord;
|
|
394
|
+
mismatches = [];
|
|
395
|
+
settled = true;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
previousInspection = finalInspection;
|
|
399
|
+
previousRecord = nextRecord;
|
|
400
|
+
previousFreeze = nextFreeze;
|
|
401
|
+
}
|
|
402
|
+
if (!settled) {
|
|
403
|
+
currentInspection = previousInspection;
|
|
404
|
+
record = previousRecord;
|
|
405
|
+
mismatches = lastMismatches;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
status: currentInspection.errors.length > 0 ? "invalid" : mismatches.length > 0 ? "changed" : "frozen",
|
|
410
|
+
packetId: currentInspection.packet?.id ?? packetId,
|
|
411
|
+
packetDigest: currentInspection.packetDigest,
|
|
412
|
+
aggregateDigest: record.aggregateDigest,
|
|
413
|
+
freezeLocator,
|
|
414
|
+
mismatches,
|
|
415
|
+
errors: currentInspection.errors,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
export function formatErrors(errors) {
|
|
419
|
+
return errors.map((error) => `${error.artifact} [${error.schemaVersion}] ${error.field}: ${error.message}`).join("\n");
|
|
420
|
+
}
|
|
421
|
+
export function assertTaskPacketFreezeRecord(record) {
|
|
422
|
+
const errors = validateFreezeRecord(record.packetLocator, record);
|
|
423
|
+
if (errors.length > 0)
|
|
424
|
+
throw new Error(formatErrors(errors));
|
|
425
|
+
const expectedAggregate = aggregateDigest(record.packetId, record.packetLocator, record.preAdmissionDigest, record.packetDigest, record.components, record.frozenAt);
|
|
426
|
+
if (!sameDigest(record.aggregateDigest, expectedAggregate)) {
|
|
427
|
+
throw new Error("Freeze record aggregate digest does not match its components.");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function resolveComponent(bundleRoot, reference, field, artifact, errors, root, maxCompressedBytes, archive) {
|
|
431
|
+
try {
|
|
432
|
+
if (maxCompressedBytes === undefined)
|
|
433
|
+
return resolveBundleArtifactDigest(bundleRoot, reference, root);
|
|
434
|
+
const bytes = resolveTaskArchive(bundleRoot, reference, maxCompressedBytes, root);
|
|
435
|
+
if (archive !== undefined)
|
|
436
|
+
validateTaskArchive(bytes, archive.limits, archive.includePaths);
|
|
437
|
+
return digestBytes(bytes);
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
errors.push(packetError(artifact, field, errorMessage(error)));
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function resolveReviewRecord(bundleRoot, reference, expectedPreAdmissionDigest, expectedAdmission, field, artifact, errors, root) {
|
|
445
|
+
try {
|
|
446
|
+
const bytes = resolveBundleArtifact(bundleRoot, reference, MAX_TASK_PACKET_METADATA_BYTES, root);
|
|
447
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
448
|
+
assertNoDuplicateJsonKeys(text);
|
|
449
|
+
const record = JSON.parse(text);
|
|
450
|
+
const bound = record !== null && typeof record === "object" && !Array.isArray(record)
|
|
451
|
+
? record.preAdmissionDigest
|
|
452
|
+
: undefined;
|
|
453
|
+
if (!isDigest(bound) || !sameDigest(bound, expectedPreAdmissionDigest)) {
|
|
454
|
+
throw new Error("Review record does not bind the packet's pre-admission digest.");
|
|
455
|
+
}
|
|
456
|
+
if (record === null || typeof record !== "object" || Array.isArray(record)) {
|
|
457
|
+
throw new Error("Review record does not bind the packet's admission decision.");
|
|
458
|
+
}
|
|
459
|
+
const reviewed = record;
|
|
460
|
+
const review = expectedAdmission.review;
|
|
461
|
+
if (review === null || reviewed.decision !== expectedAdmission.status
|
|
462
|
+
|| reviewed.reviewedAt !== review.reviewedAt || reviewed.reviewedBy !== review.reviewedBy) {
|
|
463
|
+
throw new Error("Review record does not bind the packet's admission decision.");
|
|
464
|
+
}
|
|
465
|
+
return digestBytes(bytes);
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
errors.push(packetError(artifact, field, errorMessage(error)));
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function freezeCandidate(packetLocator, inspection, frozenAt = new Date().toISOString()) {
|
|
473
|
+
const packet = inspection.packet;
|
|
474
|
+
const preAdmissionDigest = inspection.preAdmissionDigest;
|
|
475
|
+
const packetDigest = inspection.packetDigest;
|
|
476
|
+
const components = inspection.components;
|
|
477
|
+
if (packet === null || preAdmissionDigest === null || packetDigest === null || components === null) {
|
|
478
|
+
throw new Error("Cannot freeze an incomplete task-packet inspection.");
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
schemaVersion: TASK_PACKET_FREEZE_SCHEMA_VERSION,
|
|
482
|
+
packetId: packet.id,
|
|
483
|
+
assessmentMode: packet.assessmentMode,
|
|
484
|
+
packetLocator,
|
|
485
|
+
preAdmissionDigest,
|
|
486
|
+
packetDigest,
|
|
487
|
+
components,
|
|
488
|
+
aggregateDigest: aggregateDigest(packet.id, packetLocator, preAdmissionDigest, packetDigest, components, frozenAt),
|
|
489
|
+
frozenAt,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
export function assertFreezeLocatorPathWithinLimits(freezeLocator, packetLocator) {
|
|
493
|
+
const leafLength = freezeLocator.slice(freezeLocator.lastIndexOf("/") + 1).length;
|
|
494
|
+
if (freezeLocator.length > 960 || leafLength > 255) {
|
|
495
|
+
throw new Error(`Freeze locator "${freezeLocator}" for packet "${packetLocator}" exceeds safe path limits.`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function validateFreezeRecord(freezeLocator, document) {
|
|
499
|
+
if (isRecord(document) && document.schemaVersion !== TASK_PACKET_FREEZE_SCHEMA_VERSION) {
|
|
500
|
+
return [
|
|
501
|
+
{
|
|
502
|
+
artifact: freezeLocator,
|
|
503
|
+
schemaVersion: TASK_PACKET_FREEZE_SCHEMA_VERSION,
|
|
504
|
+
field: "/schemaVersion",
|
|
505
|
+
message: `must equal ${TASK_PACKET_FREEZE_SCHEMA_VERSION}`,
|
|
506
|
+
},
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
return validateArtifact(freezeLocator, document);
|
|
510
|
+
}
|
|
511
|
+
function validateTaskPacket(packetLocator, document) {
|
|
512
|
+
if (isRecord(document) && document.schemaVersion !== TASK_PACKET_SCHEMA_VERSION) {
|
|
513
|
+
return [packetError(packetLocator, "/schemaVersion", `must equal ${TASK_PACKET_SCHEMA_VERSION}`)];
|
|
514
|
+
}
|
|
515
|
+
return validateArtifact(packetLocator, document);
|
|
516
|
+
}
|
|
517
|
+
function aggregateDigest(packetId, packetLocator, preAdmissionDigest, packetDigest, components, frozenAt) {
|
|
518
|
+
return digestMetadata({ packetId, packetLocator, preAdmissionDigest, packetDigest, components, frozenAt });
|
|
519
|
+
}
|
|
520
|
+
function compareFreezeRecord(record, inspection) {
|
|
521
|
+
const mismatches = [];
|
|
522
|
+
if (record.packetId !== inspection.packet?.id)
|
|
523
|
+
mismatches.push("packetId");
|
|
524
|
+
if ((record.assessmentMode ?? "verified") !== inspection.packet?.assessmentMode)
|
|
525
|
+
mismatches.push("assessmentMode");
|
|
526
|
+
if (record.packetLocator !== inspection.packetLocator)
|
|
527
|
+
mismatches.push("packetLocator");
|
|
528
|
+
if (!sameDigest(record.preAdmissionDigest, inspection.preAdmissionDigest))
|
|
529
|
+
mismatches.push("pre-admission");
|
|
530
|
+
if (!sameDigest(record.packetDigest, inspection.packetDigest))
|
|
531
|
+
mismatches.push("packet");
|
|
532
|
+
const current = inspection.components;
|
|
533
|
+
if (current === null) {
|
|
534
|
+
mismatches.push("components");
|
|
535
|
+
return mismatches;
|
|
536
|
+
}
|
|
537
|
+
for (const component of ["prompt", "fixture", "verifier", "reviewRecord"]) {
|
|
538
|
+
if (!sameDigest(record.components[component], current[component]))
|
|
539
|
+
mismatches.push(`components.${component}`);
|
|
540
|
+
}
|
|
541
|
+
for (const component of ["reference", "controlledPerturbation"]) {
|
|
542
|
+
if (!sameComponent(record.components[component], current[component]))
|
|
543
|
+
mismatches.push(`components.${component}`);
|
|
544
|
+
}
|
|
545
|
+
if (inspection.packetDigest !== null && inspection.packet !== null && inspection.preAdmissionDigest !== null) {
|
|
546
|
+
const aggregate = aggregateDigest(inspection.packet.id, inspection.packetLocator, inspection.preAdmissionDigest, inspection.packetDigest, current, record.frozenAt);
|
|
547
|
+
if (!sameDigest(record.aggregateDigest, aggregate))
|
|
548
|
+
mismatches.push("aggregate");
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
mismatches.push("aggregate");
|
|
552
|
+
}
|
|
553
|
+
return mismatches;
|
|
554
|
+
}
|
|
555
|
+
function sameDigest(left, right) {
|
|
556
|
+
if (left === null || right === null)
|
|
557
|
+
return left === right;
|
|
558
|
+
return left.algorithm === right.algorithm && left.value === right.value;
|
|
559
|
+
}
|
|
560
|
+
function isDigest(value) {
|
|
561
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
562
|
+
&& value.algorithm === "sha256"
|
|
563
|
+
&& typeof value.value === "string"
|
|
564
|
+
&& /^[a-f0-9]{64}$/.test(value.value);
|
|
565
|
+
}
|
|
566
|
+
function isRecord(value) {
|
|
567
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
568
|
+
}
|
|
569
|
+
function sameComponent(left, right) {
|
|
570
|
+
if (left.status !== right.status)
|
|
571
|
+
return false;
|
|
572
|
+
if (left.status !== "referenced" || right.status !== "referenced")
|
|
573
|
+
return true;
|
|
574
|
+
return sameDigest(left.digest, right.digest);
|
|
575
|
+
}
|
|
576
|
+
function readOptionalJson(bundleRoot, locator, rootHandle) {
|
|
577
|
+
try {
|
|
578
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(readBundleFile(bundleRoot, locator, rootHandle));
|
|
579
|
+
assertNoDuplicateJsonKeys(text);
|
|
580
|
+
return JSON.parse(text);
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
if (isErrno(error, "ENOENT"))
|
|
584
|
+
return undefined;
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function readBundleFile(bundleRoot, locator, rootHandle) {
|
|
589
|
+
if (!isSafeArtifactRelativePath(locator))
|
|
590
|
+
throw new Error(`Artifact path "${locator}" is unsafe.`);
|
|
591
|
+
const rootIdentity = rootHandle ?? openBundleRoot(bundleRoot);
|
|
592
|
+
const ownsRoot = rootHandle === undefined;
|
|
593
|
+
const root = rootIdentity.path;
|
|
594
|
+
const rootDescriptor = rootIdentity.descriptor;
|
|
595
|
+
try {
|
|
596
|
+
const descriptor = openBundleRegularFile(bundleRoot, locator, "Artifact path", rootIdentity);
|
|
597
|
+
try {
|
|
598
|
+
const opened = fstatSync(descriptor);
|
|
599
|
+
const openedTimes = fstatSync(descriptor, { bigint: true });
|
|
600
|
+
if (!opened.isFile()) {
|
|
601
|
+
throw new Error(`Artifact path "${locator}" is not an isolated regular file.`);
|
|
602
|
+
}
|
|
603
|
+
if (!Number.isSafeInteger(opened.size) || opened.size > MAX_TASK_PACKET_METADATA_BYTES) {
|
|
604
|
+
throw new Error(`Artifact path "${locator}" exceeds its metadata size limit.`);
|
|
605
|
+
}
|
|
606
|
+
assertBundleRoot(root, rootDescriptor, locator);
|
|
607
|
+
const current = lstatSync(assertBundlePathWithoutLinks(root, locator));
|
|
608
|
+
if (opened.dev !== current.dev || opened.ino !== current.ino) {
|
|
609
|
+
throw new Error(`Artifact path "${locator}" changed after bundle-root verification.`);
|
|
610
|
+
}
|
|
611
|
+
const bytes = Buffer.alloc(opened.size);
|
|
612
|
+
for (let offset = 0; offset < opened.size;) {
|
|
613
|
+
const read = readSync(descriptor, bytes, offset, opened.size - offset, offset);
|
|
614
|
+
if (read === 0)
|
|
615
|
+
throw new Error(`Artifact path "${locator}" changed while it was being read.`);
|
|
616
|
+
offset += read;
|
|
617
|
+
}
|
|
618
|
+
const trailing = Buffer.allocUnsafe(1);
|
|
619
|
+
if (readSync(descriptor, trailing, 0, 1, opened.size) !== 0) {
|
|
620
|
+
throw new Error(`Artifact path "${locator}" changed while it was being read.`);
|
|
621
|
+
}
|
|
622
|
+
const completed = fstatSync(descriptor);
|
|
623
|
+
const completedTimes = fstatSync(descriptor, { bigint: true });
|
|
624
|
+
if (!completed.isFile() || completed.dev !== opened.dev || completed.ino !== opened.ino
|
|
625
|
+
|| completed.size !== opened.size || completedTimes.mtimeNs !== openedTimes.mtimeNs
|
|
626
|
+
|| completedTimes.ctimeNs !== openedTimes.ctimeNs
|
|
627
|
+
|| completed.size > MAX_TASK_PACKET_METADATA_BYTES || bytes.length !== opened.size) {
|
|
628
|
+
throw new Error(`Artifact path "${locator}" changed while it was being read.`);
|
|
629
|
+
}
|
|
630
|
+
assertBundleRoot(root, rootDescriptor, locator);
|
|
631
|
+
const finalPath = lstatSync(assertBundlePathWithoutLinks(root, locator));
|
|
632
|
+
if (finalPath.dev !== completed.dev || finalPath.ino !== completed.ino) {
|
|
633
|
+
throw new Error(`Artifact path "${locator}" changed after bundle-root verification.`);
|
|
634
|
+
}
|
|
635
|
+
return bytes;
|
|
636
|
+
}
|
|
637
|
+
finally {
|
|
638
|
+
closeSync(descriptor);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
finally {
|
|
642
|
+
if (ownsRoot)
|
|
643
|
+
closeBundleRoot(rootIdentity);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function assertBundlePathWithoutLinks(bundleRoot, locator) {
|
|
647
|
+
let path = bundleRoot;
|
|
648
|
+
const segments = locator.split("/");
|
|
649
|
+
for (const [index, segment] of segments.entries()) {
|
|
650
|
+
path = resolve(path, segment);
|
|
651
|
+
const entry = lstatSync(path);
|
|
652
|
+
if (!isContained(bundleRoot, path) || entry.isSymbolicLink()) {
|
|
653
|
+
throw new Error(`Artifact path "${locator}" escapes its declared root.`);
|
|
654
|
+
}
|
|
655
|
+
if (index === segments.length - 1 && !entry.isFile()) {
|
|
656
|
+
throw new Error(`Artifact path "${locator}" is not an isolated regular file.`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return path;
|
|
660
|
+
}
|
|
661
|
+
function assertBundleRoot(root, descriptor, locator) {
|
|
662
|
+
const opened = fstatSync(descriptor);
|
|
663
|
+
const current = lstatSync(root);
|
|
664
|
+
if (!opened.isDirectory() || current.isSymbolicLink() || opened.dev !== current.dev || opened.ino !== current.ino) {
|
|
665
|
+
throw new Error(`Artifact path "${locator}" bundle root changed during verification.`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function packetError(artifact, field, message) {
|
|
669
|
+
return { artifact, schemaVersion: "ebo.task-packet/v1", field, message };
|
|
670
|
+
}
|
|
671
|
+
function freezeError(artifact, message) {
|
|
672
|
+
return { artifact, schemaVersion: TASK_PACKET_FREEZE_SCHEMA_VERSION, field: "/", message };
|
|
673
|
+
}
|
|
674
|
+
function errorMessage(error) {
|
|
675
|
+
return error instanceof Error ? error.message : "Unable to read task packet.";
|
|
676
|
+
}
|
|
677
|
+
function isErrno(error, code) {
|
|
678
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
679
|
+
}
|
|
680
|
+
function isContained(root, path) {
|
|
681
|
+
const fromRoot = relative(root, path);
|
|
682
|
+
return fromRoot === "" || (!isAbsolute(fromRoot) && fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`));
|
|
683
|
+
}
|