diff --git a/nix/checks/openclaw-default-instance.nix b/nix/checks/openclaw-default-instance.nix index ea8c58e..b9f4d2d 100644 --- a/nix/checks/openclaw-default-instance.nix +++ b/nix/checks/openclaw-default-instance.nix @@ -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 diff --git a/nix/modules/home-manager/openclaw-link-codex-runtime-profiles.sh b/nix/modules/home-manager/openclaw-link-codex-runtime-profiles.sh new file mode 100755 index 0000000..33714f2 --- /dev/null +++ b/nix/modules/home-manager/openclaw-link-codex-runtime-profiles.sh @@ -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" diff --git a/nix/modules/home-manager/openclaw/config.nix b/nix/modules/home-manager/openclaw/config.nix index 8e79395..660a79c 100644 --- a/nix/modules/home-manager/openclaw/config.nix +++ b/nix/modules/home-manager/openclaw/config.nix @@ -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 != { }) diff --git a/nix/modules/home-manager/openclaw/options-instance.nix b/nix/modules/home-manager/openclaw/options-instance.nix index 92a252d..563c445 100644 --- a/nix/modules/home-manager/openclaw/options-instance.nix +++ b/nix/modules/home-manager/openclaw/options-instance.nix @@ -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 { diff --git a/nix/modules/home-manager/openclaw/options.nix b/nix/modules/home-manager/openclaw/options.nix index 2fa0314..27ff36a 100644 --- a/nix/modules/home-manager/openclaw/options.nix +++ b/nix/modules/home-manager/openclaw/options.nix @@ -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; diff --git a/nix/tests/hm-activation-macos/flake.nix b/nix/tests/hm-activation-macos/flake.nix index b2430c1..173a027 100644 --- a/nix/tests/hm-activation-macos/flake.nix +++ b/nix/tests/hm-activation-macos/flake.nix @@ -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"; diff --git a/nix/tests/hm-activation.py b/nix/tests/hm-activation.py index f3e114d..198f070 100644 --- a/nix/tests/hm-activation.py +++ b/nix/tests/hm-activation.py @@ -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") diff --git a/scripts/hm-activation-macos.sh b/scripts/hm-activation-macos.sh index 5d64efe..f952d10 100755 --- a/scripts/hm-activation-macos.sh +++ b/scripts/hm-activation-macos.sh @@ -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