@gaunt-sloth/core 2.0.0-alpha.29 → 2.0.0-alpha.30

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 (76) hide show
  1. package/dist/config/schema.d.ts +48 -48
  2. package/dist/config/schema.js +55 -29
  3. package/dist/config/schema.js.map +1 -1
  4. package/dist/config/shell-policy.d.ts +241 -26
  5. package/dist/config/shell-policy.js +353 -52
  6. package/dist/config/shell-policy.js.map +1 -1
  7. package/dist/config/tool-descriptions.d.ts +78 -32
  8. package/dist/config/tool-descriptions.js +72 -29
  9. package/dist/config/tool-descriptions.js.map +1 -1
  10. package/dist/config/types.d.ts +9 -4
  11. package/dist/config/types.js.map +1 -1
  12. package/dist/core/GthAbstractAgent.d.ts +13 -5
  13. package/dist/core/GthAbstractAgent.js +42 -13
  14. package/dist/core/GthAbstractAgent.js.map +1 -1
  15. package/dist/core/GthAgentRunner.d.ts +203 -9
  16. package/dist/core/GthAgentRunner.js +646 -39
  17. package/dist/core/GthAgentRunner.js.map +1 -1
  18. package/dist/core/GthLangChainAgent.js +58 -17
  19. package/dist/core/GthLangChainAgent.js.map +1 -1
  20. package/dist/core/reasoningBlocks.d.ts +60 -0
  21. package/dist/core/reasoningBlocks.js +98 -0
  22. package/dist/core/reasoningBlocks.js.map +1 -0
  23. package/dist/core/refusal.js +6 -2
  24. package/dist/core/refusal.js.map +1 -1
  25. package/dist/core/shell/approvalCapture.d.ts +271 -0
  26. package/dist/core/shell/approvalCapture.js +108 -0
  27. package/dist/core/shell/approvalCapture.js.map +1 -0
  28. package/dist/core/shell/approvalStop.d.ts +16 -7
  29. package/dist/core/shell/approvalStop.js +18 -7
  30. package/dist/core/shell/approvalStop.js.map +1 -1
  31. package/dist/core/shell/escalationSeverity.d.ts +141 -0
  32. package/dist/core/shell/escalationSeverity.js +89 -0
  33. package/dist/core/shell/escalationSeverity.js.map +1 -0
  34. package/dist/core/shell/framing.d.ts +190 -0
  35. package/dist/core/shell/framing.js +633 -0
  36. package/dist/core/shell/framing.js.map +1 -0
  37. package/dist/core/shell/hardline.d.ts +81 -0
  38. package/dist/core/shell/hardline.js +607 -0
  39. package/dist/core/shell/hardline.js.map +1 -0
  40. package/dist/core/shell/negotiation.d.ts +249 -0
  41. package/dist/core/shell/negotiation.js +355 -0
  42. package/dist/core/shell/negotiation.js.map +1 -0
  43. package/dist/core/shell/normalize.d.ts +5 -5
  44. package/dist/core/shell/normalize.js +5 -5
  45. package/dist/core/shell/rater.d.ts +353 -18
  46. package/dist/core/shell/rater.js +556 -39
  47. package/dist/core/shell/rater.js.map +1 -1
  48. package/dist/core/shell/rejection.d.ts +1 -1
  49. package/dist/core/types.d.ts +97 -4
  50. package/dist/providers/geminiThinking.d.ts +52 -0
  51. package/dist/providers/geminiThinking.js +72 -0
  52. package/dist/providers/geminiThinking.js.map +1 -0
  53. package/dist/providers/google-genai.js +4 -1
  54. package/dist/providers/google-genai.js.map +1 -1
  55. package/dist/providers/vertexai.js +4 -1
  56. package/dist/providers/vertexai.js.map +1 -1
  57. package/dist/runtime/askStructured.d.ts +45 -3
  58. package/dist/runtime/askStructured.js +46 -5
  59. package/dist/runtime/askStructured.js.map +1 -1
  60. package/dist/runtime/conversation.d.ts +2 -1
  61. package/dist/runtime/conversation.js +2 -1
  62. package/dist/runtime/conversation.js.map +1 -1
  63. package/dist/runtime/singleShot.d.ts +3 -2
  64. package/dist/runtime/singleShot.js +3 -2
  65. package/dist/runtime/singleShot.js.map +1 -1
  66. package/dist/utils/debugDump.d.ts +19 -0
  67. package/dist/utils/debugDump.js +11 -0
  68. package/dist/utils/debugDump.js.map +1 -1
  69. package/dist/utils/displayWidth.d.ts +23 -0
  70. package/dist/utils/displayWidth.js +61 -6
  71. package/dist/utils/displayWidth.js.map +1 -1
  72. package/dist/utils/systemPromptNotes.d.ts +12 -1
  73. package/dist/utils/systemPromptNotes.js +24 -2
  74. package/dist/utils/systemPromptNotes.js.map +1 -1
  75. package/package.json +1 -1
  76. package/schema/gsloth-config.schema.json +48 -48
@@ -0,0 +1,271 @@
1
+ /**
2
+ * @module core/shell/approvalCapture
3
+ *
4
+ * [[TUI-C27]] — **what the approvals gate DID, recorded while it was doing it**, for the
5
+ * `/debug-dump` archive.
6
+ *
7
+ * ## The two gaps this exists to close
8
+ *
9
+ * 1. **An approval used to leave no trace at all.** A rejection is legible in a dump only as a
10
+ * byproduct of the negotiation — the protocol has to hand the reason back to the agent, so it
11
+ * lands in the transcript. An approval relays nothing to anyone: the tool simply runs. So the
12
+ * archive was at its most detailed about the decisions that STOPPED something and silent about
13
+ * the one decision that let something happen, which is exactly backwards for an incident review.
14
+ * Everything here is written on both branches.
15
+ * 2. **Every field anyone had was a rater OUTPUT; its INPUT was never recorded.** Rung, outcome,
16
+ * reason, which preflight, whether the floor fired — all of them describe what the rater
17
+ * *answered*, and none records what it was *shown*. The question that actually failed in the
18
+ * field — *"were the user's messages in view on round 2?"* — needs the input of an approving
19
+ * round, the one combination the dump had neither half of.
20
+ *
21
+ * ## The one rule that shapes the whole module: CAPTURE, NEVER RE-RENDER
22
+ *
23
+ * {@link RaterCallCapture.prompt} holds the exact `{system, user}` strings handed to the model, taken
24
+ * inside `rateShellCommand` at the send site — not rebuilt afterwards from the state that produced
25
+ * them. A dump that can disagree with what actually happened is worse than no dump: it invites a
26
+ * confident wrong conclusion from the only evidence there is, which is the failure mode this node
27
+ * was filed over. The same rule is why {@link RaterCallCapture.rawResponse} keeps the model's answer
28
+ * *before* it is mapped to an outcome — a malformed or surprising answer stays visible rather than
29
+ * being smoothed into a verdict.
30
+ *
31
+ * ## The records are pushed EARLY and mutated in place, deliberately
32
+ *
33
+ * {@link ApprovalCaptureLog.begin} appends the record the moment a gated call arrives, and the gate
34
+ * fills fields in as they are decided. The alternative — assemble the whole record, push it at the
35
+ * end — loses exactly the events worth keeping: an `attack` verdict throws `AttackHaltError` out of
36
+ * the decision, so a run that halted would carry no record of the rating that halted it. Pushing
37
+ * first makes survival a property of the structure rather than of someone remembering to write the
38
+ * record before each `throw`.
39
+ *
40
+ * ## Redaction
41
+ *
42
+ * Nothing is redacted here. The archive writer routes this artifact through the SAME
43
+ * [[GS2-47]]/[[GS2-54]] pass as `transcript.json` and `model-messages.json`
44
+ * (`renderStructured` → `redactText`, over the literal secret values `collectSecretValues`
45
+ * harvested from env + config). Redacting twice, in two places, is how two policies come to exist.
46
+ */
47
+ import type { ApprovalRung } from '#src/config.js';
48
+ import type { ToolApprovalScope } from '#src/core/types.js';
49
+ import type { AbstentionDefect } from '#src/core/shell/abstention.js';
50
+ import type { NegotiationCounters } from '#src/core/shell/negotiation.js';
51
+ import type { FailClosedCause, PreflightFloorKind, RaterNegotiationContext, RaterNegotiationRound, ShellSafetyVerdict } from '#src/core/shell/rater.js';
52
+ /**
53
+ * How many gated decisions one session keeps. A ring buffer for the same reason the debug-log one
54
+ * is: each record carries a full rating prompt (a few KB), and a long `auto` session makes a lot of
55
+ * them. The newest are the ones a bug report is about, so the oldest are evicted.
56
+ */
57
+ export declare const APPROVAL_CAPTURE_MAX = 50;
58
+ /**
59
+ * **Which layer of the gate decided this call.** The diagnostic value of the whole record is
60
+ * precisely here: a bug report that says "escalated" does not distinguish a rater verdict from a
61
+ * floor match from a timeout from a deny-list hit, and those need four different answers.
62
+ *
63
+ * The names follow the numbered steps of `GthAgentRunner.decideToolApprovalInner`:
64
+ *
65
+ * - `not-gated` — the rung in force does not gate this tool at all.
66
+ * - `deny-list` — a declared `approvals.deny` entry, or an *always reject* the human chose earlier.
67
+ * - `bypass` — the gate is off for the session.
68
+ * - `hardline-floor` — §8's deterministic floor matched, before any rating or prompt.
69
+ * - `escalate-entry` — a declared `approvals.escalate` entry; a human answers, with no rating call.
70
+ * - `allow-list` — an allow entry settled it with no rating (`rate: false`, or an unrated rung).
71
+ * - `allow-tripwire` — an allow entry that kept the rater involved (§3.2's `rate: true`).
72
+ * - `rater` — the ordinary rating path.
73
+ * - `tool-open-world-floor` — §4.7.3's floor on a non-shell call whose effective `openWorldHint`
74
+ * is true. No rater sees it while §4.3's scope boundary stands.
75
+ * - `unrated-rung` — nothing but the rung itself: `manual` and `write` consult no model, so a call
76
+ * no rule claimed goes to the human on the rung's say-so. It is its own value rather than an
77
+ * absent one, because "the rung requires a person" is an answer and a blank field reads as the
78
+ * recorder having failed.
79
+ */
80
+ export type ApprovalDecidingStage = 'not-gated' | 'deny-list' | 'bypass' | 'hardline-floor' | 'escalate-entry' | 'allow-list' | 'allow-tripwire' | 'rater' | 'tool-open-world-floor' | 'unrated-rung';
81
+ /**
82
+ * What became of the call, as the agent experienced it. `error` is its own value rather than an
83
+ * absence: a decision that threw something other than a halt is a fact about the gate, and a record
84
+ * left with no action at all would read as one that never finished being written.
85
+ */
86
+ export type ApprovalCaptureAction = 'approve' | 'reject' | 'escalate' | 'halt' | 'error';
87
+ /** How an escalation ended once it reached (or failed to reach) a person. */
88
+ export type ApprovalHumanAnswer = 'approve' | 'reject' | 'no-human';
89
+ /**
90
+ * §5.1's negotiation context **as it was handed to the prompt builder**, plus the one distinction a
91
+ * reader of the archive keeps needing and could never make.
92
+ *
93
+ * **`userMessagesPopulated` is the field the node exists for.** Round 1 is context-free *by design*
94
+ * (§5.1: *"round 1 sees the command alone"*), so an empty window is not a bug — and a reader with no
95
+ * access to the source cannot know that. {@link userMessagesNote} states which case this is in
96
+ * words, so the answer to *"were the user's messages in view on round N?"* is one field and one
97
+ * sentence rather than a source-reading exercise across three files.
98
+ */
99
+ export interface RaterNegotiationCapture {
100
+ /** 1-based index of this rating within the current negotiation. */
101
+ round: number;
102
+ /** Whether this rating was a round-1 (context-free) rating. */
103
+ roundOne: boolean;
104
+ /** Whether a §5.1 context was supplied at all (it is not, at a rung that does not negotiate). */
105
+ contextSupplied: boolean;
106
+ /** The justification admitted into THIS rating. Withheld at round 1 by `contextFor`, by design. */
107
+ justification?: string;
108
+ /** `<negotiation_so_far>` as handed over: the completed rounds, oldest first. */
109
+ priorRounds: RaterNegotiationRound[];
110
+ /** `<user_messages>` as handed over, oldest first, before the builder's last-5 + truncation. */
111
+ userMessages: string[];
112
+ /** Whether that window carried anything. THE distinction this facility turns on. */
113
+ userMessagesPopulated: boolean;
114
+ /** Why the window looks the way it does, in words, for a reader who has never seen the spec. */
115
+ userMessagesNote: string;
116
+ }
117
+ /**
118
+ * One rating call: what the rater was SHOWN and what it ANSWERED, captured at the send site.
119
+ *
120
+ * Every field is filled by `rateShellCommand` itself. `prompt` is set before the call and the rest
121
+ * as the answer arrives, so a rater that never answers still leaves a record of what it was asked.
122
+ */
123
+ export interface RaterCallCapture {
124
+ /** ISO timestamp of the moment the call was sent. */
125
+ at: string;
126
+ /** Wall-clock ms the call took, once it has returned (or timed out). */
127
+ durationMs?: number;
128
+ /** The rater model's own label — an id and a provider type, never the instance. */
129
+ model?: string;
130
+ /** `approvals.rater` — the identity profile the rater model came from, when one is configured. */
131
+ profile?: string;
132
+ /** The budget this call was raced against ([[EXT-66]]). */
133
+ timeoutMs: number;
134
+ /** §5.2 — whether a rejection would be handed back to the AGENT rather than to a person. */
135
+ negotiable: boolean;
136
+ /** **The exact strings sent.** Captured, never re-rendered. */
137
+ prompt: {
138
+ system: string;
139
+ user: string;
140
+ };
141
+ /** What the negotiation looked like at the moment of this call. */
142
+ negotiation: RaterNegotiationCapture;
143
+ /** The model's answer BEFORE it is parsed or mapped, so a malformed one stays visible. */
144
+ rawResponse?: unknown;
145
+ /** The verdict this call resolved to, including a fail-closed one. */
146
+ verdict?: ShellSafetyVerdict;
147
+ /** Set when the verdict is the gate failing closed rather than the model judging ([[EXT-66]]). */
148
+ failClosed?: FailClosedCause;
149
+ }
150
+ /** A declared entry or runtime grant that decided the call, rendered as the user would read it. */
151
+ export interface ApprovalRuleMatchCapture {
152
+ action: 'allow' | 'deny' | 'escalate';
153
+ /** The entry in the words the menu and the notices use (`describeApprovalEntry`). */
154
+ entry: string;
155
+ /** §3.2 — whether an allow entry kept the rater involved as a tripwire. */
156
+ rate?: boolean;
157
+ }
158
+ /**
159
+ * §8's deterministic floor, when it matched.
160
+ *
161
+ * **This block NAMES THE MATCHED PATTERN, and that is a decision taken rather than an oversight.**
162
+ * §8.1 says the floor is never advertised, and [[CFG-31]] binds user-facing rung copy to that rule.
163
+ * The resolution taken for [[TUI-C27]] is that §8.1 governs **rung descriptions and promotional
164
+ * copy** — text that invites a user to feel safe — and not a diagnostic archive a user opens about
165
+ * their own session. "A floor matched" without saying which one leaves nobody able to act on it;
166
+ * the refusal the *user* sees ({@link import('./hardline.js').buildHardlineRefusal}) is unchanged
167
+ * and still names only the description.
168
+ */
169
+ export interface HardlineFloorCapture {
170
+ /** The human-readable description, as the refusal message carries it. */
171
+ description: string;
172
+ /** The matched pattern's source, or the stable token of the non-pattern arm. */
173
+ pattern: string;
174
+ }
175
+ /** A deterministic preflight finding, and whether it actually rewrote the rater's outcome. */
176
+ export interface PreflightFloorCapture {
177
+ kind: PreflightFloorKind;
178
+ reason: string;
179
+ /**
180
+ * Whether the floor changed the outcome. A preflight only ever RAISES, and only `safe` sits below
181
+ * the floor — so a finding on a `destructive` verdict is the floor AGREEING with the rater, not
182
+ * overriding it, and reporting those two the same way would misattribute the decision.
183
+ */
184
+ rewroteRating: boolean;
185
+ }
186
+ /** One gated tool call, from arrival to outcome. */
187
+ export interface ApprovalDecisionCapture {
188
+ /** ISO timestamp of the moment the call arrived at the gate. */
189
+ at: string;
190
+ /** The tool that was called. */
191
+ tool: string;
192
+ /** The command, for a `run_shell_command` call whose argument was a readable string. */
193
+ command?: string;
194
+ /** The rung in force for THIS decision (`/approvals <rung>` moves it mid-session). */
195
+ rung: ApprovalRung;
196
+ /** The stage that decided. `undefined` only if the decision threw before reaching one. */
197
+ stage?: ApprovalDecidingStage;
198
+ /** What became of the call. Always set by the time the decision returns or throws. */
199
+ action?: ApprovalCaptureAction;
200
+ /** The scope an approval was granted at, when one was. */
201
+ scope?: ToolApprovalScope;
202
+ /** Whether a person was actually asked, and what they said. */
203
+ humanAnswer?: ApprovalHumanAnswer;
204
+ /** §8's floor, when it matched. */
205
+ hardline?: HardlineFloorCapture;
206
+ /** The declared entry or runtime grant that decided the call, when one did. */
207
+ ruleMatch?: ApprovalRuleMatchCapture;
208
+ /** The deterministic preflight finding on this command, when there was one. */
209
+ preflight?: PreflightFloorCapture;
210
+ /**
211
+ * [[EXT-81]] — whether the gate's own parser could not statically resolve the command, so the
212
+ * rating carried a neutral note about the shape it saw.
213
+ *
214
+ * **This is what remains of "was the call an ABSTAIN".** `abstain` was an ACTION of its own until
215
+ * [[EXT-81]] retired it: a parser reporting that it could not read a string has detected nothing,
216
+ * so it no longer earns an action, and the command is now rated like any other. The observable
217
+ * that survived is this shape report, and the budget position that used to accompany it is
218
+ * {@link budget}.
219
+ */
220
+ parserUnresolved?: AbstentionDefect;
221
+ /** Where this call sat in §5.3's consecutive bound and the reachability bound, on arrival. */
222
+ budget: NegotiationCounters;
223
+ /** The rating, when one was made. Absent for every stage that decided without a model. */
224
+ rating?: RaterCallCapture;
225
+ /** The error that ended the decision, when one did. */
226
+ error?: string;
227
+ }
228
+ /**
229
+ * The per-session log of gated decisions.
230
+ *
231
+ * **Instance-scoped, on the runner, and never a module singleton** — the same reason
232
+ * `ShellNegotiationState` and the grant stores are: a concurrent ACP / AG-UI session must not
233
+ * inherit another session's approvals history, and a dump taken in one must not describe the other.
234
+ */
235
+ export declare class ApprovalCaptureLog {
236
+ private records;
237
+ /**
238
+ * Open a record for a gated call and return it LIVE, already in the buffer.
239
+ *
240
+ * The caller mutates the returned object as the decision is made. See the module docblock for why
241
+ * the push happens here rather than at the end: a halt throws out of the decision, and a record
242
+ * assembled at the end would be lost on exactly the calls most worth keeping.
243
+ */
244
+ begin(record: ApprovalDecisionCapture): ApprovalDecisionCapture;
245
+ /**
246
+ * The records so far, oldest first. A copy of the ARRAY: the records themselves are handed over
247
+ * live, because a decision still in flight is one the archive should show as it stands rather
248
+ * than not at all.
249
+ */
250
+ snapshot(): ApprovalDecisionCapture[];
251
+ /** Drop everything — the TUI's `/clear` rotates the thread. */
252
+ clear(): void;
253
+ }
254
+ /**
255
+ * A short, KEY-FREE label for the rater model: its id and its provider type, and nothing else.
256
+ *
257
+ * Deliberately not a param dump. A live `BaseChatModel` carries an `apiKey` and a client instance,
258
+ * and this string goes into an archive people attach to bug reports — so it reads a fixed handful of
259
+ * scalar fields through guards, exactly as `debugDump`'s own live-model descriptor does, rather than
260
+ * serialising anything of the instance.
261
+ */
262
+ export declare function raterModelLabel(model: unknown): string | undefined;
263
+ /**
264
+ * Describe the §5.1 context a rating is about to be made with — **as it was handed to the prompt
265
+ * builder**, so the record and the prompt cannot come to describe two different ratings.
266
+ *
267
+ * The note is the legibility half of the acceptance: an empty user-messages window means one thing
268
+ * at round 1 (by design) and something else entirely at round 3, and a reader of the archive has no
269
+ * way to tell them apart from a `[]`.
270
+ */
271
+ export declare function describeRaterNegotiation(negotiation: RaterNegotiationContext | undefined): RaterNegotiationCapture;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * How many gated decisions one session keeps. A ring buffer for the same reason the debug-log one
3
+ * is: each record carries a full rating prompt (a few KB), and a long `auto` session makes a lot of
4
+ * them. The newest are the ones a bug report is about, so the oldest are evicted.
5
+ */
6
+ export const APPROVAL_CAPTURE_MAX = 50;
7
+ /**
8
+ * The per-session log of gated decisions.
9
+ *
10
+ * **Instance-scoped, on the runner, and never a module singleton** — the same reason
11
+ * `ShellNegotiationState` and the grant stores are: a concurrent ACP / AG-UI session must not
12
+ * inherit another session's approvals history, and a dump taken in one must not describe the other.
13
+ */
14
+ export class ApprovalCaptureLog {
15
+ records = [];
16
+ /**
17
+ * Open a record for a gated call and return it LIVE, already in the buffer.
18
+ *
19
+ * The caller mutates the returned object as the decision is made. See the module docblock for why
20
+ * the push happens here rather than at the end: a halt throws out of the decision, and a record
21
+ * assembled at the end would be lost on exactly the calls most worth keeping.
22
+ */
23
+ begin(record) {
24
+ this.records.push(record);
25
+ if (this.records.length > APPROVAL_CAPTURE_MAX)
26
+ this.records.shift();
27
+ return record;
28
+ }
29
+ /**
30
+ * The records so far, oldest first. A copy of the ARRAY: the records themselves are handed over
31
+ * live, because a decision still in flight is one the archive should show as it stands rather
32
+ * than not at all.
33
+ */
34
+ snapshot() {
35
+ return [...this.records];
36
+ }
37
+ /** Drop everything — the TUI's `/clear` rotates the thread. */
38
+ clear() {
39
+ this.records = [];
40
+ }
41
+ }
42
+ /**
43
+ * A short, KEY-FREE label for the rater model: its id and its provider type, and nothing else.
44
+ *
45
+ * Deliberately not a param dump. A live `BaseChatModel` carries an `apiKey` and a client instance,
46
+ * and this string goes into an archive people attach to bug reports — so it reads a fixed handful of
47
+ * scalar fields through guards, exactly as `debugDump`'s own live-model descriptor does, rather than
48
+ * serialising anything of the instance.
49
+ */
50
+ export function raterModelLabel(model) {
51
+ if (!model || typeof model !== 'object')
52
+ return undefined;
53
+ const m = model;
54
+ let type;
55
+ try {
56
+ type = typeof m._llmType === 'function' ? m._llmType() : undefined;
57
+ }
58
+ catch {
59
+ type = undefined;
60
+ }
61
+ const id = [m.model, m.modelName, m.modelId].find((value) => typeof value === 'string');
62
+ if (id && type)
63
+ return `${type}/${id}`;
64
+ return id ?? type;
65
+ }
66
+ /**
67
+ * Describe the §5.1 context a rating is about to be made with — **as it was handed to the prompt
68
+ * builder**, so the record and the prompt cannot come to describe two different ratings.
69
+ *
70
+ * The note is the legibility half of the acceptance: an empty user-messages window means one thing
71
+ * at round 1 (by design) and something else entirely at round 3, and a reader of the archive has no
72
+ * way to tell them apart from a `[]`.
73
+ */
74
+ export function describeRaterNegotiation(negotiation) {
75
+ const priorRounds = [...(negotiation?.priorRounds ?? [])];
76
+ const userMessages = [...(negotiation?.userMessages ?? [])];
77
+ const roundOne = priorRounds.length === 0;
78
+ const populated = userMessages.length > 0;
79
+ return {
80
+ round: priorRounds.length + 1,
81
+ roundOne,
82
+ contextSupplied: negotiation !== undefined,
83
+ ...(negotiation?.justification !== undefined
84
+ ? { justification: negotiation.justification }
85
+ : {}),
86
+ priorRounds,
87
+ userMessages,
88
+ userMessagesPopulated: populated,
89
+ userMessagesNote: userMessagesNote(negotiation !== undefined, roundOne, populated),
90
+ };
91
+ }
92
+ /** The sentence {@link describeRaterNegotiation} puts on the window, for a reader with no spec. */
93
+ function userMessagesNote(contextSupplied, roundOne, populated) {
94
+ if (!contextSupplied) {
95
+ return ('No negotiation context was supplied for this rating (the rung does not negotiate, or this ' +
96
+ 'was the allow-list tripwire), so the rater saw the command alone. An empty window here is ' +
97
+ 'not a defect.');
98
+ }
99
+ if (roundOne) {
100
+ return ('Round 1: spec §5.1 admits no justification, no transcript and no user messages, so the ' +
101
+ 'rater saw the command alone. An empty window here is BY DESIGN, not a defect.');
102
+ }
103
+ return populated
104
+ ? "Round 2 or later: the user's own recent messages WERE in the rater's view for this rating."
105
+ : 'Round 2 or later, and the window is still empty — the conversation had no non-blank user ' +
106
+ 'message to admit. This one IS worth looking at.';
107
+ }
108
+ //# sourceMappingURL=approvalCapture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approvalCapture.js","sourceRoot":"","sources":["../../../src/core/shell/approvalCapture.ts"],"names":[],"mappings":"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"}
@@ -26,9 +26,8 @@
26
26
  */
