UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

197 lines (196 loc) • 14.5 kB
/** * The static tool catalog's READ FACE (design/388 §2.1): `describeToolCatalog()` and the predicates * every former name table is now DERIVED from. Readable without an env, a deps object or a tool * instance — a host predicts a leg's roster from it (`predictMountedToolNames`), a policy module * derives its covered set from it, a shell renders it. * * Every predicate here answers over DEFINITIONS and folds to names with "any definition under this * name matches" — the rule that pins the two fail-open shapes a per-name table had (one name, two * definitions; the table could only hold one). * * Layer-0 vocabulary: depends on the leaf catalog and the face vocabulary only. */ import { type ToolCallPathTarget, type ToolCatalogEntry, type ToolFamily, type ToolMountTag, type ToolPathAccess } from "./tool-face.js"; /** The whole static catalog, one frozen row per definition (design/388 §2.1 `describeToolCatalog()`). * Replaces the retired name-keyed exports `HAND_TOOL_EFFECTS` / `DEFAULT_BUDGET_EXEMPT_TOOLS` / * `COMPACTABLE_TOOLS`: filter this instead (`e.mountedBy.includes("hands")`, `e.face.budgetExempt`, * `e.face.compactable`). */ export declare function describeToolCatalog(): readonly ToolCatalogEntry[]; /** Every definition under a wire name (a name may carry more than one — the two shells). */ export declare function catalogEntriesNamed(name: string): readonly ToolCatalogEntry[]; /** Every wire name the catalog knows, aliases included (a set — the two shells fold to one name). */ export declare function catalogToolNames(): ReadonlySet<string>; /** A-11/12/13/14 — tools this engine can CONFINE BY PATH on a write: `pathTarget.access ≠ read`. * Today {Write, Edit, NotebookEdit}. */ export declare function pathConfinableWriteToolNames(): ReadonlySet<string>; /** * What the persistence judge reads off ONE tool: its name and the three face members (all optional — a mounted * instance carries the ones its definition declared, read straight off the object). A member the tool does not * carry falls to the catalog's declaration for its NAME (a bare caller tool wearing a catalog name, such as a * hands-less host's own `Write`, takes that definition's face — never a wider one). */ export interface PersistenceProbeTool { readonly name: string; readonly effect?: import("./tool-spec.js").ToolEffect; readonly pathTarget?: { readonly access: ToolPathAccess; }; readonly family?: ToolFamily; } /** The leg facts the judge reads beside the tools. */ export interface PersistenceFacts { /** Names the leg's frozen tool-face snapshot excludes — an excluded tool is unmounted, so it persists nothing. */ readonly exclude?: readonly string[]; /** The leg's by-name effect map: the fallback for a tool that declares no `effect` of its own (silent ⇒ `"write"`). */ readonly effects?: ReadonlyMap<string, import("./tool-spec.js").ToolEffect>; /** The hand band is mounted on a REMOTE env: its file tools write the sandbox filesystem, not the host-side memory * root, so their presence proves nothing about persistence there. */ readonly remoteHands: boolean; } /** * The names, among a run's MOUNTED tools (instances, whose face members sit on the object itself), the persistence * judgment passes. The judgment's inputs beyond the tools (the exclude snapshot, the by-name effect map, whether the * hand band is remote) are leg facts only the run holds; {@link rosterCanPersistMemory} is the verdict built on it. */ export declare function persistenceCapableToolNames(tools: ReadonlyArray<PersistenceProbeTool>, facts: PersistenceFacts): string[]; /** * The roster-wide persistence verdict a run reads: the deployment's own `declared` statement wins (the only honest * channel for a caller tool that persists through its own closure, which no inference can see); otherwise some tool * of the mounted roster passes {@link persistenceCapableToolNames}' judgment. */ export declare function rosterCanPersistMemory(tools: ReadonlyArray<PersistenceProbeTool>, facts: PersistenceFacts, declared?: boolean): boolean; /** * The CANDIDATE set, from the static catalog alone: the names whose definition passes the same judgment on no leg * facts at all (nothing excluded, no by-name effect map, hands not remote) — the tools that COULD persist on a leg * that mounts them. A prediction, not a verdict: whether a given run's roster persists is * {@link rosterCanPersistMemory}, which the runner calls with the facts the run holds. */ export declare function persistenceCandidateToolNames(): string[]; /** A-8 — every definition that names a path target at all (read or write): the file-tool glob lane. */ export declare function pathTargetingToolNames(): ReadonlySet<string>; /** * The SHELL FACE: every definition whose `command` argument is a shell command line the command grammar judges (today the two * `Bash` definitions and `Monitor`). A `Bash(<command>)` rule reaches a call that answers to any of these names; the coarse * command-name gate's default list, the shell read station and the shell seat are this set. Derived from the rows' rule faces, * never a list of its own. */ export declare function shellFaceToolNames(): ReadonlySet<string>; /** {@link shellFaceToolNames}' membership for one name — any definition under the name on the shell face. */ export declare function isShellFaceToolName(name: string): boolean; /** The tools whose declared path target is a READ (a file read, a search root): the file read station's membership. */ export declare function readTargetToolNames(): ReadonlySet<string>; /** A-10 — the micro-compaction whitelist (`compactable: true`). */ export declare function compactableToolNames(): ReadonlySet<string>; /** A-5 — the aggregate-budget exemptions (`budgetExempt: true`). */ export declare function budgetExemptToolNames(): ReadonlySet<string>; /** The value a call's args carry under a path target: `param` first, then each alias, first non-empty string wins. */ export declare function pathTargetValue(args: unknown, pt: { param: string; aliases?: readonly string[]; }): string | undefined; /** * CC parity: Read/Edit/Write take the file path as `file_path`. Read the target the same way * EVERYWHERE the path is consumed from a tool-call's args — not just the tool body but ALSO every gate that * extracts the write target to confine it (sensitive-path, session allowDirs, the fs-write gate). A gate that only * read `args.path` would see `undefined` for a `file_path` call and either fail-OPEN (circumvent the guard — a real * hole) or fail-closed (deny a legitimate write). Single source. * * The `path` arm is deliberately BROADER than what Read/Edit/Write accept: their schema requires * `file_path` (fs-shared.ts FILE_PATH_PARAMS; Write alone repairs the `path` spelling onto it BEFORE validation, * `coerceWriteArguments`, so a gate never sees the alias there), but this reader is * shared with gates that judge OTHER tool vocabularies — Glob's real `path` parameter, deployment-authored tools — * and a gate must never see LESS than the tool it is confining might act on. So the asymmetry is the design: the * schema decides what a caller may send, this decides what gets judged, and this side over-reads on purpose. * * `file_path` wins; an empty/non-string `file_path` falls back to `path` (NOT an alias for Read/Edit/Write, whose * schema requires `file_path`). Empty string ⇒ undefined (treated as "no path") so every caller's `=== undefined` * check is sufficient — no separate length-0 guard needed. It lives beside {@link pathTargetValue} and is that * reader over the one slot above: the engine's compaction metadata reads it too, and the engine never imports tools/. */ export declare function fileArgPath(args: unknown): string | undefined; /** The path target of a wire name — the FIRST definition's (every definition under one name declares * the same target; the conformance test pins that). */ /** A gated call as every fence sees it: the wire name, the args, and the OBJECT face a prepared leg stamped on the * request (absent on a BARE request — a policy invoked outside a Runner). */ export interface ToolCallLike { toolName: string; args: unknown; face?: { pathTarget?: ToolCallPathTarget; names?: readonly string[]; } | undefined; } type CallPathTarget = ToolCallPathTarget; /** * design/388 B6/B17 — the path target a PROTECTIVE fence reads (a deny-list judging a write's target: sensitive paths, * frozen paths, write protection, the transcript gate; edit tracking): the object face's own declaration first, the * catalog's declaration for the NAME as the FLOOR — for a bare request (a policy invoked outside a Runner) and for a * face that declares none. Reading the catalog for an undeclared same-name tool can only TIGHTEN a deny-list (it adds * a slot to judge — the pre-388 posture, never wider), which is exactly why this floor is wrong for a grant. * * The floor is a `member` seat over every name the call answers to (the face's `names`, else the call's name alone) in * the face's order — the name the call was made under first, then the wire name, then the other aliases — and the * first name the catalog declares a target for wins: a call made under a catalog name and resolved to a tool that * declares none keeps THAT name's slot, also when the tool answers to a second catalog name with another slot. * * #635: "can only tighten" is true of the SLOT and false of the RESOLUTION, so the floor hands over the first and * defaults the second (`pathTargetSlotOnly`). Telling a fence that a FOREIGN tool of the same name resolves its slot * against the project root is a claim about someone else's implementation: get it wrong and the judged path moves to * a different file, and the deny this floor exists to preserve simply misses. */ export declare function protectivePathTargetOf(call: ToolCallLike): CallPathTarget | undefined; /** The typed refusal a CONTAINMENT reader mints for a request that carries no face at all (#626 ② — design/388 B6's * bare-producer migration seam, closed). */ export declare const BARE_REQUEST_CODE = "policy.bare_request"; /** * The path target a CONTAINMENT fence reads (a grant admitting a write BY its path: session `allowDirs`, the fs-write * gate's accept/exempt dirs): the object * face's OWN declaration only — a same-name tool that declares no path target is an UNCONFIRMABLE write (the catalog's * slot may not be the one it writes; a decoy in that slot would buy an admission), so the fence fails closed. * * #626 ②: a BARE request (no face at all) is REFUSED, typed (`policy.bare_request`), never answered from the catalog * for the name. A grant is a claim about the tool's OWN slot, and only a prepared leg's roster can say which object * the call dispatches to — the catalog's declaration for a NAME is exactly the reading a same-name caller tool would * exploit. Inside a Runner every gated request carries the leg's face (the gate entry, the resume belts and the * pre-CAS edit recheck stamp it); a policy invoked outside a Runner stamps its own (`toolCallFaceOf(roster, name)`). * The PROTECTIVE floor above keeps reading the catalog for a bare request: tighten-only, a deny can only gain a slot. */ export declare function declaredPathTargetOf(call: ToolCallLike): CallPathTarget | undefined; /** Is this call a write a PROTECTIVE fence judges (a protected target whose access ≠ read)? The name sets are its static view. */ export declare function isProtectedWrite(call: ToolCallLike): boolean; /** Is this call a write a CONTAINMENT fence may ADMIT by its path (a declared target whose access ≠ read)? */ export declare function isDeclaredWrite(call: ToolCallLike): boolean; /** design/388 A-18 — the file-op access a compaction summary records for a LIVE tool name: only the FILE families' * path targets name a file (a search tool's `path` is a search ROOT, not a file the model read); every other name * records nothing (retired spellings in persisted transcripts read the frozen history table at the call site). */ export declare function fileOpAccessOf(name: string): ToolPathAccess | undefined; export declare function pathTargetOf(name: string): ToolCatalogEntry["face"]["pathTarget"]; /** * The rule vocabulary of a wire name from the STATIC catalog: `primaryParams` = the declared primaries, else * the path target's `param` + aliases; `params` = the declared vocabulary, else the path target's names — * and UNDEFINED when the catalog cannot know the full vocabulary (a definition whose schema is built inside * its factory and declares no `ruleFace.params`): the static half never claims a vocabulary it has not * read, so the rule compiler judges only what it knows and the leg's roster (which reads the schema) * judges the rest. `undefined` as a whole for a name with nothing to say. */ export declare function catalogRuleFaceOf(name: string): { primaryParams: string[]; params?: string[]; } | undefined; /** A-1 / server B-1 — the hand band's declared effects by name (any `hands` definition; the WRITE shell * wins the name when both are listed, matching the by-name table this replaces — the read-only shell's * own instance carries `effect: "read"` and the effect roster reports THAT). */ export declare function handBandEffects(): ReadonlyMap<string, NonNullable<ToolCatalogEntry["face"]["effect"]>>; /** A-3 / L4 — every `details.type` card the engine owns: the catalog's per-definition cards plus the * engine-minted ones (protocol channels, fs refusals). */ export declare function engineCardTypes(): ReadonlySet<string>; /** * L3 — the STATIC PREDICTION of a leg's core-mounted names for a set of active mount conditions: * every definition listed under at least one active tag. `scenario` and `optional` definitions mount * only when the vector names their tag (a host factory / a per-task opt-in decides them). Names are * folded (the two shells predict one `Bash`). */ export declare function predictMountedToolNames(active: ReadonlySet<ToolMountTag>): ReadonlySet<string>; /** The closed set as a Set, for callers that validate a tag read off the wire. */ export declare const TOOL_MOUNT_TAG_SET: ReadonlySet<string>; export { HANDS_BAND_TAGS, MOUNT_TAG_IN_HANDS_BAND } from "./tool-face.js";