@askalf/dario 5.4.10 → 5.4.12
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/dist/live-fingerprint.d.ts +29 -0
- package/dist/live-fingerprint.js +51 -5
- package/dist/scrub-template.d.ts +9 -3
- package/dist/scrub-template.js +46 -3
- package/package.json +2 -2
|
@@ -239,6 +239,35 @@ interface CapturedRequest {
|
|
|
239
239
|
rawHeaders: string[];
|
|
240
240
|
body: Record<string, unknown>;
|
|
241
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Does this request belong to the capture we started?
|
|
244
|
+
*
|
|
245
|
+
* The MITM used to accept the FIRST request whose URL contained
|
|
246
|
+
* `/v1/messages` with nothing tying it to the child we spawned (dario#872).
|
|
247
|
+
* The port is ephemeral, so a collision is improbable rather than
|
|
248
|
+
* impossible, and nothing downstream could tell a foreign request from the
|
|
249
|
+
* child's once it was captured.
|
|
250
|
+
*
|
|
251
|
+
* The nonce rides in the URL path because that is the one channel we fully
|
|
252
|
+
* control without touching what CC sends. Two measurements decided it over a
|
|
253
|
+
* key-borne nonce:
|
|
254
|
+
*
|
|
255
|
+
* 1. CC honours a path segment in ANTHROPIC_BASE_URL — given
|
|
256
|
+
* `http://127.0.0.1:PORT/<nonce>` it requests
|
|
257
|
+
* `/<nonce>/v1/messages?beta=true` (and probes `/<nonce>/api/hello`
|
|
258
|
+
* first, which still 404s here).
|
|
259
|
+
* 2. On a subscription install CC authenticates with
|
|
260
|
+
* `authorization: Bearer sk-ant-…`, so ANTHROPIC_API_KEY is not the auth
|
|
261
|
+
* header at all. A key-borne nonce would do nothing on OAuth installs
|
|
262
|
+
* while changing the auth path for API-key ones — and changing what the
|
|
263
|
+
* child authenticates with can change what it sends, which is the whole
|
|
264
|
+
* thing a fingerprint capture must not do.
|
|
265
|
+
*
|
|
266
|
+
* Fails closed: no nonce, no capture. A miss makes the capture return null
|
|
267
|
+
* and the bake exit non-zero, which is the correct outcome for a request we
|
|
268
|
+
* cannot attribute.
|
|
269
|
+
*/
|
|
270
|
+
export declare function isOwnCaptureRequest(url: string | undefined, nonce: string): boolean;
|
|
242
271
|
/**
|
|
243
272
|
* Run a loopback MITM server on a random port, spawn CC with
|
|
244
273
|
* ANTHROPIC_BASE_URL pointed at it, wait for one request, respond with a
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
* the right piece without re-deriving the threat model.
|
|
86
86
|
*/
|
|
87
87
|
import { spawn, execFileSync } from 'node:child_process';
|
|
88
|
+
import { randomBytes } from 'node:crypto';
|
|
88
89
|
import { createServer } from 'node:http';
|
|
89
90
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync } from 'node:fs';
|
|
90
91
|
import { homedir } from 'node:os';
|
|
@@ -363,6 +364,41 @@ export function _atomicWriteJsonForTest(targetPath, data) {
|
|
|
363
364
|
function writeLiveCache(data) {
|
|
364
365
|
atomicWriteJson(liveCachePath(), data);
|
|
365
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Does this request belong to the capture we started?
|
|
369
|
+
*
|
|
370
|
+
* The MITM used to accept the FIRST request whose URL contained
|
|
371
|
+
* `/v1/messages` with nothing tying it to the child we spawned (dario#872).
|
|
372
|
+
* The port is ephemeral, so a collision is improbable rather than
|
|
373
|
+
* impossible, and nothing downstream could tell a foreign request from the
|
|
374
|
+
* child's once it was captured.
|
|
375
|
+
*
|
|
376
|
+
* The nonce rides in the URL path because that is the one channel we fully
|
|
377
|
+
* control without touching what CC sends. Two measurements decided it over a
|
|
378
|
+
* key-borne nonce:
|
|
379
|
+
*
|
|
380
|
+
* 1. CC honours a path segment in ANTHROPIC_BASE_URL — given
|
|
381
|
+
* `http://127.0.0.1:PORT/<nonce>` it requests
|
|
382
|
+
* `/<nonce>/v1/messages?beta=true` (and probes `/<nonce>/api/hello`
|
|
383
|
+
* first, which still 404s here).
|
|
384
|
+
* 2. On a subscription install CC authenticates with
|
|
385
|
+
* `authorization: Bearer sk-ant-…`, so ANTHROPIC_API_KEY is not the auth
|
|
386
|
+
* header at all. A key-borne nonce would do nothing on OAuth installs
|
|
387
|
+
* while changing the auth path for API-key ones — and changing what the
|
|
388
|
+
* child authenticates with can change what it sends, which is the whole
|
|
389
|
+
* thing a fingerprint capture must not do.
|
|
390
|
+
*
|
|
391
|
+
* Fails closed: no nonce, no capture. A miss makes the capture return null
|
|
392
|
+
* and the bake exit non-zero, which is the correct outcome for a request we
|
|
393
|
+
* cannot attribute.
|
|
394
|
+
*/
|
|
395
|
+
export function isOwnCaptureRequest(url, nonce) {
|
|
396
|
+
if (!url || nonce.length === 0)
|
|
397
|
+
return false;
|
|
398
|
+
if (!url.startsWith(`/${nonce}/`))
|
|
399
|
+
return false;
|
|
400
|
+
return url.includes('/v1/messages');
|
|
401
|
+
}
|
|
366
402
|
/**
|
|
367
403
|
* Run a loopback MITM server on a random port, spawn CC with
|
|
368
404
|
* ANTHROPIC_BASE_URL pointed at it, wait for one request, respond with a
|
|
@@ -377,9 +413,11 @@ export async function captureLiveTemplateAsync(timeoutMs = 10_000) {
|
|
|
377
413
|
return extractTemplate(captured);
|
|
378
414
|
}
|
|
379
415
|
async function runCapture(timeoutMs) {
|
|
416
|
+
const nonce = `dario-capture-${randomBytes(12).toString('hex')}`;
|
|
380
417
|
return new Promise((resolve) => {
|
|
381
418
|
let captured = null;
|
|
382
419
|
let settled = false;
|
|
420
|
+
let foreign = 0;
|
|
383
421
|
const settle = (result) => {
|
|
384
422
|
if (settled)
|
|
385
423
|
return;
|
|
@@ -395,9 +433,17 @@ async function runCapture(timeoutMs) {
|
|
|
395
433
|
resolve(result);
|
|
396
434
|
};
|
|
397
435
|
const server = createServer((req, res) => {
|
|
398
|
-
// Only handle /v1/messages — everything else gets a 404 so CC
|
|
399
|
-
// accidentally think /v1/models is live
|
|
400
|
-
|
|
436
|
+
// Only handle OUR /v1/messages — everything else gets a 404 so CC
|
|
437
|
+
// doesn't accidentally think /v1/models is live, and so a request we
|
|
438
|
+
// cannot attribute to the spawned child is never captured (dario#872).
|
|
439
|
+
if (!isOwnCaptureRequest(req.url, nonce)) {
|
|
440
|
+
// A /v1/messages without our nonce is exactly the case #872 is about:
|
|
441
|
+
// something else reached this port. Say so — a silent 404 here is how
|
|
442
|
+
// a foreign capture would have gone unnoticed.
|
|
443
|
+
if (req.url?.includes('/v1/messages')) {
|
|
444
|
+
foreign++;
|
|
445
|
+
console.error(`[dario] capture: rejected a /v1/messages request that did not carry this capture's nonce (${foreign} so far) — see dario#872`);
|
|
446
|
+
}
|
|
401
447
|
res.writeHead(404, { 'content-type': 'application/json' });
|
|
402
448
|
res.end('{"type":"error","error":{"type":"not_found_error","message":"not found"}}');
|
|
403
449
|
return;
|
|
@@ -417,7 +463,7 @@ async function runCapture(timeoutMs) {
|
|
|
417
463
|
}
|
|
418
464
|
captured = {
|
|
419
465
|
method: req.method ?? 'POST',
|
|
420
|
-
path: req.url ?? '/v1/messages',
|
|
466
|
+
path: (req.url ?? '/v1/messages').replace(`/${nonce}`, ''),
|
|
421
467
|
headers,
|
|
422
468
|
rawHeaders: Array.isArray(req.rawHeaders) ? [...req.rawHeaders] : [],
|
|
423
469
|
body,
|
|
@@ -481,7 +527,7 @@ async function runCapture(timeoutMs) {
|
|
|
481
527
|
settle(null);
|
|
482
528
|
return;
|
|
483
529
|
}
|
|
484
|
-
const url = `http://127.0.0.1:${address.port}`;
|
|
530
|
+
const url = `http://127.0.0.1:${address.port}/${nonce}`;
|
|
485
531
|
// Spawn CC with ANTHROPIC_BASE_URL pointed at our MITM.
|
|
486
532
|
const claudeBin = findClaudeBinary();
|
|
487
533
|
if (!claudeBin) {
|
package/dist/scrub-template.d.ts
CHANGED
|
@@ -42,8 +42,14 @@ export declare function scrubTemplate(data: TemplateData): TemplateData;
|
|
|
42
42
|
*/
|
|
43
43
|
export declare function scrubText(text: string): string;
|
|
44
44
|
/**
|
|
45
|
-
* Run over a string and report
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* Run over a string and report anything that must not survive into a bake:
|
|
46
|
+
* user-identifying paths, host-context sections the strip missed, and
|
|
47
|
+
* instruction-file prose (dario#872). Despite the name the contract has never
|
|
48
|
+
* been paths-only. Used by `scripts/check-cc-drift.mjs` to gate a release and
|
|
49
|
+
* by `scripts/capture-and-bake.mjs` to fail a bake.
|
|
50
|
+
*
|
|
51
|
+
* Deliberately NOT called from `extractTemplate`: that serves the live-capture
|
|
52
|
+
* path too, and the live fingerprint keeps host context on purpose — it exists
|
|
53
|
+
* to replay the operator's own CC install faithfully. Only the bake publishes.
|
|
48
54
|
*/
|
|
49
55
|
export declare function findUserPathHits(text: string): string[];
|
package/dist/scrub-template.js
CHANGED
|
@@ -222,9 +222,44 @@ function scrubObjectStrings(value) {
|
|
|
222
222
|
return value;
|
|
223
223
|
}
|
|
224
224
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
225
|
+
* Prose CC emits when it injects user- or project-level instruction files
|
|
226
|
+
* (CLAUDE.md, memory, environment) into a prompt. The section strip above is
|
|
227
|
+
* keyed on the `# claudeMd` heading shape; these are keyed on the wrapper text
|
|
228
|
+
* itself, so a reshaped or renamed heading still gets caught.
|
|
229
|
+
*
|
|
230
|
+
* Both halves are load-bearing. `removeSection` and the heading detector in
|
|
231
|
+
* findUserPathHits read the SAME heading list, so a heading CC renames strips
|
|
232
|
+
* nothing and flags nothing — two silent failures — and once paths are scrubbed
|
|
233
|
+
* the leftover prose carries no user path for the other detectors to catch.
|
|
234
|
+
* dario#872 saw one capture in six come back carrying another session's
|
|
235
|
+
* instruction text; this is the half that does not depend on the heading.
|
|
236
|
+
*
|
|
237
|
+
* Every marker is verified absent from the real baked template — base, fable,
|
|
238
|
+
* opus-5 and sonnet-5 prompts plus every tool description. Do NOT add bare
|
|
239
|
+
* `CLAUDE.md` or `system-reminder`: CC's own system prompt and tool
|
|
240
|
+
* descriptions both mention them, so either would fail every bake. Verified,
|
|
241
|
+
* not assumed, and test/context-bleed.mjs pins it.
|
|
242
|
+
*/
|
|
243
|
+
const INSTRUCTION_INJECTION_MARKERS = [
|
|
244
|
+
{ label: 'instruction-file wrapper', re: /Codebase and user instructions are shown below/ },
|
|
245
|
+
{ label: 'instruction-file wrapper', re: /IMPORTANT: These instructions OVERRIDE any default behavior/ },
|
|
246
|
+
// Straight and typographic apostrophe both — CC emits the straight one today,
|
|
247
|
+
// and a marker that a quote-style change silently disarms is not a guard.
|
|
248
|
+
{ label: 'instruction-file annotation', re: /user['’]s private global instructions/ },
|
|
249
|
+
{ label: 'memory-file annotation', re: /auto-memory, persists across conversations/ },
|
|
250
|
+
{ label: 'instruction-file heading', re: /\bContents of [^\n]{1,300}\.md\b/ },
|
|
251
|
+
{ label: 'project-instruction wrapper', re: /Here are useful instructions from/ },
|
|
252
|
+
];
|
|
253
|
+
/**
|
|
254
|
+
* Run over a string and report anything that must not survive into a bake:
|
|
255
|
+
* user-identifying paths, host-context sections the strip missed, and
|
|
256
|
+
* instruction-file prose (dario#872). Despite the name the contract has never
|
|
257
|
+
* been paths-only. Used by `scripts/check-cc-drift.mjs` to gate a release and
|
|
258
|
+
* by `scripts/capture-and-bake.mjs` to fail a bake.
|
|
259
|
+
*
|
|
260
|
+
* Deliberately NOT called from `extractTemplate`: that serves the live-capture
|
|
261
|
+
* path too, and the live fingerprint keeps host context on purpose — it exists
|
|
262
|
+
* to replay the operator's own CC install faithfully. Only the bake publishes.
|
|
228
263
|
*/
|
|
229
264
|
export function findUserPathHits(text) {
|
|
230
265
|
const hits = [];
|
|
@@ -249,5 +284,13 @@ export function findUserPathHits(text) {
|
|
|
249
284
|
hits.push(`# ${name} (host-context section not stripped)`);
|
|
250
285
|
}
|
|
251
286
|
}
|
|
287
|
+
// Instruction-file prose, matched on the wrapper rather than the heading.
|
|
288
|
+
// Excerpt is bounded: a hit is printed by the bake, and the whole point is
|
|
289
|
+
// that the matched text may be somebody's private instructions.
|
|
290
|
+
for (const { label, re } of INSTRUCTION_INJECTION_MARKERS) {
|
|
291
|
+
const m = text.match(re);
|
|
292
|
+
if (m)
|
|
293
|
+
hits.push(`${m[0].slice(0, 60)} (${label} not stripped)`);
|
|
294
|
+
}
|
|
252
295
|
return hits;
|
|
253
296
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.12",
|
|
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": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc && cp src/cc-template-data.json dist/",
|
|
24
24
|
"test": "node --test --test-concurrency=8 test/all.test.mjs",
|
|
25
|
-
"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/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",
|
|
25
|
+
"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",
|
|
26
26
|
"audit": "npm audit --production --audit-level=high",
|
|
27
27
|
"prepublishOnly": "npm run build",
|
|
28
28
|
"start": "node dist/cli.js",
|