Files
deepseek-harness/packages/sandbox/sandbox-windows-acl/tests/grant.spec.ts
T
Huanqi Cao 5fea4b7c4b feat(sandbox): derive the windows-acl write SID per workspace, not per session
The per-session random write SID forced a full tree propagation per
session per server lifetime (minutes on large workspaces). The write
SID is now the per-workspace identity derived from the canonical
workspace path (workspaceWriteSid: sha256 -> S-1-4-x-y), stored
nowhere: the workspace-root ACE materializes once per workspace per
machine and every later provision hits the exact-ACE skip.

- workspace ACEs are STANDING (never revoked - the reuse cache); temp
  ACEs stay revocable (disposed with the provider), so an inheritable
  ACE never outlives its session's temp dir on the ambient temp root
- AclSandbox requires the write SID under workspace-write; read-only
  parses/grants nothing; the runner derives the SID itself (the
  --write-sid flag's presence still marks the seam-managed contract)
- the acl-session record drops writeSid (sessionId/workspace/tempDir
  remain): the SID-tamper surface and its validation are gone
- sandbox-local holds two grant maps: standing workspace grants and
  revocable per-session temp grants

Docs (README pair, design note pair, catalogs, type-equiv) and the
acl-session/grant/acl/probe/runner suites updated; workspace-sid.spec
pins the derivation contract.
2026-08-09 10:44:35 +08:00

78 lines
3.1 KiB
TypeScript

/**
* AclWriteGrant tests: the server-side grant materialization — SID parsing
* fail-closed, ACE add/dispose round-trip against the REAL directory DACL
* (observed through icacls, the operator's own tool), the recorded path
* order, and the standing/revocable lifecycle split (workspace ACEs outlive
* dispose as the reuse cache; temp ACEs revoke). Win32-only, like the other
* real-FFI suites.
*/
import { spawnSync } from 'node:child_process'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { AclWriteGrant } from '../src/index.ts'
const isWin32 = process.platform === 'win32'
/** The directory DACL as icacls renders it (the operator-visible form). */
function icaclsText(path: string): string {
const result = spawnSync('icacls', [path], { encoding: 'utf8' })
expect(result.status, `icacls failed: ${result.stderr}`).toBe(0)
return result.stdout
}
describe.skipIf(!isWin32)('AclWriteGrant (server-side materialization)', () => {
const scratchDirs: string[] = []
afterEach(() => {
for (const dir of scratchDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
})
function scratch(): string {
const dir = mkdtempSync(join(tmpdir(), 'dsh-acl-grant-'))
scratchDirs.push(dir)
return dir
}
it('create parses the SID fail-closed: a malformed SID throws before anything is granted', () => {
expect(() => AclWriteGrant.create('S-1-4-abc-1')).toThrow(/ConvertStringSidToSidW/u)
})
it('add materializes the ACE (idempotently) and reports grant order; dispose revokes revocable paths and keeps standing paths standing', () => {
const dir = scratch()
const standingDir = scratch()
const grant = AclWriteGrant.create('S-1-4-9000-77')
grant.add(dir) // revocable: the session-temp lifecycle
grant.add(standingDir, true) // standing: the workspace reuse cache
expect(grant.paths).toEqual([standingDir, dir])
expect(icaclsText(dir)).toContain('S-1-4-9000-77')
expect(icaclsText(standingDir)).toContain('S-1-4-9000-77')
// A second add over the standing exact ACE is a DACL-read no-op: the
// grant stays exactly one ACE (the reuse across sessions/restarts).
grant.add(dir)
grant.add(standingDir, true)
expect(icaclsText(dir)).toContain('S-1-4-9000-77')
expect(icaclsText(standingDir)).toContain('S-1-4-9000-77')
grant.dispose()
expect(icaclsText(dir)).not.toContain('S-1-4-9000-77')
expect(icaclsText(standingDir)).toContain('S-1-4-9000-77')
})
it('two grants with different SIDs coexist and revoke independently', () => {
const dir = scratch()
const grantA = AclWriteGrant.create('S-1-4-9000-78')
const grantB = AclWriteGrant.create('S-1-4-9000-79')
grantA.add(dir)
grantB.add(dir)
expect(icaclsText(dir)).toContain('S-1-4-9000-78')
expect(icaclsText(dir)).toContain('S-1-4-9000-79')
grantA.dispose()
expect(icaclsText(dir)).not.toContain('S-1-4-9000-78')
expect(icaclsText(dir)).toContain('S-1-4-9000-79')
grantB.dispose()
expect(icaclsText(dir)).not.toContain('S-1-4-9000-79')
})
})