Skip to content

fix(desktop): recognize agents without NIP-OA in Huddles - #8544

Open
loganj wants to merge 3 commits into
mainfrom
larry/huddle-own-agents
Open

loganj wants to merge 3 commits into
mainfrom
larry/huddle-own-agents

Conversation

@loganj

@loganj loganj commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

🤖

Summary

In a Huddle (a voice call in a channel), Buzz Desktop decides which participants are agents. It uses that answer to choose whose text it speaks aloud, which agents can publish voice, and when the last person leaves and the Huddle ends.

Desktop currently treats a participant as an agent only when its channel role is bot. #8494 changes the relay so that it gives bot only to agents with a verified owner attestation (NIP-OA). After that change, agents without an attestation join as ordinary members, and Desktop treats them as people:

  • their messages are not spoken,
  • their voice publisher is refused,
  • the Huddle stays open after the last person leaves while such an agent is still connected.

With this PR, Desktop also counts a member as an agent when a trusted person has published a managed-agent record for it. Desktop already publishes such a record for every agent it runs, on every machine. Trusted people are:

  • you, so your own agents count as agents on all of your machines;
  • the Huddle's owner, usually the person who started it, so the agents they added count as agents for everyone who joins.

Some agents without an attestation still count as people: an agent added by someone who does not own the Huddle, as seen from any machine other than that person's. The only way to recognize those agents is an owner attestation. This PR does not sign one.

Details

  • huddle/roster.rs (fetch_huddle_roster) is now the only place that decides agent or person for Huddles. A member is an agent if its role is bot, or if the current user or a channel owner published a kind:30177 managed-agent record whose d tag is that member's key. Records from other authors are ignored. Records whose signature does not verify are ignored.
  • All Huddle checks use the roster: voice-publisher authorization, get_huddle_agent_pubkeys (spoken-text gating and the participant list), the periodic roster refresh, adding an agent, cleanup at the end of the Huddle, and the last-person check.
  • Trusting the owner's records is a compatibility policy for Huddles, not proof of identity. A record creates no owner. It lets the channel owner mark a member as an agent for every Huddle behavior that reads the roster: spoken text, voice publishing, remote transcription and turn-taking (both skip agent audio), cleanup, and the last-person check. Before feat(relay): store the bot role only for keys with a recorded agent owner #8494 the client chose the role, so an owner could already do this by labeling a member bot.
  • If the managed-agent records cannot be read, Desktop does not drop agents that have the bot role. Spoken text, voice publishing, adding agents and cleanup keep working for those agents. The Huddle does not end automatically, because an agent without an attestation could be counted as a person. When you join, and on each periodic refresh, Desktop adds the bot agents to its stored list and keeps stored agents that are still members.

Related issue

Follow-up to #8494 (the relay stores bot only for attested agents). No existing issue found.

Testing

  • New helper-level unit tests in huddle/roster.rs. They do not run the relay query, the pipeline writers or the leave path.
    • the bot role, the user's own record and the owner's record each mark a member as an agent;
    • records from other members are ignored;
    • a record with a forged signature is ignored;
    • when the record read fails, bot agents stay agents, the Huddle cannot end automatically, and the next successful read restores the full roster.
    • when the record read fails, a joiner with no stored agents still stores the bot agents, an agent known only by its record stays stored, and a stored agent that left drops out.
  • New tests in huddle/roster_hydration_tests.rs run the production roster reads in the post-connect and hotstart paths against a fake relay whose managed-agent record read fails. Post-connect stores the bot agent; the hotstart refresh also keeps a still-member agent known from an earlier record and drops one that left; the same incomplete roster gives no human count for the last-person check. Both tests fail when the two call sites accept only complete rosters.
  • cargo test --lib huddle:: (Huddle tests only), cargo clippy --tests -D warnings and cargo fmt --check pass in desktop/src-tauri.
  • Not yet tested in a live Huddle.

Huddles decided "is this an agent" from the channel role `bot`. The
relay now stores `bot` only for keys with a recorded NIP-OA owner
(#8494), so an agent without an owner attestation joins as `member`.
Desktop then treated it as a person: its text was not spoken, its voice
publisher was refused, and the Huddle did not end while it was still
connected.

Add one roster owner, `huddle::roster::fetch_huddle_roster`. A member
is an agent when it has the `bot` role or when a trusted author
published a kind:30177 managed-agent record for it. Desktop publishes
that record for every agent it manages, from every machine. Trusted
authors are the current user and the Huddle channel's owners; a record
from anyone else is only a claim. All Huddle agent and human checks
now use this roster.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
loganj added a commit that referenced this pull request Oct 22, 2026
Mobile Huddles decided "is this an agent" from the channel role `bot`.
The relay now stores `bot` only for keys with a recorded NIP-OA owner
(#8494), so an agent without an owner attestation joins as `member`.
Mobile then left it out of the participant list after joining, labeled
it as a person, did not show its "preparing a response" state, and
counted it as a person when deciding whether leaving ends the Huddle.

Add one Huddle agent owner, `huddle_agents_provider.dart`. A member is
an agent when it has the `bot` role or when a trusted author published
a validly signed kind:30177 managed-agent record for it. Trusted authors
are the current user and the Huddle channel's owners; a record from
anyone else is only a claim. This is the same rule as the Desktop fix
(#8544). Signature checks run off the UI isolate.

If the records are still loading or cannot be read, the `bot` role still
works, and the last-person count fails so leave keeps the Huddle open.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
loganj added a commit that referenced this pull request Oct 22, 2026
Mobile Huddles decided "is this an agent" from the channel role `bot`.
The relay now stores `bot` only for keys with a recorded NIP-OA owner
(#8494), so an agent without an owner attestation joins as `member`.
Mobile then left it out of the participant list after joining, labeled
it as a person, did not show its "preparing a response" state, and
counted it as a person when deciding whether leaving ends the Huddle.

Add one Huddle agent owner, `huddle_agents_provider.dart`. A member is
an agent when it has the `bot` role or when a trusted author published
a validly signed kind:30177 managed-agent record for it. Trusted authors
are the current user and the Huddle channel's owners; a record from
anyone else is only a claim. This is the same rule as the Desktop fix
(#8544). Signature checks run off the UI isolate.

If the records are still loading or cannot be read, the `bot` role still
works, and the last-person count fails so leave keeps the Huddle open.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
@loganj
loganj marked this pull request as ready for review October 22, 2026 17:47
@loganj
loganj requested a review from a team as a code owner October 22, 2026 17:47
@loganj
loganj deployed to codex-review October 22, 2026 17:48 — with GitHub Actions Active
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated, security-focused review generated by Codex.
Use it as a supplement to human review; false positives are possible.

Scope

  • Exact PR diff: 016310d3cc44688779a7d517a13d9cc8024ddfa8...d58012d32f52c9b5f2460d2d462b14d84fe33626
  • Model: gpt-6.1-sol

💡 Click "edited" above to see earlier reviews for this PR.


Review Summary

Overall Risk: NONE

No concrete security, correctness, or reliability regressions found in the authorized PR range.

Findings

No concrete security, correctness, or reliability findings were identified.

Notes

  • Review was limited to read-only source and history inspection. Builds and tests were not executed.

Generated by Codex Security Review |
Requested by: @loganj |
Workflow run

jedwards27

This comment was marked as resolved.

@jedwards27

This comment has been minimized.

When the kind:30177 managed-agent records could not be read, the
roster still listed the members proven by role `bot`, but the
post-connect and periodic refresh paths dropped the whole incomplete
roster. A joiner then had no stored agents: its agents' text was not
spoken and their audio was treated as a person's.

`HuddleRoster::apply_agents` now owns updating the stored list. A
complete roster replaces it. An incomplete roster adds the `bot`
agents and keeps stored agents that are still members. An incomplete
roster still cannot end the Huddle.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Oct 22, 2026
@loganj
loganj deployed to codex-review October 22, 2026 23:58 — with GitHub Actions Active
@loganj

This comment has been minimized.

@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Oct 23, 2026
jedwards27

This comment was marked as resolved.

The earlier regression test called HuddleRoster::apply_agents directly,
so it stayed green if post-connect setup or the hotstart refresh stopped
using it. Move the two roster hydration steps into functions and test
them against a fake relay whose managed-agent record read fails: the
post-connect read keeps the role-proven bot agent, the hotstart refresh
also keeps a still-member agent and drops a departed one, and the same
incomplete roster gives no human count for the last-person check. Both
tests fail when the call sites accept only complete rosters.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
@loganj

This comment has been minimized.

@loganj
loganj deployed to codex-review October 23, 2026 17:21 — with GitHub Actions Active
@github-actions github-actions Bot added codex-security-review-current The posted Codex security review matches its recorded range. and removed codex-security-review-current The posted Codex security review matches its recorded range. labels Oct 23, 2026

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 :bot: Jude's code review agent

Verdict: REQUEST_CHANGES

The source behavior at d58012d32f52c9b5f2460d2d462b14d84fe33626 looks fixed, but the replacement regression still does not bind the production callers this PR needs to protect.

Blocking finding

[P1] Production-caller linkage is still test-unbound.

The new regression tests prove the extracted helper bodies, but not the top-level production wiring:

  • desktop/src-tauri/src/huddle/roster_hydration_tests.rs:98 calls hydrate_huddle_roster(...) directly.
  • desktop/src-tauri/src/huddle/roster_hydration_tests.rs:118 calls refresh_huddle_roster(...) directly.
  • The production seams are check_pipeline_hotstart -> refresh_huddle_roster at desktop/src-tauri/src/huddle/pipeline.rs:119-120 and post_connect_setup -> hydrate_huddle_roster at desktop/src-tauri/src/huddle/pipeline.rs:241-242.

I mutation-checked both seams at this exact head:

  1. Replaced the post_connect_setup call to hydrate_huddle_roster(...) with a no-op.
  2. Replaced the check_pipeline_hotstart call to refresh_huddle_roster(...) with a no-op.

In both mutants, this command still passed 2/2:

cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib huddle::pipeline::roster_hydration_tests -- --nocapture

That means a future deletion/bypass of either actual production invocation can go green while joiners or hotstart stop hydrating role-proven bot agents when kind:30177 managed-agent reads fail. That is the seam this PR was meant to lock down; green helper tests are not enough armor. The dungeon eats unguarded seams first.

Author action: add falsifiable production-caller regression coverage such that:

  1. Removing/bypassing post_connect_setup -> hydrate_huddle_roster makes a behavioral assertion fail.
  2. Removing/bypassing check_pipeline_hotstart -> refresh_huddle_roster makes a behavioral assertion fail when the Huddle is active and refresh is due.

The existing helper tests can stay, but the missing bite is that deleting pipeline.rs:119-120 or pipeline.rs:241-242 must fail a test.

Validation notes

  • Confirmed live GitHub headRefOid matched d58012d32f52c9b5f2460d2d462b14d84fe33626 before review.
  • Clean control test passed at that head: cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib huddle::pipeline::roster_hydration_tests -- --nocapture (2 passed).
  • The two mutation runs above also passed, which is the blocker.
  • Relevant Desktop/Rust/security/DCO CI checks were green on this head; that is useful evidence, but it does not cover the production-caller seam.

Confidence gaps, not author blockers: I did not run a live multi-user native Huddle/audio session, and I did not run full local just ci.

@jedwards27

Copy link
Copy Markdown
Contributor

🤖 :bot: Jude’s code review agent — integrated addendum to the existing REQUEST_CHANGES review at exact head d58012d32f52c9b5f2460d2d462b14d84fe33626.

The terminal verdict is unchanged, and I am not submitting a duplicate review.

Reconciled state: the source behavior is fixed at this head. The production source currently wires check_pipeline_hotstart -> refresh_huddle_roster and post_connect_setup -> hydrate_huddle_roster, and those helpers apply incomplete rosters into HuddleState.agent_pubkeys while humans_remaining() still refuses to make a last-person decision on incomplete managed-agent reads.

Remaining author-actionable defect: the new regression still does not bind the outer production entrypoints. The added tests call hydrate_huddle_roster(...) and refresh_huddle_roster(...) directly, so deleting or bypassing the calls from post_connect_setup and check_pipeline_hotstart can leave cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib huddle::pipeline::roster_hydration_tests -- --nocapture green. That means the exact seam that failed in the prior round is still protected one layer too low.

Author action remains: add a falsifiable production-caller regression, or an equivalent production-entrypoint spy, such that removing/bypassing post_connect_setup -> hydrate_huddle_roster fails behaviorally and removing/bypassing check_pipeline_hotstart -> refresh_huddle_roster fails behaviorally when the Huddle is active and refresh is due. Keep the helper tests; they are useful, just not sufficient by themselves.

Integrated evidence: product/UI review found no separate unresolved user-visible or policy defect: trusted agent/person classification, incomplete-roster fail-closed last-person behavior, visible participant handling, spoken-text gating, voice-publisher authorization, playout/floor behavior, and cleanup all line up with the intended contract in source. Systems/integration review confirmed the production source is currently correct but mutation-checked the missing caller binding described above. Exact-head CI relevant to Desktop/Rust/security/DCO was green. Live/native multi-user Huddle audio remains a confidence gap, not an additional author blocker.

@Chessing234 Chessing234 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trusted_record_authors always includes the viewer. a member who is not an owner can sign a kind 30177 and the roster treats that pubkey as an agent, including the count that decides whether leaving ends the huddle.

This branch was successfully deployed

1 active deployment
codex-review — d58012d3 Deployed Oct 23, 2026 by loganj via Run Codex Security Review #9663
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex-security-review-current The posted Codex security review matches its recorded range.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants