fix(workspace-runtime): add Origin header so SaaS edge WAF accepts MCP tool calls

Discovered while smoke-testing the molecule-mcp external-runtime path
against a live tenant (hongmingwang.moleculesai.app). Every tool call
that hit /workspaces/* or /registry/*/peers returned 404 — but
/registry/register and /registry/heartbeat returned 200. Diagnosis:
the tenant's edge WAF requires a same-origin header. Without it,
unhandled paths get silently rewritten to the canvas Next.js app,
which has no /workspaces or /registry/:id/peers route and returns an
empty 404. The molecule-mcp-claude-channel plugin already sets this
header (server.ts:271-276); the workspace runtime never did because
in-container PLATFORM_URLs (Docker network) aren't behind the WAF.

Fix: extend platform_auth.auth_headers() to include
Origin: ${PLATFORM_URL} whenever PLATFORM_URL is set. Inside-container
behavior is unchanged (the WAF is path-irrelevant for the internal
hostnames). External-runtime calls now thread the WAF correctly.

Verification (live, against a freshly-registered external workspace):
  pre-fix:  get_workspace_info → "not found", list_peers → 404
  post-fix: get_workspace_info → full workspace JSON,
            list_peers → "Claude Code Agent (ID: 97ac32e9..., status: online)"

This is the kind of bug unit tests can never catch — caught only by
running the wheel against the real tenant. Memory:
feedback_always_run_e2e.md.

Test coverage: 4 new tests in test_platform_auth.py — Origin alone
when no token + Origin + Authorization both, no-PLATFORM_URL falls
through to original empty-dict behavior, env-token path with Origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-30 15:30:15 -07:00
parent 169e284d57
commit 74c5e0d7a8
2 changed files with 43 additions and 6 deletions

View File

@ -111,11 +111,26 @@ def auth_headers() -> dict[str, str]:
"""Return a header dict to merge into httpx calls. Empty if no token
is available yet callers send the request as-is and the platform's
heartbeat handler grandfathers pre-token workspaces through until
their next /registry/register issues one."""
their next /registry/register issues one.
Always sets ``Origin`` to ``PLATFORM_URL`` when that env var is set.
On hosted SaaS deployments the tenant's edge WAF requires a same-
origin header without it ``/workspaces/*`` and ``/registry/*/peers``
requests get silently rewritten to the canvas Next.js app, which has
no such routes and returns an empty 404. Inside-container calls are
unaffected (Docker-internal PLATFORM_URLs aren't behind the WAF).
Discovered while smoke-testing the molecule-mcp external-runtime
path against a live tenant every tool call returned "not found"
because the WAF was eating them.
"""
headers: dict[str, str] = {}
platform_url = os.environ.get("PLATFORM_URL", "").strip()
if platform_url:
headers["Origin"] = platform_url
tok = get_token()
if not tok:
return {}
return {"Authorization": f"Bearer {tok}"}
if tok:
headers["Authorization"] = f"Bearer {tok}"
return headers
def self_source_headers(workspace_id: str) -> dict[str, str]:

View File

@ -65,15 +65,36 @@ def test_save_token_rotation_overwrites(tmp_path):
assert platform_auth.get_token() == "token-v2"
def test_auth_headers_when_no_token_is_empty():
def test_auth_headers_when_no_token_and_no_platform_is_empty(monkeypatch):
monkeypatch.delenv("PLATFORM_URL", raising=False)
assert platform_auth.auth_headers() == {}
def test_auth_headers_format():
def test_auth_headers_when_no_token_includes_origin(monkeypatch):
"""Origin must be set even without a token — the WAF gates ALL
requests to /workspaces and /registry, including pre-token bootstrap
register calls. Without Origin those would silently 404 from Next.js."""
monkeypatch.setenv("PLATFORM_URL", "https://tenant.moleculesai.app")
assert platform_auth.auth_headers() == {"Origin": "https://tenant.moleculesai.app"}
def test_auth_headers_format(monkeypatch):
monkeypatch.delenv("PLATFORM_URL", raising=False)
platform_auth.save_token("hello-world")
assert platform_auth.auth_headers() == {"Authorization": "Bearer hello-world"}
def test_auth_headers_includes_origin_when_platform_url_set(monkeypatch):
"""Both Authorization and Origin land on the same dict so the
SaaS edge WAF accepts every workspace-runtime request."""
monkeypatch.setenv("PLATFORM_URL", "https://hongmingwang.moleculesai.app")
platform_auth.save_token("tok")
assert platform_auth.auth_headers() == {
"Authorization": "Bearer tok",
"Origin": "https://hongmingwang.moleculesai.app",
}
def test_get_token_caches_after_first_disk_read(tmp_path, monkeypatch):
path = tmp_path / ".auth_token"
path.write_text("disk-token")
@ -179,5 +200,6 @@ def test_env_token_caches_like_file_token(tmp_path, monkeypatch):
def test_auth_headers_works_with_env_token(tmp_path, monkeypatch):
"""Header construction must use the env-fallback token, not silently
return {} when no file exists."""
monkeypatch.delenv("PLATFORM_URL", raising=False)
monkeypatch.setenv("MOLECULE_WORKSPACE_TOKEN", "external-bearer")
assert platform_auth.auth_headers() == {"Authorization": "Bearer external-bearer"}