fix(canvas): platform-managed provider needs no user credential (#2245) #2246

Merged
claude-ceo-assistant merged 3 commits from fix/2245-platform-managed-no-cred into main 2026-06-05 00:33:18 +00:00
3 changed files with 199 additions and 15 deletions
+34 -15
View File
@@ -10,6 +10,7 @@ import {
buildProviderCatalog,
buildProviderCatalogFromRegistry,
findProviderForModel,
isPlatformManagedProvider,
type SelectorModel,
type SelectorValue,
type RegistryProvider,
@@ -290,7 +291,15 @@ export function CreateWorkspaceButton() {
setError("Model is required");
return;
}
if (!isExternal && selectedLLMProvider?.envVars.length && !llmSecret.trim()) {
// Platform-managed providers need NO user credential — the platform injects
// its own usage token (MOLECULE_LLM_USAGE_TOKEN = tenant admin_token) at
// provision time. Only BYOK providers require a user-supplied key. (#2245)
if (
!isExternal &&
!isPlatformManagedProvider(selectedLLMProvider) &&
selectedLLMProvider?.envVars.length &&
!llmSecret.trim()
) {
setError("Provider credential is required");
return;
}
@@ -325,7 +334,11 @@ export function CreateWorkspaceButton() {
? {
model: llmSelection.model.trim(),
llm_provider: nativeProvider.vendor,
...(nativeProvider.envVars.length > 0
// Only BYOK providers carry a user secret. For platform-managed
// the token is provisioner-injected; sending an (empty) secret
// here would clobber it — so omit it entirely. (#2245)
...(nativeProvider.envVars.length > 0 &&
!isPlatformManagedProvider(nativeProvider)
? { secrets: { [nativeProvider.envVars[0]]: llmSecret.trim() } }
: {}),
}
@@ -521,20 +534,26 @@ export function CreateWorkspaceButton() {
idPrefix="create-workspace-llm"
variant="stack"
/>
{selectedLLMProvider.envVars.length > 0 && (
<div>
<label htmlFor="llm-secret-input" className="text-[11px] text-ink-mid block mb-1">
{selectedLLMProvider.envVars[0]}
</label>
<input
id="llm-secret-input"
type="password"
value={llmSecret}
onChange={(e) => setLLMSecret(e.target.value)}
autoComplete="off"
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink placeholder-ink-soft focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors font-mono"
/>
{isPlatformManagedProvider(selectedLLMProvider) ? (
<div className="text-[11px] text-ink-soft">
Platform-managed no API key required.
</div>
) : (
selectedLLMProvider.envVars.length > 0 && (
<div>
<label htmlFor="llm-secret-input" className="text-[11px] text-ink-mid block mb-1">
{selectedLLMProvider.envVars[0]}
</label>
<input
id="llm-secret-input"
type="password"
value={llmSecret}
onChange={(e) => setLLMSecret(e.target.value)}
autoComplete="off"
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink placeholder-ink-soft focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors font-mono"
/>
</div>
)
)}
</div>
)}
@@ -55,6 +55,21 @@ export interface ProviderEntry {
billingMode?: "platform_managed" | "byok";
}
/** A provider is "platform-managed" when the Molecule platform proxies the LLM
* call and injects its own usage credential — the tenant admin_token, surfaced
* to the workspace as MOLECULE_LLM_USAGE_TOKEN by the CP provisioner
* (controlplane ec2.go: `MOLECULE_LLM_USAGE_TOKEN="$ADMIN_TOKEN"`). The user
* supplies NO key for these: the credential is internal plumbing, not a user
* input. Detected by vendor==="platform" (the platform proxy provider, which
* declares MOLECULE_LLM_USAGE_TOKEN in its AuthEnv) OR
* billingMode==="platform_managed" (registry-backed, internal#718 P3). BYOK
* providers return false and DO require a user-supplied credential. */
export function isPlatformManagedProvider(
p?: Pick<ProviderEntry, "vendor" | "billingMode"> | null,
): boolean {
return p?.vendor === "platform" || p?.billingMode === "platform_managed";
}
/** RegistryProvider mirrors one entry of GET /templates `registry_providers`
* (workspace-server registryProviderView): the registry's native provider for
* a runtime, with its display label, auth-env NAMES, and billing mode. This is
@@ -2,6 +2,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
import { CreateWorkspaceButton } from "../CreateWorkspaceDialog";
import { isPlatformManagedProvider } from "../ProviderModelSelector";
vi.mock("@/lib/api", () => ({
api: {
@@ -65,6 +66,34 @@ const SAMPLE_TEMPLATES = [
{ id: "moonshot/kimi-k2.6", name: "Kimi K2.6", provider: "platform", required_env: [] },
],
},
// #2245 fixtures. The real registry `platform` provider declares
// MOLECULE_LLM_USAGE_TOKEN in its auth_env — the default mock above masks the
// bug by using required_env:[]. This template gives the platform provider a
// non-empty auth env (matching production) so the credential-suppression
// logic is actually exercised.
{
id: "platform-managed-test",
name: "Platform Managed Test",
runtime: "claude-code",
model: "moonshot/kimi-k2.6",
providers: ["platform", "minimax"],
models: [
{ id: "moonshot/kimi-k2.6", name: "Kimi K2.6", provider: "platform", required_env: ["MOLECULE_LLM_USAGE_TOKEN"] },
{ id: "MiniMax-M2.7", name: "MiniMax M2.7", required_env: ["MINIMAX_API_KEY"] },
],
},
// BYOK-only template (no platform provider) — the credential requirement
// MUST still hold for these (no-regression guard).
{
id: "byok-only-test",
name: "BYOK Only Test",
runtime: "claude-code",
model: "openai/gpt-4o",
providers: ["openai"],
models: [
{ id: "openai/gpt-4o", name: "GPT-4o", required_env: ["OPENAI_API_KEY"] },
],
},
];
beforeEach(() => {
@@ -498,6 +527,25 @@ const REGISTRY_TEMPLATE = {
],
};
// Registry-backed platform provider WITH a non-empty auth_env — this matches
// the PRODUCTION provider view, which ships the raw AuthEnv
// ([MOLECULE_LLM_USAGE_TOKEN]). REGISTRY_TEMPLATE above uses auth_env:[] so it
// never exercises suppression; this one drives the billingMode==="platform_
// managed" branch end-to-end through buildProviderCatalogFromRegistry. (#2245)
const REGISTRY_TEMPLATE_PLATFORM_AUTHENV = {
...REGISTRY_TEMPLATE,
registry_providers: [
{
name: "platform",
display_name: "Platform",
auth_env: ["MOLECULE_LLM_USAGE_TOKEN"],
billing_mode: "platform_managed",
},
{ name: "minimax", display_name: "MiniMax", auth_env: ["MINIMAX_API_KEY"], billing_mode: "byok" },
{ name: "anthropic", display_name: "Anthropic API", auth_env: ["ANTHROPIC_API_KEY"], billing_mode: "byok" },
],
};
describe("CreateWorkspaceDialog — registry-backed provider catalog (RFC#340 Fix C)", () => {
beforeEach(() => {
mockGet.mockImplementation(async (url: string) => {
@@ -574,6 +622,41 @@ describe("CreateWorkspaceDialog — registry-backed provider catalog (RFC#340 Fi
expect(body.llm_provider).toBe("minimax");
expect(body.secrets).toEqual({ MINIMAX_API_KEY: "sk-minimax-test" });
});
it("suppresses the credential for a registry-backed platform provider that declares an auth_env — billingMode path (#2245)", async () => {
// Override the default REGISTRY_TEMPLATE (auth_env:[]) with the production-
// shaped one whose platform provider declares MOLECULE_LLM_USAGE_TOKEN.
mockGet.mockImplementation(async (url: string) => {
if (url === "/templates") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return [REGISTRY_TEMPLATE_PLATFORM_AUTHENV] as any;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return SAMPLE_WORKSPACES as any;
});
await openDialog();
fireEvent.change(screen.getByPlaceholderText("e.g. SEO Agent"), {
target: { value: "Registry Platform Agent" },
});
// Platform is the default bucket; even with a non-empty auth_env the key
// field must NOT render (suppressed via billingMode==="platform_managed").
await waitFor(() => {
const sel = document.querySelector("[data-testid='provider-select']") as HTMLSelectElement;
expect(sel?.value).toBe("registry|platform");
});
expect(screen.getByText("Platform-managed — no API key required.")).toBeTruthy();
expect(document.getElementById("llm-secret-input")).toBeNull();
const createBtn = screen.getAllByRole("button").find((b) => b.textContent === "Create");
fireEvent.click(createBtn!);
await waitFor(() => expect(mockPost).toHaveBeenCalled());
expect(screen.queryByText("Provider credential is required")).toBeNull();
const body = mockPost.mock.calls[0][1] as Record<string, unknown>;
expect(body.llm_provider).toBe("platform");
// The provisioner-injected MOLECULE_LLM_USAGE_TOKEN must NOT be clobbered.
expect(body.secrets).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
@@ -657,3 +740,70 @@ describe("CreateWorkspaceDialog — budget_limit field", () => {
expect(budgetInput.value).toBe("");
});
});
describe("CreateWorkspaceDialog — platform-managed credential suppression (#2245)", () => {
describe("isPlatformManagedProvider", () => {
it("is true for the platform proxy vendor", () => {
expect(isPlatformManagedProvider({ vendor: "platform" })).toBe(true);
});
it("is true for a registry billingMode of platform_managed", () => {
expect(
isPlatformManagedProvider({ vendor: "minimax", billingMode: "platform_managed" }),
).toBe(true);
});
it("is false for a BYOK provider", () => {
expect(isPlatformManagedProvider({ vendor: "anthropic", billingMode: "byok" })).toBe(false);
expect(isPlatformManagedProvider({ vendor: "minimax" })).toBe(false);
});
it("is false for null/undefined", () => {
expect(isPlatformManagedProvider(null)).toBe(false);
expect(isPlatformManagedProvider(undefined)).toBe(false);
});
});
it("platform-managed provider with a declared auth env requires NO credential, hides the key field, and sends NO secret", async () => {
await openDialog();
await setTemplate("platform-managed-test");
fireEvent.change(screen.getByPlaceholderText("e.g. SEO Agent"), {
target: { value: "Platform Agent" },
});
// The credential input must NOT render for platform-managed; a "no key
// required" note appears instead.
await waitFor(() =>
expect(screen.getByText("Platform-managed — no API key required.")).toBeTruthy(),
);
expect(screen.queryByLabelText("MOLECULE_LLM_USAGE_TOKEN")).toBeNull();
const createBtn = screen.getAllByRole("button").find((b) => b.textContent === "Create");
fireEvent.click(createBtn!);
await waitFor(() => expect(mockPost).toHaveBeenCalled());
// No validation error, and the provisioner-injected token is NOT clobbered
// by an empty secret.
expect(screen.queryByText("Provider credential is required")).toBeNull();
const body = mockPost.mock.calls[0][1] as Record<string, unknown>;
expect(body.llm_provider).toBe("platform");
expect(body.secrets).toBeUndefined();
});
it("BYOK provider still requires a credential and renders the key field (no-regression)", async () => {
await openDialog();
await setTemplate("byok-only-test");
fireEvent.change(screen.getByPlaceholderText("e.g. SEO Agent"), {
target: { value: "BYOK Agent" },
});
// The credential field IS rendered for BYOK...
await waitFor(() => expect(screen.getByLabelText("OPENAI_API_KEY")).toBeTruthy());
const createBtn = screen.getAllByRole("button").find((b) => b.textContent === "Create");
fireEvent.click(createBtn!);
// ...and create stays blocked until it's filled.
await waitFor(() =>
expect(screen.getByText("Provider credential is required")).toBeTruthy(),
);
expect(mockPost).not.toHaveBeenCalled();
});
});