fix(system-agent): setup chat fails silently on dev-roster gateways (#121784)

* fix(system-agent): repair setup-chat execution identity, harness fallback, and error surfacing

Always inject the reserved "openclaw" execution entry for off-roster system-agent runs.
Keep implicit harness runtime selection implicit so Codex refusal falls back to the OpenClaw harness, while explicit policy remains fail-closed.
Log openclaw.chat turn failures and surface their underlying causes in the UNAVAILABLE response.

* chore(plugin-sdk): refresh agent harness API baseline

* test(system-agent): align fixtures with optional harness routes

* fix(system-agent): keep execution roster out of TUI catalog lookup

* refactor(system-agent): keep verified state types internal

* fix(system-agent): separate setup policy and execution config
This commit is contained in:
Peter Steinberger
2026-08-10 21:44:00 -07:00
committed by GitHub
parent c896b9b2cd
commit c5b7b8e33c
17 changed files with 276 additions and 99 deletions
+1 -1
View File
@@ -53,7 +53,7 @@
{"closureHash":"ff3c4616cd6212a6d698831a8b287ad87e3968c8663f4090d095bb30ec40fc1c","declaration":"export function abortAndDrainAgentHarnessRun(params: { sessionId: string; sessionKey?: string; settleMs?: number; forceClear?: boolean; reason?: string; }): Promise<AbortAndDrainEmbeddedAgentRunResult>;","entrypoint":"agent-harness","exportName":"abortAndDrainAgentHarnessRun","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"58989e3fa91eecf2e05202e36548e6e64a3b9219e83154bd99866c851604e34e","declaration":"export function createAgentToolResultMiddlewareRunner(ctx: AgentToolResultMiddlewareContext, handlers?: AgentToolResultMiddleware[]): { applyToolResultMiddleware(event: AgentToolResultMiddlewareEvent): Promise<OpenClawAgentToolResult>; };","entrypoint":"agent-harness","exportName":"createAgentToolResultMiddlewareRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"b8f6355d7ad1700aceecddaa6e9ccf43091a792d5b481a1ee3f55294a881a67d","declaration":"export function createCodexAppServerToolResultExtensionRunner(ctx: CodexAppServerExtensionContext, factories?: CodexAppServerExtensionFactory[]): { applyToolResultExtensions(event: CodexAppServerToolResultEvent): Promise<AgentToolResult<unknown>>; };","entrypoint":"agent-harness","exportName":"createCodexAppServerToolResultExtensionRunner","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"c3b6f83fc89b5682067b75004eb8fe76a780e34c0bd017a3ebb104781c6694e7","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"0511dbfbe051bdcd4302b9923203989e9222f9ed91209cff658eb41679ca316d","declaration":"export function createOpenClawCodingTools(options?: OpenClawCodingToolsOptions): AnyAgentTool[];","entrypoint":"agent-harness","exportName":"createOpenClawCodingTools","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"13649ee853485319e7449fda25c106b3f3f53d3090e53cdbb28e220a8529eb80","declaration":"export function disposeRegisteredAgentHarnesses(): Promise<void>;","entrypoint":"agent-harness","exportName":"disposeRegisteredAgentHarnesses","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"f255a91162ece4c239bb8fa745efa14a1054bc024a291e9954f75f239b2b87c3","declaration":"export function resolveActiveEmbeddedRunSessionId(sessionKey: string): string | undefined;","entrypoint":"agent-harness","exportName":"resolveActiveEmbeddedRunSessionId","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
{"closureHash":"27d9f61df9ccf68615cb2002a6da891e36ab0c07623bc37c4d07aa980b446881","declaration":"export function resolveWebSearchToolPolicy(params: WebSearchToolPolicyParams): WebSearchToolPolicyResolution;","entrypoint":"agent-harness","exportName":"resolveWebSearchToolPolicy","importSpecifier":"openclaw/plugin-sdk/agent-harness","kind":"function","recordType":"export"}
@@ -15,7 +15,6 @@ import { getActiveGatewayRootWorkCount } from "../../process/gateway-work-admiss
import { CommandLane } from "../../process/lanes.js";
import { defaultRuntime } from "../../runtime.js";
import { SystemAgentChatEngine } from "../../system-agent/chat-engine.js";
import { SystemAgentInferenceUnavailableError } from "../../system-agent/inference-error.js";
import {
createSystemAgentVerifiedInferenceTestFixture,
installSystemAgentPluginMetadataTestSnapshot,
@@ -934,10 +933,14 @@ describe("openclaw.chat", () => {
it("reuses a live session, then requires fresh fallback verification after failure", async () => {
stubEngineOverview();
const engine = makeVerifiedEngine();
vi.spyOn(engine, "handle").mockRejectedValue(
new SystemAgentInferenceUnavailableError("conversation"),
);
const engine = new SystemAgentChatEngine({
verifiedInference: requireVerifiedInferenceFixture(),
runAgentTurn: async () => {
throw new Error("workspace owner openclaw is missing from the roster");
},
planWithAssistant: async () => null,
deps: requireVerifiedInferenceDeps(),
});
const dispose = vi.spyOn(engine, "dispose").mockResolvedValue();
const sessions = new Map<string, SystemAgentChatSession>([["s1", seededSession({ engine })]]);
const context = makeContext(sessions);
@@ -948,7 +951,7 @@ describe("openclaw.chat", () => {
ok: false,
error: {
code: "UNAVAILABLE",
message: expect.stringContaining("working inference"),
message: expect.stringContaining("workspace owner openclaw is missing from the roster"),
details: { code: "system_agent_session_invalidated" },
},
});
+21 -4
View File
@@ -55,6 +55,7 @@ const mocks = vi.hoisted(() => ({
runSetupMemoryImportStep: vi.fn(),
writeWizardConfigFile: vi.fn(),
runCollectedChannelOnboardingPostWriteHooks: vi.fn(async () => {}),
chatWarn: vi.fn(),
sharedVerifiedInference: undefined as SystemAgentVerifiedInferenceBinding | undefined,
}));
@@ -65,6 +66,17 @@ vi.mock("../config/config.js", async (importOriginal) => ({
readConfigFileSnapshot: mocks.readConfigFileSnapshot,
}));
vi.mock("../logging/subsystem.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../logging/subsystem.js")>();
return {
...actual,
createSubsystemLogger: (subsystem: string) =>
subsystem === "system-agent/chat-engine"
? ({ warn: mocks.chatWarn } as unknown as ReturnType<typeof actual.createSubsystemLogger>)
: actual.createSubsystemLogger(subsystem),
};
});
vi.mock("../wizard/setup.shared.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../wizard/setup.shared.js")>()),
readSetupConfigFileSnapshot: mocks.readSetupConfigFileSnapshot,
@@ -174,7 +186,9 @@ function testHarnessBinding(route: SystemAgentConfiguredRoute) {
return { auth: {}, deps: {} };
}
const agentHarnessId =
route.agentHarnessRuntimeOverride === "auto" ? "openclaw" : route.agentHarnessRuntimeOverride;
route.agentHarnessRuntimeOverride === "auto"
? "openclaw"
: (route.agentHarnessRuntimeOverride ?? "codex");
if (agentHarnessId === "openclaw") {
return { auth: { agentHarnessId }, deps: {} };
}
@@ -3148,13 +3162,15 @@ describe("SystemAgentChatEngine", () => {
it("fails closed when neither inference path is usable", async () => {
const planner = vi.fn(async () => null);
const engine = new SystemAgentChatEngine({
runAgentTurn: async () => null,
runAgentTurn: async () => {
throw new Error("workspace owner openclaw is missing from the roster");
},
planWithAssistant: planner,
deps: { loadOverview: fakeOverviewLoader() },
});
await expect(engine.handle("please make everything nice")).rejects.toBeInstanceOf(
SystemAgentInferenceUnavailableError,
await expect(engine.handle("please make everything nice")).rejects.toThrow(
"workspace owner openclaw is missing from the roster",
);
});
});
@@ -3272,6 +3288,7 @@ describe("OpenClaw agent loop backends", () => {
expect(runCliAgent).toHaveBeenCalledOnce();
expect(reply.text).toContain("planner fallback reply");
expect(mocks.chatWarn).toHaveBeenCalledWith(expect.stringContaining("claude exploded"));
});
});
+1
View File
@@ -1136,6 +1136,7 @@ export class SystemAgentChatEngine {
session: this.agentSession,
});
} catch (error) {
log.warn(`agent turn failed before planner fallback: ${formatErrorMessage(error)}`);
agentFailure = error;
loopReply = null;
}
+20 -3
View File
@@ -1,5 +1,24 @@
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { formatErrorMessage } from "../infra/errors.js";
type SystemAgentInferenceStage = "agent-turn" | "planner" | "conversation";
const INFERENCE_UNAVAILABLE_MESSAGE =
"OpenClaw could not reach working inference. Run `openclaw onboard` on the machine running OpenClaw to reconnect — it live-tests the route before saving it. Then try again.";
const INFERENCE_FAILURE_SUMMARY_MAX_CHARS = 300;
function inferenceUnavailableMessage(failures: readonly unknown[]): string {
const detail = failures.length > 0 ? formatErrorMessage(failures[0]).trim() : "";
if (!detail) {
return INFERENCE_UNAVAILABLE_MESSAGE;
}
const summary =
detail.length > INFERENCE_FAILURE_SUMMARY_MAX_CHARS
? `${truncateUtf16Safe(detail, INFERENCE_FAILURE_SUMMARY_MAX_CHARS - 1)}`
: detail;
return `${INFERENCE_UNAVAILABLE_MESSAGE} Cause: ${summary}`;
}
/** Safe public error for an OpenClaw turn that could not complete with intelligence. */
export class SystemAgentInferenceUnavailableError extends Error {
readonly code = "SYSTEM_AGENT_INFERENCE_UNAVAILABLE";
@@ -8,9 +27,7 @@ export class SystemAgentInferenceUnavailableError extends Error {
readonly stage: SystemAgentInferenceStage,
readonly failures: readonly unknown[] = [],
) {
super(
"OpenClaw could not reach working inference. Run `openclaw onboard` on the machine running OpenClaw to reconnect — it live-tests the route before saving it. Then try again.",
);
super(inferenceUnavailableMessage(failures));
this.name = "SystemAgentInferenceUnavailableError";
}
}
+110
View File
@@ -0,0 +1,110 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { clearAgentHarnesses, registerAgentHarness } from "../agents/harness/registry.js";
import { selectAgentHarness } from "../agents/harness/selection.js";
import { resolveRunWorkspaceDir } from "../agents/workspace-run.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { SYSTEM_AGENT_ID } from "./agent-id.js";
import { resolveSystemAgentConfiguredRouteFromConfig } from "./inference-route.js";
function devConfig(agentRuntime?: string): OpenClawConfig {
return {
agents: {
defaults: { model: "openai/gpt-5.5" },
entries: {
dev: { default: true, workspace: "/tmp/x" },
},
},
models: {
providers: {
openai: {
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
...(agentRuntime ? { agentRuntime: { id: agentRuntime } } : {}),
models: [
{
id: "gpt-5.5",
name: "GPT-5.5",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128_000,
maxTokens: 8_192,
},
],
},
},
},
};
}
afterEach(() => {
clearAgentHarnesses();
});
describe("resolveSystemAgentConfiguredRouteFromConfig", () => {
it("admits the reserved execution agent for a dev-shaped roster", async () => {
const route = await resolveSystemAgentConfiguredRouteFromConfig(devConfig());
expect(route).not.toBeNull();
expect(() =>
resolveRunWorkspaceDir({
workspaceDir: "/tmp/x",
agentId: SYSTEM_AGENT_ID,
config: route!.runConfig,
}),
).not.toThrow();
});
it("keeps implicit harness selection fallible while forcing explicit policy", async () => {
const supports = vi.fn((ctx: { modelProvider?: { requestTransportOverrides?: string } }) =>
ctx.modelProvider?.requestTransportOverrides === "present"
? { supported: false as const, reason: "authored request transport overrides" }
: { supported: true as const, priority: 100 },
);
registerAgentHarness({
id: "codex",
label: "Codex",
supports: supports as never,
runAttempt: vi.fn() as never,
});
const preparedModelProvider = {
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
requestTransportOverrides: "present" as const,
};
const implicitRoute = await resolveSystemAgentConfiguredRouteFromConfig(devConfig());
expect(implicitRoute).toMatchObject({ runner: "embedded" });
expect(implicitRoute).not.toHaveProperty("agentHarnessRuntimeOverride");
expect(
selectAgentHarness({
provider: "openai",
modelId: "gpt-5.5",
modelProvider: preparedModelProvider,
config: implicitRoute!.runConfig,
agentHarnessRuntimeOverride:
implicitRoute!.runner === "embedded"
? implicitRoute!.agentHarnessRuntimeOverride
: undefined,
}).id,
).toBe("openclaw");
const explicitRoute = await resolveSystemAgentConfiguredRouteFromConfig(devConfig("codex"));
expect(explicitRoute).toMatchObject({
runner: "embedded",
agentHarnessRuntimeOverride: "codex",
});
expect(() =>
selectAgentHarness({
provider: "openai",
modelId: "gpt-5.5",
modelProvider: preparedModelProvider,
config: explicitRoute!.runConfig,
agentHarnessRuntimeOverride:
explicitRoute!.runner === "embedded"
? explicitRoute!.agentHarnessRuntimeOverride
: undefined,
}),
).toThrow("authored request transport overrides");
});
});
+18 -35
View File
@@ -14,6 +14,7 @@ import {
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { SYSTEM_AGENT_ID } from "./agent-id.js";
export type SystemAgentConfiguredRoute = {
runConfig: OpenClawConfig;
@@ -27,7 +28,7 @@ export type SystemAgentConfiguredRoute = {
| { runner: "cli" }
| {
runner: "embedded";
agentHarnessRuntimeOverride: string;
agentHarnessRuntimeOverride?: string;
}
);
@@ -70,42 +71,20 @@ export type DefaultInferenceRouteProjection = {
tools: OpenClawConfig["tools"];
};
const SYSTEM_AGENT_EXECUTION_AGENT_ID = "openclaw";
function projectSystemAgentExecutionConfig(
config: OpenClawConfig,
routeAgentId: string,
): OpenClawConfig {
const agents = listAgentEntries(config);
if (agents.length === 0) {
return config;
}
const routeAgent =
routeAgentId === SYSTEM_AGENT_EXECUTION_AGENT_ID
? undefined
: agents.find((agent) => normalizeAgentId(agent.id) === routeAgentId);
const retainedAgents = agents.filter(
(agent) => normalizeAgentId(agent.id) !== SYSTEM_AGENT_EXECUTION_AGENT_ID,
);
const hasProjectedSettings = routeAgent?.params !== undefined || routeAgent?.tools !== undefined;
if (retainedAgents.length === agents.length && !hasProjectedSettings) {
return config;
}
const routeAgent = agents.find((agent) => normalizeAgentId(agent.id) === routeAgentId);
const retainedAgents = agents.filter((agent) => normalizeAgentId(agent.id) !== SYSTEM_AGENT_ID);
const projectedAgents = [
...retainedAgents,
...(hasProjectedSettings
? [
{
id: SYSTEM_AGENT_EXECUTION_AGENT_ID,
...(routeAgent?.params !== undefined
? { params: structuredClone(routeAgent.params) }
: {}),
...(routeAgent?.tools !== undefined
? { tools: structuredClone(routeAgent.tools) }
: {}),
},
]
: []),
{
id: SYSTEM_AGENT_ID,
...(routeAgent?.params !== undefined ? { params: structuredClone(routeAgent.params) } : {}),
...(routeAgent?.tools !== undefined ? { tools: structuredClone(routeAgent.tools) } : {}),
},
];
const { list: _legacyList, ...agentsConfig } = config.agents ?? {};
return {
@@ -195,13 +174,17 @@ export async function resolveSystemAgentConfiguredRouteFromConfig(
if (isCliRoute) {
return { runner: "cli", ...base };
}
const runtime = harnessPolicy.resolveAgentHarnessPolicy({
const policy = harnessPolicy.resolveAgentHarnessPolicy({
config: runConfig,
agentId: modelOwnerAgentId,
provider: selection.provider,
modelId: selection.modelId,
}).runtime;
return { runner: "embedded", agentHarnessRuntimeOverride: runtime, ...base };
});
return {
runner: "embedded",
...(policy.runtimeSource === "implicit" ? {} : { agentHarnessRuntimeOverride: policy.runtime }),
...base,
};
}
function projectRelevantModelMap(params: {
@@ -248,7 +231,7 @@ export async function projectInferenceRoute(
const list = listAgentEntries(config);
const agent = list.find((entry) => normalizeAgentId(entry.id) === routeAgentId);
const executionAgent = listAgentEntries(route?.runConfig ?? {}).find(
(entry) => normalizeAgentId(entry.id) === SYSTEM_AGENT_EXECUTION_AGENT_ID,
(entry) => normalizeAgentId(entry.id) === SYSTEM_AGENT_ID,
);
const defaults = config.agents?.defaults;
const logicalProvider = normalizeProviderId(route?.modelLabel.split("/", 1)[0] ?? "");
@@ -344,7 +327,7 @@ export async function projectInferenceRoute(
...(executionAgent
? {
executionAgent: {
id: SYSTEM_AGENT_EXECUTION_AGENT_ID,
id: SYSTEM_AGENT_ID,
params: structuredClone(executionAgent.params),
tools: structuredClone(executionAgent.tools),
},
@@ -22,7 +22,7 @@ export async function revalidateSetupInferenceOwner(params: {
}): Promise<SystemAgentVerifiedInferenceBinding> {
const configuredHarnessId =
params.route.runner === "embedded"
? params.route.agentHarnessRuntimeOverride.trim()
? params.route.agentHarnessRuntimeOverride?.trim()
: undefined;
const successfulHarnessId =
params.auth.agentHarnessId?.trim() ||
+9 -5
View File
@@ -361,14 +361,16 @@ async function activateSetupInferenceUnredacted(
if (testPlan.runner === "embedded" && stagedRoute.runner === "embedded") {
testPlan = {
...testPlan,
config: stagedExecutionRoute.runConfig,
executionConfig: stagedExecutionRoute.runConfig,
agentDir: hasPreparedAuthProfiles ? testAgentDir : stagedRoute.agentDir,
agentHarnessRuntimeOverride: stagedRoute.agentHarnessRuntimeOverride,
...(stagedRoute.agentHarnessRuntimeOverride
? { agentHarnessRuntimeOverride: stagedRoute.agentHarnessRuntimeOverride }
: {}),
};
} else {
testPlan = {
...testPlan,
config: stagedExecutionRoute.runConfig,
executionConfig: stagedExecutionRoute.runConfig,
...(!hasPreparedAuthProfiles ? { agentDir: stagedRoute.agentDir } : {}),
};
}
@@ -479,10 +481,12 @@ async function activateSetupInferenceUnredacted(
}
if (testPlan.runner === "embedded") {
const successfulHarnessId = test.auth.agentHarnessId?.trim();
const configuredHarnessId = testPlan.agentHarnessRuntimeOverride?.trim();
if (
!successfulHarnessId ||
(testPlan.agentHarnessRuntimeOverride !== "auto" &&
successfulHarnessId !== testPlan.agentHarnessRuntimeOverride)
(configuredHarnessId !== undefined &&
configuredHarnessId !== "auto" &&
successfulHarnessId !== configuredHarnessId)
) {
return {
ok: false,
+2 -2
View File
@@ -513,7 +513,7 @@ export async function runSetupInferenceTest(params: {
sessionFile,
workspaceDir: tempDir,
...(plan.agentDir ? { agentDir: plan.agentDir } : {}),
config: plan.config,
config: plan.executionConfig ?? plan.config,
prompt: params.prompt ?? SETUP_INFERENCE_TEST_PROMPT,
provider: plan.provider,
model: plan.model,
@@ -543,7 +543,7 @@ export async function runSetupInferenceTest(params: {
sessionFile,
workspaceDir: tempDir,
...(plan.agentDir ? { agentDir: plan.agentDir } : {}),
config: plan.config,
config: plan.executionConfig ?? plan.config,
prompt: params.prompt ?? SETUP_INFERENCE_TEST_PROMPT,
provider: plan.provider,
model: plan.model,
@@ -28,7 +28,10 @@ export type SetupInferenceTestPlan = {
provider: string;
model: string;
modelRef: string;
/** Authored/staged config used for route, auth, and persistence decisions. */
config: OpenClawConfig;
/** Execution-only projection that admits the reserved OpenClaw agent. */
executionConfig?: OpenClawConfig;
/** Execution identity used by the real OpenClaw turn. */
agentId?: string;
/** Default-agent owner whose model/runtime config is being selected. */
+3 -2
View File
@@ -242,11 +242,12 @@ export async function buildTestPlan(params: {
provider: route.provider,
model: route.model,
modelRef: route.modelLabel,
config: route.runConfig,
config: cfg,
executionConfig: route.runConfig,
agentId: "openclaw",
routeAgentId: route.agentId,
agentDir: route.agentDir,
...(route.runner === "embedded"
...(route.runner === "embedded" && route.agentHarnessRuntimeOverride
? { agentHarnessRuntimeOverride: route.agentHarnessRuntimeOverride }
: {}),
...(route.authProfileId ? { authProfileId: route.authProfileId } : {}),
@@ -297,7 +297,7 @@ export async function createSystemAgentVerifiedInferenceTestFixture(
const agentHarnessId =
configuredRoute.agentHarnessRuntimeOverride === "auto"
? "openclaw"
: configuredRoute.agentHarnessRuntimeOverride;
: (configuredRoute.agentHarnessRuntimeOverride ?? "codex");
const authFingerprint =
profileId && agentHarnessId !== "openclaw"
? fingerprintResolvedAuthProfileCredential({ profileId, credential, resolvedAuth })
+14 -13
View File
@@ -8,10 +8,10 @@ import type { SystemAgentCommandDeps, SystemAgentOperation } from "./operations.
import type { SystemAgentOverview } from "./overview.js";
import { createSystemAgentVerifiedInferenceTestFixture } from "./system-agent.test-helpers.js";
import { runSystemAgentTui, type SystemAgentTuiOptions } from "./tui-backend.js";
import { resolveSystemAgentVerifiedInferenceRoute } from "./verified-inference.js";
import { resolveSystemAgentVerifiedInferenceState } from "./verified-inference.js";
const verifiedInferenceMocks = vi.hoisted(() => ({
preparedBindings: new WeakSet<object>(),
preparedBindings: new WeakMap<object, OpenClawConfig>(),
}));
vi.mock("../plugins/providers.js", () => ({
@@ -30,11 +30,12 @@ vi.mock("./verified-inference.js", async (importOriginal) => {
const original = await importOriginal<typeof import("./verified-inference.js")>();
return {
...original,
resolveSystemAgentVerifiedInferenceRoute: vi.fn(async (binding, deps) =>
verifiedInferenceMocks.preparedBindings.has(binding)
? binding.execution
: await original.resolveSystemAgentVerifiedInferenceRoute(binding, deps),
),
resolveSystemAgentVerifiedInferenceState: vi.fn(async (binding, deps) => {
const config = verifiedInferenceMocks.preparedBindings.get(binding);
return config
? { config, route: binding.execution }
: await original.resolveSystemAgentVerifiedInferenceState(binding, deps);
}),
};
});
@@ -106,7 +107,7 @@ async function createVerifiedTuiOptions(
? sharedVerifiedFixture
: await createSystemAgentVerifiedInferenceTestFixture(config);
if (!useRealVerification) {
verifiedInferenceMocks.preparedBindings.add(fixture.binding);
verifiedInferenceMocks.preparedBindings.set(fixture.binding, config);
}
return {
verifiedInference: fixture.binding,
@@ -162,8 +163,8 @@ describe("runSystemAgentTui", () => {
verifiedConfig,
true,
);
const resolveVerifiedRoute = vi.mocked(resolveSystemAgentVerifiedInferenceRoute);
resolveVerifiedRoute.mockClear();
const resolveVerifiedState = vi.mocked(resolveSystemAgentVerifiedInferenceState);
resolveVerifiedState.mockClear();
const runTui = vi.fn(
async (opts: Parameters<NonNullable<SystemAgentTuiOptions["runTui"]>>[0]) => {
runTuiCalls += 1;
@@ -181,9 +182,9 @@ describe("runSystemAgentTui", () => {
);
expect(runTuiCalls).toBe(1);
expect(resolveVerifiedRoute).toHaveBeenCalledOnce();
expect(resolveVerifiedRoute).toHaveBeenCalledWith(verified.verifiedInference, verified.deps);
const [resolveOrder] = resolveVerifiedRoute.mock.invocationCallOrder;
expect(resolveVerifiedState).toHaveBeenCalledOnce();
expect(resolveVerifiedState).toHaveBeenCalledWith(verified.verifiedInference, verified.deps);
const [resolveOrder] = resolveVerifiedState.mock.invocationCallOrder;
const [runTuiOrder] = runTui.mock.invocationCallOrder;
if (resolveOrder === undefined || runTuiOrder === undefined) {
throw new Error("expected verified route resolution before TUI startup");
+5 -4
View File
@@ -37,7 +37,7 @@ import {
} from "./operations.js";
import { formatSystemAgentStartupMessage, loadSystemAgentOverview } from "./overview.js";
import {
resolveSystemAgentVerifiedInferenceRoute,
resolveSystemAgentVerifiedInferenceState,
type SystemAgentVerifiedInferenceBinding,
} from "./verified-inference.js";
@@ -602,8 +602,9 @@ async function requireTuiVerifiedInference(
throw new SystemAgentInferenceUnavailableError("conversation");
}
try {
const route = await resolveSystemAgentVerifiedInferenceRoute(binding, opts.deps);
if (route) {
const verified = await resolveSystemAgentVerifiedInferenceState(binding, opts.deps);
if (verified) {
const { config, route } = verified;
const [{ getPreparedModelCatalogSnapshot }, { resolveThinkingDefault }] = await Promise.all([
import("../agents/prepared-model-catalog.js"),
import("../agents/model-thinking-default.js"),
@@ -611,7 +612,7 @@ async function requireTuiVerifiedInference(
// Catalog metadata improves the label but must not become a new startup
// dependency after this exact inference route has already been verified.
const catalog = getPreparedModelCatalogSnapshot({
config: route.runConfig,
config,
agentId: route.agentId,
agentDir: route.agentDir,
readOnly: true,
+4 -3
View File
@@ -222,7 +222,7 @@ async function bindingFor(
route.runner === "embedded"
? route.agentHarnessRuntimeOverride === "auto"
? "openclaw"
: route.agentHarnessRuntimeOverride
: (route.agentHarnessRuntimeOverride ?? "codex")
: undefined;
return createBinding(
route,
@@ -868,7 +868,7 @@ describe("verified OpenClaw inference binding", () => {
const fingerprint = () =>
fingerprintAwsSdkRuntimeOwner({
provider: route.provider,
backendId: route.agentHarnessRuntimeOverride,
backendId: route.agentHarnessRuntimeOverride ?? "openclaw",
auth,
});
try {
@@ -1047,7 +1047,8 @@ describe("verified OpenClaw inference binding", () => {
const route = await revalidate(binding, changed, authDeps());
expect(route).toBe(binding.execution);
expect(route?.runConfig).toEqual(baseConfig);
expect(route?.runConfig).toMatchObject(baseConfig);
expect(route?.runConfig.agents?.entries).toEqual({ openclaw: {} });
expect(route?.runConfig).not.toBe(baseConfig);
});
+54 -19
View File
@@ -48,6 +48,16 @@ type SystemAgentConfiguredRouteIdentity = DistributiveOmit<
SystemAgentConfiguredRoute,
"runConfig" | "authProfileId"
>;
type SystemAgentVerifiedExecutionRoute =
| Extract<SystemAgentConfiguredRoute, { runner: "cli" }>
| (Extract<SystemAgentConfiguredRoute, { runner: "embedded" }> & {
agentHarnessRuntimeOverride: string;
});
type SystemAgentVerifiedInferenceState = Readonly<{
config: OpenClawConfig;
route: SystemAgentVerifiedExecutionRoute;
}>;
type SystemAgentVerifiedExecutionFingerprint = {
route: unknown;
@@ -106,7 +116,7 @@ type SystemAgentOwnerPluginRegistryLoader = (params: {
/** Server-local proof returned only after the exact route completes a live turn. */
export type SystemAgentVerifiedInferenceBinding = Readonly<{
configuredRoute: SystemAgentConfiguredRouteIdentity;
execution: SystemAgentConfiguredRoute;
execution: SystemAgentVerifiedExecutionRoute;
executionFingerprint: SystemAgentVerifiedExecutionFingerprint;
ownerPluginIds: readonly string[];
ownerPluginArtifacts: readonly SystemAgentOwnerPluginArtifactIdentity[];
@@ -212,7 +222,7 @@ function systemAgentRouteIdentity(
}
async function resolveCurrentRuntimeOwnerFingerprint(params: {
route: SystemAgentConfiguredRoute;
route: SystemAgentVerifiedExecutionRoute;
kind: OpaqueRuntimeOwnerKind;
runtimeOwnerId: string;
authProfileId?: string;
@@ -400,7 +410,7 @@ function projectOwnerPluginArtifacts(params: {
}
async function projectVerifiedExecutionFingerprint(
config: OpenClawConfig,
route: SystemAgentConfiguredRoute,
route: SystemAgentVerifiedExecutionRoute,
ownerPluginIds: readonly string[],
deps: SystemAgentVerifiedInferenceDeps,
): Promise<SystemAgentVerifiedExecutionFingerprint> {
@@ -431,7 +441,11 @@ function resolveRouteHarnessOwnerPluginIds(
config: OpenClawConfig,
route: SystemAgentConfiguredRoute,
): string[] {
if (route.runner !== "embedded" || route.agentHarnessRuntimeOverride === "openclaw") {
if (
route.runner !== "embedded" ||
!route.agentHarnessRuntimeOverride ||
route.agentHarnessRuntimeOverride === "openclaw"
) {
return [];
}
const workspaceDir = resolveAgentWorkspaceDir(config, route.agentId, process.env);
@@ -487,7 +501,7 @@ export function captureSystemAgentOwnerPluginArtifacts(params: {
}
async function resolveCurrentAuthFingerprint(params: {
route: SystemAgentConfiguredRoute;
route: SystemAgentVerifiedExecutionRoute;
authProfileId?: string;
modelId?: string;
modelApi?: string;
@@ -645,12 +659,15 @@ export async function createSystemAgentVerifiedInferenceBinding(params: {
}): Promise<SystemAgentVerifiedInferenceBinding> {
const deps = params.deps ?? {};
const runConfig = structuredClone(params.executionRoute.runConfig);
const execution = { ...params.executionRoute, runConfig } as SystemAgentConfiguredRoute;
const authProfileId = params.auth.authProfileId ?? execution.authProfileId;
const configuredExecution = {
...params.executionRoute,
runConfig,
} as SystemAgentConfiguredRoute;
const authProfileId = params.auth.authProfileId ?? configuredExecution.authProfileId;
const modelId = params.auth.modelId?.trim();
const modelApi = params.auth.modelApi?.trim();
if (authProfileId) {
execution.authProfileId = authProfileId;
configuredExecution.authProfileId = authProfileId;
}
const proofKind = params.auth.runtimeOwnerFingerprint ? "runtime-owner" : "credential";
if (
@@ -664,17 +681,15 @@ export async function createSystemAgentVerifiedInferenceBinding(params: {
throw new Error("The successful inference run did not report its exact runtime owner.");
}
let successfulHarnessId: string | undefined;
if (execution.runner === "embedded") {
const configuredHarnessId = execution.agentHarnessRuntimeOverride.trim();
if (configuredExecution.runner === "embedded") {
const configuredHarnessId = configuredExecution.agentHarnessRuntimeOverride?.trim();
const reportedHarnessId = params.auth.agentHarnessId?.trim();
if (!configuredHarnessId) {
throw new Error("The configured inference route did not select an agent harness.");
}
if (configuredHarnessId === "auto" && !reportedHarnessId) {
if ((!configuredHarnessId || configuredHarnessId === "auto") && !reportedHarnessId) {
throw new Error("The successful inference run did not report its exact agent harness.");
}
if (
reportedHarnessId &&
configuredHarnessId &&
configuredHarnessId !== "auto" &&
reportedHarnessId !== configuredHarnessId
) {
@@ -683,8 +698,14 @@ export async function createSystemAgentVerifiedInferenceBinding(params: {
);
}
successfulHarnessId = reportedHarnessId ?? configuredHarnessId;
execution.agentHarnessRuntimeOverride = successfulHarnessId;
}
const execution =
configuredExecution.runner === "embedded"
? ({
...configuredExecution,
agentHarnessRuntimeOverride: successfulHarnessId!,
} satisfies SystemAgentVerifiedExecutionRoute)
: configuredExecution;
let currentRuntimeArtifactFingerprint: string | undefined;
if (execution.runner === "cli") {
if (!params.auth.runtimeArtifactFingerprint || !params.auth.runtimeArtifactId?.trim()) {
@@ -837,10 +858,10 @@ export async function hasCurrentSystemAgentOwnerPluginArtifacts(
* switch this frozen run, while relevant runtime plugin membership and the
* actual selected credential are checked explicitly.
*/
export async function resolveSystemAgentVerifiedInferenceRoute(
async function resolveSystemAgentVerifiedInferenceStateInternal(
binding: SystemAgentVerifiedInferenceBinding,
deps: SystemAgentVerifiedInferenceDeps = {},
): Promise<SystemAgentConfiguredRoute | null> {
): Promise<SystemAgentVerifiedInferenceState | null> {
const readSnapshot =
deps.readConfigFileSnapshot ?? (await import("../config/config.js")).readConfigFileSnapshot;
const snapshot = await readSnapshot();
@@ -861,7 +882,7 @@ export async function resolveSystemAgentVerifiedInferenceRoute(
}
// Keep the live-tested runner/harness selection frozen, but reproject its
// owner through current config so policy/backend changes cannot reuse proof.
const currentExecution: SystemAgentConfiguredRoute = {
const currentExecution: SystemAgentVerifiedExecutionRoute = {
...binding.execution,
runConfig: currentRoute.runConfig,
};
@@ -949,6 +970,20 @@ export async function resolveSystemAgentVerifiedInferenceRoute(
if (currentAuthFingerprint !== binding.auth.authFingerprint) {
return null;
}
return binding.execution;
return { config, route: binding.execution };
}
export async function resolveSystemAgentVerifiedInferenceState(
binding: SystemAgentVerifiedInferenceBinding,
deps: SystemAgentVerifiedInferenceDeps = {},
): Promise<SystemAgentVerifiedInferenceState | null> {
return resolveSystemAgentVerifiedInferenceStateInternal(binding, deps);
}
export async function resolveSystemAgentVerifiedInferenceRoute(
binding: SystemAgentVerifiedInferenceBinding,
deps: SystemAgentVerifiedInferenceDeps = {},
): Promise<SystemAgentVerifiedExecutionRoute | null> {
return (await resolveSystemAgentVerifiedInferenceStateInternal(binding, deps))?.route ?? null;
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */