Skip to content

feat(control-plane): sandbox-auth GET /sessions/:id/tunnel-urls#691

Merged
ColeMurray merged 3 commits into
ColeMurray:mainfrom
LarsBuur:upstream/sandbox-tunnel-urls-endpoint
Jun 13, 2026
Merged

feat(control-plane): sandbox-auth GET /sessions/:id/tunnel-urls#691
ColeMurray merged 3 commits into
ColeMurray:mainfrom
LarsBuur:upstream/sandbox-tunnel-urls-endpoint

Conversation

@LarsBuur

@LarsBuur LarsBuur commented May 31, 2026

Copy link
Copy Markdown
Contributor

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 with SANDBOX_AUTH_TOKEN.

GET /sessions/:id/tunnel-urls
Authorization: Bearer <SANDBOX_AUTH_TOKEN>

200 OK
{ "tunnelUrls": { "3000": "https://...", "5000": "https://...", ... } }

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_file writes /workspace/.tunnels.env from outside the sandbox via sandbox.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:

manager     tunnel.urls_written        path=/workspace/.tunnels.env  ports=[3000,4000,5000,6006,7000]
supervisor  tunnel.env_file_wait_timeout  path=/workspace/.tunnels.env  ports=[3000,4000,5000,6006,7000]  timeout_seconds=30

Inside the sandbox, /workspace/.tunnels.env was genuinely missing afterwards (stat: No such file or directory), even though the manager had reported a successful write. The supervisor falls open on timeout, but start.sh and 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.sh write the file from inside the sandbox. We've been running that for a day and /workspace/.tunnels.env is now reliable on every fresh session.

What changed

Mirrors the existing /sessions/:id/scm-credentials sandbox-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 in routes.test.ts)
  • http/handlers/sandbox.handler.ts: tunnelUrls() reads and parses sandbox.tunnel_urls JSON
  • session/durable-object.ts: wires the handler
  • router.ts: SANDBOX_AUTH_ROUTES regex + GET route entry + handleGetTunnelUrls

+64 lines across 6 files.

Example client (.openinspect/setup.sh)

session_id=$(printf '%s' "$SESSION_CONFIG" | jq -r '.session_id')
curl -fsS -H "Authorization: Bearer $SANDBOX_AUTH_TOKEN" \
  "$CONTROL_PLANE_URL/sessions/$session_id/tunnel-urls" \
  | jq -r '.tunnelUrls // {} | to_entries[] | "TUNNEL_\(.key)=\(.value)"' \
  > /workspace/.tunnels.env

Verification

  • npm run typecheck -w @open-inspect/shared @open-inspect/control-plane
  • npm test -w @open-inspect/control-plane -- --run src/session/http/routes.test.ts
  • Deployed on a single-tenant Modal deployment; cat /workspace/.tunnels.env now returns all configured tunnel URLs on every fresh session.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a session endpoint to retrieve tunnel URLs, returning a mapping of identifiers to URLs with Cache-Control: no-store.
  • Behavior

    • Endpoint accepts sandbox-authenticated requests, is reachable via runtime proxy, and is excluded from SCM-provider enforcement.
    • Returns 404 when no sandbox exists; returns 500 on malformed stored data.
  • Tests

    • Added unit and integration tests covering success, auth, proxying, and error cases.

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>
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aa6e300b-7452-4fca-bfd1-36df0ee5b241

📥 Commits

Reviewing files that changed from the base of the PR and between 6199acd and a9162eb.

📒 Files selected for processing (1)
  • packages/control-plane/src/session/http/handlers/sandbox.handler.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/control-plane/src/session/http/handlers/sandbox.handler.ts

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Tunnel URLs Endpoint

Layer / File(s) Summary
Internal session routes and wiring
packages/control-plane/src/session/contracts.ts, packages/control-plane/src/session/http/routes.ts, packages/control-plane/src/session/durable-object.ts, packages/control-plane/src/session/http/routes.test.ts, packages/control-plane/src/routes/session-runtime-proxy.ts
SessionInternalPaths adds tunnelUrls; SessionInternalRouteHandlers adds tunnelUrls; createSessionInternalRoutes registers GET /internal/tunnel-urls; session Durable Object delegates to sandboxHandler.tunnelUrls(); internal route tests updated; runtime proxy adds GET /sessions/:id/tunnel-urls.
Sandbox handler implementation and tests
packages/control-plane/src/session/http/handlers/sandbox.handler.ts, packages/control-plane/src/session/http/handlers/sandbox.handler.test.ts
SandboxHandler interface adds tunnelUrls(); implementation fetches sandbox, returns 404 if missing, parses sandbox.tunnel_urls JSON expecting an object, logs and returns 500 on parse/type errors, otherwise returns { tunnelUrls } with Cache-Control: no-store; tests cover success and failure variants.
Public API endpoint and authorization
packages/control-plane/src/router.ts, packages/control-plane/src/router.scm-credentials.test.ts
Public router adds /sessions/:id/tunnel-urls to sandbox-auth allowlist and to isScmAgnosticRoute; gated-access test verifies internal-token-forwarded request reaches /internal/tunnel-urls.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • ColeMurray

Poem

"I hop from route to DO to sand, 🐇
A tiny token in my hand.
JSON parsed and warnings penned,
Tunnel URLs at journey's end.
Hooray — the sandbox lets friends land!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding a sandbox-authenticated endpoint for fetching tunnel URLs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8ceafe9 and 9ad53a0.

📒 Files selected for processing (6)
  • packages/control-plane/src/router.ts
  • packages/control-plane/src/session/contracts.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/control-plane/src/session/http/handlers/sandbox.handler.ts
  • packages/control-plane/src/session/http/routes.test.ts
  • packages/control-plane/src/session/http/routes.ts

Comment thread packages/control-plane/src/router.ts
Comment thread packages/control-plane/src/session/http/handlers/sandbox.handler.ts
LarsBuur and others added 2 commits June 1, 2026 11:31
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.
@ColeMurray ColeMurray merged commit cd6c977 into ColeMurray:main Jun 13, 2026
18 checks passed
@ColeMurray

Copy link
Copy Markdown
Owner

thanks @LarsBuur !

ColeMurray added a commit that referenced this pull request Jun 14, 2026
## 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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants