mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 21:23:23 +00:00
fix(auth): prevent stale auth store reverts (#53211)
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Control UI/auth: preserve operator scopes through the device-auth bypass path, ignore cached under-scoped operator tokens, and show a clear `operator.read` fallback message when a connection really lacks read scope, so operator sessions stop failing or blanking on read-backed pages. (#53110) Thanks @BunsDev.
|
||||
- Plugins/uninstall: accept installed `clawhub:` specs and versionless ClawHub package names as uninstall targets, so `openclaw plugins uninstall clawhub:<package>` works again even when the recorded install was pinned to a version.
|
||||
- Auth/OpenAI tokens: stop live gateway auth-profile writes from reverting freshly saved credentials back to stale in-memory values, and make `models auth paste-token` write to the resolved agent store, so Configure, Onboard, and token-paste flows stop snapping back to expired OpenAI tokens. Fixes #53207. Related to #45516.
|
||||
- Agents/failover: classify generic `api_error` payloads as retryable only when they include transient failure signals, so MiniMax-style backend failures still trigger model fallback without misclassifying billing, auth, or format/context errors. (#49611) Thanks @ayushozha.
|
||||
- Diagnostics/cache trace: strip credential fields from cache-trace JSONL output while preserving non-sensitive diagnostic fields and image redaction metadata.
|
||||
- Docs/Feishu: replace `botName` with `name` in the channel config examples so the docs match the strict account schema for per-account display names. (#52753) Thanks @haroldfabla2-hue.
|
||||
|
||||
@@ -3,9 +3,11 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
calculateAuthProfileCooldownMs,
|
||||
ensureAuthProfileStore,
|
||||
markAuthProfileFailure,
|
||||
replaceRuntimeAuthProfileStoreSnapshots,
|
||||
} from "./auth-profiles.js";
|
||||
|
||||
type AuthProfileStore = ReturnType<typeof ensureAuthProfileStore>;
|
||||
@@ -48,6 +50,73 @@ function expectCooldownInRange(remainingMs: number, minMs: number, maxMs: number
|
||||
}
|
||||
|
||||
describe("markAuthProfileFailure", () => {
|
||||
it("does not overwrite fresher on-disk credentials with a stale runtime snapshot", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
try {
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-expired-old",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
agentDir,
|
||||
store: ensureAuthProfileStore(agentDir),
|
||||
},
|
||||
]);
|
||||
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-fresh-new",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const staleRuntimeStore = ensureAuthProfileStore(agentDir);
|
||||
const staleCredential = staleRuntimeStore.profiles["openai:default"];
|
||||
expect(staleCredential?.type).toBe("api_key");
|
||||
expect(staleCredential && "key" in staleCredential ? staleCredential.key : undefined).toBe(
|
||||
"sk-expired-old",
|
||||
);
|
||||
|
||||
await markAuthProfileFailure({
|
||||
store: staleRuntimeStore,
|
||||
profileId: "openai:default",
|
||||
reason: "rate_limit",
|
||||
agentDir,
|
||||
});
|
||||
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
const reloaded = ensureAuthProfileStore(agentDir);
|
||||
const reloadedCredential = reloaded.profiles["openai:default"];
|
||||
expect(reloadedCredential?.type).toBe("api_key");
|
||||
expect(
|
||||
reloadedCredential && "key" in reloadedCredential ? reloadedCredential.key : undefined,
|
||||
).toBe("sk-fresh-new");
|
||||
expect(typeof reloaded.usageStats?.["openai:default"]?.cooldownUntil).toBe("number");
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("disables billing failures for ~5 hours by default", async () => {
|
||||
await withAuthProfileStore(async ({ agentDir, store }) => {
|
||||
const startedAt = Date.now();
|
||||
|
||||
@@ -130,7 +130,10 @@ export async function updateAuthProfileStoreWithLock(params: {
|
||||
|
||||
try {
|
||||
return await withFileLock(authPath, AUTH_STORE_LOCK_OPTIONS, async () => {
|
||||
const store = ensureAuthProfileStore(params.agentDir);
|
||||
// Locked writers must reload from disk, not from any runtime snapshot.
|
||||
// Otherwise a live gateway can overwrite fresher CLI/config-auth writes
|
||||
// with stale in-memory auth state during usage/cooldown updates.
|
||||
const store = loadAuthProfileStoreForAgent(params.agentDir);
|
||||
const shouldSave = params.updater(store);
|
||||
if (shouldSave) {
|
||||
saveAuthProfileStore(store, params.agentDir);
|
||||
|
||||
@@ -315,6 +315,23 @@ describe("modelsAuthLoginCommand", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("writes pasted tokens to the resolved agent store", async () => {
|
||||
const runtime = createRuntime();
|
||||
mocks.clackText.mockResolvedValue("tok-fresh");
|
||||
|
||||
await modelsAuthPasteTokenCommand({ provider: "openai" }, runtime);
|
||||
|
||||
expect(mocks.upsertAuthProfile).toHaveBeenCalledWith({
|
||||
profileId: "openai:manual",
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "openai",
|
||||
token: "tok-fresh",
|
||||
},
|
||||
agentDir: "/tmp/openclaw/agents/main",
|
||||
});
|
||||
});
|
||||
|
||||
it("runs token auth for any token-capable provider plugin", async () => {
|
||||
const runtime = createRuntime();
|
||||
const runTokenAuth = vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -359,6 +359,7 @@ export async function modelsAuthPasteTokenCommand(
|
||||
},
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const { agentDir } = await resolveModelsAuthContext();
|
||||
const rawProvider = opts.provider?.trim();
|
||||
if (!rawProvider) {
|
||||
throw new Error("Missing --provider.");
|
||||
@@ -385,6 +386,7 @@ export async function modelsAuthPasteTokenCommand(
|
||||
token,
|
||||
...(expires ? { expires } : {}),
|
||||
},
|
||||
agentDir,
|
||||
});
|
||||
|
||||
await updateConfig((cfg) => applyAuthProfileConfig(cfg, { profileId, provider, mode: "token" }));
|
||||
|
||||
Reference in New Issue
Block a user