LLM-native operations

Give agents the full FlashVM lifecycle.

flash-mcp gives any MCP-compatible agent direct access to FlashVMM creation, commands and files inside the guest, live fork, control, telemetry, snapshots, restore, warm pools, and teardown. Every tool call maps to either the real flash CLI or one HTTP request over the VM's own control endpoint, so automation uses exactly the control path a human operator uses. The same twenty tools, with the same parameters, ship on macOS, Linux (x86_64 and arm64), and Windows (x64 and arm64). See Developer for installation.

The resource contract: limits only when you pass them

A VM's shape is required: --vcpus and --mem-mib (REST: vcpus, mem_mib). Every cap is optional. Omit a cap and that resource runs uncapped; pass it and it is enforced. Neither flash, flash-mcp, the REST API, nor the pool daemon ever picks a limit for you. A value that cannot be enforced is refused with 422, never accepted and dropped, and an out-of-range value is refused, never clamped.

ResourceCLI flagREST fieldOmitted or 0Passed
vCPUs--vcpus Nvcpusrefused (required)exactly N vCPUs
Guest RAM--mem-mib Nmem_mibrefused (required)exactly N MiB, committed only as the guest touches it
CPU cap--cpu-millis Ncpu_millisno CPU capheld to N/1000 cores
Disk size / quota--disk-mib Ndisk_mibno quotawrites past N MiB refused in the guest
Disk bandwidth--disk-bps Ndisk_bpsno capheld to N bytes/s
Disk operations--disk-iops Ndisk_iopsno capheld to N ops/s
NIC egress--nic …,mbps=Nnics[i].mbpsno capheld to N Mbit/s
NIC ingress--nic …,ingress-mbps=Nnics[i].ingress_mbpsno capheld to N Mbit/s
NIC packets--nic …,pps=Nnics[i].ppsno capheld to N packets/s

Live changes go through flash set (REST PATCH /v1/flashvm/cpu, /disk, /nics/{i}); a live rate of 0 removes that cap. Guest RAM is fixed at creation — reclaim with the balloon, or resize a region reserved with --mem-region-mib via flash mem. Platform notes an agent should know: the disk cap (disk_mib) is a live quota on what the guest writes on every OS; on macOS a VM can run on the vz engine (flash run --engine vz: Rosetta for x86_64 binaries, virtio-fs shares, NIC caps only — CPU/disk caps, snapshots and pools are refused) instead of the default hvf, which enforces every cap.

Where each VM's control endpoint lives

One flashvmm process serves one VM on its own endpoint. That endpoint is both the API and the registry entry — there is no central daemon. flash and flash-mcp find it by the VM's id.

PlatformEndpointHypervisor
Linux (x86_64, arm64)Unix socket /run/flashvm/<id>.sock (root) or $XDG_RUNTIME_DIR/flashvm/<id>.sockKVM
macOS (Apple Silicon)Unix socket in the per-user registry ($FLASHVM_API_DIR, else $XDG_RUNTIME_DIR/flashvm, else the per-user temp directory)Hypervisor.framework
Windows (x64, arm64)Named pipe \\.\pipe\flashvm-<id>Windows Hypervisor Platform

The HTTP API on that endpoint is the same resource-CRUD surface on every platform — /v1/flashvm and its sub-resources — so a tool call written once runs everywhere.

1. Create a VM with flash_cli

Creating a VM starts a new flashvmm process, so flash-mcp invokes the real flash CLI through flash_cli. Once that process binds its endpoint, the agent manages the VM directly through the HTTP API. There is no separate create tool to drift out of sync with the CLI.

# tool call
flash_cli(args: ["run", "--bundle", "/opt/flashvmm/bundles/current",
  "--vcpus", "2", "--mem-mib", "1024",
  "--cpu-template", "host-raw", "--entry", "/your/workload", "--id", "myvm"])

