1. The document
Section titled “1. The document”microvms-agentd wire protocol, generated from the daemon’s own serde types, via schemars. The same document is served at schema.json, and the daemon answers every response with the microvms-agentd-version header so a client can check which version it is talking to.
| Field | Value |
|---|---|
$schema |
https://json-schema.org/draft/2020-12/schema |
protocol_version |
2 |
daemon_version |
1.3.0 |
version_header |
microvms-agentd-version |
hook_prefix |
/aws/lambda-microvms/runtime/v1 |
2. Routes
Section titled “2. Routes”The daemon serves 21 routes. Each request and response body named below is one of the types in the next section.
| Method | Path | Auth | Summary |
|---|---|---|---|
POST |
/aws/lambda-microvms/runtime/v1/ready |
none (platform hook) | image-build readiness probe; answers 200 even before bootstrap, because the question is whether the daemon started. Every hook invocation is recorded and reported on /v1/health as hooks. |
POST |
/aws/lambda-microvms/runtime/v1/validate |
none (platform hook) | image-build validation probe; same reasoning as ready. Recorded on /v1/health’s hooks, which is the only trace this hook leaves — the platform writes no CloudWatch logs for it. |
POST |
/aws/lambda-microvms/runtime/v1/run |
none (platform hook) | one-shot token bootstrap, plus the optional launch environment. The platform wraps the caller’s string, so the payload’s own fields are one JSON parse deeper than the request body: {“runHookPayload”: “{"agent_token": "…", "env": {"KEY": "VALUE"}}”}. agent_token is required and non-empty; env is optional and becomes the BASE environment of every later exec, with each request’s own env overlaid on top of it. Values must be strings. Unknown keys are ignored, so a newer client can still bootstrap this daemon. The whole payload is capped by the platform at 4096 bytes inclusive, measured in UTF-8 bytes — the token and the env share that budget. env is installed only by the first successful bootstrap: a replay cannot edit it and a conflicting token cannot either. |
POST |
/aws/lambda-microvms/runtime/v1/suspend |
none (platform hook) | acknowledged and logged |
POST |
/aws/lambda-microvms/runtime/v1/resume |
none (platform hook) | acknowledged. Measured: the token, filesystem, exec records, and even backgrounded processes survive a suspend/resume cycle. What does not survive is the guest’s view of time, which jumps — so any timeout or lease held by a running command expires at once on resume. |
POST |
/aws/lambda-microvms/runtime/v1/terminate |
none (platform hook) | acknowledged after the terminate handler; process signals initiate shutdown |
POST |
/v1/exec/start |
bearer | start a command under a caller-minted exec_id. Idempotent on that id: a retry returns success without spawning a second child. poweroff_on_exit requests guest shutdown after the original group exits, requires daemon CAP_SYS_BOOT, and may race response delivery. |
GET |
/v1/exec/{id} |
bearer | poll status and output. Read-only: polling never mutates the entry, and output survives until an explicit ack. |
GET |
/v1/exec/{id}/stream |
bearer | follow output as Server-Sent Events from a byte offset |
POST |
/v1/exec/{id}/stdin |
bearer | write to a child’s stdin, or signal EOF. A separate request from the output stream on purpose: a dropped attach must not cost the ability to feed the process. EOF is explicit rather than inferred, because a child reading stdin cannot exit until the daemon drops its own handle. |
POST |
/v1/exec/{id}/ack |
bearer | release output and enter TTL collection. Only acked entries are ever collected, so output nobody read is never destroyed. |
POST |
/v1/exec/{id}/kill |
bearer | SIGTERM then SIGKILL to the whole process group, not just the direct child — a shell that backgrounded a server leaves the interesting process outside the child pid |
GET |
/v1/procs |
bearer | process accounting: every registered exec with its process group’s live pids, read from /proc so the guest needs no ps. child_exited beside a non-empty pids is a command that finished while something it backgrounded did not — the shape /v1/health’s busy cannot show, and the exec_id beside it is the one to pass to /v1/exec/{id}/kill. reap echoes the start request’s reap_group_on_exit. Entries whose pgid was never captured list no pids. Read-only. |
GET |
/v1/tcp |
bearer | upgrade to a WebSocket relayed to 127.0.0.1:<port> in the guest, so a caller outside the VM can speak an arbitrary TCP protocol to a server inside it. Binary frames carry bytes in both directions and a frame is a byte range rather than a message — TCP has no message boundaries and neither does this. The dial is loopback-only and never resolves a name: a relay that could reach another host would be an open proxy inside the VM reachable with the agent token. One connection per WebSocket, deliberately: multiplexing would reimplement per-stream ids, flow control, and close handshakes that the platform already provides per connection. Outcomes arrive as close codes — 4502 nothing listening, 4400 port 0, 4500 a mid-relay failure — because a WebSocket route leaves HTTP behind after its 101. Rests on a measured platform property: binary frames survive a port-scoped token byte-exact (docs/PLATFORM.md, 2026-08-29). With identity=1 the relay first completes a Noise KK handshake against the per-VM key delivered in the launch payload, which proves the far end is the launch pin under the bootstrap trust assumptions and makes every later frame ciphertext; 4401 means this VM was launched without a seed and 4403 means the handshake was refused. The handshake runs before the dial, so a refused caller never reaches a guest service. |
GET |
/v1/fs/file |
bearer | read one file, or a 1-based inclusive line range of it. ?start_line=&end_line= are the AI SDK harness’s readTextFile semantics: an end_line past the last line reads through EOF without error, and omitting both returns the whole file byte-identically. Still streamed — the range never buffers the file to slice it. |
PUT |
/v1/fs/file |
bearer | write one file. Deliberately not confined to a root: the same token authorizes exec, so a root prefix would add no security while breaking harnesses that write to home directories and /etc. |
GET |
/v1/fs/tar |
bearer | download a tree as tar. Symlinks are packed as symlinks, which is the producing half of what extraction accepts. |
PUT |
/v1/fs/tar |
bearer | upload and extract a tar under ?path=, confined to that root — the one confined write path, because member paths come from the archive rather than from the caller. Mirrors the CPython tarfile data filter: in-tree symlinks preserved, absolute link targets refused, relative targets resolved lexically so a symlink written earlier in the same archive cannot redirect a later member. |
GET |
/v1/discovery |
bearer | image configuration: the configured default exec user, or null for the daemon/root default; resolution still occurs for each request |
GET |
/v1/health |
none | liveness, daemon version, whether bootstrap has completed, and whether any exec is still running. busy exists so an orchestrator OUTSIDE the VM can hold it alive: the platform measures idleness by inbound traffic through the endpoint proxy, which terminates outside the guest, so a request from inside the guest cannot reset the idle timer. Polling this from outside is both the traffic and the decision. hooks lists the earliest hook requests kept in the daemon’s capped log, oldest first, with fired_at epoch seconds on the daemon’s clock. Later requests increment hooks_dropped without being stored or running a workload handler. Their routes still perform other work and return their normal status. Hook routes are unauthenticated and reachable over loopback from inside the guest, so entries show receipt rather than source identity. Later platform requests are subject to the same cap. microvm-poweroff-on-exit: 1 advertises shutdown support and the daemon’s CAP_SYS_BOOT privilege; 0 means unavailable. |
GET |
/v1/schema |
none | this document: every route, shape, status code, and operative limit |
3. Types
Section titled “3. Types”26 types under $defs, one subsection each, in the order the schema declares them. A type column that names another type links to its subsection.
3.1. Discovery
Section titled “3.1. Discovery”GET /v1/discovery: authenticated image configuration, not resolved identity.
A missing route means an older daemon; a present response with default_user: null means no image default. Resolution still happens for each exec request.
An object with 1 properties, 1 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
default_user |
string or null | yes | The image’s explicit default or captured numeric uid:gid, or None for root. |
3.2. DiskHealth
Section titled “3.2. DiskHealth”The disk half of Health.
An object with 3 properties, 3 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
available_bytes |
integer (uint64) |
yes | Bytes available to an unprivileged writer, from statvfs f_bavail. |
reserve_bytes |
integer (uint64) |
yes | Bytes that must stay free before a write is refused. Zero means the guard is disabled. |
under_pressure |
boolean | yes | Whether a write would be refused right now. Precomputed rather than left to the client, so every consumer applies the same comparison the write path does. |
3.3. ExitEvent
Section titled “3.3. ExitEvent”The terminal exit SSE event. Emitted before the stream ends, so a client that sees the body close without one knows the connection failed rather than the command finishing.
An object with 6 properties, 6 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
exit_code |
integer or null (int32) |
yes | |
offset |
integer (uint64) |
yes | Total bytes published, so a client can assert it saw all of them. |
signal |
integer or null (int32) |
yes | |
timed_out |
boolean | yes | The daemon execution deadline fired. Absent on older daemons, which cannot distinguish deadline expiry from other signal deaths. |
truncated |
boolean | yes | |
writers_may_be_alive |
boolean | yes |
3.4. FileReadQuery
Section titled “3.4. FileReadQuery”Query string for GET /v1/fs/file, which is FsQuery plus an optional line range.
A separate type rather than two more fields on FsQuery, because a line range means nothing on PUT /v1/fs/file or on either tar route and a shared type would publish it on all four. mode is absent here for the same reason pointing the other way: it is a property of a write.
The semantics are the AI SDK harness contract’s, verbatim, because that is the consumer: 1-based and inclusive on both ends, and an end_line past the file’s last line reads through EOF without an error. A caller asking for lines 1..1000 of a 12-line file gets the 12 lines and a 200, not a 416.
An object with 3 properties, 1 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
end_line |
integer or null (uint64) |
no | Last line to return, 1-based inclusive. Absent means through EOF. Past the last line is not an error: the read returns through EOF. A range ending before start_line is refused with 400, because it can only be a caller who computed one of the two wrong — there is no file for which it is the right question. |
path |
string | yes | |
start_line |
integer or null (uint64) |
no | First line to return, 1-based inclusive. Absent means 1. Zero is refused with 400 rather than treated as 1. A caller who sends 0 is working from a 0-based mental model, and silently reinterpreting it would hand back a window one line off from the one they will compute offsets against. |
3.5. FsQuery
Section titled “3.5. FsQuery”Query string for every route in this module.
path is required. A request missing it is 400, never 404: clients map 404 onto FileNotFoundError, so answering 404 for a protocol typo made a missing query key look like an absent artifact — that is how one defect hid for a full review round.
An object with 2 properties, 1 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
mode |
string or null | no | Octal mode for a written file, carried as a string so 0644 and 644 both parse and neither is read as decimal 644. |
path |
string | yes |
3.6. GapEvent
Section titled “3.6. GapEvent”One gap SSE event: the byte range a lagging or late subscriber lost.
An object with 2 properties, 2 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
from |
integer (uint64) |
yes | |
to |
integer (uint64) |
yes |
3.7. HandlerOutcome
Section titled “3.7. HandlerOutcome”What a workload’s handler for a lifecycle hook did.
The daemon runs <hooks dir>/<hook> when the image carries one. Output is not here: /v1/health is readable without the agent token, so a handler’s output goes to the daemon’s log instead.
An object with 5 properties, 4 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
duration_ms |
integer (uint64) |
yes | Wall-clock milliseconds from spawn to exit or kill. |
error |
string or null | no | Why the handler could not run at all: not executable, or spawn failed. |
exit_code |
integer or null (int32) |
yes | The handler’s exit code, or None when it was killed or never started. |
signal |
integer or null (int32) |
yes | The signal that ended it, if one did. |
timed_out |
boolean | yes | Whether the daemon killed it for exceeding its time budget. |
3.8. Health
Section titled “3.8. Health”GET /v1/health response.
An object with 11 properties, 11 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
bootstrapped |
boolean | yes | |
busy |
boolean | yes | Whether any exec is still running right now. Here so that an orchestrator outside the VM can decide whether to keep the VM alive, and the “outside” is the whole design. The platform measures idleness by inbound traffic through the endpoint proxy, and that proxy terminates outside the guest and forwards over loopback (measured; docs/PLATFORM.md, “The platform’s own hook arrives over loopback”). A request a guest process sends to the daemon’s own port never reaches the proxy, so no amount of in-guest traffic can reset the idle timer. A route that promised otherwise would be a keepalive that does not keep anything alive, discovered when a multi-hour run auto-suspends mid-work. What does reset it is a poll from outside, and this field is what makes such a poll informed rather than unconditional: the orchestrator polls, which is itself the inbound traffic, and reads whether the workload is busy to decide whether to keep polling. The assertion is therefore repeated and explicitly the caller’s, which is what the daemon self-keepaliving would not be — a hung process would then bill to the 8-hour ceiling with nobody asking. Computed from the exec registry rather than remembered: true iff at least one registered exec has not yet published a result. An exec that exited and is waiting to be acked is not busy — its output is being held, not produced — so an orchestrator does not keep a VM alive for a command that finished. #[serde(default)], unlike every field above it, and the asymmetry is not an oversight. The daemon is baked into an image while the client is installed separately, so a current client routinely talks to a daemon from whenever that image was built — and a required field would make health() fail outright against a daemon that predates it, turning a missing signal into an unreachable VM. False is also the right absence: a daemon that cannot say whether it is busy has not asserted that it is. |
disk |
DiskHealth or null |
yes | Free space on the daemon’s working filesystem, and the reserve it is judged against. Reported so disk pressure is something an orchestrator watches rather than something it discovers from a failed write. anthropics/claude-code#59856 filled two 10 GB disks to 100% with never-collected session directories and the first symptom was useradd: No space left on device — by which point every writer in the sandbox was already broken. A number on a health endpoint is what makes that curve visible while there is still time to act. None when free space could not be measured, which is deliberately distinct from zero: unmeasurable is not full, and a monitor that conflated them would page on a missing statvfs. |
execs |
integer (uint) |
yes | How many execs are registered, in any phase. Alongside busy because the two answer different questions and a monitor wants both: busy: false, execs: 0 is a fresh or drained VM, while busy: false, execs: 7 is a VM holding seven unacked results that somebody still has to collect. Terminating the second loses output nobody read. Defaulted for the same reason as busy: a client routinely talks to a daemon baked into an older image, and zero is the honest reading of a daemon that does not report a count. |
hooks |
array of HookObservation |
yes | Hook requests kept in the daemon’s capped log, oldest first. Each entry records the requested hook path and the daemon’s clock when it recorded the request. The platform writes no CloudWatch logs for the validate hook, so a kept entry provides a record of its receipt. Hook routes are unauthenticated and reachable over loopback from inside the guest. An entry shows receipt, not the request’s source identity. The log keeps its earliest entries and counts later requests in hooks_dropped without storing them, including later platform requests. Defaulted for busy’s reason: an older daemon omits the field, and an empty list is the honest reading of a daemon that reports no observations. |
hooks_dropped |
integer (uint64) |
yes | How many hook invocations were dropped once the log reached its cap. The log preserves its earliest entries. Later requests are counted here without being stored or running a workload handler. Their routes still perform other work and return their normal status. Defaulted like hooks: an older daemon omits it, and zero is the honest reading of a daemon that dropped nothing it could tell us about. |
identity_degraded |
boolean | yes | Whether any startup identity repair step failed. True means the VM is serving with a value from the shared image still in place — a duplicate machine-id or boot_id — which is a security-relevant condition an operator may want to drain the VM over, but is never a reason for the daemon to refuse to serve. |
identity_repaired |
boolean | yes | False when identity repair was switched off by config. Distinguished from a repair that ran and found nothing so a monitor can tell “opted out” from “nothing to do”. |
identity_steps |
array of IdentityStep |
yes | Each identity-repair step and its outcome, in the order they ran. Empty until repair runs, which is at the first successful run hook: repair at daemon start would run in the image-build VM and be captured by the snapshot, giving every VM the same “repaired” identity. identity_degraded says whether any step failed; this says which one and why. Defaulted for busy’s reason: an older daemon omits it. |
image_env_keys |
integer or null (uint) |
yes | How many variables the daemon’s image-environment snapshot holds, or None when it holds no snapshot. The snapshot is the environment the daemon inherited as the container CMD, minus every AGENTD_* variable, taken once at startup; a start request with inherit_image_env starts its child from it. A count and never the values: this route answers without the agent token, and an image’s ENV can hold anything its author put there. Defaulted for busy’s reason, and None is also what a daemon that predates inherit_image_env reads as, which is how a caller tells whether the flag will be honoured: such a daemon ignores the flag rather than refusing it. |
version |
string | yes |
3.9. HookObservation
Section titled “3.9. HookObservation”One lifecycle-hook invocation, as the daemon observed it.
A daemon-reported fact in the same trust class as an exec’s exit code: the daemon’s word about a request it served, carrying nothing the guest printed.
An object with 3 properties, 2 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
fired_at |
integer (uint64) |
yes | Seconds since the epoch on the daemon’s clock when the invocation arrived. The daemon’s clock rather than any caller’s, and recorded before the handler does any work — a run hook that was invoked and refused still fired. |
handler |
HandlerOutcome or null |
no | The workload handler’s outcome, when the image carries a handler for this hook. Absent on the wire otherwise, so an entry without one reads exactly as it did before handlers existed. |
hook |
string | yes | Which hook, spelled the way the platform’s routes spell it: ready, validate, run, suspend, resume, or terminate. |
3.10. IdentityStep
Section titled “3.10. IdentityStep”One identity-repair step, as the daemon ran it.
An object with 3 properties, 2 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
error |
string or null | no | The OS error for a failed step. Names a path or an errno, never a value. |
name |
string | yes | machine-id, hostname, boot-id, random-seed, or cached-credential. |
outcome |
string | yes | repaired, not_applicable, or failed. |
3.11. KillResponse
Section titled “3.11. KillResponse”POST /v1/exec/{id}/kill response.
A named type rather than the serde_json::json! literal this used to be: an ad-hoc Value has no schema to derive, so the one route whose body a client most needs to branch on — killed distinguishes “signalled” from “the group was already gone”, and both are 200 — would have been the one route the published document could not describe.
An object with 2 properties, 2 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
exec_id |
string | yes | |
killed |
boolean | yes | Whether a signal was actually delivered. false with a 200 means the process group had already exited, which is the outcome a kill wanted. |
3.12. NameOrId
Section titled “3.12. NameOrId”A user or group as a start request names it: a numeric id, or a name the daemon resolves against the guest’s /etc/passwd or /etc/group.
Untagged, so the wire carries a bare JSON integer or a bare JSON string and nothing else. An integer serializes exactly as the u32 this field used to be, which is what keeps a client that only ever sends ids byte-compatible with a daemon that predates names. A string sent to such a daemon is refused as malformed_request rather than misread.
A string that names no row and is all ASCII digits is read as that id, the way docker exec -u 1000 reads it: a harness that carries a uid as a string reaches the same account an integer would.
One of 2 JSON types, told apart by the type itself:
| Type | Meaning |
|---|---|
integer (uint32) |
A numeric uid or gid, applied as given. |
| string | A user or group name, resolved in the guest before anything is spawned. |
3.13. OutputEvent
Section titled “3.13. OutputEvent”One output SSE event.
An object with 3 properties, 3 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
offset |
integer (uint64) |
yes | |
output |
string | yes | |
stream |
StreamKind |
yes |
3.14. Phase
Section titled “3.14. Phase”Where an exec sits in its lifecycle. Mirrors ExecPhase in the model crate.
JsonSchema rides along with Serialize on every type from here down that crosses the wire. schemars reads the same #[serde(...)] attributes serde does, so the published schema describes what the daemon actually emits — the rename_all below is the reason this matters rather than a formality.
A string with 3 values:
| Value | Meaning |
|---|---|
running |
Child spawned, still running (or its pipes still held by a grandchild). |
exited |
Child exited and output is buffered and readable. |
acked |
Caller acked; output has been released and the entry awaits collection. |
3.15. PollResponse
Section titled “3.15. PollResponse”Captured output and exit status of a finished exec.
An object with 9 properties, 2 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
exec_id |
string | yes | |
exit_code |
integer or null (int32) |
no | Exit code, or None when the child died to a signal. |
phase |
Phase |
yes | |
signal |
integer or null (int32) |
no | Signal number that killed the child, when one did. |
stderr |
string | no | |
stdout |
string | no | |
timed_out |
boolean | no | The daemon execution deadline fired. Absent on older daemons, which cannot distinguish deadline expiry from other signal deaths. |
truncated |
boolean | no | Set when either stream hit max_output_bytes and was cut. An explicit flag rather than a sentinel string in the output: a marker inside the bytes is indistinguishable from output that happens to contain it. |
writers_may_be_alive |
boolean | no | Set when the post-exit linger deadline expired with the pipes still open, meaning some grandchild is alive and may write more that nobody will see. Reported rather than hidden, because a harness that sees empty output from a command it knows produced some needs to be able to tell why. |
3.16. ProcGroup
Section titled “3.16. ProcGroup”One entry of GET /v1/procs: an exec’s process group as the daemon sees it.
The exec registry knows the group (pgid) and whether the direct child has exited; /proc knows which pids are still in that group. Both halves are here because the interesting case is their disagreement — child_exited: true with a non-empty pids is a command that finished while something it started did not, which is exactly what a caller asking “did this step leave anything behind” needs to see and could not before.
An object with 6 properties, 6 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
child_exited |
boolean | yes | Whether the exec’s own child has exited. Read from the terminal marker an ack cannot take, so an acked exec still reads as exited here. |
exec_id |
string | yes | |
pgid |
integer or null (uint32) |
yes | The process group id captured at spawn. None when the child was reaped before it could be read, in which case pids is empty because there is no group to scan for. |
pids |
array of integer (uint32) |
yes | Live pids whose process group is pgid, read from /proc. Zombies are not live and are not listed. Empty once the group is gone, and empty for an entry whose pgid is None. |
reap |
boolean | yes | Whether the exec was started with reap_group_on_exit. |
started_at |
integer (uint64) |
yes | Seconds since the epoch on the daemon’s clock when the child was spawned. The same convention as a hook observation’s fired_at. |
3.17. ProcsResponse
Section titled “3.17. ProcsResponse”GET /v1/procs response.
An object with 1 properties, 1 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
procs |
array of ProcGroup |
yes | Every registered exec, in registry order, whether or not anything in its group is still alive. An exec that exited cleanly with nothing left behind is listed with child_exited: true and pids: []. |
3.18. RunHookEnvelope
Section titled “3.18. RunHookEnvelope”The envelope the platform posts to the run hook.
The runHookPayload string given to RunMicrovm is not delivered as the request body: the platform wraps it, so the body is {"runHookPayload": "<the caller's string>"} and the caller’s own JSON is one serde_json parse deeper. Measured 2026-08-05 — a daemon that reads agent_token from the top level answers 400, and the platform then terminates the VM with “Run lifecycle hook returned HTTP status 400” before any traffic is forwarded, so the mistake is invisible from the outside.
An object with 1 properties, 0 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
runHookPayload |
string or null | no |
3.19. Shell
Section titled “3.19. Shell”How a start request’s command is run: false (an argv), true (/bin/sh -c), or the name of a shell the daemon resolves and runs as <shell> -c.
Untagged for NameOrId’s reason: a boolean serializes exactly as the field always did, so an old daemon keeps accepting it, and a string sent to an old daemon is refused rather than misread.
One of 2 JSON types, told apart by the type itself:
| Type | Meaning |
|---|---|
| boolean | false execs command as an argv; true runs it under /bin/sh -c. |
| string | A shell name such as "bash" (resolved on the child’s PATH, the image’s PATH, /bin and /usr/bin) or an absolute path to one. |
3.20. StartRequest
Section titled “3.20. StartRequest”A start request. command is either an argv array or, with shell set, a single script string.
An object with 12 properties, 2 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
command |
array of string | yes | argv when shell is false, or the script when it is set. |
cwd |
string or null | no | Omitted means inherit the daemon’s working directory. See the module docs. |
env |
object | no | |
exec_id |
string | yes | Caller-minted idempotency key. Harbor retries, and a retry must not produce a second child. |
group |
NameOrId or null |
no | The group to demote to: a numeric gid, or a name resolved against /etc/group. A name the guest does not have answers 400 unknown_group. |
inherit_image_env |
boolean | no | Whether the child’s environment starts from the image’s ENV. Off by default, which keeps the property the daemon has always had: nothing from its own environment reaches a child. Set, the environment the daemon inherited as the container CMD, minus every AGENTD_* variable, is the lowest layer, under the passwd identity, the launch environment and env. The agent token is never in that snapshot: it arrives in the run hook, after the snapshot is taken. A daemon that predates this field ignores it, as serde ignores any unknown key; a caller that depends on it can read image_env_keys on GET /v1/health, which such a daemon does not report. |
poweroff_on_exit |
boolean | no | Power off the guest after this exec’s original process group has no live members. Off by default. Requires advertised daemon support and its CAP_SYS_BOOT privilege. Other work in the VM ends too; the start acknowledgment and exit record may be lost. Processes that leave the group are outside this condition. This does not keep an idle guest awake, and a terminated state does not establish when billing stops. |
reap_group_on_exit |
boolean | no | Whether to signal the exec’s whole process group once its own child exits. Off by default, and the default is the contract this daemon has always had: a backgrounded grandchild that inherited the output pipe keeps running and keeps writing, and the linger deadline is what bounds how long the daemon waits on it. That is the “backgrounded server keeps logging” guarantee, and callers rely on it. Set to true, the daemon runs the same SIGTERM-then- SIGKILL escalation POST /v1/exec/{id}/kill uses against the group as soon as the child’s exit is observed, so nothing the command left behind outlives it and the linger sees EOF rather than writers_may_be_alive. A timed-out exec is already escalated and is not escalated twice. |
shell |
Shell |
no | false, true (/bin/sh -c), or a shell name such as "bash". A name the guest cannot resolve answers 400 unknown_shell before anything is spawned. |
stdin |
boolean | no | Whether to give the child a writable stdin pipe. Defaults to false, which keeps Stdio::null(). Opt-in rather than always-on, and not only for tidiness: a child holding an open stdin pipe nobody will ever write to is a child that blocks forever the first time it reads. /bin/sh reading a script from stdin, git deciding it can prompt, any tool that probes for input — all of them behave differently against a pipe than against /dev/null. Every existing caller gets today’s behavior by not setting this. |
timeout_sec |
number or null (double) |
no | Wall-clock budget. Validated before the child spawns — the predecessor raised on a bad value inside the waiter thread, by which point the child was already running and became an orphan. |
user |
NameOrId or null |
no | The user to demote to: a numeric uid, or a name resolved against the guest’s /etc/passwd. Optional; omitted means the image’s AGENTD_DEFAULT_USER, resolved to the passwd row its name or uid matches, with that row’s primary gid, or the daemon’s own user when the image sets none. A name the guest does not have, or a default with no row, answers 400 unknown_user before anything is spawned. A user with a passwd row, however it was given, gets HOME, USER and LOGNAME from the row, beneath the launch environment and env. A user given by name also gets the row’s primary gid when group is omitted; a numeric uid keeps the daemon’s group, as it always did. |
3.21. StartResponse
Section titled “3.21. StartResponse”An object with 2 properties, 2 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
exec_id |
string | yes | |
phase |
Phase |
yes |
3.22. StdinRequest
Section titled “3.22. StdinRequest”POST /v1/exec/{id}/stdin body.
Both fields optional and both meaningful together: a final chunk plus EOF in one request is the common case for feeding a prompt, and forcing two round trips would leave a window where the child has the bytes but not the EOF that tells it the input is complete.
An object with 2 properties, 0 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
data_b64 |
string or null | no | Base64 so arbitrary bytes survive JSON. A JSON string cannot carry non-UTF-8, and stdin is bytes. |
signal |
string or null | no | "eof" closes the pipe after any data_b64 is written. Named rather than a bare boolean so the field has somewhere to grow. |
3.23. StdinResponse
Section titled “3.23. StdinResponse”POST /v1/exec/{id}/stdin response.
pub rather than private, like every other type in this module the schema route publishes: the generator names them by type, so a response shape that stays private is a shape a consumer cannot be told about.
An object with 3 properties, 3 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
eof |
boolean | yes | |
exec_id |
string | yes | |
written |
integer (uint) |
yes |
3.24. StreamKind
Section titled “3.24. StreamKind”Which pipe a streamed chunk came from. Both share one offset space, so a client holds one cursor rather than two that can disagree about ordering.
A string with 2 values: stdout, stderr.
3.25. StreamQuery
Section titled “3.25. StreamQuery”GET /v1/exec/{id}/stream query.
An object with 1 properties, 0 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
offset |
integer or null (uint64) |
no | Byte offset to resume from. Absent means 0, i.e. everything still in the replay window. |
3.26. TunnelQuery
Section titled “3.26. TunnelQuery”?port=<n>[&identity=true] — the guest port to relay to, and whether to prove identity.
An object with 2 properties, 1 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
identity |
boolean | no | Whether to run a Noise KK handshake before relaying any bytes. On this struct rather than in a second query type because one route has one query contract, and the schema is generated from exactly that: a separate type would need a second extractor and would not appear in docs/schema.json at all. Absent means false, which is what keeps every layer-2 client working unchanged. True against a VM launched without a seed is refused with [close::NO_IDENTITY] rather than silently downgraded — a caller who asked to verify identity and got an unverified tunnel would believe a proof it never received. See [super::identity] for what the handshake proves and its honest limit. |
port |
integer (uint16) |
yes | The guest port. Dialled on 127.0.0.1 only. 0 parses and is refused after the upgrade with [close::BAD_PORT], rather than rejected as a parse error: a caller who named it gets a reason naming the port instead of a 400 that could equally mean a missing parameter. |
4. Provenance
Section titled “4. Provenance”This page is generated from docs/schema.json, the daemon’s JSON schema, derived from its own serde types. This page reads the root fields, routes and $defs. site/scripts/gen-reference.mjs writes it into the site’s content directory on every pnpm run sync, so an edit made here is overwritten by the next run.
To change the page, change the source. Regenerate the source with mise run schema from the repository root; mise run schema:check fails when the committed file no longer matches what the binary emits.