Skip to content
Merged
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
3 changes: 2 additions & 1 deletion docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 38 additions & 5 deletions src/features/agent-harness/kit.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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: () => ({
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 2 additions & 0 deletions src/features/agent-harness/kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Loading