From 9ce501a671822cf6c8511a0c3687aad8eb56acb7 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 4 Sep 2026 16:00:35 -0400 Subject: [PATCH 1/2] feat(desktop): mount private remote Stop controls and live receiver Signed-off-by: Logan Johnson --- desktop/src/features/agents/AGENTS.md | 7 +- .../agents/ui/DesktopStopControl.test.mjs | 155 ++++++++++++++++ .../features/agents/ui/DesktopStopControl.tsx | 170 ++++++++++++++++++ .../src/features/agents/ui/KnownDesktops.tsx | 13 +- 4 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 desktop/src/features/agents/ui/DesktopStopControl.test.mjs create mode 100644 desktop/src/features/agents/ui/DesktopStopControl.tsx diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md index 63ad3ee07a9..020be239764 100644 --- a/desktop/src/features/agents/AGENTS.md +++ b/desktop/src/features/agents/AGENTS.md @@ -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. diff --git a/desktop/src/features/agents/ui/DesktopStopControl.test.mjs b/desktop/src/features/agents/ui/DesktopStopControl.test.mjs new file mode 100644 index 00000000000..d7d8b1dfc1f --- /dev/null +++ b/desktop/src/features/agents/ui/DesktopStopControl.test.mjs @@ -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("
", { + 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(); + } +}); diff --git a/desktop/src/features/agents/ui/DesktopStopControl.tsx b/desktop/src/features/agents/ui/DesktopStopControl.tsx new file mode 100644 index 00000000000..bd5e8f56cd7 --- /dev/null +++ b/desktop/src/features/agents/ui/DesktopStopControl.tsx @@ -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 ? ( +

+ {error} +

+ ) : 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(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 ( +
+ +

+ Stops only this agent on this Desktop in this community. This list does + not establish where it is running. +

+ {agents.isError &&

Your agent list is unavailable.

} + + {request && ( + + )} + {message && ( +

+ {message} +

+ )} +
+ ); +} diff --git a/desktop/src/features/agents/ui/KnownDesktops.tsx b/desktop/src/features/agents/ui/KnownDesktops.tsx index 16d872c2d8b..45e7aa68eaa 100644 --- a/desktop/src/features/agents/ui/KnownDesktops.tsx +++ b/desktop/src/features/agents/ui/KnownDesktops.tsx @@ -1,3 +1,4 @@ +import { DesktopStopControl, DesktopStopReceiver } from "./DesktopStopControl"; import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useIdentityQuery } from "@/shared/api/hooks"; @@ -19,6 +20,7 @@ import { import { DesktopCapabilityDetails } from "./DesktopCapabilityDetails"; type View = { + scope?: import("../desktopList").DesktopScope; capabilities?: DesktopCapabilities[]; capabilityWarning?: string; list: DesktopList | null; @@ -78,7 +80,7 @@ export function DesktopListStartup() { unsubscribe(); }; }, [refetch, pulse, report]); - return null; + return ; } export function KnownDesktops() { @@ -92,6 +94,7 @@ export function KnownDesktops() { }, []); return ( + {scope && ( + + )} item.id === row.id)} now={now} From dfce9ba3cfc3c93f4eaf42560108a5d63a43d8a4 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 4 Sep 2026 16:27:36 -0400 Subject: [PATCH 2/2] test(multiverse): exercise mounted Stop recovery and private fanout in CI Signed-off-by: Logan Johnson --- Justfile | 4 +- desktop/playwright.config.ts | 1 + desktop/src/testing/e2eBridge.ts | 7 ++ desktop/tests/e2e/desktop-stop.spec.ts | 163 +++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 desktop/tests/e2e/desktop-stop.spec.ts diff --git a/Justfile b/Justfile index 6df2aff7976..f61e0ef8f50 100644 --- a/Justfile +++ b/Justfile @@ -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. diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index 4a27ee671a6..4bd402673f1 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -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", diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index d54c32e4c48..4548aea1afb 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -11265,6 +11265,13 @@ function sendToMockSocket(args: { return; } + // Desktop inventory/control records are global-only. Native IPC owns their + // encryption and result validation; smoke fixtures supply that boundary. + if ([30180, 30181, 30182, 50180, 50181].includes(event.kind)) { + sendWsText(socket.handler, ["OK", event.id, true, ""]); + return; + } + const channelId = getChannelIdFromTags(event.tags); if (!channelId) { sendWsText(socket.handler, [ diff --git a/desktop/tests/e2e/desktop-stop.spec.ts b/desktop/tests/e2e/desktop-stop.spec.ts new file mode 100644 index 00000000000..a656aa45623 --- /dev/null +++ b/desktop/tests/e2e/desktop-stop.spec.ts @@ -0,0 +1,163 @@ +import { expect, test } from "@playwright/test"; +import { installMockBridge } from "../helpers/bridge"; +import { waitForAnimations } from "../helpers/animations"; + +// These are IPC fixtures, not native execution evidence. The real mounted +// Known Desktops, client, relay publisher and retry control remain in the path. +test("remote Stop distinguishes delivery, uncertainty, and confirmed result", async ({ + page, +}) => { + test.setTimeout(60_000); + const agent = "a7".repeat(32); + await installMockBridge(page, { + managedAgents: [], + relayAgents: [ + { + pubkey: agent, + name: "Scout", + ownerPubkey: "deadbeef".repeat(8), + status: "unknown", + respondTo: "owner-only", + channelNames: [], + channelIds: [], + }, + ], + }); + await page.goto("/"); + await expect(page.getByTestId("open-agents-view")).toBeVisible(); + await page.evaluate(() => { + const w = window as typeof window & { + __STOP_FIXTURE__: { + confirmed: boolean; + prepared: number; + sends: string[]; + }; + __TAURI_INTERNALS__: { + invoke: (command: string, payload?: any, options?: any) => Promise; + }; + }; + w.__STOP_FIXTURE__ = { confirmed: false, prepared: 0, sends: [] }; + const original = w.__TAURI_INTERNALS__.invoke.bind(w.__TAURI_INTERNALS__); + const now = Math.floor(Date.now() / 1000); + const local = "11111111-1111-4111-8111-111111111111"; + const remote = "22222222-2222-4222-8222-222222222222"; + const sign = async (kind: number, tags: string[][] = []) => + JSON.parse( + await original("sign_event", { + kind, + tags, + content: "encrypted IPC fixture", + createdAt: now, + }), + ); + w.__TAURI_INTERNALS__.invoke = async (command, payload, options) => { + switch (command) { + case "prepare_desktop_profile": + return { event: await sign(30180, [["d", local]]) }; + case "read_desktop_profiles": + return [ + { id: local, name: "Laptop", updated: now }, + { id: remote, name: "Lab Desktop", updated: now }, + ]; + case "prepare_desktop_observation": + return { event: await sign(30181, [["d", local]]) }; + case "read_desktop_observations": + return [ + { id: local, heard: now }, + { id: remote, heard: now - 600 }, + ]; + case "prepare_desktop_capabilities": + return { event: await sign(30182, [["d", local]]) }; + case "read_desktop_capabilities": + return [local, remote].map((id) => ({ + id, + reported: now, + runtimes: [], + })); + case "prepare_desktop_stop": + w.__STOP_FIXTURE__.prepared++; + return sign(50180, [ + ["p", payload.owner], + ["d", payload.desktop], + ]); + case "receive_desktop_stop": + return null; + case "read_desktop_stop_results": + return w.__STOP_FIXTURE__.confirmed ? "stopped" : "unknown"; + case "plugin:websocket|send": { + const wire = JSON.parse(payload.message.data); + if (wire[0] === "EVENT" && wire[1]?.kind === 50180) + w.__STOP_FIXTURE__.sends.push(JSON.stringify(wire[1])); + break; + } + } + return original(command, payload, options); + }; + }); + await page.getByTestId("open-agents-view").click(); + const desktops = page.getByRole("region", { name: "Known Desktops" }); + await desktops.getByRole("button", { name: "Refresh", exact: true }).click(); + await expect( + desktops.getByText("Lab Desktop", { exact: true }), + ).toBeVisible(); + await desktops + .getByRole("combobox", { name: "Agent to stop on Lab Desktop" }) + .selectOption(agent); + await waitForAnimations(page); + await desktops.screenshot({ + path: "test-results/desktop-stop/01-selected.png", + }); + + await desktops + .getByRole("button", { name: "Stop on Lab Desktop", exact: true }) + .click(); + await expect( + desktops.getByText("Stop requested. Waiting for this Desktop’s result…", { + exact: true, + }), + ).toBeVisible(); + await expect( + desktops.getByText("Stop confirmed by Lab Desktop.", { exact: true }), + ).toHaveCount(0); + await waitForAnimations(page); + await desktops.screenshot({ + path: "test-results/desktop-stop/02-waiting.png", + }); + + await expect( + desktops.getByText( + "Stop unconfirmed. This Desktop may be unavailable; its agents may still be running.", + { exact: true }, + ), + ).toBeVisible({ timeout: 25_000 }); + await waitForAnimations(page); + await desktops.screenshot({ + path: "test-results/desktop-stop/03-unconfirmed.png", + }); + await page.evaluate(() => { + ( + window as typeof window & { __STOP_FIXTURE__: { confirmed: boolean } } + ).__STOP_FIXTURE__.confirmed = true; + }); + await desktops + .getByRole("button", { name: "Retry same Stop", exact: true }) + .click(); + await expect( + desktops.getByText("Stop confirmed by Lab Desktop.", { exact: true }), + ).toBeVisible(); + const result = await page.evaluate( + () => + ( + window as typeof window & { + __STOP_FIXTURE__: { prepared: number; sends: string[] }; + } + ).__STOP_FIXTURE__, + ); + expect(result.prepared).toBe(1); + expect(result.sends).toHaveLength(2); + expect(result.sends[1]).toBe(result.sends[0]); + await waitForAnimations(page); + await desktops.screenshot({ + path: "test-results/desktop-stop/04-confirmed.png", + }); +});