agentapproved 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.
- agentapproved-0.1.0/LICENSE +21 -0
- agentapproved-0.1.0/PKG-INFO +111 -0
- agentapproved-0.1.0/README.md +80 -0
- agentapproved-0.1.0/agentapproved/__init__.py +16 -0
- agentapproved-0.1.0/agentapproved/exporter.py +368 -0
- agentapproved-0.1.0/agentapproved/handler.py +513 -0
- agentapproved-0.1.0/agentapproved/hasher.py +126 -0
- agentapproved-0.1.0/agentapproved/mapper.py +345 -0
- agentapproved-0.1.0/agentapproved/schema.py +60 -0
- agentapproved-0.1.0/agentapproved/transport.py +134 -0
- agentapproved-0.1.0/agentapproved.egg-info/PKG-INFO +111 -0
- agentapproved-0.1.0/agentapproved.egg-info/SOURCES.txt +20 -0
- agentapproved-0.1.0/agentapproved.egg-info/dependency_links.txt +1 -0
- agentapproved-0.1.0/agentapproved.egg-info/requires.txt +7 -0
- agentapproved-0.1.0/agentapproved.egg-info/top_level.txt +1 -0
- agentapproved-0.1.0/pyproject.toml +50 -0
- agentapproved-0.1.0/setup.cfg +4 -0
- agentapproved-0.1.0/tests/test_exporter.py +306 -0
- agentapproved-0.1.0/tests/test_handler.py +373 -0
- agentapproved-0.1.0/tests/test_mapper.py +353 -0
- agentapproved-0.1.0/tests/test_signing.py +210 -0
- agentapproved-0.1.0/tests/test_transport.py +312 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PowerBee Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentapproved
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: EU AI Act compliance evidence for AI agents — one-line SDK, tamper-proof audit trail, regulation-mapped reports
|
|
5
|
+
Author-email: Nathan Pemberton <nathan@powerbee.co.uk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://agentapproved.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/agentapproved/agentapproved-python
|
|
9
|
+
Project-URL: Documentation, https://agentapproved.ai/docs
|
|
10
|
+
Project-URL: Issues, https://github.com/agentapproved/agentapproved-python/issues
|
|
11
|
+
Keywords: ai,agents,compliance,eu-ai-act,audit,langchain,governance
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: langchain-core>=0.3.0
|
|
25
|
+
Requires-Dist: uuid-utils>=0.9.0
|
|
26
|
+
Requires-Dist: cryptography>=41.0.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: build; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# AgentApproved
|
|
33
|
+
|
|
34
|
+
**EU AI Act compliance evidence for AI agents.** One-line SDK integration captures every agent action as a tamper-proof, Ed25519-signed audit trail and maps it to specific regulatory requirements.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install agentapproved
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from agentapproved import AgentApprovedHandler, assess_compliance, generate_evidence_packet
|
|
44
|
+
from agentapproved.hasher import get_public_key_hex
|
|
45
|
+
|
|
46
|
+
# 1. Attach to your LangChain agent
|
|
47
|
+
handler = AgentApprovedHandler(agent_id="my-agent", data_dir="./evidence")
|
|
48
|
+
agent = create_agent(..., callbacks=[handler])
|
|
49
|
+
agent.invoke({"input": "What is the return policy?"})
|
|
50
|
+
|
|
51
|
+
# 2. Record human oversight (EU AI Act Art 12(2)(d))
|
|
52
|
+
handler.record_oversight(reviewer_id="jane", decision="approved", reason="Accurate")
|
|
53
|
+
handler.end_session()
|
|
54
|
+
|
|
55
|
+
# 3. Check compliance score
|
|
56
|
+
report = assess_compliance(handler.get_events())
|
|
57
|
+
print(f"EU AI Act Article 12 compliance: {report.overall_score}%")
|
|
58
|
+
for gap in report.gaps:
|
|
59
|
+
print(f" {gap.article}: {gap.remediation}")
|
|
60
|
+
|
|
61
|
+
# 4. Export evidence packet for auditors
|
|
62
|
+
generate_evidence_packet(
|
|
63
|
+
handler.get_events(),
|
|
64
|
+
"./audit-packet",
|
|
65
|
+
organisation="Acme Ltd",
|
|
66
|
+
public_key_hex=get_public_key_hex(handler._public_key),
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## What You Get
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
EU AI Act Article 12 compliance: 100%
|
|
74
|
+
✓ Article 12(1) — Automatic logging capability
|
|
75
|
+
✓ Article 12(2)(a) — Period of each use
|
|
76
|
+
✓ Article 12(2)(b) — Reference database
|
|
77
|
+
✓ Article 12(2)(c) — Input data leading to match
|
|
78
|
+
✓ Article 12(2)(d) — Human oversight verification
|
|
79
|
+
✓ Article 12(3) — Post-market monitoring traceability
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The evidence packet contains:
|
|
83
|
+
- **report.html** — auditor-readable compliance report with article-by-article assessment
|
|
84
|
+
- **evidence.json** — machine-readable event data with compliance mapping
|
|
85
|
+
- **integrity.json** — hash chain and Ed25519 signature verification proof
|
|
86
|
+
|
|
87
|
+
Every event is SHA-256 hash-chained and Ed25519 signed. Tampering with any event breaks the chain. An auditor can independently verify the entire audit trail.
|
|
88
|
+
|
|
89
|
+
## Features
|
|
90
|
+
|
|
91
|
+
- **One-line integration** — `callbacks=[handler]` on any LangChain agent
|
|
92
|
+
- **All LangChain events** — LLM calls, tool use, RAG retrieval, agent decisions, chat models
|
|
93
|
+
- **EU AI Act Article 12 mapping** — automatic compliance scoring with remediation guidance
|
|
94
|
+
- **Tamper-proof** — SHA-256 hash chain + Ed25519 signatures on every event
|
|
95
|
+
- **Human oversight capture** — `record_oversight()` satisfies Article 12(2)(d)
|
|
96
|
+
- **Self-contained evidence packets** — HTML report + JSON data + integrity proof
|
|
97
|
+
- **Local-first** — events persist to local JSON files, no cloud required
|
|
98
|
+
- **Never crashes your agent** — all errors swallowed, logging only
|
|
99
|
+
|
|
100
|
+
## Requirements
|
|
101
|
+
|
|
102
|
+
- Python 3.10+
|
|
103
|
+
- LangChain (`langchain-core >= 0.3.0`)
|
|
104
|
+
|
|
105
|
+
## Documentation
|
|
106
|
+
|
|
107
|
+
Full documentation at [agentapproved.ai/docs](https://agentapproved.ai/docs)
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# AgentApproved
|
|
2
|
+
|
|
3
|
+
**EU AI Act compliance evidence for AI agents.** One-line SDK integration captures every agent action as a tamper-proof, Ed25519-signed audit trail and maps it to specific regulatory requirements.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pip install agentapproved
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from agentapproved import AgentApprovedHandler, assess_compliance, generate_evidence_packet
|
|
13
|
+
from agentapproved.hasher import get_public_key_hex
|
|
14
|
+
|
|
15
|
+
# 1. Attach to your LangChain agent
|
|
16
|
+
handler = AgentApprovedHandler(agent_id="my-agent", data_dir="./evidence")
|
|
17
|
+
agent = create_agent(..., callbacks=[handler])
|
|
18
|
+
agent.invoke({"input": "What is the return policy?"})
|
|
19
|
+
|
|
20
|
+
# 2. Record human oversight (EU AI Act Art 12(2)(d))
|
|
21
|
+
handler.record_oversight(reviewer_id="jane", decision="approved", reason="Accurate")
|
|
22
|
+
handler.end_session()
|
|
23
|
+
|
|
24
|
+
# 3. Check compliance score
|
|
25
|
+
report = assess_compliance(handler.get_events())
|
|
26
|
+
print(f"EU AI Act Article 12 compliance: {report.overall_score}%")
|
|
27
|
+
for gap in report.gaps:
|
|
28
|
+
print(f" {gap.article}: {gap.remediation}")
|
|
29
|
+
|
|
30
|
+
# 4. Export evidence packet for auditors
|
|
31
|
+
generate_evidence_packet(
|
|
32
|
+
handler.get_events(),
|
|
33
|
+
"./audit-packet",
|
|
34
|
+
organisation="Acme Ltd",
|
|
35
|
+
public_key_hex=get_public_key_hex(handler._public_key),
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## What You Get
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
EU AI Act Article 12 compliance: 100%
|
|
43
|
+
✓ Article 12(1) — Automatic logging capability
|
|
44
|
+
✓ Article 12(2)(a) — Period of each use
|
|
45
|
+
✓ Article 12(2)(b) — Reference database
|
|
46
|
+
✓ Article 12(2)(c) — Input data leading to match
|
|
47
|
+
✓ Article 12(2)(d) — Human oversight verification
|
|
48
|
+
✓ Article 12(3) — Post-market monitoring traceability
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The evidence packet contains:
|
|
52
|
+
- **report.html** — auditor-readable compliance report with article-by-article assessment
|
|
53
|
+
- **evidence.json** — machine-readable event data with compliance mapping
|
|
54
|
+
- **integrity.json** — hash chain and Ed25519 signature verification proof
|
|
55
|
+
|
|
56
|
+
Every event is SHA-256 hash-chained and Ed25519 signed. Tampering with any event breaks the chain. An auditor can independently verify the entire audit trail.
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
|
|
60
|
+
- **One-line integration** — `callbacks=[handler]` on any LangChain agent
|
|
61
|
+
- **All LangChain events** — LLM calls, tool use, RAG retrieval, agent decisions, chat models
|
|
62
|
+
- **EU AI Act Article 12 mapping** — automatic compliance scoring with remediation guidance
|
|
63
|
+
- **Tamper-proof** — SHA-256 hash chain + Ed25519 signatures on every event
|
|
64
|
+
- **Human oversight capture** — `record_oversight()` satisfies Article 12(2)(d)
|
|
65
|
+
- **Self-contained evidence packets** — HTML report + JSON data + integrity proof
|
|
66
|
+
- **Local-first** — events persist to local JSON files, no cloud required
|
|
67
|
+
- **Never crashes your agent** — all errors swallowed, logging only
|
|
68
|
+
|
|
69
|
+
## Requirements
|
|
70
|
+
|
|
71
|
+
- Python 3.10+
|
|
72
|
+
- LangChain (`langchain-core >= 0.3.0`)
|
|
73
|
+
|
|
74
|
+
## Documentation
|
|
75
|
+
|
|
76
|
+
Full documentation at [agentapproved.ai/docs](https://agentapproved.ai/docs)
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .exporter import generate_evidence_packet
|
|
2
|
+
from .handler import AgentApprovedHandler
|
|
3
|
+
from .mapper import ComplianceReport, assess_compliance
|
|
4
|
+
from .schema import EvidenceEvent
|
|
5
|
+
from .transport import LocalTransport, load_session_file, verify_session_file
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AgentApprovedHandler",
|
|
9
|
+
"ComplianceReport",
|
|
10
|
+
"EvidenceEvent",
|
|
11
|
+
"LocalTransport",
|
|
12
|
+
"assess_compliance",
|
|
13
|
+
"generate_evidence_packet",
|
|
14
|
+
"load_session_file",
|
|
15
|
+
"verify_session_file",
|
|
16
|
+
]
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Evidence packet generator — the auditor-facing export.
|
|
2
|
+
|
|
3
|
+
Generates a self-contained evidence bundle:
|
|
4
|
+
- evidence.json Machine-readable: all events + metadata + integrity proof
|
|
5
|
+
- report.html Human-readable: article-by-article compliance report
|
|
6
|
+
- integrity.json Standalone chain + signature verification data
|
|
7
|
+
|
|
8
|
+
All three files are self-contained. An auditor can verify integrity
|
|
9
|
+
without access to the AgentApproved platform.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import hashlib
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from .hasher import get_public_key_hex, verify_signature
|
|
21
|
+
from .mapper import ComplianceReport, assess_compliance
|
|
22
|
+
from .schema import EvidenceEvent
|
|
23
|
+
from .transport import verify_session_file
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── Evidence Packet ─────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def generate_evidence_packet(
|
|
30
|
+
events: list[EvidenceEvent],
|
|
31
|
+
output_dir: str | Path,
|
|
32
|
+
organisation: str = "Organisation Name",
|
|
33
|
+
public_key_hex: str = "",
|
|
34
|
+
) -> Path:
|
|
35
|
+
"""Generate a complete evidence packet from a list of events.
|
|
36
|
+
|
|
37
|
+
Creates output_dir/ with:
|
|
38
|
+
evidence.json — full event data + metadata
|
|
39
|
+
report.html — human-readable compliance report
|
|
40
|
+
integrity.json — chain verification proof
|
|
41
|
+
|
|
42
|
+
Returns the output_dir Path.
|
|
43
|
+
"""
|
|
44
|
+
output_dir = Path(output_dir)
|
|
45
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
report = assess_compliance(events)
|
|
48
|
+
chain_valid, chain_count = _verify_chain(events, public_key_hex)
|
|
49
|
+
|
|
50
|
+
# 1. Machine-readable evidence
|
|
51
|
+
evidence_doc = _build_evidence_json(events, report, public_key_hex, organisation)
|
|
52
|
+
(output_dir / "evidence.json").write_text(
|
|
53
|
+
json.dumps(evidence_doc, indent=2, sort_keys=True, default=str),
|
|
54
|
+
encoding="utf-8",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 2. Integrity proof
|
|
58
|
+
integrity_doc = _build_integrity_json(events, chain_valid, public_key_hex)
|
|
59
|
+
(output_dir / "integrity.json").write_text(
|
|
60
|
+
json.dumps(integrity_doc, indent=2, sort_keys=True, default=str),
|
|
61
|
+
encoding="utf-8",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 3. Human-readable HTML report
|
|
65
|
+
html = _build_report_html(report, events, chain_valid, public_key_hex, organisation)
|
|
66
|
+
(output_dir / "report.html").write_text(html, encoding="utf-8")
|
|
67
|
+
|
|
68
|
+
return output_dir
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── JSON Builders ───────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _build_evidence_json(
|
|
75
|
+
events: list[EvidenceEvent],
|
|
76
|
+
report: ComplianceReport,
|
|
77
|
+
public_key_hex: str,
|
|
78
|
+
organisation: str,
|
|
79
|
+
) -> dict:
|
|
80
|
+
period_start = events[0].timestamp if events else ""
|
|
81
|
+
period_end = events[-1].timestamp if events else ""
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"format": "agentapproved-evidence-v1",
|
|
85
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
86
|
+
"organisation": organisation,
|
|
87
|
+
"period": {"start": period_start, "end": period_end},
|
|
88
|
+
"public_key": public_key_hex,
|
|
89
|
+
"compliance": report.to_dict(),
|
|
90
|
+
"event_count": len(events),
|
|
91
|
+
"events": [e.to_dict() for e in events],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _build_integrity_json(
|
|
96
|
+
events: list[EvidenceEvent],
|
|
97
|
+
chain_valid: bool,
|
|
98
|
+
public_key_hex: str,
|
|
99
|
+
) -> dict:
|
|
100
|
+
return {
|
|
101
|
+
"format": "agentapproved-integrity-v1",
|
|
102
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
103
|
+
"chain_valid": chain_valid,
|
|
104
|
+
"event_count": len(events),
|
|
105
|
+
"chain_start": events[0].previous_hash if events else "GENESIS",
|
|
106
|
+
"chain_end": events[-1].event_hash if events else "",
|
|
107
|
+
"public_key": public_key_hex,
|
|
108
|
+
"signatures_present": all(e.signature for e in events),
|
|
109
|
+
"events_summary": [
|
|
110
|
+
{
|
|
111
|
+
"sequence": e.sequence_number,
|
|
112
|
+
"event_id": e.event_id,
|
|
113
|
+
"action_type": e.action_type,
|
|
114
|
+
"event_hash": e.event_hash,
|
|
115
|
+
"signature": e.signature[:16] + "..." if e.signature else "",
|
|
116
|
+
}
|
|
117
|
+
for e in events
|
|
118
|
+
],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ── Chain Verification ──────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _verify_chain(
|
|
126
|
+
events: list[EvidenceEvent], public_key_hex: str
|
|
127
|
+
) -> tuple[bool, int]:
|
|
128
|
+
"""Verify hash chain + signatures in-memory."""
|
|
129
|
+
from .hasher import compute_event_hash, public_key_from_hex
|
|
130
|
+
|
|
131
|
+
pub_key = None
|
|
132
|
+
if public_key_hex:
|
|
133
|
+
try:
|
|
134
|
+
pub_key = public_key_from_hex(public_key_hex)
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
for i, event in enumerate(events):
|
|
139
|
+
expected_prev = "GENESIS" if i == 0 else events[i - 1].event_hash
|
|
140
|
+
if event.previous_hash != expected_prev:
|
|
141
|
+
return False, i
|
|
142
|
+
computed = compute_event_hash(event.to_hashable_dict())
|
|
143
|
+
if computed != event.event_hash:
|
|
144
|
+
return False, i
|
|
145
|
+
if pub_key and event.signature:
|
|
146
|
+
if not verify_signature(event.event_hash, event.signature, pub_key):
|
|
147
|
+
return False, i
|
|
148
|
+
|
|
149
|
+
return True, len(events)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ── HTML Report Builder ─────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
_STATUS_ICONS = {
|
|
156
|
+
"satisfied": "✓", # ✓
|
|
157
|
+
"partial": "⚠", # ⚠
|
|
158
|
+
"missing": "✗", # ✗
|
|
159
|
+
"not_applicable": "—", # —
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_STATUS_COLORS = {
|
|
163
|
+
"satisfied": "#16a34a",
|
|
164
|
+
"partial": "#d97706",
|
|
165
|
+
"missing": "#dc2626",
|
|
166
|
+
"not_applicable": "#6b7280",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_STATUS_BG = {
|
|
170
|
+
"satisfied": "#f0fdf4",
|
|
171
|
+
"partial": "#fffbeb",
|
|
172
|
+
"missing": "#fef2f2",
|
|
173
|
+
"not_applicable": "#f9fafb",
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _build_report_html(
|
|
178
|
+
report: ComplianceReport,
|
|
179
|
+
events: list[EvidenceEvent],
|
|
180
|
+
chain_valid: bool,
|
|
181
|
+
public_key_hex: str,
|
|
182
|
+
organisation: str,
|
|
183
|
+
) -> str:
|
|
184
|
+
period_start = events[0].timestamp[:10] if events else "N/A"
|
|
185
|
+
period_end = events[-1].timestamp[:10] if events else "N/A"
|
|
186
|
+
generated = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
187
|
+
|
|
188
|
+
# Event type breakdown
|
|
189
|
+
type_counts: dict[str, int] = {}
|
|
190
|
+
for e in events:
|
|
191
|
+
type_counts[e.action_type] = type_counts.get(e.action_type, 0) + 1
|
|
192
|
+
type_rows = "\n".join(
|
|
193
|
+
f"<tr><td>{t}</td><td style='text-align:right'>{c}</td></tr>"
|
|
194
|
+
for t, c in sorted(type_counts.items(), key=lambda x: -x[1])
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Requirement rows
|
|
198
|
+
req_rows = ""
|
|
199
|
+
for req in report.requirements:
|
|
200
|
+
icon = _STATUS_ICONS[req.status]
|
|
201
|
+
color = _STATUS_COLORS[req.status]
|
|
202
|
+
bg = _STATUS_BG[req.status]
|
|
203
|
+
remediation_html = ""
|
|
204
|
+
if req.remediation:
|
|
205
|
+
remediation_html = f"""
|
|
206
|
+
<div style="margin-top:8px;padding:10px;background:#f8f9fa;
|
|
207
|
+
border-left:3px solid {color};font-size:13px;color:#374151">
|
|
208
|
+
<strong>Remediation:</strong> {_esc(req.remediation)}
|
|
209
|
+
</div>"""
|
|
210
|
+
|
|
211
|
+
req_rows += f"""
|
|
212
|
+
<div style="margin-bottom:16px;padding:16px;background:{bg};
|
|
213
|
+
border:1px solid #e5e7eb;border-radius:8px">
|
|
214
|
+
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
215
|
+
<div>
|
|
216
|
+
<span style="font-size:20px;color:{color};margin-right:8px">{icon}</span>
|
|
217
|
+
<strong>{_esc(req.article)}</strong> — {_esc(req.title)}
|
|
218
|
+
</div>
|
|
219
|
+
<span style="background:{color};color:white;padding:2px 10px;
|
|
220
|
+
border-radius:12px;font-size:12px;font-weight:600;
|
|
221
|
+
text-transform:uppercase">{req.status}</span>
|
|
222
|
+
</div>
|
|
223
|
+
<p style="margin:8px 0 0 30px;color:#4b5563;font-size:14px">
|
|
224
|
+
{_esc(req.description)}
|
|
225
|
+
</p>
|
|
226
|
+
<p style="margin:4px 0 0 30px;color:#6b7280;font-size:13px">
|
|
227
|
+
Evidence events: {req.evidence_count}
|
|
228
|
+
</p>
|
|
229
|
+
{remediation_html}
|
|
230
|
+
</div>"""
|
|
231
|
+
|
|
232
|
+
# Score color
|
|
233
|
+
if report.overall_score >= 80:
|
|
234
|
+
score_color = "#16a34a"
|
|
235
|
+
elif report.overall_score >= 50:
|
|
236
|
+
score_color = "#d97706"
|
|
237
|
+
else:
|
|
238
|
+
score_color = "#dc2626"
|
|
239
|
+
|
|
240
|
+
chain_badge = (
|
|
241
|
+
'<span style="background:#16a34a;color:white;padding:2px 10px;'
|
|
242
|
+
'border-radius:12px;font-size:13px">VERIFIED</span>'
|
|
243
|
+
if chain_valid
|
|
244
|
+
else '<span style="background:#dc2626;color:white;padding:2px 10px;'
|
|
245
|
+
'border-radius:12px;font-size:13px">BROKEN</span>'
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return f"""<!DOCTYPE html>
|
|
249
|
+
<html lang="en">
|
|
250
|
+
<head>
|
|
251
|
+
<meta charset="UTF-8">
|
|
252
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
253
|
+
<title>EU AI Act Compliance Evidence — AgentApproved</title>
|
|
254
|
+
<style>
|
|
255
|
+
* {{ margin:0; padding:0; box-sizing:border-box; }}
|
|
256
|
+
body {{ font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
|
257
|
+
color:#111827; background:#ffffff; max-width:800px; margin:0 auto;
|
|
258
|
+
padding:40px 24px; line-height:1.6; }}
|
|
259
|
+
h1 {{ font-size:24px; margin-bottom:4px; }}
|
|
260
|
+
h2 {{ font-size:18px; margin:32px 0 16px; padding-bottom:8px;
|
|
261
|
+
border-bottom:2px solid #e5e7eb; }}
|
|
262
|
+
table {{ width:100%; border-collapse:collapse; margin:12px 0; }}
|
|
263
|
+
th, td {{ padding:8px 12px; text-align:left; border-bottom:1px solid #e5e7eb;
|
|
264
|
+
font-size:14px; }}
|
|
265
|
+
th {{ background:#f9fafb; font-weight:600; color:#374151; }}
|
|
266
|
+
.meta {{ color:#6b7280; font-size:14px; }}
|
|
267
|
+
@media print {{
|
|
268
|
+
body {{ padding:20px; }}
|
|
269
|
+
h2 {{ break-before:auto; }}
|
|
270
|
+
}}
|
|
271
|
+
</style>
|
|
272
|
+
</head>
|
|
273
|
+
<body>
|
|
274
|
+
|
|
275
|
+
<div style="text-align:center;margin-bottom:32px">
|
|
276
|
+
<div style="font-size:13px;text-transform:uppercase;letter-spacing:2px;
|
|
277
|
+
color:#6b7280;margin-bottom:8px">AgentApproved</div>
|
|
278
|
+
<h1>EU AI Act Article 12<br>Compliance Evidence Report</h1>
|
|
279
|
+
<div class="meta" style="margin-top:12px">
|
|
280
|
+
<div><strong>Organisation:</strong> {_esc(organisation)}</div>
|
|
281
|
+
<div><strong>Period:</strong> {period_start} to {period_end}</div>
|
|
282
|
+
<div><strong>Generated:</strong> {generated}</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div style="text-align:center;margin:24px 0 32px">
|
|
287
|
+
<div style="display:inline-block;padding:20px 40px;border:3px solid {score_color};
|
|
288
|
+
border-radius:16px">
|
|
289
|
+
<div style="font-size:48px;font-weight:700;color:{score_color}">{report.overall_score}%</div>
|
|
290
|
+
<div style="font-size:14px;color:#6b7280">Article 12 Compliance</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div style="display:flex;gap:12px;justify-content:center;margin-bottom:32px;flex-wrap:wrap">
|
|
295
|
+
<div style="padding:8px 16px;background:#f0fdf4;border-radius:8px;text-align:center">
|
|
296
|
+
<div style="font-size:20px;font-weight:700;color:#16a34a">{report.satisfied}</div>
|
|
297
|
+
<div style="font-size:12px;color:#6b7280">Satisfied</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div style="padding:8px 16px;background:#fffbeb;border-radius:8px;text-align:center">
|
|
300
|
+
<div style="font-size:20px;font-weight:700;color:#d97706">{report.partial}</div>
|
|
301
|
+
<div style="font-size:12px;color:#6b7280">Partial</div>
|
|
302
|
+
</div>
|
|
303
|
+
<div style="padding:8px 16px;background:#fef2f2;border-radius:8px;text-align:center">
|
|
304
|
+
<div style="font-size:20px;font-weight:700;color:#dc2626">{report.missing}</div>
|
|
305
|
+
<div style="font-size:12px;color:#6b7280">Missing</div>
|
|
306
|
+
</div>
|
|
307
|
+
<div style="padding:8px 16px;background:#f9fafb;border-radius:8px;text-align:center">
|
|
308
|
+
<div style="font-size:20px;font-weight:700;color:#6b7280">{report.not_applicable}</div>
|
|
309
|
+
<div style="font-size:12px;color:#6b7280">N/A</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<h2>Article-by-Article Assessment</h2>
|
|
314
|
+
{req_rows}
|
|
315
|
+
|
|
316
|
+
<h2>Event Summary</h2>
|
|
317
|
+
<table>
|
|
318
|
+
<tr><th>Metric</th><th style="text-align:right">Value</th></tr>
|
|
319
|
+
<tr><td>Total events captured</td><td style="text-align:right">{len(events)}</td></tr>
|
|
320
|
+
<tr><td>Sessions</td><td style="text-align:right">{len(set(e.session_id for e in events))}</td></tr>
|
|
321
|
+
<tr><td>Unique agents</td>
|
|
322
|
+
<td style="text-align:right">{len(set(e.actor_id for e in events if e.actor_type == 'agent'))}</td></tr>
|
|
323
|
+
<tr><td>Human oversight events</td>
|
|
324
|
+
<td style="text-align:right">{sum(1 for e in events if e.action_type == 'human_oversight')}</td></tr>
|
|
325
|
+
</table>
|
|
326
|
+
|
|
327
|
+
<h2>Event Type Breakdown</h2>
|
|
328
|
+
<table>
|
|
329
|
+
<tr><th>Event Type</th><th style="text-align:right">Count</th></tr>
|
|
330
|
+
{type_rows}
|
|
331
|
+
</table>
|
|
332
|
+
|
|
333
|
+
<h2>Integrity Verification</h2>
|
|
334
|
+
<table>
|
|
335
|
+
<tr><td>Hash chain status</td><td style="text-align:right">{chain_badge}</td></tr>
|
|
336
|
+
<tr><td>Events in chain</td><td style="text-align:right">{len(events)}</td></tr>
|
|
337
|
+
<tr><td>Chain start</td>
|
|
338
|
+
<td style="text-align:right;font-family:monospace;font-size:12px">
|
|
339
|
+
{events[0].previous_hash if events else 'N/A'}</td></tr>
|
|
340
|
+
<tr><td>Chain end</td>
|
|
341
|
+
<td style="text-align:right;font-family:monospace;font-size:12px">
|
|
342
|
+
{events[-1].event_hash[:32] + '...' if events else 'N/A'}</td></tr>
|
|
343
|
+
<tr><td>Signatures present</td>
|
|
344
|
+
<td style="text-align:right">{'Yes' if all(e.signature for e in events) else 'No'}</td></tr>
|
|
345
|
+
<tr><td>Public key</td>
|
|
346
|
+
<td style="text-align:right;font-family:monospace;font-size:12px">
|
|
347
|
+
{public_key_hex[:32] + '...' if public_key_hex else 'Not available'}</td></tr>
|
|
348
|
+
</table>
|
|
349
|
+
|
|
350
|
+
<div style="margin-top:40px;padding:16px;background:#f9fafb;border-radius:8px;
|
|
351
|
+
font-size:12px;color:#6b7280;text-align:center">
|
|
352
|
+
This report was generated by <strong>AgentApproved</strong> (agentapproved.ai).
|
|
353
|
+
Evidence integrity can be verified independently using the accompanying
|
|
354
|
+
<code>evidence.json</code> and <code>integrity.json</code> files.
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
</body>
|
|
358
|
+
</html>"""
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _esc(text: str) -> str:
|
|
362
|
+
"""Basic HTML escaping."""
|
|
363
|
+
return (
|
|
364
|
+
text.replace("&", "&")
|
|
365
|
+
.replace("<", "<")
|
|
366
|
+
.replace(">", ">")
|
|
367
|
+
.replace('"', """)
|
|
368
|
+
)
|