Problem
Nothing bounds how long a sandbox can live. A caller can ask for a one-year TTL and every layer accepts it:
| Layer |
Validation |
control-plane/src/mcp/tools/sandbox.ts:51 (create_sandbox) |
z.number().optional() — no min, no max, not even an integer |
control-plane/src/mcp/tools/sandbox.ts:101 (sandbox_renew) |
z.number() — same |
control-plane/src/routes/workspace-sandboxes.ts:39 |
c.req.json<{ timeout_seconds?: number }>() — a TypeScript type assertion, so no runtime validation at all |
control-plane/src/services/sandbox.ts:73 |
?? 3600 default only |
sandbox-service/src/schemas.ts:36,49,63 |
z.number().int().positive().optional() — positive, no max |
sandbox-service/src/lib/sandbox.ts:115 |
?? 3600 default only |
The sandbox runtime does have a knob for this — server.max_sandbox_timeout_seconds — but it defaults to unset, which disables the bound. So an unconfigured deployment has no cap anywhere.
Observed on a deployment: out of ~50 live sandboxes, TTLs ranged from 90 minutes to one year, with no pattern beyond whatever each caller happened to pass. Most were idle.
Why it matters
A sandbox reserves node capacity for its entire life whether or not anyone is using it. A long TTL on an abandoned sandbox is a reservation nobody can reclaim until it expires, and there is no mechanism to reclaim it early other than deleting it by hand.
This also blunts the memory-request derivation from a30bb4b. That only applies to newly created sandboxes — the running ones keep the spec they started with — so the longer existing TTLs are, the longer it takes for the reduced reservations to actually show up.
Proposal
A default maximum, configurable, enforced in sandbox-service.
That layer is the right one: every caller funnels through it (the control plane's REST routes and the MCP tools both call it), and it is already where the resource-request derivation lives, so the two policies stay together.
- Default to something bounded rather than unlimited. Anything from a few days to a month is defensible; the point is that the unconfigured case is bounded.
- Configurable, including a way to opt out for deployments that genuinely need non-expiring sandboxes.
- Reject over-limit requests with a clear message rather than clamping silently — a caller that asked for a month and got a day should be told.
- Apply to renew as well as create, or the bound is trivially bypassed.
Worth doing regardless of the above: control-plane/src/routes/workspace-sandboxes.ts validates none of its body at runtime. timeout_seconds is only the case that prompted this — the same route takes image and resource on the same terms.
Setting server.max_sandbox_timeout_seconds on the runtime is complementary rather than an alternative: it is the only thing that also covers callers reaching the runtime directly, but it can only return a generic error and cannot special-case renew.
Out of scope
Reclaiming sandboxes that are idle before their TTL expires. That needs a notion of activity and is a separate discussion.
Problem
Nothing bounds how long a sandbox can live. A caller can ask for a one-year TTL and every layer accepts it:
control-plane/src/mcp/tools/sandbox.ts:51(create_sandbox)z.number().optional()— no min, no max, not even an integercontrol-plane/src/mcp/tools/sandbox.ts:101(sandbox_renew)z.number()— samecontrol-plane/src/routes/workspace-sandboxes.ts:39c.req.json<{ timeout_seconds?: number }>()— a TypeScript type assertion, so no runtime validation at allcontrol-plane/src/services/sandbox.ts:73?? 3600default onlysandbox-service/src/schemas.ts:36,49,63z.number().int().positive().optional()— positive, no maxsandbox-service/src/lib/sandbox.ts:115?? 3600default onlyThe sandbox runtime does have a knob for this —
server.max_sandbox_timeout_seconds— but it defaults to unset, which disables the bound. So an unconfigured deployment has no cap anywhere.Observed on a deployment: out of ~50 live sandboxes, TTLs ranged from 90 minutes to one year, with no pattern beyond whatever each caller happened to pass. Most were idle.
Why it matters
A sandbox reserves node capacity for its entire life whether or not anyone is using it. A long TTL on an abandoned sandbox is a reservation nobody can reclaim until it expires, and there is no mechanism to reclaim it early other than deleting it by hand.
This also blunts the memory-request derivation from a30bb4b. That only applies to newly created sandboxes — the running ones keep the spec they started with — so the longer existing TTLs are, the longer it takes for the reduced reservations to actually show up.
Proposal
A default maximum, configurable, enforced in
sandbox-service.That layer is the right one: every caller funnels through it (the control plane's REST routes and the MCP tools both call it), and it is already where the resource-request derivation lives, so the two policies stay together.
Worth doing regardless of the above:
control-plane/src/routes/workspace-sandboxes.tsvalidates none of its body at runtime.timeout_secondsis only the case that prompted this — the same route takesimageandresourceon the same terms.Setting
server.max_sandbox_timeout_secondson the runtime is complementary rather than an alternative: it is the only thing that also covers callers reaching the runtime directly, but it can only return a generic error and cannot special-case renew.Out of scope
Reclaiming sandboxes that are idle before their TTL expires. That needs a notion of activity and is a separate discussion.