fix(qa): restore agentic parity tool runtime

This commit is contained in:
Peter Steinberger
2026-04-23 03:56:56 +01:00
parent ca8a6e811c
commit d0d018bdad
7 changed files with 102 additions and 13 deletions

View File

@@ -76,7 +76,8 @@ describe("qa docker harness", () => {
const config = await readFile(path.join(outputDir, "state", "openclaw.json"), "utf8");
expect(config).toContain('"allowInsecureAuth": true');
expect(config).toContain('"enabled": false');
expect(config).toContain('"pluginToolsMcpBridge": true');
expect(config).toContain('"openClawToolsMcpBridge": true');
expect(config).toContain("/app/dist/control-ui");
expect(config).toContain("C-3PO QA");
expect(config).toContain('"/tmp/openclaw/workspace"');

View File

@@ -59,7 +59,13 @@ describe("buildQaGatewayConfig", () => {
expect(cfg.models?.providers?.anthropic?.baseUrl).toBe("http://127.0.0.1:44080");
expect(cfg.models?.providers?.anthropic?.request).toEqual({ allowPrivateNetwork: true });
expect(cfg.plugins?.allow).toEqual(["acpx", "memory-core", "qa-channel"]);
expect(cfg.plugins?.entries?.acpx).toEqual({ enabled: false });
expect(cfg.plugins?.entries?.acpx).toEqual({
enabled: true,
config: {
pluginToolsMcpBridge: true,
openClawToolsMcpBridge: true,
},
});
expect(cfg.plugins?.entries?.["memory-core"]).toEqual({ enabled: true });
expect(cfg.plugins?.entries?.["qa-channel"]).toEqual({ enabled: true });
expect(cfg.plugins?.entries?.openai).toBeUndefined();

View File

@@ -118,10 +118,11 @@ export function buildQaGatewayConfig(params: {
allow: allowedPlugins,
entries: {
acpx: {
// The parity gateway stages a clean bundled-plugin tree. Keep the
// runtime backend plugin in the allowlist so this disabled entry is
// not mistaken for stale config when optional bundles are pruned.
enabled: false,
enabled: true,
config: {
pluginToolsMcpBridge: true,
openClawToolsMcpBridge: true,
},
},
"memory-core": {
enabled: true,

View File

@@ -597,9 +597,13 @@ vi.mock("../thinking.js", () => ({
dropThinkingBlocks: <T>(messages: T) => messages,
}));
vi.mock("../tool-name-allowlist.js", () => ({
collectAllowedToolNames: () => undefined,
}));
vi.mock("../tool-name-allowlist.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../tool-name-allowlist.js")>();
return {
...actual,
collectAllowedToolNames: () => undefined,
};
});
vi.mock("../tool-split.js", () => ({
splitSdkTools: ({ tools }: { tools: unknown[] }) => ({
@@ -660,6 +664,7 @@ export type MutableSession = {
};
};
prompt: (prompt: string, options?: { images?: unknown[] }) => Promise<void>;
setActiveToolsByName: (toolNames: string[]) => void;
abort: () => Promise<void>;
dispose: () => void;
steer: (text: string) => Promise<void>;
@@ -772,6 +777,7 @@ export function createDefaultEmbeddedSession(params?: {
},
},
},
setActiveToolsByName: () => {},
prompt: async (prompt, options) => {
if (params?.prompt) {
await params.prompt(session, prompt, options);

View File

@@ -0,0 +1,39 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
cleanupTempPaths,
createContextEngineAttemptRunner,
createContextEngineBootstrapAndAssemble,
getHoisted,
resetEmbeddedAttemptHarness,
} from "./attempt.spawn-workspace.test-support.js";
const hoisted = getHoisted();
describe("runEmbeddedAttempt tool allowlist", () => {
const tempPaths: string[] = [];
beforeEach(() => {
resetEmbeddedAttemptHarness();
});
afterEach(async () => {
await cleanupTempPaths(tempPaths);
});
it("passes OpenClaw-managed custom tool names to Pi's session allowlist", async () => {
await createContextEngineAttemptRunner({
sessionKey: "agent:qa:repo-contract",
tempPaths,
contextEngine: createContextEngineBootstrapAndAssemble(),
});
const options = hoisted.createAgentSessionMock.mock.calls.at(-1)?.[0] as
| {
customTools?: Array<{ name?: string }>;
tools?: string[];
}
| undefined;
expect(options?.customTools?.map((tool) => tool.name)).toContain("sessions_spawn");
expect(options?.tools).toContain("sessions_spawn");
});
});

View File

@@ -15,7 +15,9 @@ vi.mock("../provider-runtime.runtime.js", () => ({
prepareProviderRuntimeAuth: hoisted.prepareProviderRuntimeAuth,
}));
let getApiKeyForModel: typeof import("./runtime-model-auth.runtime.js").getApiKeyForModel;
let getRuntimeAuthForModel: typeof import("./runtime-model-auth.runtime.js").getRuntimeAuthForModel;
let resolveApiKeyForProvider: typeof import("./runtime-model-auth.runtime.js").resolveApiKeyForProvider;
const MODEL = {
id: "github-copilot/gpt-4o",
@@ -26,7 +28,8 @@ const MODEL = {
describe("runtime-model-auth.runtime", () => {
beforeAll(async () => {
({ getRuntimeAuthForModel } = await import("./runtime-model-auth.runtime.js"));
({ getApiKeyForModel, getRuntimeAuthForModel, resolveApiKeyForProvider } =
await import("./runtime-model-auth.runtime.js"));
});
beforeEach(() => {
@@ -115,4 +118,24 @@ describe("runtime-model-auth.runtime", () => {
});
expect(hoisted.prepareProviderRuntimeAuth).not.toHaveBeenCalled();
});
it("keeps direct model auth exports available for bundled runtime facades", async () => {
hoisted.getApiKeyForModel.mockResolvedValue({
apiKey: "model-key",
source: "env:OPENAI_API_KEY",
mode: "api-key",
});
hoisted.resolveApiKeyForProvider.mockResolvedValue({
apiKey: "provider-key",
source: "env:OPENAI_API_KEY",
mode: "api-key",
});
await expect(getApiKeyForModel({ model: MODEL as never })).resolves.toMatchObject({
apiKey: "model-key",
});
await expect(resolveApiKeyForProvider({ provider: "openai" })).resolves.toMatchObject({
apiKey: "provider-key",
});
});
});

View File

@@ -1,10 +1,23 @@
import type { Api, Model } from "@mariozechner/pi-ai";
import { getApiKeyForModel, resolveApiKeyForProvider } from "../../agents/model-auth.js";
import {
getApiKeyForModel as resolveModelApiKey,
resolveApiKeyForProvider as resolveProviderApiKey,
} from "../../agents/model-auth.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { prepareProviderRuntimeAuth } from "../provider-runtime.runtime.js";
import type { ResolvedProviderRuntimeAuth } from "./model-auth-types.js";
export { getApiKeyForModel, resolveApiKeyForProvider };
export async function getApiKeyForModel(
params: Parameters<typeof resolveModelApiKey>[0],
): Promise<Awaited<ReturnType<typeof resolveModelApiKey>>> {
return resolveModelApiKey(params);
}
export async function resolveApiKeyForProvider(
params: Parameters<typeof resolveProviderApiKey>[0],
): Promise<Awaited<ReturnType<typeof resolveProviderApiKey>>> {
return resolveProviderApiKey(params);
}
/**
* Resolve request-ready auth for a runtime model, applying any provider-owned
@@ -15,7 +28,7 @@ export async function getRuntimeAuthForModel(params: {
cfg?: OpenClawConfig;
workspaceDir?: string;
}): Promise<ResolvedProviderRuntimeAuth> {
const resolvedAuth = await getApiKeyForModel({
const resolvedAuth = await resolveModelApiKey({
model: params.model,
cfg: params.cfg,
});