Path safety now lives in one place. The NUL-byte and drive-letter rules
move from a server-only helper into the store's own filename validation,
so uploads reject those paths at write time instead of only the ZIP read
path catching them. The download still re-checks, because artifacts
stored before the rule existed can still carry an unsafe path, but it
now skips a bad path rather than failing the whole archive.
Promote is_boundary_stage to RunProjection and drop the three identical
private copies. The ZIP download used a node-name match instead, which
would have dropped artifacts from a working node that happened to be
named "start".
Compress the archive. Entries were Stored while the response was also
excluded from transfer compression, so text artifacts moved at full
size. async_zip gains the deflate feature; async-compression and flate2
were already in the lock file.
Log archive failures unconditionally. The send-succeeded guard meant a
client that had already disconnected left no record at all, which is the
case where the log is the only evidence.
Also: collapse the duplicate 500 arms, drop the dead stage-ID tiebreaker
and the cached order in the selection map, name the accessible label
after the visible one, share the run URL prefix between the two download
href builders, and document the mid-stream truncation behavior in the
OpenAPI description.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
This moves workflow-visible variables from JSON file storage into
SQLite-backed storage, establishing the first durable SQL table while
preserving the existing variable API behavior.
## What Changed
- Added a `fabro-db` crate with bundled SQLite, an embedded migration
for the `variables` table, and a `Database` owner for `connect()`,
`migrate()`, `health_check()`, and pool access.
- Replaced the `fabro-variable` JSON file store with an async
SQLx-backed `VariableStore` that preserves sorted listing,
case-sensitive names, empty string values, name validation, and
description-preserving upserts.
- Wired server startup to create `<storage>/db/fabro.sqlite3`, run
SQLite migrations, import legacy variables when needed, and pass the
shared pool into server state.
- Grouped live server stores under `AppStores` so runs, variables,
vault, environments, and automations share one state boundary while
artifacts remain separate.
- Updated variable handlers, run creation, validation, and test support
for async SQLite-backed variable access.
- Added schema, store-level, legacy import, and API-level persistence
coverage for variables.
## Legacy JSON Migration
On startup, Fabro looks for `<storage>/variables.json`. If it is
missing, startup is a no-op for legacy variables.
If the file exists, Fabro parses and validates the full file before
mutating SQLite. Valid entries are inserted with `ON CONFLICT(name) DO
NOTHING`, so existing SQLite values remain authoritative and only
missing names are imported from the legacy file.
After a successful import transaction, the source file is renamed to a
timestamped backup such as `variables.json.imported-<timestamp>.bak`. A
later startup naturally skips the import because the original source
path no longer exists. Invalid JSON or invalid variable names leave the
source file in place for operator repair.
Variable values are not logged during import. Logs include only safe
metadata such as source/backup paths, row counts, and variable names.
## Verification
- `cargo nextest run -p fabro-db -p fabro-variable`
- `cargo nextest run -p fabro-server --features test-support variables`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)