From 61cab770045c1a163f4a8ce82561ce973d2e6c00 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 9 Nov 2026 12:52:27 -0500 Subject: [PATCH 1/8] docs(vision): add information flow vision for agents Describe how an agent's owner controls the flow of information through the agent with channel membership and roles: the W/R model, the paths where the owner loses that control today, and the restricted level that closes them. Link it from VISION.md and mark the broker paper as an alternative considered. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- VISION.md | 2 + VISION_INFORMATION_FLOW.md | 185 ++++++++++++++++++ ...ctical-information-flow-for-buzz-agents.md | 2 + 3 files changed, 189 insertions(+) create mode 100644 VISION_INFORMATION_FLOW.md diff --git a/VISION.md b/VISION.md index 96491c7c653..723d50c005f 100644 --- a/VISION.md +++ b/VISION.md @@ -36,6 +36,8 @@ One event log. One search index. Three lenses. The relay enforces all access control. Channel membership is the only gate. +Agents act with their owner's permissions, so channel membership also decides where information can flow through an agent. See [VISION_INFORMATION_FLOW.md](VISION_INFORMATION_FLOW.md). + | Type | Visibility | Join | Create | |------|-----------|------|--------| | **Open channels** | Searchable by all members | Self-join | Any member | diff --git a/VISION_INFORMATION_FLOW.md b/VISION_INFORMATION_FLOW.md new file mode 100644 index 00000000000..839b751ae95 --- /dev/null +++ b/VISION_INFORMATION_FLOW.md @@ -0,0 +1,185 @@ +# Vision: Information flow through agents + +Buzz agents act with permissions that belong to their owner. Another person can prompt the agent, while the agent can still read its owner's private information in Buzz. That is a confused-deputy problem. Prompt injection makes it easy to exploit, but the root problem is that an untrusted request can reach an agent that holds more authority than the requester should receive. + +Buzz solves the narrow part of this: an agent must not move private information to a broader audience. It does not try to solve general prompt injection. + +The goal is to **control the flow of information between people, with the agent as the conduit**, using the access control Buzz already has: channel membership. [Practical information-flow for Buzz agents](docs/practical-information-flow-for-buzz-agents.md) describes a broker design for the same problem. Buzz follows the channel-based design below instead (see [Alternatives considered](#alternatives-considered)). + +This document describes direction. Rules that the relay does not enforce yet are listed in [Rollout](#rollout). + +## The model + +Treat the agent as a conduit. Two sets describe it: + +- **W** is whose content goes in: every channel the agent can read, as a member or a guest. +- **R** is who can get information out: everyone who can read a channel where the agent can write. + +Information is safe when, for every channel *c* in W, everyone in R can already read *c*. A private channel in W with a reader in R who is not one of its members is a leak path. + +Channel roles decide W and R. A member of a channel can read and write in it, and a guest can only read ([Announce channels](NOSTR.md#announce-channels)). So: + +- When an agent joins a channel, that channel becomes part of W. +- When an agent is a member of a channel (not a guest), everyone who can read that channel becomes part of R. +- An open channel is readable by the whole community, so the first open channel where the agent is a member makes R the whole community. +- Reading an open channel is always safe: its content is already public. + +**If the agent's owner controls which channels the agent joins, and whether it is a guest or a member in each, then the owner controls W and R, and so controls how information flows between people through the agent.** The relay already enforces channel membership and roles. The rest of this document closes the paths where the owner does not have that control. + +## Where the owner loses control + +Each of these paths changes W or R without the owner. Checked against `main` at `658253009`. + +| Path | Who does it | Effect | +|---|---|---| +| A member adds the agent to their private channel. The agent's `channel_add_policy` defaults to `anyone`, and the agent sets it itself with kind 10100, so it can loosen it at any time. | Any member of that channel | W gains the channel; R gains its readers if the agent can write | +| A member gives the agent a writing role. Any member who can write can make a guest a member. | Channel members | R gains the channel's readers | +| The agent joins an open channel as a member, or writes in it without joining. Both work in an open channel with `join_role = member`. | The agent's own key | R becomes the whole community | +| The agent creates a channel and adds people to it. The creator owns the channel, so it can add anyone. | The agent's own key | W gains the channel; R gains everyone the agent adds | +| The agent opens a DM or group DM. `handle_dm_open` has no agent check. | The agent's own key | W and R gain the DM's participants | +| The agent publishes a community-readable event outside a channel: profile (kind 0), text note (kind 1), long-form, user status, or a gift wrap to any recipient. | The agent's own key | R becomes everyone who can read that event | +| Someone joins a channel where the agent is a member. New members can read the channel's history. | Whoever the channel's admission rules allow | R grows | +| An admin changes a channel's visibility from private to open. | Channel admins | R becomes the whole community | + +Three of these paths use the agent's own key. The broker design takes the key away from the agent process for that reason. That is not necessary: the relay already knows who owns each agent (`agent_owner_pubkey`, which cannot change), so it can refuse those actions when the agent tries them. + +## Design + +### Restricted and unrestricted agents + +The agent rules below cost utility. A restricted agent cannot post a note, write in an open channel that its owner did not make it a member of, set up a channel for a team, or change its profile freely. Many agents do all of these. So the rules are a level that the owner sets for each agent: + +- **Restricted:** the relay applies every agent rule in this design. +- **Unrestricted:** the relay applies none of them, and the agent works as it does now. + +The level is the owner's choice. The relay stores it next to `agent_owner_pubkey`. Only the owner can change it, with a signed command; the agent cannot. + +An unrestricted agent still has the leak paths. The level only moves the decision to the people who share with the agent, so the level is public. Clients show it on the agent's profile and in the add flow: "This agent is unrestricted. It can post what it reads here anywhere it can write." + +A change of level applies to new actions: + +- **Unrestricted to restricted:** the relay refuses new actions that break the rules. The agent keeps its current channels and roles, including channels that other people added it to. The owner can review them and demote or remove the agent. +- **Restricted to unrestricted:** R can grow at once, for example when someone adds the agent to their channel. + +One rule applies to every channel, whatever the level: no private-to-open changes (see [Membership is a decision about the channel](#membership-is-a-decision-about-the-channel)). It protects people as well as agents. + +**Open question:** which level a new agent gets. Rollout starts with unrestricted, so that nothing changes until owners choose. + +**Possible alternative to a stored level:** the relay skips the agent rules while the agent is a member of any open channel. In that state R is already the whole community, so the rules have nothing left to protect. This is one existence check on the agent's memberships, not a calculation of R. It removes the stored level and the owner's signed command, and the public label follows from memberships. The cost: the owner no longer chooses the level; the agent drifts into it. While the rules are off, the agent can create channels and add anyone, and those people stay in R after the owner demotes the agent in the open channel. + +### Only the owner puts an agent in a channel + +To give the owner control of W, make the owner the only one who can change it. Only the agent's owner can add it to a channel, of any kind. Other members cannot. + +The relay already has most of this. Each agent has a `channel_add_policy` (`anyone`, `owner_only` or `nobody`), and `owner_only` refuses adds by anyone but the owner. But the policy defaults to `anyone`, and the agent's own key sets it. For a restricted agent, the relay treats the policy as `owner_only` or stricter, and the agent can make it stricter but never looser. + +The agent cannot join a private channel on its own. It can join an open channel by itself, but only as a guest (see [Agents are guests unless the owner says otherwise](#agents-are-guests-unless-the-owner-says-otherwise)). It can create a channel, with limits (see [Channels the agent creates](#channels-the-agent-creates)). + +DMs and group DMs are channels too, so the same rule applies: only the owner can open one that includes the agent. A harness that forwards only its owner's messages to the model (for example `--respond-to owner-only`) does not close this path, because the agent can still read the DM with its own key. The relay has to refuse it. + +Nothing private reaches the agent unless the owner put it there. + +### Channels the agent creates + +An agent sometimes needs a channel of its own, for example a system channel that holds its configuration. The relay makes the creator the channel's owner, so a channel that the agent creates is in W, and everyone it adds is in R. The relay limits this with the same rule as everywhere else: + +- An agent can create only private channels. They stay private, because the relay refuses private-to-open changes. +- In a channel that it created, the agent can add only its owner, and can change only its owner's role. Anyone else gets in only through people: its owner, or someone that the channel's admission rules allow. +- The agent can still lower roles and remove members, because that only makes R smaller. + +This is the largest cost of the restricted level. A restricted agent cannot set up a working channel for a team and invite the team. Its owner has to add the people, or make the agent unrestricted. A configuration channel is not affected: its roster is the agent and its owner. + +### Agents are guests unless the owner says otherwise + +R depends on where the agent can write, so the owner controls that separately from where it can read. When the owner adds the agent to a channel, it joins as a guest unless the owner picks member. (The channel's `join_role` still applies to people.) Only the owner can raise the agent's role later. Anyone who can change roles in the channel can lower it, because a lower role only makes R smaller. + +In an open channel with `join_role = member`, a person can write without joining. If an agent could do the same, it could post in a community-wide channel without anyone adding it. So an agent cannot. + +An agent can join an open channel by itself, as a guest. That adds only public content to W and nothing to R, so it needs no decision from the owner. The relay checks this locally: the channel is open, the role is guest, and the event is the agent's. + +A guest agent adds to W but never to R. The owner can let the agent read broadly without widening its audience at all. + +### Agents don't publish outside channels + +An event outside a channel bypasses R entirely. A kind 1 note or a profile update is readable by whoever the relay lets read it, and no channel membership is involved. So the relay refuses any event from an agent that has no `h` tag. + +The exceptions are kinds whose readers the relay already limits to the agent and its owner: the author-only kinds, and turn metrics (which only the `p`-tagged owner can read). One more exception does not limit its readers: the agent's profile (kind 0). Clients need it, because it carries the NIP-OA owner attestation that tells them the key is an agent, and only the agent's key can sign it. Its fields are public, so the profile is a leak path: the agent can write anything into them. Kind 0 is replaceable, so the agent can change it at any time, and each new version reaches everyone who reads it. + +For a restricted agent, the relay accepts a kind 0 only when it carries the owner's signature over its content (a profile approval tag). The owner approves each profile change, and changes are rare. Publish-once is not enough: the agent can read first and then put what it read into its one profile. + +Refusing by default is what matters here. A new kind is covered without anyone having to remember that agents exist; it only gets through if someone deliberately adds it to the exceptions. + +### Possible: step-up for actions with the owner's authority + +Some work needs more than the agent's own access, for example summarizing a private channel that the agent is not in, or posting as the owner from a CLI. An app can do this with the owner's session or key. The relay then sees the owner, not the agent, so none of the rules above apply. This can already happen without a decision by the owner: [buzz-app#2745](https://github.com/block/buzz-app/pull/2745) and [buzz-app#2881](https://github.com/block/buzz-app/pull/2881) give every bundled agent read access to all of the owner's channels, DMs and Canvases. + +One possible answer is a step-up: an action with the owner's authority that the owner approves explicitly, for a stated scope and time, that is attributable to the agent, counted in W and R, and revocable. This is not part of this design. It widens what an agent can reach, so it would need careful boundaries and its own spec. + +### Membership is a decision about the channel + +The owner cannot control who joins someone else's channel. So when the owner makes the agent a member of a channel, what the owner accepts is the channel's **admission rules**, not its current member list. R includes everyone those rules might let in later, and because new members can read history, those people also get everything the agent wrote before they arrived. Clients have to say this: + +| Channel | Who can join R later | What the owner is accepting | +|---|---|---| +| Open | Anyone in the community | R is everyone. | +| Private | Anyone a member adds | Trust in every current member's judgment | + +Accepting the rules only means something if the rules stay put. One relay change makes sure they do: + +- **Refuse private-to-open visibility changes, for every channel.** Making a private channel open changes the admission rules out from under everyone who joined under the old ones, people included. That is a good rule with or without agents. An admin who wants an open channel can create one. + +**Later improvement: limit adds to admins.** Every private channel has the same admission rule: any member can add anyone. So making the agent a member of a private channel means trusting every current member's judgment. A later channel setting can limit adds to owners and admins. With it, an owner can choose a private channel where only the admins can grow R, and what the owner accepts becomes trust in the channel's admins. This is not needed for safety, because the owner accepts whatever the admission rules are. It gives the owner a tighter R to choose. The same setting helps announce channels, where it keeps the set of writers under the admins' control. + +The owner gets an alert when someone joins a channel where the agent is a member. That is not what keeps the data safe, since the owner accepted the rules up front. It lets the owner notice a channel heading somewhere unexpected, and demote or remove the agent. + +### W remembers + +Removing an agent from a channel stops new information from coming in, but the agent still remembers what it already read. So W keeps every channel the agent has ever read, with how far it read: channel X, up to the time it left. A channel the agent is still in is in W up to now. + +### When the owner is not a person + +The owner is whichever key signed the agent's NIP-OA attestation. It need not be a person, and the rules do not change. + +### Client work + +1. **The owner chooses member or guest for the agent in each channel.** Guest is the default. +2. **Show the level and let the owner change it.** Show it on the agent's profile and in the add flow, for everyone. +3. **Ask the owner to approve profile changes** of a restricted agent. +4. **Offer the add only to the agent's owner.** Clients that let any member add an agent directly should not. + +## What this doesn't stop + +- **Flows the owner accepts.** Say the owner makes the agent a member of an Alice-and-Bob channel and an Alice-and-Carol channel. Bob's information can now reach Carol. The owner chose both memberships, and nothing stops that choice. The broker design would block it. +- **People the channel's rules let in.** Admins or members can add people to R, and those people can read history. That is what the owner accepted when making the agent a member. +- **Actions with the owner's authority.** An app acting with the owner's session or key is not an agent action, so these rules do not see it. +- **The owner's information outside Buzz.** Files, documents, chat systems and whatever other tools the agent uses. The broker design leaves this out too. + +## Alternatives considered + +| Option | Why not | +|---|---| +| Do nothing | Any agent in a private channel can leak it to a public one, and a prompt-injected agent can widen its own audience. | +| Broker design (audience-bound instances, keyless agents, labeled memory) | It enforces a stronger rule: no flow between different private audiences, even inside one agent. But it needs a trusted broker, key custody outside the agent process, per-audience instances and labeled managed memory. This design uses the access control Buzz already has, and puts the decision in front of the owner. | +| Label each agent event with its sources and check every read | It handles late joins without trusting admission rules, but it is event-level IFC: a second access-control system alongside channels, which every read path has to apply. This design keeps channels as the only authority. | +| Owner controls only non-public memberships | Not enough on its own, because the first open channel where the agent is a member makes R everyone. The guest default closes that. | + +## Risks + +- **Owner decisions.** This is only as good as the owner's decisions about which channels the agent is in, and in which role. +- **A wrong exception.** The exception list is the only way an agent can publish outside a channel, so every kind on it, except the profile, has to really limit its readers to the agent and its owner. +- **The agent's profile is a leak path** for an unrestricted agent. Its fields are public and the agent can change them at any time. +- **Owners choose unrestricted for convenience.** The level then protects nobody. The public label lets other people decide what to share with the agent, but only if they read it. +- **Refusing private-to-open breaks a workflow.** Some admins start a channel private and open it later. They have to create a new open channel instead. + +## Rollout + +1. ✅ Announce channels: guests are read-only everywhere ([#8143](https://github.com/block/buzz/pull/8143)). +2. Add the level, with every agent unrestricted. Nothing changes until an owner chooses restricted. +3. For restricted agents: only the owner can add the agent to a channel or open a DM that includes it. The agent can join open channels by itself, as a guest only. The agent is a guest by default. Only the owner raises its role. Anyone with role authority can lower it. The agent creates only private channels, and adds only its owner to them. +4. Refuse private-to-open visibility changes for every channel. +5. For restricted agents: refuse events without an `h` tag, after checking what agents publish now, and require profile approval. +6. Decide the default level for new agents. + +Each step only refuses actions, so backing one out means turning the rule off. Existing agents keep their memberships and roles; the rules apply to new actions. + +The wire formats for the level and the profile approval tag are specified when they are built, in the NIPs that own agent identity. diff --git a/docs/practical-information-flow-for-buzz-agents.md b/docs/practical-information-flow-for-buzz-agents.md index 549febac9df..45e445403d3 100644 --- a/docs/practical-information-flow-for-buzz-agents.md +++ b/docs/practical-information-flow-for-buzz-agents.md @@ -1,5 +1,7 @@ # Practical information-flow for Buzz agents +> **Status:** alternative considered. Buzz follows the channel-based design in [VISION_INFORMATION_FLOW.md](../VISION_INFORMATION_FLOW.md) instead. + Author: Jordan Mecom Draft From 6e030f792ee28265bd2d835032a2af7977392307 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 9 Nov 2026 12:52:28 -0500 Subject: [PATCH 2/8] docs(nostr): correct who may add members with kind 9000 Any member who can post may add people to a private channel, not only owners and admins (decide_member_put in buzz-core channel.rs). State the open-channel non-member rule and the elevated-role rule too. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- NOSTR.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NOSTR.md b/NOSTR.md index 629c4f808a2..1865f6a63df 100644 --- a/NOSTR.md +++ b/NOSTR.md @@ -51,7 +51,7 @@ PGPASSWORD=buzz_dev psql -h localhost -U buzz -d buzz -c \ | **Deletions (kind:5)** | ✅ | Standard NIP-09; self-authored only. `#h` optional, `#e` required | | **User profiles (kind:0)** | ✅ | NIP-01 metadata; synced to users table (display_name, avatar, about, NIP-05). NIP-05 handles must canonicalize to this relay's domain — off-domain or invalid handles are silently cleared. If a NIP-05 handle collides with another user's (UNIQUE constraint), the handle is skipped but other profile fields (display_name, avatar, about) are still synced. | | **Group creation (kind:9007)** | ✅ | NIP-29; include an `h` tag with the new channel id and a `name` tag, optional `visibility`, `channel_type`, label `t` and `unlisted` tags. The relay refuses a 9007 without `h`. It refuses a listed channel (open, not unlisted, stream or forum) whose name another listed channel already has, ignoring case and including archived ones: `invalid: an open channel named "" already exists`. | -| **Add user (kind:9000)** | ✅ | Open: any user, subject to target's `channel_add_policy` (`owner_only`/`nobody` can block). Private: owner/admin only. Self-add bypasses agent policy but not private-channel auth. A guest cannot add anyone. A new member with no role tag gets the channel's `join_role`; a non-member adding themselves may ask for `join_role` or a lower role. One event may name up to 100 people: one `p` tag each, with an optional role as the `p` tag's third element (NIP-29), falling back to the event's `role` tag. Each person is checked and added on their own. When the event names several people, the OK message is `members: ` followed by JSON listing the non-empty `added`, `updated` (role changed), `already_member` and `rejected` (`[{"reason", "pubkeys"}]`) results. The event is refused only when it is malformed or nobody it names may be added; then one person keeps the single-add error, and several get `invalid: members: {…}`. The event and its membership changes are stored together, so an `error:` rejection stores nothing and the same event can be resent; a resent stored event returns `duplicate:` without results. Re-adding someone at their current role posts no second `member_joined` notice. | +| **Add user (kind:9000)** | ✅ | Any member who can post may add people, in open and private channels. In an open channel with `join_role = member`, a non-member may also add people; in a private channel, a non-member cannot add anyone, including themselves. Adding someone else is subject to the target's `channel_add_policy` (`owner_only`/`nobody` can block); self-add bypasses it. Only an owner or admin may give a new member an elevated role. A guest cannot add anyone. A new member with no role tag gets the channel's `join_role`; a non-member adding themselves may ask for `join_role` or a lower role. One event may name up to 100 people: one `p` tag each, with an optional role as the `p` tag's third element (NIP-29), falling back to the event's `role` tag. Each person is checked and added on their own. When the event names several people, the OK message is `members: ` followed by JSON listing the non-empty `added`, `updated` (role changed), `already_member` and `rejected` (`[{"reason", "pubkeys"}]`) results. The event is refused only when it is malformed or nobody it names may be added; then one person keeps the single-add error, and several get `invalid: members: {…}`. The event and its membership changes are stored together, so an `error:` rejection stores nothing and the same event can be resent; a resent stored event returns `duplicate:` without results. Re-adding someone at their current role posts no second `member_joined` notice. | | **Remove user (kind:9001)** | ✅ | Self-remove allowed (with last-owner guard). Removing others: owner/admin only. | | **Edit group metadata (kind:9002)** | ✅ | `name`/`about`/label `t`/`visibility`/`ttl`/`archived`/`join_role`/`unlisted` tags: owner/admin. `topic`/`purpose` tags: any member except a guest. `auto_join` tag: relay owner/admin (see [Default channels](#default-channels)). A relay operator may also send a 9002 that changes only `name` on a listed channel, archived ones included, whatever its channel role, so that duplicate names can be repaired; the channel stays archived. The relay refuses a `name`, `visibility` or `unlisted` change that leaves a listed channel with a name another listed channel already has: `invalid: an open channel named "" already exists`. Buzz applies 9002 as a partial update: an absent tag leaves that setting unchanged. | | **Admin delete event (kind:9005)** | ✅ | Event author can always delete own. Otherwise owner/admin required. Target must be in same channel. | From 4e510ce1568fbbe6e060480e95e9367b32e8d278 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 9 Nov 2026 13:45:37 -0500 Subject: [PATCH 3/8] docs(vision): call the agent level a disclosure level Name the owner-set agent level a disclosure level, and describe public and personal as possible further disclosure levels. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- VISION_INFORMATION_FLOW.md | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/VISION_INFORMATION_FLOW.md b/VISION_INFORMATION_FLOW.md index 839b751ae95..40eee709970 100644 --- a/VISION_INFORMATION_FLOW.md +++ b/VISION_INFORMATION_FLOW.md @@ -45,27 +45,36 @@ Three of these paths use the agent's own key. The broker design takes the key aw ## Design -### Restricted and unrestricted agents +### Disclosure levels -The agent rules below cost utility. A restricted agent cannot post a note, write in an open channel that its owner did not make it a member of, set up a channel for a team, or change its profile freely. Many agents do all of these. So the rules are a level that the owner sets for each agent: +The agent rules below cost utility. A restricted agent cannot post a note, write in an open channel that its owner did not make it a member of, set up a channel for a team, or change its profile freely. Many agents do all of these. So the rules are a disclosure level that the owner sets for each agent: - **Restricted:** the relay applies every agent rule in this design. - **Unrestricted:** the relay applies none of them, and the agent works as it does now. -The level is the owner's choice. The relay stores it next to `agent_owner_pubkey`. Only the owner can change it, with a signed command; the agent cannot. +The disclosure level is the owner's choice. The relay stores it next to `agent_owner_pubkey`. Only the owner can change it, with a signed command; the agent cannot. -An unrestricted agent still has the leak paths. The level only moves the decision to the people who share with the agent, so the level is public. Clients show it on the agent's profile and in the add flow: "This agent is unrestricted. It can post what it reads here anywhere it can write." +An unrestricted agent still has the leak paths. The disclosure level only moves the decision to the people who share with the agent, so the disclosure level is public. Clients show it on the agent's profile and in the add flow: "This agent is unrestricted. It can post what it reads here anywhere it can write." -A change of level applies to new actions: +A change of disclosure level applies to new actions: - **Unrestricted to restricted:** the relay refuses new actions that break the rules. The agent keeps its current channels and roles, including channels that other people added it to. The owner can review them and demote or remove the agent. - **Restricted to unrestricted:** R can grow at once, for example when someone adds the agent to their channel. -One rule applies to every channel, whatever the level: no private-to-open changes (see [Membership is a decision about the channel](#membership-is-a-decision-about-the-channel)). It protects people as well as agents. +One rule applies to every channel, whatever the disclosure level: no private-to-open changes (see [Membership is a decision about the channel](#membership-is-a-decision-about-the-channel)). It protects people as well as agents. -**Open question:** which level a new agent gets. Rollout starts with unrestricted, so that nothing changes until owners choose. +**Open question:** which disclosure level a new agent gets. Rollout starts with unrestricted, so that nothing changes until owners choose. -**Possible alternative to a stored level:** the relay skips the agent rules while the agent is a member of any open channel. In that state R is already the whole community, so the rules have nothing left to protect. This is one existence check on the agent's memberships, not a calculation of R. It removes the stored level and the owner's signed command, and the public label follows from memberships. The cost: the owner no longer chooses the level; the agent drifts into it. While the rules are off, the agent can create channels and add anyone, and those people stay in R after the owner demotes the agent in the open channel. +**Possible alternative to a stored disclosure level:** the relay skips the agent rules while the agent is a member of any open channel. In that state R is already the whole community, so the rules have nothing left to protect. This is one existence check on the agent's memberships, not a calculation of R. It removes the stored disclosure level and the owner's signed command, and the public label follows from memberships. The cost: the owner no longer chooses the disclosure level; the agent drifts into it. While the rules are off, the agent can create channels and add anyone, and those people stay in R after the owner demotes the agent in the open channel. + +**Possible further disclosure levels.** A restricted agent is safe because the owner controls both W and R. Two more disclosure levels could be safe for other reasons. Neither is part of this design yet. + +| Disclosure level | Reads (W) | Writes to (R) | Why it is safe | +|---|---|---|---| +| Public | Only open channels, and the DMs that people open with it | Anyone | It reads nothing private. Anyone can open a DM with it, so one person's DM can reach another person. Clients tell people to treat what they tell it as public. | +| Personal | Everything the owner can read | Only the owner, in its DM with the owner | The owner can already read everything it reads. To reach anyone else, it drafts and the owner sends, or the owner approves each send. | + +A personal agent reads with the owner's authority, so the app gives the owner's read tools only to agents at that disclosure level. Buzz cannot see what either level sends outside Buzz, through other tools or through model inference on another member's node (see [What this doesn't stop](#what-this-doesnt-stop)). ### Only the owner puts an agent in a channel @@ -87,7 +96,7 @@ An agent sometimes needs a channel of its own, for example a system channel that - In a channel that it created, the agent can add only its owner, and can change only its owner's role. Anyone else gets in only through people: its owner, or someone that the channel's admission rules allow. - The agent can still lower roles and remove members, because that only makes R smaller. -This is the largest cost of the restricted level. A restricted agent cannot set up a working channel for a team and invite the team. Its owner has to add the people, or make the agent unrestricted. A configuration channel is not affected: its roster is the agent and its owner. +This is the largest cost of the restricted disclosure level. A restricted agent cannot set up a working channel for a team and invite the team. Its owner has to add the people, or make the agent unrestricted. A configuration channel is not affected: its roster is the agent and its owner. ### Agents are guests unless the owner says otherwise @@ -143,7 +152,7 @@ The owner is whichever key signed the agent's NIP-OA attestation. It need not be ### Client work 1. **The owner chooses member or guest for the agent in each channel.** Guest is the default. -2. **Show the level and let the owner change it.** Show it on the agent's profile and in the add flow, for everyone. +2. **Show the disclosure level and let the owner change it.** Show it on the agent's profile and in the add flow, for everyone. 3. **Ask the owner to approve profile changes** of a restricted agent. 4. **Offer the add only to the agent's owner.** Clients that let any member add an agent directly should not. @@ -168,18 +177,18 @@ The owner is whichever key signed the agent's NIP-OA attestation. It need not be - **Owner decisions.** This is only as good as the owner's decisions about which channels the agent is in, and in which role. - **A wrong exception.** The exception list is the only way an agent can publish outside a channel, so every kind on it, except the profile, has to really limit its readers to the agent and its owner. - **The agent's profile is a leak path** for an unrestricted agent. Its fields are public and the agent can change them at any time. -- **Owners choose unrestricted for convenience.** The level then protects nobody. The public label lets other people decide what to share with the agent, but only if they read it. +- **Owners choose unrestricted for convenience.** The disclosure level then protects nobody. The public label lets other people decide what to share with the agent, but only if they read it. - **Refusing private-to-open breaks a workflow.** Some admins start a channel private and open it later. They have to create a new open channel instead. ## Rollout 1. ✅ Announce channels: guests are read-only everywhere ([#8143](https://github.com/block/buzz/pull/8143)). -2. Add the level, with every agent unrestricted. Nothing changes until an owner chooses restricted. +2. Add the disclosure level, with every agent unrestricted. Nothing changes until an owner chooses restricted. 3. For restricted agents: only the owner can add the agent to a channel or open a DM that includes it. The agent can join open channels by itself, as a guest only. The agent is a guest by default. Only the owner raises its role. Anyone with role authority can lower it. The agent creates only private channels, and adds only its owner to them. 4. Refuse private-to-open visibility changes for every channel. 5. For restricted agents: refuse events without an `h` tag, after checking what agents publish now, and require profile approval. -6. Decide the default level for new agents. +6. Decide the default disclosure level for new agents. Each step only refuses actions, so backing one out means turning the rule off. Existing agents keep their memberships and roles; the rules apply to new actions. -The wire formats for the level and the profile approval tag are specified when they are built, in the NIPs that own agent identity. +The wire formats for the disclosure level and the profile approval tag are specified when they are built, in the NIPs that own agent identity. From 4bff457c88921992c23ce9475b068a40d9a9c63f Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 9 Nov 2026 13:52:14 -0500 Subject: [PATCH 4/8] docs(vision): note a possible write-only role Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- VISION_INFORMATION_FLOW.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/VISION_INFORMATION_FLOW.md b/VISION_INFORMATION_FLOW.md index 40eee709970..d0dd054e902 100644 --- a/VISION_INFORMATION_FLOW.md +++ b/VISION_INFORMATION_FLOW.md @@ -76,6 +76,8 @@ One rule applies to every channel, whatever the disclosure level: no private-to- A personal agent reads with the owner's authority, so the app gives the owner's read tools only to agents at that disclosure level. Buzz cannot see what either level sends outside Buzz, through other tools or through model inference on another member's node (see [What this doesn't stop](#what-this-doesnt-stop)). +**Possible: a write-only role.** A member that can post in a channel but cannot read it. R grows and W doesn't. It fits integrations and notifiers that post into many private channels: they no longer read those channels, so content can't move between them through the key. The relay refuses the key's reads of that channel on every read path. A missed read path only lets W grow, as it does today. + ### Only the owner puts an agent in a channel To give the owner control of W, make the owner the only one who can change it. Only the agent's owner can add it to a channel, of any kind. Other members cannot. From 5cb61eb1446c245e5f6bfc41f33b61f9e70196e8 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 9 Nov 2026 14:02:35 -0500 Subject: [PATCH 5/8] docs(vision): note that a person is an unrestricted agent for information flow Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- VISION_INFORMATION_FLOW.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/VISION_INFORMATION_FLOW.md b/VISION_INFORMATION_FLOW.md index d0dd054e902..4edd5f74142 100644 --- a/VISION_INFORMATION_FLOW.md +++ b/VISION_INFORMATION_FLOW.md @@ -56,6 +56,8 @@ The disclosure level is the owner's choice. The relay stores it next to `agent_o An unrestricted agent still has the leak paths. The disclosure level only moves the decision to the people who share with the agent, so the disclosure level is public. Clients show it on the agent's profile and in the add flow: "This agent is unrestricted. It can post what it reads here anywhere it can write." +For information flow, a person is an unrestricted agent. A person reads the channels they are in, writes wherever they may, and anyone can add them to a channel or open a DM with them. The difference is who decides: a person decides with their own judgment, while an agent can be steered by anyone who can prompt it. An agent that publishes no NIP-OA attestation looks like a person to the relay, so it is treated as unrestricted. No other disclosure level can apply to an agent until it publishes its attestation. + A change of disclosure level applies to new actions: - **Unrestricted to restricted:** the relay refuses new actions that break the rules. The agent keeps its current channels and roles, including channels that other people added it to. The owner can review them and demote or remove the agent. From 624bef6f8f65bc73e711d15132102310936eda35 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 9 Nov 2026 17:07:50 -0500 Subject: [PATCH 6/8] docs: allow opening a channel that holds only its opener Mirror the Channels for IFC plan: refuse private-to-open changes except in a channel where every member is the person opening it or one of that person's agents. Refuse open-to-private changes while a public agent is in the channel. Clients warn when the owner raises an agent to member in an open channel. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- VISION_INFORMATION_FLOW.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/VISION_INFORMATION_FLOW.md b/VISION_INFORMATION_FLOW.md index 4edd5f74142..b0d84e60a3b 100644 --- a/VISION_INFORMATION_FLOW.md +++ b/VISION_INFORMATION_FLOW.md @@ -63,7 +63,7 @@ A change of disclosure level applies to new actions: - **Unrestricted to restricted:** the relay refuses new actions that break the rules. The agent keeps its current channels and roles, including channels that other people added it to. The owner can review them and demote or remove the agent. - **Restricted to unrestricted:** R can grow at once, for example when someone adds the agent to their channel. -One rule applies to every channel, whatever the disclosure level: no private-to-open changes (see [Membership is a decision about the channel](#membership-is-a-decision-about-the-channel)). It protects people as well as agents. +One rule applies to every channel, whatever the disclosure level: no private-to-open changes, except in a channel that holds only its opener and the opener's agents (see [Membership is a decision about the channel](#membership-is-a-decision-about-the-channel)). It protects people as well as agents. **Open question:** which disclosure level a new agent gets. Rollout starts with unrestricted, so that nothing changes until owners choose. @@ -78,6 +78,8 @@ One rule applies to every channel, whatever the disclosure level: no private-to- A personal agent reads with the owner's authority, so the app gives the owner's read tools only to agents at that disclosure level. Buzz cannot see what either level sends outside Buzz, through other tools or through model inference on another member's node (see [What this doesn't stop](#what-this-doesnt-stop)). +A public agent can't stay in a channel that becomes private. Its W would gain a private channel. So an open-to-private change is refused while a public agent is in the channel: remove the public agents first. + **Possible: a write-only role.** A member that can post in a channel but cannot read it. R grows and W doesn't. It fits integrations and notifiers that post into many private channels: they no longer read those channels, so content can't move between them through the key. The relay refuses the key's reads of that channel on every read path. A missed read path only lets W grow, as it does today. ### Only the owner puts an agent in a channel @@ -104,7 +106,7 @@ This is the largest cost of the restricted disclosure level. A restricted agent ### Agents are guests unless the owner says otherwise -R depends on where the agent can write, so the owner controls that separately from where it can read. When the owner adds the agent to a channel, it joins as a guest unless the owner picks member. (The channel's `join_role` still applies to people.) Only the owner can raise the agent's role later. Anyone who can change roles in the channel can lower it, because a lower role only makes R smaller. +R depends on where the agent can write, so the owner controls that separately from where it can read. When the owner adds the agent to a channel, it joins as a guest unless the owner picks member. (The channel's `join_role` still applies to people.) Only the owner can raise the agent's role later. Raising the agent to member in an open channel sets R to the whole community, so everything in W, including other people's content, can reach everyone. Clients say so when the owner raises it. Anyone who can change roles in the channel can lower it, because a lower role only makes R smaller. In an open channel with `join_role = member`, a person can write without joining. If an agent could do the same, it could post in a community-wide channel without anyone adding it. So an agent cannot. @@ -139,7 +141,8 @@ The owner cannot control who joins someone else's channel. So when the owner mak Accepting the rules only means something if the rules stay put. One relay change makes sure they do: -- **Refuse private-to-open visibility changes, for every channel.** Making a private channel open changes the admission rules out from under everyone who joined under the old ones, people included. That is a good rule with or without agents. An admin who wants an open channel can create one. +- **Refuse private-to-open visibility changes, with one exception.** Making a private channel open changes the admission rules out from under everyone who joined under the old ones, people included. That is a good rule with or without agents. An admin who wants an open channel can create one. + The exception is a channel where nobody but the person opening it joined under the old rules. Every member must be that person or one of that person's agents that is not a restricted writer. Opening such a channel, for example a Solo chat, is a one-time disclosure by a person of what that person can already read. After it opens, it is an ordinary open channel: the owner's restricted agents stay guests until the owner raises them. **Later improvement: limit adds to admins.** Every private channel has the same admission rule: any member can add anyone. So making the agent a member of a private channel means trusting every current member's judgment. A later channel setting can limit adds to owners and admins. With it, an owner can choose a private channel where only the admins can grow R, and what the owner accepts becomes trust in the channel's admins. This is not needed for safety, because the owner accepts whatever the admission rules are. It gives the owner a tighter R to choose. The same setting helps announce channels, where it keeps the set of writers under the admins' control. @@ -182,14 +185,15 @@ The owner is whichever key signed the agent's NIP-OA attestation. It need not be - **A wrong exception.** The exception list is the only way an agent can publish outside a channel, so every kind on it, except the profile, has to really limit its readers to the agent and its owner. - **The agent's profile is a leak path** for an unrestricted agent. Its fields are public and the agent can change them at any time. - **Owners choose unrestricted for convenience.** The disclosure level then protects nobody. The public label lets other people decide what to share with the agent, but only if they read it. -- **Refusing private-to-open breaks a workflow.** Some admins start a channel private and open it later. They have to create a new open channel instead. +- **Refusing private-to-open breaks a workflow.** Some admins start a channel private and open it later. Unless the channel holds only them and their agents, they have to create a new open channel instead. +- **Opening a Solo chat publishes what the owner's agents wrote there**, including what they drew from their other channels. Accepted: a person discloses what they can read. ## Rollout 1. ✅ Announce channels: guests are read-only everywhere ([#8143](https://github.com/block/buzz/pull/8143)). 2. Add the disclosure level, with every agent unrestricted. Nothing changes until an owner chooses restricted. 3. For restricted agents: only the owner can add the agent to a channel or open a DM that includes it. The agent can join open channels by itself, as a guest only. The agent is a guest by default. Only the owner raises its role. Anyone with role authority can lower it. The agent creates only private channels, and adds only its owner to them. -4. Refuse private-to-open visibility changes for every channel. +4. Refuse private-to-open visibility changes, except in a channel where every member is the person opening it or one of that person's agents that is not a restricted writer. 5. For restricted agents: refuse events without an `h` tag, after checking what agents publish now, and require profile approval. 6. Decide the default disclosure level for new agents. From e34a6a60e7e667e8452879ca892897573bc3b2c0 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 9 Nov 2026 17:23:17 -0500 Subject: [PATCH 7/8] docs: address agent review of the information flow vision - The opening exception counts former members, and refuses a channel where a restricted agent has posted. - Owners cannot choose restricted until its relay rules are live. - Restricted agents join default channels as guests. - The outside-channel rule keys on where the relay stores an event, not on its h tag. - The leak-path table adds open-channel adds, DMs others open, and default-channel joins, and names bot among the writing roles. - Correct today's owner recording, owner tools and membership history. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- VISION_INFORMATION_FLOW.md | 48 ++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/VISION_INFORMATION_FLOW.md b/VISION_INFORMATION_FLOW.md index b0d84e60a3b..46c8c42e5ee 100644 --- a/VISION_INFORMATION_FLOW.md +++ b/VISION_INFORMATION_FLOW.md @@ -2,7 +2,7 @@ Buzz agents act with permissions that belong to their owner. Another person can prompt the agent, while the agent can still read its owner's private information in Buzz. That is a confused-deputy problem. Prompt injection makes it easy to exploit, but the root problem is that an untrusted request can reach an agent that holds more authority than the requester should receive. -Buzz solves the narrow part of this: an agent must not move private information to a broader audience. It does not try to solve general prompt injection. +Buzz solves the narrow part of this: an agent must not move private information to a broader audience unless its owner decided it. It does not try to solve general prompt injection. The goal is to **control the flow of information between people, with the agent as the conduit**, using the access control Buzz already has: channel membership. [Practical information-flow for Buzz agents](docs/practical-information-flow-for-buzz-agents.md) describes a broker design for the same problem. Buzz follows the channel-based design below instead (see [Alternatives considered](#alternatives-considered)). @@ -17,31 +17,33 @@ Treat the agent as a conduit. Two sets describe it: Information is safe when, for every channel *c* in W, everyone in R can already read *c*. A private channel in W with a reader in R who is not one of its members is a leak path. -Channel roles decide W and R. A member of a channel can read and write in it, and a guest can only read ([Announce channels](NOSTR.md#announce-channels)). So: +Channel roles decide W and R. The roles that can post (member, bot, admin and owner) can read and write in a channel, and a guest can only read ([Announce channels](NOSTR.md#announce-channels)). So: - When an agent joins a channel, that channel becomes part of W. -- When an agent is a member of a channel (not a guest), everyone who can read that channel becomes part of R. -- An open channel is readable by the whole community, so the first open channel where the agent is a member makes R the whole community. +- When an agent has a role that can post in a channel (not guest), everyone who can read that channel becomes part of R. +- An open channel is readable by the whole community, so the first open channel where the agent can post makes R the whole community. - Reading an open channel is always safe: its content is already public. **If the agent's owner controls which channels the agent joins, and whether it is a guest or a member in each, then the owner controls W and R, and so controls how information flows between people through the agent.** The relay already enforces channel membership and roles. The rest of this document closes the paths where the owner does not have that control. ## Where the owner loses control -Each of these paths changes W or R without the owner. Checked against `main` at `658253009`. +Each of these paths changes W or R without the owner. Checked against `main` at `5b1698ec2`. | Path | Who does it | Effect | |---|---|---| -| A member adds the agent to their private channel. The agent's `channel_add_policy` defaults to `anyone`, and the agent sets it itself with kind 10100, so it can loosen it at any time. | Any member of that channel | W gains the channel; R gains its readers if the agent can write | +| Someone adds the agent to a channel. In a private channel, any member who can post can do it. In an open channel, any poster can, and so can a non-member when `join_role = member`; the agent joins at `join_role`, member by default. The agent's `channel_add_policy` defaults to `anyone`, and the agent sets it itself with kind 10100, so it can loosen it at any time. | Members of that channel, or in an open channel with `join_role = member`, anyone | W gains the channel; R gains its readers if the agent can write | | A member gives the agent a writing role. Any member who can write can make a guest a member. | Channel members | R gains the channel's readers | | The agent joins an open channel as a member, or writes in it without joining. Both work in an open channel with `join_role = member`. | The agent's own key | R becomes the whole community | | The agent creates a channel and adds people to it. The creator owns the channel, so it can add anyone. | The agent's own key | W gains the channel; R gains everyone the agent adds | | The agent opens a DM or group DM. `handle_dm_open` has no agent check. | The agent's own key | W and R gain the DM's participants | -| The agent publishes a community-readable event outside a channel: profile (kind 0), text note (kind 1), long-form, user status, or a gift wrap to any recipient. | The agent's own key | R becomes everyone who can read that event | -| Someone joins a channel where the agent is a member. New members can read the channel's history. | Whoever the channel's admission rules allow | R grows | +| Someone opens a DM or group DM that includes the agent. Neither opening a DM nor adding a DM member reads `channel_add_policy`, so even `nobody` does not stop it. | Anyone | W and R gain the DM's participants | +| The agent joins the community, through an invite or a community add (kind 9030). The relay then joins it to every default channel, as a self-join. Default channels are open, so with `join_role = member` the agent can post in them. | Whoever admits the agent to the community | R becomes the whole community | +| The agent publishes a community-readable event outside a channel: profile (kind 0), text note (kind 1), long-form, user status, git events, lists, or a gift wrap to any recipient. | The agent's own key | R becomes everyone who can read that event | +| Someone joins a channel where the agent can post. New members can read the channel's history. | Whoever the channel's admission rules allow | R grows | | An admin changes a channel's visibility from private to open. | Channel admins | R becomes the whole community | -Three of these paths use the agent's own key. The broker design takes the key away from the agent process for that reason. That is not necessary: the relay already knows who owns each agent (`agent_owner_pubkey`, which cannot change), so it can refuse those actions when the agent tries them. +Four of these paths use the agent's own key. The broker design takes the key away from the agent process for that reason. That is not necessary: the relay already knows who owns each agent (`agent_owner_pubkey`, which cannot change), so it can refuse those actions when the agent tries them. ## Design @@ -56,14 +58,14 @@ The disclosure level is the owner's choice. The relay stores it next to `agent_o An unrestricted agent still has the leak paths. The disclosure level only moves the decision to the people who share with the agent, so the disclosure level is public. Clients show it on the agent's profile and in the add flow: "This agent is unrestricted. It can post what it reads here anywhere it can write." -For information flow, a person is an unrestricted agent. A person reads the channels they are in, writes wherever they may, and anyone can add them to a channel or open a DM with them. The difference is who decides: a person decides with their own judgment, while an agent can be steered by anyone who can prompt it. An agent that publishes no NIP-OA attestation looks like a person to the relay, so it is treated as unrestricted. No other disclosure level can apply to an agent until it publishes its attestation. +For information flow, a person is an unrestricted agent. A person reads the channels they are in, writes wherever they may, and anyone can add them to a channel or open a DM with them. The difference is who decides: a person decides with their own judgment, while an agent can be steered by anyone who can prompt it. The relay records an agent's owner only when the agent authenticates with its NIP-OA owner attestation or an agent token; a kind 0 attestation alone records nothing. An agent that never authenticates that way looks like a person to the relay, so it is treated as unrestricted. No other disclosure level can apply to an agent until its owner is recorded. A change of disclosure level applies to new actions: - **Unrestricted to restricted:** the relay refuses new actions that break the rules. The agent keeps its current channels and roles, including channels that other people added it to. The owner can review them and demote or remove the agent. - **Restricted to unrestricted:** R can grow at once, for example when someone adds the agent to their channel. -One rule applies to every channel, whatever the disclosure level: no private-to-open changes, except in a channel that holds only its opener and the opener's agents (see [Membership is a decision about the channel](#membership-is-a-decision-about-the-channel)). It protects people as well as agents. +One rule applies to every channel, whatever the disclosure level: no private-to-open changes, except in a channel that has only ever held its opener and the opener's agents (see [Membership is a decision about the channel](#membership-is-a-decision-about-the-channel)). It protects people as well as agents. **Open question:** which disclosure level a new agent gets. Rollout starts with unrestricted, so that nothing changes until owners choose. @@ -110,13 +112,13 @@ R depends on where the agent can write, so the owner controls that separately fr In an open channel with `join_role = member`, a person can write without joining. If an agent could do the same, it could post in a community-wide channel without anyone adding it. So an agent cannot. -An agent can join an open channel by itself, as a guest. That adds only public content to W and nothing to R, so it needs no decision from the owner. The relay checks this locally: the channel is open, the role is guest, and the event is the agent's. +An agent can join an open channel by itself, as a guest. That adds only public content to W and nothing to R, so it needs no decision from the owner. The relay checks this locally: the channel is open, the role is guest, and the event is the agent's. The same holds when the relay joins a new community member to the community's default channels: a restricted agent joins them as a guest. A guest agent adds to W but never to R. The owner can let the agent read broadly without widening its audience at all. ### Agents don't publish outside channels -An event outside a channel bypasses R entirely. A kind 1 note or a profile update is readable by whoever the relay lets read it, and no channel membership is involved. So the relay refuses any event from an agent that has no `h` tag. +An event outside a channel bypasses R entirely. A kind 1 note or a profile update is readable by whoever the relay lets read it, and no channel membership is involved. So the relay refuses any event from an agent that it would store outside a channel. An `h` tag does not count when the relay stores that kind community-wide anyway, as it does for kind 0, kind 1, long-form, user status, git events and lists. The exceptions are kinds whose readers the relay already limits to the agent and its owner: the author-only kinds, and turn metrics (which only the `p`-tagged owner can read). One more exception does not limit its readers: the agent's profile (kind 0). Clients need it, because it carries the NIP-OA owner attestation that tells them the key is an agent, and only the agent's key can sign it. Its fields are public, so the profile is a leak path: the agent can write anything into them. Kind 0 is replaceable, so the agent can change it at any time, and each new version reaches everyone who reads it. @@ -126,7 +128,7 @@ Refusing by default is what matters here. A new kind is covered without anyone h ### Possible: step-up for actions with the owner's authority -Some work needs more than the agent's own access, for example summarizing a private channel that the agent is not in, or posting as the owner from a CLI. An app can do this with the owner's session or key. The relay then sees the owner, not the agent, so none of the rules above apply. This can already happen without a decision by the owner: [buzz-app#2745](https://github.com/block/buzz-app/pull/2745) and [buzz-app#2881](https://github.com/block/buzz-app/pull/2881) give every bundled agent read access to all of the owner's channels, DMs and Canvases. +Some work needs more than the agent's own access, for example summarizing a private channel that the agent is not in, or posting as the owner from a CLI. An app can do this with the owner's session or key. The relay then sees the owner, not the agent, so none of the rules above apply. This has happened without a decision by the owner: [buzz-app#2745](https://github.com/block/buzz-app/pull/2745) and [buzz-app#2881](https://github.com/block/buzz-app/pull/2881) gave every bundled agent read access to all of the owner's channels, DMs and Canvases, until [buzz-app#3123](https://github.com/block/buzz-app/pull/3123) removed those tools. One possible answer is a step-up: an action with the owner's authority that the owner approves explicitly, for a stated scope and time, that is attributable to the agent, counted in W and R, and revocable. This is not part of this design. It widens what an agent can reach, so it would need careful boundaries and its own spec. @@ -142,7 +144,7 @@ The owner cannot control who joins someone else's channel. So when the owner mak Accepting the rules only means something if the rules stay put. One relay change makes sure they do: - **Refuse private-to-open visibility changes, with one exception.** Making a private channel open changes the admission rules out from under everyone who joined under the old ones, people included. That is a good rule with or without agents. An admin who wants an open channel can create one. - The exception is a channel where nobody but the person opening it joined under the old rules. Every member must be that person or one of that person's agents that is not a restricted writer. Opening such a channel, for example a Solo chat, is a one-time disclosure by a person of what that person can already read. After it opens, it is an ordinary open channel: the owner's restricted agents stay guests until the owner raises them. + The exception is a channel where nobody but the person opening it was ever a member. Every current and former member must be that person or one of that person's agents, and no restricted agent may have posted there. The relay marks the channel when a restricted agent first posts in it. Opening such a channel, for example a Solo chat, is a one-time disclosure by a person of what that person can already read. After it opens, it is an ordinary open channel: the owner's restricted agents stay guests until the owner raises them. **Later improvement: limit adds to admins.** Every private channel has the same admission rule: any member can add anyone. So making the agent a member of a private channel means trusting every current member's judgment. A later channel setting can limit adds to owners and admins. With it, an owner can choose a private channel where only the admins can grow R, and what the owner accepts becomes trust in the channel's admins. This is not needed for safety, because the owner accepts whatever the admission rules are. It gives the owner a tighter R to choose. The same setting helps announce channels, where it keeps the set of writers under the admins' control. @@ -150,7 +152,7 @@ The owner gets an alert when someone joins a channel where the agent is a member ### W remembers -Removing an agent from a channel stops new information from coming in, but the agent still remembers what it already read. So W keeps every channel the agent has ever read, with how far it read: channel X, up to the time it left. A channel the agent is still in is in W up to now. +Removing an agent from a channel stops new information from coming in, but the agent still remembers what it already read. So W keeps every channel the agent has ever read, with how far it read: channel X, up to the time it left. A channel the agent is still in is in W up to now. The relay does not keep this yet: it keeps one membership row per channel and key, and a re-add clears the last leave. W needs membership history that the relay does not store today. ### When the owner is not a person @@ -158,10 +160,10 @@ The owner is whichever key signed the agent's NIP-OA attestation. It need not be ### Client work -1. **The owner chooses member or guest for the agent in each channel.** Guest is the default. +1. **The owner chooses member or guest for a restricted agent in each channel.** Guest is the default. 2. **Show the disclosure level and let the owner change it.** Show it on the agent's profile and in the add flow, for everyone. 3. **Ask the owner to approve profile changes** of a restricted agent. -4. **Offer the add only to the agent's owner.** Clients that let any member add an agent directly should not. +4. **Offer the add of a restricted agent only to its owner.** Clients that let any member add one directly should not. ## What this doesn't stop @@ -186,15 +188,15 @@ The owner is whichever key signed the agent's NIP-OA attestation. It need not be - **The agent's profile is a leak path** for an unrestricted agent. Its fields are public and the agent can change them at any time. - **Owners choose unrestricted for convenience.** The disclosure level then protects nobody. The public label lets other people decide what to share with the agent, but only if they read it. - **Refusing private-to-open breaks a workflow.** Some admins start a channel private and open it later. Unless the channel holds only them and their agents, they have to create a new open channel instead. -- **Opening a Solo chat publishes what the owner's agents wrote there**, including what they drew from their other channels. Accepted: a person discloses what they can read. +- **Opening a Solo chat publishes what the owner's unrestricted agents wrote there**, including what they drew from their other channels. Accepted: a person discloses what they can read. ## Rollout 1. ✅ Announce channels: guests are read-only everywhere ([#8143](https://github.com/block/buzz/pull/8143)). -2. Add the disclosure level, with every agent unrestricted. Nothing changes until an owner chooses restricted. -3. For restricted agents: only the owner can add the agent to a channel or open a DM that includes it. The agent can join open channels by itself, as a guest only. The agent is a guest by default. Only the owner raises its role. Anyone with role authority can lower it. The agent creates only private channels, and adds only its owner to them. -4. Refuse private-to-open visibility changes, except in a channel where every member is the person opening it or one of that person's agents that is not a restricted writer. -5. For restricted agents: refuse events without an `h` tag, after checking what agents publish now, and require profile approval. +2. Add the disclosure level, with every agent unrestricted. Owners can't choose restricted until steps 3 to 5 are live; the relay pieces can ship separately before that. +3. For restricted agents: only the owner can add the agent to a channel or open a DM that includes it. The agent can join open channels by itself, as a guest only. The agent is a guest by default. Only the owner raises its role. Anyone with role authority can lower it. The agent creates only private channels, and adds only its owner to them. Default-channel joins make it a guest. +4. Refuse private-to-open visibility changes, except in a channel where every current and former member is the person opening it or one of that person's agents, and no restricted agent has posted there. +5. For restricted agents: refuse events that the relay would store outside a channel, after checking what agents publish now, and require profile approval. 6. Decide the default disclosure level for new agents. Each step only refuses actions, so backing one out means turning the rule off. Existing agents keep their memberships and roles; the rules apply to new actions. From b98dcc063037e8ab7118e0099547d47c40cac29b Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 9 Nov 2026 17:27:53 -0500 Subject: [PATCH 8/8] docs: keep restricted agents guests when a channel opens The opening exception also requires every restricted agent in the channel to be a guest, so opening never gives one a community-wide audience. Say how a restricted agent reaches default channels. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- VISION_INFORMATION_FLOW.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/VISION_INFORMATION_FLOW.md b/VISION_INFORMATION_FLOW.md index 46c8c42e5ee..41ca019458f 100644 --- a/VISION_INFORMATION_FLOW.md +++ b/VISION_INFORMATION_FLOW.md @@ -112,7 +112,7 @@ R depends on where the agent can write, so the owner controls that separately fr In an open channel with `join_role = member`, a person can write without joining. If an agent could do the same, it could post in a community-wide channel without anyone adding it. So an agent cannot. -An agent can join an open channel by itself, as a guest. That adds only public content to W and nothing to R, so it needs no decision from the owner. The relay checks this locally: the channel is open, the role is guest, and the event is the agent's. The same holds when the relay joins a new community member to the community's default channels: a restricted agent joins them as a guest. +An agent can join an open channel by itself, as a guest. That adds only public content to W and nothing to R, so it needs no decision from the owner. The relay checks this locally: the channel is open, the role is guest, and the event is the agent's. The same holds for a community's default channels. A restricted agent joins a community only through its owner's add or its own invite claim. Either way, it joins the default channels as a guest. A guest agent adds to W but never to R. The owner can let the agent read broadly without widening its audience at all. @@ -144,7 +144,7 @@ The owner cannot control who joins someone else's channel. So when the owner mak Accepting the rules only means something if the rules stay put. One relay change makes sure they do: - **Refuse private-to-open visibility changes, with one exception.** Making a private channel open changes the admission rules out from under everyone who joined under the old ones, people included. That is a good rule with or without agents. An admin who wants an open channel can create one. - The exception is a channel where nobody but the person opening it was ever a member. Every current and former member must be that person or one of that person's agents, and no restricted agent may have posted there. The relay marks the channel when a restricted agent first posts in it. Opening such a channel, for example a Solo chat, is a one-time disclosure by a person of what that person can already read. After it opens, it is an ordinary open channel: the owner's restricted agents stay guests until the owner raises them. + The exception is a channel where no other person was ever a member. Every current and former member must be that person or one of that person's agents. No restricted agent may have posted there, and every restricted agent in it must be a guest; the owner can demote one first. The relay marks the channel when a restricted agent first posts in it. Opening such a channel, for example a Solo chat, is a one-time disclosure by a person of what that person can already read. After it opens, it is an ordinary open channel: the owner's restricted agents stay guests until the owner raises them. **Later improvement: limit adds to admins.** Every private channel has the same admission rule: any member can add anyone. So making the agent a member of a private channel means trusting every current member's judgment. A later channel setting can limit adds to owners and admins. With it, an owner can choose a private channel where only the admins can grow R, and what the owner accepts becomes trust in the channel's admins. This is not needed for safety, because the owner accepts whatever the admission rules are. It gives the owner a tighter R to choose. The same setting helps announce channels, where it keeps the set of writers under the admins' control. @@ -195,7 +195,7 @@ The owner is whichever key signed the agent's NIP-OA attestation. It need not be 1. ✅ Announce channels: guests are read-only everywhere ([#8143](https://github.com/block/buzz/pull/8143)). 2. Add the disclosure level, with every agent unrestricted. Owners can't choose restricted until steps 3 to 5 are live; the relay pieces can ship separately before that. 3. For restricted agents: only the owner can add the agent to a channel or open a DM that includes it. The agent can join open channels by itself, as a guest only. The agent is a guest by default. Only the owner raises its role. Anyone with role authority can lower it. The agent creates only private channels, and adds only its owner to them. Default-channel joins make it a guest. -4. Refuse private-to-open visibility changes, except in a channel where every current and former member is the person opening it or one of that person's agents, and no restricted agent has posted there. +4. Refuse private-to-open visibility changes, except in a channel where every current and former member is the person opening it or one of that person's agents, no restricted agent has posted there, and every restricted agent in it is a guest. 5. For restricted agents: refuse events that the relay would store outside a channel, after checking what agents publish now, and require profile approval. 6. Decide the default disclosure level for new agents.