# what it runs — no caps passed, so none applied
flash run --bundle /opt/flashvmm/bundles/current --vcpus 2 --mem-mib 1024 \
  --cpu-template host-raw --entry /your/workload --id myvm

# the same VM with every limit enforced
flash run --bundle /opt/flashvmm/bundles/current --vcpus 2 --mem-mib 1024 \
  --cpu-millis 1500 --disk-mib 2048 --disk-bps 104857600 --disk-iops 10000 \
  --cpu-template host-raw --entry /your/workload --id myvm

# with the guest bridge: HTTP into the workload, and messages both ways
flash run --bundle /opt/flashvmm/bundles/current --vcpus 2 --mem-mib 1024 \
  --entry /your/workload --id myvm --http-port 8080 --messaging-topics jobs,events

--vcpus and --mem-mib are required. --http-port (for guest http) and --messaging-topics (for guest send/recv; * = every topic) turn on the guest bridge at creation. --cpu-template is optional to run and required for any VM that will be captured, because it cannot be added later. On macOS the bundle is /opt/flashvmm/bundles/default; on Windows it is C:\Program Files\FlashVMM\guest.

2. Attach networking

On Linux, host networking is set up before the VM's socket exists. flash_cli runs flashnet, the privileged helper that creates the tap, drops its capabilities, and hands execution to an unprivileged flash run — the VMM never holds network-administration privilege. On macOS and Windows, NAT networking is user-mode (gvproxy) and needs no elevation.

# tool call (Linux)
flash_cli(args: ["run", "--bundle", "/opt/flashvmm/bundles/current", "--id", "myvm",
  "--vcpus", "2", "--mem-mib", "1024", "--cpu-template", "host-raw",
  "--nic", "tap=tap1,ip=10.110.0.11/24,gateway=10.110.0.1,mbps=1000,pps=100000,ingress-mbps=1000",
  "--entry", "/your/workload"])

# what actually sets up the tap, underneath flash_cli
sudo flashnet --mode nat --vm-id myvm -- flash run ...

Every NIC rate (mbps, pps, ingress-mbps) is optional; omit one and that direction is uncapped. After startup, NIC state is a plain read: flash_resource(vm_id: "myvm", resource: "nics") maps to GET /v1/flashvm/nics.

3. Observe the VM in real time

With the process running, control and observation move to HTTP over the VM's endpoint. flash_metrics, flash_inspect, flash_logs, and flash_diagnose are structured views over that same API.

# tool call
flash_metrics(vm_id: "myvm")

# raw HTTP, the same request flash_api makes (Linux / macOS)
curl --unix-socket /run/flashvm/myvm.sock http://localhost/v1/flashvm/metrics

A metrics call returns a live sample of CPU, memory, disk I/O, and per-NIC traffic. flash_watch(vm_id, stream, for_secs) turns the same data into an SSE stream delivered as MCP progress notifications; flash_watch_fleet follows up to 20 VMs at once.

4. Capture the running state

# tool call
flash_api(vm_id: "myvm", method: "POST", path: "/v1/flashvm/snapshots",
  body: {"path": "/var/tmp/myvm.snap"})

# raw HTTP
curl --unix-socket /run/flashvm/myvm.sock -X POST http://localhost/v1/flashvm/snapshots \
  -H 'content-type: application/json' -d '{"path":"/var/tmp/myvm.snap"}'

Capture is asynchronous: poll the returned operation (flash_resource(vm_id, resource: "operation", id)). Snapshots require the VM to have started with --cpu-template.

5. Restore or clone with flash_cli

Restore creates a new VMM process from the snapshot, so the workflow returns to flash_cli. The process resumes from the captured execution state instead of repeating a cold boot.

# tool call
flash_cli(args: ["restore", "--bundle", "/opt/flashvmm/bundles/current",
  "--from", "/var/tmp/myvm.snap", "--vcpus", "2", "--mem-mib", "1024",
  "--cpu-template", "host-raw", "--id", "myvm"])

