Files
NetMesh/docs/research/claude-code-ollama-cloud-automation.md
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

320 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 Clouds 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>
```
Ollamas 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 Cursors built-in WebSearch/WebFetch, then the
helper asserts a completed web-tool event in the stream.
Claude Codes 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 todays
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`. Cursors 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`