@askalf/dario 5.5.39 → 5.5.41
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 +1 -0
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/docs/multi-instance.md +120 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,6 +207,7 @@ The split isn't live, but it was announced once on short notice and could return
|
|
|
207
207
|
- **Multi-account pool.** Several Claude seats behind one endpoint, routed by per-model headroom with sticky-session cache locality and in-flight 429 failover. → [Multi-account pool](#multi-account-pool)
|
|
208
208
|
- **Byte-faithful passthrough for real Claude Code.** A genuine CC request already *is* the CC shape, so dario forwards it verbatim — system prompt, tools, thinking, key order untouched — keeping only its billing tag, identity, and cache breakpoints. Covers CC's whole family: the main loop, its Task/Agent sub-agents, and the permission classifier. Non-CC clients get the full template rebuild that keeps them routing. Background: [#678](https://github.com/askalf/dario/issues/678).
|
|
209
209
|
- **Headless admin API (`DARIO_ADMIN=1`).** Provision and manage pool accounts entirely over HTTP — start with zero accounts, `POST /admin/login/start`, paste the code back, routable the moment the `200` lands (live hot-reload, no restart). Token-gated even on loopback, audit-logged, rate-limited. Built for Docker / k8s / Pi. → [`docs/admin-api.md`](./docs/admin-api.md)
|
|
210
|
+
- **More than one instance, same accounts.** Anthropic's refresh tokens are single-use, so two replicas refreshing the same account leaves one holding a dead token. An optional refresh lock (Redis or Cloudflare backend, same contract, fails open) makes the loser adopt the winner's fresh credentials instead. Safe credential sharing — *not* full HA: rate-limit accounting and sticky routing stay per-instance. → [`docs/multi-instance.md`](./docs/multi-instance.md)
|
|
210
211
|
- **Runs any agent.** A 64-entry schema-verified `TOOL_MAP` pre-maps Cline, Roo, Kilo, Cursor, Windsurf, Continue, Copilot, OpenHands, OpenClaw, Hermes, and [hands](https://github.com/askalf/hands) tool names to CC's native set — no flag, no validator errors. MCP tools (`mcp__server__tool`) forward verbatim. [Compatibility matrix](./docs/integrations/compat-matrix.md) · [agent-compat.md](./docs/integrations/agent-compat.md).
|
|
211
212
|
- **Behavioral stealth (`--stealth`).** Adds *when* a request arrives to *what* it looks like — response-length-correlated think time and session-start latency. → [`docs/wire-fidelity.md`](./docs/wire-fidelity.md)
|
|
212
213
|
- **VPN / egress routing.** Route dario's upstream traffic through a VPN without putting the whole host on one. → [`docs/vpn-routing.md`](./docs/vpn-routing.md)
|
|
@@ -456,7 +456,7 @@ export declare function detectDrift(t: TemplateData, installedOverride?: string
|
|
|
456
456
|
*/
|
|
457
457
|
export declare const SUPPORTED_CC_RANGE: {
|
|
458
458
|
readonly min: "1.0.0";
|
|
459
|
-
readonly maxTested: "2.1.
|
|
459
|
+
readonly maxTested: "2.1.238";
|
|
460
460
|
};
|
|
461
461
|
/**
|
|
462
462
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -1145,7 +1145,7 @@ export function detectDrift(t, installedOverride) {
|
|
|
1145
1145
|
*/
|
|
1146
1146
|
export const SUPPORTED_CC_RANGE = {
|
|
1147
1147
|
min: '1.0.0',
|
|
1148
|
-
maxTested: '2.1.
|
|
1148
|
+
maxTested: '2.1.238',
|
|
1149
1149
|
};
|
|
1150
1150
|
/**
|
|
1151
1151
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Running more than one dario against the same accounts
|
|
2
|
+
|
|
3
|
+
Short answer: **you can share credentials safely between instances, but dario is not HA.** Those are different claims, and the difference matters before you scale a Deployment to 2.
|
|
4
|
+
|
|
5
|
+
This page covers what actually breaks with two instances, which part is solved, how to turn the fix on, and how to prove it works on your own infrastructure.
|
|
6
|
+
|
|
7
|
+
Raised in [#993](https://github.com/askalf/dario/issues/993).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What breaks with two instances
|
|
12
|
+
|
|
13
|
+
Three things, and they fail differently.
|
|
14
|
+
|
|
15
|
+
### 1. The OAuth refresh race — **solved**
|
|
16
|
+
|
|
17
|
+
Anthropic's refresh tokens are **single-use**. Refreshing returns a new access token *and* a new refresh token, and invalidates the old one.
|
|
18
|
+
|
|
19
|
+
Two instances holding the same account both notice the token is near expiry, and both refresh. One wins. The loser's refresh token is now dead, and it has no way to know — so that instance is locked out of the account until someone runs `dario login` again. On a shared NAS or a k8s volume this is not a rare race; it is the normal outcome of two pods with synchronized clocks and the same 45-minute refresh margin.
|
|
20
|
+
|
|
21
|
+
A plain mutex does not fix this. The loser waits, acquires the lock, and then refreshes with a token that is *already* stale — it just loses more slowly. The fix is that the loser **adopts the winner's fresh credentials** instead of attempting its own. That is what dario's refresh lock does.
|
|
22
|
+
|
|
23
|
+
### 2. Rate-limit accounting — **not solved**
|
|
24
|
+
|
|
25
|
+
`pool.ts` keeps `accounts: Map<string, PoolAccount>` in process memory, and updates it only from rate-limit headers on responses *that instance* saw.
|
|
26
|
+
|
|
27
|
+
Two instances sharing an account each believe it has full headroom. Both route to it. Neither can see what the other is spending, so the pool overshoots the real 5-hour and 7-day windows and starts getting rejections it did not predict. Adding instances makes this worse, not better.
|
|
28
|
+
|
|
29
|
+
### 3. Session stickiness — **not solved**
|
|
30
|
+
|
|
31
|
+
`computeStickyKey()` hashes the first user message and pins that conversation to one account, so its prompt cache stays warm. The binding lives in process memory.
|
|
32
|
+
|
|
33
|
+
With two instances behind one Service, the same conversation can land on either, and get a different account each time. The prefix is re-cached per account, so you pay cache writes instead of reads. See [`docs/multi-account-pool.md`](./multi-account-pool.md) for why that costs real money.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## So what should you actually run?
|
|
38
|
+
|
|
39
|
+
| you want | do this |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Zero-downtime restarts / rolling deploys | Two instances **with the refresh lock**. Brief overlap is fine; the credential race is the only thing that corrupts state, and the lock covers it. |
|
|
42
|
+
| More throughput from more accounts | **One instance, more accounts in the pool.** The pool is the horizontal-scaling mechanism; a second instance is not. |
|
|
43
|
+
| Survive a node failure | Two instances with the lock, and accept that rate-limit accounting is approximate while both are live. |
|
|
44
|
+
| Precise rate-limit accounting | One instance. There is no shared-state mode today. |
|
|
45
|
+
|
|
46
|
+
dario starts in well under a second, so for most people a single replica with a sensible `restartPolicy` is the honest answer — which is roughly where [#993](https://github.com/askalf/dario/issues/993) landed too.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Turning the refresh lock on
|
|
51
|
+
|
|
52
|
+
Two reference backends implement the identical contract. `src/accounts.ts` knows only the two environment variables — it has no idea which one is answering.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
DARIO_REFRESH_LOCK_URL=http://<lock-host>:8080
|
|
56
|
+
DARIO_REFRESH_LOCK_TOKEN=<shared secret>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Set both on **every** instance. Unset `DARIO_REFRESH_LOCK_URL` and the code path is byte-identical to not having the feature at all.
|
|
60
|
+
|
|
61
|
+
### Redis backend — no external dependency
|
|
62
|
+
|
|
63
|
+
Use this if you are airgapped, or do not want dario's availability to depend on a third party.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
cd redis-lock
|
|
67
|
+
docker build -t dario-refresh-lock .
|
|
68
|
+
docker run -d -p 8080:8080 \
|
|
69
|
+
-e LOCK_TOKEN=<a real random value, not reused from another service> \
|
|
70
|
+
-e REDIS_HOST=<your redis host> \
|
|
71
|
+
-e REDIS_PORT=6379 \
|
|
72
|
+
dario-refresh-lock
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Zero new runtime dependencies — the RESP2 client is hand-rolled over `node:net` rather than pulling in `redis`/`ioredis`, so dario's zero-dependency invariant is intact. Details in [`redis-lock/README.md`](../redis-lock/README.md).
|
|
76
|
+
|
|
77
|
+
**Be clear about what this is:** a single Redis instance holding lock and credential-handoff state. It is a coordination point, not a consensus system. If Redis is down, the lock fails open (below). It does not do leader election, and it is not Raft — if you need split-brain guarantees, this is not that.
|
|
78
|
+
|
|
79
|
+
### Cloudflare backend — nothing to host
|
|
80
|
+
|
|
81
|
+
A Durable Object gives you serialized access without running anything yourself. See [`cloudflare/refresh-lock/README.md`](../cloudflare/refresh-lock/README.md).
|
|
82
|
+
|
|
83
|
+
Not suitable for airgapped deployments, and it adds an internet dependency to a component that otherwise only talks to Anthropic.
|
|
84
|
+
|
|
85
|
+
### It fails open, deliberately
|
|
86
|
+
|
|
87
|
+
Any lock-service error — bad response, network failure, timeout — and dario proceeds with its own refresh exactly as if no lock were configured.
|
|
88
|
+
|
|
89
|
+
The lock is resilience layered on top of dario's job, not a new dependency dario's core function needs. An outage of your Redis must not stop your proxy serving traffic. The cost of that choice is that during a lock outage you are back to the plain race, so do not treat the lock as a hard guarantee.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Proving it works on your own setup
|
|
94
|
+
|
|
95
|
+
Do not take the above on trust. `test/integration/dual-instance-race.mjs` runs the real scenario: **two genuinely separate `node` processes**, each with its own isolated `~/.dario`, sharing nothing but the lock service, both racing to refresh the same account at the same instant.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
npm run test:refresh-lock-race
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Point it at your own lock service by setting `DARIO_REFRESH_LOCK_URL` / `DARIO_REFRESH_LOCK_TOKEN` first.
|
|
102
|
+
|
|
103
|
+
**Run it both ways.** The flag is the useful part:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
node test/integration/dual-instance-race.mjs --no-lock # reproduce the failure
|
|
107
|
+
node test/integration/dual-instance-race.mjs # show it prevented
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Without the lock you should watch one instance end up holding a dead refresh token. With it, the loser adopts the winner's credentials and both keep working. Seeing the failure first is what makes the fix mean something.
|
|
111
|
+
|
|
112
|
+
Anthropic's token endpoint is the one mocked part, and only that — hammering the real endpoint with production credentials for adversarial testing risks burning your actual refresh token. The lock calls, the two processes, the isolated homes and the race itself are all real.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## If you need true HA
|
|
117
|
+
|
|
118
|
+
There is no shared-state mode today, and building one is a larger change than a lock: rate-limit snapshots and sticky bindings would both have to move out of process memory, which means every routing decision takes a network hop.
|
|
119
|
+
|
|
120
|
+
If you want that, say so on [#993](https://github.com/askalf/dario/issues/993) with your deployment shape. Concrete requirements are considerably more useful than a general "make it HA" — the design depends heavily on whether you need precise accounting or merely approximate.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.41",
|
|
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": {
|