@gaunt-sloth/core 2.0.0-beta.1 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +6 -6
  2. package/dist/config/loader.d.ts +13 -0
  3. package/dist/config/loader.js +55 -11
  4. package/dist/config/loader.js.map +1 -1
  5. package/dist/config/schema.d.ts +10 -0
  6. package/dist/config/schema.js +46 -24
  7. package/dist/config/schema.js.map +1 -1
  8. package/dist/config/shell-policy.d.ts +142 -1
  9. package/dist/config/shell-policy.js +114 -7
  10. package/dist/config/shell-policy.js.map +1 -1
  11. package/dist/config/types.d.ts +19 -0
  12. package/dist/config/types.js.map +1 -1
  13. package/dist/constants.d.ts +10 -0
  14. package/dist/constants.js +10 -0
  15. package/dist/constants.js.map +1 -1
  16. package/dist/core/GthAbstractAgent.d.ts +31 -0
  17. package/dist/core/GthAbstractAgent.js +159 -4
  18. package/dist/core/GthAbstractAgent.js.map +1 -1
  19. package/dist/core/GthAgentRunner.d.ts +314 -18
  20. package/dist/core/GthAgentRunner.js +1032 -101
  21. package/dist/core/GthAgentRunner.js.map +1 -1
  22. package/dist/core/approvals/approvalRequest.d.ts +247 -0
  23. package/dist/core/approvals/approvalRequest.js +315 -0
  24. package/dist/core/approvals/approvalRequest.js.map +1 -0
  25. package/dist/core/approvals/grants.d.ts +165 -9
  26. package/dist/core/approvals/grants.js +702 -57
  27. package/dist/core/approvals/grants.js.map +1 -1
  28. package/dist/core/plainToolIndication.d.ts +11 -1
  29. package/dist/core/plainToolIndication.js +25 -10
  30. package/dist/core/plainToolIndication.js.map +1 -1
  31. package/dist/core/shell/abstention.d.ts +1 -1
  32. package/dist/core/shell/abstention.js +65 -11
  33. package/dist/core/shell/abstention.js.map +1 -1
  34. package/dist/core/shell/alignment.d.ts +491 -0
  35. package/dist/core/shell/alignment.js +687 -0
  36. package/dist/core/shell/alignment.js.map +1 -0
  37. package/dist/core/shell/approvalCapture.d.ts +53 -41
  38. package/dist/core/shell/approvalCapture.js +0 -42
  39. package/dist/core/shell/approvalCapture.js.map +1 -1
  40. package/dist/core/shell/approvalStop.d.ts +121 -3
  41. package/dist/core/shell/approvalStop.js +153 -90
  42. package/dist/core/shell/approvalStop.js.map +1 -1
  43. package/dist/core/shell/hardline.d.ts +26 -4
  44. package/dist/core/shell/hardline.js +321 -16
  45. package/dist/core/shell/hardline.js.map +1 -1
  46. package/dist/core/shell/negotiation.d.ts +352 -63
  47. package/dist/core/shell/negotiation.js +295 -123
  48. package/dist/core/shell/negotiation.js.map +1 -1
  49. package/dist/core/shell/openWorld.d.ts +135 -0
  50. package/dist/core/shell/openWorld.js +572 -35
  51. package/dist/core/shell/openWorld.js.map +1 -1
  52. package/dist/core/shell/provenance.d.ts +91 -0
  53. package/dist/core/shell/provenance.js +136 -0
  54. package/dist/core/shell/provenance.js.map +1 -0
  55. package/dist/core/shell/rater.d.ts +394 -163
  56. package/dist/core/shell/rater.js +536 -238
  57. package/dist/core/shell/rater.js.map +1 -1
  58. package/dist/core/shell/raterHealth.d.ts +101 -0
  59. package/dist/core/shell/raterHealth.js +121 -0
  60. package/dist/core/shell/raterHealth.js.map +1 -0
  61. package/dist/core/shell/raterModel.d.ts +17 -8
  62. package/dist/core/shell/raterModel.js +11 -8
  63. package/dist/core/shell/raterModel.js.map +1 -1
  64. package/dist/core/shell/rejection.d.ts +20 -5
  65. package/dist/core/shell/rejection.js +18 -4
  66. package/dist/core/shell/rejection.js.map +1 -1
  67. package/dist/core/toolDisplay.d.ts +42 -0
  68. package/dist/core/toolDisplay.js +31 -0
  69. package/dist/core/toolDisplay.js.map +1 -1
  70. package/dist/core/types.d.ts +134 -16
  71. package/dist/core/types.js.map +1 -1
  72. package/dist/providers/configurationPassthrough.d.ts +36 -6
  73. package/dist/providers/configurationPassthrough.js +40 -28
  74. package/dist/providers/configurationPassthrough.js.map +1 -1
  75. package/dist/providers/openrouter.js +36 -6
  76. package/dist/providers/openrouter.js.map +1 -1
  77. package/dist/providers/vertexai.js +21 -0
  78. package/dist/providers/vertexai.js.map +1 -1
  79. package/dist/utils/binaryOutputUtils.js +103 -21
  80. package/dist/utils/binaryOutputUtils.js.map +1 -1
  81. package/dist/utils/fileUtils.d.ts +7 -1
  82. package/dist/utils/fileUtils.js +17 -3
  83. package/dist/utils/fileUtils.js.map +1 -1
  84. package/dist/utils/globalConfigUtils.d.ts +5 -2
  85. package/dist/utils/globalConfigUtils.js +14 -3
  86. package/dist/utils/globalConfigUtils.js.map +1 -1
  87. package/dist/utils/vertexaiUtils.js +61 -3
  88. package/dist/utils/vertexaiUtils.js.map +1 -1
  89. package/package.json +4 -4
  90. package/schema/gsloth-config.schema.json +34 -0
