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