惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
H
Help Net Security
Jina AI
Jina AI
V
V2EX
G
Google Developers Blog
B
Blog
GbyAI
GbyAI
U
Unit 42
爱范儿
爱范儿
腾讯CDC
Engineering at Meta
Engineering at Meta
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
小众软件
小众软件
D
DataBreaches.Net
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
博客园 - 聂微东
The Cloudflare Blog
I
InfoQ
Microsoft Azure Blog
Microsoft Azure Blog
Hugging Face - Blog
Hugging Face - Blog
大猫的无限游戏
大猫的无限游戏

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(plugins): discover alsoAllow plugin tools · openclaw/...
steipete · 2026-05-04 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai

5858

- Doctor/plugins: reset stale `plugins.slots.memory` and `plugins.slots.contextEngine` references during `doctor --fix`, so cleanup of missing plugin config does not leave unrecoverable slot owners behind. Fixes #76550 and #76551. Thanks @vincentkoc.

5959

- Docs/WhatsApp: merge the duplicate top-level `web` objects in the gateway channel config example so copy-pasted WhatsApp config keeps both `web.whatsapp` and reconnect settings. Fixes #76619. Thanks @WadydX.

6060

- Plugins/Anthropic: expose Claude thinking profiles from the bundled provider-policy artifact so non-runtime callers keep Opus 4.7 `adaptive`, `xhigh`, and `max` instead of downgrading to `high`. Fixes #76779. Thanks @tomascupr and @iAbhi001.

61+

- Plugins/tools: honor `tools.alsoAllow` as an optional plugin tool discovery hint without treating its internal allow-all default as permission to load every optional plugin tool. Fixes #76616.

6162

- Discord/native commands: skip slash-command registration and cleanup REST calls when `channels.discord.commands.native=false`, letting low-power gateways start without waiting on disabled native-command lifecycle requests. Fixes #76202. Thanks @vincentkoc.

6263

- CLI/plugins: reject unowned command roots such as `openclaw foo` before managed proxy startup and full plugin CLI runtime loading while preserving manifest-owned and CLI-metadata-owned plugin commands. Fixes #75287. Thanks @neilofneils404.

6364

- CLI/message: skip local configured-channel plugin preload for explicit gateway-owned message actions, letting normalized CLI delivery delegate to the gateway without initializing channel runtime in the short-lived CLI process. Fixes #75477.

Original file line numberDiff line numberDiff line change

@@ -26,21 +26,18 @@ without writing custom OpenClaw code for each workflow.

2626

}

2727

```

2828
29-

2. Allowlist the tool (it is registered with `optional: true`):

29+

2. Allow the optional tool:

3030
3131

```json

3232

{

33-

"agents": {

34-

"list": [

35-

{

36-

"id": "main",

37-

"tools": { "allow": ["llm-task"] }

38-

}

39-

]

33+

"tools": {

34+

"alsoAllow": ["llm-task"]

4035

}

4136

}

4237

```

4338
39+

Use `tools.allow` only when you want restrictive allowlist mode.

40+
4441

## Config (optional)

4542
4643

```json

Original file line numberDiff line numberDiff line change

@@ -191,7 +191,7 @@ Or per-agent:

191191

Avoid using `tools.allow: ["lobster"]` unless you intend to run in restrictive allowlist mode.

192192
193193

<Note>

194-

Allowlists are opt-in for optional plugins. If your allowlist only names plugin tools (like `lobster`), OpenClaw keeps core tools enabled. To restrict core tools, include the core tools or groups you want in the allowlist too.

194+

Allowlists are opt-in for optional plugins. `alsoAllow` enables only the named optional plugin tools while preserving the normal core tool set. To restrict core tools, use `tools.allow` with the core tools or groups you want.

195195

</Note>

196196
197197

## Example: Email triage

Original file line numberDiff line numberDiff line change

@@ -19,7 +19,7 @@ import { expectReadWriteEditTools } from "./test-helpers/pi-tools-fs-helpers.js"

1919

import { createPiToolsSandboxContext } from "./test-helpers/pi-tools-sandbox-context.js";

2020

import { providerAliasCases } from "./test-helpers/provider-alias-cases.js";

2121

import { buildEmptyExplicitToolAllowlistError } from "./tool-allowlist-guard.js";

22-

import { normalizeToolName } from "./tool-policy.js";

22+

import { DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY, normalizeToolName } from "./tool-policy.js";

2323
2424

const tinyPngBuffer = Buffer.from(

2525

"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO2f7z8AAAAASUVORK5CYII=",

@@ -180,6 +180,21 @@ describe("createOpenClawCodingTools", () => {

180180

);

181181

});

182182
183+

it("uses tools.alsoAllow for optional plugin discovery without widening to all plugins", () => {

184+

const createOpenClawToolsMock = vi.mocked(createOpenClawTools);

185+

createOpenClawToolsMock.mockClear();

186+
187+

createOpenClawCodingTools({

188+

config: { tools: { alsoAllow: ["lobster"] } },

189+

});

190+
191+

expect(createOpenClawToolsMock).toHaveBeenCalledWith(

192+

expect.objectContaining({

193+

pluginToolAllowlist: ["lobster", DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY],

194+

}),

195+

);

196+

});

197+
183198

it("passes explicit denylist entries to OpenClaw tool factory planning", () => {

184199

const createOpenClawToolsMock = vi.mocked(createOpenClawTools);

185200

createOpenClawToolsMock.mockClear();

Original file line numberDiff line numberDiff line change

@@ -1,5 +1,9 @@

11

import type { SandboxToolPolicy } from "./sandbox/types.js";

22
3+

export const IMPLICIT_ALLOW_ALL_FROM_ALSO_ALLOW = Symbol.for(

4+

"openclaw.toolPolicy.implicitAllowAllFromAlsoAllow",

5+

);

6+
37

type SandboxToolPolicyConfig = {

48

allow?: string[];

59

alsoAllow?: string[];

@@ -19,12 +23,21 @@ function unionAllow(base?: string[], extra?: string[]): string[] | undefined {

1923

return Array.from(new Set([...base, ...extra]));

2024

}

2125
26+

function hasExplicitAllowAll(list?: string[]): boolean {

27+

return Array.isArray(list) && list.some((entry) => entry.trim() === "*");

28+

}

29+
2230

export function pickSandboxToolPolicy(

2331

config?: SandboxToolPolicyConfig,

2432

): SandboxToolPolicy | undefined {

2533

if (!config) {

2634

return undefined;

2735

}

36+

const allowFromAlsoAllowOnly =

37+

!Array.isArray(config.allow) &&

38+

Array.isArray(config.alsoAllow) &&

39+

config.alsoAllow.length > 0 &&

40+

!hasExplicitAllowAll(config.alsoAllow);

2841

const allow = Array.isArray(config.allow)

2942

? unionAllow(config.allow, config.alsoAllow)

3043

: Array.isArray(config.alsoAllow) && config.alsoAllow.length > 0

@@ -34,5 +47,13 @@ export function pickSandboxToolPolicy(

3447

if (!allow && !deny) {

3548

return undefined;

3649

}

37-

return { allow, deny };

50+

const policy = { allow, deny } as SandboxToolPolicy & {

51+

[IMPLICIT_ALLOW_ALL_FROM_ALSO_ALLOW]?: true;

52+

};

53+

if (allowFromAlsoAllowOnly) {

54+

Object.defineProperty(policy, IMPLICIT_ALLOW_ALL_FROM_ALSO_ALLOW, {

55+

value: true,

56+

});

57+

}

58+

return policy;

3859

}

Original file line numberDiff line numberDiff line change

@@ -1,13 +1,15 @@

11

import { describe, expect, it } from "vitest";

22

import type { OpenClawConfig } from "../config/config.js";

33

import { DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "../security/dangerous-tools.js";

4+

import { pickSandboxToolPolicy } from "./sandbox-tool-policy.js";

45

import { isToolAllowed, resolveSandboxToolPolicyForAgent } from "./sandbox/tool-policy.js";

56

import type { SandboxToolPolicy } from "./sandbox/types.js";

67

import { isToolAllowedByPolicyName } from "./tool-policy-match.js";

78

import { TOOL_POLICY_CONFORMANCE } from "./tool-policy.conformance.js";

89

import {

910

applyOwnerOnlyToolPolicy,

1011

collectExplicitAllowlist,

12+

DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY,

1113

expandToolGroups,

1214

isOwnerOnlyToolName,

1315

normalizeToolName,

@@ -141,7 +143,7 @@ describe("tool-policy", () => {

141143

expect(applyOwnerOnlyToolPolicy(tools, true)).toHaveLength(1);

142144

});

143145
144-

it("preserves explicit alsoAllow hints when allow is empty", () => {

146+

it("collects explicit allowlist entries", () => {

145147

expect(

146148

collectExplicitAllowlist([

147149

{

@@ -151,6 +153,23 @@ describe("tool-policy", () => {

151153

).toContain("optional-demo");

152154

});

153155
156+

it("uses alsoAllow entries for plugin discovery without the synthetic allow-all", () => {

157+

expect(collectExplicitAllowlist([pickSandboxToolPolicy({ alsoAllow: ["lobster"] })])).toEqual([

158+

"lobster",

159+

DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY,

160+

]);

161+

expect(

162+

collectExplicitAllowlist([pickSandboxToolPolicy({ allow: [], alsoAllow: ["lobster"] })]),

163+

).toEqual(["*", "lobster"]);

164+

});

165+
166+

it("preserves explicit alsoAllow wildcards for plugin discovery", () => {

167+

expect(collectExplicitAllowlist([pickSandboxToolPolicy({ alsoAllow: ["*"] })])).toEqual(["*"]);

168+

expect(collectExplicitAllowlist([pickSandboxToolPolicy({ alsoAllow: [" * "] })])).toEqual([

169+

"*",

170+

]);

171+

});

172+
154173

it("strips nodes for non-owner senders via fallback policy", () => {

155174

const tools = [

156175

{

Original file line numberDiff line numberDiff line change

@@ -1,4 +1,5 @@

11

import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";

2+

import { IMPLICIT_ALLOW_ALL_FROM_ALSO_ALLOW } from "./sandbox-tool-policy.js";

23

import {

34

expandToolGroups,

45

normalizeToolList,

@@ -80,6 +81,7 @@ export function applyOwnerOnlyToolPolicy(

8081

export type ToolPolicyLike = {

8182

allow?: string[];

8283

deny?: string[];

84+

[IMPLICIT_ALLOW_ALL_FROM_ALSO_ALLOW]?: true;

8385

};

8486
8587

export type PluginToolGroups = {

@@ -93,6 +95,8 @@ export type AllowlistResolution = {

9395

pluginOnlyAllowlist: boolean;

9496

};

9597
98+

export const DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY = "__openclaw_default_plugin_tools__";

99+
96100

export function collectExplicitAllowlist(policies: Array<ToolPolicyLike | undefined>): string[] {

97101

const entries: string[] = [];

98102

for (const policy of policies) {

@@ -104,12 +108,18 @@ export function collectExplicitAllowlist(policies: Array<ToolPolicyLike | undefi

104108

continue;

105109

}

106110

const trimmed = value.trim();

111+

if (trimmed === "*" && policy[IMPLICIT_ALLOW_ALL_FROM_ALSO_ALLOW] === true) {

112+

continue;

113+

}

107114

if (trimmed) {

108115

entries.push(trimmed);

109116

}

110117

}

118+

if (policy[IMPLICIT_ALLOW_ALL_FROM_ALSO_ALLOW] === true) {

119+

entries.push(DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY);

120+

}

111121

}

112-

return entries;

122+

return Array.from(new Set(entries));

113123

}

114124
115125

export function collectExplicitDenylist(policies: Array<ToolPolicyLike | undefined>): string[] {

Original file line numberDiff line numberDiff line change

@@ -494,6 +494,26 @@ describe("POST /tools/invoke", () => {

494494

);

495495

});

496496
497+

it("uses tools.alsoAllow for optional plugin discovery without loading every plugin tool", async () => {

498+

cfg = {

499+

...cfg,

500+

agents: { list: [{ id: "main", default: true }] },

501+

tools: { alsoAllow: ["plugin_doctor"] },

502+

};

503+
504+

const res = await invokeToolAuthed({

505+

tool: "plugin_doctor",

506+

sessionKey: "main",

507+

});

508+
509+

const body = await expectOkInvokeResponse(res);

510+

expect(body.result).toMatchObject({ ok: true, permissionFlow: true });

511+

expect(lastCreateOpenClawToolsContext?.pluginToolAllowlist).toEqual(

512+

expect.arrayContaining(["plugin_doctor"]),

513+

);

514+

expect(lastCreateOpenClawToolsContext?.pluginToolAllowlist).not.toContain("*");

515+

});

516+
497517

it("blocks tool execution when before_tool_call rejects the invoke", async () => {

498518

setMainAllowedTools({ allow: ["tools_invoke_test"] });

499519

hookMocks.runBeforeToolCallHook.mockResolvedValueOnce({

Original file line numberDiff line numberDiff line change

@@ -1,4 +1,5 @@

11

import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";

2+

import { DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY } from "../agents/tool-policy.js";

23

import { resetLogger, setLoggerOverride } from "../logging/logger.js";

34

import { loggingState } from "../logging/state.js";

45

import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";

@@ -945,6 +946,26 @@ describe("resolvePluginTools optional tools", () => {

945946

expectResolvedToolNames(tools, ["optional_tool"]);

946947

});

947948
949+

it("keeps default non-optional plugin tools when alsoAllow opts into optional tools", () => {

950+

const defaultEntry: MockRegistryToolEntry = {

951+

pluginId: "multi",

952+

optional: false,

953+

source: "/tmp/multi.js",

954+

names: ["other_tool"],

955+

declaredNames: ["other_tool"],

956+

factory: () => makeTool("other_tool"),

957+

};

958+

setRegistry([defaultEntry, createOptionalDemoEntry()]);

959+
960+

const tools = resolvePluginTools(

961+

createResolveToolsParams({

962+

toolAllowlist: [DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY, "optional_tool"],

963+

}),

964+

);

965+
966+

expectResolvedToolNames(tools, ["other_tool", "optional_tool"]);

967+

});

968+
948969

it("rejects plugin id collisions with core tool names", () => {

949970

const registry = setRegistry([

950971

{

Original file line numberDiff line numberDiff line change

@@ -1,4 +1,4 @@

1-

import { normalizeToolName } from "../agents/tool-policy.js";

1+

import { DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY, normalizeToolName } from "../agents/tool-policy.js";

22

import type { AnyAgentTool } from "../agents/tools/common.js";

33

import { createSubsystemLogger } from "../logging/subsystem.js";

44

import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";

@@ -86,6 +86,10 @@ function normalizeAllowlist(list?: string[]) {

8686

return new Set((list ?? []).map(normalizeToolName).filter(Boolean));

8787

}

8888
89+

function allowlistIncludesDefaultPluginTools(allowlist: Set<string>): boolean {

90+

return allowlist.size === 0 || allowlist.has(DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY);

91+

}

92+
8993

function isOptionalToolAllowed(params: {

9094

toolName: string;

9195

pluginId: string;

@@ -289,8 +293,8 @@ function pluginToolNamesMatchAllowlist(params: {

289293

optional: boolean;

290294

allowlist: Set<string>;

291295

}): boolean {

292-

if (params.allowlist.size === 0) {

293-

return !params.optional;

296+

if (!params.optional && allowlistIncludesDefaultPluginTools(params.allowlist)) {

297+

return true;

294298

}

295299

return isOptionalToolEntryPotentiallyAllowed(params);

296300

}

@@ -303,7 +307,7 @@ function manifestToolContractMatchesAllowlist(params: {

303307

if (params.toolNames.length === 0) {

304308

return false;

305309

}

306-

if (params.allowlist.size === 0) {

310+

if (allowlistIncludesDefaultPluginTools(params.allowlist)) {

307311

return true;

308312

}

309313

if (params.allowlist.has("*") || params.allowlist.has("group:plugins")) {

@@ -322,7 +326,7 @@ function listManifestToolNamesForAvailability(params: {

322326

allowlist: Set<string>;

323327

}): string[] {

324328

if (

325-

params.allowlist.size === 0 ||

329+

allowlistIncludesDefaultPluginTools(params.allowlist) ||

326330

params.allowlist.has("*") ||

327331

params.allowlist.has("group:plugins")

328332

) {