diff --git a/plugins/admin/public/index.html b/plugins/admin/public/index.html
index f019ed3..b96e906 100644
--- a/plugins/admin/public/index.html
+++ b/plugins/admin/public/index.html
@@ -6984,19 +6984,20 @@
Confirm governance change
const baseModel = config.data.baseModel || config.data.baseModelDefault || "";
const baseProvider = onboardingProviderForModel(baseModel);
const baseStatus = onboardingModelStatuses.find((item) => item.provider === baseProvider);
- onboardingBadge(
- "onboarding-model-badge",
- baseStatus?.configured ? "Ready" : "Needs a key",
- Boolean(baseStatus?.configured),
- );
+ const harnessAuth = models.data.harnessAuth;
+ const baseHarnessAuth = Boolean(harnessAuth && harnessAuth.provider === baseProvider);
+ const baseReady = Boolean(baseModel) && (Boolean(baseStatus?.configured) || baseHarnessAuth);
+ onboardingBadge("onboarding-model-badge", baseReady ? "Ready" : "Needs a key", baseReady);
$("onboarding-model-summary").textContent = !baseModel
? "No base model is configured yet — pick a provider and model below."
: baseStatus?.configured
? baseModel + " · " + (baseStatus.source === "admin" ? "admin-managed key" : "deployment key")
- : baseModel +
- " cannot run until its " +
- (MODEL_PROVIDER_LABELS[baseProvider] || connectorName(baseProvider)) +
- " key is configured.";
+ : baseHarnessAuth
+ ? baseModel + " · authenticated by the " + harnessAuth.harnessId + " harness — no API key needed."
+ : baseModel +
+ " cannot run until its " +
+ (MODEL_PROVIDER_LABELS[baseProvider] || connectorName(baseProvider)) +
+ " key is configured.";
renderOnboardingProviderOptions(baseProvider);
renderOnboardingModelOptions(baseModel);
diff --git a/plugins/admin/test/onboarding-view.test.ts b/plugins/admin/test/onboarding-view.test.ts
index c7fe9b4..406390c 100644
--- a/plugins/admin/test/onboarding-view.test.ts
+++ b/plugins/admin/test/onboarding-view.test.ts
@@ -28,6 +28,98 @@ function resolveView(pathname: string, search: string): string {
return vm.runInContext(src, context);
}
+interface FakeElement {
+ textContent: string;
+ className: string;
+ value: string;
+ placeholder: string;
+ disabled: boolean;
+ href: string;
+ options: Array<{ value?: string; textContent?: string }>;
+ appendChild(option: { value?: string; textContent?: string }): void;
+}
+
+async function runLoadOnboarding(modelProviders: unknown): Promise> {
+ const src = slice("let onboardingModels = {};", '$("onboarding-model-provider").onchange') + "\nloadOnboarding();";
+ const elements: Record = {};
+ const fixtures: Record = {
+ "/api/model-providers": modelProviders,
+ "/api/slack-installation": { configured: false },
+ "/api/connector-catalog": { catalog: [] },
+ "/api/scopes/org%3Adefault-org": { baseModel: "claude-opus-5" },
+ };
+ const context = vm.createContext({
+ $: (id: string) =>
+ (elements[id] ??= {
+ textContent: "",
+ className: "",
+ value: "",
+ placeholder: "",
+ disabled: false,
+ href: "",
+ options: [],
+ appendChild(option) {
+ this.options.push(option);
+ },
+ }),
+ api: async (_method: string, path: string) => ({ ok: true, data: fixtures[path] ?? {} }),
+ orgScope: () => "org:default-org",
+ encodeURIComponent,
+ setStatus: () => {},
+ connectorName: (id: string) => id,
+ viewLoadedAt: {},
+ Date,
+ document: { createElement: () => ({}) },
+ });
+ await vm.runInContext(src, context);
+ return elements;
+}
+
+const UNCONFIGURED_PROVIDERS = [
+ { provider: "anthropic", configured: false, source: "absent" },
+ { provider: "openai", configured: false, source: "absent" },
+ { provider: "openrouter", configured: false, source: "absent" },
+];
+const ANTHROPIC_MODELS = [{ id: "claude-opus-5", name: "Claude Opus 5", provider: "anthropic" }];
+
+test("harness-carried auth shows the model step as ready without a stored key", async () => {
+ const elements = await runLoadOnboarding({
+ providers: UNCONFIGURED_PROVIDERS,
+ models: ANTHROPIC_MODELS,
+ harnessAuth: { harnessId: "claude", provider: "anthropic" },
+ });
+ assert.equal(elements["onboarding-model-badge"]!.textContent, "Ready");
+ assert.equal(elements["onboarding-model-badge"]!.className, "badge ok");
+ assert.equal(
+ elements["onboarding-model-summary"]!.textContent,
+ "claude-opus-5 · authenticated by the claude harness — no API key needed.",
+ );
+});
+
+test("without harness auth an unconfigured provider still needs a key", async () => {
+ const elements = await runLoadOnboarding({
+ providers: UNCONFIGURED_PROVIDERS,
+ models: ANTHROPIC_MODELS,
+ });
+ assert.equal(elements["onboarding-model-badge"]!.textContent, "Needs a key");
+ assert.equal(elements["onboarding-model-badge"]!.className, "badge warn");
+ assert.match(elements["onboarding-model-summary"]!.textContent, /cannot run until its Anthropic key is configured/);
+});
+
+test("a stored key keeps its summary even when the harness also carries auth", async () => {
+ const elements = await runLoadOnboarding({
+ providers: [
+ { provider: "anthropic", configured: true, source: "admin" },
+ { provider: "openai", configured: false, source: "absent" },
+ { provider: "openrouter", configured: false, source: "absent" },
+ ],
+ models: ANTHROPIC_MODELS,
+ harnessAuth: { harnessId: "claude", provider: "anthropic" },
+ });
+ assert.equal(elements["onboarding-model-badge"]!.textContent, "Ready");
+ assert.equal(elements["onboarding-model-summary"]!.textContent, "claude-opus-5 · admin-managed key");
+});
+
test("onboarding is a navigable view", () => {
assert.match(html, /\{ label: "Admin", views: \["onboarding",/);
});
diff --git a/src/api/deps.ts b/src/api/deps.ts
index b31743f..1e778ed 100644
--- a/src/api/deps.ts
+++ b/src/api/deps.ts
@@ -1,4 +1,4 @@
-import type { ModelProviderAvailability } from "../model/pi-models.ts";
+import type { ModelProvider, ModelProviderAvailability } from "../model/pi-models.ts";
import type { ModelCredentialStore } from "../model/model-credential-store.ts";
import type { ReplayDedupe } from "../auth/replay-dedupe.ts";
import type { FetchLike, OAuthClientResolver } from "../connectors/oauth.ts";
@@ -83,6 +83,7 @@ export interface ServerDeps {
modelCredentialFetch?: typeof fetch;
brandingDefault?: { accent?: string; mark?: string; selfLabel?: string };
harnessId?: string;
+ harnessCarriedModelAuth?: ModelProvider;
admin?: AdminService;
rateLimiter?: RateLimiter;
sessions?: SessionStore;
diff --git a/src/api/routes/admin/model-providers.ts b/src/api/routes/admin/model-providers.ts
index 66a2e09..69ae1ad 100644
--- a/src/api/routes/admin/model-providers.ts
+++ b/src/api/routes/admin/model-providers.ts
@@ -51,6 +51,9 @@ export async function getModelProviders(ctx: ApiCtx): Promise {
return sendJson(ctx.res, 200, {
providers: await ctx.deps.modelCredentials.statuses(),
models: await selectableModelCatalog(ctx.deps.modelCredentialFetch),
+ ...(ctx.deps.harnessCarriedModelAuth
+ ? { harnessAuth: { harnessId: ctx.deps.harnessId ?? "pi", provider: ctx.deps.harnessCarriedModelAuth } }
+ : {}),
});
}
diff --git a/src/api/routes/surface.ts b/src/api/routes/surface.ts
index 397a558..cd1a28f 100644
--- a/src/api/routes/surface.ts
+++ b/src/api/routes/surface.ts
@@ -889,7 +889,9 @@ async function getSurfaceConfig(ctx: ApiCtx): Promise {
webuiModels: configuredPicker.length ? configuredPicker : allowed,
baseModel: resolvedBase,
harnessId,
- ...(managedKeys ? { modelProviderConfigured: Object.values(managedKeys).some(Boolean) } : {}),
+ ...(managedKeys
+ ? { modelProviderConfigured: Object.values(managedKeys).some(Boolean) || Boolean(deps.harnessCarriedModelAuth) }
+ : {}),
externalSlackParticipants,
...(Object.keys(resolvedBranding).length ? { branding: resolvedBranding } : {}),
});
diff --git a/src/config.ts b/src/config.ts
index 534134b..23d516d 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -162,6 +162,16 @@ export function baseModelProviders(config: Config): ModelProviderAvailability |
return config.modelProvider ? onlyProvider(config.modelProvider) : undefined;
}
+export function harnessCarriedModelAuth(config: Config): ModelProvider | undefined {
+ if (
+ config.harness === "claude" &&
+ (config.claudeProcessEnv.CLAUDE_CODE_OAUTH_TOKEN || config.claudeProcessEnv.ANTHROPIC_AUTH_TOKEN)
+ )
+ return "anthropic";
+ if (config.harness === "codex" && config.codexProcessEnv.CODEX_ACCESS_TOKEN) return "openai";
+ return undefined;
+}
+
interface AwsSandboxEnv {
region: string;
profile?: string;
diff --git a/src/index.ts b/src/index.ts
index fbd390c..e833d80 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,10 @@
-import { baseModelProviders, configuredModelForHarness, loadConfig, providerKeysPresent } from "./config.ts";
+import {
+ baseModelProviders,
+ configuredModelForHarness,
+ harnessCarriedModelAuth,
+ loadConfig,
+ providerKeysPresent,
+} from "./config.ts";
import { buildApp, stopWithBackstop } from "./wiring.ts";
import { createServer } from "./api/server.ts";
import { errMessage } from "./util/errors.ts";
@@ -8,6 +14,7 @@ import { slackPluginConfigFromEnv, startSlackPlugin } from "./slack/index.ts";
import { createSlackRuntimeReconciler } from "./surfaces/slack-runtime.ts";
const config = loadConfig();
+const carriedModelAuth = harnessCarriedModelAuth(config);
const built = buildApp(config);
const envSlackConfig = slackPluginConfigFromEnv(process.env);
@@ -35,6 +42,7 @@ const server = createServer(built.app, {
modelCredentials: built.modelCredentials,
...(config.brandingDefault ? { brandingDefault: config.brandingDefault } : {}),
harnessId: config.harness,
+ ...(carriedModelAuth ? { harnessCarriedModelAuth: carriedModelAuth } : {}),
connectorTokens: built.connectorTokens,
slackInstallation: built.slackInstallation,
slackEnvironmentState,
diff --git a/test/model-credential-route.test.ts b/test/model-credential-route.test.ts
index e127025..bf3c80c 100644
--- a/test/model-credential-route.test.ts
+++ b/test/model-credential-route.test.ts
@@ -8,6 +8,7 @@ import { join } from "node:path";
import { test } from "node:test";
import { createInsecureTestServer } from "../src/api/server.ts";
import { buildApp, type BuiltApp } from "../src/wiring.ts";
+import { harnessCarriedModelAuth } from "../src/config.ts";
import { testConfig } from "./support/test-config.ts";
import { createModelCredentialStore, type StoredModelCredential } from "../src/model/model-credential-store.ts";
import { createMemoryMap } from "../src/persistence/durable-map.ts";
@@ -22,18 +23,17 @@ function start(
built: BuiltApp;
close: () => Promise;
} {
- const built = buildApp(
- testConfig({
- dataDir: mkdtempSync(join(tmpdir(), "model-credential-route-")),
- ...config,
- }),
- { modelCredentialFetch },
- );
+ const cfg = testConfig({
+ dataDir: mkdtempSync(join(tmpdir(), "model-credential-route-")),
+ ...config,
+ });
+ const built = buildApp(cfg, { modelCredentialFetch });
const server = createInsecureTestServer(built.app, {
config: built.config,
modelCredentials: built.modelCredentials,
modelCredentialFetch,
harnessId: config.harness ?? "pi",
+ ...(harnessCarriedModelAuth(cfg) ? { harnessCarriedModelAuth: harnessCarriedModelAuth(cfg) } : {}),
providerKeys: {
anthropic: Boolean(config.anthropicApiKey),
openai: Boolean(config.openaiApiKey),
@@ -401,6 +401,47 @@ test("surface-config reports whether any model provider is configured", async ()
}
});
+test("a claude harness with an OAuth token counts as configured without corrupting store statuses", async () => {
+ const srv = start({
+ harness: "claude",
+ claudeProcessEnv: { CLAUDE_CODE_OAUTH_TOKEN: "sk-ant-oat-test" } as NodeJS.ProcessEnv,
+ });
+ try {
+ const surface = await fetch(`${srv.base}/v1/surface-config`);
+ assert.equal(surface.status, 200);
+ assert.equal(((await surface.json()) as { modelProviderConfigured?: boolean }).modelProviderConfigured, true);
+
+ const providers = await fetch(`${srv.base}/v1/admin/model-providers`, { headers: ADMIN });
+ assert.equal(providers.status, 200);
+ const body = (await providers.json()) as {
+ providers: Array<{ provider: string; configured: boolean; source: string }>;
+ harnessAuth?: { harnessId: string; provider: string };
+ };
+ assert.deepEqual(body.harnessAuth, { harnessId: "claude", provider: "anthropic" });
+ assert.deepEqual(
+ body.providers.find((item) => item.provider === "anthropic"),
+ { provider: "anthropic", configured: false, source: "absent" },
+ );
+ } finally {
+ await srv.close();
+ }
+});
+
+test("a claude harness without any token stays unconfigured", async () => {
+ const srv = start({ harness: "claude" });
+ try {
+ const surface = await fetch(`${srv.base}/v1/surface-config`);
+ assert.equal(surface.status, 200);
+ assert.equal(((await surface.json()) as { modelProviderConfigured?: boolean }).modelProviderConfigured, false);
+
+ const providers = await fetch(`${srv.base}/v1/admin/model-providers`, { headers: ADMIN });
+ assert.equal(providers.status, 200);
+ assert.equal("harnessAuth" in ((await providers.json()) as Record), false);
+ } finally {
+ await srv.close();
+ }
+});
+
test("admin model credentials survive a second app instance on the same durable store", async () => {
const backing = createMemoryMap();
const first = createModelCredentialStore({ backing, keyMaterial: "shared-model-key" });