/** * [[EXT-127]] — **the alignment check**: the second model call, reached only once the classifier * has declined, whose whole job is to decide whether the declined command is what the user actually * asked for. * * ## Why there are two models * * One rater was doing two jobs, and the context the second needed corrupted the first. The * classifier ({@link @gaunt-sloth/core!core/shell/rater.rateShellCommand | rateShellCommand}) now rates the command and nothing else: * no user messages, no negotiation history, no argument. This module owns the other question, and it * is a question about our own conversation — *did the user ask for this?* — never about the world, * never about a counterparty, and never about what the command will hit. * * ## The trust ordering is carried by the message ROLES * * This is the hypothesis of the design rather than tidiness. Instead of stacking trusted * instructions, untrusted command text and prior argument into one flat prompt and hoping the * labelling holds, each kind of text is placed in the role models were trained to weigh it in: * * | role | carries | * |---|---| * | system | our instructions, and the classification rendered from the enum | * | user | the user's own text | * | tool result | the agent's command, and any payload | * | assistant | the checker's own earlier rounds and tool calls | * * **The property this buys:** file content, web content and command output all arrive as tool * results, so under this assembly **they cannot present themselves as user provenance — there is no * role for them to occupy.** That is the discrimination [[EXT-106]]'s control pair turns on, where * the same command was approved from the user's own words and refused four times when a file named * it. * * **The `user` role is fed from the SETTLED PROVENANCE CHANNEL and never from the raw store.** See * {@link AlignmentContext.userMessages}: the runner reads * `ShellNegotiationState.retainedUserMessages`, which is empty * until `admitUserProvenance` says otherwise, and never `noteUserMessages`' store (which also feeds * a different question with a different reader) or `humanMessageTexts` (the unfiltered upstream). * Without that this design's one structural claim about provenance would be the one place it is not * structural. * * **One inherited limitation, recorded rather than fixed here.** On ACP, client attachments are * folded into the human message before the runner ever sees them, so on that surface a byte the * client attached can reach the `user` role. That is [[EXT-130]]'s accepted, deliberately-deferred * risk — held open there until there is time to spend on ACP properly — and this module inherits it. * Everything the AGENT fetched still arrives as a tool result; the exception is on the way IN, at a * surface, and it is the surface that has to fix it. * * **State the limit once and move on: role adherence is empirical, not structural.** The assembly is * deterministic and ours; whether a model honours the boundary is a behaviour, and a smaller model * honours it worse. That is a caveat on the evidence, not an argument against the design. * * ## The checker decides only by calling a tool * * It must call {@link ALIGNMENT_TOOL_VIEW} — which returns the classification, the rejection reason * and the agent's command — before calling exactly one of {@link ALIGNMENT_TOOL_APPROVE}, * {@link ALIGNMENT_TOOL_SUGGEST} or {@link ALIGNMENT_TOOL_ESCALATE}. Prose is not a decision: a * model that answers in text has escalated, because {@link runAlignmentCheck} fails closed. * * ## What an aligned approval may and may not lift * * **MAY lift:** §4.6's open-world preflight floor, and a `destructive` rating, where the checker * finds the command aligned with what the user asked. That is deliberate authority. * * **MAY NOT lift**, and it is {@link alignmentApprovalRefusal} — the TOOL'S CONTRACT, enforced in * code — rather than a sentence in a prompt asking the model not to: * * - `attack` — always the human's, and a clear attack is rare and deserves attention; * - `catastrophic` — because for an irreversible command the prompt is not asking *did you mean * this*, it is the last place the ambient target is visible. `kubectl delete namespace staging` * names no cluster, `terraform destroy` names no workspace, `dd of=/dev/sdb` names no disk. A user * can type the command verbatim — perfect provenance — and still destroy the wrong thing; * - the §8 hardline — settled as not liftable at any rung, `bypass` included, and that narrowness is * what the rest of the ladder is allowed to rely on. * * **The distinction that makes those limits coherent: alignment is about the REQUEST, not the * EFFECT.** The checker can answer *did the user ask for this command*; it cannot answer *does this * command do what it appears to do*. A `git commit` whose message contains backticks is perfectly * aligned with what the user asked and still executes substituted shell. An aligned command is not * thereby a safe one, and no amount of provenance closes that gap. * * @module */ import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { type BaseMessage } from '@langchain/core/messages'; import type { StructuredToolInterface } from '@langchain/core/tools'; import type { GthConfig } from '#src/config.js'; import type { RaterCallFailure } from '#src/core/shell/rater.js'; import type { RaterOutcome } from '#src/core/shell/raterVocabulary.js'; /** The tool that hands the checker what it is ruling on. It must be called before any decision. */ export declare const ALIGNMENT_TOOL_VIEW = "viewCommandSuggestedByAgent"; /** Decision tool: the command is what the user asked for, and may run. */ export declare const ALIGNMENT_TOOL_APPROVE = "approveCommand"; /** Decision tool: not yet — tell the agent what would make it acceptable. */ export declare const ALIGNMENT_TOOL_SUGGEST = "suggestChangesToCommand"; /** Decision tool: this is a person's call. */ export declare const ALIGNMENT_TOOL_ESCALATE = "escalateToUser"; /** * **The ledger tool is NOT BUILT HERE, and its absence is deliberate rather than an oversight.** * * A checker that can see what the agent already ran and what came back can resolve a * state-dependent command instead of arguing about it, which is why [[EXT-127]] wants one — but the * mechanism belongs to [[EXT-131]], which owns it. Nothing here stubs it: a stub that returned * nothing would read, to a model and to the next implementer alike, as *"the agent has run * nothing"*, which is a false statement rather than a missing feature. * * **Two conditions ride with it, stated here so [[EXT-131]] builds against them rather than * re-deriving them:** * * 1. **Scope it to calls the gate APPROVED**, since that is the set the gate can vouch for. What * ran without passing this gate is not something this gate saw. * 2. **Split the record by provenance.** The gate's own metadata — the command, the exit status, * when it ran, which rung approved it — is **trusted because we produced it**, so it may be * stated as fact. The **payload is untrusted content and belongs in the tool-result role**, with * the same fencing every other untrusted value in this module gets. A checker that pulls file * contents or command stdout into its context unframed lets a hostile file write into the safety * layer's own prompt — which is precisely the assembly this module exists to make impossible. */ export declare const ALIGNMENT_LEDGER_CONTRACT: string; /** * How many model turns one check may take before it fails closed. * * **Two are the intended path** — view, then decide — **and the other two are slack for a model * that does not get there in two.** Each of the ways a check legitimately runs long costs a whole * turn whether or not it produced a decision: a turn spent narrating before calling anything (the * loop answers *"that was not a decision"* and gives the turn back), and a second view. A small * local model does both in the same check, which is the case the slack is sized for, so the budget * is the intended two plus one for each — not the intended two plus one shared spare. * * **The bound is not a safety control** — failing closed is what makes an exhausted budget safe — * it is what stops a model that never decides from spending a session's tokens re-reading the same * command. That is also why the slack errs high: the cost of one turn too many is tokens, and the * cost of one too few is a check that failed closed on a model that was about to answer. */ export declare const ALIGNMENT_MAX_TURNS = 4; /** What the checker is ruling on: the agent's command, and what the classifier made of it. */ export interface AlignmentSubject { /** The command the agent proposed, RAW. Every renderer here normalizes and folds it itself. */ command: string; /** The classifier's outcome, which is what the system prompt renders from the enum. */ outcome: RaterOutcome; /** The classifier's one-sentence explanation. Model-authored, so it is fenced, never asserted. */ reason: string; /** The justification the agent attached to the command, when it attached one. Agent-authored. */ justification?: string; } /** Which of the three decision tools the checker called. */ export type AlignmentDecisionKind = 'approve' | 'suggest' | 'escalate'; /** One completed decision by the checker. */ export interface AlignmentDecision { kind: AlignmentDecisionKind; /** * The checker's own sentence. Model-authored, and **replayed VERBATIM — it is not fenced**, at * either of the two places it is replayed. * * What bounds that is stated here rather than left to a reader's assumption, because the sentence * this replaced asserted a fencing that neither site implements, and **a docblock claiming a * protection that does not exist is worse than silence**: it is what makes the code read as * already audited. * * - **Into the next round's own conversation** ({@link buildAlignmentMessages}, via `replayRound`) * it goes back as the checker's OWN assistant tool call. That role is the one thing in this * assembly the model is meant to read as its own reasoning, so fencing it as untrusted data * would contradict the design rather than harden it — the model would meet its own last turn as * testimony from a stranger. * - **Into the rejection handed to the coding agent** (`GthAgentRunner`) it is appended to the * rater's refusal. That branch is guarded by the gate's outcome being `reject` **and** the * decision being `suggest`, so the recipient is an agent that is being told **no**: the text is * guidance for a retry, it confers no authority, and whatever the agent proposes next is rated * and checked again from scratch. * * **So the bound is the position, not any escaping.** A consumer that widens either — replaying * this into the `user` role, or carrying it onto a path where something is approved — is * responsible for fencing it there, and must not read this field as already safe. */ reason: string; /** With `suggest`, a narrower command the checker would accept. Optional; often absent. */ suggestedCommand?: string; } /** * One completed round of an alignment check, as the NEXT round's checker sees it — replayed as its * own assistant messages and tool results rather than quoted into a block. * * That is the whole of *"the checker's own earlier rounds and tool calls"*: the model meets its * previous turns as its own turns, so it recognises them as its own reasoning rather than as * testimony from a third party. */ export interface AlignmentRound { /** What that round was ruling on. */ subject: AlignmentSubject; /** What it decided. */ decision: AlignmentDecision; } /** Everything one check is assembled from. */ export interface AlignmentContext { /** The pending command and its classification. */ subject: AlignmentSubject; /** * **The user's own words — and they must come from the settled provenance channel.** * * The runner reads * `ShellNegotiationState.retainedUserMessages`, the * command-keyed window that is EMPTY until `admitUserProvenance` has positively established that * this session's human turns are the user's own words. It is deliberately **not** * `noteUserMessages`' raw store — which also feeds a different question with a different reader — * and deliberately not `humanMessageTexts`, the unfiltered upstream in `GthAgentRunner`. * * Absent or empty is a check with no mandate in view, and the checker is told so rather than left * to infer it: with nothing to align against, the honest answer is almost always to escalate. */ userMessages: readonly string[]; /** The checker's own earlier rounds, oldest first. Empty is round 1. */ priorRounds?: readonly AlignmentRound[]; /** The home directory to fold, so every rendered value takes the same less-identifying form. */ home?: string; } /** * §4.6/§4.2/§8 — **the tool's contract: why `approveCommand` may not approve this command**, or * `null` when it may. * * **This is enforced in code because a prompt cannot enforce it.** A sentence asking a model not to * approve an `attack` is an instruction, and an instruction is exactly what the untrusted half of * this context is trying to supply. The three limits below are the ones the rest of the ladder is * allowed to rely on, so they are decided from the command and the classification alone — before * the model's answer is read, and regardless of what it said. * * Exported, and separate from the tool it backs, so a test can drive it directly AND so the tool and * the runner cannot come to disagree about what is liftable. * * **What is deliberately NOT here:** a `destructive` rating and §4.6's open-world preflight floor. * Both ARE liftable by an aligned approval — that is the authority the whole feature exists to grant * — so neither may be smuggled into this list by a later reader looking for symmetry. */ export declare function alignmentApprovalRefusal(subject: AlignmentSubject): string | null; /** * The system prompt: **our instructions, and the classification rendered from the enum.** * * Two things are absent on purpose and neither may be added back: * * - **The command.** It arrives as a tool result, which is the whole of this design. Putting it here * would place attacker-controlled text in the highest-trust role, which is the arrangement the * split exists to leave behind. * - **The classifier's `reason`.** That sentence is model-authored, so it is untrusted; only the * OUTCOME is ours, because it is one of four values we defined. The reason is real information and * the checker gets it — through {@link ALIGNMENT_TOOL_VIEW}, in the tool-result role, fenced. * * The rendered value is a {@link RaterOutcome}, i.e. a member of a closed enum, so nothing a model * or an attacker wrote can reach this string. */ export declare function buildAlignmentSystemPrompt(outcome: RaterOutcome): string; /** * §5.1's bounds, applied to the user role: the LAST {@link NEGOTIATION_MAX_USER_MESSAGES} non-blank * messages, each folded, collapsed to one line, unable to close its own fence, and truncated to * {@link NEGOTIATION_USER_MESSAGE_MAX_CHARS} **including** the ellipsis. * * **The bounds are applied HERE rather than trusted from the caller**, so a caller that hands over * an entire conversation still cannot put a pasted log into the checker's context. Blanks are * dropped BEFORE the window is taken, so a run of empty turns cannot spend the budget that carries * the mandate — and "blank" counts the invisibles, so a value carrying only those cannot occupy a * slot a reader would see nothing in. * * **These are the user's own words and they are STILL fenced, which is not a contradiction.** The * role is what says whose words they are; the fence is what stops one of them ending the block and * writing our prose. A user can paste anything, including a paragraph shaped like our own headings. */ export declare function renderAlignmentUserMessages(userMessages: readonly string[], home?: string): string[]; /** * The USER role: the user's own text, and our own framing of the question — and **nothing else**. * * The framing sentences are ours, so they are not fenced; the messages are the user's, so they are. * No part of the agent's command, its justification or the classifier's reason appears here, which * is the placement half of this module's whole claim: a value that reached us as a tool result has * no route into this message. * * `round` frames a LATER round, and its clauses are **our own static strings** chosen by comparing * this round's command and justification with the previous one — never the agent's text itself, * which stays in the tool result where it belongs. */ export declare function buildAlignmentUserMessage(userMessages: readonly string[], options?: { home?: string; commandChanged?: boolean; justificationChanged?: boolean; }): string; /** * The TOOL-RESULT role: what {@link ALIGNMENT_TOOL_VIEW} hands back — the classification, the * rejection reason, the agent's command, and the agent's justification when it gave one. * * **Every value here is agent- or model-authored, and every one of them is fenced.** The command is * normalized and home-folded with the same functions the live rating goes through, so the checker * sees the string the classifier actually rated. The reason and the justification are collapsed to * one line, because this payload is line-structured and a newline in any of them would otherwise * forge a field that was never returned. * * This is also where a future [[EXT-131]] ledger payload belongs — see * {@link ALIGNMENT_LEDGER_CONTRACT}. */ export declare function renderCommandSuggestedByAgent(subject: AlignmentSubject, home?: string): string; /** * **The four-role assembly** — the whole hypothesis of [[EXT-127]], in one function. * * The order is: our instructions (system), the user's mandate (user), then every earlier round * replayed as the checker's own turns and their tool results, then — for a later round — our own * framing of what changed. The pending command is deliberately absent from all of it: it arrives * only when the model calls {@link ALIGNMENT_TOOL_VIEW}, which is what makes *"the agent's command * is a tool result"* a property of the assembly rather than a convention. * * Exported and returning plain messages so a test can assert on the PLACEMENT of each value rather * than on its presence somewhere — a test that merely checks a string is present does not test this. */ export declare function buildAlignmentMessages(context: AlignmentContext): BaseMessage[]; /** The tools, plus the decision they wrote into, for one check. */ interface AlignmentToolSet { tools: StructuredToolInterface[]; /** Whether {@link ALIGNMENT_TOOL_VIEW} has been called. */ viewed(): boolean; /** The decision, once one of the three decision tools has been called. */ decision(): AlignmentDecision | undefined; } /** * The checker's tool surface for one subject. * * **A factory rather than four module-level tools**, because each call has to close over the subject * it is ruling on — and because that is what lets a test call `approveCommand` DIRECTLY on an * `attack`, a `catastrophic` or a hardline-refused command and watch it refuse. *"We never invoke * the checker for those"* is a claim about a call site; the acceptance asks for a case that tries. * * **The view tool gates the decision tools**, so a decision reached without looking is not recorded. * A model that skips straight to `approveCommand` is told to look first and gets another turn; if it * spends the budget doing that, {@link runAlignmentCheck} fails closed to the human. * * **The first decision wins.** A second decision call is refused rather than overwriting the first, * because *"exactly one of the three"* is the contract and a model that calls two has already failed * to honour it — taking the last would let `escalateToUser` followed by `approveCommand` approve. */ export declare function createAlignmentTools(subject: AlignmentSubject, home?: string): AlignmentToolSet; /** * [[TUI-C27]]-shaped diagnostic record of ONE alignment check, captured at the send site. * * It carries the very messages handed to the model, so nothing downstream can rebuild a context that * disagrees with the one that was actually sent — the one thing a diagnostic archive must never do — * and a checker that never answers still leaves behind what it was asked. */ export interface AlignmentCallCapture { /** ISO timestamp of the send. */ at: string; /** `approvals.alignmentChecker`, so a dump names WHO checked. */ profile?: string; /** The wall-clock budget this call was given. */ timeoutMs: number; /** * The assembled context, by role, exactly as sent. * * **A replayed round carries its meaning in its TOOL CALLS, not in its content**, which is why * those are recorded beside the text rather than left to the role sequence. `replayRound` emits * the checker's own turns as assistant messages whose `content` is the empty string and whose * decision is the tool it called with the arguments it called it with — so a capture holding * `{role: 'ai', content: ''}` and nothing else tells an auditor that a round happened and refuses * to say what it decided, which is the one question a dump of a safety gate is opened to answer. * The paired ids are kept for the same reason the assembly makes them positional: a duplicated or * dangling id is a hard provider failure, and a dump that dropped them could not show it. */ messages: Array<{ role: string; content: string; /** The tool calls an assistant turn carried, with the arguments as sent. */ toolCalls?: Array<{ name: string; args: Record; }>; /** Which call a tool result answers. */ toolCallId?: string; /** The tool a tool result came back from. */ toolName?: string; }>; /** How long the call took. */ durationMs?: number; /** What the checker decided, or the fail-closed escalation. */ decision?: AlignmentDecision; /** Set when the decision is the gate's rather than the checker's. */ failClosed?: AlignmentFailClosedCause; /** * [[EXT-133]] — the provider's own account of a check that never reached the model, when the * `threw` arm carried one. The classifier has carried this since [[EXT-82]], as * `RaterCallCapture.providerError` in `./approvalCapture.js`; the checker did not, * and the cost was measured rather than imagined: the EXT-127 sonnet sweep recorded ten cells of * *"the auto-rater call failed"* with `durationMs` under a second, and neither the per-case JSON * nor the run log kept one word of why — so the node it raised sat open for weeks with no way to * tell a rejected request from an unreachable network. * * Sanitised where it is built ({@link describeRaterCallFailure}), for the same reason the * classifier's is: the value is carried into a reason string a user reads live, where the * archive's own redaction pass does not run. */ providerError?: RaterCallFailure; } /** * Why the gate produced a decision the checker never made. * * Named rather than inlined because two things now key on it — the capture field above and * {@link alignmentFailClosed}'s reason table — and a union spelled twice is one edit away from a * cause that records but never explains itself. */ export type AlignmentFailClosedCause = 'no-model' | 'timeout' | 'no-decision' | 'threw'; /** Options for {@link runAlignmentCheck}. */ export interface AlignmentCheckOptions { /** The checker's model. `undefined` falls back to `config.llm`, as the classifier's does. */ model?: BaseChatModel; /** The user's own words, from the settled provenance channel. */ userMessages: readonly string[]; /** The checker's own earlier rounds. */ priorRounds?: readonly AlignmentRound[]; /** Home directory to fold out of every rendered value. */ home?: string; /** `approvals.raterTimeoutMs` — one budget for one call; there is no second timeout. */ timeoutMs?: number; /** `approvals.alignmentChecker`, recorded on the capture. */ profile?: string; /** The diagnostic sink, called BEFORE the model is invoked with what is about to be sent. */ onCapture?: (capture: AlignmentCallCapture) => void; } /** The prefix every fail-closed reason carries, so a caller can tell one without matching prose. */ export declare const ALIGNMENT_COULD_NOT_CHECK_PREFIX = "The alignment check could not be completed"; /** * **The fail-closed decision, and what a caller must do with it: NOTHING.** * * `kind` is `escalate` because that is the only one of the three that exercises no authority — but * the contract a caller honours is stronger than the kind, and both call sites implement it: on a * fail-closed check, **the classifier's own action stands, unchanged**. A `reject` stays a * negotiation; a floored `escalate` stays an escalation. * * **Escalating on a failed check would be a silent, total degradation of the rung.** The whole * authority this component has is the authority to APPROVE, so a check that never happened must * exercise none of it — and must equally not take away something that was never its to take. A gate * that turned every unreachable check into a human prompt would make `auto` behave as `assisted` the * moment a checker model went missing, which is exactly the failure EXT-66 measured on the rater's * timeout: a rung drifting toward interrupting about everything while every layer reports success. * The safe reading of "we could not ask" is "then nothing this component would have said applies". * * **[[EXT-133]] — do not return this constant to fail a check closed; call * {@link alignmentFailClosed} instead.** This shape names none of the four causes, so returning it * compiles, type-checks and passes every structural test while restoring exactly the silence EXT-133 * removed — it was the mutation used to prove the new tests can fail. */ export declare const ALIGNMENT_FAIL_CLOSED: AlignmentDecision; /** * [[EXT-133]] — the fail-closed decision for a SPECIFIC cause, carrying the provider's own account * when there was one. * * **What this changes is what a human is told, and nothing else.** `kind` stays `escalate` and the * caller contract above is untouched: the classifier's action still stands on every one of these. * The prefix is kept verbatim at the head of every reason, because {@link isAlignmentFailClosed} — * and therefore both call sites' entire behaviour — keys on that prefix and not on the prose after * it, which is what makes varying the tail safe. * * **Why the tail must vary.** A checker that could not be REACHED and a checker that looked at the * command and DECLINED are different events with opposite remedies — one is a broken configuration * the user can fix, the other is the gate working — and {@link ALIGNMENT_FAIL_CLOSED} rendered all * four causes, plus every provider rejection, as one sentence that named none of them. That is the * silence [[EXT-133]] was filed about: the failure is invisible because it fails in the SAFE * direction, so it is read as a check that declined rather than one that never ran, and the natural * response to a gate that seems to decline everything is to turn the rung down. * * Deliberately mirrors `failClosedVerdict` in `./rater.js`, down to the shape of the table, because * the two gates fail in the same four ways and a reader comparing an archive's rater and checker * records should not have to learn two vocabularies. */ export declare function alignmentFailClosed(cause: AlignmentFailClosedCause, options?: { timeoutMs?: number; failure?: RaterCallFailure; }): AlignmentDecision; /** * Whether a decision is one this gate produced because it could not obtain a check, as opposed to * one a checker actually made. Keys on {@link ALIGNMENT_COULD_NOT_CHECK_PREFIX} — the same * reason-prefix-as-identity idiom the classifier's `isFailClosed` uses — so a caller never has to * match prose, and a genuine `escalateToUser` is never mistaken for a check that did not happen. */ export declare function isAlignmentFailClosed(decision: AlignmentDecision | undefined): boolean; /** * **What the human is told when an alignment check is the reason a command ran without them.** * * At `auto` a `destructive` command used to reach a person or the agent, always. An aligned * approval is new authority, so the event has to be visible: *an event the user never sees reads as * the agent quietly deciding things on their behalf* ([[EXT-106]] §4.6, arguing the same point for * the carve-out's own notice). That applies with MORE force here than to the rarer floored arm, * because this is the common one. * * **One renderer for all three arms, deliberately.** A plain `destructive` lifted by the checker, * §4.6's open-world floor lifted by the checker, and a §4.6-CARVED command lifted by the checker are * the same claim — *a second model read your messages and concluded this matches what you asked * for* — differing only in what else the user has to be told, so they differ by a clause rather than * by being three hand-written sentences. Two copies of one security notice is how the surfaces come * to describe one event two ways, which is the defect [[TUI-C72]] exists for; this is the second * time in this area, so it is not a hypothetical. The neutralisation below runs ONCE, above the * branch, for that same reason: an arm that interpolated the command for itself would be protected * only by whichever cell happened to drive the other arm. * * **The carved arm is a MERGED notice, and on that path it REPLACES [[EXT-106]]'s own.** §4.6's * notice says the user named the host, and adds *"the auto-rater found nothing wrong with it"* — * true on the path it was written for, and false here, because reaching a check at all requires the * classifier to have rated the command `destructive`. So where both apply, the two are announced as * one sentence stating what actually happened: the host the user named, the rating the classifier * gave, and the check that lifted it. The call site fires §4.6's notice only where this arm does * not, so a user is never reading two accounts of one command — and never learning to skim a stack * of warnings about a single event. * * **The host is carried in, not inferred from the floor.** `reachesNetwork` is the caller's reading * of which floor stood, and a carved command has no floor left standing — so an arm that leaned on * it would drop the only line telling the user to look at the host, on the one path where the * user's own message is what authorised the fetch. The hosts are passed because the caller is the * one reader that knows arm precedence resolved to a carve. * * The command is model-authored, so it is neutralised to one line before it reaches a terminal — * the same treatment the negotiation transcript's own rows give it. Deliberately not truncated: * this notice's whole job is to say WHICH command ran. The hosts are not neutralised and do not need * to be: `carvedOpenWorldHosts` returns a host only where it matched the user's own words verbatim, * so no character of one is the model's to choose. * * @param rungLabel the resolved rung in its §10 rule 4 display spelling, passed in rather than * spelled here so the one label table stays the only writer of it. * @param carvedHosts §4.6's carved hosts, or empty. Non-empty selects the merged arm, and is * mutually exclusive with `reachesNetwork` by construction — the caller derives hosts only where * the effective floor is `null`. */ export declare function alignmentApprovalNotice(options: { command: string; rungLabel: string; reachesNetwork: boolean; carvedHosts?: readonly string[]; }): string; /** * **Run one alignment check** and return what the checker decided. * * **Fail-closed, and the direction matters.** A missing model, a timeout, a throw, or a model that * spends {@link ALIGNMENT_MAX_TURNS} without calling a decision tool all produce `escalate` — the * command goes to a person. That is the safe direction here for the same reason the classifier's * fail-closed verdict is `destructive`: the whole authority this component has is the authority to * APPROVE, so a failure to answer must never exercise it. Note the asymmetry with the classifier — * a failure there manufactures a rejection, a failure here manufactures a question — because the two * components are asking opposite questions. * * **Drivable from a test harness with a configured model**, deliberately: the whole justification of * [[EXT-127]] is a corpus before/after across model sizes, and a decision path that could only be * reached through a live session would not be measurable at all. */ export declare function runAlignmentCheck(subject: AlignmentSubject, config: GthConfig, options: AlignmentCheckOptions): Promise; export {};