mirror of
https://github.com/wirenboard/agent-vm.git
synced 2026-07-09 16:00:54 +00:00
claude-vm-shim: forward dispatcher stdin to in-VM claude
`agent-vm`'s non-TTY exec path uses `exec_stream_with` (output-only)
*and* injects a `[ -t 0 ] || exec < /dev/null` redirect into the bash
prelude that wraps the agent invocation. Together they close the in-guest
process's stdin before it can read anything.
That's fatal for `claude --print --input-format stream-json` (the
per-session subprocess of `claude remote-control`): the in-VM claude
announces "ready" on stdout, but never receives the user messages the rc
parent forwards from claude.ai/code on stdin. The cloud session shows
"Attached" and then sits silent.
Two changes, both env-gated by AGENT_VM_FORWARD_STDIN=1 so default
behavior is unchanged:
crates/agent-vm/src/run.rs:
- Skip the `exec < /dev/null` line in the bash prelude when the env
var is set.
- In the streaming-exec branch, call `e.stdin_pipe()` and spawn a
tokio task that pumps tokio::io::stdin() into the returned
ExecSink (with .close() on EOF / error).
claude-vm-shim/dispatcher/src/main.rs:
- Set AGENT_VM_FORWARD_STDIN=1 on each agent-vm spawn.
- Reorder the agent-vm-binary candidate list to prefer
/opt/claude-vm-shim/bin/agent-vm. Required because the npm-shipped
agent-vm doesn't yet have the patch; the locally-built copy does.
Tested: rc parent boots a VM and stays connected to the cloud session.
The in-VM agent-vm-bc9a32b2e20c sandbox shows stock public_only network
policy (no `private` rule); the dispatcher now spawns the patched
agent-vm from /opt/claude-vm-shim/bin.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
44da7166fb
commit
56cbda8d65
2 changed files with 53 additions and 7 deletions
|
|
@ -143,6 +143,11 @@ fn run_vm_cloud_session(forwarded: &[OsString]) -> std::io::Result<i32> {
|
|||
|
||||
let mut cmd = Command::new(&agent_vm);
|
||||
cmd.env("MSB_PATH", &msb_path)
|
||||
// Tell agent-vm to (a) skip the default non-TTY stdin → /dev/null
|
||||
// redirect and (b) wire stdin through the streaming exec API.
|
||||
// Without this the in-guest claude can't read user messages from
|
||||
// the cloud session.
|
||||
.env("AGENT_VM_FORWARD_STDIN", "1")
|
||||
.arg("claude")
|
||||
.arg("--no-git")
|
||||
.current_dir(&vm_workdir)
|
||||
|
|
@ -180,13 +185,16 @@ fn resolve_agent_vm_binary() -> String {
|
|||
return p;
|
||||
}
|
||||
}
|
||||
// The npm-installed `/usr/bin/agent-vm` is a `#!/usr/bin/env node` shim
|
||||
// and won't resolve `node` under the daemon's scrubbed PATH. Prefer the
|
||||
// native ELF binary in the platform subpackage.
|
||||
// Prefer a local patched copy at /opt/claude-vm-shim/bin/agent-vm —
|
||||
// the upstream npm one doesn't honor AGENT_VM_FORWARD_STDIN yet, which
|
||||
// breaks stream-json input to in-VM claude. Fall back to the
|
||||
// npm-installed platform subpackage's ELF (the `/usr/bin/agent-vm`
|
||||
// shim is a `#!/usr/bin/env node` wrapper that won't resolve `node`
|
||||
// under the daemon's scrubbed PATH).
|
||||
let candidates = [
|
||||
"/opt/claude-vm-shim/bin/agent-vm",
|
||||
"/usr/lib/node_modules/@wirenboard/agent-vm/node_modules/@wirenboard/agent-vm-linux-x64/bin/agent-vm",
|
||||
"/usr/local/lib/node_modules/@wirenboard/agent-vm/node_modules/@wirenboard/agent-vm-linux-x64/bin/agent-vm",
|
||||
"/opt/claude-vm-shim/bin/agent-vm",
|
||||
"/usr/bin/agent-vm",
|
||||
"/usr/local/bin/agent-vm",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -746,10 +746,24 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
fi\n",
|
||||
)
|
||||
};
|
||||
// The `[ -t 0 ] || exec < /dev/null` redirect satisfies codex 0.133's
|
||||
// exec subcommand under non-TTY launches, but breaks any agent that
|
||||
// needs to read its stdin in non-TTY mode — notably
|
||||
// `claude --print --input-format stream-json` as used by
|
||||
// `claude remote-control` per-session subprocesses. Skip the redirect
|
||||
// when AGENT_VM_FORWARD_STDIN=1; the launcher then also wires the
|
||||
// streaming-exec path with `stdin_pipe()` below so our stdin reaches
|
||||
// the in-guest agent.
|
||||
let forward_stdin =
|
||||
std::env::var("AGENT_VM_FORWARD_STDIN").as_deref() == Ok("1");
|
||||
let stdin_null_line = if forward_stdin {
|
||||
""
|
||||
} else {
|
||||
"[ -t 0 ] || exec < /dev/null\n "
|
||||
};
|
||||
let prelude = format!(
|
||||
"sed -i '/^nameserver .*:/d' /etc/resolv.conf 2>/dev/null || true\n\
|
||||
[ -t 0 ] || exec < /dev/null\n\
|
||||
{chrome_mcp_prelude}\
|
||||
{stdin_null_line}{chrome_mcp_prelude}\
|
||||
_hook={path}/.agent-vm.runtime.sh\n\
|
||||
if [ -f \"$_hook\" ]; then\n\
|
||||
\techo \"==> sourcing $_hook\" >&2\n\
|
||||
|
|
@ -793,7 +807,8 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
use tokio::io::AsyncWriteExt as _;
|
||||
let mut handle = sandbox
|
||||
.exec_stream_with(cmd, |e| {
|
||||
e.args(agent_args).cwd(project_guest_path.clone())
|
||||
let e = e.args(agent_args).cwd(project_guest_path.clone());
|
||||
if forward_stdin { e.stdin_pipe() } else { e }
|
||||
})
|
||||
.await
|
||||
.with_context(|| {
|
||||
|
|
@ -802,6 +817,29 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
sandbox_log_dir(&session.sandbox_name).display()
|
||||
)
|
||||
})?;
|
||||
|
||||
// When AGENT_VM_FORWARD_STDIN=1, pump our stdin into the in-guest
|
||||
// process's stdin. Required for stream-json mode under `claude --print
|
||||
// --input-format stream-json` (used by `claude remote-control`
|
||||
// per-session subprocesses).
|
||||
if let Some(sink) = handle.take_stdin() {
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::AsyncReadExt as _;
|
||||
let mut stdin = tokio::io::stdin();
|
||||
let mut buf = vec![0u8; 16 * 1024];
|
||||
loop {
|
||||
match stdin.read(&mut buf).await {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => {
|
||||
if sink.write(&buf[..n]).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = sink.close().await;
|
||||
});
|
||||
}
|
||||
let mut stdout = tokio::io::stdout();
|
||||
let mut stderr = tokio::io::stderr();
|
||||
// Track whether we actually saw an Exited event. The previous
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue