@banbolee/dsh-fish-shell 0.6.0 → 0.8.0
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/CHANGELOG.md +74 -0
- package/README.md +310 -13
- package/README.zh.md +99 -4
- package/THIRD-PARTY-NOTICES.md +91 -0
- package/cordis.patch.yml +56 -0
- package/node_modules/@deepseek-ai/cosmokit/LICENSE +21 -0
- package/node_modules/@deepseek-ai/cosmokit/README.md +30 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/index.js +418 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/array.d.ts +15 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/array.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/index.d.ts +13 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/index.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/misc.d.ts +41 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/misc.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/string.d.ts +89 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/string.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/time.d.ts +19 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/time.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/types.d.ts +42 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/types.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/volatile.d.ts +40 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/volatile.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/package.json +32 -0
- package/node_modules/@deepseek-ai/cosmokit/src/array.ts +42 -0
- package/node_modules/@deepseek-ai/cosmokit/src/index.ts +12 -0
- package/node_modules/@deepseek-ai/cosmokit/src/misc.ts +78 -0
- package/node_modules/@deepseek-ai/cosmokit/src/string.ts +113 -0
- package/node_modules/@deepseek-ai/cosmokit/src/time.ts +92 -0
- package/node_modules/@deepseek-ai/cosmokit/src/types.ts +168 -0
- package/node_modules/@deepseek-ai/cosmokit/src/volatile.ts +86 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/LICENSE +21 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/README.i18n.yaml +100 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/README.md +186 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/README.zh.md +186 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/index.js +646 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/background.d.ts +20 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/index.d.ts +32 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/render.d.ts +81 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/package.json +63 -0
- package/node_modules/@deepseek-ai/schemastery/LICENSE +21 -0
- package/node_modules/@deepseek-ai/schemastery/README.md +393 -0
- package/node_modules/@deepseek-ai/schemastery/lib/index.cjs +635 -0
- package/node_modules/@deepseek-ai/schemastery/lib/index.mjs +635 -0
- package/node_modules/@deepseek-ai/schemastery/lib/types/index.d.ts +211 -0
- package/node_modules/@deepseek-ai/schemastery/lib/types/index.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/schemastery/package.json +39 -0
- package/node_modules/@deepseek-ai/schemastery/src/index.ts +962 -0
- package/node_modules/@standard-schema/spec/LICENSE +21 -0
- package/node_modules/@standard-schema/spec/README.md +198 -0
- package/node_modules/@standard-schema/spec/dist/index.cjs +18 -0
- package/node_modules/@standard-schema/spec/dist/index.d.cts +119 -0
- package/node_modules/@standard-schema/spec/dist/index.d.ts +119 -0
- package/node_modules/@standard-schema/spec/dist/index.js +0 -0
- package/node_modules/@standard-schema/spec/package.json +52 -0
- package/package.json +56 -42
- package/terminal-fish.js +18 -0
- package/terminal-tools.js +259 -0
- package/terminal.js +297 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@banbolee/dsh-fish-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Fish shell executors and tool for DeepSeek Harness: run commands with fish instead of bash.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
"./local": "./local.js",
|
|
10
10
|
"./tool": "./tool.js",
|
|
11
11
|
"./policy": "./policy.js",
|
|
12
|
+
"./terminal": "./terminal.js",
|
|
12
13
|
"./terminal-fish": "./terminal-fish.js",
|
|
13
14
|
"./persistent": "./persistent.js",
|
|
15
|
+
"./terminal-tools": "./terminal-tools.js",
|
|
14
16
|
"./package.json": "./package.json"
|
|
15
17
|
},
|
|
16
18
|
"files": [
|
|
@@ -18,62 +20,73 @@
|
|
|
18
20
|
"local.js",
|
|
19
21
|
"tool.js",
|
|
20
22
|
"policy.js",
|
|
23
|
+
"terminal.js",
|
|
21
24
|
"terminal-fish.js",
|
|
22
25
|
"persistent.js",
|
|
26
|
+
"terminal-tools.js",
|
|
23
27
|
"job-kind.d.ts",
|
|
24
28
|
"cordis.patch.yml",
|
|
25
29
|
"README.md",
|
|
26
|
-
"README.zh.md"
|
|
30
|
+
"README.zh.md",
|
|
31
|
+
"THIRD-PARTY-NOTICES.md",
|
|
32
|
+
"CHANGELOG.md"
|
|
27
33
|
],
|
|
28
34
|
"dsh": {
|
|
29
35
|
"bundle": {
|
|
30
36
|
"patch": "./cordis.patch.yml"
|
|
31
37
|
}
|
|
32
38
|
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@deepseek-ai/dsh-tool-terminal": "0.2.0-rc.1"
|
|
41
|
+
},
|
|
42
|
+
"bundledDependencies": [
|
|
43
|
+
"@deepseek-ai/dsh-tool-terminal"
|
|
44
|
+
],
|
|
33
45
|
"peerDependencies": {
|
|
34
46
|
"@deepseek-ai/cordis": "~4.0.4",
|
|
35
|
-
"@deepseek-ai/dsh-agent": "^0.
|
|
36
|
-
"@deepseek-ai/dsh-bash-local": "^0.
|
|
37
|
-
"@deepseek-ai/dsh-bash-sandbox": "^0.
|
|
38
|
-
"@deepseek-ai/dsh-home-paths": "^0.
|
|
39
|
-
"@deepseek-ai/dsh-jobs": "^0.
|
|
40
|
-
"@deepseek-ai/dsh-llm": "^0.
|
|
41
|
-
"@deepseek-ai/dsh-
|
|
42
|
-
"@deepseek-ai/dsh-sandbox
|
|
43
|
-
"@deepseek-ai/dsh-
|
|
44
|
-
"@deepseek-ai/dsh-
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
46
|
-
"@deepseek-ai/dsh-shell
|
|
47
|
-
"@deepseek-ai/dsh-
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-terminal
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
47
|
+
"@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-bash-local": "^0.2.0-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-bash-sandbox": "^0.2.0-rc.1",
|
|
50
|
+
"@deepseek-ai/dsh-home-paths": "^0.2.0-rc.1",
|
|
51
|
+
"@deepseek-ai/dsh-jobs": "^0.2.0-rc.1",
|
|
52
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
|
|
53
|
+
"@deepseek-ai/dsh-pwsh-local": "^0.2.0-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-sandbox": "^0.2.0-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.2.0-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-scope": "^0.2.0-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-session-projection": "^0.2.0-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-shell": "^0.2.0-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-shell-env": "^0.2.0-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-subprocess": "^0.2.0-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-terminal": "^0.2.0-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-terminal-bash": "^0.2.0-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-user-approval": "^0.2.0-rc.1"
|
|
53
66
|
},
|
|
54
67
|
"devDependencies": {
|
|
55
68
|
"@deepseek-ai/cordis": "~4.0.4",
|
|
56
|
-
"@deepseek-ai/dsh-agent": "^0.
|
|
57
|
-
"@deepseek-ai/dsh-bash-local": "^0.
|
|
58
|
-
"@deepseek-ai/dsh-bash-sandbox": "^0.
|
|
59
|
-
"@deepseek-ai/dsh-home-paths": "^0.
|
|
60
|
-
"@deepseek-ai/dsh-invariants": "^0.
|
|
61
|
-
"@deepseek-ai/dsh-jobs": "^0.
|
|
62
|
-
"@deepseek-ai/dsh-llm": "^0.
|
|
63
|
-
"@deepseek-ai/dsh-pwsh-local": "^0.
|
|
64
|
-
"@deepseek-ai/dsh-sandbox": "^0.
|
|
65
|
-
"@deepseek-ai/dsh-sandbox-policy": "^0.
|
|
66
|
-
"@deepseek-ai/dsh-scope": "^0.
|
|
67
|
-
"@deepseek-ai/dsh-session-projection": "^0.
|
|
68
|
-
"@deepseek-ai/dsh-shell": "^0.
|
|
69
|
-
"@deepseek-ai/dsh-shell-env": "^0.
|
|
70
|
-
"@deepseek-ai/dsh-subprocess": "^0.
|
|
71
|
-
"@deepseek-ai/dsh-subprocess-local": "^0.
|
|
72
|
-
"@deepseek-ai/dsh-system-prompt": "^0.
|
|
73
|
-
"@deepseek-ai/dsh-terminal": "^0.
|
|
74
|
-
"@deepseek-ai/dsh-terminal-bash": "^0.
|
|
75
|
-
"@deepseek-ai/dsh-tools": "^0.
|
|
76
|
-
"@deepseek-ai/dsh-user-approval": "^0.
|
|
69
|
+
"@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-bash-local": "^0.2.0-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-bash-sandbox": "^0.2.0-rc.1",
|
|
72
|
+
"@deepseek-ai/dsh-home-paths": "^0.2.0-rc.1",
|
|
73
|
+
"@deepseek-ai/dsh-invariants": "^0.2.0-rc.1",
|
|
74
|
+
"@deepseek-ai/dsh-jobs": "^0.2.0-rc.1",
|
|
75
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
|
|
76
|
+
"@deepseek-ai/dsh-pwsh-local": "^0.2.0-rc.1",
|
|
77
|
+
"@deepseek-ai/dsh-sandbox": "^0.2.0-rc.1",
|
|
78
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.2.0-rc.1",
|
|
79
|
+
"@deepseek-ai/dsh-scope": "^0.2.0-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-session-projection": "^0.2.0-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-shell": "^0.2.0-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-shell-env": "^0.2.0-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-subprocess": "^0.2.0-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-subprocess-local": "^0.2.0-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.1",
|
|
86
|
+
"@deepseek-ai/dsh-terminal": "^0.2.0-rc.1",
|
|
87
|
+
"@deepseek-ai/dsh-terminal-bash": "^0.2.0-rc.1",
|
|
88
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
|
|
89
|
+
"@deepseek-ai/dsh-user-approval": "^0.2.0-rc.1",
|
|
77
90
|
"yaml": "^2.8.0"
|
|
78
91
|
},
|
|
79
92
|
"keywords": [
|
|
@@ -86,7 +99,8 @@
|
|
|
86
99
|
"license": "MIT",
|
|
87
100
|
"publishConfig": {
|
|
88
101
|
"access": "public",
|
|
89
|
-
"registry": "https://registry.npmjs.org"
|
|
102
|
+
"registry": "https://registry.npmjs.org",
|
|
103
|
+
"provenance": true
|
|
90
104
|
},
|
|
91
105
|
"engines": {
|
|
92
106
|
"node": ">=22"
|
package/terminal-fish.js
CHANGED
|
@@ -72,6 +72,11 @@ const DEFAULT_PTY_CONFIG = {
|
|
|
72
72
|
exactProbeAfterMs: 150,
|
|
73
73
|
idleSilenceMs: 3e3,
|
|
74
74
|
handoffGraceMs: 500,
|
|
75
|
+
// The official default is `0` (tail grace DISABLED, i.e. the pre-`promptTailGraceMs`
|
|
76
|
+
// behaviour). This backend keeps that default rather than inventing a positive
|
|
77
|
+
// one: the fish prompt is verified to arrive whole under `TERM=dumb`, so the
|
|
78
|
+
// tolerance is only there for a caller that asks for it.
|
|
79
|
+
promptTailGraceMs: 0,
|
|
75
80
|
timeoutMs: 3e4,
|
|
76
81
|
disposeGraceMs: 3e3,
|
|
77
82
|
}
|
|
@@ -100,6 +105,7 @@ const DEFAULT_PTY_CONFIG = {
|
|
|
100
105
|
* @property {number} exactProbeAfterMs - delay before exact syscall probes.
|
|
101
106
|
* @property {number} idleSilenceMs - silence that yields `inferred_idle`.
|
|
102
107
|
* @property {number} handoffGraceMs - extra wait for foreground handoff.
|
|
108
|
+
* @property {number} promptTailGraceMs - extra wait for a controlled prompt prefix to complete; `0` disables the tail grace.
|
|
103
109
|
* @property {number} timeoutMs - readiness deadline for one startup sequence.
|
|
104
110
|
* @property {number} disposeGraceMs - SIGTERM→SIGKILL grace for the PTY session.
|
|
105
111
|
*/
|
|
@@ -171,12 +177,24 @@ export function validateConfig(config) {
|
|
|
171
177
|
if (resolved.backendType.length === 0) throw new Error('terminal-fish: backendType must be non-empty')
|
|
172
178
|
if (resolved.shellPath.length === 0) throw new Error('terminal-fish: shellPath must be non-empty')
|
|
173
179
|
for (const [field, value] of Object.entries(resolved)) {
|
|
180
|
+
// `promptTailGraceMs` is the one duration that legally reads `0` (it means
|
|
181
|
+
// "no tail grace", which is the official default), so it is checked on its
|
|
182
|
+
// own below instead of by the positive-integer rule.
|
|
183
|
+
if (field === 'promptTailGraceMs') continue
|
|
174
184
|
if (typeof value === 'number' && (!Number.isSafeInteger(value) || value <= 0)) {
|
|
175
185
|
throw new Error(`terminal-fish: ${field} must be a positive safe integer`)
|
|
176
186
|
}
|
|
177
187
|
}
|
|
188
|
+
if (!Number.isSafeInteger(resolved.promptTailGraceMs) || resolved.promptTailGraceMs < 0) {
|
|
189
|
+
throw new Error('terminal-fish: promptTailGraceMs must be a non-negative safe integer (0 disables the prompt tail grace)')
|
|
190
|
+
}
|
|
178
191
|
if (resolved.maxReadBytes > resolved.scrollbackMaxBytes) throw new Error('terminal-fish: maxReadBytes must not exceed scrollbackMaxBytes')
|
|
179
192
|
if (resolved.handoffGraceMs < resolved.pollIntervalMs) throw new Error('terminal-fish: handoffGraceMs must be at least pollIntervalMs so one readiness poll runs inside the grace window')
|
|
193
|
+
// A nonzero tolerance shorter than one poll cannot observe the rest of the
|
|
194
|
+
// prompt, so it would be a silent no-op; the official schema rejects it too.
|
|
195
|
+
if (resolved.promptTailGraceMs !== 0 && resolved.promptTailGraceMs < resolved.pollIntervalMs) {
|
|
196
|
+
throw new Error('terminal-fish: promptTailGraceMs must be zero or at least pollIntervalMs so a nonzero tail grace covers one readiness poll')
|
|
197
|
+
}
|
|
180
198
|
}
|
|
181
199
|
|
|
182
200
|
/**
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `terminalTools` policy for the L2 interactive terminal surface of
|
|
3
|
+
* `@banbolee/dsh-fish-shell` — the D6 switch, `'allow' | 'deny'`.
|
|
4
|
+
*
|
|
5
|
+
* The bundle's patch appends a `fish-terminal-group` insert row that mounts
|
|
6
|
+
* the official `@deepseek-ai/dsh-terminal` registry, the official
|
|
7
|
+
* `@deepseek-ai/dsh-terminal-bash` backend driven with fish argv
|
|
8
|
+
* (`--no-config -i -C <prompt setup>`), and the official
|
|
9
|
+
* `@deepseek-ai/dsh-tool-terminal`, whose six `terminal_*` tools register
|
|
10
|
+
* into the HOST tool registry (the group isolates `terminals`, not `tools`).
|
|
11
|
+
* This module is the only entry the plugin itself adds for L2: a per-agent
|
|
12
|
+
* switch that can take those six tools away again.
|
|
13
|
+
*
|
|
14
|
+
* - `terminalTools: 'allow'` (the default, matching every other tool in the
|
|
15
|
+
* deployment) does nothing at all — `apply` returns before registering a
|
|
16
|
+
* single listener, so an allowed mount has zero runtime footprint.
|
|
17
|
+
* - `terminalTools: 'deny'` restricts the six tools for every live agent at
|
|
18
|
+
* the `agent/created` publication edge and on every `tools/change`. The
|
|
19
|
+
* official `tools.restrict` is the only enforcement point, and it REJECTS
|
|
20
|
+
* names that are not registered at that moment, so an attempt made before
|
|
21
|
+
* the terminal tools exist fails and is retried on the next
|
|
22
|
+
* `tools/change` (the registry announces every registration and scoped
|
|
23
|
+
* restriction through that event).
|
|
24
|
+
*
|
|
25
|
+
* Every `apply()` call owns its ENTIRE state — its own live-agent set, its own
|
|
26
|
+
* per-agent disposers, its own reconcile flags, and its own
|
|
27
|
+
* installing/warned/removed bookkeeping. Two mounts sharing one context (two
|
|
28
|
+
* bundle rows) or two profiles living in one process therefore cannot dedupe
|
|
29
|
+
* into, lift, or orphan each other's restrictions: each mount installs its own
|
|
30
|
+
* restriction and that mount alone lifts it on teardown, so no restriction
|
|
31
|
+
* outlives the mount that owns it.
|
|
32
|
+
*
|
|
33
|
+
* The governed names are the EXPLICIT six-name enumeration measured on a real
|
|
34
|
+
* profile (`TERMINAL_TOOL_NAMES`); a `terminal_*` wildcard is deliberately not
|
|
35
|
+
* used, so an upstream seventh tool cannot silently pass the policy — the
|
|
36
|
+
* contract test compares this constant with the bundled
|
|
37
|
+
* `@deepseek-ai/dsh-tool-terminal` surface instead.
|
|
38
|
+
*
|
|
39
|
+
* Boundaries: this policy registers no tool, wraps/replaces no host object,
|
|
40
|
+
* monkey-patches nothing, and offers no privilege-escalation entry point. It
|
|
41
|
+
* only narrows an agent's tool surface through the public
|
|
42
|
+
* `agent.ctx.tools.restrict({ deny })` API. The interactive sessions
|
|
43
|
+
* themselves inherit the host's confinement (a `workspace-write` profile is
|
|
44
|
+
* fenced by the host sandbox).
|
|
45
|
+
*
|
|
46
|
+
* @module @banbolee/dsh-fish-shell/terminal-tools
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/** Cordis plugin name (the patch row id is `fish-terminal-tools`). */
|
|
50
|
+
export const name = 'fish-terminal-tools-policy'
|
|
51
|
+
/** Required service: the tool registry the six names are restricted in. */
|
|
52
|
+
export const inject = ['tools']
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The six `terminal_*` tools mounted by the bundle's `fish-terminal-group`,
|
|
56
|
+
* enumerated explicitly (never a `terminal_*` pattern): `terminal_open`,
|
|
57
|
+
* `terminal_send`, `terminal_read`, `terminal_signal`, `terminal_close`,
|
|
58
|
+
* `terminal_list`.
|
|
59
|
+
*/
|
|
60
|
+
export const TERMINAL_TOOL_NAMES = Object.freeze([
|
|
61
|
+
'terminal_open',
|
|
62
|
+
'terminal_send',
|
|
63
|
+
'terminal_read',
|
|
64
|
+
'terminal_signal',
|
|
65
|
+
'terminal_close',
|
|
66
|
+
'terminal_list',
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Plugin configuration schema: one switch. A plain object implementing the
|
|
71
|
+
* standard-schema interface (no external validator): an absent
|
|
72
|
+
* `terminalTools` defaults to `allow`; any value other than `allow`/`deny` is
|
|
73
|
+
* rejected at mount time.
|
|
74
|
+
*/
|
|
75
|
+
export const Config = {
|
|
76
|
+
'~standard': {
|
|
77
|
+
version: /** @type {1} */ (1),
|
|
78
|
+
vendor: '@banbolee/dsh-fish-shell/terminal-tools',
|
|
79
|
+
/**
|
|
80
|
+
* Normalize one mount's raw configuration.
|
|
81
|
+
* @param {unknown} value
|
|
82
|
+
*/
|
|
83
|
+
validate(value) {
|
|
84
|
+
const mode = /** @type {{ terminalTools?: unknown }} */ (value ?? {}).terminalTools ?? 'allow'
|
|
85
|
+
if (mode !== 'allow' && mode !== 'deny') {
|
|
86
|
+
throw new TypeError("banbo-fish-terminal-tools config.terminalTools must be 'allow' or 'deny'")
|
|
87
|
+
}
|
|
88
|
+
return { value: { terminalTools: mode } }
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const PLUGIN_TAG = '@banbolee/dsh-fish-shell'
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One agent as this policy reads it: the harness `Agent` viewed through the
|
|
97
|
+
* two members it keys on — its session id (for warnings) and its agent-scoped
|
|
98
|
+
* context (where the restriction lands). The full runtime `Agent` face is
|
|
99
|
+
* structurally assignable to this view.
|
|
100
|
+
* @typedef {{ id?: string, ctx: import('@deepseek-ai/cordis').Context }} PolicyAgent
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Mount one `deny` policy on `ctx`, owning every piece of state it needs.
|
|
105
|
+
* Nothing in here is shared with another mount of this module.
|
|
106
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx - the harness context this mount lives on.
|
|
107
|
+
*/
|
|
108
|
+
function mountDenyPolicy(ctx) {
|
|
109
|
+
/** Agents this mount is currently restricting (guards the synchronous
|
|
110
|
+
* `tools/change` re-entry that `tools.restrict` itself fires).
|
|
111
|
+
* @type {WeakSet<PolicyAgent>} */
|
|
112
|
+
const installing = new WeakSet()
|
|
113
|
+
/** Agents this mount already warned about a failed restriction (warn once
|
|
114
|
+
* per agent per mount).
|
|
115
|
+
* @type {WeakSet<PolicyAgent>} */
|
|
116
|
+
const warned = new WeakSet()
|
|
117
|
+
/** Agents this mount saw disposed: a `tools/change` fired by the uninstall's
|
|
118
|
+
* own disposer must not re-restrict a dying agent.
|
|
119
|
+
* @type {WeakSet<PolicyAgent>} */
|
|
120
|
+
const removed = new WeakSet()
|
|
121
|
+
/** The exact restriction disposer THIS mount installed per agent.
|
|
122
|
+
* @type {WeakMap<PolicyAgent, () => void>} */
|
|
123
|
+
const restrictions = new WeakMap()
|
|
124
|
+
/** This mount's agents with a restriction installed; traversable so its own
|
|
125
|
+
* teardown lifts exactly its own restrictions (a WeakMap would be
|
|
126
|
+
* unreachable).
|
|
127
|
+
* @type {Set<PolicyAgent>} */
|
|
128
|
+
const liveAgents = new Set()
|
|
129
|
+
/** This mount's reconcile re-entrancy guard: a `tools/change` arriving while
|
|
130
|
+
* a reconcile round is in flight marks `reconcilePending` instead of starting
|
|
131
|
+
* a nested round; the round in flight runs exactly one coalesced catch-up
|
|
132
|
+
* round afterwards. */
|
|
133
|
+
let reconciling = false
|
|
134
|
+
let reconcilePending = false
|
|
135
|
+
/** Set while THIS mount's teardown runs so its own listeners (still
|
|
136
|
+
* registered — they were created before the teardown effect, which cordis
|
|
137
|
+
* disposes first) cannot re-install a restriction being lifted. */
|
|
138
|
+
let disposed = false
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Deny the six terminal tools for one agent, once. A failure (the tools are
|
|
142
|
+
* not registered in this profile yet, so `tools.restrict` rejects the unknown
|
|
143
|
+
* names) is warned once per agent and retried on the next `tools/change`.
|
|
144
|
+
* @param {PolicyAgent} agent - the live agent (its `ctx` is the agent's scoped context).
|
|
145
|
+
*/
|
|
146
|
+
const install = (agent) => {
|
|
147
|
+
if (installing.has(agent) || removed.has(agent)) return
|
|
148
|
+
if (restrictions.has(agent)) return
|
|
149
|
+
installing.add(agent)
|
|
150
|
+
try {
|
|
151
|
+
// A fresh array: the frozen constant stays the policy's single source of
|
|
152
|
+
// names, and the toolbox never receives the shared instance.
|
|
153
|
+
const restriction = agent.ctx.tools.restrict({ deny: [...TERMINAL_TOOL_NAMES] })
|
|
154
|
+
restrictions.set(agent, restriction)
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (!warned.has(agent)) {
|
|
157
|
+
warned.add(agent)
|
|
158
|
+
console.warn(
|
|
159
|
+
`${PLUGIN_TAG}: could not deny the terminal tools for agent ${agent.id ?? '(unnamed)'} yet `
|
|
160
|
+
+ '(they are not registered in this profile so far); retrying on the next tools/change:',
|
|
161
|
+
String(error),
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
installing.delete(agent)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Lift one agent's restriction, if THIS mount installed one. The exact
|
|
171
|
+
* disposer is removed from the state BEFORE it runs: lifting a restriction
|
|
172
|
+
* synchronously fires `tools/change` as well.
|
|
173
|
+
* @param {PolicyAgent} agent - the agent whose restriction is lifted.
|
|
174
|
+
* @param {string} label - the operation name in the warning.
|
|
175
|
+
*/
|
|
176
|
+
const lift = (agent, label) => {
|
|
177
|
+
const restriction = restrictions.get(agent)
|
|
178
|
+
if (restriction === undefined) return
|
|
179
|
+
restrictions.delete(agent)
|
|
180
|
+
try {
|
|
181
|
+
restriction()
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.warn(`${PLUGIN_TAG}: could not ${label} terminal tools for agent ${agent.id ?? '(unnamed)'}:`, String(error))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Ensure every live agent carries this mount's restriction (no-op when the
|
|
189
|
+
* `agents` service is absent). Nested calls (the restriction's own
|
|
190
|
+
* `tools/change`) are absorbed into at most ONE coalesced catch-up round, so
|
|
191
|
+
* the dispatch count stays bounded regardless of how many agents are live.
|
|
192
|
+
* @param {boolean} [catchUp] - whether this round is the coalesced catch-up (never re-triggers one).
|
|
193
|
+
*/
|
|
194
|
+
const reconcile = (catchUp = false) => {
|
|
195
|
+
if (reconciling) {
|
|
196
|
+
reconcilePending = true
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
reconciling = true
|
|
200
|
+
try {
|
|
201
|
+
for (const agent of ctx.get('agents')?.list() ?? []) {
|
|
202
|
+
liveAgents.add(agent)
|
|
203
|
+
install(agent)
|
|
204
|
+
}
|
|
205
|
+
} finally {
|
|
206
|
+
reconciling = false
|
|
207
|
+
}
|
|
208
|
+
if (reconcilePending && !catchUp) {
|
|
209
|
+
reconcilePending = false
|
|
210
|
+
reconcile(true)
|
|
211
|
+
} else {
|
|
212
|
+
reconcilePending = false
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// `agent/created` fires synchronously at the agent publication boundary
|
|
217
|
+
// (before the agent's first turn); a synchronous listener error would
|
|
218
|
+
// propagate into the publish path, but `install` contains its own failures.
|
|
219
|
+
ctx.on('agent/created', ({ agent }) => {
|
|
220
|
+
if (disposed) return
|
|
221
|
+
liveAgents.add(agent)
|
|
222
|
+
install(agent)
|
|
223
|
+
})
|
|
224
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
225
|
+
if (disposed) return
|
|
226
|
+
removed.add(agent)
|
|
227
|
+
liveAgents.delete(agent)
|
|
228
|
+
lift(agent, 'lift the restriction on the')
|
|
229
|
+
})
|
|
230
|
+
ctx.on('tools/change', () => {
|
|
231
|
+
if (disposed) return
|
|
232
|
+
reconcile()
|
|
233
|
+
})
|
|
234
|
+
// Plugin-owned teardown (unload/disable/HMR): registered LAST so cordis
|
|
235
|
+
// disposes it FIRST (effects run in reverse registration order) — while this
|
|
236
|
+
// loop lifts THIS mount's restrictions, `disposed` above makes its listeners
|
|
237
|
+
// no-ops, so lifting cannot re-install.
|
|
238
|
+
ctx.effect(() => () => {
|
|
239
|
+
disposed = true
|
|
240
|
+
for (const agent of [...liveAgents]) lift(agent, 'lift the restriction on the')
|
|
241
|
+
liveAgents.clear()
|
|
242
|
+
}, 'fish terminal-tools policy teardown')
|
|
243
|
+
reconcile()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Cordis plugin entry. With `terminalTools: 'deny'` the six `terminal_*` tools
|
|
248
|
+
* are taken away from every live agent — at the `agent/created` publication
|
|
249
|
+
* edge, on every `tools/change`, and for agents that already exist (plugin
|
|
250
|
+
* hot-reload) — and every restriction this mount installed is lifted on its
|
|
251
|
+
* teardown. With the default `allow` nothing is registered and the function
|
|
252
|
+
* returns.
|
|
253
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx - the harness context.
|
|
254
|
+
* @param {{ terminalTools?: 'allow' | 'deny' }} [config] - validated plugin config.
|
|
255
|
+
*/
|
|
256
|
+
export function apply(ctx, config) {
|
|
257
|
+
if (config?.terminalTools !== 'deny') return
|
|
258
|
+
mountDenyPolicy(ctx)
|
|
259
|
+
}
|