@askalf/dario 4.8.72 → 4.8.74

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.
@@ -39,10 +39,13 @@ export declare const CC_TOOL_DEFINITIONS: {
39
39
  description: string;
40
40
  input_schema: Record<string, unknown>;
41
41
  }[];
42
- /** Lowercased CC tool name canonical name, for identity-matching a CC client's
43
- * own tools (so newer built-ins not yet in TOOL_MAP map to themselves rather
44
- * than being treated as foreign and round-robined). Tracks the live bundle. */
45
- export declare const CC_NATIVE_LOWER: Map<string, string>;
42
+ /** CC's own tool names, EXACT case ("Read", "Bash", "Agent", …). A CC client's
43
+ * tools identity-map to themselves and OVERRIDE TOOL_MAP whose lowercase
44
+ * cross-client aliases ('read' {path}/{filePath}) would otherwise mistranslate
45
+ * a CC tool (Read's file_path → path). Exact case is the discriminator: CC sends
46
+ * PascalCase, the {path}-style clients send lowercase/snake, so a non-CC `read`
47
+ * still routes through TOOL_MAP. Tracks the live bundle (refreshed each bake). */
48
+ export declare const CC_NATIVE_NAMES: Set<string>;
46
49
  /** CC's static system prompt (~25KB). */
47
50
  export declare const CC_SYSTEM_PROMPT: string;
48
51
  /** CC's agent identity string. */
@@ -45,10 +45,13 @@ export function filterToolsForPlatform(tools, platform) {
45
45
  }
46
46
  /** CC's exact tool definitions for the current platform — filtered from the bundled union. */
47
47
  export const CC_TOOL_DEFINITIONS = filterToolsForPlatform(TEMPLATE.tools, process.platform);
48
- /** Lowercased CC tool name canonical name, for identity-matching a CC client's
49
- * own tools (so newer built-ins not yet in TOOL_MAP map to themselves rather
50
- * than being treated as foreign and round-robined). Tracks the live bundle. */
51
- export const CC_NATIVE_LOWER = new Map(CC_TOOL_DEFINITIONS.map((t) => [String(t.name).toLowerCase(), String(t.name)]));
48
+ /** CC's own tool names, EXACT case ("Read", "Bash", "Agent", …). A CC client's
49
+ * tools identity-map to themselves and OVERRIDE TOOL_MAP whose lowercase
50
+ * cross-client aliases ('read' {path}/{filePath}) would otherwise mistranslate
51
+ * a CC tool (Read's file_path → path). Exact case is the discriminator: CC sends
52
+ * PascalCase, the {path}-style clients send lowercase/snake, so a non-CC `read`
53
+ * still routes through TOOL_MAP. Tracks the live bundle (refreshed each bake). */
54
+ export const CC_NATIVE_NAMES = new Set(CC_TOOL_DEFINITIONS.map((t) => String(t.name)));
52
55
  /** CC's static system prompt (~25KB). */
53
56
  export const CC_SYSTEM_PROMPT = TEMPLATE.system_prompt;
54
57
  /** CC's agent identity string. */
@@ -1234,19 +1237,20 @@ export function buildCCRequest(clientBody, billingTag, cacheControl, identity, o
1234
1237
  const claimedCC = new Set();
1235
1238
  for (const tool of clientTools) {
1236
1239
  const name = (tool.name || '').toLowerCase();
1237
- // A CC client's OWN tools map to THEMSELVES (identity). TOOL_MAP only
1238
- // carries the legacy core surface + cross-client aliases, so it lags CC's
1239
- // newer built-ins (Agent, AskUserQuestion, Cron*, Task*, NotebookEdit,
1240
- // Enter/ExitPlanMode, Workflow, …). Without this, those land "unmapped",
1241
- // get round-robined onto Read/Bash/etc., and the collision corrupts the
1242
- // calls of the tools that DID map (the dock saw Read's `file_path` arrive
1243
- // as `path`/`filePath`). Identity-matching against the live CC tool set
1244
- // (CC_NATIVE_LOWER, refreshed by every capture-and-bake) fixes it for
1245
- // every current-and-future CC client and keeps the canonical CC fingerprint.
1246
- const mapping = TOOL_MAP[name]
1247
- ?? (CC_NATIVE_LOWER.get(name)
1248
- ? { ccTool: CC_NATIVE_LOWER.get(name), translateArgs: (a) => a, translateBack: (a) => a }
1249
- : undefined);
1240
+ // A CC client's OWN tools map to THEMSELVES (identity), and this OVERRIDES
1241
+ // TOOL_MAP. Two failure modes it fixes, both seen via the dock:
1242
+ // 1. TOOL_MAP's lowercase cross-client aliases mistranslate a CC tool —
1243
+ // `Read` TOOL_MAP['read'] whose translateBack emits {path, filePath}
1244
+ // instead of {file_path}, so every Read failed validation client-side.
1245
+ // 2. CC's newer built-ins (Agent, AskUserQuestion, Cron*, Task*, Workflow,
1246
+ // NotebookEdit, Enter/ExitPlanMode, ) aren't in TOOL_MAP at all, so
1247
+ // they were round-robined onto Read/Bash/etc. and collided.
1248
+ // Exact case is the discriminator (CC sends PascalCase; {path}-style clients
1249
+ // send lowercase/snake) so a genuine non-CC `read` still routes via TOOL_MAP.
1250
+ // Tracks the live bundle, so future CC tools are covered after the next bake.
1251
+ const mapping = CC_NATIVE_NAMES.has(tool.name)
1252
+ ? { ccTool: tool.name, translateArgs: (a) => a, translateBack: (a) => a }
1253
+ : TOOL_MAP[name];
1250
1254
  if (mapping) {
1251
1255
  // In hybrid mode, clone the shared mapping and attach the
1252
1256
  // client-declared top-level field names from input_schema.
@@ -1282,8 +1286,8 @@ export function buildCCRequest(clientBody, billingTag, cacheControl, identity, o
1282
1286
  const CC_FALLBACK_TOOLS = ['Bash', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
1283
1287
  for (const tool of clientTools) {
1284
1288
  const name = (tool.name || '').toLowerCase();
1285
- if (TOOL_MAP[name] || CC_NATIVE_LOWER.has(name))
1286
- continue; // mapped, or a CC-native tool (identity in pass 1)
1289
+ if (CC_NATIVE_NAMES.has(tool.name) || TOOL_MAP[name])
1290
+ continue; // CC-native (identity in pass 1) or mapped
1287
1291
  unmappedTools.push(tool.name);
1288
1292
  if (opts.hybridTools)
1289
1293
  continue; // dropped — see comment above
@@ -0,0 +1,28 @@
1
+ /**
2
+ * /health response builder — extracted so the public-vs-internal disclosure rule
3
+ * is unit-testable without spinning a proxy.
4
+ *
5
+ * dario's /health is auth-free (docker healthchecks + `depends_on: service_healthy`
6
+ * need it before any secret is configured). When dario sits behind a Cloudflare
7
+ * tunnel with a public /health bypass (uptime monitoring), that endpoint is
8
+ * world-readable — so it must not leak OAuth internals (token countdown, request
9
+ * volume, refresh errors). The Cloudflare edge stamps `cf-ray` on every request it
10
+ * proxies, so its presence marks a request as having come from the public internet.
11
+ * Internal callers (the docker healthcheck, `dario doctor`, the self-probe) hit
12
+ * dario directly on loopback with no CF headers and still get the full detail.
13
+ *
14
+ * The HTTP status (200 healthy / 503 degraded) is identical either way, so external
15
+ * uptime monitoring that keys on the status code is unaffected.
16
+ */
17
+ export interface HealthStatusLike {
18
+ status: string;
19
+ canRefresh?: boolean;
20
+ expiresIn?: string;
21
+ refreshFailures?: number;
22
+ lastRefreshError?: string;
23
+ }
24
+ export interface HealthResponse {
25
+ httpStatus: number;
26
+ body: Record<string, unknown>;
27
+ }
28
+ export declare function buildHealthResponse(s: HealthStatusLike, requestCount: number, viaPublicTunnel: boolean): HealthResponse;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * /health response builder — extracted so the public-vs-internal disclosure rule
3
+ * is unit-testable without spinning a proxy.
4
+ *
5
+ * dario's /health is auth-free (docker healthchecks + `depends_on: service_healthy`
6
+ * need it before any secret is configured). When dario sits behind a Cloudflare
7
+ * tunnel with a public /health bypass (uptime monitoring), that endpoint is
8
+ * world-readable — so it must not leak OAuth internals (token countdown, request
9
+ * volume, refresh errors). The Cloudflare edge stamps `cf-ray` on every request it
10
+ * proxies, so its presence marks a request as having come from the public internet.
11
+ * Internal callers (the docker healthcheck, `dario doctor`, the self-probe) hit
12
+ * dario directly on loopback with no CF headers and still get the full detail.
13
+ *
14
+ * The HTTP status (200 healthy / 503 degraded) is identical either way, so external
15
+ * uptime monitoring that keys on the status code is unaffected.
16
+ */
17
+ export function buildHealthResponse(s, requestCount, viaPublicTunnel) {
18
+ const dead = s.status === 'broken' ||
19
+ s.status === 'none' ||
20
+ (s.status === 'expired' && s.canRefresh === false);
21
+ const httpStatus = dead ? 503 : 200;
22
+ const liveness = { status: dead ? 'degraded' : 'ok' };
23
+ const body = viaPublicTunnel
24
+ ? liveness
25
+ : {
26
+ ...liveness,
27
+ oauth: s.status,
28
+ expiresIn: s.expiresIn,
29
+ requests: requestCount,
30
+ ...(s.refreshFailures ? { refreshFailures: s.refreshFailures } : {}),
31
+ ...(s.lastRefreshError ? { lastRefreshError: s.lastRefreshError } : {}),
32
+ };
33
+ return { httpStatus, body };
34
+ }
package/dist/proxy.js CHANGED
@@ -7,6 +7,7 @@ import { homedir } from 'node:os';
7
7
  import { setDefaultResultOrder } from 'node:dns';
8
8
  import { arch, platform } from 'node:process';
9
9
  import { getAccessToken, getStatus } from './oauth.js';
10
+ import { buildHealthResponse } from './health-response.js';
10
11
  import { buildCCRequest, applyCcPromptCaching, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, CC_TEMPLATE } from './cc-template.js';
11
12
  import { describeTemplate, detectDrift, checkCCCompat } from './live-fingerprint.js';
12
13
  import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs } from './pool.js';
@@ -1110,18 +1111,12 @@ export async function startProxy(opts = {}) {
1110
1111
  // react instead of cheerfully passing while every /v1/messages 401s.
1111
1112
  if (urlPath === '/health' || urlPath === '/') {
1112
1113
  const s = await getStatus();
1113
- const dead = s.status === 'broken' || s.status === 'none' ||
1114
- (s.status === 'expired' && s.canRefresh === false);
1115
- const httpStatus = dead ? 503 : 200;
1114
+ // Public requests arrive through the Cloudflare tunnel (the edge stamps
1115
+ // `cf-ray`); they get only the liveness verdict, never the OAuth internals.
1116
+ // See buildHealthResponse for the full rationale.
1117
+ const { httpStatus, body } = buildHealthResponse(s, requestCount, req.headers['cf-ray'] !== undefined);
1116
1118
  res.writeHead(httpStatus, JSON_HEADERS);
1117
- res.end(JSON.stringify({
1118
- status: dead ? 'degraded' : 'ok',
1119
- oauth: s.status,
1120
- expiresIn: s.expiresIn,
1121
- requests: requestCount,
1122
- ...(s.refreshFailures ? { refreshFailures: s.refreshFailures } : {}),
1123
- ...(s.lastRefreshError ? { lastRefreshError: s.lastRefreshError } : {}),
1124
- }));
1119
+ res.end(JSON.stringify(body));
1125
1120
  return;
1126
1121
  }
1127
1122
  if (!checkAuth(req)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "4.8.72",
3
+ "version": "4.8.74",
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": {