CLI exec -i: send stdin EOF (11d follow-up) #37
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Follow-up from 11d. Step 11d added a stdin EOF frame to the exec stream channel (
1337efd). Withinfraternal exec -iand no TTY, the CLI must send that frame when its local stdin reaches EOF. Otherwiseecho hi | infraternal exec -i <c> -- cathangs instead of exiting 0 withhi.Starting issue #37 work on branch
cli-eof, based ondevatdb5b5cbf19a7c5d2455225c75e3b8216075f76e7. I’m tracing the step 11d exec EOF contract, the CLI exec/SSH paths, and will add the requested real-container integration coverage.Finding for #37: step 11d defines
FrameType::Eofas 0x13 incrates/proto/src/stream/frame.rs. The infraternal agent’s exec handler already maps EOF tosession.close_stdin()incrates/agent/src/lib.rs, and its non-TTY engine path now supportstty: false; however,StreamClient::open_execstill omitsttyandargv, so the CLI gets the legacy PTY/default shell. The CLI event loop already sends EOF and keeps reading until CLOSE. I also checked SSH: the CLI sends EOF and keeps reading, but the infraternal agent only marks SSH input closed, without delivering terminal EOT to the remote PTY. I’ll address the exec params and map SSH input EOF to Ctrl-D at the SSH stream receiver, with regression coverage for both paths.Verification finding:
scripts/verify fastexited 1 before reaching Rust checks. The gate output wasFAIL fast: build web app, and Vite reported it could not resolve@lucide/svelte/icons/ellipsisimported bypackages/web/src/routes/containers/+page.svelte. That file is unchanged and outside issue #37’s CLI scope.Targeted build evidence:
cargo test --manifest-path crates/infraternal/Cargo.toml --test cli_mcp --no-runexited 101 before compiling the CLI test becausecrates/server/build.rsrequires the embedded web bundle and reportedembedded web build missing; run scripts/build-web first. The preceding fast gate could not build that bundle because the unchanged web page has the unresolved Lucide icon import recorded above.Targeted Rust results:
cargo check -p infraternal-agent -p infraternal-protocompleted successfully (Finished dev profile), andcargo test -p infraternal-protoreportedcargo test: 57 passed (3 suites, 1.19s). The CLI integration test still cannot compile until the web bundle prerequisite succeeds.Integration gate result (load5 was 5.69 before the run):
Vite again failed to resolve
@lucide/svelte/icons/ellipsisfrompackages/web/src/routes/containers/+page.svelte, so the gate stopped before the real-container or SSH tests. This is outside #37’s owned CLI scope.Finished issue #37 on branch
cli-eof.Head SHA:
07195967deba4be34a57c281088e6888a5024a0dCommit:
0719596 Fix CLI exec stdin EOF handlingThe CLI now opens commands with direct argv and explicit TTY mode. Non-TTY stdin EOF sends the protocol half-close and the CLI reads through the remote CLOSE for its exit code. TTY Ctrl-D remains input data. The SSH stream EOF path now delivers Ctrl-D to the remote PTY and keeps reading; the contract and real-sshd regression test cover it. The requested real-container assertions and run-id labels are in the integration suite; containers remain owned by its RAII cleanup.
Gate output, quoted verbatim:
Both gates stop because Vite cannot resolve
@lucide/svelte/icons/ellipsisfrom the unchangedpackages/web/src/routes/containers/+page.svelte. The integration gate ran once with load5 at 5.69 and stopped before running the container or SSH tests. The CLI integration test compile is also blocked by the absent embedded web bundle (embedded web build missing; run scripts/build-web first).Targeted verification:
cargo fmt --all --checkandgit diff --checkpassed.cargo check -p infraternal-agent -p infraternal-proto:cargo check (194 crates compiled)/Finisheddevprofile [unoptimized] target(s) in 24.36s.cargo test -p infraternal-proto:cargo test: 57 passed (3 suites, 1.19s).Starting independent review on branch
cli-eof, base author head07195967deba4be34a57c281088e6888a5024a0d(review basedb5b5cb). I will merge currentdevatce194d4, review the requested diff and EOF behavior, then run the specified gates as load allows.Review verdict: request changes (verification blocker; no CLI EOF implementation defect found).
Review head after merging
dev:2fc8ecea8b238026b64a5bedd31a4d482f3d23ae.The fixed-code EOF helper passed in isolation, including exact
catoutput after stdin EOF and remote status 3 forsh -c "cat; exit 3". Reverting the CLI EOF change in the throwaway worktree made the newcatassertion time out after five seconds. The throwaway worktree was removed.The integration gate failed before reaching the EOF assertions at the unchanged PTY resize assertion in
crates/infraternal/tests/cli_mcp.rs:2359. The first fixed-code run missed47 123while seeing24 80; its direct retry missed24 80while seeing47 123. The gate repeated the latter failure. Full review and evidence:/mnt/hdd/infraternal/jobs/review-cli-eof/findings.md.scripts/verify fastoutput (verbatim)scripts/verify integrationoutput (verbatim)Started the EOF fix round on branch
cli-eofat2fc8ecea8b238026b64a5bedd31a4d482f3d23ae(withdevce194d4merged). I’m addressing the PTY resize test race incli_and_mcp_use_the_public_http_contract; the helper currently resizes after a fixed 300 ms and the remote command waits withsleep 1before its secondstty size. I’ll synchronize both observations explicitly, run the test 20 times, then runscripts/verify fastand integration if load5 is below 8.Finding:
assert_exec_pty_resize_and_exitran incli_and_mcp_use_the_public_http_contract; it resized after a fixed 300 ms while the remote command usedsleep 1, so neither observation established ordering. The test now waits for the initial24 80, sends a newline after resizing, and keeps the remote command checking its actual PTY size before printing47 123. The first synchronized targeted run passed:cargo test: 1 passed (1 suite, 122.81s). This did not reproduce a product resize-forwarding bug. The 20-run loop and gates are pending.Interim repeat-loop evidence through run 6: run 1 failed in the earlier SSH replay timeout; run 2 passed; run 3 failed before resize because remote
stty sizereportedstandard inputwas not a TTY; run 4 missed the earlier SSH replay marker; run 5 repeated the pre-resizestty/TTY failure; run 6 timed out in the earlier SSH piped-stdin EOF check. These failures occur before the resized-size assertion (or in unrelated SSH checks). I traced the CLI path:-tsetstty,open_execsends it inExecParams, and the Docker adapter passes it toCreateExecOptions; I have not established the cause of the intermittent pre-resize TTY failures. The sequential 20-run loop continues.Additional product finding from the repeat loop: runs 3, 5, 7–12, 15–17, and 20 reached the resize helper but remote
sttysaid stdin was not a TTY before any resize. The CLI setstty=true,ExecParamscarries it through the agent, andBollardEngine::execsetCreateExecOptions.tty=Some(true), but it called Bollard 0.21.1start_exec(..., None). The pinned Bollard implementation turnsNoneintoStartExecOptions::default()(tty=false). The Docker Engine API v1.40 spec says exec resize works only when TTY was specified on both create and start: Moby API v1.40 ExecResize. I changed the adapter to pass matchingStartExecOptions.tty; this is a product fix uncovered while verifying the requested race. The original 20-run loop ended at1/20; rerunning against this fix is pending.The diagnostic reproduction after the start-options change still failed before resize. It shows the local PTY was in raw mode (
-icanon -echo) and the server's audit for this OPEN recordedtty:true; the remote command nevertheless reportedstty: standard input. So the CLI flag and server OPEN are correct, and the failure is downstream of the server OPEN. The engine/agent source passesparams.tty_enabled()intoExecOptions; the adapter now sends TTY on both Docker create and start. I have not established why this real Docker exec sometimes still lacks a TTY. The test now includes these details on initial-size failure. I’m continuing with the requested 20-run count on the current head.Finding:
BollardEngine::execwas applying the initial PTY dimensions only after/exec/{id}/startreturned. The child can run immediately, before that resize, so the firststty sizecan miss the requested dimensions. Docker CLI sendsConsoleSizein the create request; Bollard's convenienceCreateExecOptionsomits that field even though itsExecConfigmodel has it. I changed the adapter to send[rows, cols]during exec creation and kept the start-time TTY flag and subsequent resize path.Evidence: the old real-engine and embedded-stream probes reported
stty: standard inputbefore their first expected dimensions. The fast gate passes with the change. Real Docker/Podman and 20-run public CLI verification are still pending until load5 is below 8. Docker CLI's create-time behavior is visible in exec.go; the Moby API docs defineConsoleSizeas the initial exec console size.Update: the public
cli_and_mcp_use_the_public_http_contracttest passed once after the adapter began sendingConsoleSizeduring exec creation (133.30s). The focused adapter PTY probe passed the Docker 24x80 and 47x123 checks, then failed on the rootless Podman leg at the firststty; a directpodman exec -itcontrol on the same image and dimensions succeeded. I scoped the added regression to Docker, which is the runtime used by this #37 PTY assertion. I will include the Podman adapter finding in the final report; the requested 20-run CLI loop is still pending.Evidence update: the adapter now sends the initial PTY dimensions in the exec-create config. The focused real-Docker regression passed 1/1 and observed both 24 80 and 47 123 in the child. The public CLI contract test also passed once after this change (133.30s). I am running the requested 20-run loop now; fast and integration gates remain pending final execution.
Integration finding:
scripts/verify integrationpassed the release binary build, real HTTP check, Docker and rootless Podman socket checks, engine adapters against both runtimes, and bounded follow:false snapshots. It then failed intypescript_client_streams_logs_and_exec_for_docker_and_podman: opening the Docker TTY exec returned an engine HTTP 500 (The exec process could not be started, engine_code 500). The gate started with load5 7.23 but had risen to 11.54 by the time I inspected the failure. I am checking this runtime path under load5 below 8 before classifying it as a product regression or load-sensitive failure.Second integration attempt: the TypeScript stream-client check passed. The gate then failed at
stream authorization and CLOSE regressions against Docker:every_grant_shrink_closes_logs_and_persistent_exec_on_both_enginesreported a rootless Podmanuser_disabledexec did not start;session_http_lists_only_the_owner_and_closes_real_docker_and_podman_ptysreceived an error JSON object where it expected session data. These failures are outside the CLI PTY resize assertion. I will retry the focused regression tests when load5 is below 8 before deciding whether they need changes.Finished on branch
cli-eofat head0c7f574af8ee58af93626dc2aa03dd073491a796.The focused real-Docker PTY test passed and observed both
24 80and47 123. The public CLI test loop completed 17/20; its three failures were at Podman--tailoutput, an SSH replay marker, and a PTY harness exit timeout. None reported the resize assertion.scripts/verify fastpassed. Output:scripts/verify integrationwas attempted twice, both times starting while load5 was below 8. Run 1 stopped at the TypeScript stream-client Docker TTY open (engine HTTP 500); the focused test then passed 1/1 in 8.74s under load5 7.78. Run 2 passed that TypeScript step, then stopped in the stream authorization/CLOSE regression suite: a rootless Podmanuser_disabledexec did not start, and the session-list test received error JSON instead of session data. Load5 had risen above 8 by those later checks, so I did not start another integration run outside the specified window.Integration step output, verbatim:
Orchestrator: the fix round is done (
0c7f574). It fixed two real engine bugs: exec now keeps its TTY setting when started, and exec creation sets the initial PTY size. The resize assertion is deterministic now. The remaining integration failures are Podman exec startup (#42). This branch's verify waits for #42 to merge, then gets one clean integration run on top of it.Starting final-cli-eof on branch
cli-eof, base head0c7f574af8ee58af93626dc2aa03dd073491a796. Work: mergeorigin/dev, check SSH initial-size/resize behavior and repeat SSH and exec resize assertions 10× each, then run requested gates under the load and concurrency limits.Merge resolution and SSH finding on
cli-eof: the engine conflict keeps the incoming exec boundary marker and this branch's initialconsole_sizeinCreateExecOptions, while preservingStartExecOptions { tty }. The CLI test keeps its ULID run id and the incoming SSH replay selectors. Generated OpenAPI/client files fromorigin/devare in the merge; regeneration/drift verification is pending.The SSH OPEN path already carries terminal dimensions: CLI
connectsends them inSshParams,run_sshpasses them toSessionManager::open, andcreate_sessionpasses them toConnectedClient::open_ptyforrequest_pty. The resize signal listener was created only after OPEN_OK and session-id output, leaving a window where SIGWINCH could be missed. I moved registration before stream setup and made the real-sshd CLI assertion check initial 24x80 and wait for remote 41x111 after SIGWINCH without a fixed delay. The focused SSH resize selector is in place; verification is pending.Correction after checking the locked Bollard 0.21.1 API:
CreateExecOptionsdoes not exposeconsole_size;Docker::create_execacceptsimpl Into<ExecConfig>, and the generatedExecConfigcarriesconsole_size. The conflict resolution now constructsbollard::models::ExecConfigdirectly, retaining both the boundary environment marker fromdevand this branch's initial PTY size. API references: https://docs.rs/bollard/latest/bollard/exec/struct.CreateExecOptions.html, https://docs.rs/bollard/latest/bollard/models/struct.ExecConfig.html, https://docs.rs/bollard/latest/src/bollard/exec.rs.htmlThe first focused SSH resize run exposed a harness bug: the initial marker text was present in the remote PTY's echoed command, so the harness could mistake input echo for command output and check size too early. Evidence: run 1 failed at the new initial-size assertion with only the echoed
stty size; printf ...command in captured PTY output and no24 80output. I am changing the marker construction so it cannot appear contiguously in the echoed command, then rerunning the 10-run loop.SSH resize loop status: runs 1 and 2 passed the initial and SIGWINCH size checks. Run 3 stopped before resize in the separate piped-stdin EOF check, which timed out after five seconds with only the session-id line in output.
SSH_RESIZE_ONLYcurrently ran that unrelated check before the resize assertion; I am excluding it only in the resize-only mode so the requested 10-run loop measures the resize path. The ordinary integration run retains the EOF check.SSH resize loop update: runs 1–7 passed the initial-size and SIGWINCH-size assertions. Run 8 also observed 24x80 and 41x111, then timed out waiting for the SSH shell to report exit after the test harness sent
exit 37. The resize-only selector still included that separate remote-exit check. I am making that selector detach with SIGHUP after the resize marker and close the persistent session through the existing API cleanup; the normal integration path retains the exit-code assertion.Resize repetition results: the focused CLI SSH real-sshd path passed 10/10 runs, checking initial 24x80 and resized 41x111. It started at load average 2.04/3.83/3.80 with no verify gate active. The real rootful-Docker exec integration assertion passed 10/10 runs, checking initial 24x80 and resized 47x123; it started at 4.79/4.26/3.96 with no verify gate active. The SSH resize-only mode skips its independent piped-stdin EOF and remote-exit checks; the normal integration path still runs those assertions.
Integration finding: the real-server CLI/MCP integration reached the SSH-specific piped-stdin EOF check and the CLI did not exit within its five-second deadline. Diagnostic stderr contained only the credential warning and session id. The SSH client sends a stream EOF frame, and the server maps that frame to Ctrl-D (
0x04) on the remote PTY while continuing to wait for the remote shell exit, as documented indocs/contracts.md. Resize assertions are separate and had passed 10/10. I am reproducing the EOF case and tracing the real sshd path before deciding whether the implementation or test needs correction.SSH EOF reproduction result: reran the ignored
cli_and_mcp_use_the_public_http_contracttest directly against the compiled binary, real server, Docker, rootless Podman and test sshd. This run passed, including thecat; exit 3\npiped-stdin EOF assertion:test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 131.47sThe earlier integration-gate run failed the same assertion at its five-second deadline, so the failure is intermittent. I will rerun the integration gate after the other job's browser gate exits.
Final CLI EOF verification
Branch:
cli-eofHead:
31ee7f976a4d0edceecf32c3ed1196d56dd9fdfdResize assertion loops:
2.04 / 3.83 / 3.80; exec4.79 / 4.26 / 3.96.The first integration attempt timed out waiting five seconds for piped SSH stdin EOF. The direct ignored real-engine/real-sshd test passed (1 passed in 131.47s), and the full integration rerun passed, including compiled CLI/MCP and SSH contract/API checks. The browser run did not hit the known “Jump to latest” flake. Gate-start load5: integration
3.26, browser3.99.scripts/verify fastoutput:scripts/verify integrationoutput:scripts/verify browseroutput: