UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

325 lines (324 loc) • 22.5 kB
/** * The ONE resolution of a call's EFFECTIVE PATH TARGET — the absolute path a path-targeting tool will * actually open — derived from the tool's OWN declaration ({@link ToolCallPathTarget}'s `base` / `absent` / * `patternParam`) rather than guessed, separately, by every fence. * * ## Why this module exists * * The permission-rule lanes resolved a call's path with one hard-coded rule (`liveCwd ?? root` for a * relative spelling, "no verdict" for an absent slot) while each tool resolved its own target its own way. * The three shapes that rule got wrong were all silent — a rule that names the very directory the tool * reads simply never fires: * * · Grep/Glob with NO `path` search the whole ROOT; the lanes read the absent slot as "no path", so a * deny on the root reached nothing; * · Glob with an ABSOLUTE `pattern` and no `path` searches the base the pattern carries * (`/proj/secret/**` ⇒ `/proj/secret`); the lanes never looked at `pattern`; * · Grep/Glob resolve a RELATIVE `path` against the ROOT (their describes say so, and both tools hand * `rootCanonical` to the resolver), while the lanes resolved it against the live `cd` — so after a * `cd` into a sibling directory the lane judged a path the tool would never open. * * The fix is not a fourth arm in the lanes: it is that "which base does this tool resolve against, and what * does it target when the slot is empty" is a FACT OF THE TOOL, so the tool declares it once and both * sides read the declaration. {@link pathTargetBaseOf} is the shared SELECTION (which base). * * ## The views of one target * * A target is seen three ways: its LEXICAL view (the spelling joined to the selected base, `.` / `..` folded — * zero IO), its CANONICAL view (the same spelling resolved by the execution environment: symlinks, aliases of the * tree, the disk's own case) and the HOPS of its leaf's symlink chain. {@link readPath} is the one producer of * those views — the tool's own resolution and its deny judgment read it, and so does {@link readCallTarget}, which * a Runner stamp point calls once per adjudicated call and hands to every rule lane as * `ToolCallRequest.targetReading`. Every rule lane reads a call's target through ONE reader, {@link callTargetViewsOf}: * the reading's views when the call carries one, else the call's own lexical view (a bare request). The judgment over * those views is every table's (`judgePathRows`, path-guard-rows.ts): any view a deny or ask row reaches is reached; an allow row admits a * call only when it admits every view of the declared reading. * * ## What it does NOT decide * * Nothing here judges anything: it answers "which absolute path is this call about". Containment, the * sensitive-path deny face and the rule precedence walk are unchanged and still run where they ran. * `undefined` means "this call names no path" (no declaration, a non-string slot, an absent slot that targets * nothing); a path the call does name whose base this reading lacks is `unreadable` — never a guess, never silence. * * RepoMap shares the search tools' root-relative `path` semantics but declares NO path target at all (no * rule family reaches it, and the rule grammar is derived from the catalog's path targets), so it has no * declaration to read; the day it declares one, it reads the same selector as everything else. */ import { canonicalizeTarget, type PathFamily, type PathIdentity, type Win32NamespaceRefusal } from "../tools/fs/safety.js"; import type { TargetViews } from "./path-guard-rows.js"; import type { ExecutionEnv } from "../internal/harness-types.js"; import type { ToolCallPathTarget } from "./tool-face.js"; import { type ToolCallLike } from "./tool-registry.js"; /** The bases of ONE CALL a target resolves against: the task root, and the live tracked working directory * (absent when no tracker moved or the caller keeps none). Facts of the call, never of the tool. The * tree's FAMILY is read from them once per call ({@link pathFamilyOf}). */ export interface ToolPathBases { readonly root?: string | undefined; readonly cwd?: string | undefined; } /** * WHICH base this target's relative spellings resolve against, as SPELLED by the caller (no normalization: * the tool hands this straight to `canonicalizeTarget`, which resolves win-form and symlinked spellings the * fences must not pre-fold). `undefined` ⇒ the deployment gave no such base, so a relative spelling in this * slot names nothing this reader can locate. */ export declare function pathTargetBaseOf(pt: Pick<ToolCallPathTarget, "base"> | undefined, bases: ToolPathBases): string | undefined; /** * design/116 批④ #8 (CC 2.1.198 RWp, bundle :332987) — decompose an ABSOLUTE glob pattern into a base * directory + relative pattern: cut at the last separator before the first glob metachar (`* ? [ {`); a * pattern with no metachar splits dirname/basename. `undefined` for a relative pattern (the caller keeps it * as-is) or when no usable base can be derived; a namespace refusal ({@link win32NamespaceScreen}, run * BEFORE the metachar scan so `\\?\` is a prefix, never a `?` wildcard) is answered by its word. * Absoluteness and the separator set are the FAMILY's: on a POSIX tree `C:\\x\\*` is a relative pattern. * * Lives HERE rather than beside the search engine because two readers need the identical cut: Glob's own * scope resolution and the path fences that must know which directory an absolute pattern really searches. */ export declare function splitAbsoluteGlobPattern(pattern: string, tree: PathFamily | undefined): { baseDir: string; relativePattern: string; } | { refused: Win32NamespaceRefusal; } | undefined; /** * The reading of a call's path slot under ONE declaration: `path` = the absolute target's identity; `unreadable` * = the slot spells a target (or targets its base) but the base it needs is not supplied or not a usable base of * this tree (fed to TIGHTENING rows only, as the ask a person clears — never to the allow arm); `refused` = a namespace refusal of the * spelling itself (a rule speaks about no such spelling: no verdict, no ask — the fs throat delivers the * refusal with the same word); `undefined` = no target in this reading. */ export type PathTargetReading = { readonly kind: "path"; readonly identity: PathIdentity; readonly family: PathFamily; } | { readonly kind: "unreadable"; readonly reason: string; } | { readonly kind: "refused"; readonly reason: Win32NamespaceRefusal; } | undefined; /** * The absolute target this call is about under ONE reading of its path slot, as a {@link PathTargetReading}. * `pt` is the reading (design/388 B6/B17: the PROTECTIVE target for a tightening fence, the DECLARED target * for a grant) — this function never picks between the two. * * The order is the tool's own: * ① a spelling in the slot wins — fully qualified as spelled, otherwise against {@link pathTargetBaseOf}'s base; * ② otherwise, when the declaration says an absent slot means `none`, the call is about no path; * ③ otherwise a `patternParam`'s ABSOLUTE pattern supplies its own base directory; * ④ otherwise the base itself (the tool searches all of it). * * A base the caller did not supply yields `unreadable`: a relative spelling with nothing to resolve against * names a file this reading cannot locate — claiming it names `/<spelling>` would be a fabrication, and * claiming it names nothing would let every rule about that file stay silent. */ export declare function effectivePathTargetOf(call: ToolCallLike, pt: ToolCallPathTarget | undefined, bases: ToolPathBases): PathTargetReading; /** * The SELECTION half of {@link effectivePathTargetOf}, before any identity is applied: the spelling the tool opens and * the base it resolves that spelling against — the slot's own value, or an absolute pattern's base directory — or, * with `spelled` absent, the base itself (the tool searches all of it). A caller that resolves canonically (the tool, * the read face's own station) reads this; the rule lanes read the lexical identity above. Same order as above. */ export declare function effectivePathSpellingOf(call: ToolCallLike, pt: ToolCallPathTarget | undefined, bases: ToolPathBases): { readonly spelled?: string; readonly base: string | undefined; } | { readonly refused: Win32NamespaceRefusal; } | undefined; /** * THE path prelude: ONE spelling → the identity of the path the TOOL will open it as. Every reading of a spelling passes * through here — the target slot's own value, a `patternParam`'s derived base, an empty slot's base, the views of * {@link readPath} — in one order: the family ({@link readingFamilyOf}), the namespace screen, the anchor step * ({@link anchorSpelling} against `declaredBase` — a relative spelling with no usable base is `unreadable`), the identity. * A win-form spelling on a POSIX tree is therefore an ordinary relative name under the DECLARED base — the same file the fs * throat opens — and a `/x` on a win tree lands on the base's drive. Nothing is case-folded and `~` is not expanded here: * a fold is a judge's, an expansion a reader's. */ export declare function placeSpelling(spelled: string, declaredBase: string | undefined, bases: ToolPathBases): PathTargetReading; /** The family one reading is taken in: the tree's (its root, else its cwd), or — for an address no tree places — the * spelling's own ({@link familyFor} `"own"`), so a treeless `C:\x` and a treeless `/x` are each an address in their family. */ export declare function readingFamilyOf(bases: ToolPathBases, spelled: string): PathFamily; /** * ONE reading of a call's path slot as VIEWS. `path`: the LEXICAL view (the spelling joined to the selected base and * folded — a native spelling of the tree's family), the CANONICAL view when the environment answered (absent ⇒ not asked * or no answer: an absent view takes no part in a judgment and is never a refusal) and the HOPS of the leaf's symlink * chain when the environment declares one. `unread`: a view the environment was ASKED for did not answer (the canonical * one, or the hops of a chain the canonical view says was followed) — the allow direction then admits nothing * ({@link callTargetViewsOf}: an allow must see every view where a rule names it). `unreadable` / `refused` read as in * {@link PathTargetReading}. */ export type CallTargetView = { readonly kind: "path"; readonly lexical: string; readonly canonical?: string; readonly hops?: readonly string[]; readonly unread?: true; } | { readonly kind: "unreadable"; readonly reason: string; } | { readonly kind: "refused"; readonly reason: Win32NamespaceRefusal; }; /** * A call's TARGET READING: the views of each reading of its path slot — `protective`, the tightening direction's * ({@link protectivePathTargetOf}), and `declared`, the allow direction's ({@link declaredPathTargetOf}; absent on a * request that carries no face). It carries no root and no cwd: the call's own `root` / `cwd` are the only copies. */ export interface CallTargetReading { readonly protective?: CallTargetView; readonly declared?: CallTargetView; /** Where each location on the way to a rule row's head that this leg's lanes hold LEADS in the leg's environment (a * link, a firmlink, an alias of the tree) — the rows' twins are judged there too. A fact of the judging leg, taken * once per location per leg; absent when no row head leads elsewhere. */ readonly leads?: ReadonlyMap<string, string>; } /** The environment's answer the canonical view rests on — the throat's own, so the tool keeps reading the refusal it gave. */ export type CanonicalAnswer = Awaited<ReturnType<typeof canonicalizeTarget>>; /** The three views of one spelling as {@link readPath} produces them. */ export interface PathViews { readonly lexical: PathTargetReading; /** `undefined` ⇒ the environment was not asked (the contract's `lexical` views, no environment, no tree, or a spelling * this reading cannot locate). */ readonly canonical?: CanonicalAnswer; readonly hops: readonly string[]; /** The hop chain was asked for (a link was followed) and could not be read. */ readonly hopsUnread?: true; } /** * The CONTRACT of one path reading — what the caller asks of a spelling, written at the call rather than implied by which * helper it happened to call. Two intents: * * · `"resolve"` ({@link readPath}): the spelling becomes a target. `views`: `"lexical"` — the placed spelling alone * ({@link placeSpelling}, zero I/O); `"canonical"` — and the execution environment's answer for where it lands (the * throat's own canonicalizer); `"chain"` — and the hops of the leaf's symlink chain wherever a link may be on the way. * The base a relative spelling needs is the reading's `base` argument (the tool's declared selection): absent ⇒ the * relative spelling is `unreadable`, never guessed; a namespace spelling the tree refuses is `refused` with its word; a * chain the environment cannot read is marked `hopsUnread`, never a refusal. * · `"validate-coordinate"` / `"validate-grant"` ({@link validatePath}): the value was HANDED to the engine as an address * (a configured home, a configuration root, a checkpoint's coordinates, a probe's grant offer) and is checked, never * repaired — no base, no tree, its own spelling its family source. A coordinate must be a string fully qualified in its * own family; a grant must also be bounded and free of control characters, already in normal form, not a family root, * free of a Win32 alias component and outside every device namespace (a repaired directory is a different directory * than the one a card would show). */ export type PathContract = { readonly intent: "resolve"; readonly views: "lexical" | "canonical" | "chain"; } | { readonly intent: "validate-coordinate" | "validate-grant"; }; /** The display bound on a validated grant (a probe's read-root offer): generous next to a real `PATH_MAX`, tight enough that * the seat cannot become an unbounded prose channel onto an approval card. */ export declare const READ_ROOT_CANDIDATE_DIR_MAX = 1024; /** * THE validator of a path value the engine was handed ({@link PathContract} `validate-*`): the value itself when it holds * under the contract, else `undefined`. Every seat that accepts a configured or recorded address reads it through here, so * "is this an address" is answered once — a UNC with no share (`\\x`) is no address in any family. */ export declare function validatePath(value: unknown, contract: Extract<PathContract, { readonly intent: "validate-coordinate" | "validate-grant"; }>): string | undefined; /** * The ONE producer of a target's views: `spelled` against `base` (the base the tool's declaration selected), under the * tree `bases` names, as far as `contract` asks. Lexical through the one prelude ({@link placeSpelling}); canonical through * the throat's own canonicalizer; the hops (when the contract asks for the chain and the canonical view answered) through * the leaf's chain. The tool's resolution and deny judgment read this, and so does {@link readCallTarget}; an environment * that THROWS propagates here (each caller holds its own posture: the tool refuses, the reading leaves the view absent). */ export declare function readPath(env: ExecutionEnv | undefined, spelled: string, base: string | undefined, bases: ToolPathBases, contract: Extract<PathContract, { readonly intent: "resolve"; }>, signal?: AbortSignal): Promise<PathViews>; /** The identity of an ABSOLUTE spelling under the tree `bases` names — the target side's one prelude (family, namespace * screen, identity) for a path a judge already holds (a canonical key, a hop, a lexical view). `undefined` when the * spelling is not an absolute path of that tree. */ export declare function absoluteTargetIdentityOf(spelled: string, bases: ToolPathBases): PathIdentity | undefined; /** * A call's target reading under ONE call's facts: its `root` / `cwd` (the bases the stamp point reads off the leg) and * the execution environment `env` (absent ⇒ the lexical views alone — the pre-claim recheck, which has no environment). * A canonical view the environment cannot answer (or throws on) is absent, never a refusal (an absent view does not vote). * The two readings share their views when they select the same spelling against the same base (one canonicalization). */ export declare function readCallTarget(env: ExecutionEnv | undefined, call: ToolCallLike & { readonly root?: string | undefined; readonly cwd?: string | undefined; }, signal?: AbortSignal, leads?: ReadonlyMap<string, string | undefined>): Promise<CallTargetReading>; /** The gate's entry records the loop's reading of this call's arguments against the face it stamps on the call. */ export declare function stampArgsReading(face: object, read: (args: unknown) => unknown): void; /** * `args` as the called tool will run them — its argument preparation and schema coercions, the reading the loop gave the * model's call — for the call `req` belongs to; `args` itself when nothing was stamped (a bare request outside a Runner: * a host judging a call reads it the tool's way first). Every seat that adopts a rewrite (a hook's, a policy's, a * person's edit) takes it through here before anything judges it, so what is judged is what runs. A value the reading * leaves as it is comes back as ITSELF (the same object: a target reading taken for it still describes it). */ export declare function argsAsTheToolReads(req: { readonly face?: object | undefined; }, args: unknown): unknown; /** `args` through one call's reading `read` (the face-less seat: the resumed call reads through its live tool): ITSELF when the * reading leaves its bytes as they are, else the reading's value, recorded as read from `args`. */ export declare function argsReadBy(read: (args: unknown) => unknown, args: unknown): unknown; /** The value `args` was read from, when a reading moved it; otherwise `args` itself. */ export declare function argsReadFrom(args: unknown): unknown; /** * `req` with its arguments replaced by `args` AS THE TOOL READS THEM ({@link argsAsTheToolReads}) and — when `req` carries * a reading of its own target — a reading of the NEW args taken the same way (same environment, same root / cwd, same * face). Every seat that rewrites a request's arguments and hands it on (a composition's rewrite cascade, a re-judgment of * the final or an edited form) goes through here, so the next layer never judges a request its reading does not describe * nor arguments the tool would run differently. `signal` is the CURRENT call's: a reading never carries one call's * cancellation into another's. */ export declare function withArgsReading<R extends { readonly args: unknown; readonly targetReading?: CallTargetReading | undefined; readonly face?: object | undefined; }>(req: R, args: unknown, signal: AbortSignal | undefined): Promise<R>; /** The request's target reading, when it describes the request's own `args` (the object it was taken for); otherwise * nothing — a rule lane then judges the lexical view it always judged. Every rule lane reads the reading through here. */ export declare function callTargetReadingOf(req: { readonly args: unknown; readonly targetReading?: CallTargetReading | undefined; }): CallTargetReading | undefined; /** * THE one reader of a call's target for every rule lane, in one direction's reading (`tighten` = the protective reading, * `allow` = the declared one): the views of the call's target reading when it carries one of its own args (lexical, * canonical, the leaf chain's hops), else the call's own lexical view — resolved against the call's `root` / `cwd` when it * carries either, else against `bare` (a host's direct call of a policy: the bases it declared). `undefined` = the call * names no path in this reading (or a spelling the throat refuses — no rule speaks about it); an `unreadable` answer = * the call names a path this reading cannot locate — or, in the allow reading, the target's resolved location is not in * hand (no reading, no canonical view, or a view that was asked for and did not answer: `unread`). An allow by path is * judged on where the target resolves; its spelling alone never admits it. */ export declare function callTargetViewsOf(req: ToolCallLike & { readonly root?: string | undefined; readonly cwd?: string | undefined; readonly targetReading?: CallTargetReading | undefined; }, direction: "tighten" | "allow", bare?: ToolPathBases): TargetViews | undefined; /** * A leg's STAMP seat: `req` with a target reading of its own args, or `undefined` when there is nothing to stamp — the * leg's lanes hold no path rule, the request already carries a reading of its args, or its args are not an object. The * reading is taken under the request's own `root` / `cwd` when it carries them, else the leg's (the same values every * Runner seat stamps), and memoized per args object within one call (its `toolCallId`), so every lane reading one call * costs one reading. `undefined` * rather than a resolved request, so a seat with nothing to do adds no suspension point. */ export type CallTargetStamp = <R extends ToolCallLike & { readonly toolCallId?: string; readonly root?: string | undefined; readonly cwd?: string | undefined; readonly targetReading?: CallTargetReading | undefined; }>(req: R, signal?: AbortSignal) => Promise<R & { readonly targetReading: CallTargetReading; }> | undefined; /** The stamp seat of one leg: its execution environment, its root and live working directory, whether its lanes hold a * path rule (read per call — an activated skill can add one), and where the heads of the rows its lanes hold lead in its * environment (resolved once per location per leg). */ export declare function legCallTargetStamp(leg: { readonly env: ExecutionEnv; readonly root: string; readonly cwd: () => string | undefined; readonly judgesPaths: () => boolean; readonly leads: () => Promise<ReadonlyMap<string, string | undefined>>; }): CallTargetStamp;