875 Commits
Author SHA1 Message Date
Daniel Norman 81d79077d0 protocol: Do not panic on trivially empty duration
Any connected peer can send a `Subscribe` with `since` after `until`.
The service passes the range to `Store::filtered`, which asserts
`from <= to`. The panic stops the service thread.

Remove the assertion. The query already returns no rows when
`from > to`, so the peer gets no history.
2026-10-15 00:24:28 +02:00
Daniel NormanandLorenz Leutgeb a4aae610f4 protocol: Allow storing announcements with time 0
There is no apparent reason why announcements with a timestamp of zero
cannot be stored. Avoid panicking.

Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
2026-10-15 00:16:29 +02:00
Daniel Norman 3032ba1298 radicle/patch: Use cref rule quorum for merges
To determine whether a patch has been merged to the default branch, we
tally merges based on the default branch quorum: allow set and
threshold. With the introduction of custom branches, the tallying was
not updated to use the quorum rules for the target branch, instead
merging on the identity document threshold and delegate set.

This fix uses the `allow` set and the `threshold` of the most specific
rule that matches the target branch of patch.

Note that a reference that no rule matches never becomes canonical, so a
patch that targets one can never reach a quorum.
2026-10-13 14:39:23 +02:00
Lorenz Leutgeb ada90d12ae node: Fix tests for inventory pruning 2026-10-13 14:39:23 +02:00
Daniel NormanandLorenz Leutgeb 8820be95a4 radicle, node: Enable std feature of indexmap
The crates `radicle` and `radicle-node` need the default hasher for
`IndexMap<K, V>` and `IndexSet<T>`, which currently gets enabled
indirectly by `serde_json`, which depends on `indexmap` with `std`
enabled by default.

Make the dependency on the `std` feature of `indexmap` explicit.

Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
2026-10-13 12:43:45 +02:00
Daniel Norman ceb74c9971 cli: Test explorer URL for annotated tag push
- Push an annotated tag and expect the tree URL of the tagged commit
- Currently fails: the URL uses the tag object id
2026-10-08 12:43:42 +02:00
Daniel Norman 8501e56345 remote-helper: Peel pushed tags for explorer URL
- Use the tagged commit for the tree URL, not the tag object
- Print no URL if the ref does not point to a commit
2026-10-08 12:43:42 +02:00
Lorenz Leutgeb c4cc462483 Revert "remote-helper: Compute HEAD for user namespaces"
This reverts commit 2747c52beb.

I forgot that this is only supported from Git 2.48 onwards.
2026-10-08 12:34:56 +02:00
Yorgos Saslis 736d21f606 node/db: Index routing by node
`sync_routing` calls `get_inventory(&from)` once per inbound inventory
announcement, to work out which of a peer's previously-announced repos are
no longer in its inventory:

    // crates/radicle-protocol/src/service.rs
    synced.removed.extend(
        self.db.routing().get_inventory(&from)?
            .into_iter()
            .filter(|rid| !included.contains(rid)),
    );

That is `SELECT repo FROM routing WHERE node = ?`. The table's only index is
the implicit one for the primary key `(repo, node)`, whose leading column is
`repo`, so the lookup cannot use it and scans instead. The index fixes that.
2026-10-08 12:21:20 +02:00
Yorgos Saslis 2f930e3561 node/reactor: Terminate transports whose socket is dead
A transport whose peer has gone away is never terminated, and the
reactor then spins on it at 100% of a core. On every iteration the reactor
[re-registers the socket with whatever interests the transport still
reports][1], `poll` returns it as ready immediately, and the I/O
handlers do nothing with it. The service thread stops serving the
control socket, so `rad node status` times out and the node looks
offline while its container is still up. Restarting a node only helps
until the next peer disconnect.

How the transport loses track of the socket:

1. A peer resets the connection between `poll` returning and the event
   being handled. The event carries no error flag, so the reactor
   [hands it to the transport instead of reaping it][2].
