diff --git a/crates/agent-vm/src/run.rs b/crates/agent-vm/src/run.rs index 26549f7..bc10812 100644 --- a/crates/agent-vm/src/run.rs +++ b/crates/agent-vm/src/run.rs @@ -141,6 +141,19 @@ pub struct Args { #[arg(long = "publish", short = 'p')] publish: Vec, + /// Lima-style host ← guest auto-port-forwarding. The runtime + /// polls `/proc/net/tcp{,6}` inside the guest every ~2s and + /// mirrors each detected wildcard (`0.0.0.0`/`[::]`) TCP LISTEN + /// socket onto a host listener at `127.0.0.1:` (or + /// an ephemeral host port if the preferred one is taken). + /// Loopback-only guest binds are NOT forwarded — the smoltcp + /// dial target is the guest's VLAN address, so a guest service + /// bound to `127.0.0.1` only refuses the connection. agent-vm + /// prints each new mapping to stderr as the runtime emits + /// `PortEvent`s. Off by default. + #[arg(long = "auto-publish", default_value_t = false)] + auto_publish: bool, + /// Override the OCI image reference. Default: /// `ghcr.io/wirenboard/agent-vm-template:latest`. Use a timestamped tag /// (`...:YYYY-MM-DDTHH`) to pin a reproducible image. @@ -468,7 +481,8 @@ pub async fn launch(agent: Agent, args: Args) -> Result { let has_creds = creds.anthropic_token_file.is_some() || creds.openai_token_file.is_some() || creds.gh_token_file.is_some(); - if has_creds || !publish_ports.is_empty() { + let auto_publish = args.auto_publish; + if has_creds || !publish_ports.is_empty() || auto_publish { use crate::secrets::*; let anthropic = creds.anthropic_token_file.clone(); let openai = creds.openai_token_file.clone(); @@ -488,6 +502,9 @@ pub async fn launch(agent: Agent, args: Args) -> Result { PublishProto::Udp => n.port_udp_bind(host_bind, p.host_port, p.guest_port), }; } + if auto_publish { + n = n.auto_publish(); + } if !has_creds { return n; } @@ -669,6 +686,41 @@ pub async fn launch(agent: Agent, args: Args) -> Result { .await .context("verifying image-API contract version")?; + // Subscribe to the runtime's auto-publish event stream and + // surface each mapping to the user. Spawned regardless of + // whether auto-publish is enabled — when disabled the runtime + // never emits events, so the subscriber just idles. Cheap. + if args.auto_publish { + eprintln!("==> auto-publish: watching guest LISTEN sockets via /proc/net/tcp{{,6}}"); + let sb_for_events = sandbox.clone(); + tokio::spawn(async move { + let mut events = sb_for_events.port_events().await; + use microsandbox::protocol::network::PortEvent; + while let Some(event) = events.recv().await { + match event { + PortEvent::Added { + host_bind, + host_port, + guest_port, + } => { + eprintln!( + "==> auto-publish: guest :{guest_port} → host {host_bind}:{host_port}" + ); + } + PortEvent::Removed { + host_bind, + host_port, + guest_port, + } => { + eprintln!( + "==> auto-publish: guest :{guest_port} closed (released {host_bind}:{host_port})" + ); + } + } + } + }); + } + let inner_cmd = agent.command(); // Prepend agent-vm's default flags (e.g. --dangerously-skip-permissions // for Claude) unless the user already provided them. diff --git a/vendor/microsandbox b/vendor/microsandbox index 1e9864e..1d2c984 160000 --- a/vendor/microsandbox +++ b/vendor/microsandbox @@ -1 +1 @@ -Subproject commit 1e9864e0203fc03d2b91794990445649c69943d4 +Subproject commit 1d2c98425ae23efea047411e82c3ecb148f72782