Skip to content

Add claude engine: headless Claude Code workers, incl. free local inference via Anthropic-compatible endpoints - #76

Open
perpetualsec wants to merge 1 commit into
NateBJones-Projects:mainfrom
perpetualsec:claude-engine
Open

Add claude engine: headless Claude Code workers, incl. free local inference via Anthropic-compatible endpoints#76
perpetualsec wants to merge 1 commit into
NateBJones-Projects:mainfrom
perpetualsec:claude-engine

Conversation

@perpetualsec

Copy link
Copy Markdown

What this adds

A fourth worker lane: engines/claude-sandboxed.sh, which runs the claude CLI (Claude Code) headlessly as a Ringer worker, plus a commented [engines.claude] block in config.sample.toml and a dated entry in docs/MODEL-NOTES.md.

The wrapper is deliberately the same shape as engines/opencode-sandboxed.sh — taskdir as the first arg, --no-sandbox as the second wired to full_access_args so the allow_full_access gate still governs escapes, paths reaching the Seatbelt profile as -D parameters rather than string interpolation, per-run scratch dir wired as TMPDIR/XDG_CACHE_HOME. Nothing new is invented; claude simply has no OS sandbox of its own in headless mode, exactly like OpenCode.

No machine-specific values live in repo code. The wrapper sources ~/.config/ringer/claude-engine.env if that file exists, and that is where the operator puts ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY. Both modes are documented in the header:

  1. No env file — plain claude auth applies (normal Anthropic API).
  2. Env file present — any Anthropic-compatible /v1/messages endpoint: Ollama, LM Studio, LiteLLM, a self-hosted gateway.

Why

Mode 2 is the point. It gives Ringer its first local-inference lane: a real model doing real work with no API bill and no prompt leaving hardware the operator controls. That is the mock engine's "try Ringer without a bill" purpose, except the worker actually reasons and the executed check is a genuine gate rather than a rehearsal. It also means the {model} placeholder now reaches models with no hosted route at all.

Two empirical findings, and how they were earned

Both are recorded in docs/MODEL-NOTES.md and in the sample block's comments, because both are the kind of thing that costs someone else a day.

Probe target was a 27B qwen3.6 served by Ollama, driven through this engine with claude -p --output-format json. It took two failed probe cycles before the cause was isolated, then a pass:

  • Cycle 1 (no --strict-mcp-config): the worker inherited the operator's entire MCP server roster, made an unrelated tool call, hit a schema error, and abandoned the task — its final answer was an essay on an unrelated subject. Neither required file was written.
  • Cycle 2 (--strict-mcp-config only): MCP tool calls stopped, but that flag does not stop plugin hooks or the always-loaded global ~/.claude/CLAUDE.md. The worker hallucinated a blocker that reads as prior-session context bleeding in, then ran past the 32k output-token ceiling without finishing.
  • Cycle 3 (--bare + --strict-mcp-config): pass on attempt 2 of the same one-task manifest, with an executed check — run the generated script, compare its output and a SHA-256 — confirming the artifact rather than a worker self-report.

So, finding one: headless Claude Code's injected context derails small local models, and --bare is the actual fix. It skips hooks, plugin sync, auto-memory, and CLAUDE.md auto-discovery; --strict-mcp-config stays alongside it belt-and-suspenders, since worker tasks have no business calling the operator's MCP servers either way. Harness overhead drops from 16k–32k input tokens per turn to ~1.2k, but the token saving is a side benefit — the point is that the spec stops competing with injected background.

Finding two: --bare honors ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN even though its --help text describes auth in that mode as strictly ANTHROPIC_API_KEY or apiKeyHelper (verified 2026-07-24, claude CLI on macOS). That help text reads like a hard blocker for alternative-endpoint setups and is not one. Without this, findings one and two look mutually exclusive and the whole lane looks impossible.

Both flags are passed from args_template rather than hardcoded in the wrapper, so a fully-configured Claude Code worker is still possible when that is what you want.

Verification

  • Full suite on this branch: python3 -m unittest discover -s tests218 tests, OK, including tests/test_contributors.py. No failures, nothing skipped around this change.
  • Wrapper smoke test through the env-file path, invoked exactly as the engine block invokes it (<taskdir> -p --output-format json --dangerously-skip-permissions --bare --strict-mcp-config --model <slug> "reply PONG"): returned "result":"PONG" with input_tokens: 1117 — the ~1.2k figure above, measured, not asserted — and "canonicalModel":"qwen3.6:latest".
  • Both auth modes exercised with a stub claude on PATH that prints its environment, so the sourcing logic is proven rather than reviewed: env-file mode passes ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN/empty ANTHROPIC_API_KEY through; with the file absent the wrapper adds nothing and leaves normal claude auth alone. Sandboxed and --no-sandbox paths both checked, and child exit status propagates (stub exiting 42 → wrapper exits 42).
  • The two regexes in the sample block are matched against the real --output-format json payload above, not written from memory.

The Seatbelt profile needed no change for the env file: it is sourced by the wrapper before sandbox-exec is entered, so no read rule is involved.

Notes for review

  • The sample block is commented out and follows the [engines.grok] / [engines.mock] convention — machine-specific bin path, placeholder endpoint host, the caveats in comments next to the setting they explain.
  • README changes are minimal and only what shipping a fourth lane makes stale: the lane count, a short subsection mirroring the Grok one, and the Contributors entry that tests/test_contributors.py requires.
  • Local-endpoint latency/concurrency caveat is stated generically in both the sample block and MODEL-NOTES: one local model on one box serves one request at a time, so give such an engine 1–2 tasks per batch; wider only inflates wall time.
  • Scoped to one feature, rebased on current main (4ac3791), single commit.

🤖 Generated with Claude Code

New engine wrapper engines/claude-sandboxed.sh runs the `claude` CLI as a
headless Ringer worker under the same macOS Seatbelt design as the OpenCode
wrapper. Endpoint config is machine-specific, so it lives outside the repo:
the wrapper sources ~/.config/ringer/claude-engine.env when present, which
lets the engine run against any Anthropic-compatible /v1/messages endpoint
(Ollama, LM Studio, LiteLLM) — a worker lane with no API bill and no prompt
leaving hardware you control. With no env file, plain `claude` auth applies.

The commented [engines.claude] sample block follows the grok/mock convention
and records two findings from probe runs against a 27B qwen3.6 over Ollama:
headless claude's injected context (MCP schemas, plugin hooks, global
CLAUDE.md) derails small local models — `--bare` + `--strict-mcp-config` is
the fix and drops harness overhead from 16k-32k input tokens to ~1.2k; and
`--bare` honors ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN despite its help text
documenting only API-key auth. MODEL-NOTES carries the dated entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant