fix(tools): filter MCP/LSP tools through subagent policy#53549
fix(tools): filter MCP/LSP tools through subagent policy#53549Bartok9 wants to merge 1 commit intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes a policy bypass where MCP and LSP tools were concatenated into
Confidence Score: 5/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 1873-1882
Comment:
**Use `filterToolsByPolicy` helper instead of inlining**
`pi-tools.policy.ts` already exports a `filterToolsByPolicy` helper that handles both the `undefined` policy case and the name-based filtering in one call. Using it here avoids the manual ternary and keeps the code consistent with how the pipeline applies the same logic elsewhere.
```suggestion
const filteredMcpTools = filterToolsByPolicy(bundleMcpRuntime?.tools ?? [], subagentToolPolicy);
const filteredLspTools = filterToolsByPolicy(bundleLspRuntime?.tools ?? [], subagentToolPolicy);
```
You would also need to add `filterToolsByPolicy` to the import from `../../pi-tools.policy.js`. The same pattern applies to the identical block in `compact.ts`.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(tools): filter MCP/LSP tools through..." | Re-trigger Greptile |
| const filteredMcpTools = subagentToolPolicy | ||
| ? (bundleMcpRuntime?.tools ?? []).filter((tool) => | ||
| isToolAllowedByPolicyName(tool.name, subagentToolPolicy), | ||
| ) | ||
| : (bundleMcpRuntime?.tools ?? []); | ||
| const filteredLspTools = subagentToolPolicy | ||
| ? (bundleLspRuntime?.tools ?? []).filter((tool) => | ||
| isToolAllowedByPolicyName(tool.name, subagentToolPolicy), | ||
| ) | ||
| : (bundleLspRuntime?.tools ?? []); |
There was a problem hiding this comment.
Use
filterToolsByPolicy helper instead of inlining
pi-tools.policy.ts already exports a filterToolsByPolicy helper that handles both the undefined policy case and the name-based filtering in one call. Using it here avoids the manual ternary and keeps the code consistent with how the pipeline applies the same logic elsewhere.
| const filteredMcpTools = subagentToolPolicy | |
| ? (bundleMcpRuntime?.tools ?? []).filter((tool) => | |
| isToolAllowedByPolicyName(tool.name, subagentToolPolicy), | |
| ) | |
| : (bundleMcpRuntime?.tools ?? []); | |
| const filteredLspTools = subagentToolPolicy | |
| ? (bundleLspRuntime?.tools ?? []).filter((tool) => | |
| isToolAllowedByPolicyName(tool.name, subagentToolPolicy), | |
| ) | |
| : (bundleLspRuntime?.tools ?? []); | |
| const filteredMcpTools = filterToolsByPolicy(bundleMcpRuntime?.tools ?? [], subagentToolPolicy); | |
| const filteredLspTools = filterToolsByPolicy(bundleLspRuntime?.tools ?? [], subagentToolPolicy); |
You would also need to add filterToolsByPolicy to the import from ../../pi-tools.policy.js. The same pattern applies to the identical block in compact.ts.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 1873-1882
Comment:
**Use `filterToolsByPolicy` helper instead of inlining**
`pi-tools.policy.ts` already exports a `filterToolsByPolicy` helper that handles both the `undefined` policy case and the name-based filtering in one call. Using it here avoids the manual ternary and keeps the code consistent with how the pipeline applies the same logic elsewhere.
```suggestion
const filteredMcpTools = filterToolsByPolicy(bundleMcpRuntime?.tools ?? [], subagentToolPolicy);
const filteredLspTools = filterToolsByPolicy(bundleLspRuntime?.tools ?? [], subagentToolPolicy);
```
You would also need to add `filterToolsByPolicy` to the import from `../../pi-tools.policy.js`. The same pattern applies to the identical block in `compact.ts`.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
fabianwilliams
left a comment
There was a problem hiding this comment.
This is a well-scoped security fix. The bypass is real — anyone using MCP servers with subagent tool policies would have their policy silently ignored for MCP/LSP tools.
What's good:
- Root cause is clearly documented in the PR description with the exact code path that bypassed filtering
- Fix is applied consistently in both
attempt.ts(run) andcompact.ts(compaction) — important since PR #53468 just showed us that compaction can diverge from the run path if you forget to thread context through - The
subagentToolPolicyis only resolved whenisSubagentSessionKeyreturns true, so main-agent runs skip the overhead entirely — correct guard - Non-subagent sessions pass MCP/LSP tools through unfiltered (the
undefinedpolicy fallback), which preserves existing behavior for top-level agents
One observation:
The filtering logic for MCP and LSP tools is identical — both do .filter((tool) => isToolAllowedByPolicyName(tool.name, subagentToolPolicy)). Consider extracting a small helper to DRY this up:
const filterByPolicy = (tools: Tool[]) =>
subagentToolPolicy
? tools.filter((t) => isToolAllowedByPolicyName(t.name, subagentToolPolicy))
: tools;
const effectiveTools = [
...tools,
...filterByPolicy(bundleMcpRuntime?.tools ?? []),
...filterByPolicy(bundleLspRuntime?.tools ?? []),
];This would also reduce duplication between attempt.ts and compact.ts since the same block is copy-pasted. Not a blocker — the fix is correct as-is.
Re: testing — the existing pi-tools.policy.test.ts covers the policy functions, but there's no integration test proving MCP tools are actually filtered in a subagent session. Would be good to add one, but also not a blocker for merging the fix.
LGTM — this is a real policy enforcement gap worth closing.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 29e7f4714f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| : undefined; | ||
|
|
||
| // Filter MCP/LSP tools through subagent policy (same filtering applied to built-in tools) | ||
| const filteredMcpTools = filterToolsByPolicy(bundleMcpRuntime?.tools ?? [], subagentToolPolicy); |
There was a problem hiding this comment.
Reserve LSP names from policy-filtered MCP tools
The new filtering happens after createBundleLspToolRuntime is initialized, so reservedToolNames still includes MCP tools that were later removed by subagent policy. Because createBundleLspToolRuntime skips any tool whose name is already reserved, an allowed LSP tool can be dropped if it collides with a denied MCP tool name, leaving neither tool available. This regression appears in the same pattern here and in the compaction path; reserve names from the filtered MCP set (or filter before LSP runtime creation) to avoid hiding valid LSP tools.
Useful? React with 👍 / 👎.
29e7f47 to
19206c1
Compare
When subagent sessions use MCP or LSP tools, those tools were bypassing the tools.subagents.tools.allow/deny policy because they were concatenated into effectiveTools after the applyToolPolicyPipeline had already run. Apply resolveSubagentToolPolicyForSession + filterToolsByPolicy to MCP/LSP tools in both the run path (attempt.ts) and the compaction path (compact.ts), which correctly mirrors how the subagentPolicy pipeline step filters built-in tools in pi-tools.ts. Fixes openclaw#53504
19206c1 to
8c7cc3b
Compare
Summary
MCP and LSP tools were bypassing the
tools.subagents.tools.allow/denypolicy filtering because they were added toeffectiveToolsafter theapplyToolPolicyPipelinehad already run.Problem
When spawning a sub-agent with:
tools.subagents.tools.allowpolicy (e.g., only 3 tools)The sub-agent would receive all 38 unfiltered MCP tools anyway, consuming ~12-14k tokens of schema just from MCP tools. This could exceed the model's context window:
Root Cause
In both
attempt.tsandcompact.ts,effectiveToolswas constructed by concatenating:Solution
Added subagent policy resolution and filtering for MCP/LSP tools:
This applies the same filtering that built-in tools receive via the pipeline.
Testing
pi-tools.policy.test.tstests pass (28/28)Notes
agents.list[].tools.deny- agent-level deny lists now properly filter MCP/LSP tools as wellattempt.ts(run) andcompact.ts(compaction) to ensure consistent behaviorFixes #53504