byte-bot 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.
- byte_bot-0.1.0/.gitignore +9 -0
- byte_bot-0.1.0/LICENSE +21 -0
- byte_bot-0.1.0/PKG-INFO +116 -0
- byte_bot-0.1.0/README.md +101 -0
- byte_bot-0.1.0/pyproject.toml +40 -0
- byte_bot-0.1.0/src/byte_bot/__init__.py +3 -0
- byte_bot-0.1.0/src/byte_bot/__main__.py +3 -0
- byte_bot-0.1.0/src/byte_bot/adapters/__init__.py +16 -0
- byte_bot-0.1.0/src/byte_bot/adapters/codex_exec_jsonl.py +325 -0
- byte_bot-0.1.0/src/byte_bot/adapters/legacy.py +506 -0
- byte_bot-0.1.0/src/byte_bot/adapters/opencode_http.py +968 -0
- byte_bot-0.1.0/src/byte_bot/app.py +1965 -0
- byte_bot-0.1.0/src/byte_bot/cli.py +2518 -0
- byte_bot-0.1.0/src/byte_bot/config.py +1097 -0
- byte_bot-0.1.0/src/byte_bot/demo.py +457 -0
- byte_bot-0.1.0/src/byte_bot/disclosure.py +212 -0
- byte_bot-0.1.0/src/byte_bot/discovery.py +303 -0
- byte_bot-0.1.0/src/byte_bot/doctor.py +623 -0
- byte_bot-0.1.0/src/byte_bot/http_deadline.py +198 -0
- byte_bot-0.1.0/src/byte_bot/journal_bridge.py +1382 -0
- byte_bot-0.1.0/src/byte_bot/migration.py +420 -0
- byte_bot-0.1.0/src/byte_bot/mobile.py +479 -0
- byte_bot-0.1.0/src/byte_bot/model.py +1248 -0
- byte_bot-0.1.0/src/byte_bot/notifications.py +1130 -0
- byte_bot-0.1.0/src/byte_bot/onboarding.py +282 -0
- byte_bot-0.1.0/src/byte_bot/platform_migration.py +228 -0
- byte_bot-0.1.0/src/byte_bot/presentation.py +495 -0
- byte_bot-0.1.0/src/byte_bot/projects.py +934 -0
- byte_bot-0.1.0/src/byte_bot/repository.py +151 -0
- byte_bot-0.1.0/src/byte_bot/service_manager.py +240 -0
- byte_bot-0.1.0/src/byte_bot/state.py +901 -0
- byte_bot-0.1.0/src/byte_bot/storage.py +443 -0
- byte_bot-0.1.0/src/byte_bot/structured_source.py +613 -0
- byte_bot-0.1.0/src/byte_bot/systemd_unit.py +227 -0
- byte_bot-0.1.0/src/byte_bot/telegram.py +634 -0
- byte_bot-0.1.0/src/byte_bot/telegram_setup.py +228 -0
- byte_bot-0.1.0/src/byte_bot/tui.py +738 -0
- byte_bot-0.1.0/src/byte_bot/updater.py +226 -0
byte_bot-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MD Raqibul Islam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
byte_bot-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: byte-bot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local dashboard for monitoring coding agents with optional Telegram status
|
|
5
|
+
Project-URL: Homepage, https://github.com/erajtob/byte-bot
|
|
6
|
+
Project-URL: Source, https://github.com/erajtob/byte-bot
|
|
7
|
+
Project-URL: Issues, https://github.com/erajtob/byte-bot/issues
|
|
8
|
+
Project-URL: Security, https://github.com/erajtob/byte-bot/security
|
|
9
|
+
Author-email: MD Raqibul Islam <hello@eraj.dev>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: textual<9,>=8.2.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="docs/assets/byte-wordmark.svg" alt="byte-bot wordmark" width="760">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
# byte-bot
|
|
21
|
+
|
|
22
|
+
**See what your coding agents are doing — and know when they need you.**
|
|
23
|
+
|
|
24
|
+
Byte is a small, local dashboard for coding agents. It watches supported agents
|
|
25
|
+
without controlling them, gives you one terminal view, and can optionally send the same
|
|
26
|
+
status to a private Telegram chat.
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<img src="docs/assets/dashboard-demo.svg" alt="Synthetic byte-bot dashboard activity demo" width="1000">
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
*The dashboard above uses synthetic demo data.*
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
curl -fsSL https://raw.githubusercontent.com/erajtob/byte-bot/main/install.sh | sh
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Run
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
byte-bot
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
On first launch, choose the demo or follow the guided setup for your machine.
|
|
47
|
+
|
|
48
|
+
Already installed?
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
byte-bot update
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Beta status:** Linux-first. macOS has a foreground path but is not Beta-verified yet;
|
|
55
|
+
Windows is not Beta-supported yet.
|
|
56
|
+
|
|
57
|
+
## What you can see
|
|
58
|
+
|
|
59
|
+
- active supported agent sessions, grouped by project when Byte can prove the workspace;
|
|
60
|
+
- a short current-activity line such as reading files, editing, running a command, or thinking;
|
|
61
|
+
- model and usage information when the source provides it;
|
|
62
|
+
- permission or input blockers that need you;
|
|
63
|
+
- whether status is fresh or stale, and when a source could not be read.
|
|
64
|
+
|
|
65
|
+
Byte does not store or display private chain-of-thought, prompt bodies, command output,
|
|
66
|
+
or raw tool arguments as status.
|
|
67
|
+
|
|
68
|
+
## Telegram (optional)
|
|
69
|
+
|
|
70
|
+
<p align="center">
|
|
71
|
+
<img src="docs/assets/telegram-preview.svg" alt="Synthetic byte-bot Telegram preview" width="760">
|
|
72
|
+
</p>
|
|
73
|
+
|
|
74
|
+
Pair Byte with one private Telegram chat to check status away from the terminal and receive
|
|
75
|
+
quiet notifications when an agent needs attention.
|
|
76
|
+
|
|
77
|
+
Telegram does **not** become a shell. It cannot change configured commands, start agents,
|
|
78
|
+
control Docker or sudo, browse the filesystem, or turn chat text into command input.
|
|
79
|
+
|
|
80
|
+
## Supported today
|
|
81
|
+
|
|
82
|
+
| Source | What Byte currently supports |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| OpenCode | Active sessions, projects, model/usage, current activity, and permission/input waits |
|
|
85
|
+
| Codex CLI 0.155.1 | Passive observation of an existing `exec --json` capture |
|
|
86
|
+
| Structured project sources | Status and events provided by a project adapter |
|
|
87
|
+
| Local status commands | Exact read-only commands you configure, with timeouts |
|
|
88
|
+
|
|
89
|
+
OpenCode is not pinned to one release number. Byte checks the small set of data it
|
|
90
|
+
relies on; if that data becomes incompatible, Byte marks the source degraded instead
|
|
91
|
+
of guessing.
|
|
92
|
+
|
|
93
|
+
## Read-only by design
|
|
94
|
+
|
|
95
|
+
byte-bot v0.1 only observes:
|
|
96
|
+
|
|
97
|
+
- Telegram cannot run arbitrary commands or control your agents;
|
|
98
|
+
- Byte only runs the exact local status commands you configure, with timeouts;
|
|
99
|
+
- there is no hosted Byte account; optional Telegram uses outbound connections only.
|
|
100
|
+
|
|
101
|
+
For the technical details, read [Security](SECURITY.md),
|
|
102
|
+
[How it works](docs/how-it-works.md), and [Compatibility](docs/compatibility.md).
|
|
103
|
+
|
|
104
|
+
## More
|
|
105
|
+
|
|
106
|
+
- [Install, update, and remove](docs/install.md)
|
|
107
|
+
- [Setup and configuration](docs/install.md#first-launch)
|
|
108
|
+
- [Support and troubleshooting](docs/support.md)
|
|
109
|
+
- [How it works](docs/how-it-works.md)
|
|
110
|
+
- [Contributing](CONTRIBUTING.md)
|
|
111
|
+
|
|
112
|
+
byte-bot stays project-agnostic and does not require any particular workload.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
[MIT](LICENSE).
|
byte_bot-0.1.0/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/byte-wordmark.svg" alt="byte-bot wordmark" width="760">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# byte-bot
|
|
6
|
+
|
|
7
|
+
**See what your coding agents are doing — and know when they need you.**
|
|
8
|
+
|
|
9
|
+
Byte is a small, local dashboard for coding agents. It watches supported agents
|
|
10
|
+
without controlling them, gives you one terminal view, and can optionally send the same
|
|
11
|
+
status to a private Telegram chat.
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<img src="docs/assets/dashboard-demo.svg" alt="Synthetic byte-bot dashboard activity demo" width="1000">
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
*The dashboard above uses synthetic demo data.*
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
curl -fsSL https://raw.githubusercontent.com/erajtob/byte-bot/main/install.sh | sh
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Run
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
byte-bot
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
On first launch, choose the demo or follow the guided setup for your machine.
|
|
32
|
+
|
|
33
|
+
Already installed?
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
byte-bot update
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Beta status:** Linux-first. macOS has a foreground path but is not Beta-verified yet;
|
|
40
|
+
Windows is not Beta-supported yet.
|
|
41
|
+
|
|
42
|
+
## What you can see
|
|
43
|
+
|
|
44
|
+
- active supported agent sessions, grouped by project when Byte can prove the workspace;
|
|
45
|
+
- a short current-activity line such as reading files, editing, running a command, or thinking;
|
|
46
|
+
- model and usage information when the source provides it;
|
|
47
|
+
- permission or input blockers that need you;
|
|
48
|
+
- whether status is fresh or stale, and when a source could not be read.
|
|
49
|
+
|
|
50
|
+
Byte does not store or display private chain-of-thought, prompt bodies, command output,
|
|
51
|
+
or raw tool arguments as status.
|
|
52
|
+
|
|
53
|
+
## Telegram (optional)
|
|
54
|
+
|
|
55
|
+
<p align="center">
|
|
56
|
+
<img src="docs/assets/telegram-preview.svg" alt="Synthetic byte-bot Telegram preview" width="760">
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
Pair Byte with one private Telegram chat to check status away from the terminal and receive
|
|
60
|
+
quiet notifications when an agent needs attention.
|
|
61
|
+
|
|
62
|
+
Telegram does **not** become a shell. It cannot change configured commands, start agents,
|
|
63
|
+
control Docker or sudo, browse the filesystem, or turn chat text into command input.
|
|
64
|
+
|
|
65
|
+
## Supported today
|
|
66
|
+
|
|
67
|
+
| Source | What Byte currently supports |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| OpenCode | Active sessions, projects, model/usage, current activity, and permission/input waits |
|
|
70
|
+
| Codex CLI 0.155.1 | Passive observation of an existing `exec --json` capture |
|
|
71
|
+
| Structured project sources | Status and events provided by a project adapter |
|
|
72
|
+
| Local status commands | Exact read-only commands you configure, with timeouts |
|
|
73
|
+
|
|
74
|
+
OpenCode is not pinned to one release number. Byte checks the small set of data it
|
|
75
|
+
relies on; if that data becomes incompatible, Byte marks the source degraded instead
|
|
76
|
+
of guessing.
|
|
77
|
+
|
|
78
|
+
## Read-only by design
|
|
79
|
+
|
|
80
|
+
byte-bot v0.1 only observes:
|
|
81
|
+
|
|
82
|
+
- Telegram cannot run arbitrary commands or control your agents;
|
|
83
|
+
- Byte only runs the exact local status commands you configure, with timeouts;
|
|
84
|
+
- there is no hosted Byte account; optional Telegram uses outbound connections only.
|
|
85
|
+
|
|
86
|
+
For the technical details, read [Security](SECURITY.md),
|
|
87
|
+
[How it works](docs/how-it-works.md), and [Compatibility](docs/compatibility.md).
|
|
88
|
+
|
|
89
|
+
## More
|
|
90
|
+
|
|
91
|
+
- [Install, update, and remove](docs/install.md)
|
|
92
|
+
- [Setup and configuration](docs/install.md#first-launch)
|
|
93
|
+
- [Support and troubleshooting](docs/support.md)
|
|
94
|
+
- [How it works](docs/how-it-works.md)
|
|
95
|
+
- [Contributing](CONTRIBUTING.md)
|
|
96
|
+
|
|
97
|
+
byte-bot stays project-agnostic and does not require any particular workload.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "byte-bot"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Local dashboard for monitoring coding agents with optional Telegram status"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "MD Raqibul Islam", email = "hello@eraj.dev"}]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"textual>=8.2.8,<9",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/erajtob/byte-bot"
|
|
19
|
+
Source = "https://github.com/erajtob/byte-bot"
|
|
20
|
+
Issues = "https://github.com/erajtob/byte-bot/issues"
|
|
21
|
+
Security = "https://github.com/erajtob/byte-bot/security"
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
byte-bot = "byte_bot.cli:main"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = ["ruff>=0.13,<1"]
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/byte_bot"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.sdist]
|
|
33
|
+
include = ["/src/byte_bot", "/README.md", "/LICENSE", "/pyproject.toml"]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
line-length = 100
|
|
37
|
+
target-version = "py312"
|
|
38
|
+
|
|
39
|
+
[tool.ruff.lint]
|
|
40
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
@@ -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
|
+
)
|