revert(sandbox): withdraw the credential-document read denial
The `readDenyPaths` policy field shipped in the previous commit broke Linux confinement outright. bwrap has to create the `/dev/null` bind's mount point inside a tree its own profile has already made read-only, so it refused the entire confinement whenever the parent directory was absent — every host that has not stored a credential yet, including a fresh install: bwrap: Can't mkdir parents for /home/runner/.dsh/.env: Read-only file system which the executor correctly classifies as SANDBOX_UNAVAILABLE, so every confined bash call failed closed. Landlock cannot subtract from its own `/` read grant, so it reported `partial` enforcement on every confined call for a file it never hid, with no way to switch the denial off (schemastery fills an omitted array with `[]`, so empty and omitted were indistinguishable). A protection that breaks confinement where it works and misreports it where it does not is worse than a documented absence. Revert the field, both expressible backends, the enforcement downgrade, and the policy default; state the residue plainly in the credentials-local READMEs — file mode stops other OS users, not the model — and keep the OS-keychain provider recorded as the real answer. The narrower discipline stands: no surface hoists the credential document into `process.env`, and the model is never handed a resolved path to it.
This commit is contained in:
@@ -10,14 +10,9 @@ import { join, resolve, sep } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
import SandboxPolicyService, { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
|
||||
|
||||
async function mounted(config: {
|
||||
mode?: 'read-only' | 'workspace-write' | 'danger-full-access'
|
||||
workspaceRoot?: string
|
||||
readDenyPaths?: string[]
|
||||
} = {}) {
|
||||
async function mounted(config: { mode?: 'read-only' | 'workspace-write' | 'danger-full-access'; workspaceRoot?: string } = {}) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SandboxPolicyService, config)
|
||||
return ctx
|
||||
@@ -46,35 +41,11 @@ describe('SandboxPolicyService', () => {
|
||||
expect(ctx.sandboxPolicy.workspaceRoot).toBe(resolve('/ws/../ws/./sub'))
|
||||
})
|
||||
|
||||
it('denies reading the harness credential document by default', async () => {
|
||||
const ctx = await mounted()
|
||||
// The exact file, not the whole home: the model keeps the documented
|
||||
// access to its own session log under the same directory.
|
||||
expect(ctx.sandboxPolicy.readDenyPaths).toEqual([resolve(resolveDshHome(), '.env')])
|
||||
expect(ctx.sandboxPolicy.resolve().readDenyPaths).toEqual([resolve(resolveDshHome(), '.env')])
|
||||
})
|
||||
|
||||
it('defaults the denial list under programmatic construction too', () => {
|
||||
// Constructing the service directly bypasses Schemastery, so the field
|
||||
// arrives undefined rather than as the empty array the schema fills.
|
||||
const service = new SandboxPolicyService(new Context(), {})
|
||||
expect(service.readDenyPaths).toEqual([resolve(resolveDshHome(), '.env')])
|
||||
})
|
||||
|
||||
it('replaces the default with a configured denial list', async () => {
|
||||
const configured = await mounted({ readDenyPaths: ['/vault/../vault/./keys.env'] })
|
||||
expect(configured.sandboxPolicy.readDenyPaths).toEqual([resolve('/vault/keys.env')])
|
||||
// Schemastery fills an omitted array with `[]`, so empty reads as omitted.
|
||||
const empty = await mounted({ readDenyPaths: [] })
|
||||
expect(empty.sandboxPolicy.readDenyPaths).toEqual([resolve(resolveDshHome(), '.env')])
|
||||
})
|
||||
|
||||
it('resolves the deployment policy for an agentless call', async () => {
|
||||
const ctx = await mounted({ mode: 'workspace-write', workspaceRoot: '/fallback' })
|
||||
expect(ctx.sandboxPolicy.resolve()).toEqual({
|
||||
mode: 'workspace-write',
|
||||
workspaceRoot: resolve('/fallback'),
|
||||
readDenyPaths: [resolve(resolveDshHome(), '.env')],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -87,19 +58,16 @@ describe('SandboxPolicyService', () => {
|
||||
expect(ctx.sandboxPolicy.resolve({ session: first })).toEqual({
|
||||
mode: 'workspace-write',
|
||||
workspaceRoot: resolve('/projects/first'),
|
||||
readDenyPaths: [resolve(resolveDshHome(), '.env')],
|
||||
})
|
||||
expect(ctx.sandboxPolicy.resolve({ session: second })).toEqual({
|
||||
mode: 'read-only',
|
||||
workspaceRoot: resolve('/projects/second'),
|
||||
readDenyPaths: [resolve(resolveDshHome(), '.env')],
|
||||
})
|
||||
expect(ctx.sandboxPolicy.overrideOf(first)).toBeUndefined()
|
||||
expect(ctx.sandboxPolicy.overrideOf(second)).toBe('read-only')
|
||||
expect(ctx.sandboxPolicy.resolve()).toEqual({
|
||||
mode: 'workspace-write',
|
||||
workspaceRoot: resolve('/fallback'),
|
||||
readDenyPaths: [resolve(resolveDshHome(), '.env')],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -119,7 +87,6 @@ describe('SandboxPolicyService', () => {
|
||||
expect(ctx.sandboxPolicy.resolve({ session: session('sess-symlink-parent', cwd) })).toEqual({
|
||||
mode: 'workspace-write',
|
||||
workspaceRoot: realpathSync.native(physical),
|
||||
readDenyPaths: [resolve(resolveDshHome(), '.env')],
|
||||
})
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
@@ -133,7 +100,6 @@ describe('SandboxPolicyService', () => {
|
||||
expect(ctx.sandboxPolicy.resolve({ session: active, mode: 'danger-full-access' })).toEqual({
|
||||
mode: 'danger-full-access',
|
||||
workspaceRoot: resolve('/projects/approved'),
|
||||
readDenyPaths: [resolve(resolveDshHome(), '.env')],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user