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.
- causallock-0.1.0/PKG-INFO +15 -0
- causallock-0.1.0/README.md +341 -0
- causallock-0.1.0/causallock/__init__.py +6 -0
- causallock-0.1.0/causallock/cache/tool_vcr.py +115 -0
- causallock-0.1.0/causallock/cli/__init__.py +0 -0
- causallock-0.1.0/causallock/cli/commands/diff.py +44 -0
- causallock-0.1.0/causallock/cli/commands/freeze.py +35 -0
- causallock-0.1.0/causallock/cli/commands/init.py +37 -0
- causallock-0.1.0/causallock/cli/commands/replay.py +61 -0
- causallock-0.1.0/causallock/cli/commands/run.py +0 -0
- causallock-0.1.0/causallock/cli/commands/sign.py +148 -0
- causallock-0.1.0/causallock/cli/commands/test.py +87 -0
- causallock-0.1.0/causallock/cli/commands/ui.py +9 -0
- causallock-0.1.0/causallock/cli/main.py +32 -0
- causallock-0.1.0/causallock/cli/parallel.py +24 -0
- causallock-0.1.0/causallock/cli/trajignore.py +34 -0
- causallock-0.1.0/causallock/core/__init__.py +0 -0
- causallock-0.1.0/causallock/core/context.py +41 -0
- causallock-0.1.0/causallock/core/environment.py +42 -0
- causallock-0.1.0/causallock/core/hashing.py +34 -0
- causallock-0.1.0/causallock/core/models.py +48 -0
- causallock-0.1.0/causallock/core/schema.py +64 -0
- causallock-0.1.0/causallock/core/serialization.py +74 -0
- causallock-0.1.0/causallock/core/serializer.py +16 -0
- causallock-0.1.0/causallock/core/storage.py +196 -0
- causallock-0.1.0/causallock/core/trace.py +106 -0
- causallock-0.1.0/causallock/diff/engine.py +134 -0
- causallock-0.1.0/causallock/interceptor/base.py +29 -0
- causallock-0.1.0/causallock/interceptor/mode.py +8 -0
- causallock-0.1.0/causallock/interceptor/openai_wrapper.py +146 -0
- causallock-0.1.0/causallock/replay/engine.py +123 -0
- causallock-0.1.0/causallock/replay/immutable.py +21 -0
- causallock-0.1.0/causallock/runtime/__init__.py +11 -0
- causallock-0.1.0/causallock/runtime/determinism.py +101 -0
- causallock-0.1.0/causallock/scripts/generate_regression.py +74 -0
- causallock-0.1.0/causallock/security/default_rules.py +31 -0
- causallock-0.1.0/causallock/security/key_registry.py +16 -0
- causallock-0.1.0/causallock/security/redaction.py +48 -0
- causallock-0.1.0/causallock/security/signing.py +79 -0
- causallock-0.1.0/causallock/ui/__init__.py +0 -0
- causallock-0.1.0/causallock/ui/config.py +6 -0
- causallock-0.1.0/causallock/ui/schemas.py +55 -0
- causallock-0.1.0/causallock/ui/server.py +513 -0
- causallock-0.1.0/causallock/ui/services/__init__.py +0 -0
- causallock-0.1.0/causallock/ui/services/diff_service.py +19 -0
- causallock-0.1.0/causallock/ui/services/filesystem_service.py +8 -0
- causallock-0.1.0/causallock/ui/services/trace_service.py +165 -0
- causallock-0.1.0/causallock/ui/services/verify_service.py +39 -0
- causallock-0.1.0/causallock/ui/static/assets/index-CXwYAcTX.js +14 -0
- causallock-0.1.0/causallock/ui/static/assets/index-q9B6OTLp.css +1 -0
- causallock-0.1.0/causallock/ui/static/index.html +14 -0
- causallock-0.1.0/causallock/ui/static/vite.svg +1 -0
- causallock-0.1.0/causallock.egg-info/PKG-INFO +15 -0
- causallock-0.1.0/causallock.egg-info/SOURCES.txt +72 -0
- causallock-0.1.0/causallock.egg-info/dependency_links.txt +1 -0
- causallock-0.1.0/causallock.egg-info/entry_points.txt +2 -0
- causallock-0.1.0/causallock.egg-info/requires.txt +9 -0
- causallock-0.1.0/causallock.egg-info/top_level.txt +1 -0
- causallock-0.1.0/pyproject.toml +39 -0
- causallock-0.1.0/setup.cfg +4 -0
- causallock-0.1.0/tests/test_determinism_and_diff.py +40 -0
- causallock-0.1.0/tests/test_hash_chain.py +27 -0
- causallock-0.1.0/tests/test_integrity_fuzz.py +54 -0
- causallock-0.1.0/tests/test_interceptor.py +54 -0
- causallock-0.1.0/tests/test_redaction.py +14 -0
- causallock-0.1.0/tests/test_replay.py +70 -0
- causallock-0.1.0/tests/test_sanity.py +2 -0
- causallock-0.1.0/tests/test_schema_and_cas.py +47 -0
- causallock-0.1.0/tests/test_signed_trace.py +35 -0
- causallock-0.1.0/tests/test_signing.py +12 -0
- causallock-0.1.0/tests/test_storage.py +30 -0
- causallock-0.1.0/tests/test_stream_aggregation.py +58 -0
- causallock-0.1.0/tests/test_streaming.py +88 -0
- 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,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
|