mirror of
https://github.com/wirenboard/agent-vm.git
synced 2026-07-09 16:00:54 +00:00
claude-vm-shim: pass per-session env via runtime-hook file, not cmdline
The rc parent sets a per-session JWT in CLAUDE_CODE_SESSION_ACCESS_TOKEN plus several behavior flags (CLAUDE_CODE_ENVIRONMENT_KIND=bridge, CLAUDE_CODE_USE_CCR_V2, CLAUDE_CODE_POST_FOR_SESSION_INGRESS_V2, CLAUDE_CODE_WORKER_EPOCH, CLAUDE_CODE_SESSION_ID) when spawning the cloud-session subprocess. Without them, the in-VM claude can't authenticate to api.anthropic.com/v1/code/sessions/cse_… and the SDK stalls at `SDKStartup: phase=connecting_transport`. Original attempt was to forward these via `--env` to agent-vm, but the JWT is ~700 chars and the bundle pushes the kernel cmdline past COMMAND_LINE_SIZE=2048, after which agentd never connects and the VM times out at handshake. This commit uses agent-vm's existing `.agent-vm.runtime.sh` hook mechanism: the bash prelude sources `<project>/.agent-vm.runtime.sh` before exec'ing claude. The dispatcher writes the CLAUDE_CODE_* env exports into that file in the workspace dir; agent-vm picks it up automatically. No cmdline pressure. Bonus: TLS bypass and leak-creds patches are no longer needed — ENVIRONMENT_KIND=bridge + the JWT in env let the SDK auth and talk to api.anthropic.com through the standard MITM substitution layer. Adds a tiny env-dump helper (gated by CLAUDE_VM_SHIM_LOG) for future diagnostics. Verified e2e: rc parent shows "Capacity: 1/32 · Attached" with an in-VM-generated session name, no more `connecting_transport` stall. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
e84b83cc4d
commit
3282eaac80
1 changed files with 59 additions and 8 deletions
|
|
@ -136,6 +136,14 @@ fn run_vm_cloud_session(forwarded: &[OsString]) -> std::io::Result<i32> {
|
|||
let vm_workdir = format!("{workdir_root}/{}", std::process::id());
|
||||
let _ = std::fs::create_dir_all(&vm_workdir);
|
||||
|
||||
// Drop a `.agent-vm.runtime.sh` hook in the workdir. agent-vm's bash
|
||||
// prelude sources this before exec'ing claude, so we can publish
|
||||
// `CLAUDE_CODE_*` env vars (notably the per-session JWT in
|
||||
// CLAUDE_CODE_SESSION_ACCESS_TOKEN) without putting them on the kernel
|
||||
// cmdline — stock x86 COMMAND_LINE_SIZE=2048 truncates and the VM
|
||||
// never boots if we add ~11 of these via `agent-vm --env`.
|
||||
write_cloud_runtime_hook(&vm_workdir);
|
||||
|
||||
let agent_vm = resolve_agent_vm_binary();
|
||||
let msb_path = resolve_msb_path();
|
||||
|
||||
|
|
@ -156,16 +164,10 @@ fn run_vm_cloud_session(forwarded: &[OsString]) -> std::io::Result<i32> {
|
|||
// Without this the in-guest claude can't read user messages from
|
||||
// the cloud session.
|
||||
.env("AGENT_VM_FORWARD_STDIN", "1")
|
||||
// Disable TLS MITM for the Anthropic cloud-session SDK endpoint.
|
||||
// The streaming transport at /v1/code/sessions/cse_… pins
|
||||
// Anthropic's real certificate and stalls at `connecting_transport`
|
||||
// through the microsandbox CA-signed MITM cert. Allowing this
|
||||
// host to bypass interception is correct: cloud sessions
|
||||
// authenticate with the real OAuth token directly (no
|
||||
// placeholder injection involved).
|
||||
.env("AGENT_VM_TLS_BYPASS", "api.anthropic.com")
|
||||
.arg("claude")
|
||||
.arg("--no-git")
|
||||
.arg("--memory").arg("1")
|
||||
.arg("--cpus").arg("1")
|
||||
.current_dir(&vm_workdir)
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
|
|
@ -200,6 +202,55 @@ fn run_vm_cloud_session(forwarded: &[OsString]) -> std::io::Result<i32> {
|
|||
Ok(code)
|
||||
}
|
||||
|
||||
/// Write a `.agent-vm.runtime.sh` hook into the workdir that exports the
|
||||
/// `CLAUDE_CODE_*` env vars the rc parent set for this per-session subprocess.
|
||||
/// agent-vm's bash prelude sources this file before exec'ing claude.
|
||||
fn dump_env_for_debug() {
|
||||
if let Ok(p) = std::env::var("CLAUDE_VM_SHIM_LOG") {
|
||||
if !p.is_empty() {
|
||||
let dump_path = format!("{p}.env-pid{}", std::process::id());
|
||||
use std::io::Write;
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&dump_path)
|
||||
{
|
||||
let mut vars: Vec<_> = std::env::vars().collect();
|
||||
vars.sort();
|
||||
for (k, v) in vars {
|
||||
let _ = writeln!(f, "{k}={v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_cloud_runtime_hook(vm_workdir: &str) {
|
||||
dump_env_for_debug();
|
||||
let path = format!("{vm_workdir}/.agent-vm.runtime.sh");
|
||||
let mut script = String::from("#!/bin/sh\n# auto-generated by claude-vm-shim/dispatcher\n");
|
||||
for (name, value) in std::env::vars() {
|
||||
if !name.starts_with("CLAUDE_CODE_") || value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Skip vars that would confuse the in-VM child or are host-specific.
|
||||
if matches!(name.as_str(), "CLAUDE_CODE_EXECPATH") {
|
||||
continue;
|
||||
}
|
||||
// Single-quote escape: end quote, escaped quote, reopen quote.
|
||||
let escaped = value.replace('\'', "'\\''");
|
||||
script.push_str(&format!("export {name}='{escaped}'\n"));
|
||||
}
|
||||
let _ = std::fs::write(&path, script);
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Ok(mut p) = std::fs::metadata(&path).map(|m| m.permissions()) {
|
||||
p.set_mode(0o755);
|
||||
let _ = std::fs::set_permissions(&path, p);
|
||||
}
|
||||
debug_log(&format!("wrote cloud runtime hook → {path}"));
|
||||
}
|
||||
|
||||
fn resolve_msb_path() -> Option<String> {
|
||||
if let Ok(p) = std::env::var("CLAUDE_VM_SHIM_MSB_PATH") {
|
||||
if !p.is_empty() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue