mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 20:58:49 +00:00
fix: keep config writes independent of auth profile refs
This commit is contained in:
@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/config: keep config writes from failing on unrelated unresolved auth-profile SecretRefs while preserving live auth-profile runtime snapshots.
|
||||
- Discord/OpenAI: keep realtime Discord voice sessions hearing follow-up turns with OpenAI realtime and prebuffer assistant playback to avoid choppy starts. (#80505) Thanks @Solvely-Colin.
|
||||
- Discord/subagents: route the initial reply from thread-bound delegated sessions into the bound Discord thread instead of the parent channel. Fixes #83170. (#83172) Thanks @100menotu001.
|
||||
- Media: prevent image metadata probing from invoking external decoder delegates on unrecognized image bytes, and stop fallback chaining after real processing errors.
|
||||
|
||||
@@ -100,6 +100,7 @@ import {
|
||||
getRuntimeConfigSnapshotRefreshHandler as getRuntimeConfigSnapshotRefreshHandlerState,
|
||||
setRuntimeConfigSnapshotRefreshHandler as setRuntimeConfigSnapshotRefreshHandlerState,
|
||||
type ConfigWriteAfterWrite,
|
||||
type RuntimeConfigSnapshotRefreshOptions,
|
||||
type RuntimeConfigWriteNotification,
|
||||
} from "./runtime-snapshot.js";
|
||||
import { resolveShellEnvExpectedKeys } from "./shell-env-expected-keys.js";
|
||||
@@ -219,6 +220,10 @@ export type ConfigWriteOptions = {
|
||||
* the post-write runtime snapshot refresh/reload tail entirely.
|
||||
*/
|
||||
skipRuntimeSnapshotRefresh?: boolean;
|
||||
/**
|
||||
* Optional controls for the active runtime snapshot refresh after this write.
|
||||
*/
|
||||
runtimeRefresh?: RuntimeConfigSnapshotRefreshOptions;
|
||||
/**
|
||||
* Allow intentionally destructive config writes, such as explicit reset flows.
|
||||
* Normal writers must keep this false so clobbers are rejected before disk commit.
|
||||
@@ -2546,6 +2551,7 @@ export async function writeConfigFile(
|
||||
// succeeds, so concurrent readers do not observe unresolved SecretRefs mid-refresh.
|
||||
await finalizeRuntimeSnapshotWrite({
|
||||
nextSourceConfig: canonicalSourceConfig,
|
||||
refreshOptions: options.runtimeRefresh,
|
||||
hadRuntimeSnapshot,
|
||||
hadBothSnapshots,
|
||||
loadFreshConfig: () => io.loadConfig(),
|
||||
|
||||
@@ -332,6 +332,7 @@ async function tryWriteSingleTopLevelIncludeMutation(params: {
|
||||
};
|
||||
await finalizeRuntimeSnapshotWrite({
|
||||
nextSourceConfig: refreshedSnapshot.sourceConfig,
|
||||
refreshOptions: params.writeOptions?.runtimeRefresh,
|
||||
hadRuntimeSnapshot,
|
||||
hadBothSnapshots,
|
||||
loadFreshConfig: () => refreshedSnapshot.runtimeConfig,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { OpenClawConfig } from "./types.js";
|
||||
|
||||
export type RuntimeConfigSnapshotRefreshParams = {
|
||||
export type RuntimeConfigSnapshotRefreshOptions = {
|
||||
includeAuthStoreRefs?: boolean;
|
||||
};
|
||||
|
||||
export type RuntimeConfigSnapshotRefreshParams = RuntimeConfigSnapshotRefreshOptions & {
|
||||
sourceConfig: OpenClawConfig;
|
||||
};
|
||||
|
||||
@@ -265,6 +269,7 @@ export function loadPinnedRuntimeConfig(loadFresh: () => OpenClawConfig): OpenCl
|
||||
|
||||
export async function finalizeRuntimeSnapshotWrite(params: {
|
||||
nextSourceConfig: OpenClawConfig;
|
||||
refreshOptions?: RuntimeConfigSnapshotRefreshOptions;
|
||||
hadRuntimeSnapshot: boolean;
|
||||
hadBothSnapshots: boolean;
|
||||
loadFreshConfig: () => OpenClawConfig;
|
||||
@@ -275,7 +280,10 @@ export async function finalizeRuntimeSnapshotWrite(params: {
|
||||
const refreshHandler = getRuntimeConfigSnapshotRefreshHandler();
|
||||
if (refreshHandler) {
|
||||
try {
|
||||
const refreshed = await refreshHandler.refresh({ sourceConfig: params.nextSourceConfig });
|
||||
const refreshed = await refreshHandler.refresh({
|
||||
sourceConfig: params.nextSourceConfig,
|
||||
...params.refreshOptions,
|
||||
});
|
||||
if (refreshed) {
|
||||
params.notifyCommittedWrite();
|
||||
return;
|
||||
|
||||
@@ -215,7 +215,13 @@ export async function commitGatewayConfigWrite(params: {
|
||||
}): Promise<{ path: string; config: OpenClawConfig; queueFollowUp: () => void }> {
|
||||
const result = await replaceConfigFile({
|
||||
nextConfig: params.nextConfig,
|
||||
writeOptions: params.writeOptions,
|
||||
writeOptions: {
|
||||
...params.writeOptions,
|
||||
runtimeRefresh: {
|
||||
...params.writeOptions.runtimeRefresh,
|
||||
includeAuthStoreRefs: false,
|
||||
},
|
||||
},
|
||||
afterWrite: { mode: "auto" },
|
||||
});
|
||||
return {
|
||||
|
||||
@@ -20,7 +20,12 @@ import {
|
||||
GATEWAY_AUTH_SURFACE_PATHS,
|
||||
evaluateGatewayAuthSurfaceStates,
|
||||
} from "../secrets/runtime-gateway-auth-surfaces.js";
|
||||
import { activateSecretsRuntimeSnapshotState } from "../secrets/runtime-state.js";
|
||||
import {
|
||||
activateSecretsRuntimeSnapshotState,
|
||||
getActiveSecretsRuntimeSnapshot,
|
||||
getLiveSecretsRuntimeAuthStores,
|
||||
setPreparedSecretsRuntimeSnapshotRefreshContext,
|
||||
} from "../secrets/runtime-state.js";
|
||||
import { resolveGatewayAuth } from "./auth.js";
|
||||
import { assertGatewayAuthNotKnownWeak } from "./known-weak-gateway-secrets.js";
|
||||
import {
|
||||
@@ -249,17 +254,30 @@ export function createRuntimeSecretsActivator(params: {
|
||||
snapshot,
|
||||
refreshContext: fastPath.refreshContext,
|
||||
refreshHandler: {
|
||||
refresh: async ({ sourceConfig }) => {
|
||||
refresh: async ({ sourceConfig, includeAuthStoreRefs }) => {
|
||||
const secretsRuntime = await loadSecretsRuntime();
|
||||
const activeSnapshot = getActiveSecretsRuntimeSnapshot();
|
||||
const oneShotSkipAuthStoreRefs =
|
||||
includeAuthStoreRefs === false &&
|
||||
fastPath.refreshContext.includeAuthStoreRefs;
|
||||
const refreshed = await secretsRuntime.prepareSecretsRuntimeSnapshot({
|
||||
config: sourceConfig,
|
||||
env: fastPath.refreshContext.env,
|
||||
agentDirs: resolveRefreshAgentDirs(sourceConfig, fastPath.refreshContext),
|
||||
includeAuthStoreRefs:
|
||||
includeAuthStoreRefs ?? fastPath.refreshContext.includeAuthStoreRefs,
|
||||
loadablePluginOrigins: fastPath.refreshContext.loadablePluginOrigins,
|
||||
...(fastPath.usesAuthStoreFallback || !fastPath.refreshContext.loadAuthStore
|
||||
? {}
|
||||
: { loadAuthStore: fastPath.refreshContext.loadAuthStore }),
|
||||
});
|
||||
if (oneShotSkipAuthStoreRefs && activeSnapshot) {
|
||||
refreshed.authStores = getLiveSecretsRuntimeAuthStores();
|
||||
setPreparedSecretsRuntimeSnapshotRefreshContext(
|
||||
refreshed,
|
||||
fastPath.refreshContext,
|
||||
);
|
||||
}
|
||||
secretsRuntime.activateSecretsRuntimeSnapshot(refreshed);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -303,6 +303,7 @@ export function prepareSecretsRuntimeFastPathSnapshot(params: {
|
||||
refreshContext: {
|
||||
env: runtimeEnv,
|
||||
explicitAgentDirs: params.agentDirs?.length ? [...candidateDirs] : null,
|
||||
includeAuthStoreRefs,
|
||||
loadablePluginOrigins: params.loadablePluginOrigins ?? new Map<string, PluginOrigin>(),
|
||||
...(params.loadAuthStore ? { loadAuthStore: params.loadAuthStore } : {}),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { setRuntimeAuthProfileStoreSnapshot } from "../agents/auth-profiles/runtime-snapshots.js";
|
||||
import { getRuntimeConfigSnapshotRefreshHandler } from "../config/runtime-snapshot.js";
|
||||
import { activateSecretsRuntimeSnapshot, getActiveSecretsRuntimeSnapshot } from "./runtime.js";
|
||||
import {
|
||||
asConfig,
|
||||
loadAuthStoreWithProfiles,
|
||||
@@ -41,6 +44,65 @@ describe("secrets runtime snapshot request secret refs", () => {
|
||||
expect(snapshot.authStores).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("can skip auth-profile SecretRef resolution during active runtime refresh", async () => {
|
||||
const initialEnvVar = `OPENCLAW_INITIAL_AUTH_PROFILE_SECRET_${Date.now()}`;
|
||||
const missingEnvVar = `OPENCLAW_MISSING_AUTH_PROFILE_SECRET_${Date.now()}`;
|
||||
delete process.env[missingEnvVar];
|
||||
|
||||
let useMissingProfileRef = false;
|
||||
let loadAuthStoreCalls = 0;
|
||||
const loadAuthStore = () => {
|
||||
loadAuthStoreCalls += 1;
|
||||
return loadAuthStoreWithProfiles({
|
||||
"custom:token": {
|
||||
type: "token",
|
||||
provider: "custom",
|
||||
tokenRef: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: useMissingProfileRef ? missingEnvVar : initialEnvVar,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({}),
|
||||
env: { [initialEnvVar]: "sk-initial" },
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore,
|
||||
});
|
||||
activateSecretsRuntimeSnapshot(snapshot);
|
||||
expect(loadAuthStoreCalls).toBe(1);
|
||||
setRuntimeAuthProfileStoreSnapshot(
|
||||
loadAuthStoreWithProfiles({
|
||||
"custom:token": {
|
||||
type: "token",
|
||||
provider: "custom",
|
||||
token: "sk-live",
|
||||
},
|
||||
}),
|
||||
"/tmp/openclaw-agent-main",
|
||||
);
|
||||
|
||||
useMissingProfileRef = true;
|
||||
const refreshHandler = getRuntimeConfigSnapshotRefreshHandler();
|
||||
if (!refreshHandler) {
|
||||
throw new Error("Expected active runtime refresh handler");
|
||||
}
|
||||
await expect(
|
||||
refreshHandler.refresh({
|
||||
sourceConfig: asConfig({ gateway: { port: 19001 } }),
|
||||
includeAuthStoreRefs: false,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
expect(loadAuthStoreCalls).toBe(1);
|
||||
const profile = getActiveSecretsRuntimeSnapshot()?.authStores[0]?.store.profiles[
|
||||
"custom:token"
|
||||
] as { token?: string } | undefined;
|
||||
expect(profile?.token).toBe("sk-live");
|
||||
});
|
||||
|
||||
it("resolves model provider request secret refs for headers, auth, and tls material", async () => {
|
||||
const config = asConfig({
|
||||
models: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
getRuntimeAuthProfileStoreSnapshot,
|
||||
replaceRuntimeAuthProfileStoreSnapshots,
|
||||
} from "../agents/auth-profiles/runtime-snapshots.js";
|
||||
import { clearLoadedAuthStoreCache } from "../agents/auth-profiles/store-cache.js";
|
||||
@@ -30,6 +31,7 @@ export type PreparedSecretsRuntimeSnapshot = {
|
||||
export type SecretsRuntimeRefreshContext = {
|
||||
env: Record<string, string | undefined>;
|
||||
explicitAgentDirs: string[] | null;
|
||||
includeAuthStoreRefs: boolean;
|
||||
loadAuthStore?: (agentDir?: string) => AuthProfileStore;
|
||||
loadablePluginOrigins: ReadonlyMap<string, PluginOrigin>;
|
||||
};
|
||||
@@ -48,6 +50,7 @@ export function cloneSecretsRuntimeRefreshContext(
|
||||
const cloned: SecretsRuntimeRefreshContext = {
|
||||
env: { ...context.env },
|
||||
explicitAgentDirs: context.explicitAgentDirs ? [...context.explicitAgentDirs] : null,
|
||||
includeAuthStoreRefs: context.includeAuthStoreRefs,
|
||||
loadablePluginOrigins: new Map(context.loadablePluginOrigins),
|
||||
};
|
||||
if (context.loadAuthStore) {
|
||||
@@ -131,6 +134,16 @@ export function getActiveSecretsRuntimeSnapshot(): PreparedSecretsRuntimeSnapsho
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export function getLiveSecretsRuntimeAuthStores(): PreparedSecretsRuntimeSnapshot["authStores"] {
|
||||
if (!activeSnapshot) {
|
||||
return [];
|
||||
}
|
||||
return activeSnapshot.authStores.map((entry) => ({
|
||||
agentDir: entry.agentDir,
|
||||
store: getRuntimeAuthProfileStoreSnapshot(entry.agentDir) ?? structuredClone(entry.store),
|
||||
}));
|
||||
}
|
||||
|
||||
export function clearSecretsRuntimeSnapshot(): void {
|
||||
activeSnapshot = null;
|
||||
activeRefreshContext = null;
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
getActiveSecretsRuntimeEnv as getActiveSecretsRuntimeEnvState,
|
||||
getActiveSecretsRuntimeRefreshContext,
|
||||
getActiveSecretsRuntimeSnapshot as getActiveSecretsRuntimeSnapshotState,
|
||||
getLiveSecretsRuntimeAuthStores,
|
||||
getPreparedSecretsRuntimeSnapshotRefreshContext,
|
||||
registerSecretsRuntimeStateClearHook,
|
||||
setPreparedSecretsRuntimeSnapshotRefreshContext,
|
||||
@@ -123,6 +124,7 @@ export async function prepareSecretsRuntimeSnapshot(params: {
|
||||
setPreparedSecretsRuntimeSnapshotRefreshContext(snapshot, {
|
||||
env: runtimeEnv,
|
||||
explicitAgentDirs: params.agentDirs?.length ? [...candidateDirs] : null,
|
||||
includeAuthStoreRefs,
|
||||
loadAuthStore: fastPathLoadAuthStore,
|
||||
loadablePluginOrigins: params.loadablePluginOrigins ?? new Map<string, PluginOrigin>(),
|
||||
});
|
||||
@@ -197,6 +199,7 @@ export async function prepareSecretsRuntimeSnapshot(params: {
|
||||
setPreparedSecretsRuntimeSnapshotRefreshContext(snapshot, {
|
||||
env: runtimeEnv,
|
||||
explicitAgentDirs: params.agentDirs?.length ? [...candidateDirs] : null,
|
||||
includeAuthStoreRefs,
|
||||
loadAuthStore: params.loadAuthStore ?? loadAuthProfileStoreForSecretsRuntime,
|
||||
loadablePluginOrigins,
|
||||
});
|
||||
@@ -210,6 +213,7 @@ export function activateSecretsRuntimeSnapshot(snapshot: PreparedSecretsRuntimeS
|
||||
({
|
||||
env: { ...process.env } as Record<string, string | undefined>,
|
||||
explicitAgentDirs: null,
|
||||
includeAuthStoreRefs: snapshot.authStores.length > 0,
|
||||
loadAuthStore: loadAuthProfileStoreForSecretsRuntime,
|
||||
loadablePluginOrigins: new Map<string, PluginOrigin>(),
|
||||
} satisfies SecretsRuntimeRefreshContext);
|
||||
@@ -217,20 +221,28 @@ export function activateSecretsRuntimeSnapshot(snapshot: PreparedSecretsRuntimeS
|
||||
snapshot,
|
||||
refreshContext,
|
||||
refreshHandler: {
|
||||
refresh: async ({ sourceConfig }) => {
|
||||
refresh: async ({ sourceConfig, includeAuthStoreRefs }) => {
|
||||
const activeRefreshContext = getActiveSecretsRuntimeRefreshContext();
|
||||
if (!getActiveSecretsRuntimeSnapshotState() || !activeRefreshContext) {
|
||||
const activeSnapshot = getActiveSecretsRuntimeSnapshotState();
|
||||
if (!activeSnapshot || !activeRefreshContext) {
|
||||
return false;
|
||||
}
|
||||
const oneShotSkipAuthStoreRefs =
|
||||
includeAuthStoreRefs === false && activeRefreshContext.includeAuthStoreRefs;
|
||||
const refreshed = await prepareSecretsRuntimeSnapshot({
|
||||
config: sourceConfig,
|
||||
env: activeRefreshContext.env,
|
||||
agentDirs: resolveRefreshAgentDirs(sourceConfig, activeRefreshContext),
|
||||
includeAuthStoreRefs: includeAuthStoreRefs ?? activeRefreshContext.includeAuthStoreRefs,
|
||||
loadablePluginOrigins: activeRefreshContext.loadablePluginOrigins,
|
||||
...(activeRefreshContext.loadAuthStore
|
||||
? { loadAuthStore: activeRefreshContext.loadAuthStore }
|
||||
: {}),
|
||||
});
|
||||
if (oneShotSkipAuthStoreRefs) {
|
||||
refreshed.authStores = getLiveSecretsRuntimeAuthStores();
|
||||
setPreparedSecretsRuntimeSnapshotRefreshContext(refreshed, activeRefreshContext);
|
||||
}
|
||||
activateSecretsRuntimeSnapshot(refreshed);
|
||||
return true;
|
||||
},
|
||||
@@ -248,6 +260,7 @@ export async function refreshActiveSecretsRuntimeSnapshot(): Promise<boolean> {
|
||||
config: activeSnapshot.sourceConfig,
|
||||
env: activeRefreshContext.env,
|
||||
agentDirs: resolveRefreshAgentDirs(activeSnapshot.sourceConfig, activeRefreshContext),
|
||||
includeAuthStoreRefs: activeRefreshContext.includeAuthStoreRefs,
|
||||
loadablePluginOrigins: activeRefreshContext.loadablePluginOrigins,
|
||||
...(activeRefreshContext.loadAuthStore
|
||||
? { loadAuthStore: activeRefreshContext.loadAuthStore }
|
||||
|
||||
Reference in New Issue
Block a user