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; qmd.prewarmModels.enable = true;
}; };
qmdPrewarmActivation = builtins.toJSON qmdPrewarmEval.config.home.activation.openclawQmdPrewarm; qmdPrewarmActivation = builtins.toJSON qmdPrewarmEval.config.home.activation.openclawQmdPrewarm;
qmdPrewarmCheck = qmdPrewarmCheck = builtins.deepSeq (requireNoAssertionFailures "qmd.prewarmModels" qmdPrewarmEval) (
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 if
lib.hasInfix "OPENCLAW_QMD_BIN=" qmdPrewarmActivation lib.hasInfix "openclaw-link-codex-runtime-profiles.sh" runtimeProfileActivation
&& lib.hasInfix "openclaw-qmd-prewarm.sh" qmdPrewarmActivation
then then
"ok" "ok"
else 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 [ checkKey = builtins.deepSeq [
@ -228,6 +242,7 @@ let
userPluginSkillCollisionCheck userPluginSkillCollisionCheck
secretProviderCheck secretProviderCheck
qmdPrewarmCheck qmdPrewarmCheck
runtimeProfileCheck
] "ok"; ] "ok";
in 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; gatewayPort = 18789;
gatewayPath = null; gatewayPath = null;
gatewayPnpmDepsHash = lib.fakeHash; gatewayPnpmDepsHash = lib.fakeHash;
runtimePackages = [ ];
environment = { };
launchd = cfg.launchd; launchd = cfg.launchd;
systemd = cfg.systemd; systemd = cfg.systemd;
plugins = openclawLib.effectivePlugins; plugins = openclawLib.effectivePlugins;
@ -97,7 +99,24 @@ let
else else
inst.package; inst.package;
pluginPackages = plugins.pluginPackagesFor name; 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 ( mergedConfig0 = stripNulls (
lib.recursiveUpdate (lib.recursiveUpdate baseConfig (stripNulls cfg.config)) ( lib.recursiveUpdate (lib.recursiveUpdate baseConfig (stripNulls cfg.config)) (
stripNulls inst.config stripNulls inst.config
@ -117,11 +136,20 @@ let
mergedConfig0; mergedConfig0;
configJson = builtins.toJSON mergedConfig; configJson = builtins.toJSON mergedConfig;
configFile = pkgs.writeText "openclaw-${name}.json" configJson; 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}" '' gatewayWrapper = pkgs.writeShellScriptBin "openclaw-gateway-${name}" ''
set -euo pipefail set -euo pipefail
if [ -n "${lib.makeBinPath pluginPackages}" ]; then if [ -n "${runtimePath}" ]; then
export PATH="${lib.makeBinPath pluginPackages}:$PATH" export PATH="${runtimePath}:$PATH"
fi fi
${lib.concatStringsSep "\n" ( ${lib.concatStringsSep "\n" (
@ -146,7 +174,7 @@ let
export ${entry.key}="${entry.value}" export ${entry.key}="${entry.value}"
fi fi
'' ''
) pluginEnvAll ) runtimeEnvAll
)} )}
exec "${gatewayPackage}/bin/openclaw" "$@" exec "${gatewayPackage}/bin/openclaw" "$@"
@ -181,6 +209,8 @@ let
}; };
configFile = configFile; configFile = configFile;
configPath = inst.configPath; configPath = inst.configPath;
codexRuntimeProfiles = codexRuntimeProfiles;
runtimeProfile = runtimeProfile;
dirs = [ dirs = [
inst.stateDir inst.stateDir
@ -245,6 +275,21 @@ let
}; };
instanceConfigs = lib.mapAttrsToList mkInstanceConfig enabledInstances; 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); appInstalls = lib.filter (item: item != null) (map (item: item.appInstall) instanceConfigs);
launchdLabels = lib.filter (label: label != null) (map (item: item.launchdLabel) instanceConfigs); launchdLabels = lib.filter (label: label != null) (map (item: item.launchdLabel) instanceConfigs);
launchdLabelArgs = lib.concatStringsSep " " (map lib.escapeShellArg launchdLabels); 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" ] '' home.activation.openclawPluginGuard = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
set -euo pipefail set -euo pipefail
${plugins.pluginGuards} ${plugins.pluginGuards}
''; '';
home.activation.openclawQmdPrewarm = home.activation.openclawQmdPrewarm = lib.mkIf (cfg.qmd.prewarmModels.enable && qmdPackage != null) (
lib.mkIf (cfg.qmd.prewarmModels.enable && qmdPackage != null) lib.hm.dag.entryAfter [ "openclawDirs" ] ''
( run --quiet ${lib.getExe' pkgs.coreutils "env"} \
lib.hm.dag.entryAfter [ "openclawDirs" ] '' HOME=${lib.escapeShellArg homeDir} \
run --quiet ${lib.getExe' pkgs.coreutils "env"} \ XDG_CACHE_HOME=${lib.escapeShellArg "${homeDir}/.cache"} \
HOME=${lib.escapeShellArg homeDir} \ XDG_CONFIG_HOME=${lib.escapeShellArg "${homeDir}/.config"} \
XDG_CACHE_HOME=${lib.escapeShellArg "${homeDir}/.cache"} \ XDG_DATA_HOME=${lib.escapeShellArg "${homeDir}/.local/share"} \
XDG_CONFIG_HOME=${lib.escapeShellArg "${homeDir}/.config"} \ OPENCLAW_QMD_BIN=${lib.escapeShellArg "${qmdPackage}/bin/qmd"} \
XDG_DATA_HOME=${lib.escapeShellArg "${homeDir}/.local/share"} \ ${pkgs.bash}/bin/bash ${../../../scripts/openclaw-qmd-prewarm.sh}
OPENCLAW_QMD_BIN=${lib.escapeShellArg "${qmdPackage}/bin/qmd"} \ ''
${pkgs.bash}/bin/bash ${../../../scripts/openclaw-qmd-prewarm.sh} );
''
);
home.activation.openclawAppDefaults = home.activation.openclawAppDefaults =
lib.mkIf (pkgs.stdenv.hostPlatform.isDarwin && appDefaults != { }) 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)."; 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 { plugins = lib.mkOption {
type = lib.types.listOf ( type = lib.types.listOf (
lib.types.submodule { 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 { documents = lib.mkOption {
type = lib.types.nullOr lib.types.path; type = lib.types.nullOr lib.types.path;
default = null; default = null;

View File

@ -38,6 +38,8 @@
programs.openclaw = { programs.openclaw = {
enable = true; enable = true;
installApp = false; installApp = false;
runtimePackages = [ pkgs.jq ];
environment.OPENCLAW_TEST_SECRET = "/tmp/openclaw-secret";
instances.default = { instances.default = {
gatewayPort = 18999; gatewayPort = 18999;
logPath = "/tmp/hm-activation-home/.openclaw/openclaw-gateway.log"; 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.succeed("test ! -L /home/alice/.openclaw/workspace/AGENTS.md")
machine.wait_until_succeeds("test -f /home/alice/.openclaw/workspace/skills/skill/SKILL.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.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() uid = machine.succeed("id -u alice").strip()
machine.succeed("loginctl enable-linger alice") 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 "$HOME/.openclaw/openclaw.json"
test -f "$plist" 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 if command -v launchctl >/dev/null 2>&1; then
state_file="$home_dir/launchd-state.txt" state_file="$home_dir/launchd-state.txt"
@ -52,6 +54,7 @@ if command -v launchctl >/dev/null 2>&1; then
fi fi
openclaw_bin=$(/usr/libexec/PlistBuddy -c "Print :ProgramArguments:0" "$plist") openclaw_bin=$(/usr/libexec/PlistBuddy -c "Print :ProgramArguments:0" "$plist")
grep -q OPENCLAW_TEST_SECRET "$openclaw_bin"
health_file="$home_dir/gateway-health.json" health_file="$home_dir/gateway-health.json"
healthy=false healthy=false
for _ in {1..30}; do for _ in {1..30}; do