fix(agents): restrict harness tool authority

This commit is contained in:
Vincent Koc
2026-08-06 06:44:28 +02:00
parent d863dd5f5e
commit aacbcaacc8
18 changed files with 237 additions and 46 deletions
@@ -1,7 +1,7 @@
type OpenClawCodingToolsFactory =
(typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"];
type AgentHarnessCodingToolsFactory =
(typeof import("openclaw/plugin-sdk/agent-harness-tool-runtime"))["createOpenClawCodingToolsForAgentHarness"];
(typeof import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime"))["createOpenClawCodingToolsForAgentHarness"];
/** Mutable dependency seam shared by dynamic-tool construction and its behavioral tests. */
export const dynamicToolBuildState: {
@@ -1,13 +1,13 @@
import { afterEach, expect, it, vi } from "vitest";
afterEach(() => {
vi.doUnmock("openclaw/plugin-sdk/agent-harness-tool-runtime");
vi.doUnmock("openclaw/plugin-sdk/agent-harness-tool-authority-runtime");
vi.resetModules();
});
it("does not load the private tool builder before a tool-capable turn", async () => {
let toolBuilderImports = 0;
vi.doMock("openclaw/plugin-sdk/agent-harness-tool-runtime", () => {
vi.doMock("openclaw/plugin-sdk/agent-harness-tool-authority-runtime", () => {
toolBuilderImports += 1;
return {
createOpenClawCodingToolsForAgentHarness: vi.fn(() => []),
@@ -235,7 +235,7 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
const agentHarnessCodingToolsFactory = injectedOpenClawCodingToolsFactory
? undefined
: (dynamicToolBuildState.agentHarnessCodingToolsFactory ??
(await import("openclaw/plugin-sdk/agent-harness-tool-runtime"))
(await import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime"))
.createOpenClawCodingToolsForAgentHarness);
const createOpenClawCodingTools =
injectedOpenClawCodingToolsFactory ??
@@ -100,7 +100,7 @@ vi.mock("./provider-capabilities.js", () => ({
resolveCodexProviderWebSearchSupportForClientMock(...args),
}));
vi.mock("openclaw/plugin-sdk/agent-harness-tool-runtime", () => ({
vi.mock("openclaw/plugin-sdk/agent-harness-tool-authority-runtime", () => ({
createOpenClawCodingToolsForAgentHarnessSideQuestion: (params: unknown, options: unknown) =>
createOpenClawCodingToolsForSideQuestionMock(params, options),
}));
@@ -938,7 +938,7 @@ async function createCodexSideToolBridge(input: {
let tools: AnyAgentTool[] = [];
if (supportsModelTools(runtimeModel)) {
const { createOpenClawCodingToolsForAgentHarnessSideQuestion } =
await import("openclaw/plugin-sdk/agent-harness-tool-runtime");
await import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime");
const sandboxSessionKey =
input.params.sandboxSessionKey?.trim() ||
input.params.sessionKey?.trim() ||
@@ -0,0 +1,43 @@
import { afterEach, expect, it, vi } from "vitest";
afterEach(() => {
vi.doUnmock("openclaw/plugin-sdk/agent-harness-tool-authority-runtime");
vi.resetModules();
});
it("loads the private tool builder only after the tool-construction guard", async () => {
let toolBuilderImports = 0;
vi.doMock("openclaw/plugin-sdk/agent-harness-tool-authority-runtime", () => {
toolBuilderImports += 1;
return {
createOpenClawCodingToolsForAgentHarness: vi.fn(() => []),
};
});
const { createCopilotToolBridge } = await import("./tool-bridge.js");
expect(toolBuilderImports).toBe(0);
await expect(
createCopilotToolBridge({
admittedAttempt: {} as never,
agentId: "agent-1",
attemptParams: { disableTools: true } as never,
modelId: "gpt-4o",
modelProvider: "github-copilot",
sessionId: "session-1",
}),
).resolves.toEqual({ codeModeEngaged: false, sdkTools: [], sourceTools: [] });
expect(toolBuilderImports).toBe(0);
await expect(
createCopilotToolBridge({
admittedAttempt: {} as never,
agentId: "agent-1",
attemptParams: {} as never,
modelId: "gpt-4o",
modelProvider: "github-copilot",
sessionId: "session-1",
}),
).resolves.toMatchObject({ sdkTools: [], sourceTools: [] });
expect(toolBuilderImports).toBe(1);
});
+6 -6
View File
@@ -22,10 +22,7 @@ import {
resolveModelAuthMode,
sanitizeToolResult,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import {
createAgentHarnessToolSurfaceRuntime,
createOpenClawCodingToolsForAgentHarness,
} from "openclaw/plugin-sdk/agent-harness-tool-runtime";
import { createAgentHarnessToolSurfaceRuntime } from "openclaw/plugin-sdk/agent-harness-tool-runtime";
type CreateOpenClawCodingTools =
(typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"];
@@ -204,8 +201,11 @@ export async function createCopilotToolBridge(
const createOpenClawCodingTools =
input.createOpenClawCodingTools ??
(admittedAttempt
? (options: OpenClawCodingToolsOptions) =>
createOpenClawCodingToolsForAgentHarness(admittedAttempt, options)
? async (options: OpenClawCodingToolsOptions) => {
const { createOpenClawCodingToolsForAgentHarness } =
await import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime");
return createOpenClawCodingToolsForAgentHarness(admittedAttempt, options);
}
: (await import("openclaw/plugin-sdk/agent-harness")).createOpenClawCodingTools);
const toolSurfaceRuntime = createAgentHarnessToolSurfaceRuntime({
@@ -134,6 +134,9 @@
"openclaw/plugin-sdk/agent-harness-exec-review-runtime": [
"../packages/plugin-sdk/dist/src/plugin-sdk/agent-harness-exec-review-runtime.d.ts"
],
"openclaw/plugin-sdk/agent-harness-tool-authority-runtime": [
"../packages/plugin-sdk/dist/src/plugin-sdk/agent-harness-tool-authority-runtime.d.ts"
],
"openclaw/plugin-sdk/agent-harness-tool-runtime": [
"../packages/plugin-sdk/dist/src/plugin-sdk/agent-harness-tool-runtime.d.ts"
],
+3
View File
@@ -131,6 +131,9 @@
"openclaw/plugin-sdk/agent-harness-exec-review-runtime": [
"../../packages/plugin-sdk/dist/src/plugin-sdk/agent-harness-exec-review-runtime.d.ts"
],
"openclaw/plugin-sdk/agent-harness-tool-authority-runtime": [
"../../packages/plugin-sdk/dist/src/plugin-sdk/agent-harness-tool-authority-runtime.d.ts"
],
"openclaw/plugin-sdk/agent-harness-tool-runtime": [
"../../packages/plugin-sdk/dist/src/plugin-sdk/agent-harness-tool-runtime.d.ts"
],
+1
View File
@@ -48,6 +48,7 @@
"!dist/plugin-sdk/agent-core.d.ts",
"!dist/plugin-sdk/agent-harness-exec-review-runtime.d.ts",
"!dist/plugin-sdk/agent-harness-task-runtime.d.ts",
"!dist/plugin-sdk/agent-harness-tool-authority-runtime.d.ts",
"!dist/plugin-sdk/agent-harness-tool-runtime.d.ts",
"!dist/plugin-sdk/agent-runtime-test-contracts.js",
"!dist/plugin-sdk/agent-runtime-test-contracts.d.ts",
+8 -1
View File
@@ -84,6 +84,10 @@ export const packagedPrivatePluginSdkRuntimeEntrypoints =
productionPluginSdkEntrypointSet.has(entry),
);
const ownerRestrictedPrivatePluginSdkRuntimeEntrypointSet = new Set([
"agent-harness-tool-authority-runtime",
]);
/** Private entrypoints reserved for local tests and QA builds. */
const nonProductionPrivatePluginSdkEntrypoints = privateLocalOnlyPluginSdkEntrypoints.filter(
(entry) => !productionPluginSdkEntrypointSet.has(entry),
@@ -131,7 +135,10 @@ export function buildPluginSdkPackageExports() {
],
];
}
if (packagedPrivatePluginSdkRuntimeEntrypoints.includes(entry)) {
if (
packagedPrivatePluginSdkRuntimeEntrypoints.includes(entry) &&
!ownerRestrictedPrivatePluginSdkRuntimeEntrypointSet.has(entry)
) {
// Official plugins ship separately but execute against the host's private runtime.
// Their declarations stay pack-excluded by listUnpackagedPrivatePluginSdkDistArtifacts.
return [
+1
View File
@@ -107,6 +107,7 @@
"agent-harness",
"agent-harness-exec-review-runtime",
"agent-harness-runtime",
"agent-harness-tool-authority-runtime",
"agent-harness-tool-runtime",
"hook-runtime",
"html-entity-runtime",
@@ -8,6 +8,7 @@
"agent-core",
"agent-harness-exec-review-runtime",
"agent-harness-task-runtime",
"agent-harness-tool-authority-runtime",
"agent-harness-tool-runtime",
"agent-runtime-test-contracts",
"agent-sessions",
@@ -0,0 +1,34 @@
import {
createOpenClawCodingToolsForAgentHarness as createCoreOpenClawCodingToolsForAgentHarness,
createOpenClawCodingToolsForAgentHarnessSideQuestion as createCoreOpenClawCodingToolsForAgentHarnessSideQuestion,
} from "../agents/agent-tools-internal.js";
import type {
AgentHarnessSideQuestionParams,
EmbeddedRunAttemptParams,
} from "./agent-harness-runtime.js";
type OpenClawCodingToolsOptions = NonNullable<
Parameters<typeof import("./agent-harness.js").createOpenClawCodingTools>[0]
>;
/**
* Build tools for the exact host-admitted attempt without exposing its private
* execution attribution to plugin code.
*/
export function createOpenClawCodingToolsForAgentHarness(
attempt: EmbeddedRunAttemptParams,
options?: OpenClawCodingToolsOptions,
): ReturnType<typeof createCoreOpenClawCodingToolsForAgentHarness> {
return createCoreOpenClawCodingToolsForAgentHarness(attempt, options);
}
/**
* Build tools for the exact host-admitted side-question request without
* exposing its private execution attribution to plugin code.
*/
export function createOpenClawCodingToolsForAgentHarnessSideQuestion(
params: AgentHarnessSideQuestionParams,
options?: OpenClawCodingToolsOptions,
): ReturnType<typeof createCoreOpenClawCodingToolsForAgentHarnessSideQuestion> {
return createCoreOpenClawCodingToolsForAgentHarnessSideQuestion(params, options);
}
+3 -33
View File
@@ -1,21 +1,13 @@
import {
createOpenClawCodingToolsForAgentHarness as createCoreOpenClawCodingToolsForAgentHarness,
createOpenClawCodingToolsForAgentHarnessSideQuestion as createCoreOpenClawCodingToolsForAgentHarnessSideQuestion,
} from "../agents/agent-tools-internal.js";
/**
* Packaged private runtime facade for official harness plugins.
* Focused runtime SDK subpath for native harness tool-surface routing.
*
* This subpath is local-only and declaration-excluded. Keep tool-search and
* code-mode dependencies out of the lightweight startup lifecycle facade.
* Keep tool-search and code-mode dependencies out of the lightweight harness
* lifecycle facade used during plugin startup.
*/
import {
createAgentHarnessToolSurfaceRuntime as createCoreAgentHarnessToolSurfaceRuntime,
type AgentHarnessToolSurfaceRuntime as CoreAgentHarnessToolSurfaceRuntime,
} from "../agents/harness/tool-surface-bridge.js";
import type {
AgentHarnessSideQuestionParams,
EmbeddedRunAttemptParams,
} from "./agent-harness-runtime.js";
type OpenClawCodingToolsOptions = NonNullable<
Parameters<typeof import("./agent-harness.js").createOpenClawCodingTools>[0]
@@ -41,25 +33,3 @@ export function createAgentHarnessToolSurfaceRuntime(
): AgentHarnessToolSurfaceRuntime {
return createCoreAgentHarnessToolSurfaceRuntime(params);
}
/**
* Build tools for the exact host-admitted attempt without exposing its private
* execution attribution to plugin code.
*/
export function createOpenClawCodingToolsForAgentHarness(
attempt: EmbeddedRunAttemptParams,
options?: OpenClawCodingToolsOptions,
): ReturnType<typeof createCoreOpenClawCodingToolsForAgentHarness> {
return createCoreOpenClawCodingToolsForAgentHarness(attempt, options);
}
/**
* Build tools for the exact host-admitted side-question request without
* exposing its private execution attribution to plugin code.
*/
export function createOpenClawCodingToolsForAgentHarnessSideQuestion(
params: AgentHarnessSideQuestionParams,
options?: OpenClawCodingToolsOptions,
): ReturnType<typeof createCoreOpenClawCodingToolsForAgentHarnessSideQuestion> {
return createCoreOpenClawCodingToolsForAgentHarnessSideQuestion(params, options);
}
@@ -817,6 +817,26 @@ describe("plugin-sdk package contract guardrails", () => {
});
});
it("keeps the agent harness tool authority runtime owner-restricted", () => {
const entrypoint = "agent-harness-tool-authority-runtime";
const localOnly = new Set(privateLocalOnlyPluginSdkEntrypoints);
const packageExports = new Set(collectPluginSdkPackageExports());
const typedExports = collectTypedPluginSdkPackageExports();
const excludedDeclarations = collectPackExcludedPluginSdkDeclarations();
expect({
localOnly: localOnly.has(entrypoint),
packageExport: packageExports.has(entrypoint),
typedExport: typedExports.has(entrypoint),
declarationExcluded: excludedDeclarations.has(entrypoint),
}).toEqual({
localOnly: true,
packageExport: false,
typedExport: false,
declarationExcluded: true,
});
});
it("keeps configured local-origin fetch helpers out of deprecated infra-runtime", () => {
const source = fs.readFileSync(resolve(REPO_ROOT, "src/plugin-sdk/infra-runtime.ts"), "utf8");
+100
View File
@@ -654,6 +654,106 @@ describe("plugin sdk alias helpers", () => {
expect(shadowCodexSubpaths).toEqual(["core"]);
});
it("aliases harness tool authority only for trusted official harness plugins", () => {
const fixture = createPluginSdkAliasFixture({
packageExports: {
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
},
});
const authoritySubpath = "agent-harness-tool-authority-runtime";
const sourceAuthorityPath = path.join(
fixture.root,
"src",
"plugin-sdk",
`${authoritySubpath}.ts`,
);
const distAuthorityPath = path.join(
fixture.root,
"dist",
"plugin-sdk",
`${authoritySubpath}.js`,
);
fs.writeFileSync(
path.join(fixture.root, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"),
JSON.stringify([authoritySubpath], null, 2),
"utf-8",
);
fs.writeFileSync(sourceAuthorityPath, "export const authority = true;\n", "utf-8");
fs.writeFileSync(distAuthorityPath, "export const authority = true;\n", "utf-8");
const sourceCodexEntry = writePluginEntry(
fixture.root,
bundledPluginFile("codex", "src/index.ts"),
);
const sourceCopilotEntry = writePluginEntry(
fixture.root,
bundledPluginFile("copilot", "src/index.ts"),
);
const sourceOtherEntry = writePluginEntry(
fixture.root,
bundledPluginFile("demo", "src/index.ts"),
);
const { packageRoot: installedCodexRoot, pluginEntry: installedCodexEntry } =
writeInstalledPluginEntry({
installRoot: path.join(makeTempDir(), ".openclaw", "npm"),
packageName: "@openclaw/codex",
});
const { packageRoot: installedCopilotRoot, pluginEntry: installedCopilotEntry } =
writeInstalledPluginEntry({
installRoot: path.join(makeTempDir(), ".openclaw", "npm"),
packageName: "@openclaw/copilot",
});
const { packageRoot: installedOtherRoot, pluginEntry: installedOtherEntry } =
writeInstalledPluginEntry({
installRoot: path.join(makeTempDir(), ".openclaw", "npm"),
packageName: "@openclaw/demo",
});
const sourceCodexAliases = buildPluginLoaderAliasMap(sourceCodexEntry);
const sourceCopilotAliases = buildPluginLoaderAliasMap(sourceCopilotEntry);
const sourceOtherAliases = buildPluginLoaderAliasMap(sourceOtherEntry);
const installedCodexAliases = withCwd(installedCodexRoot, () =>
buildPluginLoaderAliasMap(
installedCodexEntry,
path.join(fixture.root, "openclaw.mjs"),
undefined,
"dist",
),
);
const installedCopilotAliases = withCwd(installedCopilotRoot, () =>
buildPluginLoaderAliasMap(
installedCopilotEntry,
path.join(fixture.root, "openclaw.mjs"),
undefined,
"dist",
),
);
const installedOtherAliases = withCwd(installedOtherRoot, () =>
buildPluginLoaderAliasMap(
installedOtherEntry,
path.join(fixture.root, "openclaw.mjs"),
undefined,
"dist",
),
);
const specifier = `openclaw/plugin-sdk/${authoritySubpath}`;
expect(fs.realpathSync(sourceCodexAliases[specifier] ?? "")).toBe(
fs.realpathSync(sourceAuthorityPath),
);
expect(fs.realpathSync(sourceCopilotAliases[specifier] ?? "")).toBe(
fs.realpathSync(sourceAuthorityPath),
);
expect(fs.realpathSync(installedCodexAliases[specifier] ?? "")).toBe(
fs.realpathSync(distAuthorityPath),
);
expect(fs.realpathSync(installedCopilotAliases[specifier] ?? "")).toBe(
fs.realpathSync(distAuthorityPath),
);
expect(sourceOtherAliases[specifier]).toBeUndefined();
expect(installedOtherAliases[specifier]).toBeUndefined();
});
it("does not reuse a non-private cached subpath list after private qa gets enabled", () => {
const fixture = createPluginSdkAliasFixture({
packageExports: {
+8
View File
@@ -419,6 +419,7 @@ const cachedWorkspacePackageAliasMaps = new PluginLruCache<Record<string, string
const PLUGIN_SDK_PACKAGE_NAMES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const;
const CODEX_MCP_PROJECTION_PLUGIN_SDK_SUBPATH = "codex-mcp-projection";
const CODEX_SESSION_TRANSCRIPT_PLUGIN_SDK_SUBPATH = "codex-session-transcript-runtime";
const AGENT_HARNESS_TOOL_AUTHORITY_PLUGIN_SDK_SUBPATH = "agent-harness-tool-authority-runtime";
const OLLAMA_CONFIGURED_LOCAL_ORIGIN_RUNTIME_PLUGIN_SDK_SUBPATH = "ssrf-runtime-internal";
const PRIVATE_QA_ONLY_PLUGIN_SDK_SUBPATHS = new Set([
"agent-runtime-test-contracts",
@@ -457,10 +458,17 @@ const PRIVATE_PLUGIN_SDK_SUBPATH_OWNERS: readonly PrivatePluginSdkSubpathOwner[]
officialInstalledPackageName: "@openclaw/codex",
allowPrivateQaCli: true,
subpaths: [
AGENT_HARNESS_TOOL_AUTHORITY_PLUGIN_SDK_SUBPATH,
CODEX_MCP_PROJECTION_PLUGIN_SDK_SUBPATH,
CODEX_SESSION_TRANSCRIPT_PLUGIN_SDK_SUBPATH,
],
},
{
bundledPluginId: "copilot",
officialInstalledPackageName: "@openclaw/copilot",
allowPrivateQaCli: false,
subpaths: [AGENT_HARNESS_TOOL_AUTHORITY_PLUGIN_SDK_SUBPATH],
},
{
bundledPluginId: "ollama",
allowPrivateQaCli: false,