Skip to content

feat(#608): diagnose un-normalized tool names in allowlist hook - #6520

Merged
waynesun09 merged 1 commit into
mainfrom
agent/608-tool-allowlist-diagnostic
Aug 23, 2026
Merged

feat(#608): diagnose un-normalized tool names in allowlist hook#6520
waynesun09 merged 1 commit into
mainfrom
agent/608-tool-allowlist-diagnostic

Conversation

@fullsend-ai-coder

@fullsend-ai-coder fullsend-ai-coder Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Runtime adapters must translate their tool names to Claude vocabulary before the sandbox hooks run (ADR 0090); the pi adapter does. When one does not — an extension tool, a future adapter without a map, a pi version that adds a tool — tool_allowlist_pretool.py blocked the call with the same NOT in the allowlist reason as a genuinely forbidden tool, hiding the adapter gap behind a plausible allowlist denial. This PR makes that gap diagnosable without weakening the fail-closed, exact-match allowlist — no case-insensitive allowing.

Closes #608

Changes

  • internal/security/hooks/tool_allowlist_pretool.py — a blocked name that equals an allowlisted entry case-insensitively is reported as a normalization gap with an ALLOWLIST_HOOK_ERROR reason that says which side is wrong:
    • allowlist entry is a canonical/legacy Claude name (bash vs Bash, ls vs LS) → "the runtime adapter must translate it", finding tool_name_unnormalized (high, block);
    • tool name is the Claude one but the allowlist is not (Bash vs bash) → "FULLSEND_TOOL_ALLOWLIST entry 'bash' is not canonical … fix the allowlist", finding allowlist_entry_unnormalized (high, block);
    • neither is Claude vocabulary → says so, blames neither, finding tool_name_case_collision.
      MCP names (mcp__*) are matched verbatim and keep the plain tool_blocked (critical) path. A non-string tool_name, or a JSON payload that is not an object ([]/null/123 — pre-existing gap), now blocks with the JSON contract instead of a Python traceback. The three new findings are high, not critical, so an adapter gap does not trip critical-keyed escalation the way a forbidden tool does. Uses casefold(); when several entries fold together a known Claude name wins.
  • internal/security/canonical_tools.goCanonicalClaudeTools lists the tool names Claude Code exposes, verified 2026-08-23 against the live Claude Code tools reference (latest release, 2.1.241 at the time; the CHANGELOG records no tool additions/removals since the 2.1.234 pinned in the sandbox image — re-check on every pin bump); LegacyClaudeTools lists names agents and adapters still use (LS, MultiEditEdit, TaskAgent, TodoRead, NotebookReadRead); KnownClaudeTool(). The hook carries a mirror of both sets (scripts cannot reach Go).
  • TestsTestToolAllowlistHook_VocabularyMatchesGo parses the embedded script and fails on any drift between the Go and Python sets; TestHookPlan_ToolsAreCanonical pins every HookPlan tool (defaults and every hook enabled); pythonNameSet ignores commented-out entries (own fixture test); TestCanonicalClaudeTools_WellFormed (no (/,, no mcp__, canonical ∩ legacy = ∅, legacy replacements canonical); internal/runtime/pi_agent_test.go checks the pi maps in both directions and that every name is canonical-or-legacy Claude vocabulary — a deliberate relaxation of the issue's "canonical only" wording, because pi's ls maps to LS, which Claude Code no longer sends (the MultiEdit skip is now documented as a renaming gap the case-variant diagnostic cannot see). Python: 26 cases covering every branch via subprocess plus in-process assertions on the findings.jsonl records (name/severity/action) for each path.
  • Docsdocs/runtimes.md hook-contract bullet and the ADR 0090 note describe the vocabulary as a test-checked reference (not run-time validation), the MCP caveat, and the renaming gap.

Not changed: FULLSEND_TOOL_ALLOWLIST parsing, fail-closed-when-unset, the pi alias maps in pi_agent.go.

Testing

  • pytest internal/security/hooks/tool_allowlist_pretool_test.py — 26 pass
  • go test ./internal/security/ ./internal/runtime/ — pass; mutation check: adding a bogus name to the Python set fails TestToolAllowlistHook_VocabularyMatchesGo
  • go vet, gofmt, ruff check/format, ty, bandit, ADR + docs-link lints (pre-commit on changed files) — clean

Post-script verification

  • Branch is not main/master (agent/608-tool-allowlist-diagnostic)
  • Secret scan passed (gitleaks — 997021c226272037a8cb69b32c73e14f26073b5e..HEAD)
  • PR body secret scan passed (gitleaks — no-git)

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown

Site preview

Preview: https://8fccba3e-site.fullsend-ai.workers.dev

Commit: 5d908d6efe3baeba09b2dbcc3c784251b1fa85aa

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 23, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:53 PM UTC · Completed 1:07 PM UTC

Commit: 6b68a6e · View workflow run →

@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 23, 2026

Copy link
Copy Markdown

Looks good to me

Previous run

Review

Findings

Low

  • [consumer completeness] internal/security/canonical_tools_test.go:10 — The test for CanonicalClaudeTools only asserts the map is non-empty. There is no test verifying that the hardcoded tool names in security.HookPlan (e.g., "Bash", "WebFetch" in hooks.go) are members of CanonicalClaudeTools. If a future change adds a new HookGroup with a misspelled tool name, nothing catches it until runtime.
    Remediation: Consider adding a test that iterates HookPlan's groups and asserts each non-wildcard tool name is in CanonicalClaudeTools, similar to the pi_agent_test.go pattern.

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the ready-for-merge All reviewers approved — ready to merge label Aug 23, 2026
@waynesun09
waynesun09 force-pushed the agent/608-tool-allowlist-diagnostic branch from 6b68a6e to 3cbd90d Compare August 23, 2026 13:16
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 23, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 1:18 PM UTC · Ended 1:28 PM UTC

Commit: 3cbd90d · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 23, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 1:30 PM UTC · Ended 1:36 PM UTC

Commit: 7688769 · View workflow run →

Runtime adapters must translate their tool names to Claude vocabulary
before the sandbox hooks run (ADR 0090); the pi adapter does. When one
does not — an extension tool, a future adapter without a map, a pi
version that adds a tool — the call was blocked with the same "NOT in
the allowlist" reason as a genuinely forbidden tool, hiding the adapter
gap behind a plausible allowlist denial.

- tool_allowlist_pretool.py stays exact-match and fail-closed (no
  case-insensitive allowing). A blocked name that equals an allowlisted
  entry case-insensitively is now reported as a normalization gap with
  an ALLOWLIST_HOOK_ERROR reason that says which side is wrong: the
  runtime adapter (tool_name_unnormalized finding, severity high) when
  the allowlist entry is a canonical or legacy Claude name, the
  allowlist (allowlist_entry_unnormalized) when the tool name is the
  Claude one, and tool_name_case_collision when neither is. MCP names
  (mcp__*) are matched verbatim and keep the plain tool_blocked path. A
  non-string tool_name, or a JSON payload that is not an object, blocks
  with the JSON contract instead of a traceback.
- security.CanonicalClaudeTools records the Claude Code tool names
  (verified 2026-08-23 against the live tools reference — latest
  release 2.1.241; no tool changes in the CHANGELOG since the pinned
  2.1.234) and
  security.LegacyClaudeTools the names agents and adapters still use
  (LS, MultiEdit, Task, ...). The hook carries a mirror of both sets;
  TestToolAllowlistHook_VocabularyMatchesGo keeps them identical,
  TestHookPlan_ToolsAreCanonical pins every HookPlan tool (defaults and
  all hooks enabled), and the pi map tests check both directions and
  that every name is canonical-or-legacy Claude vocabulary (ls -> LS is
  a deliberate relaxation of "canonical only"). The pi maps themselves
  are untouched.
- Tests cover every diagnostic branch end-to-end and, in-process, the
  findings.jsonl records (name/severity/action) for each path.
- docs/runtimes.md and ADR 0090 describe the vocabulary as a
  test-checked reference (not runtime validation), the MCP caveat, and
  the renaming gap (MultiEdit vs Edit under pi) the case-variant
  diagnostic cannot see.

Closes #608

Assisted-by: Claude (code, fix), Grok (review)
Signed-off-by: Wayne Sun <gsun@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 23, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:38 PM UTC · Completed 1:55 PM UTC

Commit: 5d908d6 · View workflow run →

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Verified: internal/security tests pass; the hook this PR modifies (tool_allowlist_pretool.py) is opt-in and disabled fleet-wide today (grepped all fullsend-ai/agents harnesses — zero enable it), so this is currently pure diagnostics/test-coverage with no live runtime effect. Force-enabled it locally (security.sandbox_hooks.tool_allowlist_pretool.enabled + FULLSEND_TOOL_ALLOWLIST) and re-ran the triage agent under the pi runtime end-to-end: hook roster confirmed active, pi's bash/read tool calls passed cleanly through the new normalization check with zero tool_name_unnormalized/allowlist_entry_unnormalized/tool_name_case_collision findings. CI green, review thread resolved.

@waynesun09
waynesun09 added this pull request to the merge queue Aug 23, 2026
Merged via the queue into main with commit 5b23b4f Aug 23, 2026
52 checks passed
@waynesun09
waynesun09 deleted the agent/608-tool-allowlist-diagnostic branch August 23, 2026 14:38
@fullsend-ai-retro

fullsend-ai-retro Bot commented Aug 23, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 2:40 PM UTC · Completed 2:52 PM UTC

Commit: 5d908d6 · View workflow run →

@fullsend-ai-retro

Copy link
Copy Markdown

Retro: PR #6520 — Diagnose Un-normalized Tool Names in Allowlist Hook

Workflow Timeline

Time (UTC) Stage Event
12:33 Triage /fs-triage on issue #608 → agent confirms prerequisites, proposes test cases (run 32639741176)
12:41 Code /fs-code with detailed constraints → 7-file implementation in ~9 min (run 32640113463)
12:52 PR opened agent/608-tool-allowlist-diagnostic → PR #6520
12:52–13:07 Review #1 APPROVED with 1 Low finding: HookPlan tool names not tested against CanonicalClaudeTools (run 32640633663)
13:16–13:28 Review #2 Cancelled — superseded by force-push (run 32641863246)
13:28–13:36 Review #3 Cancelled — superseded by force-push (run 32642443289)
13:36–13:55 Review #4 APPROVED — prior finding resolved (run 32642847696)
14:38 Human review waynesun09 APPROVED with E2E verification (force-enabled hook, ran triage agent under pi runtime, confirmed zero false findings)
14:38 Merged Squash-merged to main

What Went Well

  • Efficient code agent: Comprehensive implementation across Go, Python, and Markdown (7 files, 26+ Python test cases, Go cross-validation tests, docs) in ~9 minutes.
  • Actionable review finding: Review agent caught a legitimate consumer-completeness gap (missing HookPlan conformance test) that neither the code agent nor triage agent anticipated.
  • Single code iteration: Code agent's output needed only one incremental improvement, not a rework.
  • Thorough human verification: Reviewer force-enabled the opt-in hook locally and ran a real triage agent through the pi runtime — integration-level validation the review agent cannot perform.
  • Full test coverage: Codecov confirmed all modified lines covered.

Observed Inefficiencies (Covered by Existing Issues)

  • 2 cancelled review runs (~20 min CI compute) from rapid force-pushes during manual fixes. Evidence for #4069 and #4960.
  • 4 skipped dispatch runs triggered by the bot's own pull_request_review events. Evidence for #5967 and #2994.

Agents Repo

All agents resolved from fullsend-ai/agents@main (commit a0a2c25fac3c).

Proposals filed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-merge All reviewers approved — ready to merge ready-for-review Triggers review agent dispatch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tool allowlist: diagnose un-normalized (non-Claude-vocabulary) tool names from runtime adapters

1 participant