fix(workflows): let owners manage agent-created workflows - #6697
Conversation
Co-authored-by: Brother Darryl <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz> Signed-off-by: loganj <loganj@squareup.com>
Co-authored-by: Larry <larry@buzz.local> Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <larry@buzz.local> Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <larry@buzz.local> Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <larry@buzz.local> Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <larry@buzz.local> Signed-off-by: Logan Johnson <loganj@squareup.com>
f5b46ba to
fe0b5cb
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes requested. Two lifecycle guarantees in the PR description do not hold for the private-owner update path.
-
P1: stale/conflicting terminal completions are not replay-safe.
complete_workflow_owner_commandstorescoordinate_conflict_receiptorrevision_conflict_receiptwhen the workflow moves (crates/buzz-db/src/workflow.rs:1364-1443), but the terminal replay branch compares the stored receipt only with the ordinary caller-suppliedreceipt_event(crates/buzz-db/src/workflow.rs:1317-1326). The HTTP handler always passes that ordinary receipt in this slot (crates/buzz-relay/src/api/workflows.rs:564-616). After a lost response, replaying the identical signed completion therefore returns HTTP 409 rather than the already-recorded terminal result whenever the first attempt ended inworkflow_coordinate_changedorworkflow_revision_changed. Return the authenticated target agent’s stored terminal outcome, or compare against the matching deterministic conflict receipt, and add a regression covering stale first completion followed by an identical retry withtransitioned:falseand the same stored receipt ID. -
P1: a non-member owner cannot observe an async private-channel update’s terminal result. The new owner-target endpoint deliberately lets an immutable human owner manage an agent workflow without channel membership, but terminal receipts are persisted with the workflow’s
channel_id(crates/buzz-db/src/workflow.rs:1448-1462) and published only on that channel topic (crates/buzz-relay/src/api/workflows.rs:630-644). Generic WS/HTTP event reads constrain channel-scoped rows to accessible channels, including ID queries (crates/buzz-relay/src/handlers/req.rs:1063-1091,crates/buzz-relay/src/api/bridge.rs:1424-1437), whilebuzz workflows managereturns only the initialpending_agentresponse and exposes no owner-authorized status lookup. The update may apply or reject, but the headline private owner cannot learn which. Add an owner-gated, non-enumerating terminal-status path (or an equivalently private receipt projection) and cover a non-member owner reading both applied and rejected terminal outcomes.
Focused checks at exact head fe0b5cbfffc539b7847073aa4ee359ddd08b9e05 passed for the core wire parser, SDK builder, CLI manage argument validation, and managed webhook-secret preservation. Exact-head CI is green. Those checks do not exercise either failed lifecycle seam above.
Extract the pure workflow-delivery verification behavior preserved in PR #2737 into a dedicated, isolated module (node E of the workflow replacement tree) and centralize exact definition/message/owner/ channel/step/cause verification behind a typed result, built on the canonical delivery protocol vocabulary (node B, buzz_core::workflow_delivery). - crates/buzz-acp/src/verifier.rs: pure module, no I/O and no production caller. Callers supply already-fetched signed events. - Wake authentication adopts the canonical identifier-only WorkflowDeliveryWake (strict kind-24620 parse, relay authorship, signature, exact target). Wakes are hints: they name a delivery, never carry binding authority; every binding field is verified against signed authority instead of wake tags. - Target admission uses canonical message_v1_targets: only the four-field marker p-tag admits a durable delivery recipient; ordinary mentions never do, and malformed marker tags fail closed. - verify_workflow_delivery types failures: VerifyError::Mismatch (permanent - binding disagreement or forgery) vs VerifyError::Unavailable (transient - authority not supplied). Neither becomes dispatch. - The recorded canonical WorkflowDeliveryCause is re-verified against independent caller-fetched authority (CauseAuthority) for every cause class: an Event cause requires the exact signed event (id, signature, channel binding); a Schedule cause requires the durable schedule slot; a Webhook cause requires the durable invocation identity (the durable invocation record is owned by a later node and modeled here as the required opaque authority input). Absent authority is Unavailable; disagreeing or wrong-class authority is Mismatch. The claimed delivery row is never its own proof. - Mutation tests flip every binding field (workflow, run, step, channel, target, definition event, message event, cause - including schedule slot and webhook invocation id, rendered content) one at a time and assert permanent mismatch; dropped inputs - including dropped cause authority for every cause class - assert transient unavailability; wrong-class cause authority asserts mismatch. Dormant by contract: no polling, claims, dispatch, producer, or production reachability. PRs #2737 and #6697 are unchanged. Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Wakko <0999168a3b1a4dabdf398cf43ba2893bb29b74dc06d0b31c2d3b51dbebea1612@buzz.block.builderlab.xyz>
Extract the pure workflow-delivery verification behavior preserved in PR #2737 into a dedicated, isolated module (node E of the workflow replacement tree) and centralize exact definition/message/owner/ channel/step/cause verification behind a typed result, built on the canonical delivery protocol vocabulary (node B, buzz_core::workflow_delivery). - crates/buzz-acp/src/verifier.rs: pure module, no I/O and no production caller. Callers supply already-fetched signed events. - Wake authentication adopts the canonical identifier-only WorkflowDeliveryWake (strict kind-24620 parse, relay authorship, signature, exact target). Wakes are hints: they name a delivery, never carry binding authority; every binding field is verified against signed authority instead of wake tags. - Target admission uses canonical message_v1_targets: only the four-field marker p-tag admits a durable delivery recipient; ordinary mentions never do, and malformed marker tags fail closed. - verify_workflow_delivery types failures: VerifyError::Mismatch (permanent - binding disagreement or forgery) vs VerifyError::Unavailable (transient - authority not supplied). Neither becomes dispatch. - The recorded canonical WorkflowDeliveryCause is re-verified against independent caller-fetched authority (CauseAuthority) for every cause class, and durable authority must carry the source row's full binding identity, never just the scalar the delivery re-presents: an Event cause requires the exact signed event (id, signature, channel binding); a Schedule cause requires the durable scheduled_workflow_fires claim binding (community, workflow, slot, linked run - two workflows can fire the same second); a Webhook cause requires the durable invocation record binding (community, workflow, invocation id, linked run; the durable record itself is owned by a later node and modeled here as the required authority input). The delivery snapshot carries the server-resolved CommunityId for these comparisons. Absent authority is Unavailable; disagreeing, unbound, or wrong-class authority is Mismatch. The claimed delivery row is never its own proof. - Mutation tests flip every binding field (workflow, run, step, channel, target, definition event, message event, cause, rendered content) one at a time and assert permanent mismatch. Durable-cause coverage includes the exact matching row control plus one-field row mutations (different community, same-slot different workflow, different slot/invocation, different linked run, unattached run) for both Schedule and Webhook; dropped authority for every cause class asserts transient unavailability; wrong-class authority in all directions asserts mismatch. Dormant by contract: no polling, claims, dispatch, producer, or production reachability. PRs #2737 and #6697 are unchanged. Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Wakko <0999168a3b1a4dabdf398cf43ba2893bb29b74dc06d0b31c2d3b51dbebea1612@buzz.block.builderlab.xyz>
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent
Requesting changes on exact head fe0b5cbfffc539b7847073aa4ee359ddd08b9e05 against base 83e8b7620e588e369248e4d30b57be6db754b044. Three author-actionable lifecycle defects remain:
-
P1 — non-member owners cannot observe an asynchronous private-workflow update’s terminal result. The new authorization deliberately permits the immutable owner without channel membership (
crates/buzz-relay/src/api/workflows.rs:115-155;crates/buzz-db/src/workflow.rs:530-554), butbuzz workflows managereturns only the initialpending_agentacknowledgement (crates/buzz-cli/src/commands/workflows.rs:171-206;crates/buzz-cli/src/client.rs:1423-1437). The terminal receipt is stored and published with the private workflow channel (crates/buzz-db/src/workflow.rs:1438-1462;crates/buzz-relay/src/api/workflows.rs:630-644), while generic reads restrict channel-scoped events to accessible channels (crates/buzz-relay/src/handlers/req.rs:1063-1091;crates/buzz-relay/src/api/bridge.rs:1423-1427). Thus the headline actor cannot distinguish applied, rejected, stale, invalid YAML, or indefinitely pending. Add an owner-gated, non-enumerating status/receipt path (or equivalent projection), surface the bounded terminal/pending result in the CLI, and test a non-member immutable owner observing both applied and rejected results while unrelated callers receive indistinguishable not-found. -
P1 — conflict completions are not replay-safe. A stale coordinate/revision completion stores a specialized conflict receipt (
crates/buzz-db/src/workflow.rs:1438-1443), but the terminal replay branch compares that stored receipt only with the ordinary caller-supplied receipt (crates/buzz-db/src/workflow.rs:1317-1326); the handler supplies conflict candidates separately (crates/buzz-relay/src/api/workflows.rs:570-617). If the first conflict response is lost, an identical retry returns 409 forever rather than the stored terminal result. Return the authenticated target agent’s already-stored outcome, or compare the deterministic matching conflict receipt, and test stale first completion followed by an identical retry yielding the same receipt ID/status/reason withtransitioned:false. -
P1 — completing an already-applied control command produces an internal error. Control operations intentionally persist
proposed_yaml = NULL(migrations/0035_workflow_owner_commands.sql:12-24;crates/buzz-db/src/workflow.rs:1152-1177). The authenticated completion route accepts any command UUID (crates/buzz-relay/src/router.rs:80-81) and looks up terminal rows (crates/buzz-db/src/workflow.rs:1056-1068), but decodesproposed_yamlas non-nullString(crates/buzz-db/src/workflow.rs:1083). SQLx therefore rejects the valid NULL control row and the route maps it to 500 (crates/buzz-relay/src/api/workflows.rs:461-470). Make the lookup/completion contract operation-aware—either deterministic client rejection before update-cargo decoding or terminal replay without YAML—and add a route/DB regression proving completion of an applied control row cannot 500.
Author action: fix all three defects and add the specified regressions.
Verification owner: author for fixes and affected package/integration gates; this reviewer for exact-head re-review.
Validation: at this exact clean head, cargo test -p buzz-core -p buzz-sdk -p buzz-acp --all-targets, cargo test -p buzz-relay --all-targets (916 lib + 11 main), cargo test -p buzz-cli --all-targets (366), and the runnable cargo test -p buzz-db --all-targets suite (105 passed; 194 infrastructure tests ignored) passed. git diff --check passed, and exact-head required CI is green. These do not exercise the three failed seams.
Confidence gaps, not additional author action: no isolated migrated-Postgres run was available because the reviewer machine’s shared schema was stale and Docker isolation required interactive sign-in; no live relay+ACP private-owner journey was run. A separate combined package run timed out in timing-sensitive ACP tests without a final result, so it is inconclusive and not classified as PR-caused.
🤖 Larry here.
Summary
The human owner of a managed agent can now update, enable, disable, or retire workflows that agent created on their behalf, using either the public SDK or
buzz workflows manage.Previously, this lifecycle was not usable end to end: the SDK did not export a required operation type, and the CLI could report a private agent-owned workflow as “not found” unless the human owner was also a member of its channel. The new path resolves the owner privately and routes a signed management request to the agent, while the agent remains the only authority that can sign a replacement workflow definition.
Behavior and authority
buzz workflows manage <workflow> <operation>supports update, enable, disable, and retire for the managed agent’s immutable human owner, including workflows in the agent’s private channels.Lifecycle guarantees
Migration
0035stores owner-command receipt state. The new command table participates in tenant deletion in child-before-parent order and is protected by the migration’s community write fence, preventing deletion from racing new writes. Existing workflows retain their current owner and signed definition; channel membership never becomes owner authority.Stack and scope
This PR is stacked on #2737, which establishes authenticated workflow delivery and owner-trigger authority. This layer adds lifecycle management only.
No Desktop UI changes are included.