fix(canvas): suppress MOLECULE_LLM_USAGE_TOKEN field for platform-managed providers (#2248) #2388

Merged
devops-engineer merged 2 commits from fix/2248-suppress-platform-managed-credentials into main 2026-06-07 21:39:17 +00:00
5 changed files with 436 additions and 6 deletions
+15 -3
View File
@@ -12,6 +12,7 @@ import {
ProviderModelSelector,
buildProviderCatalog,
findProviderForModel,
isPlatformManagedProvider,
type SelectorValue,
} from "./ProviderModelSelector";
@@ -267,10 +268,21 @@ function ProviderPickerModal({
setSelectorValue(initial);
}, [open, initial]);
// #2248: filter out provisioner-injected internal tokens for platform-managed
// providers so the user can't clobber them. Memoized so the array reference is
// stable across renders and does not churn the entries useEffect.
const userEditableEnvVars = useMemo(() => {
const selectedProvider = catalog.find((p) => p.id === selectorValue.providerId);
const isPlatformManaged = selectedProvider ? isPlatformManagedProvider(selectedProvider) : false;
return isPlatformManaged
? selectorValue.envVars.filter((k) => k !== "MOLECULE_LLM_USAGE_TOKEN")
: selectorValue.envVars;
}, [catalog, selectorValue.providerId, selectorValue.envVars]);
useEffect(() => {
if (!open) return;
setEntries(
selectorValue.envVars.map((key) => ({
userEditableEnvVars.map((key) => ({
key,
value: "",
// Pre-mark as saved when the key is already in the configured
@@ -283,7 +295,7 @@ function ProviderPickerModal({
);
setOptionalEntries(
optionalKeys
.filter((key) => !selectorValue.envVars.includes(key))
.filter((key) => !userEditableEnvVars.includes(key))
.map((key) => ({
key,
value: "",
@@ -292,7 +304,7 @@ function ProviderPickerModal({
error: null,
})),
);
}, [open, selectorValue.envVars, configuredKeys, optionalKeys]);
}, [open, userEditableEnvVars, configuredKeys, optionalKeys]);
useEffect(() => {
if (!open) return;
@@ -91,6 +91,7 @@ export interface RegistryModel {
name?: string;
provider?: string;
billing_mode?: "platform_managed" | "byok";
required_env?: string[];
}
export interface SelectorValue {
@@ -0,0 +1,175 @@
// @vitest-environment jsdom
/**
* Regression tests for #2248 — platform-managed provider credential suppression.
*
* Covers:
* - MOLECULE_LLM_USAGE_TOKEN is hidden when the selected provider is platform-managed
* - MOLECULE_LLM_USAGE_TOKEN is still shown for BYOK providers
* - No render churn from unstable array references (useMemo guard)
*/
import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, fireEvent, cleanup, waitFor, act } from "@testing-library/react";
import { MissingKeysModal } from "../MissingKeysModal";
import type { ModelSpec, ProviderChoice } from "@/lib/deploy-preflight";
vi.mock("@/lib/api", () => ({
api: { get: vi.fn(), put: vi.fn() },
}));
vi.mock("@/lib/deploy-preflight", async () => {
const actual = await vi.importActual<typeof import("@/lib/deploy-preflight")>(
"@/lib/deploy-preflight",
);
return actual;
});
const PLATFORM_MANAGED_MODELS: ModelSpec[] = [
{ id: "platform-claude", provider: "platform", required_env: ["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"] },
];
const BYOK_MODELS: ModelSpec[] = [
{ id: "byok-claude", provider: "anthropic", required_env: ["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"] },
];
function makeProviders(billingMode: "platform_managed" | "byok"): ProviderChoice[] {
const main = {
id: billingMode === "platform_managed" ? "platform|ANTHROPIC_API_KEY|MOLECULE_LLM_USAGE_TOKEN" : "anthropic|ANTHROPIC_API_KEY|MOLECULE_LLM_USAGE_TOKEN",
label: billingMode === "platform_managed" ? "Platform Anthropic" : "BYOK Anthropic",
envVars: ["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"],
billingMode,
};
// Need ≥2 providers so MissingKeysModal enters picker mode (pickerMode = providers.length > 1).
const dummy = {
id: "openai|OPENAI_API_KEY",
label: "OpenAI",
envVars: ["OPENAI_API_KEY"],
};
return [main, dummy];
}
describe("ProviderPickerModal — platform-managed suppression (#2248)", () => {
afterEach(() => cleanup());
it("hides MOLECULE_LLM_USAGE_TOKEN when provider is platform-managed", () => {
render(
<MissingKeysModal
open
missingKeys={["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"]}
providers={makeProviders("platform_managed")}
models={PLATFORM_MANAGED_MODELS}
runtime="claude-code"
onKeysAdded={vi.fn()}
onCancel={vi.fn()}
/>,
);
// Only ANTHROPIC_API_KEY should be rendered; MOLECULE_LLM_USAGE_TOKEN suppressed
expect(screen.getByText("ANTHROPIC_API_KEY")).toBeTruthy();
expect(screen.queryByText("MOLECULE_LLM_USAGE_TOKEN")).toBeNull();
});
it("shows MOLECULE_LLM_USAGE_TOKEN when provider is BYOK", () => {
render(
<MissingKeysModal
open
missingKeys={["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"]}
providers={makeProviders("byok")}
models={BYOK_MODELS}
runtime="claude-code"
onKeysAdded={vi.fn()}
onCancel={vi.fn()}
/>,
);
// Both keys visible for BYOK
expect(screen.getByText("ANTHROPIC_API_KEY")).toBeTruthy();
expect(screen.getByText("MOLECULE_LLM_USAGE_TOKEN")).toBeTruthy();
});
it("does not churn renders when the modal is open and platform-managed", () => {
let renderCount = 0;
function RenderSpy({ children }: { children: React.ReactNode }) {
renderCount++;
return <>{children}</>;
}
render(
<RenderSpy>
<MissingKeysModal
open
missingKeys={["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"]}
providers={makeProviders("platform_managed")}
models={PLATFORM_MANAGED_MODELS}
runtime="claude-code"
onKeysAdded={vi.fn()}
onCancel={vi.fn()}
/>
</RenderSpy>,
);
const countAfterInitial = renderCount;
// Wait a tick — if useEffect were looping, renderCount would climb.
// In jsdom without real timers there's no automatic re-render, so we
// just assert the count is stable immediately after the single
// commit required by the initial open state.
expect(renderCount).toBe(countAfterInitial);
expect(renderCount).toBeLessThanOrEqual(2); // StrictMode double-render ceiling
});
it("updates suppression correctly when switching from BYOK to platform-managed", async () => {
const providers: ProviderChoice[] = [
{
id: "anthropic|ANTHROPIC_API_KEY|MOLECULE_LLM_USAGE_TOKEN",
label: "BYOK Anthropic",
envVars: ["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"],
billingMode: "byok",
},
{
id: "platform|ANTHROPIC_API_KEY|MOLECULE_LLM_USAGE_TOKEN",
label: "Platform Anthropic",
envVars: ["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"],
billingMode: "platform_managed",
},
{
id: "openai|OPENAI_API_KEY",
label: "OpenAI",
envVars: ["OPENAI_API_KEY"],
},
];
const models: ModelSpec[] = [
{ id: "byok-claude", provider: "anthropic", required_env: ["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"] },
{ id: "platform-claude", provider: "platform", required_env: ["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"] },
];
render(
<MissingKeysModal
open
missingKeys={["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"]}
providers={providers}
models={models}
runtime="claude-code"
onKeysAdded={vi.fn()}
onCancel={vi.fn()}
/>,
);
// Default selection is providers[0] (BYOK) — both keys visible
expect(screen.getByText("ANTHROPIC_API_KEY")).toBeTruthy();
expect(screen.getByText("MOLECULE_LLM_USAGE_TOKEN")).toBeTruthy();
// Switch to platform-managed provider
const providerSelect = screen.getByTestId("provider-select") as HTMLSelectElement;
act(() => {
fireEvent.change(providerSelect, {
target: { value: "platform|ANTHROPIC_API_KEY|MOLECULE_LLM_USAGE_TOKEN" },
});
});
// MOLECULE_LLM_USAGE_TOKEN should now be suppressed
await waitFor(() => {
expect(screen.getByText("ANTHROPIC_API_KEY")).toBeTruthy();
});
expect(screen.queryByText("MOLECULE_LLM_USAGE_TOKEN")).toBeNull();
});
});
+16 -3
View File
@@ -13,6 +13,7 @@ import {
buildProviderCatalog,
buildProviderCatalogFromRegistry,
findProviderForModel,
isPlatformManagedProvider,
type SelectorValue,
type ProviderEntry,
type RegistryProvider,
@@ -682,6 +683,9 @@ export function ConfigTab({ workspaceId }: Props) {
name: m.name,
// carry the derived provider so the selector buckets correctly
...(m.provider ? { provider: m.provider } : {}),
// carry required_env so wasTemplateDriven can detect
// template-driven env lists for registry-backed runtimes
...(m.required_env ? { required_env: m.required_env } : {}),
}))
: availableModels,
[registryBacked, selectedRuntime?.registryModels, availableModels],
@@ -1017,6 +1021,15 @@ export function ConfigTab({ workspaceId }: Props) {
// top-level model. required_env follows the selected
// provider's envVars when the existing required_env
// was template-driven (don't clobber user-typed envs).
//
// #2248: suppress provisioner-injected internal tokens
// (MOLECULE_LLM_USAGE_TOKEN) for platform-managed providers
// so the user can't clobber them.
const selectedEntry = providerCatalog.find((p) => p.id === next.providerId);
const isPlatformManaged = selectedEntry ? isPlatformManagedProvider(selectedEntry) : false;
const filteredEnvVars = isPlatformManaged
? next.envVars.filter((k) => k !== "MOLECULE_LLM_USAGE_TOKEN")
: next.envVars;
setConfig((prev) => {
const v = next.model;
const prevModelId = prev.runtime_config?.model || prev.model || "";
@@ -1029,8 +1042,8 @@ export function ConfigTab({ workspaceId }: Props) {
prevRequired.every((e, i) => e === prevSpec.required_env![i])
: false);
const nextRequired =
next.envVars.length > 0 && wasTemplateDriven
? next.envVars
wasTemplateDriven
? filteredEnvVars
: prevRequired;
if (prev.runtime) {
return {
@@ -1038,7 +1051,7 @@ export function ConfigTab({ workspaceId }: Props) {
runtime_config: {
...prev.runtime_config,
model: v,
...(next.envVars.length > 0 && wasTemplateDriven
...(wasTemplateDriven
? { required_env: nextRequired }
: {}),
},
@@ -0,0 +1,229 @@
// @vitest-environment jsdom
//
// Regression tests for #2248 — platform-managed provider credential suppression
// in ConfigTab.
//
// Covers:
// - required_env is cleared to [] when switching to a platform-managed provider
// whose only declared env var is MOLECULE_LLM_USAGE_TOKEN (single-token case).
// - required_env preserves non-internal tokens for BYOK providers.
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
import { render, screen, cleanup, waitFor, fireEvent } from "@testing-library/react";
import React from "react";
afterEach(cleanup);
const apiGet = vi.fn();
const apiPatch = vi.fn();
const apiPut = vi.fn();
vi.mock("@/lib/api", () => ({
api: {
get: (path: string) => apiGet(path),
patch: (path: string, body: unknown) => apiPatch(path, body),
put: (path: string, body: unknown) => apiPut(path, body),
post: vi.fn(),
del: vi.fn(),
},
}));
vi.mock("@/store/canvas", () => ({
useCanvasStore: Object.assign(
(selector: (s: unknown) => unknown) =>
selector({ restartWorkspace: vi.fn(), updateNodeData: vi.fn() }),
{ getState: () => ({ restartWorkspace: vi.fn(), updateNodeData: vi.fn() }) },
),
}));
vi.mock("../AgentCardSection", () => ({
AgentCardSection: () => <div data-testid="agent-card-stub" />,
}));
import { ConfigTab } from "../ConfigTab";
function wireApi(opts: {
workspaceRuntime?: string;
workspaceModel?: string;
configYamlContent?: string | null;
templates?: Array<{
id: string;
name?: string;
runtime?: string;
models?: unknown[];
registry_backed?: boolean;
registry_providers?: unknown[];
registry_models?: unknown[];
}>;
}) {
apiGet.mockImplementation((path: string) => {
if (path === `/workspaces/ws-test`) {
return Promise.resolve({ runtime: opts.workspaceRuntime ?? "" });
}
if (path === `/workspaces/ws-test/model`) {
return Promise.resolve({ model: opts.workspaceModel ?? "" });
}
if (path === `/workspaces/ws-test/files/config.yaml`) {
if (opts.configYamlContent === null) {
return Promise.reject(new Error("not found"));
}
return Promise.resolve({ content: opts.configYamlContent ?? "" });
}
if (path === "/templates") {
return Promise.resolve(opts.templates ?? []);
}
return Promise.reject(new Error(`unmocked api.get: ${path}`));
});
}
beforeEach(() => {
apiGet.mockReset();
apiPatch.mockReset();
apiPut.mockReset();
});
describe("ConfigTab — platform-managed credential suppression (#2248)", () => {
it("clears required_env to [] when switching to a single-token platform-managed provider", async () => {
// Setup: workspace currently has a BYOK provider selected with both keys.
// The user switches to a platform-managed provider whose ONLY auth_env
// is MOLECULE_LLM_USAGE_TOKEN. After filtering, envVars becomes [];
// wasTemplateDriven must still overwrite required_env with [] so the
// old MOLECULE_LLM_USAGE_TOKEN requirement does not linger.
wireApi({
workspaceRuntime: "claude-code",
workspaceModel: "byok-sonnet",
configYamlContent: [
"runtime: claude-code",
"runtime_config:",
" model: byok-sonnet",
" required_env:",
" - ANTHROPIC_API_KEY",
" - MOLECULE_LLM_USAGE_TOKEN",
].join("\n"),
templates: [
{
id: "t-claude-code",
name: "Claude Code",
runtime: "claude-code",
models: [],
registry_backed: true,
registry_providers: [
{
name: "anthropic",
display_name: "BYOK Anthropic",
auth_env: ["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"],
billing_mode: "byok",
},
{
name: "platform",
display_name: "Platform Anthropic",
auth_env: ["MOLECULE_LLM_USAGE_TOKEN"],
billing_mode: "platform_managed",
},
],
registry_models: [
{ id: "byok-sonnet", provider: "anthropic", billing_mode: "byok", required_env: ["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"] },
{ id: "platform-sonnet", provider: "platform", billing_mode: "platform_managed", required_env: ["MOLECULE_LLM_USAGE_TOKEN"] },
],
},
],
});
apiPut.mockResolvedValue({});
apiPatch.mockResolvedValue({});
render(<ConfigTab workspaceId="ws-test" />);
// Wait for the provider dropdown to populate.
const providerSelect = (await waitFor(() =>
screen.getByTestId("provider-select"),
)) as HTMLSelectElement;
// Switch from BYOK to platform-managed provider.
const platformOption = Array.from(providerSelect.options).find((o) =>
o.text.includes("Platform"),
);
expect(platformOption).toBeTruthy();
fireEvent.change(providerSelect, { target: { value: platformOption!.value } });
// Save & Restart.
fireEvent.click(screen.getByRole("button", { name: /save & restart/i }));
await waitFor(() => {
expect(apiPut).toHaveBeenCalledWith(
"/workspaces/ws-test/files/config.yaml",
expect.objectContaining({
content: expect.not.stringContaining("ANTHROPIC_API_KEY"),
}),
);
});
// Verify the specific put call no longer carries the suppressed token.
const putCall = apiPut.mock.calls.find(
([path]) => path === "/workspaces/ws-test/files/config.yaml",
);
expect(putCall?.[1].content).not.toContain("MOLECULE_LLM_USAGE_TOKEN");
});
it("preserves non-internal tokens for BYOK providers", async () => {
wireApi({
workspaceRuntime: "claude-code",
workspaceModel: "byok-sonnet",
configYamlContent: [
"runtime: claude-code",
"runtime_config:",
" model: byok-sonnet",
" required_env:",
" - ANTHROPIC_API_KEY",
" - MOLECULE_LLM_USAGE_TOKEN",
].join("\n"),
templates: [
{
id: "t-claude-code",
name: "Claude Code",
runtime: "claude-code",
models: [],
registry_backed: true,
registry_providers: [
{
name: "anthropic",
display_name: "BYOK Anthropic",
auth_env: ["ANTHROPIC_API_KEY", "MOLECULE_LLM_USAGE_TOKEN"],
billing_mode: "byok",
},
],
registry_models: [
{ id: "byok-sonnet", provider: "anthropic", billing_mode: "byok" },
],
},
],
});
apiPut.mockResolvedValue({});
apiPatch.mockResolvedValue({});
render(<ConfigTab workspaceId="ws-test" />);
// Wait for load.
await waitFor(() =>
screen.getByRole("button", { name: /save & restart/i }),
);
// Click Save without changing provider — BYOK should keep both keys.
fireEvent.click(screen.getByRole("button", { name: /save & restart/i }));
await waitFor(() => {
expect(apiPut).toHaveBeenCalledWith(
"/workspaces/ws-test/files/config.yaml",
expect.objectContaining({
content: expect.stringContaining("required_env:"),
}),
);
});
const putCall = apiPut.mock.calls.find(
([path]) => path === "/workspaces/ws-test/files/config.yaml",
);
expect(putCall?.[1].content).toContain("ANTHROPIC_API_KEY");
expect(putCall?.[1].content).toContain("MOLECULE_LLM_USAGE_TOKEN");
});
});