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.
@@ -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": "&#10003;", # ✓
157
+ "partial": "&#9888;", # ⚠
158
+ "missing": "&#10007;", # ✗
159
+ "not_applicable": "&#8212;", # —
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> &mdash; {_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("&", "&amp;")
365
+ .replace("<", "&lt;")
366
+ .replace(">", "&gt;")
367
+ .replace('"', "&quot;")
368
+ )