scaffold: project structure, wire contract, roadmap and agent briefs
Lays out Velox Download Manager (IDM-class download manager for Ubuntu 26.04) as a monorepo ready for parallel lane development. No implementation code by design. - docs/: architecture, roadmap M0-M7, IDM-parity GUI spec, engine design, Firefox extension spec, risks/spikes, packaging - contracts/: wire-contract skeleton (JSON Schema + fixture templates) — the single synchronization point between lanes - docs/agents/: one brief per lane (PROTO, CORE, DAEMON, GUI, EXT, PKG/QA) with owned directories, build order and definition of done - CLAUDE.md: rules of engagement — lane ownership, layering, non-negotiables - CMake scaffolding with dev/tsan/release/ci presets Two environment findings shape the design: Firefox here is the Mozilla snap (native-messaging risk, so the extension carries a loopback-WebSocket fallback), and Wayland forbids passive clipboard monitoring (so clipboard capture is explicit-action-first). Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
|||||||
|
build/
|
||||||
|
.cache/
|
||||||
|
compile_commands.json
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
web-ext-artifacts/
|
||||||
|
*.o
|
||||||
|
*.so
|
||||||
|
*.a
|
||||||
|
*.log
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
.DS_Store
|
||||||
|
*.veloxpart
|
||||||
|
*.veloxpart.meta
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Rules of engagement — agents working in this repo
|
||||||
|
|
||||||
|
Read this before touching anything. Then read your lane brief in [docs/agents/](docs/agents/).
|
||||||
|
|
||||||
|
## 1. Stay in your lane
|
||||||
|
|
||||||
|
| Lane | Owns | Never writes |
|
||||||
|
|---|---|---|
|
||||||
|
| PROTO | `contracts/`, `tools/mockd/`, `tests/conformance/` | everything else |
|
||||||
|
| CORE | `core/`, `tools/bench/`, `tools/fuzz/` | `daemon/`, `gui/`, `extension/`, `contracts/` |
|
||||||
|
| DAEMON | `daemon/`, `cli/`, `nmhost/`, `packaging/nativehost/` | `core/`, `gui/`, `extension/`, `contracts/` |
|
||||||
|
| GUI | `gui/` | everything else |
|
||||||
|
| EXT | `extension/` | everything else |
|
||||||
|
| PKG/QA | `packaging/`, `.github/`, `tools/testserver/`, `tests/integration/`, `tests/e2e/`, root build files | any lane's feature code |
|
||||||
|
|
||||||
|
If your task seems to require editing another lane's files, that is a signal the interface
|
||||||
|
is wrong. **File the request; don't reach across.**
|
||||||
|
|
||||||
|
## 2. `contracts/` is sacred
|
||||||
|
|
||||||
|
- Only PROTO commits there.
|
||||||
|
- Generated code (`core/generated/`, `extension/src/shared/protocol/`) is committed and
|
||||||
|
**must never be hand-edited**. Fix the schema, regenerate.
|
||||||
|
- Need a new field? Open a `contracts/`-only PR: schema + fixtures + regenerated code +
|
||||||
|
`VERSION` bump. Optional field or new method = minor; rename/remove/retype = major + ADR.
|
||||||
|
- Working around a wrong contract locally is the single failure mode most likely to sink
|
||||||
|
this project. Don't.
|
||||||
|
|
||||||
|
## 3. Layering
|
||||||
|
|
||||||
|
```
|
||||||
|
core → no JSON, no SQL, no Qt, no RPC. Ever.
|
||||||
|
daemon → depends on core. No Qt.
|
||||||
|
gui / ext → zero download logic. They render state and forward user intent.
|
||||||
|
nmhost → a dumb pipe. Under 300 lines. No logic.
|
||||||
|
```
|
||||||
|
|
||||||
|
A grep for `curl|pwrite|sqlite` in `gui/` must come back empty. Same for download logic in
|
||||||
|
`extension/`.
|
||||||
|
|
||||||
|
## 4. Non-negotiable behaviours
|
||||||
|
|
||||||
|
- **Capture fails open.** Daemon down, slow, or erroring → Firefox downloads normally.
|
||||||
|
Never swallow a user's download. `capture.offer` answers within 750 ms or the extension
|
||||||
|
gives up.
|
||||||
|
- **Resume is validated.** `If-Range` with ETag/Last-Modified; a `200` where `206` was
|
||||||
|
expected means the file changed — ask the user, never silently corrupt.
|
||||||
|
- **Never bind beyond `127.0.0.1`.** The WS transport is token-authenticated, origin-checked,
|
||||||
|
and rate-limited.
|
||||||
|
- **Secrets go to the Secret Service**, never SQLite, never logs.
|
||||||
|
- **Paths are canonicalized** and checked against allowed roots before any write.
|
||||||
|
- **No allocation in the transfer hot path.**
|
||||||
|
|
||||||
|
## 5. Definition of done, everywhere
|
||||||
|
|
||||||
|
Code + tests + docs updated in the same change. A feature with no test does not exist. If
|
||||||
|
you change observable behaviour, update the doc in `docs/` that describes it in the same PR.
|
||||||
|
|
||||||
|
## 6. Style
|
||||||
|
|
||||||
|
- C++23, `-Wall -Wextra -Werror`, clang-format (config at root), clang-tidy clean.
|
||||||
|
- TypeScript strict mode, ESLint, no `any` on protocol boundaries.
|
||||||
|
- Commit messages: `lane: imperative summary` (e.g. `core: add dynamic segment stealing`).
|
||||||
|
- One logical change per commit. Rebase onto `main`; no merge commits.
|
||||||
|
|
||||||
|
## 7. When you're unsure
|
||||||
|
|
||||||
|
Ask in the PR rather than guessing at the interface. A day of clarification is cheaper than
|
||||||
|
an M2 integration rewrite. And record real decisions as an ADR in `docs/adr/` — the next
|
||||||
|
agent to touch this will have none of your context.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Velox Download Manager — top-level build
|
||||||
|
# SCAFFOLDING ONLY. Lane PKG/QA owns this file; each add_subdirectory() is enabled by the
|
||||||
|
# owning lane when it lands its first target. Nothing here builds yet by design.
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.28)
|
||||||
|
|
||||||
|
project(velox
|
||||||
|
VERSION 0.1.0
|
||||||
|
DESCRIPTION "IDM-class download manager for Linux"
|
||||||
|
LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
option(VELOX_BUILD_GUI "Build the Qt 6 GUI client" ON)
|
||||||
|
option(VELOX_BUILD_TESTS "Build unit and integration tests" ON)
|
||||||
|
option(VELOX_BUILD_FUZZ "Build libFuzzer targets (clang only)" OFF)
|
||||||
|
option(VELOX_ENABLE_MEDIA "Build the HLS/DASH media grabber (M4)" OFF)
|
||||||
|
|
||||||
|
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||||
|
if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
|
||||||
|
add_compile_options(-Werror)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# --- Dependencies (see README bootstrap; none are vendored) --------------------
|
||||||
|
# find_package(CURL 8.0 REQUIRED)
|
||||||
|
# find_package(SQLite3 REQUIRED)
|
||||||
|
# find_package(nlohmann_json 3.11 REQUIRED)
|
||||||
|
# find_package(OpenSSL REQUIRED)
|
||||||
|
# if(VELOX_BUILD_GUI)
|
||||||
|
# find_package(Qt6 6.6 REQUIRED COMPONENTS Widgets Svg Network LinguistTools)
|
||||||
|
# qt_standard_project_setup()
|
||||||
|
# endif()
|
||||||
|
|
||||||
|
# --- Targets: each lane enables its own line ----------------------------------
|
||||||
|
# add_subdirectory(core) # lane CORE → libveloxcore
|
||||||
|
# add_subdirectory(daemon) # lane DAEMON → veloxd
|
||||||
|
# add_subdirectory(cli) # lane DAEMON → velox
|
||||||
|
# add_subdirectory(nmhost) # lane DAEMON → velox-nmhost
|
||||||
|
# if(VELOX_BUILD_GUI)
|
||||||
|
# add_subdirectory(gui) # lane GUI → velox-gui
|
||||||
|
# endif()
|
||||||
|
# if(VELOX_BUILD_TESTS)
|
||||||
|
# enable_testing()
|
||||||
|
# add_subdirectory(tests)
|
||||||
|
# endif()
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"version": 6,
|
||||||
|
"cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 },
|
||||||
|
"configurePresets": [
|
||||||
|
{
|
||||||
|
"name": "base",
|
||||||
|
"hidden": true,
|
||||||
|
"generator": "Ninja",
|
||||||
|
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||||
|
"cacheVariables": { "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"inherits": "base",
|
||||||
|
"displayName": "Debug + ASan/UBSan (default for all lanes)",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_BUILD_TYPE": "Debug",
|
||||||
|
"CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer -g",
|
||||||
|
"VELOX_BUILD_TESTS": "ON"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tsan",
|
||||||
|
"inherits": "base",
|
||||||
|
"displayName": "Debug + ThreadSanitizer (required for CORE and DAEMON)",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_BUILD_TYPE": "Debug",
|
||||||
|
"CMAKE_CXX_FLAGS": "-fsanitize=thread -fno-omit-frame-pointer -g",
|
||||||
|
"VELOX_BUILD_TESTS": "ON"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "release",
|
||||||
|
"inherits": "base",
|
||||||
|
"displayName": "RelWithDebInfo + LTO (benchmarks and packages)",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
|
||||||
|
"CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON",
|
||||||
|
"VELOX_BUILD_TESTS": "OFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ci",
|
||||||
|
"inherits": "dev",
|
||||||
|
"displayName": "CI build",
|
||||||
|
"cacheVariables": { "VELOX_BUILD_FUZZ": "ON" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buildPresets": [
|
||||||
|
{ "name": "dev", "configurePreset": "dev" },
|
||||||
|
{ "name": "tsan", "configurePreset": "tsan" },
|
||||||
|
{ "name": "release", "configurePreset": "release" },
|
||||||
|
{ "name": "ci", "configurePreset": "ci" }
|
||||||
|
],
|
||||||
|
"testPresets": [
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"configurePreset": "dev",
|
||||||
|
"output": { "outputOnFailure": true },
|
||||||
|
"execution": { "noTestsAction": "error", "stopOnFailure": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Velox Download Manager (VDM)
|
||||||
|
|
||||||
|
An IDM-class download manager for Ubuntu 26.04 LTS: multi-segment accelerated HTTP(S)
|
||||||
|
downloading, resume, categories and automatic file distribution, queues and scheduler,
|
||||||
|
speed limiter, a Firefox extension that captures downloads automatically, and clipboard
|
||||||
|
link capture.
|
||||||
|
|
||||||
|
> **Name is a placeholder.** Binaries are `veloxd`, `velox-gui`, `velox`, `velox-nmhost`.
|
||||||
|
> Rename before first release if you want something else — do it in M0, never later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Phase M0 — scaffolding.** No implementation code exists yet. This repository currently
|
||||||
|
contains the architecture, the wire contract, the roadmap, and one brief per build lane
|
||||||
|
so that several agents can work in parallel without colliding.
|
||||||
|
|
||||||
|
Start here:
|
||||||
|
|
||||||
|
| Document | What it answers |
|
||||||
|
|---|---|
|
||||||
|
| [docs/01-architecture.md](docs/01-architecture.md) | Process model, why four binaries, framework choices and why |
|
||||||
|
| [docs/02-roadmap.md](docs/02-roadmap.md) | Milestones M0–M7, what runs in parallel, exit gates |
|
||||||
|
| [docs/03-gui-spec.md](docs/03-gui-spec.md) | IDM-parity UI: every window, dialog, column, menu |
|
||||||
|
| [docs/04-engine-design.md](docs/04-engine-design.md) | Segmentation, resume, buffers, rate limiting, disk I/O |
|
||||||
|
| [docs/05-extension-spec.md](docs/05-extension-spec.md) | Firefox capture, transports, pairing, media grabber |
|
||||||
|
| [docs/06-risks-and-spikes.md](docs/06-risks-and-spikes.md) | Snap Firefox, Wayland clipboard, and the other landmines |
|
||||||
|
| [docs/07-packaging.md](docs/07-packaging.md) | .deb/PPA, Flatpak, AMO signing, install layout |
|
||||||
|
| [contracts/README.md](contracts/README.md) | **The interface.** Both sides build against this |
|
||||||
|
| [CLAUDE.md](CLAUDE.md) | Rules of engagement for agents working in this repo |
|
||||||
|
|
||||||
|
Per-lane briefs live in [docs/agents/](docs/agents/) — one per agent, each with an owned
|
||||||
|
directory list, a definition of done, and the files it must never touch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture in one picture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────┐ native messaging (stdio JSON) ┌──────────────┐
|
||||||
|
│ Firefox extension │◄───────────── or ──────────────►│ velox-nmhost │
|
||||||
|
│ (MV3, TS) │ loopback WS 127.0.0.1 + token └──────┬───────┘
|
||||||
|
└────────────────────┘ │
|
||||||
|
│
|
||||||
|
┌────────────────────┐ ▼
|
||||||
|
│ velox-gui (Qt 6) │◄──── JSON-RPC 2.0 over Unix socket ──► ┌─────────────┐
|
||||||
|
└────────────────────┘ $XDG_RUNTIME_DIR/velox/velox.sock │ veloxd │
|
||||||
|
│ (daemon) │
|
||||||
|
┌────────────────────┐ │ │
|
||||||
|
│ velox (CLI) │◄───────────────────────────────────────┤ libveloxcore│
|
||||||
|
└────────────────────┘ └──────┬──────┘
|
||||||
|
│
|
||||||
|
SQLite + sparse files
|
||||||
|
```
|
||||||
|
|
||||||
|
The daemon owns all state and all sockets. The GUI is a *view* — closing it does not stop
|
||||||
|
a download. The extension never touches the disk; it hands URL + headers + cookies to the
|
||||||
|
daemon and gets a task id back.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
```
|
||||||
|
vdm/
|
||||||
|
├── contracts/ ⭐ Wire contract: JSON Schema, fixtures, codegen. Frozen per version.
|
||||||
|
├── core/ C++23 libveloxcore — engine. No UI, no RPC, no SQL.
|
||||||
|
├── daemon/ C++23 veloxd — RPC server, scheduler, queues, SQLite store.
|
||||||
|
├── gui/ C++23 velox-gui — Qt 6 Widgets, IDM-parity UI.
|
||||||
|
├── cli/ C++23 velox — scriptable client.
|
||||||
|
├── nmhost/ C++23 velox-nmhost — Firefox native-messaging bridge (thin pipe).
|
||||||
|
├── extension/ TS Firefox MV3 WebExtension.
|
||||||
|
├── tools/
|
||||||
|
│ ├── mockd/ TS mock daemon — lets GUI + extension work before veloxd exists.
|
||||||
|
│ ├── testserver/ Deliberately hostile HTTP server (no Range, flaky, redirects, auth).
|
||||||
|
│ ├── bench/ Throughput and CPU benchmarks.
|
||||||
|
│ └── fuzz/ libFuzzer targets for parsers.
|
||||||
|
├── tests/
|
||||||
|
│ ├── conformance/ Protocol suite. Every lane must pass it. Gate for merging.
|
||||||
|
│ ├── integration/ veloxd + testserver.
|
||||||
|
│ └── e2e/ Playwright: real Firefox + real daemon + real file on disk.
|
||||||
|
├── packaging/ debian/, flatpak/, appimage/, native-host manifests.
|
||||||
|
└── docs/ Everything above, plus adr/ and agents/.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Toolchain bootstrap
|
||||||
|
|
||||||
|
Surveyed on this machine 2026-09-09 — **most of it is already installed**:
|
||||||
|
|
||||||
|
| Present | Version |
|
||||||
|
|---|---|
|
||||||
|
| git · cmake · ninja · g++ · gdb | 2.53.0 · 4.2.3 · — · 15.2.0 (C++23 ready) |
|
||||||
|
| qt6-base-dev · qt6-tools-dev · qt6-tools-dev-tools | ✓ |
|
||||||
|
| libcurl4-openssl-dev · libsqlite3-dev · nlohmann-json3-dev · libssl-dev | ✓ |
|
||||||
|
| libavformat-dev · libavcodec-dev · ffmpeg | ✓ |
|
||||||
|
| clang-format · clang-tidy · python3 · pkg-config | ✓ |
|
||||||
|
|
||||||
|
**Only these four are missing:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update && sudo apt install -y \
|
||||||
|
libqt6svg6-dev \ # GUI: SVG icon rendering
|
||||||
|
libsecret-1-dev \ # DAEMON: Secret Service for site logins
|
||||||
|
nodejs npm \ # EXT + PROTO: extension build, mockd, conformance runner
|
||||||
|
clang # optional: libFuzzer targets in M7
|
||||||
|
```
|
||||||
|
|
||||||
|
Node in the 26.04 archive may lag; if the extension toolchain needs 22+, use `nvm`.
|
||||||
|
|
||||||
|
Verify with `cmake --preset dev && cmake --build --preset dev` once lane CORE lands its
|
||||||
|
first target. Note **CMake 4.2.3** is installed — newer than the `3.28` floor in
|
||||||
|
`CMakeLists.txt`, and it hard-errors on `cmake_minimum_required` below 3.5, so no
|
||||||
|
dependency may ship a pre-3.5 CMake file.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Owner: lane DAEMON. Scriptable client over the same JSON-RPC.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# contracts/ — the wire contract
|
||||||
|
|
||||||
|
**This directory is the interface between every lane.** Owner: agent **PROTO**.
|
||||||
|
Nobody else commits here. Everybody else *generates from* here.
|
||||||
|
|
||||||
|
```
|
||||||
|
contracts/
|
||||||
|
├── VERSION # protocol semver, e.g. 1.0.0
|
||||||
|
├── openrpc.json # human-readable API doc (generated from schema/)
|
||||||
|
├── schema/
|
||||||
|
│ ├── envelope.schema.json # JSON-RPC 2.0 envelope + our error codes
|
||||||
|
│ ├── types/ # Task, Segment, Category, Queue, Settings, CaptureOffer…
|
||||||
|
│ ├── methods/ # one file per method: params + result
|
||||||
|
│ └── events/ # one file per server→client notification
|
||||||
|
├── fixtures/ # golden request/response pairs, replayed by conformance
|
||||||
|
└── codegen/
|
||||||
|
├── gen_cpp.py # → core/generated/ (structs + to_json/from_json)
|
||||||
|
└── gen_ts.py # → extension/src/shared/protocol/ (types + client)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Generated code is committed.** No lane may be blocked because it can't run Python.
|
||||||
|
2. **Hand-editing generated files is a merge blocker.** Fix the schema and regenerate.
|
||||||
|
3. **Every method needs at least one fixture** — a success case and, where meaningful, an
|
||||||
|
error case. A method with no fixture is not done.
|
||||||
|
4. **Versioning:** adding an optional field or a new method → minor bump. Removing,
|
||||||
|
renaming, retyping, or changing a default → **major** bump and a written migration note
|
||||||
|
in `docs/adr/`. `session.hello` rejects a major mismatch with error `-32001` and a
|
||||||
|
message the GUI renders as "Velox needs updating".
|
||||||
|
5. **Changes arrive as a PR to `contracts/` alone**, containing: schema edit + fixtures +
|
||||||
|
regenerated code + `VERSION` bump. Lanes rebase onto it. This is the only synchronization
|
||||||
|
point in the whole project — keep it cheap and frequent rather than big and rare.
|
||||||
|
|
||||||
|
## Transport framing
|
||||||
|
|
||||||
|
| Client | Transport | Framing |
|
||||||
|
|---|---|---|
|
||||||
|
| GUI, CLI | `$XDG_RUNTIME_DIR/velox/velox.sock` | newline-delimited JSON (NDJSON) |
|
||||||
|
| nmhost ← Firefox | stdio | 4-byte little-endian length prefix (Firefox's format) |
|
||||||
|
| nmhost → daemon | same Unix socket | NDJSON |
|
||||||
|
| Extension (fallback) | `ws://127.0.0.1:520xx` | one JSON message per WS text frame |
|
||||||
|
|
||||||
|
All four carry **the same JSON-RPC 2.0 payloads**. The framing differences stop at the
|
||||||
|
transport layer; no method behaves differently depending on how it arrived — except that
|
||||||
|
methods marked `"privileged": true` in the schema are refused over the WebSocket transport.
|
||||||
|
|
||||||
|
## Method surface (v1.0.0 target — expand only via PR)
|
||||||
|
|
||||||
|
### Session
|
||||||
|
| Method | Params → Result |
|
||||||
|
|---|---|
|
||||||
|
| `session.hello` | `{clientType, clientName, protocolVersion, token?}` → `{daemonVersion, protocolVersion, capabilities[], sessionId}` |
|
||||||
|
| `session.pair` | `{clientName, extensionId}` → `{token, expiresAt}` *(WS only; triggers user prompt)* |
|
||||||
|
| `session.subscribe` | `{events[]}` → `{ok}` |
|
||||||
|
|
||||||
|
### Downloads
|
||||||
|
| Method | Params → Result |
|
||||||
|
|---|---|
|
||||||
|
| `download.probe` | `{url, headers?, cookies?, referrer?, userAgent?}` → `{filename, sizeBytes?, mime, resumable, effectiveUrl, suggestedCategoryId}` |
|
||||||
|
| `download.add` | `{url, headers?, cookies?, referrer?, userAgent?, filename?, saveDir?, categoryId?, segments?, bufferBytes?, startMode:"now"\|"later"\|"queue", queueId?, description?, checksum?}` → `{taskId, state}` |
|
||||||
|
| `download.addBatch` | `{items[], defaults}` → `{taskIds[]}` |
|
||||||
|
| `download.list` | `{filter?, sort?, offset?, limit?}` → `{total, items: TaskSummary[]}` |
|
||||||
|
| `download.get` | `{taskId}` → `TaskDetail` (includes `segments[]`) |
|
||||||
|
| `download.start` \| `.pause` \| `.resume` \| `.cancel` | `{taskIds[]}` → `{updated[]}` |
|
||||||
|
| `download.remove` | `{taskIds[], deleteFile:bool}` → `{removed[]}` |
|
||||||
|
| `download.update` | `{taskId, patch:{filename?, saveDir?, categoryId?, queueId?, description?, segments?, bufferBytes?}}` → `TaskSummary` |
|
||||||
|
| `download.refreshUrl` | `{taskId, url, headers?}` → `{ok}` *(IDM's "Refresh Download Address")* |
|
||||||
|
|
||||||
|
### Organisation
|
||||||
|
`category.list` · `category.upsert` · `category.remove` · `queue.list` · `queue.upsert` ·
|
||||||
|
`queue.start` · `queue.stop` · `queue.reorder` · `rules.list` · `rules.upsert` ·
|
||||||
|
`schedule.get` · `schedule.set`
|
||||||
|
|
||||||
|
### Settings & limits
|
||||||
|
`settings.get {keys?}` · `settings.set {values}` · `limiter.get` · `limiter.set {globalBps?, enabled}`
|
||||||
|
|
||||||
|
### Browser integration
|
||||||
|
| Method | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `capture.offer` | `{url, method, headers, cookies, contentType?, contentLength?, contentDisposition?, tabUrl, filename?}` → `{action:"take"\|"ignore", taskId?, reason?}` — **must answer within 750 ms**; the extension gives up and lets Firefox handle it otherwise |
|
||||||
|
| `capture.getRules` | Extension mirrors the daemon's monitored types so the two never disagree |
|
||||||
|
| `media.listVariants` | `{manifestUrl, headers}` → `{variants:[{id,resolution,bitrate,codec,sizeEstimate}]}` |
|
||||||
|
| `media.addVariant` | `{manifestUrl, variantId, ...addParams}` → `{taskId}` |
|
||||||
|
|
||||||
|
### Grabber
|
||||||
|
`grabber.start {startUrl, depth, includePatterns[], excludePatterns[], fileTypes[]}` →
|
||||||
|
`{jobId}`; `grabber.status {jobId}`; `grabber.harvest {jobId, select[]}` → `{taskIds[]}`
|
||||||
|
|
||||||
|
### Events (server → client notifications)
|
||||||
|
| Event | Payload |
|
||||||
|
|---|---|
|
||||||
|
| `event.task.added` / `.removed` | `{taskId, summary?}` |
|
||||||
|
| `event.task.state` | `{taskId, state, error?}` |
|
||||||
|
| `event.task.progress` | **Batched array**, emitted at ≤4 Hz: `[{taskId, downloaded, speedBps, etaSec, segments:[{i,completed,speedBps}]}]` |
|
||||||
|
| `event.speed.global` | `{downBps, activeCount}` |
|
||||||
|
| `event.auth.required` | `{taskId, host, realm, scheme}` |
|
||||||
|
| `event.notify` | `{level, title, body, taskId?}` |
|
||||||
|
| `event.settings.changed` | `{keys[]}` |
|
||||||
|
| `event.grabber.progress` | `{jobId, found, crawled, done}` |
|
||||||
|
|
||||||
|
## Error codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `-32600/-32601/-32602/-32603` | Standard JSON-RPC |
|
||||||
|
| `-32001` | Protocol major version mismatch |
|
||||||
|
| `-32002` | Not paired / invalid token |
|
||||||
|
| `-32003` | Method not permitted on this transport |
|
||||||
|
| `-32010` | Task not found |
|
||||||
|
| `-32011` | Invalid destination path (outside allowed roots, or not writable) |
|
||||||
|
| `-32012` | Disk full |
|
||||||
|
| `-32013` | Probe failed (with `data.httpStatus`) |
|
||||||
|
| `-32014` | Rate limited (pairing brute-force lockout) |
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1.0.0-draft
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "capture.offer — attachment on a monitored type is taken",
|
||||||
|
"description": "Golden fixture. tests/conformance replays this against the real daemon AND the TS client. If either side drifts, this goes red before the lanes ever integrate.",
|
||||||
|
"request": {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 42,
|
||||||
|
"method": "capture.offer",
|
||||||
|
"params": {
|
||||||
|
"url": "https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso",
|
||||||
|
"method": "GET",
|
||||||
|
"tabUrl": "https://releases.ubuntu.com/26.04/",
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:154.0) Gecko/20100101 Firefox/154.0",
|
||||||
|
"Referer": "https://releases.ubuntu.com/26.04/",
|
||||||
|
"Accept": "*/*"
|
||||||
|
},
|
||||||
|
"cookies": [],
|
||||||
|
"contentType": "application/octet-stream",
|
||||||
|
"contentLength": 6228541440,
|
||||||
|
"contentDisposition": "attachment; filename=\"ubuntu-26.04-desktop-amd64.iso\"",
|
||||||
|
"filename": "ubuntu-26.04-desktop-amd64.iso",
|
||||||
|
"origin": "moz-extension://11111111-2222-3333-4444-555555555555"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 42,
|
||||||
|
"result": {
|
||||||
|
"action": "take",
|
||||||
|
"taskId": "$uuid",
|
||||||
|
"reason": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"assertions": [
|
||||||
|
"response arrives within 750 ms",
|
||||||
|
"a task exists afterwards with state in [queued, connecting, downloading]",
|
||||||
|
"the task's saveDir resolves to the 'Programs' category folder for .iso",
|
||||||
|
"the Referer and User-Agent from params.headers are replayed on the daemon's own request"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://velox.dev/schema/methods/capture.offer.schema.json",
|
||||||
|
"title": "capture.offer",
|
||||||
|
"description": "Firefox offers an intercepted response to the daemon. The daemon MUST reply within 750 ms; the extension abandons the offer and lets Firefox download normally on timeout. TEMPLATE — lane PROTO owns the final shape.",
|
||||||
|
"x-privileged": false,
|
||||||
|
"x-transports": ["uds", "ws"],
|
||||||
|
"x-deadlineMs": 750,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"params": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["url", "method", "tabUrl"],
|
||||||
|
"properties": {
|
||||||
|
"url": { "type": "string", "format": "uri" },
|
||||||
|
"method": { "type": "string", "enum": ["GET", "POST"] },
|
||||||
|
"tabUrl": { "type": "string", "format": "uri" },
|
||||||
|
"headers": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Request headers Firefox was about to send, verbatim. Needed for signed-URL and referrer-gated CDNs.",
|
||||||
|
"additionalProperties": { "type": "string" }
|
||||||
|
},
|
||||||
|
"cookies": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Cookies for the URL, so authenticated downloads work outside the browser.",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "value"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"value": { "type": "string" },
|
||||||
|
"domain": { "type": "string" },
|
||||||
|
"path": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contentType": { "type": ["string", "null"] },
|
||||||
|
"contentLength": { "type": ["integer", "null"], "minimum": 0 },
|
||||||
|
"contentDisposition": { "type": ["string", "null"] },
|
||||||
|
"filename": { "type": ["string", "null"], "description": "Extension's best guess; the daemon may override" },
|
||||||
|
"userAgent": { "type": ["string", "null"] },
|
||||||
|
"referrer": { "type": ["string", "null"] },
|
||||||
|
"origin": { "type": "string", "description": "moz-extension://… — the daemon verifies this on the WS transport" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["action"],
|
||||||
|
"properties": {
|
||||||
|
"action": { "type": "string", "enum": ["take", "ignore"] },
|
||||||
|
"taskId": { "type": ["string", "null"], "format": "uuid" },
|
||||||
|
"reason": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"enum": ["excluded_host", "type_not_monitored", "below_min_size", "duplicate",
|
||||||
|
"capture_disabled", "user_declined", null]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://velox.dev/schema/types/TaskSummary.schema.json",
|
||||||
|
"title": "TaskSummary",
|
||||||
|
"description": "One row of the main download list. Everything the GUI table needs, and nothing more. TEMPLATE — lane PROTO owns the final shape.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["taskId", "filename", "state", "createdAt"],
|
||||||
|
"properties": {
|
||||||
|
"taskId": { "type": "string", "format": "uuid" },
|
||||||
|
"filename": { "type": "string", "maxLength": 255 },
|
||||||
|
"saveDir": { "type": "string" },
|
||||||
|
"url": { "type": "string", "format": "uri" },
|
||||||
|
"effectiveUrl": { "type": "string", "format": "uri" },
|
||||||
|
"sizeBytes": { "type": ["integer", "null"], "minimum": 0, "description": "null when the server did not report a length" },
|
||||||
|
"downloadedBytes": { "type": "integer", "minimum": 0 },
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["new", "probing", "queued", "connecting", "downloading", "paused",
|
||||||
|
"retry_wait", "assembling", "verifying", "complete", "failed", "cancelled"]
|
||||||
|
},
|
||||||
|
"speedBps": { "type": "integer", "minimum": 0 },
|
||||||
|
"etaSeconds": { "type": ["integer", "null"], "minimum": 0 },
|
||||||
|
"resumable": { "type": "boolean" },
|
||||||
|
"segments": { "type": "integer", "minimum": 1, "maximum": 32 },
|
||||||
|
"categoryId": { "type": ["string", "null"] },
|
||||||
|
"queueId": { "type": ["string", "null"] },
|
||||||
|
"queuePosition":{ "type": ["integer", "null"], "minimum": 0, "description": "the Q column" },
|
||||||
|
"description": { "type": "string", "maxLength": 1024 },
|
||||||
|
"createdAt": { "type": "string", "format": "date-time" },
|
||||||
|
"lastTryAt": { "type": ["string", "null"], "format": "date-time" },
|
||||||
|
"completedAt": { "type": ["string", "null"], "format": "date-time" },
|
||||||
|
"error": {
|
||||||
|
"type": ["object", "null"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"code": { "type": "integer" },
|
||||||
|
"message": { "type": "string" },
|
||||||
|
"httpStatus": { "type": ["integer", "null"] },
|
||||||
|
"retryable": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Owner: lane CORE. See ../docs/agents/AGENT-CORE.md and ../docs/04-engine-design.md.
|
||||||
|
No JSON, no SQL, no Qt, no RPC in this tree.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Owner: lane DAEMON. See ../docs/agents/AGENT-DAEMON.md.
|
||||||
|
Owns all state: RPC, scheduler, queues, SQLite, pairing.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# 01 — Architecture
|
||||||
|
|
||||||
|
## 1. Decision summary
|
||||||
|
|
||||||
|
| Concern | Decision | Why this and not the alternative |
|
||||||
|
|---|---|---|
|
||||||
|
| Engine language | **C++23** (`libveloxcore`) | You asked for speed and it is the right call here: segment scheduling, a 64 KiB–8 MiB write path, and 32 concurrent sockets are exactly where a GC and a per-chunk allocation tax show up. Rust would be an equally good engine choice, but Qt's C++ API means C++ removes an entire FFI boundary from the GUI lane. |
|
||||||
|
| HTTP stack | **libcurl** (multi interface, `curl_multi_poll`) | Writing an HTTP/1.1+2 client on raw epoll costs weeks and buys nothing: curl already does connection reuse, HTTP/2 multiplexing, TLS, proxies, SOCKS5, Digest/NTLM auth, redirect and cookie semantics. One `curl_multi` handle per worker thread, N easy handles = N segments. |
|
||||||
|
| GUI toolkit | **Qt 6 Widgets** (not QML, not GTK4) | IDM's UI is a dense data grid, a toolbar of big icons, a tree, and ~12 tabbed dialogs. `QTreeView` + a custom `QAbstractItemModel` renders 100k rows without breaking a sweat; QML would need that grid hand-built. GTK4/libadwaita fights you the moment you want a non-GNOME look, and gtkmm's tree/column story is worse. Qt also gives you `QSystemTrayIcon`, drag-and-drop, a global clipboard API, and `QSS` theming to get the IDM look. LGPLv3 dynamic linking is fine for an open-source app. |
|
||||||
|
| Process model | **Daemon + thin clients** | Downloads must survive closing the window, and the extension must work when no window is open. One owner of state also kills every "GUI and extension disagree" bug class. |
|
||||||
|
| Wire protocol | **JSON-RPC 2.0** over Unix domain socket (+ loopback WebSocket for the extension fallback) | One protocol for GUI, CLI, and extension = one contract to keep in sync, and it is trivially mockable so three lanes can build in parallel. D-Bus is more idiomatic on Linux but is painful to speak from a WebExtension and adds a second schema. A D-Bus *shim* can be added in M5 for desktop integration only. |
|
||||||
|
| Persistence | **SQLite** (WAL) | Task list, segment bitmaps, categories, rules, queues, history. Crash-safe, zero admin, one file. |
|
||||||
|
| Extension | **MV3 WebExtension, TypeScript** | Firefox MV3 still allows blocking `webRequest`, which is what makes true "intercept before the browser downloads it" possible — the thing Chrome MV3 took away. |
|
||||||
|
| Build | **CMake ≥ 3.28 + Ninja + presets** | Presets mean every agent and CI run the identical configure line. |
|
||||||
|
|
||||||
|
## 2. Processes
|
||||||
|
|
||||||
|
### `veloxd` — the daemon
|
||||||
|
Owns everything: task list, scheduler, queues, disk, sockets, settings.
|
||||||
|
|
||||||
|
- Single instance, enforced by an abstract-namespace lock socket.
|
||||||
|
- Started by systemd **user** unit `velox.service`, socket-activated by `velox.socket`.
|
||||||
|
Also auto-started by the GUI or nmhost if not running (`systemctl --user start velox`).
|
||||||
|
- Listens on:
|
||||||
|
- `$XDG_RUNTIME_DIR/velox/velox.sock` (mode 0600) — GUI, CLI, nmhost. Peer credentials
|
||||||
|
checked via `SO_PEERCRED`; same-UID only. No token needed, the socket permission *is*
|
||||||
|
the authorization.
|
||||||
|
- `127.0.0.1:<ephemeral, recorded in $XDG_RUNTIME_DIR/velox/ws.port>` — loopback
|
||||||
|
WebSocket for the extension fallback transport. **Token-authenticated + pairing
|
||||||
|
prompt.** See `docs/05-extension-spec.md` §4.
|
||||||
|
- Threads: 1 RPC/event loop, 1 curl-multi transfer thread per ~8 active segments
|
||||||
|
(capped), 1 disk writer thread per active task, 1 timer thread for the scheduler.
|
||||||
|
Never block the RPC loop on disk or DNS.
|
||||||
|
|
||||||
|
### `velox-gui` — Qt 6 client
|
||||||
|
Stateless view. On start: connect → `session.hello` → `download.list` → subscribe to
|
||||||
|
`event.*`. On daemon restart: exponential-backoff reconnect with a banner, never lose the
|
||||||
|
window. Everything it displays comes from the daemon; it holds no download state of its own.
|
||||||
|
|
||||||
|
### `velox` — CLI
|
||||||
|
Same RPC, scriptable: `velox add <url> --dir ~/ISOs --segments 16`, `velox ls`,
|
||||||
|
`velox pause <id>`. Falls out nearly free once the generated client exists, and it is the
|
||||||
|
fastest way to test the daemon before the GUI is ready.
|
||||||
|
|
||||||
|
### `velox-nmhost` — native-messaging bridge
|
||||||
|
Deliberately trivial and boring: reads Firefox's 4-byte-length-prefixed JSON from stdin,
|
||||||
|
writes it to the Unix socket, pumps replies back. **No business logic — ever.** Under 300
|
||||||
|
lines. It exists only because Firefox's native messaging speaks stdio and the daemon
|
||||||
|
speaks sockets. If a feature needs logic, it belongs in the daemon.
|
||||||
|
|
||||||
|
## 3. The layering rule (enforced in review)
|
||||||
|
|
||||||
|
```
|
||||||
|
libveloxcore → knows nothing about RPC, JSON, SQL, or Qt.
|
||||||
|
Input: a DownloadSpec. Output: bytes on disk + callbacks.
|
||||||
|
veloxd → knows RPC, SQL, scheduling. Depends on core. No Qt.
|
||||||
|
velox-gui → knows Qt and the generated client. Zero engine code.
|
||||||
|
extension → knows the browser and the generated TS client. Zero download logic.
|
||||||
|
```
|
||||||
|
|
||||||
|
If the GUI ever needs to know what a "segment steal" is, the layering has been violated.
|
||||||
|
The daemon's job is to project the engine's state into the contract's `TaskDetail` type,
|
||||||
|
and that is the only shape the GUI ever sees.
|
||||||
|
|
||||||
|
## 4. How three lanes build at the same time without breaking each other
|
||||||
|
|
||||||
|
This is the part that makes parallel agents work, so it is a hard process, not a habit:
|
||||||
|
|
||||||
|
1. **`contracts/` is the single source of truth** and is owned by exactly one lane (PROTO).
|
||||||
|
JSON Schema for every type, method, and event, plus an OpenRPC document.
|
||||||
|
2. **Codegen, not hand-written types.** `contracts/codegen/gen_cpp.py` emits
|
||||||
|
`libveloxproto` (structs + (de)serialization); `gen_ts.py` emits `@velox/protocol`.
|
||||||
|
Generated files are committed so nobody is blocked on running the generator. Hand-editing
|
||||||
|
a generated file is a merge-blocking offence.
|
||||||
|
3. **Golden fixtures.** Every method has request/response JSON examples in
|
||||||
|
`contracts/fixtures/`. `tests/conformance/` replays them against *both* the C++ server
|
||||||
|
and the TS client. Green fixtures = the two lanes are compatible, without them ever
|
||||||
|
having run against each other.
|
||||||
|
4. **`tools/mockd`** — a TS mock daemon that serves the fixtures and fakes progress events.
|
||||||
|
The GUI and extension lanes develop against it from day one and never wait for `veloxd`.
|
||||||
|
5. **Protocol changes are a PR to `contracts/` only**, with a `VERSION` bump and updated
|
||||||
|
fixtures. Adding an optional field = minor. Removing/renaming/retyping = major, and
|
||||||
|
`session.hello` refuses a mismatched major with a clear error the GUI shows as
|
||||||
|
"Update Velox".
|
||||||
|
|
||||||
|
## 5. Data locations
|
||||||
|
|
||||||
|
| What | Path |
|
||||||
|
|---|---|
|
||||||
|
| Config | `~/.config/velox/settings.json` |
|
||||||
|
| Database | `~/.local/share/velox/velox.db` |
|
||||||
|
| Logs | `~/.local/state/velox/velox.log` (rotated, 5×2 MiB) |
|
||||||
|
| Runtime sockets | `$XDG_RUNTIME_DIR/velox/` |
|
||||||
|
| Temp/partial data | configurable; default `~/.local/share/velox/temp/` |
|
||||||
|
| Default download root | `~/Downloads/` with category subfolders |
|
||||||
|
|
||||||
|
Partial files: the target file is created **sparse and preallocated at final size** in the
|
||||||
|
destination directory as `<name>.veloxpart`, with `<name>.veloxpart.meta` beside it. On
|
||||||
|
completion the part file is renamed in place — no copy, no second full-size write, and a
|
||||||
|
half-finished download is never mistaken for a real file by other apps.
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# 02 — Roadmap
|
||||||
|
|
||||||
|
Eight milestones. **M0 is the only serialized one** — after the contract freeze, four
|
||||||
|
lanes run in parallel and only re-synchronize at integration gates.
|
||||||
|
|
||||||
|
Durations are given in "lane-weeks" for a focused agent. Treat them as sequencing weight,
|
||||||
|
not a delivery promise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M0 — Foundations & contract freeze *(everything is blocked on this; keep it short)*
|
||||||
|
|
||||||
|
| # | Task | Lane |
|
||||||
|
|---|---|---|
|
||||||
|
| 0.1 | Toolchain bootstrap (`apt` list in README), `git init`, branch policy | PKG |
|
||||||
|
| 0.2 | CMake presets, top-level targets, clang-format/tidy, ASan/UBSan/TSan CI | PKG |
|
||||||
|
| 0.3 | **Write every schema in `contracts/schema/`** for the v1 method surface | PROTO |
|
||||||
|
| 0.4 | `gen_cpp.py` + `gen_ts.py`, generated code committed | PROTO |
|
||||||
|
| 0.5 | Golden fixtures for every method; `tests/conformance/` runner (C++ + TS) | PROTO |
|
||||||
|
| 0.6 | `tools/mockd` serving fixtures + synthetic progress events | PROTO |
|
||||||
|
| 0.7 | `tools/testserver` — a *hostile* HTTP server: no-Range, flaky, redirect chains, 401, slow-loris, changing ETag | QA |
|
||||||
|
| 0.8 | Decide product name + icon set licence; ADR for each M0 decision | PKG |
|
||||||
|
|
||||||
|
**Exit gate:** `tools/mockd` answers every fixture; the generated C++ and TS clients both
|
||||||
|
round-trip every fixture; CI is green on an empty repo. **Freeze `VERSION` at 1.0.0.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M1 — Four lanes in parallel *(the long stretch)*
|
||||||
|
|
||||||
|
### 1A · CORE — engine ⏱ 4–5
|
||||||
|
Probe → segmentation → dynamic stealing → sparse preallocation → ring-buffered `pwrite`
|
||||||
|
→ `.veloxpart.meta` resume → token-bucket limiter → retry policy.
|
||||||
|
**DoD:** downloads a 5 GB file at line rate; kill `-9` at 60 % and resume finishes with a
|
||||||
|
byte-identical SHA-256; every `tools/testserver` hostile mode handled; ASan/TSan clean;
|
||||||
|
`tools/bench` numbers recorded as the baseline.
|
||||||
|
|
||||||
|
### 1B · DAEMON — RPC + state ⏱ 3–4
|
||||||
|
Unix socket + WS listeners, JSON-RPC dispatcher, subscriptions and event batching, SQLite
|
||||||
|
schema + migrations, queues, scheduler, settings, pairing/token store, systemd user units.
|
||||||
|
**DoD:** passes the full conformance suite as a *server*; `velox` CLI can add/list/pause/
|
||||||
|
resume; survives daemon restart with all tasks intact.
|
||||||
|
|
||||||
|
### 1C · GUI — Qt shell against `mockd` ⏱ 4–5
|
||||||
|
Main window, model/delegate table, category tree, Add-URL + File-Info dialogs, progress
|
||||||
|
dialog with segment bars and speed graph, Options tabs wired to `settings.*`, tray, drop
|
||||||
|
target, QSS theming.
|
||||||
|
**DoD:** every screen in `docs/03-gui-spec.md` exists and is driven **entirely** by
|
||||||
|
`mockd`; 10 000 synthetic rows scroll at 60 fps; zero download logic in the GUI tree.
|
||||||
|
|
||||||
|
### 1D · EXT — extension against `mockd` ⏱ 3–4
|
||||||
|
Capture pipeline, both transports + pairing, context menus, popup with live progress,
|
||||||
|
options page, media detection.
|
||||||
|
**DoD:** intercepts a real download in real Firefox and hands it to `mockd`; **fail-open
|
||||||
|
verified by test** (daemon killed → Firefox downloads normally); `web-ext lint` clean.
|
||||||
|
|
||||||
|
> Lanes touch **only their own directories**. The sole shared surface is `contracts/`, and
|
||||||
|
> only PROTO writes there. See `CLAUDE.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M2 — First vertical slice ⏱ 1–2 *(all lanes, one week, together)*
|
||||||
|
|
||||||
|
Swap `mockd` for the real `veloxd`. Click a link in Firefox → extension captures →
|
||||||
|
File-Info dialog appears → download runs multi-segment → progress in GUI *and* popup →
|
||||||
|
file lands in the right category folder → checksum verified.
|
||||||
|
|
||||||
|
**Exit gate:** that flow works end-to-end on a clean Ubuntu 26.04 VM, from a `.deb`, with
|
||||||
|
**snap Firefox**. This is the moment the snap native-messaging risk is settled for real.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M3 — IDM parity ⏱ 3–4 *(parallel again)*
|
||||||
|
|
||||||
|
Categories & automatic file distribution · rules engine (extension/MIME/host/size → folder)
|
||||||
|
· queues + scheduler UI · speed limiter · batch download (wildcards + clipboard blob) ·
|
||||||
|
Site Grabber wizard · Site Logins via Secret Service · proxy/SOCKS5 · duplicate handling ·
|
||||||
|
"Refresh download address" · post-download commands · virus-scan hook.
|
||||||
|
|
||||||
|
**Exit gate:** the IDM feature checklist in this doc's appendix is ticked or has a written
|
||||||
|
"won't do, because…".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M4 — Media grabber ⏱ 2–3
|
||||||
|
HLS/DASH manifest parsing in the daemon, variant enumeration, segment-parallel fetch, mux
|
||||||
|
via ffmpeg, in-page video panel in the extension. DRM streams detected and clearly refused.
|
||||||
|
|
||||||
|
## M5 — Polish ⏱ 2
|
||||||
|
Theming light/dark, i18n + RTL, accessibility pass, notifications, sounds, global shortcut
|
||||||
|
via the portal, Wayland clipboard spike resolved, first-run wizard, tray/drop-target
|
||||||
|
behaviour, `--help` and man pages.
|
||||||
|
|
||||||
|
## M6 — Packaging & release ⏱ 1–2
|
||||||
|
`.deb` + PPA, Flatpak manifest, native-messaging manifests installed to all four locations,
|
||||||
|
AMO submission (signed XPI), autostart, upgrade/migration test from a previous DB version,
|
||||||
|
uninstall leaves no orphan sockets or manifests.
|
||||||
|
|
||||||
|
## M7 — Hardening ⏱ 2
|
||||||
|
libFuzzer on every parser (`Content-Disposition`, HLS, DASH, meta file, JSON-RPC), 72-hour
|
||||||
|
soak with 500 queued tasks, perf targets from `docs/04` §8 enforced in CI, threat-model
|
||||||
|
review of the WS transport, crash reporting (local only, opt-in, no telemetry).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency graph
|
||||||
|
|
||||||
|
```
|
||||||
|
M0 ──┬──► 1A CORE ──┐
|
||||||
|
├──► 1B DAEMON ─┼──► M2 ──► M3 ──► M4 ──► M5 ──► M6 ──► M7
|
||||||
|
├──► 1C GUI ────┤
|
||||||
|
└──► 1D EXT ────┘
|
||||||
|
(all four against mockd, no cross-lane blocking)
|
||||||
|
```
|
||||||
|
|
||||||
|
Critical path: **M0 → 1A/1B → M2**. GUI and EXT can absorb schedule slack because `mockd`
|
||||||
|
never blocks them. If you must cut scope, cut M4 (media) before anything else — it is the
|
||||||
|
largest chunk of work with the least effect on core parity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix — IDM feature checklist
|
||||||
|
|
||||||
|
| IDM feature | Milestone | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Multi-segment accelerated download | M1 | Dynamic stealing, not static split |
|
||||||
|
| Resume broken/interrupted downloads | M1 | With `If-Range` revalidation |
|
||||||
|
| Browser integration / auto-capture | M1D+M2 | Firefox first; Chrome later if wanted |
|
||||||
|
| Download categories + auto file distribution | M3 | Rules engine |
|
||||||
|
| Queues + scheduler | M3 | Per-queue time windows |
|
||||||
|
| Speed limiter | M1/M3 | Engine in M1, UI in M3 |
|
||||||
|
| Batch downloads / wildcards | M3 | |
|
||||||
|
| Site Grabber | M3 | Depth-limited crawler with filters |
|
||||||
|
| Video/media grabber | M4 | HLS/DASH; no DRM |
|
||||||
|
| Drag-and-drop drop target | M1C | |
|
||||||
|
| Clipboard monitoring | M5 | Wayland-limited; see risks |
|
||||||
|
| Proxy / SOCKS5 / site logins | M3 | Credentials in Secret Service |
|
||||||
|
| Checksum verification | M1 | MD5/SHA-256 |
|
||||||
|
| On-completion actions (open/shutdown) | M1C/M3 | Shutdown via logind, confirmed |
|
||||||
|
| ZIP preview | — | **Won't do.** Low value on Linux |
|
||||||
|
| Dial-up / VPN auto-redial | — | **Won't do.** Obsolete |
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# 03 — GUI specification (IDM parity)
|
||||||
|
|
||||||
|
Owner: lane **GUI**. Qt 6 Widgets, C++23.
|
||||||
|
|
||||||
|
> **Legal note, stated once:** replicating IDM's *layout, workflow and feature set* is
|
||||||
|
> fine — UI ideas aren't protectable. Copying its *icons, artwork, sounds or exact
|
||||||
|
> logo/branding* is not. Ship an original icon set (a Papirus/Breeze-derived set under a
|
||||||
|
> compatible licence is the cheap route) laid out in the same positions. That gives users
|
||||||
|
> the muscle memory without shipping someone else's assets.
|
||||||
|
|
||||||
|
## 1. Main window
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ Velox Download Manager ─────────────────────────────────────────────── ─ □ ✕ ┐
|
||||||
|
│ Tasks File Downloads View Help │
|
||||||
|
├───────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [+] [▶] [⏸] [⏹] [🗑] [🗑✓] [🗓] [⚙] [🌐] │
|
||||||
|
│ Add URL Resume Pause Stop All Delete Del.Compl Scheduler Options Grabber│
|
||||||
|
├──────────────────┬────────────────────────────────────────────────────────────┤
|
||||||
|
│ ▼ All Downloads │ File Name │Q│ Size │ Status │Time Left│ Speed │Date│
|
||||||
|
│ Unfinished │ ubuntu.iso │1│ 5.8GB │ 47.2 % │ 00:03:11│ 28MB/s│... │
|
||||||
|
│ Finished │ report.pdf │ │ 2.1MB │Complete │ │ │... │
|
||||||
|
│ ▼ Categories │ track.flac │2│ 38MB │ Queued │ │ │... │
|
||||||
|
│ Compressed │ film.mkv │ │ 1.4GB │ Paused │ │ │... │
|
||||||
|
│ Documents │ │
|
||||||
|
│ Music │ │
|
||||||
|
│ Programs │ │
|
||||||
|
│ Video │ │
|
||||||
|
│ ▼ Queues │ │
|
||||||
|
│ Main Queue │ │
|
||||||
|
│ Sync Queue │ │
|
||||||
|
├──────────────────┴────────────────────────────────────────────────────────────┤
|
||||||
|
│ 4 downloads, 1 active ↓ 28.4 MB/s Limit: off Queue: running ● Connected│
|
||||||
|
└───────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Columns** (reorderable, resizable, hideable, persisted): File Name · Q · Size · Status ·
|
||||||
|
Time Left · Transfer Rate · Last Try Date · Description. Sort on any column, ascending and
|
||||||
|
descending, sort state persisted.
|
||||||
|
|
||||||
|
**Implementation:** `QTreeView` + `DownloadTableModel : QAbstractItemModel` fed by
|
||||||
|
`event.task.progress` deltas. Never rebuild the model on an event — apply a row patch and
|
||||||
|
emit `dataChanged` for the touched columns only. Progress bar drawn by a `QStyledItemDelegate`
|
||||||
|
in the Status column. Coalesce progress events on a 250 ms timer; at 20 active downloads
|
||||||
|
you get 4 repaints/sec, not 400.
|
||||||
|
|
||||||
|
**Row context menu:** Open · Open With · Open Folder · Move/Rename · Redownload ·
|
||||||
|
Refresh Download Address · Resume · Pause · Delete · Add to Queue ▸ · Properties ·
|
||||||
|
Copy Download URL.
|
||||||
|
|
||||||
|
**Category tree:** counts per node, drag a row onto a category to re-file it (moves the
|
||||||
|
file on disk and updates the DB in one RPC).
|
||||||
|
|
||||||
|
## 2. Download File Info dialog (appears when a URL is added)
|
||||||
|
|
||||||
|
IDM's signature dialog. Populated from `download.probe`.
|
||||||
|
|
||||||
|
```
|
||||||
|
File Name: [ ubuntu-26.04-desktop-amd64.iso ]
|
||||||
|
Save As: [ /home/sami/Downloads/Programs/ ] [ Browse… ]
|
||||||
|
Category: [ Programs ▾ ] Size: 5.8 GB Resume capability: Yes
|
||||||
|
Description:[ ]
|
||||||
|
Connections:[ 8 ▾ ] Buffer: [ 4 MiB ▾ ] ☐ Remember for this file type
|
||||||
|
|
||||||
|
[ Download Now ] [ Download Later ] [ Add to Queue ▾ ] [ Cancel ]
|
||||||
|
```
|
||||||
|
|
||||||
|
"Download Later" = `startMode: "later"` (sits in the list as `PAUSED_MANUAL`).
|
||||||
|
Probe runs async: show the dialog immediately with a spinner in Size/Resume, fill in when
|
||||||
|
the reply lands, and never block the UI thread.
|
||||||
|
|
||||||
|
## 3. Download progress dialog
|
||||||
|
|
||||||
|
```
|
||||||
|
┌ ubuntu-26.04-desktop-amd64.iso ──────────────────────────────── ─ □ ✕ ┐
|
||||||
|
│ URL https://releases.ubuntu.com/26.04/ubuntu-26.04-…iso │
|
||||||
|
│ Status Receiving data… File size 5.80 GB │
|
||||||
|
│ Downloaded 2.74 GB (47.24 %) Transfer rate 28.41 MB/s │
|
||||||
|
│ Time left 00:03:11 Resume capability Yes │
|
||||||
|
├────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1 ████████████░░░░░ Receiving 3.9 MB/s │ 5 ██████████░░░░ 3.2 MB/s │
|
||||||
|
│ 2 ██████████░░░░░░░ Receiving 3.4 MB/s │ 6 ███████████░░░ 3.6 MB/s │
|
||||||
|
│ 3 █████████████░░░░ Receiving 4.1 MB/s │ 7 ████████░░░░░░ 2.9 MB/s │
|
||||||
|
│ 4 ███████████░░░░░░ Receiving 3.7 MB/s │ 8 ████████████░░ 3.5 MB/s │
|
||||||
|
├────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [speed graph, 60 s rolling window, filled area, 1 Hz] │
|
||||||
|
├────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ☐ Close dialog when done On completion: [ Do nothing ▾ ] │
|
||||||
|
│ [ Pause ] [ Cancel ] [ Hide ] │
|
||||||
|
└────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Per-segment bars come from `TaskDetail.segments[]`. "On completion" offers: Do nothing /
|
||||||
|
Open file / Open folder / Exit Velox / Shut down (the last one via
|
||||||
|
`org.freedesktop.login1`, and it must confirm).
|
||||||
|
|
||||||
|
## 4. Options dialog — tabs
|
||||||
|
|
||||||
|
| Tab | Contents |
|
||||||
|
|---|---|
|
||||||
|
| **General** | Launch on login · minimize to tray · show floating drop target · confirm on exit · language · check for updates |
|
||||||
|
| **File Types** | Per-category extension lists (the auto-capture table the extension mirrors) · "Automatically start downloading these types" · MIME overrides |
|
||||||
|
| **Save To** | Default download directory · per-category folders · temp folder · **file-exists policy** (ask / rename / overwrite / resume) · "create subfolder per site" |
|
||||||
|
| **Connection** | Connection type preset · **max connections per download (1–32)** · **write buffer per connection** · global max concurrent downloads · timeout · retries · per-host connection overrides |
|
||||||
|
| **Downloads** | Speed limiter default · virus-scan command · post-download command hook · duplicate-URL policy · integrity check (MD5/SHA-256) |
|
||||||
|
| **Proxy** | System / manual HTTP / HTTPS / SOCKS5 / PAC · per-host bypass list |
|
||||||
|
| **Site Logins** | Host → username/password, stored in the **Secret Service** (gnome-keyring), never in SQLite |
|
||||||
|
| **Sounds** | Per-event sound toggles (download complete, queue complete, error) |
|
||||||
|
|
||||||
|
Everything on this dialog maps 1:1 onto `settings.get`/`settings.set` keys. The settings
|
||||||
|
key list lives in `contracts/schema/types/Settings.schema.json` — the GUI must not invent
|
||||||
|
a key that isn't in the schema.
|
||||||
|
|
||||||
|
## 5. Other windows
|
||||||
|
|
||||||
|
- **Scheduler** — per-queue: start time, stop time, days of week, one-time vs periodic,
|
||||||
|
"hang up/exit when done", max concurrent per queue.
|
||||||
|
- **Site Grabber wizard** — 4 steps (project template → start URL + depth + filters →
|
||||||
|
file-type filter → review found files, check what to download).
|
||||||
|
- **Batch download from clipboard** — parse a pasted blob of URLs, dedupe, assign category.
|
||||||
|
- **Batch download with wildcards** — `http://host/img{1..50}.jpg` expansion with preview.
|
||||||
|
- **Speed limiter** — off / limit to N KB/s, with a "apply to running downloads now" button.
|
||||||
|
- **Floating drop target** — frameless always-on-top `QWidget`, accepts dropped links,
|
||||||
|
right-click menu, position remembered. IDM's drop box, minus the branding.
|
||||||
|
- **Tray icon** — active count in tooltip, menu: Show · Add URL · Pause All · Resume All ·
|
||||||
|
Speed limiter ▸ · Quit (Quit asks whether to also stop the daemon).
|
||||||
|
|
||||||
|
## 6. Clipboard capture
|
||||||
|
|
||||||
|
`QClipboard::dataChanged` → if the text is a URL whose extension is in the monitored list,
|
||||||
|
show a toast: "Download this link? [Download] [Ignore]".
|
||||||
|
|
||||||
|
⚠ **Under Wayland this does not work the way it does on X11** — a Wayland client is not
|
||||||
|
notified of clipboard changes made by other applications. This is the #2 risk in
|
||||||
|
`docs/06-risks-and-spikes.md` and has a dedicated spike. The design must therefore treat
|
||||||
|
clipboard monitoring as *best-effort* and ship these as the real paths:
|
||||||
|
1. The extension's context menu ("Download with Velox") — covers the browser case, which
|
||||||
|
is the overwhelming majority.
|
||||||
|
2. A global shortcut (via `org.freedesktop.portal.GlobalShortcuts`) that reads the
|
||||||
|
clipboard *on demand* — an explicit user action, which the portal does allow.
|
||||||
|
3. "Add URL" dialog, pre-filled from the clipboard when it opens (also an explicit action).
|
||||||
|
|
||||||
|
## 7. Theming
|
||||||
|
|
||||||
|
`gui/resources/qss/idm-like.qss` plus a `dark.qss`. Follow the system light/dark preference
|
||||||
|
via `QStyleHints::colorScheme()`. Keep every colour in one variables block at the top of
|
||||||
|
the QSS; no hard-coded hex scattered through widget code.
|
||||||
|
|
||||||
|
## 8. Accessibility & i18n (M5, not optional)
|
||||||
|
|
||||||
|
Keyboard-reachable everything, `Qt::AccessibleName` on custom widgets, `tr()` from the
|
||||||
|
first commit, `.ts` files under `gui/i18n/`, RTL layout verified with Arabic.
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# 04 — Download engine design (`libveloxcore`)
|
||||||
|
|
||||||
|
Owner: lane **CORE**. This is the part that has to be genuinely fast.
|
||||||
|
|
||||||
|
## 1. Task lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
NEW → PROBING → QUEUED → CONNECTING → DOWNLOADING ⇄ PAUSED
|
||||||
|
│ │
|
||||||
|
│ └→ RETRY_WAIT → CONNECTING
|
||||||
|
▼
|
||||||
|
ASSEMBLING → VERIFYING → COMPLETE
|
||||||
|
│
|
||||||
|
└→ FAILED | CANCELLED
|
||||||
|
```
|
||||||
|
|
||||||
|
`ASSEMBLING` is normally a no-op rename (see §4). It exists as a real state only for the
|
||||||
|
HLS/DASH path, which must mux.
|
||||||
|
|
||||||
|
## 2. Probe (`download.probe`)
|
||||||
|
|
||||||
|
Feeds IDM's "Download File Info" dialog, so it must be fast and never partially download.
|
||||||
|
|
||||||
|
1. `HEAD` with the browser's headers/cookies/referrer/UA verbatim.
|
||||||
|
2. If `HEAD` is refused (405/403 — common), fall back to `GET` with `Range: bytes=0-0` and
|
||||||
|
abort after headers.
|
||||||
|
3. Extract: final URL after redirects, `Content-Length`, `Content-Type`,
|
||||||
|
`Content-Disposition` filename (RFC 5987/6266, UTF-8 and legacy), `Accept-Ranges`,
|
||||||
|
`ETag`, `Last-Modified`.
|
||||||
|
4. **Resumability is proven, not assumed:** treat as resumable only if the range request
|
||||||
|
returned `206` *and* `Content-Range` matches. Servers lie about `Accept-Ranges`.
|
||||||
|
5. Filename resolution order: explicit user name → `Content-Disposition` → last path
|
||||||
|
segment (percent-decoded) → `Content-Type` extension → `download.bin`.
|
||||||
|
Then sanitize: strip `/` `\0`, control chars, trailing dots/spaces, cap at 200 bytes
|
||||||
|
*of UTF-8* (never split a codepoint), reject `.` and `..`.
|
||||||
|
|
||||||
|
## 3. Segmentation — the part IDM is actually famous for
|
||||||
|
|
||||||
|
Static N-way splitting is not what makes IDM feel fast; **dynamic segment stealing** is.
|
||||||
|
|
||||||
|
```
|
||||||
|
Initial: [====seg0====][====seg1====][====seg2====][====seg3====]
|
||||||
|
seg1 is on a fast mirror and finishes early:
|
||||||
|
[==seg0==....][ done ][==seg2==....][==seg3==....]
|
||||||
|
▲ largest remaining tail
|
||||||
|
Steal: seg1 restarts on the second half of seg2's remaining range:
|
||||||
|
[==seg0==....][ done ][=seg2=][==seg1'==][==seg3==....]
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Default 8 segments, user-configurable 1–32, clamped per-host by settings (some hosts
|
||||||
|
ban >4 connections; keep a per-host override table).
|
||||||
|
- Never split below `min_segment_bytes` (default 1 MiB) — more segments than that is pure
|
||||||
|
overhead and gets you rate-limited.
|
||||||
|
- On finish, a worker steals the **second half of the largest remaining range**, atomically
|
||||||
|
under the task lock, and only if that half is ≥ `min_segment_bytes`.
|
||||||
|
- A segment that fails 3× with a connection error is not retried on the same host if a
|
||||||
|
mirror exists; the range is returned to the pool and re-split.
|
||||||
|
- Non-resumable servers → exactly 1 segment, and the UI must say so
|
||||||
|
(IDM's "Resume capability: No").
|
||||||
|
|
||||||
|
## 4. Disk I/O — the buffer setting you asked for
|
||||||
|
|
||||||
|
One file, opened once, `O_WRONLY`. Each segment `pwrite()`s at its own absolute offset, so
|
||||||
|
**there is no reassembly pass and no second write of the whole file.**
|
||||||
|
|
||||||
|
- `posix_fallocate()` the full size up front → contiguous extents, no ENOSPC surprise at
|
||||||
|
99 %, no fragmentation.
|
||||||
|
- Per-segment ring buffer, size = **`buffer_bytes`** (the user-visible "Buffer size"
|
||||||
|
setting). Default 4 MiB, range 64 KiB – 64 MiB. Curl's write callback appends; the
|
||||||
|
buffer is flushed with a single `pwrite` when full or when the segment ends.
|
||||||
|
*This is the single biggest throughput knob and it is exposed in the UI: Options →
|
||||||
|
Downloads → "Write buffer per connection".*
|
||||||
|
- Global cap `max_total_buffer_bytes` (default 256 MiB) so 32 segments × 64 MiB can't OOM
|
||||||
|
the box. The per-segment value is silently reduced to fit and the effective value is
|
||||||
|
reported back to the UI.
|
||||||
|
- `posix_fadvise(POSIX_FADV_DONTNEED)` on written ranges — do not let a 40 GB ISO evict
|
||||||
|
the user's entire page cache.
|
||||||
|
- `fdatasync()` on a timer (default 5 s) and on pause, **not** per write.
|
||||||
|
- `io_uring` is a **post-1.0 optimization**, gated behind a benchmark in `tools/bench/`
|
||||||
|
that must show ≥10 % improvement on NVMe. Do not start there.
|
||||||
|
|
||||||
|
## 5. Resume metadata — `<name>.veloxpart.meta`
|
||||||
|
|
||||||
|
Written next to the part file so a download survives a daemon crash, a reboot, *and* a
|
||||||
|
database loss. Little-endian, versioned, `fdatasync`'d on every segment-boundary update:
|
||||||
|
|
||||||
|
```
|
||||||
|
magic "VDMP" | u16 version | u16 flags
|
||||||
|
u64 total_size | u64 downloaded
|
||||||
|
url_set (original + effective + mirrors, length-prefixed UTF-8)
|
||||||
|
etag | last_modified | content_type
|
||||||
|
u32 segment_count
|
||||||
|
per segment: u64 start, u64 end, u64 completed
|
||||||
|
sha256_partial_state (optional, for streaming hash)
|
||||||
|
crc32 of the whole record
|
||||||
|
```
|
||||||
|
|
||||||
|
On resume, revalidate with `If-Range: <etag or last-modified>`. If the server answers
|
||||||
|
`200` instead of `206`, the file changed underneath us: surface it as
|
||||||
|
"File on server has changed — restart download?" rather than silently corrupting the file.
|
||||||
|
This is the single most common way download managers produce broken files. Do not get it wrong.
|
||||||
|
|
||||||
|
## 6. Rate limiting
|
||||||
|
|
||||||
|
Hierarchical token buckets: global → per-queue → per-task. Refill on a 100 ms tick;
|
||||||
|
throttle by delaying curl reads (`CURLOPT_MAX_RECV_SPEED_LARGE` as a coarse floor, plus
|
||||||
|
our own pause/unpause via `curl_easy_pause` for precision). "Speed Limiter" in the UI
|
||||||
|
toggles between Full speed / a saved limit, exactly as IDM does.
|
||||||
|
|
||||||
|
## 7. Failure policy
|
||||||
|
|
||||||
|
| Case | Behaviour |
|
||||||
|
|---|---|
|
||||||
|
| 5xx / timeout / reset | Exponential backoff 1 s→2→4→8→…→cap 60 s, jitter ±20 %, `max_retries` default 10 |
|
||||||
|
| 401/407 | Emit `event.auth.required`, pause task, GUI shows credential dialog, store in secret service (never in SQLite) |
|
||||||
|
| 403 after redirect | Retry once with the original referrer; many CDNs require it |
|
||||||
|
| 416 | Metadata is stale → re-probe, re-split |
|
||||||
|
| Disk full | Pause all, one notification, do not spin |
|
||||||
|
| Server drops range support mid-download | Demote to 1 segment, keep what's on disk, continue |
|
||||||
|
|
||||||
|
## 8. Performance targets (M7 gate, `tools/bench/`)
|
||||||
|
|
||||||
|
- Saturate a 1 Gbit link with ≤ 8 % of one core.
|
||||||
|
- ≤ 60 MB RSS with 20 active downloads at default buffers.
|
||||||
|
- 10 000-row task list: RPC `download.list` under 50 ms, GUI scroll at 60 fps.
|
||||||
|
- No allocation in the curl write callback hot path (ring buffer is preallocated).
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# 05 — Firefox extension specification
|
||||||
|
|
||||||
|
Owner: lane **EXT**. TypeScript, MV3, `browser.*` promise APIs, built with esbuild,
|
||||||
|
packaged with `web-ext`.
|
||||||
|
|
||||||
|
## 1. Why Firefox MV3 is actually good news here
|
||||||
|
|
||||||
|
Chrome's MV3 removed blocking `webRequest`, which is why IDM-style "grab the download
|
||||||
|
before the browser starts it" is hard there. **Firefox kept blocking `webRequest` in
|
||||||
|
MV3.** That means we can inspect response headers and cancel the browser's own download,
|
||||||
|
which is exactly the interception model IDM uses. Build for Firefox first and do not
|
||||||
|
compromise the design to stay Chrome-portable.
|
||||||
|
|
||||||
|
## 2. Capture pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
onBeforeSendHeaders ──► stash request headers by requestId (ring buffer, 5 min TTL)
|
||||||
|
│
|
||||||
|
onHeadersReceived ───► shouldCapture(details, headers, settings)?
|
||||||
|
│ │
|
||||||
|
│ yes│
|
||||||
|
│ ▼
|
||||||
|
│ cookies.getAll(url) ──► transport.send("capture.offer", {...})
|
||||||
|
│ │
|
||||||
|
│ daemon replies {action:"take", taskId}
|
||||||
|
│ ▼
|
||||||
|
└──────────────► return {cancel: true} ← browser never starts the download
|
||||||
|
```
|
||||||
|
|
||||||
|
`shouldCapture` returns true when **any** of:
|
||||||
|
- `Content-Disposition: attachment` present, or
|
||||||
|
- the file extension is in the user's monitored list (Options → File Types, mirrored from
|
||||||
|
the daemon so the two never disagree), or
|
||||||
|
- `Content-Type` is in the monitored MIME list and not `text/html`, or
|
||||||
|
- `Content-Length` > `minSizeBytes` (default 1 MiB) **and** the type is not renderable.
|
||||||
|
|
||||||
|
And **none** of:
|
||||||
|
- the tab is a `blob:`/`data:` URL we generated,
|
||||||
|
- the host is on the user's exclusion list,
|
||||||
|
- the response is a navigation to an HTML page,
|
||||||
|
- the user held the bypass modifier (default: Alt) on the click.
|
||||||
|
|
||||||
|
**Fail-open, always.** If the daemon is unreachable or the RPC times out (750 ms budget),
|
||||||
|
`return {}` and let Firefox download it normally. A download manager that eats downloads
|
||||||
|
when its daemon is down is worse than no download manager. This rule is non-negotiable and
|
||||||
|
has a dedicated conformance test.
|
||||||
|
|
||||||
|
Belt-and-braces second path: `browser.downloads.onCreated` → if it slipped past the
|
||||||
|
header hook, `downloads.cancel(id)` + `downloads.erase(id)` and offer to the daemon. Some
|
||||||
|
downloads (form POSTs, service-worker-generated blobs) only surface here.
|
||||||
|
|
||||||
|
## 3. Other surfaces
|
||||||
|
|
||||||
|
| Surface | Behaviour |
|
||||||
|
|---|---|
|
||||||
|
| Context menu (link) | "Download with Velox" |
|
||||||
|
| Context menu (image/video/audio) | "Download with Velox" |
|
||||||
|
| Context menu (page/selection) | "Download all links with Velox…" → opens the batch dialog with the harvested list |
|
||||||
|
| Toolbar popup | Active downloads with live progress (via `event.task.progress` relayed over the transport), pause/resume, "Add URL", speed indicator, daemon status dot |
|
||||||
|
| Media panel | Floating in-page button on a tab where a video/HLS/DASH stream was detected — "Download this video ▾" listing quality variants |
|
||||||
|
| Options page | Transport & pairing · monitored types · min size · exclusion list · default category/folder · bypass modifier · enable/disable capture |
|
||||||
|
| Keyboard | Configurable command to grab the current tab's URL |
|
||||||
|
|
||||||
|
**Media detection:** `webRequest` sniffing for `.m3u8`, `.mpd`, `Content-Type:
|
||||||
|
application/vnd.apple.mpegurl` / `dash+xml`, plus a content script observing
|
||||||
|
`MediaSource.addSourceBuffer` and `<video>` `src` changes. The extension sends the manifest
|
||||||
|
URL + headers to the daemon; the **daemon** parses the manifest and enumerates variants
|
||||||
|
(`media.listVariants`). The extension never parses HLS — keep the logic in one language.
|
||||||
|
|
||||||
|
> DRM-protected streams (Widevine/EME) are explicitly out of scope. Detect them and grey
|
||||||
|
> out the button with "Protected content" rather than failing mysteriously.
|
||||||
|
|
||||||
|
## 4. Transports — the reason this design has two
|
||||||
|
|
||||||
|
**Evidence from this machine:** Firefox here is the **Mozilla snap** (`snap list firefox`
|
||||||
|
→ `154.0`, `mozilla**`). Snap-confined Firefox has a long history of trouble executing
|
||||||
|
native-messaging host binaries that live outside the snap's world. Meanwhile
|
||||||
|
`snap connections firefox` shows `network` and `network-bind` **connected**.
|
||||||
|
|
||||||
|
So the extension implements a `Transport` interface with two implementations and picks at
|
||||||
|
runtime:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Transport {
|
||||||
|
connect(): Promise<void>
|
||||||
|
call<M extends Method>(m: M, p: Params<M>): Promise<Result<M>>
|
||||||
|
on(event: string, cb: (p: unknown) => void): void
|
||||||
|
readonly state: "connected" | "connecting" | "disconnected"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### A. `NativeTransport` (preferred when it works)
|
||||||
|
`browser.runtime.connectNative("com.velox.host")` → `velox-nmhost` → Unix socket.
|
||||||
|
Manifest installed to **all** of these, because the right one depends on how Firefox was
|
||||||
|
installed:
|
||||||
|
- `~/.mozilla/native-messaging-hosts/com.velox.host.json` (deb/tarball)
|
||||||
|
- `~/snap/firefox/common/.mozilla/native-messaging-hosts/com.velox.host.json` (snap)
|
||||||
|
- `/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json` (system-wide)
|
||||||
|
- `~/.var/app/org.mozilla.firefox/.mozilla/native-messaging-hosts/` (flatpak)
|
||||||
|
|
||||||
|
### B. `WebSocketTransport` (fallback, guaranteed to work under snap confinement)
|
||||||
|
`ws://127.0.0.1:<port>` where the port is discovered by trying a small fixed range and
|
||||||
|
verifying a `session.hello` handshake — the extension cannot read
|
||||||
|
`$XDG_RUNTIME_DIR/velox/ws.port`, so the daemon binds the first free port in
|
||||||
|
`52000–52016` and the extension probes them.
|
||||||
|
|
||||||
|
**Pairing (first run only):** the extension connects and calls `session.pair`. The daemon
|
||||||
|
pops a GUI/desktop-notification dialog: *"Firefox is requesting to connect to Velox.
|
||||||
|
Pairing code: **4821**. [Allow] [Deny]"*. The user clicks Allow (or types the code in the
|
||||||
|
extension options if the GUI isn't running). The daemon returns a 256-bit token; the
|
||||||
|
extension stores it in `browser.storage.local` and sends it on every subsequent connect.
|
||||||
|
|
||||||
|
**Security requirements (non-negotiable, conformance-tested):**
|
||||||
|
- Bind `127.0.0.1` only — never `0.0.0.0`.
|
||||||
|
- Verify `Origin: moz-extension://…` on the WS upgrade, and require the token.
|
||||||
|
- Rate-limit failed auth (5/min, then a 60 s lockout) so the token can't be brute-forced
|
||||||
|
by another local process.
|
||||||
|
- Token is per-install, revocable from Options → "Unpair", and stored in the daemon's DB
|
||||||
|
as a hash, not plaintext.
|
||||||
|
- **Never** expose a method that can write to an arbitrary path without a task the user
|
||||||
|
approved. The extension can request a download; it cannot ask the daemon to write to
|
||||||
|
`~/.bashrc`.
|
||||||
|
|
||||||
|
## 5. Startup UX when the daemon is missing
|
||||||
|
|
||||||
|
Popup shows a red dot and: *"Velox isn't running. [Start it] [Install]"*. `[Start it]`
|
||||||
|
tries `session.hello` again after asking the native host to spawn the daemon; if the
|
||||||
|
native transport is unavailable, link to install instructions. Capture stays fail-open
|
||||||
|
throughout — Firefox keeps downloading normally.
|
||||||
|
|
||||||
|
## 6. Build & test
|
||||||
|
|
||||||
|
```
|
||||||
|
extension/
|
||||||
|
├── manifest.json # MV3, permissions listed and justified in a comment
|
||||||
|
├── src/background/index.ts # event page entry
|
||||||
|
│ ├── capture/{headers,rules,downloads-api,media}.ts
|
||||||
|
│ ├── transport/{index,native,websocket,discovery}.ts
|
||||||
|
│ ├── context-menus.ts badge.ts state.ts
|
||||||
|
├── src/content/{media-observer,link-harvest,video-panel}.ts
|
||||||
|
├── src/popup/ src/options/ # plain TS + minimal CSS; no framework needed
|
||||||
|
└── src/shared/protocol/ # GENERATED from contracts/ — never hand-edit
|
||||||
|
```
|
||||||
|
|
||||||
|
- Unit: **vitest** with `webextension-polyfill` mocked; every `shouldCapture` decision gets
|
||||||
|
a table-driven test (this is where the bugs will be).
|
||||||
|
- Integration: against `tools/mockd` over WebSocket.
|
||||||
|
- E2E: **Playwright** with a real Firefox and a real `veloxd`, asserting a real file lands
|
||||||
|
on disk with the right bytes. Lives in `tests/e2e/`.
|
||||||
|
- Lint: ESLint + `web-ext lint` (AMO rules) in CI from day one — finding out at submission
|
||||||
|
time that a permission is disallowed costs a week.
|
||||||
|
|
||||||
|
## 7. Permissions (keep this list short; AMO reviews it)
|
||||||
|
|
||||||
|
`webRequest`, `webRequestBlocking`, `downloads`, `cookies`, `contextMenus`, `storage`,
|
||||||
|
`notifications`, `nativeMessaging`, `<all_urls>`.
|
||||||
|
|
||||||
|
`<all_urls>` is unavoidable for a download manager but is the main review-friction item:
|
||||||
|
document *why* in the AMO submission notes and in the source, and make the exclusion list
|
||||||
|
prominent in Options.
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# 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.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 07 — Packaging & install layout
|
||||||
|
|
||||||
|
Owner: lane **PKG/QA**. Target: Ubuntu 26.04 LTS.
|
||||||
|
|
||||||
|
## Install layout (.deb)
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/bin/veloxd
|
||||||
|
/usr/bin/velox-gui
|
||||||
|
/usr/bin/velox # CLI
|
||||||
|
/usr/libexec/velox/velox-nmhost # native messaging host
|
||||||
|
/usr/lib/x86_64-linux-gnu/libveloxcore.so.1
|
||||||
|
/usr/share/applications/velox.desktop
|
||||||
|
/usr/share/icons/hicolor/*/apps/velox.png
|
||||||
|
/usr/share/man/man1/velox.1.gz
|
||||||
|
/usr/lib/systemd/user/velox.service
|
||||||
|
/usr/lib/systemd/user/velox.socket # socket activation
|
||||||
|
/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json
|
||||||
|
/etc/xdg/autostart/velox-gui.desktop # optional, off by default
|
||||||
|
```
|
||||||
|
|
||||||
|
`postinst` additionally drops per-user native-messaging manifests for the packaging formats
|
||||||
|
that need them, and **detects whether Firefox is a snap** — if so it prints (and the GUI's
|
||||||
|
first-run wizard shows) a one-line note that the extension will pair over loopback.
|
||||||
|
|
||||||
|
## Package matrix
|
||||||
|
|
||||||
|
| Format | Priority | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `.deb` via PPA | **Primary** | The only format where native messaging, systemd user units and Secret Service all behave predictably |
|
||||||
|
| Flatpak | Secondary | The sandbox changes native messaging *again*; test explicitly, don't assume. Needs `--filesystem=xdg-download` and a portal-based folder picker |
|
||||||
|
| AppImage | Optional | Convenient for testing; if it bundles Qt, honour LGPLv3 relink terms (`docs/06` R5) |
|
||||||
|
| Snap | **Not planned** | Confinement fights both native messaging and arbitrary download destinations. Revisit only if there's demand |
|
||||||
|
|
||||||
|
## Firefox extension distribution
|
||||||
|
|
||||||
|
Signed XPI on **addons.mozilla.org**. Ship the source with a reproducible build script
|
||||||
|
(AMO requires it for minified/bundled submissions). The `.deb` does *not* bundle the XPI —
|
||||||
|
it links to AMO from the first-run wizard, so extension updates flow through Firefox's own
|
||||||
|
update channel rather than through apt.
|
||||||
|
|
||||||
|
## Release checklist
|
||||||
|
|
||||||
|
- [ ] `lintian` clean
|
||||||
|
- [ ] Fresh 26.04 VM: install → install extension → M2 vertical slice passes with snap Firefox
|
||||||
|
- [ ] Upgrade from N-1: DB migrates, in-flight `.veloxpart` files still resume
|
||||||
|
- [ ] Uninstall: manifests, units and sockets removed; user data untouched unless purged
|
||||||
|
- [ ] `systemctl --user` units enabled and socket-activation verified from cold boot
|
||||||
|
- [ ] Protocol `VERSION` matches between the shipped daemon and the shipped extension, and
|
||||||
|
a deliberate mismatch produces the "Velox needs updating" message rather than a hang
|
||||||
|
- [ ] Licence audit: Qt (LGPLv3, dynamic), libcurl, SQLite, ffmpeg, icon set
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ADR 0001 — Record architecture decisions
|
||||||
|
|
||||||
|
**Status:** accepted · **Date:** 2026-09-09
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Several agents build this project in parallel, and each starts without the others' context.
|
||||||
|
Decisions that live only in a chat log get re-litigated or silently reversed.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Every non-obvious decision gets a numbered file here: context, decision, consequences,
|
||||||
|
alternatives rejected and why. Keep it under a page. Amend by adding a new ADR that
|
||||||
|
supersedes the old one — never rewrite history.
|
||||||
|
|
||||||
|
## Expected ADRs
|
||||||
|
|
||||||
|
| # | Subject | Owner | Due |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0002 | Qt 6 licensing and how packages link it | PKG | M0 |
|
||||||
|
| 0003 | Native messaging under snap Firefox (spike S1) | EXT | M0 |
|
||||||
|
| 0004 | Wayland clipboard capability (spike S2) | GUI | M1 |
|
||||||
|
| 0005 | Protocol v1.0.0 freeze and the versioning rule | PROTO | M0 |
|
||||||
|
| 0006 | Product name and icon-set licence | PKG | M0 |
|
||||||
|
| 0007 | Segment-stealing heuristics and defaults | CORE | M1 |
|
||||||
|
| 0008 | Loopback WS threat model and pairing design | DAEMON | M1 |
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Agent brief — CORE (`libveloxcore`)
|
||||||
|
|
||||||
|
**Starts when PROTO freezes `VERSION`. The critical path runs through you.**
|
||||||
|
|
||||||
|
## You own
|
||||||
|
```
|
||||||
|
core/** tools/bench/** tools/fuzz/**
|
||||||
|
```
|
||||||
|
You may read `contracts/` and `docs/`. You write nowhere else — in particular **you never
|
||||||
|
touch `daemon/`**: if the daemon needs something, expose it as a core API and tell DAEMON.
|
||||||
|
|
||||||
|
## Read first
|
||||||
|
`docs/04-engine-design.md` end to end. It is your specification, not background reading.
|
||||||
|
|
||||||
|
## Hard architectural constraints
|
||||||
|
- **No JSON, no SQL, no Qt, no RPC in `core/`.** Your public API takes a `DownloadSpec`
|
||||||
|
and emits typed callbacks. If you find yourself including a protocol header, stop.
|
||||||
|
- C++23, `-Wall -Wextra -Werror`, no raw `new`/`delete`, no naked `pthread`.
|
||||||
|
- Every public header under `core/include/vdm/` compiles standalone.
|
||||||
|
- Errors are returned (`std::expected`-style `Result<T>`), not thrown, on the transfer path.
|
||||||
|
- **No allocation in the curl write callback.** The ring buffer is preallocated at task
|
||||||
|
start. This is checked in review and by a bench assertion.
|
||||||
|
|
||||||
|
## Build order
|
||||||
|
|
||||||
|
1. `util/` — `Result<T>`, event bus, thread pool, logging, byte-span helpers.
|
||||||
|
2. `net/http_client` — libcurl multi wrapper, one `curl_multi` per worker thread, poll
|
||||||
|
loop, proxy/auth/redirect/cookie plumbing.
|
||||||
|
3. `net/probe` — HEAD then ranged-GET fallback, header parsing, **`Content-Disposition`
|
||||||
|
RFC 5987/6266 including the legacy forms** (this is a classic source of mojibake — give
|
||||||
|
it its own test table and a fuzz target).
|
||||||
|
4. `io/sparse_file` + `io/write_buffer` — `posix_fallocate`, per-segment ring buffer sized
|
||||||
|
by `buffer_bytes`, `pwrite` at absolute offsets, `posix_fadvise(DONTNEED)`, timed
|
||||||
|
`fdatasync`.
|
||||||
|
5. `meta/veloxpart` — the resume sidecar in `docs/04` §5, CRC-verified, `fdatasync`'d at
|
||||||
|
segment boundaries. **Write the reader first and fuzz it** — this file is attacker-
|
||||||
|
adjacent (it lives in a world-writable-ish download dir).
|
||||||
|
6. `segment/segmenter` + `segment/stealer` — dynamic segment stealing, `min_segment_bytes`
|
||||||
|
floor, per-host connection caps.
|
||||||
|
7. `rate/token_bucket` — hierarchical global → queue → task.
|
||||||
|
8. `task/download_task` — the state machine in `docs/04` §1, retry/backoff policy, mirrors.
|
||||||
|
9. `rules/` — filename sanitization + collision policy + category matching (pure functions;
|
||||||
|
DAEMON supplies the rule table).
|
||||||
|
10. `media/` — **M4, not now.** Leave the directory empty.
|
||||||
|
|
||||||
|
## Definition of done (M1)
|
||||||
|
- 5 GB download saturates a 1 Gbit link at ≤ 8 % of one core (recorded in `tools/bench/`).
|
||||||
|
- `kill -9` at ~60 % → resume completes → SHA-256 matches the reference byte for byte.
|
||||||
|
- Every hostile mode in `tools/testserver` handled: no-Range, lying `Accept-Ranges`,
|
||||||
|
ETag change mid-download, 401, 416, redirect chains, slow-loris, connection reset,
|
||||||
|
expiring signed URL, `Content-Length` mismatch.
|
||||||
|
- ASan + UBSan + TSan clean under a 20-task load test.
|
||||||
|
- Fuzz targets for `Content-Disposition`, the `.veloxpart.meta` reader, and URL parsing run
|
||||||
|
1 M+ execs with no crash.
|
||||||
|
- Public API documented in `core/include/vdm/README.md` and reviewed by DAEMON before M2.
|
||||||
|
|
||||||
|
## Do not
|
||||||
|
- Do not start with `io_uring`. It's a post-1.0 experiment gated on a ≥10 % bench win.
|
||||||
|
- Do not implement HLS/DASH in M1.
|
||||||
|
- Do not add a "just for testing" JSON dependency.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Agent brief — DAEMON (`veloxd`)
|
||||||
|
|
||||||
|
**Starts with CORE. You are the only process that owns state.**
|
||||||
|
|
||||||
|
## You own
|
||||||
|
```
|
||||||
|
daemon/** cli/** nmhost/** packaging/nativehost/**
|
||||||
|
```
|
||||||
|
Read `contracts/`, `core/include/`, `docs/`. Never write in `core/`, `gui/`, or `extension/`.
|
||||||
|
|
||||||
|
## Read first
|
||||||
|
`docs/01-architecture.md` §2–§5, `contracts/README.md`, `docs/05-extension-spec.md` §4
|
||||||
|
(you implement the daemon half of pairing).
|
||||||
|
|
||||||
|
## Build order
|
||||||
|
|
||||||
|
1. **RPC server** — `rpc/uds_server` (NDJSON over `$XDG_RUNTIME_DIR/velox/velox.sock`,
|
||||||
|
0600, `SO_PEERCRED` same-UID check) and `rpc/ws_server` (bind `127.0.0.1` only, first
|
||||||
|
free port in 52000–52016, write the chosen port to `ws.port`). One dispatcher, generated
|
||||||
|
from `contracts/`. Never block the RPC loop — disk and network work goes to CORE's pools.
|
||||||
|
2. **Auth & pairing** — `session.pair` triggers a user prompt (GUI dialog if connected,
|
||||||
|
else a desktop notification with actions). Tokens: 256-bit, stored **hashed**,
|
||||||
|
per-install, revocable. Failed-auth rate limit 5/min then 60 s lockout. Enforce
|
||||||
|
`x-transports` and `x-privileged` from the schema: privileged methods are refused over
|
||||||
|
WS with `-32003`.
|
||||||
|
3. **Store** — SQLite WAL. Tables: `tasks`, `segments`, `categories`, `queues`, `rules`,
|
||||||
|
`settings`, `history`, `pairings`. Numbered migrations in `store/migrations/`, applied
|
||||||
|
at startup, with a forward-only test from every released schema version.
|
||||||
|
**Credentials never go in SQLite** — Secret Service via libsecret.
|
||||||
|
4. **Scheduler & queues** — concurrency governor (global max active, per-queue max, per-host
|
||||||
|
caps), time windows, days-of-week, one-shot vs periodic, "when queue completes" actions.
|
||||||
|
5. **Event fan-out** — per-subscription filtering, and **`event.task.progress` batched at
|
||||||
|
≤ 4 Hz into a single array message**. Do not emit one message per task per tick; that is
|
||||||
|
how you turn 20 downloads into a GUI that burns a core.
|
||||||
|
6. **Capture endpoint** — `capture.offer` must answer within **750 ms**, always. Apply the
|
||||||
|
rules table, resolve the category folder, dedupe against active tasks, return
|
||||||
|
`take`/`ignore`. If anything internally is slow, answer `ignore` and let Firefox have
|
||||||
|
it. Never make the browser wait.
|
||||||
|
7. **Integration** — systemd user units (`velox.service` + `velox.socket` for socket
|
||||||
|
activation), single-instance lock, XDG autostart, `org.freedesktop.Notifications`,
|
||||||
|
graceful shutdown that flushes buffers and meta files.
|
||||||
|
8. **`velox` CLI** — `add`, `ls`, `pause`, `resume`, `rm`, `queue`, `settings`, `--json`
|
||||||
|
output. Build this early: it is how you test the daemon before the GUI exists.
|
||||||
|
9. **`velox-nmhost`** — 4-byte-length-prefixed stdio ⇄ Unix socket pump. **Under 300 lines,
|
||||||
|
zero business logic**, and it must exit cleanly when Firefox closes the pipe. Install
|
||||||
|
manifests to all four locations listed in `docs/05` §4.
|
||||||
|
|
||||||
|
## Definition of done (M1)
|
||||||
|
- Passes the full conformance suite as a server, over **both** transports.
|
||||||
|
- Kill and restart the daemon mid-download: all tasks reload with correct state and resume.
|
||||||
|
- 1 000 tasks in the DB: `download.list` with paging under 50 ms.
|
||||||
|
- Pairing flow works from a real Firefox extension; unpair revokes immediately.
|
||||||
|
- `systemctl --user status velox` clean; socket activation verified from cold.
|
||||||
|
- Security review passed: no bind beyond loopback, no path traversal in `saveDir`
|
||||||
|
(canonicalize and check against allowed roots → `-32011`), no plaintext secrets.
|
||||||
|
|
||||||
|
## Do not
|
||||||
|
- Do not put download logic here — that's CORE. You schedule and persist; CORE transfers.
|
||||||
|
- Do not invent protocol fields. File a request with PROTO.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Agent brief — EXT (Firefox extension)
|
||||||
|
|
||||||
|
**Starts the day PROTO freezes the contract. Develops against `tools/mockd` over the
|
||||||
|
WebSocket transport — you never wait for `veloxd`.**
|
||||||
|
|
||||||
|
## You own
|
||||||
|
```
|
||||||
|
extension/**
|
||||||
|
```
|
||||||
|
Read `contracts/`, `docs/`. Never write in `core/`, `daemon/`, `gui/`, or `nmhost/`
|
||||||
|
(the native host belongs to DAEMON; if you need a change there, file it).
|
||||||
|
|
||||||
|
## Read first
|
||||||
|
`docs/05-extension-spec.md` in full, then `docs/06` R1 (snap Firefox) and R3 (AMO).
|
||||||
|
|
||||||
|
## Your first task is a spike, not code
|
||||||
|
|
||||||
|
**Spike S1 (2 days, blocks nothing else):** on a clean Ubuntu 26.04 VM with snap Firefox,
|
||||||
|
determine empirically which of the four native-messaging manifest locations actually work,
|
||||||
|
and whether the launched host can reach `$XDG_RUNTIME_DIR`. Write
|
||||||
|
`docs/adr/0003-native-messaging-under-snap.md`. Then build the WebSocket transport first
|
||||||
|
regardless of the answer — it is the one that is known to work here
|
||||||
|
(`snap connections firefox` shows `network`/`network-bind` connected).
|
||||||
|
|
||||||
|
## Build order
|
||||||
|
|
||||||
|
1. **`transport/`** — the `Transport` interface, `WebSocketTransport` (port discovery over
|
||||||
|
52000–52016 + `session.hello` verification), pairing flow with token in
|
||||||
|
`browser.storage.local`, reconnect with backoff, then `NativeTransport`, then
|
||||||
|
`transport/index.ts` picking at runtime with a manual override in Options.
|
||||||
|
2. **`capture/headers.ts`** — `onBeforeSendHeaders` stash keyed by `requestId`, ring buffer,
|
||||||
|
5-minute TTL, bounded size (a leak here eats the browser's memory).
|
||||||
|
3. **`capture/rules.ts`** — `shouldCapture()` exactly as specified in `docs/05` §2.
|
||||||
|
**Table-driven tests before implementation.** This function is where the bugs will be:
|
||||||
|
too eager and you hijack page navigations; too shy and you're not a download manager.
|
||||||
|
4. **`capture/index.ts`** — the `onHeadersReceived` blocking hook: gather cookies, call
|
||||||
|
`capture.offer` with a **750 ms budget**, `{cancel: true}` only on `take`.
|
||||||
|
**Fail-open is a hard requirement** — daemon down, slow, or erroring means Firefox
|
||||||
|
downloads normally. Write that test before the feature.
|
||||||
|
5. **`capture/downloads-api.ts`** — the `downloads.onCreated` safety net for what slips past.
|
||||||
|
6. **`context-menus.ts`**, **`popup/`** (live progress from relayed events, pause/resume,
|
||||||
|
status dot), **`options/`** (transport, pairing/unpair, monitored types synced via
|
||||||
|
`capture.getRules`, min size, exclusions, default category, bypass modifier).
|
||||||
|
7. **`content/` media detection** — `.m3u8`/`.mpd`/MIME sniffing plus a `MediaSource`
|
||||||
|
observer; in-page "Download this video ▾" panel listing variants from
|
||||||
|
`media.listVariants`. **The extension never parses manifests** — the daemon does.
|
||||||
|
Detect DRM/EME and grey the button out with "Protected content".
|
||||||
|
|
||||||
|
## Definition of done (M1)
|
||||||
|
- Intercepts a real download in real Firefox and hands it to `mockd`.
|
||||||
|
- **Fail-open proven by an automated test:** kill the mock mid-flow, the file still
|
||||||
|
downloads through Firefox, no error dialog, no lost download.
|
||||||
|
- Pairing works, unpair revokes, token survives a browser restart, and a wrong token is
|
||||||
|
rejected and rate-limited.
|
||||||
|
- `web-ext lint` clean; no remote code, no `eval`, no CDN script — AMO rejects those.
|
||||||
|
- `shouldCapture` test table covers: attachment, monitored extension, monitored MIME,
|
||||||
|
size threshold, excluded host, HTML navigation, blob URL, bypass modifier held,
|
||||||
|
streaming media, and a range request the page itself issued.
|
||||||
|
- Popup shows live progress at ≤ 4 Hz without pinning a core.
|
||||||
|
- Permission justification written and committed for AMO submission.
|
||||||
|
|
||||||
|
## Do not
|
||||||
|
- Do not implement any download logic in the extension. You collect URL + headers + cookies
|
||||||
|
and hand them over. That's the whole job.
|
||||||
|
- Do not add a framework (React/Vue) for a popup and an options page; plain TS keeps the
|
||||||
|
AMO review and the bundle small.
|
||||||
|
- Do not invent protocol fields — file a request with PROTO.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Agent brief — GUI (`velox-gui`)
|
||||||
|
|
||||||
|
**Starts the day PROTO freezes the contract. You never wait for the daemon — you develop
|
||||||
|
entirely against `tools/mockd`.**
|
||||||
|
|
||||||
|
## You own
|
||||||
|
```
|
||||||
|
gui/**
|
||||||
|
```
|
||||||
|
Read `contracts/`, `docs/`. Never write in `core/`, `daemon/`, or `extension/`.
|
||||||
|
|
||||||
|
## Read first
|
||||||
|
`docs/03-gui-spec.md` — it is the spec, screen by screen. Build in the order below.
|
||||||
|
|
||||||
|
## Build order
|
||||||
|
|
||||||
|
1. **`rpc/` client** — wrap the generated C++ client: async calls on a worker thread,
|
||||||
|
Qt signals on the main thread, auto-reconnect with exponential backoff, an offline
|
||||||
|
banner instead of a modal error, and a "Connected/Reconnecting" dot in the status bar.
|
||||||
|
Get this right first; everything else depends on it.
|
||||||
|
2. **`models/DownloadTableModel`** — `QAbstractItemModel` over `TaskSummary`. Apply
|
||||||
|
`event.task.progress` batches as **row patches with narrow `dataChanged` ranges**.
|
||||||
|
Never `beginResetModel()` on a progress tick. Sorting/filtering via `QSortFilterProxyModel`
|
||||||
|
per category tree selection.
|
||||||
|
3. **Main window** — menus, toolbar, splitter, category tree, table, status bar, column
|
||||||
|
persistence in `QSettings`.
|
||||||
|
4. **`widgets/ProgressDelegate`** — the in-cell progress bar; also
|
||||||
|
`SegmentBarsWidget` and `SpeedGraphWidget` (60 s rolling, 1 Hz, painted with
|
||||||
|
`QPainterPath`, no per-frame allocation).
|
||||||
|
5. **Dialogs**, in this order: Add URL → Download File Info (async probe, spinner while it
|
||||||
|
resolves) → Download Progress → Options (all tabs, every control bound to a
|
||||||
|
`settings.*` key that exists in the schema) → Scheduler → Speed Limiter → Batch →
|
||||||
|
Grabber wizard.
|
||||||
|
6. **Tray + floating drop target** — frameless always-on-top drop widget accepting dropped
|
||||||
|
URLs and text; position persisted.
|
||||||
|
7. **Clipboard** — `clipboard/monitor.*`, but read `docs/06` R2 first: this is
|
||||||
|
best-effort under Wayland. Implement the **explicit** paths (Add-URL prefill, portal
|
||||||
|
global shortcut) as the primary UX and treat passive monitoring as a bonus that spike S2
|
||||||
|
may or may not unlock. **Don't advertise it in the UI until S2 answers.**
|
||||||
|
8. **Theming** — `resources/qss/idm-like.qss` + `dark.qss`, colours in one variables block,
|
||||||
|
follow `QStyleHints::colorScheme()`.
|
||||||
|
|
||||||
|
## Definition of done (M1)
|
||||||
|
- Every screen in `docs/03-gui-spec.md` exists and is driven **only** by `mockd`.
|
||||||
|
- 10 000 synthetic rows: scrolling holds 60 fps, memory flat over 10 minutes of progress
|
||||||
|
events (`mockd --tasks 10000`).
|
||||||
|
- Unhappy paths handled: `mockd --slow`, `--flaky`, `--drop-connection` produce a banner
|
||||||
|
and a clean recovery, never a freeze or a crash.
|
||||||
|
- Zero download logic in `gui/` — grep for `curl`, `pwrite`, `sqlite` must return nothing.
|
||||||
|
- All strings wrapped in `tr()`; a stub Arabic `.ts` proves the RTL layout survives.
|
||||||
|
- No blocking call on the UI thread: verified with a 200 ms watchdog in debug builds.
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
Ship an **original or compatibly-licensed** icon set (Papirus/Breeze-derived is fine) laid
|
||||||
|
out in IDM's positions. Do not copy IDM's artwork. Record the icon licence in
|
||||||
|
`gui/resources/icons/LICENSE`.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Agent brief — PKG/QA (build, packaging, test infrastructure)
|
||||||
|
|
||||||
|
**Starts in M0 alongside PROTO. You unblock everyone else and you own the release.**
|
||||||
|
|
||||||
|
## You own
|
||||||
|
```
|
||||||
|
packaging/** .github/workflows/** tools/testserver/** tests/integration/** tests/e2e/**
|
||||||
|
CMakeLists.txt CMakePresets.json .clang-format .clang-tidy .editorconfig .gitignore
|
||||||
|
```
|
||||||
|
Do not write feature code in any lane's directory.
|
||||||
|
|
||||||
|
## M0 — unblock the lanes (do these first, in this order)
|
||||||
|
|
||||||
|
1. **Toolchain script** — `tools/bootstrap.sh` installing the `apt` list in the README,
|
||||||
|
verified on a clean 26.04 VM. The dev machine already has git 2.53, CMake 4.2.3,
|
||||||
|
g++ 15.2, ninja, Qt 6 dev, libcurl, SQLite, nlohmann-json and ffmpeg; only
|
||||||
|
`libqt6svg6-dev`, `libsecret-1-dev`, `nodejs`/`npm` and (optionally) `clang` are
|
||||||
|
missing. The script must still install the full list for clean machines.
|
||||||
|
⚠ CMake 4.x rejects `cmake_minimum_required` below 3.5 — check every dependency.
|
||||||
|
2. **CMake** — top-level `CMakeLists.txt` + `CMakePresets.json` with presets:
|
||||||
|
`dev` (Debug + ASan/UBSan), `tsan`, `release` (RelWithDebInfo + LTO), `ci`.
|
||||||
|
Ninja, C++23, `-Wall -Wextra -Werror`.
|
||||||
|
3. **`tools/testserver`** — the hostile HTTP server every lane tests against. Modes, each
|
||||||
|
toggleable by URL path or flag:
|
||||||
|
`no-range` · `lies-about-accept-ranges` · `etag-changes` · `flaky-reset` ·
|
||||||
|
`slow-loris` · `redirect-chain` · `401-basic` · `401-digest` · `403-without-referer` ·
|
||||||
|
`416-always` · `content-length-mismatch` · `expiring-signed-url` · `throttled` ·
|
||||||
|
`chunked-no-length` · `utf8-content-disposition` · `legacy-content-disposition`.
|
||||||
|
**CORE's definition of done is written in terms of this server, so it must exist first.**
|
||||||
|
4. **CI** — GitHub Actions: build matrix (gcc + clang), unit tests, ASan/UBSan/TSan jobs,
|
||||||
|
`clang-format --dry-run -Werror`, `clang-tidy`, `web-ext lint`, and **conformance as a
|
||||||
|
required check on every PR**.
|
||||||
|
|
||||||
|
## M1–M5 — keep it honest
|
||||||
|
- Nightly integration run: real `veloxd` + `testserver`, 50 concurrent downloads, assert
|
||||||
|
checksums and zero leaked FDs.
|
||||||
|
- `tests/e2e/` with Playwright: real Firefox + real daemon + a real file on disk.
|
||||||
|
- 72-hour soak job (M7 gate): 500 queued tasks, memory and FD graphs must be flat.
|
||||||
|
- `tools/bench` results published per commit so a performance regression is visible the day
|
||||||
|
it lands, not in M7.
|
||||||
|
|
||||||
|
## M6 — packaging
|
||||||
|
|
||||||
|
- **`.deb`** (primary): `veloxd`, `velox-gui`, `velox`, `velox-nmhost`, desktop entry,
|
||||||
|
systemd **user** units, icons, man pages, and native-messaging manifests installed to
|
||||||
|
`/usr/lib/mozilla/native-messaging-hosts/` plus a postinst that also drops the per-user
|
||||||
|
snap and flatpak copies where applicable. `lintian` clean. Publish via PPA.
|
||||||
|
- **Flatpak** (secondary): note that the sandbox changes the native-messaging story again —
|
||||||
|
test it, don't assume it.
|
||||||
|
- **AppImage** (optional): if you bundle Qt, bundle it as shared objects and honour LGPLv3
|
||||||
|
relink requirements (`docs/06` R5).
|
||||||
|
- **AMO**: signed XPI, reproducible build script, permission justification from EXT.
|
||||||
|
- **Uninstall test**: removes manifests, units, and sockets; leaves user data alone unless
|
||||||
|
purged.
|
||||||
|
- **Upgrade test**: install N-1, create tasks, upgrade, confirm the DB migrates and
|
||||||
|
in-flight `.veloxpart` files still resume.
|
||||||
|
|
||||||
|
## Definition of done
|
||||||
|
- One command builds everything from a clean checkout on a clean 26.04 VM.
|
||||||
|
- CI red on: format, tidy, sanitizer failure, conformance failure, `web-ext lint` failure.
|
||||||
|
- A fresh VM can install the `.deb`, install the extension, and complete the M2 vertical
|
||||||
|
slice with **snap Firefox** — no manual steps beyond clicking "Allow" once at pairing.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Agent brief — PROTO (contract owner)
|
||||||
|
|
||||||
|
**Runs first, alone, in M0. Then stays on call as gatekeeper for the whole project.**
|
||||||
|
|
||||||
|
## You own
|
||||||
|
```
|
||||||
|
contracts/** tools/mockd/** tests/conformance/**
|
||||||
|
```
|
||||||
|
|
||||||
|
## You may read everything. You may write nowhere else.
|
||||||
|
|
||||||
|
## Mission
|
||||||
|
Make it impossible for the CORE, DAEMON, GUI and EXT lanes to become incompatible without
|
||||||
|
CI noticing the same day.
|
||||||
|
|
||||||
|
## M0 deliverables (in order)
|
||||||
|
|
||||||
|
1. **`contracts/schema/`** — JSON Schema (draft 2020-12) for every type, method and event
|
||||||
|
in `contracts/README.md`. Two templates already exist
|
||||||
|
(`types/TaskSummary.schema.json`, `methods/capture.offer.schema.json`) — follow their
|
||||||
|
shape, including the `x-privileged` / `x-transports` / `x-deadlineMs` annotations.
|
||||||
|
2. **`contracts/openrpc.json`** — generated from the schemas; it is the doc humans read.
|
||||||
|
3. **`contracts/codegen/gen_cpp.py`** → emits `core/generated/velox_proto.{hpp,cpp}`:
|
||||||
|
plain structs, `to_json`/`from_json` (nlohmann), a `Method` enum, and a
|
||||||
|
`dispatch(method, json) -> json` skeleton. No exceptions on the hot path; parse errors
|
||||||
|
return a `Result`.
|
||||||
|
4. **`contracts/codegen/gen_ts.py`** → emits `extension/src/shared/protocol/`: discriminated
|
||||||
|
union types, a typed `call<M>()` signature, event payload types, and runtime validators
|
||||||
|
for anything crossing the WS boundary (the daemon is not allowed to trust the wire, and
|
||||||
|
neither is the extension).
|
||||||
|
5. **`contracts/fixtures/`** — every method gets a success fixture; auth, timeout, and
|
||||||
|
not-found cases get error fixtures. Use `$uuid` / `$isoDate` placeholders for values
|
||||||
|
that can't be fixed.
|
||||||
|
6. **`tools/mockd`** — Node/TS. Serves the fixtures over **both** transports (Unix socket
|
||||||
|
NDJSON and loopback WS), fakes plausible progress events at 4 Hz, and has flags for
|
||||||
|
`--slow`, `--flaky`, `--drop-connection`, `--refuse-pairing` so GUI and EXT can test
|
||||||
|
their unhappy paths before `veloxd` exists.
|
||||||
|
7. **`tests/conformance/`** — one suite, two runners: replays each fixture against a live
|
||||||
|
`veloxd` (C++ side) and through the generated TS client. Wired into CI as a **required
|
||||||
|
check on every lane's PR**.
|
||||||
|
|
||||||
|
## Definition of done
|
||||||
|
- `mockd` answers all fixtures over both transports.
|
||||||
|
- Both generated clients round-trip every fixture with no hand-written types anywhere.
|
||||||
|
- Conformance is a required CI check.
|
||||||
|
- `VERSION` frozen at `1.0.0` and the freeze announced to all lanes.
|
||||||
|
|
||||||
|
## Standing duties after M0
|
||||||
|
- You are the **only** committer to `contracts/`. Other lanes file requests; you implement,
|
||||||
|
bump `VERSION`, regenerate, update fixtures, and notify the lanes in one PR.
|
||||||
|
- Reject "just add a field locally" every single time. That request is the M2 integration
|
||||||
|
disaster arriving early enough to stop.
|
||||||
|
- Optional field or new method → minor. Rename/remove/retype → major + an ADR.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Owner: lane EXT. See ../docs/agents/AGENT-EXT.md and ../docs/05-extension-spec.md.
|
||||||
|
Firefox MV3, TypeScript. Zero download logic.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Owner: lane GUI. See ../docs/agents/AGENT-GUI.md and ../docs/03-gui-spec.md.
|
||||||
|
Qt 6 Widgets. Zero download logic. Develops against tools/mockd.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Owner: lane DAEMON. Firefox native-messaging bridge: a dumb stdio<->socket pipe, <300 lines, no logic.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Owner: lane PKG/QA. See ../docs/07-packaging.md.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
conformance/ lane PROTO - replays contracts/fixtures against C++ server and TS client. Required CI check.
|
||||||
|
integration/ lane PKG/QA - veloxd + testserver
|
||||||
|
e2e/ lane PKG/QA - Playwright: real Firefox + real daemon + real file on disk
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
mockd/ lane PROTO - mock daemon serving contracts/fixtures
|
||||||
|
testserver/ lane PKG/QA - deliberately hostile HTTP server
|
||||||
|
bench/ lane CORE - throughput and CPU benchmarks
|
||||||
|
fuzz/ lane CORE - libFuzzer targets for every parser
|
||||||
Reference in New Issue
Block a user