From 4d2dfe47a1dafe8f4c5819dba9dc1d6bad1daaf7 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 17 Jul 2026 09:48:34 -0400 Subject: [PATCH] fix(mcp): platform token requests send Org-Id/Workspace-Id tenancy headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A deployed agent's auth.type=platform read failed at startup with 'platform token endpoint returned 401: missing org-id header'. The platform verifies a PER-ORG HS256 token, so it needs Org-Id to select the signing secret BEFORE it can validate the bearer — and Workspace-Id to authorize the request against the registry entry (entitlement). The token request sent only the bearer. Now sends both from the standard FORGE_ORG_ID / FORGE_WORKSPACE_ID env the platform injects (omitted when empty), exactly as the admission and remote-session-store clients already do. Test asserts the resolver receives them. Co-Authored-By: Claude Fable 5 --- forge-core/mcp/platform_token.go | 13 +++++++++++++ forge-core/mcp/platform_token_test.go | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/forge-core/mcp/platform_token.go b/forge-core/mcp/platform_token.go index c787715..ec7990f 100644 --- a/forge-core/mcp/platform_token.go +++ b/forge-core/mcp/platform_token.go @@ -98,6 +98,19 @@ func (p *platformTokenSource) fetch(ctx context.Context) (string, time.Duration, req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+identity) + // Tenancy headers, exactly as the admission + remote-session-store + // clients send them: the platform verifies a PER-ORG HS256 token, so it + // needs Org-Id to select the signing secret BEFORE it can validate the + // bearer (without this the endpoint 401s "missing org-id header"), and + // Workspace-Id to authorize the request against the entry (entitlement). + // Read from the standard FORGE_ORG_ID / FORGE_WORKSPACE_ID env the + // platform always injects; omitted when empty (standalone/dev). + if org := os.Getenv("FORGE_ORG_ID"); org != "" { + req.Header.Set("Org-Id", org) + } + if ws := os.Getenv("FORGE_WORKSPACE_ID"); ws != "" { + req.Header.Set("Workspace-Id", ws) + } client := p.client if client == nil { diff --git a/forge-core/mcp/platform_token_test.go b/forge-core/mcp/platform_token_test.go index 8d15f32..bdd73e6 100644 --- a/forge-core/mcp/platform_token_test.go +++ b/forge-core/mcp/platform_token_test.go @@ -16,9 +16,19 @@ import ( // server ref, and can emit a (forbidden) refresh_token. func newFakeResolver(t *testing.T, expiresIn int, includeRefresh bool) (*httptest.Server, *int) { t.Helper() + t.Setenv("FORGE_ORG_ID", "org-42") + t.Setenv("FORGE_WORKSPACE_ID", "ws-7") hits := 0 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits++ + // Tenancy headers must ride along so the platform can select the + // per-org HS256 secret + authorize the workspace (the "missing + // org-id header" 401 fix). + if r.Header.Get("Org-Id") != "org-42" || r.Header.Get("Workspace-Id") != "ws-7" { + w.WriteHeader(http.StatusUnauthorized) + _ = json.NewEncoder(w).Encode(map[string]string{"error": "missing org-id header"}) + return + } if r.Header.Get("Authorization") != "Bearer agent-cred-1" { w.WriteHeader(http.StatusUnauthorized) return