seatbelt.e2e.ts 4.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  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 { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
  9. import { seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts'
  10. import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
  11. /**
  12. * Keyless macOS integration of the real provider and executor through public run/start paths.
  13. * Linux rungs are forced off so Seatbelt is selected. The tests check world effects and stamped
  14. * facts, including EPERM classification through the wrap-carried dialect; backend-only
  15. * confinement is covered by `@deepseek-ai/dsh-sandbox-local`. Skips off macOS or when
  16. * `sandbox-exec` rejects the profile.
  17. */
  18. const probe = spawnSync('sandbox-exec', [...seatbeltProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: 5_000, stdio: 'ignore' })
  19. const seatbeltUsable = probe.status === 0
  20. let ctx: Context | undefined
  21. const tempDirs: string[] = []
  22. afterEach(async () => {
  23. await ctx?.fiber.dispose()
  24. ctx = undefined
  25. await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
  26. })
  27. async function tempDir(base: string): Promise<string> {
  28. const dir = await mkdtemp(join(base, 'dsh-seatbelt-e2e-'))
  29. tempDirs.push(dir)
  30. return dir
  31. }
  32. async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-write'): Promise<SandboxBashExecutor> {
  33. ctx = new Context()
  34. await ctx.plugin(LocalSandboxProvider, {})
  35. ;(ctx.sandbox as LocalSandboxProvider).internals = { probeBwrap: () => false, probeLandlock: () => 'unusable' }
  36. await ctx.plugin(SandboxBashExecutor, { mode, cwd: workspace, workspaceRoot: workspace, timeoutMs: 30_000 })
  37. return ctx.bash as SandboxBashExecutor
  38. }
  39. describe.skipIf(!seatbeltUsable)('bash-sandbox: real Seatbelt confinement through ctx.bash', () => {
  40. it('read-only denies a write — the file must NOT exist, and EPERM text classifies as a denial', async () => {
  41. const workdir = await tempDir(homedir())
  42. const bash = await sandboxedBash(workdir, 'read-only')
  43. const result = await bash.run(bash.resolve({ command: `echo hi > ${workdir}/denied.txt` }))
  44. expect(result.exitCode).not.toBe(0)
  45. expect(result.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
  46. expect(existsSync(join(workdir, 'denied.txt'))).toBe(false)
  47. })
  48. it('workspace-write lands a write inside the workspace root and still denies one beside it', async () => {
  49. // HOME-based dirs on purpose: workspace-write grants /tmp and the
  50. // per-user temp dir wholesale, so only paths outside both prove the
  51. // workspace-root boundary.
  52. const workdir = await tempDir(homedir())
  53. const outside = await tempDir(homedir())
  54. const bash = await sandboxedBash(workdir, 'workspace-write')
  55. const inside = await bash.run(bash.resolve({ command: `printf seatbelt-ok > ${workdir}/allowed.txt` }))
  56. expect(inside.exitCode).toBe(0)
  57. expect(inside.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' })
  58. expect(readFileSync(join(workdir, 'allowed.txt'), 'utf8')).toBe('seatbelt-ok')
  59. const denied = await bash.run(bash.resolve({ command: `echo hi > ${outside}/denied.txt` }))
  60. expect(denied.exitCode).not.toBe(0)
  61. expect(denied.sandbox).toEqual({ mode: 'workspace-write', denied: true, enforcement: 'full' })
  62. expect(existsSync(join(outside, 'denied.txt'))).toBe(false)
  63. })
  64. it('classifies a background denial once the task settles', async () => {
  65. const workdir = await tempDir(homedir())
  66. const bash = await sandboxedBash(workdir, 'read-only')
  67. const task = bash.start(bash.resolve({ command: `echo hi > ${workdir}/bg-denied.txt` }))
  68. await task.done
  69. expect(task.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
  70. expect(existsSync(join(workdir, 'bg-denied.txt'))).toBe(false)
  71. })
  72. it('an approved escalated retry — the spec-level workspace-write override — lands the exact write read-only denied', async () => {
  73. const workdir = await tempDir(homedir())
  74. const bash = await sandboxedBash(workdir, 'read-only')
  75. const command = `printf escalated > ${workdir}/escalated.txt`
  76. const strict = await bash.run(bash.resolve({ command }))
  77. expect(strict.exitCode).not.toBe(0)
  78. expect(strict.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
  79. expect(existsSync(join(workdir, 'escalated.txt'))).toBe(false)
  80. const retried = await bash.run(bash.resolve({ command, sandboxMode: 'workspace-write' }))
  81. expect(retried.exitCode).toBe(0)
  82. expect(retried.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' })
  83. expect(readFileSync(join(workdir, 'escalated.txt'), 'utf8')).toBe('escalated')
  84. })
  85. })