epi-recorder 2.1.0__py3-none-any.whl → 2.1.2__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.
epi_cli/__main__.py CHANGED
@@ -1,5 +1,10 @@
1
1
  """
2
- EPI CLI entry point for python -m epi_cli
2
+ EPI CLI - Main entry point for python -m epi_cli
3
+
4
+ This allows users to run the CLI even if 'epi' is not in PATH:
5
+ python -m epi_cli run script.py
6
+ python -m epi_cli view recording.epi
7
+ etc.
3
8
  """
4
9
  from epi_cli.main import cli_main
5
10
 
epi_cli/main.py CHANGED
@@ -231,43 +231,79 @@ def doctor():
231
231
  """
232
232
  [Doctor] Self-healing doctor. Fixes common issues silently.
233
233
  """
234
- console.print("\n[bold blue]EPI Doctor[/bold blue]\n")
234
+ console.print("\n[bold blue]EPI Doctor - System Health Check[/bold blue]\n")
235
235
 
236
236
  issues = 0
237
+ fixed = 0
237
238
 
238
239
  # Check 1: Keys
239
240
  console.print("1. Security Keys: ", end="")
240
241
  from epi_cli.keys import generate_default_keypair_if_missing
241
242
  if generate_default_keypair_if_missing(console_output=False):
242
- console.print("[green]FIXED (Generated)[/green]")
243
- issues += 1
243
+ console.print("[green][OK] FIXED (Generated)[/green]")
244
+ fixed += 1
244
245
  else:
245
- console.print("[green]OK[/green]")
246
+ console.print("[green][OK][/green]")
246
247
 
247
248
  # Check 2: Command on PATH
248
249
  console.print("2. 'epi' command: ", end="")
249
250
  import shutil
250
251
  if shutil.which("epi"):
251
- console.print("[green]OK[/green]")
252
+ console.print("[green][OK][/green]")
252
253
  else:
253
- console.print("[red]MISSING[/red]")
254
- console.print(" [yellow]Run: python epi_setup.py[/yellow]")
254
+ console.print("[red][X] NOT IN PATH[/red]")
255
255
  issues += 1
256
+
257
+ # Try to auto-fix on Windows
258
+ import platform
259
+ if platform.system() == "Windows":
260
+ console.print(" [cyan]→ Attempting automatic PATH fix...[/cyan]")
261
+ try:
262
+ import epi_postinstall
263
+ from pathlib import Path
264
+
265
+ scripts_dir = epi_postinstall.get_scripts_dir()
266
+ if scripts_dir and scripts_dir.exists():
267
+ console.print(f" [dim]Scripts directory: {scripts_dir}[/dim]")
268
+
269
+ if epi_postinstall.add_to_user_path_windows(scripts_dir):
270
+ console.print(" [green][OK] PATH updated successfully![/green]")
271
+ console.print(" [yellow][!] Please restart your terminal for changes to take effect[/yellow]")
272
+ fixed += 1
273
+ else:
274
+ console.print(" [yellow][!] Could not update PATH automatically[/yellow]")
275
+ console.print(" [dim]Manual fix: Use 'python -m epi_cli' instead[/dim]")
276
+ else:
277
+ console.print(" [red][X] Could not locate Scripts directory[/red]")
278
+ except Exception as e:
279
+ console.print(f" [red][X] Auto-fix failed: {e}[/red]")
280
+ console.print(" [dim]Workaround: Use 'python -m epi_cli' instead[/dim]")
281
+ else:
282
+ console.print(" [dim]Workaround: Use 'python -m epi_cli' instead[/dim]")
256
283
 
257
284
  # Check 3: Browser
258
285
  console.print("3. Browser Check: ", end="")
259
286
  try:
260
287
  import webbrowser
261
288
  webbrowser.get()
262
- console.print("[green]OK[/green]")
289
+ console.print("[green][OK][/green]")
263
290
  except:
264
- console.print("[yellow]WARNING (Headless?)[/yellow]")
291
+ console.print("[yellow][!] WARNING (Headless?)[/yellow]")
265
292
 
293
+ # Summary
266
294
  print()
295
+ console.print("[bold]" + "="*70 + "[/bold]")
267
296
  if issues == 0:
268
297
  console.print("[bold green][OK] System Healthy![/bold green]")
269
298
  else:
270
- console.print(f"[bold yellow][!] Found {issues} issues.[/bold yellow]")
299
+ if fixed > 0:
300
+ console.print(f"[bold yellow][!] Fixed {fixed}/{issues} issues[/bold yellow]")
301
+ if fixed < issues:
302
+ console.print("[dim]Some issues require manual attention (see above)[/dim]")
303
+ else:
304
+ console.print(f"[bold yellow][!] Found {issues} issues[/bold yellow]")
305
+ console.print("[dim]See suggestions above[/dim]")
306
+ console.print("[bold]" + "="*70 + "[/bold]\n")
271
307
 
272
308
 
273
309
  # Entry point for CLI
epi_core/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
  EPI Core - Core data structures, serialization, and container management.
3
3
  """
4
4
 
5
- __version__ = "2.1.0"
5
+ __version__ = "2.1.1"
6
6
 
7
7
  from epi_core.schemas import ManifestModel, StepModel
8
8
  from epi_core.serialize import get_canonical_hash
epi_core/container.py CHANGED
@@ -80,6 +80,8 @@ class EPIContainer:
80
80
  # Read template and assets
81
81
  template_html = template_path.read_text(encoding="utf-8")
82
82
  app_js = app_js_path.read_text(encoding="utf-8") if app_js_path.exists() else ""
83
+ crypto_js_path = viewer_static_dir / "crypto.js"
84
+ crypto_js = crypto_js_path.read_text(encoding="utf-8") if crypto_js_path.exists() else ""
83
85
  css_styles = css_path.read_text(encoding="utf-8") if css_path.exists() else ""
84
86
 
85
87
  # Read steps from steps.jsonl
@@ -112,10 +114,16 @@ class EPIContainer:
112
114
  f'<style>{css_styles}</style>'
113
115
  )
114
116
 
115
- # Inline app.js
117
+ # Inline crypto.js and app.js
118
+ js_content = ""
119
+ if crypto_js:
120
+ js_content += f"<script>{crypto_js}</script>\n"
121
+ if app_js:
122
+ js_content += f"<script>{app_js}</script>"
123
+
116
124
  html_with_js = html_with_css.replace(
117
125
  '<script src="app.js"></script>',
118
- f'<script>{app_js}</script>'
126
+ js_content
119
127
  )
120
128
 
121
129
  return html_with_js
epi_core/schemas.py CHANGED
@@ -18,7 +18,7 @@ class ManifestModel(BaseModel):
18
18
  """
19
19
 
20
20
  spec_version: str = Field(
21
- default="1.0-keystone",
21
+ default="1.1-json",
22
22
  description="EPI specification version"
23
23
  )
24
24
 
@@ -47,6 +47,11 @@ class ManifestModel(BaseModel):
47
47
  description="Mapping of file paths to their SHA-256 hashes for integrity verification"
48
48
  )
49
49
 
50
+ public_key: Optional[str] = Field(
51
+ default=None,
52
+ description="Hex-encoded public key used for verification"
53
+ )
54
+
50
55
  signature: Optional[str] = Field(
51
56
  default=None,
52
57
  description="Ed25519 signature of the canonical CBOR hash of this manifest (excluding signature field)"
epi_core/serialize.py CHANGED
@@ -87,27 +87,56 @@ def get_canonical_hash(model: BaseModel, exclude_fields: set[str] | None = None)
87
87
  else:
88
88
  return value
89
89
 
90
+ # Normalize datetime and UUID fields to strings
90
91
  model_dict = normalize_value(model_dict)
91
92
 
92
93
  if exclude_fields:
93
94
  for field in exclude_fields:
94
95
  model_dict.pop(field, None)
95
-
96
+
97
+ # JSON Canonicalization for Spec v1.1+
98
+ # Check if model has spec_version and if it indicates JSON usage
99
+ # We default to CBOR for backward compatibility
100
+
101
+ use_json = False
102
+
103
+ # Check spec_version in model or dict
104
+ spec_version = model_dict.get("spec_version")
105
+ if spec_version and (spec_version.startswith("1.1") or "json" in spec_version):
106
+ use_json = True
107
+
108
+ if use_json:
109
+ return _get_json_canonical_hash(model_dict)
110
+ else:
111
+ return _get_cbor_canonical_hash(model_dict)
112
+
113
+
114
+ def _get_json_canonical_hash(data: Any) -> str:
115
+ """Compute canonical SHA-256 hash using JSON (RFC 8785 style)."""
116
+ import json
117
+
118
+ # Dump to JSON with sorted keys and no whitespace
119
+ json_bytes = json.dumps(
120
+ data,
121
+ sort_keys=True,
122
+ separators=(',', ':'),
123
+ ensure_ascii=False
124
+ ).encode("utf-8")
125
+
126
+ return hashlib.sha256(json_bytes).hexdigest()
127
+
128
+
129
+ def _get_cbor_canonical_hash(data: Any) -> str:
130
+ """Compute canonical SHA-256 hash using CBOR (Legacy v1.0)."""
96
131
  # Encode to canonical CBOR
97
- # canonical=True ensures:
98
- # - Keys are sorted lexicographically
99
- # - Minimal encoding is used
100
- # - Deterministic representation
101
132
  cbor_bytes = cbor2.dumps(
102
- model_dict,
133
+ data,
103
134
  canonical=True,
104
135
  default=_cbor_default_encoder
105
136
  )
106
137
 
107
138
  # Compute SHA-256 hash
108
- hash_obj = hashlib.sha256(cbor_bytes)
109
-
110
- return hash_obj.hexdigest()
139
+ return hashlib.sha256(cbor_bytes).hexdigest()
111
140
 
112
141
 
113
142
  def verify_hash(model: BaseModel, expected_hash: str, exclude_fields: set[str] | None = None) -> bool:
epi_core/trust.py CHANGED
@@ -53,6 +53,16 @@ def sign_manifest(
53
53
  SigningError: If signing fails
54
54
  """
55
55
  try:
56
+ # Derive public key and add to manifest
57
+ public_key_obj = private_key.public_key()
58
+ public_key_hex = public_key_obj.public_bytes(
59
+ encoding=serialization.Encoding.Raw,
60
+ format=serialization.PublicFormat.Raw
61
+ ).hex()
62
+
63
+ # We must update the manifest BEFORE hashing so the public key is signed
64
+ manifest.public_key = public_key_hex
65
+
56
66
  # Compute canonical hash (excluding signature field)
57
67
  manifest_hash = get_canonical_hash(manifest, exclude_fields={"signature"})
58
68
  hash_bytes = bytes.fromhex(manifest_hash)
epi_postinstall.py ADDED
@@ -0,0 +1,197 @@
1
+ """
2
+ Post-installation script for EPI Recorder
3
+ Automatically fixes PATH issues on Windows for better UX
4
+ """
5
+ import sys
6
+ import os
7
+ import platform
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+
12
+ def get_scripts_dir():
13
+ """Get the Scripts directory where pip installs executables"""
14
+ if platform.system() == "Windows":
15
+ # Get the site-packages directory
16
+ import site
17
+ user_site = site.getusersitepackages()
18
+ if user_site:
19
+ # Scripts is typically ../Scripts relative to site-packages
20
+ scripts_dir = Path(user_site).parent / "Scripts"
21
+ if scripts_dir.exists():
22
+ return scripts_dir
23
+
24
+ # Fallback: try to find it from pip
25
+ try:
26
+ result = subprocess.run(
27
+ [sys.executable, "-m", "pip", "show", "epi-recorder"],
28
+ capture_output=True,
29
+ text=True
30
+ )
31
+ if result.returncode == 0:
32
+ for line in result.stdout.split('\n'):
33
+ if line.startswith('Location:'):
34
+ location = line.split(':', 1)[1].strip()
35
+ scripts_dir = Path(location).parent / "Scripts"
36
+ if scripts_dir.exists():
37
+ return scripts_dir
38
+ except Exception:
39
+ pass
40
+
41
+ return None
42
+
43
+
44
+ def is_in_path(directory):
45
+ """Check if directory is in PATH"""
46
+ path_env = os.environ.get('PATH', '')
47
+ path_dirs = path_env.split(os.pathsep)
48
+ dir_str = str(directory)
49
+ return any(os.path.normcase(p) == os.path.normcase(dir_str) for p in path_dirs)
50
+
51
+
52
+ def add_to_user_path_windows(directory):
53
+ """Add directory to user PATH on Windows"""
54
+ try:
55
+ import winreg
56
+
57
+ # Open the user environment variables key
58
+ key = winreg.OpenKey(
59
+ winreg.HKEY_CURRENT_USER,
60
+ 'Environment',
61
+ 0,
62
+ winreg.KEY_READ | winreg.KEY_WRITE
63
+ )
64
+
65
+ try:
66
+ # Get current PATH
67
+ current_path, _ = winreg.QueryValueEx(key, 'Path')
68
+ except WindowsError:
69
+ current_path = ''
70
+
71
+ # Add our directory if not already there
72
+ path_parts = current_path.split(os.pathsep)
73
+ dir_str = str(directory)
74
+
75
+ if not any(os.path.normcase(p) == os.path.normcase(dir_str) for p in path_parts):
76
+ new_path = current_path + os.pathsep + dir_str
77
+ winreg.SetValueEx(key, 'Path', 0, winreg.REG_EXPAND_SZ, new_path)
78
+ winreg.CloseKey(key)
79
+
80
+ # Broadcast WM_SETTINGCHANGE to notify the system
81
+ try:
82
+ import ctypes
83
+ HWND_BROADCAST = 0xFFFF
84
+ WM_SETTINGCHANGE = 0x1A
85
+ ctypes.windll.user32.SendMessageW(
86
+ HWND_BROADCAST, WM_SETTINGCHANGE, 0, 'Environment'
87
+ )
88
+ except Exception:
89
+ pass
90
+
91
+ return True
92
+
93
+ winreg.CloseKey(key)
94
+ return False
95
+
96
+ except Exception as e:
97
+ print(f"Warning: Could not modify PATH: {e}")
98
+ return False
99
+
100
+
101
+ def check_epi_command():
102
+ """Check if 'epi' command is accessible"""
103
+ try:
104
+ result = subprocess.run(
105
+ ['epi', '--version'],
106
+ capture_output=True,
107
+ timeout=2
108
+ )
109
+ return result.returncode == 0
110
+ except (FileNotFoundError, subprocess.TimeoutExpired):
111
+ return False
112
+
113
+
114
+ def post_install():
115
+ """Main post-install function"""
116
+ print("\n" + "="*70)
117
+ print("🎉 EPI Recorder Installation Complete!")
118
+ print("="*70)
119
+
120
+ # Check if epi command works
121
+ if check_epi_command():
122
+ print("\n✅ The 'epi' command is ready to use!")
123
+ print("\nTry it now:")
124
+ print(" epi --help")
125
+ print(" epi init")
126
+ return
127
+
128
+ # If not, try to fix it (Windows only for now)
129
+ if platform.system() == "Windows":
130
+ print("\n⚠️ 'epi' command not found in PATH")
131
+ print("🔧 Attempting automatic fix...")
132
+
133
+ scripts_dir = get_scripts_dir()
134
+
135
+ if scripts_dir and scripts_dir.exists():
136
+ print(f"📁 Found Scripts directory: {scripts_dir}")
137
+
138
+ if not is_in_path(scripts_dir):
139
+ print("➕ Adding to your user PATH...")
140
+
141
+ try:
142
+ if add_to_user_path_windows(scripts_dir):
143
+ print("\n✅ SUCCESS! PATH updated.")
144
+ print("\n⚠️ IMPORTANT: You must restart your terminal for changes to take effect!")
145
+ print("\nAfter restarting your terminal, try:")
146
+ print(" epi --help")
147
+ print(" epi init")
148
+ else:
149
+ print("\n⚠️ Scripts directory already in PATH, but 'epi' not found.")
150
+ print("This might require a terminal restart.")
151
+ print("\nIf 'epi' still doesn't work after restarting, use:")
152
+ print(f" python -m epi_cli")
153
+ except Exception as e:
154
+ print(f"\n❌ Automatic fix failed: {e}")
155
+ show_manual_instructions(scripts_dir)
156
+ else:
157
+ print("✅ Scripts directory is in PATH")
158
+ print("⚠️ You may need to restart your terminal for the command to work.")
159
+ else:
160
+ print("❌ Could not locate Scripts directory")
161
+ show_fallback_instructions()
162
+ else:
163
+ # Linux/Mac
164
+ print("\n⚠️ 'epi' command not found in PATH")
165
+ print("\nIf 'epi' doesn't work, use:")
166
+ print(" python -m epi_cli")
167
+ print("\nOr add pip's user base bin directory to PATH:")
168
+ print(" export PATH=$PATH:$(python -m site --user-base)/bin")
169
+
170
+ print("\n" + "="*70 + "\n")
171
+
172
+
173
+ def show_manual_instructions(scripts_dir):
174
+ """Show manual PATH update instructions"""
175
+ print("\n📖 MANUAL FIX REQUIRED:")
176
+ print("\nOption 1: Update PATH (Permanent)")
177
+ print(" 1. Press Win + R, type: sysdm.cpl")
178
+ print(" 2. Advanced → Environment Variables")
179
+ print(" 3. Under 'User variables', select 'Path' → Edit")
180
+ print(" 4. Click 'New' and add:")
181
+ print(f" {scripts_dir}")
182
+ print(" 5. Click OK, restart your terminal")
183
+ print("\nOption 2: Use python -m (Always works)")
184
+ print(" python -m epi_cli run script.py")
185
+
186
+
187
+ def show_fallback_instructions():
188
+ """Show fallback instructions if automatic fix fails"""
189
+ print("\n📖 WORKAROUND:")
190
+ print("\nUse 'python -m epi_cli' instead of 'epi':")
191
+ print(" python -m epi_cli --help")
192
+ print(" python -m epi_cli run script.py")
193
+ print(" python -m epi_cli view recording.epi")
194
+
195
+
196
+ if __name__ == "__main__":
197
+ post_install()
epi_recorder/__init__.py CHANGED
@@ -4,7 +4,7 @@ EPI Recorder - Runtime interception and workflow capture.
4
4
  Python API for recording AI workflows with cryptographic verification.
5
5
  """
6
6
 
7
- __version__ = "2.1.0"
7
+ __version__ = "2.1.1"
8
8
 
9
9
  # Export Python API
10
10
  from epi_recorder.api import (