You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while browser/API-verifying the 17.0.0-rc.1 checklist on #3909 (F1, "failures speak HTTP"). Verified on main @ 1ee48bc60 against a running showcase (os serve --dev, workspace CLI).
What happens
A script action body that calls a ctx.api method without the required capability answers:
POST /api/v1/actions/showcase_task/rc1_crash_probe
→ HTTP 400
{"success":false,"error":{"code":"VALIDATION_ERROR","message":"SandboxError: capability 'api.read' not granted to action 'rc1_crash_probe' (called ctx.api.object('showcase_task').count)","httpStatus":400}}
The debug prefix leaks. The client-facing message carries the SandboxError: name prefix the sandbox uses for server logs; the runner's own doc says only the business innerMessage should reach the client.
Repro: author any script action whose body.capabilities omits a capability its source uses (e.g. capabilities: [] + ctx.api.object('x').count({})), invoke it over REST.
Why
The capability gate throws SandboxErrorsynchronously inside a QuickJS host function (packages/runtime/src/sandbox/quickjs-runner.ts — capability '${required}' not granted…). That throw rejects the async IIFE inside the VM, so it surfaces through the __error side-channel, and the pump loop wraps it as:
i.e. innerMessage is set for a host-side denial, because anything that comes back through __error is presumed "user code threw this deliberately". The dispatcher's classifier (packages/runtime/src/domains/actions.ts, unexpectedFault) then reads innerMessage-present as a deliberate rejection → 400. The contract's "capability denial has no user-meaningful inner message" (SandboxError jsdoc) only holds for denials detected outside evaluation.
Timeouts take the separate budgetError path, so the 500 half of the contract likely still holds for them; it is the in-VM host-call denials (capability gates on ctx.api.*, ctx.log, ctx.crypto) that get misclassified.
Expected
A capability denial (and any other sandbox-internal fault crossing __error) reaches the classifier distinguishable from a user throw — e.g. the host-function throw carries a marker (code: 'CAPABILITY_DENIED' / a sentinel the pump loop recognizes before setting innerMessage) — and answers 500 through errorFromThrown, without the SandboxError: prefix reaching the client.
Found while browser/API-verifying the 17.0.0-rc.1 checklist on #3909 (F1, "failures speak HTTP"). Verified on
main@1ee48bc60against a running showcase (os serve --dev, workspace CLI).What happens
A script action body that calls a
ctx.apimethod without the required capability answers:Two things are wrong with that wire:
action-crash-vs-rejectionchangeset (fix(actions): an action that CRASHED is a 500, not a 200 reporting success:false (#3913 follow-up) #3951, shipped in this train) pins the contract: "SandboxErrorwith noinnerMessage— timeout, capability denial → crash → 500". A capability denial is not an outcome the action chose to report; served as 400 it stays invisible to gateway error rates / APM / alerting — exactly the blindness fix(actions): an action that CRASHED is a 500, not a 200 reporting success:false (#3913 follow-up) #3951 was written to close.SandboxError:name prefix the sandbox uses for server logs; the runner's own doc says only the businessinnerMessageshould reach the client.Repro: author any script action whose
body.capabilitiesomits a capability its source uses (e.g.capabilities: []+ctx.api.object('x').count({})), invoke it over REST.Why
The capability gate throws
SandboxErrorsynchronously inside a QuickJS host function (packages/runtime/src/sandbox/quickjs-runner.ts—capability '${required}' not granted…). That throw rejects the async IIFE inside the VM, so it surfaces through the__errorside-channel, and the pump loop wraps it as:i.e.
innerMessageis set for a host-side denial, because anything that comes back through__erroris presumed "user code threw this deliberately". The dispatcher's classifier (packages/runtime/src/domains/actions.ts,unexpectedFault) then readsinnerMessage-present as a deliberate rejection → 400. The contract's "capability denial has no user-meaningful inner message" (SandboxError jsdoc) only holds for denials detected outside evaluation.Timeouts take the separate
budgetErrorpath, so the 500 half of the contract likely still holds for them; it is the in-VM host-call denials (capability gates onctx.api.*,ctx.log,ctx.crypto) that get misclassified.Expected
A capability denial (and any other sandbox-internal fault crossing
__error) reaches the classifier distinguishable from a user throw — e.g. the host-function throw carries a marker (code: 'CAPABILITY_DENIED'/ a sentinel the pump loop recognizes before settinginnerMessage) — and answers 500 througherrorFromThrown, without theSandboxError:prefix reaching the client.Part of the #3909 rc.1 verification (section F1).