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.
- {gargoyle_receipts-0.16.3.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.16.4.dev0}/PKG-INFO +8 -3
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/README.md +7 -2
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/cli.py +55 -1
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/mcp_server.py +8 -6
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0/gargoyle_receipts.egg-info}/PKG-INFO +8 -3
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/pyproject.toml +1 -1
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_cli.py +29 -1
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/LICENSE +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/NOTICE +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/__init__.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/__main__.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/adapters.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/approval.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/approver_cli.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/approver_trust.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/approvers.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/consumer.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/entra_login.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/envelope.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/evidence.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/execution.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/fs_server.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/holdstore.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/identity.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/issuer.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/issuer_service.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/jws.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/key_record_source.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/notify.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/opa.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/pending_store.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/plans.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/policy.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/published_record_source.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/reconcile.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/remote.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/replay_cache.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/revert.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/rules.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/specs/graph-reference.json +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/state.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/static/approve.html +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/static/approve.js +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/verifier.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/witness.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle_receipts.egg-info/SOURCES.txt +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle_receipts.egg-info/dependency_links.txt +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle_receipts.egg-info/entry_points.txt +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle_receipts.egg-info/requires.txt +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle_receipts.egg-info/top_level.txt +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/setup.cfg +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_annotations.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_approver_cli.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_approvers.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_canonical.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_console_js.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_consumer.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_entra_identity.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_entra_login.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_evidence.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_execution.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_fs_server.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_graph.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_historical.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_identity.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_issuer_service.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_jws.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_mcp_proxy.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_mcp_self_issue.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_mcp_server.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_notify.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_opa.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_plans.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_policy.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_published_record.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_remote.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_revert.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_rules.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_state.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_stdio_encoding.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_threshold.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_typescript_conformance.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_wire_spec.py +0 -0
- {gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_witness.py +0 -0
{gargoyle_receipts-0.16.3.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.16.4.dev0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gargoyle-receipts
|
|
3
|
-
Version: 0.16.
|
|
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
|
[](https://pypi.org/project/gargoyle-receipts/)
|
|
61
61
|
|
|
62
|
-
Status: pre-release (`0.16.
|
|
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
|
[](https://pypi.org/project/gargoyle-receipts/)
|
|
23
23
|
|
|
24
|
-
Status: pre-release (`0.16.
|
|
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
|
|
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
|
|
298
|
-
"capability or workaround for that target. Reads run at once. A write
|
|
299
|
-
"approval: wait for the result, it is normal. A refused call is a decision,
|
|
300
|
-
"the reason to the user and stop; do not retry or route around it.
|
|
301
|
-
"
|
|
302
|
-
"
|
|
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
|
{gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.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.16.
|
|
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
|
[](https://pypi.org/project/gargoyle-receipts/)
|
|
61
61
|
|
|
62
|
-
Status: pre-release (`0.16.
|
|
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.
|
|
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"
|
|
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
|
|
File without changes
|
{gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.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
|
{gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.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.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/gargoyle/specs/graph-reference.json
RENAMED
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.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.16.3.dev0 → gargoyle_receipts-0.16.4.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.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_issuer_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.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.16.3.dev0 → gargoyle_receipts-0.16.4.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.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_stdio_encoding.py
RENAMED
|
File without changes
|
|
File without changes
|
{gargoyle_receipts-0.16.3.dev0 → gargoyle_receipts-0.16.4.dev0}/tests/test_typescript_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|