acl.e2e.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. /**
  2. * Real-backend end-to-end: LocalSandboxProvider (win32 chain → the
  3. * windows-acl runner), SandboxPolicyService, and SandboxPwshExecutor with
  4. * REAL pwsh spawns confined through the runner — the debug-instance
  5. * verification of both modes on ordinary user-owned paths: read-only denies
  6. * writes, workspace-write allows its promised roots while denying escape
  7. * writes, and the partial-enforcement/denial facts ride the settled result.
  8. */
  9. import { spawnSync } from 'node:child_process'
  10. import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
  11. import { homedir, tmpdir } from 'node:os'
  12. import { join } from 'node:path'
  13. import { afterAll, beforeAll, describe, expect, it } from 'vitest'
  14. import { Context } from '@deepseek-ai/cordis'
  15. import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'
  16. import { resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'
  17. import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
  18. import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
  19. import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
  20. import { SandboxPwshExecutor } from '../src/index.ts'
  21. const isWin32 = process.platform === 'win32'
  22. function pwshAvailable(): boolean {
  23. return spawnSync(resolvePwshPath(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '$true'], { encoding: 'utf8' }).status === 0
  24. }
  25. describe.skipIf(!isWin32 || !pwshAvailable())('pwsh-sandbox real ACL confinement', () => {
  26. let scratchRoot!: string
  27. let writableDir!: string
  28. let outsideTempDir!: string
  29. let secretFile!: string
  30. let escapeFile!: string
  31. let executor!: SandboxPwshExecutor
  32. beforeAll(async () => {
  33. // The workspace escape sits under the profile. A separate directory under
  34. // the ambient temp root proves that the root itself is not granted: the
  35. // runner creates its own private child and rewrites TMP/TEMP to it.
  36. scratchRoot = mkdtempSync(join(homedir(), 'dsh-pwsh-sandbox-e2e-'))
  37. writableDir = join(scratchRoot, 'writable')
  38. mkdirSync(writableDir)
  39. outsideTempDir = mkdtempSync(join(tmpdir(), 'dsh-pwsh-sandbox-e2e-outside-temp-'))
  40. secretFile = join(scratchRoot, 'secret.txt')
  41. writeFileSync(secretFile, 'top secret - must stay readable to prove the read boundary')
  42. escapeFile = join(scratchRoot, 'escaped.txt')
  43. const ctx = new Context()
  44. await ctx.plugin(LocalSandboxProvider, {})
  45. await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write', workspaceRoot: writableDir })
  46. await ctx.plugin(LocalSubprocessRuntime)
  47. await ctx.plugin(SandboxPwshExecutor, {})
  48. executor = ctx.shell as SandboxPwshExecutor
  49. })
  50. afterAll(() => {
  51. rmSync(scratchRoot, { recursive: true, force: true })
  52. rmSync(outsideTempDir, { recursive: true, force: true })
  53. })
  54. it('read-only: ordinary path writes denied, reads fine, partial and denial facts ride the result', async () => {
  55. const policy: SandboxExecutionPolicy = { mode: 'read-only', workspaceRoot: writableDir }
  56. const probe = [
  57. "$ErrorActionPreference='SilentlyContinue';",
  58. `try{Set-Content -Path '${writableDir}\\ro-write.txt' -Value ok -ErrorAction Stop;'TARGET-WRITE: OK'}catch{'TARGET-WRITE: DENIED'};`,
  59. `try{Set-Content -Path '${outsideTempDir}\\ro-write.txt' -Value ok -ErrorAction Stop;'TEMP-WRITE: OK'}catch{'TEMP-WRITE: DENIED'};`,
  60. `try{Set-Content -Path '${escapeFile}' -Value ok -ErrorAction Stop;'ESCAPE-WRITE: OK'}catch{'ESCAPE-WRITE: DENIED'};`,
  61. `try{Get-Content '${secretFile}' -ErrorAction Stop | Out-Null;'SECRET-READ: OK'}catch{'SECRET-READ: DENIED'}`,
  62. ].join('')
  63. const result = await executor.run(executor.resolve({ command: probe, sandboxPolicy: policy }))
  64. expect(result.exitCode, `stderr: ${result.stderr.text}`).toBe(0)
  65. expect(result.stdout.text).toContain('TARGET-WRITE: DENIED')
  66. expect(result.stdout.text).toContain('TEMP-WRITE: DENIED')
  67. expect(result.stdout.text).toContain('ESCAPE-WRITE: DENIED')
  68. expect(result.stdout.text).toContain('SECRET-READ: OK')
  69. expect(existsSync(join(writableDir, 'ro-write.txt'))).toBe(false)
  70. // A self-caught denial keeps the command exit 0: no denial fact.
  71. expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'partial' })
  72. // A raw failing write must classify as a denial of the ACL dialect.
  73. const denied = await executor.run(executor.resolve({
  74. command: `Set-Content -Path '${escapeFile}' -Value x`,
  75. sandboxPolicy: policy,
  76. }))
  77. expect(denied.exitCode).not.toBe(0)
  78. expect(denied.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'partial' })
  79. }, 60_000)
  80. it('workspace-write: workspace and private temp writable, ambient temp and escape denied', async () => {
  81. const policy: SandboxExecutionPolicy = { mode: 'workspace-write', workspaceRoot: writableDir }
  82. const probe = [
  83. "$ErrorActionPreference='SilentlyContinue';",
  84. `try{Set-Content -Path '${writableDir}\\ww-write.txt' -Value ok -ErrorAction Stop;'TARGET-WRITE: OK'}catch{'TARGET-WRITE: DENIED'};`,
  85. "try{Set-Content -Path (Join-Path $env:TEMP 'ww-write.txt') -Value ok -ErrorAction Stop;'TEMP-WRITE: OK'}catch{'TEMP-WRITE: DENIED'};",
  86. `try{Set-Content -Path '${outsideTempDir}\\ww-write.txt' -Value ok -ErrorAction Stop;'AMBIENT-TEMP-WRITE: OK'}catch{'AMBIENT-TEMP-WRITE: DENIED'};`,
  87. `try{Set-Content -Path '${escapeFile}' -Value ok -ErrorAction Stop;'ESCAPE-WRITE: OK'}catch{'ESCAPE-WRITE: DENIED'};`,
  88. `try{Get-Content '${secretFile}' -ErrorAction Stop | Out-Null;'SECRET-READ: OK'}catch{'SECRET-READ: DENIED'};`,
  89. "'TEMP-PATH: ' + $env:TEMP",
  90. ].join('')
  91. const result = await executor.run(executor.resolve({ command: probe, sandboxPolicy: policy }))
  92. expect(result.exitCode, `stderr: ${result.stderr.text}`).toBe(0)
  93. expect(result.stdout.text).toContain('TARGET-WRITE: OK')
  94. expect(result.stdout.text).toContain('TEMP-WRITE: OK')
  95. expect(result.stdout.text).toContain('AMBIENT-TEMP-WRITE: DENIED')
  96. expect(result.stdout.text).toContain('ESCAPE-WRITE: DENIED')
  97. expect(result.stdout.text).toContain('SECRET-READ: OK')
  98. expect(existsSync(join(writableDir, 'ww-write.txt'))).toBe(true)
  99. expect(existsSync(join(outsideTempDir, 'ww-write.txt'))).toBe(false)
  100. expect(existsSync(escapeFile)).toBe(false)
  101. const privateTemp = result.stdout.text.match(/^TEMP-PATH: (.+)$/mu)?.[1]?.trim()
  102. expect(privateTemp).toBeDefined()
  103. expect(privateTemp?.startsWith(tmpdir())).toBe(true)
  104. expect(existsSync(privateTemp ?? '')).toBe(false)
  105. expect(result.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'partial' })
  106. }, 60_000)
  107. })