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/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). The class
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 EXCEPT the explicit Windows unconfined route below;
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 ALSO overrides the confined branch itself (issue #1):
21
- * historical MSYS2/restricted-token tests failed during msys-2.0.dll
22
- * cygheap/signal-pipe initialization (Win32 error 5 / 0xC0000142;
23
- * restricting-SID object permissions, CHANGELOG v0.13.2). No working
24
- * confined Git Bash implementation is provided here; this is not a claim
25
- * that every future runtime/isolation design is impossible. Windows calls
26
- * retain the explicit UNCONFINED route, labelled on the handle/result and
27
- * in model-facing foreground/background text, plus a one-time notice.
28
- * Official file-tool write/edit policy remains separate; it does not
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 spawned as a direct child of the host
32
- * process (never through the git-bash.exe login launcher), so it inherits the
33
- * full system environment plus the DSH_* snapshot exactly like the pwsh
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.constructor.name === 'AsyncFunction'
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
- * Decide whether one spec takes the Git Bash argv path, and how to label it
237
- * (pure). Full access always does — the shipped executor hardcodes the bare
238
- * `bash` name there. A Windows confined call does too (issue #1: MSYS2 cannot
239
- * start under the restricted-token runner). Everything else inherits the
240
- * parent's sandbox path, which already runs Git Bash because `confine` below
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 policy = spec.sandboxPolicy
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') return { mode, unconfined: true }
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
- if (route.unconfined) this.warnConfinedUnconfined(route.mode)
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
- const policy = spec.sandboxPolicy
371
- if (policy === undefined) return super.run(spec)
372
- const { mode } = policy
373
- if (mode === 'danger-full-access') {
374
- const { result } = GitBashSandboxExecutor.unwrapRunArgv(
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
- spec = this.withParityEnv(spec)
391
- const policy = spec.sandboxPolicy
392
- if (policy === undefined) return super.start(spec)
393
- const { mode } = policy
394
- const fullAccess = mode === 'danger-full-access'
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
- return super.start(spec)
404
- }
405
-
406
- /** One-per-instance notice that confined Windows calls run unconfined. */
407
- warnConfinedUnconfined(mode) {
408
- if (this._warnedUnconfined) return
409
- this._warnedUnconfined = true
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