Expose runtime tools to OpenClaw Codex harness

Add Home Manager runtimePackages/environment options that feed the gateway wrapper without polluting the user PATH. Link the same runtime package set into Codex's isolated agent home so shell calls from the Codex harness see Nix-managed plugin and helper CLIs.

Tests: ./scripts/check-flake-lock-owners.sh; nix flake show --accept-flake-config; nix build --accept-flake-config .#checks.aarch64-darwin.ci --no-link; nix build --accept-flake-config .#checks.aarch64-darwin.qmd-runtime .#checks.aarch64-darwin.bin-surface .#checks.aarch64-darwin.package-contents --no-link; nix eval --accept-flake-config .#checks.x86_64-linux.default-instance.drvPath; ./scripts/hm-activation-macos.sh
This commit is contained in:
joshp123 2026-05-06 14:44:56 +02:00
parent fd30aad492
commit 54e09bce18
8 changed files with 140 additions and 22 deletions

View File

@ -209,16 +209,30 @@ let
qmd.prewarmModels.enable = true;
};
qmdPrewarmActivation = builtins.toJSON qmdPrewarmEval.config.home.activation.openclawQmdPrewarm;
qmdPrewarmCheck =
builtins.deepSeq (requireNoAssertionFailures "qmd.prewarmModels" qmdPrewarmEval)
qmdPrewarmCheck = builtins.deepSeq (requireNoAssertionFailures "qmd.prewarmModels" qmdPrewarmEval) (
if
lib.hasInfix "OPENCLAW_QMD_BIN=" qmdPrewarmActivation
&& lib.hasInfix "openclaw-qmd-prewarm.sh" qmdPrewarmActivation
then
"ok"
else
throw "qmd.prewarmModels did not wire QMD model-cache prewarm activation."
);
runtimeProfileEval = moduleEval {
runtimePackages = [ pkgs.jq ];
environment.OPENCLAW_TEST_SECRET = "/tmp/openclaw-secret";
};
runtimeProfileActivation = builtins.toJSON runtimeProfileEval.config.home.activation.openclawCodexRuntimeProfiles;
runtimeProfileCheck =
builtins.deepSeq (requireNoAssertionFailures "runtime profile" runtimeProfileEval)
(
if
lib.hasInfix "OPENCLAW_QMD_BIN=" qmdPrewarmActivation
&& lib.hasInfix "openclaw-qmd-prewarm.sh" qmdPrewarmActivation
lib.hasInfix "openclaw-link-codex-runtime-profiles.sh" runtimeProfileActivation
then
"ok"
else
throw "qmd.prewarmModels did not wire QMD model-cache prewarm activation."
throw "runtimePackages did not wire the Codex runtime profile activation."
);
checkKey = builtins.deepSeq [
@ -228,6 +242,7 @@ let
userPluginSkillCollisionCheck
secretProviderCheck
qmdPrewarmCheck
runtimeProfileCheck
] "ok";
in

View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
manifest=$1
while IFS=$'\t' read -r profile_dir bin_dir; do
[ -n "$profile_dir" ] || continue
mkdir -p "$profile_dir"
link="$profile_dir/bin"
if [ -L "$link" ]; then
rm "$link"
fi
if [ -e "$link" ]; then
echo "Refusing to replace non-symlink Codex runtime bin: $link" >&2
exit 1
fi
ln -s "$bin_dir" "$link"
done < "$manifest"

View File

