@gaunt-sloth/core 2.0.0-alpha.23 → 2.0.0-alpha.25

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 (154) hide show
  1. package/README.md +43 -18
  2. package/dist/config/colour.d.ts +38 -0
  3. package/dist/config/colour.js +36 -0
  4. package/dist/config/colour.js.map +1 -0
  5. package/dist/config/defaults.d.ts +6 -7
  6. package/dist/config/defaults.js +4 -8
  7. package/dist/config/defaults.js.map +1 -1
  8. package/dist/config/loader.d.ts +50 -3
  9. package/dist/config/loader.js +345 -29
  10. package/dist/config/loader.js.map +1 -1
  11. package/dist/config/mouse.d.ts +50 -0
  12. package/dist/config/mouse.js +44 -0
  13. package/dist/config/mouse.js.map +1 -0
  14. package/dist/config/profiles.d.ts +68 -0
  15. package/dist/config/profiles.js +93 -0
  16. package/dist/config/profiles.js.map +1 -0
  17. package/dist/config/schema.d.ts +2441 -116
  18. package/dist/config/schema.js +930 -19
  19. package/dist/config/schema.js.map +1 -1
  20. package/dist/config/shell-policy.d.ts +466 -116
  21. package/dist/config/shell-policy.js +273 -62
  22. package/dist/config/shell-policy.js.map +1 -1
  23. package/dist/config/tool-descriptions.d.ts +165 -0
  24. package/dist/config/tool-descriptions.js +229 -0
  25. package/dist/config/tool-descriptions.js.map +1 -0
  26. package/dist/config/types.d.ts +139 -6
  27. package/dist/config/types.js.map +1 -1
  28. package/dist/config.d.ts +4 -0
  29. package/dist/config.js +4 -0
  30. package/dist/config.js.map +1 -1
  31. package/dist/core/GthAbstractAgent.d.ts +85 -2
  32. package/dist/core/GthAbstractAgent.js +194 -1
  33. package/dist/core/GthAbstractAgent.js.map +1 -1
  34. package/dist/core/GthAgentRunner.d.ts +341 -58
  35. package/dist/core/GthAgentRunner.js +857 -149
  36. package/dist/core/GthAgentRunner.js.map +1 -1
  37. package/dist/core/GthLangChainAgent.d.ts +99 -0
  38. package/dist/core/GthLangChainAgent.js +388 -3
  39. package/dist/core/GthLangChainAgent.js.map +1 -1
  40. package/dist/core/approvals/annotations.d.ts +122 -0
  41. package/dist/core/approvals/annotations.js +137 -0
  42. package/dist/core/approvals/annotations.js.map +1 -0
  43. package/dist/core/approvals/grants.d.ts +216 -0
  44. package/dist/core/approvals/grants.js +469 -0
  45. package/dist/core/approvals/grants.js.map +1 -0
  46. package/dist/core/approvals/matcher.d.ts +202 -0
  47. package/dist/core/approvals/matcher.js +267 -0
  48. package/dist/core/approvals/matcher.js.map +1 -0
  49. package/dist/core/approvals/mcpSubjects.d.ts +40 -0
  50. package/dist/core/approvals/mcpSubjects.js +99 -0
  51. package/dist/core/approvals/mcpSubjects.js.map +1 -0
  52. package/dist/core/approvals/toolAnnotationSources.d.ts +105 -0
  53. package/dist/core/approvals/toolAnnotationSources.js +277 -0
  54. package/dist/core/approvals/toolAnnotationSources.js.map +1 -0
  55. package/dist/core/approvals/toolHost.d.ts +46 -0
  56. package/dist/core/approvals/toolHost.js +108 -0
  57. package/dist/core/approvals/toolHost.js.map +1 -0
  58. package/dist/core/debugCapture.d.ts +16 -0
  59. package/dist/core/debugCapture.js.map +1 -1
  60. package/dist/core/launchBanner.d.ts +120 -0
  61. package/dist/core/launchBanner.js +399 -0
  62. package/dist/core/launchBanner.js.map +1 -0
  63. package/dist/core/plainToolIndication.js +61 -35
  64. package/dist/core/plainToolIndication.js.map +1 -1
  65. package/dist/core/refusal.d.ts +53 -0
  66. package/dist/core/refusal.js +129 -0
  67. package/dist/core/refusal.js.map +1 -0
  68. package/dist/core/shell/approvalStop.d.ts +75 -0
  69. package/dist/core/shell/approvalStop.js +108 -0
  70. package/dist/core/shell/approvalStop.js.map +1 -0
  71. package/dist/core/shell/arity.d.ts +6 -0
  72. package/dist/core/shell/arity.js +20 -6
  73. package/dist/core/shell/arity.js.map +1 -1
  74. package/dist/core/shell/denylist.d.ts +11 -0
  75. package/dist/core/shell/denylist.js +37 -0
  76. package/dist/core/shell/denylist.js.map +1 -0
  77. package/dist/core/shell/normalize.d.ts +41 -1
  78. package/dist/core/shell/normalize.js +58 -4
  79. package/dist/core/shell/normalize.js.map +1 -1
  80. package/dist/core/shell/openWorld.d.ts +138 -0
  81. package/dist/core/shell/openWorld.js +523 -0
  82. package/dist/core/shell/openWorld.js.map +1 -0
  83. package/dist/core/shell/rater.d.ts +560 -0
  84. package/dist/core/shell/rater.js +895 -0
  85. package/dist/core/shell/rater.js.map +1 -0
  86. package/dist/core/shell/raterModel.d.ts +41 -0
  87. package/dist/core/shell/raterModel.js +51 -0
  88. package/dist/core/shell/raterModel.js.map +1 -0
  89. package/dist/core/shell/rejection.d.ts +66 -0
  90. package/dist/core/shell/rejection.js +38 -0
  91. package/dist/core/shell/rejection.js.map +1 -0
  92. package/dist/core/toolCallRepair/grammar.d.ts +41 -0
  93. package/dist/core/toolCallRepair/grammar.js +116 -0
  94. package/dist/core/toolCallRepair/grammar.js.map +1 -0
  95. package/dist/core/toolCallRepair/index.d.ts +2 -0
  96. package/dist/core/toolCallRepair/index.js +7 -0
  97. package/dist/core/toolCallRepair/index.js.map +1 -0
  98. package/dist/core/toolCallRepair/payload.d.ts +36 -0
  99. package/dist/core/toolCallRepair/payload.js +341 -0
  100. package/dist/core/toolCallRepair/payload.js.map +1 -0
  101. package/dist/core/toolCallRepair/promote.d.ts +45 -0
  102. package/dist/core/toolCallRepair/promote.js +90 -0
  103. package/dist/core/toolCallRepair/promote.js.map +1 -0
  104. package/dist/core/toolDisplay.d.ts +14 -1
  105. package/dist/core/toolDisplay.js +69 -12
  106. package/dist/core/toolDisplay.js.map +1 -1
  107. package/dist/core/toolOutputChannel.d.ts +33 -6
  108. package/dist/core/toolOutputChannel.js +61 -8
  109. package/dist/core/toolOutputChannel.js.map +1 -1
  110. package/dist/core/types.d.ts +71 -9
  111. package/dist/core/types.js.map +1 -1
  112. package/dist/providers/geminiSchemaSanitizer.d.ts +6 -3
  113. package/dist/providers/geminiSchemaSanitizer.js +152 -6
  114. package/dist/providers/geminiSchemaSanitizer.js.map +1 -1
  115. package/dist/providers/modelDiscovery.d.ts +18 -4
  116. package/dist/providers/modelDiscovery.js +67 -17
  117. package/dist/providers/modelDiscovery.js.map +1 -1
  118. package/dist/providers/openai.js +34 -0
  119. package/dist/providers/openai.js.map +1 -1
  120. package/dist/runtime/askStructured.d.ts +8 -7
  121. package/dist/runtime/askStructured.js +8 -7
  122. package/dist/runtime/askStructured.js.map +1 -1
  123. package/dist/runtime/conversation.js +89 -76
  124. package/dist/runtime/conversation.js.map +1 -1
  125. package/dist/runtime/singleShot.js +71 -60
  126. package/dist/runtime/singleShot.js.map +1 -1
  127. package/dist/utils/ProgressIndicator.d.ts +21 -0
  128. package/dist/utils/ProgressIndicator.js +30 -3
  129. package/dist/utils/ProgressIndicator.js.map +1 -1
  130. package/dist/utils/consoleUtils.d.ts +30 -0
  131. package/dist/utils/consoleUtils.js +27 -2
  132. package/dist/utils/consoleUtils.js.map +1 -1
  133. package/dist/utils/crashHandler.d.ts +87 -0
  134. package/dist/utils/crashHandler.js +128 -0
  135. package/dist/utils/crashHandler.js.map +1 -0
  136. package/dist/utils/debugDump.d.ts +58 -0
  137. package/dist/utils/debugDump.js +149 -15
  138. package/dist/utils/debugDump.js.map +1 -1
  139. package/dist/utils/llmUtils.d.ts +22 -8
  140. package/dist/utils/llmUtils.js +48 -8
  141. package/dist/utils/llmUtils.js.map +1 -1
  142. package/dist/utils/redactSecrets.js +68 -20
  143. package/dist/utils/redactSecrets.js.map +1 -1
  144. package/dist/utils/systemUtils.d.ts +17 -0
  145. package/dist/utils/systemUtils.js +17 -0
  146. package/dist/utils/systemUtils.js.map +1 -1
  147. package/package.json +5 -2
  148. package/schema/gsloth-config.schema.json +1484 -466
  149. package/dist/core/shell/allowlist.d.ts +0 -75
  150. package/dist/core/shell/allowlist.js +0 -187
  151. package/dist/core/shell/allowlist.js.map +0 -1
  152. package/dist/core/shell/judge.d.ts +0 -161
  153. package/dist/core/shell/judge.js +0 -261
  154. package/dist/core/shell/judge.js.map +0 -1
