- Releases
- Ironflow v0.25.1
Ironflow v0.25.1
A hardening release. Two phases of a line-by-line review of internal/ landed here, closing roughly forty findings across the storage layer, config, crypto, observability and the circuit breaker. No new features — but tenant SQL isolation is now structural rather than parsed, several silent-corruption bugs in the SQLite store are fixed, and four changes need a look before you upgrade.
Breaking Changes
Section titled “Breaking Changes”- Unknown YAML keys are now an error. A config file carrying a stray key that was previously ignored will refuse to boot.
spec.licensestays allowlisted so legacy files still start. Every YAML this repo ships passes — 4 examples, 4config initscaffolds, the Helm configmap, and the 4 public doc snippets (#1448) storage.driver: postgreswith nourl:now fails loudly instead of silently downgrading to SQLite. This one was worth breaking: a cluster in this state ran every node on its own private.ironflow/ironflow.db, sharing nothing, while the startup banner read “PostgreSQL” (#1448)- Entity IDs matching the reserved
ironflow:prefix in any letter case are now rejected.ValidateEntityIDcompared case-sensitively while the SQL that hides platform-internal streams usesLIKE— case-insensitive on SQLite, case-sensitive on Postgres — so an ID likeIronflow:tenant-42persisted and then listed inconsistently across backends. If you already have such a stream, appends now fail permanently with403/CodePermissionDeniedwhile reads keep working; no backfill ships with this change. The detection query is inCHANGELOG.mdand is expected to return zero rows on essentially every deployment (#1485) - Metrics surface reduced.
step_nameis dropped fromironflow_steps_totalandironflow_step_duration_seconds— durable step IDs are user-supplied and our own docs teach interpolating request data into them, so the label was unbounded by design. Nine metrics with no emitters are deleted, along with the two Helm alerts and the Grafana panel that read them (those alerts returned no data rather than breaching, so a lagging projection paged nobody). Connect RPC paths now collapse to/ironflow.v1.IronflowService/, losing per-method breakdown onironflow_http_requests_total(#1447)
Security
Section titled “Security”- Tenant SQL isolation is structural instead of parsed. SQLite now shadows every table in the schema with a temp view — environment-filtered for the six readable tables, always-false for the rest — and Postgres sets
search_path = pg_temp. A query the checks fail to understand now reads nothing instead of everything. This replaces two hand-written SQL parsers that were each bypassed, verified live returning another environment’s plaintext HMAC secret. Projectionwherefilters get the same treatment from the other direction: they are parsed into(column, op, value)triples and emitted as bound parameters, so the caller’s fragment never reaches SQL (#1477) Decryptrejects wrong-length nonces. Go’s GCM panics rather than erroring when a nonce isn’t 12 bytes, and the call was reachable from a bare goroutine in the cron scheduler with norecover— so a truncated or pre-encryption nonce killed the process. Separately, dev-mode passthrough returned ciphertext verbatim with a nil error, handing a function base64 ciphertext as its secret value. Genuine dev-mode secrets still pass through unchanged (#1444)- NATS URLs are redacted before they reach logs, banners and connect errors;
${VAR}is substituted into parsed YAML values rather than raw bytes, so a secret containing&,*,#or a newline can no longer re-parse as YAML structure;--devechoes only loopback CORS origins instead of*, since dev mode bypasses auth entirely; and the Helm chart stops routing/metricsthrough the public ingress (#1449) /metricsis throttled — 2 max in-flight requests and a 5s timeout. It is unauthenticated, unthrottled and fans out to two per-scrape DB collectors (#1447)
- SQLite silently zeroed
CURRENT_TIMESTAMPwrites.parseTimeaccepted only RFC3339Nano, but the capacity-gated terminal paths writeruns.updated_at/ended_atin SQLite’sYYYY-MM-DD HH:MM:SSform — so those runs reportedupdated_atof0001-01-01, and nothing healed the row because every later write is guarded on a non-terminal status. Postgres scans both shapes, so the same run rendered differently per backend. Also in the same fix: connection pragmas were lost when the pool replaced a connection, and migrations were non-atomic (#1484) - Deleting an environment could leak every one of its blobs. The blob sweep inherited the caller’s request context, which
net/httpcancels on client disconnect — so a disconnect after the DB cascade committed left the rows gone and all blobs alive, with no reconciler and noironflow blobscommand to reclaim them. The sweep now runs detached with a 2-minute ceiling (#1482) - A dashboard event page has been sorting the environment’s whole events table. Migration 024b rebuilt the events table from a schema snapshot predating migration 022, silently reverting it on SQLite only; the composite index is restored (#1479)
spec.observabilitywas parsed and then dropped, so a documented deploy got neither traces nor/metrics(#1448)- The time-travel scrubber blanked the entire dashboard on a run with no steps yet — protojson omits empty repeated fields, and an unguarded
.map()threw during render with no ErrorBoundary to catch it (#1493) - The circuit breaker admitted everyone into half-open instead of one probe, and
Registry.Getoverwrote a caller-suppliedWithOnStateChangeinstead of chaining it (#1445) - Events timestamp ordering and blob rows orphaned by cascade delete (#1486);
ListRunstiebreaker for stable pagination (#1485); a lazy S3NoSuchKeysurfacing at read time is now classified as permanent blob loss rather than a transient backend fault (#1449)
Improvements
Section titled “Improvements”- Postgres step-timeout sweeps got their indexes back. Two partial indexes keyed on columns that appear in no
WHERE,JOINorORDER BYanywhere in the repo, while the sweeps range and order onwait_timeout— so they sorted their whole waiting partition.ClaimTimedOutWaitingStepsgoes from 18.7ms / 5011 buffers to 0.38ms / 202 on 500k rows, a gap that widens as the parked set grows (#1488). The three SQLite step indexes had the mirror-image problem — keyed on the two columns their own partial predicate already pins to constants, giving them one distinct key value and zero selectivity (~6x) (#1479) - The migration suite now asserts the schema. An index deletion changes performance, not behaviour, so every contract test still passed — the class was invisible. Cross-backend parity is enforced in both directions, so an allowlist entry that stops describing a real divergence fails too (#1486)
- Config fields that were parsed and ignored now do something. The seven inert
storage.pool.*/nats.maxMemory/engine.*fields are wired at their call sites, andValidateparses every duration and byte-size string and range-checks the numeric knobs — soironflow validateandironflow servefinally agree on what a file means (#1448) serveandvalidatewarn for each environment variable that-fsilently drops, and an unencrypted stored secret namesironflow secret setas the remedy instead of reporting a nonce length (#1449)
Ironflow Desktop ships on its own cadence — its changes are in the Ironflow Desktop v0.9.0 changelog, not here.