Commit Graph
8 Commits
Author SHA1 Message Date
ab 686c74a3bd Enabled DNS by default 2026-09-22 16:08:06 +03:00
tsunagiandClaude Opus 5 9b240672e6 Put every network's address on the interface, and split up from join
Two networks on one agent, and only one of them worked. The interface
plan is exhaustive by contract — it is what the interface should carry and
nothing else — but the request that built it held a single address, so the
provisioner was told about the first network and took the second one's
address off, or never put it on. On the host that is a network whose
address the operating system has never heard of: the tunnel is up, the
status says all is well, and nothing routes. The request now carries every
address, which is also what takes one off when a network is left or
stopped.

The other half is the command line. `up --network X --secret Y` and
`network join` were two ways to do the same thing, and the one on `up`
could only be undone by restarting — which is how a network somebody left
came back, and how an invite line told the other side to start their agent
with a network baked into it. So they are one thing now, split the way the
system is: **`up` runs the agent** — the device's one process, serving
whatever it has joined, answering `status`, taking instructions — and
**`join` decides what it belongs to**, at any time, while it runs. `join`
is at the top level because it is what gets typed; `network join` is the
same command for anyone who likes the long form.

Every line that told somebody to type the old form is gone with it: the
invite after making a network, the lock error from a second `up`, the
empty-network hint in `id`, the README walkthrough and the WireGuard
document. The invite now prints the `join` line for the other machine and,
separately, the `up --peer` line for an agent that is not running yet —
two commands, because they really are two, and no amount of wording makes
starting an agent the same thing as joining a network.

Joining says how the network stood before: new, already here, or stopped
and now running again. That last one matters — joining is an instruction
to run it, so it undoes a stop, and a pause that ends without a word is a
pause nobody can rely on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 00:30:03 +01:00
tsunagiandClaude Opus 5 1b2f050c05 Start the agent without deciding anything yet
`up` demanded a network, so there was no way to run the agent in one
terminal and decide what it belongs to in another — which is the shape of
the thing now that networks are joined, stopped and left while it runs.

`--network` is optional. Without it the agent starts with whatever it is
already configured for and waits; the banner counts the networks instead of
naming one, and points at `tsunagi network join`. A secret with no network
is refused rather than ignored, because it says nothing on its own.

The periodic summary, which had one network by construction, now falls back
to the first running one — `tsunagi status` is the whole picture and this
line was never more than a glance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 23:36:36 +01:00
tsunagiandClaude Opus 5 637e2f74e4 Tell being away from giving up
Leaving was the only way out of a network, and it is the irreversible one:
it publishes a release and then removes the configuration, the secret, the
network's signed records, its cached hints and the protocol key it used.
What somebody usually wants before a reboot, a trip or an experiment is
the other thing — stop serving it and keep everything.

`tsunagi network stop <id>` closes that network's sessions, takes its
address off the interface and keeps it from starting again. Nothing is
announced, deliberately: to the others this device is away, which is an
ordinary condition they already handle, and the address and name it holds
stay reserved for it. `tsunagi network start <id>` resumes it where it left
off. Both are remembered, so a restart does what the last instruction said
rather than what the last command line happened to say.

Except when the command line says otherwise: `up --network X` starts X
whatever its stored state, because a command naming a network is an
instruction to run it. The banner now says which of the three happened —
`new`, `already here`, or `was stopped; this command starts it` — since
silently, that is a stop that comes back from the dead with nothing to
explain it.

The listing tells the three states apart too: running with its address,
stopped and kept, or configured and waiting for an agent to start. Each row
says what to type to move it, because "stop" and "leave" are a pair that
has to be easy to tell apart before the irreversible one is typed.

The local control protocol is 11.

Covered end to end against a running agent: stopping leaves it configured
and says so, stopping twice is the state asked for rather than an error,
starting brings it back, and the secret afterwards is the one from before —
so it is the same network and not a lookalike.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 23:32:23 +01:00
tsunagiandClaude Opus 5 415b6a6667 Make a network on the spot, and say which one you just started
Two reports of the same shape: a network was left and came back after a
restart, and `network join` asked for a secret it could have invented.

The first was not a bug in leaving. The secret on the start command line
derives the network id, so a command line carrying the secret of a network
you have just left recreates it on the next start — which is right, it says
to join that network, but nothing on screen said so. `up` now marks the
network `· new` or `· already here`, and warns in full when another
configured network answers to the same name. A name is a label; the id is
the identity, and the secret is what decides which of them this is. Said at
the moment it happens it is obvious; discovered later in a status report it
is a mystery, which is exactly how it went.

The second was an omission: `up` had learned to invent a secret and
`network join` had not, so the quickest possible thing — a network with
somebody for as long as it is needed, then gone — still needed a secret
generated first. Both now resolve a bare name the same way: the one network
of that name this device already has, or a fresh random secret when there
is none. It is printed in full, with the single line the other person can
paste as it stands, endpoint id included, because a secret nobody can read
is a network nobody can join.

The id is printed in full by both answers now. The shortened form belongs in
a report, where it is read; this one gets copied into the next command.

