README: verified quickstart (Docker builder, dry-run, live demo), mermaid
architecture, CLI/config/routing reference tables, status table from
REPORT.md, correct ukrrs repo URLs. docs/: Status front-matter lines on
all PORTING-NOTES (secrets notes now tracked). DESIGN: table of contents
with anchor links; sections untouched.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
10 KiB
Porting notes: crush (reference/crush @ internal/)
Scope for MOPAC harness: MCP client, sessions/resume, provider routing,
headless loop; OAuth deferred (LiteLLM key suffices). Paths relative to
reference/crush/; line refs from study 2026-08-28, drift possible.
Status: 2026-08-28 — complete; reference study, line refs may drift with upstream.
1. MCP plumbing
Code in internal/agent/tools/mcp/ (NOT internal/client/ = daemon RPC).
Uses official SDK modelcontextprotocol/go-sdk/mcp.
Config: crush.json top-level "mcp" map name->MCPConfig
(config/config.go:185-252, :383/:723):
{ "mcp": {
"local": { "type": "stdio", "command": "npx", "args": ["-y","pkg"], "env": {"K":"V"} },
"remote": { "type": "http", "url": "http://h/mcp", "headers": {"Authorization":"Bearer $TOKEN"} },
"legacy": { "type": "sse", "url": "https://h/sse" } } }
type:stdio|sse|http(http=streamable). Extras:disabled,enabled_tools/disabled_tools,timeoutsecs (dflt 10s, 30s OAuth),sessionless,oauth*.$VAR/$(cmd)expansion: config.go:438-528. PORT FORMAT VERBATIM for KNEL-AIMiddleware compat. Transports (mcp/init.go:1035-1195createTransport): stdio ->mcp.CommandTransport; env=os.Environ()+resolved; child in own process group, group SIGKILL + 5s WaitDelay (process_unix.go:22-40). PORT: process-group kill or zombies leak. http ->mcp.StreamableClientTransport; static headers viaheaderRoundTripper(init.go:1197-1206). sse ->SSEClientTransport(legacy, keep for compat). Health/restart:getOrRenewClient(init.go:659-771) — Ping fast path; on failure teardown->reconnect->re-register tools. Per-server renewal mutex + generation counters discard stale connects. Config-change reconcile single-flighted (mcp/lifecycle.go:45-125). Init: per-server goroutines + WaitGroup;WaitForInitgates NON-interactive runs only (headless waits, TUI proceeds lazily). Tool registration: discovery viaListToolson connect + list-changed subscription (mcp/tools.go:147-169). Namingmcp_<server>_<tool>SINGLE underscores (agent/tools/mcp-tools.go:58-60). Adapter maps JSON-Schema properties/required -> ToolInfo (mcp-tools.go:70-97); results map text/image/audio, base64-normalized (tools.go:54-108). Routing: RunTool -> client lookup -> CallTool; auto-renews first. Aggregation: per-run palette = built-ins +GetMCPTools, filtered per-agent by AllowedMCP map (agent/coordinator.go:768-790). Go port: adoptgo-sdk/mcp; copy config schema, naming convention, renewal-with-generations, WaitForInit gate. Skip: Claude "channel" push hack (mcp/channel.go), docker-mcp gateway (config/docker_mcp.go).
2. Session/persistence
Single SQLite DB {dataDir}/crush.db (default .crush/ per project,
discovered cwd-up-to-git-root, config/load.go:574-582). Sibling
crush.lock flock + owner JSON stops concurrent processes
(db/datadirlock.go). Schema (db/migrations/, goose, sqlc queries in
db/sql/): sessions(uuid, parent_id, title, message_count, tokens, cost, todos JSON, summary_message_id), messages(id, session_id, role, parts JSON, model, provider, finished_at), files (snapshots), read_files
(tracker). Parts = {"type","data"} array (message/content.go:56-143).
Pragmas WAL+busy_timeout=30s; SetMaxOpenConns(1) avoids SQLITE_NOTADB
(db/connect.go:18-41,137-142). Short user ID = XXH3 hex of UUID
(session/session.go:28-32).
-C/--continue vs -s/--session (mutually exclusive, cmd/root.go:63):
Continue = GetLast ORDER BY updated_at DESC skipping agent-tool/child
sessions (app/app.go:238-263). ID resolve: exact UUID, else XXH3 prefix
scan; 0 hits=not found, >1=ambiguous (root.go:941-967). Resume restores
messages+parts, file snapshots, read-files, model from last assistant msg;
NOT restored: permissions, LSP/MCP (re-init).
Crash safety: 33ms debounced writes, sync flush on terminal events
(message/message.go:218-302); read-time repair in agent/agent.go:1527-1708
— drops orphaned tool results, SYNTHESIZES results for orphaned tool_calls
(interrupted sessions still pass API validation), skips empty cancelled msgs.
PORT THIS — it is what makes resume robust. Harness v1 (bounded turns):
JSONL per session is acceptable; what matters is the repair pass and
finished_at-only-on-Finish-part semantics.
3. Provider layer
No in-house LLM client: calls delegate to external charm.land/fantasy;
catalog from charm.land/catwalk. Neither vendored here — harness owns
this layer against LiteLLM (openai-compat): port the CONFIG CONTRACT, not code.
- Catalog: embedded + remote fetch (catwalk.charm.land, ETag) w/ fallback
remote->cache->embedded (
config/catwalk.go:37-90,provider.go:37-90; cache~/.local/share/crush/providers.json, atomic write). Harness: static embedded catalog, no remote sync. - Layering low->high (
config/load.go:926-965): /etc/crush -> ~/.config/crush -> ~/.local/share/crush -> project walk (.crushrc|crushrc| .crush.json|crush.json, cwd up to worktree root) -> workspace dataDir copy LAST. Deep-merge via go-jsons. - Catalog x user (
load.go:213-365):providers.<id>overrides base_url/api_key/extra_headers; usermodels[]PREPENDED, wins by ID;disable_default_providersnukes catalog. Routing:parseModelStr(app/provider.go:15-27) — first/-segment vs provider IDs, else whole string = model ID; ambiguous -> demandprovider/model. Defaults from catalog DefaultLarge/SmallModelID (load.go:716-789); per-model overrides (max_tokens/temperature/reasoning)load.go:826-899; provider switch on Typeagent/coordinator.go:806-1161. - Retries: INSIDE fantasy, not this repo. Crush only observes:
OnRetryresets streamed content so retry doesn't concatenate (agent/agent.go:931-941);OnAuthRefresh= transparent 401 retry (:127). PORT: harness-layer retry (429/5xx/transport backoff) + reset partial assistant buffer first. Stream-truncation surfacing (z.ai "Message content shorter than read bytes"): stream errors classified atagent/agent.go:1067-1189;IsTransportError-> FinishReasonError w/ wrapped err (:1177) — unexpected EOF lands here. In headless, the z.ai-class error is DETECTED atapp/app.go:414-416(reconciles streamed bytes vs final msg, fails the run, exit 1). Zero-usage fallback:agent/usage_fallback.go. Harness v1: on EOF/short-read, retry request (per DESIGN retry/backoff) and resume turn from persisted messages; reuse delta-cursor print (app.go:406-431).
4. Headless mode
No -p/--print exists. Headless = crush run|r [prompt...]
(cmd/run.go:31-169). Prompt = args joined + stdin (pipe/file, not TTY)
PREPENDED (root.go:918-935); empty -> error. Output: plain markdown to
stdout; spinner only if stderr is TTY. Flags: -q, -v, -m provider/model,
--small-model, -s, -C. Two paths: in-process (app.RunNonInteractive,
app/app.go:267-438) or client/server SSE (run.go:334-455). Port:
- Delta printing: track messageReadBytes per msg id, print only
content[readBytes:](app.go:406-431). Server path keys completion on RunComplete event by runID, NOT message finish (tool_use mid-turn regression, run.go:385-397). - Permissions headless: session auto-approved wholesale
(
AutoApproveSessionapp.go:349; permission.go:280-284) — never blocks; interactive-only tools excluded (initCoderAgent(false)app.go:674-703). Harness: invert — per-vertical allowlist, deny-default, no auto-approve. - Exit codes: 0/1 only. 1 = RunE error (no prompt, no providers, agent error, run failed); CANCEL returns 0 (app.go:398-401); tool failures do NOT affect exit. Harness: distinct codes gate/budget/agent/tool.
5. OAuth (deferred v1 — code map)
- Hyper device-code flow:
oauth/hyper/device.go(:40 initiate, :87 poll, :153 refresh-exchange). Copilot device flow + GH->Copilot token exchange:oauth/copilot/oauth.go(:36,:69,:149), disk importcopilot/disk.go, spoof headerscopilot/http.go. Login CLI:cmd/login.go,cmd/logout.go. - MCP OAuth 2.1 auth-code+PKCE, dynamic client registration (RFC 7591),
PR metadata (8414/8707):
oauth/mcp/handler.go:110-219. Loopback/callback, ports 40704-40713 first-free (pin viaoauth_callback_port); listener bound only during in-flight auth (:487); concurrent 401s deduped to one browser tab (authFlight:420). Tokens -> config keymcp.<name>.oauth_token; save-on-change wrapperoauth/mcp/savingtokensource.go. Browser suppressed non-interactive (ErrInteractiveAuthRequiredhandler.go:32). If harness needs OAuth, port THIS flow (MCP); provider OAuth moot behind LiteLLM. Token plumbing: access token stored ASproviders.<id>.api_key(config/store.go:564-595); refresh singleflight + cross-process flock<datadir>/locks/<provider>.refresh.lock, adopts peer-rotated token (store.go:658-903). Token modeloauth/token.go. Anthropic Claude- subscription OAuth: REMOVED (config/load.go:306-314).
6. Reading list + gotchas
Top 10 to read in detail during build:
internal/agent/tools/mcp/init.go(1332 ln) — sessions, transports, renewal; MCP port blueprint.internal/agent/tools/mcp-tools.go+mcp/tools.go— tool adapter, naming, filtering, result mapping.internal/agent/agent.go(2288 ln) — stream callbacks, error classification, prompt repair (:1527-1708).internal/app/app.go— RunNonInteractive, resolveSession, delta print.internal/cmd/run.go+cmd/root.go— headless flags, stdin handling, config discovery walk.internal/config/load.go— config layering/merge, model resolution.internal/message/message.go+content.go— debounced persistence, part model.internal/session/session.go+db/sql/— session service + queries.internal/permission/permission.go— precedence chain (skip -> allowed_tools -> hook -> auto-approve -> interactive).internal/agent/coordinator.go— tool palette assembly, provider construction, AllowedMCP filtering. Gotchas:internal/client/is daemon RPC, not LLM/MCP — don't port by name. LLM lives in externalcharm.land/fantasy(not vendored here).- MCP tool names use SINGLE underscores (
mcp_server_tool); double- underscore assumption breaks KNEL compat. - stdio MCP children need own process group + group kill; plain Kill
orphans grandchildren (
process_unix.go). - Retries concatenate partial stream output unless the assistant buffer
is reset first (
agent.go:931-941OnRetry pattern). - Resume correctness lives in read-time repair (orphaned tool_call/result synthesis), not write-time transactions — skipping it makes every interrupted session permanently API-invalid.