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:
parent
169e284d57
commit
74c5e0d7a8
@ -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]:
|
||||
|
||||
@ -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"}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user