feat(daemon): serve and migrate subcommands, and the error standard they follow
Closes two gaps I had wrongly reported as done, and fixes the error handling
Indy called out.
**`agentsfleetd serve` exists now.** Rubric row R1 is a P0 — `curl -fsS
localhost:3000/readyz` after `agentsfleetd serve` → 200 — and there was no
`serve` subcommand at all; `main` ran preflight and stopped. Boot order is
`cmd/serve.zig`'s (knobs → Postgres → Redis → router → listen), with listen
LAST so every refusal above it is a refusal rather than an outage. Verified
against compose: `/readyz` 200, `/healthz` 200, oversize head 431, SIGTERM
exits clean with the port closed.
The accept loop drives hyper directly because `axum::serve` cannot bound the
header buffer and Dimension 5.3 is a bound on the header buffer. `accept()` is
a trait (M-MOCKABLE-SYSCALLS) so a failed accept — EMFILE, a peer that reset —
is provable: the loop logs and keeps serving, where returning would leave a
process alive, holding its port, accepting nothing. That is the worst shape of
outage, because every liveness probe still passes.
**`agentsfleetd migrate` exists now**, also promised by Files Changed. It reads
`DATABASE_URL_MIGRATOR`, deliberately not the serving role: the two are
separate grants, and a migrate path that fell back to the API URL would either
need DDL rights on the serving role or fail at the first CREATE TABLE with a
permission error that reads like a bug. It asks for no master key and no Redis
— a migration job that demanded them would carry credentials it has no use for,
which is how a job container ends up holding the KEK. Applied 47 migrations to
a fresh database and was a no-op on the second run.
**Error handling, per Indy.** `docs/RUST_ERROR_STANDARD.md` states the rule and
`AGENTS.md` (repo-owned, survives `orly update`) points at it. The shape is
`core_api`'s — 10 crates, `pub type Result<T>`, flat `Error`, `From`
composition — not invented here.
Crates with a Result alias 0 → 5
Explicit `Result<T, Error>` 55 → 0
From / #[from] conversions 2 → 7
map_err in agentsfleetd 5 → 0
Crates whose source() returned themselves 4 → 0
The `source()` bug was in EVERY crate that had an error type: `Display` renders
`[code] {kind}` and `source()` returned that same kind, so a chain walker
printed each message twice before reaching anything new. It also published a
`pub(crate)` type through a public trait.
Three tests asserted `source().is_some()` for every variant, which is the wrong
invariant — a variant holding only data (`MissingDatabaseUrl { knob }`) has no
cause, and nothing caused an unset variable. They now assert the real one:
where there IS a source, it is not a repeat of our own message.
`BootFailure` was stringifying errors into itself, which compiled, read fine,
and silently defeated the fatal renderer built to walk the chain. Now:
✗ agentsfleetd cannot boot: the API database would not answer
caused by: [UZ-INTERNAL-001] the api datastore is unreachable
caused by: error communicating with database: Connection refused
caused by: Connection refused (os error 61)
**Also fixed, all Indy-raised:**
- `AFD_GIT_COMMIT` was read by `/healthz` and set by nothing, so every build
ever made reported `"commit":"unknown"` — a field that can only say unknown
looks like a broken build. `afd_api/build.rs` stamps it, marking a dirty tree.
- The `M177`/`M178` consts are gone. Milestone ids are project metadata and a
daemon has no use for them; renumbering would have meant editing a shipped
binary. The porting ledger lives in `concurrency.md` alone.
- The database-URL resolution was copied into both Rust lanes. One
`RUSTD_RESOLVE_DB_URL` definition now, for the reason the tally guard has one.
- `make gen-error-codes` shelled to `zig build` and was in no gate. Deleted;
the debt is recorded as an M181 deferral, because the Rust registry carries 22
codes against Zig's 125 and the published reference now has no generator.
- `afd_observability` declared `test-util = []` unlocking nothing, and its only
test was `#![cfg(feature = "test-util")]` — so a plain `cargo test` ran ZERO
tests there. Feature and gate both removed; it runs 4 now.
make lint-rustd ✓ Lint passed
make test-unit-rustd 550 passed; 0 failed
make test-integration-rustd 47 tests against live services
make harness-verify ALL GATES GREEN
Coverage is 98.37% lines (4274/4345), DOWN from 100% and below the codecov
target. The 71 uncovered lines are all in this commit's new code — `serve::run`,
`migrate`'s connect path, and `main`'s two subcommand arms. They are reachable
and want integration tests; this is a gap to close, not an exemption to take.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> K
Kishore Kumar committed
2bc250c2babbcaf42b0171c577ddf489d34d392e
Parent: 682a8b5