Interactive PTY: compound commands silently produce no output (WASI EFAULT), and shell diagnostics never reach the terminal
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 4.6k
- Forks
- 251
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 34
Description
Summary
Under an interactive PTY (terminal.open() → brush), compound commands produce no output and no error. The same constructs work through exec() and through sh -c inside the same PTY, so this is specific to brush's interactive path, not to pipelines.
Two symptoms, likely one root cause — both are writes from the interactive shell that never reach the terminal:
- Compound commands (
;,&&,|,( ), backticks) print nothing. Redirecting stderr shows the real failure is WASI errno 21 (EFAULT) on a write. - brush's own diagnostics never reach the PTY at all, so an unknown command silently returns to the prompt instead of printing
command not found.
Symptom 2 is what makes symptom 1 invisible: the shell is reporting an error, it just goes nowhere.
Reproduction
import { AgentOs } from "@rivet-dev/agentos-core";
import common from "@agentos-software/common";
const vm = await AgentOs.create({ software: [common] });
const shell = await vm.terminal.open({ cols: 80, rows: 24 });
const id = shell.shellId;
let buf = "";
vm.onShellData(id, (e) => { buf += Buffer.from(e?.data ?? e).toString("utf8"); });
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
await sleep(1500);
for (const c of [
"echo one", // OK
"echo b; echo c", // FAIL - no output
"echo d && echo e", // FAIL
"echo a | cat", // FAIL
"(echo sub)", // FAIL
"echo `echo backtick`", // FAIL
"echo 'semi;colon'", // OK - quoted, so it is one simple command
"sh -c 'echo b; echo c'", // OK - same PTY, non-interactive brush
"nosuchcmd", // FAIL - no "command not found"
]) {
buf = "";
await vm.terminal.write(id, c + "\n");
await sleep(3000);
console.log(JSON.stringify(c), "=>", JSON.stringify(buf.replace(/\r/g, "")));
}
Observed:
| command | interactive PTY | exec() |
|---|---|---|
echo one |
one |
ok |
uname -a |
correct output | ok |
echo 'semi;colon' |
semi;colon |
ok |
sh -c 'echo a | cat' |
a |
ok |
sh -c 'echo b; echo c' |
b c |
ok |
echo b; echo c |
nothing | ok |
echo d && echo e |
nothing | ok |
echo a | cat |
nothing | ok |
(echo sub) |
nothing | ok |
nosuchcmd |
nothing (no error) | prints error |
Note echo here is a brush builtin, so the failing case involves no external command at all.
The hidden error
Redirecting stderr to a file makes both failures visible:
$ nosuchcmd 2>/tmp/e1
$ cat /tmp/e1
error: command not found: nosuchcmd
$ echo b; echo c 2>/tmp/e2
$ cat /tmp/e2
error: echo: i/o error: Bad address (os error 21)
Under wasm32-wasip1, errno 21 is EFAULT (WASI numbering, not Linux's EISDIR), so "Bad address" and the code agree. brush parsed and executed the list correctly — the write to stdout failed.
Ruled out
- Pipelines /
;themselves —exec("ls /opt/agentos/bin | head -5")returns correct output. - The kernel PTY, fd wiring, job control —
sh -c 'echo a | cat'works inside the failing interactive shell. - stderr routing generally — a child's stderr does reach the terminal (
sh -c 'echo to-stderr 1>&2'renders). Only the interactive shell's own stderr is lost. TERM— identical failure withTERM=xterm-256colorandTERM=dumb.- The terminal client — reproduced entirely server-side via
vm.terminal.write(), no browser involved.
Where to look
_fdWrite in crates/execution/assets/runners/wasi-module.js:1404 returns EFAULT only via _writeUint32 (:413) — _mapFsError (:811) defaults to EIO, not EFAULT. So the failing path is the nwritten pointer write, or a synthetic-pipe branch, rather than a generic fs error.
crates/CLAUDE.md already documents this failure mode in the WASM host-process bridge:
child_process.pollreturningECHILDafter anexitevent's trailing-drain pass is terminal, not a new fault … post-exit drain loops must stop onECHILDinstead of converting a successful pipeline intoWASI_ERRNO_FAULT.
Debug logging: AGENTOS_WASM_WASI_DEBUG=1 enables [agentos-wasi] traces from that runner.
Per software/CLAUDE.md ("fix portability one layer down, in the sysroot … patch the real upstream tool only as a fallback"), the fix likely belongs in the WASI/fd layer rather than in brush.
Related
- #1881 — adjacent but different: kernel-stub command resolution inside subshells via
exec, whereecho hi | head -n 1still works. Shares the "silent failure / misleading exit code" theme. - #1657 — other brush redirection divergences (
>>,< file). - #1746 — merged; restored interactive shell correctness and added a real Brush interactive PTY suite, which is the natural harness for a regression test here.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with _fdWrite in crates/execution/assets/runners/wasi-module.js, especially _writeUint32 and the synthetic-pipe path, then read the WASM host-process bridge guidance in crates/CLAUDE.md. Enable AGENTOS_WASM_WASI_DEBUG=1 and use the Brush interactive PTY suite added by #1746; done means compound-command output and brush diagnostics reach the terminal without EFAULT.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, rust, shell, wasm
- Domain
- cli, operating-systems, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100