fix(relay): execute a-tag workflow deletions against the a-tag owner, delete all name matches, surface no-op deletions - #2242
Conversation
d94cd43 to
103b368
Compare
delete_workflow built the NIP-09 a-tag coordinate with the caller's pubkey, but kind:30620 is addressable by (author, d-tag). For an agent-created workflow the coordinate named a record that doesn't exist — the relay accepted the kind:5 and deleted nothing while the UI reported success. Fetch the workflow's kind:30620 event first (the same #d query update_workflow already uses) and build the coordinate from the event's author. Deleting an unknown workflow id now surfaces "workflow not found" instead of silently publishing an unmatchable delete. Client half of the end-to-end fix; the relay half (execute a-tag deletions against the a-tag owner rather than the signer) is block#2242. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: chillerno1 <gh.chiller@pm.me>
… delete all name matches, surface no-op deletions Issue block#1593: NIP-09 a-tag deletion for kind:30620 (workflow) parsed the a-tag's owner pubkey but never used it. Both the UUID and name-based delete paths scoped the DELETE by `actor_bytes` (the deletion signer) instead. `validate_standard_deletion_event` already authorizes the request against the a-tag pubkey, including the `is_agent_owner` allowance that lets a human owner delete their agent's workflows -- so a human owner's deletion of an agent-owned workflow passed validation, then silently deleted nothing, while the relay still replied `accepted:true`. Three fixes, one change: - handlers/side_effects.rs: `handle_a_tag_deletion`'s KIND_WORKFLOW_DEF branch now hex-decodes the a-tag pubkey and scopes both the UUID delete and the name-based delete by that owner, not the signer. - buzz-db/workflow.rs: add `delete_workflows_for_owner_by_name`, which removes every workflow matching (community, owner, name) in one statement. `find_by_owner_and_name`'s bare `LIMIT 1` (no unique constraint on the pair) meant name-based deletion previously removed at most one of N same-named rows; it has no remaining callers but stays in place as public API. - No-op deletions (no matching workflow) now surface as an `OK` message ("info: no matching workflow found for deletion") instead of a silent tracing::warn, following this codebase's existing convention of accepted:true + an informational message prefix (see the "duplicate:" and "info: you have left this relay" messages in ingest.rs). This required widening `handle_side_effects`'s return type to `Result<Option<String>>`, which ingest_event_inner now folds into the final IngestResult.message; every other kind is unaffected and always yields `Ok(None)`. Tests: - buzz-db: delete_workflows_for_owner_by_name_removes_all_same_named_rows, delete_workflows_for_owner_by_name_leaves_other_owners_untouched (Postgres-gated, existing repo pattern). - buzz-relay: a_tag_deletion_by_human_owner_deletes_agent_owned_workflow -- exact bug regression (agent-owned workflow, deletion signed by the human owner) using the same real-AppState-over-lazy-PgPool pattern as workflow_sink::integration_tests. All three verified red (fail without the owner-scope fix) and green (pass with it) against a local Postgres instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Orlando Lopez <orlando.lf19@gmail.com>
103b368 to
b6af13b
Compare
|
Rebased onto Verified locally after the rebase: This has been open since the 21st without a reviewer assigned — could someone who knows the relay's NIP-09 deletion path take a look? Happy to split it if the three fixes (owner-scoped execution, deleting all name matches, surfacing no-op deletions) would be easier to review separately. |
Partially addresses #1593 (the going-forward half; existing-orphan reconciliation is a separate discussion — see my comment on the issue).
Problem
Workflows whose owner can no longer delete them keep executing on their cron schedules indefinitely — recurring compute and API spend with no accountable owner and no off switch. Three execution bugs make that possible:
validate_standard_deletion_eventauthorizes an a-tag deletion against the a-tag's pubkey (including theis_agent_ownerallowance that lets a human owner delete their agent's content), buthandle_a_tag_deletionparses that pubkey and never uses it — both the UUID delete and the name fallback are scoped by the deletion signer. A human owner deleting an agent-owned workflow passes validation, then silently deletes nothing.find_by_owner_and_name(LIMIT 1, noORDER BY) + a single-row delete; with several same-named rows (there's no unique constraint on owner+name) an arbitrary one is removed and the rest keep firing.tracing::warn!server-side; the client seesaccepted: truewith an empty message — which misled the issue reporter into declaring victory twice.Implementation
handle_a_tag_deletion(KIND_WORKFLOW_DEF): hex-decode the a-tag pubkey and scope both the UUID delete and the name-based delete by it. Authorization is unchanged — it already ran invalidate_standard_deletion_event; this aligns execution with validation.delete_workflows_for_owner_by_nameinbuzz-db: singleDELETE … RETURNING id, channel_idremoving every match; each affected channel's workflows are invalidated (deduped).find_by_owner_and_nameis untouched (its only caller was this handler)."duplicate:"/"info:"OK-message convention iningest.rs: the kind:5 event is stored/accepted as before, but the OK message now carriesinfo: no matching workflow found for deletion. Plumbing:handle_side_effectsreturnsanyhow::Result<Option<String>>; kind:5 routes separately and every other kind keeps its exact previous behavior (Ok(None)).Behavioral note for reviewers: cross-owner deletions that previously passed validation and then silently no-op'd now actually execute — that is the intended fix, but worth a conscious look at the authz implications (validation already gated these on
is_agent_owner).Testing
Regression tests (Postgres-gated,
#[ignore = "requires Postgres"], following the existingsetup_pool/workflow_sink::integration_testspatterns):delete_workflows_for_owner_by_name_removes_all_same_named_rows(3 same-named rows → all deleted)delete_workflows_for_owner_by_name_leaves_other_owners_untoucheda_tag_deletion_by_human_owner_deletes_agent_owned_workflow— the exact reported bug: agent-owned workflow, deletion signed by the human owner, a-tag naming the agent → row actually deleted. Red/green verified (temporarily reintroducing the old actor scoping makes it fail).Ran locally:
cargo fmt --all -- --check·cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings·cargo test -p buzz-db -p buzz-relay(82 + 715 passed; PG-gated tests run against a real Postgres 17) ·cargo check --workspace. Relying on upstream CI for the full matrix (desktop/web/mobile untouched).Deferred
Reconciliation for already-orphaned rows (pre-Harden relay attack surfaces #1369 rows are unreachable by any client-signable event, per the issue's 2026-07-09 update) — awaiting maintainer preference on shape (migration vs startup sweep vs
scripts/maintenance/), see the issue thread.CLI
buzz workflows delete --namefollow-up (the relay path now supports it correctly).No-op surfacing is deliberately scoped to the workflow branch; the generic NIP-33 soft-delete no-match case still no-ops silently — happy to extend the same message there if you want the consistency.