diff --git a/.deva.example b/.deva.example index d450f6a..1ed37a0 100644 --- a/.deva.example +++ b/.deva.example @@ -61,11 +61,11 @@ EPHEMERAL=false # Hybrid Agent Setup: # # Hybrid is the DEFAULT. deva walks every populated subdir under -# ~/.config/deva/ (claude, codex, gemini, grok, kimi, opencode) and mounts each agent's +# ~/.config/deva/ (claude, codex, gemini, grok, kimi, opencode, pi) and mounts each agent's # canonical entries into the container. Populated = you either # hand-created the subdir or autolink symlinked it from legacy -# ~/.claude, ~/.codex, ~/.gemini, ~/.grok, ~/.kimi-code, or the opencode -# XDG dirs (~/.config/opencode + ~/.local/share/opencode + +# ~/.claude, ~/.codex, ~/.gemini, ~/.grok, ~/.kimi-code, ~/.pi, or the +# opencode XDG dirs (~/.config/opencode + ~/.local/share/opencode + # ~/.local/state/opencode) on first run. # # No .deva entries required for the common case. To opt OUT of diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e6082a..34a51a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,7 @@ jobs: --build-arg GROK_CLI_VERSION="${{ steps.pins.outputs.grok_cli_version }}" \ --build-arg KIMI_CODE_VERSION="${{ steps.pins.outputs.kimi_code_version }}" \ --build-arg OPENCODE_VERSION="${{ steps.pins.outputs.opencode_version }}" \ + --build-arg PI_CODING_AGENT_VERSION="${{ steps.pins.outputs.pi_coding_agent_version }}" \ --build-arg CCX_VERSION="${{ steps.pins.outputs.ccx_version }}" \ --build-arg COPILOT_API_VERSION="${{ steps.pins.outputs.copilot_api_version }}" \ . @@ -106,6 +107,7 @@ jobs: GROK_CLI_VERSION="${{ steps.pins.outputs.grok_cli_version }}" \ KIMI_CODE_VERSION="${{ steps.pins.outputs.kimi_code_version }}" \ OPENCODE_VERSION="${{ steps.pins.outputs.opencode_version }}" \ + PI_CODING_AGENT_VERSION="${{ steps.pins.outputs.pi_coding_agent_version }}" \ CCX_VERSION="${{ steps.pins.outputs.ccx_version }}" \ COPILOT_API_VERSION="${{ steps.pins.outputs.copilot_api_version }}" \ PLAYWRIGHT_VERSION="${{ steps.pins.outputs.playwright_version }}" \ @@ -133,6 +135,7 @@ jobs: deva.sh grok -Q -- --version deva.sh kimi -Q -- --version deva.sh opencode -Q -- --version + deva.sh pi -Q -- --version - name: Smoke Claude --chrome mount assembly shell: bash @@ -194,6 +197,14 @@ jobs: DEVA_DOCKER_TAG=ci \ ./scripts/test-opencode-auth.sh + - name: Smoke pi auth wiring + shell: bash + run: | + set -euo pipefail + DEVA_DOCKER_IMAGE=deva-smoke \ + DEVA_DOCKER_TAG=ci \ + ./scripts/test-pi-auth.sh + - name: Smoke version targets shell: bash run: | diff --git a/.github/workflows/nightly-images.yml b/.github/workflows/nightly-images.yml index b8d4109..a6aa21c 100644 --- a/.github/workflows/nightly-images.yml +++ b/.github/workflows/nightly-images.yml @@ -57,6 +57,7 @@ jobs: grok_cli_version: ${{ steps.versions.outputs.grok_cli_version }} kimi_code_version: ${{ steps.versions.outputs.kimi_code_version }} opencode_version: ${{ steps.versions.outputs.opencode_version }} + pi_coding_agent_version: ${{ steps.versions.outputs.pi_coding_agent_version }} ccx_version: ${{ steps.versions.outputs.ccx_version }} copilot_api_version: ${{ steps.versions.outputs.copilot_api_version }} steps: @@ -86,6 +87,7 @@ jobs: - Grok CLI: \`${{ steps.versions.outputs.grok_cli_version }}\` - Kimi Code: \`${{ steps.versions.outputs.kimi_code_version }}\` - opencode: \`${{ steps.versions.outputs.opencode_version }}\` + - pi: \`${{ steps.versions.outputs.pi_coding_agent_version }}\` - ccx: \`${{ steps.versions.outputs.ccx_version }}\` - Copilot API: \`${{ steps.versions.outputs.copilot_api_version }}\` - Stamp: \`${{ steps.versions.outputs.stamp }}\` @@ -149,6 +151,7 @@ jobs: GROK_CLI_VERSION=${{ needs.resolve-versions.outputs.grok_cli_version }} KIMI_CODE_VERSION=${{ needs.resolve-versions.outputs.kimi_code_version }} OPENCODE_VERSION=${{ needs.resolve-versions.outputs.opencode_version }} + PI_CODING_AGENT_VERSION=${{ needs.resolve-versions.outputs.pi_coding_agent_version }} CCX_VERSION=${{ needs.resolve-versions.outputs.ccx_version }} COPILOT_API_VERSION=${{ needs.resolve-versions.outputs.copilot_api_version }} @@ -205,6 +208,7 @@ jobs: GROK_CLI_VERSION=${{ needs.resolve-versions.outputs.grok_cli_version }} KIMI_CODE_VERSION=${{ needs.resolve-versions.outputs.kimi_code_version }} OPENCODE_VERSION=${{ needs.resolve-versions.outputs.opencode_version }} + PI_CODING_AGENT_VERSION=${{ needs.resolve-versions.outputs.pi_coding_agent_version }} CCX_VERSION=${{ needs.resolve-versions.outputs.ccx_version }} PLAYWRIGHT_VERSION=${{ needs.load-version-pins.outputs.playwright_version }} RUST_TOOLCHAINS=${{ needs.load-version-pins.outputs.rust_toolchains }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a22c3e0..a673800 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,6 +53,7 @@ jobs: grok_cli_version: ${{ steps.pins.outputs.grok_cli_version }} kimi_code_version: ${{ steps.pins.outputs.kimi_code_version }} opencode_version: ${{ steps.pins.outputs.opencode_version }} + pi_coding_agent_version: ${{ steps.pins.outputs.pi_coding_agent_version }} ccx_version: ${{ steps.pins.outputs.ccx_version }} copilot_api_version: ${{ steps.pins.outputs.copilot_api_version }} playwright_version: ${{ steps.pins.outputs.playwright_version }} @@ -91,6 +92,7 @@ jobs: - Grok CLI: \`${{ steps.pins.outputs.grok_cli_version }}\` - Kimi Code: \`${{ steps.pins.outputs.kimi_code_version }}\` - opencode: \`${{ steps.pins.outputs.opencode_version }}\` + - pi: \`${{ steps.pins.outputs.pi_coding_agent_version }}\` - ccx: \`${{ steps.pins.outputs.ccx_version }}\` - Copilot API: \`${{ steps.pins.outputs.copilot_api_version }}\` - Playwright: \`${{ steps.pins.outputs.playwright_version }}\` @@ -157,6 +159,7 @@ jobs: GROK_CLI_VERSION=${{ needs.load-version-pins.outputs.grok_cli_version }} KIMI_CODE_VERSION=${{ needs.load-version-pins.outputs.kimi_code_version }} OPENCODE_VERSION=${{ needs.load-version-pins.outputs.opencode_version }} + PI_CODING_AGENT_VERSION=${{ needs.load-version-pins.outputs.pi_coding_agent_version }} CCX_VERSION=${{ needs.load-version-pins.outputs.ccx_version }} COPILOT_API_VERSION=${{ needs.load-version-pins.outputs.copilot_api_version }} @@ -215,6 +218,7 @@ jobs: GROK_CLI_VERSION=${{ needs.load-version-pins.outputs.grok_cli_version }} KIMI_CODE_VERSION=${{ needs.load-version-pins.outputs.kimi_code_version }} OPENCODE_VERSION=${{ needs.load-version-pins.outputs.opencode_version }} + PI_CODING_AGENT_VERSION=${{ needs.load-version-pins.outputs.pi_coding_agent_version }} CCX_VERSION=${{ needs.load-version-pins.outputs.ccx_version }} PLAYWRIGHT_VERSION=${{ needs.load-version-pins.outputs.playwright_version }} RUST_TOOLCHAINS=${{ needs.load-version-pins.outputs.rust_toolchains }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f6a2ac..db0b2e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- pi (earendil-works/pi, the minimal multi-provider agent harness by + badlogic) as the 7th agent: `deva.sh pi` (#552). One dot-dir — + everything persists under `~/.pi/agent/` (auth.json, sessions, + settings, trust.json); default `oauth` mounts `~/.pi` writable + (tokens auto-refresh in place, first login via in-app `/login`); + `--auth-with api-key` passes the set provider env keys + (ANTHROPIC/OPENAI/GEMINI/XAI/OPENROUTER) and mounts nothing — + pi's auth.json outranks env keys, so the blank overlay guards the + default path. pi has no permission system by design (its security + doc: run it in a container); deva passes `--approve` for the one + interactive gate (project trust) and sets PI_SKIP_VERSION_CHECK=1 + since the image pins `@earendil-works/pi-coding-agent` + (PI_CODING_AGENT_VERSION). `--trace` rejected until cctrace ships + a pi profile - Stable trace URL via portless (vercel-labs): when the `portless` CLI is on the host, `--trace` registers/refreshes the `cctrace` alias for the published UI port and announces the routed URL — the live diff --git a/DEV-LOGS.md b/DEV-LOGS.md index 018876e..0e99702 100644 --- a/DEV-LOGS.md +++ b/DEV-LOGS.md @@ -13,6 +13,11 @@ - Minimal markdown markers, no unnecessary formatting, minimal emojis. - Reference issue numbers in the format `#` for easy linking. +# [2026-08-12] Dev Log: pi as the 7th agent #552 +- Why: pi (earendil-works/pi, ex badlogic/pi-mono) is the cleanest container-fit yet — no permission system at all, its own security doc says "run it in a contained environment". Multi-provider harness rounds out the herd. +- What: agents/pi.sh (oauth mounts ~/.pi rw — tokens auto-refresh; api-key passes provider env keys, mounts nothing, blank-overlays .pi/agent/auth.json which outranks env), deva.sh registration (auth tag, version label, mounts, autolink, status, env scrub of the 5 provider keys), image pin PI_CODING_AGENT_VERSION=0.84.1 (@earendil-works/pi-coding-agent, needs node >= 22.19 — NODE_MAJOR=22 covers it) through versions.env/Makefile/scripts/CI, scripts/test-pi-auth.sh (16 asserts). Trap dodged: @mariozechner scope is dead (moved to @earendil-works), and @mariozechner/pi is an unrelated vLLM tool. +- Result: 7 agents. test-pi-auth 16/16, release-utils 67/67, tooling test green. --trace deferred (no cctrace pi profile). + # [2026-08-09] Dev Log: context injection moves to local files #548 - Why: every launch appended the container block to tracked AGENTS.md/.claude/CLAUDE.md — phantom diffs, committed container-context lying to host agents, circular @imports triple-loading the block with contradictory persist lines. - What: inject_workspace_context now writes CLAUDE.local.md (Claude Code's sanctioned local memory, per official docs) + a deva-owned AGENTS.md only when the workspace has none (survey of codex/grok/kimi/opencode sources: all read plain AGENTS.md; AGENTS.override.md SHADOWS team files so it's unsafe to auto-write; kimi's .kimi-code/AGENTS.md is the only additive slot). Both excluded via .git/info/exclude (worktree-safe via rev-parse --git-path); legacy blocks healed on next launch, marker-only files removed. Repo's own polluted AGENTS.md rewritten (imports + block removed), untracked .claude/CLAUDE.md residue deleted. diff --git a/Dockerfile b/Dockerfile index 597958e..8aa8466 100644 --- a/Dockerfile +++ b/Dockerfile @@ -214,6 +214,7 @@ ARG GEMINI_CLI_VERSION=0.42.0 ARG GROK_CLI_VERSION=0.2.93 ARG KIMI_CODE_VERSION=0.28.0 ARG OPENCODE_VERSION=1.18.14 +ARG PI_CODING_AGENT_VERSION=0.84.1 # Record key tool versions as labels for quick inspection LABEL org.opencontainers.image.claude_code_version=${CLAUDE_CODE_VERSION} @@ -222,6 +223,7 @@ LABEL org.opencontainers.image.gemini_cli_version=${GEMINI_CLI_VERSION} LABEL org.opencontainers.image.grok_cli_version=${GROK_CLI_VERSION} LABEL org.opencontainers.image.kimi_code_version=${KIMI_CODE_VERSION} LABEL org.opencontainers.image.opencode_version=${OPENCODE_VERSION} +LABEL org.opencontainers.image.pi_coding_agent_version=${PI_CODING_AGENT_VERSION} ARG CCX_VERSION=v0.7.0 diff --git a/Dockerfile.rust b/Dockerfile.rust index 4920de9..2ba0e16 100644 --- a/Dockerfile.rust +++ b/Dockerfile.rust @@ -14,6 +14,7 @@ ARG GEMINI_CLI_VERSION=0.42.0 ARG GROK_CLI_VERSION=0.2.93 ARG KIMI_CODE_VERSION=0.28.0 ARG OPENCODE_VERSION=1.18.14 +ARG PI_CODING_AGENT_VERSION=0.84.1 ARG CCX_VERSION=v0.7.0 ARG PLAYWRIGHT_VERSION=1.60.0 ARG RUST_TOOLCHAINS="stable" @@ -26,6 +27,7 @@ LABEL org.opencontainers.image.gemini_cli_version=${GEMINI_CLI_VERSION} LABEL org.opencontainers.image.grok_cli_version=${GROK_CLI_VERSION} LABEL org.opencontainers.image.kimi_code_version=${KIMI_CODE_VERSION} LABEL org.opencontainers.image.opencode_version=${OPENCODE_VERSION} +LABEL org.opencontainers.image.pi_coding_agent_version=${PI_CODING_AGENT_VERSION} LABEL org.opencontainers.image.ccx_version=${CCX_VERSION} LABEL org.opencontainers.image.playwright_version=${PLAYWRIGHT_VERSION} diff --git a/Makefile b/Makefile index 04661ab..50dadde 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,7 @@ GEMINI_CLI_VERSION ?= 0.42.0 GROK_CLI_VERSION ?= 0.2.93 KIMI_CODE_VERSION ?= 0.28.0 OPENCODE_VERSION ?= 1.18.14 +PI_CODING_AGENT_VERSION ?= 0.84.1 CCX_VERSION ?= v0.1.4 COPILOT_API_VERSION ?= 0ea08febdd7e3e055b03dd298bf57e669500b5c1 PLAYWRIGHT_VERSION ?= 1.60.0 @@ -92,6 +93,7 @@ AGENT_BUILD_ARGS := \ --build-arg GROK_CLI_VERSION=$(GROK_CLI_VERSION) \ --build-arg KIMI_CODE_VERSION=$(KIMI_CODE_VERSION) \ --build-arg OPENCODE_VERSION=$(OPENCODE_VERSION) \ + --build-arg PI_CODING_AGENT_VERSION=$(PI_CODING_AGENT_VERSION) \ --build-arg CCX_VERSION=$(CCX_VERSION) MAIN_BUILD_ARGS := $(TOOLCHAIN_BUILD_ARGS) $(AGENT_BUILD_ARGS) \ @@ -120,6 +122,7 @@ VERSION_QUERY_OVERRIDES := \ $(if $(filter command line environment environment\ override override,$(origin GROK_CLI_VERSION)),GROK_CLI_VERSION=$(GROK_CLI_VERSION)) \ $(if $(filter command line environment environment\ override override,$(origin KIMI_CODE_VERSION)),KIMI_CODE_VERSION=$(KIMI_CODE_VERSION)) \ $(if $(filter command line environment environment\ override override,$(origin OPENCODE_VERSION)),OPENCODE_VERSION=$(OPENCODE_VERSION)) \ + $(if $(filter command line environment environment\ override override,$(origin PI_CODING_AGENT_VERSION)),PI_CODING_AGENT_VERSION=$(PI_CODING_AGENT_VERSION)) \ $(if $(filter command line environment environment\ override override,$(origin CCX_VERSION)),CCX_VERSION=$(CCX_VERSION)) \ $(if $(filter command line environment environment\ override override,$(origin COPILOT_API_VERSION)),COPILOT_API_VERSION=$(COPILOT_API_VERSION)) \ $(if $(filter command line environment environment\ override override,$(origin PLAYWRIGHT_VERSION)),PLAYWRIGHT_VERSION=$(PLAYWRIGHT_VERSION)) \ @@ -167,16 +170,18 @@ build-main: build-network-check prev_grok=$$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.grok_cli_version" }}' $(MAIN_IMAGE) 2>/dev/null || true); \ prev_kimi=$$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.kimi_code_version" }}' $(MAIN_IMAGE) 2>/dev/null || true); \ prev_opencode=$$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.opencode_version" }}' $(MAIN_IMAGE) 2>/dev/null || true); \ + prev_pi=$$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.pi_coding_agent_version" }}' $(MAIN_IMAGE) 2>/dev/null || true); \ fmt() { v="$$1"; if [ -z "$$v" ] || [ "$$v" = "" ]; then echo "-"; else case "$$v" in v*) echo "$$v";; *) echo "v$$v";; esac; fi; }; \ - curC=$$(fmt "$$prev_claude"); curX=$$(fmt "$$prev_codex"); curG=$$(fmt "$$prev_gemini"); curK=$$(fmt "$$prev_grok"); curKi=$$(fmt "$$prev_kimi"); curO=$$(fmt "$$prev_opencode"); \ - tgtC=$$(fmt "$(CLAUDE_CODE_VERSION)"); tgtX=$$(fmt "$(CODEX_VERSION)"); tgtG=$$(fmt "$(GEMINI_CLI_VERSION)"); tgtK=$$(fmt "$(GROK_CLI_VERSION)"); tgtKi=$$(fmt "$(KIMI_CODE_VERSION)"); tgtO=$$(fmt "$(OPENCODE_VERSION)"); \ - if [ "$$curC" = "$$tgtC" ] && [ "$$curX" = "$$tgtX" ] && [ "$$curG" = "$$tgtG" ] && [ "$$curK" = "$$tgtK" ] && [ "$$curKi" = "$$tgtKi" ] && [ "$$curO" = "$$tgtO" ]; then \ + curC=$$(fmt "$$prev_claude"); curX=$$(fmt "$$prev_codex"); curG=$$(fmt "$$prev_gemini"); curK=$$(fmt "$$prev_grok"); curKi=$$(fmt "$$prev_kimi"); curO=$$(fmt "$$prev_opencode"); curP=$$(fmt "$$prev_pi"); \ + tgtC=$$(fmt "$(CLAUDE_CODE_VERSION)"); tgtX=$$(fmt "$(CODEX_VERSION)"); tgtG=$$(fmt "$(GEMINI_CLI_VERSION)"); tgtK=$$(fmt "$(GROK_CLI_VERSION)"); tgtKi=$$(fmt "$(KIMI_CODE_VERSION)"); tgtO=$$(fmt "$(OPENCODE_VERSION)"); tgtP=$$(fmt "$(PI_CODING_AGENT_VERSION)"); \ + if [ "$$curC" = "$$tgtC" ] && [ "$$curX" = "$$tgtX" ] && [ "$$curG" = "$$tgtG" ] && [ "$$curK" = "$$tgtK" ] && [ "$$curKi" = "$$tgtKi" ] && [ "$$curO" = "$$tgtO" ] && [ "$$curP" = "$$tgtP" ]; then \ echo "Claude: $$tgtC (no change)"; \ echo "Codex: $$tgtX (no change)"; \ echo "Gemini: $$tgtG (no change)"; \ echo "Grok: $$tgtK (no change)"; \ echo "Kimi: $$tgtKi (no change)"; \ echo "opencode: $$tgtO (no change)"; \ + echo "pi: $$tgtP (no change)"; \ echo "Already up-to-date"; \ else \ if [ "$$curC" = "$$tgtC" ]; then \ @@ -209,6 +214,11 @@ build-main: build-network-check else \ echo "opencode: $$curO -> $$tgtO"; \ fi; \ + if [ "$$curP" = "$$tgtP" ]; then \ + echo "pi: $$tgtP (no change)"; \ + else \ + echo "pi: $$curP -> $$tgtP"; \ + fi; \ fi @echo "Hint: override via GO_VERSION=... CLAUDE_CODE_VERSION=... or run 'make versions-pin'" docker build $(DOCKER_BUILD_FLAGS) -f $(DOCKERFILE) $(MAIN_BUILD_ARGS) -t $(MAIN_IMAGE) . @@ -508,6 +518,7 @@ help: @echo " GROK_CLI_VERSION Grok CLI version (default: $(GROK_CLI_VERSION))" @echo " KIMI_CODE_VERSION Kimi Code CLI version (default: $(KIMI_CODE_VERSION))" @echo " OPENCODE_VERSION opencode CLI version (default: $(OPENCODE_VERSION))" + @echo " PI_CODING_AGENT_VERSION pi CLI version (default: $(PI_CODING_AGENT_VERSION))" @echo " CCX_VERSION Atlas CLI version (default: $(CCX_VERSION))" @echo " PLAYWRIGHT_VERSION Playwright version (default: $(PLAYWRIGHT_VERSION))" @echo " CLOAKBROWSER_WRAPPER_VERSION CloakBrowser npm wrapper version (default: $(CLOAKBROWSER_WRAPPER_VERSION))" diff --git a/README.md b/README.md index 1cf8d9f..36ee002 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ English | [简体中文](README.zh-CN.md) [![Docs](https://img.shields.io/badge/docs-docs.deva.sh-111111)](https://docs.deva.sh) [![License](https://img.shields.io/github/license/thevibeworks/deva)](LICENSE) -Run Claude Code, Codex, Gemini, Grok, Kimi, and opencode inside Docker without pretending the agents' own permission prompts are the thing keeping you safe. +Run Claude Code, Codex, Gemini, Grok, Kimi, opencode, and pi inside Docker without pretending the agents' own permission prompts are the thing keeping you safe. The container is the sandbox, mounts are the contract. What that buys you: -- Full-speed agents. Permission prompts exist because the blast radius is your host. Make the blast radius a container and YOLO stops being reckless — all six agents run with their permission systems off (`claude --dangerously-skip-permissions`, `codex --dangerously-bypass-approvals-and-sandbox`, `gemini --yolo`, `grok --always-approve`, `kimi --yolo`, opencode via `OPENCODE_PERMISSION` allow-all). Worst case dies with the container. +- Full-speed agents. Permission prompts exist because the blast radius is your host. Make the blast radius a container and YOLO stops being reckless — all seven agents run with their permission systems off (`claude --dangerously-skip-permissions`, `codex --dangerously-bypass-approvals-and-sandbox`, `gemini --yolo`, `grok --always-approve`, `kimi --yolo`, opencode via `OPENCODE_PERMISSION` allow-all, pi has no permission system by design). Worst case dies with the container. - The vendor's code never sees your host. Agent CLIs are fast-moving npm trees with auto-updaters. Here they are born inside the image: no `~/.ssh`, no `~/.aws`, no shell env soup, no browser profiles. Nothing to harvest. - Everything that crosses the boundary is explicit. Files: mounts. Secrets: the env you pass. Network: the posture you pick — default bridge, your proxy, `--host-net`, or nothing. If you didn't wire it, the agent doesn't have it. - Identity is a launch flag, not a global singleton. Per-agent config homes under `~/.config/deva/`, `--auth-with` / `--config-home` for the second account or API-key billing. Switch accounts per run; project, sessions, and container state stay put. @@ -40,6 +40,7 @@ deva.sh gemini deva.sh grok deva.sh kimi deva.sh opencode +deva.sh pi deva.sh claude --rm # throwaway container deva.sh claude --debug --dry-run # inspect the docker run before trusting it diff --git a/README.zh-CN.md b/README.zh-CN.md index 8758796..5311b1c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -7,11 +7,11 @@ [![Docs](https://img.shields.io/badge/docs-docs.deva.sh-111111)](https://docs.deva.sh) [![License](https://img.shields.io/github/license/thevibeworks/deva)](LICENSE) -在 Docker 里跑 Claude Code、Codex、Gemini、Grok、Kimi、opencode 六个 agent CLI,并且不再假装 agent 自带的权限弹窗是在保护你。 +在 Docker 里跑 Claude Code、Codex、Gemini、Grok、Kimi、opencode、pi 七个 agent CLI,并且不再假装 agent 自带的权限弹窗是在保护你。 容器即沙箱,挂载即契约。这换来的是: -- 全速跑 agent。权限弹窗存在的理由是爆炸半径等于你的整机;把爆炸半径缩小到一个容器,YOLO 就不再是鲁莽 —— 六个 agent 全部关掉自带权限系统跑(`claude --dangerously-skip-permissions`、`codex --dangerously-bypass-approvals-and-sandbox`、`gemini --yolo`、`grok --always-approve`、`kimi --yolo`、opencode 走 `OPENCODE_PERMISSION` 全放行)。最坏结果随容器一起销毁。 +- 全速跑 agent。权限弹窗存在的理由是爆炸半径等于你的整机;把爆炸半径缩小到一个容器,YOLO 就不再是鲁莽 —— 七个 agent 全部关掉自带权限系统跑(`claude --dangerously-skip-permissions`、`codex --dangerously-bypass-approvals-and-sandbox`、`gemini --yolo`、`grok --always-approve`、`kimi --yolo`、opencode 走 `OPENCODE_PERMISSION` 全放行、pi 本身就没有权限系统)。最坏结果随容器一起销毁。 - 厂商代码永远见不到你的主机。agent CLI 是快速迭代、带自动更新的 npm 依赖树;在这里它们出生在镜像里:没有 `~/.ssh`、没有 `~/.aws`、没有 shell 环境变量汤、没有浏览器档案。想收集也无从下手。 - 跨越边界的一切都是显式的。文件靠挂载,密钥靠你传的 env,网络靠你选的姿态 —— 默认 bridge、走你的代理、`--host-net`、或者干脆不联网。你没接的线,agent 就没有。 - 身份是启动参数,不是全局单例。`~/.config/deva/` 下按 agent 分家,`--auth-with` / `--config-home` 切第二个账号或 API-key 计费。按次切账号,项目、会话、容器状态原地不动。 @@ -40,6 +40,7 @@ deva.sh gemini deva.sh grok deva.sh kimi deva.sh opencode +deva.sh pi deva.sh claude --rm # 一次性容器,用完即扔 deva.sh claude --debug --dry-run # 先看 docker run 长啥样再信它 diff --git a/agents/pi.sh b/agents/pi.sh new file mode 100644 index 0000000..10ef4d5 --- /dev/null +++ b/agents/pi.sh @@ -0,0 +1,94 @@ +# shellcheck shell=bash + +# shellcheck disable=SC1091 +if [ -f "$(dirname "${BASH_SOURCE[0]}")/shared_auth.sh" ]; then + source "$(dirname "${BASH_SOURCE[0]}")/shared_auth.sh" +fi + +# Provider API keys pi reads from the environment (packages/ai +# env-api-keys). deva's api-key mode passes every one that is set -- +# pi is multi-provider by design -- and requires at least one. +# Order matters: the first set key names the auth tag. +PI_API_KEY_VARS=(ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY XAI_API_KEY OPENROUTER_API_KEY) + +agent_prepare() { + local -a args + if [ $# -gt 0 ]; then + args=("$@") + else + args=() + fi + AGENT_COMMAND=("pi") + + parse_auth_args "pi" "${args[@]+"${args[@]}"}" + AUTH_METHOD="$PARSED_AUTH_METHOD" + local -a remaining_args=("${PARSED_REMAINING_ARGS[@]+"${PARSED_REMAINING_ARGS[@]}"}") + + filter_trace_flag "${remaining_args[@]+"${remaining_args[@]}"}" + local use_trace="$TRACE_FLAG_PRESENT" + remaining_args=("${TRACE_FILTERED_ARGS[@]+"${TRACE_FILTERED_ARGS[@]}"}") + + if [ "$use_trace" = true ]; then + auth_error "--trace is not supported for pi yet" \ + "cctrace has no pi profile" + fi + + # Container is the sandbox: pi has no permission system at all (its + # security doc says to run it in a contained environment -- that is + # exactly what deva does). The one interactive gate is project trust + # (loading workspace .pi/ settings/extensions, default ask); --approve + # unlocks it so unattended runs never stall on the prompt. + AGENT_COMMAND+=("--approve") + # The image pins the CLI; the startup version check would nag (and + # phone pi.dev) on every launch for an update we deliberately hold. + DOCKER_ARGS+=("-e" "PI_SKIP_VERSION_CHECK=1") + + AGENT_COMMAND+=("${remaining_args[@]+"${remaining_args[@]}"}") + + setup_pi_auth "$AUTH_METHOD" +} + +setup_pi_auth() { + local method="$1" + + case "$method" in + oauth) + AUTH_DETAILS="oauth (~/.pi)" + # Everything pi persists lives under ~/.pi/agent (no XDG): + # auth.json (from in-app /login; tokens AUTO-REFRESH, so the + # mount must stay writable), sessions, settings, trust.json. + # Only mount the host dir directly when no config-home + # mechanism is active. -Q bare mode: no mounts at all. + # Explicit/auto config-home: centralized mount handles it. + if [ "${QUICK_MODE:-false}" = false ] && [ "${CONFIG_HOME_FROM_CLI:-false}" = false ] && [ "${CONFIG_HOME_AUTO:-false}" = false ]; then + if [ ! -d "$HOME/.pi" ]; then + echo "Warning: ~/.pi directory not found, creating it" >&2 + mkdir -p "$HOME/.pi" + fi + DOCKER_ARGS+=("-v" "$HOME/.pi:/home/deva/.pi") + fi + ;; + api-key) + # Provider keys travel as env only. No mount: a mounted ~/.pi + # carries auth.json, which OUTRANKS env keys in pi and could + # silently bill another account (same no-mount contract as + # grok/kimi/opencode api-key). At least one key is required; + # all set keys are passed -- pi is multi-provider by design. + local var found="" + for var in "${PI_API_KEY_VARS[@]}"; do + if [ -n "${!var:-}" ]; then + [ -n "$found" ] || found="$var" + DOCKER_ARGS+=("-e" "${var}=${!var}") + fi + done + if [ -z "$found" ]; then + auth_error "no provider API key set for --auth-with api-key" \ + "Set at least one of: ${PI_API_KEY_VARS[*]}" + fi + AUTH_DETAILS="api-key (${found})" + ;; + *) + auth_error "auth method '$method' not implemented for pi" + ;; + esac +} diff --git a/agents/shared_auth.sh b/agents/shared_auth.sh index 40b2c21..db419bc 100644 --- a/agents/shared_auth.sh +++ b/agents/shared_auth.sh @@ -538,6 +538,9 @@ parse_auth_args() { opencode) supported_methods=(oauth api-key) ;; + pi) + supported_methods=(oauth api-key) + ;; *) auth_error "Unknown agent: $agent_name" ;; @@ -606,6 +609,7 @@ parse_auth_args() { grok) auth_method="oauth" ;; kimi) auth_method="oauth" ;; opencode) auth_method="oauth" ;; + pi) auth_method="oauth" ;; esac fi diff --git a/deva.sh b/deva.sh index c7bcb75..c25b649 100755 --- a/deva.sh +++ b/deva.sh @@ -110,7 +110,7 @@ _step() { usage() { cat <<'USAGE' -deva.sh - Docker-based multi-agent launcher (Claude, Codex, Gemini, Grok, Kimi, opencode) +deva.sh - Docker-based multi-agent launcher (Claude, Codex, Gemini, Grok, Kimi, opencode, pi) Usage: deva.sh [deva flags] [agent] [-- agent-flags] @@ -216,6 +216,7 @@ Examples: deva.sh grok # Launch grok in the same default container shape deva.sh kimi # Launch kimi (oauth default; api-key via KIMI_CODE_API_KEY) deva.sh opencode # Launch opencode (oauth default; api-key via OPENCODE_API_KEY) + deva.sh pi # Launch pi (oauth default; api-key via provider env keys) deva.sh claude --rm # Ephemeral: deva-work-myapp-claude-12345 # Container management (current project) @@ -944,7 +945,7 @@ generate_auth_tag() { fi case "$agent:$auth_method" in - claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth|opencode:oauth) + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth|opencode:oauth|pi:oauth) printf '%s' "auth-default" return ;; @@ -969,6 +970,8 @@ generate_auth_tag() { grok) key_val="${XAI_API_KEY:-}" ;; kimi) key_val="${KIMI_CODE_API_KEY:-${KIMI_API_KEY:-}}" ;; opencode) key_val="${OPENCODE_API_KEY:-}" ;; + # first set key in pi.sh's PI_API_KEY_VARS order + pi) key_val="${ANTHROPIC_API_KEY:-${OPENAI_API_KEY:-${GEMINI_API_KEY:-${XAI_API_KEY:-${OPENROUTER_API_KEY:-}}}}}" ;; esac if [ -n "$key_val" ] && [ ${#key_val} -ge 4 ]; then printf '%s' "api-key-${key_val: -4}" @@ -1016,6 +1019,7 @@ agent_version_tag() { grok) label="org.opencontainers.image.grok_cli_version" ;; kimi) label="org.opencontainers.image.kimi_code_version" ;; opencode) label="org.opencontainers.image.opencode_version" ;; + pi) label="org.opencontainers.image.pi_coding_agent_version" ;; esac local ver="" @@ -1361,7 +1365,8 @@ categorize_mount() { elif [[ "$dest" == /deva-host-chrome-bridge* ]]; then printf 'bridge' elif [[ "$dest" == /home/deva/.claude* ]] || [[ "$dest" == /home/deva/.codex* ]] || \ [[ "$dest" == /home/deva/.gemini* ]] || [[ "$dest" == /home/deva/.grok* ]] || \ - [[ "$dest" == /home/deva/.kimi-code* ]] || [[ "$dest" == /home/deva/.config/opencode* ]] || \ + [[ "$dest" == /home/deva/.kimi-code* ]] || [[ "$dest" == /home/deva/.pi* ]] || \ + [[ "$dest" == /home/deva/.config/opencode* ]] || \ [[ "$dest" == /home/deva/.local/share/opencode* ]] || [[ "$dest" == /home/deva/.local/state/opencode* ]] || \ [ "$dest" = "/home/deva/.agents" ]; then printf 'config' @@ -1746,7 +1751,7 @@ cmd_status() { if [ -d "$config_root" ]; then echo "Agent Homes ($(shorten_path "$config_root")):" - for agent_name in claude codex gemini grok kimi opencode; do + for agent_name in claude codex gemini grok kimi opencode pi; do local agent_dir="$config_root/$agent_name" if [ -d "$agent_dir" ]; then local canonical="" other_count=0 entry is_canonical @@ -2316,6 +2321,16 @@ should_skip_env_for_auth() { ;; esac ;; + pi) + # Provider keys only travel when deva injects them (api-key mode). + # pi reads all of these from the env; a host-set key leaking into + # oauth mode would enable API billing beside the subscription. + case "$name" in + ANTHROPIC_API_KEY | OPENAI_API_KEY | GEMINI_API_KEY | XAI_API_KEY | OPENROUTER_API_KEY) + return 0 + ;; + esac + ;; esac return 1 @@ -2379,6 +2394,7 @@ agent_canonical_basenames() { # verbatim (docker creates intermediate dirs); cache is left out on # purpose (disposable models.json + self-update bin). opencode) printf '%s\n' '.config/opencode' '.local/share/opencode' '.local/state/opencode' ;; + pi) printf '%s\n' '.pi' ;; *) return 0 ;; esac } @@ -2407,6 +2423,13 @@ opencode_api_key_no_mount() { [ "$ACTIVE_AGENT" = "opencode" ] && [ "${AUTH_METHOD:-}" = "api-key" ] } +# pi api-key contract: provider keys travel as env only. A mounted ~/.pi +# carries agent/auth.json, which outranks env keys in pi and could +# silently bill another account, so api-key mounts nothing. +pi_api_key_no_mount() { + [ "$ACTIVE_AGENT" = "pi" ] && [ "${AUTH_METHOD:-}" = "api-key" ] +} + # grok's self-updater writes Linux binaries into ~/.grok/bin and # ~/.grok/downloads — inside the auth dir we bind-mount. Verified against # @xai-official/grok 0.2.93: a mounted config.toml without the npm installer @@ -2447,6 +2470,9 @@ mount_agent_canonical() { if [ "$agent" = "opencode" ] && opencode_api_key_no_mount; then return 0 fi + if [ "$agent" = "pi" ] && pi_api_key_no_mount; then + return 0 + fi local entry src while IFS= read -r entry; do @@ -2503,7 +2529,7 @@ has_auth_override() { # Non-default --auth-with if [ -n "${AUTH_METHOD:-}" ]; then case "${ACTIVE_AGENT}:${AUTH_METHOD}" in - claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth|opencode:oauth) ;; + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth|opencode:oauth|pi:oauth) ;; *) return 0 ;; esac fi @@ -2517,6 +2543,7 @@ has_auth_override() { grok) auth_vars="XAI_API_KEY" ;; kimi) auth_vars="KIMI_CODE_API_KEY KIMI_API_KEY" ;; opencode) auth_vars="OPENCODE_API_KEY" ;; + pi) auth_vars="ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY XAI_API_KEY OPENROUTER_API_KEY" ;; esac local var @@ -2569,6 +2596,12 @@ default_credential_target_path() { # outranks OPENCODE_API_KEY, so blank-overlay it anyway. printf '%s' "/home/deva/.local/share/opencode/auth.json" ;; + pi) + # api-key mode mounts no ~/.pi (pi_api_key_no_mount), but an + # explicit user -v/.deva VOLUME can still carry one in; auth.json + # outranks env provider keys, so blank-overlay it anyway. + printf '%s' "/home/deva/.pi/agent/auth.json" + ;; *) return 1 ;; @@ -3908,7 +3941,7 @@ if [ "$CONFIG_HOME_AUTO" = true ]; then fi if [ "$CONFIG_HOME_FROM_CLI" = true ] && [ -n "$CONFIG_HOME" ]; then - if [ -d "$CONFIG_HOME/claude" ] || [ -d "$CONFIG_HOME/codex" ] || [ -d "$CONFIG_HOME/gemini" ] || [ -d "$CONFIG_HOME/grok" ] || [ -d "$CONFIG_HOME/kimi" ] || [ -d "$CONFIG_HOME/opencode" ]; then + if [ -d "$CONFIG_HOME/claude" ] || [ -d "$CONFIG_HOME/codex" ] || [ -d "$CONFIG_HOME/gemini" ] || [ -d "$CONFIG_HOME/grok" ] || [ -d "$CONFIG_HOME/kimi" ] || [ -d "$CONFIG_HOME/opencode" ] || [ -d "$CONFIG_HOME/pi" ]; then CONFIG_ROOT="$CONFIG_HOME" CONFIG_HOME="" CONFIG_HOME_AUTO=false @@ -3982,6 +4015,17 @@ autolink_legacy_into_deva_root() { [ -d "$CONFIG_ROOT/kimi/.kimi-code" ] || [ -L "$CONFIG_ROOT/kimi/.kimi-code" ] || mkdir -p "$CONFIG_ROOT/kimi/.kimi-code" fi + if [ -d "$HOME/.pi" ]; then + [ -d "$CONFIG_ROOT/pi" ] || mkdir -p "$CONFIG_ROOT/pi" + if [ ! -e "$CONFIG_ROOT/pi/.pi" ] && [ ! -L "$CONFIG_ROOT/pi/.pi" ]; then + ln -s "$HOME/.pi" "$CONFIG_ROOT/pi/.pi" + echo "autolink: ~/.pi -> $CONFIG_ROOT/pi/.pi" >&2 + fi + fi + if [ -d "$CONFIG_ROOT" ]; then + [ -d "$CONFIG_ROOT/pi/.pi" ] || [ -L "$CONFIG_ROOT/pi/.pi" ] || mkdir -p "$CONFIG_ROOT/pi/.pi" + fi + # opencode: XDG-native, so the legacy homes and the links are nested. local _oc_entry while IFS= read -r _oc_entry; do @@ -4034,6 +4078,9 @@ if [ -n "$CONFIG_HOME" ] && [ "$DRY_RUN" != true ]; then done < <(agent_canonical_basenames "opencode") unset _oc_entry ;; + pi) + [ -d "$CONFIG_HOME/.pi" ] || mkdir -p "$CONFIG_HOME/.pi" + ;; esac fi @@ -4085,6 +4132,12 @@ if [ "$CONFIG_HOME_FROM_CLI" = true ] && [ -n "$CONFIG_HOME" ] && [ "$_config_ho echo "warning: $CONFIG_HOME/.local/share/opencode is empty; authentication will need to be set up" >&2 fi ;; + pi) + # auth.json lives in .pi/agent; that's the one that matters. + if [ ! -d "$CONFIG_HOME/.pi/agent" ] || [ -z "$(ls -A "$CONFIG_HOME/.pi/agent" 2>/dev/null)" ]; then + echo "warning: $CONFIG_HOME/.pi/agent is empty; authentication will need to be set up" >&2 + fi + ;; esac fi @@ -4123,7 +4176,7 @@ _step "agent_prepare" if [ -n "${AUTH_METHOD:-}" ]; then case "${ACTIVE_AGENT}:${AUTH_METHOD}" in - claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth|opencode:oauth) ;; + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth|opencode:oauth|pi:oauth) ;; *) _needs_rewrite=true ;; esac @@ -4250,6 +4303,10 @@ else done < <(agent_canonical_basenames "opencode") unset _oc_entry fi + # pi api-key uses provider env keys (no mount); oauth wants ~/.pi rw. + if [ -d "$HOME/.pi" ] && ! pi_api_key_no_mount; then + DOCKER_ARGS+=("-v" "$HOME/.pi:/home/deva/.pi") + fi fi fi _step "mount dispatch: done" diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 59c564b..d903b48 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -50,7 +50,8 @@ Deva-root layout: ├── gemini/ ├── grok/ ├── kimi/ -└── opencode/ +├── opencode/ +└── pi/ ``` ```bash @@ -188,9 +189,9 @@ verbatim and is never intercepted by deva. which records every API call the agent makes — messages, OAuth, usage/credits, MCP — not just the chat endpoint. Everything else on the line goes to the agent unchanged. Codex, Grok, and Kimi use cctrace client profiles -(cctrace >= 0.11) and always run MITM capture. opencode has no cctrace -profile yet, so `deva.sh opencode --trace` is rejected -(thevibeworks/cctrace#89). +(cctrace >= 0.11) and always run MITM capture. opencode and pi have no +cctrace profile yet, so `deva.sh opencode --trace` and `deva.sh pi --trace` +are rejected (thevibeworks/cctrace#89). When tracing is on, the entrypoint installs the cctrace MITM CA into the container's system trust store (`update-ca-certificates`) so subprocesses and diff --git a/docs/authentication.md b/docs/authentication.md index 0e4a784..cd4cfb1 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -23,6 +23,7 @@ This guide documents what `deva.sh` actually supports, what env vars it reads, a | Grok | `oauth` | `api-key` | `.grok/auth.json`, `XAI_API_KEY` | | Kimi | `oauth` | `api-key` | `.kimi-code` (device-code), `KIMI_CODE_API_KEY` -> `KIMI_MODEL_*` | | opencode | `oauth` | `api-key` | `.local/share/opencode/auth.json` (device-code), `OPENCODE_API_KEY` | +| pi | `oauth` | `api-key` | `.pi/agent/auth.json` (in-app `/login`), provider env keys (`ANTHROPIC_API_KEY`, ...) | ## Claude @@ -457,6 +458,47 @@ BYO provider keys (Anthropic, OpenAI, OpenRouter, ...) are opencode config, not deva auth methods — wire them with `-e` / `.deva` `ENV=` entries and opencode's own `opencode.jsonc`. +## pi + +### Default: `--auth-with oauth` + +Mounts `~/.pi` — everything pi persists lives under `.pi/agent/` +(auth.json, sessions, settings, trust.json; no XDG dirs). The mount stays +writable on purpose: pi's OAuth tokens auto-refresh and it rewrites +`auth.json` in place. + +First login has no browser: run `/login` inside the TUI. Claude Pro/Max +and ChatGPT logins print a URL you open on any device and paste the +redirect back; GitHub Copilot and xAI use device-code flows. Or log in on +the host once; autolink carries `~/.pi` in. + +pi has no permission system at all — its own security doc says to run it +in a contained environment, which is exactly what deva does. The only +interactive gate is project trust (loading workspace `.pi/` settings and +extensions); deva passes `--approve` so unattended runs never stall. +`PI_SKIP_VERSION_CHECK=1` is set because the image pins the CLI +(`PI_CODING_AGENT_VERSION`). + +### `--auth-with api-key` + +Inputs (at least one; all set keys travel — pi is multi-provider): + +- `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`, `OPENROUTER_API_KEY` + +The keys travel as env only. This mode mounts nothing: pi's `auth.json` +OUTRANKS env keys, so a mounted `~/.pi` could silently bill another +account (same no-mount contract as grok/kimi/opencode api-key). A blank +overlay hides `auth.json` even if a user `-v` carries a dir in. The +container name is tagged from the first set key in the order above. + +```bash +export ANTHROPIC_API_KEY=sk-ant-... +deva.sh pi --auth-with api-key -- --provider anthropic +``` + +Provider/model selection (`--provider`, `--model`) is pi's own CLI +surface — pass it after `--`. + ## Config Homes And Auth Isolation Default homes live under: diff --git a/docs/how-it-works.md b/docs/how-it-works.md index fee5cda..5f2a440 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -64,13 +64,14 @@ Default per-agent homes live under: ├── gemini/ ├── grok/ ├── kimi/ -└── opencode/ +├── opencode/ +└── pi/ ``` `--config-home` supports two layouts: -- leaf home: `DIR/.claude`, `DIR/.claude.json`, `DIR/.codex`, `DIR/.gemini`, `DIR/.grok`, `DIR/.kimi-code`, or opencode's nested `DIR/.config/opencode` + `DIR/.local/share/opencode` + `DIR/.local/state/opencode` -- deva root: `DIR/claude`, `DIR/codex`, `DIR/gemini`, `DIR/grok`, `DIR/kimi`, `DIR/opencode` +- leaf home: `DIR/.claude`, `DIR/.claude.json`, `DIR/.codex`, `DIR/.gemini`, `DIR/.grok`, `DIR/.kimi-code`, `DIR/.pi`, or opencode's nested `DIR/.config/opencode` + `DIR/.local/share/opencode` + `DIR/.local/state/opencode` +- deva root: `DIR/claude`, `DIR/codex`, `DIR/gemini`, `DIR/grok`, `DIR/kimi`, `DIR/opencode`, `DIR/pi` `-Q` disables config-home resolution, autolink, and host config mounts entirely. @@ -87,6 +88,7 @@ Examples: - Grok default: `.grok/auth.json` - Kimi default: `.kimi-code` (device-code OAuth); api-key maps `KIMI_CODE_API_KEY` onto `KIMI_MODEL_*` - opencode default: the XDG trio with `auth.json` under `.local/share/opencode` (device-code OAuth); api-key passes `OPENCODE_API_KEY`, mounts nothing +- pi default: `.pi` with `auth.json` under `.pi/agent` (in-app `/login`, tokens auto-refresh); api-key passes provider env keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, ...), mounts nothing When non-default auth is active, deva mounts a blank overlay over the default credential file path so the agent cannot silently fall back to some unrelated OAuth state. That is the point of the overlay fix. @@ -118,7 +120,7 @@ Persistent is default: - one default container shape per project - reused across runs -- same workspace can run Claude, Codex, Gemini, Grok, Kimi, and opencode in the same container when mounts, config, and auth line up +- same workspace can run Claude, Codex, Gemini, Grok, Kimi, opencode, and pi in the same container when mounts, config, and auth line up - different volumes, explicit config homes, or auth modes create separate persistent containers Ephemeral with `--rm`: diff --git a/docs/index.md b/docs/index.md index 9f4c8c6..0adead9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # deva.sh -Run Codex, Claude Code, Gemini, Grok, Kimi, and opencode inside Docker without pretending +Run Codex, Claude Code, Gemini, Grok, Kimi, opencode, and pi inside Docker without pretending the agent's own sandbox is the thing keeping you safe. The container is the sandbox. Explicit mounts are the contract. @@ -24,7 +24,7 @@ If you want the internals instead of vague hand-waving: ## What This Is -- a Docker-based launcher for Codex, Claude, Gemini, Grok, Kimi, and opencode +- a Docker-based launcher for Codex, Claude, Gemini, Grok, Kimi, opencode, and pi - one warm default container shape per project by default - explicit mount and env wiring instead of mystery behavior - per-agent config homes under `~/.config/deva/` diff --git a/docs/philosophy.md b/docs/philosophy.md index 9370b4a..2fe25c2 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -39,7 +39,7 @@ Persistent per-project containers mean: - warm package caches - stateful shell history and scratch space -- fast switching between Claude, Codex, Gemini, Grok, Kimi, and opencode +- fast switching between Claude, Codex, Gemini, Grok, Kimi, opencode, and pi `--rm` still exists. It just is not the default because the default should serve real work instead of screenshots. diff --git a/docs/quick-start.md b/docs/quick-start.md index dbf580f..7fec185 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -29,6 +29,7 @@ That installs: - `agents/grok.sh` - `agents/kimi.sh` - `agents/opencode.sh` +- `agents/pi.sh` - `agents/shared_auth.sh` It also pulls `ghcr.io/thevibeworks/deva:latest`, with Docker Hub as fallback. @@ -79,6 +80,7 @@ deva.sh gemini deva.sh grok deva.sh kimi deva.sh opencode +deva.sh pi ``` That is one of the main reasons this wrapper exists. You do not need a separate pet workflow for every vendor. @@ -138,6 +140,14 @@ export OPENCODE_API_KEY=sk-... deva.sh opencode --auth-with api-key ``` +pi with any provider API key (multi-provider; auth.json outranks env keys, +so api-key mode mounts nothing): + +```bash +export ANTHROPIC_API_KEY=sk-ant-... +deva.sh pi --auth-with api-key +``` + More auth details live in [Authentication Guide](authentication.md). ## Useful Modes diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 0a565b1..e38050e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -84,7 +84,7 @@ If the dry-run shape is correct but the agent still cannot authenticate, the wra Symptom: -- first run warns that `.claude`, `.codex`, `.gemini`, `.grok`, `.kimi-code`, or `.local/share/opencode` is empty +- first run warns that `.claude`, `.codex`, `.gemini`, `.grok`, `.kimi-code`, `.pi`, or `.local/share/opencode` is empty Meaning: diff --git a/install.sh b/install.sh index d5def3d..92cef60 100644 --- a/install.sh +++ b/install.sh @@ -13,6 +13,7 @@ agent_files=( "grok.sh" "kimi.sh" "opencode.sh" + "pi.sh" "shared_auth.sh" ) @@ -144,6 +145,7 @@ echo " - $INSTALL_DIR/agents/gemini.sh" echo " - $INSTALL_DIR/agents/grok.sh" echo " - $INSTALL_DIR/agents/kimi.sh" echo " - $INSTALL_DIR/agents/opencode.sh" +echo " - $INSTALL_DIR/agents/pi.sh" echo " - $INSTALL_DIR/agents/shared_auth.sh" echo "" echo "Quick start:" @@ -156,6 +158,7 @@ echo " deva.sh gemini -- --help" echo " deva.sh grok -- --help" echo " deva.sh kimi -- --help" echo " deva.sh opencode -- --help" +echo " deva.sh pi -- --help" echo " deva.sh shell" echo "" echo "warning: do not point deva at your real home directory with dangerous permissions enabled" diff --git a/llms.txt b/llms.txt index 0e0aefc..ae50a1e 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # deva.sh -> Docker-first launcher for AI coding agents — Claude Code, Codex, Gemini, Grok, Kimi, and opencode. +> Docker-first launcher for AI coding agents — Claude Code, Codex, Gemini, Grok, Kimi, opencode, and pi. > The container is the sandbox, mounts are the explicit contract, and one warm > project container serves all six agents. A bash script, not a framework. MIT. @@ -10,8 +10,8 @@ deva.sh is a single entry point installed on the host; agents run inside are disabled by design — isolation comes from Docker, not permission theater: `claude --dangerously-skip-permissions`, `codex --dangerously-bypass-approvals-and-sandbox`, -`gemini --yolo`, `grok --always-approve`, `kimi --yolo`, and opencode via -`OPENCODE_PERMISSION` allow-all. Auth +`gemini --yolo`, `grok --always-approve`, `kimi --yolo`, opencode via +`OPENCODE_PERMISSION` allow-all, and pi (no permission system by design). Auth lives in per-agent config homes under `~/.config/deva/` with OAuth (default) or API-key modes per agent, switchable per run (`--auth-with`, `--config-home`) so accounts and billing modes swap without touching project or session state; @@ -32,6 +32,7 @@ deva.sh gemini deva.sh grok deva.sh kimi deva.sh opencode +deva.sh pi deva.sh claude -p cloak # stealth-browser image (CloakBrowser, headed Xvfb) deva.sh claude --debug --dry-run # inspect the docker run before trusting it ``` diff --git a/scripts/install-agent-tooling.sh b/scripts/install-agent-tooling.sh index 34978bb..8834cee 100644 --- a/scripts/install-agent-tooling.sh +++ b/scripts/install-agent-tooling.sh @@ -10,6 +10,7 @@ set -euo pipefail : "${GROK_CLI_VERSION:?GROK_CLI_VERSION is required}" : "${KIMI_CODE_VERSION:?KIMI_CODE_VERSION is required}" : "${OPENCODE_VERSION:?OPENCODE_VERSION is required}" +: "${PI_CODING_AGENT_VERSION:?PI_CODING_AGENT_VERSION is required}" CCTRACE_VERSION="${CCTRACE_VERSION:-0.4.0}" CCX_VERSION="${CCX_VERSION:-v0.7.0}" @@ -139,7 +140,7 @@ install_npm_agent_tooling() { log "Installing npm agent tooling" log "Proxy config:" log_proxy_config - log "Requested versions: claude=${CLAUDE_CODE_VERSION} codex=${CODEX_VERSION} gemini=${GEMINI_CLI_VERSION} grok=${GROK_CLI_VERSION} kimi=${KIMI_CODE_VERSION} opencode=${OPENCODE_VERSION}" + log "Requested versions: claude=${CLAUDE_CODE_VERSION} codex=${CODEX_VERSION} gemini=${GEMINI_CLI_VERSION} grok=${GROK_CLI_VERSION} kimi=${KIMI_CODE_VERSION} opencode=${OPENCODE_VERSION} pi=${PI_CODING_AGENT_VERSION}" mkdir -p "$DEVA_HOME/.npm-global" "$DEVA_HOME/.local/bin" # opencode is XDG-native; pre-create its dirs as the deva user so docker @@ -147,6 +148,9 @@ install_npm_agent_tooling() { mkdir -p "$DEVA_HOME/.config/opencode" \ "$DEVA_HOME/.local/share/opencode" \ "$DEVA_HOME/.local/state/opencode" + # pi keeps everything under ~/.pi/agent; pre-create it as the deva + # user for the same bind-mount-parent reason. + mkdir -p "$DEVA_HOME/.pi/agent" npm config set prefix "$DEVA_HOME/.npm-global" check_npm_registry_dns @@ -158,6 +162,7 @@ install_npm_agent_tooling() { "@xai-official/grok@${GROK_CLI_VERSION}" \ "@moonshot-ai/kimi-code@${KIMI_CODE_VERSION}" \ "opencode-ai@${OPENCODE_VERSION}" \ + "@earendil-works/pi-coding-agent@${PI_CODING_AGENT_VERSION}" \ || die "npm install failed" npm cache clean --force @@ -173,7 +178,10 @@ install_npm_agent_tooling() { # opencode's npm bin is a node shim that execs the platform package # binary from node_modules (no self-update dir shadowing) — just verify. "$DEVA_HOME/.npm-global/bin/opencode" --version - (npm list -g --depth=0 @anthropic-ai/claude-code @openai/codex @google/gemini-cli @xai-official/grok @moonshot-ai/kimi-code opencode-ai || true) + # pi phones pi.dev for an update check on startup; skip it so the + # verify works in network-restricted builds. + PI_SKIP_VERSION_CHECK=1 "$DEVA_HOME/.npm-global/bin/pi" --version + (npm list -g --depth=0 @anthropic-ai/claude-code @openai/codex @google/gemini-cli @xai-official/grok @moonshot-ai/kimi-code opencode-ai @earendil-works/pi-coding-agent || true) } # grok's npm postinstall puts the real binary in ~/.grok/bin (the CLI's diff --git a/scripts/release-utils.sh b/scripts/release-utils.sh index 2898c0b..76f6f4d 100755 --- a/scripts/release-utils.sh +++ b/scripts/release-utils.sh @@ -32,6 +32,7 @@ TOOL_REGISTRY=( "grok-cli|npm|@xai-official/grok|org.opencontainers.image.grok_cli_version|https://www.npmjs.com/package/@xai-official/grok||agent|main" "kimi-code|npm|@moonshot-ai/kimi-code|org.opencontainers.image.kimi_code_version|https://www.npmjs.com/package/@moonshot-ai/kimi-code||agent|main" "opencode|npm|opencode-ai|org.opencontainers.image.opencode_version|https://www.npmjs.com/package/opencode-ai|github:anomalyco/opencode|agent|main" + "pi|npm|@earendil-works/pi-coding-agent|org.opencontainers.image.pi_coding_agent_version|https://www.npmjs.com/package/@earendil-works/pi-coding-agent|github:earendil-works/pi|agent|main" "ccx|github-release|thevibeworks/ccx|org.opencontainers.image.ccx_version|https://github.com/thevibeworks/ccx|github:thevibeworks/ccx|agent|main" "copilot-api|github-commit|ericc-ch/copilot-api|org.opencontainers.image.copilot_api_version|https://github.com/ericc-ch/copilot-api||agent|main" "cctrace|npm|@thevibeworks/cctrace|org.opencontainers.image.cctrace_version|https://www.npmjs.com/package/@thevibeworks/cctrace|github:thevibeworks/cctrace|agent|main" diff --git a/scripts/resolve-tool-versions.sh b/scripts/resolve-tool-versions.sh index 35ecba5..4f32cc7 100644 --- a/scripts/resolve-tool-versions.sh +++ b/scripts/resolve-tool-versions.sh @@ -33,6 +33,7 @@ main() { resolve_tool "grok_cli_version" "grok-cli" resolve_tool "kimi_code_version" "kimi-code" resolve_tool "opencode_version" "opencode" + resolve_tool "pi_coding_agent_version" "pi" resolve_tool "ccx_version" "ccx" resolve_tool "copilot_api_version" "copilot-api" } diff --git a/scripts/test-install-agent-tooling.sh b/scripts/test-install-agent-tooling.sh index 59cc473..3a519ba 100755 --- a/scripts/test-install-agent-tooling.sh +++ b/scripts/test-install-agent-tooling.sh @@ -34,7 +34,7 @@ config) ;; install) mkdir -p "$DEVA_HOME/.npm-global/bin" - for bin in claude codex gemini grok kimi opencode; do + for bin in claude codex gemini grok kimi opencode pi; do cat >"$DEVA_HOME/.npm-global/bin/$bin" <<'BIN' #!/usr/bin/env bash case "$(basename "$0")" in @@ -44,6 +44,7 @@ case "$(basename "$0")" in grok) echo "grok __GROK_CLI_VERSION__" ;; kimi) echo "__KIMI_CODE_VERSION__" ;; opencode) echo "__OPENCODE_VERSION__" ;; + pi) echo "__PI_CODING_AGENT_VERSION__" ;; esac BIN chmod +x "$DEVA_HOME/.npm-global/bin/$bin" @@ -88,6 +89,7 @@ sed -i \ -e "s#__GROK_CLI_VERSION__#$GROK_CLI_VERSION#g" \ -e "s#__KIMI_CODE_VERSION__#$KIMI_CODE_VERSION#g" \ -e "s#__OPENCODE_VERSION__#$OPENCODE_VERSION#g" \ + -e "s#__PI_CODING_AGENT_VERSION__#$PI_CODING_AGENT_VERSION#g" \ "$fake_bin/npm" cat >"$fake_bin/curl" <<'EOF' diff --git a/scripts/test-pi-auth.sh b/scripts/test-pi-auth.sh new file mode 100755 index 0000000..8fac89c --- /dev/null +++ b/scripts/test-pi-auth.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# test-pi-auth.sh - pi agent auth wiring (~/.pi mount + provider-key env) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +tmp_home="$(mktemp -d)" +cleanup() { rm -rf "$tmp_home"; } +trap cleanup EXIT + +fail=0 +run_dry() { + ( + cd "$REPO_ROOT" + HOME="$tmp_home" \ + XDG_CONFIG_HOME="$tmp_home/.config" \ + XDG_CACHE_HOME="$tmp_home/.cache" \ + DEVA_NO_DOCKER=1 \ + ./deva.sh "$@" + ) 2>&1 +} + +want() { + local desc="$1" needle="$2" hay="$3" + if grep -F -- "$needle" <<<"$hay" >/dev/null; then + echo " PASS $desc" + else + echo " FAIL $desc" + echo " expected to find: $needle" >&2 + fail=1 + fi +} +want_absent() { + local desc="$1" needle="$2" hay="$3" + if grep -F -- "$needle" <<<"$hay" >/dev/null; then + echo " FAIL $desc" + echo " expected absent: $needle" >&2 + fail=1 + else + echo " PASS $desc" + fi +} + +echo "=== pi oauth (default) ===" +oauth_out="$(run_dry pi --debug --dry-run || true)" +want "runs pi" "pi --approve" "$oauth_out" +want "auth method is oauth" "DEVA_AUTH_METHOD=oauth" "$oauth_out" +want "version check disabled" "PI_SKIP_VERSION_CHECK=1" "$oauth_out" + +echo "=== pi oauth: hybrid config-root mounts ~/.pi ===" +# Seed the config-root layout an autolinked oauth run leaves behind and +# assert the centralized walk (mount_agent_canonical) emits the mount. +mkdir -p "$tmp_home/.config/deva/pi/.pi/agent" +hybrid_oauth_out="$(run_dry pi --dry-run || true)" +want "pi home mounted" ":/home/deva/.pi" "$hybrid_oauth_out" + +echo "=== pi api-key: provider keys as env, no mount ===" +apikey_out="$(ANTHROPIC_API_KEY=sk-ant-test-1234 XAI_API_KEY=xai-test-5678 run_dry pi --auth-with api-key --dry-run -- -p hi || true)" +want "anthropic key wired + redacted" "ANTHROPIC_API_KEY=" "$apikey_out" +want "second provider key travels too" "XAI_API_KEY=" "$apikey_out" +want "first-key last-4 tags container" "--api-key-1234--" "$apikey_out" +want "passes agent args after --" "pi --approve -p hi" "$apikey_out" +want_absent "no ~/.pi mount in api-key mode" ":/home/deva/.pi\"" "$apikey_out" +# ~/.pi itself must not ride in; the blank overlay at auth.json is +# expected (a user -v could still carry a dir with auth.json in it). +want "auth.json blank-overlayed" ".blank:/home/deva/.pi/agent/auth.json" "$apikey_out" + +echo "=== pi api-key: no mount on the hybrid config-root path either ===" +hybrid_apikey_out="$(ANTHROPIC_API_KEY=sk-ant-test-1234 run_dry pi --auth-with api-key --dry-run || true)" +want_absent "hybrid layout: no ~/.pi mount in api-key mode" ":/home/deva/.pi\"" "$hybrid_apikey_out" +rm -rf "$tmp_home/.config/deva/pi" + +echo "=== pi oauth: host provider keys must not leak ===" +leak_out="$(ANTHROPIC_API_KEY=sk-host-leak-9999 run_dry pi -e ANTHROPIC_API_KEY --dry-run || true)" +want_absent "host key filtered in oauth mode" "ANTHROPIC_API_KEY" "$leak_out" + +echo "=== pi --trace: rejected until cctrace ships a profile ===" +trace_out="$(run_dry pi --trace --dry-run || true)" +want "trace rejected" "--trace is not supported for pi" "$trace_out" + +echo "=== pi --trace after -- is passthrough ===" +trace_pass_out="$(run_dry pi --dry-run -- --trace || true)" +want_absent "--trace after -- not absorbed" "--trace is not supported" "$trace_pass_out" +want "--trace passed to agent" "pi --approve --trace" "$trace_pass_out" + +echo "=== pi api-key: missing key errors ===" +missing_out="$(ANTHROPIC_API_KEY= OPENAI_API_KEY= GEMINI_API_KEY= XAI_API_KEY= OPENROUTER_API_KEY= run_dry pi --auth-with api-key --dry-run || true)" +want "errors when no key set" "no provider API key set" "$missing_out" + +if [ "$fail" -ne 0 ]; then + echo "FAIL: pi auth wiring" >&2 + exit 1 +fi +echo "OK: pi auth wiring" diff --git a/scripts/update-version-pins.sh b/scripts/update-version-pins.sh index 93ecdca..71d2370 100755 --- a/scripts/update-version-pins.sh +++ b/scripts/update-version-pins.sh @@ -134,6 +134,7 @@ registry_tool() { GROK_CLI_VERSION) echo "grok-cli" ;; KIMI_CODE_VERSION) echo "kimi-code" ;; OPENCODE_VERSION) echo "opencode" ;; + PI_CODING_AGENT_VERSION) echo "pi" ;; CCX_VERSION) echo "ccx" ;; COPILOT_API_VERSION) echo "copilot-api" ;; PLAYWRIGHT_VERSION) echo "playwright" ;; @@ -230,6 +231,7 @@ main() { pin "Grok CLI" GROK_CLI_VERSION npm "@xai-official/grok" pin "Kimi Code" KIMI_CODE_VERSION npm "@moonshot-ai/kimi-code" pin "opencode" OPENCODE_VERSION npm "opencode-ai" + pin "pi" PI_CODING_AGENT_VERSION npm "@earendil-works/pi-coding-agent" pin "CCX" CCX_VERSION git-tag "https://github.com/thevibeworks/ccx.git" pin "Copilot API" COPILOT_API_VERSION git-commit "https://github.com/ericc-ch/copilot-api.git" "refs/heads/master" diff --git a/scripts/version-pins.sh b/scripts/version-pins.sh index 6b6d5a1..13a927b 100644 --- a/scripts/version-pins.sh +++ b/scripts/version-pins.sh @@ -20,6 +20,7 @@ VERSION_PIN_VARS=( GROK_CLI_VERSION KIMI_CODE_VERSION OPENCODE_VERSION + PI_CODING_AGENT_VERSION CCX_VERSION COPILOT_API_VERSION PLAYWRIGHT_VERSION @@ -87,6 +88,7 @@ GEMINI_CLI_VERSION=$GEMINI_CLI_VERSION GROK_CLI_VERSION=$GROK_CLI_VERSION KIMI_CODE_VERSION=$KIMI_CODE_VERSION OPENCODE_VERSION=$OPENCODE_VERSION +PI_CODING_AGENT_VERSION=$PI_CODING_AGENT_VERSION CCX_VERSION=$CCX_VERSION COPILOT_API_VERSION=$COPILOT_API_VERSION PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION diff --git a/scripts/version-upgrade.sh b/scripts/version-upgrade.sh index af4548c..a9714d8 100755 --- a/scripts/version-upgrade.sh +++ b/scripts/version-upgrade.sh @@ -19,6 +19,7 @@ _CLI_GEMINI="${GEMINI_CLI_VERSION:-}" _CLI_GROK="${GROK_CLI_VERSION:-}" _CLI_KIMI="${KIMI_CODE_VERSION:-}" _CLI_OPENCODE="${OPENCODE_VERSION:-}" +_CLI_PI="${PI_CODING_AGENT_VERSION:-}" _CLI_CCX="${CCX_VERSION:-}" _CLI_COPILOT="${COPILOT_API_VERSION:-}" _CLI_PLAYWRIGHT="${PLAYWRIGHT_VERSION:-}" @@ -72,7 +73,7 @@ Options: --only LIST Upgrade only these tools (comma-separated); the rest stay pinned to versions.env. Tools: claude-code, cctrace, codex, gemini-cli, grok-cli, kimi-code, - opencode, ccx, copilot-api, playwright, cloakbrowser, + opencode, pi, ccx, copilot-api, playwright, cloakbrowser, kimi-webbridge -h, --help Show this help @@ -91,6 +92,7 @@ Environment: GROK_CLI_VERSION Override grok-cli version KIMI_CODE_VERSION Override kimi-code version OPENCODE_VERSION Override opencode version + PI_CODING_AGENT_VERSION Override pi version CCX_VERSION Override ccx version COPILOT_API_VERSION Override copilot-api version PLAYWRIGHT_VERSION Override playwright version (rust image only) @@ -121,7 +123,7 @@ apply_only_filter() { [[ -n $ONLY ]] || return 0 local tool - local known="claude-code cctrace codex gemini-cli grok-cli kimi-code opencode ccx copilot-api playwright cloakbrowser kimi-webbridge" + local known="claude-code cctrace codex gemini-cli grok-cli kimi-code opencode pi ccx copilot-api playwright cloakbrowser kimi-webbridge" for tool in ${ONLY//,/ }; do case " $known " in *" $tool "*) ;; @@ -138,6 +140,7 @@ apply_only_filter() { tool_selected grok-cli || _CLI_GROK="${_CLI_GROK:-$GROK_CLI_VERSION}" tool_selected kimi-code || _CLI_KIMI="${_CLI_KIMI:-$KIMI_CODE_VERSION}" tool_selected opencode || _CLI_OPENCODE="${_CLI_OPENCODE:-$OPENCODE_VERSION}" + tool_selected pi || _CLI_PI="${_CLI_PI:-$PI_CODING_AGENT_VERSION}" tool_selected ccx || _CLI_CCX="${_CLI_CCX:-$CCX_VERSION}" tool_selected copilot-api || _CLI_COPILOT="${_CLI_COPILOT:-$COPILOT_API_VERSION}" tool_selected playwright || _CLI_PLAYWRIGHT="${_CLI_PLAYWRIGHT:-$PLAYWRIGHT_VERSION}" @@ -230,7 +233,7 @@ main() { # Resolve build versions early so we can show the manifest before countdown. # CLI override wins; otherwise use whatever load_versions fetched. - local claude_ver cctrace_ver codex_ver gemini_ver grok_ver kimi_ver opencode_ver ccx_ver copilot_ver playwright_ver + local claude_ver cctrace_ver codex_ver gemini_ver grok_ver kimi_ver opencode_ver pi_ver ccx_ver copilot_ver playwright_ver claude_ver="${_CLI_CLAUDE_CODE:-$(get_latest "claude-code")}" cctrace_ver="${_CLI_CCTRACE:-$(get_latest "cctrace")}" codex_ver="${_CLI_CODEX:-$(get_latest "codex")}" @@ -238,6 +241,7 @@ main() { grok_ver="${_CLI_GROK:-$(get_latest "grok-cli")}" kimi_ver="${_CLI_KIMI:-$(get_latest "kimi-code")}" opencode_ver="${_CLI_OPENCODE:-$(get_latest "opencode")}" + pi_ver="${_CLI_PI:-$(get_latest "pi")}" ccx_ver="${_CLI_CCX:-$(get_latest "ccx")}" copilot_ver="${_CLI_COPILOT:-$(get_latest "copilot-api")}" playwright_ver="${_CLI_PLAYWRIGHT:-${PLAYWRIGHT_VERSION}}" @@ -249,6 +253,7 @@ main() { [[ -z $grok_ver ]] && missing+=("GROK_CLI_VERSION") [[ -z $kimi_ver ]] && missing+=("KIMI_CODE_VERSION") [[ -z $opencode_ver ]] && missing+=("OPENCODE_VERSION") + [[ -z $pi_ver ]] && missing+=("PI_CODING_AGENT_VERSION") [[ -z $ccx_ver ]] && missing+=("CCX_VERSION") [[ -z $copilot_ver ]] && missing+=("COPILOT_API_VERSION") [[ -z $playwright_ver ]] && missing+=("PLAYWRIGHT_VERSION") @@ -268,6 +273,7 @@ main() { "Grok CLI|grok_ver|_CLI_GROK|grok-cli" "Kimi Code|kimi_ver|_CLI_KIMI|kimi-code" "opencode|opencode_ver|_CLI_OPENCODE|opencode" + "pi|pi_ver|_CLI_PI|pi" "CCX|ccx_ver|_CLI_CCX|ccx" "Copilot API|copilot_ver|_CLI_COPILOT|copilot-api" "Playwright|playwright_ver|_CLI_PLAYWRIGHT|playwright" @@ -397,6 +403,7 @@ main() { --build-arg GROK_CLI_VERSION="$grok_ver" \ --build-arg KIMI_CODE_VERSION="$kimi_ver" \ --build-arg OPENCODE_VERSION="$opencode_ver" \ + --build-arg PI_CODING_AGENT_VERSION="$pi_ver" \ --build-arg CCX_VERSION="$ccx_ver" \ --build-arg COPILOT_API_VERSION="$copilot_ver" \ -t "$BUILD_IMAGE" . @@ -413,6 +420,7 @@ main() { --build-arg GROK_CLI_VERSION="$grok_ver" \ --build-arg KIMI_CODE_VERSION="$kimi_ver" \ --build-arg OPENCODE_VERSION="$opencode_ver" \ + --build-arg PI_CODING_AGENT_VERSION="$pi_ver" \ --build-arg CCX_VERSION="$ccx_ver" \ --build-arg PLAYWRIGHT_VERSION="$playwright_ver" \ --build-arg RUST_TOOLCHAINS="$RUST_TOOLCHAINS" \ @@ -443,6 +451,7 @@ main() { GROK_CLI_VERSION="$grok_ver" KIMI_CODE_VERSION="$kimi_ver" OPENCODE_VERSION="$opencode_ver" + PI_CODING_AGENT_VERSION="$pi_ver" CCX_VERSION="$ccx_ver" COPILOT_API_VERSION="$copilot_ver" PLAYWRIGHT_VERSION="$playwright_ver" diff --git a/tests/test_release_utils.sh b/tests/test_release_utils.sh index 08d8400..88d503c 100644 --- a/tests/test_release_utils.sh +++ b/tests/test_release_utils.sh @@ -129,7 +129,7 @@ assert_eq "caller 'image' not clobbered" "caller-image" "${leak_after##*|}" # ───── get_tools_by_group ───── section "get_tools_by_group" agent_tools="$(get_tools_by_group agent | sort | tr '\n' ' ' | sed 's/ $//')" -expected_agent="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli kimi-code opencode" +expected_agent="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli kimi-code opencode pi" assert_eq "group=agent" "$expected_agent" "$agent_tools" browser_tools="$(get_tools_by_group browser | sort | tr '\n' ' ' | sed 's/ $//')" @@ -144,7 +144,7 @@ assert_eq "group=does-not-exist" "" "$nonexistent_group" # ───── get_tools_by_image ───── section "get_tools_by_image" main_tools="$(get_tools_by_image main | sort | tr '\n' ' ' | sed 's/ $//')" -expected_main="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli kimi-code opencode" +expected_main="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli kimi-code opencode pi" assert_eq "image=main" "$expected_main" "$main_tools" rust_tools="$(get_tools_by_image rust | sort | tr '\n' ' ' | sed 's/ $//')" @@ -156,8 +156,8 @@ assert_eq "image=base (empty until Step 3)" "" "$base_tools" # ───── filter_tools: scope resolution ───── section "filter_tools: default (no scope)" default_all="$(unset GROUP TOOL IMAGE; filter_tools | sort | tr '\n' ' ' | sed 's/ $//')" -expected_all="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli kimi-code opencode playwright" -assert_eq "default returns all 10" "$expected_all" "$default_all" +expected_all="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli kimi-code opencode pi playwright" +assert_eq "default returns all 11" "$expected_all" "$default_all" section "filter_tools: TOOL= scope" single="$(TOOL=claude-code GROUP= IMAGE= filter_tools)" diff --git a/tests/version-upgrade.sh b/tests/version-upgrade.sh index 023c0f7..456bd7d 100644 --- a/tests/version-upgrade.sh +++ b/tests/version-upgrade.sh @@ -29,6 +29,7 @@ inspect) "org.opencontainers.image.grok_cli_version":"0.2.90", "org.opencontainers.image.kimi_code_version":"0.28.0", "org.opencontainers.image.opencode_version":"1.18.14", + "org.opencontainers.image.pi_coding_agent_version":"0.84.0", "org.opencontainers.image.ccx_version":"v0.7.0", "org.opencontainers.image.copilot_api_version":"0ea08febdd7e3e055b03dd298bf57e669500b5c1", "org.opencontainers.image.playwright_version":"1.59.0" @@ -107,6 +108,7 @@ case "$url" in */-/package/@xai-official/grok/dist-tags) echo '{"latest":"0.2.93"}' ;; */-/package/@moonshot-ai/kimi-code/dist-tags) echo '{"latest":"0.28.0"}' ;; */-/package/opencode-ai/dist-tags) echo '{"latest":"1.18.14"}' ;; +*/-/package/@earendil-works/pi-coding-agent/dist-tags) echo '{"latest":"0.84.1"}' ;; */-/package/playwright/dist-tags) echo '{"latest":"1.60.0"}' ;; */-/package/cloakbrowser/dist-tags) echo '{"latest":"0.6.0"}' ;; *cdn.kimi.com/webbridge/latest/version.json) echo '{"version":"v1.12.0","binaries":{}}' ;; @@ -117,6 +119,7 @@ case "$url" in *registry.npmjs.org/@xai-official%2fgrok) echo '{"time":{"0.2.93":"2026-07-01T00:00:00Z"}}' ;; *registry.npmjs.org/@moonshot-ai%2fkimi-code) echo '{"time":{"0.28.0":"2026-07-10T00:00:00Z"}}' ;; *registry.npmjs.org/opencode-ai) echo '{"time":{"1.18.14":"2026-08-01T00:00:00Z"}}' ;; +*registry.npmjs.org/@earendil-works%2fpi-coding-agent) echo '{"time":{"0.84.1":"2026-08-07T00:00:00Z"}}' ;; *registry.npmjs.org/playwright) echo '{"time":{"1.60.0":"2026-05-14T08:00:00Z"}}' ;; *) echo "unexpected curl url: $url" >&2 @@ -178,6 +181,7 @@ for expected in \ "--build-arg GEMINI_CLI_VERSION=0.35.3" \ "--build-arg GROK_CLI_VERSION=0.2.93" \ "--build-arg KIMI_CODE_VERSION=0.28.0" \ + "--build-arg PI_CODING_AGENT_VERSION=0.84.1" \ "--build-arg CCX_VERSION=v0.7.0" \ "--build-arg COPILOT_API_VERSION=0ea08febdd7e3e055b03dd298bf57e669500b5c1" \ "--build-arg GO_VERSION=1.26.2" @@ -196,6 +200,7 @@ for expected in \ "--build-arg GEMINI_CLI_VERSION=0.35.3" \ "--build-arg GROK_CLI_VERSION=0.2.93" \ "--build-arg KIMI_CODE_VERSION=0.28.0" \ + "--build-arg PI_CODING_AGENT_VERSION=0.84.1" \ "--build-arg CCX_VERSION=v0.7.0" \ "--build-arg PLAYWRIGHT_VERSION=1.60.0" do @@ -224,6 +229,7 @@ for expected in \ "GEMINI_CLI_VERSION=0.35.3" \ "GROK_CLI_VERSION=0.2.93" \ "KIMI_CODE_VERSION=0.28.0" \ + "PI_CODING_AGENT_VERSION=0.84.1" \ "CCX_VERSION=v0.7.0" \ "COPILOT_API_VERSION=0ea08febdd7e3e055b03dd298bf57e669500b5c1" \ "PLAYWRIGHT_VERSION=1.60.0" \ @@ -367,6 +373,7 @@ case "$url" in */-/package/@xai-official/grok/dist-tags) echo '{"latest":"0.2.93"}' ;; */-/package/@moonshot-ai/kimi-code/dist-tags) echo '{"latest":"0.28.0"}' ;; */-/package/opencode-ai/dist-tags) echo '{"latest":"1.18.14"}' ;; +*/-/package/@earendil-works/pi-coding-agent/dist-tags) echo '{"latest":"0.84.1"}' ;; */-/package/playwright/dist-tags) echo '{"latest":"1.60.0"}' ;; */-/package/cloakbrowser/dist-tags) echo '{"latest":"0.6.0"}' ;; *cdn.kimi.com/webbridge/latest/version.json) echo '{"version":"v1.12.0","binaries":{}}' ;; diff --git a/versions.env b/versions.env index 104302a..14153ed 100644 --- a/versions.env +++ b/versions.env @@ -15,6 +15,7 @@ GEMINI_CLI_VERSION=0.54.4 GROK_CLI_VERSION=1.0.0 KIMI_CODE_VERSION=0.34.0 OPENCODE_VERSION=1.18.15 +PI_CODING_AGENT_VERSION=0.84.1 CCX_VERSION=v0.14.0 COPILOT_API_VERSION=0ea08febdd7e3e055b03dd298bf57e669500b5c1 PLAYWRIGHT_VERSION=1.62.1