Skip to content

feat(workflow): identity-safe assign_agent action (Slice 1) - #6060

Open
mfethe1 wants to merge 1 commit into
block:mainfrom
mfethe1:feat/workflow-assign-agent
Open

feat(workflow): identity-safe assign_agent action (Slice 1)#6060
mfethe1 wants to merge 1 commit into
block:mainfrom
mfethe1:feat/workflow-assign-agent

Conversation

@mfethe1

@mfethe1 mfethe1 commented Aug 16, 2026

Copy link
Copy Markdown

Summary

Adds an identity-safe assign_agent workflow action so agent dispatch binds to an immutable hex pubkey instead of a reverse-parsed @Name mention. Two structural failure modes of the mention path go away: (a) two channel members sharing a display name silently wake no one; (b) a rename silently rewrites the target.

Base: d8281b9c93395f15d55091b131bb2747a0a3da8a (origin/main)
Head: 6e7044ac62d81d98aa7560dccfef3b093bee879d
Originating conversation: Buzz channel 4108b496-0efb-4fc6-85e3-6c88defb467c (autonomous-collaboration Slice 1)

What ships

  • Schema (crates/buzz-workflow/src/schema.rs) — new ActionDef::AssignAgent { agent_pubkey, text, channel?, task_id? }. Definition-time validation accepts either a static 64-char lowercase hex pubkey or a single {{...}} template placeholder (e.g. {{trigger.author}}); mixed literal+template strings are rejected so a stray name cannot smuggle an identity in. text non-empty; task_id/channel UUID-shaped when set.
  • Action sink (crates/buzz-workflow/src/action_sink.rs) — extends ActionSink with assign_agent(...) (no default impl — a compile-time obligation on every implementor) and adds ActionSinkError::AssigneeNotMember.
  • Executor (crates/buzz-workflow/src/executor.rs) — new match arm resolves templates in text, channel, task_id, and agent_pubkey. Re-validates the resolved agent_pubkey as 64-char lowercase hex before dispatch; a resolved template that didn't match is caught here, not silently misrouted.
  • Relay sink (crates/buzz-relay/src/workflow_sink.rs)RelayActionSink::assign_agent builds a kind:9 message with exactly two p tags: workflow owner (attribution) + agent_pubkey (wake). Text is never reverse-parsed for @Name. Membership-checks the assignee against get_members and returns AssigneeNotMember fail-closed if they are not in the channel.

Design corrections applied (per review)

  • Singular assignee (agent_pubkey: String), not to: Vec<String>. One dispatch wakes one identity; group fan-out is a separate explicit step.
  • No prose reverse-parse on assign_agent text — the identity-safe path stays identity-safe.
  • No default ActionSink impls — compile-time exhaustiveness over runtime "unimplemented".
  • No run-event emission or #run filtering in this PR. Nostr generic-tag filters are single-letter and #run is not a valid one. Run history will be wired to the existing DB-backed GET /workflows/{workflow_id}/runs endpoint in a follow-up PR (independent of this one).

Non-goals for this slice

Task leases/claims, reviewer-independence enforcement, exact-head binding, availability-aware reassignment, hop/budget/terminal-state protocol, workflow approval suspend/resume (WF-08). Also no change to PR #5983 lanes.

Diff shape

File +lines Notes
crates/buzz-workflow/src/schema.rs +287 variant + validation + 11 tests
crates/buzz-workflow/src/action_sink.rs +39 trait method + error variant
crates/buzz-workflow/src/executor.rs +172 dispatch arm + template + 3 tests
crates/buzz-relay/src/workflow_sink.rs +386 sink impl + 2 postgres-gated tests

Test plan

  • cargo fmt -p buzz-workflow -p buzz-relay -- --check — clean
  • cargo clippy -p buzz-workflow -p buzz-relay --tests --all-features -- -D warnings — clean
  • cargo test -p buzz-workflow --lib — 169 passed, 0 failed, 2 ignored
  • cargo test -p buzz-relay --lib workflow_sink — 17 passed, 0 failed, 3 ignored (2 new + 1 pre-existing postgres-gated)
  • Postgres-gated integration tests (cargo test -p buzz-relay --lib workflow_sink -- --ignored) — authored, not runtime-verified in this env (no accessible local Postgres). Includes the duplicate-name repro (two "Winnie" members, only selected wakes) and non-member fail-closed.
  • Live-relay end-to-end (reviewer): create a workflow with an assign_agent step targeting one of two same-name members in a test channel; trigger; confirm exactly one wake.

Unrelated pre-existing failures in buzz-relay --lib (api::git::policy::tests::bash_hmac_matches_rust_hmac, api::admin::tests::feedback_attachment_rejects_unknown_feedback) reproduce on the clean d8281b9c9 base and are not caused by this change.

