Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bc7efa3
Add Windows PTY support for Python worker
t-kalinowski May 19, 2026
3ef5627
Filter Windows PTY cursor sequences
t-kalinowski May 19, 2026
fde100d
Stabilize Windows integration test harness
t-kalinowski May 20, 2026
248d2f9
Fix Windows PTY stdin accounting
t-kalinowski May 20, 2026
190fa46
Tighten Windows stdin bridge behavior
t-kalinowski May 20, 2026
1e08e32
Handle sandboxed Windows Python transport
t-kalinowski May 20, 2026
dc0df07
Fix Windows Python input accounting
t-kalinowski May 20, 2026
f0655bf
Fix Windows PTY CRLF and PATH handling
t-kalinowski May 20, 2026
5f291b1
Document Windows Python PTY support
t-kalinowski May 20, 2026
3329ee8
Fix Windows PTY Python interrupt routing
t-kalinowski May 20, 2026
27591e7
Fix Windows stdin accounting review findings
t-kalinowski May 21, 2026
fa346c7
Fix Windows pipe stdin review findings
t-kalinowski May 21, 2026
1a7e5e8
Fix Windows direct stdin interrupt discard
t-kalinowski May 21, 2026
1279cb7
Fix Windows buffered input plot state
t-kalinowski May 21, 2026
b313523
Fix Windows Python interrupt process edges
t-kalinowski May 21, 2026
823994d
Fix Windows raw stdin fd detection
t-kalinowski May 21, 2026
c1ed5a0
Track Windows stdin fd aliases
t-kalinowski May 21, 2026
e207ae3
Avoid EOF for dropped Windows CRLF bytes
t-kalinowski May 21, 2026
3a96c04
Account split UTF-8 stdin bytes
t-kalinowski May 22, 2026
e476880
Remove legacy worker sideband protocol
t-kalinowski May 22, 2026
123bceb
Support ConPTY inside Windows sandbox wrapper
t-kalinowski May 22, 2026
5801f7c
Continue Unix protocol interrupts after sideband
t-kalinowski May 22, 2026
4db45ca
Guard Python request-boundary cleanup
t-kalinowski May 22, 2026
2761ea0
Remove legacy terminology
t-kalinowski May 23, 2026
965132d
Fix review findings for interrupts and sandboxed PTY workers
t-kalinowski May 23, 2026
e584105
Deliver OS interrupts after sideband notice
t-kalinowski May 23, 2026
a3cdaf7
Restore interrupt cleanup and sandbox Ctrl-Break forwarding
t-kalinowski May 23, 2026
c2ff40b
Preserve protocol v1 text input frames
t-kalinowski May 23, 2026
1bca5a7
Scrub Windows IPC pipe names after connect
t-kalinowski May 23, 2026
e5ae11d
Use pipe transport for sandboxed PTY wrappers
t-kalinowski May 23, 2026
e66d5f6
Filter sandbox wrapper ConPTY output
t-kalinowski May 23, 2026
d8cacb9
Fix Windows CI regressions
t-kalinowski May 24, 2026
779b57a
Stop emitting R input echoes
t-kalinowski May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mcp-repl-startup.log
.Rhistory
.Rproj.user
.venv
__pycache__/
/eval
.agents
tools/drain-*
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Keep this file short. It is a table of contents, not the full manual.
- Sandbox metadata: Codex per-tool-call `_meta["codex/sandbox-state-meta"]` used by `--sandbox inherit` to choose the effective worker sandbox for that call.
- Writable root: An absolute path that a `workspace-write` worker may write, subject to forced read-only subpaths like `.git`, `.codex`, and `.agents`.
- Session temp directory: The server-allocated per-session temp path exposed to the worker as `TMPDIR` and `MCP_REPL_R_SESSION_TMPDIR`.
- Sideband IPC: The JSON-lines server/worker pipe for structural facts such as `readline_start`, `readline_input`, `readline_discard`, `output_text`, `plot_image`, and `session_end`.
- Sideband IPC: The JSON-lines server/worker pipe for structural facts such as `readline_start`, `readline_input_bytes`, `readline_discard_bytes`, `output_text`, `output_image`, and `session_end`.
- Raw output capture: The stdout/stderr pipes or PTY stream captured by the server for unowned visible text. Sideband carries worker-owned text and structural facts.
- Output timeline: The server-side reconstruction of visible output order from captured stdout/stderr plus sideband facts.
- Server-owned: State, files, or notices created and retained by the main server process, not by the runtime or the worker. Use this for output bundles, response finalization, debug logs, and server temp roots.
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
landlock = "0.4.4"
seccompiler = "0.5.0"

