gargoyle-receipts 0.14.0.dev0__tar.gz → 0.14.2.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.14.0.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.14.2.dev0}/PKG-INFO +2 -2
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/README.md +1 -1
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/cli.py +14 -1
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/reconcile.py +11 -2
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/witness.py +7 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0/gargoyle_receipts.egg-info}/PKG-INFO +2 -2
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/pyproject.toml +1 -1
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_witness.py +26 -7
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/LICENSE +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/NOTICE +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/__init__.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/adapters.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/approval.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/approver_cli.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/approver_trust.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/approvers.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/consumer.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/entra_login.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/envelope.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/evidence.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/execution.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/fs_server.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/holdstore.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/identity.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/issuer.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/issuer_service.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/jws.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/key_record_source.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/mcp_server.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/notify.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/opa.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/pending_store.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/plans.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/policy.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/published_record_source.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/remote.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/replay_cache.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/revert.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/rules.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/specs/graph-reference.json +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/state.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/static/approve.html +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/static/approve.js +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/verifier.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle_receipts.egg-info/SOURCES.txt +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle_receipts.egg-info/dependency_links.txt +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle_receipts.egg-info/entry_points.txt +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle_receipts.egg-info/requires.txt +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle_receipts.egg-info/top_level.txt +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/setup.cfg +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_annotations.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_approver_cli.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_approvers.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_canonical.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_cli.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_console_js.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_consumer.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_entra_identity.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_entra_login.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_evidence.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_execution.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_fs_server.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_graph.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_historical.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_identity.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_issuer_service.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_jws.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_mcp_proxy.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_mcp_self_issue.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_mcp_server.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_notify.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_opa.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_plans.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_policy.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_published_record.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_remote.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_revert.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_rules.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_state.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_stdio_encoding.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_threshold.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_typescript_conformance.py +0 -0
- {gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_wire_spec.py +0 -0
{gargoyle_receipts-0.14.0.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.14.2.dev0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gargoyle-receipts
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.2.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.14.
|
|
62
|
+
Status: pre-release (`0.14.2.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
|
|
@@ -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.14.
|
|
24
|
+
Status: pre-release (`0.14.2.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
|
|
@@ -361,6 +361,12 @@ def cmd_audit(args) -> int:
|
|
|
361
361
|
% (v["count"], v["honored"], v["refused"], failed, v["reason"], v["tail_sha256"][:16]))
|
|
362
362
|
rc = 0 if v["accepted"] else 1
|
|
363
363
|
entries = read_log(args.log)
|
|
364
|
+
if args.witness and not Path(args.witness).exists():
|
|
365
|
+
print("witness file not found: %s (copy the issuer's witness.jsonl here first)" % args.witness, file=sys.stderr)
|
|
366
|
+
return 2
|
|
367
|
+
if args.check_tree and not Path(args.check_tree).is_dir():
|
|
368
|
+
print("not a directory: %s" % args.check_tree, file=sys.stderr)
|
|
369
|
+
return 2
|
|
364
370
|
if args.witness:
|
|
365
371
|
# The issuer's signed record: the client's evidence must agree with it.
|
|
366
372
|
from gargoyle.reconcile import reconcile
|
|
@@ -369,9 +375,16 @@ def cmd_audit(args) -> int:
|
|
|
369
375
|
print("witness: %d entries, %s, tail %s" % (w["count"], w["reason"], w["tail_sha256"][:16]))
|
|
370
376
|
principal = next((e.get("principal") for e in entries if isinstance(e, dict) and e.get("principal")), None)
|
|
371
377
|
r = reconcile(entries, read_witness(args.witness), principal=principal)
|
|
378
|
+
older = [f for f in r["findings"] if f["finding"] == "PREDATES_WITNESS"]
|
|
372
379
|
for f in r["findings"]:
|
|
380
|
+
if f["finding"] == "PREDATES_WITNESS":
|
|
381
|
+
continue
|
|
373
382
|
print(" FINDING %-22s %s" % (f["finding"], " ".join("%s=%s" % (k, str(v)[:40]) for k, v in f.items() if k != "finding")))
|
|
374
|
-
|
|
383
|
+
if older:
|
|
384
|
+
print(" note: %d honored row(s) predate the witness (started %s) and cannot be vouched for either way: seq %s"
|
|
385
|
+
% (len(older), (r.get("horizon") or "?")[:19], ", ".join(str(f["seq"]) for f in older[:12]) + (" ..." if len(older) > 12 else "")))
|
|
386
|
+
hard = len(r["findings"]) - len(older)
|
|
387
|
+
print("reconciled: %d issued by the issuer, %d in evidence, %d finding(s)" % (r["issued"], r["evidence_with_receipt"], hard))
|
|
375
388
|
rc = rc or (0 if (w["accepted"] and r["accepted"]) else 1)
|
|
376
389
|
if args.check_tree:
|
|
377
390
|
# Every file the gate wrote must still be as the gate left it.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
the gated folder against the evidence.
|
|
3
3
|
|
|
4
4
|
Findings, never silently fixed:
|
|
5
|
+
PREDATES_WITNESS evidence from before the witness began; it can be vouched for neither way (information)
|
|
5
6
|
UNWITNESSED_RECEIPT evidence honors a decision the issuer never issued (forged or foreign)
|
|
6
7
|
UNWITNESSED_RELEASE a held call was honored but the issuer saw no grant for its hold (or plan)
|
|
7
8
|
MISSING_EVIDENCE the issuer issued a receipt to this principal and the client recorded nothing
|
|
@@ -33,9 +34,12 @@ def reconcile(evidence: Iterable[Any], witness: Iterable[Any], *, principal: Opt
|
|
|
33
34
|
issued: Dict[str, Dict[str, Any]] = {}
|
|
34
35
|
granted: Dict[str, List[Dict[str, Any]]] = {}
|
|
35
36
|
plan_granted: Dict[str, List[Dict[str, Any]]] = {}
|
|
37
|
+
horizon: Optional[datetime] = None
|
|
36
38
|
for w in witness:
|
|
37
39
|
if not isinstance(w, dict):
|
|
38
40
|
continue
|
|
41
|
+
if horizon is None:
|
|
42
|
+
horizon = _parse(w.get("at"))
|
|
39
43
|
if w.get("event") == "issued" and isinstance(w.get("decision_id"), str):
|
|
40
44
|
issued[w["decision_id"]] = w
|
|
41
45
|
elif w.get("event") == "granted" and isinstance(w.get("approval_request_id"), str):
|
|
@@ -54,7 +58,11 @@ def reconcile(evidence: Iterable[Any], witness: Iterable[Any], *, principal: Opt
|
|
|
54
58
|
w = issued.get(did)
|
|
55
59
|
if e.get("kind") == "honored":
|
|
56
60
|
if w is None:
|
|
57
|
-
|
|
61
|
+
at_e = _parse(e.get("at"))
|
|
62
|
+
if horizon is not None and at_e is not None and at_e < horizon:
|
|
63
|
+
findings.append({"finding": "PREDATES_WITNESS", "seq": e.get("seq"), "decision_id": did, "tool": e.get("tool")})
|
|
64
|
+
else:
|
|
65
|
+
findings.append({"finding": "UNWITNESSED_RECEIPT", "seq": e.get("seq"), "decision_id": did, "tool": e.get("tool")})
|
|
58
66
|
continue
|
|
59
67
|
if w.get("decision_sha256") != receipt.get("decision_sha256") or w.get("principal") != receipt.get("request_context", {}).get("principal"):
|
|
60
68
|
findings.append({"finding": "UNWITNESSED_RECEIPT", "seq": e.get("seq"), "decision_id": did, "tool": e.get("tool")})
|
|
@@ -78,7 +86,8 @@ def reconcile(evidence: Iterable[Any], witness: Iterable[Any], *, principal: Opt
|
|
|
78
86
|
continue
|
|
79
87
|
findings.append({"finding": "MISSING_EVIDENCE", "decision_id": did, "tool": w.get("tool"), "principal": w.get("principal"),
|
|
80
88
|
"held": bool(w.get("held")), "at": w.get("at")})
|
|
81
|
-
|
|
89
|
+
hard = [f for f in findings if f["finding"] != "PREDATES_WITNESS"]
|
|
90
|
+
return {"accepted": not hard, "findings": findings, "horizon": horizon.isoformat() if horizon else None,
|
|
82
91
|
"issued": len(issued), "evidence_with_receipt": len(seen_decisions)}
|
|
83
92
|
|
|
84
93
|
|
|
@@ -63,6 +63,13 @@ class WitnessLog:
|
|
|
63
63
|
e = json.loads(line)
|
|
64
64
|
self.entries.append(e)
|
|
65
65
|
self._seq, self._tail = int(e["seq"]), e["entry_sha256"]
|
|
66
|
+
elif self.path is not None:
|
|
67
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
self.path.touch()
|
|
69
|
+
# Every start is on record: the first "started" entry is the witness's
|
|
70
|
+
# horizon (evidence older than it cannot be vouched for either way),
|
|
71
|
+
# and later ones mark restarts for the operator.
|
|
72
|
+
self.record("started", resumed=bool(self.entries))
|
|
66
73
|
|
|
67
74
|
def record(self, event: str, **fields: Any) -> Dict[str, Any]:
|
|
68
75
|
with self._lock:
|
{gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.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.14.
|
|
3
|
+
Version: 0.14.2.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.14.
|
|
62
|
+
Status: pre-release (`0.14.2.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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gargoyle-receipts"
|
|
7
|
-
version = "0.14.
|
|
7
|
+
version = "0.14.2.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"
|
|
@@ -46,12 +46,13 @@ def test_witness_entries_are_signed_and_chained(tmp_path):
|
|
|
46
46
|
again.record("declined", approval_request_id="r-2")
|
|
47
47
|
entries = read_witness(str(tmp_path / "w.jsonl"))
|
|
48
48
|
v = verify_witness(entries, issuer_public_keys=ISSUER_KEYS)
|
|
49
|
-
assert v["accepted"] and v["count"] ==
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
assert v["accepted"] and v["count"] == 5 and [e["event"] for e in entries] == ["started", "issued", "granted", "started", "declined"]
|
|
50
|
+
assert entries[3]["resumed"] is True and entries[0]["resumed"] is False
|
|
51
|
+
forged = dict(entries[2], approver="mallory")
|
|
52
|
+
assert verify_witness(entries[:2] + [forged] + entries[3:], issuer_public_keys=ISSUER_KEYS)["entries"][2]["reason"] == REF_WITNESS_HASH_MISMATCH
|
|
52
53
|
from gargoyle.witness import entry_sha256
|
|
53
54
|
forged["entry_sha256"] = entry_sha256(forged) # rehashed, but the issuer did not sign it
|
|
54
|
-
assert verify_witness(
|
|
55
|
+
assert verify_witness(entries[:2] + [forged], issuer_public_keys=ISSUER_KEYS)["entries"][2]["reason"] == REF_WITNESS_SIGNATURE_INVALID
|
|
55
56
|
assert not verify_witness(entries, issuer_public_keys={"other": executor.public_key()})["accepted"]
|
|
56
57
|
|
|
57
58
|
|
|
@@ -95,14 +96,18 @@ def test_honest_evidence_reconciles_and_tampering_is_found(tmp_path):
|
|
|
95
96
|
finally:
|
|
96
97
|
up.close()
|
|
97
98
|
evidence = read_log(str(tmp_path / "audit.jsonl")); witness = read_witness(str(tmp_path / "witness.jsonl"))
|
|
98
|
-
assert [e["event"] for e in witness] == ["issued", "issued", "granted"]
|
|
99
|
+
assert [e["event"] for e in witness] == ["started", "issued", "issued", "granted"]
|
|
99
100
|
assert evidence[1]["ref"] == str(root / "n.md") and evidence[1]["evidence_version"] == "gargoyle-evidence/4"
|
|
100
101
|
assert verify_log(evidence, issuer_public_keys=ISSUER_KEYS, executor_public_keys={"executor-1": executor.public_key()},
|
|
101
102
|
approver_public_keys={"approver-1": approver.public_key()})["accepted"]
|
|
102
103
|
r = reconcile(evidence, witness, principal=PRINCIPAL)
|
|
103
104
|
assert r["accepted"] and r["issued"] == 2 and r["evidence_with_receipt"] == 2
|
|
104
105
|
# The witness is served to an authenticated caller too.
|
|
105
|
-
assert len(c.get("/witness", headers=h).json()["entries"]) ==
|
|
106
|
+
assert len(c.get("/witness", headers=h).json()["entries"]) == 4
|
|
107
|
+
# Evidence from before the witness began is information, not a finding.
|
|
108
|
+
old = dict(evidence[0], at="2026-01-01T00:00:00+00:00", receipt=dict(evidence[0]["receipt"], decision_id="before-the-witness"))
|
|
109
|
+
r = reconcile(evidence + [old], witness, principal=PRINCIPAL) # beside the real rows, not in place of one
|
|
110
|
+
assert r["accepted"] and [f["finding"] for f in r["findings"]] == ["PREDATES_WITNESS"] and r["horizon"]
|
|
106
111
|
|
|
107
112
|
# 1. A forged honored entry for a decision the issuer never issued: the user holds the executor key, so it verifies alone.
|
|
108
113
|
now = datetime.now(timezone.utc)
|
|
@@ -145,7 +150,21 @@ def test_audit_command_reports_reconciliation_and_tree(tmp_path, capsys):
|
|
|
145
150
|
assert main(["audit", "--log", str(tmp_path / "audit.jsonl"), "--out", str(out), "--witness", str(tmp_path / "witness.jsonl"),
|
|
146
151
|
"--check-tree", str(root)]) == 0
|
|
147
152
|
text = capsys.readouterr().out
|
|
148
|
-
assert "witness:
|
|
153
|
+
assert "witness: 3 entries, WITNESS_VERIFIED" in text and "0 finding(s)" in text and "1 gated file(s) checked" in text
|
|
149
154
|
(root / "n.md").write_text("tampered", encoding="utf-8")
|
|
150
155
|
assert main(["audit", "--log", str(tmp_path / "audit.jsonl"), "--out", str(out), "--check-tree", str(root)]) == 1
|
|
151
156
|
assert "CHANGED_OUTSIDE_GATE" in capsys.readouterr().out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_audit_names_a_missing_witness_or_folder_plainly(tmp_path, capsys):
|
|
160
|
+
from gargoyle.cli import main
|
|
161
|
+
log = tmp_path / "audit.jsonl"; log.write_text("")
|
|
162
|
+
out = tmp_path / "deploy"; out.mkdir()
|
|
163
|
+
(out / "public-keys.json").write_text(json.dumps({"issuer": {}, "executor": {}, "approver": {}}))
|
|
164
|
+
assert main(["audit", "--log", str(log), "--out", str(out), "--witness", str(tmp_path / "nope.jsonl")]) == 2
|
|
165
|
+
assert "witness file not found" in capsys.readouterr().err
|
|
166
|
+
assert main(["audit", "--log", str(log), "--out", str(out), "--check-tree", str(tmp_path / "nodir")]) == 2
|
|
167
|
+
assert "not a directory" in capsys.readouterr().err
|
|
168
|
+
# The issuer creates the witness file at start, before any decision.
|
|
169
|
+
w = WitnessLog(str(tmp_path / "fresh" / "witness.jsonl"), issuer, "issuer-1")
|
|
170
|
+
assert (tmp_path / "fresh" / "witness.jsonl").exists() and [e["event"] for e in w.entries] == ["started"]
|
|
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.14.0.dev0 → gargoyle_receipts-0.14.2.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.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/published_record_source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/gargoyle/specs/graph-reference.json
RENAMED
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.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
|
|
File without changes
|
{gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.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.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_issuer_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.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.14.0.dev0 → gargoyle_receipts-0.14.2.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.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_stdio_encoding.py
RENAMED
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.14.0.dev0 → gargoyle_receipts-0.14.2.dev0}/tests/test_typescript_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|