Desktop remote control: the GoodQA desktop app is the one install and opt-in path

Status (2026-10-08): visible by default after sign-in when the selected organization enables Machines. The desktop app asks its configured API and caches the answer for one minute, scoped to the deployment, account and selected organization. Login, account, deployment and organization changes invalidate the answer; an existing session is paused before access is checked again. API refusal or unavailable access stays off. Missing desktop-agent.json now means available. Either GOODQA_DESKTOP_AGENT=0 or {"enabled":false} in that file in the app's userData directory disables it; a file-level false wins over GOODQA_DESKTOP_AGENT=1. Malformed/unreadable switches fail closed. No platform setting is changed by the app.

Availability exposes Remote control inside the desktop Settings window, the menu-bar dropdown and tray shortcuts. The first-run card opens the existing confirmed enrollment flow. The machine itself remains unenrolled until its owner chooses Set up and confirms. Settings shows connection status, pause/resume/stop/block controls, live local permission rows, permission-fix buttons and diagnostics. macOS probes Screen Recording and Accessibility without prompting until a fix button is pressed. Windows displays the signed command-helper status and explains that administrator-window input requires a separate UIAccess helper, which this build does not implement. Linux reports X11 or Wayland and its limitation.

Organization enrollment, people/bot/persona access and audit are in organization Settings > Remote Control (/o/{orgSlug}/settings/remote-control). The legacy /settings/machines route redirects there. Members see owned/shared machines; admins can review all and revoke/audit, while the existing server requires the machine owner to issue new grants. Browser permissions continue through the existing per-connection consent flow. Local Linux browser/Electron evidence and native pending steps are in acceptance/remote-control-visible.live.json.

  • API reference: /v1/openapi.json
  • MCP tools: tool reference
  • Runbook: This computer user guide and Machines runbook
  • Design: desktop agent, GoodQA Machines

1. What the person sees

A Remote control drop-down in the main window's menu bar, and the same drop-down in the tray (one menu model, apps/desktop/src/remote-control/menu.ts, so the two can never disagree). The first row is always the status line:

StateStatus lineRows
off (the default)Remote control is offEnable remote desktop on this machine…
setting upSetting up remote control… / Waiting for your OK in the system prompt… / …Continue setting up remote control…
on, idleRemote control on — not in use (· Full power)End this session, Pause, Block for an hour, Block until I turn it on, Remote control settings…, Details and diagnostics…, Turn off remote desktop on this machine…
in useIn use by Ada (bot) since 14:02 — running: screenshotsame, plus Block Ada (bot) from this computer after End this session; the tray also puts both at the top level
pausedRemote control paused — nothing runs until you resumeResume instead of Pause
blockedRemote control blocked until 15:02 / until you turn it onTurn it back on (unblock), no second block

End this session and Block from this computer are different promises (owner directive 2026-10-13, ruling R10). End this session ends the current lease and stops running work; the same bot, agent or person may ask again (and, in click-to-accept mode, must be allowed again). Block also ends that session, and the host (goodqa-agentd principals.block) keeps the controller's kind and id on disk: every later lease of that controller is refused on this computer, in both consent modes, until the owner lifts it (principals.unblock, owner only). goodqa-machine asks the host about the controller of every command it runs (gate.state with principal) and refuses a blocked one with controller_blocked. Pause and Block until I turn it on stop all remote access, for everyone. An older host without principals.block refuses the per-controller block rather than pretending (no silent fallback). The block lives on this computer; GoodQA may still issue the controller a lease, which this computer then refuses.

Block covers every control path on this computer, not only native commands (fp02 F4/D2). Clicking Block closes that controller's desktop agent-browser consent at once (the same synchronous fence End this session uses) and ends its one-click session (local stop first, then GoodQA); a different controller's browser or one-click session is left running. Afterwards the desktop app reads the host's blockedPrincipals from its status: the agent browser refuses a new consent and every command (including a queued command replayed from before the block) of a blocked controller with controller_blocked, and one-click never activates a session for a blocked agent (it ends it on GoodQA instead). With no host status at all, the agent browser fails closed. There is ONE block list: the host's status merges the person's blocks with R10's control-mode policy lists (BlockedClients as kind:id, BlockedPersons as user), and R10 admission refuses the person's blocks too. An empty kind or id never matches. What is NOT prevented: GoodQA's API cannot see this computer's list, so it can still issue a lease or queue commands for a blocked controller; this computer refuses to run them. There is no menu to list or lift blocks yet; lifting one is the owner IPC principals.unblock.

Choosing Enable remote desktop on this machine… opens ONE guided window (apps/desktop/src/remote-control/index.html):

  1. You are signed in. The desktop sign-in is the authority. No pairing code, nothing to copy.
  2. Which organisations may use this computer (at most ten ticked boxes; an organisation where the person may not add computers is shown greyed with the reason).
  3. Which bots, agents and tokens, and what each may do, listed per ticked organisation by GoodQA. For each: a preset, Resting (see the screen and facts; risky work waits for approval), QA tablet (adb only, with the listed commands), Full power (everything, including administrator tasks, no approval), or Custom (capability boxes); an "Only these commands" list (one per line, * is the only wildcard; anchored at both ends, judged by the server's own command-scope.ts); a folder list for file access; and "Ask me before anything risky". QA tablet opens files only in the listed folders, and requests no file access when none are listed. Custom with files needs at least one full folder path. Invalid paths are refused before the system prompt.
  4. Name and labels (name defaults to the computer's host name).
  5. What happens when you press Enable: the OS steps, and how many system prompts (Windows 0, Linux 1, macOS 0).

Then Enable validates the choices and asks the bundled agent what enrollment methods it supports, without starting it or changing the OS. A build without enroll.begin or enroll.complete (or one that holds more organizations than the bundled agent can enroll in one go: goodqa-agentd methods says limits.maxOrgs) stops here, with no system prompt or install. A supported build runs, in order: the one OS step, install and start the service (so it is on after a reboot), enroll the machine, and on macOS the two guided permission toggles. The window ends on Online.

Turn off remote desktop on this machine… (after a confirmation) stops everything, tells GoodQA (each machine row of this computer is revoked, so every grant and lease on it ends), makes the agent forget its enrollment and removes the service. Offline it still turns off here at once and tells GoodQA when the app next starts.

2. One version, no separate installer

The app's resources carry goodqa-agentd (host), goodqa-elevate (one-operation elevation helper) and the Machines agent goodqa-machine, all built by apps/desktop/agent/scripts/build-matrix.sh with the app's version stamped in (-X main.version) and a VERSION file beside them. scripts/agent-before-pack.cjs fails a packaging run whose bundle lacks goodqa-machine or whose VERSION is not the app's, and the app refuses to enable remote control from such a bundle (remote-control/bundle.ts). The app's own update (electron-updater) is therefore the agent's update: at the next start, if remote control was on, the app reinstalls the service from its new resources (the existing host-upgrade path in this-computer.ts). Packaging here is unsigned only; signing and hosting are not part of this lane.

3. The OS step: exactly what is needed, once, and only after Enable

OSPromptWhat it doesFile
Windowsnone: per-user login tierthe host's login service, installed without elevation. Remote control works while this person is signed in; it does not start at the sign-in screen or before anyone signs in, and the page says so. There is no boot-time tier: the bundled goodqa-agentd has no setup command, so none is offered or promised (decision D6 of the one-click Remote plan, pending the owner's word: a boot tier would be a separate, elevated installer)remote-control/elevation.ts
macOSno administrator promptper-user LaunchAgent from inside the signed bundle (existing service install), then the guided toggles: Accessibility and Screen Recording with live green/red dots, the agent restarted automatically once after the Screen Recording grant; "Continue" without one says which capability will report an error (never a blank frame)elevation.ts MacPermissionProbe
Linuxone polkit promptloginctl enable-linger <user> (skipped when linger is already on), verified afterwards; when it stays off the person is told "will NOT start after a reboot until you sign in" (machines spec V2: the old installer discarded this error); then the systemd user unitelevation.ts

Cancelling the prompt is "nothing was changed" and goes back to the choices. The controller calls the OS only in the elevating phase, which is reachable only from Enable (state.ts mayElevate); a test drives every other entry point and counts zero elevations, with a planted elevation as the negative control.

If a later step fails after the administrator step succeeded, the message says what remains: Linux linger or the Windows start-up tasks and background-task right. It does not claim that nothing changed. Linger stays on because other background services may now depend on it; the message includes the command to turn it off.

4. Enrollment without a pairing code

Enrollment needs server and agent support that is not available in this build. Enable refuses before changing the OS; the intended flow below is tested against a local stand-in, and cannot yet complete against the shipped server.

app (signed-in session)            goodqa-agentd / goodqa-machine               GoodQA
-----------------------            ------------------------------               ------
                           IPC     enroll.begin = `goodqa-machine identity --json`
                                   -> { publicKey, hostname, os, arch, agentVersion } (private key stays)
POST /v1/me/machine-enrollments  { orgIds, preset: resting, grants: [], name, labels, mode: attended,
                                   machine: { publicKey, hostname, os, arch } } ------------------------------>
<------------------------------------------------------------ 201 { enrollment: { id }, enrollSecret: gqmx_... }
                           IPC     enroll.complete { apiUrl, enrollSecret, fullPower }
                                   = `goodqa-machine enroll --exchange - --api URL --json`, secret on stdin
                                   -> POST /v1/machine-agent/enroll, signed with the device key ------------->
GET /v1/me/machine-enrollments/{id}  -> completed, machines: [{ id, orgId }]   (the server's word, not the agent's)
POST /v1/machines/{machineId}/grants  one per chosen bot, as the person chose it (existing grantBody, strict)

Why the app makes the grants itself: the server's enrollment presets apply one preset to every chosen principal, while the flow lets the person choose per bot (Resting, QA tablet with its command list, Full power, or custom). So the enrollment carries preset: resting and no grants (the server makes none), and the app posts each grant on the same session right after. That is not atomic on the server, so the app makes it atomic for the person: if any step after the handoff fails, a pending enrollment is cancelled, every machine the exchange made is revoked, and the service is removed; an app that quits mid-way finds the in-flight enrollment in its store and undoes it at the next start. Full power grants the whole disk (paths: ["/"], every drive root on Windows), as the server's own full preset does.

The desktop's per-bot QA tablet preset follows the agent's tablet tier; it is distinct from the server enrollment qa-tablet preset described in ENROLLMENT.md. The desktop always starts a resting enrollment with no grants and then posts each bot's exact grant body. The in-flight marker is persisted before the secret handoff, so recovery also covers an app exit during the exchange.

The gqmx_ secret goes from the server's answer to the local agent and nowhere else: never stored, shown or logged. Controller messages and log lines are scrubbed of every gqm*_/gqut_ shape, and the desktop Sentry scrub knows them. The app never holds a machine credential (gqmt_), a device private key or a pairing code; the renderer never sees the session token (ipc-contract.ts parses every payload and refuses unknown fields).

Organisations come from GET /v1/orgs, with GET /v1/orgs/{id}/machines as the per-org probe (200 = Machines is on there, 404 = dark, 403 = not allowed). Bots and agents come from GET /v1/orgs/{id}/api-keys/agents (bot keys, member floor) and GET /v1/orgs/{id}/personas. A person's user token is not offered as a grantee: ENROLLMENT.md section 5 rule 4 keeps elevated token grants behind their own gate (machines.tokenGrants).

5. State machine

off -> choosing -> elevating -> installing -> enrolling -> [permissions, macOS] -> connecting -> online <-> in_use, paused, blocked, and turning_off -> off (apps/desktop/src/remote-control/state.ts). The status line, the menu and the window all render from it. "In use" names the holder and the time the lease started (the agent's since, or when the app first saw it). A timed block is the stricter of the agent's and the app's record; the app lifts its own when the time is over (needed while the host lacks a timed block).

We use essential cookies to keep you signed in. See our Privacy Policy for details.