feat(workflow): identity-safe assign_agent action (Slice 1) - #6060
Open
mfethe1 wants to merge 1 commit into
Open
feat(workflow): identity-safe assign_agent action (Slice 1)#6060mfethe1 wants to merge 1 commit into
mfethe1 wants to merge 1 commit into
Conversation
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>
themiguelamador
suggested changes
Aug 16, 2026
themiguelamador
left a comment
There was a problem hiding this comment.
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
ptag as a wake target. Emittingp(owner)plusp(assignee)therefore wakes the workflow owner too whenever the owner is a managed agent. Owner attribution must use Buzz's relay-trustedactortag, leaving exactly oneptag for the assignee. - P1 — templated routing fields cannot be saved, and resolved task IDs are not checked. The executor resolves templates in
channelandtask_id, but schema validation rejects those same templates. The executor test bypassesWorkflowDef::validateand uses a non-UUID event ID as a task ID, masking the mismatch. A resolvedtask_idcan 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_agentcaller can emit a non-UUIDtasktag (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.ActionSinkErrormaps toWorkflowError::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
SendMessageforAssignAgent; docs claim exactly twoptags even though the implementation deduplicates owner=assignee; and theparse_all_action_typescoverage omits the new variant. - P3 — PR metadata violates this repository's attribution rule. The
Generated with Claude Codefooter 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 ignoredcargo test -p buzz-relay --lib workflow_sink: 18 passed, 3 Postgres-gated ignoredcargo test -p buzz-relay --lib workflow_sink -- --ignored --test-threads=1against 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an identity-safe
assign_agentworkflow action so agent dispatch binds to an immutable hex pubkey instead of a reverse-parsed@Namemention. 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:
6e7044ac62d81d98aa7560dccfef3b093bee879dOriginating conversation: Buzz channel
4108b496-0efb-4fc6-85e3-6c88defb467c(autonomous-collaboration Slice 1)What ships
crates/buzz-workflow/src/schema.rs) — newActionDef::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.textnon-empty;task_id/channelUUID-shaped when set.crates/buzz-workflow/src/action_sink.rs) — extendsActionSinkwithassign_agent(...)(no default impl — a compile-time obligation on every implementor) and addsActionSinkError::AssigneeNotMember.crates/buzz-workflow/src/executor.rs) — new match arm resolves templates intext,channel,task_id, andagent_pubkey. Re-validates the resolvedagent_pubkeyas 64-char lowercase hex before dispatch; a resolved template that didn't match is caught here, not silently misrouted.crates/buzz-relay/src/workflow_sink.rs) —RelayActionSink::assign_agentbuilds akind:9message with exactly twoptags: workflow owner (attribution) +agent_pubkey(wake). Text is never reverse-parsed for@Name. Membership-checks the assignee againstget_membersand returnsAssigneeNotMemberfail-closed if they are not in the channel.Design corrections applied (per review)
agent_pubkey: String), notto: Vec<String>. One dispatch wakes one identity; group fan-out is a separate explicit step.assign_agenttext — the identity-safe path stays identity-safe.ActionSinkimpls — compile-time exhaustiveness over runtime "unimplemented".#runfiltering in this PR. Nostr generic-tag filters are single-letter and#runis not a valid one. Run history will be wired to the existing DB-backedGET /workflows/{workflow_id}/runsendpoint 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
crates/buzz-workflow/src/schema.rscrates/buzz-workflow/src/action_sink.rscrates/buzz-workflow/src/executor.rscrates/buzz-relay/src/workflow_sink.rsTest plan
cargo fmt -p buzz-workflow -p buzz-relay -- --check— cleancargo clippy -p buzz-workflow -p buzz-relay --tests --all-features -- -D warnings— cleancargo test -p buzz-workflow --lib— 169 passed, 0 failed, 2 ignoredcargo test -p buzz-relay --lib workflow_sink— 17 passed, 0 failed, 3 ignored (2 new + 1 pre-existing postgres-gated)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.assign_agentstep 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 cleand8281b9c9base and are not caused by this change.