byte-bot 0.1.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.
byte_bot/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """byte-bot."""
2
+
3
+ __version__ = "0.1.0"
byte_bot/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from byte_bot.cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,16 @@
1
+ """Static reviewed adapter-name registry.
2
+
3
+ Importing the package must not eagerly import adapter implementations: config and
4
+ legacy adapters depend on each other at module-load time. Runtime code imports the
5
+ specific reviewed implementation it needs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ ADAPTER_NAMES: tuple[str, ...] = (
11
+ "codex-exec-jsonl",
12
+ "legacy-command",
13
+ "opencode-http",
14
+ )
15
+
16
+ __all__ = ["ADAPTER_NAMES"]
@@ -0,0 +1,325 @@
1
+ """Version-pinned passive Codex exec JSONL observation.
2
+
3
+ The adapter reads one explicitly configured existing capture. It never invokes
4
+ Codex, resumes a thread, attaches to a session, or retains transcript/tool bodies.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ import json
11
+ import os
12
+ import stat as stat_module
13
+ from dataclasses import dataclass
14
+ from datetime import UTC, datetime
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from byte_bot.model import (
19
+ CollectionError,
20
+ Evidence,
21
+ Lifecycle,
22
+ Session,
23
+ SourceHealth,
24
+ SourceStatus,
25
+ )
26
+
27
+ ADAPTER_NAME = "codex-exec-jsonl"
28
+ SUPPORTED_VERSION = "0.155.1"
29
+ FIELD_VERSION = "codex-0.155.1-exec-jsonl"
30
+ CAPABILITIES = ("sessions", "source-health")
31
+ MAX_CAPTURE_BYTES = 64 * 1024
32
+ MAX_RECORDS = 32
33
+ MAX_THREAD_ID_LEN = 128
34
+
35
+ _IGNORED_ITEM_TYPES = frozenset({"item.started", "item.updated", "item.completed"})
36
+ _ALLOWED_TYPES = frozenset(
37
+ {
38
+ "thread.started",
39
+ "turn.started",
40
+ "turn.completed",
41
+ "turn.failed",
42
+ "error",
43
+ *_IGNORED_ITEM_TYPES,
44
+ }
45
+ )
46
+
47
+
48
+ class _CodexFailure(Exception):
49
+ def __init__(self, code: str, message: str) -> None:
50
+ super().__init__(code)
51
+ self.code = code
52
+ self.message = message
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class CodexObservation:
57
+ sessions: tuple[Session, ...] = ()
58
+ errors: tuple[CollectionError, ...] = ()
59
+ health: SourceHealth | None = None
60
+ complete: bool | None = None
61
+ version: str | None = None
62
+
63
+
64
+ def _read_capture(path: Path) -> bytes:
65
+ """Read a bounded regular file without following a runtime symlink."""
66
+ path = Path(path)
67
+ # Reject FIFOs after opening without waiting for a writer, including
68
+ # regular-file replacement races between metadata checks and open().
69
+ flags = os.O_RDONLY | getattr(os, "O_NONBLOCK", 0)
70
+ nofollow = getattr(os, "O_NOFOLLOW", 0)
71
+ before: os.stat_result | None = None
72
+ if nofollow:
73
+ flags |= nofollow
74
+ else:
75
+ before = path.lstat()
76
+ if stat_module.S_ISLNK(before.st_mode):
77
+ raise _CodexFailure("unsafe_path", "Codex capture path is unsafe")
78
+
79
+ try:
80
+ fd = os.open(path, flags)
81
+ except OSError as exc:
82
+ raise _CodexFailure("capture_unavailable", "Codex capture is unavailable") from exc
83
+ try:
84
+ metadata = os.fstat(fd)
85
+ if not stat_module.S_ISREG(metadata.st_mode):
86
+ raise _CodexFailure("unsafe_path", "Codex capture path is unsafe")
87
+ if before is not None and (
88
+ before.st_dev != metadata.st_dev or before.st_ino != metadata.st_ino
89
+ ):
90
+ raise _CodexFailure("unsafe_path", "Codex capture changed while opening")
91
+ if metadata.st_size > MAX_CAPTURE_BYTES:
92
+ raise _CodexFailure(
93
+ "capture_too_large",
94
+ "Codex capture exceeds the validated byte limit",
95
+ )
96
+ data = os.read(fd, MAX_CAPTURE_BYTES + 1)
97
+ if len(data) > MAX_CAPTURE_BYTES:
98
+ raise _CodexFailure(
99
+ "capture_too_large",
100
+ "Codex capture exceeds the validated byte limit",
101
+ )
102
+ return data
103
+ finally:
104
+ with contextlib.suppress(OSError):
105
+ os.close(fd)
106
+
107
+
108
+ def _valid_thread_id(value: object) -> bool:
109
+ return (
110
+ isinstance(value, str)
111
+ and bool(value)
112
+ and len(value) <= MAX_THREAD_ID_LEN
113
+ and not any(ord(char) < 0x20 or 0x7F <= ord(char) <= 0x9F for char in value)
114
+ )
115
+
116
+
117
+ def _error(
118
+ source_id: str,
119
+ failure: _CodexFailure,
120
+ observed_at: datetime,
121
+ *,
122
+ version: str | None,
123
+ ) -> CodexObservation:
124
+ return CodexObservation(
125
+ errors=(
126
+ CollectionError(
127
+ code=failure.code,
128
+ message=failure.message,
129
+ source_id=source_id,
130
+ ),
131
+ ),
132
+ health=SourceHealth(
133
+ source_id=source_id,
134
+ status=SourceStatus.ERROR,
135
+ detail=failure.message,
136
+ observed_at=observed_at,
137
+ ),
138
+ version=version,
139
+ )
140
+
141
+
142
+ def _records(data: bytes) -> tuple[list[dict[str, Any]], bool]:
143
+ try:
144
+ text = data.decode("utf-8")
145
+ except UnicodeDecodeError:
146
+ raise _CodexFailure("malformed_capture", "Codex capture is not valid UTF-8") from None
147
+
148
+ partial = bool(data) and not data.endswith(b"\n")
149
+ lines = text.splitlines()
150
+ if partial and lines:
151
+ lines = lines[:-1]
152
+ lines = [line for line in lines if line.strip()]
153
+ if len(lines) > MAX_RECORDS:
154
+ raise _CodexFailure(
155
+ "too_many_records",
156
+ "Codex capture exceeds the validated record limit",
157
+ )
158
+
159
+ records: list[dict[str, Any]] = []
160
+ for line in lines:
161
+ try:
162
+ raw = json.loads(line)
163
+ except json.JSONDecodeError:
164
+ raise _CodexFailure(
165
+ "malformed_capture", "Codex capture contains invalid JSON"
166
+ ) from None
167
+ if not isinstance(raw, dict):
168
+ raise _CodexFailure("malformed_capture", "Codex capture record must be an object")
169
+ event_type = raw.get("type")
170
+ if not isinstance(event_type, str) or event_type not in _ALLOWED_TYPES:
171
+ raise _CodexFailure(
172
+ "unsupported_event",
173
+ "Codex capture contains an unsupported event type",
174
+ )
175
+ records.append(raw)
176
+ return records, partial
177
+
178
+
179
+ def observe_codex_capture(
180
+ *,
181
+ source_id: str,
182
+ path: Path,
183
+ source_version: str,
184
+ now: datetime | None = None,
185
+ ) -> CodexObservation:
186
+ """Map a validated Codex 0.155.1 exec capture to one typed session."""
187
+ observed_at = now or datetime.now(UTC)
188
+ version = source_version if isinstance(source_version, str) else None
189
+ try:
190
+ if source_version != SUPPORTED_VERSION:
191
+ raise _CodexFailure("unsupported_version", "Codex capture version is not supported")
192
+ records, partial = _records(_read_capture(path))
193
+ if not records:
194
+ raise _CodexFailure("empty_capture", "Codex capture contains no complete records")
195
+
196
+ thread_id: str | None = None
197
+ turn_started = False
198
+ lifecycle = Lifecycle.UNKNOWN
199
+ terminal = False
200
+
201
+ for raw in records:
202
+ event_type = raw["type"]
203
+ if event_type in _IGNORED_ITEM_TYPES:
204
+ continue
205
+
206
+ if event_type == "thread.started":
207
+ value = raw.get("thread_id")
208
+ if set(raw) != {"type", "thread_id"} or not _valid_thread_id(value):
209
+ raise _CodexFailure("malformed_thread", "Codex thread identity is invalid")
210
+ if thread_id is not None:
211
+ raise _CodexFailure(
212
+ "multiple_threads",
213
+ "Codex capture contains more than one thread",
214
+ )
215
+ thread_id = value
216
+ continue
217
+
218
+ if thread_id is None:
219
+ raise _CodexFailure("event_order", "Codex lifecycle event precedes thread identity")
220
+
221
+ if event_type == "turn.started":
222
+ if set(raw) != {"type"} or turn_started or terminal:
223
+ raise _CodexFailure(
224
+ "unsupported_turn_sequence",
225
+ "Codex capture turn sequence is unsupported",
226
+ )
227
+ turn_started = True
228
+ continue
229
+
230
+ if event_type == "turn.completed":
231
+ if not turn_started or terminal or set(raw) != {"type", "usage"}:
232
+ raise _CodexFailure(
233
+ "unsupported_turn_sequence",
234
+ "Codex capture turn sequence is unsupported",
235
+ )
236
+ if not isinstance(raw.get("usage"), dict):
237
+ raise _CodexFailure(
238
+ "malformed_completion",
239
+ "Codex turn completion shape is invalid",
240
+ )
241
+ lifecycle = Lifecycle.DONE
242
+ terminal = True
243
+ continue
244
+
245
+ if event_type == "turn.failed":
246
+ if not turn_started or terminal or "error" not in raw:
247
+ raise _CodexFailure(
248
+ "unsupported_turn_sequence",
249
+ "Codex capture turn sequence is unsupported",
250
+ )
251
+ lifecycle = Lifecycle.FAILED
252
+ terminal = True
253
+ continue
254
+
255
+ if event_type == "error":
256
+ if terminal:
257
+ raise _CodexFailure(
258
+ "unsupported_turn_sequence",
259
+ "Codex capture turn sequence is unsupported",
260
+ )
261
+ lifecycle = Lifecycle.FAILED
262
+ terminal = True
263
+
264
+ if thread_id is None:
265
+ raise _CodexFailure("missing_thread", "Codex capture has no thread identity")
266
+
267
+ adapter_id = f"{ADAPTER_NAME}:{source_id}"
268
+ session = Session(
269
+ id=f"{adapter_id}/{thread_id}",
270
+ adapter_instance_id=adapter_id,
271
+ native_id=thread_id,
272
+ work_id=thread_id,
273
+ lifecycle=lifecycle,
274
+ observed_at=observed_at,
275
+ evidence=(
276
+ Evidence(
277
+ source_kind=ADAPTER_NAME,
278
+ source_id=source_id,
279
+ field_version=FIELD_VERSION,
280
+ observed_at=observed_at,
281
+ ),
282
+ ),
283
+ )
284
+ errors: tuple[CollectionError, ...] = ()
285
+ status = SourceStatus.OK
286
+ detail: str | None = None
287
+ complete = True
288
+ if partial:
289
+ errors = (
290
+ CollectionError(
291
+ code="partial_record",
292
+ message="Codex capture has an incomplete trailing record",
293
+ source_id=source_id,
294
+ ),
295
+ )
296
+ status = SourceStatus.DEGRADED
297
+ detail = "Codex capture has an incomplete trailing record"
298
+ complete = False
299
+
300
+ return CodexObservation(
301
+ sessions=(session,),
302
+ errors=errors,
303
+ health=SourceHealth(
304
+ source_id=source_id,
305
+ status=status,
306
+ detail=detail,
307
+ observed_at=observed_at,
308
+ ),
309
+ complete=complete,
310
+ version=source_version,
311
+ )
312
+ except _CodexFailure as exc:
313
+ return _error(
314
+ source_id,
315
+ exc,
316
+ observed_at,
317
+ version=version,
318
+ )
319
+ except (OSError, TypeError, ValueError):
320
+ return _error(
321
+ source_id,
322
+ _CodexFailure("capture_unavailable", "Codex capture is unavailable"),
323
+ observed_at,
324
+ version=version,
325
+ )