mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
c896b9b2cd
commit
c5b7b8e33c
+1
-1
@@ -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" },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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() ||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user