[target.'cfg(unix)'.dependencies]
[target.'cfg(any(unix, windows))'.dependencies]
portable-pty = "0.9.0"

[target.'cfg(target_os = "macos")'.dependencies]
Expand All @@ -56,6 +56,7 @@
"Win32_Security_Authorization",
"Win32_Storage_FileSystem",
"Win32_System_Console",
"Win32_System_Environment",
"Win32_System_IO",
"Win32_System_JobObjects",
"Win32_System_Pipes",
Expand Down
16 changes: 9 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,20 @@ The repository is organized around a few concrete subsystems rather than deep pa
- `src/backend.rs` selects between the R and Python implementations at launch
and install/configuration boundaries.
- Worker launch chooses the runtime stdin transport up front. R and the default
protocol-worker path use pipes; built-in Unix Python uses PTY-backed C
stdin/stdout/stderr so CPython takes its normal interactive readline path.
protocol-worker path use pipes; built-in Python uses PTY-backed C
stdin/stdout/stderr where the platform launch supports it so CPython takes
its normal interactive readline path. On Windows sandboxed Python, the server
uses pipes to the sandbox wrapper and the wrapper creates ConPTY for the
restricted Python child, so CPython still sees a console inside the sandbox.
- Both backends receive request payloads through worker stdin and use sideband
IPC for structured facts. R owns stdin through a worker reader thread keyed by
payload byte length. Unix Python lets CPython own stdin through
payload byte length. PTY-backed Python lets CPython own stdin through
`PyOS_ReadlineFunctionPointer`; the callback reports `readline_start`,
`readline_input`, and `readline_discard` accounting facts. Its legacy
`stdin_write_ack` frames acknowledge request-boundary setup, not prompt
completion or output delivery.
`readline_input_bytes`, and `readline_discard_bytes` accounting facts using
the exact bytes received over worker stdin before interpreter normalization.
- The IPC sideband is single-owner by design: startup env vars only bootstrap the main worker, then they are scrubbed before user code runs. Descendants must not emit sideband messages.
- R-specific behavior lives in `src/r_session.rs`, `src/r_controls.rs`, `src/r_graphics.rs`, and `src/r_htmd.rs`.
- Python-specific behavior lives in `src/python_ffi.rs`, `src/python_session.rs`, `src/python_worker.rs`, and `python/embedded.py`. Python worker mode dynamically loads CPython only after the worker has selected the Python backend, so R worker mode does not load Python. On the Unix PTY path, Python leaves CPython's fd-backed stdin surface intact; direct fd stdin consumers are not a request-completion contract.
- Python-specific behavior lives in `src/python_ffi.rs`, `src/python_session.rs`, `src/python_worker.rs`, and `python/embedded.py`. Python worker mode dynamically loads CPython only after the worker has selected the Python backend, so R worker mode does not load Python. On the Unix PTY path, Python leaves CPython's fd-backed stdin surface intact; Windows keeps sideband-aware direct-stdin bridges for the ConPTY path so CRLF and console reads remain accountably tied to active MCP input.

### Sandbox and process isolation

Expand Down
2 changes: 1 addition & 1 deletion docs/futurework/advisory-worker-write-observations.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ just "readline happened":
- they know which stream is being written,
- they know the exact byte slice being written,
- they know the local callback order relative to other worker-side events such
as `readline_result` and `plot_image`.
as byte-level readline accounting and `output_image`.

