perf(agent): skip plugin validation for gateway dispatch

This commit is contained in:
Peter Steinberger
2026-05-29 17:49:59 +01:00
parent 2106714f6b
commit 67faef0182
4 changed files with 62 additions and 13 deletions

View File

@@ -206,7 +206,7 @@ function createGatewayNormalCloseError() {
});
}
vi.mock("../config/config.js", () => ({ getRuntimeConfig: loadConfig, loadConfig }));
vi.mock("../config/io.js", () => ({ getRuntimeConfig: loadConfig, loadConfig }));
vi.mock("../gateway/call.js", () => ({
callGateway,
isGatewayTransportError,
@@ -284,6 +284,8 @@ describe("agentCliCommand", () => {
expect(params.sessionKey).toBe("agent:main:incident-42");
expect(params.sessionId).toBeUndefined();
expect(params.to).toBeUndefined();
expect(request.config).toBe(loadConfig.mock.results[0]?.value);
expect(loadConfig).toHaveBeenCalledWith({ skipPluginValidation: true, pin: false });
expect(agentCommand).not.toHaveBeenCalled();
expect(loadAgentSessionModuleMock).not.toHaveBeenCalled();
});
@@ -811,6 +813,7 @@ describe("agentCliCommand", () => {
});
expect(fallbackAbort?.method).toBe("chat.abort");
expect(fallbackAbort?.timeoutMs).toBe(2_000);
expect(fallbackAbort?.config).toBe(loadConfig.mock.results[0]?.value);
expect(fallbackAbort?.params).toEqual({
sessionKey: "agent:main:main",
runId: "pre-accepted-run",
@@ -960,6 +963,7 @@ describe("agentCliCommand", () => {
expect(fallbackAbort?.clientName).toBe("gateway-client");
expect(fallbackAbort?.mode).toBe("backend");
expect(fallbackAbort?.scopes).toEqual(["operator.admin"]);
expect(fallbackAbort?.config).toBe(loadConfig.mock.results[0]?.value);
expect(fallbackAbort?.params).toEqual({
sessionKey: "agent:main:main",
runId: "run-model-fallback",
@@ -1434,6 +1438,10 @@ describe("agentCliCommand", () => {
};
expect(fallbackOpts.sessionId).toMatch(/^gateway-fallback-/);
expect(fallbackOpts.sessionKey).toBe(`agent:ops:explicit:${fallbackOpts.sessionId}`);
expect(loadConfig.mock.calls).toEqual([
[{ skipPluginValidation: true, pin: false }],
[{ skipPluginValidation: true, pin: false }],
]);
},
{ agents: { list: [{ id: "ops", default: true }, { id: "main" }] } },
);
@@ -1621,6 +1629,7 @@ describe("agentCliCommand", () => {
);
expect(localOpts.agentId).toBe("ops");
expect(localOpts.sessionKey).toBe("agent:ops:incident-42");
expect(loadConfig).toHaveBeenCalledWith();
});
});

View File

@@ -4,11 +4,11 @@ import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
} from "../../packages/gateway-protocol/src/client-info.js";
import { listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope-config.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { CliDeps } from "../cli/deps.types.js";
import { withProgress } from "../cli/progress.js";
import { getRuntimeConfig } from "../config/config.js";
import { getRuntimeConfig } from "../config/io.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
callGateway,
@@ -150,6 +150,12 @@ function parseTimeoutSeconds(opts: { cfg: OpenClawConfig; timeout?: string }) {
return raw;
}
function getGatewayDispatchConfig(): OpenClawConfig {
// Scoped gateway turns need core agent/session/gateway fields only. The
// running gateway owns plugin validation and plugin metadata freshness.
return getRuntimeConfig({ skipPluginValidation: true, pin: false });
}
function formatPayloadForLog(payload: {
text?: string;
mediaUrls?: string[];
@@ -231,7 +237,9 @@ function normalizeSessionKeyOptsForDispatch(opts: AgentCliOpts): AgentCliOpts {
isLegacySessionKey && !agentIdRaw && !isUnscopedSessionKeySentinel(rawSessionKey);
const cfg =
isLegacySessionKey && (agentIdRaw || shouldScopeDefaultAgentKey)
? getRuntimeConfig()
? opts.local === true
? getRuntimeConfig()
: getGatewayDispatchConfig()
: undefined;
const sessionKey = scopeLegacySessionKeyToAgent({
agentId: agentIdRaw ?? (shouldScopeDefaultAgentKey ? resolveDefaultAgentId(cfg!) : undefined),
@@ -401,6 +409,7 @@ async function abortAcceptedGatewayAgentRunWithGatewayCall(params: {
signal: AgentCliSignal | undefined;
runtime: RuntimeEnv;
gatewayIdentity: AgentGatewayCallIdentity;
config: OpenClawConfig;
}): Promise<void> {
const request: GatewayRequestFunction = async <T = Record<string, unknown>>(
method: string,
@@ -412,6 +421,7 @@ async function abortAcceptedGatewayAgentRunWithGatewayCall(params: {
params: requestParams,
timeoutMs: opts?.timeoutMs ?? undefined,
expectFinal: opts?.expectFinal,
config: params.config,
...params.gatewayIdentity,
});
for (const [attempt, retryDelayMs] of [...GATEWAY_ABORT_RETRY_DELAYS_MS, 0].entries()) {
@@ -495,7 +505,7 @@ async function resolveAgentIdForGatewayTimeoutFallback(
return resolveAgentIdFromSessionKey(explicitSessionKey);
}
if (isUnscopedSessionKeySentinel(explicitSessionKey)) {
return resolveDefaultAgentId(getRuntimeConfig());
return resolveDefaultAgentId(getGatewayDispatchConfig());
}
const agentIdRaw = opts.agent?.trim();
@@ -506,7 +516,7 @@ async function resolveAgentIdForGatewayTimeoutFallback(
if (!opts.to && !opts.sessionId) {
return undefined;
}
const cfg = getRuntimeConfig();
const cfg = getGatewayDispatchConfig();
const { resolveSessionKeyForRequest } = await loadAgentSessionModule();
const resolvedSessionKey = resolveSessionKeyForRequest({
cfg,
@@ -558,7 +568,7 @@ async function agentViaGatewayCommand(
);
}
const cfg = getRuntimeConfig();
const cfg = getGatewayDispatchConfig();
const agentIdRaw = opts.agent?.trim();
const agentId = agentIdRaw ? normalizeAgentId(agentIdRaw) : undefined;
if (agentId) {
@@ -638,6 +648,7 @@ async function agentViaGatewayCommand(
},
expectFinal: true,
timeoutMs: gatewayTimeoutMs,
config: cfg,
signal: signalBridge.signal,
onAccepted: (payload) => {
acceptedGatewayRun = true;
@@ -670,6 +681,7 @@ async function agentViaGatewayCommand(
signal: signalBridge.getReceivedSignal(),
runtime,
gatewayIdentity,
config: cfg,
});
}
throw err;

View File

@@ -1,5 +1,10 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getRuntimeConfig, clearConfigCache, clearRuntimeConfigSnapshot } from "./config.js";
import {
getRuntimeConfig,
clearConfigCache,
clearRuntimeConfigSnapshot,
getRuntimeConfigSnapshot,
} from "./config.js";
import { withTempHomeConfig } from "./test-helpers.js";
describe("talk config validation fail-closed behavior", () => {
@@ -9,6 +14,20 @@ describe("talk config validation fail-closed behavior", () => {
vi.restoreAllMocks();
});
it("can load an unpinned runtime config without replacing the process snapshot", async () => {
await withTempHomeConfig({ gateway: { port: 19002 } }, async () => {
const unpinned = getRuntimeConfig({ skipPluginValidation: true, pin: false });
expect(unpinned.gateway?.port).toBe(19002);
expect(getRuntimeConfigSnapshot()).toBeNull();
const pinned = getRuntimeConfig();
expect(pinned.gateway?.port).toBe(19002);
expect(getRuntimeConfigSnapshot()).toBe(pinned);
});
});
async function expectInvalidTalkConfig(config: unknown, messagePattern: RegExp) {
await withTempHomeConfig(config, async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});

View File

@@ -2540,16 +2540,25 @@ export function projectConfigOntoRuntimeSourceSnapshot(config: OpenClawConfig):
return coerceConfig(applyMergePatch(projectedSource, runtimePatch));
}
export function loadConfig(options?: { skipPluginValidation?: boolean }): OpenClawConfig {
export function loadConfig(options?: {
skipPluginValidation?: boolean;
pin?: boolean;
}): OpenClawConfig {
const loadFresh = () =>
createConfigIO(options?.skipPluginValidation ? { pluginValidation: "skip" } : {}).loadConfig();
if (options?.pin === false) {
return loadFresh();
}
// First successful load becomes the process snapshot. Long-lived runtimes
// should swap this snapshot via explicit reload/watcher paths instead of
// reparsing openclaw.json on hot code paths.
return loadPinnedRuntimeConfig(() =>
createConfigIO(options?.skipPluginValidation ? { pluginValidation: "skip" } : {}).loadConfig(),
);
return loadPinnedRuntimeConfig(loadFresh);
}
export function getRuntimeConfig(options?: { skipPluginValidation?: boolean }): OpenClawConfig {
export function getRuntimeConfig(options?: {
skipPluginValidation?: boolean;
pin?: boolean;
}): OpenClawConfig {
return loadConfig(options);
}