diff --git a/.deva.example b/.deva.example index 8016826..027708c 100644 --- a/.deva.example +++ b/.deva.example @@ -61,7 +61,7 @@ EPHEMERAL=false # Hybrid Agent Setup: # # Hybrid is the DEFAULT. deva walks every populated subdir under -# ~/.config/deva/ (claude, codex, gemini, grok, kimi, opencode, pi, dsh) and mounts each agent's +# ~/.config/deva/ (claude, codex, gemini, grok, kimi, opencode, pi, dsh, cursor) 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, ~/.pi, ~/.dsh, or the diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34e080e..d0dd84c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,7 @@ jobs: --build-arg OPENCODE_VERSION="${{ steps.pins.outputs.opencode_version }}" \ --build-arg PI_CODING_AGENT_VERSION="${{ steps.pins.outputs.pi_coding_agent_version }}" \ --build-arg DSH_VERSION="${{ steps.pins.outputs.dsh_version }}" \ + --build-arg CURSOR_CLI_VERSION="${{ steps.pins.outputs.cursor_cli_version }}" \ --build-arg CCX_VERSION="${{ steps.pins.outputs.ccx_version }}" \ --build-arg COPILOT_API_VERSION="${{ steps.pins.outputs.copilot_api_version }}" \ . @@ -113,6 +114,7 @@ jobs: OPENCODE_VERSION="${{ steps.pins.outputs.opencode_version }}" \ PI_CODING_AGENT_VERSION="${{ steps.pins.outputs.pi_coding_agent_version }}" \ DSH_VERSION="${{ steps.pins.outputs.dsh_version }}" \ + CURSOR_CLI_VERSION="${{ steps.pins.outputs.cursor_cli_version }}" \ CCX_VERSION="${{ steps.pins.outputs.ccx_version }}" \ COPILOT_API_VERSION="${{ steps.pins.outputs.copilot_api_version }}" \ PLAYWRIGHT_VERSION="${{ steps.pins.outputs.playwright_version }}" \ @@ -142,6 +144,7 @@ jobs: deva.sh opencode -Q -- --version deva.sh pi -Q -- --version deva.sh dsh -Q -- --version + deva.sh cursor -Q -- --version - name: Smoke Claude --chrome mount assembly shell: bash @@ -219,6 +222,14 @@ jobs: DEVA_DOCKER_TAG=ci \ ./scripts/test-dsh-auth.sh + - name: Smoke cursor auth wiring + shell: bash + run: | + set -euo pipefail + DEVA_DOCKER_IMAGE=deva-smoke \ + DEVA_DOCKER_TAG=ci \ + ./scripts/test-cursor-auth.sh + - name: Smoke version targets shell: bash run: | diff --git a/.github/workflows/nightly-images.yml b/.github/workflows/nightly-images.yml index c298137..24c7e23 100644 --- a/.github/workflows/nightly-images.yml +++ b/.github/workflows/nightly-images.yml @@ -59,6 +59,7 @@ jobs: opencode_version: ${{ steps.versions.outputs.opencode_version }} pi_coding_agent_version: ${{ steps.versions.outputs.pi_coding_agent_version }} dsh_version: ${{ steps.versions.outputs.dsh_version }} + cursor_cli_version: ${{ steps.versions.outputs.cursor_cli_version }} ccx_version: ${{ steps.versions.outputs.ccx_version }} copilot_api_version: ${{ steps.versions.outputs.copilot_api_version }} steps: @@ -90,6 +91,7 @@ jobs: - opencode: \`${{ steps.versions.outputs.opencode_version }}\` - pi: \`${{ steps.versions.outputs.pi_coding_agent_version }}\` - dsh: \`${{ steps.versions.outputs.dsh_version }}\` + - cursor: \`${{ steps.versions.outputs.cursor_cli_version }}\` - ccx: \`${{ steps.versions.outputs.ccx_version }}\` - Copilot API: \`${{ steps.versions.outputs.copilot_api_version }}\` - Stamp: \`${{ steps.versions.outputs.stamp }}\` @@ -155,6 +157,7 @@ jobs: OPENCODE_VERSION=${{ needs.resolve-versions.outputs.opencode_version }} PI_CODING_AGENT_VERSION=${{ needs.resolve-versions.outputs.pi_coding_agent_version }} DSH_VERSION=${{ needs.resolve-versions.outputs.dsh_version }} + CURSOR_CLI_VERSION=${{ needs.resolve-versions.outputs.cursor_cli_version }} CCX_VERSION=${{ needs.resolve-versions.outputs.ccx_version }} COPILOT_API_VERSION=${{ needs.resolve-versions.outputs.copilot_api_version }} @@ -213,6 +216,7 @@ jobs: OPENCODE_VERSION=${{ needs.resolve-versions.outputs.opencode_version }} PI_CODING_AGENT_VERSION=${{ needs.resolve-versions.outputs.pi_coding_agent_version }} DSH_VERSION=${{ needs.resolve-versions.outputs.dsh_version }} + CURSOR_CLI_VERSION=${{ needs.resolve-versions.outputs.cursor_cli_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 8c8466a..0addf89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,6 +55,7 @@ jobs: opencode_version: ${{ steps.pins.outputs.opencode_version }} pi_coding_agent_version: ${{ steps.pins.outputs.pi_coding_agent_version }} dsh_version: ${{ steps.pins.outputs.dsh_version }} + cursor_cli_version: ${{ steps.pins.outputs.cursor_cli_version }} ccx_version: ${{ steps.pins.outputs.ccx_version }} copilot_api_version: ${{ steps.pins.outputs.copilot_api_version }} playwright_version: ${{ steps.pins.outputs.playwright_version }} @@ -95,6 +96,7 @@ jobs: - opencode: \`${{ steps.pins.outputs.opencode_version }}\` - pi: \`${{ steps.pins.outputs.pi_coding_agent_version }}\` - dsh: \`${{ steps.pins.outputs.dsh_version }}\` + - cursor: \`${{ steps.pins.outputs.cursor_cli_version }}\` - ccx: \`${{ steps.pins.outputs.ccx_version }}\` - Copilot API: \`${{ steps.pins.outputs.copilot_api_version }}\` - Playwright: \`${{ steps.pins.outputs.playwright_version }}\` @@ -163,6 +165,7 @@ jobs: OPENCODE_VERSION=${{ needs.load-version-pins.outputs.opencode_version }} PI_CODING_AGENT_VERSION=${{ needs.load-version-pins.outputs.pi_coding_agent_version }} DSH_VERSION=${{ needs.load-version-pins.outputs.dsh_version }} + CURSOR_CLI_VERSION=${{ needs.load-version-pins.outputs.cursor_cli_version }} CCX_VERSION=${{ needs.load-version-pins.outputs.ccx_version }} COPILOT_API_VERSION=${{ needs.load-version-pins.outputs.copilot_api_version }} @@ -223,6 +226,7 @@ jobs: OPENCODE_VERSION=${{ needs.load-version-pins.outputs.opencode_version }} PI_CODING_AGENT_VERSION=${{ needs.load-version-pins.outputs.pi_coding_agent_version }} DSH_VERSION=${{ needs.load-version-pins.outputs.dsh_version }} + CURSOR_CLI_VERSION=${{ needs.load-version-pins.outputs.cursor_cli_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 72390a2..07937e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 scripts/test-help-surface.sh pins the routing (24 asserts) ### Added +- Cursor CLI (cursor-agent) as the 9th agent: `deva.sh cursor` (#557). + Pin story solved without the installer: cursor.com/install hardcodes + the version and has no pin hook, but the download URL is + deterministic, so the image fetches + downloads.cursor.com/lab//linux//… directly + and lays it out installer-style under + ~/.local/share/cursor-agent/versions/; the write bit is then stripped + from that tree, which starves the CLI's silent startup self-update + (updaters fight pins). Version format is YYYY.MM.DD-hash; `make + versions-up`/`versions-pin` resolve latest by parsing the installer + script (new cursor-installer registry type). Default `oauth` keeps + state in the per-agent config home only — deliberately NO host + ~/.cursor autolink or fallback mount: that dir is the Cursor IDE's + state (worktrees, per-project chats), and macOS keeps CLI auth in + the keychain, so there is nothing portable to carry in; first login + runs in-container (`cursor-agent login`, NO_OPEN_BROWSER=1 prints + the URL). Two canonical entries on Linux: `.cursor` (cli-config, + projects) + `.config/cursor` (auth.json — the file store keeps auth + there even when config lands in `.cursor`). `--auth-with api-key` + passes CURSOR_API_KEY env-only, mounts nothing, blank-overlays + auth.json. YOLO via `--force`; only the `cursor-agent` bin is linked + (the official installer also squats `agent` — too generic for a + container with nine CLIs). scripts/test-cursor-auth.sh (16 asserts). + `--trace` rejected until cctrace ships a cursor profile - dsh (deepseek-ai/deepseek-harness, DeepSeek's official agent harness, launched 2026-08-13) as the 8th agent: `deva.sh dsh` (#556). One dot-dir — everything persists under `~/.dsh` (`$DSH_HOME`, pinned by diff --git a/DEV-LOGS.md b/DEV-LOGS.md index 4ed5a0b..2073061 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-13] Dev Log: cursor as the 9th agent #557 +- Why: most-requested absentee (#557). Fits the deva model exactly; the only hard part was pinning a CLI whose installer has no version hook. +- What: agents/cursor.sh (oauth keeps state in config-home only, api-key passes CURSOR_API_KEY env-only + blank-overlays .config/cursor/auth.json), install_cursor_agent in install-agent-tooling.sh (deterministic tarball URL downloads.cursor.com/lab//linux/, installer-style layout, then chmod a-w the versions tree to starve the silent startup self-updater), new cursor-installer type in release-utils/update-version-pins (latest = parse the installer script; date = the version's own YYYY.MM.DD prefix), CURSOR_CLI_VERSION=2026.08.11-e8db854 through the full pin plumbing, deva.sh registration, scripts/test-cursor-auth.sh (16 asserts). +- Result: 9 agents. Decisions worth remembering: no host ~/.cursor autolink ever — that's the IDE's state dir, not a CLI home, and mac CLI auth lives in the keychain so the mount carries nothing portable; only the cursor-agent bin is linked (installer also squats `agent`, too generic here); Linux auth quirk pinned in a comment — auth.json goes to .config/cursor even when cli-config lands in .cursor. test-install-agent-tooling now fakes the cursor tarball via the curl stub. --trace deferred (no cctrace cursor profile). + # [2026-08-13] Dev Log: dsh as the 8th agent #556 - Why: DeepSeek shipped its official harness (deepseek-ai/deepseek-harness) on launch day; developer preview with promised breaking changes is exactly what a container posture is for. - What: agents/dsh.sh (default `credentials` mounts ~/.dsh rw; api-key passes DEEPSEEK_API_KEY, mounts nothing — dsh resolves env BEFORE .credentials.yaml, reverse of pi, so no blank overlay and host-key scrub matters in credentials mode), deva.sh registration (auth tag, dsh_version label, mounts, autolink ~/.dsh, status, env scrub), DSH_PERMISSION_MODE=danger-full-access + DSH_HOME pinned at launch, image pin DSH_VERSION=0.1.0-rc.6 through versions.env/Makefile/scripts/CI, scripts/test-dsh-auth.sh (13 asserts). Traps dodged: DSH_HOME default is ~/.dsh not `~/n` (issue text was a docs misread); DSH_* is scrubbed from project-discovered env by dsh itself, so the permission mode must ride the invoking env — which deva's -e injection is; no updater exists, nothing to neutralize. diff --git a/Dockerfile b/Dockerfile index ac73ddf..706419f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -216,6 +216,7 @@ ARG KIMI_CODE_VERSION=0.28.0 ARG OPENCODE_VERSION=1.18.14 ARG PI_CODING_AGENT_VERSION=0.84.1 ARG DSH_VERSION=0.1.0-rc.6 +ARG CURSOR_CLI_VERSION=2026.08.11-e8db854 # Record key tool versions as labels for quick inspection LABEL org.opencontainers.image.claude_code_version=${CLAUDE_CODE_VERSION} @@ -226,6 +227,7 @@ 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.dsh_version=${DSH_VERSION} +LABEL org.opencontainers.image.cursor_cli_version=${CURSOR_CLI_VERSION} ARG CCX_VERSION=v0.7.0 diff --git a/Dockerfile.rust b/Dockerfile.rust index 9d68540..c754c05 100644 --- a/Dockerfile.rust +++ b/Dockerfile.rust @@ -16,6 +16,7 @@ ARG KIMI_CODE_VERSION=0.28.0 ARG OPENCODE_VERSION=1.18.14 ARG PI_CODING_AGENT_VERSION=0.84.1 ARG DSH_VERSION=0.1.0-rc.6 +ARG CURSOR_CLI_VERSION=2026.08.11-e8db854 ARG CCX_VERSION=v0.7.0 ARG PLAYWRIGHT_VERSION=1.60.0 ARG RUST_TOOLCHAINS="stable" @@ -30,6 +31,7 @@ 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.dsh_version=${DSH_VERSION} +LABEL org.opencontainers.image.cursor_cli_version=${CURSOR_CLI_VERSION} LABEL org.opencontainers.image.ccx_version=${CCX_VERSION} LABEL org.opencontainers.image.playwright_version=${PLAYWRIGHT_VERSION} diff --git a/Makefile b/Makefile index e2b4660..c3e2456 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ KIMI_CODE_VERSION ?= 0.28.0 OPENCODE_VERSION ?= 1.18.14 PI_CODING_AGENT_VERSION ?= 0.84.1 DSH_VERSION ?= 0.1.0-rc.6 +CURSOR_CLI_VERSION ?= 2026.08.11-e8db854 CCX_VERSION ?= v0.1.4 COPILOT_API_VERSION ?= 0ea08febdd7e3e055b03dd298bf57e669500b5c1 PLAYWRIGHT_VERSION ?= 1.60.0 @@ -96,6 +97,7 @@ AGENT_BUILD_ARGS := \ --build-arg OPENCODE_VERSION=$(OPENCODE_VERSION) \ --build-arg PI_CODING_AGENT_VERSION=$(PI_CODING_AGENT_VERSION) \ --build-arg DSH_VERSION=$(DSH_VERSION) \ + --build-arg CURSOR_CLI_VERSION=$(CURSOR_CLI_VERSION) \ --build-arg CCX_VERSION=$(CCX_VERSION) MAIN_BUILD_ARGS := $(TOOLCHAIN_BUILD_ARGS) $(AGENT_BUILD_ARGS) \ @@ -126,6 +128,7 @@ VERSION_QUERY_OVERRIDES := \ $(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 DSH_VERSION)),DSH_VERSION=$(DSH_VERSION)) \ + $(if $(filter command line environment environment\ override override,$(origin CURSOR_CLI_VERSION)),CURSOR_CLI_VERSION=$(CURSOR_CLI_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)) \ @@ -175,10 +178,11 @@ build-main: build-network-check 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); \ prev_dsh=$$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.dsh_version" }}' $(MAIN_IMAGE) 2>/dev/null || true); \ + prev_cursor=$$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.cursor_cli_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"); curP=$$(fmt "$$prev_pi"); curD=$$(fmt "$$prev_dsh"); \ - 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)"); tgtD=$$(fmt "$(DSH_VERSION)"); \ - if [ "$$curC" = "$$tgtC" ] && [ "$$curX" = "$$tgtX" ] && [ "$$curG" = "$$tgtG" ] && [ "$$curK" = "$$tgtK" ] && [ "$$curKi" = "$$tgtKi" ] && [ "$$curO" = "$$tgtO" ] && [ "$$curP" = "$$tgtP" ] && [ "$$curD" = "$$tgtD" ]; 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"); curD=$$(fmt "$$prev_dsh"); curCu=$$(fmt "$$prev_cursor"); \ + 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)"); tgtD=$$(fmt "$(DSH_VERSION)"); tgtCu=$$(fmt "$(CURSOR_CLI_VERSION)"); \ + if [ "$$curC" = "$$tgtC" ] && [ "$$curX" = "$$tgtX" ] && [ "$$curG" = "$$tgtG" ] && [ "$$curK" = "$$tgtK" ] && [ "$$curKi" = "$$tgtKi" ] && [ "$$curO" = "$$tgtO" ] && [ "$$curP" = "$$tgtP" ] && [ "$$curD" = "$$tgtD" ] && [ "$$curCu" = "$$tgtCu" ]; then \ echo "Claude: $$tgtC (no change)"; \ echo "Codex: $$tgtX (no change)"; \ echo "Gemini: $$tgtG (no change)"; \ @@ -187,6 +191,7 @@ build-main: build-network-check echo "opencode: $$tgtO (no change)"; \ echo "pi: $$tgtP (no change)"; \ echo "dsh: $$tgtD (no change)"; \ + echo "cursor: $$tgtCu (no change)"; \ echo "Already up-to-date"; \ else \ if [ "$$curC" = "$$tgtC" ]; then \ @@ -229,6 +234,11 @@ build-main: build-network-check else \ echo "dsh: $$curD -> $$tgtD"; \ fi; \ + if [ "$$curCu" = "$$tgtCu" ]; then \ + echo "cursor: $$tgtCu (no change)"; \ + else \ + echo "cursor: $$curCu -> $$tgtCu"; \ + 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) . @@ -530,6 +540,7 @@ help: @echo " OPENCODE_VERSION opencode CLI version (default: $(OPENCODE_VERSION))" @echo " PI_CODING_AGENT_VERSION pi CLI version (default: $(PI_CODING_AGENT_VERSION))" @echo " DSH_VERSION dsh CLI version (default: $(DSH_VERSION))" + @echo " CURSOR_CLI_VERSION Cursor CLI version (default: $(CURSOR_CLI_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 e00d2a6..52e8305 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, opencode, pi, and dsh inside Docker without pretending the agents' own permission prompts are the thing keeping you safe. +Run Claude Code, Codex, Gemini, Grok, Kimi, opencode, pi, dsh, and Cursor 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 eight 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, dsh via `DSH_PERMISSION_MODE=danger-full-access`). 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 nine 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, dsh via `DSH_PERMISSION_MODE=danger-full-access`, cursor via `--force`). 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. @@ -42,6 +42,7 @@ deva.sh kimi deva.sh opencode deva.sh pi deva.sh dsh +deva.sh cursor 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 eab0d9c..366b398 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、pi、dsh 八个 agent CLI,并且不再假装 agent 自带的权限弹窗是在保护你。 +在 Docker 里跑 Claude Code、Codex、Gemini、Grok、Kimi、opencode、pi、dsh、Cursor 九个 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` 全放行、pi 本身就没有权限系统、dsh 走 `DSH_PERMISSION_MODE=danger-full-access`)。最坏结果随容器一起销毁。 +- 全速跑 agent。权限弹窗存在的理由是爆炸半径等于你的整机;把爆炸半径缩小到一个容器,YOLO 就不再是鲁莽 —— 九个 agent 全部关掉自带权限系统跑(`claude --dangerously-skip-permissions`、`codex --dangerously-bypass-approvals-and-sandbox`、`gemini --yolo`、`grok --always-approve`、`kimi --yolo`、opencode 走 `OPENCODE_PERMISSION` 全放行、pi 本身就没有权限系统、dsh 走 `DSH_PERMISSION_MODE=danger-full-access`、cursor 走 `--force`)。最坏结果随容器一起销毁。 - 厂商代码永远见不到你的主机。agent CLI 是快速迭代、带自动更新的 npm 依赖树;在这里它们出生在镜像里:没有 `~/.ssh`、没有 `~/.aws`、没有 shell 环境变量汤、没有浏览器档案。想收集也无从下手。 - 跨越边界的一切都是显式的。文件靠挂载,密钥靠你传的 env,网络靠你选的姿态 —— 默认 bridge、走你的代理、`--host-net`、或者干脆不联网。你没接的线,agent 就没有。 - 身份是启动参数,不是全局单例。`~/.config/deva/` 下按 agent 分家,`--auth-with` / `--config-home` 切第二个账号或 API-key 计费。按次切账号,项目、会话、容器状态原地不动。 @@ -42,6 +42,7 @@ deva.sh kimi deva.sh opencode deva.sh pi deva.sh dsh +deva.sh cursor deva.sh claude --rm # 一次性容器,用完即扔 deva.sh claude --debug --dry-run # 先看 docker run 长啥样再信它 diff --git a/agents/cursor.sh b/agents/cursor.sh new file mode 100644 index 0000000..ce33510 --- /dev/null +++ b/agents/cursor.sh @@ -0,0 +1,78 @@ +# 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 + +agent_prepare() { + local -a args + if [ $# -gt 0 ]; then + args=("$@") + else + args=() + fi + AGENT_COMMAND=("cursor-agent") + + parse_auth_args "cursor" "${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 cursor yet" \ + "cctrace has no cursor profile" + fi + + # Container is the sandbox: --force ("Run Everything"; --yolo is its + # alias) allows commands unless explicitly denied, same rationale as + # every other agent. The image neutralizes the CLI's silent startup + # self-update by stripping the write bit from its versions dir -- + # updaters fight pins. + AGENT_COMMAND+=("--force") + # Login flow is browser OAuth; in a container there is no browser. + # NO_OPEN_BROWSER makes `cursor-agent login` print the URL instead, + # so first login works over any terminal. Harmless otherwise. + DOCKER_ARGS+=("-e" "NO_OPEN_BROWSER=1") + + AGENT_COMMAND+=("${remaining_args[@]+"${remaining_args[@]}"}") + + setup_cursor_auth "$AUTH_METHOD" +} + +setup_cursor_auth() { + local method="$1" + + case "$method" in + oauth) + AUTH_DETAILS="oauth (config-home cursor)" + # No host ~/.cursor mount, ever -- unlike ~/.pi or ~/.dsh + # that dir is the Cursor IDE's state (worktrees, per-project + # chats, IDE config), not a CLI-only home, and on macOS the + # CLI keeps auth in the keychain anyway so the mount would + # not even carry credentials across. Cursor state lives in + # the per-agent config home (~/.config/deva/cursor): run + # `cursor-agent login` once inside the container (the URL + # prints, NO_OPEN_BROWSER is set) and auth.json persists in + # the mounted config home. -Q bare mode: no mounts at all. + ;; + api-key) + # CURSOR_API_KEY travels as env only. No mount, and the + # default credential path gets a blank overlay: a carried-in + # auth.json could silently bill another account (same + # no-mount contract as grok/kimi/opencode/pi/dsh api-key). + if [ -z "${CURSOR_API_KEY:-}" ]; then + auth_error "CURSOR_API_KEY not set for --auth-with api-key" \ + "Set CURSOR_API_KEY or use oauth mode (default)" + fi + DOCKER_ARGS+=("-e" "CURSOR_API_KEY=${CURSOR_API_KEY}") + AUTH_DETAILS="api-key (CURSOR_API_KEY)" + ;; + *) + auth_error "auth method '$method' not implemented for cursor" + ;; + esac +} diff --git a/agents/shared_auth.sh b/agents/shared_auth.sh index c6de0d7..2626bb7 100644 --- a/agents/shared_auth.sh +++ b/agents/shared_auth.sh @@ -544,6 +544,9 @@ parse_auth_args() { dsh) supported_methods=(credentials api-key) ;; + cursor) + supported_methods=(oauth api-key) + ;; *) auth_error "Unknown agent: $agent_name" ;; @@ -614,6 +617,7 @@ parse_auth_args() { opencode) auth_method="oauth" ;; pi) auth_method="oauth" ;; dsh) auth_method="credentials" ;; + cursor) auth_method="oauth" ;; esac fi diff --git a/deva.sh b/deva.sh index 9b5af9e..2377c92 100755 --- a/deva.sh +++ b/deva.sh @@ -110,14 +110,14 @@ _step() { usage() { cat <<'USAGE' -deva.sh - Docker-based multi-agent launcher (Claude, Codex, Gemini, Grok, Kimi, opencode, pi, dsh) +deva.sh - Docker-based multi-agent launcher (Claude, Codex, Gemini, Grok, Kimi, opencode, pi, dsh, cursor) Usage: deva.sh [flags] [agent] [-- agent-flags] deva.sh [flags] Agents: - claude (default), codex, gemini, grok, kimi, opencode, pi, dsh + claude (default), codex, gemini, grok, kimi, opencode, pi, dsh, cursor Commands: ps List containers (current project; -g for all projects) @@ -269,7 +269,7 @@ USAGE usage_full() { cat <<'USAGE' -deva.sh - Docker-based multi-agent launcher (Claude, Codex, Gemini, Grok, Kimi, opencode, pi, dsh) +deva.sh - Docker-based multi-agent launcher (Claude, Codex, Gemini, Grok, Kimi, opencode, pi, dsh, cursor) Usage: deva.sh [deva flags] [agent] [-- agent-flags] @@ -377,6 +377,7 @@ Examples: 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 dsh # Launch dsh (credentials default; api-key via DEEPSEEK_API_KEY) + deva.sh cursor # Launch cursor (oauth default; api-key via CURSOR_API_KEY) deva.sh claude --rm # Ephemeral: deva-work-myapp-claude-12345 # Container management (current project) @@ -1109,7 +1110,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|pi:oauth|dsh:credentials) + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth|opencode:oauth|pi:oauth|dsh:credentials|cursor:oauth) printf '%s' "auth-default" return ;; @@ -1137,6 +1138,7 @@ generate_auth_tag() { # 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:-}}}}}" ;; dsh) key_val="${DEEPSEEK_API_KEY:-}" ;; + cursor) key_val="${CURSOR_API_KEY:-}" ;; esac if [ -n "$key_val" ] && [ ${#key_val} -ge 4 ]; then printf '%s' "api-key-${key_val: -4}" @@ -1186,6 +1188,7 @@ agent_version_tag() { opencode) label="org.opencontainers.image.opencode_version" ;; pi) label="org.opencontainers.image.pi_coding_agent_version" ;; dsh) label="org.opencontainers.image.dsh_version" ;; + cursor) label="org.opencontainers.image.cursor_cli_version" ;; esac local ver="" @@ -1533,6 +1536,7 @@ categorize_mount() { [[ "$dest" == /home/deva/.gemini* ]] || [[ "$dest" == /home/deva/.grok* ]] || \ [[ "$dest" == /home/deva/.kimi-code* ]] || [[ "$dest" == /home/deva/.pi* ]] || \ [[ "$dest" == /home/deva/.dsh* ]] || \ + [[ "$dest" == /home/deva/.cursor* ]] || [[ "$dest" == /home/deva/.config/cursor* ]] || \ [[ "$dest" == /home/deva/.config/opencode* ]] || \ [[ "$dest" == /home/deva/.local/share/opencode* ]] || [[ "$dest" == /home/deva/.local/state/opencode* ]] || \ [ "$dest" = "/home/deva/.agents" ]; then @@ -1918,7 +1922,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 pi dsh; do + for agent_name in claude codex gemini grok kimi opencode pi dsh cursor; do local agent_dir="$config_root/$agent_name" if [ -d "$agent_dir" ]; then local canonical="" other_count=0 entry is_canonical @@ -2509,6 +2513,16 @@ should_skip_env_for_auth() { ;; esac ;; + cursor) + # The key only travels when deva injects it (api-key mode); a + # host key or token leaking into oauth mode would enable API + # billing beside the subscription. + case "$name" in + CURSOR_API_KEY | CURSOR_AUTH_TOKEN) + return 0 + ;; + esac + ;; esac return 1 @@ -2574,6 +2588,10 @@ agent_canonical_basenames() { opencode) printf '%s\n' '.config/opencode' '.local/share/opencode' '.local/state/opencode' ;; pi) printf '%s\n' '.pi' ;; dsh) printf '%s\n' '.dsh' ;; + # Two homes on Linux: cli-config/data in .cursor, auth.json in + # .config/cursor (the CLI's file store ignores CURSOR_CONFIG_DIR + # for auth). Both mount rw. + cursor) printf '%s\n' '.cursor' '.config/cursor' ;; *) return 0 ;; esac } @@ -2617,6 +2635,13 @@ dsh_api_key_no_mount() { [ "$ACTIVE_AGENT" = "dsh" ] && [ "${AUTH_METHOD:-}" = "api-key" ] } +# cursor api-key contract: CURSOR_API_KEY travels as env only. A mounted +# config home carries auth.json, which could silently bill another +# account, so api-key mounts nothing and blank-overlays auth.json. +cursor_api_key_no_mount() { + [ "$ACTIVE_AGENT" = "cursor" ] && [ "${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 @@ -2663,6 +2688,9 @@ mount_agent_canonical() { if [ "$agent" = "dsh" ] && dsh_api_key_no_mount; then return 0 fi + if [ "$agent" = "cursor" ] && cursor_api_key_no_mount; then + return 0 + fi local entry src while IFS= read -r entry; do @@ -2719,7 +2747,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|pi:oauth|dsh:credentials) ;; + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth|opencode:oauth|pi:oauth|dsh:credentials|cursor:oauth) ;; *) return 0 ;; esac fi @@ -2735,6 +2763,7 @@ has_auth_override() { opencode) auth_vars="OPENCODE_API_KEY" ;; pi) auth_vars="ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY XAI_API_KEY OPENROUTER_API_KEY" ;; dsh) auth_vars="DEEPSEEK_API_KEY" ;; + cursor) auth_vars="CURSOR_API_KEY" ;; esac local var @@ -2793,6 +2822,11 @@ default_credential_target_path() { # outranks env provider keys, so blank-overlay it anyway. printf '%s' "/home/deva/.pi/agent/auth.json" ;; + cursor) + # Linux file store: auth.json under .config/cursor. api-key + # mounts nothing, but a user -v could carry a dir in. + printf '%s' "/home/deva/.config/cursor/auth.json" + ;; *) return 1 ;; @@ -4151,7 +4185,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" ] || [ -d "$CONFIG_HOME/pi" ] || [ -d "$CONFIG_HOME/dsh" ]; 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" ] || [ -d "$CONFIG_HOME/dsh" ] || [ -d "$CONFIG_HOME/cursor" ]; then CONFIG_ROOT="$CONFIG_HOME" CONFIG_HOME="" CONFIG_HOME_AUTO=false @@ -4247,6 +4281,15 @@ autolink_legacy_into_deva_root() { [ -d "$CONFIG_ROOT/dsh/.dsh" ] || [ -L "$CONFIG_ROOT/dsh/.dsh" ] || mkdir -p "$CONFIG_ROOT/dsh/.dsh" fi + # cursor: no autolink on purpose. Host ~/.cursor is the Cursor IDE's + # state dir (worktrees, per-project chats), not a CLI-only home, and + # macOS keeps CLI auth in the keychain -- there is nothing portable + # to carry in. Scaffold empty homes; first login happens in-container. + if [ -d "$CONFIG_ROOT" ]; then + [ -d "$CONFIG_ROOT/cursor/.cursor" ] || [ -L "$CONFIG_ROOT/cursor/.cursor" ] || mkdir -p "$CONFIG_ROOT/cursor/.cursor" + [ -d "$CONFIG_ROOT/cursor/.config/cursor" ] || [ -L "$CONFIG_ROOT/cursor/.config/cursor" ] || mkdir -p "$CONFIG_ROOT/cursor/.config/cursor" + fi + # opencode: XDG-native, so the legacy homes and the links are nested. local _oc_entry while IFS= read -r _oc_entry; do @@ -4305,6 +4348,10 @@ if [ -n "$CONFIG_HOME" ] && [ "$DRY_RUN" != true ]; then dsh) [ -d "$CONFIG_HOME/.dsh" ] || mkdir -p "$CONFIG_HOME/.dsh" ;; + cursor) + [ -d "$CONFIG_HOME/.cursor" ] || mkdir -p "$CONFIG_HOME/.cursor" + [ -d "$CONFIG_HOME/.config/cursor" ] || mkdir -p "$CONFIG_HOME/.config/cursor" + ;; esac fi @@ -4368,6 +4415,12 @@ if [ "$CONFIG_HOME_FROM_CLI" = true ] && [ -n "$CONFIG_HOME" ] && [ "$_config_ho echo "warning: $CONFIG_HOME/.dsh has no .credentials.yaml; authentication will need to be set up" >&2 fi ;; + cursor) + # auth.json under .config/cursor is the one that matters. + if [ ! -f "$CONFIG_HOME/.config/cursor/auth.json" ]; then + echo "warning: $CONFIG_HOME/.config/cursor has no auth.json; run 'cursor-agent login' inside the container" >&2 + fi + ;; esac fi @@ -4406,7 +4459,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|pi:oauth|dsh:credentials) ;; + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth|opencode:oauth|pi:oauth|dsh:credentials|cursor:oauth) ;; *) _needs_rewrite=true ;; esac @@ -4541,6 +4594,8 @@ else if [ -d "$HOME/.dsh" ] && ! dsh_api_key_no_mount; then DOCKER_ARGS+=("-v" "$HOME/.dsh:/home/deva/.dsh") fi + # cursor: deliberately NO legacy ~/.cursor fallback -- that dir + # is the Cursor IDE's state, not a CLI home (see autolink note). fi fi _step "mount dispatch: done" diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 5a7ba4c..d5934c5 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -52,7 +52,8 @@ Deva-root layout: ├── kimi/ ├── opencode/ ├── pi/ -└── dsh/ +├── dsh/ +└── cursor/ ``` ```bash @@ -190,9 +191,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, pi, and dsh have -no cctrace profile yet, so `--trace` on those agents is rejected -(thevibeworks/cctrace#89). +(cctrace >= 0.11) and always run MITM capture. opencode, pi, dsh, and +cursor have no cctrace profile yet, so `--trace` on those agents is +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 7fd98af..f117eba 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -25,6 +25,7 @@ This guide documents what `deva.sh` actually supports, what env vars it reads, a | 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`, ...) | | dsh | `credentials` | `api-key` | `.dsh/.credentials.yaml`, `DEEPSEEK_API_KEY` | +| cursor | `oauth` | `api-key` | `.config/cursor/auth.json` (in-container login), `CURSOR_API_KEY` | ## Claude @@ -543,6 +544,46 @@ Held back on purpose: dsh's plugin/marketplace surface is in flux (developer preview; the manifest format already broke pre-launch), so deva wires the TUI + skills dirs only. +## Cursor + +### Default: `--auth-with oauth` + +Cursor state lives in the per-agent config home only +(`~/.config/deva/cursor`): two canonical entries, `.cursor` (cli-config, +per-project chats) and `.config/cursor` (auth.json — the CLI's Linux +file store keeps auth there even when config lands in `.cursor`). + +Unlike every other agent there is NO host-dir autolink and no legacy +`~/.cursor` fallback mount: host `~/.cursor` is the Cursor IDE's state +dir (worktrees, per-project chats), not a CLI-only home, and macOS keeps +CLI auth in the keychain — there is nothing portable to carry in. + +First login happens in the container: run `cursor-agent login` — deva +sets `NO_OPEN_BROWSER=1` so the URL prints instead of trying to open a +browser; open it anywhere, and auth.json persists in the mounted config +home. + +YOLO is `--force` ("Run Everything"; `--yolo` is Cursor's own alias). +The image pins the CLI (`CURSOR_CLI_VERSION`, fetched straight from the +deterministic tarball URL — the installer script has no pin hook) and +strips the write bit from the CLI's versions dir, which starves the +silent startup self-update. + +### `--auth-with api-key` + +Input (required): `CURSOR_API_KEY` + +The key travels as env only; this mode mounts nothing, and a blank +overlay hides `auth.json` even if a user `-v` carries a config dir in. + +```bash +export CURSOR_API_KEY=key_... +deva.sh cursor --auth-with api-key -- -p "fix CI" +``` + +Headless mode (`-p/--print`, `--output-format json|stream-json`) is +Cursor'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 773d5d5..364d15f 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -66,13 +66,14 @@ Default per-agent homes live under: ├── kimi/ ├── opencode/ ├── pi/ -└── dsh/ +├── dsh/ +└── cursor/ ``` `--config-home` supports two layouts: -- leaf home: `DIR/.claude`, `DIR/.claude.json`, `DIR/.codex`, `DIR/.gemini`, `DIR/.grok`, `DIR/.kimi-code`, `DIR/.pi`, `DIR/.dsh`, 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`, `DIR/dsh` +- leaf home: `DIR/.claude`, `DIR/.claude.json`, `DIR/.codex`, `DIR/.gemini`, `DIR/.grok`, `DIR/.kimi-code`, `DIR/.pi`, `DIR/.dsh`, cursor's nested `DIR/.cursor` + `DIR/.config/cursor`, 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`, `DIR/dsh`, `DIR/cursor` `-Q` disables config-home resolution, autolink, and host config mounts entirely. @@ -91,6 +92,7 @@ Examples: - 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 - dsh default: `.dsh` with `.credentials.yaml` at its top (no login flow); api-key passes `DEEPSEEK_API_KEY`, mounts nothing — dsh reads env before the credentials file, so no overlay is needed +- cursor default: `.cursor` + `.config/cursor` with `auth.json` under the latter (in-container `cursor-agent login`; no host autolink — host `~/.cursor` is the IDE's state dir); api-key passes `CURSOR_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. @@ -122,7 +124,7 @@ Persistent is default: - one default container shape per project - reused across runs -- same workspace can run Claude, Codex, Gemini, Grok, Kimi, opencode, pi, and dsh in the same container when mounts, config, and auth line up +- same workspace can run Claude, Codex, Gemini, Grok, Kimi, opencode, pi, dsh, and cursor 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 bf10ae8..70d00b5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # deva.sh -Run Codex, Claude Code, Gemini, Grok, Kimi, opencode, pi, and dsh inside Docker without pretending +Run Codex, Claude Code, Gemini, Grok, Kimi, opencode, pi, dsh, and Cursor 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, opencode, pi, and dsh +- a Docker-based launcher for Codex, Claude, Gemini, Grok, Kimi, opencode, pi, dsh, and cursor - 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 998daa3..35965da 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, opencode, pi, and dsh +- fast switching between Claude, Codex, Gemini, Grok, Kimi, opencode, pi, dsh, and cursor `--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 a11b312..fbe2281 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -31,6 +31,7 @@ That installs: - `agents/opencode.sh` - `agents/pi.sh` - `agents/dsh.sh` +- `agents/cursor.sh` - `agents/shared_auth.sh` It also pulls `ghcr.io/thevibeworks/deva:latest`, with Docker Hub as fallback. @@ -83,6 +84,7 @@ deva.sh kimi deva.sh opencode deva.sh pi deva.sh dsh +deva.sh cursor ``` That is one of the main reasons this wrapper exists. You do not need a separate pet workflow for every vendor. @@ -158,6 +160,14 @@ export DEEPSEEK_API_KEY=sk-... deva.sh dsh --auth-with api-key ``` +cursor with an API key (oauth default logs in inside the container — +`cursor-agent login` prints the URL): + +```bash +export CURSOR_API_KEY=key_... +deva.sh cursor --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 eedb973..6cad378 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`, `.pi`, `.dsh`, or `.local/share/opencode` is empty +- first run warns that `.claude`, `.codex`, `.gemini`, `.grok`, `.kimi-code`, `.pi`, `.dsh`, `.config/cursor`, or `.local/share/opencode` is empty Meaning: diff --git a/install.sh b/install.sh index 96bdf80..3796f23 100644 --- a/install.sh +++ b/install.sh @@ -15,6 +15,7 @@ agent_files=( "opencode.sh" "pi.sh" "dsh.sh" + "cursor.sh" "shared_auth.sh" ) @@ -148,6 +149,7 @@ echo " - $INSTALL_DIR/agents/kimi.sh" echo " - $INSTALL_DIR/agents/opencode.sh" echo " - $INSTALL_DIR/agents/pi.sh" echo " - $INSTALL_DIR/agents/dsh.sh" +echo " - $INSTALL_DIR/agents/cursor.sh" echo " - $INSTALL_DIR/agents/shared_auth.sh" echo "" echo "Quick start:" @@ -162,6 +164,7 @@ echo " deva.sh kimi -- --help" echo " deva.sh opencode -- --help" echo " deva.sh pi -- --help" echo " deva.sh dsh -- --help" +echo " deva.sh cursor -- --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 f44a96f..afceac7 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, opencode, pi, and dsh. +> Docker-first launcher for AI coding agents — Claude Code, Codex, Gemini, Grok, Kimi, opencode, pi, dsh, and Cursor. > 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. @@ -11,8 +11,9 @@ 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`, opencode via -`OPENCODE_PERMISSION` allow-all, pi (no permission system by design), and -dsh via `DSH_PERMISSION_MODE=danger-full-access`. Auth +`OPENCODE_PERMISSION` allow-all, pi (no permission system by design), +dsh via `DSH_PERMISSION_MODE=danger-full-access`, and cursor via +`--force`. 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; @@ -35,6 +36,7 @@ deva.sh kimi deva.sh opencode deva.sh pi deva.sh dsh +deva.sh cursor 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 29b609c..f58da58 100644 --- a/scripts/install-agent-tooling.sh +++ b/scripts/install-agent-tooling.sh @@ -12,6 +12,7 @@ set -euo pipefail : "${OPENCODE_VERSION:?OPENCODE_VERSION is required}" : "${PI_CODING_AGENT_VERSION:?PI_CODING_AGENT_VERSION is required}" : "${DSH_VERSION:?DSH_VERSION is required}" +: "${CURSOR_CLI_VERSION:?CURSOR_CLI_VERSION is required}" CCTRACE_VERSION="${CCTRACE_VERSION:-0.4.0}" CCX_VERSION="${CCX_VERSION:-v0.7.0}" @@ -317,6 +318,46 @@ install_ccx() { log "ccx installed" } +# Cursor CLI is not on npm and its installer script offers no pin hook, +# but the download URL is deterministic. Fetch the tarball directly and +# lay it out the way the official installer does +# (~/.local/share/cursor-agent/versions//), then strip the write +# bit from the tree: the CLI schedules a silent self-update on startup +# that writes new versions and tmp dirs there -- updaters fight pins. +# Only the `cursor-agent` bin name is linked; the official installer also +# squats `agent`, which is too generic for a container with nine CLIs. +install_cursor_agent() { + ensure_safe_cwd + mkdir -p "$DEVA_HOME/.local/bin" + + log "Installing Cursor CLI pinned to ${CURSOR_CLI_VERSION}" + local arch + case "$(uname -m)" in + x86_64) arch="x64" ;; + aarch64 | arm64) arch="arm64" ;; + *) die "Unsupported architecture for cursor: $(uname -m)" ;; + esac + + local tmp_dir versions_dir target url + tmp_dir="$(mktemp -d)" + versions_dir="$DEVA_HOME/.local/share/cursor-agent/versions" + target="$versions_dir/${CURSOR_CLI_VERSION}" + url="https://downloads.cursor.com/lab/${CURSOR_CLI_VERSION}/linux/${arch}/agent-cli-package.tar.gz" + + retry_cmd 3 "download cursor tarball" download_to "$url" "$tmp_dir/cursor.tar.gz" \ + || die "cursor download failed: $url" + mkdir -p "$target" + tar -xzf "$tmp_dir/cursor.tar.gz" -C "$target" --strip-components=1 + rm -rf "$tmp_dir" + [ -x "$target/cursor-agent" ] || die "cursor tarball missing cursor-agent" + + ln -sf "$target/cursor-agent" "$DEVA_HOME/.local/bin/cursor-agent" + chmod a-w "$versions_dir" "$DEVA_HOME/.local/share/cursor-agent" + + "$DEVA_HOME/.local/bin/cursor-agent" --version || die "cursor verification failed" + log "cursor-agent installed" +} + # cctrace ships no prebuilt binaries; compile the standalone binary with bun. # The compiled binary receives argv directly, so `cctrace -- ` # works (bun's CLI would eat the leading "--"). @@ -353,6 +394,7 @@ main() { case "$stage" in agents) install_npm_agent_tooling + install_cursor_agent install_ccx ;; cctrace) @@ -360,6 +402,7 @@ main() { ;; all) install_npm_agent_tooling + install_cursor_agent install_ccx install_cctrace ;; diff --git a/scripts/release-utils.sh b/scripts/release-utils.sh index 9e05951..8628a88 100755 --- a/scripts/release-utils.sh +++ b/scripts/release-utils.sh @@ -34,6 +34,7 @@ TOOL_REGISTRY=( "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" "dsh|npm|@deepseek-ai/dsh|org.opencontainers.image.dsh_version|https://www.npmjs.com/package/@deepseek-ai/dsh|github:deepseek-ai/deepseek-harness|agent|main" + "cursor|cursor-installer|cursor.com/install|org.opencontainers.image.cursor_cli_version|https://cursor.com/docs/cli||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" @@ -187,6 +188,13 @@ _npm_registry_latest() { # Kimi WebBridge has no npm package; the CDN's version-first layout # publishes a manifest at latest/version.json ({"version":"vX.Y.Z",...}). +# Cursor's installer script hardcodes the current version (no API, no +# npm). Parse it out of the deterministic download URL it embeds. +_cursor_installer_latest() { + curl -fsSL --max-time 10 "https://cursor.com/install" 2>/dev/null | \ + sed -n 's|.*downloads\.cursor\.com/lab/\([^/]*\)/.*|\1|p' | head -1 +} + _webbridge_cdn_latest() { curl -fsSL --max-time 10 \ "https://cdn.kimi.com/webbridge/latest/version.json" 2>/dev/null | \ @@ -208,6 +216,9 @@ fetch_latest_version() { github-release) gh api "repos/$source/releases/latest" --jq '.tag_name' 2>/dev/null || echo "" ;; + cursor-installer) + _cursor_installer_latest || echo "" + ;; github-commit) local branch="master" [[ $source == "thevibeworks/ccx" ]] && branch="main" @@ -231,6 +242,10 @@ fetch_version_date() { github-release) gh api "repos/$source/releases/tags/$version" --jq '.published_at' 2>/dev/null || echo "" ;; + cursor-installer) + # The version encodes its own date: YYYY.MM.DD-hash. + printf '%sT00:00:00Z\n' "$(printf '%s' "${version%%-*}" | tr . -)" + ;; github-commit) gh api "repos/$source/commits/$version" --jq '.commit.committer.date' 2>/dev/null || echo "" ;; diff --git a/scripts/resolve-tool-versions.sh b/scripts/resolve-tool-versions.sh index 1de86bf..e69c3c1 100644 --- a/scripts/resolve-tool-versions.sh +++ b/scripts/resolve-tool-versions.sh @@ -35,6 +35,7 @@ main() { resolve_tool "opencode_version" "opencode" resolve_tool "pi_coding_agent_version" "pi" resolve_tool "dsh_version" "dsh" + resolve_tool "cursor_cli_version" "cursor" resolve_tool "ccx_version" "ccx" resolve_tool "copilot_api_version" "copilot-api" } diff --git a/scripts/test-cursor-auth.sh b/scripts/test-cursor-auth.sh new file mode 100755 index 0000000..bc59ce5 --- /dev/null +++ b/scripts/test-cursor-auth.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# test-cursor-auth.sh - cursor agent auth wiring (config-home mounts + CURSOR_API_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 "=== cursor oauth (default) ===" +oauth_out="$(run_dry cursor --debug --dry-run || true)" +want "runs cursor-agent with --force" "cursor-agent --force" "$oauth_out" +want "auth method is oauth" "DEVA_AUTH_METHOD=oauth" "$oauth_out" +want "login URL printing wired" "NO_OPEN_BROWSER=1" "$oauth_out" + +echo "=== cursor oauth: no host ~/.cursor mount, ever ===" +# Host ~/.cursor is the Cursor IDE's state dir; seed one and assert the +# legacy fallback does NOT bind it in. +mkdir -p "$tmp_home/.cursor" +ide_out="$(run_dry cursor --dry-run || true)" +want_absent "host IDE dir not mounted" "$tmp_home/.cursor:" "$ide_out" +rm -rf "$tmp_home/.cursor" + +echo "=== cursor oauth: hybrid config-root mounts both homes ===" +# Seed the config-root layout the scaffold leaves behind and assert the +# centralized walk emits both canonical mounts (.cursor + .config/cursor). +mkdir -p "$tmp_home/.config/deva/cursor/.cursor" +mkdir -p "$tmp_home/.config/deva/cursor/.config/cursor" +hybrid_out="$(run_dry cursor --dry-run || true)" +want "data home mounted" ":/home/deva/.cursor" "$hybrid_out" +want "config home mounted" ":/home/deva/.config/cursor" "$hybrid_out" + +echo "=== cursor api-key: CURSOR_API_KEY as env, no mount ===" +apikey_out="$(CURSOR_API_KEY=key_cur_test_1234 run_dry cursor --auth-with api-key --dry-run -- -p hi || true)" +want "key wired + redacted" "CURSOR_API_KEY=" "$apikey_out" +want "key last-4 tags container" "--api-key-1234--" "$apikey_out" +want "passes agent args after --" "cursor-agent --force -p hi" "$apikey_out" +want_absent "no .cursor mount in api-key mode" ":/home/deva/.cursor\"" "$apikey_out" +# a user -v could still carry a config dir in; auth.json gets blanked +want "auth.json blank-overlayed" ".blank:/home/deva/.config/cursor/auth.json" "$apikey_out" +rm -rf "$tmp_home/.config/deva/cursor" + +echo "=== cursor oauth: host CURSOR_API_KEY must not leak ===" +leak_out="$(CURSOR_API_KEY=key_host_leak_9999 run_dry cursor -e CURSOR_API_KEY --dry-run || true)" +want_absent "host key filtered in oauth mode" "CURSOR_API_KEY" "$leak_out" + +echo "=== cursor --trace: rejected until cctrace ships a profile ===" +trace_out="$(run_dry cursor --trace --dry-run || true)" +want "trace rejected" "--trace is not supported for cursor" "$trace_out" + +echo "=== cursor --trace after -- is passthrough ===" +trace_pass_out="$(run_dry cursor --dry-run -- --trace || true)" +want_absent "--trace after -- not absorbed" "--trace is not supported" "$trace_pass_out" +want "--trace passed to agent" "cursor-agent --force --trace" "$trace_pass_out" + +echo "=== cursor api-key: missing key errors ===" +missing_out="$(CURSOR_API_KEY= run_dry cursor --auth-with api-key --dry-run || true)" +want "errors when no key set" "CURSOR_API_KEY not set" "$missing_out" + +if [ "$fail" -ne 0 ]; then + echo "FAIL: cursor auth wiring" >&2 + exit 1 +fi +echo "OK: cursor auth wiring" diff --git a/scripts/test-install-agent-tooling.sh b/scripts/test-install-agent-tooling.sh index 074f9fa..7da2b10 100755 --- a/scripts/test-install-agent-tooling.sh +++ b/scripts/test-install-agent-tooling.sh @@ -10,6 +10,8 @@ load_version_pins tmp_root="$(mktemp -d)" cleanup() { chmod 700 "$tmp_root/deny" 2>/dev/null || true + # install_cursor_agent strips the write bit from its versions dir + chmod -R u+w "$tmp_root" 2>/dev/null || true rm -rf "$tmp_root" } trap cleanup EXIT @@ -120,6 +122,15 @@ case "$url" in *"/releases/download/"*) exit 22 ;; + *"downloads.cursor.com/lab/"*) + tarball_dir="$(mktemp -d)" + mkdir -p "$tarball_dir/dist-package" + printf '#!/usr/bin/env bash\necho "__CURSOR_CLI_VERSION__"\n' \ + >"$tarball_dir/dist-package/cursor-agent" + chmod +x "$tarball_dir/dist-package/cursor-agent" + tar -czf "$out" -C "$tarball_dir" dist-package + rm -rf "$tarball_dir" + ;; *"/skills/atl-cli/SKILL.md") printf '%s\n' '# fake atlas skill' >"$out" ;; @@ -176,6 +187,7 @@ BIN chmod +x "$out" EOF +sed -i -e "s#__CURSOR_CLI_VERSION__#$CURSOR_CLI_VERSION#g" "$fake_bin/curl" chmod +x "$fake_bin/npm" "$fake_bin/curl" "$fake_bin/go" "$fake_bin/bun" mkdir -p "$tmp_root/deny" @@ -205,6 +217,12 @@ mkdir -p "$tmp_root/deny" test -L "$fake_home/.npm-global/bin/grok" [ "$(readlink "$fake_home/.npm-global/bin/grok")" = "$fake_home/.local/bin/grok" ] test ! -e "$fake_home/.grok/bin" + grep -F "Installing Cursor CLI pinned to $CURSOR_CLI_VERSION" <<<"$output" >/dev/null + grep -F "cursor-agent installed" <<<"$output" >/dev/null + test -x "$fake_home/.local/bin/cursor-agent" + [ "$("$fake_home/.local/bin/cursor-agent")" = "$CURSOR_CLI_VERSION" ] + # updater-starving write-bit strip + test ! -w "$fake_home/.local/share/cursor-agent/versions" grep -F "Installing ccx pinned to $CCX_VERSION" <<<"$output" >/dev/null grep -F "falling back to go install" <<<"$output" >/dev/null grep -F "ccx installed" <<<"$output" >/dev/null diff --git a/scripts/update-version-pins.sh b/scripts/update-version-pins.sh index b1a0eca..a029ede 100755 --- a/scripts/update-version-pins.sh +++ b/scripts/update-version-pins.sh @@ -89,6 +89,7 @@ pin() { git-tag) new_val=$(fetch_latest_git_tag "$1") || true ;; git-commit) new_val=$(fetch_latest_commit "$1" "$2") || true ;; webbridge) new_val=$(_webbridge_cdn_latest) || true ;; + cursor) new_val=$(_cursor_installer_latest) || true ;; esac # Clear fetching line @@ -136,6 +137,7 @@ registry_tool() { OPENCODE_VERSION) echo "opencode" ;; PI_CODING_AGENT_VERSION) echo "pi" ;; DSH_VERSION) echo "dsh" ;; + CURSOR_CLI_VERSION) echo "cursor" ;; CCX_VERSION) echo "ccx" ;; COPILOT_API_VERSION) echo "copilot-api" ;; PLAYWRIGHT_VERSION) echo "playwright" ;; @@ -234,6 +236,7 @@ main() { pin "opencode" OPENCODE_VERSION npm "opencode-ai" pin "pi" PI_CODING_AGENT_VERSION npm "@earendil-works/pi-coding-agent" pin "dsh" DSH_VERSION npm "@deepseek-ai/dsh" + pin "cursor" CURSOR_CLI_VERSION cursor 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 d377239..24725be 100644 --- a/scripts/version-pins.sh +++ b/scripts/version-pins.sh @@ -22,6 +22,7 @@ VERSION_PIN_VARS=( OPENCODE_VERSION PI_CODING_AGENT_VERSION DSH_VERSION + CURSOR_CLI_VERSION CCX_VERSION COPILOT_API_VERSION PLAYWRIGHT_VERSION @@ -91,6 +92,11 @@ KIMI_CODE_VERSION=$KIMI_CODE_VERSION OPENCODE_VERSION=$OPENCODE_VERSION PI_CODING_AGENT_VERSION=$PI_CODING_AGENT_VERSION DSH_VERSION=$DSH_VERSION +# Cursor CLI. Not on npm; cursor.com/install hardcodes the version and +# offers no pin hook, but the download URL is deterministic: +# downloads.cursor.com/lab//linux//agent-cli-package.tar.gz +# so the image fetches the tarball directly. Version format YYYY.MM.DD-hash. +CURSOR_CLI_VERSION=$CURSOR_CLI_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 6880a28..85a461f 100755 --- a/scripts/version-upgrade.sh +++ b/scripts/version-upgrade.sh @@ -21,6 +21,7 @@ _CLI_KIMI="${KIMI_CODE_VERSION:-}" _CLI_OPENCODE="${OPENCODE_VERSION:-}" _CLI_PI="${PI_CODING_AGENT_VERSION:-}" _CLI_DSH="${DSH_VERSION:-}" +_CLI_CURSOR="${CURSOR_CLI_VERSION:-}" _CLI_CCX="${CCX_VERSION:-}" _CLI_COPILOT="${COPILOT_API_VERSION:-}" _CLI_PLAYWRIGHT="${PLAYWRIGHT_VERSION:-}" @@ -74,8 +75,8 @@ 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, pi, dsh, ccx, copilot-api, playwright, - cloakbrowser, + opencode, pi, dsh, cursor, ccx, copilot-api, + playwright, cloakbrowser, kimi-webbridge -h, --help Show this help @@ -96,6 +97,7 @@ Environment: OPENCODE_VERSION Override opencode version PI_CODING_AGENT_VERSION Override pi version DSH_VERSION Override dsh version + CURSOR_CLI_VERSION Override cursor version CCX_VERSION Override ccx version COPILOT_API_VERSION Override copilot-api version PLAYWRIGHT_VERSION Override playwright version (rust image only) @@ -126,7 +128,7 @@ apply_only_filter() { [[ -n $ONLY ]] || return 0 local tool - local known="claude-code cctrace codex gemini-cli grok-cli kimi-code opencode pi dsh ccx copilot-api playwright cloakbrowser kimi-webbridge" + local known="claude-code cctrace codex gemini-cli grok-cli kimi-code opencode pi dsh cursor ccx copilot-api playwright cloakbrowser kimi-webbridge" for tool in ${ONLY//,/ }; do case " $known " in *" $tool "*) ;; @@ -145,6 +147,7 @@ apply_only_filter() { tool_selected opencode || _CLI_OPENCODE="${_CLI_OPENCODE:-$OPENCODE_VERSION}" tool_selected pi || _CLI_PI="${_CLI_PI:-$PI_CODING_AGENT_VERSION}" tool_selected dsh || _CLI_DSH="${_CLI_DSH:-$DSH_VERSION}" + tool_selected cursor || _CLI_CURSOR="${_CLI_CURSOR:-$CURSOR_CLI_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}" @@ -237,7 +240,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 pi_ver dsh_ver ccx_ver copilot_ver playwright_ver + local claude_ver cctrace_ver codex_ver gemini_ver grok_ver kimi_ver opencode_ver pi_ver dsh_ver cursor_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")}" @@ -247,6 +250,7 @@ main() { opencode_ver="${_CLI_OPENCODE:-$(get_latest "opencode")}" pi_ver="${_CLI_PI:-$(get_latest "pi")}" dsh_ver="${_CLI_DSH:-$(get_latest "dsh")}" + cursor_ver="${_CLI_CURSOR:-$(get_latest "cursor")}" ccx_ver="${_CLI_CCX:-$(get_latest "ccx")}" copilot_ver="${_CLI_COPILOT:-$(get_latest "copilot-api")}" playwright_ver="${_CLI_PLAYWRIGHT:-${PLAYWRIGHT_VERSION}}" @@ -260,6 +264,7 @@ main() { [[ -z $opencode_ver ]] && missing+=("OPENCODE_VERSION") [[ -z $pi_ver ]] && missing+=("PI_CODING_AGENT_VERSION") [[ -z $dsh_ver ]] && missing+=("DSH_VERSION") + [[ -z $cursor_ver ]] && missing+=("CURSOR_CLI_VERSION") [[ -z $ccx_ver ]] && missing+=("CCX_VERSION") [[ -z $copilot_ver ]] && missing+=("COPILOT_API_VERSION") [[ -z $playwright_ver ]] && missing+=("PLAYWRIGHT_VERSION") @@ -281,6 +286,7 @@ main() { "opencode|opencode_ver|_CLI_OPENCODE|opencode" "pi|pi_ver|_CLI_PI|pi" "dsh|dsh_ver|_CLI_DSH|dsh" + "cursor|cursor_ver|_CLI_CURSOR|cursor" "CCX|ccx_ver|_CLI_CCX|ccx" "Copilot API|copilot_ver|_CLI_COPILOT|copilot-api" "Playwright|playwright_ver|_CLI_PLAYWRIGHT|playwright" @@ -412,6 +418,7 @@ main() { --build-arg OPENCODE_VERSION="$opencode_ver" \ --build-arg PI_CODING_AGENT_VERSION="$pi_ver" \ --build-arg DSH_VERSION="$dsh_ver" \ + --build-arg CURSOR_CLI_VERSION="$cursor_ver" \ --build-arg CCX_VERSION="$ccx_ver" \ --build-arg COPILOT_API_VERSION="$copilot_ver" \ -t "$BUILD_IMAGE" . @@ -430,6 +437,7 @@ main() { --build-arg OPENCODE_VERSION="$opencode_ver" \ --build-arg PI_CODING_AGENT_VERSION="$pi_ver" \ --build-arg DSH_VERSION="$dsh_ver" \ + --build-arg CURSOR_CLI_VERSION="$cursor_ver" \ --build-arg CCX_VERSION="$ccx_ver" \ --build-arg PLAYWRIGHT_VERSION="$playwright_ver" \ --build-arg RUST_TOOLCHAINS="$RUST_TOOLCHAINS" \ @@ -462,6 +470,7 @@ main() { OPENCODE_VERSION="$opencode_ver" PI_CODING_AGENT_VERSION="$pi_ver" DSH_VERSION="$dsh_ver" + CURSOR_CLI_VERSION="$cursor_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 6c8a7fa..959a603 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 dsh gemini-cli grok-cli kimi-code opencode pi" +expected_agent="cctrace ccx claude-code codex copilot-api cursor dsh 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 dsh gemini-cli grok-cli kimi-code opencode pi" +expected_main="cctrace ccx claude-code codex copilot-api cursor dsh 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 dsh gemini-cli grok-cli kimi-code opencode pi playwright" -assert_eq "default returns all 12" "$expected_all" "$default_all" +expected_all="cctrace ccx claude-code codex copilot-api cursor dsh gemini-cli grok-cli kimi-code opencode pi playwright" +assert_eq "default returns all 13" "$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 1645018..b888272 100644 --- a/tests/version-upgrade.sh +++ b/tests/version-upgrade.sh @@ -31,6 +31,7 @@ inspect) "org.opencontainers.image.opencode_version":"1.18.14", "org.opencontainers.image.pi_coding_agent_version":"0.84.0", "org.opencontainers.image.dsh_version":"0.1.0-rc.5", + "org.opencontainers.image.cursor_cli_version":"2026.08.10-0000000", "org.opencontainers.image.ccx_version":"v0.7.0", "org.opencontainers.image.copilot_api_version":"0ea08febdd7e3e055b03dd298bf57e669500b5c1", "org.opencontainers.image.playwright_version":"1.59.0" @@ -111,6 +112,7 @@ case "$url" in */-/package/opencode-ai/dist-tags) echo '{"latest":"1.18.14"}' ;; */-/package/@earendil-works/pi-coding-agent/dist-tags) echo '{"latest":"0.84.1"}' ;; */-/package/@deepseek-ai/dsh/dist-tags) echo '{"latest":"0.1.0-rc.6"}' ;; +*cursor.com/install) echo 'DOWNLOAD_URL="https://downloads.cursor.com/lab/2026.08.11-e8db854/linux/x64/agent-cli-package.tar.gz"' ;; */-/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":{}}' ;; @@ -234,6 +236,7 @@ for expected in \ "KIMI_CODE_VERSION=0.28.0" \ "PI_CODING_AGENT_VERSION=0.84.1" \ "DSH_VERSION=0.1.0-rc.6" \ + "CURSOR_CLI_VERSION=2026.08.11-e8db854" \ "CCX_VERSION=v0.7.0" \ "COPILOT_API_VERSION=0ea08febdd7e3e055b03dd298bf57e669500b5c1" \ "PLAYWRIGHT_VERSION=1.60.0" \ @@ -379,6 +382,7 @@ case "$url" in */-/package/opencode-ai/dist-tags) echo '{"latest":"1.18.14"}' ;; */-/package/@earendil-works/pi-coding-agent/dist-tags) echo '{"latest":"0.84.1"}' ;; */-/package/@deepseek-ai/dsh/dist-tags) echo '{"latest":"0.1.0-rc.6"}' ;; +*cursor.com/install) echo 'DOWNLOAD_URL="https://downloads.cursor.com/lab/2026.08.11-e8db854/linux/x64/agent-cli-package.tar.gz"' ;; */-/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 a91efad..7c101ac 100644 --- a/versions.env +++ b/versions.env @@ -17,6 +17,11 @@ KIMI_CODE_VERSION=0.36.0 OPENCODE_VERSION=1.18.18 PI_CODING_AGENT_VERSION=0.84.1 DSH_VERSION=0.1.0-rc.6 +# Cursor CLI. Not on npm; cursor.com/install hardcodes the version and +# offers no pin hook, but the download URL is deterministic: +# downloads.cursor.com/lab//linux//agent-cli-package.tar.gz +# so the image fetches the tarball directly. Version format YYYY.MM.DD-hash. +CURSOR_CLI_VERSION=2026.08.11-e8db854 CCX_VERSION=v0.15.0 COPILOT_API_VERSION=0ea08febdd7e3e055b03dd298bf57e669500b5c1 PLAYWRIGHT_VERSION=1.62.1