Skip to content

Commit ef17cec

Browse files
committed
docs: document auth profile store helpers
1 parent 8835787 commit ef17cec

5 files changed

Lines changed: 48 additions & 1 deletion

File tree

src/agents/auth-profiles.runtime.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { ensureAuthProfileStore as ensureAuthProfileStoreImpl } from "./auth-profiles/store.js";
22

3+
// Runtime seam for auth-profile store loading. Tests can stub this facade without
4+
// importing the full auth profile store implementation.
35
type EnsureAuthProfileStore = typeof import("./auth-profiles/store.js").ensureAuthProfileStore;
46

7+
/** Ensure an auth-profile store using the production store implementation. */
58
export function ensureAuthProfileStore(
69
...args: Parameters<EnsureAuthProfileStore>
710
): ReturnType<EnsureAuthProfileStore> {

src/agents/auth-profiles/external-cli-auth-selection.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ import { resolveProviderIdForAuth } from "../provider-auth-aliases.js";
88
import { CLAUDE_CLI_PROFILE_ID } from "./constants.js";
99
import type { AuthProfileStore } from "./types.js";
1010

11+
// Resolves which external CLI auth overlays are relevant for the currently
12+
// selected provider/model/profile. This keeps runtime discovery scoped to the
13+
// selected auth path instead of scanning every CLI profile.
1114
const CLAUDE_CLI_PROVIDER_ID = "claude-cli";
1215

16+
/** Resolve external CLI overlay scope from the user's auth/model selection. */
1317
export function resolveExternalCliAuthOverlayScopeFromSelection(params: {
1418
provider: string;
1519
cfg?: OpenClawConfig;
@@ -49,6 +53,8 @@ export function resolveExternalCliAuthOverlayScopeFromSelection(params: {
4953
return {
5054
...(providerIds.length > 0 ? { providerIds } : {}),
5155
ignoreAutoPreferredProfile:
56+
// Claude CLI should not auto-prefer a profile when runtime selection has
57+
// already chosen Claude CLI and the user did not lock a profile.
5258
!params.userLockedAuthProfileId && selectedProvider === CLAUDE_CLI_PROVIDER_ID,
5359
};
5460
}
@@ -64,6 +70,8 @@ function resolveExternalCliAuthScopeFromAuthSelection(params: {
6470
selectedProviderId?: string;
6571
} {
6672
if (params.userLockedAuthProfileId) {
73+
// Locked profile id means discovery should be scoped to that exact profile's
74+
// compatible external CLI provider, if any.
6775
const providerId = resolveExternalCliProviderIdForCompatibleAuthProfile({
6876
...params,
6977
profileId: params.userLockedAuthProfileId,
@@ -124,7 +132,8 @@ function resolveExternalCliAuthScopeFromAuthSelection(params: {
124132
return {
125133
providerIds: uniqueProviderIds,
126134
...(compatibleProfileCount === 1 && uniqueProviderIds[0]
127-
? { selectedProviderId: uniqueProviderIds[0] }
135+
? // Without explicit order, select only when compatibility is unambiguous.
136+
{ selectedProviderId: uniqueProviderIds[0] }
128137
: {}),
129138
};
130139
}

src/agents/auth-profiles/legacy-oauth-ref.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { isRecord } from "@openclaw/normalization-core/record-coerce";
22

3+
// Legacy OAuth references used by older Codex/OpenClaw credential files. Keep
4+
// this recognizer strict so migration code only preserves known legacy refs.
35
export const LEGACY_OAUTH_REF_SOURCE = "openclaw-credentials";
46
export const LEGACY_OAUTH_REF_PROVIDER = "openai-codex";
57

@@ -9,6 +11,7 @@ export type LegacyOAuthRef = {
911
id: string;
1012
};
1113

14+
/** Return true for the legacy OAuth reference shape persisted by older stores. */
1215
export function isLegacyOAuthRef(value: unknown): value is LegacyOAuthRef {
1316
if (!isRecord(value)) {
1417
return false;

src/agents/auth-profiles/oauth-refresh-failure.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
22
import { sanitizeForLog } from "../../../packages/terminal-core/src/ansi.js";
33
import { formatCliCommand } from "../../cli/command-format.js";
44

5+
// OAuth refresh failure classifiers for status/help text. Providers and reasons
6+
// are sanitized before being used in logs or suggested commands.
57
export type OAuthRefreshFailureReason =
68
| "refresh_token_reused"
79
| "invalid_grant"
@@ -14,6 +16,7 @@ export type OAuthRefreshFailure = {
1416
reason: OAuthRefreshFailureReason | null;
1517
};
1618

19+
/** Error type that carries provider and classified OAuth refresh failure reason. */
1720
export class OAuthRefreshFailureError extends Error {
1821
readonly provider: string;
1922
readonly reason: OAuthRefreshFailureReason | null;
@@ -44,11 +47,13 @@ function extractOAuthRefreshFailureProvider(message: string): string | null {
4447
}
4548

4649
function sanitizeOAuthRefreshFailureProvider(provider: string | null | undefined): string | null {
50+
// Only return normalized provider ids that are safe to embed in shell guidance.
4751
const sanitized = provider ? sanitizeForLog(provider).replaceAll("`", "").trim() : "";
4852
const normalized = normalizeProviderId(sanitized);
4953
return normalized && SAFE_PROVIDER_ID_RE.test(normalized) ? normalized : null;
5054
}
5155

56+
/** Classify a raw OAuth refresh failure message into a stable reason code. */
5257
export function classifyOAuthRefreshFailureReason(
5358
message: string,
5459
): OAuthRefreshFailureReason | null {
@@ -71,6 +76,7 @@ export function classifyOAuthRefreshFailureReason(
7176
return null;
7277
}
7378

79+
/** Classify provider/reason from a user-facing OAuth refresh failure message. */
7480
export function classifyOAuthRefreshFailure(message: string): OAuthRefreshFailure | null {
7581
if (!isOAuthRefreshFailureMessage(message)) {
7682
return null;
@@ -81,6 +87,7 @@ export function classifyOAuthRefreshFailure(message: string): OAuthRefreshFailur
8187
};
8288
}
8389

90+
/** Classify provider/reason from the structured OAuth refresh failure error. */
8491
export function classifyOAuthRefreshFailureError(err: unknown): OAuthRefreshFailure | null {
8592
if (!(err instanceof OAuthRefreshFailureError)) {
8693
return null;
@@ -91,6 +98,7 @@ export function classifyOAuthRefreshFailureError(err: unknown): OAuthRefreshFail
9198
};
9299
}
93100

101+
/** Build the login command operators should run after OAuth refresh failure. */
94102
export function buildOAuthRefreshFailureLoginCommand(provider: string | null | undefined): string {
95103
const sanitizedProvider = sanitizeOAuthRefreshFailureProvider(provider);
96104
return sanitizedProvider

src/agents/auth-profiles/store.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ import {
4343
} from "./state.js";
4444
import type { AuthProfileStore } from "./types.js";
4545

46+
// Auth profile store orchestration. This module merges persisted stores,
47+
// runtime snapshots, inherited main-agent OAuth profiles, and external CLI
48+
// overlays while keeping save paths local and secret-safe.
4649
type LoadAuthProfileStoreOptions = {
4750
allowKeychainPrompt?: boolean;
4851
config?: OpenClawConfig;
@@ -108,6 +111,8 @@ function preserveLegacyOAuthRefsOnSave(params: {
108111
) {
109112
continue;
110113
}
114+
// Preserve legacy oauthRef ownership when current save data did not replace
115+
// inline OAuth material; otherwise older credential references would be lost.
111116
nextProfiles ??= { ...params.payload.profiles };
112117
nextProfiles[profileId] = {
113118
...credential,
@@ -159,6 +164,8 @@ function isInheritedMainOAuthCredential(params: {
159164
return false;
160165
}
161166

167+
// Local agent stores can inherit main OAuth credentials. Do not persist the
168+
// inherited copy unless the local store actually owns or improves it.
162169
const mainCredential = loadPersistedAuthProfileStore()?.profiles[params.profileId];
163170
return (
164171
mainCredential?.type === "oauth" &&
@@ -308,6 +315,8 @@ function maybeSyncPersistedExternalCliAuthProfiles(params: {
308315
}
309316

310317
try {
318+
// External CLI sync writes only profiles that still match the loaded
319+
// baseline, avoiding overwrite of concurrent local auth changes.
311320
return runAuthProfileWriteTransaction(params.agentDir, (database) => {
312321
const latestStore = loadPersistedAuthProfileStore(params.agentDir, {
313322
...resolvePersistedLoadOptions(params.options),
@@ -373,6 +382,8 @@ function shouldKeepProfileInLocalStore(params: {
373382
return true;
374383
}
375384
if (params.store.runtimeExternalProfileIds?.includes(params.profileId)) {
385+
// Runtime external profiles are normally overlays. Persist only when they
386+
// have explicit local state or differ from the runtime snapshot.
376387
const persistedCredential = loadPersistedAuthProfileStore(params.agentDir)?.profiles[
377388
params.profileId
378389
];
@@ -702,6 +713,7 @@ function mergeRuntimeExternalProfileState(params: {
702713
return merged;
703714
}
704715

716+
/** Apply an auth store update inside the SQLite write lock. */
705717
export async function updateAuthProfileStoreWithLock(params: {
706718
agentDir?: string;
707719
saveOptions?: SaveAuthProfileStoreOptions;
@@ -725,6 +737,7 @@ export async function updateAuthProfileStoreWithLock(params: {
725737
}
726738
}
727739

740+
/** Load the main auth profile store with runtime external profiles overlaid. */
728741
export function loadAuthProfileStore(): AuthProfileStore {
729742
const asStore = loadPersistedAuthProfileStore();
730743
if (asStore) {
@@ -797,6 +810,7 @@ export function loadAuthProfileStoreForRuntime(
797810
);
798811
}
799812

813+
/** Load auth profiles for secret resolution without keychain prompts or writes. */
800814
export function loadAuthProfileStoreForSecretsRuntime(
801815
agentDir?: string,
802816
options?: Pick<
@@ -811,6 +825,7 @@ export function loadAuthProfileStoreForSecretsRuntime(
811825
});
812826
}
813827

828+
/** Load auth profiles with runtime external profiles removed from the result. */
814829
export function loadAuthProfileStoreWithoutExternalProfiles(
815830
agentDir?: string,
816831
loadOptions?: Pick<LoadAuthProfileStoreOptions, "allowKeychainPrompt">,
@@ -832,6 +847,7 @@ export function loadAuthProfileStoreWithoutExternalProfiles(
832847
});
833848
}
834849

850+
/** Ensure an auth store is available, including runtime/external profile overlays. */
835851
export function ensureAuthProfileStore(
836852
agentDir?: string,
837853
options?: {
@@ -862,6 +878,7 @@ export function ensureAuthProfileStore(
862878
});
863879
}
864880

881+
/** Ensure an auth store is available without external profile overlays. */
865882
export function ensureAuthProfileStoreWithoutExternalProfiles(
866883
agentDir?: string,
867884
options?: {
@@ -894,6 +911,7 @@ export function ensureAuthProfileStoreWithoutExternalProfiles(
894911
});
895912
}
896913

914+
/** Find a persisted credential in the scoped store, falling back to the main store. */
897915
export function findPersistedAuthProfileCredential(params: {
898916
agentDir?: string;
899917
profileId: string;
@@ -913,6 +931,7 @@ export function findPersistedAuthProfileCredential(params: {
913931
return loadPersistedAuthProfileStore()?.profiles[params.profileId];
914932
}
915933

934+
/** Resolve which agent dir owns a persisted profile, accounting for inherited OAuth. */
916935
export function resolvePersistedAuthProfileOwnerAgentDir(params: {
917936
agentDir?: string;
918937
profileId: string;
@@ -941,6 +960,7 @@ export function resolvePersistedAuthProfileOwnerAgentDir(params: {
941960
return mainStore?.profiles[params.profileId] ? undefined : params.agentDir;
942961
}
943962

963+
/** Load the store shape used when applying local-only auth updates. */
944964
export function ensureAuthProfileStoreForLocalUpdate(agentDir?: string): AuthProfileStore {
945965
const options: LoadAuthProfileStoreOptions = { syncExternalCli: false };
946966
const store = loadAuthProfileStoreForAgent(agentDir, options);
@@ -961,22 +981,26 @@ export function ensureAuthProfileStoreForLocalUpdate(agentDir?: string): AuthPro
961981

962982
export { hasAnyAuthProfileStoreSource, hasLocalAuthProfileStoreSource } from "./source-check.js";
963983

984+
/** Return the current runtime auth-profile snapshot for an agent dir. */
964985
export function getRuntimeAuthProfileStoreSnapshot(
965986
agentDir?: string,
966987
): AuthProfileStore | undefined {
967988
return getRuntimeAuthProfileStoreSnapshotImpl(agentDir);
968989
}
969990

991+
/** Replace runtime auth-profile snapshots, used by tests and prepared runtimes. */
970992
export function replaceRuntimeAuthProfileStoreSnapshots(
971993
entries: Array<{ agentDir?: string; store: AuthProfileStore }>,
972994
): void {
973995
replaceRuntimeAuthProfileStoreSnapshotsImpl(entries);
974996
}
975997

998+
/** Clear all runtime auth-profile snapshots. */
976999
export function clearRuntimeAuthProfileStoreSnapshots(): void {
9771000
clearRuntimeAuthProfileStoreSnapshotsImpl();
9781001
}
9791002

1003+
/** Save the auth profile store plus sidecar state, preserving runtime overlay metadata. */
9801004
export function saveAuthProfileStore(
9811005
store: AuthProfileStore,
9821006
agentDir?: string,

0 commit comments

Comments
 (0)