@ -22,6 +22,8 @@ let
gatewayPort = 18789;
gatewayPath = null;
gatewayPnpmDepsHash = lib.fakeHash;
runtimePackages = [ ];
environment = { };
launchd = cfg.launchd;
systemd = cfg.systemd;
plugins = openclawLib.effectivePlugins;
@ -97,7 +99,24 @@ let
else
inst.package;
pluginPackages = plugins.pluginPackagesFor name;
pluginEnvAll = plugins.pluginEnvAllFor name;
runtimePackages = lib.unique (
openclawLib.toolSets.tools
++ (lib.optional (qmdPackage != null) qmdPackage)
++ pluginPackages
++ cfg.runtimePackages
++ inst.runtimePackages
);
runtimeProfile = pkgs.symlinkJoin {
name = "openclaw-runtime-${name}";
paths = runtimePackages;
};
runtimePath = lib.makeBinPath runtimePackages;
runtimeEnvAll =
(plugins.pluginEnvAllFor name)
++ (lib.mapAttrsToList (key: value: {
inherit key value;
plugin = "runtime";
}) (cfg.environment // inst.environment));
mergedConfig0 = stripNulls (
lib.recursiveUpdate (lib.recursiveUpdate baseConfig (stripNulls cfg.config)) (
stripNulls inst.config
@ -117,11 +136,20 @@ let
mergedConfig0;
configJson = builtins.toJSON mergedConfig;
configFile = pkgs.writeText "openclaw-${name}.json" configJson;
agentIds =
let
agents = ((mergedConfig.agents or { }).list or [ ]);
configured = lib.filter (id: id != null) (map (agent: agent.id or null) agents);
in
lib.unique ([ "main" ] ++ configured);
codexRuntimeProfiles = map (
agentId: "${inst.stateDir}/agents/${agentId}/agent/codex-home/home/.nix-profile"
) agentIds;
gatewayWrapper = pkgs.writeShellScriptBin "openclaw-gateway-${name}" ''
set -euo pipefail
if [ -n "${lib.makeBinPath pluginPackages}" ]; then
export PATH="${lib.makeBinPath pluginPackages}:$PATH"
if [ -n "${runtimePath}" ]; then
export PATH="${runtimePath}:$PATH"
fi
${lib.concatStringsSep "\n" (
@ -146,7 +174,7 @@ let
export ${entry.key}="${entry.value}"
fi
''
) pluginEnvAll
) runtimeEnvAll
)}
exec "${gatewayPackage}/bin/openclaw" "$@"
@ -181,6 +209,8 @@ let
};
configFile = configFile;
configPath = inst.configPath;
codexRuntimeProfiles = codexRuntimeProfiles;
runtimeProfile = runtimeProfile;
dirs = [
inst.stateDir
@ -245,6 +275,21 @@ let
};
instanceConfigs = lib.mapAttrsToList mkInstanceConfig enabledInstances;
codexRuntimeProfileEntries = lib.flatten (
map (
item:
map (profileDir: {
inherit profileDir;
binDir = "${item.runtimeProfile}/bin";
}) item.codexRuntimeProfiles
) instanceConfigs
);
codexRuntimeProfilesManifest = pkgs.writeText "openclaw-codex-runtime-profiles.tsv" (
(lib.concatStringsSep "\n" (
map (entry: "${entry.profileDir}\t${entry.binDir}") codexRuntimeProfileEntries
))
+ "\n"
);
appInstalls = lib.filter (item: item != null) (map (item: item.appInstall) instanceConfigs);
launchdLabels = lib.filter (label: label != null) (map (item: item.launchdLabel) instanceConfigs);
launchdLabelArgs = lib.concatStringsSep " " (map lib.escapeShellArg launchdLabels);
@ -318,24 +363,28 @@ in
)}
'';
home.activation.openclawCodexRuntimeProfiles = lib.mkIf (codexRuntimeProfileEntries != [ ]) (
lib.hm.dag.entryAfter [ "openclawDirs" ] ''
run --quiet ${pkgs.bash}/bin/bash ${../openclaw-link-codex-runtime-profiles.sh} ${codexRuntimeProfilesManifest}
''
);
home.activation.openclawPluginGuard = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
set -euo pipefail
${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"} \
OPENCLAW_QMD_BIN=${lib.escapeShellArg "${qmdPackage}/bin/qmd"} \
${pkgs.bash}/bin/bash ${../../../scripts/openclaw-qmd-prewarm.sh}
''
);
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"} \
OPENCLAW_QMD_BIN=${lib.escapeShellArg "${qmdPackage}/bin/qmd"} \
${pkgs.bash}/bin/bash ${../../../scripts/openclaw-qmd-prewarm.sh}
''
);
home.activation.openclawAppDefaults =
lib.mkIf (pkgs.stdenv.hostPlatform.isDarwin && appDefaults != { })

View File

@ -65,6 +65,18 @@
description = "pnpmDeps hash for local gateway builds (omit to let Nix suggest the correct hash).";
};
runtimePackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = "Extra packages visible to this OpenClaw instance and its isolated Codex harness only. These are not added to the user's PATH.";
};
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Extra runtime environment for this OpenClaw gateway wrapper. Values that point to files are read at runtime unless the variable name ends in _FILE.";
};
plugins = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {

View File

@ -107,6 +107,18 @@ in
};
};
runtimePackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = "Extra packages visible to the OpenClaw gateway and isolated Codex harness only. These are not added to the user's PATH.";
};
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Extra runtime environment for OpenClaw gateway wrappers. Values that point to files are read at runtime unless the variable name ends in _FILE.";
};
documents = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;

View File

@ -38,6 +38,8 @@
programs.openclaw = {
enable = true;
installApp = false;
runtimePackages = [ pkgs.jq ];
environment.OPENCLAW_TEST_SECRET = "/tmp/openclaw-secret";
instances.default = {
gatewayPort = 18999;
logPath = "/tmp/hm-activation-home/.openclaw/openclaw-gateway.log";

View File

@ -9,6 +9,9 @@ machine.wait_until_succeeds("test -f /home/alice/.openclaw/workspace/AGENTS.md")
machine.succeed("test ! -L /home/alice/.openclaw/workspace/AGENTS.md")
machine.wait_until_succeeds("test -f /home/alice/.openclaw/workspace/skills/skill/SKILL.md")
machine.succeed("test ! -L /home/alice/.openclaw/workspace/skills/skill")
machine.wait_until_succeeds(
"test -x /home/alice/.openclaw/agents/main/agent/codex-home/home/.nix-profile/bin/jq"
)
uid = machine.succeed("id -u alice").strip()
machine.succeed("loginctl enable-linger alice")

View File

@ -35,6 +35,8 @@ nix build --accept-flake-config --impure \
test -f "$HOME/.openclaw/openclaw.json"
test -f "$plist"
test -L "$HOME/.openclaw/agents/main/agent/codex-home/home/.nix-profile/bin"
test -x "$HOME/.openclaw/agents/main/agent/codex-home/home/.nix-profile/bin/jq"
if command -v launchctl >/dev/null 2>&1; then
state_file="$home_dir/launchd-state.txt"
@ -52,6 +54,7 @@ if command -v launchctl >/dev/null 2>&1; then
fi
openclaw_bin=$(/usr/libexec/PlistBuddy -c "Print :ProgramArguments:0" "$plist")
grep -q OPENCLAW_TEST_SECRET "$openclaw_bin"
health_file="$home_dir/gateway-health.json"
healthy=false
for _ in {1..30}; do