epi-recorder 2.1.1__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_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_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 (