Skip to content

feat(desktop): scope connected agents by community - #4144

Draft
dspury wants to merge 5 commits into
block:mainfrom
Lunar-Park:pr/connected-agent-community-scope
Draft

feat(desktop): scope connected agents by community#4144
dspury wants to merge 5 commits into
block:mainfrom
Lunar-Park:pr/connected-agent-community-scope

Conversation

@dspury

@dspury dspury commented Aug 1, 2026

Copy link
Copy Markdown

Stacked on #4029. The incremental review for this PR is the top commit only: feat(desktop): scope connected agents by community.

Summary

  • stamp each connected-agent record with the normalized active community relay
  • show connected agents only in the community where they were connected
  • preserve visibility for legacy records that predate community scoping
  • keep the community field display-only so agent traffic still follows the active workspace relay

Why

Connected-agent records were stored globally and appeared in every configured community, including communities where the agent identity was not connected. This could make an unavailable agent look ready to use.

The connection boundary now derives the community from Buzz state rather than accepting it from the caller. Existing records remain compatible because the new field is optional.

Validation

  • cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --check
  • cargo test --manifest-path desktop/src-tauri/Cargo.toml remote_agent_connect --lib
  • cargo test --manifest-path desktop/src-tauri/Cargo.toml connected_agents --lib
  • just desktop-tauri-clippy
  • cd desktop && pnpm check
  • cd desktop && pnpm typecheck
  • focused Biome check for the four changed frontend files
  • 24 focused frontend intent and community-scoping tests

Review scope

The community-scoping behavior is contained in the top commit, 73f46bccc.

dspury added 5 commits July 31, 2026 14:17
Signed-off-by: dspury <demitriusspury@gmail.com>
Buzz has only ever had one answer to "where does this agent's key live" —
Buzz minted it and it is in this machine's keyring — so nothing in the code
asked. A self-hosted agent breaks that: a real agent with a real pubkey whose
secret was minted on, and never leaves, a machine the user owns. Every
lifecycle affordance Buzz offers (start, stop, deploy, tombstone, profile
republish) is predicated on holding that key.

`ConnectedAgentRecord` in `connected-agents.json`, beside `managed-agents.json`
and read by nothing else. The type carries a pubkey, a Buzz-local name, an
`~/.ssh/config` host, and an observed harness label — no key, no command, no
timeout, no auto-start flag, no pid.

Never-spawned is structural. It is not that the lifecycle paths skip these
records; it is that they cannot receive one. `load_managed_agents` — the reader
behind spawn, deploy, auto-start restore, owner-signed kind:30177 reconcile,
profile republish, and delete-with-tombstone — returns `ManagedAgentRecord`,
and a connected agent is a different type in a different file. There is no
filter to maintain and no field for a future reader to misinterpret.

An earlier cut of this got there differently: `KeyCustody { Local, Remote }` on
`ManagedAgentRecord`, plus a custody filter inside `load_managed_agents`. It
worked and it was tested, but it had three costs. It made "Buzz must not act on
this agent" a value every reader had to interpret correctly rather than a shape
they could not misuse. It required `save_managed_agents` to re-read the
connected third, because otherwise each of the dozens of existing
`load → mutate → save` sites would silently erase every connected agent — a
sharp edge that needed its own regression test. And it obliged all ~20
`ManagedAgentRecord { .. }` literals in the tree to name a custody field they
have no opinion on, which put eight already-oversized upstream files over the
desktop size ratchet.

Separating the type retires all three. `storage.rs`, `storage_tests.rs`,
`types.rs`, and `reconcile.rs` are byte-identical to upstream; the churn in the
other 23 files is gone; the erase-on-unrelated-save failure mode does not exist,
because the two stores share no payload to drop a half of. Nothing splits an
upstream file and nothing touches the ratchet.

What a type cannot state on its own is covered by cross-store tests: a
connected row does not satisfy `ManagedAgentRecord` and an owned row does not
satisfy `ConnectedAgentRecord` (which is why `host` is a plain `String` — as an
`Option` an owned row read from the wrong file would deserialize into a
connected agent that can never be probed); a connected save leaves
`managed-agents.json` byte-identical; the connected store holds nothing
key-shaped, which is what makes the ordinary non-`0o600` write correct.

Uniqueness is the one property that does not partition, so `connect` checks
both stores: one identity with a row in each would be two answers to "who is
this pubkey", and a duplicate name is ambiguous at every mention site. It also
covers the key-less definition half, whose names are just as mentionable.

Two publish paths stay cut. Connecting emits no kind:30177 — that event is the
owner asserting "I manage this agent" and is what `delete_managed_agent`
tombstones, so publishing it would let Buzz claim, and later revoke, the
directory entry for an agent it cannot restart. A self-hosted agent's directory
presence is its own replaceable kind:10100. And `disconnect_remote_agent` is
not `delete_managed_agent`: it drops Buzz's local pointer with no process stop,
no keyring delete, and no tombstone or NIP-IA archive, because those would
remove a working agent from every member picker on the relay.

Desktop surface: a Connected Agents section listing host, harness, and
reachability, with a connect dialog driven by the parsed ssh config. It renders
no start/stop control — `ConnectedAgentSummary` has no `status`, `pid`, or
`needsRestart` to render one from, which is a property of the type rather than a
rule someone has to remember.

1910 backend tests and 3782 desktop JS tests pass; fmt, clippy, tsc, biome, and
all three `pnpm check` guards are clean against base `upstream/main`.

Signed-off-by: dspury <demitriusspury@gmail.com>
(cherry picked from commit 0987111)
Signed-off-by: dspury <demitriusspury@gmail.com>
(cherry picked from commit 4a20284)
(cherry picked from commit f43c379)
Signed-off-by: dspury <demitriusspury@gmail.com>
Signed-off-by: dspury <demitriusspury@gmail.com>
Signed-off-by: dspury <demitriusspury@gmail.com>
(cherry picked from commit aa4d335)
(cherry picked from commit a3cfcad)
Signed-off-by: dspury <demitriusspury@gmail.com>
Signed-off-by: dspury <demitriusspury@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant