dsh-gitbash-shell 0.39.0 → 0.39.2
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 +78 -0
- package/dsh.plugin.json +1 -1
- package/package.json +3 -2
- package/src/bash-tool.js +28 -17
- package/src/client.js +36 -25
- package/src/compositions.js +11 -7
- package/src/fastctx-bridge.js +125 -39
- package/src/index.js +163 -141
- package/src/inline-tools.js +151 -64
- package/src/shell.js +4 -3
package/src/inline-tools.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* 1. `pwsh` present + `bash` absent -> register the plugin's OWN `bash`
|
|
27
27
|
* tool (src/bash-tool.js) in an agent-owned scope. It executes through a
|
|
28
28
|
* PRIVATE GitBashSandboxExecutor instance (constructed once per process
|
|
29
|
-
* in a
|
|
29
|
+
* in a shell-isolated cordis scope, never registered as the ROOT shell), and mirrors
|
|
30
30
|
* the official tool's schema, background jobs, escalation and rendering
|
|
31
31
|
* call-for-call.
|
|
32
32
|
* 2. `ops_*` tools present (dsh-ops installed) -> our FastCtx bridge stands
|
|
@@ -137,6 +137,43 @@ const HOST_MODULES = [
|
|
|
137
137
|
['llmModule', '@deepseek-ai/dsh-llm'],
|
|
138
138
|
]
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Mount the executor through the host class-plugin path. A plugin fiber owns
|
|
142
|
+
* effects, but does NOT create a service namespace: ShellExecutor's constructor
|
|
143
|
+
* still provides `shell`. Isolate ONLY that service, retaining the host's
|
|
144
|
+
* subprocess/sandbox/policy dependencies and leaving the official shell intact.
|
|
145
|
+
*
|
|
146
|
+
* The empty scope does not inject shell, so read its active provider through
|
|
147
|
+
* get(), not the property-access path that requires an inject declaration.
|
|
148
|
+
*/
|
|
149
|
+
export async function mountPrivateExecutor(ctx, GitBashSandboxExecutor, createScope, { timeoutMs = 8000 } = {}) {
|
|
150
|
+
const executorScope = createScope(ctx.isolate('shell'))
|
|
151
|
+
let timer
|
|
152
|
+
try {
|
|
153
|
+
const mountTimeout = new Promise((_, reject) => {
|
|
154
|
+
timer = setTimeout(() => reject(new Error('executor scope mount timed out (a required service may be absent in this composition)')), timeoutMs)
|
|
155
|
+
})
|
|
156
|
+
const executor = await Promise.race([
|
|
157
|
+
(async () => {
|
|
158
|
+
if (executorScope.fiber) await executorScope.fiber
|
|
159
|
+
const fiber = executorScope.ctx.plugin(GitBashSandboxExecutor)
|
|
160
|
+
await fiber
|
|
161
|
+
return executorScope.ctx.get('shell')
|
|
162
|
+
})(),
|
|
163
|
+
mountTimeout,
|
|
164
|
+
])
|
|
165
|
+
if (!executor || typeof executor.resolve !== 'function' || typeof executor.execute !== 'function') {
|
|
166
|
+
throw new Error('executor instance did not expose resolve()/execute()')
|
|
167
|
+
}
|
|
168
|
+
return { executor, executorScope }
|
|
169
|
+
} catch (error) {
|
|
170
|
+
await executorScope.dispose()
|
|
171
|
+
throw error
|
|
172
|
+
} finally {
|
|
173
|
+
clearTimeout(timer)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
140
177
|
/**
|
|
141
178
|
* Mount the per-agent reconcile. `deps`:
|
|
142
179
|
* - tools(ctx) -> the host tool registry (or undefined)
|
|
@@ -154,8 +191,25 @@ const HOST_MODULES = [
|
|
|
154
191
|
*/
|
|
155
192
|
export function mountInlineTools(ctx, deps) {
|
|
156
193
|
const { tools, agents, createScope, pwshEnabled, log } = deps
|
|
157
|
-
/** agent
|
|
194
|
+
/** Per-agent mounts, masks and cancellation; `agents` above is the host service. */
|
|
158
195
|
const tracked = new Map()
|
|
196
|
+
const pending = new Set()
|
|
197
|
+
const controller = new AbortController()
|
|
198
|
+
let closed = false
|
|
199
|
+
const isLive = (agent, state) => !closed && tracked.get(agent) === state && !state.controller.signal.aborted
|
|
200
|
+
const newState = () => ({ timer: undefined, scope: undefined, denyPwsh: undefined, pending: undefined, controller: new AbortController() })
|
|
201
|
+
// Loader imports cannot be cancelled, but no continuation may outlive its
|
|
202
|
+
// owner. Join our cancelled continuations without waiting on a stuck loader.
|
|
203
|
+
const untilDisposed = async (promise, signal) => {
|
|
204
|
+
signal.throwIfAborted()
|
|
205
|
+
let abort
|
|
206
|
+
const cancelled = new Promise((_, reject) => {
|
|
207
|
+
abort = () => reject(signal.reason)
|
|
208
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
209
|
+
})
|
|
210
|
+
try { return await Promise.race([promise, cancelled]) }
|
|
211
|
+
finally { signal.removeEventListener('abort', abort) }
|
|
212
|
+
}
|
|
159
213
|
/** Resolved once per process: { executor, executorScope, module }. */
|
|
160
214
|
let bashOnce
|
|
161
215
|
/** Set as soon as any face shows ops_* tools (the call-layer guard reads it). */
|
|
@@ -177,71 +231,90 @@ export function mountInlineTools(ctx, deps) {
|
|
|
177
231
|
* declarations — the §4k schemastery semantics come for free), so it never
|
|
178
232
|
* touches the root `ctx.shell` service slot the official pwsh executor
|
|
179
233
|
* owns. The executor scope lives for the plugin's lifetime and is disposed
|
|
180
|
-
* by the effect registered
|
|
234
|
+
* by the ownership effect registered before lifecycle wiring.
|
|
181
235
|
*/
|
|
182
236
|
const bashOncePromise = async () => {
|
|
237
|
+
controller.signal.throwIfAborted()
|
|
183
238
|
if (bashOnce) return bashOnce
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
throw new Error('executor
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
hostModules[key] = await resolveHostModule(ctx, name)
|
|
209
|
-
} catch { hostModules[key] = undefined }
|
|
239
|
+
const mounting = (async () => {
|
|
240
|
+
let executorScope
|
|
241
|
+
try {
|
|
242
|
+
const { GitBashSandboxExecutor } = await untilDisposed(import('./shell.js'), controller.signal)
|
|
243
|
+
if (!GitBashSandboxExecutor) throw new Error('executor class unavailable')
|
|
244
|
+
if (!GitBashSandboxExecutor.Config) {
|
|
245
|
+
throw new Error('executor Config unavailable (schemastery unresolved; see the schemastery log lines above)')
|
|
246
|
+
}
|
|
247
|
+
if (typeof createScope !== 'function') throw new Error('no createScope seam')
|
|
248
|
+
controller.signal.throwIfAborted()
|
|
249
|
+
const mounted = await mountPrivateExecutor(ctx, GitBashSandboxExecutor, createScope)
|
|
250
|
+
executorScope = mounted.executorScope
|
|
251
|
+
controller.signal.throwIfAborted()
|
|
252
|
+
const hostModules = {}
|
|
253
|
+
for (const [key, name] of HOST_MODULES) {
|
|
254
|
+
hostModules[key] = await untilDisposed(resolveHostModule(ctx, name), controller.signal)
|
|
255
|
+
}
|
|
256
|
+
const { createBashToolModule } = await untilDisposed(import('./bash-tool.js'), controller.signal)
|
|
257
|
+
const module = createBashToolModule({ executor: mounted.executor, log, ...hostModules })
|
|
258
|
+
return { executor: mounted.executor, executorScope, module }
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (executorScope) await executorScope.dispose()
|
|
261
|
+
throw error
|
|
210
262
|
}
|
|
211
|
-
const { createBashToolModule } = await import('./bash-tool.js')
|
|
212
|
-
const module = createBashToolModule({ executor, log, ...hostModules })
|
|
213
|
-
return { executor, executorScope, module }
|
|
214
263
|
})()
|
|
215
|
-
bashOnce
|
|
216
|
-
|
|
264
|
+
bashOnce = mounting
|
|
265
|
+
mounting.catch(() => { if (bashOnce === mounting) bashOnce = undefined })
|
|
266
|
+
return mounting
|
|
217
267
|
}
|
|
218
268
|
|
|
219
269
|
/** Register the own bash tool inside one agent-owned scope. */
|
|
220
270
|
const provideBash = async (agent, state) => {
|
|
221
|
-
|
|
271
|
+
let scope
|
|
272
|
+
let retained = false
|
|
222
273
|
try {
|
|
223
|
-
const { executor, module } = await bashOncePromise()
|
|
274
|
+
const { executor, module } = await untilDisposed(bashOncePromise(), state.controller.signal)
|
|
275
|
+
if (!isLive(agent, state)) return
|
|
224
276
|
log(`own bash tool ready (private Git Bash executor at ${executor.bashPath || '(unresolved)'}; official pwsh executor untouched)`)
|
|
225
|
-
|
|
277
|
+
scope = createScope(agent.ctx)
|
|
278
|
+
state.scope = scope // Own the scope BEFORE either asynchronous mount.
|
|
226
279
|
// Same asynchronous-fiber shape the official module needed: wait for
|
|
227
280
|
// the apply callback to capture scope.ctx before touching its tools.
|
|
228
|
-
if (scope.fiber)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return
|
|
233
|
-
}
|
|
281
|
+
if (scope.fiber) await untilDisposed(scope.fiber, state.controller.signal)
|
|
282
|
+
if (!isLive(agent, state)) return
|
|
283
|
+
const registry = scope.ctx?.get('tools')
|
|
284
|
+
if (!registry || typeof scope.ctx.plugin !== 'function') return
|
|
234
285
|
const fiber = scope.ctx.plugin(module)
|
|
235
|
-
await fiber
|
|
236
|
-
state
|
|
286
|
+
await untilDisposed(fiber, state.controller.signal)
|
|
287
|
+
if (!isLive(agent, state)) return
|
|
288
|
+
retained = true
|
|
237
289
|
log('own bash tool activated for this agent (Git Bash execution; official pwsh tool real but hidden by default)')
|
|
238
290
|
} catch (error) {
|
|
239
|
-
log(`own bash tool activation failed for this agent: ${error?.message ?? error}`)
|
|
291
|
+
if (isLive(agent, state)) log(`own bash tool activation failed for this agent: ${error?.message ?? error}`)
|
|
292
|
+
} finally {
|
|
293
|
+
if (scope && !retained) {
|
|
294
|
+
if (state.scope === scope) state.scope = undefined
|
|
295
|
+
await scope.dispose()
|
|
296
|
+
}
|
|
240
297
|
}
|
|
241
298
|
}
|
|
242
299
|
|
|
300
|
+
const startBash = (agent, state) => {
|
|
301
|
+
if (state.scope || state.pending || !isLive(agent, state)) return
|
|
302
|
+
const task = provideBash(agent, state)
|
|
303
|
+
state.pending = task
|
|
304
|
+
pending.add(task)
|
|
305
|
+
void task.finally(() => {
|
|
306
|
+
pending.delete(task)
|
|
307
|
+
if (state.pending === task) state.pending = undefined
|
|
308
|
+
}).catch((error) => log(`own bash tool cleanup failed: ${error?.message ?? error}`))
|
|
309
|
+
}
|
|
310
|
+
|
|
243
311
|
const reconcile = (agent, { attempt = 0 } = {}) => {
|
|
244
|
-
if (!agent?.ctx) return
|
|
312
|
+
if (closed || !agent?.ctx) return
|
|
313
|
+
// Undo ONLY our mask before observing the face. schemas() filters a
|
|
314
|
+
// restricted-away pwsh, so it cannot tell us what an ON flip must restore.
|
|
315
|
+
const previous = tracked.get(agent)
|
|
316
|
+
if (previous?.timer) { clearTimeout(previous.timer); previous.timer = undefined }
|
|
317
|
+
if (previous?.denyPwsh) { previous.denyPwsh(); previous.denyPwsh = undefined }
|
|
245
318
|
const names = faceNames(agent)
|
|
246
319
|
// The composed face may not be wired yet at created-time. Retry bounded:
|
|
247
320
|
// a face with NEITHER tool carries no verdict. Five tries at 250ms, then
|
|
@@ -251,7 +324,7 @@ export function mountInlineTools(ctx, deps) {
|
|
|
251
324
|
timer.unref?.()
|
|
252
325
|
const state = tracked.get(agent)
|
|
253
326
|
if (state) state.timer = timer
|
|
254
|
-
else tracked.set(agent, {
|
|
327
|
+
else tracked.set(agent, { ...newState(), timer })
|
|
255
328
|
return
|
|
256
329
|
}
|
|
257
330
|
const pwshVisible = deps.pwshEnabled?.() === true
|
|
@@ -260,7 +333,7 @@ export function mountInlineTools(ctx, deps) {
|
|
|
260
333
|
let state = tracked.get(agent)
|
|
261
334
|
if (state?.timer) { clearTimeout(state.timer); state.timer = undefined }
|
|
262
335
|
if (!state) {
|
|
263
|
-
state =
|
|
336
|
+
state = newState()
|
|
264
337
|
tracked.set(agent, state)
|
|
265
338
|
}
|
|
266
339
|
// The pwsh face (route B): the host tool is REAL PowerShell now, so the
|
|
@@ -283,22 +356,46 @@ export function mountInlineTools(ctx, deps) {
|
|
|
283
356
|
}
|
|
284
357
|
}
|
|
285
358
|
if (needs.needsBash && !state.scope) {
|
|
286
|
-
|
|
359
|
+
startBash(agent, state)
|
|
287
360
|
}
|
|
288
361
|
}
|
|
289
362
|
|
|
290
|
-
const teardown = (agent) => {
|
|
363
|
+
const teardown = async (agent) => {
|
|
291
364
|
const state = tracked.get(agent)
|
|
292
365
|
if (!state) return
|
|
293
366
|
tracked.delete(agent)
|
|
367
|
+
state.controller.abort(new Error('inline bash agent disposed'))
|
|
294
368
|
if (state.timer) clearTimeout(state.timer)
|
|
295
|
-
if (state.denyPwsh) {
|
|
296
|
-
|
|
369
|
+
if (state.denyPwsh) { state.denyPwsh(); state.denyPwsh = undefined }
|
|
370
|
+
await Promise.all([state.scope?.dispose(), state.pending])
|
|
297
371
|
}
|
|
298
372
|
|
|
373
|
+
// Register ownership before the sweep starts asynchronous work. A cancelled
|
|
374
|
+
// loader continuation and every per-agent mount must settle before disposal.
|
|
375
|
+
ctx.effect(() => async () => {
|
|
376
|
+
closed = true
|
|
377
|
+
const mounting = bashOnce
|
|
378
|
+
controller.abort(new Error('inline bash plugin disposed'))
|
|
379
|
+
await Promise.all([
|
|
380
|
+
...[...tracked.keys()].map(teardown),
|
|
381
|
+
...pending,
|
|
382
|
+
mounting?.then(({ executorScope }) => executorScope.dispose(), () => {}),
|
|
383
|
+
])
|
|
384
|
+
tracked.clear()
|
|
385
|
+
bashOnce = undefined
|
|
386
|
+
}, 'dsh-gitbash-shell: inline tool patching')
|
|
387
|
+
|
|
299
388
|
try {
|
|
300
389
|
ctx.on('agent/created', ({ agent } = {}) => { try { reconcile(agent) } catch { /* never break agent creation */ } })
|
|
301
|
-
ctx.on('agent/disposed', ({ agent } = {}) =>
|
|
390
|
+
ctx.on('agent/disposed', ({ agent } = {}) => teardown(agent).catch((error) => {
|
|
391
|
+
log(`agent bash tool cleanup failed: ${error?.message ?? error}`)
|
|
392
|
+
}))
|
|
393
|
+
ctx.on('loader/volatile-update', (paths) => {
|
|
394
|
+
if (!Array.isArray(paths) || !paths.some((path) => Array.isArray(path) && path[0] === 'pwshTool')) return
|
|
395
|
+
for (const agent of tracked.keys()) {
|
|
396
|
+
try { reconcile(agent) } catch (error) { log(`pwsh switch update failed: ${error?.message ?? error}`) }
|
|
397
|
+
}
|
|
398
|
+
})
|
|
302
399
|
// MOUNT-TIME SWEEP (v0.35.2): agents that already exist when this row
|
|
303
400
|
// mounts never fire agent/created. Same shape the proven dsh-ops denial
|
|
304
401
|
// uses.
|
|
@@ -322,16 +419,6 @@ export function mountInlineTools(ctx, deps) {
|
|
|
322
419
|
return
|
|
323
420
|
}
|
|
324
421
|
|
|
325
|
-
ctx.effect(() => () => {
|
|
326
|
-
for (const agent of [...tracked.keys()]) teardown(agent)
|
|
327
|
-
tracked.clear()
|
|
328
|
-
if (bashOnce) {
|
|
329
|
-
void Promise.resolve(bashOnce).then(({ executorScope } = {}) => {
|
|
330
|
-
try { executorScope?.dispose?.() } catch { /* already gone */ }
|
|
331
|
-
}).catch(() => {})
|
|
332
|
-
bashOnce = undefined
|
|
333
|
-
}
|
|
334
|
-
}, 'dsh-gitbash-shell: inline tool patching')
|
|
335
422
|
}
|
|
336
423
|
|
|
337
424
|
// The host-package resolution channel lives in bash-tool.js; import it for
|
package/src/shell.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-gitbash-shell/shell — the Git Bash executor INSTANCE (route B, v0.39.0).
|
|
3
3
|
*
|
|
4
|
-
* Since route B this class is NOT mounted as the
|
|
4
|
+
* Since route B this class is NOT mounted as the ROOT `ctx.shell` service:
|
|
5
5
|
* the official PowerShell executor keeps that slot untouched (the DSH-Store
|
|
6
6
|
* catalog gate forbids bundle patches that disable official components, and
|
|
7
7
|
* replacing the shell executor IS overriding one). Instead the plugin
|
|
@@ -36,8 +36,9 @@
|
|
|
36
36
|
* executor did.
|
|
37
37
|
*
|
|
38
38
|
* Constructed as a cordis class plugin (static inject/Config honoured) —
|
|
39
|
-
* inside
|
|
40
|
-
*
|
|
39
|
+
* inside an explicitly `ctx.isolate('shell')`-isolated scope, so the base
|
|
40
|
+
* class's service registration never touches the root `ctx.shell` slot.
|
|
41
|
+
* A plain child plugin fiber is NOT a service isolation boundary.
|
|
41
42
|
*/
|
|
42
43
|
|
|
43
44
|
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
|