That information is incomplete, but it may still be useful.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ screen” signal. That means server-side timeline fixes can only help after the
image exists; they cannot make an image arrive earlier than the worker emits it.

That limitation is separate from ordinary plot/stdout ordering across separate
input lines. The server timeline can already place an emitted `plot_image`
input lines. The server timeline can already place an emitted `output_image`
before later stdout when the sideband facts contain that ordering.

## Investigation Outcome
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This future work item covers the larger simplification goal behind the current o

- keep the R worker thin and factual,
- let it run the ordinary embedded REPL,
- emit runtime facts such as `readline_start`, `readline_result`, and plot/image events over IPC,
- emit runtime facts such as `readline_start`, byte-level input accounting, and image events over IPC,
- move request-boundary interpretation and timeline reconstruction into the server.

This is intentionally broader than the current branch milestone.
Expand Down
17 changes: 7 additions & 10 deletions docs/futurework/server-backend-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,10 @@ buckets:
is documentation wiring, not request execution policy, and should move with
`docs/futurework/composable-tool-descriptions.md`.
- `src/worker_process.rs` selects a `BackendDriver` at `WorkerManager`
creation. The driver owns backend-specific request metadata such as Python
newline normalization, Python `line_count`, whether to wait for
`stdin_write_ack`, interrupt behavior, completion waiting, and backend-info
creation. The driver owns backend-specific adapter details such as Python
newline normalization, interrupt behavior, completion waiting, and worker
startup tolerance. This is acceptable only as a server-side adapter until the
worker can advertise these narrow capabilities.
worker protocol can make these narrow capabilities generic.
- `src/worker_process.rs` also branches at spawn time to configure R worker mode
or Python worker mode in the same `mcp-repl` executable. Python launch setup
additionally resolves the selected interpreter executable and loadable
Expand Down Expand Up @@ -88,12 +87,10 @@ semantics, not implementation language.

Initial candidates:

- `supports_images`: already present in `backend_info`; controls whether image
events are expected.
- `stdin_write_ack`: whether the server must wait for worker acceptance after
`stdin_write` before writing raw stdin bytes.
- `backend_info_startup_timeout`: whether startup may continue after a short
backend-info timeout.
- `supports_images`: reported at worker startup; controls whether image events
are expected.
- `worker_ready_startup_timeout`: whether startup may continue after a short
worker-ready timeout.
- `timeout_output_settle`: whether a timed-out request needs an additional
output-settle window before the server returns the timeout reply.

Expand Down
25 changes: 14 additions & 11 deletions docs/futurework/stdin-transport-single-owner.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@ The remaining follow-up is broader than that point fix. We still need to tighten
- The problem is not "piped stdin is always broken". The hang showed up when another thread was already blocked on the same stdin pipe.
- Future embedded interpreters can run into similar issues if worker stdin
ownership drifts again.
- The current R and Python paths now keep stdin raw and use sideband metadata
for request boundaries, but their in-worker stdin ownership differs.
- The current R and Python paths now keep stdin raw and use sideband facts for
prompt state plus byte-level input accounting, but their in-worker stdin
ownership differs.

## Current Scope

This repo now uses raw stdin for worker payloads and sideband IPC for request
metadata:
This repo now uses raw stdin for worker payloads and sideband IPC for prompt
state and byte-level accounting:

- R worker mode owns stdin in a worker-side reader thread. The server announces
the payload byte length on sideband IPC; the worker reader consumes exactly
that many raw bytes and submits them to embedded R.
- Python worker mode lets CPython own stdin. The server announces request
metadata on sideband IPC, waits for `stdin_write_ack`, then writes raw bytes
for CPython's interactive loop to consume.
payload bytes by writing them to worker stdin; the worker reports exact
consumed or discarded bytes on sideband IPC.
- Python worker mode lets CPython own stdin. The server writes raw bytes for
CPython's interactive loop to consume, and the worker reports exact consumed
or discarded bytes on sideband IPC.

