dsh-ops 0.2.2 → 0.2.4
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 +16 -0
- package/PROVENANCE.md +35 -0
- package/README.en.md +31 -9
- package/README.md +20 -6
- package/README.zh.md +20 -6
- package/cordis.patch.yml +20 -155
- package/docs/manual-validation.md +10 -5
- package/docs/schema-current.json +20 -13
- package/docs/schema-measurement.md +29 -9
- package/dsh-plugin.json +38 -10
- package/icon.png +0 -0
- package/lib/components/background/icon.png +0 -0
- package/lib/components/background/index.js +3 -0
- package/lib/components/background/locale/en.json +6 -0
- package/lib/components/background/locale/zh.json +6 -0
- package/lib/components/background/package.json +6 -0
- package/lib/components/file/icon.png +0 -0
- package/lib/components/file/index.js +3 -0
- package/lib/components/file/locale/en.json +6 -0
- package/lib/components/file/locale/zh.json +6 -0
- package/lib/components/file/package.json +6 -0
- package/lib/components/shell/icon.png +0 -0
- package/lib/components/shell/index.js +3 -0
- package/lib/components/shell/locale/en.json +6 -0
- package/lib/components/shell/locale/zh.json +6 -0
- package/lib/components/shell/package.json +6 -0
- package/lib/index.js +46 -32
- package/lib/policy.js +16 -7
- package/lib/runtime-pool.js +42 -0
- package/lib/session-pwsh.js +31 -0
- package/lib/tools.js +64 -16
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +20 -5
- package/icon.svg +0 -12
package/dsh-plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifestVersion": "0.15",
|
|
4
4
|
"id": "dsh-ops.fastctx",
|
|
5
5
|
"name": "dsh-ops",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.4",
|
|
7
7
|
"license": "MIT AND Apache-2.0",
|
|
8
8
|
"facets": {
|
|
9
9
|
"host": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"contributes": {
|
|
40
40
|
"x-display": [
|
|
41
41
|
{
|
|
42
|
-
"icon": "./icon.
|
|
43
|
-
"title": "
|
|
44
|
-
"description": "
|
|
42
|
+
"icon": "./icon.png",
|
|
43
|
+
"title": "dsh-ops",
|
|
44
|
+
"description": "Provides bash, PowerShell 7, and a suite of high-performance tools written in Rust for Windows DSH to enhance token efficiency",
|
|
45
45
|
"locales": [
|
|
46
46
|
"locale/en.json",
|
|
47
47
|
"locale/zh.json"
|
|
@@ -50,9 +50,19 @@
|
|
|
50
50
|
],
|
|
51
51
|
"x-prompt-sections": [
|
|
52
52
|
{
|
|
53
|
-
"name": "dsh-ops:repository-tooling",
|
|
53
|
+
"name": "dsh-ops:repository-tooling:file",
|
|
54
54
|
"order": 3250,
|
|
55
|
-
"summary": "
|
|
55
|
+
"summary": "File operations only; follows the file component visibility."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "dsh-ops:repository-tooling:shell",
|
|
59
|
+
"order": 3251,
|
|
60
|
+
"summary": "Prefer bash, use bundled PowerShell 7 for Windows-native functionality; removed when shell component is off."
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "dsh-ops:repository-tooling:background",
|
|
64
|
+
"order": 3252,
|
|
65
|
+
"summary": "Owned background tasks only; absent by default."
|
|
56
66
|
}
|
|
57
67
|
],
|
|
58
68
|
"x-tool-namespaces": [
|
|
@@ -66,21 +76,39 @@
|
|
|
66
76
|
"grep",
|
|
67
77
|
"glob",
|
|
68
78
|
"replace",
|
|
69
|
-
"run",
|
|
70
79
|
"run_background",
|
|
71
80
|
"job_output",
|
|
72
81
|
"job_list",
|
|
73
82
|
"job_kill"
|
|
74
83
|
],
|
|
75
|
-
"shellPermission": "enableShellTools AND per-session sandboxPolicy danger-full-access"
|
|
84
|
+
"shellPermission": "background component enabled AND enableShellTools AND per-session sandboxPolicy danger-full-access"
|
|
76
85
|
},
|
|
77
86
|
{
|
|
78
87
|
"serverName": "dsh-ops",
|
|
79
88
|
"transport": "in-process",
|
|
80
89
|
"source": "the plugin's bash layer via host subprocess",
|
|
81
90
|
"publicPrefix": "ops_",
|
|
82
|
-
"tools": [
|
|
83
|
-
|
|
91
|
+
"tools": [
|
|
92
|
+
"bash"
|
|
93
|
+
],
|
|
94
|
+
"shellPermission": "shell component enabled AND publishBashTool AND per-session sandboxPolicy danger-full-access"
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"x-components": [
|
|
98
|
+
{
|
|
99
|
+
"id": "dsh-ops-shell",
|
|
100
|
+
"entry": "dsh-ops/shell",
|
|
101
|
+
"defaultEnabled": true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "dsh-ops-file",
|
|
105
|
+
"entry": "dsh-ops/file",
|
|
106
|
+
"defaultEnabled": true
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"id": "dsh-ops-background",
|
|
110
|
+
"entry": "dsh-ops/background",
|
|
111
|
+
"defaultEnabled": false
|
|
84
112
|
}
|
|
85
113
|
]
|
|
86
114
|
},
|
package/icon.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** dsh-ops host half. Own registrations only; no @deepseek-ai value imports. */
|
|
2
2
|
import { PACKAGE_ROOT, probeBinary, resolveBinary } from './binary.js'
|
|
3
3
|
import { resolveConfig } from './config.js'
|
|
4
|
-
import { SECTION_ORDERS, TOOLING_SECTION, hostShellRefusal, renderToolingPolicy } from './policy.js'
|
|
4
|
+
import { SECTION_ORDERS, TOOLING_SECTION, BACKGROUND_TOOLS, FILE_TOOLS, BASH_TOOL, publicToolName, hostShellRefusal, renderToolingPolicy } from './policy.js'
|
|
5
5
|
import { resolveShells } from './shells.js'
|
|
6
6
|
import { mountSessionBash } from './session-shells.js'
|
|
7
|
+
import { mountBundledPwsh } from './session-pwsh.js'
|
|
7
8
|
import { FastCtxTools } from './tools.js'
|
|
8
9
|
|
|
9
10
|
export const name = 'dsh-ops'
|
|
@@ -54,30 +55,34 @@ function installHostShellDenial(ctx, config) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
async function mountServer(ctx, config, runtime) {
|
|
58
|
+
async function mountServer(ctx, config, runtime, component) {
|
|
58
59
|
if (!runtime) return
|
|
59
|
-
const
|
|
60
|
+
const background = component === 'background'
|
|
61
|
+
const surface = new FastCtxTools({ ctx, config, runtime,
|
|
62
|
+
fileTools: !background, commandTools: background ? BACKGROUND_TOOLS : [], shared: true,
|
|
63
|
+
report: (message, level) => report(ctx, message, level) })
|
|
60
64
|
ctx.effect(() => () => surface.stop(), 'dsh-ops: fastctx server')
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
surface.refreshAgents()
|
|
72
|
-
authorityCtx.effect(() => () => {
|
|
73
|
-
surface.authorityAvailable = false
|
|
65
|
+
if (background) {
|
|
66
|
+
ctx.on('agent/created', ({ agent }) => surface.attachAgent(agent))
|
|
67
|
+
ctx.on('agent/disposed', ({ agent }) => surface.detachAgent(agent))
|
|
68
|
+
ctx.on('session/event', (_session, event) => {
|
|
69
|
+
if (event.type === 'sandbox/mode') surface.refreshAgents()
|
|
70
|
+
})
|
|
71
|
+
// Background command layers follow authoritative per-session permission.
|
|
72
|
+
surface.authorityAvailable = false
|
|
73
|
+
ctx.inject(['sandboxPolicy'], authorityCtx => {
|
|
74
|
+
surface.authorityAvailable = true
|
|
74
75
|
surface.refreshAgents()
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
authorityCtx.effect(() => () => {
|
|
77
|
+
surface.authorityAvailable = false
|
|
78
|
+
surface.refreshAgents()
|
|
79
|
+
}, 'dsh-ops: permission authority')
|
|
80
|
+
})
|
|
81
|
+
for (const agent of ctx.get('agents')?.list?.() ?? []) surface.attachAgent(agent)
|
|
82
|
+
}
|
|
78
83
|
try {
|
|
79
84
|
const tools = await surface.start()
|
|
80
|
-
report(ctx, `dsh-ops: FastCtx ${runtime.version} ready; ${tools.length}
|
|
85
|
+
report(ctx, `dsh-ops: FastCtx ${runtime.version} ready; ${component} component, ${tools.length} global tools.`, 'info')
|
|
81
86
|
} catch (error) {
|
|
82
87
|
report(ctx, `dsh-ops: FastCtx unavailable: ${error.message}`)
|
|
83
88
|
if (config.required) throw error
|
|
@@ -85,24 +90,33 @@ async function mountServer(ctx, config, runtime) {
|
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
export function
|
|
93
|
+
export function applyComponent(root, rawConfig, component) {
|
|
89
94
|
const config = resolveConfig(rawConfig)
|
|
90
|
-
const runtime = prepareRuntime(root, config)
|
|
91
|
-
const shells = resolveShells(config)
|
|
95
|
+
const runtime = component === 'shell' ? undefined : prepareRuntime(root, config)
|
|
96
|
+
const shells = component === 'shell' ? resolveShells(config) : undefined
|
|
97
|
+
const owned = component === 'shell' ? [BASH_TOOL, 'pwsh']
|
|
98
|
+
: component === 'file' ? FILE_TOOLS.map(publicToolName) : BACKGROUND_TOOLS.map(publicToolName)
|
|
99
|
+
if (component === 'file') owned.push('read_image')
|
|
92
100
|
if (config.promptPolicy) root.inject(['systemPrompt'], ctx => {
|
|
93
101
|
ctx.effect(() => ctx.systemPrompt.section({
|
|
94
|
-
name: TOOLING_SECTION
|
|
95
|
-
order: SECTION_ORDERS[TOOLING_SECTION],
|
|
102
|
+
name: `${TOOLING_SECTION}:${component}`,
|
|
103
|
+
order: SECTION_ORDERS[TOOLING_SECTION] + (component === 'shell' ? 1 : component === 'background' ? 2 : 0),
|
|
96
104
|
interpolate: false,
|
|
97
105
|
text: context => renderToolingPolicy({
|
|
98
|
-
published: visibleTools(ctx, context.agent)
|
|
106
|
+
published: visibleTools(ctx, context.agent).filter(name => owned.includes(name)),
|
|
107
|
+
shellComponent: component === 'shell', pwsh7: shells?.pwsh.available ?? false,
|
|
99
108
|
extraGuidance: config.extraGuidance,
|
|
100
109
|
}),
|
|
101
|
-
}),
|
|
102
|
-
})
|
|
103
|
-
root.inject(['tools'], ctx => {
|
|
104
|
-
mountSessionBash(ctx, config, shells)
|
|
105
|
-
if (config.shellPolicy === 'deny-host-shell') installHostShellDenial(ctx, config)
|
|
106
|
-
return mountServer(ctx, config, runtime)
|
|
110
|
+
}), `dsh-ops: ${component} tooling`)
|
|
107
111
|
})
|
|
112
|
+
if (component === 'shell') {
|
|
113
|
+
mountBundledPwsh(root, shells)
|
|
114
|
+
root.inject(['tools'], ctx => {
|
|
115
|
+
mountSessionBash(ctx, config, shells)
|
|
116
|
+
if (config.shellPolicy === 'deny-host-shell') installHostShellDenial(ctx, config)
|
|
117
|
+
})
|
|
118
|
+
} else root.inject(['tools'], ctx => mountServer(ctx, config, runtime, component))
|
|
108
119
|
}
|
|
120
|
+
|
|
121
|
+
// Backward-compatible module entry. The bundle uses independently toggled rows.
|
|
122
|
+
export function apply(root, rawConfig) { return applyComponent(root, rawConfig, 'file') }
|
package/lib/policy.js
CHANGED
|
@@ -5,7 +5,8 @@ export const HOST_SHELL_SECTION = 'dsh-ops:host-shell-policy'
|
|
|
5
5
|
export const SECTION_ORDERS = Object.freeze({ [TOOLING_SECTION]: 3250 })
|
|
6
6
|
export const TOOL_PREFIX = 'ops_'
|
|
7
7
|
export const FILE_TOOLS = Object.freeze(['inspect_local_file', 'grep', 'glob', 'replace'])
|
|
8
|
-
export const
|
|
8
|
+
export const BACKGROUND_TOOLS = Object.freeze(['run_background', 'job_output', 'job_list', 'job_kill'])
|
|
9
|
+
export const SHELL_TOOLS = Object.freeze(['run', ...BACKGROUND_TOOLS])
|
|
9
10
|
// The plugin's second layer executes POSIX commands; pwsh7 uses the host tool.
|
|
10
11
|
export const IN_PROCESS_TOOLS = Object.freeze(['bash'])
|
|
11
12
|
export function publicToolName(rawName) { return `${TOOL_PREFIX}${rawName}` }
|
|
@@ -23,17 +24,24 @@ export function ladderLevels({ published, pwsh = false }) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/** A single compact table; absent tools never get advertised. */
|
|
26
|
-
export function renderToolingPolicy({ published, enableShellTools, mounted, levels, pwsh7 = false, extraGuidance = '' }) {
|
|
27
|
+
export function renderToolingPolicy({ published, enableShellTools, mounted, levels, pwsh7 = false, shellComponent = true, extraGuidance = '' }) {
|
|
27
28
|
const names = new Set(published ?? (mounted || levels?.fastctx
|
|
28
29
|
? [...FILE_TOOLS, ...(enableShellTools ? SHELL_TOOLS : []), ...(levels?.bash ? IN_PROCESS_TOOLS : [])].map(publicToolName) : []))
|
|
29
30
|
if (levels?.pwsh) names.add('pwsh')
|
|
30
|
-
if (!
|
|
31
|
-
const
|
|
31
|
+
if (!shellComponent) names.delete('pwsh')
|
|
32
|
+
const hasFiles = FILE_TOOLS.some(name => names.has(publicToolName(name)))
|
|
33
|
+
const hasBash = names.has(BASH_TOOL)
|
|
34
|
+
const hasBackground = names.has(publicToolName('run_background'))
|
|
35
|
+
const hasPwsh = shellComponent && names.has('pwsh')
|
|
36
|
+
if (!hasFiles && !hasBash && !hasBackground && !hasPwsh) return ''
|
|
37
|
+
const heading = hasBash ? '# Shell routing: bash → PowerShell 7'
|
|
38
|
+
: hasFiles ? '# Repository file tools' : hasBackground ? '# Background task tools' : '# Windows-native shell'
|
|
39
|
+
const lines = [heading, '', '| Task | Tool |', '| --- | --- |']
|
|
32
40
|
const row = (raw, task) => {
|
|
33
41
|
if (names.has(publicToolName(raw))) lines.push(`| ${task} | ${publicToolName(raw)} |`)
|
|
34
42
|
}
|
|
35
43
|
row('inspect_local_file', 'Text / multiple ranges / encoding / PDF text / hex')
|
|
36
|
-
lines.push('| Images | read_image (host) |')
|
|
44
|
+
if (hasFiles && names.has('read_image')) lines.push('| Images | read_image (host) |')
|
|
37
45
|
row('grep', 'Search contents')
|
|
38
46
|
row('glob', 'Find paths')
|
|
39
47
|
row('replace', 'Batch replacement; use host edit for precise edits')
|
|
@@ -41,9 +49,10 @@ export function renderToolingPolicy({ published, enableShellTools, mounted, leve
|
|
|
41
49
|
if (names.has('pwsh')) lines.push(`| Windows-native cmdlets / registry / services only | pwsh (${pwsh7 || levels?.pwsh ? 'bundled PowerShell 7 via host' : 'host PowerShell'}) |`)
|
|
42
50
|
row('run', 'Bounded bash command result (full access only)')
|
|
43
51
|
if (names.has(publicToolName('run_background'))) {
|
|
44
|
-
lines.push(`| Background commands / owned jobs | ${
|
|
52
|
+
lines.push(`| Background commands / owned jobs | ${BACKGROUND_TOOLS.map(publicToolName).filter(name => names.has(name)).join(', ')} |`)
|
|
45
53
|
}
|
|
46
|
-
lines.push('', '-
|
|
54
|
+
lines.push('', '- ops_* arguments are strict: pass only declared fields.')
|
|
55
|
+
if (hasFiles) lines.push('- Use file tools for file operations; use count for totals and batch independent requests.',
|
|
47
56
|
'- On a tool error, correct the arguments and retry; do not switch to a shell workaround.')
|
|
48
57
|
if (names.has(BASH_TOOL)) lines.push('- Prefer bash for command execution. Use PowerShell only when Windows-native functionality is necessary; fix bash errors in bash, do not switch shells or mix their syntax.')
|
|
49
58
|
if (names.has(publicToolName('run_background'))) lines.push('- Only operate on job IDs you started in this session.')
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** One plugin-owned FastCtx connection shared by independently toggled components. */
|
|
2
|
+
import { McpStdioClient } from './handshake.js'
|
|
3
|
+
const pools = new WeakMap()
|
|
4
|
+
export async function acquireRuntime(ctx, runtime, env) {
|
|
5
|
+
const root = ctx.root
|
|
6
|
+
let pool = pools.get(root)
|
|
7
|
+
if (!pool) { pool = { clients: new Map(), closing: Promise.resolve() }; pools.set(root, pool) }
|
|
8
|
+
await pool.closing
|
|
9
|
+
let entry = pool.clients.get(runtime.file)
|
|
10
|
+
if (!entry) {
|
|
11
|
+
const client = McpStdioClient.start({ file: runtime.file, args: ['serve', '--enable-shell'], env, timeoutMs: 60000 })
|
|
12
|
+
entry = { client, refs: 0, ready: undefined }
|
|
13
|
+
pool.clients.set(runtime.file, entry)
|
|
14
|
+
entry.ready = (async () => {
|
|
15
|
+
try {
|
|
16
|
+
const handshake = await client.initialize('dsh-ops-components')
|
|
17
|
+
const tools = await client.listTools()
|
|
18
|
+
return { client, handshake, tools }
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (pool.clients.get(runtime.file) === entry) pool.clients.delete(runtime.file)
|
|
21
|
+
await client.close()
|
|
22
|
+
throw error
|
|
23
|
+
}
|
|
24
|
+
})()
|
|
25
|
+
void client.exited.then(() => { if (pool.clients.get(runtime.file) === entry) pool.clients.delete(runtime.file) })
|
|
26
|
+
}
|
|
27
|
+
entry.refs += 1
|
|
28
|
+
let released = false
|
|
29
|
+
const release = async () => {
|
|
30
|
+
if (released) return
|
|
31
|
+
released = true
|
|
32
|
+
entry.refs -= 1
|
|
33
|
+
if (!entry.refs) {
|
|
34
|
+
if (pool.clients.get(runtime.file) === entry) pool.clients.delete(runtime.file)
|
|
35
|
+
const closing = pool.closing.then(() => entry.client.close())
|
|
36
|
+
pool.closing = closing.catch(() => {})
|
|
37
|
+
await closing
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
try { return { ...await entry.ready, release } }
|
|
41
|
+
catch (error) { await release(); throw error }
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** A reversible runtime-only config overlay; never edits profile files or services. */
|
|
2
|
+
export function mountBundledPwsh(ctx, shells) {
|
|
3
|
+
if (!shells.pwsh.available || process.platform !== 'win32') return
|
|
4
|
+
ctx.inject(['loader'], loaderCtx => {
|
|
5
|
+
const loader = loaderCtx.get('loader')
|
|
6
|
+
const matches = entry => entry?.options?.id === 'pwsh-sandbox'
|
|
7
|
+
&& entry.options.name === '@deepseek-ai/dsh-pwsh-sandbox'
|
|
8
|
+
const refresh = () => {
|
|
9
|
+
for (const entry of loader.entries()) {
|
|
10
|
+
if (matches(entry) && entry.fiber?.state === 2) {
|
|
11
|
+
// Public Fiber.update with noSave recomputes from the owner's latest
|
|
12
|
+
// raw config. No stored config, volatile reference, or method is changed.
|
|
13
|
+
entry.fiber.update(entry.options.config, true)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const unhook = loaderCtx.on('internal/config', function (_raw, next) {
|
|
18
|
+
const config = next()
|
|
19
|
+
if (!matches(this.entry) || this.parent?.fiber?.entry === this.entry) return config
|
|
20
|
+
return { ...config, pwshPath: shells.pwsh.file }
|
|
21
|
+
}, { global: true, prepend: true })
|
|
22
|
+
let stopped = false
|
|
23
|
+
loaderCtx.effect(() => () => {
|
|
24
|
+
if (stopped) return
|
|
25
|
+
stopped = true
|
|
26
|
+
unhook()
|
|
27
|
+
refresh()
|
|
28
|
+
}, 'dsh-ops: restore owning PowerShell executor config')
|
|
29
|
+
refresh()
|
|
30
|
+
})
|
|
31
|
+
}
|
package/lib/tools.js
CHANGED
|
@@ -23,6 +23,8 @@ import { McpStdioClient } from './handshake.js'
|
|
|
23
23
|
import { FILE_TOOLS, SHELL_TOOLS, publicToolName } from './policy.js'
|
|
24
24
|
import { projectTool } from './presentation.js'
|
|
25
25
|
import { cleanBackground, jobEntries, startedJob } from './jobs.js'
|
|
26
|
+
import { acquireRuntime } from './runtime-pool.js'
|
|
27
|
+
import { resolveShells } from './shells.js'
|
|
26
28
|
|
|
27
29
|
/** Deadline for one connection attempt: spawn, initialize, and `tools/list`. */
|
|
28
30
|
const CONNECT_TIMEOUT_MS = 60_000
|
|
@@ -216,10 +218,16 @@ export class FastCtxTools {
|
|
|
216
218
|
* @param {{file: string, source: string, version: string}} options.runtime - the resolved FastCtx executable.
|
|
217
219
|
* @param {(message: string, level?: string) => void} [options.report] - where asynchronous events are reported.
|
|
218
220
|
*/
|
|
219
|
-
constructor({ ctx, config, runtime, report }) {
|
|
221
|
+
constructor({ ctx, config, runtime, report, fileTools = true, commandTools = SHELL_TOOLS, shared = false }) {
|
|
220
222
|
this.ctx = ctx
|
|
221
223
|
this.config = config
|
|
222
224
|
this.runtime = runtime
|
|
225
|
+
this.fileTools = fileTools
|
|
226
|
+
this.commandTools = commandTools
|
|
227
|
+
this.shared = shared
|
|
228
|
+
this.leases = new Map()
|
|
229
|
+
this.pendingCalls = new Set()
|
|
230
|
+
this.shuttingDown = false
|
|
223
231
|
this.report = report ?? (() => {})
|
|
224
232
|
/** The live connection, or undefined while the surface is down. */
|
|
225
233
|
this.client = undefined
|
|
@@ -282,7 +290,7 @@ export class FastCtxTools {
|
|
|
282
290
|
if (!entry?.ctx) return
|
|
283
291
|
for (const dispose of entry.disposers) dispose()
|
|
284
292
|
entry.disposers = []
|
|
285
|
-
if (this.stopped) return
|
|
293
|
+
if (this.stopped || this.shuttingDown) return
|
|
286
294
|
if (!this.shellAllowed(agent.session)) {
|
|
287
295
|
// A child must not inherit an authorized ancestor's command surface.
|
|
288
296
|
const names = (this.ctx.get('tools')?.schemas(agent) ?? []).map(tool => tool.name)
|
|
@@ -293,8 +301,8 @@ export class FastCtxTools {
|
|
|
293
301
|
}
|
|
294
302
|
return
|
|
295
303
|
}
|
|
296
|
-
const commandTools = this.tools.filter(tool =>
|
|
297
|
-
if (commandTools.length !==
|
|
304
|
+
const commandTools = this.tools.filter(tool => this.commandTools.includes(tool.name))
|
|
305
|
+
if (commandTools.length !== this.commandTools.length) return
|
|
298
306
|
try {
|
|
299
307
|
for (const tool of commandTools) {
|
|
300
308
|
entry.disposers.push(entry.ctx.tools.register(toolDefinition({
|
|
@@ -367,6 +375,20 @@ export class FastCtxTools {
|
|
|
367
375
|
* @returns {Promise<void>} resolves once the child is gone.
|
|
368
376
|
*/
|
|
369
377
|
async stop() {
|
|
378
|
+
this.shuttingDown = true
|
|
379
|
+
this.refreshAgents()
|
|
380
|
+
// Drain launches before killing only this component's owned jobs. Closing
|
|
381
|
+
// FastCtx alone does not stop its durable supervisors.
|
|
382
|
+
await Promise.allSettled([...this.pendingCalls])
|
|
383
|
+
const cleanupClient = this.client
|
|
384
|
+
if (cleanupClient) {
|
|
385
|
+
for (const ids of this.ownedJobs.values()) for (const job_id of ids) {
|
|
386
|
+
try {
|
|
387
|
+
const result = await cleanupClient.callTool('job_kill', { job_id }, { timeoutMs: 15000 })
|
|
388
|
+
if (result?.isError) this.report(`dsh-ops: owned job ${job_id} could not be stopped during component shutdown.`, 'warn')
|
|
389
|
+
} catch (error) { this.report(`dsh-ops: owned job ${job_id} shutdown failed: ${messageOf(error)}`, 'warn') }
|
|
390
|
+
}
|
|
391
|
+
}
|
|
370
392
|
this.stopped = true
|
|
371
393
|
if (this.timer !== undefined) {
|
|
372
394
|
clearTimeout(this.timer)
|
|
@@ -380,7 +402,14 @@ export class FastCtxTools {
|
|
|
380
402
|
this.tools = []
|
|
381
403
|
this.ownedJobs.clear()
|
|
382
404
|
await Promise.all([...this.agents.keys()].map(agent => this.detachAgent(agent)))
|
|
383
|
-
if (client !== undefined) await client
|
|
405
|
+
if (client !== undefined) await this.#release(client)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async #release(client) {
|
|
409
|
+
const lease = this.leases.get(client)
|
|
410
|
+
this.leases.delete(client)
|
|
411
|
+
if (lease) await lease.release()
|
|
412
|
+
else if (!this.shared) await client.close()
|
|
384
413
|
}
|
|
385
414
|
|
|
386
415
|
/**
|
|
@@ -388,7 +417,16 @@ export class FastCtxTools {
|
|
|
388
417
|
* @returns {Promise<void>} resolves once the generation is live.
|
|
389
418
|
*/
|
|
390
419
|
async #connect() {
|
|
391
|
-
|
|
420
|
+
let lease
|
|
421
|
+
if (this.shared) {
|
|
422
|
+
const env = childEnv()
|
|
423
|
+
const bash = resolveShells(this.config).bash
|
|
424
|
+
if (bash.available) env.FASTCTX_BASH = bash.file
|
|
425
|
+
lease = await acquireRuntime(this.ctx, this.runtime, env)
|
|
426
|
+
if (this.shuttingDown || this.stopped) { await lease.release(); throw new StoppedError() }
|
|
427
|
+
this.leases.set(lease.client, lease)
|
|
428
|
+
}
|
|
429
|
+
const client = lease?.client ?? McpStdioClient.start({
|
|
392
430
|
file: this.runtime.file,
|
|
393
431
|
args: serverArgs(this.config.enableShellTools),
|
|
394
432
|
env: childEnv(),
|
|
@@ -401,9 +439,9 @@ export class FastCtxTools {
|
|
|
401
439
|
// already replaced or abandoned.
|
|
402
440
|
void client.exited.then(({ code, signal, error }) => this.#onExit(client, code, signal, error))
|
|
403
441
|
try {
|
|
404
|
-
const handshake = await client.initialize('dsh-ops')
|
|
405
|
-
const tools = await client.listTools()
|
|
406
|
-
if (this.stopped || client !== this.client) throw new StoppedError()
|
|
442
|
+
const handshake = lease?.handshake ?? await client.initialize('dsh-ops')
|
|
443
|
+
const tools = lease?.tools ?? await client.listTools()
|
|
444
|
+
if (this.stopped || this.shuttingDown || client !== this.client) throw new StoppedError()
|
|
407
445
|
this.ownedJobs.clear()
|
|
408
446
|
this.#publish(tools)
|
|
409
447
|
this.serverInfo = handshake?.serverInfo
|
|
@@ -411,7 +449,7 @@ export class FastCtxTools {
|
|
|
411
449
|
this.serverInstructions = ''
|
|
412
450
|
this.connectedAt = Date.now()
|
|
413
451
|
} catch (error) {
|
|
414
|
-
await client
|
|
452
|
+
await this.#release(client)
|
|
415
453
|
if (this.client === client) this.client = undefined
|
|
416
454
|
throw error
|
|
417
455
|
}
|
|
@@ -426,7 +464,8 @@ export class FastCtxTools {
|
|
|
426
464
|
* @returns {void}
|
|
427
465
|
*/
|
|
428
466
|
#onExit(client, code, signal, error) {
|
|
429
|
-
if (this.stopped || client !== this.client) return
|
|
467
|
+
if (this.stopped || this.shuttingDown || client !== this.client) return
|
|
468
|
+
void this.#release(client)
|
|
430
469
|
this.client = undefined
|
|
431
470
|
this.ownedJobs.clear()
|
|
432
471
|
this.tools = []
|
|
@@ -449,7 +488,7 @@ export class FastCtxTools {
|
|
|
449
488
|
#scheduleReconnect() {
|
|
450
489
|
// One pending timer at a time: a crash and a failed first connect can both
|
|
451
490
|
// land here, and two loops would double every later attempt.
|
|
452
|
-
if (this.stopped || this.timer !== undefined) return
|
|
491
|
+
if (this.stopped || this.shuttingDown || this.timer !== undefined) return
|
|
453
492
|
// A connection that stayed up long enough closed the previous outage.
|
|
454
493
|
if (this.connectedAt !== undefined && Date.now() - this.connectedAt >= RECONNECT.maxDelayMs) {
|
|
455
494
|
this.attempts = 0
|
|
@@ -486,12 +525,12 @@ export class FastCtxTools {
|
|
|
486
525
|
* @returns {Promise<void>} resolves once this attempt settled.
|
|
487
526
|
*/
|
|
488
527
|
async #reconnect() {
|
|
489
|
-
if (this.stopped) return
|
|
528
|
+
if (this.stopped || this.shuttingDown) return
|
|
490
529
|
try {
|
|
491
530
|
await this.#connect()
|
|
492
531
|
this.report(`dsh-ops: ${this.#describe()} reconnected with ${this.names().length} tool(s).`)
|
|
493
532
|
} catch (error) {
|
|
494
|
-
if (this.stopped) return
|
|
533
|
+
if (this.stopped || this.shuttingDown) return
|
|
495
534
|
this.report(
|
|
496
535
|
`dsh-ops: FastCtx reconnect attempt ${this.attempts}/${RECONNECT.maxAttempts} `
|
|
497
536
|
+ `failed (${messageOf(error)}).`,
|
|
@@ -521,7 +560,7 @@ export class FastCtxTools {
|
|
|
521
560
|
throw new Error(`${this.#describe()} listed the tool "${String(tool?.name)}" more than once`)
|
|
522
561
|
}
|
|
523
562
|
seen.add(name)
|
|
524
|
-
if (!FILE_TOOLS.includes(tool.name)) continue
|
|
563
|
+
if (!this.fileTools || !FILE_TOOLS.includes(tool.name)) continue
|
|
525
564
|
definitions.push(toolDefinition({
|
|
526
565
|
tool,
|
|
527
566
|
call: (args, exec) => this.#call(tool.name, args, exec),
|
|
@@ -556,7 +595,15 @@ export class FastCtxTools {
|
|
|
556
595
|
* @param {{signal?: AbortSignal, agent?: {session?: object}}} [exec] - the host execution context.
|
|
557
596
|
* @returns {Promise<{content: unknown[], structuredContent?: unknown}>} the canonical value.
|
|
558
597
|
*/
|
|
559
|
-
|
|
598
|
+
#call(rawName, args, exec) {
|
|
599
|
+
if (this.shuttingDown || this.stopped) return Promise.reject(new Error('This dsh-ops component is shutting down.'))
|
|
600
|
+
const call = this.#callActive(rawName, args, exec)
|
|
601
|
+
this.pendingCalls.add(call)
|
|
602
|
+
void call.then(() => this.pendingCalls.delete(call), () => this.pendingCalls.delete(call))
|
|
603
|
+
return call
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async #callActive(rawName, args, exec) {
|
|
560
607
|
const label = publicToolName(rawName)
|
|
561
608
|
const session = exec?.agent?.session
|
|
562
609
|
if (SHELL_TOOLS.includes(rawName) && !this.shellAllowed(session)) {
|
|
@@ -569,6 +616,7 @@ export class FastCtxTools {
|
|
|
569
616
|
throw new Error('This is an image; use host read_image.')
|
|
570
617
|
}
|
|
571
618
|
}
|
|
619
|
+
if (rawName === 'run_background') args = { ...args, cwd: args?.cwd ?? session?.header?.cwd }
|
|
572
620
|
const client = this.client
|
|
573
621
|
if (client === undefined) {
|
|
574
622
|
// A dead server must be an immediate, explicit failure rather than a
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
|
-
"title": "
|
|
4
|
-
"description": "
|
|
3
|
+
"title": "dsh-ops",
|
|
4
|
+
"description": "Provides bash, PowerShell 7, and a suite of high-performance tools written in Rust for Windows DSH to enhance token efficiency"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED