Skip to content

feat(rfc008): Python SCOPE_INSUFFICIENT + structured rejection metadata#20

Merged
beonde merged 2 commits into
mainfrom
feat/rfc008-capability-class-deny-path
Apr 30, 2026
Merged

feat(rfc008): Python SCOPE_INSUFFICIENT + structured rejection metadata#20
beonde merged 2 commits into
mainfrom
feat/rfc008-capability-class-deny-path

Conversation

@beonde
Copy link
Copy Markdown
Member

@beonde beonde commented Apr 29, 2026

Summary

RFC-008 Capability Class implementation — PR B (DENY path).

Adds SCOPE_INSUFFICIENT deny reason and structured rejection metadata to the Python MCP guard, so consumers get actionable error details when capability class policy denies access.

Changes

Proto

  • Regenerated mcp_pb2.py (both _proto/ and gen/) with new fields
  • Added TOOL_SCOPE_INSUFFICIENT alias in __init__.py

Types & Errors

  • DenyReason.SCOPE_INSUFFICIENT enum value in types.py
  • GuardError: added error_code, requested_capability, presented_capability kwargs

Guard

  • GuardResult: added error_code, requested_capability, presented_capability fields
  • evaluate_tool_access(): populates new fields from proto response
  • deny_reason_map: added TOOL_SCOPE_INSUFFICIENT → SCOPE_INSUFFICIENT mapping
  • Both @guard and @guard_sync: structured deny logging (capiscio.policy_enforced)
    and propagation of rejection metadata to GuardError

Testing

  • Python import verification passes: enum value, proto fields, error kwargs all work
  • No existing test regressions

Related PRs

…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).
Copilot AI review requested due to automatic review settings April 29, 2026 23:56
@github-actions
Copy link
Copy Markdown

✅ Integration tests passed! capiscio-core gRPC tests working.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_INSUFFICIENT and map proto TOOL_SCOPE_INSUFFICIENT to it in guard evaluation.
  • Extend GuardResult and GuardError to carry error_code, requested_capability, and presented_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 thread capiscio_mcp/errors.py
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 thread capiscio_mcp/guard.py
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 thread capiscio_mcp/guard.py
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
@github-actions
Copy link
Copy Markdown

✅ Integration tests passed! capiscio-core gRPC tests working.

@beonde beonde merged commit 6b00326 into main Apr 30, 2026
10 checks passed
@beonde beonde deleted the feat/rfc008-capability-class-deny-path branch April 30, 2026 02:49
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.

2 participants