This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
默认使用中文与用户交流(解释、总结、提问均用中文)。代码、标识符、注释、提交信息和仓库文档保持英文,除非用户另有要求。
A controlled MCP gateway from ChatGPT Web to a remote Linux workspace. It is not a raw shell and must never expose one: every capability is a narrow, allowlisted, bounded, auditable MCP tool. Go 1.22, stdlib-only (zero dependencies in go.mod — adding one needs clear justification), single binary per product line, file-backed state (no database). The JSON-RPC/MCP layer is hand-rolled (no MCP SDK).
AniMonitor is a separate project: it observes/notifies/digests. AniGate reads/invokes/executes. Integration stays event/webhook-based; do not merge them.
make verify # THE local gate: mod verify, test, vet, race, build all 3 binaries,
# tool-surface checks, HTTP MCP smoke test. Run before PRs.
make build # builds bin/anigate-mini, bin/anigate-max, bin/anigate
make test # go test ./...
make race # go test -race ./... (ANIGATE_SKIP_RACE=1 skips race in verify.sh)
go test ./internal/anigate -run TestFSRead # run a single test
make tools # print product-filtered tool lists for Mini and Max
make run-http-mini # ./bin/anigate-mini http --addr 127.0.0.1:8787 --config configs/anigate.mini.example.json
make run-stdio-max # stdio mode with example configTests shell out to real git (and spawn real subprocesses), so git must be on PATH. CI (.github/workflows/ci.yml, Go 1.22.x) runs scripts/verify.sh directly — verify.sh is the single source of truth for the quality gate. Releases: push a v* tag; no version injection at build time, but the release workflow fails if the tag doesn't match the VERSION file.
Version bumps require editing both the VERSION file and the Version const in internal/anigate/version.go — TestVersionFileMatchesConstant fails otherwise.
The three cmd/*/main.go files are 3-line mains that call anigate.RunCLI(os.Args[1:], productLine) with different ProductLine constants. anigate-mini → Mini; anigate-max and anigate (legacy alias, must keep the full Max surface) → Max. Subcommands: version, stdio, http, tools (internal/anigate/cli.go).
Request flow: ServeStdio (mcp.go, newline-delimited JSON, 10MB cap) or ServeHTTP (http.go, POST /mcp, 10MB cap, bearer-token auth; refuses to listen on non-loopback without auth_token) → dispatchJSON → Service.CallTool, a single hand-written switch on tool name in service.go. The tool catalog is the static allTools() slice with inline JSON schemas.
A tool handler is func (s *Service) name(args map[string]any) (map[string]any, error); args are decoded with the stringArg/intArgDefault/boolArg helpers (JSON numbers arrive as float64). The schemas in allTools() are documentation only — nothing validates incoming args against them; handlers re-validate everything.
Adding/removing a tool touches:
allTools()inservice.go(schema)- the
CallToolswitch inservice.go(dispatch) miniToolNamesinproduct.goif it belongs in Mini (no map entry = silently Max-only)- Two hard-coded tool-count/name assertion sites:
scripts/verify.sh(21 Mini / 56 Max / 56 legacy; CI runs this script) andservice_test.go(TestMiniProductToolsArePreviewCorehas the exact ordered Mini list;TestMaxProductToolsRemainCompleteasserts the Max count)
Mini must never expose execution/mutation families: agent.*, publish.*, file.edit_apply, patch.apply, app.run_preset, job.*, project.*, task.*, audit.*, workspace.snapshot, gate.* (grepped in verify.sh).
- HTTP token auth (HTTP mode only).
- Product gate:
Service.Tools()filters tools/list for Mini;requireToolForProductruns first inCallTool. It deliberately returns nil for unknown tool names so the switch produces "unknown tool" instead of a misleading product-line error — keep that ordering. - Workspace profile ladder (
workspaceAllowsinpolicy.go): profiles arereader/operator/agent;writeneeds!ReadOnlyAND operator/agent;agent.*needs the agent profile.read_only:trueblocks only thewriteneed — presets can still execute. Default profile when omitted in config isreader. Handlers do their own permission checks — there is no central write gate. A new mutating tool must calls.workspaceAllows(workspace, "write")itself or it silently bypasses policy. Note: the preset "operate" check is duplicated inline inJobManager.RunPreset(jobs.go) — keep it in sync withworkspaceAllows. - Path confinement:
pathPolicy.resolve(pathpolicy.go) symlink-resolves and rejects anything escaping the workspace root. Existing paths are resolved directly; non-existent targets are confined via their deepest existing ancestor (resolveDeepestExisting), so a new file under an escaping symlink is still rejected. It also rejects any resolved path insidestate_dir(insideStateDir), so a workspace that physically containsstate_dir(the shipped configs do) still cannot reach publish tokens, job/task/artifact records, or the audit stream through a workspace tool.
Tool failures become MCP tool results with IsError:true and plain text — never JSON-RPC errors. RPC error codes are reserved for protocol failures only (-32700/-32602/-32601). There is no typed error hierarchy. Every tools/call is appended to the audit event log, success or failure.
Everything lives under state_dir (default <root>/.anigate/state): jobs/<id>.json, logs/<id>.log, artifacts/<id>.{json,txt}, agents/sessions/ + agents/messages/*.ndjson, tasks/, handoffs/, publish_tokens/, home/ (isolated HOME), and events.ndjson (append-only audit stream). No cross-process locking; JSON records use write-to-.tmp-then-rename — keep that pattern for new record types. All IDs come from newJobID() (timestamp + random hex) and every ID must be gated with validName() before being joined into a state_dir path — that is the only path-traversal defense. State files are 0700/0600; workspace files written by file.edit_apply are 0644.
Large tool outputs spill to artifacts via boundedTextArtifact (artifact.go): truncated inline preview + an ArtifactRef with suggested follow-up tools. Size limits are three distinct config knobs clamped at call sites: MaxReadBytes, MaxJobLogBytes (1 MiB), MaxArtifactBytes (4 MiB).
Heads-up: context.go contains only contextWithBackground(); the actual context.health, handoff, and workspace.snapshot logic lives in handoff.go.
No shell, ever: presets, agents, and git all run as argv arrays via exec.CommandContext (git/gh local ops get a 15s timeout; clone/fetch/push/gh network ops get 120s — see hostcmd.go). The environment is built from scratch: PATH only (plus HOME=<state_dir>/home when isolated_home, plus literal env pairs from config validated against env_allowlist at config-load time, not exec time). Host env is never inherited. Remote URLs are redacted in errors. Async jobs intentionally use context.Background() — do not "fix" this to the request context or async agent jobs die when the RPC returns. Job cancellation relies on an in-memory map, so a restarted process cannot cancel jobs it did not start; on startup reconcileInterruptedJobs converts stale running records to failed (audit event carries reconciled: true).
project.ensure (allowlisted remotes only; force-resets origin URL to config on every call) → task.start (worktree + branch anigate/<id>-<slug> under .anigate-worktrees/) → task.commit_preview/task.commit (gated by a diff SHA-256 fingerprint recomputed at commit time) → publish.preview (rejects dirty worktrees; issues a single-use 30-min token) → publish.branch/publish.pr_create. The publish token is deleted before the push runs — that is the single-use guarantee; do not move the deletion after. Pushes to default/main/master are refused by name.
LoadConfig order: explicit path > ANIGATE_CONFIG > DefaultConfig(cwd); auth_token falls back to ANIGATE_AUTH_TOKEN. Relative state_dir/workspace paths resolve against the config file's directory, not cwd (the example configs rely on "path": ".."). Validation only runs inside LoadConfig — NewServiceWithProductLine on a hand-built Config skips all cross-reference checks. Preset arg substitution: {name} placeholders, typed args (string/int/bool/string_array); {prompt} is reserved for agent commands.
One white-box file: internal/anigate/service_test.go (package anigate). Tests call unexported methods directly with map[string]any args rather than going through JSON-RPC (except the dispatchJSON tests). testService(t) builds a real Max service in t.TempDir(); testServiceWithProduct(t, ProductLineMini) selects the product line. Tests mutate svc.cfg.Workspaces and rebuild svc.policy to simulate profiles. Style is scenario functions asserting on result maps (e.g. got["count"].(int)), not table-driven. Git tests build real bare remotes + clones.
Add tests for anything touching security boundaries (path confinement, product gating, profiles, publish tokens).
User-facing changes → update both README.md and README.zh-CN.md. Release-worthy changes → CHANGELOG.md. AGENTS.md and docs/design.md carry the product-boundary rules summarized above.