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 |
1 |
daemon_version |
0.1.0 |
version_header |
microvms-agentd-version |
hook_prefix |
/aws/lambda-microvms/runtime/v1 |
2. Routes
Section titled “2. Routes”The daemon serves 20 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; begins graceful shutdown with in-flight requests draining |
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. |
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 this VM without trusting the endpoint proxy 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/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 every lifecycle hook the daemon observed, oldest first, with fired_at epoch seconds on the daemon’s clock, and hooks_dropped counts invocations past the cap. Hook routes are unauthenticated and loopback-reachable from inside the guest, so a hostile workload can forge ADDITIONAL entries — never remove or alter real ones; the cap keeps the earliest, which are the platform’s. |
GET |
/v1/schema |
none | this document: every route, shape, status code, and operative limit |
3. Types
Section titled “3. Types”21 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. DiskHealth
Section titled “3.1. 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.2. ExitEvent
Section titled “3.2. 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 5 properties, 5 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 | |
truncated |
boolean | yes | |
writers_may_be_alive |
boolean | yes |
3.3. FileReadQuery
Section titled “3.3. 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.4. FsQuery
Section titled “3.4. 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.5. GapEvent
Section titled “3.5. 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.6. Health
Section titled “3.6. Health”GET /v1/health response.
An object with 9 properties, 9 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 | Every lifecycle-hook invocation the daemon observed, oldest first. The platform writes no CloudWatch logs for the validate hook, so “did my validate hook even run?” has no answer anywhere else. Each entry is the daemon’s own observation — the hook path that was posted and the daemon’s clock when it arrived — never anything the guest printed. One trust caveat travels with this field and is stated rather than implied: the hook routes are unauthenticated and reachable over loopback from inside the guest, so a hostile workload can forge additional entries by posting the hook paths itself. It cannot remove or alter real ones — the daemon records before it responds and the log is append-capped, keeping the earliest entries — so the platform’s real firings are the front of the list and later spam is what the cap drops. 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. Non-zero means the list above is the earliest invocations only — the cap keeps first-N so a guest spamming the unauthenticated hook routes cannot grow daemon memory or push the platform’s real firings out of the record. 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”. |
version |
string | yes |
3.7. HookObservation
Section titled “3.7. 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 2 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. |
hook |
string | yes | Which hook, spelled the way the platform’s routes spell it: ready, validate, run, suspend, resume, or terminate. |
3.8. KillResponse
Section titled “3.8. 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.9. OutputEvent
Section titled “3.9. 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.10. Phase
Section titled “3.10. 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.11. PollResponse
Section titled “3.11. PollResponse”Captured output and exit status of a finished exec.
An object with 8 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 | |
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.12. ProcGroup
Section titled “3.12. 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.13. ProcsResponse
Section titled “3.13. 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.14. RunHookEnvelope
Section titled “3.14. 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.15. StartRequest
Section titled “3.15. StartRequest”A start request. command is either an argv array or, with shell: true, a single script string.
An object with 10 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 true. |
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 |
integer or null (uint32) |
no | |
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 |
boolean | no | |
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 |
integer or null (uint32) |
no | Numeric uid to demote to. Optional; omitted means run as the daemon’s own user. |
3.16. StartResponse
Section titled “3.16. StartResponse”An object with 2 properties, 2 of them required.
| Property | Type | Required | Description |
|---|---|---|---|
exec_id |
string | yes | |
phase |
Phase |
yes |
3.17. StdinRequest
Section titled “3.17. 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.18. StdinResponse
Section titled “3.18. 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.19. StreamKind
Section titled “3.19. 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.20. StreamQuery
Section titled “3.20. 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.21. TunnelQuery
Section titled “3.21. 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.