gargoyle-receipts 0.15.0.dev0__tar.gz → 0.15.1.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.
Potentially problematic release.
This version of gargoyle-receipts might be problematic. Click here for more details.
- {gargoyle_receipts-0.15.0.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.15.1.dev0}/PKG-INFO +11 -2
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/README.md +10 -1
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/cli.py +19 -0
- gargoyle_receipts-0.15.1.dev0/gargoyle/review.py +213 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0/gargoyle_receipts.egg-info}/PKG-INFO +11 -2
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle_receipts.egg-info/SOURCES.txt +1 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/pyproject.toml +1 -1
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_cli.py +19 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/LICENSE +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/NOTICE +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/__init__.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/adapters.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/approval.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/approver_cli.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/approver_trust.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/approvers.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/consumer.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/entra_login.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/envelope.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/evidence.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/execution.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/fs_server.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/holdstore.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/identity.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/issuer.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/issuer_service.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/jws.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/key_record_source.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/mcp_server.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/notify.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/opa.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/pending_store.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/plans.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/policy.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/published_record_source.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/reconcile.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/remote.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/replay_cache.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/revert.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/rules.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/specs/graph-reference.json +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/state.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/static/approve.html +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/static/approve.js +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/verifier.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/witness.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle_receipts.egg-info/dependency_links.txt +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle_receipts.egg-info/entry_points.txt +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle_receipts.egg-info/requires.txt +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle_receipts.egg-info/top_level.txt +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/setup.cfg +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_annotations.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_approver_cli.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_approvers.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_canonical.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_console_js.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_consumer.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_entra_identity.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_entra_login.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_evidence.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_execution.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_fs_server.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_graph.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_historical.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_identity.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_issuer_service.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_jws.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_mcp_proxy.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_mcp_self_issue.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_mcp_server.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_notify.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_opa.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_plans.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_policy.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_published_record.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_remote.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_revert.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_rules.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_state.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_stdio_encoding.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_threshold.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_typescript_conformance.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_wire_spec.py +0 -0
- {gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_witness.py +0 -0
{gargoyle_receipts-0.15.0.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.15.1.dev0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gargoyle-receipts
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.1.dev0
|
|
4
4
|
Summary: Signed, single-use, argument-bound receipts and human-approval holds for agent tool calls.
|
|
5
5
|
Author: Justin LaPorte
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -59,7 +59,7 @@ your MCP server and lets through only what both agreed to.
|
|
|
59
59
|
|
|
60
60
|
[](https://pypi.org/project/gargoyle-receipts/)
|
|
61
61
|
|
|
62
|
-
Status: pre-release (`0.15.
|
|
62
|
+
Status: pre-release (`0.15.1.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
|
|
63
63
|
still change before the first release; it is specified in [SPEC.md](SPEC.md).
|
|
64
64
|
|
|
65
65
|
## What a receipt gives you
|
|
@@ -272,6 +272,15 @@ field. A hold can also be announced to Slack or any webhook
|
|
|
272
272
|
(`GARGOYLE_HOLD_WEBHOOK_URL`); the notice carries no key, so nothing in a
|
|
273
273
|
chat channel can approve anything.
|
|
274
274
|
|
|
275
|
+
## Reviewing it
|
|
276
|
+
|
|
277
|
+
`gargoyle review-packet --out packet.zip` builds one archive for an outside
|
|
278
|
+
reviewer: the spec, the vectors, the verification source, a reviewer
|
|
279
|
+
prompt, and `verify_stdlib.py`, which recomputes every conformance vector
|
|
280
|
+
(RFC 8785, SHA-256, Ed25519) with nothing but the Python standard library,
|
|
281
|
+
so a reviewer with no pip and no network can still check every hash and
|
|
282
|
+
signature rather than take the README's word.
|
|
283
|
+
|
|
275
284
|
## Deploy it
|
|
276
285
|
|
|
277
286
|
pip install gargoyle-receipts
|
|
@@ -21,7 +21,7 @@ your MCP server and lets through only what both agreed to.
|
|
|
21
21
|
|
|
22
22
|
[](https://pypi.org/project/gargoyle-receipts/)
|
|
23
23
|
|
|
24
|
-
Status: pre-release (`0.15.
|
|
24
|
+
Status: pre-release (`0.15.1.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
|
|
25
25
|
still change before the first release; it is specified in [SPEC.md](SPEC.md).
|
|
26
26
|
|
|
27
27
|
## What a receipt gives you
|
|
@@ -234,6 +234,15 @@ field. A hold can also be announced to Slack or any webhook
|
|
|
234
234
|
(`GARGOYLE_HOLD_WEBHOOK_URL`); the notice carries no key, so nothing in a
|
|
235
235
|
chat channel can approve anything.
|
|
236
236
|
|
|
237
|
+
## Reviewing it
|
|
238
|
+
|
|
239
|
+
`gargoyle review-packet --out packet.zip` builds one archive for an outside
|
|
240
|
+
reviewer: the spec, the vectors, the verification source, a reviewer
|
|
241
|
+
prompt, and `verify_stdlib.py`, which recomputes every conformance vector
|
|
242
|
+
(RFC 8785, SHA-256, Ed25519) with nothing but the Python standard library,
|
|
243
|
+
so a reviewer with no pip and no network can still check every hash and
|
|
244
|
+
signature rather than take the README's word.
|
|
245
|
+
|
|
237
246
|
## Deploy it
|
|
238
247
|
|
|
239
248
|
pip install gargoyle-receipts
|
|
@@ -14,6 +14,8 @@ policy engine, a container runtime, or any client changes.
|
|
|
14
14
|
gargoyle remote --out DIR [--upstream-url URL | -- <command>] the gate as an HTTPS MCP
|
|
15
15
|
server, for hosted connectors
|
|
16
16
|
gargoyle remote-token --out DIR --principal UPN a capability URL for one person
|
|
17
|
+
gargoyle review-packet --out packet.zip [--audit-excerpt FILE] everything an outside
|
|
18
|
+
reviewer needs, verifier included
|
|
17
19
|
gargoyle audit <receipt.json> <execution.json> [grant.json]
|
|
18
20
|
gargoyle audit --log audit.jsonl verify the middleware's evidence log
|
|
19
21
|
|
|
@@ -282,6 +284,18 @@ def cmd_remote(args) -> int:
|
|
|
282
284
|
return 0
|
|
283
285
|
|
|
284
286
|
|
|
287
|
+
def cmd_review_packet(args) -> int:
|
|
288
|
+
"""One archive for an outside reviewer: spec, README, vectors, the
|
|
289
|
+
verification source, a reviewer prompt, and verify_stdlib.py, which
|
|
290
|
+
recomputes every vector with the standard library alone."""
|
|
291
|
+
from gargoyle.review import build_packet
|
|
292
|
+
root = Path(__file__).resolve().parent.parent
|
|
293
|
+
excerpt = Path(args.audit_excerpt).read_text(encoding="utf-8") if args.audit_excerpt else None
|
|
294
|
+
result = build_packet(str(root), args.out, audit_excerpt=excerpt, extra_files=args.extra or [])
|
|
295
|
+
print("wrote %s (%d files); give the reviewer the archive and REVIEWER_PROMPT.txt" % (result["path"], len(result["files"])))
|
|
296
|
+
return 0
|
|
297
|
+
|
|
298
|
+
|
|
285
299
|
def cmd_issuer(args) -> int:
|
|
286
300
|
import uvicorn
|
|
287
301
|
from gargoyle.issuer_service import app_from_env
|
|
@@ -457,6 +471,11 @@ def main(argv=None) -> int:
|
|
|
457
471
|
p.add_argument("--days", type=int, default=365)
|
|
458
472
|
p.add_argument("--url", help="the gate's public base URL for the printed connector URL")
|
|
459
473
|
p.set_defaults(fn=cmd_remote_token)
|
|
474
|
+
p = sub.add_parser("review-packet", help="build the archive an outside reviewer needs")
|
|
475
|
+
p.add_argument("--out", default="gargoyle-review-packet.zip")
|
|
476
|
+
p.add_argument("--audit-excerpt", help="a text file with real audit output to include")
|
|
477
|
+
p.add_argument("--extra", action="append", help="additional files to include (repeatable)")
|
|
478
|
+
p.set_defaults(fn=cmd_review_packet)
|
|
460
479
|
p = sub.add_parser("audit", help="verify a receipt chain offline, or a whole evidence log")
|
|
461
480
|
p.add_argument("receipt", nargs="?"); p.add_argument("execution", nargs="?"); p.add_argument("grant", nargs="?")
|
|
462
481
|
p.add_argument("--log", help="the middleware's audit.jsonl: verify every entry")
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""A review packet: everything an outside reviewer needs in one archive,
|
|
2
|
+
including a verifier that needs nothing but the Python standard library.
|
|
3
|
+
|
|
4
|
+
`gargoyle review-packet` writes a zip with SPEC.md, README.md, the
|
|
5
|
+
conformance vectors (current and historical), the verification modules
|
|
6
|
+
as source, the live audit excerpt if given, and verify_stdlib.py: RFC
|
|
7
|
+
8785 canonicalization, SHA-256 and Ed25519 verification implemented with
|
|
8
|
+
hashlib and integers only, so a reviewer in a sandbox with no pip and no
|
|
9
|
+
network can recompute every vector and check every signature themselves.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import io
|
|
13
|
+
import json
|
|
14
|
+
import zipfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
17
|
+
|
|
18
|
+
VERIFY_STDLIB = r'''#!/usr/bin/env python3
|
|
19
|
+
"""Recompute Gargoyle's conformance vectors with the standard library only.
|
|
20
|
+
|
|
21
|
+
No third-party packages: RFC 8785 canonical JSON, SHA-256 (hashlib) and
|
|
22
|
+
Ed25519 verification (RFC 8032, implemented here with integers) are enough
|
|
23
|
+
to check that every hash and signature in wire_vectors.json is what the
|
|
24
|
+
specification says. Run: python3 verify_stdlib.py wire_vectors.json
|
|
25
|
+
Exit code 0 means every check passed; each check prints its own line.
|
|
26
|
+
"""
|
|
27
|
+
import hashlib, json, sys
|
|
28
|
+
|
|
29
|
+
# ---- RFC 8785 canonical JSON ----
|
|
30
|
+
def _es6_number(x):
|
|
31
|
+
if x != x or x in (float("inf"), float("-inf")): raise ValueError("non-finite")
|
|
32
|
+
if x == 0: return "0"
|
|
33
|
+
sign = "-" if x < 0 else ""
|
|
34
|
+
r = repr(abs(x)); mantissa, _, exp = r.partition("e"); whole, _, frac = mantissa.partition(".")
|
|
35
|
+
digits = (whole + frac).lstrip("0"); n = len(whole) + (int(exp) if exp else 0)
|
|
36
|
+
n -= len(whole + frac) - len((whole + frac).lstrip("0")); digits = digits.rstrip("0") or "0"; k = len(digits)
|
|
37
|
+
if k <= n <= 21: return sign + digits + "0" * (n - k)
|
|
38
|
+
if 0 < n <= 21: return sign + digits[:n] + "." + digits[n:]
|
|
39
|
+
if -6 < n <= 0: return sign + "0." + "0" * (-n) + digits
|
|
40
|
+
e = n - 1; et = "e" + ("+" if e >= 0 else "-") + str(abs(e))
|
|
41
|
+
return sign + (digits if k == 1 else digits[0] + "." + digits[1:]) + et
|
|
42
|
+
|
|
43
|
+
_ESC = {"\b": "\\b", "\f": "\\f", "\n": "\\n", "\r": "\\r", "\t": "\\t", '"': '\\"', "\\": "\\\\"}
|
|
44
|
+
def _str(s):
|
|
45
|
+
return '"' + "".join(_ESC.get(c, c if c >= " " else "\\u%04x" % ord(c)) for c in s) + '"'
|
|
46
|
+
|
|
47
|
+
def canonical_json(v):
|
|
48
|
+
if v is None: return "null"
|
|
49
|
+
if v is True: return "true"
|
|
50
|
+
if v is False: return "false"
|
|
51
|
+
if isinstance(v, int): return str(v)
|
|
52
|
+
if isinstance(v, float): return _es6_number(v)
|
|
53
|
+
if isinstance(v, str): return _str(v)
|
|
54
|
+
if isinstance(v, list): return "[" + ",".join(canonical_json(x) for x in v) + "]"
|
|
55
|
+
if isinstance(v, dict):
|
|
56
|
+
return "{" + ",".join(_str(k) + ":" + canonical_json(v[k]) for k in sorted(v, key=lambda k: k.encode("utf-16-be"))) + "}"
|
|
57
|
+
raise TypeError(type(v))
|
|
58
|
+
|
|
59
|
+
def sha256_hex(text): return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
60
|
+
|
|
61
|
+
# ---- Ed25519 (RFC 8032) with integers only; slow, and that is fine for a review ----
|
|
62
|
+
P = 2**255 - 19; L = 2**252 + 27742317777372353535851937790883648493
|
|
63
|
+
D = (-121665 * pow(121666, P - 2, P)) % P
|
|
64
|
+
def _inv(x): return pow(x, P - 2, P)
|
|
65
|
+
def _pt_add(A, B):
|
|
66
|
+
x1, y1, z1, t1 = A; x2, y2, z2, t2 = B
|
|
67
|
+
a = (y1 - x1) * (y2 - x2) % P; b = (y1 + x1) * (y2 + x2) % P; c = 2 * t1 * t2 * D % P; d = 2 * z1 * z2 % P
|
|
68
|
+
e, f, g, h = b - a, d - c, d + c, b + a
|
|
69
|
+
return (e * f % P, g * h % P, f * g % P, e * h % P)
|
|
70
|
+
def _pt_mul(k, A):
|
|
71
|
+
R = (0, 1, 1, 0)
|
|
72
|
+
while k:
|
|
73
|
+
if k & 1: R = _pt_add(R, A)
|
|
74
|
+
A = _pt_add(A, A); k >>= 1
|
|
75
|
+
return R
|
|
76
|
+
def _pt_eq(A, B):
|
|
77
|
+
x1, y1, z1, _ = A; x2, y2, z2, _ = B
|
|
78
|
+
return (x1 * z2 - x2 * z1) % P == 0 and (y1 * z2 - y2 * z1) % P == 0
|
|
79
|
+
def _decode(b):
|
|
80
|
+
y = int.from_bytes(b, "little"); sign = y >> 255; y &= (1 << 255) - 1
|
|
81
|
+
if y >= P: return None
|
|
82
|
+
x2 = (y * y - 1) * _inv(D * y * y + 1) % P
|
|
83
|
+
x = pow(x2, (P + 3) // 8, P)
|
|
84
|
+
if (x * x - x2) % P != 0: x = x * pow(2, (P - 1) // 4, P) % P
|
|
85
|
+
if (x * x - x2) % P != 0: return None
|
|
86
|
+
if x & 1 != sign: x = P - x
|
|
87
|
+
return (x, y, 1, x * y % P)
|
|
88
|
+
_B = _decode(bytes.fromhex("5866666666666666666666666666666666666666666666666666666666666666"))
|
|
89
|
+
def ed25519_verify(public_key_hex, message_bytes, signature_hex):
|
|
90
|
+
A = _decode(bytes.fromhex(public_key_hex)); sig = bytes.fromhex(signature_hex)
|
|
91
|
+
if A is None or len(sig) != 64: return False
|
|
92
|
+
R = _decode(sig[:32]); S = int.from_bytes(sig[32:], "little")
|
|
93
|
+
if R is None or S >= L: return False
|
|
94
|
+
h = hashlib.sha512(sig[:32] + bytes.fromhex(public_key_hex) + message_bytes).digest()
|
|
95
|
+
k = int.from_bytes(h, "little") % L
|
|
96
|
+
return _pt_eq(_pt_mul(S, _B), _pt_add(R, _pt_mul(k, A)))
|
|
97
|
+
|
|
98
|
+
HASH_EXCLUDED = ("decision_sha256", "timestamp_utc", "issuer_key_id", "issuer_signature", "not_after", "decision_id", "approval_request_id")
|
|
99
|
+
|
|
100
|
+
def check(label, ok):
|
|
101
|
+
print(("PASS " if ok else "FAIL ") + label); return ok
|
|
102
|
+
|
|
103
|
+
def verify_receipt(signed, issuer_pub, results):
|
|
104
|
+
region = {k: v for k, v in signed.items() if k not in HASH_EXCLUDED}
|
|
105
|
+
results.append(check("decision_sha256 recomputes over the RFC 8785 hash region", sha256_hex(canonical_json(region)) == signed["decision_sha256"]))
|
|
106
|
+
srg = {k: v for k, v in signed.items() if k not in ("issuer_signature", "timestamp_utc")}
|
|
107
|
+
results.append(check("issuer_signature verifies (Ed25519, pure Python)", ed25519_verify(issuer_pub, canonical_json(srg).encode(), signed["issuer_signature"])))
|
|
108
|
+
|
|
109
|
+
def verify_grant(grant, approver_pub, decision_sha256, results):
|
|
110
|
+
region = {k: v for k, v in grant.items() if k != "approver_signature"}
|
|
111
|
+
results.append(check("grant binds the decision (%s)" % grant["approver_key_id"], grant["decision_sha256"] == decision_sha256))
|
|
112
|
+
results.append(check("approver_signature verifies (%s)" % grant["approver_key_id"], ed25519_verify(approver_pub, canonical_json(region).encode(), grant["approver_signature"])))
|
|
113
|
+
|
|
114
|
+
def verify_execution(ex, executor_pub, signed, results):
|
|
115
|
+
region = {k: v for k, v in ex.items() if k != "executor_signature"}
|
|
116
|
+
results.append(check("execution receipt binds the receipt field for field",
|
|
117
|
+
all(canonical_json(ex[k]) == canonical_json(signed[k]) for k in ("decision_id", "decision_sha256", "policy", "target_url", "request_context"))))
|
|
118
|
+
results.append(check("executor_signature verifies", ed25519_verify(executor_pub, canonical_json(region).encode(), ex["executor_signature"])))
|
|
119
|
+
|
|
120
|
+
def main(path):
|
|
121
|
+
V = json.load(open(path, encoding="utf-8")); results = []
|
|
122
|
+
print("== primary vector ==")
|
|
123
|
+
results.append(check("args_sha256 recomputes", sha256_hex(canonical_json(V["call"]["args"])) == V["args_sha256"]))
|
|
124
|
+
results.append(check("hash_region bytes match", canonical_json({k: v for k, v in V["signed"].items() if k not in HASH_EXCLUDED}) == V["hash_region"]))
|
|
125
|
+
verify_receipt(V["signed"], V["issuer_public_key_hex"], results)
|
|
126
|
+
verify_grant(V["grant"], V["approver_public_key_hex"], V["signed"]["decision_sha256"], results)
|
|
127
|
+
verify_execution(V["execution"], V["executor_public_key_hex"], V["signed"], results)
|
|
128
|
+
tampered = dict(V["signed"], target_url="https://elsewhere.example/")
|
|
129
|
+
srg = {k: v for k, v in tampered.items() if k not in ("issuer_signature", "timestamp_utc")}
|
|
130
|
+
results.append(check("a tampered receipt does NOT verify", not ed25519_verify(V["issuer_public_key_hex"], canonical_json(srg).encode(), tampered["issuer_signature"])))
|
|
131
|
+
if "threshold" in V:
|
|
132
|
+
T = V["threshold"]; print("== threshold vector (two approvers) ==")
|
|
133
|
+
verify_receipt(T["signed"], V["issuer_public_key_hex"], results)
|
|
134
|
+
results.append(check("approval terms are inside the hash region", '"approval":{"approvers":["approver-alice","approver-bob"],"required":2}' in T["hash_region"]))
|
|
135
|
+
for g, pub in zip(T["grants"], (V["approver_public_key_hex"], T["approver_2_public_key_hex"])):
|
|
136
|
+
verify_grant(g, pub, T["signed"]["decision_sha256"], results)
|
|
137
|
+
verify_execution(T["execution"], V["executor_public_key_hex"], T["signed"], results)
|
|
138
|
+
print("%d checks, %d failed" % (len(results), results.count(False)))
|
|
139
|
+
return 0 if all(results) else 1
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "wire_vectors.json"))
|
|
143
|
+
'''
|
|
144
|
+
|
|
145
|
+
SOURCE_FILES = ["envelope.py", "verifier.py", "approval.py", "execution.py", "evidence.py", "state.py", "plans.py",
|
|
146
|
+
"witness.py", "reconcile.py", "revert.py", "adapters.py", "consumer.py", "issuer.py", "jws.py"]
|
|
147
|
+
|
|
148
|
+
REVIEWER_PROMPT = '''You are an independent, skeptical reviewer of a prerelease open-source project. You
|
|
149
|
+
have this archive and nothing else; it is sufficient. Do not say "I could not access
|
|
150
|
+
the repository": every file you need is here, and verify_stdlib.py needs only Python's
|
|
151
|
+
standard library (no pip, no network).
|
|
152
|
+
|
|
153
|
+
Read SPEC.md in full, then README.md. Then:
|
|
154
|
+
|
|
155
|
+
1. Run `python3 verify_stdlib.py wire_vectors.json`. Report every PASS/FAIL line and
|
|
156
|
+
the count. If you cannot execute, read verify_stdlib.py and state which specific
|
|
157
|
+
step you would have expected to fail and why; a reviewer who cannot run it must
|
|
158
|
+
still read it.
|
|
159
|
+
2. Read the source files (envelope.py, verifier.py, approval.py, execution.py,
|
|
160
|
+
evidence.py, plans.py, witness.py, reconcile.py, state.py, revert.py, adapters.py,
|
|
161
|
+
consumer.py). For each attack below, point to the exact function and line that
|
|
162
|
+
stops it, or show the input that gets through:
|
|
163
|
+
a. a receipt honored for arguments other than the ones it binds;
|
|
164
|
+
b. a flagged receipt run without a grant from a trusted approver key;
|
|
165
|
+
c. a receipt, grant or plan step reused;
|
|
166
|
+
d. an execution receipt or evidence entry forged with the executor's own key, that
|
|
167
|
+
the audit accepts, with and without an issuer witness present;
|
|
168
|
+
e. a revert the audit marks exact that was not;
|
|
169
|
+
f. a file changed under a gated folder that the tree check misses;
|
|
170
|
+
g. a trust statement in SPEC.md sections 6.1, 8.3, 12, 15.1, 17 the code does not
|
|
171
|
+
uphold;
|
|
172
|
+
h. any place the code fails open: a missing key, an unreachable service, a
|
|
173
|
+
malformed input that is admitted rather than refused.
|
|
174
|
+
3. If audit_excerpt.txt is present, read it as a real deployment's audit output and
|
|
175
|
+
say what it does and does not establish.
|
|
176
|
+
4. Maturity for a first client beta: what you would require before letting this gate a
|
|
177
|
+
shared folder or a mailbox, citing the file or spec section that falls short.
|
|
178
|
+
|
|
179
|
+
Findings first, ordered by severity, each with file and line or the exact input. No
|
|
180
|
+
praise, no summary of the README. Where you did not verify, say exactly that.
|
|
181
|
+
'''
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def build_packet(repo_root: str, out_path: str, audit_excerpt: Optional[str] = None,
|
|
185
|
+
extra_files: Optional[Iterable[str]] = None) -> Dict[str, Any]:
|
|
186
|
+
root = Path(repo_root)
|
|
187
|
+
names: List[str] = []
|
|
188
|
+
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as z:
|
|
189
|
+
def add(arc: str, data: bytes) -> None:
|
|
190
|
+
z.writestr(arc, data); names.append(arc)
|
|
191
|
+
for rel in ("SPEC.md", "README.md", "docs/beta-checklist.md", "docs/ally-design.md"):
|
|
192
|
+
p = root / rel
|
|
193
|
+
if p.exists():
|
|
194
|
+
add(Path(rel).name if rel.startswith("docs/") else rel, p.read_bytes())
|
|
195
|
+
for rel in ("tests/fixtures/wire_vectors.json", "tests/fixtures/wire_vectors_v2.json"):
|
|
196
|
+
p = root / rel
|
|
197
|
+
if p.exists():
|
|
198
|
+
add(Path(rel).name, p.read_bytes())
|
|
199
|
+
for name in SOURCE_FILES:
|
|
200
|
+
p = root / "gargoyle" / name
|
|
201
|
+
if p.exists():
|
|
202
|
+
add("source/" + name, p.read_bytes())
|
|
203
|
+
add("verify_stdlib.py", VERIFY_STDLIB.encode("utf-8"))
|
|
204
|
+
add("REVIEWER_PROMPT.txt", REVIEWER_PROMPT.encode("utf-8"))
|
|
205
|
+
if audit_excerpt:
|
|
206
|
+
add("audit_excerpt.txt", audit_excerpt.encode("utf-8"))
|
|
207
|
+
for extra in extra_files or []:
|
|
208
|
+
p = Path(extra)
|
|
209
|
+
if p.exists():
|
|
210
|
+
add("extra/" + p.name, p.read_bytes())
|
|
211
|
+
manifest = {"files": names, "note": "Built by gargoyle review-packet. verify_stdlib.py needs only the Python standard library."}
|
|
212
|
+
z.writestr("MANIFEST.json", json.dumps(manifest, indent=2))
|
|
213
|
+
return {"path": out_path, "files": names + ["MANIFEST.json"]}
|
{gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0/gargoyle_receipts.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gargoyle-receipts
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.1.dev0
|
|
4
4
|
Summary: Signed, single-use, argument-bound receipts and human-approval holds for agent tool calls.
|
|
5
5
|
Author: Justin LaPorte
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -59,7 +59,7 @@ your MCP server and lets through only what both agreed to.
|
|
|
59
59
|
|
|
60
60
|
[](https://pypi.org/project/gargoyle-receipts/)
|
|
61
61
|
|
|
62
|
-
Status: pre-release (`0.15.
|
|
62
|
+
Status: pre-release (`0.15.1.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
|
|
63
63
|
still change before the first release; it is specified in [SPEC.md](SPEC.md).
|
|
64
64
|
|
|
65
65
|
## What a receipt gives you
|
|
@@ -272,6 +272,15 @@ field. A hold can also be announced to Slack or any webhook
|
|
|
272
272
|
(`GARGOYLE_HOLD_WEBHOOK_URL`); the notice carries no key, so nothing in a
|
|
273
273
|
chat channel can approve anything.
|
|
274
274
|
|
|
275
|
+
## Reviewing it
|
|
276
|
+
|
|
277
|
+
`gargoyle review-packet --out packet.zip` builds one archive for an outside
|
|
278
|
+
reviewer: the spec, the vectors, the verification source, a reviewer
|
|
279
|
+
prompt, and `verify_stdlib.py`, which recomputes every conformance vector
|
|
280
|
+
(RFC 8785, SHA-256, Ed25519) with nothing but the Python standard library,
|
|
281
|
+
so a reviewer with no pip and no network can still check every hash and
|
|
282
|
+
signature rather than take the README's word.
|
|
283
|
+
|
|
275
284
|
## Deploy it
|
|
276
285
|
|
|
277
286
|
pip install gargoyle-receipts
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gargoyle-receipts"
|
|
7
|
-
version = "0.15.
|
|
7
|
+
version = "0.15.1.dev0"
|
|
8
8
|
description = "Signed, single-use, argument-bound receipts and human-approval holds for agent tool calls."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -119,3 +119,22 @@ def test_init_for_a_windows_client_and_audit_log_verification(tmp_path, monkeypa
|
|
|
119
119
|
assert main(["audit", "--log", str(log), "--out", str(out)]) == 1
|
|
120
120
|
out_text = capsys.readouterr().out
|
|
121
121
|
assert "1 failed" in out_text and "REF_EVIDENCE_HASH_MISMATCH" in out_text
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_review_packet_builds_and_its_verifier_runs_on_plain_python(tmp_path):
|
|
125
|
+
"""The packet verifies the real vectors with the standard library only,
|
|
126
|
+
run through an isolated interpreter that has no site-packages."""
|
|
127
|
+
import subprocess, sys, zipfile
|
|
128
|
+
from gargoyle.cli import main
|
|
129
|
+
out = tmp_path / "packet.zip"
|
|
130
|
+
(tmp_path / "audit.txt").write_text("45 entries (42 honored, 3 refused), 0 failed")
|
|
131
|
+
assert main(["review-packet", "--out", str(out), "--audit-excerpt", str(tmp_path / "audit.txt")]) == 0
|
|
132
|
+
with zipfile.ZipFile(out) as z:
|
|
133
|
+
names = set(z.namelist())
|
|
134
|
+
assert {"SPEC.md", "README.md", "wire_vectors.json", "wire_vectors_v2.json", "verify_stdlib.py", "REVIEWER_PROMPT.txt",
|
|
135
|
+
"audit_excerpt.txt", "source/verifier.py", "source/plans.py", "MANIFEST.json"} <= names
|
|
136
|
+
z.extractall(tmp_path / "x")
|
|
137
|
+
r = subprocess.run([sys.executable, "-I", "-S", str(tmp_path / "x" / "verify_stdlib.py"), str(tmp_path / "x" / "wire_vectors.json")],
|
|
138
|
+
capture_output=True, text=True)
|
|
139
|
+
assert r.returncode == 0, r.stdout + r.stderr
|
|
140
|
+
assert "18 checks, 0 failed" in r.stdout and "a tampered receipt does NOT verify" in r.stdout
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/key_record_source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/published_record_source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/specs/graph-reference.json
RENAMED
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/gargoyle/static/approve.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_entra_identity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_issuer_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_mcp_self_issue.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_published_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_stdio_encoding.py
RENAMED
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.15.0.dev0 → gargoyle_receipts-0.15.1.dev0}/tests/test_typescript_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|