refactor(maestro): typed perimeter advertisement replaces Option pair (NAP-272)
ci / rust (push) Failing after 5m58s
ci / rust (push) Failing after 5m58s
`(perimeter_id, private_addr)` was carried as two `Option<String>`s with an
unwritten "both Some or both None" invariant, enforced ad-hoc at every reader
and silently violable (`with_perimeter(Some, None)`).
Collapse it to `Option<PerimeterCandidate>` (reusing the existing protocol
type, so it flows unconverted from Heartbeat → registry → AttachCandidates):
- protocol: `Heartbeat { perimeter: Option<PerimeterCandidate> }` (empty still
serializes to the legacy `{"type":"Heartbeat"}` shape).
- registry: `DaemonEntry.perimeter`, `update_perimeter`, `perimeter_for` lose
the defensive AND-check; `build_attach_candidates` uses it directly.
- daemon: `RegistryClient.with_perimeter(Option<PerimeterCandidate>)`; the
both-or-neither rule now lives once, in `DaemonConfig::perimeter()`, which
warns and drops a half-configured pair instead of advertising it.
Unblocked by NAP-270's coverage; broker T2 + perimeter-mismatch e2e green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,14 +113,13 @@ pub enum DaemonOutbound {
|
||||
SessionUpdated {
|
||||
session: Session,
|
||||
},
|
||||
/// Liveness ticker. The optional `perimeter_id` / `private_addr` advertise
|
||||
/// the daemon's reachability for the T2 transport (NAP-242). Old daemons
|
||||
/// that don't set them still produce the bare `{"type":"Heartbeat"}` shape.
|
||||
/// Liveness ticker. The optional `perimeter` advertises the daemon's
|
||||
/// reachability for the T2 transport (NAP-242); the typed object encodes the
|
||||
/// "both id and addr, or neither" invariant once (NAP-272). Old daemons that
|
||||
/// don't set it still produce the bare `{"type":"Heartbeat"}` shape.
|
||||
Heartbeat {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
perimeter_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
private_addr: Option<String>,
|
||||
perimeter: Option<PerimeterCandidate>,
|
||||
},
|
||||
CreateSessionResult {
|
||||
reply_to: ReplyTo,
|
||||
@@ -522,13 +521,12 @@ mod tests {
|
||||
DaemonOutbound::SessionUpdated {
|
||||
session: session.clone(),
|
||||
},
|
||||
DaemonOutbound::Heartbeat { perimeter: None },
|
||||
DaemonOutbound::Heartbeat {
|
||||
perimeter_id: None,
|
||||
private_addr: None,
|
||||
},
|
||||
DaemonOutbound::Heartbeat {
|
||||
perimeter_id: Some("wg-naps62".into()),
|
||||
private_addr: Some("10.0.0.7:7070".into()),
|
||||
perimeter: Some(PerimeterCandidate {
|
||||
perimeter_id: "wg-naps62".into(),
|
||||
private_addr: "10.0.0.7:7070".into(),
|
||||
}),
|
||||
},
|
||||
DaemonOutbound::CreateSessionResult {
|
||||
reply_to,
|
||||
@@ -690,12 +688,8 @@ mod tests {
|
||||
let legacy = serde_json::json!({ "type": "Heartbeat" });
|
||||
let parsed: DaemonOutbound = serde_json::from_value(legacy).unwrap();
|
||||
match parsed {
|
||||
DaemonOutbound::Heartbeat {
|
||||
perimeter_id,
|
||||
private_addr,
|
||||
} => {
|
||||
assert!(perimeter_id.is_none());
|
||||
assert!(private_addr.is_none());
|
||||
DaemonOutbound::Heartbeat { perimeter } => {
|
||||
assert!(perimeter.is_none());
|
||||
}
|
||||
other => panic!("expected Heartbeat, got {other:?}"),
|
||||
}
|
||||
@@ -703,13 +697,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn heartbeat_empty_serializes_to_legacy_shape() {
|
||||
// A new daemon that doesn't set perimeter/addr must produce wire bytes
|
||||
// A new daemon that doesn't set a perimeter must produce wire bytes
|
||||
// identical to an old daemon — registries doing equality checks or
|
||||
// logging payloads see no surprise.
|
||||
let hb = DaemonOutbound::Heartbeat {
|
||||
perimeter_id: None,
|
||||
private_addr: None,
|
||||
};
|
||||
let hb = DaemonOutbound::Heartbeat { perimeter: None };
|
||||
let v = serde_json::to_value(&hb).unwrap();
|
||||
assert_eq!(v, serde_json::json!({ "type": "Heartbeat" }));
|
||||
}
|
||||
@@ -717,13 +708,15 @@ mod tests {
|
||||
#[test]
|
||||
fn heartbeat_with_fields_round_trips() {
|
||||
let hb = DaemonOutbound::Heartbeat {
|
||||
perimeter_id: Some("wg-naps62".into()),
|
||||
private_addr: Some("10.0.0.7:7070".into()),
|
||||
perimeter: Some(PerimeterCandidate {
|
||||
perimeter_id: "wg-naps62".into(),
|
||||
private_addr: "10.0.0.7:7070".into(),
|
||||
}),
|
||||
};
|
||||
let v = serde_json::to_value(&hb).unwrap();
|
||||
assert_eq!(v["type"], "Heartbeat");
|
||||
assert_eq!(v["perimeter_id"], "wg-naps62");
|
||||
assert_eq!(v["private_addr"], "10.0.0.7:7070");
|
||||
assert_eq!(v["perimeter"]["perimeter_id"], "wg-naps62");
|
||||
assert_eq!(v["perimeter"]["private_addr"], "10.0.0.7:7070");
|
||||
assert_eq!(hb, round_trip(&hb));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user