Skip to content
Draft
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
4 changes: 3 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,10 @@ test-unit:
# non-postgres_tests cases only "pass" without a database by waiting out
# the ~30s sqlx acquire timeout, so they do not belong in the infra-free
# unit job either.
# The author-only fanout family uses lazy pools and in-memory recipients;
# include Stop request/result privacy and its sibling private kinds.
cargo nextest run -p buzz-relay --lib \
-E '(test(/^api::admin::/) - test(=api::admin::tests::disabled_mode_allows_unauthenticated_requests_on_the_admin_host) - test(=api::admin::tests::nip98_mode_unrostered_signer_does_not_consume_a_replay_slot)) + test(/^handlers::channel_authz::/) + test(/^handlers::moderation_authz::/) + test(/^handlers::side_effects::tests::/)'
-E '(test(/^api::admin::/) - test(=api::admin::tests::disabled_mode_allows_unauthenticated_requests_on_the_admin_host) - test(=api::admin::tests::nip98_mode_unrostered_signer_does_not_consume_a_replay_slot)) + test(/^handlers::channel_authz::/) + test(/^handlers::moderation_authz::/) + test(/^handlers::side_effects::tests::/) + test(/^handlers::event::tests::fanout_access::.*_delivers_to_author_only$/)'
# ACP author-gate and queue tests protect the trust boundary between
# relay events and agent prompts. They are infra-free; ignored lifecycle
# tests remain excluded and run in their dedicated integration lanes.
Expand Down
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default defineConfig({
name: "smoke",
testMatch: [
"**/smoke.spec.ts",
"**/desktop-stop.spec.ts",
"**/owned-agent-discovery.spec.ts",
"**/thread-head-stale-edit.spec.ts",
"**/sidebar-offcanvas-rail.spec.ts",
Expand Down
7 changes: 4 additions & 3 deletions desktop/src/features/agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,10 @@ with a TypeScript lookup table or an id comparison in a component.

## Remote Desktop Stop

Native IPC accepts an owner-private, explicitly selected agent+Desktop Stop,
not inferred agent location. The relay redelivers stored Stop duplicates without
repeating relay side effects.
Known Desktops exposes an owner-private, explicitly selected agent+Desktop Stop,
not inferred agent location. The app-scoped receiver subscribes live only;
reopening never replays commands. An explicit retry republishes the exact request;
the relay redelivers stored Stop duplicates without repeating relay side effects.
The receiver returns saved results or Unknown, never repeats a consumed Stop.
Native owner-delegation and community checks
precede durable admission and ordinary pair Stop. A delivery ACK is not success.
Expand Down
155 changes: 155 additions & 0 deletions desktop/src/features/agents/ui/DesktopStopControl.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import assert from "node:assert/strict";
import test from "node:test";
import React from "react";
import { JSDOM } from "jsdom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
DesktopStopControl,
DesktopStopReceiver,
} from "./DesktopStopControl.tsx";
import { relayClient } from "../../../shared/api/relayClient.ts";

test("mounted Stop waits for a correlated result and retries identical bytes without replay", async () => {
const dom = new JSDOM("<div id='root'></div>", {
url: "https://desktop.test",
});
Object.assign(globalThis, {
window: dom.window,
document: dom.window.document,
localStorage: dom.window.localStorage,
IS_REACT_ACT_ENVIRONMENT: true,
});
const { createRoot } = await import("react-dom/client");
const scope = { owner: "owner", community: "wss://one.example" };
const request = { id: "request", kind: 50180, tags: [["d", "desktop"]] };
const result = { id: "result", kind: 50181, tags: [["e", request.id]] };
const client = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
});
client.setQueryData(
["relay-agents"],
[
{ pubkey: "agent", name: "Owned agent", ownerPubkey: "owner" },
{ pubkey: "foreign", name: "Foreign agent", ownerPubkey: "other" },
],
);
const original = {
fetch: relayClient.fetchEvents,
publish: relayClient.publishEvent,
subscribe: relayClient.subscribeLive,
};
let live, release;
let receiveCalls = 0,
prepared = 0,
closed = 0;
const sent = [];
window.__TAURI_INTERNALS__ = {
invoke: async (command, args) => {
assert.equal(args.owner, scope.owner);
assert.equal(args.community, scope.community);
if (command === "prepare_desktop_stop") {
prepared++;
assert.equal(args.desktop, "desktop");
assert.equal(args.agent, "agent");
return request;
}
if (command === "receive_desktop_stop") {
receiveCalls++;
return result;
}
assert.equal(command, "read_desktop_stop_results");
return "stopped";
},
};
relayClient.subscribeLive = async (filter, callback) => {
assert.deepEqual(filter, {
kinds: [50180],
authors: [scope.owner],
limit: 0,
});
live = callback;
return () => {
closed++;
live = undefined;
};
};
relayClient.publishEvent = async (event) => {
sent.push(event);
live?.(event);
};
relayClient.fetchEvents = async (filter) => {
assert.deepEqual(filter, {
kinds: [50181],
authors: [scope.owner],
"#e": [request.id],
limit: 16,
});
return new Promise((resolve) => {
release = () => resolve([result]);
});
};
const root = createRoot(document.getElementById("root"));
const render = (receiver) =>
React.act(async () =>
root.render(
React.createElement(
QueryClientProvider,
{ client },
React.createElement(
React.Fragment,
null,
receiver
? React.createElement(DesktopStopReceiver, { scope })
: null,
React.createElement(DesktopStopControl, {
scope,
desktop: { id: "desktop", name: "Workshop" },
}),
),
),
),
);
const click = (text) =>
React.act(async () =>
[...document.querySelectorAll("button")]
.find((b) => b.textContent === text)
.click(),
);
try {
await render(false);
assert.doesNotMatch(document.body.textContent, /Foreign agent/);
const select = document.querySelector("select");
await React.act(async () => {
select.value = "agent";
select.dispatchEvent(new dom.window.Event("change", { bubbles: true }));
});
await click("Stop on Workshop");
assert.equal(prepared, 1);
assert.match(document.body.textContent, /Waiting for this Desktop/);
assert.doesNotMatch(document.body.textContent, /Stop confirmed/);
assert.equal(receiveCalls, 0, "absent receiver has not stopped anything");
await React.act(async () => release());
assert.match(document.body.textContent, /Stop confirmed by Workshop/);
await render(true);
assert.equal(receiveCalls, 0, "mount cannot replay stored Stop");
// The mounted receiver returns a saved native result, while the sender
// explicitly retries the exact prepared request rather than signing anew.
relayClient.publishEvent = async (event) => {
sent.push(event);
if (event.kind === 50180) live?.(event);
};
await click("Retry same Stop");
await React.act(async () => release());
assert.equal(prepared, 1);
assert.equal(receiveCalls, 1);
assert.ok(sent.filter((e) => e.kind === 50180).every((e) => e === request));
} finally {
await React.act(async () => root.unmount());
assert.equal(closed, 1);
client.clear();
relayClient.fetchEvents = original.fetch;
relayClient.publishEvent = original.publish;
relayClient.subscribeLive = original.subscribe;
dom.window.close();
}
});
170 changes: 170 additions & 0 deletions desktop/src/features/agents/ui/DesktopStopControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { useEffect, useRef, useState } from "react";
import { useRelayAgentsQuery } from "../hooks";
import {
prepareStop,
readStopOutcome,
receiveStops,
sendStop,
} from "../desktopStop";
import type { DesktopScope, DesktopRow } from "../desktopList";
import type { RelayEvent } from "@/shared/api/types";
import { Button } from "@/shared/ui/button";

/** App-scoped live receiver; no historical requests are loaded on mount. */
export function DesktopStopReceiver({ scope }: { scope: DesktopScope | null }) {
const [error, setError] = useState("");
const { owner, community } = scope ?? {};
useEffect(() => {
if (!owner || !community) return;
let active = true;
let close: (() => void) | undefined;
setError("");
void receiveStops({ owner, community }, () => active, setError)
.then((unsubscribe) => {
if (active) close = unsubscribe;
else unsubscribe();
})
.catch(() => {
if (active)
setError("Remote Stop receiver is unavailable on this Desktop.");
});
return () => {
active = false;
close?.();
};
}, [owner, community]);
return error ? (
<p role="status" className="text-xs text-muted-foreground">
{error}
</p>
) : null;
}

/** Deliberately selects a host, not an inferred running location or presence. */
export function DesktopStopControl({
scope,
desktop,
}: {
scope: DesktopScope;
desktop: DesktopRow;
}) {
const agents = useRelayAgentsQuery();
const owned = (agents.data ?? []).filter(
(agent) => agent.ownerPubkey === scope.owner,
);
const [agent, setAgent] = useState("");
const [request, setRequest] = useState<RelayEvent | null>(null);
const [message, setMessage] = useState("");
const [busy, setBusy] = useState(false);
const active = useRef(true);
useEffect(() => {
active.current = true;
return () => {
active.current = false;
};
}, []);
const run = async (retry: boolean) => {
setBusy(true);
let current = retry ? request : null;
try {
current ??= await prepareStop(
scope,
desktop.id,
agent,
() => active.current,
);
if (!active.current) return;
setRequest(current);
setMessage("Stop requested. Waiting for this Desktop’s result…");
try {
await sendStop(scope, current, () => active.current);
} catch {
if (active.current)
setMessage(
"Delivery unconfirmed. Checking for this Desktop’s result…",
);
}
for (let attempt = 0; attempt < 15 && active.current; attempt++) {
const outcome = await readStopOutcome(
scope,
current,
() => active.current,
);
if (!active.current) return;
if (outcome === "stopped") {
setMessage(`Stop confirmed by ${desktop.name}.`);
return;
}
if (outcome === "failed") {
setMessage(
`Stop failed on ${desktop.name}. No success was confirmed.`,
);
return;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
if (active.current)
setMessage(
"Stop unconfirmed. This Desktop may be unavailable; its agents may still be running.",
);
} catch {
if (active.current)
setMessage("Stop unconfirmed. No successful result could be read.");
} finally {
if (active.current) setBusy(false);
}
};
return (
<div className="mt-2 space-y-2">
<label className="block text-xs">
Agent to stop on {desktop.name}
<select
aria-label={`Agent to stop on ${desktop.name}`}
className="ml-2 rounded border bg-background p-1 text-sm"
value={agent}
disabled={busy}
onChange={(event) => {
setAgent(event.target.value);
setRequest(null);
setMessage("");
}}
>
<option value="">Choose your agent</option>
{owned.map((item) => (
<option key={item.pubkey} value={item.pubkey}>
{item.name}
</option>
))}
</select>
</label>
<p className="text-xs text-muted-foreground">
Stops only this agent on this Desktop in this community. This list does
not establish where it is running.
</p>
{agents.isError && <p role="status">Your agent list is unavailable.</p>}
<Button
size="sm"
variant="outline"
disabled={!agent || busy}
onClick={() => void run(false)}
>
Stop on {desktop.name}
</Button>
{request && (
<Button
size="sm"
variant="ghost"
disabled={busy}
onClick={() => void run(true)}
>
Retry same Stop
</Button>
)}
{message && (
<p role="status" className="text-xs">
{message}
</p>
)}
</div>
);
}
Loading
Loading