On this page
Object store
Where does fleet state actually live, and who is allowed to write it? In one
canonical object plane, selected by STADO_CONFIG, where every write is
authorized by matching its key against a namespace's per-prefix action
policies.
What it is
The object store is the single durable plane behind the fleet: queue state,
capacity broadcasts, control flags, the registry document, autonomy records,
and release artifacts all live under fixed stado:// prefixes. The selected
STADO_CONFIG names the primary backend, replication, and release origin;
coordinator and agents use the same canonical prefixes regardless of backend.
There is no provider-derived storage fallback or direct client bucket path.
Writes cross an authenticated object boundary. The gateway resolves the
request's namespace from configuration, checks that the namespace's policy
allows this action on this key or list prefix, and then compares the bearer
against the namespace's Skarbiec-held token in constant time. A key no policy
covers — or an action the prefix does not allow — is refused with
401 {"error":"unauthorized or non-immutable release write"}, a sentence that
names neither the namespace, the prefix, nor the grant.
That refusal shape hides the cause: a writer whose prefix no namespace policy
declares is refused on every write while reads keep working, so status
commands keep printing as if nothing were wrong. Every autonomy object is
therefore rooted under state/ (state/autonomy/..., state/host_silence/).
If you add a new prefix, root it under a namespace some policy declares.
Who declares it
Operators, in the deployment configuration: the backend, the namespaces, and
each namespace's prefix/action allowlist. Writers are the coordinator, agents,
the autonomy layer, and authenticated submitters — each within its namespace's
policy. Reads under stado://releases/ are the one public, bearer-free
surface; publication there is authenticated and create-only.
Who observes it
Everything. The scheduler reads the queue window, the reaper reads capacity
and heartbeats, resolvers read registry snapshots, stado optimize status
reads autonomy records. Ordinary storage clients read the configured primary
first and may retry a primary error against the mirrored backup; a successful
primary absent answer does not fall through. The object API server and other
authority-sensitive paths instead use primary-only readers so stale replica
state cannot become an authority answer. In both modes, writes commit to the
primary and are then mirrored, and the backup is never promoted automatically.
A readable forecast still proves nothing about the write path; stado doctor
checks that the configured backend can be constructed, not that a later
mutation completed.
Canonical object uploads
The CLI and graphical clients use the same authenticated object API. For a namespace declared in your deployment, upload and download use:
stado storage put stado://example/artifacts/example.bin ./example.bin
stado storage get stado://example/artifacts/example.bin ./downloaded.bin
Replace example with the configured namespace. Larger uploads use the
existing chunk-and-compose protocol and SHA-256 verification. The public
proxy buffers each incoming upload request before forwarding it through
the Node runtime, rather than forwarding the request's live body stream.
The release proxy, object proxy and chunk composition share the public-DNS
origin connector, including the chunk reads, final write and chunk cleanup.
It preserves the configured hostname, TLS verification, bearer and conditional
headers; a missing public DNS answer is a request failure, never object absence.
An upload the transport ends is the transport's failure, never the caller's
request: a connection the object API's side resets or closes while a chunk's
body is still being sent (connection reset by peer, broken pipe, a stream
that ended before the response) is reported as object chunk PUT <index> of <count> (<bytes> bytes at offset <offset>) for upload <id> of <uri>: <cause>,
with our failure [infra_down]; retry later and the retryable exit status 69,
exactly as a refused connection is. Every HTTP failure the CLI reports takes
its code from its cause chain this way: an elapsed wait carries the
catalogue's code for it, any other operating-system failure, or a request or
body the peer ended, is infra_down; only an answer that could not be decoded
stays without a code. Running the same storage put again sends every chunk again under
the same upload id (the id is the object's digest); a chunk the store already
holds is answered as a conflict and kept, never written twice, and the final
composition is the one atomic publication of the object.
A failed request reports error: "object request failed" together with the
HTTP method, object uri, failed operation, and original cause. Failure
to read the incoming body returns HTTP 400. Failure while forwarding a request
or writing the composed object returns HTTP 503; it does not claim that the
host is unreachable merely because an upload failed. The server log retains
the same details and the original error.
An HTTP 200 from a read does not prove uploads work. Keep the completed write receipt and compare the downloaded bytes with the uploaded file.
CLI connection failures
When the native HTTP transport reports that it could not establish a connection,
the CLI classifies the failure as infra_down. It preserves the original cause
chain and requested URL. The classification comes from the transport's error
type, not from words in an exception or a command argument.
Object-store HTTP errors and registry snapshot reads carry this classification through database resolution, resolver commands and publisher host lookup. For a declared product and consumer, database resolution is a read-only way to observe that dependency:
stado database resolve "$PRODUCT" --consumer "$CONSUMER" --json
A connection failure is not an absent registry object, an HTTP authentication refusal or proof that the whole host is down. It says that the requested connection could not be established; the retained cause identifies what the transport observed. The failure uses the existing fleet failure code and retry exit policy. This classification adds neither a wait nor a retry loop.
The object API, the resolver and the local API all run in this machine's one
Stado process, and a unit that exits at start is restarted by its supervisor
without telling anyone. So every stado command that fails with infra_down
also reads this machine's Stado unit, and when its supervisor holds it loaded
with no running process and a nonzero last exit, prints after the error line:
this machine's com.wisent.stado runs no process: its last run exited <code> and its supervisor has started it <runs> time(s); its last log line: <line> (`stado service unit logs com.wisent.stado --host <this host>` reads more)
The log line is the last line of the unit's error log, or of its log when the
error log is empty: the same one-line window stado service status shows for a
failed unit. With --json the same sentence is the error document's
host_unit field, null when the unit runs, last exited cleanly or is not
loaded on this machine. A read of the unit that fails is said instead,
whether this machine's Stado unit is failing could not be read: <cause>;
it is never taken for a running unit. Stado Desktop shows the line in the
operation's complete receipt, which carries the command's standard error.
Where it lives
In the backend the selected STADO_CONFIG names. It can be self-hosted — a
Stado object API on one of your machines and its disk (stado, alias
self-hosted), or MinIO, Garage, SeaweedFS or Ceph that you run — or hosted
by Google Cloud Storage, Azure, Amazon S3, Cloudflare R2, Backblaze B2,
Wasabi, DigitalOcean, Hetzner, Scaleway, OVHcloud, Akamai, Vultr, Oracle, IBM,
Alibaba, Tencent, Huawei, Exoscale, IONOS, Contabo, UpCloud, Storj, Filebase,
Tigris, Yandex or IDrive e2, or any other S3 API endpoint (s3-compatible);
storage providers lists what each declares and
self-host your storage walks through running
your own. Stado also serves the S3 API itself over the same objects; see
Stado as an S3 provider. The canonical
layout (queue/, running/, status/, leases/, capacity/, control/,
registry.json, state/..., and the rest) is tabulated in the architecture
doc and is identical across backends. system/storage-layout.json is the
versioned layout marker. A process reads one store's marker before its first
use of that store, and at most once at a time: an opening that finds the
read in flight waits for its answer, a failure included, instead of sending
its own, so a burst of openings puts one request on the store, not one per
opening. A marker found valid is not read again by that process — it is
written only when absent and never replaced — and a failed read is not kept,
so the next opening reads again.
Commands
stado config show
stado config validate
stado doctor
stado storage
stado artifact list
stado storage moves queue state between backends (the billing-outage
migration path). Flags are in cli.
For the specialized source-preserving handoff between the fixed co-located
local roots, use stado repair stado --step storage-root; its run, resume, status,
rollback, and finalize boundaries are documented in
disaster recovery.
Inspecting every object
stado storage ls queue/ --json lists every object under that prefix, sorted
by name. There is no implicit display limit. Use --limit <count> to request
a bounded result; JSON reports the requested limit, listed, total, and
truncated. With no limit, limit is null. An explicit limit equal to the
total is not truncated. With no prefix, stado storage ls --json reports counts
across the canonical prefixes instead of object rows.
--size adds each object's size as the store's listing states it, which
every backend's listing carries, so no body is downloaded for it; size_from
is listing. Only an object the listing stated no size for, one removed
between the listing and its stat, has its body read (size_from is body):
a body that is gone is reported as vanished, and a failed read keeps
size_error, never a fabricated zero. A failed prefix listing is an error,
not an empty result.
Malformed --limit arguments are refused before storage is opened.
In Desktop, open a declared host's Space panel, then Available operations → List objects in the selected API's configured store. Leave Maximum objects blank for all rows. The operation reads the selected API's configured object store, not the selected host's disk. Its receipt retains arguments, output, errors and exit status.
The single-object verbs have Space panel operations of their own, each
running the same command with the fields its arguments take: List a
namespace's objects (stado storage objects), Read whether an object is
present, its size, version and metadata (stat), Print an object's
body (cat), Download an object into a local file (get), Upload a
local file as an object (put, with its media type and --if-absent),
Remove an object (rm) and Discard the staged parts of an interrupted
upload (abort-upload, with --dry-run).
Parallel object operations
storage copy, storage backup, and recovery migrate accept an optional
positive --concurrency. When omitted, Stado measures the operating system's
available parallelism. An unavailable measurement is refused with
cannot measure available parallelism for bulk object operations and the OS
cause; it does not silently select a worker count. Zero is rejected by the
argument parser. Recovery resolves this value before fencing a store, stopping
writers or opening a billing window, then reuses it for copy and verification.
The same measurement and error propagation apply to bulk queue reads,
release reports, host software inspection, storage verify, canonical prefix
counts and the opt-in storage ls --size body reads. A measurement failure is
an operation failure, not an empty listing or a vanished object.
Queue read batches also use the measured parallelism, rather than a separate
fixed batch size. Every batch is visited unless the caller's explicit job
window or scan budget has been filled; batching does not cap the result.
Desktop's Space operations also offer Copy objects between stores, Copy objects to the configured disaster-recovery store, and Compare store names, metadata and bytes. Copy and backup expose Objects in parallel; blank measures it. Mutations require the existing review confirmation. These requests and their complete failure receipts use the selected API.
Listing qualification
From the Stado checkout, run node tests/storage/listing/journey.mjs with
STADO_BIN, STADO_TEST_SOURCE_REVISION (full native source hash),
STADO_TEST_LIST_COUNT, STADO_TEST_LIST_LIMIT,
STADO_TEST_COPY_CONCURRENCY (positive integer), and
STADO_TEST_COPY_REFUSED_CONCURRENCY (zero) declared in the run's environment.
Use more than 255 objects to exercise the removed byte-width cap, more than the
host's measured parallelism to cross queue read batches, and a smaller positive
explicit listing limit. A separate run with listing limit zero exercises an
intentionally empty selection.
The journey snapshots the executable and refuses a mismatched source revision.
It submits real jobs into a private local store and compares every status --json row with the persisted job, across measured read batches. It checks
complete JSON and text object listings, explicit truncation, exact-boundary
listing, body lengths, invalid arguments and unchanged stored bytes. It copies
those objects to isolated local
destinations with measured and explicit parallelism, compares every persisted
body, runs storage verify, and confirms zero fan-out is refused before the
destination is opened. No worker is launched. Its executable identity,
commands, exit statuses and before/after digests are retained under
.build/storage-listing/.
Not to be confused with
- The registry — one versioned document inside the store, not the store itself.
- The release channel — immutable objects under
stado://releases/, publicly readable throughhttps://stado.wisent.com/api/release/object; copying release objects into a second "public" store is a broken release channel. - Skarbiec — the credential store. The object gateway reads its verifier tokens from Skarbiec; the object store itself holds no provider or product secret material.
Source: this website