From 4414c92a87ce2dab3b703b7b1402b446541b2e46 Mon Sep 17 00:00:00 2001 From: hongming Date: Fri, 29 May 2026 00:05:21 +0000 Subject: [PATCH] =?UTF-8?q?fix(workspace-server):=20provider-matched=20byo?= =?UTF-8?q?k=20credential=20injection=20=E2=80=94=20strip=20stray=20non-ma?= =?UTF-8?q?tching=20global-origin=20LLM=20creds=20(internal#728=20Bug=201)?= =?UTF-8?q?=20[BEHAVIOR-AFFECTING=20=E2=80=94=20CTO=20merge-go]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1995 removed the blanket global-LLM-cred strip on the byok branch (correct for the platform-key co-mingling it targeted), but left EVERY claude-code workspace inheriting the tenant-global CLAUDE_CODE_OAUTH_TOKEN. The claude-code runtime greedily prefers that oauth (llm-auth: detected oauth -> api.anthropic.com), so a workspace whose RESOLVED provider is NOT anthropic-oauth (minimax, kimi-byok) routes its non-Anthropic model to Anthropic -> "Claude Code returned an error result" (agents-team Dev Engineer B, MiniMax-M2.7; live-confirmed 2026-05-28 via SSM container logs, internal#728 comment 52493). Fix: provider-AWARE replacement for the over-removed strip. On the byok/disabled branch, keep ONLY the global-origin LLM bypass creds whose env-var name is in the RESOLVED provider's auth_env; strip the rest. - minimax auth_env MINIMAX_API_KEY/ANTHROPIC_AUTH_TOKEN/ANTHROPIC_API_KEY -> stray global CLAUDE_CODE_OAUTH_TOKEN is non-matching -> stripped (fixes DevB). - anthropic-oauth auth_env CLAUDE_CODE_OAUTH_TOKEN -> matches -> kept (PM opus + reno opus-byok NOT regressed; #1994 ByokGlobalScopeOAuthSurvives guard holds). NOT a return to the blanket strip (which would re-break the byok-anthropic-oauth case #1994 fixed) — keyed off DeriveProvider's resolved provider. Provenance-scoped: only operator-store (global_secrets) origin keys are provider-gated. User-authored workspace_secrets (provenance flag cleared by loadWorkspaceSecrets) are NEVER stripped — JRS kimi workspace-key, reno's own oauth are exempt. Fail-OPEN: an underivable provider / unavailable registry strips nothing (keep-first; worst case is a kept stray, never removing the only usable cred -> never fail-closes a legitimate byok workspace). Threads loadWorkspaceSecrets's globalKeys provenance side-channel into applyPlatformManagedLLMEnv (signature +map[string]struct{}); caller prepareProvisionContext already has it. Tests (llm_billing_mode_provision_parity_test.go): - MinimaxStripsStrayGlobalOAuth — DevB repro: minimax-resolving ws strips the stray global oauth + keeps MINIMAX_API_KEY routing. - WorkspaceOriginCredExemptFromStrip — user-authored ws_secrets cred survives even when non-matching. - ByokGlobalScopeOAuthSurvives (strengthened) — global-origin oauth on opus SURVIVES via provider match (PM/reno regression guard). Mutation-load-bearing (verified RED): (1) remove strip -> blanket-keep regresses DevB; (2) empty keep set (provider-unaware) -> minimax routing + reno oauth stripped; (3) iterate all bypass keys (provenance-unaware) -> user-authored cred stripped. build ok; build -tags=integration ok; go test ./internal/handlers/ ok; golangci-lint ./internal/handlers/ -> 0 issues. Refs internal#728. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../llm_billing_mode_provision_parity_test.go | 127 +++++++++++++++++- .../internal/handlers/workspace_provision.go | 108 ++++++++++++++- .../handlers/workspace_provision_shared.go | 13 +- .../workspace_provision_shared_test.go | 30 ++--- 4 files changed, 248 insertions(+), 30 deletions(-) diff --git a/workspace-server/internal/handlers/llm_billing_mode_provision_parity_test.go b/workspace-server/internal/handlers/llm_billing_mode_provision_parity_test.go index 6b650afdd..7b0b3c70d 100644 --- a/workspace-server/internal/handlers/llm_billing_mode_provision_parity_test.go +++ b/workspace-server/internal/handlers/llm_billing_mode_provision_parity_test.go @@ -67,8 +67,10 @@ func TestApplyPlatformManagedLLMEnv_ReProvisionUsesStoredModel(t *testing.T) { "CLAUDE_CODE_OAUTH_TOKEN": "RENO-OWN-OAUTH", // workspace_secrets origin "ANTHROPIC_BASE_URL": "https://api.moleculesai.app/api/v1/internal/llm/anthropic", } - // payload.Model == "" — exactly the re-provision shape. - res := applyPlatformManagedLLMEnv(ctx, envVars, wsID, "claude-code", "") + // payload.Model == "" — exactly the re-provision shape. The oauth is + // workspace_secrets-origin (NOT in globalKeys) → exempt from the #728 + // provider-matched strip regardless of provider match. + res := applyPlatformManagedLLMEnv(ctx, envVars, wsID, "claude-code", "", nil) if res.ResolvedMode != LLMBillingModeBYOK { t.Fatalf("re-provision with stored MODEL=opus must resolve byok, got %q (source=%s) — the #1994 divergence", res.ResolvedMode, res.Source) @@ -149,7 +151,7 @@ func TestApplyPlatformManagedLLMEnv_ReadProvisionParity(t *testing.T) { "MODEL": "opus", "CLAUDE_CODE_OAUTH_TOKEN": "RENO-OWN-OAUTH", } - provRes := applyPlatformManagedLLMEnv(ctx, provEnv, wsID, "claude-code", "") + provRes := applyPlatformManagedLLMEnv(ctx, provEnv, wsID, "claude-code", "", nil) if err := provMock.ExpectationsWereMet(); err != nil { t.Errorf("provision-path sqlmock expectations: %v", err) } @@ -179,7 +181,7 @@ func TestApplyPlatformManagedLLMEnv_DefaultPreservation(t *testing.T) { // No MODEL anywhere, no auth env — nothing to derive. envVars := map[string]string{} - res := applyPlatformManagedLLMEnv(ctx, envVars, wsID, "claude-code", "") + res := applyPlatformManagedLLMEnv(ctx, envVars, wsID, "claude-code", "", nil) if res.ResolvedMode != LLMBillingModePlatformManaged { t.Fatalf("no model + no cred must default platform_managed (CTO: default stays platform), got %q (source=%s)", res.ResolvedMode, res.Source) @@ -219,8 +221,13 @@ func TestApplyPlatformManagedLLMEnv_ByokGlobalScopeOAuthSurvives(t *testing.T) { "MODEL": "opus", "CLAUDE_CODE_OAUTH_TOKEN": "TENANT-OWN-GLOBAL-OAUTH", } + // Provenance: the oauth is GLOBAL-origin (internal#728). It must STILL + // survive — opus derives anthropic-oauth, whose auth_env IS + // CLAUDE_CODE_OAUTH_TOKEN, so the provider-matched strip keeps it. This is + // the PM/reno opus-byok regression guard against #728's strip. + globalKeys := map[string]struct{}{"CLAUDE_CODE_OAUTH_TOKEN": {}} - res := applyPlatformManagedLLMEnv(ctx, envVars, wsID, "claude-code", "") + res := applyPlatformManagedLLMEnv(ctx, envVars, wsID, "claude-code", "", globalKeys) if res.ResolvedMode != LLMBillingModeBYOK { t.Fatalf("opus derives byok; got %q", res.ResolvedMode) @@ -255,3 +262,113 @@ func TestReProvisionPayloadOmitsModel(t *testing.T) { t.Fatalf("re-provision payload model expected empty (the #1994 trigger), got %q", p.Model) } } + +// --- internal#728 Bug 1: provider-matched credential injection --------------- + +// TestApplyPlatformManagedLLMEnv_MinimaxStripsStrayGlobalOAuth is the direct +// repro of DevB (Dev Engineer B, MiniMax-M2.7, claude-code; live-confirmed +// 2026-05-28). config.yaml correctly resolves provider=minimax, but the +// container inherits the tenant-GLOBAL CLAUDE_CODE_OAUTH_TOKEN; the claude-code +// runtime greedily prefers it (`llm-auth: detected oauth`) and routes +// MiniMax-M2.7 → api.anthropic.com → `Claude Code returned an error result`. +// +// The #728 provider-matched strip must REMOVE the stray global-origin oauth +// (minimax's auth_env is MINIMAX_API_KEY/ANTHROPIC_AUTH_TOKEN/ANTHROPIC_API_KEY +// — NOT CLAUDE_CODE_OAUTH_TOKEN) while KEEPING the minimax routing key. +// +// Mutation (load-bearing): remove the stripNonMatchingGlobalOriginLLMCreds +// call (revert to #1994's blanket keep) → the oauth survives → this test RED on +// the oauth-absent assertion. Make the strip provider-UNAWARE (strip all +// global bypass keys) → MINIMAX_API_KEY also vanishes → RED on the +// minimax-routing assertion. Make it provenance-UNAWARE (strip by name +// regardless of origin) → the workspace-origin exemption test below goes RED. +func TestApplyPlatformManagedLLMEnv_MinimaxStripsStrayGlobalOAuth(t *testing.T) { + ctx := context.Background() + const wsID = "22222222-3333-4444-5555-666666666666" // agents-team Dev Engineer B + + mock := setupTestDB(t) + expectOverrideQuery(mock, wsID, "") + + // The container env on a re-provision: the MiniMax routing key + the stray + // tenant-global oauth (both global_secrets origin) + the stored model. + envVars := map[string]string{ + "MODEL": "MiniMax-M2.7", + "MINIMAX_API_KEY": "MINIMAX-TENANT-KEY", + "CLAUDE_CODE_OAUTH_TOKEN": "STRAY-TENANT-GLOBAL-OAUTH", + } + // Both creds are global_secrets origin (the tenant configured them at org + // scope; no per-workspace override re-set them). + globalKeys := map[string]struct{}{ + "MINIMAX_API_KEY": {}, + "CLAUDE_CODE_OAUTH_TOKEN": {}, + } + + res := applyPlatformManagedLLMEnv(ctx, envVars, wsID, "claude-code", "", globalKeys) + + if res.ResolvedMode != LLMBillingModeBYOK { + t.Fatalf("MiniMax-M2.7 must derive minimax → byok, got %q (source=%s)", res.ResolvedMode, res.Source) + } + if res.Source != BillingModeSourceDerivedProvider { + t.Errorf("source: got %q want derived_provider (MiniMax-M2.7 → minimax)", res.Source) + } + // THE FIX: the stray global oauth that does NOT match minimax's auth_env + // must be gone, so the runtime cannot prefer it and mis-route to Anthropic. + if v, present := envVars["CLAUDE_CODE_OAUTH_TOKEN"]; present { + t.Errorf("stray global-origin CLAUDE_CODE_OAUTH_TOKEN must be STRIPPED for a minimax-resolving workspace (the DevB bug); still present=%q", v) + } + // The minimax routing key (IS in minimax's auth_env) must remain. + if envVars["MINIMAX_API_KEY"] != "MINIMAX-TENANT-KEY" { + t.Errorf("minimax routing key must SURVIVE (it matches the resolved provider's auth_env); got %q", envVars["MINIMAX_API_KEY"]) + } + if !res.HasUsableLLMCred { + t.Errorf("MINIMAX_API_KEY is a usable credential → HasUsableLLMCred must stay true (not failed-closed)") + } + if _, present := envVars["MOLECULE_LLM_USAGE_TOKEN"]; present { + t.Errorf("byok must not inject the platform usage token") + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sqlmock expectations: %v", err) + } +} + +// TestApplyPlatformManagedLLMEnv_WorkspaceOriginCredExemptFromStrip pins the +// provenance guard: a CLAUDE_CODE_OAUTH_TOKEN the USER set via the canvas +// Secrets tab (workspace_secrets origin → NOT in globalKeys) must NEVER be +// stripped, even on a minimax-resolving workspace where it doesn't match the +// derived provider's auth_env. The user authored it deliberately; the #728 +// strip is scoped to the inherited operator-store channel only. +// +// Mutation: drop the `if _, isBypass...; continue` / globalKeys gate (strip by +// name regardless of origin) → the user's oauth vanishes → RED. +func TestApplyPlatformManagedLLMEnv_WorkspaceOriginCredExemptFromStrip(t *testing.T) { + ctx := context.Background() + const wsID = "33333333-4444-5555-6666-777777777777" + + mock := setupTestDB(t) + expectOverrideQuery(mock, wsID, "") + + envVars := map[string]string{ + "MODEL": "MiniMax-M2.7", + "MINIMAX_API_KEY": "MINIMAX-TENANT-KEY", + "CLAUDE_CODE_OAUTH_TOKEN": "USER-AUTHORED-OAUTH", + } + // MINIMAX_API_KEY is global-origin; the oauth is WORKSPACE-origin (the user + // re-set it via the Secrets tab, so loadWorkspaceSecrets cleared its + // global-origin flag) → exempt. + globalKeys := map[string]struct{}{"MINIMAX_API_KEY": {}} + + res := applyPlatformManagedLLMEnv(ctx, envVars, wsID, "claude-code", "", globalKeys) + + if res.ResolvedMode != LLMBillingModeBYOK { + t.Fatalf("MiniMax-M2.7 derives byok; got %q", res.ResolvedMode) + } + if envVars["CLAUDE_CODE_OAUTH_TOKEN"] != "USER-AUTHORED-OAUTH" { + t.Errorf("workspace-origin (user-authored) oauth must NOT be stripped even when it doesn't match the provider; got %q", envVars["CLAUDE_CODE_OAUTH_TOKEN"]) + } + if envVars["MINIMAX_API_KEY"] != "MINIMAX-TENANT-KEY" { + t.Errorf("matching minimax key must survive; got %q", envVars["MINIMAX_API_KEY"]) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sqlmock expectations: %v", err) + } +} diff --git a/workspace-server/internal/handlers/workspace_provision.go b/workspace-server/internal/handlers/workspace_provision.go index 3b7609d84..92439db52 100644 --- a/workspace-server/internal/handlers/workspace_provision.go +++ b/workspace-server/internal/handlers/workspace_provision.go @@ -900,7 +900,17 @@ type platformLLMEnvResult struct { Source BillingModeSource } -func applyPlatformManagedLLMEnv(ctx context.Context, envVars map[string]string, workspaceID, runtime, model string) platformLLMEnvResult { +// globalKeys is the provenance side-channel from loadWorkspaceSecrets: the set +// of env keys that originated from the operator-controlled global_secrets table +// (a workspace_secrets row of the same name overrides and clears the flag). It +// is consumed ONLY on the byok/disabled branch's provider-matched strip +// (internal#728 Bug 1): a global-origin LLM bypass cred that does NOT match the +// resolved provider's auth_env is stripped so a greedy runtime (claude-code +// prefers CLAUDE_CODE_OAUTH_TOKEN) cannot route a non-anthropic model to the +// wrong upstream. May be nil (no global-origin keys / unknown provenance) — a +// nil set strips nothing, preserving the pre-#728 behavior for callers that do +// not thread provenance. +func applyPlatformManagedLLMEnv(ctx context.Context, envVars map[string]string, workspaceID, runtime, model string, globalKeys map[string]struct{}) platformLLMEnvResult { // internal#718 P2-B: the platform-vs-byok decision now DERIVES the provider // from (runtime, model) via the registry and keys off IsPlatform(derived) — // NOT a stored LLM_PROVIDER and NOT the org rung. This path already carries @@ -945,19 +955,43 @@ func applyPlatformManagedLLMEnv(ctx context.Context, envVars map[string]string, envVars["MOLECULE_LLM_BILLING_MODE_RESOLVED"] = res.ResolvedMode if res.ResolvedMode != LLMBillingModePlatformManaged { // byok or disabled — DO NOT force-route to CP, DO NOT override the - // workspace's own ANTHROPIC_BASE_URL / OAuth token, and DO NOT strip - // the tenant's own LLM credentials. + // workspace's own ANTHROPIC_BASE_URL, and DO NOT strip the tenant's own + // (provider-matching) LLM credentials. // // molecule-core#1994 (corrected model): `global_secrets` is the // TENANT's store, not the platform's. The tenant's own credential — // at global OR workspace scope — is exactly what byok runs on, direct. - // We leave envVars untouched here and report whether a usable LLM - // credential survived so the caller can fail closed when there is - // genuinely none (no platform-managed-shaped key at any scope). The - // platform's own credential is never in a tenant's global_secrets + // The platform's own credential is never in a tenant's global_secrets // (guarded at the SetGlobal write boundary + the proxy token is // server-env-only), so leaving the tenant's globals in place cannot // re-open the platform-credit drain. + // + // internal#728 Bug 1 (provider-matched credential injection): #1994 + // removed the BLANKET strip, which was correct for the platform-key + // co-mingling it targeted but left EVERY claude-code workspace + // inheriting the tenant-global CLAUDE_CODE_OAUTH_TOKEN. A claude-code + // runtime greedily prefers that oauth (`llm-auth: detected oauth` → + // api.anthropic.com), so a workspace whose RESOLVED provider is NOT + // anthropic-oauth (minimax, kimi-byok, …) routes its non-Anthropic + // model to Anthropic and errors (`Claude Code returned an error + // result`; DevB MiniMax-M2.7 live-confirmed 2026-05-28). + // + // The precise, provider-AWARE replacement for the over-removed strip: + // keep ONLY the global-origin bypass creds whose env-var name is in the + // RESOLVED provider's auth_env; strip the rest. This is NOT a return to + // the blanket strip — it is keyed off the derived provider: + // - minimax (auth_env: MINIMAX_API_KEY, ANTHROPIC_AUTH_TOKEN, + // ANTHROPIC_API_KEY) → global-origin CLAUDE_CODE_OAUTH_TOKEN is + // NOT a match → stripped (fixes DevB). + // - anthropic-oauth (auth_env: CLAUDE_CODE_OAUTH_TOKEN) → the + // global-origin oauth IS a match → kept (PM/reno opus byok NOT + // regressed — the #1994 ByokGlobalScopeOAuthSurvives guard holds). + // WORKSPACE-origin creds (the user explicitly set them via the canvas + // Secrets tab → NOT in globalKeys) are NEVER stripped here, even when + // they don't match: the user authored them deliberately (JRS kimi + // workspace-key, reno's own oauth). Only the inherited operator-store + // channel is provider-gated. + stripNonMatchingGlobalOriginLLMCreds(envVars, globalKeys, runtime, effectiveModel, availableAuthEnv) return platformLLMEnvResult{ ResolvedMode: res.ResolvedMode, HasUsableLLMCred: hasAnyPlatformManagedLLMKey(envVars), @@ -1028,6 +1062,66 @@ func hasAnyPlatformManagedLLMKey(envVars map[string]string) bool { return false } +// stripNonMatchingGlobalOriginLLMCreds is the byok-branch provider-matched +// credential injection (internal#728 Bug 1). It removes from envVars every +// platform-managed LLM bypass key that: +// +// 1. originated from the operator-controlled global_secrets store +// (present in globalKeys — a workspace_secrets row of the same name +// overrides + clears the flag, so user-authored creds are exempt), AND +// 2. is NOT in the RESOLVED provider's auth_env set. +// +// The motivating regression: #1994 dropped the blanket strip, so a claude-code +// workspace resolving to `minimax` still inherited the tenant-global +// CLAUDE_CODE_OAUTH_TOKEN; the runtime prefers that oauth and routes the +// MiniMax model to api.anthropic.com → error. Keeping only the resolved +// provider's own auth_env keys (minimax: MINIMAX_API_KEY/ANTHROPIC_AUTH_TOKEN/ +// ANTHROPIC_API_KEY — not the oauth) removes the stray oauth while preserving +// anthropic-oauth's CLAUDE_CODE_OAUTH_TOKEN for an opus byok workspace. +// +// Fail-OPEN by design: if the provider cannot be derived (empty model / +// unknown runtime / ambiguous) or the registry is unavailable, we strip +// NOTHING — we never strip a credential we cannot prove is non-matching, so a +// derive miss can never fail-close a legitimate byok workspace (mirrors the +// resolver's own default-closed-to-platform contract: the worst case is we +// keep a stray cred, never that we remove the only usable one). The earlier +// internal#711 blanket strip's fail-direction (remove first) was the bug; +// this strip's fail-direction is keep-first. +func stripNonMatchingGlobalOriginLLMCreds(envVars map[string]string, globalKeys map[string]struct{}, runtime, model string, availableAuthEnv []string) { + if len(globalKeys) == 0 { + return // no operator-store-origin keys to consider — nothing to strip. + } + manifest, err := providerRegistry() + if err != nil || manifest == nil { + return // registry unavailable — fail open, strip nothing. + } + provider, dErr := manifest.DeriveProvider(runtime, model, availableAuthEnv) + if dErr != nil { + return // underivable provider — fail open, strip nothing. + } + // The resolved provider's accepted auth-env-var NAMES (case-insensitive + // for parity with isPlatformManagedDirectLLMBypassKey, which upper-cases). + keep := make(map[string]struct{}, len(provider.AuthEnv)) + for _, e := range provider.AuthEnv { + keep[strings.ToUpper(strings.TrimSpace(e))] = struct{}{} + } + for key := range globalKeys { + upper := strings.ToUpper(strings.TrimSpace(key)) + if _, isBypass := platformManagedDirectLLMBypassKeys[upper]; !isBypass { + continue // not an LLM bypass cred (e.g. a non-LLM operator secret) — leave it. + } + if _, matches := keep[upper]; matches { + continue // matches the resolved provider's auth_env — this is what byok runs on. + } + // Global-origin LLM bypass cred that does NOT match the resolved + // provider — the stray that a greedy runtime would mis-prefer. Strip. + if _, present := envVars[key]; present { + log.Printf("workspace_provision: byok provider-matched strip — removing global-origin LLM cred %s (resolved provider=%s does not accept it)", key, provider.Name) + delete(envVars, key) + } + } +} + func runtimeUsesAnthropicNativeProxy(runtime string) bool { return strings.EqualFold(strings.TrimSpace(runtime), "claude-code") } diff --git a/workspace-server/internal/handlers/workspace_provision_shared.go b/workspace-server/internal/handlers/workspace_provision_shared.go index aac432384..a3ae0cbf0 100644 --- a/workspace-server/internal/handlers/workspace_provision_shared.go +++ b/workspace-server/internal/handlers/workspace_provision_shared.go @@ -195,9 +195,16 @@ func (h *WorkspaceHandler) prepareProvisionContext( applyAgentGitHTTPCreds(envVars, payload.Role) // molecule-core#1994: per-workspace LLM billing-mode resolution + env wiring. // On platform_managed it forces the CP proxy usage token; on byok/disabled - // it leaves the tenant's own creds (global OR workspace scope) untouched and - // reports whether a usable LLM credential is present. - llmRes := applyPlatformManagedLLMEnv(ctx, envVars, workspaceID, payload.Runtime, payload.Model) + // it keeps the tenant's own provider-MATCHING creds (global OR workspace + // scope) and reports whether a usable LLM credential is present. + // + // internal#728 Bug 1: globalSecretKeys (loadWorkspaceSecrets provenance) + // lets the byok branch strip ONLY operator-store-origin LLM creds that do + // NOT match the resolved provider's auth_env — so a non-anthropic-oauth + // claude-code workspace no longer inherits the stray tenant-global + // CLAUDE_CODE_OAUTH_TOKEN the runtime would greedily prefer. User-authored + // workspace_secrets (provenance flag cleared) are exempt. + llmRes := applyPlatformManagedLLMEnv(ctx, envVars, workspaceID, payload.Runtime, payload.Model, globalSecretKeys) // Fail closed for a BYOK workspace with no usable LLM credential at ANY // scope: do NOT start it credential-less. Mirror the "model+provider+ // credential REQUIRED at create" spirit with an actionable error surfaced diff --git a/workspace-server/internal/handlers/workspace_provision_shared_test.go b/workspace-server/internal/handlers/workspace_provision_shared_test.go index 3074afd5e..46fb55dad 100644 --- a/workspace-server/internal/handlers/workspace_provision_shared_test.go +++ b/workspace-server/internal/handlers/workspace_provision_shared_test.go @@ -968,7 +968,7 @@ func TestApplyPlatformManagedLLMEnv_NonClaudeRuntimeDefaultsOpenAIProxyWhenNoWor t.Setenv("MOLECULE_LLM_DEFAULT_MODEL", "moonshot/kimi-k2.6") envVars := map[string]string{} - applyPlatformManagedLLMEnv(context.Background(), envVars, "", "codex", "") + applyPlatformManagedLLMEnv(context.Background(), envVars, "", "codex", "", nil) applyRuntimeModelEnv(envVars, "codex", "") if got := envVars["OPENAI_BASE_URL"]; got != "https://api.example.test/api/v1/internal/llm/openai/v1" { @@ -998,7 +998,7 @@ func TestApplyPlatformManagedLLMEnv_StripsWorkspaceOpenAIKeyForClaudeCode(t *tes "OPENAI_BASE_URL": "https://api.openai.com/v1", "MODEL": "openai/gpt-5.5", } - applyPlatformManagedLLMEnv(context.Background(), envVars, "", "claude-code", "") + applyPlatformManagedLLMEnv(context.Background(), envVars, "", "claude-code", "", nil) if _, ok := envVars["OPENAI_API_KEY"]; ok { t.Fatalf("OPENAI_API_KEY should be stripped for claude-code platform-managed mode") @@ -1024,7 +1024,7 @@ func TestApplyPlatformManagedLLMEnv_ClaudeCodeUsesAnthropicProxyOverOAuth(t *tes "CLAUDE_CODE_OAUTH_TOKEN": "user-oauth-token", "MODEL": "sonnet", } - applyPlatformManagedLLMEnv(context.Background(), envVars, "", "claude-code", "") + applyPlatformManagedLLMEnv(context.Background(), envVars, "", "claude-code", "", nil) if _, ok := envVars["CLAUDE_CODE_OAUTH_TOKEN"]; ok { t.Fatalf("CLAUDE_CODE_OAUTH_TOKEN should be stripped in platform-managed mode") @@ -1047,7 +1047,7 @@ func TestApplyPlatformManagedLLMEnv_ClaudeCodeInjectsAnthropicProxyWhenNoWorkspa t.Setenv("MOLECULE_LLM_USAGE_TOKEN", "tenant-admin-token") envVars := map[string]string{} - applyPlatformManagedLLMEnv(context.Background(), envVars, "", "claude-code", "minimax/MiniMax-M2.7") + applyPlatformManagedLLMEnv(context.Background(), envVars, "", "claude-code", "minimax/MiniMax-M2.7", nil) if got := envVars["ANTHROPIC_BASE_URL"]; got != "https://api.example.test/api/v1/internal/llm/anthropic/v1" { t.Fatalf("ANTHROPIC_BASE_URL = %q", got) @@ -1070,7 +1070,7 @@ func TestApplyPlatformManagedLLMEnv_ClaudeCodeStripsVendorBYOK(t *testing.T) { "MINIMAX_API_KEY": "user-minimax-key", "MODEL": "MiniMax-M2.7", } - applyPlatformManagedLLMEnv(context.Background(), envVars, "", "claude-code", "") + applyPlatformManagedLLMEnv(context.Background(), envVars, "", "claude-code", "", nil) if _, ok := envVars["MINIMAX_API_KEY"]; ok { t.Fatalf("MINIMAX_API_KEY should be stripped in platform-managed mode") @@ -1104,7 +1104,7 @@ func TestApplyPlatformManagedLLMEnv_NoopsOutsidePlatformManaged(t *testing.T) { t.Setenv("MOLECULE_LLM_USAGE_TOKEN", "tenant-admin-token") envVars := map[string]string{} - res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "kimi-for-coding") + res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "kimi-for-coding", nil) if res.ResolvedMode != LLMBillingModeBYOK { t.Fatalf("resolved mode = %q, want byok (derived from non-platform model)", res.ResolvedMode) @@ -1151,7 +1151,7 @@ func TestApplyPlatformManagedLLMEnv_ClaudeCodeByokKeepsOwnProviderEnv(t *testing "CLAUDE_CODE_OAUTH_TOKEN": "user-oauth-token", "MODEL": "sonnet", } - applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "") + applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "", nil) // 1. OAuth token intact — not stripped. if got := envVars["CLAUDE_CODE_OAUTH_TOKEN"]; got != "user-oauth-token" { @@ -1213,7 +1213,7 @@ func TestApplyPlatformManagedLLMEnv_ByokGlobalScopeOAuthSurvivesAndRunsDirect(t "MODEL": "opus", } - res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "") + res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "", nil) // 1. The tenant's own global-scope oauth SURVIVES — byok runs on it. if envVars["CLAUDE_CODE_OAUTH_TOKEN"] != "TENANT-OWN-GLOBAL-OAUTH" { @@ -1266,7 +1266,7 @@ func TestApplyPlatformManagedLLMEnv_DERIVED_PlatformModelKeepsPlatformCreds(t *t t.Setenv("MOLECULE_LLM_USAGE_TOKEN", "tenant-admin-token") envVars := map[string]string{} - res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "anthropic/claude-opus-4-7") + res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "anthropic/claude-opus-4-7", nil) if res.ResolvedMode != LLMBillingModePlatformManaged { t.Fatalf("platform-derived model must resolve platform_managed, got %q (source=%s)", res.ResolvedMode, res.Source) @@ -1308,7 +1308,7 @@ func TestApplyPlatformManagedLLMEnv_DERIVED_ByokNoCredentialFailsClosed(t *testi // No LLM credential at all — neither global nor workspace scope. envVars := map[string]string{} - res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "kimi-for-coding") + res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "kimi-for-coding", nil) // 1. DERIVED byok (NOT the old platform_managed default). if res.ResolvedMode != LLMBillingModeBYOK { @@ -1346,7 +1346,7 @@ func TestApplyPlatformManagedLLMEnv_DERIVED_UnsetModelPlatformDefault(t *testing t.Setenv("MOLECULE_LLM_USAGE_TOKEN", "tenant-admin-token") envVars := map[string]string{} - res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "") + res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "", nil) if res.ResolvedMode != LLMBillingModePlatformManaged { t.Fatalf("unset model must default platform_managed, got %q (source=%s)", res.ResolvedMode, res.Source) @@ -1385,7 +1385,7 @@ func TestApplyPlatformManagedLLMEnv_ByokKeepsWorkspaceOwnOAuth(t *testing.T) { "MODEL": "opus", } - res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "") + res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "", nil) if got := envVars["CLAUDE_CODE_OAUTH_TOKEN"]; got != "CUSTOMER-OWN-OAUTH-TOKEN" { t.Fatalf("CLAUDE_CODE_OAUTH_TOKEN = %q, want the workspace's own token left intact", got) @@ -1425,7 +1425,7 @@ func TestApplyPlatformManagedLLMEnv_DisabledKeepsTenantGlobalNoProxy(t *testing. "CLAUDE_CODE_OAUTH_TOKEN": "TENANT-OWN-GLOBAL-OAUTH", } - res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "") + res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "", nil) // The tenant's own global cred survives (not stripped). if envVars["CLAUDE_CODE_OAUTH_TOKEN"] != "TENANT-OWN-GLOBAL-OAUTH" { @@ -1466,7 +1466,7 @@ func TestApplyPlatformManagedLLMEnv_PlatformManagedStillReceivesGlobalCreds(t *t "MODEL": "opus", } - res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "") + res := applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "", nil) // Platform-managed routes through the CP proxy: OAuth stripped, proxy creds forced. if _, ok := envVars["CLAUDE_CODE_OAUTH_TOKEN"]; ok { @@ -1507,7 +1507,7 @@ func TestApplyPlatformManagedLLMEnv_PlatformManagedStillEmitsResolvedMode(t *tes "CLAUDE_CODE_OAUTH_TOKEN": "user-oauth-token", "MODEL": "sonnet", } - applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "") + applyPlatformManagedLLMEnv(context.Background(), envVars, wsID, "claude-code", "", nil) // OAuth stripped, proxy forced — unchanged platform_managed contract. if _, ok := envVars["CLAUDE_CODE_OAUTH_TOKEN"]; ok { -- 2.52.0