provider.spec.ts 4.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. /** Each confinement request resolves remotely before any subprocess receives its argv. */
  2. import { Context, Service } from '@deepseek-ai/cordis'
  3. import { SandboxUnavailableError, type SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
  4. import { describe, expect, it, onTestFinished, vi } from 'vitest'
  5. import { z } from 'zod'
  6. import { SshSandboxProvider } from '../src/index.ts'
  7. const policy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: '/remote/link/..' }
  8. const completeFacts = {
  9. argv: ['/usr/bin/bwrap', '--', 'true'], enforcement: 'full', denialSignatures: ['EROFS', 'EACCES'],
  10. runnerFailureRules: [{ allowedExitCodes: [1], fatalSignatures: ['bwrap:'], informationalLines: ['notice'] }],
  11. }
  12. async function setup(raw: unknown = completeFacts) {
  13. const dispatch = vi.fn(async (_method: string, _params: unknown, _signal?: AbortSignal) => raw)
  14. class Connection extends Service {
  15. constructor(ctx: Context) { super(ctx, 'ssh') }
  16. async request<T>(method: string, params: unknown, result: z.ZodType<T>, signal?: AbortSignal): Promise<T> {
  17. return result.parse(await dispatch(method, params, signal))
  18. }
  19. }
  20. const ctx = new Context()
  21. const connection = await ctx.plugin(Connection)
  22. const fiber = await ctx.plugin(SshSandboxProvider)
  23. onTestFinished(async () => { await fiber.dispose(); await connection.dispose() })
  24. return { ctx, dispatch }
  25. }
  26. describe('SSH sandbox provider', () => {
  27. it('awaits remote policy resolution and returns the literal enforcing argv', async () => {
  28. const state = await setup()
  29. const result = Promise.withResolvers<typeof completeFacts>()
  30. state.dispatch.mockReturnValueOnce(result.promise)
  31. const argv = ['/usr/bin/node', '-e', 'console.log("shell $() ; quotes")']
  32. const controller = new AbortController()
  33. let settled = false
  34. const pending = state.ctx.sandbox.confine(argv, policy, controller.signal).then((value) => { settled = true; return value })
  35. await Promise.resolve()
  36. expect(settled).toBe(false)
  37. expect(state.dispatch).toHaveBeenCalledWith('sandbox', { argv, policy }, controller.signal)
  38. const response = { ...completeFacts, argv: ['/usr/bin/bwrap', '--', ...argv] }
  39. result.resolve(response)
  40. expect(await pending).toEqual(response)
  41. })
  42. it('obtains current backend facts for every execution policy', async () => {
  43. const state = await setup()
  44. expect((await state.ctx.sandbox.confine(['true'], policy)).enforcement).toBe('full')
  45. const partial = { ...completeFacts, argv: ['/opt/landlock', '--', 'true'], enforcement: 'partial', runnerFailureRules: [{ fatalSignatures: ['runner unavailable'] }] }
  46. state.dispatch.mockResolvedValueOnce(partial)
  47. expect(await state.ctx.sandbox.confine(['true'], { ...policy, mode: 'read-only' })).toEqual(partial)
  48. expect(state.dispatch).toHaveBeenCalledTimes(2)
  49. })
  50. it.each([null, { ...completeFacts, argv: [] }, { ...completeFacts, enforcement: 'unknown' }, { ...completeFacts, runnerFailureRules: [{ fatalSignatures: 1 }] }])(
  51. 'refuses malformed backend observations before exposing argv', async (raw) => {
  52. const state = await setup(raw)
  53. await expect(state.ctx.sandbox.confine(['true'], policy)).rejects.toBeInstanceOf(SandboxUnavailableError)
  54. expect(state.dispatch).toHaveBeenCalledTimes(1)
  55. },
  56. )
  57. it.each([new Error('remote disconnected'), 'remote disconnected'])('reports unavailable confinement without fallback or replay', async (error) => {
  58. const state = await setup()
  59. state.dispatch.mockRejectedValueOnce(error)
  60. await expect(state.ctx.sandbox.confine(['true'], policy)).rejects.toMatchObject({ name: 'SandboxUnavailableError', code: 'SANDBOX_UNAVAILABLE' })
  61. expect(state.dispatch).toHaveBeenCalledTimes(1)
  62. })
  63. it('does not send an already-cancelled confinement request', async () => {
  64. const state = await setup()
  65. const reason = new Error('cancel before confinement')
  66. await expect(state.ctx.sandbox.confine(['true'], policy, AbortSignal.abort(reason))).rejects.toBe(reason)
  67. expect(state.dispatch).not.toHaveBeenCalled()
  68. })
  69. it('preserves cancellation while remote resolution is pending', async () => {
  70. const state = await setup()
  71. const controller = new AbortController()
  72. const reason = new Error('cancel during confinement')
  73. state.dispatch.mockImplementationOnce((_method, _params, signal) => new Promise((_resolve, reject) => {
  74. signal?.addEventListener('abort', () => { reject(reason) }, { once: true })
  75. }))
  76. const pending = state.ctx.sandbox.confine(['true'], policy, controller.signal)
  77. const rejected = expect(pending).rejects.toBe(reason)
  78. controller.abort(reason)
  79. await rejected
  80. })
  81. })