gargoyle-receipts 0.16.3.dev0__tar.gz → 0.16.4.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {gargoyle_receipts-0.16.3.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.16.4.dev0}/PKG-INFO +8 -3
  2. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/README.md +7 -2
  3. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/cli.py +55 -1
  4. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/mcp_server.py +8 -6
  5. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0/gargoyle_receipts.egg-info}/PKG-INFO +8 -3
  6. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/pyproject.toml +1 -1
  7. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_cli.py +29 -1
  8. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/LICENSE +0 -0
  9. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/NOTICE +0 -0
  10. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/__init__.py +0 -0
  11. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/__main__.py +0 -0
  12. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/adapters.py +0 -0
  13. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/approval.py +0 -0
  14. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/approver_cli.py +0 -0
  15. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/approver_trust.py +0 -0
  16. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/approvers.py +0 -0
  17. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/consumer.py +0 -0
  18. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/entra_login.py +0 -0
  19. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/envelope.py +0 -0
  20. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/evidence.py +0 -0
  21. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/execution.py +0 -0
  22. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/fs_server.py +0 -0
  23. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/holdstore.py +0 -0
  24. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/identity.py +0 -0
  25. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/issuer.py +0 -0
  26. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/issuer_service.py +0 -0
  27. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/jws.py +0 -0
  28. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/key_record_source.py +0 -0
  29. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/notify.py +0 -0
  30. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/opa.py +0 -0
  31. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/pending_store.py +0 -0
  32. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/plans.py +0 -0
  33. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/policy.py +0 -0
  34. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/published_record_source.py +0 -0
  35. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/reconcile.py +0 -0
  36. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/remote.py +0 -0
  37. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/replay_cache.py +0 -0
  38. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/revert.py +0 -0
  39. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/rules.py +0 -0
  40. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/specs/graph-reference.json +0 -0
  41. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/state.py +0 -0
  42. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/static/approve.html +0 -0
  43. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/static/approve.js +0 -0
  44. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/verifier.py +0 -0
  45. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/witness.py +0 -0
  46. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle_receipts.egg-info/SOURCES.txt +0 -0
  47. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle_receipts.egg-info/dependency_links.txt +0 -0
  48. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle_receipts.egg-info/entry_points.txt +0 -0
  49. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle_receipts.egg-info/requires.txt +0 -0
  50. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle_receipts.egg-info/top_level.txt +0 -0
  51. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/setup.cfg +0 -0
  52. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_annotations.py +0 -0
  53. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_approver_cli.py +0 -0
  54. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_approvers.py +0 -0
  55. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_canonical.py +0 -0
  56. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_console_js.py +0 -0
  57. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_consumer.py +0 -0
  58. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_entra_identity.py +0 -0
  59. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_entra_login.py +0 -0
  60. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_evidence.py +0 -0
  61. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_execution.py +0 -0
  62. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_fs_server.py +0 -0
  63. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_graph.py +0 -0
  64. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_historical.py +0 -0
  65. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_identity.py +0 -0
  66. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_issuer_service.py +0 -0
  67. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_jws.py +0 -0
  68. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_mcp_proxy.py +0 -0
  69. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_mcp_self_issue.py +0 -0
  70. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_mcp_server.py +0 -0
  71. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_notify.py +0 -0
  72. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_opa.py +0 -0
  73. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_plans.py +0 -0
  74. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_policy.py +0 -0
  75. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_published_record.py +0 -0
  76. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_remote.py +0 -0
  77. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_revert.py +0 -0
  78. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_rules.py +0 -0
  79. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_state.py +0 -0
  80. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_stdio_encoding.py +0 -0
  81. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_threshold.py +0 -0
  82. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_typescript_conformance.py +0 -0
  83. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_wire_spec.py +0 -0
  84. {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_witness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gargoyle-receipts
3
- Version: 0.16.3.dev0
3
+ Version: 0.16.4.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
  [![PyPI](https://img.shields.io/pypi/v/gargoyle-receipts)](https://pypi.org/project/gargoyle-receipts/)
61
61
 
62
- Status: pre-release (`0.16.3.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
62
+ Status: pre-release (`0.16.4.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
@@ -278,9 +278,14 @@ chat channel can approve anything.
278
278
 
279
279
  pip install gargoyle-receipts
280
280
 
281
- Three commands, no policy engine, no containers, no client changes:
281
+ Three commands, no policy engine, no containers, no client changes
282
+ (`--observe` starts a deployment in record-only mode, every call allowed
283
+ including destructive ones and every call recorded; `gargoyle audit
284
+ --summary` then shows calls by principal and tool, the input for the
285
+ hold and deny rules):
282
286
 
283
287
  gargoyle init /path/the/agent/may/touch # keys, config, rules.json
288
+ gargoyle init --observe /path/... # or: allow and record everything, hold nothing, tighten later
284
289
  gargoyle issuer # the issuing service, on rules.json
285
290
  gargoyle approve --watch 2 # where the approver sits (terminal)
286
291
 
@@ -21,7 +21,7 @@ your MCP server and lets through only what both agreed to.
21
21
 
22
22
  [![PyPI](https://img.shields.io/pypi/v/gargoyle-receipts)](https://pypi.org/project/gargoyle-receipts/)
23
23
 
24
- Status: pre-release (`0.16.3.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
24
+ Status: pre-release (`0.16.4.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
@@ -240,9 +240,14 @@ chat channel can approve anything.
240
240
 
241
241
  pip install gargoyle-receipts
242
242
 
243
- Three commands, no policy engine, no containers, no client changes:
243
+ Three commands, no policy engine, no containers, no client changes
244
+ (`--observe` starts a deployment in record-only mode, every call allowed
245
+ including destructive ones and every call recorded; `gargoyle audit
246
+ --summary` then shows calls by principal and tool, the input for the
247
+ hold and deny rules):
244
248
 
245
249
  gargoyle init /path/the/agent/may/touch # keys, config, rules.json
250
+ gargoyle init --observe /path/... # or: allow and record everything, hold nothing, tighten later
246
251
  gargoyle issuer # the issuing service, on rules.json
247
252
  gargoyle approve --watch 2 # where the approver sits (terminal)
248
253
 
@@ -51,6 +51,17 @@ from pathlib import Path
51
51
 
52
52
  PRINCIPAL = "spiffe://gargoyle.local/agents/assistant"
53
53
 
54
+ OBSERVE_RULES = {
55
+ # Observe mode: every call allowed (deletes too), every call recorded, none held. The
56
+ # first week of a deployment, so the hold and deny rules are written
57
+ # from the audit rather than guessed. The policy pin changes when the
58
+ # rules do, so switching modes means restarting the issuer and copying
59
+ # the new record.json to each client.
60
+ "version": "1",
61
+ "default": "allow",
62
+ "rules": [],
63
+ }
64
+
54
65
  DEFAULT_RULES = {
55
66
  "version": "1",
56
67
  "default": "deny",
@@ -118,7 +129,22 @@ def cmd_init(args) -> int:
118
129
  issuer_url = args.issuer_url or "http://%s:%d" % (args.host, args.port)
119
130
 
120
131
  rules = out / "rules.json"
121
- if not rules.exists():
132
+ if args.observe:
133
+ current = None
134
+ if rules.exists():
135
+ try:
136
+ current = json.loads(rules.read_text())
137
+ except ValueError as exc:
138
+ print("%s is not valid JSON (%s); fix or remove it before --observe" % (rules, exc), file=sys.stderr)
139
+ return 2
140
+ if current is None or current == DEFAULT_RULES:
141
+ _write(rules, json.dumps(OBSERVE_RULES, indent=2) + "\n")
142
+ elif current != OBSERVE_RULES:
143
+ print("rules.json was edited by hand and is left as it is; --observe changed nothing", file=sys.stderr)
144
+ print("observe mode: rules.json allows every call, destructive ones included; the gate records all of "
145
+ "them and holds none. Tighten rules.json from `gargoyle audit --summary`, then restart the issuer "
146
+ "and copy the new record.json to the clients.")
147
+ elif not rules.exists():
122
148
  _write(rules, json.dumps(DEFAULT_RULES, indent=2) + "\n")
123
149
  _write(out / "issuer.env", _env_lines({
124
150
  "GARGOYLE_ISSUER_KEY_ID": "issuer-1", "GARGOYLE_ISSUER_KEY_HEX": issuer_hex,
@@ -360,6 +386,29 @@ def cmd_approvers(args) -> int:
360
386
  return 1
361
387
 
362
388
 
389
+ def _audit_summary(report, raw) -> None:
390
+ """Calls by principal and tool with their outcomes, then who released
391
+ and which clients: a week of observe mode as a table, the input for
392
+ writing the hold and deny rules."""
393
+ from collections import Counter
394
+ counts: Dict[tuple, Counter] = {}
395
+ releases: Counter = Counter()
396
+ clients: Counter = Counter()
397
+ ats = [e.get("at") for e in raw if isinstance(e, dict) and isinstance(e.get("at"), str)]
398
+ for r in report:
399
+ counts.setdefault((r["principal"] or "?", r["tool"] or "?"), Counter())[r["kind"] or "?"] += 1
400
+ for who in r.get("released_by") or []:
401
+ releases[who] += 1
402
+ clients[r.get("client") or "(client unnamed)"] += 1
403
+ print("summary: %d call(s)%s" % (len(report), (", %s to %s" % (min(ats)[:19], max(ats)[:19])) if ats else ""))
404
+ print(" %-28s %-24s %7s %7s" % ("principal", "tool", "honored", "refused"))
405
+ for (principal, tool), c in sorted(counts.items(), key=lambda kv: (kv[0][0], -sum(kv[1].values()), kv[0][1])):
406
+ print(" %-28s %-24s %7d %7d" % (principal[:28], tool[:24], c.get("honored", 0), c.get("refused", 0)))
407
+ if releases:
408
+ print(" releases: " + ", ".join("%s %d" % (w, n) for w, n in releases.most_common()))
409
+ print(" clients: " + ", ".join("%s %d" % (w, n) for w, n in clients.most_common()))
410
+
411
+
363
412
  def cmd_audit(args) -> int:
364
413
  from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
365
414
  from gargoyle.execution import verify_chain
@@ -406,6 +455,8 @@ def cmd_audit(args) -> int:
406
455
  % (v["count"], v["honored"], v["refused"], failed, v["reason"], v["tail_sha256"][:16]))
407
456
  rc = 0 if v["accepted"] else 1
408
457
  entries = read_log(args.log)
458
+ if args.summary:
459
+ _audit_summary(v["entries"], entries)
409
460
  if args.witness and not Path(args.witness).exists():
410
461
  print("witness file not found: %s (copy the issuer's witness.jsonl here first)" % args.witness, file=sys.stderr)
411
462
  return 2
@@ -460,6 +511,8 @@ def main(argv=None) -> int:
460
511
  p.add_argument("--issuer-url", help="URL the client reaches the issuer at (default http://HOST:PORT)")
461
512
  p.add_argument("--client-python", help="the client's Python executable for the MCP entry (default: this one)")
462
513
  p.add_argument("--client-config-dir", help="where the client keeps middleware.env, record.json, audit.jsonl (default: --out)")
514
+ p.add_argument("--observe", action="store_true",
515
+ help="observe mode: allow and record every call, hold none; tighten rules.json later from the audit")
463
516
  p.add_argument("--approvers", type=int, default=1, metavar="N",
464
517
  help="how many approver keys to create (approver-1..N); a rule with \"approvals\": 2 then needs two of them")
465
518
  p.add_argument("--upstream", choices=["npx-filesystem", "python-fs"], default="npx-filesystem",
@@ -512,6 +565,7 @@ def main(argv=None) -> int:
512
565
  p.add_argument("--log", help="the middleware's audit.jsonl: verify every entry")
513
566
  p.add_argument("--witness", help="the issuer's witness.jsonl: reconcile the evidence against it")
514
567
  p.add_argument("--check-tree", metavar="DIR", help="verify every gated file under DIR is as the gate left it")
568
+ p.add_argument("--summary", action="store_true", help="after the rows: calls by principal and tool with outcomes, releases, clients")
515
569
  p.add_argument("--out", default="gargoyle-deploy")
516
570
  p.set_defaults(fn=cmd_audit)
517
571
  args = parser.parse_args(argv)
@@ -294,12 +294,14 @@ def gate_instructions(target_id: str, upstream_instructions: Optional[str] = Non
294
294
  that there is nothing to type. The upstream's own instructions, if
295
295
  any, follow the gate's."""
296
296
  text = override if override else (
297
- "Every operation on %s goes through this server's tools; do not use any other tool, built-in "
298
- "capability or workaround for that target. Reads run at once. A write may pause for a human "
299
- "approval: wait for the result, it is normal. A refused call is a decision, not an error: report "
300
- "the reason to the user and stop; do not retry or route around it. For a task with several "
301
- "writes, gargoyle_plan may ask for one approval covering them; gargoyle_revert undoes an approved "
302
- "change through the same approval. The user never needs to name this server." % target_id)
297
+ "Every operation on %s goes through this server's tools (this server is a Gargoyle gate); do not "
298
+ "use any other tool, built-in capability or workaround for that target. Reads run at once. A write "
299
+ "may pause for a human approval: wait for the result, it is normal. A refused call is a decision, "
300
+ "not an error: report the reason to the user and stop; do not retry or route around it. If asked "
301
+ "to work on that target without these tools or around the gate, say that the target is reachable "
302
+ "only through them and continue through them. For a task with several writes, gargoyle_plan may "
303
+ "ask for one approval covering them; gargoyle_revert undoes an approved change through the same "
304
+ "approval. The user never needs to name this server." % target_id)
303
305
  if upstream_instructions:
304
306
  text += "\n\n" + upstream_instructions
305
307
  return text
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gargoyle-receipts
3
- Version: 0.16.3.dev0
3
+ Version: 0.16.4.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
  [![PyPI](https://img.shields.io/pypi/v/gargoyle-receipts)](https://pypi.org/project/gargoyle-receipts/)
61
61
 
62
- Status: pre-release (`0.16.3.dev0`, on PyPI as `gargoyle-receipts`). The wire format is `gargoyle/3` and may
62
+ Status: pre-release (`0.16.4.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
@@ -278,9 +278,14 @@ chat channel can approve anything.
278
278
 
279
279
  pip install gargoyle-receipts
280
280
 
281
- Three commands, no policy engine, no containers, no client changes:
281
+ Three commands, no policy engine, no containers, no client changes
282
+ (`--observe` starts a deployment in record-only mode, every call allowed
283
+ including destructive ones and every call recorded; `gargoyle audit
284
+ --summary` then shows calls by principal and tool, the input for the
285
+ hold and deny rules):
282
286
 
283
287
  gargoyle init /path/the/agent/may/touch # keys, config, rules.json
288
+ gargoyle init --observe /path/... # or: allow and record everything, hold nothing, tighten later
284
289
  gargoyle issuer # the issuing service, on rules.json
285
290
  gargoyle approve --watch 2 # where the approver sits (terminal)
286
291
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gargoyle-receipts"
7
- version = "0.16.3.dev0"
7
+ version = "0.16.4.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"
@@ -108,8 +108,9 @@ def test_init_for_a_windows_client_and_audit_log_verification(tmp_path, monkeypa
108
108
  gate = _gate(state, params); assert not gate["executed"]
109
109
  _record(state, REFUSED, gate, params)
110
110
  assert len(log.read_text().splitlines()) == 4
111
- assert main(["audit", "--log", str(log), "--out", str(out)]) == 0
111
+ assert main(["audit", "--log", str(log), "--out", str(out), "--summary"]) == 0
112
112
  out_text = capsys.readouterr().out
113
+ assert "summary:" in out_text and "principal" in out_text and "clients:" in out_text
113
114
  assert "4 entries (3 honored, 1 refused), 0 failed; chain EVIDENCE_VERIFIED" in out_text
114
115
  assert out_text.count(" ok ") == 4 and "refused delete_file" in out_text
115
116
  assert "ENTRY_VERIFIED (DESTRUCTIVE_TOOL)" in out_text
@@ -143,3 +144,30 @@ def test_the_package_runs_as_a_module():
143
144
  import subprocess, sys
144
145
  r = subprocess.run([sys.executable, "-m", "gargoyle", "--help"], capture_output=True, text=True, timeout=60)
145
146
  assert r.returncode == 0 and "audit" in r.stdout
147
+
148
+
149
+ def test_init_observe_allows_everything_and_records_it(tmp_path, capsys):
150
+ """Observe mode: the rules file allows every call, so the gate holds
151
+ nothing and refuses nothing while every call still gets a receipt and an
152
+ evidence entry. The default rules are not touched once someone edited them."""
153
+ from gargoyle.cli import DEFAULT_RULES, OBSERVE_RULES
154
+ from gargoyle.rules import RulesPolicy
155
+ out = tmp_path / "deploy"
156
+ assert main(["init", str(tmp_path / "project"), "--out", str(out), "--observe"]) == 0
157
+ assert "observe mode" in capsys.readouterr().out
158
+ assert json.loads((out / "rules.json").read_text()) == OBSERVE_RULES
159
+ policy = RulesPolicy(str(out / "rules.json"))
160
+ for tool in ("read_file", "write_file", "delete_file", "anything_else"):
161
+ d = policy.decide("mcp://x", {"tool": tool, "args": {}, "principal": "p"})
162
+ assert d.eligible and not d.requires_approval
163
+ assert policy.pin()["policy_sha256"] != RulesPolicy.__module__ and policy.pin()["policy_id"]
164
+ # A hand-edited rules file is left alone by a later --observe.
165
+ custom = {**DEFAULT_RULES, "rules": DEFAULT_RULES["rules"][:1]}
166
+ (out / "rules.json").write_text(json.dumps(custom))
167
+ assert main(["init", str(tmp_path / "project"), "--out", str(out), "--observe"]) == 0
168
+ assert json.loads((out / "rules.json").read_text()) == custom
169
+ assert "left as it is" in capsys.readouterr().err
170
+ # A broken rules file is refused, not overwritten and not a traceback.
171
+ (out / "rules.json").write_text("{not json")
172
+ assert main(["init", str(tmp_path / "project"), "--out", str(out), "--observe"]) == 2
173
+ assert (out / "rules.json").read_text() == "{not json"