Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions docs/buzz-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -957,11 +957,6 @@ available rather than silently discarding it.
- On desktop, Buzz starts every agent with `BUZZ_VERSION` set to the app's
version, and agents usually pass it on to the shell commands they run. A
script or shell config can check for it to tell it is running under Buzz.
- In communities where your build restricts workflows, workflow messages do
not automatically start or steer local Claude, Codex, Goose, or Pi agents. To
have one act on a workflow result, send it a direct request explaining what
to do with that result. Other communities still let a workflow you or your
agents created mention your agents.
- **Agents** is where people create and manage agents. For supported types,
open an agent there and use **Identity → Instructions**. Opening its avatar or
name in a conversation shows its profile, where **Settings → Instructions**
Expand Down Expand Up @@ -1537,6 +1532,35 @@ matches the displayed participant name. When no participant name is available, B
public-key prefix. A direct message with yourself is labeled **Notes to self**.
These labels only help identify the conversation you select.

## When your agent ignores a mention

Your agents act only on messages from you, from your other agents, and from
workflows that you or your agents own and that direct them. When your own
workflow posts a message that mentions one of your agents on this computer but
doesn’t direct it, a short note under the message says so. On a relay that uses
owner-only agent mentions it reads: **“‹Agent› didn’t respond to this workflow
message. This community limits which workflows can use agents, so other people
can’t use yours. The workflow’s Runs tab shows why it didn’t respond this
time.”** Elsewhere it reads: **“‹Agent› didn’t respond because this workflow
message wasn’t set up to direct agents.”** Only you see it, on this device;
nothing is posted.
Other ignored mentions show no note.

On a relay that uses owner-only agent mentions, the desktop workflow editor
warns when a message step mentions an agent in the channel:

Each warning ends with “This community limits which workflows can use agents,
so other people can’t use yours.”

- **“Agents you mention respond only when you start this workflow, not when
someone else does.”** The workflow starts on a message, reaction or diff.
- **“Agents won’t respond to workflows started by a webhook.”** It starts on a
webhook, whatever its steps.
- **“Agents won’t respond to this workflow because it has steps other than Send
Message and Delay.”** It has a step other than a message or a delay.

A schedule with only message and delay steps shows no warning.

## Deleting an agent’s workflow

In **Workflows → Owner → All channel workflows**, open the workflow’s actions
Expand Down
13 changes: 0 additions & 13 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ Build defaults are readable binary data, **never secret storage**.
| Input | Consumer and boundary |
| --- | --- |
| `BUZZ_EXTERNAL_MCP_COMMUNITIES` | Frontend build: space-separated secure community origins allowed to authorize external MCP access. Empty disables access. See below. |
| `BUZZ_WORKFLOW_RESTRICTED_COMMUNITIES` | Frontend build: space-separated secure community origins where workflow messages cannot start or steer local agents. Empty keeps checked workflow delegation everywhere. See below. |
| `BUZZ_BUILDERLAB_URL` | Shared frontend/native build: the live development broker's Builderlab, and whether a build without an added sign-in server keeps the built-in one; no runtime override. See below. |
| `BUZZ_BUILDERLAB_APPS_DOMAIN` | Shared frontend/native build: exact Apps cookie domain paired with the login origin for native viewer SSO; no runtime override. |
| `BUZZ_BUILDERLAB_APPS_CONTROL_PLANE` | Shared frontend/native build: HTTPS origin of the Apps control plane for the configured login origin, for deployments the built-in public table does not list; no runtime override. |
Expand Down Expand Up @@ -48,18 +47,6 @@ repository's workflow sets none. Community names do not grant access. The Agents
opening a connection and on each tool call. Consent disables other communities,
and Settings hides External apps there. Normal agent MCP servers are unchanged.

## Workflow restriction community flag

`BUZZ_WORKFLOW_RESTRICTED_COMMUNITIES` is a space-separated list of `https://`
or `wss://` community origins, parsed as the external flag is. The default is
empty: in every community, a workflow mention can start or steer a local Claude,
Codex, Goose or Pi agent when its creator is the agent's owner or a sibling the
owner attested. Each build lists its own restricted community origins. In a listed community no
workflow-marked message authorizes a local agent request, in bundled or plugin
harnesses; the message stays visible. While any community is listed, an agent
whose community cannot be read is restricted too. See
[workflows](workflows.md).

## Packaged community default

Internal packagers can set a public community origin for fresh installs:
Expand Down
3 changes: 2 additions & 1 deletion docs/unread.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,8 @@ channel-mute mention exception. Ordinary unread, DM and participating-thread
rules still apply. No message or read marker is rewritten.

The relay currently emits `buzz:workflow-mention` only for recipients named in
the stored template as well as the rendered output. If substituted input alone
the stored template as well as the rendered output, and a relay running
[owner-only agent mentions](https://github.com/block/buzz/blob/main/docs/operator-workflow-agent-mentions.md) may leave it off, so no agent acts on the message. If substituted input alone
names the owner, its single `p` tag cannot distinguish that mention from owner
attribution and does not create mention attention. Put the owner's explicit
`@Name` in the template when they should be alerted. Distinguishing substituted
Expand Down
15 changes: 4 additions & 11 deletions docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,21 +256,14 @@ The event must contain a canonical `buzz:workflow-mention` for the receiving
agent; all such tags
must have exactly two fields, a lowercase-hex pubkey, and no duplicate targets.
The relay emits these tags only for targets present in the owner-authored template
and the rendered output. Legacy `p` tags route mentions but do not authorize them.
and the rendered output. A relay running
[owner-only agent mentions](https://github.com/block/buzz/blob/main/docs/operator-workflow-agent-mentions.md) may leave the tag off; the message still posts, but
no agent acts on it, and the owner's app notes under the message which of their
agents didn't respond. Legacy `p` tags route mentions but do not authorize them.
Workflows created and maintained under an owned agent's own key remain owned by
that agent and can wake agents with the same proven human owner. This does not
grant an agent permission to edit a workflow owned by a different signing identity.

Builds can withhold this delegation per community. In communities listed by
`BUZZ_WORKFLOW_RESTRICTED_COMMUNITIES`, no message carrying a `buzz:workflow` tag
authorizes a local agent request, whatever its signer, owner or mention tags, in
bundled or plugin harnesses. Workflow messages there stay visible but do not start
or steer local Claude, Codex, Goose or Pi agents, including for owner-created
static templates and scheduled workflows. Each build lists its own restricted
communities; communities it does not list keep the delegation above. The restriction
does not prevent workflow output from influencing a later authorized turn. See
[configuration](configuration.md#workflow-restriction-community-flag).

Cached-only startup intentionally shows the actual signer until a live connection
establishes explicit relay authority. Saved operator-contact `relayAuthor` is not
promoted to `self`, and no new trusted storage is introduced. Signer-based search
Expand Down
66 changes: 24 additions & 42 deletions src/bundled/claude-code/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ function setup(
status: "ready",
session,
viewer: alice,
origin: "https://agent.example",
scope: `https://agent.example:${alice}`,
}),
} as unknown as RelayData;
Expand Down Expand Up @@ -2247,47 +2246,30 @@ it.each([true, false])(
},
);

it.each([
["https://agent.example", false],
["https://other.example", true],
])(
"refuses an owner's workflow mention where the build restricts %s",
async (restricted, runs) => {
vi.stubEnv("VITE_BUZZ_WORKFLOW_RESTRICTED_COMMUNITIES", restricted);
const relay = keypair();
const event = eventDto(
signed(relay, {
kind: 9,
content: "@Claude workflow",
tags: [
["h", channel],
["p", self],
["buzz:workflow", "true"],
["buzz:workflow-owner", alice],
["buzz:workflow-mention", self],
],
}),
);
const f = setup([event], { workflowAuthority: relay.pubkey });
try {
await f.deliver({ type: "mention", event });
if (runs) {
await vi.waitFor(() =>
expect(f.claudes().some((process) => process.prompts.length)).toBe(
true,
),
);
} else {
await flush(10);
expect(f.claudes()).toHaveLength(0);
expect(f.workingUntil).not.toHaveBeenCalled();
}
} finally {
f.runtime.dispose();
vi.unstubAllEnvs();
}
},
);
it("gives the workflow reason for its owner's workflow mention that doesn't name it", async () => {
const relay = keypair();
const event = eventDto(
signed(relay, {
kind: 9,
content: "@Agent workflow",
tags: [
["h", channel],
["p", self],
["buzz:workflow", "true"],
["buzz:workflow-owner", alice],
],
}),
);
const f = setup([event], { workflowAuthority: relay.pubkey });
try {
const ignored = vi.fn();
await f.deliver({ type: "mention", event }, { ignored });
expect(ignored).toHaveBeenCalledExactlyOnceWith("workflow");
expect(f.claudes()).toHaveLength(0);
} finally {
f.runtime.dispose();
}
});

it.each([false, true])(
"handles sibling workflow authorization with cancellation=%s",
Expand Down
16 changes: 8 additions & 8 deletions src/bundled/claude-code/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import {
unsupportedSynced,
unverifiedSynced,
} from "../../features/agents/agent-config";
import { fromOwner, workflowOf } from "../../features/agents/owner-attestation";
import {
mentionAuthority,
workflowOf,
} from "../../features/agents/owner-attestation";
import type { TranscriptTurn } from "../../features/agents/activity-transcript";
import type { AgentReads } from "../../features/agents/reads";
import { localStore, type AgentStore } from "../../features/agents/store";
Expand Down Expand Up @@ -963,16 +966,13 @@ export class ClaudeRuntime {
// for the owner.
const wake = trigger.type !== "mention" ? trigger : undefined;
// Claude runs commands as you; mentions need owner or delegated authority.
if (
trigger.type === "mention" &&
!(await fromOwner(
if (trigger.type === "mention") {
const authority = await mentionAuthority(
trigger.event,
agent.owner,
workflowOf(this.reads(agent.key), agent.pubkey),
))
) {
ignored?.();
return;
);
if (authority !== "owner") return ignored?.(authority);
}
signal.throwIfAborted();
const entry = this.entry(agent, settings);
Expand Down
25 changes: 24 additions & 1 deletion src/bundled/codex/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2843,7 +2843,8 @@ it("ignores non-owner messages without spawning", async () => {
if ("event" in other.trigger) other.trigger.event.pubkey = "f".repeat(64);
const ignored = vi.fn();
await f.runtime.run({ ...other, ignored });
expect(ignored).toHaveBeenCalledOnce();
// No reason, so no note for its owner.
expect(ignored).toHaveBeenCalledExactlyOnceWith(undefined);
expect(f.spawn).not.toHaveBeenCalled();
expect(f.publish).not.toHaveBeenCalled();
});
Expand Down Expand Up @@ -2872,6 +2873,28 @@ it("runs a verified relay workflow mention for its owner", async () => {
await f.complete();
expect(f.publish).toHaveBeenCalled();
});
it("gives the workflow reason for its owner's workflow mention that doesn't name it", async () => {
const f = fixture();
const relay = keypair();
f.snapshot.session.workflowAuthority = relay.pubkey;
const ask = f.delivery("@Codex workflow");
const event = eventDto(
signed(relay, {
kind: 9,
content: "@Agent workflow",
tags: [
["h", "channel"],
["p", pubkey],
["buzz:workflow", "true"],
["buzz:workflow-owner", owner],
],
}),
);
const ignored = vi.fn();
await f.runtime.run({ ...ask, trigger: { type: "mention", event }, ignored });
expect(ignored).toHaveBeenCalledExactlyOnceWith("workflow");
expect(f.spawn).not.toHaveBeenCalled();
});
it.each([false, true])(
"handles sibling workflow authorization with cancellation=%s",
async (cancelled) => {
Expand Down
16 changes: 8 additions & 8 deletions src/bundled/codex/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { isFinalMessage } from "../../buzz-mcp/session-tools";
import type { WorkOutcome } from "../../features/agents/local-activity";
import type { AgentProtection } from "../../features/agent-protection/service";
import { nativeProtectionBridge } from "../../features/agent-protection/native-bridge";
import { fromOwner, workflowOf } from "../../features/agents/owner-attestation";
import {
mentionAuthority,
workflowOf,
} from "../../features/agents/owner-attestation";
import {
type ConversationWake,
wakeAnchor,
Expand Down Expand Up @@ -1682,16 +1685,13 @@ export class CodexRuntime {
const event =
trigger.type !== "mention" ? wakeAnchor(trigger) : trigger.event;
const reads = this.reads(agent.key);
if (
trigger.type === "mention" &&
!(await fromOwner(
if (trigger.type === "mention") {
const authority = await mentionAuthority(
trigger.event,
agent.owner,
workflowOf(reads, agent.pubkey),
))
) {
delivery.ignored?.();
return;
);
if (authority !== "owner") return delivery.ignored?.(authority);
Comment thread
wpfleger96 marked this conversation as resolved.
Resolved
}
delivery.signal.throwIfAborted();
if (!reads) throw new Error("Connect to the agent's community first");
Expand Down
28 changes: 28 additions & 0 deletions src/bundled/goose/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type { Agent, Delivery } from "../../features/agents/service";
import type { AgentProtection } from "../../features/agent-protection/service";
import type { Host } from "../../features/host/service";
import type { EventData, RelayEvent } from "../../features/relay/events";
import { eventDto } from "../../features/relay/events";
import { keypair, signed } from "../../features/relay/testing";
import type { RelayData } from "../../features/relay/service";
import { fakeSpawn, flush, gated } from "./goose-testing";
import { REQUEST_TIMEOUT_MS } from "./goose";
Expand Down Expand Up @@ -85,6 +87,8 @@ function setup(
slug?: string,
) => Promise<Memories>;
thinking?: NonNullable<Parameters<typeof fakeSpawn>[0]>["thinking"];
/** The community's relay workflow key. */
workflowAuthority?: string;
/** Wraps the fake's `spawn`, as `gated(…).wrap` does. */
wrap?: (spawn: ReturnType<typeof fakeSpawn>["spawn"]) => Host["spawn"];
}> = {},
Expand Down Expand Up @@ -132,6 +136,7 @@ function setup(
dispose: () => {},
};
const session = {
workflowAuthority: options.workflowAuthority,
read,
agentMemories: {
open: vi.fn(() => memories),
Expand Down Expand Up @@ -1740,6 +1745,29 @@ it.each([
}
});

it("gives the workflow reason for its owner's workflow mention that doesn't name it", async () => {
const relay = keypair();
const f = setup({ workflowAuthority: relay.pubkey });
const ignored = vi.fn();
await f.deliver(
eventDto(
signed(relay, {
kind: 9,
content: "@Agent workflow",
tags: [
["h", channel],
["p", self],
["buzz:workflow", "true"],
["buzz:workflow-owner", alice],
],
}),
),
{ ignored },
);
expect(ignored).toHaveBeenCalledExactlyOnceWith("workflow");
expect(f.goose()).toBeUndefined();
});

it("takes a wake with no thread only in a Solo chat", async () => {
const ignored = vi.fn();
const stream = setup();
Expand Down
29 changes: 11 additions & 18 deletions src/bundled/goose/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
turnNote,
} from "../../buzz-mcp/session-tools";
import {
fromOwner,
mentionAuthority,
type Agent,
type AgentHandle,
type Agents,
Expand Down Expand Up @@ -402,23 +402,16 @@ export class GooseRuntime {
// goose runs commands as you; mentions need owner or delegated authority.
if (trigger.type === "mention") {
const snapshot = this.relay(agent.key)?.snapshot();
if (
!(await fromOwner(trigger.event, agent.owner, {
agent: agent.pubkey,
community: snapshot?.origin,
authority:
snapshot?.status === "ready"
? snapshot.session.workflowAuthority
: undefined,
profiles:
snapshot?.status === "ready"
? snapshot.session.profiles
: undefined,
}))
) {
ignored?.();
return;
}
const authority = await mentionAuthority(trigger.event, agent.owner, {
agent: agent.pubkey,
authority:
snapshot?.status === "ready"
? snapshot.session.workflowAuthority
: undefined,
profiles:
snapshot?.status === "ready" ? snapshot.session.profiles : undefined,
});
if (authority !== "owner") return ignored?.(authority);
signal.throwIfAborted();
}
const entry = this.entry(agent.key, settings);
Expand Down
Loading
Loading