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.
- gargoyle_receipts-0.4.0.dev0/LICENSE +202 -0
- gargoyle_receipts-0.4.0.dev0/NOTICE +6 -0
- gargoyle_receipts-0.4.0.dev0/PKG-INFO +361 -0
- gargoyle_receipts-0.4.0.dev0/README.md +325 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/__init__.py +0 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/approval.py +260 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/approver_cli.py +228 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/approver_trust.py +95 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/cli.py +260 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/consumer.py +344 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/envelope.py +363 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/evidence.py +237 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/execution.py +212 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/fs_server.py +124 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/identity.py +114 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/issuer.py +104 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/issuer_service.py +297 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/jws.py +214 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/key_record_source.py +273 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/mcp_server.py +608 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/notify.py +80 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/opa.py +123 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/pending_store.py +222 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/policy.py +101 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/published_record_source.py +184 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/replay_cache.py +182 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/rules.py +174 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/static/approve.html +149 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/static/approve.js +59 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle/verifier.py +399 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/PKG-INFO +361 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/SOURCES.txt +61 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/dependency_links.txt +1 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/entry_points.txt +3 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/requires.txt +12 -0
- gargoyle_receipts-0.4.0.dev0/gargoyle_receipts.egg-info/top_level.txt +1 -0
- gargoyle_receipts-0.4.0.dev0/pyproject.toml +56 -0
- gargoyle_receipts-0.4.0.dev0/setup.cfg +4 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_annotations.py +28 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_approver_cli.py +92 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_canonical.py +47 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_cli.py +121 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_console_js.py +39 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_consumer.py +318 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_entra_identity.py +156 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_evidence.py +153 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_execution.py +163 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_fs_server.py +54 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_identity.py +63 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_issuer_service.py +168 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_jws.py +98 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_mcp_proxy.py +141 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_mcp_self_issue.py +229 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_mcp_server.py +194 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_notify.py +67 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_opa.py +128 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_policy.py +132 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_published_record.py +89 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_rules.py +129 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_stdio_encoding.py +91 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_threshold.py +248 -0
- gargoyle_receipts-0.4.0.dev0/tests/test_typescript_conformance.py +20 -0
- 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,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).
|