Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
320 lines
13 KiB
Markdown
320 lines
13 KiB
Markdown
# Claude Code + Ollama Cloud 替换 Cursor 自动化 — 可行性调研
|
||
|
||
Date: 2026-08-27
|
||
Status: feasible for triage; keep the existing control plane
|
||
Scope: replace Cursor CLI as the agent runner, not rewrite GitHub routing
|
||
|
||
## Why this exists
|
||
|
||
The current pipeline is `.github/workflows/ai-automation.yml` plus
|
||
`scripts/ai-automation.cjs`. Live mode is `triage_only`: classify issues,
|
||
do not implement, do not run the Codex fix loop.
|
||
|
||
A production classify run failed because Cursor hit its usage limit:
|
||
|
||
```
|
||
ActionRequiredError: You've hit your usage limit Get Cursor Pro for more Agent usage
|
||
```
|
||
|
||
Example: https://github.com/binaricat/Netcatty/actions/runs/33050247387
|
||
(job `Classify issue`, step `Research external context for classification`).
|
||
|
||
The goal is to keep issue triage running by swapping the agent from Cursor CLI
|
||
to Claude Code, with Ollama Cloud as the Anthropic-compatible backend. Secrets
|
||
stay in GitHub Actions (set via `gh`), never in the repo.
|
||
|
||
## Verdict
|
||
|
||
**Yes, this is feasible.** Do not replace the workflow with
|
||
`anthropics/claude-code-action`. Keep the existing router, labels, rate limits,
|
||
bot identity, isolated research workspace, and publish steps. Only replace the
|
||
agent invocation.
|
||
|
||
| Layer | Keep or replace | Why |
|
||
|---|---|---|
|
||
| Event routing, labels, daily limits, handoff comments | Keep | Pure GitHub control plane in `ai-automation.cjs` |
|
||
| Codex `@codex review` loop | Keep | Independent of Cursor; currently paused by `triage_only` |
|
||
| Isolated research workspace + imgproxy screenshots | Keep | Safety contract is still needed |
|
||
| Frozen helper copy, leak scan, no `GITHUB_TOKEN` in agent | Keep | Same threat model |
|
||
| Cursor CLI install, AppArmor sandbox, API-key fd bridge | Replace | This is the part that is out of credits |
|
||
| `agent -p --sandbox enabled` | Replace | `claude --bare -p` with `dontAsk` + allowlist |
|
||
|
||
Current production path that must come back first: **research + classify**.
|
||
Implement / follow-up / Codex-fix jobs can stay paused until triage is green.
|
||
|
||
## How the current pipeline actually works
|
||
|
||
The YAML is large because it is an orchestrator, not “run an agent on the
|
||
issue”. Jobs:
|
||
|
||
1. `route` — decide `issue_classify` / `issue_followup` / `codex_loop` / skip
|
||
2. `classify` — prepare issue JSON, isolated web research, classify, apply labels
|
||
3. `implement` / `followup` / `codex_loop` — gated off in `triage_only`
|
||
4. Codex re-request and source-issue cleanup — GitHub-only, no Cursor
|
||
|
||
Classify currently shells out to Cursor like this:
|
||
|
||
```bash
|
||
sudo --preserve-env=HOME,RUNNER_TEMP,GITHUB_WORKSPACE \
|
||
"$RUNNER_TEMP/ai-claude-authenticated" \
|
||
-p --mode=ask --trust --sandbox enabled --model auto --output-format text \
|
||
--workspace "$GITHUB_WORKSPACE" \
|
||
"$PROMPT"
|
||
```
|
||
|
||
Research uses the same binary in an empty temp workspace, `--output-format
|
||
stream-json`, then `parseExternalResearchStream()` requires a real completed
|
||
WebSearch/WebFetch tool event. Classify must write
|
||
`.ai-runtime/classification.json` with `category`, `confidence`, `summary`,
|
||
`reasoning`, `reply`, `code_paths`, `code_findings`.
|
||
|
||
That JSON contract is owned by `normalizeClassification()` in
|
||
`scripts/ai-automation.cjs`, not by the checked-in
|
||
`.github/ai/schemas/classification.schema.json` (the schema file is stale:
|
||
it omits `already_available`, `code_paths`, and `code_findings`).
|
||
|
||
## Claude Code as a headless runner
|
||
|
||
Official headless mode is `claude -p` ([docs](https://code.claude.com/docs/en/headless)):
|
||
|
||
- Exit 0 / non-zero for scripts
|
||
- `--bare` skips hooks, skills, plugins, MCP, CLAUDE.md (recommended for CI)
|
||
- `--output-format json` or `stream-json`
|
||
- `--json-schema` can enforce the classification object
|
||
- `--permission-mode dontAsk` is the documented “locked-down CI” mode:
|
||
only pre-allowed tools run; everything else is denied, never prompted
|
||
- `--allowedTools` / `--disallowedTools` for the allowlist
|
||
- Auth in `-p` / `--bare` is `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN`,
|
||
not a Claude.ai subscription login
|
||
|
||
Do **not** use `--dangerously-skip-permissions` for classify. Classify is
|
||
read-only. `dontAsk` plus Read/Grep/Glob (and maybe `Bash(rg *)`) is enough.
|
||
|
||
Do **not** adopt `anthropics/claude-code-action` as the new workflow. That
|
||
action is built for `@claude` mentions, the Claude GitHub App, and posting
|
||
its own comments. This repo already has `netcatty-bot`, admission quotas,
|
||
untrusted-issue sanitization, and a separate research pass. The official
|
||
action would fight that control plane. Invoke the CLI the same way Cursor is
|
||
invoked today.
|
||
|
||
## Ollama Cloud as the Anthropic endpoint
|
||
|
||
Ollama documents an Anthropic Messages compatibility layer, including tools,
|
||
streaming, vision, and thinking
|
||
([docs](https://docs.ollama.com/api/anthropic-compatibility)).
|
||
|
||
Two ways to reach cloud models:
|
||
|
||
1. **Local Ollama proxying `:cloud` models** — `ANTHROPIC_BASE_URL=http://localhost:11434`.
|
||
Needs a local Ollama daemon and sign-in. Wrong for GitHub-hosted runners.
|
||
2. **Direct ollama.com API** — `ANTHROPIC_BASE_URL=https://ollama.com` plus an
|
||
Ollama Cloud API key. This is the CI path.
|
||
|
||
Ollama Cloud’s native auth is `Authorization: Bearer $OLLAMA_API_KEY` against
|
||
`https://ollama.com`. Claude Code maps:
|
||
|
||
| Claude Code env | HTTP header | Use with Ollama Cloud |
|
||
|---|---|---|
|
||
| `ANTHROPIC_BASE_URL` | API host | `https://ollama.com` |
|
||
| `ANTHROPIC_AUTH_TOKEN` | `Authorization: Bearer …` | **the Ollama Cloud key** |
|
||
| `ANTHROPIC_API_KEY` | `X-Api-Key` | leave empty, or same key if a probe requires it |
|
||
|
||
A user-reported Ollama docs bug ([#13854](https://github.com/ollama/ollama/issues/13854))
|
||
says setting `ANTHROPIC_API_KEY` alone is **not** enough for cloud; the Bearer
|
||
token (`ANTHROPIC_AUTH_TOKEN`) plus `https://ollama.com` is.
|
||
|
||
Must pin the model. Claude Code defaults to Anthropic IDs such as
|
||
`claude-sonnet-4-6`. Ollama Cloud will 404 those. Also set the Haiku/Sonnet/Opus
|
||
alias env vars so background/compaction calls do not fall back to Claude names:
|
||
|
||
```
|
||
ANTHROPIC_MODEL=<ollama-cloud-id>
|
||
ANTHROPIC_DEFAULT_HAIKU_MODEL=<same or cheaper cloud id>
|
||
ANTHROPIC_DEFAULT_SONNET_MODEL=<same>
|
||
ANTHROPIC_DEFAULT_OPUS_MODEL=<same>
|
||
```
|
||
|
||
Ollama’s Claude Code page recommends coding cloud models such as
|
||
`glm-4.7:cloud`, `minimax-m2.1:cloud`, and shows
|
||
`kimi-k2.7-code:cloud` in the manual example. Pick one coding model and keep
|
||
it in a **repo variable**, not hardcoded in YAML, so it can change without a
|
||
workflow PR.
|
||
|
||
Ollama does **not** support prompt caching, `/v1/messages/count_tokens`, or
|
||
forcing `tool_choice`. Fine for classify. Token counts are approximations.
|
||
|
||
## Web research gap (the one real risk)
|
||
|
||
Cursor research depends on Cursor’s built-in WebSearch/WebFetch, then the
|
||
helper asserts a completed web-tool event in the stream.
|
||
|
||
Claude Code’s WebSearch/WebFetch are Anthropic server-side tools. They are
|
||
**not** guaranteed to work when `ANTHROPIC_BASE_URL` is `https://ollama.com`.
|
||
Ollama has its own REST APIs instead:
|
||
|
||
- `POST https://ollama.com/api/web_search`
|
||
- `POST https://ollama.com/api/web_fetch`
|
||
|
||
Those use the same Cloud API key. `ollama launch claude` wires them for local
|
||
use; a raw `claude -p` on a GHA runner may not.
|
||
|
||
Recommended research strategy for this repo:
|
||
|
||
1. Keep the empty-temp-workspace + imgproxy design.
|
||
2. Prefer a **control-plane** research helper that calls Ollama
|
||
`web_search` / `web_fetch` (or Claude Code WebSearch if a smoke test proves
|
||
it works against ollama.com).
|
||
3. Rewrite `parseExternalResearchStream()` to accept Claude `stream-json`
|
||
**or** a small JSONL log from the control-plane helper. Do not drop the
|
||
“must have a real source URL” check.
|
||
4. Classify still runs with WebSearch/WebFetch denied.
|
||
|
||
If research cannot get a source for a needed external fact, keep today’s
|
||
behavior: `RESEARCH_BLOCKED` → maintainer handoff. Do not invent sources.
|
||
|
||
## Security mapping
|
||
|
||
Keep these invariants from `.github/ai/README.md`:
|
||
|
||
- Agent steps get no GitHub token
|
||
- Automation never publishes `.github/` or automation scripts
|
||
- Issue text is sanitized before prompts
|
||
- Output is scanned for the provider secret
|
||
- External research has no repo checkout and no GitHub credentials
|
||
|
||
Cursor-specific pieces we will **not** copy 1:1:
|
||
|
||
- AppArmor profile from `downloads.cursor.com`
|
||
- fd-preload that injects `--api-key` without putting it in env/argv
|
||
|
||
Claude Code **requires** `ANTHROPIC_AUTH_TOKEN` in the process environment.
|
||
Mitigations:
|
||
|
||
- Stage the key the same way (`install -m 0400` into `RUNNER_TEMP`), export
|
||
only on the `claude` invocation, never as a job-wide `env:`
|
||
- `--bare` so project `.mcp.json` / hooks cannot run
|
||
- `--permission-mode dontAsk` + deny `WebSearch` / `WebFetch` / `Edit` /
|
||
`Write` on classify
|
||
- Claude sandbox (`sandbox.enabled`) if the GHA image can load it; if not,
|
||
fail closed for implement later, but classify can ship on `dontAsk` +
|
||
no GitHub token
|
||
- Continue leak scans against `ANTHROPIC_AUTH_TOKEN`
|
||
|
||
Do not run `claude` as root/`sudo`. Cursor’s launcher used `setpriv` to drop
|
||
privileges after reading the key. Claude Code refuses
|
||
`--dangerously-skip-permissions` as root; classify should run as the runner
|
||
user.
|
||
|
||
## What to change (phased)
|
||
|
||
### Phase 0 — secrets, no YAML behavior change
|
||
|
||
Set via `gh` against `binaricat/Netcatty`. Values never land in git.
|
||
|
||
```bash
|
||
# Key: GitHub secret (Bearer token for ollama.com)
|
||
printf '%s' "$OLLAMA_API_KEY" | gh secret set ANTHROPIC_AUTH_TOKEN -R binaricat/Netcatty
|
||
|
||
# Host: repo variable is enough (not a credential)
|
||
gh variable set ANTHROPIC_BASE_URL -R binaricat/Netcatty --body 'https://ollama.com'
|
||
|
||
# Model: repo variable so it can change without rotating the key
|
||
gh variable set CLAUDE_CODE_MODEL -R binaricat/Netcatty --body 'kimi-k2.7-code:cloud'
|
||
```
|
||
|
||
Optional aliases, same key:
|
||
|
||
```bash
|
||
gh variable set ANTHROPIC_DEFAULT_HAIKU_MODEL -R binaricat/Netcatty --body 'kimi-k2.7-code:cloud'
|
||
gh variable set ANTHROPIC_DEFAULT_SONNET_MODEL -R binaricat/Netcatty --body 'kimi-k2.7-code:cloud'
|
||
gh variable set ANTHROPIC_DEFAULT_OPUS_MODEL -R binaricat/Netcatty --body 'kimi-k2.7-code:cloud'
|
||
```
|
||
|
||
Do **not** commit the key, put it in `.github/workflows/*.yml` literals, or
|
||
paste it into issues/PRs.
|
||
|
||
Existing `ANTHROPIC_AUTH_TOKEN` can stay until Cursor jobs are deleted.
|
||
|
||
### Phase 1 — restore triage (this unblocks production)
|
||
|
||
In `.github/workflows/ai-automation.yml` classify job:
|
||
|
||
1. Install Claude Code: `curl -fsSL https://claude.ai/install.sh | bash`
|
||
2. Drop Cursor CLI install, credential bridge, AppArmor sandbox host
|
||
3. Research step: isolated workspace + Ollama web APIs or proven Claude
|
||
WebSearch; parse a Claude/control-plane research envelope
|
||
4. Classify step:
|
||
|
||
```bash
|
||
claude --bare -p "$PROMPT" \
|
||
--permission-mode dontAsk \
|
||
--allowedTools "Read,Grep,Glob" \
|
||
--disallowedTools "WebSearch,WebFetch,Edit,Write,NotebookEdit" \
|
||
--output-format json \
|
||
--json-schema "$(cat "$RUNNER_TEMP/classification.schema.json")" \
|
||
--model "$CLAUDE_CODE_MODEL"
|
||
```
|
||
|
||
5. Point prompts at `.ai-runtime/issue.json` still (runtime path rename
|
||
can wait)
|
||
6. Update `parseClassificationFile` to read Claude `--output-format json`
|
||
(`structured_output` or `result`) in addition to raw JSON files
|
||
7. Keep `applyClassification`, daily limits, Slack, failure handoff
|
||
8. Replace `sandbox_smoke` with a cheap authenticated `claude -p` ping
|
||
against Ollama Cloud
|
||
|
||
Also refresh `.github/ai/schemas/classification.schema.json` so it matches
|
||
`CATEGORIES` + `code_paths` / `code_findings`. Use that schema with
|
||
`--json-schema`.
|
||
|
||
Helper script: add `prepareClaudeCliSettings()` next to
|
||
`prepareAiCliSettings()`. Do not rename `ai-automation.cjs` in this
|
||
phase (issue-format, markers, tests all import that path).
|
||
|
||
### Phase 2 — implement / follow-up (only after triage is stable)
|
||
|
||
Same CLI, wider allowlist (`Edit`, `Write`, `Bash` for tests), still no GitHub
|
||
token, still deny `.github/` publishes. Restore `AI_AUTOMATION_MODE=full`
|
||
only after classify quality looks acceptable on real issues.
|
||
|
||
### Out of scope unless asked
|
||
|
||
- Installing the Claude GitHub App / `@claude` comments
|
||
- Switching review from Codex to Claude
|
||
- Running a local Ollama daemon on GHA
|
||
- Paying Anthropic first-party API (the point of Ollama Cloud)
|
||
|
||
## Suggested classify invocation contract
|
||
|
||
Env on the agent step only:
|
||
|
||
```
|
||
ANTHROPIC_BASE_URL: ${{ vars.ANTHROPIC_BASE_URL }}
|
||
ANTHROPIC_AUTH_TOKEN: (staged file, not a job-wide secret expansion in logs)
|
||
ANTHROPIC_API_KEY: ''
|
||
ANTHROPIC_MODEL: ${{ vars.CLAUDE_CODE_MODEL }}
|
||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1'
|
||
GITHUB_TOKEN: ''
|
||
GH_TOKEN: ''
|
||
```
|
||
|
||
Prompt files stay under `.github/ai/prompts/` for now. Swap the one
|
||
Cursor-specific sentence in `research.md` (“Use only Cursor's built-in
|
||
WebSearch and WebFetch tools”) to the new research tools.
|
||
|
||
## Sources
|
||
|
||
- Claude Code headless / `-p`: https://code.claude.com/docs/en/headless
|
||
- Claude Code GitHub Actions (why we are **not** using the Action as the
|
||
orchestrator): https://code.claude.com/docs/en/github-actions.md
|
||
- Permission mode `dontAsk`: https://code.claude.com/docs/en/permission-modes.md
|
||
- Env vars `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_API_KEY`:
|
||
https://code.claude.com/docs/en/env-vars
|
||
- Ollama Anthropic compatibility: https://docs.ollama.com/api/anthropic-compatibility
|
||
- Ollama Claude Code integration: https://docs.ollama.com/integrations/claude-code
|
||
- Ollama Cloud API host: https://docs.ollama.com/cloud
|
||
- Ollama web_search / web_fetch: https://docs.ollama.com/capabilities/web-search
|
||
- Cloud auth token vs API key: https://github.com/ollama/ollama/issues/13854
|
||
- Live Cursor failure: GitHub Actions run 33050247387,
|
||
`ActionRequiredError: You've hit your usage limit`
|