sasana 1.0.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.
- sasana-1.0.0/LICENSE +21 -0
- sasana-1.0.0/PKG-INFO +216 -0
- sasana-1.0.0/README.md +193 -0
- sasana-1.0.0/archeion/__init__.py +12 -0
- sasana-1.0.0/archeion/keys.py +46 -0
- sasana-1.0.0/archeion/server.py +154 -0
- sasana-1.0.0/pyproject.toml +37 -0
- sasana-1.0.0/sasana/__init__.py +16 -0
- sasana-1.0.0/sasana/_utils.py +29 -0
- sasana-1.0.0/sasana/compliance/__init__.py +60 -0
- sasana-1.0.0/sasana/compliance/eu_ai_act.py +235 -0
- sasana-1.0.0/sasana/compliance/hipaa.py +247 -0
- sasana-1.0.0/sasana/compliance/siem.py +342 -0
- sasana-1.0.0/sasana/compliance/soc2.py +169 -0
- sasana-1.0.0/sasana/envelope.py +55 -0
- sasana-1.0.0/sasana/event_mapper.py +105 -0
- sasana-1.0.0/sasana/events.py +36 -0
- sasana-1.0.0/sasana/integrations/__init__.py +8 -0
- sasana-1.0.0/sasana/integrations/autogpt.py +301 -0
- sasana-1.0.0/sasana/integrations/crewai.py +361 -0
- sasana-1.0.0/sasana/integrations/langgraph.py +302 -0
- sasana-1.0.0/sasana/jcs.py +51 -0
- sasana-1.0.0/sasana/observer.py +140 -0
- sasana-1.0.0/sasana/rfc3161.py +288 -0
- sasana-1.0.0/sasana/signing.py +40 -0
- sasana-1.0.0/sasana/skill.py +129 -0
- sasana-1.0.0/sasana/sqlite_ledger.py +237 -0
- sasana-1.0.0/sasana/verifier.py +484 -0
- sasana-1.0.0/sasana.egg-info/PKG-INFO +216 -0
- sasana-1.0.0/sasana.egg-info/SOURCES.txt +40 -0
- sasana-1.0.0/sasana.egg-info/dependency_links.txt +1 -0
- sasana-1.0.0/sasana.egg-info/entry_points.txt +4 -0
- sasana-1.0.0/sasana.egg-info/requires.txt +15 -0
- sasana-1.0.0/sasana.egg-info/top_level.txt +5 -0
- sasana-1.0.0/sasana_cli.py +195 -0
- sasana-1.0.0/setup.cfg +4 -0
- sasana-1.0.0/tests/test_compliance.py +357 -0
- sasana-1.0.0/tests/test_e2e.py +252 -0
- sasana-1.0.0/tests/test_integrations.py +400 -0
- sasana-1.0.0/tests/test_rfc3161.py +296 -0
- sasana-1.0.0/tests/test_verifier.py +515 -0
- sasana-1.0.0/verify.py +121 -0
sasana-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sahiee-dev
|
|
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.
|
sasana-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sasana
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Tamper-evident cryptographic audit trail for OpenClaw and multi-agent AI sessions
|
|
5
|
+
Author-email: Sahir Shaik Ahamed <sahir150305@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: cryptography>=41.0
|
|
11
|
+
Provides-Extra: observer
|
|
12
|
+
Requires-Dist: websockets>=12.0; extra == "observer"
|
|
13
|
+
Requires-Dist: pyyaml>=6.0; extra == "observer"
|
|
14
|
+
Provides-Extra: archeion
|
|
15
|
+
Requires-Dist: fastapi>=0.110; extra == "archeion"
|
|
16
|
+
Requires-Dist: uvicorn[standard]>=0.27; extra == "archeion"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
20
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
21
|
+
Requires-Dist: fastapi>=0.110; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Sasana
|
|
25
|
+
|
|
26
|
+
**Your AI agent logs are not evidence. Sasana makes them defensible.**
|
|
27
|
+
|
|
28
|
+
Every major observability tool — LangSmith, Arize, Langfuse — stores logs in mutable,
|
|
29
|
+
operator-controlled storage. An administrator with file-system or database access can
|
|
30
|
+
modify or delete a record with no detectable trace. Those logs satisfy a reporting
|
|
31
|
+
requirement. They do not prove what happened.
|
|
32
|
+
|
|
33
|
+
Sasana records a SHA-256 hash-chained audit trail where any modification — to any byte,
|
|
34
|
+
in any historical event — is detectable. Raw content never leaves your machine: only
|
|
35
|
+
hashes are stored.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
$ sasana verify session.jsonl --trust-key <archeion-pubkey>
|
|
39
|
+
|
|
40
|
+
Sasana Verifier v1.0.0
|
|
41
|
+
File : session.jsonl
|
|
42
|
+
Session : 3f8a2c1d-…
|
|
43
|
+
Events : 7
|
|
44
|
+
Evidence : AUTHORITATIVE_EVIDENCE
|
|
45
|
+
|
|
46
|
+
[1/5] Structural validity ... PASS
|
|
47
|
+
[2/5] Sequence integrity ... PASS
|
|
48
|
+
[3/5] Hash chain integrity ... PASS
|
|
49
|
+
[4/5] Session completeness ... PASS
|
|
50
|
+
[5/5] Seal signature ... PASS
|
|
51
|
+
|
|
52
|
+
Result: INTACT ✅
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Who this is for
|
|
58
|
+
|
|
59
|
+
Teams deploying AI agents in regulated environments — fintech, healthtech, HR tech.
|
|
60
|
+
Specifically:
|
|
61
|
+
|
|
62
|
+
- Compliance engineers implementing **EU AI Act Article 12** (tamper-evident logging for
|
|
63
|
+
high-risk AI systems)
|
|
64
|
+
- Security teams who need a cryptographically verifiable audit trail for incident response
|
|
65
|
+
- DevSecOps engineers who need to prove a session log has not been touched since it ended
|
|
66
|
+
|
|
67
|
+
If you are building an AI agent for a use case that falls under GDPR, SOC 2, HIPAA, or
|
|
68
|
+
EU AI Act audit requirements, Sasana is the audit layer.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## What it produces
|
|
73
|
+
|
|
74
|
+
A completed session produces a JSONL file. The verifier checks five properties and
|
|
75
|
+
returns one of three results:
|
|
76
|
+
|
|
77
|
+
| Result | Meaning |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `INTACT` | All checks pass. The log has not been modified. |
|
|
80
|
+
| `PARTIAL` | Hash chain intact but events were dropped during the session. |
|
|
81
|
+
| `COMPROMISED` | Hash chain broken. Log has been modified after the fact. |
|
|
82
|
+
|
|
83
|
+
Exit codes: `0` (INTACT), `1` (COMPROMISED), `2` (PARTIAL), `3` (ERROR) — suitable for
|
|
84
|
+
CI pipeline integration.
|
|
85
|
+
|
|
86
|
+
The evidence class tells you how strong the guarantee is:
|
|
87
|
+
|
|
88
|
+
| Class | Meaning |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `AUTHORITATIVE_EVIDENCE` | Independent sealing authority verified this log. The agent could not have forged this. |
|
|
91
|
+
| `SIGNED_NON_AUTHORITATIVE` | Ed25519 signatures present. Requires private key to forge. |
|
|
92
|
+
| `NON_AUTHORITATIVE_EVIDENCE` | Hash chain intact. Proves no post-hoc modification. |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Install
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install sasana
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Install via OpenClaw
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Via GitHub URL
|
|
106
|
+
openclaw skill install https://github.com/sahiee-dev/Sasana
|
|
107
|
+
|
|
108
|
+
# Via skills.sh shorthand (when listed in the registry)
|
|
109
|
+
openclaw skill install sahiee-dev/Sasana/sasana
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Every session automatically produces `~/.openclaw/sasana/<session_id>.jsonl`.
|
|
113
|
+
No configuration required.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Quick start
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from sasana.sqlite_ledger import SqliteLedger
|
|
121
|
+
import hashlib
|
|
122
|
+
|
|
123
|
+
def sha256(text: str) -> str:
|
|
124
|
+
return hashlib.sha256(text.encode()).hexdigest()
|
|
125
|
+
|
|
126
|
+
ledger = SqliteLedger(db_path="session.db")
|
|
127
|
+
ledger.connect()
|
|
128
|
+
ledger.open_session(session_id="my-session", agent_id="my-agent")
|
|
129
|
+
ledger.record("LLM_CALL", {"prompt_hash": sha256(prompt)})
|
|
130
|
+
ledger.record("LLM_RESPONSE", {"response_hash": sha256(response)})
|
|
131
|
+
ledger.close_session(status="success")
|
|
132
|
+
ledger.export_jsonl("session.jsonl")
|
|
133
|
+
ledger.close()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Verify:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
sasana verify session.jsonl
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Passive observer (zero code changes required):
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pip install sasana[observer]
|
|
146
|
+
sasana observe # auto-detects OpenClaw WebSocket port
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Authority sealing
|
|
152
|
+
|
|
153
|
+
For regulatory submissions and legal proceedings, `NON_AUTHORITATIVE_EVIDENCE` means the
|
|
154
|
+
operator is attesting their own logs. That is insufficient when the operator is a party
|
|
155
|
+
to a dispute.
|
|
156
|
+
|
|
157
|
+
[Archeion](archeion/) is a sealing server that runs inside your security perimeter,
|
|
158
|
+
controlled by your security team — structurally separate from the agent process. The
|
|
159
|
+
agent cannot forge a seal. The sealed log carries `AUTHORITATIVE_EVIDENCE`.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Start Archeion (self-hosted, inside your perimeter)
|
|
163
|
+
docker compose up -d
|
|
164
|
+
|
|
165
|
+
# Seal a completed session
|
|
166
|
+
sasana seal session.jsonl --server http://localhost:8747
|
|
167
|
+
|
|
168
|
+
# Verify with key pinning
|
|
169
|
+
sasana verify session.jsonl --trust-key <archeion-pubkey>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
See [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) for the full deployment guide — key
|
|
173
|
+
lifecycle, network isolation, and what to tell a security reviewer.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## How it works
|
|
178
|
+
|
|
179
|
+
Each event is stored with:
|
|
180
|
+
|
|
181
|
+
- **SHA-256 hash** over RFC 8785 canonical JSON — any mutation changes the hash
|
|
182
|
+
- **`prev_hash`** — each event commits to all prior events, forming a chain
|
|
183
|
+
- **Ed25519 signature** — optional per-session keypair; or Archeion's independent key
|
|
184
|
+
|
|
185
|
+
Raw content is **never stored** — only hashes. You cannot reconstruct what the agent said
|
|
186
|
+
from a Sasana log.
|
|
187
|
+
|
|
188
|
+
A Rust binary (`sasana-rs/`) verifies sessions without a Python dependency — for forensic
|
|
189
|
+
environments where Python is not present or trusted.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Compliance mapping
|
|
194
|
+
|
|
195
|
+
| Regulation | Requirement addressed |
|
|
196
|
+
|---|---|
|
|
197
|
+
| EU AI Act Article 12 | Tamper-evident automatic recording for high-risk AI systems |
|
|
198
|
+
| SOC 2 CC7.2 | System monitoring with cryptographically verifiable audit trail |
|
|
199
|
+
| HIPAA §164.312(b) | Audit control for healthcare AI; raw PHI never recorded |
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## What Sasana does not do
|
|
204
|
+
|
|
205
|
+
- **Does not record raw content.** Hashes only — you cannot reconstruct prompts or
|
|
206
|
+
responses.
|
|
207
|
+
- **Does not prevent tampering.** Detects it. Detection and prevention are different.
|
|
208
|
+
- **Does not replace LangSmith or Arize.** Those tools are for observability. Sasana is
|
|
209
|
+
for evidence production. They are complementary.
|
|
210
|
+
- **Does not have a managed cloud offering.** Self-hosted only.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT
|
sasana-1.0.0/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Sasana
|
|
2
|
+
|
|
3
|
+
**Your AI agent logs are not evidence. Sasana makes them defensible.**
|
|
4
|
+
|
|
5
|
+
Every major observability tool — LangSmith, Arize, Langfuse — stores logs in mutable,
|
|
6
|
+
operator-controlled storage. An administrator with file-system or database access can
|
|
7
|
+
modify or delete a record with no detectable trace. Those logs satisfy a reporting
|
|
8
|
+
requirement. They do not prove what happened.
|
|
9
|
+
|
|
10
|
+
Sasana records a SHA-256 hash-chained audit trail where any modification — to any byte,
|
|
11
|
+
in any historical event — is detectable. Raw content never leaves your machine: only
|
|
12
|
+
hashes are stored.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
$ sasana verify session.jsonl --trust-key <archeion-pubkey>
|
|
16
|
+
|
|
17
|
+
Sasana Verifier v1.0.0
|
|
18
|
+
File : session.jsonl
|
|
19
|
+
Session : 3f8a2c1d-…
|
|
20
|
+
Events : 7
|
|
21
|
+
Evidence : AUTHORITATIVE_EVIDENCE
|
|
22
|
+
|
|
23
|
+
[1/5] Structural validity ... PASS
|
|
24
|
+
[2/5] Sequence integrity ... PASS
|
|
25
|
+
[3/5] Hash chain integrity ... PASS
|
|
26
|
+
[4/5] Session completeness ... PASS
|
|
27
|
+
[5/5] Seal signature ... PASS
|
|
28
|
+
|
|
29
|
+
Result: INTACT ✅
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Who this is for
|
|
35
|
+
|
|
36
|
+
Teams deploying AI agents in regulated environments — fintech, healthtech, HR tech.
|
|
37
|
+
Specifically:
|
|
38
|
+
|
|
39
|
+
- Compliance engineers implementing **EU AI Act Article 12** (tamper-evident logging for
|
|
40
|
+
high-risk AI systems)
|
|
41
|
+
- Security teams who need a cryptographically verifiable audit trail for incident response
|
|
42
|
+
- DevSecOps engineers who need to prove a session log has not been touched since it ended
|
|
43
|
+
|
|
44
|
+
If you are building an AI agent for a use case that falls under GDPR, SOC 2, HIPAA, or
|
|
45
|
+
EU AI Act audit requirements, Sasana is the audit layer.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## What it produces
|
|
50
|
+
|
|
51
|
+
A completed session produces a JSONL file. The verifier checks five properties and
|
|
52
|
+
returns one of three results:
|
|
53
|
+
|
|
54
|
+
| Result | Meaning |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `INTACT` | All checks pass. The log has not been modified. |
|
|
57
|
+
| `PARTIAL` | Hash chain intact but events were dropped during the session. |
|
|
58
|
+
| `COMPROMISED` | Hash chain broken. Log has been modified after the fact. |
|
|
59
|
+
|
|
60
|
+
Exit codes: `0` (INTACT), `1` (COMPROMISED), `2` (PARTIAL), `3` (ERROR) — suitable for
|
|
61
|
+
CI pipeline integration.
|
|
62
|
+
|
|
63
|
+
The evidence class tells you how strong the guarantee is:
|
|
64
|
+
|
|
65
|
+
| Class | Meaning |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `AUTHORITATIVE_EVIDENCE` | Independent sealing authority verified this log. The agent could not have forged this. |
|
|
68
|
+
| `SIGNED_NON_AUTHORITATIVE` | Ed25519 signatures present. Requires private key to forge. |
|
|
69
|
+
| `NON_AUTHORITATIVE_EVIDENCE` | Hash chain intact. Proves no post-hoc modification. |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install sasana
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Install via OpenClaw
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Via GitHub URL
|
|
83
|
+
openclaw skill install https://github.com/sahiee-dev/Sasana
|
|
84
|
+
|
|
85
|
+
# Via skills.sh shorthand (when listed in the registry)
|
|
86
|
+
openclaw skill install sahiee-dev/Sasana/sasana
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Every session automatically produces `~/.openclaw/sasana/<session_id>.jsonl`.
|
|
90
|
+
No configuration required.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Quick start
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from sasana.sqlite_ledger import SqliteLedger
|
|
98
|
+
import hashlib
|
|
99
|
+
|
|
100
|
+
def sha256(text: str) -> str:
|
|
101
|
+
return hashlib.sha256(text.encode()).hexdigest()
|
|
102
|
+
|
|
103
|
+
ledger = SqliteLedger(db_path="session.db")
|
|
104
|
+
ledger.connect()
|
|
105
|
+
ledger.open_session(session_id="my-session", agent_id="my-agent")
|
|
106
|
+
ledger.record("LLM_CALL", {"prompt_hash": sha256(prompt)})
|
|
107
|
+
ledger.record("LLM_RESPONSE", {"response_hash": sha256(response)})
|
|
108
|
+
ledger.close_session(status="success")
|
|
109
|
+
ledger.export_jsonl("session.jsonl")
|
|
110
|
+
ledger.close()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Verify:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
sasana verify session.jsonl
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Passive observer (zero code changes required):
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install sasana[observer]
|
|
123
|
+
sasana observe # auto-detects OpenClaw WebSocket port
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Authority sealing
|
|
129
|
+
|
|
130
|
+
For regulatory submissions and legal proceedings, `NON_AUTHORITATIVE_EVIDENCE` means the
|
|
131
|
+
operator is attesting their own logs. That is insufficient when the operator is a party
|
|
132
|
+
to a dispute.
|
|
133
|
+
|
|
134
|
+
[Archeion](archeion/) is a sealing server that runs inside your security perimeter,
|
|
135
|
+
controlled by your security team — structurally separate from the agent process. The
|
|
136
|
+
agent cannot forge a seal. The sealed log carries `AUTHORITATIVE_EVIDENCE`.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Start Archeion (self-hosted, inside your perimeter)
|
|
140
|
+
docker compose up -d
|
|
141
|
+
|
|
142
|
+
# Seal a completed session
|
|
143
|
+
sasana seal session.jsonl --server http://localhost:8747
|
|
144
|
+
|
|
145
|
+
# Verify with key pinning
|
|
146
|
+
sasana verify session.jsonl --trust-key <archeion-pubkey>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
See [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) for the full deployment guide — key
|
|
150
|
+
lifecycle, network isolation, and what to tell a security reviewer.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## How it works
|
|
155
|
+
|
|
156
|
+
Each event is stored with:
|
|
157
|
+
|
|
158
|
+
- **SHA-256 hash** over RFC 8785 canonical JSON — any mutation changes the hash
|
|
159
|
+
- **`prev_hash`** — each event commits to all prior events, forming a chain
|
|
160
|
+
- **Ed25519 signature** — optional per-session keypair; or Archeion's independent key
|
|
161
|
+
|
|
162
|
+
Raw content is **never stored** — only hashes. You cannot reconstruct what the agent said
|
|
163
|
+
from a Sasana log.
|
|
164
|
+
|
|
165
|
+
A Rust binary (`sasana-rs/`) verifies sessions without a Python dependency — for forensic
|
|
166
|
+
environments where Python is not present or trusted.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Compliance mapping
|
|
171
|
+
|
|
172
|
+
| Regulation | Requirement addressed |
|
|
173
|
+
|---|---|
|
|
174
|
+
| EU AI Act Article 12 | Tamper-evident automatic recording for high-risk AI systems |
|
|
175
|
+
| SOC 2 CC7.2 | System monitoring with cryptographically verifiable audit trail |
|
|
176
|
+
| HIPAA §164.312(b) | Audit control for healthcare AI; raw PHI never recorded |
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## What Sasana does not do
|
|
181
|
+
|
|
182
|
+
- **Does not record raw content.** Hashes only — you cannot reconstruct prompts or
|
|
183
|
+
responses.
|
|
184
|
+
- **Does not prevent tampering.** Detects it. Detection and prevention are different.
|
|
185
|
+
- **Does not replace LangSmith or Arize.** Those tools are for observability. Sasana is
|
|
186
|
+
for evidence production. They are complementary.
|
|
187
|
+
- **Does not have a managed cloud offering.** Self-hosted only.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Archeion — Sasana authority sealing server.
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def run() -> None:
|
|
5
|
+
"""Entry point for the `archeion` CLI command."""
|
|
6
|
+
try:
|
|
7
|
+
from archeion.server import run as _run
|
|
8
|
+
except ModuleNotFoundError as exc:
|
|
9
|
+
raise SystemExit(
|
|
10
|
+
f"archeion: missing dependency ({exc.name}). Install with: pip install sasana[archeion]"
|
|
11
|
+
) from exc
|
|
12
|
+
_run()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
archeion/keys.py — Persistent Ed25519 server keypair.
|
|
3
|
+
|
|
4
|
+
Storage priority:
|
|
5
|
+
1. ARCHEION_PRIVATE_KEY env var (base64 raw bytes) — for containers / CI.
|
|
6
|
+
2. ARCHEION_KEY_FILE env var path.
|
|
7
|
+
3. ~/.archeion/server_key.b64 — default persistent location.
|
|
8
|
+
|
|
9
|
+
A new keypair is generated and persisted when none of the above exist.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
19
|
+
|
|
20
|
+
_DEFAULT_KEY_FILE = Path.home() / ".archeion" / "server_key.b64"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _key_path() -> Path:
|
|
24
|
+
return Path(os.environ.get("ARCHEION_KEY_FILE", str(_DEFAULT_KEY_FILE)))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_or_generate() -> Ed25519PrivateKey:
|
|
28
|
+
"""Return the server private key, generating and persisting one if absent."""
|
|
29
|
+
raw_b64 = os.environ.get("ARCHEION_PRIVATE_KEY")
|
|
30
|
+
if raw_b64:
|
|
31
|
+
return Ed25519PrivateKey.from_private_bytes(base64.b64decode(raw_b64))
|
|
32
|
+
|
|
33
|
+
key_path = _key_path()
|
|
34
|
+
if key_path.exists():
|
|
35
|
+
return Ed25519PrivateKey.from_private_bytes(base64.b64decode(key_path.read_text().strip()))
|
|
36
|
+
|
|
37
|
+
private_key = Ed25519PrivateKey.generate()
|
|
38
|
+
key_path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
key_path.write_text(base64.b64encode(private_key.private_bytes_raw()).decode())
|
|
40
|
+
key_path.chmod(0o600)
|
|
41
|
+
return private_key
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def pubkey_b64(private_key: Ed25519PrivateKey) -> str:
|
|
45
|
+
"""Return the base64-encoded raw public key bytes."""
|
|
46
|
+
return base64.b64encode(private_key.public_key().public_bytes_raw()).decode()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
archeion/server.py — Sasana authority sealing server.
|
|
3
|
+
|
|
4
|
+
Endpoints:
|
|
5
|
+
POST /seal — Verify a session JSONL, append a signed CHAIN_SEAL, return sealed JSONL.
|
|
6
|
+
GET /pubkey — Return the server Ed25519 public key.
|
|
7
|
+
|
|
8
|
+
Run:
|
|
9
|
+
uvicorn archeion.server:app --host 0.0.0.0 --port 8747
|
|
10
|
+
|
|
11
|
+
The server key is loaded from ~/.archeion/server_key.b64 on startup (generated if absent).
|
|
12
|
+
Override with ARCHEION_PRIVATE_KEY (base64) or ARCHEION_KEY_FILE env vars.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import datetime
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
22
|
+
from fastapi.responses import PlainTextResponse
|
|
23
|
+
|
|
24
|
+
from archeion.keys import load_or_generate, pubkey_b64
|
|
25
|
+
from sasana.signing import sign_event_hash
|
|
26
|
+
from sasana.verifier import compute_event_hash, verify
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("archeion")
|
|
29
|
+
|
|
30
|
+
app = FastAPI(
|
|
31
|
+
title="Archeion",
|
|
32
|
+
description="Sasana authority sealing server — upgrades sessions to AUTHORITATIVE_EVIDENCE.",
|
|
33
|
+
version="1.0.0",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Keypair is loaded once at module import so TestClient and uvicorn share the same key.
|
|
37
|
+
_private_key = load_or_generate()
|
|
38
|
+
_pubkey = pubkey_b64(_private_key)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.get("/health")
|
|
42
|
+
async def health() -> dict:
|
|
43
|
+
"""Liveness + readiness probe. Returns current server pubkey for key-change detection."""
|
|
44
|
+
return {"status": "ok", "version": app.version, "pubkey": _pubkey}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.get("/pubkey")
|
|
48
|
+
async def get_pubkey() -> dict:
|
|
49
|
+
"""Return the server Ed25519 public key. Clients pin this to trust sealed sessions."""
|
|
50
|
+
return {"pubkey": _pubkey, "algorithm": "ed25519", "encoding": "base64"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.post("/seal", response_class=PlainTextResponse)
|
|
54
|
+
async def seal(request: Request) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Accept a session JSONL body, verify its hash chain, append a signed CHAIN_SEAL event,
|
|
57
|
+
and return the sealed JSONL.
|
|
58
|
+
|
|
59
|
+
Errors:
|
|
60
|
+
400 — malformed or empty body.
|
|
61
|
+
409 — session already carries a CHAIN_SEAL.
|
|
62
|
+
422 — hash chain broken or session incomplete.
|
|
63
|
+
"""
|
|
64
|
+
body = await request.body()
|
|
65
|
+
try:
|
|
66
|
+
events = [json.loads(ln) for ln in body.decode().splitlines() if ln.strip()]
|
|
67
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
68
|
+
raise HTTPException(status_code=400, detail=f"Invalid JSONL: {exc}")
|
|
69
|
+
|
|
70
|
+
if not events:
|
|
71
|
+
raise HTTPException(status_code=400, detail="Empty session body")
|
|
72
|
+
|
|
73
|
+
if any(e.get("event_type") == "CHAIN_SEAL" for e in events):
|
|
74
|
+
raise HTTPException(status_code=409, detail="Session is already sealed")
|
|
75
|
+
|
|
76
|
+
result = verify(events)
|
|
77
|
+
if result.status == "PARTIAL":
|
|
78
|
+
raise HTTPException(
|
|
79
|
+
status_code=422,
|
|
80
|
+
detail=(
|
|
81
|
+
f"Cannot seal: session has {result.log_drop_count} LOG_DROP event(s). "
|
|
82
|
+
"Sealing a partial log would falsely imply a complete record."
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
if result.status != "INTACT":
|
|
86
|
+
raise HTTPException(
|
|
87
|
+
status_code=422,
|
|
88
|
+
detail=f"Cannot seal: session is {result.status}. Errors: {result.errors[:5]}",
|
|
89
|
+
)
|
|
90
|
+
if not result.checks.get("completeness", {}).get("has_session_end"):
|
|
91
|
+
raise HTTPException(status_code=422, detail="Cannot seal: SESSION_END not present")
|
|
92
|
+
|
|
93
|
+
sorted_events = sorted(events, key=lambda e: e.get("seq", 0))
|
|
94
|
+
last = sorted_events[-1]
|
|
95
|
+
|
|
96
|
+
# Verify RFC 3161 timestamp token if SESSION_START carries one
|
|
97
|
+
_ts_verified = False
|
|
98
|
+
_ts_utc: str | None = None
|
|
99
|
+
session_start = next((e for e in sorted_events if e.get("event_type") == "SESSION_START"), None)
|
|
100
|
+
if session_start:
|
|
101
|
+
import base64 as _b64
|
|
102
|
+
import hashlib as _hl
|
|
103
|
+
|
|
104
|
+
from sasana.jcs import canonicalize as _jcs
|
|
105
|
+
from sasana.rfc3161 import verify_timestamp
|
|
106
|
+
|
|
107
|
+
token_b64 = session_start.get("payload", {}).get("rfc3161_token")
|
|
108
|
+
if token_b64:
|
|
109
|
+
try:
|
|
110
|
+
token_der = _b64.b64decode(token_b64)
|
|
111
|
+
payload_no_tok = {
|
|
112
|
+
k: v
|
|
113
|
+
for k, v in session_start.get("payload", {}).items()
|
|
114
|
+
if k != "rfc3161_token"
|
|
115
|
+
}
|
|
116
|
+
ev_no_tok = {
|
|
117
|
+
k: v for k, v in session_start.items() if k not in ("event_hash", "signature")
|
|
118
|
+
}
|
|
119
|
+
ev_no_tok["payload"] = payload_no_tok
|
|
120
|
+
pre_token_hash = _hl.sha256(_jcs(ev_no_tok)).digest()
|
|
121
|
+
_ts_verified, _ts_utc = verify_timestamp(token_der, pre_token_hash)
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
logger.debug("Archeion: RFC 3161 verification skipped: %s", exc)
|
|
124
|
+
|
|
125
|
+
ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
|
|
126
|
+
seal_payload: dict = {
|
|
127
|
+
"session_hash": last["event_hash"],
|
|
128
|
+
"sealed_by": "archeion",
|
|
129
|
+
"server_pubkey": _pubkey,
|
|
130
|
+
"timestamp_verified": _ts_verified,
|
|
131
|
+
}
|
|
132
|
+
if _ts_utc is not None:
|
|
133
|
+
seal_payload["timestamp_utc"] = _ts_utc
|
|
134
|
+
|
|
135
|
+
seal_event: dict = {
|
|
136
|
+
"seq": last["seq"] + 1,
|
|
137
|
+
"event_type": "CHAIN_SEAL",
|
|
138
|
+
"session_id": last["session_id"],
|
|
139
|
+
"timestamp": ts,
|
|
140
|
+
"payload": seal_payload,
|
|
141
|
+
"prev_hash": last["event_hash"],
|
|
142
|
+
}
|
|
143
|
+
seal_event["event_hash"] = compute_event_hash(seal_event)
|
|
144
|
+
seal_event["signature"] = sign_event_hash(_private_key, seal_event["event_hash"])
|
|
145
|
+
|
|
146
|
+
logger.info("Sealed session=%s events=%d", last["session_id"], len(sorted_events) + 1)
|
|
147
|
+
return "\n".join(json.dumps(e) for e in sorted_events + [seal_event]) + "\n"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def run() -> None:
|
|
151
|
+
"""Entry point for `archeion` CLI command."""
|
|
152
|
+
import uvicorn
|
|
153
|
+
|
|
154
|
+
uvicorn.run("archeion.server:app", host="0.0.0.0", port=8747, reload=False)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sasana"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Tamper-evident cryptographic audit trail for OpenClaw and multi-agent AI sessions"
|
|
9
|
+
authors = [{name = "Sahir Shaik Ahamed", email = "sahir150305@gmail.com"}]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"cryptography>=41.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
observer = ["websockets>=12.0", "pyyaml>=6.0"]
|
|
19
|
+
archeion = ["fastapi>=0.110", "uvicorn[standard]>=0.27"]
|
|
20
|
+
dev = ["pytest>=7.0", "ruff>=0.4", "httpx>=0.27", "fastapi>=0.110"]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
sasana = "sasana_cli:main"
|
|
24
|
+
agentops-verify = "verify:main"
|
|
25
|
+
archeion = "archeion:run"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
include = ["sasana*", "archeion*"]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
py-modules = ["verify", "sasana_cli"]
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
line-length = 100
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
ignore = ["E501"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sasana — Tamper-evident cryptographic audit trail for OpenClaw sessions.
|
|
3
|
+
|
|
4
|
+
Every session produces two artifacts:
|
|
5
|
+
~/.openclaw/sasana/<session_id>.db — SHA-256 hash-chained SQLite ledger
|
|
6
|
+
~/.openclaw/sasana/<session_id>.jsonl — JSONL export for offline verification
|
|
7
|
+
|
|
8
|
+
Verify with:
|
|
9
|
+
sasana verify ~/.openclaw/sasana/<session_id>.jsonl
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from sasana.skill import SasanaSkill
|
|
13
|
+
from sasana.observer import OpenClawObserver
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0"
|
|
16
|
+
__all__ = ["SasanaSkill", "OpenClawObserver"]
|