@askalf/dario 3.28.0 → 3.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
2
  <h1 align="center">dario</h1>
3
- <p align="center"><strong>A universal LLM router that runs on your machine.<br>One local endpoint, every provider — Anthropic, OpenAI, Groq, OpenRouter, Ollama, any OpenAI-compat URL. Your tools point here and stop caring which vendor is upstream.</strong></p>
3
+ <p align="center"><strong>A universal LLM router that runs on your machine.<br>One local endpoint, every provider — Anthropic, OpenAI, Groq, OpenRouter, Ollama, any OpenAI-compat URL. Point your tools at localhost and stop caring which vendor is upstream.</strong></p>
4
4
  </p>
5
5
 
6
6
  <p align="center">
@@ -26,7 +26,7 @@ One command, one local URL, every provider behind it. Point `ANTHROPIC_BASE_URL`
26
26
  - `llama-3.3-70b`, `deepseek-v3`, anything else → **Groq**, **OpenRouter**, **local LiteLLM**, **vLLM**, **Ollama**, whichever OpenAI-compat backend you wired up
27
27
  - Force a backend explicitly with a prefix: `openai:gpt-4o`, `groq:llama-3.3-70b`, `local:qwen-coder`, `claude:opus`
28
28
 
29
- Switching providers is a **model-name change** in your tool. Not a reconfigure. Not new base URLs. Not new API keys. Not a new SDK import. **Zero runtime dependencies. ~8,100 lines of TypeScript across ~15 files. ~840 assertions across 24 test suites. [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) on every release. Nothing phones home, ever.**
29
+ Switching providers is a **model-name change** in your tool. Not a reconfigure. Not new base URLs. Not new API keys. Not a new SDK import. **Zero runtime dependencies. ~11,300 lines of TypeScript across ~25 files. ~1,250 assertions across 32 test suites. [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) on every release. Nothing phones home, ever.**
30
30
 
31
31
  ---
32
32
 
@@ -44,6 +44,8 @@ You point every tool at one URL. Dario reads each request, decides which backend
44
44
 
45
45
  The tool doesn't know. The backend doesn't know. Dario is the seam.
46
46
 
47
+ Beyond routing, the Claude backend is a **full wire-level Claude Code replay** — every observable axis (bytes, headers, body key order, TLS stack, inter-request timing, session-id lifecycle, stream-consumption shape) is captured from your installed CC binary and replayed on outbound requests so Anthropic's classifier sees a CC session. See [Claude subscription backend](#2-claude-subscription-backend) and [Fingerprint axes](#fingerprint-axes).
48
+
47
49
  ---
48
50
 
49
51
  ## Quick start
@@ -57,6 +59,8 @@ npm install -g @askalf/dario
57
59
  # 1. Claude via your Claude Max / Pro subscription (uses your Claude Code
58
60
  # OAuth if CC is installed; runs its own OAuth flow otherwise)
59
61
  dario login
62
+ # or, for SSH / container setups with no browser:
63
+ dario login --manual
60
64
 
61
65
  # 2. OpenAI or any OpenAI-compat endpoint
62
66
  dario backend add openai --key=sk-proj-...
@@ -76,25 +80,29 @@ export OPENAI_API_KEY=dario
76
80
 
77
81
  That's it. Every tool that honors these standard env vars now reaches every backend you configured. No per-tool reconfiguration. No SDK changes. One URL, one fake key, every real provider behind it.
78
82
 
79
- Something broken? `dario doctor` prints a single aggregated health report — dario version, Node, platform, CC binary compat, template source + age + drift, OAuth status, pool state, configured backends. Paste that instead of screenshots when you file an issue.
83
+ Something broken? `dario doctor` prints a single aggregated health report — dario version, Node, platform, runtime/TLS classification, CC binary compat, template source + age + drift, OAuth status, pool state, configured backends, sub-agent install state. Paste that instead of screenshots when you file an issue.
80
84
 
81
85
  ---
82
86
 
83
87
  ## Why you'll install this
84
88
 
85
- **You want one URL for every provider.** Cursor, Aider, Continue, Zed, OpenHands, Claude Code, your own scripts — every tool you own has its own per-provider config. Dario collapses that into a single `localhost:3456` that speaks both Anthropic and OpenAI protocols and routes by model name. Switching providers is a model-name change in your tool, not a reconfigure of every SDK on your laptop.
89
+ **You want one URL for every provider.** Cursor, Aider, Continue, Zed, OpenHands, Claude Code, your own scripts — every tool you own has its own per-provider config. Dario collapses that into a single `localhost:3456` that speaks both Anthropic and OpenAI protocols and routes by model name.
86
90
 
87
- **You pay for Claude Max but only use it in Claude Code.** Cursor, Aider, Zed, Continue — they all want API keys and bill per-token while your $200/mo subscription sits idle. Dario's Claude backend routes requests from all of them through your plan by replaying the exact Claude Code wire shape (template, tools, headers, billing tag) that Anthropic's classifier expects for subscription billing. Section [Claude subscription backend](#2-claude-subscription-backend) has the full mechanics.
91
+ **You pay for Claude Max but only use it in Claude Code.** Cursor, Aider, Zed, Continue — they all want API keys and bill per-token while your $200/mo subscription sits idle. Dario's Claude backend routes requests from all of them through your plan by replaying the exact Claude Code wire shape (template, tools, headers, body key order, billing tag) that Anthropic's classifier expects for subscription billing. See [Claude subscription backend](#2-claude-subscription-backend).
88
92
 
89
- **You hit rate limits on long agent runs.** Add a second / third Claude subscription with `dario accounts add work` and pool mode routes each request to whichever account has the most headroom. **Session stickiness** (v3.13.0) pins a multi-turn conversation to one account so the Anthropic prompt cache survives the run. **In-flight 429 failover** retries the same request against a different account before your client sees an error. See [Multi-account pool mode](#multi-account-pool-mode).
93
+ **You hit rate limits on long agent runs.** Add a second / third Claude subscription with `dario accounts add work` and pool mode routes each request to whichever account has the most headroom. **Session stickiness** pins a multi-turn conversation to one account so the Anthropic prompt cache survives the run. **In-flight 429 failover** retries the same request against a different account before your client sees an error. See [Multi-account pool mode](#multi-account-pool-mode).
90
94
 
91
95
  **You run a coding agent that isn't Claude Code.** Cline, Roo Code, Cursor, Windsurf, Continue.dev, GitHub Copilot, OpenHands, OpenClaw, Hermes — they each ship their own tool schemas and their own validators. Dario's universal `TOOL_MAP` (**~66 schema-verified entries**) pre-maps every major coding agent's tool names to Claude Code's native set on the outbound path and rebuilds to your agent's exact expected shape on the inbound path. No `--preserve-tools`, no fingerprint loss, no validator errors. See [Agent compatibility](#agent-compatibility).
92
96
 
93
- **You want the proxy layer off the wire entirely.** **Shim mode** (v3.12, hardened in v3.13) is an in-process `globalThis.fetch` patch injected via `NODE_OPTIONS=--require`. No HTTP hop, no port to bind, no `BASE_URL` to set. `dario shim -- claude --print "hi"` and CC thinks it's talking directly to `api.anthropic.com`. See [Shim mode](#shim-mode).
97
+ **You want the proxy layer off the wire entirely.** **Shim mode** is an in-process `globalThis.fetch` patch injected via `NODE_OPTIONS=--require`. No HTTP hop, no port to bind, no `BASE_URL` to set. `dario shim -- claude --print "hi"` and CC thinks it's talking directly to `api.anthropic.com`. See [Shim mode](#shim-mode).
98
+
99
+ **You want dario itself addressable from inside Claude Code or any MCP client.** `dario subagent install` registers a first-party sub-agent under `~/.claude/agents/dario.md` so CC can delegate diagnostics and template-refresh in-session ([Claude Code sub-agent hook](#claude-code-sub-agent-hook-v326)). `dario mcp` turns dario itself into a read-only MCP server — Claude Desktop, Cursor, Zed, any MCP-aware editor can introspect dario's state (auth, pool, backends, template, fingerprint, runtime) without leaving the editor ([dario as MCP server](#dario-as-mcp-server-v327)).
100
+
101
+ **You want to share capacity with a trusted group without surveilling each other.** The **sealed-sender overflow protocol** uses RSA blind signatures (Chaum 1983, implemented from scratch over Node's `crypto`) so members of a trust group can lend unused Claude capacity to each other with cryptographic unlinkability. Dario ships the primitive; [mux](https://github.com/askalf/mux) is the dedicated product around it. See [Sealed-sender overflow](#sealed-sender-overflow-protocol).
94
102
 
95
- **You want to share capacity with a trusted group without surveilling each other.** The **sealed-sender overflow protocol** (v3.13) uses RSA blind signatures (Chaum 1983, implemented from scratch over Node's `crypto`) so members of a trust group can lend unused Claude capacity to each other with cryptographic unlinkability. A lender verifies "this is a valid group member" without learning *which* member. Dario ships the primitive; [mux](https://github.com/askalf/mux) is the dedicated product around it (group admin, key distribution, member workflow, borrower CLI). See [Sealed-sender overflow](#sealed-sender-overflow-protocol).
103
+ **You want certainty that the proxy isn't trivially fingerprintable.** The "get ahead of Anthropic" release track (v3.22 v3.28) closed six observable divergence axes between dario and real Claude Code: body field order (v3.22), TLS ClientHello (v3.23), inter-request timing (v3.24), stream-consumption shape (v3.25), sub-agent/MCP reach (v3.26/v3.27), and session-id lifecycle (v3.28). See [Fingerprint axes](#fingerprint-axes).
96
104
 
97
- **You want to actually audit the thing.** ~7,600 lines of TypeScript across ~15 files. Zero runtime dependencies (`npm ls --production` confirms). Credentials at `~/.dario/` with `0600` permissions. `127.0.0.1`-only by default. Every release [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions. Nothing phones home. Small enough to read in a weekend.
105
+ **You want to actually audit the thing.** ~11,300 lines of TypeScript across ~25 files. Zero runtime dependencies (`npm ls --production` confirms). Credentials at `~/.dario/` with `0600` permissions. `127.0.0.1`-only by default. Every release [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions. Nothing phones home. Small enough to read in a weekend.
98
106
 
99
107
  ---
100
108
 
@@ -107,6 +115,7 @@ Something broken? `dario doctor` prints a single aggregated health report — da
107
115
  - **Anyone building AI coding tools** who wants provider independence without writing an OpenAI ↔ Anthropic translator themselves.
108
116
  - **Claude Max / Pro subscribers** who want their subscription usable from every tool on their machine, not just Claude Code.
109
117
  - **Power users on multi-agent workloads** who want multi-account pooling, session stickiness, and in-flight 429 failover on their own machine, against their own subscriptions.
118
+ - **Operators who care about wire-level fidelity** — the fingerprint tightening in v3.22 – v3.28 means proxy mode's divergence from CC is observable (via `dario doctor`) and tunable (flags + env vars for each axis).
110
119
 
111
120
  **Not a fit if:**
112
121
 
@@ -154,26 +163,43 @@ Force a backend with a **provider prefix** on the model field (`openai:gpt-4o`,
154
163
 
155
164
  ### 2. Claude subscription backend
156
165
 
157
- OAuth-backed Claude Max / Pro, billed against your plan instead of the API. Activated by `dario login`.
166
+ OAuth-backed Claude Max / Pro, billed against your plan instead of the API. Activated by `dario login` (or `dario login --manual` for SSH / container setups without a browser, v3.20).
158
167
 
159
- **What it does.** Every outbound Claude request is rebuilt to look exactly like a request Claude Code itself would make — system prompt, tool definitions, fingerprint headers, billing tag, beta flags, **even the exact header insertion order and request-body key order** — using a live-extracted template from your actually-installed CC binary that self-heals on every Anthropic release. Anthropic's classifier sees a CC session because, from the wire up, it *is* one. That's what keeps your usage on subscription billing instead of API overage.
168
+ **What it does.** Every outbound Claude request is rebuilt to look exactly like a request Claude Code itself would make — system prompt, tool definitions, fingerprint headers, billing tag, beta flags, **header insertion order, static header values, `anthropic-beta` flag set, and top-level request-body key order** — using a live-extracted template from your actually-installed CC binary that self-heals on every Anthropic release. Anthropic's classifier sees a CC session because, from the wire up, it *is* one. That's what keeps your usage on subscription billing instead of API overage.
160
169
 
161
170
  **Key mechanisms:**
162
171
 
163
- - **Live fingerprint extraction** (v3.11). Dario spawns your installed `claude` binary against a loopback MITM endpoint on startup, captures its outbound request, and extracts the live template (system prompt, tools, user-agent, beta flags, **header insertion order** as of v3.13 replayed by the shim since v3.13 and the proxy since v3.16, **static header values** + **`anthropic-beta` flags** as of v3.19, and **top-level request-body key order** as of v3.22). Eliminates the "Anthropic ships a new CC, dario is stale for 48 hours" window. Cached at `~/.dario/cc-template.live.json` with a 24h TTL. Falls back to the bundled snapshot if CC isn't installed; the bundled snapshot is scrubbed of host-identifying paths at bake time (v3.21).
172
+ - **Live fingerprint extraction.** Dario spawns your installed `claude` binary against a loopback MITM endpoint on startup, captures its outbound request, and extracts the live template system prompt, tools, user-agent, beta flags, **header insertion order** (replayed by the shim since v3.13 and the proxy since v3.16), **static header values** and **`anthropic-beta` flag set** (v3.19), and **top-level request-body key order** (v3.22, schema v3). Eliminates the "Anthropic ships a new CC, dario is stale for 48 hours" window. Cached at `~/.dario/cc-template.live.json` with a 24h TTL. Falls back to the bundled snapshot if CC isn't installed; the bundled snapshot is scrubbed of host-identifying paths and `mcp__*` tool names at bake time (v3.21 — see `src/scrub-template.ts`).
164
173
  - **Drift detection** (v3.17). On startup dario probes the installed `claude` binary and compares against the captured template. Mismatch triggers a forced refresh and prints a one-line warning. Users never silently sit on a stale template again.
165
- - **Compat matrix** (v3.17). `SUPPORTED_CC_RANGE = { min: "1.0.0", maxTested: "2.1.104" }` is encoded in code. Installed CC outside that band prints a warn (untested above) or fail (below min) — zero-dep dotted-numeric comparator, no `semver` import per the dep policy.
174
+ - **Compat matrix** (v3.17, bumped in v3.19.5). `SUPPORTED_CC_RANGE` is encoded in code; installed CC outside the band prints a warn (untested above) or fail (below min) — zero-dep dotted-numeric comparator, no `semver` import per the dep policy.
166
175
  - **Billing tag** reconstructed using CC's own algorithm: `x-anthropic-billing-header: cc_version=<version>.<build_tag>; cc_entrypoint=cli; cch=<5-char-hex>;` where `build_tag = SHA-256(seed + chars[4,7,20] of user message + version).slice(0,3)`.
167
- - **OAuth config auto-detection** from the installed CC binary. When Anthropic rotates `client_id`, authorize URL, or scopes, dario picks up the new values on the next run without needing a release.
176
+ - **OAuth config auto-detection** from the installed CC binary. When Anthropic rotates `client_id`, authorize URL, or scopes, dario picks up the new values on the next run without needing a release. Cache at `~/.dario/cc-oauth-cache-v4.json`, keyed by the CC binary fingerprint.
168
177
  - **Multi-account pool mode** — see [Multi-account pool mode](#multi-account-pool-mode). Automatic when 2+ accounts are configured.
169
178
  - **Framework scrubbing** — known fingerprint tokens (`OpenClaw`, `sessions_*` prefixes, orchestration tags) stripped from system prompt and message content before the request leaves your machine.
170
179
  - **Atomic cache writes + cache corruption recovery** (v3.17). Template cache writes go through pid-qualified `.tmp` + `rename`, so an OS crash mid-write doesn't leave a half-written file. Unparseable cache files get quarantined to `cc-template.live.json.bad-<timestamp>` and dario self-heals on the next capture.
171
180
  - **OAuth single-flight** (v3.17). Two concurrent refreshes for the same account alias now share one outbound `POST /oauth/token`, so the pool's background refresh timer and a user-triggered request at the same millisecond can't race and invalidate each other's refresh token.
172
- - **Bun auto-relaunch** when Bun is installed, dario relaunches under it so the TLS fingerprint matches CC's runtime. Without Bun, dario runs on Node.js.
181
+ - **Bun auto-relaunch.** When Bun is installed, dario relaunches under it so the TLS ClientHello matches CC's runtime (Bun uses BoringSSL; Node uses OpenSSL — distinct JA3/JA4 hashes). Without Bun, dario runs on Node.js — `dario doctor` surfaces the mismatch as of v3.23 and `--strict-tls` refuses to start proxy mode until it's resolved.
173
182
 
174
183
  **Passthrough mode** (`dario proxy --passthrough`) does an OAuth swap and nothing else — no template, no identity, no scrubbing. Use it when the upstream tool already builds a Claude-Code-shaped request on its own.
175
184
 
176
- **Detection scope.** The Claude backend is a per-request layer. Template replay and scrubbing are designed to be indistinguishable from CC at the request level. What they *cannot* defend against is Anthropic's session-level behavioral classifier, which operates on cumulative per-OAuth aggregates (token throughput, conversation depth, streaming duration, inter-arrival timing). The practical answer is **pool mode** distribute load across multiple subscriptions so no single account accumulates enough signal to trip anything.
185
+ **Detection scope.** The Claude backend is a per-request layer. Template replay and scrubbing are designed to be indistinguishable from CC at the request level. What they *cannot* defend against on their own is Anthropic's session-level behavioral classifier, which operates on cumulative per-OAuth aggregates. The v3.22 – v3.28 "get ahead of Anthropic" track closed six of those cumulative axes (body order, TLS, pacing, stream-drain, session-id lifecycle, MCP/sub-agent surface); for anything left, **pool mode** distributes load across multiple subscriptions so no single account accumulates enough signal to trip anything.
186
+
187
+ ---
188
+
189
+ ## Fingerprint axes
190
+
191
+ Between v3.22 and v3.28, dario's Claude backend closed six axes along which a proxy can look different from real Claude Code. Each is a separate knob, each ships with its own test suite, each is surfaced through `dario doctor` where the axis has something to report. Defaults are chosen so existing setups don't regress.
192
+
193
+ | Axis | Release | What it does | How to tune |
194
+ |---|---|---|---|
195
+ | **Request body key order** | v3.22 | Top-level JSON key order of the outbound `/v1/messages` body is captured from CC's wire serialization and replayed byte-for-byte. Schema bumped v2 → v3; stale caches quarantined. | Automatic once a live capture exists. The baked fallback carries a v2.1.112 snapshot. |
196
+ | **Runtime / TLS ClientHello** | v3.23 | Classifies the runtime as `bun-match` / `bun-bypassed` / `node-only` and surfaces the class + hint in `dario doctor`. Bun yields the BoringSSL ClientHello CC presents; Node yields OpenSSL's (distinct JA3). | `--strict-tls` (or `DARIO_STRICT_TLS=1`) refuses to start proxy mode unless `bun-match`. `DARIO_QUIET_TLS=1` silences the startup banner in known-fine environments. |
197
+ | **Inter-request timing** | v3.24 | Replaces the hardcoded 500 ms floor with a configurable floor + uniform jitter. A 500 ms minimum-inter-arrival edge is fingerprintable at scale; jitter dissolves the edge. | `--pace-min=MS`, `--pace-jitter=MS`, or `DARIO_PACE_MIN_MS` / `DARIO_PACE_JITTER_MS`. Legacy `DARIO_MIN_INTERVAL_MS` still honored. |
198
+ | **Stream-consumption shape** | v3.25 | When a downstream client disconnects mid-stream, CC keeps reading SSE to EOF. Dario now offers the same: drain upstream to completion even when the consumer has left. Default off — don't silently burn tokens. | `--drain-on-close` / `DARIO_DRAIN_ON_CLOSE=1`. Bounded by the existing 5-minute upstream timeout. |
199
+ | **Session-ID lifecycle** | v3.28 | Generalizes the v3.19 hardcoded 15-minute idle rotation into a tunable `SessionRegistry` with jitter, max-age, and per-client bucketing. Fixes a v3.27 body/header rotation race as a side effect. | `--session-idle-rotate=MS` (default 900000), `--session-rotate-jitter=MS`, `--session-max-age=MS`, `--session-per-client`. Env mirrors `DARIO_SESSION_*`. Defaults are bit-identical to v3.27. |
200
+ | **MCP / sub-agent reach** | v3.26 + v3.27 | Not a wire axis — a *surface* axis. CC-aware tools can now address dario directly (sub-agent from inside CC, MCP server for any MCP client), so operators don't have to switch terminals to introspect the proxy. Read-only by design. | `dario subagent install` / `dario mcp`. See dedicated sections below. |
201
+
202
+ The six-direction "get ahead of Anthropic" roadmap is complete. Subsequent releases return to responding to issues and upstream template drift.
177
203
 
178
204
  ---
179
205
 
@@ -197,7 +223,7 @@ headroom = 1 - max(util_5h, util_7d)
197
223
 
198
224
  The response's `anthropic-ratelimit-unified-*` headers are parsed back into the pool so the next selection sees fresh utilization. An account that returns a 429 is marked `rejected` and routed around until its window resets. When every account is exhausted, requests queue for up to 60 seconds waiting for headroom to reappear. Plans can mix freely — Max and Pro accounts sit in the same pool; dario doesn't care about tier, only headroom.
199
225
 
200
- ### Session stickiness (v3.13)
226
+ ### Session stickiness
201
227
 
202
228
  Multi-turn agent sessions pin to one account for the life of the conversation, so the Anthropic prompt cache isn't destroyed by account rotation between turns.
203
229
 
@@ -205,7 +231,7 @@ Multi-turn agent sessions pin to one account for the life of the conversation, s
205
231
 
206
232
  **The fix.** Dario hashes a conversation's first user message into a 16-hex-char `stickyKey` (SHA-256 truncated, deterministic) and binds the key to whichever account `select()` would have picked on turn 1. Subsequent turns re-use that account as long as it's still healthy (not rejected, token not near expiry, headroom > 2%). On 429 failover, dario rebinds the key to the new account so the next turn doesn't re-select the exhausted one. 6h TTL, 2,000-entry cap, lazy cleanup. No client cooperation required.
207
233
 
208
- ### In-flight 429 failover (v3.8+)
234
+ ### In-flight 429 failover
209
235
 
210
236
  When a Claude request hits a 429 mid-flight, dario retries the *same request* against a different account before the client sees an error. The client sees one successful response; the pool sees the rejected account go cold until its window resets. Combined with session stickiness, long agent runs survive pool-level exhaustion without dropping user-facing turns.
211
237
 
@@ -220,7 +246,7 @@ Every request carries a `billingBucket` field (`subscription` / `subscription_fa
220
246
 
221
247
  ---
222
248
 
223
- ## Sealed-sender overflow protocol (v3.13)
249
+ ## Sealed-sender overflow protocol
224
250
 
225
251
  Trust-group members can lend each other Claude capacity with **cryptographic unlinkability**: a lender can verify the borrower is a valid group member without learning *which* member, so no one in the pool can surveil another through borrow telemetry.
226
252
 
@@ -255,7 +281,7 @@ Under the hood: `dario shim` spawns the child with `NODE_OPTIONS=--require <dari
255
281
 
256
282
  **Why it matters.** Anthropic can fingerprint a proxy via TLS, headers, IP, or `BASE_URL` env. They literally cannot easily detect a `globalThis.fetch` monkey-patch from inside their own process without shipping signed-binary integrity checks against `globalThis` from inside the CC binary — and even then, the shim runs *before* CC's code loads, so it could patch the integrity check too. The longest-half-life transport against classifier evolution.
257
283
 
258
- **v3.13 hardening** added runtime detection (canary for the day Anthropic ships a Bun-compiled CC), template mtime-based auto-reload (long-running children pick up mid-session fingerprint refreshes without restart), strict defensive `rewriteBody` (requires exactly 3 text blocks, passes through on any mismatch instead of inventing structure), and header-order replay (honors captured CC header sequence so the shim matches CC wire-exact).
284
+ **Hardening (v3.13+)** added runtime detection (canary for the day Anthropic ships a Bun-compiled CC), template mtime-based auto-reload (long-running children pick up mid-session fingerprint refreshes without restart), strict defensive `rewriteBody` (requires exactly 3 text blocks, passes through on any mismatch instead of inventing structure), and header-order replay (honors captured CC header sequence so the shim matches CC wire-exact).
259
285
 
260
286
  **When to use shim mode:**
261
287
  - Running a single CC instance on a locked-down machine where binding a local port is inconvenient.
@@ -272,12 +298,12 @@ Under the hood: `dario shim` spawns the child with `NODE_OPTIONS=--require <dari
272
298
 
273
299
  ## Agent compatibility
274
300
 
275
- As of **v3.22**, dario's built-in `TOOL_MAP` carries **~66 schema-verified entries** covering the tool schemas of every major coding agent. On the Claude backend, tool calls translate to CC's native `Bash / Read / Write / Edit / Glob / Grep / WebSearch / WebFetch` on the outbound path (keeping the subscription fingerprint intact) and rebuild to your agent's exact expected shape on the inbound path (so your validator is happy). No flag required.
301
+ Dario's built-in `TOOL_MAP` carries **~66 schema-verified entries** covering the tool schemas of every major coding agent. On the Claude backend, tool calls translate to CC's native `Bash / Read / Write / Edit / Glob / Grep / WebSearch / WebFetch` on the outbound path (keeping the subscription fingerprint intact) and rebuild to your agent's exact expected shape on the inbound path (so your validator is happy). No flag required.
276
302
 
277
303
  | Agent | Covered tool names (subset) |
278
304
  |---|---|
279
305
  | Claude Code | default — CC's own tools |
280
- | Cline / Roo Code | `execute_command`, `write_to_file`, `replace_in_file`, `apply_diff`, `list_files`, `search_files`, `read_file` |
306
+ | Cline / Roo Code / Kilo Code | `execute_command`, `write_to_file`, `replace_in_file`, `apply_diff`, `list_files`, `search_files`, `read_file` |
281
307
  | Cursor | `run_terminal_cmd`, `edit_file`, `search_replace`, `codebase_search`, `grep_search`, `file_search`, `list_dir`, `read_file` (`target_file`) |
282
308
  | Windsurf | `run_command`, `view_file`, `write_to_file`, `replace_file_content`, `find_by_name`, `grep_search`, `list_dir`, `search_web`, `read_url_content` |
283
309
  | Continue.dev | `builtin_run_terminal_command`, `builtin_read_file`, `builtin_create_new_file`, `builtin_edit_existing_file`, `builtin_file_glob_search`, `builtin_grep_search`, `builtin_ls` |
@@ -286,29 +312,70 @@ As of **v3.22**, dario's built-in `TOOL_MAP` carries **~66 schema-verified entri
286
312
  | OpenClaw | `exec`, `process`, `web_search`, `web_fetch`, `browser`, `message` |
287
313
  | Hermes | `terminal`, `patch`, `web_extract`, `clarify` |
288
314
 
289
- If your agent's tool names aren't pre-mapped, there are two escape hatches: **`--preserve-tools`** (forward your schema verbatim, lose the CC fingerprint) or **`--hybrid-tools`** (keep the fingerprint, fill request-context fields from headers). See [Custom tool schemas](#custom-tool-schemas).
315
+ Text-tool clients (Cline / Kilo Code / Roo Code and forks) are auto-detected via system-prompt fingerprint and automatically flipped into preserve-tools mode, because mixing CC's `tools` array with their XML protocol makes the model emit `<function_calls><invoke>` that their parsers can't read. If you run dario specifically for fingerprint fidelity and would rather pick `--preserve-tools` yourself, `--no-auto-detect` (v3.20.1, aka `--no-auto-preserve`) disables the heuristic explicit operator choice then wins.
316
+
317
+ If your agent's tool names aren't pre-mapped and its tools carry fields CC's schema doesn't have, there are two escape hatches: **`--preserve-tools`** (forward your schema verbatim, lose the CC fingerprint) or **`--hybrid-tools`** (keep the fingerprint, fill request-context fields from headers). See [Custom tool schemas](#custom-tool-schemas).
290
318
 
291
319
  The OpenAI-compat backend forwards tool definitions byte-for-byte and doesn't need any of this.
292
320
 
293
321
  ---
294
322
 
323
+ ## dario as MCP server (v3.27)
324
+
325
+ `dario mcp` turns dario itself into a **stdio JSON-RPC 2.0 MCP server**. Claude Desktop, Cursor, Zed, any MCP-aware editor can introspect dario's state without leaving the editor.
326
+
327
+ ```bash
328
+ dario mcp # spawns the MCP server on stdin/stdout — wire it up to your MCP client
329
+ ```
330
+
331
+ **Strictly read-only.** The exposed tool set is:
332
+
333
+ | Tool | What it reports |
334
+ |---|---|
335
+ | `doctor` | Full aggregated health report — same output as `dario doctor` |
336
+ | `status` | OAuth authentication state (authenticated / no-credentials / expired-but-refreshable) |
337
+ | `accounts_list` | Pool accounts + expiry times. Never touches API keys. |
338
+ | `backends_list` | Configured OpenAI-compat backends — keys redacted completely (not even a `sk-…` prefix) |
339
+ | `subagent_status` | CC sub-agent install and version-match state |
340
+ | `fingerprint_info` | Runtime / TLS classification, template source + schema version |
341
+
342
+ Mutations (`login`, `logout`, `accounts add/remove`, `backend add/remove`, `subagent install/remove`, `proxy` start/stop) are **not** exposed. An MCP client can observe dario; changing dario's state stays a CLI action the user types with intent. The test suite asserts the forbidden-tool set stays forbidden so a future accidental drift gets caught.
343
+
344
+ Zero runtime deps — the JSON-RPC dispatcher is hand-rolled over Node's `readline`. `src/mcp/protocol.ts` + `src/mcp/tools.ts` + `src/mcp/server.ts` are each pure over their inputs (streams are injectable, data sources are injectable) so the e2e test runs in-process against a `PassThrough` pair.
345
+
346
+ ---
347
+
348
+ ## Claude Code sub-agent hook (v3.26)
349
+
350
+ `dario subagent install` writes `~/.claude/agents/dario.md` so Claude Code has a named handle for running dario diagnostics and template-refresh inside an ongoing CC session. No more `Ctrl+Z → dario doctor → fg` when you hit a `[WARN]` row mid-conversation.
351
+
352
+ ```bash
353
+ dario subagent install # writes ~/.claude/agents/dario.md
354
+ dario subagent status # {not-installed, installed+current, installed+stale} + hint
355
+ dario subagent remove # idempotent
356
+ ```
357
+
358
+ **Tool-scoped.** The sub-agent is restricted to `Bash, Read` and its prompt forbids destructive operations (credential mutation, account pool changes, backend config changes) without explicit user confirmation. `dario proxy` is also off-limits from inside the sub-agent — it would block the parent CC session. CC can ask dario to *report*, not to *change state*. (MCP server has the same read-only boundary for the same reason.)
359
+
360
+ A version marker (`<!-- dario-sub-agent-version: X -->`) embedded in the markdown lets `dario doctor` distinguish installed-and-current from installed-and-stale; the "Sub-agent" row appears between Backends and Home with an inline refresh command when stale.
361
+
362
+ ---
363
+
295
364
  ## Commands
296
365
 
297
366
  | Command | Description |
298
367
  |---|---|
299
- | `dario login` | Log in to the Claude backend (detects CC credentials or runs its own OAuth flow) |
368
+ | `dario login [--manual]` | Log in to the Claude backend. Detects CC credentials or runs its own OAuth flow. `--manual` (v3.20) mirrors CC's code-paste flow for SSH / container setups without a browser. |
300
369
  | `dario proxy` | Start the local API proxy on port 3456 |
301
- | `dario doctor` | Aggregated health report — dario / Node / CC binary + compat / template + drift / OAuth / pool / backends |
370
+ | `dario doctor` | Aggregated health report — dario / Node / runtime-TLS / CC binary + compat / template + drift / OAuth / pool / backends / sub-agent |
302
371
  | `dario status` | Show Claude backend OAuth token health and expiry |
303
372
  | `dario refresh` | Force an immediate Claude token refresh |
304
373
  | `dario logout` | Delete stored Claude credentials |
305
- | `dario accounts list` | List accounts in the multi-account pool |
306
- | `dario accounts add <alias>` | Add a Claude account to the pool (runs OAuth flow) |
307
- | `dario accounts remove <alias>` | Remove an account from the pool |
308
- | `dario backend list` | List configured OpenAI-compat backends |
309
- | `dario backend add <name> --key=<key> [--base-url=<url>]` | Add an OpenAI-compat backend |
310
- | `dario backend remove <name>` | Remove an OpenAI-compat backend |
374
+ | `dario accounts list` / `add <alias>` / `remove <alias>` | Multi-account pool management |
375
+ | `dario backend list` / `add <name> --key=<key> [--base-url=<url>]` / `remove <name>` | OpenAI-compat backend management |
311
376
  | `dario shim -- <cmd> [args...]` | Run a child process with the in-process fetch patch (see [Shim mode](#shim-mode)) |
377
+ | `dario subagent install` / `remove` / `status` | CC sub-agent lifecycle (v3.26 — see [sub-agent hook](#claude-code-sub-agent-hook-v326)) |
378
+ | `dario mcp` | Run dario as an MCP server over stdio (v3.27 — see [dario as MCP server](#dario-as-mcp-server-v327)) |
312
379
  | `dario help` | Full command reference |
313
380
 
314
381
  ### Proxy options
@@ -316,18 +383,34 @@ The OpenAI-compat backend forwards tool definitions byte-for-byte and doesn't ne
316
383
  | Flag / env | Description | Default |
317
384
  |---|---|---|
318
385
  | `--passthrough` / `--thin` | Thin proxy for the Claude backend — OAuth swap only, no template injection | off |
319
- | `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC's. Required for clients whose tools have fields CC doesn't — see [Custom tool schemas](#custom-tool-schemas). Auto-enabled for Cline / Kilo Code / Roo Code and forks (dario#40 — detected via system-prompt fingerprint). | off (auto for text-tool clients) |
320
- | `--hybrid-tools` / `--context-inject` | Remap to CC tools **and** inject request-context values (`sessionId`, `requestId`, `channelId`, `userId`, `timestamp`) into client-declared fields CC's schema doesn't carry. See [Hybrid tool mode](#hybrid-tool-mode). Overrides the text-tool auto-detect. | off |
321
- | `--model=<name>` | Force a model. Shortcuts (`opus`, `sonnet`, `haiku`), full IDs (`claude-opus-4-7`), or a **provider prefix** (`openai:gpt-4o`, `groq:llama-3.3-70b`, `claude:opus`, `local:qwen-coder`) to force the backend server-wide. See [Provider prefix](#provider-prefix). | passthrough |
386
+ | `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC's. Required for clients whose tools have fields CC doesn't — see [Custom tool schemas](#custom-tool-schemas). Auto-enabled for Cline / Kilo Code / Roo Code and forks (detected via system-prompt fingerprint). | off (auto for text-tool clients) |
387
+ | `--no-auto-detect` / `--no-auto-preserve` | Disable the text-tool-client detector so the CC fingerprint stays intact on Cline/Kilo/Roo prompts (v3.20.1, dario#40). Explicit `--preserve-tools` still wins. | off |
388
+ | `--hybrid-tools` / `--context-inject` | Remap to CC tools **and** inject request-context values (`sessionId`, `requestId`, `channelId`, `userId`, `timestamp`) into client-declared fields CC's schema doesn't carry. See [Hybrid tool mode](#hybrid-tool-mode). | off |
389
+ | `--model=<name>` | Force a model. Shortcuts (`opus`, `sonnet`, `haiku`), full IDs (`claude-opus-4-7`), or a **provider prefix** (`openai:gpt-4o`, `groq:llama-3.3-70b`, `claude:opus`, `local:qwen-coder`) to force the backend server-wide. | passthrough |
322
390
  | `--port=<n>` | Port to listen on | `3456` |
323
391
  | `--host=<addr>` / `DARIO_HOST` | Bind address. Use `0.0.0.0` for LAN, or a specific IP (e.g. a Tailscale interface). When non-loopback, also set `DARIO_API_KEY`. | `127.0.0.1` |
324
392
  | `--verbose` / `-v` | Log every request (one line per request — method + path + billing bucket) | off |
325
- | `--verbose=2` / `-vv` / `DARIO_LOG_BODIES=1` | Also dump the outbound request body (redacted: bearer tokens, `sk-ant-*` keys, JWTs stripped; capped at 8KB). For wire-level client-compat debugging (dario#40). | off |
393
+ | `--verbose=2` / `-vv` / `DARIO_LOG_BODIES=1` | Also dump the outbound request body (redacted: bearer tokens, `sk-ant-*` keys, JWTs stripped; capped at 8KB). For wire-level client-compat debugging. | off |
394
+ | `--strict-tls` / `DARIO_STRICT_TLS=1` | Refuse to start proxy mode unless runtime classifies as `bun-match` — i.e. the TLS ClientHello matches CC's. See [Fingerprint axes](#fingerprint-axes). (v3.23) | off |
395
+ | `--pace-min=<ms>` / `DARIO_PACE_MIN_MS` | Minimum inter-request gap in ms. Replaces the legacy hardcoded 500 ms. (v3.24) | `500` |
396
+ | `--pace-jitter=<ms>` / `DARIO_PACE_JITTER_MS` | Uniform random jitter added to each gap. Dissolves the minimum-inter-arrival fingerprint edge. (v3.24) | `0` |
397
+ | `--drain-on-close` / `DARIO_DRAIN_ON_CLOSE=1` | When a downstream client disconnects mid-stream, keep reading upstream SSE to completion (match CC's consumption shape). Bounded by the 5-min upstream timeout. (v3.25) | off |
398
+ | `--session-idle-rotate=<ms>` / `DARIO_SESSION_IDLE_ROTATE_MS` | Idle threshold before a session-id rotates. (v3.28) | `900000` (15 min) |
399
+ | `--session-rotate-jitter=<ms>` / `DARIO_SESSION_JITTER_MS` | Jitter sampled once per session at creation — hides the exact idle floor. (v3.28) | `0` |
400
+ | `--session-max-age=<ms>` / `DARIO_SESSION_MAX_AGE_MS` | Hard ceiling on a session-id's lifetime regardless of activity. (v3.28) | off |
401
+ | `--session-per-client` / `DARIO_SESSION_PER_CLIENT=1` | Split session-id registry by a per-client header so multi-UI fan-out doesn't collapse onto one id. (v3.28) | off |
326
402
  | `DARIO_API_KEY` | If set, all endpoints (except `/health`) require a matching `x-api-key` or `Authorization: Bearer` header. Required when `--host` binds non-loopback. | unset (open) |
327
403
  | `DARIO_CORS_ORIGIN` | Override browser CORS origin | `http://localhost:${port}` |
404
+ | `DARIO_QUIET_TLS` | Suppress the runtime/TLS mismatch startup banner | unset |
328
405
  | `DARIO_NO_BUN` | Disable automatic Bun relaunch | unset |
329
- | `DARIO_MIN_INTERVAL_MS` | Minimum ms between Claude-backend requests (rate governor) | `500` |
406
+ | `DARIO_MIN_INTERVAL_MS` | Legacy name for `DARIO_PACE_MIN_MS`. Still honored; new name wins when both are set. | |
330
407
  | `DARIO_CC_PATH` | Override path to the Claude Code binary for OAuth detection | auto-detect |
408
+ | `DARIO_OAUTH_CLIENT_ID` | Override the detected Claude OAuth client id as an emergency escape hatch | unset |
409
+ | `DARIO_OAUTH_AUTHORIZE_URL` | Override the detected Claude OAuth authorize URL | unset |
410
+ | `DARIO_OAUTH_TOKEN_URL` | Override the detected Claude OAuth token URL | unset |
411
+ | `DARIO_OAUTH_SCOPES` | Override the detected Claude OAuth scopes | unset |
412
+ | `DARIO_OAUTH_OVERRIDE_PATH` | Override file path for JSON OAuth overrides | `~/.dario/oauth-config.override.json` |
413
+ | `DARIO_OAUTH_DISABLE_OVERRIDE=1` | Ignore env/file OAuth overrides entirely | unset |
331
414
 
332
415
  ---
333
416
 
@@ -418,17 +501,15 @@ curl http://localhost:3456/v1/chat/completions \
418
501
 
419
502
  ### Streaming, tool use, prompt caching, extended thinking
420
503
 
421
- All supported. Claude backend: full Anthropic SSE format plus OpenAI-SSE translation for tool_use streaming. OpenAI-compat backend: streaming body forwarded byte-for-byte.
504
+ All supported. Claude backend: full Anthropic SSE format plus OpenAI-SSE translation for tool_use streaming. OpenAI-compat backend: streaming body forwarded byte-for-byte. See [Fingerprint axes](#fingerprint-axes) for the v3.25 `--drain-on-close` knob that matches CC's read-to-EOF stream-consumption pattern.
422
505
 
423
506
  ### Provider prefix
424
507
 
425
- Any request's `model` field can be written as `<provider>:<name>` to force which backend handles it, regardless of what the model name looks like. Useful when regex-based routing (`gpt-*` → OpenAI, `claude-*` → Claude) doesn't match — for example routing a `llama-3.3-70b` request through OpenRouter, or making the same model name go to different providers on different requests.
426
-
427
- Recognized prefixes:
508
+ Any request's `model` field can be written as `<provider>:<name>` to force which backend handles it, regardless of what the model name looks like.
428
509
 
429
510
  | Prefix | Backend |
430
511
  |---|---|
431
- | `openai:` | OpenAI-compat backend (the configured one) |
512
+ | `openai:` | OpenAI-compat backend |
432
513
  | `groq:` | OpenAI-compat backend |
433
514
  | `openrouter:` | OpenAI-compat backend |
434
515
  | `local:` | OpenAI-compat backend |
@@ -452,7 +533,7 @@ Fix: run dario with `--preserve-tools`. That skips the CC tool remap entirely, p
452
533
  dario proxy --preserve-tools
453
534
  ```
454
535
 
455
- The cost: requests no longer look like CC on the wire, so the CC subscription fingerprint is gone. On a Max/Pro plan, that means the request may be counted against your API usage rather than your subscription quota. If you're on API-key billing already, `--preserve-tools` is free; if you're using dario specifically to route against a subscription, [hybrid tool mode](#hybrid-tool-mode) below is the compromise that keeps both.
536
+ The cost: requests no longer look like CC on the wire, so the CC subscription fingerprint is gone. On a Max/Pro plan, that means the request may be counted against your API usage rather than your subscription quota. [Hybrid tool mode](#hybrid-tool-mode) below is the compromise that keeps both.
456
537
 
457
538
  The OpenAI-compat backend is unaffected — it forwards tool definitions byte-for-byte and doesn't need this flag.
458
539
 
@@ -474,6 +555,7 @@ dario proxy --hybrid-tools
474
555
  | Your custom fields are request context (session/request/channel/user ids, timestamps) | `--hybrid-tools` | Keeps the CC fingerprint *and* your validator is satisfied. |
475
556
  | Your custom fields need the model's reasoning (e.g. `confidence`, `reasoning_trace`, `tool_selection_rationale`) | `--preserve-tools` | The model has to see the real schema to populate these. Accept the fingerprint loss. |
476
557
  | Your client's tools are already a subset of CC's `Bash/Read/Write/Edit/Grep/Glob/WebSearch/WebFetch` | *(neither)* | Default mode works as-is. |
558
+ | You're on a text-tool client (Cline / Kilo Code / Roo Code) and want to override the auto-detect | `--no-auto-detect` (plus `--preserve-tools` or not, your call) | Operator choice outranks the heuristic. |
477
559
 
478
560
  ### Library mode
479
561
 
@@ -515,17 +597,18 @@ Dario handles your OAuth tokens and API keys locally. Here's why you can trust i
515
597
 
516
598
  | Signal | Status |
517
599
  |---|---|
518
- | **Source code** | ~8,100 lines of TypeScript across ~15 files — small enough to audit in a weekend |
600
+ | **Source code** | ~11,300 lines of TypeScript across ~25 files — small enough to audit in a weekend |
519
601
  | **Dependencies** | 0 runtime dependencies. Verify: `npm ls --production` |
520
602
  | **npm provenance** | Every release is [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions with sigstore provenance attached to the transparency log |
521
603
  | **Security scanning** | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) runs on every push and weekly |
522
- | **Test footprint** | ~840 assertions across 24 files. Full `npm test` green on every release |
523
- | **Credential handling** | Tokens and API keys never logged, redacted from errors, stored with `0600` permissions |
524
- | **OAuth flow** | PKCE (Proof Key for Code Exchange), no client secret |
604
+ | **Test footprint** | ~1,250 assertions across 32 test suites. Full `npm test` green on every release |
605
+ | **Credential handling** | Tokens and API keys never logged, redacted from errors, stored with `0600` permissions. MCP server (v3.27) redacts keys at the tool boundary too — not even a `sk-…` prefix leaks. |
606
+ | **OAuth flow** | PKCE (Proof Key for Code Exchange), no client secret. `--manual` flow for headless setups (v3.20). |
525
607
  | **Network scope** | Binds to `127.0.0.1` by default. `--host` allows LAN/mesh with `DARIO_API_KEY` gating. Upstream traffic goes only to the configured backend target URLs over HTTPS |
526
608
  | **SSRF protection** | `/v1/messages` hits `api.anthropic.com` only; `/v1/chat/completions` hits the configured backend `baseUrl` only — hardcoded allowlist |
527
- | **Telemetry** | None. Zero analytics, tracking, or data collection |
609
+ | **Telemetry** | None. Zero analytics, tracking, or data collection. The MCP server (v3.27) and CC sub-agent (v3.26) are read-only by design — no tool can mutate dario's state from inside CC or an MCP client. |
528
610
  | **Atomic cache writes + corruption recovery** | v3.17 — template cache writes are pid-qualified `.tmp` + `rename`, corrupt cache files are quarantined and regenerated instead of crashing startup |
611
+ | **Baked template scrub** | v3.21 — the bundled fallback template is stripped of host-identifying paths and `mcp__*` tool names at bake time; the nightly drift watcher guards against regression |
529
612
  | **Audit trail** | [CHANGELOG.md](CHANGELOG.md) documents every release with file-level rationale |
530
613
 
531
614
  Verify the npm tarball matches this repo:
@@ -550,10 +633,10 @@ Claude Max and Claude Pro. Any plan that lets you use Claude Code.
550
633
  Should work if your plan includes Claude Code access. Not widely tested yet — open an issue with results.
551
634
 
552
635
  **Do I need Claude Code installed?**
553
- Recommended for the Claude backend, not strictly required. With CC installed, `dario login` picks up your credentials automatically, and the live fingerprint extractor reads your CC binary on every startup so the template stays current. Without CC, dario runs its own OAuth flow and falls back to the bundled template snapshot. Drift detection (v3.17) warns you if your installed CC doesn't match the captured template, so upgrade windows don't silently ship stale templates.
636
+ Recommended for the Claude backend, not strictly required. With CC installed, `dario login` picks up your credentials automatically, and the live fingerprint extractor reads your CC binary on every startup so the template stays current. Without CC, dario runs its own OAuth flow and falls back to the bundled template snapshot (scrubbed of host context at bake time as of v3.21). Drift detection warns you if your installed CC doesn't match the captured template, so upgrade windows don't silently ship stale templates.
554
637
 
555
638
  **Do I need Bun?**
556
- Optional, recommended for Claude-backend requests. Dario auto-relaunches under Bun when available so the TLS fingerprint matches CC's runtime. Without Bun, dario runs on Node.js and works fine; the TLS fingerprint is the only difference.
639
+ Optional, strongly recommended for Claude-backend requests. Dario auto-relaunches under Bun when available so the TLS ClientHello matches CC's runtime. Without Bun, dario runs on Node.js and works fine the TLS fingerprint is the only difference. As of v3.23, `dario doctor` surfaces the mismatch explicitly and `--strict-tls` refuses to start proxy mode until it's resolved. The shim transport sidesteps this entirely (it runs inside CC's own process, so its TLS stack *is* CC's).
557
640
 
558
641
  **Can I use dario without a Claude subscription?**
559
642
  Yes. Skip `dario login`, just run `dario backend add openai --key=...` (or any OpenAI-compat URL) and `dario proxy`. Claude-backend requests will return an authentication error; OpenAI-compat requests will work normally. Dario becomes a local OpenAI-compat router with no Claude involvement.
@@ -562,13 +645,26 @@ Yes. Skip `dario login`, just run `dario backend add openai --key=...` (or any O
562
645
  Yes — anything that speaks the OpenAI Chat Completions API. Groq, OpenRouter, LiteLLM, vLLM, Ollama's openai-compat mode, your own vLLM server, any hosted inference endpoint that exposes `/v1/chat/completions`. Just `dario backend add <name> --key=... --base-url=...`.
563
646
 
564
647
  **Something's wrong. Where do I start?**
565
- `dario doctor`. One command, one aggregated report — dario version, Node, platform, CC binary compat, template source + age + drift, OAuth status, pool state, backends, home dir. Exit code 1 if any check fails. Paste the output when you file an issue.
648
+ `dario doctor`. One command, one aggregated report — dario version, Node, platform, runtime/TLS classification, CC binary compat, template source + age + drift, OAuth status, pool state, backends, sub-agent install state, home dir. Exit code 1 if any check fails. Paste the output when you file an issue. (If you're inside Claude Code, `dario subagent install` once and then ask CC to "use the dario sub-agent to run doctor" — same output, no context switch.)
566
649
 
567
650
  **What happens when Anthropic rotates the OAuth config?**
568
- Dario auto-detects OAuth config from the installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next run. Cache at `~/.dario/cc-oauth-cache-v4.json`, keyed by the CC binary fingerprint.
651
+ Dario auto-detects OAuth config from the installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next run. Cache at `~/.dario/cc-oauth-cache-v4.json`, keyed by the CC binary fingerprint. (Path bumped from v3 → v4 in v3.19.4 to invalidate stale caches across the scope-list change that broke the authorize flow between CC v2.1.104 and v2.1.107.)
652
+
653
+ If Anthropic rotates the values before the detector is updated, you can temporarily override any field with env vars (`DARIO_OAUTH_CLIENT_ID`, `DARIO_OAUTH_AUTHORIZE_URL`, `DARIO_OAUTH_TOKEN_URL`, `DARIO_OAUTH_SCOPES`) or by writing `~/.dario/oauth-config.override.json`:
654
+
655
+ ```json
656
+ {
657
+ "clientId": "...",
658
+ "authorizeUrl": "https://claude.com/cai/oauth/authorize",
659
+ "tokenUrl": "https://platform.claude.com/v1/oauth/token",
660
+ "scopes": "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload"
661
+ }
662
+ ```
663
+
664
+ Env vars win over the file. Set `DARIO_OAUTH_DISABLE_OVERRIDE=1` to force pure auto-detection.
569
665
 
570
666
  **What happens when Anthropic changes the CC request template?**
571
- Dario extracts the live request template from your installed Claude Code binary on startup — the system prompt, tool schemas, user-agent, beta flags, header insertion order, static header values, and top-level request-body key order — and uses those to replay requests instead of a version pinned into dario itself. When CC ships a new version with a tweaked template, the next `dario proxy` run picks it up automatically. Drift detection (v3.17) forces a refresh when the installed CC version changes under dario, and the nightly `cc-drift-watch` workflow catches upstream rotations (client_id, URLs, tool set, version) the day they ship on npm.
667
+ Dario extracts the live request template from your installed Claude Code binary on startup — the system prompt, tool schemas, user-agent, beta flags, header insertion order, static header values, and top-level request-body key order — and uses those to replay requests instead of a version pinned into dario itself. When CC ships a new version with a tweaked template, the next `dario proxy` run picks it up automatically. Drift detection forces a refresh when the installed CC version changes under dario, and the nightly `cc-drift-watch` workflow catches upstream rotations (client_id, URLs, tool set, version) the day they ship on npm.
572
668
 
573
669
  **First time setup on a fresh Claude account.**
574
670
  If dario is the first thing you run against a brand-new Claude account, prime the account with a few real Claude Code commands first:
@@ -596,7 +692,10 @@ Seeing `seven_day` is a healthy state. Your Max/Pro plan is doing exactly what i
596
692
  Standalone writeup: [Discussion #32 — why you see `representative-claim: seven_day` and why it's not a downgrade](https://github.com/askalf/dario/discussions/32).
597
693
 
598
694
  **My multi-agent workload is getting reclassified to overage even though dario template-replays per request. Why?**
599
- Reclassification at high agent volume is not a per-request problem. Anthropic's classifier operates on cumulative per-OAuth-session aggregates — token throughput, conversation depth, streaming duration, inter-arrival timing, thinking-block volume. Dario's Claude backend can make each individual request indistinguishable from Claude Code and still hit this wall on a long-running agent session. Thorough diagnostic work was contributed by [@belangertrading](https://github.com/belangertrading) in [#23](https://github.com/askalf/dario/issues/23). The practical answer at the dario layer is **pool mode** — distribute load across multiple subscriptions so no single account accumulates enough signal to trip anything. See [Multi-account pool mode](#multi-account-pool-mode).
695
+ Reclassification at high agent volume is not a per-request problem. Anthropic's classifier operates on cumulative per-OAuth-session aggregates — token throughput, conversation depth, streaming duration, inter-arrival timing, thinking-block volume. Dario's Claude backend can make each individual request indistinguishable from Claude Code and still hit this wall on a long-running agent session. Thorough diagnostic work was contributed by [@belangertrading](https://github.com/belangertrading) in [#23](https://github.com/askalf/dario/issues/23). The practical answer at the dario layer is **pool mode** — distribute load across multiple subscriptions so no single account accumulates enough signal to trip anything. See [Multi-account pool mode](#multi-account-pool-mode). The v3.22 – v3.28 fingerprint track (pacing, stream-drain, session-id lifecycle) also narrows the cumulative signal on a single account — see [Fingerprint axes](#fingerprint-axes).
696
+
697
+ **My proxy is on Node, not Bun. What's the actual risk?**
698
+ Node uses OpenSSL, Bun uses BoringSSL — the TLS ClientHello differs enough to yield a distinct JA3/JA4 hash. Anthropic can see the hash. Whether they classify on it today is unknown; making the axis visible is the v3.23 contribution. If certainty matters to you, install Bun (dario auto-relaunches under it) or run `dario proxy --strict-tls` to fail loud. If it doesn't, the warning is ignorable — dario still works, the TLS fingerprint is just the one observable axis left.
600
699
 
601
700
  **Why "dario"?**
602
701
  It's a name, not an acronym. Don't overthink it.
@@ -613,22 +712,32 @@ Longer-form writing on how dario works and why it works that way:
613
712
  - [Billing tag algorithm and fingerprint analysis](https://github.com/askalf/dario/discussions/8)
614
713
  - [Rate limit header analysis](https://github.com/askalf/dario/discussions/1)
615
714
 
715
+ The CHANGELOG documents every v3.22 – v3.28 "get ahead of Anthropic" release with file-level rationale; each one is worth reading as a standalone post on the axis it closes.
716
+
616
717
  ---
617
718
 
618
719
  ## Contributing
619
720
 
620
- PRs welcome. The codebase is small TypeScript — ~8,100 lines across ~15 files:
721
+ PRs welcome. The codebase is small TypeScript — ~11,300 lines across ~25 files:
621
722
 
622
723
  | File | Purpose |
623
724
  |---|---|
624
- | `src/proxy.ts` | HTTP proxy server, request handler, rate governor, Claude backend dispatch, OpenAI-compat routing, pool failover |
725
+ | `src/proxy.ts` | HTTP proxy server, request handler, rate governor, Claude backend dispatch, OpenAI-compat routing, pool failover, session registry wiring, stream-drain gating |
625
726
  | `src/cc-template.ts` | CC request template engine, universal `TOOL_MAP` (~66 schema-verified entries), orchestration and framework scrubbing, header-order + body-field-order replay |
626
- | `src/cc-template-data.json` | Bundled fallback CC request template (used when live-fingerprint extraction isn't possible). Scrubbed of host-identifying paths at bake time. |
627
- | `src/scrub-template.ts` | Host-context scrubber for the baked fallback template — strips per-session sections, replaces user-dir paths with a placeholder, drops `mcp__*` tools (v3.21) |
727
+ | `src/cc-template-data.json` | Bundled fallback CC request template (used when live-fingerprint extraction isn't possible). Scrubbed of host-identifying paths and `mcp__*` tools at bake time (v3.21). |
728
+ | `src/scrub-template.ts` | Host-context scrubber for the baked fallback — strips per-session sections, replaces user-dir paths with a placeholder, drops `mcp__*` tools |
628
729
  | `src/cc-oauth-detect.ts` | OAuth config auto-detection from the installed CC binary |
629
730
  | `src/live-fingerprint.ts` | Live extraction of the CC request template (system prompt, tools, user-agent, beta flags, header order, static header values, body field order) from the installed Claude Code binary, drift detection, compat matrix, atomic cache writes, corruption recovery |
630
- | `src/doctor.ts` | `dario doctor` health report aggregator dario/Node/CC/template/drift/OAuth/pool/backends |
631
- | `src/oauth.ts` | Single-account token storage, PKCE flow, auto-refresh |
731
+ | `src/runtime-fingerprint.ts` | Runtime / TLS classifier (`bun-match` / `bun-bypassed` / `node-only`) surfaced through `dario doctor` and `--strict-tls` (v3.23) |
732
+ | `src/pacing.ts` | Pure inter-request delay calculator with configurable floor + uniform jitter (v3.24) |
733
+ | `src/stream-drain.ts` | Pure decision function for client-disconnect handling — `abort` / `drain` / `noop` (v3.25) |
734
+ | `src/session-rotation.ts` | `SessionRegistry` with LRU eviction + pure `decideSessionRotation` — idle, jitter, max-age, per-client bucketing (v3.28) |
735
+ | `src/subagent.ts` | CC sub-agent install / remove / status lifecycle; `buildSubagentFile(version)` is pure and pinned (v3.26) |
736
+ | `src/mcp/protocol.ts` | Hand-rolled JSON-RPC 2.0 + MCP method dispatcher — zero deps, pure over inputs, tested without streams (v3.27) |
737
+ | `src/mcp/tools.ts` | Six read-only MCP tools — `doctor`, `status`, `accounts_list`, `backends_list`, `subagent_status`, `fingerprint_info`. Redacts credentials at the tool boundary (v3.27) |
738
+ | `src/mcp/server.ts` | Stdio event loop — ordered serial dispatch, back-pressure-aware writes, injectable streams for testing (v3.27) |
739
+ | `src/doctor.ts` | `dario doctor` health report aggregator — dario / Node / runtime-TLS / CC / template / drift / OAuth / pool / backends / sub-agent |
740
+ | `src/oauth.ts` | Single-account token storage, PKCE flow, auto-refresh, manual/headless flow (v3.20) |
632
741
  | `src/accounts.ts` | Multi-account credential storage, independent OAuth lifecycle, refresh single-flight |
633
742
  | `src/pool.ts` | Account pool, headroom-aware routing, session stickiness, failover target selection |
634
743
  | `src/sealed-pool.ts` | Sealed-sender overflow protocol — RSA blind signatures for unlinkable group pooling |
@@ -636,7 +745,7 @@ PRs welcome. The codebase is small TypeScript — ~8,100 lines across ~15 files:
636
745
  | `src/openai-backend.ts` | OpenAI-compat backend credential storage and request forwarder |
637
746
  | `src/shim/runtime.cjs` | Hand-written CJS payload loaded into child processes via `NODE_OPTIONS=--require`; patches `globalThis.fetch` for Anthropic messages requests only |
638
747
  | `src/shim/host.ts` | Parent-side orchestrator for `dario shim` — spawns the child, owns the telemetry socket / named pipe, feeds analytics |
639
- | `src/cli.ts` | CLI entry point, command routing, Bun auto-relaunch |
748
+ | `src/cli.ts` | CLI entry point, command routing, Bun auto-relaunch, proxy flag parsing |
640
749
  | `src/index.ts` | Library exports |
641
750
 
642
751
  ```bash
@@ -644,7 +753,7 @@ git clone https://github.com/askalf/dario
644
753
  cd dario
645
754
  npm install
646
755
  npm run dev # runs with tsx, no build step
647
- npm test # ~640 assertions across 20 suites
756
+ npm test # ~1,250 assertions across 32 suites
648
757
  npm run e2e # live proxy + OAuth (requires a working Claude backend)
649
758
  ```
650
759
 
@@ -659,8 +768,9 @@ npm run e2e # live proxy + OAuth (requires a working Claude backend)
659
768
  | [@nathan-widjaja](https://github.com/nathan-widjaja) | README positioning rewrite structure ([#21](https://github.com/askalf/dario/issues/21)) |
660
769
  | [@iNicholasBE](https://github.com/iNicholasBE) | macOS keychain credential detection ([#30](https://github.com/askalf/dario/pull/30)) |
661
770
  | [@boeingchoco](https://github.com/boeingchoco) | Reverse-direction tool parameter translation ([#29](https://github.com/askalf/dario/issues/29)), SSE event-group framing regression catch (v3.7.1), provider-comparison diagnostic that surfaced the `--preserve-tools` discoverability gap (v3.8.1), motivating case for hybrid tool mode ([#33](https://github.com/askalf/dario/issues/33), v3.9.0), OpenClaw tool-mapping root cause that drove the universal `TOOL_MAP` work ([#36](https://github.com/askalf/dario/issues/36)) |
662
- | [@tetsuco](https://github.com/tetsuco) | Framework-name path corruption in scrubber ([#35](https://github.com/askalf/dario/issues/35)), OpenClaw Bash/Glob reverse-mapping collisions ([#37](https://github.com/askalf/dario/issues/37)) |
771
+ | [@tetsuco](https://github.com/tetsuco) | Framework-name path corruption in scrubber ([#35](https://github.com/askalf/dario/issues/35)), OpenClaw Bash/Glob reverse-mapping collisions ([#37](https://github.com/askalf/dario/issues/37)), 20x-tier invalid-x-api-key capture artifact + OAuth-scope rejection report that drove v3.19.2 / v3.19.4 / v3.19.5 ([#42](https://github.com/askalf/dario/issues/42)) |
663
772
  | [@mikelovatt](https://github.com/mikelovatt) | Silent subscription-percent drain surfaced via friendly billing buckets ([#34](https://github.com/askalf/dario/issues/34)) |
773
+ | [@ringge](https://github.com/ringge) | Fingerprint-fidelity concern motivating the `--no-auto-detect` opt-out for text-tool-client auto-preserve ([#40](https://github.com/askalf/dario/issues/40), v3.20.1) |
664
774
 
665
775
  ---
666
776
 
@@ -32,16 +32,21 @@
32
32
  * startup only re-scans when the user upgrades Claude Code. The cache suffix
33
33
  * is bumped each time scope handling or the fallback config changes, so
34
34
  * upgrading dario picks up the new values without a manual cache clear.
35
+ *
36
+ * Escape hatch: if Anthropic rotates OAuth metadata before the detector is
37
+ * updated, operators can temporarily override any detected value via env vars
38
+ * or ~/.dario/oauth-config.override.json.
35
39
  */
36
40
  export interface DetectedOAuthConfig {
37
41
  clientId: string;
38
42
  authorizeUrl: string;
39
43
  tokenUrl: string;
40
44
  scopes: string;
41
- source: 'detected' | 'cached' | 'fallback';
45
+ source: 'detected' | 'cached' | 'fallback' | 'override';
42
46
  ccPath?: string;
43
47
  ccHash?: string;
44
48
  }
49
+ export declare const FALLBACK_FOR_DRIFT_CHECK: Readonly<DetectedOAuthConfig>;
45
50
  /**
46
51
  * Scan binary bytes for the PROD OAuth config block.
47
52
  *
@@ -32,6 +32,10 @@
32
32
  * startup only re-scans when the user upgrades Claude Code. The cache suffix
33
33
  * is bumped each time scope handling or the fallback config changes, so
34
34
  * upgrading dario picks up the new values without a manual cache clear.
35
+ *
36
+ * Escape hatch: if Anthropic rotates OAuth metadata before the detector is
37
+ * updated, operators can temporarily override any detected value via env vars
38
+ * or ~/.dario/oauth-config.override.json.
35
39
  */
36
40
  import { readFile, writeFile, mkdir, stat, open as openFile } from 'node:fs/promises';
37
41
  import { existsSync } from 'node:fs';
@@ -60,10 +64,15 @@ const FALLBACK = {
60
64
  scopes: 'user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload',
61
65
  source: 'fallback',
62
66
  };
67
+ // Re-export of FALLBACK for scripts/check-cc-authorize-probe.mjs. The probe
68
+ // needs the exact values the runtime uses — hardcoding them in the script
69
+ // would drift out of sync silently.
70
+ export const FALLBACK_FOR_DRIFT_CHECK = FALLBACK;
63
71
  // -v4 suffix invalidates v3.x caches populated with the 6-scope list that
64
72
  // Anthropic now rejects (dario #42). On upgrade, users regenerate the cache
65
73
  // with the new FALLBACK scopes automatically — no manual clear required.
66
74
  const CACHE_PATH = join(homedir(), '.dario', 'cc-oauth-cache-v4.json');
75
+ const DEFAULT_OVERRIDE_PATH = join(homedir(), '.dario', 'oauth-config.override.json');
67
76
  function candidatePaths() {
68
77
  const home = homedir();
69
78
  if (platform() === 'win32') {
@@ -96,6 +105,69 @@ function findCCBinary() {
96
105
  }
97
106
  return null;
98
107
  }
108
+ function isOverrideDisabled() {
109
+ return process.env['DARIO_OAUTH_DISABLE_OVERRIDE'] === '1';
110
+ }
111
+ function getOverridePath() {
112
+ return process.env['DARIO_OAUTH_OVERRIDE_PATH']?.trim() || DEFAULT_OVERRIDE_PATH;
113
+ }
114
+ function cleanOverrideValue(value) {
115
+ const trimmed = value?.trim();
116
+ return trimmed ? trimmed : undefined;
117
+ }
118
+ function normalizeOverride(parsed) {
119
+ const override = {};
120
+ const clientId = typeof parsed.clientId === 'string' ? cleanOverrideValue(parsed.clientId) : undefined;
121
+ const authorizeUrl = typeof parsed.authorizeUrl === 'string' ? cleanOverrideValue(parsed.authorizeUrl) : undefined;
122
+ const tokenUrl = typeof parsed.tokenUrl === 'string' ? cleanOverrideValue(parsed.tokenUrl) : undefined;
123
+ const scopes = typeof parsed.scopes === 'string' ? cleanOverrideValue(parsed.scopes) : undefined;
124
+ if (clientId)
125
+ override.clientId = clientId;
126
+ if (authorizeUrl)
127
+ override.authorizeUrl = authorizeUrl;
128
+ if (tokenUrl)
129
+ override.tokenUrl = tokenUrl;
130
+ if (scopes)
131
+ override.scopes = scopes;
132
+ return Object.keys(override).length > 0 ? override : null;
133
+ }
134
+ async function loadManualOverride() {
135
+ if (isOverrideDisabled())
136
+ return null;
137
+ const envOverride = normalizeOverride({
138
+ clientId: process.env['DARIO_OAUTH_CLIENT_ID'],
139
+ authorizeUrl: process.env['DARIO_OAUTH_AUTHORIZE_URL'],
140
+ tokenUrl: process.env['DARIO_OAUTH_TOKEN_URL'],
141
+ scopes: process.env['DARIO_OAUTH_SCOPES'],
142
+ });
143
+ if (envOverride)
144
+ return envOverride;
145
+ try {
146
+ const raw = await readFile(getOverridePath(), 'utf-8');
147
+ const parsed = JSON.parse(raw);
148
+ return normalizeOverride(parsed);
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ }
154
+ function warnOnNonHttpsOverride(name, value) {
155
+ if (!value || /^https:\/\//i.test(value))
156
+ return;
157
+ console.warn(`[dario] OAuth override ${name} is non-HTTPS (${value}). ` +
158
+ 'Allowed as an emergency escape hatch, but double-check the source before using it.');
159
+ }
160
+ function applyManualOverride(config, override) {
161
+ if (!override)
162
+ return config;
163
+ warnOnNonHttpsOverride('authorizeUrl', override.authorizeUrl);
164
+ warnOnNonHttpsOverride('tokenUrl', override.tokenUrl);
165
+ return {
166
+ ...config,
167
+ ...override,
168
+ source: 'override',
169
+ };
170
+ }
99
171
  /**
100
172
  * Fast fingerprint of a binary for caching. We hash the first 64KB plus
101
173
  * size+mtime — this discriminates CC versions without reading GBs off disk.
@@ -131,9 +203,9 @@ export function scanBinaryForOAuthConfig(buf) {
131
203
  const anchorIdx = buf.indexOf(anchor);
132
204
  if (anchorIdx === -1)
133
205
  return null;
206
+ const windowStart = anchorIdx;
134
207
  // The prod config object is laid out roughly as one line of minified JS.
135
208
  // Take a generous window to be safe across minifier differences.
136
- const windowStart = anchorIdx;
137
209
  const windowEnd = Math.min(buf.length, anchorIdx + 2048);
138
210
  const prodBlock = buf.slice(windowStart, windowEnd).toString('latin1');
139
211
  const cidMatch = /CLIENT_ID\s*:\s*"([0-9a-f-]{36})"/i.exec(prodBlock);
@@ -142,7 +214,9 @@ export function scanBinaryForOAuthConfig(buf) {
142
214
  const clientId = cidMatch[1];
143
215
  // Defensive: if we somehow matched the dev client_id, reject — the
144
216
  // anchor should have put us in the prod block, but this guards against
145
- // the block being laid out in an unexpected order across builds.
217
+ // the block being laid out in an unexpected order across builds. Failing
218
+ // the scan and falling back is safer than authenticating against the
219
+ // wrong Anthropic OAuth client.
146
220
  if (clientId === '22422756-60c9-4084-8eb7-27705fd5cf9a')
147
221
  return null;
148
222
  let authorizeUrl = FALLBACK.authorizeUrl;
@@ -199,23 +273,22 @@ export async function detectCCOAuthConfig() {
199
273
  if (memoized)
200
274
  return memoized;
201
275
  try {
276
+ const manualOverride = await loadManualOverride();
202
277
  const ccPath = findCCBinary();
203
278
  if (!ccPath) {
204
- memoized = FALLBACK;
279
+ memoized = applyManualOverride(FALLBACK, manualOverride);
205
280
  return memoized;
206
281
  }
207
282
  const hash = await fingerprintBinary(ccPath);
208
- // Check cache
209
283
  const cached = await loadCache();
210
284
  if (cached && cached.hash === hash) {
211
- memoized = { ...cached.config, source: 'cached', ccPath, ccHash: hash };
285
+ memoized = applyManualOverride({ ...cached.config, source: 'cached', ccPath, ccHash: hash }, manualOverride);
212
286
  return memoized;
213
287
  }
214
- // Read binary and scan
215
288
  const buf = await readFile(ccPath);
216
289
  const scanned = scanBinaryForOAuthConfig(buf);
217
290
  if (!scanned) {
218
- memoized = { ...FALLBACK, ccPath, ccHash: hash };
291
+ memoized = applyManualOverride({ ...FALLBACK, ccPath, ccHash: hash }, manualOverride);
219
292
  return memoized;
220
293
  }
221
294
  const detected = {
@@ -225,11 +298,11 @@ export async function detectCCOAuthConfig() {
225
298
  ccHash: hash,
226
299
  };
227
300
  await saveCache(hash, detected);
228
- memoized = detected;
301
+ memoized = applyManualOverride(detected, manualOverride);
229
302
  return memoized;
230
303
  }
231
304
  catch {
232
- memoized = FALLBACK;
305
+ memoized = applyManualOverride(FALLBACK, await loadManualOverride());
233
306
  return memoized;
234
307
  }
235
308
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "_version": "2.1.112",
3
- "_captured": "2026-04-17T16:19:32.643Z",
2
+ "_version": "2.1.114",
3
+ "_captured": "2026-04-19T16:11:41.237Z",
4
4
  "_source": "bundled",
5
5
  "_schemaVersion": 3,
6
6
  "agent_identity": "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
@@ -170,7 +170,7 @@
170
170
  },
171
171
  {
172
172
  "name": "Bash",
173
- "description": "Executes a given bash command and returns its output.\n\nThe working directory persists between commands, but shell state does not. The shell environment is initialized from the user's profile (bash or zsh).\n\nIMPORTANT: Avoid using this tool to run `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user:\n\n - File search: Use Glob (NOT find or ls)\n - Content search: Use Grep (NOT grep or rg)\n - Read files: Use Read (NOT cat/head/tail)\n - Edit files: Use Edit (NOT sed/awk)\n - Write files: Use Write (NOT echo >/cat <<EOF)\n - Communication: Output text directly (NOT echo/printf)\nWhile the Bash tool can do similar things, it’s better to use the built-in tools as they provide a better user experience and make it easier to review tool calls and give permission.\n\n# Instructions\n - If your command will create new directories or files, first use this tool to run `ls` to verify the parent directory exists and is the correct location.\n - Always quote file paths that contain spaces with double quotes in your command (e.g., cd \"path with spaces/file.txt\")\n - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.\n - You may specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). By default, your command will timeout after 120000ms (2 minutes).\n - You can use the `run_in_background` parameter to run the command in the background. Only use this if you don't need the result immediately and are OK being notified when the command completes later. You do not need to check the output right away - you'll be notified when it finishes. You do not need to use '&' at the end of the command when using this parameter.\n - When issuing multiple commands:\n - If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message. Example: if you need to run \"git status\" and \"git diff\", send a single message with two Bash tool calls in parallel.\n - If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together.\n - Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.\n - DO NOT use newlines to separate commands (newlines are ok in quoted strings).\n - For git commands:\n - Prefer to create a new commit rather than amending an existing commit.\n - Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative that achieves the same goal. Only use destructive operations when they are truly the best approach.\n - Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.\n - Avoid unnecessary `sleep` commands:\n - Do not sleep between commands that can run immediately — just run them.\n - Use the Monitor tool to stream events from a background process (each stdout line is a notification). For one-shot \"wait until done,\" use Bash with run_in_background instead.\n - If your command is long running and you would like to be notified when it finishes — use `run_in_background`. No sleep needed.\n - Do not retry failing commands in a sleep loop — diagnose the root cause.\n - If waiting for a background task you started with `run_in_background`, you will be notified when it completes — do not poll.\n - Long leading `sleep` commands are blocked. To poll until a condition is met, use Monitor with an until-loop (e.g. `until <check>; do sleep 2; done`) — you get a notification when the loop exits. Do not chain shorter sleeps to work around the block.\n\n\n# Committing changes with git\n\nOnly create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully:\n\nYou can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. The numbered steps below indicate which commands should be batched in parallel.\n\nGit Safety Protocol:\n- NEVER update the git config\n- NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) unless the user explicitly requests these actions. Taking unauthorized destructive actions is unhelpful and can result in lost work, so it's best to ONLY run these commands when given direct instructions \n- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it\n- NEVER run force push to main/master, warn the user if they request it\n- CRITICAL: Always create NEW commits rather than amending, unless the user explicitly requests a git amend. When a pre-commit hook fails, the commit did NOT happen — so --amend would modify the PREVIOUS commit, which may result in destroying work or losing previous changes. Instead, after hook failure, fix the issue, re-stage, and create a NEW commit\n- When staging files, prefer adding specific files by name rather than using \"git add -A\" or \"git add .\", which can accidentally include sensitive files (.env, credentials) or large binaries\n- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive\n\n1. Run the following bash commands in parallel, each using the Bash tool:\n - Run a git status command to see all untracked files. IMPORTANT: Never use the -uall flag as it can cause memory issues on large repos.\n - Run a git diff command to see both staged and unstaged changes that will be committed.\n - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.\n2. Analyze all staged changes (both previously staged and newly added) and draft a commit message:\n - Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. \"add\" means a wholly new feature, \"update\" means an enhancement to an existing feature, \"fix\" means a bug fix, etc.).\n - Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files\n - Draft a concise (1-2 sentences) commit message that focuses on the \"why\" rather than the \"what\"\n - Ensure it accurately reflects the changes and their purpose\n3. Run the following commands in parallel:\n - Add relevant untracked files to the staging area.\n - Create the commit with a message ending with:\n Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n - Run git status after the commit completes to verify success.\n Note: git status depends on the commit completing, so run it sequentially after the commit.\n4. If the commit fails due to pre-commit hook: fix the issue and create a NEW commit\n\nImportant notes:\n- NEVER run additional commands to read or explore code, besides git bash commands\n- NEVER use the TodoWrite or Agent tools\n- DO NOT push to the remote repository unless the user explicitly asks you to do so\n- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.\n- IMPORTANT: Do not use --no-edit with git rebase commands, as the --no-edit flag is not a valid option for git rebase.\n- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit\n- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:\n<example>\ngit commit -m \"$(cat <<'EOF'\n Commit message here.\n\n Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n EOF\n )\"\n</example>\n\n# Creating pull requests\nUse the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.\n\nIMPORTANT: When the user asks you to create a pull request, follow these steps carefully:\n\n1. Run the following bash commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch:\n - Run a git status command to see all untracked files (never use -uall flag)\n - Run a git diff command to see both staged and unstaged changes that will be committed\n - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote\n - Run a git log command and `git diff [base-branch]...HEAD` to understand the full commit history for the current branch (from the time it diverged from the base branch)\n2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request title and summary:\n - Keep the PR title short (under 70 characters)\n - Use the description/body for details, not the title\n3. Run the following commands in parallel:\n - Create new branch if needed\n - Push to remote with -u flag if needed\n - Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.\n<example>\ngh pr create --title \"the pr title\" --body \"$(cat <<'EOF'\n## Summary\n<1-3 bullet points>\n\n## Test plan\n[Bulleted markdown checklist of TODOs for testing the pull request...]\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\nEOF\n)\"\n</example>\n\nImportant:\n- DO NOT use the TodoWrite or Agent tools\n- Return the PR URL when you're done, so the user can see it\n\n# Other common operations\n- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments",
173
+ "description": "Executes a given bash command and returns its output.\n\nThe working directory persists between commands, but shell state does not. The shell environment is initialized from the user's profile (bash or zsh).\n\nIMPORTANT: Avoid using this tool to run `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user:\n\n - File search: Use Glob (NOT find or ls)\n - Content search: Use Grep (NOT grep or rg)\n - Read files: Use Read (NOT cat/head/tail)\n - Edit files: Use Edit (NOT sed/awk)\n - Write files: Use Write (NOT echo >/cat <<EOF)\n - Communication: Output text directly (NOT echo/printf)\nWhile the Bash tool can do similar things, it’s better to use the built-in tools as they provide a better user experience and make it easier to review tool calls and give permission.\n\n# Instructions\n - If your command will create new directories or files, first use this tool to run `ls` to verify the parent directory exists and is the correct location.\n - Always quote file paths that contain spaces with double quotes in your command (e.g., cd \"path with spaces/file.txt\")\n - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it. In particular, never prepend `cd <current-directory>` to a `git` command — `git` already operates on the current working tree, and the compound triggers a permission prompt.\n - You may specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). By default, your command will timeout after 120000ms (2 minutes).\n - You can use the `run_in_background` parameter to run the command in the background. Only use this if you don't need the result immediately and are OK being notified when the command completes later. You do not need to check the output right away - you'll be notified when it finishes. You do not need to use '&' at the end of the command when using this parameter.\n - When issuing multiple commands:\n - If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message. Example: if you need to run \"git status\" and \"git diff\", send a single message with two Bash tool calls in parallel.\n - If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together.\n - Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.\n - DO NOT use newlines to separate commands (newlines are ok in quoted strings).\n - For git commands:\n - Prefer to create a new commit rather than amending an existing commit.\n - Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative that achieves the same goal. Only use destructive operations when they are truly the best approach.\n - Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.\n - Avoid unnecessary `sleep` commands:\n - Do not sleep between commands that can run immediately — just run them.\n - Use the Monitor tool to stream events from a background process (each stdout line is a notification). For one-shot \"wait until done,\" use Bash with run_in_background instead.\n - If your command is long running and you would like to be notified when it finishes — use `run_in_background`. No sleep needed.\n - Do not retry failing commands in a sleep loop — diagnose the root cause.\n - If waiting for a background task you started with `run_in_background`, you will be notified when it completes — do not poll.\n - Long leading `sleep` commands are blocked. To poll until a condition is met, use Monitor with an until-loop (e.g. `until <check>; do sleep 2; done`) — you get a notification when the loop exits. Do not chain shorter sleeps to work around the block.\n\n\n# Committing changes with git\n\nOnly create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully:\n\nYou can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. The numbered steps below indicate which commands should be batched in parallel.\n\nGit Safety Protocol:\n- NEVER update the git config\n- NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) unless the user explicitly requests these actions. Taking unauthorized destructive actions is unhelpful and can result in lost work, so it's best to ONLY run these commands when given direct instructions \n- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it\n- NEVER run force push to main/master, warn the user if they request it\n- CRITICAL: Always create NEW commits rather than amending, unless the user explicitly requests a git amend. When a pre-commit hook fails, the commit did NOT happen — so --amend would modify the PREVIOUS commit, which may result in destroying work or losing previous changes. Instead, after hook failure, fix the issue, re-stage, and create a NEW commit\n- When staging files, prefer adding specific files by name rather than using \"git add -A\" or \"git add .\", which can accidentally include sensitive files (.env, credentials) or large binaries\n- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive\n\n1. Run the following bash commands in parallel, each using the Bash tool:\n - Run a git status command to see all untracked files. IMPORTANT: Never use the -uall flag as it can cause memory issues on large repos.\n - Run a git diff command to see both staged and unstaged changes that will be committed.\n - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.\n2. Analyze all staged changes (both previously staged and newly added) and draft a commit message:\n - Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. \"add\" means a wholly new feature, \"update\" means an enhancement to an existing feature, \"fix\" means a bug fix, etc.).\n - Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files\n - Draft a concise (1-2 sentences) commit message that focuses on the \"why\" rather than the \"what\"\n - Ensure it accurately reflects the changes and their purpose\n3. Run the following commands in parallel:\n - Add relevant untracked files to the staging area.\n - Create the commit with a message ending with:\n Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n - Run git status after the commit completes to verify success.\n Note: git status depends on the commit completing, so run it sequentially after the commit.\n4. If the commit fails due to pre-commit hook: fix the issue and create a NEW commit\n\nImportant notes:\n- NEVER run additional commands to read or explore code, besides git bash commands\n- NEVER use the TodoWrite or Agent tools\n- DO NOT push to the remote repository unless the user explicitly asks you to do so\n- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.\n- IMPORTANT: Do not use --no-edit with git rebase commands, as the --no-edit flag is not a valid option for git rebase.\n- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit\n- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:\n<example>\ngit commit -m \"$(cat <<'EOF'\n Commit message here.\n\n Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n EOF\n )\"\n</example>\n\n# Creating pull requests\nUse the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.\n\nIMPORTANT: When the user asks you to create a pull request, follow these steps carefully:\n\n1. Run the following bash commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch:\n - Run a git status command to see all untracked files (never use -uall flag)\n - Run a git diff command to see both staged and unstaged changes that will be committed\n - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote\n - Run a git log command and `git diff [base-branch]...HEAD` to understand the full commit history for the current branch (from the time it diverged from the base branch)\n2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request title and summary:\n - Keep the PR title short (under 70 characters)\n - Use the description/body for details, not the title\n3. Run the following commands in parallel:\n - Create new branch if needed\n - Push to remote with -u flag if needed\n - Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.\n<example>\ngh pr create --title \"the pr title\" --body \"$(cat <<'EOF'\n## Summary\n<1-3 bullet points>\n\n## Test plan\n[Bulleted markdown checklist of TODOs for testing the pull request...]\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\nEOF\n)\"\n</example>\n\nImportant:\n- DO NOT use the TodoWrite or Agent tools\n- Return the PR URL when you're done, so the user can see it\n\n# Other common operations\n- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments",
174
174
  "input_schema": {
175
175
  "$schema": "https://json-schema.org/draft/2020-12/schema",
176
176
  "type": "object",
@@ -913,47 +913,43 @@
913
913
  "Write"
914
914
  ],
915
915
  "header_order": [
916
- "host",
917
- "connection",
918
916
  "accept",
919
- "x-stainless-retry-count",
920
- "x-stainless-timeout",
917
+ "content-type",
918
+ "user-agent",
919
+ "x-claude-code-session-id",
920
+ "x-stainless-arch",
921
921
  "x-stainless-lang",
922
- "x-stainless-package-version",
923
922
  "x-stainless-os",
924
- "x-stainless-arch",
923
+ "x-stainless-package-version",
924
+ "x-stainless-retry-count",
925
925
  "x-stainless-runtime",
926
926
  "x-stainless-runtime-version",
927
+ "x-stainless-timeout",
928
+ "anthropic-beta",
927
929
  "anthropic-dangerous-direct-browser-access",
928
930
  "anthropic-version",
929
931
  "x-api-key",
930
932
  "x-app",
931
- "user-agent",
932
- "x-claude-code-session-id",
933
- "content-type",
934
- "anthropic-beta",
935
- "accept-language",
936
- "sec-fetch-mode",
933
+ "connection",
934
+ "host",
937
935
  "accept-encoding",
938
936
  "content-length"
939
937
  ],
940
- "anthropic_beta": "claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24,afk-mode-2026-01-31",
938
+ "anthropic_beta": "claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advisor-tool-2026-03-01,effort-2025-11-24,afk-mode-2026-01-31",
941
939
  "header_values": {
942
940
  "accept": "application/json",
943
- "x-stainless-retry-count": "0",
944
- "x-stainless-timeout": "600",
941
+ "user-agent": "claude-cli/2.1.114 (external, sdk-cli)",
942
+ "x-stainless-arch": "x64",
945
943
  "x-stainless-lang": "js",
946
- "x-stainless-package-version": "0.81.0",
947
944
  "x-stainless-os": "Windows",
948
- "x-stainless-arch": "x64",
945
+ "x-stainless-package-version": "0.81.0",
946
+ "x-stainless-retry-count": "0",
949
947
  "x-stainless-runtime": "node",
950
- "x-stainless-runtime-version": "v22.21.1",
948
+ "x-stainless-runtime-version": "v24.3.0",
949
+ "x-stainless-timeout": "600",
951
950
  "anthropic-dangerous-direct-browser-access": "true",
952
951
  "anthropic-version": "2023-06-01",
953
- "x-app": "cli",
954
- "user-agent": "claude-cli/2.1.112 (external, sdk-cli)",
955
- "accept-language": "*",
956
- "sec-fetch-mode": "cors"
952
+ "x-app": "cli"
957
953
  },
958
954
  "body_field_order": [
959
955
  "model",
@@ -271,7 +271,7 @@ export declare function _resetInstalledVersionProbeForTest(): void;
271
271
  */
272
272
  export declare const SUPPORTED_CC_RANGE: {
273
273
  readonly min: "1.0.0";
274
- readonly maxTested: "2.1.112";
274
+ readonly maxTested: "2.1.114";
275
275
  };
276
276
  /**
277
277
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
@@ -711,7 +711,7 @@ export function _resetInstalledVersionProbeForTest() {
711
711
  */
712
712
  export const SUPPORTED_CC_RANGE = {
713
713
  min: '1.0.0',
714
- maxTested: '2.1.112',
714
+ maxTested: '2.1.114',
715
715
  };
716
716
  /**
717
717
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
package/dist/proxy.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type IncomingMessage } from 'node:http';
1
2
  export declare function parseProviderPrefix(model: string): {
2
3
  provider: 'openai' | 'claude';
3
4
  model: string;
@@ -22,5 +23,12 @@ interface ProxyOptions {
22
23
  sessionPerClient?: boolean;
23
24
  }
24
25
  export declare function sanitizeError(err: unknown): string;
26
+ /**
27
+ * Two-lane auth: DARIO_API_KEY (x-api-key / Authorization: Bearer) for
28
+ * normal clients, and MUX_COORD_SECRET (X-Mux-Coord-Secret) for the mux
29
+ * gateway forwarding a verified sealed borrow. If neither is configured
30
+ * the request is allowed (loopback-only default). Exported for tests.
31
+ */
32
+ export declare function authenticateRequest(headers: IncomingMessage['headers'], apiKeyBuf: Buffer | null, mcsBuf: Buffer | null): boolean;
25
33
  export declare function startProxy(opts?: ProxyOptions): Promise<void>;
26
34
  export {};
package/dist/proxy.js CHANGED
@@ -333,6 +333,35 @@ export function sanitizeError(err) {
333
333
  .replace(/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, '[REDACTED_JWT]')
334
334
  .replace(/Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]');
335
335
  }
336
+ /**
337
+ * Two-lane auth: DARIO_API_KEY (x-api-key / Authorization: Bearer) for
338
+ * normal clients, and MUX_COORD_SECRET (X-Mux-Coord-Secret) for the mux
339
+ * gateway forwarding a verified sealed borrow. If neither is configured
340
+ * the request is allowed (loopback-only default). Exported for tests.
341
+ */
342
+ export function authenticateRequest(headers, apiKeyBuf, mcsBuf) {
343
+ if (!apiKeyBuf && !mcsBuf)
344
+ return true;
345
+ if (mcsBuf) {
346
+ const raw = headers['x-mux-coord-secret'];
347
+ const provided = typeof raw === 'string' ? raw : Array.isArray(raw) ? raw[0] : undefined;
348
+ if (provided) {
349
+ const providedBuf = Buffer.from(provided);
350
+ if (providedBuf.length === mcsBuf.length && timingSafeEqual(providedBuf, mcsBuf))
351
+ return true;
352
+ }
353
+ }
354
+ if (apiKeyBuf) {
355
+ const provided = headers['x-api-key']
356
+ || headers.authorization?.replace(/^Bearer\s+/i, '');
357
+ if (provided) {
358
+ const providedBuf = Buffer.from(provided);
359
+ if (providedBuf.length === apiKeyBuf.length && timingSafeEqual(providedBuf, apiKeyBuf))
360
+ return true;
361
+ }
362
+ }
363
+ return false;
364
+ }
336
365
  /**
337
366
  * Enrich Anthropic's unhelpful 429 "Error" body with rate limit details from headers.
338
367
  */
@@ -618,6 +647,14 @@ export async function startProxy(opts = {}) {
618
647
  // Optional proxy authentication — pre-encode key buffer for performance
619
648
  const apiKey = process.env.DARIO_API_KEY;
620
649
  const apiKeyBuf = apiKey ? Buffer.from(apiKey) : null;
650
+ // Mux coord-secret — the shared secret mux uses when forwarding sealed
651
+ // borrow requests to this dario acting as a lender endpoint. A request
652
+ // carrying a matching X-Mux-Coord-Secret header is authenticated without
653
+ // needing DARIO_API_KEY. The sealed-sender envelope has already been
654
+ // verified upstream, so the coord secret is the one-hop auth between
655
+ // the mux gateway and this instance.
656
+ const mcs = process.env.MUX_COORD_SECRET;
657
+ const mcsBuf = mcs ? Buffer.from(mcs) : null;
621
658
  // CORS origin defaults to the localhost URL the proxy is served at. Users
622
659
  // binding to a non-loopback address (e.g. a Tailscale interface) can
623
660
  // override via DARIO_CORS_ORIGIN — otherwise browser-based clients hitting
@@ -633,7 +670,7 @@ export async function startProxy(opts = {}) {
633
670
  const CORS_HEADERS = {
634
671
  'Access-Control-Allow-Origin': corsOrigin,
635
672
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
636
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta',
673
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta, x-mux-coord-secret',
637
674
  'Access-Control-Max-Age': '86400',
638
675
  ...SECURITY_HEADERS,
639
676
  };
@@ -643,16 +680,7 @@ export async function startProxy(opts = {}) {
643
680
  const ERR_FORBIDDEN = JSON.stringify({ error: 'Forbidden', message: 'Path not allowed. Supported paths: POST /v1/messages, POST /v1/chat/completions, GET /v1/models' });
644
681
  const ERR_METHOD = JSON.stringify({ error: 'Method not allowed' });
645
682
  function checkAuth(req) {
646
- if (!apiKeyBuf)
647
- return true;
648
- const provided = req.headers['x-api-key']
649
- || req.headers.authorization?.replace(/^Bearer\s+/i, '');
650
- if (!provided)
651
- return false;
652
- const providedBuf = Buffer.from(provided);
653
- if (providedBuf.length !== apiKeyBuf.length)
654
- return false;
655
- return timingSafeEqual(providedBuf, apiKeyBuf);
683
+ return authenticateRequest(req.headers, apiKeyBuf, mcsBuf);
656
684
  }
657
685
  const server = createServer(async (req, res) => {
658
686
  if (req.method === 'OPTIONS') {
@@ -1779,14 +1807,19 @@ export async function startProxy(opts = {}) {
1779
1807
  if (!isLoopbackHost(host)) {
1780
1808
  console.log('');
1781
1809
  console.log(` ⚠ Bound to ${host} — reachable from other machines on the network.`);
1782
- if (!apiKey) {
1783
- console.log(' DARIO_API_KEY is not set. Any host that can reach this port can');
1784
- console.log(' proxy requests through your OAuth subscription. Set DARIO_API_KEY');
1785
- console.log(' before exposing dario beyond loopback.');
1810
+ if (!apiKey && !mcs) {
1811
+ console.log(' No auth configured. Any host that can reach this port can proxy');
1812
+ console.log(' requests through your OAuth subscription. Set DARIO_API_KEY (for');
1813
+ console.log(' normal clients) or MUX_COORD_SECRET (for mux lender mode) before');
1814
+ console.log(' exposing dario beyond loopback.');
1786
1815
  }
1787
1816
  else {
1788
- console.log(' DARIO_API_KEY is set — clients must send x-api-key or Authorization');
1789
- console.log(' to be accepted.');
1817
+ const lanes = [];
1818
+ if (apiKey)
1819
+ lanes.push('x-api-key / Authorization (DARIO_API_KEY)');
1820
+ if (mcs)
1821
+ lanes.push('X-Mux-Coord-Secret (MUX_COORD_SECRET — mux lender mode)');
1822
+ console.log(` Auth required — accepted credentials: ${lanes.join(' or ')}.`);
1790
1823
  }
1791
1824
  }
1792
1825
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.28.0",
3
+ "version": "3.29.1",
4
4
  "description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsc && cp src/cc-template-data.json dist/ && node -e \"require('fs').mkdirSync('dist/shim',{recursive:true})\" && cp src/shim/runtime.cjs dist/shim/",
24
- "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/pacing.mjs && node test/stream-drain.mjs && node test/subagent.mjs && node test/mcp-protocol.mjs && node test/mcp-tools.mjs && node test/mcp-e2e.mjs && node test/session-rotation.mjs && node test/drift-detection.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs",
24
+ "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/mux-coord-secret.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/pacing.mjs && node test/stream-drain.mjs && node test/subagent.mjs && node test/mcp-protocol.mjs && node test/mcp-tools.mjs && node test/mcp-e2e.mjs && node test/session-rotation.mjs && node test/drift-detection.mjs && node test/cc-authorize-probe-classifier.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs",
25
25
  "audit": "npm audit --production --audit-level=high",
26
26
  "prepublishOnly": "npm run build",
27
27
  "start": "node dist/cli.js",