On this page
Machine Interface
You are integrating a program, not an operator: how does it submit a job,
watch it, cancel it, and collect its artifacts without parsing text meant for
humans? stado machine is that surface. Its own help calls it the "Stable
JSON machine interface": every command prints exactly one JSON envelope line
on stdout, and stderr stays clean for automation. Flag-by-flag detail lives in
the cli reference; job semantics live in jobs and
primitives/job.
The envelope
Every invocation ends the same way, regardless of subcommand:
- Success:
{"schema_version":1,"ok":true,"result":...}and exit 0. - Failure:
{"schema_version":1,"ok":false,"error":{"code":...,"message":...,"retryable":...}}and exit 1.
The envelope is serialized as canonical JSON — sorted keys, compact
separators, non-ASCII preserved. A program therefore parses the single stdout
line, branches on ok, and on failure branches on error.code plus the
retryable flag; it never needs to scrape stderr or interpret prose.
Error codes are part of the contract. The set includes INVALID_REQUEST,
IDEMPOTENCY_CONFLICT, NOT_FOUND, INVALID_CURSOR, NOT_TERMINAL,
ARTIFACT_SECURITY, INVALID_SOURCE_ARCHIVE, NO_ARTIFACTS, LOG_REAPED,
CANCEL_FAILED, and SERVICE_DIRECTORY_STALE.
Unexpected storage, IO, or JSON failures map to code INTERNAL with
retryable false.
The five commands
| Command | The question it answers |
|---|---|
stado machine submit --request-file FILE |
Run this request exactly once, even if I retry this call. |
stado machine status JOB_ID |
What state is this one job in, read directly by ID? |
stado machine logs JOB_ID |
Give me the next page of the canonical command log, from a byte cursor. |
stado machine cancel JOB_ID |
Stop this job, durably, and tell me the same thing if I ask twice. |
stado machine artifacts --output-dir DIR JOB_ID |
Download this terminal job's canonical artifacts, verified. |
Submit is idempotent by construction
submit is idempotent, its source archive is bounded, a directory can be packed into one, and source staging has a real qualification: Submitting through the machine interface.
Source archive bounds
The bounds a submitted source archive is held to: Submitting through the machine interface.
Status and logs
status looks the job up directly by ID across the canonical state prefixes
(queue, running, completed, uploaded, failed, cancelled) and
returns it under result.job. Once every job of a run has ended, the run is
reaped: each job is retained as it ended in runs/<run>.json and its own
documents and log are deleted; status then reads the job back from that
retained outcome, stamped with the prefix it ended in. Retention also writes
runs/jobs/<job> naming the job's run, so this read opens that one manifest
and no other — the czekam lines on stderr (below) name each manifest lock
the read took, which is how the real test in tests/machine/ counts them;
jobs retained before that index existed have no entry, so the first read that
misses walks every run once, indexes every job each run retains and writes
runs/retained-jobs-indexed. From then on a job the index does not name was
retained by no run, since every retention writes its own entry, and no read
walks the runs again: a miss, a job still running included, costs one index
read and one marker read. An ID found in neither place is NOT_FOUND.
stado status <id> reads a job the same way: a whole ID directly, then from
its retained outcome; the first hex characters of an ID, with or without
job-, list every job they begin, in the queue and — through the index —
among the reaped. A row read from a retained outcome carries (reaped), and
--json prints each row as the job's record with its state and reaped.
An ID or start no job holds is refused by name: no job with id <id> in the queue or in any run's retained outcomes; stado status lists the jobs the queue holds. Stado Desktop's Queue screen reads one job by its ID through
the same command.
The normalized job includes instance_ref, allocation_kind,
worker_allocation, started_at, restarts, submitter_restarts and
last_restart. allocation_kind: agent means the reference uses local@<host>;
it does not mean that the physical host is local. A cloud worker uses the same
agent transport. Requested provider, machine_type, region and purchase
preferences are not observed placement or hardware measurements.
Before claiming work, the agent records its host, role kind, identity observation
time, native resource or identity-read error in worker_allocation. AWS resource
identity contains account, region and instance ID. GCP identity contains project,
zone, name and numeric instance generation. An explicit local-policy resource
selects local pricing but cannot prove a physical VM's identity or removal.
Azure native worker identity is unavailable in this revision; missing identity
remains an error rather than a guessed resource. Older unobserved jobs retain
null. See Costs for quote matching.
started_at identifies the claimed execution. A restart counter can increase
when a restart is refused; it is not sufficient evidence that another execution
ran. Consumers bind hardware, ports, cost and cleanup receipts to the job and
execution, and invalidate them when that identity changes.
logs reads the canonical command log from a byte offset, to the end:
stado machine logs --cursor 0 "$JOB_ID"
--limit N asks for at most N bytes instead; without it no page size is
assumed. The result carries job_id, cursor, next_cursor, eof, and
text. cursor is a byte offset into the log, next_cursor is where the next
read starts, and eof is true when the read reached the current end of the
log — so a poller carries next_cursor forward and never re-reads bytes it
already has. A negative cursor, a limit of zero or less, or a cursor beyond the
end of the log is INVALID_CURSOR.
A job whose run was reaped has no log any more: logs answers LOG_REAPED
and points at status, which still carries the job as it ended.
Cancel is durable and idempotent
stado machine cancel "$JOB_ID"
The cancellation marker cancellations/<job_id>.json is written first. The
claim path checks that fence before and after its queue transition, and the
restart path refuses a cancelled job. Cancellation captures one coherent job
snapshot's instance reference, execution and worker identity before ownership
can be cleared. A conflicting captured execution refuses the transition instead
of replacing its evidence. Repeating cancellation retains the same fence.
Agent references, including cloud agents using local@<host>, never authorize
deleting the shared worker VM. The worker stops its owned process and the
workload must publish its own cleanup evidence. A historical native reference
alone also grants no deletion authority. Dedicated Boat sandbox cleanup requires a
current lease matching the job, provider and resource; an already released or
absent terminal lease is not a new deletion request. This retains legitimate
lease-owned cleanup without treating a terminal job as ownership of a shared VM.
For a terminal cancelled execution, provider_cleanup in an observed status
can report a read-only observation of the captured physical resource. It retains
operation, allocation, observed_at, removed, current resource state,
error and native evidence. AWS verifies the credential's actual account and
region before reading the instance. GCP verifies project, zone, name and
generation; Azure verifies subscription, full resource ID and VM generation.
A same-name replacement with a different generation means the old resource is
gone, not that the replacement should be deleted. A stopped VM is not removed.
A missing identity, scope mismatch, denied read or unsupported provider leaves
removed unknown and retains its cause. An unscoped not-found response is not
proof. Cancellation acknowledgement or terminal: true alone never certifies
physical workload cleanup. Applications must inspect their own execution-bound
cleanup receipt or validate the scoped removal evidence before releasing
ownership or allowing a new launch. Stado Desktop's Earning → Provider prices
allocation report shows the same worker origin, removal result and complete
command receipt; it does not calculate a separate cleanup verdict.
Real cancellation-observation qualification
The reusable source is tests/cancellation/ in the Stado repository:
cargo test --manifest-path stado-rs/Cargo.toml --test service-journeys cancellation:: -- --nocapture
STADO_CANCELLATION_QUALIFICATION_CONFIG names an authorized, isolated profile.
Declare terminal, cancellation-fenced fixture jobs with
STADO_CANCELLATION_REMOVED_JOB_ID, STADO_CANCELLATION_STOPPED_JOB_ID,
STADO_CANCELLATION_REPLACED_JOB_ID and STADO_CANCELLATION_UNOBSERVED_JOB_ID.
Their normal Stado lifecycles must have produced, respectively, a removed VM
generation, a retained stopped agent VM, a same-name GCP/Azure replacement and
an actual worker-identity read failure. Never seed job documents, use production
resources or restore the intentionally detached historical GCP billing.
The journey reads real CLI and native-API observations, checks scope and captured
execution, repeats cancellation for only the declared stopped/replaced terminal
fixtures, then reads back that their retained resource remains. Missing identity
must retain an unknown removal result and its cause. It retains source revision,
source patch, binary digest, commands, statuses and receipts under
.build/desktop-api/. Missing dependencies are blocked, not passed.
Artifacts are verified on the way down
stado machine artifacts --output-dir ./out "$JOB_ID"
artifacts downloads the canonical status/<id>/output/ blobs of a terminal
job; a non-terminal job is refused with NOT_TERMINAL. Every blob is hashed
while streaming to disk and reported with its size and SHA-256. Output-path
and storage-path symlink and escape rules are enforced; a violation is
refused with ARTIFACT_SECURITY rather than written.
What a command is waiting on
While a command waits it says on stderr what it waits on, each wait is registered so another command can list it, and a request that would stand behind the vault is refused at once: What a machine-interface command is waiting on.
A request that would stand behind the vault is refused with the line
The refusal and the vault state the API reports are in What a machine-interface command is waiting on.
Stability
The envelope is versioned: schema_version is 1, and the error codes above
are stable identifiers a program may branch on. The compatibility policy:
- Within one
schema_version, existing fields keep their name, type, and meaning and are never removed. Changes ship as new optional fields, so a consumer must ignore fields it does not recognize. - An error code is never reused for a different condition. New codes may
appear; a consumer must treat an unknown code as a non-retryable failure
unless the envelope's
retryableflag says otherwise. schema_versionincrements on any breaking change. The CLI emits exactly one schema version per binary — there is no flag to request an older one — so a consumer must refuse an envelope whoseschema_versionit does not know rather than guess.
Source: this website