Skip to content

srv1 dashboard tailnet door: socket-bound serve unit, serve route retargeted to it, door entry declaring peer_user (modeled; live retarget not performed) - #13296

Merged
gunbai-bot[bot] merged 10 commits into
mainfrom
alloc/roadmap-tailnet-door
Oct 5, 2026
Merged

gunbai-bot[bot] merged 10 commits into
mainfrom
alloc/roadmap-tailnet-door

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Modeled declarations only. Nothing here was applied to a host, and merging does not apply it — fleet-converge is dispatch-only.

Why

After #13271 the tailnet path needs a kernel-attested proxy peer and a socket-backed route. Every roadmap entry is on a TCP listener, so no deployed instance can admit an allocation write. This is the production cutover that makes one possible.

What changes

  • Door socket and unit. gunbc.roadmap_dashboard_instance dashboard_instance_tailnet_door_socket; gunbc.live_deploy.spec routes production's root mount on 443 to unix:<door socket> and gives a socket-routed deployment one TailnetDoorServeUnit member. The socket is read back out of the route's backend, so unit and mapping cannot name two sockets. gunbc.live_deploy.emit live_deploy_tailnet_door_unit_file is the loopback serve unit on another entry and listener. The loopback unit's bytes are unchanged and it keeps serving the host's own readers.

  • Door entry. gunbc.roadmap_serve roadmap_serve_handle_door declares peer_user and hands the seam IngressAttestedPeer.

  • Door in readiness. gunbc.live_deploy.readiness joins the loopback observation with /healthz over the door socket, held to the same release and surface identity.

  • The re-point is a journaled transaction (reviews 5407953744, 5408258646). gunbc.live_deploy.tailnet_door_cutover, with the journal in one compare-and-set slot of the fabric state store per (host, route) (tailnet_door_cutover_host), binding route, door target, release, capture and phase:

    Step What happens
    journal read first; unreadable, about another route/door, or left in flight by another release ⇒ refuse, no route effect
    permit door readiness mints TailnetDoorReadyForCutover (sole constructor, one mint)
    capture the route as it stands becomes CapturedNoRoute or CapturedRestorableBackend carrying its backend word; unreadable, ambiguous or unrestorable ⇒ refuse before the route is touched
    Prepared the capture is journaled before the re-point; a fenced or refused write ⇒ no re-point
    re-point one LiveDeployServeRoute host effect through the ordinary mutation gate; journal Repointed
    read back, exercise the route must show exactly the door; /healthz at the tailnet name, held to the release identity
    Committed journaled; an unrecorded commit is not reported as one
    on failure journal RollingBack, restore the captured backend, prove by readback, journal RolledBack

    Resuming. A run that reads Prepared, Repointed or RollingBack uses the journal's capture: route still the capture → re-point with the permit, or settle RolledBack without it; route at the door → exercise, then commit or restore; RollingBack → finish the rollback. A Committed record is never rolled back. The gate runs whatever the readiness was, so an interrupted cutover is resumed even when this run's door is not ready; a mutation that did not converge yields no permit.

    The restore is applied only while the route still shows the door, or — when the route cannot be read — only by the run whose RollingBack write lands at the generation it holds. A route showing anything else is left alone. The mapping member emits no apply step for a door-routed deployment; port-routed deployments are unchanged. gunbc.live_deploy.apply runs mutation → readiness poll → cutover gate → readiness gate → digest and identity readbacks.

Evidence

test.claim.live_deploy.tailnet_door_cutover_witness_test runs the transaction itself over supplied effects and reads its step record:

