agent-embassy 1.6.1 → 1.7.1

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 (105) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +9 -9
  3. package/README.zh-CN.md +9 -9
  4. package/SECURITY.md +34 -77
  5. package/dist/src/gateway/acp-client.d.ts +117 -0
  6. package/dist/src/gateway/acp-client.js +382 -0
  7. package/dist/src/gateway/acp-client.js.map +1 -0
  8. package/dist/src/gateway/acp-provider.d.ts +65 -0
  9. package/dist/src/gateway/acp-provider.js +163 -0
  10. package/dist/src/gateway/acp-provider.js.map +1 -0
  11. package/dist/src/gateway/claude-helper-protocol.d.ts +3 -1
  12. package/dist/src/gateway/claude-helper-protocol.js +15 -5
  13. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  14. package/dist/src/gateway/claude-helper-supervisor.d.ts +3 -1
  15. package/dist/src/gateway/claude-helper-supervisor.js +16 -2
  16. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  17. package/dist/src/gateway/claude-helper.js +4 -3
  18. package/dist/src/gateway/claude-helper.js.map +1 -1
  19. package/dist/src/gateway/claude-peer.d.ts +1 -1
  20. package/dist/src/gateway/claude-peer.js +11 -13
  21. package/dist/src/gateway/claude-peer.js.map +1 -1
  22. package/dist/src/gateway/claude-runtime.d.ts +0 -20
  23. package/dist/src/gateway/claude-runtime.js +6 -127
  24. package/dist/src/gateway/claude-runtime.js.map +1 -1
  25. package/dist/src/gateway/cli-copy.d.ts +1 -1
  26. package/dist/src/gateway/cli-copy.en.d.ts +3 -1
  27. package/dist/src/gateway/cli-copy.en.js +5 -2
  28. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  29. package/dist/src/gateway/cli-copy.js +2 -0
  30. package/dist/src/gateway/cli-copy.js.map +1 -1
  31. package/dist/src/gateway/cli-copy.zh-CN.d.ts +3 -1
  32. package/dist/src/gateway/cli-copy.zh-CN.js +5 -2
  33. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  34. package/dist/src/gateway/cli.d.ts +2 -2
  35. package/dist/src/gateway/cli.js +73 -6
  36. package/dist/src/gateway/cli.js.map +1 -1
  37. package/dist/src/gateway/codex-app-server.d.ts +5 -74
  38. package/dist/src/gateway/codex-app-server.js +9 -599
  39. package/dist/src/gateway/codex-app-server.js.map +1 -1
  40. package/dist/src/gateway/codex-doctor.d.ts +33 -0
  41. package/dist/src/gateway/codex-doctor.js +148 -0
  42. package/dist/src/gateway/codex-doctor.js.map +1 -0
  43. package/dist/src/gateway/codex-local-transport.d.ts +13 -26
  44. package/dist/src/gateway/codex-local-transport.js +31 -76
  45. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  46. package/dist/src/gateway/compatibility.d.ts +6 -66
  47. package/dist/src/gateway/compatibility.js +14 -296
  48. package/dist/src/gateway/compatibility.js.map +1 -1
  49. package/dist/src/gateway/config.d.ts +9 -0
  50. package/dist/src/gateway/config.js +6 -2
  51. package/dist/src/gateway/config.js.map +1 -1
  52. package/dist/src/gateway/control.d.ts +16 -1
  53. package/dist/src/gateway/control.js +128 -59
  54. package/dist/src/gateway/control.js.map +1 -1
  55. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  56. package/dist/src/gateway/dashboard-copy.en.d.ts +19 -44
  57. package/dist/src/gateway/dashboard-copy.en.js +42 -67
  58. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  59. package/dist/src/gateway/dashboard-copy.js +19 -44
  60. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  61. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +19 -44
  62. package/dist/src/gateway/dashboard-copy.zh-CN.js +42 -67
  63. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  64. package/dist/src/gateway/dashboard-model.d.ts +41 -76
  65. package/dist/src/gateway/dashboard-model.js +167 -257
  66. package/dist/src/gateway/dashboard-model.js.map +1 -1
  67. package/dist/src/gateway/dashboard.d.ts +0 -3
  68. package/dist/src/gateway/dashboard.js +77 -76
  69. package/dist/src/gateway/dashboard.js.map +1 -1
  70. package/dist/src/gateway/deepseek-detect.d.ts +7 -6
  71. package/dist/src/gateway/deepseek-detect.js +31 -115
  72. package/dist/src/gateway/deepseek-detect.js.map +1 -1
  73. package/dist/src/gateway/live-dashboard-app/app.js +175 -621
  74. package/dist/src/gateway/live-dashboard-command.js +2 -4
  75. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  76. package/dist/src/gateway/live-dashboard-http.d.ts +1 -2
  77. package/dist/src/gateway/live-dashboard-http.js +14 -15
  78. package/dist/src/gateway/live-dashboard-http.js.map +1 -1
  79. package/dist/src/gateway/progress-watch-machine.d.ts +1 -4
  80. package/dist/src/gateway/progress-watch-machine.js.map +1 -1
  81. package/dist/src/gateway/provenance-envelope.d.ts +3 -2
  82. package/dist/src/gateway/provenance-envelope.js +44 -11
  83. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  84. package/dist/src/gateway/providers.d.ts +19 -85
  85. package/dist/src/gateway/providers.js +132 -835
  86. package/dist/src/gateway/providers.js.map +1 -1
  87. package/dist/src/gateway/server.d.ts +8 -9
  88. package/dist/src/gateway/server.js +63 -99
  89. package/dist/src/gateway/server.js.map +1 -1
  90. package/dist/src/gateway/service.d.ts +48 -51
  91. package/dist/src/gateway/service.js +748 -540
  92. package/dist/src/gateway/service.js.map +1 -1
  93. package/dist/src/gateway/store.d.ts +20 -29
  94. package/dist/src/gateway/store.js +306 -838
  95. package/dist/src/gateway/store.js.map +1 -1
  96. package/dist/src/gateway/types.d.ts +63 -54
  97. package/dist/src/gateway/types.js +51 -91
  98. package/dist/src/gateway/types.js.map +1 -1
  99. package/docs/CONFIGURATION.md +11 -10
  100. package/docs/CONFIGURATION.zh-CN.md +10 -9
  101. package/docs/DASHBOARD.md +4 -38
  102. package/docs/DASHBOARD.zh-CN.md +4 -2
  103. package/docs/GATEWAY-ARCHITECTURE.md +61 -119
  104. package/package.json +1 -1
  105. package/skills/embassy-peer/SKILL.md +6 -6
@@ -6,9 +6,8 @@ import path from "node:path";
6
6
  import { BridgeError } from "../errors.js";
7
7
  import { KeyedMutex } from "../mutex.js";
8
8
  import { isCodexRegistrationGeneration } from "./codex-registration-generation.js";
9
- import { compatibilityCacheKey, isCompatibilityAttestation, isPersistedCompatibilityAttestation, } from "./compatibility.js";
10
9
  import { PROGRESS_WATCH_DEFAULT_CAPACITY, PROGRESS_WATCH_HARD_CAPACITY, PROGRESS_WATCH_MAX_IDLE_MS, PROGRESS_WATCH_MIN_IDLE_MS, commitProgressWatchNudge, createProgressWatch, deferProgressWatchNudge, inspectProgressWatchDue, progressWatchJournalKinds, recordProgressWatchActivity, } from "./progress-watch-machine.js";
11
- import { CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY, CODEX_ORPHAN_REMOVAL_JOURNAL_CAPACITY, compatibilityStates, connectorHealthStates, deliveryStates, gatewayPublicSnapshotLimits, gatewayProviders, messageDirections, projectGatewayPublicSnapshot, routeRegistrationModes, routeStates, } from "./types.js";
10
+ import { CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY, CODEX_ORPHAN_REMOVAL_JOURNAL_CAPACITY, CONNECTOR_OBSERVATION_STALE_AFTER_MS, connectorHealthStates, directionId, deliveryStates, gatewayPublicSnapshotLimits, gatewayProviders, gatewayRegistrationIngressPrefixes, messageDirections, parseDirection, projectGatewayPublicSnapshot, routeRegistrationModes, routeStates, } from "./types.js";
12
11
  const STATE_MARKER = ".agent-embassy-state";
13
12
  const STATE_MARKER_CONTENT = "agent-embassy-state-v1\n";
14
13
  const STATE_FILE = "gateway-state.json";
@@ -17,7 +16,6 @@ const MAX_MARKER_FILE_BYTES = 128;
17
16
  const MAX_LOCK_FILE_BYTES = 4 * 1024;
18
17
  export const GATEWAY_MAX_STATE_FILE_BYTES = 8 * 1024 * 1024;
19
18
  const DEFAULT_RETAINED_BODY_BYTES = 1 * 1024 * 1024;
20
- const COMPATIBILITY_ATTESTATION_CAPACITY = 16;
21
19
  const ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
