Skip to content

feat(aauth): MCP tool-level grant authorization + enforcement proxy (#26)#42

Open
markmhendrickson wants to merge 1 commit into
feat/aauth-keypair-revocationfrom
feat/26-mcp-tool-grants
Open

feat(aauth): MCP tool-level grant authorization + enforcement proxy (#26)#42
markmhendrickson wants to merge 1 commit into
feat/aauth-keypair-revocationfrom
feat/26-mcp-tool-grants

Conversation

@markmhendrickson
Copy link
Copy Markdown
Owner

Closes #26.

Stacked PR. Base is feat/aauth-keypair-revocation (PR #41), since this builds on the grant_checker.py introduced there. Merge #41 first, or retarget this to main after #41 lands.

What

Extends agent_grant to gate arbitrary MCP tool calls — across any server, not just Neotoma entity operations.

grant_checker.py

  • Bug fix: _parse now matches the live agent_grant schema — identity via match_sub (was filtering on a nonexistent aauth_sub snapshot field) and capabilities as an object-array (was assuming strings). Without this, GrantChecker would never have matched a real grant.
  • Normalizes capabilities into ops (set of op strings) + tool_grants (map of <server>:<tool> → constraint dict).
  • AgentGrant.tool_constraints(server, tool) with tool:<server>:* and tool:* wildcard support.
  • GrantChecker.check_tool(server, tool)(allowed, constraints), permissive fallback when no grant declares any tool capability (un-migrated agents keep working; migrated agents get hard enforcement).
  • check_param_constraints(constraints, params) — enforces tables, max_amount_sats, to_allowlist, max_<field>, allowed_<field>; unknown keys ignored (forward-compatible).

mcp_tool_grant_proxy/proxy.py

  • Generic stdio MCP interceptor between claude --print and a downstream MCP server.
  • Forwards all JSON-RPC except tools/call, which it gates against the grant. Denied calls get a synthesized isError result and never reach the downstream tool.
  • Emits tool_call_observation (result: allowed | denied) to Neotoma for a unified cross-MCP audit trail.
  • README + launch config in the directory.

docs/aauth.md

Grant op format

{ "op": "tool:<server>:<tool>", "param_constraints": { "max_amount_sats": 500000 } }

Absent = denied. {} = unconstrained. tool:<server>:* / tool:* wildcards.

Verification

  • lib/daemon_runtime/test_grant_checker.py — 13/13 passing (parsing, tool lookup, wildcards, all constraint types, suspended-grant deny).
  • execution/mcp/mcp_tool_grant_proxy/test_proxy_smoke.py — 3/3 passing (advisory passthrough, non-tool passthrough, in-process deny path incl. constraint violation + deny-response shape).
  • All modules pass ast.parse. HEAD commit GPG-signed.

Follow-ups (from the issue's task list, not in this PR)

  • Register tool_call_observation schema in Neotoma (proxy writes it best-effort today; schema registration formalizes it).
  • Wire Anthus to pass ATELES_AGENT_SUB / ATELES_AGENT_GRANT_ID through dispatch subprocess env.
  • Instrument the owned parquet MCP server with option-A enforcement for defence-in-depth.
  • Derive/tighten capabilities.tools from allowed_tools for the 12 product-panel agents.

🤖 Generated with Claude Code

)

Extend agent_grant to gate arbitrary MCP tool calls, not just Neotoma ops.

grant_checker.py:
- Fix _parse to match the live agent_grant schema: identity via match_sub
  (was incorrectly filtering on aauth_sub), capabilities as an object-array
  (was assuming strings). Normalises into `ops` (set) + `tool_grants` (map).
- AgentGrant.tool_constraints(server, tool): resolve "<server>:<tool>" grant
  with server/global wildcard support.
- GrantChecker.check_tool(server, tool): returns (allowed, constraints) with
  permissive fallback when no grant declares any tool capability (un-migrated
  agents keep working; migrated agents get hard enforcement).
- check_param_constraints(constraints, params): enforces tables,
  max_amount_sats, to_allowlist, max_<field>, allowed_<field>; unknown keys
  ignored (forward-compatible).
- 13 unit tests (test_grant_checker.py), all passing.

mcp_tool_grant_proxy/proxy.py:
- Generic stdio MCP interceptor between `claude --print` and a downstream MCP
  server. Forwards all JSON-RPC except tools/call, which it gates against the
  agent_grant. Denied calls get a synthesized isError result and never reach
  the downstream tool. Emits tool_call_observation (allowed|denied) for a
  unified cross-MCP audit trail.
- README + 3 smoke tests (advisory passthrough, non-tool passthrough, deny
  path), all passing.

docs/aauth.md:
- Promote tool-level authz from "planned" to implemented; add the
  "Tool-level authorization (issue #26)" section (grant shape, enforcement,
  permissive-fallback semantics).

Grant op format: "tool:<server>:<tool>" with optional param_constraints.
Absent = denied; {} = unconstrained; tool:<server>:* and tool:* wildcards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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