feat(desktop): name the computer an agent lives on - #6259
Conversation
An owner signs into Buzz on several computers. Persona definitions (kind:30175) sync and insert on arrival, but managed-agent records (kind:30177) are a deliberate no-op on inbound no-match, because they carry device-local secrets that must never ride the relay. So every computer that receives a persona mints its own keypair: one name, N pubkeys, N computers. Mentions then route by pubkey. The `p`-tag match in buzz-acp reaches exactly one of the N -- whichever pubkey the sending client happened to resolve -- so a mention aimed at a sleeping computer dies in silence, and nothing in the UI ever said which computer an agent lived on. This adds the missing noun: - `device_identity`: a stable per-install id and a human label, minted once, persisted 0600 to `<app-data>/agents/device.json`. The id is an opaque uuid v4, never derived from hardware. The label is seeded from the OS host name -- which routinely contains a real person's name -- so it is user-editable and sanitized before it is published. - The label rides the agent's own kind:30177 projection, and is accepted on the way back in only when the event author matches the owner the agent's NIP-OA profile cryptographically declares. A peer cannot stamp a label onto someone else's agent. - The mention dropdown names the computer, and a mention that resolves to another computer's keypair says so instead of dead-ending. - A settings card to rename this computer. Deliberately out of scope: moving agent secrets between computers, and making the relay-side single-connection exclusion real. Both are Stage 1 and Stage 2 of docs/agent-identity-sync.md on branch design/tailnet-agent-mesh; this is that document's Stage 0. Signed-off-by: Michael Feth <michael@jira-flow.com>
EvidenceTwo agents share the display name Before —
|
ScaleLeanChris
left a comment
There was a problem hiding this comment.
Tested this on macOS at 23c95ff64. The focused implementation works as written:
agentDeviceLabel.test.mjs: 4 passed- focused Tauri device identity tests: 13 passed
- focused event projection and verified-directory tests: 3 passed
tsc --noEmit: passed- E2E build: passed
- the three new Playwright mention flows: 3 passed
I found two issues that seem important to resolve before merge:
-
Provider-backed agents get the wrong location.
managed_agents/agent_events.rsstamps the current Desktop device on everyManagedAgentRecord, includingBackendKind::Provider. The UI then says the agent is on that Desktop and that only that device can reply. For an agent deployed to Kubernetes from a MacBook, the body can stay online after the MacBook closes, so this guidance is false and conflicts with the remote-agent model. I think the device stamp and "only that device" copy need to apply only to local backends, or the protocol needs to distinguish key custody/controller from execution location. -
Stored and relay-provided metadata bypasses the label contract.
load_or_create_ataccepts any syntactically validDeviceIdentitywithout revalidating the UUID or label. The inbound kind:30177 path also forwards owner-signeddevice_idanddevice_labeldirectly to the mention row and notice. That bypasses the 32-character/control-character checks, and format characters such as bidi controls or zero-width characters are not rejected bychar::is_control. Please validate the complete stored identity after deserialization and validate the untrusted relay projection before it reaches UI. Reusing the existing shared invisible/bidi text policy would avoid a second security rule.
Two smaller propagation gaps are worth handling in the same design pass:
- Renaming reconciles only
active_retention_scope, so other configured communities keep the old public label until later activation even though the command says it republishes every local agent. - The first-run OS hostname can be published before the owner sees the warning or has a chance to edit it. Because the PR correctly notes that hostnames often contain a real name, an opaque default or explicit pre-publication choice would be safer.
The core collision UI and send behavior look good. This is useful Stage 0 work once the location and trust boundaries above are made accurate.
…ry label Review follow-up. Four issues, all confirmed against the code first. **A provider-backed agent was given the wrong location.** `agent_event_content` stamped the current device onto every record without consulting `record.backend`. A `Provider` body runs elsewhere -- deployed to a cluster from a laptop -- and stays online after that laptop sleeps, so the mention UI told the user "only that device can reply" about a machine that is not where the agent runs. The stamp is now gated on `BackendKind::Local`. That alone closes the false guidance: `describeUnrunnableMention` already returns `null` when no `device_id` is published, so such an agent now produces no notice at all rather than a corrected one. Distinguishing key *custody* from *execution* is a protocol change, not a second stamp, and is left as a named future-work note. **The label contract was bypassed on stored and inbound data.** Validation ran only when a label was typed. Three holes: - `load_or_create_at` returned whatever deserialized. A hand-edited `device.json` was published unchecked. It now validates the whole identity -- id shape and label -- and an invalid file takes the path a corrupt one already did: preserved as `device.json.corrupt`, replaced, never failing the caller. - The inbound kind:30177 projection forwarded `device_id`/`device_label` verbatim. Owner authentication proves authorship, not well-formedness; a sibling device on an older or tampered build can sign anything. Each field is now validated independently and degrades to `None`, so a bad label never hides an otherwise reachable agent. - `char::is_control` is category `Cc` only, so zero-width (U+200B) and bidi overrides (U+202E) passed through. Rather than a second rule, the label now goes through `validate_device_label`, reusing the visible-text policy that already guards agent definition text -- whose own doc comment asks for validation "at every local, inbound, and publication boundary". An over-long label is now refused rather than truncated: publishing something other than what the owner typed is the worse failure. **Rename claimed more than it did.** It reconciles only the active retention scope. Republishing every scope needs owner keys for communities that are not applied -- identity handling, not this command -- so the contract is now stated truthfully instead: immediate for the active community, eventual elsewhere via `run_event_sync` on activation. **The OS host name could be published before the owner saw the warning.** First-run labels are now opaque (`device-<8 hex>`) and `mint_identity` no longer reads the host name. It is offered as an explicit opt-in in the settings card via `get_device_name_suggestion`, so nothing derived from it reaches a relay until the owner applies it. The new tests share a process-global cache, so the seam is an RAII guard holding a mutex and restoring on drop -- deliberately not repeating the pattern that makes `claude_spawn_uses_the_probed_cli_executable` flaky. Signed-off-by: Michael Feth <michael@jira-flow.com>
|
Thanks — this was a genuinely useful review, and all four held up when I traced them. Fixed in the follow-up commit. 1. Provider-backed agents got the wrong location. Confirmed: That turned out to close the false-guidance path completely, because I did not take the second option you offered (distinguishing key custody from execution location). That is a protocol change, not a stamp change, and it belongs with the Stage 1/2 work; I left it as a named future-work note at the branch instead of half-building it. 2. Stored and relay metadata bypassed the label contract. All three holes confirmed:
One deliberate behavior change while I was there: an over-long label is now rejected rather than truncated. Silently publishing something other than what the owner typed seemed worse than an error. A small piece of corroboration: writing the test for this tripped rustc's own 3. Rename propagation. Confirmed — it only reconciled 4. Hostname before consent. Agreed, and this is the one I'd have been most unhappy to ship. The first-run label is now opaque — Verification (Windows, after rebase onto current
The known pre-existing |
|
Overlap notice for reviewers: #6337 (opened a day after this one) solves the same problem — disambiguating two same-named owned agents in mention autocomplete — and edits the same lines of That is a conflict for whichever lands second, and a duplicated disambiguator in the suggestion row if both land as-is. I have proposed the merge on #6337: keep its narrower presentation and collision-only trigger, and let the device label fill the Flagging it here so this is not reviewed in isolation. |


The problem
An owner signs into Buzz on several computers. They create an agent on one. It
appears on all of them, wearing the same name. They mention it, and often
nothing answers — while a different copy of the same agent, on a computer that
happens to be awake, answers a different mention minutes later. Nothing in the
UI ever says which computer an agent actually lives on, whether it is running,
or whether it is configured.
Why it happens
Persona definitions (kind:30175) sync between an owner's computers and insert on
arrival. Managed-agent records (kind:30177) deliberately do not — inbound
no-match is a hard no-op, because a managed agent carries device-local secrets
and "an agent that does not already exist locally has no secret key to run
with" (
desktop/src-tauri/src/commands/personas/inbound.rs:553-557).So each computer that receives a persona and instantiates it mints its own
keypair. One name, N pubkeys, N computers.
Turns then route by pubkey: the
p-tag match incrates/buzz-acp/src/filter.rs:390-395, on by default because--subscribedefaults to
mentions. A mention therefore reaches exactly one of the N —whichever pubkey the sending client happened to resolve. If that one is asleep,
the mention dies silently.
Evidence from a real fleet
Measured from one owner's local kind:30177 retention stores (aggregate only; the
stores are keyed per
(owner pubkey, relay URL)):One name,
Bumble, holds 7 distinct pubkeys in a single scope.In one channel, the member roster is 1 human and 26 agents; 23 of those resolve
to managed-agent directory entries carrying only 13 distinct names — four
Winnies, three Airys, three Todds, and so on.
The dead mention is directly observable in the harness logs: one Winnie logged
steer-acks in that channel on 2026-08-18, while a second Winnie and a second
Ernie last touched the same channel on 2026-08-15 only to log
subscribed to channel …and then go quiet.desktopInstanceId, the only per-process discriminator on a runtime receipt, isprovably not a machine identity — all 40 receipts on the machine carry the single
value
xyz.block.buzz.app, and its own doc comment says it exists to separatetwo Buzz apps on one machine, never two machines.
What this PR does
device_identity— a stable per-install id and a human label, minted onceand persisted
0600to<app-data>/agents/device.json. The id is an opaqueuuid v4, never derived from hardware. A corrupt file is preserved as
device.json.corruptand replaced rather than failing the caller.contains a real person's name, and it is published in a world-readable
kind:30177 event — so it is user-editable, length-capped, and refuses control
characters.
only when the kind:30177 author matches the owner the agent's NIP-OA profile
cryptographically declares (
nostr_convert/agent_directory.rs:115). A peercannot stamp a label onto someone else's agent.
another computer's keypair says so instead of dead-ending in silence.
It deliberately does not repurpose
desktop_instance_id, whose ownershipcheck must keep working for the two-Buzzs-on-one-machine case.
What this PR does not do
Moving agent secrets between computers, and making the relay-side
single-connection exclusion real, are Stage 1 and Stage 2 of
docs/agent-identity-sync.mdon branchdesign/tailnet-agent-mesh. This is thatdocument's Stage 0, which it recommends building first precisely because it
needs no decision about secrets. Note that unifying identity onto a
default-off, per-process exclusion would be strictly worse than today — see §3
of that document.
Note for reviewers: overlapping open PRs
This branch is rebased on
mainas of the 8 commits through #5874, whichincluded #6224 (
bound remote agent mention authorization). That PR and this oneboth add to
desktop/src/shared/api/tauriRelayAgents.tsand the e2e bridge; therebase conflict was resolved by keeping both entry points and having
revalidateRelayAgentsreuse this PR'sfromRawRelayAgentnormalizer, so thedevice fields flow through both paths.
Still-open PRs that touch overlapping files, none of which this PR depends on:
#6077 (7 files), #6126 (17 files), #6013, #6037. This PR adds no
mention-receipt work (kind 44102) and no agent-grouping changes — it is additive
to both — so it can merge in any order; whichever lands second resolves the
textual overlap.
Review follow-up (commit 2)
ScaleLeanChristested on macOS and raised four issues; all four were confirmedagainst the code and fixed in
fix(desktop): bound the device stamp to local agents and validate every label:gated on
BackendKind::Local, so an agent whose body runs in a clusterpublishes no device — and, because
describeUnrunnableMentionalreadyreturns
nullwithout adevice_id, produces no "only that device canreply" notice at all.
load_or_create_at)and inbound (
relay_agent_from_managed_policy) values are now checked, notjust typed ones — reusing
definition_validation's visible-text policy,which catches the
Cfzero-width and bidi codepointschar::is_controlmisses. Owner authentication proves authorship, not well-formedness.
eventual elsewhere on activation.
opaque
device-<8 hex>; the host name is an explicit opt-in.Verification
Run on Windows against this branch, after the rebase:
cargo check --manifest-path desktop/src-tauri/Cargo.toml --all-targetscargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warningscargo test --manifest-path desktop/src-tauri/Cargo.tomlpnpm --dir desktop exec tsc --noEmitpnpm --dir desktop testpnpm --dir desktop exec biome check srcpnpm --dir desktop check:px-textjust file-size-checkThe one Rust failure is
claude_spawn_uses_the_probed_cli_executable, whichpasses in isolation (
--lib <name>: 1 passed). It mutates the process-globalPATHviastd::env::set_varand races the other tests in the same process. Itis pre-existing and untouched by this branch — the diff contains no occurrence of
CLAUDE_CODE_EXECUTABLEand noruntime.rschange. The new device testsdeliberately avoid that pattern: their cache seam is an RAII guard that holds a
mutex and restores on drop.
Clippy reports pre-existing findings in
crates/buzz-terminal, which this PRdoes not touch.
just test(relay integration, needs Postgres + Redis) was not run — thischange touches no relay crate.