Skip to content

Commit 8d1582e

Browse files
committed
fix(gateway): tighten tools invoke rpc contract
1 parent b638f55 commit 8d1582e

11 files changed

Lines changed: 54 additions & 52 deletions

File tree

CHANGELOG.md

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

99
- Voice Call/Google Meet: add Twilio Meet join phase logs around pre-connect DTMF, realtime stream setup, and initial greeting handoff for easier live-call debugging. Thanks @donkeykong91 and @PfanP.
1010
- macOS app: move recent session context rows into a Context submenu while keeping usage and cost details root-level, so the menu bar companion stays compact with many active sessions. Thanks @guti.
11+
- Gateway/SDK: add SDK-facing tools.invoke RPC with shared HTTP policy, typed approval/refusal results, and SDK helper support. Refs #74705. Thanks @BunsDev and @ai-hpc.
1112
- Messages/docs: clarify that `BodyForAgent` is the primary inbound model text while `Body` is the legacy envelope fallback, and add Signal coverage so channel hardening patches target the real prompt path. Refs #66198. Thanks @defonota3box.
1213
- Control UI/Usage: add UTC quarter-hour token buckets for the Usage Mosaic and reuse them for hour filtering, keeping the legacy session-span fallback for older summaries. (#74337) Thanks @konanok.
1314
- BlueBubbles: add opt-in `channels.bluebubbles.replyContextApiFallback` that fetches the original message from the BlueBubbles HTTP API when the in-memory reply-context cache misses (multi-instance deployments sharing one BB account, post-restart, after long-lived TTL/LRU eviction). Off by default; channel-level setting propagates to accounts that omit the flag through `mergeAccountConfig`; routed through the typed `BlueBubblesClient` so every fetch is SSRF-guarded by the same three-mode policy as every other BB client request; reply-id shape is validated and part-index prefixes (`p:0/<guid>`) are stripped before the request; concurrent webhooks for the same `replyToId` coalesce into one fetch and successful responses populate the reply cache for subsequent hits. Also promotes BlueBubbles attachment download failures from verbose to runtime error so silently-dropped inbound images are visible at default log level, and extends `sanitizeForLog` to redact `?password=…`/`?token=…` query params and `Authorization:` headers before they reach the log sink (CWE-532). (#71820) Thanks @coletebou and @zqchris.

apps/macos/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3831,48 +3831,36 @@ public struct ToolsEffectiveResult: Codable, Sendable {
38313831
}
38323832

38333833
public struct ToolsInvokeParams: Codable, Sendable {
3834-
public let name: String?
3835-
public let tool: String?
3836-
public let action: String?
3834+
public let name: String
38373835
public let args: [String: AnyCodable]?
38383836
public let sessionkey: String?
38393837
public let agentid: String?
38403838
public let confirm: Bool?
38413839
public let idempotencykey: String?
3842-
public let dryrun: Bool?
38433840

38443841
public init(
3845-
name: String?,
3846-
tool: String?,
3847-
action: String?,
3842+
name: String,
38483843
args: [String: AnyCodable]?,
38493844
sessionkey: String?,
38503845
agentid: String?,
38513846
confirm: Bool?,
3852-
idempotencykey: String?,
3853-
dryrun: Bool?)
3847+
idempotencykey: String?)
38543848
{
38553849
self.name = name
3856-
self.tool = tool
3857-
self.action = action
38583850
self.args = args
38593851
self.sessionkey = sessionkey
38603852
self.agentid = agentid
38613853
self.confirm = confirm
38623854
self.idempotencykey = idempotencykey
3863-
self.dryrun = dryrun
38643855
}
38653856

38663857
private enum CodingKeys: String, CodingKey {
38673858
case name
3868-
case tool
3869-
case action
38703859
case args
38713860
case sessionkey = "sessionKey"
38723861
case agentid = "agentId"
38733862
case confirm
38743863
case idempotencykey = "idempotencyKey"
3875-
case dryrun = "dryRun"
38763864
}
38773865
}
38783866

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

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3831,48 +3831,36 @@ public struct ToolsEffectiveResult: Codable, Sendable {
38313831
}
38323832

38333833
public struct ToolsInvokeParams: Codable, Sendable {
3834-
public let name: String?
3835-
public let tool: String?
3836-
public let action: String?
3834+
public let name: String
38373835
public let args: [String: AnyCodable]?
38383836
public let sessionkey: String?
38393837
public let agentid: String?
38403838
public let confirm: Bool?
38413839
public let idempotencykey: String?
3842-
public let dryrun: Bool?
38433840

38443841
public init(
3845-
name: String?,
3846-
tool: String?,
3847-
action: String?,
3842+
name: String,
38483843
args: [String: AnyCodable]?,
38493844
sessionkey: String?,
38503845
agentid: String?,
38513846
confirm: Bool?,
3852-
idempotencykey: String?,
3853-
dryrun: Bool?)
3847+
idempotencykey: String?)
38543848
{
38553849
self.name = name
3856-
self.tool = tool
3857-
self.action = action
38583850
self.args = args
38593851
self.sessionkey = sessionkey
38603852
self.agentid = agentid
38613853
self.confirm = confirm
38623854
self.idempotencykey = idempotencykey
3863-
self.dryrun = dryrun
38643855
}
38653856

38663857
private enum CodingKeys: String, CodingKey {
38673858
case name
3868-
case tool
3869-
case action
38703859
case args
38713860
case sessionkey = "sessionKey"
38723861
case agentid = "agentId"
38733862
case confirm
38743863
case idempotencykey = "idempotencyKey"
3875-
case dryrun = "dryRun"
38763864
}
38773865
}
38783866

docs/gateway/protocol.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -503,9 +503,10 @@ enumeration of `src/gateway/server-methods/*.ts`.
503503
including core, plugin, and channel tools.
504504
- Operators may call `tools.invoke` (`operator.write`) to invoke one available tool through the
505505
same gateway policy path as `/tools/invoke`.
506-
- `name` is required for SDK callers; `tool` is accepted as the HTTP-compatible alias.
507-
`action`, `args`, `sessionKey`, `agentId`, `confirm`, `idempotencyKey`, and reserved
508-
`dryRun` are optional.
506+
- `name` is required. `args`, `sessionKey`, `agentId`, `confirm`, and
507+
`idempotencyKey` are optional.
508+
- If both `sessionKey` and `agentId` are present, the resolved session agent must match
509+
`agentId`.
509510
- The response is an SDK-facing envelope with `ok`, `toolName`, optional `output`, and typed
510511
`error` fields. Approval or policy refusals return `ok:false` in the payload rather than
511512
bypassing the gateway tool policy pipeline.

packages/sdk/src/client.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -769,13 +769,11 @@ export class ToolsNamespace extends RpcNamespace {
769769
async invoke(name: string, params?: ToolInvokeParams): Promise<ToolInvokeResult> {
770770
return await this.call("invoke", {
771771
name,
772-
...(params?.action ? { action: params.action } : {}),
773772
...(params?.args ? { args: params.args } : {}),
774773
...(params?.sessionKey ? { sessionKey: params.sessionKey } : {}),
775774
...(params?.agentId ? { agentId: params.agentId } : {}),
776775
...(typeof params?.confirm === "boolean" ? { confirm: params.confirm } : {}),
777776
...(params?.idempotencyKey ? { idempotencyKey: params.idempotencyKey } : {}),
778-
...(typeof params?.dryRun === "boolean" ? { dryRun: params.dryRun } : {}),
779777
});
780778
}
781779
}

packages/sdk/src/index.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -359,24 +359,20 @@ describe("OpenClaw SDK", () => {
359359
await expect(
360360
oc.tools.invoke("demo", {
361361
args: { mode: "test" },
362-
action: "json",
363362
sessionKey: "agent:main:main",
364363
confirm: false,
365364
idempotencyKey: "tools-invoke-test",
366-
dryRun: false,
367365
}),
368366
).resolves.toMatchObject({ ok: true, toolName: "demo", output: { value: 1 } });
369367
expect(transport.calls).toEqual([
370368
{
371369
method: "tools.invoke",
372370
params: {
373371
name: "demo",
374-
action: "json",
375372
args: { mode: "test" },
376373
sessionKey: "agent:main:main",
377374
confirm: false,
378375
idempotencyKey: "tools-invoke-test",
379-
dryRun: false,
380376
},
381377
options: undefined,
382378
},

packages/sdk/src/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,11 @@ export type SDKError = {
115115
};
116116

117117
export type ToolInvokeParams = {
118-
action?: string;
119118
args?: JsonObject;
120119
sessionKey?: string;
121120
agentId?: string;
122121
confirm?: boolean;
123122
idempotencyKey?: string;
124-
dryRun?: boolean;
125123
};
126124

127125
export type ToolInvokeResult = {

src/gateway/protocol/schema/agents-models-skills.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -377,15 +377,12 @@ export const ToolsEffectiveParamsSchema = Type.Object(
377377

378378
export const ToolsInvokeParamsSchema = Type.Object(
379379
{
380-
name: Type.Optional(NonEmptyString),
381-
tool: Type.Optional(NonEmptyString),
382-
action: Type.Optional(NonEmptyString),
380+
name: NonEmptyString,
383381
args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
384382
sessionKey: Type.Optional(NonEmptyString),
385383
agentId: Type.Optional(NonEmptyString),
386384
confirm: Type.Optional(Type.Boolean()),
387385
idempotencyKey: Type.Optional(NonEmptyString),
388-
dryRun: Type.Optional(Type.Boolean()),
389386
},
390387
{ additionalProperties: false },
391388
);

src/gateway/server-methods/tools-invoke.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,12 @@ export const toolsInvokeHandlers: GatewayRequestHandlers = {
4343
);
4444
return;
4545
}
46-
const requestedToolName = normalizeOptionalString(params.name ?? params.tool);
46+
const requestedToolName = normalizeOptionalString(params.name);
4747
if (!requestedToolName) {
4848
respond(
4949
false,
5050
undefined,
51-
errorShape(
52-
ErrorCodes.INVALID_REQUEST,
53-
"invalid tools.invoke params: name or tool required",
54-
),
51+
errorShape(ErrorCodes.INVALID_REQUEST, "invalid tools.invoke params: name required"),
5552
);
5653
return;
5754
}

src/gateway/tools-invoke-http.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -923,8 +923,7 @@ describe("tools.invoke Gateway RPC", () => {
923923
allowAgentsListForMain();
924924

925925
const call = await invokeToolsRpc({
926-
tool: "agents_list",
927-
action: "json",
926+
name: "agents_list",
928927
args: {},
929928
sessionKey: "main",
930929
idempotencyKey: "rpc-tool-test",
@@ -979,6 +978,33 @@ describe("tools.invoke Gateway RPC", () => {
979978
});
980979
});
981980

981+
it("rejects mismatched session and agent scope", async () => {
982+
cfg = {
983+
agents: {
984+
list: [
985+
{ id: "main", default: true, tools: { allow: ["agents_list"] } },
986+
{ id: "other", tools: { allow: ["agents_list"] } },
987+
],
988+
},
989+
};
990+
991+
const call = await invokeToolsRpc({
992+
name: "agents_list",
993+
sessionKey: "agent:main:main",
994+
agentId: "other",
995+
});
996+
997+
expect(call?.[0]).toBe(true);
998+
expect(call?.[1]).toMatchObject({
999+
ok: false,
1000+
toolName: "agents_list",
1001+
error: {
1002+
code: "validation_error",
1003+
message: 'agent id "other" does not match session agent "main"',
1004+
},
1005+
});
1006+
});
1007+
9821008
it("rejects malformed params at the RPC boundary", async () => {
9831009
const call = await invokeToolsRpc({ name: "" });
9841010

0 commit comments

Comments
 (0)