Skip to content

fix(client): make abort actually cancel, and keep the mismatch escape hatch alive - #100

Open
V3RON wants to merge 1 commit into
feat/0003-06-docsfrom
fix/0003-07-client-abort
Open

fix(client): make abort actually cancel, and keep the mismatch escape hatch alive#100
V3RON wants to merge 1 commit into
feat/0003-06-docsfrom
fix/0003-07-client-abort

Conversation

@V3RON

@V3RON V3RON commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Stacked on #99. Two blocking defects from the adversarial review of this stack.

The abort signal was a silent no-op that handed the caller the lease it abandoned

The client always sent an explicit requesterId on the abort's lease.cancel, and lease.cancel's authorize required requesterId === principal for a non-admin session. Two independent triggers:

  1. With no caller-supplied principal, the client stored "" while the daemon fixed the connection principal to its own defaultRequesterId — and hello's reply did not return it, so the client could not know it. The cancel was FORBIDDEN.
  2. ADR §4's central proxy case — one connection with principal: "host" leasing under requesterId: "agent-7" — was forbidden outright. That is the scenario the whole ADR exists for.

In both cases the bare catch (commented as handling only a dead connection) swallowed the rejection and returned the original request promise, so the caller's await resolved with a real grant and the client tracked it as held. ADR §10 requires the opposite: "the caller never holds a lease it abandoned."

Fixed properly rather than papered over:

  • hello's reply now carries the resolved principal, and the client adopts it instead of "".
  • lease.cancel is now owner-aware. A new QueueControl.pendingRequestOwner(requesterId) reads the pending request's recorded ownerId (already stored on the waiter), threaded through LeaseEngine into the dispatcher's AuthorizeContext. authorize compares the pending request's owner to the session principal, so the §4 proxy can cancel what it created. No core schema change was needed.
  • The catch is narrowed to transport-kind errors, so a FORBIDDEN/BAD_REQUEST from the cancel surfaces instead of being misread as "the connection died".

lease.request is now explicitly documented as deliberately authorize-free, resolving the incoherence the review noted (it accepted any requesterId while lease.cancel refused one).

The client closed the socket on a protocol mismatch, killing §6's escape hatch

The daemon goes out of its way to keep the socket open after a failed range negotiation, and checks daemon.stop ahead of the mismatch gate, precisely so a mismatched admin client can stop it rather than restarting the daemon and dropping every held lease on the machine. The client closed the connection on any hello failure, so simlock daemon stop — the exact command the error message tells the user to run — could not work.

Not reachable at {3,3} today, but it would ship broken into every 0.3.0 client and bite at the first version bump. Now a PROTOCOL_VERSION_UNSUPPORTED rejection returns a degraded client permitting only stopDaemon(); every other handshake failure, including ADMIN_AUTHENTICATION_FAILED, still closes and serves nothing.

Residual limitation

If a caller supplies a requesterId it genuinely does not own, the FORBIDDEN now surfaces correctly, but a grant that still arrives afterwards is tracked with nothing releasing it. Pre-existing best-effort gap; the owner-aware check should make it unreachable in normal operation.

The degraded client's own principal/role fields are best-effort, since a failed hello never reports the daemon's resolved values — not load-bearing, because stopDaemon() is gated by the daemon's credential check, not by anything the client claims about itself.

…ection open on a protocol mismatch (B3, B4)

B3: requestLease's AbortSignal was a silent no-op for two ADR-0003-mandated
cases: a client with no supplied principal (the daemon never reported its
resolved default back), and the §4 proxy case (one connection, many
requesterIds). Both tripped lease.cancel's requesterId===principal check into
FORBIDDEN, which the abort path's bare catch swallowed and treated as "the
connection died", resolving the caller's await with a real grant it had
explicitly abandoned.

Fixes, in order:
- hello's reply now carries the daemon-resolved principal (helloReplySchema,
  DaemonServer#handleHello's hello region only); the client adopts it instead
  of defaulting to "".
- lease.cancel's authorize hook is gated on the pending request's recorded
  owner (QueueControl.pendingRequestOwner, backed by the wait queue's existing
  per-waiter ownerId) rather than comparing requesterId to the principal
  directly -- this is the "do it properly" route: no core wait-queue schema
  change was needed, only a new read accessor threaded through
  LeaseAcquisitionCoordinator -> LeaseEngine -> the dispatcher's
  AuthorizeContext. This makes the §4 proxy case (principal "host",
  requesterId "agent-7") work as designed.
- lease.request is documented as deliberately authorize-free (ADR §4: any
  agent may request under an arbitrary requesterId; ownerId is never
  client-supplied) so it no longer reads as inconsistent with lease.cancel.
- the abort path's catch around lease.cancel now only swallows a
  transport-kind error (a genuinely dead connection); FORBIDDEN/BAD_REQUEST
  surface to the caller instead of being misread as connection death.

Residual limitation: if a caller passes a requesterId it does not actually
own and the daemon rejects the cancel with FORBIDDEN, that now surfaces
immediately to the caller, but if the original lease.request later still
grants, the grant is still tracked internally as held with nothing to release
it (a pre-existing best-effort gap noted at #releaseAbandonedGrant, not
touched by this fix).

B4: the client closed the socket on any hello failure, including
PROTOCOL_VERSION_UNSUPPORTED -- the one mismatch the daemon deliberately keeps
the connection open for, so an admin client can still send daemon.stop instead
of restarting the daemon and dropping every held lease (ADR §6). Now only that
specific rejection keeps the connection open and returns a degraded client
whose every operation but stopDaemon()/close() rejects with the captured
error; every other handshake failure (including a bad credential) still
closes and serves nothing.
@V3RON
V3RON force-pushed the fix/0003-07-client-abort branch from 5be2fc8 to f302446 Compare September 3, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant