feat(ai-hook): enforce MCP permissions natively - #1405
Open
amascia-gg wants to merge 2 commits into
Open
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1405 +/- ##
=======================================
Coverage 93.98% 93.98%
=======================================
Files 200 200
Lines 12638 12638
=======================================
Hits 11878 11878
Misses 760 760
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
amascia-gg
force-pushed
the
amascia/ggshield-rust-dispatcher
branch
from
August 12, 2026 11:11
29e2b83 to
9f67833
Compare
amascia-gg
force-pushed
the
amascia/rust-mcp-policy
branch
3 times, most recently
from
August 12, 2026 12:02
21684c5 to
9b4b3b4
Compare
amascia-gg
force-pushed
the
amascia/ggshield-rust-dispatcher
branch
from
August 12, 2026 12:32
365a641 to
614848c
Compare
amascia-gg
force-pushed
the
amascia/rust-mcp-policy
branch
from
August 12, 2026 13:02
346caf2 to
57e8096
Compare
amascia-gg
force-pushed
the
amascia/ggshield-rust-dispatcher
branch
from
August 12, 2026 13:23
614848c to
03b286a
Compare
amascia-gg
force-pushed
the
amascia/rust-mcp-policy
branch
from
August 12, 2026 13:23
57e8096 to
fa44941
Compare
amascia-gg
force-pushed
the
amascia/ggshield-rust-dispatcher
branch
2 times, most recently
from
August 17, 2026 09:37
2eb452c to
0610e72
Compare
amascia-gg
force-pushed
the
amascia/rust-mcp-policy
branch
3 times, most recently
from
August 18, 2026 12:16
3ef4c4b to
b116fcc
Compare
amascia-gg
force-pushed
the
amascia/ggshield-rust-dispatcher
branch
from
August 18, 2026 12:38
5b4f75b to
2e6ea62
Compare
amascia-gg
force-pushed
the
amascia/rust-mcp-policy
branch
from
August 18, 2026 12:39
b116fcc to
84490dc
Compare
amascia-gg
force-pushed
the
amascia/ggshield-rust-dispatcher
branch
from
August 18, 2026 13:03
2e6ea62 to
0dfa9d9
Compare
amascia-gg
force-pushed
the
amascia/rust-mcp-policy
branch
from
August 18, 2026 13:06
84490dc to
3464160
Compare
The native hook only scanned an MCP tool call's arguments for secrets; the call itself went unchecked, so a tool the organization denies ran anyway. It now reports every MCP tool call to /v1/agent-activity/mcp-activity and honors the verdict, like mcp.py. Splitting a mangled tool name back into (server, tool) is a lookup against ai_discovery.json rather than a split: every agent joins and mangles the two halves its own way, and either half can contain the separator. A new `ggshield-discovery` crate reads that inventory, and when two configuration names mangle to the same key the last one wins, as Python's dict build does. The inventory is deliberately not fail-open: without a fresh one the whole event is handed to the bundled Python implementation, which owns the walk. Allowing instead would make deleting one cache file enough to lift an administrator's block. An unreadable token warns rather than allows, since an argument-less MCP call has nothing else that needs the credential. Precedence, where this deviates from `_scan_payloads()`: a secret and a denial are settled per payload, the earlier index winning and a tie going to the scan. A chunk the API refused — the fail-open `Err` — is reported only when nothing blocks, so a denial already in hand is not thrown away by a later chunk that could not be scanned. Python overlaps the two requests on a thread pool and can lose the denial that way; keeping it is strictly safer, and unobservable from the equivalence gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Formatting only: the MCP inventory fixture came out of a merge with lines the formatter splits. `Lint package` was failing on `black` alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
amascia-gg
force-pushed
the
amascia/rust-mcp-policy
branch
from
August 20, 2026 08:44
3464160 to
4bfdbdc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1392. Addresses @clement-tourriere's comment there: "org denies
mcp__github__delete_repository, the args have no secret, multiscan is clean, we allow. IMO this needs to be implemented, not skipped."What it does
The native hook now asks both questions an MCP tool call raises, not one:
/v1/multiscan— already there;POST /v1/agent-activity/mcp-activity— new. A{"allowed": false, "reason": ...}blocks with that reason; the scan verdict wins per payload, in payload order, as in_scan_payloads(). A failed request still allows, matchingmcp.py.MCP telemetry therefore reaches the dashboard again.
Why the discovery walk is not ported
parse_mcp_activityneeds onlyai_config.userandai_config.servers, and both already sit in theai_discovery.jsonthat Python writes with a one hour TTL. So the ~2,700-line walk stays in Python and the newggshield-discoverycrate just reads what it left: the inventory, its freshness check (same TTL and skew tolerance ascache.py), and the per-agent server-name resolution.With a stale or absent inventory the event is declined to
ggshield-pyvia the existingOutcome::Delegate, which walks, submits and rewrites the file — so the next hour runs natively. Never allowed: failing open there would let anyone lift an administrator's block by deleting a cache file. Fail-open stays correct only for a failed request.Full native discovery is future work, behind the crate boundary —
ggshield ai discover, the history backfill and agent activity all read the same file.Notes
Tool::Mcpflows through this path whatever the agent.ggshield-pyexit 0 with no verdict.Verification
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace(138 tests) all clean. The equivalence harness printsRESULT: EQUIVALENT, now including a newmcpmatrix — 5 agents x {permitted, denied}, comparing the verdict and the activity report body, which is what proves the server name each dialect resolves to is the same on both sides.