gargoyle-receipts 0.4.0.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. gargoyle_receipts-0.4.0.dev0/LICENSE +202 -0
  2. gargoyle_receipts-0.4.0.dev0/NOTICE +6 -0
  3. gargoyle_receipts-0.4.0.dev0/PKG-INFO +361 -0
  4. gargoyle_receipts-0.4.0.dev0/README.md +325 -0
  5. gargoyle_receipts-0.4.0.dev0/gargoyle/__init__.py +0 -0
  6. gargoyle_receipts-0.4.0.dev0/gargoyle/approval.py +260 -0
  7. gargoyle_receipts-0.4.0.dev0/gargoyle/approver_cli.py +228 -0
  8. gargoyle_receipts-0.4.0.dev0/gargoyle/approver_trust.py +95 -0
  9. gargoyle_receipts-0.4.0.dev0/gargoyle/cli.py +260 -0
  10. gargoyle_receipts-0.4.0.dev0/gargoyle/consumer.py +344 -0
  11. gargoyle_receipts-0.4.0.dev0/gargoyle/envelope.py +363 -0
  12. gargoyle_receipts-0.4.0.dev0/gargoyle/evidence.py +237 -0
  13. gargoyle_receipts-0.4.0.dev0/gargoyle/execution.py +212 -0
  14. gargoyle_receipts-0.4.0.dev0/gargoyle/fs_server.py +124 -0
  15. gargoyle_receipts-0.4.0.dev0/gargoyle/identity.py +114 -0
  16. gargoyle_receipts-0.4.0.dev0/gargoyle/issuer.py +104 -0
  17. gargoyle_receipts-0.4.0.dev0/gargoyle/issuer_service.py +297 -0
  18. gargoyle_receipts-0.4.0.dev0/gargoyle/jws.py +214 -0
  19. gargoyle_receipts-0.4.0.dev0/gargoyle/key_record_source.py +273 -0
  20. gargoyle_receipts-0.4.0.dev0/gargoyle/mcp_server.py +608 -0
  21. gargoyle_receipts-0.4.0.dev0/gargoyle/notify.py +80 -0
  22. gargoyle_receipts-0.4.0.dev0/gargoyle/opa.py +123 -0
  23. gargoyle_receipts-0.4.0.dev0/gargoyle/pending_store.py +222 -0
  24. gargoyle_receipts-0.4.0.dev0/gargoyle/policy.py +101 -0
  25. gargoyle_receipts-0.4.0.dev0/gargoyle/published_record_source.py +184 -0
  26. gargoyle_receipts-0.4.0.dev0/gargoyle/replay_cache.py +182 -0
  27. gargoyle_receipts-0.4.0.dev0/gargoyle/rules.py +174 -0
  28. gargoyle_receipts-0.4.0.dev0/gargoyle/static/approve.html +149 -0
  29. gargoyle_receipts-0.4.0.dev0/gargoyle/static/approve.js +59 -0
  30. gargoyle_receipts-0.4.0.dev0/gargoyle/verifier.py +399 -0
  31. gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/PKG-INFO +361 -0
  32. gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/SOURCES.txt +61 -0
  33. gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/dependency_links.txt +1 -0
  34. gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/entry_points.txt +3 -0
  35. gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/requires.txt +12 -0
  36. gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/top_level.txt +1 -0
  37. gargoyle_receipts-0.4.0.dev0/pyproject.toml +56 -0
  38. gargoyle_receipts-0.4.0.dev0/setup.cfg +4 -0
  39. gargoyle_receipts-0.4.0.dev0/tests/test_annotations.py +28 -0
  40. gargoyle_receipts-0.4.0.dev0/tests/test_approver_cli.py +92 -0
  41. gargoyle_receipts-0.4.0.dev0/tests/test_canonical.py +47 -0
  42. gargoyle_receipts-0.4.0.dev0/tests/test_cli.py +121 -0
  43. gargoyle_receipts-0.4.0.dev0/tests/test_console_js.py +39 -0
  44. gargoyle_receipts-0.4.0.dev0/tests/test_consumer.py +318 -0
  45. gargoyle_receipts-0.4.0.dev0/tests/test_entra_identity.py +156 -0
  46. gargoyle_receipts-0.4.0.dev0/tests/test_evidence.py +153 -0
  47. gargoyle_receipts-0.4.0.dev0/tests/test_execution.py +163 -0
  48. gargoyle_receipts-0.4.0.dev0/tests/test_fs_server.py +54 -0
  49. gargoyle_receipts-0.4.0.dev0/tests/test_identity.py +63 -0
  50. gargoyle_receipts-0.4.0.dev0/tests/test_issuer_service.py +168 -0
  51. gargoyle_receipts-0.4.0.dev0/tests/test_jws.py +98 -0
  52. gargoyle_receipts-0.4.0.dev0/tests/test_mcp_proxy.py +141 -0
  53. gargoyle_receipts-0.4.0.dev0/tests/test_mcp_self_issue.py +229 -0
  54. gargoyle_receipts-0.4.0.dev0/tests/test_mcp_server.py +194 -0
  55. gargoyle_receipts-0.4.0.dev0/tests/test_notify.py +67 -0
  56. gargoyle_receipts-0.4.0.dev0/tests/test_opa.py +128 -0
  57. gargoyle_receipts-0.4.0.dev0/tests/test_policy.py +132 -0
  58. gargoyle_receipts-0.4.0.dev0/tests/test_published_record.py +89 -0
  59. gargoyle_receipts-0.4.0.dev0/tests/test_rules.py +129 -0
  60. gargoyle_receipts-0.4.0.dev0/tests/test_stdio_encoding.py +91 -0
  61. gargoyle_receipts-0.4.0.dev0/tests/test_threshold.py +248 -0
  62. gargoyle_receipts-0.4.0.dev0/tests/test_typescript_conformance.py +20 -0
  63. gargoyle_receipts-0.4.0.dev0/tests/test_wire_spec.py +200 -0
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,6 @@
1
+ Gargoyle
2
+ Copyright 2026 Justin LaPorte
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not
5
+ use this file except in compliance with the License. You may obtain a copy of
6
+ the License at http://www.apache.org/licenses/LICENSE-2.0
@@ -0,0 +1,361 @@
1
+ Metadata-Version: 2.4
2
+ Name: gargoyle-receipts
3
+ Version: 0.4.0.dev0
4
+ Summary: Signed, single-use, argument-bound receipts and human-approval holds for agent tool calls.
5
+ Author: Justin LaPorte
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/Elyon-Sol/Gargoyle
8
+ Project-URL: Source, https://github.com/Elyon-Sol/Gargoyle
9
+ Project-URL: Specification, https://github.com/Elyon-Sol/Gargoyle/blob/main/SPEC.md
10
+ Project-URL: Issues, https://github.com/Elyon-Sol/Gargoyle/issues
11
+ Keywords: agents,mcp,authorization,receipts,ed25519,human-in-the-loop
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: Security :: Cryptography
21
+ Requires-Python: >=3.12
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ License-File: NOTICE
25
+ Requires-Dist: cryptography>=44.0.0
26
+ Requires-Dist: fastapi>=0.115
27
+ Requires-Dist: uvicorn>=0.30
28
+ Requires-Dist: requests>=2.32
29
+ Provides-Extra: redis
30
+ Requires-Dist: redis; extra == "redis"
31
+ Provides-Extra: dev
32
+ Requires-Dist: httpx>=0.27; extra == "dev"
33
+ Requires-Dist: pytest>=8; extra == "dev"
34
+ Requires-Dist: redis; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # Gargoyle
38
+
39
+ Signed, single-use, argument-bound receipts and human-approval holds for
40
+ agent tool calls.
41
+
42
+ An agent asks for permission to run a tool. Something you already trust
43
+ decides (OPA, Cedar, your own code). Gargoyle turns that decision into a
44
+ **receipt**: an Ed25519-signed record that names the tool, a hash of its
45
+ arguments, the principal the decision was made for, the target it may be
46
+ presented to, and the policy that decided. The target checks the receipt
47
+ before it acts, and honors it once. When the policy says a call needs a
48
+ human, the receipt is held until an approver signs a **grant** for exactly
49
+ that decision with a key the issuing service does not have.
50
+
51
+ Gargoyle does not decide whether a call is allowed. It makes an allow
52
+ decision portable, verifiable, bound to one call by one caller, and
53
+ spendable once. The decision comes from your policy engine (an OPA adapter
54
+ is included); the caller is authenticated by your transport (the
55
+ proxy-verified client certificate, by default); Gargoyle sits in front of
56
+ your MCP server and lets through only what both agreed to.
57
+
58
+ Status: pre-release (`0.4.0.dev0`). The wire format is `gargoyle/3` and may
59
+ still change before the first release; it is specified in [SPEC.md](SPEC.md).
60
+
61
+ ## What a receipt gives you
62
+
63
+ - **Binding.** A receipt for `transfer_funds` with `{"amount": 250}` is
64
+ refused for `{"amount": 250000}`, for `delete_database`, and at any
65
+ target but the one it names.
66
+ - **A principal.** The receipt carries the identity the issuer
67
+ authenticated; the target compares it with the identity it authenticated.
68
+ A receipt obtained by one agent cannot be spent by another. When the agent
69
+ acts for a user, the receipt records that too (`on_behalf_of`).
70
+ - **Standard encodings.** A receipt can travel as a JWS (EdDSA) that any
71
+ JWT library verifies, and the issuer can take the caller's identity from
72
+ an OAuth bearer token, an RFC 8693 delegation token or a Transaction
73
+ Token, signed EdDSA, RS256 or ES256 by keys from the provider's JWKS.
74
+ With Entra ID the principal is the user's UPN and the token's groups and
75
+ app roles become policy inputs, so a rules file can say "finance may hold
76
+ invoice updates; shipping may not" without naming people.
77
+ - **Single use.** Every receipt carries a signed `decision_id`; an executor
78
+ claims it exactly once. A replayed receipt is refused.
79
+ - **Expiry.** Every receipt carries a signed `not_after` (default 300 s). A
80
+ captured receipt, or a leaked issuer key, is useful for minutes, not
81
+ forever.
82
+ - **Currency.** The receipt pins the policy that decided. If the policy
83
+ changes, outstanding receipts stop being honored and the call must be
84
+ decided again.
85
+ - **Holds.** A decision that requires approval is flagged inside the signed
86
+ receipt. The target will not run it without a grant a human signed for
87
+ that exact decision, under their own key, on their own device.
88
+ - **Proof of execution.** An executor with a key answers each honored call
89
+ with a signed execution receipt: the decision it fulfilled, the policy
90
+ version that allowed it, and a hash of the outputs. Receipt, grant and
91
+ execution receipt verify together, offline, with three public keys.
92
+
93
+ ## Install
94
+
95
+ pip install -e ".[dev]"
96
+ python -m pytest
97
+
98
+ Python 3.12 or later. A TypeScript verifier and consumer for Node 20+
99
+ lives in [ts/](ts/) (`cd ts && npm install && npm test`); its suite
100
+ recomputes the same conformance vectors byte for byte, so the two
101
+ implementations cannot drift apart silently. Redis is optional (`.[redis]`) and only needed to
102
+ share the replay cache and pending-approval set across executor replicas.
103
+
104
+ ## Issuing a receipt
105
+
106
+ The policy is anything with `pin()` and `decide()`; see `gargoyle.policy`.
107
+ The interaction you pass carries the call (`tool`, `args`, `principal`) and
108
+ whatever claims your policy reads. The receipt records only the call and
109
+ the policy pin.
110
+
111
+ ```python
112
+ from gargoyle.issuer import ENVELOPE_HEADER, issue
113
+
114
+ who = principal_from_token(bearer_or_transaction_token, IDP_KEYS, audience="issuer") # gargoyle.identity
115
+ # IDP_KEYS: pinned, or entra_keys("<tenant>") / keys_from_jwks(<provider JWKS>)
116
+ interaction = {
117
+ "tool": "transfer_funds",
118
+ "args": {"amount": 250, "to": "acct-9"},
119
+ "principal": who["principal"], # the agent, as the identity provider authenticated it
120
+ "on_behalf_of": who["on_behalf_of"], # the user it acts for, when the token says so
121
+ **claims, # whatever your policy decides on
122
+ }
123
+ result = issue(policy, "https://tools.example.com/target", interaction,
124
+ signing_key=issuer_private_key, key_id="issuer-2026-10")
125
+
126
+ if result.envelope is None:
127
+ refuse(result.decision.reason)
128
+ elif result.decision.requires_approval:
129
+ hold(result.envelope) # see "Approval holds"
130
+ else:
131
+ send(call, headers={ENVELOPE_HEADER: json.dumps(result.envelope)})
132
+ # or, for a target that speaks JWT: gargoyle.jws.encode_receipt(result.envelope, issuer_private_key)
133
+ ```
134
+
135
+ **Open Policy Agent.** `gargoyle.opa.OpaPolicy` asks OPA's data API and
136
+ pins the exact set of policy modules OPA holds, so editing a policy
137
+ invalidates every outstanding receipt until the call is decided again.
138
+
139
+ ```python
140
+ from gargoyle.opa import OpaPolicy
141
+ policy = OpaPolicy("http://127.0.0.1:8181", "gargoyle/decision")
142
+ ```
143
+
144
+ ```rego
145
+ package gargoyle
146
+ default decision := {"allow": false, "reason": "DEFAULT_DENY"}
147
+ decision := {"allow": true, "requires_approval": input.tool == "transfer_funds"} {
148
+ input.principal == "spiffe://example.org/agents/payments"
149
+ input.tool in {"read_balance", "transfer_funds"}
150
+ }
151
+ ```
152
+
153
+ OPA unreachable, an undefined document, or a result in no recognized shape
154
+ is a refusal, never an issuance. Any other engine goes behind the same
155
+ two-method interface.
156
+
157
+ **No engine.** `gargoyle.rules.RulesPolicy` decides from a JSON file:
158
+ first matching rule wins, globs on tool name, principal, delegation,
159
+ the caller's groups and roles, and string arguments, actions allow, hold
160
+ or deny. The pin is the file's hash,
161
+ so an edit invalidates outstanding receipts exactly as an engine change
162
+ does. This is what `gargoyle init` sets up.
163
+
164
+ ## Checking a receipt
165
+
166
+ Every executor runs the same check: establish the live policy pin,
167
+ establish the trusted issuer keys, verify the receipt against the call it is
168
+ about to run, claim the `decision_id`. `gargoyle.consumer.Consumer` packages
169
+ it.
170
+
171
+ ```python
172
+ from gargoyle.consumer import Consumer
173
+
174
+ consumer = Consumer(
175
+ target_id="https://tools.example.com/target",
176
+ pinned_public_keys={"issuer-2026-10": issuer_public_key},
177
+ record_bytes=open("published_record.json", "rb").read(), # {policy_id, policy_sha256}
178
+ )
179
+
180
+ call = {"tool": name, "args": arguments, "principal": peer_identity}
181
+ verdict = consumer.check(envelope, call)
182
+ if verdict.honored:
183
+ run(name, arguments)
184
+ else:
185
+ refuse(verdict.reason) # REF_VERIFY_* or REF_TARGET_*
186
+ ```
187
+
188
+ `peer_identity` is who *you* authenticated as the caller (an mTLS peer, a
189
+ bearer token's subject, an identity a trusted proxy asserted). It is never
190
+ read from the request. A consumer refuses to be built without issuer-key
191
+ trust, and the verifier refuses to run without it, so an unsigned receipt is
192
+ never honored. For a flagged receipt, pass the human's grant as
193
+ `grant=`; the consumer verifies it against the receipt's own decision and
194
+ hold, claims it once, and only then claims the receipt.
195
+
196
+ For a live pin that can be reissued, give the consumer a record source
197
+ (a publisher-signed record with a serial and an expiry) instead of a
198
+ static pin; for issuer keys that can be rotated and revoked, a key record
199
+ source. Both are consulted on every check and fail closed.
200
+
201
+ ## Proof that it ran
202
+
203
+ Give a consumer an executor key and it signs an execution receipt after the
204
+ side effect. Anyone holding the issuer's, approver's and executor's public
205
+ keys can then audit one call with no network and no live state:
206
+
207
+ ```python
208
+ from gargoyle.execution import verify_chain
209
+
210
+ chain = verify_chain(receipt, execution,
211
+ issuer_public_keys=ISSUER_KEYS, executor_public_keys=EXECUTOR_KEYS,
212
+ grant=grant, approver_public_keys=APPROVER_KEYS,
213
+ outputs=outputs, check_outputs=True)
214
+ chain["accepted"], chain["steps"] # True, [("authorization", ...), ("approval", ...), ("execution", ...)]
215
+ ```
216
+
217
+ That is the sentence Gargoyle can say that nobody else can: a specific
218
+ policy version authorized a specific call for a specific principal, a
219
+ human's key released it, and exactly that call ran once, verifiable offline
220
+ by anyone, in Python or TypeScript.
221
+
222
+ The MCP middleware also keeps an evidence log: one hash-chained line per
223
+ decision, refusals and declined holds included, so the trail answers "what
224
+ did the agent try" as well as "what did it do". `gargoyle audit --log`
225
+ verifies every entry and the chain, and prints the tail hash to note down
226
+ so a later copy that lost its end is caught (SPEC.md section 15).
227
+
228
+ ## Where it sits
229
+
230
+ MCP gateways (TrueFoundry, Monosign, hoop.dev, Akto and others) hold a
231
+ tool call, notify a reviewer and log the click. Gargoyle is not a gateway.
232
+ It runs beside the client, and its approval is a signature made with a key
233
+ the issuer never holds, checked at execution and again by anyone later. In
234
+ 2026 three Internet-Drafts describe parts of the same shape: Acta signed
235
+ receipts (an access decision, signed), EP authorization receipts (a human's
236
+ pre-execution signoff), XAIP receipts (a signed execution record). Gargoyle
237
+ chains all three on one decision id with the policy version inside, uses
238
+ the same RFC 8785 canonical form they do, and SPEC.md section 16 maps every
239
+ field. A hold can also be announced to Slack or any webhook
240
+ (`GARGOYLE_HOLD_WEBHOOK_URL`); the notice carries no key, so nothing in a
241
+ chat channel can approve anything.
242
+
243
+ ## Deploy it
244
+
245
+ Three commands, no policy engine, no containers, no client changes:
246
+
247
+ gargoyle init /path/the/agent/may/touch # keys, config, rules.json
248
+ gargoyle issuer # the issuing service, on rules.json
249
+ gargoyle approve --watch 2 # where the approver sits (terminal)
250
+
251
+ or approve in a browser: open the issuer's `/approve` page and load
252
+ `approver-web.json`; the page shows each held call with its full arguments,
253
+ checked against the receipt, and signs the grant locally with WebCrypto.
254
+ The key never leaves the browser. Then paste the server entry from
255
+ `gargoyle-deploy/mcp-config.json` into your MCP client.
256
+ [docs/guide-hyperv-ubuntu-apache.md](docs/guide-hyperv-ubuntu-apache.md)
257
+ is the full single-instance build: Ubuntu VM on Hyper-V, Apache with TLS,
258
+ a Windows 11 client running Claude Code. Reads are free, writes block until the approver's key
259
+ signs, deletes are refused; the policy is a JSON rules file you edit. A
260
+ hold can demand more than one signature: `"approvals": 2` on a rule, with
261
+ an optional `"approvers": [...]` naming which keys count, puts the terms
262
+ in the receipt itself, and `gargoyle init --approvers 2` creates the keys.
263
+ Every honored call comes back with receipt, grants and execution receipt,
264
+ and `gargoyle audit` verifies them later offline.
265
+ [examples/filesystem-agent](examples/filesystem-agent/) walks through it,
266
+ and through the alternative with OPA in Docker for sites that run a policy
267
+ engine.
268
+
269
+ ## The shipped consumer
270
+
271
+ **MCP middleware** (`python -m gargoyle.mcp_server -- <upstream command>`):
272
+ a stdio MCP server you put in front of any other stdio MCP server. The
273
+ handshake, `tools/list` and everything else are relayed. A `tools/call` is
274
+ forwarded only when `params._meta["gargoyle/envelope"]` carries a valid
275
+ receipt for that tool, those arguments and the principal the server serves
276
+ (and `params._meta["gargoyle/grant"]` a grant, when the receipt is
277
+ flagged); a refused call is answered here and never reaches the upstream.
278
+ Without an upstream command it runs a built-in demo executor. With
279
+ `GARGOYLE_MCP_ISSUER_URL` and `GARGOYLE_MCP_CLIENT_TOKEN` it obtains a
280
+ receipt from the issuing service for any call that carries none, and
281
+ waits for the human's grant when the call is flagged, so clients that
282
+ cannot attach receipts work unchanged. Configuration is by environment:
283
+
284
+ | Variable | Meaning |
285
+ |---|---|
286
+ | `GARGOYLE_MCP_GATE_KEY_ID`, `GARGOYLE_MCP_GATE_PUBLIC_KEY_HEX` | the pinned issuer key |
287
+ | `GARGOYLE_MCP_TARGET_ID` | the target identity receipts bind to |
288
+ | `GARGOYLE_MCP_PRINCIPAL` | the client this server serves |
289
+ | `GARGOYLE_MCP_RECORD_PATH` | the published policy record |
290
+ | `GARGOYLE_MCP_APPROVER_KEY_ID`, `GARGOYLE_MCP_APPROVER_PUBLIC_KEY_HEX` | the approver keys whose grants release flagged calls (optional, both or neither; comma-separated lists for several) |
291
+ | `GARGOYLE_MCP_EXECUTOR_KEY_ID`, `GARGOYLE_MCP_EXECUTOR_KEY_HEX` | this server's own signing key, so honored calls carry an execution receipt in `_meta["gargoyle/execution"]` (optional, both or neither) |
292
+ | `GARGOYLE_MCP_ISSUER_URL`, `GARGOYLE_MCP_CLIENT_TOKEN` | self-issue mode: the issuing service and the identity token (whose subject is this server's principal) to obtain receipts with; `GARGOYLE_MCP_APPROVAL_TIMEOUT_SECONDS` and `GARGOYLE_MCP_POLL_SECONDS` tune the wait for a grant |
293
+ | `GARGOYLE_HOLD_WEBHOOK_URL`, `GARGOYLE_APPROVE_URL` | issuer: POST a notice of every new hold (Slack incoming-webhook shape, `text` plus the hold) with a link to the console; notice only, the grant still needs the approver's key |
294
+ | `GARGOYLE_MCP_AUDIT_LOG` | the evidence log: every decision, honored or refused, as one hash-chained JSON line with its receipt, grant and execution receipt; `gargoyle audit --log` verifies the file |
295
+
296
+ **Issuing service** (`gargoyle.issuer_service`, run with uvicorn from
297
+ `app_from_env()`): identity token in, receipt out, with the hold and grant
298
+ exchange for flagged calls (`/issue`, `/pending`, `/grants`, `/decline`,
299
+ `/record`, `/keys`) and the browser approval console at `/approve`. Policy
300
+ from a rules file or OPA; identity from a pinned key or, with
301
+ `GARGOYLE_ENTRA_TENANT`, the tenant's signing keys.
302
+
303
+
304
+ ## Approval holds
305
+
306
+ When the policy's decision carries `requires_approval`, the receipt is
307
+ issued with `requires_approval: true` and a signed `approval_request_id`.
308
+ No target will run it on its own: the consumer demands a grant for exactly
309
+ that decision and hold, verifies it (provenance, separation of duties,
310
+ binding, expiry), claims it once, and only then claims the receipt. A human
311
+ runs `gargoyle-approver` on a machine that holds the approver private key;
312
+ given the receipt it shows the tool, argument hash, principal, target and
313
+ policy that are inside the issuer's signature, and emits the grant. The
314
+ agent presents receipt and grant together. Approver keys should come from a
315
+ signed key record where the key's role is `approver`
316
+ (`gargoyle.approver_trust`), so an issuer-controlled key can never approve.
317
+ An issuer that prefers to hold flagged receipts until the grant exists can
318
+ do that too (`gargoyle.pending_store`); the target's check does not depend
319
+ on it.
320
+
321
+ ## Modules
322
+
323
+ | Module | Role |
324
+ |---|---|
325
+ | `policy` | the decision interface |
326
+ | `opa` | the Open Policy Agent adapter |
327
+ | `rules` | a policy from a JSON rules file, for sites without an engine |
328
+ | `cli` | the `gargoyle` command: init, issuer, approve, audit |
329
+ | `issuer` | ask the policy, sign the receipt |
330
+ | `identity` | the issuer's identity input: bearer, delegation and transaction tokens to principal and on_behalf_of |
331
+ | `jws` | the receipt as a JWS any JWT library verifies |
332
+ | `envelope` | receipt construction, hash region, signature region, currency check |
333
+ | `verifier` | the pure target-side check and every refusal code |
334
+ | `consumer` | `Consumer` (pin, keys, verify, claim, attest) and the reference HTTP target |
335
+ | `execution` | execution receipts and the offline chain check |
336
+ | `evidence` | the executor's hash-chained log of every decision, and its offline verifier |
337
+ | `mcp_server` | the stdio MCP middleware (and demo executor), with self-issue mode |
338
+ | `fs_server` | a small Python filesystem MCP server, for clients without Node |
339
+ | `issuer_service` | the HTTP issuing service: identity in, receipts out, holds and grants |
340
+ | `replay_cache`, `pending_store` | single use for receipts and holds; in-memory or Redis |
341
+ | `approval`, `approver_cli`, `approver_trust` | grants, the human's tool, approver key trust |
342
+ | `published_record_source` | the live policy pin as a publisher-signed record with serial and expiry |
343
+ | `key_record_source` | root-signed issuer and approver keys with windows and revocation |
344
+
345
+ ## What Gargoyle does not do
346
+
347
+ - It does not decide. The policy does, and the receipt is only as good as
348
+ that decision.
349
+ - It does not authenticate principals. The issuer and the target each
350
+ authenticate the caller themselves (by default, the target trusts the
351
+ client certificate its proxy verified); Gargoyle compares the two results.
352
+ - It cannot stop a caller that never asks for a receipt. Only a target that
353
+ refuses unattested calls can.
354
+ - It does not distribute trust material. Issuer keys, record files, root
355
+ keys and the client token are the deployer's to protect.
356
+ - The approval flow separates duties only if the approver's private key is
357
+ out of the issuing service's reach. That is a deployment property.
358
+
359
+ ## License
360
+
361
+ Apache License 2.0; see [LICENSE](LICENSE) and [NOTICE](NOTICE).