@gaunt-sloth/core 2.0.0-beta.0 → 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.
- package/LICENSE +6 -6
- package/dist/config/loader.d.ts +13 -0
- package/dist/config/loader.js +55 -11
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +10 -0
- package/dist/config/schema.js +46 -24
- package/dist/config/schema.js.map +1 -1
- package/dist/config/shell-policy.d.ts +142 -1
- package/dist/config/shell-policy.js +114 -7
- package/dist/config/shell-policy.js.map +1 -1
- package/dist/config/types.d.ts +19 -0
- package/dist/config/types.js.map +1 -1
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +10 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +31 -0
- package/dist/core/GthAbstractAgent.js +159 -4
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +314 -18
- package/dist/core/GthAgentRunner.js +1032 -101
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/approvals/approvalRequest.d.ts +247 -0
- package/dist/core/approvals/approvalRequest.js +315 -0
- package/dist/core/approvals/approvalRequest.js.map +1 -0
- package/dist/core/approvals/grants.d.ts +165 -9
- package/dist/core/approvals/grants.js +702 -57
- package/dist/core/approvals/grants.js.map +1 -1
- package/dist/core/plainToolIndication.d.ts +11 -1
- package/dist/core/plainToolIndication.js +25 -10
- package/dist/core/plainToolIndication.js.map +1 -1
- package/dist/core/shell/abstention.d.ts +1 -1
- package/dist/core/shell/abstention.js +65 -11
- package/dist/core/shell/abstention.js.map +1 -1
- package/dist/core/shell/alignment.d.ts +491 -0
- package/dist/core/shell/alignment.js +687 -0
- package/dist/core/shell/alignment.js.map +1 -0
- package/dist/core/shell/approvalCapture.d.ts +53 -41
- package/dist/core/shell/approvalCapture.js +0 -42
- package/dist/core/shell/approvalCapture.js.map +1 -1
- package/dist/core/shell/approvalStop.d.ts +121 -3
- package/dist/core/shell/approvalStop.js +153 -90
- package/dist/core/shell/approvalStop.js.map +1 -1
- package/dist/core/shell/hardline.d.ts +26 -4
- package/dist/core/shell/hardline.js +321 -16
- package/dist/core/shell/hardline.js.map +1 -1
- package/dist/core/shell/negotiation.d.ts +352 -63
- package/dist/core/shell/negotiation.js +295 -123
- package/dist/core/shell/negotiation.js.map +1 -1
- package/dist/core/shell/openWorld.d.ts +135 -0
- package/dist/core/shell/openWorld.js +572 -35
- package/dist/core/shell/openWorld.js.map +1 -1
- package/dist/core/shell/provenance.d.ts +91 -0
- package/dist/core/shell/provenance.js +136 -0
- package/dist/core/shell/provenance.js.map +1 -0
- package/dist/core/shell/rater.d.ts +394 -163
- package/dist/core/shell/rater.js +536 -238
- package/dist/core/shell/rater.js.map +1 -1
- package/dist/core/shell/raterHealth.d.ts +101 -0
- package/dist/core/shell/raterHealth.js +121 -0
- package/dist/core/shell/raterHealth.js.map +1 -0
- package/dist/core/shell/raterModel.d.ts +17 -8
- package/dist/core/shell/raterModel.js +11 -8
- package/dist/core/shell/raterModel.js.map +1 -1
- package/dist/core/shell/rejection.d.ts +20 -5
- package/dist/core/shell/rejection.js +18 -4
- package/dist/core/shell/rejection.js.map +1 -1
- package/dist/core/toolDisplay.d.ts +42 -0
- package/dist/core/toolDisplay.js +31 -0
- package/dist/core/toolDisplay.js.map +1 -1
- package/dist/core/types.d.ts +134 -16
- package/dist/core/types.js.map +1 -1
- package/dist/providers/configurationPassthrough.d.ts +36 -6
- package/dist/providers/configurationPassthrough.js +40 -28
- package/dist/providers/configurationPassthrough.js.map +1 -1
- package/dist/providers/openrouter.js +36 -6
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/vertexai.js +21 -0
- package/dist/providers/vertexai.js.map +1 -1
- package/dist/utils/binaryOutputUtils.js +103 -21
- package/dist/utils/binaryOutputUtils.js.map +1 -1
- package/dist/utils/fileUtils.d.ts +7 -1
- package/dist/utils/fileUtils.js +17 -3
- package/dist/utils/fileUtils.js.map +1 -1
- package/dist/utils/globalConfigUtils.d.ts +5 -2
- package/dist/utils/globalConfigUtils.js +14 -3
- package/dist/utils/globalConfigUtils.js.map +1 -1
- package/dist/utils/vertexaiUtils.js +61 -3
- package/dist/utils/vertexaiUtils.js.map +1 -1
- package/package.json +4 -4
- 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 {};
|