/** * Resolve a caller-supplied path against a root, refusing anything that * lands outside it. * * The containment rule existed, in one private function inside the local * sandbox provider, and the filesystem tools never reached it. They called * `resolve(workingDirectory, input.path)` bare — so `path: "../../.."` * resolved to whatever is above the working directory and the tool read it * happily. That holds with no sandbox configured at all, which is the * common case, so the escape did not need a misconfiguration to reach: a * model that asks for a parent directory gets one. * * `relative()` rather than a `startsWith` prefix test: a prefix test says * `/workspace-backup` is inside `/workspace`, and the whole point is to be * exact about the boundary. */ export declare function resolveWithin(root: string, candidate: string | undefined): string; /** * What a refusal says after naming the path: where the tools DO reach, and * how that is widened. * * It used to end at "Tools may only reach inside X", which reads as a wall. * It is not one: a host can add a directory to the session, and a host that * reviews such paths (`QueryParams.outsideRootAccess: 'review'`) turns them * into an approval request before the tool ever runs, so a model that meets * this text is in a turn where that did not happen. Told only "may only", a * model either rewords the path forever or tells the user the file cannot be * read at all; told the route, it asks for the directory. */ export declare const OUTSIDE_ROOTS_GUIDANCE: (reach: string) => string; /** * The directories a tool may reach: the working directory first, then any * the host added for the session (`/add-dir`), then any path a review * approved for THIS call (`ToolContext.approvedPaths`). Relative paths * resolve against the first; an absolute path is accepted inside any. */ export declare function toolRoots(context: { readonly workingDirectory: string; readonly additionalDirectories?: readonly string[]; readonly approvedPaths?: readonly string[]; }): readonly string[]; /** * The absolute path `candidate` names when it lies outside every root, or * `undefined` when a tool would accept it as it stands. * * Decided with {@link resolveWithinAnyReal} — the resolver the file tools * run — so the review and the execution cannot disagree about which side of * the boundary a path is on, symlinks included. The returned path is the * lexical one, which is what {@link toolRoots} is then handed: a link inside * the working directory that points outside is approved under the name the * reviewer saw. */ export declare function pathOutsideRoots(roots: readonly string[], candidate: string | undefined): Promise; /** `resolveWithin` over several roots. See `toolRoots` for the order. */ export declare function resolveWithinAny(roots: readonly string[], candidate: string | undefined): string; /** `resolveWithinReal` over several roots: the first that contains the path, links followed. */ export declare function resolveWithinAnyReal(roots: readonly string[], candidate: string | undefined): Promise; export declare function isWithin(root: string, candidate: string): boolean; /** * The same containment rule, decided after symlinks are resolved. * * {@link resolveWithin} is lexical, and a lexical check is not a boundary for * a tool that then follows links. `./notes -> /etc` passes it, because * `./notes/passwd` climbs nothing on paper; the write lands in `/etc`. That is * CWE-59, *Improper Link Resolution Before File Access*, and the mitigation * CWE-22 states for the family is the ordering this function exists to get * right: canonicalize first, validate the canonical form, never the input. * * `atomicWriteFile` makes the ordering load-bearing rather than theoretical. * It resolves the destination and writes THROUGH a link on purpose — so that * editing a linked file updates the target instead of replacing the link with * a regular file — which is correct behaviour and, paired with a lexical * check, is check-then-follow. * * Three things this has to get right that a single `realpath` does not: * * 1. **The root can itself be a symlink.** `os.tmpdir()` is one on macOS * (`/var/folders/…` under `/private`). Canonicalizing only the candidate * and comparing against a raw root rejects every path in a temp directory — * a containment check that refuses everything is not safer, it is broken, * and it fails in exactly the environment tests run in. * 2. **The target may not exist.** `write` creates files, and `realpath` on a * missing path throws. So this canonicalizes the deepest ancestor that DOES * exist and appends the rest lexically. The remainder cannot hide a link, * because nothing is there to be one. * 3. **The lexical check still runs first.** It costs nothing, refuses the * common `../../..` before touching the filesystem, and its message names * the offending input — which the canonical comparison, working on two * absolute paths, cannot. * * What this does NOT give you is TOCTOU safety. A component swapped for a * symlink between this check and the open would still be followed; closing * that needs per-component `openat`/`O_NOFOLLOW`, which Node does not expose. * The threat here is a link that is already there — a repository that contains * one, or one an earlier tool call created — not an attacker racing the * process on the user's own machine. */ export declare function resolveWithinReal(root: string, candidate: string | undefined): Promise; //# sourceMappingURL=paths.d.ts.map