Skip to content

Commit 7a6be3d

Browse files
committed
refactor(plugins): move auth and model policy to providers
1 parent 3d8c29c commit 7a6be3d

30 files changed

Lines changed: 506 additions & 513 deletions

docs/concepts/model-providers.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ For model selection rules, see [/concepts/models](/concepts/models).
2020
OpenClaw merges that output into `models.providers` before writing
2121
`models.json`.
2222
- Provider manifests can declare `providerAuthEnvVars` so generic env-based
23-
auth probes do not need to load plugin runtime.
23+
auth probes do not need to load plugin runtime. The remaining core env-var
24+
map is now just for non-plugin/core providers and a few generic-precedence
25+
cases such as Anthropic API-key-first onboarding.
2426
- Provider plugins can also own provider runtime behavior via
2527
`resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`,
2628
`capabilities`, `prepareExtraParams`, `wrapStreamFn`,
@@ -37,6 +39,10 @@ the generic inference loop.
3739

3840
Typical split:
3941

42+
- `auth[].run` / `auth[].runNonInteractive`: provider owns onboarding/login
43+
flows for `openclaw onboard`, `openclaw models auth`, and headless setup
44+
- `wizard.onboarding` / `wizard.modelPicker`: provider owns auth-choice labels,
45+
hints, and setup entries in onboarding/model pickers
4046
- `catalog`: provider appears in `models.providers`
4147
- `resolveDynamicModel`: provider accepts model ids not present in the local
4248
static catalog yet

docs/tools/plugin.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,7 +1230,8 @@ The non-interactive context includes:
12301230
- the current and base config
12311231
- parsed onboarding CLI options
12321232
- runtime logging/error helpers
1233-
- agent/workspace dirs
1233+
- agent/workspace dirs so the provider can persist auth into the same scoped
1234+
store used by the rest of onboarding
12341235
- `resolveApiKey(...)` to read provider keys from flags, env, or existing auth
12351236
profiles while honoring `--secret-input-mode`
12361237
- `toApiKeyCredential(...)` to convert a resolved key into an auth-profile
@@ -1407,10 +1408,13 @@ api.registerProvider({
14071408
Notes:
14081409

14091410
- `run` receives a `ProviderAuthContext` with `prompter`, `runtime`,
1410-
`openUrl`, and `oauth.createVpsAwareHandlers` helpers.
1411+
`openUrl`, `oauth.createVpsAwareHandlers`, `secretInputMode`, and
1412+
`allowSecretRefPrompt` helpers/state. Onboarding/configure flows can use
1413+
these to honor `--secret-input-mode` or offer env/file/exec secret-ref
1414+
capture, while `openclaw models auth` keeps a tighter prompt surface.
14111415
- `runNonInteractive` receives a `ProviderAuthMethodNonInteractiveContext`
1412-
with `opts`, `resolveApiKey`, and `toApiKeyCredential` helpers for
1413-
headless onboarding.
1416+
with `opts`, `agentDir`, `resolveApiKey`, and `toApiKeyCredential` helpers
1417+
for headless onboarding.
14141418
- Return `configPatch` when you need to add default models or provider config.
14151419
- Return `defaultModel` so `--set-default` can update agent defaults.
14161420
- `wizard.setup` adds a provider choice to `openclaw onboard`.

extensions/anthropic/index.ts

Lines changed: 127 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ import {
55
type ProviderResolveDynamicModelContext,
66
type ProviderRuntimeModel,
77
} from "openclaw/plugin-sdk/core";
8+
import { upsertAuthProfile } from "../../src/agents/auth-profiles.js";
89
import { normalizeModelCompat } from "../../src/agents/model-compat.js";
10+
import { parseDurationMs } from "../../src/cli/parse-duration.js";
11+
import {
12+
normalizeSecretInputModeInput,
13+
promptSecretRefForOnboarding,
14+
resolveSecretInputModeForEnvSelection,
15+
} from "../../src/commands/auth-choice.apply-helpers.js";
916
import { buildTokenProfileId, validateAnthropicSetupToken } from "../../src/commands/auth-token.js";
17+
import { applyAuthProfileConfig } from "../../src/commands/onboard-auth.js";
1018
import { fetchClaudeUsage } from "../../src/infra/provider-usage.fetch.js";
1119
import type { ProviderAuthResult } from "../../src/plugins/types.js";
20+
import { normalizeSecretInput } from "../../src/utils/normalize-secret-input.js";
1221

1322
const PROVIDER_ID = "anthropic";
1423
const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6";
@@ -119,11 +128,41 @@ async function runAnthropicSetupToken(ctx: ProviderAuthContext): Promise<Provide
119128
"Anthropic setup-token",
120129
);
121130

122-
const tokenRaw = await ctx.prompter.text({
123-
message: "Paste Anthropic setup-token",
124-
validate: (value) => validateAnthropicSetupToken(String(value ?? "")),
125-
});
126-
const token = String(tokenRaw ?? "").trim();
131+
const requestedSecretInputMode = normalizeSecretInputModeInput(ctx.secretInputMode);
132+
const selectedMode = ctx.allowSecretRefPrompt
133+
? await resolveSecretInputModeForEnvSelection({
134+
prompter: ctx.prompter,
135+
explicitMode: requestedSecretInputMode,
136+
copy: {
137+
modeMessage: "How do you want to provide this setup token?",
138+
plaintextLabel: "Paste setup token now",
139+
plaintextHint: "Stores the token directly in the auth profile",
140+
},
141+
})
142+
: "plaintext";
143+
144+
let token = "";
145+
let tokenRef: { source: "env" | "file" | "exec"; provider: string; id: string } | undefined;
146+
if (selectedMode === "ref") {
147+
const resolved = await promptSecretRefForOnboarding({
148+
provider: "anthropic-setup-token",
149+
config: ctx.config,
150+
prompter: ctx.prompter,
151+
preferredEnvVar: "ANTHROPIC_SETUP_TOKEN",
152+
copy: {
153+
sourceMessage: "Where is this Anthropic setup token stored?",
154+
envVarPlaceholder: "ANTHROPIC_SETUP_TOKEN",
155+
},
156+
});
157+
token = resolved.resolvedValue.trim();
158+
tokenRef = resolved.ref;
159+
} else {
160+
const tokenRaw = await ctx.prompter.text({
161+
message: "Paste Anthropic setup-token",
162+
validate: (value) => validateAnthropicSetupToken(String(value ?? "")),
163+
});
164+
token = String(tokenRaw ?? "").trim();
165+
}
127166
const tokenError = validateAnthropicSetupToken(token);
128167
if (tokenError) {
129168
throw new Error(tokenError);
@@ -145,12 +184,80 @@ async function runAnthropicSetupToken(ctx: ProviderAuthContext): Promise<Provide
145184
type: "token",
146185
provider: PROVIDER_ID,
147186
token,
187+
...(tokenRef ? { tokenRef } : {}),
148188
},
149189
},
150190
],
151191
};
152192
}
153193

