Skip to main content

Changelog

Keep a Changelog Semantic Versioning

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​

VersionDateStatusSummary
v0.13.02026-10-20DeliveredISO 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.02026-09-28DeliveredThe 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.02026-09-24DeliveredThe 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.02026-09-07DeliveredThe semantic layer stops being documentation: a spec's rules are enforced on traffic
v0.9.02026-09-03DeliveredEnrichment from outside the message, and correlation keys that survive a bus hop
v0.8.02026-08-24DeliveredEMV chip data: BER-TLV parsing with unknown-tag preservation
v0.7.12026-08-10DeliveredISO 8583 TLV length hardening
v0.7.02026-08-01DeliveredPayment 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_iso20022 and translate gears. A scenario can now bridge an ISO 8583 terminal network to the RTP network of The Clearing House. codec_iso20022 converts pacs.008.001.08 and pacs.002.001.10 to structured fields and back, driven by an ISO 20022 protocol spec. translate maps the fields of one spec to another, as a translation spec says. See the ISO 20022 codec, the translate gear and the bridge tutorial, which make test-robot-billpay runs.
  • Upload a spec or lookup data to the Mixer. POST /api/v1/specs, and fluxrig 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 by mixer.max_payload_size. A name:tag that already names other content gets 409.
  • 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 --activate activates 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 by fluxrig-ent). Their reference blocks are left as they are and listed, instead of failing the run. make gear-docs skips sim_source and sim_responder.
  • iso8583-tool sends a bill payment. New auth flags: -auth-proc-code, -auth-amount, -auth-currency, -auth-merchant-id and -auth-rrn (auto derives the RRN from the date and the STAN). -auth-report is 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 with error.reason=overloaded instead. The default is 0 (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_order sets the drain order. With subject (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 to rack.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. 0s turns the wait off. With none, 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/dropped and flux.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 every observability.embedded.rotation.check_interval (1 hour by default). Before this, nothing removed old telemetry and the Parquet tree grew without bound. The sweep deletes .parquet files only: rows in the DuckDB telemetry_* tables are not removed.
  • observability.embedded.rotation.check_interval and observability.embedded.rotation.max_age_days are now read. max_age_days is the fallback when retention_days is not positive.
  • snake.cluster_name and snake.store_dir are 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_dir empty keeps the default, a snake directory under store.dir.
  • Dead letter queue. A message no consumer can process is now kept instead of dropped. The Mixer provisions a flux-dlq stream, 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 (default flux.dlq), snake.dlq_max_age (default 720h).

Changed​

  • BREAKING: the reference ISO 8583 spec packs a standard bitmap. examples/specs/iso8583-v87-ascii.yaml packed 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 now 3.0.0, because the store keeps every version unchanged: a Mixer that already holds iso8583-v87-ascii:v2.2.0 keeps serving the old layout under that name. Upload examples/specs/iso8583-v87-ascii.yaml and change each iso8583-v87-ascii:v2.2.0 reference to iso8583-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. Set api.auth_token, or api.auth_disabled_dangerously = true for a test.

  • on_error: reject emits on an error port. codec_iso8583, codec_iso20022 and translate send a rejected message out of their error port, with error.reason, error.gear and error.message, so a scenario can answer the sender. Before, codec_iso8583 returned the error to the runtime, which logged it and dropped the message. An unwired error port 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, default fluxrig_registry), not in the Mixer's DuckDB file or under store.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 pending unless enrollment.auto_adopt is 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_file with a .secret suffix). 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 pull request needs an issue, and community pull requests target dev. CONTRIBUTING.md states the rule, a pull request template asks for Closes #<issue>, and a check fails a community pull request that references no issue or that does not target dev.

  • Robot runs keep their workspaces under the run root they print. Each suite works in <run root>/work/<suite directory>, and run.sh keeps the newest 10 run roots under /tmp/fluxrig (FLUXRIG_ROBOT_KEEP_RUNS, 0 keeps 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. --scenario still 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 after registry.liveness_ttl (90s, three heartbeat intervals). The API reports online for 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) and write_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 than registry.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 legacy bus.* aliases, store.state_file, store.scenario_file and the Mixer's mixer.mixer_name are 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.RandomSuffix returns 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 now RandomSuffix(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.yaml forwarded 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.

  • coatcheck bounds the awaited store write. store_timeout (default 5s) 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_iso8583 decodes a fixed-length field at its declared width. The library strips a field's padding when it unpacks, so DE 11 000042 decoded as 42, an all-zero DE 3 as 0, and the MTI in Data as 200. The wire re-encoded correctly, but Data and 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's deploy:. A gear with no deploy target 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_dir can 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_tcp client. A Conductor port is available only after the I/O gear at its end reports conn.up, and only io_iso8583 reported it. A route that ended at an io_tcp client failed every request with no_destination. The io_tcp client 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-tool packs 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 reads 0210, not 210, 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 to flux.nats.messages_received. flux.nats.bytes_received and flux.nats.bytes_published were 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. Load split a reference at its last colon by hand, so a urn:flux:spec:visa:latest reference read the name as urn:flux:spec:visa and looked up the wrong artifact. A single parser now owns both the short name:tag form (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_ttl as its bucket-wide TTL, so every key lived that long whatever the message asked for: a key whose meta.coatcheck.ttl was 30 seconds still sat for max_ttl (5 minutes), and a slow or crashed daemon extended that to max_ttl past the crash. The store now writes each entry with the TTL its message resolved to (the override, else default_ttl, capped at max_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 on index.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 its coatcheck.ttl passes and a fresh one is written under the new key.
  • coatcheck restore's on_missing now defaults to error, not forward. An unset on_missing used 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 set on_missing: forward explicitly. An unrecognized value is now also an error, where it used to behave like forward too.
  • io_tcp in client mode now applies configured framing to outbound writes. It used to write RawPayload straight 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's idle_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 needs idle_timeout raised or set to 0 to 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 a known_origins allowlist. 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 setting known_origins is opt-in for narrowing further. A rejected origin now emits on the error port with the new reason invalid_origin, so a consumer that switches on error.reason needs 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/health now requires Authorization: Bearer <api.auth_token>, compared in constant time. Set api.auth_token before upgrading, or the Mixer will not start; the explicit opt-out is api.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 /racks endpoint 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 no secret in 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.duckdb by 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_secret is a new Rack-side setting (default "fluxrig", matching the Mixer's enrollment.bootstrap_secret default) 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 pending Rack it names to active. 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-pending Rack 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-msg stream 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, 0s used 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_interval and rack.lane_send_timeout are unchanged: 0/0s is their documented sentinel for "use the built-in default" or "disabled", not a mistake to clamp.
  • Two pkg/bus Go signatures changed, for anyone building against this package directly rather than through a scenario. bus.Handler now returns error (the ack-after-success fix above is what a returned error drives); NatsBus.Purge now takes a subject string parameter (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_BYTES and similar) silently changed nothing; the Rack's mapper was missing a telemetry_ entry, so every FLUXRIG_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/shutdown now 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.NewEntityID now panics if uuid.NewV7 fails, 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 of idgen is 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_tls is 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_buffer is 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, not 500. 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: new error.reason value invalid_origin, emitted when a reply's origin stamp fails the format check or the known_origins allowlist.
  • 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 bare nil used 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 of bufio.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 of include_values. It was only decoded and applied when include_values was set (default false), so a scenario relying on the override without also asking for full values had it silently ignored in favor of default_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 configuring value_fields for 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 through Get("iso8583.unknown_tags") (and coatcheck's key_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, BootstrapSecret and AuthToken are never serialized (json:"-", and explicitly cbor:"-" 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 FluxID is set and a hop appended. The clone covers Path only — Data and Metadata are 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 carried root_ca_file only.
  • 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/FlushArchiverBuffer use 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 snake directory of the Mixer's store before the first start if you may need to go back. The key is derived from cluster.key: replacing that file makes the store unreadable, unless you first print the derived key with fluxrig keys store-key and keep it as snake.store_old_key_file. To use a key of your own, set snake.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_tcp server 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_tcp server gear cannot route unambiguously is now refused instead of guessed at or silently queued. A message naming a conn.id whose 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 no conn.id while 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.
  • Activate now fails when a scenario's deploy pins reach no Rack at all, not only when one names a Rack that was never enrolled. A deploy: pin not listed under the scenario's own racks: 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 (default 2s) 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 to cmd/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_DIR points the suites at another set of binaries. The e2e helpers (test/e2e/utils/e2e_utils.sh) honor FLUXRIG_BIN_DIR too. 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.flux beside its passport, mode 0600; 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" or lane: "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) and rack.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, and fluxrig keys store-key prints 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) and snake.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_tcp gains max_buffered_messages (default 1000): the cap on outbound messages queued when nothing is connected yet, or a message names no conn.id. It was an unconfigurable, hardcoded 1000 before. The oldest is dropped past the cap, now counted on flux.gear.messages_dropped.

  • api.control_confirm_timeout (default 2s): 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_tcp server 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.layout of 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 on GetField(), 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 full snake.initial_retry_* schedule. 0s restores 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. 0s turns the probe off.
  • snake.initial_retry_wait, snake.offline_start_timeout, snake.offline_retry_interval and rack.lane_send_timeout discarded 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 in decode() after Unpack succeeded read as a successful, empty decode. Only a panic inside Unpack itself 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 reached Process() 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 decode had no panic recovery around Unpack, 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 of autoAdopt, racing SetAutoAdopt.
  • 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_tcp server 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.
  • Unsubscribe on 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_tcp client, 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_bytes back to unlimited when set to 0, unlike snake.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_iso8583 gains validation: off (default), warn or enforce. Per-MTI field usage, the conditions that decide whether a rule applies, closed value sets and cross-field checks are answered against every message the codec decodes or encodes. A rejection follows on_error, exactly as a decode failure does; violations travel on the message as codec.violations and are counted on flux.iso8583.violations by severity, kind and MTI.
    • The default is off deliberately: a message accepted yesterday must not be rejected today because the code was upgraded. warn is how you find out whether your spec matches your traffic before enforcing it.
  • 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 carries checks, written in a total, side-effect-free expression language over the message being validated.
    • A check carries a severity: reject fails the message, warn records 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.kind says it does. amount, date, time, datetime and the new numeric kind compare as numbers on every operator, so a rule can write field(4) == 1000 rather 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". pan is deliberately excluded, because a leading zero makes it a different card.
  • fluxrig spec doc renders the protocol reference from the spec. The Mixer serves it too, at GET /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 public omits every field marked scope: private and states how many it withheld; --scope complete is 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> and GET /api/v1/specs/{name} return every version of one spec, newest first by version rather than by arrival. fluxrig spec list and the /specs listing now carry when each version was filed, its size, the document's title and which version latest reaches; --json on both.

Changed​

  • BREAKING: a spec declares where its wire layer comes from. Every spec now carries a wire block: source names a Moov base (moov:<name>) or a wire document beside the spec, fields states what the dialect changes, and each is valid alone. Specs in the legacy vocabulary (meta: with a top-level fields: 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.id and spec.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_iso8583 manifest: on_error is documented as defaulting to drop. It has always defaulted to drop; the manifest said reject, so an operator reading the published contract configured for one behaviour and got the other.

Fixed​

  • unknown_tags: drop rejected instead of dropping. The policy documented three behaviours and implemented two: anything that was not preserve took 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.
  • VerifyConnectivity could 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; numeric also 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 role label reaches its description, and two gear labels, peer and peer_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. GetValue descended only into map[string]any, but CBOR decodes a nested object as map[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 bento gear 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 bento gear in a relay path dropped the wire bytes. RawPayload survived 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 viz drew 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 under iso8583.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 fluxMsg as bytes rather than as a string, including when reached through an alias or as a composite subfield. Gears and pipelines reading such a field now receive []byte where they received a string. 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 dedicated error port. Availability sensing binds each local destination to its uplink link-state. Validated end-to-end by the payment_switch e2e 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 doc generates 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 nobento compiles 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 emit path 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_tcp and bento config schemas now declare every field the gears actually accept (previously undocumented options such as io_tcp delimiter framing and bento log_level).

Phase 3: open & flexible logic​

VersionDateStatusSummary
v0.6.12026-07-20DeliveredWasm runtime, supply chain security, polyglot gears
v0.6.02026-06-06DeliveredRelease metadata only, no code changes
v0.5.02026-05-07DeliveredSovereign identity (UUID v7) & telemetry hardening
v0.4.52026-04-29DeliveredDocumentation Hardening & Zero-Config
v0.4.42026-04-23DeliveredLogic Extensibility & Secure Enrollment
v0.4.32026-02-19DeliveredOperational Resilience & NATS V2
v0.4.22026-02-15DeliveredSpec Management & E2E Automation
v0.4.12026-02-09DeliveredStateless Context & I/O Decoupling
v0.4.02026-02-01DeliveredISO8583 Native Gear & Telemetry QoS
v0.3.02026-01-08DeliveredBento 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 .wasm modules 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, and fluxrig wasm import commands.
  • Path Sanitization: Added pkg/utils/path to 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 in v0.6.1 instead.

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) and MaxPayloadSize (2MB) validation in fluxmsg to prevent bus exhaustion and "poison pill" scenarios.
  • Concurrency Resilience: Integrated global PanicMiddleware to 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 snake server 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 .flux state 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 deploy target 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/bento ecosystem, 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_load capabilities for stress testing.

Phase 2: core runtime​

VersionDateStatusSummary
v0.2.02026-01-05DeliveredObservability 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​

VersionDateStatusSummary
v0.1.02025-12-27DeliveredInitial 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.