/** * The evidence sandbox (ES-P0-ACTION-CREDENTIAL-SCOPE, filesystem half). * * A semantic agent is told to write ONE file under `.project-ai/skill-outputs/` * and nothing else. Until now the only enforcement was a `git status` diff * AFTER the agent finished — which detects an escape but does not prevent it, * and says nothing about what happened between start and finish. The agent's * working directory was the real checkout, the same tree the Governor reads * evidence from two steps later. * * The sandbox moves the boundary to BEFORE and DURING execution: the agent runs * in a detached `git worktree` of the exact head commit, created in the OS * temp directory OUTSIDE the repository. The real checkout is never the * child's cwd and never named to it. After the run, the sandbox's own * `git status` (INCLUDING ignored paths — a gitignored escape is still an * escape) must show nothing but the expected evidence; the evidence is * validated IN the sandbox; and only then is that single file copied into the * real checkout's skill-outputs directory. Everything else the agent wrote * dies with the worktree. * * EVIDENCE ORIGIN (RH-20260820-003V finding 4). The only evidence the CLI ever * accepts is the file it validated in the sandbox and copied out itself. The * real `.project-ai/skill-outputs` directory is snapshotted (names, sizes, * content hashes) before the agent runs and compared after: ANY change to it * during the run — a forged `code-review.yaml` written by absolute path, a * pre-staged file rewritten, a junction planted — fails the run, whether or * not git would have reported it. The export itself refuses symlinked or * junctioned paths on either side (an agent that replaces the sandbox's * outputs directory with a junction into the real tree gets a refusal, not a * copy), and verifies the copied bytes hash-equal to what was validated. * * Stated limit: this is a working-directory boundary, not an OS sandbox. A * TRUSTED child runs as the same user; it can read the runner's filesystem * and reach the network, and a write to a path outside both the sandbox and * the real checkout is not seen. That is why UNTRUSTED content does not run at * all (child-boundary.ts — `ES_UNTRUSTED_ISOLATION_UNAVAILABLE`). */ export interface EvidenceSandbox { kind: 'worktree' | 'in-place'; /** The child's working directory. */ dir: string; /** `/.project-ai/skill-outputs` — where the agent must write. */ outputsDir: string; /** `git status --porcelain -uall --ignored=matching` of the sandbox tree. */ porcelain(): string; /** Remove the sandbox. Best-effort; never throws. */ dispose(): void; } export type SandboxFactory = (params: { repoRoot: string; headSha: string; }) => EvidenceSandbox; /** * The canonical absolute form of `p` for every containment comparison in this * module. `fs.realpathSync.native` resolves symlinks and junctions AND, on * Windows, expands 8.3 short names to their long form with the on-disk casing: * the GitHub Windows runner hands out its temp directory as * `C:\Users\RUNNER~1\...` while the same directory resolves to * `C:\Users\runneradmin\...`. It does that only for a path that EXISTS. A path * that does not exist yet (the evidence directory before its first export, the * destination file before the copy) used to fall back to `path.resolve`, which * keeps the alias, so the SAME directory compared unequal to its own * repository and the run was refused as "outside the repository" (the * 2026-09-02 Windows CI failure: sixteen tests across evidence-sandbox, * child-boundary and semantic-agents). * * Here the DEEPEST EXISTING ancestor is canonicalised and the not-yet-existing * suffix is re-joined to it unchanged. The suffix is already normalised by * `path.resolve` (no `.` or `..` segment survives it), so nothing in it can * climb back out of the ancestor. Fail closed: an existing ancestor that * cannot be resolved (EACCES, ELOOP, EPERM, ...) or a path with no resolvable * ancestor at all throws, and every caller turns a throw into a refusal. */ export declare function canonicalizePath(p: string): string; /** * Is `child` inside `parent` (or equal to it)? Both must be canonical (see * `canonicalizePath`) for the answer to mean anything: a lexical alias of the * same directory is a different string. On Windows `path.relative` compares * case-insensitively, matching the filesystem. */ export declare function isInside(child: string, parent: string): boolean; /** * Create a detached worktree of `headSha` in the OS temp directory. Throws * (fail closed) when the worktree cannot be created or would land inside the * repository — a sandbox inside the tree it guards is not a sandbox. */ export declare const createWorktreeSandbox: SandboxFactory; /** * An in-place sandbox for tests and for callers that have no git worktree to * offer. It provides NO boundary — the child's cwd is the repository itself — * and says so in its kind. Production `agents run` never uses it. */ export declare function createInPlaceSandbox(params: { dir: string; porcelain?: () => string; }): EvidenceSandbox; export interface DirectorySnapshotEntry { /** 'file' | 'dir' | 'symlink' | 'other' */ type: string; size: number; /** sha256 of the content for regular files; the link target for symlinks. */ digest: string; } export type DirectorySnapshot = Map; /** * Snapshot a directory recursively by RELATIVE path: type, size and content * hash. Symlinks are recorded as symlinks (by target), never followed — a * planted link is part of the picture, not a path to walk. A missing directory * is an empty snapshot (that is a legitimate pre-run state). */ export declare function snapshotDirectory(dir: string): DirectorySnapshot; /** Relative paths whose entry differs between two snapshots (added, removed, or changed), sorted. */ export declare function diffSnapshots(before: DirectorySnapshot, after: DirectorySnapshot): string[]; /** * The real checkout's evidence directory must be a plain directory chain under * the repository root — not a junction into HOME or anywhere else that a * repository author could plant so that the export lands outside the tree. * Called before any agent runs; throws (fail closed) on a link. */ export declare function assertPlainEvidenceDirectory(params: { repoRoot: string; outputsDir: string; }): void; export interface ExportedEvidence { path: string; /** sha256 of the bytes validated in the sandbox AND read back from the destination. */ sha256: string; bytes: number; } /** * Copy ONE validated evidence file out of the sandbox into the real * skill-outputs directory, and prove it arrived intact. Refuses: a feeder name * that is not plain ``; a source that is not a regular file; a source * or source directory that is a symlink/junction or resolves outside the * sandbox (the junction-into-the-real-tree shape); a destination chain with a * link in it; an oversized file; and a destination whose bytes do not hash- * equal the validated source after the copy. For a worktree sandbox, a source * that resolves to the destination is itself a refusal — it means the sandbox * directory IS the real directory, which only a planted link can arrange. */ export declare function exportEvidence(params: { sandbox: EvidenceSandbox; targetOutputsDir: string; feeder: string; maxBytes: number; /** The sha256 the caller validated in the sandbox; the export re-reads and compares. */ expectedSha256?: string; }): ExportedEvidence; //# sourceMappingURL=evidence-sandbox.d.ts.map