Fix: the tool substituted the deployment sandbox default for the session's

dsh-bash-sandbox is the composed ctx.shell, and its resolve() stamps
`request.sandboxPolicy ?? ctx.sandboxPolicy.resolve()`. Omitting the policy
therefore does NOT mean "unconfined" -- it silently substitutes the DEPLOYMENT
default (`DSH_PERMISSION_MODE ?? 'workspace-write'`) for the session's preset.
On a host with no sandbox runner that confines a danger-full-access session and
fails with SandboxUnavailableError before slopo is ever reached, which is
exactly what happened: the tool was unusable while bash worked.

Resolve the calling session's policy the way dsh-tool-bash does, and pass it on
every call. A confining executor with no ctx.sandboxPolicy is now refused
outright rather than guessed at.

That fix exposes a second problem worth naming. Slopo's state lives under
$DSH_HOME, which is outside the workspace root a workspace-write sandbox permits
writing to, so under that preset nothing would work. The tool now refuses before
running and names both ways out -- run under danger-full-access, or set stateDir
to a directory inside the workspace -- instead of letting a write fail halfway
through an embed.

Five tests pin this: the session's policy is resolved exactly once and travels
with the session, a missing policy service is refused, state outside the sandbox
is refused before any execution, state inside it proceeds, and status/configure
never touch the shell at all.
This commit is contained in:
dsh-slopo agent committed 2026-09-22 07:12:03 +01:00
1 parent a8e577ae38
commit e758a723e2
3 files changed
+177 -4

No files matched your search

+109 -3
View File
@@ -76,15 +76,34 @@ const shell = {
* `jobs` is deliberately absent, which forces the tool down its foreground path
* so the assertions can see a result rather than a job handle.
*/
function harness(overrides = {}, { withSettings = true, withJobs = false } = {}) {
function harness(overrides = {}, { withSettings = true, withJobs = false, sandboxMode, sandboxPolicyResolver } = {}) {
const config = { ...FULL_CONFIG, ...overrides }
const state = { tools: new Map(), updates: [], provider: undefined, namespace: undefined, shell, jobs: undefined }
// A per-harness shell, so a test can vary sandboxMode and every resolved
// request is recorded for assertion.
const scopedShell = {
...shell,
sandboxMode,
resolve(request) {
state.shellRequests.push(request)
return { workdir: process.cwd(), timeoutMs: 600_000, stdoutMaxBytes: 10_000_000, ...request }
},
}
const state = {
tools: new Map(),
updates: [],
provider: undefined,
namespace: undefined,
shell: scopedShell,
shellRequests: [],
jobs: undefined,
}
const ctx = {
logger: () => ({ info() {}, warn() {}, debug() {}, error() {} }),
get(name) {
if (name === 'shell') return shell
if (name === 'shell') return scopedShell
if (name === 'jobs') return withJobs ? state.jobs : undefined
if (name === 'sandboxPolicy') return sandboxPolicyResolver
return undefined
},
inject(deps, callback) {
@@ -323,3 +342,90 @@ test(
assert.match(reviewed.report, /\* /)
},
)
test('the sandbox policy is resolved for the CALLING SESSION, not the deployment default', async () => {
// dsh-bash-sandbox stamps `request.sandboxPolicy ?? ctx.sandboxPolicy.resolve()`.
// Omitting it substitutes the DEPLOYMENT default, which is what made a
// danger-full-access session fail with SandboxUnavailableError before slopo
// was ever reached.
const seen = []
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{
sandboxMode: 'workspace-write',
sandboxPolicyResolver: {
resolve(arg) {
seen.push(arg)
return { mode: 'danger-full-access', workspaceRoot: '/some/workspace' }
},
},
},
)
const tool = state.tools.get('slopo')
await tool.execute({ action: 'review', foreground: true }, execContext('/tmp/nonexistent-repo-xyz'))
assert.equal(seen.length, 1, 'the policy must be resolved exactly once')
assert.ok(seen[0].session !== undefined, 'the calling session must travel with the policy')
assert.equal(state.shellRequests.length, 1)
assert.deepEqual(state.shellRequests[0].sandboxPolicy, {
mode: 'danger-full-access',
workspaceRoot: '/some/workspace',
})
})
test('a confining executor with no policy service is refused rather than guessed at', async () => {
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{ sandboxMode: 'workspace-write' },
)
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'review' }, execContext('/tmp'))
assert.equal(value.ok, false)
assert.equal(value.errorCode, 'NO_SANDBOX_POLICY')
assert.deepEqual(state.shellRequests, [], 'nothing may be executed')
})
test('a workspace-write session is told its state lives outside the sandbox', async () => {
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{
sandboxMode: 'workspace-write',
sandboxPolicyResolver: { resolve: () => ({ mode: 'workspace-write', workspaceRoot: '/some/workspace' }) },
},
)
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'review' }, execContext('/some/workspace'))
assert.equal(value.ok, false)
assert.equal(value.errorCode, 'STATE_OUTSIDE_SANDBOX')
assert.match(value.message, /workspace-write/)
assert.match(value.remedy, /danger-full-access/)
assert.match(value.remedy, /stateDir/)
assert.deepEqual(state.shellRequests, [], 'it must not run where it cannot write')
})
test('a workspace-write session with stateDir inside the workspace proceeds', async () => {
const workspace = mkdtempSync(join(tmpdir(), 'dsh-slopo-ws-'))
const { state } = harness(
{
embeddingModel: 'ollama/example:latest',
embeddingDimensions: 768,
stateDir: join(workspace, '.slopo'),
},
{
sandboxMode: 'workspace-write',
sandboxPolicyResolver: { resolve: () => ({ mode: 'workspace-write', workspaceRoot: workspace }) },
},
)
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'review', foreground: true }, execContext(workspace))
assert.notEqual(value.errorCode, 'STATE_OUTSIDE_SANDBOX')
assert.ok(state.shellRequests.length > 0, 'it should have attempted the run')
assert.deepEqual(state.shellRequests[0].sandboxPolicy, { mode: 'workspace-write', workspaceRoot: workspace })
})
test('configure and status never touch the shell, so they work under any sandbox', async () => {
const { state } = harness({}, { sandboxMode: 'workspace-write' })
const tool = state.tools.get('slopo')
await tool.execute({ action: 'status' }, execContext('/some/workspace'))
await tool.execute({ action: 'configure', config: { embeddingModel: 'x' } }, execContext('/some/workspace'))
assert.deepEqual(state.shellRequests, [])
})