Files
openclaw/extensions/migrate-hermes/auth-config.ts
Peter Steinberger 852548ce05 feat(onboard): stage migration imports before promotion (#112798)
* feat(onboard): stage migration imports before promotion

* fix(onboard): harden migration recovery

* fix(onboard): commit promoted config with CAS

* fix(onboard): resume setup after migration recovery

* test(onboard): split noninteractive migration coverage

* refactor(onboard): split migration promotion helpers

* fix(onboard): normalize migration preparation cleanup

* fix(onboard): preserve recovered inference ownership

* fix(codex): complete empty deferred plugin config

* fix(codex): reconcile deferred plugin config retries

* fix(onboard): resolve verification target after rebase

* refactor(onboard): trim retired migration exports

* test(onboard): assert read-only auth prompt store
2026-07-23 07:06:01 -04:00

101 lines
3.3 KiB
TypeScript

// Migrate Hermes helper module supports auth config behavior.
import { resolveMigrationConfigRuntime } from "openclaw/plugin-sdk/migration";
import type { MigrationProviderContext } from "openclaw/plugin-sdk/plugin-entry";
import { applyAuthProfileConfig, type OpenClawConfig } from "openclaw/plugin-sdk/provider-auth";
export type HermesAuthProfileConfig = {
profileId: string;
provider: string;
mode: "api_key" | "oauth" | "token";
email?: string;
displayName?: string;
};
type HermesAuthConfigApplyResult = "configured" | "conflict" | "unavailable";
class HermesAuthConfigConflict extends Error {}
function existingProfileIsCompatible(
existing: NonNullable<NonNullable<OpenClawConfig["auth"]>["profiles"]>[string],
profile: HermesAuthProfileConfig,
): boolean {
if (existing.provider !== profile.provider || existing.mode !== profile.mode) {
return false;
}
if (existing.email && profile.email && existing.email !== profile.email) {
return false;
}
return true;
}
export function hasAuthProfileConfigConflict(
config: OpenClawConfig,
profile: HermesAuthProfileConfig,
overwrite: boolean,
): boolean {
if (overwrite) {
return false;
}
const existing = config.auth?.profiles?.[profile.profileId];
return Boolean(existing && !existingProfileIsCompatible(existing, profile));
}
function replaceConfigDraft(draft: OpenClawConfig, next: OpenClawConfig): void {
for (const key of Object.keys(draft) as Array<keyof OpenClawConfig>) {
delete draft[key];
}
Object.assign(draft, next);
}
export function hasCurrentAuthProfileConfigConflict(
ctx: MigrationProviderContext,
profile: HermesAuthProfileConfig,
): boolean {
let config = ctx.config;
try {
config =
(resolveMigrationConfigRuntime(ctx)?.current?.() as OpenClawConfig | undefined) ?? config;
} catch {
// Fall back to the planning snapshot; apply still rechecks inside mutate.
}
return hasAuthProfileConfigConflict(config, profile, Boolean(ctx.overwrite));
}
export async function applyAuthProfileConfigWithConflictCheck(params: {
ctx: MigrationProviderContext;
profile: HermesAuthProfileConfig;
applyConfigPatch?: (config: OpenClawConfig) => OpenClawConfig;
}): Promise<HermesAuthConfigApplyResult> {
const configApi = resolveMigrationConfigRuntime(params.ctx);
if (!configApi?.current || !configApi.mutateConfigFile) {
return "unavailable";
}
try {
await configApi.mutateConfigFile({
base: "runtime",
afterWrite: { mode: "auto" },
mutate(draft) {
let next = draft;
if (params.applyConfigPatch) {
next = params.applyConfigPatch(next);
}
if (hasAuthProfileConfigConflict(next, params.profile, Boolean(params.ctx.overwrite))) {
throw new HermesAuthConfigConflict();
}
next = applyAuthProfileConfig(next, {
profileId: params.profile.profileId,
provider: params.profile.provider,
mode: params.profile.mode,
...(params.profile.email ? { email: params.profile.email } : {}),
...(params.profile.displayName ? { displayName: params.profile.displayName } : {}),
preferProfileFirst: false,
});
replaceConfigDraft(draft, next);
},
});
return "configured";
} catch (error) {
return error instanceof HermesAuthConfigConflict ? "conflict" : "unavailable";
}
}