diff --git a/README.md b/README.md index 61ef0d4..9ee0723 100644 --- a/README.md +++ b/README.md @@ -400,7 +400,7 @@ Contract to implement: 1) Add openclawPlugin output in flake.nix: - name - skills (paths to SKILL.md dirs) - - packages (CLI packages to put on PATH) + - packages (CLI packages to put on the OpenClaw runtime PATH) - needs (stateDirs + requiredEnv) Example: @@ -713,6 +713,16 @@ programs.openclaw.config = { QMD stays inside the `openclaw` wrapper PATH, so users do not need to install a separate `qmd` command. The builtin `memorySearch.provider = "local"` path is an escape hatch for people who want to manage `node-llama-cpp` themselves; it is not the primary Nix-supported path. +Plugin CLIs are also kept on the OpenClaw runtime PATH by default, not on the user's login shell PATH. Set `programs.openclaw.exposePluginPackages = true` only when you explicitly want plugin CLIs in `home.packages`. + +Optional model prewarming is also declarative: + +```nix +programs.openclaw.qmd.prewarmModels.enable = true; +``` + +That runs `qmd pull` during Home Manager activation and stores the default embedding, expansion, and reranking models in the user's QMD cache. Expect about 2.25GB of cache use. + ### What we manage vs what you manage | Component | Nix manages | You manage | diff --git a/docs/plugins-maintainers.md b/docs/plugins-maintainers.md index 01cbacf..7532501 100644 --- a/docs/plugins-maintainers.md +++ b/docs/plugins-maintainers.md @@ -14,7 +14,7 @@ Every plugin artifact exposes the same fields (flake output `openclawPlugin` tod openclawPlugin = { name = "summarize"; # unique; last-wins on collision skills = [ ./skills/summarize ]; # dirs containing SKILL.md - packages = [ pkgs.summarize-cli ]; # binaries placed on PATH + packages = [ pkgs.summarize-cli ]; # binaries placed on the OpenClaw runtime PATH needs = { stateDirs = [ ".config/summarize" ]; # created under $HOME requiredEnv = [ "SUMMARIZE_API_KEY" ]; # must point to files diff --git a/docs/rfc/2026-01-11-plugin-system.md b/docs/rfc/2026-01-11-plugin-system.md index f51c9d2..11573b1 100644 --- a/docs/rfc/2026-01-11-plugin-system.md +++ b/docs/rfc/2026-01-11-plugin-system.md @@ -50,7 +50,7 @@ plugin/ That's it. No registry. No central authority. Point at a repo, get a plugin. One install gives you: -- **Binary** on PATH (built from source, pinned version) +- **Binary** on the OpenClaw runtime PATH (built from source, pinned version) - **Skills** in workspace (agent knows how to use it) - **Config** validated (missing env = install fails, not runtime error) - **State dirs** created (plugin has a home) @@ -203,7 +203,7 @@ voicecall status --call-id abc123 # Check for responses 4. **Create state dirs** — from manifest 5. **Add `openclaw plugins` CLI** — list, enable, disable, info -That's it. No dynamic code loading, no TypeBox registration, no RPC handlers. Just: find plugins, validate their needs, put binaries on PATH, copy skills to workspace. +That's it. No dynamic code loading, no TypeBox registration, no RPC handlers. Just: find plugins, validate their needs, put binaries on the OpenClaw runtime PATH, copy skills to workspace. ### How nix-openclaw fits in diff --git a/flake.lock b/flake.lock index 95e0014..0120adc 100644 --- a/flake.lock +++ b/flake.lock @@ -43,11 +43,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1777976020, - "narHash": "sha256-IsgLwW0Y6JYiWXbxmzN1FDO0//Osu2YpeID1tFMbwkk=", + "lastModified": 1778052717, + "narHash": "sha256-EsQiDKKwBS8so+OzjPOZH+z+JOJeREAsOJ/fJAx3WCY=", "owner": "openclaw", "repo": "nix-openclaw-tools", - "rev": "08955054f466e2eb55628763c1d7ee2de5af9f6d", + "rev": "a0e7ac5ef1b6f5d1940e3efdb9ebad2dc04467f1", "type": "github" }, "original": { @@ -88,37 +88,12 @@ "type": "github" } }, - "qmd": { - "inputs": { - "flake-utils": [ - "flake-utils" - ], - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1775429264, - "narHash": "sha256-bqIVaNRTa8H5vrw3RwsD7QdtTa0xNvRuEVzlzE1hIBQ=", - "owner": "tobi", - "repo": "qmd", - "rev": "65cd1b3fd02891d1ee0eefa751620918664fa321", - "type": "github" - }, - "original": { - "owner": "tobi", - "ref": "v2.1.0", - "repo": "qmd", - "type": "github" - } - }, "root": { "inputs": { "flake-utils": "flake-utils", "home-manager": "home-manager", "nix-openclaw-tools": "nix-openclaw-tools", - "nixpkgs": "nixpkgs_2", - "qmd": "qmd" + "nixpkgs": "nixpkgs_2" } }, "systems": { diff --git a/flake.nix b/flake.nix index de53aed..969568e 100644 --- a/flake.nix +++ b/flake.nix @@ -14,9 +14,6 @@ home-manager.url = "github:nix-community/home-manager"; home-manager.inputs.nixpkgs.follows = "nixpkgs"; nix-openclaw-tools.url = "github:openclaw/nix-openclaw-tools"; - qmd.url = "github:tobi/qmd/v2.1.0"; - qmd.inputs.flake-utils.follows = "flake-utils"; - qmd.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = @@ -26,7 +23,6 @@ flake-utils, home-manager, nix-openclaw-tools, - qmd, }: let openclawToolPkgsFor = @@ -35,14 +31,10 @@ nix-openclaw-tools.packages.${system} else { }; - qmdPkgsFor = - system: - if qmd ? packages && builtins.hasAttr system qmd.packages then qmd.packages.${system} else { }; overlay = final: prev: import ./nix/overlay.nix { openclawToolPkgs = openclawToolPkgsFor prev.stdenv.hostPlatform.system; - qmdPkgs = qmdPkgsFor prev.stdenv.hostPlatform.system; } final prev; sourceInfoStable = import ./nix/sources/openclaw-source.nix; systems = [ @@ -58,9 +50,7 @@ overlays = [ overlay ]; }; openclawToolPkgs = openclawToolPkgsFor system; - qmdPkgs = qmdPkgsFor system; - qmdPackage = - if pkgs.stdenv.hostPlatform.isDarwin then null else qmdPkgs.qmd or qmdPkgs.default or null; + qmdPackage = openclawToolPkgs.qmd or null; packageSetStable = import ./nix/packages { pkgs = pkgs; sourceInfo = sourceInfoStable; diff --git a/nix/checks/openclaw-default-instance.nix b/nix/checks/openclaw-default-instance.nix index f41eae9..14d825d 100644 --- a/nix/checks/openclaw-default-instance.nix +++ b/nix/checks/openclaw-default-instance.nix @@ -205,12 +205,26 @@ let throw "secrets.providers file variant missing from generated config." ); + qmdPrewarmEval = moduleEval { + qmd.prewarmModels.enable = true; + }; + qmdPrewarmActivation = builtins.toJSON qmdPrewarmEval.config.home.activation.openclawQmdPrewarm; + qmdPrewarmCheck = + builtins.deepSeq (requireNoAssertionFailures "qmd.prewarmModels" qmdPrewarmEval) + ( + if lib.hasInfix "/bin/qmd pull" qmdPrewarmActivation then + "ok" + else + throw "qmd.prewarmModels did not wire qmd pull activation." + ); + checkKey = builtins.deepSeq [ defaultCheck customPluginCheck duplicateSkillCheck userPluginSkillCollisionCheck secretProviderCheck + qmdPrewarmCheck ] "ok"; in diff --git a/nix/modules/home-manager/openclaw/config.nix b/nix/modules/home-manager/openclaw/config.nix index 91bf375..b592b8a 100644 --- a/nix/modules/home-manager/openclaw/config.nix +++ b/nix/modules/home-manager/openclaw/config.nix @@ -10,6 +10,7 @@ let cfg = openclawLib.cfg; homeDir = openclawLib.homeDir; appPackage = openclawLib.appPackage; + qmdPackage = openclawLib.qmdPackage; defaultInstance = { enable = cfg.enable; @@ -262,7 +263,13 @@ in ] ++ files.documentsAssertions ++ files.duplicateSkillAssertion - ++ plugins.pluginAssertions; + ++ plugins.pluginAssertions + ++ [ + { + assertion = !cfg.qmd.prewarmModels.enable || qmdPackage != null; + message = "programs.openclaw.qmd.prewarmModels.enable requires a qmd package in openclawPackages."; + } + ]; home.packages = lib.unique ( (map (item: item.package) instanceConfigs) @@ -316,6 +323,19 @@ in ${plugins.pluginGuards} ''; + home.activation.openclawQmdPrewarm = + lib.mkIf (cfg.qmd.prewarmModels.enable && qmdPackage != null) + ( + lib.hm.dag.entryAfter [ "openclawDirs" ] '' + run --quiet ${lib.getExe' pkgs.coreutils "env"} \ + HOME=${lib.escapeShellArg homeDir} \ + XDG_CACHE_HOME=${lib.escapeShellArg "${homeDir}/.cache"} \ + XDG_CONFIG_HOME=${lib.escapeShellArg "${homeDir}/.config"} \ + XDG_DATA_HOME=${lib.escapeShellArg "${homeDir}/.local/share"} \ + ${qmdPackage}/bin/qmd pull + '' + ); + home.activation.openclawAppDefaults = lib.mkIf (pkgs.stdenv.hostPlatform.isDarwin && appDefaults != { }) ( diff --git a/nix/modules/home-manager/openclaw/lib.nix b/nix/modules/home-manager/openclaw/lib.nix index 85fbf52..18b2d9e 100644 --- a/nix/modules/home-manager/openclaw/lib.nix +++ b/nix/modules/home-manager/openclaw/lib.nix @@ -21,13 +21,14 @@ let else cfg.package; appPackage = if cfg.appPackage != null then cfg.appPackage else defaultPackage; + qmdPackage = pkgs.openclawPackages.qmd or null; generatedConfigOptions = import ../../../generated/openclaw-config-options.nix { lib = lib; }; pluginCatalog = import ./plugin-catalog.nix; bundledPluginSources = let - openclawToolsRev = "08955054f466e2eb55628763c1d7ee2de5af9f6d"; - openclawToolsNarHash = "sha256-IsgLwW0Y6JYiWXbxmzN1FDO0//Osu2YpeID1tFMbwkk="; + openclawToolsRev = "a0e7ac5ef1b6f5d1940e3efdb9ebad2dc04467f1"; + openclawToolsNarHash = "sha256-EsQiDKKwBS8so+OzjPOZH+z+JOJeREAsOJ/fJAx3WCY="; openclawTools = tool: "github:openclaw/nix-openclaw-tools?dir=tools/${tool}&rev=${openclawToolsRev}&narHash=${openclawToolsNarHash}"; @@ -66,6 +67,7 @@ in toolSets defaultPackage appPackage + qmdPackage generatedConfigOptions bundledPluginSources bundledPlugins diff --git a/nix/modules/home-manager/openclaw/options.nix b/nix/modules/home-manager/openclaw/options.nix index d4fba4f..2fa0314 100644 --- a/nix/modules/home-manager/openclaw/options.nix +++ b/nix/modules/home-manager/openclaw/options.nix @@ -184,10 +184,16 @@ in exposePluginPackages = lib.mkOption { type = lib.types.bool; - default = true; + default = false; description = "Add plugin packages to home.packages so CLIs are on PATH."; }; + qmd.prewarmModels.enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Download/check QMD's default GGUF models during Home Manager activation. This uses about 2.25GB under the user's QMD cache."; + }; + reloadScript = { enable = lib.mkOption { type = lib.types.bool; diff --git a/nix/modules/home-manager/openclaw/plugin-catalog.nix b/nix/modules/home-manager/openclaw/plugin-catalog.nix index 89a1a24..9027945 100644 --- a/nix/modules/home-manager/openclaw/plugin-catalog.nix +++ b/nix/modules/home-manager/openclaw/plugin-catalog.nix @@ -54,6 +54,12 @@ linux = true; }; + qmd = { + tool = "qmd"; + description = "Search local markdown knowledge bases"; + linux = true; + }; + sonoscli = { tool = "sonoscli"; description = "Control Sonos speakers"; diff --git a/nix/modules/nixos/openclaw-gateway.nix b/nix/modules/nixos/openclaw-gateway.nix index 5b97dde..cffa729 100644 --- a/nix/modules/nixos/openclaw-gateway.nix +++ b/nix/modules/nixos/openclaw-gateway.nix @@ -41,7 +41,7 @@ in package = mkOption { type = types.package; - default = if pkgs ? openclaw-gateway then pkgs.openclaw-gateway else pkgs.openclaw; + default = if pkgs ? openclaw then pkgs.openclaw else pkgs.openclaw-gateway; description = "OpenClaw gateway package."; }; diff --git a/nix/overlay.nix b/nix/overlay.nix index 93311c7..8f1bf3c 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -1,13 +1,12 @@ { openclawToolPkgs ? { }, - qmdPkgs ? { }, }: final: prev: let packages = import ./packages { pkgs = prev; openclawToolPkgs = openclawToolPkgs; - qmdPackage = qmdPkgs.qmd or qmdPkgs.default or null; + qmdPackage = openclawToolPkgs.qmd or null; }; toolNames = (import ./tools/extended.nix { @@ -22,7 +21,7 @@ let import ./packages { pkgs = prev; openclawToolPkgs = openclawToolPkgs; - qmdPackage = qmdPkgs.qmd or qmdPkgs.default or null; + qmdPackage = openclawToolPkgs.qmd or null; inherit toolNamesOverride excludeToolNames; }; in diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 83014b9..ea0e6a9 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -30,4 +30,5 @@ in openclaw-gateway = openclawGateway; openclaw = openclawBundle; } +// (if qmdPackage != null then { qmd = qmdPackage; } else { }) // (if isDarwin then { openclaw-app = openclawApp; } else { })