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]>
115 lines
6.2 KiB
Markdown
115 lines
6.2 KiB
Markdown
# 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) |
|