Skip to content

Commit 0efa6ad

Browse files
committed
feat(gateway): add tools.invoke rpc
1 parent bbf932f commit 0efa6ad

26 files changed

Lines changed: 904 additions & 204 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
66

77
### Changes
88

9+
- Gateway/SDK: add a `tools.invoke` Gateway RPC and SDK helper that run direct tool calls through the existing tool policy, Gateway tool allow/deny, and plugin approval pipeline, returning typed approval-required results when callers omit `confirm: true`. Fixes #74705. Thanks @gazeatcode.
910
- Models: suppress explicitly configured openai-codex/gpt-5.4-mini inline entries so a stale models config written by `openclaw doctor --fix` cannot bypass the manifest capability block and cause repeated assistant-turn failures when the runtime switches to that model on ChatGPT-backed Codex accounts. Conditional suppressions (e.g. qwen Coding Plan endpoint guards) remain bypassable by explicit user configuration. (#74451) Thanks @0xCyda, @hclsys, and @Marvae.
1011
- Dependencies: refresh workspace runtime, plugin, and tooling packages, including ACP, Pi, AWS SDK, TypeBox, pnpm, oxlint, oxfmt, jsdom, pdfjs, ciao, and tokenjuice, while keeping patched ACP behavior and lint gates current. Thanks @mariozechner.
1112
- Messages/queue: make `steer` drain all pending Pi steering messages at the next model boundary, keep legacy one-at-a-time steering as `queue`, and add a dedicated steering queue docs page. Thanks @vincentkoc.

apps/macos/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3644,6 +3644,40 @@ public struct ToolsEffectiveResult: Codable, Sendable {
36443644
}
36453645
}
36463646

3647+
public struct ToolsInvokeParams: Codable, Sendable {
3648+
public let name: String
3649+
public let args: [String: AnyCodable]?
3650+
public let sessionkey: String?
3651+
public let agentid: String?
3652+
public let confirm: Bool?
3653+
public let idempotencykey: String?
3654+
3655+
public init(
3656+
name: String,
3657+
args: [String: AnyCodable]?,
3658+
sessionkey: String?,
3659+
agentid: String?,
3660+
confirm: Bool?,
3661+
idempotencykey: String?)
3662+
{
3663+
self.name = name
3664+
self.args = args
3665+
self.sessionkey = sessionkey
3666+
self.agentid = agentid
3667+
self.confirm = confirm
3668+
self.idempotencykey = idempotencykey
3669+
}
3670+
3671+
private enum CodingKeys: String, CodingKey {
3672+
case name
3673+
case args
3674+
case sessionkey = "sessionKey"
3675+
case agentid = "agentId"
3676+
case confirm
3677+
case idempotencykey = "idempotencyKey"
3678+
}
3679+
}
3680+
36473681
public struct SkillsBinsParams: Codable, Sendable {}
36483682

36493683
public struct SkillsBinsResult: Codable, Sendable {

apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3644,6 +3644,40 @@ public struct ToolsEffectiveResult: Codable, Sendable {
36443644
}
36453645
}
36463646

3647+
public struct ToolsInvokeParams: Codable, Sendable {
3648+
public let name: String
3649+
public let args: [String: AnyCodable]?
3650+
public let sessionkey: String?
3651+
public let agentid: String?
3652+
public let confirm: Bool?
3653+
public let idempotencykey: String?
3654+
3655+
public init(
3656+
name: String,
3657+
args: [String: AnyCodable]?,
3658+
sessionkey: String?,
3659+
agentid: String?,
3660+
confirm: Bool?,
3661+
idempotencykey: String?)
3662+
{
3663+
self.name = name
3664+
self.args = args
3665+
self.sessionkey = sessionkey
3666+
self.agentid = agentid
3667+
self.confirm = confirm
3668+
self.idempotencykey = idempotencykey
3669+
}
3670+
3671+
private enum CodingKeys: String, CodingKey {
3672+
case name
3673+
case args
3674+
case sessionkey = "sessionKey"
3675+
case agentid = "agentId"
3676+
case confirm
3677+
case idempotencykey = "idempotencyKey"
3678+
}
3679+
}
3680+
36473681
public struct SkillsBinsParams: Codable, Sendable {}
36483682

36493683
public struct SkillsBinsResult: Codable, Sendable {

docs/concepts/openclaw-sdk.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,19 @@ Tool helpers expose the Gateway catalog and effective tool view:
218218
```typescript
219219
await oc.tools.list();
220220
await oc.tools.effective({ sessionKey: "main" });
221+
await oc.tools.invoke("tool-name", {
222+
args: { path: "README.md" },
223+
sessionKey: "main",
224+
confirm: true,
225+
});
221226
```
222227

228+
`oc.tools.invoke()` calls the Gateway `tools.invoke` RPC. By default, plugin
229+
approval requests are returned as a typed `{ ok: false, requiresApproval: true,
230+
approvalId, error }` result instead of executing through an approval wait.
231+
Pass `confirm: true` when the app wants the Gateway to run the normal plugin
232+
approval wait path before execution.
233+
223234
Approval helpers use the exec approval RPCs:
224235

225236
```typescript
@@ -238,8 +249,6 @@ await oc.tasks.list();
238249
await oc.tasks.get("task-id");
239250
await oc.tasks.cancel("task-id");
240251

241-
await oc.tools.invoke("tool-name", {});
242-
243252
await oc.artifacts.list();
244253
await oc.artifacts.get("artifact-id");
245254
await oc.artifacts.download("artifact-id");

docs/gateway/protocol.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
442442

443443
<Accordion title="Automation, skills, and tools">
444444
- Automation: `wake` schedules an immediate or next-heartbeat wake text injection; `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`, `cron.run`, `cron.runs` manage scheduled work.
445-
- Skills and tools: `commands.list`, `skills.*`, `tools.catalog`, `tools.effective`.
445+
- Skills and tools: `commands.list`, `skills.*`, `tools.catalog`, `tools.effective`, `tools.invoke`.
446446

447447
</Accordion>
448448
</AccordionGroup>
@@ -500,6 +500,18 @@ enumeration of `src/gateway/server-methods/*.ts`.
500500
caller-supplied auth or delivery context.
501501
- The response is session-scoped and reflects what the active conversation can use right now,
502502
including core, plugin, and channel tools.
503+
- Operators may call `tools.invoke` (`operator.write`) to invoke one runtime-effective tool through
504+
the Gateway policy pipeline.
505+
- Request shape: `{ name, args?, sessionKey?, agentId?, confirm?, idempotencyKey? }`.
506+
- Omit `sessionKey` to use the default main session, or pass `agentId` to target that agent's main
507+
session. When both `sessionKey` and `agentId` are present, the Gateway rejects mismatches.
508+
- The RPC reuses the same direct-invocation Gateway tool allow/deny filtering as
509+
`/tools/invoke`.
510+
- The response is typed as `{ ok: true, toolName, output? }` on success or
511+
`{ ok: false, toolName?, requiresApproval?, approvalId?, error }` for application-level refusal.
512+
- Without `confirm: true`, plugin approval requests return `requiresApproval: true` without
513+
executing the tool. With `confirm: true`, the Gateway follows the existing plugin approval wait
514+
semantics before execution.
503515
- Operators may call `skills.status` (`operator.read`) to fetch the visible
504516
skill inventory for an agent.
505517
- `agentId` is optional; omit it to read the default agent workspace.

docs/reference/openclaw-sdk-api-design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ oc.models.list();
5858
oc.models.status(); // Gateway models.authStatus
5959

6060
oc.tools.list();
61-
oc.tools.invoke(...); // future API: current SDK throws unsupported
61+
oc.tools.invoke(...); // Gateway tools.invoke
6262

6363
oc.artifacts.list({ runId }); // future API: current SDK throws unsupported
6464
oc.artifacts.get(artifactId); // future API: current SDK throws unsupported

packages/sdk/src/client.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import type {
1414
SessionCreateParams,
1515
SessionSendParams,
1616
SessionTarget,
17+
ToolInvokeParams,
18+
ToolInvokeResult,
1719
} from "./types.js";
1820

1921
const MAX_REPLAY_RUNS = 100;
@@ -159,6 +161,30 @@ function buildAgentParams(params: AgentRunParams): Record<string, unknown> {
159161
};
160162
}
161163

164+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
165+
return typeof value === "object" && value !== null && !Array.isArray(value);
166+
}
167+
168+
function buildToolInvokeParams(
169+
name: string,
170+
params?: ToolInvokeParams,
171+
): { gatewayParams: Record<string, unknown>; timeoutMs?: number } {
172+
const toolName = name.trim();
173+
if (!toolName) {
174+
throw new Error("tool name is required");
175+
}
176+
if (params === undefined) {
177+
return { gatewayParams: { name: toolName } };
178+
}
179+
if (!isPlainRecord(params)) {
180+
throw new Error("tools.invoke params must be an object when provided");
181+
}
182+
const timeoutMs =
183+
typeof params.timeoutMs === "number" ? normalizeTimeoutMs(params.timeoutMs) : undefined;
184+
const { timeoutMs: _timeoutMs, ...gatewayParams } = params;
185+
return { gatewayParams: { name: toolName, ...gatewayParams }, timeoutMs };
186+
}
187+
162188
function unsupportedGatewayApi(api: string): never {
163189
throw new Error(`${api} is not supported by the current OpenClaw Gateway yet`);
164190
}
@@ -618,10 +644,13 @@ export class ToolsNamespace extends RpcNamespace {
618644
return await this.call("effective", params);
619645
}
620646

621-
async invoke(name: string, params?: unknown): Promise<unknown> {
622-
void name;
623-
void params;
624-
return unsupportedGatewayApi("oc.tools.invoke");
647+
async invoke(name: string, params?: ToolInvokeParams): Promise<ToolInvokeResult> {
648+
const { gatewayParams, timeoutMs } = buildToolInvokeParams(name, params);
649+
return await this.call<ToolInvokeResult>(
650+
"invoke",
651+
gatewayParams,
652+
timeoutMs !== undefined ? { timeoutMs } : undefined,
653+
);
625654
}
626655
}
627656

packages/sdk/src/index.e2e.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ async function createFakeGateway(port = 0): Promise<FakeGateway> {
100100
"sessions.send",
101101
"tools.catalog",
102102
"tools.effective",
103+
"tools.invoke",
103104
],
104105
events: ["agent", "sessions.changed"],
105106
},
@@ -437,6 +438,7 @@ describe("OpenClaw SDK websocket e2e", () => {
437438
"models.authStatus",
438439
"tools.catalog",
439440
"tools.effective",
441+
"tools.invoke",
440442
"exec.approval.list",
441443
"exec.approval.resolve",
442444
]);

packages/sdk/src/index.test.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,6 @@ describe("OpenClaw SDK", () => {
249249
await expect(oc.tasks.cancel("task_123")).rejects.toThrow(
250250
"oc.tasks.cancel is not supported by the current OpenClaw Gateway yet",
251251
);
252-
await expect(oc.tools.invoke("demo")).rejects.toThrow(
253-
"oc.tools.invoke is not supported by the current OpenClaw Gateway yet",
254-
);
255252
await expect(oc.artifacts.list()).rejects.toThrow(
256253
"oc.artifacts.list is not supported by the current OpenClaw Gateway yet",
257254
);
@@ -276,6 +273,66 @@ describe("OpenClaw SDK", () => {
276273
expect(transport.calls).toEqual([]);
277274
});
278275

276+
it("invokes tools through the Gateway tools.invoke method", async () => {
277+
const transport = new FakeTransport({
278+
"tools.invoke": (params) => ({
279+
ok: true,
280+
toolName:
281+
typeof params === "object" && params !== null
282+
? (params as { name?: unknown }).name
283+
: "unknown",
284+
output: { received: params },
285+
}),
286+
});
287+
const oc = new OpenClaw({ transport });
288+
289+
const noArgs = await oc.tools.invoke("demo");
290+
const scoped = await oc.tools.invoke("demo", {
291+
args: { input: "confirmed" },
292+
sessionKey: "agent:main:main",
293+
confirm: true,
294+
idempotencyKey: "invoke-1",
295+
timeoutMs: 123,
296+
});
297+
298+
expect(noArgs).toMatchObject({
299+
ok: true,
300+
toolName: "demo",
301+
output: { received: { name: "demo" } },
302+
});
303+
expect(scoped).toMatchObject({
304+
ok: true,
305+
toolName: "demo",
306+
output: {
307+
received: {
308+
name: "demo",
309+
args: { input: "confirmed" },
310+
sessionKey: "agent:main:main",
311+
confirm: true,
312+
idempotencyKey: "invoke-1",
313+
},
314+
},
315+
});
316+
expect(transport.calls).toEqual([
317+
{
318+
method: "tools.invoke",
319+
params: { name: "demo" },
320+
options: undefined,
321+
},
322+
{
323+
method: "tools.invoke",
324+
params: {
325+
name: "demo",
326+
args: { input: "confirmed" },
327+
sessionKey: "agent:main:main",
328+
confirm: true,
329+
idempotencyKey: "invoke-1",
330+
},
331+
options: { timeoutMs: 123 },
332+
},
333+
]);
334+
});
335+
279336
it("cancels runs and checks model auth status through current Gateway methods", async () => {
280337
const transport = new FakeTransport({
281338
agent: { status: "accepted", runId: "run_without_session" },

packages/sdk/src/types.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,35 @@ export type SessionTarget = {
199199
};
200200

201201
export type RunCreateParams = AgentRunParams;
202+
203+
export type ToolInvokeParams = {
204+
args?: JsonObject;
205+
sessionKey?: string;
206+
agentId?: string;
207+
confirm?: boolean;
208+
idempotencyKey?: string;
209+
timeoutMs?: number;
210+
};
211+
212+
export type ToolInvokeResult =
213+
| {
214+
ok: true;
215+
toolName: string;
216+
output?: unknown;
217+
}
218+
| {
219+
ok: false;
220+
toolName?: string;
221+
requiresApproval?: boolean;
222+
approvalId?: string;
223+
error: {
224+
type:
225+
| "invalid_request"
226+
| "not_found"
227+
| "approval_required"
228+
| "tool_call_blocked"
229+
| "tool_error"
230+
| (string & {});
231+
message: string;
232+
};
233+
};

0 commit comments

Comments
 (0)