Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds signed “audit receipts” for MCP tools/call responses, including configuration, middleware wiring, and verification utilities.
Changes:
- Introduces Pydantic models for audit receipts and an Ed25519 signer/verifier + FastMCP middleware to attach receipts to tool results.
- Extends
MCPSettings+ skill documentation to configure audit receipts via env vars. - Adds unit tests and a new runtime dependency on
cryptography.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| openbb_platform/extensions/mcp_server/openbb_mcp_server/service/audit_receipts.py | Implements signing/verifying and middleware that attaches signed receipts to tool results. |
| openbb_platform/extensions/mcp_server/openbb_mcp_server/models/audit_receipts.py | Adds receipt payload + signed receipt models and related literal types. |
| openbb_platform/extensions/mcp_server/openbb_mcp_server/models/settings.py | Adds receipt-related settings + env var parsing/validation hooks. |
| openbb_platform/extensions/mcp_server/openbb_mcp_server/app/app.py | Registers the audit receipt middleware when enabled. |
| openbb_platform/extensions/mcp_server/openbb_mcp_server/skills/configure_mcp_server/SKILL.md | Documents the new configuration options and examples. |
| openbb_platform/extensions/mcp_server/pyproject.toml | Adds cryptography dependency for Ed25519 signing. |
| openbb_platform/extensions/mcp_server/tests/service/test_audit_receipts.py | Adds unit tests for signing/verifying and middleware attachment. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+113
to
+120
| public_key = Ed25519PublicKey.from_public_bytes(_b64decode(receipt.public_key)) | ||
| try: | ||
| public_key.verify( | ||
| _b64decode(receipt.signature), | ||
| _canonical_json(payload_dict), | ||
| ) | ||
| except InvalidSignature: | ||
| return False |
Comment on lines
+160
to
+186
| result_dict = ( | ||
| result.model_dump(mode="json", exclude_none=True) | ||
| if hasattr(result, "model_dump") | ||
| else result | ||
| ) | ||
| decision = "deny" if getattr(result, "is_error", False) else "allow" | ||
|
|
||
| payload = AuditReceiptPayload( | ||
| receipt_id=str(uuid4()), | ||
| tool_name=tool_name, | ||
| principal=self.principal, | ||
| timestamp=context.timestamp.isoformat(), | ||
| decision=decision, | ||
| request_hash=sha256_digest( | ||
| { | ||
| "tool_name": tool_name, | ||
| "arguments": arguments, | ||
| } | ||
| ), | ||
| normalized_view_hash=sha256_digest(result_dict), | ||
| retention_mode=self.retention_mode, | ||
| commitment_stage=self.commitment_stage, | ||
| policy_hash=self.policy_hash, | ||
| catalog_version=self.catalog_version, | ||
| allowed_scope=self.allowed_scope, | ||
| withheld_scope=self.withheld_scope, | ||
| ) |
Comment on lines
+155
to
+165
| """Sign successful MCP tool-call results and attach them to result metadata.""" | ||
| result = await call_next(context) | ||
|
|
||
| tool_name = getattr(context.message, "name", "unknown_tool") | ||
| arguments = getattr(context.message, "arguments", None) or {} | ||
| result_dict = ( | ||
| result.model_dump(mode="json", exclude_none=True) | ||
| if hasattr(result, "model_dump") | ||
| else result | ||
| ) | ||
| decision = "deny" if getattr(result, "is_error", False) else "allow" |
Comment on lines
+299
to
+305
| @field_validator( | ||
| "httpx_client_kwargs", | ||
| "client_auth", | ||
| "server_auth", | ||
| "audit_receipts_withheld_scope", | ||
| mode="before", | ||
| ) |
Comment on lines
+55
to
+63
| def _load_private_key(value: str) -> Ed25519PrivateKey: | ||
| """Load an Ed25519 private key from PEM or base64 raw private bytes.""" | ||
| if value.strip().startswith("-----BEGIN"): | ||
| key = serialization.load_pem_private_key(value.encode(), password=None) | ||
| if not isinstance(key, Ed25519PrivateKey): | ||
| raise ValueError("OPENBB_MCP_AUDIT_RECEIPTS_PRIVATE_KEY must be Ed25519.") | ||
| return key | ||
|
|
||
| return Ed25519PrivateKey.from_private_bytes(_b64decode(value)) |
Comment on lines
+33
to
+60
| def test_audit_receipt_sign_and_verify(): | ||
| """Sign and verify an audit receipt payload.""" | ||
| signer = AuditReceiptSigner.from_private_key_value( | ||
| _private_key_b64(), | ||
| key_id="test-key", | ||
| ) | ||
| payload = AuditReceiptPayload( | ||
| receipt_id="receipt-1", | ||
| tool_name="equity_price_historical", | ||
| principal="agent-1", | ||
| timestamp="2026-06-17T00:00:00+00:00", | ||
| decision="allow", | ||
| request_hash="request-hash", | ||
| normalized_view_hash="view-hash", | ||
| policy_hash="policy-hash", | ||
| allowed_scope=["equity.prices"], | ||
| withheld_scope=[{"scope": "portfolio.private", "reason": "client_pii"}], | ||
| ) | ||
|
|
||
| receipt = signer.sign(payload) | ||
|
|
||
| assert receipt.key_id == "test-key" | ||
| assert AuditReceiptSigner.verify(receipt) is True | ||
|
|
||
| tampered = receipt.model_copy(deep=True) | ||
| tampered.payload.tool_name = "equity_price_quote" | ||
|
|
||
| assert AuditReceiptSigner.verify(tampered) is False |
04df048 to
a8ad381
Compare
a8ad381 to
7ce7952
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.
Withdrawn by author.