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.
Files changed (71) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +34 -42
  4. package/README.md +30 -36
  5. package/bin/triflux.mjs +13 -4
  6. package/hooks/hook-orchestrator.mjs +1 -1
  7. package/hooks/hook-registry.json +76 -2
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/permission-safe-allow.mjs +233 -0
  10. package/hooks/pipeline-stop.mjs +355 -16
  11. package/hooks/post-tool-tips.mjs +306 -0
  12. package/hooks/pre-compact-snapshot.mjs +181 -0
  13. package/hooks/session-end-cleanup.mjs +201 -0
  14. package/hooks/subagent-tracker.mjs +267 -0
  15. package/hooks/subagent-verifier.mjs +4 -0
  16. package/hub/account-broker.mjs +297 -8
  17. package/hub/codex-adapter.mjs +62 -2
  18. package/hub/server.mjs +51 -4
  19. package/hub/team/cli/services/native-control.mjs +1 -0
  20. package/hub/team/conductor.mjs +94 -29
  21. package/hub/team/native-supervisor.mjs +20 -7
  22. package/hub/team/swarm-cli.mjs +98 -4
  23. package/hub/team/swarm-hypervisor.mjs +112 -10
  24. package/hub/team/swarm-locks.mjs +7 -2
  25. package/hub/team/swarm-preflight.mjs +317 -0
  26. package/hub/team/synapse-cli.mjs +243 -6
  27. package/hub/team/worker-sandbox.mjs +93 -0
  28. package/hub/team/worktree-lifecycle.mjs +49 -8
  29. package/hub/workers/delegator-mcp.mjs +14 -1
  30. package/package.json +1 -1
  31. package/scripts/__tests__/gen-skill-docs.test.mjs +4 -0
  32. package/scripts/__tests__/skill-surface.test.mjs +61 -0
  33. package/scripts/gen-skill-manifest.mjs +3 -0
  34. package/scripts/lib/skill-template.mjs +2 -0
  35. package/scripts/session-stale-cleanup.mjs +111 -2
  36. package/skills/star-prompt/skill.json +1 -1
  37. package/skills/tfx-analysis/skill.json +11 -3
  38. package/skills/tfx-auto/skill.json +2 -2
  39. package/skills/tfx-autopilot/skill.json +9 -4
  40. package/skills/tfx-consensus/skill.json +4 -3
  41. package/skills/tfx-debate/skill.json +11 -4
  42. package/skills/tfx-doctor/skill.json +3 -1
  43. package/skills/tfx-fullcycle/skill.json +10 -4
  44. package/skills/tfx-hooks/skill.json +3 -1
  45. package/skills/tfx-hub/skill.json +5 -3
  46. package/skills/tfx-index/skill.json +6 -1
  47. package/skills/tfx-interview/skill.json +6 -1
  48. package/skills/tfx-multi/skill.json +7 -3
  49. package/skills/tfx-panel/skill.json +11 -4
  50. package/skills/tfx-persist/skill.json +11 -4
  51. package/skills/tfx-plan/skill.json +13 -3
  52. package/skills/tfx-profile/skill.json +3 -1
  53. package/skills/tfx-prune/skill.json +7 -1
  54. package/skills/tfx-psmux-rules/SKILL.md +2 -0
  55. package/skills/tfx-psmux-rules/SKILL.md.tmpl +22 -307
  56. package/skills/tfx-psmux-rules/skill.json +7 -2
  57. package/skills/tfx-qa/skill.json +12 -3
  58. package/skills/tfx-ralph/skill.json +4 -1
  59. package/skills/tfx-remote/skill.json +8 -0
  60. package/skills/tfx-remote-setup/SKILL.md +2 -0
  61. package/skills/tfx-remote-setup/SKILL.md.tmpl +23 -570
  62. package/skills/tfx-remote-setup/skill.json +7 -3
  63. package/skills/tfx-remote-spawn/SKILL.md +2 -0
  64. package/skills/tfx-remote-spawn/SKILL.md.tmpl +31 -242
  65. package/skills/tfx-remote-spawn/skill.json +7 -3
  66. package/skills/tfx-research/skill.json +11 -4
  67. package/skills/tfx-review/skill.json +12 -3
  68. package/skills/tfx-setup/skill.json +3 -1
  69. package/skills/tfx-ship/skill.json +13 -0
  70. package/skills/tfx-swarm/skill.json +13 -0
  71. package/skills/tfx-wt/skill.json +12 -0
@@ -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: false,
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: false,
329
- lastUsedAt: saved?.lastUsedAt ?? 0,
330
- totalSessions: saved?.totalSessions ?? 0,
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
- return join(this.#authBasePath, `codex-auth-sync-${acct.id}.lock`);
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
- writeFileSync(destPath, payload);
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 };
@@ -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?.snapshot() || [];
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