epi-recorder 1.0.0__py3-none-any.whl → 1.1.1__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/verify.py CHANGED
@@ -46,7 +46,7 @@ def verify(
46
46
  """
47
47
  # Ensure file exists
48
48
  if not epi_file.exists():
49
- console.print(f"[red] Error:[/red] File not found: {epi_file}")
49
+ console.print(f"[red][FAIL] Error:[/red] File not found: {epi_file}")
50
50
  raise typer.Exit(1)
51
51
 
52
52
  # Initialize verification state
@@ -65,11 +65,11 @@ def verify(
65
65
  try:
66
66
  manifest = EPIContainer.read_manifest(epi_file)
67
67
  if verbose:
68
- console.print(" [green][/green] Valid ZIP format")
69
- console.print(" [green][/green] Valid mimetype")
70
- console.print(" [green][/green] Valid manifest schema")
68
+ console.print(" [green][OK][/green] Valid ZIP format")
69
+ console.print(" [green][OK][/green] Valid mimetype")
70
+ console.print(" [green][OK][/green] Valid manifest schema")
71
71
  except Exception as e:
72
- console.print(f"[red] Structural validation failed:[/red] {e}")
72
+ console.print(f"[red][FAIL] Structural validation failed:[/red] {e}")
73
73
  raise typer.Exit(1)
74
74
 
75
75
  # ========== STEP 2: INTEGRITY CHECKS ==========
@@ -80,9 +80,9 @@ def verify(
80
80
 
81
81
  if verbose:
82
82
  if integrity_ok:
83
- console.print(f" [green][/green] All {len(manifest.file_manifest)} files verified")
83
+ console.print(f" [green][OK][/green] All {len(manifest.file_manifest)} files verified")
84
84
  else:
85
- console.print(f" [red][/red] {len(mismatches)} file(s) failed verification")
85
+ console.print(f" [red][FAIL][/red] {len(mismatches)} file(s) failed verification")
86
86
  for filename, reason in mismatches.items():
87
87
  console.print(f" [red]•[/red] {filename}: {reason}")
88
88
 
@@ -101,19 +101,19 @@ def verify(
101
101
 
102
102
  if verbose:
103
103
  if signature_valid:
104
- console.print(f" [green][/green] {sig_message}")
104
+ console.print(f" [green][OK][/green] {sig_message}")
105
105
  else:
106
- console.print(f" [red][/red] {sig_message}")
106
+ console.print(f" [red][FAIL][/red] {sig_message}")
107
107
 
108
108
  except FileNotFoundError:
109
109
  signature_valid = False
110
110
  if verbose:
111
- console.print(f" [yellow]⚠️[/yellow] Public key not found: {signer_name}")
111
+ console.print(f" [yellow][WARN][/yellow] Public key not found: {signer_name}")
112
112
  console.print(" [dim]Cannot verify signature without public key[/dim]")
113
113
  else:
114
114
  signature_valid = None
115
115
  if verbose:
116
- console.print(" [yellow]⚠️[/yellow] No signature present (unsigned)")
116
+ console.print(" [yellow][WARN][/yellow] No signature present (unsigned)")
117
117
 
118
118
  # ========== CREATE REPORT ==========
119
119
  report = create_verification_report(
@@ -143,7 +143,7 @@ def verify(
143
143
  if verbose:
144
144
  console.print_exception()
145
145
  else:
146
- console.print(f"[red] Verification failed:[/red] {e}")
146
+ console.print(f"[red][FAIL] Verification failed:[/red] {e}")
147
147
  raise typer.Exit(1)
148
148
 
149
149
 
@@ -158,15 +158,15 @@ def print_trust_report(report: dict, epi_file: Path, verbose: bool = False):
158
158
  """
159
159
  # Determine overall status symbol and color
160
160
  if report["trust_level"] == "HIGH":
161
- status_symbol = ""
161
+ status_symbol = "[OK]"
162
162
  status_color = "green"
163
163
  panel_style = "green"
164
164
  elif report["trust_level"] == "MEDIUM":
165
- status_symbol = "⚠️"
165
+ status_symbol = "[WARN]"
166
166
  status_color = "yellow"
167
167
  panel_style = "yellow"
168
168
  else:
169
- status_symbol = ""
169
+ status_symbol = "[FAIL]"
170
170
  status_color = "red"
171
171
  panel_style = "red"
172
172
 
@@ -179,17 +179,17 @@ def print_trust_report(report: dict, epi_file: Path, verbose: bool = False):
179
179
 
180
180
  # Integrity status
181
181
  if report["integrity_ok"]:
182
- content_lines.append(f"[green] Integrity:[/green] Verified ({report['files_checked']} files)")
182
+ content_lines.append(f"[green][OK] Integrity:[/green] Verified ({report['files_checked']} files)")
183
183
  else:
184
- content_lines.append(f"[red] Integrity:[/red] Failed ({report['mismatches_count']} mismatches)")
184
+ content_lines.append(f"[red][FAIL] Integrity:[/red] Failed ({report['mismatches_count']} mismatches)")
185
185
 
186
186
  # Signature status
187
187
  if report["signature_valid"]:
188
- content_lines.append(f"[green] Signature:[/green] Valid (key: {report['signer']})")
188
+ content_lines.append(f"[green][OK] Signature:[/green] Valid (key: {report['signer']})")
189
189
  elif report["signature_valid"] is None:
190
- content_lines.append("[yellow]⚠️ Signature:[/yellow] Not signed")
190
+ content_lines.append("[yellow][WARN] Signature:[/yellow] Not signed")
191
191
  else:
192
- content_lines.append(f"[red] Signature:[/red] Invalid")
192
+ content_lines.append(f"[red][FAIL] Signature:[/red] Invalid")
193
193
 
194
194
  # Show metadata if verbose
195
195
  if verbose:
epi_cli/view.py CHANGED
@@ -17,26 +17,81 @@ console = Console()
17
17
 
18
18
  app = typer.Typer(name="view", help="View .epi file in browser")
19
19
 
20
+ DEFAULT_DIR = Path("epi-recordings")
21
+
22
+
23
+ def _resolve_epi_file(name_or_path: str) -> Path:
24
+ """
25
+ Resolve a name or path to an .epi file.
26
+
27
+ Tries in order:
28
+ 1. Exact path if it exists
29
+ 2. Add .epi extension if missing
30
+ 3. Look in ./epi-recordings/ directory
31
+
32
+ Args:
33
+ name_or_path: User input (name or path)
34
+
35
+ Returns:
36
+ Resolved Path object
37
+
38
+ Raises:
39
+ FileNotFoundError if file cannot be found
40
+ """
41
+ path = Path(name_or_path)
42
+
43
+ # Try exact path
44
+ if path.exists() and path.is_file():
45
+ return path
46
+
47
+ # Try adding .epi extension
48
+ if not str(path).endswith(".epi"):
49
+ with_ext = path.with_suffix(".epi")
50
+ if with_ext.exists():
51
+ return with_ext
52
+
53
+ # Try in default directory
54
+ in_default = DEFAULT_DIR / path.name
55
+ if in_default.exists():
56
+ return in_default
57
+
58
+ # Try in default directory with .epi extension
59
+ in_default_with_ext = DEFAULT_DIR / f"{path.stem}.epi"
60
+ if in_default_with_ext.exists():
61
+ return in_default_with_ext
62
+
63
+ # Not found
64
+ raise FileNotFoundError(f"Recording not found: {name_or_path}")
65
+
20
66
 
21
67
  @app.callback(invoke_without_command=True)
22
68
  def view(
23
69
  ctx: typer.Context,
24
- epi_file: Path = typer.Argument(..., help="Path to .epi file to view"),
70
+ epi_file: str = typer.Argument(..., help="Path or name of .epi file to view"),
25
71
  ):
26
72
  """
27
73
  Open .epi file in browser viewer.
28
74
 
29
- Extracts the embedded viewer.html and opens it in your default browser.
30
- All data is pre-embedded, no server required.
75
+ Accepts file path, name, or base name. Automatically resolves:
76
+ - foo -> ./epi-recordings/foo.epi
77
+ - foo.epi -> ./epi-recordings/foo.epi
78
+ - /path/to/file.epi -> /path/to/file.epi
79
+
80
+ Example:
81
+ epi view my_script_20251121_231501
82
+ epi view my_recording.epi
31
83
  """
32
- # Validate file exists
33
- if not epi_file.exists():
34
- console.print(f"[red]❌ Error:[/red] File not found: {epi_file}")
84
+ # Resolve the file path
85
+ try:
86
+ resolved_path = _resolve_epi_file(epi_file)
87
+ except FileNotFoundError as e:
88
+ console.print(f"[red][FAIL] Error:[/red] {e}")
89
+ console.print(f"[dim]Tip: Run 'epi ls' to see available recordings[/dim]")
35
90
  raise typer.Exit(1)
36
91
 
37
92
  # Validate it's a ZIP file
38
- if not zipfile.is_zipfile(epi_file):
39
- console.print(f"[red] Error:[/red] Not a valid .epi file: {epi_file}")
93
+ if not zipfile.is_zipfile(resolved_path):
94
+ console.print(f"[red][FAIL] Error:[/red] Not a valid .epi file: {resolved_path}")
40
95
  raise typer.Exit(1)
41
96
 
42
97
  try:
@@ -45,9 +100,9 @@ def view(
45
100
  viewer_path = temp_dir / "viewer.html"
46
101
 
47
102
  # Extract viewer.html
48
- with zipfile.ZipFile(epi_file, "r") as zf:
103
+ with zipfile.ZipFile(resolved_path, "r") as zf:
49
104
  if "viewer.html" not in zf.namelist():
50
- console.print("[red] Error:[/red] No viewer found in .epi file")
105
+ console.print("[red][FAIL] Error:[/red] No viewer found in .epi file")
51
106
  console.print("[dim]This file may have been created with an older version of EPI[/dim]")
52
107
  raise typer.Exit(1)
53
108
 
@@ -61,14 +116,14 @@ def view(
61
116
  success = webbrowser.open(file_url)
62
117
 
63
118
  if success:
64
- console.print("[green][/green] Viewer opened in browser")
119
+ console.print("[green][OK][/green] Viewer opened in browser")
65
120
  else:
66
- console.print("[yellow]⚠️ Could not open browser automatically[/yellow]")
121
+ console.print("[yellow][WARN] Could not open browser automatically[/yellow]")
67
122
  console.print(f"[dim]Open manually:[/dim] {file_url}")
68
123
 
69
124
  except KeyboardInterrupt:
70
125
  console.print("\n[yellow]Cancelled[/yellow]")
71
126
  raise typer.Exit(130)
72
127
  except Exception as e:
73
- console.print(f"[red] Error:[/red] {e}")
128
+ console.print(f"[red][FAIL] Error:[/red] {e}")
74
129
  raise typer.Exit(1)
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__ = "1.0.0-keystone"
5
+ __version__ = "1.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/redactor.py CHANGED
@@ -78,17 +78,19 @@ class Redactor:
78
78
  from captured workflow data.
79
79
  """
80
80
 
81
- def __init__(self, config_path: Path | None = None, enabled: bool = True):
81
+ def __init__(self, config_path: Path | None = None, enabled: bool = True, allowlist: List[str] = None):
82
82
  """
83
83
  Initialize redactor with optional custom configuration.
84
84
 
85
85
  Args:
86
86
  config_path: Optional path to config.toml with custom patterns
87
87
  enabled: Whether redaction is enabled (default: True)
88
+ allowlist: Optional list of strings to NEVER redact
88
89
  """
89
90
  self.enabled = enabled
90
91
  self.patterns: List[Tuple[re.Pattern, str]] = []
91
92
  self.env_vars_to_redact = REDACT_ENV_VARS.copy()
93
+ self.allowlist = set(allowlist) if allowlist else set()
92
94
 
93
95
  # Compile default patterns
94
96
  for pattern_str, description in DEFAULT_REDACTION_PATTERNS:
@@ -132,6 +134,10 @@ class Redactor:
132
134
  # Load custom env vars
133
135
  if 'redaction' in config and 'env_vars' in config['redaction']:
134
136
  self.env_vars_to_redact.update(config['redaction']['env_vars'])
137
+
138
+ # Load allowlist
139
+ if 'redaction' in config and 'allowlist' in config['redaction']:
140
+ self.allowlist.update(config['redaction']['allowlist'])
135
141
 
136
142
  except Exception as e:
137
143
  print(f"Warning: Could not load config from {config_path}: {e}")
@@ -176,6 +182,10 @@ class Redactor:
176
182
  return redacted_list, redaction_count
177
183
 
178
184
  elif isinstance(data, str):
185
+ # Check allowlist first
186
+ if data in self.allowlist:
187
+ return data, 0
188
+
179
189
  redacted_str = data
180
190
  for pattern, description in self.patterns:
181
191
  matches = pattern.findall(redacted_str)
@@ -239,6 +249,9 @@ enabled = true
239
249
 
240
250
  # Additional environment variable names to redact
241
251
  # env_vars = ["MY_SECRET_VAR", "CUSTOM_TOKEN"]
252
+
253
+ # Allowlist: Strings that should NEVER be redacted (exact match)
254
+ # allowlist = ["sk-not-actually-a-key", "my-public-token"]
242
255
  """
243
256
 
244
257
  config_path.parent.mkdir(parents=True, exist_ok=True)
epi_core/schemas.py CHANGED
@@ -3,7 +3,7 @@ EPI Core Schemas - Pydantic models for manifest and steps.
3
3
  """
4
4
 
5
5
  from datetime import datetime
6
- from typing import Any, Dict, Optional
6
+ from typing import Any, Dict, Optional, List, Union
7
7
  from uuid import UUID, uuid4
8
8
 
9
9
  from pydantic import BaseModel, ConfigDict, Field
@@ -52,6 +52,32 @@ class ManifestModel(BaseModel):
52
52
  description="Ed25519 signature of the canonical CBOR hash of this manifest (excluding signature field)"
53
53
  )
54
54
 
55
+ # New metadata fields for decision tracking
56
+ goal: Optional[str] = Field(
57
+ default=None,
58
+ description="Goal or objective of this workflow execution"
59
+ )
60
+
61
+ notes: Optional[str] = Field(
62
+ default=None,
63
+ description="Additional notes or context about this workflow"
64
+ )
65
+
66
+ metrics: Optional[Dict[str, Union[float, str]]] = Field(
67
+ default=None,
68
+ description="Key-value metrics for this workflow (accuracy, latency, etc.)"
69
+ )
70
+
71
+ approved_by: Optional[str] = Field(
72
+ default=None,
73
+ description="Person or entity who approved this workflow execution"
74
+ )
75
+
76
+ tags: Optional[List[str]] = Field(
77
+ default=None,
78
+ description="Tags for categorizing this workflow"
79
+ )
80
+
55
81
  model_config = ConfigDict(
56
82
  json_schema_extra={
57
83
  "example": {
@@ -65,7 +91,12 @@ class ManifestModel(BaseModel):
65
91
  "env.json": "a3c5f...",
66
92
  "artifacts/output.txt": "c7f8a..."
67
93
  },
68
- "signature": "ed25519:3a4b5c6d..."
94
+ "signature": "ed25519:3a4b5c6d...",
95
+ "goal": "Improve model accuracy",
96
+ "notes": "Switched to GPT-4 for better reasoning",
97
+ "metrics": {"accuracy": 0.92, "latency": 210},
98
+ "approved_by": "alice@company.com",
99
+ "tags": ["prod-candidate", "v1.0"]
69
100
  }
70
101
  }
71
102
  )
@@ -109,4 +140,4 @@ class StepModel(BaseModel):
109
140
  }
110
141
  }
111
142
  }
112
- )
143
+ )
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__ = "1.0.0-keystone"
7
+ __version__ = "1.1.1"
8
8
 
9
9
  # Export Python API
10
10
  from epi_recorder.api import (