gama-mandate 0.10.3__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 (43) hide show
  1. gama_mandate-0.10.3/AUTHORS +1 -0
  2. gama_mandate-0.10.3/CHANGELOG.md +124 -0
  3. gama_mandate-0.10.3/LICENSE +21 -0
  4. gama_mandate-0.10.3/MANIFEST.in +11 -0
  5. gama_mandate-0.10.3/PKG-INFO +238 -0
  6. gama_mandate-0.10.3/PLAN.md +41 -0
  7. gama_mandate-0.10.3/README.md +217 -0
  8. gama_mandate-0.10.3/docs/NON_GOALS.md +11 -0
  9. gama_mandate-0.10.3/docs/THREAT_MODEL.md +42 -0
  10. gama_mandate-0.10.3/examples/intent.json +13 -0
  11. gama_mandate-0.10.3/gama_mandate.egg-info/PKG-INFO +238 -0
  12. gama_mandate-0.10.3/gama_mandate.egg-info/SOURCES.txt +41 -0
  13. gama_mandate-0.10.3/gama_mandate.egg-info/dependency_links.txt +1 -0
  14. gama_mandate-0.10.3/gama_mandate.egg-info/entry_points.txt +4 -0
  15. gama_mandate-0.10.3/gama_mandate.egg-info/top_level.txt +1 -0
  16. gama_mandate-0.10.3/mandate/__init__.py +34 -0
  17. gama_mandate-0.10.3/mandate/__main__.py +4 -0
  18. gama_mandate-0.10.3/mandate/adapters/__init__.py +5 -0
  19. gama_mandate-0.10.3/mandate/adapters/erp.py +152 -0
  20. gama_mandate-0.10.3/mandate/adapters/smtp.py +56 -0
  21. gama_mandate-0.10.3/mandate/adapters/write.py +145 -0
  22. gama_mandate-0.10.3/mandate/broker.py +363 -0
  23. gama_mandate-0.10.3/mandate/cli.py +500 -0
  24. gama_mandate-0.10.3/mandate/core.py +470 -0
  25. gama_mandate-0.10.3/mandate/data/policy.json +14 -0
  26. gama_mandate-0.10.3/mandate/jcs.py +107 -0
  27. gama_mandate-0.10.3/mandate/journal.py +143 -0
  28. gama_mandate-0.10.3/mandate/log.py +238 -0
  29. gama_mandate-0.10.3/mandate/mcp_server.py +101 -0
  30. gama_mandate-0.10.3/mandate/py.typed +0 -0
  31. gama_mandate-0.10.3/mandate/sign.py +197 -0
  32. gama_mandate-0.10.3/mandate/witness.py +190 -0
  33. gama_mandate-0.10.3/policy.json +14 -0
  34. gama_mandate-0.10.3/pyproject.toml +46 -0
  35. gama_mandate-0.10.3/setup.cfg +4 -0
  36. gama_mandate-0.10.3/skills/mandate/SKILL.md +75 -0
  37. gama_mandate-0.10.3/spec/MANDATE-1.md +85 -0
  38. gama_mandate-0.10.3/tests/test_jcs.py +65 -0
  39. gama_mandate-0.10.3/tests/test_log.py +183 -0
  40. gama_mandate-0.10.3/tests/test_mandate.py +624 -0
  41. gama_mandate-0.10.3/tests/test_witness.py +161 -0
  42. gama_mandate-0.10.3/trust/allowed_signers +8 -0
  43. gama_mandate-0.10.3/trust/revoked_signers +2 -0
@@ -0,0 +1 @@
1
+ GAMA
@@ -0,0 +1,124 @@
1
+ # Changelog
2
+
3
+ ## 0.10.3 — 2026-11-09
4
+ - PyPI distribution name is **`gama-mandate`**. `agent-mandate` was rejected by
5
+ PyPI as too similar to an existing project (`agentmandate`). Import package and
6
+ console scripts are unchanged (`mandate`, `mandate-broker`, `mandate-mcp`).
7
+ - Code identical to 0.10.2.
8
+
9
+ ## 0.10.2 — 2026-11-09
10
+ - Published to GitHub: https://github.com/gamai-io/mandate (public, CI green on
11
+ Python 3.9–3.12). Release v0.10.1 with wheel, sdist, and checksums.
12
+ - Correct project URLs, README CI/license badges, install-from-git instructions.
13
+
14
+ ## 0.10.1 — 2026-11-09
15
+ - Security: the broker requires a bearer token for non-loopback binds
16
+ (`--token` / `MANDATE_BROKER_TOKEN`); broker and witness reject request bodies
17
+ over 256 KiB (413).
18
+ - Lint: `ruff` config, clean `ruff check .`, CI lint + build/`twine check`.
19
+ - Packaging: `py.typed`, sdist/README install instructions, governance files
20
+ (`SECURITY.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `AUTHORS`,
21
+ `RELEASE.md`).
22
+ - PyPI name `agent-mandate` verified available (2026-11-09).
23
+ - 71 tests.
24
+
25
+ ## 0.10.0 — 2026-11-09
26
+ - **Off-host STH witness**: pins the latest signed tree head, rejects rollback
27
+ (smaller tree), rejects split-view (same size, different root), and requires a
28
+ valid consistency proof for growth. State is written atomically (0600).
29
+ - CLI: `mandate witness accept | verify | show | serve | push`.
30
+ - Loopback HTTP witness with a bearer token (`/sth`, `/state`, `/health`);
31
+ `--allow-remote` required to bind elsewhere.
32
+ - 8 witness tests + 68 total.
33
+
34
+ ## 0.9.0 — 2026-11-09
35
+ - **Merkle transparency log** over ledger entries (RFC 6962/9162 style):
36
+ `Leaf=SHA256(0x00||JCS(entry))`, `Node=SHA256(0x01||L||R)`, MTH, inclusion
37
+ proofs, consistency proofs.
38
+ - **Signed tree head** via `ssh-keygen -Y sign` in namespace `mandate-log`;
39
+ `log_id` is the log key SHA256 fingerprint.
40
+ - CLI: `mandate log head | sth-verify | proof | verify-inclusion | consistency |
41
+ verify-consistency`.
42
+ - Refuses to build a log over an invalid ledger hash chain.
43
+ - 9 Merkle tests + 60 total. Off-host **witness/anchor** is still required to
44
+ rule out split-view; tracked in PLAN.md.
45
+
46
+ ## 0.8.0 — 2026-11-09
47
+ - **Effect journal** (SQLite) with a lease-based state machine:
48
+ `intent → executing → executed → receipted`, plus `failed` / `abandoned`.
49
+ - **Idempotent write adapter**: `mode=file` or `mode=sqlite` (a trusted SQL
50
+ statement runs in the same transaction as the idempotency insert). The
51
+ idempotency key is the **grant nonce**.
52
+ - The authorisation receipt is now appended **after** the effect commits,
53
+ fixing the old "ledger says executed but no effect happened" crash window.
54
+ - **Crash recovery on start**: `executing` with a committed effect → finish the
55
+ receipt; without an effect → abandoned; `executed` without a receipt →
56
+ receipted. Duplicate `execute` returns the same receipt/result instead of an
57
+ error.
58
+ - Config: `write`, `journal_db`, `lease_s`; `/health` reports journal counts.
59
+ - 43 tests (crash-after-effect recovery, crash-before-effect abandon, writer
60
+ failure, concurrent single effect, journal payload mismatch).
61
+
62
+ ## 0.7.0 — 2026-11-09
63
+ - Canonicalization is now **RFC 8785 (JCS)**: UTF-16 code-unit key ordering,
64
+ ECMAScript number serialization, minimal string escaping, and strict rejection
65
+ of non-finite numbers, lone surrogates, non-string keys, and integers outside
66
+ the IEEE-754 safe range. Passes the RFC 8785 reference vector.
67
+ - **Breaking:** commitment/receipt hashes change (e.g. `1170.0` serializes as
68
+ `1170`). Regenerate grants and ledgers from earlier versions.
69
+ - CI: GitHub Actions test matrix on Python 3.9–3.12.
70
+
71
+ ## 0.6.0 — 2026-11-09
72
+ - `allowed_signers` now requires an explicit `namespaces="mandate"` scope
73
+ (`namespaces="mandate-broker"` for broker result keys). A key without a scope
74
+ is rejected, so a grant key cannot sign broker receipts and vice versa.
75
+ - Validity windows (`valid-after` / `valid-before`) are enforced in Mandate's
76
+ own check and by OpenSSH.
77
+ - Key revocation: `mandate revoke --pub key.pub` writes the SHA256 fingerprint to
78
+ `trust/revoked_signers`; `redeem`, `verify`, and `verify-result` reject revoked
79
+ keys. Configurable via `--revoked` / `MANDATE_REVOKED`.
80
+ - `mandate signers` lists parsed principals, namespaces, validity, and fingerprints.
81
+ - 37 tests (namespace scope, validity window, revocation, revoke CLI).
82
+
83
+ ## 0.5.0 — 2026-11-09
84
+ - Grant `audience`: a grant can be bound to one broker (`mandate grant --audience <broker>`).
85
+ The broker verifies `audience == broker_name` and rejects unbound grants by default
86
+ (`--allow-any-audience` to opt out). Closes cross-broker replay.
87
+ - Broker-side re-derivation: `erp.derive` re-reads counterparty/amount/currency from the
88
+ source of truth and rejects the effect if the agent's intent diverges
89
+ (`derivation_mismatch`), not just at the precondition amount check.
90
+ - 33 tests (audience mismatch, unbound grant, currency derivation mismatch).
91
+
92
+ ## 0.4.0 — 2026-11-09
93
+ Security fixes from an independent multi-agent audit (Claude + subagent + web):
94
+ - `mandate grant` recomputes commitment/render_hash/proposal_id from the intent and rejects a mismatched proposal (approval-confusion fix).
95
+ - `redeem` enforces policy (unknown effects denied) and binds the grant to the agent (`agent` field), closing the actor-swap self-approval bypass.
96
+ - CLI/MCP redeem now performs the nonce check and append under one file lock; ledger append is fsync'd and anchored with `<ledger>.head`; `verify_chain` rejects empty or truncated ledgers.
97
+ - Intent schema validation: unknown effect/amount fields and non-finite amounts are rejected; `params` must be flat scalars.
98
+ - `render` quotes every value (no `|` injection) and includes all params.
99
+ - ERP: NaN/non-dict amounts rejected; only `bind_params` can fill query placeholders.
100
+ - `execute` refuses non-loopback brokers (unless `--allow-remote`) and does not follow redirects.
101
+ - MCP adapter moved into the package and shipped as `mandate-mcp`; `verify-result` validates the signed broker result entry.
102
+ - `doctor` fails when signers are missing; `max_uses` is honestly fixed at 1.
103
+
104
+ ## 0.3.0 — 2026-11-09
105
+ - Read-only ERP precondition (SQLite `mode=ro`, named-param query, amount tolerance).
106
+ - SMTP adapter for `message.send` (`dry_run` default, `live` reads password from env).
107
+ - Broker `--config`, `/health` reports `smtp` and `erp`.
108
+ - Preconditions run before the nonce claim, so a failed check does not consume the grant.
109
+ - Packaging: `pyproject.toml`, console scripts `mandate` / `mandate-broker`,
110
+ `mandate init`, `mandate doctor`, `mandate version`.
111
+
112
+ ## 0.2.0 — 2026-11-09
113
+ - Effect broker: `/execute` + `/health`, loopback by default.
114
+ - Atomic nonce claim (SQLite `UNIQUE`) prevents double execution.
115
+ - `render_hash` bound into commitment and grant (approver signs the rendered summary).
116
+ - Signed result entry chained after the authorisation receipt.
117
+
118
+ ## 0.1.0 — 2026-11-09
119
+ - Canonical commitment (JCS), stable ids.
120
+ - Intent / Grant / Receipt objects.
121
+ - Off-host Ed25519 grant via `ssh-keygen -Y sign`.
122
+ - One-shot redeem, replay detection, hash-chained ledger, offline verification.
123
+ - Agent cannot self-approve; unknown effects denied.
124
+ - MCP adapter and agent skill.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GAMA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ include README.md
2
+ include PLAN.md
3
+ include LICENSE
4
+ include CHANGELOG.md
5
+ include policy.json
6
+ recursive-include spec *.md
7
+ recursive-include docs *.md
8
+ recursive-include skills *.md
9
+ recursive-include examples *.json
10
+ recursive-include trust *
11
+ recursive-include mandate/data *.json
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: gama-mandate
3
+ Version: 0.10.3
4
+ Summary: Signed, verifiable business commitments for AI agents: canonical commitment, off-host Ed25519 grant, one-shot redemption, hash-chained receipts.
5
+ Author: GAMA
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/gamai-io/mandate
8
+ Project-URL: Repository, https://github.com/gamai-io/mandate
9
+ Project-URL: Issues, https://github.com/gamai-io/mandate/issues
10
+ Project-URL: Changelog, https://github.com/gamai-io/mandate/blob/main/CHANGELOG.md
11
+ Keywords: ai-agents,trust,approval,commitment,ed25519,audit
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Security
15
+ Classifier: Topic :: Office/Business
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ License-File: AUTHORS
20
+ Dynamic: license-file
21
+
22
+ # Mandate
23
+
24
+ [![tests](https://github.com/gamai-io/mandate/actions/workflows/test.yml/badge.svg)](https://github.com/gamai-io/mandate/actions/workflows/test.yml)
25
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
26
+
27
+ **Signed, verifiable business commitments for AI agents.**
28
+
29
+ Most agent safety tools answer *"is this tool allowed?"*. Mandate answers the
30
+ question that actually matters: **"who ordered this exact payment, to this
31
+ supplier, for this amount?"** A tool permission is not a business mandate.
32
+
33
+ Every consequential effect becomes a canonical **Commitment** object. An agent
34
+ signs the intent; a human co-signs a **Grant** with an Ed25519 key; execution
35
+ produces a hash-chained **Receipt**. Anyone can verify the whole chain offline.
36
+
37
+ > Approval workflows are ephemeral events. A Mandate is a durable, portable,
38
+ > cryptographically bound artifact — a commit, not a log line.
39
+
40
+ ## Why it is not another approval gate
41
+
42
+ | Approval gate | Mandate |
43
+ |---|---|
44
+ | Asks "may this tool run?" | Asks "is this exact effect authorised?" |
45
+ | Identity is a self-asserted field | Approver proves possession of an off-host key |
46
+ | Approval is a mutable DB row | Grant + receipt are signed, content-addressed |
47
+ | Logs what happened | Lets anyone prove what was authorised |
48
+
49
+ ## Install
50
+
51
+ CLI + broker + MCP adapter. There is **no web UI** by design: Mandate is a
52
+ local trust primitive you embed in your own stack.
53
+
54
+ ```bash
55
+ python3 -m pip install gama-mandate
56
+ # or from source / git
57
+ python3 -m pip install "git+https://github.com/gamai-io/mandate.git"
58
+
59
+ mandate doctor # checks python, ssh-keygen, policy
60
+ mandate init --dir ./my-deployment # policy, allowed_signers, config, example
61
+ ```
62
+
63
+ ## Quickstart
64
+
65
+ ```bash
66
+ export PYTHONPATH=$PWD
67
+
68
+ # 1. on the approver's device (never on the agent host)
69
+ ssh-keygen -t ed25519 -f ~/.ssh/mandate -C mandate
70
+ # append the PUBLIC key to trust/allowed_signers as: renato ssh-ed25519 AAAA... mandate
71
+
72
+ # 2. agent proposes
73
+ ./bin/mandate propose --intent examples/intent.json --out /tmp/proposal.json
74
+
75
+ # 3. human grants (signs the commitment, bound to one broker + the agent)
76
+ ./bin/mandate grant --proposal /tmp/proposal.json --approver renato \
77
+ --key ~/.ssh/mandate --audience local-broker --out /tmp/grant.json
78
+
79
+ # 4. agent redeems exactly once
80
+ ./bin/mandate redeem --intent examples/intent.json --grant /tmp/grant.json \
81
+ --ledger ledger.jsonl
82
+
83
+ # 5. or let the effect broker execute it (credentials stay in the broker)
84
+ ./bin/mandate-broker --port 8899 --signers trust/allowed_signers \
85
+ --ledger ledger.jsonl --nonce-db broker/used.sqlite &
86
+ ./bin/mandate execute --broker http://127.0.0.1:8899 \
87
+ --intent examples/intent.json --grant /tmp/grant.json
88
+
89
+ # 5. anyone verifies offline
90
+ ./bin/mandate verify --receipt ... --intent examples/intent.json --grant /tmp/grant.json
91
+ ./bin/mandate verify-result --entry /tmp/result.json --broker-signers trust/broker_signers
92
+ ./bin/mandate chain --ledger ledger.jsonl
93
+ ```
94
+
95
+ Exit codes for `check`: `0` allow · `10` ask (grant required) · `20` deny.
96
+
97
+ ## Effect broker (Phase 2)
98
+
99
+ `bin/mandate-broker` is the only component that executes effects. It verifies
100
+ the grant, **atomically claims the nonce** (SQLite `UNIQUE`, so two concurrent
101
+ requests cannot both succeed), runs the effect handler, and appends a signed
102
+ result entry to the hash-chained ledger. The agent calls it over loopback; the
103
+ agent never holds the effect credentials.
104
+
105
+ The shipped handlers are a sandbox (they record the effect under `effects/` or
106
+ `outbox/`). Wiring real credentials (SQL write, SMTP, payment) is Phase 2
107
+ completion — see `PLAN.md`.
108
+
109
+ ### Adapters
110
+
111
+ **ERP precondition (read-only).** Before a grant is consumed, the broker runs a
112
+ named-parameter SQLite query against master data. No row → `counterparty_not_found`;
113
+ amount outside `tolerance_pct` → `amount_mismatch`. The grant is **not**
114
+ consumed when a precondition fails, so a corrected request can retry safely.
115
+
116
+ **SMTP (`message.send`).** `mode=dry_run` writes an `.eml` to the outbox;
117
+ `mode=live` sends via SMTP. The password is read from the env var named in
118
+ `password_env` and never logged or returned.
119
+
120
+ ```json
121
+ "erp": {"db": "erp.sqlite", "checks": {"payment.send": {
122
+ "query": "SELECT invoice_total FROM vendors WHERE name = :target",
123
+ "amount_field": "invoice_total", "tolerance_pct": 0.5}}},
124
+ "smtp": {"mode": "dry_run", "host": "", "user": "",
125
+ "password_env": "MANDATE_SMTP_PASSWORD", "from": "noreply@gama.local"}
126
+ ```
127
+
128
+ ## Key scoping and revocation
129
+
130
+ An `allowed_signers` line **must** declare its namespace scope:
131
+
132
+ ```
133
+ renato namespaces="mandate" ssh-ed25519 AAAAC3Nza... mandate
134
+ gama-broker namespaces="mandate-broker" ssh-ed25519 AAAAC3Nza... broker
135
+ ```
136
+
137
+ Optional validity: `valid-after="YYYYMMDD" valid-before="YYYYMMDD"`.
138
+ Revoke a key by fingerprint:
139
+
140
+ ```bash
141
+ mandate revoke --pub key.pub --reason "laptop lost"
142
+ mandate --signers trust/allowed_signers signers # inspect scopes + fingerprints
143
+ ```
144
+
145
+ ### Idempotent writes and recovery
146
+
147
+ The broker keeps a SQLite **effect journal** (`journal_db`). The flow is
148
+ `intent → executing → executed → receipted`; the authorisation receipt is
149
+ written only **after** the effect commits. On startup the broker recovers:
150
+ a committed effect without a receipt gets one; a claimed grant with no effect is
151
+ abandoned; a duplicate `execute` returns the original receipt and result.
152
+
153
+ `write.mode=sqlite` runs a trusted configured statement together with an
154
+ idempotency insert in **one transaction**, keyed by the grant nonce:
155
+
156
+ ```json
157
+ "write": {"mode": "sqlite", "db": "erp.sqlite",
158
+ "statement": "INSERT INTO payments(nonce,vendor,amount) VALUES(:nonce,:target,:amount)"}
159
+ ```
160
+
161
+ ## Transparency log
162
+
163
+ Every ledger entry is a leaf in an RFC 6962/9162-style Merkle tree
164
+ (`Leaf=SHA256(0x00||JCS(entry))`, `Node=SHA256(0x01||L||R)`). A signed tree
165
+ head (STH) fixes the root and size; inclusion and consistency proofs verify
166
+ offline:
167
+
168
+ ```bash
169
+ mandate log head --ledger ledger.jsonl --log-name log1 --log-key ~/.ssh/mandate-log --out sth.json
170
+ mandate --signers trust/allowed_signers log sth-verify --sth sth.json
171
+ mandate log proof --ledger ledger.jsonl --index 3 --out proof.json
172
+ mandate log verify-inclusion --entry entry.json --proof proof.json --sth sth.json
173
+ mandate log consistency --ledger ledger.jsonl --first 2 --second 3 --out cons.json
174
+ mandate log verify-consistency --old old_sth.json --new sth.json --proof cons.json
175
+ ```
176
+
177
+ A single log cannot prevent **split-view** by its operator; pin the STH on a
178
+ second machine (witness/anchor). The STH signer needs
179
+ `namespaces="mandate-log"` in `allowed_signers`.
180
+
181
+ ### Off-host witness
182
+
183
+ ```bash
184
+ # on the second machine
185
+ mandate --signers log_signers witness serve --state witness.json \
186
+ --token "$WITNESS_TOKEN" --port 8898
187
+
188
+ # on the log host
189
+ mandate witness push --url http://witness:8898 --sth sth.json --token "$WITNESS_TOKEN"
190
+ mandate witness push --url http://witness:8898 --sth sth_new.json --proof cons.json --token "$WITNESS_TOKEN"
191
+ mandate witness show --state witness.json
192
+ ```
193
+
194
+ The witness pins the latest STH and rejects rollback or a same-size different
195
+ root (split-view). Growth requires a consistency proof from the pinned root.
196
+ `witness verify` checks without pinning; `witness accept` pins locally.
197
+
198
+ ## Design invariants
199
+
200
+ 1. **Effect, not tool.** The registry keys on `payment.send`, not on
201
+ `gama_sql`. The same effect is the same commitment regardless of executor.
202
+ 2. **Canonical bytes.** **RFC 8785 (JCS)** canonical JSON; the commitment is the
203
+ hash of `{v, effect, requested_by, render_hash}`. Same content ⇒ same id, in
204
+ any RFC 8785 implementation.
205
+ 3. **Off-host key.** The approver's private key never lives where the agent
206
+ runs. The host stores only public keys (`trust/allowed_signers`).
207
+ 4. **One-shot, nonce-bound.** A grant is redeemable once; replays fail.
208
+ 5. **Agent cannot self-approve.** Enforced in code, not by convention.
209
+ 6. **Fail closed.** Unknown effects are denied. Missing signer ⇒ invalid.
210
+
211
+ ## Layout
212
+
213
+ ```
214
+ spec/ MANDATE-1 data model
215
+ mandate/ stdlib-only reference implementation
216
+ bin/ CLI
217
+ adapters/ MCP server
218
+ tests/ unit + bypass tests (red-team)
219
+ docs/ THREAT_MODEL, NON_GOALS, PLAN
220
+ skills/ agent skill instructions
221
+ ```
222
+
223
+ ## Status
224
+
225
+ **Alpha** — reference implementation v0.10.1: canonical commitment, Ed25519 grant
226
+ bound to the executing agent, one-shot redeem under a file lock, hash-chained
227
+ receipts with a `.head` anchor, idempotent effect journal + write adapter,
228
+ effect broker with signed result entries, read-only ERP preconditions, SMTP
229
+ adapter, Merkle transparency log with an off-host witness, MCP server
230
+ (`mandate-mcp`), and a red-team test suite. See `PLAN.md` for the roadmap and
231
+ `docs/THREAT_MODEL.md` for the known limits.
232
+
233
+ ## Honest limitation
234
+
235
+ Cryptography proves *who authorised what*. It does **not** isolate
236
+ credentials. Until the credentials that cause the effect (SQL write, payment
237
+ API, mail) live only in an effect broker, a compromised agent can bypass
238
+ Mandate entirely. That broker is the next milestone — see `PLAN.md`.
@@ -0,0 +1,41 @@
1
+ # Plan
2
+
3
+ ## Phase 1 — Core (done, v0.1)
4
+ - [x] Canonical commitment (JCS), stable ids
5
+ - [x] Intent / Grant / Receipt objects
6
+ - [x] Ed25519 grant signing via OpenSSH (`ssh-keygen -Y sign`)
7
+ - [x] One-shot redeem with nonce, replay detection
8
+ - [x] Hash-chained ledger + offline verification
9
+ - [x] Agent cannot self-approve
10
+ - [x] Fail-closed policy (unknown effect = deny)
11
+ - [x] Bypass tests: tamper, replay, wrong signer, expiry, self-approval
12
+
13
+ ## Phase 2 — Effect broker (core done, v0.2)
14
+ - [x] Broker is the only executor; agent never runs the effect itself
15
+ - [x] HTTP `/execute` + `/health`, loopback-only by default
16
+ - [x] Atomic nonce claim (SQLite UNIQUE) → no double execution under concurrency
17
+ - [x] `render_hash`: the human signs the rendered summary, bound into the commitment
18
+ - [x] Signed result entry chained after the authorisation receipt
19
+ - [x] Read-only ERP precondition (SQLite, fail-closed target + amount check)
20
+ - [x] SMTP adapter (`dry_run` default; `live` reads password from env)
21
+ - [ ] Move write credentials (SQL write, payment, WA) into the broker
22
+ - [ ] Per-effect adapters + integration tests against a fake ERP
23
+
24
+ ## Phase 3 — Transparency + interop
25
+ - [x] Merkle transparency log (RFC 6962/9162 style) with inclusion + consistency proofs
26
+ - [x] Signed tree head via Ed25519 (`mandate-log` namespace)
27
+ - [x] Off-host witness/anchor for the STH (rules out split-view)
28
+ - [ ] Export receipt as JWS / C2PA-style manifest
29
+ - [ ] OIDC/WebAuthn co-sign (passkeys) alongside Ed25519
30
+ - [ ] Provenance enforcement: `data_ref` required for every choice parameter
31
+
32
+ ## Phase 4 — Ecosystem
33
+ - [ ] MCP server packaged for Claude Code / opencode / Cursor
34
+ - [ ] Shell shim + GitHub Action for CI bypass tests
35
+ - [ ] Policy packs per domain (trade, finance, ops)
36
+ - [ ] `mandate audit` diff of policy and trust-root changes
37
+
38
+ ## Definition of done for v1.0
39
+ An agent with no credentials to the target system cannot cause a consequential
40
+ effect without a valid, single-use, human-signed grant, and an auditor can prove
41
+ that offline.
@@ -0,0 +1,217 @@
1
+ # Mandate
2
+
3
+ [![tests](https://github.com/gamai-io/mandate/actions/workflows/test.yml/badge.svg)](https://github.com/gamai-io/mandate/actions/workflows/test.yml)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ **Signed, verifiable business commitments for AI agents.**
7
+
8
+ Most agent safety tools answer *"is this tool allowed?"*. Mandate answers the
9
+ question that actually matters: **"who ordered this exact payment, to this
10
+ supplier, for this amount?"** A tool permission is not a business mandate.
11
+
12
+ Every consequential effect becomes a canonical **Commitment** object. An agent
13
+ signs the intent; a human co-signs a **Grant** with an Ed25519 key; execution
14
+ produces a hash-chained **Receipt**. Anyone can verify the whole chain offline.
15
+
16
+ > Approval workflows are ephemeral events. A Mandate is a durable, portable,
17
+ > cryptographically bound artifact — a commit, not a log line.
18
+
19
+ ## Why it is not another approval gate
20
+
21
+ | Approval gate | Mandate |
22
+ |---|---|
23
+ | Asks "may this tool run?" | Asks "is this exact effect authorised?" |
24
+ | Identity is a self-asserted field | Approver proves possession of an off-host key |
25
+ | Approval is a mutable DB row | Grant + receipt are signed, content-addressed |
26
+ | Logs what happened | Lets anyone prove what was authorised |
27
+
28
+ ## Install
29
+
30
+ CLI + broker + MCP adapter. There is **no web UI** by design: Mandate is a
31
+ local trust primitive you embed in your own stack.
32
+
33
+ ```bash
34
+ python3 -m pip install gama-mandate
35
+ # or from source / git
36
+ python3 -m pip install "git+https://github.com/gamai-io/mandate.git"
37
+
38
+ mandate doctor # checks python, ssh-keygen, policy
39
+ mandate init --dir ./my-deployment # policy, allowed_signers, config, example
40
+ ```
41
+
42
+ ## Quickstart
43
+
44
+ ```bash
45
+ export PYTHONPATH=$PWD
46
+
47
+ # 1. on the approver's device (never on the agent host)
48
+ ssh-keygen -t ed25519 -f ~/.ssh/mandate -C mandate
49
+ # append the PUBLIC key to trust/allowed_signers as: renato ssh-ed25519 AAAA... mandate
50
+
51
+ # 2. agent proposes
52
+ ./bin/mandate propose --intent examples/intent.json --out /tmp/proposal.json
53
+
54
+ # 3. human grants (signs the commitment, bound to one broker + the agent)
55
+ ./bin/mandate grant --proposal /tmp/proposal.json --approver renato \
56
+ --key ~/.ssh/mandate --audience local-broker --out /tmp/grant.json
57
+
58
+ # 4. agent redeems exactly once
59
+ ./bin/mandate redeem --intent examples/intent.json --grant /tmp/grant.json \
60
+ --ledger ledger.jsonl
61
+
62
+ # 5. or let the effect broker execute it (credentials stay in the broker)
63
+ ./bin/mandate-broker --port 8899 --signers trust/allowed_signers \
64
+ --ledger ledger.jsonl --nonce-db broker/used.sqlite &
65
+ ./bin/mandate execute --broker http://127.0.0.1:8899 \
66
+ --intent examples/intent.json --grant /tmp/grant.json
67
+
68
+ # 5. anyone verifies offline
69
+ ./bin/mandate verify --receipt ... --intent examples/intent.json --grant /tmp/grant.json
70
+ ./bin/mandate verify-result --entry /tmp/result.json --broker-signers trust/broker_signers
71
+ ./bin/mandate chain --ledger ledger.jsonl
72
+ ```
73
+
74
+ Exit codes for `check`: `0` allow · `10` ask (grant required) · `20` deny.
75
+
76
+ ## Effect broker (Phase 2)
77
+
78
+ `bin/mandate-broker` is the only component that executes effects. It verifies
79
+ the grant, **atomically claims the nonce** (SQLite `UNIQUE`, so two concurrent
80
+ requests cannot both succeed), runs the effect handler, and appends a signed
81
+ result entry to the hash-chained ledger. The agent calls it over loopback; the
82
+ agent never holds the effect credentials.
83
+
84
+ The shipped handlers are a sandbox (they record the effect under `effects/` or
85
+ `outbox/`). Wiring real credentials (SQL write, SMTP, payment) is Phase 2
86
+ completion — see `PLAN.md`.
87
+
88
+ ### Adapters
89
+
90
+ **ERP precondition (read-only).** Before a grant is consumed, the broker runs a
91
+ named-parameter SQLite query against master data. No row → `counterparty_not_found`;
92
+ amount outside `tolerance_pct` → `amount_mismatch`. The grant is **not**
93
+ consumed when a precondition fails, so a corrected request can retry safely.
94
+
95
+ **SMTP (`message.send`).** `mode=dry_run` writes an `.eml` to the outbox;
96
+ `mode=live` sends via SMTP. The password is read from the env var named in
97
+ `password_env` and never logged or returned.
98
+
99
+ ```json
100
+ "erp": {"db": "erp.sqlite", "checks": {"payment.send": {
101
+ "query": "SELECT invoice_total FROM vendors WHERE name = :target",
102
+ "amount_field": "invoice_total", "tolerance_pct": 0.5}}},
103
+ "smtp": {"mode": "dry_run", "host": "", "user": "",
104
+ "password_env": "MANDATE_SMTP_PASSWORD", "from": "noreply@gama.local"}
105
+ ```
106
+
107
+ ## Key scoping and revocation
108
+
109
+ An `allowed_signers` line **must** declare its namespace scope:
110
+
111
+ ```
112
+ renato namespaces="mandate" ssh-ed25519 AAAAC3Nza... mandate
113
+ gama-broker namespaces="mandate-broker" ssh-ed25519 AAAAC3Nza... broker
114
+ ```
115
+
116
+ Optional validity: `valid-after="YYYYMMDD" valid-before="YYYYMMDD"`.
117
+ Revoke a key by fingerprint:
118
+
119
+ ```bash
120
+ mandate revoke --pub key.pub --reason "laptop lost"
121
+ mandate --signers trust/allowed_signers signers # inspect scopes + fingerprints
122
+ ```
123
+
124
+ ### Idempotent writes and recovery
125
+
126
+ The broker keeps a SQLite **effect journal** (`journal_db`). The flow is
127
+ `intent → executing → executed → receipted`; the authorisation receipt is
128
+ written only **after** the effect commits. On startup the broker recovers:
129
+ a committed effect without a receipt gets one; a claimed grant with no effect is
130
+ abandoned; a duplicate `execute` returns the original receipt and result.
131
+
132
+ `write.mode=sqlite` runs a trusted configured statement together with an
133
+ idempotency insert in **one transaction**, keyed by the grant nonce:
134
+
135
+ ```json
136
+ "write": {"mode": "sqlite", "db": "erp.sqlite",
137
+ "statement": "INSERT INTO payments(nonce,vendor,amount) VALUES(:nonce,:target,:amount)"}
138
+ ```
139
+
140
+ ## Transparency log
141
+
142
+ Every ledger entry is a leaf in an RFC 6962/9162-style Merkle tree
143
+ (`Leaf=SHA256(0x00||JCS(entry))`, `Node=SHA256(0x01||L||R)`). A signed tree
144
+ head (STH) fixes the root and size; inclusion and consistency proofs verify
145
+ offline:
146
+
147
+ ```bash
148
+ mandate log head --ledger ledger.jsonl --log-name log1 --log-key ~/.ssh/mandate-log --out sth.json
149
+ mandate --signers trust/allowed_signers log sth-verify --sth sth.json
150
+ mandate log proof --ledger ledger.jsonl --index 3 --out proof.json
151
+ mandate log verify-inclusion --entry entry.json --proof proof.json --sth sth.json
152
+ mandate log consistency --ledger ledger.jsonl --first 2 --second 3 --out cons.json
153
+ mandate log verify-consistency --old old_sth.json --new sth.json --proof cons.json
154
+ ```
155
+
156
+ A single log cannot prevent **split-view** by its operator; pin the STH on a
157
+ second machine (witness/anchor). The STH signer needs
158
+ `namespaces="mandate-log"` in `allowed_signers`.
159
+
160
+ ### Off-host witness
161
+
162
+ ```bash
163
+ # on the second machine
164
+ mandate --signers log_signers witness serve --state witness.json \
165
+ --token "$WITNESS_TOKEN" --port 8898
166
+
167
+ # on the log host
168
+ mandate witness push --url http://witness:8898 --sth sth.json --token "$WITNESS_TOKEN"
169
+ mandate witness push --url http://witness:8898 --sth sth_new.json --proof cons.json --token "$WITNESS_TOKEN"
170
+ mandate witness show --state witness.json
171
+ ```
172
+
173
+ The witness pins the latest STH and rejects rollback or a same-size different
174
+ root (split-view). Growth requires a consistency proof from the pinned root.
175
+ `witness verify` checks without pinning; `witness accept` pins locally.
176
+
177
+ ## Design invariants
178
+
179
+ 1. **Effect, not tool.** The registry keys on `payment.send`, not on
180
+ `gama_sql`. The same effect is the same commitment regardless of executor.
181
+ 2. **Canonical bytes.** **RFC 8785 (JCS)** canonical JSON; the commitment is the
182
+ hash of `{v, effect, requested_by, render_hash}`. Same content ⇒ same id, in
183
+ any RFC 8785 implementation.
184
+ 3. **Off-host key.** The approver's private key never lives where the agent
185
+ runs. The host stores only public keys (`trust/allowed_signers`).
186
+ 4. **One-shot, nonce-bound.** A grant is redeemable once; replays fail.
187
+ 5. **Agent cannot self-approve.** Enforced in code, not by convention.
188
+ 6. **Fail closed.** Unknown effects are denied. Missing signer ⇒ invalid.
189
+
190
+ ## Layout
191
+
192
+ ```
193
+ spec/ MANDATE-1 data model
194
+ mandate/ stdlib-only reference implementation
195
+ bin/ CLI
196
+ adapters/ MCP server
197
+ tests/ unit + bypass tests (red-team)
198
+ docs/ THREAT_MODEL, NON_GOALS, PLAN
199
+ skills/ agent skill instructions
200
+ ```
201
+
202
+ ## Status
203
+
204
+ **Alpha** — reference implementation v0.10.1: canonical commitment, Ed25519 grant
205
+ bound to the executing agent, one-shot redeem under a file lock, hash-chained
206
+ receipts with a `.head` anchor, idempotent effect journal + write adapter,
207
+ effect broker with signed result entries, read-only ERP preconditions, SMTP
208
+ adapter, Merkle transparency log with an off-host witness, MCP server
209
+ (`mandate-mcp`), and a red-team test suite. See `PLAN.md` for the roadmap and
210
+ `docs/THREAT_MODEL.md` for the known limits.
211
+
212
+ ## Honest limitation
213
+
214
+ Cryptography proves *who authorised what*. It does **not** isolate
215
+ credentials. Until the credentials that cause the effect (SQL write, payment
216
+ API, mail) live only in an effect broker, a compromised agent can bypass
217
+ Mandate entirely. That broker is the next milestone — see `PLAN.md`.
@@ -0,0 +1,11 @@
1
+ # Non-goals
2
+
3
+ - **Not a sandbox.** Mandate does not isolate code execution.
4
+ - **Not an observability platform.** It emits receipts; shipping them to a
5
+ SIEM is someone else's job.
6
+ - **Not a model.** No LLM is called anywhere in the decision path.
7
+ - **Not a payment processor.** It authorises effects; it does not move money.
8
+ - **Not a replacement for legal contracts.** A signed grant proves authorisation,
9
+ not legal validity.
10
+ - **Not zero-config in production.** A trust root (public keys) and a policy
11
+ must be provisioned deliberately.