feat(sandbox): shared browser-direct terminal connection vertical (#349) - #352
Merged
Merged
Conversation
createSandboxTerminalConnectionRoute mints the SDK scoped token behind
injected requireUser/ensureSandbox seams and returns
{sidecarUrl, token, expiresAt, status, sandboxId}, 503+status while the
box is provisioning — legal-agent's production route generalized.
Session-shaped scopes without their session id(s) fail at
factory-creation time; resolver callbacks returning empty fail the
request. The unmodified useSandboxTerminalConnection hook drives the new
factory end-to-end (fake-backed integration tests incl. 503 poll-through
and pre-expiry token rotation).
@deprecated JSDoc on the proxy transport family (connection handler, runtime proxy, WS upgrade relay, HMAC token mint/verify, and their supporting helpers/types), each pointing at createSandboxTerminalConnectionRoute. Nothing removed — three apps still import the seam; retirement is tracked in #350 and removal is a major.
/sandbox and /web-react module-map rows updated: browser-direct scoped-token is the fleet default, the proxy relay is deprecated, and the security posture is explicit — the terminal token grants command execution, so short TTL and narrow scope are load-bearing and stay caller parameters. docs/ regenerated via docs:gen.
…onnection id (#349) Three review-confirmed defects against the platform's terminal-auth contracts (verified in the orchestrator/sidecar enforcement source and the SDK's own _attachTerminal reference implementation): - scope is now PINNED to 'session-runtime' — the only scope granted the 'terminal' capability; 'project'/'session' are gateway-read tokens the sidecar rejects outright, so scope is no longer a parameter - the terminal connectionId threads end to end: the hook forwards it as a query parameter, the route resolves it (resolveConnectionId seam, 400 when absent), mints with sessionId=connectionId (the orchestrator fails closed on sid != the dialed /terminals/<id>/ws segment), and echoes it for TerminalView to use verbatim - the response returns the mint's sidecarProxyUrl — the only browser- safe terminal base; connection.runtimeUrl is demoted to a readiness gate and never handed to the browser The prior shape was modeled on legal-agent's production route, which predates the platform's terminal-auth hardening (2026-06-19 capability gate, 2026-07-15 scoped-token terminal WS path, 2026-07-17/18 sid binding) and no longer authenticates.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #349. Phase 2 keystone of epic #343 (decision #341: browser-direct scoped-token transport).
What
createSandboxTerminalConnectionRoute(src/sandbox/terminal-connection.ts, new, on the existing/sandboxsubpath) — the shared browser-direct terminal connection vertical, generalized behind two product seams (requireUser,ensureSandbox— workspace- vs user-scoped provisioning stays product domain), matching the SDK's own_attachTerminalreference implementation:session-runtime— the only scope the platform grants theterminalcapability to (cap:["read","workspace","terminal"]).project/sessionmint gateway-read tokens (HS256, nocapclaim — a different token family the sidecar rejects outright);read-onlycarriescap:["read"]withoutterminal. Scope is therefore not a parameter.ttlMinutesstays one (default 15, the SDK clamp max).connectionIdthreads end to end — the orchestrator's terminal WS gate fails closed unless the token'ssidequals the<connectionId>segment of the dialed/terminals/<connectionId>/ws. The hook forwards the client's id (tabTerminalConnectionId()) as a query parameter, the route resolves it (resolveConnectionIdseam for validation/namespacing; 400 when absent/empty), mints withsessionId: connectionId, and echoes it in the response — the client hands the echoed id toTerminalViewverbatim.sidecarProxyUrlassidecarUrl— the only browser-safe terminal base (/v1/sidecar-proxy/{id}; it is also the only hop that decodes TerminalView'sbearer.<base64url>WS subprotocol).box.connection.runtimeUrlis demoted to a readiness gate (503 {error, status}while absent) and is never handed to the browser.requireUser;500on provisioning throw;503on mint failure;200 {sidecarUrl, token, expiresAt, status, sandboxId, connectionId}.useSandboxTerminalConnectiongains an optionalconnectionIdinput (forwarded as the query param) and carries the response's echoedconnectionIdinto state; poll-on-503 / pre-expiry refresh / reconnect guard untouched. Refresh re-mints bound to the same terminal id.@deprecatedJSDoc on the full same-origin proxy family (connection handler, runtime proxy, WS upgrade relay, HMAC token mint/verify, supporting helpers/types), each pointing at the replacement.createWorkspaceSandboxManagerfamily stays undecorated (generic box lifecycle, used by both transports). Retirement tracked in [REFACTOR] Retire the deprecated terminal proxy seam after the fleet migrates (zero-importer audit) #350./sandbox+/web-reactmodule-map rows record the transport decision, the pinned-scope/sid-binding/sidecarProxyUrl contracts, and the security posture.docs/regenerated viadocs:gen.Review round 2 — three P1s validated and fixed
An external review flagged that the initial shape (modeled on legal-agent's production route:
scope:'project'+connection.runtimeUrl) produced a token for the wrong consumer, couldn't bind to TerminalView's connection id, and returned the wrong base URL. All three claims were verified against the platform enforcement source (orchestratorconsumer-scoped-token.tsscope→capability mapping;sidecar-access.tsverifyScopedSidecarCapabilitysid check; sidecarpackages/sidecar-authroute gates) and the SDK's own_attachTerminal, and all three are correct. Legal's shape predates the platform's terminal-auth hardening (2026-06-19 capability gate, 2026-07-15 scoped-token terminal WS path, 2026-07-17/18 sid binding) and no longer authenticates — it is not the spec for the token path. Fixed in609a930.Security posture
The terminal token grants command execution in the sandbox. Default TTL is 15 min (the SDK's clamp max) and stays a parameter; the scope is structurally the narrowest the platform offers — one box (
ensureSandboxresolved it for this user), one terminal connection (sid-bound).Tests — prove-it-can-fail evidence
47 targeted tests green (route factory + hook-over-factory integration, incl. 503 poll-through, pre-expiry rotation re-binding the same connectionId, resolveConnectionId rewrite, 400-no-connectionId). Six break→red→restore→green drills total; the three for the final contracts:
Drill A — un-pin the scope back to
'project'RED:
Restored → GREEN (
1 passed | 38 skipped).Drill B — return
box.connection.runtimeUrlinstead ofscoped.sidecarProxyUrlRED:
Restored → GREEN (
1 passed | 38 skipped).Drill C — mint with a hardcoded sessionId instead of the resolved connectionId
RED (5 tests across both files — route mint-args AND both hook integration tests):
Restored → GREEN (47/47).
(Round-1 drills — missing-
runtimeUrl503 guard, default ttl, hook rotation timer — remain in the history; post-drill greps confirm full restore each time.)Gates
npx vitest run tests/sandbox.test.ts tests/sandbox-terminal-hook.test.ts→ 47/47pnpm typecheck→ cleanpnpm test→ 3851 passed / 13 skipped; 7 failures intests/knowledge-loop.test.ts+src/sandbox/index.test.tsare pre-existing local-env issues — verified identical (same 7) on cleanorigin/mainvia stashNODE_OPTIONS=--max-old-space-size=8192 pnpm build→ cleanAcceptance criteria
useSandboxTerminalConnectiondrivesTerminalViewagainst a direct sidecar URL end-to-end (fake-backed test), including the 503-provisioning poll and pre-expiry refresh@deprecatedwith a pointer to the replacement; nothing removedpnpm typecheck && pnpm test && pnpm buildgreen (modulo the 7 pre-existing env failures shown identical on baseline)