Control Claim
door absent/unanswered ⇒ no re-point an_unanswering_door_performs_no_effect
wrong release ⇒ none a_door_serving_another_release_performs_no_effect
exact readiness ⇒ journaled, exactly one re-point a_ready_door_is_journaled_repointed_once_read_back_exercised_and_committed
unsupported prior target ⇒ re-point 0, restore 0 an_unrestorable_or_unjournaled_capture_causes_zero_route_effects (also unreadable, ambiguous, fenced and refused journal writes)
interrupted after the re-point an_interruption_after_the_repoint_resumes_from_the_journaled_capture (proxied fails → restore durable capture; answers → commit; door dead → restore)
interrupted at Prepared / after failed probe before restore an_interruption_at_prepared_resumes_with_the_same_capture
interrupted after restore before proof an_interruption_during_the_rollback_finishes_the_rollback
committed retry never rolls back a_retry_over_a_committed_cutover_never_rolls_back
foreign journal / foreign plan an_unreadable_or_foreign_journal_refuses_before_any_effect, another_releases_in_flight_cutover_refuses_and_its_settled_one_does_not
stale generation an_unrestorable_or_unjournaled_capture… (fenced Prepared), an_unrecorded_commit_is_not_converged_and_leaves_the_route_working, and on a real store the_cutover_journal_survives_its_writer_and_fences_a_stale_write (wet lane)
unreadable-route restore tied to the journal an_unreadable_route_is_restored_only_by_the_run_holding_the_journal
post-re-point mismatch, proxied failure, newer mapping, unproven restore a_route_not_at_the_door_after_the_repoint_is_not_committed, a_failed_proxied_path_restores_the_captured_backend_and_proves_it, a_failed_cutover_from_no_route_restores_no_route, a_newer_mapping_is_never_overwritten_by_the_restore, an_unproven_restore_stays_in_flight_in_the_journal
the deploy's own gate and codec the_deploy_gate_refuses_an_unreadable_journal_before_any_host_effect, a_failed_mutation_cannot_permit_a_repoint, the_door_route_is_written_by_the_cutover_and_not_by_the_bulk_apply, only_a_committed_cutover_leaves_the_deployment_converged, the_journal_record_round_trips_and_an_unrestorable_capture_does_not_decode

An interruption is modeled as a run that starts from the journal record the interrupted run's last write left, with the route where its last route effect put it. No claim kills a process mid-transaction.

The captured backend in these controls is deliberately not production's port, so a passing restore can only have used the first observation.

Also test.claim.live_deploy.tailnet_door_witness_test (one socket across route, unit and argv; door unit equals loopback unit up to entry and listener; door readiness join) and seam_production_instance_mints_the_tailnet_path_only_through_its_door over the real roster row.

Two existing claims in emit_test changed because the fact they pinned moved: the bulk apply now must not contain production's route write, and the root-mount/no---set-path fact is read from the cutover's script.

Local gunbc run: the cutover, journal (wet), door, seam and emit_test files green, 131 claims. Mutations on this head each turned a claim red: resuming another release's in-flight record; accepting an unrestorable capture; restoring blind without the journal; re-pointing without journaling. Earlier heads: restoring over a newer mapping; dropping the already-routed arm; putting the route write back in the bulk apply; pointing production's route back at the port. Floor result is CI's.

Limits of the transaction, stated

  • An in-flight record is resumed only by the release that wrote it. If release A is interrupted mid-cutover and release B is deployed next, B's cutover refuses and performs no route effect; the route stays wherever A left it until A's plan is re-run. That is the rule as asked for; it can strand a route at the door across a release change, and that is visible only as B's NotConverged reason.
  • The journal lives in the fabric state store, which the deploy job must be able to write. If it cannot, every cutover refuses before touching the route. Whether the live deploy job holds that credential was not checked.
  • tailscale serve has no compare-and-set. The journal fences the record, not the route write itself; a run that loses the fence after its Prepared write can still have issued one re-point. Mutual exclusion of live jobs remains the apply's host lease.
  • A status mismatch after the re-point restores only when the route is still the door or unreadable (and then only holding the journal). A third backend is a newer mapping and is left alone.
  • The proxied probe is a single bounded attempt.
  • A door that dies after a deploy is restarted by systemd on failure and not otherwise watched.

Live steps — declared, not performed, operator go-ahead needed

Applying on srv1 installs and starts gunbc-roadmap-tailnet-door.service, then runs the transaction above against the real route. Still to be established on the host: that the deploy job can write the cutover journal slot in the fabric state store; the service principal's unelevated tailscale serve status --json read; that the deploy principal can traverse the instance root to the socket; the exact Proxy text tailscaled reports for a unix target; one real proxied request; two serving processes within the existing slice's memory.

🤖 Generated with Claude Code

gunbc-ci-auto-heal and others added 6 commits October 4, 2026 12:43
…; mount POST /allocations and /allocations/{id}/release

