UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

92 lines (91 loc) • 6.7 kB
/** * ESTABLISHMENT — where a shell command's read candidates LAND once the filesystem has answered, as one * value the gate's shell read station holds per execution environment. The plan (`shell-read-plan.ts`) * decides WHICH words a command may read, `shell-read-write-targets.ts` which words it writes and at which seat; * this module decides WHERE each of them lands: the canonical * target of a spelling (symlinks resolved, a `..` applied the way the kernel applies it), the expansion of * a pattern (shell pathname semantics over `env.listDir`), the members of a traversal root (a bounded * recursive enumeration, dot files included, symlinked directories not followed), and the fail-closed arm * for what could not be established. The judge (`shell-read-judge.ts`) is downstream; nothing here decides * allow/ask/deny. * * Properties every consumer relies on: * 1. A NON-EXISTENT target is not a refusal — `cat nope.txt` keeps the shell's own "No such file". The * deepest EXISTING ancestor is canonicalized and the missing tail rejoined, so a symlinked PARENT still * cannot carry a target out. For a WRITE target a component the resolver cannot find but the filesystem * still holds (a link whose target does not exist) is not absent: a write through it creates the link's * target, so the target is established where the link lands (read one hop at a time, bounded) — or not at * all when the environment cannot read the link. * 2. An unanswerable filesystem manufactures no refusal on its own: a chain no ancestor answered degrades * to the lexical spelling on an environment that declares no authority for its resolver, and is * `unestablishable` on one that does (`canonicalPathIsAuthoritative`). * 3. An ISOLATED environment stays lexical, disclosed rather than silently degraded: the sandbox is the * containment there and `canonicalPath` is an RPC into a filesystem that is not this host's * (`boundaryIsTheSandbox`). The split is the DECLARED isolation capability, never the remote interface's * shape — a host lane or an SSH adapter implements the same interface and is resolved on its own paths. * 4. NO MATCH is not a refusal: an unmatched pattern is handed to the command verbatim, so an empty * expansion falls back to the literal spelling. * 5. What it cannot enumerate is `unestablishable`, never silently vouched: an unreadable directory, a * pattern construct the matcher does not model, an expansion or a traversal past * {@link MAX_GLOB_DIRS} / {@link MAX_GLOB_MATCHES}. * 6. Cost is paid only by candidates that carry a metacharacter or a traversal, and only past the roots' * own spelling. Inside one enumerated directory only the SYMLINK entries need their own `realpath`. * * NOT cached deliberately: a per-run memo keyed on the spelling would answer from a snapshot of where a * symlink pointed EARLIER in the run, which is the one thing establishment exists to observe. The residual * it cannot remove is the classic one — the link can be re-pointed between establishment and the shell's * own open; the pre-spawn re-establishment (`reestablish`) narrows that window to the one inside a single * call. */ import type { ExecutionEnv } from "../../internal/harness-types.js"; import type { ShellReadPlan, ShellReadSegment } from "./shell-read-plan.js"; import type { EstablishedRead, ShellReadBoundary } from "./shell-read-judge.js"; import type { HomeViewSource } from "./read-deny-home.js"; /** * How many directories one glob operand (or one traversal root) may enumerate, and how many names it may * match, before the leg gives up and answers `unestablishable`. * * Sized so the everyday shapes never hit them: 256 directories covers a two-level pattern over a source * tree's top level, and 8192 names covers `ls *` in a dependency directory an order of magnitude larger * than a normal one. Past those the answer is fail-closed, never a silent allow — an operand this leg could * not finish enumerating is exactly an operand whose expansion it cannot vouch for. */ export declare const MAX_GLOB_DIRS = 256; export declare const MAX_GLOB_MATCHES = 8192; /** The reads of one plan, established: per segment, in the plan's own order, with nested plans beside. */ export interface EstablishedPlan { readonly plan: ShellReadPlan; readonly reads: ReadonlyMap<ShellReadSegment, readonly EstablishedRead[]>; readonly nested: ReadonlyMap<ShellReadPlan, EstablishedPlan>; /** The roots the reads were judged against — canonical where the environment could say. */ readonly roots: readonly string[]; } /** ONE establishment leg per execution environment — the filesystem half of the shell read station. */ export interface ShellReadEstablisher { /** Whether this environment is judged lexically (an isolated sandbox — property 3). */ readonly lexicalOnly: boolean; /** Establish every read candidate of `plan` (nested plans included) against `boundary`'s roots. */ establish(plan: ShellReadPlan, boundary: ShellReadBoundary, signal?: AbortSignal): Promise<EstablishedPlan>; /** Establish the same plan again and report every candidate whose canonical target moved — the pre-spawn * identity re-check. Candidates that were lexical or unestablishable are not compared: nothing was * established to move. */ reestablish(previous: EstablishedPlan, boundary: ShellReadBoundary, signal?: AbortSignal): Promise<{ readonly changed: readonly EstablishedRead[]; }>; } /** * The PURE, lexical establishment of a whole plan — what an isolated sandbox gets, and what any consumer * with no environment in hand (a unit pin, a lexical pre-read) can compute. Patterns and traversal roots * are judged on their spelling alone here (KL-934a: the sandbox's own containment is the floor there). */ export declare function establishShellReadsLexically(plan: ShellReadPlan, boundary: ShellReadBoundary): EstablishedPlan; /** * The filesystem-resolving establisher for one environment and one root set. `readRoots` are the roots * as declared (canonical by contract); they are resolved once more per instance because the comparison is * against `realpath` output on both sides — an aliased root would otherwise refuse every read under itself. * `homeViews`: the home views of the placement the deny seats were compiled at (read-deny-home.ts); absent, the declared home's. */ export declare function createShellReadEstablisher(env: ExecutionEnv, readRoots: readonly string[], opts?: { readonly rootsCanonical?: boolean; readonly homeViews?: HomeViewSource; }): ShellReadEstablisher;