@gaunt-sloth/core 2.0.0-alpha.1 → 2.0.0-alpha.10

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 (114) hide show
  1. package/.gsloth.code.md +10 -0
  2. package/LICENSE +7 -0
  3. package/README.md +3 -4
  4. package/dist/config/defaults.d.ts +85 -0
  5. package/dist/config/defaults.js +101 -0
  6. package/dist/config/defaults.js.map +1 -0
  7. package/dist/config/jsonc.d.ts +12 -0
  8. package/dist/config/jsonc.js +41 -0
  9. package/dist/config/jsonc.js.map +1 -0
  10. package/dist/config/loader.d.ts +109 -0
  11. package/dist/config/loader.js +663 -0
  12. package/dist/config/loader.js.map +1 -0
  13. package/dist/config/schema.d.ts +509 -0
  14. package/dist/config/schema.js +349 -0
  15. package/dist/config/schema.js.map +1 -0
  16. package/dist/config/shell-policy.d.ts +212 -0
  17. package/dist/config/shell-policy.js +142 -0
  18. package/dist/config/shell-policy.js.map +1 -0
  19. package/dist/config/types.d.ts +490 -0
  20. package/dist/config/types.js +12 -0
  21. package/dist/config/types.js.map +1 -0
  22. package/dist/config.d.ts +18 -647
  23. package/dist/config.js +15 -516
  24. package/dist/config.js.map +1 -1
  25. package/dist/constants.d.ts +24 -0
  26. package/dist/constants.js +24 -0
  27. package/dist/constants.js.map +1 -1
  28. package/dist/core/GthAbstractAgent.d.ts +57 -1
  29. package/dist/core/GthAbstractAgent.js +171 -4
  30. package/dist/core/GthAbstractAgent.js.map +1 -1
  31. package/dist/core/GthAgentRunner.d.ts +158 -1
  32. package/dist/core/GthAgentRunner.js +367 -4
  33. package/dist/core/GthAgentRunner.js.map +1 -1
  34. package/dist/core/GthLangChainAgent.js +94 -5
  35. package/dist/core/GthLangChainAgent.js.map +1 -1
  36. package/dist/core/debugCapture.d.ts +59 -0
  37. package/dist/core/debugCapture.js +101 -0
  38. package/dist/core/debugCapture.js.map +1 -0
  39. package/dist/core/gthLeanAgentFactory.d.ts +9 -0
  40. package/dist/core/gthLeanAgentFactory.js +10 -0
  41. package/dist/core/gthLeanAgentFactory.js.map +1 -0
  42. package/dist/core/runStats.d.ts +41 -0
  43. package/dist/core/runStats.js +73 -0
  44. package/dist/core/runStats.js.map +1 -0
  45. package/dist/core/shell/ShellCommandFailedError.d.ts +54 -0
  46. package/dist/core/shell/ShellCommandFailedError.js +68 -0
  47. package/dist/core/shell/ShellCommandFailedError.js.map +1 -0
  48. package/dist/core/shell/allowlist.d.ts +75 -0
  49. package/dist/core/shell/allowlist.js +187 -0
  50. package/dist/core/shell/allowlist.js.map +1 -0
  51. package/dist/core/shell/arity.d.ts +75 -0
  52. package/dist/core/shell/arity.js +313 -0
  53. package/dist/core/shell/arity.js.map +1 -0
  54. package/dist/core/shell/judge.d.ts +161 -0
  55. package/dist/core/shell/judge.js +261 -0
  56. package/dist/core/shell/judge.js.map +1 -0
  57. package/dist/core/shell/normalize.d.ts +27 -0
  58. package/dist/core/shell/normalize.js +53 -0
  59. package/dist/core/shell/normalize.js.map +1 -0
  60. package/dist/core/types.d.ts +104 -0
  61. package/dist/core/types.js.map +1 -1
  62. package/dist/history/historyFormat.d.ts +19 -0
  63. package/dist/history/historyFormat.js +85 -0
  64. package/dist/history/historyFormat.js.map +1 -0
  65. package/dist/history/historyStore.d.ts +121 -0
  66. package/dist/history/historyStore.js +315 -0
  67. package/dist/history/historyStore.js.map +1 -0
  68. package/dist/history/recordSession.d.ts +28 -0
  69. package/dist/history/recordSession.js +29 -0
  70. package/dist/history/recordSession.js.map +1 -0
  71. package/dist/index.d.ts +4 -0
  72. package/dist/index.js +4 -0
  73. package/dist/index.js.map +1 -1
  74. package/dist/providers/anthropic.d.ts +1 -1
  75. package/dist/providers/anthropic.js +5 -10
  76. package/dist/providers/anthropic.js.map +1 -1
  77. package/dist/providers/deepseek.d.ts +1 -1
  78. package/dist/providers/deepseek.js +5 -10
  79. package/dist/providers/deepseek.js.map +1 -1
  80. package/dist/providers/google-genai.d.ts +1 -1
  81. package/dist/providers/google-genai.js +5 -10
  82. package/dist/providers/google-genai.js.map +1 -1
  83. package/dist/providers/groq.d.ts +1 -1
  84. package/dist/providers/groq.js +5 -10
  85. package/dist/providers/groq.js.map +1 -1
  86. package/dist/providers/modelDiscovery.d.ts +54 -1
  87. package/dist/providers/modelDiscovery.js +106 -8
  88. package/dist/providers/modelDiscovery.js.map +1 -1
  89. package/dist/providers/ollama.d.ts +1 -1
  90. package/dist/providers/ollama.js +5 -15
  91. package/dist/providers/ollama.js.map +1 -1
  92. package/dist/providers/openai.d.ts +1 -1
  93. package/dist/providers/openai.js +5 -10
  94. package/dist/providers/openai.js.map +1 -1
  95. package/dist/providers/openrouter.d.ts +1 -1
  96. package/dist/providers/openrouter.js +7 -12
  97. package/dist/providers/openrouter.js.map +1 -1
  98. package/dist/providers/vertexai.d.ts +1 -1
  99. package/dist/providers/vertexai.js +5 -10
  100. package/dist/providers/vertexai.js.map +1 -1
  101. package/dist/providers/xai.d.ts +1 -1
  102. package/dist/providers/xai.js +5 -10
  103. package/dist/providers/xai.js.map +1 -1
  104. package/dist/runtime/singleShot.d.ts +5 -2
  105. package/dist/runtime/singleShot.js +36 -3
  106. package/dist/runtime/singleShot.js.map +1 -1
  107. package/dist/utils/fileUtils.d.ts +24 -2
  108. package/dist/utils/fileUtils.js +54 -12
  109. package/dist/utils/fileUtils.js.map +1 -1
  110. package/dist/utils/systemUtils.d.ts +31 -0
  111. package/dist/utils/systemUtils.js +38 -0
  112. package/dist/utils/systemUtils.js.map +1 -1
  113. package/package.json +16 -11
  114. package/schema/gsloth-config.schema.json +1579 -0
@@ -0,0 +1,59 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+ /**
3
+ * The non-message parts of a `wrapModelCall` request that also shape a turn: the tool
4
+ * definitions, the system prompt, and the model params. Captured alongside the message
5
+ * history so the `/debug` panel can show the full picture of what was sent to the model.
6
+ *
7
+ * SECURITY: these are assembled from an explicit allowlist at the capture site
8
+ * (see {@link extractDebugRequestExtras}); the raw model instance (which can carry an
9
+ * `apiKey`) is never passed through here.
10
+ */
11
+ export interface DebugRequestExtras {
12
+ /** The composed system prompt string sent to the model for this call. */
13
+ systemPrompt?: string;
14
+ /** The tool definitions made available for this call (name + description + JSON schema). */
15
+ tools?: DebugToolDef[];
16
+ /** Scalar model params (model id, temperature, …) — a key-free allowlist. */
17
+ modelParams?: Record<string, unknown>;
18
+ /** The tool-choice configuration for this call, if any. */
19
+ toolChoice?: unknown;
20
+ }
21
+ /** A single tool's debug-renderable definition. */
22
+ export interface DebugToolDef {
23
+ name: string;
24
+ description?: string;
25
+ /** The tool's JSON-schema parameters, when resolvable. */
26
+ schema?: unknown;
27
+ }
28
+ /**
29
+ * Debug-capture sink for the TUI's `/debug` panel.
30
+ *
31
+ * A {@link DebugCapture} is an OPT-IN callback the TUI sets on a live agent after `init`
32
+ * (on the shared {@link import('#src/core/GthAbstractAgent.js').GthAbstractAgent} base, so
33
+ * both the lean and deep backends support it). When present, the agent's `wrapModelCall`
34
+ * middleware reports, per model call:
35
+ *
36
+ * - `request` — `request.messages: BaseMessage[]`, the real history sent to the model at
37
+ * call time (post-summarization / post-middleware), plus the {@link DebugRequestExtras}
38
+ * (tools, system prompt, model params) that also shape the turn. This is exactly what the
39
+ * model saw.
40
+ * - `response` — the resolved `AIMessage` returned by the handler, captured as a whole
41
+ * (resolved message, not per-chunk frames; keeps the streaming core untouched).
42
+ *
43
+ * The sink is read lazily inside the middleware (per call), so when no TUI debug panel is
44
+ * attached the middleware is a transparent pass-through and the normal path pays nothing.
45
+ */
46
+ export interface DebugCapture {
47
+ /** The full message history sent to the model for this call, plus the request extras. */
48
+ onRequest?(messages: BaseMessage[], extras?: DebugRequestExtras): void;
49
+ /** The resolved model response for this call (an `AIMessage`). */
50
+ onResponse?(response: unknown): void;
51
+ }
52
+ /**
53
+ * Assemble the non-message request parts ({@link DebugRequestExtras}) for the `/debug`
54
+ * panel from a `wrapModelCall` request, defensively and key-free. Never throws (the caller
55
+ * already guards, but a debug sink must never break a run) and never dumps the raw model.
56
+ * Shared by both the lean ({@link import('#src/core/GthLangChainAgent.js').GthLangChainAgent})
57
+ * and deep backends so the panel behaves identically on either.
58
+ */
59
+ export declare function extractDebugRequestExtras(request: unknown): DebugRequestExtras | undefined;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Scalar model-param fields worth surfacing in the `/debug` panel. Deliberately an
3
+ * allowlist (NOT a whole-object dump) so no credential field (`apiKey`, `accessToken`, …)
4
+ * can ever leak into the rendered debug view.
5
+ *
6
+ * `streaming` is intentionally NOT here: it is the model instance's static flag, which is
7
+ * usually `false` even when the turn streams — the GthAgentRunner decides streaming by calling
8
+ * `.stream()` vs `.invoke()`, not by this property — so surfacing it just misleads.
9
+ */
10
+ const DEBUG_MODEL_PARAM_KEYS = [
11
+ 'model',
12
+ 'modelName',
13
+ 'modelId',
14
+ 'deploymentName',
15
+ 'temperature',
16
+ 'topP',
17
+ 'topK',
18
+ 'maxTokens',
19
+ 'maxOutputTokens',
20
+ 'maxReasoningTokens',
21
+ 'reasoningEffort',
22
+ 'thinkingBudget',
23
+ 'stop',
24
+ 'provider',
25
+ ];
26
+ /** Pull the key-free scalar model params from the (provider-specific) model instance. */
27
+ function extractModelParams(model) {
28
+ if (!model || typeof model !== 'object')
29
+ return undefined;
30
+ const src = model;
31
+ const out = {};
32
+ for (const key of DEBUG_MODEL_PARAM_KEYS) {
33
+ const value = src[key];
34
+ if (value === undefined || value === null)
35
+ continue;
36
+ // Only scalars / scalar arrays — never nested objects that could carry credentials.
37
+ if (typeof value === 'object' && !Array.isArray(value))
38
+ continue;
39
+ out[key] = value;
40
+ }
41
+ // `model` / `modelName` / `modelId` are langchain aliases for the same value; collapse the
42
+ // duplicates so the panel shows the model id once instead of two identical lines.
43
+ if (typeof out.model !== 'string' && typeof out.modelName === 'string') {
44
+ out.model = out.modelName;
45
+ }
46
+ if (out.modelName === out.model)
47
+ delete out.modelName;
48
+ if (out.modelId === out.model)
49
+ delete out.modelId;
50
+ return Object.keys(out).length > 0 ? out : undefined;
51
+ }
52
+ /** Best-effort tool definition (name + description + schema) for the debug view. */
53
+ function extractToolDefs(tools) {
54
+ if (!Array.isArray(tools) || tools.length === 0)
55
+ return undefined;
56
+ const defs = [];
57
+ for (const tool of tools) {
58
+ if (!tool || typeof tool !== 'object')
59
+ continue;
60
+ const t = tool;
61
+ const name = typeof t.name === 'string' ? t.name : undefined;
62
+ if (!name)
63
+ continue;
64
+ const description = typeof t.description === 'string' ? t.description : undefined;
65
+ // LangChain StructuredTools expose a Zod/JSON `schema`; some carry it on `lc_kwargs`.
66
+ const schema = t.schema ?? undefined;
67
+ defs.push({ name, description, schema });
68
+ }
69
+ return defs.length > 0 ? defs : undefined;
70
+ }
71
+ /**
72
+ * Assemble the non-message request parts ({@link DebugRequestExtras}) for the `/debug`
73
+ * panel from a `wrapModelCall` request, defensively and key-free. Never throws (the caller
74
+ * already guards, but a debug sink must never break a run) and never dumps the raw model.
75
+ * Shared by both the lean ({@link import('#src/core/GthLangChainAgent.js').GthLangChainAgent})
76
+ * and deep backends so the panel behaves identically on either.
77
+ */
78
+ export function extractDebugRequestExtras(request) {
79
+ if (!request || typeof request !== 'object')
80
+ return undefined;
81
+ const req = request;
82
+ const systemMessage = req.systemMessage;
83
+ const systemPrompt = typeof req.systemPrompt === 'string' && req.systemPrompt
84
+ ? req.systemPrompt
85
+ : typeof systemMessage?.content === 'string'
86
+ ? systemMessage.content
87
+ : undefined;
88
+ const extras = {
89
+ systemPrompt,
90
+ tools: extractToolDefs(req.tools),
91
+ modelParams: extractModelParams(req.model),
92
+ toolChoice: req.toolChoice,
93
+ };
94
+ // Return undefined when nothing useful was captured so the renderer can show a clear empty state.
95
+ const hasAny = extras.systemPrompt !== undefined ||
96
+ extras.tools !== undefined ||
97
+ extras.modelParams !== undefined ||
98
+ extras.toolChoice !== undefined;
99
+ return hasAny ? extras : undefined;
100
+ }
101
+ //# sourceMappingURL=debugCapture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debugCapture.js","sourceRoot":"","sources":["../../src/core/debugCapture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { GthAgentFactory } from '#src/core/types.js';
2
+ /**
3
+ * The lean backend factory — a {@link GthAgentFactory} that produces a plain
4
+ * {@link GthLangChainAgent} (no deepagents graph). This is identical to the built-in
5
+ * default {@link GthAgentRunner} constructs when no factory is passed; exporting it lets
6
+ * `@gaunt-sloth/agent`'s {@link resolveAgentFactory} pick lean vs deep from config without
7
+ * duplicating the constructor call.
8
+ */
9
+ export declare const gthLeanAgentFactory: GthAgentFactory;
@@ -0,0 +1,10 @@
1
+ import { GthLangChainAgent } from '#src/core/GthLangChainAgent.js';
2
+ /**
3
+ * The lean backend factory — a {@link GthAgentFactory} that produces a plain
4
+ * {@link GthLangChainAgent} (no deepagents graph). This is identical to the built-in
5
+ * default {@link GthAgentRunner} constructs when no factory is passed; exporting it lets
6
+ * `@gaunt-sloth/agent`'s {@link resolveAgentFactory} pick lean vs deep from config without
7
+ * duplicating the constructor call.
8
+ */
9
+ export const gthLeanAgentFactory = (statusUpdate, resolvers) => new GthLangChainAgent(statusUpdate, resolvers);
10
+ //# sourceMappingURL=gthLeanAgentFactory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gthLeanAgentFactory.js","sourceRoot":"","sources":["../../src/core/gthLeanAgentFactory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAEnE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAoB,CAAC,YAAY,EAAE,SAAS,EAAE,EAAE,CAC9E,IAAI,iBAAiB,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * GS2-16 — pure, fail-soft helpers that harvest per-run analytics (token usage + invoked tool
4
+ * names) from LangChain messages, so the opt-in history recorder can populate `gth insights`
5
+ * with real numbers instead of zeros.
6
+ *
7
+ * The extraction is deliberately structural and defensive (duck-typed reads guarded by a
8
+ * try/catch) rather than `instanceof`-based: the same accumulator serves both the non-streaming
9
+ * `invoke` path (a full `messages[]` from graph state) and the streaming paths (individual
10
+ * message chunks / `ToolMessage`s as they arrive), across providers whose message shapes vary.
11
+ * Nothing here may throw into a run — a missing/odd field just means that datum is skipped.
12
+ */
13
+ import type { GthRunStats } from '#src/core/types.js';
14
+ /** Mutable tally behind {@link finalizeRunStats}; see {@link createRunStatsAccumulator}. */
15
+ export interface RunStatsAccumulator {
16
+ /** Running sum of input/prompt tokens. */
17
+ input: number;
18
+ /** Running sum of output/completion tokens. */
19
+ output: number;
20
+ /** Whether ANY message reported `usage_metadata` — gates whether tokens are recorded at all. */
21
+ sawUsage: boolean;
22
+ /** Deduplicated set of invoked tool names. */
23
+ tools: Set<string>;
24
+ }
25
+ /** A fresh, empty accumulator. */
26
+ export declare function createRunStatsAccumulator(): RunStatsAccumulator;
27
+ /**
28
+ * Fold one LangChain message (or message chunk) into the accumulator. Fail-soft: any unexpected
29
+ * shape is swallowed so a run is never affected. Harvests, when present:
30
+ * - `usage_metadata.input_tokens` / `.output_tokens` (summed; marks `sawUsage`), and
31
+ * - tool names from an AIMessage's requested `tool_calls[].name` AND from a `ToolMessage`'s own
32
+ * `.name` (the executed tool), so both "requested" and "executed" tools are captured.
33
+ */
34
+ export declare function accumulateMessage(acc: RunStatsAccumulator, message: unknown): void;
35
+ /** Freeze the accumulator into the public {@link GthRunStats}. Tokens omitted unless observed. */
36
+ export declare function finalizeRunStats(acc: RunStatsAccumulator): GthRunStats;
37
+ /**
38
+ * One-shot convenience for the non-streaming path: fold a full `messages[]` (e.g. the final graph
39
+ * state) into a fresh accumulator and finalize. Fail-soft (a non-iterable input yields empties).
40
+ */
41
+ export declare function extractRunStats(messages: unknown): GthRunStats;
@@ -0,0 +1,73 @@
1
+ /** A fresh, empty accumulator. */
2
+ export function createRunStatsAccumulator() {
3
+ return { input: 0, output: 0, sawUsage: false, tools: new Set() };
4
+ }
5
+ /**
6
+ * Fold one LangChain message (or message chunk) into the accumulator. Fail-soft: any unexpected
7
+ * shape is swallowed so a run is never affected. Harvests, when present:
8
+ * - `usage_metadata.input_tokens` / `.output_tokens` (summed; marks `sawUsage`), and
9
+ * - tool names from an AIMessage's requested `tool_calls[].name` AND from a `ToolMessage`'s own
10
+ * `.name` (the executed tool), so both "requested" and "executed" tools are captured.
11
+ */
12
+ export function accumulateMessage(acc, message) {
13
+ try {
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ const m = message;
16
+ if (!m || typeof m !== 'object')
17
+ return;
18
+ const usage = m.usage_metadata;
19
+ if (usage && typeof usage === 'object') {
20
+ acc.sawUsage = true;
21
+ if (typeof usage.input_tokens === 'number' && Number.isFinite(usage.input_tokens)) {
22
+ acc.input += usage.input_tokens;
23
+ }
24
+ if (typeof usage.output_tokens === 'number' && Number.isFinite(usage.output_tokens)) {
25
+ acc.output += usage.output_tokens;
26
+ }
27
+ }
28
+ // Requested tool calls (AIMessage / AIMessageChunk). Continuation chunks in a streamed
29
+ // tool call carry an empty name, so guard on a non-empty string; the Set dedupes repeats.
30
+ const toolCalls = m.tool_calls;
31
+ if (Array.isArray(toolCalls)) {
32
+ for (const tc of toolCalls) {
33
+ const name = tc?.name;
34
+ if (typeof name === 'string' && name.length > 0)
35
+ acc.tools.add(name);
36
+ }
37
+ }
38
+ // Executed tool result (ToolMessage). Its `.name` is the tool that produced the result.
39
+ const type = typeof m.getType === 'function' ? m.getType() : m._getType?.();
40
+ if (type === 'tool' && typeof m.name === 'string' && m.name.length > 0) {
41
+ acc.tools.add(m.name);
42
+ }
43
+ }
44
+ catch {
45
+ /* fail-soft: never let stats capture affect a run */
46
+ }
47
+ }
48
+ /** Freeze the accumulator into the public {@link GthRunStats}. Tokens omitted unless observed. */
49
+ export function finalizeRunStats(acc) {
50
+ return {
51
+ tokensInput: acc.sawUsage ? acc.input : undefined,
52
+ tokensOutput: acc.sawUsage ? acc.output : undefined,
53
+ tools: [...acc.tools],
54
+ };
55
+ }
56
+ /**
57
+ * One-shot convenience for the non-streaming path: fold a full `messages[]` (e.g. the final graph
58
+ * state) into a fresh accumulator and finalize. Fail-soft (a non-iterable input yields empties).
59
+ */
60
+ export function extractRunStats(messages) {
61
+ const acc = createRunStatsAccumulator();
62
+ try {
63
+ if (Array.isArray(messages)) {
64
+ for (const m of messages)
65
+ accumulateMessage(acc, m);
66
+ }
67
+ }
68
+ catch {
69
+ /* fail-soft */
70
+ }
71
+ return finalizeRunStats(acc);
72
+ }
73
+ //# sourceMappingURL=runStats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runStats.js","sourceRoot":"","sources":["../../src/core/runStats.ts"],"names":[],"mappings":"AA0BA,kCAAkC;AAClC,MAAM,UAAU,yBAAyB;IACvC,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,GAAG,EAAU,EAAE,CAAC;AAC5E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAwB,EAAE,OAAgB;IAC1E,IAAI,CAAC;QACH,8DAA8D;QAC9D,MAAM,CAAC,GAAG,OAAc,CAAC;QACzB,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO;QAExC,MAAM,KAAK,GAAG,CAAC,CAAC,cAAc,CAAC;QAC/B,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvC,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC;YACpB,IAAI,OAAO,KAAK,CAAC,YAAY,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC;gBAClF,GAAG,CAAC,KAAK,IAAI,KAAK,CAAC,YAAY,CAAC;YAClC,CAAC;YACD,IAAI,OAAO,KAAK,CAAC,aAAa,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;gBACpF,GAAG,CAAC,MAAM,IAAI,KAAK,CAAC,aAAa,CAAC;YACpC,CAAC;QACH,CAAC;QAED,uFAAuF;QACvF,0FAA0F;QAC1F,MAAM,SAAS,GAAG,CAAC,CAAC,UAAU,CAAC;QAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7B,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;gBAC3B,MAAM,IAAI,GAAG,EAAE,EAAE,IAAI,CAAC;gBACtB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;oBAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACvE,CAAC;QACH,CAAC;QAED,wFAAwF;QACxF,MAAM,IAAI,GAAY,OAAO,CAAC,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;QACrF,IAAI,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,qDAAqD;IACvD,CAAC;AACH,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,gBAAgB,CAAC,GAAwB;IACvD,OAAO;QACL,WAAW,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;QACjD,YAAY,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;QACnD,KAAK,EAAE,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC;KACtB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,QAAiB;IAC/C,MAAM,GAAG,GAAG,yBAAyB,EAAE,CAAC;IACxC,IAAI,CAAC;QACH,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,KAAK,MAAM,CAAC,IAAI,QAAQ;gBAAE,iBAAiB,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,eAAe;IACjB,CAAC;IACD,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,54 @@
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 declare class ShellCommandFailedError extends Error {
29
+ /** The full model-facing body (command echo + `<COMMAND_OUTPUT>` + the failure/timeout tail). */
30
+ readonly output: string;
31
+ /** The process exit code; `null` when the command was killed (timeout) and never exited cleanly. */
32
+ readonly exitCode: number | null;
33
+ /** The exact command string that was executed. */
34
+ readonly command: string;
35
+ /** The run_* tool name that invoked the command (e.g. `run_tests`, `run_shell_command`). */
36
+ readonly toolName: string;
37
+ constructor(params: {
38
+ output: string;
39
+ exitCode: number | null;
40
+ command: string;
41
+ toolName: string;
42
+ });
43
+ }
44
+ /**
45
+ * Recognise a {@link ShellCommandFailedError} for the softening middleware in either agent.
46
+ *
47
+ * Prefers a plain `instanceof` (both agents share this one core module, so the class identity is
48
+ * the same), but falls back to a STRUCTURAL check keyed on `name === 'ShellCommandFailedError'`
49
+ * plus the carried fields. The structural arm is deliberate defence against a dual-package /
50
+ * realm-boundary hazard: if the error ever crossed a module boundary that broke `instanceof`, we
51
+ * would otherwise silently rethrow a real shell failure and regress the ✗ signal. Every field the
52
+ * softener reads (`output`) is asserted so a narrowed value is safe to use.
53
+ */
54
+ export declare function isShellCommandFailedError(e: unknown): e is ShellCommandFailedError;
@@ -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,75 @@
1
+ import type { ToolApprovalScope } from '#src/core/types.js';
2
+ /**
3
+ * Approval persistence scopes, widest-lived last. Alias of the canonical
4
+ * {@link ToolApprovalScope} so the allow-list engine and the decision vocabulary stay in sync.
5
+ */
6
+ export type ApprovalScope = ToolApprovalScope;
7
+ /**
8
+ * Decide whether a candidate argv contains a flag that *widens* the approved operation.
9
+ *
10
+ * Rule implemented (documented for the coordinator):
11
+ * - Re-derive the meaningful prefix from the candidate's actual argv.
12
+ * - Inspect every token of the candidate that is a flag (starts with `-`).
13
+ * - If any flag token — normalized by stripping a trailing `=value` and lowercasing —
14
+ * is in {@link WIDENING_FLAGS}, the command is considered a widening of the approved
15
+ * operation and the match is REFUSED (returns true).
16
+ *
17
+ * This is purposely a deny-list of operation-changing flags rather than an allow-list of
18
+ * benign flags: benign flag variants (`-b`, `--oneline`, `-la`) are exactly what we WANT
19
+ * to keep auto-approving, while the handful of "run-an-arbitrary-program / redirect-the-
20
+ * transport" flags are what an injected approval must never silently enable.
21
+ */
22
+ export declare function hasWideningFlag(argv: string[]): boolean;
23
+ /**
24
+ * A holder of approved prefixes with set semantics. Used for both the in-memory session
25
+ * store and the loaded persisted store. Pure data + membership; persistence is layered on
26
+ * top by {@link PersistedAllowlist}.
27
+ */
28
+ export declare class AllowlistStore {
29
+ private readonly prefixes;
30
+ constructor(initial?: Iterable<string>);
31
+ has(prefix: string): boolean;
32
+ add(prefix: string): void;
33
+ list(): string[];
34
+ }
35
+ /**
36
+ * The persisted (`always`) allow-list, backed by a JSON file. The path is injected (the
37
+ * runner resolves it via fileUtils → `.gsloth/.gsloth-settings/shell-allowlist.json`) so
38
+ * tests can point it at a temp dir. Loads lazily/defensively: a missing or malformed file
39
+ * yields an empty store rather than throwing (fail-open on READ is safe — an empty
40
+ * allow-list just means "prompt"; it never auto-approves anything).
41
+ */
42
+ export declare class PersistedAllowlist {
43
+ private readonly store;
44
+ private readonly filePath;
45
+ constructor(filePath: string);
46
+ private static load;
47
+ has(prefix: string): boolean;
48
+ list(): string[];
49
+ /** Add a prefix and persist the whole set to disk. */
50
+ add(prefix: string): void;
51
+ private persist;
52
+ }
53
+ /**
54
+ * Read-only view over the stores consulted for an auto-approval check. The runner passes
55
+ * its per-instance session store and (optionally) the persisted store.
56
+ */
57
+ export interface ApprovalStores {
58
+ session: Pick<AllowlistStore, 'has'>;
59
+ always?: Pick<PersistedAllowlist, 'has'>;
60
+ }
61
+ /**
62
+ * Decide whether `command` is already approved by the given stores — the gate the runner
63
+ * consults BEFORE prompting the human.
64
+ *
65
+ * Returns true ONLY when ALL of the following hold:
66
+ * 1. {@link classifyCommand} returns a non-null classification (so composition /
67
+ * substitution / redirection commands can never match — anti-injection layer 1);
68
+ * 2. the classified prefix is present in the session OR persisted (`always`) store; AND
69
+ * 3. the safe-bin re-validation passes: the candidate's actual argv contains no
70
+ * operation-widening flag (anti-injection layer 2 — see {@link hasWideningFlag}).
71
+ *
72
+ * Anything else (unclassifiable, unknown prefix, or a widening flag) → false → the human
73
+ * is prompted. Fail-closed by construction.
74
+ */
75
+ export declare function matchesApproval(command: string, stores: ApprovalStores): boolean;