UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,404 lines • 106 kB
import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallFace, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
import { type CallTargetStamp } from "./effective-path-target.js";
import type { GateOutcome } from "./gate-outcome.js";
import { type PreToolUseScreening } from "./runner/gate-exit.js";
export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./rule-lane-reads.js";
import { type AskClass } from "./ask-class.js";
import { type PermissionMode } from "./ask-origin.js";
import { type AutoModeDenialTracker } from "./auto-mode.js";
export { formatHookFeedback } from "./reminder-mint.js";
import type { WiringLegKind } from "./wiring-manifest.js";
import type { PermissionSeatInputs } from "./mode-policy.js";
import type { SkillGateSeats } from "./skill-grant.js";
import type { AllowEntryGateSeats } from "./allow-entry.js";
/**
 * The classic stations' faces and the two-phase tool gate. A deployment extends the engine through the mods bus
 * (`RunnerDeps.mods` / `TaskSpec.mods`, `src/core/mods/`): each sema station raises one `classic.*` event, and the
 * types here are those events' `e` shapes and answer forms. All feedback the model should see (a block reason,
 * injected context) is wrapped as a `<system-reminder>` via {@link formatHookFeedback} so the model can read and
 * self-correct.
 *
 * **FAILURE POSTURE, and a correction to a widely-quoted anchor (recorded 2026-08-23).** "CC fails open on hook
 * failures, exit code 2 excepted" is TRUE OF ONE LANE: CC's `command` hooks (shell scripts) are non-blocking on a
 * spawn failure, a non-zero exit or a timeout. CC's in-process CALLBACK lane — the one structurally equivalent to a
 * handler here — is FAIL-CLOSED on PreToolUse (a callback that throws or times out does not execute the call) and
 * its UserPromptSubmit timeout BLOCKS the prompt. So the two decision events here (`classic.PreToolUse`,
 * `classic.UserPromptSubmit`) refuse on a handler's failure (the event table's failure column), and the observation
 * events skip the failed handler, because an observer has nothing to fail closed about.
 */
/** The members every classic event's `e` carries for the leg it fires on. */
export interface ClassicEventBase {
    /** The run/leg identity envelope — see {@link HookInvocationIdentity} for the presence law. */
    readonly identity?: HookInvocationIdentity;
    /** The delegated leg's id (its task id — the key its `task_progress` frames and its parent's fleet row use).
     *  Absent on a top-level leg. */
    readonly agentId?: string;
    /** The leg's EFFECTIVE permission mode when the event is raised — the mode seat's `observed` read (it arms no
     *  classifier). Absent where there is no leg seat: a host-driven {@link runToolGate}, the delegation-fold twin, the
     *  `"resume_message"` screen (before the resumed leg exists), and the compaction / StopFailure events (the
     *  reference's hook input omits it there). Not a version signal. */
    readonly permissionMode?: PermissionMode;
}
/**
 * `classic.PreToolUse`'s `e` — raised before a tool executes (the gate's phase 1, the approved-edit re-screen, the
 * resumed leg's re-screen of an approved call). Pinned: `tool`, `tool_use_id`, `agentId`, `input` (what a handler
 * judges is what runs — a rewrite is the answer's `updatedInput`). Answer: {@link PreToolUseResult}; a handler with no
 * opinion returns `next(e)`. A handler's `allow` does NOT circumvent the policy: the policy still runs on the (possibly
 * rewritten) args and has the final say ({@link runToolGate}).
 */
export interface PreToolUseEvent extends ClassicEventBase {
    /** The call's wire name (the tool the call dispatches to). */
    readonly tool: string;
    readonly tool_use_id: string;
    /** A deep-frozen copy of the args the gate judges. */
    readonly input: unknown;
    /** The TASK's tracked working directory at this call — the base the hand tools resolve a RELATIVE path against,
     *  read live off the same ref the tool policy reads. NOT {@link HookEnvCapabilities.cwd} (the environment's own,
     *  which never moves on a shell `cd` or a worktree entry). ABSENT when the run has no tracked cwd to report (no real
     *  write hands, a host-driven gate without one, the delegation-fold twin) — absence means "this engine cannot tell
     *  you", never "the root". */
    readonly cwd?: string;
    /** The read-only path-resolution face over the env the hands run against ({@link HookEnvCapabilities}). ABSENT when
     *  the deployment wired no execution environment at all. */
    readonly env?: HookEnvCapabilities;
}
/** `classic.PostToolUse`'s `e` — after a tool executed successfully. Answer: {@link PostToolUseResult}. */
export interface PostToolUseEvent extends ClassicEventBase {
    readonly tool: string;
    readonly tool_use_id: string;
    readonly input: unknown;
    readonly output: HookToolOutput;
    readonly cwd?: string;
    readonly env?: HookEnvCapabilities;
}
/**
 * `classic.PostToolUseFailure`'s `e` — after a tool EXECUTION failed (mutually exclusive with `classic.PostToolUse`).
 * Raised ONLY for genuine execution failures: a gate/policy block, a plan-mode deny, a pre-execution abort or a durable
 * suspend raises neither post event. Answer: {@link PostToolUseFailureResult} (context only; the failure itself cannot
 * be rewritten).
 */
export interface PostToolUseFailureEvent extends ClassicEventBase {
    readonly tool: string;
    readonly tool_use_id: string;
    readonly input: unknown;
    readonly failure: HookToolFailure;
    readonly cwd?: string;
    readonly env?: HookEnvCapabilities;
}
/**
 * `classic.PostToolBatch`'s `e` — raised ONCE after every tool call of one assistant turn resolved, before the next
 * model request (the per-call post events fire first). A zero-tool turn, an aborted or steered-away boundary raise
 * nothing. On a boundary where the engine itself injected (`injectedThisTurn`) the batch is DELIVERED with that
 * attribution and the answer's `additionalContext` is IGNORED (no stacking onto an engine injection). Answer:
 * {@link PostToolBatchResult}.
 */
export interface PostToolBatchEvent extends ClassicEventBase {
    readonly batch: readonly PostToolBatchCall[];
    readonly injectedThisTurn?: "final_verification" | "finalize";
}
/**
 * `classic.UserPromptSubmit`'s `e` — EVERY user-lane entrance: the objective, a live steer, a wake resume's message
 * (pre-claim, at the verb) and a parked steer's redelivery, discriminated by {@link source}. Pinned: `source`,
 * `inputId`, `actor`. A handler that blocks unconditionally therefore refuses steers too — branch on `source` (and
 * `actor`). Answer: {@link UserPromptSubmitResult}. Block per entrance: objective ⇒ the task ends `blocked`; live steer
 * ⇒ typed `steering.blocked_by_hook`; resume message ⇒ typed pre-claim refusal (the checkpoint stays pending); parked
 * redelivery ⇒ the frame is withheld (a `steering.parked_input_blocked` notice) and the resume proceeds.
 *
 * ⚠️ RE-ENTRANT within one leg: the steer verb goes live before the objective screen runs, so screens can overlap in
 * flight; only steer-vs-steer is serialized (FIFO — each queued steer waits behind its predecessor's whole screen).
 */
export interface UserPromptSubmitEvent extends ClassicEventBase {
    /** The caller's text, raw. */
    readonly prompt: string;
    /**
     * WHICH entrance: `"objective"` (the task's opening prompt), `"steer"` (a live mid-run steer), `"resume_message"`
     * (a wake resume's message, screened pre-claim), `"parked_redelivery"` (a parked steer re-screened on the CURRENT
     * process when any resume kind that drains parked steers redelivers it). ⚠️ DOUBLE ARRIVAL IS CONTRACTUAL: one input
     * can arrive twice (screened live as `"steer"`, parked, re-screened as `"parked_redelivery"`) — idempotency is the
     * handler's, keyed on {@link inputId}. `"followUp"` is a RESERVED spelling: no entrance carries it yet.
     */
    readonly source: "objective" | "steer" | "resume_message" | "followUp" | "parked_redelivery";
    /**
     * The screened input's correlation id — present on every entrance but the objective (no id exists there; nothing is
     * fabricated): the steer's effective id, the parked entry's stored id, or the wake message's id (the caller's, else
     * minted PER CALL — a retried wake with no caller id re-screens under a fresh id; a host that retries wakes mints its
     * own). The dedup key for the double-arrival contract.
     */
    readonly inputId?: string;
    /** The API-boundary actor snapshot attributed to this input (the one the `human_input` event reports), when the caller
     *  supplied one. Attribution only, never authority. */
    readonly actor?: ActorAssertion;
    /**
     * The files declared on this objective (`TaskSpec.userAttachments`), in order — present only on `source:"objective"`
     * when the objective declares any. A path entry shows its path; a host entry its name and byte count, never its
     * bytes. Blocking the objective reads and delivers none of them. A handler does not govern the engine's reading of an
     * allowed attachment (`classic.PreToolUse` is not raised for it either): a deployment that must keep a path from the
     * model writes a deny rule, which the attachment read honors like the Read tool does.
     */
    readonly userAttachments?: readonly import("./user-attachment.js").UserAttachmentDeclaration[];
}
/**
 * `classic.Stop`'s `e` — raised when the agent WOULD OTHERWISE END its run (no more tool calls, steering and follow-up
 * queues dry). Answer: {@link StopHookResult}; `{ block }` pushes back (the reason is injected and the run continues one
 * more turn). Guardrails (CC-exact): consecutive push-backs are capped (default 8 — the loop then overrides and ends with
 * a warning); `stopHookActive` is true while the run is already continuing because of one. `maxTurns` outranks it.
 */
export interface StopEvent extends ClassicEventBase {
    /** True when this run is already continuing because a previous answer pushed back — check it and let the run end
     *  once your condition can't be improved, or it loops to the cap. */
    readonly stopHookActive: boolean;
    /** Consecutive push-backs so far in this run (resets when the run is let proceed). */
    readonly consecutiveBlocks: number;
    /** A LAZY view of the conversation being judged (a handler that only reads the counters pays nothing). Absent when
     *  the runner cannot supply one. */
    readonly getBranch?: () => Promise<readonly SessionTreeEntry[]>;
    /** The run's background tasks still in flight (pending / running / parked under this run's owner triple; CC
     *  `SubagentStop.background_tasks`). LAZY like {@link getBranch}; `[]` = none, ABSENT = no view. */
    readonly backgroundTasks?: () => ReadonlyArray<{
        id: string;
        description?: string;
        status: string;
    }>;
}
/**
 * `classic.StopFailure`'s `e` — raised ONCE, after the result is assembled, when the task terminates `failed` BECAUSE
 * the model/API layer errored. NOT for engine-owned failure classes (budget, timeout/abort, max-turns, degenerate /
 * walltime cutoffs, storage conflict, blocked, a durable suspend). Observe-only.
 */
export interface StopFailureEvent extends ClassicEventBase {
    /** Human-readable error message (the assembled `errorMessage`; any `[code]` prefix already stripped). */
    readonly error: string;
    /** Machine-readable kind — the assembled `errorCode` (auth / network / rate_limit / server_error / …); absent when
     *  the provider stamped no code. */
    readonly errorKind?: string;
    /** Turns completed when the failure ended the run. */
    readonly turns: number;
}
/** `classic.PreCompact`'s `e` — before each compaction, after the trigger gate and a valid cut point. Answer:
 *  {@link PreCompactResult} (`block` honored on "auto" / "manual" only). */