22
20
  const HOST_PATTERN = /^[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
23
21
  const PRIVATE_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
@@ -39,7 +37,6 @@ class PostRenamePersistenceError extends BridgeError {
39
37
  }
40
38
  const PROVIDERS = new Set(gatewayProviders);
41
39
  const CONNECTOR_HEALTH = new Set(connectorHealthStates);
42
- const COMPATIBILITY = new Set(compatibilityStates);
43
40
  const ROUTE_STATES = new Set(routeStates);
44
41
  const REGISTRATION_MODES = new Set(routeRegistrationModes);
45
42
  const DIRECTIONS = new Set(messageDirections);
@@ -221,7 +218,6 @@ function isRouteRecord(value) {
221
218
  "registrationMode",
222
219
  "enabled",
223
220
  "state",
224
- "compatibility",
225
221
  "busyPolicy",
226
222
  "registeredAt",
227
223
  "updatedAt",
@@ -238,8 +234,6 @@ function isRouteRecord(value) {
238
234
  typeof value.enabled === "boolean" &&
239
235
  typeof value.state === "string" &&
240
236
  ROUTE_STATES.has(value.state) &&
241
- typeof value.compatibility === "string" &&
242
- COMPATIBILITY.has(value.compatibility) &&
243
237
  value.busyPolicy === "queue" &&
244
238
  isIsoTimestamp(value.registeredAt) &&
245
239
  isIsoTimestamp(value.updatedAt) &&
@@ -248,23 +242,19 @@ function isRouteRecord(value) {
248
242
  isRouteCounters(value.counters) &&
249
243
  (value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)));
250
244
  }
251
- function isPairRecord(value) {
245
+ function isConsentEndpoint(value) {
252
246
  return (isObject(value) &&
253
- hasOnlyKeys(value, [
254
- "claudeAlias",
255
- "codexAlias",
256
- "claudeOwnerLease",
257
- "codexOwnerLease",
258
- "createdAt",
259
- "updatedAt",
260
- "counters",
261
- ]) &&
262
- typeof value.claudeAlias === "string" &&
263
- ALIAS_PATTERN.test(value.claudeAlias) &&
264
- typeof value.codexAlias === "string" &&
265
- ALIAS_PATTERN.test(value.codexAlias) &&
266
- isPrivateToken(value.claudeOwnerLease) &&
267
- isPrivateToken(value.codexOwnerLease) &&
247
+ hasOnlyKeys(value, ["alias", "provider", "ownerLease"]) &&
248
+ typeof value.alias === "string" && ALIAS_PATTERN.test(value.alias) &&
249
+ typeof value.provider === "string" && PROVIDERS.has(value.provider) &&
250
+ isPrivateToken(value.ownerLease));
251
+ }
252
+ function isConsentEdgeRecord(value) {
253
+ return (isObject(value) &&
254
+ hasOnlyKeys(value, ["endpoints", "createdAt", "updatedAt", "counters"]) &&
255
+ Array.isArray(value.endpoints) &&
256
+ value.endpoints.length === 2 &&
257
+ value.endpoints.every(isConsentEndpoint) &&
268
258
  isIsoTimestamp(value.createdAt) &&
269
259
  isIsoTimestamp(value.updatedAt) &&
270
260
  isRouteCounters(value.counters));
@@ -277,15 +267,16 @@ function progressWatchPairKey(watch) {
277
267
  .sort()
278
268
  .join("\0");
279
269
  }
280
- function progressWatchMatchesPair(watch, pair) {
281
- return ((watch.ownerAlias === pair.claudeAlias &&
282
- watch.ownerLease === pair.claudeOwnerLease &&
283
- watch.workerAlias === pair.codexAlias &&
284
- watch.workerLease === pair.codexOwnerLease) ||
285
- (watch.ownerAlias === pair.codexAlias &&
286
- watch.ownerLease === pair.codexOwnerLease &&
287
- watch.workerAlias === pair.claudeAlias &&
288
- watch.workerLease === pair.claudeOwnerLease));
270
+ function progressWatchMatchesConsentEdge(watch, pair) {
271
+ const [left, right] = pair.endpoints;
272
+ return ((watch.ownerAlias === left.alias &&
273
+ watch.ownerLease === left.ownerLease &&
274
+ watch.workerAlias === right.alias &&
275
+ watch.workerLease === right.ownerLease) ||
276
+ (watch.ownerAlias === right.alias &&
277
+ watch.ownerLease === right.ownerLease &&
278
+ watch.workerAlias === left.alias &&
279
+ watch.workerLease === left.ownerLease));
289
280
  }
290
281
  function progressWatchJournalEvent(watch, timestamp, transition) {
291
282
  return {
@@ -459,11 +450,10 @@ function isProgressWatchJournalEvent(value) {
459
450
  return value.actor === "gateway" || value.actor === "operator";
460
451
  }
461
452
  if (value.reason === "idle_timeout" ||
462
- value.reason === "tracking_disabled" ||
463
- value.reason === "legacy_upgrade") {
453
+ value.reason === "tracking_disabled") {
464
454
  return value.actor === "gateway";
465
455
  }
466
- return value.reason === "legacy_done" && value.actor === "unknown";
456
+ return false;
467
457
  }
468
458
  function isConnectorRecord(value) {
469
459
  if (!isObject(value) ||
@@ -472,7 +462,6 @@ function isConnectorRecord(value) {
472
462
  "hostId",
473
463
  "endpointGeneration",
474
464
  "health",
475
- "compatibility",
476
465
  "protocol",
477
466
  "protocolVersion",
478
467
  "updatedAt",
@@ -486,8 +475,6 @@ function isConnectorRecord(value) {
486
475
  }) &&
487
476
  typeof value.health === "string" &&
488
477
  CONNECTOR_HEALTH.has(value.health) &&
489
- typeof value.compatibility === "string" &&
490
- COMPATIBILITY.has(value.compatibility) &&
491
478
  typeof value.protocol === "string" &&
492
479
  PROTOCOL_PATTERN.test(value.protocol) &&
493
480
  typeof value.protocolVersion === "string" &&
@@ -628,444 +615,6 @@ function isAccounting(value) {
628
615
  }
629
616
  return Object.values(value).every(isNonNegativeInteger);
630
617
  }
631
- const PRE_UNCONFIRMED_ROUTE_COUNTER_KEYS = [
632
- "accepted",
633
- "delivered",
634
- "failed",
635
- "ambiguous",
636
- "expired",
637
- "cancelled",
638
- "abandoned",
639
- "rejected",
640
- "bytesAccepted",
641
- ];
642
- const PRE_UNCONFIRMED_ACCOUNTING_KEYS = [
643
- "accepted",
644
- "duplicates",
645
- "delivered",
646
- "failed",
647
- "ambiguous",
648
- "expired",
649
- "cancelled",
650
- "abandoned",
651
- "rejected",
652
- "bytesAccepted",
653
- "queuedBytes",
654
- ];
655
- /**
656
- * One narrow migration for unpublished dogfood state created before the
657
- * `unconfirmed` terminal outcome existed. Atomic state replacement means a
658
- * valid old file has either every old counter shape or none of them; mixed or
659
- * otherwise unfamiliar shapes continue to fail strict validation.
660
- */
661
- function migratePreUnconfirmedCounters(value) {
662
- if (!isObject(value) ||
663
- value.schemaVersion !== 1 ||
664
- !isObject(value.accounting) ||
665
- !hasOnlyKeys(value.accounting, PRE_UNCONFIRMED_ACCOUNTING_KEYS) ||
666
- Object.hasOwn(value.accounting, "unconfirmed") ||
667
- !Array.isArray(value.routes) ||
668
- !value.routes.every((route) => isObject(route) &&
669
- isObject(route.counters) &&
670
- hasOnlyKeys(route.counters, PRE_UNCONFIRMED_ROUTE_COUNTER_KEYS) &&
671
- !Object.hasOwn(route.counters, "unconfirmed"))) {
672
- return value;
673
- }
674
- return {
675
- ...value,
676
- accounting: { ...value.accounting, unconfirmed: 0 },
677
- routes: value.routes.map((route) => {
678
- const record = route;
679
- return {
680
- ...record,
681
- counters: {
682
- ...record.counters,
683
- unconfirmed: 0,
684
- },
685
- };
686
- }),
687
- };
688
- }
689
- const PRE_SUCCESSION_JOURNAL_STATE_KEYS = [
690
- "schemaVersion",
691
- "createdAt",
692
- "updatedAt",
693
- "eventSequence",
694
- "routes",
695
- "connectors",
696
- "queue",
697
- "inFlight",
698
- "events",
699
- "dedupe",
700
- "rateBuckets",
701
- "accounting",
702
- ];
703
- /** Add the nullable internal journal to exact legacy v1 state only. */
704
- function migratePreSuccessionJournal(value) {
705
- if (!isObject(value) ||
706
- value.schemaVersion !== 1 ||
707
- !hasOnlyKeys(value, PRE_SUCCESSION_JOURNAL_STATE_KEYS)) {
708
- return value;
709
- }
710
- return { ...value, codexSuccession: null };
711
- }
712
- const PRE_PAIR_GRAPH_STATE_KEYS = [
713
- ...PRE_SUCCESSION_JOURNAL_STATE_KEYS,
714
- "codexSuccession",
715
- ];
716
- /**
717
- * Preserve the old explicit singleton-selection consent when adding the pair
718
- * graph. This migration accepts only the exact prior v1 top-level shape; final
719
- * state validation still proves every route, lease, host, and edge.
720
- */
721
- function migratePrePairGraph(value) {
722
- if (!isObject(value) ||
723
- value.schemaVersion !== 1 ||
724
- !hasOnlyKeys(value, PRE_PAIR_GRAPH_STATE_KEYS) ||
725
- !Array.isArray(value.routes) ||
726
- !isIsoTimestamp(value.updatedAt)) {
727
- return value;
728
- }
729
- const routes = value.routes.filter(isObject);
730
- const claudeRoutes = routes.filter((route) => route.registrationMode === "selected_live_peer" &&
731
- isObject(route.binding) &&
732
- route.binding.provider === "claude");
733
- const codexRoutes = routes.filter((route) => route.registrationMode === "explicit_opt_in" &&
734
- isObject(route.binding) &&
735
- route.binding.provider === "codex");
736
- const pairs = [];
737
- for (const claude of claudeRoutes) {
738
- for (const codex of codexRoutes) {
739
- const claudeBinding = claude.binding;
740
- const codexBinding = codex.binding;
741
- if (claudeBinding.hostId !== codexBinding.hostId ||
742
- typeof claude.alias !== "string" ||
743
- typeof codex.alias !== "string" ||
744
- typeof claudeBinding.ownerLease !== "string" ||
745
- typeof codexBinding.ownerLease !== "string") {
746
- continue;
747
- }
748
- pairs.push({
749
- claudeAlias: claude.alias,
750
- codexAlias: codex.alias,
751
- claudeOwnerLease: claudeBinding.ownerLease,
752
- codexOwnerLease: codexBinding.ownerLease,
753
- createdAt: value.updatedAt,
754
- updatedAt: value.updatedAt,
755
- counters: emptyCounters(),
756
- });
757
- }
758
- }
759
- if (pairs.length > 256)
760
- return value;
761
- return { ...value, pairs };
762
- }
763
- const PRE_PROGRESS_WATCH_STATE_KEYS = [
764
- ...PRE_PAIR_GRAPH_STATE_KEYS,
765
- "pairs",
766
- ];
767
- /** Add empty opt-in watch state to the exact preceding v1 schema only. */
768
- function migratePreProgressWatches(value) {
769
- if (!isObject(value) ||
770
- value.schemaVersion !== 1 ||
771
- !hasOnlyKeys(value, PRE_PROGRESS_WATCH_STATE_KEYS)) {
772
- return value;
773
- }
774
- return {
775
- ...value,
776
- watchSequence: 0,
777
- progressWatches: [],
778
- progressWatchEvents: [],
779
- };
780
- }
781
- const PRE_COMPATIBILITY_ATTESTATION_STATE_KEYS = [
782
- ...PRE_PROGRESS_WATCH_STATE_KEYS,
783
- "watchSequence",
784
- "progressWatches",
785
- "progressWatchEvents",
786
- ];
787
- /** Add an empty compatibility cache to the exact preceding v1 schema only. */
788
- function migratePreCompatibilityAttestations(value) {
789
- if (!isObject(value) ||
790
- value.schemaVersion !== 1 ||
791
- !hasOnlyKeys(value, PRE_COMPATIBILITY_ATTESTATION_STATE_KEYS)) {
792
- return value;
793
- }
794
- return { ...value, compatibilityAttestations: [] };
795
- }
796
- const PRE_CODEX_ENDPOINT_REFRESH_STATE_KEYS = [
797
- ...PRE_COMPATIBILITY_ATTESTATION_STATE_KEYS,
798
- "compatibilityAttestations",
799
- ];
800
- /** Add an empty bounded private endpoint-refresh journal to exact legacy v1. */
801
- function migratePreCodexEndpointRefreshJournal(value) {
802
- if (!isObject(value) ||
803
- value.schemaVersion !== 1 ||
804
- !hasOnlyKeys(value, PRE_CODEX_ENDPOINT_REFRESH_STATE_KEYS)) {
805
- return value;
806
- }
807
- return {
808
- ...value,
809
- codexEndpointRefreshSequence: 0,
810
- codexEndpointRefreshEvents: [],
811
- };
812
- }
813
- const PRE_CODEX_ORPHAN_REMOVAL_STATE_KEYS = [
814
- ...PRE_CODEX_ENDPOINT_REFRESH_STATE_KEYS,
815
- "codexEndpointRefreshSequence",
816
- "codexEndpointRefreshEvents",
817
- ];
818
- /** Add an empty private orphan-removal journal to exact preceding v1 state. */
819
- function migratePreCodexOrphanRemovalJournal(value) {
820
- if (!isObject(value) ||
821
- value.schemaVersion !== 1 ||
822
- !hasOnlyKeys(value, PRE_CODEX_ORPHAN_REMOVAL_STATE_KEYS)) {
823
- return value;
824
- }
825
- return {
826
- ...value,
827
- codexOrphanRemovalSequence: 0,
828
- codexOrphanRemovalEvents: [],
829
- };
830
- }
831
- /**
832
- * v1.2.0 persisted hop counters on queue, in-flight, and event rows. Hop
833
- * policy no longer exists; accept only the old bounded shape and discard the
834
- * obsolete field before strict current-schema validation and persistence.
835
- */
836
- function migrateLegacyHopCounts(value) {
837
- if (!isObject(value) ||
838
- value.schemaVersion !== 1 ||
839
- !Array.isArray(value.queue) ||
840
- !Array.isArray(value.inFlight) ||
841
- !Array.isArray(value.events)) {
842
- return value;
843
- }
844
- const strip = (entry) => {
845
- if (!isObject(entry) || !Object.hasOwn(entry, "hopCount"))
846
- return entry;
847
- if (!isNonNegativeInteger(entry.hopCount) || entry.hopCount > 64) {
848
- return entry;
849
- }
850
- const { hopCount: _legacyHopCount, ...current } = entry;
851
- return current;
852
- };
853
- return {
854
- ...value,
855
- queue: value.queue.map(strip),
856
- inFlight: value.inFlight.map(strip),
857
- events: value.events.map(strip),
858
- };
859
- }
860
- function isLegacyCompatibilityCertification(value) {
861
- if (!isObject(value))
862
- return false;
863
- const required = ["depth", "outcome", "certifiedAt"];
864
- if (!hasOnlyKeys(value, required, ["safeErrorCode"]))
865
- return false;
866
- if ((value.depth !== "wire" &&
867
- value.depth !== "thread_ops" &&
868
- value.depth !== "turn") ||
869
- (value.outcome !== "pass" && value.outcome !== "fail") ||
870
- !isIsoTimestamp(value.certifiedAt) ||
871
- new Date(Date.parse(value.certifiedAt)).toISOString() !==
872
- value.certifiedAt ||
873
- (value.safeErrorCode !== undefined && !isSafeCode(value.safeErrorCode))) {
874
- return false;
875
- }
876
- return (value.outcome === "pass") === (value.safeErrorCode === undefined);
877
- }
878
- /**
879
- * v1.1-v1.2 compatibility rows could contain manual live-certification
880
- * evidence. Discard only an exact old certification, retain the automatic
881
- * bounded probes as schema-attested evidence, and leave malformed rows
882
- * untouched for strict state validation to reject.
883
- */
884
- function migrateLegacyCompatibilityCertifications(value) {
885
- if (!isObject(value) ||
886
- value.schemaVersion !== 1 ||
887
- !Array.isArray(value.compatibilityAttestations)) {
888
- return value;
889
- }
890
- let changed = false;
891
- const compatibilityAttestations = value.compatibilityAttestations.map((entry) => {
892
- let candidate = entry;
893
- if (isObject(entry) &&
894
- Object.hasOwn(entry, "certification") &&
895
- isLegacyCompatibilityCertification(entry.certification)) {
896
- const { certification: _legacyCertification, ...current } = entry;
897
- candidate =
898
- current.tier === "certified"
899
- ? { ...current, tier: "schema_attested" }
900
- : current;
901
- }
902
- if (!isPersistedCompatibilityAttestation(candidate))
903
- return entry;
904
- if (candidate !== entry)
905
- changed = true;
906
- return candidate;
907
- });
908
- return changed ? { ...value, compatibilityAttestations } : value;
909
- }
910
- function legacyProgressWatchGraphIsValid(value, watches) {
911
- if (!isObject(value) ||
912
- !Array.isArray(value.routes) ||
913
- !value.routes.every(isRouteRecord) ||
914
- !Array.isArray(value.pairs) ||
915
- value.pairs.length > 256 ||
916
- !value.pairs.every(isPairRecord) ||
917
- new Set(watches.map((watch) => watch.conversationId)).size !==
918
- watches.length ||
919
- new Set(watches.map(progressWatchPairKey)).size !== watches.length) {
920
- return false;
921
- }
922
- const routes = value.routes;
923
- const pairs = value.pairs;
924
- for (const watch of watches) {
925
- const owner = routes.find((route) => route.alias === watch.ownerAlias &&
926
- route.binding.ownerLease === watch.ownerLease);
927
- const worker = routes.find((route) => route.alias === watch.workerAlias &&
928
- route.binding.ownerLease === watch.workerLease);
929
- if (owner === undefined ||
930
- worker === undefined ||
931
- owner.binding.provider === worker.binding.provider ||
932
- owner.binding.hostId !== worker.binding.hostId) {
933
- return false;
934
- }
935
- const claude = owner.binding.provider === "claude" ? owner : worker;
936
- const codex = owner.binding.provider === "codex" ? owner : worker;
937
- if (!pairs.some((pair) => pair.claudeAlias === claude.alias &&
938
- pair.codexAlias === codex.alias &&
939
- pair.claudeOwnerLease === claude.binding.ownerLease &&
940
- pair.codexOwnerLease === codex.binding.ownerLease)) {
941
- return false;
942
- }
943
- }
944
- return true;
945
- }
946
- function mapLegacyProgressWatchEvent(event) {
947
- const base = {
948
- timestamp: event.timestamp,
949
- conversationId: event.conversationId,
950
- ownerAlias: event.ownerAlias,
951
- workerAlias: event.workerAlias,
952
- };
953
- switch (event.kind) {
954
- case "opened":
955
- return { ...base, kind: "opened", actor: "owner" };
956
- case "replaced":
957
- // v1.4 used this kind for both owner replacement and an automatic
958
- // duplicate migration. Its actor cannot be reconstructed honestly.
959
- return { ...base, kind: "replaced", actor: "unknown" };
960
- case "worker_reported_complete":
961
- return { ...base, kind: "settled", actor: "worker", reason: "done" };
962
- case "done":
963
- return {
964
- ...base,
965
- kind: "settled",
966
- actor: "unknown",
967
- reason: "legacy_done",
968
- };
969
- case "unresponsive":
970
- return {
971
- ...base,
972
- kind: "settled",
973
- actor: "gateway",
974
- reason: "idle_timeout",
975
- };
976
- case "pair_removed":
977
- return {
978
- ...base,
979
- kind: "settled",
980
- actor: "operator",
981
- reason: "pair_removed",
982
- };
983
- case "endpoint_retired":
984
- return {
985
- ...base,
986
- kind: "settled",
987
- actor: "gateway",
988
- reason: "endpoint_retired",
989
- };
990
- case "disabled":
991
- return {
992
- ...base,
993
- kind: "settled",
994
- actor: "gateway",
995
- reason: "tracking_disabled",
996
- };
997
- case "activity":
998
- case "nudge":
999
- case "capability_degraded":
1000
- case "conversation_rebound":
1001
- return undefined;
1002
- }
1003
- }
1004
- /**
1005
- * v1.4.x watches carried capability and phase state that no longer has product
1006
- * meaning. Strictly recognize the complete old graph, preserve representable
1007
- * history, and settle every live row once at upgrade. Rebase the bounded
1008
- * private sequence so a historical counter cannot make the new format fatal.
1009
- */
1010
- function migrateV14ProgressWatchState(value, migratedAt) {
1011
- if (!isObject(value) ||
1012
- value.schemaVersion !== 1 ||
1013
- !isIsoTimestamp(value.updatedAt) ||
1014
- !isIsoTimestamp(migratedAt) ||
1015
- !isNonNegativeInteger(value.watchSequence) ||
1016
- !Array.isArray(value.progressWatches) ||
1017
- !Array.isArray(value.progressWatchEvents)) {
1018
- return value;
1019
- }
1020
- const hasLegacyRows = value.progressWatches.some(isLegacyV14ProgressWatch) ||
1021
- value.progressWatchEvents.some(isLegacyV14ProgressWatchJournalEvent);
1022
- if (!hasLegacyRows)
1023
- return value;
1024
- if (value.progressWatches.length > PROGRESS_WATCH_HARD_CAPACITY ||
1025
- !value.progressWatches.every(isLegacyV14ProgressWatch) ||
1026
- value.progressWatchEvents.length > gatewayPublicSnapshotLimits.messages ||
1027
- !value.progressWatchEvents.every(isLegacyV14ProgressWatchJournalEvent)) {
1028
- return value;
1029
- }
1030
- const watches = value.progressWatches;
1031
- const legacyEvents = value.progressWatchEvents;
1032
- const watchSequence = value.watchSequence;
1033
- if (!legacyProgressWatchGraphIsValid(value, watches) ||
1034
- legacyEvents.some((event, index) => event.sequence > watchSequence ||
1035
- (index > 0 && event.sequence <= legacyEvents[index - 1].sequence))) {
1036
- return value;
1037
- }
1038
- const pending = [];
1039
- for (const event of legacyEvents) {
1040
- const mapped = mapLegacyProgressWatchEvent(event);
1041
- if (mapped !== undefined)
1042
- pending.push(mapped);
1043
- }
1044
- const settlements = watches.map((watch) => ({
1045
- timestamp: migratedAt,
1046
- conversationId: watch.conversationId,
1047
- ownerAlias: watch.ownerAlias,
1048
- workerAlias: watch.workerAlias,
1049
- kind: "settled",
1050
- actor: "gateway",
1051
- reason: "legacy_upgrade",
1052
- }));
1053
- const retainedHistoryCount = gatewayPublicSnapshotLimits.messages - settlements.length;
1054
- const retained = [
1055
- ...pending.slice(Math.max(0, pending.length - retainedHistoryCount)),
1056
- ...settlements,
1057
- ];
1058
- const progressWatchEvents = retained.map((event, index) => ({
1059
- ...event,
1060
- sequence: index + 1,
1061
- }));
1062
- return {
1063
- ...value,
1064
- watchSequence: progressWatchEvents.length,
1065
- progressWatches: [],
1066
- progressWatchEvents,
1067
- };
1068
- }
1069
618
  function isPersistedState(value) {
1070
619
  if (!isObject(value) ||
1071
620
  !hasOnlyKeys(value, [
@@ -1074,7 +623,7 @@ function isPersistedState(value) {
1074
623
  "updatedAt",
1075
624
  "eventSequence",
1076
625
  "routes",
1077
- "pairs",
626
+ "consentEdges",
1078
627
  "connectors",
1079
628
  "queue",
1080
629
  "inFlight",
@@ -1085,14 +634,13 @@ function isPersistedState(value) {
1085
634
  "watchSequence",
1086
635
  "progressWatches",
1087
636
  "progressWatchEvents",
1088
- "compatibilityAttestations",
1089
637
  "codexEndpointRefreshSequence",
1090
638
  "codexEndpointRefreshEvents",
1091
639
  "codexOrphanRemovalSequence",
1092
640
  "codexOrphanRemovalEvents",
1093
641
  "codexSuccession",
1094
642
  ]) ||
1095
- value.schemaVersion !== 1 ||
643
+ value.schemaVersion !== 2 ||
1096
644
  !isIsoTimestamp(value.createdAt) ||
1097
645
  !isIsoTimestamp(value.updatedAt) ||
1098
646
  !isNonNegativeInteger(value.eventSequence) ||
@@ -1101,19 +649,15 @@ function isPersistedState(value) {
1101
649
  !isNonNegativeInteger(value.codexOrphanRemovalSequence) ||
1102
650
  !Array.isArray(value.routes) ||
1103
651
  !value.routes.every(isRouteRecord) ||
1104
- !Array.isArray(value.pairs) ||
1105
- value.pairs.length > 256 ||
1106
- !value.pairs.every(isPairRecord) ||
652
+ !Array.isArray(value.consentEdges) ||
653
+ value.consentEdges.length > gatewayPublicSnapshotLimits.consentEdges ||
654
+ !value.consentEdges.every(isConsentEdgeRecord) ||
1107
655
  !Array.isArray(value.progressWatches) ||
1108
656
  value.progressWatches.length > PROGRESS_WATCH_HARD_CAPACITY ||
1109
657
  !value.progressWatches.every(isProgressWatch) ||
1110
658
  !Array.isArray(value.progressWatchEvents) ||
1111
659
  value.progressWatchEvents.length > gatewayPublicSnapshotLimits.messages ||
1112
660
  !value.progressWatchEvents.every(isProgressWatchJournalEvent) ||
1113
- !Array.isArray(value.compatibilityAttestations) ||
1114
- value.compatibilityAttestations.length >
1115
- COMPATIBILITY_ATTESTATION_CAPACITY ||
1116
- !value.compatibilityAttestations.every(isPersistedCompatibilityAttestation) ||
1117
661
  !Array.isArray(value.codexEndpointRefreshEvents) ||
1118
662
  value.codexEndpointRefreshEvents.length >
1119
663
  CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY ||
@@ -1156,10 +700,6 @@ function isPersistedState(value) {
1156
700
  const watchPairKeys = new Set(candidate.progressWatches.map(progressWatchPairKey));
1157
701
  if (watchPairKeys.size !== candidate.progressWatches.length)
1158
702
  return false;
1159
- const compatibilityKeys = candidate.compatibilityAttestations.map(compatibilityCacheKey);
1160
- if (new Set(compatibilityKeys).size !== compatibilityKeys.length) {
1161
- return false;
1162
- }
1163
703
  for (const watch of candidate.progressWatches) {
1164
704
  const owner = candidate.routes.find((route) => route.alias === watch.ownerAlias &&
1165
705
  route.binding.ownerLease === watch.ownerLease);
@@ -1171,17 +711,10 @@ function isPersistedState(value) {
1171
711
  owner.binding.hostId !== worker.binding.hostId) {
1172
712
  return false;
1173
713
  }
1174
- const pairAliases = pairAliasesForRoutes(owner, worker);
1175
- if (!candidate.pairs.some((pair) => pair.claudeAlias === pairAliases.claudeAlias &&
1176
- pair.codexAlias === pairAliases.codexAlias &&
1177
- pair.claudeOwnerLease ===
1178
- (owner.binding.provider === "claude"
1179
- ? owner.binding.ownerLease
1180
- : worker.binding.ownerLease) &&
1181
- pair.codexOwnerLease ===
1182
- (owner.binding.provider === "codex"
1183
- ? owner.binding.ownerLease
1184
- : worker.binding.ownerLease))) {
714
+ const endpoints = canonicalConsentEndpoints(owner, worker);
715
+ if (!candidate.consentEdges.some((edge) => edge.endpoints.every((endpoint, index) => endpoint.alias === endpoints[index].alias &&
716
+ endpoint.provider === endpoints[index].provider &&
717
+ endpoint.ownerLease === endpoints[index].ownerLease))) {
1185
718
  return false;
1186
719
  }
1187
720
  }
@@ -1192,15 +725,18 @@ function isPersistedState(value) {
1192
725
  ];
1193
726
  const expectedQueuedBytes = candidate.queue.reduce((total, item) => total + item.bytes, 0);
1194
727
  const routeByAlias = new Map(candidate.routes.map((route) => [route.alias, route]));
1195
- const pairKeys = candidate.pairs.map((pair) => `${pair.claudeAlias}\0${pair.codexAlias}`);
1196
- const pairsValid = candidate.pairs.every((pair) => {
1197
- const claude = routeByAlias.get(pair.claudeAlias);
1198
- const codex = routeByAlias.get(pair.codexAlias);
1199
- return (claude?.binding.provider === "claude" &&
1200
- codex?.binding.provider === "codex" &&
1201
- claude.binding.hostId === codex.binding.hostId &&
1202
- claude.binding.ownerLease === pair.claudeOwnerLease &&
1203
- codex.binding.ownerLease === pair.codexOwnerLease);
728
+ const edgeKeys = candidate.consentEdges.map((edge) => consentEdgeKey(edge.endpoints));
729
+ const edgesValid = candidate.consentEdges.every((edge) => {
730
+ const [leftEndpoint, rightEndpoint] = edge.endpoints;
731
+ const left = routeByAlias.get(leftEndpoint.alias);
732
+ const right = routeByAlias.get(rightEndpoint.alias);
733
+ return (compareConsentEndpoints(leftEndpoint, rightEndpoint) < 0 &&
734
+ left?.binding.provider === leftEndpoint.provider &&
735
+ right?.binding.provider === rightEndpoint.provider &&
736
+ left.binding.provider !== right.binding.provider &&
737
+ left.binding.hostId === right.binding.hostId &&
738
+ left.binding.ownerLease === leftEndpoint.ownerLease &&
739
+ right.binding.ownerLease === rightEndpoint.ownerLease);
1204
740
  });
1205
741
  const claudeConnectorHosts = new Set(candidate.connectors
1206
742
  .filter((connector) => connector.provider === "claude")
@@ -1209,22 +745,23 @@ function isPersistedState(value) {
1209
745
  const isValidPersistedMessagePair = (message) => {
1210
746
  const source = routeByAlias.get(message.sourceAlias);
1211
747
  const target = routeByAlias.get(message.targetAlias);
1212
- const routeShapeValid = message.direction === "codex_to_claude"
1213
- ? source?.binding.provider === "codex" &&
1214
- ((target?.binding.provider === "claude" &&
1215
- target.binding.hostId === source.binding.hostId) ||
1216
- (target === undefined &&
1217
- claudeConnectorHosts.has(source.binding.hostId) &&
1218
- aliasHost(message.targetAlias) === source.binding.hostId))
1219
- : target?.binding.provider === "codex" &&
1220
- ((source?.binding.provider === "claude" &&
1221
- source.binding.hostId === target.binding.hostId) ||
1222
- (source === undefined &&
1223
- claudeConnectorHosts.has(target.binding.hostId) &&
1224
- aliasHost(message.sourceAlias) === target.binding.hostId));
748
+ const parsed = parseDirection(message.direction);
749
+ if (parsed === undefined)
750
+ return false;
751
+ const sourceValid = source?.binding.provider === parsed.sourceProvider ||
752
+ (source === undefined && parsed.sourceProvider === "claude" && target !== undefined &&
753
+ claudeConnectorHosts.has(target.binding.hostId) &&
754
+ aliasHost(message.sourceAlias) === target.binding.hostId);
755
+ const targetValid = target?.binding.provider === parsed.targetProvider ||
756
+ (target === undefined && parsed.targetProvider === "claude" && source !== undefined &&
757
+ claudeConnectorHosts.has(source.binding.hostId) &&
758
+ aliasHost(message.targetAlias) === source.binding.hostId);
759
+ const routeShapeValid = sourceValid && targetValid &&
760
+ (source === undefined || target === undefined ||
761
+ source.binding.hostId === target.binding.hostId);
1225
762
  return (routeShapeValid &&
1226
763
  (message.pair !== true ||
1227
- candidate.pairs.some((pair) => pairMatchesMessage(pair, message))));
764
+ candidate.consentEdges.some((edge) => consentEdgeMatchesMessage(edge, message))));
1228
765
  };
1229
766
  const sequencesStrictlyIncrease = candidate.events.every((event, index) => index === 0 ||
1230
767
  event.sequence > (candidate.events[index - 1]?.sequence ?? 0));
@@ -1250,8 +787,8 @@ function isPersistedState(value) {
1250
787
  (candidate.codexOrphanRemovalEvents[index - 1]?.sequence ?? 0) +
1251
788
  1);
1252
789
  return (aliases.size === candidate.routes.length &&
1253
- new Set(pairKeys).size === pairKeys.length &&
1254
- pairsValid &&
790
+ new Set(edgeKeys).size === edgeKeys.length &&
791
+ edgesValid &&
1255
792
  routeTargets.size === candidate.routes.length &&
1256
793
  ownerLeases.size === candidate.routes.length &&
1257
794
  connectorKeys.size === candidate.connectors.length &&
@@ -1267,10 +804,12 @@ function isPersistedState(value) {
1267
804
  endpointRefreshSequenceConsistent &&
1268
805
  orphanRemovalSequenceConsistent &&
1269
806
  candidate.routes.every((route) => route.alias.endsWith(`@${route.binding.hostId}`) &&
1270
- ((route.binding.provider === "codex" &&
1271
- route.registrationMode === "explicit_opt_in") ||
1272
- (route.binding.provider === "claude" &&
1273
- route.registrationMode === "selected_live_peer")) &&
807
+ ((route.binding.provider === "claude" &&
808
+ route.registrationMode === "selected_live_peer") ||
809
+ (route.binding.provider !== "claude" &&
810
+ route.registrationMode === "explicit_opt_in")) &&
811
+ (gatewayRegistrationIngressPrefixes[route.binding.provider] === undefined ||
812
+ route.alias.startsWith(gatewayRegistrationIngressPrefixes[route.binding.provider])) &&
1274
813
  route.queueDepth ===
1275
814
  candidate.queue.filter((item) => item.targetAlias === route.alias)
1276
815
  .length) &&
@@ -1281,8 +820,7 @@ function isPersistedState(value) {
1281
820
  }) &&
1282
821
  candidate.dedupe.every(isValidPersistedMessagePair) &&
1283
822
  candidate.rateBuckets.every((bucket) => aliases.has(bucket.sourceAlias) ||
1284
- candidate.routes.some((route) => route.binding.provider === "codex" &&
1285
- claudeConnectorHosts.has(route.binding.hostId) &&
823
+ candidate.routes.some((route) => claudeConnectorHosts.has(route.binding.hostId) &&
1286
824
  route.binding.hostId === aliasHost(bucket.sourceAlias))) &&
1287
825
  isPersistedSuccessionConsistent(candidate));
1288
826
  }
@@ -1466,41 +1004,46 @@ function renameRateBucket(state, previousAlias, newAlias) {
1466
1004
  existing.count = Math.min(Number.MAX_SAFE_INTEGER, existing.count + previous.count);
1467
1005
  state.rateBuckets = state.rateBuckets.filter((bucket) => bucket !== previous);
1468
1006
  }
1469
- function pairKey(claudeAlias, codexAlias) {
1470
- return `${claudeAlias}\0${codexAlias}`;
1007
+ function compareConsentEndpoints(left, right) {
1008
+ return gatewayProviders.indexOf(left.provider) -
1009
+ gatewayProviders.indexOf(right.provider) || left.alias.localeCompare(right.alias);
1471
1010
  }
1472
- function pairAliasesForRoutes(source, target) {
1473
- if (source.binding.provider === "claude" &&
1474
- target.binding.provider === "codex") {
1475
- return { claudeAlias: source.alias, codexAlias: target.alias };
1476
- }
1477
- if (source.binding.provider === "codex" &&
1478
- target.binding.provider === "claude") {
1479
- return { claudeAlias: target.alias, codexAlias: source.alias };
1480
- }
1481
- throw new BridgeError("INVALID_GATEWAY_ROUTE_PAIR", "Gateway pairs must connect one Claude route and one Codex route.");
1011
+ function canonicalConsentEndpoints(source, target) {
1012
+ const endpoints = [
1013
+ { alias: source.alias, provider: source.binding.provider,
1014
+ ownerLease: source.binding.ownerLease },
1015
+ { alias: target.alias, provider: target.binding.provider,
1016
+ ownerLease: target.binding.ownerLease },
1017
+ ];
1018
+ endpoints.sort(compareConsentEndpoints);
1019
+ return endpoints;
1482
1020
  }
1483
- function pairMatchesMessage(pair, message) {
1484
- return (message.pair === true &&
1485
- ((message.sourceAlias === pair.claudeAlias &&
1486
- message.targetAlias === pair.codexAlias) ||
1487
- (message.sourceAlias === pair.codexAlias &&
1488
- message.targetAlias === pair.claudeAlias)));
1021
+ function consentEdgeKey(endpoints) {
1022
+ return endpoints.map(({ provider, alias }) => `${provider}\0${alias}`).join("\0");
1489
1023
  }
1490
- function findPairForMessage(state, message) {
1491
- return state.pairs.find((pair) => pairMatchesMessage(pair, message));
1024
+ function consentEdgeMatchesMessage(pair, message) {
1025
+ const aliases = "endpoints" in pair
1026
+ ? pair.endpoints.map((endpoint) => endpoint.alias)
1027
+ : pair.aliases;
1028
+ return message.pair === true &&
1029
+ ((message.sourceAlias === aliases[0] && message.targetAlias === aliases[1]) ||
1030
+ (message.sourceAlias === aliases[1] && message.targetAlias === aliases[0]));
1492
1031
  }
1493
- function renamePairAlias(state, previousAlias, newAlias, now) {
1032
+ function findConsentEdgeForMessage(state, message) {
1033
+ return state.consentEdges.find((edge) => consentEdgeMatchesMessage(edge, message));
1034
+ }
1035
+ function renameConsentEdgeAlias(state, previousAlias, newAlias, now) {
1494
1036
  if (previousAlias === newAlias)
1495
1037
  return;
1496
- for (const pair of state.pairs) {
1497
- if (pair.claudeAlias === previousAlias)
1498
- pair.claudeAlias = newAlias;
1499
- if (pair.codexAlias === previousAlias)
1500
- pair.codexAlias = newAlias;
1501
- if (pair.claudeAlias === newAlias ||
1502
- pair.codexAlias === newAlias) {
1503
- pair.updatedAt = now.toISOString();
1038
+ for (const edge of state.consentEdges) {
1039
+ const endpoint = edge.endpoints.find(({ alias }) => alias === previousAlias);
1040
+ if (endpoint !== undefined) {
1041
+ const renamed = edge.endpoints.map((candidate) => candidate.alias === previousAlias
1042
+ ? { ...candidate, alias: newAlias }
1043
+ : candidate);
1044
+ renamed.sort(compareConsentEndpoints);
1045
+ edge.endpoints = renamed;
1046
+ edge.updatedAt = now.toISOString();
1504
1047
  }
1505
1048
  }
1506
1049
  }
@@ -1513,8 +1056,8 @@ function renameProgressWatchAlias(state, previousAlias, newAlias) {
1513
1056
  ? { ...watch, workerAlias: newAlias }
1514
1057
  : watch);
1515
1058
  }
1516
- function removePairsForAliases(state, aliases) {
1517
- state.pairs = state.pairs.filter((pair) => !aliases.has(pair.claudeAlias) && !aliases.has(pair.codexAlias));
1059
+ function removeConsentEdgesForAliases(state, aliases) {
1060
+ state.consentEdges = state.consentEdges.filter((edge) => !edge.endpoints.some(({ alias }) => aliases.has(alias)));
1518
1061
  }
1519
1062
  function routeTerminationMatches(state, aliases, message) {
1520
1063
  for (const alias of [message.sourceAlias, message.targetAlias]) {
@@ -1528,15 +1071,10 @@ function routeTerminationMatches(state, aliases, message) {
1528
1071
  return false;
1529
1072
  }
1530
1073
  function directionFor(source, target) {
1531
- if (source.binding.provider === "codex" &&
1532
- target.binding.provider === "claude") {
1533
- return "codex_to_claude";
1534
- }
1535
- if (source.binding.provider === "claude" &&
1536
- target.binding.provider === "codex") {
1537
- return "claude_to_codex";
1074
+ if (source.binding.provider !== target.binding.provider) {
1075
+ return directionId(source.binding.provider, target.binding.provider);
1538
1076
  }
1539
- throw new BridgeError("INVALID_GATEWAY_ROUTE_PAIR", "Gateway messages must cross from one provider to the other.");
1077
+ throw new BridgeError("INVALID_GATEWAY_ROUTE_PAIR", "Gateway messages must cross between distinct providers.");
1540
1078
  }
1541
1079
  export class GatewayStore {
1542
1080
  config;
@@ -1549,11 +1087,7 @@ export class GatewayStore {
1549
1087
  state;
1550
1088
  lockHandle;
1551
1089
  lockToken;
1552
- /**
1553
- * Startup admission may exercise migrated state in memory, but it must not
1554
- * strand an older binary by rewriting the durable schema before every
1555
- * provider and control-socket readiness check has passed.
1556
- */
1090
+ /** Startup may defer the first native-v2 persistence until admission passes. */
1557
1091
  persistenceDeferred = false;
1558
1092
  constructor(config, dependencies = {}) {
1559
1093
  this.config = config;
@@ -1579,12 +1113,12 @@ export class GatewayStore {
1579
1113
  const now = this.now();
1580
1114
  const loaded = await this.loadStateFile(now);
1581
1115
  this.state = loaded ?? {
1582
- schemaVersion: 1,
1116
+ schemaVersion: 2,
1583
1117
  createdAt: now.toISOString(),
1584
1118
  updatedAt: now.toISOString(),
1585
1119
  eventSequence: 0,
1586
1120
  routes: [],
1587
- pairs: [],
1121
+ consentEdges: [],
1588
1122
  connectors: [],
1589
1123
  queue: [],
1590
1124
  inFlight: [],
@@ -1595,15 +1129,14 @@ export class GatewayStore {
1595
1129
  watchSequence: 0,
1596
1130
  progressWatches: [],
1597
1131
  progressWatchEvents: [],
1598
- compatibilityAttestations: [],
1599
1132
  codexEndpointRefreshSequence: 0,
1600
1133
  codexEndpointRefreshEvents: [],
1601
1134
  codexOrphanRemovalSequence: 0,
1602
1135
  codexOrphanRemovalEvents: [],
1603
1136
  codexSuccession: null,
1604
1137
  };
1605
- if (this.state.pairs.length > this.config.limits.maxPairs) {
1606
- throw new BridgeError("PAIR_CAPACITY_REACHED", "The durable pair inventory exceeds the configured bound.");
1138
+ if (this.state.consentEdges.length > this.config.limits.maxConsentEdges) {
1139
+ throw new BridgeError("CONSENT_EDGE_CAPACITY_REACHED", "The durable consent-edge inventory exceeds the configured bound.");
1607
1140
  }
1608
1141
  if (this.state.progressWatches.length >
1609
1142
  (this.config.limits.maxWatches ?? PROGRESS_WATCH_DEFAULT_CAPACITY)) {
@@ -1661,7 +1194,6 @@ export class GatewayStore {
1661
1194
  const observed = {
1662
1195
  ...input.identity,
1663
1196
  health: input.health,
1664
- compatibility: input.compatibility,
1665
1197
  protocol: input.protocol,
1666
1198
  protocolVersion: input.protocolVersion,
1667
1199
  updatedAt: now.toISOString(),
@@ -1671,7 +1203,8 @@ export class GatewayStore {
1671
1203
  if (existingIndex >= 0)
1672
1204
  state.connectors[existingIndex] = observed;
1673
1205
  else {
1674
- if (state.connectors.length >= this.config.allowedHosts.length * 2) {
1206
+ if (state.connectors.length >=
1207
+ this.config.allowedHosts.length * gatewayProviders.length) {
1675
1208
  throw new BridgeError("CONNECTOR_CAPACITY_REACHED", "The bounded provider and host connector registry is full.");
1676
1209
  }
1677
1210
  state.connectors.push(observed);
@@ -1702,7 +1235,6 @@ export class GatewayStore {
1702
1235
  if (!sameEndpoint(route.binding, identity))
1703
1236
  continue;
1704
1237
  route.state = route.enabled ? "stale" : "disabled";
1705
- route.compatibility = "expired";
1706
1238
  route.updatedAt = now.toISOString();
1707
1239
  route.safeErrorCode = safeErrorCode;
1708
1240
  }
@@ -1715,9 +1247,8 @@ export class GatewayStore {
1715
1247
  this.assertAllowedIdentity(endpointOf(input.binding));
1716
1248
  const connector = state.connectors.find((candidate) => sameEndpoint(candidate, input.binding));
1717
1249
  if (!connector ||
1718
- !["healthy", "degraded"].includes(connector.health) ||
1719
- connector.compatibility !== "compatible") {
1720
- throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The exact compatible endpoint generation must be observed before a route can be registered.");
1250
+ !["healthy", "degraded"].includes(connector.health)) {
1251
+ throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The exact live endpoint generation must be observed before a route can be registered.");
1721
1252
  }
1722
1253
  const byAlias = state.routes.find((route) => route.alias === input.alias);
1723
1254
  const byBinding = state.routes.find((route) => sameRouteTarget(route.binding, input.binding));
@@ -1730,7 +1261,6 @@ export class GatewayStore {
1730
1261
  }
1731
1262
  byAlias.enabled = true;
1732
1263
  byAlias.state = input.state ?? "idle";
1733
- byAlias.compatibility = "compatible";
1734
1264
  byAlias.updatedAt = now.toISOString();
1735
1265
  byAlias.lastSeenAt = now.toISOString();
1736
1266
  delete byAlias.safeErrorCode;
@@ -1751,7 +1281,6 @@ export class GatewayStore {
1751
1281
  registrationMode: input.registrationMode,
1752
1282
  enabled: true,
1753
1283
  state: input.state ?? "idle",
1754
- compatibility: "compatible",
1755
1284
  busyPolicy: "queue",
1756
1285
  registeredAt: now.toISOString(),
1757
1286
  updatedAt: now.toISOString(),
@@ -1778,9 +1307,8 @@ export class GatewayStore {
1778
1307
  }
1779
1308
  const connector = state.connectors.find((candidate) => sameEndpoint(candidate, replacement.binding));
1780
1309
  if (!connector ||
1781
- !["healthy", "degraded"].includes(connector.health) ||
1782
- connector.compatibility !== "compatible") {
1783
- throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The exact compatible endpoint generation must be observed before a Claude selection can replace another.");
1310
+ !["healthy", "degraded"].includes(connector.health)) {
1311
+ throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The exact live endpoint generation must be observed before a Claude selection can replace another.");
1784
1312
  }
1785
1313
  const selectedClaudeRoutes = state.routes.filter((route) => route.binding.provider === "claude" &&
1786
1314
  route.registrationMode === "selected_live_peer");
@@ -1793,9 +1321,7 @@ export class GatewayStore {
1793
1321
  const retained = logicalMatches[0];
1794
1322
  if (retained !== undefined &&
1795
1323
  !sameBinding(retained.binding, replacement.binding) &&
1796
- (!retained.enabled ||
1797
- retained.state !== "stale" ||
1798
- retained.compatibility !== "expired")) {
1324
+ (!retained.enabled || retained.state !== "stale")) {
1799
1325
  throw new BridgeError("ROUTE_REBIND_IDENTITY_MISMATCH", "Only a stale exact Claude session may adopt a newly observed endpoint generation during replacement.");
1800
1326
  }
1801
1327
  const aliasOwner = state.routes.find((route) => route.alias === replacement.alias);
@@ -1818,7 +1344,7 @@ export class GatewayStore {
1818
1344
  this.settleProgressWatchesForAliases(state, retiredAliases, now);
1819
1345
  const settlements = this.terminateAffectedMessages(state, retiredAliases, now, "ROUTE_UNREGISTERED", settlementPlan);
1820
1346
  state.routes = state.routes.filter((route) => !retired.includes(route));
1821
- removePairsForAliases(state, retiredAliases);
1347
+ removeConsentEdgesForAliases(state, retiredAliases);
1822
1348
  state.rateBuckets = state.rateBuckets.filter((bucket) => !retiredAliases.has(bucket.sourceAlias));
1823
1349
  state.dedupe = state.dedupe.filter((record) => !retiredAliases.has(record.sourceAlias) &&
1824
1350
  !retiredAliases.has(record.targetAlias));
@@ -1828,7 +1354,6 @@ export class GatewayStore {
1828
1354
  retained.binding = { ...replacement.binding };
1829
1355
  retained.enabled = true;
1830
1356
  retained.state = replacement.state ?? "idle";
1831
- retained.compatibility = "compatible";
1832
1357
  retained.updatedAt = now.toISOString();
1833
1358
  retained.lastSeenAt = now.toISOString();
1834
1359
  delete retained.safeErrorCode;
@@ -1850,7 +1375,7 @@ export class GatewayStore {
1850
1375
  }
1851
1376
  }
1852
1377
  renameRateBucket(state, previousAlias, replacement.alias);
1853
- renamePairAlias(state, previousAlias, replacement.alias, now);
1378
+ renameConsentEdgeAlias(state, previousAlias, replacement.alias, now);
1854
1379
  renameProgressWatchAlias(state, previousAlias, replacement.alias);
1855
1380
  }
1856
1381
  }
@@ -1864,7 +1389,6 @@ export class GatewayStore {
1864
1389
  registrationMode: replacement.registrationMode,
1865
1390
  enabled: true,
1866
1391
  state: replacement.state ?? "idle",
1867
- compatibility: "compatible",
1868
1392
  busyPolicy: "queue",
1869
1393
  registeredAt: now.toISOString(),
1870
1394
  updatedAt: now.toISOString(),
@@ -1896,16 +1420,13 @@ export class GatewayStore {
1896
1420
  throw new BridgeError("ROUTE_OWNERSHIP_MISMATCH", "No enabled route matches the exact private binding and ownership lease.");
1897
1421
  }
1898
1422
  const endpointObservationValid = input.state === "stale"
1899
- ? connector?.health === "degraded" &&
1900
- connector.compatibility === "expired"
1423
+ ? connector?.health === "degraded"
1901
1424
  : connector !== undefined &&
1902
- ["healthy", "degraded"].includes(connector.health) &&
1903
- connector.compatibility === "compatible";
1425
+ ["healthy", "degraded"].includes(connector.health);
1904
1426
  if (!endpointObservationValid) {
1905
- throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The route observation must match the exact endpoint generation's current health and compatibility.");
1427
+ throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The route observation must match the exact endpoint generation's current health.");
1906
1428
  }
1907
1429
  route.state = input.state;
1908
- route.compatibility = input.compatibility;
1909
1430
  route.lastSeenAt = now.toISOString();
1910
1431
  route.updatedAt = now.toISOString();
1911
1432
  if (input.safeErrorCode)
@@ -1928,7 +1449,6 @@ export class GatewayStore {
1928
1449
  const aliases = new Set([route.alias]);
1929
1450
  const settlementPlan = this.validateAffectedInFlightSettlements(state, aliases, inFlightSettlements);
1930
1451
  route.state = "stale";
1931
- route.compatibility = "expired";
1932
1452
  route.updatedAt = now.toISOString();
1933
1453
  route.safeErrorCode = safeErrorCode;
1934
1454
  return this.terminateAffectedMessages(state, aliases, now, safeErrorCode, settlementPlan);
@@ -1955,7 +1475,6 @@ export class GatewayStore {
1955
1475
  const route = this.requireOwnedRoute(state, input.alias, input.currentOwnerLease);
1956
1476
  if (!route.enabled ||
1957
1477
  route.state !== "stale" ||
1958
- route.compatibility !== "expired" ||
1959
1478
  state.inFlight.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias)) {
1960
1479
  throw new BridgeError("ROUTE_REBIND_NOT_SAFE", "Only an enabled, expired stale route without in-flight work can be rebound.");
1961
1480
  }
@@ -1979,9 +1498,8 @@ export class GatewayStore {
1979
1498
  }
1980
1499
  const connector = state.connectors.find((candidate) => sameEndpoint(candidate, input.newBinding));
1981
1500
  if (!connector ||
1982
- !["healthy", "degraded"].includes(connector.health) ||
1983
- connector.compatibility !== "compatible") {
1984
- throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The replacement endpoint generation must be positively observed and compatible.");
1501
+ !["healthy", "degraded"].includes(connector.health)) {
1502
+ throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The replacement endpoint generation must be positively observed and live.");
1985
1503
  }
1986
1504
  if (state.routes.some((candidate) => candidate !== route &&
1987
1505
  (candidate.alias === newAlias ||
@@ -1998,7 +1516,6 @@ export class GatewayStore {
1998
1516
  route.binding = { ...input.newBinding };
1999
1517
  route.alias = newAlias;
2000
1518
  route.state = input.state ?? "idle";
2001
- route.compatibility = "compatible";
2002
1519
  route.updatedAt = now.toISOString();
2003
1520
  route.lastSeenAt = now.toISOString();
2004
1521
  delete route.safeErrorCode;
@@ -2020,7 +1537,7 @@ export class GatewayStore {
2020
1537
  }
2021
1538
  }
2022
1539
  renameRateBucket(state, previousAlias, newAlias);
2023
- renamePairAlias(state, previousAlias, newAlias, now);
1540
+ renameConsentEdgeAlias(state, previousAlias, newAlias, now);
2024
1541
  }
2025
1542
  if (input.journalReason === "boot_reactivation") {
2026
1543
  this.appendCodexEndpointRefreshEvent(state, {
@@ -2058,9 +1575,8 @@ export class GatewayStore {
2058
1575
  this.assertAllowedIdentity(input.newEndpoint);
2059
1576
  const newConnector = state.connectors.find((candidate) => sameEndpoint(candidate, input.newEndpoint));
2060
1577
  if (newConnector === undefined ||
2061
- !["healthy", "degraded"].includes(newConnector.health) ||
2062
- newConnector.compatibility !== "compatible") {
2063
- throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The replacement Codex endpoint generation must be positively observed and compatible.");
1578
+ !["healthy", "degraded"].includes(newConnector.health)) {
1579
+ throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The replacement Codex endpoint generation must be positively observed and live.");
2064
1580
  }
2065
1581
  const aliases = new Set();
2066
1582
  const threadIds = new Set();
@@ -2095,7 +1611,6 @@ export class GatewayStore {
2095
1611
  route.registrationMode !== "explicit_opt_in" ||
2096
1612
  !route.enabled ||
2097
1613
  route.state !== "stale" ||
2098
- route.compatibility !== "expired" ||
2099
1614
  !sameEndpoint(route.binding, input.oldEndpoint) ||
2100
1615
  route.binding.routeHandle !== candidate.threadId ||
2101
1616
  route.binding.ownerLease !== candidate.ownerLease ||
@@ -2125,7 +1640,6 @@ export class GatewayStore {
2125
1640
  endpointGeneration: input.newEndpoint.endpointGeneration,
2126
1641
  };
2127
1642
  replacement.route.state = replacement.state;
2128
- replacement.route.compatibility = "compatible";
2129
1643
  replacement.route.updatedAt = now.toISOString();
2130
1644
  replacement.route.lastSeenAt = now.toISOString();
2131
1645
  delete replacement.route.safeErrorCode;
@@ -2155,18 +1669,15 @@ export class GatewayStore {
2155
1669
  }
2156
1670
  const route = this.requireStaleCodexOrphan(state, input.alias);
2157
1671
  const aliases = new Set([route.alias]);
2158
- const removedPairs = state.pairs
2159
- .filter((pair) => pair.claudeAlias === route.alias || pair.codexAlias === route.alias)
2160
- .map((pair) => ({
2161
- claudeAlias: pair.claudeAlias,
2162
- codexAlias: pair.codexAlias,
2163
- }));
1672
+ const removedEdges = state.consentEdges
1673
+ .filter((edge) => edge.endpoints.some(({ alias }) => alias === route.alias))
1674
+ .map((edge) => ({ aliases: edge.endpoints.map(({ alias }) => alias) }));
2164
1675
  if (state.codexOrphanRemovalSequence >= Number.MAX_SAFE_INTEGER) {
2165
1676
  throw new BridgeError("CODEX_ORPHAN_REMOVAL_SEQUENCE_EXHAUSTED", "The bounded Codex orphan-removal sequence is exhausted.");
2166
1677
  }
2167
1678
  this.settleProgressWatchesForAliases(state, aliases, now);
2168
1679
  state.routes = state.routes.filter((candidate) => candidate !== route);
2169
- removePairsForAliases(state, aliases);
1680
+ removeConsentEdgesForAliases(state, aliases);
2170
1681
  state.rateBuckets = state.rateBuckets.filter((bucket) => bucket.sourceAlias !== route.alias);
2171
1682
  state.dedupe = state.dedupe.filter((record) => record.sourceAlias !== route.alias &&
2172
1683
  record.targetAlias !== route.alias);
@@ -2178,7 +1689,7 @@ export class GatewayStore {
2178
1689
  return {
2179
1690
  alias: route.alias,
2180
1691
  binding: { ...route.binding },
2181
- removedPairs,
1692
+ removedEdges,
2182
1693
  };
2183
1694
  });
2184
1695
  }
@@ -2201,7 +1712,7 @@ export class GatewayStore {
2201
1712
  this.settleProgressWatchesForAliases(state, aliases, now, progressWatchSettlementActor);
2202
1713
  const settlements = this.terminateAffectedMessages(state, aliases, now, "ROUTE_UNREGISTERED", settlementPlan);
2203
1714
  state.routes = state.routes.filter((candidate) => candidate !== route);
2204
- removePairsForAliases(state, aliases);
1715
+ removeConsentEdgesForAliases(state, aliases);
2205
1716
  const remainingCodexHosts = new Set(state.routes
2206
1717
  .filter((candidate) => candidate.binding.provider === "codex")
2207
1718
  .map((candidate) => candidate.binding.hostId));
@@ -2246,7 +1757,7 @@ export class GatewayStore {
2246
1757
  record.targetAlias = newAlias;
2247
1758
  }
2248
1759
  renameRateBucket(state, alias, newAlias);
2249
- renamePairAlias(state, alias, newAlias, now);
1760
+ renameConsentEdgeAlias(state, alias, newAlias, now);
2250
1761
  renameProgressWatchAlias(state, alias, newAlias);
2251
1762
  });
2252
1763
  }
@@ -2262,8 +1773,7 @@ export class GatewayStore {
2262
1773
  }
2263
1774
  if (!route ||
2264
1775
  !route.enabled ||
2265
- !["idle", "busy", "awaiting_approval"].includes(route.state) ||
2266
- route.compatibility !== "compatible") {
1776
+ !["idle", "busy", "awaiting_approval"].includes(route.state)) {
2267
1777
  throw new BridgeError("ROUTE_UNAVAILABLE", "The selected route is not currently enabled and positively observed.", true);
2268
1778
  }
2269
1779
  return { ...route.binding };
@@ -2289,7 +1799,6 @@ export class GatewayStore {
2289
1799
  registrationMode: route.registrationMode,
2290
1800
  enabled: route.enabled,
2291
1801
  state: route.state,
2292
- compatibility: route.compatibility,
2293
1802
  ...(route.safeErrorCode === undefined
2294
1803
  ? {}
2295
1804
  : { safeErrorCode: route.safeErrorCode }),
@@ -2372,7 +1881,6 @@ export class GatewayStore {
2372
1881
  registrationMode: route.registrationMode,
2373
1882
  enabled: route.enabled,
2374
1883
  state: route.state,
2375
- compatibility: route.compatibility,
2376
1884
  }));
2377
1885
  });
2378
1886
  }
@@ -2432,7 +1940,7 @@ export class GatewayStore {
2432
1940
  if (!routeMatchesSuccessionIdentity(route, input.old)) {
2433
1941
  throw new BridgeError("CODEX_SUCCESSION_OWNER_MISMATCH", "The old succession identity does not exactly own its Codex route.");
2434
1942
  }
2435
- this.requireCompatibleObservedEndpoint(state, input.new.binding);
1943
+ this.requireObservedEndpoint(state, input.new.binding);
2436
1944
  this.assertNewSuccessionIdentityAvailable(state, route, input.new);
2437
1945
  state.codexSuccession = {
2438
1946
  schemaVersion: 1,
@@ -2477,9 +1985,9 @@ export class GatewayStore {
2477
1985
  }, ["published"]);
2478
1986
  this.assertSuccessionLedgerEmpty(state, journal.old.alias);
2479
1987
  const route = this.requireJournalRoute(state, journal, "old");
2480
- this.requireCompatibleObservedEndpoint(state, journal.new.binding);
1988
+ this.requireObservedEndpoint(state, journal.new.binding);
2481
1989
  this.assertNewSuccessionIdentityAvailable(state, route, journal.new);
2482
- this.replaceCodexRouteForSuccession(state, route, journal.new, now, input.state, "compatible");
1990
+ this.replaceCodexRouteForSuccession(state, route, journal.new, now, input.state);
2483
1991
  state.codexSuccession = { ...journal, stage: "activated" };
2484
1992
  });
2485
1993
  }
@@ -2583,30 +2091,27 @@ export class GatewayStore {
2583
2091
  registrationMode: route.registrationMode,
2584
2092
  enabled: route.enabled,
2585
2093
  state: route.state,
2586
- compatibility: route.compatibility,
2587
2094
  }));
2588
2095
  });
2589
2096
  }
2590
- /** Create one explicit, durable Claude↔Codex consent edge. */
2591
- async pairRoutes(input) {
2097
+ /** Create one explicit, durable consent edge between distinct providers. */
2098
+ async addConsentEdge(input) {
2592
2099
  return this.mutate(async (state, now) => {
2593
- const { claude, codex } = this.requirePairRoutes(state, input);
2594
- const existing = state.pairs.find((pair) => pair.claudeAlias === claude.alias && pair.codexAlias === codex.alias);
2100
+ const [left, right] = this.requireConsentEdgeRoutes(state, input);
2101
+ const endpoints = canonicalConsentEndpoints(left, right);
2102
+ const key = consentEdgeKey(endpoints);
2103
+ const existing = state.consentEdges.find((edge) => consentEdgeKey(edge.endpoints) === key);
2595
2104
  if (existing !== undefined) {
2596
- if (existing.claudeOwnerLease !== claude.binding.ownerLease ||
2597
- existing.codexOwnerLease !== codex.binding.ownerLease) {
2598
- throw new BridgeError("PAIR_AUTHORITY_MISMATCH", "The pair aliases no longer match their exact route authorities.");
2105
+ if (existing.endpoints.some((endpoint, index) => endpoint.ownerLease !== endpoints[index].ownerLease)) {
2106
+ throw new BridgeError("CONSENT_EDGE_AUTHORITY_MISMATCH", "The edge aliases no longer match their exact route authorities.");
2599
2107
  }
2600
2108
  return { created: false };
2601
2109
  }
2602
- if (state.pairs.length >= this.config.limits.maxPairs) {
2603
- throw new BridgeError("PAIR_CAPACITY_REACHED", "The bounded permission graph cannot accept another pair.", true);
2110
+ if (state.consentEdges.length >= this.config.limits.maxConsentEdges) {
2111
+ throw new BridgeError("CONSENT_EDGE_CAPACITY_REACHED", "The bounded permission graph cannot accept another consent edge.", true);
2604
2112
  }
2605
- state.pairs.push({
2606
- claudeAlias: claude.alias,
2607
- codexAlias: codex.alias,
2608
- claudeOwnerLease: claude.binding.ownerLease,
2609
- codexOwnerLease: codex.binding.ownerLease,
2113
+ state.consentEdges.push({
2114
+ endpoints,
2610
2115
  createdAt: now.toISOString(),
2611
2116
  updatedAt: now.toISOString(),
2612
2117
  counters: emptyCounters(),
@@ -2615,12 +2120,14 @@ export class GatewayStore {
2615
2120
  });
2616
2121
  }
2617
2122
  /** Bounded metadata-only graph inventory for controller inference. */
2618
- async inspectPairs() {
2123
+ async inspectConsentEdges() {
2619
2124
  return this.mutex.run("gateway", async () => {
2620
2125
  const state = this.requireState();
2621
- return state.pairs
2622
- .map(({ claudeAlias, codexAlias }) => ({ claudeAlias, codexAlias }))
2623
- .sort((left, right) => pairKey(left.claudeAlias, left.codexAlias).localeCompare(pairKey(right.claudeAlias, right.codexAlias)));
2126
+ return state.consentEdges
2127
+ .map(({ endpoints }) => ({
2128
+ aliases: endpoints.map(({ alias }) => alias),
2129
+ }))
2130
+ .sort((left, right) => left.aliases.join("\0").localeCompare(right.aliases.join("\0")));
2624
2131
  });
2625
2132
  }
2626
2133
  /** Resolve only an exact live watch; recovery may substitute its suffix. */
@@ -2647,11 +2154,11 @@ export class GatewayStore {
2647
2154
  watch.workerAlias === input.sourceAlias)));
2648
2155
  if (watch === undefined)
2649
2156
  return { markerActive: false };
2650
- const hasExactPair = state.pairs.some((candidate) => pairMatchesMessage(candidate, {
2157
+ const hasExactPair = state.consentEdges.some((candidate) => consentEdgeMatchesMessage(candidate, {
2651
2158
  sourceAlias: input.sourceAlias,
2652
2159
  targetAlias: input.targetAlias,
2653
2160
  pair: true,
2654
- }) && progressWatchMatchesPair(watch, candidate));
2161
+ }) && progressWatchMatchesConsentEdge(watch, candidate));
2655
2162
  if (!hasExactPair)
2656
2163
  return { markerActive: false };
2657
2164
  return {
@@ -2661,38 +2168,6 @@ export class GatewayStore {
2661
2168
  };
2662
2169
  });
2663
2170
  }
2664
- async inspectCompatibilityAttestation(surface, version) {
2665
- return this.mutex.run("gateway", async () => {
2666
- const attestation = this.requireState().compatibilityAttestations.find((candidate) => candidate.surface === surface && candidate.version === version);
2667
- return attestation === undefined
2668
- ? undefined
2669
- : structuredClone(attestation);
2670
- });
2671
- }
2672
- async inspectCompatibilityAttestations() {
2673
- return this.mutex.run("gateway", async () => this.requireState().compatibilityAttestations
2674
- .map((attestation) => structuredClone(attestation))
2675
- .sort((left, right) => compatibilityCacheKey(left).localeCompare(compatibilityCacheKey(right))));
2676
- }
2677
- async recordCompatibilityAttestation(attestation) {
2678
- if (!isCompatibilityAttestation(attestation)) {
2679
- throw new BridgeError("COMPAT_ATTESTATION_INVALID", "Compatibility evidence failed strict schema validation.");
2680
- }
2681
- await this.mutate(async (state) => {
2682
- const key = compatibilityCacheKey(attestation);
2683
- const index = state.compatibilityAttestations.findIndex((candidate) => compatibilityCacheKey(candidate) === key);
2684
- if (index >= 0) {
2685
- state.compatibilityAttestations[index] = structuredClone(attestation);
2686
- return;
2687
- }
2688
- if (state.compatibilityAttestations.length >=
2689
- COMPATIBILITY_ATTESTATION_CAPACITY) {
2690
- state.compatibilityAttestations.sort((left, right) => left.checkedAt.localeCompare(right.checkedAt));
2691
- state.compatibilityAttestations.shift();
2692
- }
2693
- state.compatibilityAttestations.push(structuredClone(attestation));
2694
- });
2695
- }
2696
2171
  async touchProgressWatchesForAlias(alias) {
2697
2172
  return this.mutate(async (state, now) => {
2698
2173
  if (!ALIAS_PATTERN.test(alias))
@@ -2788,15 +2263,15 @@ export class GatewayStore {
2788
2263
  return true;
2789
2264
  });
2790
2265
  }
2791
- async hasPair(input) {
2266
+ async hasConsentEdge(input) {
2792
2267
  return this.mutex.run("gateway", async () => {
2793
2268
  const state = this.requireState();
2794
2269
  try {
2795
- const { claude, codex } = this.requirePairRoutes(state, input, false);
2796
- return state.pairs.some((pair) => pair.claudeAlias === claude.alias &&
2797
- pair.codexAlias === codex.alias &&
2798
- pair.claudeOwnerLease === claude.binding.ownerLease &&
2799
- pair.codexOwnerLease === codex.binding.ownerLease);
2270
+ const [left, right] = this.requireConsentEdgeRoutes(state, input, false);
2271
+ const endpoints = canonicalConsentEndpoints(left, right);
2272
+ return state.consentEdges.some((edge) => edge.endpoints.every((endpoint, index) => endpoint.alias === endpoints[index].alias &&
2273
+ endpoint.provider === endpoints[index].provider &&
2274
+ endpoint.ownerLease === endpoints[index].ownerLease));
2800
2275
  }
2801
2276
  catch (error) {
2802
2277
  if (error instanceof BridgeError)
@@ -2805,32 +2280,34 @@ export class GatewayStore {
2805
2280
  }
2806
2281
  });
2807
2282
  }
2808
- async inspectAffectedPairInFlightMessages(input) {
2283
+ async inspectAffectedConsentEdgeInFlightMessages(input) {
2809
2284
  return this.mutex.run("gateway", async () => {
2810
2285
  const state = this.requireState();
2811
- const { claude, codex } = this.requirePairRoutes(state, input, false);
2812
- this.requireExactPair(state, claude, codex);
2286
+ const routes = this.requireConsentEdgeRoutes(state, input, false);
2287
+ const edge = this.requireExactConsentEdge(state, ...routes);
2813
2288
  return state.inFlight
2814
- .filter((item) => pairMatchesMessage(input, item))
2289
+ .filter((item) => consentEdgeMatchesMessage(edge, item))
2815
2290
  .map(({ messageId, deadlineAt }) => ({ messageId, deadlineAt }));
2816
2291
  });
2817
2292
  }
2818
2293
  /**
2819
2294
  * Remove exactly one consent edge and settle only work owned by that edge.
2820
- * Adjacent pairs, terminal token metadata, and unrelated messages survive.
2295
+ * Adjacent edges, terminal token metadata, and unrelated messages survive.
2821
2296
  */
2822
- async unpairRoutes(input) {
2297
+ async removeConsentEdge(input) {
2823
2298
  return this.mutate(async (state, now) => {
2824
- const { claude, codex } = this.requirePairRoutes(state, input, false);
2825
- const pair = this.requireExactPair(state, claude, codex);
2826
- const plan = this.validateAffectedPairInFlightSettlements(state, pair, input.inFlightSettlements ?? []);
2827
- this.settleProgressWatchesForPair(state, pair, now);
2828
- const settlements = this.terminateAffectedPairMessages(state, pair, now, "PAIR_REMOVED", plan);
2829
- state.pairs = state.pairs.filter((candidate) => candidate !== pair);
2830
- state.dedupe = state.dedupe.filter((record) => !pairMatchesMessage(pair, record));
2299
+ const routes = this.requireConsentEdgeRoutes(state, input, false);
2300
+ const pair = this.requireExactConsentEdge(state, ...routes);
2301
+ const plan = this.validateAffectedConsentEdgeInFlightSettlements(state, pair, input.inFlightSettlements ?? []);
2302
+ this.settleProgressWatchesForConsentEdge(state, pair, now);
2303
+ const settlements = this.terminateAffectedConsentEdgeMessages(state, pair, now, "PAIR_REMOVED", plan);
2304
+ state.consentEdges = state.consentEdges.filter((candidate) => candidate !== pair);
2305
+ state.dedupe = state.dedupe.filter((record) => !consentEdgeMatchesMessage(pair, record));
2831
2306
  return {
2832
2307
  settlements,
2833
- claudeRouteUnreferenced: !state.pairs.some((candidate) => candidate.claudeAlias === pair.claudeAlias),
2308
+ unreferencedAliases: pair.endpoints
2309
+ .filter(({ alias }) => !state.consentEdges.some((edge) => edge.endpoints.some((endpoint) => endpoint.alias === alias)))
2310
+ .map(({ alias }) => alias),
2834
2311
  };
2835
2312
  });
2836
2313
  }
@@ -2866,8 +2343,7 @@ export class GatewayStore {
2866
2343
  targetRoute: target,
2867
2344
  };
2868
2345
  try {
2869
- const pairAliases = pairAliasesForRoutes(source, target);
2870
- this.requireExactPair(state, pairAliases.claudeAlias === source.alias ? source : target, pairAliases.codexAlias === source.alias ? source : target);
2346
+ this.requireExactConsentEdge(state, source, target);
2871
2347
  sides.pair = true;
2872
2348
  }
2873
2349
  catch (error) {
@@ -2889,24 +2365,26 @@ export class GatewayStore {
2889
2365
  async enqueueNativeIngress(input) {
2890
2366
  return this.mutate(async (state, now) => {
2891
2367
  const target = this.requireAvailableRoute(state, input.targetAlias);
2892
- if (target.binding.provider !== "codex") {
2893
- throw new BridgeError("INVALID_NATIVE_INGRESS_TARGET", "Native Claude ingress requires an explicitly registered Codex target.");
2894
- }
2895
2368
  this.validateTransientNativeClaudePeer(state, input.source, target);
2369
+ const direction = directionId("claude", target.binding.provider);
2896
2370
  const selectedClaude = state.routes.find((route) => route.alias === input.source.alias &&
2897
2371
  route.binding.provider === "claude" &&
2898
2372
  route.registrationMode === "selected_live_peer" &&
2899
2373
  route.enabled &&
2900
- route.compatibility === "compatible" &&
2901
2374
  ["idle", "busy", "awaiting_approval"].includes(route.state) &&
2902
2375
  sameBinding(route.binding, input.source.binding));
2903
2376
  const pair = selectedClaude === undefined
2904
2377
  ? undefined
2905
- : state.pairs.find((candidate) => candidate.claudeAlias === selectedClaude.alias &&
2906
- candidate.codexAlias === target.alias &&
2907
- candidate.claudeOwnerLease ===
2908
- selectedClaude.binding.ownerLease &&
2909
- candidate.codexOwnerLease === target.binding.ownerLease);
2378
+ : (() => {
2379
+ try {
2380
+ return this.requireExactConsentEdge(state, selectedClaude, target);
2381
+ }
2382
+ catch (error) {
2383
+ if (error instanceof BridgeError)
2384
+ return undefined;
2385
+ throw error;
2386
+ }
2387
+ })();
2910
2388
  if (input.authorizedPairTeardownReply === true &&
2911
2389
  selectedClaude === undefined) {
2912
2390
  throw new BridgeError("SENDER_NOT_PAIRED", "The retained reply no longer matches the exact selected Claude route.");
@@ -2920,48 +2398,47 @@ export class GatewayStore {
2920
2398
  this.recordRejection(state, {
2921
2399
  sourceAlias: input.source.alias,
2922
2400
  targetAlias: target.alias,
2923
- direction: "claude_to_codex",
2401
+ direction,
2924
2402
  targetRoute: target,
2925
2403
  }, bytes, now, "SENDER_NOT_PAIRED", input.steer, input.conversationIdSuffix);
2926
- throw new BridgeError("SENDER_NOT_PAIRED", "The native Claude sender is not the exact session paired with this Codex task.");
2404
+ throw new BridgeError("SENDER_NOT_PAIRED", "The native Claude sender does not share exact consent with this route.");
2927
2405
  }
2928
2406
  return this.enqueueResolvedMessage(state, now, input, {
2929
2407
  sourceAlias: input.source.alias,
2930
2408
  targetAlias: target.alias,
2931
- direction: "claude_to_codex",
2409
+ direction,
2932
2410
  targetRoute: target,
2933
2411
  ...(pair === undefined ? {} : { pair: true }),
2934
2412
  });
2935
2413
  });
2936
2414
  }
2937
2415
  /**
2938
- * Queue a correlated Codex reply for a caller-attested transient Claude
2416
+ * Queue a correlated provider reply for a caller-attested transient Claude
2939
2417
  * peer. The service owns conversation correlation and the live dispatch
2940
2418
  * capability; the store retains only bounded public-alias metadata.
2941
2419
  */
2942
2420
  async enqueueNativeReply(input) {
2943
2421
  return this.mutate(async (state, now) => {
2944
2422
  const source = this.requireAvailableRoute(state, input.sourceAlias);
2945
- if (source.binding.provider !== "codex") {
2946
- throw new BridgeError("INVALID_NATIVE_REPLY_SOURCE", "A native Claude reply requires an explicitly registered Codex source.");
2947
- }
2948
2423
  this.validateTransientNativeClaudePeer(state, input.target, source);
2949
- if (input.pair === true) {
2950
- const target = state.routes.find((route) => route.alias === input.target.alias &&
2424
+ const target = input.pair === true
2425
+ ? state.routes.find((route) => route.alias === input.target.alias &&
2951
2426
  route.binding.provider === "claude" &&
2952
- sameBinding(route.binding, input.target.binding));
2427
+ sameBinding(route.binding, input.target.binding))
2428
+ : undefined;
2429
+ if (input.pair === true) {
2953
2430
  if (target === undefined) {
2954
2431
  throw new BridgeError("SENDER_NOT_PAIRED", "The correlated reply no longer has its exact paired Claude route.");
2955
2432
  }
2956
- this.requireExactPair(state, target, source);
2433
+ this.requireExactConsentEdge(state, target, source);
2957
2434
  }
2958
2435
  return this.enqueueResolvedMessage(state, now, input, {
2959
2436
  sourceAlias: source.alias,
2960
2437
  targetAlias: input.target.alias,
2961
- direction: "codex_to_claude",
2438
+ direction: directionId(source.binding.provider, "claude"),
2962
2439
  sourceRoute: source,
2963
- ...(input.pair === true
2964
- ? { pair: true }
2440
+ ...(target !== undefined
2441
+ ? { pair: true, targetRoute: target }
2965
2442
  : { transientTarget: true }),
2966
2443
  });
2967
2444
  });
@@ -3231,18 +2708,31 @@ export class GatewayStore {
3231
2708
  async publicSnapshot() {
3232
2709
  return this.mutate(async (state, now) => {
3233
2710
  const connectors = state.connectors
3234
- .map((connector) => ({
3235
- provider: connector.provider,
3236
- host: connector.hostId,
3237
- health: connector.health,
3238
- compatibility: connector.compatibility,
3239
- protocol: connector.protocol,
3240
- protocolVersion: connector.protocolVersion,
3241
- ...(connector.lastSeenAt ? { lastSeenAt: connector.lastSeenAt } : {}),
3242
- ...(connector.safeErrorCode
3243
- ? { safeErrorCode: connector.safeErrorCode }
3244
- : {}),
3245
- }))
2711
+ .map((connector) => {
2712
+ const observedAt = connector.lastSeenAt === undefined
2713
+ ? undefined
2714
+ : Date.parse(connector.lastSeenAt);
2715
+ const observationAgeMs = observedAt === undefined || !Number.isFinite(observedAt)
2716
+ ? undefined
2717
+ : Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, now.getTime() - observedAt));
2718
+ const health = connector.health === "healthy" &&
2719
+ (observationAgeMs === undefined ||
2720
+ observationAgeMs > CONNECTOR_OBSERVATION_STALE_AFTER_MS)
2721
+ ? "degraded"
2722
+ : connector.health;
2723
+ return {
2724
+ provider: connector.provider,
2725
+ host: connector.hostId,
2726
+ health,
2727
+ protocol: connector.protocol,
2728
+ protocolVersion: connector.protocolVersion,
2729
+ ...(connector.lastSeenAt ? { lastSeenAt: connector.lastSeenAt } : {}),
2730
+ ...(observationAgeMs === undefined ? {} : { observationAgeMs }),
2731
+ ...(connector.safeErrorCode
2732
+ ? { safeErrorCode: connector.safeErrorCode }
2733
+ : {}),
2734
+ };
2735
+ })
3246
2736
  .sort((left, right) => `${left.provider}:${left.host}`.localeCompare(`${right.provider}:${right.host}`));
3247
2737
  const oldestQueuedAtByTarget = new Map();
3248
2738
  for (const item of state.queue) {
@@ -3261,7 +2751,6 @@ export class GatewayStore {
3261
2751
  host: route.binding.hostId,
3262
2752
  enabled: route.enabled,
3263
2753
  state: route.state,
3264
- compatibility: route.compatibility,
3265
2754
  busyPolicy: route.busyPolicy,
3266
2755
  ...(route.lastSeenAt ? { lastSeenAt: route.lastSeenAt } : {}),
3267
2756
  queueDepth: route.queueDepth,
@@ -3273,14 +2762,13 @@ export class GatewayStore {
3273
2762
  };
3274
2763
  })
3275
2764
  .sort((left, right) => left.alias.localeCompare(right.alias));
3276
- const pairs = state.pairs
3277
- .map((pair) => ({
3278
- claudeAlias: pair.claudeAlias,
3279
- codexAlias: pair.codexAlias,
3280
- host: pair.claudeAlias.slice(pair.claudeAlias.lastIndexOf("@") + 1),
3281
- counters: { ...pair.counters },
2765
+ const consentEdges = state.consentEdges
2766
+ .map((edge) => ({
2767
+ endpoints: edge.endpoints.map(({ alias, provider }) => ({ alias, provider })),
2768
+ host: edge.endpoints[0].alias.slice(edge.endpoints[0].alias.lastIndexOf("@") + 1),
2769
+ counters: { ...edge.counters },
3282
2770
  }))
3283
- .sort((left, right) => `${left.claudeAlias}\0${left.codexAlias}`.localeCompare(`${right.claudeAlias}\0${right.codexAlias}`));
2771
+ .sort((left, right) => consentEdgeKey(left.endpoints).localeCompare(consentEdgeKey(right.endpoints)));
3284
2772
  const progressWatches = state.progressWatches
3285
2773
  .map((watch) => ({
3286
2774
  conversationIdSuffix: watch.conversationId.slice(-8),
@@ -3368,14 +2856,14 @@ export class GatewayStore {
3368
2856
  return `${left.code}\0${left.provider ?? ""}\0${left.host ?? ""}\0${left.alias ?? ""}`.localeCompare(`${right.code}\0${right.provider ?? ""}\0${right.host ?? ""}\0${right.alias ?? ""}`);
3369
2857
  });
3370
2858
  return projectGatewayPublicSnapshot({
3371
- schemaVersion: 1,
2859
+ schemaVersion: 2,
3372
2860
  generatedAt: now.toISOString(),
3373
2861
  inboundMode: this.config.inboundMode,
3374
2862
  health,
3375
2863
  connectors,
3376
2864
  availablePeers: [],
3377
2865
  routes,
3378
- pairs,
2866
+ consentEdges,
3379
2867
  progressWatches,
3380
2868
  progressWatchEvents,
3381
2869
  deadlinePressure: {
@@ -3394,7 +2882,7 @@ export class GatewayStore {
3394
2882
  connectors: 0,
3395
2883
  availablePeers: 0,
3396
2884
  routes: 0,
3397
- pairs: 0,
2885
+ consentEdges: 0,
3398
2886
  progressWatches: 0,
3399
2887
  progressWatchEvents: 0,
3400
2888
  messages: 0,
@@ -3517,7 +3005,7 @@ export class GatewayStore {
3517
3005
  throw new BridgeError("CODEX_SUCCESSION_BINDING_COLLISION", "The new Codex identity collides with another persisted route.");
3518
3006
  }
3519
3007
  }
3520
- replaceCodexRouteForSuccession(state, route, identity, now, routeState, compatibility) {
3008
+ replaceCodexRouteForSuccession(state, route, identity, now, routeState) {
3521
3009
  const oldAlias = route.alias;
3522
3010
  const affectedAliases = new Set([oldAlias, identity.alias]);
3523
3011
  this.settleProgressWatchesForAliases(state, affectedAliases, now);
@@ -3526,7 +3014,6 @@ export class GatewayStore {
3526
3014
  route.registrationMode = "explicit_opt_in";
3527
3015
  route.enabled = true;
3528
3016
  route.state = routeState;
3529
- route.compatibility = compatibility;
3530
3017
  route.registeredAt = now.toISOString();
3531
3018
  route.updatedAt = now.toISOString();
3532
3019
  route.queueDepth = 0;
@@ -3545,7 +3032,7 @@ export class GatewayStore {
3545
3032
  record.targetAlias !== identity.alias);
3546
3033
  state.rateBuckets = state.rateBuckets.filter((bucket) => bucket.sourceAlias !== oldAlias &&
3547
3034
  bucket.sourceAlias !== identity.alias);
3548
- removePairsForAliases(state, affectedAliases);
3035
+ removeConsentEdgesForAliases(state, affectedAliases);
3549
3036
  }
3550
3037
  assertAllowedIdentity(identity) {
3551
3038
  if (!isPrivateEndpointIdentity(identity)) {
@@ -3555,12 +3042,11 @@ export class GatewayStore {
3555
3042
  throw new BridgeError("GATEWAY_HOST_NOT_ALLOWED", "The route host is not in the fixed gateway allowlist.");
3556
3043
  }
3557
3044
  }
3558
- requireCompatibleObservedEndpoint(state, binding) {
3045
+ requireObservedEndpoint(state, binding) {
3559
3046
  const connector = state.connectors.find((candidate) => sameEndpoint(candidate, binding));
3560
3047
  if (!connector ||
3561
- !["healthy", "degraded"].includes(connector.health) ||
3562
- connector.compatibility !== "compatible") {
3563
- throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The exact compatible endpoint generation must be observed before Codex succession can publish or activate it.");
3048
+ !["healthy", "degraded"].includes(connector.health)) {
3049
+ throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The exact live endpoint generation must be observed before Codex succession can publish or activate it.");
3564
3050
  }
3565
3051
  }
3566
3052
  validateRouteInput(input) {
@@ -3574,11 +3060,15 @@ export class GatewayStore {
3574
3060
  if (aliasHost !== input.binding.hostId) {
3575
3061
  throw new BridgeError("ROUTE_HOST_MISMATCH", "The public alias host must match the allowlisted private route host.");
3576
3062
  }
3577
- if ((input.binding.provider === "codex" &&
3578
- input.registrationMode !== "explicit_opt_in") ||
3579
- (input.binding.provider === "claude" &&
3580
- input.registrationMode !== "selected_live_peer")) {
3581
- throw new BridgeError("ROUTE_OPT_IN_REQUIRED", "Codex routes require explicit self opt-in and Claude routes require a selected live peer.");
3063
+ const requiredPrefix = gatewayRegistrationIngressPrefixes[input.binding.provider];
3064
+ if (requiredPrefix !== undefined && !input.alias.startsWith(requiredPrefix)) {
3065
+ throw new BridgeError("INVALID_GATEWAY_ALIAS", `The ${input.binding.provider} registration alias must use its required ingress prefix.`);
3066
+ }
3067
+ if ((input.binding.provider === "claude" &&
3068
+ input.registrationMode !== "selected_live_peer") ||
3069
+ (input.binding.provider !== "claude" &&
3070
+ input.registrationMode !== "explicit_opt_in")) {
3071
+ throw new BridgeError("ROUTE_OPT_IN_REQUIRED", "Non-Claude routes require explicit opt-in and Claude routes require a selected live peer.");
3582
3072
  }
3583
3073
  }
3584
3074
  requireOwnedRoute(state, alias, ownerLease) {
@@ -3602,7 +3092,6 @@ export class GatewayStore {
3602
3092
  route.registrationMode !== "explicit_opt_in" ||
3603
3093
  !route.enabled ||
3604
3094
  route.state !== "stale" ||
3605
- route.compatibility !== "expired" ||
3606
3095
  route.queueDepth !== 0 ||
3607
3096
  state.queue.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias) ||
3608
3097
  state.inFlight.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias)) {
@@ -3631,52 +3120,46 @@ export class GatewayStore {
3631
3120
  const route = state.routes.find((candidate) => candidate.alias === alias);
3632
3121
  if (!route ||
3633
3122
  !route.enabled ||
3634
- !["idle", "busy", "awaiting_approval"].includes(route.state) ||
3635
- route.compatibility !== "compatible") {
3123
+ !["idle", "busy", "awaiting_approval"].includes(route.state)) {
3636
3124
  throw new BridgeError("ROUTE_UNAVAILABLE", "The selected route is not currently enabled and positively observed.", true);
3637
3125
  }
3638
3126
  return route;
3639
3127
  }
3640
- requirePairRoutes(state, input, requireAvailable = true) {
3128
+ requireConsentEdgeRoutes(state, input, requireAvailable = true) {
3641
3129
  if (!isObject(input) ||
3642
- typeof input.claudeAlias !== "string" ||
3643
- typeof input.codexAlias !== "string" ||
3644
- !ALIAS_PATTERN.test(input.claudeAlias) ||
3645
- !ALIAS_PATTERN.test(input.codexAlias) ||
3646
- input.claudeAlias === input.codexAlias) {
3647
- throw new BridgeError("INVALID_GATEWAY_PAIR", "A pair requires distinct normalized Claude and Codex aliases.");
3130
+ !Array.isArray(input.aliases) ||
3131
+ input.aliases.length !== 2 ||
3132
+ input.aliases.some((alias) => typeof alias !== "string" || !ALIAS_PATTERN.test(alias)) ||
3133
+ input.aliases[0] === input.aliases[1]) {
3134
+ throw new BridgeError("INVALID_CONSENT_EDGE", "A consent edge requires two distinct normalized aliases.");
3648
3135
  }
3649
3136
  const resolve = (alias) => {
3650
3137
  if (requireAvailable)
3651
3138
  return this.requireAvailableRoute(state, alias);
3652
3139
  const route = state.routes.find((candidate) => candidate.alias === alias);
3653
3140
  if (route === undefined) {
3654
- throw new BridgeError("PAIR_ROUTE_NOT_FOUND", "The pair references a route that is not registered.");
3141
+ throw new BridgeError("CONSENT_EDGE_ROUTE_NOT_FOUND", "The consent edge references a route that is not registered.");
3655
3142
  }
3656
3143
  return route;
3657
3144
  };
3658
- const claude = resolve(input.claudeAlias);
3659
- const codex = resolve(input.codexAlias);
3660
- if (claude.binding.provider !== "claude" ||
3661
- claude.registrationMode !== "selected_live_peer" ||
3662
- codex.binding.provider !== "codex" ||
3663
- codex.registrationMode !== "explicit_opt_in" ||
3664
- claude.binding.hostId !== codex.binding.hostId) {
3665
- throw new BridgeError("INVALID_GATEWAY_PAIR", "A pair must connect one selected Claude route and one registered Codex route on the same host.");
3666
- }
3667
- return { claude, codex };
3668
- }
3669
- requireExactPair(state, claude, codex) {
3670
- const pair = state.pairs.find((candidate) => candidate.claudeAlias === claude.alias &&
3671
- candidate.codexAlias === codex.alias);
3145
+ const left = resolve(input.aliases[0]);
3146
+ const right = resolve(input.aliases[1]);
3147
+ if (left.binding.provider === right.binding.provider ||
3148
+ left.binding.hostId !== right.binding.hostId) {
3149
+ throw new BridgeError("INVALID_CONSENT_EDGE", "A consent edge must connect distinct providers on the same host.");
3150
+ }
3151
+ return [left, right];
3152
+ }
3153
+ requireExactConsentEdge(state, left, right) {
3154
+ const endpoints = canonicalConsentEndpoints(left, right);
3155
+ const pair = state.consentEdges.find((candidate) => consentEdgeKey(candidate.endpoints) === consentEdgeKey(endpoints));
3672
3156
  if (pair === undefined ||
3673
- pair.claudeOwnerLease !== claude.binding.ownerLease ||
3674
- pair.codexOwnerLease !== codex.binding.ownerLease) {
3157
+ pair.endpoints.some((endpoint, index) => endpoint.ownerLease !== endpoints[index].ownerLease)) {
3675
3158
  throw new BridgeError("SENDER_NOT_PAIRED", "The two exact route authorities do not share a consent edge.");
3676
3159
  }
3677
3160
  return pair;
3678
3161
  }
3679
- validateTransientNativeClaudePeer(state, peer, codexRoute) {
3162
+ validateTransientNativeClaudePeer(state, peer, route) {
3680
3163
  if (!isObject(peer) ||
3681
3164
  !hasOnlyKeys(peer, ["alias", "binding"]) ||
3682
3165
  typeof peer.alias !== "string" ||
@@ -3687,17 +3170,16 @@ export class GatewayStore {
3687
3170
  }
3688
3171
  this.assertAllowedIdentity(endpointOf(peer.binding));
3689
3172
  const aliasHost = peer.alias.slice(peer.alias.lastIndexOf("@") + 1);
3690
- if (codexRoute.binding.provider !== "codex" ||
3173
+ if (route.binding.provider === "claude" ||
3691
3174
  aliasHost !== peer.binding.hostId ||
3692
- peer.binding.hostId !== codexRoute.binding.hostId ||
3693
- peer.alias === codexRoute.alias) {
3694
- throw new BridgeError("NATIVE_PEER_SCOPE_MISMATCH", "The transient Claude peer and registered Codex route must have distinct aliases on the same allowlisted host.");
3175
+ peer.binding.hostId !== route.binding.hostId ||
3176
+ peer.alias === route.alias) {
3177
+ throw new BridgeError("NATIVE_PEER_SCOPE_MISMATCH", "The transient Claude peer and registered route must be distinct providers on the same allowlisted host.");
3695
3178
  }
3696
3179
  const connector = state.connectors.find((candidate) => sameEndpoint(candidate, peer.binding));
3697
3180
  if (!connector ||
3698
- !["healthy", "degraded"].includes(connector.health) ||
3699
- connector.compatibility !== "compatible") {
3700
- throw new BridgeError("NATIVE_PEER_ENDPOINT_NOT_OBSERVED", "The transient Claude peer's exact compatible connector generation is not live.", true);
3181
+ !["healthy", "degraded"].includes(connector.health)) {
3182
+ throw new BridgeError("NATIVE_PEER_ENDPOINT_NOT_OBSERVED", "The transient Claude peer's exact connector generation is not live.", true);
3701
3183
  }
3702
3184
  }
3703
3185
  enqueueResolvedMessage(state, now, input, sides) {
@@ -3841,7 +3323,7 @@ export class GatewayStore {
3841
3323
  this.transientBodies.set(messageId, input.body);
3842
3324
  state.accounting.accepted += 1;
3843
3325
  state.accounting.bytesAccepted += bytes;
3844
- const pair = findPairForMessage(state, metadata);
3326
+ const pair = findConsentEdgeForMessage(state, metadata);
3845
3327
  if (pair !== undefined) {
3846
3328
  pair.counters.accepted += 1;
3847
3329
  pair.counters.bytesAccepted += bytes;
@@ -3931,7 +3413,7 @@ export class GatewayStore {
3931
3413
  state.accounting.rejected += 1;
3932
3414
  if (sides.sourceRoute)
3933
3415
  sides.sourceRoute.counters.rejected += 1;
3934
- const pair = findPairForMessage(state, sides);
3416
+ const pair = findConsentEdgeForMessage(state, sides);
3935
3417
  if (pair !== undefined) {
3936
3418
  pair.counters.rejected += 1;
3937
3419
  pair.updatedAt = now.toISOString();
@@ -4035,13 +3517,13 @@ export class GatewayStore {
4035
3517
  throw new BridgeError("INVALID_PROGRESS_WATCH", "The message watch activity is malformed or contradictory.");
4036
3518
  }
4037
3519
  const pair = sides.pair === true
4038
- ? state.pairs.find((candidate) => pairMatchesMessage(candidate, sides))
3520
+ ? state.consentEdges.find((candidate) => consentEdgeMatchesMessage(candidate, sides))
4039
3521
  : undefined;
4040
3522
  const index = state.progressWatches.findIndex((watch) => watch.conversationId === activity.conversationId);
4041
3523
  const existing = state.progressWatches[index];
4042
3524
  if (existing !== undefined) {
4043
3525
  if (pair === undefined ||
4044
- !progressWatchMatchesPair(existing, pair) ||
3526
+ !progressWatchMatchesConsentEdge(existing, pair) ||
4045
3527
  !((sides.sourceAlias === existing.ownerAlias &&
4046
3528
  sides.targetAlias === existing.workerAlias) ||
4047
3529
  (sides.sourceAlias === existing.workerAlias &&
@@ -4080,12 +3562,8 @@ export class GatewayStore {
4080
3562
  conversationId: activity.conversationId,
4081
3563
  ownerAlias: sides.sourceAlias,
4082
3564
  workerAlias: sides.targetAlias,
4083
- ownerLease: sides.sourceAlias === pair.claudeAlias
4084
- ? pair.claudeOwnerLease
4085
- : pair.codexOwnerLease,
4086
- workerLease: sides.targetAlias === pair.claudeAlias
4087
- ? pair.claudeOwnerLease
4088
- : pair.codexOwnerLease,
3565
+ ownerLease: pair.endpoints.find(({ alias }) => alias === sides.sourceAlias).ownerLease,
3566
+ workerLease: pair.endpoints.find(({ alias }) => alias === sides.targetAlias).ownerLease,
4089
3567
  idleMs: activity.openIdleMs,
4090
3568
  at: now.getTime(),
4091
3569
  });
@@ -4121,7 +3599,7 @@ export class GatewayStore {
4121
3599
  });
4122
3600
  }
4123
3601
  installProgressWatch(state, watch, pair, now) {
4124
- const replacedIndex = state.progressWatches.findIndex((candidate) => progressWatchMatchesPair(candidate, pair));
3602
+ const replacedIndex = state.progressWatches.findIndex((candidate) => progressWatchMatchesConsentEdge(candidate, pair));
4125
3603
  if (replacedIndex < 0 &&
4126
3604
  state.progressWatches.length >=
4127
3605
  (this.config.limits.maxWatches ?? PROGRESS_WATCH_DEFAULT_CAPACITY)) {
@@ -4183,11 +3661,11 @@ export class GatewayStore {
4183
3661
  this.appendProgressWatchEvents(state, events);
4184
3662
  state.progressWatches = retained;
4185
3663
  }
4186
- settleProgressWatchesForPair(state, pair, now) {
3664
+ settleProgressWatchesForConsentEdge(state, pair, now) {
4187
3665
  const retained = [];
4188
3666
  const events = [];
4189
3667
  for (const watch of state.progressWatches) {
4190
- if (!progressWatchMatchesPair(watch, pair)) {
3668
+ if (!progressWatchMatchesConsentEdge(watch, pair)) {
4191
3669
  retained.push(watch);
4192
3670
  continue;
4193
3671
  }
@@ -4203,7 +3681,7 @@ export class GatewayStore {
4203
3681
  finishMetadata(state, metadata, deliveryState, now, safeErrorCode) {
4204
3682
  const counterKey = deliveryState;
4205
3683
  state.accounting[counterKey] += 1;
4206
- const pair = findPairForMessage(state, metadata);
3684
+ const pair = findConsentEdgeForMessage(state, metadata);
4207
3685
  if (pair !== undefined) {
4208
3686
  pair.counters[counterKey] += 1;
4209
3687
  pair.updatedAt = now.toISOString();
@@ -4263,12 +3741,12 @@ export class GatewayStore {
4263
3741
  }
4264
3742
  return byMessageId;
4265
3743
  }
4266
- validateAffectedPairInFlightSettlements(state, pair, requested) {
3744
+ validateAffectedConsentEdgeInFlightSettlements(state, pair, requested) {
4267
3745
  if (requested.length > this.config.limits.maxInFlightMessages) {
4268
3746
  throw new BridgeError("INVALID_PAIR_TERMINATION_PLAN", "Unpairing requires one normalized terminal settlement per affected in-flight message.");
4269
3747
  }
4270
3748
  const affectedIds = new Set(state.inFlight
4271
- .filter((item) => pairMatchesMessage(pair, item))
3749
+ .filter((item) => consentEdgeMatchesMessage(pair, item))
4272
3750
  .map((item) => item.messageId));
4273
3751
  const byMessageId = new Map();
4274
3752
  for (const settlement of requested) {
@@ -4321,10 +3799,10 @@ export class GatewayStore {
4321
3799
  }
4322
3800
  return settlements;
4323
3801
  }
4324
- terminateAffectedPairMessages(state, pair, now, safeErrorCode, inFlightSettlements) {
3802
+ terminateAffectedConsentEdgeMessages(state, pair, now, safeErrorCode, inFlightSettlements) {
4325
3803
  const settlements = [];
4326
- const queued = state.queue.filter((item) => pairMatchesMessage(pair, item));
4327
- state.queue = state.queue.filter((item) => !pairMatchesMessage(pair, item));
3804
+ const queued = state.queue.filter((item) => consentEdgeMatchesMessage(pair, item));
3805
+ state.queue = state.queue.filter((item) => !consentEdgeMatchesMessage(pair, item));
4328
3806
  for (const item of queued) {
4329
3807
  this.transientBodies.delete(item.messageId);
4330
3808
  state.accounting.queuedBytes -= item.bytes;
@@ -4334,8 +3812,8 @@ export class GatewayStore {
4334
3812
  const deadlineReached = Date.parse(item.deadlineAt) <= now.getTime();
4335
3813
  settlements.push(this.finishMetadata(state, item, deadlineReached ? "expired" : "abandoned", now, deadlineReached ? "MESSAGE_EXPIRED" : safeErrorCode));
4336
3814
  }
4337
- const inFlight = state.inFlight.filter((item) => pairMatchesMessage(pair, item));
4338
- state.inFlight = state.inFlight.filter((item) => !pairMatchesMessage(pair, item));
3815
+ const inFlight = state.inFlight.filter((item) => consentEdgeMatchesMessage(pair, item));
3816
+ state.inFlight = state.inFlight.filter((item) => !consentEdgeMatchesMessage(pair, item));
4339
3817
  for (const item of inFlight) {
4340
3818
  const requested = inFlightSettlements.get(item.messageId);
4341
3819
  if (requested === undefined) {
@@ -4372,13 +3850,11 @@ export class GatewayStore {
4372
3850
  }
4373
3851
  for (const connector of state.connectors) {
4374
3852
  connector.health = "offline";
4375
- connector.compatibility = "expired";
4376
3853
  connector.updatedAt = now.toISOString();
4377
3854
  connector.safeErrorCode = "REOBSERVATION_REQUIRED";
4378
3855
  }
4379
3856
  for (const route of state.routes) {
4380
3857
  route.state = route.enabled ? "stale" : "disabled";
4381
- route.compatibility = "expired";
4382
3858
  route.updatedAt = now.toISOString();
4383
3859
  route.safeErrorCode = "REOBSERVATION_REQUIRED";
4384
3860
  route.queueDepth = 0;
@@ -4424,7 +3900,7 @@ export class GatewayStore {
4424
3900
  const route = this.requireAnyJournalRoute(state, journal);
4425
3901
  this.assertSuccessionLedgerEmpty(state, route.alias);
4426
3902
  this.assertNewSuccessionIdentityAvailable(state, route, journal.new);
4427
- this.replaceCodexRouteForSuccession(state, route, journal.new, now, "stale", "expired");
3903
+ this.replaceCodexRouteForSuccession(state, route, journal.new, now, "stale");
4428
3904
  state.codexSuccession = {
4429
3905
  ...journal,
4430
3906
  stage: "recovery_forbidden",
@@ -4650,7 +4126,7 @@ export class GatewayStore {
4650
4126
  // Never remove a lock that cannot be proven to belong to this instance.
4651
4127
  }
4652
4128
  }
4653
- async loadStateFile(now) {
4129
+ async loadStateFile(_now) {
4654
4130
  let body;
4655
4131
  try {
4656
4132
  body = await this.readPrivateFile(this.stateFilePath, GATEWAY_MAX_STATE_FILE_BYTES);
@@ -4668,23 +4144,15 @@ export class GatewayStore {
4668
4144
  catch {
4669
4145
  throw new BridgeError("CORRUPT_GATEWAY_STATE", "The gateway controller state is not valid JSON.");
4670
4146
  }
4671
- parsed = migratePreUnconfirmedCounters(parsed);
4672
- parsed = migratePreSuccessionJournal(parsed);
4673
- parsed = migratePrePairGraph(parsed);
4674
- parsed = migratePreProgressWatches(parsed);
4675
- parsed = migratePreCompatibilityAttestations(parsed);
4676
- parsed = migratePreCodexEndpointRefreshJournal(parsed);
4677
- parsed = migratePreCodexOrphanRemovalJournal(parsed);
4678
- parsed = migrateLegacyHopCounts(parsed);
4679
- parsed = migrateLegacyCompatibilityCertifications(parsed);
4680
- parsed = migrateV14ProgressWatchState(parsed, now.toISOString());
4681
4147
  if (!isPersistedState(parsed)) {
4682
4148
  throw new BridgeError("CORRUPT_GATEWAY_STATE", "The gateway controller state failed strict schema validation.");
4683
4149
  }
4684
4150
  if (parsed.routes.some((route) => !this.config.allowedHosts.includes(route.binding.hostId)) ||
4685
4151
  parsed.connectors.some((connector) => !this.config.allowedHosts.includes(connector.hostId)) ||
4686
4152
  parsed.routes.length > this.config.limits.maxRoutes ||
4687
- parsed.connectors.length > this.config.allowedHosts.length * 2 ||
4153
+ parsed.connectors.length >
4154
+ this.config.allowedHosts.length * gatewayProviders.length ||
4155
+ parsed.consentEdges.length > this.config.limits.maxConsentEdges ||
4688
4156
  parsed.rateBuckets.length > this.config.limits.maxRoutes ||
4689
4157
  parsed.progressWatches.length >
4690
4158
  (this.config.limits.maxWatches ?? PROGRESS_WATCH_DEFAULT_CAPACITY) ||