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 +3 -0
- byte_bot/__main__.py +3 -0
- byte_bot/adapters/__init__.py +16 -0
- byte_bot/adapters/codex_exec_jsonl.py +325 -0
- byte_bot/adapters/legacy.py +506 -0
- byte_bot/adapters/opencode_http.py +968 -0
- byte_bot/app.py +1965 -0
- byte_bot/cli.py +2518 -0
- byte_bot/config.py +1097 -0
- byte_bot/demo.py +457 -0
- byte_bot/disclosure.py +212 -0
- byte_bot/discovery.py +303 -0
- byte_bot/doctor.py +623 -0
- byte_bot/http_deadline.py +198 -0
- byte_bot/journal_bridge.py +1382 -0
- byte_bot/migration.py +420 -0
- byte_bot/mobile.py +479 -0
- byte_bot/model.py +1248 -0
- byte_bot/notifications.py +1130 -0
- byte_bot/onboarding.py +282 -0
- byte_bot/platform_migration.py +228 -0
- byte_bot/presentation.py +495 -0
- byte_bot/projects.py +934 -0
- byte_bot/repository.py +151 -0
- byte_bot/service_manager.py +240 -0
- byte_bot/state.py +901 -0
- byte_bot/storage.py +443 -0
- byte_bot/structured_source.py +613 -0
- byte_bot/systemd_unit.py +227 -0
- byte_bot/telegram.py +634 -0
- byte_bot/telegram_setup.py +228 -0
- byte_bot/tui.py +738 -0
- byte_bot/updater.py +226 -0
- byte_bot-0.1.0.dist-info/METADATA +116 -0
- byte_bot-0.1.0.dist-info/RECORD +38 -0
- byte_bot-0.1.0.dist-info/WHEEL +4 -0
- byte_bot-0.1.0.dist-info/entry_points.txt +2 -0
- byte_bot-0.1.0.dist-info/licenses/LICENSE +21 -0
byte_bot/__init__.py
ADDED
byte_bot/__main__.py
ADDED
|
@@ -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
|
+
)
|