Skip to content

Commit bd3ad89

Browse files
authored
fix(security): bundle real sandbox runtime in open CLI (Gitlawb#1641)
* fix(security): bundle real sandbox runtime in open CLI * test(sandbox): cover fail-closed runtime diagnostics * fix(sandbox): report doctor inspection failures
1 parent 7c034c5 commit bd3ad89

4 files changed

Lines changed: 232 additions & 2 deletions

File tree

scripts/build.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,6 @@ export async function handleBgFlag() { throw new Error("Background sessions are
225225
'color-diff-napi',
226226
'@anthropic-ai/mcpb',
227227
'@ant/claude-for-chrome-mcp',
228-
'@anthropic-ai/sandbox-runtime',
229228
'asciichart',
230229
'plist',
231230
'cacache',

scripts/missing-module-stub.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,16 @@ test('WebFetch binds the real ssrfGuardedLookup in the CLI bundle', () => {
4545
// ...and ssrfGuard must not have been replaced by a missing-module stub.
4646
expect(bundle).not.toMatch(/missing-module-stub:.*ssrfGuard/)
4747
})
48+
49+
test('CLI bundle includes the real sandbox runtime instead of the native stub', () => {
50+
if (!existsSync(DIST)) {
51+
throw new Error(
52+
'dist/cli.mjs not found — run `bun run build` before this test',
53+
)
54+
}
55+
56+
const bundle = readFileSync(DIST, 'utf-8')
57+
58+
expect(bundle).not.toContain('native-stub:@anthropic-ai/sandbox-runtime')
59+
expect(bundle).toContain('bubblewrap (bwrap) not installed')
60+
})

scripts/system-check.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { describe, expect, test } from 'bun:test'
22

33
import {
4+
buildSandboxRuntimeCheck,
45
checkNodeVersion,
56
formatReachabilityFailureDetail,
7+
isCliSandboxRuntimeStubbed,
68
readNodeExecutableVersion,
79
} from './system-check.ts'
810

@@ -119,3 +121,131 @@ describe('checkNodeVersion', () => {
119121
})
120122
})
121123
})
124+
125+
describe('sandbox runtime diagnostics', () => {
126+
test('fails when sandbox runtime inspection throws an Error', () => {
127+
const result = buildSandboxRuntimeCheck({
128+
inspectionError: new Error('EACCES: permission denied, open dist/cli.mjs'),
129+
})
130+
131+
expect(result).toEqual({
132+
ok: false,
133+
label: 'Sandbox runtime',
134+
detail:
135+
'Unable to inspect CLI sandbox runtime: EACCES: permission denied, open dist/cli.mjs',
136+
})
137+
})
138+
139+
test('fails when sandbox runtime inspection throws a non-Error value', () => {
140+
const result = buildSandboxRuntimeCheck({
141+
inspectionError: 'bundle read failed',
142+
})
143+
144+
expect(result).toEqual({
145+
ok: false,
146+
label: 'Sandbox runtime',
147+
detail: 'Unable to inspect CLI sandbox runtime: bundle read failed',
148+
})
149+
})
150+
151+
test('detects sandbox-runtime native stubs in the CLI bundle', () => {
152+
expect(
153+
isCliSandboxRuntimeStubbed(
154+
'// native-stub:@anthropic-ai/sandbox-runtime\nconst noop = () => null',
155+
),
156+
).toBe(true)
157+
expect(isCliSandboxRuntimeStubbed('bubblewrap (bwrap) not installed')).toBe(
158+
false,
159+
)
160+
})
161+
162+
test('fails when the CLI bundle contains a sandbox runtime stub', () => {
163+
const result = buildSandboxRuntimeCheck({
164+
cliRuntimeStubbed: true,
165+
sandboxEnabled: true,
166+
failIfUnavailable: true,
167+
sandboxingEnabled: false,
168+
unavailableReason: 'sandbox.enabled is set but the runtime is stubbed',
169+
})
170+
171+
expect(result.ok).toBe(false)
172+
expect(result.label).toBe('Sandbox runtime')
173+
expect(result.detail).toContain('CLI bundle: stubbed')
174+
expect(result.detail).toContain('effective behavior: fail-closed')
175+
expect(result.detail).toContain(
176+
'reason: sandbox.enabled is set but the runtime is stubbed',
177+
)
178+
})
179+
180+
test('reports warning-only behavior when sandbox is enabled but unavailable', () => {
181+
const result = buildSandboxRuntimeCheck({
182+
cliRuntimeStubbed: false,
183+
sandboxEnabled: true,
184+
failIfUnavailable: false,
185+
sandboxingEnabled: false,
186+
unavailableReason: 'bubblewrap (bwrap) not installed',
187+
})
188+
189+
expect(result.ok).toBe(true)
190+
expect(result.detail).toContain('CLI bundle: real runtime')
191+
expect(result.detail).toContain('effective behavior: warning-only')
192+
expect(result.detail).toContain('reason: bubblewrap (bwrap) not installed')
193+
})
194+
195+
test('flags fail-closed behavior when sandbox is required but unavailable', () => {
196+
const result = buildSandboxRuntimeCheck({
197+
cliRuntimeStubbed: false,
198+
sandboxEnabled: true,
199+
failIfUnavailable: true,
200+
sandboxingEnabled: false,
201+
unavailableReason: 'bubblewrap (bwrap) not installed',
202+
})
203+
204+
expect(result.ok).toBe(false)
205+
expect(result.detail).toContain('CLI bundle: real runtime')
206+
expect(result.detail).toContain('effective behavior: fail-closed')
207+
expect(result.detail).toContain('reason: bubblewrap (bwrap) not installed')
208+
})
209+
210+
test('reports enforcing behavior when sandboxing is active', () => {
211+
const result = buildSandboxRuntimeCheck({
212+
cliRuntimeStubbed: false,
213+
sandboxEnabled: true,
214+
failIfUnavailable: true,
215+
sandboxingEnabled: true,
216+
})
217+
218+
expect(result.ok).toBe(true)
219+
expect(result.detail).toBe(
220+
'CLI bundle: real runtime; sandbox.enabled: true; failIfUnavailable: true; effective behavior: enforcing',
221+
)
222+
})
223+
224+
test('reports disabled behavior without failing when sandbox is not enabled', () => {
225+
const result = buildSandboxRuntimeCheck({
226+
cliRuntimeStubbed: false,
227+
sandboxEnabled: false,
228+
failIfUnavailable: false,
229+
sandboxingEnabled: false,
230+
})
231+
232+
expect(result.ok).toBe(true)
233+
expect(result.detail).toBe(
234+
'CLI bundle: real runtime; sandbox.enabled: false; failIfUnavailable: false; effective behavior: disabled',
235+
)
236+
})
237+
238+
test('reports disabled behavior without failing when sandbox is off and the CLI runtime is stubbed', () => {
239+
const result = buildSandboxRuntimeCheck({
240+
cliRuntimeStubbed: true,
241+
sandboxEnabled: false,
242+
failIfUnavailable: false,
243+
sandboxingEnabled: false,
244+
})
245+
246+
expect(result.ok).toBe(true)
247+
expect(result.detail).toBe(
248+
'CLI bundle: stubbed; sandbox.enabled: false; failIfUnavailable: false; effective behavior: disabled',
249+
)
250+
})
251+
})

