mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
refactor(auth): isolate external oauth overlays
This commit is contained in:
@@ -615,7 +615,8 @@ Provider plugins now have two layers:
|
||||
- runtime hooks: `normalizeModelId`, `normalizeTransport`,
|
||||
`normalizeConfig`,
|
||||
`applyNativeStreamingUsageCompat`, `resolveConfigApiKey`,
|
||||
`resolveSyntheticAuth`, `shouldDeferSyntheticProfileAuth`,
|
||||
`resolveSyntheticAuth`, `resolveExternalOAuthProfiles`,
|
||||
`shouldDeferSyntheticProfileAuth`,
|
||||
`resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`,
|
||||
`contributeResolvedModelCompat`, `capabilities`,
|
||||
`normalizeToolSchemas`, `inspectToolSchemas`,
|
||||
@@ -648,52 +649,53 @@ client-id/client-secret setup vars.
|
||||
For model/provider plugins, OpenClaw calls hooks in this rough order.
|
||||
The "When to use" column is the quick decision guide.
|
||||
|
||||
| # | Hook | What it does | When to use |
|
||||
| --- | --------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | `catalog` | Publish provider config into `models.providers` during `models.json` generation | Provider owns a catalog or base URL defaults |
|
||||
| 2 | `applyConfigDefaults` | Apply provider-owned global config defaults during config materialization | Defaults depend on auth mode, env, or provider model-family semantics |
|
||||
| -- | _(built-in model lookup)_ | OpenClaw tries the normal registry/catalog path first | _(not a plugin hook)_ |
|
||||
| 3 | `normalizeModelId` | Normalize legacy or preview model-id aliases before lookup | Provider owns alias cleanup before canonical model resolution |
|
||||
| 4 | `normalizeTransport` | Normalize provider-family `api` / `baseUrl` before generic model assembly | Provider owns transport cleanup for custom provider ids in the same transport family |
|
||||
| 5 | `normalizeConfig` | Normalize `models.providers.<id>` before runtime/provider resolution | Provider needs config cleanup that should live with the plugin; bundled Google-family helpers also backstop supported Google config entries |
|
||||
| 6 | `applyNativeStreamingUsageCompat` | Apply native streaming-usage compat rewrites to config providers | Provider needs endpoint-driven native streaming usage metadata fixes |
|
||||
| 7 | `resolveConfigApiKey` | Resolve env-marker auth for config providers before runtime auth loading | Provider has provider-owned env-marker API-key resolution; `amazon-bedrock` also has a built-in AWS env-marker resolver here |
|
||||
| 8 | `resolveSyntheticAuth` | Surface local/self-hosted or config-backed auth without persisting plaintext | Provider can operate with a synthetic/local credential marker |
|
||||
| 9 | `shouldDeferSyntheticProfileAuth` | Lower stored synthetic profile placeholders behind env/config-backed auth | Provider stores synthetic placeholder profiles that should not win precedence |
|
||||
| 10 | `resolveDynamicModel` | Sync fallback for provider-owned model ids not in the local registry yet | Provider accepts arbitrary upstream model ids |
|
||||
| 11 | `prepareDynamicModel` | Async warm-up, then `resolveDynamicModel` runs again | Provider needs network metadata before resolving unknown ids |
|
||||
| 12 | `normalizeResolvedModel` | Final rewrite before the embedded runner uses the resolved model | Provider needs transport rewrites but still uses a core transport |
|
||||
| 13 | `contributeResolvedModelCompat` | Contribute compat flags for vendor models behind another compatible transport | Provider recognizes its own models on proxy transports without taking over the provider |
|
||||
| 14 | `capabilities` | Provider-owned transcript/tooling metadata used by shared core logic | Provider needs transcript/provider-family quirks |
|
||||
| 15 | `normalizeToolSchemas` | Normalize tool schemas before the embedded runner sees them | Provider needs transport-family schema cleanup |
|
||||
| 16 | `inspectToolSchemas` | Surface provider-owned schema diagnostics after normalization | Provider wants keyword warnings without teaching core provider-specific rules |
|
||||
| 17 | `resolveReasoningOutputMode` | Select native vs tagged reasoning-output contract | Provider needs tagged reasoning/final output instead of native fields |
|
||||
| 18 | `prepareExtraParams` | Request-param normalization before generic stream option wrappers | Provider needs default request params or per-provider param cleanup |
|
||||
| 19 | `createStreamFn` | Fully replace the normal stream path with a custom transport | Provider needs a custom wire protocol, not just a wrapper |
|
||||
| 20 | `wrapStreamFn` | Stream wrapper after generic wrappers are applied | Provider needs request headers/body/model compat wrappers without a custom transport |
|
||||
| 21 | `resolveTransportTurnState` | Attach native per-turn transport headers or metadata | Provider wants generic transports to send provider-native turn identity |
|
||||
| 22 | `resolveWebSocketSessionPolicy` | Attach native WebSocket headers or session cool-down policy | Provider wants generic WS transports to tune session headers or fallback policy |
|
||||
| 23 | `formatApiKey` | Auth-profile formatter: stored profile becomes the runtime `apiKey` string | Provider stores extra auth metadata and needs a custom runtime token shape |
|
||||
| 24 | `refreshOAuth` | OAuth refresh override for custom refresh endpoints or refresh-failure policy | Provider does not fit the shared `pi-ai` refreshers |
|
||||
| 25 | `buildAuthDoctorHint` | Repair hint appended when OAuth refresh fails | Provider needs provider-owned auth repair guidance after refresh failure |
|
||||
| 26 | `matchesContextOverflowError` | Provider-owned context-window overflow matcher | Provider has raw overflow errors generic heuristics would miss |
|
||||
| 27 | `classifyFailoverReason` | Provider-owned failover reason classification | Provider can map raw API/transport errors to rate-limit/overload/etc |
|
||||
| 28 | `isCacheTtlEligible` | Prompt-cache policy for proxy/backhaul providers | Provider needs proxy-specific cache TTL gating |
|
||||
| 29 | `buildMissingAuthMessage` | Replacement for the generic missing-auth recovery message | Provider needs a provider-specific missing-auth recovery hint |
|
||||
| 30 | `suppressBuiltInModel` | Stale upstream model suppression plus optional user-facing error hint | Provider needs to hide stale upstream rows or replace them with a vendor hint |
|
||||
| 31 | `augmentModelCatalog` | Synthetic/final catalog rows appended after discovery | Provider needs synthetic forward-compat rows in `models list` and pickers |
|
||||
| 32 | `isBinaryThinking` | On/off reasoning toggle for binary-thinking providers | Provider exposes only binary thinking on/off |
|
||||
| 33 | `supportsXHighThinking` | `xhigh` reasoning support for selected models | Provider wants `xhigh` on only a subset of models |
|
||||
| 34 | `resolveDefaultThinkingLevel` | Default `/think` level for a specific model family | Provider owns default `/think` policy for a model family |
|
||||
| 35 | `isModernModelRef` | Modern-model matcher for live profile filters and smoke selection | Provider owns live/smoke preferred-model matching |
|
||||
| 36 | `prepareRuntimeAuth` | Exchange a configured credential into the actual runtime token/key just before inference | Provider needs a token exchange or short-lived request credential |
|
||||
| 37 | `resolveUsageAuth` | Resolve usage/billing credentials for `/usage` and related status surfaces | Provider needs custom usage/quota token parsing or a different usage credential |
|
||||
| 38 | `fetchUsageSnapshot` | Fetch and normalize provider-specific usage/quota snapshots after auth is resolved | Provider needs a provider-specific usage endpoint or payload parser |
|
||||
| 39 | `createEmbeddingProvider` | Build a provider-owned embedding adapter for memory/search | Memory embedding behavior belongs with the provider plugin |
|
||||
| 40 | `buildReplayPolicy` | Return a replay policy controlling transcript handling for the provider | Provider needs custom transcript policy (for example, thinking-block stripping) |
|
||||
| 41 | `sanitizeReplayHistory` | Rewrite replay history after generic transcript cleanup | Provider needs provider-specific replay rewrites beyond shared compaction helpers |
|
||||
| 42 | `validateReplayTurns` | Final replay-turn validation or reshaping before the embedded runner | Provider transport needs stricter turn validation after generic sanitation |
|
||||
| 43 | `onModelSelected` | Run provider-owned post-selection side effects | Provider needs telemetry or provider-owned state when a model becomes active |
|
||||
| # | Hook | What it does | When to use |
|
||||
| --- | --------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | `catalog` | Publish provider config into `models.providers` during `models.json` generation | Provider owns a catalog or base URL defaults |
|
||||
| 2 | `applyConfigDefaults` | Apply provider-owned global config defaults during config materialization | Defaults depend on auth mode, env, or provider model-family semantics |
|
||||
| -- | _(built-in model lookup)_ | OpenClaw tries the normal registry/catalog path first | _(not a plugin hook)_ |
|
||||
| 3 | `normalizeModelId` | Normalize legacy or preview model-id aliases before lookup | Provider owns alias cleanup before canonical model resolution |
|
||||
| 4 | `normalizeTransport` | Normalize provider-family `api` / `baseUrl` before generic model assembly | Provider owns transport cleanup for custom provider ids in the same transport family |
|
||||
| 5 | `normalizeConfig` | Normalize `models.providers.<id>` before runtime/provider resolution | Provider needs config cleanup that should live with the plugin; bundled Google-family helpers also backstop supported Google config entries |
|
||||
| 6 | `applyNativeStreamingUsageCompat` | Apply native streaming-usage compat rewrites to config providers | Provider needs endpoint-driven native streaming usage metadata fixes |
|
||||
| 7 | `resolveConfigApiKey` | Resolve env-marker auth for config providers before runtime auth loading | Provider has provider-owned env-marker API-key resolution; `amazon-bedrock` also has a built-in AWS env-marker resolver here |
|
||||
| 8 | `resolveSyntheticAuth` | Surface local/self-hosted or config-backed auth without persisting plaintext | Provider can operate with a synthetic/local credential marker |
|
||||
| 9 | `resolveExternalOAuthProfiles` | Overlay external OAuth profiles; default `persistence` is `runtime-only` for CLI/app-owned creds | Provider reuses external OAuth credentials without persisting copied refresh tokens |
|
||||
| 10 | `shouldDeferSyntheticProfileAuth` | Lower stored synthetic profile placeholders behind env/config-backed auth | Provider stores synthetic placeholder profiles that should not win precedence |
|
||||
| 11 | `resolveDynamicModel` | Sync fallback for provider-owned model ids not in the local registry yet | Provider accepts arbitrary upstream model ids |
|
||||
| 12 | `prepareDynamicModel` | Async warm-up, then `resolveDynamicModel` runs again | Provider needs network metadata before resolving unknown ids |
|
||||
| 13 | `normalizeResolvedModel` | Final rewrite before the embedded runner uses the resolved model | Provider needs transport rewrites but still uses a core transport |
|
||||
| 14 | `contributeResolvedModelCompat` | Contribute compat flags for vendor models behind another compatible transport | Provider recognizes its own models on proxy transports without taking over the provider |
|
||||
| 15 | `capabilities` | Provider-owned transcript/tooling metadata used by shared core logic | Provider needs transcript/provider-family quirks |
|
||||
| 16 | `normalizeToolSchemas` | Normalize tool schemas before the embedded runner sees them | Provider needs transport-family schema cleanup |
|
||||
| 17 | `inspectToolSchemas` | Surface provider-owned schema diagnostics after normalization | Provider wants keyword warnings without teaching core provider-specific rules |
|
||||
| 18 | `resolveReasoningOutputMode` | Select native vs tagged reasoning-output contract | Provider needs tagged reasoning/final output instead of native fields |
|
||||
| 19 | `prepareExtraParams` | Request-param normalization before generic stream option wrappers | Provider needs default request params or per-provider param cleanup |
|
||||
| 20 | `createStreamFn` | Fully replace the normal stream path with a custom transport | Provider needs a custom wire protocol, not just a wrapper |
|
||||
| 21 | `wrapStreamFn` | Stream wrapper after generic wrappers are applied | Provider needs request headers/body/model compat wrappers without a custom transport |
|
||||
| 22 | `resolveTransportTurnState` | Attach native per-turn transport headers or metadata | Provider wants generic transports to send provider-native turn identity |
|
||||
| 23 | `resolveWebSocketSessionPolicy` | Attach native WebSocket headers or session cool-down policy | Provider wants generic WS transports to tune session headers or fallback policy |
|
||||
| 24 | `formatApiKey` | Auth-profile formatter: stored profile becomes the runtime `apiKey` string | Provider stores extra auth metadata and needs a custom runtime token shape |
|
||||
| 25 | `refreshOAuth` | OAuth refresh override for custom refresh endpoints or refresh-failure policy | Provider does not fit the shared `pi-ai` refreshers |
|
||||
| 26 | `buildAuthDoctorHint` | Repair hint appended when OAuth refresh fails | Provider needs provider-owned auth repair guidance after refresh failure |
|
||||
| 27 | `matchesContextOverflowError` | Provider-owned context-window overflow matcher | Provider has raw overflow errors generic heuristics would miss |
|
||||
| 28 | `classifyFailoverReason` | Provider-owned failover reason classification | Provider can map raw API/transport errors to rate-limit/overload/etc |
|
||||
| 29 | `isCacheTtlEligible` | Prompt-cache policy for proxy/backhaul providers | Provider needs proxy-specific cache TTL gating |
|
||||
| 30 | `buildMissingAuthMessage` | Replacement for the generic missing-auth recovery message | Provider needs a provider-specific missing-auth recovery hint |
|
||||
| 31 | `suppressBuiltInModel` | Stale upstream model suppression plus optional user-facing error hint | Provider needs to hide stale upstream rows or replace them with a vendor hint |
|
||||
| 32 | `augmentModelCatalog` | Synthetic/final catalog rows appended after discovery | Provider needs synthetic forward-compat rows in `models list` and pickers |
|
||||
| 33 | `isBinaryThinking` | On/off reasoning toggle for binary-thinking providers | Provider exposes only binary thinking on/off |
|
||||
| 34 | `supportsXHighThinking` | `xhigh` reasoning support for selected models | Provider wants `xhigh` on only a subset of models |
|
||||
| 35 | `resolveDefaultThinkingLevel` | Default `/think` level for a specific model family | Provider owns default `/think` policy for a model family |
|
||||
| 36 | `isModernModelRef` | Modern-model matcher for live profile filters and smoke selection | Provider owns live/smoke preferred-model matching |
|
||||
| 37 | `prepareRuntimeAuth` | Exchange a configured credential into the actual runtime token/key just before inference | Provider needs a token exchange or short-lived request credential |
|
||||
| 38 | `resolveUsageAuth` | Resolve usage/billing credentials for `/usage` and related status surfaces | Provider needs custom usage/quota token parsing or a different usage credential |
|
||||
| 39 | `fetchUsageSnapshot` | Fetch and normalize provider-specific usage/quota snapshots after auth is resolved | Provider needs a provider-specific usage endpoint or payload parser |
|
||||
| 40 | `createEmbeddingProvider` | Build a provider-owned embedding adapter for memory/search | Memory embedding behavior belongs with the provider plugin |
|
||||
| 41 | `buildReplayPolicy` | Return a replay policy controlling transcript handling for the provider | Provider needs custom transcript policy (for example, thinking-block stripping) |
|
||||
| 42 | `sanitizeReplayHistory` | Rewrite replay history after generic transcript cleanup | Provider needs provider-specific replay rewrites beyond shared compaction helpers |
|
||||
| 43 | `validateReplayTurns` | Final replay-turn validation or reshaping before the embedded runner | Provider transport needs stricter turn validation after generic sanitation |
|
||||
| 44 | `onModelSelected` | Run provider-owned post-selection side effects | Provider needs telemetry or provider-owned state when a model becomes active |
|
||||
|
||||
`normalizeModelId`, `normalizeTransport`, and `normalizeConfig` first check the
|
||||
matched provider plugin, then fall through other hook-capable provider plugins
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
type CodexJwtPayload = {
|
||||
exp?: unknown;
|
||||
iss?: unknown;
|
||||
sub?: unknown;
|
||||
"https://api.openai.com/profile"?: {
|
||||
@@ -19,6 +20,16 @@ function normalizeNonEmptyString(value: unknown): string | undefined {
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function normalizeFutureEpochSeconds(value: unknown): number | undefined {
|
||||
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
||||
return Math.trunc(value);
|
||||
}
|
||||
if (typeof value === "string" && /^\d+$/.test(value.trim())) {
|
||||
return Number.parseInt(value.trim(), 10);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function decodeCodexJwtPayload(accessToken: string): CodexJwtPayload | null {
|
||||
const parts = accessToken.split(".");
|
||||
if (parts.length !== 3) {
|
||||
@@ -55,6 +66,12 @@ export function resolveCodexStableSubject(payload: CodexJwtPayload | null): stri
|
||||
return sub;
|
||||
}
|
||||
|
||||
export function resolveCodexAccessTokenExpiry(accessToken: string): number | undefined {
|
||||
const payload = decodeCodexJwtPayload(accessToken);
|
||||
const exp = normalizeFutureEpochSeconds(payload?.exp);
|
||||
return exp ? exp * 1000 : undefined;
|
||||
}
|
||||
|
||||
export function resolveCodexAuthIdentity(params: { accessToken: string; email?: string | null }): {
|
||||
email?: string;
|
||||
profileName?: string;
|
||||
|
||||
84
extensions/openai/openai-codex-cli-auth.test.ts
Normal file
84
extensions/openai/openai-codex-cli-auth.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import fs from "node:fs";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
readOpenAICodexCliOAuthProfile,
|
||||
} from "./openai-codex-cli-auth.js";
|
||||
|
||||
function buildJwt(payload: Record<string, unknown>) {
|
||||
const encode = (value: Record<string, unknown>) =>
|
||||
Buffer.from(JSON.stringify(value)).toString("base64url");
|
||||
return `${encode({ alg: "none", typ: "JWT" })}.${encode(payload)}.sig`;
|
||||
}
|
||||
|
||||
describe("readOpenAICodexCliOAuthProfile", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("reads Codex CLI chatgpt auth into the default OpenAI Codex profile", () => {
|
||||
const accessToken = buildJwt({
|
||||
exp: Math.floor(Date.now() / 1000) + 600,
|
||||
"https://api.openai.com/profile": {
|
||||
email: "codex@example.com",
|
||||
},
|
||||
});
|
||||
vi.spyOn(fs, "readFileSync").mockReturnValue(
|
||||
JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
access_token: accessToken,
|
||||
refresh_token: "refresh-token",
|
||||
account_id: "acct_123",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: { version: 1, profiles: {} },
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: accessToken,
|
||||
refresh: "refresh-token",
|
||||
accountId: "acct_123",
|
||||
email: "codex@example.com",
|
||||
managedBy: "codex-cli",
|
||||
},
|
||||
});
|
||||
expect(parsed?.credential.expires).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
it("does not override a locally managed OpenAI Codex profile", () => {
|
||||
vi.spyOn(fs, "readFileSync").mockReturnValue(
|
||||
JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
access_token: "access-token",
|
||||
refresh_token: "refresh-token",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const parsed = readOpenAICodexCliOAuthProfile({
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[OPENAI_CODEX_DEFAULT_PROFILE_ID]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "local-access",
|
||||
refresh: "local-refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toBeNull();
|
||||
});
|
||||
});
|
||||
99
extensions/openai/openai-codex-cli-auth.ts
Normal file
99
extensions/openai/openai-codex-cli-auth.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { AuthProfileStore, OAuthCredential } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveRequiredHomeDir } from "openclaw/plugin-sdk/provider-auth";
|
||||
import {
|
||||
resolveCodexAccessTokenExpiry,
|
||||
resolveCodexAuthIdentity,
|
||||
} from "./openai-codex-auth-identity.js";
|
||||
|
||||
const PROVIDER_ID = "openai-codex";
|
||||
const CODEX_CLI_MANAGED_BY = "codex-cli";
|
||||
|
||||
export const CODEX_CLI_PROFILE_ID = `${PROVIDER_ID}:codex-cli`;
|
||||
export const OPENAI_CODEX_DEFAULT_PROFILE_ID = `${PROVIDER_ID}:default`;
|
||||
|
||||
type CodexCliAuthFile = {
|
||||
auth_mode?: unknown;
|
||||
tokens?: {
|
||||
access_token?: unknown;
|
||||
refresh_token?: unknown;
|
||||
account_id?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
function trimNonEmptyString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function resolveCodexCliHome(env: NodeJS.ProcessEnv): string {
|
||||
const configured = trimNonEmptyString(env.CODEX_HOME);
|
||||
if (!configured) {
|
||||
return path.join(resolveRequiredHomeDir(), ".codex");
|
||||
}
|
||||
if (configured === "~") {
|
||||
return resolveRequiredHomeDir();
|
||||
}
|
||||
if (configured.startsWith("~/")) {
|
||||
return path.join(resolveRequiredHomeDir(), configured.slice(2));
|
||||
}
|
||||
return path.resolve(configured);
|
||||
}
|
||||
|
||||
function readCodexCliAuthFile(env: NodeJS.ProcessEnv): CodexCliAuthFile | null {
|
||||
try {
|
||||
const authPath = path.join(resolveCodexCliHome(env), "auth.json");
|
||||
const raw = fs.readFileSync(authPath, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed && typeof parsed === "object" ? (parsed as CodexCliAuthFile) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function readOpenAICodexCliOAuthProfile(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
store: AuthProfileStore;
|
||||
}): { profileId: string; credential: OAuthCredential } | null {
|
||||
const existing = params.store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID];
|
||||
if (
|
||||
existing?.type === "oauth" &&
|
||||
existing.provider === PROVIDER_ID &&
|
||||
existing.managedBy !== CODEX_CLI_MANAGED_BY
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const authFile = readCodexCliAuthFile(params.env ?? process.env);
|
||||
if (!authFile || authFile.auth_mode !== "chatgpt") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const access = trimNonEmptyString(authFile.tokens?.access_token);
|
||||
const refresh = trimNonEmptyString(authFile.tokens?.refresh_token);
|
||||
if (!access || !refresh) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accountId = trimNonEmptyString(authFile.tokens?.account_id);
|
||||
const identity = resolveCodexAuthIdentity({ accessToken: access });
|
||||
|
||||
return {
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: PROVIDER_ID,
|
||||
access,
|
||||
refresh,
|
||||
expires: resolveCodexAccessTokenExpiry(access) ?? 0,
|
||||
...(accountId ? { accountId } : {}),
|
||||
...(identity.email ? { email: identity.email } : {}),
|
||||
...(identity.profileName ? { displayName: identity.profileName } : {}),
|
||||
managedBy: CODEX_CLI_MANAGED_BY,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
ProviderRuntimeModel,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
type OAuthCredential,
|
||||
@@ -22,6 +21,7 @@ import { fetchCodexUsage } from "openclaw/plugin-sdk/provider-usage";
|
||||
import { OPENAI_CODEX_DEFAULT_MODEL } from "./default-models.js";
|
||||
import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js";
|
||||
import { buildOpenAICodexProvider } from "./openai-codex-catalog.js";
|
||||
import { CODEX_CLI_PROFILE_ID, readOpenAICodexCliOAuthProfile } from "./openai-codex-cli-auth.js";
|
||||
import { buildOpenAIReplayPolicy } from "./replay-policy.js";
|
||||
import {
|
||||
cloneFirstTemplateModel,
|
||||
@@ -303,6 +303,13 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin {
|
||||
},
|
||||
resolveDynamicModel: (ctx) => resolveCodexForwardCompatModel(ctx),
|
||||
buildAuthDoctorHint: (ctx) => buildOpenAICodexAuthDoctorHint(ctx),
|
||||
resolveExternalOAuthProfiles: (ctx) => {
|
||||
const profile = readOpenAICodexCliOAuthProfile({
|
||||
env: ctx.env,
|
||||
store: ctx.store,
|
||||
});
|
||||
return profile ? [{ ...profile, persistence: "runtime-only" }] : undefined;
|
||||
},
|
||||
supportsXHighThinking: ({ modelId }) =>
|
||||
matchesExactOrPrefix(modelId, OPENAI_CODEX_XHIGH_MODEL_IDS),
|
||||
isModernModelRef: ({ modelId }) => matchesExactOrPrefix(modelId, OPENAI_CODEX_MODERN_MODEL_IDS),
|
||||
|
||||
@@ -5,7 +5,6 @@ export const AUTH_PROFILE_FILENAME = "auth-profiles.json";
|
||||
export const LEGACY_AUTH_FILENAME = "auth.json";
|
||||
|
||||
export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli";
|
||||
export const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default";
|
||||
|
||||
export const AUTH_STORE_LOCK_OPTIONS = {
|
||||
retries: {
|
||||
|
||||
110
src/agents/auth-profiles/external-oauth.test.ts
Normal file
110
src/agents/auth-profiles/external-oauth.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderExternalOAuthProfile } from "../../plugins/types.js";
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
|
||||
const resolveExternalOAuthProfilesWithPluginsMock = vi.fn<
|
||||
(params: unknown) => ProviderExternalOAuthProfile[]
|
||||
>(() => []);
|
||||
|
||||
vi.mock("../../plugins/provider-runtime.js", () => ({
|
||||
resolveExternalOAuthProfilesWithPlugins: (params: unknown) =>
|
||||
resolveExternalOAuthProfilesWithPluginsMock(params),
|
||||
}));
|
||||
|
||||
function createStore(profiles: AuthProfileStore["profiles"] = {}): AuthProfileStore {
|
||||
return { version: 1, profiles };
|
||||
}
|
||||
|
||||
function createCredential(overrides: Partial<OAuthCredential> = {}): OAuthCredential {
|
||||
return {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: 123,
|
||||
managedBy: "codex-cli",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("auth external oauth helpers", () => {
|
||||
beforeEach(() => {
|
||||
resolveExternalOAuthProfilesWithPluginsMock.mockReset();
|
||||
});
|
||||
|
||||
it("overlays provider-managed runtime oauth profiles onto the store", async () => {
|
||||
resolveExternalOAuthProfilesWithPluginsMock.mockReturnValueOnce([
|
||||
{
|
||||
profileId: "openai-codex:default",
|
||||
credential: createCredential(),
|
||||
},
|
||||
]);
|
||||
|
||||
const { overlayExternalOAuthProfiles } = await import("./external-oauth.js");
|
||||
const store = overlayExternalOAuthProfiles(createStore());
|
||||
|
||||
expect(store.profiles["openai-codex:default"]).toMatchObject({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits exact runtime-only overlays from persisted store writes", async () => {
|
||||
const credential = createCredential();
|
||||
resolveExternalOAuthProfilesWithPluginsMock.mockReturnValueOnce([
|
||||
{
|
||||
profileId: "openai-codex:default",
|
||||
credential,
|
||||
},
|
||||
]);
|
||||
|
||||
const { shouldPersistExternalOAuthProfile } = await import("./external-oauth.js");
|
||||
const shouldPersist = shouldPersistExternalOAuthProfile({
|
||||
store: createStore({ "openai-codex:default": credential }),
|
||||
profileId: "openai-codex:default",
|
||||
credential,
|
||||
});
|
||||
|
||||
expect(shouldPersist).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps persisted copies when the external overlay is marked persisted", async () => {
|
||||
const credential = createCredential();
|
||||
resolveExternalOAuthProfilesWithPluginsMock.mockReturnValueOnce([
|
||||
{
|
||||
profileId: "openai-codex:default",
|
||||
credential,
|
||||
persistence: "persisted",
|
||||
},
|
||||
]);
|
||||
|
||||
const { shouldPersistExternalOAuthProfile } = await import("./external-oauth.js");
|
||||
const shouldPersist = shouldPersistExternalOAuthProfile({
|
||||
store: createStore({ "openai-codex:default": credential }),
|
||||
profileId: "openai-codex:default",
|
||||
credential,
|
||||
});
|
||||
|
||||
expect(shouldPersist).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps stale local copies when runtime overlay no longer matches", async () => {
|
||||
const credential = createCredential();
|
||||
resolveExternalOAuthProfilesWithPluginsMock.mockReturnValueOnce([
|
||||
{
|
||||
profileId: "openai-codex:default",
|
||||
credential: createCredential({ access: "fresh-access-token" }),
|
||||
},
|
||||
]);
|
||||
|
||||
const { shouldPersistExternalOAuthProfile } = await import("./external-oauth.js");
|
||||
const shouldPersist = shouldPersistExternalOAuthProfile({
|
||||
store: createStore({ "openai-codex:default": credential }),
|
||||
profileId: "openai-codex:default",
|
||||
credential,
|
||||
});
|
||||
|
||||
expect(shouldPersist).toBe(true);
|
||||
});
|
||||
});
|
||||
100
src/agents/auth-profiles/external-oauth.ts
Normal file
100
src/agents/auth-profiles/external-oauth.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { resolveExternalOAuthProfilesWithPlugins } from "../../plugins/provider-runtime.js";
|
||||
import type { ProviderExternalOAuthProfile } from "../../plugins/types.js";
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
|
||||
type ExternalOAuthProfileMap = Map<string, ProviderExternalOAuthProfile>;
|
||||
|
||||
function normalizeExternalOAuthProfile(
|
||||
profile: ProviderExternalOAuthProfile,
|
||||
): ProviderExternalOAuthProfile | null {
|
||||
if (!profile?.profileId || !profile.credential) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...profile,
|
||||
persistence: profile.persistence ?? "runtime-only",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveExternalOAuthProfileMap(params: {
|
||||
store: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ExternalOAuthProfileMap {
|
||||
const env = params.env ?? process.env;
|
||||
const profiles = resolveExternalOAuthProfilesWithPlugins({
|
||||
env,
|
||||
context: {
|
||||
config: undefined,
|
||||
agentDir: params.agentDir,
|
||||
workspaceDir: undefined,
|
||||
env,
|
||||
store: params.store,
|
||||
},
|
||||
});
|
||||
|
||||
const resolved: ExternalOAuthProfileMap = new Map();
|
||||
for (const rawProfile of profiles) {
|
||||
const profile = normalizeExternalOAuthProfile(rawProfile);
|
||||
if (!profile) {
|
||||
continue;
|
||||
}
|
||||
resolved.set(profile.profileId, profile);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function oauthCredentialMatches(a: OAuthCredential, b: OAuthCredential): boolean {
|
||||
return (
|
||||
a.type === b.type &&
|
||||
a.provider === b.provider &&
|
||||
a.access === b.access &&
|
||||
a.refresh === b.refresh &&
|
||||
a.expires === b.expires &&
|
||||
a.clientId === b.clientId &&
|
||||
a.email === b.email &&
|
||||
a.displayName === b.displayName &&
|
||||
a.enterpriseUrl === b.enterpriseUrl &&
|
||||
a.projectId === b.projectId &&
|
||||
a.accountId === b.accountId &&
|
||||
a.managedBy === b.managedBy
|
||||
);
|
||||
}
|
||||
|
||||
export function overlayExternalOAuthProfiles(
|
||||
store: AuthProfileStore,
|
||||
params?: { agentDir?: string; env?: NodeJS.ProcessEnv },
|
||||
): AuthProfileStore {
|
||||
const profiles = resolveExternalOAuthProfileMap({
|
||||
store,
|
||||
agentDir: params?.agentDir,
|
||||
env: params?.env,
|
||||
});
|
||||
if (profiles.size === 0) {
|
||||
return store;
|
||||
}
|
||||
|
||||
const next = structuredClone(store);
|
||||
for (const [profileId, profile] of profiles) {
|
||||
next.profiles[profileId] = profile.credential;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function shouldPersistExternalOAuthProfile(params: {
|
||||
store: AuthProfileStore;
|
||||
profileId: string;
|
||||
credential: OAuthCredential;
|
||||
agentDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const external = resolveExternalOAuthProfileMap({
|
||||
store: params.store,
|
||||
agentDir: params.agentDir,
|
||||
env: params.env,
|
||||
}).get(params.profileId);
|
||||
if (!external || external.persistence === "persisted") {
|
||||
return true;
|
||||
}
|
||||
return !oauthCredentialMatches(external.credential, params.credential);
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import { coerceSecretRef } from "../../config/types.secrets.js";
|
||||
import { withFileLock } from "../../infra/file-lock.js";
|
||||
import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, log } from "./constants.js";
|
||||
import {
|
||||
overlayExternalOAuthProfiles,
|
||||
shouldPersistExternalOAuthProfile,
|
||||
} from "./external-oauth.js";
|
||||
import { ensureAuthStoreFile, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js";
|
||||
import type {
|
||||
AuthProfileCredential,
|
||||
@@ -348,9 +352,24 @@ function mergeAuthProfileStores(
|
||||
};
|
||||
}
|
||||
|
||||
function buildPersistedAuthProfileStore(store: AuthProfileStore): AuthProfileStore {
|
||||
function buildPersistedAuthProfileStore(
|
||||
store: AuthProfileStore,
|
||||
params?: { agentDir?: string },
|
||||
): AuthProfileStore {
|
||||
const profiles = Object.fromEntries(
|
||||
Object.entries(store.profiles).flatMap(([profileId, credential]) => {
|
||||
if (
|
||||
credential.type === "oauth" &&
|
||||
!shouldPersistExternalOAuthProfile({
|
||||
store,
|
||||
profileId,
|
||||
credential,
|
||||
agentDir: params?.agentDir,
|
||||
})
|
||||
) {
|
||||
// Provider-managed external OAuth profiles are runtime-only overlays.
|
||||
return [];
|
||||
}
|
||||
if (credential.type === "api_key" && credential.keyRef && credential.key !== undefined) {
|
||||
const sanitized = { ...credential } as Record<string, unknown>;
|
||||
delete sanitized.key;
|
||||
@@ -445,7 +464,7 @@ export function loadAuthProfileStore(): AuthProfileStore {
|
||||
const authPath = resolveAuthStorePath();
|
||||
const asStore = loadCoercedStore(authPath);
|
||||
if (asStore) {
|
||||
return asStore;
|
||||
return overlayExternalOAuthProfiles(asStore);
|
||||
}
|
||||
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath());
|
||||
const legacy = coerceLegacyStore(legacyRaw);
|
||||
@@ -455,10 +474,10 @@ export function loadAuthProfileStore(): AuthProfileStore {
|
||||
profiles: {},
|
||||
};
|
||||
applyLegacyStore(store, legacy);
|
||||
return store;
|
||||
return overlayExternalOAuthProfiles(store);
|
||||
}
|
||||
|
||||
return { version: AUTH_STORE_VERSION, profiles: {} };
|
||||
return overlayExternalOAuthProfiles({ version: AUTH_STORE_VERSION, profiles: {} });
|
||||
}
|
||||
|
||||
function loadAuthProfileStoreForAgent(
|
||||
@@ -543,11 +562,13 @@ export function loadAuthProfileStoreForRuntime(
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
const mainAuthPath = resolveAuthStorePath();
|
||||
if (!agentDir || authPath === mainAuthPath) {
|
||||
return store;
|
||||
return overlayExternalOAuthProfiles(store, { agentDir });
|
||||
}
|
||||
|
||||
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
|
||||
return mergeAuthProfileStores(mainStore, store);
|
||||
return overlayExternalOAuthProfiles(mergeAuthProfileStores(mainStore, store), {
|
||||
agentDir,
|
||||
});
|
||||
}
|
||||
|
||||
export function loadAuthProfileStoreForSecretsRuntime(agentDir?: string): AuthProfileStore {
|
||||
@@ -560,26 +581,26 @@ export function ensureAuthProfileStore(
|
||||
): AuthProfileStore {
|
||||
const runtimeStore = resolveRuntimeAuthProfileStore(agentDir);
|
||||
if (runtimeStore) {
|
||||
return runtimeStore;
|
||||
return overlayExternalOAuthProfiles(runtimeStore, { agentDir });
|
||||
}
|
||||
|
||||
const store = loadAuthProfileStoreForAgent(agentDir, options);
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
const mainAuthPath = resolveAuthStorePath();
|
||||
if (!agentDir || authPath === mainAuthPath) {
|
||||
return store;
|
||||
return overlayExternalOAuthProfiles(store, { agentDir });
|
||||
}
|
||||
|
||||
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
|
||||
const merged = mergeAuthProfileStores(mainStore, store);
|
||||
|
||||
return merged;
|
||||
return overlayExternalOAuthProfiles(merged, { agentDir });
|
||||
}
|
||||
|
||||
export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string): void {
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
const runtimeKey = resolveRuntimeStoreKey(agentDir);
|
||||
const payload = buildPersistedAuthProfileStore(store);
|
||||
const payload = buildPersistedAuthProfileStore(store, { agentDir });
|
||||
saveJsonFile(authPath, payload);
|
||||
const runtimeStore = cloneAuthProfileStore(store);
|
||||
writeCachedAuthProfileStore(authPath, readAuthStoreMtimeMs(authPath), runtimeStore);
|
||||
|
||||
@@ -49,6 +49,7 @@ let resolveProviderDefaultThinkingLevel: typeof import("./provider-runtime.js").
|
||||
let resolveProviderModernModelRef: typeof import("./provider-runtime.js").resolveProviderModernModelRef;
|
||||
let resolveProviderReasoningOutputModeWithPlugin: typeof import("./provider-runtime.js").resolveProviderReasoningOutputModeWithPlugin;
|
||||
let resolveProviderReplayPolicyWithPlugin: typeof import("./provider-runtime.js").resolveProviderReplayPolicyWithPlugin;
|
||||
let resolveExternalOAuthProfilesWithPlugins: typeof import("./provider-runtime.js").resolveExternalOAuthProfilesWithPlugins;
|
||||
let resolveProviderSyntheticAuthWithPlugin: typeof import("./provider-runtime.js").resolveProviderSyntheticAuthWithPlugin;
|
||||
let shouldDeferProviderSyntheticProfileAuthWithPlugin: typeof import("./provider-runtime.js").shouldDeferProviderSyntheticProfileAuthWithPlugin;
|
||||
let sanitizeProviderReplayHistoryWithPlugin: typeof import("./provider-runtime.js").sanitizeProviderReplayHistoryWithPlugin;
|
||||
@@ -256,6 +257,7 @@ describe("provider-runtime", () => {
|
||||
resolveProviderModernModelRef,
|
||||
resolveProviderReasoningOutputModeWithPlugin,
|
||||
resolveProviderReplayPolicyWithPlugin,
|
||||
resolveExternalOAuthProfilesWithPlugins,
|
||||
resolveProviderSyntheticAuthWithPlugin,
|
||||
shouldDeferProviderSyntheticProfileAuthWithPlugin,
|
||||
sanitizeProviderReplayHistoryWithPlugin,
|
||||
@@ -644,6 +646,23 @@ describe("provider-runtime", () => {
|
||||
},
|
||||
createEmbeddingProvider,
|
||||
resolveSyntheticAuth,
|
||||
resolveExternalOAuthProfiles: ({ store }) =>
|
||||
store.profiles["demo:managed"]
|
||||
? []
|
||||
: [
|
||||
{
|
||||
persistence: "runtime-only",
|
||||
profileId: "demo:managed",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: DEMO_PROVIDER_ID,
|
||||
access: "external-access",
|
||||
refresh: "external-refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
managedBy: "demo-cli",
|
||||
},
|
||||
},
|
||||
],
|
||||
shouldDeferSyntheticProfileAuth,
|
||||
normalizeResolvedModel: ({ model }) => ({
|
||||
...model,
|
||||
@@ -1035,6 +1054,30 @@ describe("provider-runtime", () => {
|
||||
}),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
actual: () =>
|
||||
resolveExternalOAuthProfilesWithPlugins({
|
||||
env: process.env,
|
||||
context: {
|
||||
env: process.env,
|
||||
store: { version: 1, profiles: {} },
|
||||
},
|
||||
}),
|
||||
expected: [
|
||||
{
|
||||
persistence: "runtime-only",
|
||||
profileId: "demo:managed",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: DEMO_PROVIDER_ID,
|
||||
access: "external-access",
|
||||
refresh: "external-refresh",
|
||||
expires: expect.any(Number),
|
||||
managedBy: "demo-cli",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actual: () =>
|
||||
resolveProviderSyntheticAuthWithPlugin({
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
ProviderCacheTtlEligibilityContext,
|
||||
ProviderCreateEmbeddingProviderContext,
|
||||
ProviderDeferSyntheticProfileAuthContext,
|
||||
ProviderExternalOAuthProfile,
|
||||
ProviderResolveSyntheticAuthContext,
|
||||
ProviderCreateStreamFnContext,
|
||||
ProviderDefaultThinkingPolicyContext,
|
||||
@@ -33,6 +34,7 @@ import type {
|
||||
ProviderModernModelPolicyContext,
|
||||
ProviderPrepareExtraParamsContext,
|
||||
ProviderPrepareDynamicModelContext,
|
||||
ProviderResolveExternalOAuthProfilesContext,
|
||||
ProviderPrepareRuntimeAuthContext,
|
||||
ProviderApplyConfigDefaultsContext,
|
||||
ProviderResolveConfigApiKeyContext,
|
||||
@@ -789,6 +791,23 @@ export function resolveProviderSyntheticAuthWithPlugin(params: {
|
||||
return resolveProviderRuntimePlugin(params)?.resolveSyntheticAuth?.(params.context) ?? undefined;
|
||||
}
|
||||
|
||||
export function resolveExternalOAuthProfilesWithPlugins(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderResolveExternalOAuthProfilesContext;
|
||||
}): ProviderExternalOAuthProfile[] {
|
||||
const matches: ProviderExternalOAuthProfile[] = [];
|
||||
for (const plugin of resolveProviderPluginsForHooks(params)) {
|
||||
const profiles = plugin.resolveExternalOAuthProfiles?.(params.context);
|
||||
if (!profiles || profiles.length === 0) {
|
||||
continue;
|
||||
}
|
||||
matches.push(...profiles);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function shouldDeferProviderSyntheticProfileAuthWithPlugin(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
|
||||
@@ -1040,6 +1040,20 @@ export type ProviderSyntheticAuthResult = {
|
||||
mode: Exclude<ModelProviderAuthMode, "aws-sdk">;
|
||||
};
|
||||
|
||||
export type ProviderResolveExternalOAuthProfilesContext = {
|
||||
config?: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
store: AuthProfileStore;
|
||||
};
|
||||
|
||||
export type ProviderExternalOAuthProfile = {
|
||||
profileId: string;
|
||||
credential: OAuthCredential;
|
||||
persistence?: "runtime-only" | "persisted";
|
||||
};
|
||||
|
||||
export type ProviderDeferSyntheticProfileAuthContext = {
|
||||
config?: OpenClawConfig;
|
||||
provider: string;
|
||||
@@ -1517,6 +1531,20 @@ export type ProviderPlugin = {
|
||||
resolveSyntheticAuth?: (
|
||||
ctx: ProviderResolveSyntheticAuthContext,
|
||||
) => ProviderSyntheticAuthResult | null | undefined;
|
||||
/**
|
||||
* Provider-owned external OAuth profile discovery.
|
||||
*
|
||||
* Use this when credentials are managed by an external tool and should be
|
||||
* visible to runtime auth resolution without being written back into
|
||||
* `auth-profiles.json` by core.
|
||||
*/
|
||||
resolveExternalOAuthProfiles?: (
|
||||
ctx: ProviderResolveExternalOAuthProfilesContext,
|
||||
) =>
|
||||
| Array<ProviderExternalOAuthProfile>
|
||||
| ReadonlyArray<ProviderExternalOAuthProfile>
|
||||
| null
|
||||
| undefined;
|
||||
/**
|
||||
* Provider-owned precedence rule for stored synthetic auth profiles.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user