@sema-agent/core
Version:
Stateless, task-oriented AI agent core
2,070 lines • 164 kB
TypeScript
import { type AskOrigin, type AssertAllKeysHandled } from "./ask-origin.js";
import { type ReadRootGrantScreen } from "./read-root-grant-screen.js";
import { type Settlement } from "./gate-outcome.js";
export { policyCannotAnswer, rowDemandsAPerson } from "./real-approval.js";
import { type DecisionReason } from "./decision-receipt.js";
export { isEngineRefusal } from "./decision-receipt.js";
import { type RemovalGuardSeat } from "../tools/fs/removal-targets.js";
/** A tool call presented to a policy before it executes. */
/** design/388 B6 — the per-call face (see {@link ToolCallRequest.face}). */
export interface ToolCallFace {
/** Every name a tool-name rule may spell this tool by: the name the call was made under first (the model's spelling),
* then the tool's wire name and each other alias that resolves to it on the leg's roster (`rosterToolName`). A rule
* reaches the call iff its tool name is on this list — read it through `ruleNamesOf`, never off `toolName` alone.
* Absent on a face rebuilt from a park row that predates it (the call's own name is then the list). */
names?: readonly string[];
family: import("./tool-face.js").ToolFamily;
pathTarget?: import("./tool-face.js").ToolRosterPathTarget;
capabilityId: string;
contractId: string;
shapeDigest: string;
implementationRevision: string;
/** Which approval card this call renders under (the closed set `TOOL_APPROVAL_CARDS`; `generic` when the tool
* declares none). Minted with the face — a reserved card only for the engine tool that owns it — and copied onto the
* ask, the parked gate and the pending row, so no reader recovers the card from a name. Absent on a face rebuilt
* from a park row (a re-judgment reads no card). */
approvalCard?: import("./tool-face.js").ToolApprovalCard;
}
export interface ToolCallRequest {
/** The tool's wire name — the Runner's gate resolves a call made under an alias to the wire name of the tool it
* dispatches to before any policy runs (`rosterToolName`); the names a rule may use for it are `ruleNamesOf`. */
toolName: string;
args: unknown;
toolCallId: string;
/**
* design/388 B6 (additive) — the mounted tool's FACE as the leg's roster minted it: the declared path target
* (how the target may be confirmed by path), the display family, and the engine-minted identity
* (`capabilityId` / `contractId` / `shapeDigest`) an exemption store keys on. Stamped at the gate entry
* from the live roster, so identity follows the OBJECT the call dispatches to — a caller tool that merely
* took a built-in's name carries its own face, never the built-in's. ABSENT only on a BARE request (a
* policy invoked outside a Runner by a producer that stamped nothing): a PROTECTIVE fence (a deny-list)
* then reads the catalog's declaration for the NAME, tighten-only; a CONTAINMENT fence (a grant admitting a
* write by its path) REFUSES the request, typed (`policy.bare_request`, #626 ②) — stamp the face yourself
* (`toolCallFaceOf(roster, name)`) when driving a policy outside a Runner.
*/
face?: ToolCallFace;
/**
* The LIVE working directory this call's RELATIVE path arguments will actually resolve
* against, read at adjudication time from the same tracked `cwdRef` the fs hand tools resolve with
* (design/64 §16.3: a resident shell's `cd` persists and moves the fs tools' relative base with it).
*
* WHY IT EXISTS: every path guard here (sensitive-path, fs-write gate, session rules,
* transcript integrity) re-resolves the model's spelling itself, and each was handed a STATIC root at
* wiring time. After a `cd` the two sides of a single call disagreed about which file it was: the tool
* wrote `<liveCwd>/pre-commit` while the guard judged `<wiringRoot>/pre-commit` — measured on a host
* lane, `cd .git/hooks` + `Write("pre-commit")` landed a git hook past a guard that names `.git/hooks`.
* Reading THIS field makes the guard's resolution base the same value the tool will use.
*
* Applies to the call's TARGET only. A guard's own configured directories (accept/exempt domains,
* `allowDirs`) are DEPLOYMENT text and keep resolving against the static root —
* a shell `cd` must never relocate an allow-domain.
*
* ABSENT when the run has no tracked cwd (read-only hands, no execution env) or when a policy is
* invoked directly outside a Runner: consumers then fall back to their own configured root, which is
* exactly the pre-RB-108 behavior. Read-only — a policy cannot move the shell by writing it back.
*
* RESIDUAL (recorded honestly — this is a snapshot of a mutable ref, not a frozen binding): the gate
* reads the ref when it adjudicates and the tool reads it again when it executes. No interleaving that
* moves the cwd in between is constructible on the current loop, and it is worth naming WHY, because
* the reason is a property of the tool declarations rather than of this field: the loop adjudicates a
* whole batch before executing it, but only calls classified concurrency-safe ever share a batch, and
* every cwd MOVER (a `cd`-bearing Bash — `cd` is not on the readonly allowlist and any shell operator
* disqualifies — plus the worktree enter/exit tools) and every path-confinable WRITE tool declares
* `effect: "write"`, which derives `executionMode: "sequential"`, which forces a batch of one. A
* deployment tool that declared itself read/parallel while moving the shared cwd would reopen the
* window — at that declaration, not here.
*/
cwd?: string;
/**
* The root this leg's tools resolve a ROOT-relative spelling against (a search tool's relative `path`, a
* pattern with no `path`), stamped beside {@link cwd} at every Runner adjudication seat — the same
* canonical spelling as `cwd` (the hands' own root, the value `cwd` starts at). Present whenever the leg
* mounts file hands, read-only hands included; absent on a hands-less leg and on a bare request. A path
* guard whose deployment supplied no base of its own resolves the call's target against `root` / `cwd`.
* Read-only, like `cwd`.
*/
root?: string;
/** The call's target reading (`readCallTarget`: lexical / canonical / hop views per reading of the path slot), stamped by the Runner on a
* leg whose lanes can hold a path rule; it describes only the `args` object it was taken for (`callTargetReadingOf`). Read-only. */
targetReading?: import("./effective-path-target.js").CallTargetReading;
/**
* An ENGINE-INTERNAL seat, not a fact of the call: the anchors of the policy now judging this request — the leg its
* path rules were installed on (`installPolicyAnchors`). Present only on the copy an installed policy hands its own
* inner `check` (where `createPermissionRulePolicy` reads it; an inner installation's anchors win over an outer one's);
* the request the gate holds, an `AskRequest`, a hook's input and a parked row never carry it. A host neither sets nor
* reads it.
*/
pathRuleAnchors?: import("./permission-rule-model.js").PathRuleBases;
/**
* The CANONICAL directories THIS RUN'S PROMPT ADVERTISES AS WRITABLE — the admitted list of the root
* fence's advertised-writable-directory seat (the memory engine's write home, the deployment's
* scratchpad), in admission order, deduplicated. Stamped by the Runner on every policy adjudication of
* the run, read live at adjudication time (a directory admitted by a later phase is on the list the
* next call carries).
*
* WHY IT EXISTS: the fence and the write gate were judging one fact twice. The prompt tells the model
* "write to it directly with the Write tool" and the fence opens the hole for exactly the directories
* this list names — but a deployment-authored write gate knew nothing of them, so every advertised
* write still asked (a memory entry plus its index = two questions the person was told would not be
* asked). Reading THIS field makes the gate's exemption domain the same list the fence admitted,
* instead of a second copy the deployment maintains by hand.
*
* A containment gate reads it EXACTLY as {@link import("./fs-write-gate-policy.js").FsWriteGatePolicyOptions.exemptDirs}:
* a covered write whose canonical target lands inside one of these directories is allowed and not
* asked. It grants nothing else — a DENY composed alongside (sensitive-path, an org rule) still
* outranks it under the deny > ask > allow fold, and an unresolvable target still fails closed.
*
* ABSENT outside a Runner, or when the run has no hands, or when nothing was advertised. Absence is NO
* exemption, never a wildcard (an empty list reads the same). Read-only — a policy cannot advertise a
* directory by writing one back.
*/
advertisedWritableRoots?: readonly string[];
/**
* The engine data root's spellings — the ONE exempt root every exemptible path row is judged under (the data-root
* exemption, path-guard-rows.ts): resolved once per leg from `RunnerDeps.memoryEngineDir` (absent ⇒ the resolver's own
* chain, `$AGENT_DATA_DIR ?? ~/.ai-agent`) and stamped by the Runner on every policy adjudication of the leg. A path guard
* that judges a write under the exemption reads it here, so the guard and the engine's own write judgment speak about
* the same directory instead of a second copy the deployment keeps by hand.
*
* ABSENT outside a Runner (a policy driven directly by a host): the guard then reads the resolver's own chain, the root
* a Runner with no `memoryEngineDir` would stamp. Read-only, frozen.
*/
exemptRoots?: readonly string[];
/**
* The request's statement that nobody can be asked about THIS call — the word of a run's own declaration
* (`TaskSpec.approvalReach`, the closed set `APPROVAL_REACH`), stated per request. Present (`"none"`) on every request the
* engine's synthetic probes hand a policy layer (the "would this call be refused outright?" judgments: the visibility
* probes of a file the model did not ask to read, the background memory consolidation's write probes); absent on a call
* the gate adjudicates, and on a bare request. A policy that would put the call to a person — an approval seat it awaits,
* a hook it invokes, a card or a message it sends — answers at once instead: the engine's own factories read this member
* before reaching a seat and answer {@link noApproverSeatDeny}; a deployment closure does the same. The signal handed
* beside such a request is LIVE (the judge's wait for that layer): an aborted one means only that the task ended.
* Read-only.
*/
approvalReach?: import("./wiring-manifest.js").ApprovalReach;
/**
* design/80 D-E-core: read-only per-run budget snapshot so a STATELESS policy can self-limit against
* state that survives suspend/resume (a fresh in-memory counter would reset every leg — the supervisor
* escalation-budget gap). Built by core at prepare time from the DURABLE resource ledger
* ({@link import("./checkpoint-store.js").ResourceLedger}) + the durable suspend chain; the policy CANNOT
* widen it (no write-back). Present on every gated `check()` once the runner builds the gate; absent only
* when a policy is invoked directly (outside a Runner). A NORMAL-ask escalation policy reads it; a SAFETY
* ask (egress / irreversible) ignores it and ALWAYS asks (invariant #2 stays structural — core mints
* `irreversible_ask` for safety tools regardless of any budget).
*
* ⚖️ SCOPE (A-005.13, ruled 2026-08-09): this snapshot is PER-RUN — it is the ledger of the run whose
* gate is asking, NEVER the ledger of the run that authored the policy. A policy inherited down the
* delegation chain (`InheritedGate.parentConstraints`) therefore self-limits against each CHILD's own
* fresh ledger; the ancestor's counts do not travel (declared design — see the chain field's A-005.13
* note for the reasoning and the closure-state alternative).
*/
budget?: {
/**
* Durable resource $ this run may STILL spend (micro-USD); `undefined` = no $ ceiling configured. Frozen
* total, prior-wins → it monotonically SHRINKS across legs and is NEVER re-granted by a resume (a
* deny-resume continues with the same frozen remaining). **NEVER falsy-test** (`0` = exhausted, NOT
* "unlimited" — only `undefined` is unlimited).
*/
resourceRemainingMicroUsd?: number;
/** Durable cumulative resource $ already spent across all prior legs (micro-USD); accumulates, never refunded. */
resourceSpentMicroUsd: number;
/** How many times this run has already suspended (the durable suspend chain). Lets the policy tighten as
* the chain grows — the precise complement to the unconditional `suspendLoopCap` backstop. */
suspendCount: number;
};
}
/** Where a decision came from, for audit (design/37). `"sandbox"` (F-012 L2) marks an allow the
* sandbox-admission leg resolved: the deployment declared an isolated execution env, every surviving
* ask on the call was engine-classified sandbox-local, and the call crosses no declared boundary —
* disclosed durably as `permission.sandbox_admitted`.
*
* design/182 §7 adds the two ORG-layer words, both TIGHTENING-only:
* - `"org_rule"` — an organization policy rule spoke (a deny, or an ask that no configuration can
* dismiss). Distinct from `"rule"` (a deployment `ToolPolicy` rule) because the AUTHORITY differs:
* an org rule comes from the org's server-published snapshot, not from this deployment's policy.
* - `"org_unavailable"` — an org-governed deployment could not adjudicate against a snapshot, so the
* whole decision boundary failed closed (see `ORG_UNAVAILABLE_DECISION_REASON`, the single
* spelling this word is minted from). */
/** `read_only` (#619): the shell tool's command was PROVABLY read-only by the engine's transcribed upstream
* tables (`readOnlyShellVerdict`) and the allow layer let it run without a question — after every
* deny/ask lane and the person's own allow rules, never over a mandated ask. */
/**
* Stamp {@link ToolCallRequest.advertisedWritableRoots} from the run's advertised-writable-directory seat
* — the ONE place the seat becomes a request field, so every adjudication station stamps it alike (the
* `callFaceSeat` shape).
*
* The list is COPIED AND FROZEN here, and that is load-bearing rather than tidy. The seat's `roots` getter
* hands back its own admission ledger, and {@link combinePolicies} walks every layer with ONE request
* object: a layer handed the live array could append to the very domain the write gate exempts, widening
* it for that call and for every later one. `readonly` is a compile-time word; this is the runtime one.
* (What a frozen array does not close is a layer REPLACING the whole field on the shared request — the
* pre-existing shape that `cwd`, `budget` and `face` live with; it belongs to the fold, not to this seat.)
*/
export declare function advertisedWritableRootsSeat(seat: {
readonly roots: readonly string[];
} | undefined): {
advertisedWritableRoots?: readonly string[];
};
/** Stamp {@link ToolCallRequest.exemptRoots} from the leg's one resolution of the data root — the ONE place that value
* becomes a request field, beside {@link advertisedWritableRootsSeat} at every adjudication station. */
export declare function exemptRootsSeat(exemptRoots: readonly string[]): {
exemptRoots: readonly string[];
};
/** Declared beside the receipt's list (decision-receipt.ts) — a new member added to either side is a compile error rather
* than a silent drift between the type and the screen; re-exported here, the module every consumer of the decision reads. */
export type { DecisionReason };
/**
* The engine's ATTESTATION record — the one carrier of what an engine station knows about a decision and a policy cannot
* state: WHO settled the call's wait and WHICH question that answered (the settlement, the ask's origin word and class set,
* the digest of the arguments the answer licenses), and the facts the classifier stations, the in-fold unresolvable arms
* and the policy fold record. It rides the decision as its `attestation` member and is read through {@link attestationOf}
* alone; the decision object's identity carries nothing.
*
* Minted only by {@link mintAttestation} and combined only by {@link mergeAttestation}: both freeze the record and enter it
* in a module-private brand set, so a value a caller composed — a literal, a copy, a structured clone — is not a record, and
* the receipt refuses a decision carrying one. A record is bound to the call it was minted for (`toolCallId` + `toolName`):
* handed back for another call it reads as absent. A spread of a decision keeps its record, and the record still says only
* what the engine minted — a refusal record on an allow is a contradiction the gate's exit refuses, and a resolution's
* answer binds the arguments it licensed (`inputDigest`).
*/
export interface EngineAttestation {
readonly toolCallId: string;
readonly toolName: string;
/** What ended the call's wait. The answer group — `settlement`, `origin`, `classes`, `inputDigest` — travels as one. */
readonly settlement?: Settlement;
/** The settled ask's origin word. */
readonly origin?: AskOrigin;
/** The settled ask's class set (`ASK_CLASSES` order), when the minting station held one. */
readonly classes?: readonly import("./ask-origin.js").AskClass[];
/** `boundInputHashOf` of the arguments the settlement licenses: an approver's edit, the snapshot shown, or the asked args. */
readonly inputDigest?: string;
/** The FORM of a classifier round that did not rule (a classifier deny's cause). */
readonly classifierCause?: import("./gate-outcome.js").ClassifierDenyCause;
/** WHICH model answered a classification, on either arm. */
readonly classifierRound?: import("./auto-mode.js").AutoModeClassifierRound;
/** Why an in-fold deny refused an ask nobody could resolve. */
readonly unresolvableCause?: import("./ask-unresolvable-notice.js").AskUnresolvableCause;
/** The classes of the asks a policy fold collapsed — the proof behind a decision's `foldedAskClasses`. */
readonly foldedClasses?: readonly import("./ask-origin.js").AskClass[];
/** Where a path deny row refused the call — the row, the view of the target it met, the face the call is on. The gate's
* exit mints the refusal's card from it; the judge that found the row is its only writer. */
readonly pathDenial?: import("../tools/fs/safety.js").PathDenialEvidence;
}
/** The members a station attests — everything but the call binding. */
export type AttestationMembers = Omit<EngineAttestation, "toolCallId" | "toolName">;
/** Mint a record for `call`. Engine stations only: not re-exported from `src/index.ts`. The values are DETACHED snapshots, so a
* caller that later mutates the settlement object it was handed cannot rewrite what the engine attests. */
export declare function mintAttestation(call: {
toolCallId: string;
toolName: string;
}, members: AttestationMembers): EngineAttestation;
/** Is `v` a record the engine minted (the brand alone — no binding)? The receipt's test. */
export declare function isEngineAttestation(v: unknown): v is EngineAttestation;
/** THE read: the record `d` carries FOR `call` — a branded record bound to that call, else `undefined` (a record bound to
* another call is a replayed one and reads as absent). Read off the own data member, never through an accessor. */
export declare function attestationOf(d: unknown, call: {
toolCallId: string;
toolName: string;
}): EngineAttestation | undefined;
/**
* Combine two records for `call`, member GROUP by group: the later record's group stands where it has one, the earlier's
* fills a group the later lacks. A record not bound to `call` contributes nothing. `answer` scope is the veto's merge: the
* earlier record contributes ONLY its answer, and only when the later carries none — a person's yes rides a later layer's
* deny (invariant I3) while the refusing station's own facts stay its own.
*/
export declare function mergeAttestation(call: {
toolCallId: string;
toolName: string;
}, earlier: EngineAttestation | undefined, later: EngineAttestation | undefined, scope?: "all" | "answer"): EngineAttestation | undefined;
/** `d` with `members` attested for `call`, merged over the record it already carries for that call (the new members' groups
* win). Always a fresh object — the receipt hands out frozen ones. */
export declare function attest<T extends object>(d: T, call: {
toolCallId: string;
toolName: string;
}, members: AttestationMembers): T;
/** `d` carrying `record` (a fresh object), or `d` itself when there is no record to put on it. */
export declare function withAttestation<T extends object>(d: T, record: EngineAttestation | undefined): T;
/** The `no_approver`-attested deny an engine policy answers on a request that states nobody can be asked about it
* ({@link ToolCallRequest.approvalReach}) instead of reaching a seat — read by a probe as "would ask an approver". */
export declare function noApproverSeatDeny(req: {
toolCallId: string;
toolName: string;
}, message: string, origin: AskOrigin): PermissionResult;
/** The `task_aborted`-attested deny an engine policy answers on an aborted signal instead of reaching a seat. */
export declare function abortedSeatDeny(req: {
toolCallId: string;
toolName: string;
}, message: string, origin: AskOrigin): PermissionResult;
/** THE question every engine factory asks before it would reach a person (an approver it awaits, a hook it invokes): may the
* seat be reached? Not when the task ended (an aborted signal ⇒ {@link abortedSeatDeny}, read first) nor when the request
* states that nobody can be asked about the call ({@link ToolCallRequest.approvalReach} ⇒ {@link noApproverSeatDeny});
* `undefined` ⇒ it may. `say` is the factory's own sentence for each. */
export declare function unaskedSeatAnswer(req: Pick<ToolCallRequest, "toolCallId" | "toolName" | "approvalReach">, signal: AbortSignal | undefined, say: {
aborted: string;
unreachable: string;
}, origin: AskOrigin): PermissionResult | undefined;
/**
* design/252 G-7 — how long an approver-attribution identifier may be.
*
* Sized for the identifiers approval channels actually carry (a login, an email, an opaque account id,
* a queue name) with room to spare, and bounded at all because this value lands on an operator-plane
* frame that a deployment may persist: an unbounded field on a record channel is its own denial of a
* readable record. An over-long value is REFUSED, never truncated — a cut identifier names a different
* party, or nobody, and either is worse than the honest refusal.
*/
export declare const APPROVER_ATTRIBUTION_MAX_CHARS = 256;
/**
* design/252 G-7 — screen a deployment-supplied approver identifier before it becomes an observation.
*
* THE POSTURE, stated so it is not mistaken for something stronger: core does not authenticate this
* value, does not compare it to anything, and never reads it back to decide anything. It is a
* TRANSCRIPTION of what the approval channel said about its own settlement — the channel (a server's
* approval card, an HMAC-verified callback, an operator console) is where identity is established, and
* a library that holds no identity surface cannot second-guess it. What core owns is that the value is
* a value: a string, bounded, free of control bytes, or else loudly refused.
*
* Returns `{}` for "nothing supplied" (absent, and an empty string — an id of no characters is the
* absence of an id, the same truthiness fold the deny-note seat uses), `{ approver }` for a value to
* carry, or `{ defect }` with the sentence a caller puts in its own refusal.
*/
export declare function screenApproverAttribution(v: unknown): {
approver?: string;
defect?: string;
};
/** An integrity guard an ask stands on ({@link PermissionResult}'s `guards`). */
export type AskGuard = "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "artifact_shared_read";
/**
* A three-state permission decision for a tool call (design/37). Upgrades the old two-state
* `{allow|deny}`:
* - `allow` may carry `updatedInput` — REWRITTEN args (redact a secret, clamp a path/value) that
* REPLACE the model's args before execution (re-validated against the tool schema first).
* `deny` may ALSO carry `updatedInput` (1.256 复审 MED-2): it never executes anything, but a
* combined-policy deny reports the ACCUMULATED pre-deny rewrite (policy A redacted → policy B
* denied) so a deny OBSERVER (`classic.PermissionDenied`) sees the final post-rewrite args, not the
* stale un-redacted ones.
* - `ask` requests human confirmation; with no approver wired it resolves deterministically to deny
* (headless auto-deny, see `RunnerDeps.onAsk`). `requiresRealApproval: true` (RB-203
* confirmed P1) additionally refuses to let a BLANKET `onAsk: "allow"` circumvention clear it — that
* configuration is "no approver looked at this at all", indistinguishable from headless, for a rule
* whose whole point is that a blanket policy setting must not be the thing that clears it (see
* {@link resolveAsk}). A live auto-mode classifier verdict (tried earlier in the gate, if configured)
* or a genuine approver CALLBACK (actual judgment, even if it always says yes) still can — unless the
* callback itself reports its allow as the deployment's POLICY (`settledBy: "policy"`): that is the same
* "nobody judged this" a blanket seat is, and it is refused the same way.
* - `deny` blocks the call.
* Human-readable text is `message`; read it via {@link decisionText}.
*
* What ended a wait is not a policy's to state: it rides only the `attestation` member, an engine-minted
* record ({@link EngineAttestation}) the receipt refuses in any other form, and the gate's exit mints the one
* settlement record ({@link import("./gate-outcome.js").GateOutcome}) from it; a policy that puts a `settledBy`
* or `approver` member on its verdict is putting an unread property on it.
* RB-479-B① (ruled 2026-08-02): the legacy `reason` field is REMOVED — one name for one thing. The ask arm's one-word
* `guard` is retired the same way, for `guards`.
*
* The TYPE carries that retirement only where a decision is a literal in an annotated position; an inferred `check`
* return compiles green with a stray `reason` or `guard`, so the enforcement is at RUNTIME — every point that consumes a
* decision refuses one that carries a retired member ({@link RETIRED_DECISION_MEMBERS}), fail-closed
* ({@link refuseOutOfContractDecision}).
*/
export type PermissionResult = {
action: "allow";
updatedInput?: unknown;
message?: string;
decisionReason?: DecisionReason;
attestation?: EngineAttestation;
} | {
action: "ask";
updatedInput?: unknown;
message?: string;
decisionReason?: DecisionReason;
requiresRealApproval?: boolean;
/** This ask is the classifier DENIAL-LIMIT fallback — the block that reached a
* bound (CC 2.1.250 `FO`, count-then-judge) handed to a person instead of being denied.
* ENGINE-STAMPED beside `requiresRealApproval: true` and `decisionReason: "classifier"` at the
* classifier block sites (the gate's own and the inherited-lane arms); carries the COUNTS ONLY —
* the auto-deny window is a fact of the ask's ROUTE, so this type has no member to hold one
* ({@link import("./auto-mode.js").UnarmedDenialLimitFallback}) and the window appears first on
* the {@link AskRequest} the mint station builds, iff that request goes to a live function
* approver. Consumers: the gate's classifier step refuses to re-judge an ask carrying it (the
* classifier already spoke; rules and classifier stand BEFORE this ask, never after), and the ask
* resolver arms its deadline from the REQUEST's `autoDenyAfterMs`. Carried onto the approval
* request through one of the two route stations. A policy that self-declares it only ever opts
* its own ask OUT of classifier resolution and INTO the fallback's bounded lane (tightening — the
* same safe direction as `matchedAskRule`); it cannot state a window of its own, because the
* route it would be a window for has not been chosen yet. */
denialLimitFallback?: import("./auto-mode.js").UnarmedDenialLimitFallback;
/** WHY a classifier station handed this call to a person: the classification did not rule (`error` / `timeout` — it could
* not run; `parse_error` — no verdict). Engine-stamped with `requiresRealApproval`; class `classifier_no_ruling`. */
classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
/** The allow layer's DISCLOSURE: the members that MATCHED this call and could not clear the ask (it is a class
* no allow rule clears — a mandated, governance, hook, ask-rule or mode question — or the call reached the layer
* marked), one record per member ({@link AllowRuleShadow}). ENGINE-STAMPED by the allow layer, overwriting a
* policy's own value; a DISPLAY member: the approval card and the park row render it, nothing reads it to
* decide, and the classifier never sees it (it is not part of `message`). Absent ⇒ no member matched. */
allowRuleShadowed?: readonly import("./allow-rule-shadow.js").AllowRuleShadow[];
/** backlog #239 disclosure (additive): WHY the reversibility probe did not clear this call, in the
* probe's own words. ENGINE-STAMPED at the irreversibility tighten (which constructs this decision
* wholesale), already neutralized + capped there; a `"maybe"` tier whose probe supplied no cause,
* and the `"always"` tier (which runs no probe), leave it absent. Same DISPLAY-ONLY posture as
* {@link allowRuleShadowed}: a policy that self-declares it can only put its own prose on its
* own card — nothing reads this member to decide anything, so it is not an authority channel. */
probeReason?: string;
/** backlog #239 disclosure (additive): the STRUCTURED cause, for a probe whose verdict this engine
* understands (see {@link import("./checkpoint-store.js").ProbeCause}). Engine-stamped and
* validated at the irreversibility tighten; same display-only posture as the prose sibling. */
probeCause?: import("./checkpoint-store.js").ProbeCause;
/** The reversibility probe declared this demotion STRUCTURAL — see
* {@link import("./types.js").ReversibilityVerdict.mandated}. Unlike its two `probe*` neighbours
* this is NOT display metadata: it joins the #144 mandate family, so the persisted-rule lane
* discloses a matching allow rule as shadowed instead of clearing the ask, and the card's
* "stop asking me this" offer is withheld. ENGINE-STAMPED at the irreversibility tighten, which
* constructs this decision wholesale — a policy that self-declares it is overwritten there, and
* in the one direction the stamp cannot reach (a caller composing its own ask) the claim is
* tightening-only: it can make an ask outlive a rule, never turn one into an allow. */
probeMandated?: true;
/** This ask is MANDATED — no persisted allow rule and no STANDING ANSWER (a release remembered from an
* earlier call, at any scope, a session included) may retire it; only an answer produced for THIS call
* clears it — a person, a hook, a deployment's blanket `onAsk: "allow"` seat, or the live deciders the
* ask's class row admits ({@link import("./ask-origin.js").MODE_ARBITRATION}); {@link requiresRealApproval}
* is the stronger demand that also refuses the blanket seat. ENGINE-STAMPED at the gate's single origin stamp from the ONE derivation
* the approval card's rule-offer word also reads ({@link import("./ask-origin.js").ruleClearanceOf},
* which lists the doors). TIGHTENING-ONLY: a layer below the mint may not clear one a layer above
* set, and a policy self-declaring it only opts its own ask OUT of automatic answering. Carried
* onto the approval request as {@link AskRequest.mandated}. */
mandated?: true;
/** The TABLE WORD the {@link mandated} bit stands on — see {@link AskRequest.mandate}. ENGINE-STAMPED at
* the same origin stamp as the bit; a policy's self-declared value is dropped there. */
mandate?: import("./ask-origin.js").PersistedRuleMandate;
/** The probe named a directory that would clear this ask
* ({@link import("./types.js").ReversibilityVerdict.readRootCandidate}). Display metadata like the two
* `probe*` above; ENGINE-STAMPED and VALIDATED at the tighten, and deliberately NOT merged onto an ask
* another layer raised, which a grant leaves standing. */
probeReadRootCandidate?: import("./types.js").ReadRootGrantCandidate;
/** An EXPLICIT `ask` permission rule matched this call (design/127 DSL — the CC `alwaysAskRules` shape); carries the
* matched rule's text. Present ⇔ a rule someone WROTE says "ask about this", never for a `defaultAction:"ask"` fallback
* (an unmatched call is default-closed posture, not a per-call instruction — CC draws the same line: only a matched ask
* rule diverts the auto-mode lane, a mode-default ask stays classifier-eligible). Consumer: the gate's auto-mode
* classifier step refuses to resolve an ask carrying it — a person's standing "ask me each time" is not classifier
* hesitation, so a classifier verdict must not be the thing that clears it. A policy self-declaring the member only
* ever OPTS ITS OWN ask OUT of classifier auto-resolution (tightening — the false-claim direction is safe, unlike the
* engine-stamped `"hook"` word whose risk was laundering provenance AWAY). {@link combinePolicies} carries it onto the
* surviving ask when ANY folded-away concurrent ask bore it (monotone, tighten-only); it is consumed inside the gate
* and NOT copied onto the park/approval-card request — but it is the ONE input of the `ask_rule` origin word they DO
* carry ({@link origin}): a minter raising an ask for a written rule (a person's settings rule, an operator's must-ask
* list) that leaves it unset gets `policy` there, indistinguishable from hesitation. */
matchedAskRule?: string;
/** design/252 G-2 (additive): the RULE-PROVENANCE evidence behind this ask — see
* {@link AskRuleEvidence}. ENGINE-STAMPED inside the gate, once, after the org layer and the
* persisted-rule lane have both spoken; a policy that self-declares it is overwritten there
* (the member is a record of what the ENGINE's own governance layers did, so a layer's claim
* about itself is not evidence). Carried onto the approval request by the gate's own ask mint
* site. Display/reconciliation metadata, never adjudication input. */
ruleEvidence?: AskRuleEvidence;
/** design/375 §5.2②: the per-segment coverage table behind this surviving ask — which segments
* of a compound an eligible persisted rule already admits, read off the SAME store read that
* adjudicated the call (snapshot = adjudication snapshot). ENGINE-STAMPED at the gate's one
* stamp point whenever the persisted-rule lane reported one (overwriting a policy's claim);
* consumed only by the offer factory, which puts on the batch offer exactly the segments this
* table calls uncovered. Absent ⇒ all segments read as uncovered (over-offer, the safe way).
* Same display-only posture as {@link allowRuleShadowed}: never adjudication input, and a
* value nobody stamps over can only shrink or annotate the card's ADVISORY offers — every
* offered rule still passes the validator at redemption. */
segmentCoverage?: readonly import("./permission-rule-model.js").SegmentCoverage[];
/** WHICH AUTHORITY raised this ask — one word from the closed {@link import("./ask-origin.js").AskOrigin}
* set (org rule / org unavailable / hook / explicit ask rule / the classifier's denial-limit
* fallback / the reserved question tool / a marked call / plain policy). ENGINE-STAMPED inside the
* gate at the same single stamp point as `ruleEvidence`, derived from what the gate's own layers
* did ({@link import("./ask-origin.js").askOriginOf}) — a policy that self-declares it is overwritten
* there, so the member is never a channel a policy can loosen through. A DISCLOSURE word: the lanes
* read the ask's class set ({@link import("./ask-origin.js").classifyAsk}), an approval surface may
* render the card by it. Optional on the TYPE only because a decision is caller-composable; every ask
* the gate hands to its resolver carries it. */
origin?: import("./ask-origin.js").AskOrigin;
/** The ask's CLASS — the first member of {@link classes} (see {@link AskRequest.askClass}). ENGINE-STAMPED at the
* same single stamp as {@link origin}, from the class set the gate's lanes read; a policy that self-declares it is
* overwritten there, and no lane reads it back (they read the classification itself). */
askClass?: import("./ask-origin.js").AskClass;
/** The ask's whole class set, in {@link import("./ask-origin.js").ASK_CLASSES} order (see
* {@link AskRequest.classes}). Stamped beside {@link askClass}, under the same rule. */
classes?: readonly import("./ask-origin.js").AskClass[];
/** WHICH integrity guards stand behind this ask — a removal of a protected location, an unverifiable recursive delete,
* shell text the engine cannot read (or an environment that did not answer the read station), the session-transcript
* write. A set: one ask may stand on several, and each keeps its class. ENGINE-STAMPED by the guard policies and the
* gate fold; a fold-internal fact the class derivation reads ({@link import("./ask-origin.js").classifyAsk}) so the
* guards, which mint the same `requiresRealApproval` ask, are told apart by a fact and not by their prose. Never
* copied onto the approval request or the park row. Replaces the one-word `guard`, which a decision no longer carries:
* a decision that still does is refused by name ({@link RETIRED_DECISION_MEMBERS}). */
guards?: readonly AskGuard[];
/** The call could not be READ against a rule that may reach it — a content-form deny row, a content-form
* ask row, a persisted-rule row (the lexer's word), or rows the shell read station's boundary stands on that
* could not be read (`persisted_rows`). ENGINE-STAMPED at the four unreadable mint sites;
* the class derivation reads it as `rule_unreadable` (no mode releases it, a person may clear it, no
* automatic lane may). It replaces the former practice of stamping `matchedAskRule` on an unreadable
* read: a rule that COULD NOT be matched is not a rule that matched. Fold-internal; not on the request. */
ruleUnreadable?: "content_deny" | "content_ask" | "persisted_call" | "persisted_rows";
/** The ask was authored by the DEPLOYMENT's own governance (an operator's must-ask list, a managed command
* policy's default ask) — a fact a host's policy states on its own ask so the class derivation reads it
* as `operator_forced`: a permission mode releases nothing an operator mandated (`dontAsk` refuses it like
* every ask). Public: a host composing such a policy sets it; {@link combinePolicies} carries it onto the
* surviving ask (tighten-only). Not on the approval request — the card shows `origin` / `mandate`. */
authoredBy?: "deployment";
/** The classes of the OTHER asks a policy fold collapsed into this one
* ({@link import("./ask-origin.js").decisionAskClassesOf} per contributing ask, unioned), so a member the
* fold does not copy — a guard mark, an unreadable-rule fact, a bare real-approval bit — is not lost behind
* the ask that won the slot. ENGINE-WRITTEN by {@link combinePolicies}, which attests the list it wrote
* ({@link EngineAttestation.foldedClasses}); the receipt refuses the member unless the decision's record carries that very
* list (a policy's own, an edited list are refused). Fold-internal, never on the request. */
foldedAskClasses?: readonly import("./ask-origin.js").AskClass[];
attestation?: EngineAttestation;
} | {
action: "deny";
updatedInput?: unknown;
message?: string;
decisionReason?: DecisionReason;
attestation?: EngineAttestation;
};
/**
* design/252 G-2 — WHY a piece of rule-provenance evidence is not on an ask.
*
* The vocabulary exists because a bare `undefined` reads the same for facts that are opposite: "no
* governance layer is wired here, so there is nothing to name" and "the layer ran and we could not
* read what it said" are not the same audit answer, and collapsing them is how an evidence chain
* comes to be reconstructed as "nothing governed this call". Every member of {@link AskRuleEvidence}
* therefore ships as a value OR a named absence, never as silence.
*
* - `"not_wired"` — the lane does not exist on this leg (an ungoverned deployment, no rule store).
* The field cannot apply; nothing was lost.
* - `"not_adjudicated"` — the lane exists but this call never reached it (an exempt tool, a call
* the rule grammar cannot describe). Applicable in principle, skipped in fact.
* - `"unavailable"` — the lane was consulted and could not read its source. The evidence is LOST,
* not absent, and this is the one member of the vocabulary that means an auditor should treat the
* chain as broken rather than empty.
* - `"no_match"` — consulted, readable, and nothing spoke for this call. A real negative answer.
* - `"not_reported"` — the supplying seam answered without the identity (a foreign overlay/store
* implementation, or one written before the identity was projected). Evidence lost at the seam.
*/
export type AskEvidenceAbsence = "not_wired" | "not_adjudicated" | "unavailable" | "no_match" | "not_reported";
/** The closed set above, for runtime domain checks at the seams that accept a caller-supplied value. */
export declare const ASK_EVIDENCE_ABSENCE_VALUES: readonly AskEvidenceAbsence[];
/**
* design/252 G-2 — the machine-reconcilable provenance of the governance decision behind ONE ask.
*
* WHAT THIS IS: a PROJECTION of identity keys the engine's governance layers already hold — the org
* snapshot's `revision`, the personal rule's add `dot`s — onto the surface a consumer can actually
* read. It performs no new judgment and changes no verdict; removing it would leave every decision
* byte-identical. The prose channels ({@link PermissionResult}'s ask `message`,
* {@link AskRequest.allowRuleShadowed}) say the same things to a PERSON; those are sanitized,
* capped, display-shaped strings, and reconciling a decision against a published policy revision by
* regexing them is not an audit trail. This member is the machine's copy.
*
* WHAT THIS IS NOT: an authority channel. Nothing in the engine reads it back to decide anything, and
* a host that ignores it entirely is governed exactly as before.
*
* ABSENCE DISCIPLINE (the reason each member has a `…Absent` twin): see {@link AskEvidenceAbsence}.
* Exactly one of each pair is present — a member and its absence reason are never both set, and never
* both missing, on evidence the engine stamped.
*/
export interface AskRuleEvidence {
/** The org snapshot `revision` this call was adjudicated against — the published-policy version an
* auditor reconciles the decision against. Absent ⇒ {@link orgRevisionAbsent} names why. */
readonly orgRevision?: number;
/** Present iff {@link orgRevision} is not. `"unavailable"` here is the load-bearing one: the org
* layer spoke, its answer was "this deployment cannot see the organization's rules", and the ask
* in hand is the fail-closed tighten that followed — not an ask any published rule asked for. */
readonly orgRevisionAbsent?: AskEvidenceAbsence;
/** The org rule that spoke for this call, verbatim as published (org rules are administrator
* authored and are relayed unmodified — the prose channel's copy is the same text). Absent ⇒
* {@link orgRuleAbsent} names why. */
readonly orgRule?: string;
/** Present iff {@link orgRule} is not. */
readonly orgRuleAbsent?: AskEvidenceAbsence;
/** The add dots of the PERSONAL allow rule that matched this call but did not clear the ask (the
* #144 shadowed arm) — the deduplicated UNION across the covering set when the conjunction arm
* matched, so the array does not belong to one rule on those asks. The dots are the rule's durable
* identity — unlike
* {@link AskRequest.allowRuleShadowed}, which is a sanitized, length-capped DISPLAY value and
* deliberately not an identity channel. A rule is a set of adds (concurrent approvals on one text
* each redeem their own dot), so this is an array by construction: render/reconcile the entries as
* data, never re-derive one scalar id by joining them. Absent ⇒ {@link personalRuleDotsAbsent}. */
readonly personalRuleDots?: readonly import("./permission-rule-model.js").RuleDot[];
/** Present iff {@link personalRuleDots} is not. */
readonly personalRuleDotsAbsent?: AskEvidenceAbsence;
}
/** The human/model-readable text of a decision. */
export declare function decisionText(d: PermissionResult): string | undefined;
/**
* Gates tool calls before they execute (the production safety layer for "processing/changing data").
*
* `check` runs before each tool call. Return `{ action: "deny", message }` to block it — the model
* receives an error tool result with the reason and can continue or report blocked. Return
* `{ action: "allow" }` to permit it.
*
* `check` may be async, which is also how **human-in-the-loop approval** works: a deployment can
* hold the promise open until an operator approves/denies. `signal` fires when the task aborts
* (timeout / max turns / cancel) or the asking turn is interrupted (design/384), and on a synthetic probe when the judge
* stops waiting for this layer — honor it to release a pending approval (F4; the Runner also races `check` against it). An
* already-aborted one: see `check`. A request that states nobody can be asked about it: {@link ToolCallRequest.approvalReach}.
*/
export interface ToolPolicy {
/** @contract policy.check.aborted_signal_reaches_nobody — handed an already-aborted signal, `check` reaches no person and
* no external approval (it answers at once, or with a `task_aborted` deny). The engine's probes do not rely on it: they hand a
* live signal and state on the request that nobody can be asked (`approvalReach: "none"`, answered with `noApproverSeatDeny`). */
check(req: ToolCallRequest, signal?: AbortSignal): PermissionResult | Promise<PermissionResult>;
/**
* The policy's CONSTRAINT PROJECTION — its deterministic DENY-verdict half expressed as pure data
* (F-012 L1), so a delegation chain / durable checkpoint can freeze what this policy refuses without
* carrying the live closure. Optional: a policy without one is OPAQUE to the projection plane (its
* live closure keeps the resume re-supply contract it has today). Built-in factories in this module
* attach one where the deny half IS data (name allow/deny lists, shell command-name lists);
* a custom closure policy simply opts out.
*
* The projection is a SUBSET contract, never a replacement: executing it yields every deny the
* data half implies, and says nothing about the policy's live remainder (asks, approvals,
* rewrites). `requiresLiveRemainder` states explicitly whether such a remainder exists — "has a
* projection" ≠ "fully projectable", and consumers deciding whether a frozen projection COMPLETELY
* describes the policy must read that bit, not the field's presence.
*/
readonly projection?: ToolPolicyProjection;
/** The policy's PATH rules as a function of the ANCHORS of the leg they are installed on (`createPermissionRulePolicy`
* sets it; present exactly when the policy judges a path target): given the leg's root and home, every path rule
* bound there — the face-keyed deny rows the prepared leg joins to its read and write tables (so the shell's reads and
* writes and the traversals meet the rule, not only the tool it names), and the rows its own `check` judges at the
* gate. A policy the Runner already installed answers with its own installation's rows whatever it is given. Carried
* through {@link combinePolicies}; read with {@link toolPolicyPathRules}. Its presence also tells the Runner to read
* each call's target ({@link ToolCallRequest.targetReading}). */
readonly pathRules?: (anchors: import("./permission-rule-model.js").PathRuleBases) => readonly import("./path-rule-binding.js").BoundPathRule[];
/** The guarded WRITE rows this policy refuses — rows of the one path table (path-guard-rows.ts), stated as data. The
* engine judges them for a writer the policy cannot see as a call (the shell's file commands) by its ONE write judgment:
* the leg's views of the target (spelling, real target, symlink chain) under the leg's one exempt root
* ({@link ToolCallRequest.exemptRoots}) beside the always-on write-protection table. A target one of them covers is not
* placed, so no permission mode answers the command and its ask reaches a person — it is not refused. The rows of ONE
* judgment: a row placed below a declared root is placed where that root resolves in `env` (resolved per call). A PURE
* statement: no side effect, no verdict; a call that throws leaves the target unplaced. Carried through
* {@link combinePolicies} (the members' rows, in order); read with {@link toolPolicyWriteGuardRows}. */
readonly writeGuardRows?: (at: {
readonly env?: import("../internal/harness-types.js").ExecutionEnv;
readonly signal?: AbortSignal;
}) => Promise<readonly import("./path-guard-rows.js").PathGuardRow[]>;
/** The raw tool-name lists a name-keyed policy was built from — the prepare-time policy-name audit's feed (a retired
* name refuses the run, an unmounted one is advised). Carried through {@link combinePolicies} (the union of the
* members' groups); read with {@link toolPolicyNameSets}. */
readonly nameSets?: readonly ToolPolicyNameSets[];
}
/**
* The members a policy carries beside `check` — one table for every seat that carries them: {@link wrapToolPolicy} reads
* each by name off the wrapped policy, {@link combinePolicies} folds each. The fence makes a new {@link ToolPolicy} member
* a compile error until the table names it.
*/
declare const TOOL_POLICY_CARRIED_MEMBERS: readonly ["projection", "pathRules", "writeGuardRows", "nameSets"];
type ToolPolicyCarriedMember = (typeof TOOL_POLICY_CARRIED_MEMBERS)[number];
export type ToolPolicyCarriesEveryMember = AssertAllKeysHandled<Exclude<Exclude<keyof ToolPolicy, "check">, ToolPolicyCarriedMember>>;
/** The carried members that are set on `p`, each read by name — an accessor or a non-enumerable member included. A
* retired member `p` still holds would not be carried: it is refused by name instead (`config.deps_retired_key`). */
export declare function carriedMembersOf(p: {
readonly [K in ToolPolicyCarriedMember]?: ToolPolicy[K];
}): {
-readonly [K in ToolPolicyCarriedMember]?: ToolPolicy[K];
};
/**
* The ONE way to wrap a policy: the wrapper decides through `check`; every member {@link ToolPolicy} declares beside it
* (projection, name sets, path deny rows, guarded write rows — the carried-member table) rides as `inner` holds it, read
* by name, so a class instance's accessor or a non-enumerable member rides too — a `{ check }` literal drops them, and
* engine stations the wrapper never sees read each one. The carried members describe what `inner` refuses, so the wrapper
* must refuse at least that (an observer, a tightener); one that releases what `inner` refuses is a policy of its own.
* The ask-class brand is not carried: it attests the replaced `check`.
*/
export declare function wrapToolPolicy<P extends ToolPolicy>(inner: P, check: ToolPolicy["check"]): Pick<P, ToolPolicyCarriedMember> & ToolPolicy;
/** Every path rule the given policies carry (combined policies included), in order, bound at `anchors` — the anchors of
* the leg the policies are installed on (a policy already installed answers with its own installation's rows). The deny
* tables take the rows that guard a face (`faces` non-empty) of a deny rule. */
export declare function toolPolicyPathRules(policies: readonly (ToolPolicy | undefined)[], anchors: import("./permission-rule-model.js").PathRuleBases): readonly import("./path-rule-binding.js").BoundPathRule[];
/** The guarded write rows the given policies carry together (combined policies included): every member's rows of one
* judgment, in member order (a member that throws throws here — the judgment leaves the target unplaced). `undefined`
* when none carries any. */
export declare function toolPolicyWriteGuardRows(policies: readonly (ToolPolicy | undefined)[]): ToolPolicy["writeGuardRows"];
/**
* One deterministic DENY component of a {@link ToolPolicyProjection} (F-012 L1). All fields are plain
* serializable data — a component round-trips JSON/`structuredClone` unchanged, which is what lets a
* chain of them persist on a checkpoint and be re-executed verbatim on resume.
*
* - `tool_deny`: deny any call whose toolName is in `names`.
* - `tool_allowlist`: deny any call whose toolName is NOT in `names`.
* - `shell_command`: for calls to the named shell `tools`, parse the leading command name and deny
* per `deny`/`allow` lists; `unmatchedAction`/`unparseableAction` say whether the projection may
* speak for those arms (`"deny"`) or must stay silent (`"none"` — the live policy's ask arm owns
* them). Non-shell tools are out of scope (no opinion).
*/
export type ToolPolicyProjectionComponent = {
kind: "tool_deny";
names: readonly string[];
} | {
kind: "tool_allowlist";
names: readonly string[];
} | {
kind: "shell_command";
tools: readonly string[];
deny: readonly string[];
allow?: readonly string[];
/** Verdict for a parsed command name outside `allow` (only meaningful when `allow` is present). */
unmatchedAction: "deny" | "none";
/** Verdict for a command string this projection cannot parse (compound/missing command). */
unparseableAction: "deny" | "none";
};
/**
* The deterministic data half of a {@link ToolPolicy} (F-012 L1): ordered deny components plus the
* explicit live-remainder bit. See {@link ToolPolicy.projection} for the contract.
*/
export interface ToolPolicyProjection {
readonly components: readonly ToolPolicyProjectionComponent[];
/**
* `true` when the policy has behavior BEYOND these components (an ask arm, an approval callback,
* a rewrite) — the projection then freezes only the deny half and the live policy must still ride
* the resume re-supply channel for the rest. `false` = the components plus "allow otherwise" ARE
* the whole policy (fully projectable — e.g. a plain name allow/deny policy).
*/
readonly requiresLiveRemainder: boolean;
}
/**
* The entries of a tool-NAME list whose reach is a set of names rather than one name — the covering
* spellings (`<ns>__<peer>`, `<ns>__<peer>__<glob>`) of ANY protocol namespace, which CC's rule
* matcher resolves and an exact-membership test silently cannot. Every name-keyed lane in this file
* consults these ALONGSIDE its exact set: a covering spelling can never equal a minted tool name (`*`
* is outside the minted charset and a minted name always carries a tool segment), so leaving it in the
* exact set too costs nothing and keeps the projection/audit faces reporting the operator's own spelling.
*
* Namespace-generic on purpose: the covering grammar is a property of the shared `<prefix><peer>__<tool>`
* name shape, not of MCP, and keying it to one prefix made every sibling-namespace covering rule
* (`a2a__<peer>__*`) compile clean and match nothing — inert in the deny half, and in an ALLOW list the
* removal of that peer's whole tool set.
*
* The list is usually empty, which is why every call site tests it before scanning.
*
* Exported for the OTHER name-keyed lane in this engine — the persisted session/ancestor rules in
* `session-rule-policy.ts`. Not part of the public API (`src/index.ts` re-exports by name and does not
* list these two): every publisher of {@link ToolPolicyNameSets} must resolve a covering entry the same
* way, because the prepare-time audit's exemption for these spellings speaks for ALL published name sets
* at once — the four in-engine lanes do so by sharing this module's matcher. That premise is NOT
* structural for an OUT-OF-ENGINE publisher (a host compiling its own settings into an exact-match ask
* closure is fail-open on these spellings while the audit stays silent) — which is why the underlying
* judgment predicates (`isNamespacedCoveringRuleName` / `namespacedRuleNameCovers`) ARE exported from
* the package index: an external lane either compiles through `createPermissionRulePolicy` or
* self-checks with those.
*/
export declare function namespacedCoveringEntries(entries: readonly string[] | undefined): readonly string[];
/** Does any covering entry reach the call — under ANY name it answers to, the same reading the exact lists take through
* `nameListHolds` (a covering entry and an exact entry in one list are one rule: it reaches the call iff its name is on the
* list)? See {@link namespacedCoveringEntries}. */
export declare function namespacedCoveringHit(covering: readonly string[], call: Pick<ToolCallRequest, "toolName" | "face">): boolean;
/**
* The tool names a projection component is keyed on, and the member that holds them: a `tool_deny` / `tool_allowlist`'s
* `names`, a `shell_command`'s `tools` (its `deny` / `allow` are command names, not tool names). A frozen chain holds them only
* in the one spelling a checkpoint row records a tool name in; the resume screen reads them back by it before anything replays.
*/
export declare function projectionToolNames(c: ToolPolicyProjectionComponent): {
readonly member: "names" | "tools";
readonly list: unknown;
};
/**
* Execute a persisted {@link ToolPolicyProjection} against a call (F-012 L1): returns the first
* component's deny, or `undefined` when the projection has no opinion (it is deny-only by
* construction — never an allow, never an ask). This is what a durable RESUME runs instead of
* trusting a re-supplied policy's execution half for the frozen deny data: the projection persisted
* at suspend is the authority for what the chain refused, whatever a re-supplied closure claims.
* A tool name is matched as recorded, which is the rule syntax's reading only for a name in its canonical spelling: the resume
* screen refuses a frozen chain holding any other ({@link projectionToolNames}) before this runs on it.
*/
export declare function checkToolPolicyProjection(projection: Pick<ToolPolicyProjection, "components">, req: Pick<ToolCallRequest, "toolName" | "args" | "face">): Extract<PermissionResult, {
action: "deny";
}> | undefined;
/**
* One entry of the persisted constraint-chain topology (F-012 L1): either a layer's projection
* content, or an explicit OPAQUE placeholder for a layer that exports none. The placeholder is
* load-bearing — the chain digest binds the ORDERED topology (layer boundaries included), so an
* opaque layer's position is part of what a resume re-supply must reproduce.
*
* F-012: each entry ALSO carries the serializable DECISION-CHAIN METADATA posture —
* whether the layer was classifier-armed (`autoModeArmed`) and its durable-mandate / content-mandate
* flags. The digest binds these, so a cross-process resume re-supplying a same-shape chain whose
* projections match but whose classifier was DROPPED (or whose mandate posture changed) — a
* version-skew / reconfiguration widening — is rejected pre-CAS. The metadata is NOT read by
* {@link checkToolPolicyProjection} (execution reads only `components`); it exists purely to be
* digested. The frozen closures themselves (approver, decider) are non-serializable and stay outside
* the digest — the re-supply trust contract governs their identity — but their PRESENCE/posture is
* bound so a re-supply cannot silently shed a decision-chain layer.
*/
export type ConstraintChainEntryMeta = {
/** True iff the layer carried a frozen auto-mode classifier (`autoMode`). */
autoModeArmed?: true;
/**
* The layer's serializable auto-mode ARMING RECIPE: the criteria half of the frozen classifier
* (assembly inputs + knobs + the deployment's settings epoch), recorded so a CROSS-PROCESS redemption
* can rebuild an equivalent decider over its own fresh model leg
* ({@link import("./auto-mode-arming.js").rebuildAutoModeDecider}) instead of answering `unavailable`
* forever and parking the same approval for a human on every redemption.
*
* OPT-IN and ADDITIVE: present only when the arming deployment asked for it (`RunnerDeps.autoMode.
* persistArming`), so an unchanged deployment mints byte-identical entries and every pre-#503 row
* (and every row minted without the opt-in) reads as absence = the pre-#503 behavior. Absence is also
* what an OPEN BREAKER produces — see {@link constraintChainEntryOfLayer}.
*
* The digest binds it: a re-supply that DROPS the recorded recipe, or hands back a DIFFERENT one, is
* rejected pre-CAS — so the re-supply contract is exactly "hand back what the row carries" (a redeeming
* host reads the recipe off this field and passes it straight through). The verify side takes the
* PRESENCE of this field from the row, never from the re-supplied layer's live classifier health, so a
* row that recorded no recipe does not refuse a re-supply that offers one — the reasoning, and why that
* direction cannot be recovered, is recorded on {@link constraintChainEntryOfSuppliedLayer}.
* `checkToolPolicyProjection` never reads it: like the other metadata here it exists to be digested,
* not to execute.
*/
autoModeArming?: import("./auto-mode-arming.js").AutoModeArmingRecipe;
durableMandate?: true;
contentMandate?: true;
};
export type ConstraintChainEntry = ConstraintChainEntryMeta & ({
opaque: true;
} | {
components: readonly ToolPolicyProjectionComponent[];
requiresLiveRemainder: boolean;
});
/** Project one policy into its chain entry (opaque placeholder when it exports no projection),
* carrying the layer's decision-chain metadata posture for the digest (round 12). */
export declare function constraintChainEntryOf(policy: ToolPolicy, meta?: ConstraintChainEntryMeta): ConstraintChainEntry;
/**
* One inherited-chain LAYER as both chain-entry mint sites see it — structurally an
* `InheritedGate.parentConstraints[]` element, typed here structurally so this module (which owns the
* digest) does not depend on the runner's shapes. The classifier itself is not read: a decider carries no
* state between rounds, so nothing about its health enters a chain entry.
*/
export interface ConstraintChainLayerView {
policy: ToolPolicy;
autoMode?: {
arming?: unknown;
};
durableMandate?: boolean;
contentMandate?: boolean;
}
/**
* The WRITE-side mint: the chain entry a durable PARK records for a live inherited layer. The recipe is
* carried whenever the layer has one (the deployment opted into `persistArming`). The former gate — "only
* while the ancestor's breaker is untripped and untouched" — went with the breaker: a decider carries no
* state between rounds, so there is no ancestor health to sample here and nothing that could make a
* redemption run wider than the ancestor it inherits.
*
* ⚖️ WHAT THE ROW DOES NOT SAY. An armed layer whose deployment never opted in leaves the entry at
* `autoModeArmed: true` with no recipe. The digest does not, and cannot, stop a re-supplier from handing
* back a FRESH decider of its own: that is the pre-existing re-supply trust posture (see `resumeStream`'s
* contract — a caller able to invoke it already holds full Runner authority, and the chain checks catch
* shape MISTAKES, not a hostile trusted caller). What CORE hands forward is the recipe when there is one,
* and nothing otherwise.
*/
export declare function constraintChainEntryOfLayer(layer: ConstraintChainLayerView): ConstraintChainEntry;
/**
* The VERIFY-side mint: the entry a resume's RE-SUPPLIED layer must reproduce for the row it is
* redeeming. Same body as the park mint, with the arming carry taken from the ROW rather than from the
* layer — the digest equality is over bytes the row itself recorded, never over what a live object says at
* a second instant. A row that DID record a recipe must have that recipe handed back: a re-supply that drops
* the arming, or hands back a different one, mints a different entry and is refused pre-CAS
* (`resume.parent_constraint_mismatch`). A row that recorded none does not refuse a re-supply that hands one
* back — the recipe is not an enforcement object (the DECIDER classifies), and the re-supply trust posture
* already permits a fresh decider; what the row's absence still means is that no redemption can reconstruct
* the ancestor's classifier FROM THE CHECKPOINT.
*/
export declare function constraintChainEntryOfSuppliedLayer(layer: ConstraintChainLayerView, recorded: ConstraintChainEntry | undefined): ConstraintChainEntry;
/**
* Content digest over an ordered constraint chain (F-012 L1) — the durable half's anti-tamper anchor.
* Binds the chain TOPOLOGY (order, layer boundaries, opaque placeholders) and each projection's full
* content; version-prefixed (`cpv1:`) so a digest computed under different projection semantics can
* never false-match. Replaces the count-only re-supply contract: two chains of equal length with
* different frozen deny data now digest differently.
*/
export declare function constraintChainDigest(chain: readonly ConstraintChainEntry[]): string;
/** Every member of the decision union — a member the union gains and this list lacks is a type error at `satisfies`.
* The receipt walks it: a contract member not held as an own enumerable data property is refused if a descriptor on the
* prototype chain holds one (an `undefined` data value spells absence). Not re-exported from `src/index.ts`. */
export type DecisionMember = PermissionResult extends infer U ? (U extends unknown ? keyof U : never) : never;
export declare const DECISION_MEMBERS: readonly DecisionMember[];
/**
* The RECEIPT of a permission decision — the one point at which the engine takes a decision in from whoever composed it,
* on every lane: each layer of {@link combinePolicies}' fold, the gate's adjudication entry and its approved-edit re-check,
* the inherited lane's receipts of an ancestor policy's decision, the final-form re-check, the direct `check()` rechecks
* (the resume edit, the park re-projection, the changed-files probe), and the PreToolUse hook's result seam (its own
* reach, below). NOT re-exported from `src/index.ts`: an internal screen on inputs the engine receives.
*
* WHAT COMES BACK is the engine's own frozen copy, never the caller's object. A decision is caller-supplied data: a
* stateful accessor, a Proxy, or a policy that keeps and mutates an object it returned could otherwise answer two
* consumers with two values, and which consumer read which value would decide the call. So the caller's object is read
* ONCE, here, and every station after the receipt (the fold's ranking, the origin stamp, the classifier's eligibility, the
* mode arbitration, the approval request, the rewrite that executes) reads the copy; a copy handed to the screen again
* comes back as it is, unread (so does the engine's own refusal, {@link isEngineRefusal}). In order: (1) the CARRIER's type — a decision that is not an object (a string, a function
* holding the members) has no `action`; (2) the ONE READ — ONE function ({@link receiveContract}) for the decision and for the
* records it nests (`receiveNestedRecords`, decision-receipt.ts), on an ORDINARY object (a Proxy, as the carrier or anywhere on its prototype
* chain, is refused before a trap of its could run): each contract member absent or an own enumerable DATA property, its value
* taken off the carrier's own descriptor (a member outside the contract is neither read nor carried — a policy's self-declared
* record on another name finds no reader); any other spelling refused by name — an accessor (never invoked), a member the
* prototype or a non-enumerable property supplies unless that descriptor spells absence (an `undefined` data value) — judged by
* descriptor, never read; (3) a retired member ({@link RETIRED_DECISION_MEMBERS}: `reason`, `guard`) carried by the same presence law as a contract
* member — own, held by an accessor, or supplied by the prototype — judged without a read (ignoring it silently dropped the policy's text or the ask's guard); (4) the VALUES, off the engine's
* one-read record, never a second read of the caller's object — the retired window absent, `action` in
* its closed set (`screenContractValues`, decision-receipt.ts — every closed-set member is held to its set there; a consumer ranks a decision by testing two of its words, so any other value used to rank as an
* allow; a `canUseTool` result outside its set is a failure in CC) and `decisionReason` in {@link DECISION_REASONS} when
* present (absence stays legal); (5) the DETACH —
* the contract members' values structured-cloned ({@link tryCloneArgs}; one that cannot be, a function inside a rewrite,
* is refused), the engine's `attestation` record re-attached by reference after its brand check, the copy frozen and
* marked as this module's. A refusal is a deny in the screen's words, one code for every arm (`decisionReason: "rule"`),
* carrying no `updatedInput` from the refused object (a rewrite from an EARLIER, contract-shaped layer still rides out of
* {@link combinePolicies}' deny path). `null` / `undefined` carry nothing and take the carrier's refusal (the caller's own
* read of `.action` would throw instead, and that throw was being reported as an aborted check).
*
* SCOPE: the receipt runs NONE of the carrier's code — a Proxy (its traps are the caller's code) is refused unread, and on an
* ordinary object descriptor looks and `Object.getPrototypeOf` invoke nothing. What the receipt does not reach: code inside a
* member's VALUE, run by the detach itself (an accessor inside a rewrite is invoked by the structured clone, and what it answers
* there is what is cloned and what runs — one read, consistent) and by a refusal's rendering of a foreign word (`String(v)` on an
* out-of-set `action`, after the deny is decided). Same-process JavaScript has no trust boundary a function can enforce.
*
* `reasonIsNonInput` (the HOOK seam): the same receipt, with the two differences the seam's consumers imply — a hook's
* `decisionReason` is DISCARDED there (every mint point downstream re-stamps `"hook"`) and so is not held to its set, and a
* hook result may leave `action` out (no opinion; the policy still adjudicates). `undefined`, `null` and `false` are no
* opinion (what a hook body yields when it has nothing to say). A refusal the receipt mints at this seam is the ENGINE's
* decision ({@link isEngineRefusal}) and exits through the engine's fail-closed arm (the deny observer), never as the hook's
* own deny.
*/
export declare function refuseOutOfContractDecision(d: PermissionResult, opts?: {
reasonIsNonInput?: boolean;
}): PermissionResult;
/** The raw tool-name lists a name-keyed policy was built from (audit feed, see block note above). */
export interface ToolPolicyNameSets {
readonly allow?: readonly string[];
readonly deny?: readonly string[];
/** Names gated behind an approval decision ({@link createApprovalPolicy.requireApproval}) — a typo
* here silently WIDENS (the tool runs without ever asking), so it is audited too. */
readonly ask?: readonly string[];
}
/** A policy that states its name lists ({@link ToolPolicy.nameSets} set). Composition-transparent:
* {@link combinePolicies} (and therefore {@link tightenTaskSpec}) unions children's groups onto the
* combined policy, so metadata survives the deployment's tighten/compose path. */
export type NamedToolPolicy = ToolPolicy & {
readonly nameSets: readonly ToolPolicyNameSets[];
};
/** All name-set groups reachable on `p` — `[]` when it carries none (e.g. a custom closure policy,
* which simply opts out of the audit). */
export declare function toolPolicyNameSets(p: ToolPolicy | undefined): readonly ToolPolicyNameSets[];
/**
* Name-based allow/deny. `deny` always wins. If `allow` is provided, only those tools are permitted
* (everything else denied). Without `allow`, everything not in `deny` is permitted. Each entry is read by the rule
* syntax (`ruleSpellingOf`): a name, or `Tool()` / `Tool(*)` — the bare `Tool`, as every list reader reads them, once however
* many spellings write it; any other spelling is not a tool name (`onInvalidName`), and neither is the wildcard `*` (`*`,
* `*()`, `*(*)`) in `deny`: it names no tool, and the entry would be kept and never match. In `allow` it is one entry no tool
* carries — the list admits what else it names, and nothing for it.
* Carries {@link NamedToolPolicy.nameSets} (the names as read) so the host can audit the lists against the mounted
* pool ([c209-E] advisory); enforcement semantics are unchanged.
*/
export declare function createAllowDenyPolicy(opts: {
allow?: string[];
deny?: string[];
/**
* What to do with an entry that is not a tool NAME: a rule CONTENT form (`Bash(ps:*)`, `Edit(src/**)`), a spelling
* the rule syntax reads no tool name in (`" Bash"`, `Bash\t(*)`, `-deploy`), or the wildcard `*` in the deny list.
* `"throw"` (default, matching {@link createPermissionRulePolicy}'s posture toward a rule it cannot
* compile) refuses at construction; `"skip"` drops the entry and reports it. Under BOTH, the entry never
* silently narrows the allowlist.
*/
onInvalidName?: "throw" | "skip";
/** Called once per dropped entry under `"skip"`. */
onInvalidNameIssue?: (issue: {
entry: string;
list: "allow" | "deny";
message: string;
}) => void;
/**
* What an `allow` list HIT means. `"grant"` (default): the deployment named this tool as permitted — a statement
* (`decisionReason: "rule"`) that satisfies the fold's unstated rule for a passthrough-seat tool. `"narrowing"`: the
* list only bounds WHICH tools a lane may see (an agent definition's `allowTools`); a hit is scope, not consent, and
* answers a bare allow — the mode's unstated question still goes to a person. The deny list is unaffected.
*/
scope?: "grant" | "narrowing";
}): NamedToolPolicy;
/**
* What `createApprovalPolicy`'s `approve` may resolve to: a boolean, or the `AskOutcome` OBJECT minus the
* two members this seat has no lane for — `"unavailable"` (a ROUTING answer whose only consumer is the
* ask resolver's durable re-route) and `updatedInput` (an OPERATOR EDIT; see {@link ApproverEditLane}).
* The latter is spelled `?: never` rather than merely omitted, the way this file spells every
* prohibition a fresh object literal alone would not catch (a predeclared const, an annotated return).
*/
export type ApprovalSeatOutcome = boolean | (Extract<AskOutcome, {
allow: boolean;
}> & {
updatedInput?: never;
readRootGrant?: never;
});
/**
* Human-in-the-loop approval for selected tools. Tools in `requireApproval` call `approve(req)` and
* are allowed only if it resolves true; tools in `deny` are always blocked; everything else is allowed
* (override with `denyByDefault: true` to allow only `requireApproval` + an explicit `autoAllow`).
*
* This seat receives a bare `ToolCallRequest`, NOT the ask surface: the decision settles in-place
* inside the policy, no AskRequest is minted and `resolveAsk` is never entered — so none of the
* ask-side approval members (`hasBidiControls`, `boundInputHash`, `preview`, `riskAxes`, …) exist
* here. A surface rendering the command text for a human on this seat must render through
* {@link import("./permission-rule-model.js").renderUntrustedCommandText} (the display baseline)
* or run its own screen — absence of the warning bit is "not on this surface", never "clean".
*
* WHAT `approve` MAY RETURN: a boolean, as always, or the `{allow, …}` OUTCOME OBJECT the `onAsk` seat
* takes, read by the SAME screen ({@link readApproverOutcomeObject}) and narrowed to what this seat has
* a lane for ({@link ApprovalSeatOutcome}) — so a deployment here can report WHO ended the wait
* (`settledBy:"policy"` for its own policy's yes or no, `"timeout"` for its own elapsed window), attach the
* decider's `reason` and name the `approver`. A bare `true` / `false` is unchanged: a person's approval or
* refusal, the refusal with this factory's own terse sentence. Before the object arm a bare boolean was the
* only spelling there was, so a machine's decision filed a PERSON's record.
*/
export declare function createApprovalPolicy(opts: {
/** Tools that need an approval decision. */
requireApproval: string[];
/**
* The approval decision (e.g. await an operator). Resolve true to allow, false to deny — or resolve
* the `{allow, settledBy?, reason?, approver?, updatedInput?}` outcome object for everything a bare
* boolean cannot say (see this factory's own doc for what each member means here). `signal`
* fires when the task aborts — and, since design/384, when the asking turn is interrupted — race
* your wait against it (e.g. an OA approval callback) so a never-answered request is released at
* the deadline rather than holding the worker.
*/
approve: (req: ToolCallRequest, signal?: AbortSignal) => ApprovalSeatOutcome | Promise<ApprovalSeatOutcome>;
/** Always-denied tools. */
deny?: string[];
/** Always-allowed tools (only meaningful with `denyByDefault`). */
autoAllow?: string[];
/** When true, deny anything not in `requireApproval` or `autoAllow`. Default false. */
denyByDefault?: boolean;
/**
* Optional self-contained deadline (ms): if `approve` hasn't resolved by then, deny. Belt-and-
* suspenders for **direct** callers — the Runner already races `check` against the task signal, so
* a Runner-driven task is bounded regardless; this protects a caller invoking `check()` without a
* signal. OMIT to wait indefinitely on `approve` — omission is the only spelling of "no deadline"
* (CLS-A-9): a written-down `0` is the tightest deadline (deny at once), and a non-finite value is
* refused at construction.
*/
approvalTimeoutMs?: number;
}): NamedToolPolicy;
/**
* Combine policies with the three-state fold **deny > ask > allow** (design/37): the WORST outcome
* across all policies wins. A `deny` short-circuits (nothing can override it); an `ask` does NOT
* short-circuit — scanning continues, because a later policy may still `deny` (which outranks ask).
* With no deny, the first `ask` wins; with neither, `allow`.
*
* **An allow is STATED or not.** A layer that names the call (an allow rule, a list entry, an approver's yes, a mode's
* release) says WHY on its allow (`decisionReason`); a bare allow is "no objection" — an empty table, a list the call is
* not on, a layer that does not know the tool. The combined allow carries the FIRST stated reason in layer order, and a
* statement binds the arguments its layer judged: a layer that rewrites the arguments voids every statement before it (its
* own allow, if it states one, speaks for the new arguments). For a tool whose permission seat is `passthrough` an
* unstated allow is not a permission — the gate puts the mode's question to it; for every other tool the two forms run alike.
*
* **Idempotency requirement:** because the fold continues past an `ask` to look for a later `deny`, a
* policy's `check()` can be invoked even after an earlier policy returned `ask`. `check()` MUST be
* idempotent — repeated calls with the same {@link ToolCallRequest} return the same decision and
* accumulate no side effects (e.g. don't fire a duplicate approval notification on each call).
*/
export declare function combinePolicies(...policies: ToolPolicy[]): ToolPolicy;
/**
* The shell tools whose `{ command }` arg a coarse command-name policy parses; other tools are out of scope. It is the
* catalog's shell face ({@link shellFaceToolNames} — every definition whose `command` the command grammar judges, `Monitor`
* among them: it runs an arbitrary shell `command` through the same env seam as Bash), spelled out for the wire; the
* catalog's shell face is the truth, and a lockstep pin keeps the two one (a catalog row added or dropped turns it red):
* the always-on parity gates keyed on it, the `Bash(...)` rules and the shell seat reach the same tools. EXPORTED: a
* downstream governance layer reads it so its per-command ask-list gates the same tools core does.
*/
export declare const COARSE_SHELL_TOOLS: readonly ["Bash", "Monitor"];
/**
* design/center §10 — a COARSE allow/deny gate keyed on the LEADING command NAME (`argv[0]`) of a single
* simple shell command. It reuses the ONE shared simple-command parser
* ({@link import("../tools/fs/index.js").parseLeadingCommandName}) — it does NOT re-implement argv[0] parsing
* (a second parser would drift and open a circumvention). For a `bash`/`bash_readonly` call it extracts the leading
* command name and:
* - if the name is in `deny` → `deny` (deny-wins, matching {@link createAllowDenyPolicy}; NOT last-match-wins,
* which would fight {@link combinePolicies}'s deny>ask>allow fold);
* - else if `allow` is provided and the name is NOT in it → `defaultAction` (`"ask"` default, or `"deny"`);
* - else → `allow`.
* A command the parser cannot reduce to a single bare name (it has shell operators / a path-prefix / a leading
* env-assignment) is treated by `defaultAction` (fail-toward-gate): such a command is exactly what would circumvent
* an argv[0] filter, so it should not silently `allow`. NON-shell tools are out of scope → `allow` (this gate
* only speaks about shell command names; compose it with other policies for the rest).
*
* **core ships NO opinionated command list** — only this mechanism. The actual `allow`/`deny` rule set is
* injected by the profile/config/deployment.
*
* ⚠️ **NOT A SANDBOX — coarse filter / defense-in-depth only.** This matches `argv[0]` NAME and nothing else,
* so the circumvention surface is large and the gate MUST NOT be relied on as containment:
* - command substitution / subshells (`$(curl …)`, backticks), pipes, redirects, `;`/`&&` chaining — all
* rejected as un-parseable here (→ `defaultAction`), so they don't sneak past `allow`, but that is a GATE
* not a guarantee of safety;
* - an indirection wrapper runs an arbitrary inner program under an allowlisted `argv[0]`: `sh -c '…'`,
* `env FOO=bar curl …`, `sudo rm …`, `timeout 5 curl …`, `xargs curl`, `find . -exec rm {} \;`,
* `python -c '…'`, `npm run <script>`, `git <subcommand>` — the inner action is INVISIBLE to an
* argv[0]-name filter. Allowlisting any such wrapper effectively allowlists everything it can launch.
* Real isolation is the deployment's sandboxed `executionEnv` (a remote/E2B container with no egress, a
* read-only FS, dropped capabilities). Use this only as a cheap first-line coarse filter layered over that.
*
* 🪤 **TRAP #1 — `TaskSpec.toolPolicy` OVERRIDES (does not merge) `RunnerDeps.toolPolicy`.** A consumer that
* sets `TaskSpec.toolPolicy = createCoarseCommandNamePolicy(…)` directly **silently discards the deployment's
* baseline policy** (the runner does `spec.toolPolicy ?? deps.toolPolicy`). To LAYER this on top of a baseline
* you MUST compose explicitly: `combinePolicies(deploymentBaseline, createCoarseCommandNamePolicy(…))`
* (deny-wins — both policies' denies still fire). {@link tightenTaskSpec} does this for you.
*
* On the full shell face a plain `ask` from this policy stays a plain ask: the shell seat's
* `"maybe"` tier is the shape probe's instrument, not a risk label, so the park is the budgetable kind
* (`human` under `durableApproval`) unless the probe mandated the call (an out-of-root read) or could not
* answer. A command the shape tier's OWN tighten refuses (`rm -rf …` with no policy asking first) still parks
* non-budgetable — the safety axis is judged per call by `safetyAxisOf`.
*/
export declare function createCoarseCommandNamePolicy(opts: {
/** Command names (argv[0]) explicitly allowed. If provided, a name NOT listed falls to `defaultAction`. */
allow?: string[];
/** Command names (argv[0]) explicitly denied — `deny` always wins (checked before `allow`). */
deny?: string[];
/** What to do for a command not matched by `allow` (or un-parseable): `"ask"` (default) or `"deny"`. */
defaultAction?: "ask" | "deny";
/** Override the shell tool names this gate parses (default {@link COARSE_SHELL_TOOLS}, the shell face). */
tools?: string[];
}): ToolPolicy;
/**
* CC 220 "Unverifiable Deletion Target" parity (auto-mode rule; its text is in 220–245, gone from 2.1.250 on): ASK on a recursive+forced delete whose
* target is a variable / substitution that cannot be resolved from the command itself — fail-closed in the
* sense that with no live classifier/approver the ask degrades to deny (see {@link PermissionResult}'s
* `ask` doc), never to allow. The guard reads ONE plan, the read station's (`removalReadingOf`,
* removal-targets.ts): its firing conditions are `removalFindingsOf`'s there. The
* message carries the CC-sanctioned remediation (re-run with the resolved LITERAL path written into the
* command, or assign the variable in the same command) — it doubles as the `askMessage` a classifier sees.
*
* This used to be an unconditional `deny`, on the theory that the CC rule text forbids
* auto-allow («must be explicitly approved … cannot be auto-allowed by a permission rule»).
*
* RB-341 (2026-07-29) re-checked that citation against the 220 corpus and corrected it in BOTH directions.
* The RB-203 note claimed the phrase "traces to the 206 bundle; it does not appear anywhere in the 220
* corpus for THIS rule" — that sentence was wrong and is retracted. The phrase has 8 hits in 220, and two
* of them are this exact rule's own message text: `pretty220` 461365 (the `rm -rf $UNSET/*`
* variable-expansion arm, verbatim «e.g. `rm -rf $UNSET/*` becomes `rm -rf /*`. This requires explicit
* approval and cannot be auto-allowed by permission rules.») and 461444 (its command-substitution twin).
* Five more are the sibling deterministic `rm`/`rmdir` arms (457337/457364/457379/457387/457402) and one is
* an unrelated `find` rule (249564).
*
* The `ask` decision survives that correction, on a stronger anchor than the one RB-203 used:
*
* - **It is not a deny in 220 either.** Every one of those messages is built by the same constructor
* (`T7e`, 457298), whose `behavior` is literally `"ask"` — a deterministic gate that ASKS. The phrase
* scopes to permission RULES: no static allow rule may clear the gate. sema's equivalent is
* `combinePolicies`'s deny > ask > allow fold — an `allow` from any composed policy loses to this ask,
* in either composition order (pinned in tool-policy.test.ts).
* - **A live classifier MAY clear it, in 220 as here.** `T7e` marks the reason `classifierApprovable:
* false`, but pairs it with `circuitBreaker: "dangerousRemoval"`, and 220's auto-mode gate diverts an
* ask away from the classifier only when `!classifierApprovable && !(circuitBreaker !== undefined &&
* isAutoModePermissionSurface(ctx))` (604049-604053). The circuit-breaker arm exempts exactly this
* family, so on the auto-mode surface the classifier DOES adjudicate it. That is consistent with the
* rule's listing in the classifier prompt's **SOFT BLOCK** bucket ("destructive or irreversible — User
* Intent Rule CAN clear these", 519698 / rule text 519726), escapable when the user's own words (or an
* agent proposal they affirmed) name the exact target — the same design sema's classifier prompt already
* implements verbatim (`auto-mode-prompt-assets.ts`, unchanged). A `classifierApprovable:false` reason
* WITHOUT a `circuitBreaker` is the shape that genuinely forbids the classifier — e.g. the design-agent
* consent ASK at 480999/481016, which takes the fallback-to-human path this rule does not. This rule is
* not that shape, so "follow `classifierApprovable:false` literally" would be STRICTER than CC.
*
* So a hard `deny` was wrong twice over: it short-circuited `combinePolicies`'s fold before a live
* classifier ever ran, and CC's classifier does run here. `ask` reuses the existing generic per-ask
* classifier arm (`hooks.ts`, `decision.action === "ask"`): a `block` verdict still denies
* (`decisionReason: "classifier"`, fully audited), an `allow` verdict still requires a live classifier to
* have actually cleared it, and "no classifier configured" still means deny.
*
* A review-confirmed P1 gap: the one piece of NEW plumbing this DOES need — `requiresRealApproval: true`
* (see {@link PermissionResult}'s `ask` doc) — closes a real gap the first cut of this fix missed. A
* deployment using the documented `onAsk: "allow"` circumvention mode (sandbox-is-the-boundary posture, the
* shape a sandboxed autonomy container runs) would otherwise have this rule's `ask` rubber-stamped by that blanket
* configuration with NO classifier ever consulted and NO judgment of any kind applied — silently WEAKER
* than the unconditional deny it replaced, for exactly the deployments that opted into the most permissive
* mode for OTHER, unrelated reasons. `requiresRealApproval` makes `resolveAsk` treat a blanket
* `onAsk:"allow"` as equivalent to no approver at all for this specific ask (deny, same as headless) while
* leaving every other path untouched: a live classifier (tried first, unaffected), a real approver
* CALLBACK (genuine per-call judgment, even one that usually says yes), and "no classifier + no approver"
* (already denied). Core wires this policy ALWAYS-ON in the runner's gate composition (prepare-task); it is
* exported so deployments can also compose/test it directly.
*
* `requiresRealApproval` also turns out to be the correct sema analog of the one thing
* `classifierApprovable: false` still buys in 220 that a `circuitBreaker` does NOT take back — 220's
* bypassPermissions arm refuses to auto-allow an ask carrying `circuitBreaker === "dangerousRemoval"`
* (603856-603869), i.e. a blanket MODE setting cannot clear it, only judgment can. That is exactly the
* `onAsk:"allow"` refusal above, so no new hard bit is owed. The field's remaining 220 effects have no core
* surface to attach to: it suppresses the "always allow / don't ask again" affordance in the CC permission
* dialog (908517 / 909563) and re-attaches a matching user `ask` rule on the auto-mode surface (461507).
* Core has neither a persisted always-allow surface nor rule re-attachment — noted for whichever UI layer
* grows one.
*/
export declare function createUnverifiableDeletePolicy(opts?: {
/** Extra variable names treated as well-known-safe roots, ADDED to the default set `TMPDIR`/`HOME`
* (`KNOWN_SAFE_ROOTS`, removal-targets.ts — `HOME` is a registered sema divergence from CC's temp-only carve-out).
* Each must be a shell identifier; `PWD` / `OLDPWD` are refused — their value is where the command stands, read off
* the working-directory thread, not a root the shell provides. A bad entry throws here. */
safeVars?: readonly string[];
/** Override the shell tool names this gate parses (default {@link COARSE_SHELL_TOOLS}, the shell face). */
tools?: string[];
removalSeat?: RemovalGuardSeat;
}): ToolPolicy;
export declare function createTranscriptIntegrityPolicy(opts?: {
/** Transcript directories to protect. Default: `($AGENT_DATA_DIR ?? ~/.ai-agent)/sessions`. */
sessionsDirs?: readonly string[];
/** argv[0] allowlist for read-only access to the transcript dir (default {@link BASH_READONLY_DEFAULT_ALLOW}). */
readAllow?: readonly string[];
/** Override the shell tool names this gate parses (default {@link COARSE_SHELL_TOOLS}, the shell face). */
tools?: string[];
}): ToolPolicy;
/**
* Provenance of the DELEGATION whose child raised an ask (ruled 2026-08-04) — filled by the delegation
* tool when it forwards the parent run's approver into a child, never by the child itself (it rides the
* trusted `ToolExecuteContext.onAsk` seat, which no tool argument reaches).
*
* The INNERMOST (issuing) frame wins: a grandchild's ask carries the grandchild's own spawn facts, not
* the intermediate one's — each delegation forwards an already-stamped request verbatim.
*
* Relation to the sibling source-identity fields on {@link AskRequest}: `fromSubagent`/`sourceAgentName`
* answer "a delegated child asked, and this is its display identity" from the CHILD's own prepare; this
* one answers "which delegation CALL it belongs to" from the SPAWNER's side — the correlation an approver
* UI needs to attach the approval card to the Agent card the operator is already looking at.
*/
export interface AskDelegationProvenance {
/** The spawning tool call in the PARENT run (`ToolExecuteContext.toolCallId` at spawn time) — the
* delegation's stable identity, minted by the engine. */
readonly parentToolCallId: string;
/** Delegation nesting depth of the ISSUING child: 1 = a child of the run that owns the approver,
* 2 = its grandchild, … (as counted by the delegation-tool tree that spawned it). */
readonly depth: number;
/** The explicit spawn name / agent-definition name of the issuing child, when it has one.
* **UNTRUSTED-for-display** (the spawning model chooses `name`): redact + bound before rendering,
* never adjudication input — same posture as {@link AskRequest.sourceAgentName}. */
readonly agentName?: string;
}
/** The structured context an `onAsk` approver receives for an `ask` decision (design/37).
* Lifecycle (design/384): the wait this request fronts can be released by the run's abort AND by a
* turn-level interrupt; after that release the engine no longer awaits the approver, so anything an
* approver retains off this object it must release itself, keyed on its `signal` argument's abort
* (see {@link OnAsk} for the full detached-settlement contract). */
export interface AskRequest {
toolName: string;
/** The allow-layer members that MATCHED this call and could not clear the ask (see {@link PermissionResult}'s ask arm),
* one sanitized record per member — so the card renders "your rule is alive, just outranked" from data; the same
* account is rendered once onto {@link message} at this request's mint. A DISPLAY value, not an identity channel:
* a stored rule's identity travels on {@link AskRuleEvidence.personalRuleDots}. */
allowRuleShadowed?: readonly import("./allow-rule-shadow.js").AllowRuleShadow[];
/** WHY the reversibility probe did not clear this call — the account of the tighten the
* card was showing WITHOUT until now (the gate knew the cause and dropped it at this seam, leaving the
* approver the action but not the reason it is being asked about). Present only for a `"maybe"`-tier
* tighten whose probe supplied a cause; the durable park route carries the same string as
* `RiskDescriptor.probeReason`. Neutralized + length-capped by the gate. UNTRUSTED-for-display,
* never adjudication input. */
readonly probeReason?: string;
/** The STRUCTURED cause behind the tighten — a machine-readable code plus the operand
* families as arrays with honest totals, so this surface renders its OWN sentence rather than one the
* engine had to write before the check's edge cases were known. See
* {@link import("./checkpoint-store.js").ProbeCause}, whose contract is that the ARRAY is the
* structure: render the entries as data, never re-derive structure by splitting or joining them.
* The durable park route carries the same value as `RiskDescriptor.probeCause`. */
readonly probeCause?: import("./checkpoint-store.js").ProbeCause;
/**
* The ask-facing face of {@link import("./types.js").ReversibilityVerdict.readRootCandidate}, which owns
* the meaning, the absence reading and the spelling contract: PRESENT ⇒ declaring this one directory
* clears THIS ask, so a card can offer "add it and stop being asked" beside "yes, this once". Filled at
* the synchronous MINT station by the tighten that ran the probe — the three INHERITED-lane sites present
* an ancestor's decision from upstream of it and carry none, and the durable park row has NO twin: a
* parked out-of-root ask renders the confirmation alone.
*/
readonly readRootCandidate?: import("./types.js").ReadRootGrantCandidate;
/** design/252 G-2: the RULE-PROVENANCE evidence behind this ask — the org snapshot revision and the
* matched rules' own identity keys, each present as a value or as a named absence (see
* {@link AskRuleEvidence}). Present on EVERY ask the engine mints, so a consumer never has to tell a
* missing field from a field that means something: the gate's own mint site fills it from what its
* layers did, and the three INHERITED-lane sites — which present an ANCESTOR policy's decision, from
* upstream of both lanes — fill it with an all-`"not_adjudicated"` record rather than a fabricated
* revision. Optional on the TYPE only because a deployment may invoke its approver directly.
* RECONCILIATION metadata, never adjudication input. */
readonly ruleEvidence?: AskRuleEvidence;
toolCallId: string;
/** The (post-rewrite) args the tool would run with. */
args: unknown;
/** the tool's approval display projection of {@link args} (AgentTool.approvalPreview),
* clamped + control-char-sanitized by the gate. **UNTRUSTED, ADVISORY display metadata**: render
* with contextual escaping and show {@link args} alongside — the preview can misrepresent the
* executable action and never replaces args. Never adjudication input. */
readonly preview?: unknown;
/**
* #457 ② — PRESENT ⇔ this tool DECLARES an approval preview and the gate could not present a usable
* one, naming which way it failed: `"oversize"` (produced, but past the 16KiB display bound — the
* truncation stub still rides {@link preview}) or `"unavailable"` (the projection threw, or its value
* has no JSON serialization, so nothing was produced at all).
*
* ABSENT is the ordinary world and covers three cases that are NOT withholdings: the tool declares no
* preview, the projection declined to speak for these args, and the projection succeeded. A surface
* must therefore read presence, never absence: absence is not a claim that the preview is faithful.
*
* A DISCLOSURE, not a verdict and not a transform — the same posture as {@link hasBidiControls}. Core
* refuses nothing and narrows nothing on account of it: {@link args} and {@link boundInputHash} are
* always carried, so unlike a surface whose only content IS the projection, a core approval card can
* still show the person exactly what will run. What the bit buys is the honest sentence a card owes
* when it is rendering less than the tool meant it to — and the ground for a surface that chooses to
* withhold its own PERSISTENT "allow and stop asking me this" affordance while the action cannot be
* reviewed as intended (upstream's form: a withheld preview vetoes the standing row, leaving one-time
* approval only). Core deliberately does NOT apply that veto to {@link ruleOffers} itself — core's
* offers speak for shell COMMAND text, which rides `args` and is shown whatever the preview did.
*
* Filled at the ask MINT sites that resolve a preview (the synchronous gate leg), not at `resolveAsk`
* — the chokepoint receives an already-built request and has no tool table to project from. The
* durable park leg's twin is `PendingAction.tool_approval.previewWithheld` on the row, judged over
* that row's own fidelity-projected args snapshot; the two faces can honestly differ exactly as the
* bidi bit's two faces can.
*/
readonly previewWithheld?: "oversize" | "unavailable";
/** Canonical digest of {@link args} as presented in THIS ask, computed by the engine's one
* `boundInputHashOf` — the same digest a durable park binds its checkpoint to, so an aggregating
* approver can reconcile a synchronous ask row against a parked checkpoint row for the same call
* by `(toolCallId, hash)` equality. RECONCILIATION metadata only, never adjudication input: it
* repeats for two asks over identical args, and an `updatedInput` edit round re-mints it over the
* edited args. Filled at the `resolveAsk` chokepoint (present for every wired-approver call, so a
* mint site added later is covered by construction); optional on the type because a deployment may
* invoke its approver function directly. */
readonly boundInputHash?: string;
/** A call to the engine's `Skill` tool: the hash of {@link boundGrants} — the digest a parked approval of the same card is judged by, so a live card reconciles against its parked row by `(toolCallId, boundGrantDigest)` and an answer is remembered by "this tool + this binding". Present exactly on that call (whoever asked, however the question was worded); every other card carries {@link boundGrantAbsence} instead; `message` renders the same lists narrowed to what releases in the leg's mode. Reconciliation metadata, never adjudication input. */
readonly boundGrantDigest?: string;
/** The value {@link boundGrantDigest} hashes: `{ skill, allowedTools, disallowedTools }` in canonical spelling (de-duplicated, sorted), `skill: null` when no loaded skill is named; a fresh copy per ask; same presence. Display metadata (deployment text — render with contextual escaping). */
readonly boundGrants?: import("./skill-grant-binding.js").BoundSkillGrants;
/** Why this card carries no {@link boundGrantDigest} — every card the engine mints carries exactly one of the two: `not_bindable` (the call is not an activation of the engine's `Skill` tool, so it binds no grant and an answer may be remembered by the tool) or `unreadable` (a Skill card whose binding cannot be read — said by a stored row's reader; a live card always has its digest). The same factory mints both members; the park row and the inbox projection carry the same member. Reconciliation metadata, never adjudication input. */
readonly boundGrantAbsence?: import("./skill-grant-binding.js").BoundGrantAbsence;
/**
* design/179 §4, re-formed by design/375 §3.1 (BREAKING — this seat REPLACES the retired
* `ruleSuggestions` key, which is never emitted again; the new name is the safe failure posture for
* the hard-cut window: a consumer that has not upgraded simply renders no "don't ask again" row,
* fail toward asking, instead of mis-rendering a consent surface through its old parser) — the
* persistable allow-rule OFFERS this exact call could be covered by, so a surface can render
* "allow, and stop asking me this" with something concrete behind it. Present only when a persisted
* allow-rule lane is armed AND the call is one the lane can speak for (a redirection or a
* substitution yields NO offer, which is the honest answer rather than an option that would be
* refused on redemption) AND the ask is one a persisted rule could actually clear — a mandated ask
* (operator shellGate:"always", the tool's own egress/irreversibility marks, a
* `requiresRealApproval` demand, or — #502, the one PER-CALL member of that family — a demotion the
* call's own reversibility probe declared structural, which for the built-in shell probe means a read
* outside the session's allowed directories) and an ask carrying {@link allowRuleShadowed} offer
* none.
*
* CONTRACT — see {@link import("./permission-rule-model.js").RuleOffer} for the closed union, the
* per-element parse/degrade rules and the original-index-keying obligation. Array order is display
* order: at most 2 offers, the whole-string EXACT single always index 0 when present, the BATCH
* offer (a compound's per-segment conjunction — choosing it is one yes to all of its 1..5 rules at
* once) always last. Selection indices (`confirmRuleApproval`'s `selectedOffer`) are keyed
* against this order; redemption TICKETS are NOT — they key on the consent record's flat
* CANDIDATE list (a batch's members each carry their own ticket), so a chosen offer is redeemed
* via `redeemRuleBatch`, never by indexing a ticket array with an offer index. The durable park
* row carries the same array under the same contract.
*
* ADVISORY display metadata, never adjudication input, and never a rule by itself: minting one is a
* separate act that goes through the approval-record protocol, so a surface that ignores this field
* changes nothing about what is allowed. The AUTHORITATIVE render source for a consent card is the
* PREPARED approval record's own offers; this member is the ask-time projection, which may drift
* from it across a coverage change — harmlessly, since the record is what gets confirmed.
*/
readonly ruleOffers?: readonly import("./permission-rule-model.js").RuleOffer[];
/**
* design/382 §2.4 (adversarial-review r3, additive) — the RELATIVE-CD RESOLUTION BASE the offers
* above were minted with: the live tracked working directory at adjudication time (the RB-108
* value; equal to the task root until an observable `cd` moves the tracker). Present only beside
* {@link ruleOffers} when the run has a tracked cwd. A consumer preparing the AUTHORITATIVE
* consent record threads it as `prepareCardApproval`'s `execCwd`, so the record re-mints the SAME
* directory member this projection displayed — without it, a moved tracker would make the record
* resolve `cd ./x` from the task root and persist a rule for a directory the command never
* enters. Display/reconstruction context only, never adjudication input.
*/
readonly execCwd?: string;
/**
* #490 修② — WHY {@link ruleOffers} is absent, when the rule-offer lane is in play and has nothing
* to give. A CLOSED set, mutually exclusive with {@link ruleOffers} (never both, never neither once
* the lane is in play):
* · `"mandated"` — no persisted rule could ever clear this ask: an org/governance
* `requiresRealApproval` demand, an operator `shellGate:"always"` or the tool's own
* egress/irreversibility marks, a hook-raised ask, an ask whose authority is an ANCESTOR's
* approver rather than this task's rule lane, or (#502) a demotion this CALL's reversibility
* probe declared structural — for the built-in shell probe, a read outside the directories this
* session declared. A surface must NOT point at rule-writing here — "allow rules silence the
* classifier's questions, never a mandated one", so a rule minted in answer to this line would
* never take effect where it was minted. Whether a DIRECTORY grant is worth pointing at is not this
* seat's to say: it is answered per ask by {@link readRootCandidate}, which is present exactly when
* widening the session's read directories would clear this one and absent for every other member of
* the family — so a surface reads that seat and never infers an answer from this word;
* · `"allow_outranked"` — an allow-layer member already matched this call and this question outranks it
* ({@link allowRuleShadowed} says which): another allow rule would be outranked the same way;
* · `"ask_rule_matched"` — an explicit `ask` rule matched: it keeps asking while it stands;
* · `"lane_cannot_speak"` — the lane is armed and this ask IS clearable, but the offer lane
* produced no rule for this command. Usually because the grammar has no text for it (a
* substitution, a subshell, a backgrounded process, or a simple command carrying a redirection —
* a compound is judged per segment, so this arm means EVERY segment was unspeakable, since one
* dirty segment among clean ones yields a batch instead). It also covers the narrower case where a
* text EXISTS but the card's own array-order contract declines to present it alone (a simple
* command whose exact form the validator refuses offers nothing rather than promoting a lone
* prefix to index 0 — see `suggestRulesForCommand`). The seat says "this card has no rule to
* offer you", which is true of both; it does not claim the grammar is empty, and a surface must
* not word it as though no rule could ever be written for the command.
*
* ABSENT is not a claim, exactly as {@link previewWithheld}'s absence is not: it also covers the
* ordinary "there are offers" case and the three STRUCTURAL doors that answer with silence on
* purpose (no rule lane wired, a tool the lane does not speak for, a task that cannot hold a rule at
* all) — a reason line on every ask of a deployment that never had the feature would be noise about
* a lane that does not exist. Read presence, never absence.
*
* ADVISORY disclosure metadata, never adjudication input: it changes nothing about what is allowed,
* and a surface that ignores it renders exactly the card it rendered before. The durable park row
* carries the same seat under the same contract, filled by the same one factory.
*/
readonly ruleOffersAbsence?: import("./ask-origin.js").RuleOffersAbsence;
/** The human/model-readable prompt explaining what needs approval. */
message: string;
/** Runner-filled source identity (design S1d §1): the end-user {@link AskRequest} issuing task's
* `principal` (design/62). An AGGREGATING `onAsk` (one approver fanning over N delegated workers)
* attributes asks per-user by this. Worker/tool cannot set it (not a tool arg). Undefined ⇒ no principal.
* A non-aggregating `onAsk` simply ignores it (additive, no-op). `readonly`: Runner-populated metadata,
* not a caller/worker-settable field. Populated ONLY on the synchronous `resolveAsk`/`onAsk` path — a
* durable suspend/checkpoint flow does NOT invoke `onAsk` on the suspend side, so v1 ask-aggregation
* requires `checkpointStore` unset (design S1d §1.4 P-a). */
readonly principal?: string;
/** Runner-filled source-task identity (= the issuing task's session id). An aggregating `onAsk` uses it
* to tell which worker an ask came from (nested grandchildren each carry their own session id).
* **worker-unforgeable for Runner-created delegated subagents** (a child never sets sessionId — the
* Runner mints it); a top-level caller MAY continue its own session id via `TaskSpec.sessionId`, so this
* is NOT a global "never forgeable" run id — aggregation only aggregates delegated workers, the context
* where it holds (design S1d §1.3). `readonly` + same sync-path-only / P-a caveat as `principal`.
* Undefined ⇒ no source task id. **NOT a subagent discriminator** (the HOST's own asks
* carry it too — it is always the issuing task's session id); "from a background agent" attribution
* keys on {@link fromSubagent}, never on this field's presence. */
readonly sourceTaskId?: string;
/** design/153 §1 (RB-39②) — Runner-filled: present (`true`) exactly when the gate raising this ask
* belongs to a DELEGATED SUBAGENT (the prepareTask `isSubagent` fact — trusted `internals.parentTaskId`
* presence; a child cannot self-declare or suppress it, internals never ride a TaskSpec). Undefined ⇒ the
* host / a top-level task. THE explicit "from a background agent" discriminator (replaces presence-
* or equality-heuristics on {@link sourceTaskId}). `readonly`, sync-path-only, S1d posture. */
readonly fromSubagent?: true;
/** design/153 §1 — Runner-filled: the issuing task's display identity (a subagent's explicit agent
* name, else its agent-type; undefined for the host and unnamed children). **UNTRUSTED-for-display**
* (the spawning model chooses names): redact + bound before rendering; never adjudication input.
* `readonly`, same posture as {@link fromSubagent}. */
readonly sourceAgentName?: string;
/** Runner-filled: present (`true`) exactly when the issuing run is a **delegated child** — ANY
* spawn leg, FORK INCLUDED (the trusted `RunInternals.isDelegatedChild` fact, set unconditionally
* by every core spawn path). It selects which REFUSAL POSTURE a human deny speaks back to this
* run's model: a parent-thread run is told to stop and wait for the user (its transcript has a
* user turn coming), a delegated child is told to adapt or report the limitation. A fork is
* deliberately IN: it inherits the parent's authority (design/110 — which is why the RB-330
* non-fork facet of `effectiveDelegationFacts`, serving the authority/context faces, excludes it), but
* its interaction contract is one-shot — "report once and stop … no waiting for the user"
* (FORK_DIRECTIVE_FRAME) — so a stop-and-wait refusal would instruct it to do the impossible
* (confirmed). NOT the same fact as {@link fromSubagent} either: that is
* the design/153 attribution discriminator (`parentTaskId` presence), which a directly-started
* workflow's children lack even though refusals must still speak the child posture to them.
* `readonly`, trusted (internals never ride a TaskSpec), sync-path-only like its siblings.
*
* Key division, at a glance (the two child-ish keys serve DIFFERENT faces — neither implies the other):
* - `fromSubagent` — ATTRIBUTION: "a nameable parent task issued this" (`parentTaskId` presence);
* drives display/identity surfaces.
* - `isDelegatedChild` — REFUSAL POSTURE: "no user turn ever lands in this run's transcript";
* drives which deny text the model is told (stop-and-wait vs adapt-or-report), forks included. */
readonly isDelegatedChild?: true;
/** Carried from the originating {@link PermissionResult}'s ask
* variant of the same name: `true` ⇒ {@link resolveAsk} must not let an allow NO PERSON JUDGED clear
* this ask — a blanket `onAsk: "allow"`, or an approver's allow it reports as the deployment's policy
* (`settledBy: "policy"`) (see that field's doc). `readonly`, filled by the gate from the decision, never
* a caller/worker-settable field. */
readonly requiresRealApproval?: boolean;
/**
* PRESENCE ONLY, and a FACT ABOUT THIS ASK: no persisted allow rule and no STANDING ANSWER — a release
* remembered from an earlier call, at any scope, a session included — can retire this question; it is
* answered only for THIS call, by an answerer (see the ask-arm member of the same name for who may). ABSENT is not the inverse claim: a person's own explicit `ask` rule and a
* matching allow rule the lane already refused both leave it absent and still block rule clearance
* ({@link ruleOffersAbsence} words them `ask_rule_matched` / `allow_outranked`). The wire twin of the
* {@link PermissionResult} ask-arm member of the same name, minted once by the engine at the gate's
* origin stamp and copied here by the four mint stations. The DOORS are enumerated once, at
* {@link import("./ask-origin.js").ruleClearanceOf}: the tool seat's own egress / irreversibility
* marks, an operator's per-call shell doctrine, the read station's structural demotion or a probe
* that could not answer, a write the protection judge matched or could not resolve, governance
* ({@link requiresRealApproval}), a hook's screening authority, and an ask an ANCESTOR's approver
* decides. Not a door: a person's own explicit `ask` rule, which {@link ruleOffersAbsence} words
* `ask_rule_matched` — a rule they wrote is speaking, and a posture answering it crosses no mandate.
*
* WHY a surface must read THIS member and not its neighbour. {@link ruleOffersAbsence} answers what
* the RULE-OFFER LANE has to give, and that lane is structurally silent for any tool it does not
* speak for, any deployment that wired no rule store and any task that cannot hold a rule — its
* absence is not a claim (its own doc says so). Reading it as "not mandated" made two read faces of
* one command disagree: the same out-of-root read asked as a mandated question on the shell tool and
* as an apparently ordinary one on the monitor tool. This member is a property of the QUESTION,
* present on every ask that carries one, whatever lane is wired.
*
* WHAT IT DOES NOT SAY: that no answerer may answer. An approver — a person, a hook, a deployment's
* blanket `onAsk: "allow"` seat — resolves a mandated ask normally; {@link requiresRealApproval} is
* the stronger, separate demand and is the one the engine's blanket seat refuses. A surface whose
* POSTURE seat declines to auto-answer mandated cards reads this member to know which they are.
* ABSENT is not the opposite claim on an older engine: a consumer that must tell "this engine mints
* no such member" from "this ask is ordinary" reads the engine version, not this seat. Filled by the
* gate, never caller/worker-settable; tightening-only.
*/
readonly mandated?: true;
/** The ask was minted by the deployment's own governance layer (the {@link PermissionResult} ask arm's member of the same
* name, carried through every request mint and onto the park row): a person's question under every mode — no persisted
* rule, standing answer or mode release retires it. A host approval seat that settles asks from a session grant reads
* it to keep its hands off. Filled by the gate, never caller/worker-settable. */
readonly authoredBy?: "deployment";
/** WHICH word of the closed {@link import("./ask-origin.js").PERSISTED_RULE_MANDATES} set {@link mandated} stands
* on — present ⇔ that table word is the bit's WHOLE reason (`probe_mandate` = a read outside the session's
* directories). ABSENT beside `mandated: true` = another door holds too (governance, a hook, an ancestor's
* approver, a policy's own claim) — absence never names a word. Stamped by the gate beside
* the bit, copied by the four mint stations; the park row carries the same word. Never adjudication input. */
readonly mandate?: import("./ask-origin.js").PersistedRuleMandate;
/** Present ⇔ this ask is the classifier DENIAL-LIMIT fallback (see the
* {@link PermissionResult} ask-arm member of the same name): the counts that tripped the bound and
* this ask's own auto-deny window. Two readers: a card renders it as a countdown; {@link resolveAsk}
* arms its deadline from `autoDenyAfterMs` (> 0 ⇒ an unanswered function approver auto-denies at
* that deadline — the `denial_limit_window_expired` settlement on the gate record). Filled
* by the gate from the decision, never a caller/worker-settable field. This is the ARMED type: the
* decision's member carries the counts alone, and the only two ways to reach this one are the route
* stations — `AutoModeDenialTracker.armTimedWindow` (a hand-out to a live function approver, the one
* writer of a non-zero window) and `unarmedWindow` (every route that cannot count down). An adapter
* that forwards a decision's member here does not compile, which is the point: the window is the
* route's fact, and forwarding it silently is how a configured window becomes a wait with none. */
readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
/** The {@link PermissionResult} ask-arm member of the same name (class `classifier_no_ruling`), carried by the four mint
* stations; the `message`'s second paragraph says the same for a client that does not read this. */
readonly classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
/** (additive) WHICH AUTHORITY raised this ask — the wire twin of the {@link PermissionResult} ask-arm
* member of the same name, one word from the closed {@link import("./ask-origin.js").AskOrigin} set.
* The gate's own mint station copies the engine-stamped word; the three inherited-lane stations
* derive it from the ancestor's decision under the station facts (a self-declared value on that
* decision is never read). An approver renders the card by it; a durable park persists the same word
* on its row (`PendingAction.tool_approval.origin`). Filled by the mint station, never written by a
* caller/worker directly — a policy can only STEER the derivation toward a stricter class by
* self-declaring one of its tighten-only inputs (`decisionReason: "hook"`, `matchedAskRule`,
* `denialLimitFallback`), never toward `policy`; a card that attributes an ask to a hook or a rule is
* therefore reading the deployment's own claim about its own ask, which is the direction that cannot
* loosen anything. */
readonly origin?: import("./ask-origin.js").AskOrigin;
/** The ask's CLASS: the first member of {@link classes} in {@link import("./ask-origin.js").ASK_CLASSES} order — the
* row key of {@link import("./ask-origin.js").MODE_ARBITRATION} a permission mode reads. Filled by every station that
* fills {@link origin}, from the same classification the gate's lanes arbitrated this ask by; never written by a
* caller or worker (a policy's self-declared value is replaced at the stamp), and a policy can move it only through
* the tighten-only inputs the classification reads. ABSENT = unclassified: a request from a station that fills no
* {@link origin} (the shared-artifact read confirmation), or a request built by a deployment itself. An approver
* that answers by class treats absence as a class it will not answer. */
readonly askClass?: import("./ask-origin.js").AskClass;
/** Every class this ask belongs to, in {@link import("./ask-origin.js").ASK_CLASSES} order, `classes[0] ===
* askClass`. One ask can carry several (an org verdict beside a hook's question); an approver that answers by class
* reads the whole set — a class it will not answer anywhere in it is a question it does not answer. Present exactly
* when {@link askClass} is. */
readonly classes?: readonly import("./ask-origin.js").AskClass[];
/** Engine-judged risk axes of the action awaiting approval, for the human decision surface: a
* coarse `requiresRealApproval` cannot tell "cannot be undone" from "data leaves the machine",
* and those call for different scrutiny. Filled by the gate from the SAME resolved axes the
* safety tightens read (tool declarations + caller `toolAxes` folds) — never derived from tool
* names, never caller/worker-settable. `irreversible: true` covers both the declared-`"always"`
* and the `"maybe"` (unproven-reversible) tiers. ABSENT — the whole object or either member —
* means the engine did not judge that axis: render it as unjudged, never fold absence to `false`. */
readonly riskAxes?: {
readonly irreversible?: boolean;
readonly egress?: boolean;
};
/** Which approval card this ask renders under — the closed set `TOOL_APPROVAL_CARDS`, `generic` when the tool declares
* none; the same word the parked gate and the pending row carry for the same call. Display routing only: the card is
* untrusted-for-display input (a tool may declare any non-reserved card), so a specialised card validates the call's
* arguments itself. A word outside the set reads as `generic`. ABSENT: an ask the host built itself, or an engine
* older than this member. */
readonly approvalCard?: import("./tool-face.js").ToolApprovalCard;
/** ruled 2026-08-04 — present exactly when this ask was raised inside a DELEGATED child whose approver
* is the ancestor run's (the `ToolExecuteContext.onAsk` seat the delegation tool forwards). Additive:
* an approver that ignores it is unaffected; a durable park/suspend flow never invokes `onAsk` at all,
* so this — like `principal`/`sourceTaskId` — exists only on the synchronous resolution path.
* See {@link AskDelegationProvenance} for the trust posture of each member. */
readonly delegation?: AskDelegationProvenance;
/**
* PRESENCE ONLY: the command/argument text this ask is about carries at least one DIRECTIONAL
* format control ({@link BIDI_CONTROL_RE} — the closed set: U+061C, U+200E/U+200F, the embeddings
* and overrides U+202A–U+202E, the isolates U+2066–U+2069). Those characters change nothing about
* what EXECUTES and everything about what a terminal or a card SHOWS: bytes that run
* `rm -rf /` can display as a benign line, which is precisely the deception an approval surface
* cannot afford to render unannotated.
*
* A WARNING BIT, not a verdict and not a transform. Core does NOT strip, reorder, refuse or rewrite
* anything on account of it — {@link args} is delivered byte-identical either way, and the decision
* stays the approver's. What a surface owes the person is a visible "the text below can display
* differently from what it runs" and a rendering that neutralizes the controls (see
* {@link import("./permission-rule-model.js").renderUntrustedCommandText} for the display baseline).
*
* Read it as PRESENCE-or-nothing: the field is either `true` or ABSENT. It is never written `false`,
* because absence means "not detected", which honestly covers both "clean" and "the bounded scan did
* not reach it" (a pathologically deep or huge argument graph stops at the scan budget) — a `false`
* would claim a proof the scan does not offer. Judged over the EXECUTING argument snapshot and the
* tool's own {@link preview} projection, i.e. the payload; {@link message} is deliberately out of
* scope (engine/policy-composed prose, not the thing that runs).
*
* Filled at the `resolveAsk` chokepoint, beside {@link boundInputHash} — every wired `onAsk`
* approver call crosses it, so an ask mint site added later is covered by construction. Optional
* on the type because a deployment may invoke its approver function directly.
*
* SCOPE, stated so absence is not read as a clean bill on the other routes: this is the
* SYNCHRONOUS `onAsk` ask, like `principal`/`sourceTaskId` — the closure claim is over AskRequest
* mint sites, not over every approval callback. The other two human-decision routes:
* ① a durable park/suspend never invokes `onAsk` and never mints an AskRequest — its twin of this
* bit is the PERSISTED `PendingAction.tool_approval.hasBidiControls` on the parked row (judged at
* the park mint over the row's own fidelity-projected args snapshot + preview, so the two faces
* can honestly differ on a shape-changing backend), projected to the inbox as
* `CheckpointSummary.hasBidiControls` — see those fields in `checkpoint-store.ts` for the reading
* contract; absence there is still "not detected", never a clean bill, and an inbox rendering
* `RiskDescriptor` still owes its own screen (or the display baseline above);
* ② {@link createApprovalPolicy}'s
* `approve` seat resolves in-place inside the policy and mints no AskRequest at all — its
* callback receives a bare `ToolCallRequest` (no bit, no {@link boundInputHash}, none of this
* surface), so a deployment doing HITL through that seat must render through
* {@link import("./permission-rule-model.js").renderUntrustedCommandText} or run its own screen.
*/
readonly hasBidiControls?: true;
}
/**
* How an `ask` decision is resolved when a policy/hook requests human confirmation (design/37):
* - `"deny"` (and the default when omitted) — **headless auto-deny**: no approver, so `ask` resolves
* deterministically to `deny` with a model-readable reason. The safe default for stateless automation.
* - `"allow"` — auto-approve every `ask` (e.g. a trusted batch run).
* - a function — await an operator's decision (true=allow, false=deny). `signal` fires when the task
* aborts AND (design/384, when the gate threaded a per-call signal) when the asking turn is
* interrupted — a bare user halt or a steer-now boundary cut releases the ask exactly as the run's
* own end does; race your wait against it so an unanswered ask is released at the deadline, not hung.
* Since design/384 the engine no longer waits for you after the signal fires: `resolveAsk` races its
* await against the signal and settles the gate as an abort-family deny on its own. Your still-pending
* promise is DETACHED — a resolve the released wait never consumes is you releasing your wait, never
* a verdict (an unconsumed approval is disclosed as a notice, not honored — no claim is made about
* which settled first); an unconsumed reject is disclosed on the deployment's error face, never
* silently swallowed. Cleanup of anything you hold for the wait (the request
* payload, the signal, your own timers) is YOUR responsibility, keyed on the signal's abort.
* G1 three-value: the function may also return `"unavailable"` — an affirmative "no operator
* is reachable for THIS ask right now" (judged PER-ASK inside the callback, not at wire time). It is a
* ROUTING verdict, not a decision: the gate re-routes the ask onto the durable park leg (same behavior
* as if no live approver were wired — checkpoint + suspend, the 1.293 fallback), and when no park
* facility is armed the ask falls to the headless auto-deny (fail-closed). This lets a durable
* deployment wire ONE callback that serves live synchronous three-choice cards while attached AND
* still lands asks in the durable approval inbox when the operator goes away.
*
* **Contract:** the function MUST be fail-closed (a throw → deny, surfacing the real cause), idempotent
* for a given request, and tolerant of being called after the task signal aborted. `"unavailable"` MUST
* only be returned when NO operator interaction happened (a partial interaction that then reported
* unavailable could double-resolve the same approval once the park re-surfaces it).
*/
export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal) => AskOutcome | Promise<AskOutcome>);
/**
* What an `onAsk` approver may return (object arm):
* - `boolean` — plain verdict (true=allow, false=deny). STRICT fold (RB-482 #17, server bridge
* zero-ripple confirmed): only the literal `true` approves; an out-of-contract truthy from an
* untyped bridge is a fail-closed deny naming the defect (the historical truthy leniency was a
* fail-open on the security face with no live producer).
* - `"unavailable"` — the G1 per-ask routing verdict (see {@link OnAsk}).
* - `{ allow, updatedInput?, settledBy?, reason? }` — verdict PLUS an operator EDIT of the presented args
* (whole-replacement form, e.g. ctrl+g "edit script in $EDITOR"): the human approved a MODIFIED
* action, and executing the un-edited args would betray that consent. `allow` folds STRICTLY
* (`allow === true`) — an object arm is a deliberate caller, so no truthy leniency — and
* `updatedInput` is honored only on allow (a deny edit never executes anything). The edit is
* applied as the resolved decision's own rewrite: the gate re-validates it against the tool
* schema exactly like a hook/policy rewrite.
*
* #114② (2026-08-09) — `settledBy` on the object arm is the SYNCHRONOUS leg's only channel for saying
* what ended the wait. This leg's window belongs to the HOST (`onAsk` is a callback the deployment
* owns; the engine starts no timer for it — with the ONE #548 exception below), so a host that timed
* its own approval card out could
* previously only report a plain `false`, which the engine correctly recorded as `"human"` — a person
* refusing. The three words the host may self-report:
* - `"human"` — a person answered (identical to omitting the field);
* - `"timeout"` — the host's own approval window elapsed with no answer;
* - `"policy"` — the deployment's OWN policy answered, either way: a machine decided, not a person (an
* unattended lane answering by class, a headless lane whose approver seat exists but can show nobody a
* prompt). Its allow settles `policy_allowed`, its refusal `policy_refused`. Spelled `true` / `false`, such
* an answer filed a PERSON's record, and its refusal told the model the USER did not want to proceed and to
* wait for their direction — a sentence about a person, in a run where none is coming; with the word the
* refusal reads {@link policyRefusalMessage} instead, and the note travels as the policy's words. A
* policy's allow is the same allow a person's is — the edit, the `readRootGrant` and every lane after the
* gate treat it alike — and only the record names a different party, with ONE exception that is about the
* party itself: an ask carrying {@link AskRequest.requiresRealApproval} demands a person, and a policy's
* allow is refused there exactly as the blanket `"allow"` seat is (`blanket_allow_refused`). OMITTING the
* field still reads as `"human"`, so an unrevised host is unchanged.
* `"timeout"` is legal ONLY with `allow: false` — an elapsed window is a wait nobody answered, and it cannot be
* what APPROVED an action — so `{allow: true, settledBy: "timeout"}` is refused fail-closed.
* `"aborted"` is deliberately NOT accepted here — that word names the engine's OWN fail-closed ends
* (abort, throw, unavailable, out-of-contract value), each already stamped at its own arm, and a
* self-reported one would let a host relabel its refusal as an engine failure.
*
* The ONE ask on this leg the engine DOES time: a classifier denial-limit fallback (the ask
* carrying `denialLimitFallback` with a non-zero `autoDenyAfterMs`). Its window is core's, because
* the fallback exists to bound a classifier that would otherwise deny without end. When it elapses
* the approver's promise is DETACHED (design/384: the wait is released, not cancelled — an approver
* that answers afterwards is not consulted, and an `allow` it returns late becomes the
* `task.late_approval` notice), and the engine's own deny settles as `denial_limit_window_expired`
* (the host's own elapsed window settles as `approval_window_expired` with `who.party: "host"`).
*
* `reason` — the SYNCHRONOUS leg's seat for a model-readable reason attached to a deny, the exact
* counterpart of the durable leg's `ResumeOutcome` `policy_ask` `reason` ("Model-readable reason
* attached to a deny"): the words the decider typed with their "no" (an approval card's rejection
* input), relayed to the model so it can follow the person's direction instead of guessing. In-place
* conditions, all screened at {@link resolveAsk}:
* - meaningful ONLY on a deny (`allow: false`); on an allow the seat is NEVER READ (accepted and
* unused, literally: a junk value or a throwing accessor riding an approval cannot
* destroy the approval). Same net posture as the durable leg, where a `reason` on an `allow` row
* is never rendered;
* - UNTRUSTED input (a human's free text riding a callback): it reaches the model only inside a
* `delimitUntrusted` fence, the same containment the durable resume's reviewer note gets, and
* bounded to `REVIEWER_NOTE_MAX_BODY` code points (an oversized note arrives truncated with an
* ellipsis, never verbatim and never silently dropped — both legs share the one bound);
* - on a deny, a present non-string is refused loudly (fail-closed deny naming the defect — the
* domain the durable leg enforces pre-CAS for the same field), a THROWING read is a fail-closed
* deny naming the true cause (never a raw rejection out of the gate), and the empty string reads
* as absent (truthiness, the durable consumer's own read).
*
* `approver` (design/252 G-7) — the ATTRIBUTION seat: the identifier the approval channel reports for
* the party that ended this wait. Core does not authenticate it, compare it, or read it back to decide
* anything; it transcribes it onto the settlement observation next to `settledBy`, so an audit that can
* already say "a person ended this wait" can also say which one, without core growing an identity
* surface it deliberately does not have. Read the two together: `settledBy:"policy"` with an `approver` names
* WHICH of the deployment's answering seats decided (a closed word the host owns, never a person).
* Screened, not trusted (see {@link screenApproverAttribution}): a non-string, an over-long value or one
* carrying control characters is a loud fail-closed refusal at {@link resolveAsk}, the same posture this
* seam takes for every other out-of-contract value — and the same reason, since a defective attribution
* that were quietly dropped would leave a settlement looking unattributed rather than misreported. It is
* carried on the DECIDER arms (a person's or a policy's allow or refusal) and never on an end the engine
* produced (an abort, a throw, an unclonable edit): nobody approved those. A host's own elapsed window has
* no attribution seat on its record (`approval_window_expired` names the host as a party, not a seat).
*
* `readRootGrant` — "yes, and add this directory": the card's {@link AskRequest.readRootCandidate} `dir`, sent back
* byte for byte (`shell.read_boundary.grant_candidate_is_the_grant`). Beside `allow: true` on an ask carrying that
* candidate, THIS run's read directories include it from then on (read faces only; `covers: "exact"` = that one
* path) — announced `approval.read_root_granted`. Anything else changes nothing and is announced
* `approval.read_root_grant_rejected`; the verdict it rode stands. A seat with no card (a policy's `approve`)
* refuses it. Keeping the directory beyond this run is the deployment's (`TaskSpec.additionalReadDirectories`).
*/
export type AskOutcome = boolean | "unavailable" | {
allow: boolean;
updatedInput?: unknown;
settledBy?: "human" | "timeout" | "policy";
reason?: string;
approver?: string;
readRootGrant?: string;
};
/**
* ruled 2026-08-04 — forward an approver into a delegated child, stamping every ask it raises with the
* delegation it came from ({@link AskDelegationProvenance}).
*
* Only the FUNCTION form is wrapped: `"allow"`/`"deny"` are policy SETTINGS, not approvers, and
* {@link resolveAsk} reads them structurally — wrapping would invent an approver where the deployment
* declared a blanket mode.
*
* An ask that ALREADY carries provenance passes through untouched: chained down a delegation tree, the
* INNERMOST (actually issuing) frame is the one the approver must see, and each outer hop would
* otherwise overwrite it with its own shallower one.
*/
export declare function withDelegationProvenance(onAsk: OnAsk, delegation: AskDelegationProvenance): OnAsk;
/**
* ruled 2026-08-04 — the APPROVER BEHIND a possibly-forwarded `onAsk`: the identity that answers
* "is this the same human/UI?" across delegation hops.
*
* Consumers compare approvers by function reference to decide whether one consultation's consent covers
* another (the RB-40 duplicate-frame collapse). A provenance wrapper is a different function object but
* the same decision-maker, so a bare `===` would ask one operator the same question twice for one tool
* call. Never used for authority (it grants nothing); only for "these two consultations are the same
* consultation". A non-wrapper value (including `"allow"`/`"deny"`/undefined) is its own identity.
*/
export declare function askApproverIdentity(onAsk: unknown): unknown;
/**
* Is this `onAsk` seat a LIVE APPROVER — a person the run can actually reach?
*
* Only a function is. A blanket `"allow"`/`"deny"` string is a policy setting (the same reading the
* interaction-posture door gives it), and an absent seat is nobody at all; neither can answer an ask, so
* neither may outrank a durable park.
*
* ONE definition on purpose. Two sites must agree on this — the run's own sync-first decline, and the
* mandate frozen into the chain its children inherit — and when they were two hand-written booleans they
* drifted: the decline narrowed to the function test while the freeze kept testing mere presence, and
* each still read correctly on its own, so nothing pointed at the pair. A blanket seat was therefore not
* an approver for the parent's asks and an approver for its children's. Declared here, in the vocabulary
* beside {@link askApproverIdentity}, because the two sites are now two prepare phases (the inherited-gate
* fold and the wiring station) plus the orchestrator, and each reaches DOWN for the one definition.
*/
export declare function isLiveApproverSeat(onAsk: OnAsk | undefined): boolean;
/** Where a durable-approval MANDATE on a leg comes from — the closed set of its two sources. */
export declare const DURABLE_MANDATE_SOURCES: readonly ["force_durable_gate", "durable_approval"];
export type DurableMandateSource = (typeof DURABLE_MANDATE_SOURCES)[number];
/**
* Is every interactive permission ask of this leg MANDATED to gate durably — and by which source?
* `force_durable_gate`: the per-principal control-plane entitlement, in force regardless of any seat.
* `durable_approval`: the task's own opt-in with NO live approver seat (a blanket string or nobody at all
* cannot answer an ask, so the park is the only honest resolution). A live seat beside the opt-in is not a
* mandate: the leg's asks — and its children's, at the same frozen seat — resolve there.
*
* ONE definition on purpose, like {@link isLiveApproverSeat} beside it: the chain a parent freezes for its
* children and the loud "mandated but no store" disclosure at the wiring station used to derive the same
* fact with two hand-written conditions, and the second one read only the entitlement — a task that opted
* in and had no store degraded to the live chain in silence.
*/
export declare function durableMandateSourceOf(args: {
forceDurableGate: boolean;
durableApprovalDeclared: boolean;
onAsk: OnAsk | undefined;
}): DurableMandateSource | undefined;
/**
* Has this leg DECLARED a durable park opt-in, from EITHER source (the per-principal
* entitlement or the task's own `durableApproval`) — with NO live-seat narrowing. Deliberately
* NOT {@link durableMandateSourceOf}: that one asks "is every ask MANDATED to gate durably" (a
* live approver seat beside a mere `durableApproval` opt-in defeats the mandate), while this one
* asks "does this leg have a durable park FACILITY at all" — the question-face mount decision and
* the inherited-mandate float both need the bare declaration, seat or no seat, and had each spelled
* `forceDurableGate === true || durableApprovalDeclared` by hand (two call sites, `prepare-
* question-face.ts` and `prepare-policy-chain.ts`, one drifting from the other silently if either
* grows a third source or a typo'd operator).
*/
export declare function durableParkOptInDeclared(args: {
forceDurableGate: boolean;
durableApprovalDeclared: boolean;
}): boolean;
/** EXACT structural clone for the approval boundary (alias isolation) — fail-closed, never lossy,
* never identity: a JSON round-trip would silently DROP unclonable members (presented ≠ executing =
* consent misrepresentation) and an identity fallback would hand the approver the live executing
* object (in-place mutation + TOCTOU). Tool args are model-JSON in practice, so failure is exotic —
* and exotic means deny, not leak.
*
* Exported for the DURABLE half of the same boundary (the runner's park mint): a checkpoint row holds
* the args a human will approve and a resume will execute, so it needs exactly this snapshot — and
* exactly this refusal when the value cannot be one. Not part of the package's public surface
* (index.ts does not re-export it); one implementation so the two halves cannot drift on what
* "safely presentable" means. */
export declare function tryCloneArgs<T>(v: T): {
ok: true;
value: T;
} | {
ok: false;
reason: string;
cause?: unknown;
};
/** The ONE mint of a refusal's reported args: the deny carrying the args its refusing station or layer JUDGED, which the gate's
* exit reports (`finalInput`, the deny observer's `input`); a deny already carrying the args its own layers judged (a composed
* chain's, nested ones included) keeps them. Read by the gate's lanes and by {@link combinePolicies}. Only a deny is changed. */
export declare function refusalOf<D extends PermissionResult>(deny: D, judged: unknown): D;
/**
* A PreToolUse hook's `ask` folded over a verdict by `deny > ask > allow` — the ONE law for the gate's first screening and for
* an approved edit's re-screen. A deny stands. Over an allow, the hook's own ask, RECONSTRUCTED rather than spread (its message,
* its real-approval demand, its rewrite) under the hook provenance the engine stamps. Over an ask, that ask keeps every member,
* takes the hook provenance and the hook's real-approval demand, and its message carries the hook's beside its own — a person
* clearing one ask that stands for two gates sees both reasons.
*/
export declare function foldHookAsk(verdict: Exclude<PermissionResult, {
action: "deny";
}>, hookAsk: Extract<PermissionResult, {
action: "ask";
}>): Extract<PermissionResult, {
action: "ask";
}>;
export declare function foldHookAsk(verdict: PermissionResult, hookAsk: Extract<PermissionResult, {
action: "ask";
}>): PermissionResult;
/**
* THE args of one tool-gate pass — what every judge of the call reads and what the call runs with: ONE engine-owned
* value, the {@link tryCloneArgs} snapshot of what was handed in, deep-frozen. The gate mints it at its entry from the
* model's args, and every value that replaces it comes through here too (a hook's or a policy's rewrite at the decision
* receipt, a person's edit at the approver's answer), so the args of a pass are always such a value. Because it cannot
* change, it is handed to every judge as it is — a policy, the org and stored-rule lanes, the classifier, an extension's
* check, the person's card — and none of them can make what runs differ from what was judged: an in-place write throws
* (a rewrite is an answer's `updatedInput`). An owned value handed back is itself; a primitive is immutable as it is.
* A value the snapshot refuses is refused the same way (exotic args are a refusal, never a shared reference).
*/
export declare function ownArgs<T>(v: T, root?: string): {
ok: true;
value: T;
} | {
ok: false;
reason: string;
cause?: unknown;
};
/** A call's argument reading (the loop's: the tool's preparation, the schema's coercions) whose every value is engine-owned
* like a pass's args ({@link ownArgs}) — the gate's face seat and the resumed call's belt seat read through this one. */
export declare function ownedReading(read: (args: unknown) => unknown): (args: unknown) => unknown;
/**
* A label for a value that was THROWN at us, safe to put in a fail-closed refusal. Never coerces: `String(x)`
* throws on a null-prototype object or a untrusted `toString`/`Symbol.toPrimitive`, and the thrower here is
* deployment-supplied (a getter inside the args can throw any object at `structuredClone`, an approver can
* throw one at the gate). A coercion failure inside the catch would escape `resolveAsk` as a raw TypeError —
* turning a classified fail-closed DENY into an unclassified crash, i.e. the failing input escaping the very
* classification the refusal exists to give.
*
* The result is CONTAINED, not just described. Every caller interpolates it into a refusal that reaches
* the model inside a `<system-reminder>` frame, and the text is not ours: a `DataCloneError` renders the
* offending value INTO its message (`structuredClone` of a function yields that function's SOURCE), and
* a deployment approver throws whatever it likes. A rewrite handing over a function whose body contains
* `</system-reminder>` would otherwise close the trusted frame and continue as instructions — the exact
* threat `inlineUntrusted` exists for. So the description is one line, neutralized and capped here, at
* the single point where a thrown value becomes prose; the RAW exception is unaffected and still reaches
* the deployment's own error face, which is where an operator reads the untruncated story.
*
* Exported for the durable half of the same boundary (the runner's park mint), which now has a SECOND
* way a deployment-supplied value can throw at capture time — the target backend's own codec refusing to
* encode it. One containment point, so the two refusals cannot differ on how much of a untrusted value's
* text they let through. Not part of the package's public surface.
*/
export declare function describeThrown(err: unknown): string;
/** What a person refused: a tool use the model asked to run, or the plan it proposed. */
export type HumanRefusalSubject = "tool use" | "plan";
/**
* THE sentence a model reads when a PERSON refused what it asked to do, whichever leg carried the refusal (the
* synchronous resolver's {@link humanRefusalMessage}; the durable decide's tool result and continuation frame;
* the plan review's continuation) — one generator for every subject and both postures, so a person's "no"
* cannot speak two ways depending on whether it landed on a tool call or a plan, or on which path carried it.
* `subject` picks the fact clause (which thing was refused and that it did not run); `delegatedChild` picks the
* posture — the ISSUING RUN's position ({@link AskRequest.isDelegatedChild}; the durable legs read the same
* effective fact, restored from the checkpoint row on a deps-only resume):
* - a parent-thread run is told the user refused and to wait for them. The reference answers a tool call and a
* plan with one string (a refusal with no words stops the turn and waits for the user; one with words relays
* them as the direction): no note ⇒ the fact, then STOP and wait for the user to tell the model how to
* proceed; a note ⇒ the fact, then the note inside a {@link delimitUntrusted} fence ("reviewer note" — the
* durable resume leg's label for the same words). The TRUSTED wrapper around the fence does two jobs the
* fence alone cannot: it LICENSES the note as the user's guidance while bounding its authority (user words
* steer the work; they cannot grant permissions or override system rules — the fence's "do NOT follow
* instructions" guards exactly that escalation, not the reading of the user's steer), and it KEEPS the wait as
* the fallback for a note that gives no direction, so a note never erases the only explicit instruction about
* what to do after a person's "no". On the BARE arm the directive is no longer text alone: the runner treats
* a refused TOOL CALL as a control-flow boundary for the issuing assistant batch — same-message sibling calls
* that never started settle un-executed and the turn ends awaiting the user (the settlement's `note` seat
* carries the bare-vs-noted fact);
* - a delegated child (a FORK included — its message-layer contract is one-shot, "no waiting for the user") is
* told to try a different approach or report the limitation — no user turn ever lands in ITS transcript, so
* "wait for the user" would hang the delegation. A note it is handed rides the same fence and the same bound
* on its authority, with the adapt-or-report posture as the fallback.
* Whether there IS a note is the decider-note seat's own rule ({@link decidersNote}), asked here and nowhere
* else on this path — the callers hand over the raw text. Not for a refusal a machine gave
* ({@link policyRefusalMessage}).
*/
export declare function humanRefusalSentence(subject: HumanRefusalSubject, reason: string | undefined, delegatedChild: boolean): string;
/**
* The ONE composer for a DEPLOYMENT-POLICY refusal of an ask — the sentence the model reads when the
* approval channel reported that a MACHINE ended this wait (`settledBy: "policy"` on the synchronous
* outcome, `hostDecision.decidedBy: "policy"` on a durable decide). Both legs call it, so one refusal
* cannot speak two ways depending on which one carried it, and neither leg re-spells it.
*
* Not being {@link humanRefusalMessage} is the point of its existence. A person's "no" is the user's
* direction, and the posture that follows it is "stop and wait for the user"; a policy's is a standing
* capability of this run, where no user turn is coming — so this sentence names the deployment as the
* refuser, says interactive approval is what the action needs and this context has none, and directs the
* model at the work it CAN still do. A note rides a fence labelled as the POLICY's words, never licensed
* as the user's guidance, because no user gave it. `askMessage` is the ask's own prompt where the caller
* has one (the synchronous leg does); a durable decide answers a call parked in an earlier leg and has
* none, and the head then simply names the call.
*/
export declare function policyRefusalMessage(toolName: string, askMessage: string | undefined, note: string | undefined): string;
/**
* A {@link resolveAsk} result: always a TERMINAL `allow`/`deny` (never `ask`). `approverUnavailable`
* is the out-of-band G1 three-value marker: the live approver returned `"unavailable"` for this ask —
* the carried `deny` is the FAIL-CLOSED FALLBACK, and the gate may instead re-route the ask onto the
* durable park leg (the only consumer; everywhere else the result reads as a plain deny).
*/
export type ResolvedAsk = (Extract<PermissionResult, {
action: "allow";
}> & {
/** The EXACT data snapshot the approver was shown (the `structuredClone` of the presented args). On
* a plain approval (no edit) the gate EXECUTES this snapshot, not the original object: a stateful
* getter or an external alias must not be able to make the executed action differ from the
* approved one (shown == executed, by construction). Set only on the function-approver path
* (string modes present nothing). */
presentedInput?: unknown;
approverUnavailable?: never;
/** The answer's `readRootGrant` ({@link AskOutcome}), screened against the request's candidate. */
readRootGrant?: ReadRootGrantScreen;
}) | (Extract<PermissionResult, {
action: "deny";
}> & {
presentedInput?: unknown;
approverUnavailable?: true;
/** A grant riding a refusal: always `rejected` (`on_refusal`). */
readRootGrant?: ReadRootGrantScreen;
});
/** What one resolver ARM composes: the resolution plus the settlement it ended the wait with — composed at the arm, every deny
* naming one (there is no unclassified refusal), an allow only when a DECIDER approved (a blanket `"allow"` asked nobody). The
* exported {@link resolveAsk} turns the settlement into the resolution's `attestation`. Not re-exported from `src/index.ts`. */
export type ResolverArmResult = (Extract<ResolvedAsk, {
action: "allow";
}> & {
settlement?: Settlement;
}) | (Extract<ResolvedAsk, {
action: "deny";
}> & {
settlement: Settlement;
});
/**
* The DIRECTIONAL format controls, as a closed set (see {@link AskRequest.hasBidiControls}):
* ALM (U+061C), LRM/RLM (U+200E/U+200F), the embeddings and overrides (U+202A–U+202E) and the
* isolates (U+2066–U+2069). These reorder what a reader SEES without changing a byte of what runs.
*
* Why the whole directional family and not only the four the reordering demos use: the bit is a
* WARNING, and for a warning the safe direction is to fire on a member a reader would have to
* enumerate correctly to be safe from. ALM is in for that reason, though it is the least-abused one.
*
* Deliberately NARROWER than `permission-rule-model`'s `CONTROL_CHARS_RE` and than
* {@link APPROVER_REFUSED_CHARS_RE}: those are REFUSAL/ESCAPE classes covering every control and
* zero-width character (a rule text or an identifier has no use for any of them). This one answers a
* different question — "can this display as something other than what it is" — and a zero-width joiner
* or a soft hyphen does not reorder anything, so folding them in would make the bit fire on ordinary
* text and mean nothing where it matters.
*/
export declare const BIDI_CONTROL_CHARS = "\\u061C\\u200E\\u200F\\u202A-\\u202E\\u2066-\\u2069";
/**
* Does any string reachable in `value` carry a {@link BIDI_CONTROL_RE} member? Bounded, cycle-safe,
* and never throwing — every caller is on an approval/projection path, where a scan that failed must
* degrade to "not detected" rather than turn an ask into an error (the `deliverEngineNotice`
* posture: a derived disclosure must never become the failure of the thing it describes).
*
* OBJECT KEYS are scanned as well as values: a key is displayed text too, and an argument object
* `{ "cmd<RLO>": … }` renders its own reordering in any card that prints the shape (the marker
* is spelled out here on purpose — a literal one in this comment would reorder the comment).
*
* `limits` parameterizes the two budgets WITHOUT opening a second reader: the character class and
* the traversal stay defined exactly here, only the allowance varies by caller. Omitted ⇒ the mint
* tier above (the resolveAsk chokepoint and the durable park mint, which scan one call's own
* payload). A per-row projection caller (the checkpoint summary backfill, which may face
* deployment-written rows of arbitrary size on a many-row read) passes its own smaller tier so one
* row's work is bounded regardless of the row's provenance. Both members are required when the
* object is given — a half-specified tier would silently mix two tiers in one scan.
*
* Exported as the ONE judge of the question: a host that renders or sanitizes text next to an approval
* card answers "does this carry a directional control" with this function, and composes a wider class
* (control characters, line separators) from {@link BIDI_CONTROL_CHARS} rather than restating the members.
* The allowances above are part of the answer, not an implementation detail: a payload past them reads
* "not detected", exactly as the bit on the card does.
*/
export declare function carriesBidiControls(value: unknown, limits?: {
maxNodes: number;
maxChars: number;
}): boolean;
/**
* Resolve an `ask` decision to a terminal `allow`/`deny` via {@link OnAsk}. Centralizes the headless
* auto-deny default, fail-closed error handling, and stable deny reasons so every ask site is
* consistent. Never returns `ask`.
*
* Every arm that ENDED A WAIT composes the {@link import("./gate-outcome.js").Settlement} for it, at the
* arm: the approver's own verdicts settle as a person's unless the outcome names another word on
* {@link AskOutcome.settledBy} (its own window, its own policy), and the fail-closed ends around them
* (abort, a throw, an "unavailable" report, an unclonable presentation, an out-of-contract value) settle
* as the engine's own nobody-ended family; a policy's allow on a `requiresRealApproval` ask is refused as the
* blanket seat's is ({@link unjudgedAllowRefused}). The three POSTURE arms at the top — blanket `"allow"`, its
* `requiresRealApproval` refusal, and the headless auto-deny — name no person and no window on purpose:
* nobody was asked, so there is no wait for anyone to have ended, and `decisionReason: "mode"` is already
* the honest word for what produced them.
*/
export declare function resolveAsk(req: AskRequest, onAsk: OnAsk | undefined, signal?: AbortSignal,
/** design/384 — observer for a DETACHED approver's settlement, consulted only after the race
* arm released the wait on `signal`'s abort: `"approve"` = a value that reads as an approval
* went unconsumed by the released wait (a person said yes to an action that will never run —
* the caller turns this into its own disclosure, e.g. an engine notice; no arrival-order claim
* is made); `"error"` = the detached promise
* rejected, or its members threw on the post-release read (the caller's error face, never
* silence). An unconsumed NON-approve resolve is the approver releasing its wait — no verdict,
* no record, this observer is not consulted. Optional and advisory: absent, the detached
* settlement is still swallow-guarded (no unhandled rejection), it just leaves no trace. */
onLateSettlement?: (late: {
kind: "approve";
} | {
kind: "error";
error: unknown;
}) => void): Promise<ResolvedAsk>;