Skip to content

fix(langchain): enforce the tool allowlist in handleToolStart - #6

Merged
aymandakirgh merged 1 commit into
mainfrom
fix/langchain-tool-allowlist
Jun 18, 2026
Merged

fix(langchain): enforce the tool allowlist in handleToolStart#6
aymandakirgh merged 1 commit into
mainfrom
fix/langchain-tool-allowlist

Conversation

@aymandakirgh

Copy link
Copy Markdown
Owner

Why

aegisCallbackHandler({ allowedTools: [...] }) accepted an allowlist but never enforced it. handleToolStart only ran an OUTPUT-scope content scan of the tool input, while the LLM08 tool-call-OOB allowlist guard and the tool-only SSRF patterns run only under TOOL scope. A user who set allowedTools on the LangChain handler reasonably expected tool gating, but every out-of-allowlist tool call passed through silently.

Reproduction on main (before): aegisCallbackHandler({ allowedTools: ['search'] }).handleToolStart({}, 'delete_db') resolved without blocking — allowlist ignored.

What

When an allowlist is configured (via options.allowedTools, policy.allowedTools, or the ALLOWED_TOOLS env var), handleToolStart now runs a TOOL-scope scan of the tool identity and throws AegisBlockedError("tool", ...) on a violation.

  • Tool identity is resolved from LangChain's serialized tool descriptor (.name, or the last segment of .id), falling back to the input string when the descriptor has no name.
  • The allowlist is only enforced when one is configured — an empty list fails closed and would otherwise block every tool, so unguarded chains are unaffected (no regression).
  • The existing OUTPUT-scope scan of tool arguments (dangerous commands / secrets) is preserved.
  • No new dependencies; the adapter stays a zero-@langchain/*-dependency structural shim.

Verification

  • npm run typecheck — clean
  • npm run lint — clean
  • npm test — 319 passed / 1 skipped (added 10 regression tests in tests/langchain.test.ts)
  • Probe (node/tsx) before vs after:
    • handleToolStart({}, "delete_db") → BLOCKED (tool) TOOL_CALL_OOB
    • handleToolStart({}, "search") → passes
    • handleToolStart({ name: "delete_db" }, args) → BLOCKED
    • handleToolStart({ name: "search" }, args) → passes
    • no allowlist configured → tool calls pass; dangerous args still blocked under output scope

🤖 Generated with Claude Code

WHY: `aegisCallbackHandler({ allowedTools: [...] })` accepted an
allowlist but never enforced it. handleToolStart only ran an
OUTPUT-scope content scan, so the LLM08 tool-call-OOB allowlist guard
(and the tool-only SSRF patterns) — which run only under TOOL scope —
never executed. A user who set allowedTools reasonably expected tool
gating, but every out-of-allowlist tool call passed through silently.

WHAT: When an allowlist is configured (via options, policy, or the
ALLOWED_TOOLS env var), handleToolStart now runs a TOOL-scope scan of
the tool identity, throwing AegisBlockedError("tool", ...) on a
violation. The identity is resolved from LangChain's serialized tool
descriptor (`.name`, or the last segment of `.id`), falling back to the
input string when the descriptor carries no name. The allowlist is only
enforced when one is configured — an empty list fails closed and would
otherwise block every tool, so unguarded chains are unaffected. The
existing OUTPUT-scope scan of tool arguments (dangerous commands /
secrets) is preserved. Adds regression tests covering descriptor-name,
id-path, input-fallback, policy-supplied allowlists, and the
no-allowlist no-regression path.

Co-authored-by: mattia-mamini-gh <281593356+mattia-mamini-gh@users.noreply.github.com>
@aymandakirgh
aymandakirgh merged commit bff33e8 into main Jun 18, 2026
3 checks passed
@aymandakirgh
aymandakirgh deleted the fix/langchain-tool-allowlist branch June 18, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant