SIGN IN SIGN UP

refactor(rustd): delete the hand-rolled parsers Zig had no library for

Indy asked what `GateRef` actually was. It was me carrying a Zig-ism:
`approval_gate_async.zig` packs the reference into
`"action_id|deadline_ms"` and splits it back by hand, because Zig has no
serializer and a pipe-delimited pair is the cheapest thing to write.

Nothing else reads that key — one writer, one reader, both on the lease
path — so the format was never a contract, only the shape a hand-rolled
encoder happened to produce. It is a serde type now. The separator, the
split, the "what if an id contains a pipe" question and the two
functions that answered it are gone, and `#[serde(try_from)]` runs the
domain validation on EVERY read instead of the ones a caller remembered.

Three more of the same shape, found by looking rather than by being
asked:

**The stored vocabularies.** `Answer` and `Status` were `match` arms
over string literals — which is precisely what `runner::policy`'s own
`parse_wire` doc argues against: a hand-written match is a second copy
of every variant's name, and the failure it causes (a row one release
writes that the next cannot read) has no failing test behind it. Both
are `#[serde(rename)]` declarations now. `parse_wire` itself was a
crate-private helper with one consumer and is now
`afd_core::spelling::from_spelling` with three.

**The URL parser.** `base_url_guard.zig` scans for `://`, finds the
first `/?#`, takes the last `@`, and looks for a `:` unless the
authority opens with `[`. I ported that by hand and justified it on wire
parity — a justification that turned out to be wrong when checked: the
runner compares allowlist entries with `std.ascii.eqlIgnoreCase` at all
three of its matching sites, so normalisation is harmless. It is
`url::Url` now, already in the lock, and the win compounds: `Host` comes
out TYPED, so `ssrf` classifies an `Ipv4Addr` directly with no string
round trip and its bracket-stripping and zone-id handling disappear
along with the parse.

Three verdicts change, and every one is the parser agreeing with what an
HTTP client would actually dial — the only property this guard is for:

- `https:///just/a/path` dials `just`, where the Zig called it malformed
  and protected nothing. The SSRF check runs on that host, so
  `https:///169.254.169.254` is refused exactly as the two-slash
  spelling is.
- `https://256.1.1.1/v1` is refused. The Zig's `parseIpv4` failed,
  concluded "not a literal", and passed it through as a NAME — the
  unsafe direction.
- A schemeless host reports `InvalidScheme` rather than the parser's
  "relative URL", which is the diagnosis an operator can act on.

Two files crossed the length cap on the way and were split at real
seams, not at line counts: `gate/pending.rs` into the reference and the
vocabularies it resolves through, and `config/gates.rs` into approval
rules and anomaly rules — an approval rule asks a HUMAN and an anomaly
rule asks nobody, which is why `approval_gate_anomaly.zig` is its own
module upstream too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
K
Kishore Kumar committed
3fa990ec63d6402dc6169cbb487b762edd2fb70c
Parent: c84e126