bwrap.e2e.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import { spawnSync } from 'node:child_process'
  2. import { existsSync, readFileSync } from 'node:fs'
  3. import { mkdtemp, rm } from 'node:fs/promises'
  4. import { homedir } from 'node:os'
  5. import { join } from 'node:path'
  6. import { afterEach, describe, expect, it } from 'vitest'
  7. import { Context } from 'cordis'
  8. import { bwrapProfileArgs, LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
  9. import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
  10. /**
  11. * KEYLESS consumer-integration proof under bwrap: the REAL
  12. * `LocalSandboxProvider` (nothing forced — bwrap is the ladder's first rung,
  13. * so a passing probe selects it) underneath the REAL `SandboxBashExecutor`,
  14. * driven through the executor's public run/start paths. Verifies the WORLD
  15. * (files exist or don't) plus the stamped result facts — in particular that
  16. * bwrap's EROFS denial text classifies as `denied: true` through the
  17. * wrap-carried dialect; the backend-only confinement proofs live with
  18. * `@deepseek-ai/dsh-sandbox-local`.
  19. *
  20. * Self-skips wherever the functional probe fails — no `bwrap` on PATH, or a
  21. * host that denies unprivileged user namespaces.
  22. *
  23. * HOME-based dirs on purpose: bwrap's `/tmp` is an ephemeral mount, so only
  24. * paths outside it prove the workspace-root boundary.
  25. */
  26. const probe = spawnSync('bwrap', [...bwrapProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: 5_000, stdio: 'ignore' })
  27. const bwrapUsable = probe.status === 0
  28. let ctx: Context | undefined
  29. const tempDirs: string[] = []
  30. afterEach(async () => {
  31. await ctx?.fiber.dispose()
  32. ctx = undefined
  33. await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
  34. })
  35. async function tempDir(base: string): Promise<string> {
  36. const dir = await mkdtemp(join(base, 'dsh-bwrap-e2e-'))
  37. tempDirs.push(dir)
  38. return dir
  39. }
  40. async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-write'): Promise<SandboxBashExecutor> {
  41. ctx = new Context()
  42. await ctx.plugin(LocalSandboxProvider, {})
  43. await ctx.plugin(SandboxBashExecutor, { mode, cwd: workspace, workspaceRoot: workspace, timeoutMs: 30_000 })
  44. return ctx.bash as SandboxBashExecutor
  45. }
  46. describe.skipIf(!bwrapUsable)('bash-sandbox: real bwrap confinement through ctx.bash', () => {
  47. it('read-only denies a write — the file must NOT exist, and EROFS text classifies as a denial', async () => {
  48. const workdir = await tempDir(homedir())
  49. const bash = await sandboxedBash(workdir, 'read-only')
  50. const result = await bash.run(bash.resolve({ command: `echo hi > ${workdir}/denied.txt` }))
  51. expect(result.exitCode).not.toBe(0)
  52. expect(result.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
  53. expect(existsSync(join(workdir, 'denied.txt'))).toBe(false)
  54. })
  55. it('workspace-write lands a write inside the workspace root and still denies one beside it', async () => {
  56. const workdir = await tempDir(homedir())
  57. const outside = await tempDir(homedir())
  58. const bash = await sandboxedBash(workdir, 'workspace-write')
  59. const inside = await bash.run(bash.resolve({ command: `printf bwrap-ok > ${workdir}/allowed.txt` }))
  60. expect(inside.exitCode).toBe(0)
  61. expect(inside.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' })
  62. expect(readFileSync(join(workdir, 'allowed.txt'), 'utf8')).toBe('bwrap-ok')
  63. const denied = await bash.run(bash.resolve({ command: `echo hi > ${outside}/denied.txt` }))
  64. expect(denied.exitCode).not.toBe(0)
  65. expect(denied.sandbox).toEqual({ mode: 'workspace-write', denied: true, enforcement: 'full' })
  66. expect(existsSync(join(outside, 'denied.txt'))).toBe(false)
  67. })
  68. it('classifies a background denial once the task settles', async () => {
  69. const workdir = await tempDir(homedir())
  70. const bash = await sandboxedBash(workdir, 'read-only')
  71. const task = bash.start(bash.resolve({ command: `echo hi > ${workdir}/bg-denied.txt` }))
  72. await task.done
  73. expect(task.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
  74. expect(existsSync(join(workdir, 'bg-denied.txt'))).toBe(false)
  75. })
  76. it('an approved escalated retry — the spec-level workspace-write override — lands the exact write read-only denied', async () => {
  77. const workdir = await tempDir(homedir())
  78. const bash = await sandboxedBash(workdir, 'read-only')
  79. const command = `printf escalated > ${workdir}/escalated.txt`
  80. const strict = await bash.run(bash.resolve({ command }))
  81. expect(strict.exitCode).not.toBe(0)
  82. expect(strict.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
  83. expect(existsSync(join(workdir, 'escalated.txt'))).toBe(false)
  84. const retried = await bash.run(bash.resolve({ command, sandboxMode: 'workspace-write' }))
  85. expect(retried.exitCode).toBe(0)
  86. expect(retried.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' })
  87. expect(readFileSync(join(workdir, 'escalated.txt'), 'utf8')).toBe('escalated')
  88. })
  89. })