Files
deepseek-harness/packages/bash/pwsh-sandbox/tests/acl.e2e.ts
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

112 lines
6.1 KiB
TypeScript

/**
* Real-backend end-to-end: LocalSandboxProvider (win32 chain → the
* windows-acl runner), SandboxPolicyService, and SandboxPwshExecutor with
* REAL pwsh spawns confined through the runner — the debug-instance
* verification of both modes: read-only denies every write (not even NUL),
* workspace-write allows the workspace and temp while denying escape writes,
* and denial/classification facts ride the settled result.
*/
import { spawnSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { homedir, tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'
import { resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import { SandboxPwshExecutor } from '../src/index.ts'
const isWin32 = process.platform === 'win32'
function pwshAvailable(): boolean {
return spawnSync(resolvePwshPath(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '$true'], { encoding: 'utf8' }).status === 0
}
describe.skipIf(!isWin32 || !pwshAvailable())('pwsh-sandbox real ACL confinement', () => {
let scratchRoot!: string
let writableDir!: string
let isolatedTemp!: string
let secretFile!: string
let escapeFile!: string
let executor!: SandboxPwshExecutor
beforeAll(async () => {
// The escape probe must live OUTSIDE every legitimately granted tree: the
// provider's workspace-write grants the workspace plus the REAL temp dir
// (the 'backend-defined temp area', same as Landlock granting /tmp), so a
// scratch dir under temp would inherit the grant and the probe would be a
// false pass. A mkdtemp under the profile is removed by afterAll.
scratchRoot = mkdtempSync(join(homedir(), 'dsh-pwsh-sandbox-e2e-'))
writableDir = join(scratchRoot, 'writable')
mkdirSync(writableDir)
isolatedTemp = mkdtempSync(join(tmpdir(), 'dsh-pwsh-sandbox-e2e-temp-'))
secretFile = join(scratchRoot, 'secret.txt')
writeFileSync(secretFile, 'top secret - must stay readable to prove the read boundary')
escapeFile = join(scratchRoot, 'escaped.txt')
const ctx = new Context()
await ctx.plugin(LocalSandboxProvider, {})
await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write', workspaceRoot: writableDir })
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SandboxPwshExecutor, {})
executor = ctx.bash as SandboxPwshExecutor
})
afterAll(() => {
rmSync(scratchRoot, { recursive: true, force: true })
rmSync(isolatedTemp, { recursive: true, force: true })
})
it('read-only: every write denied (workspace, temp, NUL), reads fine, denial facts ride the result', async () => {
const policy: SandboxExecutionPolicy = { mode: 'read-only', workspaceRoot: writableDir }
const probe = [
"$ErrorActionPreference='SilentlyContinue';",
`try{Set-Content -Path '${writableDir}\\ro-write.txt' -Value ok -ErrorAction Stop;'TARGET-WRITE: OK'}catch{'TARGET-WRITE: DENIED'};`,
`try{Set-Content -Path '${isolatedTemp}\\ro-write.txt' -Value ok -ErrorAction Stop;'TEMP-WRITE: OK'}catch{'TEMP-WRITE: DENIED'};`,
`try{Set-Content -Path '${escapeFile}' -Value ok -ErrorAction Stop;'ESCAPE-WRITE: OK'}catch{'ESCAPE-WRITE: DENIED'};`,
`try{Get-Content '${secretFile}' -ErrorAction Stop | Out-Null;'SECRET-READ: OK'}catch{'SECRET-READ: DENIED'}`,
].join('')
const result = await executor.run(executor.resolve({ command: probe, sandboxPolicy: policy }))
expect(result.exitCode, `stderr: ${result.stderr.text}`).toBe(0)
expect(result.stdout.text).toContain('TARGET-WRITE: DENIED')
expect(result.stdout.text).toContain('TEMP-WRITE: DENIED')
expect(result.stdout.text).toContain('ESCAPE-WRITE: DENIED')
expect(result.stdout.text).toContain('SECRET-READ: OK')
expect(existsSync(join(writableDir, 'ro-write.txt'))).toBe(false)
// A self-caught denial keeps the command exit 0: no denial fact.
expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'full' })
// A raw failing write must classify as a denial of the ACL dialect.
const denied = await executor.run(executor.resolve({
command: `Set-Content -Path '${escapeFile}' -Value x`,
sandboxPolicy: policy,
}))
expect(denied.exitCode).not.toBe(0)
expect(denied.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
}, 60_000)
it('workspace-write: workspace and temp writable, escape denied, reads fine', async () => {
const policy: SandboxExecutionPolicy = { mode: 'workspace-write', workspaceRoot: writableDir }
const probe = [
"$ErrorActionPreference='SilentlyContinue';",
`try{Set-Content -Path '${writableDir}\\ww-write.txt' -Value ok -ErrorAction Stop;'TARGET-WRITE: OK'}catch{'TARGET-WRITE: DENIED'};`,
`try{Set-Content -Path '${isolatedTemp}\\ww-write.txt' -Value ok -ErrorAction Stop;'TEMP-WRITE: OK'}catch{'TEMP-WRITE: DENIED'};`,
`try{Set-Content -Path '${escapeFile}' -Value ok -ErrorAction Stop;'ESCAPE-WRITE: OK'}catch{'ESCAPE-WRITE: DENIED'};`,
`try{Get-Content '${secretFile}' -ErrorAction Stop | Out-Null;'SECRET-READ: OK'}catch{'SECRET-READ: DENIED'}`,
].join('')
const result = await executor.run(executor.resolve({ command: probe, sandboxPolicy: policy }))
expect(result.exitCode, `stderr: ${result.stderr.text}`).toBe(0)
expect(result.stdout.text).toContain('TARGET-WRITE: OK')
expect(result.stdout.text).toContain('TEMP-WRITE: OK')
expect(result.stdout.text).toContain('ESCAPE-WRITE: DENIED')
expect(result.stdout.text).toContain('SECRET-READ: OK')
expect(existsSync(join(writableDir, 'ww-write.txt'))).toBe(true)
expect(existsSync(escapeFile)).toBe(false)
expect(result.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' })
}, 60_000)
})