fix(agents): load manifest-owned harnesses

This commit is contained in:
Vincent Koc
2026-06-22 11:34:55 +08:00
committed by Vincent Koc
parent 8cf0d7dd33
commit 741bac9fdf
5 changed files with 249 additions and 59 deletions
+57 -1
View File
@@ -29,6 +29,7 @@ const resolveEmbeddedAgentStreamFnMock = vi.fn();
const prepareCliRunContextMock = vi.fn();
const executePreparedCliRunMock = vi.fn();
const diagDebugMock = vi.fn();
const ensureSelectedAgentHarnessPluginMock = vi.fn();
vi.mock("../llm/stream.js", async () => {
const original = await vi.importActual<typeof import("../llm/stream.js")>("../llm/stream.js");
@@ -119,7 +120,7 @@ vi.mock("./model-runtime-aliases.js", () => ({
}
}
}
return runtime || undefined;
return runtime === "claude-cli" ? runtime : undefined;
},
}));
@@ -131,6 +132,11 @@ vi.mock("./cli-runner/execute.runtime.js", () => ({
executePreparedCliRun: (...args: unknown[]) => executePreparedCliRunMock(...args),
}));
vi.mock("./harness/runtime-plugin.js", () => ({
ensureSelectedAgentHarnessPlugin: (...args: unknown[]) =>
ensureSelectedAgentHarnessPluginMock(...args),
}));
vi.mock("./embedded-agent-runner/runs.js", () => ({
getActiveEmbeddedRunSnapshot: (...args: unknown[]) => getActiveEmbeddedRunSnapshotMock(...args),
}));
@@ -455,6 +461,7 @@ describe("runBtwSideQuestion", () => {
prepareCliRunContextMock.mockReset();
executePreparedCliRunMock.mockReset();
diagDebugMock.mockReset();
ensureSelectedAgentHarnessPluginMock.mockReset();
clearAgentHarnesses();
readFileMock.mockResolvedValue("mock transcript");
@@ -835,6 +842,55 @@ describe("runBtwSideQuestion", () => {
expect(streamSimpleMock).toHaveBeenCalledTimes(1);
});
it("loads a cold Copilot harness before selecting the /btw provider fallback", async () => {
let loaded = false;
ensureSelectedAgentHarnessPluginMock.mockImplementation(async () => {
if (loaded) {
return;
}
loaded = true;
registerAgentHarness({
id: "copilot",
label: "Copilot test harness",
supports: () => ({ supported: true, priority: 100 }),
runAttempt: vi.fn(),
});
});
resolveModelWithRegistryMock.mockReturnValue({
provider: "github-copilot",
id: "gpt-4o",
api: "openai-completions",
});
mockDoneAnswer("Copilot fallback answer.");
const result = await runSideQuestion({
cfg: {
agents: {
defaults: {
models: {
"github-copilot/gpt-4o": { agentRuntime: { id: "copilot" } },
},
},
},
} as never,
provider: "github-copilot",
model: "gpt-4o",
sessionKey: DEFAULT_SESSION_KEY,
});
expect(result).toEqual({ text: "Copilot fallback answer." });
expect(ensureSelectedAgentHarnessPluginMock).toHaveBeenCalledOnce();
expect(ensureSelectedAgentHarnessPluginMock).toHaveBeenCalledWith({
provider: "github-copilot",
modelId: "gpt-4o",
config: expect.any(Object),
agentId: "main",
sessionKey: DEFAULT_SESSION_KEY,
workspaceDir: "/tmp/workspace",
});
expect(streamSimpleMock).toHaveBeenCalledOnce();
});
it("runs CLI-runtime alias BTW as an ephemeral CLI side question", async () => {
const cleanup = vi.fn(async () => undefined);
prepareCliRunContextMock.mockResolvedValueOnce({
+33 -14
View File
@@ -30,6 +30,7 @@ import { EmbeddedBlockChunker, type BlockReplyChunking } from "./embedded-agent-
import { resolveModelWithRegistry } from "./embedded-agent-runner/model.js";
import { getActiveEmbeddedRunSnapshot } from "./embedded-agent-runner/runs.js";
import { resolveEmbeddedAgentStreamFn } from "./embedded-agent-runner/stream-resolution.js";
import { ensureSelectedAgentHarnessPlugin } from "./harness/runtime-plugin.js";
import {
resolveAvailableAgentHarnessPolicy,
resolvePluginHarnessPolicyToolsAllow,
@@ -479,13 +480,32 @@ export async function runBtwSideQuestion(
config: params.cfg,
});
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, sessionAgentId);
const harness = selectAgentHarness({
provider: params.provider,
modelId: params.model,
config: params.cfg,
agentId: sessionAgentId,
sessionKey: params.sessionKey,
});
const preparedHarnesses = new Map<string, AgentHarness>();
const prepareHarness = async (provider: string, modelId: string): Promise<AgentHarness> => {
const key = `${provider}/${modelId}`;
const cached = preparedHarnesses.get(key);
if (cached) {
return cached;
}
await ensureSelectedAgentHarnessPlugin({
provider,
modelId,
config: params.cfg,
agentId: sessionAgentId,
sessionKey: params.sessionKey,
workspaceDir,
});
const harness = selectAgentHarness({
provider,
modelId,
config: params.cfg,
agentId: sessionAgentId,
sessionKey: params.sessionKey,
});
preparedHarnesses.set(key, harness);
return harness;
};
const harness = await prepareHarness(params.provider, params.model);
let runtimeSelection: Awaited<ReturnType<typeof resolveRuntimeModel>> | undefined;
const resolveRuntimeSelection = async () => {
if (!runtimeSelection) {
@@ -647,13 +667,12 @@ export async function runBtwSideQuestion(
}
const runtimeSelectionForHarness = await resolveRuntimeSelection();
const runtimeHarness = selectAgentHarness({
provider: runtimeSelectionForHarness.model.provider,
modelId: runtimeSelectionForHarness.model.id,
config: params.cfg,
agentId: sessionAgentId,
sessionKey: params.sessionKey,
});
// Model resolution can canonicalize a legacy provider alias, so reselect against the resolved
// provider/model instead of reusing the raw route's selection.
const runtimeHarness = await prepareHarness(
runtimeSelectionForHarness.model.provider,
runtimeSelectionForHarness.model.id,
);
if (runtimeHarness.runSideQuestion) {
return runHarnessSideQuestion(runtimeHarness, runtimeSelectionForHarness);
}
+79 -31
View File
@@ -6,6 +6,7 @@ const mocks = vi.hoisted(() => ({
ensurePluginRegistryLoaded: vi.fn(),
resolveActivatableProviderOwnerPluginIds: vi.fn(),
resolveBundledProviderCompatPluginIds: vi.fn(),
resolveManifestActivationPlan: vi.fn(),
resolveOwningPluginIdsForProvider: vi.fn(),
}));
@@ -20,6 +21,10 @@ vi.mock("../../plugins/providers.js", () => ({
resolveOwningPluginIdsForProviderRef: mocks.resolveOwningPluginIdsForProvider,
}));
vi.mock("../../plugins/activation-planner.js", () => ({
resolveManifestActivationPlan: mocks.resolveManifestActivationPlan,
}));
describe("ensureSelectedAgentHarnessPlugin", () => {
let ensureSelectedAgentHarnessPlugin: typeof import("./runtime-plugin.js").ensureSelectedAgentHarnessPlugin;
@@ -27,7 +32,30 @@ describe("ensureSelectedAgentHarnessPlugin", () => {
mocks.ensurePluginRegistryLoaded.mockReset();
mocks.resolveActivatableProviderOwnerPluginIds.mockReset();
mocks.resolveBundledProviderCompatPluginIds.mockReset();
mocks.resolveManifestActivationPlan.mockReset();
mocks.resolveOwningPluginIdsForProvider.mockReset();
mocks.resolveManifestActivationPlan.mockImplementation(
({
trigger,
config,
}: {
trigger: { kind: "agentHarness"; runtime: string };
config?: OpenClawConfig;
}) => {
const pluginId = trigger.runtime;
const allow = config?.plugins?.allow ?? [];
if (
config?.plugins?.entries?.[pluginId]?.enabled === false ||
(allow.length > 0 && !allow.includes(pluginId))
) {
return { entries: [] };
}
return {
entries:
pluginId === "codex" || pluginId === "copilot" ? [{ pluginId, origin: "bundled" }] : [],
};
},
);
mocks.resolveOwningPluginIdsForProvider.mockImplementation(
({ provider }: { provider: string }) => (provider === "openai" ? ["openai"] : undefined),
);
@@ -132,6 +160,54 @@ describe("ensureSelectedAgentHarnessPlugin", () => {
);
});
it("loads a manifest-owned custom harness runtime before selection", async () => {
mocks.resolveManifestActivationPlan.mockReturnValueOnce({
entries: [{ pluginId: "custom-harness-plugin", origin: "workspace" }],
});
await ensureSelectedAgentHarnessPlugin({
provider: "custom-provider",
modelId: "custom-model",
config: {
plugins: {
entries: {
"custom-harness-plugin": { enabled: true },
},
},
} as OpenClawConfig,
agentHarnessRuntimeOverride: "custom-harness",
workspaceDir: "/tmp/workspace",
});
expect(mocks.resolveManifestActivationPlan).toHaveBeenCalledWith({
trigger: { kind: "agentHarness", runtime: "custom-harness" },
config: expect.any(Object),
workspaceDir: "/tmp/workspace",
});
expect(mocks.ensurePluginRegistryLoaded).toHaveBeenCalledWith(
expect.objectContaining({
scope: "all",
workspaceDir: "/tmp/workspace",
onlyPluginIds: ["custom-harness-plugin", "memory-core"],
}),
);
});
it("does not activate an untrusted workspace harness from manifest metadata alone", async () => {
mocks.resolveManifestActivationPlan.mockReturnValueOnce({
entries: [{ pluginId: "custom-harness-plugin", origin: "workspace" }],
});
await ensureSelectedAgentHarnessPlugin({
provider: "custom-provider",
modelId: "custom-model",
agentHarnessRuntimeOverride: "custom-harness",
workspaceDir: "/tmp/workspace",
});
expect(mocks.ensurePluginRegistryLoaded).not.toHaveBeenCalled();
});
it("does not bypass a restrictive allowlist that omits a configured Copilot harness", async () => {
// A configured harness can request loading, but explicit plugin allowlists
// remain the operator's boundary and are not widened implicitly.
@@ -158,21 +234,7 @@ describe("ensureSelectedAgentHarnessPlugin", () => {
workspaceDir: "/tmp/workspace",
});
expect(mocks.ensurePluginRegistryLoaded).toHaveBeenCalledWith(
expect.objectContaining({
scope: "all",
workspaceDir: "/tmp/workspace",
onlyPluginIds: ["copilot"],
config: expect.objectContaining({
plugins: expect.objectContaining({
allow: ["telegram"],
entries: expect.not.objectContaining({
copilot: expect.anything(),
}),
}),
}),
}),
);
expect(mocks.ensurePluginRegistryLoaded).not.toHaveBeenCalled();
});
it("widens a scoped harness allowlist with the provider owner for openai models", async () => {
@@ -337,22 +399,7 @@ describe("ensureSelectedAgentHarnessPlugin", () => {
expect(mocks.resolveOwningPluginIdsForProvider).not.toHaveBeenCalled();
expect(mocks.resolveBundledProviderCompatPluginIds).not.toHaveBeenCalled();
expect(mocks.resolveActivatableProviderOwnerPluginIds).not.toHaveBeenCalled();
expect(mocks.ensurePluginRegistryLoaded).toHaveBeenCalledWith(
expect.objectContaining({
scope: "all",
workspaceDir: "/tmp/workspace",
onlyPluginIds: ["codex"],
config: expect.objectContaining({
plugins: expect.objectContaining({
allow: ["telegram"],
entries: expect.not.objectContaining({
codex: expect.anything(),
openai: expect.anything(),
}),
}),
}),
}),
);
expect(mocks.ensurePluginRegistryLoaded).not.toHaveBeenCalled();
});
it("keeps real bundled memory-core in a Codex scoped load when the provider has no owner plugin", async () => {
@@ -417,5 +464,6 @@ describe("ensureSelectedAgentHarnessPlugin", () => {
expect(mocks.ensurePluginRegistryLoaded).not.toHaveBeenCalled();
expect(mocks.resolveOwningPluginIdsForProvider).not.toHaveBeenCalled();
expect(mocks.resolveManifestActivationPlan).not.toHaveBeenCalled();
});
});
+41 -13
View File
@@ -3,8 +3,13 @@
*/
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { withActivatedPluginIds } from "../../plugins/activation-context.js";
import { resolveEffectivePluginActivationState } from "../../plugins/config-state.js";
import { resolveManifestActivationPlan } from "../../plugins/activation-planner.js";
import {
normalizePluginsConfig,
resolveEffectivePluginActivationState,
} from "../../plugins/config-state.js";
import { isPluginEnabledByDefaultForPlatform } from "../../plugins/default-enablement.js";
import { hasExplicitManifestOwnerTrust } from "../../plugins/manifest-owner-policy.js";
import {
loadPluginRegistrySnapshot,
normalizePluginsConfigWithRegistry,
@@ -16,16 +21,9 @@ import {
} from "../../plugins/providers.js";
import { isDefaultAgentRuntimeId, OPENCLAW_AGENT_RUNTIME_ID } from "../agent-runtime-id.js";
import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import { isCliRuntimeAliasForProvider } from "../model-runtime-aliases.js";
import { resolveAgentHarnessPolicy } from "./policy.js";
/**
* Lazy-loads plugin-backed harness runtimes before selection.
*
* Only cold-loadable runtimes live here; always-loaded core/openclaw runtimes should not trigger
* plugin registry scans on every embedded-agent turn.
*/
const COLD_LOADABLE_HARNESS_PLUGIN_IDS = new Set(["codex", "copilot"]);
function dedupePluginIds(values: readonly string[]): string[] {
const seen = new Set<string>();
const result: string[] = [];
@@ -82,13 +80,35 @@ function resolveHarnessPluginIds(params: {
config?: OpenClawConfig;
workspaceDir: string;
}): string[] {
const activationPlan = resolveManifestActivationPlan({
trigger: { kind: "agentHarness", runtime: params.runtime },
config: params.config,
workspaceDir: params.workspaceDir,
});
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
const harnessPluginIds = activationPlan.entries
.filter(
(entry) =>
entry.origin === "bundled" ||
hasExplicitManifestOwnerTrust({
plugin: { id: entry.pluginId },
normalizedConfig: normalizedPlugins,
}),
)
.map((entry) => entry.pluginId);
if (harnessPluginIds.length === 0) {
return [];
}
if (params.runtime !== "codex") {
return [params.runtime];
return harnessPluginIds;
}
if (!harnessPluginIds.includes("codex")) {
return harnessPluginIds;
}
if (restrictiveAllowlistOmitsPlugin(params.config, "codex")) {
// Respect a restrictive allowlist even when Codex would normally pull in provider owner
// plugins. Operators who set an allowlist expect no implicit plugin expansion.
return ["codex"];
return harnessPluginIds;
}
const providerOwnerPluginIds = dedupePluginIds(
resolveOwningPluginIdsForProviderRef({
@@ -98,7 +118,7 @@ function resolveHarnessPluginIds(params: {
}) ?? [],
);
if (providerOwnerPluginIds.length === 0) {
return ["codex"];
return harnessPluginIds;
}
const safeProviderOwnerPluginIds = dedupePluginIds([
...resolveBundledProviderCompatPluginIds({
@@ -114,6 +134,7 @@ function resolveHarnessPluginIds(params: {
]);
return dedupePluginIds([
"codex",
...harnessPluginIds,
...providerOwnerPluginIds.filter(
(pluginId) => pluginId !== "codex" && safeProviderOwnerPluginIds.includes(pluginId),
),
@@ -164,7 +185,11 @@ export async function ensureSelectedAgentHarnessPlugin(params: {
if (
isDefaultAgentRuntimeId(runtime) ||
runtime === OPENCLAW_AGENT_RUNTIME_ID ||
!COLD_LOADABLE_HARNESS_PLUGIN_IDS.has(runtime)
isCliRuntimeAliasForProvider({
runtime,
provider: params.provider,
cfg: params.config,
})
) {
return;
}
@@ -177,6 +202,9 @@ export async function ensureSelectedAgentHarnessPlugin(params: {
config: params.config,
workspaceDir: params.workspaceDir,
});
if (pluginIds.length === 0) {
return;
}
const memoryPluginIds = resolveSelectedMemoryPluginIds({
config: params.config,
workspaceDir: params.workspaceDir,
+39
View File
@@ -68,6 +68,18 @@ describe("activation planner", () => {
hooks: [],
origin: "bundled",
},
{
id: "custom-harness-plugin",
providers: [],
channels: [],
cliBackends: [],
skills: [],
hooks: [],
activation: {
onAgentHarnesses: ["custom-harness"],
},
origin: "workspace",
},
{
id: "demo-channel",
channels: ["telegram"],
@@ -145,6 +157,33 @@ describe("activation planner", () => {
).toEqual([]);
});
it("plans manifest-owned custom harnesses and respects their activation policy", () => {
expect(
resolveManifestActivationPluginIds({
trigger: {
kind: "agentHarness",
runtime: "custom-harness",
},
}),
).toEqual(["custom-harness-plugin"]);
expect(
resolveManifestActivationPluginIds({
config: {
plugins: {
entries: {
"custom-harness-plugin": { enabled: false },
},
},
},
trigger: {
kind: "agentHarness",
runtime: "custom-harness",
},
}),
).toEqual([]);
});
it("keeps ids-only provider, agent harness, channel, and route planning stable", () => {
expect(
resolveManifestActivationPluginIds({