From 4cfef9209ddebbb2b512640c2bea997cb90a639d Mon Sep 17 00:00:00 2001 From: Afi Date: Tue, 13 Oct 2026 11:01:52 +1030 Subject: [PATCH 1/6] Remove v0.1.2 proposal document and add roadmap for upcoming releases - Deleted the v0.1.2 proposal document as it is no longer needed. - Introduced a new roadmap.md file outlining features planned for versions 0.1.3 and 0.1.4, including CID denylist functionality, migration to sqlite3 for JSON stores, improved bootstrapping without a bootstrap node, and the ability to add files without duplication. --- audit.md | 483 ---------------------------------------------- roadmap.md | 25 +++ v0.1.2proposal.md | 444 ------------------------------------------ 3 files changed, 25 insertions(+), 927 deletions(-) delete mode 100644 audit.md create mode 100644 roadmap.md delete mode 100644 v0.1.2proposal.md diff --git a/audit.md b/audit.md deleted file mode 100644 index dd1e92c..0000000 --- a/audit.md +++ /dev/null @@ -1,483 +0,0 @@ -# rnfs maintainability audit - -*Date: 2026-10-13 · Revision: `d0aa468` (branch `dev`) · Scope: the whole repository (`rnfs/`, `tests/`, packaging, CI, docs)* - -This audit looks at how hard the code is to change safely. It is not a bug hunt. Two incidental bugs found along the way are noted where relevant. - -## Summary - -rnfs is in good shape for a project of its size: about 40k lines, 1,020 unit tests plus a 28-test end-to-end suite, all passing in about 2 minutes. - -**Strengths:** -- Docstrings explain *why* code exists and cite the spec. -- Policy code is pure and takes an injected clock. -- Wire and DAG parsers are strict. -- `specification.md` and `design.md` match the code. -- Supply-chain discipline is above average: hash-locked deps, SHA-pinned actions, and a 3 OS × 3 Python CI matrix. - -The risks to future change are not in any single module. They come from six **cross-cutting patterns**: - -1. **RNS and LXMF internals are patched and read in many places.** Upstream versions are not capped, and nothing checks for these accesses at startup. -2. **Low-level helpers are written several times over.** Atomic file writes exist 7 times, hex-ID parsing about 8 times, path resolution 4 times, and the feature utility functions 3–5 times each. The copies have already drifted. -3. **Layer boundaries leak.** The API imports the CLI. Core modules and `Config` name specific features. Features use node private members. The content store knows feature pin-source prefixes. -4. **A few classes do too much.** `RnfsNode` (~840 lines), `features/lxmf.py` (1,885 lines), `RequestServer` (~540 lines) and `Fetch` (~650 lines). -5. **Lock ownership and ordering are implicit.** Several objects have fields guarded by two different locks or none, and slow work runs while locks are held. -6. **There are no automated hygiene checks.** No lint, type-check or coverage in CI. Merge-conflict markers shipped in the 0.1.1 README, and releases are not gated on the test suite. - -None of these is urgent on its own. Together they make each new feature or RNS upgrade more expensive than it needs to be. The [roadmap](#suggested-roadmap) orders the fixes by cost against payoff. - -### Severity overview - -| Area | High | Medium | Low | -|---|---|---|---| -| Cross-cutting | 3 | 3 | – | -| Core (node, api, cli, config, names, publisher) | 1 | 5 | 7 | -| Content layer, fetcher, gateway | 1 | 5 | 7 | -| Network and DHT | 1 | 4 | 6 | -| Features | 1 | 5 | 8 | -| Tests, CI, packaging, docs | 1 | 6 | 6 | - -The cross-cutting findings in the next section are counted once, there. The per-area sections only add what is specific to that area. - ---- - -## Method - -The review was split into five areas, each read in full by a separate reviewer: -- core node, API and CLI -- content layer -- network and DHT -- features -- tests, packaging and docs - -Every High finding, and a sample of the others, was then checked by hand against the source. Supporting metrics came from an AST scan of `rnfs/`: - -| Metric | Value | -|---|---| -| Functions/methods | 1,313 | -| Functions > 60 lines / > 100 lines | 8 / 2 (`cli.build_parser` 136, `Publisher.publish_due` 102) | -| Broad `except Exception` / bare `except` | 95 (most in `features/lxmf.py` 13, `node.py` 10, `network/server.py` 9, `names.py` 8) | -| Function-local imports | 125+ (most in `features/lxmf.py` 32, `features/jobs.py` 27, `cli.py` 16, `features/keepers.py` 15) | -| `TODO` / `FIXME` markers | 0 | -| Unit tests / runtime | 1,020 tests, 58 s (Python 3.12, rns 1.5.6) | -| E2E tests / runtime | 28 tests, 57 s | - -Most functions are short. The problems are at the module and class level, not inside individual functions. - ---- - -## 1. Cross-cutting themes (highest leverage) - -### X1 · High: RNS/LXMF internals are used throughout, with no upper version bound - -**Process-wide mutations:** -- `RNS.Resource.AUTO_COMPRESS_MAX_SIZE = 0` is set twice (`node.py:214`, `network/server.py:360`). -- `guards.install()` is called three times (`node.py:215`, `network/server.py:362`, `network/links.py:190`). -- `features/lxmf.py:1540-1552` temporarily replaces `signal.signal` for the whole process while `LXMRouter` is constructed. Any other thread calling `signal.signal` in that window is silently ignored. - -**Private RNS attributes read:** -- Resource advertisement fields `t/d/u/l/i/q/p` (`network/guards.py:46-56`) -- `Link.pending_requests` (`guards.py:66`) -- `Link.incoming_resources` (`links.py:502`) -- `Link.register_outgoing_resource`, which is wrapped per link (`server.py:684-705`) -- `RNS.Identity.known_destinations` entry layout (`node.py:396-411`, `names.py:175-179`, `features/peers.py:133`) -- `Transport.get_cached_packet` (`features/keepers.py:548`) -- `_no_use=True` private kwarg (`node_content.py:63-67`) - -**Private LXMF attributes and methods:** -- `LxmfNetwork.__init__` (`features/lxmf.py:1576-1633`) replaces three `LXMRouter` methods. -- It reads `propagation_transfer_state`, `outbound_propagation_link` and `delivery_destinations`. -- It sets an attribute on a foreign `Link`. - -**Version bounds:** `pyproject.toml:13` is `rns>=1.5.6`, `lxmf>=1.2.0`. Neither has an upper bound. - -**Why it matters:** an RNS point release that renames `adv.t` makes `guards.refusal()` reject **every** incoming Resource in the process, LXMF's included. Nothing fails at startup to say why. - -**Recommendation:** -- Create `rnfs/network/rns_compat.py` (plus an LXMF equivalent) to hold every patch and every private-attribute access, each with a comment naming the upstream version it was written against. -- Call it exactly once from node startup. It should check that the attributes it needs exist and log loudly, or refuse to start, when they don't. -- Add a unit test that imports RNS/LXMF and asserts the attributes exist, so a dependency bump fails in CI. -- Cap the versions: `rns>=1.5.6,<1.6`, `lxmf>=1.2,<1.3`. - -### X2 · High: core primitives are written several times and have drifted - -| Primitive | Copies | Drift | -|---|---|---| -| Atomic file write | `content/blockstore.py:59-89`, `content/storage.py:81-97` (`write_json`), `names.py:747-762`, `cli.py:56-69` (`write_token`), `network/identity.py:45-65`, `dht/providers.py:346-360`, `features/lxmf.py:424-444` | `storage.write_json` fsyncs the file and its directory. `names._write_atomic` never syncs the directory and uses a fixed temp name. `write_token` doesn't fsync at all. `design.md` promises "atomically and durably" for all state. | -| 32-hex ID / identity-hash parsing | `api.parse_hex_id` (`api.py:149`), `identity.parse_node_id` / `parse_identity_hash` (`network/identity.py:88-101`), `protocol.is_id` (`protocol.py:52`), `lookup._valid_id` (`dht/lookup.py:41`), `routing.check_id` (`dht/routing.py:20`) plus an inline copy (`routing.py:205`), `names.parse_name` (`names.py:78`), `config._HEX_ID` (`config.py:38`), `contacts.parse_hex`, `lxmf._HEX32`, `jobs._NODE_ID`, inline `.strip().strip("<>").lower()` (`node.py:264`, `server.py:356`) | Rules differ on letter case and on whether `<…>` brackets are allowed. | -| Feature utility helpers | `human_size` ×3 (`lxmf.py:165`, `jobs.py:710`, `peers.py:169`, plus `log.size`), `_printable` ×3 (`contacts.py:322`, `follow.py:339`, `peers.py:237`, plus `lxmf.py:111` and `fsutil.printable`), `_log` ×5, `_feature(handler)` ×5 | Three different size formats. Two `_log` level conventions (`"info"` vs `"LOG_INFO"`). `_feature` sometimes raises `NotFound` and sometimes doesn't. | -| Path resolution | `walker.resolve` (`walker.py:241-252`), `edit._directory_chain` (`edit.py:16-31`), `Fetch._resolve` (`fetcher.py:326-340`), `Storage._path_cids` (`storage.py:480-506`) | `_path_cids` re-implements the spec's "greatest `first_name` ≤ name" index rule separately from `walker.step`. | -| Strict msgpack reader | `dag._Reader` (`dag.py:145-224`), `archive._StreamReader` (`archive.py:56-142`) | Same tag tables. `archive_size` (`archive.py:43-53`) re-derives the encoding byte by byte. | -| Archive export | `node.export` (`node.py:993-1015`), `node_content.export_stream` (`node_content.py:194-208`) | `export_stream` omits `max_output_entries`. | -| "Read exactly n bytes" | `builder._read_full` (unused), `chunker.read_full`/`_read_limited`, `seal._read_segment`, `archive._take` | – | -| Constants | `PARTIAL_SUFFIX` (`node.py:100`, `jobs.py:60`), the `".partial-"` literal (`storage.py:84`, `lxmf.py:429`) instead of `blockstore.PARTIAL_PREFIX`, `RESPONSE_MAX_GRACE_TIME * 1.125` (`links.py:36`, `server.py:42`) | – | - -**Recommendation:** -- Add `fsutil.write_atomic(path, data, *, mode=0o644, durable=True)` and route all seven writers through it. -- Add one `rnfs/ids.py` for ID parsing. -- Add one `rnfs/features/_common.py` (or use `fsutil`/`log` directly) for the feature helpers. -- Add one `walker.walk_path()` generator used by `resolve`, `edit`, `fetcher` and `storage`. - -These are mostly mechanical changes, and they remove a whole class of "fixed it in one copy" bugs. - -### X3 · High: layer boundaries leak in both directions - -The intended layering in `design.md` is content/network/dht → node → api/cli, with features as plugins. These imports and accesses go against it: - -| Leak | Where | -|---|---| -| The API server imports the CLI | `api.py:75` imports `CHALLENGE_HEADER`, `PROOF_HEADER` and `api_proof` from `rnfs.cli`. | -| The gateway depends on the API module for its HTTP framework | `gateway.py:46`. `TimedHandler`, `BoundedHTTPServer`, `LocalServer` and `BodyReader` (~290 lines) live in `api.py:194-481`. | -| Core modules name specific features | `api.py:135` (`contacts.is_nick`), `api.py:816` (`keepers.parse_name_arg`), `node.py:631-633` (`contacts.expand_target`), `cli.py:444/462/511/661` (`jobs.run_transfer`, `jobs.StreamProgress`) | -| `Config` owns every feature's settings | Fields at `config.py:96-119`. `Config.validate` imports `features.lxmf` (`config.py:206`). This contradicts the comment at `config.py:19`. | -| The content store knows feature pin sources | `storage.check_source` hard-codes `"follow:"` and `lxmf:` (`storage.py:52-67`) | -| Features use node and name-manager private members | `node._lookup`, `node._is_known` (`keepers.py:637,646`), `node.names.keepers = self` (`keepers.py:382`), argparse's `_actions`/`_SubParsersAction` (`keepers.py:714-715`) | -| Content helpers use node private members | `node._note_fetched`, `_prune_fetched`, `_fetching` (`node_content.py:29-31,186,198`) | -| The network layer imports a top-level module | `network/client.py:16` and `network/server.py:32` import `rnfs.mesh` | -| Hidden import cycle | `gateway_formats` ↔ `gateway`, hidden by a function-local import (`gateway_formats.py:33`) | - -**Why it matters:** adding a feature currently means editing `config.py`, `api.py`, and sometimes `node.py` and `storage.py`. The `Feature` contract in `features/__init__.py` exists precisely to avoid that. - -**Recommendation:** -- Move the token and proof helpers to `rnfs/auth.py`. -- Move the HTTP framework to `rnfs/httpserver.py`. -- Add `Feature` hooks: `settings()`, `validate_config()`, `expand_target()`, `resolve_name_arg()` and `pin_source_prefixes()`. -- Make `_lookup`/`_is_known` public. -- Give `NameManager` a `set_keepers()` method. -- Move `StreamProgress`/`ProgressLine` out of `features/jobs.py`; `rnfs cat` uses them and has nothing to do with jobs. - -### X4 · Medium: a few classes carry too many responsibilities - -| Unit | Size | Responsibilities mixed | -|---|---|---| -| `RnfsNode` (`node.py:205-1044`) | ~840 lines | Startup wiring, DHT join and seeding, maintenance scheduling, fetched-target bookkeeping, a remote-pin work queue (`node.py:823-915`), and the public facade | -| `features/lxmf.py` | 1,885 lines | URI and field codec (175-414), `MessageLog` persistence (416-685), `LxmfFeature` (691-1323), API handlers, CLI printing, `LxmfNetwork` RNS adapter (1537-1885) | -| `RequestServer` (`network/server.py:330-870`) | ~540 lines | Handler dispatch, link admission, RNS Resource tracking and monkeypatching, budget settlement | -| `Fetch` (`fetcher.py:220-872`) | ~650 lines | Traversal policy, provider ranking, pacing, lookup threads, event loop | - -**Recommendation:** -- Extract `RemotePinQueue` and `FetchedSet` from `RnfsNode`. -- Turn `features/lxmf.py` into a package: `uri.py`, `messagelog.py`, `feature.py`, `network.py`, `cli.py`. -- Extract `ResponseTracker` from `RequestServer`. -- Extract a provider scheduler from `Fetch`, and replace its positional string-tagged event tuples with `NamedTuple`s. - -### X5 · Medium: lock ownership and ordering are implicit - -**Fields guarded by more than one lock, or by none:** -- **`Response`:** `charged` and `finished` are guarded by `RequestServer.lock`. `resource` (`server.py:712,836`) and `sent` (`:853`) are written without a lock, and `finished` is read without one (`:572,597,824`). -- **`Job`:** `state` is written under both `JobsFeature._lock` (`jobs.py:507,586`) and `Job._lock` (`:135`). `total_bytes` is written with no lock (`:109`). -- **`Exchange`:** fields are written on RNS threads and polled without a lock (`links.py:60-86`). This relies on the GIL and on write order, which is not documented. - -**Implicit lock order:** the order is names → publisher → node → storage, and it isn't written down anywhere: -- `NameManager.publish/remove/release_gone` pins and calls `publisher.refresh()` while holding `names._lock` (`names.py:288,315,365-367`). -- `Publisher.refresh` walks every pin's DAG under its lock and takes `node._lock` (`publisher.py:252-258`). -- RNS announce threads can therefore block behind a full provide-set recompute. - -**Slow work under locks:** -- `storage.evict/gc/verify` run full DAG walks with disk reads under the storage RLock (`storage.py:706,721,738`). The module docstring (`storage.py:16`) says long walks happen outside the lock. -- `_note_fetched` does an fsync'd write under `node._lock` (`node.py:513-519`). - -**Busy-wait:** `server.py:670-674` polls with `time.sleep(0.01)` for up to 5 s on an RNS request thread. - -**Recommendation:** -- Document the lock order in `design.md` § Threads. -- Give each mutable field exactly one owning lock, and say which in a comment. -- Move pin and refresh calls out of the names lock. -- Snapshot targets under the storage lock and walk them outside it. -- Wait on `response.done` instead of polling. - -### X6 · Medium: no automated hygiene checks in CI - -- **No lint, format, type-check or coverage.** Neither `pyproject.toml` nor `.github/workflows/tests.yml` configures any. The code is fully annotated and already has `# noqa` comments, so the groundwork is there. -- **Things a basic check would have caught:** - - merge-conflict markers in the released README (T-H1) - - a stray `#yo` comment (`rnfs.py:3`) - - unused code: `builder._read_full`, `announces.read_mode`, `PeerLink.mtu`, `guards.installed`, `constants.REQUEST_FRAMING`, and the unused `when` at `follow.py:178` -- **95 broad exception handlers.** Many are deliberate best-effort guards around RNS. Some swallow errors without logging (`fetcher.py:652`, `seal.py:387`). - -**Recommendation:** add a fast ubuntu-only `lint` job: -- `ruff check` with the pyflakes, isort and bugbear rule sets -- `ruff format --check`, if wanted -- `! git grep -nE '^(<<<<<<<|>>>>>>>) '` - -Optionally, add `mypy rnfs` with a permissive baseline that is tightened over time, and `coverage run` with a report. Coverage would also make the test gaps in T-L3 visible. - ---- - -## 2. Core: node, API, CLI, config, names, publisher - -**C-H1 · High: `RnfsNode` is used as a bag of components.** See X4 for its size. On top of that, callers drive its internals directly: -- Features call `node.publisher.refresh()` 7 times and `node.storage.pin/unpin` directly. -- The "pin, then `publisher.refresh()`" sequence is repeated 17 times inside `node.py`, and again in `_NamePins` (`node.py:145-154`). - -*Fix:* add public `node.pin(cid, source)`, `node.unpin(...)`, `node.fetching(...)` and `node.note_fetched(...)` methods that refresh internally, so components stop being reached through the node. - -**C-M1 · Medium: add options are encoded and decoded in two places, and `seal_name` changes type.** -- `cli.add_options` (`cli.py:348-365`) and `node_content.add_options` (`node_content.py:214-237`) are paired only by convention. -- `seal_name` is a `bool` in `add_path` (`node.py:566`) but a `str` in `add_stream` (`node.py:594`). -- *Incidental bug:* `node_content.py:233` applies `parse_bool` only to values that are *already* bools, so `POST /api/add {"seal_name": "false"}` passes the string `"false"` to `add_path`. A non-empty string is truthy, so the name is sealed anyway. That is the safe direction, but it shows the type confusion. - -*Fix:* split the parameter into `seal_name: bool` and `sealed_name: str | None`, and define one shared options schema. - -**C-M2 · Medium: the offline node is half-built, so the facade is probed with `getattr`.** -- `RnfsNode.offline()` uses `cls.__new__` (`node.py:251`) and never sets `identity`, `destination` or `paths`. -- Callers therefore probe with `getattr(node, "identity", None)` (`node_content.py:52,115,130`, `node.py:1036`, `gateway.py:541`). -- `node_content.py:63-67` catches `AttributeError` to detect the offline case. - -*Fix:* initialise every attribute (as `None`) in `_setup`, describe the facade as a `typing.Protocol`, and inject identity recall the same way `is_known` is already injected. - -**C-M3 · Medium: config is the extension point for every feature.** See X3. *Fix:* per-feature settings dataclasses, or `Feature.settings()`/`validate()` hooks. - -**C-M4 · Medium: `maintain()` schedules work with an ad-hoc dict.** The `last` dict (`node.py:461-484`) mixes `-inf` with `setdefault(now)` to mean "run on the first pass". *Fix:* a small `Periodic(interval, first_run=…)` helper. - -**C-M5 · Medium: the HTTP server framework lives in `api.py`.** See X3. Timeout defaults are also duplicated between `TimedHandler` and `LocalServer` (`api.py:289-291` vs `454-456`). - -**Low:** -- **Long functions:** `cli.build_parser` (136 lines, `cli.py:688`), `Publisher.publish_due` (102 lines, `publisher.py:318`), `cli.run_daemon` (80 lines, `cli.py:263`). -- **Mode strings vs constants:** the literals `"server"`/`"client"` (`node.py:166`, `announces.py:47`, `features/peers.py`) sit alongside the `MODE_SERVER`/`MODE_CLIENT` ints in `constants.py`. -- **Misleading type:** `Publisher._provided: frozenset[Cid]` also holds `DhtKey` (`publisher.py:129`). Use a type alias. -- **Re-exported helper:** `fsutil.printable` is re-exported as `api.loggable` (`api.py:114`) and `cli.printable` (`cli.py:38`), and `jobs` imports it from `cli`. -- **Repeated call:** `_make_private_dirs` is called twice (`node.py:212,265`). -- **Untyped hook:** `node.names.keepers = self` is an untyped attribute set from outside, with `None` checks scattered through `names.py`. -- **Stale doc:** `design.md`'s facade table says `pin_ls` returns `{cid, sources, complete}`. The code also returns `type` and `name` (`node.py:795`). - -## 3. Content layer, fetcher, gateway - -**N-H1 · High: path resolution is implemented four times.** See X2. Lookup rules are the most likely part of the format to change, and they live in two separate implementations. - -**N-M1 · Medium: the content layer loads all of RNS.** -- *Measured:* importing `rnfs.content.dag`, `builder` or `storage` loads 46 `RNS.*` modules, `RNS.Transport` among them. Only `content/cid.py` loads none. -- *Cause:* `from RNS.vendor import umsgpack` runs `RNS/__init__`, and `rnfs.log` does `import RNS`. -- The package docstring does acknowledge "bundled msgpack (and logging)". The practical effect, though, is that the lazy-import workarounds in `seal.py:25,71-75,172` and `walker.py:433` don't avoid loading RNS. - -*Fix:* depend on standalone `msgpack`/`umsgpack` and a logging shim with no RNS import. Add a test asserting `"RNS" not in sys.modules` after importing `rnfs.content.*`. - -**N-M2 · Medium: generic JSON persistence lives in `content/storage.py`.** -- `read_json`/`write_json` (`storage.py:81-139`) are imported by 12 modules across every layer, mostly through function-local imports. -- Partial files in `state_dir` are never cleaned up; only the blocks directory is (`blockstore.py:31-34`). - -*Fix:* add `rnfs/jsonstate.py` on top of `fsutil.write_atomic` (see X2), with startup cleanup. - -**N-M3 · Medium: the store knows pin-source prefixes, and drifts from the spec.** -- `storage.check_source` hard-codes `follow:` and `lxmf:` (`storage.py:52-67`). -- The module docstring (`storage.py:3`) lists four sources. -- `name:` accepts any non-empty string, but spec §8 requires 32 hex characters. - -*Fix:* a registry of prefixes and validators that features register into. - -**N-M4 · Medium: `Fetch` uses implicit protocols.** -- Events are positional tuples unpacked by index (`fetcher.py:284,604-613,719,760-763`). -- `InvalidDag.root` is monkey-patched onto exception objects (`187-188,292,303-304`). -- The context manager is driven by hand (`264-265,294`). -- `describe()` reads `fetch._providers` (`878`). -- `Fetch.get` duplicates `Storage.getter` (`fetcher.py:312-317` vs `storage.py:309-319`). - -*Fix:* `NamedTuple` events, a real `root` field on `InvalidDag`, and `contextlib.ExitStack`. - -**N-M5 · Medium: the chunker is chosen by ambient `ContextVar` state.** The output CID of `build_file` depends on hidden context (`chunker.py:56,67-78`, `builder.py:136-137`), and threads don't inherit the `ContextVar`. *Fix:* pass `chunker` explicitly. - -**N-M6 · Medium: `Session` reaches into storage privates.** It uses `storage._lock` and `storage._put` (`storage.py:160,166`). *Fix:* a narrow internal API. - -**Low:** -- **Confusing names:** `dag.MAX_SIZE` (2⁶⁴−1) and `chunker.MAX_SIZE` (128 KiB) share a name. -- **Four "type" vocabularies:** dag ints, walker strings, `kind_of` strings and `Built.kind` ints. -- **Mixed concerns in `builder.py`:** it holds the secure filesystem-walk code (`O_NOFOLLOW`, junctions, `FD_WALK`, `builder.py:211-337`) that `shared_folder` also uses. Move it to `fsutil` or `content/fswalk.py`. -- **Repeated validity check:** `len(block) <= MAX_BLOCK_SIZE and cid.verify(block)` appears 4 times (`blockstore.py:109`, `storage.py:261,288,437`). -- **Gateway duplication:** the 304 helpers are duplicated (`gateway.py:481-486`, `gateway_formats.py:129-134`). `gateway.decode_segment` (`gateway.py:453-461`) re-implements part of `dag.check_name` and skips the 255-byte limit. -- **Scattered normative constants:** `MAX_ROOTS`, `MAX_NAME_BYTES`, the CID prefixes, the FastCDC parameters and the seal format all live outside `constants.py`, which says it holds the protocol constants. -- **Silent broad excepts:** `fetcher.py:652` and `seal.py:387`. - -## 4. Network and DHT - -**W-H1 · High: RNS patching is scattered.** See X1. - -**W-M1 · Medium: request handlers repeat boilerplate, and adding a message type touches five places.** -- The `start_request` / `try` / `finally: finish_request(...)` pattern is copied in four handlers (`server.py:483-491, 499-509, 515-521, 529-535`). -- `_handle_blocks` (`server.py:537-626`, 90 lines) ends with a three-branch `finally` that includes a no-op `if handed_over: pass` (621-622). -- A new message type touches constants, protocol encode/parse, a server handler, a `Backend` method and a client method. -- `Backend` (`server.py:46`) omits `feature_requests`, which is instead duck-typed at `server.py:372`. - -*Fix:* a `with self.incoming.slot(link_id):` context manager, and add `feature_requests` to the `Backend` protocol. - -**W-M2 · Medium: the clock is injected only partly.** Classes take a `clock`, yet these paths call `time.monotonic()` and `time.sleep()` directly: -- `_previous_done` (`server.py:658,670-674`) -- `PeerLink.acquire` (`links.py:130,135`) -- `_recall` (`links.py:306-308`) -- `announces.read_mode` (`announces.py:134-139`) - -They can't be driven by a fake clock in tests. - -**W-M3 · Medium: timing constants are scattered.** -- There are six different poll intervals: 0.25, 0.5, 0.02, 0.1, 0.1 and 0.01 s. -- `path_timeout + 120` (`links.py:213`) is unnamed. -- The 5.625 s pacing cap is derived twice (see X2). - -*Fix:* name them in `protocol.py` or `rns_compat`. - -**W-M4 · Medium: `PeerClient` reaches into `LinkPool`.** It takes `links.lock` and reads `links.shared` (`client.py:59-60,173-174`). *Fix:* add a `LinkPool.peer(node_id)` accessor. - -**Low:** -- **Dead code:** `announces.read_mode`, `PeerLink.mtu`, `guards.installed` and `constants.REQUEST_FRAMING`. -- **Understated timeout:** the `LinkPool.get` docstring (`links.py:197`) gives a smaller worst case than the code: `await_path` plus `_recall` plus establishment plus 5 s. -- **Wire format split across modules:** the `/name` message is missing from the `protocol.py` table and is encoded in `features/keepers.py:45`. -- **Duplicate helper:** `_never` is defined in both `lookup.py:36` and `providers.py:36`. -- **Mode checked twice:** `server.py:496` and `node.py:177`. -- **Unbounded threads:** `Lookup._send` starts one daemon thread per query, and hung threads are abandoned (`lookup.py:268`). - -## 5. Features - -**F-H1 · High: `features/lxmf.py` is six modules in one file.** It also carries most of the LXMF monkeypatching. See X1 and X4. - -**F-M1 · Medium: features look each other up in four different ways.** -- The four patterns: - - `getattr(node, "features", {}).get("contacts")` plus `hasattr(..., "lookup")` (`lxmf.py:847,859,1145`, `jobs.py:532`) - - `node.features.get(...) if hasattr(node, "features")` (`keepers.py:723`) - - `node.features.get("keepers")` (`contacts.py:194`) - - `node.feature("keepers")` (`follow.py:73`) -- Feature start order is enforced only by list order in `FEATURE_MODULES` (`features/__init__.py:20`), with a comment as the only guard. Reordering the list causes a `KeyError` at startup. - -*Fix:* a declared `requires = ("keepers",)` that `feature_classes()` validates, plus a `Feature.optional(name)` helper. - -**F-M2 · Medium: persisted state has no shared format or versioning.** -- contacts, follow and keepers write `"version": 1` (`contacts.py:107`, `follow.py:98`, `keepers.py:282`), but no `_load` ever reads it. -- `jobs.json` and the lxmf inbox/outbox files have no version at all. - -*Fix:* one `load_versioned(path, version, migrate)` helper, and a small base class for the repeated "dirty flag + save lock + snapshot" pattern. - -**F-M3 · Medium: lock ownership in jobs is unclear.** See X5. `KeeperStore.save` also re-marks the store dirty without holding its lock (`keepers.py:291`). - -**F-M4 · Medium: shutdown behaviour differs per feature.** -- `jobs` joins its threads within `STOP_WAIT`. -- `lxmf` joins with a hard-coded 2.0 s per thread (`lxmf.py:765`). -- `keepers` (`keepers.py:399`) and `follow` (`follow.py:113`) never join their threads, and `follow` doesn't save on `stop()`. -- The background loops of keepers, follow and lxmf are nearly identical. - -*Fix:* a shared `BackgroundLoop` helper (stop event, wake event, join with a time budget). - -**F-M5 · Medium: the CLI progress widgets live in `features/jobs.py`.** `StreamProgress`/`ProgressLine` (`jobs.py:798-856`) are used by core `rnfs cat`. See X3. - -**Low:** -- **Bare state strings:** auto-fetch states such as `"queued"`, `"pinned"` and `"skipped"` are bare strings across `lxmf.py:639,984,1030,1159,1238`. `jobs.py:48` already shows the better pattern. -- **Constructor does real work:** `keepers.sync_own()` imports RNS and signs records (`keepers.py:390,512-519`), which strains the "constructors must be cheap" rule in `features/__init__.py:10`. -- **API handler skips the feature:** `_api_sync` calls `_require_network()` directly (`lxmf.py:1398`), bypassing the feature's own logging. -- **Magic number:** `len(name) == 16` at `follow.py:92`, where `ID_LENGTH` is used elsewhere. -- **Re-stated defaults:** `getattr(config, "max_jobs", None)` re-states defaults that `Config` already guarantees (`jobs.py:283,344`). -- **Docstring contradicts packaging:** the `lxmf.py:25` docstring says LXMF is optional, but `pyproject.toml` requires it. - -## 6. Tests, CI, packaging, docs - -**T-H1 · High: unresolved merge-conflict markers shipped in the 0.1.1 README.** -- *Where:* `README.md:869` (`<<<<<<< HEAD`), `:870` (`=======`), `:895` (`>>>>>>> 3003517 …`). -- *Effect:* the "Tests" and "Known limitations" sections render inside the conflict block on GitHub and PyPI. - -*Fix:* resolve the block, and add the marker check from X6 to CI. - -**T-M1 · Medium: test fakes are copied and imported across test modules.** -- *Copied fakes:* - - `Clock`/`FakeClock` ×5 - - `FakeDestination` ×3 - - `FakeLink`, `FakeNetwork`, `FakeClient`, `FakeNode` ×2 each - - `free_port()` ×4 -- *Cross-imports:* about 17 test modules import fakes from *other test modules*. For example, `test_names_airtime.py:31-34` pulls from four of them. -- *Dead fallback:* each of these imports sits behind a `try/except ImportError` fallback that can never work, because `from tests.support import …` runs unguarded first. - -*Fix:* move the shared fakes into `tests/support.py` or a `tests/fakes/` package, add `tests/__init__.py`, and delete the fallbacks. - -**T-M2 · Medium: the facade fake can drift silently.** -- `tests/test_api.py:24-376` re-implements the `RnfsNode` facade in about 350 lines, and test_cli, test_gateway and test_jobs reuse it. -- Nothing in the suite uses `spec=`/`autospec`, so a change to the real facade won't break the fake. -- Tests also patch many private attributes: - - test_lxmf: `_held` ×8, `_delivered` ×7 - - test_node: `_remote_reserved` ×5 - - elsewhere: `keepers._store`, `follow._due`, `storage._delete` - -*Fix:* a contract test that compares the fake's public method signatures with `RnfsNode` via `inspect`, plus small test hooks for the most-patched internals. - -**T-M3 · Medium: tests synchronise with fixed sleeps.** About 30 `time.sleep` calls stand in for synchronisation (e.g. `test_api.py:747,765,852`, `test_gateway.py:378`, `test_links.py:365,375`, `test_jobs.py:216`). They are likely to flake on loaded macOS and Windows runners; `d0aa468` already had to fix cross-platform failures. *Fix:* move `wait_for()` from `test_e2e.py:74` into `tests/support.py` and use it. - -**T-M4 · Medium: the end-to-end suite is one long test.** `test_e2e.test_network` runs about 220 lines of sequential scenario (`test_e2e.py:247-468`), so an early failure hides every later check. *Fix:* split it into test methods that share a class-level daemon fixture. - -**T-M5 · Medium: releases aren't gated on tests, and the installer installs unreleased code.** -- The `release` job in `.github/workflows/release.yml:215` depends only on the build jobs, so a tag can be released without the test suite having passed. -- `install.sh:35` installs `main.zip` by default, so the README's `curl | sh` one-liner installs unreleased code. -- The packaging checks are copied between `release.yml` and `tests.yml`. - -*Fix:* require the test matrix in `release.yml`, have `install.sh` default to the latest release tag, and move the shared packaging checks into one script. - -**T-M6 · Medium: the README is duplicated and partly stale.** -- **Mismatched versions:** the Quick start links to v0.1.0 (`README.md:55,68`), while the Install table links to v0.1.1. The version appears about 17 times. -- **Broken markup:** headings contain stray `**` (`:70,76,79,88`), and there is a typo, "availible" (`:50`). -- **Duplicate walkthroughs:** two parallel five-step walkthroughs, "Quick start" (`:47`) and "Getting started" (`:386`). -- **Duplicate test docs:** test instructions appear in both the README and `design.md:391`, and the README's e2e command omits `tests.test_client`, which CI runs. - -*Fix:* link to `releases/latest`, merge the walkthroughs, and keep test instructions in one place. - -**Low:** -- **T-L1: five copies of the dependency list.** It lives in `requirements.txt`, `pyproject.toml`, `design.md:498`, the lock header and `update-lock.py`. `__version__` is also parsed four separate ways. Drop `requirements.txt`, or test it like the lock. -- **T-L2: CI misses Python 3.14 and two packages.** The matrix is 3.11–3.13. The arm64 `.deb` and the Flatpak are never actually exercised; the Flatpak step is `continue-on-error`. -- **T-L3: test layout doesn't mirror the package.** - - `features/keepers.py` (775 lines) and `features/follow.py` have no dedicated test files. - - `node_content.py` has no direct tests. - - Several tiny files are split off by feature. -- **T-L4: no contributor docs.** There is no `CONTRIBUTING.md` covering dev setup, running a single test, updating the locks, or the CHANGELOG convention. -- **T-L5: CHANGELOG and companion material lag behind.** - - There's no "Unreleased" entry for `d0aa468`/`22688db`. - - `Planned-companion-applications.md` is only headings. - - `companion-nomadnet-sites/` is undocumented and untested, yet depends on the `pin ls --json` output format. -- **T-L6: stray comment.** `rnfs.py:3` contains `#yo`. - -**Spot-checks that passed:** -- All 37 commands and subcommands in `rnfs --help` match the README command tables. -- Every flag the README mentions exists in the source. -- Spec §12 constants match `rnfs/constants.py` exactly. -- `Config` defaults match the spec settings table. -- `design.md` "Layout" lists every module. - ---- - -## Strengths to preserve - -These are worth protecting during any refactor: - -1. **Pure, clock-injected policy objects.** `ResponsePools`, `IncomingLinks`, `UploadPacer`, the whole of `rnfs/dht/` and `mesh.py` don't import RNS and are easy to unit-test. The DHT has no circular imports and no network dependency. -2. **Strict parsers at every trust boundary.** - - Wire messages: `protocol.py` - - DAG nodes: `dag.decode` re-encodes to check the input is canonical. - - Signed records: `keepers.parse_signed` - - LXMF fields: `lxmf.decode_field`, `parse_uri` - - These reject booleans posing as ints and over-long lists, and do cheap checks before expensive ones. -3. **One error taxonomy.** `errors.py` is mapped to HTTP statuses in one table (`api.error_status`) and shared by the gateway. -4. **Builders take only a `put(cid, block)` callback.** This keeps tree building independent of storage; `NO_PUT` gives hash-only adds. -5. **A clear `Feature` contract.** `features/__init__.py` documents the plugin lifecycle; start and stop failures are isolated per feature, and duplicate routes are rejected. -6. **Docs that are accurate.** `specification.md` matches the code on every constant and format checked, and docstrings explain *why*, citing spec sections and the attacks they defend against. -7. **Supply-chain and platform discipline.** - - Hash-locked runtime and build dependencies come from a single generator. - - Actions are pinned to SHAs, and builds run without build isolation. - - CI runs a 3 OS × 3 Python matrix with real package smoke tests. - - `test_packaging.py` keeps the locks and workflow in sync. -8. **Careful persistence.** Damaged state files are moved aside, and a corrupt `names.json` is refused, which prevents rollback attacks. - ---- - -## Suggested roadmap - -### Quick wins (≤ 1 day total, low risk) -1. Resolve the README conflict block, fix the v0.1.0 links and stray `**`, and remove `#yo` (T-H1, T-M6, T-L6). -2. Add a CI `lint` job: `ruff check` plus the conflict-marker grep (X6). Fix or `noqa` what it finds, and delete the dead code it reports. -3. Add upper bounds to `rns` and `lxmf` in `pyproject.toml` and regenerate the locks (X1). -4. Make the `release` job require the test matrix (T-M5). -5. Fix the `seal_name` parsing in `node_content.add_options` (C-M1). - -### Consolidation (a few days, mechanical) -6. `fsutil.write_atomic` replaces the seven atomic-write copies; `jsonstate.py` moves out of `content/storage.py` (X2, N-M2). -7. `rnfs/ids.py` for ID parsing; `features/_common.py` for `human_size`/`_printable`/`_log`/`_feature` (X2). -8. `tests/fakes/` for the shared test doubles, plus a facade contract test (T-M1, T-M2). -9. `rns_compat.py`, which centralises every RNS/LXMF patch and check and is called once at startup with an attribute self-test (X1). - -### Structural (plan as separate pieces of work) -10. Split `api.py` into `httpserver.py`, `auth.py` and the routes, removing `api → cli` and `gateway → api` (X3). -11. Add `Feature` hooks for settings, validation, target expansion and pin-source registration, then remove feature names from `config.py`, `api.py`, `node.py` and `storage.py` (X3, F-M1). -12. Break up `features/lxmf.py` into a package; extract `RemotePinQueue` from `RnfsNode`, `ResponseTracker` from `RequestServer`, and a provider scheduler from `Fetch` (X4). -13. Write down lock ownership and ordering in `design.md`, then move slow work out from under the locks (X5). -14. Have one path-walk generator in `walker` used by all four resolvers (N-H1). diff --git a/roadmap.md b/roadmap.md new file mode 100644 index 0000000..4c128da --- /dev/null +++ b/roadmap.md @@ -0,0 +1,25 @@ +# 0.1.3 + +## CID denylist + +- Ability to point to a CID or name as a CID deny list + +## Migrate to JSON stores to sqlite3 + +## Improved bootstraping without bootstrap node + +Add uptime & number of known nodes to startup announce + +If an announce is heard from a new node, advertise self to the new node after a random delay if the following conditions are met: + +* New node's number of known nodes is zero +* This node is the closest node in the DHT to the new node that it knows of +* This node's uptime is above one hour or + +This change should not break compatibility with previous versions. Announces without this information should still be recognised as normal + +# 0.1.4 + +## Add files without duplicating them with --nocopy flag + +## RNFS content as a folder (linux only) \ No newline at end of file diff --git a/v0.1.2proposal.md b/v0.1.2proposal.md deleted file mode 100644 index 20de167..0000000 --- a/v0.1.2proposal.md +++ /dev/null @@ -1,444 +0,0 @@ -# rnfs 0.1.2 proposal - -*Status: draft for review · Date: 2026-10-13 · Base: `dev` @ `22688db` · Source: [audit.md](audit.md)* - -## Goal - -0.1.2 is a **maintenance release**. It fixes what shipped broken in 0.1.1 and adds guardrails so the same problems can't ship again. It also takes the cheap, mechanical consolidation work from the audit while the test suite can still catch regressions. - -The larger refactors, such as splitting `api.py`, adding `Feature` hooks and breaking up `features/lxmf.py`, are deferred to 0.2.0 (see [Deferred to 0.2.0](#deferred-to-020)). - -## Ground rules for a patch release - -Every change below must keep these intact: - -- **Formats unchanged:** the wire protocol, DAG/CID encoding, archive format, sealed-file format, and on-disk state files (`*.json`, blocks, keys) are read and written exactly as in 0.1.1. -- **CLI and config unchanged:** no command, flag or config key is removed, renamed or reinterpreted. -- **HTTP API unchanged, with one exception:** the `seal_name` tightening in [F1](#f1--reject-non-boolean-seal_name-on-post-apiadd). It only affects requests that misbehave today. -- **Refactors are mechanical:** each one lands in its own PR and is covered by the existing tests. - -## Scope at a glance - -| ID | Change | Audit ref | Priority | Risk | Effort | -|---|---|---|---|---|---| -| **H1** | Resolve the README conflict block, fix stale links and broken markup | T-H1, T-M6 | Must | None | S | -| **H2** | Test that README release links match `__version__` | T-M6 | Must | None | S | -| **H3** | Remove the stray `#yo`; delete the stray `v1.0` and `test-0.1.0` tags | T-L6 | Must | None | XS | -| **H4** | Fix small inaccuracies in `design.md`; document the lock order | L7, N-M1, X5 | Should | None | S | -| **C1** | CI lint job: ruff (pyflakes rules) plus a conflict-marker check | X6 | Must | Low | S | -| **C2** | The release workflow requires the test matrix | T-M5 | Must | Low | S | -| **C3** | Add Python 3.14 to the CI matrix and build locks | T-L2 | Should | Low | S | -| **D1** | Cap upper versions of `rns` and `lxmf` | X1 | Must | Low | XS | -| **D2** | RNS/LXMF compatibility test for every private attribute rnfs relies on | X1 | Must | None | M | -| **D3** | A loud error when Resource advertisements stop matching rnfs's expectations | X1 | Should | Low | S | -| **D4** | Bump the lock to `rns==1.5.7` once D2 passes | – | Could | Low | XS | -| **F1** | Reject a non-boolean `seal_name` on `POST /api/add` | C-M1 | Must | Low | S | -| **F2** | `fsutil.write_atomic`, giving `names.json` and the API token durable writes | X2 | Must | Medium | M | -| **F3** | `install.sh` installs the latest release by default, not `main` | T-M5 | Should | Medium | S | -| **K1** | One `_printable`, `_log` and `_feature` helper for the features | X2 | Should | Low | S | -| **K2** | Each duplicated constant defined once | X2 | Should | Low | XS | -| **K3** | Remove dead code (driven by C1) | X6, W-Low | Should | Low | S | -| **T1** | Shared test fakes in `tests/support.py`; drop the dead import fallbacks | T-M1 | Should | Low | M | -| **T2** | A contract test that keeps `FakeNode` matching `RnfsNode` | T-M2 | Must | None | S | -| **T3** | Replace sleep-based synchronisation with `wait_for` in the worst offenders | T-M3 | Should | Low | S | - -Effort is per item: XS < 1 h, S ≈ half a day, M ≈ 1–2 days. **The Must items total about 3–4 days, and Must plus Should about 6–8 days.** - ---- - -## 1. Hygiene: what shipped broken - -### H1 · Resolve the README conflict and fix stale content - -`README.md:869-895` holds an unresolved conflict. The `HEAD` side is empty; the incoming side contains the "Tests" and "Known limitations" sections. Resolve it as follows: - -- **Conflict block:** keep the incoming side, removing the markers. In its Tests block, add `tests.test_client` to the E2E command so it matches what CI runs: - ```sh - RNFS_E2E=1 python3 -m unittest tests.test_e2e tests.test_client - ``` -- **Release links:** the Quick start still links to `v0.1.0` (`:55`, `:68`). Update them to the current release, and see H2 for keeping them in step. -- **Headings:** remove the stray `**` from the headings at `:70`, `:76`, `:79` and `:88`. -- **Code fence:** replace the inline code fence at `:61` with inline code. -- **Typo:** "availible" → "available" (`:50`). -- **Duplicate test docs:** remove the duplicate test instructions from `design.md:391` and link to the README section instead, so they live in one place. - -Merging the two parallel walkthroughs ("Quick start" `:47` and "Getting started" `:386`) is worthwhile, but it is an editorial rewrite. It is optional for 0.1.2. - -### H2 · Keep README release links in step with the version - -Add a test to `tests/test_packaging.py`. It finds every `releases/download/vX.Y.Z/` and `rnfs-X.Y.Z-` occurrence in `README.md` and asserts that each one equals `rnfs.__version__`. - -After that, bumping `__version__` without updating the README fails CI, so the v0.1.0/v0.1.1 drift can't recur. The existing `DependencyLockTest` already uses this pattern for the locks. - -### H3 · Remove the stray comment and tags - -- **Comment:** delete the `#yo` line in `rnfs.py:3`. -- **Tags:** the local tags `v1.0` and `test-0.1.0` both point at `7afb831`, the commit that introduced the README conflict. - - They were never released: the `version` job in `release.yml` rejects any tag that doesn't match `__version__`. - - They are still confusing, and F3 resolves "the latest release" from GitHub. - - Run `git ls-remote --tags origin` to check whether they were pushed. If so, delete them with `git push origin :refs/tags/v1.0 :refs/tags/test-0.1.0`, and delete them locally either way. - -### H4 · Fix small inaccuracies in design.md - -- **Facade table:** `pin_ls` also returns `type` and `name` (`node.py:795`). -- **Content layer:** replace "content/: no RNS dependency" with what is actually true. The layer uses RNS's bundled msgpack and RNS logging, so importing it loads RNS. -- **Threads section:** add the lock order as it actually is: `names._lock` → `publisher` lock → `node._lock` → `storage` lock. Also list the known places where slow work runs while a lock is held (audit X5). - - This is documentation only. It doesn't fix anything, but it makes the current behaviour reviewable, and it is the prerequisite for the 0.2.0 locking work. - ---- - -## 2. CI guardrails - -### C1 · Lint job - -The code already contains `# noqa: BLE001` and `# noqa: E402` comments, which are ruff/flake8 rule codes, so ruff is the natural fit. - -**Supply chain:** keep the existing hash-locking discipline: -- Add `ruff` to `BUILD_TOOLS` in `packaging/update-lock.py:54` and regenerate `build-tools.lock`. -- Add `ruff` to the names asserted by `test_build_locks` (`tests/test_packaging.py:162`). - -**Configuration** in `pyproject.toml`. Start with rules that only flag real defects, so the first pass is fixes, not reformatting: - -```toml -[tool.ruff] -target-version = "py311" -extend-exclude = ["packaging/flatpak"] - -[tool.ruff.lint] -select = ["F", "E9", "B006", "B008", "B904"] # pyflakes, syntax errors, mutable/call defaults, raise-from -``` - -**Job** in `.github/workflows/tests.yml`, running on `ubuntu-latest` with Python 3.12: - -```yaml -lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - with: { persist-credentials: false } - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 - with: { python-version: "3.12" } - - run: python -m pip install --require-hashes --only-binary=:all: -r packaging/build-tools.lock - - run: ruff check . - - name: No merge-conflict markers - run: "! git grep -nE '^(<<<<<<<|>>>>>>>)( |$)|^=======$' -- . ':!*.lock'" -``` - -Fix everything the first run reports in the same PR. K3 lists the dead code it is expected to find. Formatting (`ruff format`) and type-checking (mypy) stay out of 0.1.2: both would produce a large diff and belong in their own release. - -### C2 · Gate releases on the tests - -Today, `release.yml:215` makes the `release` job depend only on the build jobs, so a tag publishes even if the unit or E2E suite would fail. To fix this: - -1. Make `tests.yml` reusable by adding `workflow_call:` to its `on:` triggers. -2. In `release.yml`, add a job `tests: { uses: ./.github/workflows/tests.yml }` and add `tests` to the `release` job's `needs`. - -The packaging smoke tests then also run for every release, which means the duplicated sdist/wheel checks in `release.yml` can be removed (audit T-M5). - -### C3 · Python 3.14 - -- Add `"3.14"` to the matrix in `tests.yml` and to `BUILD_PYTHONS` in `update-lock.py:60`, then regenerate the locks. -- Add the `Programming Language :: Python :: 3.14` classifier if `pyproject.toml` lists versions. -- **Condition:** this depends on every locked dependency publishing 3.14 wheels (`cryptography` ships abi3 wheels, and `cffi` ≥ 2.0 has cp314). If the lock can't be regenerated cleanly, leave 3.14 for 0.1.3 rather than relaxing `--only-binary`. - ---- - -## 3. Dependency safety - -### D1 · Upper version bounds - -```toml -dependencies = ["rns>=1.5.6,<1.6", "lxmf>=1.2.0,<1.3"] -``` - -- **Why:** rnfs patches and reads RNS/LXMF internals in about 15 places (audit X1). An unbounded `pip install rnfs` should not pick up a release nobody has tested. -- **Caveat:** RNS does not follow strict semver; internals have changed in point releases. That is why the cap alone isn't enough and D2 is the real protection. -- **Other dependency copies:** apply the same bounds to `requirements.txt`, or delete it. It is the fifth copy of the dependency list (T-L1), and the README's "From source" can use `pip install -e .`. See Q3. -- **Lock test:** the lock test only parses package names (`tests/test_packaging.py:150`), so ranges need no test change. - -### D2 · Compatibility test for RNS and LXMF internals - -Add `tests/test_rns_compat.py`. It imports the *installed* RNS and LXMF and asserts that every private attribute, method and data layout rnfs depends on is present. Collect them into one documented list at the top of the test: - -| rnfs code | RNS/LXMF surface | -|---|---| -| `network/guards.py:46-66` | `ResourceAdvertisement` fields `t d u l i q p` (after `unpack`), `Link.pending_requests`, `Resource.accept`/`reject` | -| `network/server.py:684-705` | `Link.register_outgoing_resource`, `Link.outgoing_resources` | -| `network/links.py:502` | `Link.incoming_resources` | -| `node.py:214`, `server.py:360` | `Resource.AUTO_COMPRESS_MAX_SIZE` | -| `node.py:396-411`, `names.py:175-179`, `features/peers.py:133` | `Identity.known_destinations` entry layout | -| `features/keepers.py:548` | `Transport.get_cached_packet` | -| `node_content.py:63-67` | `Identity.recall(..., _no_use=...)` keyword | -| `features/lxmf.py:1576-1633,1712,1777,1637` | `LXMRouter.get_announce_app_data`, `delivery_resource_transfer_began`, `request_messages_from_propagation_node`, `propagation_transfer_state`, `PR_*` constants, `outbound_propagation_link`, `delivery_destinations` | - -**For the advertisement fields**, prefer a real round trip: pack an advertisement the way RNS does and `unpack` it. A simple `hasattr` on the class is not enough, because the fields are set per instance. - -Each failing assertion should name the rnfs module that needs updating. - -**Why:** a dependency bump, whether Dependabot, D4 or a user's own environment, then fails CI with a precise message rather than at runtime. The full `rns_compat.py` module that centralises the patches (audit X1) is deferred to 0.2.0. This test is the cheap half of that work, and it also produces the inventory the module will need. - -### D3 · Make an RNS incompatibility visible at runtime - -`guards.refusal()` returns `"malformed sizes"` when `adv.t`/`adv.d` are missing, so a renamed field in RNS would silently reject every incoming Resource, LXMF's included. - -**Proposed change:** -- If the attributes are *absent*, as opposed to present but invalid, log once at `LOG_ERROR`: - > `this RNS version (x.y.z) isn't supported by rnfs 0.1.2: incoming Resources are being refused` -- Keep failing closed. The guard exists to stop oversized allocations, and failing open would undo that. - -This is a change to logging only. Add a test that feeds an advertisement object without `t`/`d` and asserts on both the refusal and the single log line. - -### D4 · Bump the lock to rns 1.5.7 - -RNS 1.5.7 is on PyPI; the lock pins 1.5.6. Once D2 passes against 1.5.7, regenerate with `python3 -I packaging/update-lock.py rns==1.5.7 lxmf==1.2.0` and `python3 packaging/flatpak/generate-sources.py`, and run the full CI matrix. - -This is optional for 0.1.2. Pick it up only if the RNS 1.5.7 changelog includes fixes rnfs users need. - ---- - -## 4. Fixes - -### F1 · Reject a non-boolean `seal_name` on `POST /api/add` - -**Bug:** `node_content.add_options` (`node_content.py:231-233`) applies `parse_bool` only to values that are *already* booleans. Any other value passes through unchanged, so `POST /api/add {"seal_name": "false"}` reaches `add_path(seal_name="false")`, where the string counts as true. The failure is safe (the name is sealed anyway), but it is wrong. - -The parameter means two different things: - -| Endpoint | `seal_name` means | Type | -|---|---|---| -| `POST /api/add` (JSON body) | whether to seal the file's name | `bool` | -| `POST /api/add/stream` (query) | the name to seal inside | `str` | - -**Change:** -- Pass the endpoint kind into `add_options`, e.g. `add_options(value, stream=False)`. -- For the JSON endpoint, accept only a JSON boolean, and raise `ValueError("seal_name must be true or false")` otherwise; the API turns that into a 400. -- Leave the stream endpoint unchanged. -- The CLI (`cli.add_options`, `cli.py:362-364`) already sends `False` or a name string correctly, so it is unaffected. -- Add API tests for `true`, `false`, `"false"` (expect 400) and `1` (expect 400). - -Renaming the parameters to `seal_name: bool` and `sealed_name: str` is the clean fix, but it changes the API, so it is deferred to 0.2.0. - -### F2 · `fsutil.write_atomic` - -`design.md` promises that state is written "atomically and durably". Three of the seven copies of the atomic-write logic don't fully keep that promise: - -| Writer | fsync file | fsync dir | Unique temp | Notes | -|---|---|---|---|---| -| `content/storage.py:81-97` `write_json` | ✓ | ✓ | ✓ | the reference behaviour | -| `names.py:747-762` `_write_atomic` | ✓ | ✗ | ✗ (`path + ".tmp"`) | `names.json` and name keys | -| `cli.py:56-69` `write_token` | ✗ | ✗ | ✓ | `O_EXCL \| NOFOLLOW`, mode 0600 | -| `network/identity.py:45-65`, `dht/providers.py:346-360`, `features/lxmf.py:424-444` | ✓ | varies | ✓ | copies of `write_json` | - -**Proposed helper** in `rnfs/fsutil.py`: - -```python -def write_atomic(path: str, data: bytes, *, mode: int = 0o644, durable: bool = True) -> None: - """Replace path with data: a new temp file in the same directory (O_EXCL, O_NOFOLLOW where available, - created with mode), written and fsynced, renamed over path (retried on Windows), then the directory - synced if durable. On any failure the temp file is removed and path is left as it was.""" -``` - -**Migration**, one commit each: -1. `names._write_atomic` → `write_atomic`. It gains the directory sync and a unique temp name. -2. `cli.write_token` → `write_atomic(path, token, mode=0o600)`. It gains the fsync and keeps the 0600 and no-follow guarantees. -3. `storage.write_json` → `json.dumps` followed by `write_atomic`. Add an `ensure_ascii` parameter so `lxmf.MessageLog` can drop its own copy. -4. `identity` and `providers` → `write_atomic`. - -**Leave `blockstore.put` alone.** It is the hot path, has its own partial-file prefix and startup cleanup, and is already correct. - -**Tests:** unit tests for `write_atomic` covering mode, replacing an existing file, refusing to follow a symlinked temp, and cleanup after an exception mid-write. CI's three-OS matrix covers the Windows `replace` retry. - -**Risk:** Medium, because these paths write keys and tokens. Mitigations: one PR per call site, each with the existing tests for that file. File contents and names are unchanged, so no state migration is needed. - -### F3 · `install.sh` installs the latest release - -`install.sh:35` installs `archive/refs/heads/main.zip`, so the README's `curl … | sh` one-liner installs unreleased code, and `--update` "re-installs from the latest main branch". - -**Change:** -- **Default:** resolve the latest release by following the `https://github.com/afit21/rnfs/releases/latest` redirect with the script's existing Python downloader (`install.sh:261-267`). This uses no GitHub API and so has no rate limit. Take the tag from the final URL and install `archive/refs/tags/.zip`. -- **`--main`:** a new flag that keeps the old behaviour. -- **`RNFS_SOURCE`:** still overrides both. CI uses it, so the packaging smoke test is unaffected. -- **Docs:** update the usage text (`install.sh:4-13`) and the README install section. - -**Compatibility constraint:** the `install.sh` on `main` will now install trees from older tags. It must keep working with the 0.1.1+ layout (`packaging/requirements.lock`, `packaging/build-backend.lock`), which is already true. Add a comment saying so. - -**Risk:** Medium, because the installer is user-facing. The `--update` flow should be checked by hand on Linux and macOS before tagging. - ---- - -## 5. Low-risk consolidation - -### K1 · Shared feature helpers - -Create `rnfs/features/_common.py`. It replaces the copies of three helpers in five feature modules: - -- **`printable`:** delete the `_printable` copies in `contacts.py:322`, `follow.py:339`, `peers.py:237` and `lxmf.py:111`, and use `fsutil.printable`. Their output is identical for control characters (`repr(c)[1:-1]` versus `unicode_escape`). -- **`log(message, level=LOG_VERBOSE)`:** one helper taking RNS's level constants. It replaces the five `_log` definitions and removes lxmf's `"info"` string-level variant (`lxmf.py:103`). -- **`feature(handler, name)`:** one helper that always raises `NotFound` when the feature isn't loaded. It replaces the five `_feature` copies and the inlined lookup in `peers.py:159`. `lxmf`'s copy (`lxmf.py:1332`) lets the `KeyError` from `node.feature()` escape. In a real daemon this can't happen: a feature's routes exist only while it is loaded. It only matters for nodes without the feature, such as `RnfsNode.offline()` and test fakes, so it is a consistency fix, not a user-visible change. - -**Not in 0.1.2:** the three `human_size` implementations format sizes differently, so unifying them changes CLI output. That waits for 0.2.0. - -### K2 · Duplicated constants - -- **`PARTIAL_SUFFIX`:** defined in `node.py:100` and `features/jobs.py:60`. Define it once in `constants.py`. -- **`".partial-"` literals:** at `storage.py:84` and `lxmf.py:429`. Use `blockstore.PARTIAL_PREFIX`, or the helper from F2. -- **`RESPONSE_MAX_GRACE_TIME * 1.125`:** computed separately in `links.py:36` and `server.py:42`. Define it once in `network/protocol.py`, named for the 5.625 s pacing cap in spec §3. -- **ID length:** replace the literal `16` at `follow.py:92` with `ID_LENGTH`. - -### K3 · Dead code - -Remove the following after confirming that nothing, tests included, references them; C1 will flag most of them: - -- `builder._read_full` (`builder.py:114-124`) -- `announces.read_mode` (`announces.py:126`) -- `PeerLink.mtu` (`links.py:121`) -- `guards.installed` (`guards.py:102`) -- `constants.REQUEST_FRAMING` (`constants.py:26`) -- the unused `when` (`follow.py:178`) -- the no-op `if handed_over: pass` branch (`server.py:621-622`); invert the condition rather than removing the branch's meaning -- the second `_make_private_dirs` call (`node.py:212` / `:265`), if `_setup` always runs after the first - ---- - -## 6. Tests - -### T1 · Shared fakes in `tests/support.py` - -Move the duplicated test doubles into `tests/support.py`, or a `tests/fakes.py` if `support.py` gets too long: - -- `Clock` ×3 and `FakeClock` ×2 become one `FakeClock` -- `free_port` ×4, including `packaging/smoke_test.py`, which can import it or keep its own copy because it runs outside the test tree -- `FakeDestination` ×3 -- `FakeLink` ×2 -- `wait_for`, from `test_e2e.py:74` - -Then: -- Delete the `try/except ImportError` "run from inside tests/" fallbacks. They can't work, because `from tests.support import …` runs unguarded before them. -- Add `tests/__init__.py`. -- Check that both `python -m unittest discover tests` and the CI invocations still collect all 1,020 tests. Record the count in the PR. - -Moving `FakeNode` out of `test_api.py` is optional; the cross-module imports work, they are just untidy. - -### T2 · Facade contract test - -Add a test that compares the public methods of `tests.test_api.FakeNode` with `rnfs.node.RnfsNode` using `inspect.signature`: -- Every method the fake defines must exist on the real class. -- Parameter names and defaults must match. - -The fake is about 350 lines and is used by four test modules, and nothing currently stops it drifting from the real facade. This test is cheap insurance before the 0.2.0 refactors change the facade. - -### T3 · Replace sleeps used for synchronisation - -Using T1's `wait_for(condition, timeout)`, replace the fixed sleeps that wait for something to happen. The highest-value places: - -- `test_api.py:747`, `:765`, `:852` -- `test_gateway.py:378` -- `test_links.py:365`, `:375` -- `test_jobs.py:216` - -Sleeps that deliberately let time pass for a real clock can stay. `d0aa468` shows these timing-based tests already fail on the slower macOS and Windows CI runners. - ---- - -## Deferred to 0.2.0 - -These need design work, change interfaces, or move large amounts of code. They are worth doing, but not in a patch release. - -| Item | Audit ref | Why not now | -|---|---|---| -| `rns_compat.py` centralising every RNS/LXMF patch, called once at startup | X1 | Touches startup order in `node.py`, `server.py` and `links.py`. D2 builds the inventory first. | -| Split `api.py` into `httpserver.py`, `auth.py` and the routes | X3, C-M5 | Moves about 600 lines and changes imports in gateway, CLI and tests | -| `Feature` hooks (`settings`, `validate_config`, `expand_target`, `resolve_name_arg`, pin-source registration), and taking feature knowledge out of `config.py`, `api.py`, `node.py` and `storage.py` | X3, F-M1, N-M3 | Changes the plugin interface | -| Declared feature dependencies (`requires = (...)`) | F-M1 | Same | -| Make `features/lxmf.py` a package | X4, F-H1 | Large move; best after `rns_compat` | -| Extract `RemotePinQueue`/`FetchedSet` (node), `ResponseTracker` (server) and a provider scheduler (fetcher) | X4, C-H1, W-M1, N-M4 | Structural; needs lock-ownership decisions first | -| Lock ownership per field; move slow work out of locks | X5 | Behavioural concurrency change; H4 documents it first | -| One `walker.walk_path()` for the four path resolvers | N-H1 | Touches format-critical lookup code; wants new property tests | -| One strict msgpack reader for `dag` and `archive` | X2, N-M5 | Format-critical | -| A content layer free of RNS (standalone msgpack, logging shim) | N-M1 | Adds a dependency | -| Versioned state loader with migrations | F-M2 | Defines the state-file compatibility policy | -| `seal_name` / `sealed_name` split, one add-options schema | C-M1 | API change | -| Unify `human_size` | X2 | Changes CLI output | -| `ruff format`, mypy, coverage reporting | X6 | Large diffs; better in their own release | -| Split the E2E scenario into separate test methods | T-M4 | Larger test rewrite | -| Explicit `chunker` parameter instead of a `ContextVar` | N-M5 | Changes `build_file`'s signature | - -### Audit items examined and dropped - -Checking against the code showed that these need no change: - -- **"follow doesn't save on stop"** (audit F-M4): `follow` writes its state on every change (`follow.py:131,148,224`), so there is nothing unsaved at shutdown. -- **"`KeeperStore.save` re-marks dirty without the lock"** (F-M3): saves are serialised by `_save_lock`, and setting `dirty = True` after a failed write races only with other code setting it to `True`. It is benign. -- **"keepers `stop()` doesn't join its loop"**: the loop's `save()` and `stop()`'s `save()` are serialised by the same `_save_lock`, so there is no torn write. Joining would only tidy shutdown logging; it is left for the shared `BackgroundLoop` helper in 0.2.0. - ---- - -## Compatibility statement for 0.1.2 - -| Surface | Change | -|---|---| -| Wire protocol, DAG/CID, archive, sealed files | None | -| State files and keys | None. Same contents and names, now always written durably (F2). | -| CLI commands, flags and output | None | -| Config keys | None | -| HTTP API | `POST /api/add` returns 400 for a non-boolean `seal_name` (F1). No other change. | -| Dependencies | `rns<1.6`, `lxmf<1.3` (D1); optionally `rns==1.5.7` in the lock (D4) | -| Installer | Installs the latest release by default; `--main` gives the old behaviour (F3) | -| Python | 3.11–3.13, plus 3.14 if C3 lands | - -## Suggested PR sequence - -Each PR is independently reviewable and keeps CI green: - -1. **Hygiene:** H1, H2, H3, H4. -2. **Lint:** C1, plus the fixes ruff reports and K3. -3. **Release gating:** C2 (and C3 if the locks regenerate cleanly). -4. **Dependencies:** D1, D2, D3 (D4 optional). -5. **`seal_name`:** F1. -6. **Atomic writes:** F2, one commit per call site. -7. **Helpers and constants:** K1, K2. -8. **Tests:** T1, T2, T3. -9. **Installer:** F3, with a manual install and `--update` check on Linux and macOS. -10. **Release:** - - bump `__version__` to `0.1.2` - - update the README links (H2 enforces this) - - add the CHANGELOG entry - - regenerate the Flatpak sources if the lock changed - - tag `v0.1.2`; C2 runs the full matrix before publishing - -## Draft CHANGELOG entry - -```markdown -## 0.1.2 - -Maintenance release from the fourth audit (audit.md): fixes, release guardrails and internal consolidation. -No format, protocol, CLI or config changes. - -Fixes: -- `POST /api/add` refuses a `seal_name` that isn't `true` or `false` (a string like `"false"` used to seal the - name anyway). `/api/add/stream`, where `seal_name` is the name, is unchanged. -- `names.json`, name keys and the API token are written durably (file and directory synced, unique temp - file), like the other state files. -- The README's Tests and Known limitations sections are restored (0.1.1 shipped them inside a merge - conflict), and its download links point at the current release. - -Dependencies and install: -- rns is limited to <1.6 and lxmf to <1.3; a test checks every RNS/LXMF internal rnfs relies on, and the - daemon logs an error if incoming Resource advertisements no longer look as rnfs expects. -- install.sh installs the latest release instead of the main branch (`--main` for the old behaviour). - -Development: -- CI lints with ruff and rejects merge-conflict markers; releases run the full test matrix before - publishing. -- Shared test fakes live in tests/support.py; a contract test keeps the API tests' fake node in step with - RnfsNode. -``` - -## Open questions for the maintainer - -1. **RNS cap:** `<1.6`, or tighter (`<1.5.8`)? The tighter cap is safer, but blocks users from RNS patch fixes until rnfs is re-released. **Recommendation:** `<1.6` plus D2. -2. **Installer default (F3):** OK to change in a patch release? Users who relied on `install.sh` tracking `main` would need `--main`. **Recommendation:** yes. Today's behaviour contradicts the README. -3. **`requirements.txt`:** delete it, or keep it in sync with a test? **Recommendation:** delete it, and point "From source" at `pip install -e .`. -4. **Python 3.14 (C3):** include it if the locks regenerate cleanly, or hold it for a dedicated release? -5. **Stray tags (H3):** confirm that `v1.0` and `test-0.1.0` can be deleted from the remote, if they were pushed. From 1b5629cd5a6387bc4286d262b858ba4eb0ce70ff Mon Sep 17 00:00:00 2001 From: Afi <46953457+afit21@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:47:33 +1030 Subject: [PATCH 2/6] Remove tests and debugging section from README Removed test instructions and daemon debugging information from the README. --- README.md | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/README.md b/README.md index 724f382..fcb73b5 100644 --- a/README.md +++ b/README.md @@ -870,17 +870,7 @@ rnfs --dir /tmp/a --api-port 9001 add somefile rnfs --dir /tmp/b --api-port 9002 get -o out ``` -`tests/test_e2e.py` sets up exactly this, with short intervals in each node's `rnfs.toml`. - -## Tests - -```sh -python3 -m unittest discover tests # unit tests, no network -RNFS_E2E=1 python3 -m unittest tests.test_e2e tests.test_client # real daemons over local TCP -python3 packaging/smoke_test.py dist/rnfs # smoke-test a built package -``` - -To debug a running daemon, `kill -USR1 ` prints every thread's stack. +ery thread's stack. ## Known limitations @@ -892,6 +882,3 @@ To debug a running daemon, `kill -USR1 ` prints every thread's stack a directory leaves the sealed files inside it encrypted. - LXMF: messages over 256 kB are refused, and unsent messages are lost if the daemon restarts. Offline delivery and downloads need `lxmf_propagation_node` (or `--nearest` each time). -- Windows support is new. - -The full list is in [design.md, Status](design.md#status). From 380dc6cb6edfd06c883c716ca90251119aafaab2 Mon Sep 17 00:00:00 2001 From: Afi Date: Tue, 13 Oct 2026 19:56:40 +1030 Subject: [PATCH 3/6] Join through built-in bootstrap nodes, and rejoin while too few servers are known --- CHANGELOG.md | 11 +++++ README.md | 15 ++++--- design.md | 7 ++- rnfs/config.py | 5 ++- rnfs/constants.py | 9 ++++ rnfs/dht/routing.py | 7 +++ rnfs/node.py | 99 +++++++++++++++++++++++++++++++++++-------- roadmap.md | 38 ++++++++++++----- specification.md | 23 +++++++--- tests/test_config.py | 2 +- tests/test_e2e.py | 1 + tests/test_node.py | 77 ++++++++++++++++++++++++++++++++- tests/test_routing.py | 11 +++++ 13 files changed, 261 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51a0bb0..423ef9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ [specification.md](specification.md) is the authority on behaviour. +## Unreleased + +Joining the DHT: +- Built-in bootstrap nodes (`DEFAULT_BOOTSTRAP_NODES`): every node contacts them when joining, after its own + `bootstrap_nodes`, so a new node gets in even where no announces reach it (e.g. behind a LoRa access + point). `default_bootstrap = false` turns them off; private mode never uses them. +- Bootstrap nodes are asked one at a time until two answer, outside the lookup's budget for unknown node IDs: + a slow node used to try only one of them, and gave up if that one was down. +- The node joins again while it knows fewer than 3 live DHT servers, 5 minutes after the last join and + doubling up to `announce_interval`. It used to join only at startup. + ## 0.1.2 Maintenance release from the fourth audit: fixes, release guardrails and internal consolidation. No format, diff --git a/README.md b/README.md index fcb73b5..f74a31b 100644 --- a/README.md +++ b/README.md @@ -420,16 +420,18 @@ rnfs nodes: - **Radio:** add an `RNodeInterface` for LoRa. See the [Reticulum manual](https://reticulum.network/manual/interfaces.html). -Your node then learns about other rnfs nodes from their announces. Give it a **bootstrap node** as well, so -it can join the DHT straight away rather than waiting for announces: +Your node then learns about other rnfs nodes from their announces, and joins the DHT through the +project's **bootstrap nodes**, which are built in. It contacts them at every start, and again whenever it +knows fewer than 3 nodes, so it also gets in where no announces reach it (for example behind a LoRa access +point), as long as Reticulum can reach them. To join through nodes you choose as well, such as a friend's +(`rnfs status` shows its node ID), give them first: ```sh rnfs daemon --bootstrap # or bootstrap_nodes = [""] in ~/.rnfs/rnfs.toml ``` -> *Placeholder: the project doesn't run a public bootstrap node yet. When it does, its node ID (and -> Reticulum entry point) will be listed here. Meanwhile, use the node ID of a friend's node (`rnfs status` -> shows it).* +`default_bootstrap = false` in `rnfs.toml` stops rnfs contacting the built-in nodes. Private mode never +contacts them. `rnfs peers` lists the nodes yours knows, nearest first. @@ -526,7 +528,8 @@ lxmf_auto_fetch_from = [""] | Setting | Default | Description | |---|---|---| | `reticulum_dir` | RNS's own | Reticulum config directory (`daemon --rnsconfig`) | -| `bootstrap_nodes` | `[]` | Node IDs to join the DHT through at startup (`daemon --bootstrap`) | +| `bootstrap_nodes` | `[]` | Node IDs to join the DHT through, contacted before the built-in ones (`daemon --bootstrap`) | +| `default_bootstrap` | `true` | Also join through the project's built-in bootstrap nodes. Never used in private mode | | `dht_mode` | `"server"` | `"server"` stores DHT records for others; `"client"` only asks (for nodes often offline or short of bandwidth) | | `slow` | `"auto"` | `"yes"`, `"no"` or `"auto"`: a slow node uses one provider at a time and fewer parallel queries. `auto` is slow when every interface is under `slow_bitrate` | | `slow_bitrate` | `100000` | Links and interfaces below this (bits/s) count as slow | diff --git a/design.md b/design.md index 5ee46a3..8369bd3 100644 --- a/design.md +++ b/design.md @@ -362,7 +362,7 @@ name in the output's directory; the CLI sets it for `rnfs get ` withou | Thread | Owner | Does | |---|---|---| -| `rnfs-join`, `rnfs-publisher`, `rnfs-names`, `rnfs-maintenance` | node | joining, publishing, name announces, periodic saves and eviction | +| `rnfs-join`, `rnfs-publisher`, `rnfs-names`, `rnfs-maintenance` | node | joining (and joining again while under `MIN_SERVERS`), publishing, name announces, periodic saves and eviction | | `rnfs-announcer`, `rnfs-response-monitor`, `rnfs-link-sweeper` | network | rnfs.dht announces, `/blocks` deadlines and displacement, idle links | | `rnfs-lookup`, `rnfs-fetch`, `rnfs-find-providers`, `rnfs-remote-pin` | per operation | lookups, fetch workers, provider search, remote-pin fetches (one thread per source with pins waiting) | | `rnfs-job-` | jobs | one per running job, at most `max_jobs`; `stop()` waits ≤ 2 s | @@ -416,6 +416,11 @@ checks a built package (`--version`, `--help`, daemon and status). Everything in the specification is implemented, with these simplifications and additions: - The routing table isn't persisted. At startup it is seeded from the rnfs.dht server announces that RNS cached within 2 × `announce_interval` (`Identity.known_destinations`, an RNS internal), then the join runs. +- Joining (`RnfsNode._join`, `join`): bootstrap nodes are asked one at a time with `PeerClient.find`, outside + any `Lookup`, until `BOOTSTRAP_ANSWERS` (2) answer; the lookup that follows excludes them. The thread then + checks `RoutingTable.live_count()` every `REJOIN_DELAY` (300 s) and joins again while it is under + `MIN_SERVERS` (3), doubling the wait up to `announce_interval`. `DEFAULT_BOOTSTRAP_NODES` lives in + `rnfs/constants.py` and changes only with a release; the e2e test turns it off (`default_bootstrap = false`). - Publishing runs one closest-nodes lookup per due CID, one after another. Lookups aren't shared between keys that have the same closest nodes, but `/provide` requests are batched per node. Slow nodes pace these lookups (at most 10 per round, 30 s per lookup on average), which only delays republishing; withdrawals diff --git a/rnfs/config.py b/rnfs/config.py index 23b3544..27d291c 100644 --- a/rnfs/config.py +++ b/rnfs/config.py @@ -33,7 +33,7 @@ OPTIONAL = ("republish_interval", "max_upload_rate") # these may also be unset (None) # Some durations are waited on, and Windows can wait at most ~49 days at once (threading.TIMEOUT_MAX) MAX_DURATION = 30 * DAY -FLAGS = ("private", "gateway_scripts") +FLAGS = ("private", "gateway_scripts", "default_bootstrap") IDENTITY_LISTS = ("members", "remote_pin_identities") # RNS identity hashes: 32 hex digits, optionally _HEX_ID = re.compile(r"[0-9a-fA-F]{32}") @@ -53,7 +53,8 @@ class Config: reticulum_dir: str | None = None api_port: int = DEFAULT_API_PORT gateway_port: int = DEFAULT_GATEWAY_PORT - bootstrap_nodes: list[str] = field(default_factory=list) # node IDs (hex) queried at startup + bootstrap_nodes: list[str] = field(default_factory=list) # node IDs (hex) contacted first when joining + default_bootstrap: bool = True # also contact DEFAULT_BOOTSTRAP_NODES when joining (never in private mode) verbosity: int = 0 # added to Reticulum's loglevel: 1 shows per-query detail, 2 shows RNS debug output slow: str = "auto" # "yes", "no" or "auto" (section 2) diff --git a/rnfs/constants.py b/rnfs/constants.py index 02564e5..f6eefb8 100644 --- a/rnfs/constants.py +++ b/rnfs/constants.py @@ -44,3 +44,12 @@ # Archive header (section 8) ARCHIVE_MAGIC = "rnfs-archive" ARCHIVE_VERSION = 1 + +# Local, not protocol: the project's always-on DHT servers, which a join contacts after bootstrap_nodes unless +# default_bootstrap is off or the node is in private mode (section 6). Node IDs, changed only by a release +DEFAULT_BOOTSTRAP_NODES = ( + "d8be7a8a1a2c3beb2cdd0d33e7225c21", # Broken Hill, Australia + "5e395b3045b68c881d5db62c146033eb", # Sydney, Australia (AWS) + "bddad1ec2f4a4a6112d3d42afabfd94a", # Broken Hill, Australia + "24770a7a9221b97d583905a839bc4fce", # Central US (Google Cloud) +) \ No newline at end of file diff --git a/rnfs/dht/routing.py b/rnfs/dht/routing.py index ba05c40..eb86f9f 100644 --- a/rnfs/dht/routing.py +++ b/rnfs/dht/routing.py @@ -200,6 +200,13 @@ def node_count(self) -> int: with self._lock: return sum(len(bucket) for bucket in self._buckets) + def live_count(self) -> int: + """Number of servers in the table that /find replies would include: none failed a query since last + heard, or has been silent over 2 × announce_interval.""" + with self._lock: + now = self.clock() + return sum(1 for bucket in self._buckets for e in bucket.values() if not self._stale(e, now)) + def _find(self, node_id: bytes) -> _Entry | None: """The entry for node_id, or None (also for malformed IDs and our own). Caller holds the lock.""" if not isinstance(node_id, bytes) or len(node_id) != ID_LENGTH or node_id == self.own_id: diff --git a/rnfs/node.py b/rnfs/node.py index 1dfc922..6b5988c 100644 --- a/rnfs/node.py +++ b/rnfs/node.py @@ -4,13 +4,15 @@ Startup: Reticulum from (attaching to a shared instance if its config shares one), the node's identity and its rnfs.dht destination, then the routing table, provider records, storage, shared folder, fetcher, publisher and names; the request server answers through NodeBackend. Background threads: the -announcer, the join (bootstrap lookup for our own ID), the publish loop, the name re-announce loop and +announcer, the join (bootstrap nodes, then a lookup for our own ID, repeated while too few servers are +known), the publish loop, the name re-announce loop and maintenance (shared-folder scans, provider record expiry and saves, max_storage enforcement). Threading: RNS calls the announce and request handlers on its own threads; facade calls come from API and gateway threads. Every component locks internally; the node itself only guards the small sets it owns. """ import os +import random import secrets import shutil import stat @@ -23,7 +25,7 @@ from rnfs import __version__, features, fsutil from rnfs.config import Config -from rnfs.constants import APP_NAME, DHT_ASPECT, MODE_SERVER, PARTIAL_SUFFIX +from rnfs.constants import APP_NAME, DEFAULT_BOOTSTRAP_NODES, DHT_ASPECT, MODE_SERVER, PARTIAL_SUFFIX from rnfs.content import archive, builder, dag, edit, walker from rnfs.content.cid import Cid from rnfs import node_content # --- content: add --no-pin/--to/--chunker, sealed files, gateway formats --- @@ -56,6 +58,9 @@ GATEWAY_NAME_AGE = 60.0 # a gateway resolution reuses a record heard this recently STOP_TIMEOUT = 5.0 REMOTE_PIN_QUEUE = 64 # remote pins one source may have waiting or being fetched; more are refused +BOOTSTRAP_ANSWERS = 2 # a join stops contacting bootstrap nodes once this many have answered +MIN_SERVERS = 3 # while fewer live servers are known, the join runs again +REJOIN_DELAY = 300.0 # seconds before joining again; doubles after each join, up to announce_interval KIND_NAMES = {dag.KIND_FILE: "file", dag.KIND_EXECUTABLE: "executable", dag.KIND_DIRECTORY: "directory"} @@ -259,6 +264,12 @@ def _setup(self, config: Config, node_id: bytes, client, slow: bool, names_netwo self.slow = slow self._is_known = is_known self.members = {h.strip().strip("<>").lower() for h in config.members} + self._bootstrap_nodes: list[bytes] = [] # bootstrap_nodes that parse, in the order given + for text in config.bootstrap_nodes: + try: + self._bootstrap_nodes.append(parse_node_id(text)) + except ValueError as e: + log(f"ignoring bootstrap node: {e}", LOG_WARNING) _make_private_dirs(config) self.routing = RoutingTable(node_id, config, admit=self._is_member_node if config.private else None) @@ -410,28 +421,82 @@ def _seed_routing(self): log(f"{plural(added, 'DHT server')} known from announces heard before", LOG_VERBOSE) def _join(self): - """Query the bootstrap nodes for our own ID and continue that lookup, filling the routing table.""" + """Join at startup, then again whenever fewer than MIN_SERVERS live servers are known (section 6). + + A join that leaves too few servers is retried after REJOIN_DELAY, doubling up to announce_interval: + on radio, every path request for a bootstrap node is flooded. + """ try: if self.reticulum is not None: self._seed_routing() - bootstrap = [] - for text in self.config.bootstrap_nodes: - try: - bootstrap.append(parse_node_id(text)) - except ValueError as e: - log(f"ignoring bootstrap node: {e}", LOG_WARNING) - if bootstrap or self.routing.node_count(): - lookup = Lookup(self.node_id, bootstrap + self.routing.closest(self.node_id), self.client.find, - self.node_id, alpha=self.config.effective_alpha(self.slow), beta=self.config.beta, - timeout=self.config.lookup_timeout, is_known=self._is_known, - on_answer=self._answered, on_failure=self.routing.failed, - exclude=self.routing.is_banned) - lookup.find_closest() - log(f"joined the DHT: {plural(self.routing.node_count(), 'server')} known", LOG_NOTICE) + self.join() except Exception as e: log(f"joining the DHT failed: {e!r}", LOG_ERROR) finally: self.joined.set() + delay = REJOIN_DELAY + while not self.stop_event.wait(delay): + if self.routing.live_count() >= MIN_SERVERS: + delay = REJOIN_DELAY + continue + try: + self.join() + except Exception as e: + log(f"joining the DHT failed: {e!r}", LOG_ERROR) + delay = min(2 * delay, self.config.announce_interval) + + def join(self): + """Contact bootstrap nodes, then look up our own ID from what they and the routing table know.""" + contacted, start = self._contact_bootstrap() + start = start + self.routing.closest(self.node_id) + if not start: + log("no DHT servers known yet: waiting for announces", LOG_VERBOSE) + return + # The bootstrap nodes contacted already answered (or failed) a /find for this key: not asked again + lookup = Lookup(self.node_id, start, self.client.find, self.node_id, + alpha=self.config.effective_alpha(self.slow), beta=self.config.beta, + timeout=self.config.lookup_timeout, is_known=self._is_known, + on_answer=self._answered, on_failure=self.routing.failed, + exclude=lambda node_id: node_id in contacted or self.routing.is_banned(node_id)) + lookup.find_closest() + log(f"joined the DHT: {plural(self.routing.node_count(), 'server')} known", LOG_NOTICE) + + def bootstrap_ids(self) -> list[bytes]: + """Bootstrap nodes in the order a join contacts them: bootstrap_nodes as given, then (unless + default_bootstrap is off or in private mode) DEFAULT_BOOTSTRAP_NODES in a fresh random order.""" + defaults = [] + if self.config.default_bootstrap and not self.config.private: + defaults = [bytes.fromhex(text) for text in DEFAULT_BOOTSTRAP_NODES] + random.shuffle(defaults) + ids = list(dict.fromkeys(self._bootstrap_nodes + defaults)) + return [node_id for node_id in ids if node_id != self.node_id and not self.routing.is_banned(node_id)] + + def _contact_bootstrap(self) -> tuple[set[bytes], list[bytes]]: + """Ask bootstrap nodes for our own ID, one at a time, until BOOTSTRAP_ANSWERS have answered. + + Each costs a path request if its identity isn't known, outside any lookup's budget for unknown IDs, + so an unreachable one doesn't stop the rest being tried. Returns the nodes contacted and the + servers their answers listed. + """ + contacted: set[bytes] = set() + found: list[bytes] = [] + answered = 0 + for node_id in self.bootstrap_ids(): + if answered >= BOOTSTRAP_ANSWERS or self.stop_event.is_set(): + break + contacted.add(node_id) + try: + reply = self.client.find(node_id, self.node_id) + except Exception: + reply = None + if reply is None: + log(f"bootstrap node {node_name(node_id)} didn't answer", LOG_VERBOSE) + self.routing.failed(node_id) + continue + answered += 1 + self._answered(node_id) + found.extend(reply[1]) + return contacted, found def _run_publisher(self): """The publish loop, started once the join has filled the routing table.""" diff --git a/roadmap.md b/roadmap.md index 4c128da..1f7f8a5 100644 --- a/roadmap.md +++ b/roadmap.md @@ -1,25 +1,43 @@ # 0.1.3 +## Default Bootstrap Node List +DEFAULT_BOOTSTRAP_NODES global variable: node IDs of official, always-on DHT servers, giving new nodes a first contact when no announces reach them (e.g. behind a LoRa access point) +* Used in the startup join alongside `bootstrap_nodes` (the user's own are tried first), as IPFS does +* Used again, with backoff, whenever fewer than about 3 servers are known, so isolated nodes and small islands rejoin +* Tried in random order, with their own path-request budget, so one offline node doesn't block the rest +* Skipped in private mode, or when a setting turns them off +* After the join they are ordinary DHT servers, with no extra trust + +# 0.1.4 + ## CID denylist - Ability to point to a CID or name as a CID deny list +- Refuse to fetch, provide or store a CID. + +- Deny lists can contain file hashes and/or CIDs + ## Migrate to JSON stores to sqlite3 -## Improved bootstraping without bootstrap node +- If JSON store exists, convert to the sqlite3 stor -Add uptime & number of known nodes to startup announce +## Alternate Pin Sources +* List if a file was pinned by an application via an API -If an announce is heard from a new node, advertise self to the new node after a random delay if the following conditions are met: +## Change notifications for other applications to subscribe too +* New file pinned by daemon -* New node's number of known nodes is zero -* This node is the closest node in the DHT to the new node that it knows of -* This node's uptime is above one hour or -This change should not break compatibility with previous versions. Announces without this information should still be recognised as normal +# 0.1.5 -# 0.1.4 +## Client Library + +## API versioning and reference + +## Per-app API tokens -## Add files without duplicating them with --nocopy flag +## RNS API -## RNFS content as a folder (linux only) \ No newline at end of file +* Destination.hash(identity, "rnfs", "api") +* Whitelist and/or API key for auth \ No newline at end of file diff --git a/specification.md b/specification.md index 4483e2e..1d4fb75 100644 --- a/specification.md +++ b/specification.md @@ -385,9 +385,21 @@ big-endian integer; "closest" means smallest distance. - A node is dropped after `max_failures` queries in a row go unanswered; hearing from it resets the count. Lossy links can drop a live server this way, so a dropped node's first-seen time is kept for `2 × announce_interval`, and it regains its seniority if it comes back within that time. -- **Joining.** At startup a node queries any bootstrap node IDs it was given (`daemon --bootstrap`) with - `/find` for its own node ID, adds those that answer, and continues that lookup, which fills its table with - its neighbours. +- **Joining.** At startup a node sends `/find` for its own node ID to bootstrap nodes, one at a time, until + two have answered: first those it was given (`bootstrap_nodes`, `daemon --bootstrap`) in their order, + then, unless `default_bootstrap` is off or it is in private mode, the implementation's default bootstrap + nodes (rnfs: `DEFAULT_BOOTSTRAP_NODES`) in random order. It adds those that answer, then runs a lookup for + its own ID from the servers they listed and its routing table, without asking the contacted nodes again; + this fills its table with its neighbours. + - Bootstrap nodes are contacted outside the lookup's budget of `alpha` unknown IDs, so unreachable ones + don't stop the rest being tried. + - A node behind an RNS access-point interface hears no announces from beyond it, so bootstrap nodes are + its only way in. + - While fewer than 3 servers are known that `/find` replies would include (not failed since last heard, + nor silent for over `2 × announce_interval`), the node joins again, 5 minutes after the last join and + doubling up to `announce_interval`: on radio every path request is flooded. A node that knows only a + small island of servers still contacts the defaults at every start. + - Bootstrap nodes get no special trust: once answered, they are ordinary routing-table entries. - Because RNS floods announces, on small meshes (a few dozen nodes) the routing table holds most of the network, so lookups usually finish in one or two rounds. @@ -997,7 +1009,8 @@ digits). A node MUST NOT start with an invalid setting. | `gateway_scripts` | on | | | `gateway_fetches` | 2 | Network fetches at a time for requests that aren't the user's own navigation | | `api_port`, `gateway_port` | 8788, 8789 | Local API and gateway ports, on 127.0.0.1 (section 11) | -| `bootstrap_nodes` | empty | Node IDs queried when joining (section 6) | +| `bootstrap_nodes` | empty | Node IDs contacted first when joining (section 6) | +| `default_bootstrap` | on | Also contact the default bootstrap nodes when joining; never in private mode (section 6) | | `reticulum_dir` | RNS's own | RNS config directory, the one `rnsd` uses by default | | `verbosity` | 0 | Extra logging: 1 per-query detail, 2 RNS debug output | | `upload_budget` | none (unlimited) | Bytes per hour of `/blocks` responses, node-wide; a token bucket, at most half of it per requester in flight, unsent parts refunded (section 3) | @@ -1136,7 +1149,7 @@ unreserved, pct-encoded: as in RFC 3986 | Noise / TLS, peer authentication | RNS Links (encrypted, forward secret); `Link.identify()` | | Protocol negotiation, protobuf RPC | RNS link requests and msgpack; versions are destination aspects | | Bitswap | `/blocks` over RNS Resources | -| Bootstrap and peer discovery | RNS announces, plus optional bootstrap node IDs (`daemon --bootstrap`) | +| Bootstrap and peer discovery | RNS announces, plus bootstrap node IDs: built-in defaults, as in IPFS, and your own (`daemon --bootstrap`) | | IPNS records, DHT PUT/GET | RNS announces with signed `app_data`, cached by transport nodes and served by keepers (`/name`) | | Swarm key (private networks) | RNS Interface Access Codes; rnfs private mode | | Resource manager limits | RNS request and response size limits, plus rnfs's link and request caps | diff --git a/tests/test_config.py b/tests/test_config.py index fca4eba..6c547e0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -19,7 +19,7 @@ def test_defaults_are_valid(self): def test_flags_must_be_booleans(self): # "false" and "no" are non-empty strings, which would read as true: scripts on, private mode on - for name in ("gateway_scripts", "private"): + for name in ("gateway_scripts", "private", "default_bootstrap"): for value in ("false", "no", 0, 1, None): self.assert_invalid(**{name: value}) Config(**{name: False}).validate() diff --git a/tests/test_e2e.py b/tests/test_e2e.py index a141259..c5847f2 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -57,6 +57,7 @@ # Short intervals so the test runs in seconds; republish_interval must stay below PROVIDER_TTL / 2 SETTINGS = """ +default_bootstrap = false announce_interval = 3600 startup_announce_delay = 2 scan_interval = 2 diff --git a/tests/test_node.py b/tests/test_node.py index 0a69e29..4166102 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -12,16 +12,17 @@ import RNS from rnfs.config import Config -from rnfs.constants import CHUNK_SIZE +from rnfs.constants import CHUNK_SIZE, DEFAULT_BOOTSTRAP_NODES from rnfs.content.cid import RAW, Cid from rnfs.content.storage import remote_source from rnfs import fetcher from rnfs.errors import InvalidDag, NotFound, RnfsError, StorageFull, TooLarge from rnfs.features.keepers import name_key from rnfs.network.client import OK, BlocksResult +from rnfs.network.identity import parse_node_id from rnfs import fsutil, node as node_module from rnfs.node import RnfsNode -from tests.support import POSIX_MODES +from tests.support import POSIX_MODES, wait_for from tests.test_names import FakeNetwork A_ID, B_ID, N1 = bytes([0xA] * 16), bytes([0xB] * 16), bytes([1] * 16) @@ -629,5 +630,77 @@ def cached_targets(): self.assertEqual(held, [True]) +class JoinTest(NodeTest): + """Joining: bootstrap nodes first (the user's, then the defaults), then a lookup; again while too few.""" + + SERVERS = [bytes([0x10 + i] * 16) for i in range(4)] + DEAD = [bytes([0xD0 + i] * 16) for i in range(2)] # node IDs nobody answers for + X_ID = bytes([0xE] * 16) + + def setUp(self): + """Four servers that know each other, and a record of every /find.""" + super().setUp() + for i, server_id in enumerate(self.SERVERS): + self.make_node(f"s{i}", server_id) + for server_id in self.SERVERS: + for other in self.SERVERS: + self.net.nodes[server_id].routing.heard(other) + self.finds: list[bytes] = [] + find = self.net.find + self.net.find = lambda node_id, key: (self.finds.append(node_id), find(node_id, key))[1] + self.defaults = mock.patch.object(node_module, "DEFAULT_BOOTSTRAP_NODES", + tuple(n.hex() for n in self.SERVERS[1:])) + self.defaults.start() + self.addCleanup(self.defaults.stop) + + def joiner(self, **settings) -> RnfsNode: + """The node that joins, with an empty routing table.""" + return self.make_node("x", self.X_ID, **settings) + + def test_default_list_is_well_formed(self): + ids = [parse_node_id(text) for text in DEFAULT_BOOTSTRAP_NODES] + self.assertTrue(ids) + self.assertEqual(len(set(ids)), len(ids)) + + def test_user_bootstrap_first_then_defaults_until_two_answer(self): + x = self.joiner(bootstrap_nodes=[self.SERVERS[0].hex()]) + x.join() + self.assertEqual(self.finds[0], self.SERVERS[0]) + self.assertIn(self.finds[1], self.SERVERS[1:]) + # The lookup doesn't ask the contacted bootstrap nodes again + self.assertEqual(len(self.finds), len(set(self.finds))) + self.assertGreaterEqual(x.routing.live_count(), 3) + + def test_unreachable_bootstrap_nodes_dont_stop_the_rest(self): + # A fresh slow node knows no identities, and its lookups try only one unknown ID (alpha): bootstrap + # nodes are contacted outside that budget, so two dead ones don't use it up + x = self.joiner(bootstrap_nodes=[n.hex() for n in self.DEAD + [self.SERVERS[0]]], default_bootstrap=False) + x.slow = True + x._is_known = lambda node_id: False + x.join() + self.assertEqual(self.finds[:3], self.DEAD + [self.SERVERS[0]]) + self.assertTrue(x.routing.contains(self.SERVERS[0])) + + def test_defaults_skipped_when_turned_off_or_private(self): + user = [self.DEAD[0].hex()] + self.assertEqual(self.joiner(bootstrap_nodes=user, default_bootstrap=False).bootstrap_ids(), [self.DEAD[0]]) + private = self.make_node("p", bytes([0xF] * 16), bootstrap_nodes=user, private=True) + self.assertEqual(private.bootstrap_ids(), [self.DEAD[0]]) + self.assertEqual(sorted(self.make_node("d", bytes([0xC] * 16)).bootstrap_ids()), sorted(self.SERVERS[1:])) + + def test_joins_again_while_too_few_servers_are_known(self): + x = self.joiner(bootstrap_nodes=[self.SERVERS[0].hex()], default_bootstrap=False) + first = self.net.nodes.pop(self.SERVERS[0]) # unreachable at startup + with mock.patch.object(node_module, "REJOIN_DELAY", 0.05): + thread = threading.Thread(target=x._join, daemon=True) + thread.start() + self.addCleanup(thread.join, 5) + self.addCleanup(x.stop_event.set) + self.assertTrue(x.joined.wait(5)) + self.assertEqual(x.routing.node_count(), 0) + self.net.nodes[self.SERVERS[0]] = first + wait_for(lambda: x.routing.live_count() >= 3, what="a later join filling the routing table") + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_routing.py b/tests/test_routing.py index 49f905f..fe2d1a7 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -210,6 +210,17 @@ def test_reply_leaves_out_failed_and_silent_nodes(self): # Our own lookups still try them self.assertEqual(self.table.closest(key), [healthy, failing, silent]) + def test_live_count_leaves_out_the_same_nodes(self): + healthy, failing, silent = node_id(1), node_id(2), node_id(3) + self.table.heard(silent) + self.clock.now += 2 * ANNOUNCE + 1 + self.table.heard(healthy) + self.table.heard(failing) + self.table.failed(failing) + self.assertEqual((self.table.live_count(), self.table.node_count()), (1, 3)) + self.table.heard(failing) # hearing it again makes it live + self.assertEqual(self.table.live_count(), 2) + class BanTest(RoutingTestCase): """Bans drop a node and ignore it for ban_time.""" From c10521e72368809f3353ed856cca5a000c657037 Mon Sep 17 00:00:00 2001 From: Afi Date: Tue, 13 Oct 2026 19:57:06 +1030 Subject: [PATCH 4/6] Release v0.1.3 --- README.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f74a31b..b5212c5 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Different package options available at [latest release](https://github.com/afit2 **Windows:** -[`rnfs-0.1.2-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-windows-x64.exe), saved as `rnfs.exe` +[`rnfs-0.1.3-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-windows-x64.exe), saved as `rnfs.exe` somewhere on your PATH ([how](#windows)). **Linux:** @@ -65,7 +65,7 @@ Or installers for ([Debian & Ubuntu](#debian-and-ubuntu)). [Flatpak](#other-lin **macOS:** -`pipx install https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-py3-none-any.whl` ([details](#macos)). +`pipx install https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-py3-none-any.whl` ([details](#macos)). ### 2 - Start rnfs and leave it running in its own terminal: @@ -102,26 +102,26 @@ rnfs runs on Linux, macOS and Windows. The easiest way to install it is a file f | System | Download | Install | |---|---|---| -| Windows (64-bit) | [`rnfs-0.1.2-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-windows-x64.exe) | [Windows](#windows) | -| Debian 12+, Ubuntu 24.04+, Raspberry Pi OS (64-bit) | [`rnfs_0.1.2_amd64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs_0.1.2_amd64.deb) or [`rnfs_0.1.2_arm64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs_0.1.2_arm64.deb) | [Debian and Ubuntu](#debian-and-ubuntu) | -| Other Linux (x86_64) | [`rnfs-0.1.2-x86_64.flatpak`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-x86_64.flatpak) | [Other Linux](#other-linux-flatpak) | -| macOS, or any system with Python 3.11+ | [`rnfs-0.1.2-py3-none-any.whl`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-py3-none-any.whl) | [macOS](#macos) | +| Windows (64-bit) | [`rnfs-0.1.3-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-windows-x64.exe) | [Windows](#windows) | +| Debian 12+, Ubuntu 24.04+, Raspberry Pi OS (64-bit) | [`rnfs_0.1.3_amd64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs_0.1.3_amd64.deb) or [`rnfs_0.1.3_arm64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs_0.1.3_arm64.deb) | [Debian and Ubuntu](#debian-and-ubuntu) | +| Other Linux (x86_64) | [`rnfs-0.1.3-x86_64.flatpak`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-x86_64.flatpak) | [Other Linux](#other-linux-flatpak) | +| macOS, or any system with Python 3.11+ | [`rnfs-0.1.3-py3-none-any.whl`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-py3-none-any.whl) | [macOS](#macos) | Every package includes Reticulum and LXMF, gives you one `rnfs` command, and keeps your data in the same -places. The release also has the source archive (`rnfs-0.1.2.tar.gz`) and `SHA256SUMS`, to -[check your download](#checking-a-download). These links are for 0.1.2; for a newer version, take the +places. The release also has the source archive (`rnfs-0.1.3.tar.gz`) and `SHA256SUMS`, to +[check your download](#checking-a-download). These links are for 0.1.3; for a newer version, take the same files from the [latest release](https://github.com/afit21/rnfs/releases/latest). ### Windows -Download [`rnfs-0.1.2-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-windows-x64.exe). It is a single file that +Download [`rnfs-0.1.3-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-windows-x64.exe). It is a single file that includes Python, Reticulum and LXMF; nothing else needs installing. In PowerShell, from your download folder, put it on your PATH as `rnfs`: ```powershell $bin = "$env:USERPROFILE\.local\bin" New-Item -ItemType Directory -Force $bin | Out-Null -Move-Item .\rnfs-0.1.2-windows-x64.exe "$bin\rnfs.exe" -Force +Move-Item .\rnfs-0.1.3-windows-x64.exe "$bin\rnfs.exe" -Force Unblock-File "$bin\rnfs.exe" [Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$bin", "User") # open a new terminal, then: @@ -152,8 +152,8 @@ or newer. Ubuntu 22.04 ships Python 3.10, so it isn't supported; use the [instal there instead. Pick `amd64` or `arm64` to match `dpkg --print-architecture`: ```sh -curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs_0.1.2_amd64.deb -sudo apt install ./rnfs_0.1.2_amd64.deb +curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs_0.1.3_amd64.deb +sudo apt install ./rnfs_0.1.3_amd64.deb rnfs daemon # or run it in the background: systemctl --user enable --now rnfs # a systemd user service; logs: journalctl --user -u rnfs -f ``` @@ -172,9 +172,9 @@ For x86_64 Linux distributions without `.deb` support. The Flatpak uses the Flat `org.freedesktop.Platform` runtime: ```sh -curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-x86_64.flatpak +curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-x86_64.flatpak flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo -flatpak install --user rnfs-0.1.2-x86_64.flatpak +flatpak install --user rnfs-0.1.3-x86_64.flatpak flatpak run io.github.afit21.rnfs daemon alias rnfs='flatpak run io.github.afit21.rnfs' # add to ~/.bashrc to type just "rnfs" ``` @@ -202,7 +202,7 @@ and puts the `rnfs` command on your PATH. Homebrew also installs a recent Python ```sh brew install pipx pipx ensurepath # then open a new terminal -pipx install https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-py3-none-any.whl # includes Reticulum and LXMF +pipx install https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-py3-none-any.whl # includes Reticulum and LXMF rnfs daemon ``` @@ -228,11 +228,11 @@ Each release's `SHA256SUMS` lists the checksum of every file. Download it next t ```sh sha256sum -c SHA256SUMS --ignore-missing # Linux -shasum -a 256 rnfs-0.1.2-py3-none-any.whl # macOS: compare with the line in SHA256SUMS +shasum -a 256 rnfs-0.1.3-py3-none-any.whl # macOS: compare with the line in SHA256SUMS ``` ```powershell -Get-FileHash .\rnfs-0.1.2-windows-x64.exe # Windows: compare with the line in SHA256SUMS +Get-FileHash .\rnfs-0.1.3-windows-x64.exe # Windows: compare with the line in SHA256SUMS ``` ### Other ways to install From 19f6d22883182659bae68912c5bcb85e984ca9f8 Mon Sep 17 00:00:00 2001 From: Afi Date: Tue, 13 Oct 2026 20:22:36 +1030 Subject: [PATCH 5/6] Join through built-in bootstrap nodes, and rejoin while too few servers are known --- "e v0.1.3\"" | 103 ++++++++++++++++++++++++++++++++++++++++++++++ rnfs/constants.py | 1 + 2 files changed, 104 insertions(+) create mode 100644 "e v0.1.3\"" diff --git "a/e v0.1.3\"" "b/e v0.1.3\"" new file mode 100644 index 0000000..102ebfe --- /dev/null +++ "b/e v0.1.3\"" @@ -0,0 +1,103 @@ +diff --git a/README.md b/README.md +index f74a31b..b5212c5 100644 +--- a/README.md ++++ b/README.md +@@ -52,7 +52,7 @@ Different package options available at [latest release](https://github.com/afit2 +  + **Windows:** +  +-[`rnfs-0.1.2-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-windows-x64.exe), saved as `rnfs.exe` ++[`rnfs-0.1.3-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-windows-x64.exe), saved as `rnfs.exe` + somewhere on your PATH ([how](#windows)). +  + **Linux:** +@@ -65,7 +65,7 @@ Or installers for ([Debian & Ubuntu](#debian-and-ubuntu)). [Flatpak](#other-lin +  + **macOS:** +  +-`pipx install https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-py3-none-any.whl` ([details](#macos)). ++`pipx install https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-py3-none-any.whl` ([details](#macos)). +  + ### 2 - Start rnfs and leave it running in its own terminal: +  +@@ -102,26 +102,26 @@ rnfs runs on Linux, macOS and Windows. The easiest way to install it is a file f +  + | System | Download | Install | + |---|---|---| +-| Windows (64-bit) | [`rnfs-0.1.2-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-windows-x64.exe) | [Windows](#windows) | +-| Debian 12+, Ubuntu 24.04+, Raspberry Pi OS (64-bit) | [`rnfs_0.1.2_amd64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs_0.1.2_amd64.deb) or [`rnfs_0.1.2_arm64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs_0.1.2_arm64.deb) | [Debian and Ubuntu](#debian-and-ubuntu) | +-| Other Linux (x86_64) | [`rnfs-0.1.2-x86_64.flatpak`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-x86_64.flatpak) | [Other Linux](#other-linux-flatpak) | +-| macOS, or any system with Python 3.11+ | [`rnfs-0.1.2-py3-none-any.whl`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-py3-none-any.whl) | [macOS](#macos) | ++| Windows (64-bit) | [`rnfs-0.1.3-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-windows-x64.exe) | [Windows](#windows) | ++| Debian 12+, Ubuntu 24.04+, Raspberry Pi OS (64-bit) | [`rnfs_0.1.3_amd64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs_0.1.3_amd64.deb) or [`rnfs_0.1.3_arm64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs_0.1.3_arm64.deb) | [Debian and Ubuntu](#debian-and-ubuntu) | ++| Other Linux (x86_64) | [`rnfs-0.1.3-x86_64.flatpak`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-x86_64.flatpak) | [Other Linux](#other-linux-flatpak) | ++| macOS, or any system with Python 3.11+ | [`rnfs-0.1.3-py3-none-any.whl`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-py3-none-any.whl) | [macOS](#macos) | +  + Every package includes Reticulum and LXMF, gives you one `rnfs` command, and keeps your data in the same +-places. The release also has the source archive (`rnfs-0.1.2.tar.gz`) and `SHA256SUMS`, to +-[check your download](#checking-a-download). These links are for 0.1.2; for a newer version, take the ++places. The release also has the source archive (`rnfs-0.1.3.tar.gz`) and `SHA256SUMS`, to ++[check your download](#checking-a-download). These links are for 0.1.3; for a newer version, take the + same files from the [latest release](https://github.com/afit21/rnfs/releases/latest). +  + ### Windows +  +-Download [`rnfs-0.1.2-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-windows-x64.exe). It is a single file that ++Download [`rnfs-0.1.3-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-windows-x64.exe). It is a single file that + includes Python, Reticulum and LXMF; nothing else needs installing. In PowerShell, from your download + folder, put it on your PATH as `rnfs`: +  + ```powershell + $bin = "$env:USERPROFILE\.local\bin" + New-Item -ItemType Directory -Force $bin | Out-Null +-Move-Item .\rnfs-0.1.2-windows-x64.exe "$bin\rnfs.exe" -Force ++Move-Item .\rnfs-0.1.3-windows-x64.exe "$bin\rnfs.exe" -Force + Unblock-File "$bin\rnfs.exe" + [Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$bin", "User") + # open a new terminal, then: +@@ -152,8 +152,8 @@ or newer. Ubuntu 22.04 ships Python 3.10, so it isn't supported; use the [instal + there instead. Pick `amd64` or `arm64` to match `dpkg --print-architecture`: +  + ```sh +-curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs_0.1.2_amd64.deb +-sudo apt install ./rnfs_0.1.2_amd64.deb ++curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs_0.1.3_amd64.deb ++sudo apt install ./rnfs_0.1.3_amd64.deb + rnfs daemon # or run it in the background: + systemctl --user enable --now rnfs # a systemd user service; logs: journalctl --user -u rnfs -f + ``` +@@ -172,9 +172,9 @@ For x86_64 Linux distributions without `.deb` support. The Flatpak uses the Flat + `org.freedesktop.Platform` runtime: +  + ```sh +-curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-x86_64.flatpak ++curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-x86_64.flatpak + flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo +-flatpak install --user rnfs-0.1.2-x86_64.flatpak ++flatpak install --user rnfs-0.1.3-x86_64.flatpak + flatpak run io.github.afit21.rnfs daemon + alias rnfs='flatpak run io.github.afit21.rnfs' # add to ~/.bashrc to type just "rnfs" + ``` +@@ -202,7 +202,7 @@ and puts the `rnfs` command on your PATH. Homebrew also installs a recent Python + ```sh + brew install pipx + pipx ensurepath # then open a new terminal +-pipx install https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-py3-none-any.whl # includes Reticulum and LXMF ++pipx install https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-py3-none-any.whl # includes Reticulum and LXMF + rnfs daemon + ``` +  +@@ -228,11 +228,11 @@ Each release's `SHA256SUMS` lists the checksum of every file. Download it next t +  + ```sh + sha256sum -c SHA256SUMS --ignore-missing # Linux +-shasum -a 256 rnfs-0.1.2-py3-none-any.whl # macOS: compare with the line in SHA256SUMS ++shasum -a 256 rnfs-0.1.3-py3-none-any.whl # macOS: compare with the line in SHA256SUMS + ``` +  + ```powershell +-Get-FileHash .\rnfs-0.1.2-windows-x64.exe # Windows: compare with the line in SHA256SUMS ++Get-FileHash .\rnfs-0.1.3-windows-x64.exe # Windows: compare with the line in SHA256SUMS + ``` +  + ### Other ways to install diff --git a/rnfs/constants.py b/rnfs/constants.py index f6eefb8..1ac122c 100644 --- a/rnfs/constants.py +++ b/rnfs/constants.py @@ -52,4 +52,5 @@ "5e395b3045b68c881d5db62c146033eb", # Sydney, Australia (AWS) "bddad1ec2f4a4a6112d3d42afabfd94a", # Broken Hill, Australia "24770a7a9221b97d583905a839bc4fce", # Central US (Google Cloud) + "4a8dec339ac23f9a6f423b6ee0a84c16", # Midrid (Google Cloud) ) \ No newline at end of file From 916560cb7a3517a55e94384de536050cd24ed84a Mon Sep 17 00:00:00 2001 From: Afi Date: Tue, 13 Oct 2026 20:26:20 +1030 Subject: [PATCH 6/6] Set the version to 0.1.3 and remove a stray diff file --- CHANGELOG.md | 2 +- "e v0.1.3\"" | 103 ---------------------------------------------- rnfs/__init__.py | 2 +- rnfs/constants.py | 4 +- 4 files changed, 4 insertions(+), 107 deletions(-) delete mode 100644 "e v0.1.3\"" diff --git a/CHANGELOG.md b/CHANGELOG.md index 423ef9d..aa9f83a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ [specification.md](specification.md) is the authority on behaviour. -## Unreleased +## 0.1.3 Joining the DHT: - Built-in bootstrap nodes (`DEFAULT_BOOTSTRAP_NODES`): every node contacts them when joining, after its own diff --git "a/e v0.1.3\"" "b/e v0.1.3\"" deleted file mode 100644 index 102ebfe..0000000 --- "a/e v0.1.3\"" +++ /dev/null @@ -1,103 +0,0 @@ -diff --git a/README.md b/README.md -index f74a31b..b5212c5 100644 ---- a/README.md -+++ b/README.md -@@ -52,7 +52,7 @@ Different package options available at [latest release](https://github.com/afit2 -  - **Windows:** -  --[`rnfs-0.1.2-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-windows-x64.exe), saved as `rnfs.exe` -+[`rnfs-0.1.3-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-windows-x64.exe), saved as `rnfs.exe` - somewhere on your PATH ([how](#windows)). -  - **Linux:** -@@ -65,7 +65,7 @@ Or installers for ([Debian & Ubuntu](#debian-and-ubuntu)). [Flatpak](#other-lin -  - **macOS:** -  --`pipx install https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-py3-none-any.whl` ([details](#macos)). -+`pipx install https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-py3-none-any.whl` ([details](#macos)). -  - ### 2 - Start rnfs and leave it running in its own terminal: -  -@@ -102,26 +102,26 @@ rnfs runs on Linux, macOS and Windows. The easiest way to install it is a file f -  - | System | Download | Install | - |---|---|---| --| Windows (64-bit) | [`rnfs-0.1.2-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-windows-x64.exe) | [Windows](#windows) | --| Debian 12+, Ubuntu 24.04+, Raspberry Pi OS (64-bit) | [`rnfs_0.1.2_amd64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs_0.1.2_amd64.deb) or [`rnfs_0.1.2_arm64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs_0.1.2_arm64.deb) | [Debian and Ubuntu](#debian-and-ubuntu) | --| Other Linux (x86_64) | [`rnfs-0.1.2-x86_64.flatpak`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-x86_64.flatpak) | [Other Linux](#other-linux-flatpak) | --| macOS, or any system with Python 3.11+ | [`rnfs-0.1.2-py3-none-any.whl`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-py3-none-any.whl) | [macOS](#macos) | -+| Windows (64-bit) | [`rnfs-0.1.3-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-windows-x64.exe) | [Windows](#windows) | -+| Debian 12+, Ubuntu 24.04+, Raspberry Pi OS (64-bit) | [`rnfs_0.1.3_amd64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs_0.1.3_amd64.deb) or [`rnfs_0.1.3_arm64.deb`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs_0.1.3_arm64.deb) | [Debian and Ubuntu](#debian-and-ubuntu) | -+| Other Linux (x86_64) | [`rnfs-0.1.3-x86_64.flatpak`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-x86_64.flatpak) | [Other Linux](#other-linux-flatpak) | -+| macOS, or any system with Python 3.11+ | [`rnfs-0.1.3-py3-none-any.whl`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-py3-none-any.whl) | [macOS](#macos) | -  - Every package includes Reticulum and LXMF, gives you one `rnfs` command, and keeps your data in the same --places. The release also has the source archive (`rnfs-0.1.2.tar.gz`) and `SHA256SUMS`, to --[check your download](#checking-a-download). These links are for 0.1.2; for a newer version, take the -+places. The release also has the source archive (`rnfs-0.1.3.tar.gz`) and `SHA256SUMS`, to -+[check your download](#checking-a-download). These links are for 0.1.3; for a newer version, take the - same files from the [latest release](https://github.com/afit21/rnfs/releases/latest). -  - ### Windows -  --Download [`rnfs-0.1.2-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-windows-x64.exe). It is a single file that -+Download [`rnfs-0.1.3-windows-x64.exe`](https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-windows-x64.exe). It is a single file that - includes Python, Reticulum and LXMF; nothing else needs installing. In PowerShell, from your download - folder, put it on your PATH as `rnfs`: -  - ```powershell - $bin = "$env:USERPROFILE\.local\bin" - New-Item -ItemType Directory -Force $bin | Out-Null --Move-Item .\rnfs-0.1.2-windows-x64.exe "$bin\rnfs.exe" -Force -+Move-Item .\rnfs-0.1.3-windows-x64.exe "$bin\rnfs.exe" -Force - Unblock-File "$bin\rnfs.exe" - [Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$bin", "User") - # open a new terminal, then: -@@ -152,8 +152,8 @@ or newer. Ubuntu 22.04 ships Python 3.10, so it isn't supported; use the [instal - there instead. Pick `amd64` or `arm64` to match `dpkg --print-architecture`: -  - ```sh --curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs_0.1.2_amd64.deb --sudo apt install ./rnfs_0.1.2_amd64.deb -+curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs_0.1.3_amd64.deb -+sudo apt install ./rnfs_0.1.3_amd64.deb - rnfs daemon # or run it in the background: - systemctl --user enable --now rnfs # a systemd user service; logs: journalctl --user -u rnfs -f - ``` -@@ -172,9 +172,9 @@ For x86_64 Linux distributions without `.deb` support. The Flatpak uses the Flat - `org.freedesktop.Platform` runtime: -  - ```sh --curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-x86_64.flatpak -+curl -LO https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-x86_64.flatpak - flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo --flatpak install --user rnfs-0.1.2-x86_64.flatpak -+flatpak install --user rnfs-0.1.3-x86_64.flatpak - flatpak run io.github.afit21.rnfs daemon - alias rnfs='flatpak run io.github.afit21.rnfs' # add to ~/.bashrc to type just "rnfs" - ``` -@@ -202,7 +202,7 @@ and puts the `rnfs` command on your PATH. Homebrew also installs a recent Python - ```sh - brew install pipx - pipx ensurepath # then open a new terminal --pipx install https://github.com/afit21/rnfs/releases/download/v0.1.2/rnfs-0.1.2-py3-none-any.whl # includes Reticulum and LXMF -+pipx install https://github.com/afit21/rnfs/releases/download/v0.1.3/rnfs-0.1.3-py3-none-any.whl # includes Reticulum and LXMF - rnfs daemon - ``` -  -@@ -228,11 +228,11 @@ Each release's `SHA256SUMS` lists the checksum of every file. Download it next t -  - ```sh - sha256sum -c SHA256SUMS --ignore-missing # Linux --shasum -a 256 rnfs-0.1.2-py3-none-any.whl # macOS: compare with the line in SHA256SUMS -+shasum -a 256 rnfs-0.1.3-py3-none-any.whl # macOS: compare with the line in SHA256SUMS - ``` -  - ```powershell --Get-FileHash .\rnfs-0.1.2-windows-x64.exe # Windows: compare with the line in SHA256SUMS -+Get-FileHash .\rnfs-0.1.3-windows-x64.exe # Windows: compare with the line in SHA256SUMS - ``` -  - ### Other ways to install diff --git a/rnfs/__init__.py b/rnfs/__init__.py index 9173832..71048a0 100644 --- a/rnfs/__init__.py +++ b/rnfs/__init__.py @@ -11,4 +11,4 @@ cli.py command line: run a daemon, or talk to one through its API """ -__version__ = "0.1.2" +__version__ = "0.1.3" diff --git a/rnfs/constants.py b/rnfs/constants.py index 1ac122c..1f44d01 100644 --- a/rnfs/constants.py +++ b/rnfs/constants.py @@ -52,5 +52,5 @@ "5e395b3045b68c881d5db62c146033eb", # Sydney, Australia (AWS) "bddad1ec2f4a4a6112d3d42afabfd94a", # Broken Hill, Australia "24770a7a9221b97d583905a839bc4fce", # Central US (Google Cloud) - "4a8dec339ac23f9a6f423b6ee0a84c16", # Midrid (Google Cloud) -) \ No newline at end of file + "4a8dec339ac23f9a6f423b6ee0a84c16", # Madrid, Spain (Google Cloud) +)