feat(ai): Claude Code provider — run the CMS agent on a Claude subscription - #577
Open
cakelesscoder wants to merge 4 commits into
Open
cakelesscoder wants to merge 4 commits into
cakelesscoder wants to merge 4 commits into
Conversation
Runs the CMS agent on the machine's Claude subscription instead of a metered API key. The driver spawns the headless `claude` CLI, points it at Instatic's own MCP server, and streams its stream-json output back into the chat — so the agent is real Claude Code editing the live workspace through the same editor bridge the built-in agent uses. It is the deliberate exception to the SDK ban and compliant by construction: it imports nothing and spawns a binary. It is also the only option that works, since the subscription cost model exists solely in the CLI auth path — the Agent SDK bills API credits, which would defeat the purpose. An internal MCP connector mints a per-user bearer token granted exactly the chatting user's own capabilities, so the spawned CLI can do precisely what that user could through the agent panel, never more. Three modules by responsibility: what to ask for (claudeCode.ts), how to run it (claudeCodeProcess.ts), how to read what came back (claudeCodeEvents.ts). The last is pure and synchronous, so the translation layer is unit-testable from recorded CLI output — which is what the new tests do. A subprocess has many more ways to go quiet than an HTTP call, and all of them look identical from the composer: a few seconds of output, then nothing. Each is terminal and named — a toolless turn, a wedged child, a session mismatch, a rate limit, CLI format drift, an abandoned stream. The toolless case is the subtle one: `--tools ''` strips built-ins, but the CLI defers a large MCP toolset behind the `ToolSearch` built-in, so the combination leaves the model with zero callable tools. It then narrates a tool call in prose and ends the turn `success`. ENABLE_TOOL_SEARCH=false fixes the cause; the init tool count is checked anyway, so a future CLI that ignores the env var fails loudly instead of bluffing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude Code authenticates outside Instatic, so its connect form has no secret to collect — but the flow assumed every provider hands over either an API key or an endpoint URL. Rather than special-case one provider id in ProvidersTab, make the idea explicit in the provider catalogue: `credentialLess` says the provider brings its own auth, `credentialLessHint` is shown where the inputs would have been, and `sentinelBaseUrl` is the inert value stored on the row so it still satisfies the API's auth-shape check. The form and the detail panel read those fields and know nothing about Claude Code specifically, so any future provider that authenticates elsewhere gets the same treatment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The base image has no `claude` binary. Dockerfile.claude-code overlays it with the self-contained installer output at /opt/claude/claude and bakes INSTATIC_CLAUDE_BIN, kept separate from the main Dockerfile so the base image keeps tracking upstream cleanly. compose.claude-code.yml supplies subscription auth as CLAUDE_CODE_OAUTH_TOKEN (from `claude setup-token`). No home-directory mount, and no API key: the driver strips ANTHROPIC_API_KEY but preserves this var, so the CLI cannot silently fall back to metered billing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Architecture and request flow, why a subprocess rather than the Agent SDK, setup for both bare-metal and Docker, the tool-visibility constraint that makes ENABLE_TOOL_SEARCH=false load-bearing, the failure modes the driver now names, and the scope limits — single-operator, subscription rate limits, and auth being machine-wide rather than per user. Also wires it into the existing docs: the feature appears in the documentation map, and agent.md no longer claims that *every* driver talks to a REST API over HTTP/SSE — it names the one deliberate exception and lists the three new driver modules alongside the HTTP ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cakelesscoder
force-pushed
the
pr/claude-code-provider
branch
from
October 1, 2026 01:17
ac403e3 to
38b3e27
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a Claude Code AI provider that runs the CMS agent on the operator's Claude subscription (Pro/Max) instead of a metered Anthropic API key.
You type in the normal AI panel. Behind the scenes the driver spawns the headless
claudeCLI, points it at Instatic's own MCP server, and streams its output back into the chat — so the agent is real Claude Code (its tool loop, its harness) editing the live workspace you have open. This is the inverse of connecting an external Claude Code to Instatic over MCP connectors: here Instatic drives Claude Code, but editing still flows through the same live editor bridge.Why a subprocess, not an SDK.
ai-driver-isolation.test.tsbans every provider SDK. This driver is compliant by construction — it imports nothing and spawns a binary. That is also the only thing that works: the subscription cost model exists solely in the CLI/subscription auth path, and the Agent SDK bills API credits, which would defeat the purpose.Layout. Three modules by responsibility, each comfortably inside the 700-line budget — no
GRANDFATHEREDentry:claudeCode.tsclaudeCodeProcess.tsclaudeCodeEvents.tsclaudeCodeEvents.tsis pure and synchronous (no subprocess, no IO), which is what makes the translation layer testable from recorded CLI output.Auth is the machine's Claude subscription.
ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKENare stripped from the child env so the CLI can't silently fall back to metered billing, and--bareis never used (it forces API-key auth).Capabilities. An internal MCP connector mints a per-user bearer token granted exactly the chatting user's own capabilities, so the spawned CLI can do precisely what that user could through the built-in agent, never more.
Tool surface. The CLI sees only this server's
mcp__instatic__*tools — no Bash, Edit or WebFetch in the CMS chat.Credential-less. The provider needs no secret, but a conversation must reference a credential row, so it stores an inert
baseUrlsentinel (claude-code://local) the driver never dials. This satisfies the existingai_creds_apikey_shape_checkwithout a migration.The part worth reviewing closely
--tools ""strips built-ins. The CLI's tool search (ENABLE_TOOL_SEARCH, defaultauto) defers a large MCP toolset behind theToolSearchbuilt-in — which--tools ""strips. With ~50 tools this server crosses the deferral threshold, so the combination leaves the model with zero callable tools.A model with no tools doesn't error. It narrates a tool call in prose (
`get_context` … calling that now) and ends the turnsubtype: "success". From the composer that is indistinguishable from the agent just stopping a few seconds in.The driver sets
ENABLE_TOOL_SEARCH=false, which presents all tools directly and is also faster (~5s to first tool call vs ~13–19s through search round trips). Because that's a CLI default the integration now depends on, it is also checked: the driver reads the tool list from the CLI'ssystem/initevent and aborts the turn with a clear error if nomcp__instatic__*tool is present, rather than letting the model bluff.The same principle covers the other ways a subprocess goes quiet — each terminal and named rather than silent:
--resumeof a missing session /--session-idof an existing one each retry once as the other mode — safe because the failed attempt emitted nothingEach turn logs
[ai/claude-code] session <id> started — N CMS tool(s), which is the first thing to check if the agent misbehaves.Docker
Dockerfile.claude-codeoverlays the base image with the self-containedclaudebinary;compose.claude-code.ymlsupplies a subscription token viaCLAUDE_CODE_OAUTH_TOKEN. No home-directory mount.Verification
bun run buildbun testbun run lintsrc/__tests__/ai/claudeCodeMapping.test.ts— 14 tests over the stream-json translation: text deltas, thinking suppression, tool-call/result pairing and prefix stripping, the init tool-count guard and MCP server status, rate-limit classification, usage/context mapping, failing-result capture, and unreadable-line counting.Checklist
docs/features/claude-code-provider.mdcovers the architecture, setup (bare-metal and Docker), the tool-visibility constraint, and the limitations below. It's listed in the documentation map, andagent.mdno longer claims that every driver talks to a REST API over HTTP/SSE — it names the exception and lists the three new driver modules.Scope and caveats
auth status --jsonand an OAuth flow that proxies cleanly through a browser — if that's something you'd want in-tree.I'm running this in production against a live site, and I'm happy to adjust anything here, including dropping it if a CLI-spawning provider isn't a direction you want to take.