moltrust-enforce 0.1.0__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.
@@ -0,0 +1,87 @@
1
+ # Environment & Secrets
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ .secret
6
+ *_private_key*
7
+ *.key
8
+ *.pem
9
+
10
+ # AWS / KMS
11
+ .aws/
12
+ aws-credentials
13
+
14
+ # Backup files
15
+ *.bak
16
+ *.bak2
17
+ *.bak3
18
+
19
+ # Python
20
+ __pycache__/
21
+ *.pyc
22
+ *.pyo
23
+ *.pyd
24
+ .Python
25
+ env/
26
+ venv/
27
+ .venv/
28
+ *.egg-info/
29
+ dist/
30
+ build/
31
+
32
+ # DB
33
+ *.sql
34
+ *.sqlite
35
+ !init_db.sql
36
+ !migrations/*.sql
37
+ !app/migrations/*.sql
38
+ backups/
39
+
40
+ # Logs
41
+ *.log
42
+ logs/
43
+
44
+ # Data
45
+ data/
46
+
47
+ # Wallet
48
+ wallet/
49
+
50
+ # OS
51
+ .DS_Store
52
+ Thumbs.db
53
+
54
+ # IDE
55
+ .vscode/
56
+ .idea/
57
+
58
+ # Test artifacts
59
+ .pytest_cache/
60
+ htmlcov/
61
+ .coverage
62
+
63
+ # Node
64
+ node_modules/
65
+ reviews/
66
+
67
+ # Backup patterns - never commit, never clutter status
68
+ *.bak
69
+ *.bak.*
70
+ *.bak-*
71
+ *_backup.py
72
+ *.backup
73
+
74
+ # Experimental scratchpads — uncommit until productized
75
+ experiments/
76
+
77
+ # Runtime state written by daemons (not source-of-truth)
78
+ state/*.json
79
+ monitor/.poll_state.json
80
+ monitor/*.log
81
+ logs/
82
+ drafts/
83
+ moltbook/state.json
84
+ agents/workspace/*/MEMORY.md
85
+
86
+ # Pre-USPTO filing — no public disclosure until grace period closes (~March 2027)
87
+ docs/patent_evaluation.md
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.5
2
+ Name: moltrust-enforce
3
+ Version: 0.1.0
4
+ Summary: Reference client for the MolTrust enforce-mode runtime check (POST /enforce/check)
5
+ Project-URL: Homepage, https://moltrust.ch
6
+ Project-URL: Source, https://github.com/MoltyCel/moltrust-api/tree/main/sdk/python
7
+ Author-email: Lars Kroehl <lars@moltrust.ch>
8
+ License: MIT
9
+ Keywords: aae,agent,authorization,enforcement,moltrust
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Security
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: httpx>=0.24
16
+ Requires-Dist: jcs>=0.2
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest>=7; extra == 'test'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # moltrust-enforce
22
+
23
+ Referenz-Client für den MolTrust-Laufzeit-Check `POST /enforce/check` (`constraint_mode = "enforce"`).
24
+
25
+ Dünn und ausdrücklich: kein Decorator, kein Framework-Hook, keine versteckte Middleware. Zwei Methoden, und der Betreiber sieht bei beiden, was passiert. Framework-agnostisch — wo der Aufruf im eigenen Code steht, entscheidet der Betreiber.
26
+
27
+ Das SDK **prüft** Mandate. Es stellt keine aus: Erzeugen und Signieren von Mandaten ist nicht Teil davon.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install -e sdk/python # aus dem Repo
33
+ ```
34
+
35
+ Nicht auf PyPI. Die Veröffentlichung ist bewusst ein eigener, menschlich freigegebener Schritt.
36
+
37
+ ## Muster 1 — dem Server glauben
38
+
39
+ Der einfache Weg. Ein Aufruf, ein Verdikt.
40
+
41
+ ```python
42
+ from moltrust_enforce import EnforceClient
43
+
44
+ client = EnforceClient("https://api.moltrust.ch", api_key=API_KEY)
45
+
46
+ verdict = client.check(mandate, transaction)
47
+
48
+ if verdict.permitted:
49
+ execute(transaction)
50
+ else:
51
+ log.warning("blocked: %s (%s)", verdict.verdict, verdict.reason)
52
+ ```
53
+
54
+ `verdict.permitted` ist nur bei `PERMIT` wahr. `PENDING` ist keine Erlaubnis, `DENY` erst recht nicht.
55
+
56
+ ## Muster 2 — selbst nachrechnen
57
+
58
+ Der eigentliche Punkt. Das Verdikt hängt allein an Mandat und Transaktion — kein Serverzustand, keine Uhr, keine Datenbank. Wer beide Eingaben hat, rechnet es lokal nach und braucht dem Server nicht zu glauben.
59
+
60
+ ```python
61
+ verdict = client.check(mandate, transaction)
62
+ result = client.verify(verdict, mandate, transaction)
63
+
64
+ if not result.ok:
65
+ # Der Server hat etwas anderes gesagt als die Eingaben hergeben.
66
+ alert("enforce server disagrees with local recompute", result.mismatches)
67
+ return # nicht ausführen
68
+
69
+ if verdict.permitted:
70
+ execute(transaction)
71
+ ```
72
+
73
+ `verify()` prüft dreifach: ob die Antwort sich selbst trägt (`core_digest` passt zum mitgelieferten `core`), ob die lokale Auswertung denselben Digest ergibt, und ob der Server dasselbe Verdikt nennt wie die lokale Auswertung. Jede Abweichung landet in `result.mismatches`.
74
+
75
+ Ganz ohne Server geht es auch — der Kern ist öffentlich:
76
+
77
+ ```python
78
+ from moltrust_enforce import enforce_check
79
+ local = enforce_check(mandate, transaction)
80
+ ```
81
+
82
+ ## Fail-closed
83
+
84
+ Ein PERMIT entsteht ausschließlich aus einer gelesenen 200-Antwort, die PERMIT sagt. Alles andere ist DENY:
85
+
86
+ | Lage | Ergebnis |
87
+ |---|---|
88
+ | Server nicht erreichbar, Timeout, DNS-Fehler | `DENY`, `from_server=False` |
89
+ | HTTP 4xx/5xx | `DENY`, `from_server=False` |
90
+ | Antwort ist kein JSON / hat die falsche Form | `DENY`, `from_server=False` |
91
+ | `verdict`-Wert unbekannt | `DENY`, `from_server=False` |
92
+ | Kein gültiges Mandat im Request | `DENY` (der Server antwortet 200 mit DENY-Record) |
93
+
94
+ Wer die Störung lieber als Ausnahme behandelt:
95
+
96
+ ```python
97
+ client = EnforceClient(..., on_transport_error="raise") # wirft EnforceTransportError
98
+ ```
99
+
100
+ Beide Einstellungen sind fail-closed. Ein drittes, durchlassendes Verhalten gibt es nicht — kein Schalter, der eine unerreichbare Prüfung in eine Erlaubnis verwandelt.
101
+
102
+ ## PENDING
103
+
104
+ `check()` gibt `PENDING` unverändert zurück. Das SDK löst es nicht auf, und `permitted` bleibt False — eine PENDING-Aktion kommt hier nie still durch.
105
+
106
+ Der optionale Haken meldet, er entscheidet nicht: sein Rückgabewert wird ignoriert, das Verdikt bleibt `PENDING`.
107
+
108
+ ```python
109
+ def queue_for_approval(verdict):
110
+ approvals.put(verdict.core_digest) # melden
111
+
112
+ client = EnforceClient(..., on_pending=queue_for_approval)
113
+
114
+ verdict = client.check(mandate, transaction)
115
+ if verdict.pending:
116
+ return # der Betreiber muss handeln
117
+ ```
118
+
119
+ Ohne Haken passiert dasselbe, nur ohne Meldung: `PENDING` kommt zurück, `permitted` ist False, ausgeführt wird nichts.
120
+
121
+ ## Mandat und Transaktion
122
+
123
+ Ein Mandat trägt Grants. Ein Grant bindet an eine Aktion (`action_binding`), führt Constraints und eine `disposition`:
124
+
125
+ ```python
126
+ from moltrust_enforce import action_digest
127
+
128
+ action = {"verb": "transfer", "asset": "USDC", "chain": "base"}
129
+
130
+ mandate = {
131
+ "grants": [{
132
+ "action_binding": action_digest(action),
133
+ "disposition": "allow", # allow | hold | forbid
134
+ "constraints": [
135
+ {"type": "exact", "field": "to", "value": "0xABC…"},
136
+ {"type": "enum", "field": "region", "values": ["CH", "DE"]},
137
+ {"type": "range", "field": "amount", "lo": 0, "hi": 1000},
138
+ ],
139
+ }],
140
+ }
141
+
142
+ transaction = {"action": action, "to": "0xABC…", "region": "CH", "amount": 500}
143
+ ```
144
+
145
+ `exact` vergleicht exakt — kein Präfix, kein Case-Folding, keine Normalisierung; eine Vanity-Adresse mit gleichem Anfang fällt durch. `enum` vergleicht jedes Element exakt. `range` ist ein geschlossenes Ganzzahl-Intervall `lo ≤ arg ≤ hi`; Fließkommazahlen werden abgewiesen, weil sie die Nachrechenbarkeit brechen.
146
+
147
+ PERMIT gibt es nur, wenn ein Grant per `action_binding` trifft, alle seine Constraints halten und die `disposition` `allow` ist. Eine Aktion, die kein Grant adressiert, ist DENY und nie PENDING. `forbid` hat Vorrang vor einem erlaubenden Grant.
148
+
149
+ ## Kopplung an die Server-Signatur
150
+
151
+ Das SDK ist an die Signatur aus [PR #306](https://github.com/MoltyCel/moltrust-api/pull/306) gekoppelt:
152
+
153
+ - Request `{"mandate": …, "transaction": …, "prev_core_digest": "sha256:<64 hex>"|null}`
154
+ - Antwort `{"verdict", "reason", "grant_index", "trace", "record": {"core", "core_digest"}}`
155
+
156
+ `src/moltrust_enforce/_core.py` ist eine unveränderte Kopie von `app/enforcement/enforce_check.py`. Genau eine Zeile weicht ab: der Import der JCS-Kanonisierung zeigt hier direkt auf `jcs` statt auf `app.signature`, weil das SDK ohne das Server-Paket auskommen muss. `tests/test_core_parity.py` prüft beides — dass keine zweite Zeile abweicht, und dass beide Fassungen über einen Fallkorpus dieselben Digests liefern.
157
+
158
+ Ändert sich die Signatur, muss das SDK nachziehen.
159
+
160
+ ## Tests
161
+
162
+ ```bash
163
+ cd sdk/python && pip install -e ".[test]" && pytest
164
+ ```
165
+
166
+ ## Lizenz
167
+
168
+ MIT
@@ -0,0 +1,148 @@
1
+ # moltrust-enforce
2
+
3
+ Referenz-Client für den MolTrust-Laufzeit-Check `POST /enforce/check` (`constraint_mode = "enforce"`).
4
+
5
+ Dünn und ausdrücklich: kein Decorator, kein Framework-Hook, keine versteckte Middleware. Zwei Methoden, und der Betreiber sieht bei beiden, was passiert. Framework-agnostisch — wo der Aufruf im eigenen Code steht, entscheidet der Betreiber.
6
+
7
+ Das SDK **prüft** Mandate. Es stellt keine aus: Erzeugen und Signieren von Mandaten ist nicht Teil davon.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install -e sdk/python # aus dem Repo
13
+ ```
14
+
15
+ Nicht auf PyPI. Die Veröffentlichung ist bewusst ein eigener, menschlich freigegebener Schritt.
16
+
17
+ ## Muster 1 — dem Server glauben
18
+
19
+ Der einfache Weg. Ein Aufruf, ein Verdikt.
20
+
21
+ ```python
22
+ from moltrust_enforce import EnforceClient
23
+
24
+ client = EnforceClient("https://api.moltrust.ch", api_key=API_KEY)
25
+
26
+ verdict = client.check(mandate, transaction)
27
+
28
+ if verdict.permitted:
29
+ execute(transaction)
30
+ else:
31
+ log.warning("blocked: %s (%s)", verdict.verdict, verdict.reason)
32
+ ```
33
+
34
+ `verdict.permitted` ist nur bei `PERMIT` wahr. `PENDING` ist keine Erlaubnis, `DENY` erst recht nicht.
35
+
36
+ ## Muster 2 — selbst nachrechnen
37
+
38
+ Der eigentliche Punkt. Das Verdikt hängt allein an Mandat und Transaktion — kein Serverzustand, keine Uhr, keine Datenbank. Wer beide Eingaben hat, rechnet es lokal nach und braucht dem Server nicht zu glauben.
39
+
40
+ ```python
41
+ verdict = client.check(mandate, transaction)
42
+ result = client.verify(verdict, mandate, transaction)
43
+
44
+ if not result.ok:
45
+ # Der Server hat etwas anderes gesagt als die Eingaben hergeben.
46
+ alert("enforce server disagrees with local recompute", result.mismatches)
47
+ return # nicht ausführen
48
+
49
+ if verdict.permitted:
50
+ execute(transaction)
51
+ ```
52
+
53
+ `verify()` prüft dreifach: ob die Antwort sich selbst trägt (`core_digest` passt zum mitgelieferten `core`), ob die lokale Auswertung denselben Digest ergibt, und ob der Server dasselbe Verdikt nennt wie die lokale Auswertung. Jede Abweichung landet in `result.mismatches`.
54
+
55
+ Ganz ohne Server geht es auch — der Kern ist öffentlich:
56
+
57
+ ```python
58
+ from moltrust_enforce import enforce_check
59
+ local = enforce_check(mandate, transaction)
60
+ ```
61
+
62
+ ## Fail-closed
63
+
64
+ Ein PERMIT entsteht ausschließlich aus einer gelesenen 200-Antwort, die PERMIT sagt. Alles andere ist DENY:
65
+
66
+ | Lage | Ergebnis |
67
+ |---|---|
68
+ | Server nicht erreichbar, Timeout, DNS-Fehler | `DENY`, `from_server=False` |
69
+ | HTTP 4xx/5xx | `DENY`, `from_server=False` |
70
+ | Antwort ist kein JSON / hat die falsche Form | `DENY`, `from_server=False` |
71
+ | `verdict`-Wert unbekannt | `DENY`, `from_server=False` |
72
+ | Kein gültiges Mandat im Request | `DENY` (der Server antwortet 200 mit DENY-Record) |
73
+
74
+ Wer die Störung lieber als Ausnahme behandelt:
75
+
76
+ ```python
77
+ client = EnforceClient(..., on_transport_error="raise") # wirft EnforceTransportError
78
+ ```
79
+
80
+ Beide Einstellungen sind fail-closed. Ein drittes, durchlassendes Verhalten gibt es nicht — kein Schalter, der eine unerreichbare Prüfung in eine Erlaubnis verwandelt.
81
+
82
+ ## PENDING
83
+
84
+ `check()` gibt `PENDING` unverändert zurück. Das SDK löst es nicht auf, und `permitted` bleibt False — eine PENDING-Aktion kommt hier nie still durch.
85
+
86
+ Der optionale Haken meldet, er entscheidet nicht: sein Rückgabewert wird ignoriert, das Verdikt bleibt `PENDING`.
87
+
88
+ ```python
89
+ def queue_for_approval(verdict):
90
+ approvals.put(verdict.core_digest) # melden
91
+
92
+ client = EnforceClient(..., on_pending=queue_for_approval)
93
+
94
+ verdict = client.check(mandate, transaction)
95
+ if verdict.pending:
96
+ return # der Betreiber muss handeln
97
+ ```
98
+
99
+ Ohne Haken passiert dasselbe, nur ohne Meldung: `PENDING` kommt zurück, `permitted` ist False, ausgeführt wird nichts.
100
+
101
+ ## Mandat und Transaktion
102
+
103
+ Ein Mandat trägt Grants. Ein Grant bindet an eine Aktion (`action_binding`), führt Constraints und eine `disposition`:
104
+
105
+ ```python
106
+ from moltrust_enforce import action_digest
107
+
108
+ action = {"verb": "transfer", "asset": "USDC", "chain": "base"}
109
+
110
+ mandate = {
111
+ "grants": [{
112
+ "action_binding": action_digest(action),
113
+ "disposition": "allow", # allow | hold | forbid
114
+ "constraints": [
115
+ {"type": "exact", "field": "to", "value": "0xABC…"},
116
+ {"type": "enum", "field": "region", "values": ["CH", "DE"]},
117
+ {"type": "range", "field": "amount", "lo": 0, "hi": 1000},
118
+ ],
119
+ }],
120
+ }
121
+
122
+ transaction = {"action": action, "to": "0xABC…", "region": "CH", "amount": 500}
123
+ ```
124
+
125
+ `exact` vergleicht exakt — kein Präfix, kein Case-Folding, keine Normalisierung; eine Vanity-Adresse mit gleichem Anfang fällt durch. `enum` vergleicht jedes Element exakt. `range` ist ein geschlossenes Ganzzahl-Intervall `lo ≤ arg ≤ hi`; Fließkommazahlen werden abgewiesen, weil sie die Nachrechenbarkeit brechen.
126
+
127
+ PERMIT gibt es nur, wenn ein Grant per `action_binding` trifft, alle seine Constraints halten und die `disposition` `allow` ist. Eine Aktion, die kein Grant adressiert, ist DENY und nie PENDING. `forbid` hat Vorrang vor einem erlaubenden Grant.
128
+
129
+ ## Kopplung an die Server-Signatur
130
+
131
+ Das SDK ist an die Signatur aus [PR #306](https://github.com/MoltyCel/moltrust-api/pull/306) gekoppelt:
132
+
133
+ - Request `{"mandate": …, "transaction": …, "prev_core_digest": "sha256:<64 hex>"|null}`
134
+ - Antwort `{"verdict", "reason", "grant_index", "trace", "record": {"core", "core_digest"}}`
135
+
136
+ `src/moltrust_enforce/_core.py` ist eine unveränderte Kopie von `app/enforcement/enforce_check.py`. Genau eine Zeile weicht ab: der Import der JCS-Kanonisierung zeigt hier direkt auf `jcs` statt auf `app.signature`, weil das SDK ohne das Server-Paket auskommen muss. `tests/test_core_parity.py` prüft beides — dass keine zweite Zeile abweicht, und dass beide Fassungen über einen Fallkorpus dieselben Digests liefern.
137
+
138
+ Ändert sich die Signatur, muss das SDK nachziehen.
139
+
140
+ ## Tests
141
+
142
+ ```bash
143
+ cd sdk/python && pip install -e ".[test]" && pytest
144
+ ```
145
+
146
+ ## Lizenz
147
+
148
+ MIT
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "moltrust-enforce"
7
+ version = "0.1.0"
8
+ description = "Reference client for the MolTrust enforce-mode runtime check (POST /enforce/check)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ # Klassische License-Metadaten (CLAUDE.md Python-Package-Standard): text + leere
12
+ # license-files, damit PyPI/twine kein License-Expression/License-File erhalten.
13
+ license = { text = "MIT" }
14
+ license-files = []
15
+ authors = [{ name = "Lars Kroehl", email = "lars@moltrust.ch" }]
16
+ keywords = ["moltrust", "agent", "authorization", "enforcement", "aae"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Security",
22
+ ]
23
+ dependencies = [
24
+ "httpx>=0.24",
25
+ "jcs>=0.2",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ test = ["pytest>=7"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://moltrust.ch"
33
+ Source = "https://github.com/MoltyCel/moltrust-api/tree/main/sdk/python"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/moltrust_enforce"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
@@ -0,0 +1,56 @@
1
+ """moltrust-enforce — Referenz-Client fuer MolTrust `POST /enforce/check`.
2
+
3
+ Zwei Nutzungsmuster, beide ausdruecklich:
4
+
5
+ from moltrust_enforce import EnforceClient
6
+
7
+ client = EnforceClient("https://api.moltrust.ch", api_key=KEY)
8
+
9
+ v = client.check(mandate, transaction) # dem Server glauben
10
+ if not v.permitted:
11
+ return
12
+
13
+ r = client.verify(v, mandate, transaction) # selbst nachrechnen
14
+ if not r.ok:
15
+ raise RuntimeError(r.mismatches)
16
+
17
+ Der lokale Nachrechen-Kern (`enforce_check`, `action_digest`, `core_digest`, `recompute`) ist
18
+ eine unveraenderte Kopie von `app/enforcement/enforce_check.py` aus dem Server-Repo; einzig
19
+ die Import-Zeile fuer die JCS-Kanonisierung zeigt hier direkt auf `jcs` statt auf
20
+ `app.signature`. `tests/test_core_parity.py` haelt das nach.
21
+
22
+ Das SDK prueft Mandate, es stellt keine aus. Signieren und Ausgeben von Mandaten ist
23
+ ausdruecklich nicht Teil davon.
24
+ """
25
+ from ._core import (
26
+ DENY,
27
+ ENFORCE_VERSION,
28
+ PENDING,
29
+ PERMIT,
30
+ action_digest,
31
+ core_digest,
32
+ enforce_check,
33
+ recompute,
34
+ )
35
+ from .client import EnforceClient, Verdict, VerifyResult
36
+ from .errors import EnforceError, EnforceProtocolError, EnforceTransportError
37
+
38
+ __version__ = "0.1.0"
39
+
40
+ __all__ = [
41
+ "EnforceClient",
42
+ "Verdict",
43
+ "VerifyResult",
44
+ "EnforceError",
45
+ "EnforceTransportError",
46
+ "EnforceProtocolError",
47
+ "enforce_check",
48
+ "recompute",
49
+ "action_digest",
50
+ "core_digest",
51
+ "PERMIT",
52
+ "DENY",
53
+ "PENDING",
54
+ "ENFORCE_VERSION",
55
+ "__version__",
56
+ ]