@@ -0,0 +1,560 @@
1
+ /**
2
+ * @module core/shell/rater
3
+ *
4
+ * CFG-27 (CFG-26 rework) — the **auto-rater**: the LLM that rates a pending `run_shell_command`
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 — `auto-safe` and `full-auto` (see `APPROVAL_RUNGS`); `read-only`,
7
+ * `write` and `bypass` are fully deterministic and never pay for a model call.
8
+ *
9
+ * NOTE ON THE NAME: "judge" is reserved for the **eval grader** (`gth eval --judge <profile>`,
10
+ * `@gaunt-sloth/batch`) — a different concept. This module is the approvals rater.
11
+ *
12
+ * Validated prior art (both place the rater in front of the human prompt as an auto-approve
13
+ * fatigue-reducer): openclaw `exec-auto-reviewer.ts` and hermes-agent `approval.py` "smart" mode.
14
+ *
15
+ * Two hardening guarantees are baked in here:
16
+ *
17
+ * 1. **Prompt-injection defense.** The command is attacker-controlled text. It is normalized
18
+ * (reusing {@link normalizeCommand} + home-path folding) and embedded inside an XML
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}.
21
+ * 2. **Fail-closed on error.** If the LLM call throws, times out, or returns unparseable
22
+ * output, the verdict returned NEVER auto-approves — it is `destructive` with an honest
23
+ * "could not assess" reason. A rater failure can never silently green-light a command.
24
+ * See {@link FAIL_CLOSED_VERDICT}.
25
+ *
26
+ * Fail-closed-on-AMBIGUITY (when the command's target can't be statically resolved) lives in the
27
+ * decision mapping ({@link mapVerdictToAction}), not here, so it applies regardless of what the
28
+ * rater says.
29
+ *
30
+ * Mirrors the QA-3 rating substrate (`packages/review/src/middleware/reviewRateMiddleware.ts`):
31
+ * structured-output evaluation over `config.llm`, wrapped in try/catch.
32
+ */
33
+ import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
34
+ import * as z from 'zod';
35
+ import type { ApprovalRung, GrantedToolSummary, GthConfig } from '#src/config.js';
36
+ import type { EffectiveToolAnnotations } from '#src/core/approvals/matcher.js';
37
+ /**
38
+ * CFG-28 (spec §4.1) — the **four** outcomes the rater may return. The retired third outcome named
39
+ * the halt trigger by *mechanism* while positioning it as the top *severity*; the rescale (§11.1)
40
+ * splits that into two outcomes asking two different questions. There is no
41
+ * ordering knob and no threshold: each outcome's consequence is fixed by the rung
42
+ * ({@link mapVerdictToAction}).
43
+ *
44
+ * - `safe` — no harmful effect.
45
+ * - `destructive` — **the catch-all**: harmful, but recoverable from inside the session, and not
46
+ * an attack — **and anything the rater cannot assess**. The rating prompt defines it *by
47
+ * exclusion* ("not safe, not catastrophic and not an attack") precisely so no command can fall
48
+ * outside the four.
49
+ * - `catastrophic` — *can this be undone from inside the session?* Irreversible without something
50
+ * OUTSIDE the session: rescue media, a backup, a re-provision, a restore from a third party.
51
+ * Escalates at both rated rungs; never negotiable and never sticky (§4.2).
52
+ * - `attack` — *is something hostile acting here?* The command's own **structure** evidences
53
+ * compromise (§4.1.1: credential targeting, privilege escalation / permission weakening,
54
+ * persistence, deception, obfuscation). It is the only outcome that HALTS the run.
55
+ *
56
+ * **`catastrophic` and `attack` are not ranked against each other** — they ask different
57
+ * questions, and the spec says so explicitly. A command can be both; `attack` wins the
58
+ * *consequence* (a manipulated session cannot be trusted to continue) but MUST NOT swallow the
59
+ * finding — see the §6.1 clause in {@link buildRaterSystemPrompt}. Nothing here may be written as
60
+ * a severity comparison between the two.
61
+ */
62
+ export declare const RATER_OUTCOMES: readonly ['safe', 'destructive', 'catastrophic', 'attack'];
63
+ /** One outcome of {@link RATER_OUTCOMES}. */
64
+ export type RaterOutcome = (typeof RATER_OUTCOMES)[number];
65
+ /**
66
+ * Structured verdict the rater model must return: one outcome plus one short sentence. There is
67
+ * deliberately nothing else — no severity number, no booleans to recombine into a compound
68
+ * condition. The consequence is a property of the rung, not of a knob.
69
+ */
70
+ export declare const ShellSafetyVerdictSchema: z.ZodObject<{
71
+ outcome: z.ZodEnum<{
72
+ attack: "attack";
73
+ catastrophic: "catastrophic";
74
+ destructive: "destructive";
75
+ safe: "safe";
76
+ }>;
77
+ reason: z.ZodString;
78
+ suggestedTool: z.ZodOptional<z.ZodString>;
79
+ }, z.core.$strip>;
80
+ /**
81
+ * The rater's structured verdict on a single shell command.
82
+ */
83
+ export type ShellSafetyVerdict = z.infer<typeof ShellSafetyVerdictSchema>;
84
+ /**
85
+ * The honest reason text used whenever the outcome was NOT assessed by the rater — a rater failure
86
+ * ({@link FAIL_CLOSED_VERDICT}) or a command the gate itself cannot statically vet
87
+ * ({@link mapVerdictToAction}'s ambiguity / script-env-leak preflight). Spec rule (§4.1):
88
+ * *uncertainty is not an outcome*, so it maps to `destructive` while SAYING it could not be
89
+ * assessed rather than pretending the command was found harmful.
90
+ */
91
+ export declare const COULD_NOT_ASSESS_PREFIX = "Could not assess this command";
92
+ /**
93
+ * EXT-61 (§4.6) — the reason text prefix for the **open-world** preflight, and deliberately NOT
94
+ * {@link COULD_NOT_ASSESS_PREFIX}: this preflight *did* assess the command and found something
95
+ * specific. Saying "could not assess" here would be a lie, and the named host is the whole value of
96
+ * the escalation — "it downloads something, confirm" and "it fetches from registry.npmjs.ag" are
97
+ * different warnings, and only the second is worth reading.
98
+ */
99
+ export declare const NAMES_A_HOST_PREFIX = "This command names a host";
100
+ /**
101
+ * EXT-70 (§4.7.2, §4.7.3) — the reason text prefix for the **tool** arm of the open-world floor: a
102
+ * call whose EFFECTIVE `openWorldHint` is true. Like {@link NAMES_A_HOST_PREFIX} and unlike
103
+ * {@link COULD_NOT_ASSESS_PREFIX}, it states something the gate positively established.
104
+ *
105
+ * It names the hint rather than paraphrasing it, because the hint is also the thing the user can
106
+ * act on: trust it from that server (`approvals.mcp`), or declare the call in `approvals.allow`.
107
+ */
108
+ export declare const REACHES_OPEN_WORLD_PREFIX = "This tool reaches the open world";
109
+ /**
110
+ * The closing clause **shared by every open-world floor reason**, shell and tool alike.
111
+ *
112
+ * It is a constant rather than two copies of a sentence, and that is load-bearing rather than
113
+ * tidiness: the two arms are one rule (§4.6 for a shell fetch, §4.7.3 for the same fetch reached
114
+ * through a tool), so a reader who has seen one escalation reads the other as the same decision.
115
+ * It is also the one part of the floor a **second implementation** cannot reproduce by accident —
116
+ * an inline `{ outcome: 'destructive', reason: … }` written at some future call site would say
117
+ * something else, and the assertions that compare a floored reason against the exported reason
118
+ * builders are what turn that into a red test rather than a slow divergence.
119
+ */
120
+ export declare const NEVER_AUTO_APPROVED_CLAUSE = "so it is never auto-approved.";
121
+ /**
122
+ * The verdict returned whenever the rater cannot produce a trustworthy answer (LLM throws,
123
+ * times out, or returns unparseable output). Fail-closed: `destructive`, never auto-approved,
124
+ * and never `attack` or `catastrophic` either — a failure to assess must not manufacture a
125
+ * run-halting outcome any more than it may manufacture an approval.
126
+ */
127
+ export declare const FAIL_CLOSED_VERDICT: ShellSafetyVerdict;
128
+ /**
129
+ * EXT-66 — why the gate failed closed. **Every one of these is a fact about the GATE, not about the
130
+ * command**, which is the whole point of naming them: {@link FAIL_CLOSED_VERDICT} collapsed four
131
+ * different gate failures into one verdict that reads, downstream and in every eval report, exactly
132
+ * like a model that looked at the command and judged it `destructive`.
133
+ *
134
+ * That is not hypothetical. The EXT-62 anchoring sweep read its first `gemma4:12b` column as full
135
+ * coverage of the interpreter-wrapper misses; 3 of those escalations were the gate defaulting after
136
+ * 30 seconds, not the model judging, and only the reason string distinguished them.
137
+ *
138
+ * The outcome stays `destructive` for all four — failing closed is right and stays right, and a
139
+ * failure to assess must not manufacture `catastrophic`/`attack` any more than it may manufacture
140
+ * an approval. What changes is that the reason now says which failure happened. [[EXT-64]] routes
141
+ * the `timeout` and `no-model` causes onto its `abstain` ACTION by reading this, rather than
142
+ * re-deriving it; this module deliberately does not anticipate that vocabulary.
143
+ */
144
+ export type FailClosedCause = 'no-model' | 'timeout' | 'unparseable' | 'threw';
145
+ /**
146
+ * The fail-closed verdict for a specific {@link FailClosedCause}. Keeps
147
+ * {@link COULD_NOT_ASSESS_PREFIX} — the statement "this was not assessed" is still true and is what
148
+ * downstream keys on — and appends what actually went wrong.
149
+ *
150
+ * The timeout arm names the budget, because "the rater timed out" is not actionable and "the rater
151
+ * did not answer within 30000ms" points straight at `approvals.raterTimeoutMs`.
152
+ */
153
+ export declare function failClosedVerdict(cause: FailClosedCause, timeoutMs?: number): ShellSafetyVerdict;
154
+ /**
155
+ * Whether a verdict is one this gate produced because it could not obtain a rating, as opposed to
156
+ * one a rater actually returned. Keys on {@link COULD_NOT_ASSESS_PREFIX} — the same
157
+ * reason-prefix-as-identity idiom {@link NAMES_A_HOST_PREFIX} already uses — so it covers the
158
+ * legacy {@link FAIL_CLOSED_VERDICT} as well as every {@link failClosedVerdict} cause.
159
+ *
160
+ * Exported so a caller can tell "the gate defaulted" from "the model judged" without string
161
+ * matching at the call site, which is the distinction an eval column and a session summary both
162
+ * need and neither could previously make.
163
+ */
164
+ export declare function isFailClosed(verdict: ShellSafetyVerdict | undefined): boolean;
165
+ /** Whether a verdict is specifically the {@link FailClosedCause} `timeout` arm. */
166
+ export declare function isRaterTimeout(verdict: ShellSafetyVerdict | undefined): boolean;
167
+ /**
168
+ * Default wall-clock budget (ms) for the rater LLM call. Kept low so a slow/hung rater can't
169
+ * wedge the approval flow — on timeout we fail closed. Mirrors openclaw's low exec-reviewer
170
+ * timeout minimum.
171
+ *
172
+ * **EXT-66 — this is a HOSTED-model number, and it is now a default rather than the only value.**
173
+ * `claude-haiku-4-5` and `gemini-3.6-flash` answered a 23-case corpus well inside it, 0 fail-closed.
174
+ * `gemma4:12b` over a local GPU took 6.0s–114.7s on the same corpus, and the harder the command the
175
+ * longer it thought — so the fixed limit preferentially clipped exactly the commands that most
176
+ * needed rating (3 of 18 calls in one run, 9 of 17 in the next; all of them returned real verdicts
177
+ * at 120s, including a correct `catastrophic` returned 85 seconds after the gate had given up).
178
+ * Override with `approvals.raterTimeoutMs`.
179
+ */
180
+ export declare const RATER_DEFAULT_TIMEOUT_MS = 30000;
181
+ /**
182
+ * The invariant half of the rater's system prompt: the role and the untrusted-input contract (the
183
+ * tagged command is DATA, not instructions). Patterned after openclaw's
184
+ * `DEFAULT_EXEC_REVIEWER_SYSTEM_PROMPT` and hermes' untrusted-input framing. Exported so tests can
185
+ * assert the injection-defense preamble survives.
186
+ */
187
+ export declare const RATER_SYSTEM_PREAMBLE: string;
188
+ /**
189
+ * The `catastrophic` half of the rating prompt (§4.1). The whole outcome is one question — *can
190
+ * this be undone from inside the session?* — and the prompt asks it that way rather than offering
191
+ * a severity scale, because a rater given "how bad is it" instead of "is it recoverable" produces a
192
+ * ranking rather than the classification §4.2 acts on.
193
+ *
194
+ * The examples deliberately include the members the §8 deterministic floor **cannot** reach
195
+ * (`DROP DATABASE`, `terraform destroy -auto-approve`, `kubectl delete namespace production`):
196
+ * §11.1 records that only a model recognises that class, which is the reason the outcome exists.
197
+ * The counter-examples matter just as much — `git reset --hard` is reflog-recoverable and therefore
198
+ * `destructive`, and that is precisely what lets §5's negotiation open on it at all (§11.1a).
199
+ */
200
+ export declare const RATER_CATASTROPHIC_GUIDANCE: string;
201
+ /**
202
+ * The `attack` half of the rating prompt, kept as its own export because **§4.1.1 requires the
203
+ * prompt to carry the structural test in its own words, not merely the one-line table
204
+ * definition.**
205
+ *
206
+ * The reason is worth restating where the prompt author will read it: `attack` is the only outcome
207
+ * that halts the run, the halt is not negotiable, and from `auto-safe` — the default rung for
208
+ * everyone — a non-interactive run has no key at all and the only ways through are an allow-list
209
+ * entry or dropping to `bypass`, i.e. one step from the default gate to no gate. An outcome that
210
+ * expensive MUST NOT fire on ordinary work, so the test is **what the command's own structure
211
+ * shows, not who it talks to**.
212
+ *
213
+ * Two clauses here are measurements rather than opinions, and neither may be quietly relaxed:
214
+ *
215
+ * - **Origin trust is not the rater's job** (§4.1.1's measured note). A cheap model rated
216
+ * `curl https://pypi.org.packages-cdn.io/simple/ -o index.html` as `safe` while *naming the
217
+ * deception in its own reasoning*, and split identically-shaped commands at random. A property
218
+ * that holds only on the good model is not a property, and `auto-safe` is the default for people
219
+ * pointed at small local models. `curl … | <shell>` is therefore `destructive` whatever the host.
220
+ * - **Clause (1) is narrowed** (§11.1b, 2026-07-28). A secret handed to a tool that is doing some
221
+ * other job is not credential targeting; a secret exposed for its own sake is. Read literally,
222
+ * the un-narrowed clause halted the run on `python deploy.py --key $AWS_SECRET_ACCESS_KEY` — one
223
+ * of the most ordinary shapes in deployment work, from a section that opens by saying an outcome
224
+ * this expensive must not fire on ordinary work. **Nothing is given up**: the deterministic
225
+ * {@link hasScriptEnvLeakRisk} preflight fires on exactly that shape, annotates this prompt, and
226
+ * forces the fail-closed `destructive` path in {@link mapVerdictToAction} — so a secret in an
227
+ * argument cannot reach `safe` however the rater rates it. The change trades a halt for a prompt
228
+ * on commands that were never going to auto-approve.
229
+ */
230
+ export declare const RATER_ATTACK_GUIDANCE: string;
231
+ /**
232
+ * EXT-61 (spec §4.6.1) — **deception becomes a REPORT and an upgrade, never a gate.**
233
+ *
234
+ * This is the rater-side half of the open-world preflight, and it exists because the preflight
235
+ * changed what the rater's hostname judgement is *for*. §4.6 floors every command that names a host
236
+ * at `destructive` before the rater is called, so that judgement no longer stands between a
237
+ * typosquat and execution. Its job is now to *tell the user what it sees*.
238
+ *
239
+ * **The error costs invert, and that is the point.** A missed deception costs a warning rather than
240
+ * a bypass; a wrongly-suspected one costs a sentence rather than a halted run. That is the correct
241
+ * place to put a judgement neither models nor humans perform reliably — §4.1.1's measurement is a
242
+ * cheap model rating a lookalike host `safe` *while naming the deception in its own reasoning*, and
243
+ * a working developer who did not discriminate hostnames in either direction.
244
+ *
245
+ * Both halves are normative and neither may be softened into the other:
246
+ *
247
+ * - **(1) Always report it**, naming the mechanism, **even when the outcome stays `destructive`**.
248
+ * The "even when" is the whole clause; a rater that only names a typosquat when it is halting the
249
+ * run reports nothing on the commands this preflight was built for, since they all land on
250
+ * `destructive`.
251
+ * - **(2) Upgrade to `attack` only when the deception is CLEAR**, resolving uncertainty *downward*
252
+ * to `destructive` with the doubt stated. §12.1 is the reason: a halt that fires is already more
253
+ * likely wrong than right, so it must be spent only where the structure is unambiguous.
254
+ *
255
+ * CFG-28 deliberately left (2) out of the prompt, on the reasoning that it presumes the §4.6 floor.
256
+ * The scoping call was right and the reasoning was not: at both rated rungs a `destructive` outcome
257
+ * escalates anyway, so the clause would have cost nothing had it landed early. **The floor is what
258
+ * makes the `destructive` half of §4.6.1 meaningful, not what gates the upgrade.**
259
+ */
260
+ export declare const RATER_DECEPTION_GUIDANCE: string;
261
+ /**
262
+ * EXT-58 (spec §4.4) — the granted-alternative section of the rating prompt, built from the
263
+ * already-granted built-in tools of the current rung.
264
+ *
265
+ * Three properties are normative and each is spelled out to the rater:
266
+ *
267
+ * - It must name a granted tool **whenever** the outcome is not `safe` and one of them would do the
268
+ * job, because a free built-in call beats an interruption.
269
+ * - It must **not** name one when none can do the job — a path outside the working folder is the
270
+ * canonical case, where neither the read nor the edit tool can reach either. A facility that
271
+ * manufactures suggestions makes "a suggestion is never an approval" meaningless.
272
+ * - A suggestion is **never an approval**: it does not change the outcome, does not approve the
273
+ * original command, and does not pre-approve the suggested tool (which is gated normally when it
274
+ * arrives). The gate enforces this structurally — {@link mapVerdictToAction} never reads the
275
+ * field — but the rater is told so it does not soften an outcome because an alternative exists.
276
+ *
277
+ * The list is **trusted, locally-generated text** (§4.3) and therefore lives in the SYSTEM prompt,
278
+ * structurally outside the `<command_to_evaluate>` block that carries the untrusted command. Only
279
+ * tool names and one-line descriptions authored in `config/tool-descriptions.ts` ever appear here;
280
+ * no MCP/custom/A2A tool's own description can reach the rater.
281
+ *
282
+ * Returns `null` when nothing is granted (or the caller supplied no list), so the prompt is exactly
283
+ * the pre-EXT-58 text and the rater is never invited to invent a tool out of an empty list.
284
+ */
285
+ export declare function buildGrantedToolsGuidance(grantedTools: readonly GrantedToolSummary[] | undefined): string | null;
286
+ /**
287
+ * Build the rater's system prompt: the invariant {@link RATER_SYSTEM_PREAMBLE}, the four outcome
288
+ * definitions (with §4.1's recoverability question spelled out in
289
+ * {@link RATER_CATASTROPHIC_GUIDANCE} and §4.1.1's structural test in
290
+ * {@link RATER_ATTACK_GUIDANCE}), the rules that make `destructive` the catch-all and uncertainty a
291
+ * `destructive` rather than an outcome of its own, and — when the caller supplies them — the
292
+ * already-granted tools of §4.4 ({@link buildGrantedToolsGuidance}).
293
+ *
294
+ * `destructive` is defined **last and by exclusion** on purpose: the two severe outcomes have to be
295
+ * on the page before "everything else" can mean anything, and defining the catch-all in terms of
296
+ * the other three is what makes it structurally impossible for a command to fall outside the four.
297
+ *
298
+ * There is no strictness parameter: §1 removed strictness levels along with severity thresholds,
299
+ * so this prompt is the same at every rated rung.
300
+ */
301
+ export declare function buildRaterSystemPrompt(grantedTools?: readonly GrantedToolSummary[]): string;
302
+ /**
303
+ * Detect whether the command invokes an interpreter on a script target AND passes an
304
+ * `$ALL_CAPS` shell-variable expansion in its arguments — openclaw's "script preflight". Such a
305
+ * command can leak environment (often secrets) into the script, so it should bias toward
306
+ * escalation. Lightweight heuristic over the normalized command; a positive flag is fed to the
307
+ * rater prompt AND forces the fail-closed `destructive` path in the decision mapping.
308
+ *
309
+ * @returns true when an interpreter+script invocation also expands an ALL_CAPS env var.
310
+ */
311
+ export declare function hasScriptEnvLeakRisk(normalizedCommand: string): boolean;
312
+ /**
313
+ * Fold an absolute home path to `~` so the rater sees a stable, less-identifying form (mirrors
314
+ * hermes `_normalize_command_for_detection` path folding). Best-effort: only the literal home
315
+ * dir prefix is folded.
316
+ */
317
+ export declare function foldHomePath(command: string, home: string | undefined): string;
318
+ /**
319
+ * Build the messages for the rater call: the system prompt ({@link buildRaterSystemPrompt}) plus a
320
+ * human message that embeds the NORMALIZED command inside an XML `<command_to_evaluate>` tag and
321
+ * (optionally) notes what a deterministic preflight already found — the script-env-leak flag, and
322
+ * (§4.6) a host literal in a fetch position. The command text is only ever DATA in the tag — the
323
+ * builder never executes or interpolates it as instructions.
324
+ *
325
+ * The two preflight notes are worded differently on purpose. The script-env-leak note caps nothing
326
+ * but says "treat this as at least destructive"; the open-world note must NOT, because §4.6.1 asks
327
+ * the rater to *upgrade* a clear typosquat to `attack` and a note that anchors on `destructive`
328
+ * would talk it out of the one judgement it is still being asked for.
329
+ *
330
+ * §4.3 defines the rated unit tool-generally (tool name + JSON arguments); `run_shell_command` is
331
+ * the case whose argument is a command string, and it alone is additionally normalized and
332
+ * home-path-folded before fencing. The first implementation covers the shell only — every other
333
+ * tool is granted or escalated by the rung without a rating call until [[EXT-30]] widens the gate.
334
+ *
335
+ * Exposed (and returning plain strings) so tests can assert the structure: the tag is present,
336
+ * the untrusted-input preamble is present, and an injection string inside the command lands
337
+ * inside the tag rather than being acted on.
338
+ */
339
+ export declare function buildRaterPrompt(command: string, options?: {
340
+ home?: string;
341
+ grantedTools?: readonly GrantedToolSummary[];
342
+ }): {
343
+ system: string;
344
+ user: string;
345
+ };
346
+ /**
347
+ * Rate a single shell command with the rater model and return a structured
348
+ * {@link ShellSafetyVerdict}.
349
+ *
350
+ * - Builds an injection-hardened, normalized prompt ({@link buildRaterPrompt}).
351
+ * - Calls the rater model (defaults to `config.llm`) via `withStructuredOutput(schema)`.
352
+ * - Races the call against {@link RATER_DEFAULT_TIMEOUT_MS}.
353
+ * - **Fail-closed:** any throw / timeout / parse failure returns {@link FAIL_CLOSED_VERDICT}
354
+ * (`destructive` + "could not assess"), never an approval.
355
+ *
356
+ * Note: this only produces a verdict; the approve / escalate / halt decision (including
357
+ * fail-closed-on-ambiguity) is made by {@link mapVerdictToAction} in the runner.
358
+ */
359
+ export declare function rateShellCommand(command: string, config: GthConfig, options?: {
360
+ model?: BaseChatModel;
361
+ home?: string;
362
+ timeoutMs?: number;
363
+ /**
364
+ * EXT-58 (§4.4) — the already-granted built-ins of the current rung. Supplied, the rater is
365
+ * asked to name one whenever it does not return `safe` and one would do the job; omitted, the
366
+ * prompt is exactly as before and no suggestion is ever produced.
367
+ */
368
+ grantedTools?: readonly GrantedToolSummary[];
369
+ }): Promise<ShellSafetyVerdict>;
370
+ /**
371
+ * The action the approvals gate resolves to for a single gated call, BEFORE the human prompt.
372
+ *
373
+ * - `approve` — approve ONCE; do not touch the human or the allow-list.
374
+ * - `escalate` — fall through to the human approval callback, carrying the verdict when one
375
+ * exists. Where there is no human, §6.2 turns this into an immediate non-zero exit — that
376
+ * translation belongs to the runner, not here.
377
+ * - `halt` — **end the agent loop** (§4.2). Reserved for `attack`. It is not a rejection the
378
+ * model can respond to and offers it no moves; no rung except `bypass` can turn it into
379
+ * anything else.
380
+ *
381
+ * There is deliberately **no `refuse` arm for `catastrophic`** (settled 2026-07-27c, §4.2). The
382
+ * deterministic members of that class — fork bomb, `mkfs`, `rm -rf /`, `dd` to a block device — are
383
+ * already refused unappealably by the §8 hardline floor under every rung including `bypass`, so a
384
+ * refusing `catastrophic` would add nothing for the commands that motivate the idea. What it would
385
+ * newly refuse is the remainder the floor cannot reach, every member of which has routine
386
+ * legitimate use (a staging database, an ephemeral `terraform destroy`, a preview namespace). An
387
+ * unmeasured classifier belongs behind a human who can correct it; a refusal has no correction
388
+ * path.
389
+ */
390
+ export type RaterAction = 'approve' | 'escalate' | 'halt';
391
+ /** Inputs to the decision mapping: just the rung. Each rung fully determines behaviour (§1). */
392
+ export interface RaterDecisionOptions {
393
+ /** The rung in force for this session. */
394
+ rung: ApprovalRung;
395
+ }
396
+ /**
397
+ * The verdict {@link mapVerdictToAction} acts on, after the gate's own fail-closed preflight has
398
+ * overridden the rater where it must. Returned alongside the action so the caller surfaces the
399
+ * HONEST reason (a "could not assess" note) rather than whatever the rater claimed about a
400
+ * command the gate could not statically vet. `undefined` at the unrated rungs, where no rating
401
+ * call was made at all.
402
+ */
403
+ export interface RaterDecision {
404
+ action: RaterAction;
405
+ /** The verdict actually used — the rater's, or the fail-closed `destructive` override. */
406
+ verdict?: ShellSafetyVerdict;
407
+ }
408
+ /**
409
+ * Is this outcome below the deterministic `destructive` floor — i.e. may a preflight rewrite it?
410
+ *
411
+ * See {@link BELOW_DESTRUCTIVE_FLOOR}. An outcome that is not in the table is treated as below the
412
+ * floor, so an out-of-band value is FLOORED to `destructive` rather than sailing past the preflight
413
+ * carrying the model's own unvalidated reason.
414
+ *
415
+ * The lookup is OWN-PROPERTY-ONLY, and the declared `boolean` return is the reason. `outcome` is
416
+ * only `RaterOutcome` as far as the compiler is concerned — this helper exists to be robust to a
417
+ * value that lied — and a `?? undefined` default would still hand back the *inherited* value for a
418
+ * prototype-chain key (`'toString'` → a function, `'constructor'` → `Object`). Those happen to be
419
+ * truthy, so today's single caller would still floor; but a caller written as `=== true`, which is
420
+ * how a predicate advertised as hardened invites being consumed, would fail OPEN on exactly the
421
+ * class of input this function is for. `Object.hasOwn` makes the advertised invariant true rather
422
+ * than incidentally true.
423
+ */
424
+ export declare function isBelowDestructiveFloor(outcome: RaterOutcome): boolean;
425
+ /**
426
+ * **THE deterministic floor — the one place an outcome is raised to `destructive`.**
427
+ *
428
+ * Every gated call reaches it: a shell command through {@link mapVerdictToAction}'s preflights
429
+ * ({@link preflightFloorReason}), a tool call through its effective `openWorldHint`
430
+ * ({@link openWorldToolFloorReason}, §4.7.3). They differ only in the *reason* they compute; what
431
+ * the reason then does to the outcome is decided here and nowhere else. A second implementation is
432
+ * how a gate and a display come to disagree about what a call is, and how one of them comes to
433
+ * *lower* an outcome the other raised.
434
+ *
435
+ * Two properties, both delegated to {@link isBelowDestructiveFloor} so they hold for every caller:
436
+ *
437
+ * - **It only ever RAISES.** A `destructive`, `catastrophic` or `attack` verdict passes through
438
+ * untouched, keeping its own explanation (and any §4.4 suggestion) — the floor is agreeing with
439
+ * it, not overriding it, and a floor that rewrote `catastrophic` would silently trade an
440
+ * unnegotiable escalation for a negotiable one.
441
+ * - **`undefined` is below the floor.** Nothing has assessed the call, so there is no outcome for
442
+ * the floor to defer to; a call nobody rated is exactly the call this rule exists to speak for.
443
+ * That is what lets a tool call — which no rater sees while §4.3's scope boundary stands — be
444
+ * floored by the same function that floors a rated shell command.
445
+ *
446
+ * @param verdict The outcome so far, or `undefined` when nothing has rated the call.
447
+ * @param reason The floor reason, or `null` when no preflight fired (the verdict is returned as-is).
448
+ */
449
+ export declare function applyDestructiveFloor(verdict: ShellSafetyVerdict, reason: string | null): ShellSafetyVerdict;
450
+ export declare function applyDestructiveFloor(verdict: ShellSafetyVerdict | undefined, reason: string | null): ShellSafetyVerdict | undefined;
451
+ /**
452
+ * EXT-70 (§4.7.2, §4.7.3) — the **tool** arm of the open-world floor: the reason a call whose
453
+ * EFFECTIVE `openWorldHint` is true is floored at `destructive`, or `null` when it is not.
454
+ *
455
+ * It sits beside {@link preflightFloorReason} because it is the same rule seen from the other side.
456
+ * §4.6 floors a shell fetch before any model call precisely so that no misreading of a hostname can
457
+ * auto-approve; *the same fetch reached through a tool instead of through `curl` must not be
458
+ * ungated*, or the preflight is a rule about spelling rather than about fetching. Both feed
459
+ * {@link applyDestructiveFloor}.
460
+ *
461
+ * **Independent of `readOnlyHint`, and that is the whole of §4.7.3.** A fetch tool is read-only in
462
+ * the local sense — it mutates nothing on this machine — while reaching the network; the two facts
463
+ * are unrelated, and `gth_web_fetch` (`readOnlyHint: true`, `openWorldHint: true`) is the case that
464
+ * proves it. `destructiveHint` is not consulted either: §4.7.2 lets it only ever RAISE, so a
465
+ * `destructiveHint: false` can never lower a floor this rule set. `idempotentHint` has no built-in
466
+ * consumer at all — do not invent one here.
467
+ *
468
+ * @param annotations The call's effective set (§4.7.1), never its declared one — trust has already
469
+ * been applied, so an untrusted server's `openWorldHint: false` has already collapsed to the
470
+ * fail-closed `true` by the time it arrives. `undefined` (a source that cannot decide) floors, in
471
+ * the same direction as the fail-closed default it would otherwise have returned.
472
+ */
473
+ export declare function openWorldToolFloorReason(annotations: EffectiveToolAnnotations | undefined): string | null;
474
+ /**
475
+ * CFG-27 — pure, testable mapping from a {@link ShellSafetyVerdict} + the raw command to a
476
+ * {@link RaterAction}, keyed on the **rung** (spec §4.2, §8):
477
+ *
478
+ * | Outcome | `read-only`/`write` | `auto-safe` | `full-auto` | `bypass` |
479
+ * |---|---|---|---|---|
480
+ * | — (no rating) | escalate | | | approve |
481
+ * | `safe` | — | approve | approve | — |
482
+ * | `destructive` | — | escalate | negotiate ([[EXT-29]]; escalate for now) | — |
483
+ * | `catastrophic` | — | escalate | escalate — **never negotiate** | — |
484
+ * | `attack` | — | **halt** | **halt** | — |
485
+ *
486
+ * Order of precedence (fail-closed FIRST — **this ordering IS the safety property**):
487
+ *
488
+ * 1. `bypass` → `approve`. The gate is off. The declared deny list and the exec-time hardline
489
+ * floor still apply, but neither is decided here.
490
+ * 2. Unrated rungs (`read-only`, `write`) → `escalate`. No model is consulted at all; the
491
+ * allow-list is checked by the caller BEFORE this function, so reaching here means the human
492
+ * decides. (Both rungs behave identically for the shell because the shell is the only gated
493
+ * tool today — the built-in read/write tools each rung grants are not gated until [[EXT-30]]
494
+ * widens the gate. That is a scope boundary, not a missing branch.)
495
+ * 3. **The deterministic preflights, which FLOOR the outcome at `destructive` and never lower
496
+ * one** ({@link preflightFloorReason}). Ambiguity ({@link classifyCommand} returns null — the
497
+ * command composes / substitutes / redirects, so its target cannot be statically resolved), the
498
+ * script-env-leak preflight ({@link hasScriptEnvLeakRisk}), and EXT-61's open-world preflight
499
+ * ({@link findOpenWorldHostLiterals} — a host literal in a fetch/transfer position, §4.6) are all
500
+ * recomputed from the RAW command, independently of what
501
+ * the rater said. Any one of them rewrites a verdict that sits BELOW the floor — i.e. `safe`, and
502
+ * only `safe` ({@link isBelowDestructiveFloor}) — to `destructive` with an honest
503
+ * {@link COULD_NOT_ASSESS_PREFIX} reason, **before the `safe` check**, so a manipulated `safe`
504
+ * verdict can never slip an unresolvable command through. **A rater verdict may only ever make
505
+ * an outcome worse, never better**, and so may a preflight: `destructive`, `catastrophic` and
506
+ * `attack` all pass through UNCHANGED. (Before the rescale this branch excluded the single
507
+ * halting outcome BY NAME. Renamed in place it would have let a preflight hit *downgrade* a
508
+ * `catastrophic` verdict to `destructive` — the exact inverse of the invariant above, silently
509
+ * trading an unnegotiable escalation for a negotiable one at `full-auto`.)
510
+ * 4. `attack` → `halt`, at both rated rungs, never negotiable.
511
+ * 5. `safe` → `approve`; `catastrophic` → `escalate` and MUST NOT enter §5; `destructive` →
512
+ * `escalate` (a negotiation at `full-auto` once [[EXT-29]] lands).
513
+ *
514
+ * **EXT-58 (§4.4): the verdict's `suggestedTool` is not read here, and that is deliberate.** A
515
+ * suggestion is never an approval — it must not change the action, must not approve the original
516
+ * command, and must not pre-approve the suggested tool. The gate also never decides for itself that
517
+ * a shell command is "equivalent" to a built-in and substitutes it: any such equivalence test would
518
+ * be a second command parser, and a second command parser is a second place for the gate to be
519
+ * bypassed. The suggestion is carried, untouched, to the human (§6) and to the model (§7) — nothing
520
+ * else. Note that the fail-closed rewrite in (3) builds a FRESH verdict and therefore drops any
521
+ * suggestion along with the reason it belonged to: a verdict the gate has just declared
522
+ * untrustworthy must not keep recommending anything. A verdict the preflight leaves alone was never
523
+ * declared untrustworthy — the gate is agreeing with it, not overriding it — so it keeps both.
524
+ *
525
+ * @param command The raw command string (used to recompute ambiguity + preflight independently
526
+ * of the rater, so the gate is robust even if the rater is wrong or manipulated).
527
+ * @param verdict The rater's verdict (or {@link FAIL_CLOSED_VERDICT}); `undefined` at the unrated
528
+ * rungs. A missing verdict at a RATED rung is treated as {@link FAIL_CLOSED_VERDICT}.
529
+ * @param opts The rung in force.
530
+ */
531
+ export declare function mapVerdictToAction(command: string, verdict: ShellSafetyVerdict | undefined, opts: RaterDecisionOptions): RaterDecision;
532
+ /**
533
+ * EXT-71 §3.2 — the mapping for a call an **allow entry already matched** while keeping the rater
534
+ * involved (`rate: true`). This is a **TRIPWIRE, not a re-adjudication**, and the difference is the
535
+ * whole reason it is a separate function from {@link mapVerdictToAction}:
536
+ *
537
+ * | Outcome | Action | Why |
538
+ * |---|---|---|
539
+ * | `safe` | approve | nothing to say |
540
+ * | `destructive` | **approve** | the human already authorized this call; the rater does not overrule a standing human decision by disliking it |
541
+ * | `catastrophic` | escalate | a human decides, and per §4.2 that approval is never sticky |
542
+ * | `attack` | **halt** | exactly per §4.2 — the structure evidenced compromise, which no prior grant answers |
543
+ *
544
+ * The rater's job on an allow-listed call is to catch the tail where a broad entry matched
545
+ * something structurally hostile — not to re-ask a question the human answered.
546
+ *
547
+ * **The deterministic preflights are deliberately not consulted** ({@link preflightFloorReason} is
548
+ * not called). §4.6 states it directly for the open-world arm: *an allow match lifts this floor even
549
+ * when the entry keeps the rater involved — the tripwire still sees the call; the floor does not
550
+ * apply to it.* The other two arms are lifted with it, and doing so changes no outcome: a preflight
551
+ * only ever raises `safe` to `destructive`, and both of those run here. Applying the floor would
552
+ * therefore alter nothing except to replace an honest verdict with a note about a decision this
553
+ * mapping does not make. (The ambiguity arm cannot fire at all — an allow entry does not match a
554
+ * command that fails to statically resolve.)
555
+ *
556
+ * @param verdict The rater's verdict; `undefined` or a fail-closed verdict is `destructive` and so
557
+ * runs — the tripwire failing to answer does not revoke the human's standing decision, exactly as
558
+ * `rate: false` would not have asked in the first place.
559
+ */
560
+ export declare function mapAllowMatchedVerdictToAction(verdict: ShellSafetyVerdict | undefined): RaterDecision;