From 4fdeabdbe001d1ddf58f09080e1016d5f850d8a3 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Fri, 24 Apr 2026 18:13:13 -0700 Subject: [PATCH] =?UTF-8?q?fix(e2e):=20send=20X-Molecule-Org-Id=20header?= =?UTF-8?q?=20=E2=80=94=20TenantGuard=20404s=20without=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third E2E bug in the staging→main chain, found while debugging the \`Workspace create 404\` failure that surfaced after the previous two E2E fixes (instance_status, staging.moleculesai.app DNS). Root cause: workspace-server's \`middleware/TenantGuard\` middleware returns 404 (not 401/403, intentionally — see comment in \`tenant_guard.go\`: "must not be inferable by probing other orgs' machines") when a request to the tenant origin lacks one of: - X-Molecule-Org-Id header matching MOLECULE_ORG_ID env on the tenant - Fly-Replay-Src state from the CP router (production browser path) - Same-origin Canvas (Referer == Host) The E2E was a direct GitHub-Actions curl with neither — every non- allowlisted route 404'd with the platform's ratelimit headers but none of the security headers, which made it look like a missing route in the platform. The org UUID is already on the admin-orgs row alongside instance_status, so capture it during the readiness poll and add it to the tenantAuth header bag. Both /workspaces (POST) and /workspaces/:id (GET) now carry it. Allowlist still contains /health, /metrics, /registry/register, /registry/heartbeat — so the TLS readiness step (which hits /health) keeps working without the header. Co-Authored-By: Claude Opus 4.7 (1M context) --- canvas/e2e/staging-setup.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/canvas/e2e/staging-setup.ts b/canvas/e2e/staging-setup.ts index b76e395f..963f9ccb 100644 --- a/canvas/e2e/staging-setup.ts +++ b/canvas/e2e/staging-setup.ts @@ -128,13 +128,23 @@ export default async function globalSetup(_config: FullConfig): Promise { // waitFor never resolved truthy and the harness invariably timed // out at 1200s — masking real CP bugs (see #242 chain) AND // surviving real CP fixes alike. + // Capture the org UUID alongside the running check — every request + // we send to the tenant URL after this point needs an + // X-Molecule-Org-Id header (see workspace-server middleware/tenant_guard.go). + // Without it, TenantGuard returns 404 ("must not be inferable by + // probing other orgs' machines"). The CP returns the id on the + // admin-orgs row; capture it here while we're already polling. + let orgID = ""; await waitFor( async () => { const r = await jsonFetch(`${CP_URL}/cp/admin/orgs`, { headers: adminAuth }); if (r.status !== 200) return null; const row = (r.body?.orgs || []).find((o: any) => o.slug === slug); if (!row) return null; - if (row.instance_status === "running") return true; + if (row.instance_status === "running") { + orgID = row.id; + return true; + } if (row.instance_status === "failed") throw new Error(`provision failed: ${slug}`); return null; }, @@ -142,7 +152,10 @@ export default async function globalSetup(_config: FullConfig): Promise { 15_000, "tenant provision", ); - console.log(`[staging-setup] Tenant running`); + if (!orgID) { + throw new Error(`expected admin-orgs row to carry id, got empty for slug=${slug}`); + } + console.log(`[staging-setup] Tenant running (org_id=${orgID})`); // 3. Fetch per-tenant admin token const tokRes = await jsonFetch( @@ -176,7 +189,17 @@ export default async function globalSetup(_config: FullConfig): Promise { ); // 5. Provision workspace - const tenantAuth = { Authorization: `Bearer ${tenantToken}` }; + // + // tenantAuth carries TWO headers, both required: + // - Authorization: Bearer — wsAdmin middleware gate + // - X-Molecule-Org-Id: — TenantGuard cross-org gate + // Missing the org-id header silently 404s every non-allowlisted + // route, with no body and no security headers. The 404 is intentional + // (existence-non-inference) which makes it look like a missing route. + const tenantAuth = { + "Authorization": `Bearer ${tenantToken}`, + "X-Molecule-Org-Id": orgID, + }; const ws = await jsonFetch(`${tenantURL}/workspaces`, { method: "POST", headers: tenantAuth,