194+
async function runAnthropicSetupTokenNonInteractive(ctx: {
195+
config: ProviderAuthContext["config"];
196+
opts: {
197+
tokenProvider?: string;
198+
token?: string;
199+
tokenExpiresIn?: string;
200+
tokenProfileId?: string;
201+
};
202+
runtime: ProviderAuthContext["runtime"];
203+
agentDir?: string;
204+
}): Promise<ProviderAuthContext["config"] | null> {
205+
const provider = ctx.opts.tokenProvider?.trim().toLowerCase();
206+
if (!provider) {
207+
ctx.runtime.error("Missing --token-provider for --auth-choice token.");
208+
ctx.runtime.exit(1);
209+
return null;
210+
}
211+
if (provider !== PROVIDER_ID) {
212+
ctx.runtime.error("Only --token-provider anthropic is supported for --auth-choice token.");
213+
ctx.runtime.exit(1);
214+
return null;
215+
}
216+
217+
const token = normalizeSecretInput(ctx.opts.token);
218+
if (!token) {
219+
ctx.runtime.error("Missing --token for --auth-choice token.");
220+
ctx.runtime.exit(1);
221+
return null;
222+
}
223+
const tokenError = validateAnthropicSetupToken(token);
224+
if (tokenError) {
225+
ctx.runtime.error(tokenError);
226+
ctx.runtime.exit(1);
227+
return null;
228+
}
229+
230+
let expires: number | undefined;
231+
const expiresInRaw = ctx.opts.tokenExpiresIn?.trim();
232+
if (expiresInRaw) {
233+
try {
234+
expires = Date.now() + parseDurationMs(expiresInRaw, { defaultUnit: "d" });
235+
} catch (err) {
236+
ctx.runtime.error(`Invalid --token-expires-in: ${String(err)}`);
237+
ctx.runtime.exit(1);
238+
return null;
239+
}
240+
}
241+
242+
const profileId =
243+
ctx.opts.tokenProfileId?.trim() || buildTokenProfileId({ provider: PROVIDER_ID, name: "" });
244+
upsertAuthProfile({
245+
profileId,
246+
agentDir: ctx.agentDir,
247+
credential: {
248+
type: "token",
249+
provider: PROVIDER_ID,
250+
token,
251+
...(expires ? { expires } : {}),
252+
},
253+
});
254+
return applyAuthProfileConfig(ctx.config, {
255+
profileId,
256+
provider: PROVIDER_ID,
257+
mode: "token",
258+
});
259+
}
260+
154261
const anthropicPlugin = {
155262
id: PROVIDER_ID,
156263
name: "Anthropic Provider",
@@ -169,8 +276,23 @@ const anthropicPlugin = {
169276
hint: "Paste a setup-token from `claude setup-token`",
170277
kind: "token",
171278
run: async (ctx: ProviderAuthContext) => await runAnthropicSetupToken(ctx),
279+
runNonInteractive: async (ctx) =>
280+
await runAnthropicSetupTokenNonInteractive({
281+
config: ctx.config,
282+
opts: ctx.opts,
283+
runtime: ctx.runtime,
284+
agentDir: ctx.agentDir,
285+
}),
172286
},
173287
],
288+
wizard: {
289+
onboarding: {
290+
choiceId: "token",
291+
choiceLabel: "Anthropic token (paste setup-token)",
292+
choiceHint: "Run `claude setup-token` elsewhere, then paste the token here",
293+
methodId: "setup-token",
294+
},
295+
},
174296
resolveDynamicModel: (ctx) => resolveAnthropicForwardCompatModel(ctx),
175297
capabilities: {
176298
providerFamily: "anthropic",

extensions/openai/openai-codex-provider.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,20 @@ function resolveCodexForwardCompatModel(
133133
}
134134

135135
async function runOpenAICodexOAuth(ctx: ProviderAuthContext) {
136-
const creds = await loginOpenAICodexOAuth({
137-
prompter: ctx.prompter,
138-
runtime: ctx.runtime,
139-
isRemote: ctx.isRemote,
140-
openUrl: ctx.openUrl,
141-
localBrowserMessage: "Complete sign-in in browser…",
142-
});
136+
let creds;
137+
try {
138+
creds = await loginOpenAICodexOAuth({
139+
prompter: ctx.prompter,
140+
runtime: ctx.runtime,
141+
isRemote: ctx.isRemote,
142+
openUrl: ctx.openUrl,
143+
localBrowserMessage: "Complete sign-in in browser…",
144+
});
145+
} catch {
146+
return { profiles: [] };
147+
}
143148
if (!creds) {
144-
throw new Error("OpenAI Codex OAuth did not return credentials.");
149+
return { profiles: [] };
145150
}
146151

147152
return buildOauthProviderAuthResult({
@@ -168,6 +173,14 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin {
168173
run: async (ctx) => await runOpenAICodexOAuth(ctx),
169174
},
170175
],
176+
wizard: {
177+
onboarding: {
178+
choiceId: "openai-codex",
179+
choiceLabel: "OpenAI Codex (ChatGPT OAuth)",
180+
choiceHint: "Browser sign-in",
181+
methodId: "oauth",
182+
},
183+
},
171184
catalog: {
172185
order: "profile",
173186
run: async (ctx) => {

extensions/xai/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { normalizeProviderId } from "../../src/agents/model-selection.js";
12
import {
23
createPluginBackedWebSearchProvider,
34
getScopedCredentialValue,
@@ -6,12 +7,28 @@ import {
67
import { emptyPluginConfigSchema } from "../../src/plugins/config-schema.js";
78
import type { OpenClawPluginApi } from "../../src/plugins/types.js";
89

10+
const XAI_MODERN_MODEL_PREFIXES = ["grok-4"] as const;
11+
12+
function matchesModernXaiModel(modelId: string): boolean {
13+
const normalized = modelId.trim().toLowerCase();
14+
return XAI_MODERN_MODEL_PREFIXES.some((prefix) => normalized.startsWith(prefix));
15+
}
16+
917
const xaiPlugin = {
1018
id: "xai",
1119
name: "xAI Plugin",
1220
description: "Bundled xAI plugin",
1321
configSchema: emptyPluginConfigSchema(),
1422
register(api: OpenClawPluginApi) {
23+
api.registerProvider({
24+
id: "xai",
25+
label: "xAI",
26+
docsPath: "/providers/models",
27+
envVars: ["XAI_API_KEY"],
28+
auth: [],
29+
isModernModelRef: ({ provider, modelId }) =>
30+
normalizeProviderId(provider) === "xai" ? matchesModernXaiModel(modelId) : undefined,
31+
});
1532
api.registerWebSearchProvider(
1633
createPluginBackedWebSearchProvider({
1734
id: "grok",

extensions/xai/openclaw.plugin.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{
22
"id": "xai",
3+
"providers": ["xai"],
4+
"providerAuthEnvVars": {
5+
"xai": ["XAI_API_KEY"]
6+
},
37
"configSchema": {
48
"type": "object",
59
"additionalProperties": false,

0 commit comments

Comments
 (0)