Covered end to end against a running agent: joining with no secret prints a
secret and a pasteable command with a peer in it, and joining a name this
device already has resumes that network instead of making another that
merely looks the same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 23:19:58 +01:00
tsunagiandClaude Opus 5 0f97d60854 Serve a zone per network, and make a network in one command
Twice now a report has read "dns not serving" and been taken for a broken
resolver. It was accurate both times: the agent had been started without
`--dns`. That is the flag's fault, not the reader's — a resolver that
disappears because one word was not retyped is worse than none, since the
names simply stop working. So the setting belongs to the device now: `--dns`
turns it on and it stays on, `tsunagi dns off` turns it off, and
`tsunagi dns on` turns it on for an agent that is already running, without
restarting it. `tsunagi dns` says what it is doing, or what it will do at
the next start when nothing is running.

One agent has one identity and as many networks as it likes, so one DNS
service serves them all: each network is a zone named after it, and joining
or leaving one changes what resolves with no restart. A question carries a
name and not the network it belongs to, so the suffix decides and nothing is
shared between zones — a member of one network is not a name in another.
`--dns-zone` is gone with that: there is no single zone to name any more, and
a network name may contain dots, so `--network lab.internal` is how you get
`music.lab.internal`.

It listens on loopback only, where it always could have. Binding the overlay
address put the zones in front of the whole mesh, and with several networks
on one agent that would have answered one network's questions about
another's names.

That made a gap plain: a second network on an agent had no addresses at all,
because the configured range belongs to whichever network took it first, so
its members had nothing to allocate from and no names to answer with. A
second network now uses the range **derived from its own network id** —
every member derives the same one from something they all already have, so
it is an agreement rather than a local invention. It is held back for a
moment first, because a network that already exists has a range of its own
and a joiner should adopt it rather than argue; that wait is what keeps
"the first member settles it" true.

And a network needs no ceremony to start. `tsunagi up --network lab` with no
secret resolves the obvious way: the one network of that name this device
already has, or — when there is none — a fresh random secret, printed in
full with the single line to send the others. That is the ad-hoc case, one
person makes a network and passes the command round, and it was previously
two steps with a flag people could not find. The secret is printed only when
the agent invented it, because then there is nowhere else to read it from;
one that was supplied is not echoed. Two networks of one name and no secret
is the one case with no answer, and it says so rather than choosing.

Releasing now also stops this agent claiming again. The periodic check would
otherwise publish a fresh claim in the moment between the goodbye and the
teardown, turning a release into a hello nobody asked for.

Exercised with the real binary: a zone per network as a second one is joined
into a running agent, the resolver switched on and off while it runs, and an
ad-hoc network printing its secret and the line to share.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 23:03:42 +01:00
tsunagiandClaude Opus 5 ff7e235414 Split the command line by level
`up` now says which level each setting belongs to, and `--help` shows the
two sections. System: how the agent reaches peers, the one interface it
owns, the address range, the resolver. Transport: which protocols carry
packets and what they take.

`--wireguard` is gone. `--protocol` takes a list and defaults to
`wg-quic`, which is what the protocol is now called — WireGuard's
cryptography in QUIC datagrams, so the name says what is on the wire
rather than what the implementation borrows. `--protocol none` runs the
control plane alone.

Protocol settings moved to `-o key=value`, or `-o protocol:key=value`
when several are selected. Each protocol declares its own settings and
their help, so `tsunagi protocols` can list them without the agent
knowing anything about any protocol, and a setting nobody takes is
refused rather than dropped — a dropped setting looks exactly like one
that did not work. What the user asked for is checked before anything
that could fail on its own, so a misspelled protocol is not buried under
a privilege error.

`--wg-prefix` and `--wg-mtu` became `--interface` and `--mtu`: they were
never the protocol's, and the interface they describe belongs to the
agent. `--transport` became `--reach`, because "transport" now means the
protocol level and using the word for iroh's path policy as well would
be a collision of meaning rather than a shortage of words.

The plugin gave up the last things that were not its own: the interface
name it carried in its own state, and the check that this agent's
address is really on an interface. Both are the agent's, and the check
is now the agent's too, still said once per address rather than every
round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 19:44:21 +01:00
tsunagiandClaude Opus 5 60e6b263d1 Split the system level and the command line into a workspace
First step of separating the layers. The library and the binary are now
crates/tsunagi and crates/tsunagi-cli, which means the plugin crate to
come can be told apart from the core by the compiler rather than by
discipline.

Falls out of it immediately: the CLI's dependencies stop being features
of the library. clap, anstream and tracing-subscriber were optional
dependencies behind a `cli` feature that every library user had to
remember to turn off; now they belong to the crate that uses them, and
the library defaults to no features at all.

The one test that drives the binary moved beside it — a library cannot
depend on a binary built from a crate that depends on the library — and
was rewritten against the public API instead of the test harness.

AGENTS.md said to prefer one crate. It now says the system level and its
plugins are separate crates, for the reason above, and that everything
else stays one crate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 18:05:42 +01:00