Skip to content

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.

  • Unknown YAML keys are now an error. A config file carrying a stray key that was previously ignored will refuse to boot. spec.license stays allowlisted so legacy files still start. Every YAML this repo ships passes — 4 examples, 4 config init scaffolds, the Helm configmap, and the 4 public doc snippets (#1448)
  • storage.driver: postgres with no url: 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. ValidateEntityID compared case-sensitively while the SQL that hides platform-internal streams uses LIKE — case-insensitive on SQLite, case-sensitive on Postgres — so an ID like Ironflow:tenant-42 persisted and then listed inconsistently across backends. If you already have such a stream, appends now fail permanently with 403 / CodePermissionDenied while reads keep working; no backfill ships with this change. The detection query is in CHANGELOG.md and is expected to return zero rows on essentially every deployment (#1485)
  • Metrics surface reduced. step_name is dropped from ironflow_steps_total and ironflow_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 on ironflow_http_requests_total (#1447)
  • 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. Projection where filters 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)
  • Decrypt rejects 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 no recover — 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; --dev echoes only loopback CORS origins instead of *, since dev mode bypasses auth entirely; and the Helm chart stops routing /metrics through the public ingress (#1449)
  • /metrics is 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_TIMESTAMP writes. parseTime accepted only RFC3339Nano, but the capacity-gated terminal paths write runs.updated_at / ended_at in SQLite’s YYYY-MM-DD HH:MM:SS form — so those runs reported updated_at of 0001-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/http cancels on client disconnect — so a disconnect after the DB cascade committed left the rows gone and all blobs alive, with no reconciler and no ironflow blobs command 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.observability was 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.Get overwrote a caller-supplied WithOnStateChange instead of chaining it (#1445)
  • Events timestamp ordering and blob rows orphaned by cascade delete (#1486); ListRuns tiebreaker for stable pagination (#1485); a lazy S3 NoSuchKey surfacing at read time is now classified as permanent blob loss rather than a transient backend fault (#1449)
  • Postgres step-timeout sweeps got their indexes back. Two partial indexes keyed on columns that appear in no WHERE, JOIN or ORDER BY anywhere in the repo, while the sweeps range and order on wait_timeout — so they sorted their whole waiting partition. ClaimTimedOutWaitingSteps goes 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, and Validate parses every duration and byte-size string and range-checks the numeric knobs — so ironflow validate and ironflow serve finally agree on what a file means (#1448)
  • serve and validate warn for each environment variable that -f silently drops, and an unencrypted stored secret names ironflow secret set as 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.