Files
openclaw/src/agents/tool-fs-policy.ts
Alex Knight 4aa08e9d79 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
2026-04-30 22:19:26 +10:00

61 lines
2.1 KiB
TypeScript

import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveAgentConfig } from "./agent-scope.js";
import { pickSandboxToolPolicy } from "./sandbox-tool-policy.js";
import { isToolAllowedByPolicies } from "./tool-policy-match.js";
import { mergeAlsoAllowPolicy, resolveToolProfilePolicy } from "./tool-policy.js";
export type ToolFsPolicy = {
workspaceOnly: boolean;
};
export function createToolFsPolicy(params: { workspaceOnly?: boolean }): ToolFsPolicy {
return {
workspaceOnly: params.workspaceOnly === true,
};
}
export function resolveToolFsConfig(params: { cfg?: OpenClawConfig; agentId?: string }): {
workspaceOnly?: boolean;
} {
const cfg = params.cfg;
const globalFs = cfg?.tools?.fs;
const agentFs =
cfg && params.agentId ? resolveAgentConfig(cfg, params.agentId)?.tools?.fs : undefined;
return {
workspaceOnly: agentFs?.workspaceOnly ?? globalFs?.workspaceOnly,
};
}
export function resolveEffectiveToolFsWorkspaceOnly(params: {
cfg?: OpenClawConfig;
agentId?: string;
}): boolean {
return resolveToolFsConfig(params).workspaceOnly === true;
}
export function resolveEffectiveToolFsRootExpansionAllowed(params: {
cfg?: OpenClawConfig;
agentId?: string;
}): boolean {
const cfg = params.cfg;
if (!cfg) {
return true;
}
const agentTools = params.agentId ? resolveAgentConfig(cfg, params.agentId)?.tools : undefined;
const globalTools = cfg.tools;
const profile = agentTools?.profile ?? globalTools?.profile;
const profileAlsoAllow = new Set(agentTools?.alsoAllow ?? globalTools?.alsoAllow ?? []);
const fsConfig = resolveToolFsConfig(params);
if (fsConfig.workspaceOnly === true) {
return false;
}
// tools.fs presence does not grant access; require profile or alsoAllow (#47487).
const profilePolicy = mergeAlsoAllowPolicy(
resolveToolProfilePolicy(profile),
profileAlsoAllow.size > 0 ? Array.from(profileAlsoAllow) : undefined,
);
const globalPolicy = pickSandboxToolPolicy(globalTools);
const agentPolicy = pickSandboxToolPolicy(agentTools);
return isToolAllowedByPolicies("read", [profilePolicy, globalPolicy, agentPolicy]);
}