@askalf/dario 4.0.1 → 4.1.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 +50 -5
- package/dist/cli.js +128 -1
- package/dist/config-file.d.ts +26 -0
- package/dist/config-file.js +23 -0
- package/dist/notify.d.ts +48 -0
- package/dist/notify.js +120 -0
- package/dist/overage-guard.d.ts +102 -0
- package/dist/overage-guard.js +189 -0
- package/dist/proxy.d.ts +14 -0
- package/dist/proxy.js +106 -1
- package/dist/tui/proxy-client.d.ts +44 -1
- package/dist/tui/proxy-client.js +66 -2
- package/dist/tui/tabs/analytics.js +13 -0
- package/dist/tui/tabs/config.js +35 -0
- package/dist/tui/tabs/hits.d.ts +14 -0
- package/dist/tui/tabs/hits.js +54 -4
- package/dist/tui/tabs/status.d.ts +14 -0
- package/dist/tui/tabs/status.js +109 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ That's the whole setup. Every tool that honors those env vars now runs on your s
|
|
|
34
34
|
|
|
35
35
|
**New in v4:** type `dario` (no args) in another terminal to open the interactive TUI — live request stream, per-model burn-rate, rate-limit utilization, and a config editor that writes to `~/.dario/config.json`. Migrating from v3? See [MIGRATION.md](MIGRATION.md).
|
|
36
36
|
|
|
37
|
+
**New in v4.1 — overage-guard:** dario halts itself the moment a single response carries `representative-claim: overage` and returns 503 with a clean error body until you run `dario resume` or the cooldown clears. Subscribers should never see an overage hit during normal operation; one means something is wrong, and continuing to forward requests bleeds against per-token billing. Active protection by default; flip to warn-only with `--overage-behavior=warn` or off entirely with `--no-overage-guard`.
|
|
38
|
+
|
|
37
39
|
```
|
|
38
40
|
┌─ dario v4 ──────────────────────────[ q quit · Tab next · ? help ]──┐
|
|
39
41
|
│ Status Config ▎Analytics▎ Hits Accounts Backends │
|
|
@@ -154,6 +156,45 @@ Reclassification flips the request from `five_hour` (your subscription) to `over
|
|
|
154
156
|
|
|
155
157
|
---
|
|
156
158
|
|
|
159
|
+
## What dario does when overage lands (v4.1)
|
|
160
|
+
|
|
161
|
+
v4 made the billing bucket visible per-request in the TUI's Hits tab. v4.1 turns that visibility into active protection.
|
|
162
|
+
|
|
163
|
+
The moment any upstream response carries `representative-claim: overage`, dario **halts the proxy**. Every subsequent `/v1/messages`, `/v1/complete`, `/v1/chat/completions` request returns `503` with an Anthropic-shaped error body the client surfaces verbatim:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"type": "error",
|
|
168
|
+
"error": {
|
|
169
|
+
"type": "dario_overage_guard",
|
|
170
|
+
"message": "dario halted to prevent API-rate bleed. A request was classified as 'overage' (per-token billing) instead of your subscription pool. To resume: run `dario resume` in another terminal, or wait until <ISO ts> for the cooldown to auto-clear. Details: github.com/askalf/dario/issues/288"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The TUI's Status tab pins the loud version:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
┌─ dario v4 ──────────────────────────[ q quit · Tab next · ? help ]──┐
|
|
179
|
+
│ ▎Status▎ Config Analytics Hits Accounts Backends │
|
|
180
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
181
|
+
│ Overage-guard │
|
|
182
|
+
│ ⚠ HALTED overage detected 12s ago │
|
|
183
|
+
│ Request: claude-opus-4-7 account=default │
|
|
184
|
+
│ Cause: representative-claim = overage │
|
|
185
|
+
│ Auto-resume in 29m 48s │
|
|
186
|
+
│ Manual resume press R here, or `dario resume` from any shell │
|
|
187
|
+
│ │
|
|
188
|
+
│ Last refresh: just now. r refresh · R resume. │
|
|
189
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Why "halt at hit #1" is the right default: subscribers should never see a single overage response during normal operation. One means something is wrong — wire-shape drift, classifier change, account misconfig — and continuing to forward requests in the same shape bleeds real money for accounts with extra-usage enabled, or returns wall-of-rejections for accounts without it. The first hit is the signal; the second through hundredth are damage.
|
|
193
|
+
|
|
194
|
+
**Resume paths** — `dario resume` from any shell, `R` on the TUI Status tab, or the cooldown timer (default 30 min). **Configuration** — `~/.dario/config.json` → `overageGuard`, or CLI flags (`--overage-behavior=warn` for visibility-only, `--no-overage-guard` to disable, `--overage-cooldown=<ms>` to tune). **OS notification** — best-effort native toast (osascript / notify-send / BurntToast) plus terminal BEL as the unconditional floor. See [#288](https://github.com/askalf/dario/issues/288).
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
157
198
|
## Does it actually work?
|
|
158
199
|
|
|
159
200
|
Four LLMs reviewed the codebase cold, same prompt ([`reviews/PROMPT.md`](./reviews/PROMPT.md)), each signed a verdict:
|
|
@@ -238,6 +279,7 @@ The tool doesn't know. The backend doesn't know. Dario is the seam.
|
|
|
238
279
|
- **Shim mode.** Take the proxy off the wire entirely — `dario shim -- claude --print "hi"` patches `globalThis.fetch` in-process. No HTTP hop, no port, no `BASE_URL`. → [`docs/shim.md`](./docs/shim.md)
|
|
239
280
|
- **Recover output capability.** `dario proxy --system-prompt=partial` strips CC's tone/verbosity/no-comments constraints for 1.2–2.8× more output on open-ended work — empirically without flipping billing (the classifier doesn't read that slot). [Discussion #183](https://github.com/askalf/dario/discussions/183) has the per-prompt receipts. → [`docs/system-prompt.md`](./docs/system-prompt.md)
|
|
240
281
|
- **Reachable from inside CC / any MCP client.** `dario subagent install` registers a CC sub-agent for in-session diagnostics; `dario mcp` exposes dario as a read-only MCP server. → [`docs/sub-agent.md`](./docs/sub-agent.md) · [`docs/mcp-server.md`](./docs/mcp-server.md)
|
|
282
|
+
- **Active overage protection (v4.1).** Halts the proxy on any `representative-claim: overage` response and returns 503 to subsequent requests until you run `dario resume` or the cooldown clears. Visibility-only mode (`--overage-behavior=warn`) for operators who want the signal without disrupting traffic. Halt state visible in TUI Status/Hits/Analytics tabs, surfaced as named SSE events, and as a best-effort native desktop notification. [#288](https://github.com/askalf/dario/issues/288).
|
|
241
283
|
|
|
242
284
|
---
|
|
243
285
|
|
|
@@ -245,11 +287,11 @@ The tool doesn't know. The backend doesn't know. Dario is the seam.
|
|
|
245
287
|
|
|
246
288
|
| Signal | Status |
|
|
247
289
|
|---|---|
|
|
248
|
-
| Source | **
|
|
290
|
+
| Source | **~18.5k** lines of TypeScript across **44** files — auditable in a weekend |
|
|
249
291
|
| Dependencies | **0 runtime.** Verify: `npm ls --production` |
|
|
250
|
-
| Provenance | Every release [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions + Sigstore
|
|
292
|
+
| Provenance | Every release [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions + Sigstore. v4.1.0 published 2026-05-16T15:13:24Z |
|
|
251
293
|
| Scanning | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) on every push and weekly |
|
|
252
|
-
| Tests | **
|
|
294
|
+
| Tests | **80 test files**, **74 in default `npm test` suite** — green on every release |
|
|
253
295
|
| Drift response | [`cc-drift-watch.yml`](./.github/workflows/cc-drift-watch.yml) hourly cron, [`cc-drift-auto-release.yml`](./.github/workflows/cc-drift-auto-release.yml) auto-publish on merge — median CC-release → dario-release under one hour |
|
|
254
296
|
| Credentials | Never logged, redacted from errors, `0600` on disk in `0700` dirs; MCP server redacts at the tool boundary |
|
|
255
297
|
| Network | Binds `127.0.0.1` by default; upstream only to configured backends over HTTPS; hardcoded SSRF allowlist |
|
|
@@ -273,7 +315,7 @@ cd $(npm root -g)/@askalf/dario && npm ls --production
|
|
|
273
315
|
|
|
274
316
|
## Commands
|
|
275
317
|
|
|
276
|
-
`dario` (TUI) · `login` · `proxy` · `doctor` · `accounts {list,add,remove}` · `backend {list,add,remove}` · `shim` · `mcp` · `subagent {install,status,remove}` · `usage` · `config` · `upgrade` · `status` · `refresh` · `logout` · `help`
|
|
318
|
+
`dario` (TUI) · `login` · `proxy` · `doctor` · `accounts {list,add,remove}` · `backend {list,add,remove}` · `shim` · `mcp` · `subagent {install,status,remove}` · `usage` · `config` · `upgrade` · `status` · `refresh` · `resume` · `logout` · `help`
|
|
277
319
|
|
|
278
320
|
Full flag/env reference: [`docs/commands.md`](./docs/commands.md) · SDK examples + per-tool setup: [`docs/usage.md`](./docs/usage.md)
|
|
279
321
|
|
|
@@ -285,7 +327,10 @@ Full flag/env reference: [`docs/commands.md`](./docs/commands.md) · SDK example
|
|
|
285
327
|
Mechanically, dario uses your existing Claude Code OAuth tokens — it authenticates you as you, with your subscription, through Anthropic's official endpoints. Whether any particular use complies with current terms is between you and Anthropic; consult their terms and your agreement. Independent, unofficial, third-party — see [DISCLAIMER.md](DISCLAIMER.md).
|
|
286
328
|
|
|
287
329
|
**What does the v4 TUI actually do?**
|
|
288
|
-
Open `dario` with no args. Six tabs: **Status** shows proxy health + OAuth expiry + config source; **Config** edits `~/.dario/config.json` in place (bool toggles inline, numbers/strings open a prompt, `s` saves); **Analytics** polls `/analytics` every 2s and renders per-model bars + rate-limit utilization +
|
|
330
|
+
Open `dario` with no args. Six tabs: **Status** shows proxy health + OAuth expiry + config source + overage-guard state (v4.1: halt banner with countdown + `R` to resume); **Config** edits `~/.dario/config.json` in place (bool toggles inline, numbers/strings open a prompt, `s` saves); **Analytics** polls `/analytics` every 2s and renders per-model bars + rate-limit utilization + an Overage bar that's red the moment count is non-zero (v4.1); **Hits** subscribes to `/analytics/stream` SSE for the live request feed with per-record detail drilldown and a pinned halt banner when overage is detected (v4.1); **Accounts** lists the pool; **Backends** lists OpenAI-compat backends. Pure ANSI, zero new runtime deps. Migration from v3: [MIGRATION.md](MIGRATION.md).
|
|
331
|
+
|
|
332
|
+
**What if a request lands in `overage` despite the wire-shape replay?**
|
|
333
|
+
v4.1+ halts the proxy on the first overage response and returns 503 to subsequent requests until you investigate. See [What dario does when overage lands](#what-dario-does-when-overage-lands-v41). The TUI Status tab shows the triggering request + countdown to auto-resume; `dario resume` from any shell clears the halt immediately; `--overage-behavior=warn` switches to visibility-only mode if you'd rather see the signal than block traffic.
|
|
289
334
|
|
|
290
335
|
**Do I need Claude Code installed?**
|
|
291
336
|
Recommended, not required. With CC, `dario login` picks up credentials automatically and the live template extractor reads your binary on every startup. Without it, dario runs its own OAuth flow and falls back to the bundled (scrubbed) template snapshot.
|
package/dist/cli.js
CHANGED
|
@@ -189,6 +189,56 @@ async function refresh() {
|
|
|
189
189
|
process.exit(1);
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
|
+
async function resume() {
|
|
193
|
+
// v4.1, dario#288 — clear the overage-guard halt state on a running
|
|
194
|
+
// dario proxy via POST /admin/resume. The proxy returns 200 with
|
|
195
|
+
// {ok, wasHalted, resumedAt}; we surface that to the operator.
|
|
196
|
+
//
|
|
197
|
+
// Port resolution mirrors `dario doctor` — --port flag > DARIO_PORT
|
|
198
|
+
// env > config file > 3456 default. Auth: DARIO_API_KEY when set
|
|
199
|
+
// (matches the same auth chain dario applies to every endpoint).
|
|
200
|
+
const { loadConfig } = await import('./config-file.js');
|
|
201
|
+
const fileResult = loadConfig();
|
|
202
|
+
const portArg = args.find(a => a.startsWith('--port='));
|
|
203
|
+
const portFromCli = portArg ? parseInt(portArg.split('=')[1], 10) : undefined;
|
|
204
|
+
const portFromEnv = process.env['DARIO_PORT'] ? parseInt(process.env['DARIO_PORT'], 10) : undefined;
|
|
205
|
+
const port = portFromCli ?? portFromEnv ?? fileResult.config.port ?? 3456;
|
|
206
|
+
const url = `http://127.0.0.1:${port}/admin/resume`;
|
|
207
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
208
|
+
const apiKey = process.env['DARIO_API_KEY'];
|
|
209
|
+
if (apiKey) {
|
|
210
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
211
|
+
}
|
|
212
|
+
let resp;
|
|
213
|
+
try {
|
|
214
|
+
resp = await fetch(url, { method: 'POST', headers, body: '{}' });
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
const msg = err.message;
|
|
218
|
+
// The proxy-not-running case is the common failure path; surface a
|
|
219
|
+
// friendly hint instead of a raw fetch error. Match across runtimes:
|
|
220
|
+
// - Node: 'ECONNREFUSED', 'fetch failed', 'ENOTFOUND', 'ETIMEDOUT'
|
|
221
|
+
// - Bun: 'Unable to connect' (different fetch error wording)
|
|
222
|
+
// - Both: 'connect EHOSTUNREACH', 'getaddrinfo' (DNS path)
|
|
223
|
+
if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|EHOSTUNREACH|fetch failed|unable to connect|getaddrinfo/i.test(msg)) {
|
|
224
|
+
console.error(`[dario] No proxy running on localhost:${port}. Start one with \`dario proxy\` (overage-guard state is per-process; there's nothing to resume on a stopped proxy).`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
console.error(`[dario] Resume request failed: ${msg}`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
if (!resp.ok) {
|
|
231
|
+
console.error(`[dario] Resume request returned HTTP ${resp.status}. Body: ${(await resp.text()).slice(0, 500)}`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
const result = await resp.json();
|
|
235
|
+
if (result.wasHalted) {
|
|
236
|
+
console.log(`[dario] Resumed at ${result.resumedAt}. Proxy returning to normal request handling.`);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
console.log(`[dario] Proxy was not halted — no-op. (Overage-guard state was already clear.)`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
192
242
|
async function logout() {
|
|
193
243
|
const path = join(homedir(), '.dario', 'credentials.json');
|
|
194
244
|
try {
|
|
@@ -423,6 +473,46 @@ async function proxy() {
|
|
|
423
473
|
// billable-filter. Empty values are dropped. Falls back to
|
|
424
474
|
// DARIO_PASSTHROUGH_BETAS env var.
|
|
425
475
|
const passthroughBetas = parsePassthroughBetasFlag(args, process.env['DARIO_PASSTHROUGH_BETAS']);
|
|
476
|
+
// --overage-guard / --no-overage-guard / DARIO_OVERAGE_GUARD=off|on (v4.1)
|
|
477
|
+
// When any upstream response carries `representative-claim: overage`,
|
|
478
|
+
// halt the proxy: every new request returns 503 with an Anthropic-shaped
|
|
479
|
+
// error body until cooldown expires or `dario resume` clears the state.
|
|
480
|
+
// Subscribers should never see an overage hit during normal operation,
|
|
481
|
+
// so this defaults ON — the cost of a false negative (silent per-token
|
|
482
|
+
// billing) far exceeds the cost of a false positive (one disrupted
|
|
483
|
+
// session that resumes with a single command). See dario#288.
|
|
484
|
+
//
|
|
485
|
+
// --no-overage-guard → fully disabled
|
|
486
|
+
// --overage-behavior=halt|warn → halt (default) or warn-only (no 503)
|
|
487
|
+
// --overage-cooldown=<MS> → auto-resume after this delay (default 30 min)
|
|
488
|
+
// --no-overage-notify → suppress OS-level desktop notification
|
|
489
|
+
// DARIO_OVERAGE_GUARD=off → env equivalent of --no-overage-guard
|
|
490
|
+
// DARIO_OVERAGE_BEHAVIOR=halt|warn → env equivalent of --overage-behavior
|
|
491
|
+
// DARIO_OVERAGE_COOLDOWN=<MS> → env equivalent of --overage-cooldown
|
|
492
|
+
// DARIO_OVERAGE_NOTIFY=off → env equivalent of --no-overage-notify
|
|
493
|
+
const overageGuardEnabledFromFlag = args.includes('--no-overage-guard') ? false
|
|
494
|
+
: args.includes('--overage-guard') ? true : undefined;
|
|
495
|
+
const overageGuardEnabledFromEnv = parseBooleanEnv(process.env['DARIO_OVERAGE_GUARD']);
|
|
496
|
+
const overageGuardEnabled = overageGuardEnabledFromFlag
|
|
497
|
+
?? overageGuardEnabledFromEnv
|
|
498
|
+
?? fileCfg.overageGuard?.enabled
|
|
499
|
+
?? true;
|
|
500
|
+
const overageBehaviorFromFlag = args.find((a) => a.startsWith('--overage-behavior='))?.split('=').slice(1).join('=');
|
|
501
|
+
const overageBehaviorFromEnv = process.env['DARIO_OVERAGE_BEHAVIOR'];
|
|
502
|
+
const overageBehaviorRaw = overageBehaviorFromFlag ?? overageBehaviorFromEnv;
|
|
503
|
+
const overageGuardBehavior = overageBehaviorRaw === 'halt' || overageBehaviorRaw === 'warn'
|
|
504
|
+
? overageBehaviorRaw
|
|
505
|
+
: (fileCfg.overageGuard?.behavior ?? 'halt');
|
|
506
|
+
const overageGuardCooldownMs = parsePositiveIntFlag('--overage-cooldown=')
|
|
507
|
+
?? parsePositiveIntEnv(process.env['DARIO_OVERAGE_COOLDOWN'])
|
|
508
|
+
?? fileCfg.overageGuard?.cooldownMs
|
|
509
|
+
?? 30 * 60 * 1000;
|
|
510
|
+
const overageNotifyFromFlag = args.includes('--no-overage-notify') ? false : undefined;
|
|
511
|
+
const overageNotifyFromEnv = parseBooleanEnv(process.env['DARIO_OVERAGE_NOTIFY']);
|
|
512
|
+
const overageGuardNotifyOs = overageNotifyFromFlag
|
|
513
|
+
?? overageNotifyFromEnv
|
|
514
|
+
?? fileCfg.overageGuard?.notifyOs
|
|
515
|
+
?? true;
|
|
426
516
|
// Non-loopback bind without DARIO_API_KEY turns dario into an open
|
|
427
517
|
// OAuth-subscription relay for anyone on the reachable network. Refuse
|
|
428
518
|
// to start rather than rely on the operator to read the startup banner.
|
|
@@ -442,7 +532,7 @@ async function proxy() {
|
|
|
442
532
|
console.error(`[dario] Override (not recommended): pass --unsafe-no-auth if you have out-of-band network controls and accept the risk.`);
|
|
443
533
|
process.exit(1);
|
|
444
534
|
}
|
|
445
|
-
await startProxy({ port, host, verbose, verboseBodies, model, passthrough, preserveTools, hybridTools, mergeTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, thinkTimeBaseMs, thinkTimePerTokenMs, thinkTimeJitterMs, thinkTimeMaxMs, sessionStartMinMs, sessionStartJitterMs, stealth, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, effort, maxTokens, logFile, passthroughBetas, systemPrompt });
|
|
535
|
+
await startProxy({ port, host, verbose, verboseBodies, model, passthrough, preserveTools, hybridTools, mergeTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, thinkTimeBaseMs, thinkTimePerTokenMs, thinkTimeJitterMs, thinkTimeMaxMs, sessionStartMinMs, sessionStartJitterMs, stealth, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, effort, maxTokens, logFile, passthroughBetas, systemPrompt, overageGuardEnabled, overageGuardBehavior, overageGuardCooldownMs, overageGuardNotifyOs });
|
|
446
536
|
}
|
|
447
537
|
/**
|
|
448
538
|
* Parse `--system-prompt=<verbatim|partial|aggressive|filepath>` (or the
|
|
@@ -937,6 +1027,11 @@ async function help() {
|
|
|
937
1027
|
dario proxy [options] Start the API proxy server
|
|
938
1028
|
dario status Check authentication status
|
|
939
1029
|
dario refresh Force token refresh
|
|
1030
|
+
dario resume Clear the overage-guard halt on a running proxy.
|
|
1031
|
+
v4.1.0+. Idempotent: returns "no-op" if the
|
|
1032
|
+
proxy isn't halted. Errors with a friendly hint
|
|
1033
|
+
if no proxy is running on localhost:3456.
|
|
1034
|
+
POSTs /admin/resume on the local proxy. (dario#288)
|
|
940
1035
|
dario logout Remove saved credentials
|
|
941
1036
|
dario accounts list List accounts in the multi-account pool
|
|
942
1037
|
dario accounts add NAME [--manual] [--from-keychain[=<target>]]
|
|
@@ -1194,6 +1289,37 @@ async function help() {
|
|
|
1194
1289
|
ceiling server-side, so too-high values
|
|
1195
1290
|
return a clean 400.
|
|
1196
1291
|
Env: DARIO_MAX_TOKENS. (dario#88)
|
|
1292
|
+
--no-overage-guard Disable the overage-guard (v4.1.0+). Default
|
|
1293
|
+
behavior halts the proxy on any response with
|
|
1294
|
+
representative-claim=overage and returns 503
|
|
1295
|
+
with an Anthropic-shaped error body until
|
|
1296
|
+
cooldown expires or \`dario resume\` clears
|
|
1297
|
+
the state. Subscribers should never see an
|
|
1298
|
+
overage hit during normal operation; one
|
|
1299
|
+
means something is wrong (wire drift,
|
|
1300
|
+
classifier change, account misconfig).
|
|
1301
|
+
Env: DARIO_OVERAGE_GUARD=off. (dario#288)
|
|
1302
|
+
--overage-behavior=<halt|warn>
|
|
1303
|
+
Behavior when overage is detected (v4.1.0+):
|
|
1304
|
+
halt — return 503 to new /v1/messages
|
|
1305
|
+
requests until resume / cooldown.
|
|
1306
|
+
Default. Strongest protection.
|
|
1307
|
+
warn — emit events + OS notification only,
|
|
1308
|
+
keep forwarding. Visibility mode for
|
|
1309
|
+
operators who want the signal without
|
|
1310
|
+
cutting off traffic.
|
|
1311
|
+
Env: DARIO_OVERAGE_BEHAVIOR.
|
|
1312
|
+
--overage-cooldown=MS Ms to wait before auto-clearing the halt
|
|
1313
|
+
state (v4.1.0+). Default: 1800000 (30 min).
|
|
1314
|
+
Manual \`dario resume\` clears immediately
|
|
1315
|
+
regardless of this value.
|
|
1316
|
+
Env: DARIO_OVERAGE_COOLDOWN.
|
|
1317
|
+
--no-overage-notify Suppress the native desktop notification on
|
|
1318
|
+
halt (v4.1.0+). Terminal BEL is the
|
|
1319
|
+
unconditional floor; TUI banner + SSE event
|
|
1320
|
+
still fire regardless. Use in headless / CI
|
|
1321
|
+
contexts where toast popups don't make sense.
|
|
1322
|
+
Env: DARIO_OVERAGE_NOTIFY=off.
|
|
1197
1323
|
--port=PORT Port to listen on (default: 3456)
|
|
1198
1324
|
--host=ADDRESS Address to bind to (default: 127.0.0.1)
|
|
1199
1325
|
Use 0.0.0.0 for LAN; see README for DARIO_API_KEY
|
|
@@ -1802,6 +1928,7 @@ const commands = {
|
|
|
1802
1928
|
status,
|
|
1803
1929
|
proxy,
|
|
1804
1930
|
refresh,
|
|
1931
|
+
resume,
|
|
1805
1932
|
logout,
|
|
1806
1933
|
accounts,
|
|
1807
1934
|
backend,
|
package/dist/config-file.d.ts
CHANGED
|
@@ -86,6 +86,32 @@ export interface DarioConfig {
|
|
|
86
86
|
systemPrompt?: string | null;
|
|
87
87
|
preserveOrchestrationTags?: boolean;
|
|
88
88
|
logFile?: string | null;
|
|
89
|
+
/**
|
|
90
|
+
* Overage-guard — halt the proxy on the first response carrying
|
|
91
|
+
* `representative-claim: overage`. Subscribers should never see a
|
|
92
|
+
* single overage hit during normal operation; one means something
|
|
93
|
+
* is wrong (wire-shape drift, classifier change, account misconfig)
|
|
94
|
+
* and continuing to forward requests bleeds against per-token
|
|
95
|
+
* billing. See dario#288.
|
|
96
|
+
*
|
|
97
|
+
* `behavior: 'halt'` — return 503 with an Anthropic-shaped error
|
|
98
|
+
* body until cooldown expires or `dario resume`
|
|
99
|
+
* runs. Default.
|
|
100
|
+
* `behavior: 'warn'` — emit the SSE event + OS notification but
|
|
101
|
+
* leave proxy behavior unchanged.
|
|
102
|
+
*
|
|
103
|
+
* `cooldownMs` — auto-resume delay after a halt. 30 min default.
|
|
104
|
+
*
|
|
105
|
+
* `notifyOs` — best-effort native desktop notification on halt
|
|
106
|
+
* (osascript/notify-send/BurntToast); terminal BEL is
|
|
107
|
+
* the unconditional floor.
|
|
108
|
+
*/
|
|
109
|
+
overageGuard?: {
|
|
110
|
+
enabled?: boolean;
|
|
111
|
+
behavior?: 'halt' | 'warn';
|
|
112
|
+
cooldownMs?: number;
|
|
113
|
+
notifyOs?: boolean;
|
|
114
|
+
};
|
|
89
115
|
}
|
|
90
116
|
/**
|
|
91
117
|
* Defaults match the v3.x CLI flag defaults exactly. Any value not
|
package/dist/config-file.js
CHANGED
|
@@ -71,6 +71,12 @@ export function defaultConfig() {
|
|
|
71
71
|
systemPrompt: null,
|
|
72
72
|
preserveOrchestrationTags: false,
|
|
73
73
|
logFile: null,
|
|
74
|
+
overageGuard: {
|
|
75
|
+
enabled: true,
|
|
76
|
+
behavior: 'halt',
|
|
77
|
+
cooldownMs: 30 * 60 * 1000,
|
|
78
|
+
notifyOs: true,
|
|
79
|
+
},
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
82
|
/**
|
|
@@ -320,6 +326,23 @@ function sanitize(parsed) {
|
|
|
320
326
|
const logFile = pickStringOrNull('logFile');
|
|
321
327
|
if (logFile !== undefined)
|
|
322
328
|
out.logFile = logFile;
|
|
329
|
+
if (isPlainObject(parsed.overageGuard)) {
|
|
330
|
+
out.overageGuard = {};
|
|
331
|
+
if (typeof parsed.overageGuard.enabled === 'boolean') {
|
|
332
|
+
out.overageGuard.enabled = parsed.overageGuard.enabled;
|
|
333
|
+
}
|
|
334
|
+
if (parsed.overageGuard.behavior === 'halt' || parsed.overageGuard.behavior === 'warn') {
|
|
335
|
+
out.overageGuard.behavior = parsed.overageGuard.behavior;
|
|
336
|
+
}
|
|
337
|
+
if (typeof parsed.overageGuard.cooldownMs === 'number'
|
|
338
|
+
&& Number.isFinite(parsed.overageGuard.cooldownMs)
|
|
339
|
+
&& parsed.overageGuard.cooldownMs >= 0) {
|
|
340
|
+
out.overageGuard.cooldownMs = parsed.overageGuard.cooldownMs;
|
|
341
|
+
}
|
|
342
|
+
if (typeof parsed.overageGuard.notifyOs === 'boolean') {
|
|
343
|
+
out.overageGuard.notifyOs = parsed.overageGuard.notifyOs;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
323
346
|
// Silence unused-warning helper.
|
|
324
347
|
void pickNumberOrNull;
|
|
325
348
|
return out;
|
package/dist/notify.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort cross-platform desktop notification dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Pure Node, no new dependencies. Resolves the platform's native toast
|
|
5
|
+
* mechanism at load time, falls back to the terminal BEL character on any
|
|
6
|
+
* platform that doesn't have one (or where the native path is missing).
|
|
7
|
+
*
|
|
8
|
+
* Backends:
|
|
9
|
+
* - macOS: `osascript -e 'display notification "msg" with title "dario"'`
|
|
10
|
+
* - Linux: `notify-send "dario" "msg"` (gnome / kde / dunst / mako)
|
|
11
|
+
* - Windows: `powershell -Command New-BurntToastNotification ...` if the
|
|
12
|
+
* `BurntToast` module is installed; falls back to `msg.exe`,
|
|
13
|
+
* else BEL only.
|
|
14
|
+
*
|
|
15
|
+
* BEL char (`\x07`) is the unconditional floor — works on every terminal
|
|
16
|
+
* that respects ANSI control characters, which is nearly all of them.
|
|
17
|
+
*
|
|
18
|
+
* Silent on failure: a missing `osascript`/`notify-send`/PowerShell path
|
|
19
|
+
* is the common case for non-interactive sessions, headless CI runs, and
|
|
20
|
+
* SSH-into-a-server flows. The TUI banner is the authoritative surface;
|
|
21
|
+
* OS-notify is the loud, attention-grabbing supplement.
|
|
22
|
+
*
|
|
23
|
+
* See dario#288 — overage-guard.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Fire a native notification. Returns immediately — the underlying spawn
|
|
27
|
+
* is fire-and-forget. Errors (missing binary, permission denied, no
|
|
28
|
+
* graphical session) are swallowed; the caller already has the in-app
|
|
29
|
+
* surface and shouldn't depend on this firing.
|
|
30
|
+
*
|
|
31
|
+
* `title` and `message` are passed verbatim except for shell-meta escaping
|
|
32
|
+
* — single quotes and backticks are stripped so the AppleScript / shell
|
|
33
|
+
* payload can't be hijacked by a malicious upstream response.
|
|
34
|
+
*/
|
|
35
|
+
export declare function notify(title: string, message: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Test-mode hook — returns a notifier that pushes into a captured array
|
|
38
|
+
* instead of firing real OS notifications. Used by test/notify-cross-
|
|
39
|
+
* platform.mjs to verify the dispatch path without invoking osascript.
|
|
40
|
+
*/
|
|
41
|
+
export declare function captureNotifier(): {
|
|
42
|
+
notify: (title: string, message: string) => void;
|
|
43
|
+
captured: Array<{
|
|
44
|
+
title: string;
|
|
45
|
+
message: string;
|
|
46
|
+
ts: number;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
package/dist/notify.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort cross-platform desktop notification dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Pure Node, no new dependencies. Resolves the platform's native toast
|
|
5
|
+
* mechanism at load time, falls back to the terminal BEL character on any
|
|
6
|
+
* platform that doesn't have one (or where the native path is missing).
|
|
7
|
+
*
|
|
8
|
+
* Backends:
|
|
9
|
+
* - macOS: `osascript -e 'display notification "msg" with title "dario"'`
|
|
10
|
+
* - Linux: `notify-send "dario" "msg"` (gnome / kde / dunst / mako)
|
|
11
|
+
* - Windows: `powershell -Command New-BurntToastNotification ...` if the
|
|
12
|
+
* `BurntToast` module is installed; falls back to `msg.exe`,
|
|
13
|
+
* else BEL only.
|
|
14
|
+
*
|
|
15
|
+
* BEL char (`\x07`) is the unconditional floor — works on every terminal
|
|
16
|
+
* that respects ANSI control characters, which is nearly all of them.
|
|
17
|
+
*
|
|
18
|
+
* Silent on failure: a missing `osascript`/`notify-send`/PowerShell path
|
|
19
|
+
* is the common case for non-interactive sessions, headless CI runs, and
|
|
20
|
+
* SSH-into-a-server flows. The TUI banner is the authoritative surface;
|
|
21
|
+
* OS-notify is the loud, attention-grabbing supplement.
|
|
22
|
+
*
|
|
23
|
+
* See dario#288 — overage-guard.
|
|
24
|
+
*/
|
|
25
|
+
import { spawn } from 'node:child_process';
|
|
26
|
+
import { platform } from 'node:os';
|
|
27
|
+
/**
|
|
28
|
+
* Fire a native notification. Returns immediately — the underlying spawn
|
|
29
|
+
* is fire-and-forget. Errors (missing binary, permission denied, no
|
|
30
|
+
* graphical session) are swallowed; the caller already has the in-app
|
|
31
|
+
* surface and shouldn't depend on this firing.
|
|
32
|
+
*
|
|
33
|
+
* `title` and `message` are passed verbatim except for shell-meta escaping
|
|
34
|
+
* — single quotes and backticks are stripped so the AppleScript / shell
|
|
35
|
+
* payload can't be hijacked by a malicious upstream response.
|
|
36
|
+
*/
|
|
37
|
+
export function notify(title, message) {
|
|
38
|
+
// Always BEL first — works in every TTY, doesn't depend on a graphical
|
|
39
|
+
// session. The OS notification on top is best-effort.
|
|
40
|
+
try {
|
|
41
|
+
process.stderr.write('\x07');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// stderr write can fail under exotic conditions (closed handle,
|
|
45
|
+
// detached process); silent — we have nothing else to fall back to.
|
|
46
|
+
}
|
|
47
|
+
const safeTitle = sanitize(title);
|
|
48
|
+
const safeMessage = sanitize(message);
|
|
49
|
+
const plat = platform();
|
|
50
|
+
try {
|
|
51
|
+
if (plat === 'darwin') {
|
|
52
|
+
// AppleScript single-quote inside the script body would need
|
|
53
|
+
// escaping; sanitize() strips them so the literal substitution
|
|
54
|
+
// below stays safe. Spawned via array argv to avoid a shell
|
|
55
|
+
// entirely.
|
|
56
|
+
spawn('osascript', [
|
|
57
|
+
'-e',
|
|
58
|
+
`display notification "${safeMessage}" with title "${safeTitle}"`,
|
|
59
|
+
], { detached: true, stdio: 'ignore' }).unref();
|
|
60
|
+
}
|
|
61
|
+
else if (plat === 'linux') {
|
|
62
|
+
spawn('notify-send', [safeTitle, safeMessage], {
|
|
63
|
+
detached: true,
|
|
64
|
+
stdio: 'ignore',
|
|
65
|
+
}).unref();
|
|
66
|
+
}
|
|
67
|
+
else if (plat === 'win32') {
|
|
68
|
+
// BurntToast is the cleanest path — single-line PowerShell, real
|
|
69
|
+
// Windows toast notification. If BurntToast isn't installed the
|
|
70
|
+
// command fails silently (stdio: ignore swallows the error
|
|
71
|
+
// output), which is the desired behavior.
|
|
72
|
+
//
|
|
73
|
+
// We don't probe-then-spawn; the cost of one failed BurntToast
|
|
74
|
+
// attempt is the same as one probe attempt, and probing makes the
|
|
75
|
+
// hot path slower for the success case.
|
|
76
|
+
const ps = `try { Import-Module BurntToast -ErrorAction Stop; New-BurntToastNotification -Text '${safeTitle}', '${safeMessage}' } catch { exit 1 }`;
|
|
77
|
+
spawn('powershell.exe', [
|
|
78
|
+
'-NoProfile',
|
|
79
|
+
'-NonInteractive',
|
|
80
|
+
'-Command',
|
|
81
|
+
ps,
|
|
82
|
+
], { detached: true, stdio: 'ignore' }).unref();
|
|
83
|
+
}
|
|
84
|
+
// freebsd / openbsd / aix / sunos / android — BEL only. There's no
|
|
85
|
+
// single "right" native notification on these platforms.
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// spawn() itself can throw on EMFILE or similar; silent.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Strip characters that would break the embedded shell/AppleScript
|
|
93
|
+
* payload or allow command injection. Conservative: only allow printable
|
|
94
|
+
* ASCII + common Unicode word chars + a small whitelist of punctuation.
|
|
95
|
+
*
|
|
96
|
+
* This is NOT a general-purpose sanitizer; it exists to defang text we
|
|
97
|
+
* already control (our own messages) from accidentally containing
|
|
98
|
+
* AppleScript-breaking characters like a stray double quote.
|
|
99
|
+
*/
|
|
100
|
+
function sanitize(s) {
|
|
101
|
+
return s
|
|
102
|
+
.replace(/[\r\n]/g, ' ') // collapse newlines
|
|
103
|
+
.replace(/[`'"$]/g, '') // strip shell metas + quotes
|
|
104
|
+
.replace(/\\/g, '/') // strip backslashes
|
|
105
|
+
.slice(0, 200); // cap length; notifications truncate anyway
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Test-mode hook — returns a notifier that pushes into a captured array
|
|
109
|
+
* instead of firing real OS notifications. Used by test/notify-cross-
|
|
110
|
+
* platform.mjs to verify the dispatch path without invoking osascript.
|
|
111
|
+
*/
|
|
112
|
+
export function captureNotifier() {
|
|
113
|
+
const captured = [];
|
|
114
|
+
return {
|
|
115
|
+
notify: (title, message) => {
|
|
116
|
+
captured.push({ title, message, ts: Date.now() });
|
|
117
|
+
},
|
|
118
|
+
captured,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overage-guard — halt the proxy on the first `representative-claim: overage`
|
|
3
|
+
* response to prevent silent API-rate bleed.
|
|
4
|
+
*
|
|
5
|
+
* Subscribers should never see a single overage hit during normal
|
|
6
|
+
* operation. One means something is wrong (wire-shape drift, classifier
|
|
7
|
+
* change, account misconfig, billing-flip after a CC release) and
|
|
8
|
+
* continuing to forward requests bleeds against per-token billing.
|
|
9
|
+
*
|
|
10
|
+
* The guard subscribes to the Analytics record stream — every completed
|
|
11
|
+
* request emits a record carrying its `claim` (raw representative-claim
|
|
12
|
+
* value). When `claim === 'overage'` lands, the guard transitions to a
|
|
13
|
+
* halted state and emits a `'halt'` event. The HTTP request path checks
|
|
14
|
+
* `isHalted()` on every incoming request and returns 503 with an
|
|
15
|
+
* Anthropic-shaped error body when halted.
|
|
16
|
+
*
|
|
17
|
+
* Resume paths:
|
|
18
|
+
* - explicit: `dario resume` CLI → POST /admin/resume → `clear('manual')`
|
|
19
|
+
* - automatic: cooldown expires (default 30 min) → `clear('cooldown')`
|
|
20
|
+
* - TUI: `r` key on Status tab → POST /admin/resume (same as CLI)
|
|
21
|
+
*
|
|
22
|
+
* Behavior:
|
|
23
|
+
* - `halt` (default) — record halted state + return 503 on subsequent requests
|
|
24
|
+
* - `warn` — emit events + notify only; proxy keeps forwarding (visibility-only mode)
|
|
25
|
+
*
|
|
26
|
+
* See dario#288.
|
|
27
|
+
*/
|
|
28
|
+
import { EventEmitter } from 'node:events';
|
|
29
|
+
import type { Analytics, RequestRecord } from './analytics.js';
|
|
30
|
+
export interface HaltState {
|
|
31
|
+
since: number;
|
|
32
|
+
cooldownUntil: number;
|
|
33
|
+
reason: 'overage_detected';
|
|
34
|
+
request: {
|
|
35
|
+
timestamp: number;
|
|
36
|
+
model: string;
|
|
37
|
+
account: string;
|
|
38
|
+
claim: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export interface OverageGuardOptions {
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
behavior: 'halt' | 'warn';
|
|
44
|
+
cooldownMs: number;
|
|
45
|
+
notifyOs: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Best-effort native desktop notification dispatcher. Pass the function
|
|
48
|
+
* from `./notify.ts` here. Optional — silent failure if absent. The
|
|
49
|
+
* guard always emits the `'halt'` event for in-process subscribers
|
|
50
|
+
* (the SSE stream, the TUI) regardless of whether OS-notify fired.
|
|
51
|
+
*/
|
|
52
|
+
notifier?: (title: string, message: string) => void;
|
|
53
|
+
}
|
|
54
|
+
export declare class OverageGuard extends EventEmitter {
|
|
55
|
+
private opts;
|
|
56
|
+
private halted;
|
|
57
|
+
private cooldownTimer;
|
|
58
|
+
private analyticsListener;
|
|
59
|
+
constructor(opts: OverageGuardOptions);
|
|
60
|
+
/**
|
|
61
|
+
* Subscribe to an Analytics instance. Every record emitted with
|
|
62
|
+
* `claim === 'overage'` triggers halt (when behavior === 'halt') or a
|
|
63
|
+
* warn-only event (when behavior === 'warn').
|
|
64
|
+
*
|
|
65
|
+
* Idempotent — calling attach() a second time replaces the listener
|
|
66
|
+
* rather than stacking; useful for tests.
|
|
67
|
+
*/
|
|
68
|
+
attach(analytics: Analytics): void;
|
|
69
|
+
/**
|
|
70
|
+
* Synthesize a halt event from a record. Public for the test harness;
|
|
71
|
+
* production code reaches this via attach() + the live Analytics stream.
|
|
72
|
+
*/
|
|
73
|
+
onOverageDetected(r: RequestRecord): void;
|
|
74
|
+
/**
|
|
75
|
+
* Resume the proxy. Emits a 'resume' event with the reason.
|
|
76
|
+
*
|
|
77
|
+
* No-op when not currently halted. Safe to call from any path
|
|
78
|
+
* (CLI, /admin/resume HTTP endpoint, TUI `r` key, cooldown timer).
|
|
79
|
+
*/
|
|
80
|
+
clear(reason: 'manual' | 'cooldown'): void;
|
|
81
|
+
/** Current halt state, or `null` if not halted. */
|
|
82
|
+
state(): HaltState | null;
|
|
83
|
+
/** Quick boolean for the request hot-path. */
|
|
84
|
+
isHalted(): boolean;
|
|
85
|
+
/** Detach from Analytics. Used by tests and by graceful shutdown. */
|
|
86
|
+
destroy(): void;
|
|
87
|
+
/** Expose options for the /status endpoint + TUI Status tab. */
|
|
88
|
+
config(): Readonly<OverageGuardOptions>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The Anthropic-shaped error body returned by halted-503 responses. The
|
|
92
|
+
* shape matches what `api.anthropic.com` emits for any 4xx so CC /
|
|
93
|
+
* Cursor / Aider / Cline surface the message verbatim to the user — no
|
|
94
|
+
* client-specific handling needed.
|
|
95
|
+
*/
|
|
96
|
+
export declare function buildHaltErrorBody(state: HaltState): {
|
|
97
|
+
type: 'error';
|
|
98
|
+
error: {
|
|
99
|
+
type: string;
|
|
100
|
+
message: string;
|
|
101
|
+
};
|
|
102
|
+
};
|