From fcd88db315ea78c1b03108a1c9ccfd18ae7c81b7 Mon Sep 17 00:00:00 2001 From: hongming Date: Thu, 4 Jun 2026 01:03:24 +0000 Subject: [PATCH 1/9] feat(providers): mirror google-adk platform provider + derive required_env from IsPlatform (proper SSOT, task #65) --- .../internal/providers/providers.yaml | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/workspace-server/internal/providers/providers.yaml b/workspace-server/internal/providers/providers.yaml index 5035bfa3f..bd02487c9 100644 --- a/workspace-server/internal/providers/providers.yaml +++ b/workspace-server/internal/providers/providers.yaml @@ -844,11 +844,24 @@ runtimes: # this runtimes entry declares the selectable model set. google-adk: providers: - # Keyless Vertex (org-compliant default): Gemini via Vertex AI + ADC/WIF. - - name: vertex + # Platform-managed (keyless, metered) Gemini via the Molecule LLM proxy -> + # Vertex AI (server-side WIF mint; NO credential on the tenant box — the + # keyless-Vertex leak fix). Resolves to the closed `platform` provider -> + # IsPlatform=true -> platform_managed billing + required_env=[] (derived). + # The org-compliant default. The runtime translates the `platform:` select + # id to the bare wire id the proxy routes to Vertex. + - name: platform models: - - vertex:gemini-2.5-pro - # API-key BYOK arm: AI Studio GEMINI_API_KEY/GOOGLE_API_KEY. + - platform:gemini-2.5-pro + - platform:gemini-2.5-flash + # API-key BYOK arm: AI Studio (the tenant's OWN GOOGLE_API_KEY). - name: google models: - - gemini-2.5-pro \ No newline at end of file + - gemini-2.5-pro + - gemini-2.5-flash + # DEPRECATED transitional: vertex: ids stay registered until templates + # move to platform: (superseded by the platform arm above). Remove in a + # cleanup once no template references vertex:gemini-*. + - name: vertex + models: + - vertex:gemini-2.5-pro \ No newline at end of file -- 2.52.0 From eec4dc6d49d4b206c2cf0a6b84b748d736f6e249 Mon Sep 17 00:00:00 2001 From: hongming Date: Thu, 4 Jun 2026 01:03:26 +0000 Subject: [PATCH 2/9] feat(providers): mirror google-adk platform provider + derive required_env from IsPlatform (proper SSOT, task #65) --- workspace-server/internal/providers/gen/registry_gen.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/workspace-server/internal/providers/gen/registry_gen.go b/workspace-server/internal/providers/gen/registry_gen.go index 51c96dee6..be99cfb0e 100644 --- a/workspace-server/internal/providers/gen/registry_gen.go +++ b/workspace-server/internal/providers/gen/registry_gen.go @@ -16,7 +16,7 @@ const SchemaVersion = 1 // Fingerprint is a stable content hash of the generated projection (schema // version + provider catalog + runtime native sets). It changes iff the // registry DATA changes (comment-only YAML edits do not churn it). -const Fingerprint = "8f733b112695b926" +const Fingerprint = "9401318bb76fe4fc" // GenProvider is the generated projection of one provider catalog entry — // the subset a downstream consumer needs to derive + display a provider. @@ -89,8 +89,9 @@ var Runtimes = map[string][]GenRuntimeRef{ {Name: "platform", Models: []string{"openai/gpt-5.4", "openai/gpt-5.4-mini"}}, }, "google-adk": { + {Name: "platform", Models: []string{"platform:gemini-2.5-pro", "platform:gemini-2.5-flash"}}, + {Name: "google", Models: []string{"gemini-2.5-pro", "gemini-2.5-flash"}}, {Name: "vertex", Models: []string{"vertex:gemini-2.5-pro"}}, - {Name: "google", Models: []string{"gemini-2.5-pro"}}, }, "hermes": { {Name: "kimi-coding", Models: []string{"kimi-coding/kimi-k2"}}, -- 2.52.0 From 73dce44cd499eda3ca536295efd4cf9a87de69da Mon Sep 17 00:00:00 2001 From: hongming Date: Thu, 4 Jun 2026 01:03:27 +0000 Subject: [PATCH 3/9] feat(providers): mirror google-adk platform provider + derive required_env from IsPlatform (proper SSOT, task #65) --- .../internal/handlers/templates_registry.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/workspace-server/internal/handlers/templates_registry.go b/workspace-server/internal/handlers/templates_registry.go index c9a431b63..fa62c3c98 100644 --- a/workspace-server/internal/handlers/templates_registry.go +++ b/workspace-server/internal/handlers/templates_registry.go @@ -44,6 +44,20 @@ func billingModeForRegistryProvider(p providers.Provider) string { return LLMBillingModeBYOK } +// requiredEnvForRegistryProvider derives the env vars the USER must supply for +// a model owned by the resolved provider — the proper-SSOT single fact behind +// the canvas "Missing API Keys" preflight (task #65). The closed platform +// provider injects credentials server-side (the metered proxy) -> nothing +// required; BYOK providers require their auth_env. Derived from IsPlatform + +// AuthEnv so a template can no longer hand-author a required_env that drifts +// from the registry's serving classification. +func requiredEnvForRegistryProvider(p providers.Provider) []string { + if p.IsPlatform() { + return nil + } + return p.AuthEnv +} + // enrichFromRegistry populates the registry-served fields on a templateSummary // when its runtime is known to the provider registry. It is a no-op (leaves // RegistryBacked=false and the registry slices nil) for a runtime the registry @@ -98,6 +112,7 @@ func enrichFromRegistry(summary *templateSummary, runtime string) { if derived, derr := m.DeriveProvider(runtime, id, nil); derr == nil { ms.Provider = derived.Name ms.BillingMode = billingModeForRegistryProvider(derived) + ms.RequiredEnv = requiredEnvForRegistryProvider(derived) } // If DeriveProvider errors (ambiguous/overlap — a manifest defect the // loader's tests pin against), still serve the id without a provider -- 2.52.0 From 36f9ed84a0753b98b669f54ff2abf51e3e2515c6 Mon Sep 17 00:00:00 2001 From: hongming Date: Thu, 4 Jun 2026 01:03:30 +0000 Subject: [PATCH 4/9] feat(providers): mirror google-adk platform provider + derive required_env from IsPlatform (proper SSOT, task #65) --- .../providers/google_adk_platform_test.go | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 workspace-server/internal/providers/google_adk_platform_test.go diff --git a/workspace-server/internal/providers/google_adk_platform_test.go b/workspace-server/internal/providers/google_adk_platform_test.go new file mode 100644 index 000000000..54906d4c4 --- /dev/null +++ b/workspace-server/internal/providers/google_adk_platform_test.go @@ -0,0 +1,42 @@ +package providers + +import "testing" + +// Proper-SSOT (task #65): google-adk keyless Gemini resolves to the closed +// platform provider -> IsPlatform=true; BYOK AI Studio -> google. The +// platform: select ids are registered so workspace-create accepts them +// (was 422 UNREGISTERED_MODEL_FOR_RUNTIME). +func TestGoogleADK_PlatformGeminiResolvesToPlatform(t *testing.T) { + m, err := LoadManifest() + if err != nil { + t.Fatal(err) + } + for _, id := range []string{"platform:gemini-2.5-pro", "platform:gemini-2.5-flash"} { + p, err := m.DeriveProvider("google-adk", id, nil) + if err != nil { + t.Fatalf("%s: %v", id, err) + } + if p.Name != PlatformProviderName || !p.IsPlatform() { + t.Errorf("%s -> %q IsPlatform=%v; want platform", id, p.Name, p.IsPlatform()) + } + } + p, err := m.DeriveProvider("google-adk", "gemini-2.5-pro", nil) + if err != nil { + t.Fatal(err) + } + if p.IsPlatform() || p.Name != "google" { + t.Errorf("gemini-2.5-pro -> %q IsPlatform=%v; want google byok", p.Name, p.IsPlatform()) + } + models, _ := m.ModelsForRuntime("google-adk") + want := map[string]bool{"platform:gemini-2.5-pro": false, "platform:gemini-2.5-flash": false} + for _, id := range models { + if _, ok := want[id]; ok { + want[id] = true + } + } + for id, ok := range want { + if !ok { + t.Errorf("%s not registered for google-adk — create would 422", id) + } + } +} -- 2.52.0 From fa00d4f01822632c960e05b069cc03a1ac2a4133 Mon Sep 17 00:00:00 2001 From: hongming Date: Thu, 4 Jun 2026 01:03:31 +0000 Subject: [PATCH 5/9] feat(providers): mirror google-adk platform provider + derive required_env from IsPlatform (proper SSOT, task #65) --- .../handlers/required_env_derive_test.go | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 workspace-server/internal/handlers/required_env_derive_test.go diff --git a/workspace-server/internal/handlers/required_env_derive_test.go b/workspace-server/internal/handlers/required_env_derive_test.go new file mode 100644 index 000000000..6c84d0365 --- /dev/null +++ b/workspace-server/internal/handlers/required_env_derive_test.go @@ -0,0 +1,21 @@ +package handlers + +import ( + "testing" + + "git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/providers" +) + +// Proper-SSOT (task #65): required_env is DERIVED from the resolved provider's +// serving classification (IsPlatform), not hand-authored — platform injects +// creds server-side (none required), BYOK requires its auth_env. +func TestRequiredEnvForRegistryProvider(t *testing.T) { + if got := requiredEnvForRegistryProvider(providers.Provider{Name: providers.PlatformProviderName}); got != nil { + t.Errorf("platform provider requiredEnv = %v; want nil (creds injected server-side)", got) + } + byok := providers.Provider{Name: "google", AuthEnv: []string{"GEMINI_API_KEY", "GOOGLE_API_KEY"}} + got := requiredEnvForRegistryProvider(byok) + if len(got) != 2 || got[0] != "GEMINI_API_KEY" { + t.Errorf("byok requiredEnv = %v; want its auth_env", got) + } +} -- 2.52.0 From 227abeb4320c2628ca60323b3779233d7dd0790d Mon Sep 17 00:00:00 2001 From: hongming Date: Thu, 4 Jun 2026 01:13:40 +0000 Subject: [PATCH 6/9] chore(providers): re-pin canonical providers.yaml sha256 for the google-adk platform-arm sync (cp#511) --- workspace-server/internal/providers/sync_canonical_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace-server/internal/providers/sync_canonical_test.go b/workspace-server/internal/providers/sync_canonical_test.go index 56acff2e0..08c4d05cf 100644 --- a/workspace-server/internal/providers/sync_canonical_test.go +++ b/workspace-server/internal/providers/sync_canonical_test.go @@ -29,7 +29,7 @@ import ( // canonicalProvidersYAMLSHA256 is the sha256 of the canonical providers.yaml as // synced from molecule-controlplane. Bumped deliberately on each re-sync (see // file doc). Cross-checked live by the sync-providers-yaml CI workflow. -const canonicalProvidersYAMLSHA256 = "dec73199e26cee2d395a0acece99771618d3879dc5ca724ba57cb5b38079c6ce" +const canonicalProvidersYAMLSHA256 = "cb2b98cdc887776fd8b982b8ec89e92b73a7fdbff872082265e44077fb900b63" func TestSyncedYAMLMatchesCanonicalSHA(t *testing.T) { sum := sha256.Sum256(embeddedYAML) -- 2.52.0 From f1c86e188e3d5f9198d065aaabf601a9c87cc862 Mon Sep 17 00:00:00 2001 From: hongming Date: Thu, 4 Jun 2026 01:16:19 +0000 Subject: [PATCH 7/9] fix(providers): byte-sync core providers.yaml to controlplane canonical (cp#511 google-adk platform arm) --- .../internal/providers/providers.yaml | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/workspace-server/internal/providers/providers.yaml b/workspace-server/internal/providers/providers.yaml index bd02487c9..1514ab5de 100644 --- a/workspace-server/internal/providers/providers.yaml +++ b/workspace-server/internal/providers/providers.yaml @@ -292,7 +292,7 @@ providers: # PR-1 simplification when only claude-code referenced platform. base_url_template: "https://api.moleculesai.app/api/v1/internal/llm/openai/v1" base_url_anthropic: "https://api.moleculesai.app/api/v1/internal/llm/anthropic/v1" - auth_env: [ANTHROPIC_API_KEY, MOLECULE_LLM_USAGE_TOKEN] + auth_env: [MOLECULE_LLM_USAGE_TOKEN] auth_token_env: ANTHROPIC_API_KEY # Adapter routes kimi- / moonshot/ through platform by default. No bare # vendor prefix of its own; it multiplexes other vendors' slugs. Match @@ -412,14 +412,24 @@ providers: model_prefix_match: "^gemini-" model_aliases: [] - # Google Vertex AI — KEYLESS arm (mirrors the anthropic-oauth / anthropic-api - # and openai-subscription / openai-api split: same vendor, distinct auth). - # google-adk serves Gemini via Vertex using Application Default Credentials - # over Workload Identity Federation (AWS EC2 role -> GCP STS -> SA), injected - # by the provisioner (cp#416 + envs.yaml vertex block) as a NON-SECRET - # external_account cred-config at GOOGLE_APPLICATION_CREDENTIALS. No API key. - # Distinct `vertex:` model namespace keeps it unambiguous vs the API-key - # `google` vendor's ^gemini- (TestNoAmbiguousModelMatch). + # Google Vertex AI — served via the Molecule CP LLM proxy (NOT on-box ADC). + # google-adk routes ALL Gemini through the proxy, which mints a Vertex token + # server-side over Workload Identity Federation (AWS -> GCP STS -> SA; see + # internal/vertexauth + llm_proxy.go google/vertex case) and meters usage to + # org credits. The former on-box ADC delivery (a NON-SECRET external_account + # cred-config written to /configs/gcp-adc.json via GOOGLE_APPLICATION_CREDENTIALS, + # injected by the provisioner) was REMOVED: a tenant has EC2 root and could + # have used that credential to call Vertex DIRECTLY, bypassing metering + # (keyless-Vertex billing leak — task #64 / RFC internal#763; provisioner + # force-off + template routes vertex: through the proxy). The `vertex:` + # namespace + this entry remain for proxy routing/billing of Vertex-upstream + # requests, distinct from the API-key `google` vendor's ^gemini- + # (TestNoAmbiguousModelMatch). + # + # NOTE: display_name ("keyless ADC") and auth_env (GOOGLE_APPLICATION_CREDENTIALS) + # are now VESTIGIAL — no consumer reads auth_env post-leak-fix, but it must stay + # non-empty (providers.go validate). Left as-is to keep this a comment-only, + # regen-free change; retiring them is a registry-regen follow-up. - name: vertex display_name: "Google Vertex AI (keyless ADC)" vendor_logo: "google" -- 2.52.0 From c81ac8f84961fb0a45947fbfe26d635f027987bd Mon Sep 17 00:00:00 2001 From: hongming Date: Thu, 4 Jun 2026 01:16:20 +0000 Subject: [PATCH 8/9] chore(gen): regenerate registry from byte-synced providers.yaml --- workspace-server/internal/providers/gen/registry_gen.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workspace-server/internal/providers/gen/registry_gen.go b/workspace-server/internal/providers/gen/registry_gen.go index be99cfb0e..f4501f13a 100644 --- a/workspace-server/internal/providers/gen/registry_gen.go +++ b/workspace-server/internal/providers/gen/registry_gen.go @@ -16,7 +16,7 @@ const SchemaVersion = 1 // Fingerprint is a stable content hash of the generated projection (schema // version + provider catalog + runtime native sets). It changes iff the // registry DATA changes (comment-only YAML edits do not churn it). -const Fingerprint = "9401318bb76fe4fc" +const Fingerprint = "a491f5ff8a17ef59" // GenProvider is the generated projection of one provider catalog entry — // the subset a downstream consumer needs to derive + display a provider. @@ -50,7 +50,7 @@ var Providers = []GenProvider{ {Name: "openai-api", DisplayName: "OpenAI API", Protocol: "openai", AuthMode: "anthropic_api", AuthEnv: []string{"OPENAI_API_KEY"}, ModelPrefixMatch: "^openai-api[:/]", IsPlatform: false, UpstreamVendor: "openai"}, {Name: "moonshot", DisplayName: "Moonshot (Kimi)", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"MOONSHOT_API_KEY", "KIMI_API_KEY"}, ModelPrefixMatch: "^moonshot[:/-]", IsPlatform: false, UpstreamVendor: "moonshot"}, {Name: "minimax", DisplayName: "MiniMax", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"MINIMAX_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"}, ModelPrefixMatch: "(?i)^minimax-m", IsPlatform: false, UpstreamVendor: "minimax"}, - {Name: "platform", DisplayName: "Platform", Protocol: "anthropic", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"}, ModelPrefixMatch: "^platform/", IsPlatform: true}, + {Name: "platform", DisplayName: "Platform", Protocol: "anthropic", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"MOLECULE_LLM_USAGE_TOKEN"}, ModelPrefixMatch: "^platform/", IsPlatform: true}, {Name: "xiaomi-mimo", DisplayName: "Xiaomi MiMo", Protocol: "anthropic", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"}, ModelPrefixMatch: "^mimo-", IsPlatform: false}, {Name: "zai", DisplayName: "Z.ai (GLM)", Protocol: "anthropic", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"GLM_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"}, ModelPrefixMatch: "(?i)^glm-", IsPlatform: false}, {Name: "kimi-coding", DisplayName: "Moonshot Kimi (coding-tuned)", Protocol: "anthropic", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"KIMI_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"}, ModelPrefixMatch: "^kimi-", IsPlatform: false}, -- 2.52.0 From 86760f7a3e4d941c528edc5bdb0d8e132716afaf Mon Sep 17 00:00:00 2001 From: hongming Date: Thu, 4 Jun 2026 01:16:21 +0000 Subject: [PATCH 9/9] chore: re-pin canonical sha256 to byte-synced controlplane value --- workspace-server/internal/providers/sync_canonical_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace-server/internal/providers/sync_canonical_test.go b/workspace-server/internal/providers/sync_canonical_test.go index 08c4d05cf..463ad3003 100644 --- a/workspace-server/internal/providers/sync_canonical_test.go +++ b/workspace-server/internal/providers/sync_canonical_test.go @@ -29,7 +29,7 @@ import ( // canonicalProvidersYAMLSHA256 is the sha256 of the canonical providers.yaml as // synced from molecule-controlplane. Bumped deliberately on each re-sync (see // file doc). Cross-checked live by the sync-providers-yaml CI workflow. -const canonicalProvidersYAMLSHA256 = "cb2b98cdc887776fd8b982b8ec89e92b73a7fdbff872082265e44077fb900b63" +const canonicalProvidersYAMLSHA256 = "021ae082c2bbbbb61c406cae03205ac6b7fff160ae5976cfc64de3de676d02b2" func TestSyncedYAMLMatchesCanonicalSHA(t *testing.T) { sum := sha256.Sum256(embeddedYAML) -- 2.52.0