Skip to content

feat(desktop): connect self-hosted agents - #4006

Draft
dspury wants to merge 3 commits into
block:mainfrom
Lunar-Park:pr/connected-agent-records
Draft

feat(desktop): connect self-hosted agents#4006
dspury wants to merge 3 commits into
block:mainfrom
Lunar-Park:pr/connected-agent-records

Conversation

@dspury

@dspury dspury commented Jul 31, 2026

Copy link
Copy Markdown

Stacked on #3990. The incremental review for this PR is two commits:

  • feat(desktop): connect self-hosted agents Buzz does not own
  • refactor(desktop): split initial window helpers

Summary

  • add connected-agent storage separate from Buzz-managed agent records
  • add read-only/connect Tauri commands for recording self-hosted agent identities by host, pubkey, name, and observed harness
  • add the desktop Connect agent dialog and Connected agents section
  • validate pubkey input so pasted secret keys are rejected and never echoed
  • keep connected agents out of lifecycle/spawn fields by using a dedicated record type
  • move existing initial-window helpers out of lib.rs so new command registrations stay within the desktop file-size guard

Why

Some agents already run outside the Buzz desktop process. Buzz should be able to remember and display those identities without taking custody of their keys, command, process lifecycle, or environment.

The additional Tauri commands also pushed the app entrypoint across its file-size limit. The initial-window helpers now live in a focused module with no behavior change.

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
  • cd desktop && node --import ./test-loader.mjs --experimental-strip-types --test src/features/agents/ui/connectAgentIntent.test.mjs
  • git diff --check origin/pr/remote-harness-discovery..HEAD

dspury added 2 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>
@murdawkmedia

Copy link
Copy Markdown

I reviewed the connected-agent path at the current head (the latest initial_window extraction does not affect these findings). The separate public-only store and lack of local lifecycle controls are strong boundaries, but I think two issues need resolving before the UI calls this a connected agent:

  1. The host is not bound to the supplied identity. connect_remote_agent accepts any valid pubkey for any configured SSH alias, and the UI intentionally allows submission after a failed/absent probe. Reachability plus an observed harness binary does not prove that the holder of that pubkey runs on that host. I would model this as an unverified endpoint bookmark unless a host-executed signed challenge or matching self-signed relay assertion establishes the binding. Tests should cover mismatched host/pubkey and stale/replayed proof.

  2. The SSH probe can disagree with OpenSSH and runs before explicit consent. The partial parser ignores Include, Match, global/default precedence, inline-comment and first-obtained-value semantics, then passes its parsed port with -p, overriding the effective configuration. Prefer passing only a validated alias and deriving facts from ssh -G alias. Opening the dialog selects the first host and starts a POSIX-shell probe automatically; that invokes remote CLIs with --version before an explicit Probe action and does not support Windows SSH targets. Please make probing explicit and platform-aware.

The product-level split I would preserve is: community agents come from signed relay identity/membership facts; this file supplies only optional local endpoint annotations keyed by pubkey. That retains the good secret/lifecycle isolation here without making SSH a prerequisite for visibility or implying an unproven host↔identity relationship.

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.

2 participants