Summary
RegistrationService.finalizeServerOrder documents that a terminal
certificate-order failure leaves the lifecycle at PENDING_VALIDATION
"so the operator can cancel and re-register without the ANS name being
burned by a FAILED agent" (internal/ra/service/lifecycle.go:702-703).
That guarantee does not hold: the ANS name is consumed the moment a row
exists in agent_registrations, and it stays consumed after cancel.
Current behavior
- Re-registration is gated by
preflightRegistrationConflicts, which
calls ExistsByAnsName (internal/ra/service/registration.go:579).
ExistsByAnsName is status-blind:
SELECT COUNT(1) FROM agent_registrations WHERE ans_name = ?
(internal/adapter/store/sqlite/agent.go:328-335).
- Cancel/revoke retires the row to
REVOKED but does not remove it, so
the ans_name still matches → re-register fails with ANS_NAME_TAKEN.
Net: a name is burned by a registration that never reached ACTIVE,
which is exactly what the comment says won't happen.
Expected behavior
An ANS name should only be consumed once an agent becomes ACTIVE (i.e.
once an AGENT_REGISTERED leaf is sealed into the TL and the name is
resolvable). A registration that fails or is cancelled before activation
should free the name for re-registration by the same owner.
Root cause
In-flight registrations and committed registrations share one table
(agent_registrations) keyed on ans_name. There is no separation
between "an application in progress" and "a name that is live in the
registry," so name exclusivity is enforced from first insert rather than
from activation.
Proposed fix
Introduce a dedicated agent_application table for in-flight
registrations. A name is promoted into agent_registrations
(and thus becomes "taken") only at activation. Pre-active cancel/failure
simply drops the application row, freeing the name.
- Names are never taken until an agent becomes ACTIVE.
- Removes the status-blind exclusivity check as the burn point.
Scope / acceptance criteria
Notes
Preserve the existing invariant that names which did reach ACTIVE
(TL leaf sealed, potentially resolved/cached by clients) remain
permanently burned. The security boundary is "was this name ever
ACTIVE," not "did it pass verify-acme."
Summary
RegistrationService.finalizeServerOrderdocuments that a terminalcertificate-order failure leaves the lifecycle at
PENDING_VALIDATION"so the operator can cancel and re-register without the ANS name being
burned by a FAILED agent" (
internal/ra/service/lifecycle.go:702-703).That guarantee does not hold: the ANS name is consumed the moment a row
exists in
agent_registrations, and it stays consumed after cancel.Current behavior
preflightRegistrationConflicts, whichcalls
ExistsByAnsName(internal/ra/service/registration.go:579).ExistsByAnsNameis status-blind:SELECT COUNT(1) FROM agent_registrations WHERE ans_name = ?(
internal/adapter/store/sqlite/agent.go:328-335).REVOKEDbut does not remove it, sothe ans_name still matches → re-register fails with
ANS_NAME_TAKEN.Net: a name is burned by a registration that never reached ACTIVE,
which is exactly what the comment says won't happen.
Expected behavior
An ANS name should only be consumed once an agent becomes ACTIVE (i.e.
once an
AGENT_REGISTEREDleaf is sealed into the TL and the name isresolvable). A registration that fails or is cancelled before activation
should free the name for re-registration by the same owner.
Root cause
In-flight registrations and committed registrations share one table
(
agent_registrations) keyed onans_name. There is no separationbetween "an application in progress" and "a name that is live in the
registry," so name exclusivity is enforced from first insert rather than
from activation.
Proposed fix
Introduce a dedicated
agent_applicationtable for in-flightregistrations. A name is promoted into
agent_registrations(and thus becomes "taken") only at activation. Pre-active cancel/failure
simply drops the application row, freeing the name.
Scope / acceptance criteria
agent_applicationtable + store/port for in-flight state.ANS_NAME_TAKEN) enforced against activeregistrations only; already-ACTIVE/DEPRECATED names stay burned
(identity-confusion protection — a name with a TL leaf must never
be reused).
by the same owner succeeds.
finalizeServerOrdercomment matches actual behavior.internal/domainstays at 100%,make checkpasses.
Notes
Preserve the existing invariant that names which did reach ACTIVE
(TL leaf sealed, potentially resolved/cached by clients) remain
permanently burned. The security boundary is "was this name ever
ACTIVE," not "did it pass verify-acme."