Replaces prose-inferred agent dispatch (via `send_message` reverse-parsing
of `@Name` mentions) with a pubkey-addressed workflow action. Two failure
modes of the mention path are structural: (a) two channel members sharing
a display name make the mention ambiguous and wake no one; (b) a rename
silently rewrites the target. `assign_agent` binds dispatch to the
target's hex pubkey and membership-checks at emit time.

Contract per Airy's Slice-1 review:
- Singular `agent_pubkey` — group fan-out is a separate explicit step.
- The relay sink emits exactly two `p` tags: workflow owner (attribution)
  and `agent_pubkey` (wake). Text is NEVER reverse-parsed for `@Name`.
- Fail-closed: `AssigneeNotMember` if the agent is not a channel member.
- Schema accepts a static 64-hex pubkey OR a single `{{...}}` template
  placeholder (e.g. `{{trigger.author}}`); mixed literal+template strings
  are rejected so a stray name cannot smuggle an identity in. The resolved
  value is re-validated as 64-char lowercase hex at dispatch time.
- No default `ActionSink` impls; the new `assign_agent` method is a hard
  compile-time obligation on any implementor.

Tests: 11 schema tests (parse/round-trip/hex-shape rejects), 3 executor
template-resolution tests, 2 postgres-gated relay-sink integration tests
covering the duplicate-name repro (two "Winnie" members, only the
selected pubkey wakes) and the non-member fail-closed path.

Non-goals for this slice (per Airy): task leases, reviewer-independence,
approval suspend/resume, run-event emission. Run history is addressed
separately by wiring the CLI to the existing DB-backed
`GET /workflows/{id}/runs` endpoint in a follow-up PR.

Signed-off-by: Michael Feth <michael@jira-flow.com>
@mfethe1
mfethe1 requested a review from a team as a code owner August 16, 2026 16:42

@themiguelamador themiguelamador left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. The identity-safe action is the right direction, but the submitted head does not yet preserve its advertised single-assignee and template contracts.

Findings:

  • P1 — owner attribution can wake a second agent. ACP treats every p tag as a wake target. Emitting p(owner) plus p(assignee) therefore wakes the workflow owner too whenever the owner is a managed agent. Owner attribution must use Buzz's relay-trusted actor tag, leaving exactly one p tag for the assignee.
  • P1 — templated routing fields cannot be saved, and resolved task IDs are not checked. The executor resolves templates in channel and task_id, but schema validation rejects those same templates. The executor test bypasses WorkflowDef::validate and uses a non-UUID event ID as a task ID, masking the mismatch. A resolved task_id can consequently reach the sink without the UUID guarantee documented by the action.
  • P2 — identity shape checks accept values the runtime rejects. Both the pubkey and single-template validators trim only for validation, then retain the padded original value. Inputs such as " <64-hex>" and " {{trigger.author}}" save successfully and fail later. The template check also accepts malformed nested/triple-brace forms.
  • P2 — the public sink boundary trusts malformed correlation IDs. A direct ActionSink::assign_agent caller can emit a non-UUID task tag (and the submitted implementation silently drops an empty one), despite the public contract claiming a UUID. The sink needs its own validation/canonicalization.
  • P2 — action failures are reported as webhook_failed. ActionSinkError maps to WorkflowError::WebhookError, so a removed assignee or invalid assignment is persisted with the wrong stable error code. Database failures also need to retain their database classification.
  • P2 — assignment text is copied into an info log. Task content may contain sensitive incident/customer data; the new action should log routing metadata without duplicating the full message body.
  • P3 — diagnostics and docs describe the wrong action/contract. Shared channel resolution reports SendMessage for AssignAgent; docs claim exactly two p tags even though the implementation deduplicates owner=assignee; and the parse_all_action_types coverage omits the new variant.
  • P3 — PR metadata violates this repository's attribution rule. The Generated with Claude Code footer must be removed. I attempted to remove only that footer, but the reviewer account cannot edit another author's PR description.

I prepared the complete fix as 626849e7 on Complear:review/pr-6060-fix. It uses actor for attribution and a sole assignee p tag, aligns definition/runtime validation for all templated identity fields, validates UUIDs at both executor and sink boundaries, preserves action/database error classifications, removes message content from the new info log, and adds regression coverage.

Verification on the fix:

  • cargo test -p buzz-workflow --lib: 173 passed, 2 Postgres-gated ignored
  • cargo test -p buzz-relay --lib workflow_sink: 18 passed, 3 Postgres-gated ignored
  • cargo test -p buzz-relay --lib workflow_sink -- --ignored --test-threads=1 against an isolated fully migrated database: 3 passed
  • strict clippy for buzz-workflow + buzz-relay: passed
  • cargo doc -p buzz-workflow --no-deps: passed
  • full relay library suite: 878 passed; its two unrelated global-state/timing failures both passed on exact isolated rerun
  • formatting and git diff --check: passed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants