@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.
- package/dist/config/schema.d.ts +48 -48
- package/dist/config/schema.js +55 -29
- package/dist/config/schema.js.map +1 -1
- package/dist/config/shell-policy.d.ts +241 -26
- package/dist/config/shell-policy.js +353 -52
- package/dist/config/shell-policy.js.map +1 -1
- package/dist/config/tool-descriptions.d.ts +78 -32
- package/dist/config/tool-descriptions.js +72 -29
- package/dist/config/tool-descriptions.js.map +1 -1
- package/dist/config/types.d.ts +9 -4
- package/dist/config/types.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +13 -5
- package/dist/core/GthAbstractAgent.js +42 -13
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +203 -9
- package/dist/core/GthAgentRunner.js +646 -39
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.js +58 -17
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/reasoningBlocks.d.ts +60 -0
- package/dist/core/reasoningBlocks.js +98 -0
- package/dist/core/reasoningBlocks.js.map +1 -0
- package/dist/core/refusal.js +6 -2
- package/dist/core/refusal.js.map +1 -1
- package/dist/core/shell/approvalCapture.d.ts +271 -0
- package/dist/core/shell/approvalCapture.js +108 -0
- package/dist/core/shell/approvalCapture.js.map +1 -0
- package/dist/core/shell/approvalStop.d.ts +16 -7
- package/dist/core/shell/approvalStop.js +18 -7
- package/dist/core/shell/approvalStop.js.map +1 -1
- package/dist/core/shell/escalationSeverity.d.ts +141 -0
- package/dist/core/shell/escalationSeverity.js +89 -0
- package/dist/core/shell/escalationSeverity.js.map +1 -0
- package/dist/core/shell/framing.d.ts +190 -0
- package/dist/core/shell/framing.js +633 -0
- package/dist/core/shell/framing.js.map +1 -0
- package/dist/core/shell/hardline.d.ts +81 -0
- package/dist/core/shell/hardline.js +607 -0
- package/dist/core/shell/hardline.js.map +1 -0
- package/dist/core/shell/negotiation.d.ts +249 -0
- package/dist/core/shell/negotiation.js +355 -0
- package/dist/core/shell/negotiation.js.map +1 -0
- package/dist/core/shell/normalize.d.ts +5 -5
- package/dist/core/shell/normalize.js +5 -5
- package/dist/core/shell/rater.d.ts +353 -18
- package/dist/core/shell/rater.js +556 -39
- package/dist/core/shell/rater.js.map +1 -1
- package/dist/core/shell/rejection.d.ts +1 -1
- package/dist/core/types.d.ts +97 -4
- package/dist/providers/geminiThinking.d.ts +52 -0
- package/dist/providers/geminiThinking.js +72 -0
- package/dist/providers/geminiThinking.js.map +1 -0
- package/dist/providers/google-genai.js +4 -1
- package/dist/providers/google-genai.js.map +1 -1
- package/dist/providers/vertexai.js +4 -1
- package/dist/providers/vertexai.js.map +1 -1
- package/dist/runtime/askStructured.d.ts +45 -3
- package/dist/runtime/askStructured.js +46 -5
- package/dist/runtime/askStructured.js.map +1 -1
- package/dist/runtime/conversation.d.ts +2 -1
- package/dist/runtime/conversation.js +2 -1
- package/dist/runtime/conversation.js.map +1 -1
- package/dist/runtime/singleShot.d.ts +3 -2
- package/dist/runtime/singleShot.js +3 -2
- package/dist/runtime/singleShot.js.map +1 -1
- package/dist/utils/debugDump.d.ts +19 -0
- package/dist/utils/debugDump.js +11 -0
- package/dist/utils/debugDump.js.map +1 -1
- package/dist/utils/displayWidth.d.ts +23 -0
- package/dist/utils/displayWidth.js +61 -6
- package/dist/utils/displayWidth.js.map +1 -1
- package/dist/utils/systemPromptNotes.d.ts +12 -1
- package/dist/utils/systemPromptNotes.js +24 -2
- package/dist/utils/systemPromptNotes.js.map +1 -1
- package/package.json +1 -1
- package/schema/gsloth-config.schema.json +48 -48
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* @module core/shell/normalize
|
|
3
3
|
*
|
|
4
4
|
* Command-string normalization shared by the shell hardening layer. The hardline
|
|
5
|
-
* blocklist (
|
|
5
|
+
* blocklist (`core/shell/hardline`) and the EXT-9 Tier-2
|
|
6
6
|
* allow-list classifier ({@link ./arity.js}) both match against the *normalized* form so
|
|
7
7
|
* trivial obfuscation (ANSI escapes, fullwidth glyphs, backslash splits, padded
|
|
8
|
-
* whitespace) cannot smuggle a command past the guard. Canonical home is core so
|
|
9
|
-
*
|
|
8
|
+
* whitespace) cannot smuggle a command past the guard. Canonical home is core so every
|
|
9
|
+
* consumer — the allow-list, the hardline floor, the approvals gate — imports one implementation.
|
|
10
10
|
*
|
|
11
11
|
* Patterned after hermes-agent `tools/approval.py:_normalize_command_for_detection`.
|
|
12
12
|
*/
|
|
@@ -32,8 +32,8 @@ const LINE_BREAK_RUN = /\s*\n\s*/g;
|
|
|
32
32
|
* every character at which the shell stops one command and starts the next: `;`, `&` (hence
|
|
33
33
|
* `&&`), `|` (hence `||`), and a LINE BREAK. Both consumers of the normalized form build their
|
|
34
34
|
* patterns from it — the allow-list classifier's fail-closed check
|
|
35
|
-
* ({@link import('./arity.js').classifyCommand}) and the
|
|
36
|
-
* terminators (
|
|
35
|
+
* ({@link import('./arity.js').classifyCommand}) and the hardline blocklist's pattern
|
|
36
|
+
* terminators (`core/shell/hardline`) — so the two layers can never again
|
|
37
37
|
* disagree about what a separator is.
|
|
38
38
|
*
|
|
39
39
|
* The layers disagreed before EXT-55: `;`/`&&`/`|` made a command ambiguous (fail-closed) but a
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* CFG-27 (CFG-26 rework) — the **auto-rater**: the LLM that rates a pending `run_shell_command`
|
|
5
5
|
* before it executes, sitting *in front of* the human approval prompt (EXT-9). It is consulted at
|
|
6
|
-
* exactly two of the five rungs — `
|
|
6
|
+
* exactly two of the five rungs — `assisted` and `auto` (see `APPROVAL_RUNGS`); `manual`,
|
|
7
7
|
* `write` and `bypass` are fully deterministic and never pay for a model call.
|
|
8
8
|
*
|
|
9
9
|
* NOTE ON THE NAME: "judge" is reserved for the **eval grader** (`gth eval --judge <profile>`,
|
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
* 1. **Prompt-injection defense.** The command is attacker-controlled text. It is normalized
|
|
18
18
|
* (reusing {@link normalizeCommand} + home-path folding) and embedded inside an XML
|
|
19
19
|
* `<command_to_evaluate>` tag, behind a preamble that states the tagged text is UNTRUSTED
|
|
20
|
-
* DATA to be analyzed, never instructions to follow. See {@link buildRaterPrompt}.
|
|
20
|
+
* DATA to be analyzed, never instructions to follow. See {@link buildRaterPrompt}. §5.1's
|
|
21
|
+
* negotiation context ([[EXT-29]]) is admitted on exactly those terms — the justification, the
|
|
22
|
+
* prior rounds and the last user messages are each fenced in the user message, and
|
|
23
|
+
* {@link RATER_NEGOTIATION_GUIDANCE} extends the untrusted-data contract to their tags. Nothing
|
|
24
|
+
* else from the conversation is admitted at any round: no tool output, no file contents, no
|
|
25
|
+
* fetched pages (§4.3).
|
|
21
26
|
* 2. **Fail-closed on error.** If the LLM call throws, times out, or returns unparseable
|
|
22
27
|
* output, the verdict returned NEVER auto-approves — it is `destructive` with an honest
|
|
23
28
|
* "could not assess" reason. A rater failure can never silently green-light a command.
|
|
@@ -34,6 +39,7 @@ import type { BaseChatModel } from '@langchain/core/language_models/chat_models'
|
|
|
34
39
|
import * as z from 'zod';
|
|
35
40
|
import type { ApprovalRung, GrantedToolSummary, GthConfig } from '#src/config.js';
|
|
36
41
|
import type { EffectiveToolAnnotations } from '#src/core/approvals/matcher.js';
|
|
42
|
+
import { type RaterCallCapture } from '#src/core/shell/approvalCapture.js';
|
|
37
43
|
/**
|
|
38
44
|
* CFG-28 (spec §4.1) — the **four** outcomes the rater may return. The retired third outcome named
|
|
39
45
|
* the halt trigger by *mechanism* while positioning it as the top *severity*; the rescale (§11.1)
|
|
@@ -212,7 +218,7 @@ export declare const RATER_CATASTROPHIC_GUIDANCE: string;
|
|
|
212
218
|
* definition.**
|
|
213
219
|
*
|
|
214
220
|
* The reason is worth restating where the prompt author will read it: `attack` is the only outcome
|
|
215
|
-
* that halts the run, the halt is not negotiable, and from `
|
|
221
|
+
* that halts the run, the halt is not negotiable, and from `assisted` — the default rung for
|
|
216
222
|
* everyone — a non-interactive run has no key at all and the only ways through are an allow-list
|
|
217
223
|
* entry or dropping to `bypass`, i.e. one step from the default gate to no gate. An outcome that
|
|
218
224
|
* expensive MUST NOT fire on ordinary work, so the test is **what the command's own structure
|
|
@@ -223,7 +229,7 @@ export declare const RATER_CATASTROPHIC_GUIDANCE: string;
|
|
|
223
229
|
* - **Origin trust is not the rater's job** (§4.1.1's measured note). A cheap model rated
|
|
224
230
|
* `curl https://pypi.org.packages-cdn.io/simple/ -o index.html` as `safe` while *naming the
|
|
225
231
|
* deception in its own reasoning*, and split identically-shaped commands at random. A property
|
|
226
|
-
* that holds only on the good model is not a property, and `
|
|
232
|
+
* that holds only on the good model is not a property, and `assisted` is the default for people
|
|
227
233
|
* pointed at small local models. `curl … | <shell>` is therefore `destructive` whatever the host.
|
|
228
234
|
* - **Clause (1) is narrowed** (§11.1b, 2026-07-28). A secret handed to a tool that is doing some
|
|
229
235
|
* other job is not credential targeting; a secret exposed for its own sake is. Read literally,
|
|
@@ -266,6 +272,62 @@ export declare const RATER_ATTACK_GUIDANCE: string;
|
|
|
266
272
|
* makes the `destructive` half of §4.6.1 meaningful, not what gates the upgrade.**
|
|
267
273
|
*/
|
|
268
274
|
export declare const RATER_DECEPTION_GUIDANCE: string;
|
|
275
|
+
/**
|
|
276
|
+
* [[EXT-29]] (spec §5.1) — how to WEIGH the negotiation context, added to the SYSTEM prompt for
|
|
277
|
+
* exactly the ratings that carry one ({@link buildNegotiationContextBlock}).
|
|
278
|
+
*
|
|
279
|
+
* **It is conditional on the CONTEXT, and that is the design rather than an optimisation.** Round 1
|
|
280
|
+
* is deliberately context-free (§5.1): there is no justification to weigh and no transcript to
|
|
281
|
+
* reason from, so a prompt explaining how a justification may lower an outcome has nothing to
|
|
282
|
+
* govern and everything to prime. Tying it to the block means the two can never disagree — the
|
|
283
|
+
* rules about weighing the extra context appear exactly when the extra context does.
|
|
284
|
+
*
|
|
285
|
+
* **§5.2's wording rules are NOT here, and the split is the point.** They are scoped by *whether
|
|
286
|
+
* the rejection is addressed to the agent at all* — which is what `auto` means — not by whether a
|
|
287
|
+
* transcript happens to exist yet; see {@link RATER_NEGOTIABLE_REJECTION_GUIDANCE}.
|
|
288
|
+
*
|
|
289
|
+
* Three rules are normative and none may be softened into another:
|
|
290
|
+
*
|
|
291
|
+
* - **A justification may only ever LOWER a rating** — *lower* meaning **less severe**. It may move
|
|
292
|
+
* `destructive` to `safe`; it may never move `safe` to `destructive`. That is the permissive
|
|
293
|
+
* direction on purpose: a negotiation the justification cannot win is not a negotiation.
|
|
294
|
+
* - **A stated intent that does not match what the command does is grounds for REJECTION, not for a
|
|
295
|
+
* discount.** This is the counterweight to the rule above and is not optional — the justification
|
|
296
|
+
* is the one place in the whole design where attacker-influenceable text can *reduce* an outcome.
|
|
297
|
+
* - **`attack` and `catastrophic` are exempt from the entire mechanism** (§4.2). No justification,
|
|
298
|
+
* no revision and no accumulated context moves either.
|
|
299
|
+
*
|
|
300
|
+
* The opening clause extends {@link RATER_SYSTEM_PREAMBLE}'s untrusted-data contract to the new
|
|
301
|
+
* tags. The preamble names `<command_to_evaluate>` alone and cannot be edited without breaking
|
|
302
|
+
* round-1 byte-identity, so the tags this block introduces are declared untrusted *here*, in the
|
|
303
|
+
* same prompt that introduces them.
|
|
304
|
+
*/
|
|
305
|
+
export declare const RATER_NEGOTIATION_CONTEXT_GUIDANCE: string;
|
|
306
|
+
/**
|
|
307
|
+
* [[EXT-29]] (spec §5.2) — **how a rejection must be WORDED, added whenever the rejection will be
|
|
308
|
+
* read by the agent** rather than by a person.
|
|
309
|
+
*
|
|
310
|
+
* **This is scoped by MODE, not by round, and the distinction is the whole of §5.2.** §5.1 governs
|
|
311
|
+
* what *context* a rating is allowed to see, and round 1 sees the command alone; §5.2 governs how a
|
|
312
|
+
* rejection is *written*, and a rejection is written the same way in every round of a negotiation
|
|
313
|
+
* — including the first, which is the round §5.6's escalation example requires to name the fix
|
|
314
|
+
* (*"Name the commits to drop, or use `--soft`"*). What decides whether these rules apply at all is
|
|
315
|
+
* whether the rejection is *addressed to the agent*: at `auto` it is, at `assisted` a `destructive`
|
|
316
|
+
* outcome goes to the human instead, so *"MUST invite a response"* would be addressed to nobody.
|
|
317
|
+
*
|
|
318
|
+
* Turning it on therefore keys on {@link import('#src/config.js').isNegotiatingRung} and NOT on
|
|
319
|
+
* whether a negotiation block exists. The two are independent by construction: a cleared transcript
|
|
320
|
+
* (§5.3) produces a round-1 *context* that is still a round of a negotiation.
|
|
321
|
+
*
|
|
322
|
+
* It sits LAST in the system prompt, after {@link buildGrantedToolsGuidance}, because §5.2's list of
|
|
323
|
+
* things a rejection may name ends with *a granted built-in that does the job* — a clause that reads
|
|
324
|
+
* as an instruction only once that list is already on the page.
|
|
325
|
+
*
|
|
326
|
+
* The two named anti-patterns are stated as failures rather than merely left out: a bare *"Rejected.
|
|
327
|
+
* This is destructive."* leaves the agent nothing to act on, and a rejection that restates the danger
|
|
328
|
+
* and asks the agent to explain itself invites another justification rather than a better command.
|
|
329
|
+
*/
|
|
330
|
+
export declare const RATER_NEGOTIABLE_REJECTION_GUIDANCE: string;
|
|
269
331
|
/**
|
|
270
332
|
* EXT-58 (spec §4.4) — the granted-alternative section of the rating prompt, built from the
|
|
271
333
|
* already-granted built-in tools of the current rung.
|
|
@@ -304,9 +366,31 @@ export declare function buildGrantedToolsGuidance(grantedTools: readonly Granted
|
|
|
304
366
|
* the other three is what makes it structurally impossible for a command to fall outside the four.
|
|
305
367
|
*
|
|
306
368
|
* There is no strictness parameter: §1 removed strictness levels along with severity thresholds,
|
|
307
|
-
* so
|
|
369
|
+
* so the rating criteria are the same at every rated rung. Two blocks are appended on top of them,
|
|
370
|
+
* and they key on **two independent things** ([[EXT-29]]):
|
|
371
|
+
*
|
|
372
|
+
* - `hasNegotiationContext` appends {@link RATER_NEGOTIATION_CONTEXT_GUIDANCE} — the rules for
|
|
373
|
+
* weighing a justification and a transcript, which have nothing to govern until one exists.
|
|
374
|
+
* - `negotiable` appends {@link RATER_NEGOTIABLE_REJECTION_GUIDANCE} — §5.2's rules for wording a
|
|
375
|
+
* rejection that the *agent* will read, which apply at every round of a negotiation including the
|
|
376
|
+
* first.
|
|
377
|
+
*
|
|
378
|
+
* **They are two parameters and not one because they are two questions.** Tying §5.2 to the context
|
|
379
|
+
* would silence it in exactly the two rounds §5.6 requires it in: round 1, and the round right after
|
|
380
|
+
* a §5.3 reset — both of which are round-1 *contexts* inside a live negotiation. Both blocks only
|
|
381
|
+
* ever APPEND, so a negotiated system prompt still has the plain one as its prefix.
|
|
382
|
+
*
|
|
383
|
+
* @param grantedTools §4.4's already-granted built-ins, or nothing.
|
|
384
|
+
* @param options `hasNegotiationContext` — whether this rating carries a §5.1 context; callers
|
|
385
|
+
* should not decide it for themselves, since {@link buildRaterPrompt} derives it from the one
|
|
386
|
+
* thing that decides it, namely whether {@link buildNegotiationContextBlock} produced a block.
|
|
387
|
+
* `negotiable` — whether a rejection will be handed back to the agent (§5.2), i.e. the rung
|
|
388
|
+
* negotiates ({@link import('#src/config.js').isNegotiatingRung}).
|
|
308
389
|
*/
|
|
309
|
-
export declare function buildRaterSystemPrompt(grantedTools?: readonly GrantedToolSummary[]
|
|
390
|
+
export declare function buildRaterSystemPrompt(grantedTools?: readonly GrantedToolSummary[], options?: {
|
|
391
|
+
hasNegotiationContext?: boolean;
|
|
392
|
+
negotiable?: boolean;
|
|
393
|
+
}): string;
|
|
310
394
|
/**
|
|
311
395
|
* Detect whether the command invokes an interpreter on a script target AND passes an
|
|
312
396
|
* `$ALL_CAPS` shell-variable expansion in its arguments — openclaw's "script preflight". Such a
|
|
@@ -323,13 +407,174 @@ export declare function hasScriptEnvLeakRisk(normalizedCommand: string): boolean
|
|
|
323
407
|
* dir prefix is folded.
|
|
324
408
|
*/
|
|
325
409
|
export declare function foldHomePath(command: string, home: string | undefined): string;
|
|
410
|
+
/**
|
|
411
|
+
* [[EXT-29]] (spec §5.1) — one COMPLETED round of a negotiation, as the next round's rater sees it.
|
|
412
|
+
*
|
|
413
|
+
* It carries exactly what §5.1's third bullet admits and nothing else: the command the agent
|
|
414
|
+
* proposed, the justification it attached (when it attached one), and **the rater's own outcome and
|
|
415
|
+
* explanation** for that round — *"the rater reasons from its earlier positions rather than
|
|
416
|
+
* re-deriving them."*
|
|
417
|
+
*
|
|
418
|
+
* **The rater's half is two flat fields rather than an embedded {@link ShellSafetyVerdict}**, and
|
|
419
|
+
* that is deliberate. A verdict also carries §4.4's `suggestedTool`, which is advice about the
|
|
420
|
+
* rating that produced it and not part of the history §5.1 admits; a renderer handed a three-field
|
|
421
|
+
* object that renders two of them is a silent drop waiting to be read as a bug. Flat fields mean the
|
|
422
|
+
* builder renders everything it is given, and the type states the admitted set by construction.
|
|
423
|
+
*
|
|
424
|
+
* `command` is the RAW command as the agent proposed it. The builder normalizes and home-folds it
|
|
425
|
+
* with the same functions the live command goes through, so the transcript shows a past round in the
|
|
426
|
+
* form that round was actually rated in, then renders it on one line — see
|
|
427
|
+
* {@link buildNegotiationContextBlock}.
|
|
428
|
+
*/
|
|
429
|
+
export interface RaterNegotiationRound {
|
|
430
|
+
/**
|
|
431
|
+
* The command the agent proposed in that round, RAW. The builder normalizes, home-folds and
|
|
432
|
+
* one-lines it; a caller that pre-processes it is doing the work twice and differently.
|
|
433
|
+
*/
|
|
434
|
+
command: string;
|
|
435
|
+
/** The justification the agent attached to it, if any. Omitted when it argued nothing. */
|
|
436
|
+
justification?: string;
|
|
437
|
+
/** The rater's own outcome for that round. */
|
|
438
|
+
outcome: RaterOutcome;
|
|
439
|
+
/** The rater's own one-sentence explanation for that round. */
|
|
440
|
+
reason: string;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* [[EXT-29]] (spec §5.1) — the additional context a rating from **round 2 onward** carries. Absent,
|
|
444
|
+
* or present but carrying nothing, means a round-1 rating: the command alone, byte-for-byte the
|
|
445
|
+
* prompt this module built before the negotiation existed.
|
|
446
|
+
*
|
|
447
|
+
* "Carrying nothing" is defined so a caller never has to choose a spelling: `undefined`, `{}`, an
|
|
448
|
+
* empty or whitespace-only `justification`, and empty arrays are all the same round-1 context. §5.3
|
|
449
|
+
* clears the transcript with the counter, so the state the runner holds after a reset is exactly
|
|
450
|
+
* this, whichever way it spells it.
|
|
451
|
+
*
|
|
452
|
+
* Nothing here is bounded by the caller: the last-5 rule and the 1000-character truncation are
|
|
453
|
+
* applied by {@link buildNegotiationContextBlock}, so a caller that hands over an entire
|
|
454
|
+
* conversation still cannot put a pasted log into the rater's context.
|
|
455
|
+
*/
|
|
456
|
+
export interface RaterNegotiationContext {
|
|
457
|
+
/** The main model's justification for **this** command. Untrusted; fenced like the command. */
|
|
458
|
+
justification?: string;
|
|
459
|
+
/**
|
|
460
|
+
* The conversation's user messages, oldest first. Only the last {@link NEGOTIATION_MAX_USER_MESSAGES}
|
|
461
|
+
* are admitted and each is truncated — hand over as many as are to hand.
|
|
462
|
+
*/
|
|
463
|
+
userMessages?: readonly string[];
|
|
464
|
+
/** The negotiation so far, oldest round first. */
|
|
465
|
+
priorRounds?: readonly RaterNegotiationRound[];
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Neutralise any sequence that would CLOSE the fence `tag`, so untrusted text cannot escape it.
|
|
469
|
+
*
|
|
470
|
+
* A fenced block is only a boundary if the fenced text cannot write the boundary itself. Untrusted
|
|
471
|
+
* content containing its own closing tag ends the fence early and everything after it reads as our
|
|
472
|
+
* own prose — and inside `<negotiation_so_far>` that is not merely confusing but *persuasive*: the
|
|
473
|
+
* block quotes the rater's own previous positions back to it, and {@link RATER_NEGOTIATION_GUIDANCE}
|
|
474
|
+
* tells it to reason from those positions. A forged prior `safe` therefore argues for approval in
|
|
475
|
+
* the rater's own voice.
|
|
476
|
+
*
|
|
477
|
+
* The replacement carries no angle brackets at all (a marker that spelled the tag out would be the
|
|
478
|
+
* very sequence being removed) and says what happened, because a rater that can see text was
|
|
479
|
+
* tampered with has been told something useful about the command it is rating.
|
|
480
|
+
*
|
|
481
|
+
* **Matching is deliberately loose, and the looseness is the mechanism.** The reader is a language
|
|
482
|
+
* model, not a parser, so a matcher that is stricter than the reader is not a filter — it is a list
|
|
483
|
+
* of spellings the attacker gets to choose from. Four kinds of slack are closed: case, the
|
|
484
|
+
* whitespace an XML parser would ignore (`</ justification >`), the compatibility glyphs NFKC folds
|
|
485
|
+
* (a fullwidth solidus is a solidus to a reader), and any invisible spliced into the tag
|
|
486
|
+
* ({@link INVISIBLE_FORMAT_CHARS}).
|
|
487
|
+
*
|
|
488
|
+
* **That is four kinds of slack, not all of them**, and the difference is worth keeping in view: the
|
|
489
|
+
* invisibles are covered by an enumeration of Unicode properties, so this is as tolerant as those
|
|
490
|
+
* properties are and no more. {@link INVISIBLE_FORMAT_CHARS} states that residual; a character
|
|
491
|
+
* measured to render as blank and walk through belongs in that class, not in a second matcher here.
|
|
492
|
+
*
|
|
493
|
+
* Self-reconstruction is impossible by construction: the replacement contains no angle bracket and
|
|
494
|
+
* no slash, so no arrangement of neutralised text can rebuild a closing tag.
|
|
495
|
+
*
|
|
496
|
+
* Parameterised by tag because it guards every fence in the rating prompt — the three §5.1 ones and
|
|
497
|
+
* `<command_to_evaluate>` — rather than each growing a mechanism that escapes differently. Exported
|
|
498
|
+
* so a test can drive the matcher directly.
|
|
499
|
+
*
|
|
500
|
+
* **THE RESIDUAL IS WIDER THAN A HOMOGLYPH LIST — measured, and do not size it from this comment.**
|
|
501
|
+
* This matcher catches the tag spelled essentially exactly. Four classes walk through, and the first
|
|
502
|
+
* needs no Unicode at all:
|
|
503
|
+
*
|
|
504
|
+
* 1. **Pure ASCII near-misses** — `</tag foo>` (a trailing attribute), `<//tag>`, `</tag/>`, and a
|
|
505
|
+
* plain space inside the name. Note the asymmetry that makes the last one easy to miss: a
|
|
506
|
+
* ZERO-WIDTH space between two letters of the name IS neutralised by the strip below, while an
|
|
507
|
+
* ordinary space in the identical position is not.
|
|
508
|
+
* 2. **Solidus homoglyphs** — NFKC folds the fullwidth solidus (U+FF0F) but not the fraction slash
|
|
509
|
+
* (U+2044), the division slash (U+2215) or the big solidus (U+29F8).
|
|
510
|
+
* 3. **Bracket homoglyphs** — U+2039, U+27E8, U+3008, U+2329, U+276C.
|
|
511
|
+
* 4. **Tag-name homoglyphs** — Cyrillic and Greek lookalikes inside the tag name.
|
|
512
|
+
*
|
|
513
|
+
* **The ASCII class is the serious one.** The reader this function defends against is a language
|
|
514
|
+
* model, and `</tag foo>` reads as a closing tag to a model more readily than any homoglyph does —
|
|
515
|
+
* it is what a closing tag with an attribute looks like in the XML-shaped prompt it sits in.
|
|
516
|
+
*
|
|
517
|
+
* **Nothing mechanical is fooled by any of it:** none produces the literal `</tag>`, so no boundary
|
|
518
|
+
* count is wrong and no test that counts fences can see it. That is precisely why it is written here.
|
|
519
|
+
*
|
|
520
|
+
* **Left open for SCOPE, not because it is mild** — the matcher guards all four fences, so widening
|
|
521
|
+
* it lands on every one at once. That decision, and the shape it should take (a tolerant matcher
|
|
522
|
+
* reaches class 1; a confusable skeleton answers 2-4), is [[EXT-111]].
|
|
523
|
+
*/
|
|
524
|
+
export declare function neutralizeClosingTag(text: string, tag: string): string;
|
|
525
|
+
/**
|
|
526
|
+
* [[EXT-29]] (spec §5.1) — the negotiation context block appended to the rater's USER message, or
|
|
527
|
+
* `null` when there is no negotiation (a round-1 rating).
|
|
528
|
+
*
|
|
529
|
+
* **Everything here is attacker-influenceable and every part of it is fenced**, exactly as
|
|
530
|
+
* `<command_to_evaluate>` is: the justification is written by a main model that reads untrusted
|
|
531
|
+
* input, the user messages arrive through a channel an attacker can reach, and the prior rounds
|
|
532
|
+
* carry both. The system prompt's {@link RATER_NEGOTIATION_GUIDANCE} declares these tags untrusted
|
|
533
|
+
* and is emitted with this block for that reason. Only the rater's own past outcomes and our own
|
|
534
|
+
* headings are ours, and they are the block's structure rather than its contents.
|
|
535
|
+
*
|
|
536
|
+
* **The fences here are enforced, not merely drawn.** Two things could otherwise write the block's
|
|
537
|
+
* own structure from inside it, and both are neutralised at the point of rendering: a closing tag
|
|
538
|
+
* ({@link neutralizeClosingTag}) and a newline in any one-line slot ({@link oneLine}). The amplifier
|
|
539
|
+
* that makes this worth more than tidiness is what the block IS — it quotes the rater's previous
|
|
540
|
+
* outcomes back to it under guidance telling it to reason from them, so a forged prior `safe` argues
|
|
541
|
+
* for approval in the rater's own voice.
|
|
542
|
+
*
|
|
543
|
+
* **The order inside the block narrows outward from the command being rated**: the justification is
|
|
544
|
+
* about THIS command, the transcript is the exchange that produced it, and the user messages are the
|
|
545
|
+
* mandate around the whole thing. It also keeps the agent's argument for the pending command out of
|
|
546
|
+
* the final position, which is the one a model weighs hardest.
|
|
547
|
+
*
|
|
548
|
+
* **Prior commands are normalized here**, by the same function the live command goes through, so a
|
|
549
|
+
* past round appears in the form it was actually rated in. **Home-folding applies to every value the
|
|
550
|
+
* block renders** — the justification and the user messages as much as the commands — because
|
|
551
|
+
* {@link foldHomePath} exists to keep the identifying form out of the prompt, and a prose field is
|
|
552
|
+
* where an absolute home path is most likely to appear, not least.
|
|
553
|
+
*
|
|
554
|
+
* Bounds are applied here rather than trusted from the caller: at most
|
|
555
|
+
* {@link NEGOTIATION_MAX_USER_MESSAGES} messages (the LAST that many), each truncated to
|
|
556
|
+
* {@link NEGOTIATION_USER_MESSAGE_MAX_CHARS}. Blank entries are dropped before the last-5 window is
|
|
557
|
+
* taken, so a run of empty messages cannot spend the budget that carries the mandate — and "blank"
|
|
558
|
+
* counts the characters {@link INVISIBLE_FORMAT_CHARS} names as nothing ({@link isBlank}), so a
|
|
559
|
+
* value carrying only those cannot render a block that a plain rating would not have.
|
|
560
|
+
*
|
|
561
|
+
* @param negotiation The §5.1 context, or nothing.
|
|
562
|
+
* @param home The home directory to fold — the caller's own `home`, so the block folds exactly as
|
|
563
|
+
* the live command does.
|
|
564
|
+
* @returns The block, or `null` when nothing would be rendered. `null` is the single signal that
|
|
565
|
+
* this is a round-1 rating: {@link buildRaterPrompt} uses it for both halves of the prompt, so the
|
|
566
|
+
* guidance and the context can never appear without each other.
|
|
567
|
+
*/
|
|
568
|
+
export declare function buildNegotiationContextBlock(negotiation: RaterNegotiationContext | undefined, home?: string): string | null;
|
|
326
569
|
/**
|
|
327
570
|
* Build the messages for the rater call: the system prompt ({@link buildRaterSystemPrompt}) plus a
|
|
328
571
|
* human message that embeds the NORMALIZED command inside an XML `<command_to_evaluate>` tag and
|
|
329
572
|
* (optionally) notes what a deterministic preflight already found — the script-env-leak flag,
|
|
330
573
|
* (§4.6) a host literal in a fetch position, and ([[EXT-81]]) the shape our own parser could not
|
|
331
574
|
* resolve. The command text is only ever DATA in the tag — the builder never executes or
|
|
332
|
-
* interpolates it as instructions, and the notes are our own trusted text beside it.
|
|
575
|
+
* interpolates it as instructions, and the notes are our own trusted text beside it. That
|
|
576
|
+
* separation is ENFORCED rather than merely drawn: the command cannot close its own fence
|
|
577
|
+
* ({@link neutralizeClosingTag}), so no part of it can render where our notes render.
|
|
333
578
|
*
|
|
334
579
|
* The four preflight notes are worded differently on purpose, and the differences are the design:
|
|
335
580
|
*
|
|
@@ -351,7 +596,15 @@ export declare function foldHomePath(command: string, home: string | undefined):
|
|
|
351
596
|
*
|
|
352
597
|
* **Order matters here and is the order of a reader's attention**: the two hazard notes come first
|
|
353
598
|
* because each names something positively established, then the parser note, then its open-world
|
|
354
|
-
* elaboration — general shape of what could not be resolved, then the specific flow inside it.
|
|
599
|
+
* elaboration — general shape of what could not be resolved, then the specific flow inside it. Then,
|
|
600
|
+
* last, [[EXT-29]]'s negotiation context ({@link buildNegotiationContextBlock}) — the notes describe
|
|
601
|
+
* THIS command, the negotiation is the history around it.
|
|
602
|
+
*
|
|
603
|
+
* **Round 1 is the prompt this function built before the negotiation existed, character for
|
|
604
|
+
* character.** No negotiation, or one carrying nothing, adds no block and no guidance; a negotiated
|
|
605
|
+
* round only ever APPENDS to both halves. §5.3 clears the transcript with the counter, so the rating
|
|
606
|
+
* after a reset is a round-1 rating by construction rather than by the caller remembering to make
|
|
607
|
+
* one.
|
|
355
608
|
*
|
|
356
609
|
* §4.3 defines the rated unit tool-generally (tool name + JSON arguments); `run_shell_command` is
|
|
357
610
|
* the case whose argument is a command string, and it alone is additionally normalized and
|
|
@@ -365,6 +618,27 @@ export declare function foldHomePath(command: string, home: string | undefined):
|
|
|
365
618
|
export declare function buildRaterPrompt(command: string, options?: {
|
|
366
619
|
home?: string;
|
|
367
620
|
grantedTools?: readonly GrantedToolSummary[];
|
|
621
|
+
/**
|
|
622
|
+
* [[EXT-29]] (§5.1) — the negotiation so far, from round 2 onward. Absent or empty builds
|
|
623
|
+
* exactly the round-1 prompt this function built before the negotiation existed, character for
|
|
624
|
+
* character, which is what makes §5.6's *"a cleared transcript means a round-1 context"* a
|
|
625
|
+
* property of this function rather than a discipline the caller has to keep.
|
|
626
|
+
*/
|
|
627
|
+
negotiation?: RaterNegotiationContext;
|
|
628
|
+
/**
|
|
629
|
+
* [[EXT-29]] (§5.2) — whether a rejection will be handed back to the AGENT rather than to a
|
|
630
|
+
* person, i.e. the rung negotiates ({@link import('#src/config.js').isNegotiatingRung}).
|
|
631
|
+
*
|
|
632
|
+
* **Independent of `negotiation` on purpose.** §5.1 decides what the rating may SEE; this
|
|
633
|
+
* decides how a rejection must be WRITTEN, and the two diverge in exactly the round §5.6 cares
|
|
634
|
+
* most about — round 1 of a negotiation, and the round right after a §5.3 reset, where the
|
|
635
|
+
* context is empty and the rejection is still addressed to the agent.
|
|
636
|
+
*
|
|
637
|
+
* It changes the SYSTEM prompt only. The user message is a function of the command and the
|
|
638
|
+
* context alone, so a negotiation's round 1 has a byte-identical user prompt to an
|
|
639
|
+
* `assisted` rating of the same command.
|
|
640
|
+
*/
|
|
641
|
+
negotiable?: boolean;
|
|
368
642
|
}): {
|
|
369
643
|
system: string;
|
|
370
644
|
user: string;
|
|
@@ -392,6 +666,38 @@ export declare function rateShellCommand(command: string, config: GthConfig, opt
|
|
|
392
666
|
* prompt is exactly as before and no suggestion is ever produced.
|
|
393
667
|
*/
|
|
394
668
|
grantedTools?: readonly GrantedToolSummary[];
|
|
669
|
+
/**
|
|
670
|
+
* [[EXT-29]] (§5.1) — the negotiation so far, for a rating from round 2 onward. Passed straight
|
|
671
|
+
* to {@link buildRaterPrompt}; absent or empty, this call is a round-1 rating and the prompt,
|
|
672
|
+
* the verdict and every decision made from it are exactly what they were before.
|
|
673
|
+
*/
|
|
674
|
+
negotiation?: RaterNegotiationContext;
|
|
675
|
+
/**
|
|
676
|
+
* [[EXT-29]] (§5.2) — whether a rejection is addressed to the agent (the rung negotiates).
|
|
677
|
+
* Passed straight to {@link buildRaterPrompt}; see the option there for why it is independent
|
|
678
|
+
* of `negotiation`.
|
|
679
|
+
*/
|
|
680
|
+
negotiable?: boolean;
|
|
681
|
+
/**
|
|
682
|
+
* [[TUI-C27]] — the sink for the diagnostic record of THIS call, handed over **at the send
|
|
683
|
+
* site**, carrying the prompt strings that are about to be sent.
|
|
684
|
+
*
|
|
685
|
+
* It is called once, BEFORE the model is invoked, with a record this function then fills in as
|
|
686
|
+
* the answer arrives. Two properties follow, and both are the point:
|
|
687
|
+
*
|
|
688
|
+
* - **The prompt is captured, never re-rendered.** The caller receives the very strings passed
|
|
689
|
+
* to `invoke`, so nothing downstream can rebuild a prompt that disagrees with the one that
|
|
690
|
+
* was actually sent — the one thing a diagnostic archive must never do.
|
|
691
|
+
* - **A rater that never answers still leaves a record of what it was asked.** The record is in
|
|
692
|
+
* the caller's hands before the call is made, so a hang, a timeout or a throw cannot take the
|
|
693
|
+
* question with it.
|
|
694
|
+
*
|
|
695
|
+
* Omitted (the eval target, and every caller that wants no diagnostics) costs nothing: no
|
|
696
|
+
* record is built.
|
|
697
|
+
*/
|
|
698
|
+
onCapture?: (capture: RaterCallCapture) => void;
|
|
699
|
+
/** [[TUI-C27]] — `approvals.rater`, recorded on the capture so a dump names WHO rated. */
|
|
700
|
+
raterProfile?: string;
|
|
395
701
|
}): Promise<ShellSafetyVerdict>;
|
|
396
702
|
/**
|
|
397
703
|
* The action the approvals gate resolves to for a single gated call, BEFORE the human prompt.
|
|
@@ -403,6 +709,16 @@ export declare function rateShellCommand(command: string, config: GthConfig, opt
|
|
|
403
709
|
* - `halt` — **end the agent loop** (§4.2). Reserved for `attack`. It is not a rejection the
|
|
404
710
|
* model can respond to and offers it no moves; no rung except `bypass` can turn it into
|
|
405
711
|
* anything else.
|
|
712
|
+
* - `reject` — [[EXT-29]] (§5): hand the rater's explanation back to the **agent** as the refused
|
|
713
|
+
* call's tool result (§7), opening a round of the negotiation. Returned for `destructive` at
|
|
714
|
+
* `auto` and nowhere else.
|
|
715
|
+
*
|
|
716
|
+
* **`reject` says the outcome is negotiable, NOT that the negotiation may continue.** This mapping
|
|
717
|
+
* is keyed on the rung and knows nothing about how many rounds have been spent; §5.3's consecutive
|
|
718
|
+
* cap and the reachability bound live with the state they count, in the runner, which turns a
|
|
719
|
+
* `reject` into an escalation once either is spent. Putting the counters in here would make a pure
|
|
720
|
+
* rung-keyed table depend on session history, and would give the eval target
|
|
721
|
+
* (`@gaunt-sloth/batch`'s `raterTarget`) an action that varies with something it does not model.
|
|
406
722
|
*
|
|
407
723
|
* **[[EXT-81]] retired the fourth arm, `abstain`.** A command whose target the gate could not
|
|
408
724
|
* statically resolve used to skip the rating call entirely and return that action instead. Under
|
|
@@ -422,7 +738,7 @@ export declare function rateShellCommand(command: string, config: GthConfig, opt
|
|
|
422
738
|
* unmeasured classifier belongs behind a human who can correct it; a refusal has no correction
|
|
423
739
|
* path.
|
|
424
740
|
*/
|
|
425
|
-
export type RaterAction = 'approve' | 'escalate' | 'halt';
|
|
741
|
+
export type RaterAction = 'approve' | 'escalate' | 'halt' | 'reject';
|
|
426
742
|
/** Inputs to the decision mapping: just the rung. Each rung fully determines behaviour (§1). */
|
|
427
743
|
export interface RaterDecisionOptions {
|
|
428
744
|
/** The rung in force for this session. */
|
|
@@ -506,15 +822,33 @@ export declare function applyDestructiveFloor(verdict: ShellSafetyVerdict | unde
|
|
|
506
822
|
* the same direction as the fail-closed default it would otherwise have returned.
|
|
507
823
|
*/
|
|
508
824
|
export declare function openWorldToolFloorReason(annotations: EffectiveToolAnnotations | undefined): string | null;
|
|
825
|
+
/** Which of the two deterministic preflights fired. See {@link preflightFloorFinding}. */
|
|
826
|
+
export type PreflightFloorKind = 'script-env-leak' | 'open-world';
|
|
827
|
+
/** A preflight finding: which arm fired, and the reason it floors the command with. */
|
|
828
|
+
export interface PreflightFloorFinding {
|
|
829
|
+
kind: PreflightFloorKind;
|
|
830
|
+
reason: string;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* [[TUI-C27]] — the same finding {@link preflightFloorReason} returns, with the ARM NAMED.
|
|
834
|
+
*
|
|
835
|
+
* The reason alone is what the decision needs; a diagnostic archive needs to say *which* stage
|
|
836
|
+
* decided, and "an environment variable was expanded into a script" and "a host literal sat in a
|
|
837
|
+
* fetch position" are two different findings a reader must be able to tell apart without matching
|
|
838
|
+
* prose prefixes. `preflightFloorReason` delegates here rather than the two existing side by side:
|
|
839
|
+
* a second copy of this ordering is how a gate and a dump come to disagree about what floored a
|
|
840
|
+
* command.
|
|
841
|
+
*/
|
|
842
|
+
export declare function preflightFloorFinding(command: string): PreflightFloorFinding | null;
|
|
509
843
|
/**
|
|
510
844
|
* CFG-27 — pure, testable mapping from a {@link ShellSafetyVerdict} + the raw command to a
|
|
511
845
|
* {@link RaterAction}, keyed on the **rung** (spec §4.2, §8):
|
|
512
846
|
*
|
|
513
|
-
* | Outcome | `
|
|
847
|
+
* | Outcome | `manual`/`write` | `assisted` | `auto` | `bypass` |
|
|
514
848
|
* |---|---|---|---|---|
|
|
515
849
|
* | — (no rating) | escalate | | | approve |
|
|
516
850
|
* | `safe` | — | approve | approve | — |
|
|
517
|
-
* | `destructive` | — | escalate |
|
|
851
|
+
* | `destructive` | — | escalate | **reject** — §5's negotiation ([[EXT-29]]) | — |
|
|
518
852
|
* | `catastrophic` | — | escalate | escalate — **never negotiate** | — |
|
|
519
853
|
* | `attack` | — | **halt** | **halt** | — |
|
|
520
854
|
*
|
|
@@ -529,11 +863,12 @@ export declare function openWorldToolFloorReason(annotations: EffectiveToolAnnot
|
|
|
529
863
|
*
|
|
530
864
|
* 1. `bypass` → `approve`. The gate is off. The declared deny list and the exec-time hardline
|
|
531
865
|
* floor still apply, but neither is decided here.
|
|
532
|
-
* 2. Unrated rungs (`
|
|
866
|
+
* 2. Unrated rungs (`manual`, `write`) → `escalate`. No model is consulted at all; the
|
|
533
867
|
* allow-list is checked by the caller BEFORE this function, so reaching here means the human
|
|
534
|
-
* decides. (
|
|
535
|
-
*
|
|
536
|
-
*
|
|
868
|
+
* decides. (The two rungs behave identically here, and that is not a missing branch: what
|
|
869
|
+
* separates them is which tools they auto-grant, decided once in `resolveGatedToolNames` when the
|
|
870
|
+
* gated set is built. A call that reaches this function is already one the rung did not grant,
|
|
871
|
+
* and the shell — this function's only subject — is granted by neither.)
|
|
537
872
|
* 3. **The deterministic preflight FINDINGS, which FLOOR the outcome at `destructive` and never
|
|
538
873
|
* lower one** ({@link preflightFloorReason}): the script-env-leak preflight
|
|
539
874
|
* ({@link hasScriptEnvLeakRisk}) and EXT-61's open-world preflight
|
|
@@ -546,11 +881,11 @@ export declare function openWorldToolFloorReason(annotations: EffectiveToolAnnot
|
|
|
546
881
|
* `catastrophic` and `attack` all pass through UNCHANGED. (Before the rescale this branch
|
|
547
882
|
* excluded the single halting outcome BY NAME. Renamed in place it would have let a preflight hit
|
|
548
883
|
* *downgrade* a `catastrophic` verdict to `destructive` — the exact inverse of the invariant
|
|
549
|
-
* above, silently trading an unnegotiable escalation for a negotiable one at `
|
|
884
|
+
* above, silently trading an unnegotiable escalation for a negotiable one at `auto`.)
|
|
550
885
|
* 4. `attack` → `halt`, at both rated rungs, never negotiable.
|
|
551
886
|
* 5. `catastrophic` → `escalate`, and MUST NOT enter §5's negotiation.
|
|
552
|
-
* 6. `safe` → `approve`; `destructive` → `escalate`
|
|
553
|
-
*
|
|
887
|
+
* 6. `safe` → `approve`; `destructive` → `escalate` at `assisted`, `reject` at `auto` (§5's
|
|
888
|
+
* negotiation, [[EXT-29]]).
|
|
554
889
|
*
|
|
555
890
|
* **EXT-58 (§4.4): the verdict's `suggestedTool` is not read here, and that is deliberate.** A
|
|
556
891
|
* suggestion is never an approval — it must not change the action, must not approve the original
|