--vcpus and --mem-mib must match the capture. Restore returns the captured VM under its original identity, so --id must match the snapshot; use flash_cli(args: ["clone", ...]) to create a new identity from the same artifact. Caps are optional here too.

6. Stop and remove the VM

# tool call
flash_api(vm_id: "myvm", method: "DELETE", path: "/v1/flashvm")

# raw HTTP
curl --unix-socket /run/flashvm/myvm.sock -X DELETE http://localhost/v1/flashvm

The DELETE stops the guest and exits its process. Because the endpoint belongs to that process, the VM no longer appears in flash_ls or flash ls.

Run commands and move files with flash_exec and flash_cp

An agent works inside its VM directly — no network, no SSH, no shared folder needed. Both tools ride a dedicated virtio-console port into the guest agent, on every platform.

# tool calls
flash_cp(src: "/Users/me/sales.csv", dst: "agent-exec:/data/sales.csv")
flash_exec(vm_id: "agent-exec", argv: ["sh", "-c", "awk -F, 'NR>1 {s+=$3} END {print s}' /data/sales.csv"],
  timeout_s: 30)
# -> {"exit_code": 0, "state": "exited", "stdout": "48210\n", "stderr": "", "duration_ms": 41, "truncated": false}
flash_cp(src: "agent-exec:/data/report.txt", dst: "/Users/me/report.txt")
flash_cli(args: ["del", "agent-exec:/data", "--recursive"])

# kill or remove an exec record (resource CRUD on the VM's endpoint)
flash_api(vm_id: "agent-exec", method: "PATCH", path: "/v1/flashvm/guest/execs/7", body: {"state": "killed"})
flash_api(vm_id: "agent-exec", method: "DELETE", path: "/v1/flashvm/guest/execs/7")

Caps: 64 concurrent execs per VM (the 65th is 429 too_many_execs), 16 MiB of output kept per stream (truncated says when retention cut it), 1 GiB per file. A non-empty directory without --recursive is 409 not_empty; / is never deleted.

Limit what the VM can reach: --egress and flash_connections

Give an agent's VM exactly the network its task needs. FlashVMM enforces it outside the guest, so nothing the agent runs can widen it.

# tool calls
flash_cli(args: ["run", "--bundle", "/opt/flashvmm/bundles/current", "--id", "job", "--vcpus", "1",
  "--mem-mib", "512", "--nic", "{NIC}", "--egress", "public",
  "--entry", "/work/run.sh", "--detach"])              # crawl the internet; nothing private
flash_connections(vm_id: "job")
flash_cli(args: ["set", "job", "--egress", "allow:pypi.org,files.pythonhosted.org"])   # narrow it live
# -> {"connections": [{"proto": "dns", "dst": "pypi.org", "allowed": true, ...},
#      {"proto": "tcp", "dst": "151.101.0.223", "port": 443, "name": "pypi.org", "allowed": true, "bytes": 18234, ...},
#      {"proto": "dns", "dst": "evil.example", "allowed": false, "reason": "dns", ...}], "next": 3}

public reaches the whole internet but no private address (the LAN, the host, cloud metadata) — the right default for an agent that has to look things up; deny: adds a blocklist; none allows nothing; allow: takes names, *.name, addresses and CIDRs. flash set --egress (or flash_api PATCH on the NIC) changes it while the VM runs. A lookup of a name off the list is answered REFUSED and never leaves; an allowed name answering a private address is refused. Forks inherit the policy and restores keep it.

Use an API without holding its key: --secret-proxy

The agent in the VM calls its model (or any HTTP API) through a URL on the host; FlashVMM adds the key outside the VM. The key is never in the guest's environment, files or memory, and an upstream that echoes it comes back masked.

# tool calls
flash_cli(args: ["run", "--bundle", "/opt/flashvmm/bundles/current", "--id", "agent", "--vcpus", "1",
  "--mem-mib", "1024", "--nic", "{NIC}", "--egress", "none",
  "--secret-proxy", "model=/home/me/model.key,upstream=https://api.example.com,header=x-api-key,max-calls=200",
  "--entry", "/work/agent", "--detach"])
# inside the VM: POST $FLASH_PROXY_MODEL_URL/v1/messages  (plain HTTP; no key, no TLS)
flash_secret_proxy_calls(vm_id: "agent", name: "model")
# -> {"calls": [{"seq": 1, "method": "POST", "path": "/v1/messages", "status": 200, "bytes": 2438, "duration_ms": 1895}, ...], "next": 3}

--egress none plus a proxy is an agent that can reach its model and nothing else. Streaming replies pass straight through. Past max-calls or max-bytes the guest gets 429; flash proxies ID NAME --max-calls N|none changes a cap while the VM runs. A fork child gets the parent's proxies with its own counters, a pool spare takes them with its claim, and a restore must be given the key again (a snapshot never holds one).

Fork a running VM with flash_cli

Explore several approaches from one prepared state: fork the VM and give each child a different task. Children continue the parent's running program with its memory, files and processes; the parent keeps running.

# tool calls
flash_cli(args: ["run", "--bundle", "/opt/flashvmm/bundles/current", "--id", "base", "--vcpus", "1",
  "--mem-mib", "1024", "--cpu-template", "host-raw", "--forkable", "--entry", "/your/agent", "--detach"])
flash_cli(args: ["fork", "base", "--count", "3"])          # base-fork1..3
flash_exec(vm_id: "base-fork2", argv: ["sh", "-c", "run-variant b"])
flash_cli(args: ["rm", "base-fork1"])

The first fork of a --forkable VM freezes its RAM in place; later forks are diffs (15 ms on Linux x86_64, ~65 ms on Windows x64). Give children their own network with --nic pool=ID.

Warm pools with flash_pool

flashpoold keeps pools of parked spares ready to claim. flash_pool sends one resource-CRUD request to its socket (Linux/macOS) or pipe (Windows). Only the shape is required; caps are optional and apply to every member when given.

# tool call: a pool of 3 spares, 1 vCPU / 512 MiB each, uncapped
flash_pool(method: "POST", path: "/v1/pools?wait=idle&timeout_s=120",
  body: {"shape": {"vcpus": 1, "mem_mib": 512}, "count": 3})

# list, resize, drain, delete — no verbs in paths
flash_pool(method: "GET", path: "/v1/pools")
flash_pool(method: "PATCH", path: "/v1/pools/pool-ab12cd34", body: {"target_count": 5})
flash_pool(method: "DELETE", path: "/v1/pools/pool-ab12cd34")

# claim an idle member on its own endpoint
flash_cli(args: ["claim", "pool-ab12cd34-0001", "--entry", "/your/workload", "--cpu-millis", "1000"])

A claim's --cpu-millis and --disk-mib are optional (omitted = uncapped). shape.nics holds at most one {"kind":"nat","mbps_up"?,"mbps_down"?,"pps"?}.

Hosts and fleets with flash_host and flash_controller

flash_host reads and sets one host through its host daemon, flashhostd: every VM with what it asked for and really uses, the host's free room, its policy, its events, and what every VM used (GET /v1/host/usage). flash_controller drives a controller that places VMs across many hosts and moves running ones between them.

# how full is this host, and what does each VM really use
flash_host(method: "GET", path: "/v1/host")
flash_host(method: "GET", path: "/v1/host/flashvms")

# limits: refuse what does not fit, free room when memory runs short
flash_host(method: "PUT", path: "/v1/host/policy",
  body: {"admission": "enforce", "memory_overcommit": 3, "sleep_on_pressure": true})

# place a VM on the best-fitting host, then move it while it runs
flash_controller(method: "POST", path: "/v1/flashvms",
  body: {"id": "web-1", "shape": {"vcpus": 2, "mem_mib": 2048}, "entry": "my-server"})
