Files
vdm/docs/06-risks-and-spikes.md
T
samiandClaude Sonnet 5 c2eef96175 gui: floating drop target, clipboard global shortcut, theming, UI watchdog
Continues the build order past Options/Scheduler/Speed Limiter/Batch/
Grabber/tray.

- DropTargetWidget: frameless always-on-top drop target (docs/03-gui-spec.md
  §5), position persisted, accepts a dropped http(s) URL or link text and
  opens FileInfoDialog directly (skipping Add URL, since the URL is already
  known). Shown/hidden from general.showDropTarget, live via
  event.settings.changed, same pattern MainWindow already used for
  general.minimizeToTray.
- Clipboard, explicit path #2 (docs/06-risks-and-spikes.md R2):
  GlobalShortcut wraps org.freedesktop.portal.GlobalShortcuts
  (CreateSession -> BindShortcuts -> Activated), triggering the same Add URL
  flow. Guarded end-to-end on `if(TARGET Qt6::DBus)` / VELOX_GUI_HAVE_DBUS
  so a build without the component degrades to "feature skipped," not
  broken (gui/docs/pkg-qa-requests-m1.md R4). Best-effort by design per the
  risk doc: fails silent, never advertised.

  Verified live against the real portal (a real Wayland session, not just
  offscreen): `CreateSession` refuses every caller with "An app id is
  required" — reproduced identically via a bare `busctl` call with no Qt
  involved at all, so this is the portal requiring a sandboxed caller
  identity, not something fixable from an unconfined process. Recorded as
  a partial Spike S2 answer in docs/06-risks-and-spikes.md: this explicit
  path likely doesn't work for Velox as a traditionally-packaged app on
  stock GNOME, only if/when it ships confined. Also fixed a real leak this
  verification caught: QDBusInterface's introspection cache reads as a
  LeakSanitizer leak the first time anything touches D-Bus (tst_rtl went
  red under ASan) — switched to QDBusMessage::createMethodCall, which
  needs no introspection.
- Theming (docs/03-gui-spec.md §7): gui/resources/qss/{idm-like,dark}.qss,
  each with a documented palette block up top (QSS itself has no variable
  syntax), applied by ThemeManager and kept live via
  QStyleHints::colorSchemeChanged. util/Theme.hpp gives the handful of
  inline C++ styles (status dot, offline banner, the eleven identical
  error-label styles across dialogs) named constants instead of a twelfth
  copy of the same hex.
- UiThreadWatchdog: the M1 DoD's 200 ms debug-build watchdog. A background
  std::thread pings the UI thread every 50 ms via a queued invokeMethod and
  warns once (not per-poll) if a ping goes unanswered past 200 ms; no
  QThread, no Qt event loop of its own, so the watchdog itself can never be
  what blocks the thread it watches. No-op in a release build. Proven both
  ways in tst_uithreadwatchdog: fires on a genuinely blocked UI thread
  (synchronous sleep, no processEvents) and stays silent on a responsive
  one.

Full non-conformance suite (55 tests across every lane, `ctest -LE
conformance`) passes clean at this point, including the whole gui label
under ASan+UBSan.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
2026-09-12 21:30:13 +04:00

6.7 KiB

06 — Risks and spikes

Each spike is a timeboxed M0/M1 investigation with a written answer in docs/adr/. Do not let any of these be discovered in M6.


R1 — Snap-packaged Firefox blocks native messaging ⚠ HIGH

Evidence gathered on this machine (2026-09-09):

$ snap list firefox        →  firefox 154.0  mozilla**
$ snap connections firefox →  home            connected
                              network         connected
                              network-bind    connected
                              personal-files  (dot-mozilla-firefox)

Ubuntu ships Firefox as a strictly-confined snap. Executing a native-messaging host binary that lives outside the snap's confinement has a long history of breaking, and even when the manifest is found, the host runs under the snap's constraints.

Why it's already handled: network and network-bind are connected, so a loopback WebSocket to 127.0.0.1 is available regardless. The dual-transport design in docs/05-extension-spec.md §4 is not belt-and-braces engineering for its own sake — it is the direct consequence of this finding.

Spike S1 (2 days, M0, lane EXT): on a clean 26.04 VM, install a trivial native host into each of the four manifest locations and record exactly which ones snap Firefox can launch, and whether the launched process can reach $XDG_RUNTIME_DIR. Write the result to docs/adr/0003-native-messaging-under-snap.md.

Decision rule: if native messaging works → prefer it, keep WS as fallback. If it does not → WS becomes the primary path, velox-nmhost still ships for deb/flatpak/tarball Firefox users, and the installer detects the snap and configures pairing automatically. Either way, M2 ships. The installer must detect which Firefox is in use and say so.


