execweave 0.6.0__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.
- execweave/__init__.py +3 -0
- execweave/__main__.py +5 -0
- execweave/analysis.py +406 -0
- execweave/backends.py +63 -0
- execweave/benchmark.py +80 -0
- execweave/claude_adapter.py +448 -0
- execweave/claude_hook_cli.py +101 -0
- execweave/claude_record.py +106 -0
- execweave/cli.py +588 -0
- execweave/codex_adapter.py +314 -0
- execweave/codex_hook_cli.py +98 -0
- execweave/codex_record.py +111 -0
- execweave/collector.py +301 -0
- execweave/correlation.py +604 -0
- execweave/cursor_adapter.py +347 -0
- execweave/cursor_hook_cli.py +82 -0
- execweave/cursor_record.py +96 -0
- execweave/filesystem.py +103 -0
- execweave/focus.py +118 -0
- execweave/gemini_adapter.py +265 -0
- execweave/gemini_hook_cli.py +77 -0
- execweave/gemini_record.py +94 -0
- execweave/graph.py +300 -0
- execweave/graph_ops.py +446 -0
- execweave/inference_gateway.py +422 -0
- execweave/inference_gateway_cli.py +106 -0
- execweave/inference_identity.py +76 -0
- execweave/inference_identity_cli.py +60 -0
- execweave/live.py +275 -0
- execweave/model_runtime.py +535 -0
- execweave/model_runtime_cli.py +154 -0
- execweave/opencode_adapter.py +316 -0
- execweave/opencode_hook_cli.py +57 -0
- execweave/opencode_plugin_cli.py +110 -0
- execweave/opencode_record.py +96 -0
- execweave/overhead_benchmark.py +440 -0
- execweave/provider_record.py +215 -0
- execweave/schema.py +62 -0
- execweave/semantic.py +346 -0
- execweave/sink.py +33 -0
- execweave/strace_backend.py +682 -0
- execweave/validate.py +193 -0
- execweave/viewer.py +283 -0
- execweave/workflow.py +114 -0
- execweave-0.6.0.dist-info/METADATA +356 -0
- execweave-0.6.0.dist-info/RECORD +49 -0
- execweave-0.6.0.dist-info/WHEEL +4 -0
- execweave-0.6.0.dist-info/entry_points.txt +17 -0
- execweave-0.6.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
_MAX_COMMAND_CHARS = 4096
|
|
13
|
+
_MAX_LABEL_CHARS = 160
|
|
14
|
+
_SUPPORTED_EVENTS = {"SessionStart", "PreToolUse", "PostToolUse"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _now() -> str:
|
|
18
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _entity(
|
|
22
|
+
entity_type: str,
|
|
23
|
+
entity_id: str,
|
|
24
|
+
*,
|
|
25
|
+
name: str | None = None,
|
|
26
|
+
attributes: dict[str, Any] | None = None,
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
return {
|
|
29
|
+
"type": entity_type,
|
|
30
|
+
"id": entity_id,
|
|
31
|
+
"name": name,
|
|
32
|
+
"attributes": attributes or {},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _event(
|
|
37
|
+
*,
|
|
38
|
+
timestamp: str,
|
|
39
|
+
event_type: str,
|
|
40
|
+
relation: str,
|
|
41
|
+
source: dict[str, Any],
|
|
42
|
+
target: dict[str, Any],
|
|
43
|
+
attributes: dict[str, Any] | None = None,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
merged_attributes: dict[str, Any] = {
|
|
46
|
+
"backend": "semantic",
|
|
47
|
+
"attribution": "codex_hook",
|
|
48
|
+
"evidence_source": "provider_hook",
|
|
49
|
+
"provider": "codex",
|
|
50
|
+
"causal": False,
|
|
51
|
+
}
|
|
52
|
+
if attributes:
|
|
53
|
+
merged_attributes.update(attributes)
|
|
54
|
+
return {
|
|
55
|
+
"timestamp": timestamp,
|
|
56
|
+
"event_type": event_type,
|
|
57
|
+
"relation": relation,
|
|
58
|
+
"source": source,
|
|
59
|
+
"target": target,
|
|
60
|
+
"attributes": merged_attributes,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _clean_text(value: object, *, limit: int) -> tuple[str | None, bool]:
|
|
65
|
+
if not isinstance(value, str):
|
|
66
|
+
return None, False
|
|
67
|
+
text = value.replace("\x00", "")
|
|
68
|
+
if len(text) <= limit:
|
|
69
|
+
return text, False
|
|
70
|
+
return text[:limit], True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _main_agent() -> dict[str, Any]:
|
|
74
|
+
return _entity("agent", "agent:OpenAI Codex", name="OpenAI Codex")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _actor(payload: dict[str, Any]) -> dict[str, Any]:
|
|
78
|
+
agent_id = payload.get("agent_id")
|
|
79
|
+
if isinstance(agent_id, str) and agent_id:
|
|
80
|
+
agent_type = payload.get("agent_type")
|
|
81
|
+
name = agent_type if isinstance(agent_type, str) and agent_type else "Codex subagent"
|
|
82
|
+
session_id = payload.get("session_id")
|
|
83
|
+
scope = session_id if isinstance(session_id, str) and session_id else "unknown"
|
|
84
|
+
return _entity(
|
|
85
|
+
"agent",
|
|
86
|
+
f"agent:codex:{scope}:subagent:{agent_id}",
|
|
87
|
+
name=name,
|
|
88
|
+
attributes={"provider": "codex", "agent_id": agent_id, "agent_type": name},
|
|
89
|
+
)
|
|
90
|
+
return _main_agent()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _common_attributes(payload: dict[str, Any]) -> dict[str, Any]:
|
|
94
|
+
result: dict[str, Any] = {}
|
|
95
|
+
for key in (
|
|
96
|
+
"session_id",
|
|
97
|
+
"turn_id",
|
|
98
|
+
"cwd",
|
|
99
|
+
"permission_mode",
|
|
100
|
+
"agent_id",
|
|
101
|
+
"agent_type",
|
|
102
|
+
"model",
|
|
103
|
+
):
|
|
104
|
+
value = payload.get(key)
|
|
105
|
+
if isinstance(value, (str, int, float, bool)) and value != "":
|
|
106
|
+
result[f"codex_{key}"] = value
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _tool_entity(tool_name: str) -> dict[str, Any]:
|
|
111
|
+
return _entity(
|
|
112
|
+
"tool",
|
|
113
|
+
f"tool:codex:{tool_name}",
|
|
114
|
+
name=tool_name,
|
|
115
|
+
attributes={"provider": "codex", "native_name": tool_name},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _tool_call_entity(payload: dict[str, Any], tool_name: str) -> dict[str, Any]:
|
|
120
|
+
session_id = payload.get("session_id")
|
|
121
|
+
tool_use_id = payload.get("tool_use_id")
|
|
122
|
+
session = session_id if isinstance(session_id, str) and session_id else "unknown"
|
|
123
|
+
use_id = tool_use_id if isinstance(tool_use_id, str) and tool_use_id else "unknown"
|
|
124
|
+
attrs = _common_attributes(payload)
|
|
125
|
+
attrs.update({"provider": "codex", "tool_name": tool_name, "tool_use_id": use_id})
|
|
126
|
+
tool_input = payload.get("tool_input")
|
|
127
|
+
if isinstance(tool_input, dict):
|
|
128
|
+
attrs["input_keys"] = sorted(str(key) for key in tool_input)
|
|
129
|
+
return _entity(
|
|
130
|
+
"tool_call",
|
|
131
|
+
f"tool-call:codex:{session}:{use_id}",
|
|
132
|
+
name=tool_name,
|
|
133
|
+
attributes=attrs,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _command_entity(tool_input: dict[str, Any]) -> dict[str, Any] | None:
|
|
138
|
+
command, truncated = _clean_text(tool_input.get("command"), limit=_MAX_COMMAND_CHARS)
|
|
139
|
+
if not command:
|
|
140
|
+
return None
|
|
141
|
+
digest = hashlib.sha256(command.encode("utf-8", errors="replace")).hexdigest()
|
|
142
|
+
label, _ = _clean_text(command.replace("\n", " "), limit=_MAX_LABEL_CHARS)
|
|
143
|
+
return _entity(
|
|
144
|
+
"command",
|
|
145
|
+
f"command:sha256:{digest}",
|
|
146
|
+
name=label,
|
|
147
|
+
attributes={"command": command, "truncated": truncated},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _session_start_events(payload: dict[str, Any], *, timestamp: str) -> list[dict[str, Any]]:
|
|
152
|
+
model = payload.get("model")
|
|
153
|
+
if not isinstance(model, str) or not model:
|
|
154
|
+
return []
|
|
155
|
+
attrs = _common_attributes(payload)
|
|
156
|
+
source = payload.get("source")
|
|
157
|
+
if isinstance(source, str) and source:
|
|
158
|
+
attrs["codex_session_source"] = source
|
|
159
|
+
return [
|
|
160
|
+
_event(
|
|
161
|
+
timestamp=timestamp,
|
|
162
|
+
event_type="semantic.codex.model.observed",
|
|
163
|
+
relation="USED_MODEL",
|
|
164
|
+
source=_main_agent(),
|
|
165
|
+
target=_entity(
|
|
166
|
+
"model",
|
|
167
|
+
f"model:codex:{model}",
|
|
168
|
+
name=model,
|
|
169
|
+
attributes={"provider": "codex"},
|
|
170
|
+
),
|
|
171
|
+
attributes=attrs,
|
|
172
|
+
)
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _tool_pre_events(payload: dict[str, Any], *, timestamp: str) -> list[dict[str, Any]]:
|
|
177
|
+
tool_name = payload.get("tool_name")
|
|
178
|
+
tool_use_id = payload.get("tool_use_id")
|
|
179
|
+
if not isinstance(tool_name, str) or not tool_name:
|
|
180
|
+
raise ValueError("PreToolUse requires tool_name")
|
|
181
|
+
if not isinstance(tool_use_id, str) or not tool_use_id:
|
|
182
|
+
raise ValueError("PreToolUse requires tool_use_id")
|
|
183
|
+
|
|
184
|
+
call = _tool_call_entity(payload, tool_name)
|
|
185
|
+
tool = _tool_entity(tool_name)
|
|
186
|
+
common = _common_attributes(payload)
|
|
187
|
+
events = [
|
|
188
|
+
_event(
|
|
189
|
+
timestamp=timestamp,
|
|
190
|
+
event_type="semantic.codex.tool.requested",
|
|
191
|
+
relation="REQUESTED_TOOL_CALL",
|
|
192
|
+
source=_actor(payload),
|
|
193
|
+
target=call,
|
|
194
|
+
attributes=common,
|
|
195
|
+
),
|
|
196
|
+
_event(
|
|
197
|
+
timestamp=timestamp,
|
|
198
|
+
event_type="semantic.codex.tool.selected",
|
|
199
|
+
relation="USES_TOOL",
|
|
200
|
+
source=call,
|
|
201
|
+
target=tool,
|
|
202
|
+
attributes=common,
|
|
203
|
+
),
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
tool_input = payload.get("tool_input")
|
|
207
|
+
if tool_name == "Bash" and isinstance(tool_input, dict):
|
|
208
|
+
command = _command_entity(tool_input)
|
|
209
|
+
if command is not None:
|
|
210
|
+
events.append(
|
|
211
|
+
_event(
|
|
212
|
+
timestamp=timestamp,
|
|
213
|
+
event_type="semantic.codex.command.declared",
|
|
214
|
+
relation="DECLARED_COMMAND",
|
|
215
|
+
source=call,
|
|
216
|
+
target=command,
|
|
217
|
+
attributes=common,
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
return events
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _tool_post_events(payload: dict[str, Any], *, timestamp: str) -> list[dict[str, Any]]:
|
|
224
|
+
tool_name = payload.get("tool_name")
|
|
225
|
+
tool_use_id = payload.get("tool_use_id")
|
|
226
|
+
if not isinstance(tool_name, str) or not tool_name:
|
|
227
|
+
raise ValueError("PostToolUse requires tool_name")
|
|
228
|
+
if not isinstance(tool_use_id, str) or not tool_use_id:
|
|
229
|
+
raise ValueError("PostToolUse requires tool_use_id")
|
|
230
|
+
|
|
231
|
+
attrs = _common_attributes(payload)
|
|
232
|
+
response = payload.get("tool_response")
|
|
233
|
+
if isinstance(response, str):
|
|
234
|
+
attrs["tool_response_type"] = "string"
|
|
235
|
+
attrs["tool_response_chars"] = len(response)
|
|
236
|
+
elif response is not None:
|
|
237
|
+
attrs["tool_response_type"] = type(response).__name__
|
|
238
|
+
attrs["outcome_semantics"] = "provider_reported_completion_without_reliable_success_signal"
|
|
239
|
+
return [
|
|
240
|
+
_event(
|
|
241
|
+
timestamp=timestamp,
|
|
242
|
+
event_type="semantic.codex.tool.returned",
|
|
243
|
+
relation="TOOL_CALL_RETURNED",
|
|
244
|
+
source=_tool_call_entity(payload, tool_name),
|
|
245
|
+
target=_tool_entity(tool_name),
|
|
246
|
+
attributes=attrs,
|
|
247
|
+
)
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def codex_hook_to_semantic_events(
|
|
252
|
+
payload: dict[str, Any],
|
|
253
|
+
*,
|
|
254
|
+
timestamp: str | None = None,
|
|
255
|
+
) -> list[dict[str, Any]]:
|
|
256
|
+
hook_event = payload.get("hook_event_name")
|
|
257
|
+
if not isinstance(hook_event, str) or not hook_event:
|
|
258
|
+
raise ValueError("Codex hook payload requires hook_event_name")
|
|
259
|
+
if hook_event not in _SUPPORTED_EVENTS:
|
|
260
|
+
return []
|
|
261
|
+
observed_at = timestamp or _now()
|
|
262
|
+
if hook_event == "SessionStart":
|
|
263
|
+
return _session_start_events(payload, timestamp=observed_at)
|
|
264
|
+
if hook_event == "PreToolUse":
|
|
265
|
+
return _tool_pre_events(payload, timestamp=observed_at)
|
|
266
|
+
if hook_event == "PostToolUse":
|
|
267
|
+
return _tool_post_events(payload, timestamp=observed_at)
|
|
268
|
+
return []
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def append_semantic_records(path: str | Path, records: list[dict[str, Any]]) -> Path:
|
|
272
|
+
output = Path(path).expanduser().resolve()
|
|
273
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
274
|
+
if not records:
|
|
275
|
+
return output
|
|
276
|
+
blob = "".join(
|
|
277
|
+
json.dumps(record, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n"
|
|
278
|
+
for record in records
|
|
279
|
+
)
|
|
280
|
+
lock_dir = output.with_name(output.name + ".lock")
|
|
281
|
+
deadline = time.monotonic() + 5.0
|
|
282
|
+
while True:
|
|
283
|
+
try:
|
|
284
|
+
lock_dir.mkdir()
|
|
285
|
+
break
|
|
286
|
+
except FileExistsError:
|
|
287
|
+
if time.monotonic() >= deadline:
|
|
288
|
+
raise TimeoutError(f"timed out waiting for semantic sidecar lock: {lock_dir}")
|
|
289
|
+
time.sleep(0.01)
|
|
290
|
+
try:
|
|
291
|
+
with output.open("a", encoding="utf-8", newline="\n") as handle:
|
|
292
|
+
handle.write(blob)
|
|
293
|
+
handle.flush()
|
|
294
|
+
os.fsync(handle.fileno())
|
|
295
|
+
finally:
|
|
296
|
+
try:
|
|
297
|
+
lock_dir.rmdir()
|
|
298
|
+
except OSError:
|
|
299
|
+
pass
|
|
300
|
+
return output
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def read_hook_payload(stream: Any = None) -> dict[str, Any]:
|
|
304
|
+
source = stream if stream is not None else sys.stdin
|
|
305
|
+
raw = source.read()
|
|
306
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
307
|
+
raise ValueError("Codex hook stdin is empty")
|
|
308
|
+
try:
|
|
309
|
+
payload = json.loads(raw)
|
|
310
|
+
except json.JSONDecodeError as exc:
|
|
311
|
+
raise ValueError(f"Codex hook stdin is invalid JSON: {exc.msg}") from exc
|
|
312
|
+
if not isinstance(payload, dict):
|
|
313
|
+
raise ValueError("Codex hook stdin must be one JSON object")
|
|
314
|
+
return payload
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .codex_adapter import append_semantic_records, codex_hook_to_semantic_events, read_hook_payload
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _hook_handler(command: str) -> dict[str, str]:
|
|
14
|
+
return {"type": "command", "command": command}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def codex_hook_config(command: str = "execweave-codex-hook") -> dict[str, Any]:
|
|
18
|
+
handler = _hook_handler(command)
|
|
19
|
+
tool_group = {"matcher": ".*", "hooks": [handler]}
|
|
20
|
+
plain_group = {"hooks": [handler]}
|
|
21
|
+
return {
|
|
22
|
+
"hooks": {
|
|
23
|
+
"SessionStart": [plain_group],
|
|
24
|
+
"PreToolUse": [tool_group],
|
|
25
|
+
"PostToolUse": [tool_group],
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _default_sidecar(payload: dict[str, Any]) -> Path:
|
|
31
|
+
cwd = payload.get("cwd")
|
|
32
|
+
session_id = payload.get("session_id")
|
|
33
|
+
if not isinstance(cwd, str) or not cwd:
|
|
34
|
+
raise ValueError("Codex hook payload has no cwd for automatic sidecar placement")
|
|
35
|
+
if not isinstance(session_id, str) or not session_id:
|
|
36
|
+
raise ValueError("Codex hook payload has no session_id for automatic sidecar placement")
|
|
37
|
+
safe_session = "".join(
|
|
38
|
+
character if character.isalnum() or character in {"-", "_", "."} else "_"
|
|
39
|
+
for character in session_id
|
|
40
|
+
)
|
|
41
|
+
return Path(cwd) / ".execweave" / "semantic" / "codex" / f"{safe_session}.jsonl"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
45
|
+
parser = argparse.ArgumentParser(
|
|
46
|
+
prog="execweave-codex-hook",
|
|
47
|
+
description="Capture OpenAI Codex lifecycle hook input as local ExecWeave semantic telemetry.",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--sidecar",
|
|
51
|
+
type=Path,
|
|
52
|
+
default=None,
|
|
53
|
+
help=(
|
|
54
|
+
"Semantic JSONL output path. Defaults to EXECWEAVE_SEMANTIC_SIDECAR, then "
|
|
55
|
+
"<cwd>/.execweave/semantic/codex/<Codex-session-id>.jsonl."
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--strict",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="Return non-zero on telemetry errors. Default is fail-open so tracing cannot block Codex.",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--print-config",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="Print a Codex hooks.json fragment for the supported ExecWeave lifecycle hooks and exit.",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--command",
|
|
70
|
+
default="execweave-codex-hook",
|
|
71
|
+
help="Hook command embedded by --print-config (default: execweave-codex-hook).",
|
|
72
|
+
)
|
|
73
|
+
return parser
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main(argv: list[str] | None = None) -> int:
|
|
77
|
+
parser = build_parser()
|
|
78
|
+
args = parser.parse_args(argv)
|
|
79
|
+
if args.print_config:
|
|
80
|
+
print(json.dumps(codex_hook_config(args.command), indent=2, sort_keys=True))
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
payload = read_hook_payload()
|
|
85
|
+
sidecar = args.sidecar
|
|
86
|
+
if sidecar is None:
|
|
87
|
+
configured = os.environ.get("EXECWEAVE_SEMANTIC_SIDECAR")
|
|
88
|
+
sidecar = Path(configured) if configured else _default_sidecar(payload)
|
|
89
|
+
records = codex_hook_to_semantic_events(payload)
|
|
90
|
+
append_semantic_records(sidecar, records)
|
|
91
|
+
except (OSError, TimeoutError, ValueError) as exc:
|
|
92
|
+
print(f"ExecWeave Codex hook warning: {exc}", file=sys.stderr)
|
|
93
|
+
return 1 if args.strict else 0
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .backends import BackendName
|
|
8
|
+
from .provider_record import ProviderRecordResult, record_provider_to_viewer
|
|
9
|
+
|
|
10
|
+
CodexRecordResult = ProviderRecordResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def record_codex_to_viewer(
|
|
14
|
+
command: list[str],
|
|
15
|
+
*,
|
|
16
|
+
watch_root: str | Path,
|
|
17
|
+
output_dir: str | Path | None = None,
|
|
18
|
+
backend: BackendName = "auto",
|
|
19
|
+
poll_interval: float = 0.10,
|
|
20
|
+
collect_filesystem: bool = True,
|
|
21
|
+
collect_network: bool = True,
|
|
22
|
+
keep_raw_trace: bool = False,
|
|
23
|
+
correlation_window_ms: int = 3000,
|
|
24
|
+
open_browser: bool = False,
|
|
25
|
+
) -> CodexRecordResult:
|
|
26
|
+
"""Record one Codex run using the shared provider-record pipeline.
|
|
27
|
+
|
|
28
|
+
Codex must already be configured to invoke ``execweave-codex-hook``. This
|
|
29
|
+
wrapper never edits Codex settings; it only binds the child process to the
|
|
30
|
+
run-specific semantic sidecar managed by the shared core.
|
|
31
|
+
"""
|
|
32
|
+
return record_provider_to_viewer(
|
|
33
|
+
command,
|
|
34
|
+
provider_name="Codex",
|
|
35
|
+
watch_root=watch_root,
|
|
36
|
+
output_dir=output_dir,
|
|
37
|
+
backend=backend,
|
|
38
|
+
poll_interval=poll_interval,
|
|
39
|
+
collect_filesystem=collect_filesystem,
|
|
40
|
+
collect_network=collect_network,
|
|
41
|
+
keep_raw_trace=keep_raw_trace,
|
|
42
|
+
correlation_window_ms=correlation_window_ms,
|
|
43
|
+
open_browser=open_browser,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _clean_command(command: list[str]) -> list[str]:
|
|
48
|
+
result = list(command)
|
|
49
|
+
if result and result[0] == "--":
|
|
50
|
+
result = result[1:]
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
55
|
+
parser = argparse.ArgumentParser(
|
|
56
|
+
prog="execweave-codex-record",
|
|
57
|
+
description=(
|
|
58
|
+
"Record runtime evidence, OpenAI Codex hook telemetry, and conservative "
|
|
59
|
+
"Tool-to-Process correlation in one local run."
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument("--watch-root", type=Path, default=None)
|
|
63
|
+
parser.add_argument("--output-dir", type=Path, default=None)
|
|
64
|
+
parser.add_argument("--interval", type=float, default=0.10)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--backend",
|
|
67
|
+
choices=["auto", "portable", "strace"],
|
|
68
|
+
default="auto",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--correlation-window-ms",
|
|
72
|
+
type=int,
|
|
73
|
+
default=3000,
|
|
74
|
+
help="maximum Tool-to-Process correlation window in milliseconds (default: 3000)",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument("--no-files", action="store_true")
|
|
77
|
+
parser.add_argument("--no-network", action="store_true")
|
|
78
|
+
parser.add_argument("--keep-native-trace", action="store_true")
|
|
79
|
+
parser.add_argument("--open", action="store_true", dest="open_browser")
|
|
80
|
+
parser.add_argument("command", nargs=argparse.REMAINDER)
|
|
81
|
+
return parser
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main(argv: list[str] | None = None) -> int:
|
|
85
|
+
parser = build_parser()
|
|
86
|
+
args = parser.parse_args(argv)
|
|
87
|
+
command = _clean_command(args.command)
|
|
88
|
+
if not command:
|
|
89
|
+
parser.error("a Codex command is required, e.g. execweave-codex-record --open -- codex")
|
|
90
|
+
watch_root = (args.watch_root or Path.cwd()).expanduser().resolve()
|
|
91
|
+
try:
|
|
92
|
+
result = record_codex_to_viewer(
|
|
93
|
+
command,
|
|
94
|
+
watch_root=watch_root,
|
|
95
|
+
output_dir=args.output_dir,
|
|
96
|
+
backend=args.backend,
|
|
97
|
+
poll_interval=args.interval,
|
|
98
|
+
collect_filesystem=not args.no_files,
|
|
99
|
+
collect_network=not args.no_network,
|
|
100
|
+
keep_raw_trace=args.keep_native_trace,
|
|
101
|
+
correlation_window_ms=args.correlation_window_ms,
|
|
102
|
+
open_browser=args.open_browser,
|
|
103
|
+
)
|
|
104
|
+
except (FileExistsError, RuntimeError, ValueError, OSError) as exc:
|
|
105
|
+
parser.error(str(exc))
|
|
106
|
+
print(json.dumps(result.to_dict(), indent=2, sort_keys=True))
|
|
107
|
+
return result.runtime.return_code
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
raise SystemExit(main())
|