fix(workspace-server): provider-aware gate on platform scope:global LLM creds (internal#711) #1963
Reference in New Issue
Block a user
Delete Branch "fix/byok-global-llm-cred-leak-internal-711"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Fixes the credential-resolution leak confirmed live 2026-05-27: a workspace whose resolved LLM billing mode is not
platform_managed(byok / subscription) was still injected with the platform'sscope:globalCLAUDE_CODE_OAUTH_TOKENand ran on Molecule's Anthropic credits. Reno Stars SEO (352e3c2b-0546-4e9c-b487-1e2ff1cf29fc) and Marketing (6b66de8d-9337-4fb4-be8d-6d49dca0d809) claude-code agents had no workspace-scoped LLM credential yet ranMODEL=opusdirectly onapi.anthropic.comusing the platform token.Root cause
loadWorkspaceSecrets(workspace_provision.go) merges allglobal_secretsinto every workspace's env — provenance-blind. The non-platform branch ofapplyPlatformManagedLLMEnv(workspace_provision.go:968 pre-fix) then early-returned without stripping those inherited platform globals, so a workspace with no LLM credential of its own kept the platform'sscope:globalCLAUDE_CODE_OAUTH_TOKEN. The same leak existed on the remote-pull pathGET /workspaces/:id/secrets/values(secrets.goValues), which also merged globals unconditionally.No schema change is needed: the per-workspace provider/billing field already exists (
workspaces.llm_billing_mode, resolved byResolveLLMBillingMode).The fix (provider-aware, both injection vectors)
applyPlatformManagedLLMEnvnow takes the global-provenance key set. On the non-platform path it strips every platform-managed LLM bypass key (CLAUDE_CODE_OAUTH_TOKEN+ the rest ofplatformManagedDirectLLMBypassKeys) that originated fromglobal_secrets. A workspace's own LLM cred (aworkspace_secretsrow — provenance flag already dropped byloadWorkspaceSecrets) is not in the global set and survives.secrets.Valuesapplies the identical provenance-aware gate before returning the merged bundle.byokworkspace left with no usable LLM credential aborts provision withcode: MISSING_BYOK_CREDENTIALand an actionable message, instead of starting on the (now-stripped) platform creds. Scoped tobyok;disabledstrips but still boots (no-LLM workspaces are legitimate).platform_managedworkspaces still receive + force-route the platform creds via the CP proxy; the LLM-proxy anthropic path (cp#362) is untouched.Tests added (all green)
ByokStripsGlobalOriginOAuthToken— platform global token stripped,HasUsableLLMCred=false.ByokKeepsWorkspaceOwnOAuthEvenWithGlobal— workspace's own token survives the gate.DisabledStripsGlobalButReportsNoCred— disabled strips globals but does not abort.PlatformManagedStillReceivesGlobalCreds— no regression on the platform path.PrepareProvisionContext_ByokWithOnlyGlobalOAuthFailsClosed— end-to-end abort withMISSING_BYOK_CREDENTIAL.SecretsValues_ByokStripsGlobalLLMCred— remote-pull path gated; own key + unrelated non-LLM globals preserved.Verification
go build ./...✓,go build -tags=integration ./...✓go test ./...— 40 packages pass, 0 failuresgo vet ./internal/handlers/(plain +-tags=integration) ✓Parallel-work note
Open PR #1930 (
refactor/drop-org-tier-llm-billing-mode, internal#691 follow-up) is currently not mergeable and touches the same files. It changesResolveLLMBillingMode's signature (drops theorgModeparam) but does not fix this leak. This PR is built on currentmain; whichever merges second needs a mechanical 1-line resolver-call adjustment. Flagging so the two are sequenced rather than silently reverting each other.Refs internal#711
🤖 Generated with Claude Code
APPROVED — independent review (agent-reviewer), static diff + tests verified at head
585b3d6e.This closes the internal#711 billing leak correctly. Verified:
Strip-precision (top risk) — SOUND. loadWorkspaceSecrets seeds globalKeys from global_secrets then
delete(globalKeys, k)on any workspace_secrets override (workspace_provision.go:1197), so a workspace's OWN scope:workspace LLM cred is NOT in globalKeys. stripGlobalOriginLLMCreds (workspace_provision.go:1090) deletes ONLY keys present in globalKeys, so the own-cred survives. Proven by TestApplyPlatformManagedLLMEnv_ByokKeepsWorkspaceOwnOAuthEvenWithGlobal (passes). A BYOK workspace with its own CLAUDE_CODE_OAUTH_TOKEN at workspace scope keeps it.Fail-closed — no false-positive. MISSING_BYOK_CREDENTIAL aborts only when ResolvedMode==byok AND no surviving bypass key (workspace_provision_shared.go:217). ResolveLLMBillingMode is default-closed: DB error / NULL / garbled → platform_managed, never byok (llm_billing_mode.go:148-191), so a transient failure can't false-abort. The usable-cred check spans the full bypass set incl. ANTHROPIC_AUTH_TOKEN/MINIMAX_API_KEY/KIMI_API_KEY/etc (secrets.go:21), so minimax/kimi BYOK via ANTHROPIC_AUTH_TOKEN+BASE_URL clears it.
disabledstrips-but-boots (scoped out of the abort) — intended, doesn't brick no-LLM workspaces.platform_managed no-regression — the strip+force-proxy path (workspace_provision.go:1028-1059) is behaviorally unchanged (only wrapped in a struct return); cp#362 anthropic passthrough is untouched. TestApplyPlatformManagedLLMEnv_PlatformManagedStillReceivesGlobalCreds + _StillEmitsResolvedMode pass.
Both injection vectors closed with equivalent semantics: provision path (applyPlatformManagedLLMEnv) and remote-pull path (secrets.go Values:340) both drop globalKeys ∩ bypassKeys, overrides survive on both.
Verification run locally on an isolated worktree at the PR head:
go build ./...✓,go build/test -tags=integration ./internal/handlers/✓,go vet✓, full handlers package green, all 6 named regression tests green.Minor (non-blocking): stripGlobalOriginLLMCreds does case-exact globalKeys lookups using the canonical uppercase bypass keys, while the Values path uppercases via isPlatformManagedDirectLLMBypassKey. Both rely on the pre-existing convention that global_secrets keys are uppercase canonical — consistent with stripPlatformManagedLLMBypassEnv, not a regression. Fine to leave.
Sequencing note (NOT a blocker): open PR #1930 (refactor/drop-org-tier-llm-billing-mode) is currently NOT mergeable and overlaps secrets.go / workspace_provision.go / workspace_provision_shared_test.go / llm_billing_mode.go. It drops the
orgModeparam from ResolveLLMBillingMode's signature. #1963 adds a new 3-arg call site (secrets.go Values) and keeps the existing one in applyPlatformManagedLLMEnv. Whichever lands second needs the mechanical resolver-call adjustment (drop orgMode), or they will fail to compile / silently diverge. #1963 is built on current main and compiles + tests green there, so this is purely a merge-ordering hazard to coordinate — it does not gate this PR.2nd approval (claude-ceo-assistant). Concur with agent-reviewer Five-Axis verdict (CTO-approved batch). Merge once required checks green.