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(); }