R2 — Wayland clipboard monitoring ⚠ HIGH (feature-shaping)

Ubuntu 26.04 defaults to GNOME on Wayland. A Wayland client cannot passively observe clipboard changes made by other applications — that is a deliberate security property, not a bug, and it is the mechanism IDM's clipboard capture relies on.

Spike S2 (2 days, M1, lane GUI): test, on this exact desktop, (a) whether Qt receives QClipboard::dataChanged for copies made in another app, (b) whether Mutter exposes ext-data-control-v1 / wlr-data-control, (c) whether org.freedesktop.portal.GlobalShortcuts gives a reliable "grab clipboard now" hotkey, (d) XWayland fallback behaviour.

Ship-regardless design: the extension context menu covers the browser case (where almost all copied download links come from), the portal global shortcut covers explicit capture, and the Add-URL dialog pre-fills from the clipboard when opened. Background monitoring is a bonus if the spike says yes. Do not let this block the release, and do not promise it in the UI before S2 answers.

Spike S2, item (c) answered — verified live, not the full spike: on this desktop (GNOME/Mutter, Ubuntu 26.04, plain non-Flatpak/non-snap process), CreateSession on org.freedesktop.portal.GlobalShortcuts refuses every caller with "An app id is required" — reproduced two ways: gui/src/clipboard/GlobalShortcut.cpp's real async D-Bus call, and a bare busctl --user call … CreateSession from an interactive shell (no Qt involved at all), both under a real Wayland session (WAYLAND_DISPLAY set), not just offscreen. Because the second reproduction has no Qt/app-level identity to configure at all and still fails identically, this looks like the portal requiring the caller's bus connection to already carry a sandboxed app id (Flatpak/snap portal-confined) — something no amount of QGuiApplication::setDesktopFileName() or similar can supply from an unconfined process. Practical read: the global-shortcut explicit path likely does not work at all for Velox as a traditionally-packaged (.deb/AppImage) app on stock GNOME — only if/when it ships confined. The code is still in (best-effort, fails silent exactly like this, never advertised — see the file's own header), since it costs nothing and activates automatically the day that changes. Items (a) QClipboard::dataChanged cross-app, (b) wlr-data-control, and (d) XWayland fallback are still unanswered — this was one item of S2's four, not the full spike.


R3 — AMO review friction 🟠 MEDIUM

<all_urls> + webRequestBlocking + nativeMessaging is a heavyweight permission set; a download manager legitimately needs it, but reviews take longer and can bounce. Mitigation: run web-ext lint in CI from day one, no remote code execution at all (no CDN scripts, no eval), ship readable source with a build-reproduction script, write the permission justification in M1 rather than at submission, and submit an early unlisted build in M3 to shake out review problems while there's still time.


R4 — Servers that lie about ranges 🟠 MEDIUM

Accept-Ranges: bytes present but 206 never delivered; ETags that change per request; CDNs that 403 a second connection; signed URLs that expire mid-download. Mitigation: resumability is proven by an actual 206 with a matching Content-Range (docs/04 §2); tools/testserver implements each of these as an explicit hostile mode and CORE's DoD requires passing all of them; download.refreshUrl exists so the user can paste a fresh signed URL into a running task.


R5 — Qt 6 LGPL compliance 🟢 LOW but do it right

Dynamic linking against unmodified system Qt satisfies LGPLv3. Do not static-link Qt into the AppImage without reading the terms; if the AppImage bundles Qt, bundle it as shared objects and ship the relink information. Record in docs/adr/0002-qt-licensing.md.


R6 — ffmpeg/libav licensing for the media grabber 🟢 LOW

Depend on the distro's ffmpeg rather than bundling; keep the muxer behind a runtime check so the app degrades gracefully when ffmpeg is absent. Never bundle a GPL build into a package whose licence conflicts.


R7 — Contract drift between lanes 🟠 MEDIUM

The classic parallel-development failure: GUI and extension each "fix" the protocol in their own tree and integration in M2 becomes a rewrite. Mitigation: the entire contracts/ discipline — single owner, generated code, golden fixtures, conformance suite as a merge gate. If a lane finds the contract wrong, it opens a contracts/-only PR; it does not work around it locally. This is the single most important process rule in the project.


R8 — Scope creep into a browser-agnostic product 🟢 LOW

Chrome/Chromium support means losing blocking webRequest and rebuilding capture on declarativeNetRequest + downloads.onDeterminingFilename, which is a different design. Ship Firefox 1.0 first. Revisit after M7 as its own project, not as an M3 side quest.