feat(bash): generalize managed shell environment

This commit is contained in:
Yichen Jiang
2026-07-12 15:41:42 +08:00
parent acdbe7b828
commit df9617aaff
40 changed files with 790 additions and 197 deletions

View File

@@ -136,21 +136,28 @@ describe('LocalBashExecutor.run', () => {
await expect(bash.run(bash.resolve({ command: 'true', workdir: '/nonexistent-dsh' }))).rejects.toThrow(/ENOENT/)
})
it('resolve() carries stdin/env onto the spec, and run() threads them to the command', async () => {
it('resolve() carries stdin/env/dshEnv onto the spec, and run() threads them to the command', async () => {
const { bash } = await setup()
const spec = bash.resolve({ command: 'cat; echo "[$DSH_SEAM_VAR]"', stdin: 'piped\n', env: { DSH_SEAM_VAR: 'env-ok' } })
// resolve() keeps the stdin/env fields verbatim (optional, no default).
const spec = bash.resolve({
command: 'cat; echo "[$SEAM_VAR][$DSH_SEAM_VAR]"',
stdin: 'piped\n',
env: { SEAM_VAR: 'env-ok' },
dshEnv: { DSH_SEAM_VAR: 'dsh-ok' },
})
// resolve() keeps the optional input/environment fields verbatim.
expect(spec.stdin).toBe('piped\n')
expect(spec.env).toEqual({ DSH_SEAM_VAR: 'env-ok' })
expect(spec.env).toEqual({ SEAM_VAR: 'env-ok' })
expect(spec.dshEnv).toEqual({ DSH_SEAM_VAR: 'dsh-ok' })
const result = await bash.run(spec)
expect(result.stdout.text).toBe('piped\n[env-ok]\n')
expect(result.stdout.text).toBe('piped\n[env-ok][dsh-ok]\n')
})
it('resolve() omits stdin/env when the request supplies neither', async () => {
it('resolve() omits stdin/env/dshEnv when the request supplies none', async () => {
const { bash } = await setup()
const spec = bash.resolve({ command: 'true' })
expect('stdin' in spec).toBe(false)
expect('env' in spec).toBe(false)
expect('dshEnv' in spec).toBe(false)
})
})
@@ -177,14 +184,15 @@ describe('LocalBashExecutor background tasks', () => {
await Promise.all([first.done, second.done])
})
it('threads stdin and extra env into a background task', async () => {
it('threads stdin, ordinary env, and managed env into a background task', async () => {
const { bash } = await setup()
const task = bash.start(bash.resolve({
command: 'cat; echo "[$DSH_BG_VAR]"',
command: 'cat; echo "[$BG_VAR][$DSH_BG_VAR]"',
stdin: 'bg-stdin\n',
env: { DSH_BG_VAR: 'bg-env' },
env: { BG_VAR: 'bg-env' },
dshEnv: { DSH_BG_VAR: 'bg-dsh-env' },
}))
const read = await readUntil(bash, task.id, '[bg-env]')
const read = await readUntil(bash, task.id, '[bg-env][bg-dsh-env]')
expect(read.delta).toContain('bg-stdin')
await task.done
expect(task.exitCode).toBe(0)

View File

@@ -201,19 +201,19 @@ describe('stdin and extra env (set by in-process plugins)', () => {
expect(piped.stdout.text).toBe('socket\n')
})
it('merges extra env entries onto the scrubbed environment', async () => {
const result = await runBash(spec('echo "$DSH_EXTRA_ONE/$DSH_EXTRA_TWO"', {
env: { DSH_EXTRA_ONE: 'alpha', DSH_EXTRA_TWO: 'beta' },
it('merges ordinary extra env entries onto the scrubbed environment', async () => {
const result = await runBash(spec('echo "$EXTRA_ONE/$EXTRA_TWO"', {
env: { EXTRA_ONE: 'alpha', EXTRA_TWO: 'beta' },
})).done
expect(result.stdout.text).toBe('alpha/beta\n')
})
it('an explicit extra env entry overrides the model-friendly override and the scrub', async () => {
// TERM is a model-friendly OVERRIDE (dumb); an explicit extra entry wins.
// DSH_OVERRIDE_KEY matches the credential scrub pattern, yet an explicit
// EXPLICIT_OVERRIDE_KEY matches the credential scrub pattern, yet an explicit
// entry is still honored — the scrub only drops AMBIENT process.env creds.
const result = await runBash(spec('echo "$TERM/$DSH_OVERRIDE_KEY"', {
env: { TERM: 'xterm-256color', DSH_OVERRIDE_KEY: 'explicit-wins' },
const result = await runBash(spec('echo "$TERM/$EXPLICIT_OVERRIDE_KEY"', {
env: { TERM: 'xterm-256color', EXPLICIT_OVERRIDE_KEY: 'explicit-wins' },
})).done
expect(result.stdout.text).toBe('xterm-256color/explicit-wins\n')
})
@@ -361,13 +361,13 @@ describe('abort edge cases', () => {
})
describe('review fixes: env scrubbing and spill hardening', () => {
it('scrubs credential-shaped env vars from child processes', async () => {
it('scrubs credential-shaped and ambient DSH env vars from child processes', async () => {
process.env.DSH_TEST_API_KEY = 'super-secret'
process.env.DSH_TEST_TOKEN = 'also-secret'
process.env.DSH_TEST_PLAIN = 'visible'
try {
const result = await runBash(spec('echo "[${DSH_TEST_API_KEY:-absent}|${DSH_TEST_TOKEN:-absent}|${DSH_TEST_PLAIN:-absent}]"')).done
expect(result.stdout.text.trim()).toBe('[absent|absent|visible]')
expect(result.stdout.text.trim()).toBe('[absent|absent|absent]')
} finally {
delete process.env.DSH_TEST_API_KEY
delete process.env.DSH_TEST_TOKEN
@@ -375,6 +375,23 @@ describe('review fixes: env scrubbing and spill hardening', () => {
}
})
it('injects only the current trusted DSH environment after scrubbing ambient values', async () => {
process.env.DSH_STALE = 'old-value'
try {
const result = await runBash(spec('echo "[${DSH_STALE:-absent}|$DSH_SHELL|$DSH_SESSION_ID]"', {
dshEnv: { DSH_SHELL: '1', DSH_SESSION_ID: 'current-session' },
})).done
expect(result.stdout.text.trim()).toBe('[absent|1|current-session]')
} finally {
delete process.env.DSH_STALE
}
})
it('rejects DSH variables on the ordinary env channel', () => {
expect(() => runBash(spec('true', { env: { DSH_WRONG_CHANNEL: 'bad' } })))
.toThrow(/DSH_WRONG_CHANNEL.*dshEnv/)
})
it('creates spill files with owner-only permissions and random names', async () => {
const result = await runBash(
spec('for i in $(seq 1 200); do printf "line-%04d\\n" $i; done', { maxOutputBytes: 500 }),