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.
- openwright_core-0.6.0/LICENSE +52 -0
- openwright_core-0.6.0/PKG-INFO +174 -0
- openwright_core-0.6.0/README.pypi.md +137 -0
- openwright_core-0.6.0/pyproject.toml +81 -0
- openwright_core-0.6.0/src/openwright/__init__.py +20 -0
- openwright_core-0.6.0/src/openwright/_ed25519_pure.py +98 -0
- openwright_core-0.6.0/src/openwright/adapters/__init__.py +37 -0
- openwright_core-0.6.0/src/openwright/adapters/a2a.py +71 -0
- openwright_core-0.6.0/src/openwright/adapters/base.py +24 -0
- openwright_core-0.6.0/src/openwright/adapters/langfuse.py +56 -0
- openwright_core-0.6.0/src/openwright/adapters/otel_genai.py +180 -0
- openwright_core-0.6.0/src/openwright/adapters/policy.py +63 -0
- openwright_core-0.6.0/src/openwright/adapters/receipt.py +198 -0
- openwright_core-0.6.0/src/openwright/adapters/sarif_in.py +68 -0
- openwright_core-0.6.0/src/openwright/anchor.py +134 -0
- openwright_core-0.6.0/src/openwright/authz.py +68 -0
- openwright_core-0.6.0/src/openwright/browser_verifier.py +197 -0
- openwright_core-0.6.0/src/openwright/canonical.py +119 -0
- openwright_core-0.6.0/src/openwright/checkpoint_store.py +140 -0
- openwright_core-0.6.0/src/openwright/cli.py +445 -0
- openwright_core-0.6.0/src/openwright/connectors/__init__.py +236 -0
- openwright_core-0.6.0/src/openwright/connectors/builtin.py +88 -0
- openwright_core-0.6.0/src/openwright/crosswalk.py +372 -0
- openwright_core-0.6.0/src/openwright/crosswalk_loader.py +53 -0
- openwright_core-0.6.0/src/openwright/crosswalks/CHANGELOG.md +47 -0
- openwright_core-0.6.0/src/openwright/crosswalks/__init__.py +7 -0
- openwright_core-0.6.0/src/openwright/crosswalks/eu_ai_act.yaml +294 -0
- openwright_core-0.6.0/src/openwright/crosswalks/eu_ai_act_v1.yaml +107 -0
- openwright_core-0.6.0/src/openwright/crosswalks/gdpr.yaml +304 -0
- openwright_core-0.6.0/src/openwright/crosswalks/iso_42001.yaml +209 -0
- openwright_core-0.6.0/src/openwright/crosswalks/nist_ai_rmf.yaml +204 -0
- openwright_core-0.6.0/src/openwright/crosswalks/soc2.yaml +246 -0
- openwright_core-0.6.0/src/openwright/dashboard.py +100 -0
- openwright_core-0.6.0/src/openwright/demo.py +270 -0
- openwright_core-0.6.0/src/openwright/events.py +210 -0
- openwright_core-0.6.0/src/openwright/identity.py +61 -0
- openwright_core-0.6.0/src/openwright/ingest/__init__.py +13 -0
- openwright_core-0.6.0/src/openwright/ingest/durable.py +144 -0
- openwright_core-0.6.0/src/openwright/ingest/fanout.py +38 -0
- openwright_core-0.6.0/src/openwright/ingest/grpc_server.py +44 -0
- openwright_core-0.6.0/src/openwright/ingest/http_server.py +163 -0
- openwright_core-0.6.0/src/openwright/ingest/otlp_common.py +69 -0
- openwright_core-0.6.0/src/openwright/ingest/pipeline.py +307 -0
- openwright_core-0.6.0/src/openwright/ledger.py +479 -0
- openwright_core-0.6.0/src/openwright/merkle.py +262 -0
- openwright_core-0.6.0/src/openwright/report.py +388 -0
- openwright_core-0.6.0/src/openwright/sbom.py +42 -0
- openwright_core-0.6.0/src/openwright/scheduler.py +97 -0
- openwright_core-0.6.0/src/openwright/sdk.py +297 -0
- openwright_core-0.6.0/src/openwright/signing.py +343 -0
- openwright_core-0.6.0/src/openwright/spec.py +109 -0
- openwright_core-0.6.0/src/openwright/vault.py +55 -0
- openwright_core-0.6.0/src/openwright/verify.py +392 -0
- openwright_core-0.6.0/src/openwright/web_demo.py +275 -0
- openwright_core-0.6.0/src/openwright/witness.py +90 -0
- 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
|
+
)
|