feat(control-plane): sandbox-auth GET /sessions/:id/tunnel-urls#691
Conversation
Adds a sandbox-authenticated endpoint that returns the tunnel URLs
already stored on the session's sandbox row (populated by the
existing storeAndBroadcastTunnelUrls flow on Modal/Daytona tunnel
resolution).
Why: on the Modal provider, sandbox_manager._write_tunnel_env_file
writes /workspace/.tunnels.env from outside the sandbox via
sandbox.open.aio(), and that write does not propagate to the
in-sandbox filesystem view in time for the supervisor's 30s wait
(observed: tunnel.urls_written logged by the manager, file never
visible inside the sandbox, tunnel.env_file_wait_timeout follows).
A repo-level .openinspect/setup.sh can now curl this endpoint with
SANDBOX_AUTH_TOKEN and write the file itself, sidestepping the
Modal-SDK filesystem-sync gap entirely.
Wiring (small, mirrors the scm-credentials sandbox-auth route):
- contracts.ts: tunnelUrls = '/internal/tunnel-urls'
- routes.ts: GET handler entry + interface field
- sandbox.handler.ts: new tunnelUrls() reads sandbox.tunnel_urls JSON
- durable-object.ts: handler wiring
- router.ts: SANDBOX_AUTH_ROUTES regex + GET route + handler fn
No new tables, no migration. Response shape:
{ "tunnelUrls": { "3000": "https://...", ... } }
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds GET /sessions/:id/tunnel-urls: new internal path and route handler, Durable Object delegation to SandboxHandler.tunnelUrls(), public route/proxy wiring, sandbox-token auth exemption and SCM-agnostic flag, plus handler tests and gated-access router test. ChangesTunnel URLs Endpoint
Sequence Diagram(s)sequenceDiagram
participant Client
participant Router
participant Runtime
participant SessionDO
participant SandboxHandler
Client->>Router: GET /sessions/:id/tunnel-urls (Authorization)
Router->>Runtime: proxy GET -> SessionInternalPaths.tunnelUrls
Runtime->>SessionDO: GET /internal/tunnel-urls (runtimeMethod: GET)
SessionDO->>SandboxHandler: call tunnelUrls()
SandboxHandler-->>SessionDO: Response { tunnelUrls } / 404 / 500
SessionDO-->>Runtime: forward response
Runtime-->>Router: forward response
Router-->>Client: response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed: dependency version conflict. Check your lock file or package.json. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/control-plane/src/router.ts`:
- Line 173: The /sessions/:id/tunnel-urls route is SCM-agnostic but is being
blocked by enforceImplementedScmProvider() because isProviderImplementedRoute()
only allows all routes for GitHub and a single GitLab exception; update
isProviderImplementedRoute() (or its route whitelist) to treat the
/^\/sessions\/[^/]+\/tunnel-urls$/ pattern as provider-agnostic so
handleRequest() will not enforce an SCM provider for that endpoint, ensuring
GitLab deployments can reach the tunnel-urls handler.
In `@packages/control-plane/src/session/http/handlers/sandbox.handler.ts`:
- Around line 195-225: The tunnelUrls handler currently swallows malformed
sandbox.tunnel_urls and returns 200 with an empty map; change tunnelUrls() so
that if JSON.parse throws or the parsed value is not an object (the same
validation used by SessionDO.safeParseTunnelUrls()), the handler returns an
error response (e.g., 400/422 or 500) with a descriptive error message and does
not treat it as "no tunnels"; update the catch branch and the parsed-type check
in tunnelUrls() to return a non-200 Response containing the parse/validation
error (and log it via deps.getLog()) instead of returning { tunnelUrls: {} } so
corrupted persisted data is distinguishable from "no tunnels resolved yet".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d3100436-3071-482d-82d0-8da3662bb229
📒 Files selected for processing (6)
packages/control-plane/src/router.tspackages/control-plane/src/session/contracts.tspackages/control-plane/src/session/durable-object.tspackages/control-plane/src/session/http/handlers/sandbox.handler.tspackages/control-plane/src/session/http/routes.test.tspackages/control-plane/src/session/http/routes.ts
Resolve router.ts conflict against upstream's route-module refactor: move the tunnel-urls route into the session-runtime-proxy module and keep the sandbox-auth allowlist entry. Also address review feedback: - mark /sessions/:id/tunnel-urls SCM-agnostic so GitLab deployments can reach it (was blocked by enforceImplementedScmProvider) - return 500 instead of an empty 200 when stored tunnel_urls JSON is malformed, so corrupted data is distinguishable from no-tunnels-yet - add handler + router tests covering both Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the response contract (404/500/200) to satisfy the docstring coverage check.
|
thanks @LarsBuur ! |
## Summary Follow-up to #691 (already merged). That PR added the sandbox-auth `GET /sessions/:id/tunnel-urls` endpoint with an **inline** JSON parser for `sandbox.tunnel_urls`, duplicating the `safeParseTunnelUrls` logic already living in `SessionDO`. The two had quietly diverged: | | `safeParseTunnelUrls` (DO state path) | inline parser (new endpoint) | |---|---|---| | invalid JSON | `null` | `500` | | not a plain object / is array | **not checked** (blind cast) | `500` | | value not a string | not checked | not checked | Two parsers for one column, neither aware of the other — drift bait the next time the stored format changes. ## What changed Extract one pure helper, `parseTunnelUrls(raw)`, in `packages/control-plane/src/session/tunnel-urls.ts`, and route both call sites through it: - **handler `tunnelUrls()`** drops its inline `try/catch` + shape check (~15 lines → 4). - **`SessionDO#safeParseTunnelUrls`** delegates to the helper, keeping its contextual `warn` log, and now gains the object + string-value validation it previously lacked. The helper validates JSON, plain-object shape, and that **every value is a string**, returning `null` on any failure. Logging stays in the callers, so the helper is pure and unit-testable; each caller maps `null` as it sees fit (the DO state path falls open to `null` for the UI; the endpoint returns `500` so the in-sandbox `setup.sh` hard-fails instead of writing a garbage `.tunnels.env`). Also tightened the endpoint doc comment: it implied the `500` path fully guards against an empty `.tunnels.env`, but a *not-yet-resolved* sandbox still returns `200 {}` — so the client must tolerate an empty result and retry. The comment now says so. ## Behavior Unchanged for valid data. The only differences are **stricter rejection of corrupt stored data**: the DO state path now treats a non-object/non-string blob as `null` (was a blind cast), and the endpoint now `500`s on non-string values (was silently returned). ## Tests - New `tunnel-urls.test.ts` unit-tests the parser directly (valid, empty, invalid JSON, non-object, non-string values). - Added a handler case: non-string value → `500`. - `npm run typecheck -w @open-inspect/control-plane` — clean. - `npm test -w @open-inspect/control-plane` for `tunnel-urls`, `sandbox.handler`, `routes`, and `router.scm-credentials` — 32 passed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved tunnel URL validation with clearer error responses when misconfigured tunnel data is detected, returning HTTP 500 with a descriptive error message instead of generic failures. * **Tests** * Added comprehensive test coverage for tunnel URL parsing, including handling of invalid JSON, malformed data structures, and non-string values. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Summary
Adds a sandbox-authenticated read endpoint that returns the tunnel URLs already stored on the session's sandbox row by
SandboxLifecycleManager#storeAndBroadcastTunnelUrls. The agent inside a Modal sandbox can now fetch its own tunnel URLs withSANDBOX_AUTH_TOKEN.Why
We hit this running #663 (
feat(modal): expose tunnel URLs via /workspace/.tunnels.env) on our deployment.On the Modal provider,
SandboxManager._write_tunnel_env_filewrites/workspace/.tunnels.envfrom outside the sandbox viasandbox.open.aio(). In our case that write didn't propagate to the in-sandbox filesystem view in time for the supervisor's 30s_wait_for_tunnel_env_file. The logs showed the expected sequence and then never recovered:Inside the sandbox,
/workspace/.tunnels.envwas genuinely missing afterwards (stat: No such file or directory), even though the manager had reported a successful write. The supervisor falls open on timeout, butstart.shand the agent then have no URL to share.The control plane already has the resolved URLs (in
sandbox.tunnel_urls, populated regardless of the file-write step). The simplest fix that doesn't depend on the Modal SDK's filesystem semantics is to expose those URLs over a sandbox-auth endpoint and let a small.openinspect/setup.shwrite the file from inside the sandbox. We've been running that for a day and/workspace/.tunnels.envis now reliable on every fresh session.What changed
Mirrors the existing
/sessions/:id/scm-credentialssandbox-auth pattern. No new tables, no migration, no new state — purely exposes data the DO already stores.contracts.ts:tunnelUrls = '/internal/tunnel-urls'http/routes.ts: GET route + interface field (and assertion inroutes.test.ts)http/handlers/sandbox.handler.ts:tunnelUrls()reads and parsessandbox.tunnel_urlsJSONsession/durable-object.ts: wires the handlerrouter.ts:SANDBOX_AUTH_ROUTESregex + GET route entry +handleGetTunnelUrls+64 lines across 6 files.
Example client (
.openinspect/setup.sh)Verification
npm run typecheck -w @open-inspect/shared @open-inspect/control-planenpm test -w @open-inspect/control-plane -- --run src/session/http/routes.test.tscat /workspace/.tunnels.envnow returns all configured tunnel URLs on every fresh session.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Behavior
Tests