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:
Yichen Jiang
2026-07-30 17:09:42 +08:00
parent 52ae578982
commit a90ccc4453
27 changed files with 38 additions and 262 deletions

View File

@@ -5,7 +5,6 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve, sep } from 'node:path'
@@ -113,9 +112,6 @@ function text(result: { content: { type: string; text?: string }[] }): string {
return result.content.filter(b => b.type === 'text').map(b => b.text).join('')
}
/** The policy home's default read denial: the harness credential document. */
const DEFAULT_DENY = [resolve(resolveDshHome(), '.env')]
describe('session cwd resolution', () => {
const execution = (cwd?: string) => cwd === undefined
? {}
@@ -767,13 +763,13 @@ describe('sandbox escalation surface (write/edit)', () => {
it('a plain write stamps the default mode with the calling session root', async () => {
const { ctx, fs } = await setupConfining()
await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent())
expect(fs.stamped).toEqual([{ mode: 'workspace-write', workspaceRoot: resolve('/session-project'), readDenyPaths: DEFAULT_DENY }])
expect(fs.stamped).toEqual([{ mode: 'workspace-write', workspaceRoot: resolve('/session-project') }])
})
it('a standing session override folds onto the stamp', async () => {
const { ctx, fs } = await setupConfining()
await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent([{ type: 'sandbox/mode', data: { mode: 'read-only' } }]))
expect(fs.stamped).toEqual([{ mode: 'read-only', workspaceRoot: resolve('/session-project'), readDenyPaths: DEFAULT_DENY }])
expect(fs.stamped).toEqual([{ mode: 'read-only', workspaceRoot: resolve('/session-project') }])
})
it('a denied write maps to the shared marker plus the escalation hint (isError)', async () => {
@@ -806,7 +802,7 @@ describe('sandbox escalation surface (write/edit)', () => {
agent: escalationAgent() as never,
signal: new AbortController().signal,
})
expect(fs.stamped).toEqual([{ mode: 'danger-full-access', workspaceRoot: resolve('/session-project'), readDenyPaths: DEFAULT_DENY }])
expect(fs.stamped).toEqual([{ mode: 'danger-full-access', workspaceRoot: resolve('/session-project') }])
})
it('a rejected escalation fails closed with its own text and never mutates', async () => {