flash_controller(method: "POST", path: "/v1/moves", body: {"flashvm": "web-1", "to": "host-2"})
flash_controller(method: "GET", path: "/v1/events?since=0&wait_s=30")

A host that is full says why (host_full), and the controller hands the VM on to the next host. A moved VM continues exactly where it was.

GPU pools with flash_gpu

No VM gets a GPU device. flashgpud runs a model on the host GPU (Metal on macOS, Vulkan on Linux and Windows) and VMs call its chat-completions HTTP API over their NIC, so snapshots and warm pools keep working. flash run --gpu-pool hands the workload FLASH_GPU_URL and a per-VM FLASH_GPU_TOKEN.

# tool call: load a model on the host GPU
flash_gpu(method: "POST", path: "/v1/gpu-pools?wait=ready&timeout_s=300",
  body: {"id": "qwen", "model": "/models/qwen2.5-0.5b-instruct-q4_k_m.gguf", "slots": 4})

# a VM whose workload uses it
flash_cli(args: ["run", "--bundle", "/opt/flashvmm/bundles/default", "--id", "agent1",
  "--vcpus", "1", "--mem-mib", "512",
  "--nic", "tap=agent1,mbps=1000,pps=100000,ingress-mbps=1000,ip=192.168.127.2/24,gateway=192.168.127.1",
  "--gpu-pool", "qwen", "--entry", "/your/agent", "--detach"])

# who is using it, then remove it
flash_gpu(method: "GET", path: "/v1/gpu-grants?gpu_pool=qwen")
flash_gpu(method: "DELETE", path: "/v1/gpu-pools/qwen")

Requests beyond the engine's slots wait and are served round-robin between VMs. ctx, slots and gpu_layers are optional (omitted = the engine's default).

Restore-via-claim: skip the cold boot on a warm spare

A VM booted with --pool parks at Ready instead of running a workload. claim --from hands that spare a snapshot to become instead of a program to run — the spare's own process is reused, so the restore pays no spawn or privilege-drop cost. arm64 hosts only.

# tool call
flash_cli(args: ["claim", "spare1", "--from", "/var/tmp/myvm.snap",
  "--bundle", "/opt/flashvmm/bundles/current", "--identity", "myvm", "--tenant", "acme"])

# raw HTTP, the same request flash_api makes
curl --unix-socket /run/flashvm/spare1.sock -X POST http://localhost/v1/flashvm/claim \
  -H 'content-type: application/json' \
  -d '{"snapshot_path":"/var/tmp/myvm.snap","bundle_dir":"/opt/flashvmm/bundles/current",
       "identity":"myvm","tenant":"acme"}'

--identity must match the identity the snapshot was captured under; --tenant is the restored VM's own. Add --cpu-millis / --disk-mib (cpu_millis / disk_cap_mib) only to cap the claimed workload. Like every claim, this returns once the request is accepted — poll flash_inspect for entry_started / state.

flash-mcp tools — the exact contract

Eighteen tools, identical on every platform. Parameters marked required must be present; everything else is optional.

