@gaunt-sloth/agent 2.0.0-alpha.2 → 2.0.0-alpha.21
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 +10 -0
- package/dist/builtInToolsConfig.d.ts +17 -3
- package/dist/builtInToolsConfig.js +41 -14
- package/dist/builtInToolsConfig.js.map +1 -1
- package/dist/core/GthDeepAgent.d.ts +63 -13
- package/dist/core/GthDeepAgent.js +360 -130
- package/dist/core/GthDeepAgent.js.map +1 -1
- package/dist/core/debugCapture.d.ts +8 -49
- package/dist/core/debugCapture.js +1 -1
- package/dist/core/debugCapture.js.map +1 -1
- package/dist/core/deepAgentPermissions.d.ts +73 -13
- package/dist/core/deepAgentPermissions.js +191 -29
- package/dist/core/deepAgentPermissions.js.map +1 -1
- package/dist/core/gthAcpServer.js +11 -0
- package/dist/core/gthAcpServer.js.map +1 -1
- package/dist/core/gthDeepAgentFactory.d.ts +3 -0
- package/dist/core/gthDeepAgentFactory.js +9 -1
- package/dist/core/gthDeepAgentFactory.js.map +1 -1
- package/dist/core/resolveAgentFactory.d.ts +14 -0
- package/dist/core/resolveAgentFactory.js +18 -0
- package/dist/core/resolveAgentFactory.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/OAuthClientProviderImpl.js +2 -2
- package/dist/mcp/OAuthClientProviderImpl.js.map +1 -1
- package/dist/mcp/tlsTrust.d.ts +61 -0
- package/dist/mcp/tlsTrust.js +109 -0
- package/dist/mcp/tlsTrust.js.map +1 -0
- package/dist/middleware/registry.d.ts +1 -1
- package/dist/middleware/registry.js +1 -1
- package/dist/middleware/types.d.ts +1 -1
- package/dist/middleware/types.js +1 -1
- package/dist/modules/a2a/A2AClientWrapper.d.ts +46 -0
- package/dist/modules/a2a/A2AClientWrapper.js +71 -0
- package/dist/modules/a2a/A2AClientWrapper.js.map +1 -1
- package/dist/modules/acpModule.d.ts +5 -2
- package/dist/modules/acpModule.js +13 -2
- package/dist/modules/acpModule.js.map +1 -1
- package/dist/modules/apiAgUiModule.js +48 -16
- package/dist/modules/apiAgUiModule.js.map +1 -1
- package/dist/modules/interactiveSessionModule.js +140 -7
- package/dist/modules/interactiveSessionModule.js.map +1 -1
- package/dist/resolvers.js +104 -9
- package/dist/resolvers.js.map +1 -1
- package/dist/tools/GthCustomToolkit.js +24 -9
- package/dist/tools/GthCustomToolkit.js.map +1 -1
- package/dist/tools/GthDevToolkit.d.ts +45 -1
- package/dist/tools/GthDevToolkit.js +229 -31
- package/dist/tools/GthDevToolkit.js.map +1 -1
- package/dist/tools/GthFileSystemToolkit.d.ts +47 -0
- package/dist/tools/GthFileSystemToolkit.js +305 -18
- package/dist/tools/GthFileSystemToolkit.js.map +1 -1
- package/dist/tools/gthChecklistTool.d.ts +30 -0
- package/dist/tools/gthChecklistTool.js +81 -0
- package/dist/tools/gthChecklistTool.js.map +1 -0
- package/dist/tools/gthGrepTool.d.ts +54 -0
- package/dist/tools/gthGrepTool.js +482 -0
- package/dist/tools/gthGrepTool.js.map +1 -0
- package/dist/tools/shell/allowlist.d.ts +11 -0
- package/dist/tools/shell/allowlist.js +12 -0
- package/dist/tools/shell/allowlist.js.map +1 -0
- package/dist/tools/shell/arity.d.ts +11 -0
- package/dist/tools/shell/arity.js +12 -0
- package/dist/tools/shell/arity.js.map +1 -0
- package/dist/tools/shell/env.d.ts +22 -0
- package/dist/tools/shell/env.js +110 -0
- package/dist/tools/shell/env.js.map +1 -0
- package/dist/tools/shell/hardline.d.ts +15 -0
- package/dist/tools/shell/hardline.js +88 -0
- package/dist/tools/shell/hardline.js.map +1 -0
- package/dist/tools/shell/normalize.d.ts +10 -0
- package/dist/tools/shell/normalize.js +11 -0
- package/dist/tools/shell/normalize.js.map +1 -0
- package/dist/tools/shell/outputBuffer.d.ts +53 -0
- package/dist/tools/shell/outputBuffer.js +157 -0
- package/dist/tools/shell/outputBuffer.js.map +1 -0
- package/dist/tools/shell/workDir.d.ts +11 -0
- package/dist/tools/shell/workDir.js +45 -0
- package/dist/tools/shell/workDir.js.map +1 -0
- package/dist/utils/mcpAuthError.d.ts +36 -0
- package/dist/utils/mcpAuthError.js +89 -0
- package/dist/utils/mcpAuthError.js.map +1 -0
- package/dist/utils/mcpUtils.js +16 -0
- package/dist/utils/mcpUtils.js.map +1 -1
- package/package.json +14 -13
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module tools/shell/allowlist
|
|
3
|
+
*
|
|
4
|
+
* Re-export of the EXT-9 Tier-2 allow-list engine. The implementation lives in
|
|
5
|
+
* `@gaunt-sloth/core` (`core/shell/allowlist`) because the core {@link GthAgentRunner}
|
|
6
|
+
* owns the per-instance session store and the loaded persisted store and consults
|
|
7
|
+
* `matchesApproval` before prompting (core cannot import from `@gaunt-sloth/agent`).
|
|
8
|
+
*
|
|
9
|
+
* See the core module for the exact safe-bin / anti-widening matching rule.
|
|
10
|
+
*/
|
|
11
|
+
export { matchesApproval, hasWideningFlag, AllowlistStore, PersistedAllowlist, } from '@gaunt-sloth/core/core/shell/allowlist.js';
|
|
12
|
+
//# sourceMappingURL=allowlist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowlist.js","sourceRoot":"","sources":["../../../src/tools/shell/allowlist.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,eAAe,EACf,eAAe,EACf,cAAc,EACd,kBAAkB,GAGnB,MAAM,2CAA2C,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module tools/shell/arity
|
|
3
|
+
*
|
|
4
|
+
* Re-export of the EXT-9 Tier-2 command classifier. The implementation lives in
|
|
5
|
+
* `@gaunt-sloth/core` (`core/shell/arity`) because the core {@link GthAgentRunner}
|
|
6
|
+
* must consult it BEFORE prompting (and core cannot import from `@gaunt-sloth/agent`).
|
|
7
|
+
* This stable agent-side path is kept for tests and any agent-layer consumers.
|
|
8
|
+
*
|
|
9
|
+
* See the core module for the arity table scope and the anti-injection fail-closed rule.
|
|
10
|
+
*/
|
|
11
|
+
export { classifyCommand, tokenize, meaningfulPrefixTokens, type CommandClassification, } from '@gaunt-sloth/core/core/shell/arity.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module tools/shell/arity
|
|
3
|
+
*
|
|
4
|
+
* Re-export of the EXT-9 Tier-2 command classifier. The implementation lives in
|
|
5
|
+
* `@gaunt-sloth/core` (`core/shell/arity`) because the core {@link GthAgentRunner}
|
|
6
|
+
* must consult it BEFORE prompting (and core cannot import from `@gaunt-sloth/agent`).
|
|
7
|
+
* This stable agent-side path is kept for tests and any agent-layer consumers.
|
|
8
|
+
*
|
|
9
|
+
* See the core module for the arity table scope and the anti-injection fail-closed rule.
|
|
10
|
+
*/
|
|
11
|
+
export { classifyCommand, tokenize, meaningfulPrefixTokens, } from '@gaunt-sloth/core/core/shell/arity.js';
|
|
12
|
+
//# sourceMappingURL=arity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arity.js","sourceRoot":"","sources":["../../../src/tools/shell/arity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,eAAe,EACf,QAAQ,EACR,sBAAsB,GAEvB,MAAM,uCAAuC,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit blocklist of LLM-provider and cloud credentials. Covers the providers
|
|
3
|
+
* gaunt-sloth (and its consumers) can be configured against, plus the standard
|
|
4
|
+
* cloud secret-bearing vars. Matched case-insensitively.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CREDENTIAL_BLOCKLIST: ReadonlyArray<string>;
|
|
7
|
+
/**
|
|
8
|
+
* Allow-list of credential-shaped names that must survive the wildcard sweep
|
|
9
|
+
* because gaunt-sloth legitimately depends on them. Matched case-insensitively.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CREDENTIAL_ALLOWLIST: ReadonlyArray<string>;
|
|
12
|
+
/**
|
|
13
|
+
* True when an env var name should be scrubbed from the child environment.
|
|
14
|
+
* Exported for testing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function shouldScrubEnvVar(name: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Build the child environment for a spawned shell command: a copy of the parent
|
|
19
|
+
* env with LLM/cloud credentials removed. Defaults to the live `process.env`
|
|
20
|
+
* (via systemUtils); a source can be injected for testing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildScrubbedEnv(source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
@@ -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,MAAM,GAAsB,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":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working directory the shell tool spawns in: the ACP session override when set (ACP transport),
|
|
3
|
+
* otherwise `getCurrentWorkDir()` (local runner + AG-UI, unchanged).
|
|
4
|
+
*/
|
|
5
|
+
export declare function getShellWorkDir(): string;
|
|
6
|
+
/**
|
|
7
|
+
* ACP-only: point the shell tool at the current ACP session's fs-backend root (the resolved
|
|
8
|
+
* `session/new.cwd`, or the startup workspace when a session carries no cwd). Pass `undefined` to
|
|
9
|
+
* clear the override and restore the `getCurrentWorkDir()` default (used by tests).
|
|
10
|
+
*/
|
|
11
|
+
export declare function setAcpShellWorkDir(cwd: string | undefined): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Resolves the working directory the shell tool (`run_shell_command`) spawns in, keeping it aligned
|
|
4
|
+
* with the deepagents filesystem-backend root so the shell and the fs tools (ls/read_file/write_file/
|
|
5
|
+
* edit_file/glob/grep) share ONE path namespace.
|
|
6
|
+
*
|
|
7
|
+
* DEFAULT (local `code`/`chat` runner + AG-UI): `getCurrentWorkDir()`. On these surfaces the fs
|
|
8
|
+
* backend is rooted at `getCurrentWorkDir()` by `GthDeepAgent.init()` (`rootDir: getCurrentWorkDir()`),
|
|
9
|
+
* so the shell already matches it — this is exactly EXT-22 S4. These surfaces NEVER call
|
|
10
|
+
* {@link setAcpShellWorkDir}, so {@link getShellWorkDir} stays `getCurrentWorkDir()` for them.
|
|
11
|
+
*
|
|
12
|
+
* ACP transport override: unlike the local runner, the ACP server does NOT call `init()`; it re-roots
|
|
13
|
+
* ITS per-session fs backend to each `session/new.cwd` (see `gthAcpServer.ts`). The shell tool
|
|
14
|
+
* instances are built ONCE at startup (`buildDeepAgentParams`), so they cannot themselves see the
|
|
15
|
+
* later session cwd. The ACP server therefore updates this module-level override on every
|
|
16
|
+
* `session/new`; the shell reads it at spawn time so it tracks the session fs root.
|
|
17
|
+
*
|
|
18
|
+
* This also removes the EXT-22 S4 `INIT_CWD` hazard on ACP: `getCurrentWorkDir()` prefers `INIT_CWD`,
|
|
19
|
+
* which is stale in the long-lived ACP subprocess (the reason `acpModule` roots the startup workspace
|
|
20
|
+
* at raw `process.cwd()`). Once the override tracks the session cwd, the ACP shell never consults
|
|
21
|
+
* `INIT_CWD`.
|
|
22
|
+
*
|
|
23
|
+
* Scope note: this mirrors the ACP design, which is ALREADY "latest-session-cwd wins" process-wide —
|
|
24
|
+
* `gthAcpServer` clobbers every backend to the newest session cwd. A single process-wide override has
|
|
25
|
+
* the same semantics. The ACP subprocess is dedicated (never co-hosts a local runner / AG-UI), so the
|
|
26
|
+
* override can never leak into a non-ACP surface.
|
|
27
|
+
*/
|
|
28
|
+
import { getCurrentWorkDir } from '@gaunt-sloth/core/utils/systemUtils.js';
|
|
29
|
+
let acpShellWorkDir;
|
|
30
|
+
/**
|
|
31
|
+
* Working directory the shell tool spawns in: the ACP session override when set (ACP transport),
|
|
32
|
+
* otherwise `getCurrentWorkDir()` (local runner + AG-UI, unchanged).
|
|
33
|
+
*/
|
|
34
|
+
export function getShellWorkDir() {
|
|
35
|
+
return acpShellWorkDir ?? getCurrentWorkDir();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* ACP-only: point the shell tool at the current ACP session's fs-backend root (the resolved
|
|
39
|
+
* `session/new.cwd`, or the startup workspace when a session carries no cwd). Pass `undefined` to
|
|
40
|
+
* clear the override and restore the `getCurrentWorkDir()` default (used by tests).
|
|
41
|
+
*/
|
|
42
|
+
export function setAcpShellWorkDir(cwd) {
|
|
43
|
+
acpShellWorkDir = cwd;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=workDir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workDir.js","sourceRoot":"","sources":["../../../src/tools/shell/workDir.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAE3E,IAAI,eAAmC,CAAC;AAExC;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,eAAe,IAAI,iBAAiB,EAAE,CAAC;AAChD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAuB;IACxD,eAAe,GAAG,GAAG,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* EXT-31 — classify and format MCP integration connect/tool-load failures so an expired or invalid
|
|
4
|
+
* credential is SURFACED (named + actionable) instead of silently dropping the integration's tools.
|
|
5
|
+
*
|
|
6
|
+
* The `@langchain/mcp-adapters` client only tags a bare HTTP 401 as an authentication error; a 403
|
|
7
|
+
* (forbidden / expired scope) otherwise falls through as a generic "Failed to connect". This module
|
|
8
|
+
* classifies auth failures independently (401/403 + credential keywords) so a stale token is not
|
|
9
|
+
* mistaken for "this integration was never configured" or an unrelated transient error.
|
|
10
|
+
*
|
|
11
|
+
* Pure + side-effect free: callers do the surfacing (`displayWarning`) with the returned message.
|
|
12
|
+
*/
|
|
13
|
+
/** Coarse classification of an MCP connect/tool-load failure. */
|
|
14
|
+
export type McpConnectErrorKind = 'auth' | 'other';
|
|
15
|
+
/** Options that tune the suggested fix path in the surfaced message. */
|
|
16
|
+
export interface McpConnectFailureOptions {
|
|
17
|
+
/** The server uses OAuth (`authProvider: 'OAuth'`) — suggest re-running the OAuth login. */
|
|
18
|
+
oauth?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Classify an MCP connect/tool-load failure as a credential problem (`auth`) or anything else
|
|
22
|
+
* (`other`). Detection is independent of the adapter (which only tags 401), so a 403 or an
|
|
23
|
+
* "expired token" surfaced by the upstream server is still recognised as auth.
|
|
24
|
+
*/
|
|
25
|
+
export declare function classifyMcpConnectError(error: unknown): McpConnectErrorKind;
|
|
26
|
+
/**
|
|
27
|
+
* Build the user-facing message for a failed MCP integration. For `auth` failures it names the
|
|
28
|
+
* integration, states the credential is expired/invalid, and points at the concrete fix path
|
|
29
|
+
* (OAuth login or the API token / authorization header, plus the `mcpServers` entry in gth config).
|
|
30
|
+
* For `other` failures it surfaces the failure plainly and explicitly says it is NOT an auth error,
|
|
31
|
+
* so an unrelated transient never triggers a spurious "re-authenticate" nudge.
|
|
32
|
+
*
|
|
33
|
+
* Either way the caller degrades gracefully AFTER surfacing this — the integration's tools are
|
|
34
|
+
* dropped for the session, but the user (and the log) now knows why.
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatMcpConnectFailureMessage(serverName: string | undefined, error: unknown, options?: McpConnectFailureOptions): string;
|