Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
REVIEW: fixes to Scope and radicle-fetch
✗ CI failure Fintan Halpenny committed 2 months ago
commit 8fd9641707005cec1fc4550d289ff19375d1d334
parent e624474ec983bbd138053af66de9931b7c6ccf12
1 failed (1 total) View logs
27 files changed +256 -38
modified crates/radicle-cli/examples/rad-block.md
@@ -38,7 +38,7 @@ $ rad seed
╭───────────────────────────────────────────────────────────╮
│ Repository                          Name   Policy   Scope │
├───────────────────────────────────────────────────────────┤
-
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji          block    all   │
+
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji          block          │
╰───────────────────────────────────────────────────────────╯
```

modified crates/radicle-cli/examples/rad-clone-connect.md
@@ -3,7 +3,7 @@ automatically connect to the necessary seeds.

```
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
+
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 2 potential seed(s).
✓ Target met: 1 seed(s)
✓ Creating checkout in ./heartwood..
modified crates/radicle-cli/examples/rad-clone-partial-fail.md
@@ -10,12 +10,13 @@ $ rad node routing
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
╰──────────────────────────────────────────────────────────────────────────────────────╯
```
+

When she tries to clone, one of those will fail to fetch. But the clone command
still returns successfully.

```
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --timeout 3
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
+
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 3 potential seed(s).
✓ Target met: 1 seed(s)
✓ Creating checkout in ./heartwood..
modified crates/radicle-cli/examples/rad-clone.md
@@ -2,7 +2,7 @@ To create a local copy of a repository on the radicle network, we use the
`clone` command, followed by the identifier or *RID* of the repository:

```
-
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope followed
+
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found [..] potential seed(s).
✓ Target met: [..] seed(s)
modified crates/radicle-cli/examples/rad-fetch.md
@@ -10,7 +10,7 @@ have to update our seeding policy for the project.

```
$ rad seed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-fetch
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
+
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
```

Now that the project is seeding we can fetch it and we will have it in
modified crates/radicle-cli/examples/rad-id-threshold.md
@@ -170,7 +170,7 @@ sync` and fetch his references:

``` ~bob
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
+
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 2 potential seed(s).
✓ Target met: 1 seed(s)
✓ Creating checkout in ./heartwood..
modified crates/radicle-cli/examples/rad-init-no-seed.md
@@ -21,7 +21,7 @@ If we then seed it, it becomes advertised in our inventory:
```
$ rad seed rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK
✓ Inventory updated with rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK
-
✓ Seeding policy updated for rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK with scope 'all'
+
✓ Seeding policy updated for rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK with scope 'followed'
```
```
$ rad node inventory
modified crates/radicle-cli/examples/rad-init-private-clone-seed.md
@@ -30,7 +30,7 @@ $ rad inspect --identity
``` ~bob
$ rad ls --all --private
$ rad clone rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
-
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'all'
+
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'followed'
Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from the network, found 1 potential seed(s).
✓ Target met: 1 preferred seed(s).
✓ Creating checkout in ./heartwood..
@@ -49,7 +49,7 @@ We can also use `rad seed` to seed and fetch without creating a checkout.

``` ~bob
$ rad seed rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --from z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
-
✓ Seeding policy exists for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'all'
+
✓ Seeding policy exists for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'followed'
Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from the network, found 1 potential seed(s).
✓ Target met: 1 seed(s)
```
modified crates/radicle-cli/examples/rad-init-private-clone.md
@@ -6,7 +6,7 @@ $ rad ls
```
``` ~bob (fail)
$ rad clone rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
-
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'all'
+
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'followed'
Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from the network, found 1 potential seed(s).
✗ Target not met: could not fetch from [z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi], and required 1 more seed(s)
! Warning: Failed to fetch from 1 seed(s).
modified crates/radicle-cli/examples/rad-init-private-no-seed.md
@@ -28,7 +28,7 @@ We can decide to seed it later, so that others can fetch it from us, given
that they are part of the allow list:
```
$ rad seed rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu
-
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'all'
+
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'followed'
```

But it still won't show up in our inventory, since it's private:
modified crates/radicle-cli/examples/rad-node.md
@@ -108,7 +108,7 @@ up in our inventory:
```
$ rad seed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
✓ Inventory updated with rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
+
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
$ rad node inventory
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
```
modified crates/radicle-cli/examples/rad-patch-pull-update.md
@@ -22,7 +22,7 @@ To push changes, run `git push`.

``` ~bob
$ rad clone rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK
-
✓ Seeding policy updated for rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK with scope 'all'
+
✓ Seeding policy updated for rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK with scope 'followed'
Fetching rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK from the network, found 1 potential seed(s).
✓ Target met: 1 seed(s)
✓ Creating checkout in ./heartwood..
modified crates/radicle-cli/examples/rad-seed-many.md
@@ -4,10 +4,10 @@ is used):

```
$ rad seed rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW rad:z3zTnCfi6cVSZG8eCGn6AMDypgAPm
-
✓ Seeding policy updated for rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW with scope 'all'
+
✓ Seeding policy updated for rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW with scope 'followed'
Fetching rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW from the network, found 1 potential seed(s).
✓ Target met: 1 seed(s)
-
✓ Seeding policy updated for rad:z3zTnCfi6cVSZG8eCGn6AMDypgAPm with scope 'all'
+
✓ Seeding policy updated for rad:z3zTnCfi6cVSZG8eCGn6AMDypgAPm with scope 'followed'
Fetching rad:z3zTnCfi6cVSZG8eCGn6AMDypgAPm from the network, found 1 potential seed(s).
✓ Target met: 1 seed(s)
```
added crates/radicle-cli/examples/rad-seed-policy-allow-no-scope.md
@@ -0,0 +1,7 @@
+
We want to ensure that a warning is printed when the `scope` field is missing in the `seedingPolicy`.
+

+
``` alice
+
$ rad node status
+
! Warning: node 'seedingPolicy.scope' has been set to 'all' by default. This default value will be removed in a future release. Please explicitly set it to one of ['all', 'followed'] in your node config.
+
[..]
+
```
modified crates/radicle-cli/examples/rad-sync-without-node.md
@@ -14,5 +14,5 @@ Note that seeding works fine without a running node:

``` ~alice
$ rad seed rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
-
✓ Seeding policy updated for rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 with scope 'all'
+
✓ Seeding policy updated for rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 with scope 'followed'
```
modified crates/radicle-cli/examples/rad-sync.md
@@ -107,7 +107,7 @@ It's also possible to receive an error if a repository is not found anywhere.

```
$ rad seed rad:z39mP9rQAaGmERfUMPULfPUi473tY --no-fetch
-
✓ Seeding policy updated for rad:z39mP9rQAaGmERfUMPULfPUi473tY with scope 'all'
+
✓ Seeding policy updated for rad:z39mP9rQAaGmERfUMPULfPUi473tY with scope 'followed'
```
``` (fail)
$ rad sync rad:z39mP9rQAaGmERfUMPULfPUi473tY
modified crates/radicle-cli/src/commands/clone/args.rs
@@ -62,7 +62,7 @@ pub struct Args {
    /// Follow scope
    #[arg(
        long,
-
        default_value_t = Scope::All,
+
        default_value_t = Scope::Followed,
        value_parser = terminal::args::ScopeParser
    )]
    pub(super) scope: Scope,
modified crates/radicle-cli/src/commands/debug.rs
@@ -131,7 +131,7 @@ fn stderr_of(bin: &str, args: &[&str]) -> anyhow::Result<String> {

fn collect_warnings(profile: Option<&Profile>) -> Vec<String> {
    match profile {
-
        Some(profile) => crate::warning::nodes_renamed(&profile.config),
+
        Some(profile) => crate::warning::config_warnings(&profile.config),
        None => vec!["No Radicle profile found.".to_string()],
    }
}
modified crates/radicle-cli/src/commands/node/control.rs
@@ -257,7 +257,7 @@ pub fn connect_many(
}

pub fn status(node: &Node, profile: &Profile) -> anyhow::Result<()> {
-
    for warning in crate::warning::nodes_renamed(&profile.config) {
+
    for warning in crate::warning::config_warnings(&profile.config) {
        term::warning(warning);
    }

modified crates/radicle-cli/src/commands/seed.rs
@@ -84,7 +84,9 @@ pub fn seeding(profile: &Profile) -> anyhow::Result<()> {
                    .repository(rid)
                    .and_then(|repo| repo.project().map(|proj| proj.name().to_string()))
                    .unwrap_or_default();
-
                let scope = policy.scope().unwrap_or_default().to_string();
+
                let scope = policy
+
                    .scope()
+
                    .map_or(String::new(), |scope| scope.to_string());
                let policy = term::format::policy(&Policy::from(policy));

                t.push([
modified crates/radicle-cli/src/commands/seed/args.rs
@@ -49,7 +49,7 @@ pub struct Args {
    /// Peer follow scope for this repository
    #[arg(
        long,
-
        default_value_t = Scope::All,
+
        default_value_t = Scope::Followed,
        value_parser = terminal::args::ScopeParser
    )]
    pub(super) scope: Scope,
modified crates/radicle-cli/src/warning.rs
@@ -22,26 +22,50 @@ fn nodes_renamed_for_option(
    option: &'static str,
    iter: impl IntoIterator<Item = ConnectAddress>,
) -> Vec<String> {
-
    let mut warnings: Vec<String> = vec![];
-

-
    for (i, value) in iter.into_iter().enumerate() {
+
    iter.into_iter().enumerate().fold(Vec::new(), |mut warnings, (i, value)| {
        let old: Address = value.into();
        if let Some(new) = NODES_RENAMED.get(&old) {
            warnings.push(format!(
                "Value of configuration option `{option}` at index {i} mentions node with address '{old}', which has been renamed to '{new}'. Please update your configuration."
            ));
        }
-
    }
-

-
    warnings
+
        warnings
+
    })
}

-
pub(crate) fn nodes_renamed(config: &Config) -> Vec<String> {
+
fn nodes_renamed(config: &Config) -> Vec<String> {
    let mut warnings = nodes_renamed_for_option("node.connect", config.node.connect.clone());
    warnings.extend(nodes_renamed_for_option(
        "preferredSeeds",
        config.preferred_seeds.clone(),
    ));
+

+
    warnings
+
}
+

+
fn implicit_seeding_policy_allow_scope(config: &Config) -> Vec<String> {
+
    use radicle::node::config::DefaultSeedingPolicy;
+
    use radicle::node::policy;
+

+
    let DefaultSeedingPolicy::Allow { scope } = config.node.seeding_policy else {
+
        return vec![];
+
    };
+
    if scope.is_implicit() {
+
        let scope = scope.into_inner();
+
        vec![format!(
+
                "node 'seedingPolicy.scope' has been set to '{scope}' by default. This default value will be removed in a future release. Please explicitly set it to one of ['{}', '{}'] in your node config.",
+
                policy::Scope::All,
+
                policy::Scope::Followed,
+
            )]
+
    } else {
+
        vec![]
+
    }
+
}
+

+
pub(crate) fn config_warnings(config: &Config) -> Vec<String> {
+
    let mut warnings = nodes_renamed(config);
+
    warnings.extend(implicit_seeding_policy_allow_scope(config));
+

    warnings
}

modified crates/radicle-cli/tests/commands.rs
@@ -572,6 +572,7 @@ fn rad_id_multi_delegate() {

    alice.handle.seed(acme, Scope::All).unwrap();
    bob.handle.follow(eve.id, None).unwrap();
+
    eve.handle.follow(bob.id, None).unwrap();
    alice.connect(&bob).converge([&bob]);
    eve.connect(&alice).converge([&alice]);

@@ -2059,17 +2060,19 @@ fn rad_remote() {
        .handle
        .follow(bob.id, Some(Alias::new("bob")))
        .unwrap();
+
    alice
+
        .handle
+
        .follow(eve.id, Some(Alias::new("eve")))
+
        .unwrap();

    bob.connect(&alice);
    bob.routes_to(&[(rid, alice.id)]);
    bob.fork(rid, bob.home.path()).unwrap();
-
    bob.announce(rid, 2, bob.home.path()).unwrap();
    alice.has_remote_of(&rid, &bob.id);

-
    eve.connect(&bob);
+
    eve.connect(&alice);
    eve.routes_to(&[(rid, alice.id)]);
    eve.fork(rid, eve.home.path()).unwrap();
-
    eve.announce(rid, 2, eve.home.path()).unwrap();
    alice.has_remote_of(&rid, &eve.id);

    test(
@@ -2868,3 +2871,24 @@ fn rad_workflow() {
    )
    .unwrap();
}
+

+
#[test]
+
fn rad_seed_policy_allow_no_scope() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node_with(Config {
+
        seeding_policy: DefaultSeedingPolicy::Allow {
+
            scope: node::config::Scope::implicit(),
+
        },
+
        ..Config::test(Alias::new("alice"))
+
    });
+

+
    let alice = alice.spawn();
+

+
    test(
+
        "examples/rad-seed-policy-allow-no-scope.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+
}
modified crates/radicle-protocol/src/service.rs
@@ -1655,6 +1655,13 @@ where
                // Refs can be relayed by peers who don't have the data in storage,
                // therefore we only check whether we are connected to the *announcer*,
                // which is required by the protocol to only announce refs it has.
+
                //
+
                // TODO(Ade): Perhaps it makes sense in a more peer-to-peer arrangement
+
                // to establish connections to peers whom you follow but aren't directly connected
+
                // to. E.g. Alice <--> Bob <--> Eve
+
                // Alice follows Eve, Eve announces refs of interest but because Alice
+
                // is not directly connected she cant see said refs if Bob isn't also interested in
+
                // Eve's refs.
                let Some(remote) = self.sessions.get(announcer).cloned() else {
                    trace!(
                        target: "service",
modified crates/radicle/Cargo.toml
@@ -71,6 +71,7 @@ pretty_assertions = { workspace = true }
qcheck = { workspace = true }
qcheck-macros = { workspace = true }
radicle-cob = { workspace = true, features = ["stable-commit-ids", "test"] }
+
radicle-core = {workspace = true, features = ["qcheck"]}
radicle-crypto = { workspace = true, features = ["test"] }
radicle-git-metadata = { workspace = true }
tempfile = { workspace = true }
modified crates/radicle/src/node/config.rs
@@ -9,9 +9,11 @@ use serde::{Deserialize, Serialize};
use serde_json as json;

use crate::node;
-
use crate::node::policy::{Scope, SeedingPolicy};
+
use crate::node::policy::SeedingPolicy;
use crate::node::{Address, Alias, NodeId};

+
use super::policy;
+

/// Peer-to-peer protocol version.
pub type ProtocolVersion = u8;

@@ -364,13 +366,13 @@ pub enum AddressConfig {

/// Default seeding policy. Applies when no repository policies for the given repo are found.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
-
#[serde(rename_all = "camelCase", tag = "default")]
+
#[serde(tag = "default", rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum DefaultSeedingPolicy {
    /// Allow seeding.
    Allow {
        /// Seeding scope.
-
        #[serde(default)]
+
        #[serde(skip_serializing_if = "Scope::is_implicit")]
        scope: Scope,
    },
    /// Block seeding.
@@ -378,6 +380,58 @@ pub enum DefaultSeedingPolicy {
    Block,
}

+
/// [`Scope`] provides a schema for [`policy::Scope`], where the inner scope is optional.
+
///
+
/// It is used in [`DefaultSeedingPolicy`] to allow for optionally setting the
+
/// scope in the [`DefaultSeedingPolicy::Allow`] variant.
+
/// This materializes as the `seedingPolicy.scope` configuration value, when
+
/// `"default": "allow"` is set.
+
///
+
/// [`Scope`] is introduced to allow migrating to a required value in a future
+
/// version (post v1.6.x).
+
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+
#[serde(transparent)]
+
pub struct Scope(Option<policy::Scope>);
+

+
impl Scope {
+
    /// Construct the implicit scope, where the default value,
+
    /// [`policy::Scope::All`], is chosen for the final scope value.
+
    pub fn implicit() -> Self {
+
        Self(None)
+
    }
+

+
    /// Construct the explicit scope, where the given [`policy::Scope`] is used.
+
    pub fn explicit(scope: policy::Scope) -> Self {
+
        Self(Some(scope))
+
    }
+

+
    /// Resolve this [`Scope`] to its [`policy::Scope`] value.
+
    ///
+
    /// If the scope is implicit, then [`policy::Scope::All`] is returned.
+
    pub fn into_inner(self) -> policy::Scope {
+
        self.0.unwrap_or(policy::Scope::All)
+
    }
+

+
    /// Returns `true` when the scope is implicit, i.e. no [`policy::Scope`] was
+
    /// given.
+
    pub fn is_implicit(&self) -> bool {
+
        self.0.is_none()
+
    }
+

+
    /// Construct the explicit [`Scope`] where the inner scope is
+
    /// [`policy::Scope::All`].
+
    fn all() -> Self {
+
        Self::explicit(policy::Scope::All)
+
    }
+

+
    /// Construct the explicit [`Scope`] where the inner scope is
+
    /// [`policy::Scope::Followed`].
+
    fn followed() -> Self {
+
        Self::explicit(policy::Scope::Followed)
+
    }
+
}
+

impl DefaultSeedingPolicy {
    /// Is this an "allow" policy.
    pub fn is_allow(&self) -> bool {
@@ -386,7 +440,16 @@ impl DefaultSeedingPolicy {

    /// Seed everything from anyone.
    pub fn permissive() -> Self {
-
        Self::Allow { scope: Scope::All }
+
        Self::Allow {
+
            scope: Scope::all(),
+
        }
+
    }
+

+
    /// Seed only delegate changes.
+
    pub fn followed() -> Self {
+
        Self::Allow {
+
            scope: Scope::followed(),
+
        }
    }
}

@@ -394,7 +457,9 @@ impl From<DefaultSeedingPolicy> for SeedingPolicy {
    fn from(policy: DefaultSeedingPolicy) -> Self {
        match policy {
            DefaultSeedingPolicy::Block => Self::Block,
-
            DefaultSeedingPolicy::Allow { scope } => Self::Allow { scope },
+
            DefaultSeedingPolicy::Allow { scope } => SeedingPolicy::Allow {
+
                scope: scope.into_inner(),
+
            },
        }
    }
}
@@ -646,6 +711,10 @@ wrapper!(
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
+
    use super::{DefaultSeedingPolicy, Scope};
+
    use crate::node::policy;
+
    use serde_json::json;
+

    #[test]
    fn partial() {
        use super::Config;
@@ -683,4 +752,85 @@ mod test {
        );
        assert_eq!(config.limits.connection.outbound.0, 1337);
    }
+

+
    #[test]
+
    fn deserialize_migrating_scope() {
+
        let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
+
            "default": "allow"
+
        }))
+
        .unwrap();
+

+
        assert_eq!(
+
            seeding_policy,
+
            DefaultSeedingPolicy::Allow { scope: Scope(None) }
+
        );
+

+
        let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
+
            "default": "allow",
+
            "scope": null
+
        }))
+
        .unwrap();
+

+
        assert_eq!(
+
            seeding_policy,
+
            DefaultSeedingPolicy::Allow { scope: Scope(None) }
+
        );
+

+
        let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
+
            "default": "allow",
+
            "scope": "all"
+
        }))
+
        .unwrap();
+

+
        assert_eq!(
+
            seeding_policy,
+
            DefaultSeedingPolicy::Allow {
+
                scope: Scope(Some(policy::Scope::All))
+
            }
+
        );
+

+
        let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
+
            "default": "allow",
+
            "scope": "followed"
+
        }))
+
        .unwrap();
+

+
        assert_eq!(
+
            seeding_policy,
+
            DefaultSeedingPolicy::Allow {
+
                scope: Scope(Some(policy::Scope::Followed))
+
            }
+
        )
+
    }
+

+
    #[test]
+
    fn serialize_migrating_scope() {
+
        assert_eq!(
+
            json!({
+
                "default": "allow"
+
            }),
+
            serde_json::to_value(DefaultSeedingPolicy::Allow { scope: Scope(None) }).unwrap()
+
        );
+

+
        assert_eq!(
+
            json!({
+
                "default": "allow",
+
                "scope": "all"
+
            }),
+
            serde_json::to_value(DefaultSeedingPolicy::Allow {
+
                scope: Scope(Some(policy::Scope::All))
+
            })
+
            .unwrap()
+
        );
+
        assert_eq!(
+
            json!({
+
                "default": "allow",
+
                "scope": "followed"
+
            }),
+
            serde_json::to_value(DefaultSeedingPolicy::Allow {
+
                scope: Scope(Some(policy::Scope::Followed))
+
            })
+
            .unwrap()
+
        );
+
    }
}
modified crates/radicle/src/profile/config.rs
@@ -192,7 +192,9 @@ impl Config {
            ) {
                log::warn!(target: "radicle", "Overwriting `seedingPolicy` configuration");
                cfg.node.seeding_policy = match policy {
-
                    Policy::Allow => DefaultSeedingPolicy::Allow { scope },
+
                    Policy::Allow => DefaultSeedingPolicy::Allow {
+
                        scope: node::config::Scope::explicit(scope),
+
                    },
                    Policy::Block => DefaultSeedingPolicy::Block,
                }
            }