scripts/system-check.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @ts-nocheck
2-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
2+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
33
import { dirname, join, resolve } from 'node:path'
44
import { spawnSync } from 'node:child_process'
55
import {
@@ -17,6 +17,7 @@ import {
1717
MIN_NODE_ENGINE_RANGE,
1818
checkSupportedNodeVersion,
1919
} from '../src/utils/nodeRuntime.js'
20+
import { SandboxManager } from '../src/utils/sandbox/sandbox-adapter.js'
2021

2122
type CheckResult = {
2223
ok: boolean
@@ -173,6 +174,92 @@ function checkBuildArtifacts(): CheckResult {
173174
return pass('Build artifacts', distCli)
174175
}
175176

177+
export function isCliSandboxRuntimeStubbed(bundleText: string): boolean {
178+
return bundleText.includes('native-stub:@anthropic-ai/sandbox-runtime')
179+
}
180+
181+
type SandboxRuntimeCheckInput =
182+
| {
183+
inspectionError: unknown
184+
}
185+
| {
186+
cliRuntimeStubbed: boolean
187+
sandboxEnabled: boolean
188+
failIfUnavailable: boolean
189+
sandboxingEnabled: boolean
190+
unavailableReason?: string
191+
}
192+
193+
function formatUnknownError(error: unknown): string {
194+
return error instanceof Error ? error.message : String(error)
195+
}
196+
197+
export function buildSandboxRuntimeCheck(
198+
input: SandboxRuntimeCheckInput,
199+
): CheckResult {
200+
if ('inspectionError' in input) {
201+
return fail(
202+
'Sandbox runtime',
203+
`Unable to inspect CLI sandbox runtime: ${formatUnknownError(input.inspectionError)}`,
204+
)
205+
}
206+
207+
const effectiveBehavior = input.sandboxingEnabled
208+
? 'enforcing'
209+
: input.sandboxEnabled
210+
? input.failIfUnavailable
211+
? 'fail-closed'
212+
: 'warning-only'
213+
: 'disabled'
214+
215+
const detailParts = [
216+
`CLI bundle: ${input.cliRuntimeStubbed ? 'stubbed' : 'real runtime'}`,
217+
`sandbox.enabled: ${input.sandboxEnabled}`,
218+
`failIfUnavailable: ${input.failIfUnavailable}`,
219+
`effective behavior: ${effectiveBehavior}`,
220+
]
221+
const reason =
222+
input.unavailableReason ??
223+
(input.cliRuntimeStubbed && input.sandboxEnabled
224+
? 'CLI bundle contains a no-op sandbox runtime stub'
225+
: undefined)
226+
if (reason) {
227+
detailParts.push(`reason: ${reason}`)
228+
}
229+
230+
const ok = !(
231+
input.sandboxEnabled &&
232+
input.failIfUnavailable &&
233+
Boolean(reason)
234+
)
235+
return ok
236+
? pass('Sandbox runtime', detailParts.join('; '))
237+
: fail('Sandbox runtime', detailParts.join('; '))
238+
}
239+
240+
function checkSandboxRuntime(): CheckResult {
241+
const distCli = resolve(process.cwd(), 'dist', 'cli.mjs')
242+
if (!existsSync(distCli)) {
243+
return fail(
244+
'Sandbox runtime',
245+
`CLI bundle missing at ${distCli}. Run: bun run build`,
246+
)
247+
}
248+
249+
try {
250+
const bundle = readFileSync(distCli, 'utf8')
251+
return buildSandboxRuntimeCheck({
252+
cliRuntimeStubbed: isCliSandboxRuntimeStubbed(bundle),
253+
sandboxEnabled: SandboxManager.isSandboxEnabledInSettings(),
254+
failIfUnavailable: SandboxManager.isSandboxRequired(),
255+
sandboxingEnabled: SandboxManager.isSandboxingEnabled(),
256+
unavailableReason: SandboxManager.getSandboxUnavailableReason(),
257+
})
258+
} catch (error) {
259+
return buildSandboxRuntimeCheck({ inspectionError: error })
260+
}
261+
}
262+
176263
function isLocalBaseUrl(baseUrl: string): boolean {
177264
return isProviderLocalUrl(baseUrl)
178265
}
@@ -717,6 +804,7 @@ async function main(): Promise<void> {
717804
results.push(checkNodeVersion())
718805
results.push(checkBunRuntime())
719806
results.push(checkBuildArtifacts())
807+
results.push(checkSandboxRuntime())
720808
results.push(...checkOpenAIEnv())
721809
results.push(await checkBaseUrlReachability())
722810
results.push(await checkProviderGenerationReadiness())

0 commit comments

Comments
 (0)