consume QMD through OpenClaw tools

What:
- consume QMD from nix-openclaw-tools instead of a separate upstream flake input
- expose QMD as an internal OpenClaw battery on Darwin and Linux
- add an opt-in Home Manager qmd model prewarm activation
- keep plugin packages off the user's shell PATH by default while preserving the runtime PATH

Why:
- nix-openclaw-tools owns reproducible tool packages and cacheable plugin metadata
- nixos-config should configure OpenClaw, not hand-wire runtime tools

Tests:
- nix build .#checks.aarch64-darwin.package-contents --accept-flake-config --no-link
- nix build .#checks.aarch64-darwin.qmd-runtime --accept-flake-config --no-link
- nix build .#checks.aarch64-darwin.bin-surface .#checks.aarch64-darwin.config-validity .#checks.aarch64-darwin.gateway-smoke --accept-flake-config --no-link
- nix eval .#checks.x86_64-linux.default-instance.drvPath --accept-flake-config
This commit is contained in:
joshp123 2026-05-06 09:43:53 +02:00
parent 0a70262dda
commit d56fa8a75c
13 changed files with 75 additions and 52 deletions

View File

@ -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 |

View File

@ -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

View File

@ -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

33
flake.lock generated
View File

@ -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": {

View File

@ -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;

View File

@ -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

View File

@ -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 != { })
(

View File

@ -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

View File

@ -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;

View File

@ -54,6 +54,12 @@
linux = true;
};
qmd = {
tool = "qmd";
description = "Search local markdown knowledge bases";
linux = true;
};
sonoscli = {
tool = "sonoscli";
description = "Control Sonos speakers";

View File

@ -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.";
};

View File

@ -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

View File

@ -30,4 +30,5 @@ in
openclaw-gateway = openclawGateway;
openclaw = openclawBundle;
}
// (if qmdPackage != null then { qmd = qmdPackage; } else { })
// (if isDarwin then { openclaw-app = openclawApp; } else { })