Commit 3e72f337e4f9

Vincent Demeester <vincent@sbr.pm>
2026-09-29 16:32:44
feat: added nono Pi sandbox
Added opt-in Pi sandbox launchers with isolated Pi state, model-only credentials, and read-only GitHub research support for Fedora workstations. Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent 5e2c380
dots/nono/profiles/pi-nono-github-ro.json
@@ -0,0 +1,58 @@
+{
+  "extends": "pi-nono-local",
+  "meta": {
+    "name": "pi-nono-github-ro",
+    "description": "Pi with read-only GitHub research through a brokered token"
+  },
+  "command_policies": {
+    "credentials": {
+      "github-api": {
+        "type": "proxy",
+        "upstream": "https://api.github.com",
+        "credential_key": "env://NONO_GH_TOKEN",
+        "env_var": "GH_TOKEN",
+        "inject_header": "Authorization",
+        "credential_format": "Bearer {}"
+      }
+    },
+    "commands": {
+      "gh": {
+        "from": {
+          "session": {
+            "sandbox": {
+              "fs_read": ["$WORKDIR"],
+              "credentials": [
+                {
+                  "name": "github-api",
+                  "endpoint_policy": {
+                    "default": "deny",
+                    "allow": [
+                      { "method": "GET", "path": "/user" },
+                      { "method": "GET", "path": "/repos/**" },
+                      { "method": "GET", "path": "/user/repos" }
+                    ]
+                  }
+                }
+              ]
+            },
+            "invocation_policy": {
+              "default": "deny",
+              "allow": [
+                { "argv": { "prefix": ["repo", "view"] } },
+                { "argv": { "prefix": ["repo", "list"] } },
+                { "argv": { "prefix": ["issue", "list"] } },
+                { "argv": { "prefix": ["issue", "view"] } },
+                { "argv": { "prefix": ["pr", "list"] } },
+                { "argv": { "prefix": ["pr", "view"] } },
+                { "argv": { "prefix": ["pr", "checks"] } },
+                { "argv": { "prefix": ["run", "list"] } },
+                { "argv": { "prefix": ["run", "view"] } },
+                { "argv": { "prefix": ["api"] } }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}
dots/nono/profiles/pi-nono-local.json
@@ -0,0 +1,25 @@
+{
+  "meta": {
+    "name": "pi-nono-local",
+    "description": "Pi with a writable current worktree and only model-provider network access"
+  },
+  "groups": {
+    "include": ["deny_credentials"]
+  },
+  "workdir": {
+    "access": "readwrite"
+  },
+  "filesystem": {
+    "read": ["$XDG_DATA_HOME/nono/pi-agent"],
+    "write": ["$XDG_DATA_HOME/nono/pi-agent"]
+  },
+  "network": {
+    "allow_domain": [
+      "aiplatform.googleapis.com",
+      "generativelanguage.googleapis.com",
+      "api.githubcopilot.com",
+      "api.individual.githubcopilot.com",
+      "github.com"
+    ]
+  }
+}
dots/Makefile
@@ -53,8 +53,9 @@ claude-compat : ~/.claude
 # We provide everything else via the ~/.pi pattern rule.
 # Sessions are special: redirected to ai-sync for syncthing sharing.
 
-all += pi-agent pi-agent-settings pi-agent-auth
+all += pi-agent pi-agent-settings pi-agent-auth nono-profiles
 pi-agent : ~/.pi/agent/extensions ~/.pi/agent/agents ~/.pi/agent/README.md ~/.pi/agent/keybindings.json ~/.pi/agent/modes.json ~/.pi/agent/models.json ~/.pi/agent/sessions
+nono-profiles : ~/.config/nono/profiles/pi-nono-local.json ~/.config/nono/profiles/pi-nono-github-ro.json
 pi-agent-settings : pi-agent
 	@$(dotfiles)/pi/agent/ensure-settings.sh
 pi-agent-auth : pi-agent
home/common/dev/ai.nix
@@ -1,6 +1,7 @@
 {
   pkgs,
   config,
+  inputs,
   ...
 }:
 let
@@ -14,6 +15,73 @@ let
   # 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;
+  # v0.78.0's upstream prebuilt x86_64-linux hash does not match the release
+  # artifact; use the same locked upstream source instead of weakening hashing.
+  nono = inputs.nono.packages.${pkgs.stdenv.hostPlatform.system}.default;
+  piNono = pkgs.writeShellApplication {
+    name = "pi-nono";
+    runtimeInputs = [
+      pkgs.coreutils
+      pkgs.gnugrep
+      pkgs.jq
+      pkgs.pass-run
+    ];
+    text = ''
+      set -euo pipefail
+
+      mode="''${1:-}"
+      case "$mode" in
+        local|github-ro) shift ;;
+        *)
+          echo "usage: pi-nono {local|github-ro} [pi arguments...]" >&2
+          exit 64
+          ;;
+      esac
+
+      if ! grep -qw landlock /sys/kernel/security/lsm 2>/dev/null; then
+        echo "pi-nono requires Landlock enforcement" >&2
+        exit 1
+      fi
+
+      source_dir="$HOME/.pi/agent"
+      sandbox_dir="''${XDG_DATA_HOME:-$HOME/.local/share}/nono/pi-agent"
+      test -d "$source_dir" || { echo "Pi configuration is missing: $source_dir" >&2; exit 1; }
+      mkdir -p "$sandbox_dir"
+      chmod 700 "$sandbox_dir"
+
+      for file in settings.json modes.json models.json keybindings.json; do
+        if [ -e "$source_dir/$file" ]; then
+          cp -a "$source_dir/$file" "$sandbox_dir/$file"
+        fi
+      done
+      for directory in agents extensions; do
+        if [ -d "$source_dir/$directory" ]; then
+          rm -rf "''${sandbox_dir:?}/$directory"
+          cp -a "$source_dir/$directory" "$sandbox_dir/$directory"
+        fi
+      done
+
+      jq '{
+        "google-vertex": .["google-vertex"],
+        "vertex-anthropic": .["vertex-anthropic"],
+        "github-copilot": .["github-copilot"]
+      } | with_entries(select(.value != null))' "$source_dir/auth.json" > "$sandbox_dir/auth.json"
+      chmod 600 "$sandbox_dir/auth.json"
+
+      export PI_CODING_AGENT_DIR="$sandbox_dir"
+      secret_args=(
+        -e GOOGLE_CLOUD_PROJECT=${osp}/project
+        -e GOOGLE_CLOUD_LOCATION=${osp}/location
+        -e GEMINI_API_KEY=${osp}/vdeemest-api-key
+      )
+      if [ "$mode" = github-ro ]; then
+        secret_args+=(-e NONO_GH_TOKEN=ai/github/nono-readonly-token)
+      fi
+
+      exec ${pkgs.pass-run}/bin/pass-run -q "''${secret_args[@]}" -- \
+        ${nono}/bin/nono run --profile "pi-nono-$mode" -- ${pkgs.llm-agents.pi}/bin/pi "$@"
+    '';
+  };
 
   agents = {
     pi = config.custom.agents.wrap {
@@ -145,6 +213,8 @@ in
     geminiCli
     llm-agents.opencode
     llm-agents.pi
+    nono
+    piNono
     # Wrappers shadowing pi/claude/gemini/opencode (see `agents` above)
     agents.pi
     agents.claude
tests/nono-pi.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
+cd "$repo_root"
+
+fail() {
+  printf 'FAIL: %s\n' "$*" >&2
+  exit 1
+}
+
+assert_file_contains() {
+  local file=$1 expected=$2
+  grep -Fq -- "$expected" "$file" || fail "$file does not contain: $expected"
+}
+
+assert_file_contains flake.nix 'nono = {'
+assert_file_contains flake.nix 'url = "github:nolabs-ai/nono/v0.78.0";'
+assert_file_contains flake.nix 'inputs.nixpkgs.follows = "nixpkgs";'
+# shellcheck disable=SC2016 # Literal Nix interpolation syntax is expected.
+assert_file_contains home/common/dev/ai.nix 'inputs.nono.packages.${pkgs.stdenv.hostPlatform.system}.default'
+assert_file_contains home/common/dev/ai.nix 'pi-nono'
+assert_file_contains dots/Makefile 'nono-profiles'
+
+for profile in dots/nono/profiles/pi-nono-local.json dots/nono/profiles/pi-nono-github-ro.json; do
+  test -f "$profile" || fail "missing profile $profile"
+  jq empty "$profile" || fail "invalid JSON: $profile"
+done
+
+assert_file_contains dots/nono/profiles/pi-nono-local.json 'aiplatform.googleapis.com'
+assert_file_contains dots/nono/profiles/pi-nono-local.json 'generativelanguage.googleapis.com'
+assert_file_contains dots/nono/profiles/pi-nono-local.json 'api.githubcopilot.com'
+assert_file_contains dots/nono/profiles/pi-nono-github-ro.json 'github-api'
+
+printf 'nono Pi configuration checks passed\n'
flake.lock
@@ -786,6 +786,22 @@
         "type": "github"
       }
     },
+    "nixpkgs-darwin-legacy": {
+      "locked": {
+        "lastModified": 1788334797,
+        "narHash": "sha256-nskSa8kDimM7F0VFSP8IdoSNo4G0vKzOtxwvOFpoGDI=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "c19db427a1fdfc7591c0b0baeb4665dcef2c61da",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-26.05-darwin",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
     "nixpkgs-lib": {
       "locked": {
         "lastModified": 1788057806,
@@ -864,6 +880,28 @@
         "type": "github"
       }
     },
+    "nono": {
+      "inputs": {
+        "nixpkgs": [
+          "nixpkgs"
+        ],
+        "nixpkgs-darwin-legacy": "nixpkgs-darwin-legacy"
+      },
+      "locked": {
+        "lastModified": 1789572240,
+        "narHash": "sha256-40ekvTMiIrFW8Fv2P7M/5vGpx3ekKq1kbPXPtXfbRWc=",
+        "owner": "nolabs-ai",
+        "repo": "nono",
+        "rev": "432523457d2e2c174714abaf1b8cf028a2ee2412",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nolabs-ai",
+        "ref": "v0.78.0",
+        "repo": "nono",
+        "type": "github"
+      }
+    },
     "pass-run": {
       "inputs": {
         "nixpkgs": [
@@ -1000,6 +1038,7 @@
         "nixpkgs-26_05": "nixpkgs-26_05",
         "nixpkgs-master": "nixpkgs-master",
         "nixpkgs-wip-consolidated": "nixpkgs-wip-consolidated",
+        "nono": "nono",
         "pass-run": "pass-run",
         "praetorian": "praetorian",
         "pre-commit-hooks": "pre-commit-hooks",
flake.nix
@@ -409,6 +409,10 @@
       ref = "nixos-26.05";
     };
     nixpkgs-master.url = "github:nixos/nixpkgs/master";
+    nono = {
+      url = "github:nolabs-ai/nono/v0.78.0";
+      inputs.nixpkgs.follows = "nixpkgs";
+    };
     nixpkgs-wip-consolidated = {
       type = "github";
       owner = "vdemeester";