Changelog
All notable changes to the fluxrig project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Phase 4: scale & hardening
| Version | Date | Status | Summary |
|---|---|---|---|
| v0.13.0 | 2026-10-20 | Delivered | ISO 20022 support: an ISO 8583 network bridges to the RTP rail, specs and lookup data upload to the Mixer, the registry lives on the bus, and a Rack parks messages while the bus is down |
| v0.12.0 | 2026-09-28 | Delivered | The Mixer's API and a Rack's enrollment and heartbeat now require authentication, a Rack pins the Mixer's signing key, and guaranteed-lane messages ack only once handled |
| v0.11.0 | 2026-09-24 | Delivered | The message store is encrypted at rest, wires choose a lane (hot or guaranteed), and the first licensed enterprise gears ship alongside the core release |
| v0.10.0 | 2026-09-07 | Delivered | The semantic layer stops being documentation: a spec's rules are enforced on traffic |
| v0.9.0 | 2026-09-03 | Delivered | Enrichment from outside the message, and correlation keys that survive a bus hop |
| v0.8.0 | 2026-08-24 | Delivered | EMV chip data: BER-TLV parsing with unknown-tag preservation |
| v0.7.1 | 2026-08-10 | Delivered | ISO 8583 TLV length hardening |
| v0.7.0 | 2026-08-01 | Delivered | Payment switch: Conductor gear, gear manifests, ISO 8583 TLS |
v0.13.0 - 2026-10-20
Before upgrading, follow Upgrading fluxrig: from v0.12 to v0.13. It gives the steps in order. The BREAKING entries below explain each change.
Added
- ISO 20022 support: the
codec_iso20022andtranslategears. A scenario can now bridge an ISO 8583 terminal network to the RTP network of The Clearing House.codec_iso20022convertspacs.008.001.08andpacs.002.001.10to structured fields and back, driven by an ISO 20022 protocol spec.translatemaps the fields of one spec to another, as a translation spec says. See the ISO 20022 codec, the translate gear and the bridge tutorial, whichmake test-robot-billpayruns. - Upload a spec or lookup data to the Mixer.
POST /api/v1/specs, andfluxrig spec import --api, file an artefact in the Mixer's spec store. The Mixer checks it with the loader of its kind (ISO 8583, ISO 20022, translation, or data) and refuses one that a Rack cannot load. A translation spec must name artefacts that the store already holds. The body is bounded bymixer.max_payload_size. Aname:tagthat already names other content gets409. - A scenario carries what its translation specs name. The Mixer sends a Rack the specs that its gears name and, for a translation spec, its protocol specs and its lookup records too. A Rack then runs a translation with none of its files.
fluxrig scenario import --api --activateactivates the scenario after the import. The API supported it, but the command could not ask for it.fluxrig gears doc --write --skip-types. Names the gear types another binary documents (the enterprise gears, refreshed byfluxrig-ent). Their reference blocks are left as they are and listed, instead of failing the run.make gear-docsskipssim_sourceandsim_responder.iso8583-toolsends a bill payment. New auth flags:-auth-proc-code,-auth-amount,-auth-currency,-auth-merchant-idand-auth-rrn(autoderives the RRN from the date and the STAN).-auth-reportis also written for a single transaction.- Conductor overload shedding (
valet.max_in_flight). The number of open tickets a Conductor holds is now bounded. A stuck destination used to accumulate tickets in RAM until each timed out; past the bound a new request is shed on the error port witherror.reason=overloadedinstead. The default is0(unbounded), so existing scenarios are unchanged. A retransmission of an already-open key and a replay of a retained outcome are never shed: the bound stops new work, not duplicates of work already accepted. - Offline store-and-forward. A gear emission to a bus-bound subject
while the bus is down is now parked in a bounded in-memory outbox instead
of failing the message. When the bus returns, the outbox drains.
rack.outbox_max_messages(10000) caps it; when full the oldest entry is dropped and counted, and the emitting gear is never blocked.rack.outbox_ordersets the drain order. Withsubject(the default), a message never reaches the bus ahead of an older one on the same subject: a reversal parked after its authorization arrives after it, and a live emission on a subject that still has parked messages queues behind them. While the bus is up and the outbox drains, such an emission first waits up torack.outbox_backpressure_timeout(1s) for that backlog to clear and then goes straight to the bus, so the emitting gear slows to the drain's pace instead of filling the outbox until the cap drops messages. With the bus down nothing waits.0sturns the wait off. Withnone, the drain forwards whatever the bus takes, as it takes it. A message is dropped only when the bus marks it as one it can never accept (it fails validation, cannot be encoded, or exceeds the payload limit). Any other refusal keeps it parked, so a dead bus never eats the queue, however many rejoins fail.rack.outbox_drain_retries(3) is how many more times the drain tries a refused message before it moves on. A drain starts on rejoin and on the first emission the bus accepts, so a reconnect inside the bus connection also empties the outbox. Metrics:flux.outbox.parked/drained/droppedandflux.outbox.depth. Memory only: a restart with a non-empty outbox loses it, and the reference says so. fluxrig-mixer --repair. A corrupt stream no longer leaves the operator with only "move the directory away". With the Mixer stopped, the mode starts the embedded server without provisioning, diagnoses each managed stream (healthy, drifted, absent, unreadable), and reports. Drift is fixed by updating the stream config and an absent stream is created; both lose nothing. An unreadable stream blocks with exit 1 and names--force, which recreates it and reports exactly what was discarded. Boot failures name the mode as the next step. There is still no automatic repair at boot, on purpose.- Telemetry retention sweep. The Mixer now starts the retention janitor
at boot. It deletes Parquet telemetry files older than
observability.embedded.retention_days(30 days by default), once at startup and then everyobservability.embedded.rotation.check_interval(1 hour by default). Before this, nothing removed old telemetry and the Parquet tree grew without bound. The sweep deletes.parquetfiles only: rows in the DuckDBtelemetry_*tables are not removed. observability.embedded.rotation.check_intervalandobservability.embedded.rotation.max_age_daysare now read.max_age_daysis the fallback whenretention_daysis not positive.snake.cluster_nameandsnake.store_dirare now read. Both were already fields of the embedded server's own configuration but had no key on the config struct, so setting them had no effect.store_dirempty keeps the default, asnakedirectory understore.dir.- Dead letter queue. A message no consumer can process is now kept
instead of dropped. The Mixer provisions a
flux-dlqstream, and the bus republishes the message there when its payload does not decode, fails validation, or the handler fails on every redelivery. The original bytes are stored unchanged with the failure context in headers, so an operator can find the message, fix the cause, and replay it. Keys:snake.dlq_enabled(default true),snake.dlq_subject(defaultflux.dlq),snake.dlq_max_age(default 720h).
Changed
-
BREAKING: the reference ISO 8583 spec packs a standard bitmap.
examples/specs/iso8583-v87-ascii.yamlpacked one 128-bit bitmap on every message and never set bit 1. A peer that follows ISO 8583 reads 16 hex characters of primary bitmap, so it read the second half as data, and its own messages did not unpack. The spec now declares an 8-byte primary bitmap: 16 hex characters, and a secondary bitmap only when a field above 64 is present, announced by bit 1. A peer that was built against the old layout must change with it. The spec's version is now3.0.0, because the store keeps every version unchanged: a Mixer that already holdsiso8583-v87-ascii:v2.2.0keeps serving the old layout under that name. Uploadexamples/specs/iso8583-v87-ascii.yamland change eachiso8583-v87-ascii:v2.2.0reference toiso8583-v87-ascii:v3.0.0. Reported by Galih Lasahido. -
BREAKING: the Mixer refuses to start without its API. With no
api.auth_token, or an API port that is in use, the Mixer used to report ready, enroll Racks and deploy its startup scenario with no management API. Nobody could manage it or upload to it. It now exits with code 1 before it reports ready, and an API that stops later stops the Mixer. Setapi.auth_token, orapi.auth_disabled_dangerously = truefor a test. -
on_error: rejectemits on anerrorport.codec_iso8583,codec_iso20022andtranslatesend a rejected message out of theirerrorport, witherror.reason,error.gearanderror.message, so a scenario can answer the sender. Before,codec_iso8583returned the error to the runtime, which logged it and dropped the message. An unwirederrorport still drops it. On the guaranteed lane a rejected message is now acknowledged, not redelivered to the dead letter queue. -
BREAKING: the registry lives on the bus. Rack records, the name index, scenario files and the active-scenario pointer now live in a JetStream key-value bucket (
registry.bucket, defaultfluxrig_registry), not in the Mixer's DuckDB file or understore.dir/scenarios. Every write is checked against the revision it read, so two Hellos that claim one name can no longer both succeed. Nothing is migrated:- Every Rack enrolls again on its own: the empty registry denies its old
identity, and the Rack drops it and enrolls with the bootstrap secret.
It comes back with a new MachineID and waits in
pendingunlessenrollment.auto_adoptis on. Approve each Rack again. - Import each scenario again, then activate it.
- Upgrade every Rack before the Mixer. The Mixer stores only a hash of a
Rack's secret, so only the first passport carries the secret. A Rack
keeps it in a file beside its passport (
base.state_filewith a.secretsuffix). A Rack of this version works under an earlier Mixer. A Rack of an earlier version under this Mixer reads the next passport's empty secret as its own and stops authenticating. - The upgrade deletes the old Rack, Mixer and Snake rows from DuckDB, plaintext secrets included.
- Every Rack enrolls again on its own: the empty registry denies its old
identity, and the Rack drops it and enrolls with the bootstrap secret.
It comes back with a new MachineID and waits in
-
Every pull request needs an issue, and community pull requests target
dev.CONTRIBUTING.mdstates the rule, a pull request template asks forCloses #<issue>, and a check fails a community pull request that references no issue or that does not targetdev. -
Robot runs keep their workspaces under the run root they print. Each suite works in
<run root>/work/<suite directory>, andrun.shkeeps the newest 10 run roots under/tmp/fluxrig(FLUXRIG_ROBOT_KEEP_RUNS,0keeps all). A run root that a run still uses is never removed. -
A restarted Mixer restores its active scenario. It reads the scenario from the registry at startup, as it was registered and sent, and reports it. It pushes nothing on its own. A Rack that enrolls or rejoins receives the scenario byte for byte as before, so a Rack that already runs it keeps its gears.
--scenariostill overrides. The restore never waits for a Rack: startup is not held up by one that is gone. -
Presence is separate from status. A heartbeat now records presence in a second bucket (
registry.liveness_bucket) whose entries expire afterregistry.liveness_ttl(90s, three heartbeat intervals). The API reportsonlinefor each Rack. A Rack that stops sending heartbeats shows as not online; its status (pending,active,inactive) does not change. -
New
[registry]section.bucket,liveness_bucket,replicas(1),max_value_size(1 MiB, headers included; bounds a scenario file),liveness_ttl(90s) andwrite_retries(3). -
The registry repairs what a crash between two writes leaves behind. At startup and every
registry.sweep_interval(10m), the Mixer deletes a Rack record that never finished registering (no name points at it, so its secret never reached a Rack), completes or rolls back a rename that stopped halfway, and deletes a name key whose entity is gone. A record younger thanregistry.orphan_min_age(5m) is left alone: it may still be in progress. A failure to mint an ID is now an error. It no longer produces a record with the nil ID. -
A configuration with a stray or misspelled key now refuses to start. The load reports the key instead of ignoring it. A key that mapped to no setting used to be dropped silently, so the process ran with a default the operator meant to override and nothing said so. The Rack's
rack.bus.*and legacybus.*aliases,store.state_file,store.scenario_fileand the Mixer'smixer.mixer_nameare read by the loader and stay accepted. Config files in this repository were corrected where they carried keys no setting reads. -
A duration that does not parse now refuses to start, and names the field. Durations were parsed where they were used, often with the error ignored, so a typo such as
operation_timeout = "5 seconds"became zero: the timer it bounds fired immediately, or panicked a ticker. -
idgen.RandomSuffixreturns an error again instead of a placeholder. It returned the constant"0000"when the entropy source failed, so a caller received a plausible, wrong suffix with no signal. The signature is nowRandomSuffix(n int) (string, error). It has no caller in this repository; the change affects an external user of the exported symbol.
Fixed
-
The roaming example writes its correlation entry before the authorization moves on.
examples/scenarios/roaming_enrichment.yamlforwarded each authorization before its correlation entry was written (await_store: false), but that entry carries the connection the reply must go back to. An authorizer that answered in a few milliseconds beat the write: the reply found no entry and was lost, or, when a trace number repeated on the same terminal within the entry's TTL, found the older entry and was written to the other request's connection. The store now completes before the request moves on (await_store: true), so a write that fails now fails the authorization. The correlation key must still be unique among the requests in flight: two at once with the same key share one entry. Copy this change into a scenario derived from the example. -
coatcheckbounds the awaited store write.store_timeout(default5s) now limits every store write. Before, it applied only to the detached write (await_store: false), and the awaited write could wait without a bound. -
codec_iso8583decodes a fixed-length field at its declared width. The library strips a field's padding when it unpacks, so DE 11000042decoded as42, an all-zero DE 3 as0, and the MTI inDataas200. The wire re-encoded correctly, butDataand every alias carried the trimmed value, so a rule or a translation that compared the fixed-width value saw a different string. A fixed-length text field (String or Numeric) with a padder now decodes at the width the wire carried. A variable-length field, and a Binary or Hex field, is unchanged. Reported by Galih Lasahido. -
A scenario with global gears reaches the Racks that are already enrolled. Activation pushed a scenario only to the Racks it names, in
racks:or in a gear'sdeploy:. A gear with nodeploytarget reached a Rack only if that Rack enrolled after the activation, so activating such a scenario on a running fleet reached no Rack. Activation now pushes it to every active Rack. The push that follows an enrollment skips a Rack that the activation has already reached, so its gears do not restart twice. -
A Rack with a new
state_dircan enroll. The Rack pinned the Mixer key before its state directory existed, so its first enrollment failed with "pin mixer key: no such file or directory" and the Rack stopped. -
The Conductor routes to an
io_tcpclient. A Conductor port is available only after the I/O gear at its end reportsconn.up, and onlyio_iso8583reported it. A route that ended at anio_tcpclient failed every request withno_destination. Theio_tcpclient now publishes its link state. -
A spec imported while the Mixer runs is sent with the next scenario. The Mixer opened its spec store twice, and each instance kept its own index, so the scenario controller did not see what the API stored until a restart.
-
iso8583-toolpacks the reference v87 spec. DE 3 of that spec has no encoding of its own, and the tool packed it with the spec as compiled, so every auth request that carried DE 3 panicked. The tool now makes the spec nil-safe as the codec does (sdl.MakeNilSafe). It also reads a fixed field with its padding, so an MTI reads0210, not210, and compares with-auth-expect-mti. -
The Mixer's Hello throttle no longer grows without bound. It kept one entry for every Rack name and machine ID it had ever seen. A Hello carries its name unverified, so a stream of distinct names grew the Mixer's memory for as long as it ran. Expired entries are now removed at most once per throttle window.
-
The raw bus subscription path now records receive metrics, and the bus byte counters are no longer empty.
SubscribeRaw(used by the Coat Check daemon) delegated with no instrumentation, so its consumed messages were invisible toflux.nats.messages_received.flux.nats.bytes_receivedandflux.nats.bytes_publishedwere declared and never incremented; both are now recorded where the payload size is in hand (the raw paths), so the byte series reports real values instead of reading as a system that is not reporting. -
A fully qualified
urn:flux:<kind>:<name>:<tag>reference now resolves.Loadsplit a reference at its last colon by hand, so aurn:flux:spec:visa:latestreference read the name asurn:flux:spec:visaand looked up the wrong artifact. A single parser now owns both the shortname:tagform (unchanged) and the reserved scheme, and rejects the malformed cases instead of resolving them wrongly. -
A Coat Check entry now carries its own TTL, so it expires without the daemon. The bucket was created with
max_ttlas its bucket-wide TTL, so every key lived that long whatever the message asked for: a key whosemeta.coatcheck.ttlwas 30 seconds still sat formax_ttl(5 minutes), and a slow or crashed daemon extended that tomax_ttlpast the crash. The store now writes each entry with the TTL its message resolved to (the override, elsedefault_ttl, capped atmax_ttl), and the bucket opts into per-message TTL. The daemon still emits the timeout event; the storage expiry no longer depends on it being alive. An existing bucket keeps the entries already in it; the change applies to new writes. -
Concurrent spec imports no longer lose a version. A CLI and a Mixer share one
index.json, and both did read-modify-write on it, so the second writer overwrote the first writer's entry with its stale in-memory copy. The artifact's bytes stayed in the store but became unreachable by name. An import now takes a cross-process lock onindex.lock, re-reads the file, merges its entry into what is on disk, and replaces the file atomically, so a reader never sees a torn index either.
v0.12.0 - 2026-09-28
Before upgrading. Most of the changes below are self-resolving once both sides run the new version, or a one-time config change. One is not: it is permanent and needs a manual, per-row fix. Look for "Permanent, needs a manual fix" below and act on it first.
- A coatcheck or conductor correlation key built from more than one field
now joins them differently, and an entry stored under the old key is
unreadable until it expires. Key parts used to be joined with a fixed
separator (
_in coatcheck, the conductor's own\x1f) that a field's own bytes (a PAN, a Track2 blob, binary PIN/MAC/EMV data) could themselves contain, making two different sets of parts collide on the same key. Parts are now joined unambiguously (length-prefixed), so a value stored by an older version is not found by this one; it is not lost, just unreachable until itscoatcheck.ttlpasses and a fresh one is written under the new key. coatcheck restore'son_missingnow defaults toerror, notforward. An unseton_missingused to forward the reply on as if nothing were missing, silently sending a PAN-stripped (or otherwise incomplete) reply onward. A scenario that relied on the old default must now seton_missing: forwardexplicitly. An unrecognized value is now also an error, where it used to behave likeforwardtoo.io_tcpin client mode now applies configured framing to outbound writes. It used to writeRawPayloadstraight to the socket with no framing at all, while server mode framed correctly; a client-mode connection to a peer expecting the configured framing (length-prefix, delimiter, fixed-length) now sends properly framed bytes where it previously sent raw, unframed ones.io_tcp'sidle_timeout(default 60s) is now actually enforced. It was parsed and defaulted but never applied to the connection; a connection idle past it is now closed. A long-lived but genuinely idle connection that relied on this never firing needsidle_timeoutraised or set to0to disable it.- A conductor reply's origin stamp must now look like a peer identifier
(letters, digits,
-,_, up to 128 characters), and can optionally be checked against aknown_originsallowlist. The origin arrives in the request's own metadata, sender-controlled, and used to be built directly into a wire port name with no validation; a mesh whose peer names happen to use another character will need renaming, and settingknown_originsis opt-in for narrowing further. A rejected origin now emits on the error port with the new reasoninvalid_origin, so a consumer that switches onerror.reasonneeds to account for it alongside the existing values. - The Mixer's management API now requires authentication, and refuses to
start without it. Every route but
/api/v1/healthnow requiresAuthorization: Bearer <api.auth_token>, compared in constant time. Setapi.auth_tokenbefore upgrading, or the Mixer will not start; the explicit opt-out isapi.auth_disabled_dangerously = true, which logs a warning on every start and is not recommended. - A Rack's heartbeat is now authenticated against its own registered
secret, and is silently dropped if it does not match. Before, any caller
who learned a Rack's
machine_id(previously readable from the unauthenticated/racksendpoint above) could forge its liveness and overwrite its stats and config with none of its own. Upgrade the Mixer and its Racks together, or close together: a Rack still on the previous binary sends nosecretin its heartbeat and every one of its heartbeats will be rejected until it is upgraded too. - Permanent, needs a manual fix: a registry row with an empty stored
secret can no longer heartbeat or re-register. Upgrading the Rack does
not fix it, and there is no automatic migration: an empty secret never
matches anything, by design, on either side. Find affected rows with a
direct query against the Mixer's registry store (
store.database_file,flux.duckdbby default):duckdb flux.duckdb "SELECT machine_id, name FROM registry WHERE type_id = 4 AND (secret IS NULL OR secret = '');". For each row returned,admin racks remove <machine_id>and let it re-enroll as a fresh Zero-Config identity; this version issues it a real secret on the way back. - A brand-new Rack identity must now present the shared bootstrap secret
on its first Hello. It used to be accepted with an empty secret
regardless of what was already on file for that name, which let a second
caller claim an existing Rack's identity.
rack.bootstrap_secretis a new Rack-side setting (default"fluxrig", matching the Mixer'senrollment.bootstrap_secretdefault) so Zero-Config enrollment keeps working unconfigured; set both to a non-default, matching value for anything beyond a trial deployment. A Rack whose registration was removed (admin racks remove) and that still holds a stale Passport now gets an explicit "denied" reply and re-provisions as fresh Zero-Config, instead of retrying silently forever. - Importing or activating a scenario no longer promotes a
pendingRack it names toactive. That promotion used to happen as a side effect of the import; approving a Rack is now only ever the operator's own deliberate action (admin racks activate). A scenario that names a still-pendingRack now fails to find it (a timeout, with a clear error) instead of silently activating it first. - A scenario activation no longer purges every Rack's traffic on the shared
bus stream. It used to call a blanket purge of the whole
flux-msgstream whenever it needed the bus, discarding every other Rack's queued guaranteed-lane messages along with the activating Rack's own. It now purges only that Rack's own subjects. - A guaranteed-lane message is now acked only after its handler succeeds, and a failed handler gets up to 5 redeliveries before JetStream gives up on it. It used to ack immediately on receipt, before decoding or handling — any failure after that point (a panic further down the call chain, a downstream publish error) silently lost the message while looking like a success. A message that fails every one of its 5 attempts is now dropped after the last one, the same outcome as before, but only after retrying. The retry count isn't config-exposed yet.
- Applying a new scenario or shutting down now waits up to 5 seconds for the hot lane to drain before tearing it down. It used to unsubscribe immediately, dropping whatever was still queued. A teardown that used to be instant can now take up to 5 seconds; this timeout isn't config-exposed yet.
- A Rack timeout or interval set to
0s(or a negative value) now silently falls back to its documented default instead of misbehaving. Depending on the setting,0sused to fire instantly on every message, panic a ticker on startup, or block a connection attempt forever. Six settings that were previously parsed ad hoc with the parse error discarded outright (snake.connect_timeout,snake.reconnect_wait,rack.enrollment_timeout,rack.enrollment_interval,rack.heartbeat_interval,snake.inactive_threshold) now go through the same validation as the rest and fail loudly on a genuine typo instead of silently reading as zero.snake.initial_retry_wait,snake.initial_retry_attempts,snake.offline_start_timeout,snake.offline_retry_intervalandrack.lane_send_timeoutare unchanged:0/0sis their documented sentinel for "use the built-in default" or "disabled", not a mistake to clamp. - Two
pkg/busGo signatures changed, for anyone building against this package directly rather than through a scenario.bus.Handlernow returnserror(the ack-after-success fix above is what a returned error drives);NatsBus.Purgenow takes asubject stringparameter (the scoped-purge fix above), where it used to take none. - Environment-variable overrides that previously matched nothing now
take effect. The Mixer's env mapper had no per-section handling at
all, so a multi-word field (
FLUXRIG_SNAKE_STREAM_MAX_BYTESand similar) silently changed nothing; the Rack's mapper was missing atelemetry_entry, so everyFLUXRIG_TELEMETRY_*multi-word field did the same. Both are fixed. If you were relying on the Mixer or Rack ignoring one of these env vars, it will not ignore it anymore — check your deployment's environment before upgrading. A list-valued field (stream_subjects) set via env var now actually splits on commas too; it previously became a single, wrong entry. fluxrig admin racks remove/suspend/activate/shutdownnow prompt for confirmation. Pass--force(-f) in a script or any other non-interactive caller. Without a terminal attached and without--force, the command now aborts instead of hanging or proceeding.- An entity ID minted before a Rack has a machine ID (Zero-Config,
before enrollment assigns one) now carries different bits in its
machine-hint region. It used to be zero-filled from the Nil machine
ID; it is now
uuid.NewV7's own random bytes, left alone instead of overwritten. IDs already issued by an earlier version are unaffected; this only changes what a not-yet-enrolled Rack mints from this version on. idgen.IDGenerator.NewEntityIDnow panics ifuuid.NewV7fails, instead of silently minting an ID from a zero UUID.NextEntityID(the entry point most of the codebase actually calls — scenario apply, gear/port/session IDs, the Mixer's own entity ID) is a thin wrapper around it and inherits the same panic. The rest ofidgenis unaffected.NewV7's only failure mode is the entropy source itself (crypto/rand) failing, which is fatal for the rest of the process too; this is a new crash path, intentionally fail-fast rather than issuing an ID that was never really a V7 UUID.- A Rack now pins the Mixer's signing key the first time it accepts a passport, and rejects any later passport signed by a different key. Before, the key embedded in the passport itself was trusted for every verification, so a self-signed envelope on disk would have verified. Nothing to do for an already-enrolled Rack: the pin is taken from the first cached passport it already has.
- The Snake now refuses a plaintext connection once TLS certs are
configured, unless
snake.allow_non_tlsis set. It used to accept plaintext alongside TLS whenever certs were present, with no way to turn that off. - A KV bucket the Mixer provisions (currently only
wasm_catalog) is now capped (snake.kv_max_bytes,snake.kv_max_value_size,snake.kv_ttl; defaults 1 GiB / 1 MiB / no expiry). An existing bucket picks up a changed limit on the next start, the same way a stream already does. archiver_bufferis now actually flushed to Parquet under<data_dir>/messages/. A bug in how a wire ID was read back (store/duckdb) meant every flush silently failed and the buffer was never drained; expect real disk growth there that a prior version never produced, and expect old, unflushed rows to appear once on the first start of this version.- Scenario activation that reaches no target Rack now answers
202 Accepted, not500. The scenario is genuinely active either way; a Rack that enrolls afterwards still receives it. A caller that treated any non-200 as a hard failure needs to accept 202 as success too.
Added
conductor:known_origins, an optional allowlist of peer Conductor names a reply's origin stamp must match.conductor: newerror.reasonvalueinvalid_origin, emitted when a reply's origin stamp fails the format check or theknown_originsallowlist.api.auth_token,api.auth_disabled_dangerously: gate the management API. See "Before upgrading".rack.bootstrap_secret: the shared secret a Rack presents on its first Hello. See "Before upgrading".test/e2e/20_security_regression: standalone coverage for the auth/secret-redaction behavior above, without needing the full regression sweep.runtime.ErrLaneClosed: returned to an emitting gear when the hot lane's subscription was unsubscribed while the message was still on its way to the queue. A barenilused to be returned instead, which counted as a successful delivery to a wire that was actually torn down mid-send.snake.tls_ca_file/snake.tls_verify: wires the mTLS client-cert verification that already existed in code but had no config surface.
Fixed
io_tcp(client and server): the scanner buffer is now sized to accept the largest message the configured framing allows (up to ~1MB), instead ofbufio.Scanner's ~64KiB default; a message near or above that size used to fail to scan.coatcheck's per-message TTL override (Metadata["coatcheck.ttl"]) is now honored regardless ofinclude_values. It was only decoded and applied wheninclude_valueswas set (defaultfalse), so a scenario relying on the override without also asking for full values had it silently ignored in favor ofdefault_ttl.- The TTL override itself is now always carried into the daemon's stored
partial clone. It previously survived only when the override's field
happened to also be listed in
value_fields, which nothing documents and no scenario configuringvalue_fieldsfor an unrelated reason would think to do — so the fix above had nothing to read for most callers until this one landed too. iso8583.unknown_tags, once preserved, is now actually reachable throughGet("iso8583.unknown_tags")(and coatcheck'skey_fields/value_fields, which resolve through the same helper). It was stored as a flat key containing a literal dot, which every dotted-path reader in the codebase resolves as nested maps, not a flat string — so the lookup a downstream gear would actually make could never find it.Secret,BootstrapSecretandAuthTokenare never serialized (json:"-", and explicitlycbor:"-"alongside it) anywhere they appear in a registry record or config struct — protecting every API handler and wire payload that touches one, present or future, rather than each call site remembering to redact it.- All secret comparisons (API token, re-registration bootstrap secret,
heartbeat secret) go through one constant-time helper
(
security.SecretsEqual), closing a timing side-channel that a byte-by-byte==comparison leaves open. It started as three independent, identical copies of the same function across three packages; consolidated into one shared implementation so a future fix or audit only has one place to look. - A fresh registration now issues a unique,
crypto/rand-generated secret instead of persisting the caller's bootstrap secret as the new identity's own secret; every Zero-Config Rack sharing the same default bootstrap secret could otherwise let a second caller adopt an already-enrolled name later. - The enrollment reply topic is now built from a sanitized name and nonce
on both the success and denial paths, closing a subject-injection path a
.or a NATS wildcard in either field used to open. - An undecodable or otherwise invalid message on the bus is now explicitly
terminated (
Term) instead of acked; it never redelivered either way, but it no longer looks identical to a message a handler actually processed. - The OTel trace context is now injected before a message is serialized, so it actually reaches the wire; it used to be injected after serialization and was silently dropped.
- A gear emitting the same message to more than one port, or retrying an
emission with the message it still holds, no longer races on it: the
message is cloned before its
FluxIDis set and a hop appended. The clone coversPathonly —DataandMetadataare not deep-copied and can still be mutated by a concurrent emission sharing the original message. - Telemetry dials for the deferred telemetry bus connections now carry
the same TLS posture (
insecure_skip_verify) as the main bus dial; they previously carriedroot_ca_fileonly. - Telemetry: a bad row no longer aborts the rest of its batch; a
nil-entity metric is now logged instead of dropped silently;
Stop()flushes once, guarded against being called twice. pkg/store/duckdb:Migrate()is now safe under concurrent access (in-process and cross-process);FlushTelemetry/FlushArchiverBufferuse bound query parameters instead of formatting values into SQL text.
[v0.11.0] - 2026-09-24
Before upgrading. Four changes alter what a running deployment does, and one of them cannot be undone by installing the old binary.
- The Mixer's message store is encrypted on the first start of this version, and
an older Mixer cannot read it. Copy the
snakedirectory of the Mixer's store before the first start if you may need to go back. The key is derived fromcluster.key: replacing that file makes the store unreadable, unless you first print the derived key withfluxrig keys store-keyand keep it assnake.store_old_key_file. To use a key of your own, setsnake.store_key_file. - The bus now keeps a message for 24 hours and a stream up to 1 GiB
(
snake.stream_max_age,snake.stream_max_bytes). It had no limit. Raise them if a consumer needs older messages. - A wire inside one Rack is delivered through memory, at most once. A message
still queued when the Rack process ends is lost, where the bus stored it. Ask for
lane: "guaranteed"on a wire that needs the message stored. An emission that no wire consumes is no longer published to the bus, so anything outside the scenario that listened for it on the bus stops receiving it. - The
io_tcpserver gear and the Bento input gear no longer write the payload of a message to the log at INFO. A procedure that read card data or payloads from those logs stops finding them. TRACE writes it with card numbers masked. - A message the
io_tcpserver gear cannot route unambiguously is now refused instead of guessed at or silently queued. A message naming aconn.idwhose connection has disconnected used to be queued for whoever connected next, which could hand it to the wrong client; it now returns an error. A message naming noconn.idwhile several connections are active used to go to "the first" one, arbitrarily; it now returns an error too. A message naming none with exactly one connection active, or none at all, is unaffected. Activatenow fails when a scenario's deploy pins reach no Rack at all, not only when one names a Rack that was never enrolled. Adeploy:pin not listed under the scenario's ownracks:section used to compute zero push targets and report success anyway; the scenario stays the Mixer's active one either way, and a Rack that enrolls afterwards still receives it.- The simulator control endpoints (
POST /api/v1/control/sim/{start,stop,rate,reset}) now answer 503, naming the gear, when nothing acknowledges the command, instead of 200 as soon as NATS accepted the publish.api.control_confirm_timeout(default2s) bounds how long it waits.
Added
-
gears.RegisterExtension: a module outside this repository adds gears to the gear factory. Its binary registers them before the factory is built, and the engine never imports that module. The Mixer command line moved tocmd/fluxrig-mixer/mixercmd, so another module can build a Mixer whose gear catalog lists those gears too. -
The Robot runner takes a suite directory given as an absolute path, and
FLUXRIG_BIN_DIRpoints the suites at another set of binaries. The e2e helpers (test/e2e/utils/e2e_utils.sh) honorFLUXRIG_BIN_DIRtoo. A suite or an e2e test kept in another repository runs on this tooling and exercises the binaries built there. -
A Rack keeps its own copy of the last scenario it applied, and resumes it at start without waiting for the Mixer to send it again (
base.scenario_file,scenario.fluxbeside its passport, mode0600; empty keeps no copy). When the Mixer then sends the same scenario the gears keep running instead of restarting. A copy projected for another Rack is ignored. A Rack that starts with the bus unreachable starts it on its own when none of its wires uses the bus, and keeps a scenario that does waiting for the bus, whole, saying so in its log. -
Wires have a lane. A wire says
lane: "hot"orlane: "guaranteed". A hot wire carries its messages through the Rack's memory: nothing is stored and nothing reaches the Mixer. A guaranteed wire goes over the bus, which stores each message before the gear is told it was accepted. A wire that names none is hot when both gears run on one Rack and guaranteed between Racks. A hot wire between Racks is rejected at import. A Rack now keeps running the flows that stay inside it while the Mixer is away.rack.lane_queue_size(1024) andrack.lane_send_timeout(5s) bound each hot wire, and a graceful stop delivers what is queued. -
The Mixer encrypts its message store at rest. On by default, with a key derived from the cluster key, or with a key file the operator provides (
snake.store_encryption,store_cipher,store_key_file,store_old_key_file). It covers every message, stream and key-value bucket of the bus. An existing store is converted on the first start with no message lost, a store that was encrypted does not open without its key, andfluxrig keys store-keyprints the derived key for a rotation. A copy of the store directory or a backup no longer shows a message. The Mixer, and a client of the bus, still see clear data. -
snake.stream_max_age(24h) andsnake.stream_max_bytes(1 GiB) bound the streams of the bus, which had no limit. -
A log payload is masked at TRACE: card numbers written as digits keep their first six and last four digits.
-
A Rack starts without the Mixer. With a Passport and a saved scenario whose wires all stay inside the Rack, it runs the scenario on its own and serves traffic. When the Mixer returns the Rack joins it without stopping the gears: a client that was connected stays connected, and the gears are not started again. Scenarios that use the bus wait for it. Until it is bound, the gears' control plane carries commands in memory between the gears of the Rack (a link that goes down, a connection to close), and once the Rack has joined it also carries the Mixer's commands to gears that were already running. A Manager can change the bus under its running gears (
Manager.SetBus). -
io_tcpgainsmax_buffered_messages(default1000): the cap on outbound messages queued when nothing is connected yet, or a message names noconn.id. It was an unconfigurable, hardcoded 1000 before. The oldest is dropped past the cap, now counted onflux.gear.messages_dropped. -
api.control_confirm_timeout(default2s): how long a simulator control command waits for a gear to acknowledge receiving it before the request answers that nobody is listening.
Changed
- A wire inside one Rack no longer goes through the bus by default. It goes
through memory, delivered at most once and in order: a message still queued when
the Rack process ends is lost, where the bus stored it. Nothing of a hot wire is
stored, so a card number on one is no longer written to the Mixer's disk. A
scenario that needs the message stored asks for
lane: "guaranteed"on that wire. An emission that no wire consumes is dropped and no longer published to the bus. - The
io_tcpserver gear and the Bento input gear no longer write the payload of each message to the log at INFO, and no gear writes it at DEBUG. Those logs were shipped to the Mixer and kept there.
Fixed
- The spec loader reads
format.layoutof a date or time field (MMDD,hhmmss,MMDDhhmmss,YYMM). The reference spec declared them and the loader dropped them, so nothing that generates traffic could write DE 14 as an expiry. - Documentation said a Rack keeps processing while the Mixer is unreachable, and that a silent Mixer does not stop traffic. Gears exchange their messages over the bus embedded in the Mixer, wires inside one Rack included, so while the Mixer is away a running Rack stays up and reconnects by itself but what its gears emit fails. The operations, deployment, architecture, security, data, observability and conductor pages, the payment switch tutorial and the README now describe what a Rack does in each case, and list isolated operation as roadmap. The pages also said a Rack embeds NATS JetStream (it connects to the Mixer's), that a scenario that fails to apply rolls back to the previous one (it stops the gears it started), and that every signal a Rack handles goes through a write-ahead log (only its logs do).
- Decoder gear (
codec_iso8583) nil-pointer panics on composite fields: nil guards onGetField(),GetSubfields(),Bitmap()and subfields, plus per-subfield panic recovery. - A Rack holding a passport no longer waits out the whole bus retry schedule
(about 37 seconds) before starting offline. The first connection retry, added
for a bus that is not accepting connections yet, applied to every Rack alike.
A Rack with a passport now gives the bus
snake.offline_start_timeout(3s by default) and then runs offline; a Rack without one keeps the fullsnake.initial_retry_*schedule.0srestores the unbounded retry. - Activating a scenario that deploys to a Rack that is not enrolled and active answers HTTP 409 with the Rack's name, where it answered 500. The scenario stays imported, and activates once the Rack enrolls. Any other activation failure remains a 500.
- A Rack that started offline now finds the Mixer when it comes back. It probes
the bus every
snake.offline_retry_interval(5s by default) and, once the bus answers, restarts its session and goes online: hello, passport, telemetry and scenario as on any start. Before, it stayed offline until it was restarted by hand.0sturns the probe off. snake.initial_retry_wait,snake.offline_start_timeout,snake.offline_retry_intervalandrack.lane_send_timeoutdiscarded a parse error and silently became zero, where their six sibling settings already failed fast on a typo."0s", their documented way to disable the bound, still parses without error.codec_iso8583: a panic anywhere indecode()afterUnpacksucceeded read as a successful, empty decode. Only a panic insideUnpackitself was turned into an error; the field-extraction loop after it (bitmap iteration, composite subfields) was guarded by a recover that only logged, so the message reachedProcess()looking cleanly decoded, with an empty MTI and no fields.coatcheck: restoring a previously-vaulted field into a message whose nested data had crossed the bus (CBOR-shaped) silently did nothing. The merge built a normalized copy of the nested map, merged the restored value into that copy, and never wrote the copy back onto the live message.iso8583-tool decodehad no panic recovery aroundUnpack, unlike the gear it mirrors: a malformed payload crashed the process instead of reporting a clean error the way every other failure in the tool already does.- A data race on
RegisterEntity's read ofautoAdopt, racingSetAutoAdopt. - Mixer startup with a pinned scenario logged a false "Failed to activate startup scenario" warning on every normal cold start, since no Rack has enrolled yet at that point by definition. It now logs at Info that the scenario is active and will reach Racks as they enroll; any other activation failure still warns.
- Deploy targets naming a Rack group now get a distinct error ("cannot be matched against active Racks yet") instead of the generic "unknown or inactive target": nothing in the registry lets a live Rack carry labels yet, so a group can never be resolved today, and the old message wrongly implied enrolling a Rack would fix it.
- The
io_tcpserver gear's outbound message buffer now delivers its whole backlog to a newly-connected client, in the order it was queued, instead of one message per connection with the rest stranded; a write failure partway through requeues what was not sent instead of losing it. Unsubscribeon a hot-lane subscription could block forever if the gear's handler never returned, deadlocking every later scenario apply or shutdown on that Rack. It now gives up after a bounded wait and logs, rather than hanging.- The hot lane stopped delivering to every subscriber of a subject as soon as one of them failed (a full or stuck queue). A stuck subscriber's failure is still reported, but only after every other subscriber has had its turn.
- A Rack's initial NATS connection retry ignored a shutdown signal, so a SIGTERM
during that retry (NATS down at startup) was not observed until the whole retry
schedule elapsed (about 37s by default, longer with a configured
snake.initial_retry_timeout). io_tcpclient,read_responses: false: a peer that closed or reset the connection was never noticed, so the reconnect logic never ran again until the gear was stopped by hand.- Updating an existing JetStream stream never cleared
snake.stream_max_bytesback to unlimited when set to0, unlikesnake.stream_max_age, which already did.
[v0.10.0] - 2026-09-07
The semantic layer stops being documentation: a spec could state its rules and nothing applied them.
Before upgrading. Two changes refuse specs that used to load, and both are
about a spec declaring what it is. Every spec must carry spec.id and
spec.version, and every spec must carry a wire block saying where its wire
layer comes from. A spec missing either is refused at boot, with a message naming
it, never per transaction. Every spec shipped in this repository is already in
that shape; both entries under Changed say what one that is not needs. New
behaviour is off by default: validation starts at off, so a message accepted
yesterday is not rejected today because the code was upgraded.
Added
- The spec's rules can be enforced on traffic.
codec_iso8583gainsvalidation:off(default),warnorenforce. Per-MTI field usage, the conditions that decide whether a rule applies, closed value sets and cross-fieldchecksare answered against every message the codec decodes or encodes. A rejection followson_error, exactly as a decode failure does; violations travel on the message ascodec.violationsand are counted onflux.iso8583.violationsby severity, kind and MTI.- The default is
offdeliberately: a message accepted yesterday must not be rejected today because the code was upgraded.warnis how you find out whether your spec matches your traffic before enforcing it.
- The default is
- A spec can state when a rule applies, and what a message must satisfy. Fields carry a per-MTI matrix (
usage,when,values_ref) and a spec carrieschecks, written in a total, side-effect-free expression language over the message being validated.- A
checkcarries a severity:rejectfails the message,warnrecords it and lets it through, which is what makes a rule deployable to a live fleet before it is enforced on one. - An element compares the way its
format.kindsays it does.amount,date,time,datetimeand the newnumerickind compare as numbers on every operator, so a rule can writefield(4) == 1000rather than the element's own zero padding. Every other kind compares as the characters it carries, which is what a response code needs:"00"is not"0".panis deliberately excluded, because a leading zero makes it a different card.
- A
fluxrig spec docrenders the protocol reference from the spec. The Mixer serves it too, atGET /api/v1/specs/{name}/{tag}/doc, and every entry in the spec listing carries the path to its own. Markdown for a repository or a docs site, HTML for a page that is read, printed or mailed. The HTML is self-contained: no scripts, stylesheets or fonts are fetched, so it opens offline.--scope publicomits every field markedscope: privateand states how many it withheld;--scope completeis the internal view. The per-message tables are derived on every render from the rules stored on the fields, so the two cannot drift.format.kind: numeric. A value that is a number rather than a code (a trace number, a sequence, a count), which had no way to be declared.- A stored spec's versions can be listed.
fluxrig spec history <name>andGET /api/v1/specs/{name}return every version of one spec, newest first by version rather than by arrival.fluxrig spec listand the/specslisting now carry when each version was filed, its size, the document's title and which versionlatestreaches;--jsonon both.
Changed
- BREAKING: a spec declares where its wire layer comes from. Every spec now carries a
wireblock:sourcenames a Moov base (moov:<name>) or a wire document beside the spec,fieldsstates what the dialect changes, and each is valid alone. Specs in the legacy vocabulary (meta:with a top-levelfields:fusing both layers,llvar_n-style type tokens) no longer load. Every spec shipped in this repository is already in the new shape; the SDL reference describes what a spec that is not needs. - BREAKING: a spec must declare
spec.idandspec.version. The schema has required them all along and the loader never read them, so a spec with no version at all loaded clean and its traffic carried a content hash that says which file ran and not which contract. A spec missing either is now refused at load: at boot, with a message naming the spec, never per transaction. Every spec shipped in this repository already declares both; a spec that does not needs the two fields added before upgrading. codec_iso8583manifest:on_erroris documented as defaulting todrop. It has always defaulted todrop; the manifest saidreject, so an operator reading the published contract configured for one behaviour and got the other.
Fixed
unknown_tags: droprejected instead of dropping. The policy documented three behaviours and implemented two: anything that was notpreservetook the same branch, so a spec asking to drop an unknown TLV tag had the whole message rejected, which is the opposite of what it asked for.VerifyConnectivitycould report convergence on a cancelled context. The bus delivers to its handlers without consulting the caller's context, so a probe could land after cancellation and Go picked at random between the two ready cases. A caller shutting down was told the telemetry plane is ready.
[v0.9.0] - 2026-09-03
Added
coatcheck:key_normalize. Key field values are canonicalized before they are joined, so the two sides of an exchange agree on a key even when they render the same value differently.trim(default) removes surrounding whitespace;numericalso drops leading zeros, for fixed-width fields decoded against specs that declare different widths.coatcheck:await_store. A store can forward the message without waiting for the write. The default stays blocking, which is right when the reply depends on the entry; set it false when the entry only enriches a record and holding a message for a control-plane write costs more than losing one.codec_iso8583:iso8583.mti_class. A decode now also exposes the leading MTI digits, which a request and its reply share. Correlation keys can be scoped by message class, so an authorization and a reversal reusing a trace number are no longer indistinguishable.- Scenarios can name what a diagram cannot derive. A Rack's
rolelabel reaches its description, and two gear labels,peerandpeer_played_by, say who is on the far side of a socket and whether anything is standing in for it. iso8583-tool:-scheme-move. The host simulator can report a request field back in another, so a host receiving a private field can show which value arrived. It reports into a different field deliberately, and pads to the destination's declared width.- Enriching an authorization from outside the message. A use case on the GSMA Open Gateway APIs in payments, and a tutorial building one end to end: an operator call under a hard deadline, four named reasons for having no signal, and the degradation paths, all as configuration.
Fixed
- Nested field paths silently stopped resolving after a bus hop.
GetValuedescended only intomap[string]any, but CBOR decodes a nested object asmap[any]any. A correlation key built from an ISO 8583 field therefore worked in a single-gear pipeline and returned "field missing" in any real deployment, with nothing logged. - The
bentogear injected a memory buffer over a config that declared its own input. A buffer acknowledges the input before the pipeline has produced anything, so a synchronous responder answered with an empty body and no error anywhere. The injection now happens only when the gear supplies the input itself. - A
bentogear in a relay path dropped the wire bytes.RawPayloadsurvived only when the structured view was empty, so any gear that merely read fields destroyed the payload a downstream io gear had to write. It now travels across the bridge. fluxrig scenario vizdrew both ends of one socket as strangers. Every I/O gear produced its own external box, so a client and the server it dials appeared as two unrelated outsiders. When both ends are in the scenario they are now one relationship.
[v0.8.0] - 2026-08-24
The EMV release: fluxrig parses BER-TLV composite fields, and carries the tags it does not model through untouched.
Added
- BER-TLV composite parsing: a field declared
structure: tlv, such as ICC data in DE 55, is parsed into its EMV tags. Tags the spec does not declare are retained rather than discarded and re-emitted on encode, which is what forwarding to a scheme depends on, and are exposed to downstream gears underiso8583.unknown_tags. Validated by a round trip across a serialization boundary and by a Robot suite against a running Rack.- Tag order is canonical rather than preserved: tags are re-emitted sorted, so every value survives but the byte layout of a field may differ from the one that arrived.
Changed
- Binary field values cross the bus as bytes: a field whose value is not valid UTF-8, such as chip data, PIN blocks or MACs, is placed on the
fluxMsgas bytes rather than as a string, including when reached through analiasor as a composite subfield. Gears and pipelines reading such a field now receive[]bytewhere they received astring. Serialization requires it: a Go string holding binary is not valid CBOR text, so a spec declaring a binary field previously lost the message at the first rack boundary.
Fixed
- The bus no longer drops undecodable messages silently: a message that cannot be decoded is logged with its subject and the reason, instead of being discarded without trace.
[v0.7.1] - 2026-08-10
Fixed
- ISO 8583 TLV length hardening: a crafted BER-TLV long-form length in a composite field (for example ICC data in DE 55) could wrap to a negative value, bypass the bounds check and crash the codec while skipping the unknown tag. Length decoding now rejects unsupported long forms, out-of-range values, invalid BCD length bytes and non-numeric ASCII lengths, so malformed input is reported as an error instead of failing the message.
v0.7.0 - 2026-08-01
The payment-switch release: the Conductor transaction switch, a manifest system that makes every gear self-describing, and native TLS on the ISO 8583 I/O path.
Added
- Conductor gear (transaction switch): routes each request across a destination tree of strategy nodes (
failover,round_robin,least_loaded) whose leaves are output ports, correlates the reply under a ticket, and surfaces timeouts on a dedicatederrorport. Availability sensing binds each local destination to its uplink link-state. Validated end-to-end by thepayment_switche2e and the Conductor stress and chaos Robot suites. - Valet correlation engine: the local-by-default ticket store behind the Conductor's reply matching, with per-ticket TTLs and idempotent redemption.
- Cross-Conductor handoff: a request can exit one Conductor and its reply return through another, routed by an in-band origin stamp, so active/active multi-region topologies need no shared session state.
- Named multi-port gear I/O: gears declare multiple named input/output ports; the Bento gear honors every declared output port and fails loudly on an ambiguous multi-output config.
- Gear manifests: every gear publishes a manifest (identity, ports, config JSON Schema, terminus). The runtime validates a gear's config against its schema at activation, the Mixer API serves the manifest catalog, and
fluxrig gears docgenerates the gear reference documentation from it. - Native TLS / mTLS on
io_iso8583, plus connection link-state signals (conn.up/conn.down) on the control plane that drive Conductor availability sensing. fluxrig scenario viz: generates an interactive LikeC4 topology model (zones, racks, gears, sockets, the Snake) from a scenario file.- Lean Rack build:
-tags nobentocompiles a Rack without the Bento gear for minimal-footprint deployments.
Changed
- Port addressing: port names are dot-free and wires address the fully-qualified
rack.gear.port, so a wire endpoint is unambiguous across a multi-rack topology.
Fixed
- Reply-correlation timeout under load: JetStream deduplication is now keyed per subject, fixing a Conductor timeout where distinct requests collided on the dedup key.
- Rack stability: panics on the gear
emitpath are recovered and nil emits dropped, so a misbehaving source gear can no longer crash the Rack. - Conductor field resolution: correlation, match, and park fields resolve as dotted paths (e.g.
iso8583.field.11) consistently. - Loud topology validation: scenario topology inconsistencies now fail at load instead of surfacing later as runtime errors.
- Gear schema completeness: the
io_tcpandbentoconfig schemas now declare every field the gears actually accept (previously undocumented options such asio_tcpdelimiter framing andbentolog_level).
Phase 3: open & flexible logic
| Version | Date | Status | Summary |
|---|---|---|---|
| v0.6.1 | 2026-07-20 | Delivered | Wasm runtime, supply chain security, polyglot gears |
| v0.6.0 | 2026-06-06 | Delivered | Release metadata only, no code changes |
| v0.5.0 | 2026-05-07 | Delivered | Sovereign identity (UUID v7) & telemetry hardening |
| v0.4.5 | 2026-04-29 | Delivered | Documentation Hardening & Zero-Config |
| v0.4.4 | 2026-04-23 | Delivered | Logic Extensibility & Secure Enrollment |
| v0.4.3 | 2026-02-19 | Delivered | Operational Resilience & NATS V2 |
| v0.4.2 | 2026-02-15 | Delivered | Spec Management & E2E Automation |
| v0.4.1 | 2026-02-09 | Delivered | Stateless Context & I/O Decoupling |
| v0.4.0 | 2026-02-01 | Delivered | ISO8583 Native Gear & Telemetry QoS |
| v0.3.0 | 2026-01-08 | Delivered | Bento Integration & Load Testing |
v0.6.1 - 2026-07-20
Added
- Wazero Integration: Implemented a secure, native Wasm execution environment using
wazero. - Wasm Supply Chain Security: Embedded Ed25519 signatures within
.wasmmodules with Mixer-level trust roots and countersignature enforcement prior to Rack execution. - Dynamic Catalog Distribution: Added NATS Snake hot-loading for edge distribution of Wasm logic.
- PKI & Catalog CLI: Introduced
fluxrig keys gen-cluster,fluxrig wasm sign, andfluxrig wasm importcommands. - Path Sanitization: Added
pkg/utils/pathto centralize traversal-safe path handling.
v0.6.0 - 2026-06-06
Changed
- Release metadata only. This tag contains no source changes relative to
v0.5.0; the Wasm work intended for it was not merged and shipped inv0.6.1instead.
v0.5.0 - 2026-05-07
Changed
- Sovereign Identity Plane (v0.5.0 Foundation): Migrated the entire platform identity system to 128-bit UUID v7 (RFC 9562). This enhances entropy, ensures global uniqueness without centralized coordination, and provides time-ordered sequence integrity for high-performance storage indexes.
- Deduplication Logic: Updated NATS JetStream deduplication to utilize 128-bit identifiers, ensuring consistent exactly-once delivery across complex telemetry pipelines.
- Telemetry Hardening: Standardized the dotted metric naming schema (e.g.,
flux.gear.messages_in) across OTel, Prometheus, and DuckDB. - Directional Monitoring: Split unified I/O counters into distinct Inbound and Outbound channels for precise protocol translation metrics.
- Resource Guardrails: Implemented mandatory
MaxHops(64) andMaxPayloadSize(2MB) validation influxmsgto prevent bus exhaustion and "poison pill" scenarios. - Concurrency Resilience: Integrated global
PanicMiddlewareto ensure Rack stability during individual Gear failures and hardened mutex locking for atomic hot-reloads. - Mixer Reliability: Replaced fragile telemetry discovery with a robust recursive traversal engine, ensuring full visibility of historical Parquet data via the API.
- Security Hardening (CodeQL Certification):
- Fixed high-severity path traversal in scenario management by implementing robust name sanitization.
- Hardened TLS configuration in the
snakeserver with CA-based client verification support. - Resolved integer overflow/truncation risks in telemetry ingestion and ISO8583 codecs.
- Upgraded core dependencies (NATS Server v2.14, NATS Go v1.52) to address multiple upstream vulnerabilities.
CAUTION
DESTRUCTIVE CHANGE: This migration is a hard architectural break.
- Storage: Existing DuckDB databases (V3 and below) and cached
.fluxstate files are incompatible with this version. - API: REST handlers and NATS topics have transitioned from decimal integer IDs to standard UUID string representations.
v0.4.5 - 2026-04-29
Added
- Zero-Config Getting Started: Global Gears (a gear with no
deploytarget runs on every connected Rack), enabling scenarios that work without knowing Rack names in advance.
v0.4.4 - 2026-04-23
Added
- Enrollment Architecture: Implemented configuration-driven rack adoption with secure nonce-based passports.
- CBOR Migration: Transitioned internal wire-format to deterministic CBOR for binary stability.
- Data-Plane Integrity: Enforced technical UTF-8 validation and hex-encoded binary metadata handling.
- IO Stabilization: Implemented robust connection polling and rate-limited background WAL replay.
v0.4.3 - 2026-02-19
Added
- Documentation Website: Docusaurus-based documentation site with diagram support and full-text search.
v0.4.2 - 2026-02-15
Added
- Spec & Scenario Manager: CAS-backed spec/scenario management with CLI (
fluxrig spec,fluxrig scenario) and API integration. - E2E Test Suite: Comprehensive test runner for spec lifecycle, API scenarios, and concurrent access.
v0.4.1 - 2026-02-09
Added
- Coat Check Pattern: Implemented architectural pattern to handle "Detached State" during connection handovers.
- Bus KV: Implemented the
Bus.KV()key-value interface with a NATS backend, backing the Coat Check ticket store.
Changed
- IO Refactor: Decoupled TCP connection management from protocol logic.
- Gear Rename:
simple_tcp→io_tcp(renamed as part of the IO refactor above).
v0.4.0 - 2026-02-01
Added
- ISO8583 Native Gear (Alpha): First release of the high-performance payment switch gear.
- Telemetry Governor: Introduced QoS constraints for telemetry ingress to protect business traffic.
v0.3.0 - 2026-01-08
Added
- Bento Integration: Native support for the
warpstreamlabs/bentoecosystem, enabling the Bento connector ecosystem. The standard binary ships the Pure Logic and Local I/O sets; institutional connectors (Kafka, SQL, AWS) require a custom build. - Load Testing Suite: Integrated
e2e_loadcapabilities for stress testing.
Phase 2: core runtime
| Version | Date | Status | Summary |
|---|---|---|---|
| v0.2.0 | 2026-01-05 | Delivered | Observability Stack & TLS Foundations |
v0.2.0 - 2026-01-05
Added
- Observability Stack: Full OTel integration (Metrics, Traces) with DuckDB backend.
- Configuration V2: Unified TOML-based configuration schema.
- TLS Support: Enabled mutual TLS for internal bus and HTTPS for management API.
Phase 1: architecture & foundation
| Version | Date | Status | Summary |
|---|---|---|---|
| v0.1.0 | 2025-12-27 | Delivered | Initial engine architecture and Snake Protocol |
v0.1.0 - 2025-12-27
Added
- Foundation: Initial release of the 4-Repo Architecture.
- Snake Protocol: Secure tunneling implementation for Rack-to-Mixer connectivity.
- FluxMsg: Canonical JSON schema for inter-gear communication.