Make joining a network idempotent and shut down cleanly on every path

Running `tsunagi up` twice with the same arguments failed with "network ...
is already active", and then dropped the iroh endpoint without closing it.

A configured network is activated automatically at startup, so the second
run found it already up. `join_network` is declarative — "be a member of
this network" — so joining one that is already active now succeeds and
changes nothing. `activate_network` stays strict for callers that
specifically want to know whether an inactive network was started.

The CLI now closes the agent on the error path too, and handles SIGTERM as
well as Ctrl-C, so a service manager stopping the agent gets the same clean
shutdown an interactive user does.

Also documents the two lookups people conflate: resolving one endpoint's
address is iroh's public pkarr/DNS service and works today, which is why
`--peer <endpoint-id>` needs no address; finding who is in a network is this
project's `NetworkDiscovery` and is still static bootstrap only. Notes in
the README and the threat model that `n0` and `direct` publish this
endpoint's addresses to a public third-party service.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
tsunagi
2026-09-21 12:08:48 +01:00
co-authored by Claude Opus 5
parent 21be7e9b44
commit 5cc92d7067
6 changed files with 143 additions and 20 deletions
+16 -3
View File
@@ -242,10 +242,17 @@ impl Agent {
self.inner.events.subscribe()
}
/// Adds a network to the persistent configuration and activates it.
/// Makes this agent a member of a network, activating it.
///
/// The same `(name, secret)` always produces the same [`NetworkId`], on
/// every device.
///
/// This is declarative and therefore **idempotent**: joining a network
/// that is already active succeeds and changes nothing. That matters
/// because a configured network is activated automatically at startup, so
/// running the same command twice must not be an error. Use
/// [`Agent::activate_network`] when you specifically want to know whether
/// an inactive network was started.
pub async fn join_network(
&self,
name: &NetworkName,
@@ -257,11 +264,17 @@ impl Agent {
.storage
.upsert_network(network_id, name.clone(), secret.clone(), true)
.await?;
self.activate_with_keys(keys).await?;
Ok(network_id)
match self.activate_with_keys(keys).await {
// Already a member of exactly this network space: nothing to do.
Ok(()) | Err(Error::NetworkAlreadyActive(_)) => Ok(network_id),
Err(err) => Err(err),
}
}
/// Activates a configured network that is currently inactive.
///
/// Fails with [`Error::NetworkAlreadyActive`] if it is already running.
/// [`Agent::join_network`] is the forgiving version.
pub async fn activate_network(&self, network_id: NetworkId) -> Result<()> {
let stored = self
.inner