From 62d5df28dc4ac50dc6f0fbfad784a0b70d009101 Mon Sep 17 00:00:00 2001 From: Robin Waslander Date: Wed, 11 Mar 2026 04:58:49 +0100 Subject: [PATCH] fix(agents): add nodes to owner-only tool policy fallbacks The nodes tool was missing from OWNER_ONLY_TOOL_NAME_FALLBACKS in tool-policy.ts. applyOwnerOnlyToolPolicy() correctly removed gateway and cron for non-owners but kept nodes, which internally issues privileged gateway calls: node.pair.approve (operator.pairing) and node.invoke (operator.write). A non-owner sender could approve pending node pairings and invoke arbitrary node commands, extending to system.run on paired nodes. Add nodes to the fallback owner-only set. Non-owners no longer receive the nodes tool after policy application; owners retain it. Fixes GHSA-r26r-9hxr-r792 --- CHANGELOG.md | 2 ++ src/agents/tool-policy.test.ts | 22 ++++++++++++++++++++++ src/agents/tool-policy.ts | 7 ++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7421450d59..3ca52a6e9ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,8 @@ Docs: https://docs.openclaw.ai - Agents/context pruning: prune image-only tool results during soft-trim, align context-pruning coverage with the new tool-result contract, and extend historical image cleanup to the same screenshot-heavy session path. (#43045) Thanks @MoerAI. - fix(models): guard optional model.input capability checks (#42096) thanks @andyliu - Security/plugin runtime: stop unauthenticated plugin HTTP routes from inheriting synthetic admin gateway scopes when they call `runtime.subagent.*`, so admin-only methods like `sessions.delete` stay blocked without gateway auth. +- Security/session_status: enforce sandbox session-tree visibility and shared agent-to-agent access guards before reading or mutating target session state, so sandboxed subagents can no longer inspect parent session metadata or write parent model overrides via `session_status`. +- Security/nodes: treat the `nodes` agent tool as owner-only fallback policy so non-owner senders cannot reach paired-node approval or invoke paths through the shared tool set. ## 2026.3.8 diff --git a/src/agents/tool-policy.test.ts b/src/agents/tool-policy.test.ts index 9a9f512189b..963c703a409 100644 --- a/src/agents/tool-policy.test.ts +++ b/src/agents/tool-policy.test.ts @@ -80,6 +80,7 @@ describe("tool-policy", () => { expect(isOwnerOnlyToolName("whatsapp_login")).toBe(true); expect(isOwnerOnlyToolName("cron")).toBe(true); expect(isOwnerOnlyToolName("gateway")).toBe(true); + expect(isOwnerOnlyToolName("nodes")).toBe(true); expect(isOwnerOnlyToolName("read")).toBe(false); }); @@ -107,6 +108,27 @@ describe("tool-policy", () => { expect(applyOwnerOnlyToolPolicy(tools, false)).toEqual([]); expect(applyOwnerOnlyToolPolicy(tools, true)).toHaveLength(1); }); + + it("strips nodes for non-owner senders via fallback policy", () => { + const tools = [ + { + name: "read", + // oxlint-disable-next-line typescript/no-explicit-any + execute: async () => ({ content: [], details: {} }) as any, + }, + { + name: "nodes", + // oxlint-disable-next-line typescript/no-explicit-any + execute: async () => ({ content: [], details: {} }) as any, + }, + ] as unknown as AnyAgentTool[]; + + expect(applyOwnerOnlyToolPolicy(tools, false).map((tool) => tool.name)).toEqual(["read"]); + expect(applyOwnerOnlyToolPolicy(tools, true).map((tool) => tool.name)).toEqual([ + "read", + "nodes", + ]); + }); }); describe("TOOL_POLICY_CONFORMANCE", () => { diff --git a/src/agents/tool-policy.ts b/src/agents/tool-policy.ts index 188a9c3361c..5538fb765ce 100644 --- a/src/agents/tool-policy.ts +++ b/src/agents/tool-policy.ts @@ -28,7 +28,12 @@ function wrapOwnerOnlyToolExecution(tool: AnyAgentTool, senderIsOwner: boolean): }; } -const OWNER_ONLY_TOOL_NAME_FALLBACKS = new Set(["whatsapp_login", "cron", "gateway"]); +const OWNER_ONLY_TOOL_NAME_FALLBACKS = new Set([ + "whatsapp_login", + "cron", + "gateway", + "nodes", +]); export function isOwnerOnlyToolName(name: string) { return OWNER_ONLY_TOOL_NAME_FALLBACKS.has(normalizeToolName(name));