mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
fix: treat aws sdk auth profiles as config metadata
This commit is contained in:
@@ -160,8 +160,8 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541.
|
||||
- Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with documented fallback signatures, accept legacy `__env__:VAR` custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858.
|
||||
- Telegram/models: parse provider ids containing dots in `/models` callback buttons so `hf.co` model lists render as inline keyboard buttons. Fixes #38745.
|
||||
- Auth profiles/Bedrock: accept persisted `type: "aws-sdk"` auth profiles so EC2/IMDS and shared AWS credential-chain Bedrock setups are not dropped as `invalid_type`. Fixes #69708.
|
||||
- Amazon Bedrock: refresh shared AWS profile/config file credentials before Bedrock model, discovery, and embedding requests so long-running Gateway processes pick up renewed profile credentials without restart. Fixes #77551.
|
||||
- Amazon Bedrock: treat named `aws-sdk` auth profiles as config routing metadata instead of stored credentials, and let `doctor --fix` move legacy markers out of `auth-profiles.json`. Fixes #69708.
|
||||
- Anthropic: reject uppercase provider-prefixed forward-compat model ids locally instead of sending malformed dynamic ids upstream. Fixes #73715.
|
||||
- OpenAI/embeddings: pass configured output dimensionality through single and batched embedding requests so memory embedding indexes can request smaller vectors. Fixes #55126.
|
||||
- CLI/infer: normalize HEIC/HEIF image files to JPEG before model-run requests, avoiding providers that reject Apple image container formats. Fixes #50081.
|
||||
|
||||
@@ -62,6 +62,18 @@ Explicit copy flows, such as `openclaw agents add`, use this portability policy:
|
||||
Non-portable profiles remain available through read-through inheritance unless
|
||||
the target agent signs in separately and creates its own local profile.
|
||||
|
||||
## Config-only auth routes
|
||||
|
||||
`auth.profiles` entries with `mode: "aws-sdk"` are routing metadata, not stored
|
||||
credentials. They are valid when the target provider uses
|
||||
`models.providers.<id>.auth: "aws-sdk"` or the built-in Amazon Bedrock default
|
||||
AWS SDK route. These profile ids may appear in `auth.order` and session
|
||||
overrides even when no matching entry exists in `auth-profiles.json`.
|
||||
|
||||
Do not write `type: "aws-sdk"` into `auth-profiles.json`. If a legacy install
|
||||
has such a marker, `openclaw doctor --fix` moves it to `auth.profiles` and
|
||||
removes the marker from the credential store.
|
||||
|
||||
## Explicit auth order filtering
|
||||
|
||||
- When `auth.order.<provider>` or the auth-store order override is set for a
|
||||
|
||||
@@ -110,6 +110,8 @@ openclaw models auth paste-token --provider openrouter
|
||||
|
||||
OpenClaw expects the canonical `version` + `profiles` shape at runtime. If an older install still has a flat file such as `{ "openrouter": { "apiKey": "..." } }`, run `openclaw doctor --fix` to rewrite it as an `openrouter:default` API-key profile; doctor keeps a `.legacy-flat.*.bak` copy beside the original. Endpoint details such as `baseUrl`, `api`, model ids, headers, and timeouts belong under `models.providers.<id>` in `openclaw.json` or `models.json`, not in `auth-profiles.json`.
|
||||
|
||||
External auth routes such as Bedrock `auth: "aws-sdk"` are also not credentials. If you want a named Bedrock route, put `auth.profiles.<id>.mode: "aws-sdk"` in `openclaw.json`; do not write `type: "aws-sdk"` into `auth-profiles.json`. `openclaw doctor --fix` moves legacy AWS SDK markers from the credential store into config metadata.
|
||||
|
||||
Auth profile refs are also supported for static credentials:
|
||||
|
||||
- `api_key` credentials can use `keyRef: { source, provider, id }`
|
||||
|
||||
@@ -17,7 +17,7 @@ export type AuthProfileHealthStatus = "ok" | "expiring" | "expired" | "missing"
|
||||
type AuthProfileHealth = {
|
||||
profileId: string;
|
||||
provider: string;
|
||||
type: "oauth" | "token" | "api_key" | "aws-sdk";
|
||||
type: "oauth" | "token" | "api_key";
|
||||
status: AuthProfileHealthStatus;
|
||||
reasonCode?: AuthCredentialReasonCode;
|
||||
expiresAt?: number;
|
||||
@@ -127,17 +127,6 @@ function buildProfileHealth(params: {
|
||||
};
|
||||
}
|
||||
|
||||
if (healthCredential.type === "aws-sdk") {
|
||||
return {
|
||||
profileId,
|
||||
provider,
|
||||
type: "aws-sdk",
|
||||
status: "static",
|
||||
source,
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
if (healthCredential.type === "token") {
|
||||
const eligibility = evaluateStoredCredentialEligibility({
|
||||
credential: healthCredential,
|
||||
|
||||
@@ -996,25 +996,6 @@ describe("ensureAuthProfileStore", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts aws-sdk auth profiles without static credential material (#69708)", () => {
|
||||
withTempAgentDir("openclaw-auth-aws-sdk-", (agentDir) => {
|
||||
writeAuthProfileStore(agentDir, {
|
||||
"amazon-bedrock:default": {
|
||||
type: "aws-sdk",
|
||||
provider: "amazon-bedrock",
|
||||
createdAt: "2026-03-15T10:00:00.000Z",
|
||||
},
|
||||
});
|
||||
|
||||
const profile = loadAuthProfile(agentDir, "amazon-bedrock:default");
|
||||
|
||||
expect(profile).toMatchObject({
|
||||
type: "aws-sdk",
|
||||
provider: "amazon-bedrock",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "migrates SecretRef object in `key` to `keyRef` and clears `key`",
|
||||
|
||||
@@ -42,6 +42,67 @@ describe("resolveAuthProfileOrder", () => {
|
||||
const store = ANTHROPIC_STORE;
|
||||
const cfg = ANTHROPIC_CFG;
|
||||
|
||||
it("keeps config-only aws-sdk profiles for aws-sdk providers", () => {
|
||||
const order = resolveAuthProfileOrder({
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
"amazon-bedrock": {
|
||||
auth: "aws-sdk",
|
||||
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
api: "bedrock-converse-stream",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
order: {
|
||||
"amazon-bedrock": ["amazon-bedrock:default"],
|
||||
},
|
||||
profiles: {
|
||||
"amazon-bedrock:default": {
|
||||
provider: "amazon-bedrock",
|
||||
mode: "aws-sdk",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
store: { version: 1, profiles: {} },
|
||||
provider: "amazon-bedrock",
|
||||
});
|
||||
|
||||
expect(order).toEqual(["amazon-bedrock:default"]);
|
||||
});
|
||||
|
||||
it("rejects config-only aws-sdk profiles for non aws-sdk providers", () => {
|
||||
const order = resolveAuthProfileOrder({
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
anthropic: {
|
||||
auth: "api-key",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
api: "anthropic-messages",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
profiles: {
|
||||
"anthropic:aws": {
|
||||
provider: "anthropic",
|
||||
mode: "aws-sdk",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
store: { version: 1, profiles: {} },
|
||||
provider: "anthropic",
|
||||
});
|
||||
|
||||
expect(order).toEqual([]);
|
||||
});
|
||||
|
||||
function resolveWithAnthropicOrderAndUsage(params: {
|
||||
orderSource: "store" | "config";
|
||||
usageStats: NonNullable<AuthProfileStore["usageStats"]>;
|
||||
|
||||
@@ -16,7 +16,11 @@ export {
|
||||
type ExternalCliAuthDiscovery,
|
||||
} from "./auth-profiles/external-cli-discovery.js";
|
||||
export { resolveApiKeyForProfile } from "./auth-profiles/oauth.js";
|
||||
export { resolveAuthProfileEligibility, resolveAuthProfileOrder } from "./auth-profiles/order.js";
|
||||
export {
|
||||
isConfiguredAwsSdkAuthProfileForProvider,
|
||||
resolveAuthProfileEligibility,
|
||||
resolveAuthProfileOrder,
|
||||
} from "./auth-profiles/order.js";
|
||||
export {
|
||||
resolveAuthStatePathForDisplay,
|
||||
resolveAuthStorePathForDisplay,
|
||||
|
||||
@@ -85,10 +85,6 @@ export function evaluateStoredCredentialEligibility(params: {
|
||||
return { eligible: true, reasonCode: "ok" };
|
||||
}
|
||||
|
||||
if (credential.type === "aws-sdk") {
|
||||
return { eligible: true, reasonCode: "ok" };
|
||||
}
|
||||
|
||||
if (credential.type === "token") {
|
||||
const hasToken = hasConfiguredSecretString(credential.token);
|
||||
const hasTokenRef = hasConfiguredSecretRef(credential.tokenRef);
|
||||
|
||||
@@ -80,7 +80,7 @@ function isProfileConfigCompatible(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
profileId: string;
|
||||
provider: string;
|
||||
mode: "api_key" | "aws-sdk" | "token" | "oauth";
|
||||
mode: "api_key" | "token" | "oauth";
|
||||
allowOAuthTokenCompatibility?: boolean;
|
||||
}): boolean {
|
||||
const profileConfig = params.cfg?.auth?.profiles?.[params.profileId];
|
||||
@@ -311,9 +311,6 @@ export async function resolveApiKeyForProfile(
|
||||
}
|
||||
return buildApiKeyProfileResult({ apiKey: key, provider: cred.provider, email: cred.email });
|
||||
}
|
||||
if (cred.type === "aws-sdk") {
|
||||
return null;
|
||||
}
|
||||
if (cred.type === "token") {
|
||||
const expiryState = resolveTokenExpiryState(cred.expires);
|
||||
if (expiryState === "expired" || expiryState === "invalid_expires") {
|
||||
|
||||
@@ -24,6 +24,45 @@ export type AuthProfileEligibility = {
|
||||
reasonCode: AuthProfileEligibilityReasonCode;
|
||||
};
|
||||
|
||||
function resolveProviderAuthMode(
|
||||
cfg: OpenClawConfig | undefined,
|
||||
provider: string,
|
||||
): string | undefined {
|
||||
const providers = cfg?.models?.providers;
|
||||
if (!providers) {
|
||||
return undefined;
|
||||
}
|
||||
const entry = findNormalizedProviderValue(providers, provider);
|
||||
const auth = entry?.auth;
|
||||
return typeof auth === "string" ? auth : undefined;
|
||||
}
|
||||
|
||||
function providerAllowsAwsSdkAuth(cfg: OpenClawConfig | undefined, provider: string): boolean {
|
||||
const authMode = resolveProviderAuthMode(cfg, provider);
|
||||
return (
|
||||
authMode === "aws-sdk" ||
|
||||
(authMode === undefined && normalizeProviderId(provider) === "amazon-bedrock")
|
||||
);
|
||||
}
|
||||
|
||||
export function isConfiguredAwsSdkAuthProfileForProvider(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
provider: string;
|
||||
profileId: string;
|
||||
}): boolean {
|
||||
const profileConfig = params.cfg?.auth?.profiles?.[params.profileId];
|
||||
if (!profileConfig || profileConfig.mode !== "aws-sdk") {
|
||||
return false;
|
||||
}
|
||||
const providerAuthKey = resolveProviderIdForAuth(params.provider, { config: params.cfg });
|
||||
if (
|
||||
resolveProviderIdForAuth(profileConfig.provider, { config: params.cfg }) !== providerAuthKey
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return providerAllowsAwsSdkAuth(params.cfg, params.provider);
|
||||
}
|
||||
|
||||
export function resolveAuthProfileEligibility(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
store: AuthProfileStore;
|
||||
@@ -34,6 +73,15 @@ export function resolveAuthProfileEligibility(params: {
|
||||
const providerAuthKey = resolveProviderIdForAuth(params.provider, { config: params.cfg });
|
||||
const cred = params.store.profiles[params.profileId];
|
||||
if (!cred) {
|
||||
if (
|
||||
isConfiguredAwsSdkAuthProfileForProvider({
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
profileId: params.profileId,
|
||||
})
|
||||
) {
|
||||
return { eligible: true, reasonCode: "ok" };
|
||||
}
|
||||
return { eligible: false, reasonCode: "profile_missing" };
|
||||
}
|
||||
if (resolveProviderIdForAuth(cred.provider, { config: params.cfg }) !== providerAuthKey) {
|
||||
|
||||
@@ -31,12 +31,7 @@ export type LegacyAuthStore = Record<string, AuthProfileCredential>;
|
||||
type CredentialRejectReason = "non_object" | "invalid_type" | "missing_provider";
|
||||
type RejectedCredentialEntry = { key: string; reason: CredentialRejectReason };
|
||||
|
||||
const AUTH_PROFILE_TYPES = new Set<AuthProfileCredential["type"]>([
|
||||
"api_key",
|
||||
"aws-sdk",
|
||||
"oauth",
|
||||
"token",
|
||||
]);
|
||||
const AUTH_PROFILE_TYPES = new Set<AuthProfileCredential["type"]>(["api_key", "oauth", "token"]);
|
||||
|
||||
function normalizeSecretBackedField(params: {
|
||||
entry: Record<string, unknown>;
|
||||
@@ -544,14 +539,6 @@ export function applyLegacyAuthStore(store: AuthProfileStore, legacy: LegacyAuth
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (cred.type === "aws-sdk") {
|
||||
store.profiles[profileId] = {
|
||||
type: "aws-sdk",
|
||||
provider: credentialProvider,
|
||||
...(cred.email ? { email: cred.email } : {}),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (cred.type === "token") {
|
||||
store.profiles[profileId] = {
|
||||
type: "token",
|
||||
|
||||
@@ -25,7 +25,15 @@ const authStoreMocks = vi.hoisted(() => {
|
||||
state.store = { version: 1, profiles: {} };
|
||||
},
|
||||
resolveAuthProfileOrder: vi.fn(
|
||||
({ store, provider }: { store: AuthProfileStore; provider: string }) => {
|
||||
({
|
||||
cfg,
|
||||
store,
|
||||
provider,
|
||||
}: {
|
||||
cfg?: OpenClawConfig;
|
||||
store: AuthProfileStore;
|
||||
provider: string;
|
||||
}) => {
|
||||
const providerKey = normalizeProvider(provider);
|
||||
const ordered = Object.entries(store.order ?? {}).find(
|
||||
([key]) => normalizeProvider(key) === providerKey,
|
||||
@@ -33,6 +41,18 @@ const authStoreMocks = vi.hoisted(() => {
|
||||
if (ordered) {
|
||||
return ordered;
|
||||
}
|
||||
const configured = Object.entries(cfg?.auth?.profiles ?? {})
|
||||
.filter(([profileId, profile]) => {
|
||||
if (normalizeProvider(profile.provider) !== providerKey) {
|
||||
return false;
|
||||
}
|
||||
const stored = store.profiles[profileId];
|
||||
return !stored || normalizeProvider(stored.provider) === providerKey;
|
||||
})
|
||||
.map(([profileId]) => profileId);
|
||||
if (configured.length > 0) {
|
||||
return configured;
|
||||
}
|
||||
return Object.entries(store.profiles)
|
||||
.filter(([, profile]) => normalizeProvider(profile.provider) === providerKey)
|
||||
.map(([profileId]) => profileId);
|
||||
@@ -47,6 +67,22 @@ vi.mock("./store.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./order.js", () => ({
|
||||
isConfiguredAwsSdkAuthProfileForProvider: ({
|
||||
cfg,
|
||||
provider,
|
||||
profileId,
|
||||
}: {
|
||||
cfg?: OpenClawConfig;
|
||||
provider: string;
|
||||
profileId: string;
|
||||
}) => {
|
||||
const normalizeProvider = (value: string) => value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
||||
const profile = cfg?.auth?.profiles?.[profileId];
|
||||
return (
|
||||
profile?.mode === "aws-sdk" &&
|
||||
normalizeProvider(profile.provider) === normalizeProvider(provider)
|
||||
);
|
||||
},
|
||||
resolveAuthProfileOrder: authStoreMocks.resolveAuthProfileOrder,
|
||||
}));
|
||||
|
||||
@@ -157,6 +193,115 @@ describe("resolveSessionAuthProfileOverride", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps config-only aws-sdk user overrides", async () => {
|
||||
await withAuthState(async (state) => {
|
||||
const agentDir = state.agentDir();
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
authStoreMocks.state.hasSource = false;
|
||||
authStoreMocks.state.store = { version: 1, profiles: {} };
|
||||
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: Date.now(),
|
||||
authProfileOverride: "amazon-bedrock:default",
|
||||
authProfileOverrideSource: "user",
|
||||
};
|
||||
const sessionStore = { "agent:main:main": sessionEntry };
|
||||
|
||||
const resolved = await resolveSessionAuthProfileOverride({
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
"amazon-bedrock": {
|
||||
auth: "aws-sdk",
|
||||
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
api: "bedrock-converse-stream",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
profiles: {
|
||||
"amazon-bedrock:default": {
|
||||
provider: "amazon-bedrock",
|
||||
mode: "aws-sdk",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
provider: "amazon-bedrock",
|
||||
agentDir,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:main",
|
||||
storePath: undefined,
|
||||
isNewSession: false,
|
||||
});
|
||||
|
||||
expect(resolved).toBe("amazon-bedrock:default");
|
||||
expect(sessionEntry.authProfileOverride).toBe("amazon-bedrock:default");
|
||||
});
|
||||
});
|
||||
|
||||
it("clears aws-sdk config override when stored profile drifted to another provider", async () => {
|
||||
await withAuthState(async (state) => {
|
||||
const agentDir = state.agentDir();
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
authStoreMocks.state.hasSource = true;
|
||||
authStoreMocks.state.store = createAuthStoreWithProfiles({
|
||||
profiles: {
|
||||
"amazon-bedrock:default": {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key: "sk-drifted",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: Date.now(),
|
||||
authProfileOverride: "amazon-bedrock:default",
|
||||
authProfileOverrideSource: "user",
|
||||
};
|
||||
const sessionStore = { "agent:main:main": sessionEntry };
|
||||
|
||||
const resolved = await resolveSessionAuthProfileOverride({
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
"amazon-bedrock": {
|
||||
auth: "aws-sdk",
|
||||
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
api: "bedrock-converse-stream",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
profiles: {
|
||||
"amazon-bedrock:default": {
|
||||
provider: "amazon-bedrock",
|
||||
mode: "aws-sdk",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
provider: "amazon-bedrock",
|
||||
agentDir,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:main",
|
||||
storePath: undefined,
|
||||
isNewSession: false,
|
||||
});
|
||||
|
||||
expect(resolved).toBeUndefined();
|
||||
expect(sessionEntry.authProfileOverride).toBeUndefined();
|
||||
expect(sessionEntry.authProfileOverrideSource).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps explicit user override when stored order prefers another profile", async () => {
|
||||
await withAuthState(async (state) => {
|
||||
const agentDir = state.agentDir();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
||||
import { resolveAuthProfileOrder } from "../auth-profiles/order.js";
|
||||
import {
|
||||
isConfiguredAwsSdkAuthProfileForProvider,
|
||||
resolveAuthProfileOrder,
|
||||
} from "../auth-profiles/order.js";
|
||||
import { ensureAuthProfileStore, hasAnyAuthProfileStoreSource } from "../auth-profiles/store.js";
|
||||
import { isProfileInCooldown } from "../auth-profiles/usage.js";
|
||||
import { resolveProviderIdForAuth } from "../provider-auth-aliases.js";
|
||||
@@ -20,12 +23,24 @@ function isProfileForProvider(params: {
|
||||
profileId: string;
|
||||
store: ReturnType<typeof ensureAuthProfileStore>;
|
||||
}): boolean {
|
||||
const providerKey = resolveProviderIdForAuth(params.provider, { config: params.cfg });
|
||||
const entry = params.store.profiles[params.profileId];
|
||||
if (!entry?.provider) {
|
||||
if (entry) {
|
||||
if (!entry.provider) {
|
||||
return false;
|
||||
}
|
||||
return resolveProviderIdForAuth(entry.provider, { config: params.cfg }) === providerKey;
|
||||
}
|
||||
if (
|
||||
!isConfiguredAwsSdkAuthProfileForProvider({
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
profileId: params.profileId,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const providerKey = resolveProviderIdForAuth(params.provider, { config: params.cfg });
|
||||
return resolveProviderIdForAuth(entry.provider, { config: params.cfg }) === providerKey;
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function clearSessionAuthProfileOverride(params: {
|
||||
@@ -95,7 +110,11 @@ export async function resolveSessionAuthProfileOverride(params: {
|
||||
? "user"
|
||||
: undefined);
|
||||
|
||||
if (current && !store.profiles[current]) {
|
||||
if (
|
||||
current &&
|
||||
!store.profiles[current] &&
|
||||
!isConfiguredAwsSdkAuthProfileForProvider({ cfg, provider, profileId: current })
|
||||
) {
|
||||
await clearSessionAuthProfileOverride({ sessionEntry, sessionStore, sessionKey, storePath });
|
||||
current = undefined;
|
||||
}
|
||||
|
||||
@@ -29,17 +29,6 @@ export type ApiKeyCredential = {
|
||||
metadata?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type AwsSdkCredential = {
|
||||
type: "aws-sdk";
|
||||
provider: string;
|
||||
/** Explicit opt-out for copying this profile when creating another agent. */
|
||||
copyToAgents?: boolean;
|
||||
email?: string;
|
||||
displayName?: string;
|
||||
/** Optional provider-specific metadata (e.g., account IDs, regions). */
|
||||
metadata?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type TokenCredential = {
|
||||
/**
|
||||
* Static bearer-style token (often OAuth access token / PAT).
|
||||
@@ -70,11 +59,7 @@ export type OAuthCredential = OAuthCredentials & {
|
||||
displayName?: string;
|
||||
};
|
||||
|
||||
export type AuthProfileCredential =
|
||||
| ApiKeyCredential
|
||||
| AwsSdkCredential
|
||||
| TokenCredential
|
||||
| OAuthCredential;
|
||||
export type AuthProfileCredential = ApiKeyCredential | TokenCredential | OAuthCredential;
|
||||
|
||||
export type AuthProfileFailureReason =
|
||||
| "auth"
|
||||
|
||||
@@ -99,14 +99,6 @@ function encodeAuthProfileCredential(credential: AuthProfileCredential): string
|
||||
credential.displayName ?? null,
|
||||
encodeUnknown(credential.metadata),
|
||||
]);
|
||||
case "aws-sdk":
|
||||
return JSON.stringify([
|
||||
"aws-sdk",
|
||||
credential.provider,
|
||||
credential.email ?? null,
|
||||
credential.displayName ?? null,
|
||||
encodeUnknown(credential.metadata),
|
||||
]);
|
||||
case "token":
|
||||
return JSON.stringify([
|
||||
"token",
|
||||
|
||||
@@ -270,6 +270,21 @@ const BEDROCK_PROVIDER_CFG = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const BEDROCK_PROVIDER_CFG_WITH_PROFILE = {
|
||||
...BEDROCK_PROVIDER_CFG,
|
||||
auth: {
|
||||
order: {
|
||||
"amazon-bedrock": ["amazon-bedrock:default"],
|
||||
},
|
||||
profiles: {
|
||||
"amazon-bedrock:default": {
|
||||
provider: "amazon-bedrock",
|
||||
mode: "aws-sdk",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
async function resolveBedrockProvider() {
|
||||
return resolveApiKeyForProvider({
|
||||
provider: "amazon-bedrock",
|
||||
@@ -290,59 +305,35 @@ async function expectBedrockAuthSource(params: {
|
||||
});
|
||||
}
|
||||
|
||||
it("resolves persisted aws-sdk auth profiles without static keys (#69708)", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-bedrock-auth-profile-"));
|
||||
try {
|
||||
await fs.writeFile(
|
||||
path.join(agentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"amazon-bedrock:default": {
|
||||
type: "aws-sdk",
|
||||
provider: "amazon-bedrock",
|
||||
createdAt: "2026-03-15T10:00:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
it("resolves config-only aws-sdk profiles without stored credentials", async () => {
|
||||
const resolved = await resolveApiKeyForProvider({
|
||||
provider: "amazon-bedrock",
|
||||
profileId: "amazon-bedrock:default",
|
||||
store: { version: 1, profiles: {} },
|
||||
cfg: BEDROCK_PROVIDER_CFG_WITH_PROFILE as never,
|
||||
});
|
||||
|
||||
const resolved = await resolveApiKeyForProvider({
|
||||
provider: "amazon-bedrock",
|
||||
profileId: "amazon-bedrock:default",
|
||||
cfg: BEDROCK_PROVIDER_CFG as never,
|
||||
store,
|
||||
agentDir,
|
||||
});
|
||||
expect(resolved).toMatchObject({
|
||||
mode: "aws-sdk",
|
||||
profileId: "amazon-bedrock:default",
|
||||
source: "profile:amazon-bedrock:default",
|
||||
});
|
||||
expect(resolved.apiKey).toBeUndefined();
|
||||
});
|
||||
|
||||
expect(resolved).toMatchObject({
|
||||
mode: "aws-sdk",
|
||||
profileId: "amazon-bedrock:default",
|
||||
source: "profile:amazon-bedrock:default",
|
||||
});
|
||||
expect(resolved.apiKey).toBeUndefined();
|
||||
await expect(
|
||||
hasAvailableAuthForProvider({
|
||||
provider: "amazon-bedrock",
|
||||
cfg: BEDROCK_PROVIDER_CFG as never,
|
||||
store,
|
||||
agentDir,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
expect(resolveModelAuthMode("amazon-bedrock", BEDROCK_PROVIDER_CFG as never, store)).toBe(
|
||||
"aws-sdk",
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
}
|
||||
it("uses configured aws-sdk profile order without stored credentials", async () => {
|
||||
const resolved = await resolveApiKeyForProvider({
|
||||
provider: "amazon-bedrock",
|
||||
store: { version: 1, profiles: {} },
|
||||
cfg: BEDROCK_PROVIDER_CFG_WITH_PROFILE as never,
|
||||
});
|
||||
|
||||
expect(resolved).toMatchObject({
|
||||
mode: "aws-sdk",
|
||||
profileId: "amazon-bedrock:default",
|
||||
source: "profile:amazon-bedrock:default",
|
||||
});
|
||||
expect(resolved.apiKey).toBeUndefined();
|
||||
});
|
||||
|
||||
function buildDemoLocalStore(keys: string[]) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
externalCliDiscoveryForProviderAuth,
|
||||
ensureAuthProfileStore,
|
||||
ensureAuthProfileStoreWithoutExternalProfiles,
|
||||
isConfiguredAwsSdkAuthProfileForProvider,
|
||||
listProfilesForProvider,
|
||||
resolveApiKeyForProfile,
|
||||
resolveAuthProfileOrder,
|
||||
@@ -44,7 +45,6 @@ import {
|
||||
type ResolvedProviderAuth,
|
||||
} from "./model-auth-runtime-shared.js";
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
import { resolveProviderIdForAuth } from "./provider-auth-aliases.js";
|
||||
|
||||
export {
|
||||
ensureAuthProfileStore,
|
||||
@@ -237,50 +237,17 @@ function resolveProviderAuthOverride(
|
||||
}
|
||||
|
||||
function profileTypeToAuthMode(type: AuthProfileCredential["type"]): ResolvedProviderAuth["mode"] {
|
||||
return type === "oauth"
|
||||
? "oauth"
|
||||
: type === "token"
|
||||
? "token"
|
||||
: type === "aws-sdk"
|
||||
? "aws-sdk"
|
||||
: "api-key";
|
||||
return type === "oauth" ? "oauth" : type === "token" ? "token" : "api-key";
|
||||
}
|
||||
|
||||
function isProfileForProvider(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
credential: AuthProfileCredential;
|
||||
provider: string;
|
||||
}): boolean {
|
||||
return (
|
||||
resolveProviderIdForAuth(params.credential.provider, { config: params.cfg }) ===
|
||||
resolveProviderIdForAuth(params.provider, { config: params.cfg })
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAwsSdkProfileAuth(params: {
|
||||
function resolveConfiguredAwsSdkProfileAuth(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
provider: string;
|
||||
profileId: string;
|
||||
credential: AuthProfileCredential | undefined;
|
||||
}): ResolvedProviderAuth | null {
|
||||
if (params.credential?.type !== "aws-sdk") {
|
||||
if (!isConfiguredAwsSdkAuthProfileForProvider(params)) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
!isProfileForProvider({
|
||||
cfg: params.cfg,
|
||||
credential: params.credential,
|
||||
provider: params.provider,
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const profileConfig = params.cfg?.auth?.profiles?.[params.profileId];
|
||||
if (profileConfig) {
|
||||
if (profileConfig.provider !== params.credential.provider || profileConfig.mode !== "aws-sdk") {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...resolveAwsSdkAuthInfo(),
|
||||
profileId: params.profileId,
|
||||
@@ -582,8 +549,13 @@ export async function resolveApiKeyForProvider(params: {
|
||||
credentialPrecedence?: ProviderCredentialPrecedence;
|
||||
}): Promise<ResolvedProviderAuth> {
|
||||
const { provider, cfg, profileId, preferredProfile } = params;
|
||||
let scopedStore: AuthProfileStore | undefined = params.store;
|
||||
|
||||
if (profileId) {
|
||||
const awsSdkProfileAuth = resolveConfiguredAwsSdkProfileAuth({ cfg, provider, profileId });
|
||||
if (awsSdkProfileAuth) {
|
||||
return awsSdkProfileAuth;
|
||||
}
|
||||
const store =
|
||||
params.store ??
|
||||
resolveScopedAuthProfileStore({
|
||||
@@ -593,15 +565,6 @@ export async function resolveApiKeyForProvider(params: {
|
||||
profileId,
|
||||
preferredProfile,
|
||||
});
|
||||
const awsSdkProfileAuth = resolveAwsSdkProfileAuth({
|
||||
cfg,
|
||||
provider,
|
||||
profileId,
|
||||
credential: store.profiles[profileId],
|
||||
});
|
||||
if (awsSdkProfileAuth) {
|
||||
return awsSdkProfileAuth;
|
||||
}
|
||||
const resolved = await resolveApiKeyForProfile({
|
||||
cfg,
|
||||
store,
|
||||
@@ -637,6 +600,31 @@ export async function resolveApiKeyForProvider(params: {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (cfg?.auth?.profiles || cfg?.auth?.order) {
|
||||
scopedStore ??= resolveScopedAuthProfileStore({
|
||||
agentDir: params.agentDir,
|
||||
cfg,
|
||||
provider,
|
||||
preferredProfile,
|
||||
});
|
||||
const configuredProfileOrder = resolveAuthProfileOrder({
|
||||
cfg,
|
||||
store: scopedStore,
|
||||
provider,
|
||||
preferredProfile,
|
||||
});
|
||||
for (const candidate of configuredProfileOrder) {
|
||||
const awsSdkProfileAuth = resolveConfiguredAwsSdkProfileAuth({
|
||||
cfg,
|
||||
provider,
|
||||
profileId: candidate,
|
||||
});
|
||||
if (awsSdkProfileAuth) {
|
||||
return awsSdkProfileAuth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const authOverride = resolveProviderAuthOverride(cfg, provider);
|
||||
if (authOverride === "aws-sdk") {
|
||||
return resolveAwsSdkAuthInfo();
|
||||
@@ -688,7 +676,7 @@ export async function resolveApiKeyForProvider(params: {
|
||||
};
|
||||
}
|
||||
const store =
|
||||
params.store ??
|
||||
scopedStore ??
|
||||
resolveScopedAuthProfileStore({
|
||||
agentDir: params.agentDir,
|
||||
cfg,
|
||||
@@ -704,11 +692,10 @@ export async function resolveApiKeyForProvider(params: {
|
||||
let deferredAuthProfileResult: ResolvedProviderAuth | null = null;
|
||||
for (const candidate of order) {
|
||||
try {
|
||||
const awsSdkProfileAuth = resolveAwsSdkProfileAuth({
|
||||
const awsSdkProfileAuth = resolveConfiguredAwsSdkProfileAuth({
|
||||
cfg,
|
||||
provider,
|
||||
profileId: candidate,
|
||||
credential: store.profiles[candidate],
|
||||
});
|
||||
if (awsSdkProfileAuth) {
|
||||
return awsSdkProfileAuth;
|
||||
@@ -843,10 +830,10 @@ export function resolveModelAuthMode(
|
||||
const modes = new Set(
|
||||
profiles
|
||||
.map((id) => authStore.profiles[id]?.type)
|
||||
.filter((mode): mode is "api_key" | "aws-sdk" | "oauth" | "token" => Boolean(mode)),
|
||||
.filter((mode): mode is "api_key" | "oauth" | "token" => Boolean(mode)),
|
||||
);
|
||||
const distinct = ["oauth", "token", "api_key", "aws-sdk"].filter((k) =>
|
||||
modes.has(k as "oauth" | "token" | "api_key" | "aws-sdk"),
|
||||
const distinct = ["oauth", "token", "api_key"].filter((k) =>
|
||||
modes.has(k as "oauth" | "token" | "api_key"),
|
||||
);
|
||||
if (distinct.length >= 2) {
|
||||
return "mixed";
|
||||
@@ -860,9 +847,6 @@ export function resolveModelAuthMode(
|
||||
if (modes.has("api_key")) {
|
||||
return "api-key";
|
||||
}
|
||||
if (modes.has("aws-sdk")) {
|
||||
return "aws-sdk";
|
||||
}
|
||||
}
|
||||
|
||||
if (authOverride === undefined && normalizeProviderId(resolved) === "amazon-bedrock") {
|
||||
@@ -931,14 +915,7 @@ export async function hasAvailableAuthForProvider(params: {
|
||||
});
|
||||
for (const candidate of order) {
|
||||
try {
|
||||
if (
|
||||
resolveAwsSdkProfileAuth({
|
||||
cfg,
|
||||
provider,
|
||||
profileId: candidate,
|
||||
credential: store.profiles[candidate],
|
||||
})
|
||||
) {
|
||||
if (resolveConfiguredAwsSdkProfileAuth({ cfg, provider, profileId: candidate })) {
|
||||
return true;
|
||||
}
|
||||
const resolved = await resolveApiKeyForProfile({
|
||||
|
||||
@@ -24,6 +24,21 @@ vi.mock("../../agents/auth-health.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/auth-profiles.js", () => ({
|
||||
isConfiguredAwsSdkAuthProfileForProvider: ({
|
||||
cfg,
|
||||
provider,
|
||||
profileId,
|
||||
}: {
|
||||
cfg?: OpenClawConfig;
|
||||
provider: string;
|
||||
profileId: string;
|
||||
}) => {
|
||||
const profile = cfg?.auth?.profiles?.[profileId];
|
||||
return (
|
||||
profile?.mode === "aws-sdk" &&
|
||||
profile.provider.trim().toLowerCase() === provider.trim().toLowerCase()
|
||||
);
|
||||
},
|
||||
isProfileInCooldown: () => false,
|
||||
resolveAuthProfileDisplayLabel: ({ profileId }: { profileId: string }) => profileId,
|
||||
resolveAuthStorePathForDisplay: () => "/tmp/auth-profiles.json",
|
||||
@@ -128,6 +143,72 @@ describe("resolveAuthLabel ref-aware labels", () => {
|
||||
expect(result.label).not.toContain("token:missing");
|
||||
});
|
||||
|
||||
it("labels config-only aws-sdk profiles as valid in compact mode", async () => {
|
||||
mockOrder = ["amazon-bedrock:default"];
|
||||
const result = await resolveAuthLabel(
|
||||
"amazon-bedrock",
|
||||
{
|
||||
models: {
|
||||
providers: {
|
||||
"amazon-bedrock": {
|
||||
auth: "aws-sdk",
|
||||
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
api: "bedrock-converse-stream",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
profiles: {
|
||||
"amazon-bedrock:default": {
|
||||
provider: "amazon-bedrock",
|
||||
mode: "aws-sdk",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
"/tmp/models.json",
|
||||
undefined,
|
||||
"compact",
|
||||
);
|
||||
|
||||
expect(result.label).toBe("amazon-bedrock:default aws-sdk");
|
||||
expect(result.label).not.toContain("missing");
|
||||
});
|
||||
|
||||
it("labels config-only aws-sdk profiles as valid in verbose mode", async () => {
|
||||
mockOrder = ["amazon-bedrock:default"];
|
||||
const result = await resolveAuthLabel(
|
||||
"amazon-bedrock",
|
||||
{
|
||||
models: {
|
||||
providers: {
|
||||
"amazon-bedrock": {
|
||||
auth: "aws-sdk",
|
||||
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
api: "bedrock-converse-stream",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
profiles: {
|
||||
"amazon-bedrock:default": {
|
||||
provider: "amazon-bedrock",
|
||||
mode: "aws-sdk",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
"/tmp/models.json",
|
||||
undefined,
|
||||
"verbose",
|
||||
);
|
||||
|
||||
expect(result.label).toContain("amazon-bedrock:default=aws-sdk");
|
||||
expect(result.label).not.toContain("missing");
|
||||
});
|
||||
|
||||
it("passes workspace scope to env auth labels", async () => {
|
||||
const cfg = { plugins: { allow: ["workspace-auth-label"] } } as OpenClawConfig;
|
||||
resolveEnvApiKeyMock.mockReturnValue({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { formatRemainingShort } from "../../agents/auth-health.js";
|
||||
import {
|
||||
isConfiguredAwsSdkAuthProfileForProvider,
|
||||
isProfileInCooldown,
|
||||
resolveAuthProfileDisplayLabel,
|
||||
resolveAuthStorePathForDisplay,
|
||||
@@ -78,6 +79,13 @@ export const resolveAuthLabel = async (
|
||||
}
|
||||
const profile = store.profiles[profileId];
|
||||
const configProfile = cfg.auth?.profiles?.[profileId];
|
||||
const configOnlyAwsSdk = !profile
|
||||
? isConfiguredAwsSdkAuthProfileForProvider({ cfg, provider, profileId })
|
||||
: false;
|
||||
const more = order.length > 1 ? ` (+${order.length - 1})` : "";
|
||||
if (configOnlyAwsSdk) {
|
||||
return { label: `${profileId} aws-sdk${more}`, source: "" };
|
||||
}
|
||||
const missing =
|
||||
!profile ||
|
||||
(configProfile?.provider && configProfile.provider !== profile.provider) ||
|
||||
@@ -85,7 +93,6 @@ export const resolveAuthLabel = async (
|
||||
configProfile.mode !== profile.type &&
|
||||
!(configProfile.mode === "oauth" && profile.type === "token"));
|
||||
|
||||
const more = order.length > 1 ? ` (+${order.length - 1})` : "";
|
||||
if (missing) {
|
||||
return { label: `${profileId} missing${more}`, source: "" };
|
||||
}
|
||||
@@ -113,12 +120,6 @@ export const resolveAuthLabel = async (
|
||||
source: "",
|
||||
};
|
||||
}
|
||||
if (profile.type === "aws-sdk") {
|
||||
return {
|
||||
label: `${profileId} aws-sdk${more}`,
|
||||
source: "",
|
||||
};
|
||||
}
|
||||
const display = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
|
||||
const label = display === profileId ? profileId : display;
|
||||
const exp = formatExpirationLabel(profile.expires, now, formatUntil);
|
||||
@@ -143,6 +144,10 @@ export const resolveAuthLabel = async (
|
||||
flags.push("cooldown");
|
||||
}
|
||||
}
|
||||
if (!profile && isConfiguredAwsSdkAuthProfileForProvider({ cfg, provider, profileId })) {
|
||||
const suffix = formatFlagsSuffix(flags);
|
||||
return `${profileId}=aws-sdk${suffix}`;
|
||||
}
|
||||
if (
|
||||
!profile ||
|
||||
(configProfile?.provider && configProfile.provider !== profile.provider) ||
|
||||
@@ -175,10 +180,6 @@ export const resolveAuthLabel = async (
|
||||
const suffix = formatFlagsSuffix(flags);
|
||||
return `${profileId}=token:${tokenLabel}${suffix}`;
|
||||
}
|
||||
if (profile.type === "aws-sdk") {
|
||||
const suffix = formatFlagsSuffix(flags);
|
||||
return `${profileId}=aws-sdk${suffix}`;
|
||||
}
|
||||
const display = resolveAuthProfileDisplayLabel({
|
||||
cfg,
|
||||
store,
|
||||
|
||||
@@ -100,4 +100,57 @@ describe("maybeRepairLegacyFlatAuthProfileStores", () => {
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual(legacy);
|
||||
});
|
||||
|
||||
it("moves aws-sdk auth profile markers into config metadata", async () => {
|
||||
const state = await makeTestState();
|
||||
const legacy = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"amazon-bedrock:default": {
|
||||
type: "aws-sdk",
|
||||
createdAt: "2026-03-15T10:00:00.000Z",
|
||||
},
|
||||
"openrouter:default": {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key: "sk-openrouter",
|
||||
},
|
||||
},
|
||||
};
|
||||
const authPath = await state.writeAuthProfiles(legacy);
|
||||
const cfg = {};
|
||||
|
||||
const result = await maybeRepairLegacyFlatAuthProfileStores({
|
||||
cfg,
|
||||
prompter: makePrompter(true),
|
||||
now: () => 456,
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toHaveLength(1);
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(cfg).toEqual({
|
||||
auth: {
|
||||
profiles: {
|
||||
"amazon-bedrock:default": {
|
||||
provider: "amazon-bedrock",
|
||||
mode: "aws-sdk",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openrouter:default": {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key: "sk-openrouter",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(JSON.parse(fs.readFileSync(`${authPath}.aws-sdk-profile.456.bak`, "utf8"))).toEqual(
|
||||
legacy,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import type { AuthProfileCredential, AuthProfileStore } from "../agents/auth-profiles/types.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import type { AuthProfileConfig } from "../config/types.auth.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { loadJsonFile } from "../infra/json-file.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
@@ -27,6 +28,20 @@ type LegacyFlatAuthProfileStore = {
|
||||
store: AuthProfileStore;
|
||||
};
|
||||
|
||||
type AwsSdkProfileMarker = {
|
||||
profileId: string;
|
||||
provider: string;
|
||||
email?: string;
|
||||
displayName?: string;
|
||||
};
|
||||
|
||||
type AwsSdkAuthProfileMarkerStore = {
|
||||
agentDir?: string;
|
||||
authPath: string;
|
||||
raw: Record<string, unknown>;
|
||||
profiles: AwsSdkProfileMarker[];
|
||||
};
|
||||
|
||||
export type LegacyFlatAuthProfileRepairResult = {
|
||||
detected: string[];
|
||||
changes: string[];
|
||||
@@ -47,16 +62,19 @@ function isSafeLegacyProviderKey(key: string): boolean {
|
||||
return key.trim().length > 0 && !UNSAFE_LEGACY_AUTH_PROFILE_KEYS.has(key);
|
||||
}
|
||||
|
||||
function extractProviderFromProfileId(profileId: string): string | undefined {
|
||||
const colon = profileId.indexOf(":");
|
||||
if (colon <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return readNonEmptyString(profileId.slice(0, colon));
|
||||
}
|
||||
|
||||
function inferLegacyCredentialType(
|
||||
record: Record<string, unknown>,
|
||||
): AuthProfileCredential["type"] | undefined {
|
||||
const explicit = readNonEmptyString(record.type) ?? readNonEmptyString(record.mode);
|
||||
if (
|
||||
explicit === "api_key" ||
|
||||
explicit === "aws-sdk" ||
|
||||
explicit === "token" ||
|
||||
explicit === "oauth"
|
||||
) {
|
||||
if (explicit === "api_key" || explicit === "token" || explicit === "oauth") {
|
||||
return explicit;
|
||||
}
|
||||
if (readNonEmptyString(record.key) ?? readNonEmptyString(record.apiKey)) {
|
||||
@@ -216,6 +234,74 @@ function backupAuthProfileStore(authPath: string, now: () => number): string {
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
function backupAwsSdkProfileMarkerStore(authPath: string, now: () => number): string {
|
||||
const backupPath = `${authPath}.aws-sdk-profile.${now()}.bak`;
|
||||
fs.copyFileSync(authPath, backupPath);
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
function resolveAwsSdkAuthProfileMarkerStore(
|
||||
candidate: AuthProfileRepairCandidate,
|
||||
): AwsSdkAuthProfileMarkerStore | null {
|
||||
if (!fs.existsSync(candidate.authPath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = loadJsonFile(candidate.authPath);
|
||||
if (!isRecord(raw) || !isRecord(raw.profiles)) {
|
||||
return null;
|
||||
}
|
||||
const markers: AwsSdkProfileMarker[] = [];
|
||||
for (const [profileId, value] of Object.entries(raw.profiles)) {
|
||||
if (!isRecord(value)) {
|
||||
continue;
|
||||
}
|
||||
const mode = readNonEmptyString(value.type) ?? readNonEmptyString(value.mode);
|
||||
if (mode !== "aws-sdk") {
|
||||
continue;
|
||||
}
|
||||
const provider = readNonEmptyString(value.provider) ?? extractProviderFromProfileId(profileId);
|
||||
if (!provider || !isSafeLegacyProviderKey(provider)) {
|
||||
continue;
|
||||
}
|
||||
markers.push({
|
||||
profileId,
|
||||
provider,
|
||||
...(readNonEmptyString(value.email) ? { email: readNonEmptyString(value.email) } : {}),
|
||||
...(readNonEmptyString(value.displayName)
|
||||
? { displayName: readNonEmptyString(value.displayName) }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
return markers.length > 0
|
||||
? {
|
||||
...candidate,
|
||||
raw,
|
||||
profiles: markers,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
function ensureConfigAuthProfiles(config: OpenClawConfig): Record<string, AuthProfileConfig> {
|
||||
const root = config as Record<string, unknown>;
|
||||
const auth = isRecord(root.auth) ? root.auth : {};
|
||||
if (root.auth !== auth) {
|
||||
root.auth = auth;
|
||||
}
|
||||
if (!isRecord(auth.profiles)) {
|
||||
auth.profiles = {};
|
||||
}
|
||||
return auth.profiles as Record<string, AuthProfileConfig>;
|
||||
}
|
||||
|
||||
function removeAwsSdkProfileMarkers(raw: Record<string, unknown>, profileIds: string[]): void {
|
||||
if (!isRecord(raw.profiles)) {
|
||||
return;
|
||||
}
|
||||
for (const profileId of profileIds) {
|
||||
delete raw.profiles[profileId];
|
||||
}
|
||||
}
|
||||
|
||||
export async function maybeRepairLegacyFlatAuthProfileStores(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: DoctorPrompter;
|
||||
@@ -225,28 +311,45 @@ export async function maybeRepairLegacyFlatAuthProfileStores(params: {
|
||||
const legacyStores = listAuthProfileRepairCandidates(params.cfg)
|
||||
.map(resolveLegacyFlatStore)
|
||||
.filter((entry): entry is LegacyFlatAuthProfileStore => entry !== null);
|
||||
const awsSdkMarkerStores = listAuthProfileRepairCandidates(params.cfg)
|
||||
.map(resolveAwsSdkAuthProfileMarkerStore)
|
||||
.filter((entry): entry is AwsSdkAuthProfileMarkerStore => entry !== null);
|
||||
|
||||
const result: LegacyFlatAuthProfileRepairResult = {
|
||||
detected: legacyStores.map((entry) => entry.authPath),
|
||||
detected: [
|
||||
...legacyStores.map((entry) => entry.authPath),
|
||||
...awsSdkMarkerStores.map((entry) => entry.authPath),
|
||||
],
|
||||
changes: [],
|
||||
warnings: [],
|
||||
};
|
||||
if (legacyStores.length === 0) {
|
||||
if (legacyStores.length === 0 && awsSdkMarkerStores.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
note(
|
||||
[
|
||||
...legacyStores.map(
|
||||
(entry) => `- ${shortenHomePath(entry.authPath)} uses the legacy flat auth profile format.`,
|
||||
),
|
||||
const noteLines = [
|
||||
...legacyStores.map(
|
||||
(entry) => `- ${shortenHomePath(entry.authPath)} uses the legacy flat auth profile format.`,
|
||||
),
|
||||
...awsSdkMarkerStores.map(
|
||||
(entry) =>
|
||||
`- ${shortenHomePath(entry.authPath)} contains aws-sdk profile markers that belong in openclaw.json auth.profiles.`,
|
||||
),
|
||||
];
|
||||
if (legacyStores.length > 0) {
|
||||
noteLines.push(
|
||||
`- The gateway expects the canonical version/profiles store; ${formatCliCommand("openclaw doctor --fix")} rewrites this legacy shape with a backup.`,
|
||||
].join("\n"),
|
||||
"Auth profiles",
|
||||
);
|
||||
);
|
||||
}
|
||||
if (awsSdkMarkerStores.length > 0) {
|
||||
noteLines.push(
|
||||
`- AWS SDK profile markers are routing metadata, not stored credentials; ${formatCliCommand("openclaw doctor --fix")} moves them to config with a backup.`,
|
||||
);
|
||||
}
|
||||
note(noteLines.join("\n"), "Auth profiles");
|
||||
|
||||
const shouldRepair = await params.prompter.confirmAutoFix({
|
||||
message: "Rewrite legacy flat auth-profiles.json files now?",
|
||||
message: "Repair legacy auth-profiles.json files now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!shouldRepair) {
|
||||
@@ -264,6 +367,32 @@ export async function maybeRepairLegacyFlatAuthProfileStores(params: {
|
||||
result.warnings.push(`Failed to rewrite ${shortenHomePath(entry.authPath)}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
for (const entry of awsSdkMarkerStores) {
|
||||
try {
|
||||
const backupPath = backupAwsSdkProfileMarkerStore(entry.authPath, now);
|
||||
const configProfiles = ensureConfigAuthProfiles(params.cfg);
|
||||
for (const marker of entry.profiles) {
|
||||
configProfiles[marker.profileId] = {
|
||||
provider: marker.provider,
|
||||
mode: "aws-sdk",
|
||||
...(marker.email ? { email: marker.email } : {}),
|
||||
...(marker.displayName ? { displayName: marker.displayName } : {}),
|
||||
};
|
||||
}
|
||||
removeAwsSdkProfileMarkers(
|
||||
entry.raw,
|
||||
entry.profiles.map((profile) => profile.profileId),
|
||||
);
|
||||
fs.writeFileSync(entry.authPath, `${JSON.stringify(entry.raw, null, 2)}\n`);
|
||||
result.changes.push(
|
||||
`Moved aws-sdk profile metadata from ${shortenHomePath(entry.authPath)} to auth.profiles (backup: ${shortenHomePath(backupPath)}).`,
|
||||
);
|
||||
} catch (err) {
|
||||
result.warnings.push(
|
||||
`Failed to migrate aws-sdk profile markers from ${shortenHomePath(entry.authPath)}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
if (result.changes.length > 0) {
|
||||
note(result.changes.map((change) => `- ${change}`).join("\n"), "Doctor changes");
|
||||
|
||||
@@ -46,9 +46,7 @@ function formatTimestamp(value: number | undefined): string | undefined {
|
||||
}
|
||||
|
||||
function resolveProfileExpiry(profile: AuthProfileCredential): string | undefined {
|
||||
return profile.type === "oauth" || profile.type === "token"
|
||||
? formatTimestamp(profile.expires)
|
||||
: undefined;
|
||||
return profile.type === "api_key" ? undefined : formatTimestamp(profile.expires);
|
||||
}
|
||||
|
||||
function summarizeProfile(params: {
|
||||
|
||||
@@ -588,13 +588,10 @@ export function resolveRequestedLoginProviderOrThrow(
|
||||
return resolveRequestedProviderOrThrow(providers, rawProvider);
|
||||
}
|
||||
|
||||
function credentialMode(credential: AuthProfileCredential): AuthProfileCredential["type"] {
|
||||
function credentialMode(credential: AuthProfileCredential): "api_key" | "oauth" | "token" {
|
||||
if (credential.type === "api_key") {
|
||||
return "api_key";
|
||||
}
|
||||
if (credential.type === "aws-sdk") {
|
||||
return "aws-sdk";
|
||||
}
|
||||
if (credential.type === "token") {
|
||||
return "token";
|
||||
}
|
||||
|
||||
@@ -110,9 +110,6 @@ export function resolveProviderAuthOverview(params: {
|
||||
profileId,
|
||||
);
|
||||
}
|
||||
if (profile.type === "aws-sdk") {
|
||||
return withUnusableSuffix(`${profileId}=AWS SDK`, profileId);
|
||||
}
|
||||
const display = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
|
||||
const suffix =
|
||||
display === profileId
|
||||
@@ -126,7 +123,6 @@ export function resolveProviderAuthOverview(params: {
|
||||
const oauthCount = profiles.filter((id) => store.profiles[id]?.type === "oauth").length;
|
||||
const tokenCount = profiles.filter((id) => store.profiles[id]?.type === "token").length;
|
||||
const apiKeyCount = profiles.filter((id) => store.profiles[id]?.type === "api_key").length;
|
||||
const awsSdkCount = profiles.filter((id) => store.profiles[id]?.type === "aws-sdk").length;
|
||||
|
||||
const envKey = resolveEnvApiKey(provider, process.env, {
|
||||
config: cfg,
|
||||
@@ -175,7 +171,6 @@ export function resolveProviderAuthOverview(params: {
|
||||
oauth: oauthCount,
|
||||
token: tokenCount,
|
||||
apiKey: apiKeyCount,
|
||||
awsSdk: awsSdkCount,
|
||||
labels,
|
||||
},
|
||||
...(envKey
|
||||
|
||||
@@ -671,7 +671,7 @@ export async function modelsStatusCommand(
|
||||
bits.push(
|
||||
formatKeyValue(
|
||||
"profiles",
|
||||
`${entry.profiles.count} (oauth=${entry.profiles.oauth}, token=${entry.profiles.token}, api_key=${entry.profiles.apiKey}, aws-sdk=${entry.profiles.awsSdk})`,
|
||||
`${entry.profiles.count} (oauth=${entry.profiles.oauth}, token=${entry.profiles.token}, api_key=${entry.profiles.apiKey})`,
|
||||
),
|
||||
);
|
||||
if (entry.profiles.labels.length > 0) {
|
||||
|
||||
@@ -28,7 +28,6 @@ export type ProviderAuthOverview = {
|
||||
oauth: number;
|
||||
token: number;
|
||||
apiKey: number;
|
||||
awsSdk: number;
|
||||
labels: string[];
|
||||
};
|
||||
env?: { value: string; source: string };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export type AuthProfileConfig = {
|
||||
provider: string;
|
||||
/**
|
||||
* Credential type expected in auth-profiles.json for this profile id.
|
||||
* Auth route selected by this profile id.
|
||||
* - api_key: static provider API key
|
||||
* - oauth: refreshable OAuth credentials (access+refresh+expires)
|
||||
* - token: static bearer-style token (optionally expiring; no refresh)
|
||||
|
||||
@@ -42,7 +42,7 @@ export type ModelAuthExpiry = {
|
||||
|
||||
export type ModelAuthStatusProfile = {
|
||||
profileId: string;
|
||||
type: "oauth" | "token" | "api_key" | "aws-sdk";
|
||||
type: "oauth" | "token" | "api_key";
|
||||
status: AuthProfileHealthStatus;
|
||||
expiry?: ModelAuthExpiry;
|
||||
};
|
||||
|
||||
@@ -337,12 +337,7 @@ function hasAuthProfileCredentialSource(params: {
|
||||
if (
|
||||
dedupeProfileIds(order).some((profileId) => {
|
||||
const cred = store.profiles[profileId];
|
||||
return (
|
||||
cred?.type === "api_key" ||
|
||||
cred?.type === "aws-sdk" ||
|
||||
cred?.type === "oauth" ||
|
||||
cred?.type === "token"
|
||||
);
|
||||
return cred?.type === "api_key" || cred?.type === "oauth" || cred?.type === "token";
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user