1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 11:36:05 +01:00

radicle: init (#5409)

Co-authored-by: Matthias Beyer <mail@beyermatthias.de>
Co-authored-by: Austin Horstman <khaneliman12@gmail.com>
This commit is contained in:
Lorenz Leutgeb 2025-10-26 21:34:09 +01:00 committed by GitHub
parent 1830716059
commit 2aaf924e82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 547 additions and 0 deletions

View file

@ -0,0 +1,12 @@
{ pkgs, ... }:
{
time = "2025-09-08T18:49:30+00:00";
condition = pkgs.stdenv.hostPlatform.isLinux;
message = ''
A new module is available: 'programs.radicle'.
A new service is available: 'services.radicle.node'.
Radicle is a distributed code forge built on Git.
Since it is possible to interact with Radicle storage without running the service, two modules were introduced.
'';
}

View file

@ -0,0 +1,269 @@
{
config,
options,
lib,
pkgs,
...
}:
let
inherit (lib)
attrValues
filter
filterAttrs
getExe
hasSuffix
head
id
length
listToAttrs
mkDefault
mkEnableOption
mkIf
mkOption
mkPackageOption
replaceStrings
;
inherit (lib.types)
listOf
nullOr
str
submodule
;
cfg = config.programs.radicle;
opt = options.programs.radicle;
configFile = rec {
format = pkgs.formats.json { };
name = "config.json";
path = pkgs.runCommand name { nativeBuildInputs = [ cfg.cli.package ]; } ''
mkdir keys
echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID/this/is/not/a/real/key/only/a/placeholder" \
> keys/radicle.pub
cp ${format.generate name cfg.settings} ${name}
RAD_HOME=$PWD rad config
cp ${name} $out
'';
};
publicExplorerSuffix = "$rid$path";
in
{
options = {
programs.radicle = {
enable = mkEnableOption "Radicle";
cli.package = mkPackageOption pkgs "radicle-node" { };
uri = {
rad = {
browser = {
enable = mkOption {
description = "Whether to enable `rad:`-URI handling by web browser";
default =
(hasSuffix publicExplorerSuffix cfg.settings.publicExplorer) && pkgs.stdenv.hostPlatform.isLinux;
defaultText = "`true` if a suitable public explorer is detected.";
example = false;
};
preferredNode = mkOption {
type = str;
description = "The hostname of an instance of `radicle-node`, reachable via HTTPS.";
default = "iris.radicle.xyz";
example = "radicle-node.example.com";
};
};
vscode = {
enable = mkEnableOption "`rad:`-URI handling by VSCode";
extension = mkOption {
type = str;
description = "The unique identifier of the VSCode extension that should handle `rad:`-URIs.";
default = "radicle-ide-plugins-team.radicle";
};
};
};
web-rad =
let
detected =
let
detectionList = attrValues (
filterAttrs (n: _: config.programs.${n}.enable) {
librewolf = "librewolf.desktop";
firefox = "firefox.desktop";
chromium = "chromium-browser.desktop";
}
);
in
lib.optionals (detectionList == [ ]) (head detectionList);
in
{
enable = mkEnableOption "`web+rad:`-URI handling by web browser";
browser = mkOption {
description = ''
Name of the XDG Desktop Entry for your browser.
LibreWolf, Firefox and Chromium configured via home-manager will
be detected automatically. The value of this option should likely
be the same as the output of
`xdg-mime query default x-scheme-handler/https`.
'';
type = nullOr str;
default = detected;
defaultText = "Automatically detected browser.";
example = "brave.desktop";
};
};
};
settings = mkOption {
default = { };
description = "Radicle configuration, written to `~/.radicle/config.json.";
type = submodule {
freeformType = configFile.format.type;
options = {
publicExplorer = mkOption {
type = str;
description = "HTTPS URL pattern used to generate links to view content on Radicle via the browser.";
default = "https://app.radicle.xyz/nodes/$host/$rid$path";
example = "https://radicle.example.com/nodes/seed.example.com/$rid$path";
};
node = {
alias = mkOption {
type = str;
description = "Human readable alias for your node.";
default = config.home.username;
defaultText = lib.literalExpression "config.home.username";
};
listen = mkOption {
type = listOf str;
description = "Addresses to bind to and listen for inbound connections.";
default = [ ];
example = [ "127.0.0.1:58776" ];
};
};
};
};
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion =
!pkgs.hostPlatform.isLinux
-> (
(filter id [
cfg.uri.rad.browser.enable
cfg.uri.rad.vscode.enable
]) == [ ]
);
message = "`rad:`-URI handlers are only supported on Linux.";
}
{
assertion = cfg.uri.web-rad.enable -> cfg.uri.web-rad.browser != null;
message = "Could not detect preferred browser. Please set `${builtins.toString opt.uri.web-rad.browser}`.";
}
{
assertion =
1 >= length (
filter id [
cfg.uri.rad.browser.enable
cfg.uri.rad.vscode.enable
]
);
message = "At most one `rad:`-URI handler may be enabled.";
}
{
assertion =
cfg.uri.rad.browser.enable -> hasSuffix publicExplorerSuffix cfg.settings.publicExplorer;
message = "${opt.uri.rad.browser.enable} is only compatible with a public explorer URL ending in '${publicExplorerSuffix}' but '${cfg.settings.publicExplorer}' does not end with '${publicExplorerSuffix}'.";
}
];
home = {
packages = [ cfg.cli.package ];
file.".radicle/${configFile.name}".source = configFile.path;
};
xdg = {
mimeApps.defaultApplications = {
"x-scheme-handler/rad" =
let
isEnabled = cfg.uri.rad.browser.enable || cfg.uri.rad.vscode.enable;
handlerTarget =
if cfg.uri.rad.browser.enable then
"rad-to-browser.desktop"
else if cfg.uri.rad.vscode.enable then
"rad-to-vscode.desktop"
else
throw "unreachable";
handler = mkDefault handlerTarget;
in
mkIf isEnabled handler;
"x-scheme-handler/web+rad" = mkIf cfg.uri.web-rad.enable (mkDefault cfg.uri.web-rad.browser);
};
desktopEntries =
let
mkHandler =
{
name,
shortName,
prefix,
}:
{
name = "Open Radicle URIs with ${name}";
genericName = "Code Forge";
categories = [
"Development"
"RevisionControl"
];
exec = getExe (
pkgs.writeShellApplication {
name = "rad-to-${shortName}";
meta.mainProgram = "rad-to-${shortName}";
text = ''xdg-open "${prefix}$1"'';
}
);
mimeType = [ "x-scheme-handler/rad" ];
noDisplay = true;
};
toHandler = v: {
name = "rad-to-${v.shortName}";
value = mkIf cfg.uri.rad.${v.shortName}.enable (mkHandler v);
};
in
listToAttrs (
map toHandler [
{
name = "Web Browser";
shortName = "browser";
prefix =
replaceStrings
[ "$host" publicExplorerSuffix ]
[
cfg.uri.rad.browser.preferredNode
""
]
cfg.settings.publicExplorer;
}
{
name = "VSCode";
shortName = "vscode";
prefix = "vscode://${cfg.uri.rad.vscode.extension}/";
}
]
);
};
};
meta.maintainers = with lib.maintainers; [
lorenzleutgeb
matthiasbeyer
];
}

View file

@ -0,0 +1,245 @@
{
config,
lib,
pkgs,
options,
...
}:
let
inherit (lib)
generators
getBin
getExe'
last
mapAttrsToList
mkDefault
mkEnableOption
mkIf
mkOption
mkMerge
mkPackageOption
splitString
;
inherit (lib.types)
attrsOf
nullOr
oneOf
package
path
str
;
cfg = config.services.radicle;
radicleHome = config.home.homeDirectory + "/.radicle";
gitPath = [ "PATH=${getBin pkgs.gitMinimal}/bin" ];
env = attrs: (mapAttrsToList (generators.mkKeyValueDefault { } "=") attrs) ++ gitPath;
in
{
options = {
services.radicle = {
node = {
enable = mkEnableOption "Radicle Node";
package = mkPackageOption pkgs "radicle-node" { };
args = mkOption {
type = str;
description = "Additional command line arguments to pass when executing `radicle-node`.";
default = "";
example = "--force";
};
environment = mkOption {
type = attrsOf (
nullOr (oneOf [
str
path
package
])
);
description = "Environment to set when executing `radicle-node`.";
default = { };
example = {
"RUST_BACKTRACE" = "full";
};
};
lazy = {
enable = mkEnableOption "a proxy service to lazily start and stop Radicle Node on demand";
exitIdleTime = mkOption {
type = str;
description = "The idle time after which no interaction with Radicle Node via the `rad` CLI should be stopped, in a format that {manpage}`systemd-socket-proxyd(8)` understands for its `--exit-idle-time` argument.";
default = "30min";
example = "1h";
};
};
};
};
};
config = mkIf cfg.node.enable {
systemd.user = {
services = {
"radicle-node" =
let
keyFile = name: "${radicleHome}/keys/${name}";
keyPair = name: [
(keyFile name)
(keyFile (name + ".pub"))
];
radicleKeyPair = keyPair "radicle";
in
{
Unit = {
Description = "Radicle Node";
Documentation = [
"https://radicle.xyz/guides"
"man:radicle-node(1)"
];
StopWhenUnneeded = cfg.node.lazy.enable;
ConditionPathExists = radicleKeyPair;
};
Service = mkMerge ([
{
Slice = "session.slice";
ExecStart = "${getExe' cfg.node.package "radicle-node"} ${cfg.node.args}";
Environment = env cfg.node.environment;
KillMode = "process";
Restart = "no";
RestartSec = "2";
RestartSteps = "100";
RestartMaxDelaySec = "1min";
}
{
# Hardening
BindPaths = [
"${radicleHome}/storage"
"${radicleHome}/node"
"${radicleHome}/cobs"
];
BindReadOnlyPaths = [
"${radicleHome}/config.json"
"${radicleHome}/keys"
"-/etc/resolv.conf"
"/run/systemd"
];
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
AmbientCapabilities = "";
CapabilityBoundingSet = "";
NoNewPrivileges = true;
DeviceAllow = ""; # ProtectClock= adds DeviceAllow=char-rtc r
KeyringMode = "private";
LockPersonality = true;
MemoryDenyWriteExecute = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = "self";
ProcSubset = "pid";
ProtectClock = true;
ProtectHome = "tmpfs";
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RuntimeDirectoryMode = "0700";
SocketBindDeny = [ "any" ];
SocketBindAllow = map (
addr: "tcp:${last (splitString ":" addr)}"
) config.programs.radicle.settings.node.listen;
StateDirectoryMode = "0750";
UMask = "0067";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@aio"
"~@chown"
"~@keyring"
"~@memlock"
"~@privileged"
"~@resources"
"~@setuid"
];
}
]);
};
"radicle-node-proxy" = mkIf cfg.node.lazy.enable {
Unit = {
Description = "Radicle Node Proxy";
BindsTo = [
"radicle-node-proxy.socket"
"radicle-node.service"
];
After = [
"radicle-node-proxy.socket"
"radicle-node.service"
];
Documentation = [ "man:systemd-socket-proxyd(8)" ];
};
Service = {
ExecSearchPath = "${pkgs.systemd}/lib/systemd";
ExecStart = "systemd-socket-proxyd --exit-idle-time=${cfg.node.lazy.exitIdleTime} %t/radicle-node/proxy.sock";
PrivateTmp = "yes";
PrivateNetwork = "yes";
RuntimeDirectory = "radicle";
RuntimeDirectoryPreserve = "yes";
};
};
};
sockets = mkIf cfg.node.lazy.enable {
"radicle-node-control" = {
Unit = {
Description = "Radicle Node Control Socket";
Documentation = [ "man:radicle-node(1)" ];
};
Socket = {
Service = "radicle-node-proxy.service";
ListenStream = "%t/radicle-node/control.sock";
RuntimeDirectory = "radicle-node";
RuntimeDirectoryPreserve = "yes";
};
Install.WantedBy = [ "sockets.target" ];
};
"radicle-node-proxy" = {
Unit = {
Description = "Radicle Node Proxy Socket";
Documentation = [ "man:systemd-socket-proxyd(8)" ];
};
Socket = {
Service = "radicle-node.service";
FileDescriptorName = "control";
ListenStream = "%t/radicle-node/proxy.sock";
RuntimeDirectory = "radicle-node";
RuntimeDirectoryPreserve = "yes";
};
Install.WantedBy = [ "sockets.target" ];
};
};
};
programs.radicle.enable = mkDefault true;
home.sessionVariables = mkIf cfg.node.lazy.enable {
RAD_SOCKET = "\${XDG_RUNTIME_DIR:-/run/user/$UID}/radicle-node/control.sock";
};
};
meta.maintainers = with lib.maintainers; [
lorenzleutgeb
matthiasbeyer
];
}