Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Simplify configuration
Alexis Sellier committed 1 year ago
commit 3ae7e305bd6d93f3e92167285e2fe00c4cdef50d
parent cc7d0cf3633282ee0307ddbc20f7641c9deec52e
19 files changed +317 -224
modified radicle-cli/examples/rad-block.md
@@ -35,9 +35,9 @@ And a 'block' policy was added:

```
$ rad seed
-
╭──────────────────────────────────────────────────────────────╮
-
│ Repository                          Name   Policy   Scope    │
-
├──────────────────────────────────────────────────────────────┤
-
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji          block    followed │
-
╰──────────────────────────────────────────────────────────────╯
+
╭───────────────────────────────────────────────────────────╮
+
│ Repository                          Name   Policy   Scope │
+
├───────────────────────────────────────────────────────────┤
+
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji          block    all   │
+
╰───────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-config.md
@@ -21,14 +21,10 @@ $ rad config
    "alias": "alice",
    "listen": [],
    "peers": {
-
      "type": "dynamic",
-
      "target": 8
+
      "type": "dynamic"
    },
    "connect": [],
    "externalAddresses": [],
-
    "db": {
-
      "journalMode": "rollback"
-
    },
    "network": "main",
    "log": "INFO",
    "relay": "auto",
@@ -54,8 +50,9 @@ $ rad config
      }
    },
    "workers": 8,
-
    "policy": "block",
-
    "scope": "all"
+
    "seedingPolicy": {
+
      "default": "block"
+
    }
  }
}
```
modified radicle-cli/src/commands/auth.rs
@@ -169,6 +169,12 @@ pub fn authenticate(options: Options, profile: &Profile) -> anyhow::Result<()> {
        term::success!("Authenticated as {}", term::format::tertiary(profile.id()));
        return Ok(());
    }
+
    for (key, _) in &profile.config.node.extra {
+
        term::warning(format!(
+
            "unused or deprecated configuration attribute {:?}",
+
            key
+
        ));
+
    }

    // If our key is encrypted, we try to authenticate with SSH Agent and
    // register it; only if it is running.
modified radicle-cli/src/commands/inspect.rs
@@ -9,7 +9,7 @@ use chrono::prelude::*;

use radicle::identity::RepoId;
use radicle::identity::{DocAt, Identity};
-
use radicle::node::policy::Policy;
+
use radicle::node::policy::SeedingPolicy;
use radicle::node::AliasStore as _;
use radicle::storage::git::{Repository, Storage};
use radicle::storage::refs::RefsAt;
@@ -175,15 +175,15 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            let policies = profile.policies()?;
            let seed = policies.seed_policy(&rid)?;
            match seed.policy {
-
                Policy::Allow => {
+
                SeedingPolicy::Allow { scope } => {
                    println!(
                        "Repository {} is {} with scope {}",
                        term::format::tertiary(&rid),
                        term::format::positive("being seeded"),
-
                        term::format::dim(format!("`{}`", seed.scope))
+
                        term::format::dim(format!("`{scope}`"))
                    );
                }
-
                Policy::Block => {
+
                SeedingPolicy::Block => {
                    println!(
                        "Repository {} is {}",
                        term::format::tertiary(&rid),
modified radicle-cli/src/commands/seed.rs
@@ -3,7 +3,7 @@ use std::ffi::OsString;
use anyhow::anyhow;

use radicle::node::policy;
-
use radicle::node::policy::Scope;
+
use radicle::node::policy::{Policy, Scope};
use radicle::node::Handle;
use radicle::{prelude::*, storage, Node};
use radicle_term::Element as _;
@@ -164,15 +164,15 @@ pub fn seeding(profile: &Profile) -> anyhow::Result<()> {
    ]);
    t.divider();

-
    for policy::SeedPolicy { rid, scope, policy } in store.seed_policies()? {
+
    for policy::SeedPolicy { rid, policy } in store.seed_policies()? {
        let id = rid.to_string();
        let name = storage
            .repository(rid)
            .map_err(storage::RepositoryError::from)
            .and_then(|repo| repo.project().map(|proj| proj.name().to_string()))
            .unwrap_or_default();
-
        let scope = scope.to_string();
-
        let policy = term::format::policy(&policy);
+
        let scope = policy.scope().unwrap_or_default().to_string();
+
        let policy = term::format::policy(&Policy::from(policy));

        t.push([
            term::format::tertiary(id),
modified radicle-cli/tests/commands.rs
@@ -16,7 +16,7 @@ use radicle::storage::{ReadStorage, RefUpdate, RemoteRepository};
use radicle::test::fixtures;

use radicle_cli_test::TestFormula;
-
use radicle_node::service::policy::{Policy, Scope};
+
use radicle_node::service::policy::{Scope, SeedingPolicy};
use radicle_node::service::Event;
use radicle_node::test::environment::{Config, Environment, Node};
#[allow(unused_imports)]
@@ -1188,7 +1188,7 @@ fn rad_unseed() {
fn rad_block() {
    let mut environment = Environment::new();
    let alice = environment.node(Config {
-
        policy: Policy::Allow,
+
        seeding_policy: SeedingPolicy::permissive(),
        ..Config::test(Alias::new("alice"))
    });
    let working = tempfile::tempdir().unwrap();
@@ -1491,7 +1491,7 @@ fn rad_init_sync_preferred() {
    let mut environment = Environment::new();
    let mut alice = environment
        .node(Config {
-
            policy: Policy::Allow,
+
            seeding_policy: SeedingPolicy::permissive(),
            ..Config::test(Alias::new("alice"))
        })
        .spawn();
@@ -1523,7 +1523,7 @@ fn rad_init_sync_timeout() {
    let mut environment = Environment::new();
    let mut alice = environment
        .node(Config {
-
            policy: Policy::Block,
+
            seeding_policy: SeedingPolicy::Block,
            ..Config::test(Alias::new("alice"))
        })
        .spawn();
@@ -1866,8 +1866,7 @@ fn test_replication_via_seed() {
    let alice = environment.node(config::relay("alice"));
    let bob = environment.node(config::relay("bob"));
    let seed = environment.node(Config {
-
        policy: Policy::Allow,
-
        scope: Scope::All,
+
        seeding_policy: SeedingPolicy::permissive(),
        ..config::relay("seed")
    });
    let working = environment.tmp().join("working");
@@ -2115,8 +2114,7 @@ fn rad_patch_open_explore() {
    let mut environment = Environment::new();
    let seed = environment
        .node(Config {
-
            policy: Policy::Allow,
-
            scope: Scope::All,
+
            seeding_policy: SeedingPolicy::permissive(),
            ..config::seed("seed")
        })
        .spawn();
modified radicle-fetch/src/policy.rs
@@ -5,7 +5,7 @@ use radicle::node::policy::config::Config;
use radicle::node::policy::store::Read;
use radicle::prelude::RepoId;

-
pub use radicle::node::policy::{Policy, Scope};
+
pub use radicle::node::policy::{Policy, Scope, SeedingPolicy};

#[derive(Clone, Debug)]
pub enum Allowed {
@@ -19,23 +19,23 @@ impl Allowed {
            .seed_policy(&rid)
            .map_err(|err| error::Policy::FailedPolicy { rid, err })?;
        match entry.policy {
-
            Policy::Block => {
+
            SeedingPolicy::Block => {
                log::error!(target: "fetch", "Attempted to fetch non-seeded repo {rid}");
                Err(error::Policy::BlockedPolicy { rid })
            }
-
            Policy::Allow => match entry.scope {
-
                Scope::All => Ok(Self::All),
-
                Scope::Followed => {
-
                    let nodes = config
-
                        .follow_policies()
-
                        .map_err(|err| error::Policy::FailedNodes { rid, err })?;
-
                    let followed: HashSet<_> = nodes
-
                        .filter_map(|node| (node.policy == Policy::Allow).then_some(node.nid))
-
                        .collect();
-

-
                    Ok(Allowed::Followed { remotes: followed })
-
                }
-
            },
+
            SeedingPolicy::Allow { scope: Scope::All } => Ok(Self::All),
+
            SeedingPolicy::Allow {
+
                scope: Scope::Followed,
+
            } => {
+
                let nodes = config
+
                    .follow_policies()
+
                    .map_err(|err| error::Policy::FailedNodes { rid, err })?;
+
                let followed: HashSet<_> = nodes
+
                    .filter_map(|node| (node.policy == Policy::Allow).then_some(node.nid))
+
                    .collect();
+

+
                Ok(Allowed::Followed { remotes: followed })
+
            }
        }
    }
}
modified radicle-httpd/src/api/v1/node.rs
@@ -8,7 +8,10 @@ use serde_json::json;

use radicle::identity::RepoId;
use radicle::node::routing::Store;
-
use radicle::node::{policy, AliasStore, Handle, NodeId, DEFAULT_TIMEOUT};
+
use radicle::node::{
+
    policy::{Policy, SeedPolicy},
+
    AliasStore, Handle, NodeId, DEFAULT_TIMEOUT,
+
};
use radicle::Node;

use crate::api::error::Error;
@@ -84,16 +87,11 @@ async fn node_policies_repos_handler(State(ctx): State<Context>) -> impl IntoRes
    let policies = ctx.profile.policies()?;
    let mut repos = Vec::new();

-
    for policy::SeedPolicy {
-
        rid: id,
-
        scope,
-
        policy,
-
    } in policies.seed_policies()?
-
    {
+
    for SeedPolicy { rid: id, policy } in policies.seed_policies()? {
        repos.push(json!({
            "id": id,
-
            "scope": scope,
-
            "policy": policy,
+
            "scope": policy.scope().unwrap_or_default(),
+
            "policy": Policy::from(policy),
        }));
    }

modified radicle-httpd/src/api/v1/profile.rs
@@ -83,13 +83,9 @@ mod routes {
                "node": {
                  "alias": "seed",
                  "listen": [],
-
                  "peers": {
-
                    "type": "dynamic",
-
                    "target": 8
-
                  },
+
                  "peers": { "type": "dynamic" },
                  "connect": [],
                  "externalAddresses": [],
-
                  "db": { "journalMode": "rollback" },
                  "network": "main",
                  "log": "INFO",
                  "relay": "auto",
@@ -115,8 +111,9 @@ mod routes {
                    }
                  },
                  "workers": 8,
-
                  "policy": "block",
-
                  "scope": "all"
+
                  "seedingPolicy": {
+
                      "default": "block",
+
                  }
                }
              },
              "home": seed.profile.path()
modified radicle-node/src/runtime.rs
@@ -119,16 +119,20 @@ impl Runtime {
        let clock = LocalTime::now();
        let timestamp = clock.into();
        let storage = Storage::open(home.storage(), git::UserInfo { alias, key: id })?;
-
        let scope = config.scope;
-
        let policy = config.policy;
+
        let policy = config.seeding_policy;

+
        for (key, _) in &config.extra {
+
            log::warn!(target: "node", "Unused or deprecated configuration attribute {:?}", key);
+
        }
        log::info!(target: "node", "Opening node database..");
-
        let db = home.database_mut()?.journal_mode(config.db.journal_mode)?;
+
        let db = home
+
            .database_mut()?
+
            .journal_mode(node::db::JournalMode::default())?;
        let mut stores: service::Stores<_> = db.clone().into();

        log::info!(target: "node", "Opening policy database..");
        let policies = home.policies_mut()?;
-
        let policies = policy::Config::new(policy, scope, policies);
+
        let policies = policy::Config::new(policy, policies);
        let notifications = home.notifications_mut()?;
        let cobs_cache = cob::cache::Store::open(home.cobs().join(cob::cache::COBS_DB_FILE))?;

@@ -234,7 +238,6 @@ impl Runtime {
                storage: storage.clone(),
                fetch,
                policy,
-
                scope,
                policies_db: home.node().join(node::POLICIES_DB_FILE),
            },
        )?;
modified radicle-node/src/service.rs
@@ -34,6 +34,7 @@ use radicle::node::seed::Store as _;
use radicle::node::{ConnectOptions, Penalty, Severity};
use radicle::storage::refs::SIGREFS_BRANCH;
use radicle::storage::RepositoryError;
+
use radicle_fetch::policy::SeedingPolicy;

use crate::crypto;
use crate::crypto::{Signer, Verified};
@@ -49,7 +50,7 @@ use crate::service::gossip::Store as _;
use crate::service::message::{
    Announcement, AnnouncementMessage, Info, NodeAnnouncement, Ping, RefsAnnouncement, RefsStatus,
};
-
use crate::service::policy::{store::Write, Policy, Scope};
+
use crate::service::policy::{store::Write, Scope};
use crate::storage;
use crate::storage::{refs::RefsAt, Namespaces, ReadStorage};
use crate::worker::fetch;
@@ -102,6 +103,8 @@ pub const MAX_RECONNECTION_DELTA: LocalDuration = LocalDuration::from_mins(60);
pub const CONNECTION_RETRY_DELTA: LocalDuration = LocalDuration::from_mins(10);
/// How long to wait for a fetch to stall before aborting, default is 3s.
pub const FETCH_TIMEOUT: time::Duration = time::Duration::from_secs(3);
+
/// Target number of peers to maintain connections to.
+
pub const TARGET_OUTBOUND_PEERS: usize = 8;

/// Maximum external address limit imposed by message size limits.
pub use message::ADDRESS_LIMIT;
@@ -560,7 +563,7 @@ where
        self.filter = Filter::new(
            self.policies
                .seed_policies()?
-
                .filter_map(|t| (t.policy == Policy::Allow).then_some(t.rid)),
+
                .filter_map(|t| (t.policy.is_allow()).then_some(t.rid)),
        );
        Ok(updated)
    }
@@ -735,7 +738,7 @@ where
        self.filter = Filter::new(
            self.policies
                .seed_policies()?
-
                .filter_map(|t| (t.policy == Policy::Allow).then_some(t.rid)),
+
                .filter_map(|t| (t.policy.is_allow()).then_some(t.rid)),
        );
        // Connect to configured peers.
        let addrs = self.config.connect.clone();
@@ -1216,9 +1219,12 @@ where
                    .policies
                    .seed_policy(&rid)
                    .expect("Service::dequeue_fetch: error accessing repo seeding configuration");
-

+
                let SeedingPolicy::Allow { scope } = repo_entry.policy else {
+
                    debug!(target: "service", "Repository {rid} is no longer seeded, skipping..");
+
                    continue;
+
                };
                // Keep dequeueing if there was nothing to fetch, otherwise break.
-
                if self.fetch_refs_at(rid, from, refs, repo_entry.scope, timeout, channel) {
+
                if self.fetch_refs_at(rid, from, refs, scope, timeout, channel) {
                    break;
                }
            } else {
@@ -1625,14 +1631,14 @@ where
                let repo_entry = self.policies.seed_policy(&message.rid).expect(
                    "Service::handle_announcement: error accessing repo seeding configuration",
                );
-
                if repo_entry.policy != Policy::Allow {
+
                let SeedingPolicy::Allow { scope } = repo_entry.policy else {
                    debug!(
                        target: "service",
                        "Ignoring refs announcement from {announcer}: repository {} isn't seeded (t={timestamp})",
                        message.rid
                    );
                    return Ok(None);
-
                }
+
                };
                // 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.
@@ -1645,14 +1651,8 @@ where
                    return Ok(relay);
                };
                // Finally, start the fetch.
-
                self.fetch_refs_at(
-
                    message.rid,
-
                    remote.id,
-
                    refs,
-
                    repo_entry.scope,
-
                    FETCH_TIMEOUT,
-
                    None,
-
                );
+
                self.fetch_refs_at(message.rid, remote.id, refs, scope, FETCH_TIMEOUT, None);
+

                return Ok(relay);
            }
            AnnouncementMessage::Node(
@@ -2202,7 +2202,7 @@ where

    /// Return a new filter object, based on our seeding policy.
    fn filter(&self) -> Filter {
-
        if self.config.policy == Policy::Allow {
+
        if self.config.seeding_policy.is_allow() {
            // TODO: Remove bits for blocked repos.
            Filter::default()
        } else {
@@ -2361,7 +2361,7 @@ where
        let missing = self
            .policies
            .seed_policies()?
-
            .filter_map(|t| (t.policy == Policy::Allow).then_some(t.rid))
+
            .filter_map(|t| (t.policy.is_allow()).then_some(t.rid))
            .filter(|rid| !inventory.contains(rid));

        for rid in missing {
@@ -2412,11 +2412,12 @@ where

    /// Try to maintain a target number of connections.
    fn maintain_connections(&mut self) {
-
        let PeerConfig::Dynamic { target } = self.config.peers else {
+
        let PeerConfig::Dynamic = self.config.peers else {
            return;
        };
        trace!(target: "service", "Maintaining connections..");

+
        let target = TARGET_OUTBOUND_PEERS;
        let now = self.clock;
        let outbound = self
            .sessions
modified radicle-node/src/test/peer.rs
@@ -24,7 +24,7 @@ use crate::runtime::Emitter;
use crate::service;
use crate::service::io::Io;
use crate::service::message::*;
-
use crate::service::policy::{Policy, Scope};
+
use crate::service::policy::{Scope, SeedingPolicy};
use crate::service::*;
use crate::storage::git::transport::remote;
use crate::storage::Inventory;
@@ -101,8 +101,7 @@ pub struct Config<G: Signer + 'static> {
    pub config: service::Config,
    pub db: Stores<node::Database>,
    pub local_time: LocalTime,
-
    pub policy: Policy,
-
    pub scope: Scope,
+
    pub policy: SeedingPolicy,
    pub signer: G,
    pub rng: fastrand::Rng,
    pub tmp: tempfile::TempDir,
@@ -121,8 +120,7 @@ impl Default for Config<MockSigner> {
            config: service::Config::test(Alias::from_str("mocky").unwrap()),
            db,
            local_time: LocalTime::now(),
-
            policy: Policy::default(),
-
            scope: Scope::default(),
+
            policy: SeedingPolicy::default(),
            signer,
            rng,
            tmp,
@@ -162,7 +160,7 @@ where
        mut config: Config<G>,
    ) -> Self {
        let policies = policy::Store::<policy::store::Write>::memory().unwrap();
-
        let mut policies = policy::Config::new(config.policy, config.scope, policies);
+
        let mut policies = policy::Config::new(config.policy, policies);
        let id = *config.signer.public_key();
        let ip = ip.into();
        let local_addr = net::SocketAddr::new(ip, config.rng.u16(..));
modified radicle-node/src/worker.rs
@@ -20,7 +20,7 @@ use radicle_fetch::FetchLimit;

use crate::runtime::{thread, Emitter, Handle};
use crate::service::policy;
-
use crate::service::policy::Policy;
+
use crate::service::policy::SeedingPolicy;
use crate::wire::StreamId;

pub use channels::{ChannelEvent, Channels};
@@ -34,9 +34,7 @@ pub struct Config {
    /// Configuration for performing fetched.
    pub fetch: FetchConfig,
    /// Default policy, if a policy for a specific node or repository was not found.
-
    pub policy: Policy,
-
    /// Default scope, if a scope for a specific repository was not found.
-
    pub scope: policy::Scope,
+
    pub policy: SeedingPolicy,
    /// Path to the policies database.
    pub policies_db: PathBuf,
}
@@ -284,7 +282,7 @@ impl Worker {
        let policy = self.policies.seed_policy(&rid)?.policy;
        let repo = self.storage.repository(rid)?;
        let doc = repo.identity_doc()?;
-
        if !doc.is_visible_to(&remote) || policy == Policy::Block {
+
        if !doc.is_visible_to(&remote) || policy.is_block() {
            Err(UploadError::Unauthorized(remote, rid))
        } else {
            Ok(())
@@ -357,11 +355,8 @@ impl Pool {
    ) -> Result<Self, policy::Error> {
        let mut pool = Vec::with_capacity(config.capacity);
        for i in 0..config.capacity {
-
            let policies = policy::Config::new(
-
                config.policy,
-
                config.scope,
-
                policy::Store::reader(&config.policies_db)?,
-
            );
+
            let policies =
+
                policy::Config::new(config.policy, policy::Store::reader(&config.policies_db)?);
            let worker = Worker {
                nid,
                tasks: tasks.clone(),
modified radicle/src/node/config.rs
@@ -4,13 +4,12 @@ use std::ops::Deref;

use cyphernet::addr::PeerAddr;
use localtime::LocalDuration;
+
use serde_json as json;

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

-
/// Target number of peers to maintain connections to.
-
pub const TARGET_OUTBOUND_PEERS: usize = 8;
/// Default number of workers to spawn.
pub const DEFAULT_WORKERS: usize = 8;

@@ -134,8 +133,7 @@ impl Default for Limits {
pub struct ConnectionLimits {
    /// Max inbound connections.
    pub inbound: usize,
-
    /// Max outbound connections. Note that this is higher than the *target* number
-
    /// in [`TARGET_OUTBOUND_PEERS`].
+
    /// Max outbound connections. Note that this can be higher than the *target* number.
    pub outbound: usize,
}

@@ -223,14 +221,12 @@ pub enum PeerConfig {
    /// Static peer set. Connect to the configured peers and maintain the connections.
    Static,
    /// Dynamic peer set.
-
    Dynamic { target: usize },
+
    Dynamic,
}

impl Default for PeerConfig {
    fn default() -> Self {
-
        Self::Dynamic {
-
            target: TARGET_OUTBOUND_PEERS,
-
        }
+
        Self::Dynamic
    }
}

@@ -261,14 +257,6 @@ pub enum AddressConfig {
    Forward,
}

-
/// Database configuration.
-
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
-
#[serde(rename_all = "camelCase")]
-
pub struct DbConfig {
-
    #[serde(default)]
-
    pub journal_mode: db::JournalMode,
-
}
-

/// Service configuration.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -288,9 +276,6 @@ pub struct Config {
    /// Specify the node's public addresses
    #[serde(default)]
    pub external_addresses: Vec<Address>,
-
    /// Database config.
-
    #[serde(default)]
-
    pub db: DbConfig,
    /// Global proxy.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub proxy: Option<net::SocketAddr>,
@@ -315,10 +300,10 @@ pub struct Config {
    pub workers: usize,
    /// Default seeding policy.
    #[serde(default)]
-
    pub policy: Policy,
-
    /// Default seeding scope.
-
    #[serde(default)]
-
    pub scope: Scope,
+
    pub seeding_policy: SeedingPolicy,
+
    /// Extra fields that aren't supported.
+
    #[serde(flatten, skip_serializing)]
+
    pub extra: json::Map<String, json::Value>,
}

impl Config {
@@ -336,7 +321,6 @@ impl Config {
            listen: vec![],
            connect: HashSet::default(),
            external_addresses: vec![],
-
            db: DbConfig::default(),
            network: Network::default(),
            proxy: None,
            onion: None,
@@ -344,8 +328,8 @@ impl Config {
            limits: Limits::default(),
            workers: DEFAULT_WORKERS,
            log: defaults::log(),
-
            policy: Policy::default(),
-
            scope: Scope::default(),
+
            seeding_policy: SeedingPolicy::default(),
+
            extra: json::Map::default(),
        }
    }

modified radicle/src/node/db.rs
@@ -48,10 +48,10 @@ pub enum Error {
pub enum JournalMode {
    /// "WAL" mode. Good for concurrent reads & writes, but keeps some extra files around.
    #[serde(rename = "wal")]
+
    #[default]
    WriteAheadLog,
    /// Default "rollback" mode. Certain writes may block reads.
    #[serde(alias = "rollback")]
-
    #[default]
    Rollback,
}

modified radicle/src/node/policy.rs
@@ -15,8 +15,15 @@ pub use super::{Alias, NodeId};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SeedPolicy {
    pub rid: RepoId,
-
    pub scope: Scope,
-
    pub policy: Policy,
+
    pub policy: SeedingPolicy,
+
}
+

+
impl std::ops::Deref for SeedPolicy {
+
    type Target = SeedingPolicy;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.policy
+
    }
}

/// Node following policy.
@@ -27,6 +34,66 @@ pub struct FollowPolicy {
    pub policy: Policy,
}

+
/// Default seeding policy. Applies when no repository policies for the given repo are found.
+
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+
#[serde(rename_all = "camelCase", tag = "default")]
+
pub enum SeedingPolicy {
+
    /// Allow seeding.
+
    Allow {
+
        /// Seeding scope.
+
        #[serde(default)]
+
        scope: Scope,
+
    },
+
    /// Block seeding.
+
    #[default]
+
    Block,
+
}
+

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

+
    /// Is this an "allow" policy.
+
    pub fn is_allow(&self) -> bool {
+
        matches!(self, Self::Allow { .. })
+
    }
+

+
    /// Is this a "block" policy.
+
    pub fn is_block(&self) -> bool {
+
        !self.is_allow()
+
    }
+

+
    /// Scope, if any.
+
    pub fn scope(&self) -> Option<Scope> {
+
        match self {
+
            Self::Allow { scope } => Some(*scope),
+
            Self::Block => None,
+
        }
+
    }
+
}
+

+
impl From<SeedingPolicy> for Policy {
+
    fn from(p: SeedingPolicy) -> Self {
+
        match p {
+
            SeedingPolicy::Block => Policy::Block,
+
            SeedingPolicy::Allow { .. } => Policy::Allow,
+
        }
+
    }
+
}
+

+
impl std::fmt::Display for SeedingPolicy {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(
+
            f,
+
            "{} ({})",
+
            Policy::from(*self),
+
            self.scope().unwrap_or_default()
+
        )
+
    }
+
}
+

/// Resource policy.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
modified radicle/src/node/policy/config.rs
@@ -6,13 +6,13 @@ use log::error;
use thiserror::Error;

use crate::crypto::PublicKey;
-
use crate::prelude::{NodeId, RepoId};
+
use crate::prelude::RepoId;
use crate::storage::{Namespaces, ReadRepository as _, ReadStorage, RepositoryError};

pub use crate::node::policy::store;
pub use crate::node::policy::store::Error;
pub use crate::node::policy::store::Store;
-
pub use crate::node::policy::{Alias, FollowPolicy, Policy, Scope, SeedPolicy};
+
pub use crate::node::policy::{Alias, FollowPolicy, Policy, Scope, SeedPolicy, SeedingPolicy};

#[derive(Debug, Error)]
pub enum NamespacesError {
@@ -45,9 +45,7 @@ pub enum NamespacesError {
/// Policies configuration.
pub struct Config<T> {
    /// Default policy, if a policy for a specific node or repository was not found.
-
    policy: Policy,
-
    /// Default scope, if a scope for a specific repository was not found.
-
    scope: Scope,
+
    policy: SeedingPolicy,
    /// Underlying configuration store.
    store: Store<T>,
}
@@ -58,7 +56,6 @@ impl<T> fmt::Debug for Config<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Config")
            .field("policy", &self.policy)
-
            .field("scope", &self.scope)
            .field("store", &self.store)
            .finish()
    }
@@ -66,34 +63,13 @@ impl<T> fmt::Debug for Config<T> {

impl<T> Config<T> {
    /// Create a new policy configuration.
-
    pub fn new(policy: Policy, scope: Scope, store: Store<T>) -> Self {
-
        Self {
-
            policy,
-
            scope,
-
            store,
-
        }
+
    pub fn new(policy: SeedingPolicy, store: Store<T>) -> Self {
+
        Self { policy, store }
    }

    /// Check if a repository is seeded.
    pub fn is_seeding(&self, rid: &RepoId) -> Result<bool, Error> {
-
        self.seed_policy(rid)
-
            .map(|entry| entry.policy == Policy::Allow)
-
    }
-

-
    /// Check if a node is followed.
-
    pub fn is_following(&self, nid: &NodeId) -> Result<bool, Error> {
-
        self.follow_policy(nid)
-
            .map(|entry| entry.policy == Policy::Allow)
-
    }
-

-
    /// Get a node's following information.
-
    /// Returns the default policy if the node isn't found.
-
    pub fn follow_policy(&self, nid: &NodeId) -> Result<FollowPolicy, Error> {
-
        Ok(self.store.follow_policy(nid)?.unwrap_or(FollowPolicy {
-
            nid: *nid,
-
            alias: None,
-
            policy: self.policy,
-
        }))
+
        self.seed_policy(rid).map(|entry| entry.policy.is_allow())
    }

    /// Get a repository's seeding information.
@@ -101,7 +77,6 @@ impl<T> Config<T> {
    pub fn seed_policy(&self, rid: &RepoId) -> Result<SeedPolicy, Error> {
        Ok(self.store.seed_policy(rid)?.unwrap_or(SeedPolicy {
            rid: *rid,
-
            scope: self.scope,
            policy: self.policy,
        }))
    }
@@ -120,37 +95,37 @@ impl<T> Config<T> {
            .seed_policy(rid)
            .map_err(|err| FailedPolicy { rid: *rid, err })?;
        match entry.policy {
-
            Policy::Block => {
+
            SeedingPolicy::Block => {
                error!(target: "service", "Attempted to fetch untracked repo {rid}");
                Err(NamespacesError::BlockedPolicy { rid: *rid })
            }
-
            Policy::Allow => match entry.scope {
-
                Scope::All => Ok(Namespaces::All),
-
                Scope::Followed => {
-
                    let nodes = self
-
                        .follow_policies()
-
                        .map_err(|err| FailedNodes { rid: *rid, err })?;
-
                    let mut followed: HashSet<_> = nodes
-
                        .filter_map(|node| (node.policy == Policy::Allow).then_some(node.nid))
-
                        .collect();
-

-
                    if let Ok(repo) = storage.repository(*rid) {
-
                        let delegates = repo
-
                            .delegates()
-
                            .map_err(|err| FailedDelegates { rid: *rid, err })?
-
                            .map(PublicKey::from);
-
                        followed.extend(delegates);
-
                    };
-
                    if followed.is_empty() {
-
                        // Nb. returning All here because the
-
                        // fetching logic will correctly determine
-
                        // followed and delegate remotes.
-
                        Ok(Namespaces::All)
-
                    } else {
-
                        Ok(Namespaces::Followed(followed))
-
                    }
+
            SeedingPolicy::Allow { scope: Scope::All } => Ok(Namespaces::All),
+
            SeedingPolicy::Allow {
+
                scope: Scope::Followed,
+
            } => {
+
                let nodes = self
+
                    .follow_policies()
+
                    .map_err(|err| FailedNodes { rid: *rid, err })?;
+
                let mut followed: HashSet<_> = nodes
+
                    .filter_map(|node| (node.policy == Policy::Allow).then_some(node.nid))
+
                    .collect();
+

+
                if let Ok(repo) = storage.repository(*rid) {
+
                    let delegates = repo
+
                        .delegates()
+
                        .map_err(|err| FailedDelegates { rid: *rid, err })?
+
                        .map(PublicKey::from);
+
                    followed.extend(delegates);
+
                };
+
                if followed.is_empty() {
+
                    // Nb. returning All here because the
+
                    // fetching logic will correctly determine
+
                    // followed and delegate remotes.
+
                    Ok(Namespaces::All)
+
                } else {
+
                    Ok(Namespaces::Followed(followed))
                }
-
            },
+
            }
        }
    }
}
modified radicle/src/node/policy/store.rs
@@ -9,7 +9,7 @@ use thiserror::Error;
use crate::node::{Alias, AliasStore};
use crate::prelude::{NodeId, RepoId};

-
use super::{FollowPolicy, Policy, Scope, SeedPolicy};
+
use super::{FollowPolicy, Policy, Scope, SeedPolicy, SeedingPolicy};

/// How long to wait for the database lock to be released before failing a read.
const DB_READ_TIMEOUT: time::Duration = time::Duration::from_secs(3);
@@ -218,10 +218,8 @@ impl<T> Store<T> {
    pub fn is_seeding(&self, id: &RepoId) -> Result<bool, Error> {
        Ok(matches!(
            self.seed_policy(id)?,
-
            Some(SeedPolicy {
-
                policy: Policy::Allow,
-
                ..
-
            })
+
            Some(SeedPolicy { policy, .. })
+
            if policy.is_allow()
        ))
    }

@@ -260,11 +258,13 @@ impl<T> Store<T> {
        stmt.bind((1, id))?;

        if let Some(Ok(row)) = stmt.into_iter().next() {
-
            return Ok(Some(SeedPolicy {
-
                rid: *id,
-
                scope: row.read::<Scope, _>("scope"),
-
                policy: row.read::<Policy, _>("policy"),
-
            }));
+
            let policy = match row.read::<Policy, _>("policy") {
+
                Policy::Allow => SeedingPolicy::Allow {
+
                    scope: row.read::<Scope, _>("scope"),
+
                },
+
                Policy::Block => SeedingPolicy::Block,
+
            };
+
            return Ok(Some(SeedPolicy { rid: *id, policy }));
        }
        Ok(None)
    }
@@ -307,14 +307,13 @@ impl<T> Store<T> {

        while let Some(Ok(row)) = stmt.next() {
            let id = row.read("id");
-
            let scope = row.read("scope");
-
            let policy = row.read::<Policy, _>("policy");
-

-
            entries.push(SeedPolicy {
-
                rid: id,
-
                scope,
-
                policy,
-
            });
+
            let policy = match row.read::<Policy, _>("policy") {
+
                Policy::Allow => SeedingPolicy::Allow {
+
                    scope: row.read::<Scope, _>("scope"),
+
                },
+
                Policy::Block => SeedingPolicy::Block,
+
            };
+
            entries.push(SeedPolicy { rid: id, policy });
        }
        Ok(Box::new(entries.into_iter()))
    }
@@ -415,9 +414,15 @@ mod test {
        let mut db = Store::open(":memory:").unwrap();

        assert!(db.seed(&id, Scope::All).unwrap());
-
        assert_eq!(db.seed_policy(&id).unwrap().unwrap().scope, Scope::All);
+
        assert_eq!(
+
            db.seed_policy(&id).unwrap().unwrap().scope(),
+
            Some(Scope::All)
+
        );
        assert!(db.seed(&id, Scope::Followed).unwrap());
-
        assert_eq!(db.seed_policy(&id).unwrap().unwrap().scope, Scope::Followed);
+
        assert_eq!(
+
            db.seed_policy(&id).unwrap().unwrap().scope(),
+
            Some(Scope::Followed)
+
        );
    }

    #[test]
@@ -426,9 +431,10 @@ mod test {
        let mut db = Store::open(":memory:").unwrap();

        assert!(db.seed(&id, Scope::All).unwrap());
-
        assert_eq!(db.seed_policy(&id).unwrap().unwrap().policy, Policy::Allow);
+
        assert!(db.seed_policy(&id).unwrap().unwrap().is_allow());
        assert!(db.set_seed_policy(&id, Policy::Block).unwrap());
-
        assert_eq!(db.seed_policy(&id).unwrap().unwrap().policy, Policy::Block);
+
        assert!(!db.seed_policy(&id).unwrap().unwrap().is_allow());
+
        assert_eq!(db.seed_policy(&id).unwrap().unwrap().scope(), None);
    }

    #[test]
modified radicle/src/profile.rs
@@ -15,6 +15,7 @@ use std::path::{Path, PathBuf};
use std::{fs, io};

use serde::Serialize;
+
use serde_json as json;
use thiserror::Error;

use crate::crypto::ssh::agent::Agent;
@@ -22,13 +23,16 @@ use crate::crypto::ssh::{keystore, Keystore, Passphrase};
use crate::crypto::{PublicKey, Signer};
use crate::explorer::Explorer;
use crate::node::policy::config::store::Read;
-
use crate::node::{notifications, policy, Alias, AliasStore};
-
use crate::prelude::Did;
-
use crate::prelude::NodeId;
+
use crate::node::{
+
    notifications, policy,
+
    policy::{Policy, Scope, SeedingPolicy},
+
    Alias, AliasStore,
+
};
+
use crate::prelude::{Did, NodeId};
use crate::storage::git::transport;
use crate::storage::git::Storage;
-
use crate::storage::{self, ReadRepository};
-
use crate::{cli, cob, git, node, web};
+
use crate::storage::ReadRepository;
+
use crate::{cli, cob, git, node, storage, web};

/// Environment variables used by radicle.
pub mod env {
@@ -229,12 +233,28 @@ impl Config {

    /// Load a configuration from the given path.
    pub fn load(path: &Path) -> Result<Self, ConfigError> {
-
        match fs::File::open(path) {
+
        let mut cfg: Self = match fs::File::open(path) {
            Ok(cfg) => {
-
                serde_json::from_reader(cfg).map_err(|e| ConfigError::Load(path.to_path_buf(), e))
+
                json::from_reader(cfg).map_err(|e| ConfigError::Load(path.to_path_buf(), e))?
+
            }
+
            Err(e) => return Err(ConfigError::Io(path.to_path_buf(), e)),
+
        };
+

+
        // Handle deprecated policy configuration.
+
        // Nb. This will override "seedingPolicy" if set! This code should be removed after 1.0.
+
        if let (Some(p), Some(s)) = (cfg.node.extra.get("policy"), cfg.node.extra.get("scope")) {
+
            if let (Ok(policy), Ok(scope)) = (
+
                json::from_value::<Policy>(p.clone()),
+
                json::from_value::<Scope>(s.clone()),
+
            ) {
+
                log::warn!(target: "radicle", "Overwriting `seedingPolicy` configuration");
+
                cfg.node.seeding_policy = match policy {
+
                    Policy::Allow => SeedingPolicy::Allow { scope },
+
                    Policy::Block => SeedingPolicy::Block,
+
                }
            }
-
            Err(e) => Err(ConfigError::Io(path.to_path_buf(), e)),
        }
+
        Ok(cfg)
    }

    /// Write configuration to disk.
@@ -243,8 +263,8 @@ impl Config {
            .create_new(true)
            .write(true)
            .open(path)?;
-
        let formatter = serde_json::ser::PrettyFormatter::with_indent(b"  ");
-
        let mut serializer = serde_json::Serializer::with_formatter(&file, formatter);
+
        let formatter = json::ser::PrettyFormatter::with_indent(b"  ");
+
        let mut serializer = json::Serializer::with_formatter(&file, formatter);

        self.serialize(&mut serializer)?;
        file.write_all(b"\n")?;
@@ -315,7 +335,6 @@ impl Profile {
                key: public_key,
            },
        )?;
-

        transport::local::register(storage.clone());

        Ok(Profile {
@@ -382,8 +401,7 @@ impl Profile {
    pub fn policies(&self) -> Result<policy::config::Config<Read>, policy::store::Error> {
        let path = self.node().join(node::POLICIES_DB_FILE);
        let config = policy::config::Config::new(
-
            self.config.node.policy,
-
            self.config.node.scope,
+
            self.config.node.seeding_policy,
            policy::store::Store::reader(path)?,
        );
        Ok(config)
@@ -629,10 +647,9 @@ impl Home {
#[cfg(test)]
#[cfg(not(target_os = "macos"))]
mod test {
+
    use super::*;
    use std::fs;

-
    use super::Home;
-

    // Checks that if we have:
    // '/run/user/1000/.tmpqfK6ih/../.tmpqfK6ih/Radicle/Home'
    //
@@ -656,4 +673,55 @@ mod test {

        assert_eq!(home.path, path);
    }
+

+
    #[test]
+
    fn test_config() {
+
        let cfg = json::from_value::<Config>(json::json!({
+
          "publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
+
          "preferredSeeds": [],
+
          "web": {
+
            "pinned": {
+
              "repositories": [
+
                "rad:z3TajuiHXifEDEX4qbJxe8nXr9ufi",
+
                "rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5"
+
              ]
+
            }
+
          },
+
          "cli": { "hints": true },
+
          "node": {
+
            "alias": "seed.radicle.xyz",
+
            "listen": [],
+
            "peers": { "type": "dynamic", "target": 8 },
+
            "connect": [
+
              "z6Mkmqogy2qEM2ummccUthFEaaHvyYmYBYh3dbe9W4ebScxo@ash.radicle.garden:8776",
+
              "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776"
+
            ],
+
            "externalAddresses": [ "seed.radicle.xyz:8776" ],
+
            "db": { "journalMode": "wal" },
+
            "network": "main",
+
            "log": "INFO",
+
            "relay": "always",
+
            "limits": {
+
              "routingMaxSize": 1000,
+
              "routingMaxAge": 604800,
+
              "gossipMaxAge": 604800,
+
              "fetchConcurrency": 1,
+
              "maxOpenFiles": 4096,
+
              "rate": {
+
                "inbound": { "fillRate": 10.0, "capacity": 2048 },
+
                "outbound": { "fillRate": 10.0, "capacity": 2048 }
+
              },
+
              "connection": { "inbound": 128, "outbound": 16 }
+
            },
+
            "workers": 32,
+
            "policy": "allow",
+
            "scope": "all"
+
          }
+
        }))
+
        .unwrap();
+

+
        assert!(cfg.node.extra.contains_key("db"));
+
        assert!(cfg.node.extra.contains_key("policy"));
+
        assert!(cfg.node.extra.contains_key("scope"));
+
    }
}