Skip to content

Commit ac2dbfc

Browse files
committed
docs: document auth profile persistence helpers
1 parent d6ab1fd commit ac2dbfc

6 files changed

Lines changed: 47 additions & 2 deletions

File tree

src/agents/auth-profiles/clone.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { AuthProfileStore } from "./types.js";
22

3+
/** Deep-clones an auth profile store and rejects non-JSON values. */
34
export function cloneAuthProfileStore(store: AuthProfileStore): AuthProfileStore {
45
return JSON.parse(
56
JSON.stringify(store, (_key, value: unknown) => {

src/agents/auth-profiles/persisted.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import type {
3030
OAuthCredentials,
3131
} from "./types.js";
3232

33+
/** Legacy auth.json store shape before auth-profiles.json/SQLite. */
3334
export type LegacyAuthStore = Record<string, AuthProfileCredential>;
3435

3536
type LoadPersistedAuthProfileStoreOptions = {
@@ -42,6 +43,8 @@ type RejectedCredentialEntry = { key: string; reason: CredentialRejectReason };
4243

4344
const AUTH_PROFILE_TYPES = new Set<AuthProfileCredential["type"]>(["api_key", "oauth", "token"]);
4445

46+
// Persisted credential normalization accepts old field names and SecretRef-ish
47+
// values, then emits the current credential discriminated union.
4548
function normalizeOptionalCredentialString(value: unknown): string | undefined {
4649
if (typeof value !== "string") {
4750
return undefined;
@@ -70,6 +73,8 @@ function normalizeCredentialMetadata(value: unknown): Record<string, string> | u
7073
return Object.keys(metadata).length > 0 ? metadata : undefined;
7174
}
7275

76+
// Secret-backed key/token fields may have been stored in the value field by old
77+
// writers. Move them to the ref field so secret values are not treated as text.
7378
function normalizeSecretBackedField(params: {
7479
entry: Record<string, unknown>;
7580
valueField: "key" | "token";
@@ -258,6 +263,7 @@ function coerceLegacyAuthStore(raw: unknown): LegacyAuthStore | null {
258263
return Object.keys(entries).length > 0 ? entries : null;
259264
}
260265

266+
/** Coerces a persisted auth profile store payload into the current store shape. */
261267
export function coercePersistedAuthProfileStore(raw: unknown): AuthProfileStore | null {
262268
if (!isRecord(raw)) {
263269
return null;
@@ -286,6 +292,8 @@ export function coercePersistedAuthProfileStore(raw: unknown): AuthProfileStore
286292
};
287293
}
288294

295+
// Merge store/state records by key. Undefined means "no persisted record", not
296+
// "empty override", so preserve the other side in that case.
289297
function mergeRecord<T>(
290298
base?: Record<string, T>,
291299
override?: Record<string, T>,
@@ -306,6 +314,8 @@ function dedupeMergedProfileOrder(profileIds: string[]): string[] {
306314
return uniqueStrings(profileIds);
307315
}
308316

317+
// Legacy OAuth profiles may be replaced by safer main-store profiles when the
318+
// main store has a newer compatible credential for the same provider identity.
309319
function hasComparableOAuthIdentityConflict(
310320
existing: OAuthCredential,
311321
candidate: OAuthCredential,
@@ -498,6 +508,7 @@ function reconcileMainStoreOAuthProfileDrift(params: {
498508
});
499509
}
500510

511+
/** Merges two auth profile stores, preserving valid runtime external profile metadata. */
501512
export function mergeAuthProfileStores(
502513
base: AuthProfileStore,
503514
override: AuthProfileStore,
@@ -525,6 +536,8 @@ export function mergeAuthProfileStores(
525536
: [],
526537
);
527538
const profiles = { ...base.profiles, ...override.profiles };
539+
// Authoritative runtime snapshots may remove stale external profiles that are
540+
// no longer observed, unless the caller is intentionally preserving base ones.
528541
for (const profileId of removedRuntimeExternalProfileIds) {
529542
delete profiles[profileId];
530543
}
@@ -596,6 +609,7 @@ export function mergeAuthProfileStores(
596609
});
597610
}
598611

612+
/** Builds the persisted secrets store, stripping resolved literals when refs exist. */
599613
export function buildPersistedAuthProfileSecretsStore(
600614
store: AuthProfileStore,
601615
shouldPersistProfile?: (params: {
@@ -628,6 +642,7 @@ export function buildPersistedAuthProfileSecretsStore(
628642
};
629643
}
630644

645+
/** Applies legacy auth.json credentials into an auth profile store. */
631646
export function applyLegacyAuthStore(store: AuthProfileStore, legacy: LegacyAuthStore): void {
632647
for (const [provider, cred] of Object.entries(legacy)) {
633648
const profileId = `${provider}:default`;
@@ -665,6 +680,7 @@ export function applyLegacyAuthStore(store: AuthProfileStore, legacy: LegacyAuth
665680
}
666681
}
667682

683+
/** Imports the legacy oauth.json file into missing default OAuth profiles. */
668684
export function mergeOAuthFileIntoStore(store: AuthProfileStore): boolean {
669685
const oauthPath = resolveOAuthPath();
670686
const oauthRaw = loadJsonFile(oauthPath);
@@ -691,6 +707,7 @@ export function mergeOAuthFileIntoStore(store: AuthProfileStore): boolean {
691707
return mutated;
692708
}
693709

710+
/** Loads the persisted auth profile store and merges runtime state. */
694711
export function loadPersistedAuthProfileStore(
695712
agentDir?: string,
696713
options?: LoadPersistedAuthProfileStoreOptions,
@@ -710,6 +727,7 @@ export function loadPersistedAuthProfileStore(
710727
return merged;
711728
}
712729

730+
/** Loads the legacy auth.json auth profile store if present. */
713731
export function loadLegacyAuthProfileStore(agentDir?: string): LegacyAuthStore | null {
714732
return coerceLegacyAuthStore(loadJsonFile(resolveLegacyAuthStorePath(agentDir)));
715733
}

src/agents/auth-profiles/sqlite.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ type AuthProfileDatabase = Pick<
2424
"auth_profile_store" | "auth_profile_state"
2525
>;
2626

27+
// Auth profiles store one JSON blob for secrets and one JSON blob for runtime
28+
// state. SQLite owns durability/transactions; JSON shape owns compatibility.
2729
const PRIMARY_ROW_KEY = "primary";
2830

2931
function resolveAgentDir(agentDir?: string): string {
@@ -42,6 +44,8 @@ function inferAgentIdFromDir(agentDir: string): string {
4244
return `custom-${hash}`;
4345
}
4446

47+
// The auth database lives in the agent dir and shares the openclaw-agent schema
48+
// so auth store/state can move with the rest of agent-local durable state.
4549
function resolveAuthProfileDatabaseOptions(agentDir?: string) {
4650
const dir = resolveAgentDir(agentDir);
4751
return {
@@ -50,15 +54,19 @@ function resolveAuthProfileDatabaseOptions(agentDir?: string) {
5054
};
5155
}
5256

57+
/** Resolves the SQLite database path that stores auth profiles for an agent dir. */
5358
export function resolveAuthProfileDatabasePath(agentDir?: string): string {
5459
return resolveAuthProfileDatabaseOptions(agentDir).path;
5560
}
5661

62+
/** Resolves the SQLite database and sidecar paths used by auth profiles. */
5763
export function resolveAuthProfileDatabaseFilePaths(agentDir?: string): string[] {
5864
const databasePath = resolveAuthProfileDatabasePath(agentDir);
5965
return [databasePath, `${databasePath}-wal`, `${databasePath}-shm`];
6066
}
6167

68+
// Read-only probes must tolerate old/corrupt/missing rows. Coercion happens
69+
// above this layer; this layer only returns raw JSON-ish payloads.
6270
function parseJsonCell(raw: string | null | undefined): unknown {
6371
if (!raw) {
6472
return null;
@@ -74,6 +82,7 @@ function getAuthProfileKysely(db: DatabaseSync) {
7482
return getNodeSqliteKysely<AuthProfileDatabase>(db);
7583
}
7684

85+
/** Opens the auth profile SQLite database for an agent dir. */
7786
export function openAuthProfileDatabase(agentDir?: string): OpenClawAgentDatabase {
7887
return openOpenClawAgentDatabase(resolveAuthProfileDatabaseOptions(agentDir));
7988
}
@@ -109,6 +118,7 @@ function readAuthProfileJsonCellReadOnly(pathname: string, target: "store" | "st
109118
}
110119
}
111120

121+
/** Reads the raw persisted auth profile store payload. */
112122
export function readPersistedAuthProfileStoreRaw(
113123
agentDir?: string,
114124
database?: OpenClawAgentDatabase,
@@ -131,6 +141,7 @@ export function readPersistedAuthProfileStoreRaw(
131141
return readAuthProfileJsonCellReadOnly(databasePath, "store");
132142
}
133143

144+
/** Reads the raw persisted auth profile runtime state payload. */
134145
export function readPersistedAuthProfileStateRaw(
135146
agentDir?: string,
136147
database?: OpenClawAgentDatabase,
@@ -153,6 +164,7 @@ export function readPersistedAuthProfileStateRaw(
153164
return readAuthProfileJsonCellReadOnly(databasePath, "state");
154165
}
155166

167+
/** Writes the raw persisted auth profile store payload. */
156168
export function writePersistedAuthProfileStoreRaw(
157169
payload: unknown,
158170
agentDir?: string,
@@ -184,6 +196,7 @@ export function writePersistedAuthProfileStoreRaw(
184196
runOpenClawAgentWriteTransaction(write, resolveAuthProfileDatabaseOptions(agentDir));
185197
}
186198

199+
/** Deletes the raw persisted auth profile store payload. */
187200
export function deletePersistedAuthProfileStoreRaw(
188201
agentDir?: string,
189202
database?: OpenClawAgentDatabase,
@@ -202,6 +215,7 @@ export function deletePersistedAuthProfileStoreRaw(
202215
runOpenClawAgentWriteTransaction(remove, resolveAuthProfileDatabaseOptions(agentDir));
203216
}
204217

218+
/** Writes or deletes the raw persisted auth profile runtime state payload. */
205219
export function writePersistedAuthProfileStateRaw(
206220
payload: unknown,
207221
agentDir?: string,
@@ -240,6 +254,7 @@ export function writePersistedAuthProfileStateRaw(
240254
runOpenClawAgentWriteTransaction(write, resolveAuthProfileDatabaseOptions(agentDir));
241255
}
242256

257+
/** Runs an auth-profile database write transaction. */
243258
export function runAuthProfileWriteTransaction<T>(
244259
agentDir: string | undefined,
245260
operation: (database: OpenClawAgentDatabase) => T,

src/agents/auth-profiles/state-observation.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { AuthProfileFailureReason, ProfileUsageStats } from "./types.js";
55

66
const observationLog = createSubsystemLogger("agent/embedded");
77

8+
/** Logs an auth profile failure/cooldown/disable state transition. */
89
export function logAuthProfileFailureStateChange(params: {
910
runId?: string;
1011
profileId: string;
@@ -18,8 +19,8 @@ export function logAuthProfileFailureStateChange(params: {
1819
params.reason === "billing" || params.reason === "auth_permanent" ? "disabled" : "cooldown";
1920
const previousCooldownUntil = params.previous?.cooldownUntil;
2021
const previousDisabledUntil = params.previous?.disabledUntil;
21-
// Active cooldown/disable windows are intentionally immutable; log whether this
22-
// update reused the existing window instead of extending it.
22+
// Active cooldown/disable windows are intentionally immutable; log whether
23+
// this update reused the existing window instead of extending it.
2324
const windowReused =
2425
windowType === "disabled"
2526
? typeof previousDisabledUntil === "number" &&

src/agents/auth-profiles/state.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const AUTH_FAILURE_REASONS = new Set<AuthProfileFailureReason>([
3434
const AUTH_BLOCKED_REASONS = new Set<AuthProfileBlockedReason>(["subscription_limit"]);
3535
const AUTH_BLOCKED_SOURCES = new Set<AuthProfileBlockedSource>(["codex_rate_limits", "wham"]);
3636

37+
// Runtime auth state is operator-controlled durability. Coerce every persisted
38+
// field through closed enums/numbers so bad rows do not poison auth selection.
3739
function normalizeFiniteNumber(value: unknown): number | undefined {
3840
return asFiniteNumber(value);
3941
}
@@ -145,6 +147,7 @@ function normalizeUsageStats(raw: unknown): AuthProfileState["usageStats"] {
145147
return Object.keys(normalized).length > 0 ? normalized : undefined;
146148
}
147149

150+
/** Coerces persisted auth profile runtime state into the current shape. */
148151
export function coerceAuthProfileState(raw: unknown): AuthProfileState {
149152
if (!isRecord(raw)) {
150153
return {};
@@ -156,6 +159,7 @@ export function coerceAuthProfileState(raw: unknown): AuthProfileState {
156159
};
157160
}
158161

162+
/** Merges auth profile runtime state, with override records winning per key. */
159163
export function mergeAuthProfileState(
160164
base: AuthProfileState,
161165
override: AuthProfileState,
@@ -180,13 +184,15 @@ export function mergeAuthProfileState(
180184
};
181185
}
182186

187+
/** Loads persisted auth profile runtime state from SQLite. */
183188
export function loadPersistedAuthProfileState(
184189
agentDir?: string,
185190
database?: OpenClawAgentDatabase,
186191
): AuthProfileState {
187192
return coerceAuthProfileState(readPersistedAuthProfileStateRaw(agentDir, database));
188193
}
189194

195+
/** Builds the persisted auth profile runtime state payload. */
190196
export function buildPersistedAuthProfileState(
191197
store: AuthProfileState,
192198
): AuthProfileStateStore | null {
@@ -202,6 +208,7 @@ export function buildPersistedAuthProfileState(
202208
};
203209
}
204210

211+
/** Saves auth profile runtime state when it differs from the persisted payload. */
205212
export function savePersistedAuthProfileState(
206213
store: AuthProfileState,
207214
agentDir?: string,

src/agents/auth-profiles/upsert-with-lock.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
22
import { updateAuthProfileStoreWithLock } from "./store.js";
33
import type { AuthProfileCredential, AuthProfileStore } from "./types.js";
44

5+
// Upserts normalize literal secrets before persistence; SecretRef fields are
6+
// preserved by leaving non-string key/token values untouched.
57
function normalizeAuthProfileCredential(credential: AuthProfileCredential): AuthProfileCredential {
68
if (credential.type === "api_key") {
79
if (typeof credential.key !== "string") {
@@ -25,6 +27,7 @@ function normalizeAuthProfileCredential(credential: AuthProfileCredential): Auth
2527
return credential;
2628
}
2729

30+
/** Upserts an auth profile under the store lock, returning null on write failure. */
2831
export async function upsertAuthProfileWithLock(params: {
2932
profileId: string;
3033
credential: AuthProfileCredential;

0 commit comments

Comments
 (0)