@sema-agent/core
Version:
Stateless, task-oriented AI agent core
276 lines (275 loc) • 22.4 kB
TypeScript
/**
* The permission MODE's two derivations, beside the arbitration that reads it (`ask-origin.ts`):
*
* - {@link modePolicyOf} — what a mode INSTALLS on the leg: the plan pair and the mode's own write gate (the
* reference implementation's per-mode defaults: `plan` keeps every tool mounted and ASKS at the call for a write
* and for a tool it did not define that is not read-only, `acceptEdits` writes inside the root without asking,
* `default` / `auto` / `dontAsk` ask on every covered write — auto's classifier and dontAsk's refusal then act on
* that ask — and `bypassPermissions` installs no gate). One writer of the mode (the caller, the steer verb, the
* model's `EnterPlanMode`), one interpreter (this engine): a library embedder's `plan` is not a word a serving
* layer has to translate first.
* - {@link PermissionModeController} — the one RESOLUTION of a leg's effective mode, read live by every mode
* consumer (the gate's arbitration, the mode gate policy, the manifest, the children's ceiling): the
* requested word, clamped under the parent's live ceiling, folded back to `default` where the deployment
* denies it or the arming face is absent. A steer rewrites the requested word; nothing caches the answer.
*/
import type { AgentTool, ExecutionEnv } from "../internal/harness-types.js";
import type { ContainmentFace } from "../tools/fs/containment-face.js";
import type { PermissionResult, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
import { type ToolRoster } from "./tool-roster.js";
import { type PermissionMode } from "./ask-origin.js";
import type { RuntimeCaps } from "./runtime-caps.js";
import type { RunnerDeps } from "./runner-deps.js";
import type { TaskSpec } from "./task-spec.js";
/** WHY an effective mode is not the requested one: the deployment's deny bit (`RuntimeCaps.bypassPermissions` /
* `RuntimeCaps.autoMode`), a requested `auto` with no classifier face wired, or a child's declaration folded under
* its parent's ceiling. */
export declare const PERMISSION_MODE_FOLD_REASONS: readonly ["operator_deny", "face_absent", "inherited_ceiling"];
export type PermissionModeFoldReason = (typeof PERMISSION_MODE_FOLD_REASONS)[number];
/** The mode's seats on a leg. `writeGate`: `none` — no mode write gate; `ask` — every covered write asks;
* `accept_root` — writes inside the leg's write roots (the task root and every additional working directory) are allowed,
* writes outside them ask; `plan` — every write {@link planRefusesWrite} names is the mode's OWN question (the plan
* sentence, `decisionReason: "mode"`: a person may say yes, no rule and no automatic lane answers it). `fileCommands`: the
* shell commands whose ordinary question the mode answers itself ({@link modeAcceptsFileCommand}) — the reference
* implementation's `acceptEdits` set ("auto-approve file edits and common file commands"); empty for every other mode.
* `writeFace`: the write face the mode states where no seat declared one (the mode row of {@link import("../tools/fs/containment-face.js").resolveContainmentFace}) —
* `bypassPermissions` writes outside the write roots (`open`); every other mode states nothing and the default fence stands.
* `unstated`: the mode's answer for a call to a tool that states no permission of its own (permission seat `passthrough`)
* when no layer of the fold stated the allow — `ask` (the arbitration then answers it as any ask: dontAsk refuses it, auto's
* classifier may answer it), or `none` (the call runs; `bypassPermissions`, which installs no gate and asks nothing it would
* release). */
export interface ModePolicy {
readonly enablePlanMode: boolean;
readonly writeGate: "none" | "ask" | "accept_root" | "plan";
readonly fileCommands: readonly string[];
readonly writeFace: ContainmentFace | undefined;
readonly unstated: "ask" | "none";
}
/** The `acceptEdits` file commands, verbatim from the reference implementation (2.1.280): a closed set. */
export declare const ACCEPT_EDITS_FILE_COMMANDS: readonly ["mkdir", "touch", "rm", "rmdir", "mv", "cp", "sed"];
/** The mode → seats table, total over {@link PermissionMode}. */
export declare function modePolicyOf(mode: PermissionMode): ModePolicy;
/**
* Is this call a shell command the mode's {@link ModePolicy.fileCommands} cover — every segment a SIMPLE command
* whose command word is, as written, one of them? The reference implementation's reading (it compares the first
* word of each command against its list): no redirection, no pipe or background, no substitution, no leading
* assignment, no wrapper in front, a literal unquoted command word — `/bin/rm` and `"rm"` are not `rm`. The answer
* is only ever a license for the mode to answer the call's ORDINARY question: the gate's arbitration reads it beside
* the class set and the read station's `writesPlaced` fact (every write target established inside the write roots, no
* removal of a root, every program in the text), so a deny rule, an ask rule, the delete guard, a read-boundary finding
* or a write the station could not place still stands. A `run_in_background` call is not accepted (as a `&` is not).
*/
export declare function modeAcceptsFileCommand(mode: PermissionMode, toolName: string, args: unknown): boolean;
/** What the mode gate policy resolves paths against. `acceptDirs` = the leg's WRITE ROOTS (the task root plus every additional
* working directory), the set `acceptEdits` allows writes inside — absent ⇒ the root alone; `exemptDirs` = the task's scratchpad,
* never asked about; `roster` = the leg's live tool roster (who defined the called tool and its effect) and `readOnlyDeclared` =
* whether the mounted tool of that name declares itself read-only, both read per call by {@link planRefusesWrite}. */
export interface ModeGateSeats {
env: ExecutionEnv;
rootPath: string;
acceptDirs?: readonly string[];
exemptDirs?: readonly string[];
roster?: {
readonly current: ToolRoster | undefined;
};
readOnlyDeclared?: (toolName: string) => boolean;
}
/** Does the LAST mounted instance of this name (the one a call dispatches to) declare itself read-only — an MCP server's
* `readOnlyHint` ({@link AgentTool.mcpReadOnlyHint})? The mode gate's `readOnlyDeclared` seat over the leg's live tools. */
export declare function mountedToolDeclaresReadOnly(tools: readonly AgentTool[], toolName: string): boolean;
/** The ONE rule of the unstated question, read at the fold's last station and at the approved-edit round: an allow nobody
* stated (no `decisionReason`) on a tool that states no permission of its own, under a mode whose `unstated` answer is
* `ask`. Who may answer it is the allow layer's business (a hook's own `allow` is one of its members), not this rule's. */
export declare function asksUnstated(d: PermissionResult, seat: "passthrough" | undefined, mode: PermissionMode | undefined): boolean;
/** The unstated question's sentence (the reference implementation's passthrough sentence, without its product name). */
export declare const unstatedAskMessage: (toolName: string) => string;
/** One call's permission seat: `passthrough` (an allow nobody stated meets the mode's unstated answer) and the question
* put when it does — absent ⇒ {@link unstatedAskMessage}. */
export interface PermissionSeatReading {
readonly seat: "passthrough";
readonly question?: string;
}
/** The gate's permission-seat inputs (a member group of `ToolGateInput`), read through {@link permissionSeatAt}. */
export interface PermissionSeatInputs {
/** The called tool's permission seat ({@link import("./tool-spec.js").ToolSpec.permissionSeat}): `"passthrough"` ⇒ an allow no layer stated
* meets the mode's answer for an unstated call ({@link ModePolicy.unstated}). Absent ⇒ the tool answers for itself. */
permissionSeat?: "passthrough";
/** The seat read off THIS call's arguments, for a tool whose seat depends on them (the Runner wires it for the `Skill`
* tool: a skill that declares a list asks `Execute skill: <name>`). Present ⇒ it is the reading. */
permissionSeatOf?: (args: unknown) => PermissionSeatReading | undefined;
}
/**
* The ONE reading of a call's permission seat, taken on the arguments the gate is judging (the policy's rewrite and the
* hook's included): the per-call reader where the tool's seat depends on its arguments, else the mounted tool's static
* seat. Read at the fold's last station and at the approved-edit round, on that round's edited bytes.
*/
export declare function permissionSeatAt(input: PermissionSeatInputs, args: unknown): PermissionSeatReading | undefined;
/** The PERMISSION SEAT of the LAST mounted instance of this name (the one a call dispatches to) — `passthrough` when the
* instance states no permission of its own ({@link AgentTool.permissionSeat}), else `undefined` (the engine's stations and
* the deployment's declarations answer for it). Read per call off the leg's live mount array, so a refresh is seen. */
export declare function mountedToolPermissionSeat(tools: readonly AgentTool[], toolName: string): "passthrough" | undefined;
/** Does the LAST mounted instance of this name (the one a call dispatches to) carry its server's declaration that a call must be put to a
* person ({@link AgentTool.mcpRequiresUserInteraction})? Read per call off the leg's live mount array, so a refresh is seen. The gate's
* seat for the `requires_user_interaction` class. */
export declare function mountedToolRequiresUserInteraction(tools: readonly AgentTool[], toolName: string): boolean;
/**
* Is this call a WRITE the `plan` mode asks about? Two faces, one question — does the call write by its declaration:
* · a covered path write (the catalog's protective floor: a file-write tool, or a same-name tool wearing its slot), and
* · a call to a tool the engine does not define (a caller's, an MCP server's, an A2A peer's) that is not read-only — the
* reference implementation's plan arm for a tool it did not write (`!isReadOnly` ⇒ the mode). Read-only is the tool's
* effect on the leg's roster being `read`, or — only where that effect is the fail-closed default (nobody declared one)
* — the mounted tool's own read-only declaration (`readOnlyDeclared`: an MCP server's `readOnlyHint`, which the
* reference reads for this question). A declared effect (the tool's own, or the deployment's `toolAxes`) always
* outranks the server's claim; the effect is the repeat-safety axis a server's self-claim never lowers.
* The engine's own tools answer through their own seats: a child a delegation spawns inherits `plan` as its ceiling, and
* the shell station and the tool marks raise their own questions, which reach a person under `plan` as under `default`.
* A name the roster does not carry dispatches nowhere and is not judged here.
*/
export declare function planRefusesWrite(req: ToolCallRequest, roster: ToolRoster | undefined, readOnlyDeclared?: (toolName: string) => boolean): boolean;
/**
* The mode's WRITE GATE as one live policy layer: every call reads the mode ref and dispatches to the gate the
* table names for it, so a steer changes the next call's gate without re-composing the chain. The three gates are
* built once (their root / exemption screens run at construction). The plan gate is the ask gate with the plan
* sentence in place of the ordinary write question: the same structural exemptions (the scratchpad, the advertised
* writable roots — the reference exempts its scratchpad and agent-memory files under plan too) admit a write, everything
* else the mode names asks.
*/
export declare function createModeGatePolicy(modeRef: {
readonly current: PermissionMode;
}, seats: ModeGateSeats): ToolPolicy;
/**
* The mode a resumed leg REQUESTS: the re-passed spec's word, else the word the checkpoint row recorded — except where the
* resume approves or edits a `plan_review` over a row recorded in `plan`: the plan exits to the word the leg had before it
* (`prePlanMode`, the reference implementation's ExitPlanMode: `prePlanMode ?? "default"`; absent ⇒ the leg's own default),
* unless the resume names another mode — the approver's choice. A re-passed `plan` does not keep an approved plan open.
*/
export declare function modeRequestAfterResume(seat: PermissionMode | undefined, recorded: {
readonly permissionMode?: PermissionMode;
readonly prePlanMode?: PermissionMode;
}, outcome: {
readonly gate: string;
readonly decision?: string;
} | undefined): PermissionMode | undefined;
/**
* The permission MODE's door — three refusals in the pre-first-await stretch, each by name and never a fold back to
* absence (a mode a caller believes it declared must not silently run as another): a RETIRED mode key on the spec
* (`config.spec_retired_key` — `autoModeRequested` / `approverPosture` are one key now); a `permissionMode` outside
* the closed set (`config.permission_mode_invalid`); a `shellGate` value other than the floor word `"always"`
* (`config.shell_gate_value_retired` — `"off"` / `"classify"` were a doctrine the mode arbitration replaced). The
* value it screened is the value it RETURNS — the mode seat reads this snapshot, never the caller's spec again. The ONE
* function both entrances call: prepare (a run's spec, in its pre-first-await stretch) and the resume ladder's admission rung
* (the re-supplied config, before the claim) — a resume prepare would refuse is refused before it consumes the row.
*/
export declare function refusePermissionModeDoors(spec: Pick<TaskSpec, "permissionMode" | "shellGate">): PermissionMode | undefined;
/** The resume's ONE refusal channel for a door prepare would refuse at: runs `screen` (prepare's own function, which throws prepare's sentence and code) and
* leaves a refusal of one of {@link CHECKPOINT_CONFIG_REFUSAL_CODES} as `checkpoint.config_refused` — the sentence, prepare's code as `detail.code`, the input
* that carries it as `detail.field`, and prepare's own arm as `detail.reason` when its error names one. Anything else leaves as it was thrown. What the
* door returns is returned (a {@link ConfigDoorEntry}: the resume's way of entering a door the config-door phase lists). */
export declare function asResumeConfigRefusal<T>(field: "taskConfig" | "deps" | "inheritedGate", screen: () => T): T;
/** The three-member answer every mode question gets — the manifest member, the steer verb's return value and the
* fold notice's detail share it. `requested` is absent when the caller declared nothing (a root inherits `default`,
* a child inherits its parent). */
export interface PermissionModeReceipt {
readonly requested?: PermissionMode;
readonly effective: PermissionMode;
readonly foldedBy?: PermissionModeFoldReason;
}
/** The arming facts the controller folds a declared mode against, bound once the deployment's caps resolved. */
export interface PermissionModeArming {
readonly caps: Pick<RuntimeCaps, "bypassPermissions" | "autoMode"> | undefined;
readonly autoFacePresent: boolean;
/** Arm the classifier for a leg whose effective mode became `auto` after prepare (a steer, a parent's live ceiling);
* idempotent. A failed arming leaves no classifier: a steer is rolled back and rejects with the failure; an arming a
* read started (no caller awaits it) reports it through {@link onArmFault}. */
readonly armAuto?: () => Promise<void> | void;
/** Where an arming a read of {@link PermissionModeController.current} started reports its failure. Observe-only: a throw
* or a rejection from it is contained. */
readonly onArmFault?: (error: unknown) => void;
}
/**
* The auto-mode classifier FACE a leg arms from — ONE reading for the arming fact ({@link PermissionModeArming.autoFacePresent}),
* the arming itself and the face this leg hands its children: the leg's own deployment face (`RunnerDeps.autoMode`), else
* the face its ancestor armed from, riding the trusted chain (`InheritedGate.autoModeFace`). A delegated child whose own
* runner wires no face is classified from its ancestor's deployment face — each of a subagent's actions goes through the
* same decision order as the parent's — on the child's own transcript and model; the child's own face wins where it has one.
*/
export declare function autoModeFaceOf(deps: Pick<RunnerDeps, "autoMode">, inherited: InheritedAutoModeFace | undefined): RunnerDeps["autoMode"];
/** The chain's carriage of that face (`InheritedGate` extends it). */
export interface InheritedAutoModeFace {
/** The auto-mode face the spawning leg armed from — opaque half, never serialized; present only when that leg's deps (or its own
* chain) carried one. A child whose runner wires no face of its own arms its classifier from it ({@link autoModeFaceOf}). */
autoModeFace?: RunnerDeps["autoMode"];
}
/** A live ceiling: the parent leg's own controller (`current` tracks the parent's steers), or a recorded word. A ceiling
* that can start something on a read (a controller, the engine's leg ceiling) also answers `observed` — the same word,
* read without starting anything; one without it is read through `current` on an observation too. */
export type PermissionModeCeiling = {
readonly current: PermissionMode;
readonly observed?: PermissionMode;
} | PermissionMode;
/**
* The one resolution of a leg's effective mode. `current` is derived on every read from three inputs —
* the requested word (the spec's, or the newest steer's whose arming has not failed), the ceiling (a parent's live controller, or the
* word a checkpoint recorded) and the arming facts — so a parent's steer reaches a running child on the
* child's next read with no forwarding, and nothing holds a stale answer.
*/
export declare class PermissionModeController {
private readonly requestedBySpec;
private readonly ceiling;
/** The steers that still count, oldest first: each is pending (its arming running) or took effect. A steer whose
* arming fails leaves the list; one that takes effect drops the older ones (a newer steer that took effect outranks
* them whatever they do next). */
private readonly steers;
private arming;
private readonly onFold;
private prePlan;
/** `onFold` hears every fold with the word the fold FOLDED — what a fold notice names: the leg's own request for the
* ceiling fold (the word the parent's mode outranks); for the deployment's folds the declared word (the request after
* the ceiling, or — a leg that requested nothing — the word it inherited). */
constructor(requestedBySpec: PermissionMode | undefined, ceiling: PermissionModeCeiling | undefined, opts?: {
onFold?: (receipt: PermissionModeReceipt, folded: PermissionMode) => void;
prePlanMode?: PermissionMode;
});
/** The declared word the leg had when a steer (or the model's `EnterPlanMode`) last moved it into `plan`, while it is
* there; a leg that started in plan has none, and a steer out of plan clears it. A checkpoint row records it. */
get prePlanMode(): PermissionMode | undefined;
/** The word the caller asked for right now: the newest steer that still counts, else the spec's. */
get requested(): PermissionMode | undefined;
private ceilingNow;
/** The declared mode after the ceiling clamp — what a checkpoint records and what the leg's children inherit. */
get declared(): PermissionMode;
private declaration;
private armedForAuto;
/** Contains the fault observer: its synchronous throw and its rejection both stop here. */
private readonly faultSeat;
/** The effective mode: declared, then folded against the arming facts. A read that finds the leg in `auto` with its
* classifier not yet armed (a parent's steer reached this child through the live ceiling) starts the arming — idempotent,
* not awaited: the call that observed the transition may still meet the person, the next one meets the classifier. A
* failure goes to {@link PermissionModeArming.onArmFault}; the leg then has no classifier, so its asks go on to the
* approval seat as under `default`. */
get current(): PermissionMode;
/** The effective mode read to REPORT it (a hook context), not to decide with it: the word {@link current} answers,
* reached through the ceilings' own `observed`, so neither this leg nor an ancestor starts its classifier's arming.
* A decision read is what starts it; an observation that did would arm a leg earlier than its own gate, and so move
* who answers the leg's next ask. */
get observed(): PermissionMode;
/** The three-member answer as it stands right now. */
receipt(): PermissionModeReceipt;
private receiptOf;
/** The receipt and, when it records a fold, the word that fold folded — one declaration read for both. */
private answerOf;
/** Hand a fold to the observer with the word it folded (an answer without a fold says nothing). */
private announce;
/** The construction-time announcement: a fold the leg STARTS with (a ceiling fold, before any arming is bound). */
announceStart(): PermissionModeReceipt;
/** The ceiling's source moved to another leg (the parent session's next leg); `before` is this leg's receipt under the
* previous one. A ceiling fold the move produced — or changed — is announced, as the construction-time fold is. */
ceilingRebound(before: PermissionModeReceipt): void;
/** Bind the deployment's arming facts (once, after the caps resolved); announces a fold-back if one results. */
bindArming(arming: PermissionModeArming): PermissionModeReceipt;
/** A steer: the requested word changes; the arming fold is re-judged; a leg that became `auto` arms its classifier.
* When that arming fails the steer rejects with the failure, stops counting (the requested word is again the newest
* steer's that still counts, else the spec's — a failed arming never undoes another steer, and never leaves the leg
* in `auto` on its own word), and the arming may start again on the leg's next move into `auto`. */
steer(mode: PermissionMode): Promise<PermissionModeReceipt>;
}