The remaining follow-up is to make this ownership split more explicit in code
and reduce server-side backend branching around request metadata.
Expand All @@ -36,8 +37,10 @@ and reduce server-side backend branching around request metadata.

- Treat worker stdin as the real raw input stream delivered to the interpreter.
- Do not add framing headers or other synthetic protocol markers to stdin.
- Mirror request metadata over IPC instead: request start, expected input payload, completion, and other turn/state signals.
- Let the worker use the IPC envelope to know when the current stdin payload is complete, while still feeding raw stdin through the interpreter-facing path.
- Mirror interpreter state over IPC instead: prompt starts, consumed bytes,
discarded bytes, completion, and other turn/state signals.
- Let the worker use exact sideband accounting for stdin bytes while still
feeding raw stdin through the interpreter-facing path.
- For line-oriented runtimes such as embedded R, expect a single logical request to be satisfied across multiple `readline` or `ReadConsole` calls.

The current embedded worker implementation keeps stdin raw and preserves request
Expand Down
19 changes: 10 additions & 9 deletions docs/futurework/worker-pty-stdin-transport.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
# Worker PTY Stdin Transport

Status: implemented for Unix built-in Python and custom protocol-worker launch
configuration. This note is retained as historical design context; the current
contract is documented in `docs/architecture.md`,
`docs/worker_sideband_protocol.md`, and `docs/output_timeline.md`.
Status: implemented for Unix built-in Python, unsandboxed Windows built-in
Python, and custom protocol-worker launch configuration. This note is retained
as historical design context; the current contract is documented in
`docs/architecture.md`, `docs/worker_sideband_protocol.md`, and
`docs/output_timeline.md`.

## Use Case

Some runtimes may need TTY-like stdin for their normal interactive
hooks. For example, a Python embedding that relies on
`PyOS_ReadlineFunctionPointer` may only use that hook when stdin is a
TTY. The Unix Python worker now uses that PTY-backed path. R and default
protocol workers continue to use pipe stdin unless their launch spec selects a
PTY.
TTY. The Unix Python worker and unsandboxed Windows Python worker now use that
PTY-backed path. R and default protocol workers continue to use pipe stdin
unless their launch spec selects a PTY.

## Boundary

Expand Down Expand Up @@ -54,5 +55,5 @@ steady-state request handling.

The repository now has protocol-worker coverage for PTY launch with sideband IPC
kept separate from visible PTY output, plus public Python backend tests proving
that Unix Python gets TTY-backed C stdio and CPython `input()` consumes stdin
through the readline path.
that Unix and unsandboxed Windows Python get TTY-backed C stdio and CPython
`input()` consumes stdin through the readline path.
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ checked-in execution plans without relying on stale notes.
- `docs/futurework/server-backend-boundary.md`: deferred note on removing backend-specific execution semantics from server-side request handling.
- `docs/futurework/sidecar-viewer-observability.md`: deferred note on a local read-only sidecar viewer for transcripts, plots, and output bundles.
- `docs/futurework/worker-session-tempdir-rotation.md`: deferred design note on rotating worker tempdir paths per launch so stale temp trees do not block respawn.
- `docs/futurework/worker-pty-stdin-transport.md`: historical design note for the implemented Unix Python and custom-worker PTY launch path.
- `docs/futurework/worker-pty-stdin-transport.md`: historical design note for the implemented Python and custom-worker PTY launch path.
- `docs/futurework/stronger-worker-child-containment.md`: deferred design note on tighter worker descendant containment, especially on Windows.
- `docs/futurework/unified-output-timeline-pipeline.md`: deferred design note for converging pager and files mode onto one shared resolved timeline pipeline.
- `docs/futurework/stdin-transport-single-owner.md`: deferred design for making worker stdin ownership explicit instead of relying on a Windows-only gate.
Expand Down
Loading
Loading