fix: send Origin header so SaaS edge WAF accepts /workspaces/* fetches

Production tenants front the workspace-server with a Cloudflare WAF that
enforces same-origin on /workspaces/* paths. Without an Origin header the
WAF silently re-routes the request to the canvas Next.js (which has no
/workspaces page), so polls returned empty 404s and replies failed with
an opaque error.

Browsers set Origin automatically for cross-origin POSTs; Node/Bun fetch
does not (it's a browser-only concern). Both fetch sites in this plugin
hit /workspaces/* with a workspace bearer that's only valid against
PLATFORM_URL anyway, so we set Origin: PLATFORM_URL explicitly — no risk
of leaking the bearer to a different origin.

Verified against hongmingwang.moleculesai.app:
  - Pre-fix: GET /workspaces/:id/activity → empty 404 (WAF re-route)
  - Post-fix: GET /workspaces/:id/activity → 200 [] (correct)

The reply path gets the same fix; e2e verification of replies is blocked
upstream (the platform's outbound forward to a registered URL needs the
agent reachable from the platform's network), unrelated to this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-29 21:08:04 -07:00
parent f3402e48ff
commit 2f08478edb

View File

@ -162,7 +162,17 @@ async function pollWorkspace(workspaceId: string, mcp: Server): Promise<void> {
let resp: Response
try {
resp = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
headers: {
Authorization: `Bearer ${token}`,
// Same-origin header — required by the tenant's edge WAF on hosted
// SaaS deployments. Without it the WAF rewrites the request and
// /workspaces/* returns an empty 404 (it's silently routed to the
// canvas Next.js, which has no /workspaces page). Node/Bun fetch
// doesn't auto-set Origin (that's a browser-only concern), so we
// set it explicitly to PLATFORM_URL — the only origin the bearer
// is valid against anyway, so no risk of leaking it elsewhere.
Origin: PLATFORM_URL,
},
signal: AbortSignal.timeout(10_000),
})
} catch (err) {
@ -343,6 +353,10 @@ async function replyToWorkspace(args: z.infer<typeof ReplyArgsSchema>): Promise<
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
'X-Source-Workspace-Id': workspace_id,
// Same-origin header for SaaS edge WAF — see pollWorkspace fetch
// for the full explanation. /workspaces/* requires it on hosted
// tenants; localhost ignores it.
Origin: PLATFORM_URL,
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30_000),