/**
* 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