@sema-agent/core
Version:
Stateless, task-oriented AI agent core
92 lines (91 loc) • 6.7 kB
TypeScript
/**
* 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;