81 lines
6.2 KiB
Markdown
81 lines
6.2 KiB
Markdown
# shell: processes, boxes, live output, speech
|
|||
|
|
|
||
|
|
Scope: `src/shell/` (namespace `detz::shell`). Source of truth is `../tai.py`; this file states what the C++ port
|
||
|
|
does today, how it is verified, and the gaps. Wiring for other modules: [wiring/shell.md](wiring/shell.md).
|
||
|
|
|
||
|
|
## Command Output Rule (repository AGENTS.md)
|
||
|
|
|
||
|
|
Every command an agent runs for the user streams live and is returned to the agent. The window is a fixed-height
|
||
|
|
message (`LiveStream`, or the chat TUI's `live` block through the `StreamSink` protocol). A command that stops on a
|
||
|
|
prompt (sudo, ssh, y/n) runs on a pty and asks through `ask(text, secret)`; the answer is typed into the command, a
|
||
|
|
decline (`nullopt`) sends end-of-file, and time spent waiting for the user never counts against the timeout.
|
||
|
|
Anything that runs a process goes through `RunLive` with `ShellIo::Stream` and `ShellIo::Ask`. No bare
|
||
|
|
`subprocess`-style calls for agent commands.
|
||
|
|
|
||
|
|
## Caps (constants, all from tai.py)
|
||
|
|
|
||
|
|
| constant | value | where |
|
||
|
|
|---|---|---|
|
||
|
|
| `kShellTimeout` (SHELL_TIMEOUT) | 120 s | runlive.h |
|
||
|
|
| `kRunOutputCap` (RUN_OUTPUT_CAP) | 4,000,000 chars, pty path only | runlive.h |
|
||
|
|
| `kRunPromptIdle` (RUN_PROMPT_IDLE) | 1.0 s of silence before a prompt is asked | runlive.h |
|
||
|
|
| `kRunMaxAsks` (RUN_MAX_ASKS) | 20 questions per command | runlive.h |
|
||
|
|
| `kRunAnswerCap` (RUN_ANSWER_CAP) | 1000 chars typed per answer | runlive.h |
|
||
|
|
| `kRunRows` x `kRunCols` | 24 x 120 pty window | pty.h |
|
||
|
|
| `kStreamHeight` (STREAM_HEIGHT) | 4 lines | livestream.h |
|
||
|
|
| `kShellSnapshotMaxFiles` | 25 files | targets.h |
|
||
|
|
| ShellSession caps | SID_MAX 64, CHAIN_CAP 64, ROWS_CAP 1000, TREE_DEPTH 8, TREE_KIDS 10, TREE_LINES 40 | session.h |
|
||
|
|
|
||
|
|
## Parity table (tai.py symbol -> C++)
|
||
|
|
|
||
|
|
| tai.py | C++ | notes |
|
||
|
|
|---|---|---|
|
||
|
|
| `run_live`, `run_live_pty` | `RunLive::Run`, `RunLive::RunPty` | pipe path with reader thread, pty path with prompt detection |
|
||
|
|
| `RUN_CTRL_RE`, `RUN_PROMPT_RE` | `RunLive::StripControl`, `RunLive::LooksLikePrompt` | hand-written scanners, same alternation |
|
||
|
|
| `PtyProc` | `PtyProc` | posix_openpt, TIOCSCTTY, TIOCSWINSZ, poll reader, echo probe |
|
||
|
|
| `subprocess.Popen/run`, `shutil.which` | `Popen`, `Subprocess::Run`, `Subprocess::Which` (proc.h) | errno reporting through a CLOEXEC pipe, Python `OSError`/`TimeoutExpired` texts |
|
||
|
|
| `LiveStream`, `stream_status`, `human_size` | `LiveStream`, `StreamStatus`, `HumanSize` | |
|
||
|
|
| `Spinner` | `Spinner` | stderr tty only, 80 ms frames, shimmer + clock when color is on |
|
||
|
|
| `LaneBoard` | `LaneBoard` | `list_agents` injected as `AgentLister` |
|
||
|
|
| `Glow` (every static method) | `Glow` (glow.h) | `describe_call` over `JsonValue`, `agent_row`/`lanes` over `AgentRow` |
|
||
|
|
| `Sandbox`, `container_engine`, `box_state`, `update_box_caps`, `ensure_box`, `box_exec`, `box_python`, `box_transcribe`, `box_speak`, `box_opencode_binary`, `box_opencode_run` | `Sandbox` (sandbox.h) | `BoxOpencodeExec` is the exec half; the caller runs `opencode_parse_events` |
|
||
|
|
| `BOX_CONTAINERFILE`, `BOX_STT`, `BOX_TTS`, `BOX_OPENCODE_INSTALL_SCRIPT` | `BoxData` (boxdata.cpp, generated from tai.py) | byte-identical, checked by the box parity case |
|
||
|
|
| `curl_hint`, `CURL_HINT` | `CurlHint`, `kCurlHint` | |
|
||
|
|
| `ShellSession` (current, register, tree_lines, report, ...) | `ShellSession` (session.h) | `SessionDb` interface replaces `store.db`; pid/boot/chain logic identical |
|
||
|
|
| `shell_target_paths`, `segment_targets` | `ShellTargets::Paths`, `ShellTargets::Segment` | shlex, glob and os.walk ported (`Shlex`, `PyGlob`) |
|
||
|
|
| `Tools.shell_stream`, `Tools.shell_ask` | `ShellIo::Stream`, `ShellIo::Ask` over `ShellApp` | |
|
||
|
|
| `edge_token`, `edge_timestamp`, `edge_ssml`, `edge_chunks`, `ws_send`, `ws_recv_exact`, `ws_recv`, `edge_skew`, `edge_connect`, `edge_synthesize` | `EdgeSpeech` (edge.h) | TLS bytes via libcurl connect-only, `EdgeSocket` seam for tests |
|
||
|
|
| `play_audio`, `listen_audio`, `RECORDERS`, `TRANSCRIBERS` | `Audio` (audio.h) | |
|
||
|
|
|
||
|
|
Old invented API (`Sandbox::run(allow-list)`, `ShellSession::exec`, `PtyProc::spawn/read_all`, `Spinner::tick/done`,
|
||
|
|
`LiveStream::push/drain/joined`, `LaneBoard::set/render`, `Glow::highlight`, `speak_text`) is gone; tai.py has none of
|
||
|
|
it. Replacements are listed in wiring/shell.md.
|
||
|
|
|
||
|
|
## How it is verified
|
||
|
|
|
||
|
|
- `tests/shell_parity.py` builds `tests/shell_parity_driver.cpp` and runs every case through the driver and through
|
||
|
|
the same function of `../tai.py`, comparing bytes. Real processes, a real pty (including the real `su` password
|
||
|
|
prompt), real files, a scripted fake `podman`, a fake `/proc`, a fake websocket server. Fuzzed inputs for markdown,
|
||
|
|
wrap, truncate, diff, json. `python3 tests/shell_parity.py [--fuzz N] [--filter NAME] [--live] [-v]`.
|
||
|
|
- `tests/shell_test.cpp`: unit tests, build line in its header.
|
||
|
|
- Both build with `-Wall -Wextra` and must be warning-free; the parity run fails on any compiler warning.
|
||
|
|
|
||
|
|
## Gaps and deviations (stated, not hidden)
|
||
|
|
|
||
|
|
- Unicode classes: `\w`, `\d`, `str.isalnum`, `str.lower` use hand-written ranges (exact for ASCII, Latin-1, Greek,
|
||
|
|
Cyrillic basics; approximate beyond). Affects markdown italics boundaries, number tokens in `highlight_python`,
|
||
|
|
`complete_files` case folding for exotic scripts.
|
||
|
|
- The pipe path of `run_live` keeps all output in memory (no cap), exactly like tai.py; only the pty path is capped.
|
||
|
|
- After a timeout only the shell is killed (`Popen.kill`), as in tai.py. A grandchild that keeps the pipe open makes
|
||
|
|
`run_live` wait up to 10 s for the reader and leaves the grandchild running.
|
||
|
|
- `Spinner` and `LaneBoard` are time based; the parity run compares the first frames, the final clear and the frame
|
||
|
|
count within 2, not every frame.
|
||
|
|
- Edge speech: wire handling is verified offline against tai.py with a scripted server. The live service answered
|
||
|
|
`403 Forbidden` to tai.py and to the port alike when last tried (2026-10-25, this machine), so end-to-end audio
|
||
|
|
was not verified.
|
||
|
|
- `Sandbox::EnsureBox` image build and the opencode install were exercised only against a scripted fake engine, not
|
||
|
|
a real multi-minute podman build.
|
||
|
|
- `ShellIo::Ask` REPL fallback: Ctrl-C during the question is not mapped to a decline (tai.py maps
|
||
|
|
`KeyboardInterrupt`); the REPL owner decides signal handling.
|
||
|
|
- `BackendError` here is `detz::shell::BackendError`; the agent core converts it at the call site.
|