openwright-core 0.6.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 (56) hide show
  1. openwright_core-0.6.0/LICENSE +52 -0
  2. openwright_core-0.6.0/PKG-INFO +174 -0
  3. openwright_core-0.6.0/README.pypi.md +137 -0
  4. openwright_core-0.6.0/pyproject.toml +81 -0
  5. openwright_core-0.6.0/src/openwright/__init__.py +20 -0
  6. openwright_core-0.6.0/src/openwright/_ed25519_pure.py +98 -0
  7. openwright_core-0.6.0/src/openwright/adapters/__init__.py +37 -0
  8. openwright_core-0.6.0/src/openwright/adapters/a2a.py +71 -0
  9. openwright_core-0.6.0/src/openwright/adapters/base.py +24 -0
  10. openwright_core-0.6.0/src/openwright/adapters/langfuse.py +56 -0
  11. openwright_core-0.6.0/src/openwright/adapters/otel_genai.py +180 -0
  12. openwright_core-0.6.0/src/openwright/adapters/policy.py +63 -0
  13. openwright_core-0.6.0/src/openwright/adapters/receipt.py +198 -0
  14. openwright_core-0.6.0/src/openwright/adapters/sarif_in.py +68 -0
  15. openwright_core-0.6.0/src/openwright/anchor.py +134 -0
  16. openwright_core-0.6.0/src/openwright/authz.py +68 -0
  17. openwright_core-0.6.0/src/openwright/browser_verifier.py +197 -0
  18. openwright_core-0.6.0/src/openwright/canonical.py +119 -0
  19. openwright_core-0.6.0/src/openwright/checkpoint_store.py +140 -0
  20. openwright_core-0.6.0/src/openwright/cli.py +445 -0
  21. openwright_core-0.6.0/src/openwright/connectors/__init__.py +236 -0
  22. openwright_core-0.6.0/src/openwright/connectors/builtin.py +88 -0
  23. openwright_core-0.6.0/src/openwright/crosswalk.py +372 -0
  24. openwright_core-0.6.0/src/openwright/crosswalk_loader.py +53 -0
  25. openwright_core-0.6.0/src/openwright/crosswalks/CHANGELOG.md +47 -0
  26. openwright_core-0.6.0/src/openwright/crosswalks/__init__.py +7 -0
  27. openwright_core-0.6.0/src/openwright/crosswalks/eu_ai_act.yaml +294 -0
  28. openwright_core-0.6.0/src/openwright/crosswalks/eu_ai_act_v1.yaml +107 -0
  29. openwright_core-0.6.0/src/openwright/crosswalks/gdpr.yaml +304 -0
  30. openwright_core-0.6.0/src/openwright/crosswalks/iso_42001.yaml +209 -0
  31. openwright_core-0.6.0/src/openwright/crosswalks/nist_ai_rmf.yaml +204 -0
  32. openwright_core-0.6.0/src/openwright/crosswalks/soc2.yaml +246 -0
  33. openwright_core-0.6.0/src/openwright/dashboard.py +100 -0
  34. openwright_core-0.6.0/src/openwright/demo.py +270 -0
  35. openwright_core-0.6.0/src/openwright/events.py +210 -0
  36. openwright_core-0.6.0/src/openwright/identity.py +61 -0
  37. openwright_core-0.6.0/src/openwright/ingest/__init__.py +13 -0
  38. openwright_core-0.6.0/src/openwright/ingest/durable.py +144 -0
  39. openwright_core-0.6.0/src/openwright/ingest/fanout.py +38 -0
  40. openwright_core-0.6.0/src/openwright/ingest/grpc_server.py +44 -0
  41. openwright_core-0.6.0/src/openwright/ingest/http_server.py +163 -0
  42. openwright_core-0.6.0/src/openwright/ingest/otlp_common.py +69 -0
  43. openwright_core-0.6.0/src/openwright/ingest/pipeline.py +307 -0
  44. openwright_core-0.6.0/src/openwright/ledger.py +479 -0
  45. openwright_core-0.6.0/src/openwright/merkle.py +262 -0
  46. openwright_core-0.6.0/src/openwright/report.py +388 -0
  47. openwright_core-0.6.0/src/openwright/sbom.py +42 -0
  48. openwright_core-0.6.0/src/openwright/scheduler.py +97 -0
  49. openwright_core-0.6.0/src/openwright/sdk.py +297 -0
  50. openwright_core-0.6.0/src/openwright/signing.py +343 -0
  51. openwright_core-0.6.0/src/openwright/spec.py +109 -0
  52. openwright_core-0.6.0/src/openwright/vault.py +55 -0
  53. openwright_core-0.6.0/src/openwright/verify.py +392 -0
  54. openwright_core-0.6.0/src/openwright/web_demo.py +275 -0
  55. openwright_core-0.6.0/src/openwright/witness.py +90 -0
  56. openwright_core-0.6.0/src/openwright/witness_service.py +136 -0
@@ -0,0 +1,52 @@
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
+ "You" (or "Your") shall mean an individual or Legal Entity
16
+ exercising permissions granted by this License.
17
+
18
+ 2. Grant of Copyright License. Subject to the terms and conditions of
19
+ this License, each Contributor hereby grants to You a perpetual,
20
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
21
+ copyright license to reproduce, prepare Derivative Works of,
22
+ publicly display, publicly perform, sublicense, and distribute the
23
+ Work and such Derivative Works in Source or Object form.
24
+
25
+ 3. Grant of Patent License. Subject to the terms and conditions of
26
+ this License, each Contributor hereby grants to You a perpetual,
27
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
28
+ (except as stated in this section) patent license to make, have
29
+ made, use, offer to sell, sell, import, and otherwise transfer the
30
+ Work.
31
+
32
+ 7. Disclaimer of Warranty. Unless required by applicable law or
33
+ agreed to in writing, Licensor provides the Work (and each
34
+ Contributor provides its Contributions) on an "AS IS" BASIS,
35
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
36
+ implied.
37
+
38
+ See http://www.apache.org/licenses/LICENSE-2.0 for the complete license text.
39
+
40
+ Copyright 2026 allthingsN — OpenWright maintainers.
41
+
42
+ Licensed under the Apache License, Version 2.0 (the "License");
43
+ you may not use this file except in compliance with the License.
44
+ You may obtain a copy of the License at
45
+
46
+ http://www.apache.org/licenses/LICENSE-2.0
47
+
48
+ Unless required by applicable law or agreed to in writing, software
49
+ distributed under the License is distributed on an "AS IS" BASIS,
50
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
51
+ See the License for the specific language governing permissions and
52
+ limitations under the License.
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: openwright-core
3
+ Version: 0.6.0
4
+ Summary: Agent Evidence Layer — turns AI-agent runtime behavior into signed, tamper-evident, control-mapped audit evidence.
5
+ License: Apache-2.0
6
+ License-File: LICENSE
7
+ Keywords: ai-agents,compliance,evidence,audit,eu-ai-act,opentelemetry,a2a,merkle,transparency-log,attestation
8
+ Author: OpenWright maintainers
9
+ Requires-Python: >=3.10,<3.15
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Security
15
+ Provides-Extra: hsm
16
+ Provides-Extra: langgraph
17
+ Provides-Extra: postgres
18
+ Provides-Extra: s3
19
+ Requires-Dist: boto3 (>=1.34) ; extra == "s3"
20
+ Requires-Dist: cryptography (>=42)
21
+ Requires-Dist: grpcio (>=1.60)
22
+ Requires-Dist: jsonschema (>=4.20)
23
+ Requires-Dist: langgraph (>=0.2) ; extra == "langgraph"
24
+ Requires-Dist: opentelemetry-api (>=1.27)
25
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.27)
26
+ Requires-Dist: opentelemetry-proto (>=1.27)
27
+ Requires-Dist: opentelemetry-sdk (>=1.27)
28
+ Requires-Dist: protobuf (>=4.25)
29
+ Requires-Dist: psycopg[binary] (>=3.1) ; extra == "postgres"
30
+ Requires-Dist: pydantic (>=2.7,<3)
31
+ Requires-Dist: python-pkcs11 (>=0.7) ; extra == "hsm"
32
+ Requires-Dist: pyyaml (>=6)
33
+ Requires-Dist: reportlab (>=4.1)
34
+ Requires-Dist: typer (>=0.12)
35
+ Description-Content-Type: text/markdown
36
+
37
+ # OpenWright — the Agent Evidence Layer
38
+
39
+ **Turn the runtime behavior of AI agents into signed, tamper-evident,
40
+ control-mapped audit evidence — verifiable by anyone, offline.**
41
+
42
+ OpenWright sits on top of your existing OpenTelemetry instrumentation, forks a
43
+ copy of your agent's runtime behavior, and turns it into signed, append-only,
44
+ tamper-evident records mapped to specific regulatory controls. The evidence is
45
+ verifiable by a third party **without** access to your data or infrastructure,
46
+ and the ledger holds only hashes — never prompts or PII.
47
+
48
+ It ships five built-in, primary-source-cited framework crosswalks — plus a
49
+ narrowed EU AI Act **v1** review subset (`eu-ai-act-v1`) and deployer-profile
50
+ variants of Art. 14 (in-the-loop / on-the-loop) and Art. 27 (FRIA-gated). You can
51
+ also write your own:
52
+
53
+ | Crosswalk | Covers | Controls |
54
+ |---|---|---|
55
+ | EU AI Act (Reg. 2024/1689) | Art. 12, 13, 14, 26, 27, 73 (high-risk subset) | 6 |
56
+ | NIST AI RMF 1.0 | Govern / Map / Measure / Manage subset | 7 |
57
+ | ISO/IEC 42001:2023 | Annex A AI-management-system subset | 6 |
58
+ | GDPR (Reg. 2016/679) | Art. 5, 22, 25, 30, 33, 35 (accountability + automated decisions) | 8 |
59
+ | SOC 2 | Trust Services Criteria — Common Criteria + Processing Integrity | 7 |
60
+
61
+ These crosswalks are **maintainer-authored and cited, but not yet legally
62
+ reviewed** — they produce *evidence*, not a compliance determination.
63
+
64
+ > **OpenWright produces _evidence that controls were exercised_. It does NOT
65
+ > assert legal compliance, certification, or an audit opinion** — those are for
66
+ > qualified auditors and counsel. Every artifact carries this boundary.
67
+
68
+ ## Install
69
+
70
+ ```bash
71
+ pip install openwright-core
72
+ ```
73
+
74
+ The distribution is `openwright-core`; it imports as `openwright`. Requires Python 3.10+.
75
+
76
+ ## Try it (one command)
77
+
78
+ ```bash
79
+ openwright demo
80
+ ```
81
+
82
+ Runs a complete, self-hosted, no-network demo: it instruments a sample high-risk
83
+ agent, forks the telemetry into an evidence ledger, records human approvals and
84
+ risk classifications, produces a **signed report** (JSON + PDF + OSCAL + SARIF)
85
+ against the EU AI Act crosswalk, verifies it offline, and shows that tampering
86
+ fails verification. It then **opens an interactive page in your browser** that
87
+ walks through what happened and lets you **verify the real signed report
88
+ yourself — offline, in-browser (WebAssembly)** — including a "Tamper" button to
89
+ watch verification fail and a "Restore" button to watch it pass again. (Use
90
+ `openwright demo --no-browser` for headless/CI.)
91
+
92
+ It also prints where the artifacts landed and a command to verify them yourself:
93
+
94
+ ```bash
95
+ openwright verify <report.json> --pubkey <public_key.pem> --deep
96
+ ```
97
+
98
+ ## Use it in your code
99
+
100
+ ```python
101
+ from datetime import timedelta
102
+ from openwright.ledger import FileLedgerBackend, Ledger
103
+ from openwright.sdk import EvidenceClient
104
+ from openwright.signing import InMemoryKeySource # use FileKeySource in production
105
+ from openwright.crosswalk import evaluate
106
+ from openwright.crosswalk_loader import load_builtin
107
+ from openwright.report import build_report
108
+ from openwright.verify import verify_report
109
+
110
+ key = InMemoryKeySource()
111
+ ledger = Ledger(FileLedgerBackend("./ledger"), retention=timedelta(days=200))
112
+ client = EvidenceClient(ledger, agent_id="my-agent")
113
+
114
+ # Record the evidence telemetry can't infer — linked by task id.
115
+ with client.task("task-42", context_id="session-7"):
116
+ approval = client.record_human_approval(reviewer="me@example.com", rationale="looks good")
117
+ client.record_risk_classification("high", rationale="high-impact decision", fria="FRIA-1")
118
+ client.record_decision(
119
+ output="APPROVED",
120
+ risk_classification="high",
121
+ rationale="meets policy",
122
+ approval_ref=approval.event_id, # links the decision to the approval
123
+ control="art-14-human-oversight",
124
+ )
125
+
126
+ # Evaluate a crosswalk, build a signed report, verify it offline.
127
+ result = evaluate(load_builtin("eu-ai-act"), list(ledger.events()))
128
+ report = build_report(ledger, result, key, scope_description="my agent")
129
+ verdict = verify_report(report, trusted_public_key_raw=key.public_key_raw())
130
+
131
+ print({c["control_id"]: c["status"] for c in report["controls"]})
132
+ print("verified offline:", verdict.valid)
133
+ ```
134
+
135
+ ## Collect from a live agent
136
+
137
+ Point your app's OTLP exporter at the collector; your existing backend keeps
138
+ receiving telemetry unchanged while a copy is forked into the evidence ledger:
139
+
140
+ ```bash
141
+ openwright collector --downstream http://your-existing-otlp-backend:4318
142
+ ```
143
+
144
+ ## CLI
145
+
146
+ ```
147
+ openwright demo Run the full end-to-end demo.
148
+ openwright collector --downstream URL Run the OTLP collector (gRPC+HTTP); fan out + fork to ledger.
149
+ openwright report LEDGER --key K Build a signed report (JSON/PDF/OSCAL/SARIF) from a ledger.
150
+ openwright verify REPORT --pubkey K Verify a signed report offline (--deep re-checks verdicts).
151
+ openwright gate REPORT -r CONTROL CI/CD gate: non-zero exit if a required control is unsatisfied.
152
+ openwright crosswalks List built-in crosswalks.
153
+ openwright connectors list List installed connectors (sources/forwarders/exporters/storage).
154
+ openwright export REPORT --to NAME Export a report to a sink (GRC/CI) via an installed exporter.
155
+ openwright keygen --out key.pem Generate an Ed25519 signing key you control.
156
+ openwright version
157
+ ```
158
+
159
+ ## Connectors
160
+
161
+ OpenWright exposes a small, versioned **connector contract** (`openwright.connectors`,
162
+ v1.0): framework-capture sources, downstream forwarders, report exporters, and
163
+ pluggable ledger/checkpoint storage backends. Connectors are independently
164
+ installable `openwright-<name>` packages discovered via entry points — **core never
165
+ depends on a connector**, so adding one needs zero core change. Resolve storage by
166
+ URI (`openwright collector --ledger-backend postgres://… --checkpoint-store s3://…`)
167
+ and see what's installed with `openwright connectors list`.
168
+
169
+ Run `openwright --help` for the full list.
170
+
171
+ ## License
172
+
173
+ Apache-2.0.
174
+
@@ -0,0 +1,137 @@
1
+ # OpenWright — the Agent Evidence Layer
2
+
3
+ **Turn the runtime behavior of AI agents into signed, tamper-evident,
4
+ control-mapped audit evidence — verifiable by anyone, offline.**
5
+
6
+ OpenWright sits on top of your existing OpenTelemetry instrumentation, forks a
7
+ copy of your agent's runtime behavior, and turns it into signed, append-only,
8
+ tamper-evident records mapped to specific regulatory controls. The evidence is
9
+ verifiable by a third party **without** access to your data or infrastructure,
10
+ and the ledger holds only hashes — never prompts or PII.
11
+
12
+ It ships five built-in, primary-source-cited framework crosswalks — plus a
13
+ narrowed EU AI Act **v1** review subset (`eu-ai-act-v1`) and deployer-profile
14
+ variants of Art. 14 (in-the-loop / on-the-loop) and Art. 27 (FRIA-gated). You can
15
+ also write your own:
16
+
17
+ | Crosswalk | Covers | Controls |
18
+ |---|---|---|
19
+ | EU AI Act (Reg. 2024/1689) | Art. 12, 13, 14, 26, 27, 73 (high-risk subset) | 6 |
20
+ | NIST AI RMF 1.0 | Govern / Map / Measure / Manage subset | 7 |
21
+ | ISO/IEC 42001:2023 | Annex A AI-management-system subset | 6 |
22
+ | GDPR (Reg. 2016/679) | Art. 5, 22, 25, 30, 33, 35 (accountability + automated decisions) | 8 |
23
+ | SOC 2 | Trust Services Criteria — Common Criteria + Processing Integrity | 7 |
24
+
25
+ These crosswalks are **maintainer-authored and cited, but not yet legally
26
+ reviewed** — they produce *evidence*, not a compliance determination.
27
+
28
+ > **OpenWright produces _evidence that controls were exercised_. It does NOT
29
+ > assert legal compliance, certification, or an audit opinion** — those are for
30
+ > qualified auditors and counsel. Every artifact carries this boundary.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install openwright-core
36
+ ```
37
+
38
+ The distribution is `openwright-core`; it imports as `openwright`. Requires Python 3.10+.
39
+
40
+ ## Try it (one command)
41
+
42
+ ```bash
43
+ openwright demo
44
+ ```
45
+
46
+ Runs a complete, self-hosted, no-network demo: it instruments a sample high-risk
47
+ agent, forks the telemetry into an evidence ledger, records human approvals and
48
+ risk classifications, produces a **signed report** (JSON + PDF + OSCAL + SARIF)
49
+ against the EU AI Act crosswalk, verifies it offline, and shows that tampering
50
+ fails verification. It then **opens an interactive page in your browser** that
51
+ walks through what happened and lets you **verify the real signed report
52
+ yourself — offline, in-browser (WebAssembly)** — including a "Tamper" button to
53
+ watch verification fail and a "Restore" button to watch it pass again. (Use
54
+ `openwright demo --no-browser` for headless/CI.)
55
+
56
+ It also prints where the artifacts landed and a command to verify them yourself:
57
+
58
+ ```bash
59
+ openwright verify <report.json> --pubkey <public_key.pem> --deep
60
+ ```
61
+
62
+ ## Use it in your code
63
+
64
+ ```python
65
+ from datetime import timedelta
66
+ from openwright.ledger import FileLedgerBackend, Ledger
67
+ from openwright.sdk import EvidenceClient
68
+ from openwright.signing import InMemoryKeySource # use FileKeySource in production
69
+ from openwright.crosswalk import evaluate
70
+ from openwright.crosswalk_loader import load_builtin
71
+ from openwright.report import build_report
72
+ from openwright.verify import verify_report
73
+
74
+ key = InMemoryKeySource()
75
+ ledger = Ledger(FileLedgerBackend("./ledger"), retention=timedelta(days=200))
76
+ client = EvidenceClient(ledger, agent_id="my-agent")
77
+
78
+ # Record the evidence telemetry can't infer — linked by task id.
79
+ with client.task("task-42", context_id="session-7"):
80
+ approval = client.record_human_approval(reviewer="me@example.com", rationale="looks good")
81
+ client.record_risk_classification("high", rationale="high-impact decision", fria="FRIA-1")
82
+ client.record_decision(
83
+ output="APPROVED",
84
+ risk_classification="high",
85
+ rationale="meets policy",
86
+ approval_ref=approval.event_id, # links the decision to the approval
87
+ control="art-14-human-oversight",
88
+ )
89
+
90
+ # Evaluate a crosswalk, build a signed report, verify it offline.
91
+ result = evaluate(load_builtin("eu-ai-act"), list(ledger.events()))
92
+ report = build_report(ledger, result, key, scope_description="my agent")
93
+ verdict = verify_report(report, trusted_public_key_raw=key.public_key_raw())
94
+
95
+ print({c["control_id"]: c["status"] for c in report["controls"]})
96
+ print("verified offline:", verdict.valid)
97
+ ```
98
+
99
+ ## Collect from a live agent
100
+
101
+ Point your app's OTLP exporter at the collector; your existing backend keeps
102
+ receiving telemetry unchanged while a copy is forked into the evidence ledger:
103
+
104
+ ```bash
105
+ openwright collector --downstream http://your-existing-otlp-backend:4318
106
+ ```
107
+
108
+ ## CLI
109
+
110
+ ```
111
+ openwright demo Run the full end-to-end demo.
112
+ openwright collector --downstream URL Run the OTLP collector (gRPC+HTTP); fan out + fork to ledger.
113
+ openwright report LEDGER --key K Build a signed report (JSON/PDF/OSCAL/SARIF) from a ledger.
114
+ openwright verify REPORT --pubkey K Verify a signed report offline (--deep re-checks verdicts).
115
+ openwright gate REPORT -r CONTROL CI/CD gate: non-zero exit if a required control is unsatisfied.
116
+ openwright crosswalks List built-in crosswalks.
117
+ openwright connectors list List installed connectors (sources/forwarders/exporters/storage).
118
+ openwright export REPORT --to NAME Export a report to a sink (GRC/CI) via an installed exporter.
119
+ openwright keygen --out key.pem Generate an Ed25519 signing key you control.
120
+ openwright version
121
+ ```
122
+
123
+ ## Connectors
124
+
125
+ OpenWright exposes a small, versioned **connector contract** (`openwright.connectors`,
126
+ v1.0): framework-capture sources, downstream forwarders, report exporters, and
127
+ pluggable ledger/checkpoint storage backends. Connectors are independently
128
+ installable `openwright-<name>` packages discovered via entry points — **core never
129
+ depends on a connector**, so adding one needs zero core change. Resolve storage by
130
+ URI (`openwright collector --ledger-backend postgres://… --checkpoint-store s3://…`)
131
+ and see what's installed with `openwright connectors list`.
132
+
133
+ Run `openwright --help` for the full list.
134
+
135
+ ## License
136
+
137
+ Apache-2.0.
@@ -0,0 +1,81 @@
1
+ [project]
2
+ name = "openwright-core"
3
+ version = "0.6.0"
4
+ description = "Agent Evidence Layer — turns AI-agent runtime behavior into signed, tamper-evident, control-mapped audit evidence."
5
+ # Public PyPI long-description: a self-contained subset of README.md with the
6
+ # internal-repo "Documentation" + "Project layout" sections removed (the source
7
+ # repo is private). The full README.md stays the in-repo doc for org members.
8
+ readme = "README.pypi.md"
9
+ requires-python = ">=3.10,<3.15"
10
+ license = { text = "Apache-2.0" }
11
+ authors = [{ name = "OpenWright maintainers" }]
12
+ keywords = [
13
+ "ai-agents", "compliance", "evidence", "audit", "eu-ai-act",
14
+ "opentelemetry", "a2a", "merkle", "transparency-log", "attestation",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: Apache Software License",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Security",
22
+ ]
23
+
24
+ # Core dependencies. The standalone verifier (openwright.verify) deliberately
25
+ # imports ONLY `cryptography` + the standard library so it can be audited and
26
+ # run in isolation (FR-VER-03). Everything else below supports producing
27
+ # evidence; the verifier does not need it.
28
+ dependencies = [
29
+ "pydantic>=2.7,<3",
30
+ "cryptography>=42",
31
+ "pyyaml>=6",
32
+ "jsonschema>=4.20",
33
+ "typer>=0.12",
34
+ "reportlab>=4.1",
35
+ # OTLP ingest + example agent (FR-ING-01/02/03, AC-01).
36
+ "opentelemetry-api>=1.27",
37
+ "opentelemetry-sdk>=1.27",
38
+ "opentelemetry-exporter-otlp-proto-http>=1.27",
39
+ "opentelemetry-proto>=1.27",
40
+ "protobuf>=4.25",
41
+ # gRPC ingest transport (FR-ING-01). Lazily imported so the rest of the
42
+ # system works even if the wheel is unavailable on a given platform.
43
+ "grpcio>=1.60",
44
+ ]
45
+
46
+ [project.optional-dependencies]
47
+ langgraph = ["langgraph>=0.2"]
48
+ # Production storage/crypto backends. Each is lazily imported so the default
49
+ # install — and especially the shallow verifier (INV-2) — never pulls them in.
50
+ postgres = ["psycopg[binary]>=3.1"]
51
+ s3 = ["boto3>=1.34"]
52
+ hsm = ["python-pkcs11>=0.7"]
53
+
54
+ [project.scripts]
55
+ openwright = "openwright.cli:main"
56
+
57
+ # [project.urls] intentionally omitted: the source repo is private, so public
58
+ # Homepage/Documentation links would 404 on PyPI. Restore when a public site exists.
59
+
60
+ [tool.poetry]
61
+ packages = [
62
+ { include = "openwright", from = "src" },
63
+ ]
64
+
65
+ [tool.poetry.group.dev.dependencies]
66
+ pytest = "^8.2"
67
+ pytest-timeout = "^2.3"
68
+ # Backend tests (V2/V3/V4) run against real/emulated services when present and
69
+ # skip cleanly otherwise, so the default suite needs no infrastructure.
70
+ psycopg = { version = "^3.1", extras = ["binary"] }
71
+ boto3 = "^1.34"
72
+ moto = { version = "^5.0", extras = ["s3", "kms"] }
73
+
74
+ [tool.pytest.ini_options]
75
+ testpaths = ["tests"]
76
+ addopts = "-q"
77
+ timeout = 120
78
+
79
+ [build-system]
80
+ requires = ["poetry-core>=2.0"]
81
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,20 @@
1
+ """OpenWright — the Agent Evidence Layer.
2
+
3
+ OpenWright turns the runtime behavior of AI agents into signed, tamper-evident,
4
+ control-mapped audit evidence: telemetry/SDK signals → a canonical
5
+ ``ComplianceEvent`` → declarative regulatory crosswalks → an append-only Merkle
6
+ log → signed reports → independent verification.
7
+
8
+ Boundary (non-negotiable, see FR-RPT-07 / NFR-COMP-01): OpenWright produces
9
+ *evidence that controls were exercised*. It does NOT assert or imply legal
10
+ compliance, certification, or fitness. Those are judgments reserved for
11
+ qualified auditors and counsel.
12
+ """
13
+
14
+ __version__ = "0.6.0"
15
+
16
+ # Schema version for the canonical event model. Bumped independently of the
17
+ # package version; readers MUST tolerate unknown future fields (DR-04).
18
+ COMPLIANCE_EVENT_SCHEMA_VERSION = "1.0.0"
19
+
20
+ __all__ = ["__version__", "COMPLIANCE_EVENT_SCHEMA_VERSION"]
@@ -0,0 +1,98 @@
1
+ """Pure-Python Ed25519 signature *verification* (RFC 8032).
2
+
3
+ Used as a fallback by the verifier when the ``cryptography`` package is not
4
+ available — e.g. when running the verifier inside a stock WASM Python (Pyodide)
5
+ in the browser (FR-VER-04 [P2]). With this fallback the verifier needs **zero
6
+ third-party dependencies**, only the standard library.
7
+
8
+ This implements verification only (never signing); it follows the cofactorless
9
+ group-equation check ``[S]B == R + [k]A`` from RFC 8032 §5.1.7, which accepts all
10
+ signatures produced by a compliant signer (e.g. ``cryptography``). It is not
11
+ constant-time, which is fine — verification uses only public data.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+
18
+ _P = 2**255 - 19
19
+ _L = 2**252 + 27742317777372353535851937790883648493
20
+
21
+
22
+ def _inv(x: int) -> int:
23
+ return pow(x, _P - 2, _P)
24
+
25
+
26
+ _D = (-121665 * _inv(121666)) % _P
27
+ _I = pow(2, (_P - 1) // 4, _P)
28
+
29
+
30
+ def _xrecover(y: int) -> int:
31
+ xx = (y * y - 1) * _inv(_D * y * y + 1)
32
+ x = pow(xx, (_P + 3) // 8, _P)
33
+ if (x * x - xx) % _P != 0:
34
+ x = (x * _I) % _P
35
+ if x % 2 != 0:
36
+ x = _P - x
37
+ return x
38
+
39
+
40
+ _BY = (4 * _inv(5)) % _P
41
+ _BX = _xrecover(_BY) % _P
42
+ _B = (_BX, _BY)
43
+
44
+
45
+ def _edwards_add(p1, p2):
46
+ x1, y1 = p1
47
+ x2, y2 = p2
48
+ denom = _inv(1 + _D * x1 * x2 * y1 * y2)
49
+ x3 = (x1 * y2 + x2 * y1) * denom % _P
50
+ denom2 = _inv(1 - _D * x1 * x2 * y1 * y2)
51
+ y3 = (y1 * y2 + x1 * x2) * denom2 % _P
52
+ return (x3, y3)
53
+
54
+
55
+ def _scalarmult(point, e: int):
56
+ result = (0, 1) # neutral element
57
+ addend = point
58
+ while e > 0:
59
+ if e & 1:
60
+ result = _edwards_add(result, addend)
61
+ addend = _edwards_add(addend, addend)
62
+ e >>= 1
63
+ return result
64
+
65
+
66
+ def _is_on_curve(point) -> bool:
67
+ x, y = point
68
+ return (-x * x + y * y - 1 - _D * x * x * y * y) % _P == 0
69
+
70
+
71
+ def _decodepoint(s: bytes):
72
+ val = int.from_bytes(s, "little")
73
+ y = val & ((1 << 255) - 1)
74
+ x = _xrecover(y)
75
+ if (x & 1) != ((val >> 255) & 1):
76
+ x = _P - x
77
+ point = (x, y)
78
+ if not _is_on_curve(point):
79
+ raise ValueError("point not on curve")
80
+ return point
81
+
82
+
83
+ def verify(public_key: bytes, signature: bytes, message: bytes) -> bool:
84
+ """Return True iff ``signature`` is a valid Ed25519 signature of ``message``."""
85
+ if len(signature) != 64 or len(public_key) != 32:
86
+ return False
87
+ try:
88
+ r_point = _decodepoint(signature[:32])
89
+ a_point = _decodepoint(public_key)
90
+ except (ValueError, Exception): # noqa: BLE001
91
+ return False
92
+ s = int.from_bytes(signature[32:], "little")
93
+ if s >= _L:
94
+ return False
95
+ h = int.from_bytes(hashlib.sha512(signature[:32] + public_key + message).digest(), "little") % _L
96
+ sb = _scalarmult(_B, s)
97
+ ha = _scalarmult(a_point, h)
98
+ return sb == _edwards_add(r_point, ha)
@@ -0,0 +1,37 @@
1
+ """Source-format adapters (FR-NRM-03).
2
+
3
+ Each adapter isolates one upstream convention so that a change there (e.g. the
4
+ OTel GenAI conventions leaving experimental status — C-01) requires editing only
5
+ that adapter, never the core or the canonical event schema. Adapters are
6
+ deliberately proto-agnostic: they consume plain Python dicts, so the ingest
7
+ layer owns OTLP/protobuf decoding.
8
+ """
9
+
10
+ from .base import SpanData
11
+ from .otel_genai import span_to_event
12
+ from .a2a import reconstruct_provenance
13
+ from .langfuse import apply_langfuse_precedence, has_langfuse_attributes
14
+ from .sarif_in import sarif_to_events
15
+ from .policy import policy_decisions_to_events
16
+ from .receipt import (
17
+ SignedActionReceiptSource,
18
+ ReceiptSource,
19
+ ReceiptVerificationError,
20
+ receipt_to_event,
21
+ sign_receipt,
22
+ )
23
+
24
+ __all__ = [
25
+ "SpanData",
26
+ "span_to_event",
27
+ "reconstruct_provenance",
28
+ "apply_langfuse_precedence",
29
+ "has_langfuse_attributes",
30
+ "sarif_to_events",
31
+ "policy_decisions_to_events",
32
+ "SignedActionReceiptSource",
33
+ "ReceiptSource",
34
+ "ReceiptVerificationError",
35
+ "receipt_to_event",
36
+ "sign_receipt",
37
+ ]
@@ -0,0 +1,71 @@
1
+ """A2A task-provenance reconstruction (FR-ING-05).
2
+
3
+ Verified against the A2A spec (v0.3.0): a ``Task`` has ``id`` and ``contextId``
4
+ but NO parent-task field; cross-task lineage is expressed softly via
5
+ ``Message.referenceTaskIds`` and grouping via ``contextId``. OpenWright therefore
6
+ *reconstructs* a parent/root chain from those signals — the ``parent_task_id``
7
+ and ``root_task_id`` on a :class:`Provenance` are OpenWright-derived, not native
8
+ A2A fields. Absent A2A, provenance degrades to single-event records (A-02).
9
+
10
+ All A2A-origin data is treated as untrusted input (FR-ING-10): IDs are used only
11
+ as opaque correlation keys, never interpreted or executed.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Dict, List, Optional
17
+
18
+ from ..events import Provenance
19
+
20
+
21
+ def reconstruct_provenance(
22
+ task_id: Optional[str],
23
+ context_id: Optional[str] = None,
24
+ reference_task_ids: Optional[List[str]] = None,
25
+ known: Optional[Dict[str, Provenance]] = None,
26
+ ) -> Provenance:
27
+ """Reconstruct parent/root for ``task_id`` from A2A signals.
28
+
29
+ ``known`` maps already-seen task IDs to their reconstructed provenance, used
30
+ to walk the root chain. ``reference_task_ids`` (from the triggering Message)
31
+ yields the parent; the root is the parent's root, or this task if it has none.
32
+ """
33
+ known = known or {}
34
+ parent_task_id = reference_task_ids[0] if reference_task_ids else None
35
+
36
+ if parent_task_id and parent_task_id in known and known[parent_task_id].root_task_id:
37
+ root_task_id = known[parent_task_id].root_task_id
38
+ elif parent_task_id:
39
+ root_task_id = parent_task_id # parent unseen; best-effort root = parent
40
+ else:
41
+ root_task_id = task_id # no parent → this task is its own root
42
+
43
+ return Provenance(
44
+ task_id=task_id,
45
+ context_id=context_id,
46
+ parent_task_id=parent_task_id,
47
+ root_task_id=root_task_id,
48
+ )
49
+
50
+
51
+ # Attribute keys some instrumentations use to carry A2A identifiers on spans.
52
+ A2A_TASK_ID = "a2a.task.id"
53
+ A2A_CONTEXT_ID = "a2a.context.id"
54
+ A2A_REFERENCE_TASK_IDS = "a2a.reference_task_ids"
55
+
56
+
57
+ def provenance_from_attributes(
58
+ attrs: Dict[str, object], known: Optional[Dict[str, Provenance]] = None
59
+ ) -> Optional[Provenance]:
60
+ """Reconstruct provenance from A2A identifiers carried as span attributes."""
61
+ task_id = attrs.get(A2A_TASK_ID)
62
+ if not task_id:
63
+ return None
64
+ refs = attrs.get(A2A_REFERENCE_TASK_IDS)
65
+ ref_list = list(refs) if isinstance(refs, (list, tuple)) else ([refs] if refs else None)
66
+ return reconstruct_provenance(
67
+ str(task_id),
68
+ context_id=str(attrs[A2A_CONTEXT_ID]) if attrs.get(A2A_CONTEXT_ID) else None,
69
+ reference_task_ids=[str(r) for r in ref_list] if ref_list else None,
70
+ known=known,
71
+ )