SIGN IN SIGN UP

fix(rustd): an SSRF-refused endpoint ends the event, and only objects are objects

Two calls from Indy, both taken.

**The SSRF classification flips to permanent.** `resolveTenant` omits
`SecretEndpointInvalid` from its permanent list, so it falls through the
`else` and a stored `base_url` pointing at the metadata service is
re-polled at the poll interval forever: the event never terminates, no
terminal row is written, and the only trace is a warn line repeating.
A stored URL does not become safe by being retried.

The row-parity this costs is documentary rather than operational. The
dual-run differ went with the Zig integration lanes (M175 §6), so
nothing compares the two daemons' rows at runtime; what grades Invariant
5 is REVIEW reading the ported SQL side by side, and this changes no
statement. Registered as a divergence beside the issue-time debit.

`Vault` deliberately stays transient next to the variant that moved. An
envelope that will not open is usually permanent too — a damaged row, a
rotated key — but it is also what a truncated read looks like, and those
recover. A stored URL is data this daemon parsed and rejected; an
unopened envelope is data it never got to see.

**The object gate is now one helper, applied at six boundaries.**
`afd_core::json::object_from_slice` — a drop-in for
`serde_json::from_slice` with the same signature and the same error
type, so every call site keeps whatever it already did with the failure.
Generic over `Deserialize<'de>` rather than `DeserializeOwned`, because
the request shapes that needed it most are `#[serde(borrow)]`.

Serde still does all the parsing. What is added in front of it is a
structural O(1) check — the first non-whitespace byte of a JSON object
is `{` and of nothing else is — chosen over the two pure-serde routes
for reasons the module records: deserializing through `Value` or `Map`
parses the document twice on a per-request path, and holds every string
in an intermediate with no destructor, which for a vault credential is
exactly the un-wipeable copy `SecretString` exists to prevent. A
per-type hand-written `Deserialize` is airtight and costs the derive.

`deny_unknown_fields` does not close this: an array has no field names
to be unknown.

The sites: the runner enrolment body and capability report, the JWT
header, the JWKS document, the OIDC discovery document, and the vault
credential that found it. None was a privilege escalation — validation
still ran after the parse in every case — but all six accepted a shape
their own contract forbids, and two of them are RFC-specified objects.

`serde_json` moves from a dev-dependency to a real one on `afd_core`,
since the gate it now ships is on production paths.

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