rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood7e5378a3e1b36e81622aaf3b4b15d9f487c5a167
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Updated",
"patch": {
"id": "942696775c06df96f3924e0ea84c5a7bf6336a6b",
"author": {
"id": "did:key:z6MktwkohCx8aHZ1QCjVZUiLmX92oPZFxRiFZkbq32Tk5Tkm",
"alias": "2color"
},
"title": "protocol: fix private refs leak via subscribe",
"state": {
"status": "open",
"conflicts": []
},
"before": "71a91042d96bb994c4cd4311d85f5cd1a6311b4e",
"after": "7e5378a3e1b36e81622aaf3b4b15d9f487c5a167",
"commits": [
"7e5378a3e1b36e81622aaf3b4b15d9f487c5a167",
"4024bac1f4d503144534ff8afc5f57f85ad4a64c",
"5d6ab3b782be10168fedb6c9ea81af61ad3d5f53"
],
"target": "71a91042d96bb994c4cd4311d85f5cd1a6311b4e",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "942696775c06df96f3924e0ea84c5a7bf6336a6b",
"author": {
"id": "did:key:z6MktwkohCx8aHZ1QCjVZUiLmX92oPZFxRiFZkbq32Tk5Tkm",
"alias": "2color"
},
"description": "There are three code paths that emit refs announcements:\n\n- When you publish your own refs (`announce_own_refs`)\n- When you relay received refs (`relay`)\n- When you reply to Subscribe (`handle_message`)\n\nThe first two correctly filter the receivers of the announcement with\n`doc.is_visible_to`, but replies to Subscribe don't, causing refs announcements\nfrom private repos to leak to users not on the visiblity allow list.\n\nThe patch adds a new test to verify that ref announcements relayed via\nsubscribe correctly apply visibility filtering, thereby ensuring private repo\nrefs are only propagated to users on the allow list. It also fixes the leak\nby applying filtering in `handle_message`",
"base": "71a91042d96bb994c4cd4311d85f5cd1a6311b4e",
"oid": "4024bac1f4d503144534ff8afc5f57f85ad4a64c",
"timestamp": 1785420034
},
{
"id": "a0521b982460201428c10aad3bcbeb1af65ca6ad",
"author": {
"id": "did:key:z6MktwkohCx8aHZ1QCjVZUiLmX92oPZFxRiFZkbq32Tk5Tkm",
"alias": "2color"
},
"description": "node/test: fix the dedpulication test\n\nensure that all mock repos are public and in storage so that they don't\nget filtered out in case of ambiguity about their visiblity.",
"base": "71a91042d96bb994c4cd4311d85f5cd1a6311b4e",
"oid": "7e5378a3e1b36e81622aaf3b4b15d9f487c5a167",
"timestamp": 1785426405
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "45820eee-2cd0-4f4d-bb0e-ff89c987bec8"
},
"info_url": "https://cci.rad.levitte.org//45820eee-2cd0-4f4d-bb0e-ff89c987bec8.html"
}
Started at: 2026-07-30 17:46:55.610996+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/45820eee-2cd0-4f4d-bb0e-ff89c987bec8/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 187 issues · 43 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 942696775c06df96f3924e0ea84c5a7bf6336a6b
✓ Switched to branch patch/9426967 at revision a0521b9
✓ Branch patch/9426967 setup to track rad/patches/942696775c06df96f3924e0ea84c5a7bf6336a6b
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 7e5378a3e1b36e81622aaf3b4b15d9f487c5a167
HEAD is now at 7e5378a3 node/test: fix the dedpulication test
Exit code: 0
$ rad patch show 942696775c06df96f3924e0ea84c5a7bf6336a6b -p
╭─────────────────────────────────────────────────────────────────────────────────╮
│ Title protocol: fix private refs leak via subscribe │
│ Patch 942696775c06df96f3924e0ea84c5a7bf6336a6b │
│ Author 2color z6Mktwk…2Tk5Tkm │
│ Head 7e5378a3e1b36e81622aaf3b4b15d9f487c5a167 │
│ Base 71a91042d96bb994c4cd4311d85f5cd1a6311b4e │
│ Branches patch/9426967 │
│ Commits ahead 3, behind 0 │
│ Status open │
│ │
│ There are three code paths that emit refs announcements: │
│ │
│ - When you publish your own refs (`announce_own_refs`) │
│ - When you relay received refs (`relay`) │
│ - When you reply to Subscribe (`handle_message`) │
│ │
│ The first two correctly filter the receivers of the announcement with │
│ `doc.is_visible_to`, but replies to Subscribe don't, causing refs announcements │
│ from private repos to leak to users not on the visiblity allow list. │
│ │
│ The patch adds a new test to verify that ref announcements relayed via │
│ subscribe correctly apply visibility filtering, thereby ensuring private repo │
│ refs are only propagated to users on the allow list. It also fixes the leak │
│ by applying filtering in `handle_message` │
├─────────────────────────────────────────────────────────────────────────────────┤
│ 7e5378a node/test: fix the dedpulication test │
│ 4024bac protocol: fix private refs leak via subscribe │
│ 5d6ab3b node/test: test subscribe relay privacy │
├─────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 9426967 @ 71a9104..4024bac by 2color z6Mktwk…2Tk5Tkm 1 hour ago │
│ ↑ Revision a0521b9 @ 71a9104..7e5378a by 2color z6Mktwk…2Tk5Tkm 13 seconds ago │
╰─────────────────────────────────────────────────────────────────────────────────╯
commit 7e5378a3e1b36e81622aaf3b4b15d9f487c5a167
Author: Daniel Norman <daniel@norman.life>
Date: Thu Jul 30 17:39:55 2026 +0200
node/test: fix the dedpulication test
ensure that all mock repos are public and in storage so that they don't
get filtered out in case of ambiguity about their visiblity.
diff --git a/crates/radicle-node/src/tests.rs b/crates/radicle-node/src/tests.rs
index b3822681ba..fd7318391b 100644
--- a/crates/radicle-node/src/tests.rs
+++ b/crates/radicle-node/src/tests.rs
@@ -482,13 +482,25 @@ fn announcement_rebroadcast() {
}
#[test]
-fn announcement_rebroadcast_duplicates() {
+fn announcement_rebroadcast_deduplicates() {
let mut cid = Peer::cid();
let mut amy = Peer::amy();
let bob = Peer::bob();
let dan = Peer::dan();
let rids = arbitrary::set::<RepoId>(3..=3);
+ for rid in &rids {
+ let mut repo = r#gen::<MockRepository>(1);
+ repo.doc.doc = repo
+ .doc
+ .doc
+ .with_edits(|doc| {
+ doc.visibility = Visibility::Public
+ })
+ .unwrap();
+ amy.storage_mut().repos.insert(rid.clone(), repo);
+ }
+
amy.connect_to(&bob);
amy.receive(*bob.nid(), cid.node_announcement());
@@ -820,7 +832,6 @@ fn refs_announcement_subscribe_private() {
Peer::with_storage("bob", BOB, storage)
};
-
// Amy holds the repo, and it isn't visible to Cid.
let repo = {
let mut repo = r#gen::<MockRepository>(1);
commit 4024bac1f4d503144534ff8afc5f57f85ad4a64c
Author: Daniel Norman <daniel@norman.life>
Date: Thu Jul 30 12:24:19 2026 +0200
protocol: fix private refs leak via subscribe
diff --git a/crates/radicle-protocol/src/service.rs b/crates/radicle-protocol/src/service.rs
index 75a56badb7..b40b01a045 100644
--- a/crates/radicle-protocol/src/service.rs
+++ b/crates/radicle-protocol/src/service.rs
@@ -1840,6 +1840,22 @@ where
if ann.node == *remote {
continue;
}
+ // Only send refs announcements for repositories the peer is allowed
+ // to know about. If we don't have the repository, we can't tell
+ // whether it's private, so we default to false because we can't
+ // determine if it's private or public.
+ if let AnnouncementMessage::Refs(RefsAnnouncement { rid, .. }) =
+ &ann.message
+ && !self
+ .storage
+ .get(*rid)
+ .ok()
+ .flatten()
+ .map(|doc| doc.is_visible_to(&(*remote).into()))
+ .unwrap_or(false)
+ {
+ continue;
+ }
// Only send messages if we're a relay, or it's our own messages.
if relay || ann.node == local {
self.outbox.write(peer, ann.into());
commit 5d6ab3b782be10168fedb6c9ea81af61ad3d5f53
Author: Daniel Norman <daniel@norman.life>
Date: Thu Jul 30 12:01:49 2026 +0200
node/test: test subscribe relay privacy
Add a new test to verify that ref announcements relayed via subscribe
correctly apply visibility filtering, there by ensuring private repo
refs are only propagated to users on the allow list.
Test the subscribe code path which has different relaying logic (in the
Subscribe handler in `handle_message`) compared to `relay` and
`announce_refs`, the two other code paths that emit ref announcements
of others and one's own respectively.
diff --git a/crates/radicle-node/src/tests.rs b/crates/radicle-node/src/tests.rs
index 60dd40eb73..b3822681ba 100644
--- a/crates/radicle-node/src/tests.rs
+++ b/crates/radicle-node/src/tests.rs
@@ -803,6 +803,69 @@ fn refs_announcement_relay_private() {
);
}
+#[test]
+fn refs_announcement_subscribe_private() {
+ let tmp = tempfile::tempdir().unwrap();
+ let mut amy = Peer::amy();
+ let cid = Peer::with_storage(
+ "cid",
+ CID,
+ Storage::open(tmp.path().join("cid"), fixtures::user()).unwrap(),
+ );
+
+ let bob = {
+ let signer = SigningKey::mock(BOB as usize);
+ let storage = fixtures::storage(tmp.path().join("bob"), &signer).unwrap();
+
+ Peer::with_storage("bob", BOB, storage)
+ };
+
+
+ // Amy holds the repo, and it isn't visible to Cid.
+ let repo = {
+ let mut repo = r#gen::<MockRepository>(1);
+ repo.doc.doc = repo
+ .doc
+ .doc
+ .with_edits(|doc| {
+ doc.visibility = Visibility::Private {
+ allow: [(*amy.nid()).into(), (*bob.nid()).into()].into(),
+ };
+ })
+ .unwrap();
+ repo
+ };
+ let private = repo.id;
+
+ amy.seed(&repo.id, policy::Scope::All).unwrap();
+ amy.storage_mut().repos.insert(private, repo);
+
+ amy.connect_to(&bob);
+ amy.connect_to(&cid);
+
+ // Amy stores Bob's announcement *before* Cid subscribes.
+ amy.receive(*bob.nid(), bob.refs_announcement(private));
+ amy.messages(*cid.nid()).for_each(drop);
+
+ // Cid subscribes with the default filter, which matches every repo.
+ amy.receive(*cid.nid(), Message::Subscribe(Subscribe::all()));
+
+ let private_refs_announcement = amy.messages(*cid.nid()).find(|m| {
+ matches!(
+ m,
+ Message::Announcement(Announcement {
+ message: AnnouncementMessage::Refs(RefsAnnouncement { rid, .. }),
+ ..
+ }) if *rid == private
+ )
+ });
+ assert_matches!(
+ private_refs_announcement,
+ None,
+ "The refs announcement for private repo {private} is not sent to Cid"
+ );
+}
+
/// Even if Amy is not tracking Bob, Amy will fetch Bob's refs for a repo she doesn't have.
#[test]
fn refs_announcement_fetch_trusted_no_inventory() {
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps --all-features cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 45820eee-2cd0-4f4d-bb0e-ff89c987bec8 -v /opt/radcis/ci.rad.levitte.org/cci/state/45820eee-2cd0-4f4d-bb0e-ff89c987bec8/s:/45820eee-2cd0-4f4d-bb0e-ff89c987bec8/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/45820eee-2cd0-4f4d-bb0e-ff89c987bec8/w:/45820eee-2cd0-4f4d-bb0e-ff89c987bec8/w -w /45820eee-2cd0-4f4d-bb0e-ff89c987bec8/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /45820eee-2cd0-4f4d-bb0e-ff89c987bec8/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.97.1-x86_64-unknown-linux-gnu'
info: latest update on 2026-07-16, rust version 1.97.1 (8bab26f4f 2026-07-14)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.97.1 (c980f4866 2026-06-30)
+ rustc --version
rustc 1.97.1 (8bab26f4f 2026-07-14)
+ cargo fmt --check
Diff in /45820eee-2cd0-4f4d-bb0e-ff89c987bec8/w/crates/radicle-node/src/tests.rs:494:
repo.doc.doc = repo
.doc
.doc
- .with_edits(|doc| {
- doc.visibility = Visibility::Public
- })
+ .with_edits(|doc| doc.visibility = Visibility::Public)
.unwrap();
amy.storage_mut().repos.insert(rid.clone(), repo);
}
Exit code: 1
{
"response": "finished",
"result": "failure"
}