2. On the writable event the transport [first asks whether the session
   is established][3]. That answer is [derived from the session
   artifact][4], which for a TCP stream [comes from `getpeername`][5].
   On a reset socket that fails with ENOTCONN, so the transport takes
   the "handshake in progress" branch.
3. That branch [pokes the session with a zero-length write and
   discards the result][6]. The write [passes straight through to the
   socket][7], and a zero-length send is exactly what surfaces the
   pending ECONNRESET. The one signal that the peer is gone is thrown
   away, and the transport [re-arms its write interest][8] instead.
4. On the readable event the read also [passes straight through to
   the socket][9] and returns zero bytes, which the transport
   [ignores as well][10].

Nothing ever [marks the transport terminated][11], so its [interest
set][12] stays READABLE and WRITABLE on a closed socket forever.

On an affected node, strace shows thousands of failing `getpeername`
calls in 5 seconds, 5 per iteration, with `epoll_pwait` returning 1,200
times a second.

Two changes:

- Propagate a real error from the handshake-branch zero-length write
  as a termination instead of discarding it. WouldBlock, Interrupted
  and WriteZero stay non-fatal, matching the established branch. A
  socket whose error was already consumed elsewhere reports EPIPE
  here, so this also reaps transports that are already stuck.
- Treat a zero-byte read as end-of-file once the transport is
  [active][13]. From that point reads pass straight through to the
  socket. During the handshake the protocol layer [returns zero after
  consuming a handshake step][14], so that case keeps its current
  behaviour. Without this a half-closed peer also spins, until the
  service's [stale connection reaper][15] disconnects it about
  [2 minutes][16] later.

The termination event [reaches the wire layer][17], which [unregisters
the transport whether or not it has a peer entry for it][18]. That
covers inbound connections that die mid-handshake, which have no node
ID and so are invisible to the stale reaper.

Tests use real loopback sockets: a peer that resets (SO_LINGER 0), the
same after the pending error has already been taken, and a peer that
half-closes. All three failed before this change.

[1]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor.rs#L350
[2]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor.rs#L444
[3]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/transport.rs#L206
[4]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/session.rs#L21
[5]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/session.rs#L180
[6]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/transport.rs#L207
[7]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/session.rs#L109
[8]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/transport.rs#L208
[9]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/session.rs#L78
[10]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/transport.rs#L298
[11]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/transport.rs#L144
[12]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/transport.rs#L317
[13]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/transport.rs#L45
[14]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/reactor/session.rs#L99
[15]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-protocol/src/service.rs#L2417
[16]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-protocol/src/service.rs#L91
[17]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/wire.rs#L819
[18]: https://radicle.network/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/0e5a56f25f51d07290613a9363bd8c0c9fc8e0be/crates/radicle-node/src/wire.rs#L356
2026-10-08 12:12:15 +02:00
Yorgos Saslis 0f71581033 protocol/node: count received pack bytes as peer liveness
`disconnect_unresponsive_peers` drops any session whose `last_active` is
older than STALE_CONNECTION_TIMEOUT (hard-coded to 2 minutes).
`last_active` is written in only two places: `to_connected`, and
`handle_message` (the gossip message handler).

Pack data never reaches `handle_message` which is what marks . It arrives as `FrameData::Git`
and goes straight to the worker thread over a channel. So a peer busy
sending us a large pack appears completely idle, and
after two minutes we hang up on it mid-transfer. gitoxide then sees a
truncated stream and the fetch fails with:

    Clone of rad:<rid> from <nid> failed with
    'gix fetch error: Failed to consume the pack sent by the remote'

This only affects large repositories. Small ones finish inside the
window. Neither `limits.fetchPackReceive` nor `limits.fetchTimeout`
bounds it, and it reproduces against every peer, since any peer sending a
big pack goes gossip-quiet for the duration.

We now treat arriving pack bytes as liveness, symmetrically with the
`FrameData::Gossip` arm immediately above, which already calls
`received_message`.

