epi-recorder 2.1.3__py3-none-any.whl → 2.3.0__py3-none-any.whl

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 (45) hide show
  1. epi_analyzer/__init__.py +9 -0
  2. epi_analyzer/detector.py +337 -0
  3. epi_cli/__init__.py +4 -0
  4. epi_cli/__main__.py +4 -0
  5. epi_cli/chat.py +21 -3
  6. epi_cli/debug.py +107 -0
  7. epi_cli/keys.py +4 -0
  8. epi_cli/ls.py +5 -1
  9. epi_cli/main.py +8 -0
  10. epi_cli/record.py +4 -0
  11. epi_cli/run.py +12 -4
  12. epi_cli/verify.py +4 -0
  13. epi_cli/view.py +4 -0
  14. epi_core/__init__.py +5 -1
  15. epi_core/container.py +68 -55
  16. epi_core/redactor.py +4 -0
  17. epi_core/schemas.py +6 -2
  18. epi_core/serialize.py +4 -0
  19. epi_core/storage.py +186 -0
  20. epi_core/trust.py +4 -0
  21. epi_recorder/__init__.py +13 -1
  22. epi_recorder/api.py +211 -5
  23. epi_recorder/async_api.py +151 -0
  24. epi_recorder/bootstrap.py +4 -0
  25. epi_recorder/environment.py +4 -0
  26. epi_recorder/patcher.py +79 -19
  27. epi_recorder/test_import.py +2 -0
  28. epi_recorder/test_script.py +2 -0
  29. epi_recorder/wrappers/__init__.py +16 -0
  30. epi_recorder/wrappers/base.py +79 -0
  31. epi_recorder/wrappers/openai.py +178 -0
  32. epi_recorder-2.3.0.dist-info/METADATA +269 -0
  33. epi_recorder-2.3.0.dist-info/RECORD +41 -0
  34. {epi_recorder-2.1.3.dist-info → epi_recorder-2.3.0.dist-info}/WHEEL +1 -1
  35. epi_recorder-2.3.0.dist-info/licenses/LICENSE +21 -0
  36. {epi_recorder-2.1.3.dist-info → epi_recorder-2.3.0.dist-info}/top_level.txt +1 -0
  37. epi_viewer_static/app.js +113 -7
  38. epi_viewer_static/crypto.js +3 -0
  39. epi_viewer_static/index.html +4 -2
  40. epi_viewer_static/viewer_lite.css +3 -1
  41. epi_postinstall.py +0 -197
  42. epi_recorder-2.1.3.dist-info/METADATA +0 -577
  43. epi_recorder-2.1.3.dist-info/RECORD +0 -34
  44. epi_recorder-2.1.3.dist-info/licenses/LICENSE +0 -201
  45. {epi_recorder-2.1.3.dist-info → epi_recorder-2.3.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,269 @@
1
+ Metadata-Version: 2.4
2
+ Name: epi-recorder
3
+ Version: 2.3.0
4
+ Summary: Verifiable execution evidence for AI systems. Portable, cryptographically signed artifacts.
5
+ Author-email: EPI Labs <mohdibrahim@epilabs.org>
6
+ Maintainer-email: Mohd Ibrahim Afridi <mohdibrahim@epilabs.org>
7
+ License: MIT
8
+ Project-URL: Homepage, https://epilabs.org
9
+ Project-URL: Documentation, https://epilabs.org/docs
10
+ Project-URL: Repository, https://github.com/mohdibrahimaiml/epi-recorder
11
+ Project-URL: Issues, https://github.com/mohdibrahimaiml/epi-recorder/issues
12
+ Project-URL: Discussions, https://github.com/mohdibrahimaiml/epi-recorder/discussions
13
+ Keywords: evidence,forensics,audit,compliance,cryptography,ai,llm,verification,artifact,execution-trace,reproducibility,tamper-evident
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Legal Industry
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Classifier: Topic :: Security :: Cryptography
25
+ Classifier: Topic :: System :: Logging
26
+ Classifier: Typing :: Typed
27
+ Classifier: Framework :: Pydantic
28
+ Classifier: Framework :: Pydantic :: 2
29
+ Requires-Python: >=3.11
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Requires-Dist: pydantic>=2.0.0
33
+ Requires-Dist: cryptography>=41.0.0
34
+ Requires-Dist: cbor2>=5.6.0
35
+ Requires-Dist: typer[all]>=0.12.0
36
+ Requires-Dist: rich>=13.0.0
37
+ Requires-Dist: google-generativeai>=0.4.0
38
+ Provides-Extra: dev
39
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
40
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
41
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
42
+ Requires-Dist: black>=24.0.0; extra == "dev"
43
+ Requires-Dist: ruff>=0.3.0; extra == "dev"
44
+ Dynamic: license-file
45
+
46
+ <p align="center">
47
+ <img src="https://raw.githubusercontent.com/mohdibrahimaiml/epi-recorder/main/docs/assets/logo.png" alt="EPI Logo" width="180"/>
48
+ <br>
49
+ <h1 align="center">EPI</h1>
50
+ <p align="center"><strong>Verifiable Execution Evidence for AI Systems / AI Agents</strong></p>
51
+ <p align="center">
52
+ <em>A portable, cryptographically sealed artifact format for AI execution records.</em>
53
+ </p>
54
+ </p>
55
+
56
+ <p align="center">
57
+ <a href="https://pypi.org/project/epi-recorder/"><img src="https://img.shields.io/pypi/v/epi-recorder?style=for-the-badge&color=00d4ff&label=PyPI" alt="PyPI"/></a>
58
+ <a href="https://github.com/mohdibrahimaiml/epi-recorder"><img src="https://img.shields.io/badge/python-3.11%2B-blue?style=for-the-badge&logo=python&logoColor=white" alt="Python"/></a>
59
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="License"/></a>
60
+ </p>
61
+
62
+ ---
63
+
64
+ ## What is EPI?
65
+
66
+ EPI (Evidence Package for AI) is a **file format** for capturing and verifying AI execution evidence.
67
+
68
+ An `.epi` file is to AI execution what PDF is to documents:
69
+ - **Self-contained** — prompts, responses, environment, viewer — all in one file
70
+ - **Universally viewable** — opens in any browser, no software required
71
+ - **Tamper-evident** — Ed25519 signatures prove the record wasn't altered
72
+
73
+ EPI is designed for **adversarial review**: audits, incident response, compliance, litigation.
74
+
75
+ ---
76
+
77
+ ## Design Guarantees
78
+
79
+ EPI artifacts provide the following guarantees:
80
+
81
+ | Guarantee | Implementation |
82
+ |:----------|:---------------|
83
+ | **Explicitness** | Evidence capture is intentional and reviewable in source code |
84
+ | **Portability** | Single file, no external dependencies, works offline |
85
+ | **Offline Verifiability** | Signature verification requires no network or cloud services |
86
+ | **Adversarial Review** | Format assumes the reviewer does not trust the producer |
87
+
88
+ These are not features. They are **constraints** that define what EPI is.
89
+
90
+ ---
91
+
92
+ ## Quick Start
93
+
94
+ ```bash
95
+ pip install epi-recorder
96
+ ```
97
+
98
+ ### Capture Evidence (Explicit API)
99
+
100
+ ```python
101
+ from epi_recorder import record
102
+
103
+ with record("evidence.epi") as epi:
104
+ response = client.chat.completions.create(model="gpt-4", messages=[...])
105
+ epi.log_llm_call(response) # Explicit capture
106
+ ```
107
+
108
+ ### Capture Evidence (Wrapper Client)
109
+
110
+ ```python
111
+ from epi_recorder import record, wrap_openai
112
+ from openai import OpenAI
113
+
114
+ client = wrap_openai(OpenAI())
115
+
116
+ with record("evidence.epi"):
117
+ response = client.chat.completions.create(...) # Captured via wrapper
118
+ ```
119
+
120
+ ### Verify Evidence
121
+
122
+ ```bash
123
+ epi verify evidence.epi
124
+ ```
125
+
126
+ ---
127
+
128
+ ## The `.epi` Artifact Format
129
+
130
+ An `.epi` file is a ZIP archive with a defined structure. See [docs/EPI-SPEC.md](docs/EPI-SPEC.md) for the full specification.
131
+
132
+ ```
133
+ evidence.epi
134
+ ├── mimetype # "application/epi+zip"
135
+ ├── manifest.json # Metadata + Ed25519 signature
136
+ ├── steps.jsonl # Execution steps (NDJSON)
137
+ ├── env.json # Runtime environment snapshot
138
+ └── viewer/
139
+ └── index.html # Self-contained offline viewer
140
+ ```
141
+
142
+ The embedded viewer allows any recipient to:
143
+ - View the complete execution timeline
144
+ - Verify cryptographic integrity
145
+ - Inspect individual steps
146
+
147
+ No software installation required.
148
+
149
+ ---
150
+
151
+ ## CLI Reference
152
+
153
+ ### Primary Commands
154
+
155
+ | Command | Purpose |
156
+ |:--------|:--------|
157
+ | `epi run <script.py>` | Capture execution evidence to `.epi` |
158
+ | `epi verify <file.epi>` | Verify artifact integrity and signature |
159
+ | `epi view <file.epi>` | Open artifact in browser viewer |
160
+ | `epi keys list` | Manage signing keys |
161
+
162
+ ### Secondary Tools
163
+
164
+ These tools consume evidence artifacts for analysis:
165
+
166
+ | Command | Purpose |
167
+ |:--------|:--------|
168
+ | `epi debug <file.epi>` | Heuristic analysis (loops, errors, inefficiencies) |
169
+ | `epi chat <file.epi>` | Natural language querying via LLM |
170
+
171
+ > **Note:** `debug` and `chat` are convenience tools built on top of the evidence format.
172
+ > They are not part of the core specification.
173
+
174
+ ---
175
+
176
+ ## Cryptographic Properties
177
+
178
+ | Property | Implementation |
179
+ |:---------|:---------------|
180
+ | **Signatures** | Ed25519 (RFC 8032) |
181
+ | **Hashing** | SHA-256 content addressing |
182
+ | **Key Storage** | Local keyring, user-controlled |
183
+ | **Verification** | Client-side, zero external dependencies |
184
+
185
+ Signatures are **optional but recommended**. Unsigned artifacts are still valid but cannot prove origin.
186
+
187
+ ---
188
+
189
+ ## When to Use EPI
190
+
191
+ ### Appropriate Use Cases
192
+
193
+ - Post-incident forensics
194
+ - Compliance documentation
195
+ - Audit trails for autonomous systems
196
+ - Reproducibility evidence for research
197
+ - Litigation-grade execution records
198
+
199
+ ### Not Designed For
200
+
201
+ - Real-time monitoring dashboards
202
+ - High-frequency telemetry
203
+ - System health metrics
204
+ - Application performance monitoring
205
+
206
+ EPI produces **artifacts**, not **streams**.
207
+
208
+ ---
209
+
210
+ ## Supported Providers
211
+
212
+ | Provider | Capture Method |
213
+ |:---------|:---------------|
214
+ | OpenAI | Wrapper client or explicit API |
215
+ | Anthropic | Explicit API |
216
+ | Google Gemini | Explicit API |
217
+ | Any HTTP-based LLM | Explicit API via `log_llm_call()` |
218
+
219
+ EPI does not depend on provider-specific integrations. The explicit API works with any response format.
220
+
221
+ ---
222
+
223
+ ## v2.3.0 — Design Correction
224
+
225
+ This release corrects EPI's evidence capture model.
226
+
227
+ | Before (v2.2.x) | After (v2.3.0) |
228
+ |:----------------|:---------------|
229
+ | Implicit monkey-patching | Explicit capture |
230
+ | Fragile to SDK changes | Stable across versions |
231
+ | Hidden instrumentation | Reviewable in source |
232
+
233
+ **Rationale:** Evidence systems must be intentional. Implicit capture was convenient but violated the explicitness guarantee.
234
+
235
+ **Migration:** Replace implicit capture with `epi.log_llm_call(response)` or `wrap_openai()`.
236
+
237
+ **Legacy mode:** `record(legacy_patching=True)` is deprecated and will be removed in v3.0.
238
+
239
+ ---
240
+
241
+ ## Release History
242
+
243
+ | Version | Date | Summary |
244
+ |:--------|:-----|:--------|
245
+ | **2.3.0** | 2026-02-06 | Explicit capture, wrapper clients, design correction |
246
+ | **2.2.0** | 2026-01-30 | SQLite storage, async support, context isolation |
247
+ | **2.1.3** | 2026-01-24 | Gemini capture, evidence querying |
248
+ | **2.1.0** | 2025-12-15 | Initial release |
249
+
250
+ See [CHANGELOG.md](./CHANGELOG.md) for full details.
251
+
252
+ ---
253
+
254
+ ## Contributing
255
+
256
+ ```bash
257
+ git clone https://github.com/mohdibrahimaiml/epi-recorder.git
258
+ cd epi-recorder
259
+ pip install -e ".[dev]"
260
+ pytest
261
+ ```
262
+
263
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
264
+
265
+ ---
266
+
267
+ ## License
268
+
269
+ MIT License. See [LICENSE](./LICENSE).
@@ -0,0 +1,41 @@
1
+ epi_analyzer/__init__.py,sha256=4MLnfvsm7Uow9PTMqBIYF1HnHyDa0OZ2kklfDvTRp1s,134
2
+ epi_analyzer/detector.py,sha256=JrZ7NGmG0vWb2Vskh-U_S1KYkyVjscUywwu2roe1HCQ,13665
3
+ epi_cli/__init__.py,sha256=KEh3YUH01d0w7B-56gEIgI87jHh-UDuIkxzVSfzl7y4,104
4
+ epi_cli/__main__.py,sha256=HzwyIuqH0lO6pJMApM8ZsXcNzmq9wIKtWM1zJvzVSzY,302
5
+ epi_cli/chat.py,sha256=D7ULAbciCPi_2rcGyoUmSYGGD0ceYYVcw4ekGSKoTQc,7400
6
+ epi_cli/debug.py,sha256=khGJ2xiohQYUgyTBJE5iZM-w1pKA0nou6VJPvmORKF4,3794
7
+ epi_cli/keys.py,sha256=3EZaNc-NvHNWWeHfxKTkZs3bUhOdw7sJ3X2_021__gE,9117
8
+ epi_cli/ls.py,sha256=ijFZdnzb8ncDFmNG7-j_mg7xLkXMJ_Cd_AwIE4tE7gI,5023
9
+ epi_cli/main.py,sha256=NXzJb2dt0R5zLIcaiy1yzdxbRcAbKWv1Oxr4tRZ4vBE,12366
10
+ epi_cli/record.py,sha256=bmUNr2cELwo6qVKbFvWlI2HpIbcGzXcM1MT2-Fs2cxI,7327
11
+ epi_cli/run.py,sha256=JHTL_sm1LN02IiaWBbM7vBwybzrrJbwsQsUxXg13fDY,14376
12
+ epi_cli/verify.py,sha256=9zr5gNH0v70Ngg_5F_JuFZQcUzWQ3YhH9WFlfUS1I0o,8244
13
+ epi_cli/view.py,sha256=EP9takENuZnRllBsxDze9Mm32TGsyxsQaUhlNmUNA_w,4027
14
+ epi_core/__init__.py,sha256=8CTVjxZDI6oy-MMqWTILY9h8rgSZXS8kVzgySympGJU,309
15
+ epi_core/container.py,sha256=Eop4CN3TgCoxRyEWorbjvVBnFaxS4zkccdDwgXQ4eIk,13344
16
+ epi_core/redactor.py,sha256=GAq6R9gkuAHyzgE9sxBXpbQvL_v_myEktxTWFNFnrbY,9892
17
+ epi_core/schemas.py,sha256=iVz6yqVnXEe_mG5wh6Y4ojGHyop_kB5igpKXRkK7fT8,4734
18
+ epi_core/serialize.py,sha256=KB7Z7dfDFh6qq0tlrwjWADOBUV4z32q29Dt2yiniGGg,5691
19
+ epi_core/storage.py,sha256=XEVbdr5xf00LDDJMqCdrZDFvVS-BZ1e1CWzDaJqG0jE,5374
20
+ epi_core/trust.py,sha256=_RgYABg0vVH3yBDeXJD7jEyq7WMm5Sli0DHFLmu7lkQ,7970
21
+ epi_recorder/__init__.py,sha256=tCq2jrJHLxOfO4FPJL3W7F9dRIiOmbNbOZ52cUnqa90,565
22
+ epi_recorder/api.py,sha256=HK10VkaOLxdi-QSw4Jb_NfW-_p0FtXqWQqTJT-rOvlQ,29820
23
+ epi_recorder/async_api.py,sha256=a2WQL8MnJ8uwnLD6unDZxASe5JbywP1V-8gcFyySFM8,4949
24
+ epi_recorder/bootstrap.py,sha256=vk6mKnaHcnanm8SB7dYGPDJ8E2iSBSX3OTQ3zyO-6b0,1851
25
+ epi_recorder/environment.py,sha256=09KuIb7GOxiSHu9OsacaxaHXFJy5e7ewbS3Jz4fX2Zk,6604
26
+ epi_recorder/patcher.py,sha256=U_1MRMhLtcdrYxDdD3Lt6MZ4nolkSiYIa93Vn1Go4yI,20981
27
+ epi_recorder/test_import.py,sha256=_wrlfu0BLtT21AINf1_NugJTvM-RVNKJOyzokMezjO0,462
28
+ epi_recorder/test_script.py,sha256=ot2vRtgvUdeqk6Oj_cz0TZyQN9fUFVHy2E82jdzZUOs,95
29
+ epi_recorder/wrappers/__init__.py,sha256=uG0jSBjM_wYo2_lmEiHzDtJbSgF0XlguaNye-dzVjPY,409
30
+ epi_recorder/wrappers/base.py,sha256=uZNNMJSgkf50J_Z-VYO9HOPk-1A1J00BivvKET4opIk,2641
31
+ epi_recorder/wrappers/openai.py,sha256=Mc5LxUSBQtaFyCh7MG0kwe8Wt0VM17weOCRh0N3vjuY,5977
32
+ epi_recorder-2.3.0.dist-info/licenses/LICENSE,sha256=C9g69QrsraEzIORyleKkUeim8h49747brp68ZUf9_ms,1089
33
+ epi_viewer_static/app.js,sha256=2CkRO3iq4J0QTpFgpfAvzGQDrrAIXGp61cJfIC225Cc,20362
34
+ epi_viewer_static/crypto.js,sha256=2bdANR9tLCPRE9joOih4kKVtptpfRXxERNps4IEhjAQ,19082
35
+ epi_viewer_static/index.html,sha256=sPNXnDTnk0ArVLofdKB3hhd8q-NL1AUmjucytXoythk,3302
36
+ epi_viewer_static/viewer_lite.css,sha256=EGsbTiaSZcnep5GMXm6eKxsfr9oIg_IjEDDI94KI4vc,4695
37
+ epi_recorder-2.3.0.dist-info/METADATA,sha256=9ZGZtvoz7vNiy6e35cB9AQUqRWpNNDQGHPqEZ_bfl1Q,8761
38
+ epi_recorder-2.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
39
+ epi_recorder-2.3.0.dist-info/entry_points.txt,sha256=MfMwqVRx_yMGbuPpiyjz2f8fQp8TUbHmRC1H_bupoyM,41
40
+ epi_recorder-2.3.0.dist-info/top_level.txt,sha256=osrjwlhDfJZSucB-G1u-rF6o0L1OCx2d892gSWr8Iik,77
41
+ epi_recorder-2.3.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 EPI Labs
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.
@@ -1,3 +1,4 @@
1
+ epi_analyzer
1
2
  epi_cli
2
3
  epi_core
3
4
  epi_postinstall
epi_viewer_static/app.js CHANGED
@@ -37,18 +37,20 @@ async function renderTrustBadge(manifest) {
37
37
  `;
38
38
 
39
39
  // Check verification logic availability
40
+ const hasSignature = manifest.signature && manifest.signature !== "null" && manifest.signature.trim() !== "";
41
+
40
42
  if (typeof window.verifyManifestSignature !== 'function') {
41
- renderBadgeResult(false, 'Missing crypto lib', manifest.signature != null);
43
+ renderBadgeResult(false, 'Missing crypto lib', hasSignature);
42
44
  return;
43
45
  }
44
46
 
45
47
  try {
46
48
  const result = await window.verifyManifestSignature(manifest);
47
49
  console.log("Verification Result:", result);
48
- renderBadgeResult(result.valid, result.reason, manifest.signature != null);
50
+ renderBadgeResult(result.valid, result.reason, hasSignature);
49
51
  } catch (e) {
50
52
  console.error("Verification error:", e);
51
- renderBadgeResult(false, e.message, manifest.signature != null);
53
+ renderBadgeResult(false, e.message, hasSignature);
52
54
  }
53
55
  }
54
56
 
@@ -211,7 +213,12 @@ function renderStep(step) {
211
213
  </span>
212
214
  <span class="text-sm font-medium text-gray-900">${kind}</span>
213
215
  </div>
214
- <span class="text-xs text-gray-500">${time}</span>
216
+ <div class="flex items-center space-x-2">
217
+ <span class="text-xs text-gray-500">${time}</span>
218
+ <button onclick='copyStepData(${JSON.stringify(JSON.stringify(content))})' class="text-gray-400 hover:text-blue-600 transition-colors" title="Copy Raw JSON">
219
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path></svg>
220
+ </button>
221
+ </div>
215
222
  `;
216
223
  wrapper.appendChild(header);
217
224
 
@@ -223,6 +230,14 @@ function renderStep(step) {
223
230
  contentDiv.innerHTML = renderLLMRequest(content);
224
231
  } else if (kind === 'llm.response') {
225
232
  contentDiv.innerHTML = renderLLMResponse(content);
233
+ } else if (kind === 'llm.error') {
234
+ contentDiv.innerHTML = renderLLMError(content);
235
+ } else if (kind === 'http.request') {
236
+ contentDiv.innerHTML = renderHTTPRequest(content);
237
+ } else if (kind === 'http.response') {
238
+ contentDiv.innerHTML = renderHTTPResponse(content);
239
+ } else if (kind === 'http.error') {
240
+ contentDiv.innerHTML = renderHTTPError(content);
226
241
  } else if (kind === 'security.redaction') {
227
242
  contentDiv.innerHTML = renderRedaction(content);
228
243
  } else {
@@ -283,7 +298,7 @@ function renderLLMResponse(content) {
283
298
  html += `
284
299
  <div class="chat-bubble mr-auto bg-green-100 text-green-900 rounded-lg px-4 py-2 text-sm">
285
300
  <div class="text-xs font-medium mb-1 uppercase">Assistant</div>
286
- <div class="whitespace-pre-wrap">${escapeHTML(choice.message.content)}</div>
301
+ <div class="whitespace-pre-wrap">${formatMessageContent(choice.message.content)}</div>
287
302
  ${choice.finish_reason ? `<div class="text-xs text-green-700 mt-2">• ${choice.finish_reason}</div>` : ''}
288
303
  </div>
289
304
  `;
@@ -296,7 +311,7 @@ function renderLLMResponse(content) {
296
311
  html += `
297
312
  <div class="mt-3 text-xs text-gray-600 flex items-center space-x-4">
298
313
  <span>📊 ${content.usage.total_tokens} tokens</span>
299
- <span>⚡ ${content.latency_seconds}s</span>
314
+ ${content.latency_seconds ? `<span>⚡ ${content.latency_seconds}s</span>` : ''}
300
315
  </div>
301
316
  `;
302
317
  }
@@ -321,6 +336,74 @@ function renderRedaction(content) {
321
336
  `;
322
337
  }
323
338
 
339
+ // Render LLM error
340
+ function renderLLMError(content) {
341
+ return `
342
+ <div class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm">
343
+ <div class="flex items-center text-red-800">
344
+ <svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
345
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
346
+ </svg>
347
+ <span class="font-medium">LLM Error</span>
348
+ </div>
349
+ <div class="mt-2 text-red-700 font-mono text-xs">
350
+ ${escapeHTML(content.error || content.message || JSON.stringify(content))}
351
+ </div>
352
+ ${content.provider ? `<div class="mt-1 text-xs text-red-600">${content.provider} • ${content.model || 'unknown'}</div>` : ''}
353
+ </div>
354
+ `;
355
+ }
356
+
357
+ // Render HTTP request
358
+ function renderHTTPRequest(content) {
359
+ return `
360
+ <div class="bg-indigo-50 border border-indigo-200 rounded-lg p-3 text-sm">
361
+ <div class="flex items-center text-indigo-800 mb-2">
362
+ <span class="font-medium px-2 py-0.5 bg-indigo-200 rounded text-xs">${content.method || 'GET'}</span>
363
+ <span class="ml-2 font-mono text-xs break-all">${escapeHTML(content.url || '')}</span>
364
+ </div>
365
+ ${content.headers ? `<div class="text-xs text-indigo-600">Headers: ${Object.keys(content.headers).length}</div>` : ''}
366
+ ${content.body ? `<pre class="mt-2 text-xs bg-indigo-100 p-2 rounded overflow-auto max-h-32">${escapeHTML(typeof content.body === 'string' ? content.body : JSON.stringify(content.body, null, 2))}</pre>` : ''}
367
+ </div>
368
+ `;
369
+ }
370
+
371
+ // Render HTTP response
372
+ function renderHTTPResponse(content) {
373
+ const statusColor = (content.status_code >= 200 && content.status_code < 300) ? 'green' :
374
+ (content.status_code >= 400) ? 'red' : 'yellow';
375
+ return `
376
+ <div class="bg-${statusColor}-50 border border-${statusColor}-200 rounded-lg p-3 text-sm">
377
+ <div class="flex items-center text-${statusColor}-800 mb-2">
378
+ <span class="font-medium px-2 py-0.5 bg-${statusColor}-200 rounded text-xs">${content.status_code || '???'}</span>
379
+ <span class="ml-2 text-xs">${content.url || ''}</span>
380
+ ${content.latency_seconds ? `<span class="ml-auto text-xs">⚡ ${content.latency_seconds}s</span>` : ''}
381
+ </div>
382
+ ${content.body ? `<pre class="mt-2 text-xs bg-gray-100 p-2 rounded overflow-auto max-h-32">${escapeHTML(typeof content.body === 'string' ? content.body.slice(0, 500) : JSON.stringify(content.body, null, 2).slice(0, 500))}${(content.body.length > 500) ? '...' : ''}</pre>` : ''}
383
+ </div>
384
+ `;
385
+ }
386
+
387
+ // Render HTTP error
388
+ function renderHTTPError(content) {
389
+ return `
390
+ <div class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm">
391
+ <div class="flex items-center text-red-800">
392
+ <svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
393
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
394
+ </svg>
395
+ <span class="font-medium">HTTP Error</span>
396
+ </div>
397
+ <div class="mt-2 text-red-700">
398
+ <span class="font-mono text-xs">${escapeHTML(content.url || '')}</span>
399
+ </div>
400
+ <div class="mt-1 text-red-600 text-xs">
401
+ ${escapeHTML(content.error || content.message || JSON.stringify(content))}
402
+ </div>
403
+ </div>
404
+ `;
405
+ }
406
+
324
407
  // Escape HTML to prevent XSS
325
408
  function escapeHTML(str) {
326
409
  const div = document.createElement('div');
@@ -348,6 +431,28 @@ function renderTimeline(steps) {
348
431
  }
349
432
  }
350
433
 
434
+ // Helper: Format message content with bolding
435
+ function formatMessageContent(text) {
436
+ if (!text) return '';
437
+ // Escape HTML first
438
+ let escaped = escapeHTML(text);
439
+ // Apply bold formatting for **text**
440
+ return escaped.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
441
+ }
442
+
443
+ // Helper: Copy to clipboard
444
+ window.copyStepData = function (dataStr) {
445
+ try {
446
+ const data = JSON.parse(dataStr); // It was doubly stringified
447
+ navigator.clipboard.writeText(JSON.stringify(data, null, 2)).then(() => {
448
+ // Visual feedback could be added here
449
+ console.log('Copied to clipboard');
450
+ });
451
+ } catch (e) {
452
+ console.error('Copy failed', e);
453
+ }
454
+ };
455
+
351
456
  // Initialize viewer
352
457
  async function init() {
353
458
  const data = loadEPIData();
@@ -374,4 +479,5 @@ if (document.readyState === 'loading') {
374
479
  document.addEventListener('DOMContentLoaded', init);
375
480
  } else {
376
481
  init();
377
- }
482
+ }
483
+
@@ -515,3 +515,6 @@ async function verifyManifestSignature(manifest) {
515
515
  return { valid: false, reason: e.message };
516
516
  }
517
517
  }
518
+
519
+
520
+
@@ -94,7 +94,7 @@
94
94
  <!-- Footer -->
95
95
  <footer class="mt-12 bg-white border-t border-gray-200">
96
96
  <div class="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
97
- EPI v2.1.0 | <span class="font-mono">application/epi+zip</span>
97
+ EPI v2.2.0 | <span class="font-mono">application/epi+zip</span>
98
98
  </div>
99
99
  </footer>
100
100
  </div>
@@ -102,4 +102,6 @@
102
102
  <script src="app.js"></script>
103
103
  </body>
104
104
 
105
- </html>
105
+ </html>
106
+
107
+
@@ -362,4 +362,6 @@ body {
362
362
 
363
363
  .mr-2 {
364
364
  margin-right: 0.5rem;
365
- }
365
+ }
366
+
367
+