mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 00:36:35 +00:00
Expose one plan/execute replay contract through both CLI and MCP with exact source, input, runtime, sandbox, limit, and export commitments. Run selected ESM and extracted CommonJS/Rspack factories in a disposable Linux x64 Bubblewrap namespace with a content-addressed Node ELF closure, architecture-checked seccomp, delegated systemd cgroup limits, bounded protocol capture, cancellation, and observable cleanup. Preserve left and right observations as Evidence, derive differential comparisons without erasing unknowns, add optional separately authorized reproducers, and fail closed on stale plans or unavailable host boundaries. Add source-owned parser, sanitizer, clipboard, denial, timeout, OOM, protocol-forgery, and cancellation coverage; a real Linux verifier and workflow; regenerated catalogs and TypeDoc; and updated the packaged REA analysis skill for the 83-tool inventory.
279 lines
8.2 KiB
TypeScript
279 lines
8.2 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import {
|
|
lstat,
|
|
mkdir,
|
|
open,
|
|
readFile,
|
|
realpath,
|
|
stat,
|
|
unlink,
|
|
type FileHandle,
|
|
} from "node:fs/promises";
|
|
import { dirname } from "node:path";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
|
|
import writeFileAtomic from "write-file-atomic";
|
|
import { z } from "zod";
|
|
|
|
import type { PermissionGrant } from "../domain/permissionPolicy.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
|
|
const grantSchema = z.object({
|
|
grant_id: z.string().min(1),
|
|
capability: z.enum([
|
|
"process_capture",
|
|
"browser_observe",
|
|
"electron_observe",
|
|
"evidence_read",
|
|
"evidence_write",
|
|
"investigation_input",
|
|
"investigation_workspace_read",
|
|
"investigation_workspace_write",
|
|
"snapshot_read",
|
|
"snapshot_write",
|
|
"artifact_extract",
|
|
"native_mount",
|
|
"reference_read",
|
|
"javascript_replay",
|
|
]),
|
|
roots: z.array(z.string()),
|
|
executables: z.array(z.string()),
|
|
environment_names: z.array(z.string()),
|
|
origins: z.array(z.string()).optional(),
|
|
network: z.enum(["none", "loopback", "external"]),
|
|
mount: z.boolean(),
|
|
lifetime: z.literal("project"),
|
|
operation_identity: z.string().nullable(),
|
|
expires_at: z.iso.datetime().nullable(),
|
|
});
|
|
|
|
const storeSchema = z.object({
|
|
schema_version: z.literal(1),
|
|
project_id: z.string().regex(/^project_[a-f0-9]{64}$/u),
|
|
project_root: z.string(),
|
|
grants: z.array(grantSchema).max(1_000),
|
|
});
|
|
|
|
export type ProjectPermissionStore = z.infer<typeof storeSchema>;
|
|
|
|
/** Owner-only project policy persistence failure. */
|
|
export class ProjectPermissionStoreError extends Error {
|
|
readonly _tag = "ProjectPermissionStoreError" as const;
|
|
|
|
constructor(
|
|
readonly reason:
|
|
| "project_not_found"
|
|
| "not_owner_only"
|
|
| "invalid"
|
|
| "locked"
|
|
| "io",
|
|
options?: ErrorOptions,
|
|
) {
|
|
super(`Project permission store failed: ${reason}`, options);
|
|
}
|
|
}
|
|
|
|
/** Derive relocation-explicit project identity from its canonical root. */
|
|
const identifyPermissionProject = async (
|
|
projectRoot: string,
|
|
): Promise<
|
|
Result<
|
|
{ readonly id: string; readonly root: string },
|
|
ProjectPermissionStoreError
|
|
>
|
|
> => {
|
|
try {
|
|
const root = await realpath(projectRoot);
|
|
return ok({
|
|
id: `project_${createHash("sha256").update(root).digest("hex")}`,
|
|
root,
|
|
});
|
|
} catch (cause: unknown) {
|
|
return err(new ProjectPermissionStoreError("project_not_found", { cause }));
|
|
}
|
|
};
|
|
|
|
/** Read and validate an owner-only store bound to the requested project. */
|
|
export const readProjectPermissionStore = async (
|
|
path: string,
|
|
projectRoot: string,
|
|
): Promise<
|
|
Result<ProjectPermissionStore | null, ProjectPermissionStoreError>
|
|
> => {
|
|
const project = await identifyPermissionProject(projectRoot);
|
|
if (!project.ok) return project;
|
|
try {
|
|
const metadata = await stat(path);
|
|
if ((metadata.mode & 0o077) !== 0)
|
|
return err(new ProjectPermissionStoreError("not_owner_only"));
|
|
const encoded = await readFile(path, "utf8");
|
|
let decoded: unknown;
|
|
try {
|
|
decoded = JSON.parse(encoded);
|
|
} catch (cause: unknown) {
|
|
return err(new ProjectPermissionStoreError("invalid", { cause }));
|
|
}
|
|
const parsed = storeSchema.safeParse(decoded);
|
|
if (
|
|
!parsed.success ||
|
|
parsed.data.project_id !== project.value.id ||
|
|
parsed.data.project_root !== project.value.root
|
|
)
|
|
return err(new ProjectPermissionStoreError("invalid"));
|
|
return ok(parsed.data);
|
|
} catch (cause: unknown) {
|
|
if (isNotFound(cause)) return ok(null);
|
|
return err(new ProjectPermissionStoreError("io", { cause }));
|
|
}
|
|
};
|
|
|
|
/** Atomically replace explicit project grants with owner-only permissions. */
|
|
export const writeProjectPermissionStore = async (
|
|
path: string,
|
|
projectRoot: string,
|
|
grants: readonly PermissionGrant[],
|
|
): Promise<Result<ProjectPermissionStore, ProjectPermissionStoreError>> => {
|
|
const project = await identifyPermissionProject(projectRoot);
|
|
if (!project.ok) return project;
|
|
const candidate = storeSchema.safeParse({
|
|
schema_version: 1,
|
|
project_id: project.value.id,
|
|
project_root: project.value.root,
|
|
grants,
|
|
});
|
|
if (!candidate.success)
|
|
return err(
|
|
new ProjectPermissionStoreError("invalid", { cause: candidate.error }),
|
|
);
|
|
try {
|
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
await writeFileAtomic(
|
|
path,
|
|
`${JSON.stringify(candidate.data, null, 2)}\n`,
|
|
{
|
|
encoding: "utf8",
|
|
mode: 0o600,
|
|
},
|
|
);
|
|
const metadata = await stat(path);
|
|
if ((metadata.mode & 0o077) !== 0) {
|
|
return err(new ProjectPermissionStoreError("not_owner_only"));
|
|
}
|
|
return ok(candidate.data);
|
|
} catch (cause: unknown) {
|
|
return err(new ProjectPermissionStoreError("io", { cause }));
|
|
}
|
|
};
|
|
|
|
/** Revoke one grant while serializing the complete read-modify-write cycle. */
|
|
export const revokeProjectPermissionGrant = async (
|
|
path: string,
|
|
projectRoot: string,
|
|
grantId: string,
|
|
): Promise<Result<boolean, ProjectPermissionStoreError>> => {
|
|
let lock: PermissionStoreLock | undefined;
|
|
try {
|
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
const acquired = await acquirePermissionStoreLock(path);
|
|
if (!acquired.ok) return acquired;
|
|
lock = acquired.value;
|
|
const current = await readProjectPermissionStore(path, projectRoot);
|
|
if (!current.ok) return current;
|
|
const grants = current.value?.grants ?? [];
|
|
const retained = grants.filter(({ grant_id }) => grant_id !== grantId);
|
|
if (retained.length === grants.length) return ok(false);
|
|
const written = await writeProjectPermissionStore(
|
|
path,
|
|
projectRoot,
|
|
retained,
|
|
);
|
|
return written.ok ? ok(true) : written;
|
|
} catch (cause: unknown) {
|
|
return err(new ProjectPermissionStoreError("io", { cause }));
|
|
} finally {
|
|
if (lock !== undefined) await releasePermissionStoreLock(lock);
|
|
}
|
|
};
|
|
|
|
interface PermissionStoreLock {
|
|
readonly path: string;
|
|
readonly handle: FileHandle;
|
|
}
|
|
|
|
const acquirePermissionStoreLock = async (
|
|
destination: string,
|
|
): Promise<Result<PermissionStoreLock, ProjectPermissionStoreError>> => {
|
|
const path = `${destination}.lock`;
|
|
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
try {
|
|
return ok(await createPermissionStoreLock(path));
|
|
} catch (cause: unknown) {
|
|
if (!isAlreadyExists(cause))
|
|
return err(new ProjectPermissionStoreError("io", { cause }));
|
|
if (await removeStalePermissionStoreLock(path)) continue;
|
|
if (attempt === 99)
|
|
return err(new ProjectPermissionStoreError("locked", { cause }));
|
|
await delay(10);
|
|
}
|
|
}
|
|
return err(new ProjectPermissionStoreError("locked"));
|
|
};
|
|
|
|
const createPermissionStoreLock = async (
|
|
path: string,
|
|
): Promise<PermissionStoreLock> => {
|
|
const handle = await open(path, "wx", 0o600);
|
|
try {
|
|
await handle.writeFile(`${String(process.pid)}\n`, "utf8");
|
|
await handle.sync();
|
|
return { path, handle };
|
|
} catch (cause: unknown) {
|
|
await handle.close().catch(() => undefined);
|
|
await unlink(path).catch(() => undefined);
|
|
throw cause;
|
|
}
|
|
};
|
|
|
|
const removeStalePermissionStoreLock = async (
|
|
path: string,
|
|
): Promise<boolean> => {
|
|
try {
|
|
const metadata = await lstat(path);
|
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > 32)
|
|
return false;
|
|
const encoded = (await readFile(path, "utf8")).trim();
|
|
if (!/^[1-9][0-9]{0,9}$/u.test(encoded)) return false;
|
|
if (processIsAlive(Number(encoded))) return false;
|
|
await unlink(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const processIsAlive = (pid: number): boolean => {
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch (cause: unknown) {
|
|
return errorCode(cause) !== "ESRCH";
|
|
}
|
|
};
|
|
|
|
const releasePermissionStoreLock = async (
|
|
lock: PermissionStoreLock,
|
|
): Promise<void> => {
|
|
await lock.handle.close().catch(() => undefined);
|
|
await unlink(lock.path).catch(() => undefined);
|
|
};
|
|
|
|
const isNotFound = (cause: unknown): boolean => errorCode(cause) === "ENOENT";
|
|
|
|
const isAlreadyExists = (cause: unknown): boolean =>
|
|
errorCode(cause) === "EEXIST";
|
|
|
|
const errorCode = (cause: unknown): unknown =>
|
|
typeof cause === "object" && cause !== null && "code" in cause
|
|
? cause.code
|
|
: undefined;
|