Skip to content

fix(workflows): let owners manage agent-created workflows - #6697

Open
loganj wants to merge 6 commits into
fix/workflows-discoveryfrom
fix/workflow-owner-management
Open

fix(workflows): let owners manage agent-created workflows#6697
loganj wants to merge 6 commits into
fix/workflows-discoveryfrom
fix/workflow-owner-management

Conversation

@loganj

@loganj loganj commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

🤖 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

  • The SDK exports the complete workflow-management API.
  • 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.
  • Private discovery returns only the agent identity and current workflow revision needed to route the request. It does not reveal workflow or channel content, grant membership, or distinguish an unauthorized workflow from a nonexistent one.
  • Each request is bound to the current signed revision, immutable agent owner, channel, and workflow identity. Stale or mismatched requests fail without changing the workflow.
  • The relay never signs a workflow definition for the agent. The owner requests a lifecycle change; the agent validates it and signs the resulting definition.

Lifecycle guarantees

  • Updates preserve existing webhook secrets. A change that would require generating a secret the caller cannot receive is rejected atomically.
  • Re-enabling a retired workflow restores it to active trigger discovery.
  • Commands are replay-safe: if a response is lost, the agent can resend the same signed completion and the relay records only one terminal result.

Migration 0035 stores 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.

loganj and others added 6 commits August 24, 2026 12:26
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>
@loganj
loganj force-pushed the fix/workflow-owner-management branch from f5b46ba to fe0b5cb Compare August 24, 2026 16:29

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

  1. P1: stale/conflicting terminal completions are not replay-safe. complete_workflow_owner_command stores coordinate_conflict_receipt or revision_conflict_receipt when 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-supplied receipt_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 in workflow_coordinate_changed or workflow_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 with transitioned:false and the same stored receipt ID.

  2. 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), while buzz workflows manage returns only the initial pending_agent response 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.

loganj added a commit that referenced this pull request Aug 25, 2026
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>
loganj added a commit that referenced this pull request Aug 25, 2026
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 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:bot: Jude’s code review agent

Requesting changes on exact head fe0b5cbfffc539b7847073aa4ee359ddd08b9e05 against base 83e8b7620e588e369248e4d30b57be6db754b044. Three author-actionable lifecycle defects remain:

  1. 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), but buzz workflows manage returns only the initial pending_agent acknowledgement (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.

  2. 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 with transitioned:false.

  3. 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 decodes proposed_yaml as non-null String (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.

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.

3 participants