From 6291cae59296a04e7cd9d52cdeadc065e44a6fc9 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Mon, 23 Feb 2026 18:04:29 +0000 Subject: [PATCH] cli: add `rad config schema` to the `rad-config` test Adds the `rad config schema` to the `rad-config` test so that changes to the schema will result in errors, forcing the implementor to ensure the changes are correct. --- crates/radicle-cli/examples/rad-config.md | 616 ++++++++++++++++++++++ 1 file changed, 616 insertions(+) diff --git a/crates/radicle-cli/examples/rad-config.md b/crates/radicle-cli/examples/rad-config.md index 6e208b20..9de6ab34 100644 --- a/crates/radicle-cli/examples/rad-config.md +++ b/crates/radicle-cli/examples/rad-config.md @@ -57,6 +57,622 @@ $ rad config } ``` +The `rad config schema` command provides the JSON schema that can be used to +validate the JSON of the user configuration. + +``` +$ rad config schema +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Config", + "description": "Local radicle configuration.", + "type": "object", + "properties": { + "publicExplorer": { + "description": "Public explorer. This is used for generating links.", + "$ref": "#/$defs/Explorer", + "default": "https://app.radicle.xyz/nodes/$host/$rid$path" + }, + "preferredSeeds": { + "description": "Preferred seeds. These seeds will be used for explorer links/nand in other situations when a seed needs to be chosen.", + "type": "array", + "items": { + "$ref": "#/$defs/ConnectAddress" + }, + "default": [] + }, + "web": { + "description": "Web configuration.", + "$ref": "#/$defs/WebConfig", + "default": { + "pinned": { + "repositories": [] + } + } + }, + "cli": { + "description": "CLI configuration.", + "$ref": "#/$defs/CliConfig", + "default": { + "hints": true + } + }, + "node": { + "description": "Node configuration.", + "$ref": "#/$defs/NodeConfig" + } + }, + "required": [ + "node" + ], + "$defs": { + "Explorer": { + "description": "A public explorer.", + "type": "string" + }, + "ConnectAddress": { + "description": "A node address to connect to. Format: An Ed25519 public key in multibase encoding, followed by the symbol '@', followed by an IP address, or a DNS name, or a Tor onion name, followed by the symbol ':', followed by a TCP port number.", + "type": "string", + "examples": [ + "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@rosa.radicle.xyz:8776", + "z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C@xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776", + "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi@seed.example.com:8776", + "z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5@192.0.2.0:31337" + ], + "pattern": "^.+@.+:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$" + }, + "WebConfig": { + "description": "Web configuration.", + "type": "object", + "properties": { + "pinned": { + "description": "Pinned content.", + "$ref": "#/$defs/Pinned" + }, + "bannerUrl": { + "description": "URL pointing to an image used in the header of a node page.", + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "avatarUrl": { + "description": "URL pointing to an image used as the node avatar.", + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "description": { + "description": "Node description.", + "type": [ + "string", + "null" + ], + "format": "uri" + } + }, + "required": [ + "pinned" + ] + }, + "Pinned": { + "description": "Pinned content. This can be used to pin certain content when/nlisting, e.g. pin repositories on a web client.", + "type": "object", + "properties": { + "repositories": { + "description": "Pinned repositories.", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/RepoId" + } + } + }, + "required": [ + "repositories" + ] + }, + "RepoId": { + "description": "A repository identifier.", + "type": "string", + "examples": [ + "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5" + ], + "minLength": 5, + "pattern": "rad:z[1-9a-km-zA-HJ-NP-Z]+" + }, + "CliConfig": { + "description": "CLI configuration.", + "type": "object", + "properties": { + "hints": { + "description": "Whether to show hints or not in the CLI.", + "type": "boolean", + "default": false + } + } + }, + "NodeConfig": { + "description": "Service configuration.", + "type": "object", + "properties": { + "alias": { + "description": "Node alias.", + "$ref": "#/$defs/Alias" + }, + "listen": { + "description": "Socket address (a combination of IPv4 or IPv6 address and TCP port) to listen on.", + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + "127.0.0.1:8776" + ], + "default": [] + }, + "peers": { + "description": "Peer configuration.", + "$ref": "#/$defs/PeerConfig", + "default": { + "type": "dynamic" + } + }, + "connect": { + "description": "Peers to connect to on startup./nConnections to these peers will be maintained.", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/ConnectAddress" + }, + "default": [] + }, + "externalAddresses": { + "description": "Specify the node's public addresses", + "type": "array", + "items": { + "$ref": "#/$defs/Address" + }, + "default": [] + }, + "proxy": { + "description": "Global proxy.", + "type": [ + "string", + "null" + ] + }, + "onion": { + "description": "Onion address config.", + "anyOf": [ + { + "$ref": "#/$defs/AddressConfig" + }, + { + "type": "null" + } + ] + }, + "network": { + "description": "Peer-to-peer network.", + "$ref": "#/$defs/Network", + "default": "main" + }, + "log": { + "description": "Log level.", + "$ref": "#/$defs/LogLevel", + "default": "INFO" + }, + "relay": { + "description": "Whether or not our node should relay messages.", + "$ref": "#/$defs/Relay", + "default": "auto" + }, + "limits": { + "description": "Configured service limits.", + "$ref": "#/$defs/Limits", + "default": { + "routingMaxSize": 1000, + "routingMaxAge": 604800, + "gossipMaxAge": 1209600, + "fetchConcurrency": 1, + "maxOpenFiles": 4096, + "rate": { + "inbound": { + "fillRate": 5.0, + "capacity": 1024 + }, + "outbound": { + "fillRate": 10.0, + "capacity": 2048 + } + }, + "connection": { + "inbound": 128, + "outbound": 16 + }, + "fetchPackReceive": "500.0 MiB" + } + }, + "workers": { + "description": "Number of worker threads to spawn.", + "type": "integer", + "format": "uint", + "minimum": 0, + "default": 8 + }, + "seedingPolicy": { + "description": "Default seeding policy.", + "$ref": "#/$defs/DefaultSeedingPolicy", + "default": { + "default": "block" + } + }, + "secret": { + "description": "Path to a file containing an Ed25519 secret key, in OpenSSH format, i.e./nwith the `-----BEGIN OPENSSH PRIVATE KEY-----` header. The corresponding/npublic key will be used as the Node ID./n/nA decryption password cannot be configured, but passed at runtime via/nthe environment variable `RAD_PASSPHRASE`.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "alias" + ], + "additionalProperties": true + }, + "Alias": { + "description": "Node alias, i.e. a short and memorable name for it.", + "type": "string" + }, + "PeerConfig": { + "description": "Peer configuration.", + "oneOf": [ + { + "description": "Static peer set. Connect to the configured peers and maintain the connections.", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "static" + } + }, + "required": [ + "type" + ] + }, + { + "description": "Dynamic peer set.", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "dynamic" + } + }, + "required": [ + "type" + ] + } + ] + }, + "Address": { + "description": "An IP address, or a DNS name, or a Tor onion name, followed by the symbol ':', followed by a TCP port number.", + "type": "string", + "examples": [ + "xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776", + "seed.example.com:8776", + "192.0.2.0:31337" + ], + "pattern": "^.+:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$" + }, + "AddressConfig": { + "description": "Proxy configuration.", + "oneOf": [ + { + "description": "Proxy connections to this address type.", + "type": "object", + "properties": { + "address": { + "description": "Proxy address.", + "type": "string" + }, + "mode": { + "type": "string", + "const": "proxy" + } + }, + "required": [ + "mode", + "address" + ] + }, + { + "description": "Forward address to the next layer. Either this is the global proxy,/nor the operating system, via DNS.", + "type": "object", + "properties": { + "mode": { + "type": "string", + "const": "forward" + } + }, + "required": [ + "mode" + ] + } + ] + }, + "Network": { + "description": "Peer-to-peer network.", + "type": "string", + "enum": [ + "main", + "test" + ] + }, + "LogLevel": { + "$ref": "#/$defs/Level" + }, + "Level": { + "description": "A log level.", + "oneOf": [ + { + "description": "Designates very serious errors.", + "type": "string", + "const": "ERROR" + }, + { + "description": "Designates hazardous situations.", + "type": "string", + "const": "WARN" + }, + { + "description": "Designates useful information.", + "type": "string", + "const": "INFO" + }, + { + "description": "Designates lower priority information.", + "type": "string", + "const": "DEBUG" + }, + { + "description": "Designates very low priority, often extremely verbose, information.", + "type": "string", + "const": "TRACE" + } + ] + }, + "Relay": { + "description": "Relay configuration.", + "oneOf": [ + { + "description": "Always relay messages.", + "type": "string", + "const": "always" + }, + { + "description": "Never relay messages.", + "type": "string", + "const": "never" + }, + { + "description": "Relay messages when applicable.", + "type": "string", + "const": "auto" + } + ] + }, + "Limits": { + "description": "Configuration parameters defining attributes of minima and maxima.", + "type": "object", + "properties": { + "routingMaxSize": { + "description": "Number of routing table entries before we start pruning.", + "type": "integer", + "format": "uint", + "minimum": 0, + "default": 1000 + }, + "routingMaxAge": { + "description": "How long to keep a routing table entry before being pruned.", + "$ref": "#/$defs/LocalDuration", + "default": 604800 + }, + "gossipMaxAge": { + "description": "How long to keep a gossip message entry before pruning it.", + "$ref": "#/$defs/LocalDuration", + "default": 1209600 + }, + "fetchConcurrency": { + "description": "Maximum number of concurrent fetches per peer connection.", + "type": "integer", + "format": "uint", + "minimum": 0, + "default": 1 + }, + "maxOpenFiles": { + "description": "Maximum number of open files.", + "type": "integer", + "format": "uint", + "minimum": 0, + "default": 4096 + }, + "rate": { + "description": "Rate limiter settings.", + "$ref": "#/$defs/RateLimits", + "default": { + "inbound": { + "fillRate": 5.0, + "capacity": 1024 + }, + "outbound": { + "fillRate": 10.0, + "capacity": 2048 + } + } + }, + "connection": { + "description": "Connection limits.", + "$ref": "#/$defs/ConnectionLimits", + "default": { + "inbound": 128, + "outbound": 16 + } + }, + "fetchPackReceive": { + "description": "Channel limits.", + "$ref": "#/$defs/FetchPackSizeLimit", + "default": "500.0 MiB" + } + } + }, + "LocalDuration": { + "description": "A time duration measured locally in seconds.", + "type": "integer", + "format": "uint128", + "minimum": 0 + }, + "RateLimits": { + "description": "Rate limits for inbound and outbound connections.", + "type": "object", + "properties": { + "inbound": { + "$ref": "#/$defs/RateLimit" + }, + "outbound": { + "$ref": "#/$defs/RateLimit" + } + }, + "required": [ + "inbound", + "outbound" + ] + }, + "RateLimit": { + "description": "Rate limits for a single connection.", + "type": "object", + "properties": { + "fillRate": { + "type": "number", + "format": "double" + }, + "capacity": { + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "fillRate", + "capacity" + ] + }, + "ConnectionLimits": { + "description": "Connection limits.", + "type": "object", + "properties": { + "inbound": { + "description": "Max inbound connections.", + "type": "integer", + "format": "uint", + "minimum": 0, + "default": 128 + }, + "outbound": { + "description": "Max outbound connections. Note that this can be higher than the *target* number.", + "type": "integer", + "format": "uint", + "minimum": 0, + "default": 16 + } + } + }, + "FetchPackSizeLimit": { + "description": "Limiter for byte streams./n/nDefault: 500MiB", + "$ref": "#/$defs/ByteSize" + }, + "ByteSize": { + "description": "Byte quantities using unit prefixes according to SI or ISO/IEC 80000-13.", + "type": "string", + "pattern": "^//d+(//.//d+)? ((K|M|G|T|P)i?B?|B)$", + "examples": [ + "7 G", + "50.3 TiB", + "200 B", + "4 Ki", + "10 MB" + ] + }, + "DefaultSeedingPolicy": { + "description": "Default seeding policy. Applies when no repository policies for the given repo are found.", + "oneOf": [ + { + "description": "Allow seeding.", + "type": "object", + "properties": { + "scope": { + "description": "Seeding scope.", + "$ref": "#/$defs/Scope" + }, + "default": { + "type": "string", + "const": "allow" + } + }, + "required": [ + "default", + "scope" + ] + }, + { + "description": "Block seeding.", + "type": "object", + "properties": { + "default": { + "type": "string", + "const": "block" + } + }, + "required": [ + "default" + ] + } + ] + }, + "Scope": { + "description": "[`Scope`] provides a schema for [`policy::Scope`], where the inner scope is/noptional. It is introduced to allow ease migration to a future/nversion of [`DefaultSeedingPolicy::Allow`], where no or different defaults/napply to [`DefaultSeedingPolicy::Allow::scope`].", + "anyOf": [ + { + "$ref": "#/$defs/Scope2" + }, + { + "type": "null" + } + ] + }, + "Scope2": { + "description": "Follow scope of a seeded repository.", + "oneOf": [ + { + "description": "Seed remotes that are explicitly followed.", + "type": "string", + "const": "followed" + }, + { + "description": "Seed all remotes.", + "type": "string", + "const": "all" + } + ] + } + } +} +``` + You can also get any value in the configuration by path, eg. ```