mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-04 00:34:02 +00:00
perf(gateway): skip unchanged auth persistence writes
This commit is contained in:
@@ -1160,6 +1160,121 @@ describe("saveAuthProfileStore", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not rewrite auth secrets when only runtime scheduling state changes", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-state-only-"));
|
||||
try {
|
||||
const profileId = "anthropic:default";
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-anthropic-plain",
|
||||
},
|
||||
},
|
||||
usageStats: {
|
||||
[profileId]: { lastUsed: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
saveAuthProfileStore(store, agentDir);
|
||||
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
const statePath = resolveAuthStatePath(agentDir);
|
||||
const oldTimestamp = new Date("2001-01-01T00:00:00.000Z");
|
||||
await fs.utimes(authPath, oldTimestamp, oldTimestamp);
|
||||
await fs.utimes(statePath, oldTimestamp, oldTimestamp);
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
...store,
|
||||
usageStats: {
|
||||
[profileId]: { lastUsed: 2 },
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
expect((await fs.stat(authPath)).mtimeMs).toBe(oldTimestamp.getTime());
|
||||
expect((await fs.stat(statePath)).mtimeMs).toBeGreaterThan(oldTimestamp.getTime());
|
||||
|
||||
const authProfiles = JSON.parse(await fs.readFile(authPath, "utf8")) as {
|
||||
usageStats?: unknown;
|
||||
};
|
||||
expect(authProfiles.usageStats).toBeUndefined();
|
||||
|
||||
const authState = JSON.parse(await fs.readFile(statePath, "utf8")) as {
|
||||
usageStats?: Record<string, { lastUsed?: number }>;
|
||||
};
|
||||
expect(authState.usageStats?.[profileId]?.lastUsed).toBe(2);
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"repairs auth secrets permissions when the payload is unchanged",
|
||||
async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-permissions-"));
|
||||
try {
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-anthropic-plain",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
saveAuthProfileStore(store, agentDir);
|
||||
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
await fs.chmod(authPath, 0o644);
|
||||
|
||||
saveAuthProfileStore(store, agentDir);
|
||||
|
||||
expect((await fs.stat(authPath)).mode & 0o777).toBe(0o600);
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("does not rewrite unchanged runtime scheduling state", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-state-same-"));
|
||||
try {
|
||||
const profileId = "anthropic:default";
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-anthropic-plain",
|
||||
},
|
||||
},
|
||||
usageStats: {
|
||||
[profileId]: { lastUsed: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
saveAuthProfileStore(store, agentDir);
|
||||
|
||||
const statePath = resolveAuthStatePath(agentDir);
|
||||
const oldTimestamp = new Date("2001-01-01T00:00:00.000Z");
|
||||
await fs.utimes(statePath, oldTimestamp, oldTimestamp);
|
||||
|
||||
saveAuthProfileStore(store, agentDir);
|
||||
|
||||
expect((await fs.stat(statePath)).mtimeMs).toBe(oldTimestamp.getTime());
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not persist unchanged inherited main OAuth when saving secondary local updates", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-inherited-"));
|
||||
const stateDir = path.join(root, ".openclaw");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import { loadJsonFile, repairJsonFilePermissions, saveJsonFile } from "../../infra/json-file.js";
|
||||
import { asFiniteNumber } from "../../shared/number-coercion.js";
|
||||
import { isRecord } from "../../shared/record-coerce.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
@@ -213,6 +214,10 @@ export function savePersistedAuthProfileState(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
saveJsonFile(statePath, payload);
|
||||
if (isDeepStrictEqual(loadJsonFile(statePath), payload)) {
|
||||
repairJsonFilePermissions(statePath);
|
||||
} else {
|
||||
saveJsonFile(statePath, payload);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { withFileLock } from "../../infra/file-lock.js";
|
||||
import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
|
||||
import { loadJsonFile, repairJsonFilePermissions, saveJsonFile } from "../../infra/json-file.js";
|
||||
import { cloneAuthProfileStore } from "./clone.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, log } from "./constants.js";
|
||||
import {
|
||||
@@ -1056,11 +1056,16 @@ export function saveAuthProfileStore(
|
||||
.map(([profileId]) => profileId),
|
||||
);
|
||||
const localStore = buildLocalAuthProfileStoreForSave({ store, agentDir, options });
|
||||
const existingRaw = loadJsonFile(authPath);
|
||||
const payload = buildPersistedAuthProfileSecretsStore(localStore, undefined, {
|
||||
existingRaw: loadJsonFile(authPath),
|
||||
existingRaw,
|
||||
runtimeLegacyOAuthSidecarProfileIds,
|
||||
});
|
||||
saveJsonFile(authPath, payload);
|
||||
if (isDeepStrictEqual(existingRaw, payload)) {
|
||||
repairJsonFilePermissions(authPath);
|
||||
} else {
|
||||
saveJsonFile(authPath, payload);
|
||||
}
|
||||
savePersistedAuthProfileState(localStore, agentDir);
|
||||
writeCachedAuthProfileStore({
|
||||
authPath,
|
||||
|
||||
@@ -50,7 +50,7 @@ import { getTotalQueueSize } from "../process/command-queue.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
clearSecretsRuntimeSnapshot,
|
||||
getActiveSecretsRuntimeSnapshot,
|
||||
getActiveSecretsRuntimeConfigSnapshot,
|
||||
} from "../secrets/runtime-state.js";
|
||||
import { uniqueStrings } from "../shared/string-normalization.js";
|
||||
import { createAuthRateLimiter, type AuthRateLimiter } from "./auth-rate-limit.js";
|
||||
@@ -779,7 +779,8 @@ export async function startGatewayServer(
|
||||
const getResolvedAuth = () =>
|
||||
resolveGatewayAuth({
|
||||
authConfig:
|
||||
getActiveSecretsRuntimeSnapshot()?.config.gateway?.auth ?? getRuntimeConfig().gateway?.auth,
|
||||
getActiveSecretsRuntimeConfigSnapshot()?.config.gateway?.auth ??
|
||||
getRuntimeConfig().gateway?.auth,
|
||||
authOverride: opts.auth,
|
||||
env: process.env,
|
||||
tailscaleMode,
|
||||
|
||||
@@ -36,6 +36,31 @@ export function saveJsonFile(pathname: string, data: unknown): void {
|
||||
writeJsonSync(resolveJsonSaveTarget(pathname), data);
|
||||
}
|
||||
|
||||
export function repairJsonFilePermissions(pathname: string): void {
|
||||
const target = resolveJsonSaveTarget(pathname);
|
||||
let fd: number | undefined;
|
||||
try {
|
||||
fd = fs.openSync(
|
||||
target,
|
||||
fs.constants.O_RDONLY |
|
||||
(process.platform !== "win32" && "O_NOFOLLOW" in fs.constants
|
||||
? fs.constants.O_NOFOLLOW
|
||||
: 0),
|
||||
);
|
||||
fs.fchmodSync(fd, 0o600);
|
||||
} catch {
|
||||
// Matches fs-safe JSON writes: permission repair is best-effort.
|
||||
} finally {
|
||||
if (fd !== undefined) {
|
||||
try {
|
||||
fs.closeSync(fd);
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript-eslint/no-unnecessary-type-parameters -- legacy typed JSON loader alias.
|
||||
export function loadJsonFile<T = unknown>(pathname: string): T | undefined {
|
||||
const direct = tryReadJsonSync<T>(pathname);
|
||||
|
||||
Reference in New Issue
Block a user