@@ -0,0 +1,491 @@
1
+ /**
2
+ * @module core/shell/alignment
3
+ *
4
+ * [[EXT-127]] — **the alignment check**: the second model call, reached only once the classifier
5
+ * has declined, whose whole job is to decide whether the declined command is what the user actually
6
+ * asked for.
7
+ *
8
+ * ## Why there are two models
9
+ *
10
+ * One rater was doing two jobs, and the context the second needed corrupted the first. The
11
+ * classifier ({@link import('./rater.js').rateShellCommand}) now rates the command and nothing else:
12
+ * no user messages, no negotiation history, no argument. This module owns the other question, and it
13
+ * is a question about our own conversation — *did the user ask for this?* — never about the world,
14
+ * never about a counterparty, and never about what the command will hit.
15
+ *
16
+ * ## The trust ordering is carried by the message ROLES
17
+ *
18
+ * This is the hypothesis of the design rather than tidiness. Instead of stacking trusted
19
+ * instructions, untrusted command text and prior argument into one flat prompt and hoping the
20
+ * labelling holds, each kind of text is placed in the role models were trained to weigh it in:
21
+ *
22
+ * | role | carries |
23
+ * |---|---|
24
+ * | system | our instructions, and the classification rendered from the enum |
25
+ * | user | the user's own text |
26
+ * | tool result | the agent's command, and any payload |
27
+ * | assistant | the checker's own earlier rounds and tool calls |
28
+ *
29
+ * **The property this buys:** file content, web content and command output all arrive as tool
30
+ * results, so under this assembly **they cannot present themselves as user provenance — there is no
31
+ * role for them to occupy.** That is the discrimination [[EXT-106]]'s control pair turns on, where
32
+ * the same command was approved from the user's own words and refused four times when a file named
33
+ * it.
34
+ *
35
+ * **The `user` role is fed from the SETTLED PROVENANCE CHANNEL and never from the raw store.** See
36
+ * {@link AlignmentContext.userMessages}: the runner reads
37
+ * {@link import('./negotiation.js').ShellNegotiationState.retainedUserMessages}, which is empty
38
+ * until `admitUserProvenance` says otherwise, and never `noteUserMessages`' store (which also feeds
39
+ * a different question with a different reader) or `humanMessageTexts` (the unfiltered upstream).
40
+ * Without that this design's one structural claim about provenance would be the one place it is not
41
+ * structural.
42
+ *
43
+ * **One inherited limitation, recorded rather than fixed here.** On ACP, client attachments are
44
+ * folded into the human message before the runner ever sees them, so on that surface a byte the
45
+ * client attached can reach the `user` role. That is [[EXT-130]]'s accepted, deliberately-deferred
46
+ * risk — held open there until there is time to spend on ACP properly — and this module inherits it.
47
+ * Everything the AGENT fetched still arrives as a tool result; the exception is on the way IN, at a
48
+ * surface, and it is the surface that has to fix it.
49
+ *
50
+ * **State the limit once and move on: role adherence is empirical, not structural.** The assembly is
51
+ * deterministic and ours; whether a model honours the boundary is a behaviour, and a smaller model
52
+ * honours it worse. That is a caveat on the evidence, not an argument against the design.
53
+ *
54
+ * ## The checker decides only by calling a tool
55
+ *
56
+ * It must call {@link ALIGNMENT_TOOL_VIEW} — which returns the classification, the rejection reason
57
+ * and the agent's command — before calling exactly one of {@link ALIGNMENT_TOOL_APPROVE},
58
+ * {@link ALIGNMENT_TOOL_SUGGEST} or {@link ALIGNMENT_TOOL_ESCALATE}. Prose is not a decision: a
59
+ * model that answers in text has escalated, because {@link runAlignmentCheck} fails closed.
60
+ *
61
+ * ## What an aligned approval may and may not lift
62
+ *
63
+ * **MAY lift:** §4.6's open-world preflight floor, and a `destructive` rating, where the checker
64
+ * finds the command aligned with what the user asked. That is deliberate authority.
65
+ *
66
+ * **MAY NOT lift**, and it is {@link alignmentApprovalRefusal} — the TOOL'S CONTRACT, enforced in
67
+ * code — rather than a sentence in a prompt asking the model not to:
68
+ *
69
+ * - `attack` — always the human's, and a clear attack is rare and deserves attention;
70
+ * - `catastrophic` — because for an irreversible command the prompt is not asking *did you mean
71
+ * this*, it is the last place the ambient target is visible. `kubectl delete namespace staging`
72
+ * names no cluster, `terraform destroy` names no workspace, `dd of=/dev/sdb` names no disk. A user
73
+ * can type the command verbatim — perfect provenance — and still destroy the wrong thing;
74
+ * - the §8 hardline — settled as not liftable at any rung, `bypass` included, and that narrowness is
75
+ * what the rest of the ladder is allowed to rely on.
76
+ *
77
+ * **The distinction that makes those limits coherent: alignment is about the REQUEST, not the
78
+ * EFFECT.** The checker can answer *did the user ask for this command*; it cannot answer *does this
79
+ * command do what it appears to do*. A `git commit` whose message contains backticks is perfectly
80
+ * aligned with what the user asked and still executes substituted shell. An aligned command is not
81
+ * thereby a safe one, and no amount of provenance closes that gap.
82
+ */
83
+ import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
84
+ import { type BaseMessage } from '@langchain/core/messages';
85
+ import type { StructuredToolInterface } from '@langchain/core/tools';
86
+ import type { GthConfig } from '#src/config.js';
87
+ import type { RaterOutcome } from '#src/core/shell/raterVocabulary.js';
88
+ /** The tool that hands the checker what it is ruling on. It must be called before any decision. */
89
+ export declare const ALIGNMENT_TOOL_VIEW = "viewCommandSuggestedByAgent";
90
+ /** Decision tool: the command is what the user asked for, and may run. */
91
+ export declare const ALIGNMENT_TOOL_APPROVE = "approveCommand";
92
+ /** Decision tool: not yet — tell the agent what would make it acceptable. */
93
+ export declare const ALIGNMENT_TOOL_SUGGEST = "suggestChangesToCommand";
94
+ /** Decision tool: this is a person's call. */
95
+ export declare const ALIGNMENT_TOOL_ESCALATE = "escalateToUser";
96
+ /**
97
+ * **The ledger tool is NOT BUILT HERE, and its absence is deliberate rather than an oversight.**
98
+ *
99
+ * A checker that can see what the agent already ran and what came back can resolve a
100
+ * state-dependent command instead of arguing about it, which is why [[EXT-127]] wants one — but the
101
+ * mechanism belongs to [[EXT-131]], which owns it. Nothing here stubs it: a stub that returned
102
+ * nothing would read, to a model and to the next implementer alike, as *"the agent has run
103
+ * nothing"*, which is a false statement rather than a missing feature.
104
+ *
105
+ * **Two conditions ride with it, stated here so [[EXT-131]] builds against them rather than
106
+ * re-deriving them:**
107
+ *
108
+ * 1. **Scope it to calls the gate APPROVED**, since that is the set the gate can vouch for. What
109
+ * ran without passing this gate is not something this gate saw.
110
+ * 2. **Split the record by provenance.** The gate's own metadata — the command, the exit status,
111
+ * when it ran, which rung approved it — is **trusted because we produced it**, so it may be
112
+ * stated as fact. The **payload is untrusted content and belongs in the tool-result role**, with
113
+ * the same fencing every other untrusted value in this module gets. A checker that pulls file
114
+ * contents or command stdout into its context unframed lets a hostile file write into the safety
115
+ * layer's own prompt — which is precisely the assembly this module exists to make impossible.
116
+ */
117
+ export declare const ALIGNMENT_LEDGER_CONTRACT: string;
118
+ /**
119
+ * How many model turns one check may take before it fails closed.
120
+ *
121
+ * **Two are the intended path** — view, then decide — **and the other two are slack for a model
122
+ * that does not get there in two.** Each of the ways a check legitimately runs long costs a whole
123
+ * turn whether or not it produced a decision: a turn spent narrating before calling anything (the
124
+ * loop answers *"that was not a decision"* and gives the turn back), and a second view. A small
125
+ * local model does both in the same check, which is the case the slack is sized for, so the budget
126
+ * is the intended two plus one for each — not the intended two plus one shared spare.
127
+ *
128
+ * **The bound is not a safety control** — failing closed is what makes an exhausted budget safe —
129
+ * it is what stops a model that never decides from spending a session's tokens re-reading the same
130
+ * command. That is also why the slack errs high: the cost of one turn too many is tokens, and the
131
+ * cost of one too few is a check that failed closed on a model that was about to answer.
132
+ */
133
+ export declare const ALIGNMENT_MAX_TURNS = 4;
134
+ /** What the checker is ruling on: the agent's command, and what the classifier made of it. */
135
+ export interface AlignmentSubject {
136
+ /** The command the agent proposed, RAW. Every renderer here normalizes and folds it itself. */
137
+ command: string;
138
+ /** The classifier's outcome, which is what the system prompt renders from the enum. */
139
+ outcome: RaterOutcome;
140
+ /** The classifier's one-sentence explanation. Model-authored, so it is fenced, never asserted. */
141
+ reason: string;
142
+ /** The justification the agent attached to the command, when it attached one. Agent-authored. */
143
+ justification?: string;
144
+ }
145
+ /** Which of the three decision tools the checker called. */
146
+ export type AlignmentDecisionKind = 'approve' | 'suggest' | 'escalate';
147
+ /** One completed decision by the checker. */
148
+ export interface AlignmentDecision {
149
+ kind: AlignmentDecisionKind;
150
+ /**
151
+ * The checker's own sentence. Model-authored, and **replayed VERBATIM — it is not fenced**, at
152
+ * either of the two places it is replayed.
153
+ *
154
+ * What bounds that is stated here rather than left to a reader's assumption, because the sentence
155
+ * this replaced asserted a fencing that neither site implements, and **a docblock claiming a
156
+ * protection that does not exist is worse than silence**: it is what makes the code read as
157
+ * already audited.
158
+ *
159
+ * - **Into the next round's own conversation** ({@link buildAlignmentMessages}, via `replayRound`)
160
+ * it goes back as the checker's OWN assistant tool call. That role is the one thing in this
161
+ * assembly the model is meant to read as its own reasoning, so fencing it as untrusted data
162
+ * would contradict the design rather than harden it — the model would meet its own last turn as
163
+ * testimony from a stranger.
164
+ * - **Into the rejection handed to the coding agent** (`GthAgentRunner`) it is appended to the
165
+ * rater's refusal. That branch is guarded by the gate's outcome being `reject` **and** the
166
+ * decision being `suggest`, so the recipient is an agent that is being told **no**: the text is
167
+ * guidance for a retry, it confers no authority, and whatever the agent proposes next is rated
168
+ * and checked again from scratch.
169
+ *
170
+ * **So the bound is the position, not any escaping.** A consumer that widens either — replaying
171
+ * this into the `user` role, or carrying it onto a path where something is approved — is
172
+ * responsible for fencing it there, and must not read this field as already safe.
173
+ */
174
+ reason: string;
175
+ /** With `suggest`, a narrower command the checker would accept. Optional; often absent. */
176
+ suggestedCommand?: string;
177
+ }
178
+ /**
179
+ * One completed round of an alignment check, as the NEXT round's checker sees it — replayed as its
180
+ * own assistant messages and tool results rather than quoted into a block.
181
+ *
182
+ * That is the whole of *"the checker's own earlier rounds and tool calls"*: the model meets its
183
+ * previous turns as its own turns, so it recognises them as its own reasoning rather than as
184
+ * testimony from a third party.
185
+ */
186
+ export interface AlignmentRound {
187
+ /** What that round was ruling on. */
188
+ subject: AlignmentSubject;
189
+ /** What it decided. */
190
+ decision: AlignmentDecision;
191
+ }
192
+ /** Everything one check is assembled from. */
193
+ export interface AlignmentContext {
194
+ /** The pending command and its classification. */
195
+ subject: AlignmentSubject;
196
+ /**
197
+ * **The user's own words — and they must come from the settled provenance channel.**
198
+ *
199
+ * The runner reads
200
+ * {@link import('./negotiation.js').ShellNegotiationState.retainedUserMessages}, the
201
+ * command-keyed window that is EMPTY until `admitUserProvenance` has positively established that
202
+ * this session's human turns are the user's own words. It is deliberately **not**
203
+ * `noteUserMessages`' raw store — which also feeds a different question with a different reader —
204
+ * and deliberately not `humanMessageTexts`, the unfiltered upstream in `GthAgentRunner`.
205
+ *
206
+ * Absent or empty is a check with no mandate in view, and the checker is told so rather than left
207
+ * to infer it: with nothing to align against, the honest answer is almost always to escalate.
208
+ */
209
+ userMessages: readonly string[];
210
+ /** The checker's own earlier rounds, oldest first. Empty is round 1. */
211
+ priorRounds?: readonly AlignmentRound[];
212
+ /** The home directory to fold, so every rendered value takes the same less-identifying form. */
213
+ home?: string;
214
+ }
215
+ /**
216
+ * §4.6/§4.2/§8 — **the tool's contract: why `approveCommand` may not approve this command**, or
217
+ * `null` when it may.
218
+ *
219
+ * **This is enforced in code because a prompt cannot enforce it.** A sentence asking a model not to
220
+ * approve an `attack` is an instruction, and an instruction is exactly what the untrusted half of
221
+ * this context is trying to supply. The three limits below are the ones the rest of the ladder is
222
+ * allowed to rely on, so they are decided from the command and the classification alone — before
223
+ * the model's answer is read, and regardless of what it said.
224
+ *
225
+ * Exported, and separate from the tool it backs, so a test can drive it directly AND so the tool and
226
+ * the runner cannot come to disagree about what is liftable.
227
+ *
228
+ * **What is deliberately NOT here:** a `destructive` rating and §4.6's open-world preflight floor.
229
+ * Both ARE liftable by an aligned approval — that is the authority the whole feature exists to grant
230
+ * — so neither may be smuggled into this list by a later reader looking for symmetry.
231
+ */
232
+ export declare function alignmentApprovalRefusal(subject: AlignmentSubject): string | null;
233
+ /**
234
+ * The system prompt: **our instructions, and the classification rendered from the enum.**
235
+ *
236
+ * Two things are absent on purpose and neither may be added back:
237
+ *
238
+ * - **The command.** It arrives as a tool result, which is the whole of this design. Putting it here
239
+ * would place attacker-controlled text in the highest-trust role, which is the arrangement the
240
+ * split exists to leave behind.
241
+ * - **The classifier's `reason`.** That sentence is model-authored, so it is untrusted; only the
242
+ * OUTCOME is ours, because it is one of four values we defined. The reason is real information and
243
+ * the checker gets it — through {@link ALIGNMENT_TOOL_VIEW}, in the tool-result role, fenced.
244
+ *
245
+ * The rendered value is a {@link RaterOutcome}, i.e. a member of a closed enum, so nothing a model
246
+ * or an attacker wrote can reach this string.
247
+ */
248
+ export declare function buildAlignmentSystemPrompt(outcome: RaterOutcome): string;
249
+ /**
250
+ * §5.1's bounds, applied to the user role: the LAST {@link NEGOTIATION_MAX_USER_MESSAGES} non-blank
251
+ * messages, each folded, collapsed to one line, unable to close its own fence, and truncated to
252
+ * {@link NEGOTIATION_USER_MESSAGE_MAX_CHARS} **including** the ellipsis.
253
+ *
254
+ * **The bounds are applied HERE rather than trusted from the caller**, so a caller that hands over
255
+ * an entire conversation still cannot put a pasted log into the checker's context. Blanks are
256
+ * dropped BEFORE the window is taken, so a run of empty turns cannot spend the budget that carries
257
+ * the mandate — and "blank" counts the invisibles, so a value carrying only those cannot occupy a
258
+ * slot a reader would see nothing in.
259
+ *
260
+ * **These are the user's own words and they are STILL fenced, which is not a contradiction.** The
261
+ * role is what says whose words they are; the fence is what stops one of them ending the block and
262
+ * writing our prose. A user can paste anything, including a paragraph shaped like our own headings.
263
+ */
264
+ export declare function renderAlignmentUserMessages(userMessages: readonly string[], home?: string): string[];
265
+ /**
266
+ * The USER role: the user's own text, and our own framing of the question — and **nothing else**.
267
+ *
268
+ * The framing sentences are ours, so they are not fenced; the messages are the user's, so they are.
269
+ * No part of the agent's command, its justification or the classifier's reason appears here, which
270
+ * is the placement half of this module's whole claim: a value that reached us as a tool result has
271
+ * no route into this message.
272
+ *
273
+ * `round` frames a LATER round, and its clauses are **our own static strings** chosen by comparing
274
+ * this round's command and justification with the previous one — never the agent's text itself,
275
+ * which stays in the tool result where it belongs.
276
+ */
277
+ export declare function buildAlignmentUserMessage(userMessages: readonly string[], options?: {
278
+ home?: string;
279
+ commandChanged?: boolean;
280
+ justificationChanged?: boolean;
281
+ }): string;
282
+ /**
283
+ * The TOOL-RESULT role: what {@link ALIGNMENT_TOOL_VIEW} hands back — the classification, the
284
+ * rejection reason, the agent's command, and the agent's justification when it gave one.
285
+ *
286
+ * **Every value here is agent- or model-authored, and every one of them is fenced.** The command is
287
+ * normalized and home-folded with the same functions the live rating goes through, so the checker
288
+ * sees the string the classifier actually rated. The reason and the justification are collapsed to
289
+ * one line, because this payload is line-structured and a newline in any of them would otherwise
290
+ * forge a field that was never returned.
291
+ *
292
+ * This is also where a future [[EXT-131]] ledger payload belongs — see
293
+ * {@link ALIGNMENT_LEDGER_CONTRACT}.
294
+ */
295
+ export declare function renderCommandSuggestedByAgent(subject: AlignmentSubject, home?: string): string;
296
+ /**
297
+ * **The four-role assembly** — the whole hypothesis of [[EXT-127]], in one function.
298
+ *
299
+ * The order is: our instructions (system), the user's mandate (user), then every earlier round
300
+ * replayed as the checker's own turns and their tool results, then — for a later round — our own
301
+ * framing of what changed. The pending command is deliberately absent from all of it: it arrives
302
+ * only when the model calls {@link ALIGNMENT_TOOL_VIEW}, which is what makes *"the agent's command
303
+ * is a tool result"* a property of the assembly rather than a convention.
304
+ *
305
+ * Exported and returning plain messages so a test can assert on the PLACEMENT of each value rather
306
+ * than on its presence somewhere — a test that merely checks a string is present does not test this.
307
+ */
308
+ export declare function buildAlignmentMessages(context: AlignmentContext): BaseMessage[];
309
+ /** The tools, plus the decision they wrote into, for one check. */
310
+ interface AlignmentToolSet {
311
+ tools: StructuredToolInterface[];
312
+ /** Whether {@link ALIGNMENT_TOOL_VIEW} has been called. */
313
+ viewed(): boolean;
314
+ /** The decision, once one of the three decision tools has been called. */
315
+ decision(): AlignmentDecision | undefined;
316
+ }
317
+ /**
318
+ * The checker's tool surface for one subject.
319
+ *
320
+ * **A factory rather than four module-level tools**, because each call has to close over the subject
321
+ * it is ruling on — and because that is what lets a test call `approveCommand` DIRECTLY on an
322
+ * `attack`, a `catastrophic` or a hardline-refused command and watch it refuse. *"We never invoke
323
+ * the checker for those"* is a claim about a call site; the acceptance asks for a case that tries.
324
+ *
325
+ * **The view tool gates the decision tools**, so a decision reached without looking is not recorded.
326
+ * A model that skips straight to `approveCommand` is told to look first and gets another turn; if it
327
+ * spends the budget doing that, {@link runAlignmentCheck} fails closed to the human.
328
+ *
329
+ * **The first decision wins.** A second decision call is refused rather than overwriting the first,
330
+ * because *"exactly one of the three"* is the contract and a model that calls two has already failed
331
+ * to honour it — taking the last would let `escalateToUser` followed by `approveCommand` approve.
332
+ */
333
+ export declare function createAlignmentTools(subject: AlignmentSubject, home?: string): AlignmentToolSet;
334
+ /**
335
+ * [[TUI-C27]]-shaped diagnostic record of ONE alignment check, captured at the send site.
336
+ *
337
+ * It carries the very messages handed to the model, so nothing downstream can rebuild a context that
338
+ * disagrees with the one that was actually sent — the one thing a diagnostic archive must never do —
339
+ * and a checker that never answers still leaves behind what it was asked.
340
+ */
341
+ export interface AlignmentCallCapture {
342
+ /** ISO timestamp of the send. */
343
+ at: string;
344
+ /** `approvals.alignmentChecker`, so a dump names WHO checked. */
345
+ profile?: string;
346
+ /** The wall-clock budget this call was given. */
347
+ timeoutMs: number;
348
+ /**
349
+ * The assembled context, by role, exactly as sent.
350
+ *
351
+ * **A replayed round carries its meaning in its TOOL CALLS, not in its content**, which is why
352
+ * those are recorded beside the text rather than left to the role sequence. `replayRound` emits
353
+ * the checker's own turns as assistant messages whose `content` is the empty string and whose
354
+ * decision is the tool it called with the arguments it called it with — so a capture holding
355
+ * `{role: 'ai', content: ''}` and nothing else tells an auditor that a round happened and refuses
356
+ * to say what it decided, which is the one question a dump of a safety gate is opened to answer.
357
+ * The paired ids are kept for the same reason the assembly makes them positional: a duplicated or
358
+ * dangling id is a hard provider failure, and a dump that dropped them could not show it.
359
+ */
360
+ messages: Array<{
361
+ role: string;
362
+ content: string;
363
+ /** The tool calls an assistant turn carried, with the arguments as sent. */
364
+ toolCalls?: Array<{
365
+ name: string;
366
+ args: Record<string, unknown>;
367
+ }>;
368
+ /** Which call a tool result answers. */
369
+ toolCallId?: string;
370
+ /** The tool a tool result came back from. */
371
+ toolName?: string;
372
+ }>;
373
+ /** How long the call took. */
374
+ durationMs?: number;
375
+ /** What the checker decided, or the fail-closed escalation. */
376
+ decision?: AlignmentDecision;
377
+ /** Set when the decision is the gate's rather than the checker's. */
378
+ failClosed?: 'no-model' | 'timeout' | 'no-decision' | 'threw';
379
+ }
380
+ /** Options for {@link runAlignmentCheck}. */
381
+ export interface AlignmentCheckOptions {
382
+ /** The checker's model. `undefined` falls back to `config.llm`, as the classifier's does. */
383
+ model?: BaseChatModel;
384
+ /** The user's own words, from the settled provenance channel. */
385
+ userMessages: readonly string[];
386
+ /** The checker's own earlier rounds. */
387
+ priorRounds?: readonly AlignmentRound[];
388
+ /** Home directory to fold out of every rendered value. */
389
+ home?: string;
390
+ /** `approvals.raterTimeoutMs` — one budget for one call; there is no second timeout. */
391
+ timeoutMs?: number;
392
+ /** `approvals.alignmentChecker`, recorded on the capture. */
393
+ profile?: string;
394
+ /** The diagnostic sink, called BEFORE the model is invoked with what is about to be sent. */
395
+ onCapture?: (capture: AlignmentCallCapture) => void;
396
+ }
397
+ /** The prefix every fail-closed reason carries, so a caller can tell one without matching prose. */
398
+ export declare const ALIGNMENT_COULD_NOT_CHECK_PREFIX = "The alignment check could not be completed";
399
+ /**
400
+ * **The fail-closed decision, and what a caller must do with it: NOTHING.**
401
+ *
402
+ * `kind` is `escalate` because that is the only one of the three that exercises no authority — but
403
+ * the contract a caller honours is stronger than the kind, and both call sites implement it: on a
404
+ * fail-closed check, **the classifier's own action stands, unchanged**. A `reject` stays a
405
+ * negotiation; a floored `escalate` stays an escalation.
406
+ *
407
+ * **Escalating on a failed check would be a silent, total degradation of the rung.** The whole
408
+ * authority this component has is the authority to APPROVE, so a check that never happened must
409
+ * exercise none of it — and must equally not take away something that was never its to take. A gate
410
+ * that turned every unreachable check into a human prompt would make `auto` behave as `assisted` the
411
+ * moment a checker model went missing, which is exactly the failure EXT-66 measured on the rater's
412
+ * timeout: a rung drifting toward interrupting about everything while every layer reports success.
413
+ * The safe reading of "we could not ask" is "then nothing this component would have said applies".
414
+ */
415
+ export declare const ALIGNMENT_FAIL_CLOSED: AlignmentDecision;
416
+ /**
417
+ * Whether a decision is one this gate produced because it could not obtain a check, as opposed to
418
+ * one a checker actually made. Keys on {@link ALIGNMENT_COULD_NOT_CHECK_PREFIX} — the same
419
+ * reason-prefix-as-identity idiom the classifier's `isFailClosed` uses — so a caller never has to
420
+ * match prose, and a genuine `escalateToUser` is never mistaken for a check that did not happen.
421
+ */
422
+ export declare function isAlignmentFailClosed(decision: AlignmentDecision | undefined): boolean;
423
+ /**
424
+ * **What the human is told when an alignment check is the reason a command ran without them.**
425
+ *
426
+ * At `auto` a `destructive` command used to reach a person or the agent, always. An aligned
427
+ * approval is new authority, so the event has to be visible: *an event the user never sees reads as
428
+ * the agent quietly deciding things on their behalf* ([[EXT-106]] §4.6, arguing the same point for
429
+ * the carve-out's own notice). That applies with MORE force here than to the rarer floored arm,
430
+ * because this is the common one.
431
+ *
432
+ * **One renderer for all three arms, deliberately.** A plain `destructive` lifted by the checker,
433
+ * §4.6's open-world floor lifted by the checker, and a §4.6-CARVED command lifted by the checker are
434
+ * the same claim — *a second model read your messages and concluded this matches what you asked
435
+ * for* — differing only in what else the user has to be told, so they differ by a clause rather than
436
+ * by being three hand-written sentences. Two copies of one security notice is how the surfaces come
437
+ * to describe one event two ways, which is the defect [[TUI-C72]] exists for; this is the second
438
+ * time in this area, so it is not a hypothetical. The neutralisation below runs ONCE, above the
439
+ * branch, for that same reason: an arm that interpolated the command for itself would be protected
440
+ * only by whichever cell happened to drive the other arm.
441
+ *
442
+ * **The carved arm is a MERGED notice, and on that path it REPLACES [[EXT-106]]'s own.** §4.6's
443
+ * notice says the user named the host, and adds *"the auto-rater found nothing wrong with it"* —
444
+ * true on the path it was written for, and false here, because reaching a check at all requires the
445
+ * classifier to have rated the command `destructive`. So where both apply, the two are announced as
446
+ * one sentence stating what actually happened: the host the user named, the rating the classifier
447
+ * gave, and the check that lifted it. The call site fires §4.6's notice only where this arm does
448
+ * not, so a user is never reading two accounts of one command — and never learning to skim a stack
449
+ * of warnings about a single event.
450
+ *
451
+ * **The host is carried in, not inferred from the floor.** `reachesNetwork` is the caller's reading
452
+ * of which floor stood, and a carved command has no floor left standing — so an arm that leaned on
453
+ * it would drop the only line telling the user to look at the host, on the one path where the
454
+ * user's own message is what authorised the fetch. The hosts are passed because the caller is the
455
+ * one reader that knows arm precedence resolved to a carve.
456
+ *
457
+ * The command is model-authored, so it is neutralised to one line before it reaches a terminal —
458
+ * the same treatment the negotiation transcript's own rows give it. Deliberately not truncated:
459
+ * this notice's whole job is to say WHICH command ran. The hosts are not neutralised and do not need
460
+ * to be: `carvedOpenWorldHosts` returns a host only where it matched the user's own words verbatim,
461
+ * so no character of one is the model's to choose.
462
+ *
463
+ * @param rungLabel the resolved rung in its §10 rule 4 display spelling, passed in rather than
464
+ * spelled here so the one label table stays the only writer of it.
465
+ * @param carvedHosts §4.6's carved hosts, or empty. Non-empty selects the merged arm, and is
466
+ * mutually exclusive with `reachesNetwork` by construction — the caller derives hosts only where
467
+ * the effective floor is `null`.
468
+ */
469
+ export declare function alignmentApprovalNotice(options: {
470
+ command: string;
471
+ rungLabel: string;
472
+ reachesNetwork: boolean;
473
+ carvedHosts?: readonly string[];
474
+ }): string;
475
+ /**
476
+ * **Run one alignment check** and return what the checker decided.
477
+ *
478
+ * **Fail-closed, and the direction matters.** A missing model, a timeout, a throw, or a model that
479
+ * spends {@link ALIGNMENT_MAX_TURNS} without calling a decision tool all produce `escalate` — the
480
+ * command goes to a person. That is the safe direction here for the same reason the classifier's
481
+ * fail-closed verdict is `destructive`: the whole authority this component has is the authority to
482
+ * APPROVE, so a failure to answer must never exercise it. Note the asymmetry with the classifier —
483
+ * a failure there manufactures a rejection, a failure here manufactures a question — because the two
484
+ * components are asking opposite questions.
485
+ *
486
+ * **Drivable from a test harness with a configured model**, deliberately: the whole justification of
487
+ * [[EXT-127]] is a corpus before/after across model sizes, and a decision path that could only be
488
+ * reached through a live session would not be measurable at all.
489
+ */
490
+ export declare function runAlignmentCheck(subject: AlignmentSubject, config: GthConfig, options: AlignmentCheckOptions): Promise<AlignmentDecision>;
491
+ export {};