sandbox-stack.spec.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. /** The unchanged sandbox-local → bash-sandbox → subprocess stack over the Worker Node layer. */
  2. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { Context } from '@deepseek-ai/cordis'
  4. import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
  5. import LocalSandboxProvider from '@deepseek-ai/dsh-sandbox-local'
  6. import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
  7. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  8. import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
  9. import { MemoryVfs } from '../../src/storage/memory.ts'
  10. import { setActiveVfs } from '../../src/storage/active.ts'
  11. import { processAlive, signalProcess } from '../../src/node/process-table.ts'
  12. vi.mock('node:child_process', async () => await import('../../src/node/builtin_modules/implemented/child_process.ts'))
  13. const WORKSPACE = '/dsh/workspace'
  14. const OUTSIDE = '/dsh/home'
  15. let vfs: MemoryVfs
  16. const contexts: Context[] = []
  17. beforeEach(() => {
  18. vfs = new MemoryVfs()
  19. setActiveVfs(vfs)
  20. vfs.mkdirSync(WORKSPACE, { recursive: true })
  21. vfs.mkdirSync(OUTSIDE, { recursive: true })
  22. vfs.mkdirSync('/dsh/tmp', { recursive: true })
  23. vi.spyOn(process, 'kill').mockImplementation((pid: number, signal?: string | number): true => {
  24. if (signal === 0) {
  25. if (processAlive(pid)) return true
  26. const error = new Error('kill ESRCH') as NodeJS.ErrnoException
  27. error.code = 'ESRCH'
  28. throw error
  29. }
  30. signalProcess(pid, (signal ?? 'SIGTERM') as NodeJS.Signals)
  31. return true
  32. })
  33. })
  34. afterEach(async () => {
  35. await Promise.all(contexts.splice(0).map(async (ctx) => { await ctx.fiber.dispose() }))
  36. vi.restoreAllMocks()
  37. })
  38. /** Boot the production providers while only their platform primitives are replaced. */
  39. async function setup(mode: 'read-only' | 'workspace-write' | 'danger-full-access'): Promise<SandboxBashExecutor> {
  40. const ctx = new Context()
  41. contexts.push(ctx)
  42. await ctx.plugin(LocalSandboxProvider)
  43. await ctx.plugin(SessionProjectionRegistry)
  44. await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: WORKSPACE })
  45. await ctx.plugin(LocalSubprocessRuntime)
  46. await ctx.plugin(SandboxBashExecutor, { cwd: WORKSPACE })
  47. return ctx.shell as SandboxBashExecutor
  48. }
  49. describe('Worker Landlock through the production sandbox stack', () => {
  50. it('allows workspace and temp writes while classifying an outside write as denied', async () => {
  51. const bash = await setup('workspace-write')
  52. const allowed = await bash.run(bash.resolve({
  53. command: `echo workspace > ${WORKSPACE}/allowed.txt; echo temp > /tmp/allowed.txt`,
  54. }))
  55. expect(allowed.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' })
  56. expect(vfs.readFileSync(`${WORKSPACE}/allowed.txt`, 'utf8')).toBe('workspace\n')
  57. expect(vfs.readFileSync('/dsh/tmp/allowed.txt', 'utf8')).toBe('temp\n')
  58. const denied = await bash.run(bash.resolve({ command: `echo denied > ${OUTSIDE}/denied.txt` }))
  59. expect(denied.exitCode).toBe(1)
  60. expect(denied.sandbox).toEqual({ mode: 'workspace-write', denied: true, enforcement: 'full' })
  61. expect(vfs.existsSync(`${OUTSIDE}/denied.txt`)).toBe(false)
  62. })
  63. it('keeps read-only confined and danger-full-access unwrapped', async () => {
  64. const readOnly = await setup('read-only')
  65. const strict = await readOnly.run(readOnly.resolve({
  66. command: `echo discarded > /dev/null; echo denied > ${WORKSPACE}/strict.txt`,
  67. }))
  68. expect(strict.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
  69. expect(vfs.existsSync(`${WORKSPACE}/strict.txt`)).toBe(false)
  70. const unrestricted = await setup('danger-full-access')
  71. const result = await unrestricted.run(unrestricted.resolve({ command: `echo allowed > ${OUTSIDE}/full.txt` }))
  72. expect(result.sandbox).toEqual({ mode: 'danger-full-access', denied: false })
  73. expect(vfs.readFileSync(`${OUTSIDE}/full.txt`, 'utf8')).toBe('allowed\n')
  74. })
  75. it('does not leak a concurrent command policy into another process', async () => {
  76. const bash = await setup('read-only')
  77. const strict = bash.run(bash.resolve({
  78. command: `sleep 0.02; echo denied > ${WORKSPACE}/strict.txt`,
  79. }))
  80. const writable = bash.run(bash.resolve({
  81. command: `echo allowed > ${WORKSPACE}/writable.txt`,
  82. sandboxPolicy: { mode: 'workspace-write', workspaceRoot: WORKSPACE },
  83. }))
  84. const [strictResult, writableResult] = await Promise.all([strict, writable])
  85. expect(strictResult.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
  86. expect(writableResult.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' })
  87. expect(vfs.existsSync(`${WORKSPACE}/strict.txt`)).toBe(false)
  88. expect(vfs.readFileSync(`${WORKSPACE}/writable.txt`, 'utf8')).toBe('allowed\n')
  89. })
  90. })