Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions browser-host/workflow-broker.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,14 @@ it("existing backend signs only canonical workflow sign/publish with exact own e
["a", `30620:${h.viewer}:${id}`],
],
},
// The human signs an agent-targeted deletion; the relay checks ownership.
{
...template(5),
tags: [
["h", runId],
["a", `30620:${"a".repeat(64)}:${id}`],
],
},
]) {
const event = await t.writer.sign(input, signal());
expect(verifyEvent(event)).toBe(true);
Expand All @@ -421,7 +429,7 @@ it("existing backend signs only canonical workflow sign/publish with exact own e
expect(await t.writer.publish(event, signal())).toBe("workflow-result");
expect(h.publications.at(-1)).toEqual(JSON.parse(JSON.stringify(event)));
}
expect(h.publications).toHaveLength(4);
expect(h.publications).toHaveLength(5);
expect(h.calls).toHaveLength(0);
} finally {
live?.dispose();
Expand Down Expand Up @@ -462,12 +470,12 @@ const invalidCommands = [
}),
],
[
"other owner",
"malformed owner",
() => ({
...template(5),
tags: [
["h", runId],
["a", `30620:${"a".repeat(64)}:${id}`],
["a", `30620:invalid:${id}`],
],
}),
],
Expand Down
11 changes: 11 additions & 0 deletions docs/buzz-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -549,3 +549,14 @@ With the pinned-viewer development server, Settings → Plugins → Builderlab m
### Reconnecting an old agent in a DM

On desktop, **Connect [agent name]** may appear inside a DM with an old agent you own that is available to import from this computer. It preselects compatible saved harness, provider and model choices, and preserves saved effort settings. Choose a harness if the old type is unavailable; settings that were not saved use the selected type’s defaults. Review the inline controls, complete installation or sign-in if prompted, stop the agent in old Buzz, then choose **Connect**. Your agent keeps its identity and conversation. This does not resend earlier messages. The same private card can appear in a channel or thread after you mention an eligible agent you own. Only you see it, once per agent in the loaded conversation. After connecting, **Ask the agent to respond** explicitly queues a follow-up addressed only to that agent in the original message’s thread; it does not automatically replay your request. If no prompt appears, use **Agents → New agent → Import an agent**.

## Deleting an agent’s workflow

In **Workflows → Owner → All channel workflows**, open the workflow’s actions
menu and choose **Delete workflow**. You can delete workflows you authored or
workflows authored by an agent whose ownership Buzz has verified belongs to you.
This also works when your channel role is Guest.
Confirm the deletion; pending or uncertain results remain in the workflow notices.
If permission changes during confirmation, Buzz explains why deletion could not
start and keeps any editor draft.
Owning the agent does not grant permission to edit or run its workflow.
12 changes: 10 additions & 2 deletions docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,16 @@ See the [capability contract](../src/features/workflows/types.ts).
the active naming policy. Fallbacks lengthen their suffix to stay distinct across
visible owners. Visible owners hydrate together in background profile
batches of up to 500 identities. Narrow rows reflow their artwork, text, and
independent controls as text grows. Ownership and management
permissions remain tied to the signing identity, including agent-owned workflows.
independent controls as text grows. Editing, enabling and running remain tied
to the workflow author. A verified owner of an agent can also delete that
agent's workflows, signing as themselves;
select All channel workflows to find them. This ownership-authorized deletion
also works with a Guest channel role. Ownership claims in display metadata
do not grant permission, and the relay authorizes every deletion. Losing the
verified ownership proof disables deletion in the app; it does not clear the
relay's separately recorded agent owner.
If permission changes during confirmation, an error explains why deletion
could not start; the editor retains its draft.
Clearing the owner filter or Clear all shows all channel workflows.
Enabled and Disabled match saved configuration (omitted means enabled), not
confirmed runtime activity; unreadable configurations are excluded from both.
Expand Down
9 changes: 6 additions & 3 deletions src-tauri/src/relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,12 @@ pub(crate) async fn relay_sign(
{
return Err("Relay event is too large".into());
}
if coordinate_delete
.is_some_and(|coordinate| coordinate.split(':').nth(1) != signed["pubkey"].as_str())
{
// Canonical workflow deletes may target an agent owned by the signer.
// The relay authorizes ownership; other coordinate deletes remain author-only.
if coordinate_delete.is_some_and(|coordinate| {
!coordinate.starts_with("30620:")
&& coordinate.split(':').nth(1) != signed["pubkey"].as_str()
}) {
return Err("Only the author can delete this event".into());
}
Ok(signed)
Expand Down
12 changes: 9 additions & 3 deletions src-tauri/src/relay/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -837,16 +837,22 @@ fn isolated_agent_ipc_probe() {
}
})).is_err());
let id = "11111111-1111-4111-8111-111111111111";
assert!(invoke(
let agent_deletion = invoke(
"relay_sign",
serde_json::json!({
"community": "https://relay.test", "event": {
"kind": 5, "created_at": 123,
"tags": [["h", id], ["a", format!("30620:{}:{id}", "a".repeat(64))]], "content": ""
}
})
}),
)
.is_err());
.unwrap();
verify(&agent_deletion);
assert_eq!(agent_deletion["pubkey"], public);
assert_eq!(
agent_deletion["tags"][1][1],
format!("30620:{}:{id}", "a".repeat(64))
);
let deletion = invoke("relay_sign", serde_json::json!({
"community": "https://relay.test", "event": {
"kind": 5, "created_at": 123,
Expand Down
64 changes: 46 additions & 18 deletions src/bundled/workflows/WorkflowChannel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
useEffect,
useId,
useCallback,
useRef,
useState,
Expand All @@ -10,7 +11,11 @@ import type {
WorkflowDefinition,
} from "../../features/workflows/types";
import { PlayIcon, TrashIcon } from "../../shared/design-system/icons";
import { MenuIcon, MenuItem } from "../../shared/design-system/ui/Menu";
import {
MenuIcon,
MenuItem,
MenuNote,
} from "../../shared/design-system/ui/Menu";
import { Button } from "../../shared/design-system/ui/Button";
import { IconButton } from "../../shared/design-system/ui/IconButton";
import { ConfirmAction } from "./ConfirmAction";
Expand All @@ -21,6 +26,7 @@ import { saveAwaitsReadback } from "./WorkflowLanding";
import { exactSaveReadback } from "./editor-model";
import { DEFAULT_FORM_STATE, formStateToYaml } from "./workflowFormTypes";
import { useWorkflowView } from "./useWorkflowView";
import { useWorkflowCanDelete } from "./useWorkflowCanDelete";
import {
type ChannelLabel,
channelIcon,
Expand Down Expand Up @@ -82,6 +88,7 @@ export function WorkflowChannel({
capability.operations.snapshot,
);
const submission = useRef<string | null>(null);
const deleteReasonId = useId();
const editorGeneration = useRef(0);
const [draft, setDraft] = useState<Draft | null>(() =>
draftFor(initialSelection, 0),
Expand All @@ -106,6 +113,7 @@ export function WorkflowChannel({
!!draft?.operationId && (!operation || operation.outcome === "pending");
const [localDraftAtRisk, setLocalDraftAtRisk] = useState(false);
const readonly = !!draft?.original && draft.original.owner !== viewer;
const canDelete = useWorkflowCanDelete(capability, draft?.original);
const dirty = !!draft && (draft.yaml !== draft.initial || localDraftAtRisk);
const atRisk = dirty || !!draft?.operationId;
// A run request never changes configuration, so its lost receipt cannot
Expand Down Expand Up @@ -236,12 +244,22 @@ export function WorkflowChannel({
if (
!draft?.original ||
submission.current ||
readonly ||
unresolvedWrite ||
!capability.availability.delete ||
draft.operationId
)
return;
if (
!capability.permissions.canDelete(draft.original) ||
!capability.availability.delete
) {
setConfirmDelete(false);
setError(
capability.availability.delete
? "You no longer have permission to delete this workflow. Refresh and try again."
: "Workflow deletion is unavailable on this connection.",
);
return;
}
// The page owns submission and recovery after this editor is discarded.
const definition = draft.original;
setDraft(null);
Expand Down Expand Up @@ -371,21 +389,31 @@ export function WorkflowChannel({
)
}
actions={
Comment thread
ravarora2 marked this conversation as resolved.
Resolved
!readonly && (
<MenuItem
tone="danger"
disabled={
!!draft.operationId ||
unresolvedWrite ||
!capability.availability.delete
}
onClick={() => setConfirmDelete(true)}
>
<MenuIcon>
<TrashIcon size={14} />
</MenuIcon>
Delete workflow
</MenuItem>
draft.original && (
<>
<MenuItem
tone="danger"
aria-describedby={!canDelete ? deleteReasonId : undefined}
disabled={
!canDelete ||
!!draft.operationId ||
unresolvedWrite ||
!capability.availability.delete
}
onClick={() => setConfirmDelete(true)}
>
<MenuIcon>
<TrashIcon size={14} />
</MenuIcon>
Delete workflow
</MenuItem>
{!canDelete && (
<MenuNote id={deleteReasonId}>
Only the author or their verified agent owner can delete
this workflow.
</MenuNote>
)}
</>
)
}
runs={
Expand Down
26 changes: 23 additions & 3 deletions src/bundled/workflows/WorkflowLanding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
MenuTrigger,
MenuPopup,
MenuItem,
MenuNote,
MenuIcon,
MenuRadioGroup,
MenuRadioItem,
Expand All @@ -85,6 +86,7 @@ import { EmptyState } from "../../shared/design-system/ui/EmptyState";
import { ToastNotice } from "../../shared/design-system/ui/Toast";
import { messages } from "./WorkflowOperations";
import { ConfirmAction } from "./ConfirmAction";
import { useWorkflowCanDelete } from "./useWorkflowCanDelete";
import { WorkflowRuns } from "./WorkflowRuns";
import { getWorkflowActivationWarning } from "./workflowActivationWarning";
import {
Expand Down Expand Up @@ -546,6 +548,7 @@ function WorkflowCard({
const presentation = workflowCardPresentation(definition.yaml);
const enabled = fields.enabled !== false;
const readonly = definition.owner !== viewer;
const canDelete = useWorkflowCanDelete(capability, definition);
const submittedOperation = operations.find(
(operation) => operation.eventId === submitted?.operationId,
);
Expand All @@ -562,6 +565,7 @@ function WorkflowCard({
? "Saving is unavailable from this host."
: undefined;
const reasonId = useId();
const deleteReasonId = useId();
const descriptionId = useId();
const statusId = useId();
const warning = getWorkflowActivationWarning(definition.yaml);
Expand Down Expand Up @@ -676,8 +680,9 @@ function WorkflowCard({
</MenuItem>
<MenuItem
tone="danger"
aria-describedby={!canDelete ? `${deleteReasonId}-row` : undefined}
disabled={
readonly ||
!canDelete ||
Comment thread
ravarora2 marked this conversation as resolved.
Resolved
locked ||
awaitingReadback ||
!capability.availability.delete
Expand All @@ -689,6 +694,12 @@ function WorkflowCard({
</MenuIcon>
Delete workflow
</MenuItem>
{!canDelete && (
<MenuNote id={`${deleteReasonId}-row`}>
Only the author or their verified agent owner can delete this
workflow.
</MenuNote>
)}
</MenuPopup>
</MenuRoot>
</div>
Expand Down Expand Up @@ -768,8 +779,11 @@ function WorkflowCard({
<MenuPopup size="compact" align="end">
<MenuItem
tone="danger"
aria-describedby={
!canDelete ? `${deleteReasonId}-detail` : undefined
}
disabled={
readonly ||
!canDelete ||
locked ||
awaitingReadback ||
!capability.availability.delete
Expand All @@ -781,6 +795,12 @@ function WorkflowCard({
</MenuIcon>
Delete workflow
</MenuItem>
{!canDelete && (
<MenuNote id={`${deleteReasonId}-detail`}>
Only the author or their verified agent owner can delete this
workflow.
</MenuNote>
)}
</MenuPopup>
</MenuRoot>
</div>
Expand Down Expand Up @@ -1662,7 +1682,7 @@ export function WorkflowLanding({
}
description={
emptyFromOwnership
? "Workflows you own appear here. Other people’s workflows are in All channel workflows."
? "Workflows you authored appear here. Other authors’ workflows are in All channel workflows."
: emptyFromFilters
? "Try changing or clearing your filters."
: "Automations that keep your community moving."
Expand Down
Loading
Loading