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 retryable flag says otherwise.
  • schema_version increments 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 whose schema_version it does not know rather than guess.

Source: this website