export interface PreCompactEvent extends ClassicEventBase {
    readonly trigger: PreCompactContext["trigger"];
    readonly customInstructions?: string;
}
/** `classic.PostCompact`'s `e` — observe-only, after the compaction landed. */
export interface PostCompactEvent extends ClassicEventBase {
    readonly trigger: PostCompactContext["trigger"];
    readonly summary: string;
    readonly tokensBefore?: number;
    readonly tokensAfter?: number;
    readonly freedTokens?: number;
}
/** `classic.PermissionDenied`'s `e` (CC-exact fields + the gate outcome) — raised when the gate's adjudicate chain
 *  DENY-SHORT-CIRCUITS a call (a policy deny, a tighten whose ask resolved to deny, the plan-mode write-deny, the
 *  compliance deny) and on the engine's fail-closed refusal of a failed PreToolUse screening (`source:"hook"`); NOT on a
 *  PreToolUse handler's own deny (that is the handler's decision, and this event attributes the ENGINE's) nor on a
 *  durable suspend. Observe-only. `identity` is present on every ENGINE emission; absent only when a host drives
 *  {@link runToolGate} without `ToolGateInput.identity`. (CC raises its PermissionDenied from the auto-mode classifier's
 *  refusals alone; sema's trigger surface is deliberately WIDER — a divergence, not a parity claim.) */
export interface PermissionDeniedPayload extends ClassicEventBase {
    readonly toolName: string;
    /** The FINAL (post-rewrite) args the chain adjudicated — what would have executed; not necessarily the model's
     *  original args. A detached copy (the live args are shared with later frames and audit records). */
    readonly input: unknown;
    readonly toolCallId: string;
    /** Human/model-readable deny reason (CC `reason`) — the RAW decision text, NOT `<system-reminder>`-wrapped. */
    readonly reason: string;
    /**
     * The gate outcome this deny is — the SAME object the call's `tool_end` frame carries as `gate` and the engine's
     * per-call sideband records (one mint, three faces). `disposition.deniedBy` says which LAYER refused; when the
     * refusal came through an ask resolution, `settlement` says how the wait ended and `origin` who asked.
     */
    readonly gate: GateOutcome;
}
/** The failed tool execution a PostToolUseFailure callback inspects (design/134). */
export interface HookToolFailure {
    /** Model-facing error text of the failed call (the error tool result's text content, joined). */
    error: string;
    /** Wide any-abort semantics (⊇ CC `is_interrupt`, which is user-interrupt specifically): true when
     *  the task abort signal fired — timeout, budget, cancel or suspend — not only a human interrupt. */
    isInterrupt: boolean;
    /** Tool execution time in ms when the engine has it (CC 0.3.202 `duration_ms` parity); else absent. */
    durationMs?: number;
    /** Element-level clone of the error result's content blocks (mutating them cannot rewrite the
     *  transcript — the failure is observe-only). */
    content: Array<TextContent | ImageContent | DocumentContent>;
    /** BEST-EFFORT deep clone (structuredClone) of the tool's raw details. When the details graph is
     *  not structured-cloneable (functions/handles), this is the LIVE reference — treat it as
     *  READ-ONLY: a mutation would corrupt the result the loop is about to commit. */
    details: unknown;
}
/** A PostToolUseFailure result: append model-readable context to the error result (CC-exact, no rewrite). */
export interface PostToolUseFailureResult {
    additionalContext?: string;
}
/** One resolved call in a postToolBatch payload (CC PostToolBatchToolCall shape, thin projection —
 *  `response` is a bounded text digest of the result, never the full details object). */
export interface PostToolBatchCall {
    /** The name the model spelled on the tool_use block (an alias stays an alias) — the reference's PostToolBatch payload
     *  names each block the same way, while the per-call post hooks receive the resolved tool's wire name. Correlate
     *  the two faces by `toolCallId`. */
    toolName: string;
    input: unknown;
    toolCallId: string;
    response?: string;
    isError: boolean;
}
/** A PostToolBatch result: inject context once for the whole batch at the turn boundary. */
export interface PostToolBatchResult {
    additionalContext?: string;
}
/** What the compaction module hands its `preCompact` option (design/134); the Runner raises `classic.PreCompact` from it. */
export interface PreCompactContext {
    /** "auto" = threshold-triggered; "manual" = /compact; "forced" = promptTooLong recovery or
     *  trim-pressure propagation (block is ignored on forced — the compaction is not optional). */
    trigger: "auto" | "manual" | "forced";
    /** The summarization instructions in effect (spec/deployment-level), when set. */
    customInstructions?: string;
}
/** A PreCompact result: skip this compaction (auto/manual only) and/or extend the summary instructions. */
export interface PreCompactResult {
    /** When set (and trigger is not "forced"), this compaction is skipped; the reason is traced. */
    block?: string;
    /** Appended to the summarization instructions for this compaction (never replaces them).
     *  LLM-summary path only: a compaction served from a `summaryProvider` reuse (Seam C) never runs
     *  the summarization call, so these instructions are not consumed there (recorded design gap —
     *  extending the provider contract is out of scope for design/134). */
    additionalInstructions?: string;
}
/** What the compaction module hands its `postCompact` option (design/134, observe-only); the Runner raises `classic.PostCompact` from it. */
export interface PostCompactContext {
    trigger: "auto" | "manual" | "forced";
    /** The conversation summary the compaction produced (CC `compact_summary` parity). */
    summary: string;
    /** DIAGNOSTIC, NOT subtractable: prepareCompaction's coordinate, LACKING the overhead term
     *  {@link tokensAfter} carries. Use {@link freedTokens}. */
    tokensBefore?: number;
    /** DIAGNOSTIC, NOT subtractable: always structural + prompt overhead, a different coordinate from
     *  {@link tokensBefore}, so the difference can go negative. Absent when the post measurement failed. */
    tokensAfter?: number;
    /** The engine's measured delta (clamped ≥ 0) — the ONE subtractable number here, and the same value
     *  this pass's `compacted` frame carries. Absent ⇒ the post measurement failed. */
    freedTokens?: number;
}
/** A Stop hook result: block the run from ending, with a model-readable reason. */
export interface StopHookResult {
    /** When set, the run does NOT end: this reason is injected (as a `<system-reminder>`) and the
     *  model gets another turn to address it. */
    block?: string;
    /**
     * Extra model-readable context injected at the stop point — DECOUPLED from `block`. When present it
     * is injected (as a `<system-reminder>`, neutral framing: "Stop hook additional context: …") whether
     * or not the hook blocked, and an `additionalContext` WITHOUT `block` still continues the run one
     * more turn.
     *
     * **It DOES count toward the consecutive-block cap, and it never resets it.** This sentence used to
     * say the opposite ("the cap is driven by `block` alone", cited to CC 2.1.201), and it was wrong on
     * both halves. Re-read against CC: the `additionalContext` branch pushes into the SAME array as the
     * blocking-error branch, the loop counts that array's length against the cap, and CC never resets
     * mid-run. The engine was fixed to match; the sentence here was not, so the module that DEFINES this
     * seat and the module that IMPLEMENTS it stated opposite behaviors for years. What the cap bounds is
     * "the run did not end when it otherwise would have", and a context-only push-back is exactly that —
     * a hook whose most common failure mode is "no parseable verdict, here is some text" would otherwise
     * run to `maxTurns` looking busy rather than stuck.
     */
    additionalContext?: string;
}
/**
 * A READ-ONLY path-resolution capability face over the task's {@link ExecutionEnv}, handed to tool hooks
 * on {@link PreToolUseEvent.env} (and the two post events' `env`).
 *
 * **Why it exists.** A hook that judges a path — "is this write inside the workspace", "does this
 * innocent-looking name resolve onto a guarded file" — must resolve it in the filesystem the HANDS
 * actually write to. For a sandboxed/remote deployment that filesystem is NOT the host process's `fs`:
 * the env is minted by `RunnerDeps.executionEnvFactory` DURING prepare, i.e. after the spec (and any
 * policy/hook the caller built with it) already exists, so a hook closed over a spec-time env resolves
 * against the wrong disk and answers `exists`/symlink questions about a different machine. Reading the
 * env off the CALL CONTEXT removes the ordering problem structurally: the context is built after the env
 * is, and a resumed/revived run's rebuilt env is picked up with no re-instantiation protocol.
 *
 * **Capabilities, not the object.** A classic handler is an extension boundary the deployment fills with code whose
 * job is to DECIDE; the whole `ExecutionEnv` is a face that can ACT (`exec`, `writeFile`, `remove`,
 * `cleanup`). So this is four INDEPENDENTLY BOUND read primitives and nothing else: no `env`
 * back-reference, no prototype of the env object, and the face itself is frozen (a hook cannot re-point a
 * member for a later hook in the same run). Widening it later is additive and cheap; taking a wide object
 * back is not.
 *
 * The line this draws, stated so nobody reads more into it: it separates what a hook is HANDED from what
 * the environment can do. It is NOT an isolation boundary against code that can already reach the env's own
 * class — anything that can redefine `SomeExecutionEnv.prototype.exec` intercepts every call the ENGINE
 * makes into that env, receiver included, and needs nothing from this face to do it. Deployments that must
 * withstand that run their hooks somewhere else, not behind a narrower argument.
 *
 * **Names are the env's own** ({@link ExecutionEnv} / `FileSystem`), deliberately: a hook author reads one
 * contract, not a translation of it. The seam sketch's `canonicalize`/`root` are `canonicalPath`/`cwd`
 * here — `canonicalPath` IS the realpath-semantics primitive, and `ExecutionEnv` declares no "root" at all
 * (the engine's task root is a RUNNER notion — `deps.rootPath`/worktree/`requestedCwd` — not something the
 * environment reports about itself), so the honest anchor is the env's own working directory. `cwd` is a
 * FUNCTION, not the snapshot string, so a hook reading it late sees where the env is NOW. Read {@link
 * HookEnvCapabilities.cwd} before using it as a base for RELATIVE paths — the task's tracked cwd is a
 * different value, and this face carries the env's, on purpose.
 *
 * **Every member is optional, and absence is the signal.** A member is present iff the underlying env
 * exposes that primitive (`readLink` is optional in `FileSystem`; a transport env may implement less than
 * it declares). A missing member is never a stub that throws or an emulation that guesses — the hook is
 * told, in the only way that cannot be misread, that this environment cannot answer, and takes its own
 * conservative branch. The face carries a NULL PROTOTYPE so that reading is a question about this object
 * alone (see {@link createHookEnvCapabilities}). Errors that the env CAN report stay in the `Result`
 * (these methods never throw, inheriting the `FileSystem` contract verbatim).
 */
export interface HookEnvCapabilities {
    /** {@link ExecutionEnv.canonicalPath} — the canonical path of an EXISTING path, symlinks resolved
     *  (realpath semantics), in the env's own namespace. */
    canonicalPath?(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
    /** {@link ExecutionEnv.exists} — `false` for a missing path; other failures (permission, transport)
     *  come back as a `FileError`, which is NOT an answer of "absent". */
    exists?(path: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
    /** {@link ExecutionEnv.readLink} — a symlink's raw target, ONE hop, not followed. Absent whenever the
     *  env omits the primitive: a DANGLING symlink's real target is then unknowable to the hook. */
    readLink?(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
    /**
     * {@link ExecutionEnv.cwd} — the ENVIRONMENT's own working directory, read live: the base the env's own
     * primitives resolve a relative path against, and the only anchor it reports about itself.
     *
     * NOT the task's tracked cwd, and the difference is load-bearing for anyone judging a RELATIVE path.
     * The engine keeps a separate per-task cwd — it starts at the resolved task root (which is
     * `deps.rootPath` when that is set, not necessarily the env's cwd) and moves with the shell's `cd` and
     * with worktree entry — and the TOOLS' relative paths resolve against THAT, while the environment is
     * never told it moved. So a hook that must decide a relative target the way the tool will interpret it
     * cannot take the base from here: judge absolute targets, or have the deployment hand the tracked cwd
     * to the hook through its own channel. What this member is good for is the env-side anchor — which
     * filesystem/mount the answers below are about.
     */
    cwd?(): string;
}
/**
 * Build the {@link HookEnvCapabilities} face for `env`. One wrapper per primitive (never a bound method of
 * the env, never the env itself), presence probed at build time so the face's shape reports what this env
 * can actually do, and frozen so it cannot be re-pointed by whoever receives it.
 *
 * Two details the "absent means cannot" contract rests on, both cheap and both load-bearing:
 *  · **null prototype, both ways.** Absence must be a fact about THIS object — on the way OUT (a face
 *    behind `Object.prototype` would answer `ctx.env.readLink` from there, turning "this env cannot read
 *    links" into "here is some function"; freezing the face does not freeze what it inherits) and on the
 *    way IN ({@link findEnvSlot} stops before `Object.prototype`, so an ambient property of that name is
 *    never adopted as a capability and never gets called with the env as its receiver).
 *  · **capture, do not re-read.** Each primitive is read ONCE, checked, and that captured callable is what
 *    the wrapper invokes — through module-load-captured intrinsics, never a build-time or call-time global
 *    lookup. Re-reading the property, or the machinery that does the reading, would let something other
 *    than what was validated end up running, or the receiver end up somewhere it was never handed.
 *    {@link createCwdReader} carries the one live read under the same rules.
 */
export declare function createHookEnvCapabilities(env: ExecutionEnv): HookEnvCapabilities;
/**
 * The UNIFIED IDENTITY ENVELOPE every classic event can carry (`e.identity`): which run, which leg, and
 * where in the delegation tree the event is raised. The CC analog is `createBaseHookInput`'s per-event base fields;
 * the members here are sema's OWN identity facts (session/leg/delegation axes), not a transcription of CC's names.
 *
 * WHY: the deployment seat (`RunnerDeps.mods`) is PROCESS-level — one handler serves the root run, every delegated
 * child and every resume leg at once, and without this envelope none of those events could say which leg it belongs
 * to (`e.agentId` names a delegated leg on every event too).
 *
 * PRESENCE LAW:
 *  · PRESENT on every ENGINE-raised classic event: the gate's `classic.PreToolUse` screenings (phase 1, the
 *    approval-edit re-screen, the resumed leg's re-screen), the two post events, `classic.PostToolBatch`, every
 *    `classic.UserPromptSubmit` entrance (the resume-message screen carries the pre-leg envelope minted off the
 *    checkpoint and the trusted spawn facts), `classic.Stop`, `classic.StopFailure`, the compaction pair, and every
 *    engine `classic.PermissionDenied`. The engine mints ONE frozen envelope per prepared leg
 *    ({@link mintHookInvocationIdentity} — single mint home) and every station reuses it.
 *  · HONESTLY ABSENT (no fabrication) where the engine structurally cannot know:
 *     1. a HOST driving the exported {@link runToolGate} directly without supplying `ToolGateInput.identity`;
 *     2. an ancestor's chain riding a DESCENDANT's constraint fold ({@link createPreToolUseConstraintPolicy}) — the
 *        fold's request carries no leg identity, and stamping the INSTALL-site leg would name the wrong run;
 *     3. a deployment driving the compaction module directly with its own `preCompact`/`postCompact` options.
 *
 * CONSUMER NOTE — the minted object is FROZEN and carries a NULL PROTOTYPE (same rule and same
 * trade as {@link HookEnvCapabilities}: absence must be a fact about this object, not about what
 * `Object.prototype` happens to hold). Read members directly (`identity.agentName`), test presence
 * with `in` or `!== undefined`; do NOT call inherited methods on it — `identity.hasOwnProperty(...)`
 * or implicit string coercion would throw, exactly as on the env capability face.
 */
export interface HookInvocationIdentity {
    /** The run's session id (uuid domain) — the same key `TaskResult.sessionId` reports. */
    readonly sessionId: string;
    /** The run's unified task id (`spec.taskId ?? sessionId` — the value TaskEvent's `sourceTaskId`
     *  and `task_progress` frames key on). Equal to {@link sessionId} when no task id was declared. */
    readonly taskId: string;
    /** Which prepared leg this invocation belongs to — the wiring manifest's own `leg.kind`
     *  derivation, verbatim ({@link WiringLegKind}: `"root" | "child" | "resume"`). NOTE the fork
     *  lane's legs read `"child"` here like any other delegation; {@link insideFork} is the
     *  fork-lane discriminator. */
    readonly legKind: WiringLegKind;
    /** RB-204 delegation fact, verbatim: `true` iff this leg runs as a delegated child (every core
     *  spawn lane — sync/steer/background/fork/workflow — sets it; a top-level run reads `false`). */
    readonly isDelegatedChild: boolean;
    /** Present (`true`) iff this leg IS a forked child (`Agent(subagent_type:"fork")` — the
     *  design/110 trusted internals fact). Absent everywhere else. */
    readonly insideFork?: true;
    /** The delegated child's display name, when one was threaded at spawn (taskName / agent-type).
     *  UNTRUSTED display text (model-chosen) — already single-line-bounded at mint (same
     *  `inlineUntrusted` treatment as the `task_progress` name lane); never an identity key. */
    readonly agentName?: string;
    /** The EXPLICIT `Agent({name})` spawn identity, when the spawn carried one — sema's teammate key
     *  (validated at spawn: `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`, never `main`). The same string the
     *  child's roster row, its uplink `from` label and its `task_progress.spawnName` carry, so a hook
     *  seat binds "which teammate" by string equality. ABSENT = an unnamed delegation (not a
     *  teammate); {@link agentName} above stays the DISPLAY label and is never an identity key. */
    readonly spawnName?: string;
    /** The RESOLVED subagent type the leg was spawned as (`"general-purpose"` / `"fork"` / a roster
     *  definition's name — the fleet row's `agentType` word, never the description). Absent on a
     *  top-level run and on a leg whose spawner threaded no type (a workflow-lane agent). */
    readonly agentType?: string;
    /** The spawning Agent tool call's own id, when this leg runs under one — the delegation-chain
     *  position anchor ({@link import("./types.js").TaskEvent}'s `parentToolCallId` twin). Absent on
     *  a top-level run and on a directly-started workflow child (no launching tool call — no id is
     *  fabricated). */
    readonly parentToolCallId?: string;
}
/**
 * #281 件A — the ONE construction home of {@link HookInvocationIdentity} (domain-lexicon: one mint
 * site; the runner calls it once per prepared leg and every hook station reuses the same object).
 * Frozen AND null-prototype: the envelope is handed to arbitrarily many deployment callbacks, and a
 * hook mutating `ctx.identity` must not rewrite what a later hook (or the delegation-lifecycle
 * observer) reads. The null prototype is the {@link createHookEnvCapabilities} rule applied to the
 * other absence-signaling face this module mints — "absent member" must be a fact about THIS object,
 * and a frozen object with an ordinary prototype still answers `insideFork`/`agentName` reads (and
 * `in` probes) from `Object.prototype`, so a prototype write elsewhere in the process could hand
 * every leg a delegation axis the mint never stamped. `agentName` is sanitized here — it
 * is the only member whose value a model influences.
 */
export declare function mintHookInvocationIdentity(facts: HookInvocationIdentity): HookInvocationIdentity;
/** The executed tool result a PostToolUse hook inspects. */
export interface HookToolOutput {
    content: Array<TextContent | ImageContent | DocumentContent>;
    details: unknown;
    isError: boolean;
}
/**
 * A PreToolUse hook result: a {@link PermissionResult} (so a hook can `deny`/`ask`, or `allow` with an
 * `updatedInput` rewrite) plus optional `additionalContext` injected into the eventual tool result.
 *
 * **The provenance members are not hook-settable inputs** (#174). `decisionReason` is the ENGINE's account
 * of which layer produced a verdict, and `allowRuleShadowed` is the allow layer's own disclosure that a
 * member matched without clearing the ask. The gate stamps both itself — a `decisionReason` on a hook
 * result is DISCARDED (a hook ask always reads `"hook"` downstream), and an `allowRuleShadowed` is
 * dropped rather than rendered on an approval card as an entry no member matched.
 *
 * Which of a hook's OTHER members reach the surviving decision depends on which fold arm ran, so read
 * the arm, not a single list: an ask promoted from a policy `allow` is rebuilt from the hook's `message`,
 * `requiresRealApproval` and `updatedInput`, while a hook ask folded BESIDE a policy ask contributes its
 * message and its mandate to the policy's decision and leaves the policy's own rewrite in place (the
 * hook's rewrite still reaches execution — through the gate's captured rewrite, not through the decision
 * object). Both arms are at the `hookAsk` fold in {@link runToolGate}.
 *
 * `additionalContext` is on neither list and never was consumed as a decision member: it rides its own
 * channel — collected into the gate's `preToolContext` as soon as the hook answers, whichever verdict it
 * returned, and drained onto the tool result by the runner. DELIVERY is narrower than collection: the
 * runner stashes the context only for a call that will EXECUTE, because a blocked or suspended call never
 * reaches the result step that drains it and carries its own model-facing reason instead. The context
 * formerly APPEARED on the promoted ask object as an undeclared property, because that fold spread the
 * whole hook result; the concurrent arm never carried it, so the two arms disagreed on a member neither
 * of them reads. Nothing in the engine reads it off a decision.
 *
 * PreToolUse lane × settlement table (the gate's phase 1 in {@link runToolGate}; the delegated twin in
 * {@link createPreToolUseConstraintPolicy} speaks the same words as a policy layer). "Held" = the ask survives to a person:
 * neither an allow rule (`decisionReason: "hook"` is outside the allow layer) nor the classifier (hook asks are
 * never classifier-answerable) clears it.
 *  · returns `allow` (± `updatedInput` / `additionalContext`) → the face's own word (`hookAllowedBy`, naming the registration); the policy
 *    still adjudicates the FINAL args; the allow layer's hook member may clear an unmandated ask. Not held.
 *  · returns `ask` (± `updatedInput`, ± `requiresRealApproval`) → folded as `hookAsk`: a policy deny still
 *    outranks it, a policy allow is PROMOTED to this ask with `"hook"` stamped by the engine; the rewrite rides
 *    into execution and the real-approval bit crosses delegation. Held.
 *  · returns `deny` → {@link hookDenyExit}: the hook's OWN refusal — the deny observer does NOT fire, the model
 *    reads the hook's sentence. Nothing executes.
 *  · returns `undefined` ("no opinion") or no screening face is installed → no word at all; the policy decides.
 *  · a handler throws / runs past its budget / breaks the site's contract (an out-of-contract answer, refused by
 *    {@link screenPreToolUseResult}), or the dispatch is aborted → {@link engineFailClosedExit}: an ENGINE deny — the
 *    deny observer fires, a crash and a timeout also reach `onError(phase:"hook")` (an abort does not), the model reads
 *    the engine's envelope. Nothing executes, nothing is held.
 */
export type PreToolUseResult = PermissionResult & {
    additionalContext?: string;
};
/** A PostToolUse hook result: replace the tool output and/or append context (both optional). */
export interface PostToolUseResult {
    /** If provided, replaces the tool result content array in full. */
    updatedOutput?: Array<TextContent | ImageContent | DocumentContent>;
    /** Appended to the result as a `<system-reminder>` the model can read. */
    additionalContext?: string;
}
/** A UserPromptSubmit hook result: block the submission, or inject context ahead of the prompt. */
export interface UserPromptSubmitResult {
    /** Block submission entirely; the task fails with this model-readable reason. */
    block?: string;
    /** Injected ahead of the user's prompt (wrapped as a `<system-reminder>`). */
    additionalContext?: string;
}
/** The outcome of the two-phase tool gate, mapped onto the harness `tool_call` hook return shape. */
export interface ToolGateResult {
    /** Block execution (the loop emits an error tool result with `reason`). */
    block?: boolean;
    /** Model-readable block reason (already `<system-reminder>`-wrapped). */
    reason?: string;
    /**
     * The record of this pass ({@link GateOutcome}): its final disposition, the ask it settled (if any) and
     * that ask's origin. Present on BOTH terminal exits, the block and the allow — an approved call is a
     * settled approval too, and a caller that saw the record only on refusals would read its absence on an
     * execution as "nobody approved this". Absent only on the durable-suspend exit, where the pass decided
     * nothing yet. The caller records it on its own per-call sideband so it reaches that call's `tool_end`
     * frame, and the deny observer receives the same object: one mint, three faces.
     */
    gate?: GateOutcome;
    /** The args to execute with — the pass's own immutable copy, the value every judge of the call read (the model's args
     *  as taken at the gate's entry, or what a hook, a policy or a person rewrote them to); re-validated by the loop. Present
     *  on every allow exit, whether or not anything rewrote (compare values to tell); absent on a block and a suspend. */
    updatedInput?: unknown;
    /** The input this pass decided on (what a refusal refused, what an allowed call runs with) as a deep, FROZEN copy taken
     *  at the exit — never the live args. Beside {@link gate} on both terminal exits; absent on the suspend exit. The deny
     *  observer gets a detached, writable copy of it. */
    finalInput?: unknown;
    /** A blocking refusal's structured details, when the refusing station minted any (the shell read station's
     *  `read_path_denied` / `readonly_out_of_root` cards): the loop attaches them to the error result it mints
     *  from `reason`, so the refusal reaches `tool_end.structured` / `errorCode` exactly as the tool's own
     *  refusal of the same finding did. Absent on every other block. */
    details?: unknown;
    /**
     * design/45: the gate routed a policy `ask` to a **durable suspension** (no synchronous approver).
     * `suspendAsk` already persisted the checkpoint and aborted the run; the caller records this so the
     * task assembles as `status:"suspended"` with the token. Suspend lives ONLY here in the gate's `ask`
     * branch — it never becomes a `PermissionResult` action (policy stays pure allow/ask/deny, §15.2).
     */
    suspend?: {
        token: import("./checkpoint-store.js").CheckpointToken;
        gate: import("./checkpoint-store.js").CheckpointGate;
    };
    /** PreToolUse `additionalContext` to append to this call's eventual tool result (correlated by id). */
    preToolContext: string[];
}
/**
 * The gate's content-ask routing outcome — deliberately a channel of its OWN, not a
 * field on the permission verdict. A permission verdict is folded, re-adjudicated and re-presented by
 * layers that know nothing about answers; an answer riding one of those would be dropped by the fold or
 * carried into a second adjudication. Three arms, one per thing that can happen:
 *  · `answered` — a person answered. The gate lets the call execute and the answer is delivered to the
 *    tool through the run's own one-shot binding (the gate never carries the answer itself).
 *  · `unavailable` — nobody was reachable and NOTHING was shown (the {@link
 *    import("./ask-question.js").QuestionUnavailable} contract). Only this arm may be re-routed — the
 *    gate parks the question for a later human, or answers the honest "it was NOT shown" refusal.
 *  · `delivery_failure` — delivery was attempted and failed. NEVER re-routed: the question may already
 *    have been rendered to a person, so parking it would put one question in front of two deciders. It
 *    is reported to the model through the tool's own coded-failure / degrade exits.
 */
export type ContentAskOutcome = {
    kind: "answered";
} | {
    kind: "unavailable";
    /** True iff the park closure took its SYNC-FIRST DECLINE for this call — it has not yet made a
     *  real suspend attempt, so re-entering it now IS that attempt rather than a second one. False
     *  when the park already had its attempt (no live face to prefer, or a durable mandate): the gate
     *  must not re-enter, because a suspend and a checkpoint write are not idempotent, and a backend
     *  that committed before surfacing a transport error would be left an orphan row under a second
     *  token. Exactly one real attempt per call, on every path. */
    parkDeclined: boolean;
} | {
    kind: "delivery_failure";
    code: string;
};
/**
 * A durable park that was ATTEMPTED for this call and could not be minted, carrying the reason in the
 * words of whatever refused it. Distinct from the `undefined` return, which means "no park attempt
 * happened" (no facility armed, or the closure took its sync-first decline) — that case hides nothing,
 * while this one does: the gate falls back to the synchronous approver chain either way (the
 * compensation contract — an uncommitted checkpoint is a safe fallback — is unchanged), but on a
 * headless deployment that fallback is the auto-deny whose text is "no approver is wired". True as far
 * as it goes, and the whole reason the call did not park lives somewhere else entirely (the host's
 * `onError` face). So the reason travels with the fall-through and the deny quotes it.
 */
export interface ParkAttemptFailed {
    /** Why the park could not be minted — a sentence, already worded for a reader. */
    parkFailed: string;
}
/**
 * design/182 §7 — what the ORG layer answers for ONE call (see `ToolGateInput.orgRules`).
 *
 * The two arms are distinguished by `status`, NEVER by the absence of a verdict: "the org published no
 * rule for this call" and "this deployment cannot see the org's rules" are opposite facts, and reading
 * an empty answer as the first one is exactly the governance fail-open the availability contract exists
 * to prevent.
 */
export type OrgGateVerdict = {
    status: "available";
    /** The org rule that speaks for this call, if any. Deny outranks ask; there is no allow bucket. An `unreadable` member says why
     *  `rule` could not be judged against the call (the message names it): the call could not be READ against it (the lexer's reason;
     *  `behavior` is `ask`), or the row is one the org layer does not read, standing at its strictest reading (`deny`, or `ask`). */
    verdict?: {
        behavior: "deny" | "ask";
        rule: string;
        unreadable?: string;
    };
    /** design/252 G-2 (additive): the REVISION of the snapshot this answer was read off — the
     *  published-policy version an auditor reconciles the resulting decision against. Present on the
     *  available arm whether or not a rule spoke: "revision 41 published nothing about this call" is
     *  itself the audit answer, and it is a different fact from "we never looked". ABSENT ⇒ the lane
     *  did not report one (a foreign overlay, or a revision that failed its own domain screen — the
     *  evidence records that as `"not_reported"` rather than inventing a number). Never adjudication
     *  input: the verdict above is the whole decision, and dropping this field would leave every
     *  outcome byte-identical. */
    revision?: number;
} | {
    status: "unavailable";
    /** The overlay's own disclosure lines (engine/deployment-authored), for the operator channel. */
    disclosures: readonly string[];
};
/** One member of a {@link PersistedRuleHit} coverage set: a rule's canonical text plus (optionally)
 *  its add-dot identity. */
export interface PersistedRuleHitRule {
    /** The canonical text of one admitting rule. */
    readonly rule: string;
    /** The rule's LIVE add dots (tombstoned adds already filtered by the store's read). Omitted by a
     *  lane that has the text but not the dots; the evidence then reports `"not_reported"`. */
    readonly dots?: readonly import("./permission-rule-model.js").RuleDot[];
}
/**
 * design/252 G-2, re-formed by design/375 and again by the three-behavior model (BREAKING) — the
 * structured answer a persisted-rule lane gives: WHICH behavior spoke for the call, and the rule set it
 * stands on. For an `allow` the set is the COVERAGE SET that admits the call — a single rule admitting
 * the whole command is a one-element set (the historical case); a compound admitted by the conjunction
 * of per-segment rules (design/375 §5.1) is the set of those rules, in segment order. For a `deny` or an
 * `ask` the set is the one rule of that behavior that reaches the call (the lane's precedence walk —
 * deny > ask > allow — already chose it). `rules` is non-empty by contract — an empty set covers nothing
 * and is not a hit (the normalizer reads it as a clean negative).
 *
 * Why dots and not an id: a persisted rule HAS no scalar id — it is a (behavior, text, scope) group whose identity is
 * the set of add dots that redeemed it, and two concurrently approved records for one text each carry their own.
 *
 * The pre-375 single-rule object shape (`{ rule, dots? }`) and the pre-behavior `{ rules }` shape are NOT accepted:
 * on this loosening seam an unrecognized answer degrades to a clean negative (more asks), never to a match.
 */
export interface PersistedRuleHit {
    /** What the deciding rule(s) say about the call — the closed set's word. */
    readonly behavior: import("./permission-rule-model.js").RuleBehavior;
    /** The rule set the verdict stands on. Non-empty. */
    readonly rules: readonly PersistedRuleHitRule[];
    /** A path DENY's coordinates (where the reaching row met the call): the gate's exits mint the refusal's card from it. */
    readonly denial?: import("../tools/fs/safety.js").PathDenialEvidence;
}
/**
 * design/252 G-2 — the answer a lane gives when it could not READ its store, or a partition of it (no `reason`; the CALL form is `reason`'s).
 *
 * The store form fails CLOSED: a deny or ask row inside what could not be read may forbid this call and an allow beside it
 * is no yes, so the decision becomes a real-approval ask (origin `rule_store_unavailable`, class `rule_unreadable`:
 * no automatic lane clears it, `bypassPermissions` keeps it for a person, headless it is refused) until a read succeeds
 * again; a deny or ask row that WAS read still stands. The RECORD says `"unavailable"`, not `"no_match"`; the lane
 * announces the skipped partition once per run.
 */
export interface PersistedRuleUnreadable {
    readonly unreadable: true;
    /** WHY the lane could not read: absent for a store (or partition) read failure — the store form, a real-approval ask of
     *  class `rule_unreadable`; for a CALL the lane could not read against the person's standing deny/ask rows (an
     *  expansion where a rule reads a word, an unterminated quote, a syntax error) the lexer's reason — the call form, an allow
     *  tightens to a real-approval ask of the same class. Both carry the origin `rule_store_unavailable`. */
    readonly reason?: string;
}
/**
 * design/375 §5.2② — the CLEAN NEGATIVE that still carries the per-segment coverage table: no rule
 * admits the whole call, and THESE segments are the ones an eligible rule already covers. Minted by
 * the lane from the SAME store read that adjudicated the call (snapshot = adjudication snapshot, no
 * second read), and consumed only by the suggestion face — which puts on the batch offer exactly the
 * segments this table calls uncovered. Its DECISION meaning is identical to "no rule": the seat is
 * advisory, and a lane that omits it (or an unreadable one) degrades to all-uncovered, which merely
 * over-offers (every offered rule still passes the validator at redemption).
 */
export interface PersistedRuleCoverage {
    readonly segmentCoverage: readonly import("./permission-rule-model.js").SegmentCoverage[];
}
/** Every shape a lane may answer with. The retired bare-string form (a single allow rule's text) is read
 *  by the normalizer as a clean negative, like every other unrecognized shape. */
export type PersistedRuleAnswer = PersistedRuleHit | PersistedRuleCoverage | PersistedRuleUnreadable | undefined;
/** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort
 *  signal; when the caller also supplies {@link ToolGateInput.callSignal}, the Runner's closures
 *  additionally bind their waits to that per-call signal (`AbortSignal.any` of the two), so a turn
 *  interrupt releases a pending ask instead of leaving it hanging on the run's lifetime alone. */
export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, AllowEntryGateSeats {
    /** RB-473 batch 2b — bounded disclosure for a HOST callback that throws inside this gate call's
     *  isolation scope (onHookError itself, the deny observer, the durable-park observer). The runner
     *  wires this to an `observer.notify_failed` trace event; absent = counted only (unchanged). */
    onNotifyError?: (failure: import("./safe-notify.js").SafeNotifyFailure) => void;
    /** The call. `toolName` is the name every policy, hook and name-keyed table of this gate reads — the wire name of the
     *  tool the call dispatches to: the Runner's station resolves a call made under an alias through the roster
     *  (`rosterToolName`) before this gate runs; a host driving the gate resolves it the same way. `readArguments` = the loop's
     *  reading of the call's args: stamped on the call's face, every rewrite this gate adopts is read through it (`argsAsTheToolReads`). */
    event: {
        toolCallId: string;
        toolName: string;
        input: Record<string, unknown>;
        readArguments?: (args: unknown) => unknown;
    };
    /** The run/leg identity envelope, put verbatim on every {@link PreToolUseEvent} this
     *  gate call builds and on every {@link PermissionDeniedPayload} it emits. The Runner supplies its
     *  per-leg mint; a host driving the gate directly may omit it, and the contexts then carry no
     *  identity (honest absence — this layer never fabricates a leg). */
    identity?: HookInvocationIdentity;
    /** design/319 (A ticket) — the run's reminder provenance mark: the gate's own model-facing deny
     *  reasons ride `formatHookFeedback`, so their open tags must carry the same mark the run's
     *  system-prompt declaration names. Absent (a host driving the gate directly) ⇒ bare tags. */
    reminderMark?: string;
    /**
     * The leg's mods bus: phase 1, the approved-edit re-screen and (through the Runner) the approved-call re-screen raise
     * `classic.PreToolUse` on it; the lanes raise `tool.check` (once per adjudication, never over a deny). Absent, or no
     * handler on the event ⇒ nothing raised. A handler that throws, runs past its budget or breaks the site's contract refuses
     * the call fail-closed (the event's failure column), and so does a cancel; an observe handler never decides.
     */
    modBus?: Pick<import("./mods/kernel/index.js").ModBus, "hasHandlers" | "dispatch">;
    /** The inherited installations whose `tool.check` judges this leg's calls too (`inheritedToolChecks`, the rule's one home):
     *  raised after {@link modBus}, each over the verdict the previous left standing, read by the one comparison — their `allow`
     *  never clears an ask (a constraint only tightens), and their `ask` goes to each installation's own `settle` (where its
     *  installation froze the question), never to this gate's resolver; an approval there that edits the call takes this gate's
     *  approved-edit pass. Absent ⇒ only {@link modBus} is raised. */
    inheritedToolChecks?: readonly import("./mods/stations.js").InheritedToolCheck[];
    /** The leg's observed permission mode, carried on every `classic.PreToolUse` e this gate call raises. Absent ⇒ the e
     *  carries none (a host-driven gate with no leg seat). */
    observedPermissionMode?: () => PermissionMode;
    /** The task's live tracked cwd reader, put on every {@link PreToolUseEvent} this gate call raises
     *  (see {@link PreToolUseEvent.cwd}). A FUNCTION rather than a string for the reason the policy's own
     *  threading gives: capturing the value when the gate's wiring is built would freeze the task's
     *  starting directory and answer for a run that has since `cd`ed. Absent ⇒ the contexts carry no
     *  `cwd`, which is the honest answer for a run with no tracked one. */
    trackedCwd?: () => string | undefined;
    /** The leg's target-reading stamp, applied where the gate's request is minted over the final args (a rewrite re-reads it: `withArgsReading`). */
    stampCallTarget?: CallTargetStamp;
    /** The read-only env capability face put on every {@link PreToolUseEvent} this gate call
     *  builds ({@link HookEnvCapabilities}). Built ONCE per task by the runner (after the env is minted) and
     *  passed through unchanged; omitted when the deployment wired no execution environment. */
    hookEnv?: HookEnvCapabilities;
    /**
     * design/388 B6 — the leg's live roster lookup: the face of the mounted tool a call dispatches to, stamped
     * on the {@link ToolCallRequest} every policy sees (read at the gate entry, so a run-time roster change is
     * honoured on the next call). Absent ⇒ requests carry no face (a bare producer).
     */
    toolFaceOf?: (toolName: string) => ToolCallFace | undefined;
    /**
     * design/384 slice 1 — the PER-CALL abort signal, carried in from the `tool_call` hook event's
     * own `signal` seat (the loop's turn-scoped controller; the run abort is composed into it). The
     * gate itself never awaits on it — it hands the signal verbatim to the four consuming seats
     * below: `adjudicate`, `resolveAsk` and `resolveContentAsk` take it as their optional trailing
     * argument; `suspendAsk` (slice 2) reads it as `carry.signal` inside its structural seat
     * ({@link AskCarry}), and its park closure composes it into the halt×park fence. The Runner's
     * closures compose it with the run signal so a turn interrupt (bare halt / steer-now) releases
     * the wait — or, on the durable leg, makes the park concede. Absent (a host driving the gate
     * directly, or an unthreaded emit) ⇒ the three synchronous seats are invoked exactly as before —
     * same arity, no trailing `undefined` (the threading is a conditional invocation) — the carry has
     * no `signal` member, and waits stay bound to the run signal alone: the historical shape.
     */
    callSignal?: AbortSignal;
    /** The combined tool-policy check (abort-bound), or undefined when no policy is wired (→ allow).
     *  The optional trailing `callSignal` (design/384) is {@link ToolGateInput.callSignal}, passed
     *  through verbatim; an implementation that ignores it keeps the run-level binding. */
    adjudicate?: (req: ToolCallRequest, callSignal?: AbortSignal) => Promise<PermissionResult>;
    /** Resolve an `ask` to allow/deny via `onAsk` (abort-bound). Required iff a decision can be `ask`.
     *  G1 three-value: the result may carry `approverUnavailable` — the live approver affirmed no
     *  operator is reachable for THIS ask; the gate then re-routes onto the durable park leg (below),
     *  keeping the carried fail-closed deny when no park is armed. The optional trailing `callSignal`
     *  (design/384) is {@link ToolGateInput.callSignal}, passed through verbatim. */
    resolveAsk: (decision: PermissionResult, req: ToolCallRequest, callSignal?: AbortSignal) => Promise<ResolvedAsk>;
    /**
     * design/45: route a policy `ask` to a **durable suspension** instead of the synchronous `resolveAsk`
     * (onAsk) path. Called in the `ask` branch with the FINAL post-hook args; if it returns a suspend
     * directive it has already persisted the checkpoint + aborted the run, and the gate short-circuits to
     * `{ suspend }`. Returns `undefined` to fall through to `resolveAsk` (non-durable / declined). A
     * {@link ParkAttemptFailed} ALSO falls through to `resolveAsk` — same fallback, same compensation —
     * but carries why the mint failed so a resulting deny can name it instead of leaving the model and
     * the operator with the fallback's own reason as if it were the cause.
     * Omitted entirely → no durable mode (the 1.63 synchronous path, unchanged).
     *
     * The armed closure itself may also DECLINE (return `undefined` up front) to prefer the live
     * synchronous approver: the Runner's closure does so for a plain AND for a SAFETY ask whenever the
     * resolved seat is a FUNCTION-valued `onAsk` — even under `durableApproval`, which is the FALLBACK for
     * when no live approver is present, not an "always park" mandate — an interactive per-edit ask must
     * resolve in-stream, not cost a park/rebuild round-trip. The gate stays a pure fall-through consumer of
     * that choice. What KEEPS the durable leg is no function seat (absent, or a blanket `"allow"`/`"deny"`
     * string mode — a policy setting is not a live approver), a control-plane `forceDurableGate`, a live
     * face that already answered `"unavailable"` for this ask, or an inherited-unavailable marked call.
     * `safety` no longer participates in the park/sync decision at all — it only selects the gate KIND
     * (`irreversible_ask`) once a park is minted.
     */
    suspendAsk?: (req: ToolCallRequest, postHookArgs: unknown, 
    /** design/80 D-2: present (non-undefined) ONLY when this ask is a SAFETY tighten (the egress/
     *  irreversibility gate tightened a surviving `allow` to `ask`, `decisionReason === "safety"`). Drives the
     *  mint-site gate-kind choice — a safety tighten mints `irreversible_ask` (non-budgetable) EVEN WHEN
     *  `durableApproval` is wired. The object records which axis(es) tightened (persisted as the gate's
     *  `safetyAxis`). Absent for a plain policy/hook `ask` (→ the normal `human`/`irreversible_ask` choice). */
    safety?: import("./checkpoint-store.js").SafetyAxis, 
    /** The three-valued live-face verdict, widened by design/174 to cover BOTH live faces: true ⇒ the live face this
     *  ask would have been routed to already answered `"unavailable"` for it — the permission approver for
     *  a permission ask, the question face for a content ask. Either way there is no live leg left to
     *  prefer, so the closure must NOT take its sync-first decline and should park durably. All other
     *  decline/pre-commit-failure paths keep their existing `undefined` fallbacks (the gate then keeps the
     *  fail-closed deny / typed refusal the caller carries). */
    liveFaceUnavailable?: boolean, 
    /** Present ⇒ the surviving ask carries `requiresRealApproval` and the mint must escalate to
     *  the non-budgetable `irreversible_ask` kind carrying this bit (a budget resolver must never
     *  auto-approve what only judgment may clear). `origin` records whether the bit came from an org
     *  ASK rule, from the org-unavailable tighten (whose resume semantics differ — see
     *  {@link import("./checkpoint-store.js").RealApprovalGateBit}), or from a policy/hook. */
    realApproval?: import("./checkpoint-store.js").RealApprovalGateBit, 
    /** The allow-layer members that matched and could not clear (the surviving ask's `allowRuleShadowed`)
     *  — threaded so the park mint's risk descriptor carries the disclosure on the durable route. */
    allowRuleShadowed?: readonly import("./allow-rule-shadow.js").AllowRuleShadow[], 
    /** The surviving ask's `decisionReason` — threaded so the park mint's rule-suggestion door can
     *  read the ask's origin on the durable route exactly as the synchronous mints read it off the
     *  decision (a hook-raised ask is one no persisted rule may clear, so its parked card must not
     *  offer to mint one). */
    askDecisionReason?: DecisionReason, 
    /** The reversibility probe's cause for the tighten (the surviving ask's `probeReason`)
     *  — threaded so the park mint's risk descriptor carries on the durable route the same account of
     *  WHY the call was gated that the synchronous card gets. Already neutralized + capped at the gate. */
    probeReason?: string, 
    /** The STRUCTURED cause (the surviving ask's `probeCause`) — same durable-route errand
     *  as `probeReason`, already validated at the gate. */
    probeCause?: import("./checkpoint-store.js").ProbeCause, 
    /** design/375 §5.2②: the surviving ask's engine-stamped `segmentCoverage` — threaded so the park
     *  mint's offer factory reads the same coverage snapshot on the durable route as the synchronous
     *  mints read off the decision (absent ⇒ all segments uncovered ⇒ over-offer, the safe way). */
    segmentCoverage?: readonly import("./permission-rule-model.js").SegmentCoverage[], 
    /** #457 ④: the surviving ask's `matchedAskRule` — threaded for the SAME errand as
     *  `askDecisionReason`: the park mint's rule-suggestion door must know that an EXPLICIT `ask` rule
     *  matched this call, because the persisted-rule lane refuses to clear such an ask (the
     *  `decision.matchedAskRule === undefined` conjunct on its allow arm), so the parked card must not
     *  offer to mint a rule that would never silence it. APPENDED at the tail deliberately: every
     *  parameter here is positional and `probeReason` is also a `string`, so inserting this beside its
     *  logical neighbour would silently re-bind three existing call sites' arguments with no type
     *  error to catch it. */
    matchedAskRule?: string, 
    /** The surviving ask's engine-stamped `probeMandated` — threaded for the SAME errand as
     *  `askDecisionReason` and `matchedAskRule`: the park mint's rule-suggestion door must know that
     *  this ask is one no persisted rule may clear, or the parked card offers a "stop asking me this"
     *  rule the lane would then refuse to honor. APPENDED at the tail for the reason its neighbour
     *  states — every parameter here is positional — and typed `boolean` so a mis-bind against the two
     *  trailing `string` seats is a compile error rather than a silent re-binding. */
    probeMandated?: boolean, 
    /** The ONE structural seat after the twelve positional ones — everything else the park closure needs
     *  travels inside it ({@link AskCarry}: the per-call signal, the surviving ask's fallback member, its
     *  origin word), so the tuple does not grow a seat per member. ALWAYS passed by the gate (thirteen
     *  arguments, every call) — optional on the TYPE only because the ten seats before it are optional and
     *  a required parameter cannot follow one. The twelve seats above are the historical shape; a host-built
     *  closure that read the signal as the 13th positional argument now reads `carry.signal`. */
    carry?: AskCarry) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
    /**
     * design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
     * before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
     * post-policy request (the same args that will execute), and it owns everything about delivery:
     * validating the batch before anything is put in front of a person, calling the live face, and binding
     * the answer to this call id for the tool to pick up. It returns only WHICH of the three outcomes
     * happened — never the answer, which travels the run's own binding.
     *
     * CONTRACT: it must not throw (a delivery failure is a returned value, so the gate can tell "failed"
     * from "nobody home" — the two have opposite re-routing rules). Passed UNCONDITIONALLY by the runner,
     * including on runs with no live face: it then answers `unavailable` without contacting anything, which
     * is exactly the pre-174 park/refusal behavior. Absent entirely ⇒ the pre-174 path verbatim.
     * The optional trailing `callSignal` (design/384) is {@link ToolGateInput.callSignal}, passed
     * through verbatim; an implementation that ignores it keeps the run-level binding.
     */
    resolveContentAsk?: (req: ToolCallRequest, callSignal?: AbortSignal) => Promise<ContentAskOutcome>;
    /**
     * design/70: the called tool is egress-marked (`ToolSpec.egress` — an external write: open a PR,
     * push, send). The gate tightens a surviving `allow` to `ask` so an egress tool is NEVER
     * auto-allowed — execution always passes an explicit ask-resolution (onAsk / durable suspend);
     * headless resolves to deny. Filled by the caller from the tool's spec; policies stay egress-blind.
     */
    egress?: boolean;
    /**
     * design/77 §4 (Gate 4): the called tool's irreversibility tier (`ToolSpec.irreversibility`), filled by
     * the caller from the tool's spec. After the egress tighten, the gate runs a SECOND deterministic tighten:
     * `"always"` tightens a surviving `allow` to `ask`; `"maybe"` calls {@link reversibilityProbe} and tightens
     * UNLESS the probe reports reversible (fail-closed on timeout/throw); `"never"`/undefined → untouched.
     * Policies stay irreversibility-blind; the gate is the single chokepoint.
     */
    irreversibility?: "never" | "maybe" | "always";
    /**
     * design/77 §4: the `"maybe"`-tier probe from the tool's spec (read at prepare-time, captured here — not a
     * tool argument). Called when `irreversibility === "maybe"` && the surviving decision is `allow` (the
     * TIGHTEN arm) — and, since #502, when it is already `ask` (the MANDATE arm: the probe is the only
     * source of `mandated`, and an ask another layer raised needs it too; there the `reversible` verdict is
     * discarded, since a probe may never un-ask another layer's question). Both arms are time-bounded by
     * {@link approvalTimeoutMs} (30s default). Fail-closed: anything other than `{ reversible: true }`
     * (including a timeout or a throw) tightens to `ask`. A probe is never trusted to AUTO-ALLOW past the gate.
     * The invocation set is therefore WIDER than the tighten's: an ask-first policy calls this on every
     * `"maybe"`-tier call, and a throwing/hanging probe costs an `onError(phase:"hook")` and the wait there
     * too — see `ToolSpec.reversibilityProbe` for the deployment-facing statement of that cost.
     *
     * An optional `reason` on a TIGHTENING verdict names the cause. The gate neutralizes + caps it and
     * carries it onto both approval routes (`AskRequest.probeReason`, `RiskDescriptor.probeReason`) — see
     * `ToolSpec.reversibilityProbe`. Display/triage only: `reversible` alone decides the verdict, so a
     * probe cannot widen anything through this member.
     */
    reversibilityProbe?: (args: unknown) => import("./types.js").ReversibilityVerdict | Promise<import("./types.js").ReversibilityVerdict>;
    /** design/77 §4: deadline (ms) for {@link reversibilityProbe}; on timeout the gate fails closed to `ask`.
     *  ABSENT ⇒ a bounded default applies (30s — the probe wait is never unbounded, even with no
     *  {@link abortSignal}); a non-finite/negative value is refused loudly (via {@link onHookError}) to
     *  that same default, never silently reinterpreted. `0` is honored as written (immediate deadline). */
    approvalTimeoutMs?: number;
    /** design/77 §4: the task abort signal — bounds {@link reversibilityProbe} by the task's real deadline
     *  (timeout/cancel) in addition to {@link approvalTimeoutMs}; an abort while probing fails closed to `ask`. */
    abortSignal?: AbortSignal;
    /**
     * design/134 R5: a host's own observer for the gate's DENY short-circuit (the Runner's are its leg's handlers on
     * {@link modBus}), run as one more observe handler of `classic.PermissionDenied` — that station's columns; a failure
     * is disclosed on {@link onNotifyError} as `toolGate.permissionDenied`. Fired ONCE, at the single deny exit of the
     * adjudicate chain — a policy deny or a resolved-ask deny. NOT fired for a PreToolUse hook deny (the
     * exclusion on {@link PermissionDeniedPayload}, which also records why that exclusion is not a parity
     * claim) or a durable suspend (the ask path).
     *
     * ALSO fired for the engine's fail-closed interception of a THROWING PreToolUse hook, with
     * `source:"hook"` — the exclusion is the hook's own deny DECISION, not an engine decision.
     */
    permissionDenied?: (payload: PermissionDeniedPayload) => void | Promise<void>;
    /**
     * Trace sink for a deployment-supplied callback of this gate that failed while the gate holds the fail-closed line
     * — a {@link reversibilityProbe} that threw or timed out — and for a malformed {@link approvalTimeoutMs} refused to
     * the bounded default (`onError(phase:"hook")`, wired by the caller). A failed `classic.PreToolUse` handler is
     * reported by the bus itself. Never affects the outcome (a throwing sink is swallowed). A task-abort rejection
     * mid-probe is NOT reported (normal cancellation, not a defect).
     */
    onHookError?: (err: unknown) => void;
    /**
     * design/134 R5: true when this tool's `irreversibility` tier was installed by the ENGINE'S SHELL SEAT
     * (the read-boundary judge on every shell face, plus the shape tier on every full shell and the
     * `TaskSpec.shellGate: "always"` floor — design/80 D-E; the seat's `shellGatedTools` membership), not an explicit per-tool mark —
     * attributes a tighten-deny to `source:"shellGate"` instead of `"safety"`.
     */
    shellGated?: boolean;
    /**
     * design/276 (CC 2.1.233 P11 arm 4): true when the called tool is the peer-message verb — a message
     * to another agent (`SendMessage`). ENGINE-FILLED from the wire name at the caller, never a
     * configuration knob; a caller's same-named shadow tool is keyed too (for a tighten, over-asking is
     * the fail-safe direction). Under an armed {@link autoMode} the gate tightens a surviving `allow` to
     * `ask` (the peer-referral tighten below) so the message passes the classifier's eye — its prompt
     * already carries the Multi-Agent Coordination exemption, so the ordinary teammate message resolves
     * allow and the injection-shaped one is what this member exists to catch. Absent/false, or with no
     * armed auto mode, the decision path is byte-identical. Per-run semantic: armed by THIS run's
     * resolved caps, never inherited down the delegation chain (CC parity — the mode predicate reads the
     * SENDING session's own mode).
     */
    peerMessage?: boolean;
    /**
     * The write-protection judge — ENGINE-BUILT from the deployment's table seat by `createWriteProtectionCheck`
     * (write-protect.ts), never a deployment callback (the deployment authors table ROWS, validated loudly at
     * compile; the judge's only I/O is the shared canonicalizer over the run's own environment, so it is trusted
     * here like the `egress` mark). Judges the FINAL args of a path-confinable write tool by the file the tool
     * will write — spelling and resolved real target; a hit demotes a surviving `allow` to `ask`, an unresolvable
     * target is refused (the tighten below). `null` SYNCHRONOUSLY for a call it does not cover (no suspension
     * point there), a promise for a covered write. Absent ⇒ no table (`[]`) and the decision path is byte-identical.
     */
    writeProtectionCheck?: import("./write-protect.js").WriteProtectionCheck;
    /**
     * The shell READ STATION — ENGINE-BUILT by the hands phase (`createShellReadStation`,
     * `src/tools/fs/shell-read-check.ts`) for the shell-running tools of this leg, never a deployment callback.
     * Judges the FINAL args of a shell call: ONE parse of the command into its read candidates, each
     * established against the execution environment (symlinks resolved, patterns expanded, traversal roots
     * enumerated, within a bound) and judged by the leg's read deny seat and its containment roots. A
     * POLICY-class member of the fold: a deny-listed read is REFUSED (`deniedBy: "read_boundary"`, composing
     * under `deny > ask > allow`), an out-of-root or unestablishable read tightens a surviving allow to a
     * MANDATED ask (or marks an existing ask mandated), a read the station declined to judge tightens to an
     * ordinary ask on the full shell face (under `auto` the classifier answers it, shown the read-deny table) and
     * is refused on the read-only face, and a vouched call is untouched.
     * `null` SYNCHRONOUSLY for a call it does not cover (no suspension point there), a promise for a shell
     * call. Absent ⇒ no station (no shell mounted) and the decision path is byte-identical.
     */
    shellReadCheck?: import("../tools/fs/shell-read-check.js").ShellReadCheck;
    /**
     * design/143 批2 (A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the
     * small-model policy CLASSIFIER before any human/durable resolution:
     *  - verdict `allow` → the ask resolves to allow (`decisionReason:"classifier"`) — no suspend, no onAsk;
     *  - verdict `block` → deny (`decisionReason:"classifier"`, `source:"classifier"`; CC `automode-blocked`); `parse_error` too;
     *  - `unavailable` → where `unavailable.route` names a person (`live_approver`, or `durable_park`) the ask goes on, its text saying why;
     *    else CC's refusal (`cause: "unavailable"`). Never counted by the denial limit (`classifierUnavailableArm`).
     * SCOPE (declared semantic): this covers every surviving ask INCLUDING the deterministic egress/
     * irreversibility safety tightens — auto mode is the deployment's explicit choice to let the
     * classifier be the "explicit ask-resolution" for this session (its rule set covers exactly those
     * action classes: outbound writes, irreversible operations, and sensitive-value handling). A
     * deployment that wants human-only
     * resolution for safety asks simply does not arm auto mode. The classifier can never AUTO-ALLOW by
     * failing — only an affirmative `<block>no</block>` allows.
     * EXCLUDED from that scope (the ask flows the ORIGINAL chain untouched, exactly as if this field
     * were absent for that one call): every ask whose ORIGIN is not the plain `policy` family — the
     * closed set and the classifier's per-origin table are in `ask-origin.ts` (an org-constrained ask on
     * either org arm, the reserved question tool, a call MARKED unresolvable, a hook-raised or
     * hook-promoted ask, an explicit matched `ask` rule, and the classifier's own denial-limit fallback).
     * The hook and ask-rule rows are the same pair the persisted-rule lane refuses, so the gate's two
     * ask→allow seams answer provenance questions identically.
     */
    autoMode?: {
        decider: import("./auto-mode.js").AutoModeDecider;
        /**
         * The per-run DENIAL-LIMIT tracker beside the decider (CC 2.1.250 `FO`/`Wie`; same owner,
         * same lifetime). Absent ⇒ the pre-#548 chain byte for byte: every block is a deny, without bound.
         * Present ⇒ a block first counts, then judges; the block that reaches a bound becomes the fallback
         * ask (`requiresRealApproval: true` + `denialLimitFallback`), and a classifier allow or a person's
         * allow of that ask resets the consecutive count. The Runner builds it from
         * `RunnerDeps.autoMode.denialLimit`; a host driving the gate directly may hand its own.
         */
        denialTracking?: AutoModeDenialTracker;
        unavailable?: import("./classifier-unavailable-arm.js").ClassifierUnavailableSeat;
    };
    /**
     * The leg's EFFECTIVE permission mode, read at every arbitration (a steer changes the next call's answer). The
     * gate's one mode read point sits after the classifier and the sandbox leg and before the park / resolution:
     * `bypassPermissions` releases every non-immune class (`GateDisposition.released`), `dontAsk` refuses every ask
     * but a hook's own (`deniedBy: "mode"`), the other modes hand the ask on. ABSENT ⇒ no
     * arbitration (a host driving the gate directly keeps the pre-mode chain); the classifier station then reads
     * `autoMode` presence alone as the arming fact.
     */
    permissionMode?: () => import("./ask-origin.js").PermissionMode;
    /** The leg's approval-seat form (`WiringManifest.ask.form`): the deny exit's way-out clause names a declaration, never a missing approver. */
    askSeatForm?: import("./wiring-manifest.js").AskSeamForm;
    /** The read face the leg resolved, read per call into the class derivation (`roots` withholds the classifier from a `probe` question); absent ⇒ none resolved. */
    readFace?: () => import("../tools/fs/containment-face.js").ContainmentFace | undefined;
    /** WHO put the tool's egress / irreversibility marks on the seat ({@link import("./ask-origin.js").ToolMarkSource});
     *  absent or empty with a mark present reads as the deployment's own (fail-closed: an operator's mandate). */
    toolMarkSources?: ReadonlySet<import("./ask-origin.js").ToolMarkSource>;
    /** The called tool's seat carries its server's declaration that a call must be put to a person
     *  ({@link import("../engine/loop/types.js").AgentTool.mcpRequiresUserInteraction}): a surviving `allow` is tightened to an `ask`, and every ask on the call carries
     *  the class `requires_user_interaction` and the real-approval bit (no rule, grant, classifier, bypass mode, blanket seat or policy-settled approver answers it). Absent ⇒ not declared. */
    requiresUserInteraction?: true;
    /** The deployment's A2A peer-isolation switch: a set switch makes the A2A egress default an operator mandate. */
    a2aIsolatePeers?: boolean;
    /** Observe a mode RELEASE (fired before the released allow leaves the arbitration point; `readUnestablished`: the read station could not establish what the call reads). Observe-only. */
    onModeReleased?: (info: {
        toolName: string;
        toolCallId: string;
        release: import("./gate-outcome.js").ModeRelease;
        readUnestablished?: true;
    }) => void;
    /** An ancestor station's arbitration of this call (the inherited lane arbitrates inside the fold and records
     *  here); the exit reads it onto the disposition when the gate's own point did not arbitrate. */
    ancestorArbitrationOf?: (toolCallId: string) => import("./ask-origin.js").Arbitration | undefined;
    /**
     * The HEADLESS arm of the denial-limit fallback: the fallback ask was resolved by a deny no
     * person made (no approver wired; a blanket `"allow"` refused as no approver; an approver that
     * reported unavailable with no park to take it). The fallback had nowhere to go, so the caller — which
     * owns the run — stops it (`TaskResult.errorCode = "classifier.denial_limit"`, CC's "too many
     * classifier denials in headless mode" abort). The deny itself still stands; this gate only names the
     * fact. A TOP-LEVEL seat, deliberately NOT under `autoMode`: the fallback can arrive from an ANCESTOR's
     * frozen tracker through the fold (a delegated child with no armed classifier of its own), and that
     * child's run is the one that has to stop. Observe-only for the gate: a throwing seat never alters
     * the deny.
     */
    onHeadlessDenialLimit?: (info: {
        toolName: string;
        toolCallId: string;
        fallback: import("./auto-mode.js").DenialLimitFallback;
    }) => void;
    /**
     * The deny exit nobody judged: the call's approval could not be RESOLVED (`cause` — the approver
     * answered `unavailable`, no approver is wired, or an inherited durable mandate has no park facility on
     * this leg) and no durable park took the call, so the fail-closed deny STANDS. Fired once per such call
     * from the gate's single deny exit, after every park re-route has had its attempt, with the facts the
     * `delegation.ask_unresolvable` notice is composed from (`ask-unresolvable-notice.ts`);
     * `parkLaneExisted` = a park lane was armed for this run; `remedy` = the caller-side change that would have let the
     * call run, present for the causes that say nobody could be asked (`approver_unavailable`, `no_approver_wired`).
     * Observe-only: a throwing seat never alters the deny. Absent ⇒ the deny stands silently (a host driving the gate
     * directly, no notice sink).
     */
    onAskUnresolvable?: (info: {
        toolName: string;
        toolCallId: string;
        cause: import("./ask-unresolvable-notice.js").AskUnresolvableCause;
        parkLaneExisted: boolean;
        remedy?: string;
    }) => void;
    /**
     * design/153 §2/§7.4 (件4 复审, MED): true means this call's ask is MARKED — an inherited ancestor
     * constraint already determined "no synchronous layer may resolve this ask" (the ancestor's frozen
     * approver was unreachable, OR an ancestor durable mandate was floated back because THIS task's own
     * park facility can honor it). The marker's whole contract is that the ask reaches EITHER the durable
     * park leg OR the fail-closed `resolveAskBound` intercept, with no synchronous decision-maker in
     * between — the auto-mode classifier is exactly such a decision-maker (`decide()` can `allow`, executing
     * the call) and sat strictly BEFORE both marker consumers, so a marked call under an armed classifier
     * bypassed the marker's contract entirely. When true, the classifier step is skipped (falls through to
     * `suspendAsk`/`resolveAsk` untouched) so a marked ask can only ever be resolved by an operator (durable
     * park) or fail closed — never by the classifier.
     */
    isMarkedUnresolvable?: (toolCallId: string) => boolean;
    /**
     * design/179 — the persisted-rule lane. Present only when a deployment wired a rule store; absent it,
     * this whole step does not exist and the gate's decision path is byte-identical to what it was.
     *
     * The lane answers with the BEHAVIOR that spoke ({@link PersistedRuleHit}), and the gate consumes the
     * three behaviors at two different places — the upstream arbiter's order, deny > ask > allow, across
     * the person's own standing rules:
     *  · a DENY row refuses the call outright, on EVERY decision (an allow from the policy fold included):
     *    it is the person's own "never run this" (their settings' deny list, imported), the personal-store
     *    sibling of an org deny, and it runs right after the org layer — nothing downstream re-opens it;
     *  · an ASK row is the person's standing "ask me each time": it turns a surviving allow into an ask
     *    and stamps `matchedAskRule` on it (the same marker an explicit `ask` permission rule stamps), so
     *    the ask's origin is `ask_rule` — the auto-mode classifier never answers it and a persisted ALLOW
     *    never clears it; on a call that is already an ask it stamps the marker and leaves the rest;
     *  · an ALLOW row is the standing form of ONE recorded human approval, so it resolves the asks a real
     *    person saying "allow" would have resolved — and nothing else. It sits post-fold, at the entry of
     *    the `ask` branch, BEFORE the classifier and AFTER every tighten: earlier and a tighten would
     *    simply put the ask back; later and the classifier would already have spoken.
     *
     * The deny/ask arms tighten and take no conjunct but the two structural ones every lane shares (the
     * engine's own content-ask tool is exempt; the lane must be wired). The ALLOW arm's consumption
     * predicate is its own, not the classifier's blanket flip:
     *  · `requiresRealApproval` asks are never consumed — the two integrity gates are the floor, and the
     *    same flag is what a v2 organization-level ask will carry;
     *  · a PreToolUse hook's ask is never consumed — that is the deployment's own adjudication, and a rule
     *    minted for a command does not overrule it;
     *  · the reserved question tool and a call MARKED unresolvable are excluded for the same reason the
     *    classifier excludes them: both contracts require that no synchronous decision-maker stands between
     *    the ask and the park / content route, and this lane is one.
     *  · #144 (ruled): a MANDATED ask is never consumed — an operator's shellGate:"always" tier and a
     *    tool's own egress/irreversibility marks are structural requirements, not classifier hesitation
     *    ("allow rules silence the classifier's questions, never a mandated one"). When a rule matches
     *    but cannot clear, the surviving ask discloses it (`allowRuleShadowed`, member `persisted`).
     *  · #502: an ask the call's own reversibility probe declared STRUCTURAL (`probeMandated`) joins that
     *    family. It is the one PER-CALL member of it, and the reason there has to be one: the built-in
     *    shell probe raises it for a read outside the session's allowed directories, which is a boundary
     *    the deployment declared but which arrives wearing the shape tier's own "maybe" —
     *    the very tier the bullet below keeps as this lane's home turf. Without a per-call member, a
     *    compound was cleared segment-wise by the conjunction arm and an out-of-root read plus a sender
     *    ran on two ordinary standing rules with no question asked.
     *  · #213: an ask carrying `matchedAskRule` (an explicit `ask` permission rule matched this call)
     *    is never consumed — a person's standing "ask me each time" outranks the memory of one past
     *    yes, across lanes exactly as the DSL's own deny > ask > allow order says within one. Same
     *    disclosure as the mandated family when a rule matches anyway.
     * What it DOES consume is the classify-DOCTRINE shell ask (the coarse tier "maybe" — the
     * don't-ask-again main case this feature exists for) and unmarked bare asks. Consuming such an ask
     * also skips the park it would have minted; that IS what a standing approval means, and the
     * bullets above are what keep integrity/hook/mandated asks out of that set.
     */
    persistedRules?: {
        /** The lane's verdict for this call — a {@link PersistedRuleHit} naming the behavior that spoke and
         *  the rule set it stands on, a coverage-carrying clean negative, an unreadable report, or
         *  `undefined` (the store was READ and no rule speaks). Must not throw — and a store (or a partition of it) that cannot
         *  be read answers `{ unreadable: true }`, not `undefined`: it fails closed (a row nobody could read may forbid the call;
         *  the decision becomes a real-approval ask) and the evidence records `"unavailable"`, not `"no_match"`; the lane owns
         *  announcing the skip. A call unreadable against the rows answers `{ unreadable: true, reason }` (the call form).
         *
         *  For an ALLOW the set is the COVERAGE SET — each member's canonical text plus its add DOTS. A
         *  single rule admitting the whole command is a one-element set; a compound admitted by the
         *  per-segment conjunction (design/375 §5.1) is the set of the covering rules. The dots are a rule's
         *  durable identity, and the text is not one (it reaches a card sanitized and capped); a lane that
         *  can name them lets the shadowed-ask evidence say WHICH stored rules matched rather than merely
         *  what they looked like. The retired shapes — a bare rule TEXT, the pre-375 `{ rule, dots? }` object,
         *  a `{ rules }` hit without its behavior — are read as a clean negative by the normalizer: a stale
         *  lane degrades toward asking, never toward an allow. Consulted on EVERY decision (a deny/ask row
         *  must be able to tighten an allow), so the lane pays one store read per call on the tools it
         *  speaks for; the runner's lane hands the org layer's read to it, so a governed deployment still
         *  pays one. */
        adjudicate: (req: ToolCallRequest) => Promise<PersistedRuleAnswer>;
        /** Observation sink for an ask a rule coverage resolved — the allow-side disclosure. `rules` is
         *  the coverage set's canonical texts in reporting order (design/375: a conjunction names every
         *  rule the decision stands on). Never affects the outcome. */
        onResolved?: (info: {
            toolName: string;
            toolCallId: string;
            rules: readonly string[];
        }) => void;
    };
    /**
     * Observation sink for a shell call the READ-ONLY reader cleared (`readOnlyShellVerdict`, the
     * allow layer's second member beside the persisted allow rule: consulted for the shell tool only, after
     * the person's allow rules, under the SAME consumption predicate — never over a mandated ask, a hook's
     * ask, an explicit ask rule, governance or a marked call). The allow-side disclosure of "why did this run
     * without asking me?" for that arm; `command` is the FINAL command the gate judged (a policy rewrite
     * included). Never affects the outcome. Absent ⇒ the arm still runs, silently.
     */
    onReadOnlyAllowed?: (info: {
        toolName: string;
        toolCallId: string;
        command: string;
    }) => void;
    /**
     * design/182 §7 — the ORG layer. Present ONLY when the deployment DECLARED org governance (the
     * overlay constructor is the boot gate: a governed declaration with no snapshot provider refuses to
     * boot). Absent ⇒ the whole step does not exist and the decision path is byte-identical.
     *
     * The org layer is the outermost TIGHTENING authority, so it runs after every fold and every engine
     * tighten and before the ask-resolution chain — "org deny > org ask (non-dismissable) > personal
     * allow rule > bare ask". It never loosens: there is structurally no org allow bucket.
     *
     * The `unavailable` arm is the load-bearing one. An org-governed deployment that cannot reach a
     * snapshot fails the WHOLE decision boundary closed — every terminal allow (a deployment-policy
     * allow and the no-policy default allow included) becomes an ask carrying
     * `decisionReason: "org_unavailable"` AND `requiresRealApproval: true`, and both ask→allow seams (the
     * personal-rule lane and the auto-mode classifier) are disarmed. Tightening only the two seams was
     * refuted during design: a call that is ALREADY allow never passes through either seam, so it would
     * have sailed past the org deny that could not be read. The `requiresRealApproval` bit closes the
     * third loosening path — a blanket `onAsk: "allow"` would otherwise clear the synthetic ask.
     */
    orgRules?: {
        /** Adjudicate this call against the current org overlay. MUST NOT throw — a throw is read as
         *  `unavailable` (fail-closed), never as "no org rule spoke". */
        adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
        /** True iff the reserved question-tool NAME on this leg resolves to the ENGINE's own content-ask
         *  tool. It is the exemption's real predicate: the name alone is not, because a caller may mount
         *  its own tool under that name when the first-party one is not mounted, and such a tool has
         *  arbitrary side effects (confirmed MED). Absent ⇒ `false` ⇒ the call is governed
         *  like any other, which is the fail-closed default. */
        contentAskToolMounted?: boolean;
        /** Disclosure sink for the unavailable arm (trace `permission.org_snapshot_unavailable`). Never
         *  affects the outcome. */
        onUnavailable?: (info: {
            toolName: string;
            toolCallId: string;
            message: string;
        }) => void;
    };
    /**
     * F-012 L2 — the SANDBOX ADMISSION leg. Present ONLY when the runner armed the mode: the execution
     * env DECLARES isolation (`RemoteExecutionEnv.capabilities.isolation === true` — a Runner-resolved
     * fact, never caller self-report) AND the park prerequisites hold (durable park configured + a
     * checkpoint store that explicitly declares `redecision.reopen`). Absent ⇒ the whole step does not
     * exist and the decision path is byte-identical to a build without it.
     *
     * Semantics (the ruling): inside a declared sandbox, an engine-authored "confirm a potentially
     * destructive local action" ask is answered by containment itself — auto-allow + durable
     * disclosure (`permission.sandbox_admitted`), never a headless deny that pollutes the model.
     * STRICT dominance governs: the leg admits only when EVERY surviving ask on the call is classified
     * `sandbox_local` in the engine's private envelope (deployment-policy asks are `unknown`, hook
     * asks `hook`, marked calls `mandate`, `requiresRealApproval` asks external — none admit), and
     * the call crosses NO declared boundary (`boundaryCapable`: egress-marked tools and
     * protocol-mounted (MCP/A2A) tools — a server's self-report is always boundary-capable). A
     * boundary-crossing ask under the armed mode takes the ordinary durable-park leg below — "park to
     * a human", never an auto-deny.
     */
    sandboxAdmission?: {
        /** The ask classes recorded for this call's fold-layer asks (with their source layers), or
         *  `undefined` when no fold layer asked — the gate's own safety tighten is then the only asker. */
        askClassesOf: (toolCallId: string) => ReadonlyArray<{
            cls: AskClass;
            layer: string;
        }> | undefined;
        /** This call's tool crosses a DECLARED boundary (egress axis / protocol-mounted) — never admitted. */
        boundaryCapable: boolean;
        /** The durable disclosure sink — fired on every admission BEFORE the allow is returned. */
        onAdmitted: (info: {
            toolCallId: string;
            toolName: string;
            askClasses: ReadonlyArray<{
                cls: AskClass;
                layer: string;
            }>;
            boundary: "sandbox_internal";
        }) => void;
    };
}
/**
 * The PreToolUse screening face ({@link PreToolUseScreening}) for ONE call — `classic.PreToolUse` raised on the leg's
 * bus, the per-screening e and the fail-closed sentences, built in ONE place for every site that screens a call: the
 * gate's phase 1 and approved-edit re-screen (through {@link runToolGate}'s pass) and the resumed leg's re-screen of an
 * approved call. Undefined when no handler hangs on the event.
 *
 * The e: `tool` (the call's wire name), `tool_use_id`, `input` — a deep-frozen copy of the args, so what a handler
 * judges is what runs (`input` is pinned; a rewrite is the answer's `updatedInput`) — `agentId` on a delegated leg,
 * `cwd` (the task's tracked cwd, read per screening), `env` (the capability face), `identity`, `permissionMode`.
 */
export declare function createPreToolUseScreening(input: Pick<ToolGateInput, "event" | "modBus" | "observedPermissionMode" | "trackedCwd" | "hookEnv" | "identity" | "abortSignal">): PreToolUseScreening | undefined;
/**
 * Fold a leg's `classic.PreToolUse` chain into a {@link ToolPolicy} layer, so the screening a task installed can also
 * travel down a delegation chain as one more inherited constraint — the constraint MINT. `chain` is the installing
 * leg's bus; the policy raises `classic.PreToolUse` on it with the handlers that JUDGE (an observe handler never enters
 * a constraint — the participant filter below is that rule's one home).
 *
 * Why a policy and not a second channel: a delegated child already folds each ancestor's caller policy
 * (`InheritedGate.parentConstraints`), and that fold is where `deny > ask > allow`, the rewrite cascade, the
 * ancestor-frozen approver and the durable mandate all already live. Expressing the screening in the same shape means
 * the child gains no new decision channel — the existing fold consumes it — and the tighten-only property is
 * structural rather than restated.
 *
 * The mapping mirrors the gate's own phase 1 decision for decision:
 *  - no opinion → `allow`, contributing nothing;
 *  - `deny` → `deny` in the fold's own words;
 *  - `ask` → `ask`, stamped `decisionReason:"hook"` (and the mandate, when the answer set it) so every consumer that
 *    distinguishes a screening ask from a policy ask reads the same fact one level down;
 *  - `allow`/`ask` with `updatedInput` → the same rewrite, which the fold cascades and re-checks;
 *  - a failed screening (a handler that threw, ran past its budget or broke the site's contract) or a cancel → the
 *    fail-closed deny, because a screening that cannot answer must not pass the call. The installing leg's bus
 *    already reported a crash or a timeout on that leg's error lane.
 *
 * **Idempotency** ({@link combinePolicies}): a layer's `check` may run more than once for a single tool call (the fold
 * scans past an `ask`, a later rewrite re-runs the prefix, an approved edit is re-adjudicated), so a handler with a side
 * effect sees that call more than once on those paths. Its own leg's phase 1 has always had the third of these.
 *
 * Not carried, and not reconstructible in this shape: `additionalContext` (a policy verdict has no channel for
 * model-facing text — the context belongs to the run whose gate ran it), the leg identity and the tracked cwd (the
 * fold's request carries neither, and the installing leg's would name the wrong run), and the phase-1 observer
 * exclusion (a deny raised here is a fold deny: the descendant's deny observer sees it as `source:"policy"`). `env` is
 * the capability face of the environment the chain was installed against, captured at the install site.
 *
 * The chain travels on the minted policy: a descendant's gate raises the installation's `tool.check` on it (a verdict read is
 * not a fold layer). A host re-supplying the entry hands back the policy minted here, as it is.
 */
export declare function createPreToolUseConstraintPolicy(chain: Pick<import("./mods/kernel/index.js").ModBus, "hasHandlers" | "dispatch">, env?: HookEnvCapabilities): ToolPolicy;
/**
 * The park closure's ONE structural seat — what the gate hands the durable park beyond the twelve
 * positional seats. Every member is optional and read by name, so a new thing the park must know
 * becomes a member here, never a fourteenth positional argument (each of the last four seats was
 * appended "at the tail so nothing re-binds", which is the growth this seat ends).
 */
export interface AskCarry {
    /** design/384 slice 2 — {@link ToolGateInput.callSignal}, verbatim: the park closure composes it
     *  with the run signal into the halt×park fence's cut signal. Absent ⇒ the run-level binding alone. */
    readonly signal?: AbortSignal;
    /** The surviving ask's `denialLimitFallback` (the classifier denial-limit counts) run through the
     *  window-less route station ({@link import("./auto-mode.js").unarmedWindow}) when the gate builds the
     *  carry: a park is not a hand-out to a live approver, so nothing on this lane counts down. The
     *  decision's member cannot state a window (its type has no such member), and this is the one place
     *  the `0` a park row shows is written. Persisted on the row so a durable card shows the counts the
     *  synchronous card shows. Absent then means: not a fallback ask. */
    readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
    /** The surviving ask's no-ruling member ({@link import("./tool-policy.js").AskRequest.classifierNoRuling}), for the row's twin. */
    readonly classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
    /** The surviving ask's engine-stamped origin word ({@link import("./ask-origin.js").AskOrigin}), persisted
     *  on the row so a durable card is rendered by the same word the synchronous card is. */
    readonly origin?: import("./ask-origin.js").AskOrigin;
    /** The surviving ask's engine-stamped class ({@link import("./tool-policy.js").AskRequest.askClass}), persisted on the
     *  row so a durable approval seat answers by the class the synchronous one reads. Present exactly when
     *  {@link classes} is. */
    readonly askClass?: import("./ask-origin.js").AskClass;
    /** The whole class set beside it ({@link import("./tool-policy.js").AskRequest.classes}), in `ASK_CLASSES` order. */
    readonly classes?: readonly import("./ask-origin.js").AskClass[];
    /** The stamped mandate ({@link import("./tool-policy.js").AskRequest.mandated}) — the park site's
     *  rule-offer facts read it, so a parked mandated ask offers no rule, like the live card. */
    readonly mandated?: true;
    /** The table word stamped beside it ({@link import("./tool-policy.js").AskRequest.mandate}), for the row's twin. */
    readonly mandate?: import("./ask-origin.js").PersistedRuleMandate;
    /** The deployment's authorship ({@link import("./tool-policy.js").AskRequest.authoredBy}), for the row's twin. */
    readonly authoredBy?: "deployment";
}
/** What a durable row persists of an {@link AskCarry}: the fallback member COPIED (the row never aliases
 *  the gate's object; its window is already `0` — the gate's carry mint is the one writer of that
 *  number), the origin word only when it is a member of the closed set, and the class pair only when it is
 *  well-formed, as a fresh copy (the carry is typed, but the row is the boundary a host reads back through —
 *  the same screen the summary applies to the word). */
export declare function askCarryRowMembers(carry: AskCarry | undefined): {
    denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
    classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
    origin?: import("./ask-origin.js").AskOrigin;
    askClass?: import("./ask-origin.js").AskClass;
    classes?: readonly import("./ask-origin.js").AskClass[];
};
/**
 * The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
 * structural: a hook's `allow` passes no deny, no ask rule and no mandated question; it is one member of the
 * allow layer and clears only the questions an allow rule may clear (a deployment that wants a question put to a
 * person whatever a hook says marks it `authoredBy: "deployment"` or `requiresRealApproval`):
 *
 *  1. **collect** — run the PreToolUse hook, threading any `updatedInput` rewrite into `currentInput`
 *     and collecting `additionalContext`. A hook `deny` short-circuits to a block immediately; a hook
 *     `ask` is remembered (it does not short-circuit — a later policy `deny` outranks it).
 *  2. **adjudicate** — run the tool policy on the FINAL `currentInput` (never the model's stale args),
 *     fold it with any remembered hook-ask via `deny > ask > allow`, and resolve a surviving `ask`
 *     through `onAsk`. The policy ALWAYS runs regardless of the hook's verdict. A policy `allow` may
 *     itself carry an `updatedInput` rewrite (redact/clamp), applied last over any hook rewrite.
 *
 * Because both phases run in this one linear function, the ordering — and the "policy is final"
 * invariant — is enforced by the call stack, not by registration convention. Returns the gate result
 * for the harness `tool_call` hook plus the PreToolUse context to attach to the tool result.
 */
export declare function runToolGate(input: ToolGateInput): Promise<ToolGateResult>;