codex-multi-auth 2.8.4 → 2.8.5
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/.codex-plugin/plugin.json +1 -1
- package/README.md +4 -4
- package/dist/lib/auth/auth.d.ts.map +1 -1
- package/dist/lib/auth/auth.js +8 -3
- package/dist/lib/codex-manager/commands/rotation.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/rotation.js +43 -20
- package/dist/lib/runtime/app-bind.d.ts +7 -0
- package/dist/lib/runtime/app-bind.d.ts.map +1 -1
- package/dist/lib/runtime/app-bind.js +174 -57
- package/dist/lib/runtime/app-helper-selection.d.ts +69 -0
- package/dist/lib/runtime/app-helper-selection.d.ts.map +1 -0
- package/dist/lib/runtime/app-helper-selection.js +100 -0
- package/dist/lib/runtime/rotation-server-types.d.ts +7 -0
- package/dist/lib/runtime/rotation-server-types.d.ts.map +1 -1
- package/dist/lib/runtime/runtime-current-account.d.ts +3 -2
- package/dist/lib/runtime/runtime-current-account.d.ts.map +1 -1
- package/dist/lib/runtime/runtime-current-account.js +34 -26
- package/dist/lib/runtime-constants.d.ts +30 -0
- package/dist/lib/runtime-constants.d.ts.map +1 -1
- package/dist/lib/runtime-constants.js +53 -0
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +4 -0
- package/package.json +1 -2
- package/scripts/codex.js +601 -49
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.5",
|
|
4
4
|
"description": "Install and operate codex-multi-auth for the official @openai/codex CLI with multi-account OAuth rotation, switching, health checks, and recovery tools.",
|
|
5
5
|
"interface": {
|
|
6
6
|
"composerIcon": "./assets/codex-multi-auth-icon.svg"
|
package/README.md
CHANGED
|
@@ -257,7 +257,7 @@ For remote or headless shells, prefer `codex-multi-auth login --device-auth`.
|
|
|
257
257
|
| Routing profiles | `~/.codex/multi-auth/routing-profiles.json` |
|
|
258
258
|
| Budget guards | `~/.codex/multi-auth/budget-guards.json` |
|
|
259
259
|
| Local client tokens | `~/.codex/multi-auth/local-client-tokens.json` |
|
|
260
|
-
| Runtime app helper status | `~/.codex/multi-auth/runtime-rotation-app-helper
|
|
260
|
+
| Runtime app helper status | `~/.codex/multi-auth/runtime-rotation-app-helper.<pid>.json` |
|
|
261
261
|
| Runtime app helper owner metadata | `~/.codex/multi-auth/runtime-rotation-app-helper-owner.<pid>.json` |
|
|
262
262
|
| Persistent app bind state/logs | `~/.codex/multi-auth/app-bind/` |
|
|
263
263
|
| Logs | `~/.codex/multi-auth/logs/codex-plugin/` |
|
|
@@ -392,9 +392,9 @@ codex-multi-auth doctor --json
|
|
|
392
392
|
|
|
393
393
|
## Release Notes
|
|
394
394
|
|
|
395
|
-
- Current stable: [docs/releases/v2.8.
|
|
396
|
-
- Previous stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md)
|
|
397
|
-
- Previous stable: [docs/releases/v2.8.2.md](docs/releases/v2.8.2.md)
|
|
395
|
+
- Current stable: [docs/releases/v2.8.5.md](docs/releases/v2.8.5.md) — install via `npm i -g codex-multi-auth`
|
|
396
|
+
- Previous stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md)
|
|
397
|
+
- Previous stable: [docs/releases/v2.8.2.md](docs/releases/v2.8.2.md)
|
|
398
398
|
- Previous stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md)
|
|
399
399
|
- Previous stable: [docs/releases/v2.7.0.md](docs/releases/v2.7.0.md)
|
|
400
400
|
- Previous stable: [docs/releases/v2.6.0.md](docs/releases/v2.6.0.md)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../lib/auth/auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../lib/auth/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEX,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,UAAU,EACV,MAAM,aAAa,CAAC;AAMrB,eAAO,MAAM,SAAS,iCAAiC,CAAC;AACxD,eAAO,MAAM,aAAa,4CAA4C,CAAC;AACvE,eAAO,MAAM,SAAS,wCAAwC,CAAC;AAC/D,eAAO,MAAM,YAAY,wCAAwC,CAAC;AAClE,eAAO,MAAM,KAAK,wCAAwC,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa;;;;;;EAYzB,CAAC;AA0EF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAyBvE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAY3D;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,CAoCtE;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC9C,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,GAAE,MAAqB,GAChC,OAAO,CAAC,WAAW,CAAC,CAwDtB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAe1D;AAED;;;;GAIG;AACH,KAAK,yBAAyB,GAAG;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAEF,wBAAsB,kBAAkB,CACvC,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,yBAA8B,GACrC,OAAO,CAAC,WAAW,CAAC,CAsEtB;AAED,MAAM,WAAW,wBAAwB;IACxC;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAYD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC5C,OAAO,CAAC,EAAE,wBAAwB,GAChC,OAAO,CAAC,iBAAiB,CAAC,CAuB5B"}
|
package/dist/lib/auth/auth.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { randomBytes, webcrypto } from "node:crypto";
|
|
3
2
|
import { logError } from "../logger.js";
|
|
4
3
|
import { safeParseOAuthTokenResponse } from "../schemas.js";
|
|
5
4
|
import { isAbortError } from "../utils.js";
|
|
@@ -340,13 +339,19 @@ export async function refreshAccessToken(refreshToken, options = {}) {
|
|
|
340
339
|
return { type: "failed", reason: "network_error", message: err?.message };
|
|
341
340
|
}
|
|
342
341
|
}
|
|
342
|
+
async function generatePKCE() {
|
|
343
|
+
const verifier = randomBytes(64).toString("base64url");
|
|
344
|
+
const digest = await webcrypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
345
|
+
const challenge = Buffer.from(digest).toString("base64url");
|
|
346
|
+
return { verifier, challenge };
|
|
347
|
+
}
|
|
343
348
|
/**
|
|
344
349
|
* Create OAuth authorization flow
|
|
345
350
|
* @param options - Optional configuration for the flow
|
|
346
351
|
* @returns Authorization flow details
|
|
347
352
|
*/
|
|
348
353
|
export async function createAuthorizationFlow(options) {
|
|
349
|
-
const pkce =
|
|
354
|
+
const pkce = await generatePKCE();
|
|
350
355
|
const state = createState();
|
|
351
356
|
const url = new URL(AUTHORIZE_URL);
|
|
352
357
|
url.searchParams.set("response_type", "code");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rotation.d.ts","sourceRoot":"","sources":["../../../../lib/codex-manager/commands/rotation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rotation.d.ts","sourceRoot":"","sources":["../../../../lib/codex-manager/commands/rotation.ts"],"names":[],"mappings":"AA2BA,OAAO,EAEN,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,2BAA2B,CAAC;AAMnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAEN,KAAK,4BAA4B,EACjC,MAAM,wCAAwC,CAAC;AAahD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAqB,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAG5E,KAAK,aAAa,GAAG,gBAAgB,GAAG,IAAI,CAAC;AAoB7C,MAAM,WAAW,mBAAmB;IACnC,gBAAgB,EAAE,MAAM,YAAY,CAAC;IACrC,gBAAgB,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,4BAA4B,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,OAAO,CAAC;IAChE,YAAY,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC3C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,kBAAkB,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CAAC;IAC1D,cAAc,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACpC,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9C,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5C,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC9C,qBAAqB,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IACrD,gCAAgC,CAAC,EAAE,MAAM,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC,CAAC;IACtF,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACtD,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AA4pBD,wBAAsB,kBAAkB,CACvC,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,mBAAmB,GACvB,OAAO,CAAC,MAAM,CAAC,CAkFjB"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
2
|
import { AccountManager, formatAccountLabel, formatCooldown, formatWaitTime, } from "../../accounts.js";
|
|
4
3
|
import { parseBooleanEnv } from "../../env-parsing.js";
|
|
5
4
|
import { getCodexMultiAuthDir } from "../../runtime-paths.js";
|
|
@@ -18,10 +17,11 @@ function redactedResetRateLimitsLabel(account, index) {
|
|
|
18
17
|
return `account ${index + 1} (id:${tail})`;
|
|
19
18
|
}
|
|
20
19
|
import { formatAppBindStatus, } from "../../runtime/app-bind.js";
|
|
21
|
-
import {
|
|
20
|
+
import { listRuntimeHelperStatusPaths } from "../../runtime-constants.js";
|
|
22
21
|
import { findQuotaCacheEntryForAccount, isQuotaCacheEntryExhausted, } from "../../quota-readiness.js";
|
|
23
22
|
import { recordRuntimeReset, } from "../../runtime/runtime-observability.js";
|
|
24
23
|
import { appRuntimeHelperStatusToSignal as appRuntimeHelperStatusToRuntimeSignal, resolveAccountCurrentMarkers, resolveRuntimeCurrentAccount, } from "../../runtime/runtime-current-account.js";
|
|
24
|
+
import { isLiveRuntimeHelper, liveRuntimeHelpers, readRuntimeHelperPid, selectRuntimeHelperStatus, } from "../../runtime/app-helper-selection.js";
|
|
25
25
|
import { isRateLimitedMarker } from "../rate-limit-markers.js";
|
|
26
26
|
import { isRecord } from "../../utils.js";
|
|
27
27
|
function printRotationUsage(logInfo) {
|
|
@@ -402,8 +402,7 @@ function readOptionalString(record, key) {
|
|
|
402
402
|
: null;
|
|
403
403
|
}
|
|
404
404
|
const MAX_STATUS_FILE_BYTES = 1024 * 1024; // 1 MB sanity cap
|
|
405
|
-
function
|
|
406
|
-
const statusPath = join(getCodexMultiAuthDir(), APP_RUNTIME_HELPER_STATUS_FILE);
|
|
405
|
+
function readAppRuntimeHelperStatusFile(statusPath) {
|
|
407
406
|
if (!existsSync(statusPath))
|
|
408
407
|
return null;
|
|
409
408
|
try {
|
|
@@ -421,7 +420,8 @@ function readAppRuntimeHelperStatus() {
|
|
|
421
420
|
return {
|
|
422
421
|
state: readOptionalString(parsed, "state"),
|
|
423
422
|
kind: readOptionalString(parsed, "kind"),
|
|
424
|
-
pid:
|
|
423
|
+
pid: readRuntimeHelperPid(parsed.pid),
|
|
424
|
+
startedAt: readOptionalNumber(parsed, "startedAt"),
|
|
425
425
|
idleExpiresAt: readOptionalNumber(parsed, "idleExpiresAt"),
|
|
426
426
|
totalRequests: readOptionalNumber(parsed, "totalRequests"),
|
|
427
427
|
rotations: readOptionalNumber(parsed, "rotations"),
|
|
@@ -437,17 +437,24 @@ function readAppRuntimeHelperStatus() {
|
|
|
437
437
|
return null;
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
440
|
+
// One directory scan feeds both the status line and the live-helper count so
|
|
441
|
+
// the two cannot observe different moments; path discovery is shared with
|
|
442
|
+
// every other reader via listRuntimeHelperStatusPaths in runtime-constants.
|
|
443
|
+
function readAppRuntimeHelperStatuses() {
|
|
444
|
+
const multiAuthDir = getCodexMultiAuthDir();
|
|
445
|
+
let entries = [];
|
|
443
446
|
try {
|
|
444
|
-
|
|
445
|
-
return true;
|
|
447
|
+
entries = readdirSync(multiAuthDir);
|
|
446
448
|
}
|
|
447
|
-
catch
|
|
448
|
-
|
|
449
|
-
return code === "EPERM";
|
|
449
|
+
catch {
|
|
450
|
+
entries = [];
|
|
450
451
|
}
|
|
452
|
+
return listRuntimeHelperStatusPaths(multiAuthDir, entries)
|
|
453
|
+
.map(readAppRuntimeHelperStatusFile)
|
|
454
|
+
.filter((status) => status !== null && status.kind === "codex-app-runtime-rotation-helper");
|
|
455
|
+
}
|
|
456
|
+
function readAppRuntimeHelperStatus(now = Date.now()) {
|
|
457
|
+
return selectRuntimeHelperStatus(readAppRuntimeHelperStatuses(), now);
|
|
451
458
|
}
|
|
452
459
|
function formatHelperLastAccount(status) {
|
|
453
460
|
if (status.lastAccountLabel && !status.lastAccountLabel.includes("@")) {
|
|
@@ -463,14 +470,19 @@ function formatHelperLastAccount(status) {
|
|
|
463
470
|
}
|
|
464
471
|
return null;
|
|
465
472
|
}
|
|
466
|
-
function formatAppRuntimeHelperStatus(now, status = readAppRuntimeHelperStatus()) {
|
|
473
|
+
function formatAppRuntimeHelperStatus(now, status = readAppRuntimeHelperStatus(now), liveHelperCount = status ? 1 : 0) {
|
|
467
474
|
if (!status)
|
|
468
475
|
return "Codex app helper: not running";
|
|
469
476
|
if (status.kind !== "codex-app-runtime-rotation-helper") {
|
|
470
477
|
return "Codex app helper: not running";
|
|
471
478
|
}
|
|
472
|
-
|
|
473
|
-
|
|
479
|
+
// Only "running" is running: "stopped", "idle-timeout", "max-lifetime",
|
|
480
|
+
// "owner-gone", "error", and anything a future helper invents are all
|
|
481
|
+
// terminal, and a live kill(pid, 0) on a terminal record proves nothing —
|
|
482
|
+
// the PID may be recycled, which is the exact gate this fix stopped
|
|
483
|
+
// trusting. `isLiveRuntimeHelper` folds both conditions together, and is
|
|
484
|
+
// the same predicate the selection above and the account marker below use.
|
|
485
|
+
if (!isLiveRuntimeHelper(status, now)) {
|
|
474
486
|
return "Codex app helper: not running";
|
|
475
487
|
}
|
|
476
488
|
const parts = [`running${status.pid ? ` pid=${status.pid}` : ""}`];
|
|
@@ -484,6 +496,12 @@ function formatAppRuntimeHelperStatus(now, status = readAppRuntimeHelperStatus()
|
|
|
484
496
|
if (status.idleExpiresAt !== null && status.idleExpiresAt > now) {
|
|
485
497
|
parts.push(`idle-expires=${formatWaitTime(status.idleExpiresAt - now)}`);
|
|
486
498
|
}
|
|
499
|
+
// The line shows the most recently active helper; with per-account
|
|
500
|
+
// app-servers several can be live at once, so say so instead of implying
|
|
501
|
+
// this one is the only one.
|
|
502
|
+
if (liveHelperCount > 1) {
|
|
503
|
+
parts.push(`(+${liveHelperCount - 1} more running)`);
|
|
504
|
+
}
|
|
487
505
|
return `Codex app helper: ${parts.join(", ")}`;
|
|
488
506
|
}
|
|
489
507
|
function shouldAutoBindCodexApp(env = process.env) {
|
|
@@ -532,8 +550,13 @@ async function printRotationStatus(deps) {
|
|
|
532
550
|
logInfo(`Runtime rotation proxy: ${enabled ? "enabled" : "disabled"}`);
|
|
533
551
|
logInfo(`Stored setting: ${config.codexRuntimeRotationProxy === true ? "enabled" : "disabled"}`);
|
|
534
552
|
logInfo(`Env override: ${formatEnvOverride()}`);
|
|
535
|
-
|
|
536
|
-
|
|
553
|
+
// One scan and one selection feed the status line, the live count and the
|
|
554
|
+
// account marker below. Selecting twice re-ran the liveness probes at a
|
|
555
|
+
// later instant, so the helper named on the line and the helper whose
|
|
556
|
+
// account is marked `current` could be different processes (#667).
|
|
557
|
+
const helperStatuses = readAppRuntimeHelperStatuses();
|
|
558
|
+
const selectedHelperStatus = selectRuntimeHelperStatus(helperStatuses, now);
|
|
559
|
+
logInfo(formatAppRuntimeHelperStatus(now, selectedHelperStatus, liveRuntimeHelpers(helperStatuses, now).length));
|
|
537
560
|
const appBindStatus = await printCodexAppBindStatus(deps);
|
|
538
561
|
logInfo(`Storage: ${storagePath}`);
|
|
539
562
|
if (!storage || storage.accounts.length === 0) {
|
|
@@ -552,7 +575,7 @@ async function printRotationStatus(deps) {
|
|
|
552
575
|
const runtimeCurrent = resolveRuntimeCurrentAccount(storage, {
|
|
553
576
|
runtimeSnapshot,
|
|
554
577
|
appBindStatus: appBindStatus?.running ? appBindStatus.router : null,
|
|
555
|
-
appHelperStatus: appRuntimeHelperStatusToRuntimeSignal(
|
|
578
|
+
appHelperStatus: appRuntimeHelperStatusToRuntimeSignal(selectedHelperStatus, now),
|
|
556
579
|
}, { now });
|
|
557
580
|
logInfo(`Accounts: ${storage.accounts.length}`);
|
|
558
581
|
for (let index = 0; index < storage.accounts.length; index += 1) {
|
|
@@ -111,6 +111,13 @@ export declare function stopRuntimeRotationRouterProcess(router: Pick<AppBindRou
|
|
|
111
111
|
/** Parse the documented local-time format emitted by `ps -o lstart=`. */
|
|
112
112
|
export declare function parsePosixProcessStartTime(value: string): number | null;
|
|
113
113
|
export declare function stopDetachedProcess(pid: number | null, platform: NodeJS.Platform, options?: DetachedProcessStopOptions): Promise<boolean>;
|
|
114
|
+
/**
|
|
115
|
+
* How many helper records unbind processes at once. Exported so a regression
|
|
116
|
+
* test can observe the bound rather than only its effects — with a handful of
|
|
117
|
+
* records any width behaves identically, so an edit to `Infinity` would
|
|
118
|
+
* otherwise ship green.
|
|
119
|
+
*/
|
|
120
|
+
export declare const UNBIND_HELPER_CONCURRENCY = 8;
|
|
114
121
|
export declare function stopRuntimeRotationAppHelperProcess(helper: RuntimeRotationAppHelperStatus, options?: DetachedProcessStopOptions & {
|
|
115
122
|
platform?: NodeJS.Platform;
|
|
116
123
|
}): Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-bind.d.ts","sourceRoot":"","sources":["../../../lib/runtime/app-bind.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app-bind.d.ts","sourceRoot":"","sources":["../../../lib/runtime/app-bind.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { closeSync, existsSync, mkdirSync, openSync } from "node:fs";
|
|
4
|
-
import { mkdir, open, readFile, rename, rm, unlink } from "node:fs/promises";
|
|
4
|
+
import { mkdir, open, readFile, readdir, rename, rm, unlink } from "node:fs/promises";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { basename, dirname, join } from "node:path";
|
|
7
7
|
import process from "node:process";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { withFileOperationRetry } from "../fs-retry.js";
|
|
10
10
|
import { getCodexMultiAuthDir } from "../runtime-paths.js";
|
|
11
|
-
import { APP_RUNTIME_HELPER_OWNER_FILE,
|
|
11
|
+
import { APP_RUNTIME_HELPER_OWNER_FILE, listRuntimeHelperOwnerPaths, listRuntimeHelperStatusPaths, } from "../runtime-constants.js";
|
|
12
12
|
import { configHasRuntimeRotationProvider, restoreConfigTomlFromRuntimeRotationProvider, restoreConfigTomlFromRuntimeRotationProviderWithoutBackup, rewriteConfigTomlForRuntimeRotationProvider, } from "./config-toml.js";
|
|
13
13
|
const APP_BIND_DIR_NAME = "app-bind";
|
|
14
14
|
const APP_BIND_STATE_FILE = "runtime-rotation-app-bind.json";
|
|
@@ -879,6 +879,44 @@ function resolveRuntimeHelperOwnerPath(baseDir, pid) {
|
|
|
879
879
|
return null;
|
|
880
880
|
return join(baseDir, APP_RUNTIME_HELPER_OWNER_FILE.replace(/\.json$/i, `.${pid}.json`));
|
|
881
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* How many helper records unbind processes at once. Exported so a regression
|
|
884
|
+
* test can observe the bound rather than only its effects — with a handful of
|
|
885
|
+
* records any width behaves identically, so an edit to `Infinity` would
|
|
886
|
+
* otherwise ship green.
|
|
887
|
+
*/
|
|
888
|
+
export const UNBIND_HELPER_CONCURRENCY = 8;
|
|
889
|
+
/**
|
|
890
|
+
* `Promise.all` over `items` with at most `limit` in flight, preserving input
|
|
891
|
+
* order in the result. Used where the per-item work is independent but not
|
|
892
|
+
* free — a helper stop pays a SIGTERM, a graceful wait and possibly a SIGKILL —
|
|
893
|
+
* so serialising it multiplies a single stop window by the number of stale
|
|
894
|
+
* helpers, and unbounded parallelism signals every one of them at once.
|
|
895
|
+
*/
|
|
896
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
897
|
+
const results = new Array(items.length);
|
|
898
|
+
let next = 0;
|
|
899
|
+
const runners = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
900
|
+
for (;;) {
|
|
901
|
+
const index = next;
|
|
902
|
+
next += 1;
|
|
903
|
+
// Only running past the end retires a runner. Folding the
|
|
904
|
+
// `undefined` check into the same `return` would make a sparse
|
|
905
|
+
// array or a `(T | undefined)[]` silently drop every item after
|
|
906
|
+
// the first hole — on a cleanup path whose failure mode is
|
|
907
|
+
// "helpers left running while the user is told the app was
|
|
908
|
+
// unbound".
|
|
909
|
+
if (index >= items.length)
|
|
910
|
+
return;
|
|
911
|
+
const item = items[index];
|
|
912
|
+
if (item === undefined)
|
|
913
|
+
continue;
|
|
914
|
+
results[index] = await worker(item, index);
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
await Promise.all(runners);
|
|
918
|
+
return results;
|
|
919
|
+
}
|
|
882
920
|
export async function stopRuntimeRotationAppHelperProcess(helper, options = {}) {
|
|
883
921
|
if (helper.kind !== "codex-app-runtime-rotation-helper" ||
|
|
884
922
|
helper.state !== "running" ||
|
|
@@ -1054,66 +1092,146 @@ async function unbindCodexAppRuntimeRotationLocked(options, paths) {
|
|
|
1054
1092
|
if (router?.pid && (!routerStopped || isProcessAlive(router.pid))) {
|
|
1055
1093
|
options.log?.(`Warning: runtime router (pid ${router.pid}) did not stop; continuing cleanup`);
|
|
1056
1094
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1095
|
+
// Helpers publish per-PID status files (`runtime-rotation-app-helper.<pid>.json`);
|
|
1096
|
+
// the un-suffixed name is the legacy shared path from before that change,
|
|
1097
|
+
// still checked so a pre-upgrade helper is torn down too. Every candidate
|
|
1098
|
+
// walks the same per-helper logic the single file used to get: stopping is
|
|
1099
|
+
// gated on ownership verification (status/owner identity-token agreement
|
|
1100
|
+
// plus process identity), so unbind reaps each helper it can prove is one
|
|
1101
|
+
// of ours and preserves — with a warning — anything it cannot.
|
|
1102
|
+
const helperBaseDir = dirname(paths.bindDir);
|
|
1103
|
+
let helperDirEntries = [];
|
|
1104
|
+
try {
|
|
1105
|
+
helperDirEntries = await withFileOperationRetry(() => readdir(helperBaseDir));
|
|
1106
|
+
}
|
|
1107
|
+
catch (error) {
|
|
1108
|
+
// Degrading to legacy-only cleanup while reporting success would leave
|
|
1109
|
+
// every per-PID helper running with the user told the app was unbound —
|
|
1110
|
+
// say so. ENOENT just means no helper ever ran.
|
|
1111
|
+
const code = error && typeof error === "object" && "code" in error
|
|
1112
|
+
? String(error.code)
|
|
1113
|
+
: "unknown";
|
|
1114
|
+
if (code !== "ENOENT") {
|
|
1115
|
+
options.log?.(`Warning: could not enumerate runtime app helper status files (${code}); only the legacy helper path is checked`);
|
|
1116
|
+
}
|
|
1117
|
+
helperDirEntries = [];
|
|
1118
|
+
}
|
|
1119
|
+
const helperStatusPaths = listRuntimeHelperStatusPaths(helperBaseDir, helperDirEntries);
|
|
1120
|
+
// Each candidate is independent — a read, a liveness check, and at most one
|
|
1121
|
+
// SIGTERM/graceful-wait/SIGKILL sequence — and on the machine from #663 there
|
|
1122
|
+
// were 183 of them. Run them in a bounded pool rather than one after another,
|
|
1123
|
+
// so unbind costs roughly one stop window instead of N of them; the bound
|
|
1124
|
+
// keeps a machine full of stale helpers from being hit with 183 concurrent
|
|
1125
|
+
// signal sequences.
|
|
1126
|
+
const helperResults = await mapWithConcurrency(helperStatusPaths, UNBIND_HELPER_CONCURRENCY, async (helperStatusPath) => {
|
|
1127
|
+
const helperRead = await readRuntimeHelperStatus(helperStatusPath);
|
|
1128
|
+
if (helperRead.kind !== "valid")
|
|
1129
|
+
return null;
|
|
1063
1130
|
const helper = helperRead.status;
|
|
1064
|
-
if (helper.kind
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1131
|
+
if (helper.kind !== "codex-app-runtime-rotation-helper")
|
|
1132
|
+
return null;
|
|
1133
|
+
let removeHelperStatus = false;
|
|
1134
|
+
let removeHelperOwner = false;
|
|
1135
|
+
const helperOwnerPath = resolveRuntimeHelperOwnerPath(helperBaseDir, helper.pid);
|
|
1136
|
+
const helperOwner = helperOwnerPath
|
|
1137
|
+
? await readRuntimeHelperOwner(helperOwnerPath)
|
|
1138
|
+
: null;
|
|
1139
|
+
const helperOwnershipMatches = !helper.identityToken ||
|
|
1140
|
+
(helperOwner !== null &&
|
|
1141
|
+
helper.identityToken === helperOwner.identityToken);
|
|
1142
|
+
if (helper.state === "running") {
|
|
1143
|
+
if (helper.pid === null) {
|
|
1144
|
+
options.log?.("Warning: runtime app helper status has no valid PID; preserving status");
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
const wasAlive = isProcessAlive(helper.pid);
|
|
1148
|
+
if (!wasAlive) {
|
|
1149
|
+
// Decisive, and deliberately not gated on ownership (#666): a
|
|
1150
|
+
// dead PID means both files describe a process that no longer
|
|
1151
|
+
// exists, so keeping the owner file preserves nothing. It used
|
|
1152
|
+
// to be gated, which deleted the status file and stranded the
|
|
1153
|
+
// owner file — and because unbind then enumerated status paths
|
|
1154
|
+
// only, nothing ever rediscovered it. This matches what the
|
|
1155
|
+
// launcher-side sweep already does with a dead PID.
|
|
1156
|
+
removeHelperStatus = true;
|
|
1157
|
+
removeHelperOwner = helperOwnerPath !== null;
|
|
1158
|
+
}
|
|
1159
|
+
else if (!helperOwnershipMatches) {
|
|
1160
|
+
options.log?.("Warning: runtime app helper ownership metadata does not match; preserving status");
|
|
1075
1161
|
}
|
|
1076
1162
|
else {
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
identityToken: helper.identityToken
|
|
1091
|
-
? helperOwner?.identityToken
|
|
1092
|
-
: undefined,
|
|
1093
|
-
verifyProcessIdentity: options.verifyProcessIdentity,
|
|
1094
|
-
});
|
|
1095
|
-
const stillAlive = isProcessAlive(helper.pid);
|
|
1096
|
-
removeHelperStatus = stopped && !stillAlive;
|
|
1097
|
-
removeHelperOwner =
|
|
1098
|
-
removeHelperStatus && helperOwnerPath !== null;
|
|
1099
|
-
if (!removeHelperStatus) {
|
|
1100
|
-
options.log?.(`Warning: runtime app helper (pid ${helper.pid}) did not stop; preserving status`);
|
|
1101
|
-
}
|
|
1163
|
+
const stopped = await stopRuntimeRotationAppHelperProcess(helper, {
|
|
1164
|
+
platform,
|
|
1165
|
+
log: options.log,
|
|
1166
|
+
identityToken: helper.identityToken
|
|
1167
|
+
? helperOwner?.identityToken
|
|
1168
|
+
: undefined,
|
|
1169
|
+
verifyProcessIdentity: options.verifyProcessIdentity,
|
|
1170
|
+
});
|
|
1171
|
+
const stillAlive = isProcessAlive(helper.pid);
|
|
1172
|
+
removeHelperStatus = stopped && !stillAlive;
|
|
1173
|
+
removeHelperOwner = removeHelperStatus && helperOwnerPath !== null;
|
|
1174
|
+
if (!removeHelperStatus) {
|
|
1175
|
+
options.log?.(`Warning: runtime app helper (pid ${helper.pid}) did not stop; preserving status`);
|
|
1102
1176
|
}
|
|
1103
1177
|
}
|
|
1104
1178
|
}
|
|
1105
|
-
else {
|
|
1106
|
-
// A non-running, owned record is removable only when its PID is
|
|
1107
|
-
// absent or no longer alive. This avoids deleting a status file
|
|
1108
|
-
// while a helper is still serving despite a stale state value.
|
|
1109
|
-
removeHelperStatus =
|
|
1110
|
-
helper.pid === null || !isProcessAlive(helper.pid);
|
|
1111
|
-
removeHelperOwner =
|
|
1112
|
-
removeHelperStatus &&
|
|
1113
|
-
helperOwnershipMatches &&
|
|
1114
|
-
helperOwnerPath !== null;
|
|
1115
|
-
}
|
|
1116
1179
|
}
|
|
1180
|
+
else {
|
|
1181
|
+
// A non-running, owned record is removable only when its PID is
|
|
1182
|
+
// absent or no longer alive. This avoids deleting a status file
|
|
1183
|
+
// while a helper is still serving despite a stale state value.
|
|
1184
|
+
// Ownership does not gate the owner file here either, for the same
|
|
1185
|
+
// reason as above: the PID is gone, so neither file describes
|
|
1186
|
+
// anything that can still be running.
|
|
1187
|
+
removeHelperStatus = helper.pid === null || !isProcessAlive(helper.pid);
|
|
1188
|
+
removeHelperOwner = removeHelperStatus && helperOwnerPath !== null;
|
|
1189
|
+
}
|
|
1190
|
+
return {
|
|
1191
|
+
statusPath: helperStatusPath,
|
|
1192
|
+
ownerPath: helperOwnerPath,
|
|
1193
|
+
removeHelperStatus,
|
|
1194
|
+
removeHelperOwner,
|
|
1195
|
+
};
|
|
1196
|
+
});
|
|
1197
|
+
const helperCleanupPaths = [];
|
|
1198
|
+
// Owner paths this pass already reasoned about, whether or not it decided to
|
|
1199
|
+
// remove them — a preserved live helper's owner file must not then be swept
|
|
1200
|
+
// by the orphan pass below.
|
|
1201
|
+
const consideredOwnerPaths = new Set();
|
|
1202
|
+
for (const result of helperResults) {
|
|
1203
|
+
if (!result)
|
|
1204
|
+
continue;
|
|
1205
|
+
if (result.ownerPath !== null)
|
|
1206
|
+
consideredOwnerPaths.add(result.ownerPath);
|
|
1207
|
+
if (result.removeHelperStatus)
|
|
1208
|
+
helperCleanupPaths.push(result.statusPath);
|
|
1209
|
+
if (result.removeHelperOwner && result.ownerPath !== null) {
|
|
1210
|
+
helperCleanupPaths.push(result.ownerPath);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// Owner files with no status record left to pair them with. Before #666 these
|
|
1214
|
+
// were unreachable: every earlier pass walked status paths only, so an owner
|
|
1215
|
+
// file that outlived its status file was never looked at again. A dead PID is
|
|
1216
|
+
// the whole test — a live PID's owner file belongs to a helper that is still
|
|
1217
|
+
// running, and was already considered above.
|
|
1218
|
+
for (const owner of listRuntimeHelperOwnerPaths(helperBaseDir, helperDirEntries)) {
|
|
1219
|
+
if (consideredOwnerPaths.has(owner.path))
|
|
1220
|
+
continue;
|
|
1221
|
+
if (isProcessAlive(owner.pid)) {
|
|
1222
|
+
// An owner file with no status record whose PID is nonetheless live
|
|
1223
|
+
// is the one shape this pass cannot reclaim. Either a helper is
|
|
1224
|
+
// starting right now and has not published yet, or — the case that
|
|
1225
|
+
// accumulates — the PID was recycled by an unrelated process after
|
|
1226
|
+
// the status file was already gone. Telling those apart needs the
|
|
1227
|
+
// recorded-start-time comparison the launcher-side sweep does, which
|
|
1228
|
+
// unbind has no equivalent of; that is a scope decision, but it
|
|
1229
|
+
// should not be a silent one. Every other preserve in this function
|
|
1230
|
+
// warns, so this one does too.
|
|
1231
|
+
options.log?.(`Warning: runtime app helper owner metadata (pid ${owner.pid}) has no status record but its PID is live; preserving`);
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
helperCleanupPaths.push(owner.path);
|
|
1117
1235
|
}
|
|
1118
1236
|
await removeAppBindStartup(state ?? paths);
|
|
1119
1237
|
const backup = await readAppBindBackup(paths.backupPath);
|
|
@@ -1154,8 +1272,7 @@ async function unbindCodexAppRuntimeRotationLocked(options, paths) {
|
|
|
1154
1272
|
paths.backupPath,
|
|
1155
1273
|
paths.statusPath,
|
|
1156
1274
|
state?.logPath ?? paths.logPath,
|
|
1157
|
-
...
|
|
1158
|
-
...(removeHelperOwner && helperOwnerPath ? [helperOwnerPath] : []),
|
|
1275
|
+
...helperCleanupPaths,
|
|
1159
1276
|
];
|
|
1160
1277
|
for (const candidate of cleanupCandidates) {
|
|
1161
1278
|
try {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fields any helper status record has to expose for the shared selector to
|
|
3
|
+
* reason about it. `rotation status` and runtime account resolution carry
|
|
4
|
+
* different extra fields — request counters on one side, account identity on
|
|
5
|
+
* the other — but they answer "which helper is current" the same way, and used
|
|
6
|
+
* to answer it with two hand-rolled copies that could drift apart within a
|
|
7
|
+
* single command (#667).
|
|
8
|
+
*/
|
|
9
|
+
export interface RuntimeHelperSelectable {
|
|
10
|
+
state: string | null;
|
|
11
|
+
pid: number | null;
|
|
12
|
+
startedAt: number | null;
|
|
13
|
+
updatedAt: number | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* How stale a `running` record may be before it stops counting as live.
|
|
17
|
+
*
|
|
18
|
+
* A running helper republishes its status on every tick, and the publish path
|
|
19
|
+
* heartbeats at least once per `APP_RUNTIME_HELPER_STATUS_HEARTBEAT_MS` (60s)
|
|
20
|
+
* even when nothing in the payload changed. Ten heartbeats of silence is not a
|
|
21
|
+
* helper that is merely quiet — it is a record whose writer is gone.
|
|
22
|
+
*
|
|
23
|
+
* This is the identity check these readers were missing. `kill(pid, 0)` answers
|
|
24
|
+
* "does *a* process hold this integer", so a stale record — classically the
|
|
25
|
+
* legacy shared `runtime-rotation-app-helper.json` left behind by a SIGKILLed
|
|
26
|
+
* pre-upgrade helper — passes liveness as soon as an unrelated process is
|
|
27
|
+
* handed its PID, and can then win selection outright. Freshness is the half of
|
|
28
|
+
* identity available to a synchronous reader: the wrapper verifies identity by
|
|
29
|
+
* probing kernel start times, but that costs a `ps` per candidate, and these
|
|
30
|
+
* two call sites are read-only status paths reached from the interactive menu
|
|
31
|
+
* as well as the CLI. Whoever holds the PID now, they are not the process that
|
|
32
|
+
* last wrote this file.
|
|
33
|
+
*/
|
|
34
|
+
export declare const RUNTIME_HELPER_STATUS_STALE_MS: number;
|
|
35
|
+
/** Tolerance for clock skew between the writing helper and the reader. */
|
|
36
|
+
export declare const RUNTIME_HELPER_CLOCK_TOLERANCE_MS: number;
|
|
37
|
+
/**
|
|
38
|
+
* A PID is a positive integer or it is nothing. Both readers used to accept any
|
|
39
|
+
* finite number, so a corrupt or hand-edited record carrying `-1234` reached
|
|
40
|
+
* `process.kill(-1234, 0)` — which on POSIX probes process *group* 1234 and
|
|
41
|
+
* succeeds on any busy machine, reporting a helper that does not exist as live.
|
|
42
|
+
* Fractional values were accepted the same way.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readRuntimeHelperPid(value: unknown): number | null;
|
|
45
|
+
/**
|
|
46
|
+
* Best-effort liveness probe. `EPERM` means a process exists that this user may
|
|
47
|
+
* not signal, so it counts as alive; every other errno — including the `EINVAL`
|
|
48
|
+
* some platforms raise for a PID above their ceiling — counts as dead.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isRuntimeHelperProcessAlive(pid: number | null): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The single definition of "this helper is currently serving": a running state,
|
|
53
|
+
* a live PID, and a record recent enough to have been written by that PID's
|
|
54
|
+
* current occupant.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isLiveRuntimeHelper(status: RuntimeHelperSelectable, now?: number): boolean;
|
|
57
|
+
/** Every helper that passes {@link isLiveRuntimeHelper}, input order preserved. */
|
|
58
|
+
export declare function liveRuntimeHelpers<T extends RuntimeHelperSelectable>(statuses: readonly T[], now?: number): T[];
|
|
59
|
+
/**
|
|
60
|
+
* Prefer a live helper, and among several the most recently updated. Absent any
|
|
61
|
+
* live helper, fall back to the freshest record so the previous "reports the
|
|
62
|
+
* last helper's final state" behaviour survives.
|
|
63
|
+
*
|
|
64
|
+
* Callers that also need the live count must pass the same `statuses` array and
|
|
65
|
+
* the same `now` to {@link liveRuntimeHelpers}, so the selected helper and the
|
|
66
|
+
* count describe one instant rather than two.
|
|
67
|
+
*/
|
|
68
|
+
export declare function selectRuntimeHelperStatus<T extends RuntimeHelperSelectable>(statuses: readonly T[], now?: number): T | null;
|
|
69
|
+
//# sourceMappingURL=app-helper-selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-helper-selection.d.ts","sourceRoot":"","sources":["../../../lib/runtime/app-helper-selection.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAuB;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,8BAA8B,QAAiB,CAAC;AAE7D,0EAA0E;AAC1E,eAAO,MAAM,iCAAiC,QAAY,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAIlE;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAWvE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,uBAAuB,EAC/B,GAAG,GAAE,MAAmB,GACtB,OAAO,CAgBT;AAED,mFAAmF;AACnF,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,uBAAuB,EACnE,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,GAAG,GAAE,MAAmB,GACtB,CAAC,EAAE,CAEL;AASD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,uBAAuB,EAC1E,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,GAAG,GAAE,MAAmB,GACtB,CAAC,GAAG,IAAI,CAKV"}
|