mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
7a0c47bfa4
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>
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
import { readdir, rmdir, unlink } from "node:fs/promises";
|
|
import { dirname, sep } from "node:path";
|
|
|
|
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
|
import {
|
|
assertFilePathIdentity,
|
|
assertPathIdentity,
|
|
replacedDirectory,
|
|
type DirectoryIdentity,
|
|
type FileIdentity,
|
|
} from "./SafeOutputTreeIdentity.js";
|
|
|
|
/** Remove only tracked files and empty directories, preserving successful progress. */
|
|
export const removeOwnedTree = async (input: {
|
|
readonly outputRoot: string;
|
|
readonly rootIdentity: DirectoryIdentity;
|
|
readonly directories: Map<string, DirectoryIdentity>;
|
|
readonly files: Map<string, FileIdentity>;
|
|
readonly preserveRoot?: boolean;
|
|
}): Promise<void> => {
|
|
const { outputRoot, rootIdentity, directories, files } = input;
|
|
const filesByDirectory = new Map<string, [string, FileIdentity][]>();
|
|
for (const entry of files) {
|
|
const directory = dirname(entry[0]);
|
|
const group = filesByDirectory.get(directory) ?? [];
|
|
group.push(entry);
|
|
filesByDirectory.set(directory, group);
|
|
}
|
|
const postorder = [...directories].sort(
|
|
([left], [right]) =>
|
|
right.slice(outputRoot.length).split(sep).length -
|
|
left.slice(outputRoot.length).split(sep).length,
|
|
);
|
|
for (const [path, identity] of postorder) {
|
|
await removeFiles(path, identity, filesByDirectory.get(path) ?? [], files);
|
|
const parentPath = dirname(path);
|
|
const parentIdentity =
|
|
parentPath === outputRoot ? rootIdentity : directories.get(parentPath);
|
|
if (parentIdentity === undefined) throw replacedDirectory(parentPath);
|
|
await assertPathIdentity(parentPath, parentIdentity);
|
|
await assertPathIdentity(path, identity);
|
|
await requireEmpty(path);
|
|
await rmdir(path);
|
|
directories.delete(path);
|
|
}
|
|
await removeFiles(
|
|
outputRoot,
|
|
rootIdentity,
|
|
filesByDirectory.get(outputRoot) ?? [],
|
|
files,
|
|
);
|
|
await assertPathIdentity(outputRoot, rootIdentity);
|
|
await requireEmpty(outputRoot);
|
|
if (!input.preserveRoot) await rmdir(outputRoot);
|
|
};
|
|
|
|
const removeFiles = async (
|
|
directory: string,
|
|
identity: DirectoryIdentity,
|
|
filesInDirectory: readonly (readonly [string, FileIdentity])[],
|
|
retainedFiles: Map<string, FileIdentity>,
|
|
): Promise<void> => {
|
|
for (const [path, fileIdentity] of filesInDirectory) {
|
|
await assertPathIdentity(directory, identity);
|
|
await assertFilePathIdentity(path, fileIdentity);
|
|
await unlink(path);
|
|
retainedFiles.delete(path);
|
|
}
|
|
};
|
|
|
|
const requireEmpty = async (directory: string): Promise<void> => {
|
|
if ((await readdir(directory)).length > 0)
|
|
throw new ArtifactReaderFailure(
|
|
"path",
|
|
`Unowned entries remain in extraction directory: ${directory}`,
|
|
);
|
|
};
|