style: nix fmt (nixfmt-tree) + exclude generated config options

This commit is contained in:
joshp123 2026-02-14 23:24:11 -08:00
parent f8681411dd
commit c3d3be60ac
20 changed files with 908 additions and 664 deletions

View File

@ -6,11 +6,18 @@
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
}; };
outputs = { self, nixpkgs, flake-utils }: outputs =
flake-utils.lib.eachDefaultSystem (system: {
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
in { in
{
packages.default = pkgs.buildGoModule { packages.default = pkgs.buildGoModule {
pname = "hello-world"; pname = "hello-world";
version = "0.1.0"; version = "0.1.0";
@ -27,8 +34,8 @@
skills = [ ./skills/hello-world ]; skills = [ ./skills/hello-world ];
packages = [ self.packages.${system}.default ]; packages = [ self.packages.${system}.default ];
needs = { needs = {
stateDirs = []; stateDirs = [ ];
requiredEnv = []; requiredEnv = [ ];
}; };
}; };
} }

View File

@ -51,7 +51,11 @@
}; };
in in
{ {
formatter = pkgs.nixfmt-tree; formatter = pkgs.nixfmt-tree.override {
settings = {
global.excludes = [ "nix/generated/openclaw-config-options.nix" ];
};
};
packages = packageSetStable // { packages = packageSetStable // {
default = packageSetStable.openclaw; default = packageSetStable.openclaw;

View File

@ -1,86 +1,97 @@
{ lib, pkgs, stdenv, nodejs_22, openclawGateway }: {
lib,
pkgs,
stdenv,
nodejs_22,
openclawGateway,
}:
let let
stubModule = { lib, ... }: { stubModule =
options = { { lib, ... }:
assertions = lib.mkOption { {
type = lib.types.listOf lib.types.attrs; options = {
default = []; assertions = lib.mkOption {
}; type = lib.types.listOf lib.types.attrs;
default = [ ];
};
home.homeDirectory = lib.mkOption { home.homeDirectory = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "/tmp"; default = "/tmp";
}; };
home.packages = lib.mkOption { home.packages = lib.mkOption {
type = lib.types.listOf lib.types.anything; type = lib.types.listOf lib.types.anything;
default = []; default = [ ];
}; };
home.file = lib.mkOption { home.file = lib.mkOption {
type = lib.types.attrs; type = lib.types.attrs;
default = {}; default = { };
}; };
home.activation = lib.mkOption { home.activation = lib.mkOption {
type = lib.types.attrs; type = lib.types.attrs;
default = {}; default = { };
}; };
launchd.agents = lib.mkOption { launchd.agents = lib.mkOption {
type = lib.types.attrs; type = lib.types.attrs;
default = {}; default = { };
}; };
systemd.user.services = lib.mkOption { systemd.user.services = lib.mkOption {
type = lib.types.attrs; type = lib.types.attrs;
default = {}; default = { };
}; };
programs.git.enable = lib.mkOption { programs.git.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
}; };
lib = lib.mkOption { lib = lib.mkOption {
type = lib.types.attrs; type = lib.types.attrs;
default = {}; default = { };
};
}; };
}; };
};
moduleEval = lib.evalModules { moduleEval = lib.evalModules {
modules = [ modules = [
stubModule stubModule
../modules/home-manager/openclaw.nix ../modules/home-manager/openclaw.nix
({ lib, ... }: { (
config = { { lib, ... }:
home.homeDirectory = "/tmp"; {
programs.git.enable = false; config = {
lib.file.mkOutOfStoreSymlink = path: path; home.homeDirectory = "/tmp";
programs.openclaw = { programs.git.enable = false;
enable = true; lib.file.mkOutOfStoreSymlink = path: path;
launchd.enable = false; programs.openclaw = {
systemd.enable = false; enable = true;
instances.default = {}; launchd.enable = false;
config = { systemd.enable = false;
gateway = { instances.default = { };
bind = "tailnet"; config = {
auth = { gateway = {
mode = "token"; bind = "tailnet";
token = "test-token"; auth = {
}; mode = "token";
reload = { token = "test-token";
mode = "hot"; };
debounceMs = 500; reload = {
mode = "hot";
debounceMs = 500;
};
}; };
discovery.mdns.mode = "minimal";
}; };
discovery.mdns.mode = "minimal";
}; };
}; };
}; }
}) )
]; ];
specialArgs = { inherit pkgs; }; specialArgs = { inherit pkgs; };
}; };

View File

@ -1,71 +1,80 @@
{ lib, pkgs, stdenv }: {
lib,
pkgs,
stdenv,
}:
let let
stubModule = { lib, ... }: { stubModule =
options = { { lib, ... }:
assertions = lib.mkOption { {
type = lib.types.listOf lib.types.attrs; options = {
default = []; assertions = lib.mkOption {
}; type = lib.types.listOf lib.types.attrs;
default = [ ];
};
home.homeDirectory = lib.mkOption { home.homeDirectory = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "/tmp"; default = "/tmp";
}; };
home.packages = lib.mkOption { home.packages = lib.mkOption {
type = lib.types.listOf lib.types.anything; type = lib.types.listOf lib.types.anything;
default = []; default = [ ];
}; };
home.file = lib.mkOption { home.file = lib.mkOption {
type = lib.types.attrs; type = lib.types.attrs;
default = {}; default = { };
}; };
home.activation = lib.mkOption { home.activation = lib.mkOption {
type = lib.types.attrs; type = lib.types.attrs;
default = {}; default = { };
}; };
launchd.agents = lib.mkOption { launchd.agents = lib.mkOption {
type = lib.types.attrs; type = lib.types.attrs;
default = {}; default = { };
}; };
systemd.user.services = lib.mkOption { systemd.user.services = lib.mkOption {
type = lib.types.attrs; type = lib.types.attrs;
default = {}; default = { };
}; };
programs.git.enable = lib.mkOption { programs.git.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
}; };
lib = lib.mkOption { lib = lib.mkOption {
type = lib.types.attrs; type = lib.types.attrs;
default = {}; default = { };
};
}; };
}; };
};
eval = lib.evalModules { eval = lib.evalModules {
modules = [ modules = [
stubModule stubModule
../modules/home-manager/openclaw.nix ../modules/home-manager/openclaw.nix
({ lib, ... }: { (
config = { { lib, ... }:
home.homeDirectory = "/tmp"; {
programs.git.enable = false; config = {
lib.file.mkOutOfStoreSymlink = path: path; home.homeDirectory = "/tmp";
programs.openclaw = { programs.git.enable = false;
enable = true; lib.file.mkOutOfStoreSymlink = path: path;
launchd.enable = false; programs.openclaw = {
systemd.enable = true; enable = true;
launchd.enable = false;
systemd.enable = true;
};
}; };
}; }
}) )
]; ];
specialArgs = { inherit pkgs; }; specialArgs = { inherit pkgs; };
}; };

View File

@ -8,7 +8,8 @@ in
pkgs.testers.nixosTest { pkgs.testers.nixosTest {
name = "openclaw-hm-activation"; name = "openclaw-hm-activation";
nodes.machine = { ... }: nodes.machine =
{ ... }:
{ {
imports = [ home-manager.nixosModules.home-manager ]; imports = [ home-manager.nixosModules.home-manager ];
@ -23,7 +24,8 @@ pkgs.testers.nixosTest {
home-manager = { home-manager = {
useGlobalPkgs = true; useGlobalPkgs = true;
useUserPackages = true; useUserPackages = true;
users.alice = { lib, ... }: users.alice =
{ lib, ... }:
{ {
imports = [ openclawModule ]; imports = [ openclawModule ];

View File

@ -1,4 +1,8 @@
{ lib, stdenv, openclawGateway }: {
lib,
stdenv,
openclawGateway,
}:
stdenv.mkDerivation { stdenv.mkDerivation {
pname = "openclaw-package-contents"; pname = "openclaw-package-contents";

View File

@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
openclawLib = import ./lib.nix { inherit config lib pkgs; }; openclawLib = import ./lib.nix { inherit config lib pkgs; };
@ -19,7 +24,7 @@ let
launchd = cfg.launchd; launchd = cfg.launchd;
systemd = cfg.systemd; systemd = cfg.systemd;
plugins = openclawLib.effectivePlugins; plugins = openclawLib.effectivePlugins;
config = {}; config = { };
appDefaults = { appDefaults = {
enable = true; enable = true;
attachExistingOnly = true; attachExistingOnly = true;
@ -32,20 +37,38 @@ let
}; };
}; };
instances = if cfg.instances != {} instances =
then cfg.instances if cfg.instances != { } then
else lib.optionalAttrs cfg.enable { default = defaultInstance; }; cfg.instances
else
lib.optionalAttrs cfg.enable { default = defaultInstance; };
enabledInstances = lib.filterAttrs (_: inst: inst.enable) instances; enabledInstances = lib.filterAttrs (_: inst: inst.enable) instances;
plugins = import ./plugins.nix { inherit lib pkgs openclawLib enabledInstances; }; plugins = import ./plugins.nix {
inherit
files = import ./files.nix { lib
inherit config lib pkgs openclawLib enabledInstances plugins; pkgs
openclawLib
enabledInstances
;
}; };
stripNulls = value: files = import ./files.nix {
if value == null then null inherit
config
lib
pkgs
openclawLib
enabledInstances
plugins
;
};
stripNulls =
value:
if value == null then
null
else if builtins.isAttrs value then else if builtins.isAttrs value then
lib.filterAttrs (_: v: v != null) (builtins.mapAttrs (_: stripNulls) value) lib.filterAttrs (_: v: v != null) (builtins.mapAttrs (_: stripNulls) value)
else if builtins.isList value then else if builtins.isList value then
@ -59,154 +82,183 @@ let
}; };
}; };
mkInstanceConfig = name: inst: let mkInstanceConfig =
gatewayPackage = name: inst:
if inst.gatewayPath != null then let
pkgs.callPackage ../../packages/openclaw-gateway.nix { gatewayPackage =
gatewaySrc = builtins.path { if inst.gatewayPath != null then
path = inst.gatewayPath; pkgs.callPackage ../../packages/openclaw-gateway.nix {
name = "openclaw-gateway-src"; gatewaySrc = builtins.path {
}; path = inst.gatewayPath;
pnpmDepsHash = inst.gatewayPnpmDepsHash; name = "openclaw-gateway-src";
} };
else pnpmDepsHash = inst.gatewayPnpmDepsHash;
inst.package; }
pluginPackages = plugins.pluginPackagesFor name;
pluginEnvAll = plugins.pluginEnvAllFor name;
mergedConfig0 = stripNulls (lib.recursiveUpdate (lib.recursiveUpdate baseConfig cfg.config) inst.config);
existingWorkspace = (((mergedConfig0.agents or {}).defaults or {}).workspace or null);
mergedConfig =
if (cfg.workspace.pinAgentDefaults or true) && existingWorkspace == null then
lib.recursiveUpdate mergedConfig0 { agents = { defaults = { workspace = inst.workspaceDir; }; }; }
else
mergedConfig0;
configJson = builtins.toJSON mergedConfig;
configFile = pkgs.writeText "openclaw-${name}.json" configJson;
gatewayWrapper = pkgs.writeShellScriptBin "openclaw-gateway-${name}" ''
set -euo pipefail
if [ -n "${lib.makeBinPath pluginPackages}" ]; then
export PATH="${lib.makeBinPath pluginPackages}:$PATH"
fi
${lib.concatStringsSep "\n" (map (entry:
let
isFile = lib.hasSuffix "_FILE" entry.key;
in ''
if [ -f "${entry.value}" ]; then
if ${if isFile then "true" else "false"}; then
export ${entry.key}="${entry.value}"
else
rawValue="$("${lib.getExe' pkgs.coreutils "cat"}" "${entry.value}")"
if [ "''${rawValue#${entry.key}=}" != "$rawValue" ]; then
export ${entry.key}="''${rawValue#${entry.key}=}"
else
export ${entry.key}="$rawValue"
fi
fi
else else
export ${entry.key}="${entry.value}" inst.package;
pluginPackages = plugins.pluginPackagesFor name;
pluginEnvAll = plugins.pluginEnvAllFor name;
mergedConfig0 = stripNulls (
lib.recursiveUpdate (lib.recursiveUpdate baseConfig cfg.config) inst.config
);
existingWorkspace = (((mergedConfig0.agents or { }).defaults or { }).workspace or null);
mergedConfig =
if (cfg.workspace.pinAgentDefaults or true) && existingWorkspace == null then
lib.recursiveUpdate mergedConfig0 {
agents = {
defaults = {
workspace = inst.workspaceDir;
};
};
}
else
mergedConfig0;
configJson = builtins.toJSON mergedConfig;
configFile = pkgs.writeText "openclaw-${name}.json" configJson;
gatewayWrapper = pkgs.writeShellScriptBin "openclaw-gateway-${name}" ''
set -euo pipefail
if [ -n "${lib.makeBinPath pluginPackages}" ]; then
export PATH="${lib.makeBinPath pluginPackages}:$PATH"
fi fi
'') pluginEnvAll)}
exec "${gatewayPackage}/bin/openclaw" "$@" ${lib.concatStringsSep "\n" (
''; map (
appDefaults = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isDarwin && inst.appDefaults.enable) { entry:
attachExistingOnly = inst.appDefaults.attachExistingOnly; let
gatewayPort = inst.gatewayPort; isFile = lib.hasSuffix "_FILE" entry.key;
nixMode = inst.appDefaults.nixMode; in
}; ''
if [ -f "${entry.value}" ]; then
if ${if isFile then "true" else "false"}; then
export ${entry.key}="${entry.value}"
else
rawValue="$("${lib.getExe' pkgs.coreutils "cat"}" "${entry.value}")"
if [ "''${rawValue#${entry.key}=}" != "$rawValue" ]; then
export ${entry.key}="''${rawValue#${entry.key}=}"
else
export ${entry.key}="$rawValue"
fi
fi
else
export ${entry.key}="${entry.value}"
fi
''
) pluginEnvAll
)}
appInstall = if !(pkgs.stdenv.hostPlatform.isDarwin && inst.app.install.enable && appPackage != null) then exec "${gatewayPackage}/bin/openclaw" "$@"
null '';
else { appDefaults = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isDarwin && inst.appDefaults.enable) {
name = lib.removePrefix "${homeDir}/" inst.app.install.path; attachExistingOnly = inst.appDefaults.attachExistingOnly;
value = { gatewayPort = inst.gatewayPort;
source = "${appPackage}/Applications/OpenClaw.app"; nixMode = inst.appDefaults.nixMode;
recursive = true;
force = true;
}; };
};
package = gatewayPackage; appInstall =
in { if !(pkgs.stdenv.hostPlatform.isDarwin && inst.app.install.enable && appPackage != null) then
homeFile = { null
name = openclawLib.toRelative inst.configPath; else
value = { text = configJson; }; {
}; name = lib.removePrefix "${homeDir}/" inst.app.install.path;
configFile = configFile; value = {
configPath = inst.configPath; source = "${appPackage}/Applications/OpenClaw.app";
recursive = true;
force = true;
};
};
dirs = [ inst.stateDir inst.workspaceDir (builtins.dirOf inst.logPath) ]; package = gatewayPackage;
in
{
homeFile = {
name = openclawLib.toRelative inst.configPath;
value = {
text = configJson;
};
};
configFile = configFile;
configPath = inst.configPath;
launchdAgent = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isDarwin && inst.launchd.enable) { dirs = [
"${inst.launchd.label}" = { inst.stateDir
enable = true; inst.workspaceDir
config = { (builtins.dirOf inst.logPath)
Label = inst.launchd.label; ];
ProgramArguments = [
"${gatewayWrapper}/bin/openclaw-gateway-${name}" launchdAgent = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isDarwin && inst.launchd.enable) {
"gateway" "${inst.launchd.label}" = {
"--port" enable = true;
"${toString inst.gatewayPort}" config = {
]; Label = inst.launchd.label;
RunAtLoad = true; ProgramArguments = [
KeepAlive = true; "${gatewayWrapper}/bin/openclaw-gateway-${name}"
WorkingDirectory = inst.stateDir; "gateway"
StandardOutPath = inst.logPath; "--port"
StandardErrorPath = inst.logPath; "${toString inst.gatewayPort}"
EnvironmentVariables = { ];
HOME = homeDir; RunAtLoad = true;
OPENCLAW_CONFIG_PATH = inst.configPath; KeepAlive = true;
OPENCLAW_STATE_DIR = inst.stateDir; WorkingDirectory = inst.stateDir;
OPENCLAW_IMAGE_BACKEND = "sips"; StandardOutPath = inst.logPath;
OPENCLAW_NIX_MODE = "1"; StandardErrorPath = inst.logPath;
EnvironmentVariables = {
HOME = homeDir;
OPENCLAW_CONFIG_PATH = inst.configPath;
OPENCLAW_STATE_DIR = inst.stateDir;
OPENCLAW_IMAGE_BACKEND = "sips";
OPENCLAW_NIX_MODE = "1";
};
}; };
}; };
}; };
};
systemdService = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux && inst.systemd.enable) { systemdService = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux && inst.systemd.enable) {
"${inst.systemd.unitName}" = { "${inst.systemd.unitName}" = {
Unit = { Unit = {
Description = "OpenClaw gateway (${name})"; Description = "OpenClaw gateway (${name})";
}; };
Service = { Service = {
ExecStart = "${gatewayWrapper}/bin/openclaw-gateway-${name} gateway --port ${toString inst.gatewayPort}"; ExecStart = "${gatewayWrapper}/bin/openclaw-gateway-${name} gateway --port ${toString inst.gatewayPort}";
WorkingDirectory = inst.stateDir; WorkingDirectory = inst.stateDir;
Restart = "always"; Restart = "always";
RestartSec = "1s"; RestartSec = "1s";
Environment = [ Environment = [
"HOME=${homeDir}" "HOME=${homeDir}"
"OPENCLAW_CONFIG_PATH=${inst.configPath}" "OPENCLAW_CONFIG_PATH=${inst.configPath}"
"OPENCLAW_STATE_DIR=${inst.stateDir}" "OPENCLAW_STATE_DIR=${inst.stateDir}"
"OPENCLAW_NIX_MODE=1" "OPENCLAW_NIX_MODE=1"
]; ];
StandardOutput = "append:${inst.logPath}"; StandardOutput = "append:${inst.logPath}";
StandardError = "append:${inst.logPath}"; StandardError = "append:${inst.logPath}";
};
}; };
}; };
};
appDefaults = appDefaults; appDefaults = appDefaults;
appInstall = appInstall; appInstall = appInstall;
package = package; package = package;
}; };
instanceConfigs = lib.mapAttrsToList mkInstanceConfig enabledInstances; instanceConfigs = lib.mapAttrsToList mkInstanceConfig enabledInstances;
appInstalls = lib.filter (item: item != null) (map (item: item.appInstall) instanceConfigs); appInstalls = lib.filter (item: item != null) (map (item: item.appInstall) instanceConfigs);
appDefaults = lib.foldl' (acc: item: lib.recursiveUpdate acc item.appDefaults) {} instanceConfigs; appDefaults = lib.foldl' (acc: item: lib.recursiveUpdate acc item.appDefaults) { } instanceConfigs;
appDefaultsEnabled = lib.filterAttrs (_: inst: inst.appDefaults.enable) enabledInstances; appDefaultsEnabled = lib.filterAttrs (_: inst: inst.appDefaults.enable) enabledInstances;
in { in
config = lib.mkIf (cfg.enable || cfg.instances != {}) { {
config = lib.mkIf (cfg.enable || cfg.instances != { }) {
assertions = [ assertions = [
{ {
assertion = lib.length (lib.attrNames appDefaultsEnabled) <= 1; assertion = lib.length (lib.attrNames appDefaultsEnabled) <= 1;
message = "Only one OpenClaw instance may enable appDefaults."; message = "Only one OpenClaw instance may enable appDefaults.";
} }
] ++ files.documentsAssertions ++ files.skillAssertions ++ plugins.pluginAssertions ++ plugins.pluginSkillAssertions; ]
++ files.documentsAssertions
++ files.skillAssertions
++ plugins.pluginAssertions
++ plugins.pluginSkillAssertions;
home.packages = lib.unique ( home.packages = lib.unique (
(map (item: item.package) instanceConfigs) (map (item: item.package) instanceConfigs)
@ -242,12 +294,20 @@ in {
); );
home.activation.openclawDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] '' home.activation.openclawDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
run --quiet ${lib.getExe' pkgs.coreutils "mkdir"} -p ${lib.concatStringsSep " " (lib.concatMap (item: item.dirs) instanceConfigs)} run --quiet ${lib.getExe' pkgs.coreutils "mkdir"} -p ${
${lib.optionalString (plugins.pluginStateDirsAll != []) "run --quiet ${lib.getExe' pkgs.coreutils "mkdir"} -p ${lib.concatStringsSep " " plugins.pluginStateDirsAll}"} lib.concatStringsSep " " (lib.concatMap (item: item.dirs) instanceConfigs)
}
${lib.optionalString (plugins.pluginStateDirsAll != [ ])
"run --quiet ${lib.getExe' pkgs.coreutils "mkdir"} -p ${lib.concatStringsSep " " plugins.pluginStateDirsAll}"
}
''; '';
home.activation.openclawConfigFiles = lib.hm.dag.entryAfter [ "openclawDirs" ] '' home.activation.openclawConfigFiles = lib.hm.dag.entryAfter [ "openclawDirs" ] ''
${lib.concatStringsSep "\n" (map (item: "run --quiet ${lib.getExe' pkgs.coreutils "ln"} -sfn ${item.configFile} ${item.configPath}") instanceConfigs)} ${lib.concatStringsSep "\n" (
map (
item: "run --quiet ${lib.getExe' pkgs.coreutils "ln"} -sfn ${item.configFile} ${item.configPath}"
) instanceConfigs
)}
''; '';
home.activation.openclawPluginGuard = lib.hm.dag.entryAfter [ "writeBoundary" ] '' home.activation.openclawPluginGuard = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
@ -255,14 +315,22 @@ in {
${plugins.pluginGuards} ${plugins.pluginGuards}
''; '';
home.activation.openclawAppDefaults = lib.mkIf (pkgs.stdenv.hostPlatform.isDarwin && appDefaults != {}) ( home.activation.openclawAppDefaults =
lib.hm.dag.entryAfter [ "writeBoundary" ] '' lib.mkIf (pkgs.stdenv.hostPlatform.isDarwin && appDefaults != { })
# Nix mode + app defaults (OpenClaw.app) (
/usr/bin/defaults write ai.openclaw.mac openclaw.nixMode -bool ${lib.boolToString (appDefaults.nixMode or true)} lib.hm.dag.entryAfter [ "writeBoundary" ] ''
/usr/bin/defaults write ai.openclaw.mac openclaw.gateway.attachExistingOnly -bool ${lib.boolToString (appDefaults.attachExistingOnly or true)} # Nix mode + app defaults (OpenClaw.app)
/usr/bin/defaults write ai.openclaw.mac gatewayPort -int ${toString (appDefaults.gatewayPort or 18789)} /usr/bin/defaults write ai.openclaw.mac openclaw.nixMode -bool ${
'' lib.boolToString (appDefaults.nixMode or true)
); }
/usr/bin/defaults write ai.openclaw.mac openclaw.gateway.attachExistingOnly -bool ${
lib.boolToString (appDefaults.attachExistingOnly or true)
}
/usr/bin/defaults write ai.openclaw.mac gatewayPort -int ${
toString (appDefaults.gatewayPort or 18789)
}
''
);
home.activation.openclawLaunchdRelink = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin ( home.activation.openclawLaunchdRelink = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin (
lib.hm.dag.entryAfter [ "linkGeneration" ] '' lib.hm.dag.entryAfter [ "linkGeneration" ] ''

View File

@ -1,8 +1,19 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
{ {
imports = [ imports = [
(lib.mkRenamedOptionModule [ "programs" "openclaw" "firstParty" ] [ "programs" "openclaw" "bundledPlugins" ]) (lib.mkRenamedOptionModule
(lib.mkRenamedOptionModule [ "programs" "openclaw" "plugins" ] [ "programs" "openclaw" "customPlugins" ]) [ "programs" "openclaw" "firstParty" ]
[ "programs" "openclaw" "bundledPlugins" ]
)
(lib.mkRenamedOptionModule
[ "programs" "openclaw" "plugins" ]
[ "programs" "openclaw" "customPlugins" ]
)
./options.nix ./options.nix
./config.nix ./config.nix
]; ];

View File

@ -1,4 +1,11 @@
{ config, lib, pkgs, openclawLib, enabledInstances, plugins }: {
config,
lib,
pkgs,
openclawLib,
enabledInstances,
plugins,
}:
let let
cfg = openclawLib.cfg; cfg = openclawLib.cfg;
@ -8,7 +15,8 @@ let
documentsEnabled = cfg.documents != null; documentsEnabled = cfg.documents != null;
instanceWorkspaceDirs = map (inst: resolvePath inst.workspaceDir) (lib.attrValues enabledInstances); instanceWorkspaceDirs = map (inst: resolvePath inst.workspaceDir) (lib.attrValues enabledInstances);
renderSkill = skill: renderSkill =
skill:
let let
frontmatterLines = [ frontmatterLines = [
"---" "---"
@ -24,15 +32,18 @@ let
frontmatter = lib.concatStringsSep "\n" frontmatterLines; frontmatter = lib.concatStringsSep "\n" frontmatterLines;
body = if skill ? body then skill.body else ""; body = if skill ? body then skill.body else "";
in in
"${frontmatter}\n\n${body}\n"; "${frontmatter}\n\n${body}\n";
skillAssertions = skillAssertions =
let let
names = map (skill: skill.name) cfg.skills; names = map (skill: skill.name) cfg.skills;
nameCounts = lib.foldl' (acc: name: acc // { "${name}" = (acc.${name} or 0) + 1; }) {} names; nameCounts = lib.foldl' (acc: name: acc // { "${name}" = (acc.${name} or 0) + 1; }) { } names;
duplicateNames = lib.attrNames (lib.filterAttrs (_: v: v > 1) nameCounts); duplicateNames = lib.attrNames (lib.filterAttrs (_: v: v > 1) nameCounts);
in in
if duplicateNames == [] then [] else [ if duplicateNames == [ ] then
[ ]
else
[
{ {
assertion = false; assertion = false;
message = "programs.openclaw.skills has duplicate names: ${lib.concatStringsSep ", " duplicateNames}"; message = "programs.openclaw.skills has duplicate names: ${lib.concatStringsSep ", " duplicateNames}";
@ -41,48 +52,56 @@ let
skillFiles = skillFiles =
let let
entriesForInstance = instName: inst: entriesForInstance =
instName: inst:
let let
base = "${toRelative (resolvePath inst.workspaceDir)}/skills"; base = "${toRelative (resolvePath inst.workspaceDir)}/skills";
entryFor = skill: entryFor =
skill:
let let
mode = skill.mode or "symlink"; mode = skill.mode or "symlink";
source = if skill ? source && skill.source != null then resolvePath skill.source else null; source = if skill ? source && skill.source != null then resolvePath skill.source else null;
in in
if mode == "inline" then if mode == "inline" then
{ {
name = "${base}/${skill.name}/SKILL.md"; name = "${base}/${skill.name}/SKILL.md";
value = { text = renderSkill skill; }; value = {
} text = renderSkill skill;
else if mode == "copy" then
{
name = "${base}/${skill.name}";
value = {
source = builtins.path {
name = "openclaw-skill-${skill.name}";
path = source;
};
recursive = true;
};
}
else
{
name = "${base}/${skill.name}";
value = {
source = config.lib.file.mkOutOfStoreSymlink source;
recursive = true;
};
}; };
pluginEntriesFor = p: }
else if mode == "copy" then
{
name = "${base}/${skill.name}";
value = {
source = builtins.path {
name = "openclaw-skill-${skill.name}";
path = source;
};
recursive = true;
};
}
else
{
name = "${base}/${skill.name}";
value = {
source = config.lib.file.mkOutOfStoreSymlink source;
recursive = true;
};
};
pluginEntriesFor =
p:
map (skillPath: { map (skillPath: {
name = "${base}/${builtins.baseNameOf skillPath}"; name = "${base}/${builtins.baseNameOf skillPath}";
value = { source = skillPath; recursive = true; }; value = {
source = skillPath;
recursive = true;
};
}) p.skills; }) p.skills;
pluginsForInstance = plugins.resolvedPluginsByInstance.${instName} or []; pluginsForInstance = plugins.resolvedPluginsByInstance.${instName} or [ ];
in in
(map entryFor cfg.skills) ++ (lib.flatten (map pluginEntriesFor pluginsForInstance)); (map entryFor cfg.skills) ++ (lib.flatten (map pluginEntriesFor pluginsForInstance));
in in
lib.listToAttrs (lib.flatten (lib.mapAttrsToList entriesForInstance enabledInstances)); lib.listToAttrs (lib.flatten (lib.mapAttrsToList entriesForInstance enabledInstances));
documentsRequiredFiles = [ documentsRequiredFiles = [
"AGENTS.md" "AGENTS.md"
@ -103,9 +122,9 @@ let
let let
extra = lib.filter (file: builtins.pathExists (cfg.documents + "/${file}")) documentsOptionalFiles; extra = lib.filter (file: builtins.pathExists (cfg.documents + "/${file}")) documentsOptionalFiles;
in in
documentsRequiredFiles ++ extra documentsRequiredFiles ++ extra
else else
[]; [ ];
documentsAssertions = lib.optionals documentsEnabled [ documentsAssertions = lib.optionals documentsEnabled [
{ {
@ -126,77 +145,74 @@ let
} }
]; ];
documentsGuard = documentsGuard = lib.optionalString documentsEnabled (
lib.optionalString documentsEnabled ( let
let guardLine = file: ''
guardLine = file: '' if [ -e "${file}" ] && [ ! -L "${file}" ]; then
if [ -e "${file}" ] && [ ! -L "${file}" ]; then echo "OpenClaw documents are managed by Nix. Please adopt ${file} into your documents directory and re-run." >&2
echo "OpenClaw documents are managed by Nix. Please adopt ${file} into your documents directory and re-run." >&2 exit 1
exit 1 fi
fi '';
''; guardForDir = dir: ''
guardForDir = dir: '' ${lib.concatStringsSep "\n" (map (name: guardLine "${dir}/${name}") documentsFileNames)}
${lib.concatStringsSep "\n" (map (name: guardLine "${dir}/${name}") documentsFileNames)} '';
''; in
in lib.concatStringsSep "\n" (map guardForDir instanceWorkspaceDirs)
lib.concatStringsSep "\n" (map guardForDir instanceWorkspaceDirs) );
);
toolsReport = toolsReport =
if documentsEnabled then if documentsEnabled then
let let
toolNames = toolSets.toolNames or []; toolNames = toolSets.toolNames or [ ];
renderPkgName = pkg: renderPkgName = pkg: if pkg ? pname then pkg.pname else lib.getName pkg;
if pkg ? pname then pkg.pname else lib.getName pkg; renderPlugin =
renderPlugin = plugin: plugin:
let let
pkgNames = map renderPkgName (lib.filter (p: p != null) plugin.packages); pkgNames = map renderPkgName (lib.filter (p: p != null) plugin.packages);
pkgSuffix = pkgSuffix = if pkgNames == [ ] then "" else " " + (lib.concatStringsSep ", " pkgNames);
if pkgNames == [] in
then "" "- " + plugin.name + pkgSuffix + " (" + plugin.source + ")";
else " " + (lib.concatStringsSep ", " pkgNames); pluginLinesFor =
in instName: inst:
"- " + plugin.name + pkgSuffix + " (" + plugin.source + ")"; let
pluginLinesFor = instName: inst: pluginsForInstance = plugins.resolvedPluginsByInstance.${instName} or [ ];
let lines = if pluginsForInstance == [ ] then [ "- (none)" ] else map renderPlugin pluginsForInstance;
pluginsForInstance = plugins.resolvedPluginsByInstance.${instName} or []; in
lines = if pluginsForInstance == [] then [ "- (none)" ] else map renderPlugin pluginsForInstance;
in
[
""
"### Instance: ${instName}"
] ++ lines;
reportLines =
[ [
"<!-- BEGIN NIX-REPORT -->"
"" ""
"## Nix-managed tools" "### Instance: ${instName}"
""
"### Built-in toolchain"
] ]
++ (if toolNames == [] then [ "- (none)" ] else map (name: "- " + name) toolNames) ++ lines;
++ [ reportLines = [
"" "<!-- BEGIN NIX-REPORT -->"
"## Nix-managed plugin report" ""
"" "## Nix-managed tools"
"Plugins enabled per instance (last-wins on name collisions):" ""
] "### Built-in toolchain"
++ lib.concatLists (lib.mapAttrsToList pluginLinesFor enabledInstances) ]
++ [ ++ (if toolNames == [ ] then [ "- (none)" ] else map (name: "- " + name) toolNames)
"" ++ [
"Tools: batteries-included toolchain + plugin-provided CLIs." ""
"" "## Nix-managed plugin report"
"<!-- END NIX-REPORT -->" ""
]; "Plugins enabled per instance (last-wins on name collisions):"
]
++ lib.concatLists (lib.mapAttrsToList pluginLinesFor enabledInstances)
++ [
""
"Tools: batteries-included toolchain + plugin-provided CLIs."
""
"<!-- END NIX-REPORT -->"
];
reportText = lib.concatStringsSep "\n" reportLines; reportText = lib.concatStringsSep "\n" reportLines;
in in
pkgs.writeText "openclaw-tools-report.md" reportText pkgs.writeText "openclaw-tools-report.md" reportText
else else
null; null;
toolsWithReport = toolsWithReport =
if documentsEnabled then if documentsEnabled then
pkgs.runCommand "openclaw-tools-with-report.md" {} '' pkgs.runCommand "openclaw-tools-with-report.md" { } ''
cat ${cfg.documents + "/TOOLS.md"} > $out cat ${cfg.documents + "/TOOLS.md"} > $out
echo "" >> $out echo "" >> $out
cat ${toolsReport} >> $out cat ${toolsReport} >> $out
@ -207,7 +223,8 @@ let
documentsFiles = documentsFiles =
if documentsEnabled then if documentsEnabled then
let let
mkDocFiles = dir: mkDocFiles =
dir:
let let
mkDoc = name: { mkDoc = name: {
name = toRelative (dir + "/${name}"); name = toRelative (dir + "/${name}");
@ -216,18 +233,20 @@ let
}; };
}; };
in in
lib.listToAttrs (map mkDoc documentsFileNames); lib.listToAttrs (map mkDoc documentsFileNames);
in in
lib.mkMerge (map mkDocFiles instanceWorkspaceDirs) lib.mkMerge (map mkDocFiles instanceWorkspaceDirs)
else else
{}; { };
in { in
{
inherit inherit
documentsEnabled documentsEnabled
documentsAssertions documentsAssertions
documentsGuard documentsGuard
documentsFiles documentsFiles
skillAssertions skillAssertions
skillFiles; skillFiles
;
} }

View File

@ -1,4 +1,8 @@
{ config, lib, pkgs }: {
config,
lib,
pkgs,
}:
let let
cfg = config.programs.openclaw; cfg = config.programs.openclaw;
@ -9,59 +13,62 @@ let
toolNamesOverride = cfg.toolNames; toolNamesOverride = cfg.toolNames;
excludeToolNames = effectiveExcludeTools; excludeToolNames = effectiveExcludeTools;
}; };
toolOverridesEnabled = cfg.toolNames != null || effectiveExcludeTools != []; toolOverridesEnabled = cfg.toolNames != null || effectiveExcludeTools != [ ];
toolSets = import ../../../tools/extended.nix ({ inherit pkgs; } // toolOverrides); toolSets = import ../../../tools/extended.nix ({ inherit pkgs; } // toolOverrides);
defaultPackage = defaultPackage =
if toolOverridesEnabled && cfg.package == pkgs.openclaw if toolOverridesEnabled && cfg.package == pkgs.openclaw then
then (pkgs.openclawPackages.withTools toolOverrides).openclaw (pkgs.openclawPackages.withTools toolOverrides).openclaw
else cfg.package; else
cfg.package;
appPackage = if cfg.appPackage != null then cfg.appPackage else defaultPackage; appPackage = if cfg.appPackage != null then cfg.appPackage else defaultPackage;
generatedConfigOptions = import ../../../generated/openclaw-config-options.nix { lib = lib; }; generatedConfigOptions = import ../../../generated/openclaw-config-options.nix { lib = lib; };
bundledPluginSources = let bundledPluginSources =
stepieteRev = "983210e3b6e9285780e87f48ce9354b51a270e95";
stepieteNarHash = "sha256-fY8t41kMSHu2ovf89mIdvC7vkceroCwKxw/MKVn4rsE=";
stepiete = tool:
"github:openclaw/nix-steipete-tools?dir=tools/${tool}&rev=${stepieteRev}&narHash=${stepieteNarHash}";
in {
summarize = stepiete "summarize";
peekaboo = stepiete "peekaboo";
oracle = stepiete "oracle";
poltergeist = stepiete "poltergeist";
sag = stepiete "sag";
camsnap = stepiete "camsnap";
gogcli = stepiete "gogcli";
goplaces = stepiete "goplaces";
bird = stepiete "bird";
sonoscli = stepiete "sonoscli";
imsg = stepiete "imsg";
};
bundledPlugins = lib.filter (p: p != null) (lib.mapAttrsToList (name: source:
let let
pluginCfg = cfg.bundledPlugins.${name}; stepieteRev = "983210e3b6e9285780e87f48ce9354b51a270e95";
stepieteNarHash = "sha256-fY8t41kMSHu2ovf89mIdvC7vkceroCwKxw/MKVn4rsE=";
stepiete =
tool:
"github:openclaw/nix-steipete-tools?dir=tools/${tool}&rev=${stepieteRev}&narHash=${stepieteNarHash}";
in in
if (pluginCfg.enable or false) then { {
inherit source; summarize = stepiete "summarize";
config = pluginCfg.config or {}; peekaboo = stepiete "peekaboo";
} else null oracle = stepiete "oracle";
) bundledPluginSources); poltergeist = stepiete "poltergeist";
sag = stepiete "sag";
camsnap = stepiete "camsnap";
gogcli = stepiete "gogcli";
goplaces = stepiete "goplaces";
bird = stepiete "bird";
sonoscli = stepiete "sonoscli";
imsg = stepiete "imsg";
};
bundledPlugins = lib.filter (p: p != null) (
lib.mapAttrsToList (
name: source:
let
pluginCfg = cfg.bundledPlugins.${name};
in
if (pluginCfg.enable or false) then
{
inherit source;
config = pluginCfg.config or { };
}
else
null
) bundledPluginSources
);
effectivePlugins = cfg.customPlugins ++ bundledPlugins; effectivePlugins = cfg.customPlugins ++ bundledPlugins;
resolvePath = p: resolvePath = p: if lib.hasPrefix "~/" p then "${homeDir}/${lib.removePrefix "~/" p}" else p;
if lib.hasPrefix "~/" p then
"${homeDir}/${lib.removePrefix "~/" p}"
else
p;
toRelative = p: toRelative = p: if lib.hasPrefix "${homeDir}/" p then lib.removePrefix "${homeDir}/" p else p;
if lib.hasPrefix "${homeDir}/" p then
lib.removePrefix "${homeDir}/" p
else
p;
in { in
{
inherit inherit
cfg cfg
homeDir homeDir
@ -75,5 +82,6 @@ in {
bundledPlugins bundledPlugins
effectivePlugins effectivePlugins
resolvePath resolvePath
toRelative; toRelative
;
} }

View File

@ -17,9 +17,11 @@
stateDir = lib.mkOption { stateDir = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = if name == "default" default =
then "${openclawLib.homeDir}/.openclaw" if name == "default" then
else "${openclawLib.homeDir}/.openclaw-${name}"; "${openclawLib.homeDir}/.openclaw"
else
"${openclawLib.homeDir}/.openclaw-${name}";
description = "State directory for this OpenClaw instance (logs, sessions, config)."; description = "State directory for this OpenClaw instance (logs, sessions, config).";
}; };
@ -37,9 +39,11 @@
logPath = lib.mkOption { logPath = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = if name == "default" default =
then "/tmp/openclaw/openclaw-gateway.log" if name == "default" then
else "/tmp/openclaw/openclaw-gateway-${name}.log"; "/tmp/openclaw/openclaw-gateway.log"
else
"/tmp/openclaw/openclaw-gateway-${name}.log";
description = "Log path for this OpenClaw gateway instance."; description = "Log path for this OpenClaw gateway instance.";
}; };
@ -62,26 +66,28 @@
}; };
plugins = lib.mkOption { plugins = lib.mkOption {
type = lib.types.listOf (lib.types.submodule { type = lib.types.listOf (
options = { lib.types.submodule {
source = lib.mkOption { options = {
type = lib.types.str; source = lib.mkOption {
description = "Plugin source pointer (e.g., github:owner/repo or path:/...)."; type = lib.types.str;
description = "Plugin source pointer (e.g., github:owner/repo or path:/...).";
};
config = lib.mkOption {
type = lib.types.attrs;
default = { };
description = "Plugin-specific configuration (env/files/etc).";
};
}; };
config = lib.mkOption { }
type = lib.types.attrs; );
default = {};
description = "Plugin-specific configuration (env/files/etc).";
};
};
});
default = openclawLib.effectivePlugins; default = openclawLib.effectivePlugins;
description = "Plugins enabled for this instance (includes first-party toggles)."; description = "Plugins enabled for this instance (includes first-party toggles).";
}; };
config = lib.mkOption { config = lib.mkOption {
type = lib.types.submodule { options = openclawLib.generatedConfigOptions; }; type = lib.types.submodule { options = openclawLib.generatedConfigOptions; };
default = {}; default = { };
description = "OpenClaw config (schema-typed)."; description = "OpenClaw config (schema-typed).";
}; };
@ -93,9 +99,11 @@
launchd.label = lib.mkOption { launchd.label = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = if name == "default" default =
then "com.steipete.openclaw.gateway" if name == "default" then
else "com.steipete.openclaw.gateway.${name}"; "com.steipete.openclaw.gateway"
else
"com.steipete.openclaw.gateway.${name}";
description = "launchd label for this instance."; description = "launchd label for this instance.";
}; };
@ -107,9 +115,7 @@
systemd.unitName = lib.mkOption { systemd.unitName = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = if name == "default" default = if name == "default" then "openclaw-gateway" else "openclaw-gateway-${name}";
then "openclaw-gateway"
else "openclaw-gateway-${name}";
description = "systemd user service unit name for this instance."; description = "systemd user service unit name for this instance.";
}; };

View File

@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
openclawLib = import ./lib.nix { inherit config lib pkgs; }; openclawLib = import ./lib.nix { inherit config lib pkgs; };
@ -30,7 +35,11 @@ let
description = "Optional openclaw metadata for the skill frontmatter."; description = "Optional openclaw metadata for the skill frontmatter.";
}; };
mode = lib.mkOption { mode = lib.mkOption {
type = lib.types.enum [ "symlink" "copy" "inline" ]; type = lib.types.enum [
"symlink"
"copy"
"inline"
];
default = "symlink"; default = "symlink";
description = "Install mode for the skill (symlink/copy/inline)."; description = "Install mode for the skill (symlink/copy/inline).";
}; };
@ -42,7 +51,8 @@ let
}; };
}; };
in { in
{
options.programs.openclaw = { options.programs.openclaw = {
enable = lib.mkEnableOption "OpenClaw (batteries-included)"; enable = lib.mkEnableOption "OpenClaw (batteries-included)";
@ -60,7 +70,7 @@ in {
excludeTools = lib.mkOption { excludeTools = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = []; default = [ ];
description = "Tool names to remove from the built-in toolchain."; description = "Tool names to remove from the built-in toolchain.";
}; };
@ -104,54 +114,66 @@ in {
skills = lib.mkOption { skills = lib.mkOption {
type = lib.types.listOf mkSkillOption; type = lib.types.listOf mkSkillOption;
default = []; default = [ ];
description = "Declarative skills installed into each instance workspace."; description = "Declarative skills installed into each instance workspace.";
}; };
customPlugins = lib.mkOption { customPlugins = lib.mkOption {
type = lib.types.listOf (lib.types.submodule { type = lib.types.listOf (
options = { lib.types.submodule {
source = lib.mkOption { options = {
type = lib.types.str; source = lib.mkOption {
description = "Plugin source pointer (e.g., github:owner/repo or path:/...)."; type = lib.types.str;
description = "Plugin source pointer (e.g., github:owner/repo or path:/...).";
};
config = lib.mkOption {
type = lib.types.attrs;
default = { };
description = "Plugin-specific configuration (env/files/etc).";
};
}; };
config = lib.mkOption { }
type = lib.types.attrs; );
default = {}; default = [ ];
description = "Plugin-specific configuration (env/files/etc).";
};
};
});
default = [];
description = "Custom/community plugins (merged with bundled plugin toggles)."; description = "Custom/community plugins (merged with bundled plugin toggles).";
}; };
bundledPlugins = let bundledPlugins =
mkPlugin = { name, defaultEnable ? false }: { let
enable = lib.mkOption { mkPlugin =
type = lib.types.bool; {
default = defaultEnable; name,
description = "Enable the ${name} plugin (bundled)."; defaultEnable ? false,
}; }:
config = lib.mkOption { {
type = lib.types.attrs; enable = lib.mkOption {
default = {}; type = lib.types.bool;
description = "Bundled plugin configuration passed through to ${name} (env/settings)."; default = defaultEnable;
description = "Enable the ${name} plugin (bundled).";
};
config = lib.mkOption {
type = lib.types.attrs;
default = { };
description = "Bundled plugin configuration passed through to ${name} (env/settings).";
};
};
in
{
summarize = mkPlugin { name = "summarize"; };
peekaboo = mkPlugin { name = "peekaboo"; };
oracle = mkPlugin { name = "oracle"; };
poltergeist = mkPlugin { name = "poltergeist"; };
sag = mkPlugin { name = "sag"; };
camsnap = mkPlugin { name = "camsnap"; };
gogcli = mkPlugin { name = "gogcli"; };
goplaces = mkPlugin {
name = "goplaces";
defaultEnable = true;
}; };
bird = mkPlugin { name = "bird"; };
sonoscli = mkPlugin { name = "sonoscli"; };
imsg = mkPlugin { name = "imsg"; };
}; };
in {
summarize = mkPlugin { name = "summarize"; };
peekaboo = mkPlugin { name = "peekaboo"; };
oracle = mkPlugin { name = "oracle"; };
poltergeist = mkPlugin { name = "poltergeist"; };
sag = mkPlugin { name = "sag"; };
camsnap = mkPlugin { name = "camsnap"; };
gogcli = mkPlugin { name = "gogcli"; };
goplaces = mkPlugin { name = "goplaces"; defaultEnable = true; };
bird = mkPlugin { name = "bird"; };
sonoscli = mkPlugin { name = "sonoscli"; };
imsg = mkPlugin { name = "imsg"; };
};
launchd.enable = lib.mkOption { launchd.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
@ -179,7 +201,7 @@ in {
instances = lib.mkOption { instances = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule instanceModule); type = lib.types.attrsOf (lib.types.submodule instanceModule);
default = {}; default = { };
description = "Named OpenClaw instances (prod/test)."; description = "Named OpenClaw instances (prod/test).";
}; };
@ -199,7 +221,7 @@ in {
config = lib.mkOption { config = lib.mkOption {
type = lib.types.submodule { options = openclawLib.generatedConfigOptions; }; type = lib.types.submodule { options = openclawLib.generatedConfigOptions; };
default = {}; default = { };
description = "OpenClaw config (schema-typed)."; description = "OpenClaw config (schema-typed).";
}; };
}; };

View File

@ -1,104 +1,124 @@
{ lib, pkgs, openclawLib, enabledInstances }: {
lib,
pkgs,
openclawLib,
enabledInstances,
}:
let let
resolvePath = openclawLib.resolvePath; resolvePath = openclawLib.resolvePath;
toRelative = openclawLib.toRelative; toRelative = openclawLib.toRelative;
resolvePlugin = plugin: let resolvePlugin =
flake = builtins.getFlake plugin.source; plugin:
system = pkgs.stdenv.hostPlatform.system;
openclawPluginRaw =
if flake ? openclawPlugin then flake.openclawPlugin
else throw "openclawPlugin missing in ${plugin.source}";
openclawPlugin =
if builtins.isFunction openclawPluginRaw
then openclawPluginRaw system
else openclawPluginRaw;
resolvedPlugin =
if openclawPlugin == null
then throw "openclawPlugin is null in ${plugin.source} for ${system}"
else openclawPlugin;
needs = resolvedPlugin.needs or {};
in {
source = plugin.source;
name = resolvedPlugin.name or (throw "openclawPlugin.name missing in ${plugin.source}");
skills = resolvedPlugin.skills or [];
packages = resolvedPlugin.packages or [];
needs = {
stateDirs = needs.stateDirs or [];
requiredEnv = needs.requiredEnv or [];
};
config = plugin.config or {};
};
resolvedPluginsByInstance =
lib.mapAttrs (instName: inst:
let
resolved = map resolvePlugin inst.plugins;
counts = lib.foldl' (acc: p:
acc // { "${p.name}" = (acc.${p.name} or 0) + 1; }
) {} resolved;
duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts);
byName = lib.foldl' (acc: p: acc // { "${p.name}" = p; }) {} resolved;
ordered = lib.attrValues byName;
in
if duplicates == []
then ordered
else lib.warn
"programs.openclaw.instances.${instName}: duplicate plugin names detected (${lib.concatStringsSep ", " duplicates}); last entry wins."
ordered
) enabledInstances;
pluginPackagesFor = instName:
lib.flatten (map (p: p.packages) (resolvedPluginsByInstance.${instName} or []));
pluginPackagesAll =
lib.flatten (map pluginPackagesFor (lib.attrNames enabledInstances));
pluginStateDirsFor = instName:
let let
dirs = lib.flatten (map (p: p.needs.stateDirs) (resolvedPluginsByInstance.${instName} or [])); flake = builtins.getFlake plugin.source;
system = pkgs.stdenv.hostPlatform.system;
openclawPluginRaw =
if flake ? openclawPlugin then
flake.openclawPlugin
else
throw "openclawPlugin missing in ${plugin.source}";
openclawPlugin =
if builtins.isFunction openclawPluginRaw then openclawPluginRaw system else openclawPluginRaw;
resolvedPlugin =
if openclawPlugin == null then
throw "openclawPlugin is null in ${plugin.source} for ${system}"
else
openclawPlugin;
needs = resolvedPlugin.needs or { };
in in
map (dir: resolvePath ("~/" + dir)) dirs; {
source = plugin.source;
name = resolvedPlugin.name or (throw "openclawPlugin.name missing in ${plugin.source}");
skills = resolvedPlugin.skills or [ ];
packages = resolvedPlugin.packages or [ ];
needs = {
stateDirs = needs.stateDirs or [ ];
requiredEnv = needs.requiredEnv or [ ];
};
config = plugin.config or { };
};
pluginStateDirsAll = resolvedPluginsByInstance = lib.mapAttrs (
lib.flatten (map pluginStateDirsFor (lib.attrNames enabledInstances)); instName: inst:
pluginEnvFor = instName:
let let
entries = resolvedPluginsByInstance.${instName} or []; resolved = map resolvePlugin inst.plugins;
toPairs = p: counts = lib.foldl' (acc: p: acc // { "${p.name}" = (acc.${p.name} or 0) + 1; }) { } resolved;
duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts);
byName = lib.foldl' (acc: p: acc // { "${p.name}" = p; }) { } resolved;
ordered = lib.attrValues byName;
in
if duplicates == [ ] then
ordered
else
lib.warn "programs.openclaw.instances.${instName}: duplicate plugin names detected (${lib.concatStringsSep ", " duplicates}); last entry wins." ordered
) enabledInstances;
pluginPackagesFor =
instName: lib.flatten (map (p: p.packages) (resolvedPluginsByInstance.${instName} or [ ]));
pluginPackagesAll = lib.flatten (map pluginPackagesFor (lib.attrNames enabledInstances));
pluginStateDirsFor =
instName:
let
dirs = lib.flatten (map (p: p.needs.stateDirs) (resolvedPluginsByInstance.${instName} or [ ]));
in
map (dir: resolvePath ("~/" + dir)) dirs;
pluginStateDirsAll = lib.flatten (map pluginStateDirsFor (lib.attrNames enabledInstances));
pluginEnvFor =
instName:
let
entries = resolvedPluginsByInstance.${instName} or [ ];
toPairs =
p:
let let
env = (p.config.env or {}); env = (p.config.env or { });
required = p.needs.requiredEnv; required = p.needs.requiredEnv;
in in
map (k: { key = k; value = env.${k} or ""; plugin = p.name; }) required; map (k: {
key = k;
value = env.${k} or "";
plugin = p.name;
}) required;
in in
lib.flatten (map toPairs entries); lib.flatten (map toPairs entries);
pluginEnvAllFor = instName: pluginEnvAllFor =
instName:
let let
entries = resolvedPluginsByInstance.${instName} or []; entries = resolvedPluginsByInstance.${instName} or [ ];
toPairs = p: toPairs =
let env = (p.config.env or {}); p:
in map (k: { key = k; value = env.${k}; plugin = p.name; }) (lib.attrNames env); let
env = (p.config.env or { });
in
map (k: {
key = k;
value = env.${k};
plugin = p.name;
}) (lib.attrNames env);
in in
lib.flatten (map toPairs entries); lib.flatten (map toPairs entries);
pluginAssertions = pluginAssertions = lib.flatten (
lib.flatten (lib.mapAttrsToList (instName: inst: lib.mapAttrsToList (
instName: inst:
let let
plugins = resolvedPluginsByInstance.${instName} or []; plugins = resolvedPluginsByInstance.${instName} or [ ];
envFor = p: (p.config.env or {}); envFor = p: (p.config.env or { });
missingFor = p: missingFor = p: lib.filter (req: !(builtins.hasAttr req (envFor p))) p.needs.requiredEnv;
lib.filter (req: !(builtins.hasAttr req (envFor p))) p.needs.requiredEnv; configMissingStateDir = p: (p.config.settings or { }) != { } && (p.needs.stateDirs or [ ]) == [ ];
configMissingStateDir = p: mkAssertion =
(p.config.settings or {}) != {} && (p.needs.stateDirs or []) == []; p:
mkAssertion = p:
let let
missing = missingFor p; missing = missingFor p;
in { in
assertion = missing == []; {
assertion = missing == [ ];
message = "programs.openclaw.instances.${instName}: plugin ${p.name} missing required env: ${lib.concatStringsSep ", " missing}"; message = "programs.openclaw.instances.${instName}: plugin ${p.name} missing required env: ${lib.concatStringsSep ", " missing}";
}; };
mkConfigAssertion = p: { mkConfigAssertion = p: {
@ -106,75 +126,86 @@ let
message = "programs.openclaw.instances.${instName}: plugin ${p.name} provides settings but declares no stateDirs (needed for config.json)."; message = "programs.openclaw.instances.${instName}: plugin ${p.name} provides settings but declares no stateDirs (needed for config.json).";
}; };
in in
(map mkAssertion plugins) ++ (map mkConfigAssertion plugins) (map mkAssertion plugins) ++ (map mkConfigAssertion plugins)
) enabledInstances); ) enabledInstances
);
pluginSkillsFiles = pluginSkillsFiles =
let let
entriesForInstance = instName: inst: entriesForInstance =
instName: inst:
let let
base = "${toRelative (resolvePath inst.workspaceDir)}/skills"; base = "${toRelative (resolvePath inst.workspaceDir)}/skills";
skillEntriesFor = p: skillEntriesFor =
p:
map (skillPath: { map (skillPath: {
name = "${base}/${builtins.baseNameOf skillPath}"; name = "${base}/${builtins.baseNameOf skillPath}";
value = { source = skillPath; recursive = true; }; value = {
source = skillPath;
recursive = true;
};
}) p.skills; }) p.skills;
plugins = resolvedPluginsByInstance.${instName} or []; plugins = resolvedPluginsByInstance.${instName} or [ ];
in in
lib.flatten (map skillEntriesFor plugins); lib.flatten (map skillEntriesFor plugins);
in in
lib.listToAttrs (lib.flatten (lib.mapAttrsToList entriesForInstance enabledInstances)); lib.listToAttrs (lib.flatten (lib.mapAttrsToList entriesForInstance enabledInstances));
pluginConfigFiles = pluginConfigFiles =
let let
entryFor = instName: inst: entryFor =
instName: inst:
let let
plugins = resolvedPluginsByInstance.${instName} or []; plugins = resolvedPluginsByInstance.${instName} or [ ];
mkEntries = p: mkEntries =
p:
let let
cfg = p.config.settings or {}; cfg = p.config.settings or { };
dir = dir = if (p.needs.stateDirs or [ ]) == [ ] then null else lib.head (p.needs.stateDirs or [ ]);
if (p.needs.stateDirs or []) == []
then null
else lib.head (p.needs.stateDirs or []);
in in
if cfg == {} then if cfg == { } then
[] [ ]
else else
(if dir == null then (
if dir == null then
throw "plugin ${p.name} provides settings but no stateDirs are defined" throw "plugin ${p.name} provides settings but no stateDirs are defined"
else [ else
{ [
name = toRelative (resolvePath ("~/" + dir + "/config.json")); {
value = { text = builtins.toJSON cfg; }; name = toRelative (resolvePath ("~/" + dir + "/config.json"));
} value = {
]); text = builtins.toJSON cfg;
};
}
]
);
in in
lib.flatten (map mkEntries plugins); lib.flatten (map mkEntries plugins);
entries = lib.flatten (lib.mapAttrsToList entryFor enabledInstances); entries = lib.flatten (lib.mapAttrsToList entryFor enabledInstances);
in in
lib.listToAttrs entries; lib.listToAttrs entries;
pluginSkillAssertions = pluginSkillAssertions =
let let
skillTargets = skillTargets = lib.flatten (
lib.flatten (lib.concatLists (lib.mapAttrsToList (instName: inst: lib.concatLists (
let lib.mapAttrsToList (
base = "${toRelative (resolvePath inst.workspaceDir)}/skills"; instName: inst:
plugins = resolvedPluginsByInstance.${instName} or []; let
in base = "${toRelative (resolvePath inst.workspaceDir)}/skills";
map (p: plugins = resolvedPluginsByInstance.${instName} or [ ];
map (skillPath: in
"${base}/${p.name}/${builtins.baseNameOf skillPath}" map (p: map (skillPath: "${base}/${p.name}/${builtins.baseNameOf skillPath}") p.skills) plugins
) p.skills ) enabledInstances
) plugins )
) enabledInstances)); );
counts = lib.foldl' (acc: path: counts = lib.foldl' (acc: path: acc // { "${path}" = (acc.${path} or 0) + 1; }) { } skillTargets;
acc // { "${path}" = (acc.${path} or 0) + 1; }
) {} skillTargets;
duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts); duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts);
in in
if duplicates == [] then [] else [ if duplicates == [ ] then
[ ]
else
[
{ {
assertion = false; assertion = false;
message = "Duplicate skill paths detected: ${lib.concatStringsSep ", " duplicates}"; message = "Duplicate skill paths detected: ${lib.concatStringsSep ", " duplicates}";
@ -193,13 +224,14 @@ let
exit 1 exit 1
fi fi
''; '';
entriesForInstance = instName: entriesForInstance =
map (entry: entry // { instance = instName; }) (pluginEnvFor instName); instName: map (entry: entry // { instance = instName; }) (pluginEnvFor instName);
entries = lib.flatten (map entriesForInstance (lib.attrNames enabledInstances)); entries = lib.flatten (map entriesForInstance (lib.attrNames enabledInstances));
in in
lib.concatStringsSep "\n" (map renderCheck entries); lib.concatStringsSep "\n" (map renderCheck entries);
in { in
{
inherit inherit
resolvedPluginsByInstance resolvedPluginsByInstance
pluginPackagesFor pluginPackagesFor
@ -212,5 +244,6 @@ in {
pluginSkillsFiles pluginSkillsFiles
pluginConfigFiles pluginConfigFiles
pluginSkillAssertions pluginSkillAssertions
pluginGuards; pluginGuards
;
} }

View File

@ -2,13 +2,18 @@ final: prev:
let let
packages = import ./packages { pkgs = prev; }; packages = import ./packages { pkgs = prev; };
toolNames = (import ./tools/extended.nix { pkgs = prev; }).toolNames; toolNames = (import ./tools/extended.nix { pkgs = prev; }).toolNames;
withTools = { toolNamesOverride ? null, excludeToolNames ? [] }: withTools =
{
toolNamesOverride ? null,
excludeToolNames ? [ ],
}:
import ./packages { import ./packages {
pkgs = prev; pkgs = prev;
inherit toolNamesOverride excludeToolNames; inherit toolNamesOverride excludeToolNames;
}; };
in in
packages // { packages
// {
openclawPackages = packages // { openclawPackages = packages // {
inherit toolNames withTools; inherit toolNames withTools;
}; };

View File

@ -1,8 +1,9 @@
{ pkgs {
, sourceInfo ? import ../sources/openclaw-source.nix pkgs,
, steipetePkgs ? {} sourceInfo ? import ../sources/openclaw-source.nix,
, toolNamesOverride ? null steipetePkgs ? { },
, excludeToolNames ? [] toolNamesOverride ? null,
excludeToolNames ? [ ],
}: }:
let let
isDarwin = pkgs.stdenv.hostPlatform.isDarwin; isDarwin = pkgs.stdenv.hostPlatform.isDarwin;
@ -26,8 +27,10 @@ let
openclaw-app = openclawApp; openclaw-app = openclawApp;
extendedTools = toolSets.tools; extendedTools = toolSets.tools;
}; };
in { in
{
openclaw-gateway = openclawGateway; openclaw-gateway = openclawGateway;
openclaw = openclawBundle; openclaw = openclawBundle;
openclaw-tools = openclawTools; openclaw-tools = openclawTools;
} // (if isDarwin then { openclaw-app = openclawApp; } else {}) }
// (if isDarwin then { openclaw-app = openclawApp; } else { })

View File

@ -1,6 +1,7 @@
{ lib {
, stdenvNoCC lib,
, fetchzip stdenvNoCC,
fetchzip,
}: }:
stdenvNoCC.mkDerivation { stdenvNoCC.mkDerivation {

View File

@ -1,8 +1,9 @@
{ lib {
, buildEnv lib,
, openclaw-gateway buildEnv,
, openclaw-app ? null openclaw-gateway,
, extendedTools ? [] openclaw-app ? null,
extendedTools ? [ ],
}: }:
let let

View File

@ -8,45 +8,55 @@
nix-openclaw.url = "github:openclaw/nix-openclaw"; nix-openclaw.url = "github:openclaw/nix-openclaw";
}; };
outputs = { nixpkgs, home-manager, nix-openclaw, ... }: outputs =
{
nixpkgs,
home-manager,
nix-openclaw,
...
}:
let let
system = "aarch64-darwin"; system = "aarch64-darwin";
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
overlays = [ nix-openclaw.overlays.default ]; overlays = [ nix-openclaw.overlays.default ];
}; };
in { in
{
homeConfigurations.hm-test = home-manager.lib.homeManagerConfiguration { homeConfigurations.hm-test = home-manager.lib.homeManagerConfiguration {
inherit pkgs; inherit pkgs;
modules = [ modules = [
nix-openclaw.homeManagerModules.openclaw nix-openclaw.homeManagerModules.openclaw
({ ... }: { (
home = { { ... }:
username = "runner"; {
homeDirectory = "/tmp/hm-activation-home"; home = {
stateVersion = "23.11"; username = "runner";
}; homeDirectory = "/tmp/hm-activation-home";
stateVersion = "23.11";
};
programs.openclaw = { programs.openclaw = {
enable = true; enable = true;
installApp = false; installApp = false;
instances.default = { instances.default = {
gatewayPort = 18999; gatewayPort = 18999;
config = { config = {
logging = { logging = {
level = "debug"; level = "debug";
file = "/tmp/openclaw/openclaw-gateway.log"; file = "/tmp/openclaw/openclaw-gateway.log";
}; };
gateway = { gateway = {
mode = "local"; mode = "local";
auth = { auth = {
token = "hm-activation-test-token"; token = "hm-activation-test-token";
};
}; };
}; };
}; };
}; };
}; }
}) )
]; ];
}; };
}; };

View File

@ -1,19 +1,26 @@
{ pkgs {
, steipetePkgs ? {} pkgs,
, toolNamesOverride ? null steipetePkgs ? { },
, excludeToolNames ? [] toolNamesOverride ? null,
excludeToolNames ? [ ],
}: }:
let let
lib = pkgs.lib; lib = pkgs.lib;
safe = list: builtins.filter (p: p != null) list; safe = list: builtins.filter (p: p != null) list;
pickFrom = scope: name: pickFrom =
scope: name:
if builtins.hasAttr name scope then if builtins.hasAttr name scope then
let pkg = scope.${name}; in let
pkg = scope.${name};
in
if lib.meta.availableOn pkgs.stdenv.hostPlatform pkg then pkg else null if lib.meta.availableOn pkgs.stdenv.hostPlatform pkg then pkg else null
else else
null; null;
pick = name: pick =
let fromSteipete = pickFrom steipetePkgs name; in name:
let
fromSteipete = pickFrom steipetePkgs name;
in
if fromSteipete != null then fromSteipete else pickFrom pkgs name; if fromSteipete != null then fromSteipete else pickFrom pkgs name;
ensure = names: safe (map pick names); ensure = names: safe (map pick names);
@ -55,7 +62,8 @@ let
toolNamesBase = if toolNamesOverride != null then toolNamesOverride else baseNames ++ extraNames; toolNamesBase = if toolNamesOverride != null then toolNamesOverride else baseNames ++ extraNames;
toolNames = builtins.filter (name: !builtins.elem name excludeToolNames) toolNamesBase; toolNames = builtins.filter (name: !builtins.elem name excludeToolNames) toolNamesBase;
in { in
{
tools = ensure toolNames; tools = ensure toolNames;
toolNames = toolNames; toolNames = toolNames;
} }

View File

@ -8,12 +8,22 @@
nix-openclaw.url = "github:openclaw/nix-openclaw"; nix-openclaw.url = "github:openclaw/nix-openclaw";
}; };
outputs = { self, nixpkgs, home-manager, nix-openclaw }: outputs =
{
self,
nixpkgs,
home-manager,
nix-openclaw,
}:
let let
# REPLACE: aarch64-darwin (Apple Silicon), x86_64-darwin (Intel), or x86_64-linux # REPLACE: aarch64-darwin (Apple Silicon), x86_64-darwin (Intel), or x86_64-linux
system = "<system>"; system = "<system>";
pkgs = import nixpkgs { inherit system; overlays = [ nix-openclaw.overlays.default ]; }; pkgs = import nixpkgs {
in { inherit system;
overlays = [ nix-openclaw.overlays.default ];
};
in
{
# REPLACE: <user> with your username (run `whoami`) # REPLACE: <user> with your username (run `whoami`)
homeConfigurations."<user>" = home-manager.lib.homeManagerConfiguration { homeConfigurations."<user>" = home-manager.lib.homeManagerConfiguration {
inherit pkgs; inherit pkgs;
@ -47,7 +57,9 @@
# REPLACE: your Telegram user ID (get from @userinfobot) # REPLACE: your Telegram user ID (get from @userinfobot)
allowFrom = [ <allowFrom> ]; allowFrom = [ <allowFrom> ];
groups = { groups = {
"*" = { requireMention = true; }; "*" = {
requireMention = true;
};
}; };
}; };
}; };