@tokenfactory/acc-runner 0.47.9 → 0.47.11

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.
Files changed (44) hide show
  1. package/dist/build-info.generated.d.ts +1 -1
  2. package/dist/build-info.generated.js +2 -2
  3. package/dist/companion-autostart.d.ts.map +1 -1
  4. package/dist/companion-autostart.js +5 -0
  5. package/dist/companion-autostart.js.map +1 -1
  6. package/dist/companion-run.d.ts.map +1 -1
  7. package/dist/companion-run.js +46 -18
  8. package/dist/companion-run.js.map +1 -1
  9. package/dist/doctor.d.ts +20 -0
  10. package/dist/doctor.d.ts.map +1 -1
  11. package/dist/doctor.js +187 -3
  12. package/dist/doctor.js.map +1 -1
  13. package/dist/keychain.d.ts +31 -0
  14. package/dist/keychain.d.ts.map +1 -1
  15. package/dist/keychain.js +70 -1
  16. package/dist/keychain.js.map +1 -1
  17. package/dist/program.d.ts.map +1 -1
  18. package/dist/program.js +23 -1
  19. package/dist/program.js.map +1 -1
  20. package/dist/runtime/auth-lifecycle.d.ts +22 -1
  21. package/dist/runtime/auth-lifecycle.d.ts.map +1 -1
  22. package/dist/runtime/auth-lifecycle.js +28 -1
  23. package/dist/runtime/auth-lifecycle.js.map +1 -1
  24. package/dist/runtime/refresh-lock.d.ts +232 -0
  25. package/dist/runtime/refresh-lock.d.ts.map +1 -0
  26. package/dist/runtime/refresh-lock.js +609 -0
  27. package/dist/runtime/refresh-lock.js.map +1 -0
  28. package/dist/supervisor.d.ts +46 -0
  29. package/dist/supervisor.d.ts.map +1 -1
  30. package/dist/supervisor.js +65 -0
  31. package/dist/supervisor.js.map +1 -1
  32. package/dist/task-runner.d.ts.map +1 -1
  33. package/dist/task-runner.js +16 -0
  34. package/dist/task-runner.js.map +1 -1
  35. package/dist/token-provider.d.ts +12 -0
  36. package/dist/token-provider.d.ts.map +1 -1
  37. package/dist/token-provider.js +23 -3
  38. package/dist/token-provider.js.map +1 -1
  39. package/dist/watch.d.ts +31 -13
  40. package/dist/watch.d.ts.map +1 -1
  41. package/dist/watch.js +180 -37
  42. package/dist/watch.js.map +1 -1
  43. package/package.json +1 -1
  44. package/scripts/setup-dedicated-runner-linux.sh +17 -35
@@ -0,0 +1,609 @@
1
+ /**
2
+ * RUNNER-REFRESH-ROTATION-LOST (ROTATED): coordinate a token refresh across
3
+ * every process on a host that shares ONE keychain slot.
4
+ *
5
+ * The first occurrence (a rotated successor never reaching the keychain) is
6
+ * handled by keychain.ts's verified save. This file handles the SECOND shape:
7
+ * two rotators — a `watch` and a `companion`, or two `watch` units — that both
8
+ * resolve the SAME instance keychain slot. When one rotates the refresh token
9
+ * the server revokes the predecessor; the OTHER process is still holding that
10
+ * predecessor in memory and, on its next refresh, re-presents it. The server's
11
+ * reuse detection then revokes the WHOLE family and both processes die.
12
+ *
13
+ * Two primitives fix it, both keyed to the keychain account (= the instance
14
+ * slot), so coordination is per-slot:
15
+ *
16
+ * 1. acquireRefreshLock() — an instance-scoped advisory lock with TWO layers,
17
+ * so it serializes rotators both within one process and across processes:
18
+ * - an in-process async mutex (keyed by the keychain account) that every
19
+ * same-process acquirer queues on FIRST, so at most one rotator in this
20
+ * process is ever between taking and releasing the lock; and
21
+ * - an O_EXCL lock FILE under the instance-namespaced cache dir that
22
+ * serializes rotators in DISTINCT processes (the `watch` and the
23
+ * `companion` are separate processes with distinct pids).
24
+ * Because the in-process mutex guarantees single-rotator access within the
25
+ * process, the file layer never races a LIVE same-pid holder — a lock file
26
+ * carrying our own pid is therefore always a stale leftover from THIS
27
+ * process (an acquire that could not unlink on release), safe to reclaim.
28
+ * A rotator takes the lock before refreshing so two rotators on one slot
29
+ * serialize instead of racing. It WAITS for a live cross-process holder
30
+ * (polling) and reclaims a dead holder's lock. It fails CLOSED: if the wait
31
+ * budget is exhausted while a live holder still owns it, it throws
32
+ * `RefreshLockBusyError` rather than proceeding lockless. An earlier build
33
+ * failed OPEN here on the theory that the adoption re-read below was a
34
+ * sufficient safety net; it is not — the holder may have called
35
+ * /api/runner/refresh but not yet SAVED the successor, so a lockless
36
+ * rotator re-reads its own (now superseded) token, re-presents it, and
37
+ * trips reuse detection. The HTTP refresh therefore only ever runs while
38
+ * this lock is HELD.
39
+ * 2. coordinatedRefresh() — under the lock, RE-READ the slot. If it now holds
40
+ * a session whose refresh token differs from the one we loaded, another
41
+ * rotator already rotated: ADOPT its successor rather than re-presenting
42
+ * our superseded token (which would trip reuse detection). Only when the
43
+ * slot still matches our token do we actually call /api/runner/refresh.
44
+ * If the lock could not be taken (a live sibling is mid-rotation), it makes
45
+ * no HTTP call: it either adopts a successor the sibling has already saved
46
+ * (safe — nothing is presented) or refuses, surfacing a transient failure
47
+ * the caller retries (by which point the sibling has saved and we adopt).
48
+ */
49
+ import fs from "node:fs";
50
+ import fsp from "node:fs/promises";
51
+ import path from "node:path";
52
+ import crypto from "node:crypto";
53
+ import chalk from "chalk";
54
+ import { cacheDir } from "./cache-dir.js";
55
+ import { isProcessAlive } from "./locks.js";
56
+ import { loadSessionFromAccount } from "../keychain.js";
57
+ import { expiresSoon, refreshAccessToken } from "./auth-lifecycle.js";
58
+ /**
59
+ * RUNNER-REFRESH-ROTATION-LOST: exit status for a TERMINAL refresh refusal
60
+ * (reuse / expired) or an unprovable keychain save. EX_CONFIG from sysexits(3):
61
+ * the config the process was handed is unusable and a restart would only
62
+ * re-present it, so BOTH the `watch` and `companion` supervisor units carry
63
+ * `RestartPreventExitStatus=78` and the process exits this code rather than
64
+ * letting the supervisor loop every minute and burn every token family it can
65
+ * reach. Defined here (not in watch.ts) so the companion can share it without
66
+ * importing the whole watch task-loop module. watch.ts re-exports it.
67
+ */
68
+ export const EX_CONFIG = 78;
69
+ /**
70
+ * RUNNER-REFRESH-ROTATION-LOST (ROTATED): print the operator-actionable banner
71
+ * for a TERMINAL refresh refusal and emit the runner-lane `auth.refresh_refused`
72
+ * board breadcrumb — AC3, carrying the `process` field so the board sees WHICH
73
+ * rotator (watch or companion) died on the shared slot. Shared verbatim by both
74
+ * rotators so the payload shape can never drift between them. Does NOT exit; the
75
+ * caller decides (both exit EX_CONFIG). The breadcrumb is emitted only while a
76
+ * usable bearer remains and is strictly best-effort — it never throws and never
77
+ * blocks the exit.
78
+ */
79
+ export async function emitRefreshRefused(opts) {
80
+ const write = opts.write ?? ((s) => void process.stderr.write(s));
81
+ const { session, instance, account, error } = opts;
82
+ write(chalk.red.bold(`\n[acc-runner] CRITICAL: refresh refused (${error.code}).\n`));
83
+ write(chalk.red(`${error.message}\n`));
84
+ if (error.code === "refresh_reuse_detected") {
85
+ write(chalk.red("If this machine is the legitimate owner, an attacker may have rotated your tokens.\n" +
86
+ "Rotate any other credentials that were on this box.\n"));
87
+ }
88
+ write(chalk.red(`Clear the stale keychain slot "${account}" and re-login: ` +
89
+ `acc-runner login${instance ? ` --instance ${instance}` : ""}\n` +
90
+ `The unit stays failed (exit ${EX_CONFIG}); it will NOT restart-loop.\n`));
91
+ // Emit the board breadcrumb ONLY while the current access token is still
92
+ // usable — a refusal reached after the bearer itself expired has no authority
93
+ // to call log_activity, and must not block the exit waiting on a 401.
94
+ if (!expiresSoon(session, 0)) {
95
+ try {
96
+ const client = opts.makeClient();
97
+ const call = Promise.resolve(client.rpc("log_activity", {
98
+ p_verb: "auth.refresh_refused",
99
+ p_target_id: session.runner_id,
100
+ p_target_type: "runner",
101
+ p_payload: {
102
+ runner_id: session.runner_id,
103
+ instance,
104
+ process: opts.process,
105
+ error: error.code,
106
+ keychain_account: account,
107
+ },
108
+ }));
109
+ await (opts.withDeadline ? opts.withDeadline(call, 5_000) : call);
110
+ }
111
+ catch {
112
+ /* best-effort breadcrumb — never let it hold up the terminal exit */
113
+ }
114
+ }
115
+ }
116
+ /**
117
+ * Thrown by acquireRefreshLock when the instance-scoped lock could not be taken
118
+ * within the wait budget because a LIVE sibling still holds it. It is NOT a
119
+ * terminal auth error — the caller must treat it as transient (refuse to rotate
120
+ * lockless, then retry), never exit on it.
121
+ */
122
+ /**
123
+ * The keychain slot could not be re-read under the held lock, so the cached
124
+ * refresh token cannot be proven current. No HTTP refresh is made; the caller
125
+ * treats this like a transient refresh failure and retries.
126
+ */
127
+ export class RefreshSlotUnreadableError extends Error {
128
+ account;
129
+ constructor(account, cause) {
130
+ super(`refresh skipped: keychain slot ${JSON.stringify(account)} could not be re-read ` +
131
+ `(${cause instanceof Error ? cause.message : String(cause)}); not presenting the cached token`);
132
+ this.name = "RefreshSlotUnreadableError";
133
+ this.account = account;
134
+ }
135
+ }
136
+ export class RefreshLockBusyError extends Error {
137
+ account;
138
+ code = "refresh_lock_busy";
139
+ constructor(account) {
140
+ super(`refresh lock for "${account}" is held by a live sibling and was not released ` +
141
+ `within the wait budget; refusing to rotate without it`);
142
+ this.account = account;
143
+ this.name = "RefreshLockBusyError";
144
+ }
145
+ }
146
+ /**
147
+ * `<cache>/refresh.<hash>.lock`. The hash is the keychain account (the instance
148
+ * slot), so every process resolving the same slot contends on one file while
149
+ * two distinct instances (distinct slots) never do.
150
+ */
151
+ export function refreshLockPath(account) {
152
+ const hash = crypto.createHash("sha1").update(account).digest("hex").slice(0, 12);
153
+ return path.join(cacheDir(), `refresh.${hash}.lock`);
154
+ }
155
+ /**
156
+ * How long an UNPARSEABLE lock file is presumed to be a live holder's before it
157
+ * may be reclaimed. The lock file is created atomically with its full payload
158
+ * (temp file + hard link, below), so an unreadable or partial file is never a
159
+ * holder mid-write on this build; it is either a crashed older build's leftover
160
+ * or filesystem damage. It is still never reclaimed while fresh: a reader that
161
+ * cannot name the holder's pid has no proof the holder is dead, and unlinking a
162
+ * live holder's lock is exactly the race this lock exists to prevent.
163
+ */
164
+ export const UNPARSEABLE_LOCK_STALE_MS = 30_000;
165
+ function readRefreshLock(file, now) {
166
+ let raw;
167
+ try {
168
+ raw = fs.readFileSync(file, "utf8");
169
+ }
170
+ catch (err) {
171
+ if (err.code === "ENOENT")
172
+ return { kind: "absent" };
173
+ throw err;
174
+ }
175
+ try {
176
+ const parsed = JSON.parse(raw);
177
+ if (typeof parsed.pid === "number" && Number.isFinite(parsed.pid)) {
178
+ return {
179
+ kind: "holder",
180
+ info: {
181
+ pid: parsed.pid,
182
+ account: typeof parsed.account === "string" ? parsed.account : "<unknown>",
183
+ acquiredAt: typeof parsed.acquiredAt === "string" ? parsed.acquiredAt : "<unknown>",
184
+ },
185
+ };
186
+ }
187
+ }
188
+ catch {
189
+ /* fall through: unparseable */
190
+ }
191
+ // Corrupt / partial / empty lock file. NOT reclaimable while fresh — see
192
+ // UNPARSEABLE_LOCK_STALE_MS. Its age comes from the file's mtime; an mtime
193
+ // that cannot be read counts as fresh (the conservative direction).
194
+ let ageMs = 0;
195
+ try {
196
+ ageMs = Math.max(0, now() - fs.statSync(file).mtimeMs);
197
+ }
198
+ catch (err) {
199
+ if (err.code === "ENOENT")
200
+ return { kind: "absent" };
201
+ }
202
+ return { kind: "unparseable", ageMs };
203
+ }
204
+ function readRefreshLockInfo(file) {
205
+ const read = readRefreshLock(file, Date.now);
206
+ return read.kind === "holder" ? read.info : null;
207
+ }
208
+ /**
209
+ * In-process serialization gate, keyed by keychain account. The O_EXCL lock FILE
210
+ * below serializes rotators ACROSS processes (distinct pids); this async mutex
211
+ * serializes rotators WITHIN one process. Without it, two same-process acquirers
212
+ * would each find their OWN pid on the lock file and — on the stale-reclaim path
213
+ * — the second could delete the first's LIVE lock and proceed in parallel
214
+ * instead of waiting, breaking single-rotator serialization. With it, a
215
+ * same-process acquirer queues here first, so only one is ever between
216
+ * file-create and release; a same-pid lock file the file layer encounters is
217
+ * then provably a stale leftover from THIS process, never a live concurrent
218
+ * holder, and is safe to reclaim.
219
+ *
220
+ * Implemented as a per-account promise chain: each acquirer awaits the previous
221
+ * tail, and its release resolves the gate for the next in line. The map entry is
222
+ * dropped once its queue has fully drained so it cannot grow unbounded.
223
+ */
224
+ const inProcessGates = new Map();
225
+ function acquireInProcessGate(account) {
226
+ const prev = inProcessGates.get(account) ?? Promise.resolve();
227
+ let release;
228
+ const held = new Promise((r) => {
229
+ release = r;
230
+ });
231
+ const tail = prev.then(() => held);
232
+ inProcessGates.set(account, tail);
233
+ // Hand control to the caller only after every earlier holder has released.
234
+ return prev.then(() => () => {
235
+ release();
236
+ // Drop the entry once we are the tail (queue drained) so long-lived
237
+ // processes rotating many accounts do not accumulate resolved promises.
238
+ if (inProcessGates.get(account) === tail)
239
+ inProcessGates.delete(account);
240
+ });
241
+ }
242
+ /**
243
+ * Acquire the instance-scoped advisory refresh lock for `account`. Two layers:
244
+ * an in-process mutex (same-process rotators queue) and an O_EXCL lock file
245
+ * (cross-process rotators contend). Blocks (polling) while a LIVE process holds
246
+ * the file and reclaims a DEAD holder's lock. If the wait budget is exhausted
247
+ * while a live holder still owns it, it fails CLOSED — throwing
248
+ * `RefreshLockBusyError` — because an HTTP refresh must never run without the
249
+ * lock held (a lockless rotator can re-present a superseded token mid-rotation
250
+ * and trip reuse detection). The caller treats the throw as transient and
251
+ * retries. The in-process gate is always released, including on a throw, so a
252
+ * failed acquire never wedges later same-process rotators.
253
+ */
254
+ export async function acquireRefreshLock(account, opts = {}) {
255
+ const releaseGate = await acquireInProcessGate(account);
256
+ try {
257
+ return await acquireFileLock(account, opts, releaseGate);
258
+ }
259
+ catch (err) {
260
+ releaseGate();
261
+ throw err;
262
+ }
263
+ }
264
+ /**
265
+ * The cross-process half: the O_EXCL lock file. Only ever entered while the
266
+ * in-process gate is held, so a same-pid lock file here is a stale leftover from
267
+ * this process (safe to reclaim), never a live concurrent same-process holder.
268
+ */
269
+ async function acquireFileLock(account, opts, releaseGate) {
270
+ const file = refreshLockPath(account);
271
+ const pid = opts.pid ?? process.pid;
272
+ const waitMs = opts.waitMs ?? 10_000;
273
+ const pollMs = opts.pollMs ?? 100;
274
+ const now = opts.now ?? Date.now;
275
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
276
+ // Synchronous on the uncontended happy path (no await) so taking the lock
277
+ // adds no extra microtask/IO turn to the refresh — it only awaits when it
278
+ // genuinely has to WAIT for a live holder.
279
+ fs.mkdirSync(path.dirname(file), { recursive: true });
280
+ const payload = `${JSON.stringify({
281
+ pid,
282
+ account,
283
+ acquiredAt: new Date().toISOString(),
284
+ })}\n`;
285
+ const deadline = now() + waitMs;
286
+ // The lock file appears with its FULL payload or not at all: the payload is
287
+ // written to a private temp file first and then hard-linked to the lock path.
288
+ // link(2) fails with EEXIST when the lock exists, giving O_EXCL semantics,
289
+ // and a sibling that reads the lock can never observe it empty or partial
290
+ // (an `openSync("wx")` + `writeSync` pair had exactly that window, and a
291
+ // sibling reading the empty file in it treated the live lock as corrupt and
292
+ // reclaimed it — the race the reviewer found on 84c75503).
293
+ const tmp = `${file}.${pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
294
+ for (;;) {
295
+ fs.writeFileSync(tmp, payload, { flag: "w" });
296
+ try {
297
+ fs.linkSync(tmp, file);
298
+ return makeRefreshLock(file, pid, releaseGate);
299
+ }
300
+ catch (err) {
301
+ if (err.code !== "EEXIST")
302
+ throw err;
303
+ const read = readRefreshLock(file, now);
304
+ const reclaimable = read.kind === "absent" ||
305
+ (read.kind === "holder" && (read.info.pid === pid || !isProcessAlive(read.info.pid))) ||
306
+ (read.kind === "unparseable" && read.ageMs >= UNPARSEABLE_LOCK_STALE_MS);
307
+ if (reclaimable) {
308
+ // Reclaimable: a dead cross-process holder, OUR OWN pid, an already
309
+ // vanished file, or an unparseable file that has sat untouched for
310
+ // UNPARSEABLE_LOCK_STALE_MS. Our own pid can only be a stale leftover —
311
+ // the in-process gate above guarantees no other same-process rotator is
312
+ // holding the lock right now — so reclaiming it unwedges this process
313
+ // rather than stomping a live sibling. A live cross-process sibling
314
+ // carries a DIFFERENT pid, and a FRESH unparseable file is treated as a
315
+ // live holder too; both fall through to the wait below.
316
+ try {
317
+ fs.unlinkSync(file);
318
+ }
319
+ catch (unlinkErr) {
320
+ if (unlinkErr.code !== "ENOENT")
321
+ throw unlinkErr;
322
+ }
323
+ continue;
324
+ }
325
+ if (now() >= deadline)
326
+ throw new RefreshLockBusyError(account);
327
+ await sleep(pollMs);
328
+ }
329
+ finally {
330
+ try {
331
+ fs.unlinkSync(tmp);
332
+ }
333
+ catch {
334
+ /* already gone */
335
+ }
336
+ }
337
+ }
338
+ }
339
+ function makeRefreshLock(file, pid, releaseGate) {
340
+ let released = false;
341
+ return {
342
+ held: true,
343
+ async release() {
344
+ // Idempotent: callers may release more than once; the gate is freed once.
345
+ if (released)
346
+ return;
347
+ released = true;
348
+ try {
349
+ const holder = readRefreshLockInfo(file);
350
+ if (holder && holder.pid === pid)
351
+ await fsp.unlink(file);
352
+ }
353
+ catch (err) {
354
+ if (err.code !== "ENOENT")
355
+ throw err;
356
+ }
357
+ finally {
358
+ // Always hand the in-process gate to the next same-process rotator, even
359
+ // if unlinking the file failed, so a stuck unlink cannot wedge the queue.
360
+ releaseGate();
361
+ }
362
+ },
363
+ };
364
+ }
365
+ /**
366
+ * RUNNER-REFRESH-ROTATION-LOST (ROTATED): refresh the session the rotation-safe way.
367
+ *
368
+ * - Env-token providers have no shared keychain slot (boundAccount() === null)
369
+ * so they just refresh in place — there is no sibling to race.
370
+ * - Keychain providers take the instance-scoped lock, then RE-READ the slot. A
371
+ * refresh token in the slot that differs from ours means a sibling rotator
372
+ * already rotated while we held our (now superseded) token — adopt its
373
+ * successor instead of re-presenting ours. Only when the slot still matches
374
+ * do we call /api/runner/refresh ourselves and save the successor.
375
+ */
376
+ export async function coordinatedRefresh(deps) {
377
+ const refresh = deps.refresh ?? refreshAccessToken;
378
+ const account = deps.provider.boundAccount();
379
+ if (account === null) {
380
+ const updated = await refresh(deps.cfg, deps.session);
381
+ await deps.provider.save(updated);
382
+ return { session: updated, adopted: false, refreshed: true };
383
+ }
384
+ const acquire = deps.acquireLock ?? acquireRefreshLock;
385
+ const load = deps.loadFromAccount ?? loadSessionFromAccount;
386
+ let lock;
387
+ try {
388
+ lock = await acquire(account);
389
+ }
390
+ catch (err) {
391
+ if (!(err instanceof RefreshLockBusyError))
392
+ throw err;
393
+ // We could NOT take the instance-scoped lock within the wait budget — a live
394
+ // sibling is mid-rotation. Rotating ourselves now (presenting our token, or
395
+ // even refreshing from a near-expired sibling successor) without the lock
396
+ // can re-trip reuse detection, so make NO HTTP call. The one safe move is to
397
+ // re-read the slot and ADOPT a successor the sibling has already saved
398
+ // (nothing is presented). Otherwise re-throw: the caller surfaces this as a
399
+ // transient refresh failure and retries, by which point the holder has saved
400
+ // and we adopt on the next pass.
401
+ const current = await load(account).catch(() => null);
402
+ if (current?.refresh_token &&
403
+ current.refresh_token !== deps.session.refresh_token &&
404
+ !expiresSoon(current)) {
405
+ return { session: current, adopted: true, refreshed: false };
406
+ }
407
+ throw err;
408
+ }
409
+ try {
410
+ // The slot re-read is the proof that our cached token is still the slot's
411
+ // current one. If the keychain cannot be read, that proof is missing, and
412
+ // presenting the cached token anyway is exactly the superseded-token
413
+ // re-presentation this function exists to prevent — so NO HTTP call: fail
414
+ // this attempt (the caller treats it as transient and retries).
415
+ let current;
416
+ try {
417
+ current = await load(account);
418
+ }
419
+ catch (err) {
420
+ throw new RefreshSlotUnreadableError(account, err);
421
+ }
422
+ if (current && current.refresh_token && current.refresh_token !== deps.session.refresh_token) {
423
+ // A sibling rotator already rotated the slot. Its successor is the only
424
+ // credential the server will still accept; ours was revoked when it
425
+ // rotated. Adopt it if it is still a usable bearer; otherwise refresh
426
+ // FROM it (the freshest refresh token), never from our superseded one.
427
+ // Both branches run under the held lock.
428
+ if (!expiresSoon(current)) {
429
+ return { session: current, adopted: true, refreshed: false };
430
+ }
431
+ const updated = await refresh(deps.cfg, current);
432
+ await deps.provider.save(updated);
433
+ return { session: updated, adopted: false, refreshed: true };
434
+ }
435
+ const updated = await refresh(deps.cfg, deps.session);
436
+ await deps.provider.save(updated);
437
+ return { session: updated, adopted: false, refreshed: true };
438
+ }
439
+ finally {
440
+ await lock.release();
441
+ }
442
+ }
443
+ /** Map an instance name (or null for the unnamed default) to its keychain slot. */
444
+ export function rotatorSlot(instance) {
445
+ return instance ? `instance:${instance}` : "default";
446
+ }
447
+ /**
448
+ * pgrep-style scan for other `acc-runner watch` / `acc-runner companion`
449
+ * processes, extracting the `--instance <name>` each was launched with. Offline
450
+ * of any lock file and best-effort (returns [] on any failure). It reads only
451
+ * the command LINE (`ps -axo args`), never a process's environment — reading
452
+ * /proc/<pid>/environ is banned (it would leak secrets per CLAUDE.md).
453
+ *
454
+ * The process scan only sees RUNNING rotators, and only the slot that appears as
455
+ * `--instance` in argv — a stopped/failed unit, or one that carries its slot in
456
+ * `ACC_RUNNER_INSTANCE` rather than argv, is invisible here. `listRefreshRotatorUnits`
457
+ * below covers both gaps from the read-only systemd unit config.
458
+ */
459
+ export async function listRefreshRotators(selfPid = process.pid) {
460
+ try {
461
+ const { execa } = await import("execa");
462
+ const { stdout } = await execa("ps", ["-axo", "pid=,args="]);
463
+ const rotators = [];
464
+ for (const line of stdout.split(/\r?\n/)) {
465
+ const m = line.trim().match(/^(\d+)\s+(.*)$/);
466
+ if (!m)
467
+ continue;
468
+ const pid = Number.parseInt(m[1], 10);
469
+ const args = m[2];
470
+ if (pid === selfPid)
471
+ continue;
472
+ if (!/acc-runner\b/.test(args) && !/\bacc-runner-(watch|companion)\b/.test(args))
473
+ continue;
474
+ const kind = /\bwatch\b/.test(args)
475
+ ? "watch"
476
+ : /\bcompanion\b/.test(args)
477
+ ? "companion"
478
+ : null;
479
+ if (!kind)
480
+ continue;
481
+ const instMatch = args.match(/--instance[=\s]+([a-z0-9][a-z0-9-]{0,31})/);
482
+ rotators.push({ pid, kind, instance: instMatch ? instMatch[1] : null });
483
+ }
484
+ return rotators;
485
+ }
486
+ catch {
487
+ return [];
488
+ }
489
+ }
490
+ /**
491
+ * Instance name carried in a unit basename: `acc-runner-<name>.service` or the
492
+ * template form `acc-runner@<name>.service` (the impl1–4 lanes). Null for any
493
+ * other name — a companion unit's name (`acc-staging-companion.service`) says
494
+ * nothing about its slot; its environment does.
495
+ */
496
+ export function instanceFromUnitName(unit) {
497
+ const m = unit.match(/^acc-runner[-@]([a-z0-9][a-z0-9-]{0,31})\.service$/);
498
+ return m ? m[1] : null;
499
+ }
500
+ /**
501
+ * Classify one systemd unit as a rotator from what it RUNS and what its
502
+ * environment says, never from its name alone. A unit is a rotator when its
503
+ * ExecStart runs the runner binary (`acc-runner` / `acc`) as `watch` or
504
+ * `companion`, or when its environment carries `ACC_RUNNER_INSTANCE` (a unit
505
+ * that pins a slot refreshes it). Anything else — `acc-keyring.service`, a
506
+ * backup timer, an unrelated service that merely has "acc" in its name — is
507
+ * null. This is the rule the reviewer asked for on 91c768ad: the earlier
508
+ * name filter (`/acc-runner/`) skipped `acc-staging-companion.service`, the
509
+ * very unit that burnt the staging-smoke family (ledger 66).
510
+ */
511
+ export function classifyRotatorUnit(unit, props) {
512
+ const exec = props.ExecStart ?? "";
513
+ const env = props.Environment ?? "";
514
+ const runsRunner = /(^|[\s/=])(acc-runner|acc)(\s|$)/.test(exec);
515
+ const kind = runsRunner && /\bcompanion\b/.test(exec)
516
+ ? "companion"
517
+ : runsRunner && /\bwatch\b/.test(exec)
518
+ ? "watch"
519
+ : runsRunner
520
+ ? "unknown"
521
+ : null;
522
+ const envMatch = env.match(/\bACC_RUNNER_INSTANCE=([a-z0-9][a-z0-9-]{0,31})/);
523
+ if (kind === null && !envMatch)
524
+ return null;
525
+ // Slot precedence: the unit's ACC_RUNNER_INSTANCE env (authoritative, what
526
+ // instance.ts resolves), then an argv --instance, then the unit basename.
527
+ const argvMatch = exec.match(/--instance[=\s]+([a-z0-9][a-z0-9-]{0,31})/);
528
+ const instance = envMatch ? envMatch[1] : argvMatch ? argvMatch[1] : instanceFromUnitName(unit);
529
+ const mainPidRaw = Number.parseInt(props.MainPID ?? "0", 10);
530
+ return {
531
+ unit,
532
+ kind: kind ?? (/companion/.test(unit) ? "companion" : "unknown"),
533
+ instance,
534
+ state: props.ActiveState ?? "unknown",
535
+ mainPid: Number.isFinite(mainPidRaw) && mainPidRaw > 0 ? mainPidRaw : null,
536
+ };
537
+ }
538
+ /** Parse `Key=Value` lines from `systemctl show` into a flat map. */
539
+ function parseSystemctlShow(stdout) {
540
+ const out = {};
541
+ for (const line of stdout.split(/\r?\n/)) {
542
+ const eq = line.indexOf("=");
543
+ if (eq <= 0)
544
+ continue;
545
+ out[line.slice(0, eq)] = line.slice(eq + 1);
546
+ }
547
+ return out;
548
+ }
549
+ /**
550
+ * RUNNER-REFRESH-ROTATION-LOST (ROTATED, AC4): enumerate acc-runner rotator
551
+ * units from systemd --user CONFIG, INCLUDING stopped/failed ones, and resolve
552
+ * the keychain slot each targets from its READ-ONLY unit environment rather than
553
+ * a running process's argv. This closes the two gaps in the process scan:
554
+ *
555
+ * - A stopped or failed unit never appears in `ps`, yet it shares the slot it
556
+ * will rotate the instant it (re)starts — so doctor must still count it.
557
+ * - A unit that carries its slot in `Environment=ACC_RUNNER_INSTANCE=<name>`
558
+ * (not in argv) would be misread as the default slot by the argv-only scan.
559
+ * We read it from `systemctl show --property=Environment`, extracting ONLY
560
+ * `ACC_RUNNER_INSTANCE` and discarding the rest of the string, so no other
561
+ * (possibly secret-bearing) unit variable is ever surfaced. This reads the
562
+ * static unit config — NOT a live process's /proc/<pid>/environ, which stays
563
+ * banned per CLAUDE.md.
564
+ *
565
+ * Best-effort: returns [] on any failure (e.g. no systemd, non-Linux).
566
+ */
567
+ export async function listRefreshRotatorUnits() {
568
+ try {
569
+ const { execa } = await import("execa");
570
+ // `list-units --all` includes loaded units in every state — active, failed,
571
+ // and inactive — so a stopped/failed rotator is enumerated, not skipped.
572
+ const { stdout: list } = await execa("systemctl", [
573
+ "--user",
574
+ "list-units",
575
+ "--all",
576
+ "--no-legend",
577
+ "--plain",
578
+ "--type=service",
579
+ ]);
580
+ // Candidate units: every service whose name mentions acc at all (watch
581
+ // units, template lanes, companions, keyring, …). Which of them are rotators
582
+ // is decided by classifyRotatorUnit from ExecStart/Environment, not here.
583
+ const units = list
584
+ .split(/\r?\n/)
585
+ .map((l) => l.trim().split(/\s+/)[0] ?? "")
586
+ .filter((u) => /\.service$/.test(u) && /acc/i.test(u));
587
+ const out = [];
588
+ for (const unit of units) {
589
+ const { stdout } = await execa("systemctl", [
590
+ "--user",
591
+ "show",
592
+ unit,
593
+ "--property=Environment",
594
+ "--property=ExecStart",
595
+ "--property=ActiveState",
596
+ "--property=MainPID",
597
+ ]);
598
+ const props = parseSystemctlShow(stdout);
599
+ const classified = classifyRotatorUnit(unit, props);
600
+ if (classified)
601
+ out.push(classified);
602
+ }
603
+ return out;
604
+ }
605
+ catch {
606
+ return [];
607
+ }
608
+ }
609
+ //# sourceMappingURL=refresh-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh-lock.js","sourceRoot":"","sources":["../../src/runtime/refresh-lock.ts"],"names":[],"mappings":"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"}