Files
rea/src/artifacts/SafeOutputTreeIdentity.ts
T
Edgar Arakelyan 7a0c47bfa4 feat(environment): export an environment manifest as an agent environment bundle (#4999)
Export an authenticated capture and environment manifest as a pinned agentenv bundle with provenance, a replay verifier and smoke task.

Followups preserve immutable input ownership, bounded discovery, caller paths and numeric values; bind bundle writes and package copying to retained filesystem owners; verify POSIX modes; retain cleanup authority; and verify framework/npm executable selection. Default MCP remains stdio.

Validation: pinned Linux toolchain, typecheck/build/lint/format/docs/knip, 452 focused tests, ten product-catalog cases, real GitHub response replay in the generated non-root Docker image, and the real pinned agentenv framework smoke workflow. Platform and resource-measurement limits are recorded in the feature verification document.

Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-20 05:29:43 +08:00

132 lines
4.0 KiB
TypeScript

import type { BigIntStats } from "node:fs";
import { lstat, readdir } from "node:fs/promises";
import { basename, dirname } from "node:path";
import type { FileHandle } from "node:fs/promises";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
export type DirectoryIdentity = Pick<BigIntStats, "dev" | "ino">;
export type FileIdentity = DirectoryIdentity;
/** Reject unowned, missing or replaced entries before an output tree is published. */
export const assertExactTreeEntries = async (
root: string,
directories: ReadonlyMap<string, DirectoryIdentity>,
files: ReadonlyMap<string, FileIdentity>,
): Promise<void> => {
const expected = new Map<string, Set<string>>();
expected.set(root, new Set());
for (const directory of directories.keys())
expected.set(directory, new Set());
for (const path of directories.keys())
expected.get(dirname(path))?.add(basename(path));
for (const path of files.keys())
expected.get(dirname(path))?.add(basename(path));
for (const [directory, names] of expected) {
const entries = await readdir(directory);
const remaining = new Map<string, number>();
for (const name of names) {
const key = name.normalize("NFC");
remaining.set(key, (remaining.get(key) ?? 0) + 1);
}
let unexpected = entries.length !== names.size;
for (const entry of entries) {
const key = entry.normalize("NFC");
const count = remaining.get(key) ?? 0;
if (count === 0) unexpected = true;
else remaining.set(key, count - 1);
}
// HFS+ can return an equivalent decomposed spelling. Counts avoid merging
// distinct Linux names; the original paths must still resolve to owned IDs.
if (unexpected)
throw new ArtifactReaderFailure(
"path",
`Unowned or missing output entries: ${directory}`,
);
}
for (const [path, identity] of files)
await assertFilePathIdentity(path, identity);
};
export const readDirectoryIdentity = async (
path: string,
): Promise<DirectoryIdentity> => {
const metadata = await lstat(path, { bigint: true });
if (!metadata.isDirectory()) throw replacedDirectory(path);
return { dev: metadata.dev, ino: metadata.ino };
};
export const assertPathIdentity = async (
path: string,
expected: DirectoryIdentity,
): Promise<void> => {
const metadata = await lstat(path, { bigint: true });
if (
!metadata.isDirectory() ||
metadata.dev !== expected.dev ||
metadata.ino !== expected.ino
)
throw replacedDirectory(path);
};
export const assertHandleIdentity = async (
handle: FileHandle,
expected: DirectoryIdentity,
path: string,
): Promise<void> => {
const metadata = await handle.stat({ bigint: true });
if (
!metadata.isDirectory() ||
metadata.dev !== expected.dev ||
metadata.ino !== expected.ino
)
throw replacedDirectory(path);
};
export const readFileIdentity = async (
handle: FileHandle,
path: string,
): Promise<FileIdentity> => {
const metadata = await handle.stat({ bigint: true });
if (!metadata.isFile())
throw new ArtifactReaderFailure(
"path",
`Extraction file identity changed: ${path}`,
);
return { dev: metadata.dev, ino: metadata.ino };
};
export const assertFilePathIdentity = async (
path: string,
expected: FileIdentity,
): Promise<void> => {
const metadata = await lstat(path, { bigint: true });
if (
!metadata.isFile() ||
metadata.dev !== expected.dev ||
metadata.ino !== expected.ino
)
throw new ArtifactReaderFailure(
"path",
`Extraction file identity changed: ${path}`,
);
};
export const isAbsent = async (path: string): Promise<boolean> =>
lstat(path).then(
() => false,
(cause: unknown) => {
if (isNotFound(cause)) return true;
throw cause;
},
);
export const replacedDirectory = (path: string): ArtifactReaderFailure =>
new ArtifactReaderFailure(
"path",
`Extraction directory identity changed: ${path}`,
);
const isNotFound = (cause: unknown): boolean =>
cause instanceof Error && "code" in cause && cause.code === "ENOENT";