From ad04b76171219b02865c5378b6602c1dee9f1b5b Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Sat, 4 Jul 2026 09:27:42 +0700 Subject: [PATCH 1/2] feat(pi-agent): add native agent browser --- flake.lock | 121 +++++++++++++++++++++++++++++++-------- flake.nix | 1 + nix/modules/pi-agent.nix | 30 +++++++++- 3 files changed, 126 insertions(+), 26 deletions(-) diff --git a/flake.lock b/flake.lock index 4e027815..693acd94 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,25 @@ { "nodes": { + "agent-browser": { + "inputs": { + "flake-parts": "flake-parts", + "import-tree": "import-tree", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1783131873, + "narHash": "sha256-3iAbDkyb9bxVgllCTGh7hwhGB9A8qjawNloL1RhSZzE=", + "owner": "rrvsh", + "repo": "agent-browser-nix", + "rev": "e90f41a9065d4bb219db128427f21653d33b2724", + "type": "github" + }, + "original": { + "owner": "rrvsh", + "repo": "agent-browser-nix", + "type": "github" + } + }, "aquamarine": { "inputs": { "hyprutils": [ @@ -52,8 +72,8 @@ }, "cl-nix-lite": { "inputs": { - "flake-parts": "flake-parts_2", - "nixpkgs": "nixpkgs", + "flake-parts": "flake-parts_3", + "nixpkgs": "nixpkgs_2", "systems": "systems_2", "treefmt-nix": "treefmt-nix" }, @@ -105,6 +125,27 @@ } }, "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "agent-browser", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1782949081, + "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { "inputs": { "nixpkgs-lib": [ "nixpkgs" @@ -124,7 +165,7 @@ "type": "github" } }, - "flake-parts_2": { + "flake-parts_3": { "inputs": { "nixpkgs-lib": "nixpkgs-lib" }, @@ -142,7 +183,7 @@ "type": "github" } }, - "flake-parts_3": { + "flake-parts_4": { "inputs": { "nixpkgs-lib": [ "pi", @@ -163,7 +204,7 @@ "type": "github" } }, - "flake-parts_4": { + "flake-parts_5": { "inputs": { "nixpkgs-lib": [ "pi-claude-bridge", @@ -644,12 +685,27 @@ "type": "github" } }, + "import-tree_4": { + "locked": { + "lastModified": 1778781969, + "narHash": "sha256-Jjuz5CmSkur8KvLDoGa+vylEp+RkQtv4mt/qcMznpH0=", + "owner": "vic", + "repo": "import-tree", + "rev": "d321337efd0f23a9eb14a42adb7b2c29313ab274", + "type": "github" + }, + "original": { + "owner": "vic", + "repo": "import-tree", + "type": "github" + } + }, "mac-app-util": { "inputs": { "cl-nix-lite": "cl-nix-lite", "flake-compat": "flake-compat_2", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_3", + "nixpkgs": "nixpkgs_4", "systems": "systems_3", "treefmt-nix": "treefmt-nix_2" }, @@ -748,16 +804,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1766736597, - "narHash": "sha256-BASnpCLodmgiVn0M1MU2Pqyoz0aHwar/0qLkp7CjvSQ=", + "lastModified": 1782959384, + "narHash": "sha256-xnJJk+ct+D2+wdRxj1wk36w5zV9RVESwRqcklPdt3fM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "f560ccec6b1116b22e6ed15f4c510997d99d5852", + "rev": "65179426c83bb3f6bc14898b42ea1c6f01d374b0", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-25.11", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } @@ -814,6 +870,22 @@ } }, "nixpkgs_2": { + "locked": { + "lastModified": 1766736597, + "narHash": "sha256-BASnpCLodmgiVn0M1MU2Pqyoz0aHwar/0qLkp7CjvSQ=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "f560ccec6b1116b22e6ed15f4c510997d99d5852", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { "locked": { "lastModified": 1761236834, "narHash": "sha256-+pthv6hrL5VLW2UqPdISGuLiUZ6SnAXdd2DdUE+fV2Q=", @@ -829,7 +901,7 @@ "type": "github" } }, - "nixpkgs_3": { + "nixpkgs_4": { "locked": { "lastModified": 1732617236, "narHash": "sha256-PYkz6U0bSEaEB1al7O1XsqVNeSNS+s3NVclJw7YC43w=", @@ -845,7 +917,7 @@ "type": "github" } }, - "nixpkgs_4": { + "nixpkgs_5": { "locked": { "lastModified": 1761236834, "narHash": "sha256-+pthv6hrL5VLW2UqPdISGuLiUZ6SnAXdd2DdUE+fV2Q=", @@ -861,7 +933,7 @@ "type": "github" } }, - "nixpkgs_5": { + "nixpkgs_6": { "locked": { "lastModified": 1781074563, "narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=", @@ -877,7 +949,7 @@ "type": "github" } }, - "nixpkgs_6": { + "nixpkgs_7": { "locked": { "lastModified": 1781577229, "narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=", @@ -895,9 +967,9 @@ }, "pi": { "inputs": { - "flake-parts": "flake-parts_3", - "import-tree": "import-tree_2", - "nixpkgs": "nixpkgs_6" + "flake-parts": "flake-parts_4", + "import-tree": "import-tree_3", + "nixpkgs": "nixpkgs_7" }, "locked": { "lastModified": 1782350491, @@ -915,8 +987,8 @@ }, "pi-claude-bridge": { "inputs": { - "flake-parts": "flake-parts_4", - "import-tree": "import-tree_3", + "flake-parts": "flake-parts_5", + "import-tree": "import-tree_4", "nixpkgs": [ "nixpkgs" ] @@ -960,18 +1032,19 @@ }, "root": { "inputs": { - "flake-parts": "flake-parts", + "agent-browser": "agent-browser", + "flake-parts": "flake-parts_2", "home-manager": "home-manager", "homebrew-cask": "homebrew-cask", "homebrew-core": "homebrew-core", "hyprland": "hyprland", - "import-tree": "import-tree", + "import-tree": "import-tree_2", "mac-app-util": "mac-app-util", "nix-darwin": "nix-darwin", "nix-homebrew": "nix-homebrew", "nix-index-database": "nix-index-database", "nix-rosetta-builder": "nix-rosetta-builder", - "nixpkgs": "nixpkgs_5", + "nixpkgs": "nixpkgs_6", "nixpkgs-firefox-darwin": "nixpkgs-firefox-darwin", "nixpkgs-master": "nixpkgs-master", "pi": "pi", @@ -1047,7 +1120,7 @@ }, "treefmt-nix": { "inputs": { - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_3" }, "locked": { "lastModified": 1766000401, @@ -1065,7 +1138,7 @@ }, "treefmt-nix_2": { "inputs": { - "nixpkgs": "nixpkgs_4" + "nixpkgs": "nixpkgs_5" }, "locked": { "lastModified": 1766000401, diff --git a/flake.nix b/flake.nix index 1c1ba803..32a0b849 100644 --- a/flake.nix +++ b/flake.nix @@ -4,6 +4,7 @@ inputs@{ flake-parts, import-tree, ... }: flake-parts.lib.mkFlake { inherit inputs; } ((import-tree ./nix) // { flake.paths.root = ./.; }); inputs = { + agent-browser.url = "github:rrvsh/agent-browser-nix"; flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; flake-parts.url = "github:hercules-ci/flake-parts"; home-manager.inputs.nixpkgs.follows = "nixpkgs"; diff --git a/nix/modules/pi-agent.nix b/nix/modules/pi-agent.nix index 64cce52c..ea25b0a9 100644 --- a/nix/modules/pi-agent.nix +++ b/nix/modules/pi-agent.nix @@ -20,13 +20,25 @@ in nixos.pi-agent = osModule; homeManager.pi-agent = { pkgs, config, ... }: + let + system = pkgs.stdenv.hostPlatform.system; + agentBrowser = inputs.agent-browser.packages.${system}.agent-browser; + agentBrowserStatePath = "${config.home.homeDirectory}/.agent-browser/state/agent.json"; + in { + home.packages = [ agentBrowser ]; programs.pi-coding-agent = { enable = true; - package = inputs.pi.packages.${pkgs.stdenv.hostPlatform.system}.pi-coding-agent; - extraPackages = [ pkgs.nodejs_22 ]; + package = inputs.pi.packages.${system}.pi-coding-agent; + extraPackages = [ + pkgs.nodejs_22 + agentBrowser + ]; settings.lastChangelogVersion = lib.getVersion config.programs.pi-coding-agent.package; settings.packages = [ + # Keep this pinned with the agent-browser flake input: pi-agent-browser-native + # tracks specific agent-browser CLI versions in its command surface and result parsing. + "npm:pi-agent-browser-native@0.2.64" "npm:pi-mcp-adapter" "npm:pi-subagents" "npm:pi-web-access" @@ -48,6 +60,20 @@ in - To inspect flake inputs locally: - `nix flake metadata --json | jq '.locks.nodes | keys'` - `nix eval --impure --raw --expr '(builtins.getFlake (toString ./.)).inputs..outPath'` + - Agent browser: + - `agent-browser` is installed in the user shell and on Pi's wrapped runtime `PATH`. + - On a fresh machine after rebuilding this config, run `agent-browser install` once to download the agent-owned Chrome. + - Use the native `agent_browser` tool for browser automation; do not drive `agent-browser` through bash from Pi. + - Auth-walled and account-specific sites, especially LinkedIn, must use `agent_browser` with the shared auth state before declaring an auth wall or login problem. Do not use `ddgr`, `web_search`, `fetch_content`, `curl`, or bash to read LinkedIn page content. + - Use one shared auth state file for all sites: `${agentBrowserStatePath}`. Do not create per-service restore buckets such as `--restore=linkedin` unless explicitly asked. + - Normal authenticated browsing is headless and state-backed. Launch fresh with the shared state file, e.g. `args`: `["--state", "${agentBrowserStatePath}", "open", "https://www.linkedin.com/feed/"]`, `sessionMode`: `"fresh"`. Use the same pattern for LinkedIn profiles, posts, search, and jobs by replacing the URL. + - After a state-backed open, inspect with `snapshot -i`, `get`, or `eval --stdin`. If the page still shows a login form or password field, ask the user whether to refresh the shared login; do not stop at "auth wall". + - Do not use `--headed` for normal browsing. Use `--headed` only for initial auth setup or refreshing an expired login when the user needs to sign in manually. + - For auth setup/refresh, launch a headed session and keep it alive with a long wait while the user signs in. Example LinkedIn flow: + ```json + {"args":["--headed","batch"],"stdin":"[[\"open\",\"https://www.linkedin.com/feed/\"],[\"wait\",\"--fn\",\"location.href.includes('/feed') && !document.querySelector('input[type=password]')\",\"--timeout\",\"600000\"]]","sessionMode":"fresh","timeoutMs":650000} + ``` + - After the user is logged in, save or refresh the shared state with `args`: `["state", "save", "${agentBrowserStatePath}"]`. ''; }; }; From bc03683441b2025f77803e11a1f23f98721c4ea2 Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Mon, 6 Jul 2026 09:12:38 +0800 Subject: [PATCH 2/2] pi-agent: provide chromium to agent-browser extension for nixos --- nix/modules/pi-agent.nix | 76 +++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/nix/modules/pi-agent.nix b/nix/modules/pi-agent.nix index ea25b0a9..9422df4c 100644 --- a/nix/modules/pi-agent.nix +++ b/nix/modules/pi-agent.nix @@ -23,10 +23,20 @@ in let system = pkgs.stdenv.hostPlatform.system; agentBrowser = inputs.agent-browser.packages.${system}.agent-browser; + # NixOS cannot run agent-browser's downloaded generic Chrome, so point the + # extension at Nix-provided Chromium for fresh browser launches by default. + agentBrowserConfig = { + version = 1; + } + // lib.optionalAttrs pkgs.stdenv.isLinux { + browser.executablePath = "${pkgs.chromium}/bin/chromium"; + }; agentBrowserStatePath = "${config.home.homeDirectory}/.agent-browser/state/agent.json"; in { - home.packages = [ agentBrowser ]; + home.packages = [ agentBrowser ] ++ lib.optionals pkgs.stdenv.isLinux [ pkgs.chromium ]; + home.file.".pi/config/pi-agent-browser-native/config.json".text = + builtins.toJSON agentBrowserConfig; programs.pi-coding-agent = { enable = true; package = inputs.pi.packages.${system}.pi-coding-agent; @@ -43,38 +53,38 @@ in "npm:pi-subagents" "npm:pi-web-access" ]; - context = '' - # Global pi instructions - - - If a command is missing, use comma: `, [args...]` - - For Python, run via comma: `, python3 ...` - - Load and follow the global skill: `comma` (`~/.pi/agent/skills/comma/SKILL.md`) - - For quick web search, prefer `ddgr` first (example: `ddgr -n 5 -x --np `). - - If `ddgr` is missing, use comma: `, ddgr -n 5 -x --np `. - - For scripted/API search, use Python via comma and prefer stdlib first (`urllib`, `json`, `re`, `xml.etree`). - - Avoid assuming third-party Python deps (e.g., `requests`, `bs4`) are installed. - - DuckDuckGo HTML may return bot challenges; if blocked, fall back to Bing RSS or site APIs. - - Useful fallbacks: - - Bing RSS: `https://www.bing.com/search?format=rss&q=...` - - GitHub repo search API: `https://api.github.com/search/repositories?q=...` - - To inspect flake inputs locally: - - `nix flake metadata --json | jq '.locks.nodes | keys'` - - `nix eval --impure --raw --expr '(builtins.getFlake (toString ./.)).inputs..outPath'` - - Agent browser: - - `agent-browser` is installed in the user shell and on Pi's wrapped runtime `PATH`. - - On a fresh machine after rebuilding this config, run `agent-browser install` once to download the agent-owned Chrome. - - Use the native `agent_browser` tool for browser automation; do not drive `agent-browser` through bash from Pi. - - Auth-walled and account-specific sites, especially LinkedIn, must use `agent_browser` with the shared auth state before declaring an auth wall or login problem. Do not use `ddgr`, `web_search`, `fetch_content`, `curl`, or bash to read LinkedIn page content. - - Use one shared auth state file for all sites: `${agentBrowserStatePath}`. Do not create per-service restore buckets such as `--restore=linkedin` unless explicitly asked. - - Normal authenticated browsing is headless and state-backed. Launch fresh with the shared state file, e.g. `args`: `["--state", "${agentBrowserStatePath}", "open", "https://www.linkedin.com/feed/"]`, `sessionMode`: `"fresh"`. Use the same pattern for LinkedIn profiles, posts, search, and jobs by replacing the URL. - - After a state-backed open, inspect with `snapshot -i`, `get`, or `eval --stdin`. If the page still shows a login form or password field, ask the user whether to refresh the shared login; do not stop at "auth wall". - - Do not use `--headed` for normal browsing. Use `--headed` only for initial auth setup or refreshing an expired login when the user needs to sign in manually. - - For auth setup/refresh, launch a headed session and keep it alive with a long wait while the user signs in. Example LinkedIn flow: - ```json - {"args":["--headed","batch"],"stdin":"[[\"open\",\"https://www.linkedin.com/feed/\"],[\"wait\",\"--fn\",\"location.href.includes('/feed') && !document.querySelector('input[type=password]')\",\"--timeout\",\"600000\"]]","sessionMode":"fresh","timeoutMs":650000} - ``` - - After the user is logged in, save or refresh the shared state with `args`: `["state", "save", "${agentBrowserStatePath}"]`. - ''; + context = lib.concatStringsSep "\n" ( + [ + "# Global pi instructions" + "" + "- If a command is missing, use comma: `, [args...]`" + "- For Python, run via comma: `, python3 ...`" + "- Load and follow the global skill: `comma` (`~/.pi/agent/skills/comma/SKILL.md`)" + "- Avoid assuming third-party Python deps (e.g., `requests`, `bs4`) are installed." + "- To inspect flake inputs locally:" + " - `nix flake metadata --json | jq '.locks.nodes | keys'`" + " - `nix eval --impure --raw --expr '(builtins.getFlake (toString ./.)).inputs..outPath'`" + "- Agent browser:" + " - `agent-browser` is installed in the user shell and on Pi's wrapped runtime `PATH`." + " - Use the native `agent_browser` tool for browser automation; do not drive `agent-browser` through bash from Pi." + " - Auth-walled and account-specific sites, especially LinkedIn, must use `agent_browser` with the shared auth state before declaring an auth wall or login problem. Do not use `ddgr`, `web_search`, `fetch_content`, `curl`, or bash to read LinkedIn page content." + " - Use one shared auth state file for all sites: `${agentBrowserStatePath}`. Do not create per-service restore buckets such as `--restore=linkedin` unless explicitly asked." + " - Normal authenticated browsing is headless and state-backed. Launch fresh with the shared state file, e.g. `args`: `[\"--state\", \"${agentBrowserStatePath}\", \"open\", \"https://www.linkedin.com/feed/\"]`, `sessionMode`: `\"fresh\"`. Use the same pattern for LinkedIn profiles, posts, search, and jobs by replacing the URL." + " - After a state-backed open, inspect with `snapshot -i`, `get`, or `eval --stdin`. If the page still shows a login form or password field, ask the user whether to refresh the shared login; do not stop at \"auth wall\"." + " - Do not use `--headed` for normal browsing. Use `--headed` only for initial auth setup or refreshing an expired login when the user needs to sign in manually." + " - For auth setup/refresh, launch a headed session and keep it alive with a long wait while the user signs in. Example LinkedIn flow:" + " ```json" + ''{"args":["--headed","batch"],"stdin":"[[\"open\",\"https://www.linkedin.com/feed/\"],[\"wait\",\"--fn\",\"location.pathname.includes('/feed') && !document.querySelector('input[type=password]')\",\"--timeout\",\"600000\"]]","sessionMode":"fresh","timeoutMs":650000}'' + " ```" + " - After the user is logged in, save or refresh the shared state with `args`: `[\"state\", \"save\", \"${agentBrowserStatePath}\"]`." + ] + ++ lib.optionals pkgs.stdenv.isLinux [ + " - On Linux/NixOS, the agent_browser extension config points fresh launches at Nix-provided Chromium; the generic browser downloaded by `agent-browser install` is not NixOS-compatible." + ] + ++ lib.optionals pkgs.stdenv.isDarwin [ + " - On Darwin, run `agent-browser install` if the agent browser executable is not present." + ] + ); }; }; };