engineering-behavior-observatory 0.2.3 → 0.2.5
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/dist/src/artifacts.d.ts +2 -2
- package/dist/src/artifacts.js +25 -7
- package/dist/src/run-bundles.js +86 -25
- package/dist/src/verifiers.js +27 -7
- package/docs/guides/evidence-and-sharing.md +37 -0
- package/package.json +3 -1
- package/release/0.2.4/KNOWN_LIMITATIONS.md +16 -0
- package/release/0.2.4/README.md +23 -0
- package/release/0.2.4/reproducibility.json +74 -0
- package/release/0.2.5/KNOWN_LIMITATIONS.md +12 -0
- package/release/0.2.5/README.md +25 -0
- package/release/0.2.5/reproducibility.json +74 -0
- package/release/README.md +2 -0
package/dist/src/artifacts.d.ts
CHANGED
|
@@ -20,14 +20,14 @@ export declare function validateArtifact(artifact: string, document: unknown): A
|
|
|
20
20
|
export declare function validateRunManifestEvidence(artifact: string, manifest: unknown, bundleRoot: string): ArtifactValidationError[];
|
|
21
21
|
export declare function validateExportManifest(artifact: string, exportManifest: unknown, containingManifest: unknown | undefined, bundleRoot?: string): ArtifactValidationError[];
|
|
22
22
|
export declare function readVerifiedArtifact(artifactRoot: string, relativePath: string, expectedDigest: Digest, maxBytes?: number): Promise<Buffer>;
|
|
23
|
-
export declare function inspectRetainedArtifact(artifactRoot: string, relativePath: string): Promise<{
|
|
23
|
+
export declare function inspectRetainedArtifact(artifactRoot: string, relativePath: string, maxBytes?: number): Promise<{
|
|
24
24
|
digest: Digest;
|
|
25
25
|
sizeBytes: number;
|
|
26
26
|
}>;
|
|
27
27
|
export declare function writeMetadataAtomically(artifactRoot: string, relativePath: string, metadata: unknown, signal?: AbortSignal, options?: {
|
|
28
28
|
overwrite?: boolean;
|
|
29
29
|
}): Promise<Digest>;
|
|
30
|
-
export declare function writeArtifactAtomically(artifactRoot: string, relativePath: string, content: Uint8Array
|
|
30
|
+
export declare function writeArtifactAtomically(artifactRoot: string, relativePath: string, content: Uint8Array | AsyncIterable<Uint8Array>, signal?: AbortSignal, options?: {
|
|
31
31
|
overwrite?: boolean;
|
|
32
32
|
}): Promise<Digest>;
|
|
33
33
|
export declare function writeMetadataAtomicallyIfAbsentSync(artifactRoot: string, relativePath: string, metadata: unknown, rootHandle?: BundleRootHandle, beforeDestinationPublish?: () => void, afterDestinationPublish?: () => void): {
|
package/dist/src/artifacts.js
CHANGED
|
@@ -852,22 +852,35 @@ export async function readVerifiedArtifact(artifactRoot, relativePath, expectedD
|
|
|
852
852
|
await handle.close();
|
|
853
853
|
}
|
|
854
854
|
}
|
|
855
|
-
export async function inspectRetainedArtifact(artifactRoot, relativePath) {
|
|
855
|
+
export async function inspectRetainedArtifact(artifactRoot, relativePath, maxBytes) {
|
|
856
|
+
if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes < 0)) {
|
|
857
|
+
throw new Error("Artifact byte limit must be a nonnegative safe integer.");
|
|
858
|
+
}
|
|
856
859
|
const { path } = await resolveExistingArtifactPath(artifactRoot, relativePath);
|
|
857
|
-
return inspectExistingPath(path, relativePath);
|
|
860
|
+
return inspectExistingPath(path, relativePath, maxBytes);
|
|
858
861
|
}
|
|
859
862
|
export async function writeMetadataAtomically(artifactRoot, relativePath, metadata, signal, options = {}) {
|
|
860
863
|
const bytes = Buffer.from(canonicalizeMetadata(metadata));
|
|
861
864
|
return writeArtifactAtomically(artifactRoot, relativePath, bytes, signal, options);
|
|
862
865
|
}
|
|
863
866
|
export async function writeArtifactAtomically(artifactRoot, relativePath, content, signal, options = {}) {
|
|
864
|
-
const
|
|
867
|
+
const source = content instanceof Uint8Array ? [Buffer.from(content)] : content;
|
|
868
|
+
const hash = createHash("sha256");
|
|
869
|
+
async function* chunks() {
|
|
870
|
+
for await (const chunk of source) {
|
|
871
|
+
if (signal?.aborted)
|
|
872
|
+
throw new Error("Artifact metadata write interrupted.");
|
|
873
|
+
const bytes = Buffer.from(chunk);
|
|
874
|
+
hash.update(bytes);
|
|
875
|
+
yield bytes;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
865
878
|
const { parent, path } = await prepareArtifactPath(artifactRoot, relativePath);
|
|
866
879
|
const temporaryPath = resolve(parent, `.${randomUUID()}.tmp`);
|
|
867
880
|
let handle;
|
|
868
881
|
try {
|
|
869
882
|
handle = await open(temporaryPath, "wx", 0o600);
|
|
870
|
-
await handle.writeFile(
|
|
883
|
+
await handle.writeFile(chunks());
|
|
871
884
|
await handle.sync();
|
|
872
885
|
await handle.close();
|
|
873
886
|
handle = undefined;
|
|
@@ -894,8 +907,10 @@ export async function writeArtifactAtomically(artifactRoot, relativePath, conten
|
|
|
894
907
|
await handle?.close();
|
|
895
908
|
await rm(temporaryPath, { force: true });
|
|
896
909
|
}
|
|
897
|
-
const digest =
|
|
898
|
-
await
|
|
910
|
+
const digest = { algorithm: "sha256", value: hash.digest("hex") };
|
|
911
|
+
const inspected = await inspectRetainedArtifact(artifactRoot, relativePath);
|
|
912
|
+
if (inspected.digest.value !== digest.value)
|
|
913
|
+
throw new Error(`Artifact "${relativePath}" digest does not match its source reference.`);
|
|
899
914
|
return digest;
|
|
900
915
|
}
|
|
901
916
|
export function writeMetadataAtomicallyIfAbsentSync(artifactRoot, relativePath, metadata, rootHandle, beforeDestinationPublish, afterDestinationPublish) {
|
|
@@ -1270,7 +1285,7 @@ function prepareArtifactPathSync(artifactRoot, relativePath) {
|
|
|
1270
1285
|
function digestExistingPath(path, relativePath) {
|
|
1271
1286
|
return inspectExistingPath(path, relativePath).digest;
|
|
1272
1287
|
}
|
|
1273
|
-
function inspectExistingPath(path, relativePath) {
|
|
1288
|
+
function inspectExistingPath(path, relativePath, maxBytes) {
|
|
1274
1289
|
const descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
1275
1290
|
try {
|
|
1276
1291
|
let opened = fstatSync(descriptor);
|
|
@@ -1283,6 +1298,9 @@ function inspectExistingPath(path, relativePath) {
|
|
|
1283
1298
|
|| !Number.isSafeInteger(opened.size) || opened.size < 0) {
|
|
1284
1299
|
throw new Error(`Artifact path "${relativePath}" is not an isolated regular file.`);
|
|
1285
1300
|
}
|
|
1301
|
+
if (maxBytes !== undefined && opened.size > maxBytes) {
|
|
1302
|
+
throw new Error(`Artifact "${relativePath}" exceeds the qualification byte limit of ${maxBytes}.`);
|
|
1303
|
+
}
|
|
1286
1304
|
const hash = createHash("sha256");
|
|
1287
1305
|
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
1288
1306
|
for (let offset = 0; offset < opened.size;) {
|
package/dist/src/run-bundles.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
4
|
+
import { cp, lstat, mkdir, mkdtemp, readdir, realpath, rm, rmdir, utimes, writeFile } from "node:fs/promises";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { join, resolve, sep } from "node:path";
|
|
6
7
|
import { promisify } from "node:util";
|
|
8
|
+
import { Transform } from "node:stream";
|
|
9
|
+
import { pipeline } from "node:stream/promises";
|
|
7
10
|
import { HOOK_EVENTS } from "@anthropic-ai/claude-agent-sdk";
|
|
8
11
|
import { assertNoDuplicateJsonKeys, assertUniqueArtifactIdentities, canonicalizeMetadata, inspectRetainedArtifact, readVerifiedArtifact, validateArtifact, validateExportManifest, validateRunManifestEvidence, writeArtifactAtomically, writeMetadataAtomically, } from "./artifacts.js";
|
|
9
12
|
import { isSafeArtifactRelativePath } from "./contracts.js";
|
|
@@ -11,7 +14,7 @@ import { digestWorkspace, digestWorkspaceTree } from "./verifiers.js";
|
|
|
11
14
|
import { readBoundedFile } from "./scheduler.js";
|
|
12
15
|
const execFileAsync = promisify(execFile);
|
|
13
16
|
const MAX_WORKSPACE_PATCH_BYTES = 64 * 1024 * 1024;
|
|
14
|
-
const MAX_WORKSPACE_SNAPSHOT_BYTES =
|
|
17
|
+
const MAX_WORKSPACE_SNAPSHOT_BYTES = 1024 * 1024 * 1024;
|
|
15
18
|
const MAX_QUALIFICATION_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
16
19
|
const QUALIFICATION_DIMENSION_RANK = { qualified: 0, unsupported: 1, gap: 2, unqualified: 3 };
|
|
17
20
|
const PINNED_HOOK_EVENTS = new Set(HOOK_EVENTS);
|
|
@@ -151,11 +154,17 @@ export class RunBundleAssembler {
|
|
|
151
154
|
throw new Error("Final workspace metadata changed while its outcome was being captured.");
|
|
152
155
|
}
|
|
153
156
|
const format = patch === undefined ? "snapshot" : "patch";
|
|
154
|
-
const content = patch ?? await workspaceSnapshot(capturedPath, treeDigest);
|
|
155
157
|
const relativePath = format === "patch"
|
|
156
158
|
? options.relativePath ?? "workspace.patch"
|
|
157
159
|
: options.snapshotRelativePath ?? "workspace.tar.gz";
|
|
158
|
-
|
|
160
|
+
if (patch !== undefined) {
|
|
161
|
+
await writeArtifactAtomically(this.bundleRoot, relativePath, patch, undefined, { overwrite: false });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
await workspaceSnapshot(capturedPath, treeDigest, async (snapshotPath) => {
|
|
165
|
+
await writeArtifactAtomically(this.bundleRoot, relativePath, createReadStream(snapshotPath), undefined, { overwrite: false });
|
|
166
|
+
});
|
|
167
|
+
}
|
|
159
168
|
const descriptor = await this.registerArtifact({
|
|
160
169
|
id: options.id ?? "workspace",
|
|
161
170
|
source: options.source ?? `workspace-${format}`,
|
|
@@ -259,6 +268,7 @@ async function withWorkspaceOutcomeProjection(options, use) {
|
|
|
259
268
|
try {
|
|
260
269
|
await cp(finalPath, projectedPath, {
|
|
261
270
|
recursive: true,
|
|
271
|
+
verbatimSymlinks: true,
|
|
262
272
|
preserveTimestamps: true,
|
|
263
273
|
force: false,
|
|
264
274
|
filter: async (source) => source === finalPath
|
|
@@ -312,9 +322,15 @@ export async function qualifyRunBundle(bundleRoot, options = {}) {
|
|
|
312
322
|
const state = { descriptor, valid: true };
|
|
313
323
|
states.set(descriptor.id, state);
|
|
314
324
|
try {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
325
|
+
if (descriptor.kind === "workspace" && descriptor.mediaType === "application/gzip") {
|
|
326
|
+
// Opaque snapshots need streaming integrity checks, not a whole-archive buffer.
|
|
327
|
+
const inspected = await inspectRetainedArtifact(bundleRoot, descriptor.relativePath, MAX_WORKSPACE_SNAPSHOT_BYTES);
|
|
328
|
+
if (inspected.sizeBytes !== descriptor.sizeBytes || digestString(inspected.digest.value) !== descriptor.digest) {
|
|
329
|
+
throw new Error("Workspace snapshot size or digest does not match its descriptor.");
|
|
330
|
+
}
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const bytes = await readVerifiedArtifact(bundleRoot, descriptor.relativePath, digestValue(descriptor.digest), MAX_QUALIFICATION_ARTIFACT_BYTES);
|
|
318
334
|
if (descriptor.mediaType === "application/x-ndjson") {
|
|
319
335
|
const summary = parseNativeJsonl(bytes);
|
|
320
336
|
state.hookNames = summary.hookNames;
|
|
@@ -633,7 +649,7 @@ async function workspacePatchApplies(startingWorkspacePath, patch) {
|
|
|
633
649
|
const applied = join(temporaryRoot, "workspace");
|
|
634
650
|
const patchPath = join(temporaryRoot, "workspace.patch");
|
|
635
651
|
try {
|
|
636
|
-
await cp(resolve(startingWorkspacePath), applied, { recursive: true, preserveTimestamps: true, force: false });
|
|
652
|
+
await cp(resolve(startingWorkspacePath), applied, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
|
|
637
653
|
if (patch.length > 0) {
|
|
638
654
|
await writeFile(patchPath, patch, { flag: "wx", mode: 0o600 });
|
|
639
655
|
await execFileAsync("git", ["apply", "--check", "--binary", patchPath], { cwd: applied });
|
|
@@ -827,7 +843,7 @@ async function removeIgnoredWorkspaceEntries(startPath, finalPath) {
|
|
|
827
843
|
const globalExcludes = join(temporaryRoot, "global-excludes");
|
|
828
844
|
try {
|
|
829
845
|
await writeFile(globalExcludes, "");
|
|
830
|
-
await cp(startPath, baseline, { recursive: true, preserveTimestamps: true, force: false });
|
|
846
|
+
await cp(startPath, baseline, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
|
|
831
847
|
await execFileAsync("git", ["init", "--quiet"], { cwd: baseline });
|
|
832
848
|
await execFileAsync("git", ["add", "--force", "--all"], { cwd: baseline });
|
|
833
849
|
for (const relativePath of await ignoredWorkspacePaths(baseline, finalPath, globalExcludes)) {
|
|
@@ -895,12 +911,31 @@ async function isExcludedWorkspaceDirectory(root, source, exclusions) {
|
|
|
895
911
|
}
|
|
896
912
|
return false;
|
|
897
913
|
}
|
|
898
|
-
async function removeEmptyDirectories(directory, root = directory) {
|
|
914
|
+
async function removeEmptyDirectories(directory, root = directory, targets) {
|
|
915
|
+
if (targets === undefined) {
|
|
916
|
+
targets = new Set();
|
|
917
|
+
async function collect(path) {
|
|
918
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
919
|
+
const child = join(path, entry.name);
|
|
920
|
+
if (entry.isDirectory())
|
|
921
|
+
await collect(child);
|
|
922
|
+
else if (entry.isSymbolicLink()) {
|
|
923
|
+
// Omission must not turn a valid link into a dangling one. Invalid
|
|
924
|
+
// links are rejected later by workspace hashing; no content is read.
|
|
925
|
+
try {
|
|
926
|
+
targets.add(await realpath(child));
|
|
927
|
+
}
|
|
928
|
+
catch { /* validated by hashing */ }
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
await collect(directory);
|
|
933
|
+
}
|
|
899
934
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
900
935
|
if (entry.isDirectory())
|
|
901
|
-
await removeEmptyDirectories(join(directory, entry.name), root);
|
|
936
|
+
await removeEmptyDirectories(join(directory, entry.name), root, targets);
|
|
902
937
|
}
|
|
903
|
-
if (directory !== root && (await readdir(directory)).length === 0)
|
|
938
|
+
if (directory !== root && !targets.has(await realpath(directory)) && (await readdir(directory)).length === 0)
|
|
904
939
|
await rmdir(directory);
|
|
905
940
|
}
|
|
906
941
|
async function restoreProjectedDirectoryTimestamps(source, projected) {
|
|
@@ -923,7 +958,7 @@ async function workspacePatch(startPath, finalPath, finalTreeDigest) {
|
|
|
923
958
|
const applied = join(temporaryRoot, "applied");
|
|
924
959
|
const patchPath = join(temporaryRoot, "workspace.patch");
|
|
925
960
|
try {
|
|
926
|
-
await cp(startPath, worktree, { recursive: true, preserveTimestamps: true, force: false });
|
|
961
|
+
await cp(startPath, worktree, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
|
|
927
962
|
await execFileAsync("git", ["init", "--quiet"], { cwd: worktree });
|
|
928
963
|
await execFileAsync("git", ["add", "--force", "--all"], { cwd: worktree });
|
|
929
964
|
// A tree is enough for the diff. A commit can spawn detached maintenance
|
|
@@ -936,7 +971,7 @@ async function workspacePatch(startPath, finalPath, finalTreeDigest) {
|
|
|
936
971
|
for (const entry of await readdir(finalPath)) {
|
|
937
972
|
if (entry === ".git")
|
|
938
973
|
throw new Error("Workspace patches cannot retain Git administrative state.");
|
|
939
|
-
await cp(join(finalPath, entry), join(worktree, entry), { recursive: true, preserveTimestamps: true, force: false });
|
|
974
|
+
await cp(join(finalPath, entry), join(worktree, entry), { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
|
|
940
975
|
}
|
|
941
976
|
await execFileAsync("git", ["add", "--force", "--intent-to-add", "--all"], { cwd: worktree });
|
|
942
977
|
let stdout;
|
|
@@ -951,12 +986,14 @@ async function workspacePatch(startPath, finalPath, finalTreeDigest) {
|
|
|
951
986
|
throw error;
|
|
952
987
|
}
|
|
953
988
|
const patch = Buffer.from(stdout);
|
|
954
|
-
await cp(startPath, applied, { recursive: true, preserveTimestamps: true, force: false });
|
|
989
|
+
await cp(startPath, applied, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
|
|
955
990
|
if (patch.length > 0) {
|
|
956
991
|
await writeFile(patchPath, patch, { flag: "wx", mode: 0o600 });
|
|
957
992
|
await execFileAsync("git", ["apply", "--binary", patchPath], { cwd: applied });
|
|
958
993
|
}
|
|
959
|
-
|
|
994
|
+
// Git omits empty directories, including legitimate link targets. An
|
|
995
|
+
// invalid reconstructed tree is a patch limitation, so use the snapshot.
|
|
996
|
+
const appliedTreeDigest = await digestWorkspaceTree(applied).catch(() => undefined);
|
|
960
997
|
if (appliedTreeDigest !== finalTreeDigest) {
|
|
961
998
|
return undefined;
|
|
962
999
|
}
|
|
@@ -966,7 +1003,7 @@ async function workspacePatch(startPath, finalPath, finalTreeDigest) {
|
|
|
966
1003
|
await rm(temporaryRoot, { recursive: true, force: true });
|
|
967
1004
|
}
|
|
968
1005
|
}
|
|
969
|
-
async function workspaceSnapshot(finalPath, finalTreeDigest) {
|
|
1006
|
+
async function workspaceSnapshot(finalPath, finalTreeDigest, use) {
|
|
970
1007
|
const temporaryRoot = await mkdtemp(join(tmpdir(), "ebo-workspace-snapshot-"));
|
|
971
1008
|
const snapshotPath = join(temporaryRoot, "workspace.tar.gz");
|
|
972
1009
|
const snapshotParent = join(temporaryRoot, "source");
|
|
@@ -974,19 +1011,43 @@ async function workspaceSnapshot(finalPath, finalTreeDigest) {
|
|
|
974
1011
|
const extracted = join(temporaryRoot, "extracted");
|
|
975
1012
|
try {
|
|
976
1013
|
await mkdir(snapshotParent);
|
|
977
|
-
await cp(finalPath, snapshotRoot, { recursive: true, preserveTimestamps: true, force: false });
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
1014
|
+
await cp(finalPath, snapshotRoot, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
|
|
1015
|
+
const child = spawn(TAR_COMMAND, [...(process.platform === "darwin" ? ["--no-mac-metadata"] : []), "-czf", "-", "-C", snapshotParent, "workspace"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
1016
|
+
let stderr = "";
|
|
1017
|
+
child.stderr.on("data", (chunk) => { stderr = (stderr + chunk.toString("utf8")).slice(-8192); });
|
|
1018
|
+
const exited = new Promise((resolveExit, reject) => {
|
|
1019
|
+
child.once("error", reject);
|
|
1020
|
+
child.once("close", (code) => code === 0 ? resolveExit() : reject(new Error(`Workspace snapshot tar failed (${code}): ${stderr.trim()}`)));
|
|
981
1021
|
});
|
|
982
|
-
|
|
983
|
-
|
|
1022
|
+
let size = 0;
|
|
1023
|
+
const limit = new Transform({ transform(chunk, _encoding, callback) {
|
|
1024
|
+
size += chunk.length;
|
|
1025
|
+
callback(size > MAX_WORKSPACE_SNAPSHOT_BYTES ? new Error("Workspace snapshot exceeds the 1 GiB compressed byte limit.") : null, chunk);
|
|
1026
|
+
} });
|
|
1027
|
+
const streamed = pipeline(child.stdout, limit, createWriteStream(snapshotPath, { flags: "wx", mode: 0o600 }));
|
|
1028
|
+
try {
|
|
1029
|
+
await Promise.all([exited, streamed]);
|
|
1030
|
+
}
|
|
1031
|
+
finally {
|
|
1032
|
+
if (child.exitCode === null)
|
|
1033
|
+
child.kill("SIGKILL");
|
|
1034
|
+
await Promise.allSettled([exited, streamed]);
|
|
1035
|
+
}
|
|
984
1036
|
await mkdir(extracted, { mode: 0o700 });
|
|
985
|
-
|
|
1037
|
+
// macOS libarchive consumes genuine ._* AppleDouble files even when its
|
|
1038
|
+
// metadata flags are disabled. Native pax preserves them with copyfile off.
|
|
1039
|
+
if (process.platform === "darwin") {
|
|
1040
|
+
await execFileAsync("/bin/pax", ["-rz", "-p", "p", "-f", snapshotPath], {
|
|
1041
|
+
cwd: extracted, env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
await execFileAsync(TAR_COMMAND, ["-xzpf", snapshotPath, "-C", extracted]);
|
|
1046
|
+
}
|
|
986
1047
|
if (await digestWorkspaceTree(join(extracted, "workspace")) !== finalTreeDigest) {
|
|
987
1048
|
throw new Error("Bounded workspace snapshot cannot reproduce the final workspace tree.");
|
|
988
1049
|
}
|
|
989
|
-
|
|
1050
|
+
await use(snapshotPath);
|
|
990
1051
|
}
|
|
991
1052
|
finally {
|
|
992
1053
|
await rm(temporaryRoot, { recursive: true, force: true });
|
package/dist/src/verifiers.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
4
|
-
import { chmod, lstat, mkdir, mkdtemp, open, readdir, readFile, realpath, rm, utimes } from "node:fs/promises";
|
|
4
|
+
import { chmod, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rm, utimes } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { performance } from "node:perf_hooks";
|
|
@@ -72,10 +72,10 @@ async function digestWorkspaceEntries(workspacePath, includeTimestamps, signal)
|
|
|
72
72
|
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
73
73
|
throw new Error("Workspace root is not a directory.");
|
|
74
74
|
hash.update(`root\0${metadata.mode & 4095n}\0${includeTimestamps ? `${workspaceTimestamp(metadata)}\0` : ""}`);
|
|
75
|
-
await hashWorkspaceDirectory(root, "", hash, includeTimestamps, signal);
|
|
75
|
+
await hashWorkspaceDirectory(root, "", hash, includeTimestamps, signal, root);
|
|
76
76
|
return `sha256:${hash.digest("hex")}`;
|
|
77
77
|
}
|
|
78
|
-
async function hashWorkspaceDirectory(directory, relativeDirectory, hash, includeTimestamps, signal) {
|
|
78
|
+
async function hashWorkspaceDirectory(directory, relativeDirectory, hash, includeTimestamps, signal, root = directory) {
|
|
79
79
|
assertNotAborted(signal);
|
|
80
80
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
81
81
|
entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
@@ -83,11 +83,31 @@ async function hashWorkspaceDirectory(directory, relativeDirectory, hash, includ
|
|
|
83
83
|
const path = join(directory, entry.name);
|
|
84
84
|
const relativePath = posix.join(relativeDirectory, entry.name);
|
|
85
85
|
const metadata = await lstat(path, { bigint: true });
|
|
86
|
-
if (metadata.isSymbolicLink())
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
if (metadata.isSymbolicLink()) {
|
|
87
|
+
// Preserve link text, never hash the referent through the link. Both its
|
|
88
|
+
// lexical target and resolved chain must stay in this workspace.
|
|
89
|
+
const target = await readlink(path);
|
|
90
|
+
const contained = (candidate) => {
|
|
91
|
+
const rel = relative(root, candidate);
|
|
92
|
+
return !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
93
|
+
};
|
|
94
|
+
if (isAbsolute(target) || !contained(resolve(dirname(path), target))) {
|
|
95
|
+
throw new Error(`Workspace symbolic link escapes its root at "${relativePath}".`);
|
|
96
|
+
}
|
|
97
|
+
let resolved;
|
|
98
|
+
try {
|
|
99
|
+
resolved = await realpath(path);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new Error(`Workspace symbolic link is dangling or cyclic at "${relativePath}".`);
|
|
103
|
+
}
|
|
104
|
+
if (!contained(resolved))
|
|
105
|
+
throw new Error(`Workspace symbolic link escapes its root at "${relativePath}".`);
|
|
106
|
+
hash.update(`symlink\0${relativePath}\0${metadata.mode & 4095n}\0${includeTimestamps ? `${workspaceTimestamp(metadata)}\0` : ""}${Buffer.byteLength(target)}\0${target}`);
|
|
107
|
+
}
|
|
108
|
+
else if (metadata.isDirectory()) {
|
|
89
109
|
hash.update(`directory\0${relativePath}\0${metadata.mode & 4095n}\0${includeTimestamps ? `${workspaceTimestamp(metadata)}\0` : ""}`);
|
|
90
|
-
await hashWorkspaceDirectory(path, relativePath, hash, includeTimestamps, signal);
|
|
110
|
+
await hashWorkspaceDirectory(path, relativePath, hash, includeTimestamps, signal, root);
|
|
91
111
|
}
|
|
92
112
|
else if (metadata.isFile()) {
|
|
93
113
|
if (metadata.nlink > 1n)
|
|
@@ -46,6 +46,43 @@ missing-evidence entries. Inspect the detail before retrying capture against a
|
|
|
46
46
|
retained workspace. A later successful capture is recovery evidence; it does
|
|
47
47
|
not change the original attempt's recorded failure.
|
|
48
48
|
|
|
49
|
+
When the native run completed but capture failed, recover capture without
|
|
50
|
+
executing the task again. Materialize the same frozen starting workspace, use
|
|
51
|
+
the same capture exclusions, and assemble a separate bundle with byte-identical
|
|
52
|
+
native evidence plus the recovered workspace outcome. Record the original
|
|
53
|
+
manifest digest, recovery time, capture-code identity and qualification beside
|
|
54
|
+
the recovered bundle. Preserve the original failed bundle. A later workspace
|
|
55
|
+
capture cannot prove the retained directory was unchanged since execution ended
|
|
56
|
+
unless a fingerprint was recorded then. Do not count the recovery as another
|
|
57
|
+
model trial or as an independent sample.
|
|
58
|
+
|
|
59
|
+
Workspace outcomes preserve relative symbolic links whose lexical targets and
|
|
60
|
+
resolved referents stay inside the workspace. Link text is hashed and copied
|
|
61
|
+
verbatim, not replaced by the referent's bytes. Empty-directory omission keeps
|
|
62
|
+
directories referenced by links. Absolute, escaping, dangling and
|
|
63
|
+
cyclic links fail capture. Patch application or snapshot extraction must still
|
|
64
|
+
reproduce the resulting tree digest. This does not relax task-archive admission
|
|
65
|
+
or verifier sandbox rules.
|
|
66
|
+
|
|
67
|
+
On macOS, snapshot creation disables generated AppleDouble metadata. Snapshot
|
|
68
|
+
verification uses the built-in `/bin/pax` with `COPYFILE_DISABLE=1` so genuine
|
|
69
|
+
`._*` files remain files. macOS tar may otherwise consume them as metadata,
|
|
70
|
+
changing the reconstructed tree. To extract a snapshot for inspection on macOS:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
mkdir restored
|
|
74
|
+
(cd restored && COPYFILE_DISABLE=1 /bin/pax -rz -p p -f /absolute/path/workspace.tar.gz)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Workspace capture tries a patch up to 64 MiB, then falls back to a snapshot
|
|
78
|
+
when the patch is too large or cannot reproduce the tree. Snapshots stream to
|
|
79
|
+
disk with a 1 GiB compressed limit and are extracted to verify the tree digest
|
|
80
|
+
before publication. Snapshot qualification also checks integrity without loading
|
|
81
|
+
the archive into memory. Exceeding the limit leaves explicit missing evidence
|
|
82
|
+
and retains the workspace; it never truncates the archive. These capture limits
|
|
83
|
+
are separate from portable-export limits. Configure known cache-directory
|
|
84
|
+
exclusions before a study rather than dropping files from an existing attempt.
|
|
85
|
+
|
|
49
86
|
Use a new output destination for derived records and reruns. Do not edit a
|
|
50
87
|
native bundle to make a validator accept it.
|
|
51
88
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-behavior-observatory",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Capture engineering-agent trajectories, evaluate behavior, and inspect cited evidence across harnesses.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"release/0.2.1/",
|
|
48
48
|
"release/0.2.2/",
|
|
49
49
|
"release/0.2.3/",
|
|
50
|
+
"release/0.2.4/",
|
|
51
|
+
"release/0.2.5/",
|
|
50
52
|
"schemas/",
|
|
51
53
|
"scripts/atlas-grafana.sh"
|
|
52
54
|
],
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# v0.2.4 known limitations
|
|
2
|
+
|
|
3
|
+
The [v0.2.3 limitations](../0.2.3/KNOWN_LIMITATIONS.md) continue to apply.
|
|
4
|
+
|
|
5
|
+
- Snapshots remain bounded to 1 GiB compressed. Temporary source and extracted
|
|
6
|
+
copies also require free disk space; large cache trees increase capture time.
|
|
7
|
+
- Known cache exclusions must be declared before a study. Capture does not
|
|
8
|
+
silently omit additional files to fit a limit.
|
|
9
|
+
- Opaque snapshots remain restricted evidence; larger capture support does
|
|
10
|
+
not make them portable sanitized exports.
|
|
11
|
+
- Full-parallel testing exposed intermittent interruption/cleanup failures.
|
|
12
|
+
Release acceptance runs tests serially, as in earlier releases.
|
|
13
|
+
- npm audit reports three dependency findings (two moderate, one high) through
|
|
14
|
+
the pinned Cursor SDK's connect-node/undici dependency chain. npm reports no
|
|
15
|
+
available automatic fix for this dependency graph. This patch leaves those
|
|
16
|
+
dependencies unchanged; passing acceptance is not a clean security audit.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# v0.2.4: Stream large workspace snapshots
|
|
2
|
+
|
|
3
|
+
Workspace snapshot capture no longer buffers the compressed archive in child
|
|
4
|
+
process stdout. Archives stream to disk, pass extraction and tree-digest checks,
|
|
5
|
+
and publish atomically. Qualification verifies snapshot integrity in chunks.
|
|
6
|
+
|
|
7
|
+
The compressed snapshot limit increases from 128 MiB to 1 GiB. Output above
|
|
8
|
+
that limit fails explicitly and retains partial evidence. The 64 MiB patch
|
|
9
|
+
limit, capture exclusions, archive format, and portable-export policy are
|
|
10
|
+
unchanged. No runtime pins or dependencies change.
|
|
11
|
+
|
|
12
|
+
## Verification
|
|
13
|
+
|
|
14
|
+
Regression tests cover a 129 MiB incompressible snapshot, streamed-write failure
|
|
15
|
+
cleanup, no-clobber publication, and bounded integrity checks. A retained real
|
|
16
|
+
workspace that failed on v0.2.3 was captured separately as a 605,143,040-byte
|
|
17
|
+
snapshot, with its extracted tree digest verified. Original evidence was not
|
|
18
|
+
rewritten.
|
|
19
|
+
|
|
20
|
+
Run `npm ci` and `npm run acceptance` on Node 24.19.0 for the full suite,
|
|
21
|
+
documentation and package checks, and two byte-identical package builds.
|
|
22
|
+
See [known limitations](KNOWN_LIMITATIONS.md) and the
|
|
23
|
+
[reproducibility manifest](reproducibility.json).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "ebo.release-reproducibility/v1",
|
|
3
|
+
"release": {
|
|
4
|
+
"name": "engineering-behavior-observatory",
|
|
5
|
+
"version": "0.2.4"
|
|
6
|
+
},
|
|
7
|
+
"runtime": {
|
|
8
|
+
"node": "24.19.0",
|
|
9
|
+
"claudeAgentSdk": "0.3.258",
|
|
10
|
+
"openhandsAgentServer": "1.46.0",
|
|
11
|
+
"deepseekClient": "0.1.1-rc.2",
|
|
12
|
+
"deepseekProtocol": "0.1.1-rc.2",
|
|
13
|
+
"deepseekRuntime": "0.1.1-rc.2",
|
|
14
|
+
"codexAppServer": "0.153.4",
|
|
15
|
+
"grafana": "13.2.0",
|
|
16
|
+
"grafanaInfinity": "4.0.0",
|
|
17
|
+
"piSdk": "0.85.1",
|
|
18
|
+
"cursorSdk": "1.0.31"
|
|
19
|
+
},
|
|
20
|
+
"commands": [
|
|
21
|
+
"npm ci",
|
|
22
|
+
"npm run acceptance"
|
|
23
|
+
],
|
|
24
|
+
"fixtureCoverage": {
|
|
25
|
+
"agent-sdk": "frozen queue entry through qualified capture, approved export, normalization, configurable judging, review, aggregation, and Atlas",
|
|
26
|
+
"openhands-agent-server": "pinned REST/WebSocket stream-final reconciliation, workspace/outcome evidence, partial capture, and retained evaluation",
|
|
27
|
+
"deepseek-harness": "official-client JSON-RPC composition, receipt-to-idle completion, stderr, interruption, shutdown, swaps, and retained evaluation",
|
|
28
|
+
"codex-app-server": "owned stdio lifecycle, full history, OTLP, interruption/failure, export, and retained evaluation; configurable workspace-write networking with explicit offline queue propagation",
|
|
29
|
+
"acceptance-cases": "seeded ordering, secret scanning, partial attempts, native references, abstention, disputes, denominators, and unsupported comparisons",
|
|
30
|
+
"pi-sdk": "frozen queue, passive hooks, durable session, partial cleanup, export, retained normalization and evaluation",
|
|
31
|
+
"cursor-sdk": "frozen queue, official store, callbacks/history, bounded cancellation, export, retained normalization and evaluation",
|
|
32
|
+
"workspace-capture": "streamed snapshot capture, atomic publication and qualification beyond 128 MiB; bounded 1 GiB compressed output; retained error and partial evidence"
|
|
33
|
+
},
|
|
34
|
+
"determinism": {
|
|
35
|
+
"stable": [
|
|
36
|
+
"task, run, attempt, event, assertion, aggregate, and Atlas case identities",
|
|
37
|
+
"fixture, native-reference, normalized-dataset, source/cohort, archive, and package digests",
|
|
38
|
+
"seeded queue and review-sample ordering"
|
|
39
|
+
],
|
|
40
|
+
"excluded": [
|
|
41
|
+
"native and EBO observation timestamps",
|
|
42
|
+
"lifecycle start/finish timestamps",
|
|
43
|
+
"provider timing and usage",
|
|
44
|
+
"temporary workspace and output paths"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"fixtures": {
|
|
48
|
+
"tests/fixtures/task-packet.valid.v1.json": "ac2ef1043c0cc16bf4c21d05c1dc880ca1b4b8cc2f9ea7653328e91f0b0e2681",
|
|
49
|
+
"test/fixtures/agent-sdk-normalizer/complete.input.json": "7285dee088517e5de38ceda84fc7c58d9303949c7448ab3b5e10a5a2d2ebdfe3",
|
|
50
|
+
"test/fixtures/agent-sdk-normalizer/complete.expected.jsonl": "d32fa3a013bc179f1673f7e1a89b8a721bb8ddaf63eba5bc8a3f14f1cc148ed8",
|
|
51
|
+
"contracts/openhands-agent-server-v1.44.1.json": "e7a07977688a0703b15751a8a1ab17f29be45f9fdf438017e0dbcbc49cca37b0",
|
|
52
|
+
"contracts/openhands-agent-server-v1.46.0.json": "455cdddd2c206cb2f20245c2189b92e773657ec14d651236baa532b2dbadeb28",
|
|
53
|
+
"test/fixtures/openhands/v1.44.1/streamed-events.json": "d751083ba2ef94c9865117db481cc2f4933772bbbbb6b9fcf60571563715c1ea",
|
|
54
|
+
"test/fixtures/openhands/v1.44.1/final-events.json": "551db9904e860004d604c923692b5c5d3aaee966220492214c60003d2df415b6",
|
|
55
|
+
"test/fixtures/deepseek/golden-success.json": "f3867d38169bf0e28f64aba1dd60553f21cf07f41f81b4bad48261e49fa3ca88",
|
|
56
|
+
"test/fixtures/deepseek/golden-interrupted.json": "79e1f8404df1ee3b562d0bdbc0b2ee712ffc6d5774992b92970814ea815677b7",
|
|
57
|
+
"test/fixtures/deepseek/compositions/minimal/composition.json": "72fe4368e105f1bad82b7fdf8380e92c9fa689d8985f6ab91f534f0baf3eda02",
|
|
58
|
+
"contracts/codex-app-server-0.153.4/manifest.json": "e62281ff5e5d1cc71d07763b85e997ccc9ddbad9aa82a469da4181ee4b1890a8",
|
|
59
|
+
"test/fixtures/codex/legacy-0.150.1.dataset.json": "f71906b009e546afbf3b4e79396f223181382f3b3d8795e9cfcd032398a9d6bf",
|
|
60
|
+
"test/fixtures/behavior-assertions/abstained.json": "7f1962864f664c794925a67d93074e7b150af0f57c58041a81e062990d641197",
|
|
61
|
+
"test/fixtures/behavior-assertions/disputed.review.json": "450d3d9efe7feefaf7500b3ad20ab2803b412c7e53b91763125e15a83be87981",
|
|
62
|
+
"test/fixtures/comparison/exact.json": "cafa68e94b8d50dc308ddcfc2e73aa139c828b4ecd1e1c762bbc51bf1b44501e",
|
|
63
|
+
"test/fixtures/structural-observations/golden.json": "bad71a3a93524b76c0a2fdbb58e98e1e65be46cae14acf18ac89d9ba76e03e8a",
|
|
64
|
+
"test/atlas-fixture.ts": "9f03ce9cbcd0e5d3e3a5916866554ceb1c21f1c8712e58529499c1ea499f048d",
|
|
65
|
+
"test/pi.test.ts": "a00997c5617ca178e19b25b0d483cbdce8ce7c306d4c871557945b31a2e657cf",
|
|
66
|
+
"test/cursor-sdk.test.ts": "753094f127b11dc392ec2244c9edddabec27aa94347bb6159a292129ed2a2822",
|
|
67
|
+
"examples/cursor-sdk/README.md": "a2cf481123e7f3deb1c5a682ba208a775c3f91c35e1a6c1a428c517c79692618",
|
|
68
|
+
"examples/cursor-sdk/capture-profile.json": "d1a5787875daf5523b3783f259b891c256a904cbb10279d5be214af846305a2d",
|
|
69
|
+
"examples/cursor-sdk/harness.json": "2f1da6f2b17258d0ee59431f687696901ce3692589175d182f3a4bbe8dd4d689",
|
|
70
|
+
"examples/cursor-sdk/model.json": "ad8f31ff5d2387c02d79226679022358afafc61ba5dda73040e3f754530a230a",
|
|
71
|
+
"examples/cursor-sdk/native-limits.json": "40fe451f56b0863e79b42f5c85defbce678e1dc90dd248d15c430db176d71511",
|
|
72
|
+
"examples/cursor-sdk/native-tool-policy.json": "0fef98b36cd39de82914c5b068022c1335dadda24b7d6e7df398b2430e6a16e0"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# v0.2.5 known limitations
|
|
2
|
+
|
|
3
|
+
The [v0.2.4 limitations](../0.2.4/KNOWN_LIMITATIONS.md) continue to apply.
|
|
4
|
+
|
|
5
|
+
- Symlink support applies to workspace outcome capture, not task-archive
|
|
6
|
+
admission or permission to read outside the workspace.
|
|
7
|
+
- On macOS, use the documented native `pax` command when inspecting snapshots
|
|
8
|
+
containing genuine AppleDouble files. Default `tar` extraction can consume them.
|
|
9
|
+
- Capture-only recovery can use retained evidence and workspace files without
|
|
10
|
+
rerunning a model, but cannot reconstruct missing files or prove an earlier
|
|
11
|
+
workspace state without a fingerprint captured at that time.
|
|
12
|
+
- Workspace snapshots remain restricted evidence, not sanitized partner exports.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# v0.2.5: Preserve framework files in workspace capture
|
|
2
|
+
|
|
3
|
+
Workspace capture now preserves contained relative symbolic links, including
|
|
4
|
+
framework link chains and links to empty directories. Absolute, escaping,
|
|
5
|
+
dangling, and cyclic links remain rejected. Copies retain the original link
|
|
6
|
+
text, and patches that cannot reproduce the tree fall back to a snapshot.
|
|
7
|
+
|
|
8
|
+
On macOS, snapshot verification uses native `pax` to preserve genuine AppleDouble
|
|
9
|
+
files that macOS `tar` otherwise consumes as metadata. Snapshots remain tar.gz
|
|
10
|
+
archives. No runtime dependencies, pins, or serialized artifact formats change.
|
|
11
|
+
|
|
12
|
+
## Verification
|
|
13
|
+
|
|
14
|
+
Regression tests cover link preservation, invalid targets, empty target
|
|
15
|
+
directories, AppleDouble bytes, and long paths. A completed model attempt's
|
|
16
|
+
retained workspace was recovered as a 695,459,840-byte snapshot with a verified
|
|
17
|
+
extracted tree digest, without another model call or rewriting original evidence.
|
|
18
|
+
The recovery qualified with an explicit missing-telemetry-receipt gap; it records
|
|
19
|
+
the later capture time rather than claiming an exact end-of-turn snapshot.
|
|
20
|
+
|
|
21
|
+
Run `npm ci` and `npm run acceptance` on Node 24.19.0 for tests, documentation
|
|
22
|
+
and package checks, and two byte-identical package builds. See
|
|
23
|
+
[known limitations](KNOWN_LIMITATIONS.md), the
|
|
24
|
+
[reproducibility manifest](reproducibility.json), and the
|
|
25
|
+
[workspace inspection guide](../../docs/guides/evidence-and-sharing.md).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "ebo.release-reproducibility/v1",
|
|
3
|
+
"release": {
|
|
4
|
+
"name": "engineering-behavior-observatory",
|
|
5
|
+
"version": "0.2.5"
|
|
6
|
+
},
|
|
7
|
+
"runtime": {
|
|
8
|
+
"node": "24.19.0",
|
|
9
|
+
"claudeAgentSdk": "0.3.258",
|
|
10
|
+
"openhandsAgentServer": "1.46.0",
|
|
11
|
+
"deepseekClient": "0.1.1-rc.2",
|
|
12
|
+
"deepseekProtocol": "0.1.1-rc.2",
|
|
13
|
+
"deepseekRuntime": "0.1.1-rc.2",
|
|
14
|
+
"codexAppServer": "0.153.4",
|
|
15
|
+
"grafana": "13.2.0",
|
|
16
|
+
"grafanaInfinity": "4.0.0",
|
|
17
|
+
"piSdk": "0.85.1",
|
|
18
|
+
"cursorSdk": "1.0.31"
|
|
19
|
+
},
|
|
20
|
+
"commands": [
|
|
21
|
+
"npm ci",
|
|
22
|
+
"npm run acceptance"
|
|
23
|
+
],
|
|
24
|
+
"fixtureCoverage": {
|
|
25
|
+
"agent-sdk": "frozen queue entry through qualified capture, approved export, normalization, configurable judging, review, aggregation, and Atlas",
|
|
26
|
+
"openhands-agent-server": "pinned REST/WebSocket stream-final reconciliation, workspace/outcome evidence, partial capture, and retained evaluation",
|
|
27
|
+
"deepseek-harness": "official-client JSON-RPC composition, receipt-to-idle completion, stderr, interruption, shutdown, swaps, and retained evaluation",
|
|
28
|
+
"codex-app-server": "owned stdio lifecycle, full history, OTLP, interruption/failure, export, and retained evaluation; configurable workspace-write networking with explicit offline queue propagation",
|
|
29
|
+
"acceptance-cases": "seeded ordering, secret scanning, partial attempts, native references, abstention, disputes, denominators, and unsupported comparisons",
|
|
30
|
+
"pi-sdk": "frozen queue, passive hooks, durable session, partial cleanup, export, retained normalization and evaluation",
|
|
31
|
+
"cursor-sdk": "frozen queue, official store, callbacks/history, bounded cancellation, export, retained normalization and evaluation",
|
|
32
|
+
"workspace-capture": "Contained relative symlinks, empty link targets, AppleDouble files and long paths; streamed snapshots with verified tree digests and unchanged 1 GiB bound"
|
|
33
|
+
},
|
|
34
|
+
"determinism": {
|
|
35
|
+
"stable": [
|
|
36
|
+
"task, run, attempt, event, assertion, aggregate, and Atlas case identities",
|
|
37
|
+
"fixture, native-reference, normalized-dataset, source/cohort, archive, and package digests",
|
|
38
|
+
"seeded queue and review-sample ordering"
|
|
39
|
+
],
|
|
40
|
+
"excluded": [
|
|
41
|
+
"native and EBO observation timestamps",
|
|
42
|
+
"lifecycle start/finish timestamps",
|
|
43
|
+
"provider timing and usage",
|
|
44
|
+
"temporary workspace and output paths"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"fixtures": {
|
|
48
|
+
"tests/fixtures/task-packet.valid.v1.json": "ac2ef1043c0cc16bf4c21d05c1dc880ca1b4b8cc2f9ea7653328e91f0b0e2681",
|
|
49
|
+
"test/fixtures/agent-sdk-normalizer/complete.input.json": "7285dee088517e5de38ceda84fc7c58d9303949c7448ab3b5e10a5a2d2ebdfe3",
|
|
50
|
+
"test/fixtures/agent-sdk-normalizer/complete.expected.jsonl": "d32fa3a013bc179f1673f7e1a89b8a721bb8ddaf63eba5bc8a3f14f1cc148ed8",
|
|
51
|
+
"contracts/openhands-agent-server-v1.44.1.json": "e7a07977688a0703b15751a8a1ab17f29be45f9fdf438017e0dbcbc49cca37b0",
|
|
52
|
+
"contracts/openhands-agent-server-v1.46.0.json": "455cdddd2c206cb2f20245c2189b92e773657ec14d651236baa532b2dbadeb28",
|
|
53
|
+
"test/fixtures/openhands/v1.44.1/streamed-events.json": "d751083ba2ef94c9865117db481cc2f4933772bbbbb6b9fcf60571563715c1ea",
|
|
54
|
+
"test/fixtures/openhands/v1.44.1/final-events.json": "551db9904e860004d604c923692b5c5d3aaee966220492214c60003d2df415b6",
|
|
55
|
+
"test/fixtures/deepseek/golden-success.json": "f3867d38169bf0e28f64aba1dd60553f21cf07f41f81b4bad48261e49fa3ca88",
|
|
56
|
+
"test/fixtures/deepseek/golden-interrupted.json": "79e1f8404df1ee3b562d0bdbc0b2ee712ffc6d5774992b92970814ea815677b7",
|
|
57
|
+
"test/fixtures/deepseek/compositions/minimal/composition.json": "72fe4368e105f1bad82b7fdf8380e92c9fa689d8985f6ab91f534f0baf3eda02",
|
|
58
|
+
"contracts/codex-app-server-0.153.4/manifest.json": "e62281ff5e5d1cc71d07763b85e997ccc9ddbad9aa82a469da4181ee4b1890a8",
|
|
59
|
+
"test/fixtures/codex/legacy-0.150.1.dataset.json": "f71906b009e546afbf3b4e79396f223181382f3b3d8795e9cfcd032398a9d6bf",
|
|
60
|
+
"test/fixtures/behavior-assertions/abstained.json": "7f1962864f664c794925a67d93074e7b150af0f57c58041a81e062990d641197",
|
|
61
|
+
"test/fixtures/behavior-assertions/disputed.review.json": "450d3d9efe7feefaf7500b3ad20ab2803b412c7e53b91763125e15a83be87981",
|
|
62
|
+
"test/fixtures/comparison/exact.json": "cafa68e94b8d50dc308ddcfc2e73aa139c828b4ecd1e1c762bbc51bf1b44501e",
|
|
63
|
+
"test/fixtures/structural-observations/golden.json": "bad71a3a93524b76c0a2fdbb58e98e1e65be46cae14acf18ac89d9ba76e03e8a",
|
|
64
|
+
"test/atlas-fixture.ts": "9f03ce9cbcd0e5d3e3a5916866554ceb1c21f1c8712e58529499c1ea499f048d",
|
|
65
|
+
"test/pi.test.ts": "a00997c5617ca178e19b25b0d483cbdce8ce7c306d4c871557945b31a2e657cf",
|
|
66
|
+
"test/cursor-sdk.test.ts": "753094f127b11dc392ec2244c9edddabec27aa94347bb6159a292129ed2a2822",
|
|
67
|
+
"examples/cursor-sdk/README.md": "a2cf481123e7f3deb1c5a682ba208a775c3f91c35e1a6c1a428c517c79692618",
|
|
68
|
+
"examples/cursor-sdk/capture-profile.json": "d1a5787875daf5523b3783f259b891c256a904cbb10279d5be214af846305a2d",
|
|
69
|
+
"examples/cursor-sdk/harness.json": "2f1da6f2b17258d0ee59431f687696901ce3692589175d182f3a4bbe8dd4d689",
|
|
70
|
+
"examples/cursor-sdk/model.json": "ad8f31ff5d2387c02d79226679022358afafc61ba5dda73040e3f754530a230a",
|
|
71
|
+
"examples/cursor-sdk/native-limits.json": "40fe451f56b0863e79b42f5c85defbce678e1dc90dd248d15c430db176d71511",
|
|
72
|
+
"examples/cursor-sdk/native-tool-policy.json": "0fef98b36cd39de82914c5b068022c1335dadda24b7d6e7df398b2430e6a16e0"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/release/README.md
CHANGED
|
@@ -5,6 +5,8 @@ For downloads and published artifacts, see
|
|
|
5
5
|
|
|
6
6
|
| Version | Changes and verification | Support boundary |
|
|
7
7
|
| :--- | :--- | :--- |
|
|
8
|
+
| [0.2.5](0.2.5/README.md) | Preserve contained workspace symlinks and macOS AppleDouble files | [Known limitations](0.2.5/KNOWN_LIMITATIONS.md) |
|
|
9
|
+
| [0.2.4](0.2.4/README.md) | Stream workspace snapshots and integrity checks; support archives up to 1 GiB | [Known limitations](0.2.4/KNOWN_LIMITATIONS.md) |
|
|
8
10
|
| [0.2.3](0.2.3/README.md) | Retain workspace capture errors in partial-bundle reports | [Known limitations](0.2.3/KNOWN_LIMITATIONS.md) |
|
|
9
11
|
| [0.2.2](0.2.2/README.md) | Configurable Codex tool networking, enabled by default for workspace-write | [Known limitations](0.2.2/KNOWN_LIMITATIONS.md) |
|
|
10
12
|
| [0.2.1](0.2.1/README.md) | Documentation redesign, Apache-2.0, and npm distribution setup | [Known limitations](0.2.1/KNOWN_LIMITATIONS.md) |
|