Skip to content

feat(relay): agents cannot write in DMs except with their owner - #9371

Merged
joahg merged 5 commits into
mainfrom
joah/relay-agents-no-dm
Nov 12, 2026
Merged

joahg merged 5 commits into
mainfrom
joah/relay-agents-no-dm

Conversation

@joahg

@joahg joahg commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

Summary

Agents stop writing in direct messages, except a DM with only their owner. This is the relay-side backstop for block/buzz-app#3261, which takes away the app agents' DM tools. An agent holding its own key could still sign DM events directly, and only the relay can stop that.

A key counts as an agent when it has a stored owner (users.agent_owner_pubkey, written when the agent authenticates with a NIP-OA owner attestation). For those keys:

  • Writes into a DM channel are refused with restricted: agents cannot post in direct messages; start a Solo chat that includes your owner instead. The check sits in check_channel_membership for ChannelWrite::Post, so it covers stream messages, reactions, deletions, typing and other channel ephemerals, artifact moves and edit validation. Non-DM channels skip it at no extra cost.
  • Workflow send_message (which posts as the workflow owner and checks may_post directly) applies the same rule before persisting, so an agent can't route a DM post through a workflow.
  • Put-user (9000) and edit-metadata (9002) skip the generic gate, so they get their own check in a DM: an agent never adds DM members with 9000 (an addition to an owner-only DM always widens it), and edits topic or purpose only in its owner-only DM (the edit posts a system message with the text).
  • Huddle admission checks may_post directly, so it applies the same rule to the huddle channel and its lifecycle parent.
  • DM open (41010) and DM add-member (41011) are refused unless the resulting participant set is exactly the agent and its owner.
  • Exemption: a DM whose live members are exactly the agent and its owner stays open (for example, a personal assistant's home chat).
  • Lookup failures fail closed. Confirmed agent owners are cached per community (agent_owner_cache, 5 min TTL). The link is first-write-wins and never changes once set. A missing link is never cached, because another relay process may write it later. link_agent_owner primes the cache.

Humans are unaffected: they can still open DMs that include an agent and post in them. The agent answers elsewhere.

Not covered:

  • A key that never presented an owner attestation has no stored owner and looks human.
  • The huddle join commit re-check (4e) doesn't repeat the DM rule; the pre-join admission check enforces it.
  • Agents also can't delete their own earlier messages in a non-owner DM via kind 5, since deletions pass the same Post gate.

Related issue

Companion to block/buzz-app#3261. AGNTOPS-780.

Testing

New Postgres ingest tests (handlers::ingest::postgres_tests::agent_*), run locally:

  • An agent is refused in a 1:1 DM with someone else and in a group DM with its owner; the humans in both still post.
  • An agent posts in a DM with only its owner.
  • An agent can open a DM only with its owner; a human can still open a DM with an agent.
  • An agent can't add members to its owner DM, through 41011 or 9000.
  • An agent can't set the topic in a DM with a stranger.
  • An agent is refused admission to a huddle in a DM with a stranger and admitted in its owner-only DM; a human still joins.
  • An agent's workflow send_message is refused in a DM with a stranger and allowed in its owner-only DM.
  • An owner linked after a lookup miss is seen on the next lookup (no negative caching).

cargo clippy -p buzz-relay --all-targets -D warnings is clean. The other ingest and command-executor tests pass locally (214), except two that fail only because of my local database: one needs the desired-state FTS schema, and the other fails creating its restricted role in setup. Neither reaches DM code. I pushed without the local pre-push just test-unit lane because a buzz-cli unit test hung there for about 30 minutes; CI runs that lane.

🤖 Generated with Claude Code

A key with a stored agent owner (users.agent_owner_pubkey) is refused when
it writes into a DM channel, opens a DM, or adds DM members, unless the
DM's participants are exactly the agent and its owner. Humans and non-DM
channels are untouched. The check sits in the shared channel write gate,
so messages, reactions, edits, typing and artifact moves all hit it, and
it fails closed on a lookup error.

Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Joah Gerstenberg <[email protected]>
@joahg
joahg marked this pull request as ready for review November 11, 2026 22:34
@joahg
joahg requested a review from a team as a code owner November 11, 2026 22:34
@joahg
joahg deployed to codex-review November 11, 2026 23:22 — with GitHub Actions Active

@kazemek21 kazemek21 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.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

A missing owner link can be written later, possibly by another relay
process, so caching its absence let a newly linked agent pass as human
for up to five minutes. Only a confirmed owner, which never changes once
set, is cached now.

Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Joah Gerstenberg <[email protected]>
@joahg
joahg deployed to codex-review November 12, 2026 01:27 — 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 Nov 12, 2026
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 85c7156085d902f5d5fd59e193263b109c6d5ba0...ea3585e1ff091caca802a5b90b6ac73ccca9689a.
A new review must complete for this exact range. When manual authorization
is required, a user with write access must comment exactly
@buzz-security-review ea3585e1ff091caca802a5b90b6ac73ccca9689a to authorize a new review.
Any previous review applies only to its recorded range.

A workflow's send_message posts as the workflow owner and checks may_post
directly, skipping the shared channel gate, so an agent could save a
workflow that posts into a DM its direct writes are refused in. The sink
now applies the same owner-only DM rule before persisting.

Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Joah Gerstenberg <[email protected]>
@joahg

joahg commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

🤖 Re the Codex Security Review finding (workflow send_message bypass): fixed in 1ce727c. RelayActionSink::send_message now runs the same owner-only DM check (check_agent_dm_write) right after may_post, before anything is persisted. Added workflow_send_message_keeps_agents_out_of_dms: an agent's workflow is refused in a DM with a stranger and allowed in its owner-only DM.

@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 12, 2026
@joahg
joahg deployed to codex-review November 12, 2026 02:31 — 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 Nov 12, 2026
Kind 9000 (put-user) and kind 9002 (edit metadata) skip the generic Post
gate. An agent could use 9000 to add a third person to its owner-only DM,
or 9002 to put text into a DM it may not post in through the system
message. In a DM, an agent may now never put users, and may edit metadata
only in its owner-only DM.

Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Joah Gerstenberg <[email protected]>
@joahg

joahg commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

🤖 Re the Codex Security Review findings on 1ce727c, both fixed in d990020:

  • [HIGH] kind 9000 additions: in a DM, an agent's put-user is now refused, since any addition to its owner-only DM widens it. Test: agent_cannot_put_users_into_its_owner_dm.
  • [MEDIUM] kind 9002 metadata: an agent's topic or purpose edit in a DM goes through the same owner-only rule before anything is persisted or a system message is sent. Test: agent_cannot_set_metadata_in_a_dm_with_someone_other_than_its_owner.
    Both checks run in ingest_event_inner right after the channel row loads, for the kinds that skip the generic Post gate.

@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 12, 2026
@joahg
joahg deployed to codex-review November 12, 2026 02:41 — 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 Nov 12, 2026
Huddle admission checks may_post directly, so an agent could join a DM's
huddle and talk there even though its posts are refused. Admission now
applies the owner-only DM rule to the huddle channel and its lifecycle
parent.

Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Joah Gerstenberg <[email protected]>
@joahg

joahg commented Nov 12, 2026 •

Copy link
Copy Markdown
Member Author

🤖 Re the Codex Security Review finding on d990020 (audio admission): fixed in ea3585e. check_membership_for_admission now applies the owner-only DM rule (agent_dm_write_allowed) to both the huddle channel and its lifecycle parent, right after the role checks. Test: agent_joins_dm_huddles_only_with_its_owner. The commit-time re-check doesn't repeat it. One window remains: a human could widen an owner-only DM between the agent's admission and its commit. The agent still can't widen it itself.

@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 12, 2026
@joahg
joahg deployed to codex-review November 12, 2026 03:59 — with GitHub Actions Active
@joahg

joahg commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

@buzz-security-review ea3585e

@kazemek21 kazemek21 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.

[P2] Recheck the owner-only DM rule when committing huddle admission

The admission check now covers both the huddle channel and its lifecycle parent. Non-blocking remaining portion of my earlier concern: could the commit-time validation also recheck the owner-only DM rule alongside the role checks? That would close the documented window where a human widens the DM after admission but before the join commits.

Kyle reviewed this PR and the Nexus review-tracker notes and approves.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

@joahg
joahg merged commit a09b84d into main Nov 12, 2026
96 of 97 checks passed
@joahg
joahg deleted the joah/relay-agents-no-dm branch November 12, 2026 05:18
ravarora2 pushed a commit that referenced this pull request Nov 12, 2026
…small

* origin/main:
  fix(db): seek replaceable heads by channel (#9147)
  buzz-db: check events created_at floor in app writers (phase 1) (#8325)
  fix(mobile): keep a thread tail behind the keyboard unread (#9295)
  fix(mobile): restore header cues and per-channel identity check for #9369 (#9370)
  fix(relay): treat empty filter sets as match-nothing in SQL (#9342)
  buzz-cli: add --before and --before-id to messages search (#9382)
  feat(relay): agents cannot write in DMs except with their owner (#9371)
  fix(relay): bound each cross-node delivery attempt (#9305)
  feat(mobile): add iOS Home Screen shortcuts for New chat, New DM and the Bestie (#9360)
  perf(mobile): stop downloading whole videos for posterless stills (#9367)
  Enforce single-community relay mode (#9149)
  fix(relay): match #a tag filters in SQL before LIMIT (#9339)
  Add participant avatars to mobile conversation headers (#9369)
  fix(mobile): stop asking for an API token when the channel list is refused (#9327)
  chore(release): release Buzz Relay version 0.3.0 (#9362)
  Fix gallery video selection, thumbnails, and swipe dismissal (#9356)

Signed-off-by: dessert <7d616e7e143e5d408159614dccff1371350a1c1bd36c8de838a45ece5aca0aca@buzz.block.builderlab.xyz>
TheSentinel454 added a commit that referenced this pull request Nov 12, 2026
…04 (#9418)

## Summary

Two separate CI breaks are hitting every PR right now. Each one blocks a
required check, and neither fix can go green on its own, so this PR
fixes both.

**1. Rust test targets stopped compiling on main at fc8c802.** #9227
added a required `RunCause` argument to `create_workflow_run` and a
`may_direct_agents` flag to `ActionSink::send_message`. Two tests that
merged around the same time still used the old signatures:

- `attach_rejects_run_from_another_workflow` in
`crates/buzz-db/src/store/workflow.rs` (from #8317)
- the agent-DM refusal test in `crates/buzz-relay/src/workflow_sink.rs`
(from #9371)

Each PR passed CI on its own base. Together they break Rust Lint, Unit
Tests, Windows Rust and the Desktop E2E Relay artifact build (main CI
run 43291564834). The fix passes `RunCause::Schedule` and `true`,
matching the neighbouring tests. Neither test is about run cause or
agent mentions, so neither assertion changes.

**2. Desktop E2E Integration fails on GitHub's new Ubuntu 26.04
runners.** `ubuntu-latest` is rolling to 26.04, and Playwright 1.60 (our
lockfile) fails with `Playwright does not support chromium on
ubuntu26.04-x64`. Playwright 1.61 is the first release that knows
ubuntu26.04. This PR bumps `@playwright/test` to `^1.65.0` in
`desktop/`, `web/` and `admin-web/` and regenerates `pnpm-lock.yaml`. It
also pins every `ubuntu-latest` job in `ci.yml` and `_ci-*.yml` to
`ubuntu-26.04`, so the next runner image change arrives in a reviewed
diff. macOS and Windows jobs are unchanged.

The newer Chromium bundled with Playwright 1.65 changed two smoke specs.
It reports Tailwind's `items-end` as `align-items: flow-end`
(`doctor-states` now accepts `flex-end` or `flow-end`), and it draws the
textarea resize grip differently, so the
`workflow-template-variable-autocomplete` baseline is refreshed from the
ubuntu-26.04 CI output.

It also exposed a timing race in `profile.spec.ts` "owned agent profile
stays in parity": when the Message click lands while the profile panel
is still settling, Playwright's scroll-into-view retry scrolls the
profile body, which hides the owned-agent action row (aria-hidden +
inert) for good. The test now pins the body to the top and waits for the
row before clicking (12/12 repeats plus the full profile spec pass on
Linux).

## Verification

- `cargo clippy -p buzz-db -p buzz-relay --tests -- -D warnings` is
clean on fc8c802 with commit 1 applied.
- `pnpm install --frozen-lockfile` succeeds with the new lockfile, and
`playwright --version` reports 1.65.0.
- `tsc --noEmit` passes for `desktop`, `web` and `admin-web`.
- This PR's CI is the E2E check on 26.04.

## Notes

- Workflows that don't run on PRs (release, helm charts, sprig, image
builds, codex-security-review) still use `ubuntu-latest`. This PR can't
exercise them, so they're left for a follow-up.
- The hermit-pinned actionlint (1.7.12) doesn't know the `ubuntu-26.04`
label yet. CI only actionlints `codex-security-review.yml`, which this
PR doesn't touch.

## Simpler

No production code changes. The runner pin swaps an implicit moving
target for an explicit one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Luke Tornquist <[email protected]>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>
wpfleger96 added a commit that referenced this pull request Nov 14, 2026
#9500)

…er (#9371)"

This reverts commit a09b84d, resolved
against current main (#9227 changed the neighboring workflow_sink
tests).

Supersedes #9393

Signed-off-by: Joah Gerstenberg <[email protected]>
Co-authored-by: Joah Gerstenberg <[email protected]>
Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>

This branch was successfully deployed

1 active deployment
codex-review — ea3585e1 Deployed Nov 12, 2026 by joahg via Run Codex Security Review #14555
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