Fleets
docs/FLEETS.md · the same file GitHub renders ·Edit on GitHub ·Markdown
One service CLI can supervise installed HTTP emulators, PostgreSQL wire servers and Redis RESP servers. Every listener starts before readiness is published; a boot failure names the service, closes listeners that already started, and exits nonzero. SIGINT/SIGTERM closes the whole fleet.
{
"log": "off",
"control": { "port": 0 },
"services": {
"stripe": { "port": 0, "seed": "fixture" },
"postgres": {
"protocol": "postgres", "port": 0, "database": "app",
"namespaces": { "worker-7": "app", "other": "app" }
},
"redis": {
"protocol": "redis", "port": 0,
"namespaces": { "worker-7": 0, "other": 0 }
}
}
}mockingbird-stripe serve --config mockingbird.json --ready-file ready.json --ready-jsonSet top-level adminControls to boot HTTP services and protocol clocks with the same
admin controls (clock, latency, faults, a starting snapshot); a service
entry's own adminControls replaces the keys it names. --admin-controls, --clock,
--freeze, --latency, --fault and MOCKINGBIRD_ADMIN_CONTROLS apply on top of the file.
A document that cannot be read stops the fleet before any listener opens, and names the service.
Two more keys boot HTTP services apart from one run to the next. idBase (a non-negative integer
or "boot", top-level for every service or on one entry, which replaces the fleet's) starts
generated ids at a base so a restart does not reissue the last boot's ids; --id-base and
MOCKINGBIRD_ID_BASE fill in a top-level one the file leaves out. A service entry's settings
object is that emulator's own boot settings, the same document serve --settings takes, checked by
the emulator's schema before any listener opens (see Boot).
{
"adminControls": { "clock": { "now": "2030-01-01T00:00:00Z", "frozen": true } },
"services": {
"stripe": { "port": 0, "seed": "fixture", "adminControls": { "latency": "40ms" } },
"resend": { "port": 0 }
}
}Set top-level adminPrefix (default /__admin) to move the aggregate API and all
HTTP children together. A service entry may override adminPrefix. CLI
--admin-prefix and MOCKINGBIRD_ADMIN_PREFIX also apply; ready manifests always
report the actual URLs. Aggregate controls are under <adminPrefix>/fleet, and
readiness is <adminPrefix>/health. Old /__fleet and /health routes are removed.
HTTP namespace selection uses x-mockingbird-namespace or /__admin/ns/{name}. Protocol namespaces
must be declared in the config; each gets its own engine and listener. Use the URL from
services[name].namespaces.endpoints[namespace]. This isolates scripts, Pub/Sub, faults and
clocks as well as records. Within one Redis endpoint, SELECT retains Redis's logical-database
semantics: keys are partitioned, but scripts, Pub/Sub and time belong to the server.
The aggregate /__admin/health endpoint reports processReady, protocolReady and status per child.
If any child stops, it returns 503. The control listener defaults to a loopback ephemeral port.
Endpoint discovery
Provider entries can also carry their synthetic fixture object and advertised public URL:
{
"services": {
"google": {
"port": 0,
"baseUrl": "https://preview.example.test/google",
"fixtures": { "users": [{ "email": "fixture@example.test" }] }
},
"vercel": { "port": 0, "fixtures": { "projects": [{ "name": "fixture-app" }] } }
}
}This applies to the added providers and expanded GitHub, Slack, Resend and Twilio services.
Their options.fixtures may instead name a JSON file, relative to the command's working
directory; that file takes precedence over the inline object. options.base-url overrides
baseUrl. Fixture parsing and public URL validation happen before the child listener opens.
Reset replays fixtures; restoring a snapshot uses its stored state. baseUrl controls URLs
advertised by the provider, while the endpoint manifest reports the actual local listener.
--ready-json emits one version-1 ready record with pid, a unique id, startedAt, aggregate
healthUrl/adminBase, and every child's actual protocol URL, health URL, admin URL and namespace
mechanism. The ready file contains the same JSON. It is published by rename after all children
are healthy, and removed on shutdown only if it still belongs to this process. Starting with
an existing ready-file path invalidates the earlier artifact before boot. Give concurrent fleets
different paths.
Public URLs omit passwords. --connections-file private.json writes credential-bearing default and namespace
connection URLs to a separate file, atomically and with mode 0600. Keep configuration and this
private payload outside tracked files. PostgreSQL accepts user, password and database;
Redis accepts password. Protocol seeds are numeric. An admin key belongs in the config's
adminKey field or MOCKINGBIRD_ADMIN_KEY; it is never part of discovery.
Fleet controls
Call these paths under the manifest's adminBase. Every control request requires
x-mockingbird-admin-key when a fleet admin key is configured.
| Method | Path relative to adminBase |
Behavior |
|---|---|---|
| POST | /namespaces/{name}/reset |
Reset records, namespace faults, clock and pending work across children. |
| POST | /namespaces/{name}/snapshots |
Capture an opaque in-process checkpoint; optional label and services list. |
| POST | /namespaces/{name}/snapshots/{id}/restore |
Restore every child captured by that checkpoint, including deterministic random state. |
| GET/POST | /clock?namespace={name} |
Read or update logical time with set, freeze, and advance (milliseconds or e.g. "1h"). |
| GET | /metrics?namespace={name} |
Per-child unmatched requests, active requests/connections, blocked commands, jobs and webhooks. |
| POST | /wait-until-idle?namespace={name} |
Return 200 when idle, otherwise 409 with exact per-child diagnostics. |
Reset and clock operations accept an optional services list. Fleet mutations fence new work,
checkpoint each child, then apply the operation. A rejection returns the failed service and each
rollback result, including rolledBack: false if any rollback failed. Only a completed operation
returns global success. Opaque checkpoints belong to one namespace and the current fleet process. Export a portable archive for process replacement.
Active HTTP requests, pending webhooks, active BullMQ jobs, blocked Redis commands, or live PostgreSQL sessions
cause a precise 409 conflict. Close workers and PostgreSQL pools before checkpointing/resetting;
PostgreSQL sessions can retain transactions and prepared statements even when apparently idle.
Queued and delayed Redis jobs are checkpointable state; /wait-until-idle still counts them.
HTTP clocks and random streams are independent per namespace in a fleet. Reset/restore of one
namespace leaves the others unchanged.
BullMQ timestamps and worker timers also read the client process's clock. For deterministic
delay/backoff tests, inject the same clock in the client test harness and Redis, then use public
Worker.getNextJob/Job APIs. Advancing the server clock cannot change Date.now() in a separate
consumer process.
Management discovery and errors
GET <adminBase> returns capability contract version 1, routes/methods, per-child protocol,
package/codec versions, portable snapshot and provisioning support, supported budgets and limits.
The readiness document includes capabilityVersion: 1. Feature-detect this contract rather than
assuming a package version supplies a route. GET <adminPrefix>/ui serves the authenticated fleet
console with child links, namespace actions and operation/rollback results. Supply the admin key
as an HTTP header when loading the console and in its key field for control calls.
Fleet failures use {error:{type:"mockingbird_admin",code,message}}, with namespace, services,
failedService, rollback, rolledBack and operationId alongside it where applicable. This
replaces the former string error. Invalid documents/encoding return 400, missing namespaces or
checkpoints 404, conflicting/active work 409, and failed transactions 500. Unknown control fields,
non-string or over-256-character labels, and service selection on checkpoint restore are rejected
before any checkpoint/mutation. Restore always covers the checkpoint's recorded services.
Numeric protocol seeds must be finite. Ready/private output destinations must differ after path
and symlink resolution. Common configuration, policies, target loading and control documents are preflighted before any listener starts; service factory failures close all started listeners.
Startup failures retain their cause and install remediation. Shutdown attempts every child and
returns a failing exit status if any child fails to close.
Namespaces, quiescence and generations
| Method | Path relative to adminBase |
Body / result |
|---|---|---|
| GET | /namespaces |
Namespace generations, lifecycle, creation/activity/expiry times and pin state. |
| POST | /namespaces/{name} |
{} or {baseline:"checkpoint-id"}; provision isolated HTTP/PG/Redis state and return updated discovery and private connections. |
| DELETE | /namespaces/{name} |
Refuse active work or clients, delete children and release listeners; roll back partial deletion. The default and pinned namespaces cannot be deleted. |
| POST | /namespaces/{name}/policy |
{pinned:true} protects a namespace from cleanup/deletion. |
| POST | /can-checkpoint?namespace={name} |
{services?:[...],timeoutMs?:0..30000}; read-only admission probe. |
Provisioning reserves capacity before asynchronous work; a partial failure cleans up created children. Explicit operations for alpha and beta run concurrently. Conflicting operations return 409 and the active operation identity. All-namespace capture/restore coordinates an environment fence across selected children. An incomplete rollback keeps the affected namespace fenced; use an explicit reset or checkpoint/archive restore to recover it. Other namespaces stay available.
/wait-until-idle evaluates only selected children and counts pending jobs. /can-checkpoint
uses the same active-request/session, blocked-command, active-job and pending-webhook rules as
snapshot admission. Delayed/queued Redis jobs can be checkpoint-ready while not idle. The probe
creates no checkpoint and does not advance time. HTTP responses count as active until their
bodies finish or are cancelled, including event streams. Webhook counts are namespace-local and
count an active delivery once.
Namespace generations change after successful reset, restore and provisioning, including a repeat
restore of the same archive. They never rewind with saved data. HTTP responses advertise
x-mockingbird-generation; an HTTP write supplying an old generation receives 409. Protocol
clients must reconnect/resynchronize using discovery after replacement. Failed operations do not
advance the generation. Aggregate clocks start from shared/per-namespace boot controls; empty
clock documents preserve every child clock. Relative advances preserve intentional child offsets.
Reset returns children and the aggregate reference clock to their configured boot baseline.
Checkpoints and portable archives
GET /namespaces/{name}/snapshots lists checkpoint ids, creation times, labels, captured service
inventory and portability. GET and DELETE the collection's /{id} inspect/delete one. Retention
is configured programmatically with retention:{maxCount,ttlMs}; defaults are 50 checkpoints per
fleet and 24 hours. Expiry is swept on control requests; count eviction is oldest first across the
fleet. Dropping a checkpoint releases its opaque handles for garbage collection.
| Method | Path | Body |
|---|---|---|
| POST | /archives/export?namespace={name} |
{services?:[...],allNamespaces?:true} |
| POST | /archives/import |
{archive:<document>,namespace?:"one-captured-namespace"} |
| POST | /archives/inspect |
{archive:<document>} |
| POST | /archives/diff |
{left:<document>,right:<document>,namespace?,service?,values?:false,limit?:100} |
mockingbird.fleet/v1 archives carry source/creation metadata, namespace clocks, service protocol
and codec version, package provenance and a SHA-256 digest of canonical content. HTTP state carries
records, sequences, random/control streams, latency/faults and quiescent webhook endpoints/history.
PostgreSQL carries encoded PGMM snapshots for every database, PRNG state, faults and clock. Redis
uses a tagged JSON representation for binary values, Maps and bigint stream ids, including scripts,
queued/delayed work and consumer pause state. Archives contain private data/configuration: store
them privately. Unfinished outbound webhook deliveries and live protocol sessions must be drained.
Imports validate integrity, namespace/service/codec support and decode every child before changing state. They create absent namespaces in a fresh fleet, restore selected state transactionally, and report rollback failures. Unsupported codecs/services and corrupt archives are refused. Compatibility is determined by archive and child codec versions; package versions are provenance rather than an assumption of binary compatibility. Keep independent backups before replacing a running fleet. Inspection/diff is read-only, versioned and bounded; raw HTTP/Redis values are opt-in. Raw PostgreSQL diff is explicitly unsupported; use bounded inspection for row values. HTTP record identifiers, PostgreSQL table/record hashes, Redis key identifiers, clock/control changes and protocol metadata changes are compared without restoring either side.
Operations, events and retry safety
Mutations return x-mockingbird-operation-id. GET /operations?namespace={name} and
GET /operations/{id}?namespace={name} expose phase, measured completed/total child work,
terminal status and bounded result metadata. POST /operations/{id}/cancel requests cancellation
before commit; committed work is never described as cancelled. Retain at most 200 operations per
process. Checkpointing, committing, verification, resume and rollback are distinct phases.
Send Idempotency-Key on retried mutations. The key is bound to caller, namespace, route, method
and canonical body. Simultaneous/identical retries receive the original result and operation id;
conflicting reuse receives 409. Retention is one host-clock hour, at most 1000 keys; a restart
clears this in-memory retry history. Persist archive identities externally for recovery across
process replacement.
GET /events?namespace={name} is an SSE stream with ids containing the fleet process id and a
sequence. Reconnect with Last-Event-ID to replay up to 500 retained events for that namespace.
A missing/expired/other-process cursor emits resync with the current generation/lifecycle.
memberStatus:true enables an anonymous namespace-only projection of operation id, phase, status
and progress; it omits administrative results, configuration and other namespaces.
GET <adminPrefix>/live reports controller liveness throughout initialization, maintenance and
shutdown. Readiness returns 503 with lifecycle and affected operation/fence scopes during planned
work, rather than misreporting a protocol fence as an unexpected child failure. Startup verifies
actual protocol handshakes; once admission policies are installed, readiness checks listener
availability without consuming namespace connection quotas.
Workload and startup hooks
The programmatic startFleet(config, options) accepts workload:{prepare,verify,resume}. Each
async hook receives {namespace,services,signal,operationId}. Prepare should pause application
admission/workers and close pools. Verification runs after child mutations; resume runs only for
the selected namespace. Hook deadlines default to 30 seconds (hookTimeoutMs). A failed drain
or mutation triggers safe compensation; failed rollback or resume retains the recovery fence.
Observe the abort signal and make application-side hooks idempotent.
initialize({manifest,connections,headers,signal}) runs asynchronously after listeners bind and
before readiness/private artifacts or the existing synchronous onReady callback are published.
Use its discovered HTTP endpoints with the supplied temporary initializer header. For protocols,
use its private connections: temporary loopback listeners share the actual namespace engines
while ordinary listeners remain gated. Temporary listeners close before readiness. Initialization
is bounded by initializationTimeoutMs (default 30 seconds); failure closes the fleet with its
original cause. PostgreSQL initialization listeners select the namespace's configured database.
beforeClose({manifest,signal,capture}) is a final backup/acknowledgement gate, with
shutdownTimeoutMs (default 30 seconds). Status remains readable and vendor admission is blocked
until it completes. Failure defaults to aborting shutdown and leaving a failed fenced environment;
a repeated close retries the hook. shutdownFailure:"close" instead attempts all cleanup and
still rejects/reports failure. SIGKILL, crashes and host loss cannot guarantee a final backup.
Profiles and reusable scenarios
profiles:{name:{service:controls}} registers named cross-service profiles.
POST /profiles/apply?namespace={name} accepts {name} or {profile:{service:controls}}.
The entire profile is normalized before mutation; protocol children accept clock controls only.
Unsupported latency/fault controls are rejected rather than skipped. Partial failures restore all
captured children. POST /profiles/baseline accepts optional services and clears HTTP faults and
latency, leaving data intact. Use namespace reset for a full configured boot baseline.
scenarios registers bundles with format:"mockingbird.scenario/v1", positive version, service
protocol requirements, optional deterministic seed/clock, per-service controls/fixtures, and
an optional async seedApplication hook. The bundle seed applies to HTTP random streams; protocol engines retain their configured numeric seeds. Fixtures are portable HTTP snapshot record arrays.
GET /scenarios describes available bundles; POST /scenarios/{name}/apply?namespace={customer}
applies one transactionally. Incompatible services are rejected before mutation. Application seed
hooks receive the workload context, private namespace connections, manifest and temporary HTTP headers. Only the bundle's selected services are exposed through private seed access. Hooks must honor the deadline/signal; an uncertain application seed failure retains the recovery fence.
Backups, quotas and expiry
The public fleet entry point exports an ArchiveRepository interface (put/get/list/delete/latest/publishLatest),
filesystemRepository(directory,maxBytes?) and s3Repository({endpoint,bucket,region,accessKeyId, secretAccessKey,sessionToken?,prefix?,pathStyle?,maxBytes?,fetch?}). Explicit S3 storage credentials
are separate from any captured application S3 emulator. Path-style endpoints default on; use
pathStyle:false for virtual-hosted buckets. Signed requests use the S3 general-purpose bucket
API. Transfers are bounded (64 MiB default), immutable objects use archive digest identities, and
latest publication follows a verified read-back. Interrupted/corrupt uploads never advance it.
Filesystem objects/pointers are atomically published with private permissions. Latest archives
cannot be deleted until a newer verified pointer replaces them.
Configure repository and backupPolicy:{intervalMs,namespaces?,keep?} to enable scheduled verified
backups on real host elapsed time, independent of a frozen emulator clock. Runs never overlap;
an overlapping interval is skipped. GET /backups reports last attempt, last verified success,
next run, failures and retained ids. POST /backups captures one namespace, or accepts
{allNamespaces:true}. Scheduler history is process-local; repositories/latest pointers survive
restart. Keep protects recent repository objects after a verified publication.
limits:{maxNamespaces,maxListeners,connections} bounds namespace reservations, listener count
(including the controller), and namespace protocol connections/HTTP active admissions. Discovery
reports usage, remaining namespace capacity and unsupported budgets. New protocol connections
are closed at the quota boundary; existing clients continue. Object-byte, record and pending-job
budgets are explicitly unsupported and rejected when configured. Deleting a namespace releases
capacity. No limits are enabled by default.
expiry:{idleMs,intervalMs?,finalBackup?} opts into host-clock idle cleanup. GET /cleanup previews
candidates and pin/expiry state; POST /cleanup executes eligible cleanup. Requests/connections
refresh host activity; active clients/work, operations, recovery fences and pins prevent deletion.
finalBackup:true requires a repository and a verified final upload before retirement. Failed
uploads leave the namespace present for a later retry. Default cleanup cadence is 60 seconds.
Client, configuration rendering and preflight tools
The supported @crvouga/mockingbird-service-stripe/fleet entry point exports FleetClient, FleetClientError, renderConnections, writeConnections,
doctorFleet and rehearseFleet. Use the discovered adminBase, including a custom prefix, and
pass keys in client options/headers. Namespace-specific private URL mappings must come from actual
discovery. Explicit variable mappings avoid guessing application variable names:
const values = renderConnections(publicManifest, privateManifest, "synthetic-customer", {
DATABASE_URL: { service: "postgres" },
REDIS_URL: { service: "redis" },
STRIPE_BASE_URL: { service: "stripe" },
})
await writeConnections("app.env", values, "dotenv")Missing services/namespaces are refused; HTTP URLs include the namespace base path.
writeConnections atomically publishes JSON/dotenv with mode 0600. Re-render after listener
replacement. The common HTTP service CLIs offer:
mockingbird-stripe fleet doctor --config mockingbird.json --json
mockingbird-stripe fleet control --base http://127.0.0.1:9000/__admin/fleet --action namespaces
mockingbird-stripe fleet render --manifest ready.json --connections private.json --mapping mapping.json --namespace synthetic-customer --output app.env --format dotenv
mockingbird-stripe fleet rehearse --config mockingbird.json --archive saved-archive.jsonControl uses MOCKINGBIRD_ADMIN_KEY (or --key-env NAME); --body FILE, --method and
--idempotency-key support other routes. Doctor performs read-only load/schema/output-path and
optional --endpoint URL reachability checks, reports versions and a dry startup plan, and returns
nonzero for failures. It never resets state or probes storage by writing. Programmatic rehearsals
accept bounded async probes, restore an archive into distinct ephemeral listeners/storage, report
a machine-readable result, and clean up without changing source state or its latest pointer.
GET /inspect?namespace={name}&service={service}&offset=0&limit=100&collection={table-or-key}
reads the actual child engine. PostgreSQL lists public tables or bounded records from an explicitly
quoted table; Redis lists keys, type/TTL and queue counters. Inspection is separate from mutations,
and cannot execute arbitrary SQL/Redis commands. Limits are 1–1000 rows/keys.
GET /diagnostics?namespace={name}&service={optional-service} exports
mockingbird.diagnostics/v1: timestamp/process/generation, package/capability versions, configuration
fingerprint, selected diagnostic counters and up to 50 unmatched-request method/path fingerprints, limits and the last 50 operation summaries. It excludes
credentials, request bodies, raw records, model text and operation payloads. Truncation is explicit;
use a separately requested private archive for privileged state capture.
Node transport limits
serve(api,{maxBodyBytes,requestTimeoutMs}) streams uploads to Fetch handlers with incremental byte
limits (16 MiB and 30 seconds by default), including chunked bodies without Content-Length. Configure
larger bounded limits for archive imports; they still parse bounded JSON at the control handler.
Oversize and stalled uploads return 413 and 408. Request aborts, URL/Request construction failures
and response-body errors stay inside the adapter boundary. Disconnect cancels response streams and
releases backpressure waits. Configure the exported Node serve directly when custom transport
limits are needed; ordinary fleet CLI listeners use these defaults.
Isolated runs, namespace forks and warm leases
All helpers below ship from @crvouga/mockingbird-service-stripe/fleet; the standalone internal
adapter package is not a consumer dependency. Load supported service serveTarget exports from
matching installed @crvouga/mockingbird-service-*/server packages. Independently bundled runtime
declarations are accepted at the loader boundary and their fleet protocol is checked before
listening. Missing/incompatible packages fail with concrete installation/version diagnostics.
POST /namespaces/{source}/fork accepts {destination,bindings?}. It fences the selected environment,
captures all child state and clocks consistently, provisions a new namespace, and restores rebased
HTTP/SQL/Redis state. Source state and archive identity stay unchanged. Existing destinations are
refused. The caller must be an administrator. Failed provisioning/restore is reported and cleaned
up or left fenced when recovery cannot be proven.
withFleetRun(client,{baseline?,scenario?,verify?,preserveOnFailureMs?},async run => …) owns a unique
namespace. The run descriptor exposes namespace, generation and discovered endpoints without admin
credentials. Use run.registerCleanup(async () => …) for application clients/workers. Cleanup runs
on success and failure, then the namespace is deleted. FleetRunError preserves original/cleanup
failures and diagnostic evidence. Optional debug preservation expires within one day; it does not
silently leak an indefinitely pinned namespace.
NamespacePool provides bounded prewarmed namespace leases, not shared vendor state. Acquiring
returns a unique lease identity/generation and expiry. Register client cleanup on the lease, then
release it: the pool closes clients, checks quiescence, restores and verifies its baseline before
making it warm again. Failed or expired leases are quarantined rather than reassigned. Waits have
a timeout/cancellation policy. Changing baseline version retires stale warm entries. This is an
in-process lease coordinator; use one pool owner, not independent competing pools for the same names.
Restore bindings, archive protection and version upgrades
POST /archives/bindings plans explicit {bindings:[{namespace,service,endpointId,endpointRef,secretRef?,signing?,signingRef?}],endpoints:{ref:url}}
bindings without restoring. The RestoreBindings API lists only supported webhook endpoint and
signing-material fields; opaque records are never searched/replaced. Reports redact signing
material. Resolve secret references through the host-owned resolveArchiveSecret hook. Import and
fork may apply the same binding plan. Missing or ambiguous references fail before child mutation;
leave application-owned URLs untouched unless they have an explicit supported binding.
archiveProtector({activeKeyId,trustedKeyIds,resolveKey}) uses RFC 7516 compact JWE with direct
A256GCM, random 96-bit nonces and authenticated key-id metadata. The resolver supplies 32-byte keys
from outside the artifact; the artifact contains no key. Keep previous key ids trusted only for the
required rotation window. Unknown keys, corruption and authentication failures reject before
archive parsing/restore; protection never falls back to plaintext. Hash identity remains the
verified inner archive identity, not a claim of ciphertext authenticity by itself.
Set archiveProtection on the fleet for protected control exports/imports. For at-rest protection,
pass the same protector to filesystemRepository(directory,maxBytes?,protector) or
s3Repository({...options,protection:protector}); protected fleet policy rejects an unprotected
repository. Use client.exportProtectedArchive() for its typed envelope and importArchive() for
either supported artifact type. Generic request() can also transport envelopes. Configure body
and repository byte limits for encryption overhead. Secrets and opaque application fields still
require careful privileged artifact access even when protection is disabled intentionally.
archiveCompatibility(input,registry) reports supported source formats, per-target version
transforms and blockers. migrateArchive applies only explicitly registered deterministic
transformations, produces a separately hashed v1 artifact and records provenance. Unknown formats
or incomplete transformation chains fail; source bytes/identity are preserved. migrateAndVerify
requires an independent verification callback before publishing to a repository. Rehearse a migrated
archive in a fresh fleet and verify application-specific probes before promoting it.
incrementalArchive, materializeArchive, archiveReachability, publishIncremental and
compactArchive offer optional content-addressed section deduplication with verified base chains.
Participants may still capture full sections; this does not claim engine-level change tracking.
Missing/corrupt bases, cycles and inconsistent target hashes fail. Materialization must match the
full archive's state and identity before promotion. Reachability protects every required base/object
from pruning. Compaction publishes only a verified independent full archive. Full archives remain
the default and the management import surface consumes materialized full artifacts.
Competing writers and interrupted operation recovery
For shared storage, use RepositoryWriter with filesystemConditionalStore (same-host filesystem
mutex/CAS) or s3ConditionalStore (ETag/If-Match and If-None-Match). Acquire an owner lease and bind it
to the repository before supplying it to a fleet. Expired takeover must be explicitly allowed.
Renewals and latest publication compare the current generation/lease; a delayed previous writer
cannot publish after takeover. Immutable uploads can succeed independently, but only verified
objects become the fenced recovery pointer. Storage without the required conditional semantics
is refused. The ordinary repository alone is a single-writer convenience, not a distributed lock.
For a voluntary handoff, first stop accepting workload writes, drain application processes,
await the final archive upload and verified publish, then await writer.release(lease).
Release keeps the latest verified archive and generation, marks the lease relinquished with a
conditional write, and lets the replacement acquire immediately without expired-takeover
permission. The replacement increments the generation. Release returns "released" or
"already-released" for a repeat of the same lease, and rejects stale leases after handoff.
Renew and publish reject released leases. A conditional conflict throws writer release conflict;
connectivity/storage errors propagate. After an uncertain response, retry the same lease (or
read the control record); never delete the record. Filesystem and S3 use the same CAS contract.
Expired-owner takeover remains a separate explicit policy.
journal:filesystemOperationJournal(directory) opts into fsynced per-operation metadata and
requires an archive repository. Before a mutation, capture a verified portable before-state where
available; durably record prepared/committing, then upload/publication and terminal outcomes.
Metadata contains ids, scope, action, fingerprints and archive hashes, never raw request bodies or
snapshots. Stale journal revisions cannot overwrite reconciliation from another controller.
On restart, prepared-only records can be marked abandoned before commit. A published archive is
accepted only after object verification and matching conditional-writer generation; it is restored
before readiness without rerunning the original mutation. Unprovable commit/rollback remains
recovery-required: affected namespaces stay fenced, health fails, and the manifest does not claim
ready. GET /recovery lists interrupted work. POST /recovery/{id}/rollback restores a verified
before-state; explicit namespace reset/restore can also resolve its own scope. An absent before-state
requires explicit reset. Required journal read/write failures are visible and cannot claim a durable
outcome. This covers declared portable participants, not exactly-once external effects or a final
backup after SIGKILL. Archive access and journal storage must remain available together.
Audit, lifecycle telemetry, assertions and traces
audit:{write,failurePolicy:"open"|"closed"} sends attributable denied/attempt/terminal metadata to
a host sink. Entries contain actor/credential identity, namespace, selected participants, operation
id, real timestamp and outcome. They omit keys, bodies, records and signed URLs. GET /audit exposes
a bounded 200-entry administrator-only history and whether a durable sink accepted each record.
Restore does not rewind this administrative history. Fail-closed admission prevents mutation when
the sink is unavailable; terminal sink failure retains recovery fences. Memory-only history does
not claim durable or tamper-proof audit storage.
telemetry(event) receives operation id, phase, optional participant, action, status, host elapsed
milliseconds and recovery flag. Checkpoint, commit, rollback, workload hooks, upload verification
and publication share the operation correlation. GET /telemetry reports aggregate started,
succeeded, failed/cancelled, rollback attempts/failures, recovery-required, duration count/sum/max,
and hook/sink failure counts. Clock rewind cannot rewind durations. Defaults have no namespace
metric labels and require no collector. Hook failures are counted without changing vendor output.
POST /assertions/baseline creates a run-scoped observation cursor. POST /assertions/{id}/check
reports unmatched/rejected requests, applied faults, failed deliveries and current active/pending
work since that baseline. Explicit service/path-fingerprint/operation allowances may constrain
status and count. Pending queues are opt-in allowances; active work remains visible. Baselines do
not clear another run's vendor history. Retention is bounded and truncated evidence is explicit.
Request observations exclude bodies and credentials. See the exported AssertionAllowance type
for the exact allowlist shape.
trace:true enables privileged POST /trace/start, GET /trace and POST /trace/stop. A trace
holds a verified baseline plus bounded HTTP observations, response hashes and supported clock/reset
controls. Body capture requires includeBodies:true; sensitive JSON/form/query fields and
credentials are omitted. Limits are 200 events/1 MiB per trace, 64 KiB captured bodies and four
retained traces; overflow/truncation and unsupported controls are explicit limitations. Streams,
concurrent order and unrecorded protocol/external effects cannot claim complete serial replay.
replayFleetTrace restores into a distinct explicitly supplied synthetic fleet and checks response
hashes. It rejects replay into its source fleet, admin-path requests and undeclared/live destinations.
Only discovered loopback endpoints or an explicit synthetic-origin allowlist are eligible.
Incomplete artifacts require explicit allowIncomplete plus any necessary application credential
bindings; replay never silently reconstructs omitted credentials or contacts vendor production.
Logical event stepping and participant certification
POST /time/step-next advances to the earliest declared event; /time/run-until accepts bounded
condition:"no-events"|"idle"|"checkpoint-ready", maxEvents, maxAdvanceMs and timeoutMs, with
optional service selection. First freeze and align selected clocks. Redis declares key expiry and
BullMQ delayed-job deadlines; HTTP participants opt in through nextEvent(), while unsupported
participants are reported. After every step the controller settles timers and rechecks cross-child
events. Cycles or a delayed job without a worker stop at the budget with diagnostics. Optional
synchronizeClock runs after releasing mutation fences so application workers can consume work;
external worker clocks are not advanced by assumption. Host timeout uncertainty retains fences.
launchFleet(command,config) owns an installed CLI subprocess, private temporary config/discovery
files and bounded readiness/teardown. participantConformance(factory,probe) populates independent
namespaces, verifies reset isolation, captures state, destroys the first process, restores in a
second process and compares deterministic continuation. Declare included/excluded state explicitly.
Failure-injection probes certify capture/restore/rollback fences; unconfigured probes are reported
as exclusions, never inferred passes. The test suite exercises the bundled public Stripe CLI and
real mixed-protocol fresh-process recovery in the container reference.
External host state can implement FleetChild and register through participants; it joins the
same readiness, namespace, archive and fencing lifecycle. fileParticipant(root,allowlist) supplies
a bounded reference for explicitly allowed relative files. It rejects traversal, unexpected archive
paths and symlink traversal, scopes files by namespace and never lets an archive choose a host root.
Coordinate application processes through workload hooks; the helper cannot stop unrelated writers
that bypass its API.
Schema, ordering, ingress and container deployment
Set schemaVersion:1 and $schema:"https://mockingbird.chrisvouga.dev/schemas/fleet-v1.json" in fleet
JSON. The published JSON Schema and runtime validator share one source. composeFleetSchema admits
only declared target flags and registered extensions for authoring tools. Runtime config errors
include precise paths; unknown fields/future versions reject before listeners. Programmatic
extensions need explicit host integration rather than silently ignored configuration keys.
An instance name is independent of vendor identity: {services:{billing:{target:"stripe"},payments:{target:"stripe"}}}
creates independent endpoints and state. Omit target for existing suffix-based configurations.
Archives record target identity and reject mismatched restore targets. dependsOn:["billing"]
orders startup/verification and reverses shutdown; unknown edges and cycles fail in preflight.
Dependency ordering does not imply snapshot consistency with unregistered external systems.
cors:{origins:["https://console.example.test"],methods?,headers?} permits explicit browser origins
for aggregate controls. OPTIONS is unauthenticated only for allowed preflight combinations; actual
mutations still require credentials/scopes. Responses vary by Origin. Child vendor routes retain
their provider CORS behavior. Avoid wildcard credential policies.
For Stripe, publicOrigin:"https://mocks.example.test",publicPath:"/billing" separates the advertised
HTTP base from the private listener. Generated Checkout/portal links and namespace paths use the
fixed configured origin. createFleetIngress(config,privateConnections) is a streaming Fetch proxy
reference; use the private connection manifest, not advertised URLs, as its upstream map. It strips
forwarding/admin headers, preserves query and namespace carriers, streams bodies, propagates
cancellation, and keeps administration on a separate private listener. Supply TLS/DNS externally.
| Provider/profile | Origin/prefix behavior | Signing policy |
|---|---|---|
| Stripe reference | Configured origin/prefix, hosted pages and namespace follow-ups tested | Vendor requests do not use host/path signing; webhook payload signatures remain intact |
| Cognito | Configured origin/prefix and namespace JWT discovery/authentication; restore on changed private ports tested | RS256 issuer/JWKS certified; SigV4 requests require direct endpoints |
| Medplum | Configured FHIR/OAuth origin/prefix and namespace links, pagination, restored state tested | Existing OAuth/project policies; reacquire tokens after restore |
| S3 and other providers | Public origin/prefix mapping is explicitly unsupported/discoverable | Use direct endpoints preserving signed host/path/query; arbitrary rewrites are not certified |
| PostgreSQL/Redis | Actual protocol endpoints and explicit host port mappings | HTTP reverse-proxy options do not apply |
Untrusted Host/Forwarded headers never replace the configured public origin. Other mappings fail preflight; SDK path-prefix support must not be assumed. The opt-in single-container reference pins a source/lockfile inventory and Bun image digest, runs non-root without a Docker socket, supports Linux amd64/arm64, and verifies controlled shutdown plus fresh-process HTTP/PostgreSQL/Redis archive restoration. It supplies packaging and lifecycle, not live hosting or an automatic deployment.
outbound:{mode:"offline"|"allowlist",origins?,maxRedirects?,maxBodyBytes?,onReject?} restricts
fleet-owned webhook delivery. Offline sends fail locally; allowlist mode checks every destination
and redirect, bounds bodies/redirects, and emits only namespace/policy/reason rejection metadata.
Configured proxy/fetch injection remains host-owned. Discovery identifies HTTP children using the
WebhookHub transport and reports unsupported participants. This is not a network sandbox for SDKs,
application sockets, arbitrary provider-specific transports or DNS rebinding; apply a separate
network policy when those need isolation. No outbound restriction is enabled by default.
Provider-specific ingress limits and acceptance coverage are documented in the Cognito README and Medplum README.