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