Skip to content

fix(tools): filter MCP/LSP tools through subagent policy#53549

Open
Bartok9 wants to merge 1 commit intoopenclaw:mainfrom
Bartok9:fix/53504-mcp-tools-subagent-policy-filter
Open

fix(tools): filter MCP/LSP tools through subagent policy#53549
Bartok9 wants to merge 1 commit intoopenclaw:mainfrom
Bartok9:fix/53504-mcp-tools-subagent-policy-filter

Conversation

@Bartok9
Copy link
Copy Markdown
Contributor

@Bartok9 Bartok9 commented Mar 24, 2026

Summary

MCP and LSP tools were bypassing the tools.subagents.tools.allow/deny policy filtering because they were added to effectiveTools after the applyToolPolicyPipeline had already run.

Problem

When spawning a sub-agent with:

  • An MCP server providing many tools (e.g., 38 tools)
  • A small context window model (e.g., 16k tokens)
  • A restrictive tools.subagents.tools.allow policy (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:

400 This model's maximum context length is 16384 tokens.
However, your request has 16905 input tokens.

Root Cause

In both attempt.ts and compact.ts, effectiveTools was constructed by concatenating:

  • Built-in tools (filtered via pipeline ✅)
  • MCP tools (unfiltered ❌)
  • LSP tools (unfiltered ❌)
const effectiveTools = [
    ...tools,                            // ✅ filtered
    ...(bundleMcpRuntime?.tools ?? []),   // ❌ NOT filtered
    ...(bundleLspRuntime?.tools ?? [])    // ❌ NOT filtered
];

Solution

Added subagent policy resolution and filtering for MCP/LSP tools:

const subagentToolPolicy =
  params.sessionKey && isSubagentSessionKey(params.sessionKey)
    ? resolveSubagentToolPolicyForSession(params.config, params.sessionKey)
    : undefined;

const filteredMcpTools = subagentToolPolicy
  ? (bundleMcpRuntime?.tools ?? []).filter((tool) =>
      isToolAllowedByPolicyName(tool.name, subagentToolPolicy),
    )
  : (bundleMcpRuntime?.tools ?? []);
// ... same for LSP tools

const effectiveTools = [...tools, ...filteredMcpTools, ...filteredLspTools];

This applies the same filtering that built-in tools receive via the pipeline.

Testing

  • The existing pi-tools.policy.test.ts tests pass (28/28)
  • Manual verification: policy functions work correctly with tool names

Notes

  • This also affects agents.list[].tools.deny - agent-level deny lists now properly filter MCP/LSP tools as well
  • The fix is applied in both attempt.ts (run) and compact.ts (compaction) to ensure consistent behavior

Fixes #53504

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: S labels Mar 24, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 24, 2026

Greptile Summary

This PR fixes a policy bypass where MCP and LSP tools were concatenated into effectiveTools after the applyToolPolicyPipeline had already run for built-in tools, meaning tools.subagents.tools.allow/deny had no effect on MCP/LSP tool availability in sub-agent sessions. The fix applies resolveSubagentToolPolicyForSession + isToolAllowedByPolicyName 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.

  • The root cause (late concatenation of MCP/LSP tools after policy pipeline) is accurately diagnosed and fixed in both relevant code paths.
  • The filtering logic is consistent with the existing subagentPolicy step used for built-in tools — same function (resolveSubagentToolPolicyForSession), same matcher (isToolAllowedByPolicyName).
  • A minor style opportunity: pi-tools.policy.ts already exports a filterToolsByPolicy(tools, policy) helper that handles the undefined policy case internally, which could replace the manual ternary blocks in both files.
  • Note: other policy layers (global tools.allow, agent-specific tools.allow, group policy) still do not filter MCP/LSP tools, but this is a pre-existing gap outside the scope of this PR.

Confidence Score: 5/5

  • Safe to merge — the fix is targeted, correct, and consistent with existing policy machinery.
  • The change is a focused, well-understood fix: it applies the same resolveSubagentToolPolicyForSession call already used for built-in tools to the MCP and LSP tool lists, solving the specific context-window overflow problem described. Both the run path and compaction path are updated consistently. The only feedback is a non-blocking style suggestion to use the filterToolsByPolicy helper instead of inlining the same conditional. No logic or security concerns found.
  • No files require special attention.
Prompt To Fix All 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.

Reviews (1): Last reviewed commit: "fix(tools): filter MCP/LSP tools through..." | Re-trigger Greptile

Comment on lines +1873 to +1882
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 ?? []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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!

Copy link
Copy Markdown
Contributor

@fabianwilliams fabianwilliams left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) and compact.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 subagentToolPolicy is only resolved when isSubagentSessionKey returns true, so main-agent runs skip the overhead entirely — correct guard
  • Non-subagent sessions pass MCP/LSP tools through unfiltered (the undefined policy 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.

@fabianwilliams fabianwilliams self-assigned this Mar 24, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@Bartok9 Bartok9 force-pushed the fix/53504-mcp-tools-subagent-policy-filter branch from 29e7f47 to 19206c1 Compare March 28, 2026 08:35
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
@Bartok9 Bartok9 force-pushed the fix/53504-mcp-tools-subagent-policy-filter branch from 19206c1 to 8c7cc3b Compare March 31, 2026 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP tools bypass tools.subagents.tools.allow/deny policy filtering

3 participants