27
27
  /**
28
28
  * Base class for the two run-ending approvals outcomes, so a caller that wants to present them as
29
- * an ending rather than a crash can catch both with one `instanceof`. Nothing does that today —
30
- * every surface currently shows the message as-is, which is already the whole explanation — and
31
- * [[TUI-C26]] is the node that will catch it to render the §6.1 banner.
29
+ * an ending rather than a crash can catch both with one `instanceof`. Every surface shows the
30
+ * message as-is, which is already the whole explanation.
32
31
  */
33
32
  export declare abstract class ApprovalStopError extends Error {
34
33
  /** The command that ended the run. */
@@ -40,9 +39,11 @@ export declare abstract class ApprovalStopError extends Error {
40
39
  * credential targeting, privilege escalation, persistence, deception, obfuscation). Ends the agent
41
40
  * loop; the model is told nothing and offered nothing.
42
41
  *
43
- * [[TUI-C26]] will present this interactively as the §6.1 red banner any key stops, typing
44
- * `run anyway` runs this one command BEFORE the throw; until then the run simply ends with this
45
- * message, which is what a non-interactive session gets either way (§6.2).
42
+ * **This is what reaches a surface that cannot ask.** An interactive surface is offered §6.1's red
43
+ * banner first`GthAgentRunner.setAttackHaltCallback`, where typing `run anyway` runs this one
44
+ * command and everything else stops the run — and this error is thrown when no banner is wired, or
45
+ * when the banner is answered with anything but that phrase. A non-interactive session wires
46
+ * nothing and so gets this message directly (§6.2).
46
47
  *
47
48
  * The recovery this message names is deliberately the **allow-list**, not `bypass`. §4.2 makes
48
49
  * `approvals.allow` the supported way to run such a command unattended (it is consulted before the
@@ -71,5 +72,13 @@ export declare class NonInteractiveEscalationError extends ApprovalStopError {
71
72
  * `escalate` outranks a match on `allow`.
72
73
  */
73
74
  readonly escalatedBy: string | undefined;
74
- constructor(command: string, outcome?: string, reason?: string, escalatedBy?: string);
75
+ /**
76
+ * [[EXT-29]] §6 — the §5 negotiation that preceded this escalation, rendered, when there was one.
77
+ *
78
+ * §6.2's message is the ONLY thing a person sees on this path — there is no prompt to attach a
79
+ * transcript to — so an unattended run that ended after three rejections would otherwise report
80
+ * the last command and give no hint that the agent had already been told twice what to fix.
81
+ */
82
+ readonly negotiation: string | undefined;
83
+ constructor(command: string, outcome?: string, reason?: string, escalatedBy?: string, negotiation?: string);
75
84
  }
@@ -26,9 +26,8 @@
26
26
  */
27
27
  /**
28
28
  * Base class for the two run-ending approvals outcomes, so a caller that wants to present them as
29
- * an ending rather than a crash can catch both with one `instanceof`. Nothing does that today —
30
- * every surface currently shows the message as-is, which is already the whole explanation — and
31
- * [[TUI-C26]] is the node that will catch it to render the §6.1 banner.
29
+ * an ending rather than a crash can catch both with one `instanceof`. Every surface shows the
30
+ * message as-is, which is already the whole explanation.
32
31
  */
33
32
  export class ApprovalStopError extends Error {
34
33
  /** The command that ended the run. */
@@ -46,9 +45,11 @@ export class ApprovalStopError extends Error {
46
45
  * credential targeting, privilege escalation, persistence, deception, obfuscation). Ends the agent
47
46
  * loop; the model is told nothing and offered nothing.
48
47
  *
49
- * [[TUI-C26]] will present this interactively as the §6.1 red banner any key stops, typing
50
- * `run anyway` runs this one command BEFORE the throw; until then the run simply ends with this
51
- * message, which is what a non-interactive session gets either way (§6.2).
48
+ * **This is what reaches a surface that cannot ask.** An interactive surface is offered §6.1's red
49
+ * banner first`GthAgentRunner.setAttackHaltCallback`, where typing `run anyway` runs this one
50
+ * command and everything else stops the run — and this error is thrown when no banner is wired, or
51
+ * when the banner is answered with anything but that phrase. A non-interactive session wires
52
+ * nothing and so gets this message directly (§6.2).
52
53
  *
53
54
  * The recovery this message names is deliberately the **allow-list**, not `bypass`. §4.2 makes
54
55
  * `approvals.allow` the supported way to run such a command unattended (it is consulted before the
@@ -86,11 +87,20 @@ export class NonInteractiveEscalationError extends ApprovalStopError {
86
87
  * `escalate` outranks a match on `allow`.
87
88
  */
88
89
  escalatedBy;
89
- constructor(command, outcome, reason, escalatedBy) {
90
+ /**
91
+ * [[EXT-29]] §6 — the §5 negotiation that preceded this escalation, rendered, when there was one.
92
+ *
93
+ * §6.2's message is the ONLY thing a person sees on this path — there is no prompt to attach a
94
+ * transcript to — so an unattended run that ended after three rejections would otherwise report
95
+ * the last command and give no hint that the agent had already been told twice what to fix.
96
+ */
97
+ negotiation;
98
+ constructor(command, outcome, reason, escalatedBy, negotiation) {
90
99
  super(`Approval required, but this session has no one to ask.\n` +
91
100
  ` Command: ${command}\n` +
92
101
  (outcome ? ` Rating: ${outcome}\n` : '') +
93
102
  (reason ? ` Reason: ${reason}\n` : '') +
103
+ (negotiation ? `${negotiation}\n` : '') +
94
104
  (escalatedBy
95
105
  ? ` Matched approvals.escalate: ${escalatedBy}\n` +
96
106
  `An escalate entry always asks a human, whatever the rung would have done, so no ` +
@@ -103,6 +113,7 @@ export class NonInteractiveEscalationError extends ApprovalStopError {
103
113
  this.outcome = outcome;
104
114
  this.reason = reason;
105
115
  this.escalatedBy = escalatedBy;
116
+ this.negotiation = negotiation;
106
117
  }
107
118
  }
108
119
  //# sourceMappingURL=approvalStop.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"approvalStop.js","sourceRoot":"","sources":["../../../src/core/shell/approvalStop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;;GAKG;AACH,MAAM,OAAgB,iBAAkB,SAAQ,KAAK;IACnD,sCAAsC;IAC7B,OAAO,CAAS;IAEzB,YAAsB,OAAe,EAAE,OAAe;QACpD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,+FAA+F;QAC/F,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC;QAClD,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,eAAgB,SAAQ,iBAAiB;IACpD,sEAAsE;IAC7D,MAAM,CAAS;IAExB,YAAY,OAAe,EAAE,MAAc;QACzC,KAAK,CACH,mFAAmF;YACjF,cAAc,OAAO,IAAI;YACzB,aAAa,MAAM,IAAI;YACvB,wFAAwF;YACxF,oFAAoF;YACpF,yFAAyF;YACzF,wDAAwD,EAC1D,OAAO,CACR,CAAC;QACF,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,6BAA8B,SAAQ,iBAAiB;IAClE,gFAAgF;IACvE,OAAO,CAAqB;IACrC,sDAAsD;IAC7C,MAAM,CAAqB;IACpC;;;;;OAKG;IACM,WAAW,CAAqB;IAEzC,YAAY,OAAe,EAAE,OAAgB,EAAE,MAAe,EAAE,WAAoB;QAClF,KAAK,CACH,0DAA0D;YACxD,cAAc,OAAO,IAAI;YACzB,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,MAAM,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACvC,CAAC,WAAW;gBACV,CAAC,CAAC,iCAAiC,WAAW,IAAI;oBAChD,kFAAkF;oBAClF,oFAAoF;oBACpF,wBAAwB;gBAC1B,CAAC,CAAC,sFAAsF;oBACtF,+EAA+E;oBAC/E,kFAAkF;oBAClF,YAAY,CAAC,EACnB,OAAO,CACR,CAAC;QACF,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;CACF"}
1
+ {"version":3,"file":"approvalStop.js","sourceRoot":"","sources":["../../../src/core/shell/approvalStop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;GAIG;AACH,MAAM,OAAgB,iBAAkB,SAAQ,KAAK;IACnD,sCAAsC;IAC7B,OAAO,CAAS;IAEzB,YAAsB,OAAe,EAAE,OAAe;QACpD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,+FAA+F;QAC/F,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC;QAClD,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,eAAgB,SAAQ,iBAAiB;IACpD,sEAAsE;IAC7D,MAAM,CAAS;IAExB,YAAY,OAAe,EAAE,MAAc;QACzC,KAAK,CACH,mFAAmF;YACjF,cAAc,OAAO,IAAI;YACzB,aAAa,MAAM,IAAI;YACvB,wFAAwF;YACxF,oFAAoF;YACpF,yFAAyF;YACzF,wDAAwD,EAC1D,OAAO,CACR,CAAC;QACF,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,6BAA8B,SAAQ,iBAAiB;IAClE,gFAAgF;IACvE,OAAO,CAAqB;IACrC,sDAAsD;IAC7C,MAAM,CAAqB;IACpC;;;;;OAKG;IACM,WAAW,CAAqB;IAEzC;;;;;;OAMG;IACM,WAAW,CAAqB;IAEzC,YACE,OAAe,EACf,OAAgB,EAChB,MAAe,EACf,WAAoB,EACpB,WAAoB;QAEpB,KAAK,CACH,0DAA0D;YACxD,cAAc,OAAO,IAAI;YACzB,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,MAAM,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACvC,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACvC,CAAC,WAAW;gBACV,CAAC,CAAC,iCAAiC,WAAW,IAAI;oBAChD,kFAAkF;oBAClF,oFAAoF;oBACpF,wBAAwB;gBAC1B,CAAC,CAAC,sFAAsF;oBACtF,+EAA+E;oBAC/E,kFAAkF;oBAClF,YAAY,CAAC,EACnB,OAAO,CACR,CAAC;QACF,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;CACF"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @module core/shell/escalationSeverity
3
+ *
4
+ * [[TUI-C26]] (spec §6) — **how severe the escalation is, said in words.**
5
+ *
6
+ * Every escalation used to look identical: the same yellow line, the same
7
+ * `⚠ Auto-rater (<outcome>):`, on both surfaces. So `npm install lodash` and a typosquatted
8
+ * `curl | bash` produced the same dialog, and a dialog that looks the same for everything trains
9
+ * the reader to answer it the same way — which costs more than having no dialog at all.
10
+ *
11
+ * ## Why the words carry it, not the colour
12
+ *
13
+ * Colour is not reliably available. `NO_COLOR` is set on plenty of machines, output gets piped, a
14
+ * monochrome or high-contrast terminal renders every tone identically, and a reader may simply not
15
+ * be looking at hue. So each outcome carries **three** independent signals — a glyph, a tone and a
16
+ * sentence — and the sentence is the one that always arrives. A change that makes two outcomes read
17
+ * the same in words has removed the signal even if the colours still differ.
18
+ *
19
+ * **What the sentence says is the consequence, not the severity word again.** `catastrophic` is
20
+ * defined by needing something from *outside* the session to undo (rescue media, a backup, a
21
+ * re-provision), and that — not the adjective — is what a person can act on.
22
+ *
23
+ * ## The map is total, and `attack` is in it
24
+ *
25
+ * A `Record<RaterOutcome, …>` rather than a lookup with a default: an outcome added to
26
+ * {@link RATER_OUTCOMES} must be given words here, and cannot silently inherit another outcome's.
27
+ *
28
+ * `attack` is present for that totality and **not because the approval prompt renders it**. An
29
+ * `attack` verdict halts the run (`AttackHaltError`) on both rating paths, so it never reaches an
30
+ * approval dialog: it is answered at [[TUI-C68]]'s §6.1 banner, whose copy is
31
+ * {@link attackBannerCopy} — here, beside the heading it reuses, so the two cannot come to describe
32
+ * one verdict two ways.
33
+ *
34
+ * `attack` shares the `danger` tone and glyph with `catastrophic` deliberately: the two are not
35
+ * ranked against each other (they answer different questions — *can this be undone?* versus *is
36
+ * something hostile acting here?*), so distinguishing them by loudness would assert an ordering the
37
+ * rater's own schema refuses. They are distinguished where it matters, in what they say.
38
+ */
39
+ import type { RaterOutcome } from '#src/core/shell/rater.js';
40
+ /**
41
+ * How loud a surface should be about an outcome. A surface maps this to its own vocabulary — Ink
42
+ * colours on the TUI, the `display*` channel on the readline prompt — so neither has to hold its
43
+ * own opinion about which outcome is worse than which.
44
+ */
45
+ export type EscalationTone = 'notice' | 'warn' | 'danger';
46
+ /** Everything a surface needs to render one outcome, so no surface invents its own wording. */
47
+ export interface RaterOutcomeDisplay {
48
+ /**
49
+ * The heading a surface paints above the rater's reason, glyph and sentence included.
50
+ *
51
+ * It keeps the `Auto-rater (<outcome>):` opening the dialog has always had — that is the
52
+ * attribution, and the reason underneath it is the rater's, not the gate's — and adds the
53
+ * consequence after it.
54
+ */
55
+ heading: string;
56
+ /** The tone the heading and the framed reason are painted in. */
57
+ tone: EscalationTone;
58
+ }
59
+ /**
60
+ * The label above the framed reason.
61
+ *
62
+ * The heading now carries a sentence of the gate's own, so without this the model-authored prose
63
+ * beneath it could be read as a continuation of what the gate said. §6 wants the opposite: the
64
+ * reason is the rater's, quoted, and it is framed exactly like the command for the same reason.
65
+ */
66
+ export declare const RATER_REASON_LABEL = " the rater's own words:";
67
+ /** How to render {@link RaterOutcome} on an approval dialog. */
68
+ export declare function describeRaterOutcome(outcome: RaterOutcome): RaterOutcomeDisplay;
69
+ /**
70
+ * [[TUI-C68]] §6.1 — **the phrase that runs an `attack`-rated command anyway.** The only string on
71
+ * the banner that is not a refusal.
72
+ *
73
+ * A typed phrase rather than a key, because an irreversible decision must not be reachable by the
74
+ * muscle memory built answering routine prompts. A key merely *disjoint from today's menu* stops
75
+ * being disjoint the moment someone binds one; a phrase is immune to any future binding. `run`
76
+ * alone was considered and rejected as the leading token of half of what anyone types into a
77
+ * terminal.
78
+ *
79
+ * The label is never `bypass`: that is a rung (§2.5) and a far broader thing — it turns off the
80
+ * rater, the escalation and the halt together, for every command, for the whole run — and a user
81
+ * must never read this banner as switching to it.
82
+ */
83
+ export declare const RUN_ANYWAY_PHRASE = "run anyway";
84
+ /**
85
+ * §6.1 — does what the human typed grant this one command?
86
+ *
87
+ * **The whole matching rule, in one place both surfaces call**, so what the banner *says* is
88
+ * answerable and what a surface *accepts* cannot drift. Trimmed, lower-cased, compared whole:
89
+ * `RUN ANYWAY` and a phrase with surrounding spaces grant; `run`, `runanyway`, `run anyway please`
90
+ * and the phrase with a doubled inner space do not. No prefix, no initial, no `y`, no alias.
91
+ *
92
+ * **Everything that is not the phrase is a refusal, and that is the property the banner rests on.**
93
+ * An approval prompt grants only on an offered key and treats every other keystroke as a rejection;
94
+ * a text buffer inverts that by accumulating keystrokes instead of rejecting them, and a matcher
95
+ * this narrow is what puts it back — the buffer may hold anything at all, and only one value of it
96
+ * runs the command.
97
+ *
98
+ * Deliberately NOT whitespace-normalising the middle: a rule that repairs what the user typed is a
99
+ * rule that grants on something they did not type, and retyping a phrase costs nothing next to
100
+ * running this command by accident.
101
+ */
102
+ export declare function grantsRunAnyway(typed: string): boolean;
103
+ /**
104
+ * §6.1 — the banner's own words, shared so no surface invents its own.
105
+ *
106
+ * It carries what every surface must say and nothing about how any one of them is driven: the
107
+ * readline prompt reads a line in cooked mode and has no `q` or `Esc` to bind, so a keyboard line
108
+ * here would be false on one of the two surfaces. A surface adds its own keys beside these.
109
+ */
110
+ export interface AttackBannerCopy {
111
+ /** The banner's title row — what happened, above everything else. */
112
+ title: string;
113
+ /** The `attack` heading from {@link describeRaterOutcome}, glyph and consequence included. */
114
+ heading: string;
115
+ /**
116
+ * **The irreversibility line, and it is UNCONDITIONAL** — on every attack banner, whatever the
117
+ * rating said, and not only where the command was also rated `catastrophic`.
118
+ *
119
+ * The banner is rare by construction (§4.1.1 forbids `attack` firing on ordinary work), so the
120
+ * line cannot become noise; and a static string cannot fail the way a model's explanation can. It
121
+ * is the last thing between a human and an action nothing else will stop.
122
+ */
123
+ irreversible: string;
124
+ /** What each control does, in the phrase's own words. */
125
+ controls: readonly string[];
126
+ /** The label a surface puts in front of the buffer the human types into. */
127
+ prompt: string;
128
+ /**
129
+ * What a surface says once the phrase has been accepted.
130
+ *
131
+ * It states the scope of what was just granted, because that is the half a user cannot observe:
132
+ * the command running is visible, and *only this one running* is not. Shared so neither surface
133
+ * can promise a persistence the runner does not perform.
134
+ */
135
+ granted: string;
136
+ }
137
+ /**
138
+ * §6.1 — the banner copy. A function rather than an exported object so one surface cannot mutate
139
+ * the strings another is about to paint.
140
+ */
141
+ export declare function attackBannerCopy(): AttackBannerCopy;