Skip to content

feat(desktop): name the computer an agent lives on - #6259

Open
mfethe1 wants to merge 1 commit into
block:mainfrom
mfethe1:feat/agent-device-identity
Open

feat(desktop): name the computer an agent lives on#6259
mfethe1 wants to merge 1 commit into
block:mainfrom
mfethe1:feat/agent-device-identity

Conversation

@mfethe1

@mfethe1 mfethe1 commented Aug 18, 2026

Copy link
Copy Markdown

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 in
crates/buzz-acp/src/filter.rs:390-395, on by default because --subscribe
defaults 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)):

Scope Distinct agent identities Distinct display names
owner A, relay 1 52 15
owner A, relay 2 22 10

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, is
provably 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 separate
two Buzz apps on one machine, never two machines.

What this PR does

  • device_identity — a stable per-install id and a human label, minted once
    and persisted 0600 to <app-data>/agents/device.json. The id is an opaque
    uuid v4, never derived from hardware. A corrupt file is preserved as
    device.json.corrupt and replaced rather than failing the caller.
  • Privacy on the label. It is seeded from the OS host name, which routinely
    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.
  • Owner-authenticated on the way back in. An inbound device label is accepted
    only when the kind:30177 author matches the owner the agent's NIP-OA profile
    cryptographically declares (nostr_convert/agent_directory.rs:115). 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 in silence.
  • A settings card to rename this computer.

It deliberately does not repurpose desktop_instance_id, whose ownership
check 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.md on branch design/tailnet-agent-mesh. This is that
document'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 main as of the 8 commits through #5874, which
included #6224 (bound remote agent mention authorization). That PR and this one
both add to desktop/src/shared/api/tauriRelayAgents.ts and the e2e bridge; the
rebase conflict was resolved by keeping both entry points and having
revalidateRelayAgents reuse this PR's fromRawRelayAgent normalizer, so the
device 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.

Verification

Run on Windows against this branch, after the rebase:

Gate Result
cargo check --manifest-path desktop/src-tauri/Cargo.toml --all-targets clean
cargo test --manifest-path desktop/src-tauri/Cargo.toml 2406 passed, 1 failed
pnpm --dir desktop exec tsc --noEmit clean
pnpm --dir desktop test 4996 passed, 0 failed
pnpm --dir desktop check:px-text clean
just file-size-check 6 passed, 0 failed
Playwright smoke, device tests 3 passed

The one Rust failure is claude_spawn_uses_the_probed_cli_executable, which
passes in isolation (--lib <name>: 1 passed). It mutates the process-global
PATH via std::env::set_var and races the other tests in the same process. It
is pre-existing and untouched by this branch — the diff contains no occurrence of
CLAUDE_CODE_EXECUTABLE and no runtime.rs change.

Likewise channels.spec.ts › shows cached profile labels failed once in a
combined Playwright run and then passed 3/3 in isolation; it asserts a cached
user-profile label with a 1s timeout and is unrelated to this change.

just test (relay integration, needs Postgres + Redis) was not run — this
change touches no relay crate.

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>
@mfethe1
mfethe1 requested a review from a team as a code owner August 18, 2026 20:27
@mfethe1

mfethe1 commented Aug 18, 2026

Copy link
Copy Markdown
Author

Evidence

Two agents share the display name alice on two computers of one account. Both
captures are the same surface — /#general → composer, typing @alice — at
the same viewport: one built from origin/main, one from this branch. The base
worktree was seeded with the identical second alice so the only difference on
screen is the one this PR introduces.

Before — origin/main

Two rows, same name, separable only by a truncated npub. Nothing says which
computer holds the secret, so nothing says which one can answer.

After — this branch

Each row names its computer: on this device for the one whose keypair is local,
on mfeth-win for the twin that lives elsewhere. The collision npub is
deliberately unchanged — it is the impersonation guard, and the device line sits
beside it rather than replacing it.

When a name is unique and the agent is local, no device line renders at all — the
single-computer case stays free of noise. That case is covered by
mention dropdown stays silent about the device when a name is unique in
desktop/tests/e2e/mentions.spec.ts.

Images are pinned to an immutable commit SHA on a fork branch, per
CONTRIBUTING's guidance that relay media URLs fail through GitHub's camo proxy.

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