diff --git a/extensions/codex/harness.ts b/extensions/codex/harness.ts index 8353e8229d9e..22d7d7b7da7c 100644 --- a/extensions/codex/harness.ts +++ b/extensions/codex/harness.ts @@ -33,6 +33,11 @@ type CodexAppServerAgentHarness = AgentHarness & { ): Promise; }; +type CodexAgentHarnessToolAuthority = { + createForAttempt: (typeof import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime"))["createOpenClawCodingToolsForAgentHarness"]; + createForSideQuestion: (typeof import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime"))["createOpenClawCodingToolsForAgentHarnessSideQuestion"]; +}; + async function disposeSharedCodexAppServerClients(): Promise { const dispose = ( globalThis as typeof globalThis & { @@ -56,6 +61,7 @@ export function createCodexAppServerAgentHarness(options: { runtime?: PluginRuntime; bindingStore: CodexAppServerBindingStore; sessionCatalogControl?: CodexSessionCatalogControl; + toolAuthority?: CodexAgentHarnessToolAuthority; }): AgentHarness { const harnessRuntimeId = options?.id ?? "codex"; const normalizedHarnessRuntimeId = harnessRuntimeId.trim().toLowerCase(); @@ -180,6 +186,7 @@ export function createCodexAppServerAgentHarness(options: { // cold provider catalog reads do not pull in the whole Codex runtime. const { runCodexAppServerAttempt } = await import("./src/app-server/run-attempt.js"); return runCodexAppServerAttempt(params, { + agentHarnessCodingToolsFactory: options.toolAuthority?.createForAttempt, bindingStore: options.bindingStore, pluginConfig: options?.resolvePluginConfig?.() ?? options?.pluginConfig, nativeHookRelay: { enabled: true }, @@ -221,6 +228,7 @@ export function createCodexAppServerAgentHarness(options: { runSideQuestion: async (params) => { const { runCodexAppServerSideQuestion } = await import("./src/app-server/side-question.js"); return runCodexAppServerSideQuestion(params, { + agentHarnessCodingToolsFactory: options.toolAuthority?.createForSideQuestion, bindingStore: options.bindingStore, pluginConfig: options?.resolvePluginConfig?.() ?? options?.pluginConfig, nativeHookRelay: { enabled: true }, diff --git a/extensions/codex/index.test.ts b/extensions/codex/index.test.ts index c70591e2e1b5..f64b287dee77 100644 --- a/extensions/codex/index.test.ts +++ b/extensions/codex/index.test.ts @@ -852,6 +852,7 @@ describe("codex plugin", () => { expect(runCodexAppServerAttemptMock).toHaveBeenCalledWith( { prompt: "hello" }, { + agentHarnessCodingToolsFactory: undefined, bindingStore: testCodexAppServerBindingStore, pluginConfig: { appServer: {} }, nativeHookRelay: { enabled: true }, @@ -917,6 +918,7 @@ describe("codex plugin", () => { expect(runCodexAppServerAttemptMock).toHaveBeenCalledWith( { prompt: "calendar" }, { + agentHarnessCodingToolsFactory: expect.any(Function), bindingStore: expect.any(Object), pluginConfig: liveConfig.plugins.entries.codex.config, nativeHookRelay: { enabled: true }, diff --git a/extensions/codex/index.ts b/extensions/codex/index.ts index a02e915f02f8..5f34088748a6 100644 --- a/extensions/codex/index.ts +++ b/extensions/codex/index.ts @@ -2,6 +2,10 @@ * Bundled Codex plugin entry: app-server harness, media understanding, * migration provider, CLI-session commands, and binding hooks. */ +import { + createOpenClawCodingToolsForAgentHarness, + createOpenClawCodingToolsForAgentHarnessSideQuestion, +} from "openclaw/plugin-sdk/agent-harness-tool-authority-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation"; import { @@ -166,6 +170,10 @@ export default definePluginEntry({ createCodexAppServerAgentHarness({ bindingStore, sessionCatalogControl, + toolAuthority: { + createForAttempt: createOpenClawCodingToolsForAgentHarness, + createForSideQuestion: createOpenClawCodingToolsForAgentHarnessSideQuestion, + }, resolveConfig: resolveCurrentConfig, resolvePluginConfig: resolveCurrentPluginConfig, runtime: api.runtime, diff --git a/extensions/codex/src/app-server/dynamic-tool-build-state.ts b/extensions/codex/src/app-server/dynamic-tool-build-state.ts index 7c8b16b95268..cab879267418 100644 --- a/extensions/codex/src/app-server/dynamic-tool-build-state.ts +++ b/extensions/codex/src/app-server/dynamic-tool-build-state.ts @@ -1,6 +1,6 @@ type OpenClawCodingToolsFactory = (typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"]; -type AgentHarnessCodingToolsFactory = +export type AgentHarnessCodingToolsFactory = (typeof import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime"))["createOpenClawCodingToolsForAgentHarness"]; /** Mutable dependency seam shared by dynamic-tool construction and its behavioral tests. */ diff --git a/extensions/codex/src/app-server/dynamic-tool-build.lazy-import.test.ts b/extensions/codex/src/app-server/dynamic-tool-build.lazy-import.test.ts index 05a6da6f85dd..d007c70089dc 100644 --- a/extensions/codex/src/app-server/dynamic-tool-build.lazy-import.test.ts +++ b/extensions/codex/src/app-server/dynamic-tool-build.lazy-import.test.ts @@ -1,33 +1,21 @@ -import { afterEach, expect, it, vi } from "vitest"; +import { expect, it, vi } from "vitest"; +import { buildDynamicTools } from "./dynamic-tool-build.js"; -afterEach(() => { - 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-authority-runtime", () => { - toolBuilderImports += 1; - return { - createOpenClawCodingToolsForAgentHarness: vi.fn(() => []), - }; - }); - - const { buildDynamicTools } = await import("./dynamic-tool-build.js"); - await import("./side-question.js"); - expect(toolBuilderImports).toBe(0); +it("does not invoke host tool authority before a tool-capable turn", async () => { + const agentHarnessCodingToolsFactory = vi.fn(async () => []); await expect( buildDynamicTools({ + agentHarnessCodingToolsFactory, attributionAttempt: {} as never, params: { disableTools: true } as never, } as never), ).resolves.toEqual([]); - expect(toolBuilderImports).toBe(0); + expect(agentHarnessCodingToolsFactory).not.toHaveBeenCalled(); await expect( buildDynamicTools({ + agentHarnessCodingToolsFactory, attributionAttempt: {} as never, params: { disableTools: false, @@ -35,5 +23,5 @@ it("does not load the private tool builder before a tool-capable turn", async () } as never, } as never), ).resolves.toEqual([]); - expect(toolBuilderImports).toBe(0); + expect(agentHarnessCodingToolsFactory).not.toHaveBeenCalled(); }); diff --git a/extensions/codex/src/app-server/dynamic-tool-build.test.ts b/extensions/codex/src/app-server/dynamic-tool-build.test.ts index 2111ab7aa9e6..9b2fc09cd4a5 100644 --- a/extensions/codex/src/app-server/dynamic-tool-build.test.ts +++ b/extensions/codex/src/app-server/dynamic-tool-build.test.ts @@ -165,7 +165,7 @@ describe("Codex app-server dynamic tool build", () => { admittedAttempt.runtimePlan = createCodexRuntimePlanFixture(); const runtimeParams = { ...admittedAttempt }; const factory = vi.fn>( - () => [], + async () => [], ); dynamicToolBuildState.agentHarnessCodingToolsFactory = factory; diff --git a/extensions/codex/src/app-server/dynamic-tool-build.ts b/extensions/codex/src/app-server/dynamic-tool-build.ts index 67854e6d83d9..c69cec7a2459 100644 --- a/extensions/codex/src/app-server/dynamic-tool-build.ts +++ b/extensions/codex/src/app-server/dynamic-tool-build.ts @@ -21,7 +21,10 @@ import { import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime"; import { isToolAllowed } from "openclaw/plugin-sdk/sandbox"; import { readCodexPluginConfig, type CodexPluginConfig } from "./config.js"; -import { dynamicToolBuildState } from "./dynamic-tool-build-state.js"; +import { + dynamicToolBuildState, + type AgentHarnessCodingToolsFactory, +} from "./dynamic-tool-build-state.js"; import { filterCodexDynamicTools, filterCodexDynamicToolsWithOpenClawShell, @@ -83,6 +86,7 @@ function preserveRingZeroSystemAgentTool (agentHarnessModule ??= await import("openclaw/plugin-sdk/agent-harness")); - const agentHarnessCodingToolsFactory = injectedOpenClawCodingToolsFactory - ? undefined - : (dynamicToolBuildState.agentHarnessCodingToolsFactory ?? - (await import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime")) - .createOpenClawCodingToolsForAgentHarness); + const agentHarnessCodingToolsFactory = + input.agentHarnessCodingToolsFactory ?? dynamicToolBuildState.agentHarnessCodingToolsFactory; + if (!injectedOpenClawCodingToolsFactory && !agentHarnessCodingToolsFactory) { + throw new Error("[codex] host tool authority is unavailable"); + } const createOpenClawCodingTools = injectedOpenClawCodingToolsFactory ?? ((options: OpenClawCodingToolsOptions) => @@ -244,7 +248,7 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) { toolBuildStages.mark("load-agent-harness-tools"); const sessionKeys = resolveOpenClawCodingToolsSessionKeys(params, input.sandboxSessionKey); const nativeExecutionPolicy = resolveCodexNativeExecutionPolicyForDynamicTools(input); - const allTools = createOpenClawCodingTools({ + const allTools = await createOpenClawCodingTools({ agentId: input.sessionAgentId, ...buildEmbeddedAttemptToolRunContext(params), exec: { diff --git a/extensions/codex/src/app-server/run-attempt-test-harness.ts b/extensions/codex/src/app-server/run-attempt-test-harness.ts index 591e3559f6d8..a6fe9554f5dc 100644 --- a/extensions/codex/src/app-server/run-attempt-test-harness.ts +++ b/extensions/codex/src/app-server/run-attempt-test-harness.ts @@ -159,6 +159,12 @@ export function runCodexAppServerAttempt( }; const promise = runCodexAppServerAttemptImpl(trackedParams, { ...options, + agentHarnessCodingToolsFactory: + options.agentHarnessCodingToolsFactory ?? + (async (_attempt, toolOptions) => { + const factory = dynamicToolBuildState.openClawCodingToolsFactory; + return factory ? await factory(toolOptions) : []; + }), bindingStore: options.bindingStore ?? testCodexAppServerBindingStore, ...(clientFactory ? { clientFactory } : {}), }).finally(() => { diff --git a/extensions/codex/src/app-server/run-attempt-tool-setup.ts b/extensions/codex/src/app-server/run-attempt-tool-setup.ts index 51e40d0c740b..1d5dd5c7717a 100644 --- a/extensions/codex/src/app-server/run-attempt-tool-setup.ts +++ b/extensions/codex/src/app-server/run-attempt-tool-setup.ts @@ -109,6 +109,7 @@ export async function prepareCodexAttemptTools(runtime: CodexAttemptRuntime) { frameImageIdentity?: string; } = { value: 0 }; const commonToolParams = { + agentHarnessCodingToolsFactory: connection.options.agentHarnessCodingToolsFactory, // Private attribution is bound to the exact host-admitted object. Codex // runtime/tool clones carry behavior only and must not replace this owner. attributionAttempt: params, diff --git a/extensions/codex/src/app-server/run-attempt-types.ts b/extensions/codex/src/app-server/run-attempt-types.ts index 652c11c19538..6dfab1810b60 100644 --- a/extensions/codex/src/app-server/run-attempt-types.ts +++ b/extensions/codex/src/app-server/run-attempt-types.ts @@ -6,6 +6,7 @@ import type { CodexAppServerBindingStore } from "./session-binding.js"; import type { CodexAppServerClientFactory } from "./shared-client.js"; export type CodexRunAttemptOptions = { + agentHarnessCodingToolsFactory?: (typeof import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime"))["createOpenClawCodingToolsForAgentHarness"]; bindingStore: CodexAppServerBindingStore; pluginConfig?: unknown; startupTimeoutFloorMs?: number; diff --git a/extensions/codex/src/app-server/side-question.test.ts b/extensions/codex/src/app-server/side-question.test.ts index 1c277643f58f..9391176b2358 100644 --- a/extensions/codex/src/app-server/side-question.test.ts +++ b/extensions/codex/src/app-server/side-question.test.ts @@ -115,9 +115,17 @@ const bindingStore: CodexAppServerBindingStore = { function runCodexAppServerSideQuestion( params: Parameters[0], - options: Omit[1], "bindingStore"> = {}, + options: Omit< + Parameters[1], + "agentHarnessCodingToolsFactory" | "bindingStore" + > = {}, ) { - return runCodexAppServerSideQuestionImpl(params, { ...options, bindingStore }); + return runCodexAppServerSideQuestionImpl(params, { + ...options, + agentHarnessCodingToolsFactory: (attributionParams, toolOptions) => + createOpenClawCodingToolsForSideQuestionMock(attributionParams, toolOptions), + bindingStore, + }); } function createFakeClient() { diff --git a/extensions/codex/src/app-server/side-question.ts b/extensions/codex/src/app-server/side-question.ts index 7345a8e9a7df..b7555ee0eee9 100644 --- a/extensions/codex/src/app-server/side-question.ts +++ b/extensions/codex/src/app-server/side-question.ts @@ -161,6 +161,7 @@ Do not modify files, source, git state, permissions, configuration, workspace st export async function runCodexAppServerSideQuestion( params: AgentHarnessSideQuestionParams, options: { + agentHarnessCodingToolsFactory?: (typeof import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime"))["createOpenClawCodingToolsForAgentHarnessSideQuestion"]; bindingStore: CodexAppServerBindingStore; pluginConfig?: unknown; nativeHookRelay?: { @@ -413,6 +414,7 @@ export async function runCodexAppServerSideQuestion( }) : "unsupported"; const { toolBridge, webSearchPlan } = await createCodexSideToolBridge({ + agentHarnessCodingToolsFactory: options.agentHarnessCodingToolsFactory, attributionParams: params, params: effectiveParams, cwd, @@ -921,6 +923,7 @@ function buildSideRunAttemptParams( } async function createCodexSideToolBridge(input: { + agentHarnessCodingToolsFactory?: (typeof import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime"))["createOpenClawCodingToolsForAgentHarnessSideQuestion"]; attributionParams: AgentHarnessSideQuestionParams; params: AgentHarnessSideQuestionParams; cwd: string; @@ -937,8 +940,9 @@ async function createCodexSideToolBridge(input: { const messageToolProvider = resolveCodexMessageToolProvider(input.params); let tools: AnyAgentTool[] = []; if (supportsModelTools(runtimeModel)) { - const { createOpenClawCodingToolsForAgentHarnessSideQuestion } = - await import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime"); + if (!input.agentHarnessCodingToolsFactory) { + throw new Error("[codex] host side-question tool authority is unavailable"); + } const sandboxSessionKey = input.params.sandboxSessionKey?.trim() || input.params.sessionKey?.trim() || @@ -949,7 +953,7 @@ async function createCodexSideToolBridge(input: { sessionKey: sandboxSessionKey, workspaceDir: input.cwd, }); - const allTools = createOpenClawCodingToolsForAgentHarnessSideQuestion(input.attributionParams, { + const allTools = await input.agentHarnessCodingToolsFactory(input.attributionParams, { agentId: input.sessionAgentId, sessionKey: sandboxSessionKey, runSessionKey: diff --git a/extensions/copilot/harness.ts b/extensions/copilot/harness.ts index be00320f0dd2..c3a639e6d977 100644 --- a/extensions/copilot/harness.ts +++ b/extensions/copilot/harness.ts @@ -39,6 +39,7 @@ type AgentHarnessIsolatedCompletionResult = Awaited = new Set(["github-copilot"]); interface CreateCopilotAgentHarnessOptions { + agentHarnessCodingToolsFactory?: (typeof import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime"))["createOpenClawCodingToolsForAgentHarness"]; id?: string; label?: string; pluginConfig?: unknown; @@ -691,7 +692,10 @@ export function createCopilotAgentHarness( // uncaught harness rejection. Finalization cannot safely create a new // incompatible session and therefore keeps the failure closed. if (operation === "attempt" && isCopilotByokUnsupportedProviderError(error)) { - return runCopilotAttempt(params, { pool }); + return runCopilotAttempt(params, { + pool, + createOpenClawCodingToolsForAgentHarness: options?.agentHarnessCodingToolsFactory, + }); } throw error; } @@ -765,6 +769,7 @@ export function createCopilotAgentHarness( const result = await runCopilotAttempt(effectiveParams, { pool, + createOpenClawCodingToolsForAgentHarness: options?.agentHarnessCodingToolsFactory, ...(operation === "settled-tool-finalization" ? { operation } : {}), onSessionEstablished: operation === "attempt" && openclawSessionId diff --git a/extensions/copilot/index.ts b/extensions/copilot/index.ts index b99947aad1dd..0616a384da47 100644 --- a/extensions/copilot/index.ts +++ b/extensions/copilot/index.ts @@ -1,4 +1,5 @@ // Copilot plugin entrypoint registers its OpenClaw integration. +import { createOpenClawCodingToolsForAgentHarness } from "openclaw/plugin-sdk/agent-harness-tool-authority-runtime"; import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { createCopilotAgentHarness, type CopilotSessionBinding } from "./harness.js"; @@ -42,6 +43,7 @@ export default definePluginEntry({ api.registerAgentHarness( createCopilotAgentHarness({ + agentHarnessCodingToolsFactory: createOpenClawCodingToolsForAgentHarness, ...(poolOptions ? { poolOptions } : {}), sessionStore, }), diff --git a/extensions/copilot/src/attempt-execution.ts b/extensions/copilot/src/attempt-execution.ts index eeb1f00d8823..b0a87e3c70bf 100644 --- a/extensions/copilot/src/attempt-execution.ts +++ b/extensions/copilot/src/attempt-execution.ts @@ -260,6 +260,7 @@ export async function runCopilotExecution(context: { if (!settledToolFinalization) { try { const toolBridge = await createToolBridge({ + agentHarnessCodingToolsFactory: deps.createOpenClawCodingToolsForAgentHarness, allowModelTools: poolAcquire.provider.mode === "byok", modelProvider: modelRef.provider, modelId: modelRef.id, diff --git a/extensions/copilot/src/attempt-types.ts b/extensions/copilot/src/attempt-types.ts index 56ef1c5492f3..af477d783d73 100644 --- a/extensions/copilot/src/attempt-types.ts +++ b/extensions/copilot/src/attempt-types.ts @@ -12,7 +12,7 @@ import type { OnAssistantDeltaPayload } from "./event-bridge.js"; import type { CopilotHooksConfig } from "./hooks-bridge.js"; import type { CopilotPermissionPolicy } from "./permission-bridge.js"; import type { CopilotClientPool, PooledClient } from "./runtime.js"; -import type { createCopilotToolBridge } from "./tool-bridge.js"; +import type { AgentHarnessCodingToolsFactory, createCopilotToolBridge } from "./tool-bridge.js"; export const BACKGROUND_COMPACTION_CANCEL_TIMEOUT_MS = 5_000; export const COPILOT_ASK_USER_AVAILABLE_TOOLS = ["builtin:ask_user"] as const; export const COPILOT_SETTLED_FINALIZATION_SYSTEM_MESSAGE = @@ -144,6 +144,7 @@ export interface CopilotAttemptDeps { operation?: CopilotAttemptOperation; now?: () => number; createToolBridge?: typeof createCopilotToolBridge; + createOpenClawCodingToolsForAgentHarness?: AgentHarnessCodingToolsFactory; isHostScopedToolActive?: (toolName: string) => boolean; resolveSandboxContextOverride?: ResolveSandboxContextFn; onSessionEstablished?: (info: { diff --git a/extensions/copilot/src/attempt.test.ts b/extensions/copilot/src/attempt.test.ts index 7ef38db01d7f..aa4b417faa8b 100644 --- a/extensions/copilot/src/attempt.test.ts +++ b/extensions/copilot/src/attempt.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import type { CopilotClient, Tool as SdkTool } from "@github/copilot-sdk"; import { expectDefined } from "@openclaw/normalization-core"; +import { createOpenClawCodingTools } from "openclaw/plugin-sdk/agent-harness"; import { abortAgentHarnessRun, attachModelProviderRequestTransport, @@ -552,7 +553,11 @@ describe("runCopilotAttempt", () => { disableTools: false, config: { tools: { codeMode: true } }, } as never), - { pool: makeFakePool(sdk) }, + { + createOpenClawCodingToolsForAgentHarness: async (_attempt, options) => + createOpenClawCodingTools(options), + pool: makeFakePool(sdk), + }, ); expect(result.codeModeEngaged).toBe(true); diff --git a/extensions/copilot/src/tool-bridge.lazy-import.test.ts b/extensions/copilot/src/tool-bridge.lazy-import.test.ts index 79df4c88c18e..eac47d413c2e 100644 --- a/extensions/copilot/src/tool-bridge.lazy-import.test.ts +++ b/extensions/copilot/src/tool-bridge.lazy-import.test.ts @@ -1,24 +1,12 @@ -import { afterEach, expect, it, vi } from "vitest"; +import { expect, it, vi } from "vitest"; +import { createCopilotToolBridge } from "./tool-bridge.js"; -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); +it("invokes host tool authority only after the tool-construction guard", async () => { + const agentHarnessCodingToolsFactory = vi.fn(async () => []); await expect( createCopilotToolBridge({ + agentHarnessCodingToolsFactory, admittedAttempt: {} as never, agentId: "agent-1", attemptParams: { disableTools: true } as never, @@ -27,10 +15,11 @@ it("loads the private tool builder only after the tool-construction guard", asyn sessionId: "session-1", }), ).resolves.toEqual({ codeModeEngaged: false, sdkTools: [], sourceTools: [] }); - expect(toolBuilderImports).toBe(0); + expect(agentHarnessCodingToolsFactory).not.toHaveBeenCalled(); await expect( createCopilotToolBridge({ + agentHarnessCodingToolsFactory, admittedAttempt: {} as never, agentId: "agent-1", attemptParams: {} as never, @@ -39,5 +28,5 @@ it("loads the private tool builder only after the tool-construction guard", asyn sessionId: "session-1", }), ).resolves.toMatchObject({ sdkTools: [], sourceTools: [] }); - expect(toolBuilderImports).toBe(1); + expect(agentHarnessCodingToolsFactory).toHaveBeenCalledOnce(); }); diff --git a/extensions/copilot/src/tool-bridge.ts b/extensions/copilot/src/tool-bridge.ts index a6b0608c625b..24068e3ca4e9 100644 --- a/extensions/copilot/src/tool-bridge.ts +++ b/extensions/copilot/src/tool-bridge.ts @@ -27,6 +27,8 @@ import { createAgentHarnessToolSurfaceRuntime } from "openclaw/plugin-sdk/agent- type CreateOpenClawCodingTools = (typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"]; type OpenClawCodingToolsOptions = NonNullable[0]>; +export type AgentHarnessCodingToolsFactory = + (typeof import("openclaw/plugin-sdk/agent-harness-tool-authority-runtime"))["createOpenClawCodingToolsForAgentHarness"]; type AgentHarnessToolSurfaceRuntime = ReturnType; type CatalogExecuteParams = Parameters< NonNullable @@ -71,6 +73,7 @@ type CopilotToolCompletion = { }; interface CopilotToolBridgeInput { + agentHarnessCodingToolsFactory?: AgentHarnessCodingToolsFactory; allowModelTools?: boolean; /** Invalidates screenshot-bound computer actions after context compaction. */ computerContextEpoch?: { @@ -136,7 +139,9 @@ interface CopilotToolBridgeInput { onYieldDetected?: (message?: string) => void; onToolCompleted?: (completion: CopilotToolCompletion) => void | Promise; observeToolTerminal?: CopilotToolTerminalObserver; - createOpenClawCodingTools?: (opts: unknown) => AnyAgentTool[] | Promise; + createOpenClawCodingTools?: ( + options: OpenClawCodingToolsOptions, + ) => AnyAgentTool[] | Promise; beforeExecute?: (ctx: { toolName: string; toolCallId: string; @@ -198,15 +203,16 @@ export async function createCopilotToolBridge( } const admittedAttempt = input.admittedAttempt; - const createOpenClawCodingTools = - input.createOpenClawCodingTools ?? - (admittedAttempt - ? 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); + let createOpenClawCodingTools = input.createOpenClawCodingTools; + if (!createOpenClawCodingTools && admittedAttempt) { + if (!input.agentHarnessCodingToolsFactory) { + throw new Error("[copilot-tool-bridge] host tool authority is unavailable"); + } + createOpenClawCodingTools = (options: OpenClawCodingToolsOptions) => + input.agentHarnessCodingToolsFactory!(admittedAttempt, options); + } + createOpenClawCodingTools ??= (await import("openclaw/plugin-sdk/agent-harness")) + .createOpenClawCodingTools; const toolSurfaceRuntime = createAgentHarnessToolSurfaceRuntime({ abortSignal: input.abortSignal, diff --git a/src/plugin-sdk/agent-harness-tool-authority-runtime.ts b/src/plugin-sdk/agent-harness-tool-authority-runtime.ts index ca75062dffc1..0eadcdc817c5 100644 --- a/src/plugin-sdk/agent-harness-tool-authority-runtime.ts +++ b/src/plugin-sdk/agent-harness-tool-authority-runtime.ts @@ -1,7 +1,3 @@ -import { - createOpenClawCodingToolsForAgentHarness as createCoreOpenClawCodingToolsForAgentHarness, - createOpenClawCodingToolsForAgentHarnessSideQuestion as createCoreOpenClawCodingToolsForAgentHarnessSideQuestion, -} from "../agents/agent-tools-internal.js"; import type { AgentHarnessSideQuestionParams, EmbeddedRunAttemptParams, @@ -15,10 +11,16 @@ type OpenClawCodingToolsOptions = NonNullable< * Build tools for the exact host-admitted attempt without exposing its private * execution attribution to plugin code. */ -export function createOpenClawCodingToolsForAgentHarness( +export async function createOpenClawCodingToolsForAgentHarness( attempt: EmbeddedRunAttemptParams, options?: OpenClawCodingToolsOptions, -): ReturnType { +): Promise< + ReturnType< + typeof import("../agents/agent-tools-internal.js").createOpenClawCodingToolsForAgentHarness + > +> { + const { createOpenClawCodingToolsForAgentHarness: createCoreOpenClawCodingToolsForAgentHarness } = + await import("../agents/agent-tools-internal.js"); return createCoreOpenClawCodingToolsForAgentHarness(attempt, options); } @@ -26,9 +28,17 @@ export function createOpenClawCodingToolsForAgentHarness( * Build tools for the exact host-admitted side-question request without * exposing its private execution attribution to plugin code. */ -export function createOpenClawCodingToolsForAgentHarnessSideQuestion( +export async function createOpenClawCodingToolsForAgentHarnessSideQuestion( params: AgentHarnessSideQuestionParams, options?: OpenClawCodingToolsOptions, -): ReturnType { +): Promise< + ReturnType< + typeof import("../agents/agent-tools-internal.js").createOpenClawCodingToolsForAgentHarnessSideQuestion + > +> { + const { + createOpenClawCodingToolsForAgentHarnessSideQuestion: + createCoreOpenClawCodingToolsForAgentHarnessSideQuestion, + } = await import("../agents/agent-tools-internal.js"); return createCoreOpenClawCodingToolsForAgentHarnessSideQuestion(params, options); } diff --git a/src/plugins/plugin-sdk-native-resolver.test.ts b/src/plugins/plugin-sdk-native-resolver.test.ts index afb6f39f5de1..754bdd840a79 100644 --- a/src/plugins/plugin-sdk-native-resolver.test.ts +++ b/src/plugins/plugin-sdk-native-resolver.test.ts @@ -1,7 +1,7 @@ // Verifies native plugin SDK resolver behavior and import aliases. import { spawnSync } from "node:child_process"; import fs from "node:fs"; -import { createRequire } from "node:module"; +import Module, { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; @@ -637,6 +637,37 @@ describe("installOpenClawPluginSdkNativeResolver", () => { expect(() => requireFromPlugin.resolve("openclaw/plugin-sdk/source-only")).toThrow(); }); + it("keeps harness tool authority out of the forgeable native resolver", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-authority-")); + const { loaderModulePath } = writeFakeOpenClawPackage(root); + addFakePluginSdkDistExport(root, "agent-harness-tool-authority-runtime"); + const installedCodexEntry = writeExternalPluginEntry( + fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-installed-codex-")), + ); + + const installedAliases = installOpenClawPluginSdkNativeResolver({ + modulePath: loaderModulePath, + pluginModulePath: installedCodexEntry, + pluginSdkResolution: "dist", + trustedInstalledPrivateSdkOwner: "codex", + }); + + const authoritySpecifier = "openclaw/plugin-sdk/agent-harness-tool-authority-runtime"; + expect(installedAliases).not.toContain(authoritySpecifier); + const resolveFilename = ( + Module as unknown as { + _resolveFilename: (request: string, parent: NodeJS.Module, isMain: boolean) => string; + } + )._resolveFilename; + expect(() => + resolveFilename( + authoritySpecifier, + { filename: installedCodexEntry } as NodeJS.Module, + false, + ), + ).toThrow(); + }); + it("scopes private SSRF SDK aliases to bundled local IPC native parents", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-ssrf-")); const { loaderModulePath } = writeFakeOpenClawPackage(root); diff --git a/src/plugins/plugin-sdk-native-resolver.ts b/src/plugins/plugin-sdk-native-resolver.ts index 985c2f3eb24f..6bc7fe2a10c3 100644 --- a/src/plugins/plugin-sdk-native-resolver.ts +++ b/src/plugins/plugin-sdk-native-resolver.ts @@ -311,6 +311,13 @@ function listPluginSdkNativeAliases( } const aliases = Object.entries(aliasMap) .filter(([specifier]) => isPluginSdkAliasSpecifier(specifier)) + // This capability is injected through the owner-scoped plugin loader. + // A process-global resolver cannot authenticate caller-supplied parent metadata. + .filter( + ([specifier]) => + specifier !== "openclaw/plugin-sdk/agent-harness-tool-authority-runtime" && + specifier !== "openclaw/plugin-sdk/agent-harness-tool-authority-runtime.js", + ) .filter(([, target]) => isNativeLoadableSdkTarget(target)) .flatMap(([specifier, target]) => { if (specifier.endsWith(".js")) {