Add community node protocol spec, Agent Skills, and make the failing tests hermetic
DevPlace CI / test (push) Failing after 31m42s

Community nodes (Phase 1 of plan.md): the protocol specification in
services/nodes/spec.py as the single source of truth, GET /nodes/spec.json,
a generated protocol docs page, the Community nodes API docs group, and the
implementation plan plus the residential proxy research. The no-open-proxy rule
is enforced in the spec (open_proxy cannot be set).

Agent Skills: register the Skills API docs group, cascade skills on account
deletion, add the nested CLAUDE.md, README and routes docs, and cover the
parser, the fetcher (end to end against a mock origin), the data layer and the
routes with 73 unit and 16 api tests.

Tests made hermetic with the repo's existing patterns instead of ambient
environment: embeddings tests set OPENROUTER_API_KEY via monkeypatch, the
workspace tests install FakeBackend via runtime.set_backend, and the skills
install tests use run_async. DockerCliBackend now returns 127 (command not
found) when the CLI is missing instead of raising FileNotFoundError, so a host
without docker answers with a clean error rather than a 500. The Providers tab
empty state no longer names the Model routes tab.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 07:13:25 +02:00
co-authored by Claude Sonnet 5.5
parent cae139ead9
commit f55c7ee5f4
102 changed files with 7414 additions and 65 deletions
+55
View File
@@ -0,0 +1,55 @@
This file documents the community node network (`devplacepy/services/nodes/`, `devplacepy/routers/nodes/`): members opt in to donate a little of their connection so search can reach sites that block datacenter addresses. Claude Code auto-loads it whenever a file under `devplacepy/services/nodes/` is read or edited. The design record, with the measured and unmeasured claims separated, is `plan.md` at the repository root.
## What exists today
Only Phase 1 (the specification) is built. Everything else is planned and listed under "Gaps" next to the phase that closes it. Nothing here is enabled for any account.
| Piece | File | State |
|-------|------|-------|
| Protocol specification | `spec.py` | built, tested |
| `GET /nodes/spec.json` | `routers/nodes/spec.py` | built, tested, public |
| Generated protocol docs page | `templates/docs/nodes-protocol.html` | built, reads the `nodes_spec` Jinja global |
| API docs group | `docs_api/groups/nodes.py` | built, one endpoint |
## The specification is the contract
`spec.py` is the single source of truth. The server, the Go client, the browser extension, the docs page and the tests all read it. Nothing may restate a number, a frame field, an allowlisted host or a refusal code anywhere else: import it, or read it from `/nodes/spec.json`.
- `ProtocolSpec` holds `version`, `min_supported`, `LimitSpec`, `FrameSpec` tuple, `DestinationSpec` tuple and `RefusalSpec` tuple. Every one is a frozen dataclass. `SPEC.describe()` is the JSON form and the only thing the router returns.
- `open_proxy` is `field(default=False, init=False)`: it cannot be set through the constructor. **There is no open proxy mode and there will not be one.** Changing that is a protocol change that needs a new `PROTOCOL_VERSION`, not a flag.
- `ALLOWED_PORTS` is `(80, 443)`. A `DestinationSpec` may narrow it, never widen it (a unit test fails otherwise).
- `DestinationSpec.matches` is suffix-safe: a host matches when it equals the suffix or ends with `.` plus the suffix, so `evilreddit.com` and `reddit.com.evil.net` never match `reddit.com`.
- `LimitSpec` is the list of hard caps from the plan. Nothing is unbounded and nothing autotunes: a constant you can read beats magic you cannot debug. A number an admin may lower becomes a `ConfigField` later; the ceiling stays here.
- A frame type is unique per direction. An unknown type, a missing required field, an unexpected field or an oversize frame is a protocol error that closes the socket loudly, never an ignore.
The docs page `nodes-protocol.html` renders its tables from the `nodes_spec` Jinja global (`templating.py`, `NODES_SPEC.describe()`), so it cannot drift from the code. `tests/api/nodes/spec.py` asserts that every frame, destination, refusal code and limit name appears on the rendered page.
## Decisions taken (recorded so they are not re-litigated)
| Id | Decision |
|----|----------|
| D1 | devplacepy owns the node fabric; rsearch is a consumer, not the home |
| D2 | the native client is Go, one static binary per OS and architecture |
| D3 | the extension ships as a sideloadable zip first, store submission later |
| D4 | rewards are XP, badges and a leaderboard only: no coins, no money, nothing purchasable |
| D5 | donors must be adults (`age_band`) |
| D6 | a datacenter address is not a donor: residential and mobile only, admin override |
## Two transports, two trust levels
`tunnel` (native client) relays raw TCP so TLS runs end to end between DevPlace and the destination: the donor cannot read or forge content. `fetch` (browser extension, which has no raw sockets) lets the donor read and forge the response, so every `fetch` result is verified and labelled. A plain web page or a service worker cannot do either (CORS, no sockets) and is out of scope for good.
## Gaps (what the next phases close)
- Phase 2: tables, indexes, the `bandwidth_sharing` consent kind, `OWNED_TABLES` and `UNREPORTABLE_TABLES` entries, the usage ledger.
- Phase 3: registry, session, the consent choke point, the kill switch, device and ticket routes, the lock-owner WebSocket, the nginx upgrade location.
- Phase 4: broker, scheduler, dispatcher, destination policy, circuit breakers, result cache.
- Phase 5 onward: the reference client, adapters, the Go client, the extension, the profile UI, Devii tools, the CLI, the privacy and donor-terms pages, the legal gate.
- Not measured yet: Reddit and X success rates per address class, WebSocket idle timeouts through `pravda.education` and `devplace.net`, dispatcher tick cost against SQLite, socket capacity of one worker. The caps in `LimitSpec` are starting ceilings, not measurements.
## Rules for work in this directory
- Every user-facing or protocol-facing claim in the docs must describe what runs today. A page for an unbuilt feature states its status at the top, as `nodes-protocol.html` does.
- A new consent is enforced at one choke point, never at call sites (see `services/moderation/CLAUDE.md`).
- A new table here is either in `SOFT_DELETE_TABLES` or documented as derived and hard-garbage-collected; it is either in `REPORTABLE_TARGETS` or in `UNREPORTABLE_TABLES` with a reason.
- The machine may suspend a node (reversible, timed). It may never ban a user or delete a ledger row on its own: it raises a flag for a human.