From 037e817b2967bf347fd2b6de64bfd2bfbee1871d Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Fri, 2 Oct 2026 19:09:42 +0700 Subject: [PATCH] fix(devtools): package Codex daemon and use upstream Pi --- flake.nix | 2 +- modules/home/devtools/ai-tools.nix | 14 +++--- packages/codex-daemon-test.py | 40 +++++++++++++++ packages/codex-package-test.py | 25 ++++++++++ packages/codex.md | 39 +++++++++++++++ packages/codex.nix | 38 ++++++++++++++ packages/default.nix | 2 +- packages/pi-codemode-test.ts | 79 ------------------------------ packages/pi.nix | 33 ------------- 9 files changed, 151 insertions(+), 121 deletions(-) create mode 100644 packages/codex-daemon-test.py create mode 100644 packages/codex-package-test.py create mode 100644 packages/codex.md create mode 100644 packages/codex.nix delete mode 100644 packages/pi-codemode-test.ts delete mode 100644 packages/pi.nix diff --git a/flake.nix b/flake.nix index eab8a05..9ed0cc1 100644 --- a/flake.nix +++ b/flake.nix @@ -211,7 +211,7 @@ formatter = forEachSystem (system: (mkPkgs system).nixfmt); checks = forEachSystem (system: { - inherit (self.packages.${system}) cf pi; + inherit (self.packages.${system}) cf codex; }); packages = forEachSystem ( diff --git a/modules/home/devtools/ai-tools.nix b/modules/home/devtools/ai-tools.nix index 3465588..4cdc33d 100644 --- a/modules/home/devtools/ai-tools.nix +++ b/modules/home/devtools/ai-tools.nix @@ -274,10 +274,10 @@ in config = mkIf cfg.enable { home.packages = [ llmPkgs.claude-code # latest - customPkgs.pi # latest, with the local codemode worker fix + llmPkgs.pi # latest llmPkgs.ccstatusline # latest llmPkgs.ccusage # latest - llmPkgs.codex + customPkgs.codex # complete local package for daemon startup llmPkgs.rtk llmPkgs.cursor-agent llmPkgs.herdr # latest - terminal agent multiplexer @@ -290,7 +290,7 @@ in ++ cfg.extraPackages; home.activation.installPiFff = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - if [ -x ${lib.getExe customPkgs.pi} ]; then + if [ -x ${lib.getExe llmPkgs.pi} ]; then export PI_SKIP_VERSION_CHECK=1 export PI_TELEMETRY=0 export PATH=${pkgs.nodejs}/bin:${pkgs.git}/bin:$PATH @@ -299,10 +299,10 @@ in mkdir -p "$(dirname "$settings_file")" ${pkgs.nodejs}/bin/node -e "const fs=require('fs'); const path=process.env.HOME+'/.pi/agent/settings.json'; let settings={}; if (fs.existsSync(path)) settings=JSON.parse(fs.readFileSync(path,'utf8')); settings.theme='dark'; settings.defaultProvider='openai-codex'; settings.defaultModel='gpt-5.6-sol'; fs.writeFileSync(path, JSON.stringify(settings, null, 2)+String.fromCharCode(10));" - ${lib.getExe customPkgs.pi} list | ${pkgs.gnugrep}/bin/grep -q '@ff-labs/pi-fff' || \ - ${lib.getExe customPkgs.pi} install npm:@ff-labs/pi-fff@0.9.4 - ${lib.getExe customPkgs.pi} list | ${pkgs.gnugrep}/bin/grep -q '@juicesharp/rpiv-ask-user-question' || \ - ${lib.getExe customPkgs.pi} install npm:@juicesharp/rpiv-ask-user-question@1.20.0 + ${lib.getExe llmPkgs.pi} list | ${pkgs.gnugrep}/bin/grep -q '@ff-labs/pi-fff' || \ + ${lib.getExe llmPkgs.pi} install npm:@ff-labs/pi-fff@0.9.4 + ${lib.getExe llmPkgs.pi} list | ${pkgs.gnugrep}/bin/grep -q '@juicesharp/rpiv-ask-user-question' || \ + ${lib.getExe llmPkgs.pi} install npm:@juicesharp/rpiv-ask-user-question@1.20.0 fi ''; diff --git a/packages/codex-daemon-test.py b/packages/codex-daemon-test.py new file mode 100644 index 0000000..dbcc930 --- /dev/null +++ b/packages/codex-daemon-test.py @@ -0,0 +1,40 @@ +"""Run manually outside the Nix sandbox: python3 packages/codex-daemon-test.py /path/to/codex.""" +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile + +binary = str(Path(sys.argv[1]).resolve()) +with tempfile.TemporaryDirectory(prefix="codex-daemon-test-") as directory: + home = Path(directory) + settings = home / "app-server-daemon/settings.json" + settings.parent.mkdir() + settings.write_text(json.dumps({"updater": {"autoUpdateEnabled": False}})) + env = {**os.environ, "CODEX_HOME": directory} + + def run(*arguments, check=True): + return subprocess.run( + [binary, "app-server", "daemon", *arguments], + env=env, check=check, timeout=60, text=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + ) + + try: + started = run("start", check=False) + print(started.stdout) + started.check_returncode() + release = home / "packages/app-server-daemon/current" + assert (release / "codex-package.json").is_file() + assert (release / "bin/codex").is_file() + version = run("version") + print(version.stdout) + status = json.loads(version.stdout) + assert status["status"] == "running", status + assert status["appServerVersion"] == status["cliVersion"], status + print("Codex daemon startup passed") + finally: + stopped = run("stop", check=False) + print(stopped.stdout) + stopped.check_returncode() diff --git a/packages/codex-package-test.py b/packages/codex-package-test.py new file mode 100644 index 0000000..e704723 --- /dev/null +++ b/packages/codex-package-test.py @@ -0,0 +1,25 @@ +"""Check the layout required by Codex daemon provisioning.""" +import json +import os +from pathlib import Path +import sys + +root = Path(sys.argv[1]).resolve() +version, target = sys.argv[2:] +manifest = json.loads((root / "codex-package.json").read_text()) +assert manifest == { + "layoutVersion": 1, + "version": version, + "target": target, + "entrypoint": "bin/codex", +} +required = ["bin/codex", "bin/codex-code-mode-host", "codex-path/rg"] +if "linux" in target: + required.append("codex-resources/bwrap") +for name in required: + path = root / name + assert path.is_file() and os.access(path, os.X_OK), name +for path in root.rglob("*"): + assert path.resolve().is_relative_to(root), f"Escaping link: {path}" + assert not (path.is_symlink() and path.is_dir()), f"Directory link: {path}" +print("Codex package layout checks passed") diff --git a/packages/codex.md b/packages/codex.md new file mode 100644 index 0000000..accc9bb --- /dev/null +++ b/packages/codex.md @@ -0,0 +1,39 @@ +# Local Codex package adapter + +`codex.nix` adds the complete package layout required by Codex daemon startup. +The pinned Numtide package supplies binaries but lacks the manifest and bundled +ripgrep. The adapter copies the binaries without rebuilding Rust. + +Remove this adapter and select `llmPkgs.codex` when Numtide supplies the complete +layout. Also remove the custom package export and adjust the flake check. + +## Validation + +Build the package and run its layout checks: + +```sh +nix build .#codex +``` + +Test daemon provisioning outside the Nix sandbox: + +```sh +python3 packages/codex-daemon-test.py ./result/bin/codex +``` + +The daemon test uses a temporary `CODEX_HOME`, disables automatic updates there, +and stops the test daemon. It does not use your normal Codex sessions or settings. + +## Runtime limits + +Codex copies the package into `~/.codex/packages/app-server-daemon`. +The copied Nix binaries still reference dependencies in `/nix/store`. +Keep the corresponding Nix system generation while using that daemon version. +After a Nix upgrade, `codex app-server daemon update --help` describes how to +select the current CLI package with `--from-cli`. + +The adapter does not change your daemon update settings. Codex can automatically +update its private daemon independently of the CLI installed by Nix. + +Only aarch64-darwin daemon startup has been tested locally. The derivation also +provides the required bubblewrap executable on Linux. diff --git a/packages/codex.nix b/packages/codex.nix new file mode 100644 index 0000000..8d38cf4 --- /dev/null +++ b/packages/codex.nix @@ -0,0 +1,38 @@ +{ pkgs, llm-agents }: +let + upstream = llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.codex; + manifest = pkgs.writeText "codex-package.json" ( + builtins.toJSON { + layoutVersion = 1; + version = upstream.version; + target = pkgs.stdenv.hostPlatform.rust.rustcTarget; + entrypoint = "bin/codex"; + } + ); +in +# Temporary adapter for Numtide's source build. Remove when upstream supplies +# the complete daemon package layout. Reuse binaries without rebuilding Rust. +pkgs.runCommand "codex-${upstream.version}" + { + inherit (upstream) version meta; + nativeBuildInputs = [ pkgs.python3 ]; + passthru = { inherit upstream; }; + } + '' + mkdir -p "$out/bin" "$out/codex-path" + source=${upstream}/bin + if [ -d ${upstream}/libexec/codex/bin ]; then + source=${upstream}/libexec/codex/bin + fi + # Copy real executables: daemon provisioning rejects escaping symlinks. + cp -L "$source/codex" "$source/codex-code-mode-host" "$source/logs_client" "$out/bin/" + cp -L ${pkgs.ripgrep}/bin/rg "$out/codex-path/rg" + cp ${manifest} "$out/codex-package.json" + ${pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux '' + mkdir -p "$out/codex-resources" + cp -L ${pkgs.bubblewrap}/bin/bwrap "$out/codex-resources/bwrap" + ''} + cp -RL ${upstream}/share "$out/share" + python ${./codex-package-test.py} "$out" '${upstream.version}' '${pkgs.stdenv.hostPlatform.rust.rustcTarget}' + "$out/bin/codex" --version + '' diff --git a/packages/default.nix b/packages/default.nix index 2265059..78a16ed 100644 --- a/packages/default.nix +++ b/packages/default.nix @@ -1,6 +1,6 @@ { pkgs, llm-agents, ... }: { cf = pkgs.callPackage ./cf.nix { }; - pi = pkgs.callPackage ./pi.nix { inherit llm-agents; }; + codex = pkgs.callPackage ./codex.nix { inherit llm-agents; }; pikpak-cli = pkgs.callPackage ./pikpak-cli.nix { }; } diff --git a/packages/pi-codemode-test.ts b/packages/pi-codemode-test.ts deleted file mode 100644 index a217d7c..0000000 --- a/packages/pi-codemode-test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import assert from "node:assert/strict"; -import { Type } from "@earendil-works/pi-ai"; -import { - createCodemodeExtension, - type ExtensionAPI, - type ExtensionToolContext, - type ToolDefinition, -} from "@earendil-works/pi-coding-agent"; - -// Run the embedded sandbox without a model request or provider credentials. -export default function (pi: ExtensionAPI) { - let codemode: ToolDefinition | undefined; - createCodemodeExtension({ models: false })({ - ...pi, - registerTool: (tool) => { - codemode = tool; - }, - }); - - pi.on("session_start", async (_event, ctx) => { - try { - assert.ok(codemode); - let nestedCalls = 0; - const toolContext: ExtensionToolContext = { - ...ctx, - tools: [ - { - name: "echo", - description: "Return the input value.", - parameters: Type.Object({ value: Type.Number() }), - outputSchema: Type.Object({ value: Type.Number() }), - }, - ], - executeTool: async (name, args) => { - assert.equal(name, "echo"); - nestedCalls += 1; - return { - toolCall: { type: "toolCall", id: `test/${nestedCalls}`, name, arguments: args }, - isError: false, - result: { - content: [{ type: "text", text: JSON.stringify(args) }], - structuredContent: args, - details: undefined, - }, - }; - }, - }; - const run = (code: string) => - codemode!.execute("codemode-test", { code }, new AbortController().signal, undefined, toolContext); - const output = (result: Awaited>) => - result.content.filter((item) => item.type === "text").map((item) => item.text).join("\n"); - - const arithmetic = await run("return 6 * 7;"); - assert.match(output(arithmetic), /Script completed/); - assert.match(output(arithmetic), /42/); - - const nested = await run(` - const values = await Promise.all([ - tools.echo({ value: 20 }), - tools.echo({ value: 22 }), - ]); - return values.reduce((sum, item) => sum + item.value, 0); - `); - assert.match(output(nested), /Script completed/); - assert.match(output(nested), /42/); - assert.equal(nestedCalls, 2); - - const failure = await run('throw new Error("expected-test-error");'); - assert.match(output(failure), /Script failed/); - assert.match(output(failure), /expected-test-error/); - console.error("codemode regression test passed"); - } catch (error) { - console.error(error); - process.exitCode = 1; - } finally { - ctx.shutdown(); - } - }); -} diff --git a/packages/pi.nix b/packages/pi.nix deleted file mode 100644 index 105ff5d..0000000 --- a/packages/pi.nix +++ /dev/null @@ -1,33 +0,0 @@ -{ pkgs, llm-agents }: -let - upstream = llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.pi; - compileCommand = "bun build --compile ./dist/bun/cli.js ./src/utils/image-resize-worker.ts --outfile dist/pi"; -in -upstream.overrideAttrs (old: { - # The standalone binary resolves this exact source path at runtime. - # The npm source only ships dist/, so create and embed a worker entry. - # Fail evaluation if the upstream build changes instead of dropping the fix. - preInstall = - assert pkgs.lib.hasInfix compileCommand old.preInstall; - pkgs.lib.replaceStrings - [ compileCommand ] - [ - '' - mkdir -p src/extensions/codemode - echo 'import "../../../dist/extensions/codemode/worker.js";' > src/extensions/codemode/worker.ts - bun build --compile ./dist/bun/cli.js ./src/utils/image-resize-worker.ts ./src/extensions/codemode/worker.ts --outfile dist/pi - '' - ] - old.preInstall; - - postInstallCheck = (old.postInstallCheck or "") + '' - test_home=$(mktemp -d) - HOME="$test_home" PI_CODING_AGENT_DIR="$test_home/agent" \ - "$out/bin/pi" --offline --no-session --no-extensions --no-skills \ - --no-prompt-templates --no-themes --no-context-files --no-approve \ - --extension ${./pi-codemode-test.ts} --mode rpc < /dev/null > codemode-test.log 2>&1 - tail -n 60 codemode-test.log - # RPC shutdown can return zero after an extension failure. - grep -Fx 'codemode regression test passed' codemode-test.log - ''; -})