triflux 10.20.0 → 10.20.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +34 -42
- package/README.md +30 -36
- package/bin/triflux.mjs +13 -4
- package/hooks/hook-orchestrator.mjs +1 -1
- package/hooks/hook-registry.json +76 -2
- package/hooks/hooks.json +49 -1
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +355 -16
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/session-end-cleanup.mjs +201 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +4 -0
- package/hub/account-broker.mjs +297 -8
- package/hub/codex-adapter.mjs +62 -2
- package/hub/server.mjs +51 -4
- package/hub/team/cli/services/native-control.mjs +1 -0
- package/hub/team/conductor.mjs +94 -29
- package/hub/team/native-supervisor.mjs +20 -7
- package/hub/team/swarm-cli.mjs +98 -4
- package/hub/team/swarm-hypervisor.mjs +112 -10
- package/hub/team/swarm-locks.mjs +7 -2
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/synapse-cli.mjs +243 -6
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worktree-lifecycle.mjs +49 -8
- package/hub/workers/delegator-mcp.mjs +14 -1
- package/package.json +1 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +4 -0
- package/scripts/__tests__/skill-surface.test.mjs +61 -0
- package/scripts/gen-skill-manifest.mjs +3 -0
- package/scripts/lib/skill-template.mjs +2 -0
- package/scripts/session-stale-cleanup.mjs +111 -2
- package/skills/star-prompt/skill.json +1 -1
- package/skills/tfx-analysis/skill.json +11 -3
- package/skills/tfx-auto/skill.json +2 -2
- package/skills/tfx-autopilot/skill.json +9 -4
- package/skills/tfx-consensus/skill.json +4 -3
- package/skills/tfx-debate/skill.json +11 -4
- package/skills/tfx-doctor/skill.json +3 -1
- package/skills/tfx-fullcycle/skill.json +10 -4
- package/skills/tfx-hooks/skill.json +3 -1
- package/skills/tfx-hub/skill.json +5 -3
- package/skills/tfx-index/skill.json +6 -1
- package/skills/tfx-interview/skill.json +6 -1
- package/skills/tfx-multi/skill.json +7 -3
- package/skills/tfx-panel/skill.json +11 -4
- package/skills/tfx-persist/skill.json +11 -4
- package/skills/tfx-plan/skill.json +13 -3
- package/skills/tfx-profile/skill.json +3 -1
- package/skills/tfx-prune/skill.json +7 -1
- package/skills/tfx-psmux-rules/SKILL.md +2 -0
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +22 -307
- package/skills/tfx-psmux-rules/skill.json +7 -2
- package/skills/tfx-qa/skill.json +12 -3
- package/skills/tfx-ralph/skill.json +4 -1
- package/skills/tfx-remote/skill.json +8 -0
- package/skills/tfx-remote-setup/SKILL.md +2 -0
- package/skills/tfx-remote-setup/SKILL.md.tmpl +23 -570
- package/skills/tfx-remote-setup/skill.json +7 -3
- package/skills/tfx-remote-spawn/SKILL.md +2 -0
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +31 -242
- package/skills/tfx-remote-spawn/skill.json +7 -3
- package/skills/tfx-research/skill.json +11 -4
- package/skills/tfx-review/skill.json +12 -3
- package/skills/tfx-setup/skill.json +3 -1
- package/skills/tfx-ship/skill.json +13 -0
- package/skills/tfx-swarm/skill.json +13 -0
- package/skills/tfx-wt/skill.json +12 -0
package/hub/account-broker.mjs
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
import { EventEmitter } from "node:events";
|
|
7
7
|
import {
|
|
8
|
+
chmodSync,
|
|
8
9
|
closeSync,
|
|
9
10
|
existsSync,
|
|
10
11
|
constants as fsConstants,
|
|
11
12
|
mkdirSync,
|
|
12
13
|
openSync,
|
|
13
14
|
readFileSync,
|
|
15
|
+
renameSync,
|
|
14
16
|
statSync,
|
|
15
17
|
unlinkSync,
|
|
16
18
|
writeFileSync,
|
|
@@ -183,6 +185,7 @@ function createSyncResult({
|
|
|
183
185
|
direction,
|
|
184
186
|
sourcePath,
|
|
185
187
|
cachePath,
|
|
188
|
+
runtimePath,
|
|
186
189
|
copied = false,
|
|
187
190
|
skipped = false,
|
|
188
191
|
reason = "ok",
|
|
@@ -193,6 +196,7 @@ function createSyncResult({
|
|
|
193
196
|
direction,
|
|
194
197
|
sourcePath,
|
|
195
198
|
cachePath,
|
|
199
|
+
runtimePath,
|
|
196
200
|
copied,
|
|
197
201
|
skipped,
|
|
198
202
|
reason,
|
|
@@ -283,6 +287,7 @@ class AccountBroker extends EventEmitter {
|
|
|
283
287
|
_authSyncLockRetryMs = AUTH_SYNC_LOCK_RETRY_MS,
|
|
284
288
|
_authSyncLockStaleMs = AUTH_SYNC_LOCK_STALE_MS,
|
|
285
289
|
_syncCopyDelayMs = 0,
|
|
290
|
+
_activeLeases = [],
|
|
286
291
|
} = {},
|
|
287
292
|
) {
|
|
288
293
|
super();
|
|
@@ -309,8 +314,17 @@ class AccountBroker extends EventEmitter {
|
|
|
309
314
|
const persisted = this.#persist ? loadPersistedState() : null;
|
|
310
315
|
const pEntries = persisted?.entries || {};
|
|
311
316
|
|
|
317
|
+
const activeLeaseById = new Map(
|
|
318
|
+
Array.isArray(_activeLeases)
|
|
319
|
+
? _activeLeases
|
|
320
|
+
.filter((lease) => lease?.id)
|
|
321
|
+
.map((lease) => [lease.id, lease])
|
|
322
|
+
: [],
|
|
323
|
+
);
|
|
324
|
+
|
|
312
325
|
for (const account of allAccounts) {
|
|
313
326
|
const saved = pEntries[account.id];
|
|
327
|
+
const activeLease = activeLeaseById.get(account.id);
|
|
314
328
|
this.#state.set(account.id, {
|
|
315
329
|
id: account.id,
|
|
316
330
|
provider: account.provider,
|
|
@@ -320,14 +334,17 @@ class AccountBroker extends EventEmitter {
|
|
|
320
334
|
authFile: account.authFile,
|
|
321
335
|
host: account.host,
|
|
322
336
|
tier: account.tier ?? "unknown",
|
|
323
|
-
busy:
|
|
324
|
-
leasedAt: null,
|
|
337
|
+
busy: activeLease?.busy === true,
|
|
338
|
+
leasedAt: activeLease?.leasedAt ?? null,
|
|
325
339
|
cooldownUntil: saved?.cooldownUntil ?? 0,
|
|
326
340
|
failureTimestamps: saved?.failureTimestamps ?? [],
|
|
327
341
|
circuitOpenedAt: saved?.circuitOpenedAt ?? 0,
|
|
328
|
-
circuitTrialInFlight:
|
|
329
|
-
lastUsedAt: saved?.lastUsedAt ?? 0,
|
|
330
|
-
totalSessions:
|
|
342
|
+
circuitTrialInFlight: activeLease?.circuitTrialInFlight === true,
|
|
343
|
+
lastUsedAt: activeLease?.lastUsedAt ?? saved?.lastUsedAt ?? 0,
|
|
344
|
+
totalSessions: Math.max(
|
|
345
|
+
saved?.totalSessions ?? 0,
|
|
346
|
+
activeLease?.totalSessions ?? 0,
|
|
347
|
+
),
|
|
331
348
|
});
|
|
332
349
|
}
|
|
333
350
|
}
|
|
@@ -351,7 +368,10 @@ class AccountBroker extends EventEmitter {
|
|
|
351
368
|
}
|
|
352
369
|
|
|
353
370
|
#getLockPath(acct) {
|
|
354
|
-
|
|
371
|
+
if (acct?.provider === "codex" && acct?.mode === "auth") {
|
|
372
|
+
return join(this.#authBasePath, "codex-auth-source.lock");
|
|
373
|
+
}
|
|
374
|
+
return join(this.#authBasePath, `auth-sync-${acct.id}.lock`);
|
|
355
375
|
}
|
|
356
376
|
|
|
357
377
|
#copyAuthPayload(sourcePath, destPath) {
|
|
@@ -360,13 +380,68 @@ class AccountBroker extends EventEmitter {
|
|
|
360
380
|
sleepSync(this.#syncCopyDelayMs);
|
|
361
381
|
}
|
|
362
382
|
mkdirSync(dirname(destPath), { recursive: true });
|
|
363
|
-
|
|
383
|
+
const tmpPath = join(
|
|
384
|
+
dirname(destPath),
|
|
385
|
+
`.${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`,
|
|
386
|
+
);
|
|
387
|
+
try {
|
|
388
|
+
writeFileSync(tmpPath, payload, { mode: 0o600 });
|
|
389
|
+
renameSync(tmpPath, destPath);
|
|
390
|
+
try {
|
|
391
|
+
chmodSync(destPath, 0o600);
|
|
392
|
+
} catch {}
|
|
393
|
+
} catch (error) {
|
|
394
|
+
try {
|
|
395
|
+
unlinkSync(tmpPath);
|
|
396
|
+
} catch {}
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
#runtimeAuthPath(acct) {
|
|
402
|
+
const safeId = String(acct.id).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
403
|
+
return join(this.#authBasePath, "codex-home", safeId, "auth.json");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#prepareRuntimeAuthFile(acct, cachePath) {
|
|
407
|
+
const runtimePath = this.#runtimeAuthPath(acct);
|
|
408
|
+
try {
|
|
409
|
+
const cacheAuth = readAuthPayload(cachePath);
|
|
410
|
+
if (!cacheAuth.accountId) {
|
|
411
|
+
return {
|
|
412
|
+
ok: false,
|
|
413
|
+
reason: "cache_account_unknown",
|
|
414
|
+
authFile: runtimePath,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (cacheAuth.accountId !== acct.id) {
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
reason: "cache_account_mismatch",
|
|
421
|
+
authFile: runtimePath,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
this.#copyAuthPayload(cachePath, runtimePath);
|
|
425
|
+
return {
|
|
426
|
+
ok: true,
|
|
427
|
+
reason: "runtime_auth_prepared",
|
|
428
|
+
authFile: runtimePath,
|
|
429
|
+
};
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return {
|
|
432
|
+
ok: false,
|
|
433
|
+
reason:
|
|
434
|
+
error?.code === "ENOENT" ? "cache_missing" : "runtime_auth_error",
|
|
435
|
+
authFile: runtimePath,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
364
438
|
}
|
|
365
439
|
|
|
366
440
|
#syncAuth(accountId, direction) {
|
|
367
441
|
const acct = this.#state.get(accountId);
|
|
368
442
|
const sourcePath = this.#getSourceAuthPath(acct);
|
|
369
443
|
const cachePath = this.#resolveCacheAuthPath(acct);
|
|
444
|
+
const runtimePath = acct ? this.#runtimeAuthPath(acct) : null;
|
|
370
445
|
|
|
371
446
|
if (!acct || acct.mode !== "auth" || acct.provider !== "codex") {
|
|
372
447
|
return createSyncResult({
|
|
@@ -374,6 +449,7 @@ class AccountBroker extends EventEmitter {
|
|
|
374
449
|
direction,
|
|
375
450
|
sourcePath,
|
|
376
451
|
cachePath,
|
|
452
|
+
runtimePath,
|
|
377
453
|
skipped: true,
|
|
378
454
|
reason: "unsupported_account",
|
|
379
455
|
});
|
|
@@ -389,6 +465,7 @@ class AccountBroker extends EventEmitter {
|
|
|
389
465
|
direction,
|
|
390
466
|
sourcePath,
|
|
391
467
|
cachePath,
|
|
468
|
+
runtimePath,
|
|
392
469
|
skipped: true,
|
|
393
470
|
reason: "invalid_cache_path",
|
|
394
471
|
});
|
|
@@ -400,6 +477,7 @@ class AccountBroker extends EventEmitter {
|
|
|
400
477
|
direction,
|
|
401
478
|
sourcePath,
|
|
402
479
|
cachePath,
|
|
480
|
+
runtimePath,
|
|
403
481
|
skipped: true,
|
|
404
482
|
reason: "unsupported_source",
|
|
405
483
|
});
|
|
@@ -410,6 +488,109 @@ class AccountBroker extends EventEmitter {
|
|
|
410
488
|
this.#authSyncLockOpts,
|
|
411
489
|
() => {
|
|
412
490
|
try {
|
|
491
|
+
if (direction === "runtime-to-source") {
|
|
492
|
+
const runtimeStat = statOrNull(runtimePath);
|
|
493
|
+
if (!runtimeStat) {
|
|
494
|
+
return createSyncResult({
|
|
495
|
+
accountId,
|
|
496
|
+
direction,
|
|
497
|
+
sourcePath,
|
|
498
|
+
cachePath,
|
|
499
|
+
runtimePath,
|
|
500
|
+
skipped: true,
|
|
501
|
+
reason: "runtime_missing",
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const runtimeAuth = readAuthPayload(runtimePath);
|
|
506
|
+
if (!runtimeAuth.accountId) {
|
|
507
|
+
return createSyncResult({
|
|
508
|
+
accountId,
|
|
509
|
+
direction,
|
|
510
|
+
sourcePath,
|
|
511
|
+
cachePath,
|
|
512
|
+
runtimePath,
|
|
513
|
+
skipped: true,
|
|
514
|
+
reason: "runtime_account_unknown",
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (runtimeAuth.accountId !== accountId) {
|
|
518
|
+
return createSyncResult({
|
|
519
|
+
accountId,
|
|
520
|
+
direction,
|
|
521
|
+
sourcePath,
|
|
522
|
+
cachePath,
|
|
523
|
+
runtimePath,
|
|
524
|
+
skipped: true,
|
|
525
|
+
reason: "runtime_account_mismatch",
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const cacheStat = statOrNull(cachePath);
|
|
530
|
+
let shouldCopyRuntimeToCache = !cacheStat;
|
|
531
|
+
if (cacheStat) {
|
|
532
|
+
const cacheAuth = readAuthPayload(cachePath);
|
|
533
|
+
if (!cacheAuth.accountId) {
|
|
534
|
+
return createSyncResult({
|
|
535
|
+
accountId,
|
|
536
|
+
direction,
|
|
537
|
+
sourcePath,
|
|
538
|
+
cachePath,
|
|
539
|
+
runtimePath,
|
|
540
|
+
skipped: true,
|
|
541
|
+
reason: "cache_account_unknown",
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
if (cacheAuth.accountId !== accountId) {
|
|
545
|
+
return createSyncResult({
|
|
546
|
+
accountId,
|
|
547
|
+
direction,
|
|
548
|
+
sourcePath,
|
|
549
|
+
cachePath,
|
|
550
|
+
runtimePath,
|
|
551
|
+
skipped: true,
|
|
552
|
+
reason: "cache_account_mismatch",
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
shouldCopyRuntimeToCache =
|
|
556
|
+
runtimeStat.mtimeMs > cacheStat.mtimeMs ||
|
|
557
|
+
!runtimeAuth.buffer.equals(cacheAuth.buffer);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
let copied = false;
|
|
561
|
+
if (shouldCopyRuntimeToCache) {
|
|
562
|
+
this.#copyAuthPayload(runtimePath, cachePath);
|
|
563
|
+
copied = true;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const nextCacheStat = statOrNull(cachePath);
|
|
567
|
+
const nextCacheAuth = readAuthPayload(cachePath);
|
|
568
|
+
const sourceStat = statOrNull(sourcePath);
|
|
569
|
+
let shouldCopyCacheToSource = !sourceStat;
|
|
570
|
+
if (sourceStat) {
|
|
571
|
+
const sourceAuth = readAuthPayload(sourcePath);
|
|
572
|
+
shouldCopyCacheToSource =
|
|
573
|
+
sourceAuth.accountId !== accountId ||
|
|
574
|
+
nextCacheStat.mtimeMs > sourceStat.mtimeMs ||
|
|
575
|
+
!nextCacheAuth.buffer.equals(sourceAuth.buffer);
|
|
576
|
+
}
|
|
577
|
+
if (shouldCopyCacheToSource) {
|
|
578
|
+
this.#copyAuthPayload(cachePath, sourcePath);
|
|
579
|
+
copied = true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return createSyncResult({
|
|
583
|
+
accountId,
|
|
584
|
+
direction,
|
|
585
|
+
sourcePath,
|
|
586
|
+
cachePath,
|
|
587
|
+
runtimePath,
|
|
588
|
+
copied,
|
|
589
|
+
skipped: !copied,
|
|
590
|
+
reason: copied ? "runtime_propagated" : "up_to_date",
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
413
594
|
if (direction === "from-source") {
|
|
414
595
|
const sourceStat = statOrNull(sourcePath);
|
|
415
596
|
if (!sourceStat) {
|
|
@@ -418,6 +599,7 @@ class AccountBroker extends EventEmitter {
|
|
|
418
599
|
direction,
|
|
419
600
|
sourcePath,
|
|
420
601
|
cachePath,
|
|
602
|
+
runtimePath,
|
|
421
603
|
skipped: true,
|
|
422
604
|
reason: "source_missing",
|
|
423
605
|
});
|
|
@@ -430,6 +612,7 @@ class AccountBroker extends EventEmitter {
|
|
|
430
612
|
direction,
|
|
431
613
|
sourcePath,
|
|
432
614
|
cachePath,
|
|
615
|
+
runtimePath,
|
|
433
616
|
skipped: true,
|
|
434
617
|
reason: "source_account_unknown",
|
|
435
618
|
});
|
|
@@ -440,18 +623,46 @@ class AccountBroker extends EventEmitter {
|
|
|
440
623
|
direction,
|
|
441
624
|
sourcePath,
|
|
442
625
|
cachePath,
|
|
626
|
+
runtimePath,
|
|
443
627
|
skipped: true,
|
|
444
628
|
reason: "source_account_mismatch",
|
|
445
629
|
});
|
|
446
630
|
}
|
|
447
631
|
|
|
448
632
|
const cacheStat = statOrNull(cachePath);
|
|
633
|
+
if (cacheStat) {
|
|
634
|
+
const cacheAuth = readAuthPayload(cachePath);
|
|
635
|
+
if (!cacheAuth.accountId) {
|
|
636
|
+
return createSyncResult({
|
|
637
|
+
accountId,
|
|
638
|
+
direction,
|
|
639
|
+
sourcePath,
|
|
640
|
+
cachePath,
|
|
641
|
+
runtimePath,
|
|
642
|
+
skipped: true,
|
|
643
|
+
reason: "cache_account_unknown",
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
if (cacheAuth.accountId !== accountId) {
|
|
647
|
+
return createSyncResult({
|
|
648
|
+
accountId,
|
|
649
|
+
direction,
|
|
650
|
+
sourcePath,
|
|
651
|
+
cachePath,
|
|
652
|
+
runtimePath,
|
|
653
|
+
skipped: true,
|
|
654
|
+
reason: "cache_account_mismatch",
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
449
659
|
if (cacheStat && sourceStat.mtimeMs <= cacheStat.mtimeMs) {
|
|
450
660
|
return createSyncResult({
|
|
451
661
|
accountId,
|
|
452
662
|
direction,
|
|
453
663
|
sourcePath,
|
|
454
664
|
cachePath,
|
|
665
|
+
runtimePath,
|
|
455
666
|
skipped: true,
|
|
456
667
|
reason: "up_to_date",
|
|
457
668
|
});
|
|
@@ -463,6 +674,7 @@ class AccountBroker extends EventEmitter {
|
|
|
463
674
|
direction,
|
|
464
675
|
sourcePath,
|
|
465
676
|
cachePath,
|
|
677
|
+
runtimePath,
|
|
466
678
|
copied: true,
|
|
467
679
|
reason: cacheStat ? "cache_updated" : "cache_created",
|
|
468
680
|
});
|
|
@@ -475,6 +687,7 @@ class AccountBroker extends EventEmitter {
|
|
|
475
687
|
direction,
|
|
476
688
|
sourcePath,
|
|
477
689
|
cachePath,
|
|
690
|
+
runtimePath,
|
|
478
691
|
skipped: true,
|
|
479
692
|
reason: "cache_missing",
|
|
480
693
|
});
|
|
@@ -487,6 +700,7 @@ class AccountBroker extends EventEmitter {
|
|
|
487
700
|
direction,
|
|
488
701
|
sourcePath,
|
|
489
702
|
cachePath,
|
|
703
|
+
runtimePath,
|
|
490
704
|
skipped: true,
|
|
491
705
|
reason: "cache_account_unknown",
|
|
492
706
|
});
|
|
@@ -497,6 +711,7 @@ class AccountBroker extends EventEmitter {
|
|
|
497
711
|
direction,
|
|
498
712
|
sourcePath,
|
|
499
713
|
cachePath,
|
|
714
|
+
runtimePath,
|
|
500
715
|
skipped: true,
|
|
501
716
|
reason: "cache_account_mismatch",
|
|
502
717
|
});
|
|
@@ -509,6 +724,7 @@ class AccountBroker extends EventEmitter {
|
|
|
509
724
|
direction,
|
|
510
725
|
sourcePath,
|
|
511
726
|
cachePath,
|
|
727
|
+
runtimePath,
|
|
512
728
|
skipped: true,
|
|
513
729
|
reason: "up_to_date",
|
|
514
730
|
});
|
|
@@ -520,6 +736,7 @@ class AccountBroker extends EventEmitter {
|
|
|
520
736
|
direction,
|
|
521
737
|
sourcePath,
|
|
522
738
|
cachePath,
|
|
739
|
+
runtimePath,
|
|
523
740
|
copied: true,
|
|
524
741
|
reason: sourceStat ? "source_updated" : "source_created",
|
|
525
742
|
});
|
|
@@ -529,6 +746,7 @@ class AccountBroker extends EventEmitter {
|
|
|
529
746
|
direction,
|
|
530
747
|
sourcePath,
|
|
531
748
|
cachePath,
|
|
749
|
+
runtimePath,
|
|
532
750
|
skipped: true,
|
|
533
751
|
reason: error?.code === "ENOENT" ? "file_missing" : "read_error",
|
|
534
752
|
});
|
|
@@ -542,6 +760,7 @@ class AccountBroker extends EventEmitter {
|
|
|
542
760
|
direction,
|
|
543
761
|
sourcePath,
|
|
544
762
|
cachePath,
|
|
763
|
+
runtimePath,
|
|
545
764
|
skipped: true,
|
|
546
765
|
reason: "lock_timeout",
|
|
547
766
|
});
|
|
@@ -558,6 +777,10 @@ class AccountBroker extends EventEmitter {
|
|
|
558
777
|
return this.#syncAuth(accountId, "to-source");
|
|
559
778
|
}
|
|
560
779
|
|
|
780
|
+
syncRuntimeAuthToSource(accountId) {
|
|
781
|
+
return this.#syncAuth(accountId, "runtime-to-source");
|
|
782
|
+
}
|
|
783
|
+
|
|
561
784
|
// ── per-account circuit breaker ─────────────────────────────────
|
|
562
785
|
|
|
563
786
|
#getCircuitState(acct, now) {
|
|
@@ -697,14 +920,33 @@ class AccountBroker extends EventEmitter {
|
|
|
697
920
|
try {
|
|
698
921
|
const syncResult = this.syncAuthFromSource(acct.id);
|
|
699
922
|
this.emit("authSync", syncResult);
|
|
923
|
+
if (!syncResult?.ok) {
|
|
924
|
+
this.emit("authSyncError", {
|
|
925
|
+
accountId: acct.id,
|
|
926
|
+
direction: "from-source",
|
|
927
|
+
reason: syncResult?.reason || "sync_failed",
|
|
928
|
+
});
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
700
931
|
} catch (error) {
|
|
701
932
|
this.emit("authSyncError", {
|
|
702
933
|
accountId: acct.id,
|
|
703
934
|
direction: "from-source",
|
|
704
935
|
error: error?.message || String(error),
|
|
705
936
|
});
|
|
937
|
+
return null;
|
|
706
938
|
}
|
|
707
939
|
}
|
|
940
|
+
const runtimeAuth = this.#prepareRuntimeAuthFile(acct, authFile);
|
|
941
|
+
if (!runtimeAuth.ok) {
|
|
942
|
+
this.emit("authSyncError", {
|
|
943
|
+
accountId: acct.id,
|
|
944
|
+
direction: "runtime-auth",
|
|
945
|
+
reason: runtimeAuth.reason,
|
|
946
|
+
});
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
authFile = runtimeAuth.authFile;
|
|
708
950
|
}
|
|
709
951
|
|
|
710
952
|
// mark half-open trial if applicable
|
|
@@ -746,6 +988,14 @@ class AccountBroker extends EventEmitter {
|
|
|
746
988
|
const acct = this.#state.get(accountId);
|
|
747
989
|
if (!acct?.busy) return;
|
|
748
990
|
|
|
991
|
+
if (acct.provider === "codex" && acct.mode === "auth") {
|
|
992
|
+
const authSync = this.syncRuntimeAuthToSource(accountId);
|
|
993
|
+
this.emit(authSync?.ok ? "authSync" : "authSyncError", {
|
|
994
|
+
...authSync,
|
|
995
|
+
accountId,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
749
999
|
const now = Date.now();
|
|
750
1000
|
const ok = result?.ok === true;
|
|
751
1001
|
const circuit = this.#getCircuitState(acct, now);
|
|
@@ -820,6 +1070,41 @@ class AccountBroker extends EventEmitter {
|
|
|
820
1070
|
}));
|
|
821
1071
|
}
|
|
822
1072
|
|
|
1073
|
+
publicSnapshot() {
|
|
1074
|
+
const now = Date.now();
|
|
1075
|
+
this.#pruneExpiredLeases(now);
|
|
1076
|
+
return [...this.#state.values()].map((acct) => ({
|
|
1077
|
+
id: acct.id,
|
|
1078
|
+
provider: acct.provider,
|
|
1079
|
+
tier: acct.tier ?? "unknown",
|
|
1080
|
+
busy: acct.busy === true,
|
|
1081
|
+
cooldownUntil: acct.cooldownUntil ?? 0,
|
|
1082
|
+
lastUsedAt: acct.lastUsedAt ?? 0,
|
|
1083
|
+
totalSessions: acct.totalSessions ?? 0,
|
|
1084
|
+
remainingMs: getRemainingLeaseMs(acct, now),
|
|
1085
|
+
circuitState: this.#getCircuitState(acct, now).state,
|
|
1086
|
+
failureCount: Array.isArray(acct.failureTimestamps)
|
|
1087
|
+
? acct.failureTimestamps.length
|
|
1088
|
+
: 0,
|
|
1089
|
+
}));
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
activeLeases() {
|
|
1093
|
+
const now = Date.now();
|
|
1094
|
+
this.#pruneExpiredLeases(now);
|
|
1095
|
+
return [...this.#state.values()]
|
|
1096
|
+
.filter((acct) => acct.busy && acct.leasedAt !== null)
|
|
1097
|
+
.map((acct) => ({
|
|
1098
|
+
id: acct.id,
|
|
1099
|
+
provider: acct.provider,
|
|
1100
|
+
busy: acct.busy,
|
|
1101
|
+
leasedAt: acct.leasedAt,
|
|
1102
|
+
lastUsedAt: acct.lastUsedAt,
|
|
1103
|
+
totalSessions: acct.totalSessions,
|
|
1104
|
+
circuitTrialInFlight: acct.circuitTrialInFlight,
|
|
1105
|
+
}));
|
|
1106
|
+
}
|
|
1107
|
+
|
|
823
1108
|
// ── disabled marker ───────────────────────────────────────────
|
|
824
1109
|
|
|
825
1110
|
/**
|
|
@@ -914,6 +1199,10 @@ function createBroker() {
|
|
|
914
1199
|
|
|
915
1200
|
/** Re-read config and replace the module-level singleton. ESM live binding propagates to all importers. */
|
|
916
1201
|
function reloadBroker() {
|
|
1202
|
+
const activeLeases =
|
|
1203
|
+
broker && typeof broker.activeLeases === "function"
|
|
1204
|
+
? broker.activeLeases()
|
|
1205
|
+
: [];
|
|
917
1206
|
if (isBrokerDisabledByEnv()) {
|
|
918
1207
|
broker = createEmptyBroker();
|
|
919
1208
|
return { ok: true, broker, disabled: true };
|
|
@@ -921,7 +1210,7 @@ function reloadBroker() {
|
|
|
921
1210
|
const config = loadConfig();
|
|
922
1211
|
if (!config) return { ok: false, error: "Config not found or invalid" };
|
|
923
1212
|
try {
|
|
924
|
-
broker = new AccountBroker(config);
|
|
1213
|
+
broker = new AccountBroker(config, { _activeLeases: activeLeases });
|
|
925
1214
|
return { ok: true, broker };
|
|
926
1215
|
} catch (err) {
|
|
927
1216
|
return { ok: false, error: err.message };
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
buildExecCommand,
|
|
6
|
+
createResult,
|
|
6
7
|
executeWithCircuitBroker,
|
|
7
8
|
normalizePathForShell,
|
|
8
9
|
runProcess,
|
|
@@ -155,6 +156,14 @@ async function runCodex(prompt, workdir, preflight, attempt, lease) {
|
|
|
155
156
|
// account 의 auth.json 을 사용하게 한다. lease 가 null 이면 default ~/.codex
|
|
156
157
|
// 동작 유지 (회귀 없음). lease.env 는 추가 환경변수 (provider 고정 등) 주입용.
|
|
157
158
|
const spawnEnv = lease ? buildLeaseSpawnEnv(lease) : undefined;
|
|
159
|
+
const authCheck = validateLeaseAuth(lease, spawnEnv);
|
|
160
|
+
if (!authCheck.ok) {
|
|
161
|
+
return createResult(false, {
|
|
162
|
+
stderr: authCheck.error,
|
|
163
|
+
failureMode: "auth_sync",
|
|
164
|
+
skipCircuit: true,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
158
167
|
return runProcess(command, workdir, attempt.timeout, {
|
|
159
168
|
resultFile,
|
|
160
169
|
inferStallMode,
|
|
@@ -182,6 +191,57 @@ function buildLeaseSpawnEnv(lease) {
|
|
|
182
191
|
return Object.keys(extra).length ? { ...process.env, ...extra } : undefined;
|
|
183
192
|
}
|
|
184
193
|
|
|
194
|
+
function validateLeaseAuth(lease, spawnEnv) {
|
|
195
|
+
if (!lease?.authFile) return { ok: true };
|
|
196
|
+
if (basename(lease.authFile) !== "auth.json") {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
error: `lease authFile must resolve to auth.json for account ${lease.id}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (!existsSync(lease.authFile)) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
error: `lease authFile missing for account ${lease.id}: ${lease.authFile}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (dirnameOf(lease.authFile) !== spawnEnv?.CODEX_HOME) {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
// This is currently expected to match because buildLeaseSpawnEnv derives
|
|
212
|
+
// CODEX_HOME from authFile. Keep the guard so future lease.env overrides
|
|
213
|
+
// cannot silently point Codex at a different account home.
|
|
214
|
+
error: `CODEX_HOME mismatch for account ${lease.id}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const parsed = JSON.parse(readFileSync(lease.authFile, "utf8"));
|
|
219
|
+
const accountId =
|
|
220
|
+
parsed?.tokens?.account_id ??
|
|
221
|
+
parsed?.account_id ??
|
|
222
|
+
parsed?.accountId ??
|
|
223
|
+
null;
|
|
224
|
+
if (!accountId) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
error: `lease authFile has no account_id for account ${lease.id}`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (accountId !== lease.id) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
error: `lease authFile account mismatch: expected ${lease.id}, got ${accountId}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
error: `lease authFile unreadable for account ${lease.id}: ${error?.message || error}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return { ok: true };
|
|
243
|
+
}
|
|
244
|
+
|
|
185
245
|
function dirnameOf(filePath) {
|
|
186
246
|
if (typeof filePath !== "string" || !filePath) return null;
|
|
187
247
|
const lastSep = Math.max(
|
package/hub/server.mjs
CHANGED
|
@@ -64,6 +64,7 @@ import { createDelegatorMcpWorker } from "./workers/delegator-mcp.mjs";
|
|
|
64
64
|
registerTeamBridge(nativeProxy);
|
|
65
65
|
|
|
66
66
|
const hubLog = createModuleLogger("hub");
|
|
67
|
+
const brokerDiagnosticsAttached = new WeakSet();
|
|
67
68
|
|
|
68
69
|
const MAX_BODY_SIZE = 1024 * 1024;
|
|
69
70
|
const PUBLIC_PATHS = new Set(["/", "/status", "/health", "/healthz"]);
|
|
@@ -673,6 +674,45 @@ function getQosStatsPayload() {
|
|
|
673
674
|
};
|
|
674
675
|
}
|
|
675
676
|
|
|
677
|
+
export function sanitizeBrokerDiagnosticEvent(event = {}) {
|
|
678
|
+
const diagnostic = {
|
|
679
|
+
accountId: event.accountId ?? event.id,
|
|
680
|
+
provider: event.provider,
|
|
681
|
+
direction: event.direction,
|
|
682
|
+
reason: event.reason,
|
|
683
|
+
error: event.error,
|
|
684
|
+
};
|
|
685
|
+
return Object.fromEntries(
|
|
686
|
+
Object.entries(diagnostic).filter(([, value]) => value !== undefined),
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function attachBrokerDiagnostics(currentBroker, logger = hubLog) {
|
|
691
|
+
if (
|
|
692
|
+
!currentBroker ||
|
|
693
|
+
typeof currentBroker.on !== "function" ||
|
|
694
|
+
brokerDiagnosticsAttached.has(currentBroker)
|
|
695
|
+
) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
currentBroker.on("securityViolation", (event) => {
|
|
700
|
+
logger.warn(
|
|
701
|
+
{ brokerEvent: sanitizeBrokerDiagnosticEvent(event) },
|
|
702
|
+
"broker.security_violation",
|
|
703
|
+
);
|
|
704
|
+
});
|
|
705
|
+
currentBroker.on("authSyncError", (event) => {
|
|
706
|
+
logger.warn(
|
|
707
|
+
{ brokerEvent: sanitizeBrokerDiagnosticEvent(event) },
|
|
708
|
+
"broker.auth_sync_error",
|
|
709
|
+
);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
brokerDiagnosticsAttached.add(currentBroker);
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
|
|
676
716
|
function syncBrokerAuthCache(currentBroker, logger = hubLog) {
|
|
677
717
|
if (
|
|
678
718
|
!currentBroker?.snapshot ||
|
|
@@ -716,6 +756,13 @@ function syncBrokerAuthCache(currentBroker, logger = hubLog) {
|
|
|
716
756
|
});
|
|
717
757
|
}
|
|
718
758
|
|
|
759
|
+
function getBrokerPublicSnapshot(currentBroker = brokerInstance) {
|
|
760
|
+
if (!currentBroker || typeof currentBroker.publicSnapshot !== "function") {
|
|
761
|
+
return [];
|
|
762
|
+
}
|
|
763
|
+
return currentBroker.publicSnapshot();
|
|
764
|
+
}
|
|
765
|
+
|
|
719
766
|
function resolvePublicFilePath(path) {
|
|
720
767
|
let relativePath = null;
|
|
721
768
|
if (path === "/dashboard") {
|
|
@@ -812,6 +859,7 @@ export async function startHub({
|
|
|
812
859
|
});
|
|
813
860
|
if (existingHub) return existingHub;
|
|
814
861
|
|
|
862
|
+
attachBrokerDiagnostics(brokerInstance);
|
|
815
863
|
syncBrokerAuthCache(brokerInstance);
|
|
816
864
|
|
|
817
865
|
const hubIdleTimeoutMs = parsePositiveInt(
|
|
@@ -1185,7 +1233,7 @@ export async function startHub({
|
|
|
1185
1233
|
}
|
|
1186
1234
|
|
|
1187
1235
|
if (path === "/broker/snapshot" && req.method === "GET") {
|
|
1188
|
-
const snap = brokerInstance
|
|
1236
|
+
const snap = getBrokerPublicSnapshot(brokerInstance);
|
|
1189
1237
|
return writeJson(res, 200, {
|
|
1190
1238
|
ok: true,
|
|
1191
1239
|
accounts: snap,
|
|
@@ -1224,10 +1272,9 @@ export async function startHub({
|
|
|
1224
1272
|
if (!result.ok) {
|
|
1225
1273
|
return writeJson(res, 200, { ok: false, error: result.error });
|
|
1226
1274
|
}
|
|
1275
|
+
attachBrokerDiagnostics(result.broker);
|
|
1227
1276
|
syncBrokerAuthCache(result.broker);
|
|
1228
|
-
const accounts = result.broker
|
|
1229
|
-
? [...result.broker.snapshot()].length
|
|
1230
|
-
: 0;
|
|
1277
|
+
const accounts = getBrokerPublicSnapshot(result.broker).length;
|
|
1231
1278
|
return writeJson(res, 200, { ok: true, accounts });
|
|
1232
1279
|
}
|
|
1233
1280
|
|