@gaunt-sloth/agent 2.0.0-alpha.2 → 2.0.0-alpha.3

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 (42) hide show
  1. package/README.md +9 -0
  2. package/dist/builtInToolsConfig.js +11 -2
  3. package/dist/builtInToolsConfig.js.map +1 -1
  4. package/dist/core/GthDeepAgent.d.ts +32 -0
  5. package/dist/core/GthDeepAgent.js +113 -9
  6. package/dist/core/GthDeepAgent.js.map +1 -1
  7. package/dist/core/deepAgentPermissions.d.ts +29 -11
  8. package/dist/core/deepAgentPermissions.js +67 -26
  9. package/dist/core/deepAgentPermissions.js.map +1 -1
  10. package/dist/mcp/OAuthClientProviderImpl.js +2 -2
  11. package/dist/mcp/OAuthClientProviderImpl.js.map +1 -1
  12. package/dist/middleware/registry.d.ts +1 -1
  13. package/dist/middleware/registry.js +1 -1
  14. package/dist/middleware/types.d.ts +1 -1
  15. package/dist/middleware/types.js +1 -1
  16. package/dist/modules/acpModule.d.ts +5 -2
  17. package/dist/modules/acpModule.js +5 -2
  18. package/dist/modules/acpModule.js.map +1 -1
  19. package/dist/modules/interactiveSessionModule.js +56 -2
  20. package/dist/modules/interactiveSessionModule.js.map +1 -1
  21. package/dist/tools/GthDevToolkit.d.ts +40 -1
  22. package/dist/tools/GthDevToolkit.js +169 -17
  23. package/dist/tools/GthDevToolkit.js.map +1 -1
  24. package/dist/tools/shell/allowlist.d.ts +11 -0
  25. package/dist/tools/shell/allowlist.js +12 -0
  26. package/dist/tools/shell/allowlist.js.map +1 -0
  27. package/dist/tools/shell/arity.d.ts +11 -0
  28. package/dist/tools/shell/arity.js +12 -0
  29. package/dist/tools/shell/arity.js.map +1 -0
  30. package/dist/tools/shell/env.d.ts +22 -0
  31. package/dist/tools/shell/env.js +110 -0
  32. package/dist/tools/shell/env.js.map +1 -0
  33. package/dist/tools/shell/hardline.d.ts +15 -0
  34. package/dist/tools/shell/hardline.js +88 -0
  35. package/dist/tools/shell/hardline.js.map +1 -0
  36. package/dist/tools/shell/normalize.d.ts +10 -0
  37. package/dist/tools/shell/normalize.js +11 -0
  38. package/dist/tools/shell/normalize.js.map +1 -0
  39. package/dist/tools/shell/outputBuffer.d.ts +53 -0
  40. package/dist/tools/shell/outputBuffer.js +157 -0
  41. package/dist/tools/shell/outputBuffer.js.map +1 -0
  42. package/package.json +5 -5
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @module tools/shell/env
3
+ *
4
+ * Credential scrubbing for the shell tool's child environment. By default a
5
+ * spawned child inherits `process.env` verbatim, so an approved (or yolo'd)
6
+ * command can `echo $ANTHROPIC_API_KEY` and exfiltrate the operator's LLM/cloud
7
+ * credentials. {@link buildScrubbedEnv} returns a copy of the parent env with
8
+ * those credentials removed before spawn.
9
+ *
10
+ * Policy (deliberately scoped):
11
+ * - Strip LLM provider keys and cloud-provider secrets (the explicit blocklist +
12
+ * a wildcard sweep for `*_API_KEY` / `*_TOKEN` / `*_SECRET` / `*SECRET_KEY`).
13
+ * - LEAVE generic dev env intact (PATH, HOME, SHELL, LANG, npm/pnpm config, …)
14
+ * so normal commands still work.
15
+ * - LEAVE `GITHUB_TOKEN` / `GH_TOKEN` intact: gaunt-sloth's content/requirement
16
+ * providers shell out to `gh` (`gh pr diff`, `gh issue view`), so stripping
17
+ * these would break first-class workflows. They are explicitly allow-listed
18
+ * against the wildcard `*_TOKEN` sweep.
19
+ *
20
+ * Patterned after hermes-agent `_HERMES_PROVIDER_ENV_BLOCKLIST` (tools/environments/local.py)
21
+ * — but narrower: we only own the provider/cloud-secret floor.
22
+ */
23
+ import { env as processEnv } from '@gaunt-sloth/core/utils/systemUtils.js';
24
+ /**
25
+ * Explicit blocklist of LLM-provider and cloud credentials. Covers the providers
26
+ * gaunt-sloth (and its consumers) can be configured against, plus the standard
27
+ * cloud secret-bearing vars. Matched case-insensitively.
28
+ */
29
+ export const CREDENTIAL_BLOCKLIST = [
30
+ // LLM providers
31
+ 'ANTHROPIC_API_KEY',
32
+ 'ANTHROPIC_AUTH_TOKEN',
33
+ 'CLAUDE_CODE_OAUTH_TOKEN',
34
+ 'OPENAI_API_KEY',
35
+ 'GOOGLE_API_KEY',
36
+ 'GEMINI_API_KEY',
37
+ 'GOOGLE_APPLICATION_CREDENTIALS',
38
+ 'GROQ_API_KEY',
39
+ 'XAI_API_KEY',
40
+ 'DEEPSEEK_API_KEY',
41
+ 'MISTRAL_API_KEY',
42
+ 'OPENROUTER_API_KEY',
43
+ 'COHERE_API_KEY',
44
+ 'TOGETHER_API_KEY',
45
+ 'PERPLEXITY_API_KEY',
46
+ 'FIREWORKS_API_KEY',
47
+ // Azure OpenAI
48
+ 'AZURE_OPENAI_API_KEY',
49
+ 'AZURE_API_KEY',
50
+ // Cloud provider secrets (AWS / GCP)
51
+ 'AWS_SECRET_ACCESS_KEY',
52
+ 'AWS_SESSION_TOKEN',
53
+ 'AWS_ACCESS_KEY_ID',
54
+ ];
55
+ /**
56
+ * Allow-list of credential-shaped names that must survive the wildcard sweep
57
+ * because gaunt-sloth legitimately depends on them. Matched case-insensitively.
58
+ */
59
+ export const CREDENTIAL_ALLOWLIST = [
60
+ // `gh` CLI auth — used by the github content/requirement providers.
61
+ 'GITHUB_TOKEN',
62
+ 'GH_TOKEN',
63
+ ];
64
+ // Wildcard sweep: any var whose name ends in one of these suffixes is treated as
65
+ // a secret and stripped (unless allow-listed). Catches provider keys we didn't
66
+ // enumerate (e.g. a new `FOO_API_KEY`).
67
+ const SECRET_SUFFIXES = [
68
+ /_API_KEY$/i,
69
+ /_SECRET_ACCESS_KEY$/i,
70
+ /_SECRET_KEY$/i,
71
+ /_SECRET$/i,
72
+ /_TOKEN$/i,
73
+ ];
74
+ function isAllowlisted(name) {
75
+ return CREDENTIAL_ALLOWLIST.some((a) => a.toUpperCase() === name.toUpperCase());
76
+ }
77
+ function isBlocklisted(name) {
78
+ return CREDENTIAL_BLOCKLIST.some((b) => b.toUpperCase() === name.toUpperCase());
79
+ }
80
+ function matchesSecretSuffix(name) {
81
+ return SECRET_SUFFIXES.some((re) => re.test(name));
82
+ }
83
+ /**
84
+ * True when an env var name should be scrubbed from the child environment.
85
+ * Exported for testing.
86
+ */
87
+ export function shouldScrubEnvVar(name) {
88
+ if (isAllowlisted(name))
89
+ return false;
90
+ if (isBlocklisted(name))
91
+ return true;
92
+ return matchesSecretSuffix(name);
93
+ }
94
+ /**
95
+ * Build the child environment for a spawned shell command: a copy of the parent
96
+ * env with LLM/cloud credentials removed. Defaults to the live `process.env`
97
+ * (via systemUtils); a source can be injected for testing.
98
+ */
99
+ export function buildScrubbedEnv(source = processEnv) {
100
+ const scrubbed = {};
101
+ for (const [key, value] of Object.entries(source)) {
102
+ if (value === undefined)
103
+ continue;
104
+ if (shouldScrubEnvVar(key))
105
+ continue;
106
+ scrubbed[key] = value;
107
+ }
108
+ return scrubbed;
109
+ }
110
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../../../src/tools/shell/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,GAAG,IAAI,UAAU,EAAE,MAAM,wCAAwC,CAAC;AAE3E;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAA0B;IACzD,gBAAgB;IAChB,mBAAmB;IACnB,sBAAsB;IACtB,yBAAyB;IACzB,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,gCAAgC;IAChC,cAAc;IACd,aAAa;IACb,kBAAkB;IAClB,iBAAiB;IACjB,oBAAoB;IACpB,gBAAgB;IAChB,kBAAkB;IAClB,oBAAoB;IACpB,mBAAmB;IACnB,eAAe;IACf,sBAAsB;IACtB,eAAe;IACf,qCAAqC;IACrC,uBAAuB;IACvB,mBAAmB;IACnB,mBAAmB;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAA0B;IACzD,oEAAoE;IACpE,cAAc;IACd,UAAU;CACX,CAAC;AAEF,iFAAiF;AACjF,+EAA+E;AAC/E,wCAAwC;AACxC,MAAM,eAAe,GAAG;IACtB,YAAY;IACZ,sBAAsB;IACtB,eAAe;IACf,WAAW;IACX,UAAU;CACX,CAAC;AAEF,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;AAClF,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;AAClF,CAAC;AAED,SAAS,mBAAmB,CAAC,IAAY;IACvC,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AACrD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,aAAa,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACtC,IAAI,aAAa,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAA4B,UAAU;IACrE,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,IAAI,iBAAiB,CAAC,GAAG,CAAC;YAAE,SAAS;QACrC,QAAQ,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACxB,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Hardline patterns: [regex, human description]. Matched case-insensitively
3
+ * against the normalized command.
4
+ */
5
+ export declare const HARDLINE_PATTERNS: ReadonlyArray<readonly [RegExp, string]>;
6
+ export interface HardlineMatch {
7
+ /** Human-readable reason the command was refused. */
8
+ description: string;
9
+ }
10
+ /**
11
+ * Check a raw command against the hardline blocklist. Normalizes first so
12
+ * obfuscated variants are caught. Returns the match (with a description) when the
13
+ * command is catastrophic, or `null` when it is allowed to proceed.
14
+ */
15
+ export declare function checkHardline(command: string): HardlineMatch | null;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @module tools/shell/hardline
3
+ *
4
+ * Unbypassable hardline blocklist for the shell tool. These are catastrophic,
5
+ * non-recoverable commands (wipe the root filesystem, format a disk, overwrite a
6
+ * raw block device, fork-bomb, take the host down). They are refused inside
7
+ * `executeCommand` itself — BEFORE spawn — so the refusal fires regardless of
8
+ * yolo (`shellYolo`), any allow-list, or the confirmation path. yolo deliberately
9
+ * bypasses the *confirmation*; it does NOT bypass this floor.
10
+ *
11
+ * Recoverable-but-costly operations (e.g. `git reset --hard`, `rm -rf ./build`,
12
+ * `chmod -R 777 ./dir`, `curl | sh`) are intentionally NOT here — those are what
13
+ * the confirmation dialog / yolo are for.
14
+ *
15
+ * Patterns match the NORMALIZED command ({@link ./normalize.js}) so obfuscation
16
+ * (ANSI/fullwidth/backslash splits/whitespace padding) cannot bypass them.
17
+ *
18
+ * Patterned after hermes-agent `tools/approval.py` HARDLINE_PATTERNS.
19
+ */
20
+ import { normalizeCommand } from '#src/tools/shell/normalize.js';
21
+ // Matches a position where the shell would begin parsing a NEW command: start of
22
+ // string, after a separator (; & | newline), after `$(` or backtick, optionally
23
+ // consuming leading wrappers (sudo/env VAR=VAL/exec/nohup/setsid/time). Used by
24
+ // the shutdown-family patterns so they don't false-positive on `echo reboot`.
25
+ const CMD_POS = '(?:^|[;&|\\n`]|\\$\\()' +
26
+ '\\s*' +
27
+ '(?:sudo\\s+(?:-[^\\s]+\\s+)*)?' +
28
+ '(?:env\\s+(?:\\w+=\\S*\\s+)*)?' +
29
+ '(?:(?:exec|nohup|setsid|time)\\s+)*' +
30
+ '\\s*';
31
+ /**
32
+ * Hardline patterns: [regex, human description]. Matched case-insensitively
33
+ * against the normalized command.
34
+ */
35
+ export const HARDLINE_PATTERNS = [
36
+ // rm -rf targeting the root filesystem (`/`, `/*`).
37
+ [/\brm\s+(?:-[^\s]*\s+)*\/\s*\*?\s*(?:$|[;&|])/, 'recursive delete of root filesystem'],
38
+ // rm -rf targeting protected system directories (with optional /* suffix).
39
+ [
40
+ /\brm\s+(?:-[^\s]*\s+)*(?:\/(?:home|root|etc|usr|var|bin|sbin|boot|lib|lib64|opt|sys|proc))(?:\/\*)?\s*(?:$|[;&|])/,
41
+ 'recursive delete of system directory',
42
+ ],
43
+ // rm -rf targeting the home directory (~ or $HOME).
44
+ // Note: patterns match the LOWERCASED normalized command, so $HOME → $home.
45
+ [
46
+ /\brm\s+(?:-[^\s]*\s+)*(?:~|\$home)(?:\/\*)?\s*(?:$|[;&|])/,
47
+ 'recursive delete of home directory',
48
+ ],
49
+ // Filesystem format.
50
+ [/\bmkfs(?:\.[a-z0-9]+)?\b/, 'format filesystem (mkfs)'],
51
+ // dd writing to a raw block device.
52
+ [/\bdd\b[^\n]*\bof=\/dev\/(?:sd|nvme|hd|mmcblk|vd|xvd)[a-z0-9]*/, 'dd to raw block device'],
53
+ // Shell redirection to a raw block device (`> /dev/sda`).
54
+ [/>\s*\/dev\/(?:sd|nvme|hd|mmcblk|vd|xvd)[a-z0-9]*\b/, 'redirect to raw block device'],
55
+ // Classic fork bomb `:(){ :|:& };:`.
56
+ [/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, 'fork bomb'],
57
+ // chmod -R 777 / (recursive world-writable on root).
58
+ [
59
+ /\bchmod\s+(?:-[^\s]*\s+)*(?:-r|--recursive)\s+(?:-[^\s]*\s+)*777\s+\//,
60
+ 'recursive chmod 777 of root',
61
+ ],
62
+ // Kill every process on the system (`kill -1`, `kill -9 -1`).
63
+ [/\bkill\s+(?:-[^\s]+\s+)*-1\b/, 'kill all processes'],
64
+ // System shutdown / reboot — anchored to a command position so `echo reboot`
65
+ // and `grep shutdown log` don't trip it.
66
+ [new RegExp(CMD_POS + '(?:shutdown|reboot|halt|poweroff)\\b'), 'system shutdown/reboot'],
67
+ [new RegExp(CMD_POS + 'init\\s+[06]\\b'), 'init 0/6 (shutdown/reboot)'],
68
+ [
69
+ new RegExp(CMD_POS + 'systemctl\\s+(?:poweroff|reboot|halt|kexec)\\b'),
70
+ 'systemctl poweroff/reboot',
71
+ ],
72
+ [new RegExp(CMD_POS + 'telinit\\s+[06]\\b'), 'telinit 0/6 (shutdown/reboot)'],
73
+ ];
74
+ /**
75
+ * Check a raw command against the hardline blocklist. Normalizes first so
76
+ * obfuscated variants are caught. Returns the match (with a description) when the
77
+ * command is catastrophic, or `null` when it is allowed to proceed.
78
+ */
79
+ export function checkHardline(command) {
80
+ const normalized = normalizeCommand(command).toLowerCase();
81
+ for (const [pattern, description] of HARDLINE_PATTERNS) {
82
+ if (pattern.test(normalized)) {
83
+ return { description };
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ //# sourceMappingURL=hardline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hardline.js","sourceRoot":"","sources":["../../../src/tools/shell/hardline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEjE,iFAAiF;AACjF,gFAAgF;AAChF,gFAAgF;AAChF,8EAA8E;AAC9E,MAAM,OAAO,GACX,wBAAwB;IACxB,MAAM;IACN,gCAAgC;IAChC,gCAAgC;IAChC,qCAAqC;IACrC,MAAM,CAAC;AAET;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAA6C;IACzE,oDAAoD;IACpD,CAAC,8CAA8C,EAAE,qCAAqC,CAAC;IACvF,2EAA2E;IAC3E;QACE,mHAAmH;QACnH,sCAAsC;KACvC;IACD,oDAAoD;IACpD,4EAA4E;IAC5E;QACE,2DAA2D;QAC3D,oCAAoC;KACrC;IACD,qBAAqB;IACrB,CAAC,0BAA0B,EAAE,0BAA0B,CAAC;IACxD,oCAAoC;IACpC,CAAC,+DAA+D,EAAE,wBAAwB,CAAC;IAC3F,0DAA0D;IAC1D,CAAC,oDAAoD,EAAE,8BAA8B,CAAC;IACtF,qCAAqC;IACrC,CAAC,gDAAgD,EAAE,WAAW,CAAC;IAC/D,qDAAqD;IACrD;QACE,uEAAuE;QACvE,6BAA6B;KAC9B;IACD,8DAA8D;IAC9D,CAAC,8BAA8B,EAAE,oBAAoB,CAAC;IACtD,6EAA6E;IAC7E,yCAAyC;IACzC,CAAC,IAAI,MAAM,CAAC,OAAO,GAAG,sCAAsC,CAAC,EAAE,wBAAwB,CAAC;IACxF,CAAC,IAAI,MAAM,CAAC,OAAO,GAAG,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;IACvE;QACE,IAAI,MAAM,CAAC,OAAO,GAAG,gDAAgD,CAAC;QACtE,2BAA2B;KAC5B;IACD,CAAC,IAAI,MAAM,CAAC,OAAO,GAAG,oBAAoB,CAAC,EAAE,+BAA+B,CAAC;CAC9E,CAAC;AAOF;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,MAAM,UAAU,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;IAC3D,KAAK,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,IAAI,iBAAiB,EAAE,CAAC;QACvD,IAAI,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAC7B,OAAO,EAAE,WAAW,EAAE,CAAC;QACzB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @module tools/shell/normalize
3
+ *
4
+ * Re-export of the canonical command normalizer, which now lives in
5
+ * `@gaunt-sloth/core` (`core/shell/normalize`) so the core runner's EXT-9 Tier-2
6
+ * allow-list classifier and the agent's Tier-1 hardline blocklist share ONE
7
+ * implementation. Kept as a stable import path for existing agent-side consumers
8
+ * (`tools/shell/hardline`, specs).
9
+ */
10
+ export { normalizeCommand } from '@gaunt-sloth/core/core/shell/normalize.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @module tools/shell/normalize
3
+ *
4
+ * Re-export of the canonical command normalizer, which now lives in
5
+ * `@gaunt-sloth/core` (`core/shell/normalize`) so the core runner's EXT-9 Tier-2
6
+ * allow-list classifier and the agent's Tier-1 hardline blocklist share ONE
7
+ * implementation. Kept as a stable import path for existing agent-side consumers
8
+ * (`tools/shell/hardline`, specs).
9
+ */
10
+ export { normalizeCommand } from '@gaunt-sloth/core/core/shell/normalize.js';
11
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../../src/tools/shell/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC"}
@@ -0,0 +1,53 @@
1
+ export interface CappedOutput {
2
+ /** The bounded text to surface to the model (head + tail with a gap note). */
3
+ text: string;
4
+ /** True when the output exceeded the budget and was truncated. */
5
+ truncated: boolean;
6
+ /** Total bytes captured (the full, untruncated size). */
7
+ totalBytes: number;
8
+ /** Absolute path to the spilled full output, or undefined when not truncated. */
9
+ spillPath?: string;
10
+ }
11
+ /**
12
+ * Accumulates command output up to a byte budget using a head + tail window.
13
+ * The full output is retained in memory so it can be spilled to disk on demand;
14
+ * the in-memory full copy is itself bounded at a hard safety ceiling to avoid
15
+ * unbounded growth from a runaway command.
16
+ */
17
+ export declare class OutputBuffer {
18
+ private head;
19
+ private headBytes;
20
+ private readonly tailChunks;
21
+ private tailBytes;
22
+ private totalBytesCount;
23
+ /** Full copy for spillover, bounded by a hard ceiling. */
24
+ private full;
25
+ private fullTruncated;
26
+ private readonly headBudget;
27
+ private readonly tailBudget;
28
+ private readonly fullCeiling;
29
+ /**
30
+ * @param maxOutputBytes total byte budget for the returned (head+tail) window.
31
+ * @param fullCeiling hard ceiling for the in-memory full copy kept for spillover.
32
+ * Defaults to a generous 16MB so the spill file holds the real output for
33
+ * typical noisy logs, while still bounding memory for a runaway command.
34
+ */
35
+ constructor(maxOutputBytes: number, fullCeiling?: number);
36
+ /** Append a chunk of output. Byte accounting uses UTF-8 byte length. */
37
+ append(chunk: string): void;
38
+ /** Push into the tail ring-buffer, evicting oldest data beyond the budget. */
39
+ private pushTail;
40
+ get totalBytes(): number;
41
+ get isTruncated(): boolean;
42
+ /** The full captured output (bounded by the in-memory ceiling). */
43
+ getFull(): string;
44
+ /**
45
+ * Finalize the capture. When not truncated, returns the output verbatim. When
46
+ * truncated, spills the full output to a temp file and returns head + a gap
47
+ * note (with the file path) + tail.
48
+ *
49
+ * @param spill optional injected spill function (path-returning) for testing.
50
+ * Defaults to writing into `os.tmpdir()`.
51
+ */
52
+ finalize(spill?: (content: string) => string): CappedOutput;
53
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @module tools/shell/outputBuffer
3
+ *
4
+ * Bounded capture of a shell command's combined stdout/stderr for the value
5
+ * returned to the model. A noisy build/test log can be megabytes; dumping it
6
+ * verbatim into a ToolMessage blows the context window. This buffer keeps a
7
+ * HEAD window (the first N bytes) and a TAIL ring-buffer (the last N bytes) and
8
+ * drops the middle, while accumulating the FULL output separately so it can be
9
+ * spilled to a temp file the model can re-read with `read_file`.
10
+ *
11
+ * Live streaming to the terminal is unaffected — the toolkit still writes every
12
+ * chunk to stdout as it arrives; only the returned string is capped.
13
+ *
14
+ * Patterned after opencode `bash.ts` (per-stream cap + temp-file tail) and
15
+ * openclaw `bash-tools.shared.ts` `truncateMiddle`.
16
+ */
17
+ import { writeFileSync } from 'node:fs';
18
+ import { tmpdir } from 'node:os';
19
+ import path from 'node:path';
20
+ /**
21
+ * Accumulates command output up to a byte budget using a head + tail window.
22
+ * The full output is retained in memory so it can be spilled to disk on demand;
23
+ * the in-memory full copy is itself bounded at a hard safety ceiling to avoid
24
+ * unbounded growth from a runaway command.
25
+ */
26
+ export class OutputBuffer {
27
+ head = '';
28
+ headBytes = 0;
29
+ tailChunks = [];
30
+ tailBytes = 0;
31
+ totalBytesCount = 0;
32
+ /** Full copy for spillover, bounded by a hard ceiling. */
33
+ full = '';
34
+ fullTruncated = false;
35
+ headBudget;
36
+ tailBudget;
37
+ fullCeiling;
38
+ /**
39
+ * @param maxOutputBytes total byte budget for the returned (head+tail) window.
40
+ * @param fullCeiling hard ceiling for the in-memory full copy kept for spillover.
41
+ * Defaults to a generous 16MB so the spill file holds the real output for
42
+ * typical noisy logs, while still bounding memory for a runaway command.
43
+ */
44
+ constructor(maxOutputBytes, fullCeiling = 16 * 1024 * 1024) {
45
+ // Split the budget evenly between head and tail.
46
+ this.headBudget = Math.max(1, Math.floor(maxOutputBytes / 2));
47
+ this.tailBudget = Math.max(1, maxOutputBytes - this.headBudget);
48
+ // The full copy must hold at least the preview window.
49
+ this.fullCeiling = Math.max(fullCeiling, maxOutputBytes);
50
+ }
51
+ /** Append a chunk of output. Byte accounting uses UTF-8 byte length. */
52
+ append(chunk) {
53
+ const bytes = Buffer.byteLength(chunk, 'utf8');
54
+ this.totalBytesCount += bytes;
55
+ // Maintain the full copy up to the ceiling (for spillover). Store as much of
56
+ // an oversized chunk as fits rather than dropping it wholesale.
57
+ if (!this.fullTruncated) {
58
+ const used = Buffer.byteLength(this.full, 'utf8');
59
+ const room = this.fullCeiling - used;
60
+ if (bytes <= room) {
61
+ this.full += chunk;
62
+ }
63
+ else {
64
+ if (room > 0) {
65
+ this.full += Buffer.from(chunk, 'utf8').subarray(0, room).toString('utf8');
66
+ }
67
+ this.fullTruncated = true;
68
+ }
69
+ }
70
+ // Fill the head window first.
71
+ if (this.headBytes < this.headBudget) {
72
+ const room = this.headBudget - this.headBytes;
73
+ if (bytes <= room) {
74
+ this.head += chunk;
75
+ this.headBytes += bytes;
76
+ return;
77
+ }
78
+ // Partial: take what fits into head, the rest flows to tail.
79
+ const take = Buffer.from(chunk, 'utf8').subarray(0, room).toString('utf8');
80
+ this.head += take;
81
+ this.headBytes += Buffer.byteLength(take, 'utf8');
82
+ const rest = chunk.slice(take.length);
83
+ this.pushTail(rest);
84
+ return;
85
+ }
86
+ this.pushTail(chunk);
87
+ }
88
+ /** Push into the tail ring-buffer, evicting oldest data beyond the budget. */
89
+ pushTail(chunk) {
90
+ if (chunk.length === 0)
91
+ return;
92
+ this.tailChunks.push(chunk);
93
+ this.tailBytes += Buffer.byteLength(chunk, 'utf8');
94
+ while (this.tailBytes > this.tailBudget && this.tailChunks.length > 1) {
95
+ const removed = this.tailChunks.shift();
96
+ this.tailBytes -= Buffer.byteLength(removed, 'utf8');
97
+ }
98
+ // If a single chunk alone exceeds the tail budget, trim it from the front.
99
+ if (this.tailBytes > this.tailBudget && this.tailChunks.length === 1) {
100
+ const only = this.tailChunks[0];
101
+ const overflow = this.tailBytes - this.tailBudget;
102
+ const buf = Buffer.from(only, 'utf8');
103
+ const trimmed = buf.subarray(overflow).toString('utf8');
104
+ this.tailChunks[0] = trimmed;
105
+ this.tailBytes = Buffer.byteLength(trimmed, 'utf8');
106
+ }
107
+ }
108
+ get totalBytes() {
109
+ return this.totalBytesCount;
110
+ }
111
+ get isTruncated() {
112
+ return this.totalBytesCount > this.headBytes + this.tailBytes;
113
+ }
114
+ /** The full captured output (bounded by the in-memory ceiling). */
115
+ getFull() {
116
+ return this.full;
117
+ }
118
+ /**
119
+ * Finalize the capture. When not truncated, returns the output verbatim. When
120
+ * truncated, spills the full output to a temp file and returns head + a gap
121
+ * note (with the file path) + tail.
122
+ *
123
+ * @param spill optional injected spill function (path-returning) for testing.
124
+ * Defaults to writing into `os.tmpdir()`.
125
+ */
126
+ finalize(spill = defaultSpill) {
127
+ const tail = this.tailChunks.join('');
128
+ if (!this.isTruncated) {
129
+ return {
130
+ text: this.head + tail,
131
+ truncated: false,
132
+ totalBytes: this.totalBytesCount,
133
+ };
134
+ }
135
+ const spillPath = spill(this.full);
136
+ const fullNote = this.fullTruncated
137
+ ? `First ${Buffer.byteLength(this.full, 'utf8')} bytes`
138
+ : 'Full output';
139
+ const droppedNote = `\n\n... [output truncated: ${this.totalBytesCount} bytes total; ` +
140
+ `showing first ${this.headBytes} + last ${this.tailBytes} bytes. ` +
141
+ `${fullNote} written to ${spillPath} — use read_file to inspect.] ...\n\n`;
142
+ return {
143
+ text: this.head + droppedNote + tail,
144
+ truncated: true,
145
+ totalBytes: this.totalBytesCount,
146
+ spillPath,
147
+ };
148
+ }
149
+ }
150
+ /** Default spill: write to a uniquely-named file in the OS temp dir. */
151
+ function defaultSpill(content) {
152
+ const name = `gsloth-shell-${Date.now()}-${Math.random().toString(36).slice(2, 10)}.log`;
153
+ const filePath = path.join(tmpdir(), name);
154
+ writeFileSync(filePath, content, 'utf8');
155
+ return filePath;
156
+ }
157
+ //# sourceMappingURL=outputBuffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outputBuffer.js","sourceRoot":"","sources":["../../../src/tools/shell/outputBuffer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,IAAI,MAAM,WAAW,CAAC;AAa7B;;;;;GAKG;AACH,MAAM,OAAO,YAAY;IACf,IAAI,GAAG,EAAE,CAAC;IACV,SAAS,GAAG,CAAC,CAAC;IACL,UAAU,GAAa,EAAE,CAAC;IACnC,SAAS,GAAG,CAAC,CAAC;IACd,eAAe,GAAG,CAAC,CAAC;IAC5B,0DAA0D;IAClD,IAAI,GAAG,EAAE,CAAC;IACV,aAAa,GAAG,KAAK,CAAC;IAEb,UAAU,CAAS;IACnB,UAAU,CAAS;IACnB,WAAW,CAAS;IAErC;;;;;OAKG;IACH,YAAY,cAAsB,EAAE,WAAW,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI;QAChE,iDAAiD;QACjD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;QAChE,uDAAuD;QACvD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAC3D,CAAC;IAED,wEAAwE;IACxE,MAAM,CAAC,KAAa;QAClB,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC/C,IAAI,CAAC,eAAe,IAAI,KAAK,CAAC;QAE9B,6EAA6E;QAC7E,gEAAgE;QAChE,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAClD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;YACrC,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;gBAClB,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC;YACrB,CAAC;iBAAM,CAAC;gBACN,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;oBACb,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBAC7E,CAAC;gBACD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;YAC5B,CAAC;QACH,CAAC;QAED,8BAA8B;QAC9B,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YACrC,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC;YAC9C,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;gBAClB,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC;gBACnB,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;gBACxB,OAAO;YACT,CAAC;YACD,6DAA6D;YAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC3E,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC;YAClB,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAClD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACtC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YACpB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IAED,8EAA8E;IACtE,QAAQ,CAAC,KAAa;QAC5B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC/B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5B,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtE,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,EAAG,CAAC;YACzC,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACvD,CAAC;QACD,2EAA2E;QAC3E,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrE,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC;YAClD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACtC,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACxD,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;YAC7B,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,eAAe,CAAC;IAC9B,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IAChE,CAAC;IAED,mEAAmE;IACnE,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAqC,YAAY;QACxD,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,OAAO;gBACL,IAAI,EAAE,IAAI,CAAC,IAAI,GAAG,IAAI;gBACtB,SAAS,EAAE,KAAK;gBAChB,UAAU,EAAE,IAAI,CAAC,eAAe;aACjC,CAAC;QACJ,CAAC;QAED,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa;YACjC,CAAC,CAAC,SAAS,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ;YACvD,CAAC,CAAC,aAAa,CAAC;QAClB,MAAM,WAAW,GACf,8BAA8B,IAAI,CAAC,eAAe,gBAAgB;YAClE,iBAAiB,IAAI,CAAC,SAAS,WAAW,IAAI,CAAC,SAAS,UAAU;YAClE,GAAG,QAAQ,eAAe,SAAS,uCAAuC,CAAC;QAC7E,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI,GAAG,WAAW,GAAG,IAAI;YACpC,SAAS,EAAE,IAAI;YACf,UAAU,EAAE,IAAI,CAAC,eAAe;YAChC,SAAS;SACV,CAAC;IACJ,CAAC;CACF;AAED,wEAAwE;AACxE,SAAS,YAAY,CAAC,OAAe;IACnC,MAAM,IAAI,GAAG,gBAAgB,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC;IACzF,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,CAAC;IAC3C,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IACzC,OAAO,QAAQ,CAAC;AAClB,CAAC"}
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@gaunt-sloth/agent",
3
- "version": "2.0.0-alpha.2",
3
+ "version": "2.0.0-alpha.3",
4
4
  "description": "Deep agent runtime for Gaunt Sloth: tools, middleware, MCP/A2A clients and the AG-UI server",
5
5
  "license": "MIT",
6
6
  "author": "Andrew Kondratev",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/Galvanized-Pukeko/gaunt-sloth.git",
9
+ "url": "git+https://github.com/pukeko-robotics/gaunt-sloth.git",
10
10
  "directory": "packages/agent"
11
11
  },
12
12
  "bugs": {
13
- "url": "https://github.com/Galvanized-Pukeko/gaunt-sloth/issues"
13
+ "url": "https://github.com/pukeko-robotics/gaunt-sloth/issues"
14
14
  },
15
- "homepage": "https://github.com/Galvanized-Pukeko/gaunt-sloth/tree/main/packages/agent#readme",
15
+ "homepage": "https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/agent#readme",
16
16
  "keywords": [
17
17
  "ai",
18
18
  "agent",
@@ -52,7 +52,7 @@
52
52
  "langchain": "^1.5.0",
53
53
  "uuid": ">=11.1.1",
54
54
  "zod": "^3.25.0 || ^4.0.0",
55
- "@gaunt-sloth/core": "2.0.0-alpha.2"
55
+ "@gaunt-sloth/core": "2.0.0-alpha.3"
56
56
  },
57
57
  "files": [
58
58
  "./dist/*",