verityledger 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.
- verityledger-0.1.0/LICENSE +21 -0
- verityledger-0.1.0/PKG-INFO +158 -0
- verityledger-0.1.0/README.md +134 -0
- verityledger-0.1.0/pyproject.toml +58 -0
- verityledger-0.1.0/setup.cfg +4 -0
- verityledger-0.1.0/src/verityledger/__init__.py +24 -0
- verityledger-0.1.0/src/verityledger/chain.py +107 -0
- verityledger-0.1.0/src/verityledger/cli/__init__.py +3 -0
- verityledger-0.1.0/src/verityledger/cli/__main__.py +134 -0
- verityledger-0.1.0/src/verityledger/core.py +204 -0
- verityledger-0.1.0/src/verityledger/exceptions.py +34 -0
- verityledger-0.1.0/src/verityledger/py.typed +0 -0
- verityledger-0.1.0/src/verityledger/storage/__init__.py +4 -0
- verityledger-0.1.0/src/verityledger/storage/base.py +45 -0
- verityledger-0.1.0/src/verityledger/storage/local.py +73 -0
- verityledger-0.1.0/src/verityledger.egg-info/PKG-INFO +158 -0
- verityledger-0.1.0/src/verityledger.egg-info/SOURCES.txt +23 -0
- verityledger-0.1.0/src/verityledger.egg-info/dependency_links.txt +1 -0
- verityledger-0.1.0/src/verityledger.egg-info/entry_points.txt +2 -0
- verityledger-0.1.0/src/verityledger.egg-info/requires.txt +6 -0
- verityledger-0.1.0/src/verityledger.egg-info/top_level.txt +1 -0
- verityledger-0.1.0/tests/test_chain.py +62 -0
- verityledger-0.1.0/tests/test_cli.py +120 -0
- verityledger-0.1.0/tests/test_core.py +165 -0
- verityledger-0.1.0/tests/test_storage.py +87 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tracewell
|
|
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,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verityledger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tamper-evident decision and tool-call logging for AI agents.
|
|
5
|
+
Author: VerityLedger
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/verityledger/verityledger
|
|
8
|
+
Project-URL: Issues, https://github.com/verityledger/verityledger/issues
|
|
9
|
+
Keywords: ai,agents,audit,logging,llm,observability
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
22
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# VerityLedger
|
|
26
|
+
|
|
27
|
+
**Know exactly what your AI agent did, and prove nothing was changed after the fact.**
|
|
28
|
+
|
|
29
|
+
VerityLedger is a small Python library that wraps your agent's tool calls and
|
|
30
|
+
decisions in a tamper-evident, hash-chained log. When something goes wrong —
|
|
31
|
+
a bad refund, a broken deploy, a strange customer reply — you can pull up the
|
|
32
|
+
exact sequence of tool calls, model inputs/outputs, and reasoning that led
|
|
33
|
+
there, and prove the record hasn't been altered.
|
|
34
|
+
|
|
35
|
+
No database. No external service. No blockchain. Just an append-only file
|
|
36
|
+
and SHA-256.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from verityledger import Tracer
|
|
40
|
+
|
|
41
|
+
tracer = Tracer() # writes to ./verityledger_log.jsonl
|
|
42
|
+
|
|
43
|
+
with tracer.session(agent="support-bot", user="user_123") as session:
|
|
44
|
+
|
|
45
|
+
@session.trace_tool
|
|
46
|
+
def issue_refund(order_id: str, amount: float) -> str:
|
|
47
|
+
return f"refunded {amount} for {order_id}"
|
|
48
|
+
|
|
49
|
+
issue_refund("ORD-4471", 42.00)
|
|
50
|
+
|
|
51
|
+
session.log_decision(
|
|
52
|
+
"approved refund",
|
|
53
|
+
reasoning="customer reported damaged item, photo provided, within policy",
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Every call to `issue_refund`, every `log_decision`, and any model calls you
|
|
58
|
+
log are written as chained entries — each one includes a hash of the
|
|
59
|
+
previous entry. If anyone edits a past entry, the chain breaks at exactly
|
|
60
|
+
that point.
|
|
61
|
+
|
|
62
|
+
## Why this exists
|
|
63
|
+
|
|
64
|
+
Agents are making real decisions — refunds, emails, code pushes, customer
|
|
65
|
+
replies — and most teams have no record of *why* beyond scattered print
|
|
66
|
+
statements and provider dashboards. When a regulator, a customer, or your
|
|
67
|
+
own team asks "why did the bot do that?", you want an answer that's both
|
|
68
|
+
complete and verifiable.
|
|
69
|
+
|
|
70
|
+
## Install
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install verityledger
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Verify the chain
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
valid, break_index = tracer.verify(session.id)
|
|
80
|
+
# valid == True, break_index == None (until someone tampers with the log)
|
|
81
|
+
|
|
82
|
+
# Or raise on problems:
|
|
83
|
+
tracer.assert_valid(session.id)
|
|
84
|
+
# raises ChainIntegrityError or SessionNotFoundError
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Export an audit report
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
tracer.export_report(session.id, "incident_report.json")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Produces a single JSON file with every entry for that session, plus the
|
|
94
|
+
verification result — ready to attach to an incident review or compliance
|
|
95
|
+
request.
|
|
96
|
+
|
|
97
|
+
## CLI
|
|
98
|
+
|
|
99
|
+
Installing the package also installs a `verityledger` command:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
verityledger sessions # list session ids in the log
|
|
103
|
+
verityledger show <session_id> # print all entries for a session
|
|
104
|
+
verityledger verify <session_id> # check the hash chain for tampering
|
|
105
|
+
verityledger export <session_id> report.json # write a JSON audit report
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
All commands accept `--log PATH` to point at a specific log file
|
|
109
|
+
(default: `./verityledger_log.jsonl`).
|
|
110
|
+
|
|
111
|
+
## Architecture
|
|
112
|
+
|
|
113
|
+
The library is layered so each piece can be tested and replaced independently:
|
|
114
|
+
|
|
115
|
+
- **`chain.py`** — the cryptographic primitive. Defines `Entry`, hashing,
|
|
116
|
+
and `verify_chain`. No I/O, no dependencies.
|
|
117
|
+
- **`storage/`** — the `Store` interface plus `LocalStore` (append-only
|
|
118
|
+
JSONL). A hosted backend (SQLite/Postgres/API) implements the same
|
|
119
|
+
interface and drops in without touching anything above it.
|
|
120
|
+
- **`core.py`** — the public API: `Tracer` and `Session`.
|
|
121
|
+
- **`cli/`** — the `verityledger` terminal command. Built entirely on top of
|
|
122
|
+
the public API above; no logic of its own beyond argument parsing
|
|
123
|
+
and output formatting.
|
|
124
|
+
- **`exceptions.py`** — shared error types (`StorageError`,
|
|
125
|
+
`ChainIntegrityError`, `SessionNotFoundError`).
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pip install -e ".[dev]"
|
|
131
|
+
ruff check . # lint
|
|
132
|
+
mypy src/verityledger # strict type check
|
|
133
|
+
pytest # tests + coverage
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Status
|
|
137
|
+
|
|
138
|
+
Early release. The local file-based logger (above) is free and open source
|
|
139
|
+
(MIT) — your data never leaves your machine. A hosted dashboard for
|
|
140
|
+
searching across sessions, team access, and longer retention is in
|
|
141
|
+
development.
|
|
142
|
+
|
|
143
|
+
## Roadmap
|
|
144
|
+
|
|
145
|
+
- [x] Hash-chained local logging (Python)
|
|
146
|
+
- [x] Tool-call decorator, decision logging, model-call logging
|
|
147
|
+
- [x] Tamper detection / chain verification
|
|
148
|
+
- [x] Audit report export
|
|
149
|
+
- [x] Full test suite, type checking, CI
|
|
150
|
+
- [x] CLI (`verityledger sessions/show/verify/export`)
|
|
151
|
+
- [ ] JavaScript/TypeScript SDK
|
|
152
|
+
- [ ] Hosted dashboard (search, team accounts, retention policies)
|
|
153
|
+
- [ ] LangChain / OpenAI / Anthropic tool-use integrations
|
|
154
|
+
- [ ] Remote ingestion endpoint (send logs to VerityLedger Cloud)
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# VerityLedger
|
|
2
|
+
|
|
3
|
+
**Know exactly what your AI agent did, and prove nothing was changed after the fact.**
|
|
4
|
+
|
|
5
|
+
VerityLedger is a small Python library that wraps your agent's tool calls and
|
|
6
|
+
decisions in a tamper-evident, hash-chained log. When something goes wrong —
|
|
7
|
+
a bad refund, a broken deploy, a strange customer reply — you can pull up the
|
|
8
|
+
exact sequence of tool calls, model inputs/outputs, and reasoning that led
|
|
9
|
+
there, and prove the record hasn't been altered.
|
|
10
|
+
|
|
11
|
+
No database. No external service. No blockchain. Just an append-only file
|
|
12
|
+
and SHA-256.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from verityledger import Tracer
|
|
16
|
+
|
|
17
|
+
tracer = Tracer() # writes to ./verityledger_log.jsonl
|
|
18
|
+
|
|
19
|
+
with tracer.session(agent="support-bot", user="user_123") as session:
|
|
20
|
+
|
|
21
|
+
@session.trace_tool
|
|
22
|
+
def issue_refund(order_id: str, amount: float) -> str:
|
|
23
|
+
return f"refunded {amount} for {order_id}"
|
|
24
|
+
|
|
25
|
+
issue_refund("ORD-4471", 42.00)
|
|
26
|
+
|
|
27
|
+
session.log_decision(
|
|
28
|
+
"approved refund",
|
|
29
|
+
reasoning="customer reported damaged item, photo provided, within policy",
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Every call to `issue_refund`, every `log_decision`, and any model calls you
|
|
34
|
+
log are written as chained entries — each one includes a hash of the
|
|
35
|
+
previous entry. If anyone edits a past entry, the chain breaks at exactly
|
|
36
|
+
that point.
|
|
37
|
+
|
|
38
|
+
## Why this exists
|
|
39
|
+
|
|
40
|
+
Agents are making real decisions — refunds, emails, code pushes, customer
|
|
41
|
+
replies — and most teams have no record of *why* beyond scattered print
|
|
42
|
+
statements and provider dashboards. When a regulator, a customer, or your
|
|
43
|
+
own team asks "why did the bot do that?", you want an answer that's both
|
|
44
|
+
complete and verifiable.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install verityledger
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Verify the chain
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
valid, break_index = tracer.verify(session.id)
|
|
56
|
+
# valid == True, break_index == None (until someone tampers with the log)
|
|
57
|
+
|
|
58
|
+
# Or raise on problems:
|
|
59
|
+
tracer.assert_valid(session.id)
|
|
60
|
+
# raises ChainIntegrityError or SessionNotFoundError
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Export an audit report
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
tracer.export_report(session.id, "incident_report.json")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Produces a single JSON file with every entry for that session, plus the
|
|
70
|
+
verification result — ready to attach to an incident review or compliance
|
|
71
|
+
request.
|
|
72
|
+
|
|
73
|
+
## CLI
|
|
74
|
+
|
|
75
|
+
Installing the package also installs a `verityledger` command:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
verityledger sessions # list session ids in the log
|
|
79
|
+
verityledger show <session_id> # print all entries for a session
|
|
80
|
+
verityledger verify <session_id> # check the hash chain for tampering
|
|
81
|
+
verityledger export <session_id> report.json # write a JSON audit report
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
All commands accept `--log PATH` to point at a specific log file
|
|
85
|
+
(default: `./verityledger_log.jsonl`).
|
|
86
|
+
|
|
87
|
+
## Architecture
|
|
88
|
+
|
|
89
|
+
The library is layered so each piece can be tested and replaced independently:
|
|
90
|
+
|
|
91
|
+
- **`chain.py`** — the cryptographic primitive. Defines `Entry`, hashing,
|
|
92
|
+
and `verify_chain`. No I/O, no dependencies.
|
|
93
|
+
- **`storage/`** — the `Store` interface plus `LocalStore` (append-only
|
|
94
|
+
JSONL). A hosted backend (SQLite/Postgres/API) implements the same
|
|
95
|
+
interface and drops in without touching anything above it.
|
|
96
|
+
- **`core.py`** — the public API: `Tracer` and `Session`.
|
|
97
|
+
- **`cli/`** — the `verityledger` terminal command. Built entirely on top of
|
|
98
|
+
the public API above; no logic of its own beyond argument parsing
|
|
99
|
+
and output formatting.
|
|
100
|
+
- **`exceptions.py`** — shared error types (`StorageError`,
|
|
101
|
+
`ChainIntegrityError`, `SessionNotFoundError`).
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install -e ".[dev]"
|
|
107
|
+
ruff check . # lint
|
|
108
|
+
mypy src/verityledger # strict type check
|
|
109
|
+
pytest # tests + coverage
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Status
|
|
113
|
+
|
|
114
|
+
Early release. The local file-based logger (above) is free and open source
|
|
115
|
+
(MIT) — your data never leaves your machine. A hosted dashboard for
|
|
116
|
+
searching across sessions, team access, and longer retention is in
|
|
117
|
+
development.
|
|
118
|
+
|
|
119
|
+
## Roadmap
|
|
120
|
+
|
|
121
|
+
- [x] Hash-chained local logging (Python)
|
|
122
|
+
- [x] Tool-call decorator, decision logging, model-call logging
|
|
123
|
+
- [x] Tamper detection / chain verification
|
|
124
|
+
- [x] Audit report export
|
|
125
|
+
- [x] Full test suite, type checking, CI
|
|
126
|
+
- [x] CLI (`verityledger sessions/show/verify/export`)
|
|
127
|
+
- [ ] JavaScript/TypeScript SDK
|
|
128
|
+
- [ ] Hosted dashboard (search, team accounts, retention policies)
|
|
129
|
+
- [ ] LangChain / OpenAI / Anthropic tool-use integrations
|
|
130
|
+
- [ ] Remote ingestion endpoint (send logs to VerityLedger Cloud)
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "verityledger"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Tamper-evident decision and tool-call logging for AI agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "VerityLedger" }]
|
|
13
|
+
keywords = ["ai", "agents", "audit", "logging", "llm", "observability"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Software Development :: Libraries",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=8.0",
|
|
26
|
+
"pytest-cov>=5.0",
|
|
27
|
+
"ruff>=0.6",
|
|
28
|
+
"mypy>=1.10",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
verityledger = "verityledger.cli:main"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/verityledger/verityledger"
|
|
36
|
+
Issues = "https://github.com/verityledger/verityledger/issues"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
verityledger = ["py.typed"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
addopts = "--cov=verityledger --cov-report=term-missing"
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 100
|
|
50
|
+
src = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
54
|
+
|
|
55
|
+
[tool.mypy]
|
|
56
|
+
python_version = "3.10"
|
|
57
|
+
strict = true
|
|
58
|
+
mypy_path = "src"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .chain import Entry, verify_chain
|
|
2
|
+
from .core import Session, Tracer
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
ChainIntegrityError,
|
|
5
|
+
SessionNotFoundError,
|
|
6
|
+
StorageError,
|
|
7
|
+
VerityLedgerError,
|
|
8
|
+
)
|
|
9
|
+
from .storage import LocalStore, Store
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Tracer",
|
|
13
|
+
"Session",
|
|
14
|
+
"Entry",
|
|
15
|
+
"verify_chain",
|
|
16
|
+
"Store",
|
|
17
|
+
"LocalStore",
|
|
18
|
+
"VerityLedgerError",
|
|
19
|
+
"StorageError",
|
|
20
|
+
"ChainIntegrityError",
|
|
21
|
+
"SessionNotFoundError",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hash-chained, tamper-evident log entries.
|
|
3
|
+
|
|
4
|
+
Each entry includes a hash of the previous entry, forming a chain.
|
|
5
|
+
Any modification to a past entry breaks the chain for everything after it,
|
|
6
|
+
making tampering detectable without needing a blockchain or external service.
|
|
7
|
+
|
|
8
|
+
This module has zero dependencies on storage or the public API — it is the
|
|
9
|
+
single source of truth for what an "entry" is and how the chain is verified.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
import uuid
|
|
18
|
+
from dataclasses import asdict, dataclass, field
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
GENESIS_HASH = "0" * 64
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _canonical_json(data: dict[str, Any]) -> str:
|
|
25
|
+
"""Serialize a dict deterministically so hashes are reproducible."""
|
|
26
|
+
return json.dumps(data, sort_keys=True, separators=(",", ":"), default=str)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Entry:
|
|
31
|
+
"""A single chained log entry."""
|
|
32
|
+
|
|
33
|
+
id: str
|
|
34
|
+
session_id: str
|
|
35
|
+
timestamp: float
|
|
36
|
+
event_type: str
|
|
37
|
+
data: dict[str, Any]
|
|
38
|
+
previous_hash: str
|
|
39
|
+
hash: str = field(default="")
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
return asdict(self)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_dict(cls, raw: dict[str, Any]) -> Entry:
|
|
46
|
+
return cls(**raw)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def content_hash(self) -> str:
|
|
50
|
+
"""Hash of this entry's content, excluding the stored `hash` field."""
|
|
51
|
+
content = {k: v for k, v in self.to_dict().items() if k != "hash"}
|
|
52
|
+
return hashlib.sha256(_canonical_json(content).encode("utf-8")).hexdigest()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def make_entry(
|
|
56
|
+
session_id: str,
|
|
57
|
+
event_type: str,
|
|
58
|
+
data: dict[str, Any],
|
|
59
|
+
previous_hash: str,
|
|
60
|
+
) -> Entry:
|
|
61
|
+
"""
|
|
62
|
+
Build a single chained log entry, with its hash computed and set.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
session_id: identifier for the agent run / conversation this belongs to.
|
|
66
|
+
event_type: e.g. "tool_call", "model_call", "decision".
|
|
67
|
+
data: arbitrary JSON-serializable payload for this event.
|
|
68
|
+
previous_hash: hash of the prior entry in this session's chain
|
|
69
|
+
(use GENESIS_HASH for the first entry).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A completed, hashed Entry.
|
|
73
|
+
"""
|
|
74
|
+
entry = Entry(
|
|
75
|
+
id=str(uuid.uuid4()),
|
|
76
|
+
session_id=session_id,
|
|
77
|
+
timestamp=time.time(),
|
|
78
|
+
event_type=event_type,
|
|
79
|
+
data=data,
|
|
80
|
+
previous_hash=previous_hash,
|
|
81
|
+
hash="",
|
|
82
|
+
)
|
|
83
|
+
return Entry(**{**entry.to_dict(), "hash": entry.content_hash})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def verify_chain(entries: list[Entry]) -> tuple[bool, int | None]:
|
|
87
|
+
"""
|
|
88
|
+
Verify a list of entries forms an unbroken, untampered hash chain.
|
|
89
|
+
|
|
90
|
+
The list is assumed to be in chronological order for a single session.
|
|
91
|
+
An empty list is considered valid (vacuously).
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
(is_valid, index_of_first_break_or_none)
|
|
95
|
+
"""
|
|
96
|
+
expected_previous = GENESIS_HASH
|
|
97
|
+
|
|
98
|
+
for i, entry in enumerate(entries):
|
|
99
|
+
if entry.previous_hash != expected_previous:
|
|
100
|
+
return False, i
|
|
101
|
+
|
|
102
|
+
if entry.content_hash != entry.hash:
|
|
103
|
+
return False, i
|
|
104
|
+
|
|
105
|
+
expected_previous = entry.hash
|
|
106
|
+
|
|
107
|
+
return True, None
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VerityLedger CLI.
|
|
3
|
+
|
|
4
|
+
Lets you inspect, verify, and export logs from the terminal without
|
|
5
|
+
writing any Python. Built entirely on top of the public `verityledger`
|
|
6
|
+
API (Tracer/LocalStore) - the CLI has no logic of its own beyond
|
|
7
|
+
argument parsing and formatting.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
verityledger sessions [--log PATH]
|
|
11
|
+
verityledger show SESSION_ID [--log PATH]
|
|
12
|
+
verityledger verify SESSION_ID [--log PATH]
|
|
13
|
+
verityledger export SESSION_ID OUT_PATH [--log PATH]
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
|
|
22
|
+
from ..core import Tracer
|
|
23
|
+
from ..exceptions import ChainIntegrityError, SessionNotFoundError, VerityLedgerError
|
|
24
|
+
|
|
25
|
+
DEFAULT_LOG_PATH = "./verityledger_log.jsonl"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="verityledger",
|
|
31
|
+
description="Inspect, verify, and export VerityLedger agent logs.",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--log",
|
|
35
|
+
default=DEFAULT_LOG_PATH,
|
|
36
|
+
help=f"Path to the log file (default: {DEFAULT_LOG_PATH})",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
40
|
+
|
|
41
|
+
subparsers.add_parser("sessions", help="List all session ids in the log.")
|
|
42
|
+
|
|
43
|
+
show = subparsers.add_parser("show", help="Print all entries for a session.")
|
|
44
|
+
show.add_argument("session_id")
|
|
45
|
+
|
|
46
|
+
verify = subparsers.add_parser("verify", help="Check a session's hash chain for tampering.")
|
|
47
|
+
verify.add_argument("session_id")
|
|
48
|
+
|
|
49
|
+
export = subparsers.add_parser("export", help="Export a session as a JSON audit report.")
|
|
50
|
+
export.add_argument("session_id")
|
|
51
|
+
export.add_argument("out_path")
|
|
52
|
+
|
|
53
|
+
return parser
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _format_timestamp(ts: float) -> str:
|
|
57
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_sessions(tracer: Tracer) -> int:
|
|
61
|
+
sessions = tracer.store.all_sessions()
|
|
62
|
+
if not sessions:
|
|
63
|
+
print("No sessions found.")
|
|
64
|
+
return 0
|
|
65
|
+
for sid in sessions:
|
|
66
|
+
count = len(tracer.store.get_session(sid))
|
|
67
|
+
print(f"{sid} ({count} entries)")
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_show(tracer: Tracer, session_id: str) -> int:
|
|
72
|
+
entries = tracer.store.get_session(session_id)
|
|
73
|
+
if not entries:
|
|
74
|
+
print(f"No entries found for session '{session_id}'.", file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
for entry in entries:
|
|
77
|
+
print(f"[{_format_timestamp(entry.timestamp)}] {entry.event_type}")
|
|
78
|
+
for key, value in entry.data.items():
|
|
79
|
+
print(f" {key}: {value}")
|
|
80
|
+
print(f" hash: {entry.hash[:12]}...")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cmd_verify(tracer: Tracer, session_id: str) -> int:
|
|
85
|
+
try:
|
|
86
|
+
tracer.assert_valid(session_id)
|
|
87
|
+
except SessionNotFoundError as exc:
|
|
88
|
+
print(str(exc), file=sys.stderr)
|
|
89
|
+
return 1
|
|
90
|
+
except ChainIntegrityError as exc:
|
|
91
|
+
print(f"TAMPERING DETECTED: {exc}", file=sys.stderr)
|
|
92
|
+
return 1
|
|
93
|
+
|
|
94
|
+
entry_count = len(tracer.store.get_session(session_id))
|
|
95
|
+
print(f"Chain valid: {entry_count} entries, no tampering detected.")
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def cmd_export(tracer: Tracer, session_id: str, out_path: str) -> int:
|
|
100
|
+
try:
|
|
101
|
+
report = tracer.export_report(session_id, out_path)
|
|
102
|
+
except SessionNotFoundError as exc:
|
|
103
|
+
print(str(exc), file=sys.stderr)
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
status = "valid" if report["chain_valid"] else "TAMPERED"
|
|
107
|
+
print(f"Exported {report['entry_count']} entries to {out_path} (chain: {status}).")
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main(argv: list[str] | None = None) -> int:
|
|
112
|
+
parser = _build_parser()
|
|
113
|
+
args = parser.parse_args(argv)
|
|
114
|
+
tracer = Tracer(log_path=args.log)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
if args.command == "sessions":
|
|
118
|
+
return cmd_sessions(tracer)
|
|
119
|
+
if args.command == "show":
|
|
120
|
+
return cmd_show(tracer, args.session_id)
|
|
121
|
+
if args.command == "verify":
|
|
122
|
+
return cmd_verify(tracer, args.session_id)
|
|
123
|
+
if args.command == "export":
|
|
124
|
+
return cmd_export(tracer, args.session_id, args.out_path)
|
|
125
|
+
except VerityLedgerError as exc:
|
|
126
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
parser.print_help()
|
|
130
|
+
return 1
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
sys.exit(main())
|