agent-embassy 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +9 -9
- package/README.zh-CN.md +9 -9
- package/SECURITY.md +34 -77
- package/dist/src/gateway/acp-client.d.ts +117 -0
- package/dist/src/gateway/acp-client.js +382 -0
- package/dist/src/gateway/acp-client.js.map +1 -0
- package/dist/src/gateway/acp-provider.d.ts +65 -0
- package/dist/src/gateway/acp-provider.js +163 -0
- package/dist/src/gateway/acp-provider.js.map +1 -0
- package/dist/src/gateway/claude-helper-protocol.d.ts +3 -1
- package/dist/src/gateway/claude-helper-protocol.js +15 -5
- package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
- package/dist/src/gateway/claude-helper-supervisor.d.ts +3 -1
- package/dist/src/gateway/claude-helper-supervisor.js +16 -2
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
- package/dist/src/gateway/claude-helper.js +4 -3
- package/dist/src/gateway/claude-helper.js.map +1 -1
- package/dist/src/gateway/claude-peer.d.ts +1 -1
- package/dist/src/gateway/claude-peer.js +11 -13
- package/dist/src/gateway/claude-peer.js.map +1 -1
- package/dist/src/gateway/claude-runtime.d.ts +0 -20
- package/dist/src/gateway/claude-runtime.js +6 -127
- package/dist/src/gateway/claude-runtime.js.map +1 -1
- package/dist/src/gateway/cli-copy.en.d.ts +1 -1
- package/dist/src/gateway/cli-copy.en.js +2 -2
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.js +2 -2
- package/dist/src/gateway/cli.d.ts +1 -1
- package/dist/src/gateway/cli.js +26 -5
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +5 -74
- package/dist/src/gateway/codex-app-server.js +9 -597
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-local-transport.d.ts +8 -26
- package/dist/src/gateway/codex-local-transport.js +26 -76
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/compatibility.d.ts +6 -66
- package/dist/src/gateway/compatibility.js +14 -296
- package/dist/src/gateway/compatibility.js.map +1 -1
- package/dist/src/gateway/config.d.ts +9 -0
- package/dist/src/gateway/config.js +6 -2
- package/dist/src/gateway/config.js.map +1 -1
- package/dist/src/gateway/control.d.ts +16 -1
- package/dist/src/gateway/control.js +89 -57
- package/dist/src/gateway/control.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.d.ts +1 -1
- package/dist/src/gateway/dashboard-copy.en.d.ts +14 -44
- package/dist/src/gateway/dashboard-copy.en.js +37 -67
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +14 -44
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +14 -44
- package/dist/src/gateway/dashboard-copy.zh-CN.js +37 -67
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +38 -76
- package/dist/src/gateway/dashboard-model.js +158 -256
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.d.ts +0 -3
- package/dist/src/gateway/dashboard.js +75 -76
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/deepseek-detect.d.ts +7 -6
- package/dist/src/gateway/deepseek-detect.js +31 -115
- package/dist/src/gateway/deepseek-detect.js.map +1 -1
- package/dist/src/gateway/live-dashboard-app/app.js +166 -621
- package/dist/src/gateway/live-dashboard-command.js +2 -4
- package/dist/src/gateway/live-dashboard-command.js.map +1 -1
- package/dist/src/gateway/live-dashboard-http.d.ts +1 -2
- package/dist/src/gateway/live-dashboard-http.js +14 -15
- package/dist/src/gateway/live-dashboard-http.js.map +1 -1
- package/dist/src/gateway/progress-watch-machine.d.ts +1 -4
- package/dist/src/gateway/progress-watch-machine.js.map +1 -1
- package/dist/src/gateway/provenance-envelope.d.ts +3 -2
- package/dist/src/gateway/provenance-envelope.js +44 -11
- package/dist/src/gateway/provenance-envelope.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +12 -84
- package/dist/src/gateway/providers.js +69 -806
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +8 -9
- package/dist/src/gateway/server.js +51 -99
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +22 -49
- package/dist/src/gateway/service.js +196 -432
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +20 -29
- package/dist/src/gateway/store.js +281 -827
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +55 -55
- package/dist/src/gateway/types.js +49 -91
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +11 -10
- package/docs/CONFIGURATION.zh-CN.md +10 -9
- package/docs/DASHBOARD.md +4 -38
- package/docs/DASHBOARD.zh-CN.md +4 -2
- package/docs/GATEWAY-ARCHITECTURE.md +61 -119
- package/package.json +1 -1
- 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,
|
|
10
|
+
import { CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY, CODEX_ORPHAN_REMOVAL_JOURNAL_CAPACITY, 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
|
|
245
|
+
function isConsentEndpoint(value) {
|
|
252
246
|
return (isObject(value) &&
|
|
253
|
-
hasOnlyKeys(value, [
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
watch.
|
|
284
|
-
watch.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
watch.
|
|
288
|
-
watch.
|
|
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
|
|
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
|
-
"
|
|
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 !==
|
|
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.
|
|
1105
|
-
value.
|
|
1106
|
-
!value.
|
|
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
|
|
1175
|
-
if (!candidate.
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
|
1196
|
-
const
|
|
1197
|
-
const
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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.
|
|
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(
|
|
1254
|
-
|
|
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 === "
|
|
1271
|
-
route.registrationMode === "
|
|
1272
|
-
(route.binding.provider
|
|
1273
|
-
route.registrationMode === "
|
|
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.
|
|
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
|
|
1470
|
-
return
|
|
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
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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;
|
|
1020
|
+
}
|
|
1021
|
+
function consentEdgeKey(endpoints) {
|
|
1022
|
+
return endpoints.map(({ provider, alias }) => `${provider}\0${alias}`).join("\0");
|
|
1482
1023
|
}
|
|
1483
|
-
function
|
|
1484
|
-
|
|
1485
|
-
((
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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]));
|
|
1489
1031
|
}
|
|
1490
|
-
function
|
|
1491
|
-
return state.
|
|
1032
|
+
function findConsentEdgeForMessage(state, message) {
|
|
1033
|
+
return state.consentEdges.find((edge) => consentEdgeMatchesMessage(edge, message));
|
|
1492
1034
|
}
|
|
1493
|
-
function
|
|
1035
|
+
function renameConsentEdgeAlias(state, previousAlias, newAlias, now) {
|
|
1494
1036
|
if (previousAlias === newAlias)
|
|
1495
1037
|
return;
|
|
1496
|
-
for (const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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
|
|
1517
|
-
state.
|
|
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
|
|
1532
|
-
target.binding.provider
|
|
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
|
|
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:
|
|
1116
|
+
schemaVersion: 2,
|
|
1583
1117
|
createdAt: now.toISOString(),
|
|
1584
1118
|
updatedAt: now.toISOString(),
|
|
1585
1119
|
eventSequence: 0,
|
|
1586
1120
|
routes: [],
|
|
1587
|
-
|
|
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.
|
|
1606
|
-
throw new BridgeError("
|
|
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 >=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2159
|
-
.filter((
|
|
2160
|
-
.map((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
2591
|
-
async
|
|
2097
|
+
/** Create one explicit, durable consent edge between distinct providers. */
|
|
2098
|
+
async addConsentEdge(input) {
|
|
2592
2099
|
return this.mutate(async (state, now) => {
|
|
2593
|
-
const
|
|
2594
|
-
const
|
|
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.
|
|
2597
|
-
|
|
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.
|
|
2603
|
-
throw new BridgeError("
|
|
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.
|
|
2606
|
-
|
|
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
|
|
2123
|
+
async inspectConsentEdges() {
|
|
2619
2124
|
return this.mutex.run("gateway", async () => {
|
|
2620
2125
|
const state = this.requireState();
|
|
2621
|
-
return state.
|
|
2622
|
-
.map(({
|
|
2623
|
-
.
|
|
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.
|
|
2157
|
+
const hasExactPair = state.consentEdges.some((candidate) => consentEdgeMatchesMessage(candidate, {
|
|
2651
2158
|
sourceAlias: input.sourceAlias,
|
|
2652
2159
|
targetAlias: input.targetAlias,
|
|
2653
2160
|
pair: true,
|
|
2654
|
-
}) &&
|
|
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
|
|
2266
|
+
async hasConsentEdge(input) {
|
|
2792
2267
|
return this.mutex.run("gateway", async () => {
|
|
2793
2268
|
const state = this.requireState();
|
|
2794
2269
|
try {
|
|
2795
|
-
const
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
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
|
|
2283
|
+
async inspectAffectedConsentEdgeInFlightMessages(input) {
|
|
2809
2284
|
return this.mutex.run("gateway", async () => {
|
|
2810
2285
|
const state = this.requireState();
|
|
2811
|
-
const
|
|
2812
|
-
this.
|
|
2286
|
+
const routes = this.requireConsentEdgeRoutes(state, input, false);
|
|
2287
|
+
const edge = this.requireExactConsentEdge(state, ...routes);
|
|
2813
2288
|
return state.inFlight
|
|
2814
|
-
.filter((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
|
|
2295
|
+
* Adjacent edges, terminal token metadata, and unrelated messages survive.
|
|
2821
2296
|
*/
|
|
2822
|
-
async
|
|
2297
|
+
async removeConsentEdge(input) {
|
|
2823
2298
|
return this.mutate(async (state, now) => {
|
|
2824
|
-
const
|
|
2825
|
-
const pair = this.
|
|
2826
|
-
const plan = this.
|
|
2827
|
-
this.
|
|
2828
|
-
const settlements = this.
|
|
2829
|
-
state.
|
|
2830
|
-
state.dedupe = state.dedupe.filter((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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
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
|
|
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
|
|
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
|
|
2409
|
+
direction,
|
|
2932
2410
|
targetRoute: target,
|
|
2933
2411
|
...(pair === undefined ? {} : { pair: true }),
|
|
2934
2412
|
});
|
|
2935
2413
|
});
|
|
2936
2414
|
}
|
|
2937
2415
|
/**
|
|
2938
|
-
* Queue a correlated
|
|
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
|
-
|
|
2950
|
-
|
|
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.
|
|
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: "
|
|
2438
|
+
direction: directionId(source.binding.provider, "claude"),
|
|
2962
2439
|
sourceRoute: source,
|
|
2963
|
-
...(
|
|
2964
|
-
? { pair: true }
|
|
2440
|
+
...(target !== undefined
|
|
2441
|
+
? { pair: true, targetRoute: target }
|
|
2965
2442
|
: { transientTarget: true }),
|
|
2966
2443
|
});
|
|
2967
2444
|
});
|
|
@@ -3235,7 +2712,6 @@ export class GatewayStore {
|
|
|
3235
2712
|
provider: connector.provider,
|
|
3236
2713
|
host: connector.hostId,
|
|
3237
2714
|
health: connector.health,
|
|
3238
|
-
compatibility: connector.compatibility,
|
|
3239
2715
|
protocol: connector.protocol,
|
|
3240
2716
|
protocolVersion: connector.protocolVersion,
|
|
3241
2717
|
...(connector.lastSeenAt ? { lastSeenAt: connector.lastSeenAt } : {}),
|
|
@@ -3261,7 +2737,6 @@ export class GatewayStore {
|
|
|
3261
2737
|
host: route.binding.hostId,
|
|
3262
2738
|
enabled: route.enabled,
|
|
3263
2739
|
state: route.state,
|
|
3264
|
-
compatibility: route.compatibility,
|
|
3265
2740
|
busyPolicy: route.busyPolicy,
|
|
3266
2741
|
...(route.lastSeenAt ? { lastSeenAt: route.lastSeenAt } : {}),
|
|
3267
2742
|
queueDepth: route.queueDepth,
|
|
@@ -3273,14 +2748,13 @@ export class GatewayStore {
|
|
|
3273
2748
|
};
|
|
3274
2749
|
})
|
|
3275
2750
|
.sort((left, right) => left.alias.localeCompare(right.alias));
|
|
3276
|
-
const
|
|
3277
|
-
.map((
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
counters: { ...pair.counters },
|
|
2751
|
+
const consentEdges = state.consentEdges
|
|
2752
|
+
.map((edge) => ({
|
|
2753
|
+
endpoints: edge.endpoints.map(({ alias, provider }) => ({ alias, provider })),
|
|
2754
|
+
host: edge.endpoints[0].alias.slice(edge.endpoints[0].alias.lastIndexOf("@") + 1),
|
|
2755
|
+
counters: { ...edge.counters },
|
|
3282
2756
|
}))
|
|
3283
|
-
.sort((left, right) =>
|
|
2757
|
+
.sort((left, right) => consentEdgeKey(left.endpoints).localeCompare(consentEdgeKey(right.endpoints)));
|
|
3284
2758
|
const progressWatches = state.progressWatches
|
|
3285
2759
|
.map((watch) => ({
|
|
3286
2760
|
conversationIdSuffix: watch.conversationId.slice(-8),
|
|
@@ -3368,14 +2842,14 @@ export class GatewayStore {
|
|
|
3368
2842
|
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
2843
|
});
|
|
3370
2844
|
return projectGatewayPublicSnapshot({
|
|
3371
|
-
schemaVersion:
|
|
2845
|
+
schemaVersion: 2,
|
|
3372
2846
|
generatedAt: now.toISOString(),
|
|
3373
2847
|
inboundMode: this.config.inboundMode,
|
|
3374
2848
|
health,
|
|
3375
2849
|
connectors,
|
|
3376
2850
|
availablePeers: [],
|
|
3377
2851
|
routes,
|
|
3378
|
-
|
|
2852
|
+
consentEdges,
|
|
3379
2853
|
progressWatches,
|
|
3380
2854
|
progressWatchEvents,
|
|
3381
2855
|
deadlinePressure: {
|
|
@@ -3394,7 +2868,7 @@ export class GatewayStore {
|
|
|
3394
2868
|
connectors: 0,
|
|
3395
2869
|
availablePeers: 0,
|
|
3396
2870
|
routes: 0,
|
|
3397
|
-
|
|
2871
|
+
consentEdges: 0,
|
|
3398
2872
|
progressWatches: 0,
|
|
3399
2873
|
progressWatchEvents: 0,
|
|
3400
2874
|
messages: 0,
|
|
@@ -3517,7 +2991,7 @@ export class GatewayStore {
|
|
|
3517
2991
|
throw new BridgeError("CODEX_SUCCESSION_BINDING_COLLISION", "The new Codex identity collides with another persisted route.");
|
|
3518
2992
|
}
|
|
3519
2993
|
}
|
|
3520
|
-
replaceCodexRouteForSuccession(state, route, identity, now, routeState
|
|
2994
|
+
replaceCodexRouteForSuccession(state, route, identity, now, routeState) {
|
|
3521
2995
|
const oldAlias = route.alias;
|
|
3522
2996
|
const affectedAliases = new Set([oldAlias, identity.alias]);
|
|
3523
2997
|
this.settleProgressWatchesForAliases(state, affectedAliases, now);
|
|
@@ -3526,7 +3000,6 @@ export class GatewayStore {
|
|
|
3526
3000
|
route.registrationMode = "explicit_opt_in";
|
|
3527
3001
|
route.enabled = true;
|
|
3528
3002
|
route.state = routeState;
|
|
3529
|
-
route.compatibility = compatibility;
|
|
3530
3003
|
route.registeredAt = now.toISOString();
|
|
3531
3004
|
route.updatedAt = now.toISOString();
|
|
3532
3005
|
route.queueDepth = 0;
|
|
@@ -3545,7 +3018,7 @@ export class GatewayStore {
|
|
|
3545
3018
|
record.targetAlias !== identity.alias);
|
|
3546
3019
|
state.rateBuckets = state.rateBuckets.filter((bucket) => bucket.sourceAlias !== oldAlias &&
|
|
3547
3020
|
bucket.sourceAlias !== identity.alias);
|
|
3548
|
-
|
|
3021
|
+
removeConsentEdgesForAliases(state, affectedAliases);
|
|
3549
3022
|
}
|
|
3550
3023
|
assertAllowedIdentity(identity) {
|
|
3551
3024
|
if (!isPrivateEndpointIdentity(identity)) {
|
|
@@ -3555,12 +3028,11 @@ export class GatewayStore {
|
|
|
3555
3028
|
throw new BridgeError("GATEWAY_HOST_NOT_ALLOWED", "The route host is not in the fixed gateway allowlist.");
|
|
3556
3029
|
}
|
|
3557
3030
|
}
|
|
3558
|
-
|
|
3031
|
+
requireObservedEndpoint(state, binding) {
|
|
3559
3032
|
const connector = state.connectors.find((candidate) => sameEndpoint(candidate, binding));
|
|
3560
3033
|
if (!connector ||
|
|
3561
|
-
!["healthy", "degraded"].includes(connector.health)
|
|
3562
|
-
|
|
3563
|
-
throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The exact compatible endpoint generation must be observed before Codex succession can publish or activate it.");
|
|
3034
|
+
!["healthy", "degraded"].includes(connector.health)) {
|
|
3035
|
+
throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The exact live endpoint generation must be observed before Codex succession can publish or activate it.");
|
|
3564
3036
|
}
|
|
3565
3037
|
}
|
|
3566
3038
|
validateRouteInput(input) {
|
|
@@ -3574,11 +3046,15 @@ export class GatewayStore {
|
|
|
3574
3046
|
if (aliasHost !== input.binding.hostId) {
|
|
3575
3047
|
throw new BridgeError("ROUTE_HOST_MISMATCH", "The public alias host must match the allowlisted private route host.");
|
|
3576
3048
|
}
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
(input.binding.provider
|
|
3580
|
-
|
|
3581
|
-
|
|
3049
|
+
const requiredPrefix = gatewayRegistrationIngressPrefixes[input.binding.provider];
|
|
3050
|
+
if (requiredPrefix !== undefined && !input.alias.startsWith(requiredPrefix)) {
|
|
3051
|
+
throw new BridgeError("INVALID_GATEWAY_ALIAS", `The ${input.binding.provider} registration alias must use its required ingress prefix.`);
|
|
3052
|
+
}
|
|
3053
|
+
if ((input.binding.provider === "claude" &&
|
|
3054
|
+
input.registrationMode !== "selected_live_peer") ||
|
|
3055
|
+
(input.binding.provider !== "claude" &&
|
|
3056
|
+
input.registrationMode !== "explicit_opt_in")) {
|
|
3057
|
+
throw new BridgeError("ROUTE_OPT_IN_REQUIRED", "Non-Claude routes require explicit opt-in and Claude routes require a selected live peer.");
|
|
3582
3058
|
}
|
|
3583
3059
|
}
|
|
3584
3060
|
requireOwnedRoute(state, alias, ownerLease) {
|
|
@@ -3602,7 +3078,6 @@ export class GatewayStore {
|
|
|
3602
3078
|
route.registrationMode !== "explicit_opt_in" ||
|
|
3603
3079
|
!route.enabled ||
|
|
3604
3080
|
route.state !== "stale" ||
|
|
3605
|
-
route.compatibility !== "expired" ||
|
|
3606
3081
|
route.queueDepth !== 0 ||
|
|
3607
3082
|
state.queue.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias) ||
|
|
3608
3083
|
state.inFlight.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias)) {
|
|
@@ -3631,52 +3106,46 @@ export class GatewayStore {
|
|
|
3631
3106
|
const route = state.routes.find((candidate) => candidate.alias === alias);
|
|
3632
3107
|
if (!route ||
|
|
3633
3108
|
!route.enabled ||
|
|
3634
|
-
!["idle", "busy", "awaiting_approval"].includes(route.state)
|
|
3635
|
-
route.compatibility !== "compatible") {
|
|
3109
|
+
!["idle", "busy", "awaiting_approval"].includes(route.state)) {
|
|
3636
3110
|
throw new BridgeError("ROUTE_UNAVAILABLE", "The selected route is not currently enabled and positively observed.", true);
|
|
3637
3111
|
}
|
|
3638
3112
|
return route;
|
|
3639
3113
|
}
|
|
3640
|
-
|
|
3114
|
+
requireConsentEdgeRoutes(state, input, requireAvailable = true) {
|
|
3641
3115
|
if (!isObject(input) ||
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
!ALIAS_PATTERN.test(
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
throw new BridgeError("INVALID_GATEWAY_PAIR", "A pair requires distinct normalized Claude and Codex aliases.");
|
|
3116
|
+
!Array.isArray(input.aliases) ||
|
|
3117
|
+
input.aliases.length !== 2 ||
|
|
3118
|
+
input.aliases.some((alias) => typeof alias !== "string" || !ALIAS_PATTERN.test(alias)) ||
|
|
3119
|
+
input.aliases[0] === input.aliases[1]) {
|
|
3120
|
+
throw new BridgeError("INVALID_CONSENT_EDGE", "A consent edge requires two distinct normalized aliases.");
|
|
3648
3121
|
}
|
|
3649
3122
|
const resolve = (alias) => {
|
|
3650
3123
|
if (requireAvailable)
|
|
3651
3124
|
return this.requireAvailableRoute(state, alias);
|
|
3652
3125
|
const route = state.routes.find((candidate) => candidate.alias === alias);
|
|
3653
3126
|
if (route === undefined) {
|
|
3654
|
-
throw new BridgeError("
|
|
3127
|
+
throw new BridgeError("CONSENT_EDGE_ROUTE_NOT_FOUND", "The consent edge references a route that is not registered.");
|
|
3655
3128
|
}
|
|
3656
3129
|
return route;
|
|
3657
3130
|
};
|
|
3658
|
-
const
|
|
3659
|
-
const
|
|
3660
|
-
if (
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
requireExactPair(state, claude, codex) {
|
|
3670
|
-
const pair = state.pairs.find((candidate) => candidate.claudeAlias === claude.alias &&
|
|
3671
|
-
candidate.codexAlias === codex.alias);
|
|
3131
|
+
const left = resolve(input.aliases[0]);
|
|
3132
|
+
const right = resolve(input.aliases[1]);
|
|
3133
|
+
if (left.binding.provider === right.binding.provider ||
|
|
3134
|
+
left.binding.hostId !== right.binding.hostId) {
|
|
3135
|
+
throw new BridgeError("INVALID_CONSENT_EDGE", "A consent edge must connect distinct providers on the same host.");
|
|
3136
|
+
}
|
|
3137
|
+
return [left, right];
|
|
3138
|
+
}
|
|
3139
|
+
requireExactConsentEdge(state, left, right) {
|
|
3140
|
+
const endpoints = canonicalConsentEndpoints(left, right);
|
|
3141
|
+
const pair = state.consentEdges.find((candidate) => consentEdgeKey(candidate.endpoints) === consentEdgeKey(endpoints));
|
|
3672
3142
|
if (pair === undefined ||
|
|
3673
|
-
pair.
|
|
3674
|
-
pair.codexOwnerLease !== codex.binding.ownerLease) {
|
|
3143
|
+
pair.endpoints.some((endpoint, index) => endpoint.ownerLease !== endpoints[index].ownerLease)) {
|
|
3675
3144
|
throw new BridgeError("SENDER_NOT_PAIRED", "The two exact route authorities do not share a consent edge.");
|
|
3676
3145
|
}
|
|
3677
3146
|
return pair;
|
|
3678
3147
|
}
|
|
3679
|
-
validateTransientNativeClaudePeer(state, peer,
|
|
3148
|
+
validateTransientNativeClaudePeer(state, peer, route) {
|
|
3680
3149
|
if (!isObject(peer) ||
|
|
3681
3150
|
!hasOnlyKeys(peer, ["alias", "binding"]) ||
|
|
3682
3151
|
typeof peer.alias !== "string" ||
|
|
@@ -3687,17 +3156,16 @@ export class GatewayStore {
|
|
|
3687
3156
|
}
|
|
3688
3157
|
this.assertAllowedIdentity(endpointOf(peer.binding));
|
|
3689
3158
|
const aliasHost = peer.alias.slice(peer.alias.lastIndexOf("@") + 1);
|
|
3690
|
-
if (
|
|
3159
|
+
if (route.binding.provider === "claude" ||
|
|
3691
3160
|
aliasHost !== peer.binding.hostId ||
|
|
3692
|
-
peer.binding.hostId !==
|
|
3693
|
-
peer.alias ===
|
|
3694
|
-
throw new BridgeError("NATIVE_PEER_SCOPE_MISMATCH", "The transient Claude peer and registered
|
|
3161
|
+
peer.binding.hostId !== route.binding.hostId ||
|
|
3162
|
+
peer.alias === route.alias) {
|
|
3163
|
+
throw new BridgeError("NATIVE_PEER_SCOPE_MISMATCH", "The transient Claude peer and registered route must be distinct providers on the same allowlisted host.");
|
|
3695
3164
|
}
|
|
3696
3165
|
const connector = state.connectors.find((candidate) => sameEndpoint(candidate, peer.binding));
|
|
3697
3166
|
if (!connector ||
|
|
3698
|
-
!["healthy", "degraded"].includes(connector.health)
|
|
3699
|
-
connector
|
|
3700
|
-
throw new BridgeError("NATIVE_PEER_ENDPOINT_NOT_OBSERVED", "The transient Claude peer's exact compatible connector generation is not live.", true);
|
|
3167
|
+
!["healthy", "degraded"].includes(connector.health)) {
|
|
3168
|
+
throw new BridgeError("NATIVE_PEER_ENDPOINT_NOT_OBSERVED", "The transient Claude peer's exact connector generation is not live.", true);
|
|
3701
3169
|
}
|
|
3702
3170
|
}
|
|
3703
3171
|
enqueueResolvedMessage(state, now, input, sides) {
|
|
@@ -3841,7 +3309,7 @@ export class GatewayStore {
|
|
|
3841
3309
|
this.transientBodies.set(messageId, input.body);
|
|
3842
3310
|
state.accounting.accepted += 1;
|
|
3843
3311
|
state.accounting.bytesAccepted += bytes;
|
|
3844
|
-
const pair =
|
|
3312
|
+
const pair = findConsentEdgeForMessage(state, metadata);
|
|
3845
3313
|
if (pair !== undefined) {
|
|
3846
3314
|
pair.counters.accepted += 1;
|
|
3847
3315
|
pair.counters.bytesAccepted += bytes;
|
|
@@ -3931,7 +3399,7 @@ export class GatewayStore {
|
|
|
3931
3399
|
state.accounting.rejected += 1;
|
|
3932
3400
|
if (sides.sourceRoute)
|
|
3933
3401
|
sides.sourceRoute.counters.rejected += 1;
|
|
3934
|
-
const pair =
|
|
3402
|
+
const pair = findConsentEdgeForMessage(state, sides);
|
|
3935
3403
|
if (pair !== undefined) {
|
|
3936
3404
|
pair.counters.rejected += 1;
|
|
3937
3405
|
pair.updatedAt = now.toISOString();
|
|
@@ -4035,13 +3503,13 @@ export class GatewayStore {
|
|
|
4035
3503
|
throw new BridgeError("INVALID_PROGRESS_WATCH", "The message watch activity is malformed or contradictory.");
|
|
4036
3504
|
}
|
|
4037
3505
|
const pair = sides.pair === true
|
|
4038
|
-
? state.
|
|
3506
|
+
? state.consentEdges.find((candidate) => consentEdgeMatchesMessage(candidate, sides))
|
|
4039
3507
|
: undefined;
|
|
4040
3508
|
const index = state.progressWatches.findIndex((watch) => watch.conversationId === activity.conversationId);
|
|
4041
3509
|
const existing = state.progressWatches[index];
|
|
4042
3510
|
if (existing !== undefined) {
|
|
4043
3511
|
if (pair === undefined ||
|
|
4044
|
-
!
|
|
3512
|
+
!progressWatchMatchesConsentEdge(existing, pair) ||
|
|
4045
3513
|
!((sides.sourceAlias === existing.ownerAlias &&
|
|
4046
3514
|
sides.targetAlias === existing.workerAlias) ||
|
|
4047
3515
|
(sides.sourceAlias === existing.workerAlias &&
|
|
@@ -4080,12 +3548,8 @@ export class GatewayStore {
|
|
|
4080
3548
|
conversationId: activity.conversationId,
|
|
4081
3549
|
ownerAlias: sides.sourceAlias,
|
|
4082
3550
|
workerAlias: sides.targetAlias,
|
|
4083
|
-
ownerLease:
|
|
4084
|
-
|
|
4085
|
-
: pair.codexOwnerLease,
|
|
4086
|
-
workerLease: sides.targetAlias === pair.claudeAlias
|
|
4087
|
-
? pair.claudeOwnerLease
|
|
4088
|
-
: pair.codexOwnerLease,
|
|
3551
|
+
ownerLease: pair.endpoints.find(({ alias }) => alias === sides.sourceAlias).ownerLease,
|
|
3552
|
+
workerLease: pair.endpoints.find(({ alias }) => alias === sides.targetAlias).ownerLease,
|
|
4089
3553
|
idleMs: activity.openIdleMs,
|
|
4090
3554
|
at: now.getTime(),
|
|
4091
3555
|
});
|
|
@@ -4121,7 +3585,7 @@ export class GatewayStore {
|
|
|
4121
3585
|
});
|
|
4122
3586
|
}
|
|
4123
3587
|
installProgressWatch(state, watch, pair, now) {
|
|
4124
|
-
const replacedIndex = state.progressWatches.findIndex((candidate) =>
|
|
3588
|
+
const replacedIndex = state.progressWatches.findIndex((candidate) => progressWatchMatchesConsentEdge(candidate, pair));
|
|
4125
3589
|
if (replacedIndex < 0 &&
|
|
4126
3590
|
state.progressWatches.length >=
|
|
4127
3591
|
(this.config.limits.maxWatches ?? PROGRESS_WATCH_DEFAULT_CAPACITY)) {
|
|
@@ -4183,11 +3647,11 @@ export class GatewayStore {
|
|
|
4183
3647
|
this.appendProgressWatchEvents(state, events);
|
|
4184
3648
|
state.progressWatches = retained;
|
|
4185
3649
|
}
|
|
4186
|
-
|
|
3650
|
+
settleProgressWatchesForConsentEdge(state, pair, now) {
|
|
4187
3651
|
const retained = [];
|
|
4188
3652
|
const events = [];
|
|
4189
3653
|
for (const watch of state.progressWatches) {
|
|
4190
|
-
if (!
|
|
3654
|
+
if (!progressWatchMatchesConsentEdge(watch, pair)) {
|
|
4191
3655
|
retained.push(watch);
|
|
4192
3656
|
continue;
|
|
4193
3657
|
}
|
|
@@ -4203,7 +3667,7 @@ export class GatewayStore {
|
|
|
4203
3667
|
finishMetadata(state, metadata, deliveryState, now, safeErrorCode) {
|
|
4204
3668
|
const counterKey = deliveryState;
|
|
4205
3669
|
state.accounting[counterKey] += 1;
|
|
4206
|
-
const pair =
|
|
3670
|
+
const pair = findConsentEdgeForMessage(state, metadata);
|
|
4207
3671
|
if (pair !== undefined) {
|
|
4208
3672
|
pair.counters[counterKey] += 1;
|
|
4209
3673
|
pair.updatedAt = now.toISOString();
|
|
@@ -4263,12 +3727,12 @@ export class GatewayStore {
|
|
|
4263
3727
|
}
|
|
4264
3728
|
return byMessageId;
|
|
4265
3729
|
}
|
|
4266
|
-
|
|
3730
|
+
validateAffectedConsentEdgeInFlightSettlements(state, pair, requested) {
|
|
4267
3731
|
if (requested.length > this.config.limits.maxInFlightMessages) {
|
|
4268
3732
|
throw new BridgeError("INVALID_PAIR_TERMINATION_PLAN", "Unpairing requires one normalized terminal settlement per affected in-flight message.");
|
|
4269
3733
|
}
|
|
4270
3734
|
const affectedIds = new Set(state.inFlight
|
|
4271
|
-
.filter((item) =>
|
|
3735
|
+
.filter((item) => consentEdgeMatchesMessage(pair, item))
|
|
4272
3736
|
.map((item) => item.messageId));
|
|
4273
3737
|
const byMessageId = new Map();
|
|
4274
3738
|
for (const settlement of requested) {
|
|
@@ -4321,10 +3785,10 @@ export class GatewayStore {
|
|
|
4321
3785
|
}
|
|
4322
3786
|
return settlements;
|
|
4323
3787
|
}
|
|
4324
|
-
|
|
3788
|
+
terminateAffectedConsentEdgeMessages(state, pair, now, safeErrorCode, inFlightSettlements) {
|
|
4325
3789
|
const settlements = [];
|
|
4326
|
-
const queued = state.queue.filter((item) =>
|
|
4327
|
-
state.queue = state.queue.filter((item) => !
|
|
3790
|
+
const queued = state.queue.filter((item) => consentEdgeMatchesMessage(pair, item));
|
|
3791
|
+
state.queue = state.queue.filter((item) => !consentEdgeMatchesMessage(pair, item));
|
|
4328
3792
|
for (const item of queued) {
|
|
4329
3793
|
this.transientBodies.delete(item.messageId);
|
|
4330
3794
|
state.accounting.queuedBytes -= item.bytes;
|
|
@@ -4334,8 +3798,8 @@ export class GatewayStore {
|
|
|
4334
3798
|
const deadlineReached = Date.parse(item.deadlineAt) <= now.getTime();
|
|
4335
3799
|
settlements.push(this.finishMetadata(state, item, deadlineReached ? "expired" : "abandoned", now, deadlineReached ? "MESSAGE_EXPIRED" : safeErrorCode));
|
|
4336
3800
|
}
|
|
4337
|
-
const inFlight = state.inFlight.filter((item) =>
|
|
4338
|
-
state.inFlight = state.inFlight.filter((item) => !
|
|
3801
|
+
const inFlight = state.inFlight.filter((item) => consentEdgeMatchesMessage(pair, item));
|
|
3802
|
+
state.inFlight = state.inFlight.filter((item) => !consentEdgeMatchesMessage(pair, item));
|
|
4339
3803
|
for (const item of inFlight) {
|
|
4340
3804
|
const requested = inFlightSettlements.get(item.messageId);
|
|
4341
3805
|
if (requested === undefined) {
|
|
@@ -4372,13 +3836,11 @@ export class GatewayStore {
|
|
|
4372
3836
|
}
|
|
4373
3837
|
for (const connector of state.connectors) {
|
|
4374
3838
|
connector.health = "offline";
|
|
4375
|
-
connector.compatibility = "expired";
|
|
4376
3839
|
connector.updatedAt = now.toISOString();
|
|
4377
3840
|
connector.safeErrorCode = "REOBSERVATION_REQUIRED";
|
|
4378
3841
|
}
|
|
4379
3842
|
for (const route of state.routes) {
|
|
4380
3843
|
route.state = route.enabled ? "stale" : "disabled";
|
|
4381
|
-
route.compatibility = "expired";
|
|
4382
3844
|
route.updatedAt = now.toISOString();
|
|
4383
3845
|
route.safeErrorCode = "REOBSERVATION_REQUIRED";
|
|
4384
3846
|
route.queueDepth = 0;
|
|
@@ -4424,7 +3886,7 @@ export class GatewayStore {
|
|
|
4424
3886
|
const route = this.requireAnyJournalRoute(state, journal);
|
|
4425
3887
|
this.assertSuccessionLedgerEmpty(state, route.alias);
|
|
4426
3888
|
this.assertNewSuccessionIdentityAvailable(state, route, journal.new);
|
|
4427
|
-
this.replaceCodexRouteForSuccession(state, route, journal.new, now, "stale"
|
|
3889
|
+
this.replaceCodexRouteForSuccession(state, route, journal.new, now, "stale");
|
|
4428
3890
|
state.codexSuccession = {
|
|
4429
3891
|
...journal,
|
|
4430
3892
|
stage: "recovery_forbidden",
|
|
@@ -4650,7 +4112,7 @@ export class GatewayStore {
|
|
|
4650
4112
|
// Never remove a lock that cannot be proven to belong to this instance.
|
|
4651
4113
|
}
|
|
4652
4114
|
}
|
|
4653
|
-
async loadStateFile(
|
|
4115
|
+
async loadStateFile(_now) {
|
|
4654
4116
|
let body;
|
|
4655
4117
|
try {
|
|
4656
4118
|
body = await this.readPrivateFile(this.stateFilePath, GATEWAY_MAX_STATE_FILE_BYTES);
|
|
@@ -4668,23 +4130,15 @@ export class GatewayStore {
|
|
|
4668
4130
|
catch {
|
|
4669
4131
|
throw new BridgeError("CORRUPT_GATEWAY_STATE", "The gateway controller state is not valid JSON.");
|
|
4670
4132
|
}
|
|
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
4133
|
if (!isPersistedState(parsed)) {
|
|
4682
4134
|
throw new BridgeError("CORRUPT_GATEWAY_STATE", "The gateway controller state failed strict schema validation.");
|
|
4683
4135
|
}
|
|
4684
4136
|
if (parsed.routes.some((route) => !this.config.allowedHosts.includes(route.binding.hostId)) ||
|
|
4685
4137
|
parsed.connectors.some((connector) => !this.config.allowedHosts.includes(connector.hostId)) ||
|
|
4686
4138
|
parsed.routes.length > this.config.limits.maxRoutes ||
|
|
4687
|
-
parsed.connectors.length >
|
|
4139
|
+
parsed.connectors.length >
|
|
4140
|
+
this.config.allowedHosts.length * gatewayProviders.length ||
|
|
4141
|
+
parsed.consentEdges.length > this.config.limits.maxConsentEdges ||
|
|
4688
4142
|
parsed.rateBuckets.length > this.config.limits.maxRoutes ||
|
|
4689
4143
|
parsed.progressWatches.length >
|
|
4690
4144
|
(this.config.limits.maxWatches ?? PROGRESS_WATCH_DEFAULT_CAPACITY) ||
|