Skip to content

feat(init): add Cloudflare DO agent transport with server-controlled split#1180

Draft
betegon wants to merge 3 commits into
mainfrom
feat/init-cf-agent-cli
Draft

feat(init): add Cloudflare DO agent transport with server-controlled split#1180
betegon wants to merge 3 commits into
mainfrom
feat/init-cf-agent-cli

Conversation

@betegon

@betegon betegon commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

Adds a second transport for sentry init: a reconnecting WebSocket client that talks to the new Cloudflare Durable Object agent (server side: getsentry/cli-init-api#187). Which transport a run uses is decided server-side via /route, so the canary split changes without shipping a new CLI. The default and fallback is the existing Mastra path, so this is a no-op for current users until the split is turned up.

Older CLIs never call /route, so they are unaffected — they keep using Mastra.

Architecture

This diagram is shared with the server PR.

flowchart TB
  U["Developer runs: sentry init"]

  subgraph CLI["CLI (sentry binary, Node)"]
    PF["preflight: org / project / team + Sentry auth"]
    RT["resolveTransport(): GET /route"]
    ADR["agent-do-runner: reconnecting WebSocket client"]
    LT["local tools + prompts (executeTool, handleInteractive)"]
  end

  subgraph NET["Transport"]
    WS["WebSocket - live and resilient (client pings + reconnect)"]
    LEGACY["HTTP suspend/resume (Mastra, existing)"]
  end

  subgraph SRV["Server: Cloudflare Worker (agents SDK)"]
    RTE["/route: server-controlled canary split (AGENT_DO_PERCENT)"]
    DO["SentryInitAgent: Durable Object"]
    AG["one agent: analyze + gates + implement + verify"]
  end

  subgraph INF["Infra and libraries"]
    SQL["DO embedded SQLite: durable phase + pending (no D1)"]
    GW["Vercel AI Gateway via ai SDK -> LLM"]
    SAPI["Sentry API: ensure project + DSN"]
    MASTRA["Mastra worker + D1 (existing path)"]
  end

  U --> PF --> RT
  RT -->|"decision"| RTE
  RT -->|"agent-do"| ADR
  RT -->|"mastra"| LEGACY
  ADR <-->|"tool-request / prompt / result"| WS
  WS <--> DO
  DO --> AG
  AG -->|"LLM turns"| GW
  AG -->|"ensure project"| SAPI
  DO --> SQL
  ADR -.->|"runs tools locally, returns results"| LT
  LEGACY <--> MASTRA
Loading

Changes

  • agent-do-runner.ts (new): the WebSocket client. Reuses the existing executeTool registry and handleInteractive prompts, reconnects on drop (deploys / laptop sleep), and sends client-side WebSocket pings for idle keepalive (Cloudflare auto-answers pong without waking the DO).
  • wizard-runner.ts: resolveTransport() asks the server /route and obeys; runViaAgentDO() drives the run. The Mastra suspend/resume path is untouched. SENTRY_INIT_AGENT_DO=1|0 is a manual override; SENTRY_INIT_AGENT_DO_PERCENT is a local fallback if /route is unreachable.
  • Adds ws (bundled into the single-file build).

Test plan

  • Ran against the deployed worker; server-controlled split verified at 50% and forced to 100%.
  • /verify-sentry confirmed real errors + traces for node-express and nextjs.
  • Mastra path unchanged (default when /route says so or is unreachable).

Paired with getsentry/cli-init-api#187 (server side).

betegon and others added 2 commits July 2, 2026 21:43
…split

New reconnecting WebSocket client that runs sentry init against the
Durable Object agent, reusing the existing local tools + interactive
prompts. Transport is chosen server-side via /route, defaulting to the
existing Mastra path, so the canary percentage changes without a CLI
release. Older CLIs are unaffected.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-1180/

Built to branch gh-pages at 2026-07-02 20:09 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Codecov Results 📊

❌ Patch coverage is 11.69%. Project has 5392 uncovered lines.
❌ Project coverage is 81.05%. Comparing base (base) to head (head).

Files with missing lines (3)
File Patch % Lines
src/lib/init/agent-do-runner.ts 2.08% ⚠️ 188 Missing
src/lib/init/wizard-runner.ts 56.25% ⚠️ 14 Missing and 6 partials
src/lib/init/constants.ts 71.43% ⚠️ 2 Missing and 1 partials
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    81.62%    81.05%    -0.57%
==========================================
  Files          408       409        +1
  Lines        28227     28458      +231
  Branches     18384     18569      +185
==========================================
+ Hits         23040     23066       +26
- Misses        5187      5392      +205
- Partials      1886      1894        +8

Generated by Codecov Action

Comment thread src/lib/init/agent-do-runner.ts
Comment on lines +627 to +631
const base = (args.agentDoUrl ?? INIT_AGENT_DO_URL).replace(
TRAILING_SLASHES_RE,
""
);
const url = `${base}/agents/sentry-init-agent/${runId}`;

@sentry-warden sentry-warden Bot Jul 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Server-supplied agentDoUrl used as WebSocket target without scheme validation, risking auth-token exposure over cleartext

resolveTransport() returns agentDoUrl from the /route JSON body with no validation, and runViaAgentDO uses it verbatim to build the WebSocket URL (args.agentDoUrl ?? INIT_AGENT_DO_URL). There is no check that the value uses the secure wss:// scheme. connectOnce then sends the Sentry auth token as an Authorization: Bearer header to whatever host/scheme the URL specifies. If the server returns (or is misconfigured to return) a ws:// URL, the bearer token is transmitted in cleartext and can be captured by an on-path network attacker. This is a downgrade/defense-in-depth weakness independent of server trust. Note the escalated 'arbitrary code execution' concern is largely inherent to the intended design: a server that routes a run to the DO transport already drives local tool execution (run-commands via executeTool) over the legitimate connection, so a compromised/malicious /route server is already fully trusted in this flow; the distinct, avoidable risk here is the lack of a secure-scheme (and ideally host allowlist) check before the token is sent.

Evidence
  • resolveTransport() (wizard-runner.ts:464-469) casts the /route body as { agentDoUrl?: string } and returns data.agentDoUrl with no scheme or host validation.
  • runViaAgentDO (agent-do-runner.ts:627) uses args.agentDoUrl ?? INIT_AGENT_DO_URL directly to form the WS URL; a ws:// value bypasses TLS.
  • connectOnce (agent-do-runner.ts:387-390) attaches headers: { Authorization: 'Bearer ${token}' } to the WebSocket regardless of scheme, so a cleartext ws:// target leaks the token.
  • The /route request itself is TLS-protected (customFetch over the https-normalized base), so injecting a malicious URL requires a compromised server; that same server can already issue run-commands, so the RCE angle is not a new privilege — the concrete avoidable defect is the missing wss:// enforcement.
Also found at 3 additional locations
  • src/lib/init/wizard-runner.ts:466-469
  • src/lib/init/wizard-runner.ts:49
  • src/lib/init/wizard-runner.ts:934

Identified by Warden find-bugs, security-review · YJR-CDF

If the first WebSocket connect failed before `open` (e.g. a transient on
the upgrade), the reconnect loop still marked start as sent, so the retry
never sent `start` and the run hung. Track whether start was actually sent
and emit it on the first socket that opens.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

1 participant