From 82a496bc062679ecc58ff59956350d7b08ff27d5 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Wed, 4 Nov 2026 11:58:11 -0500 Subject: [PATCH] test(agents): refuse a real relay workflow mention in the harness kit The kit test "refuses a mention from anyone else, and triggers that are not its" fails on main. Its workflow fixture was a message signed by the owner with a `buzz:workflow` tag. #2364 wrote it against #2360, whose `fromOwner` refused any event with that tag. #2399 merged first and restored the older `fromOwner`, which admits any event the owner signs, so the fixture became an owner message and the kit admitted it. The kit itself still calls `fromOwner` without a workflow check, so a real relay workflow mention is refused. Make the fixture that real mention: signed by the community's workflow key, owned by the owner and aimed at this agent, with the workflow key set on the session. The bundled harnesses admit this event; the kit must not. Wiring the bundled workflow check into the kit makes the test fail. Before the refusal, assert that the bundled check admits the fixture, so the negative case cannot pass for the wrong reason again. Also assert that an owner mention carrying only a `buzz:workflow` marker is still admitted: the marker neither grants authority nor revokes the owner's own. Say at the call that the missing workflow check is deliberate, and fix the workflow docs, which named only Claude Code and Codex. Signed-off-by: Logan Johnson Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 --- docs/workflows.md | 3 +- src/features/agent-harness/kit.test.ts | 43 +++++++++++++++++++++++--- src/features/agent-harness/kit.ts | 2 ++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/docs/workflows.md b/docs/workflows.md index 711735de2c..0e4a2ca899 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -227,7 +227,8 @@ The owner tag identifies the identity that scheduled the workflow, not the autho of every rendered word. Templates can include trigger-controlled values; those values do not acquire the owner's prompt authority. -Claude Code and Codex also use this proof to authorize delegated workflow mentions. +Claude Code, Codex, Pi and Goose also use this proof to authorize delegated +workflow mentions. Agent types that plugins bring do not. The receiving agent's ready community session must supply the explicit relay authority. The workflow owner must be that agent's owner, or an agent whose admitted kind-0 profile cryptographically proves the same owner through NIP-OA. diff --git a/src/features/agent-harness/kit.test.ts b/src/features/agent-harness/kit.test.ts index 1b0da28f0c..1f1bcba444 100644 --- a/src/features/agent-harness/kit.test.ts +++ b/src/features/agent-harness/kit.test.ts @@ -1,14 +1,17 @@ import { describe, expect, it, vi } from "vitest"; import { RESUMED_PROMPT } from "../../buzz-mcp/prompt"; +import { fromOwner } from "../agents/owner-attestation"; import type { AgentHandle, Delivery } from "../agents/service"; -import type { EventData, RelayEvent } from "../relay/events"; +import { eventDto, type EventData, type RelayEvent } from "../relay/events"; import type { RelayData } from "../relay/service"; +import { keypair, signed } from "../relay/testing"; import { harnessKit } from "./kit"; const self = "c".repeat(64); const alice = "a".repeat(64); const mallory = "f".repeat(64); const channel = "chan-1"; +const workflowKey = keypair(); const message = ( id: string, @@ -77,6 +80,7 @@ function setup( channelWrites: { startingCanvas: () => undefined }, canvas: { available: false, read: vi.fn(async () => null) }, agentMemories: { open: vi.fn() }, + workflowAuthority: workflowKey.pubkey, }; const relay = { snapshot: () => ({ @@ -175,18 +179,47 @@ describe("harnessKit.conversation", () => { expect( await kit.conversation(delivery({ type: "mention", event }), "thread"), ).toBeUndefined(); - const workflow = message("3", "@Ampy go", { + // A relay workflow mention the bundled harnesses would run: signed by + // the community's workflow key, owned by the owner, aimed at this agent. + const workflow = eventDto( + signed(workflowKey, { + kind: 9, + content: "@Ampy go", + tags: [ + ["h", channel], + ["p", self], + ["buzz:workflow", "true"], + ["buzz:workflow-owner", alice], + ["buzz:workflow-mention", self], + ], + }), + ); + expect( + await fromOwner(workflow, alice, { + agent: self, + authority: workflowKey.pubkey, + }), + ).toBe(true); + expect( + await kit.conversation( + delivery({ type: "mention", event: workflow }), + "thread", + ), + ).toBeUndefined(); + // A workflow marker does not revoke the owner's own mention. + const marked = message("3", "@Ampy go", { tags: [ ["h", channel], - ["buzz:workflow", "w"], + ["p", self], + ["buzz:workflow", "true"], ], }); expect( await kit.conversation( - delivery({ type: "mention", event: workflow }), + delivery({ type: "mention", event: marked }), "thread", ), - ).toBeUndefined(); + ).toBeDefined(); expect( await kit.conversation( delivery({ diff --git a/src/features/agent-harness/kit.ts b/src/features/agent-harness/kit.ts index dac6a8b483..8538697926 100644 --- a/src/features/agent-harness/kit.ts +++ b/src/features/agent-harness/kit.ts @@ -221,6 +221,8 @@ export function harnessKit(sources: Sources): HarnessKit { // for the owner. const wake = trigger.type !== "mention" ? trigger : undefined; if (trigger.type === "mention") { + // Only the owner or an attested sibling. Unlike the bundled harnesses, + // which kept relay workflow mentions for compatibility, no workflow check. if (!(await fromOwner(trigger.event, agent.owner))) return undefined; signal.throwIfAborted(); }