Expand description
Cloud’s floating/reserved-IP surface: the vendor adapters, the credentialed constructor, and a re-export of the shared seam.
The seam itself moved. FloatingIpProvider, reconcile_assignment,
plan_ingress_owner_effect and everything around them now live in the
yah-floating-ip crate (oss/yubaba/crates/floating-ip), which both this
crate and yubaba depend on. R859-F2 landed the planner here with no
production caller because its caller belongs in yubaba’s scheduler tick and
a runtime yubaba -> cloud edge would have pulled velveteen,
velveteen-exec, yah-hetzner, yah-mesofact-bundle and yah-almanac into the
release daemon on every fleet node — reversing the carve-out R374-F3 made
for local-driver (see this crate’s yah-local-driver dep comment).
Operator decision, 2026-09-08: repeat R374-F3’s move instead. Hence the
extraction, and hence this module being a thin lid on it.
Everything the old module exported is still reachable at the same path —
cloud::provider::floating_ip::* — via the pub use below, so no call site
moved. What is defined here is only what genuinely cannot leave:
floating_ip_provider_for, because it resolves vault slots throughfoband constructs the three vendor adapters. Credentials are the consumer’s concern, not the seam’s.machine_facts, the boundary conversion from this crate’sMachineConfigto the shared crate’sFloatingIpMachine. Five fields are the whole contract; keeping it that narrow is what letsyubaba, which has no.yah/infra/machines/tree at all, use the same planner.
The vendor adapters left too, on 2026-09-08 (R859-F3). This module’s
previous revision said they could not, because each was two things at once —
a FloatingIpProvider and an EnvoyAdapter — and their typed envoy
handlers were inherent impls, which Rust forbids on a foreign type. Only the
second half of that was true: impl EnvoyAdapter for HetznerFloatingIp is a
local trait on a foreign type, which the orphan rule permits, and the
inherent handlers were byte-identical across all three, so one blanket
FloatingIpEnvoy impl replaced them. The transports are now
yah-floating-ip-adapters and the envoy half is super::floating_ip_envoy
— which is what makes yubaba’s Reassign arm reachable at all.
This mirrors, at the sovereign-ingress tier, the “external identity follows placement” property R591 names for Headscale via a Cloudflare Tunnel.
@yah:ticket(R859-F2, “Wire floating-ip.* provider adapters to ingress_owner transitions + health-checked DNS withdrawal for dead origins”)
@yah:status(review)
@yah:phase(P2)
@yah:at(2026-09-08T21:24:15Z)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:parent(R859)
@yah:handoff(“LANDED, uncommitted. Six pieces. (1) THE BLOCKER, fixed as decided: MemberInfo and YubabaRequest::SetMember each gained machine: Optioncloudflared) + cloud::validate::check_ingress_floating_ip, wired into yah cloud validate (error) and the apply preflight (warning), same split as R605-F12. (6) DNS withdrawal through F1’s EXISTING seam: public_origins gained a health_excluded arg and returns ResolvedOrigins{origins, health_withdrawn}; DomainPasswayPlan gained health_withdrawn; diff_apex_records prunes a health-withdrawn address regardless of origins_complete.”)
@yah:handoff(“THE DEPENDENCY FORK, resolved with evidence, and the answer is NOT the one the brief’s criterion predicts. Read both manifests: oss/yubaba/crates/cloud/Cargo.toml has NO yubaba dep, and oss/yubaba/crates/yubaba/Cargo.toml already carries cloud = { package = \"yah-cloud\", path = \"../cloud\" } — but under [dev-dependencies]. So a runtime yubaba -> cloud edge would create NO cargo cycle. I did not take it anyway, and the reason is a documented architectural rule the brief’s cycle-check could not see: cloud/Cargo.toml’s local-driver dep comment records that local-driver was carved out of cloud in R374-F3 SPECIFICALLY "so yubaba could own MinIO lifecycle without a reverse yubaba->cloud dep". Adding that edge would put velveteen, velveteen-exec, yah-hetzner, yah-mesofact-bundle and yah-almanac into the release daemon shipped to every fleet node — an architecture call outside a courier’s blast radius. So I took the SECOND branch: plan_ingress_owner_effect() is pure, fully tested, and UNWIRED. Everything else in the ticket ships.”)
@yah:handoff(“WHERE THE PLANNER LIVES, and why there. plan_ingress_owner_effect is in cloud (provider/floating_ip.rs), not yubaba, because its inputs include MachineConfig and its outputs command cloud adapters. The two yubaba-side facts cross the boundary AS PLAIN DATA, never as types: OwnerLiveness{ConfirmedUp,ConfirmedDown,Unconfirmed} re-spells TransitionTracker::committed, and QuorumHealth{Healthy,Degraded{reason}} re-spells QuorumVerdict (its Unknown collapses into Degraded — both refuse, and the distinction survives in the reason string). That honours decision 7: raft stays read-only from the cloud side, and no dep edge is created in either direction. Signature: plan_ingress_owner_effect(previous_owner, current_owner, current_owner_liveness, &quorum, machines) -> IngressOwnerEffect{Reassign{machine,ip_id} | Withdraw{machine,reason} | Refuse{reason} | NoOp{reason}}. NoOp carries a reason (the brief wrote it bare) because a log line saying which of the five no-op paths was taken is worth six characters. The two action variants ARE W267’s two failover speeds: Reassign is intra-provider, Withdraw feeds public_origins’ health_excluded set for the cross-provider path — which is what gives the enum’s fourth variant real work rather than a placeholder.”)
@yah:gotcha(“READ THIS BEFORE ATTACHING THE EFFECTOR — the identity bridge is narrower than its name. ingress_owner and the new MemberInfo.machine both carry /etc/hostname (leader::derive_machine_name), which is NOT reliably the .yah/infra/machines/vps-4c1efa56 for the box declared us-west-001. The brief’s decided fix assumed these were machine names; they are not, and I corrected the field’s doc comment rather than shipping a plausible-looking lie. What the bridge DOES guarantee is exact: node_id <-> ingress_owner, because both strings come from one derivation. Resolving that string to a MachineConfig is a SEPARATE, fail-loud step — cloud::provider::floating_ip::resolve_ingress_owner matches declared names EXACTLY (no prefix match, no fuzzy fallback, no "it is probably the only public-ip box") and refuses by name, listing every declared machine and explaining the hostname mismatch. Pinned by an_ingress_owner_that_names_no_declared_machine_refuses_loudly. So on today’s fleet a us-west-001 flip would REFUSE rather than misfire. Closing it properly means renaming hostnames to match machine names, or adding a declared hostname alias to MachineConfig — separable work, not R859-F2’s.”)
@yah:handoff(“TWO DESIGN CALLS I MADE THAT ARE NOT IN THE BRIEF, both forced by a test that failed. (a) Degraded means A VOTER IS DOWN, not "this topology has no redundancy". My first judge_quorum keyed purely on margin and my own 1-voter test failed it: a rig has zero margin at its healthiest, so a margin-only rule calls its best possible state degraded and refuses every withdrawal forever — an inert feature wearing the costume of a safety check. Rule is now Degraded iff available < majority, OR available == majority AND available < voters. A fully-available cluster is Healthy at any size with margin stating the slack honestly. Pinned by an_intact_two_voter_cluster_is_healthy_but_a_three_voter_one_reduced_to_two_is_not — identical available, opposite verdicts, because one lost a voter and the other did not. (b) The empty-apex guard in plan_domain_passway is checked against the origins that SURVIVE health exclusion, which makes it the health failover’s backstop for free: if every declared front door is confirmed down, plan_domain_passway refuses. "All our front doors are down" must never render as "withdraw every A record" — a dead origin still in DNS is a partial outage, an empty apex is a total one. The health withdrawal is therefore capped at all-but-the-last origin by construction, with no second rule to keep in sync. Same reasoning one level finer: an address a SURVIVING origin still answers on is dropped from health_withdrawn (two machines can share a floating IP).”)
@yah:handoff(“THE origins_complete x health CROSS-PRODUCT, decided and tested as four cells (health_withdrawal_and_declaration_completeness_are_independent). complete+healthy -> prune. complete+down -> prune. incomplete+healthy -> withheld_prune. incomplete+down -> PRUNE ANYWAY. The bottom-right cell is the whole point and it is a judgement, so here is the reasoning: origins_complete=false protects against mistaking an ABSENCE for a withdrawal, and a health withdrawal is not an absence — it is a positive observation about a machine the collation resolved, taint-checked and address-checked on the way into health_withdrawn. An unrelated service’s broken TOML is not evidence about a box we watched go down; letting it veto the prune would leave a dead origin taking its share of the round-robin for as long as that typo lives. Fail-closed-on-withdrawal is NOT weakened: the gate on a health withdrawal is the QUORUM verdict, applied one layer up in plan_ingress_owner_effect, which refuses to emit the exclusion at all out of a degraded quorum. Two withdrawal paths, each fail-closed on the evidence actually relevant to it. Also tested: an_incomplete_collation_prunes_only_the_health_withdrawn_surplus (both reasons coexist in one diff — health-excluded pruned, merely-absent still withheld). Decisions 1, 8 and 9 held as written: TransitionTracker/HysteresisPolicy reused with no new debounce type; no TTL parameter anywhere and a doc comment saying why so the next reader does not re-open it; cloud.mesh_failover untouched and the planner cannot transfer leadership.”)
@yah:verify(“Baselines measured BEFORE any edit, on this tree. cloud (cargo test --manifest-path oss/yubaba/crates/cloud/Cargo.toml --features json-schema): 1124 passed / 0 failed / 4 ignored (lib) + 3/0/1 + 2/0/0 + 0/0/1. yubaba (--manifest-path oss/yubaba/crates/yubaba/Cargo.toml --lib): 708 passed / 0 failed. cargo build --workspace: GREEN before I started — it was not already red, so nothing here is inherited. AFTER: cloud 1152 / 0 / 4 (+28, same other three targets); yubaba lib 724 / 0 / 0 (+16); cargo build --workspace green; cargo check --manifest-path oss/yubaba/Cargo.toml --all-targets clean (covers the two integration-test files I touched). Epoch gate: RUSTC_WRAPPER='' cargo run -p xtask -- cluster-epochs GREEN — both axes were red from my raft/mod.rs + raft/store.rs edits, verdict NOT BREAKING on both, hashes re-recorded, cluster_protocol stays 5 and state_epoch stays 4, with a full why_not_a_bump entry in cluster-epochs.json surface_rerecords[2026-09-05]. Both drifted surfaces were verified to contain ONLY my hunks (git diff -U0: store.rs is one 47-line insertion) before writing, so no peer’s unanalysed change was swept into a verdict. scripts/check-workload-spec-ts.sh: ok.”)
@yah:gotcha(“scripts/check-schema-drift.sh is RED, and it is NOT this ticket’s drift. The gate regenerates and then git diff --quiet -- .yah/schema, so it fails for ANY uncommitted regeneration, in sync or not — exactly the condition R860-T1 already recorded ("both gates go red for that reason; a pathspec commit of the generated paths was attempted and DENIED by the approval gate"). It was red before I started (.yah/schema/{machine,workload}.toml.schema.json were both already dirty in the tree at session start). I ran cargo run -p xtask -- emit-schemas as required — MachineConfig gained a field — and machine.toml.schema.json:77 now carries ingress_floating_ip. Note the regen ALSO shrank workload.toml.schema.json’s WorkloadSpec description, because R860-T1’s @yah: annotations have since left workload-spec/src/lib.rs; that is a correct regeneration of a generated artifact, not damage, and any peer running the same command gets the same output. The gate goes green when those two paths are committed. I did not commit (instructed not to).”)
@yah:gotcha(“OVH’s floating-IP adapter is now REGISTERED but is NOT live-ready, and registering it was still right. ovh_floating_ip.rs’s own module doc records that its auth is a placeholder — OVH signs with an application key + secret + consumer key + timestamped HMAC, not the bare X-Ovh-Consumer header the adapter sends. Registering it in default_adapters() makes the verb dispatchable (the latent bug decision 4 names); it does not make it correct against api.ovh.com. All three registrations are gated on their credential being present via fob::get_or_env, so the adapter is absent from every camp that has not deliberately set ovh-consumer-key/$OVH_CONSUMER_KEY. Swap in real OVH request signing before pointing it at anything live. Checked rather than assumed: HetznerEnvoy (cloud.vps.) and HetznerFloatingIp (floating_ip.) share the adapter id "hetzner" but claim DISJOINT verb sets, and agent-tools/src/envoy_tools.rs:214 groups by VERB id rather than adapter id — so neither shadows the other, and the per-verb provider enum still gets three distinct choices.”)
@yah:handoff(“FILES (all uncommitted; tree anchor f086233d, the commit this session started from — quote that SHA, not HEAD, in any restore instruction). yubaba: raft/mod.rs (field on MemberInfo + SetMember, apply arm, 3 new tests), raft/store.rs:769 (machine_for_node/node_for_machine), quorum_health.rs (NEW, 12 tests), lib.rs (module decl), scheduler.rs (judge_quorum caller + debug! import), member_registration.rs (machine param through spawn/run/plan_registration/write_row, 3 new tests, 12 existing call sites updated), leader.rs (derive_machine_name now pub, doc), main.rs (passes it), leader_pin.rs + headroom.rs (MemberInfo literals), tests/raft_tenant_placement.rs, yubaba-test-harness/src/solo_node.rs (per-node stand-in name — /etc/hostname would make every in-process node identical and node_for_machine ambiguous), cluster-epochs.json. cloud: provider/floating_ip.rs (registry + planner + resolver + 14 tests), provider/mod.rs (re-exports), envoy.rs (default_adapters), config.rs (ingress_floating_ip + 4 test literals), validate.rs (check_ingress_floating_ip + 8 tests), reconciler/domain.rs (ResolvedOrigins, health_withdrawn, diff rule, 5 new tests), plus mechanical ingress_floating_ip: None, in 10 more files’ MachineConfig literals. app/yah/cli/src/cloud.rs: lint wired at both sites + tally. .yah/schema/{machine,workload}.toml.schema.json regenerated. COLLISION CHECK: envoy.rs has 5 hunks and only ONE is mine (default_adapters); the other four are R859-F1’s known_verb_descriptors work — expected, not a collision. I did not touch the foreign hunks the brief named (proc_control.rs, topology.rs, cloud.rs’s header) and no unexpected diffs appeared in any file I own.”)
@yah:handoff(“LEADER SIGN-OFF, independently verified by a separate session that re-ran every gate and traced each claim to file:line — not taken on the implementer’s word. Commands: cloud tests EXIT=0 at 1152 passed / 0 failed / 4 ignored (baseline 1124/0/4 after R859-F1, +28); yubaba –lib EXIT=0 at 724/0 (baseline 708/0, +16); cargo build –workspace EXIT=0; cargo check oss/yubaba –all-targets EXIT=0. Confirmed in code: MemberInfo.machine and SetMember.machine are Optiongit diff f086233d6b092de2f32cafad5e0010494078269c..HEAD) to see what landed under you, and quote this SHA rather than ‘HEAD’ in any revert/restore instruction.”)
@yah:gotcha(“check-schema-drift.sh exits 1, and it is NOT this relay’s drift. Verified by hashing .yah/schema/.json before and after: the script’s own regeneration produces byte-identical files, so the gate is red purely on its git diff --quiet uncommitted-artifact condition — R860-T1’s recorded state, independently corroborated by an unrelated session’s gotcha at topology.rs:67. Nothing was changed. Note the uncommitted schema diff is MIXED: machine.toml.schema.json:77 carries this ticket’s own regenerated ingress_floating_ip entry alongside R860’s description churn, so whoever commits must not assume the whole diff is theirs.”)
@yah:handoff(“DELIVERED BUT UNWIRED, and this is the relay’s one genuine operator call. plan_ingress_owner_effect() landed pure, fully tested, and with no production caller: inputs are (previous ingress_owner, current ingress_owner, quorum verdict, hysteresis verdict, machine configs), output is an action enum. scheduler.rs’s call site is prepared and already computes the quorum verdict the effector would need, so attaching it is a small change — but it is not a courier’s call to make. There is NO cargo cycle today (cloud has no yubaba dep; yubaba depends on cloud only under [dev-dependencies], Cargo.toml:152). What blocks it is a deliberate architectural decision, not a technical impossibility: cloud/Cargo.toml:75-78’s local-driver comment records R374-F3 carving that crate out SPECIFICALLY to avoid a reverse yubaba->cloud dependency, and taking that edge would pull velveteen/hetzner/mesofact/almanac into the fleet daemon. Reversing a documented carve-out is an operator decision, so everything else in the ticket shipped and this one seam waits on an answer.”)
@yah:gotcha(“PREMISE CORRECTION, found by the implementer against a claim the Leader’s dispatch had asserted — the dispatch said to populate the new machine tag from derive_machine_name(), assuming it yields the .yah/infra/machines/vps-4c1efa56 for the box declared us-west-001. The bridge is still exact where it matters, because node_id and ingress_owner come from ONE derivation — but turning that string into a MachineConfig is now a separate fail-loud step, resolve_ingress_owner (floating_ip.rs:383), which refuses by exact name rather than guessing (test at :887). On today’s fleet an ingress_owner flip would therefore REFUSE rather than misfire — correct, but it means the floating-IP path is inert until hostnames and machine-TOML names are reconciled. That reconciliation is not in this relay.”)
@yah:handoff(“OPERATOR DECISION 1 (2026-09-08, ask_user F4086), answering the ticket’s one genuine architecture call: EXTRACT A SHARED CRATE — do not take the runtime yubaba -> cloud edge, and do not leave the effector operator-driven. LANDED: new oss/yubaba/crates/floating-ip (package yah-floating-ip, extern name floating_ip), registered in oss/yubaba/Cargo.toml’s members. It holds the seam and the decision layer — FloatingIpProvider, FloatingIpTarget/State/AssignOutcome, reconcile_assignment, on_ingress_owner_changed, FLOATING_IP_PROVIDERS + provider_has_floating_ip_adapter + new supported_floating_ip_providers(), OwnerLiveness, QuorumHealth, IngressOwnerEffect, resolve_ingress_owner, plan_ingress_owner_effect — on exactly TWO dependencies, anyhow and async-trait. That budget is load-bearing, not tidiness: this crate links into the fleet daemon, which is the whole reason it exists, so a reqwest or a serde in its manifest ships to every node. cloud::provider::floating_ip is now a thin lid that pub uses all of it at the historical path, so NO call site moved (validate.rs, app/yah/cli/src/cloud.rs, reconciler/domain.rs all untouched).”)
@yah:handoff(“WHAT MADE THE EXTRACTION POSSIBLE, and it is the load-bearing design choice: FloatingIpMachine, a five-field value (name, provider, location, region, ingress_floating_ip) replacing cloud::config::MachineConfig throughout the moved code. Checked rather than assumed — the three vendor adapters read ONLY those five fields off a machine (rg over hetzner/ovh/vultr_floating_ip.rs), so five is the entire contract. cloud converts at the boundary via machine_facts(&MachineConfig) -> FloatingIpMachine (a function, not a From impl, because the source is a borrow and the target is five clones), pinned by machine_facts_carries_every_field_the_floating_ip_layer_reads — a dropped field there would silently disable a failover path rather than fail to compile, since a missing ingress_floating_ip reads exactly like the supported "this machine has no floating IP". And it is what lets yubaba use the planner at all: a fleet node cannot construct a MachineConfig, but it can name a machine and say which provider hosts it.”)
@yah:handoff(“WHAT DELIBERATELY DID NOT MOVE, with the reason, because the decision text said "trait + adapters" and only the trait went. The three vendor adapters stay in cloud because each is TWO things at once — a FloatingIpProvider and an EnvoyAdapter — and splitting them hits the orphan rule: their typed envoy handlers (floating_ip_assign/floating_ip_status) are an INHERENT impl on the struct, and Rust forbids an inherent impl on a foreign type, so moving the struct would force those handlers into free functions and reshape cloud’s envoy dispatch. That refactor buys nothing today, because yubaba cannot use a floating-IP adapter until it can resolve a machine to a provider and an IP id — see the next entry. The adapters were retargeted onto &FloatingIpMachine (their resolve_target signature, ovh_zone_for, and the three test fixtures, each of which shrank from a 22-field MachineConfig literal to a 5-field one). floating_ip_provider_for also stays in cloud: it reads FLOATING_IP_PROVIDERS’ slot/env columns through fob and constructs the adapters, and neither fob nor a vendor HTTP client belongs in a crate that links into the fleet daemon.”)
@yah:gotcha(“THE EXTRACTION REMOVES THE DEPENDENCY OBJECTION BUT DOES NOT BY ITSELF ATTACH THE EFFECTOR — there is a SECOND, independent blocker the previous handoff did not name, and its next-step (a) asserted the opposite ("the three facts it needs are all in scope there"). plan_ingress_owner_effect takes FOUR inputs, not three, and the fourth is machines. A fleet node has NO fleet-wide machine declarations: rg MachineConfig oss/yubaba/crates/yubaba/src/*.rs returns only doc-comment prose, yubaba never loads .yah/infra/machines/, and each node knows only its OWN declared facts, passed as CLI flags (–sovereign-group, –sovereign-role, region). So the leader cannot resolve a dead ingress owner to a provider, a floating IP id, or a public address — resolve_ingress_owner would refuse every time — and it has no path to the apex A records, which cloud’s dns.yah cloud apply to consume — smallest change, but the withdrawal then waits for a human to run apply, so a 3am node loss still costs half the round-robin, which is the exact pain R859 was filed against; (ii) reusing the elected ACME issuer’s Cloudflare credential (acme_issuer.rs:273/399 — the fleet DOES already write Cloudflare DNS, via passway-acme’s DNS-01 TXT publisher), because that makes a SECOND writer of yah.dev records outside the dns.* seam R859-F1 just established as the single source — the two-source problem this relay exists to dissolve, relocated rather than fixed. Chosen path is both automatic and single-seam, and costs a raft-surface change (epoch re-record) plus a Cloudflare credential on the nodes.”)
@yah:gotcha(“YIELDED ON THE YUBABA-SIDE PHASE — a live peer owns those files. @Ashguard:coffee (session:c431ac51, relay R600, lifecycle=working) is actively editing oss/yubaba/crates/yubaba/src/: scheduler.rs, headscale_appliance.rs and service_records.rs all had mtimes inside a 5-minute window while I was scoping the wiring (12:13-12:14 PDT 2026-09-08), and acme_issuer.rs / leader.rs / member_registration.rs / raft/mod.rs are dirty too. Those are exactly the files decision 2’s plumbing needs. Per the shared-tree rule I stopped BEFORE touching any of them rather than hand-fighting the file, so there is no seam of mine in their tree and nothing for them to re-check. Everything I did land is in disjoint paths (the new crate, cloud/src/provider/floating_ip.rs, two Cargo.tomls) and no unexpected diff appeared in any of them. Whoever picks this up: re-check camp.roster for a live session on R600 before opening scheduler.rs.”)
@yah:verify(“EXTRACTION VERIFIED 2026-09-08. New crate: cargo test --manifest-path oss/yubaba/crates/floating-ip/Cargo.toml = 18 passed / 0 failed (17 moved from cloud verbatim + 1 new, every_registered_provider_is_named_in_the_supported_list, pinning the table-to-error-message join now that the constructor reading the slot/env columns lives in a different crate). cargo clippy --manifest-path oss/yubaba/crates/floating-ip/Cargo.toml --all-targets: ZERO errors and ZERO warnings.”)
@yah:verify(“cloud after the extraction: lib 1136 passed / 0 failed / 4 ignored (from 1152/0/4 before — 17 tests moved out to the new crate, 1 new one added here: machine_facts_carries_every_field_the_floating_ip_layer_reads); tests/main 3 passed / 0 failed / 2 ignored; pond_smoke 2/0; doc-tests 0/0/1. Net across the two crates: 1152 -> 1154. cargo check --manifest-path oss/yubaba/Cargo.toml --all-targets clean, and cargo build --workspace EXIT=0 — the root workspace matters here because app/yah/cli consumes cloud through the [patch.crates-io] bridge, so a broken re-export would have surfaced there. Note the camp’s skew guard flagged concurrent peer edits to unrelated yubaba/src files during two of these runs; the final pass above was re-run and is the one quoted.”)
@yah:handoff(“Tree anchor at handoff: af805057ba632298d3e7e53c0197a164ed413824 (HEAD; everything below is uncommitted against it — quote this SHA, not ‘HEAD’, in any revert instruction). My files, all disjoint from the live peer’s yubaba/src edits: NEW oss/yubaba/crates/floating-ip/{Cargo.toml,src/lib.rs}; oss/yubaba/Cargo.toml (members); oss/yubaba/crates/cloud/Cargo.toml (the dep); cloud/src/provider/{floating_ip,hetzner_floating_ip,ovh_floating_ip,vultr_floating_ip}.rs. The @yah: annotation block on provider/floating_ip.rs was preserved verbatim through the rewrite — this ticket’s source anchor did not move.”)
@yah:handoff(“Tree anchor at handoff: af805057ba632298d3e7e53c0197a164ed413824 — the shared tree as I left it. Diff against it (git diff af805057ba632298d3e7e53c0197a164ed413824..HEAD) to see what landed under you, and quote this SHA rather than ‘HEAD’ in any revert/restore instruction.”)
@yah:handoff(“PHASE A + PHASE B BOTH LANDED, uncommitted against tree anchor e336e7278f7f40780323d40a8ceb6e438bead0b1 (quote that SHA, not HEAD, in any restore instruction). THE WHOLE CHAIN NOW RUNS ON THE FLEET: a node declares its public-ingress facts on its own flags -> member_registration publishes them into its raft member row -> the leader’s scheduler tick assembles Vec<FloatingIpMachine> from the member map, reads ingress_owner, judges the owner’s liveness through the hysteresis tracker and the quorum verdict, calls floating_ip::plan_ingress_owner_effect, and APPLIES a Withdraw as one content-matched Cloudflare A-record delete. R859 was filed because node loss left a dead A record taking ~half the round-robin until a human edited DNS; that is now automatic.”)
@yah:handoff(“PHASE A — the declarations. MemberInfo and YubabaRequest::SetMember each gained provider, location, ingress_floating_ip and public_address (Option<String>, #[serde(default)], raft/mod.rs), fed from four new yubaba serve flags of the same names, copied per node from .yah/infra/machines/<name>.toml exactly as –region is. location ships now although nothing reads it yet on the fleet path — every vendor adapter needs it to derive a mobility zone, and adding it later would cost a SECOND raft-surface change and a second cluster-epoch re-record for one optional string. Two accessors on YubabaStateMachine (raft/store.rs): floating_ip_machines() reassembles the fleet’s declarations into the planner’s shape, SKIPPING rows with no machine name (an empty name matches no ingress_owner, so including it would turn a clean "cannot resolve — refusing" into a silent lookup failure, and two such rows would look like a duplicate declaration); public_address_for_machine() is the one fact a content-matched delete cannot be performed without.”)
@yah:handoff(“PHASE A DISSOLVES THIS TICKET’S OWN LEAD GOTCHA, on the fleet path only. The recorded trap was that ingress_owner carries /etc/hostname and is not reliably the .yah/infra/machines/<name>.toml name (R841 saw vps-4c1efa56 for the box declared us-west-001), so cloud::resolve_ingress_owner would REFUSE on today’s fleet rather than misfire. That still holds for the yah cloud apply path, which resolves against machine TOMLs. It does NOT hold for the fleet path this ticket just built: MemberInfo.machine and ingress_owner are both written from leader::derive_machine_name() on the same box, so floating_ip_machines() is a list of hostnames and resolve_ingress_owner matches BY CONSTRUCTION rather than by luck. That is the quiet win in phase A and the reason the effector is live rather than inert.”)
@yah:handoff(“PHASE B — the effector, and the invariant that makes two writers of one apex safe. New module oss/yubaba/crates/yubaba/src/ingress_effector.rs. THE FLEET MAY ONLY EVER SUBTRACT; THE DECLARATION MAY ONLY EVER ADD. The fleet issues ONE targeted delete — the dead origin’s A record at the apex, matched on CONTENT, never on name (a round-robin apex holds several A records under one name, so deleting by name would take the survivors with it and turn a partial outage into a total one). It never renders the apex, never adds a record, and never needs the domain manifests or the machine TOMLs. yah cloud apply re-adds the record when the box returns, because R859-F1’s diff is fail-open on addition. The ApexWithdrawal trait has exactly ONE method, it deletes, and the module doc says in as many words that teaching it to add would put the two writers in a loop. Neither the empty-apex backstop nor the degraded-quorum refusal is re-implemented here — both live one layer up, and a second copy of a safety rule is a second thing to drift.”)
@yah:handoff(“WHY THE EFFECTOR RIDES THE SCHEDULER TICK and not a loop of its own: that tick is the one place per pass already holding all four inputs — the leader check, the TransitionTracker’s committed verdict, the quorum verdict R859-F2 added there last session, and the member map. A second loop would duplicate all four and could disagree with this one about any of them. previous_ingress_owner is tracked across ticks because plan_ingress_owner_effect is stateless; starting at None on a freshly elected leader is correct rather than a gap (first tick reads an existing owner as a change and converges the IP idempotently, withdrawal arms on the tick after). The marker is NOT advanced past a Failed apply — a withdrawal that did not land must be re-decided, not recorded as done. Config is env-sourced (YUBABA_INGRESS_APEX + a fob-injected CF token file + zone id), mirroring acme_issuer::parse_issuer_config rather than adding a fourth credential flag; unset is the default and every camp today. Half-configured is FATAL AT STARTUP on purpose — the opposite of member_registration’s never-wedge-a-boot rule — because an effector that could not read its token would discover that during the 3am failover it exists to perform.”)
@yah:handoff(“FOUR DISCOVERED FIXES OUTSIDE THE TICKET TITLE, all landed rather than filed. (1) raft/mod.rs:1801/:1849/:2617 — three #[cfg(test)] SetMember sites broke the whole camp’s cargo test -p yubaba --lib for a window; @Ashguard:dove flagged it with line numbers and I fixed it in the same pass. (2) NodeDeclaration (raft/mod.rs) + MemberInfo::from_declaration replace six positional Option<String>s threaded through member_registration::spawn -> run -> plan_registration -> write_row; at six params two adjacent Options could be swapped at a call site with no type error, and provider and location are both "a short lowercase string naming where this box lives". (3) SchedulerDeps (scheduler.rs) does the same for scheduler::spawn/run, which clippy flagged at 8/7 args — all four are Option<Arc<dyn …>> and three of the four would swap silently. Default is all-None, which is exactly "behaves as before any of these gates existed". (4) QuorumVerdict::as_ingress_health() (quorum_health.rs) implements the Unknown -> Degraded collapse this ticket’s own @yah:assumes recorded as documented-but-unwritten; it is now written and tested, and the collapse is only reachable because the effector is attached.”)
@yah:handoff(“CLUSTER-EPOCHS RE-RECORDED, both axes, NO BUMP — cluster_protocol stays 5 (a77f2501… → d4b539bd…), state_epoch stays 4 (10215557… → 5b11870a…). Gate re-run after: green on both. Full verdict in oss/yubaba/crates/yubaba/cluster-epochs.json surface_rerecords[2026-09-08]. TWO sessions’ work was in that diff and each verdict was made by the session that wrote its half: my four #[serde(default)] Option fields are the identical shape to machine, which surface_rerecords[2026-09-05] already ruled NOT BREAKING on the SAME struct and SAME variant; @Ashguard:dove’s R869 half (applied_state / seed_state_machine) is quoted verbatim and attributed. Checked git diff -U0 on raft/mod.rs and raft/store.rs before writing to confirm no third party’s unanalysed hunk was swept in — mod.rs is entirely mine, store.rs is exactly the two of us.”)
@yah:handoff(“FILES (all uncommitted against e336e7278f7f40780323d40a8ceb6e438bead0b1). NEW: oss/yubaba/crates/yubaba/src/ingress_effector.rs (11 tests). yubaba: raft/mod.rs (4 fields on MemberInfo + SetMember, NodeDeclaration, MemberInfo::from_declaration, apply arm, 4 test hunks), raft/store.rs (2 accessors + 1 test), member_registration.rs (NodeDeclaration through the whole loop, 1 new test), scheduler.rs (SchedulerDeps + the effector tick), quorum_health.rs (as_ingress_health + 1 test), lib.rs (module decl), main.rs (4 flags, effector construction, 2 no-raft warnings, SchedulerDeps), leader_pin.rs + headroom.rs (MemberInfo literals via from_declaration), Cargo.toml (yah-floating-ip as a RUNTIME dep), cluster-epochs.json, tests/raft_appliance_ownership.rs, tests/raft_tenant_placement.rs, yubaba-test-harness/src/solo_node.rs. app/yah/cli: resources/yubaba.service (EnvironmentFile=-/etc/yah-cloud/ingress.env + the reasoning), tests/camp_systemd_unit_emit.rs (1 new test pinning it). cloud: provider/floating_ip.rs is annotation-only (my board.claim timestamp), no code change. COLLISION CHECK: leader.rs, acme_issuer.rs, domain_issuer.rs, cert_store.rs, state_backup.rs and everything under oss/passway/ are dirty and are NOT mine — @Ashguard:coffee (R853) and @Ashguard:dove (R869). I touched none of them.”)
@yah:gotcha(“THE CODE IS DONE; THE FLEET IS NOT CONFIGURED, AND THAT IS AN OPERATOR STEP ON LIVE NODES, NOT A CODE ONE. Nothing withdraws anything until (a) each node’s systemd drop-in gains --provider --location --ingress-floating-ip --public-address and (b) /etc/yah-cloud/ingress.env is laid down on the raft voters. Grounded, not assumed: fleet flags are hand-rolled per node into a drop-in (see .yah/infra/cloud-init/dev-raft-node.sh:97 writing ExecStart= then a full yubaba serve … line) — nothing renders them from the machine TOML, and –region is not even passed on the dev-raft boxes today. Worse, R858’s measurement at app/yah/cli/src/mesh.rs:117 records that /etc/yah-cloud/ DOES NOT EXIST on us-east-001, so the acme.env and litestream.env EnvironmentFile lines already resolve to nothing there; ingress.env would too. The three public origins are us-west-001 15.204.89.240, us-south-001 45.32.194.254, us-east-001 51.81.85.145 — those are the –public-address values.”)
@yah:gotcha(“THE FLEET’S CLOUDFLARE TOKEN MUST BE ITS OWN, NOT THE ACME ISSUER’S — deliberately, and it is the decision this ticket’s operator call turned on. Operator decision 2 rejected reusing the elected ACME issuer’s credential (acme_issuer.rs:273/399) because that makes one token both publish TXT challenges and delete A records, which fuses the two writers this design keeps apart. The effector’s token needs DNS: Edit on the apex zone and nothing else. Also note R858’s finding at mesh.rs:117: yubaba’s ACME issuer is not running anywhere on this fleet (no YUBABA_ACME_* is set on any box) — the live doors get their certs from PASSWAY’s own acme engine. So there is no existing yubaba-side CF credential on the fleet to reuse even if that were wanted.”)
@yah:gotcha(“REASSIGN IS NOT APPLIED ON THE FLEET, IT IS REFUSED LOUDLY, AND IT IS UNREACHABLE ANYWAY — filed as R859-F3 rather than built, because it is genuinely separable rather than work I was standing on. Grounded: all nine machines in .yah/infra/machines/*.toml declare provider = \"static\", which has no floating-IP adapter, and none declares an ingress_floating_ip, so plan_ingress_owner_effect returns NoOp("declares no ingress_floating_ip") and cannot emit Reassign on this fleet at all. Making it real means extracting the three vendor adapters into a sibling of yah-floating-ip plus a vendor credential on every node — see R859-F3, which also records that the orphan-rule objection blocking that extraction is half wrong (a local trait on a foreign type is fine; only the three byte-identical inherent handler pairs bind, and one blanket impl replaces them). Until then apply_effect’s Reassign arm returns NotApplied naming the machine, the IP and the exact missing piece — pinned by an_unappliable_reassign_names_the_missing_transport, so it can never degrade into a silent no-op.”)
@yah:verify(“FINAL PASS 2026-09-08, every command re-run after the last edit, on tree anchor e336e7278f7f40780323d40a8ceb6e438bead0b1. cargo test --manifest-path oss/yubaba/Cargo.toml -p yubaba --lib = 828 passed / 0 failed / 0 ignored (baseline at the previous handoff was 724/0; the delta is this pass’s 14 plus peers’ tests landing alongside). cargo test --manifest-path oss/yubaba/crates/cloud/Cargo.toml --features json-schema = lib 1138/0/4, tests/main 3/0/2, pond_smoke 2/0, doc-tests 0/0/1 — cloud is untouched by this pass and stayed green. cargo check --manifest-path oss/yubaba/Cargo.toml --all-targets = no errors (covers the two integration-test files I edited). cargo build --workspace = no errors. cargo clippy --manifest-path oss/yubaba/Cargo.toml -p yubaba --all-targets = ZERO findings on any file I touched; the only warnings in that crate are pre-existing (raft/store.rs:397/:419 clone_on_copy, :491 derivable_impls) and belong to code I did not write. cargo test -p yah --test main camp_systemd_unit_emit = 14/0 (was 13; +1 for the new unit assertion). RUSTC_WRAPPER='' cargo run -p xtask -- cluster-epochs = GREEN on both axes after re-recording. scripts/check-schema-drift.sh = ok (this pass touched no generator input — MachineConfig did not move). scripts/check-workload-spec-ts.sh = ok. cargo run --bin yubaba -- serve --help shows all four new flags with their help text, so the clap wiring is proven rather than assumed.”)
@yah:verify(“WHAT THE 14 NEW TESTS ACTUALLY PIN, since a count is not evidence. ingress_effector.rs (11): a_withdrawal_deletes_the_dead_origins_own_address asserts the ADDRESS handed to the transport is the dead machine’s and not a survivor’s — the one mistake that turns this feature into an outage; an_undeclared_public_address_refuses_instead_of_guessing asserts NOTHING is deleted when the address is unknown; a_repeated_withdrawal_deleting_nothing_is_still_a_success pins the steady state while a node stays down (the planner re-emits Withdraw every tick, so 0-deleted must not read as failure); a_failed_provider_call_is_reported_as_failed_not_withdrawn keeps a failed withdrawal retryable instead of recorded as done; an_unappliable_reassign_names_the_missing_transport stops the unwired arm degrading into a silent no-op; not_knowing_is_never_confirmed_down pins the veto-only direction of the liveness channel through BOTH ways of not knowing (owner maps to no node id; node not committed either way); plus four config-parse cases including a_declared_apex_with_no_credential_is_refused_at_parse_time. raft/store.rs (1): the_member_map_reassembles_the_fleets_declarations — round-trips three SetMember rows through the real state machine, asserts the no-machine-name row is OMITTED, asserts resolve_ingress_owner Ok on a declared name and Err on an undeclared one, and asserts public_address_for_machine returns None rather than a fallback. member_registration.rs (1): the_public_ingress_declaration_is_compared_like_every_other_field — THE UPGRADE PATH: a row identical on every older field still rewrites when only the ingress declaration differs, then converges. quorum_health.rs (1): unknown_crosses_into_the_planner_as_degraded_carrying_its_own_reason.”)
Structs§
- Floating
IpAssign Outcome - Outcome of
reconcile_assignment/on_ingress_owner_changed. - Floating
IpMachine - The machine facts floating-IP mobility actually needs — the whole contract between this crate and whatever declares machines.
- Floating
IpState - Current provider-side state of a floating/reserved IP.
- Floating
IpTarget - A resolved reassign target: provider-native attach id + the mobility zone it lives in.
Enums§
- Ingress
Owner Effect - What should happen to public ingress, given an
ingress_ownerobservation. - Owner
Liveness - What a
TransitionTracker-style hysteresis says about one machine, crossed into this crate as plain data. - Quorum
Health - Live consensus health, crossed into this crate as plain data.
Constants§
- FLOATING_
IP_ PROVIDERS - Which providers ship a
FloatingIpProvideradapter, and the credential each one authenticates with —(provider id, vault slot, env fallback).
Traits§
- Floating
IpProvider - One provider’s floating/reserved-IP transport + mobility policy.
Functions§
- floating_
ip_ provider_ for - Resolve
machine.providerto a liveFloatingIpProvider— the registry R594-F5 left out. - machine_
facts - Narrow a
MachineConfigto the five facts floating-IP mobility reads. - on_
ingress_ owner_ changed - Callable entry point: react to the raft
ingress_ownerseam namingmachineas the box that now owns public ingress, by commandingip_idto follow it. - plan_
ingress_ owner_ effect - Decide what public ingress should do about an
ingress_ownerobservation — pure, so the decision is testable as arithmetic and the I/O is somebody else’s problem. - provider_
has_ floating_ ip_ adapter - Does
providerhave a floating-IP adapter at all? - reconcile_
assignment - Idempotent, zone-checked core shared by every provider adapter and by
on_ingress_owner_changed. - resolve_
ingress_ owner - Resolve an
ingress_ownerstring to the machine it names. - supported_
floating_ ip_ providers - The provider ids that ship an adapter, for an error message that names what is supported rather than only what is not.