From 86314bf38dd284b649e3d7569aa3517f2a25435b Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Wed, 3 Jun 2026 15:53:16 +0200 Subject: [PATCH] docs: add containerization guide and Gondolin example (#5356) --- README.md | 10 + package-lock.json | 184 +++++- package.json | 3 +- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/README.md | 2 +- .../coding-agent/docs/containerization.md | 111 ++++ packages/coding-agent/docs/docs.json | 4 + packages/coding-agent/docs/extensions.md | 1 + packages/coding-agent/docs/index.md | 1 + .../examples/extensions/README.md | 1 + .../examples/extensions/gondolin/.gitignore | 1 + .../examples/extensions/gondolin/index.ts | 531 ++++++++++++++++++ .../extensions/gondolin/package-lock.json | 185 ++++++ .../examples/extensions/gondolin/package.json | 19 + packages/coding-agent/package.json | 1 + 15 files changed, 1052 insertions(+), 3 deletions(-) create mode 100644 packages/coding-agent/docs/containerization.md create mode 100644 packages/coding-agent/examples/extensions/gondolin/.gitignore create mode 100644 packages/coding-agent/examples/extensions/gondolin/index.ts create mode 100644 packages/coding-agent/examples/extensions/gondolin/package-lock.json create mode 100644 packages/coding-agent/examples/extensions/gondolin/package.json diff --git a/README.md b/README.md index 92ad3d7bb..563b68582 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,16 @@ I regularly publish my own `pi-mono` work sessions here: For Slack/chat automation and workflows see [earendil-works/pi-chat](https://github.com/earendil-works/pi-chat). +## Permissions & Containerization + +Pi does not include a built-in permission system for restricting filesystem, process, network, or credential access. By default, it runs with the permissions of the user and process that launched it. + +If you need stronger boundaries, containerize or sandbox Pi. See [packages/coding-agent/docs/containerization.md](packages/coding-agent/docs/containerization.md) for three patterns: + +- **OpenShell**: run the whole `pi` process in a policy-controlled sandbox. +- **Gondolin extension**: keep `pi` and provider auth on the host while routing built-in tools and `!` commands into a local Linux micro-VM. +- **Plain Docker**: run the whole `pi` process in a local container for simple isolation. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and [AGENTS.md](AGENTS.md) for project-specific rules (for both humans and agents). diff --git a/package-lock.json b/package-lock.json index 5ddf98990..46b200b07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "packages/coding-agent/examples/extensions/with-deps", "packages/coding-agent/examples/extensions/custom-provider-anthropic", "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo", - "packages/coding-agent/examples/extensions/sandbox" + "packages/coding-agent/examples/extensions/sandbox", + "packages/coding-agent/examples/extensions/gondolin" ], "devDependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26", @@ -722,6 +723,78 @@ "node": ">=14.21.3" } }, + "node_modules/@cto.af/wtf8": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@cto.af/wtf8/-/wtf8-0.0.5.tgz", + "integrity": "sha512-LfUFi+Vv4eDzj+XAtR89e3wwjXA/NZjUSwU5NhwbBrLecxPaBYFy3exCuc1j+D4UZeOVdqlsl8G7LmOt18V0tg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@earendil-works/gondolin": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@earendil-works/gondolin/-/gondolin-0.12.0.tgz", + "integrity": "sha512-BXbvzQKb5QmxY5NtthRDONJTu7+IDKbzqWGrJyyNXMP7N681Tx0Q9TK8pK1ba8nUvYQTipNJyGZOsJfYiZll1A==", + "license": "Apache-2.0", + "dependencies": { + "cbor2": "^2.3.0", + "node-forge": "^1.3.3", + "ssh2": "^1.17.0", + "undici": "^6.21.0" + }, + "bin": { + "gondolin": "dist/bin/gondolin.js" + }, + "engines": { + "node": ">=23.6.0" + }, + "optionalDependencies": { + "@earendil-works/gondolin-krun-runner-darwin-arm64": "0.12.0", + "@earendil-works/gondolin-krun-runner-linux-x64": "0.12.0" + } + }, + "node_modules/@earendil-works/gondolin-krun-runner-darwin-arm64": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@earendil-works/gondolin-krun-runner-darwin-arm64/-/gondolin-krun-runner-darwin-arm64-0.12.0.tgz", + "integrity": "sha512-ftDlusht4PcT7Y3TuPrZIKrCXy3isiBTVMvlXYK0pcud2uXY6uwFTGeunYgP+8ND/60ddb+MImqbfmkcK8B84A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "gondolin-krun-runner": "bin/gondolin-krun-runner" + } + }, + "node_modules/@earendil-works/gondolin-krun-runner-linux-x64": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@earendil-works/gondolin-krun-runner-linux-x64/-/gondolin-krun-runner-linux-x64-0.12.0.tgz", + "integrity": "sha512-RRYsgwe2r5ApKmFNy469QgwnyjAHpAs9XANdWpTd9ol4iUYOY3sX7e0xIooAKxd+ktxGI4N/xRWicwGen3D/Ow==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "gondolin-krun-runner": "bin/gondolin-krun-runner" + } + }, + "node_modules/@earendil-works/gondolin/node_modules/undici": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/@earendil-works/pi-agent-core": { "resolved": "packages/agent", "link": true @@ -2572,6 +2645,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2623,6 +2705,15 @@ ], "license": "MIT" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -2706,6 +2797,15 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2731,6 +2831,18 @@ "node": "^18.12.0 || >= 20.9.0" } }, + "node_modules/cbor2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cbor2/-/cbor2-2.3.0.tgz", + "integrity": "sha512-76WB3hq8BoaGkMkBVJ27fW5LJU+qqDLEpgRNCG/SYKhODWXpVPOTD4UcUto3IEzYLA52nsvbhb0wabhHDn3qXg==", + "license": "MIT", + "dependencies": { + "@cto.af/wtf8": "0.0.5" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -2806,6 +2918,20 @@ "node": ">=18" } }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3943,6 +4069,13 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nan": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", + "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -4034,6 +4167,15 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -4202,6 +4344,10 @@ "resolved": "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo", "link": true }, + "node_modules/pi-extension-gondolin": { + "resolved": "packages/coding-agent/examples/extensions/gondolin", + "link": true + }, "node_modules/pi-extension-sandbox": { "resolved": "packages/coding-agent/examples/extensions/sandbox", "link": true @@ -4544,6 +4690,12 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", @@ -4696,6 +4848,23 @@ "node": ">=0.10.0" } }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -5116,6 +5285,12 @@ "node": "*" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/typebox": { "version": "1.1.38", "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", @@ -6154,6 +6329,13 @@ "name": "pi-extension-custom-provider-gitlab-duo", "version": "0.78.0" }, + "packages/coding-agent/examples/extensions/gondolin": { + "name": "pi-extension-gondolin", + "version": "0.78.0", + "dependencies": { + "@earendil-works/gondolin": "0.12.0" + } + }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", "version": "1.8.0", diff --git a/package.json b/package.json index 072c908af..ed9dbec96 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "packages/coding-agent/examples/extensions/with-deps", "packages/coding-agent/examples/extensions/custom-provider-anthropic", "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo", - "packages/coding-agent/examples/extensions/sandbox" + "packages/coding-agent/examples/extensions/sandbox", + "packages/coding-agent/examples/extensions/gondolin" ], "scripts": { "clean": "npm run clean --workspaces", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7da2178d8..a0c747813 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added containerization documentation and a Gondolin extension example for routing built-in tools into a local micro-VM. - Added Ant Ling provider selection and setup documentation. - Added NVIDIA NIM provider selection, setup documentation, and direct NIM request attribution headers. - Added `ctx.mode` to extension contexts so extensions can distinguish TUI, RPC, JSON, and print mode. diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 60e8da1a4..cec58d505 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -95,7 +95,7 @@ pi Then just talk to pi. By default, pi gives the model four tools: `read`, `write`, `edit`, and `bash`. The model uses these to fulfill your requests. Add capabilities via [skills](#skills), [prompt templates](#prompt-templates), [extensions](#extensions), or [pi packages](#pi-packages). -**Platform notes:** [Windows](docs/windows.md) | [Termux (Android)](docs/termux.md) | [tmux](docs/tmux.md) | [Terminal setup](docs/terminal-setup.md) | [Shell aliases](docs/shell-aliases.md) +**Platform notes:** [Windows](docs/windows.md) | [Termux (Android)](docs/termux.md) | [tmux](docs/tmux.md) | [Terminal setup](docs/terminal-setup.md) | [Shell aliases](docs/shell-aliases.md) | [Containerization](docs/containerization.md) --- diff --git a/packages/coding-agent/docs/containerization.md b/packages/coding-agent/docs/containerization.md new file mode 100644 index 000000000..a3a96bee8 --- /dev/null +++ b/packages/coding-agent/docs/containerization.md @@ -0,0 +1,111 @@ +# Containerization + +Pi runs with all permissions by default, but in some cases, you will want to have more control over what directories Pi can write to and which accesses it has. + +There are two general options. You can either +1. run the whole `pi` process inside an isolated environment, or +2. run `pi` on the host and route tool execution into an isolated environment. + +## Choose a pattern + +| Pattern | What is isolated | Best for | Notes | +| --- | --- | --- | --- | +| OpenShell | Whole `pi` process in a policy-controlled sandbox | Local or remote managed sandbox | Requires an OpenShell gateway | +| Gondolin extension | Built-in tools and `!` commands | Local micro-VM isolation while keeping auth on host | See [`examples/extensions/gondolin/`](../examples/extensions/gondolin/). | +| Plain Docker | Whole `pi` process in a local container | Simple local isolation | Provider API keys enter the container. | + +Extensions run wherever the `pi` process runs. If you run host `pi` with a tool-routing extension, other custom extension tools still run on the host unless they also delegate their operations. + +## OpenShell + +Use [NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview) when you want a policy-controlled sandbox with filesystem, process, network, credential, and inference controls. +OpenShell can run sandboxes through a local gateway backed by Docker, Podman, or a VM runtime, or through a remote Kubernetes gateway. + +Every sandbox requires an active gateway. +Register and select one before creating a sandbox: + +```bash +openshell gateway add --name +openshell gateway select +``` + +Launch `pi` inside an OpenShell sandbox: + +```bash +openshell sandbox create --name pi-sandbox --from pi -- pi +``` + +In this pattern, the whole `pi` process runs inside the sandbox. +Built-in tools, `!` commands, and extension tools execute inside the OpenShell boundary. + +If the gateway is remote, project files are not bind-mounted from the host, meaning writes in the sandbox are not reflected on your machine. +Clone the repository inside the sandbox or use OpenShell file transfer commands: + +```bash +openshell sandbox upload pi-sandbox ./repo /workspace +openshell sandbox download pi-sandbox /workspace/repo ./repo-out +``` + +OpenShell providers can keep raw model API keys outside the sandbox. +When inference routing is configured, code inside the sandbox can call `https://inference.local`, and the gateway injects the configured provider credentials upstream. +Configure Pi to use the corresponding OpenAI-compatible or Anthropic-compatible endpoint if you want model traffic to use this route. + +## Gondolin + +[Gondolin](https://github.com/earendil-works/gondolin) is a local Linux micro-VM. +Use the [example extension](../examples/extensions/gondolin) when you want `pi` on the host but all built-in tools routed into the VM. + +Setup: + +```bash +cp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin +cd ~/.pi/agent/extensions/gondolin +npm install --ignore-scripts +``` + +Run from the project you want mounted: + +```bash +cd /path/to/project +pi -e ~/.pi/agent/extensions/gondolin +``` + +The extension mounts the host cwd at `/workspace` in the VM and overrides `read`, `write`, `edit`, `bash`, `grep`, `find`, and `ls`. +User `!` commands are routed into the VM, as well. +File changes under `/workspace` write through to the host. + +Requirements: Node.js >= 23.6.0 for `@earendil-works/gondolin`, plus QEMU (requires installation through your package manager). + +## Plain Docker + +Run the whole `pi` process in Docker when you want the simplest local container boundary. + +`Dockerfile.pi`: + +```dockerfile +FROM node:24-bookworm-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \ + && rm -rf /var/lib/apt/lists/* +RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent + +WORKDIR /workspace +ENTRYPOINT ["pi"] +``` + +Build and run: + +```bash +docker build -t pi-sandbox -f Dockerfile.pi . + +docker run --rm -it \ + -e ANTHROPIC_API_KEY \ + -v "$PWD:/workspace" \ + -v pi-agent-home:/root/.pi/agent \ + pi-sandbox +``` + +The `-v "$PWD:/workspace"` mounts your current directory into the container at /workspace such that reads and writes in `/workspace` inside Docker directly affect your host files, like in the Gondolin example. + +Use a named volume for `/root/.pi/agent` if you want container-local settings and sessions. Mounting your host `~/.pi/agent` exposes host auth and session files to the container. diff --git a/packages/coding-agent/docs/docs.json b/packages/coding-agent/docs/docs.json index f96321d9f..f781abc26 100644 --- a/packages/coding-agent/docs/docs.json +++ b/packages/coding-agent/docs/docs.json @@ -19,6 +19,10 @@ "title": "Providers", "path": "providers.md" }, + { + "title": "Containerization", + "path": "containerization.md" + }, { "title": "Settings", "path": "settings.md" diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index a8b453a63..08f60162c 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -2606,6 +2606,7 @@ All examples in [examples/extensions/](../examples/extensions/). | `ssh.ts` | SSH remote execution | `registerFlag`, `on("user_bash")`, `on("before_agent_start")`, tool operations | | `interactive-shell.ts` | Persistent shell session | `on("user_bash")` | | `sandbox/` | Sandboxed tool execution | Tool operations | +| `gondolin/` | Route built-in tools and `!` commands into a Gondolin micro-VM | Tool operations, built-in tool overrides, `on("user_bash")` | | `subagent/` | Spawn sub-agents | `registerTool`, `exec` | | **Games** ||| | `snake.ts` | Snake game | `registerCommand`, `ui.custom`, keyboard handling | diff --git a/packages/coding-agent/docs/index.md b/packages/coding-agent/docs/index.md index 75a527d65..2a334e8a6 100644 --- a/packages/coding-agent/docs/index.md +++ b/packages/coding-agent/docs/index.md @@ -41,6 +41,7 @@ For the full first-run flow, see [Quickstart](quickstart.md). - [Quickstart](quickstart.md) - install, authenticate, and run a first session. - [Using Pi](usage.md) - interactive mode, slash commands, context files, and CLI reference. - [Providers](providers.md) - subscription and API-key setup for built-in providers. +- [Containerization](containerization.md) - sandbox pi with OpenShell, Gondolin, or Docker. - [Settings](settings.md) - global and project settings. - [Keybindings](keybindings.md) - default shortcuts and custom keybindings. - [Sessions](sessions.md) - session management, branching, and tree navigation. diff --git a/packages/coding-agent/examples/extensions/README.md b/packages/coding-agent/examples/extensions/README.md index fc2049e69..971033475 100644 --- a/packages/coding-agent/examples/extensions/README.md +++ b/packages/coding-agent/examples/extensions/README.md @@ -23,6 +23,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/ | `confirm-destructive.ts` | Confirms before destructive session actions (clear, switch, fork) | | `dirty-repo-guard.ts` | Prevents session changes with uncommitted git changes | | `sandbox/` | OS-level sandboxing using `@anthropic-ai/sandbox-runtime` with per-project config | +| `gondolin/` | Route built-in tools and `!` commands into a Gondolin micro-VM | ### Custom Tools diff --git a/packages/coding-agent/examples/extensions/gondolin/.gitignore b/packages/coding-agent/examples/extensions/gondolin/.gitignore new file mode 100644 index 000000000..c2658d7d1 --- /dev/null +++ b/packages/coding-agent/examples/extensions/gondolin/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/packages/coding-agent/examples/extensions/gondolin/index.ts b/packages/coding-agent/examples/extensions/gondolin/index.ts new file mode 100644 index 000000000..ab050df7c --- /dev/null +++ b/packages/coding-agent/examples/extensions/gondolin/index.ts @@ -0,0 +1,531 @@ +/** + * Gondolin Tool Routing Example + * + * Runs pi's built-in tools inside a local Gondolin micro-VM. The host working + * directory is mounted at /workspace in the guest. File changes under + * /workspace write through to the host; other guest filesystem changes are + * isolated to the VM. + * + * Setup: + * cd packages/coding-agent/examples/extensions/gondolin + * npm install --ignore-scripts + * + * Usage: + * cd /path/to/project + * pi -e /path/to/pi/packages/coding-agent/examples/extensions/gondolin + * + * Requirements: + * - Node.js >= 23.6.0 for @earendil-works/gondolin + * - QEMU installed (for example, `brew install qemu` on macOS) + */ + +import path from "node:path"; +import { RealFSProvider, VM } from "@earendil-works/gondolin"; +import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { + type BashOperations, + createBashTool, + createEditTool, + createFindTool, + createGrepTool, + createLsTool, + createReadTool, + createWriteTool, + DEFAULT_MAX_BYTES, + type EditOperations, + type FindOperations, + formatSize, + type GrepToolDetails, + type GrepToolInput, + type LsOperations, + type ReadOperations, + truncateHead, + truncateLine, + type WriteOperations, +} from "@earendil-works/pi-coding-agent"; + +const GUEST_WORKSPACE = "/workspace"; +const DEFAULT_GREP_LIMIT = 100; + +type TextToolResult = { + content: Array<{ type: "text"; text: string }>; + details: TDetails | undefined; +}; + +function stripAtPrefix(value: string): string { + return value.startsWith("@") ? value.slice(1) : value; +} + +function toPosix(value: string): string { + return value.split(path.sep).join(path.posix.sep); +} + +function isInsideHostPath(root: string, value: string): boolean { + const relativePath = path.relative(root, value); + return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)); +} + +function hostPathToGuest(localCwd: string, hostPath: string): string { + const relativePath = path.relative(localCwd, hostPath); + if (!isInsideHostPath(localCwd, hostPath)) return toPosix(hostPath); + return relativePath ? path.posix.join(GUEST_WORKSPACE, toPosix(relativePath)) : GUEST_WORKSPACE; +} + +function toGuestPath(localCwd: string, inputPath: string): string { + const trimmed = stripAtPrefix(inputPath.trim()); + if (!trimmed) return GUEST_WORKSPACE; + if (path.isAbsolute(trimmed)) { + if (isInsideHostPath(localCwd, trimmed)) return hostPathToGuest(localCwd, trimmed); + return path.posix.resolve("/", toPosix(trimmed)); + } + return path.posix.resolve(GUEST_WORKSPACE, toPosix(trimmed)); +} + +function createGondolinReadOps(vm: VM, localCwd: string): ReadOperations { + return { + readFile: async (filePath) => vm.fs.readFile(toGuestPath(localCwd, filePath)), + access: async (filePath) => { + await vm.fs.access(toGuestPath(localCwd, filePath)); + }, + detectImageMimeType: async (filePath) => { + const ext = path.posix.extname(toGuestPath(localCwd, filePath)).toLowerCase(); + if (ext === ".png") return "image/png"; + if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg"; + if (ext === ".gif") return "image/gif"; + if (ext === ".webp") return "image/webp"; + return null; + }, + }; +} + +function createGondolinWriteOps(vm: VM, localCwd: string): WriteOperations { + return { + writeFile: async (filePath, content) => { + await vm.fs.writeFile(toGuestPath(localCwd, filePath), content, { encoding: "utf8" }); + }, + mkdir: async (dirPath) => { + await vm.fs.mkdir(toGuestPath(localCwd, dirPath), { recursive: true }); + }, + }; +} + +function createGondolinEditOps(vm: VM, localCwd: string): EditOperations { + const readOps = createGondolinReadOps(vm, localCwd); + const writeOps = createGondolinWriteOps(vm, localCwd); + return { + readFile: readOps.readFile, + writeFile: writeOps.writeFile, + access: readOps.access, + }; +} + +function createGondolinLsOps(vm: VM, localCwd: string): LsOperations { + return { + exists: async (filePath) => { + try { + await vm.fs.access(toGuestPath(localCwd, filePath)); + return true; + } catch { + return false; + } + }, + stat: async (filePath) => vm.fs.stat(toGuestPath(localCwd, filePath)), + readdir: async (dirPath) => vm.fs.listDir(toGuestPath(localCwd, dirPath)), + }; +} + +async function walkGuestFiles( + vm: VM, + root: string, + visit: (guestPath: string, relativePath: string) => Promise, + signal?: AbortSignal, +): Promise { + if (signal?.aborted) throw new Error("Operation aborted"); + const stat = await vm.fs.stat(root, { signal }); + if (!stat.isDirectory()) return visit(root, path.posix.basename(root)); + + const walkDirectory = async (dir: string, relativeDir: string): Promise => { + if (signal?.aborted) throw new Error("Operation aborted"); + const entries = await vm.fs.listDir(dir, { signal }); + for (const entry of entries) { + if (entry === ".git" || entry === "node_modules") continue; + const guestPath = path.posix.join(dir, entry); + const relativePath = relativeDir ? path.posix.join(relativeDir, entry) : entry; + let entryStat: Awaited>; + try { + entryStat = await vm.fs.stat(guestPath, { signal }); + } catch { + continue; + } + if (entryStat.isDirectory()) { + if (!(await walkDirectory(guestPath, relativePath))) return false; + } else if (!(await visit(guestPath, relativePath))) { + return false; + } + } + return true; + }; + + return walkDirectory(root, ""); +} + +function matchesToolGlob(relativePath: string, pattern: string): boolean { + const normalizedPattern = toPosix(pattern); + if (normalizedPattern.includes("/")) { + return ( + path.posix.matchesGlob(relativePath, normalizedPattern) || + path.posix.matchesGlob(relativePath, `**/${normalizedPattern}`) + ); + } + return path.posix.matchesGlob(path.posix.basename(relativePath), normalizedPattern); +} + +function createGondolinFindOps(vm: VM, localCwd: string): FindOperations { + return { + exists: async (filePath) => { + try { + await vm.fs.access(toGuestPath(localCwd, filePath)); + return true; + } catch { + return false; + } + }, + glob: async (pattern, cwd, options) => { + const root = toGuestPath(localCwd, cwd); + const results: string[] = []; + await walkGuestFiles(vm, root, async (guestPath, relativePath) => { + if (results.length >= options.limit) return false; + if (matchesToolGlob(relativePath, pattern)) results.push(guestPath); + return results.length < options.limit; + }); + return results; + }, + }; +} + +function createLineMatcher(pattern: string, literal: boolean | undefined, ignoreCase: boolean | undefined) { + if (literal) { + const needle = ignoreCase ? pattern.toLowerCase() : pattern; + return (line: string) => (ignoreCase ? line.toLowerCase() : line).includes(needle); + } + const regex = new RegExp(pattern, ignoreCase ? "i" : undefined); + return (line: string) => regex.test(line); +} + +function appendGrepBlock(params: { + outputLines: string[]; + lines: string[]; + relativePath: string; + lineIndex: number; + contextLines: number; +}): boolean { + let linesTruncated = false; + const start = params.contextLines > 0 ? Math.max(0, params.lineIndex - params.contextLines) : params.lineIndex; + const end = + params.contextLines > 0 + ? Math.min(params.lines.length - 1, params.lineIndex + params.contextLines) + : params.lineIndex; + + for (let index = start; index <= end; index++) { + const rawLine = params.lines[index] ?? ""; + const { text, wasTruncated } = truncateLine(rawLine.replace(/\r/g, "")); + if (wasTruncated) linesTruncated = true; + const separator = index === params.lineIndex ? ":" : "-"; + params.outputLines.push(`${params.relativePath}${separator}${index + 1}${separator} ${text}`); + } + return linesTruncated; +} + +async function executeGondolinGrep( + vm: VM, + localCwd: string, + params: GrepToolInput, + signal?: AbortSignal, +): Promise> { + const root = toGuestPath(localCwd, params.path ?? "."); + const rootStat = await vm.fs.stat(root, { signal }); + const rootIsDirectory = rootStat.isDirectory(); + const matcher = createLineMatcher(params.pattern, params.literal, params.ignoreCase); + const contextLines = params.context && params.context > 0 ? params.context : 0; + const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT); + const outputLines: string[] = []; + const details: GrepToolDetails = {}; + let matchCount = 0; + let matchLimitReached = false; + let linesTruncated = false; + + await walkGuestFiles( + vm, + root, + async (guestPath, relativePath) => { + if (matchCount >= effectiveLimit) return false; + if (params.glob && !matchesToolGlob(relativePath, params.glob)) return true; + let content: string; + try { + content = await vm.fs.readFile(guestPath, { encoding: "utf8", signal }); + } catch { + return true; + } + const lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); + const displayPath = rootIsDirectory ? relativePath : path.posix.basename(guestPath); + for (let index = 0; index < lines.length; index++) { + if (signal?.aborted) throw new Error("Operation aborted"); + if (!matcher(lines[index] ?? "")) continue; + matchCount++; + if (appendGrepBlock({ outputLines, lines, relativePath: displayPath, lineIndex: index, contextLines })) { + linesTruncated = true; + } + if (matchCount >= effectiveLimit) { + matchLimitReached = true; + return false; + } + } + return true; + }, + signal, + ); + + if (matchCount === 0) return { content: [{ type: "text", text: "No matches found" }], details: undefined }; + + const rawOutput = outputLines.join("\n"); + const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); + const notices: string[] = []; + let output = truncation.content; + + if (matchLimitReached) { + details.matchLimitReached = effectiveLimit; + notices.push(`${effectiveLimit} matches limit reached`); + } + if (linesTruncated) { + details.linesTruncated = true; + notices.push("long lines truncated"); + } + if (truncation.truncated) { + details.truncation = truncation; + notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); + } + if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`; + + return { + content: [{ type: "text", text: output }], + details: Object.keys(details).length > 0 ? details : undefined, + }; +} + +function sanitizeEnv(env: NodeJS.ProcessEnv | undefined): Record | undefined { + if (!env) return undefined; + const result: Record = {}; + for (const [key, value] of Object.entries(env)) { + if (typeof value === "string") result[key] = value; + } + return result; +} + +function createGondolinBashOps(vm: VM, localCwd: string, shellPath: string): BashOperations { + return { + exec: async (command, cwd, { onData, signal, timeout, env }) => { + if (signal?.aborted) throw new Error("aborted"); + const guestCwd = toGuestPath(localCwd, cwd); + const controller = new AbortController(); + const onAbort = () => controller.abort(); + signal?.addEventListener("abort", onAbort, { once: true }); + + let timedOut = false; + const timer = + timeout && timeout > 0 + ? setTimeout(() => { + timedOut = true; + controller.abort(); + }, timeout * 1000) + : undefined; + + try { + const proc = vm.exec([shellPath, "-lc", command], { + cwd: guestCwd, + env: sanitizeEnv(env), + signal: controller.signal, + stdout: "pipe", + stderr: "pipe", + }); + for await (const chunk of proc.output()) onData(chunk.data); + const result = await proc; + return { exitCode: result.exitCode }; + } catch (error) { + if (signal?.aborted) throw new Error("aborted"); + if (timedOut) throw new Error(`timeout:${timeout}`); + throw error; + } finally { + if (timer) clearTimeout(timer); + signal?.removeEventListener("abort", onAbort); + } + }, + }; +} + +export default function (pi: ExtensionAPI) { + const localCwd = process.cwd(); + const localRead = createReadTool(localCwd); + const localWrite = createWriteTool(localCwd); + const localEdit = createEditTool(localCwd); + const localBash = createBashTool(localCwd); + const localGrep = createGrepTool(localCwd); + const localFind = createFindTool(localCwd); + const localLs = createLsTool(localCwd); + + let vm: VM | undefined; + let vmStarting: Promise | undefined; + let shellPath = "/bin/sh"; + + async function startVm(ctx?: ExtensionContext): Promise { + ctx?.ui.setStatus("gondolin", ctx.ui.theme.fg("accent", `Gondolin: starting ${GUEST_WORKSPACE}`)); + const created = await VM.create({ + sessionLabel: `pi ${path.basename(localCwd)}`, + vfs: { + mounts: { + [GUEST_WORKSPACE]: new RealFSProvider(localCwd), + }, + }, + }); + const bashProbe = await created.exec(["/bin/sh", "-lc", "command -v bash || true"]); + shellPath = bashProbe.stdout.trim() || "/bin/sh"; + vm = created; + ctx?.ui.setStatus( + "gondolin", + ctx.ui.theme.fg("accent", `Gondolin: ${created.id.slice(0, 8)} (${GUEST_WORKSPACE})`), + ); + ctx?.ui.notify(`Gondolin VM ready. ${localCwd} is mounted at ${GUEST_WORKSPACE}.`, "info"); + return created; + } + + async function ensureVm(ctx?: ExtensionContext): Promise { + if (vm) return vm; + if (!vmStarting) { + vmStarting = startVm(ctx).finally(() => { + vmStarting = undefined; + }); + } + return vmStarting; + } + + pi.on("session_start", async (_event, ctx) => { + await ensureVm(ctx); + }); + + pi.on("session_shutdown", async (_event, ctx) => { + const activeVm = vm; + vm = undefined; + vmStarting = undefined; + if (!activeVm) return; + ctx.ui.setStatus("gondolin", ctx.ui.theme.fg("muted", "Gondolin: stopping")); + try { + await activeVm.close(); + } finally { + ctx.ui.setStatus("gondolin", undefined); + } + }); + + pi.registerCommand("gondolin", { + description: "Show Gondolin VM status", + handler: async (_args, ctx) => { + const activeVm = await ensureVm(ctx); + ctx.ui.notify( + [ + `Gondolin VM: ${activeVm.id}`, + `Host workspace: ${localCwd}`, + `Guest workspace: ${GUEST_WORKSPACE}`, + `Shell: ${shellPath}`, + ].join("\n"), + "info", + ); + }, + }); + + pi.registerTool({ + ...localRead, + async execute(id, params, signal, onUpdate, ctx) { + const activeVm = await ensureVm(ctx); + const tool = createReadTool(GUEST_WORKSPACE, { + operations: createGondolinReadOps(activeVm, localCwd), + }); + return tool.execute(id, params, signal, onUpdate); + }, + }); + + pi.registerTool({ + ...localWrite, + async execute(id, params, signal, onUpdate, ctx) { + const activeVm = await ensureVm(ctx); + const tool = createWriteTool(GUEST_WORKSPACE, { + operations: createGondolinWriteOps(activeVm, localCwd), + }); + return tool.execute(id, params, signal, onUpdate); + }, + }); + + pi.registerTool({ + ...localEdit, + async execute(id, params, signal, onUpdate, ctx) { + const activeVm = await ensureVm(ctx); + const tool = createEditTool(GUEST_WORKSPACE, { + operations: createGondolinEditOps(activeVm, localCwd), + }); + return tool.execute(id, params, signal, onUpdate); + }, + }); + + pi.registerTool({ + ...localBash, + async execute(id, params, signal, onUpdate, ctx) { + const activeVm = await ensureVm(ctx); + const tool = createBashTool(GUEST_WORKSPACE, { + operations: createGondolinBashOps(activeVm, localCwd, shellPath), + }); + return tool.execute(id, params, signal, onUpdate); + }, + }); + + pi.registerTool({ + ...localLs, + async execute(id, params, signal, onUpdate, ctx) { + const activeVm = await ensureVm(ctx); + const tool = createLsTool(GUEST_WORKSPACE, { + operations: createGondolinLsOps(activeVm, localCwd), + }); + return tool.execute(id, params, signal, onUpdate); + }, + }); + + pi.registerTool({ + ...localFind, + async execute(id, params, signal, onUpdate, ctx) { + const activeVm = await ensureVm(ctx); + const tool = createFindTool(GUEST_WORKSPACE, { + operations: createGondolinFindOps(activeVm, localCwd), + }); + return tool.execute(id, params, signal, onUpdate); + }, + }); + + pi.registerTool({ + ...localGrep, + async execute(_id, params, signal, _onUpdate, ctx) { + const activeVm = await ensureVm(ctx); + return executeGondolinGrep(activeVm, localCwd, params, signal); + }, + }); + + pi.on("user_bash", async (_event, ctx) => { + const activeVm = await ensureVm(ctx); + return { operations: createGondolinBashOps(activeVm, localCwd, shellPath) }; + }); + + pi.on("before_agent_start", async (event, ctx) => { + await ensureVm(ctx); + const localLine = `Current working directory: ${localCwd}`; + const guestLine = `Current working directory: ${GUEST_WORKSPACE} (Gondolin VM; host workspace mounted from ${localCwd})`; + const systemPrompt = event.systemPrompt.includes(localLine) + ? event.systemPrompt.replace(localLine, guestLine) + : `${event.systemPrompt}\n\n${guestLine}`; + return { systemPrompt }; + }); +} diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json new file mode 100644 index 000000000..705ae8a12 --- /dev/null +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -0,0 +1,185 @@ +{ + "name": "pi-extension-gondolin", + "version": "0.78.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pi-extension-gondolin", + "version": "0.78.0", + "dependencies": { + "@earendil-works/gondolin": "0.12.0" + } + }, + "node_modules/@cto.af/wtf8": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@cto.af/wtf8/-/wtf8-0.0.5.tgz", + "integrity": "sha512-LfUFi+Vv4eDzj+XAtR89e3wwjXA/NZjUSwU5NhwbBrLecxPaBYFy3exCuc1j+D4UZeOVdqlsl8G7LmOt18V0tg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@earendil-works/gondolin": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@earendil-works/gondolin/-/gondolin-0.12.0.tgz", + "integrity": "sha512-BXbvzQKb5QmxY5NtthRDONJTu7+IDKbzqWGrJyyNXMP7N681Tx0Q9TK8pK1ba8nUvYQTipNJyGZOsJfYiZll1A==", + "license": "Apache-2.0", + "dependencies": { + "cbor2": "^2.3.0", + "node-forge": "^1.3.3", + "ssh2": "^1.17.0", + "undici": "^6.21.0" + }, + "bin": { + "gondolin": "dist/bin/gondolin.js" + }, + "engines": { + "node": ">=23.6.0" + }, + "optionalDependencies": { + "@earendil-works/gondolin-krun-runner-darwin-arm64": "0.12.0", + "@earendil-works/gondolin-krun-runner-linux-x64": "0.12.0" + } + }, + "node_modules/@earendil-works/gondolin-krun-runner-darwin-arm64": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@earendil-works/gondolin-krun-runner-darwin-arm64/-/gondolin-krun-runner-darwin-arm64-0.12.0.tgz", + "integrity": "sha512-ftDlusht4PcT7Y3TuPrZIKrCXy3isiBTVMvlXYK0pcud2uXY6uwFTGeunYgP+8ND/60ddb+MImqbfmkcK8B84A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "gondolin-krun-runner": "bin/gondolin-krun-runner" + } + }, + "node_modules/@earendil-works/gondolin-krun-runner-linux-x64": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@earendil-works/gondolin-krun-runner-linux-x64/-/gondolin-krun-runner-linux-x64-0.12.0.tgz", + "integrity": "sha512-RRYsgwe2r5ApKmFNy469QgwnyjAHpAs9XANdWpTd9ol4iUYOY3sX7e0xIooAKxd+ktxGI4N/xRWicwGen3D/Ow==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "gondolin-krun-runner": "bin/gondolin-krun-runner" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/cbor2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cbor2/-/cbor2-2.3.0.tgz", + "integrity": "sha512-76WB3hq8BoaGkMkBVJ27fW5LJU+qqDLEpgRNCG/SYKhODWXpVPOTD4UcUto3IEzYLA52nsvbhb0wabhHDn3qXg==", + "license": "MIT", + "dependencies": { + "@cto.af/wtf8": "0.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/nan": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", + "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/undici": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + } + } +} diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json new file mode 100644 index 000000000..58aa8a0b8 --- /dev/null +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -0,0 +1,19 @@ +{ + "name": "pi-extension-gondolin", + "private": true, + "version": "0.78.0", + "type": "module", + "scripts": { + "clean": "echo 'nothing to clean'", + "build": "echo 'nothing to build'", + "check": "echo 'nothing to check'" + }, + "pi": { + "extensions": [ + "./index.ts" + ] + }, + "dependencies": { + "@earendil-works/gondolin": "0.12.0" + } +} diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index b9fdb2891..28550f970 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -25,6 +25,7 @@ "dist", "docs", "examples", + "containerization.md", "CHANGELOG.md", "npm-shrinkwrap.json" ],