feat(gateway): add SDK-facing tools.invoke RPC

Adds the SDK-facing tools.invoke Gateway RPC for #74705.

Reuses the /tools/invoke policy path for tool policy, deny-list, owner filtering, before-tool-call hooks, session/agent scoping, and plugin approval handling. Returns typed SDK approval/refusal/success results while preserving HTTP compatibility and uses idempotencyKey as the stable tool-call id.

Includes protocol schema exports, method scope/list registration, SDK helper/types, docs, generated Swift models, tests, and changelog credit.
This commit is contained in:
NVIDIAN
2026-05-01 01:16:53 -07:00
committed by GitHub
parent 37f8c3806a
commit ef0eb12615
24 changed files with 932 additions and 251 deletions

View File

@@ -18,6 +18,8 @@ import type {
SessionCreateParams,
SessionSendParams,
SessionTarget,
ToolInvokeParams,
ToolInvokeResult,
} from "./types.js";
const MAX_REPLAY_RUNS = 100;
@@ -764,10 +766,15 @@ export class ToolsNamespace extends RpcNamespace {
return await this.call("effective", params);
}
async invoke(name: string, params?: unknown): Promise<unknown> {
void name;
void params;
return unsupportedGatewayApi("oc.tools.invoke");
async invoke(name: string, params?: ToolInvokeParams): Promise<ToolInvokeResult> {
return await this.call("invoke", {
name,
...(params?.args ? { args: params.args } : {}),
...(params?.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params?.agentId ? { agentId: params.agentId } : {}),
...(typeof params?.confirm === "boolean" ? { confirm: params.confirm } : {}),
...(params?.idempotencyKey ? { idempotencyKey: params.idempotencyKey } : {}),
});
}
}

View File

@@ -100,6 +100,7 @@ async function createFakeGateway(port = 0): Promise<FakeGateway> {
"sessions.send",
"tools.catalog",
"tools.effective",
"tools.invoke",
],
events: ["agent", "sessions.changed"],
},
@@ -253,6 +254,11 @@ async function createFakeGateway(port = 0): Promise<FakeGateway> {
return;
}
if (frame.method === "tools.invoke") {
reply({ ok: true, toolName: "shell", output: { ok: true } });
return;
}
if (frame.method === "exec.approval.list") {
reply({ approvals: [] });
return;
@@ -414,6 +420,9 @@ describe("OpenClaw SDK websocket e2e", () => {
await expect(oc.tools.effective({ sessionKey: "sdk-session" })).resolves.toMatchObject({
tools: [{ name: "shell", enabled: true }],
});
await expect(
oc.tools.invoke("shell", { args: { command: "pwd" }, sessionKey: "sdk-session" }),
).resolves.toMatchObject({ ok: true, toolName: "shell", output: { ok: true } });
await expect(oc.approvals.list()).resolves.toMatchObject({ approvals: [] });
await expect(
oc.approvals.respond("approval-1", { decision: "approve" }),
@@ -437,6 +446,7 @@ describe("OpenClaw SDK websocket e2e", () => {
"models.authStatus",
"tools.catalog",
"tools.effective",
"tools.invoke",
"exec.approval.list",
"exec.approval.resolve",
]);

View File

@@ -335,9 +335,6 @@ describe("OpenClaw SDK", () => {
await expect(oc.tasks.cancel("task_123")).rejects.toThrow(
"oc.tasks.cancel is not supported by the current OpenClaw Gateway yet",
);
await expect(oc.tools.invoke("demo")).rejects.toThrow(
"oc.tools.invoke is not supported by the current OpenClaw Gateway yet",
);
await expect(oc.environments.list()).rejects.toThrow(
"oc.environments.list is not supported by the current OpenClaw Gateway yet",
);
@@ -353,6 +350,35 @@ describe("OpenClaw SDK", () => {
expect(transport.calls).toEqual([]);
});
it("invokes tools through the Gateway tools.invoke method", async () => {
const transport = new FakeTransport({
"tools.invoke": { ok: true, toolName: "demo", output: { value: 1 }, source: "core" },
});
const oc = new OpenClaw({ transport });
await expect(
oc.tools.invoke("demo", {
args: { mode: "test" },
sessionKey: "agent:main:main",
confirm: false,
idempotencyKey: "tools-invoke-test",
}),
).resolves.toMatchObject({ ok: true, toolName: "demo", output: { value: 1 } });
expect(transport.calls).toEqual([
{
method: "tools.invoke",
params: {
name: "demo",
args: { mode: "test" },
sessionKey: "agent:main:main",
confirm: false,
idempotencyKey: "tools-invoke-test",
},
options: undefined,
},
]);
});
it("cancels runs and checks model auth status through current Gateway methods", async () => {
const transport = new FakeTransport({
agent: { status: "accepted", runId: "run_without_session" },

View File

@@ -42,5 +42,7 @@ export type {
SessionCreateParams,
SessionSendParams,
SessionTarget,
ToolInvokeParams,
ToolInvokeResult,
WorkspaceSelection,
} from "./types.js";

View File

@@ -114,6 +114,24 @@ export type SDKError = {
details?: unknown;
};
export type ToolInvokeParams = {
args?: JsonObject;
sessionKey?: string;
agentId?: string;
confirm?: boolean;
idempotencyKey?: string;
};
export type ToolInvokeResult = {
ok: boolean;
toolName: string;
output?: unknown;
requiresApproval?: boolean;
approvalId?: string;
source?: string;
error?: SDKError;
};
export type RunResult = {
runId: string;
status: RunStatus;