@gaunt-sloth/core 2.0.0-alpha.3 → 2.0.0-alpha.31
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/README.md +52 -20
- package/dist/config/colour.d.ts +38 -0
- package/dist/config/colour.js +36 -0
- package/dist/config/colour.js.map +1 -0
- package/dist/config/defaults.d.ts +84 -0
- package/dist/config/defaults.js +99 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/filesystem-tools.d.ts +41 -0
- package/dist/config/filesystem-tools.js +56 -0
- package/dist/config/filesystem-tools.js.map +1 -0
- package/dist/config/jsonc.d.ts +12 -0
- package/dist/config/jsonc.js +41 -0
- package/dist/config/jsonc.js.map +1 -0
- package/dist/config/loader.d.ts +246 -0
- package/dist/config/loader.js +1321 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/mouse.d.ts +50 -0
- package/dist/config/mouse.js +44 -0
- package/dist/config/mouse.js.map +1 -0
- package/dist/config/profiles.d.ts +68 -0
- package/dist/config/profiles.js +93 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/config/providerKeys.d.ts +69 -0
- package/dist/config/providerKeys.js +69 -0
- package/dist/config/providerKeys.js.map +1 -0
- package/dist/config/schema.d.ts +2999 -0
- package/dist/config/schema.js +1519 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/shell-policy.d.ts +896 -0
- package/dist/config/shell-policy.js +750 -0
- package/dist/config/shell-policy.js.map +1 -0
- package/dist/config/tool-descriptions.d.ts +211 -0
- package/dist/config/tool-descriptions.js +272 -0
- package/dist/config/tool-descriptions.js.map +1 -0
- package/dist/config/types.d.ts +760 -0
- package/dist/config/types.js +13 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config.d.ts +34 -827
- package/dist/config.js +25 -657
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +46 -0
- package/dist/constants.js +46 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +135 -2
- package/dist/core/GthAbstractAgent.js +496 -30
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +568 -54
- package/dist/core/GthAgentRunner.js +1537 -136
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.d.ts +115 -0
- package/dist/core/GthLangChainAgent.js +644 -17
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/approvals/annotations.d.ts +122 -0
- package/dist/core/approvals/annotations.js +137 -0
- package/dist/core/approvals/annotations.js.map +1 -0
- package/dist/core/approvals/grants.d.ts +216 -0
- package/dist/core/approvals/grants.js +469 -0
- package/dist/core/approvals/grants.js.map +1 -0
- package/dist/core/approvals/matcher.d.ts +202 -0
- package/dist/core/approvals/matcher.js +267 -0
- package/dist/core/approvals/matcher.js.map +1 -0
- package/dist/core/approvals/mcpSubjects.d.ts +40 -0
- package/dist/core/approvals/mcpSubjects.js +99 -0
- package/dist/core/approvals/mcpSubjects.js.map +1 -0
- package/dist/core/approvals/toolAnnotationSources.d.ts +105 -0
- package/dist/core/approvals/toolAnnotationSources.js +277 -0
- package/dist/core/approvals/toolAnnotationSources.js.map +1 -0
- package/dist/core/approvals/toolHost.d.ts +46 -0
- package/dist/core/approvals/toolHost.js +108 -0
- package/dist/core/approvals/toolHost.js.map +1 -0
- package/dist/core/debugCapture.d.ts +75 -0
- package/dist/core/debugCapture.js +101 -0
- package/dist/core/debugCapture.js.map +1 -0
- package/dist/core/gthLeanAgentFactory.d.ts +9 -0
- package/dist/core/gthLeanAgentFactory.js +10 -0
- package/dist/core/gthLeanAgentFactory.js.map +1 -0
- package/dist/core/launchBanner.d.ts +120 -0
- package/dist/core/launchBanner.js +418 -0
- package/dist/core/launchBanner.js.map +1 -0
- package/dist/core/plainToolIndication.d.ts +15 -0
- package/dist/core/plainToolIndication.js +174 -0
- package/dist/core/plainToolIndication.js.map +1 -0
- 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.d.ts +53 -0
- package/dist/core/refusal.js +133 -0
- package/dist/core/refusal.js.map +1 -0
- package/dist/core/runStats.d.ts +52 -0
- package/dist/core/runStats.js +118 -0
- package/dist/core/runStats.js.map +1 -0
- package/dist/core/shell/ShellCommandFailedError.d.ts +54 -0
- package/dist/core/shell/ShellCommandFailedError.js +68 -0
- package/dist/core/shell/ShellCommandFailedError.js.map +1 -0
- package/dist/core/shell/abstention.d.ts +88 -0
- package/dist/core/shell/abstention.js +184 -0
- package/dist/core/shell/abstention.js.map +1 -0
- 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 +84 -0
- package/dist/core/shell/approvalStop.js +119 -0
- package/dist/core/shell/approvalStop.js.map +1 -0
- package/dist/core/shell/arity.d.ts +6 -0
- package/dist/core/shell/arity.js +20 -6
- package/dist/core/shell/arity.js.map +1 -1
- package/dist/core/shell/denylist.d.ts +11 -0
- package/dist/core/shell/denylist.js +37 -0
- package/dist/core/shell/denylist.js.map +1 -0
- 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 +44 -4
- package/dist/core/shell/normalize.js +61 -7
- package/dist/core/shell/normalize.js.map +1 -1
- package/dist/core/shell/openWorld.d.ts +263 -0
- package/dist/core/shell/openWorld.js +1188 -0
- package/dist/core/shell/openWorld.js.map +1 -0
- package/dist/core/shell/rater.d.ts +935 -0
- package/dist/core/shell/rater.js +1473 -0
- package/dist/core/shell/rater.js.map +1 -0
- package/dist/core/shell/raterModel.d.ts +41 -0
- package/dist/core/shell/raterModel.js +51 -0
- package/dist/core/shell/raterModel.js.map +1 -0
- package/dist/core/shell/rejection.d.ts +69 -0
- package/dist/core/shell/rejection.js +38 -0
- package/dist/core/shell/rejection.js.map +1 -0
- package/dist/core/toolCallRepair/grammar.d.ts +41 -0
- package/dist/core/toolCallRepair/grammar.js +116 -0
- package/dist/core/toolCallRepair/grammar.js.map +1 -0
- package/dist/core/toolCallRepair/index.d.ts +2 -0
- package/dist/core/toolCallRepair/index.js +7 -0
- package/dist/core/toolCallRepair/index.js.map +1 -0
- package/dist/core/toolCallRepair/payload.d.ts +36 -0
- package/dist/core/toolCallRepair/payload.js +341 -0
- package/dist/core/toolCallRepair/payload.js.map +1 -0
- package/dist/core/toolCallRepair/promote.d.ts +45 -0
- package/dist/core/toolCallRepair/promote.js +90 -0
- package/dist/core/toolCallRepair/promote.js.map +1 -0
- package/dist/core/toolDisplay.d.ts +123 -0
- package/dist/core/toolDisplay.js +451 -0
- package/dist/core/toolDisplay.js.map +1 -0
- package/dist/core/toolOutputChannel.d.ts +95 -0
- package/dist/core/toolOutputChannel.js +165 -0
- package/dist/core/toolOutputChannel.js.map +1 -0
- package/dist/core/types.d.ts +294 -12
- package/dist/core/types.js.map +1 -1
- package/dist/history/historyFormat.d.ts +28 -0
- package/dist/history/historyFormat.js +127 -0
- package/dist/history/historyFormat.js.map +1 -0
- package/dist/history/historyStore.d.ts +198 -0
- package/dist/history/historyStore.js +482 -0
- package/dist/history/historyStore.js.map +1 -0
- package/dist/history/recordSession.d.ts +37 -0
- package/dist/history/recordSession.js +56 -0
- package/dist/history/recordSession.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +5 -10
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/deepseek.d.ts +1 -1
- package/dist/providers/deepseek.js +5 -10
- package/dist/providers/deepseek.js.map +1 -1
- package/dist/providers/geminiSchemaSanitizer.d.ts +55 -0
- package/dist/providers/geminiSchemaSanitizer.js +347 -0
- package/dist/providers/geminiSchemaSanitizer.js.map +1 -0
- 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.d.ts +1 -1
- package/dist/providers/google-genai.js +12 -11
- package/dist/providers/google-genai.js.map +1 -1
- package/dist/providers/groq.d.ts +1 -1
- package/dist/providers/groq.js +5 -10
- package/dist/providers/groq.js.map +1 -1
- package/dist/providers/huggingface.d.ts +25 -0
- package/dist/providers/huggingface.js +69 -0
- package/dist/providers/huggingface.js.map +1 -0
- package/dist/providers/modelCatalog.d.ts +109 -0
- package/dist/providers/modelCatalog.js +245 -0
- package/dist/providers/modelCatalog.js.map +1 -0
- package/dist/providers/modelDiscovery.d.ts +153 -6
- package/dist/providers/modelDiscovery.js +295 -41
- package/dist/providers/modelDiscovery.js.map +1 -1
- package/dist/providers/ollama.d.ts +19 -5
- package/dist/providers/ollama.js +60 -52
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +39 -10
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +4 -5
- package/dist/providers/openrouter.js +20 -35
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/vertexai.d.ts +1 -1
- package/dist/providers/vertexai.js +12 -11
- package/dist/providers/vertexai.js.map +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +5 -10
- package/dist/providers/xai.js.map +1 -1
- package/dist/runtime/askStructured.d.ts +105 -0
- package/dist/runtime/askStructured.js +120 -0
- package/dist/runtime/askStructured.js.map +1 -0
- package/dist/runtime/conversation.d.ts +60 -0
- package/dist/runtime/conversation.js +151 -0
- package/dist/runtime/conversation.js.map +1 -0
- package/dist/runtime/singleShot.d.ts +25 -5
- package/dist/runtime/singleShot.js +95 -36
- package/dist/runtime/singleShot.js.map +1 -1
- package/dist/runtime/structuredOutput.d.ts +104 -0
- package/dist/runtime/structuredOutput.js +393 -0
- package/dist/runtime/structuredOutput.js.map +1 -0
- package/dist/utils/ProgressIndicator.d.ts +21 -0
- package/dist/utils/ProgressIndicator.js +30 -3
- package/dist/utils/ProgressIndicator.js.map +1 -1
- package/dist/utils/aiignoreUtils.js.map +1 -1
- package/dist/utils/binaryOutputUtils.js.map +1 -1
- package/dist/utils/consoleUtils.d.ts +52 -0
- package/dist/utils/consoleUtils.js +72 -2
- package/dist/utils/consoleUtils.js.map +1 -1
- package/dist/utils/crashHandler.d.ts +87 -0
- package/dist/utils/crashHandler.js +128 -0
- package/dist/utils/crashHandler.js.map +1 -0
- package/dist/utils/debugDump.d.ts +134 -0
- package/dist/utils/debugDump.js +381 -0
- package/dist/utils/debugDump.js.map +1 -0
- package/dist/utils/debugUtils.d.ts +13 -4
- package/dist/utils/debugUtils.js +36 -13
- package/dist/utils/debugUtils.js.map +1 -1
- package/dist/utils/displayWidth.d.ts +53 -0
- package/dist/utils/displayWidth.js +195 -0
- package/dist/utils/displayWidth.js.map +1 -0
- package/dist/utils/fileUtils.d.ts +24 -2
- package/dist/utils/fileUtils.js +54 -12
- package/dist/utils/fileUtils.js.map +1 -1
- package/dist/utils/llmUtils.d.ts +39 -8
- package/dist/utils/llmUtils.js +76 -8
- package/dist/utils/llmUtils.js.map +1 -1
- package/dist/utils/redactSecrets.d.ts +63 -0
- package/dist/utils/redactSecrets.js +286 -0
- package/dist/utils/redactSecrets.js.map +1 -0
- package/dist/utils/systemPromptNotes.d.ts +225 -0
- package/dist/utils/systemPromptNotes.js +340 -0
- package/dist/utils/systemPromptNotes.js.map +1 -0
- package/dist/utils/systemUtils.d.ts +49 -1
- package/dist/utils/systemUtils.js +67 -3
- package/dist/utils/systemUtils.js.map +1 -1
- package/dist/utils/toolMatching.d.ts +30 -0
- package/dist/utils/toolMatching.js +44 -0
- package/dist/utils/toolMatching.js.map +1 -0
- package/dist/utils/untrustedText.d.ts +66 -0
- package/dist/utils/untrustedText.js +80 -0
- package/dist/utils/untrustedText.js.map +1 -0
- package/package.json +22 -6
- package/schema/gsloth-config.schema.json +3134 -0
- package/dist/core/shell/allowlist.d.ts +0 -75
- package/dist/core/shell/allowlist.js +0 -187
- package/dist/core/shell/allowlist.js.map +0 -1
- package/dist/core/shell/judge.d.ts +0 -161
- package/dist/core/shell/judge.js +0 -261
- package/dist/core/shell/judge.js.map +0 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/shell/ShellCommandFailedError
|
|
3
|
+
*
|
|
4
|
+
* The typed error a `run_*` shell/dev tool throws when a command did NOT exit cleanly
|
|
5
|
+
* (non-zero exit code, or killed for exceeding the timeout). It carries the FULL model-facing
|
|
6
|
+
* body so a softening middleware can hand the model the exact observation it saw before — the
|
|
7
|
+
* only change being that the tool result's status flips to `'error'` (→ `isError` → the ✗ glyph).
|
|
8
|
+
*
|
|
9
|
+
* Canonical home is **core** so BOTH agents can recognise a shell failure without violating the
|
|
10
|
+
* `agent → core` dependency direction:
|
|
11
|
+
* - the deep agent (`code` mode, `@gaunt-sloth/agent` `GthDeepShellExitSoftening`) and
|
|
12
|
+
* - the lean agent (`exec` / `ask --write`, core `GthLangChainAgent` `GthLeanShellExitSoftening`).
|
|
13
|
+
*
|
|
14
|
+
* The throw site (`GthDevToolkit.executeCommand`) lives in the `agent` package and re-exports this
|
|
15
|
+
* class, so its `throw new ShellCommandFailedError(...)` is one and the same type both agents catch.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A `run_*` command that did NOT exit cleanly (non-zero exit code, or was killed for exceeding the
|
|
19
|
+
* timeout). Carries the FULL model-facing body text ({@link output}) so a softening middleware can
|
|
20
|
+
* hand the model the exact same observation it saw before — the only change is the tool result's
|
|
21
|
+
* status flips to `'error'`, which drives the ✗ (`isError`) glyph.
|
|
22
|
+
*
|
|
23
|
+
* `executeCommand` previously `resolve()`d on a non-zero exit, so the LangChain `ToolMessage` stayed
|
|
24
|
+
* `status: 'success'` and every failure rendered a ✓. Throwing this typed error instead lets each
|
|
25
|
+
* agent's softening middleware convert it into an error `ToolMessage`. A clean exit (`code === 0`)
|
|
26
|
+
* still `resolve()`s; a spawn-level `child.on('error')` still rejects with a plain `Error`.
|
|
27
|
+
*/
|
|
28
|
+
export class ShellCommandFailedError extends Error {
|
|
29
|
+
/** The full model-facing body (command echo + `<COMMAND_OUTPUT>` + the failure/timeout tail). */
|
|
30
|
+
output;
|
|
31
|
+
/** The process exit code; `null` when the command was killed (timeout) and never exited cleanly. */
|
|
32
|
+
exitCode;
|
|
33
|
+
/** The exact command string that was executed. */
|
|
34
|
+
command;
|
|
35
|
+
/** The run_* tool name that invoked the command (e.g. `run_tests`, `run_shell_command`). */
|
|
36
|
+
toolName;
|
|
37
|
+
constructor(params) {
|
|
38
|
+
// Use the full body as the Error message so any generic logger/handler still surfaces the
|
|
39
|
+
// real command output rather than an opaque wrapper string.
|
|
40
|
+
super(params.output);
|
|
41
|
+
this.name = 'ShellCommandFailedError';
|
|
42
|
+
this.output = params.output;
|
|
43
|
+
this.exitCode = params.exitCode;
|
|
44
|
+
this.command = params.command;
|
|
45
|
+
this.toolName = params.toolName;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Recognise a {@link ShellCommandFailedError} for the softening middleware in either agent.
|
|
50
|
+
*
|
|
51
|
+
* Prefers a plain `instanceof` (both agents share this one core module, so the class identity is
|
|
52
|
+
* the same), but falls back to a STRUCTURAL check keyed on `name === 'ShellCommandFailedError'`
|
|
53
|
+
* plus the carried fields. The structural arm is deliberate defence against a dual-package /
|
|
54
|
+
* realm-boundary hazard: if the error ever crossed a module boundary that broke `instanceof`, we
|
|
55
|
+
* would otherwise silently rethrow a real shell failure and regress the ✗ signal. Every field the
|
|
56
|
+
* softener reads (`output`) is asserted so a narrowed value is safe to use.
|
|
57
|
+
*/
|
|
58
|
+
export function isShellCommandFailedError(e) {
|
|
59
|
+
if (e instanceof ShellCommandFailedError)
|
|
60
|
+
return true;
|
|
61
|
+
return (typeof e === 'object' &&
|
|
62
|
+
e !== null &&
|
|
63
|
+
e.name === 'ShellCommandFailedError' &&
|
|
64
|
+
typeof e.output === 'string' &&
|
|
65
|
+
typeof e.command === 'string' &&
|
|
66
|
+
typeof e.toolName === 'string');
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=ShellCommandFailedError.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShellCommandFailedError.js","sourceRoot":"","sources":["../../../src/core/shell/ShellCommandFailedError.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAChD,iGAAiG;IACxF,MAAM,CAAS;IACxB,oGAAoG;IAC3F,QAAQ,CAAgB;IACjC,kDAAkD;IACzC,OAAO,CAAS;IACzB,4FAA4F;IACnF,QAAQ,CAAS;IAE1B,YAAY,MAKX;QACC,0FAA0F;QAC1F,4DAA4D;QAC5D,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;QACtC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QAChC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC9B,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAClC,CAAC;CACF;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CAAC,CAAU;IAClD,IAAI,CAAC,YAAY,uBAAuB;QAAE,OAAO,IAAI,CAAC;IACtD,OAAO,CACL,OAAO,CAAC,KAAK,QAAQ;QACrB,CAAC,KAAK,IAAI;QACT,CAAwB,CAAC,IAAI,KAAK,yBAAyB;QAC5D,OAAQ,CAA0B,CAAC,MAAM,KAAK,QAAQ;QACtD,OAAQ,CAA2B,CAAC,OAAO,KAAK,QAAQ;QACxD,OAAQ,CAA4B,CAAC,QAAQ,KAAK,QAAQ,CAC3D,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape in the command that the gate's parser could not resolve.
|
|
3
|
+
*
|
|
4
|
+
* These are FORMS, not judgements — nothing here says a command is dangerous, and nothing here is a
|
|
5
|
+
* rating. They exist to select the sentence the rater is shown.
|
|
6
|
+
*/
|
|
7
|
+
export type AbstentionMechanism =
|
|
8
|
+
/** A git commit whose message contains a substitution the SHELL runs before git sees it. */
|
|
9
|
+
'commit-message-substitution'
|
|
10
|
+
/** More than one command: `;`, `&`, `&&`, `|`, `||`, or a line break. */
|
|
11
|
+
| 'composition'
|
|
12
|
+
/** Command / variable / process substitution: `$(…)`, backticks, `${…}`, `<(…)`. */
|
|
13
|
+
| 'substitution'
|
|
14
|
+
/** A stream redirect: `>`, `>>`, `<`, `2>`, `&>`. */
|
|
15
|
+
| 'redirect'
|
|
16
|
+
/** None of the above — an unbalanced quote, an empty command, or no resolvable program name. */
|
|
17
|
+
| 'unparseable';
|
|
18
|
+
/** What the parser saw in a command it could not resolve: the shapes, and the note for each. */
|
|
19
|
+
export interface AbstentionDefect {
|
|
20
|
+
/** The most specific mechanism found — what the note leads with. */
|
|
21
|
+
mechanism: AbstentionMechanism;
|
|
22
|
+
/** Every mechanism found, in note order. A command may carry more than one. */
|
|
23
|
+
mechanisms: AbstentionMechanism[];
|
|
24
|
+
/** One note per mechanism, in the same order. Never empty. */
|
|
25
|
+
notes: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* **The per-family note text — one distinct string per {@link AbstentionMechanism}.**
|
|
29
|
+
*
|
|
30
|
+
* Typed as a total `Record`, so adding a mechanism is a COMPILE ERROR until someone writes its
|
|
31
|
+
* sentence. A family that silently fell back to another family's note would be the failure this
|
|
32
|
+
* table exists to prevent: [[QA-17]] measured that the note's effect is family-specific, and
|
|
33
|
+
* `docs/test-sessions/qa-17-substitution-note-2026-08-03/` records a narrowing by family name that
|
|
34
|
+
* was FALSIFIED — `gh pr comment 42 --body "…"` carrying an executing substitution classifies as
|
|
35
|
+
* generic `substitution`, and three of three hosted raters called it safe unassisted.
|
|
36
|
+
*
|
|
37
|
+
* Every entry states a fact about what THE SHELL does, and then asks a question. Neither is a
|
|
38
|
+
* verdict about the command in front of the rater, which is what keeps the note neutral while still
|
|
39
|
+
* carrying the mechanism that the measurement showed is load-bearing.
|
|
40
|
+
*/
|
|
41
|
+
export declare const MECHANISM_NOTES: Readonly<Record<AbstentionMechanism, string>>;
|
|
42
|
+
/**
|
|
43
|
+
* Classify a command the gate's parser could not resolve, or return `null` when it resolves.
|
|
44
|
+
*
|
|
45
|
+
* **The `null` arm is the guard, and it is structural on purpose.** Every mechanism below is a
|
|
46
|
+
* regex, and `unparseable` is the fallback — so without this gate the function would classify `ls
|
|
47
|
+
* -la` as `unparseable` and a note would be attached to every command in the session. The predicate
|
|
48
|
+
* is {@link classifyCommand}'s own, not a second reading of it, so the note appears on exactly the
|
|
49
|
+
* set of commands the gate could not resolve and on no others.
|
|
50
|
+
*
|
|
51
|
+
* Detection runs on the NORMALIZED command (the same form `classifyCommand` refused), so obfuscation
|
|
52
|
+
* that the normalizer collapses cannot hide a mechanism from the note — while the command shown to
|
|
53
|
+
* the rater stays the one the rater is being asked about, fenced by its own caller.
|
|
54
|
+
*
|
|
55
|
+
* The line-break check reads the RAW command, exactly as `classifyCommand` does: the normalizer is
|
|
56
|
+
* not required to preserve that separator, and EXT-55 is the node about what happens when a caller
|
|
57
|
+
* assumes it did.
|
|
58
|
+
*
|
|
59
|
+
* **Order is note order, most specific first**, and `commit-message-substitution` SUPPRESSES the
|
|
60
|
+
* generic substitution note it is a special case of — telling the rater the same thing twice about
|
|
61
|
+
* one `$(…)` spends attention on a repetition.
|
|
62
|
+
*
|
|
63
|
+
* @param command The raw command string as the model proposed it.
|
|
64
|
+
* @returns What the parser saw, or `null` when the command's target statically resolves.
|
|
65
|
+
*/
|
|
66
|
+
export declare function describeAbstention(command: string): AbstentionDefect | null;
|
|
67
|
+
/**
|
|
68
|
+
* The opening line of the preflight note: **what this is, and what it is not.**
|
|
69
|
+
*
|
|
70
|
+
* It says the finding is about OUR PARSER rather than about the command, and that nothing has been
|
|
71
|
+
* judged. Both halves are load-bearing. The first stops the rater reading the note as a report of
|
|
72
|
+
* something found — the failure mode a bare observation produced, where a rater treats our
|
|
73
|
+
* uncertainty as evidence and either inflates or explains it away. The second is what keeps this
|
|
74
|
+
* note out of the open-world note's register: that one may say the command *"has ALREADY been
|
|
75
|
+
* floored deterministically and will be shown to the user whatever you return"*, and here that
|
|
76
|
+
* sentence would simply be false.
|
|
77
|
+
*/
|
|
78
|
+
export declare const PARSER_NOTE_PREAMBLE: string;
|
|
79
|
+
/**
|
|
80
|
+
* Build the neutral preflight note for a command, or `null` when the gate resolved it.
|
|
81
|
+
*
|
|
82
|
+
* One note per mechanism the command carries, most specific first, bulleted when there is more than
|
|
83
|
+
* one — a command can compose AND substitute AND redirect, and a note that mentioned only the first
|
|
84
|
+
* would leave the rater reasoning about a fragment of the shape.
|
|
85
|
+
*
|
|
86
|
+
* @param command The raw command string as the model proposed it.
|
|
87
|
+
*/
|
|
88
|
+
export declare function buildParserPreflightNote(command: string): string | null;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/shell/abstention
|
|
3
|
+
*
|
|
4
|
+
* EXT-81 — **what the RATER is told about a command our parser could not resolve.**
|
|
5
|
+
*
|
|
6
|
+
* `classifyCommand` resolves a command's target, or returns `null` when it cannot. That `null` used
|
|
7
|
+
* to be an ACTION: the gate refused the call before any rating, on the theory that a reading of a
|
|
8
|
+
* command it could not parse was not worth buying. §6.1's rule reverses that — *deterministic checks
|
|
9
|
+
* fire only where we are confident something is a threat; where we cannot tell, the model decides* —
|
|
10
|
+
* and a parser reporting that it could not resolve a string is not a detection of anything. So the
|
|
11
|
+
* finding is now **neutral context handed to the rater**, and the flow is one path: preflight → note
|
|
12
|
+
* → rate → approve / escalate / halt.
|
|
13
|
+
*
|
|
14
|
+
* This module turns the `null` into that note: **which shape the parser saw, and the mechanism that
|
|
15
|
+
* makes the shape worth a second look.** Two properties are the whole design, and each has a
|
|
16
|
+
* measurement behind it ([[QA-17]], `docs/test-sessions/qa-17-substitution-note-2026-08-03/`):
|
|
17
|
+
*
|
|
18
|
+
* - **The note carries the MECHANISM, not just the construct's name.** A bare *"the parser saw a
|
|
19
|
+
* substitution"* observation was measured NEGATIVE on two raters — one became more confidently
|
|
20
|
+
* wrong than with no note at all (*"not executed since it's within double quotes"*), another lost
|
|
21
|
+
* severity against the control. Adding one sentence of mechanism — that a double-quoted argument
|
|
22
|
+
* is not inert prose, because the shell expands it before the outer program runs — is what moved
|
|
23
|
+
* every deployment-class rater to the correct verdict. A bare observation from a component that
|
|
24
|
+
* has just announced it could not read the command reads as DOUBT, and doubt is not information.
|
|
25
|
+
* - **The note is ASSISTANCE, never an accusation.** *"Hey rater, here is a command, please pay
|
|
26
|
+
* attention it includes composition."* No verdict, no severity, no *suspicious*, no *careful*, no
|
|
27
|
+
* *treat this as at least X*, and nothing that caps the outcome — `catastrophic` and `attack` stay
|
|
28
|
+
* reachable. A question is allowed; a question is not a lean. A composition is not a hazard:
|
|
29
|
+
* `cd build && ls`, `npm test && npm run build` and `git add -A && git status` are the
|
|
30
|
+
* overwhelming majority of what this fires on, and a leading note would re-create the very
|
|
31
|
+
* escalation this design removes — laundered through the model instead of the parser, and now
|
|
32
|
+
* unfalsifiable because a model said it.
|
|
33
|
+
*
|
|
34
|
+
* It also states nothing about what the gate will DO with the verdict. The open-world note's shape
|
|
35
|
+
* (*"has ALREADY been floored deterministically and will be shown to the user whatever you
|
|
36
|
+
* return"*) is honest only where a floor exists, and for these families none does.
|
|
37
|
+
*
|
|
38
|
+
* **Scope, deliberately narrow.** Flag what the parser saw and stop. Extracting spans, naming which
|
|
39
|
+
* segment is unresolvable, and drawing on surrounding context all wait for real user trial — the
|
|
40
|
+
* elaborate note is the one most likely to acquire connotation by accident.
|
|
41
|
+
*/
|
|
42
|
+
import { classifyCommand } from '#src/core/shell/arity.js';
|
|
43
|
+
import { COMMAND_SEPARATOR_RE, LINE_BREAK_RE, normalizeCommand, } from '#src/core/shell/normalize.js';
|
|
44
|
+
/**
|
|
45
|
+
* **The per-family note text — one distinct string per {@link AbstentionMechanism}.**
|
|
46
|
+
*
|
|
47
|
+
* Typed as a total `Record`, so adding a mechanism is a COMPILE ERROR until someone writes its
|
|
48
|
+
* sentence. A family that silently fell back to another family's note would be the failure this
|
|
49
|
+
* table exists to prevent: [[QA-17]] measured that the note's effect is family-specific, and
|
|
50
|
+
* `docs/test-sessions/qa-17-substitution-note-2026-08-03/` records a narrowing by family name that
|
|
51
|
+
* was FALSIFIED — `gh pr comment 42 --body "…"` carrying an executing substitution classifies as
|
|
52
|
+
* generic `substitution`, and three of three hosted raters called it safe unassisted.
|
|
53
|
+
*
|
|
54
|
+
* Every entry states a fact about what THE SHELL does, and then asks a question. Neither is a
|
|
55
|
+
* verdict about the command in front of the rater, which is what keeps the note neutral while still
|
|
56
|
+
* carrying the mechanism that the measurement showed is load-bearing.
|
|
57
|
+
*/
|
|
58
|
+
export const MECHANISM_NOTES = {
|
|
59
|
+
'commit-message-substitution': 'This is a git commit carrying its message inline, and the message contains a ' +
|
|
60
|
+
'dollar-parenthesis or a backtick. The SHELL expands that before git ever sees the message, ' +
|
|
61
|
+
'so the text inside the quotes is not inert prose: double quotes do not stop `$(…)` or a ' +
|
|
62
|
+
'backtick from running, and git receives whatever it produced. What does the shell run when ' +
|
|
63
|
+
'it expands that message?',
|
|
64
|
+
composition: 'This command line runs MORE THAN ONE command — a `;`, `&&`, `||`, `|` or a line break ' +
|
|
65
|
+
'separates them — and the shell runs each part in turn, feeding one into the next where the ' +
|
|
66
|
+
'separator is a pipe. What does the whole line do once every part has run?',
|
|
67
|
+
substitution: 'This command line contains a substitution — `$(…)`, a backtick, `${…}` or `<(…)`. The SHELL ' +
|
|
68
|
+
'expands it BEFORE the outer program runs, and hands that program the result. A ' +
|
|
69
|
+
'double-quoted argument is therefore not inert prose: double quotes do not stop `$(…)` or a ' +
|
|
70
|
+
'backtick from being run, so a substitution inside what reads as ordinary text is still ' +
|
|
71
|
+
'executed. What does the shell run when it expands this one?',
|
|
72
|
+
redirect: 'This command line redirects a stream — `>`, `>>`, `<`, `2>` or `&>`. The shell attaches a ' +
|
|
73
|
+
'file to the program before the program starts, so its output lands in that file rather than ' +
|
|
74
|
+
'on the terminal, and a `>` truncates the file it names first. Which file is being read or ' +
|
|
75
|
+
'written here?',
|
|
76
|
+
unparseable: 'The gate could not tokenize this command line at all — most often an unbalanced quote, or no ' +
|
|
77
|
+
'program name it could identify. So we cannot tell you which program this runs; what is ' +
|
|
78
|
+
'quoted above is exactly the text that would be handed to the shell. What does it do?',
|
|
79
|
+
};
|
|
80
|
+
/** Process substitution `<(…)` / `>(…)`, which is a SUBSTITUTION and must not also read as a redirect. */
|
|
81
|
+
const PROCESS_SUBSTITUTION_RE = /[<>]\(/g;
|
|
82
|
+
/**
|
|
83
|
+
* Does this look like a `git commit` carrying an inline message?
|
|
84
|
+
*
|
|
85
|
+
* Deliberately a heuristic and not a parser: the command has already defeated the gate's tokenizer,
|
|
86
|
+
* so there is nothing precise left to parse. It is safe to be approximate here because a wrong
|
|
87
|
+
* answer costs at most a less specific note — the rating is bought either way.
|
|
88
|
+
*/
|
|
89
|
+
const GIT_COMMIT_RE = /\bgit\b[^\n]*\bcommit\b/i;
|
|
90
|
+
const INLINE_MESSAGE_FLAG_RE = /(^|\s)(-m\b|--message\b|-\w*m\b)/;
|
|
91
|
+
/** Command substitution `$(…)` or a backtick — the two the shell EXECUTES. */
|
|
92
|
+
const EXECUTING_SUBSTITUTION_RE = /\$\(|`/;
|
|
93
|
+
/** Every substitution/expansion form, including the non-executing ones. */
|
|
94
|
+
const ANY_SUBSTITUTION_RE = /\$\(|`|\$\{|[<>]\(/;
|
|
95
|
+
/**
|
|
96
|
+
* Classify a command the gate's parser could not resolve, or return `null` when it resolves.
|
|
97
|
+
*
|
|
98
|
+
* **The `null` arm is the guard, and it is structural on purpose.** Every mechanism below is a
|
|
99
|
+
* regex, and `unparseable` is the fallback — so without this gate the function would classify `ls
|
|
100
|
+
* -la` as `unparseable` and a note would be attached to every command in the session. The predicate
|
|
101
|
+
* is {@link classifyCommand}'s own, not a second reading of it, so the note appears on exactly the
|
|
102
|
+
* set of commands the gate could not resolve and on no others.
|
|
103
|
+
*
|
|
104
|
+
* Detection runs on the NORMALIZED command (the same form `classifyCommand` refused), so obfuscation
|
|
105
|
+
* that the normalizer collapses cannot hide a mechanism from the note — while the command shown to
|
|
106
|
+
* the rater stays the one the rater is being asked about, fenced by its own caller.
|
|
107
|
+
*
|
|
108
|
+
* The line-break check reads the RAW command, exactly as `classifyCommand` does: the normalizer is
|
|
109
|
+
* not required to preserve that separator, and EXT-55 is the node about what happens when a caller
|
|
110
|
+
* assumes it did.
|
|
111
|
+
*
|
|
112
|
+
* **Order is note order, most specific first**, and `commit-message-substitution` SUPPRESSES the
|
|
113
|
+
* generic substitution note it is a special case of — telling the rater the same thing twice about
|
|
114
|
+
* one `$(…)` spends attention on a repetition.
|
|
115
|
+
*
|
|
116
|
+
* @param command The raw command string as the model proposed it.
|
|
117
|
+
* @returns What the parser saw, or `null` when the command's target statically resolves.
|
|
118
|
+
*/
|
|
119
|
+
export function describeAbstention(command) {
|
|
120
|
+
if (classifyCommand(command, normalizeCommand) !== null)
|
|
121
|
+
return null;
|
|
122
|
+
const normalized = normalizeCommand(command);
|
|
123
|
+
const composes = COMMAND_SEPARATOR_RE.test(normalized) || LINE_BREAK_RE.test(command.trim());
|
|
124
|
+
const substitutes = ANY_SUBSTITUTION_RE.test(normalized);
|
|
125
|
+
const commitSubstitution = GIT_COMMIT_RE.test(normalized) &&
|
|
126
|
+
INLINE_MESSAGE_FLAG_RE.test(normalized) &&
|
|
127
|
+
EXECUTING_SUBSTITUTION_RE.test(normalized);
|
|
128
|
+
// A redirect is any `<`/`>` that is NOT the opening of a process substitution — that form is
|
|
129
|
+
// already reported as a substitution, and reporting it twice would describe one piece of syntax
|
|
130
|
+
// with two different mechanisms.
|
|
131
|
+
const redirects = /[<>]/.test(normalized.replace(PROCESS_SUBSTITUTION_RE, ''));
|
|
132
|
+
const mechanisms = [];
|
|
133
|
+
if (commitSubstitution)
|
|
134
|
+
mechanisms.push('commit-message-substitution');
|
|
135
|
+
if (composes)
|
|
136
|
+
mechanisms.push('composition');
|
|
137
|
+
if (substitutes && !commitSubstitution)
|
|
138
|
+
mechanisms.push('substitution');
|
|
139
|
+
if (redirects)
|
|
140
|
+
mechanisms.push('redirect');
|
|
141
|
+
// The fallback for the causes that are not a shape at all — an unbalanced quote, an empty
|
|
142
|
+
// command, a command with no resolvable program name — so a note is never empty.
|
|
143
|
+
if (mechanisms.length === 0)
|
|
144
|
+
mechanisms.push('unparseable');
|
|
145
|
+
return {
|
|
146
|
+
mechanism: mechanisms[0],
|
|
147
|
+
mechanisms,
|
|
148
|
+
notes: mechanisms.map((mechanism) => MECHANISM_NOTES[mechanism]),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The opening line of the preflight note: **what this is, and what it is not.**
|
|
153
|
+
*
|
|
154
|
+
* It says the finding is about OUR PARSER rather than about the command, and that nothing has been
|
|
155
|
+
* judged. Both halves are load-bearing. The first stops the rater reading the note as a report of
|
|
156
|
+
* something found — the failure mode a bare observation produced, where a rater treats our
|
|
157
|
+
* uncertainty as evidence and either inflates or explains it away. The second is what keeps this
|
|
158
|
+
* note out of the open-world note's register: that one may say the command *"has ALREADY been
|
|
159
|
+
* floored deterministically and will be shown to the user whatever you return"*, and here that
|
|
160
|
+
* sentence would simply be false.
|
|
161
|
+
*/
|
|
162
|
+
export const PARSER_NOTE_PREAMBLE = 'PREFLIGHT NOTE: our command parser could not resolve what this command line runs, so here is ' +
|
|
163
|
+
'what it did see. This is a fact about OUR PARSER and not a finding about the command — nothing ' +
|
|
164
|
+
'has been judged, nothing has been floored, and none of this is a verdict or a severity. Rate ' +
|
|
165
|
+
'the command on its own merits.';
|
|
166
|
+
/**
|
|
167
|
+
* Build the neutral preflight note for a command, or `null` when the gate resolved it.
|
|
168
|
+
*
|
|
169
|
+
* One note per mechanism the command carries, most specific first, bulleted when there is more than
|
|
170
|
+
* one — a command can compose AND substitute AND redirect, and a note that mentioned only the first
|
|
171
|
+
* would leave the rater reasoning about a fragment of the shape.
|
|
172
|
+
*
|
|
173
|
+
* @param command The raw command string as the model proposed it.
|
|
174
|
+
*/
|
|
175
|
+
export function buildParserPreflightNote(command) {
|
|
176
|
+
const defect = describeAbstention(command);
|
|
177
|
+
if (defect === null)
|
|
178
|
+
return null;
|
|
179
|
+
const body = defect.notes.length === 1
|
|
180
|
+
? defect.notes[0]
|
|
181
|
+
: defect.notes.map((note) => `- ${note}`).join('\n');
|
|
182
|
+
return `${PARSER_NOTE_PREAMBLE}\n${body}`;
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=abstention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abstention.js","sourceRoot":"","sources":["../../../src/core/shell/abstention.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/shell/approvalCapture
|
|
3
|
+
*
|
|
4
|
+
* [[TUI-C27]] — **what the approvals gate DID, recorded while it was doing it**, for the
|
|
5
|
+
* `/debug-dump` archive.
|
|
6
|
+
*
|
|
7
|
+
* ## The two gaps this exists to close
|
|
8
|
+
*
|
|
9
|
+
* 1. **An approval used to leave no trace at all.** A rejection is legible in a dump only as a
|
|
10
|
+
* byproduct of the negotiation — the protocol has to hand the reason back to the agent, so it
|
|
11
|
+
* lands in the transcript. An approval relays nothing to anyone: the tool simply runs. So the
|
|
12
|
+
* archive was at its most detailed about the decisions that STOPPED something and silent about
|
|
13
|
+
* the one decision that let something happen, which is exactly backwards for an incident review.
|
|
14
|
+
* Everything here is written on both branches.
|
|
15
|
+
* 2. **Every field anyone had was a rater OUTPUT; its INPUT was never recorded.** Rung, outcome,
|
|
16
|
+
* reason, which preflight, whether the floor fired — all of them describe what the rater
|
|
17
|
+
* *answered*, and none records what it was *shown*. The question that actually failed in the
|
|
18
|
+
* field — *"were the user's messages in view on round 2?"* — needs the input of an approving
|
|
19
|
+
* round, the one combination the dump had neither half of.
|
|
20
|
+
*
|
|
21
|
+
* ## The one rule that shapes the whole module: CAPTURE, NEVER RE-RENDER
|
|
22
|
+
*
|
|
23
|
+
* {@link RaterCallCapture.prompt} holds the exact `{system, user}` strings handed to the model, taken
|
|
24
|
+
* inside `rateShellCommand` at the send site — not rebuilt afterwards from the state that produced
|
|
25
|
+
* them. A dump that can disagree with what actually happened is worse than no dump: it invites a
|
|
26
|
+
* confident wrong conclusion from the only evidence there is, which is the failure mode this node
|
|
27
|
+
* was filed over. The same rule is why {@link RaterCallCapture.rawResponse} keeps the model's answer
|
|
28
|
+
* *before* it is mapped to an outcome — a malformed or surprising answer stays visible rather than
|
|
29
|
+
* being smoothed into a verdict.
|
|
30
|
+
*
|
|
31
|
+
* ## The records are pushed EARLY and mutated in place, deliberately
|
|
32
|
+
*
|
|
33
|
+
* {@link ApprovalCaptureLog.begin} appends the record the moment a gated call arrives, and the gate
|
|
34
|
+
* fills fields in as they are decided. The alternative — assemble the whole record, push it at the
|
|
35
|
+
* end — loses exactly the events worth keeping: an `attack` verdict throws `AttackHaltError` out of
|
|
36
|
+
* the decision, so a run that halted would carry no record of the rating that halted it. Pushing
|
|
37
|
+
* first makes survival a property of the structure rather than of someone remembering to write the
|
|
38
|
+
* record before each `throw`.
|
|
39
|
+
*
|
|
40
|
+
* ## Redaction
|
|
41
|
+
*
|
|
42
|
+
* Nothing is redacted here. The archive writer routes this artifact through the SAME
|
|
43
|
+
* [[GS2-47]]/[[GS2-54]] pass as `transcript.json` and `model-messages.json`
|
|
44
|
+
* (`renderStructured` → `redactText`, over the literal secret values `collectSecretValues`
|
|
45
|
+
* harvested from env + config). Redacting twice, in two places, is how two policies come to exist.
|
|
46
|
+
*/
|
|
47
|
+
import type { ApprovalRung } from '#src/config.js';
|
|
48
|
+
import type { ToolApprovalScope } from '#src/core/types.js';
|
|
49
|
+
import type { AbstentionDefect } from '#src/core/shell/abstention.js';
|
|
50
|
+
import type { NegotiationCounters } from '#src/core/shell/negotiation.js';
|
|
51
|
+
import type { FailClosedCause, PreflightFloorKind, RaterNegotiationContext, RaterNegotiationRound, ShellSafetyVerdict } from '#src/core/shell/rater.js';
|
|
52
|
+
/**
|
|
53
|
+
* How many gated decisions one session keeps. A ring buffer for the same reason the debug-log one
|
|
54
|
+
* is: each record carries a full rating prompt (a few KB), and a long `auto` session makes a lot of
|
|
55
|
+
* them. The newest are the ones a bug report is about, so the oldest are evicted.
|
|
56
|
+
*/
|
|
57
|
+
export declare const APPROVAL_CAPTURE_MAX = 50;
|
|
58
|
+
/**
|
|
59
|
+
* **Which layer of the gate decided this call.** The diagnostic value of the whole record is
|
|
60
|
+
* precisely here: a bug report that says "escalated" does not distinguish a rater verdict from a
|
|
61
|
+
* floor match from a timeout from a deny-list hit, and those need four different answers.
|
|
62
|
+
*
|
|
63
|
+
* The names follow the numbered steps of `GthAgentRunner.decideToolApprovalInner`:
|
|
64
|
+
*
|
|
65
|
+
* - `not-gated` — the rung in force does not gate this tool at all.
|
|
66
|
+
* - `deny-list` — a declared `approvals.deny` entry, or an *always reject* the human chose earlier.
|
|
67
|
+
* - `bypass` — the gate is off for the session.
|
|
68
|
+
* - `hardline-floor` — §8's deterministic floor matched, before any rating or prompt.
|
|
69
|
+
* - `escalate-entry` — a declared `approvals.escalate` entry; a human answers, with no rating call.
|
|
70
|
+
* - `allow-list` — an allow entry settled it with no rating (`rate: false`, or an unrated rung).
|
|
71
|
+
* - `allow-tripwire` — an allow entry that kept the rater involved (§3.2's `rate: true`).
|
|
72
|
+
* - `rater` — the ordinary rating path.
|
|
73
|
+
* - `tool-open-world-floor` — §4.7.3's floor on a non-shell call whose effective `openWorldHint`
|
|
74
|
+
* is true. No rater sees it while §4.3's scope boundary stands.
|
|
75
|
+
* - `unrated-rung` — nothing but the rung itself: `manual` and `write` consult no model, so a call
|
|
76
|
+
* no rule claimed goes to the human on the rung's say-so. It is its own value rather than an
|
|
77
|
+
* absent one, because "the rung requires a person" is an answer and a blank field reads as the
|
|
78
|
+
* recorder having failed.
|
|
79
|
+
*/
|
|
80
|
+
export type ApprovalDecidingStage = 'not-gated' | 'deny-list' | 'bypass' | 'hardline-floor' | 'escalate-entry' | 'allow-list' | 'allow-tripwire' | 'rater' | 'tool-open-world-floor' | 'unrated-rung';
|
|
81
|
+
/**
|
|
82
|
+
* What became of the call, as the agent experienced it. `error` is its own value rather than an
|
|
83
|
+
* absence: a decision that threw something other than a halt is a fact about the gate, and a record
|
|
84
|
+
* left with no action at all would read as one that never finished being written.
|
|
85
|
+
*/
|
|
86
|
+
export type ApprovalCaptureAction = 'approve' | 'reject' | 'escalate' | 'halt' | 'error';
|
|
87
|
+
/** How an escalation ended once it reached (or failed to reach) a person. */
|
|
88
|
+
export type ApprovalHumanAnswer = 'approve' | 'reject' | 'no-human';
|
|
89
|
+
/**
|
|
90
|
+
* §5.1's negotiation context **as it was handed to the prompt builder**, plus the one distinction a
|
|
91
|
+
* reader of the archive keeps needing and could never make.
|
|
92
|
+
*
|
|
93
|
+
* **`userMessagesPopulated` is the field the node exists for.** Round 1 is context-free *by design*
|
|
94
|
+
* (§5.1: *"round 1 sees the command alone"*), so an empty window is not a bug — and a reader with no
|
|
95
|
+
* access to the source cannot know that. {@link userMessagesNote} states which case this is in
|
|
96
|
+
* words, so the answer to *"were the user's messages in view on round N?"* is one field and one
|
|
97
|
+
* sentence rather than a source-reading exercise across three files.
|
|
98
|
+
*/
|
|
99
|
+
export interface RaterNegotiationCapture {
|
|
100
|
+
/** 1-based index of this rating within the current negotiation. */
|
|
101
|
+
round: number;
|
|
102
|
+
/** Whether this rating was a round-1 (context-free) rating. */
|
|
103
|
+
roundOne: boolean;
|
|
104
|
+
/** Whether a §5.1 context was supplied at all (it is not, at a rung that does not negotiate). */
|
|
105
|
+
contextSupplied: boolean;
|
|
106
|
+
/** The justification admitted into THIS rating. Withheld at round 1 by `contextFor`, by design. */
|
|
107
|
+
justification?: string;
|
|
108
|
+
/** `<negotiation_so_far>` as handed over: the completed rounds, oldest first. */
|
|
109
|
+
priorRounds: RaterNegotiationRound[];
|
|
110
|
+
/** `<user_messages>` as handed over, oldest first, before the builder's last-5 + truncation. */
|
|
111
|
+
userMessages: string[];
|
|
112
|
+
/** Whether that window carried anything. THE distinction this facility turns on. */
|
|
113
|
+
userMessagesPopulated: boolean;
|
|
114
|
+
/** Why the window looks the way it does, in words, for a reader who has never seen the spec. */
|
|
115
|
+
userMessagesNote: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* One rating call: what the rater was SHOWN and what it ANSWERED, captured at the send site.
|
|
119
|
+
*
|
|
120
|
+
* Every field is filled by `rateShellCommand` itself. `prompt` is set before the call and the rest
|
|
121
|
+
* as the answer arrives, so a rater that never answers still leaves a record of what it was asked.
|
|
122
|
+
*/
|
|
123
|
+
export interface RaterCallCapture {
|
|
124
|
+
/** ISO timestamp of the moment the call was sent. */
|
|
125
|
+
at: string;
|
|
126
|
+
/** Wall-clock ms the call took, once it has returned (or timed out). */
|
|
127
|
+
durationMs?: number;
|
|
128
|
+
/** The rater model's own label — an id and a provider type, never the instance. */
|
|
129
|
+
model?: string;
|
|
130
|
+
/** `approvals.rater` — the identity profile the rater model came from, when one is configured. */
|
|
131
|
+
profile?: string;
|
|
132
|
+
/** The budget this call was raced against ([[EXT-66]]). */
|
|
133
|
+
timeoutMs: number;
|
|
134
|
+
/** §5.2 — whether a rejection would be handed back to the AGENT rather than to a person. */
|
|
135
|
+
negotiable: boolean;
|
|
136
|
+
/** **The exact strings sent.** Captured, never re-rendered. */
|
|
137
|
+
prompt: {
|
|
138
|
+
system: string;
|
|
139
|
+
user: string;
|
|
140
|
+
};
|
|
141
|
+
/** What the negotiation looked like at the moment of this call. */
|
|
142
|
+
negotiation: RaterNegotiationCapture;
|
|
143
|
+
/** The model's answer BEFORE it is parsed or mapped, so a malformed one stays visible. */
|
|
144
|
+
rawResponse?: unknown;
|
|
145
|
+
/** The verdict this call resolved to, including a fail-closed one. */
|
|
146
|
+
verdict?: ShellSafetyVerdict;
|
|
147
|
+
/** Set when the verdict is the gate failing closed rather than the model judging ([[EXT-66]]). */
|
|
148
|
+
failClosed?: FailClosedCause;
|
|
149
|
+
}
|
|
150
|
+
/** A declared entry or runtime grant that decided the call, rendered as the user would read it. */
|
|
151
|
+
export interface ApprovalRuleMatchCapture {
|
|
152
|
+
action: 'allow' | 'deny' | 'escalate';
|
|
153
|
+
/** The entry in the words the menu and the notices use (`describeApprovalEntry`). */
|
|
154
|
+
entry: string;
|
|
155
|
+
/** §3.2 — whether an allow entry kept the rater involved as a tripwire. */
|
|
156
|
+
rate?: boolean;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* §8's deterministic floor, when it matched.
|
|
160
|
+
*
|
|
161
|
+
* **This block NAMES THE MATCHED PATTERN, and that is a decision taken rather than an oversight.**
|
|
162
|
+
* §8.1 says the floor is never advertised, and [[CFG-31]] binds user-facing rung copy to that rule.
|
|
163
|
+
* The resolution taken for [[TUI-C27]] is that §8.1 governs **rung descriptions and promotional
|
|
164
|
+
* copy** — text that invites a user to feel safe — and not a diagnostic archive a user opens about
|
|
165
|
+
* their own session. "A floor matched" without saying which one leaves nobody able to act on it;
|
|
166
|
+
* the refusal the *user* sees ({@link import('./hardline.js').buildHardlineRefusal}) is unchanged
|
|
167
|
+
* and still names only the description.
|
|
168
|
+
*/
|
|
169
|
+
export interface HardlineFloorCapture {
|
|
170
|
+
/** The human-readable description, as the refusal message carries it. */
|
|
171
|
+
description: string;
|
|
172
|
+
/** The matched pattern's source, or the stable token of the non-pattern arm. */
|
|
173
|
+
pattern: string;
|
|
174
|
+
}
|
|
175
|
+
/** A deterministic preflight finding, and whether it actually rewrote the rater's outcome. */
|
|
176
|
+
export interface PreflightFloorCapture {
|
|
177
|
+
kind: PreflightFloorKind;
|
|
178
|
+
reason: string;
|
|
179
|
+
/**
|
|
180
|
+
* Whether the floor changed the outcome. A preflight only ever RAISES, and only `safe` sits below
|
|
181
|
+
* the floor — so a finding on a `destructive` verdict is the floor AGREEING with the rater, not
|
|
182
|
+
* overriding it, and reporting those two the same way would misattribute the decision.
|
|
183
|
+
*/
|
|
184
|
+
rewroteRating: boolean;
|
|
185
|
+
}
|
|
186
|
+
/** One gated tool call, from arrival to outcome. */
|
|
187
|
+
export interface ApprovalDecisionCapture {
|
|
188
|
+
/** ISO timestamp of the moment the call arrived at the gate. */
|
|
189
|
+
at: string;
|
|
190
|
+
/** The tool that was called. */
|
|
191
|
+
tool: string;
|
|
192
|
+
/** The command, for a `run_shell_command` call whose argument was a readable string. */
|
|
193
|
+
command?: string;
|
|
194
|
+
/** The rung in force for THIS decision (`/approvals <rung>` moves it mid-session). */
|
|
195
|
+
rung: ApprovalRung;
|
|
196
|
+
/** The stage that decided. `undefined` only if the decision threw before reaching one. */
|
|
197
|
+
stage?: ApprovalDecidingStage;
|
|
198
|
+
/** What became of the call. Always set by the time the decision returns or throws. */
|
|
199
|
+
action?: ApprovalCaptureAction;
|
|
200
|
+
/** The scope an approval was granted at, when one was. */
|
|
201
|
+
scope?: ToolApprovalScope;
|
|
202
|
+
/** Whether a person was actually asked, and what they said. */
|
|
203
|
+
humanAnswer?: ApprovalHumanAnswer;
|
|
204
|
+
/** §8's floor, when it matched. */
|
|
205
|
+
hardline?: HardlineFloorCapture;
|
|
206
|
+
/** The declared entry or runtime grant that decided the call, when one did. */
|
|
207
|
+
ruleMatch?: ApprovalRuleMatchCapture;
|
|
208
|
+
/** The deterministic preflight finding on this command, when there was one. */
|
|
209
|
+
preflight?: PreflightFloorCapture;
|
|
210
|
+
/**
|
|
211
|
+
* [[EXT-81]] — whether the gate's own parser could not statically resolve the command, so the
|
|
212
|
+
* rating carried a neutral note about the shape it saw.
|
|
213
|
+
*
|
|
214
|
+
* **This is what remains of "was the call an ABSTAIN".** `abstain` was an ACTION of its own until
|
|
215
|
+
* [[EXT-81]] retired it: a parser reporting that it could not read a string has detected nothing,
|
|
216
|
+
* so it no longer earns an action, and the command is now rated like any other. The observable
|
|
217
|
+
* that survived is this shape report, and the budget position that used to accompany it is
|
|
218
|
+
* {@link budget}.
|
|
219
|
+
*/
|
|
220
|
+
parserUnresolved?: AbstentionDefect;
|
|
221
|
+
/** Where this call sat in §5.3's consecutive bound and the reachability bound, on arrival. */
|
|
222
|
+
budget: NegotiationCounters;
|
|
223
|
+
/** The rating, when one was made. Absent for every stage that decided without a model. */
|
|
224
|
+
rating?: RaterCallCapture;
|
|
225
|
+
/** The error that ended the decision, when one did. */
|
|
226
|
+
error?: string;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The per-session log of gated decisions.
|
|
230
|
+
*
|
|
231
|
+
* **Instance-scoped, on the runner, and never a module singleton** — the same reason
|
|
232
|
+
* `ShellNegotiationState` and the grant stores are: a concurrent ACP / AG-UI session must not
|
|
233
|
+
* inherit another session's approvals history, and a dump taken in one must not describe the other.
|
|
234
|
+
*/
|
|
235
|
+
export declare class ApprovalCaptureLog {
|
|
236
|
+
private records;
|
|
237
|
+
/**
|
|
238
|
+
* Open a record for a gated call and return it LIVE, already in the buffer.
|
|
239
|
+
*
|
|
240
|
+
* The caller mutates the returned object as the decision is made. See the module docblock for why
|
|
241
|
+
* the push happens here rather than at the end: a halt throws out of the decision, and a record
|
|
242
|
+
* assembled at the end would be lost on exactly the calls most worth keeping.
|
|
243
|
+
*/
|
|
244
|
+
begin(record: ApprovalDecisionCapture): ApprovalDecisionCapture;
|
|
245
|
+
/**
|
|
246
|
+
* The records so far, oldest first. A copy of the ARRAY: the records themselves are handed over
|
|
247
|
+
* live, because a decision still in flight is one the archive should show as it stands rather
|
|
248
|
+
* than not at all.
|
|
249
|
+
*/
|
|
250
|
+
snapshot(): ApprovalDecisionCapture[];
|
|
251
|
+
/** Drop everything — the TUI's `/clear` rotates the thread. */
|
|
252
|
+
clear(): void;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* A short, KEY-FREE label for the rater model: its id and its provider type, and nothing else.
|
|
256
|
+
*
|
|
257
|
+
* Deliberately not a param dump. A live `BaseChatModel` carries an `apiKey` and a client instance,
|
|
258
|
+
* and this string goes into an archive people attach to bug reports — so it reads a fixed handful of
|
|
259
|
+
* scalar fields through guards, exactly as `debugDump`'s own live-model descriptor does, rather than
|
|
260
|
+
* serialising anything of the instance.
|
|
261
|
+
*/
|
|
262
|
+
export declare function raterModelLabel(model: unknown): string | undefined;
|
|
263
|
+
/**
|
|
264
|
+
* Describe the §5.1 context a rating is about to be made with — **as it was handed to the prompt
|
|
265
|
+
* builder**, so the record and the prompt cannot come to describe two different ratings.
|
|
266
|
+
*
|
|
267
|
+
* The note is the legibility half of the acceptance: an empty user-messages window means one thing
|
|
268
|
+
* at round 1 (by design) and something else entirely at round 3, and a reader of the archive has no
|
|
269
|
+
* way to tell them apart from a `[]`.
|
|
270
|
+
*/
|
|
271
|
+
export declare function describeRaterNegotiation(negotiation: RaterNegotiationContext | undefined): RaterNegotiationCapture;
|