gargoyle-receipts 0.17.0.dev0__tar.gz → 0.19.0.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gargoyle_receipts-0.17.0.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.19.0.dev0}/PKG-INFO +7 -5
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/README.md +6 -4
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/cli.py +151 -26
- gargoyle_receipts-0.19.0.dev0/gargoyle/emilia.py +438 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/evidence.py +51 -12
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/issuer_service.py +22 -1
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/mcp_server.py +131 -9
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/published_record_source.py +14 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/static/approve.html +13 -7
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/static/approve.js +46 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0/gargoyle_receipts.egg-info}/PKG-INFO +7 -5
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle_receipts.egg-info/SOURCES.txt +4 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/pyproject.toml +1 -1
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_cli.py +2 -2
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_console_js.py +32 -0
- gargoyle_receipts-0.19.0.dev0/tests/test_emilia_interop.py +338 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_issuer_service.py +2 -1
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_mcp_self_issue.py +3 -3
- gargoyle_receipts-0.19.0.dev0/tests/test_operability.py +126 -0
- gargoyle_receipts-0.19.0.dev0/tests/test_record_follow.py +92 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_rules.py +1 -1
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/LICENSE +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/NOTICE +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/__init__.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/__main__.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/adapters.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/approval.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/approver_cli.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/approver_trust.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/approvers.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/consumer.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/entra_login.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/envelope.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/execution.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/fs_server.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/globs.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/holdstore.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/identity.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/issuer.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/jws.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/key_record_source.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/notify.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/opa.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/pending_store.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/plans.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/policy.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/reconcile.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/remote.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/replay_cache.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/revert.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/rules.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/specs/graph-reference.json +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/state.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/verifier.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle/witness.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle_receipts.egg-info/dependency_links.txt +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle_receipts.egg-info/entry_points.txt +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle_receipts.egg-info/requires.txt +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/gargoyle_receipts.egg-info/top_level.txt +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/setup.cfg +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_annotations.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_approver_cli.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_approvers.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_canonical.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_consumer.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_entra_identity.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_entra_login.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_evidence.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_execution.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_fs_server.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_globs.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_graph.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_historical.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_identity.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_jws.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_mcp_proxy.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_mcp_server.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_notify.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_opa.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_plans.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_policy.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_published_record.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_remote.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_revert.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_review_fixes.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_state.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_state_text.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_stdio_encoding.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_threshold.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_typescript_conformance.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_wire_spec.py +0 -0
- {gargoyle_receipts-0.17.0.dev0 → gargoyle_receipts-0.19.0.dev0}/tests/test_witness.py +0 -0
{gargoyle_receipts-0.17.0.dev0/gargoyle_receipts.egg-info → gargoyle_receipts-0.19.0.dev0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gargoyle-receipts
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0.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.
|
|
62
|
+
Status: pre-release (`0.19.0.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
|
|
@@ -103,7 +103,7 @@ Python 3.12 or later. A TypeScript verifier and consumer for Node 20+
|
|
|
103
103
|
lives in [ts/](ts/) (`cd ts && npm install && npm test`); its suite
|
|
104
104
|
recomputes the same conformance vectors byte for byte, so the two
|
|
105
105
|
implementations cannot drift apart silently. Redis is optional (`.[redis]`) and only needed to
|
|
106
|
-
share the replay cache and pending-approval set across
|
|
106
|
+
share the replay cache and pending-approval set across replicas of the remote gate; a single gate keeps its replay claims in SQLite beside its evidence log.
|
|
107
107
|
|
|
108
108
|
## Issuing a receipt
|
|
109
109
|
|
|
@@ -288,6 +288,8 @@ hold and deny rules):
|
|
|
288
288
|
|
|
289
289
|
gargoyle init /path/the/agent/may/touch # keys, config, rules.json
|
|
290
290
|
gargoyle init --observe /path/... # or: allow and record everything, hold nothing, tighten later
|
|
291
|
+
gargoyle tokens --out gargoyle-deploy # later: new client and approver tokens, same keys
|
|
292
|
+
gargoyle emilia --log audit.jsonl # export approved calls as EMILIA Trust Receipts (docs/interop-emilia.md)
|
|
291
293
|
gargoyle issuer # the issuing service, on rules.json
|
|
292
294
|
gargoyle approve --watch 2 # where the approver sits (terminal)
|
|
293
295
|
|
|
@@ -397,7 +399,7 @@ cannot attach receipts work unchanged. Configuration is by environment:
|
|
|
397
399
|
| `GARGOYLE_MCP_GATE_KEY_ID`, `GARGOYLE_MCP_GATE_PUBLIC_KEY_HEX` | the pinned issuer key |
|
|
398
400
|
| `GARGOYLE_MCP_TARGET_ID` | the target identity receipts bind to |
|
|
399
401
|
| `GARGOYLE_MCP_PRINCIPAL` | the client this server serves |
|
|
400
|
-
| `GARGOYLE_MCP_RECORD_PATH` | the
|
|
402
|
+
| `GARGOYLE_MCP_RECORD_PATH`, `GARGOYLE_MCP_RECORD_URL` | the live policy pin: a file copied out of band, or (what `init` sets) the issuer's signed record at `<issuer>/record`, followed live and cached a minute, so a rule change reaches every gate without a copy or a restart |
|
|
401
403
|
| `GARGOYLE_MCP_APPROVER_KEY_ID`, `GARGOYLE_MCP_APPROVER_PUBLIC_KEY_HEX` | the approver keys whose grants release flagged calls (optional, both or neither; comma-separated lists for several) |
|
|
402
404
|
| `GARGOYLE_MCP_EXECUTOR_KEY_ID`, `GARGOYLE_MCP_EXECUTOR_KEY_HEX` | this server's own signing key, so honored calls carry an execution receipt in `_meta["gargoyle/execution"]` (optional, both or neither) |
|
|
403
405
|
| `GARGOYLE_MCP_ISSUER_URL`, `GARGOYLE_MCP_CLIENT_TOKEN` | self-issue mode: the issuing service and the identity token (whose subject is this server's principal) to obtain receipts with; `GARGOYLE_MCP_APPROVAL_TIMEOUT_SECONDS` and `GARGOYLE_MCP_POLL_SECONDS` tune the wait for a grant |
|
|
@@ -459,7 +461,7 @@ on it.
|
|
|
459
461
|
| `jws` | the receipt as a JWS any JWT library verifies |
|
|
460
462
|
| `envelope` | receipt construction, hash region, signature region, currency check |
|
|
461
463
|
| `verifier` | the pure target-side check and every refusal code |
|
|
462
|
-
| `consumer` | `Consumer` (pin, keys, verify, claim, attest)
|
|
464
|
+
| `consumer` | `Consumer` (pin, keys, verify, claim, attest) |
|
|
463
465
|
| `execution` | execution receipts and the offline chain check |
|
|
464
466
|
| `evidence` | the executor's hash-chained log of every decision, and its offline verifier |
|
|
465
467
|
| `mcp_server` | the stdio MCP middleware (and demo executor), with self-issue mode |
|
|
@@ -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.
|
|
24
|
+
Status: pre-release (`0.19.0.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
|
|
@@ -65,7 +65,7 @@ Python 3.12 or later. A TypeScript verifier and consumer for Node 20+
|
|
|
65
65
|
lives in [ts/](ts/) (`cd ts && npm install && npm test`); its suite
|
|
66
66
|
recomputes the same conformance vectors byte for byte, so the two
|
|
67
67
|
implementations cannot drift apart silently. Redis is optional (`.[redis]`) and only needed to
|
|
68
|
-
share the replay cache and pending-approval set across
|
|
68
|
+
share the replay cache and pending-approval set across replicas of the remote gate; a single gate keeps its replay claims in SQLite beside its evidence log.
|
|
69
69
|
|
|
70
70
|
## Issuing a receipt
|
|
71
71
|
|
|
@@ -250,6 +250,8 @@ hold and deny rules):
|
|
|
250
250
|
|
|
251
251
|
gargoyle init /path/the/agent/may/touch # keys, config, rules.json
|
|
252
252
|
gargoyle init --observe /path/... # or: allow and record everything, hold nothing, tighten later
|
|
253
|
+
gargoyle tokens --out gargoyle-deploy # later: new client and approver tokens, same keys
|
|
254
|
+
gargoyle emilia --log audit.jsonl # export approved calls as EMILIA Trust Receipts (docs/interop-emilia.md)
|
|
253
255
|
gargoyle issuer # the issuing service, on rules.json
|
|
254
256
|
gargoyle approve --watch 2 # where the approver sits (terminal)
|
|
255
257
|
|
|
@@ -359,7 +361,7 @@ cannot attach receipts work unchanged. Configuration is by environment:
|
|
|
359
361
|
| `GARGOYLE_MCP_GATE_KEY_ID`, `GARGOYLE_MCP_GATE_PUBLIC_KEY_HEX` | the pinned issuer key |
|
|
360
362
|
| `GARGOYLE_MCP_TARGET_ID` | the target identity receipts bind to |
|
|
361
363
|
| `GARGOYLE_MCP_PRINCIPAL` | the client this server serves |
|
|
362
|
-
| `GARGOYLE_MCP_RECORD_PATH` | the
|
|
364
|
+
| `GARGOYLE_MCP_RECORD_PATH`, `GARGOYLE_MCP_RECORD_URL` | the live policy pin: a file copied out of band, or (what `init` sets) the issuer's signed record at `<issuer>/record`, followed live and cached a minute, so a rule change reaches every gate without a copy or a restart |
|
|
363
365
|
| `GARGOYLE_MCP_APPROVER_KEY_ID`, `GARGOYLE_MCP_APPROVER_PUBLIC_KEY_HEX` | the approver keys whose grants release flagged calls (optional, both or neither; comma-separated lists for several) |
|
|
364
366
|
| `GARGOYLE_MCP_EXECUTOR_KEY_ID`, `GARGOYLE_MCP_EXECUTOR_KEY_HEX` | this server's own signing key, so honored calls carry an execution receipt in `_meta["gargoyle/execution"]` (optional, both or neither) |
|
|
365
367
|
| `GARGOYLE_MCP_ISSUER_URL`, `GARGOYLE_MCP_CLIENT_TOKEN` | self-issue mode: the issuing service and the identity token (whose subject is this server's principal) to obtain receipts with; `GARGOYLE_MCP_APPROVAL_TIMEOUT_SECONDS` and `GARGOYLE_MCP_POLL_SECONDS` tune the wait for a grant |
|
|
@@ -421,7 +423,7 @@ on it.
|
|
|
421
423
|
| `jws` | the receipt as a JWS any JWT library verifies |
|
|
422
424
|
| `envelope` | receipt construction, hash region, signature region, currency check |
|
|
423
425
|
| `verifier` | the pure target-side check and every refusal code |
|
|
424
|
-
| `consumer` | `Consumer` (pin, keys, verify, claim, attest)
|
|
426
|
+
| `consumer` | `Consumer` (pin, keys, verify, claim, attest) |
|
|
425
427
|
| `execution` | execution receipts and the offline chain check |
|
|
426
428
|
| `evidence` | the executor's hash-chained log of every decision, and its offline verifier |
|
|
427
429
|
| `mcp_server` | the stdio MCP middleware (and demo executor), with self-issue mode |
|
|
@@ -55,8 +55,8 @@ OBSERVE_RULES = {
|
|
|
55
55
|
# Observe mode: every call allowed (deletes too), every call recorded, none held. The
|
|
56
56
|
# first week of a deployment, so the hold and deny rules are written
|
|
57
57
|
# from the audit rather than guessed. The policy pin changes when the
|
|
58
|
-
# rules do
|
|
59
|
-
# the new
|
|
58
|
+
# rules do; the issuer applies the edit at once and clients that follow
|
|
59
|
+
# its signed record pick the new pin up within a minute.
|
|
60
60
|
"version": "1",
|
|
61
61
|
"default": "allow",
|
|
62
62
|
"rules": [],
|
|
@@ -93,6 +93,27 @@ def _write(path: Path, text: str, private: bool = False) -> None:
|
|
|
93
93
|
print("wrote", path)
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def _observe_rules(rules: Path) -> int:
|
|
97
|
+
"""Switch a deployment's rules file to observe mode (allow and record
|
|
98
|
+
everything), leaving a hand-edited file alone."""
|
|
99
|
+
current = None
|
|
100
|
+
if rules.exists():
|
|
101
|
+
try:
|
|
102
|
+
current = json.loads(rules.read_text())
|
|
103
|
+
except ValueError as exc:
|
|
104
|
+
print("%s is not valid JSON (%s); fix or remove it before --observe" % (rules, exc), file=sys.stderr)
|
|
105
|
+
return 2
|
|
106
|
+
if current is None or current == DEFAULT_RULES:
|
|
107
|
+
_write(rules, json.dumps(OBSERVE_RULES, indent=2) + "\n")
|
|
108
|
+
elif current != OBSERVE_RULES:
|
|
109
|
+
print("rules.json was edited by hand and is left as it is; --observe changed nothing", file=sys.stderr)
|
|
110
|
+
print("observe mode: rules.json allows every call, destructive ones included; the gate records all of "
|
|
111
|
+
"them and holds none. Tighten rules.json from `gargoyle audit --summary`; the issuer applies the "
|
|
112
|
+
"change at once and clients that follow its record (GARGOYLE_MCP_RECORD_URL) pick the new pin up "
|
|
113
|
+
"within a minute. A client on a static record.json needs the new file.")
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
|
|
96
117
|
def cmd_init(args) -> int:
|
|
97
118
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
98
119
|
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat
|
|
@@ -100,12 +121,26 @@ def cmd_init(args) -> int:
|
|
|
100
121
|
|
|
101
122
|
out = Path(args.out).resolve()
|
|
102
123
|
out.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
if (out / "issuer.env").exists() and not args.force:
|
|
125
|
+
# A deployment lives here. Its keys and tokens are what every client
|
|
126
|
+
# and approver holds; regenerating them by accident strands them all.
|
|
127
|
+
if args.observe:
|
|
128
|
+
return _observe_rules(out / "rules.json")
|
|
129
|
+
print("%s already holds a deployment; its keys and tokens are kept. New tokens: `gargoyle tokens --out %s`. "
|
|
130
|
+
"Fresh keys: `gargoyle init --force` (every client and approver must then be re-provisioned), or a new --out."
|
|
131
|
+
% (out, out), file=sys.stderr)
|
|
132
|
+
return 2
|
|
103
133
|
# A Windows path given on a Linux issuer host is the client's and is kept as typed.
|
|
104
134
|
windows = re.match(r"^[A-Za-z]:[\\/]", args.directory) is not None
|
|
105
135
|
allowed = args.directory if windows else str(Path(args.directory).resolve())
|
|
106
136
|
sep = "\\" if windows else "/"
|
|
107
137
|
client_dir = args.client_config_dir or str(out)
|
|
108
138
|
client_python = args.client_python or sys.executable
|
|
139
|
+
from gargoyle.globs import normalize_path
|
|
140
|
+
if normalize_path(client_dir) == normalize_path(allowed) or normalize_path(client_dir).startswith(normalize_path(allowed).rstrip("/") + "/"):
|
|
141
|
+
print("note: the client config folder %s is inside the folder the gate serves (%s). The gate refuses calls into its "
|
|
142
|
+
"own files (REF_TARGET_GATE_FILES), but secrets are better kept out of the served tree: --client-config-dir."
|
|
143
|
+
% (client_dir, allowed), file=sys.stderr)
|
|
109
144
|
|
|
110
145
|
def new_key():
|
|
111
146
|
k = Ed25519PrivateKey.generate()
|
|
@@ -130,20 +165,9 @@ def cmd_init(args) -> int:
|
|
|
130
165
|
|
|
131
166
|
rules = out / "rules.json"
|
|
132
167
|
if args.observe:
|
|
133
|
-
|
|
134
|
-
if
|
|
135
|
-
|
|
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.")
|
|
168
|
+
rc = _observe_rules(rules)
|
|
169
|
+
if rc:
|
|
170
|
+
return rc
|
|
147
171
|
elif not rules.exists():
|
|
148
172
|
_write(rules, json.dumps(DEFAULT_RULES, indent=2) + "\n")
|
|
149
173
|
_write(out / "issuer.env", _env_lines({
|
|
@@ -159,6 +183,7 @@ def cmd_init(args) -> int:
|
|
|
159
183
|
"GARGOYLE_MCP_GATE_KEY_ID": "issuer-1", "GARGOYLE_MCP_GATE_PUBLIC_KEY_HEX": issuer_pub,
|
|
160
184
|
"GARGOYLE_MCP_TARGET_ID": "mcp://filesystem/" + allowed, "GARGOYLE_MCP_PRINCIPAL": PRINCIPAL,
|
|
161
185
|
"GARGOYLE_MCP_RECORD_PATH": client_dir + sep + "record.json",
|
|
186
|
+
"GARGOYLE_MCP_RECORD_URL": issuer_url + "/record", # follow the issuer's signed pin; the file is the offline fallback
|
|
162
187
|
"GARGOYLE_MCP_APPROVER_KEY_ID": approver_ids, "GARGOYLE_MCP_APPROVER_PUBLIC_KEY_HEX": approver_pubs,
|
|
163
188
|
"GARGOYLE_MCP_EXECUTOR_KEY_ID": "executor-1", "GARGOYLE_MCP_EXECUTOR_KEY_HEX": executor_hex,
|
|
164
189
|
"GARGOYLE_MCP_ISSUER_URL": issuer_url, "GARGOYLE_MCP_CLIENT_TOKEN": client_token,
|
|
@@ -226,6 +251,58 @@ WantedBy=multi-user.target
|
|
|
226
251
|
return 0
|
|
227
252
|
|
|
228
253
|
|
|
254
|
+
def cmd_tokens(args) -> int:
|
|
255
|
+
"""New identity tokens from the deployment's existing identity key: the
|
|
256
|
+
client's shared token (unless the client signs in with Entra, whose
|
|
257
|
+
token renews itself) and every file-key approver's. Nothing else
|
|
258
|
+
changes, so clients keep their keys and the audit its trust."""
|
|
259
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
260
|
+
from gargoyle.jws import encode
|
|
261
|
+
out = Path(args.out).resolve()
|
|
262
|
+
issuer_env = out / "issuer.env"
|
|
263
|
+
if not issuer_env.exists():
|
|
264
|
+
print("%s holds no deployment (no issuer.env)" % out, file=sys.stderr)
|
|
265
|
+
return 2
|
|
266
|
+
env = _read_env(issuer_env)
|
|
267
|
+
idp_hex = env.get("GARGOYLE_IDP_KEY_HEX")
|
|
268
|
+
if not idp_hex:
|
|
269
|
+
print("issuer.env keeps no identity key (a deployment older than 0.9); make a fresh one with `gargoyle init` to a new --out", file=sys.stderr)
|
|
270
|
+
return 2
|
|
271
|
+
idp = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(idp_hex))
|
|
272
|
+
kid = env.get("GARGOYLE_IDP_KEY_ID", "idp-1")
|
|
273
|
+
exp = int((datetime.now(timezone.utc) + timedelta(days=args.token_days)).timestamp())
|
|
274
|
+
mw = out / "middleware.env"
|
|
275
|
+
if mw.exists():
|
|
276
|
+
menv = _read_env(mw)
|
|
277
|
+
if "GARGOYLE_MCP_CLIENT_TOKEN" in menv:
|
|
278
|
+
menv["GARGOYLE_MCP_CLIENT_TOKEN"] = encode({"sub": menv.get("GARGOYLE_MCP_PRINCIPAL", PRINCIPAL), "aud": "gargoyle-issuer", "exp": exp}, idp, kid)
|
|
279
|
+
_write(mw, _env_lines(menv), private=True)
|
|
280
|
+
mcp = out / "mcp-config.json"
|
|
281
|
+
if mcp.exists():
|
|
282
|
+
doc = json.loads(mcp.read_text())
|
|
283
|
+
for server in doc.get("mcpServers", {}).values():
|
|
284
|
+
if "GARGOYLE_MCP_CLIENT_TOKEN" in server.get("env", {}):
|
|
285
|
+
server["env"]["GARGOYLE_MCP_CLIENT_TOKEN"] = menv["GARGOYLE_MCP_CLIENT_TOKEN"]
|
|
286
|
+
_write(mcp, json.dumps(doc, indent=2) + "\n", private=True)
|
|
287
|
+
else:
|
|
288
|
+
print("middleware.env signs in with Entra; that token renews itself and was left alone")
|
|
289
|
+
for env_path in sorted(out.glob("approver*.env")):
|
|
290
|
+
aenv = _read_env(env_path)
|
|
291
|
+
akid = aenv.get("GARGOYLE_APPROVER_KEY_ID")
|
|
292
|
+
if not akid:
|
|
293
|
+
continue
|
|
294
|
+
tok = encode({"sub": akid, "roles": [DEFAULT_APPROVER_ROLE], "aud": "gargoyle-issuer", "exp": exp}, idp, kid)
|
|
295
|
+
aenv["GARGOYLE_APPROVER_TOKEN"] = tok
|
|
296
|
+
_write(env_path, _env_lines(aenv), private=True)
|
|
297
|
+
web = out / env_path.name.replace("approver", "approver-web", 1).replace(".env", ".json")
|
|
298
|
+
if web.exists():
|
|
299
|
+
doc = json.loads(web.read_text())
|
|
300
|
+
doc["token"] = tok
|
|
301
|
+
_write(web, json.dumps(doc, indent=2) + "\n", private=True)
|
|
302
|
+
print("new tokens, valid %d days; copy middleware.env (and approver-web*.json) to the machines that use them and restart their MCP clients" % args.token_days)
|
|
303
|
+
return 0
|
|
304
|
+
|
|
305
|
+
|
|
229
306
|
def cmd_login(args) -> int:
|
|
230
307
|
"""Device-code sign-in; then middleware.env (and mcp-config.json when
|
|
231
308
|
present) switch from the shared init token to this user's Entra token,
|
|
@@ -413,22 +490,58 @@ def _audit_summary(report, raw) -> None:
|
|
|
413
490
|
print(" clients: " + ", ".join("%s %d" % (w, n) for w, n in clients.most_common()))
|
|
414
491
|
|
|
415
492
|
|
|
493
|
+
def _trust_hex(out: Path) -> "tuple[Dict[str, Any], Dict[str, Any]]":
|
|
494
|
+
"""public-keys.json plus, when present, every key each enrolled approver
|
|
495
|
+
ever held (approvers.json, current first): grants signed under a key
|
|
496
|
+
since replaced in the browser or revoked still have to verify. Only the
|
|
497
|
+
live service and middleware are limited to the current key. Copy the
|
|
498
|
+
issuer's file beside the evidence first. Returns (keys, approvers_hex)."""
|
|
499
|
+
keys = json.loads((out / "public-keys.json").read_text())
|
|
500
|
+
approvers: Dict[str, Any] = dict(keys["approver"])
|
|
501
|
+
enrolled = out / "approvers.json" # approvers who enrolled through Entra, by UPN
|
|
502
|
+
if enrolled.exists():
|
|
503
|
+
from gargoyle.approvers import ApproverRegistry
|
|
504
|
+
approvers.update(ApproverRegistry(str(enrolled)).audit_keys_hex())
|
|
505
|
+
return keys, approvers
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def cmd_emilia(args) -> int:
|
|
509
|
+
"""Export an evidence log's honored, approved calls as EMILIA Trust
|
|
510
|
+
Receipts (docs/interop-emilia.md): one file in the format EMILIA's
|
|
511
|
+
conformance runners read, every receipt carrying the signoff the
|
|
512
|
+
approver's console made, the whole set under one Merkle checkpoint
|
|
513
|
+
signed by this gate's executor key."""
|
|
514
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
515
|
+
from gargoyle.emilia import trust_receipts_from_entries, verification_for
|
|
516
|
+
from gargoyle.evidence import read_log
|
|
517
|
+
out = Path(args.out).resolve()
|
|
518
|
+
keys, approvers = _trust_hex(out)
|
|
519
|
+
env = _read_env(Path(args.env or (out / "middleware.env")))
|
|
520
|
+
if "GARGOYLE_MCP_EXECUTOR_KEY_HEX" not in env:
|
|
521
|
+
print("%s has no GARGOYLE_MCP_EXECUTOR_KEY_HEX; point --env at this gate's middleware.env" % (args.env or out / "middleware.env"), file=sys.stderr)
|
|
522
|
+
return 2
|
|
523
|
+
executor = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(env["GARGOYLE_MCP_EXECUTOR_KEY_HEX"]))
|
|
524
|
+
log_key_id = env.get("GARGOYLE_MCP_EXECUTOR_KEY_ID", "executor-1")
|
|
525
|
+
result = trust_receipts_from_entries(read_log(args.log), log_signing_key=executor, log_key_id=log_key_id)
|
|
526
|
+
vectors = [{"id": r["receipt_id"], "expect": {"valid": True}, "trust_receipt": r,
|
|
527
|
+
"verification": verification_for(r, approvers, executor.public_key())} for r in result["receipts"]]
|
|
528
|
+
missing = [v["id"] for v in vectors if len(v["verification"]["approver_keys"]) < len(v["trust_receipt"]["signoffs"])]
|
|
529
|
+
doc = {"suite": "GARGOYLE EP-TRUST-RECEIPT-v1 export", "source": "gargoyle-receipts", "log": str(Path(args.log).resolve()),
|
|
530
|
+
"exported_at": datetime.now(timezone.utc).isoformat(), "count": len(vectors), "vectors": vectors}
|
|
531
|
+
_write(Path(args.write), json.dumps(doc, indent=1) + "\n")
|
|
532
|
+
print("%d trust receipts under one checkpoint (tree size %d) -> %s; %d approved calls skipped (no console signoff on every grant)%s"
|
|
533
|
+
% (len(vectors), len(vectors), args.write, result["skipped"],
|
|
534
|
+
("; %d receipts name an approver key not in %s/public-keys.json or approvers.json" % (len(missing), out)) if missing else ""))
|
|
535
|
+
return 0
|
|
536
|
+
|
|
537
|
+
|
|
416
538
|
def cmd_audit(args) -> int:
|
|
417
539
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
418
540
|
from gargoyle.execution import verify_chain
|
|
419
541
|
|
|
420
|
-
keys =
|
|
542
|
+
keys, approvers = _trust_hex(Path(args.out).resolve())
|
|
421
543
|
one = lambda h: Ed25519PublicKey.from_public_bytes(bytes.fromhex(h))
|
|
422
544
|
load = lambda m: {k: ([one(x) for x in v] if isinstance(v, list) else one(v)) for k, v in m.items()}
|
|
423
|
-
approvers: Dict[str, Any] = dict(keys["approver"])
|
|
424
|
-
enrolled = Path(args.out).resolve() / "approvers.json" # approvers who enrolled through Entra, by UPN
|
|
425
|
-
if enrolled.exists():
|
|
426
|
-
from gargoyle.approvers import ApproverRegistry
|
|
427
|
-
# Every key each approver ever held, current first: grants signed
|
|
428
|
-
# under a key since replaced in the browser or revoked still have to
|
|
429
|
-
# verify. Only the live service and middleware are limited to the
|
|
430
|
-
# current key. Copy the issuer's file beside the evidence first.
|
|
431
|
-
approvers.update(ApproverRegistry(str(enrolled)).audit_keys_hex())
|
|
432
545
|
trust = dict(issuer_public_keys=load(keys["issuer"]), executor_public_keys=load(keys["executor"]),
|
|
433
546
|
approver_public_keys=load(approvers))
|
|
434
547
|
if args.log:
|
|
@@ -523,7 +636,13 @@ def main(argv=None) -> int:
|
|
|
523
636
|
help="the filesystem MCP server behind the gate: the official one via npx, or gargoyle.fs_server")
|
|
524
637
|
p.add_argument("--entra-tenant", help="trust this Entra ID tenant's tokens at the issuer (users then run `gargoyle login`)")
|
|
525
638
|
p.add_argument("--entra-audience", help="the issuer API app's client id or app id URI, required in Entra tokens")
|
|
639
|
+
p.add_argument("--force", action="store_true",
|
|
640
|
+
help="regenerate keys and tokens in an existing --out (every client and approver must be re-provisioned)")
|
|
526
641
|
p.set_defaults(fn=cmd_init)
|
|
642
|
+
p = sub.add_parser("tokens", help="mint new client and approver tokens from a deployment's existing keys")
|
|
643
|
+
p.add_argument("--out", default="gargoyle-deploy")
|
|
644
|
+
p.add_argument("--token-days", type=int, default=30)
|
|
645
|
+
p.set_defaults(fn=cmd_tokens)
|
|
527
646
|
p = sub.add_parser("login", help="sign this workstation's user in with Entra ID (device code)")
|
|
528
647
|
p.add_argument("--tenant", required=True, help="tenant id or domain")
|
|
529
648
|
p.add_argument("--client-id", required=True, help="the public client app registration")
|
|
@@ -572,6 +691,12 @@ def main(argv=None) -> int:
|
|
|
572
691
|
p.add_argument("--summary", action="store_true", help="after the rows: calls by principal and tool with outcomes, releases, clients")
|
|
573
692
|
p.add_argument("--out", default="gargoyle-deploy")
|
|
574
693
|
p.set_defaults(fn=cmd_audit)
|
|
694
|
+
p = sub.add_parser("emilia", help="export an evidence log's approved calls as EMILIA Trust Receipts")
|
|
695
|
+
p.add_argument("--log", required=True, help="the middleware's audit.jsonl")
|
|
696
|
+
p.add_argument("--write", default="emilia-trust-receipts.json", help="output file, in the format EMILIA's conformance runners read")
|
|
697
|
+
p.add_argument("--env", help="this gate's middleware.env (executor key signs the checkpoint); default <out>/middleware.env")
|
|
698
|
+
p.add_argument("--out", default="gargoyle-deploy")
|
|
699
|
+
p.set_defaults(fn=cmd_emilia)
|
|
575
700
|
args = parser.parse_args(argv)
|
|
576
701
|
return args.fn(args)
|
|
577
702
|
|