@sema-agent/core
Version:
Stateless, task-oriented AI agent core
357 lines (356 loc) • 28.3 kB
TypeScript
/**
* The tool gate's RECORD FLOOR — everything about how one gate pass is recorded and how it ends:
*
* - the ONE mint of a {@link GateOutcome} ({@link mintGateOutcome}) — the record of what a tool-gate pass
* decided, what ask it settled and who asked. The gate accumulates two facts on its way to an exit (WHO
* refused, written once at the refusing site; WHAT was settled, written at the resolution site) and hands
* them here; the mint assembles the record, runs the invariant screen, and refuses a record the engine
* composed wrong. Every projection of the outcome (the deny observer's payload, the call's `tool_end`
* frame, the call's occurrence record) is the object minted here, never a re-derivation;
* - the settlement LEDGER of a pass ({@link createSettlementLedger}) — the one rule every attestation read
* goes through;
* - the PASS RECORD ({@link GatePass}) — the per-call cells the gate's station groups read and write, and
* the hook-machinery seats they borrow, built once per call by `runToolGate` and handed to every station;
* - the EXITS — the two phase-1 short-circuits ({@link engineFailClosedExit}, {@link hookDenyExit}) and the
* single deny / allow exit of the adjudicate chain ({@link exitGate}), the only places a pass turns into a
* `ToolGateResult`; plus the two observer-facing helpers every exit uses (the payload isolation clone and
* the deployment-lane crash report).
*
* A floor module (layer 0 of the prepare/run path): the two station modules (`gate-fold.ts`, `gate-lanes.ts`)
* and the gate's dispatch in `hooks.ts` all reach DOWN for it, and it reaches down only for the vocabulary,
* the reminder envelope and the text helpers. Its `import type` edges up to `hooks.ts` name the gate's public
* contract (`ToolGateInput`, `ToolGateResult`, the observer payload), which stays declared there.
*/
import { type ClassifierDenyCause, type DeniedBy, type GateOutcome, type ModeRefusal, type ModeRelease, type Settlement } from "../gate-outcome.js";
import type { AutoModeClassifierRound } from "../auto-mode.js";
import { type Arbitration, type AskClassification, type AskFace, type AskOrigin, type AskOriginFacts } from "../ask-origin.js";
import { type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
import type { SafeNotifier } from "../safe-notify.js";
import type { PermissionDeniedPayload, PreToolUseResult, ToolGateInput, ToolGateResult } from "../hooks.js";
/** The ask a gate pass settled, as the gate captured it at its resolution site: the settlement the
* resolver composed and the origin word the ask carried. Both or neither (invariant I1). `licensed` is the engine record's
* digest of the bytes the settlement licensed: the allow exit keeps a settlement only for the bytes that run. */
export interface SettledAsk {
settlement: Settlement;
origin: AskOrigin;
licensed?: string;
}
/** The facts a gate pass hands the mint. `deniedBy` is present exactly when the pass refused; `cause` only
* beside it, when the refusing round was a classifier round that did not rule (the station's attestation,
* read once by the fold or the classifier step); `settled` exactly when the pass consumed an ask's
* settlement (a person's yes that was later vetoed included). */
export interface GateExitFacts {
deniedBy?: DeniedBy;
cause?: ClassifierDenyCause;
settled?: SettledAsk;
/** The classifier CLASSIFICATION this pass ran, when it ran one on a decider that can name its model.
* Orthogonal to `deniedBy`: one that ALLOWED is attributed too (a fallback model's allow is exactly the case
* an operator needs to see), which is why it rides the disposition's shared member, not the deny arm. */
classifier?: AutoModeClassifierRound;
/** The permission mode RELEASED the surviving ask (the allow exit's account of it); never beside `deniedBy`. */
released?: ModeRelease;
/** The permission mode REFUSED the surviving ask — only beside `deniedBy: "mode"`. */
modeRefusal?: ModeRefusal;
}
/**
* Mint the outcome. A defect here is an ENGINE defect (a refusing site that did not say who refused, a
* settlement without its origin, a person's yes beside a non-veto layer's deny) and throws: the gate's
* own composition is not a caller input, so there is no fail-closed "deny" to fall back to that would be
* more honest than the fault.
*/
export declare function mintGateOutcome(facts: GateExitFacts): GateOutcome;
/** A direct refusal by one layer, with no ask settled — the five short-circuit exits (a hook that threw or
* never answered, the hook's own deny, the compliance lock, the plan-mode block). */
export declare function directDeny(deniedBy: Exclude<DeniedBy, "ask_resolution">): GateOutcome;
/**
* The gate's settlement LEDGER for one pass — the one place "which ask did this pass consume" is written,
* with ONE rule for every read, all through the decision's engine record ({@link attestationOf}):
* - a record on an adjudication (the approval factory's wait ended inside a policy `check()`; an inherited
* lane resolved an ancestor's ask) is a settled ask of THIS call — an approval only for the bytes it licensed (an
* allow that runs other bytes runs unattributed); a deny carrying a REFUSAL record is the
* ask's own resolution (`ask_resolution`), a deny carrying a person's YES is a later layer's VETO (the yes
* stays on the settlement, the layer is the disposition — invariant I3), and an allow carrying a refusal
* record is that refusal (invariant I4: the record says what ended the wait, whatever the decision claims);
* - a resolution's record counts only when it answers THE ask the gate put to the resolver — its origin word,
* its class set, the bytes the answer licenses (the record's `inputDigest`); a resolution
* without such a record settled nothing the engine can attribute: a deny is recorded as the resolver's
* out-of-contract refusal (`approver_contract`), an allow on a real-approval ask is refused, a plain allow
* is a posture allow; a resolution still carrying the retired `settlement` member is refused loudly;
* - the LATEST resolution wins (a re-check refusal replaces the first approval; a re-approval renews it);
* - a SURVIVING ask supersedes any settlement read off an earlier decision (a tighten minted a new
* question; whoever resolves it settles it afresh).
*/
export interface SettlementLedger {
/** The ask this pass consumed, or undefined when no ask was settled (a direct deny, a plain allow). */
readonly settled: SettledAsk | undefined;
/** An engine-composed settlement the gate itself minted (the unshown content ask's refusal). */
set(settled: SettledAsk): void;
/** Record an ask's resolution; returns the resolution the gate goes on with (the resolver's, or the engine's refusal). */
fromResolved(resolved: ResolvedAsk, asked: PermissionResult, req: ToolCallRequest): ResolvedAsk;
/** Read an adjudication's record; returns the decision the gate goes on with and, for a deny, the refusing layer word
* (`ask_resolution` for a refusal record, `policy` otherwise — a veto when a yes rides the settlement). */
fromDecision(decision: PermissionResult): {
decision: PermissionResult;
deniedBy?: DeniedBy;
};
/** The allow exit: the bytes that RUN are decided — a settlement licensed for other bytes leaves the record (the call runs
* unattributed). */
keepForBytes(runs: unknown): void;
/** A deny's recorded classifier CAUSE for this call (an inherited station's `unavailable` / `parse_error` outcome). */
causeOf(decision: PermissionResult): ClassifierDenyCause | undefined;
/** A decision's recorded CLASSIFICATION for this call — which model an ancestor's frozen classifier asked and which
* answered, on either arm. */
classifierRoundOf(decision: PermissionResult): AutoModeClassifierRound | undefined;
/** A surviving ask supersedes whatever an earlier decision settled. */
supersede(): void;
}
export declare function createSettlementLedger(call: {
toolCallId: string;
toolName: string;
}): SettlementLedger;
/**
* Observe-only payload isolation for {@link import("../hooks.js").Hooks.permissionDenied}:
* clone the tool args before they ride the observer payload, so a hook mutating `payload.input` can never
* pollute the LIVE args object (later events / audit records share it). Same posture as the postToolUseFailure
* details clone (the gate-stations phase): `structuredClone` first; a non-structured-cloneable graph
* (functions/handles) falls back to a SHALLOW plain object/array copy (top-level mutation isolated);
* a non-object primitive passes through as-is (immutable anyway).
*/
export declare function cloneObserverInput(input: unknown): unknown;
/**
* The input a gate exit decided on, as the call's record keeps it ({@link ToolGateResult.finalInput}): a deep,
* FROZEN copy — never the live object the tool receives (a tool or a later layer writing into its own arguments
* must not rewrite what the gate decided), and never writable by a reader; the deny observer gets a writable
* {@link cloneObserverInput} of it. A graph `structuredClone` refuses (functions, handles) is kept as a shallow
* copy frozen at its own top level only — its members are the live arguments' own, and freezing those would
* freeze the call's input under the tool.
*/
export declare function frozenInputSnapshot(input: unknown): unknown;
/** What one `classic.PreToolUse` screening came to: an answer the receipt took (`undefined` = no opinion), or the
* engine's fail-closed refusal of the call with its sentence (a handler that failed or broke the site's contract,
* or a cancel). */
/** A screen's outcome; an answer names the registration whose answer the chain returned (`answeredBy`, absent when no
* handler spoke). */
export type PreToolUseScreen = {
readonly kind: "answered";
readonly answer: PreToolUseResult | undefined;
readonly answeredBy?: string;
} | {
readonly kind: "refused";
readonly reason: string;
};
/**
* The PreToolUse screening face as the gate's stations see it — `classic.PreToolUse` raised on the leg's bus, the
* per-screening e, the answer receipt and the fail-closed sentences behind one method, so the approved-edit re-screen
* in the lanes runs the SAME screening the phase-1 pass ran. Present on the pass exactly when a handler hangs on the
* event (an observe handler runs inside the screening too; it never decides).
*/
export interface PreToolUseScreening {
/** Raise `classic.PreToolUse` over `args`; `subject` names what is screened in a fail-closed sentence. What the gate
* does after the screen answers runs in the screen's own continuation. */
screen(args: unknown, subject: string): Promise<PreToolUseScreen>;
/** The model-facing text for the screen's OWN deny of this call (unwrapped; every exit wraps it with the leg's mark). */
deniedReason(answer: PreToolUseResult): string;
}
/** The FACE a pass's questions are put on ({@link askFaceOf} of the call's wire name) — the one reading of it the
* write-protection tighten (the fold) and every class derivation (the lanes) make. */
export declare function passAskFace(pass: Pick<GatePass, "req">): AskFace;
/**
* ONE gate pass — the record the station groups (`runGateFold`, `runGateLanes`) and the exits read and write,
* built once per call by `runToolGate` before the screening pass. What used to be the closure-captured `let`
* cells of a 1 500-line function are its MUTABLE cells (each written by the station named on it, read by the
* ones after); what the stations borrow from the hook machinery are its SEATS (fixed for the pass). Ownership
* classes follow the phase-Input convention: `borrowed-readonly` = fixed for the pass, the stations only read
* it; `borrowed-mutable` = a cell a station writes.
*/
export interface GatePass {
/** borrowed-readonly — the gate's input: every deployment seat the stations read by name (`egress`,
* `orgRules`, `persistedRules`, `autoMode`, …), and the identity / reminder mark the exits stamp. */
readonly input: ToolGateInput;
/** borrowed-readonly — the gate's event, read ONCE at entry (the dispatch's entry destructure): the exit compares the
* final args against THIS event's input, never against a re-read of the input record. */
readonly event: ToolGateInput["event"];
/** borrowed-readonly — the deployment's policy face, captured ONCE at entry before the screening pass. The fold
* adjudicates through it and the edit chain re-adjudicates through the SAME binding: a stateful accessor on the
* input cannot hand the fold one policy and the approved-edit re-check another (or none). */
readonly adjudicate: ToolGateInput["adjudicate"];
/** borrowed-readonly — the ask resolver, captured ONCE at entry (same rule: the first-round resolution and the edit
* round's re-ask resolve through one binding). */
readonly resolveAsk: ToolGateInput["resolveAsk"];
/** borrowed-readonly — the durable park closure, captured ONCE at entry (the three park sites hand it the same
* thirteen seats; presence is judged off this one read). */
readonly suspendAsk: ToolGateInput["suspendAsk"];
/** borrowed-readonly — `input.event.toolName`, read once. */
readonly toolName: string;
/** borrowed-readonly — `input.event.toolCallId`, read once. */
readonly toolCallId: string;
/** borrowed-readonly — ONE read of the per-call signal for the whole gate call (the one-read
* discipline the settlement screen already states): every conditional seat invocation (the three wait
* seats plus the three suspendAsk park sites) judges presence and passes the value from this same cell, so
* a stateful accessor cannot answer the arity check with a signal and the call with something else. */
readonly callSignal: AbortSignal | undefined;
/** borrowed-readonly — the one host-callback isolation scope of this gate call (the deny observer,
* the hook-error trace sink, the org and rule lanes' disclosure sinks). */
readonly notifier: SafeNotifier;
/** borrowed-readonly — the settlement LEDGER of this pass (the binding is fixed; the ledger's own methods
* are how the resolution sites write it — one rule per read, see {@link createSettlementLedger}). */
readonly ledger: SettlementLedger;
/** borrowed-readonly — the ONE emission seat of the deny observer for this pass, bounded and contained like
* every other seat (the hook machinery owns the seat runner; the exits call this). */
readonly notifyPermissionDenied: (payload: PermissionDeniedPayload) => Promise<void>;
/** borrowed-readonly — the PreToolUse screening face ({@link PreToolUseScreening}), or undefined when no handler hangs
* on `classic.PreToolUse` (the edit chain then re-screens nothing). */
readonly screening: PreToolUseScreening | undefined;
/** borrowed-readonly — the station group that runs AFTER the fold (the lanes: org layer, rule lane, classifier,
* parks, resolution, edit chain, and the exit they end in), handed the pass by the fold's LAST station on the
* fold's own completing tick. A seat rather than an import because the two station modules sit on one layer
* and may not name each other; a seat rather than a second `await` in the dispatch because that await is a
* suspension point of its own: the org face and the rule store read HOST state the host may refresh
* concurrently, and the tick on which they are consulted is the contract — when the station bodies were one
* function, the org face was invoked in the same continuation that ran the last tighten, and a refresh queued
* behind the policy's answer could not land in between. Returns the gate result (a park's suspension or the
* exit's record). Writer: the dispatch (`runToolGate`, which wires `runGateLanes`). Reader: the fold's tail. */
readonly afterFold: (pass: GatePass) => Promise<ToolGateResult>;
/** borrowed-mutable — the FINAL args, always the pass's own immutable value (`ownArgs`): the model's input as taken at
* the entry, rewritten by the phase-1 hook, by the policy rewrite on the ask path and by the exits' rewrite fold.
* Writers: the dispatch (phase 1), the lanes, the exits. */
currentInput: unknown;
/** borrowed-mutable — the adjudication request. Minted over the pass's args so the record is whole from
* the first station, and RE-MINTED by the adjudication station over the FINAL post-screen args (the
* pre-existing `const req` site); `args` is re-pointed at the policy rewrite on the ask path. Writers: the
* fold (mint), the lanes (`args`). */
req: ToolCallRequest;
/** borrowed-mutable — the PreToolUse `additionalContext` collected for this call (pushed into; the binding
* is fixed). Writers: the dispatch (phase 1), the lanes (sandbox admission note, the edit re-screen). */
readonly preToolContext: string[];
/** borrowed-mutable — the phase-1 hook's ASK, remembered for the fold. Only ever an ask (assigned under the
* `r.action === "ask"` arm); typed as the narrowed member so the fold's reconstruction reads the ask-only
* fields without a runtime re-test that could, if it ever failed, silently skip the promotion (i.e. loosen).
* Writer: the dispatch (phase 1). Readers: the fold, the sandbox-admission conjunct. */
hookAsk: Extract<PermissionResult, {
action: "ask";
}> | undefined;
/** borrowed-mutable — the phase-1 screen answered its own `allow` (the exact word), naming the registration that answered
* it: the allow layer's hook member (its `entries`), which speaks only while no policy rewrote the arguments after the
* screen saw them. Writer: the dispatch (phase 1). Reader: the lanes' allow layer (its one reader). */
hookAllowedBy?: readonly string[];
/** borrowed-mutable — `tool.check` was raised in this pass's first adjudication, which raises it once: set by the
* raise in front of the ask branch, read by the raise behind the read-only arm (it then raises nothing). Writer and
* reader: the lanes. */
toolCheckRaised?: true;
/** borrowed-mutable — set by a park attempt that FAILED (see `ParkAttemptFailed`). Read at the single deny
* exit — whatever the fallback chain then decides to say, it says it knowing the park was tried first.
* Writers: the three park sites. */
parkFailed: string | undefined;
/** borrowed-mutable — WHO refused, written ONCE by the refusing site (a direct policy / org / classifier
* deny, a hook re-screen refusal, the write-protection re-judge, the edit chain's cap, an ask resolution
* that ended as a refusal). The exit mints the outcome from it beside the ledger's `settled`; a refusal
* that reaches the exit without it is an engine defect and the mint throws. */
deniedBy: DeniedBy | undefined;
/** borrowed-mutable — a refusing station's STRUCTURED details (the shell read station's refusal card), written
* beside `deniedBy` by that station alone; the exit puts it on the gate result for the loop's error result.
* OPTIONAL (the `probeUnanswered` form): the dispatch that builds the pass has nothing to declare here. */
denyDetails?: unknown;
/** borrowed-mutable — the shell read station's containment card behind a standing MANDATED ask (an out-of-root
* or unestablishable read): when that ask ends as a refusal (a person's no, nobody to ask), the exit puts the
* card on the gate result, so the refusal's `tool_end` still says WHAT was found (`readonly_out_of_root`) beside
* WHO refused (`ask_resolution`). Writer: the fold's read station. */
shellReadDetails?: unknown;
/** The read station PLACED every write the command makes inside the write roots (`ShellReadVerdict.writesPlaced`) — the
* one precondition the mode's file-command acceptance reads (the reference's path checks precede its acceptEdits arm). */
shellWritesPlaced?: true;
/** The read station did NOT place the command's writes because a `cp` / `mv` segment carries an option word
* (`ShellReadVerdict.unplacedOptions`) — the one non-placement reason the mode's card and the headless refusal name. */
shellUnplacedOptions?: {
readonly program: "cp" | "mv";
};
/** borrowed-mutable — the FORM of the classifier round behind a deny (`unavailable` / `parse_error`), written
* beside `deniedBy` by the gate's own classifier step (its attestation, read once) or by the fold / the
* re-check when an inherited station's attested deny arrives through the chain; undefined for a ruling
* block and every non-classifier deny. The exit mints `GateDisposition.cause` from it. */
denyCause: ClassifierDenyCause | undefined;
/** borrowed-mutable — the classifier CLASSIFICATION this pass ran ({@link AutoModeClassifierRound}).
* Written ONCE by the gate's classifier step from the verdict the decider handed back, on EVERY arm (allow
* included — a fallback model's allow is the fact the record exists for); absent when no classifier was
* consulted or the decider could name no model. Both exits mint `GateDisposition.classifier` from it.
* OPTIONAL rather than `| undefined` (the `probeUnanswered` form): the dispatch that builds the pass is
* not a station of this lane and has nothing to declare here. */
classifierRound?: AutoModeClassifierRound;
/** borrowed-mutable — the standing verdict. The no-policy default until the adjudication station writes the
* policy's answer; then rewritten by every station that changes the verdict (fold, tightens, org, rule
* lane, stamp, classifier, sandbox, content lane, resolution, edit chain). */
decision: PermissionResult;
/** borrowed-mutable — which of the gate's OWN post-fold stations (the tightens and the unstated station) minted the standing ask, if one did — an
* input of the ask-origin derivation (`AskOriginFacts.tightened`). Written at most once per pass: every
* tighten fires only over a surviving `allow`, and the first one to fire leaves an `ask` the others do not
* touch. Recorded by the gate rather than read off `decisionReason` because that member is a policy's to
* compose (a policy stamping `"safety"` on its own ask must read as `policy`). Writer: the fold. */
tightenedBy: AskOriginFacts["tightened"];
/** borrowed-mutable — the reversibility probe was consulted on this pass and DID NOT ANSWER (it threw or
* timed out). The out-of-root mandate has no other source than the probe's answer, so an unanswered probe
* leaves the call's boundary UNKNOWN: the allow layer (a stored allow rule, the read-only reader) must
* not clear the surviving ask — a person confirms it. Writer: the fold. Reader: the lanes' mandate. */
probeUnanswered?: true;
/** borrowed-mutable — the probe ANSWERED and reported that the read boundary DECLINED to judge where this
* command reads ({@link import("../tool-spec.js").ReversibilityVerdict.boundaryDeclined}). The member above's
* twin on the answered side, and narrower: the ask stays ordinary for every ANSWERER, and only the allow
* layer's read-only-command arm stands down. Writer: the fold. Reader: that arm, and nothing else. */
probeBoundaryDeclined?: true;
/** borrowed-mutable — the shell read station could not establish what this command reads (finding `unestablishable`
* or `declined`): a mode's release of the call is a release past the leg's read-deny table, announced as such.
* Writer: the fold's read station. Reader: the release point. */
shellReadUnestablished?: true;
/** borrowed-mutable — the shell read station minted the standing ask's directory-grant offer
* (`probeReadRootCandidate`). Read by the classify seat's shape probe, which withdraws the offer when its
* own objection stands beside the station's — the grant answers the station's finding, not the shape's.
* Writer: the fold's read station. */
shellReadOffer?: true;
/** borrowed-mutable — the write-protection judge covered this call and its table MATCHED: the allow
* layer must not clear the surviving ask, a live decider still may. Writers: the fold's fourth tighten,
* on BOTH its arms (a write tool's target), and the fold's read station (a shell command's written targets,
* `ShellReadVerdict.writeProtected`). Reader: the lanes' mandate. */
writeProtected?: true;
/** borrowed-mutable — the READING of a written target could not say where it leads on disk (a target a shell command's
* program writes, placed, whose destination the read station could not resolve): no automatic lane may answer the ask.
* A fact of the reading, not of a judge. Writer: the fold's read station (`ShellReadVerdict.targetUnresolved`). Reader:
* the lanes' mandate. */
targetUnresolved?: true;
/** borrowed-mutable — the rewrite remembered BEFORE a hook-ask promotion / tighten, re-applied on the
* approved path (a redaction is not cancelled because a person confirmed). Writer: the fold. Readers: the
* fold's probe and write-protection judges, the lanes, the allow exit. */
policyRewrite: unknown;
/** borrowed-mutable — the standing ask's CLASS SET and folded row (`classifyAsk`), written at the lanes' origin
* stamp and at every re-mint (the denial-limit fallback, the approved-edit round); read by the classifier
* station, the sandbox leg and the arbitration point. Writer: the lanes. */
askClassification?: AskClassification;
/** borrowed-mutable — the mode arbitration's verdict when it RELEASED or REFUSED the standing ask (the exit
* projects it onto the disposition). Writer: the lanes' arbitration point. */
arbitration?: Exclude<Arbitration, {
kind: "ask";
}>;
}
/**
* The engine's fail-closed block of phase 1 — the PreToolUse face THREW or never ANSWERED, so the engine
* decides, on its own rule, that the call does not run. BOUNDARY (deliberate asymmetry with
* {@link hookDenyExit} — do NOT unify the two): a hook `deny` is the HOOK's decision and the observer
* exclusion covers it; a crash / an unanswered seat produces no decision at all, so the deny OBSERVER fires
* here — an attribution channel whose job is "why was this call denied" must see an engine deny, or the
* deployment's only evidence of a broken hook is a tool that mysteriously stops working.
*/
export declare function engineFailClosedExit(pass: GatePass, reason: string): Promise<ToolGateResult>;
/** The refusal sentence for args the gate could not take into its own copy (`ownArgs` refused them), naming what. */
export declare const unownedArgsMessage: (subject: string, reason: string) => string;
/**
* The model's args could not be taken into the pass's own copy (a function, a shared buffer, a typed array): nobody judged
* them, so nothing may run them. The single deny exit (the observer fires), in the policy layer's word — the value is the
* deployment's own (a host driving the gate; the runner's calls are always clonable).
*/
export declare function unownedArgsExit(pass: GatePass, reason: string): Promise<ToolGateResult>;
/** The hook's OWN deny in phase 1. A PreToolUse hook deny does NOT fire `permissionDenied`
* (the exclusion recorded on `PermissionDeniedPayload`) — this return stays observer-free. */
export declare function hookDenyExit(pass: GatePass, deniedReason: string): ToolGateResult;
/**
* The exit of the adjudicate chain — the pass has a standing `deny` or `allow` (every ask was resolved or
* parked before this), and this is where it becomes the gate result and the ONE record of the pass.
*/
export declare function exitGate(pass: GatePass): Promise<ToolGateResult>;