The microvm binary has twenty-eight subcommands, declared as one clap Subcommand enum in microvms-cli/src/cli.rs and built from microvms-cli/Cargo.toml:20-22.
Global flags
Section titled “Global flags”These three are global = true, so they parse on either side of the subcommand. microvm --json ls and microvm ls --json are the same invocation. microvms-cli/src/cli.rs:63-80.
Flags:
--json— emit the typed JSON envelope on stdout instead of human output; wins over every other format, including an interactive terminal.microvms-cli/src/cli.rs:67-72.--dense— token-lean output, for a consumer paying per token: tab-separated alone, compact one-line JSON with--json.microvms-cli/src/cli.rs:74-76.--quiet— suppress progress on stderr; warnings still print.microvms-cli/src/cli.rs:78-80.
The output format depends only on the two flags and on whether stdout is a terminal. --json is checked first, then --dense; after that, a terminal gets a ratatui surface and a pipe gets plain text. microvms-cli/src/envelope.rs:298-305.
The manifest publishes the three as globalFlags (#131), in the same parameter shape as a command’s own, read off the root command’s global = true arguments without building the tree — building would propagate each global into every subcommand’s parameter list. microvms-cli/src/manifest.rs:91-95. The generated Reference overview renders that array as its “Global flags” table.
Shared flag groups
Section titled “Shared flag groups”Four flattened Args structs supply the flags that repeat across commands, so a relationship like the region conflict is declared once rather than per command. RegionFlags, AttachFlags and InfraFlags are described here; ConfigFlags (--config / --no-config, flattened into run and doctor) is described under run. microvms-cli/src/cli.rs:460-461, microvms-cli/src/cli.rs:498-499, microvms-cli/src/cli.rs:544-545, microvms-cli/src/cli.rs:595-596.
RegionFlags — flattened into every command that talks to AWS. microvms-cli/src/cli.rs:460-472.
Flags:
--region <REGION>— AWS region; defaults to$AWS_REGION, then$AWS_DEFAULT_REGION, thenus-east-1. Closed set:us-east-1,us-east-2,us-west-2,eu-west-1,ap-northeast-1.microvms-cli/src/cli.rs:462-464, domain atmicrovms-cli/src/cli.rs:412-424.--unlisted-region <NAME>— use a region this client has not seen carry MicroVMs; conflicts with--region. An unsupported region answersAccessDeniedExceptionwith a null message, which looks like an IAM denial, so the caller loses the real diagnostic.microvms-cli/src/cli.rs:466-471.
AttachFlags — the identifiers that address a VM this invocation did not launch: the explicit triple, or a registered name standing in for it. Carried by exec, health, ack, stdin, cp, and sync. microvms-cli/src/cli.rs, AttachFlags.
Flags:
--endpoint <ENDPOINT>— the VM’s endpoint, as reported byrun. Required unless--nameis given.--agent-token <AGENT_TOKEN>— the agent token delivered to the VM at launch. Required unless--nameis given.--microvm-id <MICROVM_ID>— the MicroVM id, needed to mint the endpoint proxy token. Required unless--nameis given.--name <NAME>— the namerun --keep --vm-nameregistered, standing in for the whole triple. Resolved through the local name registry with zero AWS calls: the record carries the endpoint, the agent token, the MicroVM id, and the launch region (used as the region default when no--regionflag is given). Conflicts with the explicit triple. A name this state directory never registered fails locally withERR_PRECONDITION.--port <PORT>— the daemon’s port inside the guest.--state-dir <STATE_DIR>— where the local state lives: the name registry, and exec’s per-VM history. Defaults to$MICROVM_STATE_DIRor~/.microvm/runs.
InfraFlags — the three account-specific values the AWS commands need. Carried by run, build, and doctor. microvms-cli/src/cli.rs:594-608.
Flags:
--bucket <BUCKET>— S3 bucket for the build artifact; defaults to$MICROVM_BUCKET.microvms-cli/src/cli.rs:597-599.--build-role-arn <BUILD_ROLE_ARN>— build role ARN; defaults to$MICROVM_BUILD_ROLE_ARN.microvms-cli/src/cli.rs:601-603.--execution-role-arn <EXECUTION_ROLE_ARN>— execution role ARN; defaults to$MICROVM_EXECUTION_ROLE_ARN.microvms-cli/src/cli.rs:605-607.
microvm run [OPTIONS] [BINARY]Builds an image, launches a VM, runs a command, reports the cost, and tears the VM down. Teardown is the default so that a closed laptop does not leave a billable VM.
microvms-cli/src/commands/lifecycle.rs:119
Flags:
[BINARY_OR_DIR]— the aarch64 agentd binary to bake in as the image CMD (ignored when--imagenames an image to launch instead), or a directory to sync. The two readings cannot collide: a path is a directory or it is not. See “Sync mode” below.microvms-cli/src/cli.rs,RunArgs::binary.--image <IDENTIFIER>— launch this existing image instead of building one. Takes an ARN or a bare image name: a name is resolved to its ARN through the account’s image listing (exact match, every page read) before the launch, with a progress line naming the resolved ARN. An identifier already shaped like an ARN passes through with zero extra calls. The envelope’simageNamereports the launched image’s own name (the ARN’s last colon segment), never the per-invocation default a build would have used. A name that resolves to nothing fails locally withERR_PRECONDITIONnaming the name and suggestingmicrovm build— the service’s own answer to a bare name is HTTP 400 “Malformed ARN”, which says nothing about names.microvms-cli/src/commands/lifecycle.rs:965-979, resolution inmicrovms-core/src/control/image.rs:411-475.--image-version <VERSION>— launch this exact image version instead of the image’s latest active one. Omitted takes whateverlatestActiveImageVersionis at the moment the call lands, which is right for the ordinary case and wrong for the two that matter: a canary wants the version it just built rather than whatever became latest while it was starting, and a rollback wants the known-good version, which “latest” cannot name once a bad version is the latest one. A version the control plane has setINACTIVErefuses to launch when named here — measured, the answer is HTTP 404No active version found for MicroVM image <arn> and version <v>, which is what makes a retire real rather than advisory. Free text rather than a closed set, because a version’s legal values are an account fact onlyListManagedMicrovmImageVersionscan answer; the constraint that is knowable is checked before any call, so an empty version, one over 2048 characters, or one containing whitespace anywhere fails locally withERR_INVALID_ARGand the reason. A version pasted from a terminal carries a trailing newline, which is that case.microvms-core/src/control/mod.rs’srequire_valid_version, wiring inmicrovms-cli/src/commands/lifecycle.rs.--artifact-uri <S3_URI>— where the build artifact already is;microvms-corebuilds the artifact bytes and takes the URI but does not upload.microvms-cli/src/cli.rs:671-672.--exec <COMMAND>— a shell command to run in the VM. When it is omitted, the run only launches and tears down, which is how you check that an image boots.microvms-cli/src/cli.rs:677-678.--name <NAME>— image name; defaults to a per-invocation name, because reusing a name can trigger aclientTokenreplay that wedges the image.microvms-cli/src/cli.rs:682-683.--vm-name <NAME>— register a local name for the kept VM, so later commands can say--name <NAME>(attached commands) or use the name as the positional (suspend, resume, terminate, history) instead of pasting identifiers. Requires--keep. The name is a purely local fact in the state directory’s registry (<state-dir>/names/<NAME>.json, written owner-only because the record carries the agent token), costs zero AWS calls, and is released when a terminate is accepted. Names take ASCII letters, digits,-and_, at most 128 bytes, and never a MicroVM id prefix (microvm-is the service’s real prefix, measured live;mvm-is the test fixtures’) — that exclusion is what lets every identifier-taking command tell a name from a MicroVM id. A name registered to a live VM is refused locally withERR_NAME_TAKEN(exit 14) before any billable call.microvms-cli/src/cli.rs,RunArgs::vm_name; registry inmicrovms-cli/src/ledger.rs,Names.--memory <MEMORY>— baseline MiB, selecting a documented size class; default2048. Closed set:512,1024,2048,4096,8192.microvms-cli/src/cli.rs:690-691.--dockerfile <DOCKERFILE>— a Dockerfile to use instead of the library’s default; itsFROMmust match the base.microvms-cli/src/cli.rs:694-695.--log-group <GROUP>/--log-stream <STREAM>— build-log destination, applied when this invocation builds; same semantics asbuild’s flags (the stream is a prefix the client suffixes with/<16 hex>per build). Both are alsomicrovm.tomlkeys (log-group,log-stream), with the flag winning per knob; a stream with no group from either layer is refused locally.microvms-cli/src/cli.rs, merge inmicrovms-cli/src/commands/lifecycle.rs’smerge_config.--repair-identity— request identity repair and additional OS capabilities. Inspect health foridentity_degraded; requested capabilities do not guarantee every repair succeeds.--egress— request the managedINTERNET_EGRESSconnector. Omission does not disable internet access. Also theegressconfig key. See Networking.--egress-network-connector <ARN>— attach an existing custom VPC connector; repeatable. Conflicts with--egress. Config key:egress-network-connectors; explicit flags replace the configured list. Internet isolation requires a VPC without an IGW or NAT gateway and no alternative internet path.--deny-egress— set proxy variables pointing to an unreachable local proxy. Reportsbest-effort; workloads can bypass it. This is not network enforcement and conflicts with--egress. Also thedeny-egressconfig key.--launch-env <KEY=VALUE>— set one launch-environment variable for every exec in the VM; repeatable. Delivered in the samerunHookPayloadas the agent token, at launch, so it never touches the shared image snapshot and never touches disk. The daemon applies it as the base environment of every exec, withexec --envon the same key winning. Same parser asexec --env, so the first=splits, an empty VALUE is legal, and a missing=or empty KEY is refused at parse time. The whole payload shares a 4096-byte ceiling with the token, checked locally before the launch: an over-budget env fails with the byte count and the env’s share of it, rather than as an AWSValidationExceptionafter the call. The total must fit after serialization; large credential sets belong onmicrovm cpafter bootstrap or on a role the workload assumes.microvms-cli/src/cli.rs, wiring inmicrovms-cli/src/commands/lifecycle.rs.--keep— retain the VM and image; the VM continues to incur charges until stopped, suspended, or terminated according to its lifecycle.--keepis also the only case in which the envelope’sagentTokencarries a value: a run that tears its VM down emits the key asnull, because stdout outlives the process and the token would name a VM that no longer exists (#161).microvms-cli/src/cli.rs:800-801,microvms-cli/src/render.rs,RunOutcome::to_data.--timeout <TIMEOUT>— how long to wait for the exec, in seconds; default300.microvms-cli/src/cli.rs:838-839.--max-idle-sec <MAX_IDLE_SEC>— suspend the VM after this much inbound-traffic idleness; default600.microvms-cli/src/cli.rs:842-843.--suspended-sec <SUSPENDED_SEC>— terminate the VM after this long suspended; a resume attempted after this window fails because the VM no longer exists. Default600.microvms-cli/src/cli.rs:846-847.--auto-resume— let the platform resume a suspended VM on an incoming request, instead of requiring an explicitmicrovm resume; omitted by default. SetsidlePolicy.autoResumeEnabledon the launch. Also amicrovm.tomlkey (auto-resume).microvms-cli/src/cli.rs, merge inmerge_config.--max-duration-sec <MAX_DURATION_SEC>— hard ceiling on the VM’s life; refused above 28800 before any call. Default3600.microvms-cli/src/cli.rs:854-855.--port <PORT>— the daemon’s port inside the guest.microvms-cli/src/cli.rs:858-859.--state-dir <STATE_DIR>— where the run ledger is written; defaults to$MICROVM_STATE_DIRor~/.microvm/runs.microvms-cli/src/cli.rs:862-863.--config <PATH>— read this project config file instead of./microvm.toml. Naming a file that does not exist is refused withERR_CONFIG: a typed path that is wrong must not silently become “no config”. Conflicts with--no-config.microvms-cli/src/cli.rs,ConfigFlags.--no-config— ignore anymicrovm.toml, even a malformed one; flags and built-in defaults apply.microvms-cli/src/cli.rs,ConfigFlags.- Plus
RegionFlagsandInfraFlags.microvms-cli/src/cli.rs:875-879.
Precedence, when a config file is in play: a typed flag beats the file, and the file beats the built-in default. “Typed” is read off the parse (clap’s value_source), not off the value, so --memory 2048 overrides a file that says 4096 even though 2048 is also the default. The merge happens in exactly one place (merge_config in microvms-cli/src/commands/lifecycle.rs, per-knob precedence in config::pick) and its outcome is reported in the success envelope’s resolvedConfig key — each knob’s winning value and the source it came from (flag, config, env, or default; env appears only on the region, the one knob whose chain continues past the file into $AWS_REGION/$AWS_DEFAULT_REGION) — so a caller never has to re-derive which source won. egress and denyEgress are two of those knobs; the conservative assessment of internet isolation is a separate top-level envelope key, egressPosture (open | unsealed | best-effort | sealed), because a request flag is not an answer — see docs/TRUST.md, Egress. One deliberate pairing rule: a typed BINARY positional with no typed --image suppresses the file’s image, because run builds exactly when the merged image is absent, and a file that silently won that pair would run the caller’s tests against a stale pinned image. A positional that names a directory does not suppress it — sync mode launches, so the file’s pinned image is exactly what run . wants. See “Project config” below for the file itself.
Sync mode (issue #72): a positional that names a directory switches run into a pack-run-collect round trip — microvm run . --image ci-image --exec "make test" is the headline spelling. The tree is packed locally (deterministic member order; .git, target, node_modules, and .venv skipped whole; sockets, fifos, and devices skipped individually; symlinks preserved as links, never followed), uploaded to /workspace in the guest, and the exec runs with /workspace as its working directory. The pack is budgeted against the daemon’s own caps — 512 MiB of file bytes, 100 000 members — during the walk, so an over-budget tree is ERR_SYNC naming the offending subtree before any archive bytes are allocated or any AWS call is made.
Afterwards — including when the exec exited non-zero, because a failing run’s report is the artifact CI most wants — the workdir comes back and the members matching the config file’s artifacts globs are written into the directory. Only glob-matched regular-file members land, and never under .git: symlinks, hardlinks, specials, unmatched members, anything attempting traversal outside the directory, and any .git path are skipped, because the returned archive is the VM’s word, the VM is where untrusted work runs, and a workload-written .git/hooks/pre-commit would execute on the host at the caller’s next commit. With no artifacts globs configured the workdir is not downloaded at all, and the sync key says so in a note. A download failure does not fail the run — by then the exec’s result is already in hand, and discarding a green test run because make clean removed the workdir would be the report lying — the error lands in sync.error and the exec’s own exit code stands.
Sync mode launches an existing image (--image, or image pinned in microvm.toml); a directory with nothing supplying an image is refused with ERR_PRECONDITION before any call. A local pack or extraction failure is ERR_SYNC (exit 16). The envelope’s sync key reports the workdir, uploaded bytes and member count, and each artifact brought back with its size — or error / note for the two no-artifact shapes. microvms-cli/src/sync.rs; wiring in microvms-cli/src/commands/lifecycle.rs.
microvm build [OPTIONS] [BINARY]Builds a MicroVM image and waits for it to be usable. Nothing is torn down afterward. The image is the durable artifact, and because its snapshot has a one-week minimum retention, deleting it early saves nothing.
microvms-cli/src/commands/lifecycle.rs:1296
Flags:
[BINARY]— the aarch64 agentd binary to bake in as the image CMD. Omitted, the CLI provisions its own version’s release asset and caches it under the state directory;$MICROVM_AGENTDnames a binary without touching the command line.microvms-cli/src/cli.rs:918-919.--artifact-uri <S3_URI>— where the build artifact already is, as ans3://URI.microvms-cli/src/cli.rs:928-929.--name <NAME>— image name; defaults to a per-invocation name.microvms-cli/src/cli.rs:932-933.--memory <MEMORY>— baseline MiB, selecting a documented size class; default2048.microvms-cli/src/cli.rs:936-937.--dockerfile <DOCKERFILE>— a Dockerfile to use instead of the library’s default.microvms-cli/src/cli.rs:940-941.--project <DIR>— bake an environment layer from the directory’s dependency files (#74). Exactly one ecosystem’s manifest+lockfile pair must be present —pyproject.toml+uv.lock,package.json+package-lock.json, orCargo.toml+Cargo.lock— and only that pair enters the shared image snapshot, under names fixed by the ecosystem (nothing else in the directory can enter, which is what keeps a.envout of a snapshot every VM shares). The derived Dockerfile copies the pair into the working directory (/projectwhen none is named) and installs from the lockfile with the lockfile-faithful spelling:uv sync --locked,npm ci, orcargo fetch --locked, each refusing a lockfile that disagrees with its manifest rather than quietly re-resolving. Launches from the image then start with dependencies already installed — the 31–48% env-init share of launch timedocs/STRATEGY.mdmeasures is paid once at build. A caller--dockerfilethat never mentions the lockfile is refused before the upload, because it would bake no layer while building cleanly. Missing lockfile, missing manifest, and two-ecosystem directories are each refused naming their remedy. Measured launch delta, and why in-guest code should call/project/.venv/bin/pythondirectly rather thanuv(an exec sees noPATH):docs/PLATFORM.md, “A baked environment layer removes the guest’s env init”.microvms-cli/src/commands/lifecycle.rs’sread_project_files, the entries inmicrovms-core/src/control/artifact.rs.--base-image-version <VERSION>— pin the managed base image to one version instead of taking the service’s default. Without this a build floats: the managed base’s version list is not static —al2023-1carried one version in June and two by July — so two builds of identical inputs weeks apart can sit on different bases and neither recorded which. The build succeeds either way; the difference shows up in the guest. The legal values come fromListManagedMicrovmImageVersions, whichmicrovm doctorprints as itsbase-image-versionscheck, and they are bare integers for a managed base (0,1) where a custom image’s versions are1.0. A bogus pin is refused by the service before anything is created (HTTP 400No managed MicroVM Image with arn <base-arn> and version 999 is available), but it costs the artifact upload first, so theVersionshape’s own constraints — non-empty, at most 2048 characters, no whitespace anywhere — are checked locally before the upload. Note that the value comes back normalised: a build pinned with1reads backbaseImageVersion: "1.0"fromGetMicrovmImageVersion, so the echoed value cannot be fed back into a request.microvms-cli/src/commands/lifecycle.rs’sBuildSpec, guard inmicrovms-core/src/control/image.rs’screate_image.--log-group <GROUP>— CloudWatch log group for the build’s logs, instead of the service-created/aws/lambda-microvms/<image-name>. Letters, digits, and_ - / . #only, up to 512 characters, validated locally before the artifact upload. The build role must be able to write to whatever this names (logs:CreateLogGroup/CreateLogStream/PutLogEvents); a group outside a granted prefix builds with no logs at all, the same silent outcome as the wrong-prefix policy indocs/PLATFORM.md.--log-stream <STREAM>— log stream name prefix inside--log-group; requires it. The platform’slogging.logStreammember is an exact stream name — prefixes are unsupported — and one image build is three VMs writing three streams (docker build, Graviton 3 snapshot, Graviton 4 snapshot), so a fixed configured name would collapse every build’s logs into one indistinguishable stream. The client therefore appends/<16 hex>of fresh randomness per build attempt, and the envelope reports the resolved exact name aslogStream— the only place it exists. No:or*(the shape’s pattern is[^:*]*); up to 495 characters (the platform’s 512 minus the suffix’s 17). Seedocs/PLATFORM.md, “An image build is three VMs and three log streams”.--repair-identity— request identity repair and additional OS capabilities. Inspect health foridentity_degraded; requested capabilities do not guarantee every repair succeeds.--reuse— reuse an existing image whose build inputs match, instead of building. Computes a sha256 over the build inputs (the daemon binary’s bytes, the Dockerfile, and — with--project— the manifest and lockfile, names and bytes both), derives the image name<name>-<hash12>— where the prefix is--nameor the stable stemmicrovm-cli— and checks the listing for that exact name. A hit skips the build entirely and reports the existing image withreused: truein the envelope; a miss builds under the derived name, so the next invocation with the same inputs hits. The hash is in the name because recreating an image under a previously-used fixed name can serve a stale snapshot (measured; the same hazard class as the clientToken replay indocs/PLATFORM.md) — content-keying gives both properties at once: unchanged inputs reuse their image, changed inputs get a fresh name and a fresh build. With--projectthat is #74’s promise: two projects with identical dependency files share a layer, and a lockfile edit builds a fresh one.--memoryis not part of the identity, so a reused image keeps the size class it was created with; the envelope’ssizeis the requested class and the text says so.microvms-cli/src/commands/lifecycle.rs:1370-1376, the hash atmicrovms-core/src/control/artifact.rs’sartifact_content_hash.--port <PORT>— the daemon’s port inside the guest.microvms-cli/src/cli.rs:1010-1011.- Plus
RegionFlagsandInfraFlags.microvms-cli/src/cli.rs:1013-1017.
The success envelope always carries reused (false for a plain build) and logStream (null when no --log-stream was configured; a reused image also reports null, because no build ran and no stream was resolved), so a consumer never guards for either key. microvms-cli/src/commands/mod.rs.
agent-up
Section titled “agent-up”microvm agent-up [OPTIONS] --vm-name <NAME> [BINARY]Brings up a VM with a coding agent in it: image, launch, model credentials, non-root user. The L3 helper (docs/AGENT-VMS.md) composes build --reuse, run --keep --egress --vm-name, a Bedrock bearer token minted from the caller’s own AWS credentials, and the file uploads and chown the coding-agents example performed by hand. Two paths, decided by one local registry read: a name this state directory has not registered means build (or reuse), launch, provision, register; a name it has registered means attach, mint a fresh token, re-install the same files, and report vmReused: true, with no build and no launch. That refresh path is how a 12-hour token is renewed on a long-lived VM. Teardown is microvm terminate <NAME>.
microvms-cli/src/commands/agent.rs
Flags:
[BINARY]: the aarch64 agentd binary to bake in. Omitted provisions this CLI’s own release asset, the same chainrunandbuilduse; the envelope’sagentdkey reports the path and source.microvms-cli/src/cli.rs,AgentUpArgs::binary.--vm-name <NAME>: the local name to register for the VM, and the handle every later command uses. Required, because an agent VM is kept by definition. Same grammar asrun --vm-name. A name already registered to a live VM is not a collision here: it selects the refresh path. A name registered to a torn record (empty MicroVM id, a process died mid-register) is refused withERR_NAME_TAKENand the record’s path.microvms-cli/src/cli.rs,AgentUpArgs::vm_name.--agent <AGENT>: which agent to install. Closed set:claude-code,codex. Repeatable; omitted meansclaude-code; a repeated value collapses to one row. On the refresh path, omitted keeps the agents and models the guest marker names, and a typed--agentis the caller changing them.microvms-cli/src/cli.rs,AgentArg.--claude-model <MODEL_ID>: the Bedrock inference-profile id Claude Code uses; defaultglobal.anthropic.claude-opus-5, from the profile table.microvms-core/src/agents/profile.rs,CLAUDE_CODE.--codex-model <MODEL_ID>: the Bedrock model id Codex uses; defaultglobal.openai.gpt-5.6-sol, from the profile table.microvms-core/src/agents/profile.rs,CODEX.--claude-version <VERSION>/--codex-version <VERSION>: pin the agent’s npm package to this version (@anthropic-ai/claude-code@<V>,@openai/codex@<V>). Unpinned, the image carries the registry’s latest at build time. A pin changes the Dockerfile text and therefore the image’s reuse hash.microvms-cli/src/cli.rs,AgentUpArgs::claude_version.--project <DIR>: a local directory to upload into/workspaceafter launch, packed the wayrun <DIR>packs: same skip list (.git,target,node_modules,.venv), same budgets, packed before any AWS call so an unreadable tree costs nothing. The upload lands before the install’schown, so the agent owns the tree. Bring results back withmicrovm cp --tar vm:/workspace <LOCAL> --name <NAME>. Also honored on the refresh path.microvms-cli/src/cli.rs,AgentUpArgs::project.--memory <MEMORY>: baseline MiB; default1024, notrun’s2048: a 4 GiB always-present ceiling at half the floor cost, which fits peaky agent sessions. Closed set as forrun.microvms-cli/src/cli.rs,AgentUpArgs::memory;microvms-core/src/agents/mod.rs,DEFAULT_SIZE.--token-ttl-hours <HOURS>: how long the Bedrock bearer token lives; default and ceiling12. Core refuses a lifetime outside(0, 12 h]withERR_INVALID_ARG; the service additionally caps validity at the signing credentials’ own expiry.microvms-core/src/agents/bedrock.rs,MAX_LIFETIME.--max-idle-sec <MAX_IDLE_SEC>: suspend the VM after this much inbound-traffic idleness; default600.--suspended-sec <SUSPENDED_SEC>: terminate the VM after this long suspended; default600.--auto-resume: let the platform resume a suspended VM on an incoming request; omitted by default.--max-duration-sec <MAX_DURATION_SEC>: hard ceiling on the VM’s life; refused above 28800 before any call. Default3600.--port <PORT>: the daemon’s port inside the guest.--state-dir <STATE_DIR>: where the run ledger and name registry live; defaults to$MICROVM_STATE_DIRor~/.microvm/runs.- Plus
RegionFlagsandInfraFlags. The fresh path requires the bucket, the build role, and the execution role (ERR_PRECONDITIONnaming the missing one); the refresh path makes no control-plane call before the attach, and takes the region from the flag when typed, else from the name record.
Egress is always requested, because neither agent reaches Bedrock without it. The image is named agent-vm-<agents>-<hash12>, the hash over the same inputs build --reuse covers, so an unchanged daemon and agent set reuse their image and a version pin builds a fresh one. Provisioning writes /workspace/.agent-env (mode 0600; HOME, PATH, AWS_REGION, and each agent’s credential and model lines), /workspace/.codex/config.toml when Codex is present, and the marker /workspace/.agent-vm.json (mode 0644) naming the installed agents and models, then runs one root chown -R 1000:1000 /workspace. The token travels only as a file over the authenticated channel; it is never an argv element and never in the launch payload. microvms-core/src/agents/mod.rs, provisioning_files and install_access.
A failure after the launch and before the registration (token mint, project upload, credential install, or an interrupt) tears the VM down, because a VM with no name and no credentials is one nobody can use; the failure envelope carries microvmId, imageIdentifier, leaked, and terminateAccepted. The image stays, as the durable artifact. The name is registered last, over a VM every step succeeded on; a registry write failure is ERR_PRECONDITION with the identifier triple in data. No new exit row: every failure maps onto the existing table.
The success envelope’s type is microvm.agent; its keys are vmName, microvmId, endpoint, agentToken, imageIdentifier, imageName, imageReused, vmReused, agents ([{agent, model, cliVersion, headlessCommand}]), credentialExpiresAt (epoch seconds), workdir, project ({workdir, uploadedBytes, uploadedMembers} or null), and agentd. On the refresh path the two image keys and imageReused are null, because no image was built or looked up. agents[].headlessCommand is the exact template agent-prompt runs with <TASK> where the quoted task goes, so a caller who wants exec --stream --user 1000 --group 1000 over it can spell the command without knowing the profile. microvms-cli/src/commands/mod.rs, the agent-up row.
agent-prompt
Section titled “agent-prompt”microvm agent-prompt [OPTIONS] <TASK>Hands a coding agent in a running agent VM one task, headless, as the non-root user. Runs the agent’s headless command (claude -p <TASK> --allowedTools Bash,Read,Edit,Write,Grep,Glob or codex exec --skip-git-repo-check -s workspace-write <TASK>) as uid 1000, gid 1000, in /workspace, with . /workspace/.agent-env && in front so the credentials agent-up installed are the environment; the request’s own env map is empty, so the token never rides in a request body. By default it waits and acks, exactly as exec does; --detach starts and returns the exec id.
microvms-cli/src/commands/agent.rs
Flags:
<TASK>: the task, as prose. Passed to the headless command single-quoted forsh. Required. An empty or whitespace-only task is refused locally withERR_INVALID_ARGbefore the attach, so it costs zero calls.microvms-cli/src/cli.rs,AgentPromptArgs::task.--agent <AGENT>: which installed agent to prompt; closed setclaude-code,codex. Omitted reads the guest marker/workspace/.agent-vm.jsonand takes the one agent it names; a marker naming two agents is refused withERR_PRECONDITIONlisting both and suggesting--agent. A typed--agentstill reads the marker when it can, so the prompt uses the model the VM was provisioned with; a VM with no marker and a typed agent runs the profile’s defaults; a VM with no marker and no flag isERR_PRECONDITIONnamingagent-up.microvms-cli/src/commands/agent.rs,choose_spec;microvms-core/src/agents/mod.rs,installed_agents.--timeout <TIMEOUT>: how long this process waits for the agent, in seconds; default900, because agent tasks run minutes, not seconds. A client-side deadline: the exec on the daemon’s side is started with no wall-clock budget of its own.microvms-cli/src/cli.rs,AgentPromptArgs::timeout.--detach: start the agent and return immediately, without waiting and without acking; the envelope reportsphase: runningand anullexit code.microvm exec --poll <ID> --name <NAME>reads it back andmicrovm ackreleases it, as forexec --detach.microvms-cli/src/cli.rs,AgentPromptArgs::detach.--exec-id <ID>: use this exec id instead of a fresh one, making a retry idempotent.microvms-cli/src/cli.rs,AgentPromptArgs::exec_id.- Plus
AttachFlagsandRegionFlags;--name <NAME>is the usual spelling, the nameagent-up --vm-nameregistered.
The success envelope’s type is microvm.agent.prompt, under its own discriminant because a consumer that learned microvm.exec is reading a command it chose and this one ran a template it did not. Its keys are exec’s plus which agent ran: execId, agent, model, phase, exitCode, stdout, stderr, truncated. A non-zero agent exit earns ERR_EXEC_FAILED on a success envelope, as exec does. microvms-cli/src/commands/mod.rs, the agent-prompt row; microvms-cli/src/commands/attached.rs, render_exec_as.
microvm exec [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID> [COMMAND]Runs one command in a MicroVM that is already running. The single subcommand covers four uses. It can start a command and wait for it, start one and stream its output (--stream), start one and feed its stdin (--stdin), or read an existing exec (--poll).
microvms-cli/src/commands/attached.rs:178
Flags:
[COMMAND]— a shell command to run in the VM; omitted only with--poll.microvms-cli/src/cli.rs:1023-1024.--timeout <TIMEOUT>— how long to wait for the command, in seconds; default300.microvms-cli/src/cli.rs:1027-1028.--cwd <CWD>— working directory. When omitted, the command inherits the image WORKDIR, which is not the same as passing/.microvms-cli/src/cli.rs:1034-1035.--env <KEY=VALUE>— set one environment variable for the command; repeatable. These flags are the child’s whole environment: the daemon starts every exec from an empty one and applies exactly this map, so there is no inherited PATH to append to. Split at the first=, so a value may itself contain=; an empty VALUE is legal (--env EMPTY=), and a missing=or an empty KEY is refused at parse time.microvms-cli/src/cli.rs:1050-1051.--user <UID>— numeric uid to run the command as; omitted runs as the daemon’s own user. Numeric because that is the protocol’s type and the daemon’s mechanism (Command::uid, between fork and exec) — a name would need an/etc/passwdlookup inside a guest whose base image may not have one.microvms-cli/src/cli.rs:1060-1061.--group <GID>— numeric gid to run the command as; omitted keeps the daemon’s own group.microvms-cli/src/cli.rs:1064-1065.--exec-id <ID>— use this exec id instead of a fresh one, making a retry idempotent; the daemon returns success for a known id without spawning a second child.microvms-cli/src/cli.rs:1084-1085.--poll <ID>— read an existing exec’s status and output instead of starting anything; read-only server-side, does not ack. Conflicts with--exec-id,--stream,--stdin,--cwd,--detach,--env,--user,--group.microvms-cli/src/cli.rs:1093-1094.--detach— start the command and return immediately, without waiting and without acking; prints the exec id andphase: running. Conflicts with--streamand--stdin.microvms-cli/src/cli.rs:1113-1114.--stream— stream output as it arrives rather than waiting for the whole thing; under--jsonor into a pipe this writes NDJSON.microvms-cli/src/cli.rs:1123-1124.--from-offset <BYTES>— resume a stream at this byte offset; requires--stream.microvms-cli/src/cli.rs:1131-1132.--stdin— give the command a stdin pipe, feed it this process’s stdin, then close it.microvms-cli/src/cli.rs:1139-1140.--reap— signal the command’s whole process group once its own child exits, so nothing it backgrounded outlives it (reap_group_on_exit: trueon the wire). Off by default, and the default is a contract: a backgrounded grandchild that inherited the output pipe keeps running and keeps writing. With the flag,psafterwards shows the group empty and the exec’swritersMayBeAlivereads false because the linger saw EOF. Conflicts with--poll.microvms-cli/src/cli.rs:1151-1152.--kill-on-timeout— onERR_TIMEOUT, send onePOST /v1/exec/{id}/killand put the daemon’s verdict in the failure envelope’sdata.killed. A plain--timeoutis a client-side deadline that abandons the exec and leaves it running in the guest; this turns it into a stop. The exit code staysERR_TIMEOUT, because the deadline is still what ended the wait. Conflicts with--poll,--detach, and--stream.microvms-cli/src/cli.rs:1162-1163.- Plus
AttachFlagsandRegionFlags.microvms-cli/src/cli.rs:1165-1169.
A timeout is not a stop. ERR_TIMEOUT’s suggestions say so in as many words and name microvm kill <exec-id> as the remedy, beside the older fact that the exec and its output are untouched and re-pollable. microvms-cli/src/exit.rs:402.
health
Section titled “health”microvm health [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID>Asks a running MicroVM’s daemon whether it is up and what its identity repair did. This is the one command that reports identityDegraded and diskUnderPressure, and either flag is a reason to drain the VM rather than keep scheduling onto it.
It also reports busy and execs, which is what makes this the command an orchestrator loops on to hold a long-running VM alive. The platform measures idleness by inbound traffic through the endpoint proxy, and that proxy terminates outside the guest, so a request sent from inside the VM cannot reset the idle timer — only a poll from outside counts, and this poll is that traffic. busy is what makes the loop informed rather than unconditional: it is true only while some exec is actually running, so an exec that exited and is waiting to be acked reads false. execs counts every registered entry in any phase, so busy: false with a non-zero count is a VM holding unacked output somebody still has to collect before terminating it. See docs/PROTOCOL.md, “Idle policy, and why liveness is a field rather than a route”.
And it reports hooks, the daemon’s own record of every lifecycle-hook invocation it observed — [{hook, firedAt}], oldest first, with firedAt in epoch seconds on the daemon’s clock — beside hooksDropped, how many invocations the daemon’s capped log discarded. The platform writes no CloudWatch logs for the validate hook, so this is the only place “did my validate hook even run?” is answerable; validate and ready fire in the snapshot VM before the snapshot is taken, so a launched VM commonly reports them from the memory image it restored from. Each observation the poll returns is also appended to the VM’s local history as a hookObserved event, deduplicated on the (hook, firedAt) pair, so re-polling appends nothing and the record survives the VM. One caveat travels with the field: the daemon’s hook routes are unauthenticated and reachable over loopback from inside the guest, so a hostile workload can forge additional observations by posting the hook paths itself — it can never remove or alter real ones, and the capped log keeps the earliest entries, which are the platform’s. Both fields are empty/zero against a daemon that predates them.
microvms-cli/src/commands/attached.rs:635
Flags:
AttachFlagsandRegionFlagsonly; this command has no arguments of its own.microvms-cli/src/cli.rs:1172-1179.
microvm ack [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID> <EXEC_ID>Releases a finished exec’s buffered output, which starts its collection clock. A second ack returns a 409 because the first one already released the output.
microvms-cli/src/commands/attached.rs:811
Flags:
<EXEC_ID>— the exec whose output to release. Required.microvms-cli/src/cli.rs:1346-1347.- Plus
AttachFlagsandRegionFlags.microvms-cli/src/cli.rs:1349-1353.
microvm kill [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID> <EXEC_ID>Stops a running exec: SIGTERM to its whole process group, SIGKILL after the daemon’s grace (POST /v1/exec/{id}/kill). This is the stop button exec --timeout is not — a client-side timeout abandons an exec and leaves it running in the guest (issue #156). The VM is addressed the way every attached command addresses it, by --name or the identifier triple, and the exec by its id, which exec --detach, exec --exec-id, and ps all print.
The envelope carries the daemon’s own verdict. killed: false with exit 0 means the process group had already exited, which is the outcome a kill was asking for, so microvm kill x-1 && collect runs its second half either way. An unknown exec id is the daemon’s 404, arriving as ERR_PROTOCOL with data.kind: NotFound.
microvms-cli/src/commands/attached.rs:852
Flags:
<EXEC_ID>— the exec whose process group to signal. Required.microvms-cli/src/cli.rs:1359-1360.- Plus
AttachFlagsandRegionFlags.
microvm ps [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID>Lists every exec’s process group and its live pids (GET /v1/procs). The daemon reads /proc itself, so this works against the al2023 base image, which ships no ps (issue #157); nothing needs installing in the image for this command. The row worth reading is childExited: true with a non-empty pids: a command that finished while something it backgrounded did not, which health’s busy cannot show because busy is about the exec’s own child. The execId on that row is what kill takes.
data.procs is one object per registered exec in any phase: {execId, pgid, startedAt, childExited, reap, pids}. pgid is null (never absent) when the daemon captured none; startedAt is epoch seconds on the daemon’s clock; reap echoes exec --reap. Zombies are not live and are not listed. --dense prints one TSV row per group — exec id, pgid, childExited, pid count, startedAt — exec id first, so cut -f1 feeds kill.
microvms-cli/src/commands/attached.rs:904
Flags:
AttachFlagsandRegionFlagsonly; this command has no arguments of its own.microvms-cli/src/cli.rs:1369-1376.
microvm stdin [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID> <EXEC_ID>Writes to a running exec’s stdin and optionally closes it. It only works on an exec started with exec --stdin, and it is the only way to close the pipe.
microvms-cli/src/commands/attached.rs:995
Flags:
<EXEC_ID>— the exec to write to; must have been started withexec --stdin. Required.microvms-cli/src/cli.rs:1381-1382.--data <DATA>— what to write;-reads this process’s stdin, and omitting the flag writes nothing. The value is raw bytes either way, and core base64-encodes them for the wire.microvms-cli/src/cli.rs:1388-1389.--eof— close stdin after any--datais written, in the same request rather than a second one.microvms-cli/src/cli.rs:1396-1397.- Plus
AttachFlagsandRegionFlags.microvms-cli/src/cli.rs:1399-1403.
microvm cp [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID> <SRC> <DST>Copies a file or a tar archive between here and a running MicroVM: cp ./local vm:/remote writes, cp vm:/remote ./local reads.
microvms-cli/src/commands/attached.rs:1162
Flags:
<SRC>— source;vm:/pathreads from the VM, anything else is a local path. Required.microvms-cli/src/cli.rs:1409-1410.<DST>— destination;vm:/pathwrites to the VM, anything else is a local path. Required.microvms-cli/src/cli.rs:1413-1414.--tar— move a whole directory tree as an uncompressed tar archive; thevm:side is a directory the daemon packs or extracts, the local side is a.tarfile.microvms-cli/src/cli.rs:1437-1438.--mode <OCTAL>— permissions for an uploaded file, octal as a string; conflicts with--tar, since a tar carries its members’ own modes.microvms-cli/src/cli.rs:1445-1446.- Plus
AttachFlagsandRegionFlags.microvms-cli/src/cli.rs:1448-1452.
microvm sync [OPTIONS] --name <NAME> <DIR>Syncs a project directory into a running MicroVM’s /workspace, incrementally: the first sync uploads the tree and writes a content manifest beside it in the guest; every later sync hashes the local tree, diffs against that manifest, and uploads only the members whose bytes differ. A second sync of an unchanged tree sends no archive — one manifest read, zero uploads. Locally deleted paths are removed in the guest by one argv rm -rf -- exec rooted in /workspace, and deletion paths are validated first: the manifest is the VM’s word, and it cannot order deletions outside the workspace. The incremental half of #71; run <DIR> is the launch-coupled batch half.
microvms-cli/src/commands/attached.rs, sync
Flags:
<DIR>— the project directory; packed withrun <DIR>’s rules (.git,target,node_modules,.venvstay home; symlinks travel as links; the daemon’s budgets enforced during the walk). Required.--watch— keep syncing on debounced filesystem changes until Ctrl-C, which resolves into the summary envelope. Events only wake a re-hash; the hash compare decides whether bytes move. Withmicrovm port-forwardin a second terminal, an edit here is a reload there.--full— upload the whole tree even where the guest manifest claims members are unchanged; the recovery when a workload edited/workspacebehind sync’s back.--timeout <SECONDS>— budget for the in-guest deletion exec; default60.- Plus
AttachFlagsandRegionFlags.
A disk-pressure refusal (the daemon’s 507, with the byte counts in the body) surfaces as ERR_PLATFORM with the free-space remedy and data.diskUnderPressure: true — never ERR_RETRYABLE, because retrying an identical upload against a full disk cannot succeed.
attach
Section titled “attach”microvm attach --from <FILE|-> [--name <NEW_NAME>] [--verify-identity] [--state-dir <DIR>]microvm attach --name <NAME> --microvm-id <ID> --endpoint <URL> --agent-token <TOKEN> [--region <R>] [--identity-host-seed <BASE64> --identity-vm-public-key <BASE64>] [--verify-identity]Registers a name for a running MicroVM this state directory did not launch, so every later --name command here can act on it. Cross-machine adoption (issue #66): same-machine adoption already exists through the registry run --keep --vm-name writes, and what was missing was moving that record between machines. Two spellings of the same facts, mutually exclusive: --from reads a name record exactly as the registry writes it (<state-dir>/names/<name>.json on the launching machine — the export format is the existing file, so microvm attach --from <(ssh other cat ~/.microvm/runs/names/ci.json) needs no export command), and the explicit triple is for a caller holding a run envelope from another machine. Nothing is written until the VM has answered one authenticated request with the token being registered.
microvms-cli/src/commands/attached.rs, attach_vm
Flags:
--from <FILE>— a name record file, or-for stdin. The record’s own name is registered unless--namerenames it; its region is the default unless a region flag is given. Identity material in the record is kept: the VM pin is what a latertunnel --name <NAME> --verify-identitychecks against, and the host seed is in the same trust domain as the agent token beside it. Conflicts with the explicit triple.--name <NAME>— the name to register. Required with the explicit triple; optional rename with--from. Validated likerun --vm-name.--endpoint <URL>,--agent-token <TOKEN>,--microvm-id <ID>— the explicit triple, from arunenvelope. Required unless--fromis given.--identity-host-seed <BASE64>,--identity-vm-public-key <BASE64>— the pair fromrun --identity’s envelope, stored on the record for a later verified tunnel. Each requires the other: the Noise KK pattern proves this side too, so the pin alone cannot run a handshake.--verify-identity— prove the far end is the VM the record was created for before writing anything, with the same Noise KK handshaketunnel --verify-identityruns (microvms-core/src/session/tunnel.rs,verify_identity— the tunnel’s open-and-handshake step with no relay after it). Refused up front when the record carries no identity pair, naming the missing field.--port <PORT>— the daemon’s port inside the guest, for the probe. Not recorded.--state-dir <DIR>— the registry the record is written into; defaults to$MICROVM_STATE_DIRor~/.microvm/runs.- Plus
RegionFlags.
Refusals, each before anything is written:
- A name already registered here to a different MicroVM id is
ERR_NAME_TAKEN(14) before any AWS call, withdata.holdernaming the VM that holds it — the same row and the same reasoning asrun --keep --vm-name, and a torn record reads as taken. The same VM under the same name is an idempotent success withreplaced: trueand a refreshed record. - The probe is one authenticated read (
GET /v1/fs/file?path=/dev/null), not/v1/health: health is an open route on the daemon, so a probe that stopped there would accept a wrong token. A dead endpoint, a wrong token (ERR_CREDENTIALS,data.kind: Unauthorized), or an unbootstrapped daemon (data.kind: NotBootstrapped) surfaces with the daemon’s own class and leaves the registry untouched. --verify-identitywithout the pair, a VM launched without--identity(close 4401), or a record replayed from another VM (the daemon’s reply does not verify against the pin) isERR_PRECONDITION, and nothing is written.
The envelope is {name, microvmId, endpoint, region, verifiedIdentity, replaced, statePath} — never the agent token, which is in the owner-only (0600 on Unix) record file at statePath. Text rendering is one line.
A terminate releases a name only in the state directory it ran against: a record adopted into a second machine stays there until that machine terminates by the name or deletes the file.
suspend
Section titled “suspend”microvm suspend [OPTIONS] <MICROVM_ID>Freezes a MicroVM, which keeps its memory, filesystem, token, and endpoint. The operation is a freeze and restore rather than a stop and start.
microvms-cli/src/commands/lifecycle.rs:1908
Flags:
<MICROVM_ID>— the MicroVM to freeze: a MicroVM id, or a namerun --keep --vm-nameregistered (resolved locally, zero extra calls; an unknown name fails withERR_PRECONDITIONbefore any call). Required.--timeout <TIMEOUT>— how long to wait for the state transition, in seconds; default300.microvms-cli/src/cli.rs:1582-1583.- Plus
RegionFlags.microvms-cli/src/cli.rs:1589-1590.
resume
Section titled “resume”microvm resume [OPTIONS] <MICROVM_ID>Thaws a suspended MicroVM and reports its endpoint. Past the launch-time suspendedDurationSeconds window the VM has been terminated, so the resume fails.
microvms-cli/src/commands/lifecycle.rs:1979
Flags:
<MICROVM_ID>— the MicroVM to thaw: a MicroVM id, or a registered name (resolved locally). Required.--timeout <TIMEOUT>— how long to wait for RUNNING, in seconds; default300.microvms-cli/src/cli.rs:1600-1601.- Plus
RegionFlags.microvms-cli/src/cli.rs:1607-1608.
terminate
Section titled “terminate”microvm terminate [OPTIONS] <MICROVM_ID>Tears down a MicroVM and optionally its image and build log group. When part of the teardown fails, the command still exits successfully and reports the leaked identifier.
microvms-cli/src/commands/lifecycle.rs:2050
Flags:
<MICROVM_ID>— the MicroVM to terminate: a MicroVM id, or a registered name (resolved locally). An accepted terminate releases every name this state directory registered to the VM, whichever spelling addressed it — the launch’s own and anyattachalias — so the names are reusable. Required.--image-identifier <IMAGE_IDENTIFIER>— the image to delete, if--delete-imageis given. Omitted, the image is read off the kept run’s record in the state directory:run --keepwrote the VM, the image, and the image’s name there, so the flag is an override rather than a requirement (#160).microvms-cli/src/cli.rs,TerminateArgs::image_identifier.--image-name <IMAGE_NAME>— the image’s name, needed to name its build log group; the service created that group, soterraform destroynever removes it. Omitted, it is read off the same record when one names the image.microvms-cli/src/cli.rs,TerminateArgs::image_name.--delete-image— also delete the image and name its build log group. With neither the flag nor a record naming an image, the command is refused locally withERR_INVALID_ARGbefore any call. After the teardown the run record is narrowed to what is still outstanding — a kept image, a failed delete, and the build log group this CLI can only name — and removed when nothing is, solsstops reporting a VM this command removed.microvms-cli/src/cli.rs,TerminateArgs::delete_image;microvms-cli/src/commands/lifecycle.rs,terminate;microvms-cli/src/ledger.rs,Ledger::open_for_vm.--wait— wait for TERMINATED rather than returning as soon as the call is accepted.microvms-cli/src/cli.rs:1639-1640.- Plus
RegionFlags.microvms-cli/src/cli.rs:1646-1647.
microvm ls [OPTIONS]Lists the local ledger of a state directory: what this CLI could not confirm it deleted, not what exists in the account. It reads ~/.microvm/runs/*.json rather than asking AWS, because the ledger still names the resources a killed process never got to delete, and an entry can outlive the resource it names — the measurement behind #159 was 68 entries with populated leaked lists in an account both scripts/verify-clean.py and the live APIs showed empty. Every microvm.runs envelope therefore carries source: "local-ledger", and the text and TUI header reads local ledger of <state dir>: what this CLI could not confirm it deleted, not what exists in the account. microvms-cli/src/commands/local.rs:32-52.
--remote asks the other question. Through the same control plane every other command uses (CoreSeam::control_plane; one AWS service, no second client), it reads ListMicrovms and ListMicrovmImages to their last page and judges each identifier in a ledger entry’s leaked list — the ledger’s own statement of what is outstanding, which teardown narrows to what a delete did not report gone. An identifier found in either listing takes the listed state: alive is every MicroVM state but TERMINATED and every image state but DELETING/DELETED, the predicate scripts/verify-clean.py uses, so the two tools cannot disagree about a leak. An identifier absent from both is gone only when it is spelled as a MicroVM id (microvm-…) or an image ARN (…:microvm-image:<name>), the two things the listings could have shown; any other spelling, a service-created /aws/lambda-microvms/… log group above all, is something neither listing can see, and its absence says nothing. The entry is then live if any identifier is, else unjudged if any is, else gone; a record written for another region or one that cannot be read is unjudged outright. A record whose leaked list is empty, the shape a run that created nothing leaves, is judged by its named microvmId/imageIdentifier instead. data.remote is null without the flag and otherwise {region, microvms: [{microvmId, state, imageArn}], images: [{imageArn, name, state}], entries: [{runId, microvmId, imageIdentifier, microvmState, imageState, status}], unknownToLedger: {microvms, images}}, where unknownToLedger is what is alive in the account that no entry names — the sibling-client case the issue measured. microvms-cli/src/commands/local.rs:256.
--prune (requires --remote) removes the ledger files of gone entries and lists their run ids in data.pruned, which is [] on every other invocation. Only gone: a live record is the one the ledger exists for, and an unjudged one names something these listings cannot answer for — for a leaked log group the ledger file is the only pointer there is (microvms-cli/src/ledger.rs:6-9), so removing it on the strength of a TERMINATED VM beside it would lose the resource. The removal goes through ledger::remove, which checks the run id against the ledger’s own grammar before it becomes a path component. microvms-cli/src/ledger.rs:197-209.
Flags:
--state-dir <STATE_DIR>— where the ledgers live; defaults to$MICROVM_STATE_DIRor~/.microvm/runs.microvms-cli/src/cli.rs:1653-1654.--remote— list live MicroVMs and images from the account and mark each ledger entrylive,gone, orunjudgedby the identifiers in itsleakedlist; conflicts with--watch, which is ledger-only by contract.microvms-cli/src/cli.rs:1693-1694.--prune— remove the ledger files ofgoneentries; requires--remote.microvms-cli/src/cli.rs:1700-1701.--region <REGION>/--unlisted-region <NAME>— the region--remotelists; the sharedRegionFlagsgroup, ignored without--remote.microvms-cli/src/cli.rs:1703-1704.--watch— re-read the ledger on an interval until Ctrl-C,port-forwardstyle: snapshots on stderr, one summary envelope at the end (data.watch = {refreshes, intervalSeconds, interrupted, calls},nullfor a plainls), Ctrl-C exits 0 (#78). The loop is ledger-only: zero platform calls per refresh — noGetMicrovm, no/v1/health— so it resets no idle timer, keeps no VM alive, and bills nothing;data.watch.callsstates that flatly and the text carries the measurement behind it. The distinction matters because an outside/v1/healthpoll does reset the platform idle timer (docs/PLATFORM.md, idle-timer section: a polled VM stayed RUNNING through 311s of a 60s window while the unpolled control suspended at 66s), so a health-polling watcher would keep every watched VM billing. The corollary: what a watch shows is what this CLI last recorded, not the platform’s live state — a VM the platform suspended at its idle window still reads as this ledger wrote it.microvms-cli/src/commands/local.rs,watch.--interval-sec <SECONDS>— seconds between ledger re-reads under--watch; default2, floor0.1. The default can be short because the loop is local-only; a health-polling watcher would instead need its interval judged against every watched VM’smaxIdleDurationSeconds.--max-refreshes <N>— stop after N snapshots instead of on Ctrl-C, for scripts;0is refused.
history
Section titled “history”microvm history [OPTIONS] <VM_ID>Prints what was asked of one MicroVM and what the platform reported back: imageBuilt (when run’s own invocation built the image — a standalone build has no VM id and writes no history), launched, exec per exec, hookObserved per lifecycle-hook firing the daemon reported, suspended, resumed, and terminated with the teardown’s own verdict. The record is an append-only JSONL file at <state-dir>/history/<vm-id>.jsonl, one camelCase object per line with a per-VM seq counter and an epoch-seconds at, appended by run, exec, health, suspend, resume, and terminate with the same swallowed-failure discipline as the ledger — an unwritable state directory never fails a teardown.
hookObserved events (issue #80) carry hook — the route’s own spelling: ready, validate, run, suspend, resume, terminate — and firedAt, epoch seconds on the daemon’s clock, distinct from the line’s own at (when this CLI learned of it). They arrive by health poll rather than push: run’s teardown fetches health once before terminating, resume polls after RUNNING (the only moment a suspend-hook firing can become visible, since a frozen VM cannot answer), and microvm health appends whenever it knows the VM id. Every append deduplicates on the (hook, firedAt) pair, so polling twice appends nothing. The validate hook writes no CloudWatch logs, so this record is the only local answer to “did my validate hook even run?”.
Unlike the ledger ls reads, nothing ever deletes a history file: the record survives terminate on purpose, because a caller attesting over a run needs it precisely after the VM is gone. A VM with no history file is a clean empty result rather than an error — asking about a VM this state dir never saw is a question, not a mistake. A truncated last line (a process killed mid-append) is reported as an unreadable record rather than skipped.
What the record does not prove: it shows what was asked of the VM and what the daemon and the control plane reported back — identifiers, endpoints, exit codes, teardown verdicts, hook firings — not what a process inside the guest did between execs. Every value is the platform’s or the daemon’s; nothing the guest printed ever reaches a history line, because a record built from guest output would be a record the workload can forge. hookObserved lines carry one additive caveat on top of that: the daemon’s hook routes are unauthenticated and loopback-reachable from inside the guest, so a hostile workload can make the daemon observe extra firings by posting the hook paths itself — it cannot remove or alter real ones, because the daemon records before responding and its capped log keeps the earliest entries, which are the platform’s. Read a hook line as “the daemon saw this hook fire”, never as “only the platform could have fired it”.
microvms-cli/src/commands/local.rs, storage in microvms-cli/src/history.rs.
Flags:
<VM_ID>— the MicroVM whose history to print: a MicroVM id, or a registered name (resolved locally; an unregistered name reads as a clean empty history, this command’s usual answer for an unseen VM). Required.--state-dir <STATE_DIR>— where the histories live; defaults to$MICROVM_STATE_DIRor~/.microvm/runs.
microvm logs [OPTIONS] <IMAGE_NAME>Names an image’s build log group, /aws/lambda-microvms/<image-name>, and prints the working aws logs tail invocation that reads it — a success, exit 0. That log group holds the only evidence a failed build leaves behind.
The envelope’s data carries tailCommand (aws logs tail <group> --since 1h --format short --region <region>, pinned to the region this invocation resolved so the printed line is runnable regardless of the shell’s default) and tailRequires: the command needs AWS CLI v2 — aws logs tail does not exist in v1 (verified present in 2.35.7). The identity running the tail also needs logs:FilterLogEvents, logs:GetLogEvents, and logs:DescribeLogStreams on the group; the Terraform stack’s logs_read_policy_arn output is exactly that grant, ready to attach. The CLI itself never reads CloudWatch — lines is explicitly null, never [], so a consumer cannot mistake “this client did not read the group” for “the group has no events” (the wrong-prefix signature).
data also carries streams: three objects labelling the build’s log topology by role — docker-build (zip pull and docker image build), snapshot-graviton3, and snapshot-graviton4 (the snapshot VMs are the ones that start the app, so application startup logs land there). One build is three VMs and three streams, with random service-chosen stream names by default; a configured logStream collapses all three into one exact stream, distinguished across builds only by the per-build /<16 hex> suffix this client appends, and the resolved name is on the build envelope’s logStream. The point of returning the topology here is that an agent handed this envelope knows what to look for inside the group without measuring the platform itself. See docs/PLATFORM.md, “An image build is three VMs and three log streams”.
microvms-cli/src/commands/local.rs:696
Flags:
<IMAGE_NAME>— the image whose log group to name. Required.microvms-cli/src/cli.rs:1721-1722.- Plus
RegionFlags.microvms-cli/src/cli.rs:1724-1725.
microvm cost [OPTIONS]Reports what a run cost or what a plan will cost, with every figure labelled. Dollar figures are estimates derived from published rates, not invoice amounts. A line item with no published rate reads unpriced rather than $0.00.
microvms-cli/src/commands/cost.rs:27
Flags:
--estimate— treat the durations as a plan rather than as timings, so every duration is labelled projected.microvms-cli/src/cli.rs:1734-1735.--compare— also print running versus suspended for the same hold, with the break-even.microvms-cli/src/cli.rs:1738-1739.--memory <MEMORY>— baseline MiB, selecting a documented size class; default2048.microvms-cli/src/cli.rs:1742-1743.--running-sec <RUNNING_SEC>— seconds the VM spent, or will spend, RUNNING; billed at baseline whether or not anything is executing. Default0.microvms-cli/src/cli.rs:1749-1750.--suspended-sec <SUSPENDED_SEC>— seconds spent suspended; storage only, no compute line. Default0.microvms-cli/src/cli.rs:1753-1754.--build-sec <BUILD_SEC>— seconds the image build took; appears as an unpriced line. Default0.microvms-cli/src/cli.rs:1760-1761.--image-gb <IMAGE_GB>— image size in GB; adds storage with its one-week minimum retention.microvms-cli/src/cli.rs:1764-1765.--cycles <CYCLES>— suspend/resume cycles, each paying a snapshot write plus a read; default1.microvms-cli/src/cli.rs:1768-1769.--hold-sec <HOLD_SEC>— the hold to compare running against suspended over, in seconds; default3600.microvms-cli/src/cli.rs:1772-1773.--max-cost <USD>— a budget the report’s total is checked against (#77). The comparison is against the priced total, which is a lower bound whenever any line is unpriced — so a detected breach has already been exceeded by an unknown margin. The verdict lands indata.budget({maxUsd, onBreach, basis, breached, overageAtLeastUsd},nullwith no budget), in the text, and in the dense rendering;basissays whether the compared total wasexactorlower-bound. A breach is always a stderr warning,--quietincluded. Requires--on-breach.microvms-cli/src/commands/cost.rs, budget gate.--on-breach <warn|abort>— what a--max-costbreach does, and deliberately without a default: because the compared total can be a lower bound, whether a breach warns (exit 0) or aborts is the caller’s judgement.abortexitsERR_PRECONDITION(12) after the full report is written — the same success-envelope-then-non-zero mechanism asrun’s workload exit — so a CI gate branches on 12 while still receiving the figures. No new exit code: 12 is an existing row, and 12-vs-2 distinguishes “over budget” from “bad flag”.microvms-cli/src/cli.rs,OnBreach.
doctor
Section titled “doctor”microvm doctor [OPTIONS]Checks every prerequisite and says which one is wrong. The checks cover the project config file, credentials, the region, the three Terraform outputs, whether the stack is applied, the managed base images AWS publishes, and whether the daemon binary is aarch64.
The config check runs first because it is the one check that is entirely local, and a malformed file fails run before anything else would. doctor --config ci.toml validates the exact file run --config ci.toml would read, through the same loader, so the two commands cannot disagree about which config applies. A broken file — unparseable, an unknown key, a value outside its flag’s domain — is a fatal fail naming the reason; an absent ./microvm.toml is an advisory pass, because a project configured by flags is not a finding. A valid file’s pass line names which knobs it pins. microvms-cli/src/commands/doctor.rs’s check_config.
microvms-cli/src/commands/doctor.rs:32
Two of the checks are about the managed base rather than about this machine, and both are advisory.
managed-bases reads ListManagedMicrovmImages and answers whether AWS publishes a base this client does not know about. It reports rather than offers, and the reason is a limitation in the shape: ManagedMicrovmImageSummary carries an ARN and two timestamps and nothing else — no registry reference, no architecture, no working directory. A BaseImage in microvms-core pairs the ARN with the Dockerfile FROM and with whether the image declares a WORKDIR, because require_matching_from compares a caller’s Dockerfile against the first and require_workdir refuses inheritance on the second. Neither guard has an input from this listing, and the registry ref is not derivable from the ARN — al2023-1 pairs with public.ecr.aws/amazonlinux/amazonlinux:2023-minimal and nothing in the ARN says so. So a discovered base cannot be built from through this client, and the check says which one it knows.
base-image-versions reads ListManagedMicrovmImageVersions and prints the values build --base-image-version may take. That is the actionable half: an unpinned build takes whatever the service currently defaults to, and that default has already moved once.
Both are advisory because nothing about a build depends on either read succeeding, and a doctor that failed on a listing would add a way to be wrong rather than report one. microvms-cli/src/commands/doctor.rs’s check_managed_bases.
Flags:
--binary <BINARY>— the agentd binary to check the architecture of.microvms-cli/src/cli.rs:1814-1815.--infra-dir <INFRA_DIR>— the Terraform stack directory; defaults to./conformance/infra.microvms-cli/src/cli.rs:1818-1819.- Plus
RegionFlagsandInfraFlags.microvms-cli/src/cli.rs:1821-1828.
manifest
Section titled “manifest”microvm manifest [OPTIONS]Emits the whole command surface, its exit codes, and its envelope schema. The manifest is generated from the registered clap tree rather than maintained by hand, so it cannot drift from what the binary accepts.
microvms-cli/src/commands/local.rs:785
The command takes no flags and always emits JSON, because the only consumer that asks for a manifest is one that parses it.
constants
Section titled “constants”microvm constants [OPTIONS]Emits every service constraint this client believes, for the drift gate that scripts/check-model-drift.py runs against the pinned botocore model.
microvms-cli/src/commands/local.rs:808
Flags:
--emit-json— emit the raw constants object without an envelope. This is the one stdout write in this binary that is not an envelope.microvms-cli/src/cli.rs:1839-1840.
dockerfile
Section titled “dockerfile”microvm dockerfile [OPTIONS]Prints the Dockerfile stanza that wraps any base image with agentd. The stanza is what microvm build bakes when no --dockerfile is given, so appending your own RUN layers to this output and passing the result to build --dockerfile is the default build plus your layers. The text comes from microvms-core’s own generator rather than a copy, so it cannot drift from what a default build produces. microvms-cli/src/commands/local.rs:843, reusing microvms-core/src/control/artifact.rs:145.
The output’s comments name the two platform constraints a hand-written wrapper hits. The FROM must match the managed base’s docker_ref, because the build runs the Dockerfile on top of the base that baseImageArn names and microvms-core refuses a Dockerfile whose FROM disagrees. And a WORKDIR is required when the base declares none — the managed al2023 base does not, so without one every relative path resolves against /. microvms-core/src/control/artifact.rs:228-244, microvms-core/src/control/artifact.rs:196-220.
This is a local command: no account is involved, and the stanza is built from compile-time constants.
Flags:
--from <IMAGE_REF>— the image ref for theFROMline; defaults to the managed al2023 base’s pair,public.ecr.aws/amazonlinux/amazonlinux:2023-minimal. Only change this when you are also changingbaseImageArn.microvms-cli/src/cli.rs:1845-1852.--port <PORT>— the port agentd listens on inside the guest; default9000. Reaches bothENV AGENTD_PORTandEXPOSE.microvms-cli/src/cli.rs:1854-1856.--workdir <DIR>— a working directory to create and set, as both aRUN mkdir -pand aWORKDIR. Strongly recommended, because the managed base declares no WorkingDir.microvms-cli/src/cli.rs:1858-1864.
The JSON envelope carries the stanza text plus the base image pair — baseImageName for deriving baseImageArn and baseImageDockerRef for the FROM — so a consumer holds both halves of the agreement the platform enforces. microvms-cli/src/commands/mod.rs:455-465.
For the full recipe — appending tool layers, building, and driving the daemon from your own harness — see docs/EMBEDDING.md.
Project config: microvm.toml
Section titled “Project config: microvm.toml”run and doctor read an optional ./microvm.toml beside the invocation (or the file --config <PATH> names). Every key in it already exists as a run flag; the file adds no capability, only persistence — microvm run in a configured project needs zero flags. microvms-cli/src/config.rs.
image = "coding-agents" # run --imagebinary = "target/agentd" # run [BINARY]exec = "make test" # run --execmemory = 4096 # run --memory; same closed set as the flagregion = "us-west-2" # any region string, resolved where the flag's isegress = true # run --egress# deny-egress = true # run --deny-egress; advisory. Refused beside egress = true, # so this line and the one above are alternatives, never bothauto-resume = true # run --auto-resumemax-idle-sec = 600 # run --max-idle-secsuspended-sec = 600 # run --suspended-secmax-duration-sec = 3600 # run --max-duration-secartifacts = ["dist/**"] # run <DIR> artifact globs (issue #72); validated herelog-group = "/aws/lambda-microvms/ci-builds" # run --log-group; the build role must be able to write herelog-stream = "ci-image" # run --log-stream; a PREFIX — the client appends /<16 hex> per build (issue #98)[env] # run --launch-env, as a table; per-key merge, flag wins its keyCI = "1"Key names are the flag names with - for _, so the file reads like the command line it replaces. All keys are optional; an absent key means the flag or the built-in default decides. A relative binary path resolves against the config file’s own directory, not the process cwd — --config /repo/microvm.toml exists precisely for the invoke-from-elsewhere case, and target/agentd must mean the same binary from anywhere. An unknown key is refused by name rather than silently ignored — memroy = 4096 launching a 2 GB VM is the failure that closes. A value outside its flag’s domain (memory = 1500, an artifact glob that will not compile) is refused with the same closed-set reasoning the parser enforces, so the file cannot be a side door past the flag domains.
On Windows, two binary shapes are refused at load because they mean two things at once (issue #87): a rooted path with no drive (/opt/agentd — Windows parses it as relative, so the file-directory join would silently re-anchor it onto the config file’s drive) and a drive with no root (C:agentd — resolves against that drive’s current directory at spawn time). The remedy differs per intent — write the drive letter, or write a genuinely relative path — so the loader refuses rather than guesses. On Unix neither shape exists and /opt/agentd is simply absolute.
--timeout is deliberately not a config key: it is a client-side wait, not a property of the VM the project wants to pin.
A file that cannot be used is ERR_CONFIG (exit 15), refused locally before any billable call. The remedy differs from ERR_INVALID_ARG’s — edit (or --no-config bypass) a file the invocation may never have named, rather than edit the command line — which is why it has its own row.
The JSON envelope
Section titled “The JSON envelope”Each invocation writes exactly one JSON object on stdout. Progress always goes to stderr. --quiet suppresses progress but not warnings, so a leaked resource is still reported even in quiet mode. microvms-cli/src/envelope.rs:1-18. apiVersion is "1"; it is bumped when a field’s meaning changes, not when a command is added. microvms-cli/src/envelope.rs:66.
A success envelope carries status, apiVersion, type, and data. type is the discriminant to branch on first. microvms-cli/src/envelope.rs:311-318.
{ "status": "ok", "apiVersion": "1", "type": "microvm.run", "data": { }}A failure envelope carries status, apiVersion, error, code, exitCode, finding, suggestions, and data. Every field is always present, so a consumer never has to guard against a missing key. microvms-cli/src/envelope.rs:323-342.
{ "status": "error", "apiVersion": "1", "error": "human readable, may be reworded between releases", "code": "ERR_PROTOCOL", "exitCode": 5, "finding": "", "suggestions": [], "data": { "kind": "Conflict" }}Branch on code rather than error; the error text may be reworded between releases while code is stable. microvms-cli/src/manifest.rs:120-121.
data.kind
Section titled “data.kind”data.kind carries the daemon’s own status name when the exit code is coarser than the failure. Five WireKinds (Conflict, NotFound, ProtocolError, StdinClosed, TooLarge) all map to ERR_PROTOCOL. Collapsing them is deliberate. A shell branching on $? cannot act differently on a 400 than on a 409, and a consumer that can act differently reads data.kind instead. microvms-cli/src/exit.rs:39-44, inserted at microvms-cli/src/envelope.rs:326-328, and pinned at microvms-cli/src/exit.rs:594-627.
A request rejected locally reports no data.kind, because nothing reached the daemon. microvms-cli/src/envelope.rs:445-450.
Response types
Section titled “Response types”Each command declares its type discriminant and the data keys its success envelope carries. microvms-cli/src/commands/mod.rs:102-466.
| Command | type |
|---|---|
run |
microvm.run |
quickstart |
microvm.run |
build |
microvm.image |
agent-up |
microvm.agent |
agent-prompt |
microvm.agent.prompt |
exec |
microvm.exec |
health |
microvm.health |
ack |
microvm.exec |
kill |
microvm.kill |
ps |
microvm.procs |
stdin |
microvm.stdin |
cp |
microvm.copy |
sync |
microvm.sync |
attach |
microvm.attach |
tunnel |
microvm.tunnel |
port-forward |
microvm.port-forward |
shell |
microvm.shell |
suspend |
microvm.state |
resume |
microvm.state |
terminate |
microvm.teardown |
ls |
microvm.runs |
history |
microvm.history |
logs |
microvm.logs |
cost |
microvm.cost |
doctor |
microvm.doctor |
manifest |
microvm.manifest |
constants |
microvm.constants |
dockerfile |
microvm.dockerfile |
The NDJSON stream exception
Section titled “The NDJSON stream exception”exec --stream is the one invocation that writes more than one object to stdout. Under --json (or into a pipe asking for it) it emits NDJSON, meaning one JSON object per event and then the envelope as the final line. This is a second, narrower contract that sits alongside the one-envelope rule. Three things keep the two contracts distinguishable. microvms-cli/src/envelope.rs:35-54.
First, the discriminant differs. A streamed exec’s final envelope has type microvm.exec.stream, while a non-streamed exec has microvm.exec. A consumer branching on type learns which parse applies from the field it already reads first. microvms-cli/src/commands/mod.rs:468-494.
Second, the manifest publishes it. exec’s entry carries an alternateResponse object naming when: "--stream", the responseType, the responseKeys, and a stdout description of the NDJSON shape. The entry is generated from the flag’s presence in the command tree, so removing --stream from exec removes the entry too. microvms-cli/src/manifest.rs:50-71.
Third, the envelope is written compact once a stream has started, because “the last line is the envelope” is only true if the envelope is one line. A pretty-printed envelope at the end of an NDJSON stream would parse as several broken records. microvms-cli/src/envelope.rs:171-175.
{"event":"output","stream":"stdout","offset":0,"bytes":12,"text":"hello world\n","lossy":false}{"event":"exit","exitCode":0,"signal":null,"truncated":false,"writersMayBeAlive":false,"offset":12}{"status":"ok","apiVersion":"1","type":"microvm.exec.stream","data":{"execId":"x-1","events":2,"bytes":12,"nextOffset":12,"gaps":0,"exitCode":0,"truncated":false}}Three event kinds reach a line: output (with stream, offset, bytes, text, lossy), gap (with from and to), and exit (with exitCode, signal, truncated, writersMayBeAlive, offset). microvms-cli/src/commands/attached.rs:492-531.
Output arrives as lossy text beside the true byte count rather than as base64. lossy is set when the conversion actually replaced anything, so a consumer can tell when the text differs from the original bytes. The non-JSON path carries the exact bytes. microvms-cli/src/commands/attached.rs:480-511.
The stream envelope’s keys summarize the stream. The output itself was the NDJSON events, and repeating it in the envelope would double a stream’s memory cost for a consumer that has already seen every byte. events and bytes let a caller assert it read everything, and nextOffset is where a resume with --from-offset would continue. microvms-cli/src/commands/mod.rs:468-494.
The events are the command’s output, not progress about it, so they cannot go to stderr even though that would preserve the simpler one-envelope rule. Sending a workload’s stdout to the caller’s stderr would make microvm exec --stream build.sh > log write an empty log. Buffering the events to keep stdout a single document would remove the only reason to stream. microvms-cli/src/envelope.rs:51-54.
The non-JSON formats emit no NDJSON at all. The raw child bytes go to stdout untouched, with no lossy string conversion applied. microvms-cli/src/envelope.rs:232-247.
A stream that fails part-way through has already written events and no envelope. On the JSON path the failure envelope becomes the stream’s compact last line, because an NDJSON consumer reading line by line needs a terminating record saying why the events stopped. On the human paths the same failure goes to stderr instead, because appending an error message to the child’s raw output would corrupt the file a caller was redirecting into. microvms-cli/src/main.rs:353-367.
A stream that ended without an exit event was cut. exitCode is reported as null rather than 0, because reporting zero would turn a truncated stream into a passing build. The command exits ERR_EXEC_FAILED. microvms-cli/src/commands/attached.rs:455-474.
Exit codes
Section titled “Exit codes”The table has seventeen rows, 0 through 16, and is append-only because consumers branch on the values. The rows are split by what the caller should do next; a distinction that does not change the caller’s next action does not get its own integer. microvms-cli/src/exit.rs:196-309, with the enum’s explicit discriminants at microvms-cli/src/exit.rs:76-133.
| Exit | Code | Meaning | docs/PLATFORM.md finding |
|---|---|---|---|
| 0 | — | the command did what it said | |
| 1 | ERR_UNEXPECTED |
an exception no handler claimed — a bug in this CLI, not the platform | |
| 2 | ERR_INVALID_ARG |
the request was refused locally, before any AWS call | |
| 3 | ERR_RETRYABLE |
a transient condition; run the identical command again | Endpoint authentication |
| 4 | ERR_CREDENTIALS |
an identity is wrong or absent; waiting will not fix it | |
| 5 | ERR_PROTOCOL |
the daemon rejected the request on its merits | |
| 6 | ERR_BUILD_WEDGED |
the image build was never scheduled — the clientToken replay signature | clientToken is a permanent idempotency key |
| 7 | ERR_LAUNCH_DIED |
the MicroVM reached a terminal state before RUNNING; read stateReason | runHookPayload arrives wrapped, not as the body |
| 8 | ERR_WINDOW_CLOSED |
the launch-time suspended window passed, so there is nothing to resume | idlePolicy |
| 9 | ERR_PLATFORM |
a control-plane failure with no more specific class | |
| 10 | ERR_TIMEOUT |
a client-side deadline elapsed; the VM and the exec are untouched | |
| 11 | ERR_INTERRUPTED |
interrupted after launch; teardown ran and any leak is named in the payload | The build log group survives Terraform |
| 12 | ERR_PRECONDITION |
a prerequisite is missing — run microvm doctor |
|
| 13 | ERR_EXEC_FAILED |
the sandbox worked and the command in it exited non-zero | |
| 14 | ERR_NAME_TAKEN |
the VM name is registered to a live VM; refused locally, before any AWS call | |
| 15 | ERR_CONFIG |
the project config file is missing, malformed, or out of domain; refused locally — fix the file, or pass --no-config |
|
| 16 | ERR_SYNC |
run <DIR> could not pack the directory or write an artifact back; the failure is on this machine’s filesystem, not the platform’s |
Row 0 is the only one with no ERR_* string, because a success envelope has no code field to put one in. microvms-cli/src/exit.rs:52-59.
ERR_NAME_TAKEN, ERR_CONFIG, and ERR_SYNC are the rows with no core ErrorKind behind them: the name registry, the config file, and run <DIR>’s pack/extract are the CLI’s own filesystem work, so these refusals can only arise locally. Each is distinct from ERR_INVALID_ARG because the next actions differ — an invalid argument is fixed by editing the flag, a taken name by terminating the VM that holds it or picking another name, a broken config by editing (or --no-config bypassing) a file the invocation may never have named, and a sync failure by fixing the local file or disk the pack or extraction named. ERR_SYNC is also distinct from ERR_EXEC_FAILED: a CI caller needs to tell “the sync plumbing failed here” from “your tests failed” from “the platform failed”.
ERR_EXEC_FAILED has its own code because it is the one non-zero exit that means nothing is wrong with the platform, the credentials, or the CLI. A CI caller needs to tell “your tests failed” apart from “we never got a VM”. microvms-cli/src/exit.rs:96-101.
A clap parse failure maps to exit 2 / ERR_INVALID_ARG, forwarding clap’s own message verbatim including its did-you-mean line. This deliberately matches clap’s own convention, so a caller who reads $? sees the same number either way. microvms-cli/src/exit.rs:434-442, microvms-cli/src/cli.rs:56-59.
--help and --version are successes that print themselves and exit 0, never becoming envelopes. microvms-cli/src/main.rs:95-108.
Suggestions
Section titled “Suggestions”The exit code comes from the failure class and nothing else. The CLI adds the suggestion on top. The library reports what went wrong, and the CLI names the flag or command that addresses it. Two failures sharing ERR_CREDENTIALS therefore get different remedies. A 401 names the agent token. An unresolvable credential chain names microvm doctor and the unsupported-region null-message signature. microvms-cli/src/exit.rs:369-426.
Conventions
Section titled “Conventions”microvm manifest publishes six conventions alongside the command tree. microvms-cli/src/manifest.rs:129-146.
- Exactly one envelope object on stdout per invocation; progress is on stderr.
- Branch on
code, never onerror. - Dollar figures are estimates derived from published rates, never an invoice.
- An unpriced line item omits
usdrather than reporting zero. data.kindcarries the daemon’s own status name when the exit code is coarser than the failure (ERR_PROTOCOLcovers five).exec --streamis the one exception to the first line: it writes NDJSON with the discriminantmicrovm.exec.stream. Every other invocation writes exactly one object.
Closed option domains
Section titled “Closed option domains”Two options carry a closed set rather than free text, and the parser refuses everything else before any handler runs. Refusing an off-table value in the parser reports the error immediately, while refusing it in microvms-core would cost a build cycle first. microvms-cli/src/cli.rs:4-19.
--memory accepts exactly 512, 1024, 2048, 4096, 8192, the five documented size-class baselines. microvms-cli/src/cli.rs:361-373.
--region accepts exactly us-east-1, us-east-2, us-west-2, eu-west-1, ap-northeast-1, the five regions measured to carry MicroVMs. eu-central-1 is excluded on measurement. microvms-cli/src/cli.rs:412-424.
The escape hatch is a separate flag rather than a permissive parser. --unlisted-region <NAME> conflicts with --region and carries its cost in its help text, so a reader of a command line can see that someone opted in. microvms-cli/src/cli.rs:31-37.
Four option names — --client-token, --capabilities, --connector, and --architecture — are deliberately absent. Three of them because microvms-core has no parameter for the values they would carry; --connector because the intent is spelled --egress (the managed connector) or --egress-network-connector <ARN> (an existing VPC connector, see above). A test asserts the absence of all four names over every argument of every subcommand. microvms-cli/src/cli.rs:21-29, microvms-cli/src/cli.rs:2911-2935.
In the manifest, a boolean flag reports type: "boolean" and choices: null even though clap gives a SetTrue flag the possible values ["true", "false"]. Publishing those would put a choices array on every flag and make the closed-domain field unreadable. microvms-cli/src/manifest.rs:151-170.