@askalf/dario 5.4.16 → 5.4.19
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/cc-template.d.ts +11 -0
- package/dist/cc-template.js +68 -0
- package/dist/live-fingerprint.d.ts +0 -6
- package/dist/live-fingerprint.js +0 -8
- package/dist/proxy.js +26 -3
- package/dist/version.d.ts +0 -2
- package/dist/version.js +0 -4
- package/docs/admin-api.md +154 -0
- package/docs/commands.md +77 -0
- package/docs/configuration.md +109 -0
- package/docs/docker.md +233 -0
- package/docs/drift-monitor.md +290 -0
- package/docs/faq.md +145 -0
- package/docs/integrations/agent-compat.md +269 -0
- package/docs/integrations/compat-matrix.md +51 -0
- package/docs/integrations/hands-walkthrough.md +295 -0
- package/docs/integrations/openclaw-walkthrough.md +248 -0
- package/docs/integrations/openhands-walkthrough.md +255 -0
- package/docs/mcp-server.md +22 -0
- package/docs/multi-account-pool.md +68 -0
- package/docs/research/system-prompt-classifier-study.md +288 -0
- package/docs/returning.md +94 -0
- package/docs/sub-agent.md +13 -0
- package/docs/system-prompt.md +107 -0
- package/docs/usage.md +123 -0
- package/docs/vpn-routing.md +108 -0
- package/docs/why-now-2026-06.md +93 -0
- package/docs/wire-fidelity.md +16 -0
- package/package.json +4 -2
package/docs/docker.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Running dario in Docker
|
|
2
|
+
|
|
3
|
+
Official image: **`ghcr.io/askalf/dario`**
|
|
4
|
+
|
|
5
|
+
Multi-arch (`linux/amd64` + `linux/arm64`), published from the same release
|
|
6
|
+
workflow that ships the npm package, so image tags are always in lockstep
|
|
7
|
+
with `@askalf/dario` versions on npm.
|
|
8
|
+
|
|
9
|
+
## Tags
|
|
10
|
+
|
|
11
|
+
| Tag | Tracks |
|
|
12
|
+
|------------|-------------------------------------------------------|
|
|
13
|
+
| `latest` | Latest published release |
|
|
14
|
+
| `vX.Y.Z` | A specific release (recommended for production pins) |
|
|
15
|
+
| `vX.Y` | Latest patch on a minor line |
|
|
16
|
+
| `vX` | Latest minor on a major line |
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# 1. One-time OAuth bootstrap — interactive, manual flow (no localhost callback).
|
|
22
|
+
docker volume create dario-config
|
|
23
|
+
docker run --rm -it -v dario-config:/home/dario/.dario \
|
|
24
|
+
ghcr.io/askalf/dario:latest login --manual
|
|
25
|
+
|
|
26
|
+
# 2. Run the proxy. DARIO_API_KEY is REQUIRED — see "Why an API key is mandatory" below.
|
|
27
|
+
docker run -d --name dario \
|
|
28
|
+
-p 3456:3456 \
|
|
29
|
+
-v dario-config:/home/dario/.dario \
|
|
30
|
+
-e DARIO_API_KEY="$(openssl rand -hex 32)" \
|
|
31
|
+
ghcr.io/askalf/dario:latest
|
|
32
|
+
|
|
33
|
+
# 3. Point your tools at it (using the same key you set above).
|
|
34
|
+
export ANTHROPIC_BASE_URL=http://localhost:3456
|
|
35
|
+
export ANTHROPIC_API_KEY=<the key from step 2>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Why an API key is mandatory
|
|
39
|
+
|
|
40
|
+
The image binds to `0.0.0.0` so port maps and k8s services can reach the
|
|
41
|
+
proxy. dario refuses to start on a non-loopback bind unless `DARIO_API_KEY`
|
|
42
|
+
is set, because an unauthenticated proxy on a reachable interface is an
|
|
43
|
+
open OAuth-subscription relay for anyone on the network (dario#74). The
|
|
44
|
+
image inherits that refusal — there is no "just don't set a key" path.
|
|
45
|
+
|
|
46
|
+
Escape hatches if you have out-of-band network controls:
|
|
47
|
+
|
|
48
|
+
- `-e DARIO_HOST=127.0.0.1` — bind loopback inside the container. Useless
|
|
49
|
+
for `-p` port maps, but makes sense if another container in the same
|
|
50
|
+
network namespace is the only client.
|
|
51
|
+
- `--unsafe-no-auth` as a CMD arg. Don't.
|
|
52
|
+
|
|
53
|
+
## OAuth in a container
|
|
54
|
+
|
|
55
|
+
`dario login --manual` skips the localhost-callback flow and prints a URL you
|
|
56
|
+
open in any browser on any machine. Anthropic's authorize endpoint renders the
|
|
57
|
+
authorization code on a copy-paste page; paste it back into the container's
|
|
58
|
+
stdin and dario writes `~/.dario/credentials.json` into the mounted volume.
|
|
59
|
+
Subsequent container starts find the credentials and the `proxy` subcommand
|
|
60
|
+
uses them directly.
|
|
61
|
+
|
|
62
|
+
The container detector (`/.dockerenv` + cgroup probe) auto-suggests
|
|
63
|
+
`--manual` if you accidentally run `login` without it — but the suggestion
|
|
64
|
+
prints to stdout and then fails on the localhost bind, so just use `--manual`
|
|
65
|
+
from the start.
|
|
66
|
+
|
|
67
|
+
### Pre-seeding credentials (no interactive container needed)
|
|
68
|
+
|
|
69
|
+
If you can't allocate a TTY (k8s Job, CI, immutable infra), run
|
|
70
|
+
`dario login --manual` once on your workstation, then ship
|
|
71
|
+
`~/.dario/credentials.json` into the volume by your usual secrets path
|
|
72
|
+
(SOPS, sealed-secrets, `kubectl create secret generic … --from-file`, etc.).
|
|
73
|
+
The refresh token in that file is good until you revoke it.
|
|
74
|
+
|
|
75
|
+
### Headless bootstrap over HTTP (`DARIO_ADMIN=1`) — no console at all
|
|
76
|
+
|
|
77
|
+
The third option skips both the interactive container *and* the pre-seeded
|
|
78
|
+
file: start the container **empty** with the admin API enabled, then provision
|
|
79
|
+
the first account over HTTP from anywhere:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
docker run -d -v dario-data:/home/dario/.dario -p 3456:3456 \
|
|
83
|
+
-e DARIO_API_KEY=<proxy-key> -e DARIO_ADMIN=1 -e DARIO_ADMIN_TOKEN=<admin-token> \
|
|
84
|
+
ghcr.io/askalf/dario:latest
|
|
85
|
+
|
|
86
|
+
curl -s -X POST -H "authorization: Bearer <admin-token>" \
|
|
87
|
+
http://<host>:3456/admin/login/start # -> { authorize_url, alias, ... }
|
|
88
|
+
# open authorize_url in any browser, approve, paste the code back:
|
|
89
|
+
curl -s -X POST -H "authorization: Bearer <admin-token>" \
|
|
90
|
+
http://<host>:3456/admin/login/complete -d '{"alias":"account-1","code":"<code>"}'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The account is routable the moment the `200` lands — the live pool hot-reloads,
|
|
94
|
+
no restart. Until then, LLM requests answer a truthful
|
|
95
|
+
`503 { "error": "No account configured" }` and `/health` reports `degraded`,
|
|
96
|
+
so gate any bootstrap automation on the container being *up*, not *healthy*.
|
|
97
|
+
Full endpoint reference, audit trail, and rate-limit behavior:
|
|
98
|
+
[`docs/admin-api.md`](./admin-api.md).
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
All flags have env-var equivalents. The image sets sensible container defaults:
|
|
103
|
+
|
|
104
|
+
| Variable | Default in image | Purpose |
|
|
105
|
+
|---------------------------|------------------|-----------------------------------------------|
|
|
106
|
+
| `DARIO_HOST` | `0.0.0.0` | Bind address (image flips from `127.0.0.1`) |
|
|
107
|
+
| `DARIO_PORT` | `3456` | Listen port |
|
|
108
|
+
| `DARIO_API_KEY` | unset (**required**) | Required because of the `0.0.0.0` bind — see "Why an API key is mandatory" |
|
|
109
|
+
| `DARIO_CORS_ORIGIN` | unset | Override the default `http://localhost:<port>` allow-list |
|
|
110
|
+
| `DARIO_LOG_FILE` | unset | Path inside the container (mount a volume) |
|
|
111
|
+
| `DARIO_LOG_BODIES` | unset | `1` to log request/response bodies |
|
|
112
|
+
| `DARIO_PASSTHROUGH_BETAS` | unset | `1` to forward `anthropic-beta` headers as-is |
|
|
113
|
+
| `DARIO_CLAUDE_BIN` | unset | Path to a Claude Code binary (optional, for live template capture) |
|
|
114
|
+
| `DARIO_NO_BUN` | unset | `1` to skip the Bun auto-relaunch (not recommended) |
|
|
115
|
+
| `DARIO_ADMIN` | unset | `1` mounts the headless admin API at `/admin/*` — see [`docs/admin-api.md`](./admin-api.md) |
|
|
116
|
+
| `DARIO_ADMIN_TOKEN` | unset | Bearer token for admin calls (falls back to `DARIO_API_KEY`); enabled-but-tokenless fails closed |
|
|
117
|
+
| `DARIO_ADMIN_RATE_LIMIT` | unset | `off` to disable admin rate limiting (on by default) |
|
|
118
|
+
|
|
119
|
+
### Why `DARIO_HOST=0.0.0.0` in the image
|
|
120
|
+
|
|
121
|
+
dario defaults to `127.0.0.1` on host installs because it's a local-only
|
|
122
|
+
proxy. In a container the loopback interface is internal to the container,
|
|
123
|
+
so the proxy would be unreachable through `-p` port maps or k8s services.
|
|
124
|
+
The image flips the default to `0.0.0.0` and pairs it with the
|
|
125
|
+
mandatory-API-key refusal above; the container's network namespace boundary
|
|
126
|
+
becomes the trust boundary instead.
|
|
127
|
+
|
|
128
|
+
## Persistence
|
|
129
|
+
|
|
130
|
+
Mount `/home/dario/.dario` as a volume. It holds:
|
|
131
|
+
|
|
132
|
+
- `credentials.json` — OAuth tokens (access + refresh)
|
|
133
|
+
- `accounts/*.json` — multi-account pool entries, if you use the pool
|
|
134
|
+
- `cc-oauth-cache-v6.json` — cached CC OAuth config (auto-refreshes)
|
|
135
|
+
- `oauth-config.override.json` — user-supplied OAuth config override
|
|
136
|
+
|
|
137
|
+
Without a volume, you'd lose the refresh token on every container restart and
|
|
138
|
+
have to re-run `dario login` each time.
|
|
139
|
+
|
|
140
|
+
## Healthcheck
|
|
141
|
+
|
|
142
|
+
The image ships a Docker `HEALTHCHECK` that hits `/health` every 30s. Once an
|
|
143
|
+
account is loaded and its OAuth is usable the endpoint returns HTTP 200
|
|
144
|
+
`{"status":"ok", ...}`; when OAuth is broken/absent it returns HTTP 503
|
|
145
|
+
`{"status":"degraded", ...}`. Use the same endpoint for k8s liveness/readiness
|
|
146
|
+
probes:
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
livenessProbe:
|
|
150
|
+
httpGet: { path: /health, port: 3456 }
|
|
151
|
+
periodSeconds: 30
|
|
152
|
+
readinessProbe:
|
|
153
|
+
httpGet: { path: /health, port: 3456 }
|
|
154
|
+
periodSeconds: 5
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
> **Empty admin-mode start:** with `DARIO_ADMIN=1` and no accounts yet,
|
|
158
|
+
> `/health` is `degraded`/503 by design (every LLM call would 503 until an
|
|
159
|
+
> account exists), so a readiness probe on `/health` holds the pod out of
|
|
160
|
+
> rotation until you provision the first account via the
|
|
161
|
+
> [admin API](./admin-api.md). Provision before relying on readiness, or use a
|
|
162
|
+
> long-window `startupProbe` around the bootstrap.
|
|
163
|
+
|
|
164
|
+
## Kubernetes example
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
apiVersion: v1
|
|
168
|
+
kind: Secret
|
|
169
|
+
metadata: { name: dario-credentials }
|
|
170
|
+
type: Opaque
|
|
171
|
+
stringData:
|
|
172
|
+
# The API key clients send as `ANTHROPIC_API_KEY` to reach the proxy.
|
|
173
|
+
api-key: <SOME_RANDOM_SECRET>
|
|
174
|
+
data:
|
|
175
|
+
# base64-encoded contents of ~/.dario/credentials.json from a prior
|
|
176
|
+
# `dario login --manual` on your workstation.
|
|
177
|
+
credentials.json: <BASE64>
|
|
178
|
+
---
|
|
179
|
+
apiVersion: apps/v1
|
|
180
|
+
kind: Deployment
|
|
181
|
+
metadata: { name: dario }
|
|
182
|
+
spec:
|
|
183
|
+
replicas: 1
|
|
184
|
+
selector: { matchLabels: { app: dario } }
|
|
185
|
+
template:
|
|
186
|
+
metadata: { labels: { app: dario } }
|
|
187
|
+
spec:
|
|
188
|
+
containers:
|
|
189
|
+
- name: dario
|
|
190
|
+
image: ghcr.io/askalf/dario:latest
|
|
191
|
+
ports: [{ containerPort: 3456 }]
|
|
192
|
+
env:
|
|
193
|
+
- { name: DARIO_API_KEY, valueFrom: { secretKeyRef: { name: dario-credentials, key: api-key } } }
|
|
194
|
+
volumeMounts:
|
|
195
|
+
- { name: config, mountPath: /home/dario/.dario }
|
|
196
|
+
livenessProbe:
|
|
197
|
+
httpGet: { path: /health, port: 3456 }
|
|
198
|
+
periodSeconds: 30
|
|
199
|
+
readinessProbe:
|
|
200
|
+
httpGet: { path: /health, port: 3456 }
|
|
201
|
+
periodSeconds: 5
|
|
202
|
+
volumes:
|
|
203
|
+
- name: config
|
|
204
|
+
projected:
|
|
205
|
+
sources:
|
|
206
|
+
- secret:
|
|
207
|
+
name: dario-credentials
|
|
208
|
+
items:
|
|
209
|
+
- { key: credentials.json, path: credentials.json }
|
|
210
|
+
---
|
|
211
|
+
apiVersion: v1
|
|
212
|
+
kind: Service
|
|
213
|
+
metadata: { name: dario }
|
|
214
|
+
spec:
|
|
215
|
+
selector: { app: dario }
|
|
216
|
+
ports: [{ port: 3456, targetPort: 3456 }]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Replicas should stay at `1` — dario's OAuth refresh races on a single
|
|
220
|
+
credentials file. For HA, run multiple dario instances each with their own
|
|
221
|
+
account in a [multi-account pool](./multi-account-pool.md).
|
|
222
|
+
|
|
223
|
+
## Image updates
|
|
224
|
+
|
|
225
|
+
The image is rebuilt on every release tag, so any image-update tool that
|
|
226
|
+
watches semver tags works out of the box:
|
|
227
|
+
|
|
228
|
+
- **Renovate** — `docker:enableMajor` + `:vX.Y.Z` pin
|
|
229
|
+
- **Argo CD Image Updater** — `update-strategy: semver`
|
|
230
|
+
- **Keel** — `keel.sh/policy: minor`
|
|
231
|
+
|
|
232
|
+
`:latest` is provided for convenience but you should pin a major or minor in
|
|
233
|
+
production so a breaking change in dario doesn't roll out unattended.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# Drift monitor
|
|
2
|
+
|
|
3
|
+
Dario's bundled CC template (`src/cc-template-data.json`) is the wire-shape
|
|
4
|
+
fallback the proxy uses when it can't fingerprint a live CC install. For that
|
|
5
|
+
fallback to be honest, the bundle has to keep up with what real CC is actually
|
|
6
|
+
sending on the wire. CC drifts in two distinct ways, and there are two
|
|
7
|
+
distinct watchers — one of which needs a self-hosted runner.
|
|
8
|
+
|
|
9
|
+
## Two classes of drift
|
|
10
|
+
|
|
11
|
+
**Class A — npm-release drift.** Anthropic ships a new `@anthropic-ai/claude-code`
|
|
12
|
+
to npm. The binary changes; the wire shape usually changes with it (new tools,
|
|
13
|
+
new system-prompt slots, beta-header swaps). This is the visible kind.
|
|
14
|
+
|
|
15
|
+
> Watched by `.github/workflows/cc-drift-watch.yml`. Runs on a GitHub-hosted
|
|
16
|
+
> runner, no auth required — polls npm, diffs the dist file, opens an issue
|
|
17
|
+
> when the bundled `_version` is behind latest.
|
|
18
|
+
|
|
19
|
+
**Class B — same-binary remote-config drift.** Anthropic does *not* ship a new
|
|
20
|
+
npm version, but the wire shape changes anyway. We documented an instance of
|
|
21
|
+
this in [CHANGELOG `4.2.1`](../CHANGELOG.md#421---2026-05-17): same CC `2.1.143`
|
|
22
|
+
binary, same machine, captures 24h apart produced materially different
|
|
23
|
+
`/v1/messages` bodies (different anthropic-beta header, +355 char system
|
|
24
|
+
prompt). The npm watcher can't see this; the binary is unchanged.
|
|
25
|
+
|
|
26
|
+
> Watched by `.github/workflows/cc-drift-template-watch.yml`. Runs on a
|
|
27
|
+
> **self-hosted** runner because it needs a live, authenticated CC install
|
|
28
|
+
> to capture against. Runs `node scripts/capture-and-bake.mjs --check` every
|
|
29
|
+
> 30 min and opens (or comments on) a `cc-drift-template`-labeled issue when
|
|
30
|
+
> the captured template diverges from the committed bundle.
|
|
31
|
+
>
|
|
32
|
+
> Watched-by-the-watcher: `.github/workflows/cc-drift-watcher-liveness.yml`
|
|
33
|
+
> runs every 2 hours on a github-hosted runner and opens a
|
|
34
|
+
> `cc-watcher-liveness`-labeled alert if the class-B watcher has not had a
|
|
35
|
+
> successful run within 8 hours (≥ 16 missed cycles). Catches "runner went
|
|
36
|
+
> offline silently" — the failure mode where class-B drift goes uncaught
|
|
37
|
+
> because the watcher itself is down. The liveness watcher lives on
|
|
38
|
+
> github-hosted infrastructure deliberately so it survives the exact failure
|
|
39
|
+
> modes it's designed to detect.
|
|
40
|
+
|
|
41
|
+
**Class C — billing-classifier drift** *(v4.6.0)*. Different signal again:
|
|
42
|
+
Anthropic changes the classifier *rules* — adds a new signal, tightens an
|
|
43
|
+
existing one, flips a threshold — and dario's canonical-rebuild output no
|
|
44
|
+
longer scores as `subscription` even though CC's wire shape is unchanged.
|
|
45
|
+
The template-drift watcher cannot see this because nothing in CC's outbound
|
|
46
|
+
has moved; only an end-to-end "send a real request, inspect the billing
|
|
47
|
+
bucket" probe catches it.
|
|
48
|
+
|
|
49
|
+
> Watched by `.github/workflows/cc-billing-classifier-canary.yml`. Runs daily
|
|
50
|
+
> at 06:30 UTC on the same self-hosted runner. Sends one tiny haiku request
|
|
51
|
+
> through `dario proxy` (canonical-rebuild mode, NOT `--passthrough`),
|
|
52
|
+
> captures the `representative-claim` response header, opens a
|
|
53
|
+
> `cc-billing-canary`-labeled alert when it flips to `overage` / `api`
|
|
54
|
+
> / `unknown`. Auto-closes when it next returns a subscription bucket.
|
|
55
|
+
> Cost: ~1 small subscription request per day.
|
|
56
|
+
|
|
57
|
+
## What --check considers drift
|
|
58
|
+
|
|
59
|
+
The `--check` mode in `scripts/capture-and-bake.mjs` deliberately ignores
|
|
60
|
+
fields that always differ between runs (`_captured` timestamp, user-agent
|
|
61
|
+
string, `_version` / `_supportedMaxTested` labels). It flags **shape** drift in:
|
|
62
|
+
|
|
63
|
+
- **tools** added or removed (by name set)
|
|
64
|
+
- **anthropic_beta** header values added or removed
|
|
65
|
+
- **system_prompt** content (any character delta)
|
|
66
|
+
- **body_field_order** (top-level JSON key order)
|
|
67
|
+
- **header_order**
|
|
68
|
+
- **agent_identity** content
|
|
69
|
+
|
|
70
|
+
Exit codes:
|
|
71
|
+
|
|
72
|
+
| Code | Meaning |
|
|
73
|
+
|---|---|
|
|
74
|
+
| 0 | Full match — wire shape AND `_version` label both current |
|
|
75
|
+
| 1 | Infrastructure failure (CC not on PATH, capture timeout, scrub leak, or installed CC **older** than the bundle's capture — stale runner) |
|
|
76
|
+
| 2 | **Shape** drift vs current bundled template (needs a real re-bake) |
|
|
77
|
+
| 3 | **Label-only** drift — wire shape matches but `_version` lags the live CC version |
|
|
78
|
+
|
|
79
|
+
The workflow swallows exit 2 and 3 (continues to the next step) so the
|
|
80
|
+
remediation steps can run; exit 1 fails the job.
|
|
81
|
+
|
|
82
|
+
The stale-runner case matters because an older binary cannot observe forward
|
|
83
|
+
drift — it re-captures the *previous* wire shape, which `--check` would report
|
|
84
|
+
as exit-2 drift and the watcher would auto-rebake as a template **downgrade**.
|
|
85
|
+
That exact sequence reached the ship gate on 2026-07-02 (PR #632: runner CC at
|
|
86
|
+
2.1.197 against the 2.1.198-baked bundle reported the afk-mode beta
|
|
87
|
+
"removed"). The guard compares the captured CC version against the bundle's
|
|
88
|
+
`_version` and exits 1 with an update-the-runner message when the binary is
|
|
89
|
+
older. A *deliberate* downgrade bake (an upstream CC release gets pulled and
|
|
90
|
+
the bundle must go backward) bypasses it with `--allow-older-cc`.
|
|
91
|
+
|
|
92
|
+
### Exit 2 vs exit 3 — why the split, and why only one auto-merges
|
|
93
|
+
|
|
94
|
+
Because `--check` ignores `_version`, a CC release whose wire shape is
|
|
95
|
+
*unchanged* (the common case for a patch bump) produces a bundle whose shape
|
|
96
|
+
matches live CC but whose `_version` label is stale. The shape-only detector
|
|
97
|
+
sees no drift (exit 0 territory), yet `sdk-drift-watch.yml` — which compares
|
|
98
|
+
the `_version` label against `@anthropic-ai/claude-code@latest` on npm — flags
|
|
99
|
+
it, with **nothing to re-bake**. That mismatch used to require a hand PR every
|
|
100
|
+
time (issues #418, #426/#427, #445/#451).
|
|
101
|
+
|
|
102
|
+
Exit 3 captures exactly that case (`computeDrift` empty **and**
|
|
103
|
+
`bundled._version !== live._version`) and writes the live version to
|
|
104
|
+
`label-target.txt`. The **Label-sync** workflow step then runs
|
|
105
|
+
`scripts/label-sync.mjs`, which bumps only the three version-label fields
|
|
106
|
+
(`_version`, `_supportedMaxTested`, and the `claude-cli/<v>` token in the
|
|
107
|
+
user-agent header) — never the wire shape — patch-bumps `package.json`,
|
|
108
|
+
promotes the CHANGELOG, opens a `bot/template-label-*` PR, and turns on
|
|
109
|
+
**auto-merge**.
|
|
110
|
+
|
|
111
|
+
Auto-merge is safe for exit 3 but **not** for exit 2: an empty `computeDrift`
|
|
112
|
+
is a proof that the tools / system_prompt / beta headers / field orders are
|
|
113
|
+
byte-identical at the live version, so only the version string moves — the
|
|
114
|
+
same deterministic-bump risk class `cc-drift-watch.yml` already auto-merges for
|
|
115
|
+
`SUPPORTED_CC_RANGE.maxTested`. Auto-merge still gates on the required checks
|
|
116
|
+
(build ×3, compat, test, docker-cap-drop-smoke); a red check leaves the PR open
|
|
117
|
+
with the bot branch preserved. A shape rebake (exit 2) changes the wire-shape
|
|
118
|
+
contract, so a human reviews compat-test + the diff before merging.
|
|
119
|
+
|
|
120
|
+
## Setting up the self-hosted runner
|
|
121
|
+
|
|
122
|
+
Any dedicated Linux host works. Hetzner / DO / EC2 / etc. The runner needs
|
|
123
|
+
Node 22, a logged-in `claude` CLI, and disk for a clone of the repo (~200 MB
|
|
124
|
+
including `node_modules`).
|
|
125
|
+
|
|
126
|
+
### Prerequisites
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# 1. Node 22 + npm
|
|
130
|
+
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
|
|
131
|
+
sudo apt-get install -y nodejs
|
|
132
|
+
|
|
133
|
+
# 2. GitHub CLI (`gh`) — the workflow's issue-open step shells out to it.
|
|
134
|
+
# Without this, --check correctly detects drift but the "Open / update
|
|
135
|
+
# drift issue" step fails with `gh: command not found`.
|
|
136
|
+
sudo apt-get install -y gh # or follow https://github.com/cli/cli#installation
|
|
137
|
+
|
|
138
|
+
# 3. CC + dario CLI (dario provides the headless OAuth flow)
|
|
139
|
+
sudo npm i -g @anthropic-ai/claude-code @askalf/dario
|
|
140
|
+
|
|
141
|
+
# 4. OAuth — manual flow for headless boxes. Run from the host's shell,
|
|
142
|
+
# follow the printed URL in any browser, paste the post-login callback
|
|
143
|
+
# URL back into the SSH session.
|
|
144
|
+
#
|
|
145
|
+
# SHARE the credential with any other CC clients that auto-refresh
|
|
146
|
+
# (e.g. a platform dario container running 24/7). Just run the standard
|
|
147
|
+
# `dario login --manual` against /root/.claude/.credentials.json and let
|
|
148
|
+
# that long-running refresh authority keep the token fresh. The workflows
|
|
149
|
+
# read whatever's current at fire time and never attempt a refresh from
|
|
150
|
+
# the runner side.
|
|
151
|
+
#
|
|
152
|
+
# History: v4.4.1 isolated the runner's credential at /root/.claude-runner
|
|
153
|
+
# to avoid OAuth refresh-token rotation races between the runner and other
|
|
154
|
+
# CC clients on the host. The isolation worked for races but introduced a
|
|
155
|
+
# different failure: the isolated token had no refresh authority between
|
|
156
|
+
# workflow fires (each <10 min, often hours apart), and Anthropic invalidates
|
|
157
|
+
# refresh tokens that idle too long. Result: `invalid_grant` on every
|
|
158
|
+
# workflow fire, recoverable only via interactive `dario login --manual`.
|
|
159
|
+
#
|
|
160
|
+
# Sharing with a 24/7 refresh authority (typical setup: the docker-stack
|
|
161
|
+
# dario container) fixes that. The race the isolation was protecting
|
|
162
|
+
# against is rare in practice — platform dario refreshes proactively
|
|
163
|
+
# when the access token has ~1h life remaining, so workflow runs hit a
|
|
164
|
+
# fresh token and don't need to refresh themselves.
|
|
165
|
+
dario login --manual
|
|
166
|
+
|
|
167
|
+
# 5. Smoke test:
|
|
168
|
+
echo "Reply with PONG" | claude --print # should print PONG
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### One-time repo setup
|
|
172
|
+
|
|
173
|
+
The workflow's issue-open step calls `gh issue create --label cc-drift-template`
|
|
174
|
+
which fails if the label doesn't exist in the repo. Create it once before the
|
|
175
|
+
runner's first execution:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
gh label create cc-drift-template \
|
|
179
|
+
--description "Bundled CC template has drifted from live capture" \
|
|
180
|
+
--color FBCA04
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Register the runner
|
|
184
|
+
|
|
185
|
+
In a browser: `https://github.com/<owner>/<repo>/settings/actions/runners/new`.
|
|
186
|
+
Pick Linux x64. GitHub prints a `mkdir`/`curl`/`tar`/`./config.sh` snippet.
|
|
187
|
+
Paste it into the host. At the labels prompt, type **`dario-drift`** — the
|
|
188
|
+
workflow gates on `runs-on: [self-hosted, dario-drift]`, so the label is
|
|
189
|
+
load-bearing.
|
|
190
|
+
|
|
191
|
+
### Install as a systemd service
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
cd ~/actions-runner
|
|
195
|
+
sudo ./svc.sh install $(whoami) # run as the same user that owns ~/.claude
|
|
196
|
+
sudo ./svc.sh start
|
|
197
|
+
sudo ./svc.sh status # → "active (running)"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
If the runner runs as `root` and `~/.claude/.credentials.json` lives under
|
|
201
|
+
`/root/`, `RUNNER_ALLOW_RUNASROOT=1 ./config.sh ...` lets `./config.sh` run as
|
|
202
|
+
root; GitHub's runner otherwise refuses root by default.
|
|
203
|
+
|
|
204
|
+
### Trigger once to verify
|
|
205
|
+
|
|
206
|
+
GitHub UI → Actions → **CC template drift watch (self-hosted)** → "Run
|
|
207
|
+
workflow." It should pick up the labeled runner within seconds and finish
|
|
208
|
+
within ~60s. Exit 0 (no drift) or exit 2 (issue auto-opened with the drift
|
|
209
|
+
report) both mean the pipeline is healthy. Exit 1 means the capture broke;
|
|
210
|
+
check the workflow logs.
|
|
211
|
+
|
|
212
|
+
After the first successful run, the `*/30 * * * *` cron takes over.
|
|
213
|
+
|
|
214
|
+
## When --check fires an issue
|
|
215
|
+
|
|
216
|
+
The workflow opens (or comments on) an issue labeled `cc-drift-template`
|
|
217
|
+
containing the `[bake]` output — the list of differing slots, sizes, tool
|
|
218
|
+
names. From there:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# On a maintainer machine with CC + a logged-in OAuth credential:
|
|
222
|
+
npm run build
|
|
223
|
+
node scripts/capture-and-bake.mjs # rewrites src/cc-template-data.json
|
|
224
|
+
git diff src/cc-template-data.json # review
|
|
225
|
+
# Open a PR with the re-bake; the auto-release pipeline publishes a patch
|
|
226
|
+
# version. The next clean --check cycle auto-closes the drift issue.
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Optional: PAT for downstream workflow triggers
|
|
230
|
+
|
|
231
|
+
Since v4.4.0, the watcher auto-opens a `bot/template-rebake-*` PR on detection. Since v4.3.0, `compat-test-self-hosted.yml` is supposed to run on PRs touching `src/cc-template-data.json`. **Without the setup below, it doesn't.** GitHub Actions has a deliberate restriction: workflows authenticated by the default `GITHUB_TOKEN` cannot trigger downstream workflow runs ([docs](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow)). The auto-rebake PR is therefore invisible to compat-test, and the validation gate the v4.4.0 design promised is effectively bypassed.
|
|
232
|
+
|
|
233
|
+
To close the gap, create a fine-grained personal access token (PAT) scoped to this repo and expose it to the watcher as `DARIO_DRIFT_BOT_PAT`:
|
|
234
|
+
|
|
235
|
+
1. **Generate** at `https://github.com/settings/personal-access-tokens/new`:
|
|
236
|
+
- Resource owner: your user (or org)
|
|
237
|
+
- Repository access: select `dario` only
|
|
238
|
+
- Permissions: **Contents: read & write**, **Pull requests: read & write**, **Issues: read & write**
|
|
239
|
+
- Expiration: whatever your security policy mandates (90 days / 1 year)
|
|
240
|
+
|
|
241
|
+
2. **Store** at `Settings → Secrets and variables → Actions → New repository secret`:
|
|
242
|
+
- Name: `DARIO_DRIFT_BOT_PAT`
|
|
243
|
+
- Value: the PAT from step 1
|
|
244
|
+
|
|
245
|
+
3. **Verify** on the next watcher cycle that detects drift. The auto-rebake PR's "Checks" tab should now include the `compat` job (which it didn't pre-v4.6.5).
|
|
246
|
+
|
|
247
|
+
The watcher workflow uses `GH_TOKEN: ${{ secrets.DARIO_DRIFT_BOT_PAT || secrets.GITHUB_TOKEN }}` for `gh` CLI ops, so the PAT is **optional** — the watcher keeps working without it, you just don't get compat-test gating on auto-rebake PRs (same behavior as v4.4.0 through v4.6.4). The fallback exists so a maintainer can defer the PAT setup without breaking the loop.
|
|
248
|
+
|
|
249
|
+
## Runner credential rate-limit headroom
|
|
250
|
+
|
|
251
|
+
Workflows that exercise live `dario proxy` paths (compat-test, billing canary, future end-to-end probes) all consume against the runner credential's subscription pool. The cadence assumptions are:
|
|
252
|
+
|
|
253
|
+
| Workflow | Declared cadence | Observed cadence | Requests per fire |
|
|
254
|
+
|---|---|---|---|
|
|
255
|
+
| `cc-drift-template-watch.yml` (`--check`) | every 30 min (`*/30 * * * *`) | typically every 2-4h | 1 capture (no /v1/messages traffic — MITM-only) |
|
|
256
|
+
| `cc-billing-classifier-canary.yml` | daily 06:30 UTC | daily | 1 small haiku request |
|
|
257
|
+
| `compat-test-self-hosted.yml` | per qualifying PR | per qualifying PR | ~11 small requests |
|
|
258
|
+
|
|
259
|
+
**Cron scheduler reality.** GitHub Actions' free-tier cron scheduler is best-effort, not guaranteed. The class-B watcher declares `*/30 * * * *` but in practice GitHub honors it every 2-4 hours on this repo. The liveness alarm (added v4.4.2) has its threshold set to 8h (raised from 3h in v4.7.1) to absorb this skew — anything past that is signal, not scheduler noise. If you need a tighter SLA (sub-hour), self-host the runner *and* the cron driver (e.g. a cron entry on the same Hetzner box invoking `gh workflow run` directly).
|
|
260
|
+
|
|
261
|
+
At steady state, this is comfortably inside Pro/Max headroom. The failure mode to watch for is **batched firing** — manually re-triggering the same workflow several times in a single hour, or PRs landing in rapid succession that each fire compat-test. We tripped this during the v4.6.x rollout: a half-dozen manual re-runs in a 2-hour window 429'd the runner credential. Pro/Max accounts have per-hour rate caps as well as per-5h / per-7d pools, and the per-hour cap is what surfaces first.
|
|
262
|
+
|
|
263
|
+
If the runner credential is rate-limited and a workflow run reports 429s across the board, the right diagnosis order is: (a) check `claude --print` directly — if it 429s, the credential pool is dry, just wait an hour; (b) check the credential is still on a subscription account (`dario doctor`); (c) check workflow cadence assumptions haven't changed.
|
|
264
|
+
|
|
265
|
+
The runner shares its OAuth credential with any other long-running CC client on the box (typically the platform dario container, which auto-refreshes 24/7). Sharing is intentional: a workflow that fires sparsely cannot keep its own refresh token alive — Anthropic invalidates idle refresh tokens, and `invalid_grant` then breaks every subsequent run. Letting a 24/7 refresh authority own the token rotation eliminates that failure mode at the cost of competing for the same Pro/Max headroom. With current cadence (drift-template-watch every 30 min + compat per PR + canary daily), runner-side burn on the shared account is a manageable fraction of the headroom available on a Max plan; reducing cadence further is a knob if a particular workload needs more of it.
|
|
266
|
+
|
|
267
|
+
## Why a self-hosted runner
|
|
268
|
+
|
|
269
|
+
GitHub-hosted runners can't capture CC. They have no Pro/Max subscription
|
|
270
|
+
session, no MITM cert trust for CC's loopback proxy, no way to authenticate
|
|
271
|
+
against `claude.ai/oauth`. Anything that needs real CC running against real
|
|
272
|
+
Anthropic has to live on a host you control with an account you've logged
|
|
273
|
+
in to.
|
|
274
|
+
|
|
275
|
+
The runner is read-only against the repo (`contents: read`) and only writes
|
|
276
|
+
to issues (`issues: write`). It cannot push, tag, or release.
|
|
277
|
+
|
|
278
|
+
## Platform-superset preservation
|
|
279
|
+
|
|
280
|
+
CC ships different tools on different platforms — currently just `PowerShell`
|
|
281
|
+
on Windows, but the surface grows over time. The bundled template is meant to
|
|
282
|
+
be a **union** across platforms, and `filterToolsForPlatform()` strips it down
|
|
283
|
+
at request time. So a bake on Linux must not silently drop the Windows tool
|
|
284
|
+
set, or Windows dario users would lose those tools on the next release.
|
|
285
|
+
|
|
286
|
+
`scripts/capture-and-bake.mjs` preserves tools from the previous bundle whose
|
|
287
|
+
names are listed in `PLATFORM_ONLY_TOOLS` for a platform other than the
|
|
288
|
+
baking host's. The merged set is re-sorted alphabetically to match CC's wire
|
|
289
|
+
order. The runner can therefore bake from Linux without regressing Windows
|
|
290
|
+
users.
|