@askalf/dario 5.4.17 → 5.4.20

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.
@@ -170,6 +170,17 @@ export declare const CC_AGENT_IDENTITY: string;
170
170
  */
171
171
  export declare const CLIENT_SYSTEM_PREFACE: string;
172
172
  export declare function resolveSystemPrompt(arg: string | undefined, model?: string): string;
173
+ /**
174
+ * Pick the CC-identity headers out of an inbound request so the passthrough
175
+ * path can forward them unchanged.
176
+ *
177
+ * Pure and exported so the allow/deny behaviour is unit-testable without
178
+ * standing up the proxy, matching `orderHeadersForOutbound` and
179
+ * `overlayTemplateHeaderValues`. Array-valued headers (Node allows repeats)
180
+ * take the first value; empty strings are skipped so a client sending a blank
181
+ * header cannot blank out a value dario would otherwise supply.
182
+ */
183
+ export declare function forwardClientCCIdentityHeaders(reqHeaders: Record<string, string | string[] | undefined>): Record<string, string>;
173
184
  /**
174
185
  * Overlay a captured template's `header_values` onto the outbound header record,
175
186
  * skipping the keys that must never be replayed.
@@ -283,6 +283,74 @@ function stripBehavioralConstraints(input, level) {
283
283
  * so new captures are clean. This skip list is what makes every ALREADY-baked
284
284
  * template and warm cache self-heal without waiting for a re-bake.
285
285
  */
286
+ /**
287
+ * Headers a genuine Claude Code client sends to identify itself, which the
288
+ * passthrough path forwards verbatim instead of substituting template values.
289
+ *
290
+ * The template exists to SYNTHESISE CC's shape for clients that are not CC. On
291
+ * the passthrough path `isGenuineCCClient` has already established that the
292
+ * caller really is Claude Code, so its own headers are the authentic article and
293
+ * a capture is at best a good imitation of them. Measured before this changed:
294
+ * of 13 CC-identity headers a real client sent, 12 were replaced with template
295
+ * values and 1 was dropped — 0 forwarded (dario#885).
296
+ *
297
+ * Deliberately NOT in this list, because dario must own them:
298
+ *
299
+ * authorization / x-api-key must become the pool account's credential
300
+ * x-claude-code-session-id session rotation is a feature, not an accident
301
+ * anthropic-beta merged with operator pins + the per-account
302
+ * rejection cache; the client's set is not final
303
+ * anthropic-version already read from the client at the call site
304
+ * accept / content-type body framing, owned by the proxy
305
+ * host / connection / transport, owned by the HTTP stack; forwarding
306
+ * content-length / them corrupts the request
307
+ * transfer-encoding /
308
+ * accept-encoding / keep-alive
309
+ *
310
+ * Anything a future CC adds under the `x-stainless-*` or `x-claude-code-*`
311
+ * prefixes is forwarded by the prefix rules in
312
+ * `forwardClientCCIdentityHeaders` rather than needing to be enumerated here —
313
+ * the capture cannot see first-party-conditional headers at all (dario#885), so
314
+ * an allowlist of exact names would silently miss them.
315
+ */
316
+ const CC_IDENTITY_HEADERS_TO_FORWARD = new Set([
317
+ 'user-agent',
318
+ 'x-app',
319
+ 'anthropic-dangerous-direct-browser-access',
320
+ ]);
321
+ /** Prefixes whose every member is CC self-identification, forwarded wholesale. */
322
+ const CC_IDENTITY_HEADER_PREFIXES = ['x-stainless-', 'x-claude-code-', 'x-client-'];
323
+ /** Never forwarded from the client even when it matches a prefix above. */
324
+ const NEVER_FORWARD_FROM_CLIENT = new Set([
325
+ 'x-claude-code-session-id', // dario rotates sessions deliberately
326
+ ]);
327
+ /**
328
+ * Pick the CC-identity headers out of an inbound request so the passthrough
329
+ * path can forward them unchanged.
330
+ *
331
+ * Pure and exported so the allow/deny behaviour is unit-testable without
332
+ * standing up the proxy, matching `orderHeadersForOutbound` and
333
+ * `overlayTemplateHeaderValues`. Array-valued headers (Node allows repeats)
334
+ * take the first value; empty strings are skipped so a client sending a blank
335
+ * header cannot blank out a value dario would otherwise supply.
336
+ */
337
+ export function forwardClientCCIdentityHeaders(reqHeaders) {
338
+ const out = {};
339
+ for (const [rawName, rawValue] of Object.entries(reqHeaders)) {
340
+ const name = rawName.toLowerCase();
341
+ if (NEVER_FORWARD_FROM_CLIENT.has(name))
342
+ continue;
343
+ const allowed = CC_IDENTITY_HEADERS_TO_FORWARD.has(name)
344
+ || CC_IDENTITY_HEADER_PREFIXES.some((p) => name.startsWith(p));
345
+ if (!allowed)
346
+ continue;
347
+ const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
348
+ if (typeof value !== 'string' || value.length === 0)
349
+ continue;
350
+ out[name] = value;
351
+ }
352
+ return out;
353
+ }
286
354
  const NEVER_REPLAY_HEADER_VALUES = new Set([
287
355
  'x-api-key',
288
356
  'x-stainless-os',
@@ -336,12 +336,6 @@ export interface DriftResult {
336
336
  * callers pass nothing and the real binary probe runs.
337
337
  */
338
338
  export declare function detectDrift(t: TemplateData, installedOverride?: string | null): DriftResult;
339
- /**
340
- * Reset the memoized `claude --version` probe. Test-only — production
341
- * code should never need to clear the cache since the installed binary
342
- * doesn't change mid-process.
343
- */
344
- export declare function _resetInstalledVersionProbeForTest(): void;
345
339
  /**
346
340
  * The CC version range the current dario release has been exercised
347
341
  * against. Update `maxTested` every time we validate against a new CC
@@ -919,14 +919,6 @@ export function detectDrift(t, installedOverride) {
919
919
  message: `cache is from CC v${cachedVersion} but installed CC is v${installed} — background refresh will re-capture`,
920
920
  };
921
921
  }
922
- /**
923
- * Reset the memoized `claude --version` probe. Test-only — production
924
- * code should never need to clear the cache since the installed binary
925
- * doesn't change mid-process.
926
- */
927
- export function _resetInstalledVersionProbeForTest() {
928
- _installedVersionProbe = { value: null, cached: false };
929
- }
930
922
  // ============================================================
931
923
  // CC version compat matrix (v3.17)
932
924
  // ============================================================
package/dist/proxy.js CHANGED
@@ -9,7 +9,7 @@ import { arch, platform } from 'node:process';
9
9
  import { getAccessToken, getStatus } from './oauth.js';
10
10
  import { buildHealthResponse, derivePoolStatus, shouldDiscloseHealthInternals } from './health-response.js';
11
11
  import { darioVersion } from './version.js';
12
- import { buildCCRequest, applyCcPromptCaching, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, overlayTemplateHeaderValues, isMcpToolName, CC_TEMPLATE, effectiveCacheControl, withForced1hBeta } from './cc-template.js';
12
+ import { buildCCRequest, applyCcPromptCaching, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, overlayTemplateHeaderValues, forwardClientCCIdentityHeaders, isMcpToolName, CC_TEMPLATE, effectiveCacheControl, withForced1hBeta } from './cc-template.js';
13
13
  import { stampCch, hasCchSeed } from './cch.js';
14
14
  import { describeTemplate, detectDrift, checkCCCompat } from './live-fingerprint.js';
15
15
  import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, reconcilePoolAccounts, resolvePoolStrategy } from './pool.js';
@@ -2064,6 +2064,12 @@ export async function startProxy(opts = {}) {
2064
2064
  let requestModel = '';
2065
2065
  let detectedClientForLog;
2066
2066
  let preserveToolsEffective = Boolean(opts.preserveTools);
2067
+ // Per-request: did isGenuineCCClient recognise the caller as real Claude
2068
+ // Code? Hoisted because the header build below needs it and `genuineCC` is
2069
+ // block-scoped where buildCCRequest destructures it. NOT the same thing as
2070
+ // `passthrough`, which is a startup CLI flag — conflating the two is why
2071
+ // the first version of this forwarded nothing (dario#885).
2072
+ let genuineCCRequest = false;
2067
2073
  try {
2068
2074
  // Select an account by headroom (v5.0: the pool is the one credential
2069
2075
  // model, so every OAuth request selects from it — a plain `dario login`
@@ -2442,6 +2448,7 @@ export async function startProxy(opts = {}) {
2442
2448
  preserveToolsEffective = Boolean(opts.preserveTools)
2443
2449
  || Boolean(genuineCC)
2444
2450
  || (Boolean(detectedClient) && !opts.hybridTools && !opts.mergeTools);
2451
+ genuineCCRequest = Boolean(genuineCC);
2445
2452
  if (genuineCC && !ccPassthroughLogged) {
2446
2453
  ccPassthroughLogged = true;
2447
2454
  console.log('[dario] genuine Claude Code client — system + tools forwarded verbatim (byte-faithful passthrough)');
@@ -2688,15 +2695,31 @@ export async function startProxy(opts = {}) {
2688
2695
  console.log(`[dario] #${requestCount} session: rotate (${assigned.reason})${poolAccount ? ` [${poolAccount.alias}]` : ''}`);
2689
2696
  }
2690
2697
  }
2698
+ // On the passthrough path `isGenuineCCClient` has already established the
2699
+ // caller IS Claude Code, so its own identity headers are the authentic
2700
+ // article — forward them instead of substituting template values. The
2701
+ // template is for synthesising CC's shape when the client is not CC; here it
2702
+ // would only be an imitation of headers we already hold. Measured before
2703
+ // this: 12 of 13 client CC-identity headers were replaced and 1 dropped,
2704
+ // 0 forwarded (dario#885). Spread AFTER staticHeaders so the client wins
2705
+ // over template values, and BEFORE the dario-owned block below so auth,
2706
+ // session rotation and the merged beta set still win over the client.
2707
+ const forwardedIdentity = (passthrough || genuineCCRequest)
2708
+ ? forwardClientCCIdentityHeaders(req.headers)
2709
+ : {};
2691
2710
  const headers = {
2692
2711
  ...staticHeaders,
2712
+ ...forwardedIdentity,
2693
2713
  ...upstreamAuthHeaders(upstreamApiKey, accessToken),
2694
2714
  'x-claude-code-session-id': outboundSessionId,
2695
2715
  'anthropic-version': passthrough ? (req.headers['anthropic-version'] || '2023-06-01') : '2023-06-01',
2696
2716
  'anthropic-beta': beta,
2697
- 'x-client-request-id': randomUUID(),
2717
+ // Prefer the client's own when passthrough forwarded one: a genuine CC
2718
+ // request already carries a real request id, and synthesising over it
2719
+ // discards information for no gain. Falls back to a fresh uuid otherwise.
2720
+ 'x-client-request-id': forwardedIdentity['x-client-request-id'] ?? randomUUID(),
2698
2721
  // CC sends 600 on first request per session. With rotation, every request is "first"
2699
- 'x-stainless-timeout': '600',
2722
+ 'x-stainless-timeout': forwardedIdentity['x-stainless-timeout'] ?? '600',
2700
2723
  };
2701
2724
  // Client-disconnect abort: if the client drops the connection before
2702
2725
  // we've finished sending the response, we default to aborting the
package/dist/version.d.ts CHANGED
@@ -1,3 +1 @@
1
1
  export declare function darioVersion(): string;
2
- /** Test-only: clear the memoized version. */
3
- export declare function _resetVersionCacheForTest(): void;
package/dist/version.js CHANGED
@@ -26,7 +26,3 @@ export function darioVersion() {
26
26
  cached = v;
27
27
  return v;
28
28
  }
29
- /** Test-only: clear the memoized version. */
30
- export function _resetVersionCacheForTest() {
31
- cached = null;
32
- }
@@ -11,4 +11,6 @@ Between v3.22 and v3.28, dario's Claude backend closed six axes along which a pr
11
11
  | **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. |
12
12
  | **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 [`mcp-server.md`](./mcp-server.md) and [`sub-agent.md`](./sub-agent.md). |
13
13
 
14
- The six-direction wire-fidelity roadmap is complete. Subsequent releases return to responding to issues and upstream template drift.
14
+ | **Client identity headers** | v5.4.19 | On the passthrough path `isGenuineCCClient` has already established the caller *is* Claude Code, so its own identity headers (`user-agent`, `x-app`, `x-stainless-*`, `x-claude-code-*`, `x-client-*`) are forwarded unchanged instead of being replaced with template values. The template exists to synthesise CC's shape for clients that are not CC; where the genuine article is in hand, forwarding beats imitating. Measured before the change: of 13 identity headers a real client sent, 12 were replaced and 1 dropped — 0 forwarded (#885). Auth, session id and the merged beta set stay dario's. | Automatic on the genuine-CC path; nothing to tune. Non-CC clients are unaffected, and the gate reads the request **body**, so headers cannot self-authorise. |
15
+
16
+ The original six-direction roadmap is complete; the axes below it are later findings. Note that header **order** is deliberately absent as an axis: `orderHeadersForOutbound` builds the captured sequence, but `fetch()` re-normalises it before the wire, and on Bun you cannot have both CC's JA3 and raw header control in one process (#813). The honest framing of the whole table is byte-identical **body**, structurally close **headers** — not packet-identical.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.4.17",
3
+ "version": "5.4.20",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@
24
24
  "scripts": {
25
25
  "build": "tsc && cp src/cc-template-data.json dist/",
26
26
  "test": "node --test --test-concurrency=8 test/all.test.mjs",
27
- "test:serial": "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/live-fingerprint.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/doctor-identity-drift.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/durable-token-persist.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs && node test/context-bleed.mjs && node test/capture-provenance.mjs && node test/sanitize-messages.mjs && node test/platform-tools.mjs && node test/strict-template-flags.mjs && node test/request-queue.mjs && node test/effort-flag.mjs && node test/template-invariants.mjs",
27
+ "test:serial": "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/live-fingerprint.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/doctor-identity-drift.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/durable-token-persist.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs && node test/context-bleed.mjs && node test/passthrough-header-forwarding.mjs && node test/capture-provenance.mjs && node test/sanitize-messages.mjs && node test/platform-tools.mjs && node test/strict-template-flags.mjs && node test/request-queue.mjs && node test/effort-flag.mjs && node test/template-invariants.mjs",
28
28
  "audit": "npm audit --production --audit-level=high",
29
29
  "prepublishOnly": "npm run build",
30
30
  "start": "node dist/cli.js",