fix(desktop): publish kind:10100 agent profile directory records - #3708
Open
4dlt wants to merge 1 commit into
Open
fix(desktop): publish kind:10100 agent profile directory records#37084dlt wants to merge 1 commit into
4dlt wants to merge 1 commit into
Conversation
The desktop reads a kind:10100 agent-profile directory to decide which agents a client may mention, but nothing has ever written to it. The directory is empty in every deployment, so a managed agent is only ever mentionable from the machine that runs it, and every eligibility rule that consults the directory is a no-op in practice. Reported in block#3277. Publish the record when a managed agent starts, signed by the agent's own key, and refresh it as offline when the agent stops. kind:10100 is replaceable and keyed by pubkey, so a refresh is a plain re-publish with no tombstone. Content is an explicit allowlist, because the relay serves this kind to any authenticated member: display name, agent type, liveness, respond_to, respond_to_allowlist, channel ids, channel names, capabilities and channel_add_policy. The agent's secret key, its NIP-OA auth tag, its environment variables and its runtime configuration are deliberately absent, pinned by tests. Three things worth calling out for review: Published from the runtime start path, not profile reconciliation. A managed agent can start four ways. `reconcile_agent_profile` is reached from the UI start command and from boot restore, but the renderer drives `start_managed_agent_runtime`, which goes `start_pair` -> `spawn_agent_child` and reconciles nothing. Publishing only from reconciliation therefore publishes nothing on the common path. `start_pair` covers every path. channel_add_policy is read back and re-sent verbatim. The relay's side effect for this kind writes the field straight into `users.channel_add_policy`, so sending a fixed value would reset a deliberate `owner_only` or `nobody` to that value on every agent start. The prior record's policy is preserved, falling back to the schema default only when no record exists. The directory publish is not gated on `agentManagedProfiles`. That experiment decides whether the desktop or the agent owns kind:0 profile metadata. The directory record is discovery, has no other publisher in the product, and gating it there leaves remote agents undiscoverable for exactly the users who opted in. kind:0 handling still re-checks the flag and is unchanged. Channel names come from the kind:39000 `name` tag rather than the event content, which is empty on those events. No client-visible behaviour changes: this only publishes a record the app already reads. The mention-eligibility rule is deliberately left for a follow-up, since block#3277 has an open product question about whether a channel-member agent with no local runtime should stay hidden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Alvaro de la Torre <a.dltorreruiz@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The desktop reads a kind:10100 agent-profile directory to decide which agents a client may mention, but nothing has ever written to it. The directory is empty in every deployment, so a managed agent is only mentionable from the machine that runs it, and every eligibility rule that consults the directory is a no-op in practice.
This PR adds the publisher. It publishes an agent's directory record when the agent starts, signed by the agent's own key, and refreshes it as
offlinewhen the agent stops.Fixes half of #3277. No client-visible behaviour changes: this only publishes a record the app already reads.
Why this is separate from the eligibility change
#3277 has an open product question that the mention rule depends on:
The publisher does not depend on that answer. It keeps
mirahidden either way, and it unblocks every directory-based approach in the queue regardless of which rule is chosen. I have the client-side rule working too, but it seemed better to land the uncontested half first rather than add another competing eligibility PR while that question is open. Happy to open the follow-up once the rule is settled.What is published
An explicit allowlist, because the relay serves this kind to any authenticated member:
name,agent_type,status,respond_to,respond_to_allowlist,channel_ids,channels,capabilities,channel_add_policyDeliberately absent, each pinned by a test: the agent's nsec, its NIP-OA auth tag, its environment variables, and its runtime/harness configuration.
The record is signed by the agent's own key rather than the owner's.
agents_from_eventsoverwrites the content pubkey with the event author, so identity derives from the signature and one agent cannot publish an entry impersonating another. That is also why only the hosting machine can publish.Three decisions worth reviewing
Published from the runtime start path, not profile reconciliation. A managed agent can start four different ways.
reconcile_agent_profileis reached from the UI start command and from boot restore, but the renderer drivesstart_managed_agent_runtime, which callsstart_pairthenspawn_agent_childand reconciles nothing. Hooking only reconciliation publishes nothing on the common path, which is what I did first and it silently produced no records.start_paircovers every path.channel_add_policyis read back and re-sent verbatim. The relay's side effect for this kind writes the field straight into the users table:Sending a fixed value would therefore reset a deliberate
owner_onlyornobodyto that value on every agent start. The prior record's policy is preserved, falling back to the schema default (anyone) only when there is no prior record, which is a no-op for a never-configured agent.The publish is not gated on
agentManagedProfiles. That experiment decides whether the desktop or the agent owns kind:0 profile metadata. The directory record is discovery, nothing else in the product publishes it, and gating it there leaves remote agents undiscoverable for exactly the users who opted in. kind:0 handling still re-checks the flag and is unchanged. Please push back if that guard was intended to cover discovery too.Testing
New unit tests in
desktop/src-tauri/src/commands/agents_tests.rs:respond_to_allowlistis carried whenrespond_toisallowlistofflinedtags of kind:39002 membership events, deduped and sorted, skipping malformed eventsnametag, falling back to the id when metadata is missingchannel_add_policyis preserved from a prior record, and falls back to the schema default when absent or malformedjust desktop-tauri-test,just desktop-tauri-clippyandjust desktop-tauri-fmt-checkpass.Verified at runtime on this branch, not just in tests
Built from this branch, deployed to a self-hosted relay running Desktop 0.5.2, and queried directly against the relay's event store.
Starting an agent that was stopped (so boot-restore could not have been the publisher) produced a fresh record, which is what confirms the new
start_pairhook actually executes rather than being dead code:{ "name": "Fizz", "agent_type": "buzz-agent", "status": "online", "respond_to": "anyone", "respond_to_allowlist": [], "channel_ids": ["<uuid>", "<general-uuid>", "<uuid>"], "channels": ["DM", "general", "Welcome"], "channel_add_policy": "anyone", "capabilities": [] }Stopping an agent produced a matching
"status":"offline"record two seconds before its restart produced a new"online"one, so both the start and stop paths are exercised.channel_add_policystayed"anyone"across four consecutive publishes, confirming the read-back does not clobber it under repeated start/stop cycles.With the companion client-side rule applied locally, a message mentioning a remote agent from a second machine then carries the agent's
ptag where previously it did not, and the harness acknowledges within one second. That client-side rule is not part of this PR.One question I would like a maintainer's view on
The record publishes private and DM channel ids.
channel_idsis built from every kind:39002 membership event tagging the agent, with no filtering, so an agent that is in DM channels publishes those channel UUIDs into a record the relay serves to any authenticated member. In my testing an agent's record listed two["private"]["hidden"]["t","dm"]channels alongside the public one.I did not filter them, for two reasons: the spec for this work asks for the channels the agent is in without qualification, and
relayAgentIsSharedWithUseruses channel overlap to decide invocability, so removing DM channels would break mentioning an agent from inside a DM (an explicit requirement in the linked issue).But it is a disclosure the field allowlist was otherwise careful to avoid, and it was not called out in the spec. If the preference is to exclude
private/hiddenchannels and accept that DM mentions need a different signal, that is a small change and I am happy to make it. Flagging rather than deciding, since it is a product call.Related and minor: because DM channels are all literally named
DM, thechannelsarray can contain repeated labels for distinct ids.channel_idsremains unique and is what eligibility consumes, so this is presentational only.Notes for reviewers
channelsfield currently carries names resolved from kind:39000; where a channel has no readable metadata it falls back to the channel id.channel_idsis the field eligibility logic uses, so this is cosmetic.capabilitiesis always empty: a managed agent record declares no capability set today, and the client already defaults the field when absent.