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.
- moltrust_enforce-0.1.0/.gitignore +87 -0
- moltrust_enforce-0.1.0/PKG-INFO +168 -0
- moltrust_enforce-0.1.0/README.md +148 -0
- moltrust_enforce-0.1.0/pyproject.toml +39 -0
- moltrust_enforce-0.1.0/src/moltrust_enforce/__init__.py +56 -0
- moltrust_enforce-0.1.0/src/moltrust_enforce/_core.py +335 -0
- moltrust_enforce-0.1.0/src/moltrust_enforce/client.py +288 -0
- moltrust_enforce-0.1.0/src/moltrust_enforce/errors.py +27 -0
- moltrust_enforce-0.1.0/tests/conftest.py +91 -0
- moltrust_enforce-0.1.0/tests/test_client.py +193 -0
- moltrust_enforce-0.1.0/tests/test_core_parity.py +128 -0
- moltrust_enforce-0.1.0/tests/test_verify.py +182 -0
|
@@ -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
|
+
]
|