dsh-gitbash-shell 0.39.3 → 0.40.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 +185 -0
- package/README.md +75 -35
- package/README_EN.md +48 -49
- package/docs/experimental-native-execution.md +224 -0
- package/docs/experimental-native-verification.md +184 -0
- package/dsh.plugin.json +1 -1
- package/package.json +4 -3
- package/src/bash-tool.js +5 -7
- package/src/client.js +75 -22
- package/src/experimental-tools.js +47 -0
- package/src/index.js +98 -26
- package/src/inline-tools.js +103 -15
- package/src/native-executor.js +283 -0
- package/src/native-output.js +347 -0
- package/src/native-tool.js +195 -0
- package/src/path-tool.js +297 -0
- package/src/session-workdir.js +12 -0
- package/src/shell.js +78 -76
package/src/index.js
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
cpSync,
|
|
47
47
|
existsSync,
|
|
48
48
|
mkdirSync,
|
|
49
|
+
mkdtempSync,
|
|
49
50
|
readFileSync,
|
|
50
51
|
readdirSync,
|
|
51
52
|
rmSync,
|
|
@@ -57,7 +58,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
57
58
|
|
|
58
59
|
import { PRESET_META, minimalPluginsFor, pluginsFor } from './compositions.js'
|
|
59
60
|
import { loadSchemastery } from './schemastery.js'
|
|
60
|
-
import {
|
|
61
|
+
import { applyToolRouting, mountInlineTools } from './inline-tools.js'
|
|
61
62
|
import { mountFastctx } from './fastctx-bridge.js'
|
|
62
63
|
|
|
63
64
|
/**
|
|
@@ -512,12 +513,14 @@ export const Config = Schema === null ? undefined : Schema.object({
|
|
|
512
513
|
* now controls the real thing.
|
|
513
514
|
*/
|
|
514
515
|
pwshTool: live(Schema.boolean().default(false)),
|
|
516
|
+
// Maintainer PR #26 decision: one live opt-out for refusal and companions.
|
|
517
|
+
experimentalNative: live(Schema.boolean().default(true)),
|
|
515
518
|
/**
|
|
516
519
|
* Publish the FastCtx file tools (`gb_*`) through the local MCP bridge.
|
|
517
520
|
* Default ON; Windows-only payload, stands down entirely when dsh-ops is
|
|
518
521
|
* present (its ops_* tools already cover the same ground).
|
|
519
522
|
*/
|
|
520
|
-
fastctxTools: live(Schema.boolean().default(
|
|
523
|
+
fastctxTools: live(Schema.boolean().default(false)),
|
|
521
524
|
})
|
|
522
525
|
|
|
523
526
|
/** Read one Config value across eras: Volatile ref (>= 0.1.7) or plain value. */
|
|
@@ -582,8 +585,8 @@ function makeLiveReader(ctx, config) {
|
|
|
582
585
|
variantPresets: valueOf(config.variantPresets) === true,
|
|
583
586
|
// Default ON (v0.35.0): the pwsh replacement fills the denied host tool.
|
|
584
587
|
pwshTool: valueOf(config.pwshTool) !== false,
|
|
585
|
-
// Default
|
|
586
|
-
fastctxTools: valueOf(config.fastctxTools)
|
|
588
|
+
// Default OFF (v0.39.4, dsh-ops 0.3.0): FastCtx is dead upstream and dsh-ops ships stronger native ops_* file tools; an explicit true re-enables the bridge. It still stands down when dsh-ops is present.
|
|
589
|
+
fastctxTools: valueOf(config.fastctxTools) === true,
|
|
587
590
|
}
|
|
588
591
|
}
|
|
589
592
|
return {
|
|
@@ -593,7 +596,8 @@ function makeLiveReader(ctx, config) {
|
|
|
593
596
|
autoTerminalShell: () => dialect().autoTerminalShell !== false,
|
|
594
597
|
variantPresets: () => (hasConfig ? valueOf(config.variantPresets) === true : false),
|
|
595
598
|
pwshTool: () => (hasConfig ? valueOf(config.pwshTool) !== false : true),
|
|
596
|
-
|
|
599
|
+
experimentalNative: () => (hasConfig ? valueOf(config.experimentalNative) !== false : true),
|
|
600
|
+
fastctxTools: () => (hasConfig ? valueOf(config.fastctxTools) === true : false),
|
|
597
601
|
// Default OFF: only an explicit true asks for deduplication.
|
|
598
602
|
suppressPeerCordis: () => (hasConfig ? valueOf(config.suppressPeerCordis) === true : false),
|
|
599
603
|
}
|
|
@@ -773,7 +777,9 @@ function pathEcho(value, env) {
|
|
|
773
777
|
* content VERBATIM: output is data, never rewritten (the same red line as
|
|
774
778
|
* file content in successful results).
|
|
775
779
|
*/
|
|
776
|
-
|
|
780
|
+
// ops_* six (v0.39.4, dsh-ops 0.3.0): their failures quote Windows paths too —
|
|
781
|
+
// the error content and error.message must answer in the same /c/ dialect.
|
|
782
|
+
const ERROR_CONTENT_TOOLS = new Set(['read', 'read_image', 'write', 'edit', 'glob', 'grep', 'present', 'ops_inspect_local_file', 'ops_glob', 'ops_grep', 'ops_replace', 'ops_batch', 'ops_result'])
|
|
777
783
|
|
|
778
784
|
export function rewriteErrorContent(content, options) {
|
|
779
785
|
if (!Array.isArray(content)) return content
|
|
@@ -868,6 +874,37 @@ export function rewriteResultPaths(name, value, env) {
|
|
|
868
874
|
})
|
|
869
875
|
return changed ? { ...value, files } : value
|
|
870
876
|
}
|
|
877
|
+
// v0.39.4 (dsh-ops 0.3.0): ops_* tools answer with the same paths the
|
|
878
|
+
// model must keep speaking /c/ about. Their Rust worker names every path
|
|
879
|
+
// field `path` (items[] / matches[]) and ops_batch/ops_result echo the
|
|
880
|
+
// operation `root`; detailsRef ids are opaque, never rewritten.
|
|
881
|
+
if (name === 'ops_grep' && Array.isArray(value.matches)) {
|
|
882
|
+
let changed = false
|
|
883
|
+
const matches = value.matches.map((match) => {
|
|
884
|
+
if (!match || typeof match !== 'object' || typeof match.path !== 'string' || match.path === '') return match
|
|
885
|
+
const out = toMsys(match.path)
|
|
886
|
+
if (out === match.path) return match
|
|
887
|
+
changed = true
|
|
888
|
+
return { ...match, path: out }
|
|
889
|
+
})
|
|
890
|
+
return changed ? { ...value, matches } : value
|
|
891
|
+
}
|
|
892
|
+
if ((name === 'ops_inspect_local_file' || name === 'ops_glob' || name === 'ops_replace') && Array.isArray(value.items)) {
|
|
893
|
+
let changed = false
|
|
894
|
+
const items = value.items.map((item) => {
|
|
895
|
+
if (!item || typeof item !== 'object' || typeof item.path !== 'string' || item.path === '') return item
|
|
896
|
+
const out = toMsys(item.path)
|
|
897
|
+
if (out === item.path) return item
|
|
898
|
+
changed = true
|
|
899
|
+
return { ...item, path: out }
|
|
900
|
+
})
|
|
901
|
+
return changed ? { ...value, items } : value
|
|
902
|
+
}
|
|
903
|
+
if ((name === 'ops_batch' || name === 'ops_result') && typeof value.root === 'string' && value.root !== '') {
|
|
904
|
+
const out = toMsys(value.root)
|
|
905
|
+
if (out !== value.root) return { ...value, root: out }
|
|
906
|
+
return value
|
|
907
|
+
}
|
|
871
908
|
if (name === 'glob' && Array.isArray(value.paths)) {
|
|
872
909
|
let changed = false
|
|
873
910
|
const paths = value.paths.map((p) => {
|
|
@@ -909,7 +946,9 @@ export function rewriteResultPaths(name, value, env) {
|
|
|
909
946
|
|
|
910
947
|
const MSYS_DRIVE_ROOT = /^\/([a-z])\/(.*)$/i
|
|
911
948
|
const MSYS_BARE_DRIVE = /^\/([a-z])$/i
|
|
912
|
-
|
|
949
|
+
// 'root' (v0.39.4): dsh-ops ops_batch/ops_result name their path argument `root`;
|
|
950
|
+
// no official tool uses that key, so the extra alias is collision-free.
|
|
951
|
+
const TRANSLATABLE_PATH_FIELDS = new Set(['file_path', 'path', 'workdir', 'root'])
|
|
913
952
|
|
|
914
953
|
// ── virtual mounts + env (v0.17.0) ─────────────────────────────────────
|
|
915
954
|
//
|
|
@@ -1143,6 +1182,37 @@ export function translatePathArguments(args, env) {
|
|
|
1143
1182
|
changed = true
|
|
1144
1183
|
}
|
|
1145
1184
|
}
|
|
1185
|
+
// v0.39.4 (dsh-ops 0.3.0): ops_batch carries its paths one level down —
|
|
1186
|
+
// operations[] objects with path/to, and protect[] as a plain string
|
|
1187
|
+
// array. Same deal as present's files[].path: the field-name whitelist
|
|
1188
|
+
// only sees top-level keys. The model speaks /c/ everywhere; these
|
|
1189
|
+
// arrive at the Rust worker as native absolute paths.
|
|
1190
|
+
if (Array.isArray(next.operations)) {
|
|
1191
|
+
const operations = next.operations.map((operation) => {
|
|
1192
|
+
if (!operation || typeof operation !== 'object') return operation
|
|
1193
|
+
let out = operation
|
|
1194
|
+
for (const field of ['path', 'to']) {
|
|
1195
|
+
if (typeof out[field] !== 'string') continue
|
|
1196
|
+
const translated = translateMsysPath(out[field], env)
|
|
1197
|
+
if (translated !== out[field]) out = { ...out, [field]: translated }
|
|
1198
|
+
}
|
|
1199
|
+
return out
|
|
1200
|
+
})
|
|
1201
|
+
if (operations.some((operation, index) => operation !== next.operations[index])) {
|
|
1202
|
+
next.operations = operations
|
|
1203
|
+
changed = true
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (Array.isArray(next.protect)) {
|
|
1207
|
+
const protect = next.protect.map((entry) => {
|
|
1208
|
+
if (typeof entry !== 'string') return entry
|
|
1209
|
+
return translateMsysPath(entry, env)
|
|
1210
|
+
})
|
|
1211
|
+
if (protect.some((entry, index) => entry !== next.protect[index])) {
|
|
1212
|
+
next.protect = protect
|
|
1213
|
+
changed = true
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1146
1216
|
return changed ? next : args
|
|
1147
1217
|
}
|
|
1148
1218
|
|
|
@@ -2153,25 +2223,15 @@ export async function apply(ctx, config = {}) {
|
|
|
2153
2223
|
no run_code hint — so they meet the shell they were composed for. */
|
|
2154
2224
|
const dialect = liveSettings.dialect()
|
|
2155
2225
|
const applies = dialectApplies(dialect, assembleContext && assembleContext.agent)
|
|
2156
|
-
//
|
|
2157
|
-
//
|
|
2158
|
-
//
|
|
2159
|
-
//
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
// teaches the model to call a hidden name.
|
|
2166
|
-
if (assembly && typeof assembly === 'object' && Array.isArray(assembly.tools)) {
|
|
2167
|
-
const routeSentence = liveSettings.pwshTool() === true ? GITBASH_ROUTE_ADDENDUM : GITBASH_ROUTE_ADDENDUM_NO_PWSH
|
|
2168
|
-
for (const tool of assembly.tools) {
|
|
2169
|
-
if (tool && tool.name === 'bash' && typeof tool.description === 'string'
|
|
2170
|
-
&& !tool.description.includes('Prefer this bash tool')) {
|
|
2171
|
-
tool.description += '\n' + routeSentence
|
|
2172
|
-
+ (applies && dialect.posixPaths === true ? '\n' + GITBASH_MSYS_ADDENDUM : '')
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2226
|
+
// Routing is dialect-independent and follows actual offered tools.
|
|
2227
|
+
// PTC may expose only run_code here: its generic SDK decision rule
|
|
2228
|
+
// relies on the registered bindings' own descriptions, not guessed
|
|
2229
|
+
// hidden names. Replace our block on reassembly/visibility changes.
|
|
2230
|
+
if (assembly && typeof assembly === 'object') {
|
|
2231
|
+
applyToolRouting(assembly.tools, {
|
|
2232
|
+
pwshVisible: liveSettings.pwshTool() === true,
|
|
2233
|
+
msys: applies && dialect.posixPaths === true,
|
|
2234
|
+
})
|
|
2175
2235
|
}
|
|
2176
2236
|
if (applies && dialect.posixPaths === true && assembly && typeof assembly === 'object') {
|
|
2177
2237
|
for (const section of Array.isArray(assembly.sections) ? assembly.sections : []) {
|
|
@@ -2699,6 +2759,18 @@ export async function apply(ctx, config = {}) {
|
|
|
2699
2759
|
}
|
|
2700
2760
|
},
|
|
2701
2761
|
pwshEnabled: () => liveSettings.pwshTool() === true,
|
|
2762
|
+
experimentalEnabled: () => liveSettings.experimentalNative(),
|
|
2763
|
+
// Explicit conversion does not rewrite source or change auto-dialect
|
|
2764
|
+
// settings. The same lazy, verified mount facts are used each call.
|
|
2765
|
+
translatePath: (value) => translateMsysPath(value, buildTranslateEnv(readConfiguredBashPath())),
|
|
2766
|
+
// Trusted host-owned output storage, invoked only after the native
|
|
2767
|
+
// executor's actual policy/argument/capability gate. Never model cwd.
|
|
2768
|
+
// Completed bounded artifacts remain readable like Core spill files;
|
|
2769
|
+
// this does not claim a cross-task quota or automatic retention purge.
|
|
2770
|
+
createArtifactDirectory: (_request, _exec, signal) => {
|
|
2771
|
+
signal?.throwIfAborted()
|
|
2772
|
+
return mkdtempSync(join(tmpdir(), 'dsh-gitbash-native-'))
|
|
2773
|
+
},
|
|
2702
2774
|
log: (m) => console.log(TAG + ' ' + m),
|
|
2703
2775
|
})
|
|
2704
2776
|
console.log(TAG + ' inline tool patch armed (own bash tool over a private Git Bash executor; official pwsh executor untouched)')
|
package/src/inline-tools.js
CHANGED
|
@@ -101,14 +101,14 @@ export function inlineNeeds(names, { pwshVisible } = {}) {
|
|
|
101
101
|
* routing. Both hold regardless of the path dialect a delegate sees.
|
|
102
102
|
*/
|
|
103
103
|
export const GITBASH_ROUTE_ADDENDUM = [
|
|
104
|
-
'Prefer this bash tool for
|
|
105
|
-
'Fix failed bash
|
|
104
|
+
'Prefer this bash tool for shell syntax, scripts, pipelines, redirection, globbing and POSIX utilities; use the pwsh tool ONLY when PowerShell cmdlets, registry, services or WMI are truly required.',
|
|
105
|
+
'Fix failed bash syntax in bash; never embed `pwsh -Command` inside a bash command as a workaround (variable expansion and output encoding break there). Choose a separately offered native route for its documented purpose before execution, never as a policy-denial retry.',
|
|
106
106
|
].join('\n')
|
|
107
107
|
|
|
108
108
|
/** The bash-only routing variant, used while the pwsh switch is off (the default). */
|
|
109
109
|
export const GITBASH_ROUTE_ADDENDUM_NO_PWSH = [
|
|
110
|
-
'Prefer this bash tool for
|
|
111
|
-
'Fix failed
|
|
110
|
+
'Prefer this bash tool for shell syntax, scripts, pipelines, redirection, globbing and POSIX utilities — it is this host\'s shell.',
|
|
111
|
+
'Fix failed bash syntax in bash; no PowerShell tool is offered here. Do not invent a hidden tool or embed another interpreter as a workaround.',
|
|
112
112
|
].join('\n')
|
|
113
113
|
|
|
114
114
|
/**
|
|
@@ -118,6 +118,38 @@ export const GITBASH_ROUTE_ADDENDUM_NO_PWSH = [
|
|
|
118
118
|
*/
|
|
119
119
|
export const GITBASH_MSYS_ADDENDUM = 'This bash is Git for Windows (MSYS): paths use drive roots (/c/Users/...), and ~, /tmp, /dev/null and /usr/... resolve exactly as Git Bash resolves them.'
|
|
120
120
|
|
|
121
|
+
const ROUTING_START = '\n[gitbash tool routing]\n'
|
|
122
|
+
const ROUTING_END = '\n[/gitbash tool routing]'
|
|
123
|
+
const ROUTING_SAFETY = 'Choose the execution route before running. A policy/access denial is a stop condition: never switch tools, shells or permissions to bypass it. A decoding/display failure does not mean the command failed to run; preserve its exit facts and never blindly repeat a side-effecting command. Do not guess unknown or mixed encodings.'
|
|
124
|
+
const PTC_ROUTING = 'Inside run_code, call only bindings actually offered by the current SDK with await tools.<name>(...). Select by their descriptions: shell execution for shell syntax/pipelines; an offered typed native-executable binding for exact Windows argv or explicitly known per-stream encodings; an offered complete-path conversion binding before passing an MSYS value to Node fs/path. These are different operations, not substitutes for permissions. Never pass routes, search text or source code to a path converter. Program strings do not perform shell expansion. If a needed binding is absent, report the missing capability; do not invent its name. ' + ROUTING_SAFETY
|
|
125
|
+
|
|
126
|
+
/** Model guidance names only tools in this assembly, not a switch's intent. */
|
|
127
|
+
export function toolRoutingHintFor(tools, { pwshVisible = false } = {}) {
|
|
128
|
+
const names = new Set((Array.isArray(tools) ? tools : []).map(tool => tool?.name))
|
|
129
|
+
if (!names.has('bash')) return ''
|
|
130
|
+
const lines = [pwshVisible && names.has('pwsh') ? GITBASH_ROUTE_ADDENDUM : GITBASH_ROUTE_ADDENDUM_NO_PWSH]
|
|
131
|
+
if (names.has('gitbash_native')) lines.push('Use gitbash_native before execution when a Windows .exe needs exact literal argv such as /all or /c without MSYS conversion, or independently declared stdout/stderr encodings. Supply an absolute executable, mark each argument literal or path, and select only a known encoding (raw for unknown/mixed bytes). It does not implement shell pipelines, expansion or .cmd/.bat wrappers; only resolved danger-full-access can execute it.')
|
|
132
|
+
if (names.has('gitbash_to_native_path')) lines.push('Use gitbash_to_native_path only for a complete caller-declared MSYS filesystem path that a native Node fs/path API needs, especially a runtime-built value. Read native_path from the result. Never convert routes, search strings or shell/program source. Already-native or cwd-relative Node paths need no conversion; ordinary tool path fields already use their own translation. Conversion grants no access.')
|
|
133
|
+
lines.push(ROUTING_SAFETY)
|
|
134
|
+
return lines.join('\n')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Add a replaceable, idempotent block; a reused assembly follows visibility. */
|
|
138
|
+
export function applyToolRouting(tools, { pwshVisible = false, msys = false } = {}) {
|
|
139
|
+
if (!Array.isArray(tools)) return
|
|
140
|
+
for (const tool of tools) {
|
|
141
|
+
if (!tool || typeof tool.description !== 'string' || !['bash', 'run_code'].includes(tool.name)) continue
|
|
142
|
+
let description = tool.description
|
|
143
|
+
const start = description.indexOf(ROUTING_START)
|
|
144
|
+
const end = start < 0 ? -1 : description.indexOf(ROUTING_END, start)
|
|
145
|
+
if (end >= 0) description = description.slice(0, start) + description.slice(end + ROUTING_END.length)
|
|
146
|
+
const hint = tool.name === 'bash'
|
|
147
|
+
? toolRoutingHintFor(tools, { pwshVisible }) + (msys ? '\n' + GITBASH_MSYS_ADDENDUM : '')
|
|
148
|
+
: PTC_ROUTING
|
|
149
|
+
tool.description = description + ROUTING_START + hint + ROUTING_END
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
121
153
|
/** @deprecated composed form kept for tests: route + MSYS sentences. */
|
|
122
154
|
export const GITBASH_BASH_ADDENDUM = GITBASH_ROUTE_ADDENDUM + '\n' + GITBASH_MSYS_ADDENDUM
|
|
123
155
|
|
|
@@ -158,7 +190,12 @@ export async function mountPrivateExecutor(ctx, GitBashSandboxExecutor, createSc
|
|
|
158
190
|
if (executorScope.fiber) await executorScope.fiber
|
|
159
191
|
const fiber = executorScope.ctx.plugin(GitBashSandboxExecutor)
|
|
160
192
|
await fiber
|
|
161
|
-
|
|
193
|
+
const executor = executorScope.ctx.get('shell')
|
|
194
|
+
const sandboxModule = await resolveHostModule(executorScope.ctx, '@deepseek-ai/dsh-sandbox')
|
|
195
|
+
if (typeof sandboxModule?.SandboxUnavailableError === 'function') {
|
|
196
|
+
executor.unavailableErrorType = sandboxModule.SandboxUnavailableError
|
|
197
|
+
}
|
|
198
|
+
return executor
|
|
162
199
|
})(),
|
|
163
200
|
mountTimeout,
|
|
164
201
|
])
|
|
@@ -198,6 +235,7 @@ export function mountInlineTools(ctx, deps) {
|
|
|
198
235
|
let closed = false
|
|
199
236
|
const isLive = (agent, state) => !closed && tracked.get(agent) === state && !state.controller.signal.aborted
|
|
200
237
|
const newState = () => ({ timer: undefined, scope: undefined, denyPwsh: undefined, pending: undefined, controller: new AbortController() })
|
|
238
|
+
const experimentalEnabled = () => deps.experimentalEnabled?.() !== false
|
|
201
239
|
// Loader imports cannot be cancelled, but no continuation may outlive its
|
|
202
240
|
// owner. Join our cancelled continuations without waiting on a stuck loader.
|
|
203
241
|
const untilDisposed = async (promise, signal) => {
|
|
@@ -247,6 +285,7 @@ export function mountInlineTools(ctx, deps) {
|
|
|
247
285
|
if (typeof createScope !== 'function') throw new Error('no createScope seam')
|
|
248
286
|
controller.signal.throwIfAborted()
|
|
249
287
|
const mounted = await mountPrivateExecutor(ctx, GitBashSandboxExecutor, createScope)
|
|
288
|
+
mounted.executor.experimentalEnabled = experimentalEnabled
|
|
250
289
|
executorScope = mounted.executorScope
|
|
251
290
|
controller.signal.throwIfAborted()
|
|
252
291
|
const hostModules = {}
|
|
@@ -255,7 +294,33 @@ export function mountInlineTools(ctx, deps) {
|
|
|
255
294
|
}
|
|
256
295
|
const { createBashToolModule } = await untilDisposed(import('./bash-tool.js'), controller.signal)
|
|
257
296
|
const module = createBashToolModule({ executor: mounted.executor, log, ...hostModules })
|
|
258
|
-
|
|
297
|
+
const extraModules = []
|
|
298
|
+
if (typeof deps.translatePath === 'function') {
|
|
299
|
+
const { createPathToolModule, PATH_TOOL_NAME } = await untilDisposed(import('./path-tool.js'), controller.signal)
|
|
300
|
+
extraModules.push({ name: PATH_TOOL_NAME, create: scope => createPathToolModule({
|
|
301
|
+
toolsModule: hostModules.toolsModule, translate: deps.translatePath, experimentalEnabled, log, scope,
|
|
302
|
+
}) })
|
|
303
|
+
try {
|
|
304
|
+
const localBashModule = await untilDisposed(resolveHostModule(ctx, '@deepseek-ai/dsh-bash-local'), controller.signal)
|
|
305
|
+
const outputModule = await untilDisposed(resolveHostModule(ctx, '@deepseek-ai/dsh-subprocess-local/output'), controller.signal)
|
|
306
|
+
if (typeof localBashModule?.LocalBashExecutor !== 'function'
|
|
307
|
+
|| typeof outputModule?.OutputCollector !== 'function') {
|
|
308
|
+
throw new Error('public native executor/OutputCollector capability unavailable')
|
|
309
|
+
}
|
|
310
|
+
const { createNativeOutputAdapter } = await untilDisposed(import('./native-output.js'), controller.signal)
|
|
311
|
+
const { createNativeToolModule } = await untilDisposed(import('./native-tool.js'), controller.signal)
|
|
312
|
+
extraModules.push({ name: 'gitbash_native', create: scope => createNativeToolModule({
|
|
313
|
+
toolsModule: hostModules.toolsModule, sandboxModule: hostModules.sandboxModule,
|
|
314
|
+
localBashModule, createScope, translateMsysPath: deps.translatePath,
|
|
315
|
+
createNativeOutputAdapter, outputOptions: { OutputCollector: outputModule?.OutputCollector },
|
|
316
|
+
createArtifactDirectory: deps.createArtifactDirectory, experimentalEnabled, log, scope,
|
|
317
|
+
}) })
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (controller.signal.aborted) throw error
|
|
320
|
+
log(`native tool capability unavailable; bash/path tools remain available: ${error?.message ?? error}`)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { executor: mounted.executor, executorScope, module, extraModules }
|
|
259
324
|
} catch (error) {
|
|
260
325
|
if (executorScope) await executorScope.dispose()
|
|
261
326
|
throw error
|
|
@@ -271,7 +336,7 @@ export function mountInlineTools(ctx, deps) {
|
|
|
271
336
|
let scope
|
|
272
337
|
let retained = false
|
|
273
338
|
try {
|
|
274
|
-
const { executor, module } = await untilDisposed(bashOncePromise(), state.controller.signal)
|
|
339
|
+
const { executor, module, extraModules } = await untilDisposed(bashOncePromise(), state.controller.signal)
|
|
275
340
|
if (!isLive(agent, state)) return
|
|
276
341
|
log(`own bash tool ready (private Git Bash executor at ${executor.bashPath || '(unresolved)'}; official pwsh executor untouched)`)
|
|
277
342
|
scope = createScope(agent.ctx)
|
|
@@ -285,19 +350,42 @@ export function mountInlineTools(ctx, deps) {
|
|
|
285
350
|
const fiber = scope.ctx.plugin(module)
|
|
286
351
|
await untilDisposed(fiber, state.controller.signal)
|
|
287
352
|
if (!isLive(agent, state)) return
|
|
288
|
-
retained = true
|
|
289
353
|
// An INACTIVE fiber resolves its await() without ever running apply()
|
|
290
354
|
// (issue #27): awaiting the plugin proves nothing, so read the registry
|
|
291
|
-
// back before claiming success. A miss
|
|
292
|
-
//
|
|
355
|
+
// back before claiming success. A miss THROWS — the catch below logs the
|
|
356
|
+
// explicit activation failure and the finally disposes this half-mounted
|
|
357
|
+
// scope instead of leaking it for the agent's lifetime (shape credit:
|
|
358
|
+
// KouzakiUmi's PR #28).
|
|
293
359
|
const registeredNames = scope.ctx.get !== undefined
|
|
294
360
|
? ((scope.ctx.get('tools')?.schemas?.(agent) ?? []).map((schema) => schema && typeof schema.name === 'string' ? schema.name : ''))
|
|
295
361
|
: []
|
|
296
|
-
if (registeredNames.includes('bash')) {
|
|
297
|
-
|
|
298
|
-
} else {
|
|
299
|
-
log('own bash tool did NOT register for this agent: the module fiber likely went INACTIVE (this host lacks an injected service the module requires) or the tools read-back is unavailable — bash is NOT available in this session; please report this with your dsh version (issue #27 pattern)')
|
|
362
|
+
if (registeredNames.includes('bash') === false) {
|
|
363
|
+
throw new Error('bash tool did not register for this agent (module fiber INACTIVE — this host lacks an injected service the module requires — or the tools read-back is unavailable); bash is NOT available in this session, please report this with your dsh version (issue #27)')
|
|
300
364
|
}
|
|
365
|
+
for (const extra of extraModules ?? []) {
|
|
366
|
+
let extraFiber
|
|
367
|
+
try {
|
|
368
|
+
let applied = false
|
|
369
|
+
const module = extra.create(agent)
|
|
370
|
+
extraFiber = scope.ctx.plugin({ ...module, async apply(extraCtx, config) {
|
|
371
|
+
await module.apply(extraCtx, config)
|
|
372
|
+
applied = true
|
|
373
|
+
} })
|
|
374
|
+
await untilDisposed(extraFiber, state.controller.signal)
|
|
375
|
+
if (!isLive(agent, state)) return
|
|
376
|
+
if (!applied) {
|
|
377
|
+
throw new Error(`${extra.name} scope inactive (a required service may be missing)`)
|
|
378
|
+
}
|
|
379
|
+
// Registration/stand-down is reported by the shared live registrar.
|
|
380
|
+
log(`${extra.name} scope ready; registration follows experimentalNative and existing owners`)
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (!isLive(agent, state)) return
|
|
383
|
+
await extraFiber?.dispose()
|
|
384
|
+
log(`${extra.name} activation failed; bash remains available: ${error?.message ?? error}`)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
retained = true
|
|
388
|
+
log('own bash tool activated for this agent (Git Bash execution; official pwsh tool real but hidden by default)')
|
|
301
389
|
} catch (error) {
|
|
302
390
|
if (isLive(agent, state)) log(`own bash tool activation failed for this agent: ${error?.message ?? error}`)
|
|
303
391
|
} finally {
|
|
@@ -402,7 +490,7 @@ export function mountInlineTools(ctx, deps) {
|
|
|
402
490
|
log(`agent bash tool cleanup failed: ${error?.message ?? error}`)
|
|
403
491
|
}))
|
|
404
492
|
ctx.on('loader/volatile-update', (paths) => {
|
|
405
|
-
if (!Array.isArray(paths) || !paths.some((path) => Array.isArray(path) && path[0]
|
|
493
|
+
if (!Array.isArray(paths) || !paths.some((path) => Array.isArray(path) && ['pwshTool', 'experimentalNative'].includes(path[0]))) return
|
|
406
494
|
for (const agent of tracked.keys()) {
|
|
407
495
|
try { reconcile(agent) } catch (error) { log(`pwsh switch update failed: ${error?.message ?? error}`) }
|
|
408
496
|
}
|