Skip to content

Commit d3019e6

Browse files
committed
fix(copilot): tighten harness sdk boundaries
1 parent 21d67b1 commit d3019e6

9 files changed

Lines changed: 36 additions & 15 deletions

File tree

docs/plugins/copilot.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ controls it can enforce at the SDK boundary: `includeCoreTools`, the
325325
runtime tool allowlist, and `toolConstructionPlan`.
326326

327327
The bridge also uses the shared harness tool-surface helper from
328-
`openclaw/plugin-sdk/agent-harness-runtime` for PI parity. When
328+
`openclaw/plugin-sdk/agent-harness-tool-runtime` for PI parity. When
329329
tool-search is enabled, the SDK sees compact control tools plus a hidden
330330
catalog executor instead of every OpenClaw tool schema. When code mode is
331331
enabled, the helper builds the same code-mode control surface and catalog

docs/plugins/sdk-agent-harness.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ helper keeps channel/TUI presentation consistent while each harness keeps its
206206
own protocol parsing and pending-request lifecycle.
207207

208208
Native harnesses that need PI-like compact tool routing should use
209-
`createAgentHarnessToolSurfaceRuntime(...)` from the same SDK subpath. It owns
209+
`createAgentHarnessToolSurfaceRuntime(...)` from
210+
`openclaw/plugin-sdk/agent-harness-tool-runtime`. It owns
210211
tool-search/code-mode control selection, local-model lean defaults,
211212
runtime-compatible schema filtering, hidden catalog execution, directory
212213
hydration, and catalog cleanup. Harnesses still own their SDK-specific tool

extensions/copilot/src/attempt.test.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1910,6 +1910,7 @@ describe("runCopilotAttempt", () => {
19101910
it("retains a timed-out session until later compaction reaches session.idle", async () => {
19111911
const afterCompaction = vi.fn();
19121912
const onDeferredCompaction = vi.fn();
1913+
const cleanupToolBridge = vi.fn();
19131914
initializeGlobalHookRunner(
19141915
createMockPluginRegistry([{ hookName: "after_compaction", handler: afterCompaction }]),
19151916
);
@@ -1922,8 +1923,14 @@ describe("runCopilotAttempt", () => {
19221923
);
19231924
},
19241925
});
1926+
const createToolBridge = vi.fn(async () => ({
1927+
cleanup: cleanupToolBridge,
1928+
sdkTools: [],
1929+
sourceTools: [],
1930+
}));
19251931

19261932
const result = await runCopilotAttempt(makeParams(), {
1933+
createToolBridge,
19271934
onDeferredCompaction,
19281935
pool: makeFakePool(sdk),
19291936
});
@@ -1933,6 +1940,7 @@ describe("runCopilotAttempt", () => {
19331940
expect(onDeferredCompaction).toHaveBeenCalledWith(
19341941
expect.objectContaining({ sdkSessionId: "sess-1" }),
19351942
);
1943+
expect(cleanupToolBridge).not.toHaveBeenCalled();
19361944
expect(activeSession?.disconnect).not.toHaveBeenCalled();
19371945

19381946
activeSession?.emit("session.compaction_start", {});
@@ -1946,6 +1954,7 @@ describe("runCopilotAttempt", () => {
19461954
await vi.waitFor(() => {
19471955
expect(activeSession?.disconnect).toHaveBeenCalledTimes(1);
19481956
});
1957+
expect(cleanupToolBridge).toHaveBeenCalledTimes(1);
19491958
});
19501959

19511960
it("does not mark a timeout after SDK compaction has completed as active compaction", async () => {
@@ -3150,7 +3159,6 @@ describe("runCopilotAttempt", () => {
31503159
expect(readAvailableTools(sdk.createSession.mock.calls[0])).toEqual([
31513160
"read",
31523161
"edit",
3153-
"ask_user",
31543162
"builtin:ask_user",
31553163
]);
31563164
});
@@ -3168,10 +3176,7 @@ describe("runCopilotAttempt", () => {
31683176

31693177
await runCopilotAttempt(makeParams(), { createToolBridge, pool });
31703178

3171-
expect(readAvailableTools(sdk.createSession.mock.calls[0])).toEqual([
3172-
"ask_user",
3173-
"builtin:ask_user",
3174-
]);
3179+
expect(readAvailableTools(sdk.createSession.mock.calls[0])).toEqual(["builtin:ask_user"]);
31753180
});
31763181

31773182
it("forwards the full bridged set when the run is unrestricted (no toolsAllow)", async () => {
@@ -3197,7 +3202,6 @@ describe("runCopilotAttempt", () => {
31973202
"edit",
31983203
"exec",
31993204
"message",
3200-
"ask_user",
32013205
"builtin:ask_user",
32023206
]);
32033207
});
@@ -3224,7 +3228,7 @@ describe("runCopilotAttempt", () => {
32243228

32253229
const resumeCall = sdk.resumeSession.mock.calls[0] as unknown[] | undefined;
32263230
const resumeCfg = resumeCall?.[1] as { availableTools?: string[] };
3227-
expect(resumeCfg?.availableTools).toEqual(["read", "ask_user", "builtin:ask_user"]);
3231+
expect(resumeCfg?.availableTools).toEqual(["read", "builtin:ask_user"]);
32283232
});
32293233

32303234
it("forwards `[]` to resumeSession when the bridge returns no tools", async () => {
@@ -3243,7 +3247,7 @@ describe("runCopilotAttempt", () => {
32433247

32443248
const resumeCall = sdk.resumeSession.mock.calls[0] as unknown[] | undefined;
32453249
const resumeCfg = resumeCall?.[1] as { availableTools?: string[] };
3246-
expect(resumeCfg?.availableTools).toEqual(["ask_user", "builtin:ask_user"]);
3250+
expect(resumeCfg?.availableTools).toEqual(["builtin:ask_user"]);
32473251
});
32483252
});
32493253

extensions/copilot/src/attempt.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import { resolveCopilotWorkspaceBootstrapContext } from "./workspace-bootstrap.j
6262

6363
const SUPPORTED_PROVIDERS = new Set(["github-copilot"]);
6464
const BACKGROUND_COMPACTION_CANCEL_TIMEOUT_MS = 5_000;
65-
const COPILOT_ASK_USER_AVAILABLE_TOOLS = ["ask_user", "builtin:ask_user"] as const;
65+
const COPILOT_ASK_USER_AVAILABLE_TOOLS = ["builtin:ask_user"] as const;
6666

6767
type AttemptResultWithSdkSessionId = AgentHarnessAttemptResult & { sdkSessionId?: string };
6868
type PromptErrorWithCode = Error & { code?: string; cause?: unknown };
@@ -227,6 +227,7 @@ function deferBackgroundCompactionCleanup(params: {
227227
bridge: ReturnType<typeof attachEventBridge>;
228228
handle: PooledClient;
229229
pool: CopilotClientPool;
230+
cleanupToolBridge?: () => void;
230231
sdkSessionId?: string;
231232
session: SessionLike;
232233
timeoutMs: number;
@@ -255,6 +256,7 @@ function deferBackgroundCompactionCleanup(params: {
255256
} catch {
256257
// The attempt has already returned its timeout result.
257258
}
259+
params.cleanupToolBridge?.();
258260
if (outcome !== "completed" && params.sdkSessionId) {
259261
try {
260262
await params.handle.client.deleteSession(params.sdkSessionId);
@@ -852,7 +854,6 @@ export async function runCopilotAttempt(
852854
}
853855
} finally {
854856
settled = true;
855-
cleanupToolBridge?.();
856857
userInputBridgeRef?.cancelPending();
857858
if (activeRunHandleRef) {
858859
clearActiveEmbeddedRun(
@@ -876,6 +877,7 @@ export async function runCopilotAttempt(
876877
abortSignal: cleanupAbort.signal,
877878
awaitSessionIdle: !bridge.hasObservedSessionIdle(),
878879
bridge,
880+
cleanupToolBridge,
879881
handle,
880882
pool: deps.pool,
881883
sdkSessionId,
@@ -904,6 +906,7 @@ export async function runCopilotAttempt(
904906
// defines as no background agents in flight. Timeouts retain the bridge
905907
// until that event so compaction that starts after the timer still completes.
906908
await bridge?.awaitCompactionChain();
909+
cleanupToolBridge?.();
907910
bridge?.detach();
908911
params.abortSignal?.removeEventListener("abort", onAbort);
909912

extensions/copilot/src/tool-bridge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type {
88
import {
99
applyEmbeddedAttemptToolsAllow,
1010
buildEmbeddedAttemptToolRunContext,
11-
createAgentHarnessToolSurfaceRuntime,
1211
extractToolErrorMessage,
1312
getPluginToolMeta,
1413
isSubagentSessionKey,
@@ -18,6 +17,7 @@ import {
1817
resolveModelAuthMode,
1918
sanitizeToolResult,
2019
} from "openclaw/plugin-sdk/agent-harness-runtime";
20+
import { createAgentHarnessToolSurfaceRuntime } from "openclaw/plugin-sdk/agent-harness-tool-runtime";
2121

2222
type CreateOpenClawCodingTools =
2323
(typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"];

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,10 @@
629629
"types": "./dist/plugin-sdk/agent-harness-runtime.d.ts",
630630
"default": "./dist/plugin-sdk/agent-harness-runtime.js"
631631
},
632+
"./plugin-sdk/agent-harness-tool-runtime": {
633+
"types": "./dist/plugin-sdk/agent-harness-tool-runtime.d.ts",
634+
"default": "./dist/plugin-sdk/agent-harness-tool-runtime.js"
635+
},
632636
"./plugin-sdk/hook-runtime": {
633637
"types": "./dist/plugin-sdk/hook-runtime.d.ts",
634638
"default": "./dist/plugin-sdk/hook-runtime.js"

scripts/lib/plugin-sdk-entrypoints.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"agent-harness",
114114
"agent-harness-exec-review-runtime",
115115
"agent-harness-runtime",
116+
"agent-harness-tool-runtime",
116117
"hook-runtime",
117118
"host-runtime",
118119
"types",

src/plugin-sdk/agent-harness-runtime.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export type {
5151
AgentHarnessUserInputPromptOptions,
5252
AgentHarnessUserInputQuestion,
5353
} from "../agents/harness/user-input-bridge.js";
54-
export type { AgentHarnessToolSurfaceRuntime } from "../agents/harness/tool-surface-bridge.js";
5554
export type {
5655
EmbeddedRunAttemptParams,
5756
EmbeddedRunAttemptResult,
@@ -165,7 +164,6 @@ export {
165164
formatAgentHarnessUserInputPrompt,
166165
normalizeAgentHarnessUserInputAnswer,
167166
} from "../agents/harness/user-input-bridge.js";
168-
export { createAgentHarnessToolSurfaceRuntime } from "../agents/harness/tool-surface-bridge.js";
169167
export {
170168
buildSkillWorkshopPromptSection,
171169
SKILL_WORKSHOP_TOOL_NAME,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Focused runtime SDK subpath for native harness tool-surface routing.
3+
*
4+
* Keep tool-search and code-mode dependencies out of the lightweight harness
5+
* lifecycle facade used during plugin startup.
6+
*/
7+
export {
8+
createAgentHarnessToolSurfaceRuntime,
9+
type AgentHarnessToolSurfaceRuntime,
10+
} from "../agents/harness/tool-surface-bridge.js";

0 commit comments

Comments
 (0)