Commit 12e8348d5553
Changed files (11)
home/common/dev/ai.nix
@@ -8,6 +8,58 @@ let
# Unified AI agent storage (XDG-compliant)
# Physical storage in ai-sync (syncthing folder), symlinked to ai/
aiSyncDir = "${config.xdg.dataHome}/ai-sync";
+
+ # Secret-provisioning wrappers that shadow the agent binaries themselves, so
+ # anything launching an agent by name (herdr session restart, shpool, Emacs)
+ # gets the credentials. The unwrapped binary stays available as <name>-orig.
+ osp = "redhat/google/osp";
+ geminiCli = config.custom.nss.wrap pkgs.llm-agents.gemini-cli;
+
+ agents = {
+ pi = config.custom.agents.wrap {
+ exe = "pi";
+ pkg = pkgs.llm-agents.pi;
+ aliases = [ "pir" ];
+ secrets = {
+ GOOGLE_CLOUD_PROJECT = "${osp}/project";
+ GOOGLE_CLOUD_LOCATION = "${osp}/location";
+ GEMINI_API_KEY = "${osp}/vdeemest-api-key";
+ SYNTHETIC_API_KEY = "ai/synthetic.new/api_key";
+ BRAVE_API_KEY = "ai/brave/api_key";
+ };
+ };
+
+ claude = config.custom.agents.wrap {
+ exe = "claude";
+ pkg = pkgs.llm-agents.claude-code;
+ aliases = [ "cr" ];
+ env.CLAUDE_CODE_USE_VERTEX = "1";
+ secrets = {
+ CLOUD_ML_REGION = "${osp}/location";
+ ANTHROPIC_VERTEX_PROJECT_ID = "${osp}/project";
+ };
+ };
+
+ gemini = config.custom.agents.wrap {
+ exe = "gemini";
+ pkg = geminiCli;
+ aliases = [
+ "geminir"
+ "ge"
+ ];
+ secrets.GEMINI_API_KEY = "${osp}/vdeemest-api-key";
+ };
+
+ opencode = config.custom.agents.wrap {
+ exe = "opencode";
+ pkg = pkgs.llm-agents.opencode;
+ aliases = [ "oco" ];
+ secrets = {
+ GOOGLE_CLOUD_PROJECT = "${osp}/core-project";
+ VERTEX_LOCATION = "${osp}/core-location";
+ };
+ };
+ };
in
{
# Ensure claude-sync directory structure exists (legacy, still used by claude)
@@ -90,9 +142,14 @@ in
# AI coding agents (from numtide/llm-agents.nix)
llm-agents.claude-code
llm-agents.claude-agent-acp
- (config.custom.nss.wrap llm-agents.gemini-cli)
+ geminiCli
llm-agents.opencode
llm-agents.pi
+ # Wrappers shadowing pi/claude/gemini/opencode (see `agents` above)
+ agents.pi
+ agents.claude
+ agents.gemini
+ agents.opencode
llm-agents.skills
llm-agents.cursor-agent
llm-agents.copilot-cli
home/modules/agent-wrapper.nix
@@ -0,0 +1,94 @@
+# Expose mkAgentWrapper as config.custom.agents.wrap.
+#
+# Historically the secret-provisioning wrappers lived next to the real binary
+# under a different name (pir, cr, geminir, ...). Anything that discovers an
+# agent by its canonical name — herdr restarting a session, shpool, the Emacs
+# daemon, an agent spawning subagents — bypassed them and ran the bare binary
+# with no credentials.
+#
+# This builds a package whose bin/<exe> IS the wrapper, and exposes the
+# untouched upstream binary as bin/<exe>-orig. The wrapper execs the store path
+# directly, so there is no PATH-shadowing recursion and -orig is a real bypass.
+#
+# The result is hiPrio so it wins the buildEnv collision against the upstream
+# package, which stays in home.packages to provide man pages, libexec, etc.
+{
+ pkgs,
+ lib,
+ ...
+}:
+{
+ options.custom.agents.wrap = lib.mkOption {
+ type = lib.types.raw;
+ readOnly = true;
+ description = "Shadow an agent binary with a secret-provisioning wrapper.";
+ default =
+ {
+ # Binary to shadow, e.g. "pi". Must exist as ${pkg}/bin/${exe}.
+ exe,
+ # Upstream package providing it.
+ pkg,
+ # ENV_VAR -> passage secret path. Empty means no pass-run indirection.
+ secrets ? { },
+ # ENV_VAR -> literal value, exported before the secrets are resolved.
+ env ? { },
+ # Extra names in bin/ pointing at the wrapper (muscle memory: pir, cr).
+ aliases ? [ ],
+ }:
+ let
+ real = "${pkg}/bin/${exe}";
+
+ exports = lib.concatMapStringsSep "\n" (
+ n: "export ${n}=${lib.escapeShellArg (toString env.${n})}"
+ ) (lib.attrNames env);
+
+ secretArgs = lib.concatMapStringsSep " \\\n " (n: "-e ${n}=${secrets.${n}}") (
+ lib.attrNames secrets
+ );
+
+ # Nothing to provision: skip pass-run rather than paying a passage
+ # decrypt for nothing.
+ launch =
+ if secrets == { } then
+ ''exec ${real} "$@"''
+ else
+ ''
+ # passage needs its store and identities even when launched from an
+ # environment inheriting no session variables (Emacs daemon, shpool).
+ export PASSAGE_DIR="''${PASSAGE_DIR:-$HOME/.local/share/passage}"
+ export PASSAGE_IDENTITIES_FILE="''${PASSAGE_IDENTITIES_FILE:-$PASSAGE_DIR/identities}"
+
+ exec ${pkgs.pass-run}/bin/pass-run -q \
+ ${secretArgs} \
+ -- ${real} "$@"
+ '';
+
+ script = pkgs.writeShellScript "${exe}-wrapper" ''
+ # Agents re-invoke themselves for subagents; the environment is already
+ # populated by then, so re-running pass-run would only cost a decrypt
+ # per spawn (and can prompt where no tty is attached).
+ if [ -n "''${VDE_AGENT_WRAPPED:-}" ]; then
+ exec ${real} "$@"
+ fi
+ export VDE_AGENT_WRAPPED=1
+
+ ${exports}
+ ${launch}
+ '';
+ in
+ lib.hiPrio (
+ pkgs.runCommand "${exe}-wrapped"
+ {
+ meta = (pkg.meta or { }) // {
+ mainProgram = exe;
+ };
+ }
+ ''
+ mkdir -p $out/bin
+ ln -s ${script} $out/bin/${exe}
+ ln -s ${real} $out/bin/${exe}-orig
+ ${lib.concatMapStringsSep "\n" (a: "ln -s ${script} $out/bin/${a}") aliases}
+ ''
+ );
+ };
+}
home/default.nix
@@ -12,6 +12,7 @@
{
imports = [
./modules/nss-wrapper.nix
+ ./modules/agent-wrapper.nix
./common/shell
]
++ lib.optional (builtins.isString desktop) ./common/desktop
pkgs/my/scripts/bin/agyr
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-# Antigravity (agy) with secrets provisioned via pass-run
-# Usage: agyr [args...]
-
-exec pass-run -q \
- -e GEMINI_API_KEY=redhat/google/osp/vdeemest-api-key \
- -- agy "$@"
pkgs/my/scripts/bin/cr
@@ -1,11 +0,0 @@
-#!/usr/bin/env bash
-# Claude Code with Vertex AI environment
-# Usage: cr [args...]
-# This is a script version of the 'cr' alias for use with shpool and other tools
-
-export CLAUDE_CODE_USE_VERTEX=1
-
-exec pass-run -q \
- -e CLOUD_ML_REGION=redhat/google/osp/location \
- -e ANTHROPIC_VERTEX_PROJECT_ID=redhat/google/osp/project \
- -- claude "$@"
pkgs/my/scripts/bin/crf
@@ -1,10 +1,6 @@
#!/usr/bin/env bash
-# Claude Code with Vertex AI environment (Opus model - full power)
+# Claude Code with Vertex AI environment (Opus model - heavyweight)
# Usage: crf [args...]
+# Vertex config and secrets are provided by the `claude` wrapper itself.
-export CLAUDE_CODE_USE_VERTEX=1
-
-exec pass-run -q \
- -e CLOUD_ML_REGION=redhat/google/osp/location \
- -e ANTHROPIC_VERTEX_PROJECT_ID=redhat/google/osp/project \
- -- claude --model claude-opus-4-5 "$@"
+exec claude --model claude-opus-4-5 "$@"
pkgs/my/scripts/bin/crl
@@ -1,10 +1,6 @@
#!/usr/bin/env bash
# Claude Code with Vertex AI environment (Haiku model - lightweight)
# Usage: crl [args...]
+# Vertex config and secrets are provided by the `claude` wrapper itself.
-export CLAUDE_CODE_USE_VERTEX=1
-
-exec pass-run -q \
- -e CLOUD_ML_REGION=redhat/google/osp/location \
- -e ANTHROPIC_VERTEX_PROJECT_ID=redhat/google/osp/project \
- -- claude --model claude-haiku-4-5 "$@"
+exec claude --model claude-haiku-4-5 "$@"
pkgs/my/scripts/bin/ge
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-# Gemini with API key from passage
-# Usage: ge [args...]
-
-exec pass-run -q \
- -e GEMINI_API_KEY=redhat/google/osp/vdeemest-api-key \
- -- gemini "$@"
pkgs/my/scripts/bin/geminir
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-# Gemini CLI with secrets provisioned via pass-run
-# Usage: geminir [args...]
-
-exec pass-run -q \
- -e GEMINI_API_KEY=redhat/google/osp/vdeemest-api-key \
- -- gemini "$@"
pkgs/my/scripts/bin/oco
@@ -1,8 +0,0 @@
-#!/usr/bin/env bash
-# OpenCode with Vertex AI environment
-# Usage: oco [args...]
-
-exec pass-run -q \
- -e GOOGLE_CLOUD_PROJECT=redhat/google/osp/core-project \
- -e VERTEX_LOCATION=redhat/google/osp/core-location \
- -- opencode "$@"
pkgs/my/scripts/bin/pir
@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-# Pi coding agent with secrets provisioned via pass-run
-# Usage: pir [args...]
-# Model/provider controlled by ~/.pi/agent/modes.json
-
-# Ensure passage finds its store and identities even when launched from
-# environments that don't inherit session variables (e.g. the Emacs daemon).
-export PASSAGE_DIR="${PASSAGE_DIR:-$HOME/.local/share/passage}"
-export PASSAGE_IDENTITIES_FILE="${PASSAGE_IDENTITIES_FILE:-$PASSAGE_DIR/identities}"
-
-exec pass-run -q \
- -e GOOGLE_CLOUD_PROJECT=redhat/google/osp/project \
- -e GOOGLE_CLOUD_LOCATION=redhat/google/osp/location \
- -e GEMINI_API_KEY=redhat/google/osp/vdeemest-api-key \
- -e SYNTHETIC_API_KEY=ai/synthetic.new/api_key \
- -e BRAVE_API_KEY=ai/brave/api_key \
- -- pi "$@"