feat(rfc008): Python SCOPE_INSUFFICIENT + structured rejection metadata#20
Merged
Conversation
…ction - Add DenyReason.SCOPE_INSUFFICIENT to types.py - Regenerate proto gen/ with new MCP_DENY_REASON_SCOPE_INSUFFICIENT - Add TOOL_SCOPE_INSUFFICIENT alias to __init__.py - Add SCOPE_INSUFFICIENT to deny_reason_map in guard.py - Extend GuardResult with error_code/requested_capability/presented_capability - Extend GuardError with structured rejection kwargs - Propagate rejection fields from proto response through to GuardError - Add structured deny logging (capiscio.policy_enforced) in both @guard (async) and @guard_sync decorators Part of RFC-008 Capability Class implementation (PR B — DENY path).
|
✅ Integration tests passed! capiscio-core gRPC tests working. |
There was a problem hiding this comment.
Pull request overview
Implements the RFC-008 DENY path in capiscio_mcp by adding a new SCOPE_INSUFFICIENT deny reason and propagating structured rejection metadata (error code + requested/presented capability) through the guard result/error surface so consumers can react programmatically to capability-class policy denials.
Changes:
- Add
DenyReason.SCOPE_INSUFFICIENTand map protoTOOL_SCOPE_INSUFFICIENTto it in guard evaluation. - Extend
GuardResultandGuardErrorto carryerror_code,requested_capability, andpresented_capability, and log them on DENY. - Regenerate protobuf Python modules to include new response fields and add a compatibility alias for
TOOL_SCOPE_INSUFFICIENT.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| capiscio_mcp/types.py | Adds DenyReason.SCOPE_INSUFFICIENT enum value for RFC-008. |
| capiscio_mcp/guard.py | Adds structured rejection metadata to GuardResult, maps new deny reason, logs/raises GuardError with metadata. |
| capiscio_mcp/errors.py | Extends GuardError to store structured rejection metadata fields. |
| capiscio_mcp/_proto/gen/capiscio/v1/mcp_pb2.py | Regenerated proto with new response fields / enum value (but import-time collision handling changed). |
| capiscio_mcp/_proto/capiscio/v1/mcp_pb2.py | Regenerated mirrored proto module with new fields/enum value. |
| capiscio_mcp/_proto/capiscio/v1/init.py | Adds TOOL_SCOPE_INSUFFICIENT compatibility alias. |
Comment on lines
+80
to
+82
| error_code: Optional[str] = None, | ||
| requested_capability: Optional[str] = None, | ||
| presented_capability: Optional[str] = None, |
| "'capiscio.v1.EvaluateToolAccessRequest' not found." | ||
| ) from lookup_err | ||
| DESCRIPTOR = file_descriptor | ||
| DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63\x61piscio/v1/mcp.proto\x12\x0b\x63\x61piscio.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xbe\x03\n\x19\x45valuateToolAccessRequest\x12\x11\n\ttool_name\x18\x01 \x01(\t\x12\x13\n\x0bparams_hash\x18\x02 \x01(\t\x12\x15\n\rserver_origin\x18\x03 \x01(\t\x12\x13\n\tbadge_jws\x18\x04 \x01(\tH\x00\x12\x11\n\x07\x61pi_key\x18\x05 \x01(\tH\x00\x12\x16\n\x0epolicy_version\x18\x06 \x01(\t\x12+\n\x06\x63onfig\x18\x07 \x01(\x0b\x32\x1b.capiscio.v1.EvaluateConfig\x12\x18\n\x10\x65nforcement_mode\x18\x08 \x01(\t\x12\x18\n\x10\x63\x61pability_class\x18\n \x01(\t\x12\x13\n\x0b\x65nvelope_id\x18\x0b \x01(\t\x12\x18\n\x10\x64\x65legation_depth\x18\x0c \x01(\x05\x12\x18\n\x10\x63onstraints_json\x18\r \x01(\t\x12\x1f\n\x17parent_constraints_json\x18\x0e \x01(\t\x12\"\n\x15\x64\x65ny_on_unknown_class\x18\x0f \x01(\x08H\x01\x88\x01\x01\x42\x13\n\x11\x63\x61ller_credentialB\x18\n\x16_deny_on_unknown_classJ\x04\x08\t\x10\n\"t\n\x0e\x45valuateConfig\x12\x17\n\x0ftrusted_issuers\x18\x01 \x03(\t\x12\x17\n\x0fmin_trust_level\x18\x02 \x01(\x05\x12\x19\n\x11\x61\x63\x63\x65pt_level_zero\x18\x03 \x01(\x08\x12\x15\n\rallowed_tools\x18\x04 \x03(\t\"\xbd\x04\n\x1a\x45valuateToolAccessResponse\x12*\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32\x18.capiscio.v1.MCPDecision\x12/\n\x0b\x64\x65ny_reason\x18\x02 \x01(\x0e\x32\x1a.capiscio.v1.MCPDenyReason\x12\x13\n\x0b\x64\x65ny_detail\x18\x03 \x01(\t\x12\x11\n\tagent_did\x18\x04 \x01(\t\x12\x11\n\tbadge_jti\x18\x05 \x01(\t\x12-\n\nauth_level\x18\x06 \x01(\x0e\x32\x19.capiscio.v1.MCPAuthLevel\x12\x13\n\x0btrust_level\x18\x07 \x01(\x05\x12\x15\n\revidence_json\x18\x08 \x01(\t\x12\x13\n\x0b\x65vidence_id\x18\t \x01(\t\x12-\n\ttimestamp\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1a\n\x12policy_decision_id\x18\x0b \x01(\t\x12\x17\n\x0fpolicy_decision\x18\x0c \x01(\t\x12\x18\n\x10\x65nforcement_mode\x18\r \x01(\t\x12/\n\x0bobligations\x18\x0e \x03(\x0b\x32\x1a.capiscio.v1.MCPObligation\x12\x12\n\nerror_code\x18\x0f \x01(\t\x12\x18\n\x10rejection_detail\x18\x10 \x01(\t\x12\x1c\n\x14requested_capability\x18\x11 \x01(\t\x12\x1c\n\x14presented_capability\x18\x12 \x01(\t\"2\n\rMCPObligation\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x13\n\x0bparams_json\x18\x02 \x01(\t\"\xe3\x01\n\x15PolicyDecisionRequest\x12+\n\x07subject\x18\x01 \x01(\x0b\x32\x1a.capiscio.v1.PolicySubject\x12)\n\x06\x61\x63tion\x18\x02 \x01(\x0b\x32\x19.capiscio.v1.PolicyAction\x12-\n\x08resource\x18\x03 \x01(\x0b\x32\x1b.capiscio.v1.PolicyResource\x12)\n\x06\x63onfig\x18\x04 \x01(\x0b\x32\x19.capiscio.v1.PolicyConfig\x12\x18\n\x10\x62reakglass_token\x18\x05 \x01(\t\"d\n\rPolicySubject\x12\x0b\n\x03\x64id\x18\x01 \x01(\t\x12\x11\n\tbadge_jti\x18\x02 \x01(\t\x12\x0b\n\x03ial\x18\x03 \x01(\t\x12\x13\n\x0btrust_level\x18\x04 \x01(\t\x12\x11\n\tbadge_exp\x18\x05 \x01(\x03\";\n\x0cPolicyAction\x12\x11\n\toperation\x18\x01 \x01(\t\x12\x18\n\x10\x63\x61pability_class\x18\x02 \x01(\t\"$\n\x0ePolicyResource\x12\x12\n\nidentifier\x18\x01 \x01(\t\"\x98\x01\n\x0cPolicyConfig\x12\x14\n\x0cpdp_endpoint\x18\x01 \x01(\t\x12\x16\n\x0epdp_timeout_ms\x18\x02 \x01(\x05\x12\x18\n\x10\x65nforcement_mode\x18\x03 \x01(\t\x12\x0e\n\x06pep_id\x18\x04 \x01(\t\x12\x11\n\tworkspace\x18\x05 \x01(\t\x12\x1d\n\x15\x62reakglass_public_key\x18\x06 \x01(\x0c\"\xab\x02\n\x16PolicyDecisionResponse\x12\x10\n\x08\x64\x65\x63ision\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65\x63ision_id\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x0b\n\x03ttl\x18\x04 \x01(\x05\x12/\n\x0bobligations\x18\x05 \x03(\x0b\x32\x1a.capiscio.v1.MCPObligation\x12\x18\n\x10\x65nforcement_mode\x18\x06 \x01(\t\x12\x11\n\tcache_hit\x18\x07 \x01(\x08\x12\x1b\n\x13\x62reakglass_override\x18\x08 \x01(\x08\x12\x16\n\x0e\x62reakglass_jti\x18\t \x01(\t\x12\x12\n\nerror_code\x18\n \x01(\t\x12\x16\n\x0epdp_latency_ms\x18\x0b \x01(\x03\x12\x0e\n\x06txn_id\x18\x0c \x01(\t\"\xa6\x01\n\x1bVerifyServerIdentityRequest\x12\x12\n\nserver_did\x18\x01 \x01(\t\x12\x14\n\x0cserver_badge\x18\x02 \x01(\t\x12\x18\n\x10transport_origin\x18\x03 \x01(\t\x12\x15\n\rendpoint_path\x18\x04 \x01(\t\x12,\n\x06\x63onfig\x18\x05 \x01(\x0b\x32\x1c.capiscio.v1.MCPVerifyConfig\"\x91\x01\n\x0fMCPVerifyConfig\x12\x17\n\x0ftrusted_issuers\x18\x01 \x03(\t\x12\x17\n\x0fmin_trust_level\x18\x02 \x01(\x05\x12\x19\n\x11\x61\x63\x63\x65pt_level_zero\x18\x03 \x01(\x08\x12\x14\n\x0coffline_mode\x18\x04 \x01(\x08\x12\x1b\n\x13skip_origin_binding\x18\x05 \x01(\x08\"\xd1\x01\n\x1cVerifyServerIdentityResponse\x12*\n\x05state\x18\x01 \x01(\x0e\x32\x1b.capiscio.v1.MCPServerState\x12\x13\n\x0btrust_level\x18\x02 \x01(\x05\x12\x12\n\nserver_did\x18\x03 \x01(\t\x12\x11\n\tbadge_jti\x18\x04 \x01(\t\x12\x33\n\nerror_code\x18\x05 \x01(\x0e\x32\x1f.capiscio.v1.MCPServerErrorCode\x12\x14\n\x0c\x65rror_detail\x18\x06 \x01(\t\"\x90\x01\n\x1aParseServerIdentityRequest\x12\x33\n\x0chttp_headers\x18\x01 \x01(\x0b\x32\x1b.capiscio.v1.MCPHttpHeadersH\x00\x12\x33\n\x0cjsonrpc_meta\x18\x02 \x01(\x0b\x32\x1b.capiscio.v1.MCPJsonRpcMetaH\x00\x42\x08\n\x06source\"L\n\x0eMCPHttpHeaders\x12\x1b\n\x13\x63\x61piscio_server_did\x18\x01 \x01(\t\x12\x1d\n\x15\x63\x61piscio_server_badge\x18\x02 \x01(\t\"#\n\x0eMCPJsonRpcMeta\x12\x11\n\tmeta_json\x18\x01 \x01(\t\"a\n\x1bParseServerIdentityResponse\x12\x12\n\nserver_did\x18\x01 \x01(\t\x12\x14\n\x0cserver_badge\x18\x02 \x01(\t\x12\x18\n\x10identity_present\x18\x03 \x01(\x08\"*\n\x10MCPHealthRequest\x12\x16\n\x0e\x63lient_version\x18\x01 \x01(\t\"m\n\x11MCPHealthResponse\x12\x0f\n\x07healthy\x18\x01 \x01(\x08\x12\x14\n\x0c\x63ore_version\x18\x02 \x01(\t\x12\x15\n\rproto_version\x18\x03 \x01(\t\x12\x1a\n\x12version_compatible\x18\x04 \x01(\x08*Z\n\x0bMCPDecision\x12\x1c\n\x18MCP_DECISION_UNSPECIFIED\x10\x00\x12\x16\n\x12MCP_DECISION_ALLOW\x10\x01\x12\x15\n\x11MCP_DECISION_DENY\x10\x02*\x82\x01\n\x0cMCPAuthLevel\x12\x1e\n\x1aMCP_AUTH_LEVEL_UNSPECIFIED\x10\x00\x12\x1c\n\x18MCP_AUTH_LEVEL_ANONYMOUS\x10\x01\x12\x1a\n\x16MCP_AUTH_LEVEL_API_KEY\x10\x02\x12\x18\n\x14MCP_AUTH_LEVEL_BADGE\x10\x03*\xfb\x02\n\rMCPDenyReason\x12\x1f\n\x1bMCP_DENY_REASON_UNSPECIFIED\x10\x00\x12!\n\x1dMCP_DENY_REASON_BADGE_MISSING\x10\x01\x12!\n\x1dMCP_DENY_REASON_BADGE_INVALID\x10\x02\x12!\n\x1dMCP_DENY_REASON_BADGE_EXPIRED\x10\x03\x12!\n\x1dMCP_DENY_REASON_BADGE_REVOKED\x10\x04\x12&\n\"MCP_DENY_REASON_TRUST_INSUFFICIENT\x10\x05\x12$\n MCP_DENY_REASON_TOOL_NOT_ALLOWED\x10\x06\x12$\n MCP_DENY_REASON_ISSUER_UNTRUSTED\x10\x07\x12!\n\x1dMCP_DENY_REASON_POLICY_DENIED\x10\x08\x12&\n\"MCP_DENY_REASON_SCOPE_INSUFFICIENT\x10\t*\xac\x01\n\x0eMCPServerState\x12 \n\x1cMCP_SERVER_STATE_UNSPECIFIED\x10\x00\x12\'\n#MCP_SERVER_STATE_VERIFIED_PRINCIPAL\x10\x01\x12\'\n#MCP_SERVER_STATE_DECLARED_PRINCIPAL\x10\x02\x12&\n\"MCP_SERVER_STATE_UNVERIFIED_ORIGIN\x10\x03*\xd7\x02\n\x12MCPServerErrorCode\x12\x19\n\x15MCP_SERVER_ERROR_NONE\x10\x00\x12 \n\x1cMCP_SERVER_ERROR_DID_INVALID\x10\x01\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_INVALID\x10\x02\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_EXPIRED\x10\x03\x12\"\n\x1eMCP_SERVER_ERROR_BADGE_REVOKED\x10\x04\x12\'\n#MCP_SERVER_ERROR_TRUST_INSUFFICIENT\x10\x05\x12$\n MCP_SERVER_ERROR_ORIGIN_MISMATCH\x10\x06\x12\"\n\x1eMCP_SERVER_ERROR_PATH_MISMATCH\x10\x07\x12%\n!MCP_SERVER_ERROR_ISSUER_UNTRUSTED\x10\x08\x32\xf6\x03\n\nMCPService\x12\x65\n\x12\x45valuateToolAccess\x12&.capiscio.v1.EvaluateToolAccessRequest\x1a\'.capiscio.v1.EvaluateToolAccessResponse\x12\x61\n\x16\x45valuatePolicyDecision\x12\".capiscio.v1.PolicyDecisionRequest\x1a#.capiscio.v1.PolicyDecisionResponse\x12k\n\x14VerifyServerIdentity\x12(.capiscio.v1.VerifyServerIdentityRequest\x1a).capiscio.v1.VerifyServerIdentityResponse\x12h\n\x13ParseServerIdentity\x12\'.capiscio.v1.ParseServerIdentityRequest\x1a(.capiscio.v1.ParseServerIdentityResponse\x12G\n\x06Health\x12\x1d.capiscio.v1.MCPHealthRequest\x1a\x1e.capiscio.v1.MCPHealthResponseB;Z9github.com/capiscio/capiscio-core/pkg/rpc/gen/capiscio/v1b\x06proto3') |
Comment on lines
350
to
+354
| evidence_id=response.evidence_id, | ||
| evidence_json=response.evidence_json, | ||
| error_code=response.error_code or None, | ||
| requested_capability=response.requested_capability or None, | ||
| presented_capability=response.presented_capability or None, |
Comment on lines
328
to
332
| mcp_pb2.TOOL_NOT_ALLOWED: DenyReason.TOOL_NOT_ALLOWED, | ||
| mcp_pb2.TOOL_ISSUER_UNTRUSTED: DenyReason.ISSUER_UNTRUSTED, | ||
| mcp_pb2.TOOL_POLICY_DENIED: DenyReason.POLICY_DENIED, | ||
| mcp_pb2.TOOL_SCOPE_INSUFFICIENT: DenyReason.SCOPE_INSUFFICIENT, | ||
| } |
- Wrap AddSerializedFile in try/except TypeError to handle duplicate descriptor registration when both gen/ and non-gen/ proto copies are imported - Add GuardError tests for error_code, requested_capability, and presented_capability kwargs (scope-insufficient fields) - Add guard decorator test for SCOPE_INSUFFICIENT deny path - Add evaluate_tool_access test verifying structured field propagation
|
✅ Integration tests passed! capiscio-core gRPC tests working. |
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.
Summary
RFC-008 Capability Class implementation — PR B (DENY path).
Adds
SCOPE_INSUFFICIENTdeny reason and structured rejection metadata to the Python MCP guard, so consumers get actionable error details when capability class policy denies access.Changes
Proto
mcp_pb2.py(both_proto/andgen/) with new fieldsTOOL_SCOPE_INSUFFICIENTalias in__init__.pyTypes & Errors
DenyReason.SCOPE_INSUFFICIENTenum value intypes.pyGuardError: addederror_code,requested_capability,presented_capabilitykwargsGuard
GuardResult: addederror_code,requested_capability,presented_capabilityfieldsevaluate_tool_access(): populates new fields from proto responsedeny_reason_map: addedTOOL_SCOPE_INSUFFICIENT → SCOPE_INSUFFICIENTmapping@guardand@guard_sync: structured deny logging (capiscio.policy_enforced)and propagation of rejection metadata to
GuardErrorTesting
Related PRs
feat/rfc008-capability-class-deny-path