omega-audit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ __pycache__/
5
+ *.pyc
6
+ .pytest_cache/
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to the Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2024-2026 Jesse Mahoney
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: omega-audit
3
+ Version: 0.1.0
4
+ Summary: Tamper-evident audit + provenance for AI agents — an offline, zero-dependency, hash-chained append-only ledger. Wrap your agent's tool calls, then prove what it did: any edit, reorder, insertion, or deletion fails verification.
5
+ Project-URL: Homepage, https://omegaengine.ai
6
+ Project-URL: Documentation, https://github.com/TheArkhitect/Omegaengine/tree/main/packages/omega-audit-py#readme
7
+ Project-URL: Repository, https://github.com/TheArkhitect/Omegaengine/tree/main/packages/omega-audit-py
8
+ Project-URL: Issues, https://github.com/TheArkhitect/Omegaengine/issues
9
+ Author-email: OmegaEngine <team@omegaengine.ai>
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: ai-agents,append-only,audit-log,compliance,eu-ai-act,hash-chain,omegaengine,provenance,sha256,tamper-evident
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Topic :: Security
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Requires-Python: >=3.9
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # omega-audit
31
+
32
+ **Tamper-evident audit + provenance for AI agents — prove what your agent did, hash-chained, verifiable, auditor-ready.**
33
+
34
+ Wrap your agent's tool calls. Every call is appended to a local, append-only, SHA-256 hash-chained ledger. Later, anyone can recompute the chain and detect *any* edit, reorder, insertion, or deletion — offline, in one command, with no signup and no trust in us.
35
+
36
+ - **Zero runtime dependencies.** Python's standard library only (`hashlib`, `json`, `base64`). No SDK, no account, no network.
37
+ - **Offline and local.** The ledger is a plain JSONL file on your disk. You own it.
38
+ - **Framework-agnostic.** Wrap any callable — sync or async — plain tools, OpenAI/LangChain/LlamaIndex tool defs, MCP handlers.
39
+ - **Independently verifiable.** `omega-audit verify` recomputes the whole chain. Break one byte and it fails.
40
+ - **Byte-compatible with the TypeScript twin** [`@omegaengine/audit`](https://github.com/TheArkhitect/Omegaengine/tree/main/packages/omega-audit): a ledger written in Python verifies under the TS CLI and vice versa — same bytes, same hashes.
41
+ - **Apache-2.0.**
42
+
43
+ ---
44
+
45
+ ## Why now
46
+
47
+ The **EU AI Act's record-keeping obligation (Article 12) begins to apply on August 2, 2026** for high-risk AI systems: providers must ensure their systems automatically record events (logs) over their lifetime, to a degree appropriate to the system's purpose. A policy document that says "we log agent actions" is not evidence. **Evidence is a log you can hand an auditor that provably has not been altered.**
48
+
49
+ Append-only, cryptographically hash-chained logs are the well-established way to make an audit trail tamper-evident: each record commits to the one before it, so the chain itself proves nothing was quietly changed after the fact. That's the same construction behind Git commit history and RFC 6962 Certificate Transparency. This package gives you that construction for your agent's actions, in about twenty minutes.
50
+
51
+ > This is honest scope: `omega-audit` produces **tamper-evident** local logs — you can *detect* tampering by recomputing the chain. It is not, by itself, a certification, a signature from us, or proof to a third party who doesn't have your file. For cross-organization, independently-anchored proof, see [Anchoring to the transparency log](#anchor-to-the-transparency-log-optional) below.
52
+
53
+ ---
54
+
55
+ ## 20-minute quickstart
56
+
57
+ ### 1. Install
58
+
59
+ ```bash
60
+ pip install omega-audit
61
+ ```
62
+
63
+ ### 2. Wrap a tool
64
+
65
+ ```python
66
+ from omega_audit import Ledger, wrap_tool
67
+
68
+ # One local, append-only ledger file. Offline — no signup, no network.
69
+ ledger = Ledger("ledger.jsonl")
70
+
71
+ # Your existing tool — any callable, sync or async.
72
+ def send_email(to: str, subject: str):
73
+ # ... your real implementation ...
74
+ return {"delivered": True}
75
+
76
+ # Wrap it. Same signature, same return value — it just records every call.
77
+ tracked_send_email = wrap_tool(ledger, "send_email", send_email)
78
+
79
+ tracked_send_email("auditor@example.com", "Q3 evidence")
80
+ ```
81
+
82
+ Each call appends one record: `{tool, args, ok, result | error, duration_ms}`. Errors are recorded too (with `ok=False` and the message) and then re-raised unchanged, so wrapping never swallows a failure.
83
+
84
+ Async tools work the same way — if the wrapped function is a coroutine, the wrapper is too:
85
+
86
+ ```python
87
+ import asyncio
88
+ from omega_audit import Ledger, wrap_tool
89
+
90
+ ledger = Ledger("ledger.jsonl")
91
+
92
+ async def fetch_row(row_id: str):
93
+ # ... await your real async implementation ...
94
+ return {"id": row_id}
95
+
96
+ tracked = wrap_tool(ledger, "fetch_row", fetch_row)
97
+ asyncio.run(tracked("abc")) # the record is written when the awaited call settles
98
+ ```
99
+
100
+ This composes with agent frameworks: an OpenAI or LangChain tool is just a callable with a schema, so wrap its implementation and register the wrapped version. Nothing about your agent loop changes.
101
+
102
+ ### 3. Look at the ledger
103
+
104
+ `ledger.jsonl` is one JSON object per line — readable, greppable, diffable:
105
+
106
+ ```json
107
+ {"seq":0,"ts":"2026-08-02T09:00:00.000Z","action":{"tool":"send_email","args":["auditor@example.com","Q3 evidence"],"ok":true,"result":{"delivered":true},"durationMs":501},"prevHash":"0000000000000000000000000000000000000000000000000000000000000000","hash":"44dab7b4…"}
108
+ ```
109
+
110
+ Every record's `hash` is `sha256(canonical_json({seq, ts, action, prevHash}))`, and its `prevHash` is the previous record's `hash` (the first record links to 64 zeros). That chain is what makes tampering detectable. The field is stored on disk as `prevHash` (camelCase) so the bytes are identical to the TypeScript twin.
111
+
112
+ ### 4. Verify it
113
+
114
+ ```bash
115
+ omega-audit verify ledger.jsonl
116
+ ```
117
+
118
+ ```
119
+ ✓ 3 records — chain intact & tamper-evident
120
+ ```
121
+
122
+ Exit code `0` when intact, `1` when tampered. Drop it in CI to fail a build if an agent's audit trail was touched.
123
+
124
+ You can also verify in code:
125
+
126
+ ```python
127
+ from omega_audit import verify
128
+
129
+ result = verify("ledger.jsonl")
130
+ # VerifyResult(valid=True, records=3, broken_at=None, reason=None)
131
+ # or VerifyResult(valid=False, records=3, broken_at=2, reason="record 2 was edited — …")
132
+ ```
133
+
134
+ ---
135
+
136
+ ## See it break: the tamper demo
137
+
138
+ One command builds a small ledger, tampers a record, and shows verification catch it:
139
+
140
+ ```bash
141
+ omega-audit demo
142
+ ```
143
+
144
+ ```
145
+ omega-audit demo — tamper-evident agent audit in one command
146
+
147
+ 1. An agent runs three tool calls. Each one is appended to a hash-chained ledger:
148
+
149
+ #0 search_web hash=ce4ca688b665… prev=000000000000…
150
+ #1 read_file hash=5c7f4ae5891f… prev=ce4ca688b665…
151
+ #2 send_email hash=a0c640028849… prev=5c7f4ae5891f…
152
+
153
+ 2. Verify the untouched ledger:
154
+
155
+ $ omega-audit verify ledger.jsonl
156
+ ✓ 3 records — chain intact & tamper-evident
157
+
158
+ 3. Now someone quietly edits record #2 to hide who the email went to:
159
+
160
+ before: send_email → auditor@example.com
161
+ after: send_email → attacker@evil.example (hash left unchanged to hide the edit)
162
+
163
+ 4. Verify again — the chain no longer recomputes:
164
+
165
+ $ omega-audit verify ledger.jsonl
166
+ ✗ TAMPER DETECTED at record 2: record 2 was edited — its stored hash a0c64002…30cc ≠ the recomputed hash 6e1be9ab…e0ef
167
+
168
+ That ✗ is the whole point: the record was changed, and verification proved it.
169
+ Nothing here touched the network. You can run the same check on your own ledger.
170
+ ```
171
+
172
+ The `demo` command exits `1` — its whole job is to prove that a tampered ledger fails verification. (The hashes vary run to run because each record's timestamp is real; the chain is self-consistent every time.)
173
+
174
+ ---
175
+
176
+ ## Enforce vs. prove
177
+
178
+ These are two different jobs, and you want both:
179
+
180
+ - **A policy layer *enforces*** — it decides, at run time, whether an action is allowed and blocks the ones that aren't.
181
+ - **`omega-audit` *proves*** — it produces the independent, tamper-evident evidence trail of what actually happened, which you can verify after the fact.
182
+
183
+ Enforcement without evidence is a claim. Evidence without enforcement is a bystander. Run your guardrails to stop bad actions; run `omega-audit` to prove — to yourself, to an auditor, to a customer — exactly what your agent did and that the record wasn't altered.
184
+
185
+ ---
186
+
187
+ ## Anchor to the transparency log (optional)
188
+
189
+ Your local ledger is tamper-evident to *anyone who has the file*. To make it verifiable *across organizations* — so a customer or auditor who does **not** have your machine can still confirm the record — you anchor the ledger's root to a public, append-only transparency log.
190
+
191
+ The record hashing here is byte-compatible with the OmegaEngine platform's canonical JSON + SHA-256 **and with the TypeScript twin** — a ledger written by this Python package verifies under `@omegaengine/audit` and vice versa, so it is already in the shape the hosted log expects. [OmegaEngine](https://omegaengine.ai) offers, as a hosted backend:
192
+
193
+ - **Anchoring** your ledger root into a public RFC 6962 transparency log for cross-org, independently-checkable proof of inclusion.
194
+ - **Long-retention storage** to support multi-year record-keeping regimes (for example SEC Rule 17a-4 or HIPAA retention).
195
+ - **Auditor exports** — signed, verifiable bundles an auditor can check with the open-source [`@omegaengine/verify`](https://github.com/TheArkhitect/Omegaengine/tree/main/packages/omega-verify) tool, no OmegaEngine account required.
196
+
197
+ The local package is fully useful on its own and stays free and open source. The hosted log is the optional upgrade when "verifiable on my disk" needs to become "verifiable by someone else."
198
+
199
+ ---
200
+
201
+ ## API
202
+
203
+ ```python
204
+ Ledger(path: str)
205
+ ledger.append(action) -> dict # append one record; returns it
206
+ ledger.size() -> int
207
+ ledger.tip_hash() -> str # hash of the last record (chain tip)
208
+
209
+ wrap_tool(ledger, name, fn) -> fn # wrap a named tool (sync or async); records every call
210
+ capture(ledger, fn) -> fn # like wrap_tool, using fn.__name__
211
+
212
+ verify(path: str) -> VerifyResult
213
+ # VerifyResult(valid, records, broken_at, reason)
214
+
215
+ read_ledger(path) -> list[dict] # parse JSONL into records (no chain check)
216
+ verify_entries(entries) -> VerifyResult # verify already-parsed records
217
+ canonical_json(value) -> str # the deterministic serializer used for hashing
218
+ sha256_hex(s) -> str
219
+ ```
220
+
221
+ A ledger entry is a dict `{seq, ts, action, prevHash, hash}`.
222
+
223
+ ---
224
+
225
+ ## Design partners
226
+
227
+ We're looking for teams putting agents in front of real users or real money who need to *prove* what those agents did. If you're wiring agent actions into an audit or compliance story — EU AI Act, financial record-keeping, healthcare — we'd like to build with you. Open an issue on [github.com/TheArkhitect/Omegaengine](https://github.com/TheArkhitect/Omegaengine/discussions) or reach out via [omegaengine.ai](https://omegaengine.ai).
228
+
229
+ ---
230
+
231
+ ## License
232
+
233
+ Apache-2.0. See the repository root for the full text.
@@ -0,0 +1,204 @@
1
+ # omega-audit
2
+
3
+ **Tamper-evident audit + provenance for AI agents — prove what your agent did, hash-chained, verifiable, auditor-ready.**
4
+
5
+ Wrap your agent's tool calls. Every call is appended to a local, append-only, SHA-256 hash-chained ledger. Later, anyone can recompute the chain and detect *any* edit, reorder, insertion, or deletion — offline, in one command, with no signup and no trust in us.
6
+
7
+ - **Zero runtime dependencies.** Python's standard library only (`hashlib`, `json`, `base64`). No SDK, no account, no network.
8
+ - **Offline and local.** The ledger is a plain JSONL file on your disk. You own it.
9
+ - **Framework-agnostic.** Wrap any callable — sync or async — plain tools, OpenAI/LangChain/LlamaIndex tool defs, MCP handlers.
10
+ - **Independently verifiable.** `omega-audit verify` recomputes the whole chain. Break one byte and it fails.
11
+ - **Byte-compatible with the TypeScript twin** [`@omegaengine/audit`](https://github.com/TheArkhitect/Omegaengine/tree/main/packages/omega-audit): a ledger written in Python verifies under the TS CLI and vice versa — same bytes, same hashes.
12
+ - **Apache-2.0.**
13
+
14
+ ---
15
+
16
+ ## Why now
17
+
18
+ The **EU AI Act's record-keeping obligation (Article 12) begins to apply on August 2, 2026** for high-risk AI systems: providers must ensure their systems automatically record events (logs) over their lifetime, to a degree appropriate to the system's purpose. A policy document that says "we log agent actions" is not evidence. **Evidence is a log you can hand an auditor that provably has not been altered.**
19
+
20
+ Append-only, cryptographically hash-chained logs are the well-established way to make an audit trail tamper-evident: each record commits to the one before it, so the chain itself proves nothing was quietly changed after the fact. That's the same construction behind Git commit history and RFC 6962 Certificate Transparency. This package gives you that construction for your agent's actions, in about twenty minutes.
21
+
22
+ > This is honest scope: `omega-audit` produces **tamper-evident** local logs — you can *detect* tampering by recomputing the chain. It is not, by itself, a certification, a signature from us, or proof to a third party who doesn't have your file. For cross-organization, independently-anchored proof, see [Anchoring to the transparency log](#anchor-to-the-transparency-log-optional) below.
23
+
24
+ ---
25
+
26
+ ## 20-minute quickstart
27
+
28
+ ### 1. Install
29
+
30
+ ```bash
31
+ pip install omega-audit
32
+ ```
33
+
34
+ ### 2. Wrap a tool
35
+
36
+ ```python
37
+ from omega_audit import Ledger, wrap_tool
38
+
39
+ # One local, append-only ledger file. Offline — no signup, no network.
40
+ ledger = Ledger("ledger.jsonl")
41
+
42
+ # Your existing tool — any callable, sync or async.
43
+ def send_email(to: str, subject: str):
44
+ # ... your real implementation ...
45
+ return {"delivered": True}
46
+
47
+ # Wrap it. Same signature, same return value — it just records every call.
48
+ tracked_send_email = wrap_tool(ledger, "send_email", send_email)
49
+
50
+ tracked_send_email("auditor@example.com", "Q3 evidence")
51
+ ```
52
+
53
+ Each call appends one record: `{tool, args, ok, result | error, duration_ms}`. Errors are recorded too (with `ok=False` and the message) and then re-raised unchanged, so wrapping never swallows a failure.
54
+
55
+ Async tools work the same way — if the wrapped function is a coroutine, the wrapper is too:
56
+
57
+ ```python
58
+ import asyncio
59
+ from omega_audit import Ledger, wrap_tool
60
+
61
+ ledger = Ledger("ledger.jsonl")
62
+
63
+ async def fetch_row(row_id: str):
64
+ # ... await your real async implementation ...
65
+ return {"id": row_id}
66
+
67
+ tracked = wrap_tool(ledger, "fetch_row", fetch_row)
68
+ asyncio.run(tracked("abc")) # the record is written when the awaited call settles
69
+ ```
70
+
71
+ This composes with agent frameworks: an OpenAI or LangChain tool is just a callable with a schema, so wrap its implementation and register the wrapped version. Nothing about your agent loop changes.
72
+
73
+ ### 3. Look at the ledger
74
+
75
+ `ledger.jsonl` is one JSON object per line — readable, greppable, diffable:
76
+
77
+ ```json
78
+ {"seq":0,"ts":"2026-08-02T09:00:00.000Z","action":{"tool":"send_email","args":["auditor@example.com","Q3 evidence"],"ok":true,"result":{"delivered":true},"durationMs":501},"prevHash":"0000000000000000000000000000000000000000000000000000000000000000","hash":"44dab7b4…"}
79
+ ```
80
+
81
+ Every record's `hash` is `sha256(canonical_json({seq, ts, action, prevHash}))`, and its `prevHash` is the previous record's `hash` (the first record links to 64 zeros). That chain is what makes tampering detectable. The field is stored on disk as `prevHash` (camelCase) so the bytes are identical to the TypeScript twin.
82
+
83
+ ### 4. Verify it
84
+
85
+ ```bash
86
+ omega-audit verify ledger.jsonl
87
+ ```
88
+
89
+ ```
90
+ ✓ 3 records — chain intact & tamper-evident
91
+ ```
92
+
93
+ Exit code `0` when intact, `1` when tampered. Drop it in CI to fail a build if an agent's audit trail was touched.
94
+
95
+ You can also verify in code:
96
+
97
+ ```python
98
+ from omega_audit import verify
99
+
100
+ result = verify("ledger.jsonl")
101
+ # VerifyResult(valid=True, records=3, broken_at=None, reason=None)
102
+ # or VerifyResult(valid=False, records=3, broken_at=2, reason="record 2 was edited — …")
103
+ ```
104
+
105
+ ---
106
+
107
+ ## See it break: the tamper demo
108
+
109
+ One command builds a small ledger, tampers a record, and shows verification catch it:
110
+
111
+ ```bash
112
+ omega-audit demo
113
+ ```
114
+
115
+ ```
116
+ omega-audit demo — tamper-evident agent audit in one command
117
+
118
+ 1. An agent runs three tool calls. Each one is appended to a hash-chained ledger:
119
+
120
+ #0 search_web hash=ce4ca688b665… prev=000000000000…
121
+ #1 read_file hash=5c7f4ae5891f… prev=ce4ca688b665…
122
+ #2 send_email hash=a0c640028849… prev=5c7f4ae5891f…
123
+
124
+ 2. Verify the untouched ledger:
125
+
126
+ $ omega-audit verify ledger.jsonl
127
+ ✓ 3 records — chain intact & tamper-evident
128
+
129
+ 3. Now someone quietly edits record #2 to hide who the email went to:
130
+
131
+ before: send_email → auditor@example.com
132
+ after: send_email → attacker@evil.example (hash left unchanged to hide the edit)
133
+
134
+ 4. Verify again — the chain no longer recomputes:
135
+
136
+ $ omega-audit verify ledger.jsonl
137
+ ✗ TAMPER DETECTED at record 2: record 2 was edited — its stored hash a0c64002…30cc ≠ the recomputed hash 6e1be9ab…e0ef
138
+
139
+ That ✗ is the whole point: the record was changed, and verification proved it.
140
+ Nothing here touched the network. You can run the same check on your own ledger.
141
+ ```
142
+
143
+ The `demo` command exits `1` — its whole job is to prove that a tampered ledger fails verification. (The hashes vary run to run because each record's timestamp is real; the chain is self-consistent every time.)
144
+
145
+ ---
146
+
147
+ ## Enforce vs. prove
148
+
149
+ These are two different jobs, and you want both:
150
+
151
+ - **A policy layer *enforces*** — it decides, at run time, whether an action is allowed and blocks the ones that aren't.
152
+ - **`omega-audit` *proves*** — it produces the independent, tamper-evident evidence trail of what actually happened, which you can verify after the fact.
153
+
154
+ Enforcement without evidence is a claim. Evidence without enforcement is a bystander. Run your guardrails to stop bad actions; run `omega-audit` to prove — to yourself, to an auditor, to a customer — exactly what your agent did and that the record wasn't altered.
155
+
156
+ ---
157
+
158
+ ## Anchor to the transparency log (optional)
159
+
160
+ Your local ledger is tamper-evident to *anyone who has the file*. To make it verifiable *across organizations* — so a customer or auditor who does **not** have your machine can still confirm the record — you anchor the ledger's root to a public, append-only transparency log.
161
+
162
+ The record hashing here is byte-compatible with the OmegaEngine platform's canonical JSON + SHA-256 **and with the TypeScript twin** — a ledger written by this Python package verifies under `@omegaengine/audit` and vice versa, so it is already in the shape the hosted log expects. [OmegaEngine](https://omegaengine.ai) offers, as a hosted backend:
163
+
164
+ - **Anchoring** your ledger root into a public RFC 6962 transparency log for cross-org, independently-checkable proof of inclusion.
165
+ - **Long-retention storage** to support multi-year record-keeping regimes (for example SEC Rule 17a-4 or HIPAA retention).
166
+ - **Auditor exports** — signed, verifiable bundles an auditor can check with the open-source [`@omegaengine/verify`](https://github.com/TheArkhitect/Omegaengine/tree/main/packages/omega-verify) tool, no OmegaEngine account required.
167
+
168
+ The local package is fully useful on its own and stays free and open source. The hosted log is the optional upgrade when "verifiable on my disk" needs to become "verifiable by someone else."
169
+
170
+ ---
171
+
172
+ ## API
173
+
174
+ ```python
175
+ Ledger(path: str)
176
+ ledger.append(action) -> dict # append one record; returns it
177
+ ledger.size() -> int
178
+ ledger.tip_hash() -> str # hash of the last record (chain tip)
179
+
180
+ wrap_tool(ledger, name, fn) -> fn # wrap a named tool (sync or async); records every call
181
+ capture(ledger, fn) -> fn # like wrap_tool, using fn.__name__
182
+
183
+ verify(path: str) -> VerifyResult
184
+ # VerifyResult(valid, records, broken_at, reason)
185
+
186
+ read_ledger(path) -> list[dict] # parse JSONL into records (no chain check)
187
+ verify_entries(entries) -> VerifyResult # verify already-parsed records
188
+ canonical_json(value) -> str # the deterministic serializer used for hashing
189
+ sha256_hex(s) -> str
190
+ ```
191
+
192
+ A ledger entry is a dict `{seq, ts, action, prevHash, hash}`.
193
+
194
+ ---
195
+
196
+ ## Design partners
197
+
198
+ We're looking for teams putting agents in front of real users or real money who need to *prove* what those agents did. If you're wiring agent actions into an audit or compliance story — EU AI Act, financial record-keeping, healthcare — we'd like to build with you. Open an issue on [github.com/TheArkhitect/Omegaengine](https://github.com/TheArkhitect/Omegaengine/discussions) or reach out via [omegaengine.ai](https://omegaengine.ai).
199
+
200
+ ---
201
+
202
+ ## License
203
+
204
+ Apache-2.0. See the repository root for the full text.
@@ -0,0 +1,56 @@
1
+ """omega-audit — tamper-evident audit + provenance for AI agents (Python twin).
2
+
3
+ A local, offline, append-only, hash-chained ledger: wrap your agent's tool
4
+ calls, then PROVE what it did. Any edit, reorder, insertion, or deletion of a
5
+ record breaks the chain and fails verification — you confirm it yourself, with
6
+ no network, no signup, no trust in OmegaEngine.
7
+
8
+ Zero runtime dependencies: only Python's stdlib (``hashlib``, ``json``, ``base64``,
9
+ ``datetime``, ``time``).
10
+
11
+ The record hashing is BYTE-IDENTICAL to the OmegaEngine platform's canonical
12
+ JSON + SHA-256 and to the TypeScript twin ``@omegaengine/audit``. A ledger you
13
+ capture locally in Python verifies against a TS one and can be anchored,
14
+ unchanged, to the hosted transparency log for cross-org verifiability.
15
+
16
+ Public API (mirrors the TS twin 1:1, Pythonic names):
17
+
18
+ Ledger(path) .append(action) -> dict, .size(), .tip_hash()
19
+ wrap_tool(ledger, name, fn) wrap a named tool (sync or async)
20
+ capture(ledger, fn) like wrap_tool, using fn.__name__
21
+ verify(path) -> VerifyResult
22
+ verify_entries(entries) -> VerifyResult
23
+ read_ledger(path) -> list[dict]
24
+ canonical_json(value) -> str
25
+ sha256_hex(s) -> str
26
+ GENESIS_PREV_HASH
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from .canonical import (
32
+ GENESIS_PREV_HASH,
33
+ canonical_json,
34
+ hash_entry,
35
+ sha256_hex,
36
+ )
37
+ from .ledger import Ledger, read_ledger
38
+ from .verify import VerifyResult, verify, verify_entries
39
+ from .wrap import capture, wrap_tool
40
+
41
+ __version__ = "0.1.0"
42
+
43
+ __all__ = [
44
+ "Ledger",
45
+ "read_ledger",
46
+ "verify",
47
+ "verify_entries",
48
+ "VerifyResult",
49
+ "wrap_tool",
50
+ "capture",
51
+ "canonical_json",
52
+ "sha256_hex",
53
+ "hash_entry",
54
+ "GENESIS_PREV_HASH",
55
+ "__version__",
56
+ ]
@@ -0,0 +1,8 @@
1
+ """``python -m omega_audit`` — same entry point as the ``omega-audit`` console script."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,137 @@
1
+ """Canonical JSON + SHA-256 — byte-identical to the TypeScript twin.
2
+
3
+ This is the load-bearing compatibility layer. The record ``hash`` is
4
+ ``sha256(canonical_json({seq, ts, action, prev_hash}))``, and it MUST produce
5
+ the exact same bytes as:
6
+
7
+ * the OmegaEngine platform (``lib/crypto/canonicalJson.ts``),
8
+ * ``@omegaengine/verify``,
9
+ * the TypeScript twin ``@omegaengine/audit`` (``packages/omega-audit/src/index.ts``).
10
+
11
+ If it does not, a Python-captured ledger will not verify against a TS one and
12
+ cannot be anchored to the hosted transparency log — which is the whole point of
13
+ the cross-language design.
14
+
15
+ Canonicalization rules (mirroring the TS ``canonicalJson``):
16
+
17
+ * objects: keys sorted lexicographically, recursively;
18
+ * arrays: order preserved;
19
+ * ``None`` -> ``null`` (kept);
20
+ * ``undefined`` has no Python analogue — object keys whose value normalizes to
21
+ "drop" are omitted (see below); inside arrays a dropped element becomes null,
22
+ matching ``JSON.stringify``;
23
+ * bool: kept (``True`` -> ``true``); note bools are checked BEFORE ints because
24
+ ``isinstance(True, int)`` is ``True`` in Python;
25
+ * numbers: non-finite (NaN/inf) -> ``null`` (matches ``JSON.stringify``);
26
+ * bytes / bytearray -> base64 (matches Buffer/Uint8Array -> base64);
27
+ * strings kept verbatim (non-ASCII NOT escaped, matching ``ensure_ascii=False``).
28
+
29
+ Zero runtime dependencies: only ``hashlib``, ``json``, ``base64`` from stdlib.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import base64
35
+ import hashlib
36
+ import json
37
+ import math
38
+ from typing import Any
39
+
40
+ # The genesis link: 64 hex zeros (the "prev_hash" of the very first record).
41
+ GENESIS_PREV_HASH = "0" * 64
42
+
43
+ # Sentinel meaning "this value has no JSON representation and should be dropped
44
+ # from an object" — the Python analogue of JavaScript's `undefined`. It is never
45
+ # emitted; in arrays it is replaced by null, exactly like `JSON.stringify`.
46
+ _DROP = object()
47
+
48
+
49
+ def _normalize(value: Any, seen: set[int]) -> Any:
50
+ """Recursively normalize a value into JSON-canonical primitives.
51
+
52
+ Returns the sentinel ``_DROP`` for values that ``JSON.stringify`` would omit
53
+ from an object (functions/undefined have no Python equivalent, so the only
54
+ droppable case here is a plain, unserializable object — we raise on those
55
+ instead to fail loud rather than silently lose data).
56
+ """
57
+ if value is None:
58
+ return None
59
+ # bool BEFORE int: isinstance(True, int) is True in Python.
60
+ if isinstance(value, bool):
61
+ return value
62
+ if isinstance(value, str):
63
+ return value
64
+ if isinstance(value, int):
65
+ return value
66
+ if isinstance(value, float):
67
+ # JSON.stringify turns NaN/Infinity into null; match that.
68
+ return value if math.isfinite(value) else None
69
+ if isinstance(value, (bytes, bytearray)):
70
+ # Buffer/Uint8Array -> base64 in the TS twin.
71
+ return base64.b64encode(bytes(value)).decode("ascii")
72
+ if isinstance(value, (list, tuple)):
73
+ out = []
74
+ for item in value:
75
+ n = _normalize(item, seen)
76
+ # In arrays, JSON.stringify turns undefined/dropped into null.
77
+ out.append(None if n is _DROP else n)
78
+ return out
79
+ if isinstance(value, dict):
80
+ obj_id = id(value)
81
+ if obj_id in seen:
82
+ raise ValueError("[canonical_json] cycle detected")
83
+ seen.add(obj_id)
84
+ out_obj: dict[str, Any] = {}
85
+ # Sort keys lexicographically. Keys must be strings for JSON; coerce
86
+ # non-str keys via str() the way json.dumps(sort_keys=True) would reject,
87
+ # so we normalize to str first to keep sorting well-defined.
88
+ for k in sorted(value.keys(), key=lambda x: str(x)):
89
+ n = _normalize(value[k], seen)
90
+ if n is _DROP:
91
+ continue
92
+ out_obj[str(k)] = n
93
+ seen.discard(obj_id)
94
+ return out_obj
95
+ # Anything else (a custom object, a set, etc.) has no defined canonical form.
96
+ # Fail loud rather than emit bytes that would not match the TS twin.
97
+ raise TypeError(
98
+ f"[canonical_json] value of type {type(value).__name__!r} is not JSON-canonicalizable"
99
+ )
100
+
101
+
102
+ def canonical_json(value: Any) -> str:
103
+ """Deterministically serialize ``value`` to a canonical JSON string.
104
+
105
+ Byte-identical to the TS ``canonicalJson``. Equivalent to
106
+ ``json.dumps(normalized, sort_keys=True, separators=(",", ":"),
107
+ ensure_ascii=False)`` where ``normalized`` has already had object keys
108
+ recursively sorted, non-finite numbers folded to null, and unserializable
109
+ values dropped/base64-encoded per the rules above.
110
+
111
+ We still pass ``sort_keys=True`` to ``json.dumps`` as belt-and-suspenders,
112
+ but the recursive normalization above is what guarantees the byte match.
113
+ """
114
+ norm = _normalize(value, set())
115
+ return json.dumps(
116
+ norm,
117
+ sort_keys=True,
118
+ ensure_ascii=False,
119
+ separators=(",", ":"),
120
+ )
121
+
122
+
123
+ def sha256_hex(s: str) -> str:
124
+ """SHA-256 hex digest of a string (UTF-8) — the platform's record primitive."""
125
+ return hashlib.sha256(s.encode("utf-8")).hexdigest()
126
+
127
+
128
+ def hash_entry(seq: int, ts: str, action: Any, prev_hash: str) -> str:
129
+ """Compute an entry's canonical hash from its content (everything but ``hash``).
130
+
131
+ ``hash = sha256(canonical_json({seq, ts, action, prev_hash}))``. The key
132
+ names in the hashed object use the platform's camelCase (``prevHash``) so the
133
+ bytes match the TS twin exactly — this differs from the Pythonic
134
+ ``prev_hash`` field name used on the returned dict.
135
+ """
136
+ content = {"seq": seq, "ts": ts, "action": action, "prevHash": prev_hash}
137
+ return sha256_hex(canonical_json(content))
@@ -0,0 +1,104 @@
1
+ """omega-audit — tamper-evident audit for AI agents (CLI).
2
+
3
+ omega-audit verify <ledger.jsonl> check a ledger's chain (exit 0 intact, 1 tampered)
4
+ omega-audit demo build a 3-record ledger, tamper it, show verify FAIL
5
+
6
+ Pure, offline, zero-dependency (stdlib only). Exit codes:
7
+ 0 = chain intact 1 = tamper detected 2 = usage error.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import sys
15
+ import tempfile
16
+
17
+ from .ledger import Ledger, read_ledger
18
+ from .verify import verify
19
+
20
+
21
+ def _usage() -> int:
22
+ sys.stderr.write("usage:\n")
23
+ sys.stderr.write(" omega-audit verify <ledger.jsonl> verify a ledger's hash-chain\n")
24
+ sys.stderr.write(" omega-audit demo see a tampered record fail verification\n")
25
+ return 2
26
+
27
+
28
+ def _print_verify(path: str) -> int:
29
+ r = verify(path)
30
+ if r.valid:
31
+ print(f"✓ {r.records} records — chain intact & tamper-evident")
32
+ return 0
33
+ if (r.reason or "").startswith("no such file"):
34
+ # Missing file ≠ tamper — but it must still fail closed (exit 1).
35
+ print(f"✗ CANNOT VERIFY: {r.reason}")
36
+ return 1
37
+ print(f"✗ TAMPER DETECTED at record {r.broken_at}: {r.reason}")
38
+ return 1
39
+
40
+
41
+ def _run_demo() -> int:
42
+ print("omega-audit demo — tamper-evident agent audit in one command\n")
43
+
44
+ tmp_dir = tempfile.mkdtemp(prefix="omega-audit-demo-")
45
+ path = os.path.join(tmp_dir, "ledger.jsonl")
46
+ ledger = Ledger(path)
47
+
48
+ # 1) Capture three sample agent actions into a hash-chained ledger.
49
+ print("1. An agent runs three tool calls. Each one is appended to a hash-chained ledger:\n")
50
+ ledger.append({"tool": "search_web", "args": {"query": "EU AI Act Article 12"}, "ok": True, "durationMs": 214})
51
+ ledger.append({"tool": "read_file", "args": {"path": "/reports/q3.md"}, "ok": True, "durationMs": 8})
52
+ ledger.append({"tool": "send_email", "args": {"to": "auditor@example.com", "subject": "Q3 evidence"}, "ok": True, "durationMs": 501})
53
+
54
+ for e in read_ledger(path):
55
+ tool = e["action"].get("tool") if isinstance(e["action"], dict) else None
56
+ print(f" #{e['seq']} {tool} hash={e['hash'][:12]}… prev={e['prevHash'][:12]}…")
57
+
58
+ # 2) Verify the untouched ledger — it passes.
59
+ print("\n2. Verify the untouched ledger:\n")
60
+ print(" $ omega-audit verify ledger.jsonl")
61
+ sys.stdout.write(" ")
62
+ _print_verify(path)
63
+
64
+ # 3) Tamper with one record — change what the agent claims it did.
65
+ print("\n3. Now someone quietly edits record #2 to hide who the email went to:\n")
66
+ with open(path, "r", encoding="utf-8") as fh:
67
+ lines = fh.read().strip().split("\n")
68
+ tampered = json.loads(lines[2])
69
+ before = tampered["action"]["args"].get("to")
70
+ print(f" before: send_email → {before}")
71
+ tampered["action"]["args"]["to"] = "attacker@evil.example" # edit payload, leave the hash
72
+ print(f" after: send_email → {tampered['action']['args']['to']} (hash left unchanged to hide the edit)")
73
+ lines[2] = json.dumps(tampered, ensure_ascii=False, separators=(",", ":"))
74
+ with open(path, "w", encoding="utf-8") as fh:
75
+ fh.write("\n".join(lines) + "\n")
76
+
77
+ # 4) Verify again — the recomputed hash no longer matches. Tamper is caught.
78
+ print("\n4. Verify again — the chain no longer recomputes:\n")
79
+ print(" $ omega-audit verify ledger.jsonl")
80
+ sys.stdout.write(" ")
81
+ code = _print_verify(path)
82
+
83
+ print("\nThat ✗ is the whole point: the record was changed, and verification proved it.")
84
+ print("Nothing here touched the network. You can run the same check on your own ledger.")
85
+ return code # 1 when the tamper was caught — that is the expected demo result
86
+
87
+
88
+ def main(argv: list[str] | None = None) -> int:
89
+ args = sys.argv[1:] if argv is None else argv
90
+ cmd = args[0] if args else None
91
+
92
+ if cmd == "verify":
93
+ if len(args) < 2 or not args[1]:
94
+ return _usage()
95
+ return _print_verify(args[1])
96
+ if cmd == "demo":
97
+ # `demo` exits 1: its job is to prove a tampered ledger FAILS verification,
98
+ # and the resulting exit-1 is the signal a CI check or a reader can rely on.
99
+ return _run_demo()
100
+ return _usage()
101
+
102
+
103
+ if __name__ == "__main__":
104
+ sys.exit(main())
@@ -0,0 +1,109 @@
1
+ """The append-only, hash-chained ledger.
2
+
3
+ A ledger is a plain JSONL file: one JSON object per line. Each line is an entry
4
+
5
+ {"seq", "ts", "action", "prevHash", "hash"}
6
+
7
+ written with the SAME field names the TypeScript twin writes (note ``prevHash``
8
+ in camelCase on disk — this is deliberate: it makes a Python-written ledger and
9
+ a TS-written ledger the same bytes, so they cross-verify and can be anchored to
10
+ the same transparency log). The genesis entry's ``prevHash`` is 64 zeros.
11
+
12
+ Offline: no network, no signup. Each ``append`` reads the tail hash, links the
13
+ new record to it, hashes the record, and writes one line.
14
+
15
+ Zero runtime dependencies: only ``json``/``time`` from stdlib (plus ``canonical``).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ from datetime import datetime, timezone
23
+ from typing import Any, Dict, List
24
+
25
+ from .canonical import GENESIS_PREV_HASH, hash_entry
26
+
27
+
28
+ def _iso_now() -> str:
29
+ """UTC timestamp in the same shape the TS twin uses: ``…T…Z`` with millis.
30
+
31
+ ``new Date().toISOString()`` yields e.g. ``2026-08-02T09:00:00.000Z``. We
32
+ match that format (milliseconds, ``Z`` suffix) so timestamps look identical
33
+ across the two implementations.
34
+ """
35
+ now = datetime.now(timezone.utc)
36
+ return now.strftime("%Y-%m-%dT%H:%M:%S.") + f"{now.microsecond // 1000:03d}Z"
37
+
38
+
39
+ def read_ledger(path: str) -> List[Dict[str, Any]]:
40
+ """Read a JSONL ledger file into a list of entry dicts.
41
+
42
+ Ignores blank lines; raises ``ValueError`` on a line that is not valid JSON
43
+ (a structurally corrupt file). Does NOT check the chain — use ``verify``.
44
+ Each returned dict has the on-disk keys: ``seq, ts, action, prevHash, hash``.
45
+ """
46
+ if not os.path.exists(path):
47
+ return []
48
+ out: List[Dict[str, Any]] = []
49
+ with open(path, "r", encoding="utf-8") as fh:
50
+ for i, raw in enumerate(fh.read().split("\n")):
51
+ line = raw.strip()
52
+ if line == "":
53
+ continue
54
+ try:
55
+ parsed = json.loads(line)
56
+ except json.JSONDecodeError as exc:
57
+ raise ValueError(f"[read_ledger] line {i + 1} is not valid JSON") from exc
58
+ out.append(parsed)
59
+ return out
60
+
61
+
62
+ class Ledger:
63
+ """An append-only, hash-chained ledger persisted as JSONL at ``path``.
64
+
65
+ Mirrors the TS ``Ledger`` interface: ``append(action) -> dict``, ``size()``,
66
+ ``tip_hash()``.
67
+ """
68
+
69
+ def __init__(self, path: str) -> None:
70
+ self.path = path
71
+
72
+ def _entries(self) -> List[Dict[str, Any]]:
73
+ return read_ledger(self.path)
74
+
75
+ def size(self) -> int:
76
+ """Number of records currently in the ledger file."""
77
+ return len(self._entries())
78
+
79
+ def tip_hash(self) -> str:
80
+ """Hash of the last record (or ``GENESIS_PREV_HASH`` if empty) — the chain tip."""
81
+ entries = self._entries()
82
+ return GENESIS_PREV_HASH if not entries else entries[-1]["hash"]
83
+
84
+ def append(self, action: Any) -> Dict[str, Any]:
85
+ """Append an ``action``.
86
+
87
+ Reads the current tail hash, builds the next linked+hashed record, and
88
+ writes one JSON line. Returns the persisted entry dict, keyed with the
89
+ on-disk field names ``{seq, ts, action, prevHash, hash}`` (so it matches
90
+ the TS twin's returned entry byte-for-byte when re-serialized).
91
+ """
92
+ entries = self._entries()
93
+ seq = len(entries)
94
+ prev_hash = GENESIS_PREV_HASH if not entries else entries[-1]["hash"]
95
+ ts = _iso_now()
96
+ entry_hash = hash_entry(seq, ts, action, prev_hash)
97
+ entry = {
98
+ "seq": seq,
99
+ "ts": ts,
100
+ "action": action,
101
+ "prevHash": prev_hash,
102
+ "hash": entry_hash,
103
+ }
104
+ # One JSON line, no whitespace, non-ASCII preserved — same shape the TS
105
+ # twin writes via JSON.stringify.
106
+ line = json.dumps(entry, ensure_ascii=False, separators=(",", ":"))
107
+ with open(self.path, "a", encoding="utf-8") as fh:
108
+ fh.write(line + "\n")
109
+ return entry
@@ -0,0 +1,97 @@
1
+ """Chain verification — recompute the whole chain and report any tampering.
2
+
3
+ Detects ANY tampering — an edit to any field, a reordering, an inserted record,
4
+ or a deleted record — because each record's ``hash`` covers its content and its
5
+ ``prevHash`` must equal the previous record's ``hash``. Pure and offline. This
6
+ is the same logic as the TS twin's ``verify`` / ``verifyEntries``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from dataclasses import dataclass
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ from .canonical import GENESIS_PREV_HASH, hash_entry
16
+ from .ledger import read_ledger
17
+
18
+
19
+ @dataclass
20
+ class VerifyResult:
21
+ """Outcome of verifying a ledger.
22
+
23
+ ``broken_at`` is the zero-based index of the first record that fails the
24
+ chain (``None`` when valid). ``reason`` is a human-readable explanation,
25
+ present iff ``not valid``.
26
+ """
27
+
28
+ valid: bool
29
+ records: int
30
+ broken_at: Optional[int] = None
31
+ reason: Optional[str] = None
32
+
33
+
34
+ def _short(h: Any) -> str:
35
+ if isinstance(h, str) and len(h) > 12:
36
+ return f"{h[:8]}…{h[-4:]}"
37
+ return str(h)
38
+
39
+
40
+ def verify_entries(entries: List[Dict[str, Any]]) -> VerifyResult:
41
+ """Verify already-parsed entry dicts (same logic as ``verify`` without I/O)."""
42
+ expected_prev = GENESIS_PREV_HASH
43
+ for i, e in enumerate(entries):
44
+ if not isinstance(e, dict):
45
+ return VerifyResult(False, len(entries), i, f"record {i} is not an object")
46
+
47
+ if e.get("seq") != i:
48
+ return VerifyResult(
49
+ False,
50
+ len(entries),
51
+ i,
52
+ f"record {i} has seq {e.get('seq')} — expected {i} "
53
+ "(a record was reordered, inserted, or deleted)",
54
+ )
55
+
56
+ prev = e.get("prevHash")
57
+ if prev != expected_prev:
58
+ if i == 0:
59
+ reason = (
60
+ f"record 0 must link to the genesis hash "
61
+ f"({GENESIS_PREV_HASH[:8]}…) but links to {_short(prev)}"
62
+ )
63
+ else:
64
+ reason = (
65
+ f"record {i} does not link to record {i - 1} "
66
+ f"(prevHash {_short(prev)} ≠ {_short(expected_prev)}) "
67
+ "— a record was altered, inserted, or deleted"
68
+ )
69
+ return VerifyResult(False, len(entries), i, reason)
70
+
71
+ recomputed = hash_entry(e.get("seq"), e.get("ts"), e.get("action"), e.get("prevHash"))
72
+ if recomputed != e.get("hash"):
73
+ return VerifyResult(
74
+ False,
75
+ len(entries),
76
+ i,
77
+ f"record {i} was edited — its stored hash {_short(e.get('hash'))} "
78
+ f"≠ the recomputed hash {_short(recomputed)}",
79
+ )
80
+
81
+ expected_prev = e["hash"]
82
+
83
+ return VerifyResult(True, len(entries))
84
+
85
+
86
+ def verify(path: str) -> VerifyResult:
87
+ """Recompute the entire chain from a JSONL ledger file and report integrity."""
88
+ # Fail closed: read_ledger treats a missing file as an empty ledger (right for
89
+ # the append path), but for verification "file not found" must never report
90
+ # "chain intact" — an auditor with a typo'd path would get a green check.
91
+ if not os.path.exists(path):
92
+ return VerifyResult(False, 0, 0, f"no such file: {path}")
93
+ try:
94
+ entries = read_ledger(path)
95
+ except Exception as exc: # a structurally corrupt file is itself a failure
96
+ return VerifyResult(False, 0, 0, str(exc))
97
+ return verify_entries(entries)
@@ -0,0 +1,114 @@
1
+ """Wrapping tool calls — record every invocation to the ledger.
2
+
3
+ ``wrap_tool(ledger, name, fn)`` returns a wrapper that appends a
4
+ ``{"tool", "args", "ok", "result"|"error", "duration_ms"}`` record for every
5
+ call, then returns or raises EXACTLY as the original did.
6
+
7
+ Python agents are frequently async, so both sync and async callables are
8
+ supported: if ``fn`` is a coroutine function the wrapper is itself a coroutine
9
+ function (so ``await wrapped(...)`` works and the record is written when the
10
+ awaited call settles); otherwise the wrapper is an ordinary function.
11
+
12
+ The recorded ``args`` mirror the TS twin: positional arguments as a list. When
13
+ keyword arguments are also passed, they are recorded under a ``kwargs`` key so
14
+ nothing about the call is lost.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import functools
20
+ import inspect
21
+ import time
22
+ from typing import Any, Callable
23
+
24
+ from .ledger import Ledger
25
+
26
+
27
+ def _record_args(args: tuple, kwargs: dict) -> Any:
28
+ """Shape call arguments for the ledger.
29
+
30
+ Positional-only calls record ``args`` as a plain list (identical to the TS
31
+ twin, where a tool is called positionally). If keyword arguments are present
32
+ we record ``{"args": [...], "kwargs": {...}}`` so the record stays complete.
33
+ """
34
+ if kwargs:
35
+ return {"args": list(args), "kwargs": dict(kwargs)}
36
+ return list(args)
37
+
38
+
39
+ def wrap_tool(ledger: Ledger, name: str, fn: Callable[..., Any]) -> Callable[..., Any]:
40
+ """Wrap a named tool so every call is appended to ``ledger``.
41
+
42
+ Works for sync and async ``fn``. Returns/raises unchanged; on a raised
43
+ exception the record is written with ``ok=False`` and the error message, and
44
+ the exception is re-raised (never swallowed).
45
+ """
46
+ if inspect.iscoroutinefunction(fn):
47
+
48
+ @functools.wraps(fn)
49
+ async def async_wrapped(*args: Any, **kwargs: Any) -> Any:
50
+ start = time.monotonic()
51
+ try:
52
+ result = await fn(*args, **kwargs)
53
+ ledger.append(
54
+ {
55
+ "tool": name,
56
+ "args": _record_args(args, kwargs),
57
+ "ok": True,
58
+ "result": result,
59
+ "duration_ms": int((time.monotonic() - start) * 1000),
60
+ }
61
+ )
62
+ return result
63
+ except Exception as err: # noqa: BLE001 — record then re-raise unchanged
64
+ ledger.append(
65
+ {
66
+ "tool": name,
67
+ "args": _record_args(args, kwargs),
68
+ "ok": False,
69
+ "error": str(err),
70
+ "duration_ms": int((time.monotonic() - start) * 1000),
71
+ }
72
+ )
73
+ raise
74
+
75
+ return async_wrapped
76
+
77
+ @functools.wraps(fn)
78
+ def sync_wrapped(*args: Any, **kwargs: Any) -> Any:
79
+ start = time.monotonic()
80
+ try:
81
+ result = fn(*args, **kwargs)
82
+ ledger.append(
83
+ {
84
+ "tool": name,
85
+ "args": _record_args(args, kwargs),
86
+ "ok": True,
87
+ "result": result,
88
+ "duration_ms": int((time.monotonic() - start) * 1000),
89
+ }
90
+ )
91
+ return result
92
+ except Exception as err: # noqa: BLE001 — record then re-raise unchanged
93
+ ledger.append(
94
+ {
95
+ "tool": name,
96
+ "args": _record_args(args, kwargs),
97
+ "ok": False,
98
+ "error": str(err),
99
+ "duration_ms": int((time.monotonic() - start) * 1000),
100
+ }
101
+ )
102
+ raise
103
+
104
+ return sync_wrapped
105
+
106
+
107
+ def capture(ledger: Ledger, fn: Callable[..., Any]) -> Callable[..., Any]:
108
+ """Convenience alias: wrap a tool under its own ``__name__``.
109
+
110
+ Equivalent to ``wrap_tool(ledger, fn.__name__, fn)``. Handy for wrapping
111
+ named functions inline.
112
+ """
113
+ name = getattr(fn, "__name__", "") or "anonymous"
114
+ return wrap_tool(ledger, name, fn)
@@ -0,0 +1,70 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "omega-audit"
7
+ version = "0.1.0"
8
+ description = "Tamper-evident audit + provenance for AI agents — an offline, zero-dependency, hash-chained append-only ledger. Wrap your agent's tool calls, then prove what it did: any edit, reorder, insertion, or deletion fails verification."
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "OmegaEngine", email = "team@omegaengine.ai" }
14
+ ]
15
+ keywords = [
16
+ "ai-agents",
17
+ "audit-log",
18
+ "tamper-evident",
19
+ "hash-chain",
20
+ "provenance",
21
+ "append-only",
22
+ "eu-ai-act",
23
+ "compliance",
24
+ "sha256",
25
+ "omegaengine",
26
+ ]
27
+ classifiers = [
28
+ "Development Status :: 4 - Beta",
29
+ "Intended Audience :: Developers",
30
+ "License :: OSI Approved :: Apache Software License",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.9",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Programming Language :: Python :: 3.13",
37
+ "Topic :: Software Development :: Libraries :: Python Modules",
38
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
39
+ "Topic :: Security",
40
+ ]
41
+
42
+ # Zero runtime dependencies — stdlib only (hashlib / json / base64 / datetime).
43
+ dependencies = []
44
+
45
+ [project.optional-dependencies]
46
+ dev = [
47
+ "pytest>=7.0",
48
+ ]
49
+
50
+ [project.scripts]
51
+ omega-audit = "omega_audit.cli:main"
52
+
53
+ [project.urls]
54
+ Homepage = "https://omegaengine.ai"
55
+ Documentation = "https://github.com/TheArkhitect/Omegaengine/tree/main/packages/omega-audit-py#readme"
56
+ Repository = "https://github.com/TheArkhitect/Omegaengine/tree/main/packages/omega-audit-py"
57
+ Issues = "https://github.com/TheArkhitect/Omegaengine/issues"
58
+
59
+ [tool.hatch.build.targets.sdist]
60
+ include = [
61
+ "/omega_audit",
62
+ "/README.md",
63
+ "/LICENSE",
64
+ ]
65
+
66
+ [tool.hatch.build.targets.wheel]
67
+ packages = ["omega_audit"]
68
+
69
+ [tool.pytest.ini_options]
70
+ testpaths = ["tests"]