mirror of
https://github.com/wirenboard/agent-vm.git
synced 2026-07-09 16:00:54 +00:00
help: regroup and tighten the CLI usage output
The launch verbs (claude/codex/opencode/shell) share one `run::Args`,
and every flag carried its full multi-sentence explanation as the
*short* help. clap renders the first doc paragraph as `-h` text, so
`-h` was a wall: 13 flags, each 3-8 lines of run-on prose. Worse, the
crate built clap without `wrap_help`, so `--help` printed each long
paragraph as one unwrapped line — the terminal soft-wrapped it, but
copy/paste and narrow widths showed a single giant line per flag.
Split short vs long help, and group the flags:
- Every option now leads with a terse one-line summary (the `-h`
text); the original detail is kept as follow-on paragraphs that
only show under `--help`.
- Flags are bucketed with `help_heading`: Sandbox resources /
GitHub access / Mounts & ports / Network egress / Image.
- Added the `wrap_help` clap feature so `--help` prose wraps to the
terminal width instead of emitting one line per paragraph.
- Real value names: <GIB>, <N>, <HOST[:GUEST]>, <[BIND:]HOST:GUEST>,
<IP|CIDR>, <OWNER/REPO>, <REF>.
- `--help` gains an Examples block, a host<->guest Networking
cheatsheet, and an Environment section (vars verified against the
code).
Top level: wire up `-V/--version` (it errored before), add a
"Getting started" footer, make the subcommand descriptions terse and
parallel, and drop the redundant "See `clipboard --help`" line.
setup/pull `--image` get the same terse-first-line split.
Pure presentation change — no flag, env var, or behaviour added or
removed. `cargo test -p agent-vm`: 123 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
63cab08d82
commit
f881e073c1
6 changed files with 148 additions and 62 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -639,6 +639,7 @@ dependencies = [
|
|||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"terminal_size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4627,6 +4628,16 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ path = "src/main.rs"
|
|||
[dependencies]
|
||||
microsandbox = { path = "../../vendor/microsandbox/crates/microsandbox" }
|
||||
tokio = { version = "1.42", features = ["macros", "rt-multi-thread", "time"] }
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
# `wrap_help` makes clap detect the terminal width and wrap long help
|
||||
# text to it; without it the multi-paragraph `--help` prose renders as
|
||||
# unbroken one-line-per-paragraph walls.
|
||||
clap = { version = "4.5", features = ["derive", "env", "wrap_help"] }
|
||||
anyhow = "1.0"
|
||||
base64 = "0.22"
|
||||
sha2 = "0.10"
|
||||
|
|
|
|||
|
|
@ -18,8 +18,23 @@ mod setup;
|
|||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
// Shown under the top-level `agent-vm --help`, after the command list.
|
||||
const TOP_AFTER_HELP: &str = "\
|
||||
Getting started:
|
||||
agent-vm setup fetch and verify the base image (run once first)
|
||||
cd ~/your-project
|
||||
agent-vm claude launch in this project — or codex / opencode / shell
|
||||
|
||||
claude, codex, opencode and shell share the same options;
|
||||
see `agent-vm claude --help` for mounts, ports, networking and credentials.";
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "agent-vm", about = "Sandboxed VMs for AI coding agents.")]
|
||||
#[command(
|
||||
name = "agent-vm",
|
||||
version,
|
||||
about = "Sandboxed microVMs for AI coding agents.",
|
||||
after_help = TOP_AFTER_HELP
|
||||
)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
cmd: Cmd,
|
||||
|
|
@ -27,26 +42,25 @@ struct Cli {
|
|||
|
||||
#[derive(Subcommand)]
|
||||
enum Cmd {
|
||||
/// Build (and verify) the agent-vm base image.
|
||||
/// Pull and verify the base image (run once first).
|
||||
Setup(setup::Args),
|
||||
|
||||
/// Pull the latest image from the registry into the microsandbox cache.
|
||||
/// Refresh the cached base image.
|
||||
Pull(pull::Args),
|
||||
|
||||
/// Launch Claude Code in a sandbox mounted at the project's host path.
|
||||
/// Launch Claude Code in a per-project sandbox.
|
||||
Claude(run::Args),
|
||||
|
||||
/// Launch Codex CLI in a sandbox mounted at the project's host path.
|
||||
/// Launch Codex CLI in a per-project sandbox.
|
||||
Codex(run::Args),
|
||||
|
||||
/// Launch OpenCode in a sandbox mounted at the project's host path.
|
||||
/// Launch OpenCode in a per-project sandbox.
|
||||
Opencode(run::Args),
|
||||
|
||||
/// Open a bash shell in a sandbox mounted at the project's host path.
|
||||
/// Open a bash shell in a per-project sandbox.
|
||||
Shell(run::Args),
|
||||
|
||||
/// Exchange a string between the host and the per-project sandbox.
|
||||
/// See `agent-vm clipboard --help`.
|
||||
/// Exchange a string between the host and the sandbox.
|
||||
Clipboard(clipboard::Args),
|
||||
|
||||
/// Internal: invoked by msb's interceptor hook for matched OAuth
|
||||
|
|
|
|||
|
|
@ -24,11 +24,12 @@ use microsandbox::{Sandbox, sandbox::PullPolicy};
|
|||
|
||||
#[derive(ClapArgs)]
|
||||
pub struct Args {
|
||||
/// Override the image reference. Defaults to
|
||||
/// `ghcr.io/wirenboard/agent-vm-template:latest` or the value of
|
||||
/// `AGENT_VM_IMAGE_TAG`. Use a timestamped tag
|
||||
/// Override the image reference.
|
||||
///
|
||||
/// Defaults to `ghcr.io/wirenboard/agent-vm-template:latest` or the
|
||||
/// value of `AGENT_VM_IMAGE_TAG`. Use a timestamped tag
|
||||
/// (`...:YYYY-MM-DDTHH`) to pin a specific build.
|
||||
#[arg(long, env = "AGENT_VM_IMAGE_TAG")]
|
||||
#[arg(long, env = "AGENT_VM_IMAGE_TAG", value_name = "REF")]
|
||||
image: Option<String>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,32 +94,77 @@ impl Agent {
|
|||
}
|
||||
}
|
||||
|
||||
// Footer shown under `-h` (the short summary). A few high-value
|
||||
// examples plus a pointer to `--help`. Printed verbatim by clap, so
|
||||
// this is exactly what the user sees. Kept command-agnostic: all four
|
||||
// launch verbs (claude/codex/opencode/shell) share this `Args`.
|
||||
const LAUNCH_AFTER_HELP: &str = "\
|
||||
Examples:
|
||||
agent-vm claude launch Claude Code in the current project
|
||||
agent-vm shell open a bash shell instead
|
||||
agent-vm claude -p 8080:3000 publish guest :3000 to host 127.0.0.1:8080
|
||||
agent-vm claude -- --model opus forward args to the agent (after --)
|
||||
|
||||
Trailing args go to the agent. Run with --help for networking, security, and env details.";
|
||||
|
||||
// Fuller footer shown under `--help`. Same command-agnostic constraint.
|
||||
const LAUNCH_AFTER_LONG_HELP: &str = "\
|
||||
Examples:
|
||||
agent-vm claude launch in the current project
|
||||
agent-vm shell open a bash shell instead
|
||||
agent-vm shell -- -c 'cargo test' run one command, then exit
|
||||
agent-vm claude -- --model opus --resume forward args to the agent
|
||||
agent-vm claude --memory 8 --cpus 4 a bigger sandbox
|
||||
agent-vm claude --mount ~/ref -p 3000:3000 extra mount + publish a port
|
||||
agent-vm claude --repo owner/other-repo widen the GitHub allow-list
|
||||
|
||||
Networking (deny-by-default; flags compose):
|
||||
--publish [BIND:]HOST:GUEST host → guest open an inbound port
|
||||
--auto-publish guest → host mirror every guest listener to loopback
|
||||
--allow-egress IP|CIDR guest → IP/LAN open one address or subnet
|
||||
--allow-lan guest → LAN open the whole private range
|
||||
--allow-host guest → host reach services on host 127.0.0.1
|
||||
|
||||
Environment:
|
||||
AGENT_VM_MEMORY_GIB / AGENT_VM_CPUS same as --memory / --cpus
|
||||
AGENT_VM_IMAGE_TAG same as --image
|
||||
AGENT_VM_PROFILE print per-phase boot timings
|
||||
AGENT_VM_DEBUG_CONFIG dump the SandboxConfig JSON before boot
|
||||
AGENT_VM_NO_CHROME_MCP skip the Chrome DevTools MCP setup
|
||||
RUST_LOG tracing filter (e.g. agent_vm=debug)";
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
#[command(after_help = LAUNCH_AFTER_HELP, after_long_help = LAUNCH_AFTER_LONG_HELP)]
|
||||
pub struct Args {
|
||||
/// Sandbox memory in GiB.
|
||||
#[arg(long, env = "AGENT_VM_MEMORY_GIB", default_value_t = 2)]
|
||||
/// Sandbox memory, in GiB.
|
||||
#[arg(long, env = "AGENT_VM_MEMORY_GIB", default_value_t = 2,
|
||||
value_name = "GIB", help_heading = "Sandbox resources")]
|
||||
memory: u32,
|
||||
|
||||
/// vCPU count for the sandbox.
|
||||
#[arg(long, env = "AGENT_VM_CPUS", default_value_t = 2)]
|
||||
#[arg(long, env = "AGENT_VM_CPUS", default_value_t = 2,
|
||||
value_name = "N", help_heading = "Sandbox resources")]
|
||||
cpus: u8,
|
||||
|
||||
/// Don't inject host gh/git credentials into the guest. With this
|
||||
/// set, no gh auth flows through the proxy and the guest agent
|
||||
/// can't `git push` / `gh pr create` etc. Useful for one-off
|
||||
/// Don't inject host gh/git credentials into the guest.
|
||||
///
|
||||
/// With this set, no gh auth flows through the proxy and the guest
|
||||
/// agent can't `git push` / `gh pr create` etc. Useful for one-off
|
||||
/// throwaway sessions on a repo you don't trust the agent with.
|
||||
#[arg(long = "no-git", default_value_t = false)]
|
||||
#[arg(long = "no-git", default_value_t = false, help_heading = "GitHub access")]
|
||||
no_git: bool,
|
||||
|
||||
/// Add a GitHub `owner/repo` slug to the per-launch allow-list
|
||||
/// (repeatable). The cwd's `git remote -v` GitHub entries are
|
||||
/// always included; use this to widen for cross-repo work.
|
||||
#[arg(long = "repo")]
|
||||
/// Add a repo to the GitHub allow-list (repeatable).
|
||||
///
|
||||
/// The cwd's `git remote -v` GitHub entries are always included;
|
||||
/// use this to widen the allow-list for cross-repo work.
|
||||
#[arg(long = "repo", value_name = "OWNER/REPO", help_heading = "GitHub access")]
|
||||
repo: Vec<String>,
|
||||
|
||||
/// Extra host directories to bind into the guest. Format:
|
||||
/// `HOST[:GUEST]`. `GUEST` defaults to `HOST` (mirror at the
|
||||
/// same absolute path). Repeatable.
|
||||
/// Bind an extra host directory into the guest (repeatable).
|
||||
///
|
||||
/// Format `HOST[:GUEST]`; `GUEST` defaults to `HOST` (mirror at the
|
||||
/// same absolute path).
|
||||
///
|
||||
/// Each `--mount` consumes one virtio-fs device. The microsandbox
|
||||
/// runtime enables msb_krun's userspace split irqchip, which on
|
||||
|
|
@ -129,24 +174,27 @@ pub struct Args {
|
|||
/// (shared with rootfs, network, vsock, console, and any
|
||||
/// `--volume` disks — call it ~210 user mounts for the common
|
||||
/// config). You can stop worrying about it for typical workloads.
|
||||
#[arg(long = "mount")]
|
||||
#[arg(long = "mount", value_name = "HOST[:GUEST]", help_heading = "Mounts & ports")]
|
||||
mount: Vec<String>,
|
||||
|
||||
/// Publish a guest TCP port to the host. Format:
|
||||
/// `[HOST_BIND:]HOST_PORT:GUEST_PORT` (docker-style). HOST_BIND
|
||||
/// defaults to `127.0.0.1` — pass `0.0.0.0:HOST_PORT:GUEST_PORT`
|
||||
/// to expose on every host interface. Repeatable.
|
||||
/// Publish a guest TCP port to the host, docker-style (repeatable).
|
||||
///
|
||||
/// Format `[HOST_BIND:]HOST_PORT:GUEST_PORT`. HOST_BIND defaults to
|
||||
/// `127.0.0.1` — pass `0.0.0.0:HOST_PORT:GUEST_PORT` to expose on
|
||||
/// every host interface.
|
||||
///
|
||||
/// The guest service must listen on `0.0.0.0` (or the assigned
|
||||
/// guest IP from `MSB_NET_IPV4`); a bare `127.0.0.1` bind inside
|
||||
/// the guest is not reachable because the smoltcp dial target is
|
||||
/// the guest's assigned VLAN address, not loopback.
|
||||
#[arg(long = "publish", short = 'p')]
|
||||
#[arg(long = "publish", short = 'p', value_name = "[BIND:]HOST:GUEST",
|
||||
help_heading = "Mounts & ports")]
|
||||
publish: Vec<String>,
|
||||
|
||||
/// 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`/`[::]`) OR
|
||||
/// Auto-forward every guest listener onto the host (Lima-style).
|
||||
///
|
||||
/// The runtime polls `/proc/net/tcp{,6}` inside the guest every ~2s
|
||||
/// and mirrors each detected wildcard (`0.0.0.0`/`[::]`) OR
|
||||
/// loopback (`127.0.0.1`/`[::1]`) TCP LISTEN socket onto a
|
||||
/// host listener at `127.0.0.1:<same port>` (or an ephemeral
|
||||
/// host port if the preferred one is taken). Loopback-only
|
||||
|
|
@ -162,14 +210,14 @@ pub struct Args {
|
|||
/// other processes on the host's loopback. If you don't want
|
||||
/// that, omit `--auto-publish` and use `--publish` to expose
|
||||
/// only the specific ports you mean to share.
|
||||
#[arg(long = "auto-publish", default_value_t = false)]
|
||||
#[arg(long = "auto-publish", default_value_t = false, help_heading = "Mounts & ports")]
|
||||
auto_publish: bool,
|
||||
|
||||
/// Punch a hole through the default egress policy for one IP
|
||||
/// or CIDR. Repeatable. Examples:
|
||||
/// `--allow-egress 10.100.1.75` (single host)
|
||||
/// `--allow-egress 10.100.1.0/24` (CIDR)
|
||||
/// `--allow-egress fd00::1/128` (IPv6)
|
||||
/// Allow guest egress to one IP or CIDR (repeatable).
|
||||
///
|
||||
/// Examples: `--allow-egress 10.100.1.75` (single host),
|
||||
/// `--allow-egress 10.100.1.0/24` (CIDR),
|
||||
/// `--allow-egress fd00::1/128` (IPv6).
|
||||
///
|
||||
/// The default policy (`NetworkPolicy::public_only`) only
|
||||
/// allows DNS and the `Public` destination group, so RFC1918
|
||||
|
|
@ -178,10 +226,12 @@ pub struct Args {
|
|||
/// ECONNREFUSED. Use this flag to reach a specific dev box on
|
||||
/// the same LAN as the host. Use `--allow-lan` instead if you
|
||||
/// want to open the entire Private group at once.
|
||||
#[arg(long = "allow-egress")]
|
||||
#[arg(long = "allow-egress", value_name = "IP|CIDR", help_heading = "Network egress")]
|
||||
allow_egress: Vec<String>,
|
||||
|
||||
/// Switch the egress policy from `public_only` to `non_local`
|
||||
/// Allow guest egress to the whole private LAN.
|
||||
///
|
||||
/// Switches the egress policy from `public_only` to `non_local`
|
||||
/// — adds the entire `DestinationGroup::Private` (10/8,
|
||||
/// 172.16/12, 192.168/16, 100.64/10, fc00::/7) to the allow
|
||||
/// list. Coarser than `--allow-egress <CIDR>`; useful for
|
||||
|
|
@ -191,11 +241,12 @@ pub struct Args {
|
|||
/// Security note: a compromised in-guest process gets full
|
||||
/// access to every device on your LAN with this flag. Prefer
|
||||
/// `--allow-egress <CIDR>` for production-ish uses.
|
||||
#[arg(long = "allow-lan", default_value_t = false)]
|
||||
#[arg(long = "allow-lan", default_value_t = false, help_heading = "Network egress")]
|
||||
allow_lan: bool,
|
||||
|
||||
/// Allow the guest to reach services bound to `127.0.0.1` on the
|
||||
/// host. The smoltcp stack rewrites the per-sandbox gateway IP
|
||||
/// Allow the guest to reach the host's 127.0.0.1 services.
|
||||
///
|
||||
/// The smoltcp stack rewrites the per-sandbox gateway IP
|
||||
/// (resolves as `host.microsandbox.internal` inside the guest)
|
||||
/// to host's loopback, so e.g. a dev server bound to
|
||||
/// `127.0.0.1:8080` on the host becomes reachable from the guest
|
||||
|
|
@ -209,22 +260,27 @@ pub struct Args {
|
|||
/// listening on a TCP port — becomes reachable from a possibly-
|
||||
/// compromised in-guest process. Use only when you actually need
|
||||
/// it.
|
||||
#[arg(long = "allow-host", default_value_t = false)]
|
||||
#[arg(long = "allow-host", default_value_t = false, help_heading = "Network egress")]
|
||||
allow_host: 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.
|
||||
#[arg(long, env = "AGENT_VM_IMAGE_TAG")]
|
||||
/// 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.
|
||||
#[arg(long, env = "AGENT_VM_IMAGE_TAG", value_name = "REF", help_heading = "Image")]
|
||||
image: Option<String>,
|
||||
|
||||
/// Don't HEAD the registry for a newer manifest digest at
|
||||
/// launch (skips the "==> A newer image is available …" banner).
|
||||
/// Useful in CI and on flaky networks.
|
||||
#[arg(long = "no-update-check", default_value_t = false)]
|
||||
/// Skip the launch-time registry update check.
|
||||
///
|
||||
/// Don't HEAD the registry for a newer manifest digest (skips the
|
||||
/// "==> A newer image is available …" banner). Useful in CI and on
|
||||
/// flaky networks.
|
||||
#[arg(long = "no-update-check", default_value_t = false, help_heading = "Image")]
|
||||
no_update_check: bool,
|
||||
|
||||
/// Args forwarded verbatim to the in-sandbox agent command. Use `--` if
|
||||
/// Args passed verbatim to the agent; use -- before any agent flags.
|
||||
///
|
||||
/// Forwarded verbatim to the in-sandbox agent command. Use `--` if
|
||||
/// any argument starts with `-` to keep clap from claiming it.
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true, num_args = 0..)]
|
||||
agent_args: Vec<String>,
|
||||
|
|
|
|||
|
|
@ -21,12 +21,13 @@ pub struct Args {
|
|||
#[arg(long)]
|
||||
no_verify: bool,
|
||||
|
||||
/// Override the image reference. Defaults to
|
||||
/// `ghcr.io/wirenboard/agent-vm-template:latest`. Source-checkout users
|
||||
/// who built a local image can point at it
|
||||
/// Override the image reference.
|
||||
///
|
||||
/// Defaults to `ghcr.io/wirenboard/agent-vm-template:latest`.
|
||||
/// Source-checkout users who built a local image can point at it
|
||||
/// (`--image localhost:5000/agent-vm-template:latest`) — agent-vm
|
||||
/// detects local registries and uses plain HTTP.
|
||||
#[arg(long, env = "AGENT_VM_IMAGE_TAG")]
|
||||
#[arg(long, env = "AGENT_VM_IMAGE_TAG", value_name = "REF")]
|
||||
image: Option<String>,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue