causallock 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.
Files changed (74) hide show
  1. causallock-0.1.0/PKG-INFO +15 -0
  2. causallock-0.1.0/README.md +341 -0
  3. causallock-0.1.0/causallock/__init__.py +6 -0
  4. causallock-0.1.0/causallock/cache/tool_vcr.py +115 -0
  5. causallock-0.1.0/causallock/cli/__init__.py +0 -0
  6. causallock-0.1.0/causallock/cli/commands/diff.py +44 -0
  7. causallock-0.1.0/causallock/cli/commands/freeze.py +35 -0
  8. causallock-0.1.0/causallock/cli/commands/init.py +37 -0
  9. causallock-0.1.0/causallock/cli/commands/replay.py +61 -0
  10. causallock-0.1.0/causallock/cli/commands/run.py +0 -0
  11. causallock-0.1.0/causallock/cli/commands/sign.py +148 -0
  12. causallock-0.1.0/causallock/cli/commands/test.py +87 -0
  13. causallock-0.1.0/causallock/cli/commands/ui.py +9 -0
  14. causallock-0.1.0/causallock/cli/main.py +32 -0
  15. causallock-0.1.0/causallock/cli/parallel.py +24 -0
  16. causallock-0.1.0/causallock/cli/trajignore.py +34 -0
  17. causallock-0.1.0/causallock/core/__init__.py +0 -0
  18. causallock-0.1.0/causallock/core/context.py +41 -0
  19. causallock-0.1.0/causallock/core/environment.py +42 -0
  20. causallock-0.1.0/causallock/core/hashing.py +34 -0
  21. causallock-0.1.0/causallock/core/models.py +48 -0
  22. causallock-0.1.0/causallock/core/schema.py +64 -0
  23. causallock-0.1.0/causallock/core/serialization.py +74 -0
  24. causallock-0.1.0/causallock/core/serializer.py +16 -0
  25. causallock-0.1.0/causallock/core/storage.py +196 -0
  26. causallock-0.1.0/causallock/core/trace.py +106 -0
  27. causallock-0.1.0/causallock/diff/engine.py +134 -0
  28. causallock-0.1.0/causallock/interceptor/base.py +29 -0
  29. causallock-0.1.0/causallock/interceptor/mode.py +8 -0
  30. causallock-0.1.0/causallock/interceptor/openai_wrapper.py +146 -0
  31. causallock-0.1.0/causallock/replay/engine.py +123 -0
  32. causallock-0.1.0/causallock/replay/immutable.py +21 -0
  33. causallock-0.1.0/causallock/runtime/__init__.py +11 -0
  34. causallock-0.1.0/causallock/runtime/determinism.py +101 -0
  35. causallock-0.1.0/causallock/scripts/generate_regression.py +74 -0
  36. causallock-0.1.0/causallock/security/default_rules.py +31 -0
  37. causallock-0.1.0/causallock/security/key_registry.py +16 -0
  38. causallock-0.1.0/causallock/security/redaction.py +48 -0
  39. causallock-0.1.0/causallock/security/signing.py +79 -0
  40. causallock-0.1.0/causallock/ui/__init__.py +0 -0
  41. causallock-0.1.0/causallock/ui/config.py +6 -0
  42. causallock-0.1.0/causallock/ui/schemas.py +55 -0
  43. causallock-0.1.0/causallock/ui/server.py +513 -0
  44. causallock-0.1.0/causallock/ui/services/__init__.py +0 -0
  45. causallock-0.1.0/causallock/ui/services/diff_service.py +19 -0
  46. causallock-0.1.0/causallock/ui/services/filesystem_service.py +8 -0
  47. causallock-0.1.0/causallock/ui/services/trace_service.py +165 -0
  48. causallock-0.1.0/causallock/ui/services/verify_service.py +39 -0
  49. causallock-0.1.0/causallock/ui/static/assets/index-CXwYAcTX.js +14 -0
  50. causallock-0.1.0/causallock/ui/static/assets/index-q9B6OTLp.css +1 -0
  51. causallock-0.1.0/causallock/ui/static/index.html +14 -0
  52. causallock-0.1.0/causallock/ui/static/vite.svg +1 -0
  53. causallock-0.1.0/causallock.egg-info/PKG-INFO +15 -0
  54. causallock-0.1.0/causallock.egg-info/SOURCES.txt +72 -0
  55. causallock-0.1.0/causallock.egg-info/dependency_links.txt +1 -0
  56. causallock-0.1.0/causallock.egg-info/entry_points.txt +2 -0
  57. causallock-0.1.0/causallock.egg-info/requires.txt +9 -0
  58. causallock-0.1.0/causallock.egg-info/top_level.txt +1 -0
  59. causallock-0.1.0/pyproject.toml +39 -0
  60. causallock-0.1.0/setup.cfg +4 -0
  61. causallock-0.1.0/tests/test_determinism_and_diff.py +40 -0
  62. causallock-0.1.0/tests/test_hash_chain.py +27 -0
  63. causallock-0.1.0/tests/test_integrity_fuzz.py +54 -0
  64. causallock-0.1.0/tests/test_interceptor.py +54 -0
  65. causallock-0.1.0/tests/test_redaction.py +14 -0
  66. causallock-0.1.0/tests/test_replay.py +70 -0
  67. causallock-0.1.0/tests/test_sanity.py +2 -0
  68. causallock-0.1.0/tests/test_schema_and_cas.py +47 -0
  69. causallock-0.1.0/tests/test_signed_trace.py +35 -0
  70. causallock-0.1.0/tests/test_signing.py +12 -0
  71. causallock-0.1.0/tests/test_storage.py +30 -0
  72. causallock-0.1.0/tests/test_stream_aggregation.py +58 -0
  73. causallock-0.1.0/tests/test_streaming.py +88 -0
  74. causallock-0.1.0/tests/test_tool_vcr.py +51 -0
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: causallock
3
+ Version: 0.1.0
4
+ Summary: Deterministic Replay & Audit SDK for AI Agents
5
+ Author: Guna
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: pydantic
8
+ Requires-Dist: orjson
9
+ Requires-Dist: rich
10
+ Requires-Dist: typer
11
+ Requires-Dist: cryptography
12
+ Requires-Dist: httpx
13
+ Requires-Dist: fastapi
14
+ Requires-Dist: uvicorn
15
+ Requires-Dist: openai
@@ -0,0 +1,341 @@
1
+ # causallock
2
+
3
+ causallock is a deterministic execution recording, replay, and audit platform for AI agents.
4
+
5
+ It provides:
6
+ - deterministic trace capture (`.traj`)
7
+ - hash-chain integrity verification
8
+ - replay validation and divergence detection
9
+ - regression testing at trace level
10
+ - cryptographic signing support
11
+ - UI control plane for replay/diff/VCR/redaction/trust workflows
12
+
13
+ ---
14
+
15
+ ## What This Project Is
16
+
17
+ causallock is not a prompt evaluator.
18
+ It is execution infrastructure for reliability and auditability:
19
+
20
+ - record each LLM/tool event
21
+ - preserve event ordering and hashes
22
+ - replay under deterministic constraints
23
+ - detect divergence at payload/hash/order levels
24
+ - expose trust evidence for compliance-style review
25
+
26
+ ---
27
+
28
+ ## Distribution Model (No Node Required for End Users)
29
+
30
+ End users install and run with Python only:
31
+
32
+ ```bash
33
+ pip install causallock
34
+ causallock ui
35
+ ```
36
+
37
+ This works because the built React dashboard is packaged into `causallock/ui/static` and served by FastAPI.
38
+
39
+ You do **not** need `cd dashboard` as an end user.
40
+
41
+ ---
42
+
43
+ ## Install
44
+
45
+ ### From local source
46
+
47
+ ```bash
48
+ pip install .
49
+ ```
50
+
51
+ ### Editable (contributors)
52
+
53
+ ```bash
54
+ pip install -e .
55
+ ```
56
+
57
+ ### From GitHub
58
+
59
+ ```bash
60
+ pip install "git+https://github.com/GunaShankar0213/CausalLock.git"
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Quick Run
66
+
67
+ ```bash
68
+ causallock init run .
69
+ causallock ui
70
+ ```
71
+
72
+ Open:
73
+ - `http://127.0.0.1:8765/ui`
74
+
75
+ Recommended server start options:
76
+
77
+ ```bash
78
+ # Preferred
79
+ causallock ui
80
+
81
+ # Equivalent backend launch
82
+ uvicorn causallock.ui.server:app --host 127.0.0.1 --port 8765
83
+ ```
84
+
85
+ ---
86
+
87
+ ## CLI Commands
88
+
89
+ ### Replay
90
+
91
+ ```bash
92
+ causallock replay run regression/sample1.traj
93
+ causallock replay run regression/sample1.traj --immutable --seed 1337
94
+ causallock replay run regression/sample1.traj --immutable --force-env-mismatch
95
+ ```
96
+
97
+ ### Diff
98
+
99
+ ```bash
100
+ causallock diff run regression/sample1.traj regression/sample2.traj
101
+ causallock diff run regression/sample1.traj regression/sample2.traj --semantic
102
+ ```
103
+
104
+ ### Regression
105
+
106
+ ```bash
107
+ causallock test run regression --workers 4 --json-output
108
+ causallock test run regression --strict
109
+ ```
110
+
111
+ ### Freeze snapshots
112
+
113
+ ```bash
114
+ causallock freeze run regression/
115
+ ```
116
+
117
+ ### Signing
118
+
119
+ ```bash
120
+ causallock sign keygen --output keys
121
+ causallock sign sign regression/sample1.traj keys/private.key
122
+ causallock sign verify regression/sample1.traj
123
+ causallock sign sign regression/sample1.traj keys/private.key --detached --signature-path regression/sample1.sig --key-id team-key-1
124
+ causallock sign verify regression/sample1.traj --signature-path regression/sample1.sig
125
+ ```
126
+
127
+ ---
128
+
129
+ ## API Surface (`/api/*`)
130
+
131
+ ### Trace + search
132
+ - `GET /api/traces`
133
+ - `GET /api/trace/{trace_name}`
134
+ - `GET /api/trace/{trace_name}/events`
135
+ - `GET /api/trace/{trace_name}/event/{index}`
136
+ - `GET /api/trace/{trace_name}/state?index=N`
137
+ - `GET /api/search?q=...`
138
+
139
+ ### Replay control
140
+ - `POST /api/replay/start`
141
+ - `POST /api/replay/step`
142
+ - `POST /api/replay/stop`
143
+ - `GET /api/replay/status`
144
+ - `GET /api/replay/progress/{session_id}` (SSE event: `replay_progress`)
145
+
146
+ ### Diff / VCR / redaction / trust
147
+ - `GET /api/diff?traceA=...&traceB=...&semantic=true|false`
148
+ - `GET /api/vcr/stats`
149
+ - `GET /api/vcr/{tool_hash}`
150
+ - `GET /api/redaction/rules`
151
+ - `GET /api/trace/{trace_name}/redaction-log`
152
+ - `GET /api/trust/{trace_name}`
153
+ - `POST /api/test/run`
154
+
155
+ ---
156
+
157
+ ## UI Pages and Components
158
+
159
+ The packaged dashboard includes:
160
+
161
+ ### 1. Explorer
162
+ - grouped runs by `project_name -> agent_name`
163
+ - trace cards (name/id/model/seed/signature/hash-chain/event count)
164
+ - virtualized timeline
165
+ - lazy payload inspector
166
+
167
+ ### 2. Replay
168
+ - start/step/stop replay
169
+ - immutable replay toggle
170
+ - optional seed override
171
+ - replay status panel
172
+ - live SSE progress stream
173
+ - deterministic state snapshot viewer
174
+
175
+ ### 3. Diff
176
+ - trace A/B selection
177
+ - semantic toggle
178
+ - structured divergence result:
179
+ - `type`
180
+ - `summary`
181
+ - `field_deltas`
182
+ - `divergence_index`
183
+
184
+ ### 4. VCR
185
+ - cache hit/miss totals
186
+ - unique payload hash count
187
+ - per-tool usage
188
+ - lookup by tool payload hash
189
+
190
+ ### 5. Redaction
191
+ - active rule listing
192
+ - per-trace redaction marker log
193
+
194
+ ### 6. Trust
195
+ - hash-chain status
196
+ - signature status
197
+ - environment mismatch fields
198
+ - CAS presence check
199
+ - strict regression run trigger
200
+
201
+ ---
202
+
203
+ ## UI Event Tags and Meanings
204
+
205
+ Event row colors/types:
206
+ - `llm_call`: discrete model invocation
207
+ - `llm_stream`: streaming model output event
208
+ - `tool_call`: tool execution capture/replay unit
209
+ - generic fallback: non-categorized deterministic event
210
+
211
+ Trace card tags:
212
+ - `signed` / `unsigned`
213
+ - `chain ok` / `chain bad`
214
+ - `model <name>`
215
+ - `seed <value>`
216
+
217
+ ---
218
+
219
+ ## Multi-Agent Grouping
220
+
221
+ Trace schema fields:
222
+ - `project_name`
223
+ - `agent_name`
224
+
225
+ UI groups using these fields.
226
+
227
+ Backward compatibility for older traces:
228
+ - inferred from filename patterns:
229
+ - `project__agent__...`
230
+ - `project-agent-...`
231
+
232
+ ---
233
+
234
+ ## Real-Time Example (Ollama Phi)
235
+
236
+ ```bash
237
+ python scripts/ollama_realtime_example.py \
238
+ --model phi4 \
239
+ --project customer_support \
240
+ --agent planner \
241
+ --runs 5 \
242
+ --seed 1337 \
243
+ --show-ui
244
+ ```
245
+
246
+ What this does:
247
+ - live OpenAI-compatible calls to Ollama
248
+ - deterministic trace capture
249
+ - CAS write
250
+ - immutable replay verification per run
251
+ - auto-opens UI
252
+ - file naming suitable for grouping:
253
+ - `project__agent__model__run_N.traj`
254
+
255
+ After generating demo traces, replay one run:
256
+
257
+ ```bash
258
+ causallock replay run regression/demo__planner__phi4__run_1.traj --immutable --seed 1337
259
+ ```
260
+
261
+ ---
262
+
263
+ ## Contributor Workflow (when editing UI)
264
+
265
+ ### Dev mode (HMR)
266
+
267
+ ```bash
268
+ cd dashboard
269
+ npm install
270
+ npm run dev
271
+ ```
272
+
273
+ ### Sync built UI into Python package
274
+
275
+ ```bash
276
+ python scripts/sync_dashboard_static.py
277
+ ```
278
+
279
+ This builds `dashboard/dist` and copies it into `causallock/ui/static`.
280
+
281
+ ---
282
+
283
+ ## Release Automation
284
+
285
+ Prepare release artifacts with one command:
286
+
287
+ ```bash
288
+ python scripts/prepare_release.py
289
+ ```
290
+
291
+ It runs:
292
+ 1. Python tests
293
+ 2. dashboard build + static sync
294
+ 3. `python -m build`
295
+
296
+ ---
297
+
298
+ ## Verification
299
+
300
+ ```bash
301
+ python -m pytest -q
302
+ python -m py_compile causallock/ui/server.py scripts/ollama_realtime_example.py
303
+ cd dashboard && npm run build
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Why Use causallock (Pros)
309
+
310
+ - reproducible replay workflows for agent debugging
311
+ - deterministic integrity model (hash-chain + root hash metadata)
312
+ - fast CI-grade regression without paid model calls
313
+ - strong auditability path (signature + trust diagnostics)
314
+ - project/agent grouping for multi-agent systems
315
+ - Python-first distribution (`pip install` + single `causallock ui`)
316
+
317
+ ---
318
+
319
+ ## Conclusion and Expansion Path
320
+
321
+ causallock now supports a full local-first deterministic execution workflow:
322
+ - record
323
+ - replay
324
+ - diff
325
+ - regression
326
+ - trust inspection
327
+
328
+ Natural next expansion tracks:
329
+ - WebGL graph layer for large trace topology
330
+ - WASM diff/hashing accelerators
331
+ - multi-user relay and RBAC
332
+ - compliance bundle export pipeline
333
+
334
+ ---
335
+
336
+ ## Troubleshooting
337
+
338
+ - Use `causallock ui` or `uvicorn causallock.ui.server:app --host 127.0.0.1 --port 8765` to start UI backend.
339
+ - Do not rely on `python -m causallock.ui.server` unless you explicitly add a module runner block.
340
+ - If replay says `Trace file not found`, first list available files: `dir regression` then replay with an exact filename.
341
+ - If `causallock` command is not found, use `python -m causallock.cli.main --help` or add Python Scripts path to PATH.
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version('causallock')
5
+ except PackageNotFoundError: # pragma: no cover
6
+ __version__ = '0.0.0'
@@ -0,0 +1,115 @@
1
+ # causallock/cache/tool_vcr.py
2
+
3
+ from typing import Callable, Any, Dict, Optional
4
+ from functools import wraps
5
+ import hashlib
6
+
7
+ from causallock.interceptor.mode import AuditMode
8
+ from causallock.core.trace import TraceBuilder
9
+ from causallock.core.serializer import canonical_dumps
10
+ from causallock.replay.engine import ReplayEngine, ReplayDivergenceError
11
+
12
+
13
+ class ToolDivergenceError(Exception):
14
+ pass
15
+
16
+
17
+ class ToolVCR:
18
+ """
19
+ Deterministic Tool VCR layer.
20
+
21
+ RECORD mode:
22
+ - Executes tool
23
+ - Stores input/output in trace
24
+
25
+ REPLAY mode:
26
+ - Skips execution
27
+ - Returns cached output
28
+ - Enforces strict divergence detection
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ mode: AuditMode,
34
+ trace_builder: Optional[TraceBuilder] = None,
35
+ replay_engine: Optional[ReplayEngine] = None,
36
+ tool_version: str = "1",
37
+ deterministic: bool = True,
38
+ allow_side_effects_in_replay: bool = False,
39
+ ):
40
+ self._mode = mode
41
+ self._trace_builder = trace_builder
42
+ self._replay_engine = replay_engine
43
+ self._tool_version = tool_version
44
+ self._deterministic = deterministic
45
+ self._allow_side_effects_in_replay = allow_side_effects_in_replay
46
+
47
+ def wrap(self, func: Callable[..., Any]) -> Callable[..., Any]:
48
+ """
49
+ Decorator to wrap deterministic tool execution.
50
+ """
51
+
52
+ @wraps(func)
53
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
54
+
55
+ payload: Dict[str, Any] = {
56
+ "args": args,
57
+ "kwargs": kwargs,
58
+ "tool_version": self._tool_version,
59
+ "deterministic": self._deterministic,
60
+ }
61
+ payload_hash = hashlib.sha256(canonical_dumps(payload)).hexdigest()
62
+
63
+ # ---------------------------
64
+ # REPLAY MODE
65
+ # ---------------------------
66
+ if self._mode == AuditMode.REPLAY:
67
+
68
+ if self._replay_engine is None:
69
+ raise RuntimeError(
70
+ "Replay engine not provided in REPLAY mode."
71
+ )
72
+
73
+ try:
74
+ event_output = self._replay_engine.next_tool_call(payload)
75
+ except ReplayDivergenceError as e:
76
+ raise ToolDivergenceError(str(e)) from e
77
+
78
+ if "result" not in event_output:
79
+ raise ToolDivergenceError(
80
+ "Malformed tool replay payload: missing 'result'."
81
+ )
82
+ if not self._deterministic and not self._allow_side_effects_in_replay:
83
+ raise ToolDivergenceError(
84
+ "Replay disallowed for non-deterministic side-effect tool."
85
+ )
86
+ if event_output.get("payload_hash") != payload_hash:
87
+ raise ToolDivergenceError(
88
+ "Tool payload hash mismatch during replay."
89
+ )
90
+
91
+ return event_output["result"]
92
+
93
+ # ---------------------------
94
+ # RECORD MODE
95
+ # ---------------------------
96
+ result = func(*args, **kwargs)
97
+
98
+ if self._trace_builder is None:
99
+ raise RuntimeError(
100
+ "TraceBuilder not provided in RECORD mode."
101
+ )
102
+
103
+ self._trace_builder.add_event(
104
+ event_type="tool_call",
105
+ input_data=payload,
106
+ output_data={
107
+ "result": result,
108
+ "payload_hash": payload_hash,
109
+ "tool_version": self._tool_version,
110
+ },
111
+ )
112
+
113
+ return result
114
+
115
+ return wrapper
File without changes
@@ -0,0 +1,44 @@
1
+ # causallock/cli/commands/diff.py
2
+
3
+ import typer
4
+ from pathlib import Path
5
+ from rich.console import Console
6
+
7
+ from causallock.core.storage import TraceStorage
8
+ from causallock.diff.engine import TraceDiffer
9
+
10
+ app = typer.Typer()
11
+ console = Console()
12
+
13
+
14
+ @app.command()
15
+ def run(
16
+ trace_a: Path,
17
+ trace_b: Path,
18
+ semantic: bool = typer.Option(
19
+ False, help="Ignore non-impacting metadata fields (timestamps, etc)."
20
+ ),
21
+ ):
22
+ """
23
+ Diff two .traj files.
24
+ """
25
+
26
+ if not trace_a.exists() or not trace_b.exists():
27
+ console.print("[red]Trace file not found.[/red]")
28
+ raise typer.Exit(code=1)
29
+
30
+ t1 = TraceStorage.load(trace_a, verify_integrity=True)
31
+ t2 = TraceStorage.load(trace_b, verify_integrity=True)
32
+
33
+ result = TraceDiffer.diff(t1, t2, semantic=semantic)
34
+
35
+ if result.diverged:
36
+ console.print("[red]Traces diverged.[/red]")
37
+ console.print(f"Index: {result.index}")
38
+ console.print(f"Reason: {result.reason}")
39
+ console.print(f"Classification: {result.classification}")
40
+ if result.details:
41
+ console.print(f"Details: {result.details}")
42
+ raise typer.Exit(code=1)
43
+
44
+ console.print("[green]Traces are identical.[/green]")
@@ -0,0 +1,35 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ import typer
5
+ from rich.console import Console
6
+
7
+
8
+ app = typer.Typer()
9
+ console = Console()
10
+
11
+
12
+ @app.command()
13
+ def run(
14
+ directory: Path,
15
+ dry_run: bool = typer.Option(False, help="Preview files without changing mode."),
16
+ ):
17
+ """
18
+ Freeze regression snapshots by making .traj files read-only.
19
+ """
20
+ if not directory.exists():
21
+ console.print("[red]Directory not found.[/red]")
22
+ raise typer.Exit(code=1)
23
+
24
+ files = sorted(directory.glob("*.traj"), key=lambda p: p.name.lower())
25
+ if not files:
26
+ console.print("[yellow]No .traj files found.[/yellow]")
27
+ raise typer.Exit(code=0)
28
+
29
+ for file in files:
30
+ if dry_run:
31
+ console.print(f"[cyan]would-freeze[/cyan] {file}")
32
+ continue
33
+ mode = file.stat().st_mode
34
+ file.chmod(mode & ~os.W_OK)
35
+ console.print(f"[green]frozen[/green] {file}")
@@ -0,0 +1,37 @@
1
+ import typer
2
+ from pathlib import Path
3
+ from rich.console import Console
4
+
5
+ from causallock.cli.trajignore import ensure_trajignore
6
+
7
+ app = typer.Typer()
8
+ console = Console()
9
+
10
+
11
+ @app.command()
12
+ def run(
13
+ project_root: Path = typer.Argument(
14
+ Path("."),
15
+ help="Project directory for causallock initialization.",
16
+ )
17
+ ):
18
+ """
19
+ Initialize causallock project defaults.
20
+ """
21
+ root = project_root.resolve()
22
+ root.mkdir(parents=True, exist_ok=True)
23
+
24
+ regression_dir = root / "regression"
25
+ cas_dir = root / "storage"
26
+ regression_dir.mkdir(parents=True, exist_ok=True)
27
+ cas_dir.mkdir(parents=True, exist_ok=True)
28
+
29
+ changed = ensure_trajignore(root)
30
+
31
+ console.print(f"[green]Initialized causallock at {root}[/green]")
32
+ console.print(f"Regression dir: {regression_dir}")
33
+ console.print(f"CAS storage dir: {cas_dir}")
34
+ if changed:
35
+ console.print("[green].trajignore updated[/green]")
36
+ else:
37
+ console.print("[yellow].trajignore already up to date[/yellow]")
@@ -0,0 +1,61 @@
1
+ # causallock/cli/commands/replay.py
2
+
3
+ import typer
4
+ from pathlib import Path
5
+ from rich.console import Console
6
+
7
+ from causallock.core.storage import TraceStorage
8
+ from causallock.replay.engine import ReplayEngine
9
+ from causallock.cli.trajignore import ensure_trajignore
10
+
11
+ app = typer.Typer()
12
+ console = Console()
13
+
14
+
15
+ @app.command()
16
+ def run(
17
+ trace_path: Path,
18
+ immutable: bool = typer.Option(
19
+ False, help="Enable immutable replay mode."
20
+ ),
21
+ force_env_mismatch: bool = typer.Option(
22
+ False, help="Allow replay even when environment fingerprint differs."
23
+ ),
24
+ seed: int | None = typer.Option(
25
+ None, help="Expected deterministic seed recorded in the trace."
26
+ ),
27
+ ):
28
+ """
29
+ Replay a .traj file and verify integrity.
30
+ """
31
+ if not trace_path.exists():
32
+ console.print("[red]Trace file not found.[/red]")
33
+ raise typer.Exit(code=1)
34
+
35
+ ensure_trajignore(Path.cwd())
36
+
37
+ try:
38
+ stat_before = trace_path.stat()
39
+ trace = TraceStorage.load(
40
+ trace_path,
41
+ verify_integrity=True,
42
+ verify_signature=immutable,
43
+ )
44
+ ReplayEngine(
45
+ trace,
46
+ strict=immutable,
47
+ immutable=immutable,
48
+ force_env_mismatch=force_env_mismatch,
49
+ expected_seed=seed,
50
+ )
51
+ if immutable:
52
+ stat_after = trace_path.stat()
53
+ if (stat_before.st_mtime_ns != stat_after.st_mtime_ns) or (
54
+ stat_before.st_size != stat_after.st_size
55
+ ):
56
+ raise RuntimeError("Immutable replay violation: trace file changed during replay.")
57
+
58
+ console.print("[green]Replay validation successful.[/green]")
59
+ except Exception as e:
60
+ console.print(f"[red]Replay failed:[/red] {e}")
61
+ raise typer.Exit(code=1)
File without changes