diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c2dcf678..9b130e93 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,6 +70,7 @@ jobs: - { suffix: "-claude", dockerfile: "Dockerfile.claude", artifact: "claude" } - { suffix: "-gemini", dockerfile: "Dockerfile.gemini", artifact: "gemini" } - { suffix: "-copilot", dockerfile: "Dockerfile.copilot", artifact: "copilot" } + - { suffix: "-cursor", dockerfile: "Dockerfile.cursor", artifact: "cursor" } platform: - { os: linux/amd64, runner: ubuntu-latest } - { os: linux/arm64, runner: ubuntu-24.04-arm } @@ -131,6 +132,7 @@ jobs: - { suffix: "-claude", artifact: "claude" } - { suffix: "-gemini", artifact: "gemini" } - { suffix: "-copilot", artifact: "copilot" } + - { suffix: "-cursor", artifact: "cursor" } runs-on: ubuntu-latest permissions: contents: read @@ -179,6 +181,7 @@ jobs: - { suffix: "-claude" } - { suffix: "-gemini" } - { suffix: "-copilot" } + - { suffix: "-cursor" } runs-on: ubuntu-latest permissions: contents: read diff --git a/Dockerfile.cursor b/Dockerfile.cursor new file mode 100644 index 00000000..b6969f65 --- /dev/null +++ b/Dockerfile.cursor @@ -0,0 +1,46 @@ +# --- Build stage --- +FROM rust:1-bookworm AS builder +WORKDIR /build +COPY Cargo.toml Cargo.lock ./ +RUN mkdir src && echo 'fn main() {}' > src/main.rs && cargo build --release && rm -rf src +COPY src/ src/ +RUN touch src/main.rs && cargo build --release + +# --- Runtime stage --- +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl procps && rm -rf /var/lib/apt/lists/* + +# Install Cursor Agent CLI (pinned version) +# Tarball source: https://downloads.cursor.com/lab//linux//agent-cli-package.tar.gz +# URL scheme scraped from Cursor's official downloads page β€” no apt/yum package exists. +# If Cursor changes this pattern, the build fails with curl 404. Monitor +# https://cursor.com/cli or https://docs.cursor.com/cli for version/URL updates. +ARG CURSOR_VERSION=2026.04.08-a41fba1 +RUN ARCH=$(dpkg --print-architecture) && \ + if [ "$ARCH" = "arm64" ]; then ARCH=arm64; else ARCH=x64; fi && \ + curl -fSL "https://downloads.cursor.com/lab/${CURSOR_VERSION}/linux/${ARCH}/agent-cli-package.tar.gz" \ + -o /tmp/cursor.tar.gz && \ + tar xzf /tmp/cursor.tar.gz -C /opt && \ + mv /opt/dist-package /opt/cursor-agent && \ + ln -s /opt/cursor-agent/cursor-agent /usr/local/bin/cursor-agent && \ + rm /tmp/cursor.tar.gz + +# Install gh CLI (for auth and token management) +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list && \ + apt-get update && apt-get install -y --no-install-recommends gh && \ + rm -rf /var/lib/apt/lists/* + +RUN useradd -m -s /bin/bash -u 1000 agent +ENV HOME=/home/agent +WORKDIR /home/agent + +COPY --from=builder --chown=agent:agent /build/target/release/openab /usr/local/bin/openab + +USER agent +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD pgrep -x openab || exit 1 +ENTRYPOINT ["openab"] +CMD ["/etc/openab/config.toml"] diff --git a/README.md b/README.md index 6ad1dbcd..fa49fd0c 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ The bot creates a thread. After that, just type in the thread β€” no @mention ne | Codex | `codex-acp` | [@zed-industries/codex-acp](https://github.com/zed-industries/codex-acp) | [docs/codex.md](docs/codex.md) | | Gemini | `gemini --acp` | Native | [docs/gemini.md](docs/gemini.md) | | Copilot CLI ⚠️ | `copilot --acp --stdio` | Native | [docs/copilot.md](docs/copilot.md) | +| Cursor | `cursor-agent acp` | Native | [docs/cursor.md](docs/cursor.md) | > πŸ”§ Running multiple agents? See [docs/multi-agent.md](docs/multi-agent.md) diff --git a/RELEASING.md b/RELEASING.md index 8e2f35d6..7aa8594a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -140,13 +140,15 @@ release-pr.yml 在 Release PR δΈ­θ‡ͺε‹•ζ›΄ζ–°δ»₯δΈ‹ζͺ”ζ‘ˆηš„η‰ˆζœ¬οΌš ## Image Variants -每欑 build η”’ε‡Ί 4 個 multi-arch image (linux/amd64 + linux/arm64): +每欑 build η”’ε‡Ί 6 個 multi-arch image (linux/amd64 + linux/arm64): ``` ghcr.io/openabdev/openab # default (kiro-cli) ghcr.io/openabdev/openab-codex # codex ghcr.io/openabdev/openab-claude # claude ghcr.io/openabdev/openab-gemini # gemini +ghcr.io/openabdev/openab-copilot # copilot +ghcr.io/openabdev/openab-cursor # cursor ``` Image tags 依 release ι‘žεž‹δΈεŒοΌš diff --git a/charts/openab/templates/NOTES.txt b/charts/openab/templates/NOTES.txt index 37f1c709..ca292b6f 100644 --- a/charts/openab/templates/NOTES.txt +++ b/charts/openab/templates/NOTES.txt @@ -24,6 +24,9 @@ Agents deployed: {{- else if eq $cfg.command "gemini" }} Authenticate: kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- gemini +{{- else if eq $cfg.command "cursor-agent" }} + Authenticate: + kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- cursor-agent login {{- end }} Restart after auth: diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 0e4f6e7a..ae724af0 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -48,6 +48,33 @@ agents: # tolerations: [] # affinity: {} # image: "ghcr.io/openabdev/openab-claude:latest" + # cursor: + # command: cursor-agent + # args: + # - acp + # - --model + # - auto + # - --workspace + # - /home/agent + # discord: + # botToken: "" + # allowedChannels: + # - "YOUR_CHANNEL_ID" + # allowedUsers: [] + # workingDir: /home/agent + # env: {} + # envFrom: [] + # pool: + # maxSessions: 10 + # sessionTtlHours: 24 + # reactions: + # enabled: true + # removeAfterReply: false + # persistence: + # enabled: true + # storageClass: "" + # size: 1Gi + # image: "ghcr.io/openabdev/openab-cursor:latest" image: "" command: kiro-cli args: diff --git a/config.toml.example b/config.toml.example index 6b377e5f..2b3199c9 100644 --- a/config.toml.example +++ b/config.toml.example @@ -32,6 +32,12 @@ working_dir = "/home/agent" # working_dir = "/home/agent" # env = {} # Auth via: kubectl exec -it -- gh auth login -p https -w +# [agent] +# command = "cursor-agent" +# args = ["acp", "--model", "auto", "--workspace", "/home/agent"] +# working_dir = "/home/agent" +# env = {} # Auth via: kubectl exec -it -- cursor-agent login + [pool] max_sessions = 10 session_ttl_hours = 24 diff --git a/docs/cursor.md b/docs/cursor.md new file mode 100644 index 00000000..94490817 --- /dev/null +++ b/docs/cursor.md @@ -0,0 +1,157 @@ +# Cursor Agent CLI β€” Agent Backend Guide + +How to run OpenAB with [Cursor Agent CLI](https://www.cursor.com/) as the agent backend. + +## Prerequisites + +- A paid [Cursor](https://www.cursor.com/pricing) subscription (**Pro or Business** β€” Free tier does not include Agent CLI access) +- Cursor Agent CLI with native ACP support + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Gateway WS β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ACP stdio β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Discord │◄─────────────►│ openab │──────────────►│ cursor-agent acp β”‚ +β”‚ User β”‚ β”‚ (Rust) │◄── JSON-RPC ──│ (Cursor Agent CLI) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +OpenAB spawns `cursor-agent acp` as a child process and communicates via stdio JSON-RPC. No intermediate layers. + +## Configuration + +```toml +[agent] +command = "cursor-agent" +args = ["acp"] +working_dir = "/home/agent" +# Auth via: kubectl exec -it -- cursor-agent login +``` + +## Docker + +Build with the Cursor-specific Dockerfile: + +```bash +docker build -f Dockerfile.cursor -t openab-cursor . +``` + +The Dockerfile installs a pinned version of Cursor Agent CLI via direct download from `downloads.cursor.com`. The version is controlled by the `CURSOR_VERSION` build arg. + +## Authentication + +Cursor Agent CLI uses its own login flow. In a headless container: + +```bash +# 1. Exec into the running pod/container +kubectl exec -it deployment/openab-cursor -- bash + +# 2. Authenticate via device flow +cursor-agent login + +# 3. Follow the device code flow in your browser + +# 4. Restart the pod (token is persisted via PVC) +kubectl rollout restart deployment/openab-cursor +``` + +The auth token is stored under `~/.cursor/` and persisted across pod restarts via PVC. + +## Helm Install + +> **Note**: The `ghcr.io/openabdev/openab-cursor` image is not published yet. You must build it locally first with `docker build -f Dockerfile.cursor -t openab-cursor .` and push to your own registry, or use a local image. + +```bash +helm install openab openab/openab \ + --set agents.kiro.enabled=false \ + --set agents.cursor.discord.botToken="$DISCORD_BOT_TOKEN" \ + --set-string 'agents.cursor.discord.allowedChannels[0]=YOUR_CHANNEL_ID' \ + --set agents.cursor.image=ghcr.io/openabdev/openab-cursor:latest \ + --set agents.cursor.command=cursor-agent \ + --set 'agents.cursor.args={acp}' \ + --set agents.cursor.persistence.enabled=true \ + --set agents.cursor.workingDir=/home/agent +``` + +## Model Selection + +List available models: + +```bash +cursor-agent --list-models +# or +cursor-agent models +``` + +To specify a model, pass `--model` as an arg: + +```toml +[agent] +command = "cursor-agent" +args = ["acp", "--model", "auto"] +``` + +In ACP mode, `--model` can be appended after `acp`. If omitted, the account default is used. + +To verify which model is active, ask the agent "who are you" β€” the underlying model will typically self-identify (e.g. "I am Gemini, a large language model built by Google."). + +## MCP Usage (ACP mode caveats) + +Cursor Agent CLI supports MCP servers configured via `.cursor/mcp.json` in the active workspace directory. **Which directory counts as the workspace is determined by the `--workspace` flag** β€” if omitted, cursor-agent auto-detects from `cwd`, which is usually `/home/agent` in OpenAB containers via the Dockerfile `WORKDIR` directive but can drift in interactive or local runs. For reproducible MCP loading, pass `--workspace` explicitly: + +```toml +[agent] +command = "cursor-agent" +args = ["acp", "--model", "auto", "--workspace", "/home/agent"] +``` + +This anchors: +- **MCP config lookup**: `/home/agent/.cursor/mcp.json` +- **Approval file path**: `/home/agent/.cursor/projects/home-agent/mcp-approvals.json` (slug = URL-safe workspace path) + +Without `--workspace`, a different cwd would produce a different slug and cursor-agent would not find previously saved approvals. + +### Example MCP config + +```json +{ + "mcpServers": { + "playwright": { + "command": "/usr/bin/npx", + "args": ["-y", "@playwright/mcp@latest"] + } + } +} +``` + +### Approval quirk in ACP mode + +Cursor's `--approve-mcps` flag **does not apply in ACP mode** β€” it only affects the interactive CLI. In ACP mode, MCP servers are gated by an approval file. Two options: + +1. **Pre-create the approvals file** at `/.cursor/projects//mcp-approvals.json`: + ```json + ["-"] + ``` + Hash is derived from workspace path + server config. + +2. **Approve once interactively**, then let Cursor persist the approval: + ```bash + kubectl exec -it deployment/openab-cursor -- cursor-agent + # invoke an MCP tool, approve the prompt; approval is saved + ``` + +OpenAB itself auto-responds to ACP `session/request_permission` with `allow_always` (see `src/acp/connection.rs`), so once an MCP server is *loaded*, subsequent tool calls pass without prompting. The approval file only gates the initial load. + +### Verifying MCP is loaded + +```bash +kubectl exec deployment/openab-cursor -- cursor-agent mcp list +# Expected: ": ready" +``` + +## Known Limitations + +- Cursor Agent CLI is a separate distribution from Cursor Desktop β€” they are not the same binary +- No official apt/yum package; the Dockerfile downloads a pinned tarball directly +- `cursor-agent login` requires an interactive terminal for the device flow +- Auth token persistence requires a PVC mount at the user home directory