ToolParametersWhat it does
flash_lsnoneList every VM id with a live endpoint in this user's registry
flash_inspectvm_id (required)Full state of one VM (GET /v1/flashvm)
flash_metricsvm_id (required)One point-in-time metrics sample (GET /v1/flashvm/metrics)
flash_diagnosevm_id (required), log_lines (1–500, default 50), metrics_window_secs (default 60)Inspect + a bounded metrics window + a log excerpt, merged into one read-only report
flash_resourcevm_id, resource (required); id for nic, snapshot, operationRead one resource: source, cpu, boot, memory, balloon, mem, disk, nics, nic, share, snapshots, snapshot, version, operation
flash_logsvm_id (required), stream (run|console), since, since_console, limitCursor-paged run and console log; pass back next_since / next_since_console to page forward
flash_watchvm_id, stream (metrics|logs|lifecycle|guest_messages), for_secs (1–3600) — all requiredFollow one VM's SSE stream as MCP progress notifications, then return a summary
flash_watch_fleetvm_ids (up to 20), stream, for_secs — all requiredThe same across up to 20 VMs in one merged, id-prefixed stream; more than 20 is refused, not truncated
flash_apivm_id, method (GET|POST|PUT|PATCH|DELETE), path — required; bodyOne raw HTTP request against an already-running VM's endpoint
flash_cliargs (required): the argv after flashRun the real flash binary — the only way to create (run/restore/clone) or remove a VM. No shell; 120 s timeout. run takes --http-port and --messaging-topics for the guest bridge
flash_execvm_id, argv (required); timeout_s (default 300), cwd, env, stdinRun one command inside a running VM and get {exit_code, signal, state, stdout, stderr, duration_ms, truncated} back. No shell unless you pass one (["sh", "-c", "..."]); the whole process tree is killed at the timeout
flash_cpsrc, dst (required; one side is VM_ID:/absolute/path)Copy a file or a directory tree into or out of a running VM, keeping permissions
flash_connectionsvm_id (required); nic (default 0), sinceThe outbound connections and DNS questions the VM made, allowed and refused, with the reason and bytes — what --egress decided
flash_secret_proxy_callsvm_id (required); name, sinceThe VM's secret proxies (URL, caps, calls, bytes, refused), or one proxy's calls with method, path, status, bytes and time — never the key
flash_sleepvm_id (required)Put a running VM to sleep: memory to a file, no process, no memory or CPU used; keeps its name, share, egress and secret proxies
flash_wakevm_id (required); nicWake a sleeping VM under the same name, carrying on from where it slept; a VM that had a network card needs nic again (refused nic_missing otherwise)
flash_poolmethod, path (required); body, socketOne request to flashpoold: create, list, resize, drain, and delete warm pools and their members
flash_gpumethod, path (required); body, socket (pipe on Windows)One request to flashgpud: GPU pools (a model on the host GPU shared by VMs over their NIC) and per-VM grants

Operational guidance

FlashVMM returns specific errors with a direct corrective action. These are the responses an agent should recognize and handle.

ErrorMeaningAction
the following required arguments were not provided: --vcpus / --mem-mibThe VM's shape was not statedPass --vcpus and --mem-mib; nothing picks them for you
422 invalid_vcpus / 422 missing_mem_mibA REST create with vcpus 0 or above the platform maximum, or mem_mib 0Send a real shape; out-of-range values are refused, never clamped
422 cap_not_enforceableA cap this engine cannot enforce (CPU/disk caps on the macOS vz engine; a CPU cap where Windows cannot create its CPU-rate job)Omit the cap, or use a backend that enforces it
422 too_many_nicsMore than 8 NICsGive the VM at most 8 NICs
422 unsupported_engine--engine names an engine this host lacks (macOS: hvf, vz; Linux: kvm; Windows: whp)Omit --engine, or name one the host has
no kernel in bundle '…' (looked for […])The bundle has no vmlinux, bzImage, Image, or kernelPoint --bundle at a directory containing one
restore refused [identity]: snapshot identity mismatchThe requested --id differs from the snapshot'sRestore with the captured --id, or clone for a new identity
422 memory_immutable on PATCH /memoryGuest RAM is fixed at creationUse PATCH /memory/balloon, or PATCH /memory/mem for a region reserved with mem_region_mib
409 on NIC add/remove after creationNIC topology is fixed at creationDeclare every NIC in flash run
no VM at …/ID.sock — is it running? try flash lsThe VM process has exitedRun flash ls before the next request
503 not_ready on a CPU-cap patchBoot grace is still activeRetry after boot, or create with --boot-grace-ms 0

Live context for long-running operations

Metrics, logs, lifecycle events, and guest messages stream over Server-Sent Events on the same per-VM endpoint used for control. Long-running tools report progress to the calling agent, and an explicit lagged event identifies any gap when a consumer falls behind.