mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:40:43 +00:00
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:
@@ -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 } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
]);
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -42,5 +42,7 @@ export type {
|
||||
SessionCreateParams,
|
||||
SessionSendParams,
|
||||
SessionTarget,
|
||||
ToolInvokeParams,
|
||||
ToolInvokeResult,
|
||||
WorkspaceSelection,
|
||||
} from "./types.js";
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user