Commit c8995a1
Changed files (8)
modules
dev
modules/dev/agents/crush/default.nix
@@ -1,4 +1,8 @@
-{den, ...}: {
+{
+ den,
+ inputs,
+ ...
+}: {
den.aspects.dev.agents.crush = {
includes = [den.aspects.dev.agents];
@@ -19,8 +23,31 @@
lib,
config,
...
- }: {
- packages = [pkgs.nur.repos.charmbracelet.crush];
+ }: let
+ mkJailedAgent =
+ (import ../_jail.nix {
+ inherit pkgs;
+ jail = inputs.jail-nix.lib.init pkgs;
+ }).mkJailedAgent;
+
+ crush = pkgs.nur.repos.charmbracelet.crush.overrideAttrs (old: {
+ meta = (old.meta or {}) // {mainProgram = "crush";};
+ });
+
+ agent = mkJailedAgent {
+ name = "crush";
+ pkg = crush;
+ configPaths = [
+ "${config.xdg.config.directory}/crush"
+ "${config.xdg.data.directory}/crush"
+ ];
+ extraPkgs = [
+ pkgs.deno
+ pkgs.nur.repos.hpcesia.dcg
+ ];
+ };
+ in {
+ packages = [agent.jailed agent.unjailed];
xdg.config.files = {
"crush/crush.json" = {
modules/dev/agents/_jail.nix
@@ -0,0 +1,262 @@
+{
+ pkgs,
+ jail,
+}: let
+ inherit (pkgs) lib;
+
+ commonPkgs = with pkgs; [
+ bashInteractive
+
+ git
+ jujutsu
+
+ curl
+ wget
+
+ findutils
+ gnugrep
+ gawkInteractive
+ jq
+ ripgrep
+ which
+
+ gnused
+ gnutar
+ gzip
+ unzip
+
+ diffutils
+ just
+ ps
+
+ bash-language-server
+ nixd
+ ty
+ ];
+
+ gitExe = lib.getExe pkgs.git;
+ sedExe = lib.getExe pkgs.gnused;
+ direnvExe = lib.getExe pkgs.direnv;
+ jqExe = lib.getExe pkgs.jq;
+
+ # Minimal toolchain to evaluate `.envrc` file
+ devshellPath = lib.makeBinPath (with pkgs; [
+ bashInteractive
+ coreutils
+ direnv
+ findutils
+ git
+ gnugrep
+ gnused
+ jq
+ nix
+ ]);
+
+ commonJailOptions = with jail.combinators; [
+ network
+ time-zone
+ no-new-session
+ ];
+
+ nixDaemonAccess = {
+ readwriteDirs = [
+ "/nix/var/nix/daemon-socket"
+ ];
+ readonlyDirs = [
+ "/nix"
+ "/etc/nix/nix.conf"
+ ];
+ pkgs = [pkgs.nix];
+ };
+
+ openUrls = with jail.combinators;
+ compose [
+ (add-pkg-deps [pkgs.xdg-utils])
+ (set-env "BROWSER" "browserchannel")
+ (jail-to-host-channel "browserchannel" ''
+ case "$1" in
+ https://*|http://*) ''${BROWSER:-xdg-open} "$1" ;;
+ esac
+ '')
+ ];
+
+ # For git worktree or jj workspace
+ workspaceResolver = jail.combinators.add-runtime ''
+ _ixBindSharedVcs() {
+ local git_common gitdir_ref gitdir jj_ref jj_store
+
+ git_common="$(${gitExe} -C "$PWD" rev-parse --git-common-dir 2>/dev/null || true)"
+ git_common="$(realpath "$git_common" 2>/dev/null || true)"
+
+ # A genuine linked worktree has a `.git` file pointing strictly inside
+ # the shared `.git`; a plain dir or crafted pointer binds nothing.
+ if [ -f "$PWD/.git" ] && [ -n "$git_common" ]; then
+ gitdir_ref="$(${sedExe} -n 's/^gitdir: //p' "$PWD/.git" 2>/dev/null | head -n1 || true)"
+ gitdir="$(realpath "$gitdir_ref" 2>/dev/null || true)"
+ case "$gitdir" in
+ "$git_common"/*) RUNTIME_ARGS+=(--bind "$git_common" "$git_common") ;;
+ esac
+ fi
+
+ # jj marks a linked workspace with a `.jj/repo` pointer file, relative to
+ # the `.jj` directory it sits in.
+ if [ -f "$PWD/.jj/repo" ]; then
+ jj_ref="$(cat "$PWD/.jj/repo" 2>/dev/null || true)"
+ case "$jj_ref" in
+ /*) : ;;
+ "") jj_ref="" ;;
+ *) jj_ref="$PWD/.jj/$jj_ref" ;;
+ esac
+ jj_store="$(realpath "$jj_ref" 2>/dev/null || true)"
+ if [ -d "$jj_store/store" ] && [ -d "$jj_store/op_store" ]; then
+ RUNTIME_ARGS+=(--bind "$jj_store" "$jj_store")
+
+ # A colocated repo keeps its git dir outside the store; jj reads it
+ # through the store's git_target pointer and needs it writable too.
+ jj_git=""
+ if [ -f "$jj_store/store/git_target" ]; then
+ jj_git="$(cat "$jj_store/store/git_target" 2>/dev/null || true)"
+ fi
+ if [ -n "$jj_git" ]; then
+ case "$jj_git" in
+ git:*) jj_git="$jj_store/store/git" ;;
+ /*) : ;;
+ *) jj_git="$jj_store/store/$jj_git" ;;
+ esac
+ jj_git="$(realpath "$jj_git" 2>/dev/null || true)"
+ if [ -d "$jj_git" ]; then
+ RUNTIME_ARGS+=(--bind "$jj_git" "$jj_git")
+ fi
+ fi
+ fi
+ fi
+ }
+ _ixBindSharedVcs
+ '';
+
+ # Prints NUL-separated bubblewrap args that load the project's dev shell,
+ # or nothing when direnv has not approved the project's `.envrc`.
+ devshellLoader = pkgs.writeShellApplication {
+ name = "ix-devshell-env";
+ runtimeInputs = [pkgs.coreutils];
+ text = ''
+ jail_path="''${IX_JAIL_PATH:-}"
+
+ # Only `.envrc` is trusted; loading a bare `.env` would leak its secrets.
+ status_json="$(${direnvExe} status --json 2>/dev/null || true)"
+ rc_path="$(printf '%s' "$status_json" | ${jqExe} -r '.state.foundRC.path // empty' 2>/dev/null || true)"
+ case "$rc_path" in
+ */.envrc) ;;
+ *) exit 0 ;;
+ esac
+ [ "$(printf '%s' "$status_json" | ${jqExe} -r '.state.foundRC.allowed // 9' 2>/dev/null || echo 9)" = 0 ] || exit 0
+
+ # `.envrc` can still pull in `.env` through `dotenv`; never forward its keys.
+ skip_keys='[]'
+ if [ -f "$PWD/.env" ]; then
+ skip_keys="$(${direnvExe} dotenv bash "$PWD/.env" 2>/dev/null \
+ | ${jqExe} -R -s 'gsub("export |;"; "\n") | split("\n") | map(split("=")[0]) | map(select(length > 0)) | unique' || true)"
+ [ -n "$skip_keys" ] || skip_keys='[]'
+ fi
+
+ # `env -i` yields a full diff; direnv reports only the delta otherwise.
+ pristine=(
+ "HOME=''${HOME:-}"
+ "USER=''${USER:-$(id -un)}"
+ "TERM=''${TERM:-dumb}"
+ "PATH=${devshellPath}"
+ )
+ for xdg in XDG_DATA_HOME XDG_CONFIG_HOME XDG_CACHE_HOME XDG_STATE_HOME; do
+ [ -n "''${!xdg:-}" ] && pristine+=("$xdg=''${!xdg}")
+ done
+
+ env_json="$(env -i "''${pristine[@]}" ${direnvExe} export json 2>/dev/null)" || exit 0
+ [ -n "$env_json" ] || exit 0
+
+ dev_path="$(printf '%s' "$env_json" | ${jqExe} -r '.PATH // empty' 2>/dev/null || true)"
+ if [ -n "$dev_path" ]; then
+ printf '%s\0%s\0%s\0' --setenv PATH "$dev_path''${jail_path:+:$jail_path}"
+ fi
+
+ # PATH is set above; the rest are paths the jail already controls.
+ printf '%s' "$env_json" | ${jqExe} -j --argjson skip "$skip_keys" '
+ to_entries[]
+ | select(.key | startswith("DIRENV_") | not)
+ | select(.value != null)
+ | select(.key as $k
+ | (["PATH", "HOME", "TMPDIR", "TMP", "TEMP", "TEMPDIR", "NIX_BUILD_TOP"] + $skip)
+ | index($k) | not)
+ | "--setenv\u0000\(.key)\u0000\(.value)\u0000"
+ '
+ '';
+ };
+
+ devshellEnv = state: let
+ jailPath = state.env.PATH or "";
+ in
+ state
+ // {
+ runtime =
+ state.runtime
+ + ''
+ _ix_devshell_jail_path=${lib.escapeShellArg jailPath}
+ while IFS= read -r -d "" _ix_devshell_arg; do
+ RUNTIME_ARGS+=("$_ix_devshell_arg")
+ done < <(IX_JAIL_PATH="$_ix_devshell_jail_path" ${lib.getExe devshellLoader})
+ '';
+ };
+
+ mkJailedAgent = {
+ name,
+ pkg,
+ configPaths,
+ extraReadwriteDirs ? [],
+ extraReadonlyDirs ? [],
+ extraPkgs ? [],
+ env ? {},
+ fwdEnv ? [],
+ enableNix ? true,
+ enableDevshell ? true,
+ enableOpenUrls ? false,
+ enableWorkspaces ? true,
+ basePackages ? commonPkgs,
+ baseJailOptions ? commonJailOptions,
+ }: let
+ readonlyDirs =
+ [
+ "~/.config/agents"
+ "~/.config/git"
+ "~/.config/jj"
+ ]
+ ++ extraReadonlyDirs
+ ++ lib.optionals enableNix nixDaemonAccess.readonlyDirs;
+ readwriteDirs =
+ configPaths
+ ++ extraReadwriteDirs
+ ++ lib.optionals enableNix nixDaemonAccess.readwriteDirs;
+ extraPackages = extraPkgs ++ lib.optionals enableNix nixDaemonAccess.pkgs;
+ in {
+ jailed = jail name pkg (
+ with jail.combinators;
+ baseJailOptions
+ ++ lib.optionals enableOpenUrls [openUrls]
+ ++ lib.optionals enableWorkspaces [workspaceResolver]
+ ++ (map (p: readonly (noescape p)) readonlyDirs)
+ ++ [mount-cwd]
+ ++ (map (p: readwrite (noescape p)) readwriteDirs)
+ ++ [
+ (add-pkg-deps basePackages)
+ (add-pkg-deps extraPackages)
+ ]
+ ++ (map try-fwd-env fwdEnv)
+ ++ (lib.mapAttrsToList set-env env)
+ ++ lib.optionals (enableDevshell && enableNix) [devshellEnv]
+ );
+
+ unjailed = pkgs.writeShellScriptBin "${name}-unjailed" ''
+ exec ${lib.getExe pkg} "$@"
+ '';
+ };
+in {
+ inherit commonPkgs commonJailOptions mkJailedAgent;
+}
modules/dev/agents/default.nix
@@ -1,4 +1,6 @@
{
+ flake-file.inputs.jail-nix.url = "sourcehut:~alexdavid/jail.nix";
+
den.aspects.dev.agents = {
hjem = {lib, ...}: let
skills = builtins.listToAttrs (
modules/dev/agents/goose.nix
@@ -1,4 +1,8 @@
-{den, ...}: {
+{
+ den,
+ inputs,
+ ...
+}: {
den.aspects.dev.agents.goose = {
includes = [den.aspects.dev.agents];
@@ -37,10 +41,23 @@
GOOSE_TELEMETRY_ENABLED = false;
GOOSE_MODE = "smart_approve";
};
+
+ mkJailedAgent =
+ (import ./_jail.nix {
+ inherit pkgs;
+ jail = inputs.jail-nix.lib.init pkgs;
+ }).mkJailedAgent;
+
+ agent = mkJailedAgent {
+ name = "goose";
+ pkg = goose-cli;
+ configPaths = [
+ "${config.xdg.config.directory}/goose"
+ "${config.xdg.state.directory}/goose"
+ ];
+ };
in {
- packages = [
- goose-cli
- ];
+ packages = [agent.jailed agent.unjailed];
xdg.config.files."goose/permission.yaml" = {
type = "copy";
modules/dev/agents/opencode.nix
@@ -1,4 +1,8 @@
-{den, ...}: {
+{
+ den,
+ inputs,
+ ...
+}: {
den.aspects.dev.agents.opencode = {
includes = [den.aspects.dev.agents];
@@ -19,8 +23,25 @@
config,
pkgs,
...
- }: {
- packages = [pkgs.opencode];
+ }: let
+ mkJailedAgent =
+ (import ./_jail.nix {
+ inherit pkgs;
+ jail = inputs.jail-nix.lib.init pkgs;
+ }).mkJailedAgent;
+
+ agent = mkJailedAgent {
+ name = "opencode";
+ pkg = pkgs.opencode;
+ configPaths = [
+ "${config.xdg.config.directory}/opencode"
+ "${config.xdg.data.directory}/opencode"
+ "${config.xdg.state.directory}/opencode"
+ ];
+ env.OPENCODE_DISABLE_LSP_DOWNLOAD = "true";
+ };
+ in {
+ packages = [agent.jailed agent.unjailed];
environment.sessionVariables = {
OPENCODE_DISABLE_LSP_DOWNLOAD = "true";
modules/dev/agents/pi.nix
@@ -1,4 +1,8 @@
-{den, ...}: {
+{
+ den,
+ inputs,
+ ...
+}: {
den.aspects.dev.agents.pi = {
includes = [den.aspects.dev.agents];
@@ -19,20 +23,36 @@
lib,
config,
...
- }: {
- packages = [
- (pkgs.symlinkJoin {
- name = "pi-coding-agent";
- buildInputs = [pkgs.makeWrapper];
- paths = [pkgs.pi-coding-agent];
- postBuild = ''
- wrapProgram $out/bin/pi \
- --set PI_CODING_AGENT_DIR ${config.xdg.config.directory}/pi \
- --suffix PATH : ${lib.makeBinPath [pkgs.nodejs]} \
- --run 'export NPM_CONFIG_PREFIX="''${NPM_CONFIG_PREFIX:-''${XDG_DATA_HOME:-$HOME/.local/share}/pi/npm}"'
- '';
- })
- ];
+ }: let
+ pi-coding-agent = pkgs.symlinkJoin {
+ name = "pi-coding-agent";
+ buildInputs = [pkgs.makeWrapper];
+ paths = [pkgs.pi-coding-agent];
+ postBuild = ''
+ wrapProgram $out/bin/pi \
+ --set PI_CODING_AGENT_DIR ${config.xdg.config.directory}/pi \
+ --suffix PATH : ${lib.makeBinPath [pkgs.nodejs]} \
+ --run 'export NPM_CONFIG_PREFIX="''${NPM_CONFIG_PREFIX:-''${XDG_DATA_HOME:-$HOME/.local/share}/pi/npm}"'
+ '';
+ meta.mainProgram = "pi";
+ };
+
+ mkJailedAgent =
+ (import ./_jail.nix {
+ inherit pkgs;
+ jail = inputs.jail-nix.lib.init pkgs;
+ }).mkJailedAgent;
+
+ agent = mkJailedAgent {
+ name = "pi";
+ pkg = pi-coding-agent;
+ configPaths = [
+ "${config.xdg.config.directory}/pi"
+ "${config.xdg.data.directory}/pi"
+ ];
+ };
+ in {
+ packages = [agent.jailed agent.unjailed];
xdg.config.files."pi/settings.json" = {
type = "copy";
flake.lock
@@ -243,6 +243,21 @@
"type": "github"
}
},
+ "jail-nix": {
+ "locked": {
+ "lastModified": 1788928033,
+ "narHash": "sha256-UQRytK4SsshV1CeX6aKF+P855Sbcqg9aA731eYTakNg=",
+ "owner": "~alexdavid",
+ "repo": "jail.nix",
+ "rev": "7cd53acef3ad51b9c775be759f5643f8d3bcf6eb",
+ "type": "sourcehut"
+ },
+ "original": {
+ "owner": "~alexdavid",
+ "repo": "jail.nix",
+ "type": "sourcehut"
+ }
+ },
"ncro": {
"inputs": {
"nixpkgs": [
@@ -396,6 +411,7 @@
"helix-plugins-nix": "helix-plugins-nix",
"hjem": "hjem",
"import-tree": "import-tree",
+ "jail-nix": "jail-nix",
"ncro": "ncro",
"nixpak": "nixpak",
"nixpkgs": "nixpkgs",
flake.nix
@@ -37,6 +37,7 @@
inputs.nixpkgs.follows = "nixpkgs";
};
import-tree.url = "github:denful/import-tree";
+ jail-nix.url = "sourcehut:~alexdavid/jail.nix";
ncro = {
url = "github:manic-systems/ncro";
inputs.nixpkgs.follows = "nixpkgs";