Commit 3e72f337e4f9
Changed files (7)
dots
nono
profiles
home
common
dev
tests
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";