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/shell.js
CHANGED
|
@@ -7,31 +7,28 @@
|
|
|
7
7
|
* replacing the shell executor IS overriding one). Instead the plugin
|
|
8
8
|
* constructs ONE private instance of this class in a PRIVATE cordis scope
|
|
9
9
|
* (`executorScope.ctx.plugin(GitBashSandboxExecutor)`, src/inline-tools.js)
|
|
10
|
-
* and hands it to the plugin's OWN bash tool (src/bash-tool.js).
|
|
11
|
-
* body is unchanged from its executor-row era:
|
|
10
|
+
* and hands it to the plugin's OWN bash tool (src/bash-tool.js).
|
|
12
11
|
*
|
|
13
12
|
* - extends @deepseek-ai/dsh-bash-sandbox to inherit policy, denial and
|
|
14
|
-
* lifecycle semantics
|
|
13
|
+
* lifecycle semantics; unsupported Windows policy is refused, not weakened;
|
|
15
14
|
* - overrides `confine` so the inner argv is [git-bash, -c, command];
|
|
16
15
|
* - overrides run/start ONLY for danger-full-access (the parent's
|
|
17
16
|
* full-access branch calls LocalBashExecutor.run, which hardcodes the
|
|
18
17
|
* bare `bash` name), routing through runArgv/startArgv with the same
|
|
19
18
|
* argv as the confined branch;
|
|
20
|
-
* - on Windows
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* Official file-tool
|
|
29
|
-
* constrain shell code and its denials must never be bypassed through shell.
|
|
19
|
+
* - on Windows refuses read-only/workspace-write or unresolved policy before
|
|
20
|
+
* argv resolution or process start. Historical restricted-token failures
|
|
21
|
+
* (Win32 error 5 / 0xC0000142, CHANGELOG v0.13.2) are not proof that every
|
|
22
|
+
* future isolation design is impossible. The matched modern MSYS signal
|
|
23
|
+
* pipe has an explicit SD; cygheap uses VirtualAlloc, and Core already
|
|
24
|
+
* supplements TokenDefaultDacl. No verified confined Git Bash is provided
|
|
25
|
+
* here. experimentalNative defaults ON (refusal); an explicit user opt-out
|
|
26
|
+
* restores the labelled legacy unconfined route, with truthful facts.
|
|
27
|
+
* Official file-tool policy is separate and its denials must not be bypassed.
|
|
30
28
|
*
|
|
31
|
-
* Environment inheritance: bash.exe is
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* executor did.
|
|
29
|
+
* Environment inheritance: bash.exe is a direct child (not a login launcher)
|
|
30
|
+
* of the host's managed, credential-scrubbing subprocess provider. The trusted
|
|
31
|
+
* DSH_* snapshot does not exempt it from protected environment rules.
|
|
35
32
|
*
|
|
36
33
|
* Constructed as a cordis class plugin (static inject/Config honoured) —
|
|
37
34
|
* inside an explicitly `ctx.isolate('shell')`-isolated scope, so the base
|
|
@@ -48,9 +45,6 @@ export { DEFAULT_GIT_BASH }
|
|
|
48
45
|
import { loadSchemastery } from './schemastery.js'
|
|
49
46
|
|
|
50
47
|
|
|
51
|
-
/** Log prefix, matching src/index.js. */
|
|
52
|
-
const TAG = '[gitbash-shell]'
|
|
53
|
-
|
|
54
48
|
/**
|
|
55
49
|
* The plugin row's id, which doubles as the settings namespace on <= 0.1.6 and
|
|
56
50
|
* as the row-config namespace on 0.1.7+ (same string by design).
|
|
@@ -212,7 +206,8 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
212
206
|
* @returns true when the base class start is an async function.
|
|
213
207
|
*/
|
|
214
208
|
static baseStartIsAsync() {
|
|
215
|
-
return SandboxBashExecutor.prototype.start
|
|
209
|
+
return typeof SandboxBashExecutor.prototype.start === 'function'
|
|
210
|
+
&& SandboxBashExecutor.prototype.start.constructor.name === 'AsyncFunction'
|
|
216
211
|
}
|
|
217
212
|
|
|
218
213
|
/**
|
|
@@ -232,25 +227,54 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
232
227
|
return { result: raw, spawnRequested: true }
|
|
233
228
|
}
|
|
234
229
|
|
|
230
|
+
/** Construct the host's structured refusal when its module was resolved. */
|
|
231
|
+
policyUnavailableError(mode) {
|
|
232
|
+
const knownMode = mode === 'read-only' || mode === 'workspace-write' ? mode : 'unresolved'
|
|
233
|
+
const detail = 'dsh-gitbash-shell does not provide verified Windows Git for Windows confinement. '
|
|
234
|
+
+ 'The command was not started; no unconfined fallback is permitted. '
|
|
235
|
+
+ 'Do not bypass this refusal with another tool or change permissions automatically.'
|
|
236
|
+
+ ' The user can open Settings > Plugins > dsh-gitbash-shell and turn off '
|
|
237
|
+
+ '"Experimental native execution / fail-closed" (experimentalNative) to restore the legacy '
|
|
238
|
+
+ 'explicitly UNCONFINED Bash behavior, or select danger-full-access in the session access-mode control.'
|
|
239
|
+
if (typeof this.unavailableErrorType === 'function') return new this.unavailableErrorType(knownMode, detail)
|
|
240
|
+
// Direct/older callers without the optional host error export still refuse.
|
|
241
|
+
const error = new Error('sandbox mode "' + knownMode + '" cannot execute Git Bash on this Windows host. ' + detail)
|
|
242
|
+
error.name = 'SandboxUnavailableError'
|
|
243
|
+
error.code = 'SANDBOX_UNAVAILABLE'
|
|
244
|
+
return error
|
|
245
|
+
}
|
|
246
|
+
|
|
235
247
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* substitutes the argv.
|
|
242
|
-
* @param spec the shell request.
|
|
243
|
-
* @returns `{ mode, unconfined }`, or undefined to inherit the parent path.
|
|
248
|
+
* Full access uses explicit Git Bash argv. Windows restricted or unresolved
|
|
249
|
+
* policy fails BEFORE argv resolution, job admission or spawning; disclosure
|
|
250
|
+
* of an unconfined route is not permission to weaken the requested policy.
|
|
251
|
+
* Non-Windows restricted calls retain the parent's actual sandbox path.
|
|
252
|
+
* @returns `{ mode, unconfined: false }`, or undefined to inherit.
|
|
244
253
|
*/
|
|
245
254
|
gitBashRoute(spec) {
|
|
246
|
-
const
|
|
247
|
-
const mode = policy === undefined ? undefined : policy.mode
|
|
248
|
-
if (mode === undefined) return undefined
|
|
255
|
+
const mode = spec.sandboxPolicy?.mode
|
|
249
256
|
if (mode === 'danger-full-access') return { mode, unconfined: false }
|
|
250
|
-
if (process.platform === 'win32')
|
|
257
|
+
if (process.platform === 'win32') {
|
|
258
|
+
if (this.experimentalEnabled?.() !== false) throw this.policyUnavailableError(mode)
|
|
259
|
+
// Exact legacy opt-out: unresolved policy still delegates to the host.
|
|
260
|
+
if (mode !== undefined) return { mode, unconfined: true }
|
|
261
|
+
}
|
|
251
262
|
return undefined
|
|
252
263
|
}
|
|
253
264
|
|
|
265
|
+
warnConfinedUnconfined(mode) {
|
|
266
|
+
if (this._warnedUnconfined) return
|
|
267
|
+
this._warnedUnconfined = true
|
|
268
|
+
console.log('dsh-gitbash-shell: experimentalNative is OFF. Requested mode ' + mode
|
|
269
|
+
+ ' uses an UNCONFINED Git Bash route: shell file-write restrictions are NOT enforced. '
|
|
270
|
+
+ 'Official file-tool policy is separate; never bypass its denial through shell.')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
routeSandbox(route) {
|
|
274
|
+
if (route.unconfined) this.warnConfinedUnconfined(route.mode)
|
|
275
|
+
return { mode: route.mode, denied: false, ...(route.unconfined ? { enforcement: 'unconfined' } : {}) }
|
|
276
|
+
}
|
|
277
|
+
|
|
254
278
|
/**
|
|
255
279
|
* Label one execution handle's foreground result without changing its
|
|
256
280
|
* identity (the parent's `decorateResult` is private to the shipped class, so
|
|
@@ -285,16 +309,13 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
285
309
|
* @param spec resolved execution settings and caller-owned command metadata.
|
|
286
310
|
* @returns the live execution handle.
|
|
287
311
|
*/
|
|
288
|
-
execute(spec) {
|
|
312
|
+
async execute(spec) {
|
|
289
313
|
if (typeof super.execute !== 'function') return this.run(spec)
|
|
314
|
+
const route = this.gitBashRoute(spec)
|
|
290
315
|
const routed = this.withParityEnv(spec)
|
|
291
|
-
const route = this.gitBashRoute(routed)
|
|
292
316
|
if (route === undefined) return super.execute(routed)
|
|
293
317
|
const argv = [this.requireBashPath(), '-c', routed.command]
|
|
294
|
-
|
|
295
|
-
const sandbox = route.unconfined
|
|
296
|
-
? { mode: route.mode, denied: false, enforcement: 'unconfined' }
|
|
297
|
-
: { mode: route.mode, denied: false }
|
|
318
|
+
const sandbox = this.routeSandbox(route)
|
|
298
319
|
// `executeArgv` is the shipped helper for exactly this: run an explicit
|
|
299
320
|
// argv with the executor's own lifecycle, output and deadline semantics.
|
|
300
321
|
return Promise.resolve(this.executeArgv(routed, argv))
|
|
@@ -366,48 +387,29 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
|
|
|
366
387
|
* old-era entry point.
|
|
367
388
|
*/
|
|
368
389
|
async run(spec) {
|
|
390
|
+
const route = this.gitBashRoute(spec)
|
|
369
391
|
spec = this.withParityEnv(spec)
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
const {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
await this.runArgv(spec, [this.requireBashPath(), '-c', spec.command]))
|
|
376
|
-
return { ...result, sandbox: { mode, denied: false } }
|
|
377
|
-
}
|
|
378
|
-
if (process.platform === 'win32') {
|
|
379
|
-
this.warnConfinedUnconfined(mode)
|
|
380
|
-
const { result, spawnRequested } = GitBashSandboxExecutor.unwrapRunArgv(
|
|
381
|
-
await this.runArgv(spec, [this.requireBashPath(), '-c', spec.command]))
|
|
382
|
-
// No spawn means no argv ran, so there is nothing to label as
|
|
383
|
-
// unconfined; the parent reports the same bare sandbox facts there.
|
|
384
|
-
return { ...result, sandbox: spawnRequested ? { mode, denied: false, enforcement: 'unconfined' } : { mode, denied: false } }
|
|
385
|
-
}
|
|
386
|
-
return super.run(spec)
|
|
392
|
+
if (route === undefined) return super.run(spec)
|
|
393
|
+
if (route.unconfined) this.warnConfinedUnconfined(route.mode)
|
|
394
|
+
const { result, spawnRequested } = GitBashSandboxExecutor.unwrapRunArgv(
|
|
395
|
+
await this.runArgv(spec, [this.requireBashPath(), '-c', spec.command]))
|
|
396
|
+
return { ...result, sandbox: this.routeSandbox({ ...route, unconfined: route.unconfined && spawnRequested }) }
|
|
387
397
|
}
|
|
388
398
|
|
|
389
399
|
start(spec) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (fullAccess || process.platform === 'win32') {
|
|
396
|
-
if (!fullAccess) this.warnConfinedUnconfined(mode)
|
|
397
|
-
const proc = this.startArgv(spec, [this.requireBashPath(), '-c', spec.command])
|
|
398
|
-
proc.sandbox = fullAccess ? { mode, denied: false } : { mode, denied: false, enforcement: 'unconfined' }
|
|
399
|
-
// Match the loaded base contract: the async one (dsh >= 0.1.6) is
|
|
400
|
-
// awaited by its callers, the synchronous one is consumed directly.
|
|
401
|
-
return GitBashSandboxExecutor.baseStartIsAsync() ? Promise.resolve(proc) : proc
|
|
400
|
+
let route
|
|
401
|
+
try { route = this.gitBashRoute(spec) }
|
|
402
|
+
catch (error) {
|
|
403
|
+
if (GitBashSandboxExecutor.baseStartIsAsync()) return Promise.reject(error)
|
|
404
|
+
throw error
|
|
402
405
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
console.log(TAG + ' Windows: confined Git Bash is not provided by this restricted-token backend (issue #1). Requested mode ' + mode + ' will use an UNCONFINED Git Bash route: shell file-write restrictions are NOT enforced. Official file-tool write/edit policy is separate; never bypass its denial through shell.')
|
|
406
|
+
spec = this.withParityEnv(spec)
|
|
407
|
+
if (route === undefined) return super.start(spec)
|
|
408
|
+
const proc = this.startArgv(spec, [this.requireBashPath(), '-c', spec.command])
|
|
409
|
+
const label = (handle) => { handle.sandbox = this.routeSandbox(route); return handle }
|
|
410
|
+
// Preserve the old host's synchronous/async start contract, including its
|
|
411
|
+
// refusal shape; modern execute() callers never enter this legacy method.
|
|
412
|
+
return GitBashSandboxExecutor.baseStartIsAsync() ? Promise.resolve(proc).then(label) : label(proc)
|
|
411
413
|
}
|
|
412
414
|
}
|
|
413
415
|
|