agent-embassy 1.6.1 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -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.d.ts +1 -1
- package/dist/src/gateway/cli-copy.en.d.ts +3 -1
- package/dist/src/gateway/cli-copy.en.js +5 -2
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.js +2 -0
- package/dist/src/gateway/cli-copy.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +3 -1
- package/dist/src/gateway/cli-copy.zh-CN.js +5 -2
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +2 -2
- package/dist/src/gateway/cli.js +73 -6
- 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 -599
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-doctor.d.ts +33 -0
- package/dist/src/gateway/codex-doctor.js +148 -0
- package/dist/src/gateway/codex-doctor.js.map +1 -0
- package/dist/src/gateway/codex-local-transport.d.ts +13 -26
- package/dist/src/gateway/codex-local-transport.js +31 -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 +128 -59
- 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 +19 -44
- package/dist/src/gateway/dashboard-copy.en.js +42 -67
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +19 -44
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +19 -44
- package/dist/src/gateway/dashboard-copy.zh-CN.js +42 -67
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +41 -76
- package/dist/src/gateway/dashboard-model.js +167 -257
- 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 +77 -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 +175 -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 +19 -85
- package/dist/src/gateway/providers.js +132 -835
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +8 -9
- package/dist/src/gateway/server.js +63 -99
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +48 -51
- package/dist/src/gateway/service.js +748 -540
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +20 -29
- package/dist/src/gateway/store.js +306 -838
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +63 -54
- package/dist/src/gateway/types.js +51 -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, CONNECTOR_OBSERVATION_STALE_AFTER_MS, connectorHealthStates, directionId, deliveryStates, gatewayPublicSnapshotLimits, gatewayProviders, gatewayRegistrationIngressPrefixes, messageDirections, parseDirection, projectGatewayPublicSnapshot, routeRegistrationModes, routeStates, } from "./types.js";
|
|
12
11
|
const STATE_MARKER = ".agent-embassy-state";
|
|
13
12
|
const STATE_MARKER_CONTENT = "agent-embassy-state-v1\n";
|
|
14
13
|
const STATE_FILE = "gateway-state.json";
|
|
@@ -17,7 +16,6 @@ const MAX_MARKER_FILE_BYTES = 128;
|
|
|
17
16
|
const MAX_LOCK_FILE_BYTES = 4 * 1024;
|
|
18
17
|
export const GATEWAY_MAX_STATE_FILE_BYTES = 8 * 1024 * 1024;
|
|
19
18
|
const DEFAULT_RETAINED_BODY_BYTES = 1 * 1024 * 1024;
|
|
20
|
-
const COMPATIBILITY_ATTESTATION_CAPACITY = 16;
|
|
21
19
|
const ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
22
20
|
const HOST_PATTERN = /^[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
23
21
|
const PRIVATE_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
|
|
@@ -39,7 +37,6 @@ class PostRenamePersistenceError extends BridgeError {
|
|
|
39
37
|
}
|
|
40
38
|
const PROVIDERS = new Set(gatewayProviders);
|
|
41
39
|
const CONNECTOR_HEALTH = new Set(connectorHealthStates);
|
|
42
|
-
const COMPATIBILITY = new Set(compatibilityStates);
|
|
43
40
|
const ROUTE_STATES = new Set(routeStates);
|
|
44
41
|
const REGISTRATION_MODES = new Set(routeRegistrationModes);
|
|
45
42
|
const DIRECTIONS = new Set(messageDirections);
|
|
@@ -221,7 +218,6 @@ function isRouteRecord(value) {
|
|
|
221
218
|
"registrationMode",
|
|
222
219
|
"enabled",
|
|
223
220
|
"state",
|
|
224
|
-
"compatibility",
|
|
225
221
|
"busyPolicy",
|
|
226
222
|
"registeredAt",
|
|
227
223
|
"updatedAt",
|
|
@@ -238,8 +234,6 @@ function isRouteRecord(value) {
|
|
|
238
234
|
typeof value.enabled === "boolean" &&
|
|
239
235
|
typeof value.state === "string" &&
|
|
240
236
|
ROUTE_STATES.has(value.state) &&
|
|
241
|
-
typeof value.compatibility === "string" &&
|
|
242
|
-
COMPATIBILITY.has(value.compatibility) &&
|
|
243
237
|
value.busyPolicy === "queue" &&
|
|
244
238
|
isIsoTimestamp(value.registeredAt) &&
|
|
245
239
|
isIsoTimestamp(value.updatedAt) &&
|
|
@@ -248,23 +242,19 @@ function isRouteRecord(value) {
|
|
|
248
242
|
isRouteCounters(value.counters) &&
|
|
249
243
|
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)));
|
|
250
244
|
}
|
|
251
|
-
function
|
|
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
|
-
throw new BridgeError("INVALID_GATEWAY_ROUTE_PAIR", "Gateway pairs must connect one Claude route and one Codex route.");
|
|
1011
|
+
function canonicalConsentEndpoints(source, target) {
|
|
1012
|
+
const endpoints = [
|
|
1013
|
+
{ alias: source.alias, provider: source.binding.provider,
|
|
1014
|
+
ownerLease: source.binding.ownerLease },
|
|
1015
|
+
{ alias: target.alias, provider: target.binding.provider,
|
|
1016
|
+
ownerLease: target.binding.ownerLease },
|
|
1017
|
+
];
|
|
1018
|
+
endpoints.sort(compareConsentEndpoints);
|
|
1019
|
+
return endpoints;
|
|
1482
1020
|
}
|
|
1483
|
-
function
|
|
1484
|
-
return (
|
|
1485
|
-
((message.sourceAlias === pair.claudeAlias &&
|
|
1486
|
-
message.targetAlias === pair.codexAlias) ||
|
|
1487
|
-
(message.sourceAlias === pair.codexAlias &&
|
|
1488
|
-
message.targetAlias === pair.claudeAlias)));
|
|
1021
|
+
function consentEdgeKey(endpoints) {
|
|
1022
|
+
return endpoints.map(({ provider, alias }) => `${provider}\0${alias}`).join("\0");
|
|
1489
1023
|
}
|
|
1490
|
-
function
|
|
1491
|
-
|
|
1024
|
+
function consentEdgeMatchesMessage(pair, message) {
|
|
1025
|
+
const aliases = "endpoints" in pair
|
|
1026
|
+
? pair.endpoints.map((endpoint) => endpoint.alias)
|
|
1027
|
+
: pair.aliases;
|
|
1028
|
+
return message.pair === true &&
|
|
1029
|
+
((message.sourceAlias === aliases[0] && message.targetAlias === aliases[1]) ||
|
|
1030
|
+
(message.sourceAlias === aliases[1] && message.targetAlias === aliases[0]));
|
|
1492
1031
|
}
|
|
1493
|
-
function
|
|
1032
|
+
function findConsentEdgeForMessage(state, message) {
|
|
1033
|
+
return state.consentEdges.find((edge) => consentEdgeMatchesMessage(edge, message));
|
|
1034
|
+
}
|
|
1035
|
+
function renameConsentEdgeAlias(state, previousAlias, newAlias, now) {
|
|
1494
1036
|
if (previousAlias === newAlias)
|
|
1495
1037
|
return;
|
|
1496
|
-
for (const
|
|
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
|
});
|
|
@@ -3231,18 +2708,31 @@ export class GatewayStore {
|
|
|
3231
2708
|
async publicSnapshot() {
|
|
3232
2709
|
return this.mutate(async (state, now) => {
|
|
3233
2710
|
const connectors = state.connectors
|
|
3234
|
-
.map((connector) =>
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
2711
|
+
.map((connector) => {
|
|
2712
|
+
const observedAt = connector.lastSeenAt === undefined
|
|
2713
|
+
? undefined
|
|
2714
|
+
: Date.parse(connector.lastSeenAt);
|
|
2715
|
+
const observationAgeMs = observedAt === undefined || !Number.isFinite(observedAt)
|
|
2716
|
+
? undefined
|
|
2717
|
+
: Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, now.getTime() - observedAt));
|
|
2718
|
+
const health = connector.health === "healthy" &&
|
|
2719
|
+
(observationAgeMs === undefined ||
|
|
2720
|
+
observationAgeMs > CONNECTOR_OBSERVATION_STALE_AFTER_MS)
|
|
2721
|
+
? "degraded"
|
|
2722
|
+
: connector.health;
|
|
2723
|
+
return {
|
|
2724
|
+
provider: connector.provider,
|
|
2725
|
+
host: connector.hostId,
|
|
2726
|
+
health,
|
|
2727
|
+
protocol: connector.protocol,
|
|
2728
|
+
protocolVersion: connector.protocolVersion,
|
|
2729
|
+
...(connector.lastSeenAt ? { lastSeenAt: connector.lastSeenAt } : {}),
|
|
2730
|
+
...(observationAgeMs === undefined ? {} : { observationAgeMs }),
|
|
2731
|
+
...(connector.safeErrorCode
|
|
2732
|
+
? { safeErrorCode: connector.safeErrorCode }
|
|
2733
|
+
: {}),
|
|
2734
|
+
};
|
|
2735
|
+
})
|
|
3246
2736
|
.sort((left, right) => `${left.provider}:${left.host}`.localeCompare(`${right.provider}:${right.host}`));
|
|
3247
2737
|
const oldestQueuedAtByTarget = new Map();
|
|
3248
2738
|
for (const item of state.queue) {
|
|
@@ -3261,7 +2751,6 @@ export class GatewayStore {
|
|
|
3261
2751
|
host: route.binding.hostId,
|
|
3262
2752
|
enabled: route.enabled,
|
|
3263
2753
|
state: route.state,
|
|
3264
|
-
compatibility: route.compatibility,
|
|
3265
2754
|
busyPolicy: route.busyPolicy,
|
|
3266
2755
|
...(route.lastSeenAt ? { lastSeenAt: route.lastSeenAt } : {}),
|
|
3267
2756
|
queueDepth: route.queueDepth,
|
|
@@ -3273,14 +2762,13 @@ export class GatewayStore {
|
|
|
3273
2762
|
};
|
|
3274
2763
|
})
|
|
3275
2764
|
.sort((left, right) => left.alias.localeCompare(right.alias));
|
|
3276
|
-
const
|
|
3277
|
-
.map((
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
counters: { ...pair.counters },
|
|
2765
|
+
const consentEdges = state.consentEdges
|
|
2766
|
+
.map((edge) => ({
|
|
2767
|
+
endpoints: edge.endpoints.map(({ alias, provider }) => ({ alias, provider })),
|
|
2768
|
+
host: edge.endpoints[0].alias.slice(edge.endpoints[0].alias.lastIndexOf("@") + 1),
|
|
2769
|
+
counters: { ...edge.counters },
|
|
3282
2770
|
}))
|
|
3283
|
-
.sort((left, right) =>
|
|
2771
|
+
.sort((left, right) => consentEdgeKey(left.endpoints).localeCompare(consentEdgeKey(right.endpoints)));
|
|
3284
2772
|
const progressWatches = state.progressWatches
|
|
3285
2773
|
.map((watch) => ({
|
|
3286
2774
|
conversationIdSuffix: watch.conversationId.slice(-8),
|
|
@@ -3368,14 +2856,14 @@ export class GatewayStore {
|
|
|
3368
2856
|
return `${left.code}\0${left.provider ?? ""}\0${left.host ?? ""}\0${left.alias ?? ""}`.localeCompare(`${right.code}\0${right.provider ?? ""}\0${right.host ?? ""}\0${right.alias ?? ""}`);
|
|
3369
2857
|
});
|
|
3370
2858
|
return projectGatewayPublicSnapshot({
|
|
3371
|
-
schemaVersion:
|
|
2859
|
+
schemaVersion: 2,
|
|
3372
2860
|
generatedAt: now.toISOString(),
|
|
3373
2861
|
inboundMode: this.config.inboundMode,
|
|
3374
2862
|
health,
|
|
3375
2863
|
connectors,
|
|
3376
2864
|
availablePeers: [],
|
|
3377
2865
|
routes,
|
|
3378
|
-
|
|
2866
|
+
consentEdges,
|
|
3379
2867
|
progressWatches,
|
|
3380
2868
|
progressWatchEvents,
|
|
3381
2869
|
deadlinePressure: {
|
|
@@ -3394,7 +2882,7 @@ export class GatewayStore {
|
|
|
3394
2882
|
connectors: 0,
|
|
3395
2883
|
availablePeers: 0,
|
|
3396
2884
|
routes: 0,
|
|
3397
|
-
|
|
2885
|
+
consentEdges: 0,
|
|
3398
2886
|
progressWatches: 0,
|
|
3399
2887
|
progressWatchEvents: 0,
|
|
3400
2888
|
messages: 0,
|
|
@@ -3517,7 +3005,7 @@ export class GatewayStore {
|
|
|
3517
3005
|
throw new BridgeError("CODEX_SUCCESSION_BINDING_COLLISION", "The new Codex identity collides with another persisted route.");
|
|
3518
3006
|
}
|
|
3519
3007
|
}
|
|
3520
|
-
replaceCodexRouteForSuccession(state, route, identity, now, routeState
|
|
3008
|
+
replaceCodexRouteForSuccession(state, route, identity, now, routeState) {
|
|
3521
3009
|
const oldAlias = route.alias;
|
|
3522
3010
|
const affectedAliases = new Set([oldAlias, identity.alias]);
|
|
3523
3011
|
this.settleProgressWatchesForAliases(state, affectedAliases, now);
|
|
@@ -3526,7 +3014,6 @@ export class GatewayStore {
|
|
|
3526
3014
|
route.registrationMode = "explicit_opt_in";
|
|
3527
3015
|
route.enabled = true;
|
|
3528
3016
|
route.state = routeState;
|
|
3529
|
-
route.compatibility = compatibility;
|
|
3530
3017
|
route.registeredAt = now.toISOString();
|
|
3531
3018
|
route.updatedAt = now.toISOString();
|
|
3532
3019
|
route.queueDepth = 0;
|
|
@@ -3545,7 +3032,7 @@ export class GatewayStore {
|
|
|
3545
3032
|
record.targetAlias !== identity.alias);
|
|
3546
3033
|
state.rateBuckets = state.rateBuckets.filter((bucket) => bucket.sourceAlias !== oldAlias &&
|
|
3547
3034
|
bucket.sourceAlias !== identity.alias);
|
|
3548
|
-
|
|
3035
|
+
removeConsentEdgesForAliases(state, affectedAliases);
|
|
3549
3036
|
}
|
|
3550
3037
|
assertAllowedIdentity(identity) {
|
|
3551
3038
|
if (!isPrivateEndpointIdentity(identity)) {
|
|
@@ -3555,12 +3042,11 @@ export class GatewayStore {
|
|
|
3555
3042
|
throw new BridgeError("GATEWAY_HOST_NOT_ALLOWED", "The route host is not in the fixed gateway allowlist.");
|
|
3556
3043
|
}
|
|
3557
3044
|
}
|
|
3558
|
-
|
|
3045
|
+
requireObservedEndpoint(state, binding) {
|
|
3559
3046
|
const connector = state.connectors.find((candidate) => sameEndpoint(candidate, binding));
|
|
3560
3047
|
if (!connector ||
|
|
3561
|
-
!["healthy", "degraded"].includes(connector.health)
|
|
3562
|
-
|
|
3563
|
-
throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The exact compatible endpoint generation must be observed before Codex succession can publish or activate it.");
|
|
3048
|
+
!["healthy", "degraded"].includes(connector.health)) {
|
|
3049
|
+
throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The exact live endpoint generation must be observed before Codex succession can publish or activate it.");
|
|
3564
3050
|
}
|
|
3565
3051
|
}
|
|
3566
3052
|
validateRouteInput(input) {
|
|
@@ -3574,11 +3060,15 @@ export class GatewayStore {
|
|
|
3574
3060
|
if (aliasHost !== input.binding.hostId) {
|
|
3575
3061
|
throw new BridgeError("ROUTE_HOST_MISMATCH", "The public alias host must match the allowlisted private route host.");
|
|
3576
3062
|
}
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
(input.binding.provider
|
|
3580
|
-
|
|
3581
|
-
|
|
3063
|
+
const requiredPrefix = gatewayRegistrationIngressPrefixes[input.binding.provider];
|
|
3064
|
+
if (requiredPrefix !== undefined && !input.alias.startsWith(requiredPrefix)) {
|
|
3065
|
+
throw new BridgeError("INVALID_GATEWAY_ALIAS", `The ${input.binding.provider} registration alias must use its required ingress prefix.`);
|
|
3066
|
+
}
|
|
3067
|
+
if ((input.binding.provider === "claude" &&
|
|
3068
|
+
input.registrationMode !== "selected_live_peer") ||
|
|
3069
|
+
(input.binding.provider !== "claude" &&
|
|
3070
|
+
input.registrationMode !== "explicit_opt_in")) {
|
|
3071
|
+
throw new BridgeError("ROUTE_OPT_IN_REQUIRED", "Non-Claude routes require explicit opt-in and Claude routes require a selected live peer.");
|
|
3582
3072
|
}
|
|
3583
3073
|
}
|
|
3584
3074
|
requireOwnedRoute(state, alias, ownerLease) {
|
|
@@ -3602,7 +3092,6 @@ export class GatewayStore {
|
|
|
3602
3092
|
route.registrationMode !== "explicit_opt_in" ||
|
|
3603
3093
|
!route.enabled ||
|
|
3604
3094
|
route.state !== "stale" ||
|
|
3605
|
-
route.compatibility !== "expired" ||
|
|
3606
3095
|
route.queueDepth !== 0 ||
|
|
3607
3096
|
state.queue.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias) ||
|
|
3608
3097
|
state.inFlight.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias)) {
|
|
@@ -3631,52 +3120,46 @@ export class GatewayStore {
|
|
|
3631
3120
|
const route = state.routes.find((candidate) => candidate.alias === alias);
|
|
3632
3121
|
if (!route ||
|
|
3633
3122
|
!route.enabled ||
|
|
3634
|
-
!["idle", "busy", "awaiting_approval"].includes(route.state)
|
|
3635
|
-
route.compatibility !== "compatible") {
|
|
3123
|
+
!["idle", "busy", "awaiting_approval"].includes(route.state)) {
|
|
3636
3124
|
throw new BridgeError("ROUTE_UNAVAILABLE", "The selected route is not currently enabled and positively observed.", true);
|
|
3637
3125
|
}
|
|
3638
3126
|
return route;
|
|
3639
3127
|
}
|
|
3640
|
-
|
|
3128
|
+
requireConsentEdgeRoutes(state, input, requireAvailable = true) {
|
|
3641
3129
|
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.");
|
|
3130
|
+
!Array.isArray(input.aliases) ||
|
|
3131
|
+
input.aliases.length !== 2 ||
|
|
3132
|
+
input.aliases.some((alias) => typeof alias !== "string" || !ALIAS_PATTERN.test(alias)) ||
|
|
3133
|
+
input.aliases[0] === input.aliases[1]) {
|
|
3134
|
+
throw new BridgeError("INVALID_CONSENT_EDGE", "A consent edge requires two distinct normalized aliases.");
|
|
3648
3135
|
}
|
|
3649
3136
|
const resolve = (alias) => {
|
|
3650
3137
|
if (requireAvailable)
|
|
3651
3138
|
return this.requireAvailableRoute(state, alias);
|
|
3652
3139
|
const route = state.routes.find((candidate) => candidate.alias === alias);
|
|
3653
3140
|
if (route === undefined) {
|
|
3654
|
-
throw new BridgeError("
|
|
3141
|
+
throw new BridgeError("CONSENT_EDGE_ROUTE_NOT_FOUND", "The consent edge references a route that is not registered.");
|
|
3655
3142
|
}
|
|
3656
3143
|
return route;
|
|
3657
3144
|
};
|
|
3658
|
-
const
|
|
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);
|
|
3145
|
+
const left = resolve(input.aliases[0]);
|
|
3146
|
+
const right = resolve(input.aliases[1]);
|
|
3147
|
+
if (left.binding.provider === right.binding.provider ||
|
|
3148
|
+
left.binding.hostId !== right.binding.hostId) {
|
|
3149
|
+
throw new BridgeError("INVALID_CONSENT_EDGE", "A consent edge must connect distinct providers on the same host.");
|
|
3150
|
+
}
|
|
3151
|
+
return [left, right];
|
|
3152
|
+
}
|
|
3153
|
+
requireExactConsentEdge(state, left, right) {
|
|
3154
|
+
const endpoints = canonicalConsentEndpoints(left, right);
|
|
3155
|
+
const pair = state.consentEdges.find((candidate) => consentEdgeKey(candidate.endpoints) === consentEdgeKey(endpoints));
|
|
3672
3156
|
if (pair === undefined ||
|
|
3673
|
-
pair.
|
|
3674
|
-
pair.codexOwnerLease !== codex.binding.ownerLease) {
|
|
3157
|
+
pair.endpoints.some((endpoint, index) => endpoint.ownerLease !== endpoints[index].ownerLease)) {
|
|
3675
3158
|
throw new BridgeError("SENDER_NOT_PAIRED", "The two exact route authorities do not share a consent edge.");
|
|
3676
3159
|
}
|
|
3677
3160
|
return pair;
|
|
3678
3161
|
}
|
|
3679
|
-
validateTransientNativeClaudePeer(state, peer,
|
|
3162
|
+
validateTransientNativeClaudePeer(state, peer, route) {
|
|
3680
3163
|
if (!isObject(peer) ||
|
|
3681
3164
|
!hasOnlyKeys(peer, ["alias", "binding"]) ||
|
|
3682
3165
|
typeof peer.alias !== "string" ||
|
|
@@ -3687,17 +3170,16 @@ export class GatewayStore {
|
|
|
3687
3170
|
}
|
|
3688
3171
|
this.assertAllowedIdentity(endpointOf(peer.binding));
|
|
3689
3172
|
const aliasHost = peer.alias.slice(peer.alias.lastIndexOf("@") + 1);
|
|
3690
|
-
if (
|
|
3173
|
+
if (route.binding.provider === "claude" ||
|
|
3691
3174
|
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
|
|
3175
|
+
peer.binding.hostId !== route.binding.hostId ||
|
|
3176
|
+
peer.alias === route.alias) {
|
|
3177
|
+
throw new BridgeError("NATIVE_PEER_SCOPE_MISMATCH", "The transient Claude peer and registered route must be distinct providers on the same allowlisted host.");
|
|
3695
3178
|
}
|
|
3696
3179
|
const connector = state.connectors.find((candidate) => sameEndpoint(candidate, peer.binding));
|
|
3697
3180
|
if (!connector ||
|
|
3698
|
-
!["healthy", "degraded"].includes(connector.health)
|
|
3699
|
-
connector
|
|
3700
|
-
throw new BridgeError("NATIVE_PEER_ENDPOINT_NOT_OBSERVED", "The transient Claude peer's exact compatible connector generation is not live.", true);
|
|
3181
|
+
!["healthy", "degraded"].includes(connector.health)) {
|
|
3182
|
+
throw new BridgeError("NATIVE_PEER_ENDPOINT_NOT_OBSERVED", "The transient Claude peer's exact connector generation is not live.", true);
|
|
3701
3183
|
}
|
|
3702
3184
|
}
|
|
3703
3185
|
enqueueResolvedMessage(state, now, input, sides) {
|
|
@@ -3841,7 +3323,7 @@ export class GatewayStore {
|
|
|
3841
3323
|
this.transientBodies.set(messageId, input.body);
|
|
3842
3324
|
state.accounting.accepted += 1;
|
|
3843
3325
|
state.accounting.bytesAccepted += bytes;
|
|
3844
|
-
const pair =
|
|
3326
|
+
const pair = findConsentEdgeForMessage(state, metadata);
|
|
3845
3327
|
if (pair !== undefined) {
|
|
3846
3328
|
pair.counters.accepted += 1;
|
|
3847
3329
|
pair.counters.bytesAccepted += bytes;
|
|
@@ -3931,7 +3413,7 @@ export class GatewayStore {
|
|
|
3931
3413
|
state.accounting.rejected += 1;
|
|
3932
3414
|
if (sides.sourceRoute)
|
|
3933
3415
|
sides.sourceRoute.counters.rejected += 1;
|
|
3934
|
-
const pair =
|
|
3416
|
+
const pair = findConsentEdgeForMessage(state, sides);
|
|
3935
3417
|
if (pair !== undefined) {
|
|
3936
3418
|
pair.counters.rejected += 1;
|
|
3937
3419
|
pair.updatedAt = now.toISOString();
|
|
@@ -4035,13 +3517,13 @@ export class GatewayStore {
|
|
|
4035
3517
|
throw new BridgeError("INVALID_PROGRESS_WATCH", "The message watch activity is malformed or contradictory.");
|
|
4036
3518
|
}
|
|
4037
3519
|
const pair = sides.pair === true
|
|
4038
|
-
? state.
|
|
3520
|
+
? state.consentEdges.find((candidate) => consentEdgeMatchesMessage(candidate, sides))
|
|
4039
3521
|
: undefined;
|
|
4040
3522
|
const index = state.progressWatches.findIndex((watch) => watch.conversationId === activity.conversationId);
|
|
4041
3523
|
const existing = state.progressWatches[index];
|
|
4042
3524
|
if (existing !== undefined) {
|
|
4043
3525
|
if (pair === undefined ||
|
|
4044
|
-
!
|
|
3526
|
+
!progressWatchMatchesConsentEdge(existing, pair) ||
|
|
4045
3527
|
!((sides.sourceAlias === existing.ownerAlias &&
|
|
4046
3528
|
sides.targetAlias === existing.workerAlias) ||
|
|
4047
3529
|
(sides.sourceAlias === existing.workerAlias &&
|
|
@@ -4080,12 +3562,8 @@ export class GatewayStore {
|
|
|
4080
3562
|
conversationId: activity.conversationId,
|
|
4081
3563
|
ownerAlias: sides.sourceAlias,
|
|
4082
3564
|
workerAlias: sides.targetAlias,
|
|
4083
|
-
ownerLease:
|
|
4084
|
-
|
|
4085
|
-
: pair.codexOwnerLease,
|
|
4086
|
-
workerLease: sides.targetAlias === pair.claudeAlias
|
|
4087
|
-
? pair.claudeOwnerLease
|
|
4088
|
-
: pair.codexOwnerLease,
|
|
3565
|
+
ownerLease: pair.endpoints.find(({ alias }) => alias === sides.sourceAlias).ownerLease,
|
|
3566
|
+
workerLease: pair.endpoints.find(({ alias }) => alias === sides.targetAlias).ownerLease,
|
|
4089
3567
|
idleMs: activity.openIdleMs,
|
|
4090
3568
|
at: now.getTime(),
|
|
4091
3569
|
});
|
|
@@ -4121,7 +3599,7 @@ export class GatewayStore {
|
|
|
4121
3599
|
});
|
|
4122
3600
|
}
|
|
4123
3601
|
installProgressWatch(state, watch, pair, now) {
|
|
4124
|
-
const replacedIndex = state.progressWatches.findIndex((candidate) =>
|
|
3602
|
+
const replacedIndex = state.progressWatches.findIndex((candidate) => progressWatchMatchesConsentEdge(candidate, pair));
|
|
4125
3603
|
if (replacedIndex < 0 &&
|
|
4126
3604
|
state.progressWatches.length >=
|
|
4127
3605
|
(this.config.limits.maxWatches ?? PROGRESS_WATCH_DEFAULT_CAPACITY)) {
|
|
@@ -4183,11 +3661,11 @@ export class GatewayStore {
|
|
|
4183
3661
|
this.appendProgressWatchEvents(state, events);
|
|
4184
3662
|
state.progressWatches = retained;
|
|
4185
3663
|
}
|
|
4186
|
-
|
|
3664
|
+
settleProgressWatchesForConsentEdge(state, pair, now) {
|
|
4187
3665
|
const retained = [];
|
|
4188
3666
|
const events = [];
|
|
4189
3667
|
for (const watch of state.progressWatches) {
|
|
4190
|
-
if (!
|
|
3668
|
+
if (!progressWatchMatchesConsentEdge(watch, pair)) {
|
|
4191
3669
|
retained.push(watch);
|
|
4192
3670
|
continue;
|
|
4193
3671
|
}
|
|
@@ -4203,7 +3681,7 @@ export class GatewayStore {
|
|
|
4203
3681
|
finishMetadata(state, metadata, deliveryState, now, safeErrorCode) {
|
|
4204
3682
|
const counterKey = deliveryState;
|
|
4205
3683
|
state.accounting[counterKey] += 1;
|
|
4206
|
-
const pair =
|
|
3684
|
+
const pair = findConsentEdgeForMessage(state, metadata);
|
|
4207
3685
|
if (pair !== undefined) {
|
|
4208
3686
|
pair.counters[counterKey] += 1;
|
|
4209
3687
|
pair.updatedAt = now.toISOString();
|
|
@@ -4263,12 +3741,12 @@ export class GatewayStore {
|
|
|
4263
3741
|
}
|
|
4264
3742
|
return byMessageId;
|
|
4265
3743
|
}
|
|
4266
|
-
|
|
3744
|
+
validateAffectedConsentEdgeInFlightSettlements(state, pair, requested) {
|
|
4267
3745
|
if (requested.length > this.config.limits.maxInFlightMessages) {
|
|
4268
3746
|
throw new BridgeError("INVALID_PAIR_TERMINATION_PLAN", "Unpairing requires one normalized terminal settlement per affected in-flight message.");
|
|
4269
3747
|
}
|
|
4270
3748
|
const affectedIds = new Set(state.inFlight
|
|
4271
|
-
.filter((item) =>
|
|
3749
|
+
.filter((item) => consentEdgeMatchesMessage(pair, item))
|
|
4272
3750
|
.map((item) => item.messageId));
|
|
4273
3751
|
const byMessageId = new Map();
|
|
4274
3752
|
for (const settlement of requested) {
|
|
@@ -4321,10 +3799,10 @@ export class GatewayStore {
|
|
|
4321
3799
|
}
|
|
4322
3800
|
return settlements;
|
|
4323
3801
|
}
|
|
4324
|
-
|
|
3802
|
+
terminateAffectedConsentEdgeMessages(state, pair, now, safeErrorCode, inFlightSettlements) {
|
|
4325
3803
|
const settlements = [];
|
|
4326
|
-
const queued = state.queue.filter((item) =>
|
|
4327
|
-
state.queue = state.queue.filter((item) => !
|
|
3804
|
+
const queued = state.queue.filter((item) => consentEdgeMatchesMessage(pair, item));
|
|
3805
|
+
state.queue = state.queue.filter((item) => !consentEdgeMatchesMessage(pair, item));
|
|
4328
3806
|
for (const item of queued) {
|
|
4329
3807
|
this.transientBodies.delete(item.messageId);
|
|
4330
3808
|
state.accounting.queuedBytes -= item.bytes;
|
|
@@ -4334,8 +3812,8 @@ export class GatewayStore {
|
|
|
4334
3812
|
const deadlineReached = Date.parse(item.deadlineAt) <= now.getTime();
|
|
4335
3813
|
settlements.push(this.finishMetadata(state, item, deadlineReached ? "expired" : "abandoned", now, deadlineReached ? "MESSAGE_EXPIRED" : safeErrorCode));
|
|
4336
3814
|
}
|
|
4337
|
-
const inFlight = state.inFlight.filter((item) =>
|
|
4338
|
-
state.inFlight = state.inFlight.filter((item) => !
|
|
3815
|
+
const inFlight = state.inFlight.filter((item) => consentEdgeMatchesMessage(pair, item));
|
|
3816
|
+
state.inFlight = state.inFlight.filter((item) => !consentEdgeMatchesMessage(pair, item));
|
|
4339
3817
|
for (const item of inFlight) {
|
|
4340
3818
|
const requested = inFlightSettlements.get(item.messageId);
|
|
4341
3819
|
if (requested === undefined) {
|
|
@@ -4372,13 +3850,11 @@ export class GatewayStore {
|
|
|
4372
3850
|
}
|
|
4373
3851
|
for (const connector of state.connectors) {
|
|
4374
3852
|
connector.health = "offline";
|
|
4375
|
-
connector.compatibility = "expired";
|
|
4376
3853
|
connector.updatedAt = now.toISOString();
|
|
4377
3854
|
connector.safeErrorCode = "REOBSERVATION_REQUIRED";
|
|
4378
3855
|
}
|
|
4379
3856
|
for (const route of state.routes) {
|
|
4380
3857
|
route.state = route.enabled ? "stale" : "disabled";
|
|
4381
|
-
route.compatibility = "expired";
|
|
4382
3858
|
route.updatedAt = now.toISOString();
|
|
4383
3859
|
route.safeErrorCode = "REOBSERVATION_REQUIRED";
|
|
4384
3860
|
route.queueDepth = 0;
|
|
@@ -4424,7 +3900,7 @@ export class GatewayStore {
|
|
|
4424
3900
|
const route = this.requireAnyJournalRoute(state, journal);
|
|
4425
3901
|
this.assertSuccessionLedgerEmpty(state, route.alias);
|
|
4426
3902
|
this.assertNewSuccessionIdentityAvailable(state, route, journal.new);
|
|
4427
|
-
this.replaceCodexRouteForSuccession(state, route, journal.new, now, "stale"
|
|
3903
|
+
this.replaceCodexRouteForSuccession(state, route, journal.new, now, "stale");
|
|
4428
3904
|
state.codexSuccession = {
|
|
4429
3905
|
...journal,
|
|
4430
3906
|
stage: "recovery_forbidden",
|
|
@@ -4650,7 +4126,7 @@ export class GatewayStore {
|
|
|
4650
4126
|
// Never remove a lock that cannot be proven to belong to this instance.
|
|
4651
4127
|
}
|
|
4652
4128
|
}
|
|
4653
|
-
async loadStateFile(
|
|
4129
|
+
async loadStateFile(_now) {
|
|
4654
4130
|
let body;
|
|
4655
4131
|
try {
|
|
4656
4132
|
body = await this.readPrivateFile(this.stateFilePath, GATEWAY_MAX_STATE_FILE_BYTES);
|
|
@@ -4668,23 +4144,15 @@ export class GatewayStore {
|
|
|
4668
4144
|
catch {
|
|
4669
4145
|
throw new BridgeError("CORRUPT_GATEWAY_STATE", "The gateway controller state is not valid JSON.");
|
|
4670
4146
|
}
|
|
4671
|
-
parsed = migratePreUnconfirmedCounters(parsed);
|
|
4672
|
-
parsed = migratePreSuccessionJournal(parsed);
|
|
4673
|
-
parsed = migratePrePairGraph(parsed);
|
|
4674
|
-
parsed = migratePreProgressWatches(parsed);
|
|
4675
|
-
parsed = migratePreCompatibilityAttestations(parsed);
|
|
4676
|
-
parsed = migratePreCodexEndpointRefreshJournal(parsed);
|
|
4677
|
-
parsed = migratePreCodexOrphanRemovalJournal(parsed);
|
|
4678
|
-
parsed = migrateLegacyHopCounts(parsed);
|
|
4679
|
-
parsed = migrateLegacyCompatibilityCertifications(parsed);
|
|
4680
|
-
parsed = migrateV14ProgressWatchState(parsed, now.toISOString());
|
|
4681
4147
|
if (!isPersistedState(parsed)) {
|
|
4682
4148
|
throw new BridgeError("CORRUPT_GATEWAY_STATE", "The gateway controller state failed strict schema validation.");
|
|
4683
4149
|
}
|
|
4684
4150
|
if (parsed.routes.some((route) => !this.config.allowedHosts.includes(route.binding.hostId)) ||
|
|
4685
4151
|
parsed.connectors.some((connector) => !this.config.allowedHosts.includes(connector.hostId)) ||
|
|
4686
4152
|
parsed.routes.length > this.config.limits.maxRoutes ||
|
|
4687
|
-
parsed.connectors.length >
|
|
4153
|
+
parsed.connectors.length >
|
|
4154
|
+
this.config.allowedHosts.length * gatewayProviders.length ||
|
|
4155
|
+
parsed.consentEdges.length > this.config.limits.maxConsentEdges ||
|
|
4688
4156
|
parsed.rateBuckets.length > this.config.limits.maxRoutes ||
|
|
4689
4157
|
parsed.progressWatches.length >
|
|
4690
4158
|
(this.config.limits.maxWatches ?? PROGRESS_WATCH_DEFAULT_CAPACITY) ||
|