feat(#2363): full-DHCP for all agents — drop static agent_network_ip

All agent containers now receive their bridge IP dynamically via DHCP
from the dnsmasq pool instead of a hash-derived static address:

- nix/templates/harness-base.nix: networking.useDHCP = true
- nix/modules/hive-gateway.nix: expand DHCP pool to full usable range
  (.2 to .254 on /24) — was last-14-IPs-only
- hive-sh4re/src/priv_proto.rs: remove agent_ip from NetworkIsolation
- hive-c0re/src/lifecycle/mod.rs: drop agent_network_ip + DHCP_POOL_SIZE
- hive-c0re/src/lifecycle/host_config.rs: remove agent_network_ip call
- hive-priv/src/main.rs: LOCAL_ADDRESS= empty (DHCP assigns IP);
  HOST_ADDRESS still set so nixos-container installs default route
  before the DHCP lease arrives
- nix/dhcp-pool-size: deleted (no longer needed)

The nix/dhcp-pool-size single-source-of-truth file and all associated
Rust/Nix dual-constant plumbing are gone — there is no static map.
bridge_gateway_ip() is retained (still needed for HOST_ADDRESS).

Closes #2363
This commit is contained in:
atlas 2026-07-13 11:18:42 +02:00 committed by mara
commit 4cdbbafc44
8 changed files with 37 additions and 244 deletions

View file

@ -37,80 +37,6 @@ async fn setup_proposed_seeds_flake_nix() {
assert!(tracked.contains("flake.nix"), "flake.nix not committed");
}
#[test]
fn agent_network_ip_is_in_subnet() {
// Default subnet 10.42.0.0/24 — agents get .2 to .240 (last 14 are DHCP pool).
// agent_slots = usable(253) - DHCP_POOL_SIZE(14) = 239 → offsets in [2, 240].
let ip = agent_network_ip("alice", "10.42.0.0/24").expect("should produce an IP");
let octets: Vec<u8> = ip.split('.').map(|o| o.parse().unwrap()).collect();
assert_eq!(&octets[..3], &[10, 42, 0], "wrong /24 prefix");
assert!(
octets[3] >= 2 && octets[3] <= 240,
"host byte {} — expected in agent-only range [2,240]",
octets[3]
);
}
#[test]
fn agent_network_ip_never_in_dhcp_pool() {
// No agent should be assigned an address in the DHCP pool
// (.241-.254 on a /24 with DHCP_POOL_SIZE=14).
// agent_slots = usable(253) - DHCP_POOL_SIZE(14) = 239 → offsets in [2, 240].
let subnet = "10.42.0.0/24";
let names = [
"alice",
"bob",
"carol",
"damocles",
"iris",
"argus",
"atlas",
"ruth",
"dmatrix",
"bitburner",
"lexis",
"sock",
"triage",
"janet",
"eve",
"frank",
"grace",
"heidi",
];
for name in names {
let ip = agent_network_ip(name, subnet).unwrap_or_else(|| panic!("{name} returned None"));
let last: u8 = ip.rsplit('.').next().unwrap().parse().unwrap();
assert!(
last < 241,
"{name} got .{last} — inside the DHCP pool [.241-.254]"
);
}
}
#[test]
fn agent_network_ip_stable() {
// Same name + subnet must always produce the same IP.
let a = agent_network_ip("damocles", "10.42.0.0/24");
let b = agent_network_ip("damocles", "10.42.0.0/24");
assert_eq!(a, b);
}
#[test]
fn agent_network_ip_different_agents() {
// Different agent names very likely produce different IPs (not guaranteed,
// but for these two names the hashes don't collide).
let alice = agent_network_ip("alice", "10.42.0.0/24").unwrap();
let bob = agent_network_ip("bob", "10.42.0.0/24").unwrap();
assert_ne!(alice, bob, "alice and bob collide — rename one");
}
#[test]
fn agent_network_ip_different_subnet() {
let ip = agent_network_ip("alice", "192.168.5.0/24").expect("should produce an IP");
let octets: Vec<u8> = ip.split('.').map(|o| o.parse().unwrap()).collect();
assert_eq!(&octets[..3], &[192, 168, 5]);
}
#[test]
fn bridge_gateway_ip_extracts_verbatim_address() {
// HIVE_NETWORK_SUBNET carries the bridge IP verbatim, not the
@ -139,31 +65,6 @@ fn bridge_gateway_ip_rejects_bad_input() {
assert!(bridge_gateway_ip("10.42.0/24").is_none()); // 3 octets
}
#[test]
fn agent_network_ip_rejects_bad_input() {
assert!(agent_network_ip("alice", "notanip/24").is_none());
assert!(agent_network_ip("alice", "10.0.0.0/33").is_none()); // prefix > 32
assert!(agent_network_ip("alice", "10.0.0.0/31").is_none()); // too small
assert!(agent_network_ip("alice", "10.0.0.0").is_none()); // no prefix
}
#[test]
fn agent_network_ip_normalizes_bridge_ip_subnet() {
// HIVE_NETWORK_SUBNET carries the bridge IP (10.42.0.1/24), not
// canonical network (10.42.0.0/24). Both must produce the same result
// after host-bit masking.
let from_bridge = agent_network_ip("alice", "10.42.0.1/24");
let from_canonical = agent_network_ip("alice", "10.42.0.0/24");
assert_eq!(
from_bridge, from_canonical,
"bridge-IP and canonical-network form should normalize to the same result"
);
// Result must be in .2-.240 (DHCP pool .241-.254 is excluded).
let ip = from_bridge.unwrap();
let last: u8 = ip.rsplit('.').next().unwrap().parse().unwrap();
assert!((2..=240).contains(&last), "host byte {last}");
}
/// `setup_proposed` is idempotent: calling it on an existing repo is a
/// no-op (the fresh guard skips all writes).
#[tokio::test]