summaryrefslogtreecommitdiffstatshomepage
path: root/nix
diff options
context:
space:
mode:
Diffstat (limited to 'nix')
-rw-r--r--nix/common.nix78
-rw-r--r--nix/flake.nix186
-rw-r--r--nix/releases.json37
-rw-r--r--nix/update-releases.sh142
4 files changed, 436 insertions, 7 deletions
diff --git a/nix/common.nix b/nix/common.nix
index 7c94c28..143b161 100644
--- a/nix/common.nix
+++ b/nix/common.nix
@@ -8,6 +8,45 @@
# Shared Home-Manager package profile. Chezmoi owns dotfile deployment.
+let
+ configuredClaude = pkgs.symlinkJoin {
+ name = "claude-configured-${pkgs.claude-release.version}";
+ paths = [ pkgs.claude-release ];
+ nativeBuildInputs = [ pkgs.makeWrapper ];
+ postBuild = ''
+ wrapProgram "$out/bin/claude" \
+ --add-flags --mcp-config \
+ --add-flags "${config.home.homeDirectory}/.claude/mcp-config.json"
+ '';
+ };
+ configuredCopilot = pkgs.symlinkJoin {
+ name = "copilot-configured-${pkgs.copilot-release.version}";
+ paths = [ pkgs.copilot-release ];
+ nativeBuildInputs = [ pkgs.makeWrapper ];
+ postBuild = ''
+ wrapProgram "$out/bin/copilot" \
+ --set COPILOT_ALLOW_ALL true
+ '';
+ };
+ configuredHermes = pkgs.symlinkJoin {
+ name = "hermes-configured-${pkgs.hermes-release.version}";
+ paths = [ pkgs.hermes-release ];
+ nativeBuildInputs = [ pkgs.makeWrapper ];
+ postBuild = ''
+ wrapProgram "$out/bin/hermes" \
+ --set HERMES_MANAGED_DIR "${config.home.homeDirectory}/.hermes/managed"
+ '';
+ };
+ configuredOmp = pkgs.symlinkJoin {
+ name = "omp-configured-${pkgs.omp-release.version}";
+ paths = [ pkgs.omp-release ];
+ nativeBuildInputs = [ pkgs.makeWrapper ];
+ postBuild = ''
+ wrapProgram "$out/bin/omp" \
+ --set PI_CONFIG_FILES "${config.home.homeDirectory}/.omp/agent/policy.yml"
+ '';
+ };
+in
{
home.stateVersion = "25.05";
@@ -139,10 +178,14 @@
uv # for project tooling that asks for `uv`/`uvx`; brings no python
python3Packages.ipython # interactive REPL; pulls its own python, only `ipython` lands on PATH
- # AI coding agents
- claude-code
- codex # OpenAI Codex CLI
- github-copilot-cli # `copilot`; prebuilt-binary derivation since 1.0.43
+ # AI tools
+ configuredClaude # Anthropic latest release pinned in nix/releases.json
+ codex-release # OpenAI stable release pinned in nix/releases.json
+ configuredCopilot # GitHub stable release pinned in nix/releases.json
+ configuredHermes # Hermes Agent stable release pinned in nix/releases.json
+ configuredOmp # Oh My Pi stable release pinned in nix/releases.json
+ opencode-release # OpenCode stable release pinned in nix/releases.json
+ ori-release # OpenRouter Ori stable release pinned in nix/releases.json
tuicr # interactive git-change reviewer; flake input, see nix/flake.nix. Skill: dot_claude/skills/tuicr/
aibox # Bubblewrap sandbox for AI coding agent sessions; flake input, see nix/flake.nix
@@ -189,7 +232,32 @@
zsh-history-substring-search
];
- # ── direnv + nix-direnv ─────────────────────────────────────────────────────
+ # AI agent policy
+ # Hermes keeps MCP and provider settings in one mutable file. Merge only the
+ # shared server so authentication and provider choices stay untracked.
+ home.activation.configureHermesMcp = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
+ config_file="$HOME/.hermes/config.yaml"
+ mkdir -p "$(dirname "$config_file")"
+ if [ ! -e "$config_file" ]; then
+ printf '{}\n' >"$config_file"
+ fi
+
+ if current_url="$(${pkgs.yq-go}/bin/yq -r '.mcp_servers.openaiDeveloperDocs.url // ""' "$config_file" 2>/dev/null)" \
+ && current_enabled="$(${pkgs.yq-go}/bin/yq -r '.mcp_servers.openaiDeveloperDocs.enabled // false' "$config_file" 2>/dev/null)"; then
+ if [ "$current_url" != "https://developers.openai.com/mcp" ] || [ "$current_enabled" != true ]; then
+ ${pkgs.yq-go}/bin/yq -i '
+ .mcp_servers.openaiDeveloperDocs = {
+ "url": "https://developers.openai.com/mcp",
+ "enabled": true
+ }
+ ' "$config_file"
+ fi
+ else
+ echo "warning: cannot update invalid Hermes config: $config_file" >&2
+ fi
+ '';
+
+ # direnv + nix-direnv
programs.direnv = {
enable = true;
nix-direnv.enable = true;
diff --git a/nix/flake.nix b/nix/flake.nix
index 834e213..d871ce7 100644
--- a/nix/flake.nix
+++ b/nix/flake.nix
@@ -35,6 +35,8 @@
...
}:
let
+ releases = builtins.fromJSON (builtins.readFile ./releases.json);
+ hermes = builtins.getFlake "github:NousResearch/hermes-agent/${releases.hermes.rev}";
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
@@ -44,10 +46,190 @@
(final: prev: {
tuicr = tuicr.packages.${system}.default;
aibox = aibox.packages.${system}.default;
+ claude-release = prev.claude-code.overrideAttrs {
+ version = releases.claude.version;
+ src = final.fetchurl {
+ inherit (releases.claude) url hash;
+ };
+ };
+ copilot-release = final.stdenvNoCC.mkDerivation {
+ pname = "github-copilot-cli";
+ version = releases.copilot.version;
+ src = final.fetchurl {
+ inherit (releases.copilot) url hash;
+ };
+ sourceRoot = ".";
+ nativeBuildInputs = [
+ final.autoPatchelfHook
+ final.makeBinaryWrapper
+ ];
+ buildInputs = [
+ final.glibc
+ final.stdenv.cc.cc.lib
+ final.glib
+ final.libsecret
+ ];
+ runtimeDependencies = [
+ final.glibc
+ final.stdenv.cc.cc.lib
+ final.glib
+ final.libsecret
+ ];
+ dontStrip = true;
+ installPhase = ''
+ runHook preInstall
+ install -Dm755 copilot "$out/libexec/copilot"
+ makeWrapper "$out/libexec/copilot" "$out/bin/copilot" \
+ --add-flag --no-auto-update \
+ --set-default SSL_CERT_DIR ${final.cacert}/etc/ssl/certs \
+ --prefix PATH : ${final.lib.makeBinPath [ final.bash ]}
+ runHook postInstall
+ '';
+ meta = {
+ description = "GitHub Copilot CLI";
+ homepage = "https://github.com/github/copilot-cli";
+ license = final.lib.licenses.unfree;
+ mainProgram = "copilot";
+ platforms = [ "x86_64-linux" ];
+ sourceProvenance = with final.lib.sourceTypes; [
+ binaryNativeCode
+ binaryBytecode
+ obfuscatedCode
+ ];
+ };
+ };
+ codex-release = final.stdenvNoCC.mkDerivation {
+ pname = "codex";
+ version = releases.codex.version;
+ src = final.fetchurl {
+ inherit (releases.codex) url hash;
+ };
+ sourceRoot = ".";
+ nativeBuildInputs = [ final.autoPatchelfHook ];
+ buildInputs = [
+ final.glibc
+ final.ncurses
+ ];
+ runtimeDependencies = [
+ final.glibc
+ final.ncurses
+ ];
+ installPhase = ''
+ runHook preInstall
+ mkdir -p "$out"
+ cp -R . "$out/"
+ chmod -R u+w "$out"
+ runHook postInstall
+ '';
+ meta = {
+ description = "OpenAI Codex CLI";
+ homepage = "https://developers.openai.com/codex/cli";
+ license = final.lib.licenses.asl20;
+ mainProgram = "codex";
+ platforms = [ "x86_64-linux" ];
+ sourceProvenance = [ final.lib.sourceTypes.binaryNativeCode ];
+ };
+ };
+ hermes-release = hermes.packages.${system}.minimal;
+ omp-release = final.stdenvNoCC.mkDerivation {
+ pname = "oh-my-pi";
+ version = releases.omp.version;
+ src = final.fetchurl {
+ inherit (releases.omp) url hash;
+ };
+ dontUnpack = true;
+ nativeBuildInputs = [ final.makeBinaryWrapper ];
+ dontStrip = true;
+ installPhase = ''
+ runHook preInstall
+ install -Dm755 "$src" "$out/libexec/omp"
+ makeWrapper ${final.glibc}/lib/ld-linux-x86-64.so.2 "$out/bin/omp" \
+ --add-flags --library-path \
+ --add-flags ${final.glibc}/lib \
+ --add-flags "$out/libexec/omp"
+ runHook postInstall
+ '';
+ meta = {
+ description = "AI coding agent for the terminal";
+ homepage = "https://github.com/can1357/oh-my-pi";
+ license = final.lib.licenses.mit;
+ mainProgram = "omp";
+ platforms = [ "x86_64-linux" ];
+ sourceProvenance = with final.lib.sourceTypes; [
+ binaryNativeCode
+ binaryBytecode
+ ];
+ };
+ };
+ opencode-release = final.stdenvNoCC.mkDerivation {
+ pname = "opencode";
+ version = releases.opencode.version;
+ src = final.fetchurl {
+ inherit (releases.opencode) url hash;
+ };
+ sourceRoot = ".";
+ nativeBuildInputs = [
+ final.autoPatchelfHook
+ final.makeBinaryWrapper
+ ];
+ buildInputs = [ final.glibc ];
+ runtimeDependencies = [ final.glibc ];
+ dontStrip = true;
+ installPhase = ''
+ runHook preInstall
+ install -Dm755 opencode "$out/bin/opencode"
+ wrapProgram "$out/bin/opencode" \
+ --prefix PATH : ${final.lib.makeBinPath [ final.ripgrep ]} \
+ --set OPENCODE_DISABLE_AUTOUPDATE true
+ runHook postInstall
+ '';
+ meta = {
+ description = "AI coding agent built for the terminal";
+ homepage = "https://opencode.ai";
+ license = final.lib.licenses.mit;
+ mainProgram = "opencode";
+ platforms = [ "x86_64-linux" ];
+ sourceProvenance = with final.lib.sourceTypes; [
+ binaryNativeCode
+ binaryBytecode
+ ];
+ };
+ };
+ ori-release = final.stdenvNoCC.mkDerivation {
+ pname = "ori";
+ version = releases.ori.version;
+ src = final.fetchurl {
+ inherit (releases.ori) url hash;
+ };
+ dontUnpack = true;
+ nativeBuildInputs = [ final.makeBinaryWrapper ];
+ dontStrip = true;
+ installPhase = ''
+ runHook preInstall
+ install -Dm755 "$src" "$out/libexec/ori"
+ makeWrapper ${final.glibc}/lib/ld-linux-x86-64.so.2 "$out/bin/ori" \
+ --add-flags --library-path \
+ --add-flags ${final.glibc}/lib \
+ --add-flags "$out/libexec/ori" \
+ --set ORI_NO_UPDATE_CHECK 1 \
+ --set ORI_TELEMETRY 0
+ runHook postInstall
+ '';
+ meta = {
+ description = "OpenRouter harness for local coding agents";
+ homepage = "https://openrouter.ai/blog/announcements/ori-harness/";
+ license = final.lib.licenses.asl20;
+ mainProgram = "ori";
+ platforms = [ "x86_64-linux" ];
+ sourceProvenance = with final.lib.sourceTypes; [
+ binaryNativeCode
+ binaryBytecode
+ ];
+ };
+ };
})
];
- # Whitelist specific unfree packages (claude-code,
- # github-copilot-cli) instead of globally setting allowUnfree,
+ # Whitelist specific unfree packages instead of globally setting allowUnfree,
# so a typo elsewhere can't silently pull in additional unfree
# deps.
config.allowUnfreePredicate =
diff --git a/nix/releases.json b/nix/releases.json
new file mode 100644
index 0000000..9e5825a
--- /dev/null
+++ b/nix/releases.json
@@ -0,0 +1,37 @@
+{
+ "claude": {
+ "version": "2.1.267",
+ "url": "https://downloads.claude.ai/claude-code-releases/2.1.267/linux-x64/claude.zst",
+ "hash": "sha256-8h7WbQLr3XWzO4zhnJMJaLKgBqba1cW1nV8rNVYixMA="
+ },
+ "codex": {
+ "version": "0.154.0",
+ "url": "https://github.com/openai/codex/releases/download/rust-v0.154.0/codex-package-x86_64-unknown-linux-musl.tar.gz",
+ "hash": "sha256-/G4+O4Xyz31mRSDuXGan/kqhK659RoNPR+LxZf0Nb3g="
+ },
+ "copilot": {
+ "version": "1.0.83",
+ "url": "https://github.com/github/copilot-cli/releases/download/v1.0.83/copilot-linux-x64.tar.gz",
+ "hash": "sha256-/74cQpZkuKBe/tZ+zbRnEj5A/Ko8bBTvmpi6dNpGh7c="
+ },
+ "hermes": {
+ "version": "0.21.1",
+ "tag": "v2026.9.7",
+ "rev": "2237be355906fbe6065ce1815711eee52b2d646e"
+ },
+ "omp": {
+ "version": "18.1.16",
+ "url": "https://github.com/can1357/oh-my-pi/releases/download/v18.1.16/omp-linux-x64",
+ "hash": "sha256-7+vvt0NZ0gNi4HNB8Bpl/AolnnvHlFTvCLDr9SMquJc="
+ },
+ "opencode": {
+ "version": "1.18.30",
+ "url": "https://github.com/anomalyco/opencode/releases/download/v1.18.30/opencode-linux-x64.tar.gz",
+ "hash": "sha256-VQByRoWBZUlv+FuhwrZI90IejiATv0GJpoDJ/45pnRc="
+ },
+ "ori": {
+ "version": "0.14.1+5bb4241",
+ "url": "https://github.com/OpenRouterLabs/ori-releases/releases/download/cli-0.14.1-5bb4241/ori-linux-x64",
+ "hash": "sha256-VqzA5u97tRqgXpy/tS1oXrby+Gtv/hgi6TRfkGLU66Q="
+ }
+}
diff --git a/nix/update-releases.sh b/nix/update-releases.sh
new file mode 100644
index 0000000..661bec5
--- /dev/null
+++ b/nix/update-releases.sh
@@ -0,0 +1,142 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if ! command -v nix >/dev/null 2>&1; then
+ echo "nix not installed; skipping release update" >&2
+ exit 0
+fi
+
+repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
+lockfile="$repo_root/nix/releases.json"
+tmp=$(mktemp)
+trap 'rm -f "$tmp"' EXIT
+
+fetch_url() {
+ local response
+ if response=$(curl --fail-with-body -sSL "$1"); then
+ printf '%s\n' "$response"
+ else
+ printf 'Failed to fetch %s\n%s\n' "$1" "$response" >&2
+ return 1
+ fi
+}
+
+github_api() {
+ if command -v gh >/dev/null 2>&1 && gh auth token --hostname github.com >/dev/null 2>&1; then
+ gh api --hostname github.com "$1"
+ else
+ fetch_url "https://api.github.com/$1" || {
+ echo 'For a GitHub rate limit, wait for reset or authenticate with: gh auth login --hostname github.com' >&2
+ return 1
+ }
+ fi
+}
+
+github_asset() {
+ local repository=$1
+ local asset=$2
+ local release
+
+ release=$(github_api "repos/$repository/releases/latest") || return
+ jq -er --arg asset "$asset" '
+ . as $release
+ | .assets[]
+ | select(.name == $asset)
+ | [$release.tag_name, .browser_download_url, .digest]
+ | @tsv
+ ' <<<"$release"
+}
+
+sri_hash() {
+ local digest=${1#sha256:}
+ nix hash convert --hash-algo sha256 --to sri "$digest"
+}
+
+claude_base=https://downloads.claude.ai/claude-code-releases
+claude_version=$(fetch_url "$claude_base/latest")
+claude_manifest=$(fetch_url "$claude_base/$claude_version/manifest.zst.json")
+claude_binary=$(jq -er '.platforms["linux-x64"].binary' <<<"$claude_manifest")
+claude_digest=$(jq -er '.platforms["linux-x64"].checksum' <<<"$claude_manifest")
+claude_url="$claude_base/$claude_version/linux-x64/$claude_binary"
+asset_info=$(github_asset openai/codex codex-package-x86_64-unknown-linux-musl.tar.gz)
+read -r codex_tag codex_url codex_digest <<<"$asset_info"
+asset_info=$(github_asset github/copilot-cli copilot-linux-x64.tar.gz)
+read -r copilot_tag copilot_url copilot_digest <<<"$asset_info"
+hermes_release=$(github_api repos/NousResearch/hermes-agent/releases/latest)
+hermes_tag=$(jq -er .tag_name <<<"$hermes_release")
+hermes_version=$(jq -er '.name | capture("^Hermes Agent v(?<version>[0-9]+\\.[0-9]+\\.[0-9]+)").version' <<<"$hermes_release")
+hermes_rev=$(
+ github_api "repos/NousResearch/hermes-agent/commits/$hermes_tag" |
+ jq -er .sha
+)
+asset_info=$(github_asset can1357/oh-my-pi omp-linux-x64)
+read -r omp_tag omp_url omp_digest <<<"$asset_info"
+asset_info=$(github_asset anomalyco/opencode opencode-linux-x64.tar.gz)
+read -r opencode_tag opencode_url opencode_digest <<<"$asset_info"
+asset_info=$(github_asset OpenRouterLabs/ori-releases ori-linux-x64)
+read -r ori_tag ori_url ori_digest <<<"$asset_info"
+
+codex_version=${codex_tag#rust-v}
+copilot_version=${copilot_tag#v}
+omp_version=${omp_tag#v}
+opencode_version=${opencode_tag#v}
+ori_version=${ori_tag#cli-}
+ori_version=${ori_version/-/+}
+if [[ ! $claude_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ||
+ $codex_version == "$codex_tag" ||
+ $copilot_version == "$copilot_tag" ||
+ ! $hermes_tag =~ ^v[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}$ ||
+ ! $hermes_rev =~ ^[0-9a-f]{40}$ ||
+ $omp_version == "$omp_tag" ||
+ $opencode_version == "$opencode_tag" ||
+ $ori_version == "$ori_tag" ]]; then
+ echo "unexpected release tag" >&2
+ exit 1
+fi
+claude_hash=$(sri_hash "$claude_digest")
+codex_hash=$(sri_hash "$codex_digest")
+copilot_hash=$(sri_hash "$copilot_digest")
+omp_hash=$(sri_hash "$omp_digest")
+opencode_hash=$(sri_hash "$opencode_digest")
+ori_hash=$(sri_hash "$ori_digest")
+
+jq -n \
+ --arg claude_version "$claude_version" \
+ --arg claude_url "$claude_url" \
+ --arg claude_hash "$claude_hash" \
+ --arg codex_version "$codex_version" \
+ --arg codex_url "$codex_url" \
+ --arg codex_hash "$codex_hash" \
+ --arg copilot_version "$copilot_version" \
+ --arg copilot_url "$copilot_url" \
+ --arg copilot_hash "$copilot_hash" \
+ --arg hermes_version "$hermes_version" \
+ --arg hermes_tag "$hermes_tag" \
+ --arg hermes_rev "$hermes_rev" \
+ --arg omp_version "$omp_version" \
+ --arg omp_url "$omp_url" \
+ --arg omp_hash "$omp_hash" \
+ --arg opencode_version "$opencode_version" \
+ --arg opencode_url "$opencode_url" \
+ --arg opencode_hash "$opencode_hash" \
+ --arg ori_version "$ori_version" \
+ --arg ori_url "$ori_url" \
+ --arg ori_hash "$ori_hash" \
+ '{
+ claude: {version: $claude_version, url: $claude_url, hash: $claude_hash},
+ codex: {version: $codex_version, url: $codex_url, hash: $codex_hash},
+ copilot: {version: $copilot_version, url: $copilot_url, hash: $copilot_hash},
+ hermes: {version: $hermes_version, tag: $hermes_tag, rev: $hermes_rev},
+ omp: {version: $omp_version, url: $omp_url, hash: $omp_hash},
+ opencode: {version: $opencode_version, url: $opencode_url, hash: $opencode_hash},
+ ori: {version: $ori_version, url: $ori_url, hash: $ori_hash}
+ }' >"$tmp"
+
+if cmp -s "$tmp" "$lockfile"; then
+ echo "release lock is current"
+ exit 0
+fi
+
+chmod 0644 "$tmp"
+mv "$tmp" "$lockfile"
+echo "updated: nix/releases.json"