Note that a remote that spends more than STALE_CONNECTION_TIMEOUT
server-side (e.g. enumerating or compressing a large pack before sending
its first byte), is still reaped, and `limits.fetchTimeout` does not help.
This is not a change in behaviour. Gossip silence would have reaped it anyway.
2026-10-08 12:09:25 +02:00
Yorgos Saslis 73ad1de2a4 node/routing: Enforce routingMaxSize
`limits.routingMaxSize` is documented as "Number of routing table entries
before we start pruning", but the implementation diverges from this.

    DELETE FROM routing WHERE node <> ?1 AND rowid IN
      (SELECT rowid FROM routing WHERE timestamp < ?2 ORDER BY timestamp LIMIT ?3)

`?2` is `now - routingMaxAge`. So if nothing is older than routingMaxAge,
nothing is deleted, however far over routingMaxSize the table has grown. On a
seed following a busy network, entries are refreshed by gossip faster than
they age out, and the cap is never reachable.

For example, on a permissive node with routingMaxSize=1000, routingMaxAge=7d,
I observed:

    routing rows                             94,254
    older than routingMaxAge, ie. prunable       16

Dropping routingMaxAge to 1 day cleared the backlog the 7-day setting had
accumulated -- from 94,254 down to 82,400 rows -- but then stopped, with
8 rows prunable and the table still ... 82x over the cap. The `routing` table
size is therefore decided by arrival rate; the configured max size has no
effect on it.

Split the two evictions so each does what its name says:

- size eviction removes the oldest entries down to `routingMaxSize`,
  regardless of age;
- age eviction then removes anything still older than
`routingMaxAge`.

Size runs first: it takes the oldest rows, so the age pass only fires when
the table was near its limit.
2026-10-08 12:06:06 +02:00
Yorgos Saslis 3129de3b95 cli/init: Warn about Vulnerability
All released versions of Radicle are affected by vulnerabilities [1] in the
network protocol: traffic is unencrypted and peers are not authenticated.
A network observer can read the contents of private repositories, and an
attacker can impersonate a node that to fetch them. No fix has been released
yet.

Since users may rely on private repositories being confidential, `rad
init` now warns about this before the interactive "Visibility" prompt,
so the warning is printed before the choice is made. The warning is printed
again in the epilogue whenever the created repository is private. This
also covers `--private`, which skips the prompt.

Update the `rad-init-private` and `rad-init-private-no-seed` examples
to match the new output.

[1]: https://radicle.dev/2026/09/23/disclosure-of-vulnerability-in-network-protocol
2026-10-05 16:36:47 +02:00
Lorenz Leutgeb 0e5a56f25f protocol: Limit allocations when decoding frames
`VarInt` can get large enough to exhaust available memory on many
machines. Instead of allocating directly, check the size of the
allocation against some limit.

The limit chosen for frames is 128 KiB, which is roughly twice as much
as actual gossip and Git protocol frames are expected to be. However,
128 KiB is arguably short enough to not burden a standard consumer
machine too much.
2026-09-10 10:22:22 +02:00
Lorenz Leutgeb d8298a1d4c treewide: Remove test_ prefix from test fns
This redundancy does not add much. These functions are already marked
via `#[test]`, and often times they are in a separate module named
"test" or "tests".
2026-09-08 14:47:53 +02:00
Lorenz Leutgeb 3fc92f83fe radicle/identity: Remove useless @ binding 2026-09-07 21:13:06 +02:00
Lorenz Leutgeb d285eccb23 fetch: Include the founder namespace always
Read the initial identity document from the blob addressed by the
repository ID and include its founder in the set of namespaces to
be fetched.

This lets clones resolve the identity COB root even if the founder
is not a delegate anymore.
2026-09-07 18:54:53 +02:00
Lorenz Leutgeb 90f448241e node/test/e2e: Clone without founder namespace
Add a test that demonstrates erroneous behavior in case the founder of a
repository is not a delegate anymore.

The change in commit 388d985578 changed
the semantics of loading the identity COB so that the founder is now
crucial.
2026-09-07 18:54:43 +02:00
Steven RooseandLorenz Leutgeb 919d5c780c crypto: Error on empty SSH_AUTH_SOCK
The empty string will never give a useful socket.

Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
2026-09-04 13:58:57 +02:00
Lorenz Leutgeb 50129a7179 radicle/canonical: Remove dangling reference
An oversight in commit `218ba30d73adbab0c2c51c6`.
2026-09-04 13:58:53 +02:00
Daniel NormanandLorenz Leutgeb 218ba30d73 radicle/canonical: Introduce RawSymbolicRefs
Serialization and deserialization of symbolic references was implemented
with assumptions on the order of pairs of key and value in JSON objects,
and further, that the enabling the `preserve_order` of `serde_json` would
always guarante that these assumptions hold. See
<https://docs.rs/crate/serde_json/1.0.149/features#preserve_order>.

This is mistaken. The identity document is serialized via `Doc::encode`
which uses `radicle::…::CanonicalFormatter`, which enforces a particular
ordering. From the point of view of the implementation of symbolic
references, this means that the ordering changes, thus violating the
ordering assumption.
The effects are that during deserialization, some symbolic references
might be considered dangling, when they are only missing other symbolic
references that were reordered to occur "later" in the JSON object.

To remedy, introduce `RawSymbolicRefs`, in analogy to `RawRules`. Just
like `RawRules` may be converted to `Rules`, `RawSymbolicRefs` may be
converted to `SymbolicRefs`. This gives the most flexibility for
handling `RawSymbolicRefs`, and one clear point at which validation
happens.

The fact that the implementation cannot depend on ordering anymore means
that all symbolic references have to be validated together, which fits
the pattern of `Raw{,*}` well.

Tests expose the issue.

Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
2026-09-03 16:47:46 +02:00
Lorenz Leutgeb 0c2e8c1511 radicle/config: Remove public_seeds
This function is unused.
2026-09-02 17:14:04 +02:00
Lorenz Leutgeb 5790e319d0 radicle/config: Do not populate preferred_seeds
Adding the "public seeds" of the network to the `preferred_seeds`
configuration on all nodes causes all nodes to connect to the preferred
seeds and to have them attempt to keep the connection open at all times.

Assuming some growth in the network, and no growth in the number of
"public seeds", this means that the number of inbound connections to the
public seeds grows as well. Once the public seeds are at capacity,
the network as a whole is in some sense bottlenecked by the capacity of
the public seeds: As the network grows further, more and more outbound
connections to the public seeds will fail.

To avoid such issues, do not populate `preferred_seeds`.
2026-09-02 17:14:04 +02:00
Lorenz Leutgeb c14346d93a radicle/canonical: Remove MatchedRule
This `struct` is unused.
2026-09-01 19:41:02 +02:00
Daniel NormanandLorenz Leutgeb 4867917f37 radicle/canonical: Remove RawRules::matches
Testing revealed that the implementation violated RIP-0006, which
requires the most specific rule matching the target of `HEAD`
to use an `allow` of `delegates` and the document threshold.
`validate_head_rule` took the first match from `RawRules`, which orders
patterns lexicographically, so a glob could be validated in place of the
rule that actually governs the branch.
The issue here is that the correct order for matching patterns is only
guaranteed on `Rules`, but not on `RawRules`.

To prevent such errors in the future, remove `RawRules::matches`. This
requires some refactoring, and makes synthesis of the default branch
rule slightly less efficient in the worst case, but we are willing to
take that hit for a cleaner implementation.

Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
2026-09-01 19:41:02 +02:00
Lorenz Leutgeb fd4e4903d7 radicle, cli: Move Git versions
Move `struct Version` from `radicle` to `radicle-cli`, where it is used.
2026-08-30 23:53:25 +02:00
Lorenz Leutgeb 05a192daff fetch: Do not pretend to be Git
There is no good reason to fake being Git.
2026-08-30 23:53:25 +02:00
Lorenz Leutgeb 2747c52beb remote-helper: Compute HEAD for user namespaces
If a user pushes to the reference of the same name as the default branch
according to the identity document, expose this as `HEAD`, too.
2026-08-30 17:37:09 +02:00
Lorenz Leutgeb fba60164f0 radicle/identity/doc: LazyLock for PayloadId
The functions `PayloadId::{project,canonical_refs}` have two
weaknesses:
 1. They always execute the checks in `from_str` for the same
    `str` over and over again.
 2. They do not return a reference, thus, callers that only
    need a reference make a copy that they then discard.

For improved performance, switch to `LazyLocks`. Keep the less efficient
functions, but deprecate them.
2026-08-30 17:37:09 +02:00
Lorenz Leutgeb 138404df6f radicle: Rely less on ….project payload
Many codepaths error if the `xyz.radicle.project` payload is not present
in the identity document. Change these to fall back gracefully.

Loading of payloads is unified in `GetPayload`.

An important detail is that `PayloadError::NotFound` is removed, and
`Option` is used instead.
2026-08-30 17:37:09 +02:00
Lorenz Leutgeb de43682f16 rust: 1.97.1 → 1.98.0
The changes to `**/args.rs` are due to better linting: Clippy found that
we may use `as_chunks`. Note that this function was stabilized in 1.88.0.
2026-08-30 14:28:36 +02:00
Lorenz Leutgeb 3054779bfa windows: Set process creation flags
When executing programs on Windows, we suppress creating a new window,
and, if the process should continue to run in the background, we
explicitly create a new process group and detach.
2026-08-25 22:58:56 +02:00
Lorenz Leutgeb 9c06c4086a cli/patch: Spawn git for diffs directly
Spawning `rad diff` works, but prints a deprecation notice, because
`rad diff` itself is deprecated. `rad patch diff` however is not
deprecated and should just take a shortcut.
2026-08-25 20:39:29 +02:00
Lorenz Leutgeb 3da781f9e1 remote-helper: Accept option "pushcert if-asked"
This fixes issue 135835b572.
2026-08-23 23:09:38 +02:00
Lorenz Leutgeb 17ff650ec1 cli/node: Label Process ID
Resolve issue bb28940307 by clarifying
that the number printed is a process identifier.
2026-08-23 22:48:34 +02:00
Lorenz Leutgeb 96d74ea9ba radicle/cob/identity: Tighten initial revision
Make `Identity::new` private, to have more control over the initial
revision of the COB. Also assert that the author of the revision is the
solve delegate of the initial revision, i.e., the "founder".
2026-08-23 20:51:13 +02:00
Lorenz Leutgeb 675b82f344 radicle: Remove ReadRepository::identity_root_of
This function is unused.
2026-08-23 20:51:13 +02:00
Lorenz Leutgeb 388d985578 radicle/storage: Secure canonical_identity_head
Do not iterate over all user namespaces to find the ID of the identity
COB, but load from the founder's namespace specifically.

If the namespace of the founder is gone, attempt to recover by
walking back from `rad/id`.
2026-08-23 20:51:13 +02:00
Lorenz Leutgeb 26708e81a8 node/test: Demonstrate injection of Identity COB
This test shows how Bob is able to change visibility of Alice's repository from
private to public, even though Bob is not a delegate.
2026-08-23 20:49:09 +02:00
Lorenz Leutgeb d66f7e598b cli, crypto: Comment keys in SSH Agent
To avoid confusion how the key ended up in SSH Agent, add a comment that
adds some explanation.

See also
<https://radicle.zulipchat.com/#narrow/channel/392584-Feedback/topic/ssh-key.20with.20comment.20in.20agent/with/618208959>.
2026-08-23 19:28:32 +02:00
Lorenz Leutgeb 31f3d08d63 radicle/cob/thread: Use timeline: IndexSet
Plenty conservative `debug_assert!`s, cause panics as soon as two
actions that should be added to the timeline appear within the same
operation. But there is no code in place to prevent this.

It also does not seem to be problematic to visit the same operation
multiple times (once per contained action).

A set that retains iteration order does the job.

Analysis of this bug was done over Zulip, see
<https://radicle.zulipchat.com/#narrow/channel/369873-Support/topic/radicle-node.201.2E10.20segfaults/with/616991470>.

`Update::Reviewed::review` is boxed due to:

    error: large size difference between variants
       --> crates/radicle-cli/src/terminal/patch/timeline.rs:158:1
        |
    158 | /  enum Update<'a> {
    159 | |      /// A revision of the patch was reviewed.
    160 | |      Reviewed { review: Review },
        | |      --------------------------- the largest variant contains at least 352 bytes
    161 | |      /// A revision of the patch was merged.
    162 | |/     Merged {
    163 | ||         author: Author<'a>,
    164 | ||         /// If the merge is none, this means that it was a fast-forward merge.
    165 | ||         merge: Option<Merge>,
    166 | ||     },
        | ||_____- the second-largest variant contains at least 120 bytes
    167 | |  }
        | |__^ the entire enum is at least 352 bytes
2026-08-23 19:15:09 +02:00
Lorenz Leutgeb d7edbfb217 radicle/cob/thread: Make timeline private
There is no reason to have `pub(crate)` here.
2026-08-23 19:15:09 +02:00
Lorenz Leutgeb 5ddd5c64ea radicle/cob/thread: Make comments private
There is no strong reason to have `pub(crate)` here.
2026-08-23 19:15:09 +02:00
Lars WirzeniusandShawn Webb 52ac5d3b3e cli/debug: Add hardenedBsd detection
This is conditional on the target operating system being FreeBSDs.
HardenedBSD is FreeBSD but with changes. We determine which one it is,
by running 'sysctl --values hardening.version'.

Co-authored-by: Shawn Webb <shawn.webb@hardenedbsd.org>
2026-08-12 16:42:48 +02:00
Lars Wirzenius 9f05038234 cli/debug: Output /etc/os-release if available 2026-08-12 16:42:18 +02:00
Lorenz Leutgeb d18293c6ad cli/test: Do not accidentally test rad debug
The tests `rad-warn-ipv6.md` and `rad-warn-old-nodes.md` are meant to
primarily test the output of warnings. They test the output twice, via
`rad debug` and via `rad node status`.

Testing the output via one command should be sufficient.
2026-08-12 16:14:34 +02:00
Lorenz Leutgeb 4e3c274468 crypto, node: Randomly generate ephemeral keys 2026-08-12 15:28:40 +02:00
Lorenz LeutgebandKonstantinos Maninakis 4fff390c32 crypto: Use VerifyingKey::verify_strict
Refer to
<https://docs.rs/ed25519-dalek/2.2.0/ed25519_dalek/struct.VerifyingKey.html#method.verify_strict>.

Co-authored-by: Konstantinos Maninakis <maninak@protonmail.com>
2026-08-12 15:28:40 +02:00
Daniel NormanandLorenz Leutgeb d4acccf614 protocol/service: Check visibility on Subscribe
When `Service::handle_message` handles `Message::Subscribe`, constraints
regarding visibility of repositories are not considered, this leads to
leaking RIDs of private repositories.

Add a corresponding check.

Add a test to verify that reference announcements relayed via subscribe
correctly apply visibility filtering, there by ensuring references of
private repositories are only propagated to nodes on the allow list.

The test regarding deduplication needs a fix: Ensure that all mock
repositories are public and in storage so that they do not
get filtered out in case of unknown visiblity.

Co-authored-by: Lorenz Leutgeb <lorenz.leutgeb@radicle.dev>
2026-08-12 15:28:40 +02:00
Adrian Duke 1a5dfc2616 surf: Fix flakey test with sorted vector 2026-08-05 13:22:09 +02:00