phb-agentledger 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.
Files changed (28) hide show
  1. phb_agentledger-0.1.0/.gitignore +8 -0
  2. phb_agentledger-0.1.0/LICENSE +40 -0
  3. phb_agentledger-0.1.0/PKG-INFO +135 -0
  4. phb_agentledger-0.1.0/README.md +114 -0
  5. phb_agentledger-0.1.0/agentledger/__init__.py +15 -0
  6. phb_agentledger-0.1.0/agentledger/canonical.py +43 -0
  7. phb_agentledger-0.1.0/agentledger/events.py +113 -0
  8. phb_agentledger-0.1.0/agentledger/ledger.py +544 -0
  9. phb_agentledger-0.1.0/agentledger/schemas/audit_value_packaging/AuditEvent-1.0.0.schema.json +159 -0
  10. phb_agentledger-0.1.0/agentledger/schemas/audit_value_packaging/AuditQueryRequest-1.0.0.schema.json +177 -0
  11. phb_agentledger-0.1.0/agentledger/schemas/audit_value_packaging/AuditTrace-1.0.0.schema.json +279 -0
  12. phb_agentledger-0.1.0/agentledger/schemas/core_wire/RecordEnvelope-1.0.0.schema.json +115 -0
  13. phb_agentledger-0.1.0/agentledger/validation.py +214 -0
  14. phb_agentledger-0.1.0/demo.py +53 -0
  15. phb_agentledger-0.1.0/pyproject.toml +37 -0
  16. phb_agentledger-0.1.0/tests/fixtures/invalid/missing_required.json +30 -0
  17. phb_agentledger-0.1.0/tests/fixtures/invalid/null_where_forbidden.json +31 -0
  18. phb_agentledger-0.1.0/tests/fixtures/invalid/record_digest_mismatch.json +31 -0
  19. phb_agentledger-0.1.0/tests/fixtures/invalid/schema_hash_mismatch.json +31 -0
  20. phb_agentledger-0.1.0/tests/fixtures/invalid/unknown_field.json +32 -0
  21. phb_agentledger-0.1.0/tests/fixtures/invalid/unsupported_version.json +31 -0
  22. phb_agentledger-0.1.0/tests/fixtures/invalid/wrong_type.json +31 -0
  23. phb_agentledger-0.1.0/tests/fixtures/valid/golden_full.json +33 -0
  24. phb_agentledger-0.1.0/tests/fixtures/valid/golden_minimal.json +31 -0
  25. phb_agentledger-0.1.0/tests/test_durability.py +329 -0
  26. phb_agentledger-0.1.0/tests/test_ledger.py +220 -0
  27. phb_agentledger-0.1.0/tests/test_make_event.py +112 -0
  28. phb_agentledger-0.1.0/tests/test_reload_fast_path.py +154 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ demo_audit.jsonl
5
+ demo_audit.jsonl.tail
6
+ *.egg-info/
7
+ build/
8
+ dist/
@@ -0,0 +1,40 @@
1
+ AgentLedger — Licence
2
+ Copyright (c) 2026 Przemyslaw H. Budrewicz trading as PHB Studio.
3
+ All rights reserved.
4
+
5
+ This software is source-available, not open source. Two licences are offered.
6
+
7
+ 1. NON-COMMERCIAL LICENCE (free)
8
+
9
+ You may use, copy, modify, and run this software free of charge for personal
10
+ use, education, academic research, and internal evaluation, including
11
+ evaluation inside a company. You may not use it in or as part of a product,
12
+ service, or internal system that supports a commercial activity beyond
13
+ evaluation. You must keep this notice intact in any copy you distribute.
14
+
15
+ 2. COMMERCIAL LICENCE (paid)
16
+
17
+ Any use beyond the above — including use in production, in a product or
18
+ service offered to others, or in the internal operations of a business —
19
+ requires a commercial licence. A commercial licence is purchased directly at:
20
+
21
+ https://budrewicz.gumroad.com/l/AgentLedger
22
+
23
+ It is perpetual for the version purchased, covers unlimited developers within
24
+ the purchasing organisation, and does not require a separate negotiated
25
+ agreement.
26
+
27
+ NO WARRANTY
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
33
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
34
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35
+
36
+ COMPLIANCE NOTE
37
+
38
+ This software helps you keep a tamper-evident record of what an AI system did.
39
+ It is not legal advice and it does not by itself make any system compliant with
40
+ any regulation.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: phb-agentledger
3
+ Version: 0.1.0
4
+ Summary: A tamper-evident, hash-chained audit ledger for AI agent actions.
5
+ Project-URL: Homepage, https://budrewicz.gumroad.com/l/AgentLedger
6
+ Project-URL: Commercial licence, https://budrewicz.gumroad.com/l/AgentLedger
7
+ Author: PHB Studio
8
+ License-Expression: LicenseRef-PHB-Studio-Commercial
9
+ License-File: LICENSE
10
+ Keywords: ai-agents,audit,compliance,ledger,provenance,tamper-evident
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Security
15
+ Classifier: Topic :: System :: Logging
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: filelock>=3.12
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0; extra == 'dev'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # agentledger
23
+
24
+ A tamper-evident, hash-chained audit ledger for AI agent actions.
25
+
26
+ When an AI system does something consequential — calls a tool, spends money,
27
+ escalates to a person, refuses a request — you need a record that still means
28
+ something six months later, when someone asks what happened and why. An
29
+ application log does not survive that question: anyone with file access can
30
+ edit it, and nothing in the file says whether they did.
31
+
32
+ `agentledger` writes each action as a validated, content-addressed record in a
33
+ SHA-256 hash chain. Editing any stored record, in any field, breaks the chain,
34
+ and `verify_chain()` says so.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install phb-agentledger
40
+ ```
41
+
42
+ Installed as `phb-agentledger`, imported as `agentledger`. One runtime
43
+ dependency (`filelock`). Python 3.10+.
44
+
45
+ ## Sixty seconds
46
+
47
+ ```python
48
+ from agentledger import AuditLedger, make_event
49
+
50
+ ledger = AuditLedger(path="audit.jsonl")
51
+
52
+ ledger.append_event(make_event(
53
+ "TOOL_CALL", "ACME-AGENT-001",
54
+ summary="looked up order 1234",
55
+ subject_refs=["order:1234"],
56
+ ))
57
+ ledger.append_event(make_event(
58
+ "POLICY_DECISION", "ACME-GATE-001",
59
+ summary="refund above the auto-approve limit: escalated to a human",
60
+ subject_refs=["order:1234"],
61
+ ))
62
+
63
+ ledger.verify_chain() # True
64
+ ledger.query(subject_refs=["order:1234"]) # an AuditTrace of both events
65
+ ```
66
+
67
+ Run `python demo.py` to see the same thing end to end, including a hand-edit of
68
+ the stored file and the chain check catching it.
69
+
70
+ ## What it guarantees
71
+
72
+ - **Order is assigned by the ledger, never by the caller.** Sequence numbers
73
+ and chain hashes are computed on append. A producer cannot claim a position
74
+ in history.
75
+ - **Every record carries its own digest**, SHA-256 over JCS-1 canonical JSON
76
+ with the digest field omitted. A record that does not match its own content
77
+ is rejected before it is stored.
78
+ - **Appends are idempotent** on `(record_id, event_id)`. Resubmitting the same
79
+ record is a no-op; resubmitting different content under the same key is a
80
+ rejected conflict, not a silent overwrite.
81
+ - **Queries never overclaim.** A result is explicitly scoped to a ledger cut,
82
+ so "no matches in this view" is never returned as "this never happened".
83
+ - **Concurrent writers are safe.** The load → mint → append critical section
84
+ is held under a cross-process file lock, and a torn tail from a killed
85
+ process is quarantined rather than silently truncating history.
86
+ - **Two refusals are enforced in code, not documentation.** The ledger will
87
+ not store records classified as raw internal model reasoning, and it rejects
88
+ any event that asserts externally-verified truth without evidence lineage.
89
+
90
+ ## What it deliberately does not do
91
+
92
+ - **It does not verify payloads it never saw.** `payload_digest` is your
93
+ assertion about content held elsewhere; the ledger stores and chains it, but
94
+ cannot confirm it.
95
+ - **It is not a signing system.** Records are tamper-*evident* against edits to
96
+ the stored file. They are not signed, so a party who can rewrite the whole
97
+ file, including recomputing the chain, is out of scope. Signing is the next
98
+ layer, not this one.
99
+ - **It does not make you compliant with anything.** It gives you a defensible
100
+ record. Whether that record satisfies a given regulation is a question for
101
+ your counsel.
102
+ - **`SCHEMA_HASH_CATALOG` ships with a placeholder hash** for AuditEvent 1.0.0
103
+ rather than the digest of the shipped schema file. Set it from your deployed
104
+ schemas at boot if you want that check to be meaningful.
105
+
106
+ ## Event shape
107
+
108
+ `make_event()` fills the 22-field envelope for you and computes both digests.
109
+ The fields you supply are the ones that carry meaning:
110
+
111
+ | Argument | Meaning |
112
+ |---|---|
113
+ | `event_type` | What happened, as a `SCREAMING_SNAKE` label you choose |
114
+ | `actor` | Who did it, as `VENDOR-COMPONENT-NNN` (e.g. `ACME-AGENT-001`) |
115
+ | `summary` *or* `payload_ref` | A short line, or a pointer to the payload held elsewhere |
116
+ | `subject_refs` | What it was about (`order:1234`, `user:42`) |
117
+ | `data_classification` | Sensitivity label; drives the refusals above |
118
+ | `truth_refs` | Evidence lineage, required for external-truth claims |
119
+
120
+ Hand-built dicts are still accepted — `make_event` is a convenience, not a
121
+ bypass. `validate_audit_event()` judges both the same way.
122
+
123
+ ## Tests
124
+
125
+ ```bash
126
+ pip install -e ".[dev]"
127
+ python -m pytest tests -q
128
+ ```
129
+
130
+ 43 tests, no network, under a second.
131
+
132
+ ## Licence
133
+
134
+ Source-available. Free for personal, educational, and evaluation use;
135
+ commercial use requires a paid licence. See [LICENSE](LICENSE).
@@ -0,0 +1,114 @@
1
+ # agentledger
2
+
3
+ A tamper-evident, hash-chained audit ledger for AI agent actions.
4
+
5
+ When an AI system does something consequential — calls a tool, spends money,
6
+ escalates to a person, refuses a request — you need a record that still means
7
+ something six months later, when someone asks what happened and why. An
8
+ application log does not survive that question: anyone with file access can
9
+ edit it, and nothing in the file says whether they did.
10
+
11
+ `agentledger` writes each action as a validated, content-addressed record in a
12
+ SHA-256 hash chain. Editing any stored record, in any field, breaks the chain,
13
+ and `verify_chain()` says so.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install phb-agentledger
19
+ ```
20
+
21
+ Installed as `phb-agentledger`, imported as `agentledger`. One runtime
22
+ dependency (`filelock`). Python 3.10+.
23
+
24
+ ## Sixty seconds
25
+
26
+ ```python
27
+ from agentledger import AuditLedger, make_event
28
+
29
+ ledger = AuditLedger(path="audit.jsonl")
30
+
31
+ ledger.append_event(make_event(
32
+ "TOOL_CALL", "ACME-AGENT-001",
33
+ summary="looked up order 1234",
34
+ subject_refs=["order:1234"],
35
+ ))
36
+ ledger.append_event(make_event(
37
+ "POLICY_DECISION", "ACME-GATE-001",
38
+ summary="refund above the auto-approve limit: escalated to a human",
39
+ subject_refs=["order:1234"],
40
+ ))
41
+
42
+ ledger.verify_chain() # True
43
+ ledger.query(subject_refs=["order:1234"]) # an AuditTrace of both events
44
+ ```
45
+
46
+ Run `python demo.py` to see the same thing end to end, including a hand-edit of
47
+ the stored file and the chain check catching it.
48
+
49
+ ## What it guarantees
50
+
51
+ - **Order is assigned by the ledger, never by the caller.** Sequence numbers
52
+ and chain hashes are computed on append. A producer cannot claim a position
53
+ in history.
54
+ - **Every record carries its own digest**, SHA-256 over JCS-1 canonical JSON
55
+ with the digest field omitted. A record that does not match its own content
56
+ is rejected before it is stored.
57
+ - **Appends are idempotent** on `(record_id, event_id)`. Resubmitting the same
58
+ record is a no-op; resubmitting different content under the same key is a
59
+ rejected conflict, not a silent overwrite.
60
+ - **Queries never overclaim.** A result is explicitly scoped to a ledger cut,
61
+ so "no matches in this view" is never returned as "this never happened".
62
+ - **Concurrent writers are safe.** The load → mint → append critical section
63
+ is held under a cross-process file lock, and a torn tail from a killed
64
+ process is quarantined rather than silently truncating history.
65
+ - **Two refusals are enforced in code, not documentation.** The ledger will
66
+ not store records classified as raw internal model reasoning, and it rejects
67
+ any event that asserts externally-verified truth without evidence lineage.
68
+
69
+ ## What it deliberately does not do
70
+
71
+ - **It does not verify payloads it never saw.** `payload_digest` is your
72
+ assertion about content held elsewhere; the ledger stores and chains it, but
73
+ cannot confirm it.
74
+ - **It is not a signing system.** Records are tamper-*evident* against edits to
75
+ the stored file. They are not signed, so a party who can rewrite the whole
76
+ file, including recomputing the chain, is out of scope. Signing is the next
77
+ layer, not this one.
78
+ - **It does not make you compliant with anything.** It gives you a defensible
79
+ record. Whether that record satisfies a given regulation is a question for
80
+ your counsel.
81
+ - **`SCHEMA_HASH_CATALOG` ships with a placeholder hash** for AuditEvent 1.0.0
82
+ rather than the digest of the shipped schema file. Set it from your deployed
83
+ schemas at boot if you want that check to be meaningful.
84
+
85
+ ## Event shape
86
+
87
+ `make_event()` fills the 22-field envelope for you and computes both digests.
88
+ The fields you supply are the ones that carry meaning:
89
+
90
+ | Argument | Meaning |
91
+ |---|---|
92
+ | `event_type` | What happened, as a `SCREAMING_SNAKE` label you choose |
93
+ | `actor` | Who did it, as `VENDOR-COMPONENT-NNN` (e.g. `ACME-AGENT-001`) |
94
+ | `summary` *or* `payload_ref` | A short line, or a pointer to the payload held elsewhere |
95
+ | `subject_refs` | What it was about (`order:1234`, `user:42`) |
96
+ | `data_classification` | Sensitivity label; drives the refusals above |
97
+ | `truth_refs` | Evidence lineage, required for external-truth claims |
98
+
99
+ Hand-built dicts are still accepted — `make_event` is a convenience, not a
100
+ bypass. `validate_audit_event()` judges both the same way.
101
+
102
+ ## Tests
103
+
104
+ ```bash
105
+ pip install -e ".[dev]"
106
+ python -m pytest tests -q
107
+ ```
108
+
109
+ 43 tests, no network, under a second.
110
+
111
+ ## Licence
112
+
113
+ Source-available. Free for personal, educational, and evaluation use;
114
+ commercial use requires a paid licence. See [LICENSE](LICENSE).
@@ -0,0 +1,15 @@
1
+ from .canonical import compute_record_digest, verify_record_digest
2
+ from .events import make_event
3
+ from .ledger import AuditLedger, AuditEventRejected, LedgerEntry
4
+ from .validation import validate_audit_event, ValidationResult
5
+
6
+ __all__ = [
7
+ "AuditLedger",
8
+ "make_event",
9
+ "AuditEventRejected",
10
+ "LedgerEntry",
11
+ "validate_audit_event",
12
+ "ValidationResult",
13
+ "compute_record_digest",
14
+ "verify_record_digest",
15
+ ]
@@ -0,0 +1,43 @@
1
+ """
2
+ Canonical JSON serialization and digest helpers.
3
+
4
+ The RecordEnvelope contract (urn:neo:contract:RecordEnvelope:1.0.0) requires every
5
+ record to be content-addressable: ``record_digest`` must equal SHA-256 over the
6
+ record's JCS-1 canonical JSON with ``record_digest`` itself omitted.
7
+
8
+ This module implements a canonicalization profile sufficient for records that
9
+ never contain floats (Neo's schemas only use strings, ints, bools, null, arrays
10
+ and objects), which covers every contract in this pack. It intentionally does
11
+ NOT depend on any third-party library.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import json
17
+ from typing import Any, Mapping
18
+
19
+
20
+ def canonicalize(value: Any) -> str:
21
+ """Serialize ``value`` to canonical JSON text (sorted keys, no insignificant
22
+ whitespace, UTF-8 safe)."""
23
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
24
+
25
+
26
+ def sha256_hex(text: str) -> str:
27
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
28
+
29
+
30
+ def compute_record_digest(record: Mapping[str, Any]) -> str:
31
+ """Compute the record_digest a well-formed record MUST carry: SHA-256 of the
32
+ canonical JSON of the record with ``record_digest`` omitted."""
33
+ stripped = {k: v for k, v in record.items() if k != "record_digest"}
34
+ return sha256_hex(canonicalize(stripped))
35
+
36
+
37
+ def verify_record_digest(record: Mapping[str, Any]) -> bool:
38
+ """True iff record['record_digest'] matches the value computed from the rest
39
+ of the record."""
40
+ claimed = record.get("record_digest")
41
+ if not isinstance(claimed, str):
42
+ return False
43
+ return claimed == compute_record_digest(record)
@@ -0,0 +1,113 @@
1
+ """
2
+ Build a valid AuditEvent without hand-assembling the envelope.
3
+
4
+ The `AuditEvent` contract this ledger enforces is deliberately strict: 22
5
+ required fields, a closed shape, two content digests and a canonicalisation
6
+ profile. That strictness is the point — it is what makes a stored record
7
+ independently checkable months later — but it makes the first five minutes
8
+ with the library harder than they need to be.
9
+
10
+ `make_event()` fills in everything that can be derived (the contract
11
+ constants, the timestamps, the identifiers, the schema hash, and both
12
+ digests) and leaves the caller with the handful of fields that actually carry
13
+ meaning: what happened, who did it, what it was about. The result is a plain
14
+ dict that `AuditLedger.append_event()` accepts, and that `validate_audit_event`
15
+ independently agrees is well-formed — this helper is a convenience, never a
16
+ bypass.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import uuid
21
+ from datetime import datetime, timezone
22
+ from typing import Any, Iterable, Mapping, Optional, Sequence
23
+
24
+ from .canonical import compute_record_digest, sha256_hex
25
+
26
+ # Imported lazily-by-name to keep the module graph one-directional
27
+ # (events -> ledger is fine; ledger never imports events).
28
+ from .ledger import SCHEMA_HASH_CATALOG
29
+
30
+ __all__ = ["make_event"]
31
+
32
+
33
+ def _now_iso() -> str:
34
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
35
+
36
+
37
+ def _new_id(prefix: str) -> str:
38
+ # record_id / event_id must be 8..128 chars; a prefixed uuid4 hex is 37.
39
+ return f"{prefix}_{uuid.uuid4().hex}"
40
+
41
+
42
+ def make_event(
43
+ event_type: str,
44
+ actor: str,
45
+ *,
46
+ summary: Optional[str] = None,
47
+ payload_ref: Optional[str] = None,
48
+ subject_refs: Sequence[str] = (),
49
+ truth_refs: Sequence[str] = (),
50
+ authority_refs: Sequence[str] = (),
51
+ data_classification: str = "OWNER_PRIVATE",
52
+ producer: Optional[str] = None,
53
+ correlation_ids: Iterable[Mapping[str, str]] = (),
54
+ provenance: Iterable[Mapping[str, str]] = (),
55
+ payload_digest: Optional[str] = None,
56
+ occurred_at: Optional[str] = None,
57
+ clock_quality: str = "SYNCHRONIZED",
58
+ record_id: Optional[str] = None,
59
+ event_id: Optional[str] = None,
60
+ created_at: Optional[str] = None,
61
+ ) -> dict[str, Any]:
62
+ """Return a complete, digest-correct AuditEvent dict.
63
+
64
+ `event_type` and `data_classification` are SCREAMING_SNAKE labels you
65
+ choose (e.g. `TOOL_CALL`, `POLICY_DECISION`). `actor` is the component
66
+ that did the thing, as `VENDOR-COMPONENT-NNN` (e.g. `ACME-AGENT-001`);
67
+ `producer` is the component writing the record, and defaults to `actor`.
68
+
69
+ Exactly one of `summary` or `payload_ref` must be given: a short
70
+ human-readable line, or a pointer to the payload held elsewhere. The
71
+ ledger stores the pointer or the summary — never a payload it was not
72
+ given ownership of.
73
+
74
+ `payload_digest` should be the SHA-256 of the real payload when you have
75
+ it. When omitted it is derived from the summary or ref, which keeps the
76
+ record structurally valid and self-consistent but says nothing about a
77
+ payload the ledger never saw.
78
+ """
79
+ if (summary is None) == (payload_ref is None):
80
+ raise ValueError("make_event: pass exactly one of summary= or payload_ref=")
81
+
82
+ payload = {"summary": summary} if summary is not None else {"payload_ref": payload_ref}
83
+ now = created_at or _now_iso()
84
+
85
+ record: dict[str, Any] = {
86
+ "contract_name": "AuditEvent",
87
+ "contract_version": "1.0.0",
88
+ "record_id": record_id or _new_id("record"),
89
+ "created_at": now,
90
+ "producer_module_id": producer or actor,
91
+ "correlation_ids": [dict(c) for c in correlation_ids],
92
+ "provenance": [dict(p) for p in provenance],
93
+ "schema_hash": SCHEMA_HASH_CATALOG[("AuditEvent", "1.0.0")],
94
+ "digest_algorithm": "SHA-256",
95
+ "canonicalization_profile": "JCS-1",
96
+ "event_id": event_id or _new_id("event"),
97
+ "event_schema_version": "1.0.0",
98
+ "event_type": event_type,
99
+ "actor_module": actor,
100
+ "subject_refs": list(subject_refs),
101
+ "authority_refs_if_relevant": list(authority_refs),
102
+ "truth_refs_if_relevant": list(truth_refs),
103
+ "payload_ref_or_summary": payload,
104
+ "payload_digest": payload_digest or sha256_hex(summary if summary is not None else payload_ref),
105
+ "data_classification": data_classification,
106
+ "producer_emitted_at": now,
107
+ "producer_clock_quality": clock_quality,
108
+ }
109
+ if occurred_at is not None:
110
+ record["producer_occurred_at_if_known"] = occurred_at
111
+
112
+ record["record_digest"] = compute_record_digest(record)
113
+ return record