Skip to content

Commit 4aa08e9

Browse files
authored
fix(security): stop implicit tool grants from config sections (#47487) (#75055)
* fix(security): stop implicit tool grants from config sections (#47487) Configured tool sections (tools.exec, tools.fs) no longer implicitly widen restrictive profiles (messaging, minimal). Previously, having a tools.exec section anywhere in config — even just safety settings like security: "allowlist" — would automatically add exec and process to the profile's allowed tools, defeating the purpose of the restrictive profile. The same pattern existed in tool-fs-policy.ts where tools.fs presence would add read/write/edit to the profile allowlist for root expansion. Changes: - pi-tools.policy.ts: Stop merging implicit grants into profileAlsoAllow. Renamed resolveImplicitProfileAlsoAllow → detectImplicitProfileGrants and use it only for a startup warning that tells users to add explicit alsoAllow entries. - tool-fs-policy.ts: Remove the implicit read/write/edit grant from resolveEffectiveToolFsRootExpansionAllowed when tools.fs is present. Root expansion now requires actual read access via profile or alsoAllow. - Updated 4 existing tests and added 3 new regression tests. Migration: users who relied on tools.exec or tools.fs implicitly granting access under a restrictive profile should add explicit alsoAllow entries: tools: profile: "messaging" alsoAllow: ["exec", "process"] # was implicit, now required exec: { security: "allowlist" } Fixes #47487 * fix: address tool policy review feedback
1 parent 58a0b07 commit 4aa08e9

6 files changed

Lines changed: 109 additions & 28 deletions

File tree

CHANGELOG.md

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

2222
### Changes
2323

24+
- Security/tools: configured tool sections (`tools.exec`, `tools.fs`) no longer implicitly widen restrictive profiles (`messaging`, `minimal`). Users who need those tools under a restricted profile must add explicit `alsoAllow` entries; a startup warning identifies affected configs. Fixes #47487. Thanks @amknight.
2425
- Agents/commitments: add opt-in inferred follow-up commitments with hidden batched extraction, per-agent/per-channel scoping, heartbeat delivery, CLI management, a simple `commitments.enabled`/`commitments.maxPerDay` config, and heartbeat-interval due-time clamping so magical check-ins do not echo immediately. (#74189) Thanks @vignesh07.
2526
- 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.
2627
- Messages/queue: default active-run queueing to `steer` with a 500ms followup fallback debounce, and document the queue modes, precedence, and drop policies on the command queue page. Thanks @vincentkoc.

src/agents/pi-tools.policy.test.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -504,29 +504,29 @@ describe("resolveEffectiveToolPolicy", () => {
504504
).toBeUndefined();
505505
});
506506

507-
it("implicitly re-exposes exec and process when tools.exec is configured", () => {
507+
it("does not implicitly re-expose exec when tools.exec is configured (#47487)", () => {
508508
const cfg = {
509509
tools: {
510510
profile: "messaging",
511511
exec: { host: "sandbox" },
512512
},
513513
} as OpenClawConfig;
514514
const result = resolveEffectiveToolPolicy({ config: cfg });
515-
expect(result.profileAlsoAllow).toEqual(["exec", "process"]);
515+
expect(result.profileAlsoAllow).toBeUndefined();
516516
});
517517

518-
it("implicitly re-exposes read, write, and edit when tools.fs is configured", () => {
518+
it("does not implicitly re-expose fs tools when tools.fs is configured (#47487)", () => {
519519
const cfg = {
520520
tools: {
521521
profile: "messaging",
522522
fs: { workspaceOnly: false },
523523
},
524524
} as OpenClawConfig;
525525
const result = resolveEffectiveToolPolicy({ config: cfg });
526-
expect(result.profileAlsoAllow).toEqual(["read", "write", "edit"]);
526+
expect(result.profileAlsoAllow).toBeUndefined();
527527
});
528528

529-
it("merges explicit alsoAllow with implicit tool-section exposure", () => {
529+
it("explicit alsoAllow works without implicit widening (#47487)", () => {
530530
const cfg = {
531531
tools: {
532532
profile: "messaging",
@@ -535,10 +535,10 @@ describe("resolveEffectiveToolPolicy", () => {
535535
},
536536
} as OpenClawConfig;
537537
const result = resolveEffectiveToolPolicy({ config: cfg });
538-
expect(result.profileAlsoAllow).toEqual(["web_search", "exec", "process"]);
538+
expect(result.profileAlsoAllow).toEqual(["web_search"]);
539539
});
540540

541-
it("uses agent tool sections when resolving implicit exposure", () => {
541+
it("does not implicitly re-expose fs tools from agent tool sections (#47487)", () => {
542542
const cfg = {
543543
tools: {
544544
profile: "messaging",
@@ -555,6 +555,41 @@ describe("resolveEffectiveToolPolicy", () => {
555555
},
556556
} as OpenClawConfig;
557557
const result = resolveEffectiveToolPolicy({ config: cfg, agentId: "coder" });
558-
expect(result.profileAlsoAllow).toEqual(["read", "write", "edit"]);
558+
expect(result.profileAlsoAllow).toBeUndefined();
559+
});
560+
561+
it("global tools.exec does not widen agent messaging profile (#47487)", () => {
562+
const cfg = {
563+
tools: {
564+
exec: { security: "allowlist" },
565+
},
566+
agents: {
567+
list: [
568+
{
569+
id: "messenger",
570+
tools: {
571+
profile: "messaging",
572+
alsoAllow: ["image"],
573+
},
574+
},
575+
],
576+
},
577+
} as OpenClawConfig;
578+
const result = resolveEffectiveToolPolicy({ config: cfg, agentId: "messenger" });
579+
expect(result.profileAlsoAllow).toEqual(["image"]);
580+
expect(result.profileAlsoAllow).not.toContain("exec");
581+
expect(result.profileAlsoAllow).not.toContain("process");
582+
});
583+
584+
it("explicit alsoAllow with exec still grants exec under messaging profile", () => {
585+
const cfg = {
586+
tools: {
587+
profile: "messaging",
588+
alsoAllow: ["exec", "process"],
589+
exec: { host: "sandbox" },
590+
},
591+
} as OpenClawConfig;
592+
const result = resolveEffectiveToolPolicy({ config: cfg });
593+
expect(result.profileAlsoAllow).toEqual(["exec", "process"]);
559594
});
560595
});

src/agents/pi-tools.policy.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js";
44
import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js";
55
import type { OpenClawConfig } from "../config/types.openclaw.js";
66
import type { AgentToolsConfig } from "../config/types.tools.js";
7+
import { logWarn } from "../logger.js";
78
import { normalizeAgentId } from "../routing/session-key.js";
89
import {
910
parseRawSessionConversationRef,
@@ -26,7 +27,11 @@ import {
2627
type SubagentSessionRole,
2728
} from "./subagent-capabilities.js";
2829
import { isToolAllowedByPolicies, isToolAllowedByPolicyName } from "./tool-policy-match.js";
29-
import { normalizeToolName } from "./tool-policy.js";
30+
import {
31+
mergeAlsoAllowPolicy,
32+
normalizeToolName,
33+
resolveToolProfilePolicy,
34+
} from "./tool-policy.js";
3035

3136
/**
3237
* Tools always denied for sub-agents regardless of depth.
@@ -367,7 +372,9 @@ function hasExplicitToolSection(section: unknown): boolean {
367372
return section !== undefined && section !== null;
368373
}
369374

370-
function resolveImplicitProfileAlsoAllow(params: {
375+
/** Detect tool config sections that previously widened profiles implicitly.
376+
* Used only for migration warnings — not merged into profileAlsoAllow. #47487 */
377+
function detectImplicitProfileGrants(params: {
371378
globalTools?: OpenClawConfig["tools"];
372379
agentTools?: AgentToolsConfig;
373380
}): string[] | undefined {
@@ -422,13 +429,33 @@ export function resolveEffectiveToolPolicy(params: {
422429
});
423430
const explicitProfileAlsoAllow =
424431
resolveExplicitProfileAlsoAllow(agentTools) ?? resolveExplicitProfileAlsoAllow(globalTools);
425-
const implicitProfileAlsoAllow = resolveImplicitProfileAlsoAllow({ globalTools, agentTools });
426-
const profileAlsoAllow =
427-
explicitProfileAlsoAllow || implicitProfileAlsoAllow
428-
? Array.from(
429-
new Set([...(explicitProfileAlsoAllow ?? []), ...(implicitProfileAlsoAllow ?? [])]),
430-
)
431-
: undefined;
432+
433+
// Warn affected users about removed implicit grants (#47487), but only when
434+
// the active profile/explicit alsoAllow do not already grant those tools.
435+
if (profile) {
436+
const implicitGrants = detectImplicitProfileGrants({ globalTools, agentTools });
437+
if (implicitGrants) {
438+
const profilePolicy = mergeAlsoAllowPolicy(
439+
resolveToolProfilePolicy(profile),
440+
explicitProfileAlsoAllow,
441+
);
442+
const uncovered = implicitGrants.filter(
443+
(toolName) => !isToolAllowedByPolicyName(toolName, profilePolicy),
444+
);
445+
if (uncovered.length > 0) {
446+
logWarn(
447+
`tools policy: profile "${profile}"${agentId ? ` (agent "${agentId}")` : ""} has ` +
448+
`configured tool sections (tools.exec / tools.fs) that no longer implicitly widen ` +
449+
`the profile. Add alsoAllow: [${uncovered.map((t) => `"${t}"`).join(", ")}] ` +
450+
`explicitly if these tools should be available. See #47487.`,
451+
);
452+
}
453+
}
454+
}
455+
456+
const profileAlsoAllow = explicitProfileAlsoAllow
457+
? Array.from(new Set(explicitProfileAlsoAllow))
458+
: undefined;
432459
return {
433460
agentId,
434461
globalPolicy: pickSandboxToolPolicy(globalTools),
@@ -437,7 +464,7 @@ export function resolveEffectiveToolPolicy(params: {
437464
agentProviderPolicy: pickSandboxToolPolicy(agentProviderPolicy),
438465
profile,
439466
providerProfile: agentProviderPolicy?.profile ?? providerPolicy?.profile,
440-
// alsoAllow is applied at the profile stage (to avoid being filtered out early).
467+
// alsoAllow is applied at the profile stage to avoid early filtering.
441468
profileAlsoAllow,
442469
providerProfileAlsoAllow: Array.isArray(agentProviderPolicy?.alsoAllow)
443470
? agentProviderPolicy?.alsoAllow

src/agents/tool-fs-policy.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,34 @@ describe("resolveEffectiveToolFsRootExpansionAllowed", () => {
6464
expect(resolveEffectiveToolFsRootExpansionAllowed({ cfg, agentId: "main" })).toBe(false);
6565
});
6666

67-
it("re-enables root expansion when tools.fs explicitly allows non-workspace reads", () => {
67+
it("does not re-enable root expansion from tools.fs alone under messaging profile (#47487)", () => {
6868
const cfg: OpenClawConfig = {
6969
tools: {
7070
profile: "messaging",
7171
fs: { workspaceOnly: false },
7272
},
7373
};
74-
expect(resolveEffectiveToolFsRootExpansionAllowed({ cfg, agentId: "main" })).toBe(true);
74+
expect(resolveEffectiveToolFsRootExpansionAllowed({ cfg, agentId: "main" })).toBe(false);
7575
});
7676

77-
it("treats an explicit tools.fs block as a filesystem opt-in", () => {
77+
it("does not treat an explicit tools.fs block as a filesystem opt-in (#47487)", () => {
7878
const cfg: OpenClawConfig = {
7979
tools: {
8080
profile: "messaging",
8181
fs: {},
8282
},
8383
};
84+
expect(resolveEffectiveToolFsRootExpansionAllowed({ cfg, agentId: "main" })).toBe(false);
85+
});
86+
87+
it("re-enables root expansion when alsoAllow explicitly includes read (#47487)", () => {
88+
const cfg: OpenClawConfig = {
89+
tools: {
90+
profile: "messaging",
91+
alsoAllow: ["read"],
92+
fs: { workspaceOnly: false },
93+
},
94+
};
8495
expect(resolveEffectiveToolFsRootExpansionAllowed({ cfg, agentId: "main" })).toBe(true);
8596
});
8697

src/agents/tool-fs-policy.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,10 @@ export function resolveEffectiveToolFsRootExpansionAllowed(params: {
4646
const profile = agentTools?.profile ?? globalTools?.profile;
4747
const profileAlsoAllow = new Set(agentTools?.alsoAllow ?? globalTools?.alsoAllow ?? []);
4848
const fsConfig = resolveToolFsConfig(params);
49-
const hasExplicitFsConfig = agentTools?.fs !== undefined || globalTools?.fs !== undefined;
5049
if (fsConfig.workspaceOnly === true) {
5150
return false;
5251
}
53-
if (hasExplicitFsConfig) {
54-
profileAlsoAllow.add("read");
55-
profileAlsoAllow.add("write");
56-
profileAlsoAllow.add("edit");
57-
}
52+
// tools.fs presence does not grant access; require profile or alsoAllow (#47487).
5853
const profilePolicy = mergeAlsoAllowPolicy(
5954
resolveToolProfilePolicy(profile),
6055
profileAlsoAllow.size > 0 ? Array.from(profileAlsoAllow) : undefined,

src/media/local-roots.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,26 @@ describe("local media roots", () => {
154154
shouldContainPictures: false,
155155
},
156156
{
157-
name: "widens media roots again when messaging-profile agents explicitly enable filesystem tools",
157+
name: "does not widen media roots when messaging-profile agents only configure filesystem guards",
158158
stateDir: path.join("/tmp", "openclaw-messaging-fs-media-roots-state"),
159159
cfg: {
160160
tools: {
161161
profile: "messaging",
162162
fs: { workspaceOnly: false },
163163
},
164164
},
165+
shouldContainPictures: false,
166+
},
167+
{
168+
name: "widens media roots when messaging-profile agents explicitly allow reads",
169+
stateDir: path.join("/tmp", "openclaw-messaging-read-media-roots-state"),
170+
cfg: {
171+
tools: {
172+
profile: "messaging",
173+
alsoAllow: ["read"] as string[],
174+
fs: { workspaceOnly: false },
175+
},
176+
},
165177
shouldContainPictures: true,
166178
},
167179
] as const)("$name", ({ stateDir, cfg, shouldContainPictures }) => {

0 commit comments

Comments
 (0)