Repository navigation
Changelog
All notable changes to the agentcell crate and the sand / agentlsm
binaries. This project adheres to Semantic Versioning.
0.2.3 — 2026-09-29
Fixed
agentlsm servenow honors its own NETUP error contract: a failed NETUP
no longer overwrites the specificERR egress_unresolved <host>/
ERR egress_too_many_ipsreplies with a genericERR netup, so clients
finally see why egress provisioning failed.- A crashed/restarted daemon no longer leaks state into new cells. On
startup (and when a NETUP recycles a veth index whose stale link survived
a crash) it removes leftovervethh*pairs and every iptables rule that
mentions them — previously the previous cell's per-IP ACCEPT rules
survived the crash and silently grafted its allowlist onto the next cell
using that index. Leftover global NAT rules are cleaned up too. egress_applyno longer counts a rule thatiptables -Ifailed to
install; the NETUP reply's address count is now the truth, and failures
are logged per rule.- Egress resolution is bounded (1 s per nameserver attempt) instead of the
resolver defaults; NETUP and the TTL refresh run inline in the serve
loop, and the old defaults could stall every control client for minutes
when a nameserver was unreachable. - An over-long control line no longer desynchronizes the protocol: after
ERR line_too_long, the daemon drains the rest of the offending line
instead of parsing its tail as new commands. agentlsm servetakes an exclusive lock (/run/agentcell/agentlsm.lock)
and refuses to start a second instance. Startup recovery deletes every
vethh*rule it finds, which would otherwise destroy a live daemon's
cells; the lock is released on process death, so a crash does not lock
out the next start.net.ipv4.ip_forwardis restored even after a crash: the pre-daemon
value is persisted to/run/agentcell/ip_forward.savedwhen NAT is
enabled and consumed by startup recovery when a previous run died before
nat_teardown().- Egress no longer falls back to
getaddrinfo()on the host stub. A
NETUP that asks for egress but carries noRESOLVis refused with
ERR egress_no_resolv, and a nameserver that answers nothing yields
egress_unresolved— so an oldsand(or a cell with no resolvers)
fails loudly instead of silently resolving with the host's view. egress_no_resolvis covered bytests/contract_test.
0.2.2 — 2026-09-28
Fixed
sandnow requires the egress counts in theNETUPreply. When egress
was requested it demands all five fields,n_hosts == the requested host countandn_ips > 0; a pre-0.2.1 daemon — which ignores theRESOLV
token, installs no accept rules and still answersOK <if> <cell> <gw>— no longer reads as success. Mismatches take the existing
egress unavailablepath (exit 127, no silent--net nonefallback).
The three-field reply is still accepted when no egress was requested.
Added
agentlsmlogs a warning when a veth is built with no egress
allowlist (WARNING: vethN (pid P) has no egress allowlist — unrestricted NAT). Plain--net vethremains real networking with NAT; this only
makes the situation visible in the journal.AGENTCELL_LSM_SOCKoverrides the daemon control socket path, and
tests/stub-agentlsm.cdrives theNETUPreply branches without root; the
newtests/run.shcases cover the old/short/zero-address replies and show
the 0.2.1 reply is still accepted.
0.2.1 — 2026-09-27
Fixed
- Egress now resolves against the cell's nameservers.
sandsends them
inNETUP … RESOLV <addr>[:port] …(parsed from systemd-resolved's
upstream list that it bind-mounts into the cell), andagentlsmresolves
each host withres_nqueryagainst those servers instead of the host
stub. This removes the resolver skew that made the allowlist miss the
address the cell dials. - Egress addresses are refreshed on the record TTL (capped at 60 s)
while the cell lives, so CDNs that rotate addresses within one resolver
stay reachable. A transient refresh failure keeps the last good set. - Egress failures are loud: the daemon replies
ERR egress_unresolved <host>/egress_too_many_ips/egress_too_many_hosts(rolls back the
veth), andsandexits nonzero withegress unavailable: <reason>
instead of silently falling back to--net nonewhen egress was
requested. The success reply now carries host/address counts. ERR line_too_longis returned instead of silently discarding an
over-long control line; theNETUPbuilder no longer truncates (and
fails instead), and the control buffers were enlarged.
Added
sand --capabilitiesprints the feature flags (egress_multi,
egress_refresh,egress_resolv,env,env_file,secret,
workdir_size) for hosts to gate on.--egressvalidation rejects empty/whitespace hosts, IPv6 literals
(unsupported by the AF_INET rules) and duplicates.
0.2.0 — 2026-09-27
Added
--egress HOST[:PORT]is repeatable (port defaults to 443, up to 16
entries).agentlsmresolves every A record of each host and installs
one ACCEPT per address, so CDN registries with rotating IPs
(static.crates.io,files.pythonhosted.org,registry.npmjs.org) stay
reachable. Daemon protocol is now
NETUP <pid> EGRESS H1 P1 [EGRESS H2 P2 …].--env K=V(repeatable) and--env-file PATHset the cell environment.
Use the file form (0600) for secrets: argv is visible to other host users.--secret DST=SRCcopies a 0600 host file into the cell's tmpfs;DST
must be under/tmp,/runor/var/tmp.--workdir-size SIZEbacks the workspace (/home/agent) with a size-capped
tmpfs instead of a host bind. A runaway build getsENOSPCinstead of
filling the node disk. Counts against--mem.- C ABI:
agentcell_config.egress_list(NULL-terminated) for multiple
destinations; the existingegressfield still works. - Rust:
Config::egressis repeatable (Vec<String>) and feeds
egress_list.
Changed
--egress HOST(no port) is accepted and defaults to 443; the cell's
http_proxy/https_proxyare only set when exactly one entry is given.agentlsmcontrol line buffer grew 256 → 2048 and thesandcommand
buffer 320 → 1024 to fit multi-hostNETUPlines.
Fixed
agentlsmno longer leaks per-cell egress rules when the daemon exits and
now frees the/30bit during cleanup.
Known limitations
- The egress allowlist is resolved once, at cell start, on the host's
resolver. A cell whose own resolvers return a different CDN anycast set can
dial an address the firewall does not allow (intermittentnetwork_denied).
Align the resolvers or use a host-side CONNECT proxy. --net vethwithout any--egressentry still has full NAT (default-deny
applies only when at least one entry is given).
0.1.0 — 2025
- First public crate: unprivileged cell built on user/mount/pid/net/ipc/uts/
cgroup namespaces, Landlock, seccomp-BPF and cgroup v2, with a Rust wrapper
over the C ABI.