roadmap_serve_request_security hard-coded ServeStatusUnreadable / no route, so
AccessPathTailnetProxy was unconstructible and the allocation writes stayed
unmounted. The seam now consumes a serve-route readback:

- extdeps.tailscale.serve tailscale_serve_status_route_proxy_targets projects
  the status document at a route key (listener x mount), not a mount across
  every listener.
- gunbc.auth.tailnet_serve_readback: the timed `tailscale serve status --json`
  read and the standing fold. No declared endpoint, no reading, a stale or
  undated reading, an unreadable document, or another backend at the declared
  route all yield no route.
- gunbc.roadmap_serve: the declared route is the desired-deployment roster's
  endpoint for the instance; the reading is taken only for a selected route
  whose write law requires the tailnet path.
- gunbc.workspace_allocation_routes mounts create and release under
  tailnet_path_required: true, with expiry, image digest and public key
  reference from server policy.

Witnesses: floor claims over the seam's access path (missing route, mismatched
route, header without reading, malformed identity, stale reading, CSRF,
browser-supplied terms, mounted routes, positive control) and one wet claim in
which the admitted create and release land in a file store.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ble with cause

A tailnet header no reading vouched for is now AccessPathTailnetUnobserved
carrying a TailnetRouteGap, one arm per cause: no declared route, reading not
taken (not demanded, clock unreadable, not spawned, command failed with its
exit status and text), stale, unreadable document, route not proxied, route
proxying elsewhere. A serving principal that cannot read the serve config is
therefore told apart from a route that is not installed.

The allocation write routes answer a signed-in request on that path with 503
{"availability": "unavailable", "reason", "cause"}, derived from the request's
own access path. An unauthenticated request is answered 401 and is not told the
host's standing; no header and a direct bind stay 403 from the write law.

request_access_path_of keeps its signature and delegates to
request_access_path_of_standing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dcard arm over the closed handler type

The floor refused roadmap_serve_selection_requires_tailnet_path as an
unrostered non-fold residue site. The predicate now compares the selected
handler to the two allocation write handlers, so there is no wildcard arm and
no residue row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e, not a status and a header

Review 5406776483 (P1): serve status is a node-level configuration fact, so
status + loopback bind + any non-empty Tailscale-User-Login header minted
AccessPathTailnetProxy for a direct local request with a forged header -- the
backend is a TCP listener any co-located process can dial.

- RequestIngress (IngressUnattested | IngressAttestedPeer) carries what the
  serving host attests about the connection. request_access_path_of_standing
  judges it first: only a connection whose kernel-attested peer is the
  tailscale proxy (extdeps.tailscale.serve tailscale_serve_unix_proxy_peer,
  the fact the fabric door already relied on) can mint the tailnet path.
- tailnet_route_standing refuses a declared route whose backend is a TCP port,
  whatever the reading says.
- Every roadmap entry is launched on a TCP listener and hands the seam
  IngressUnattested, so no deployed instance mints the tailnet path and the
  allocation writes answer 503 ingress-unattested there.
- New causes: ingress-unattested, ingress-not-the-proxy,
  route-backend-not-a-socket.

Controls: same fresh status and forged header on an unattested listener and
from an ordinary local account mint no tailnet path, while the proxy's
connection does; the production instance as declared mints none.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e route retargeted to it, door entry declaring peer_user

After the request-security seam requires a kernel-attested proxy peer, no
deployed instance can mint the tailnet path: every roadmap entry is on a TCP
listener. This models the cutover for production (modeled only; nothing is
applied to a host):

- gunbc.roadmap_dashboard_instance dashboard_instance_tailnet_door_socket: the
  socket in the instance root.
- gunbc.live_deploy.spec: srv1_live_deployment_names routes the root mount to
  unix:<door socket>; a deployment whose route targets a socket owns one
  TailnetDoorServeUnit member, ordered before its serve mapping. The socket is
  read back out of the route's backend, so unit and mapping cannot disagree.
- gunbc.live_deploy.emit: the door unit is the loopback serve unit on another
  entry and listener (live_deploy_serve_unit_file_over); apply and teardown
  arms. The loopback unit and its bytes are unchanged and keep serving the
  host's own readers.
- gunbc.roadmap_serve roadmap_serve_handle_door: the entry the unit launches,
  handing the seam IngressAttestedPeer { peer_user }.

Witnesses: one socket across route, unit and mapping; door unit equals the
loopback unit up to entry and listener; door precedes the mapping and the
port-routed twin has none; the door entry answers; and the seam over the real
roster row mints the tailnet path only through the door.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eld to the release identity

A deployment whose serve route targets a unix socket has two serving processes
and every tailnet request goes to the door, which deploy readiness did not
probe. live_deploy_service_ready_once_for_deploy now joins the loopback
observation with the same /healthz read over the door socket
(extdeps.http.client GetWithinUnixSocket / http_client_get_unix_socket_argv),
held to the same release and surface identity. A door that is unanswered or
serving another release is ServiceTailnetDoorPending naming the socket, which
the poll bound turns into the deploy's typed refusal. A door observation never
upgrades a loopback one, and a deployment with no door is its loopback
observation unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Base automatically changed from session/wise-wolf-139 to main October 4, 2026 18:55
# Conflicts:
#	dag/extdeps/tailscale/serve.dag
#	dag/gunbc/roadmap/roadmap_serve.dag
#	dag/test/claim/roadmap/workspace_allocation_http_witness_test.dag
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review October 4, 2026 18:58
…ain merge

The merge of main reported a third conflicted file that I did not see and
staged with its markers, so the corpus did not parse. Both sides were this
branch's own text; the content is unchanged from before the merge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HOLD at exact head 7f3248271ac346cf18f59f2c80920c25dd92318f.

The socket/peer authority itself is sound: production derives one door socket into the unit and Serve mapping, TCP requests remain unattested, the door entry consumes kernel-derived peer_user, and readiness joins the loopback and door health documents to the same release identity. Exact-head required CI is green.

P1: the apply transaction redirects the live root route before it proves the door ready, and a failed readiness gate has no compensation. TailnetDoorServeUnit runs systemctl restart on a Type=simple service, then the next deployment member immediately executes tailscale serve --bg --https=443 unix:<socket>. Only after the entire mutation returns does live_deploy_apply_readiness_gate probe the loopback listener and door. A successful systemctl restart proves only that ExecStart was accepted, not that the socket exists, is traversable, or serves the candidate release.

The existing transaction explicitly justifies route-before-readiness only because the old operation reasserted the same endpoint-to-8080 mapping; this PR changes that premise into a real backend redirection. Failure sequence:

  1. door unit start returns successfully but the process exits, cannot bind/traverse the socket, or serves a stale/wrong release;
  2. apply re-points all browser traffic from 127.0.0.1:8080 to the socket;
  3. the later door readiness probe refuses;
  4. the deployment returns NotConverged, but the route remains pointed at the failed door. Retract turns the route off; it does not restore the old port mapping.

So the source can report a failed deploy while leaving the production dashboard unavailable. The roster-order witness proves only restart precedes serve; it does not discriminate readiness-before-cutover.

Required repair: make door readiness a cutover permit, not a post-mutation diagnosis. Install/start the door, obtain a bounded direct-socket /healthz readback for the exact release/surface, then allow the mapping re-point. After re-point, read back the exact Serve target and exercise the proxied path; if that second phase fails, restore the captured prior port mapping (or otherwise retain it until the new route is committed). The old backend must be carried as rollback authority rather than reconstructed.

Add effect-level controls showing: a missing/wrong-release door emits no re-point; exact door readiness permits the re-point; a post-repoint status/proxy failure restores the old target; and retry is idempotent. The existing live checks (service-principal status read, socket traversal, actual proxied request, concurrent two-process memory within the existing slice) remain necessary after the source repair.

No host mutation was performed by this review.

…e-point is read back and exercised, a failure restores the captured backend

Review 5407953744 (P1): the door unit's restart was immediately followed by the
serve root route re-point inside the bulk apply, with readiness only afterwards
and no compensation, so a dead or stale door left production pointed at it.

- gunbc.live_deploy.tailnet_door_cutover: the transaction. Door readiness mints
  TailnetDoorReadyForCutover (sole constructor, one mint); with it the run
  observes the route and captures its backend, re-points, reads the route
  back, exercises the proxied path and only then commits. After a failure it
  restores the CAPTURED backend and proves it by readback; it restores only
  while the route is still the door it wrote, never over a newer mapping. A
  route already at the door is a retry: nothing is re-pointed.
- The mapping member emits no apply step for a door-routed deployment
  (emit tailscale_serve_mapping_apply_steps); port-routed deployments are
  unchanged.
- HostEffect LiveDeployServeRoute: one serve route written or turned off
  through the ordinary mutation gate, used for the re-point and the restore.
- gunbc.live_deploy.apply: mutation -> readiness poll (loopback and door) ->
  cutover gate -> digest and identity readbacks. Anything but a committed
  cutover is NotConverged carrying what became of the route.
- extdeps.tailscale.serve tailscale_serve_backend_of_proxy_target restates a
  captured status target as a serve backend, or nothing.

Controls run the transaction over supplied effects and read its step record:
unready or wrong-release door performs no effect; exact readiness re-points
exactly once; proxied failure and unreadable post-status restore the captured
backend; a newer mapping is never overwritten; retry is idempotent; the
deploy's own gate refuses an unready door before any host effect.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

Re review 5407953744 (P1, route re-pointed before the door is proven, no compensation): repaired at 096acdf.

The mapping member no longer writes the route in the bulk apply for a door-routed deployment. gunbc.live_deploy.tailnet_door_cutover owns it as a transaction: door readiness mints the permit, the route is observed and its backend captured, re-pointed through one gated LiveDeployServeRoute effect, read back, the proxied path exercised, and only then committed; a failure restores the captured backend and proves it by readback. The old backend is carried from the pre-mutation observation and restated by tailscale_serve_backend_of_proxy_target, never reconstructed — the controls capture a port that is not production's.

Effect-level controls are in test.claim.live_deploy.tailnet_door_cutover_witness_test; the PR body maps each requested control to its claim.

Three places where I did less than a literal reading, each stated in the body: a post-re-point mismatch restores only when the route is unreadable or still the door (a third backend is treated as a newer mapping and left alone); two concurrent cutovers writing the same door target are not told apart by this code and rely on the apply lease; and tailscale serve offers no compare-and-set, so decisions are read-then-act.

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HOLD at exact head 096acdf69527dea76336adc4e4e5870158eb8710.

The prior ordering finding is substantially repaired: a socket-routed mapping emits no bulk-apply route write; direct socket readiness precedes the cutover; the route is observed, re-pointed once through LiveDeployServeRoute, read back, exercised through the tailnet name, and compensated from the captured backend on an in-process failure. Leaving an explicitly observed third backend alone is the right rule.

Two P1s remain:

  1. The captured backend is process-local, so interruption after re-point loses the rollback authority. CapturedServeBackend is only a local value in tailnet_door_cutover_run; there is no durable Prepared/Repointed/Committed journal. If the workflow/process is cancelled, times out, or crashes after the route write (or after a failed proxied probe but before restore), the GitHub host-mutation concurrency group releases. A replacement run observes the route already at the door and enters tailnet_door_cutover_already_routed. The committed control explicitly establishes that when the proxied path then fails, it refuses and performs no restore, because the prior backend is gone. Thus the former failure mode remains reachable across an interruption: failed deployment, production route still pointed at the failed door.

The host lease serializes live jobs; it does not survive their termination or preserve prestate. Add a generation-fenced durable cutover journal binding route key, reviewed plan/release, door target, captured prior backend, and phase. Persist Prepared before the route effect, record Repointed, then Committed or RolledBack. Recovery from Prepared/Repointed must use the durable capture; a committed retry must not roll back. Add controls for interruption after re-point, after failed proxy observation before restore, and after restore before proof/publication, plus stale/foreign journal refusal.

  1. A single prior target is overwritten before the code proves it can restore it. The first observation admits any one String as CapturedBackend. tailscale_serve_backend_of_proxy_target is consulted only later inside live_deploy_serve_route_restore, after the route has already been changed. A valid or drifted Serve target outside the inverse's unix/loopback-port subset can therefore be displaced, followed by a rollback that can only report that the captured backend cannot be restated while leaving the door in place. Make capture produce a typed restorable backend before mutation (or refuse the cutover before re-point when the observed target is not exactly representable). Add a RED for one unrepresentable prior target causing zero route effects.

The disclosed no-CAS limitation is acceptable only inside that durable, lease-bound recovery model. The current in-memory transaction is not crash-recoverable, so this head should not land or be used for the live retarget yet.

…able capture, resume from the journaled capture

Review 5408258646, two P1s.

(1) The captured prior backend was a local value, so a run interrupted after
the re-point lost the rollback authority and its replacement, finding the route
at the door, could only refuse. The transaction is now journaled in one
compare-and-set slot of the fabric state store per (host, route), binding
route, door target, release, the capture and the phase:

  Absent -> Prepared -> Repointed -> Committed | RollingBack -> RolledBack

Prepared is written BEFORE the route effect. A run that reads an in-flight
record resumes it with the journal's capture: route still the capture ->
re-point (with the permit) or settle RolledBack (without); route at the door
-> exercise, then commit or restore the durable capture; RollingBack -> finish
the rollback. A Committed record is never rolled back. An unreadable journal,
a record about another route or door, an in-flight record left by another
release, and a write fenced by a newer generation each refuse with no route
effect. A blind restore over an unreadable route is made only by the run whose
RollingBack write lands. The gate now runs whatever the readiness was, so an
interrupted cutover is resumed even when this run's door is not ready; a
mutation that did not converge yields no permit.

(2) Capture accepted any single target and checked restorability only inside
the restore. serve_route_capture now yields CapturedNoRoute or
CapturedRestorableBackend carrying its backend word, or refuses before the
route is touched; an unrestorable prior target causes zero route effects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

Re review 5408258646: both P1s repaired at ce6cf2c.

(1) Durable journal. One generation-fenced compare-and-set slot in the fabric state store per (host, route) binds route, door target, release, capture and phase. Prepared is written before the route effect; Repointed, Committed, RollingBack and RolledBack follow. A run that finds an in-flight record resumes with the journal's capture, so an interruption after the re-point, after a failed probe, or after the restore but before its proof each ends in a commit or a proven restoration of the durable capture. A Committed record is never rolled back. Unreadable journal, another route or door, an in-flight record from another release, and a write fenced by a newer generation refuse with no route effect. The cutover gate now runs whatever the readiness was, since the case that most needs a resume is the one where the door is dead.

(2) Typed capture. serve_route_capture yields CapturedNoRoute or CapturedRestorableBackend carrying its backend word, or refuses before the route is touched. an_unrestorable_or_unjournaled_capture_causes_zero_route_effects is the requested RED.

Two things to weigh, both in the body under Limits: an in-flight record is resumable only by the release that wrote it, so a release change after an interruption strands the route until the original plan is re-run; and the interruption controls start a run from the journal an interrupted run would have left — no claim kills a process mid-transaction.

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE at exact head ce6cf2c0c302a1c81018432dc366b824d7f817e4.

The two blockers from review 5408258646 are addressed.

  1. Cutover state is now durably journaled in one generation-fenced fabric-state CAS slot per host/route. The captured prior backend is persisted in Prepared before any route effect; Repointed/RollingBack/Committed/RolledBack allow an interrupted run to resume from durable state rather than guessing. Foreign in-flight releases, stale generations, unreadable journal state, and newer mappings fail closed. The real-store wet control establishes survival across handles and stale-write fencing.
  2. Capture is typed as CapturedNoRoute or CapturedRestorableBackend. An unsupported singleton target refuses before journaling/repoint, so rollback feasibility is established before mutation.

The former ordering P1 remains closed: door readiness precedes cutover, the door-routed mapping emits no bulk-apply route write, cutover reads back the exact door, exercises the proxied path, and commits only after its journal write. Failed probes enter journaled rollback and restore the durable capture; restoration is proven by readback. Committed state is never rolled back.

I accept the stated concurrency limitation: Tailscale exposes no route CAS, so live cutovers rely on the existing per-host mutation lease; the transaction still refuses to overwrite an observed third/newer mapping. This should remain explicit in live acceptance.

Live acceptance still must establish the deploy job's fabric-store write authority, socket traversal, unelevated Serve-status read, exact Unix-target status spelling, proxied ingress, concurrent memory fit, and interruption recovery against the real host.

LAND, conditional on exact-head/merge-group CI completing green.

@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit ca6b6fd Oct 5, 2026
4 checks passed
@gunbai-bot
gunbai-bot Bot deleted the alloc/roadmap-tailnet-door branch October 5, 2026 00:39
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