agent-vm: fix long-session ECONNRESET burst + MCP proxy auth

Long Claude Code sessions in agent-vm (the failure mode users see
as "every 10 seconds, Retrying in 17s · attempt 6/10" until the
session is dead) were hitting a false positive in the secret-
substitution layer. When a session's prompt body contained the
literal name of any non-Anthropic placeholder string (e.g.
investigation sessions reading other sessions' jsonl), the scan
flagged the request, dropped the conn mid-upload, and undici
reported it as ECONNRESET. Retries hit the same content, same
violation; the session became permanently wedged from that turn on.

This commit pairs with the vendor change `secrets/handler: scope
violation scan to header bytes only`, which restricts the scan to
credential-bearing positions (Authorization / X-*-Key headers, URL
query params on the request line) and leaves user-supplied body
content alone. See the bumped submodule for full rationale.

Local changes:

* **secrets.rs**: add ANTHROPIC_MCP_PROXY_HOST. Claude Code's MCP
  relay endpoint sends the same Anthropic access token but wasn't
  in the placeholder's allowed_hosts list, so every MCP request
  tripped the violation scan (also surfaced as ECONNRESET to the
  MCP client). Allowing this host fixes MCP for in-VM Claude.

* **run.rs**: thread that new host into the network builder, and
  add a focused post-mortem hint when `attach()` returns an error.
  The hint points at the per-sandbox log files
  (`~/.microsandbox/sandboxes/<name>/logs/{runtime.log,
  msb.stderr.log,msb-exit.log}`) and suggests `dmesg -T | tail`
  for host-OOM-kill cases. Surfaces what the vendor's `runtime:
  tee msb stderr` change makes available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Evgeny Boger 2026-05-25 01:01:29 +03:00
parent 920f6c155c
commit dbe92103b1
3 changed files with 41 additions and 5 deletions

View file

@ -407,6 +407,7 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
.inject_basic_auth(false)
.allow_host(ANTHROPIC_API_HOST)
.allow_host(ANTHROPIC_OAUTH_HOST)
.allow_host(ANTHROPIC_MCP_PROXY_HOST)
});
}
if let Some(file) = openai {
@ -622,10 +623,26 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
let t_run = Instant::now();
let exit = if std::io::stdin().is_terminal() {
eprintln!("==> Attaching to {inner_cmd}");
sandbox
.attach(cmd, agent_args)
.await
.with_context(|| format!("attaching to {inner_cmd}"))?
match sandbox.attach(cmd, agent_args).await {
Ok(code) => code,
Err(err) => {
// Print focused pointers to the post-mortem logs the
// runtime writes (spawn.rs tees msb's stderr;
// handle.rs appends to msb-exit.log on reap). The
// raw-mode scopeguard inside attach_inner has already
// restored the terminal by the time we see this Err.
eprintln!("==> attach failed: {err:#}");
eprintln!(
"==> Post-mortem (if a VM death happened, the cause is usually in one of these):"
);
let log_dir = msb_sandbox_log_dir(&session.sandbox_name);
eprintln!(" {}/runtime.log (msb tracing / panic)", log_dir.display());
eprintln!(" {}/msb.stderr.log (libkrun / kernel printk / OOM)", log_dir.display());
eprintln!(" {}/msb-exit.log (exit status of each boot)", log_dir.display());
eprintln!(" dmesg -T | tail (host-side OOM-kill of the msb process)");
return Err(err).with_context(|| format!("attaching to {inner_cmd}"));
}
}
} else {
// No host TTY (piped, redirected, smoke-tested under `sg`/`sudo` etc.).
// attach() needs a real /dev/tty for raw-mode stdin, so use the
@ -703,6 +720,20 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
Ok(exit)
}
/// Best-effort guess at where msb writes per-sandbox runtime logs.
/// Mirrors `microsandbox_utils::resolve_home()` / `sandboxes_dir()`:
/// `$MSB_HOME` or `~/.microsandbox`, then `sandboxes/<name>/logs`.
/// Used only for human-readable post-mortem hints, so a stale guess
/// (e.g. caller overrode the home via config) just makes the hint
/// slightly off rather than breaking anything.
fn msb_sandbox_log_dir(sandbox_name: &str) -> PathBuf {
let home = env::var_os("MSB_HOME")
.map(PathBuf::from)
.or_else(|| env::var_os("HOME").map(|h| PathBuf::from(h).join(".microsandbox")))
.unwrap_or_else(|| PathBuf::from("/var/lib/microsandbox"));
home.join("sandboxes").join(sandbox_name).join("logs")
}
/// One `--mount HOST[:GUEST]` argument resolved into separate paths.
struct ExtraMount {
host: PathBuf,

View file

@ -81,6 +81,11 @@ pub const GH_TOKEN_PLACEHOLDER: &str = "MSB_PLACEHOLDER_GH_TOKEN_v1";
pub const ANTHROPIC_API_HOST: &str = "api.anthropic.com";
pub const ANTHROPIC_OAUTH_HOST: &str = "platform.claude.com";
/// Claude Code's MCP relay endpoint. Claude Code's HTTP client sends
/// the same Anthropic access token here, so the secret substitution
/// has to allow this destination too — otherwise the placeholder
/// trips the violation scan and the conn gets dropped, breaking MCP.
pub const ANTHROPIC_MCP_PROXY_HOST: &str = "mcp-proxy.anthropic.com";
pub const OPENAI_API_HOST: &str = "api.openai.com";
pub const OPENAI_CHATGPT_HOST: &str = "chatgpt.com";
pub const OPENAI_OAUTH_HOST: &str = "auth.openai.com";

2
vendor/microsandbox vendored

@ -1 +1 @@
Subproject commit 4ad3a67f91e237aea2e4e295a086675ae37dddde
Subproject commit db9865556d66e3a06cebb9583c6b83d5253a8bd0