agent-embassy 1.8.2 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +23 -3
- package/README.zh-CN.md +23 -3
- package/SECURITY.md +30 -6
- package/dist/src/gateway/cli-copy.en.d.ts +1 -1
- package/dist/src/gateway/cli-copy.en.js +8 -0
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.js +8 -0
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +8 -3
- package/dist/src/gateway/cli.js +191 -19
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/config.d.ts +2 -0
- package/dist/src/gateway/config.js +1 -1
- package/dist/src/gateway/config.js.map +1 -1
- package/dist/src/gateway/control.d.ts +79 -25
- package/dist/src/gateway/control.js +98 -18
- 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 +2 -0
- package/dist/src/gateway/dashboard-copy.en.js +2 -0
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +2 -0
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +2 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.js +2 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +3 -0
- package/dist/src/gateway/dashboard-model.js +3 -0
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/federation-nodes.d.ts +18 -0
- package/dist/src/gateway/federation-nodes.js +142 -0
- package/dist/src/gateway/federation-nodes.js.map +1 -0
- package/dist/src/gateway/live-dashboard-app/app.js +5 -4
- package/dist/src/gateway/peer-client.d.ts +57 -0
- package/dist/src/gateway/peer-client.js +208 -0
- package/dist/src/gateway/peer-client.js.map +1 -0
- package/dist/src/gateway/peer-mailbox.d.ts +78 -0
- package/dist/src/gateway/peer-mailbox.js +168 -0
- package/dist/src/gateway/peer-mailbox.js.map +1 -0
- package/dist/src/gateway/peer-protocol.d.ts +107 -0
- package/dist/src/gateway/peer-protocol.js +72 -0
- package/dist/src/gateway/peer-protocol.js.map +1 -0
- package/dist/src/gateway/peer-stdio.d.ts +23 -0
- package/dist/src/gateway/peer-stdio.js +100 -0
- package/dist/src/gateway/peer-stdio.js.map +1 -0
- package/dist/src/gateway/provenance-envelope.js +6 -1
- package/dist/src/gateway/provenance-envelope.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +5 -2
- package/dist/src/gateway/providers.js +10 -7
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +4 -1
- package/dist/src/gateway/server.js +21 -8
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +19 -0
- package/dist/src/gateway/service.js +437 -37
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/state-v2-to-v3.js +1 -0
- package/dist/src/gateway/state-v2-to-v3.js.map +1 -1
- package/dist/src/gateway/store.d.ts +9 -1
- package/dist/src/gateway/store.js +183 -43
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +7 -6
- package/dist/src/gateway/types.js +5 -10
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +23 -8
- package/docs/CONFIGURATION.zh-CN.md +12 -4
- package/docs/DASHBOARD.md +2 -2
- package/docs/DASHBOARD.zh-CN.md +2 -2
- package/docs/DELIVERY.md +3 -1
- package/docs/DELIVERY.zh-CN.md +3 -1
- package/docs/GATEWAY-ARCHITECTURE.md +6 -4
- package/package.json +1 -1
- package/skills/embassy-peer/SKILL.md +32 -4
- package/skills/embassy-peer/agents/openai.yaml +2 -2
|
@@ -5,6 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { BridgeError } from "../errors.js";
|
|
7
7
|
import { KeyedMutex } from "../mutex.js";
|
|
8
|
+
import { peerEdgeRef } from "./peer-protocol.js";
|
|
8
9
|
import { deliveryStates, directionId, gatewayActivityActions, gatewayActivityKinds, gatewayProviders, gatewayPublicSnapshotLimits, gatewayRegistrationIngressPrefixes, parseDirection, projectGatewayPublicSnapshot, routeRegistrationModes } from "./types.js";
|
|
9
10
|
const STATE_MARKER = ".agent-embassy-state";
|
|
10
11
|
const STATE_MARKER_CONTENT = "agent-embassy-state-v1\n";
|
|
@@ -17,6 +18,7 @@ const DEFAULT_RETAINED_BODY_BYTES = 1 * 1024 * 1024;
|
|
|
17
18
|
const ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
18
19
|
const HOST_PATTERN = /^[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
19
20
|
const PRIVATE_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
|
|
21
|
+
const PEER_ROUTE_HANDLE_PATTERN = /^peer:[0-9a-f]{64}$/;
|
|
20
22
|
const SAFE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
21
23
|
const MESSAGE_ID_PATTERN = /^msg_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
24
|
const MESSAGE_SUFFIX_PATTERN = /^[0-9a-f]{8}$/;
|
|
@@ -103,6 +105,8 @@ function isRoute(value) {
|
|
|
103
105
|
isLogicalBinding(value.binding) &&
|
|
104
106
|
typeof value.registrationMode === "string" &&
|
|
105
107
|
REGISTRATION_MODES.has(value.registrationMode) &&
|
|
108
|
+
(value.binding.provider !== "peer" || value.registrationMode === "federated_peer" ||
|
|
109
|
+
PEER_ROUTE_HANDLE_PATTERN.test(value.binding.routeHandle)) &&
|
|
106
110
|
typeof value.enabled === "boolean" &&
|
|
107
111
|
(value.busyPolicy === "queue" || value.busyPolicy === "refuse") &&
|
|
108
112
|
isIsoTimestamp(value.registeredAt) &&
|
|
@@ -141,11 +145,25 @@ function consentKey(endpoints) {
|
|
|
141
145
|
.map((endpoint) => [endpoint.provider, endpoint.alias, endpoint.registrationId].join("\0"))
|
|
142
146
|
.join("\0");
|
|
143
147
|
}
|
|
148
|
+
function edgeOwnerHost(endpoints) {
|
|
149
|
+
return endpoints.map((endpoint) => aliasHost(endpoint.alias)).sort()[0];
|
|
150
|
+
}
|
|
151
|
+
function peerMirrorRegistrationId(host, routeRef) {
|
|
152
|
+
return `reg_peer_${createHash("sha256").update(`${host}\0${routeRef}`).digest("base64url").slice(0, 32)}`;
|
|
153
|
+
}
|
|
144
154
|
function sameConsent(left, right) {
|
|
145
155
|
if (left === null || right === null)
|
|
146
156
|
return left === right;
|
|
147
157
|
return consentKey(left) === consentKey(right);
|
|
148
158
|
}
|
|
159
|
+
function routeModeMatchesHost(route, localHost) {
|
|
160
|
+
if (route.registrationMode === "federated_peer") {
|
|
161
|
+
return localHost !== undefined && route.binding.hostId !== localHost;
|
|
162
|
+
}
|
|
163
|
+
return route.binding.hostId === localHost && (route.binding.provider === "claude"
|
|
164
|
+
? route.registrationMode === "selected_live_peer"
|
|
165
|
+
: route.registrationMode === "explicit_opt_in");
|
|
166
|
+
}
|
|
149
167
|
function isConsentEdge(value) {
|
|
150
168
|
if (!isObject(value) ||
|
|
151
169
|
!hasOnlyKeys(value, ["endpoints", "createdAt", "updatedAt", "counters"]) ||
|
|
@@ -156,7 +174,7 @@ function isConsentEdge(value) {
|
|
|
156
174
|
}
|
|
157
175
|
const endpoints = value.endpoints;
|
|
158
176
|
return (compareConsentEndpoints(endpoints[0], endpoints[1]) < 0 &&
|
|
159
|
-
endpoints[0].
|
|
177
|
+
endpoints[0].alias !== endpoints[1].alias &&
|
|
160
178
|
isIsoTimestamp(value.createdAt) &&
|
|
161
179
|
isIsoTimestamp(value.updatedAt) &&
|
|
162
180
|
isCounters(value.counters));
|
|
@@ -167,7 +185,9 @@ function isPrepared(value) {
|
|
|
167
185
|
(value.kind === "claude_mailbox" ||
|
|
168
186
|
value.kind === "codex_turn_start" ||
|
|
169
187
|
value.kind === "codex_turn_steer" ||
|
|
170
|
-
value.kind === "acp_prompt"
|
|
188
|
+
value.kind === "acp_prompt" ||
|
|
189
|
+
value.kind === "peer_mailbox" ||
|
|
190
|
+
value.kind === "peer_handoff") &&
|
|
171
191
|
isPositiveInteger(value.bodyBytes) &&
|
|
172
192
|
typeof value.bodySha256 === "string" &&
|
|
173
193
|
SHA256_PATTERN.test(value.bodySha256) &&
|
|
@@ -176,13 +196,17 @@ function isPrepared(value) {
|
|
|
176
196
|
typeof value.sha256 === "string" &&
|
|
177
197
|
SHA256_PATTERN.test(value.sha256));
|
|
178
198
|
}
|
|
179
|
-
function expectedPreparedKind(message) {
|
|
199
|
+
function expectedPreparedKind(message, targetMode) {
|
|
200
|
+
if (targetMode === "federated_peer")
|
|
201
|
+
return "peer_handoff";
|
|
180
202
|
const target = parseDirection(message.direction).targetProvider;
|
|
181
203
|
if (target === "claude")
|
|
182
204
|
return "claude_mailbox";
|
|
183
205
|
if (target === "codex") {
|
|
184
206
|
return message.steer === true ? "codex_turn_steer" : "codex_turn_start";
|
|
185
207
|
}
|
|
208
|
+
if (target === "peer")
|
|
209
|
+
return "peer_mailbox";
|
|
186
210
|
return "acp_prompt";
|
|
187
211
|
}
|
|
188
212
|
function isAttemptAuthority(value) {
|
|
@@ -460,11 +484,15 @@ export function isGatewayPersistedStateV3(value) {
|
|
|
460
484
|
}
|
|
461
485
|
}
|
|
462
486
|
}
|
|
487
|
+
const localHosts = new Set(state.routes
|
|
488
|
+
.filter((route) => route.registrationMode !== "federated_peer")
|
|
489
|
+
.map((route) => route.binding.hostId));
|
|
490
|
+
if (localHosts.size > 1)
|
|
491
|
+
return false;
|
|
492
|
+
const localHost = [...localHosts][0];
|
|
463
493
|
for (const route of state.routes) {
|
|
464
494
|
if (!route.alias.endsWith(`@${route.binding.hostId}`) ||
|
|
465
|
-
(route
|
|
466
|
-
? route.registrationMode !== "selected_live_peer"
|
|
467
|
-
: route.registrationMode !== "explicit_opt_in")) {
|
|
495
|
+
!routeModeMatchesHost(route, localHost)) {
|
|
468
496
|
return false;
|
|
469
497
|
}
|
|
470
498
|
const prefix = gatewayRegistrationIngressPrefixes[route.binding.provider];
|
|
@@ -479,7 +507,8 @@ export function isGatewayPersistedStateV3(value) {
|
|
|
479
507
|
right?.binding.registrationId !== rightEndpoint.registrationId ||
|
|
480
508
|
left.binding.provider !== leftEndpoint.provider ||
|
|
481
509
|
right.binding.provider !== rightEndpoint.provider ||
|
|
482
|
-
left.binding.hostId
|
|
510
|
+
(left.binding.hostId === right.binding.hostId &&
|
|
511
|
+
left.binding.provider === right.binding.provider)) {
|
|
483
512
|
return false;
|
|
484
513
|
}
|
|
485
514
|
}
|
|
@@ -497,7 +526,8 @@ export function isGatewayPersistedStateV3(value) {
|
|
|
497
526
|
if (message.sourceRegistrationId !== null) {
|
|
498
527
|
if (source?.binding.registrationId !== message.sourceRegistrationId ||
|
|
499
528
|
source.binding.provider !== direction.sourceProvider ||
|
|
500
|
-
(target !== undefined && source.binding.hostId
|
|
529
|
+
(target !== undefined && source.binding.hostId === target.binding.hostId &&
|
|
530
|
+
source.binding.provider === target.binding.provider)) {
|
|
501
531
|
return false;
|
|
502
532
|
}
|
|
503
533
|
}
|
|
@@ -537,7 +567,7 @@ export function isGatewayPersistedStateV3(value) {
|
|
|
537
567
|
}
|
|
538
568
|
if ((message.state.phase === "armed" ||
|
|
539
569
|
message.state.phase === "accepted") &&
|
|
540
|
-
(message.state.prepared.kind !== expectedPreparedKind(message) ||
|
|
570
|
+
(message.state.prepared.kind !== expectedPreparedKind(message, target?.registrationMode) ||
|
|
541
571
|
message.state.prepared.bodyBytes !== message.bytes ||
|
|
542
572
|
message.state.prepared.bodySha256 !==
|
|
543
573
|
createHash("sha256").update(message.body, "utf8").digest("hex"))) {
|
|
@@ -714,6 +744,9 @@ export class GatewayStore {
|
|
|
714
744
|
async listLogicalRoutes() {
|
|
715
745
|
return this.inspectPrivateRoutes();
|
|
716
746
|
}
|
|
747
|
+
async inspectPrivateConsentEdges() {
|
|
748
|
+
return this.read((state) => structuredClone(state.consentEdges));
|
|
749
|
+
}
|
|
717
750
|
async inspectPrivateRoute(alias) {
|
|
718
751
|
return this.read((state) => {
|
|
719
752
|
const route = state.routes.find((candidate) => candidate.alias === alias);
|
|
@@ -725,6 +758,108 @@ export class GatewayStore {
|
|
|
725
758
|
};
|
|
726
759
|
});
|
|
727
760
|
}
|
|
761
|
+
async reconcilePeerCatalog(peerHost, catalog) {
|
|
762
|
+
return this.mutate((state, now) => {
|
|
763
|
+
const localHost = this.config.hostId ?? this.config.allowedHosts[0];
|
|
764
|
+
if (!this.config.allowedHosts.includes(peerHost) || peerHost === localHost ||
|
|
765
|
+
!catalog.complete || catalog.truncated || catalog.routes.some((route) => route.host !== peerHost) ||
|
|
766
|
+
catalog.consentEdges.some((edge) => edge.ownerHost !== peerHost)) {
|
|
767
|
+
throw new BridgeError("INVALID_PEER_CATALOG", "The peer catalog is not an exact complete projection for this configured host.");
|
|
768
|
+
}
|
|
769
|
+
const desired = new Map(catalog.routes.map((route) => [route.ref, route]));
|
|
770
|
+
const existing = state.routes.filter((route) => route.registrationMode === "federated_peer" && route.binding.hostId === peerHost);
|
|
771
|
+
const settlements = [];
|
|
772
|
+
for (const route of existing) {
|
|
773
|
+
const row = desired.get(route.binding.routeHandle);
|
|
774
|
+
if (row !== undefined && row.provider === route.binding.provider)
|
|
775
|
+
continue;
|
|
776
|
+
settlements.push(...this.terminalizeRegistration(state, route.binding.registrationId, now));
|
|
777
|
+
this.removeRegistrationMetadata(state, route);
|
|
778
|
+
}
|
|
779
|
+
for (const row of catalog.routes) {
|
|
780
|
+
let route = state.routes.find((candidate) => candidate.registrationMode === "federated_peer" &&
|
|
781
|
+
candidate.binding.hostId === peerHost && candidate.binding.routeHandle === row.ref &&
|
|
782
|
+
candidate.binding.provider === row.provider);
|
|
783
|
+
const collision = state.routes.find((candidate) => candidate.alias === row.alias && candidate !== route);
|
|
784
|
+
if (collision !== undefined)
|
|
785
|
+
throw new BridgeError("ROUTE_ALIAS_ALREADY_REGISTERED", "A peer catalog alias conflicts with current local authority.");
|
|
786
|
+
if (route === undefined) {
|
|
787
|
+
if (state.routes.length >= this.config.limits.maxRoutes)
|
|
788
|
+
throw new BridgeError("ROUTE_CAPACITY_REACHED", "The bounded logical route inventory is full.", true);
|
|
789
|
+
const input = { alias: row.alias, registrationMode: "federated_peer",
|
|
790
|
+
binding: { provider: row.provider, hostId: peerHost, routeHandle: row.ref,
|
|
791
|
+
registrationId: peerMirrorRegistrationId(peerHost, row.ref) } };
|
|
792
|
+
route = routeRecord(input, now);
|
|
793
|
+
state.routes.push(route);
|
|
794
|
+
}
|
|
795
|
+
else if (route.alias !== row.alias) {
|
|
796
|
+
this.renameRegistrationCoordinates(state, route.alias, row.alias, route.binding.registrationId);
|
|
797
|
+
route.alias = row.alias;
|
|
798
|
+
}
|
|
799
|
+
route.enabled = row.enabled;
|
|
800
|
+
route.updatedAt = now.toISOString();
|
|
801
|
+
}
|
|
802
|
+
const peerOwned = (edge) => edgeOwnerHost(edge.endpoints) === peerHost &&
|
|
803
|
+
edge.endpoints.some((endpoint) => endpoint.alias.endsWith(`@${peerHost}`));
|
|
804
|
+
const desiredEdges = [];
|
|
805
|
+
for (const edge of catalog.consentEdges) {
|
|
806
|
+
const resolved = edge.endpoints.map((endpoint) => state.routes.find((route) => route.alias === endpoint.alias && route.binding.provider === endpoint.provider &&
|
|
807
|
+
(endpoint.host === peerHost ? route.registrationMode === "federated_peer" && route.binding.hostId === peerHost &&
|
|
808
|
+
route.binding.routeHandle === endpoint.routeRef : endpoint.host === localHost &&
|
|
809
|
+
route.registrationMode !== "federated_peer" && route.binding.hostId === localHost &&
|
|
810
|
+
route.binding.registrationId === endpoint.routeRef)));
|
|
811
|
+
if (resolved[0] === undefined || resolved[1] === undefined || edge.ref !== peerEdgeRef(edge.endpoints))
|
|
812
|
+
throw new BridgeError("INVALID_PEER_CATALOG", "A peer consent edge does not match current endpoint authority.");
|
|
813
|
+
desiredEdges.push(canonicalConsentEndpoints(resolved[0], resolved[1]));
|
|
814
|
+
}
|
|
815
|
+
const removedEdges = state.consentEdges.filter((edge) => peerOwned(edge) &&
|
|
816
|
+
!desiredEdges.some((candidate) => sameConsent(edge.endpoints, candidate)));
|
|
817
|
+
for (const edge of removedEdges)
|
|
818
|
+
settlements.push(...this.terminalizeConsentEdge(state, edge, now));
|
|
819
|
+
state.consentEdges = state.consentEdges.filter((edge) => !removedEdges.includes(edge));
|
|
820
|
+
for (const endpoints of desiredEdges) {
|
|
821
|
+
if (state.consentEdges.some((edge) => sameConsent(edge.endpoints, endpoints)))
|
|
822
|
+
continue;
|
|
823
|
+
if (state.consentEdges.length >= this.config.limits.maxConsentEdges)
|
|
824
|
+
throw new BridgeError("CONSENT_EDGE_CAPACITY_REACHED", "The bounded consent-edge inventory is full.", true);
|
|
825
|
+
state.consentEdges.push({ endpoints, createdAt: now.toISOString(), updatedAt: now.toISOString(), counters: emptyCounters() });
|
|
826
|
+
}
|
|
827
|
+
return { settlements, routes: state.routes.filter((route) => route.registrationMode === "federated_peer" &&
|
|
828
|
+
route.binding.hostId === peerHost).map((route) => ({ alias: route.alias, binding: { ...route.binding },
|
|
829
|
+
registrationMode: route.registrationMode, enabled: route.enabled })) };
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
async enqueuePeerHandoff(peerHost, handoff) {
|
|
833
|
+
return this.mutate((state, now) => {
|
|
834
|
+
const localHost = this.config.hostId ?? this.config.allowedHosts[0];
|
|
835
|
+
if (!this.config.allowedHosts.includes(peerHost) || peerHost === localHost || handoff.source.host !== peerHost ||
|
|
836
|
+
handoff.target.host !== localHost || handoff.edgeOwnerHost !== [peerHost, localHost].sort()[0] ||
|
|
837
|
+
handoff.edgeRef !== peerEdgeRef([handoff.source, handoff.target]))
|
|
838
|
+
throw new BridgeError("INVALID_PEER_HANDOFF", "The peer handoff does not match this configured direct link.");
|
|
839
|
+
const source = state.routes.find((route) => route.registrationMode === "federated_peer" && route.binding.hostId === peerHost &&
|
|
840
|
+
route.alias === handoff.source.alias && route.binding.provider === handoff.source.provider && route.binding.routeHandle === handoff.source.routeRef);
|
|
841
|
+
const target = state.routes.find((route) => route.registrationMode !== "federated_peer" && route.binding.hostId === localHost &&
|
|
842
|
+
route.alias === handoff.target.alias && route.binding.provider === handoff.target.provider &&
|
|
843
|
+
route.binding.registrationId === handoff.target.routeRef && route.enabled);
|
|
844
|
+
if (source === undefined || target === undefined)
|
|
845
|
+
throw new BridgeError("ROUTE_UNREGISTERED", "The peer handoff endpoint is no longer current.");
|
|
846
|
+
const endpoints = canonicalConsentEndpoints(source, target);
|
|
847
|
+
let edge = state.consentEdges.find((candidate) => sameConsent(candidate.endpoints, endpoints));
|
|
848
|
+
if (edge === undefined && handoff.edgeOwnerHost === peerHost) {
|
|
849
|
+
if (state.consentEdges.length >= this.config.limits.maxConsentEdges)
|
|
850
|
+
throw new BridgeError("CONSENT_EDGE_CAPACITY_REACHED", "The bounded consent-edge inventory is full.", true);
|
|
851
|
+
edge = { endpoints, createdAt: now.toISOString(), updatedAt: now.toISOString(), counters: emptyCounters() };
|
|
852
|
+
state.consentEdges.push(edge);
|
|
853
|
+
}
|
|
854
|
+
if (edge === undefined)
|
|
855
|
+
throw new BridgeError("SENDER_NOT_PAIRED", "The peer handoff lacks current exact consent.");
|
|
856
|
+
return this.enqueueResolved(state, now, { body: handoff.body, dedupeKey: `${peerHost}:${handoff.originMessageId}`,
|
|
857
|
+
deadlineAt: handoff.deadlineAt, ...(handoff.conversationCorrelation === undefined ? {} :
|
|
858
|
+
{ conversationIdSuffix: handoff.conversationCorrelation }), ...(handoff.steer === true ? { steer: true } : {}) }, { sourceAlias: source.alias, targetAlias: target.alias, direction: directionId(source.binding.provider, target.binding.provider),
|
|
859
|
+
sourceRegistrationId: source.binding.registrationId, targetRegistrationId: target.binding.registrationId,
|
|
860
|
+
consentEdge: edge.endpoints, pair: true, exposeDeliveryToken: false });
|
|
861
|
+
});
|
|
862
|
+
}
|
|
728
863
|
async registerRoute(input) {
|
|
729
864
|
await this.mutate((state, now) => {
|
|
730
865
|
this.validateRouteInput(input);
|
|
@@ -909,23 +1044,7 @@ export class GatewayStore {
|
|
|
909
1044
|
if (index < 0)
|
|
910
1045
|
return { settlements: [], unreferencedAliases: [] };
|
|
911
1046
|
const edge = state.consentEdges[index];
|
|
912
|
-
const settlements =
|
|
913
|
-
for (const message of state.messages) {
|
|
914
|
-
if (message.state.phase === "terminal" ||
|
|
915
|
-
!sameConsent(message.consentEdge, edge.endpoints)) {
|
|
916
|
-
continue;
|
|
917
|
-
}
|
|
918
|
-
const outcome = message.state.phase === "armed"
|
|
919
|
-
? "ambiguous"
|
|
920
|
-
: message.state.phase === "accepted"
|
|
921
|
-
? message.state.lossOutcome
|
|
922
|
-
: "cancelled";
|
|
923
|
-
settlements.push(this.finishMessage(state, message, outcome, now, outcome === "ambiguous"
|
|
924
|
-
? "DISPATCH_OUTCOME_AMBIGUOUS"
|
|
925
|
-
: outcome === "unconfirmed"
|
|
926
|
-
? "DELIVERY_UNCONFIRMED"
|
|
927
|
-
: "SENDER_NOT_PAIRED"));
|
|
928
|
-
}
|
|
1047
|
+
const settlements = this.terminalizeConsentEdge(state, edge, now);
|
|
929
1048
|
state.consentEdges.splice(index, 1);
|
|
930
1049
|
return {
|
|
931
1050
|
settlements,
|
|
@@ -973,9 +1092,11 @@ export class GatewayStore {
|
|
|
973
1092
|
target.binding.registrationId !== input.expectedTargetRegistrationId) {
|
|
974
1093
|
throw new BridgeError("ROUTE_UNREGISTERED", "The native ingress target registration is no longer current.");
|
|
975
1094
|
}
|
|
1095
|
+
const nativeSourceHost = target.registrationMode === "federated_peer"
|
|
1096
|
+
? (this.config.hostId ?? this.config.allowedHosts[0]) : target.binding.hostId;
|
|
976
1097
|
if (input.source.binding.provider !== "claude" ||
|
|
977
|
-
input.source.binding.hostId !==
|
|
978
|
-
input.source.alias.endsWith(`@${
|
|
1098
|
+
input.source.binding.hostId !== nativeSourceHost ||
|
|
1099
|
+
input.source.alias.endsWith(`@${nativeSourceHost}`) === false) {
|
|
979
1100
|
throw new BridgeError("INVALID_NATIVE_PEER", "The native Claude sender does not match the target host.");
|
|
980
1101
|
}
|
|
981
1102
|
const selected = state.routes.find((route) => route.alias === input.source.alias &&
|
|
@@ -985,7 +1106,7 @@ export class GatewayStore {
|
|
|
985
1106
|
const edge = selected === undefined
|
|
986
1107
|
? undefined
|
|
987
1108
|
: state.consentEdges.find((candidate) => sameConsent(candidate.endpoints, canonicalConsentEndpoints(selected, target)));
|
|
988
|
-
if (this.config.inboundMode === "paired" && edge === undefined) {
|
|
1109
|
+
if ((this.config.inboundMode === "paired" || target.registrationMode === "federated_peer") && edge === undefined) {
|
|
989
1110
|
this.recordRejection(state, {
|
|
990
1111
|
sourceAlias: input.source.alias, targetAlias: target.alias,
|
|
991
1112
|
direction: directionId("claude", target.binding.provider),
|
|
@@ -1137,15 +1258,15 @@ export class GatewayStore {
|
|
|
1137
1258
|
const bodySha256 = createHash("sha256")
|
|
1138
1259
|
.update(message.body ?? "", "utf8")
|
|
1139
1260
|
.digest("hex");
|
|
1261
|
+
const target = state.routes.find((route) => route.alias === message.targetAlias &&
|
|
1262
|
+
route.binding.registrationId === message.targetRegistrationId &&
|
|
1263
|
+
route.enabled);
|
|
1140
1264
|
if (!isPrepared(input.prepared) ||
|
|
1141
|
-
input.prepared.kind !== expectedPreparedKind(message) ||
|
|
1265
|
+
input.prepared.kind !== expectedPreparedKind(message, target?.registrationMode) ||
|
|
1142
1266
|
input.prepared.bodyBytes !== message.bytes ||
|
|
1143
1267
|
input.prepared.bodySha256 !== bodySha256) {
|
|
1144
1268
|
throw new BridgeError("INVALID_PREPARED_WRITE_EVIDENCE", "The prepared payload evidence does not match the admitted message.");
|
|
1145
1269
|
}
|
|
1146
|
-
const target = state.routes.find((route) => route.alias === message.targetAlias &&
|
|
1147
|
-
route.binding.registrationId === message.targetRegistrationId &&
|
|
1148
|
-
route.enabled);
|
|
1149
1270
|
if (input.sourceRegistrationId !== message.sourceRegistrationId ||
|
|
1150
1271
|
input.targetRegistrationId !== message.targetRegistrationId ||
|
|
1151
1272
|
(message.transientTarget !== true && target === undefined)) {
|
|
@@ -1228,8 +1349,8 @@ export class GatewayStore {
|
|
|
1228
1349
|
if ((message.state.phase === "armed" &&
|
|
1229
1350
|
(input.state === "expired" ||
|
|
1230
1351
|
(input.state === "unconfirmed" &&
|
|
1231
|
-
(input.safeErrorCode
|
|
1232
|
-
message.state.prepared.kind
|
|
1352
|
+
!((input.safeErrorCode === "ACP_OUTCOME_COARSE" && message.state.prepared.kind === "acp_prompt") ||
|
|
1353
|
+
(input.safeErrorCode === "PEER_HANDOFF_ACCEPTANCE_UNCONFIRMED" && message.state.prepared.kind === "peer_handoff"))))) ||
|
|
1233
1354
|
(message.state.phase === "accepted" &&
|
|
1234
1355
|
(input.state === "expired" ||
|
|
1235
1356
|
(input.state === "unconfirmed" &&
|
|
@@ -1314,6 +1435,7 @@ export class GatewayStore {
|
|
|
1314
1435
|
? {}
|
|
1315
1436
|
: { oldestQueuedAt: queued[0].enqueuedAt }),
|
|
1316
1437
|
counters: { ...route.counters },
|
|
1438
|
+
mutable: route.registrationMode !== "federated_peer",
|
|
1317
1439
|
};
|
|
1318
1440
|
});
|
|
1319
1441
|
const consentEdges = state.consentEdges.map((edge) => ({
|
|
@@ -1325,7 +1447,8 @@ export class GatewayStore {
|
|
|
1325
1447
|
alias: edge.endpoints[1].alias, provider: edge.endpoints[1].provider,
|
|
1326
1448
|
},
|
|
1327
1449
|
],
|
|
1328
|
-
host:
|
|
1450
|
+
host: edgeOwnerHost(edge.endpoints), counters: { ...edge.counters },
|
|
1451
|
+
mutable: edgeOwnerHost(edge.endpoints) === (this.config.hostId ?? this.config.allowedHosts[0]),
|
|
1329
1452
|
}));
|
|
1330
1453
|
const currentEvents = state.messages.map((message) => this.projectMessageEvent(message));
|
|
1331
1454
|
const legacyEvents = state.activity
|
|
@@ -1422,15 +1545,16 @@ export class GatewayStore {
|
|
|
1422
1545
|
};
|
|
1423
1546
|
}
|
|
1424
1547
|
validateRouteInput(input) {
|
|
1548
|
+
const localHost = this.config.hostId ?? this.config.allowedHosts[0];
|
|
1425
1549
|
if (!isObject(input) ||
|
|
1426
1550
|
!ALIAS_PATTERN.test(input.alias) ||
|
|
1427
1551
|
!isLogicalBinding(input.binding) ||
|
|
1428
1552
|
!REGISTRATION_MODES.has(input.registrationMode) ||
|
|
1553
|
+
(input.binding.provider === "peer" && input.registrationMode !== "federated_peer" &&
|
|
1554
|
+
!PEER_ROUTE_HANDLE_PATTERN.test(input.binding.routeHandle)) ||
|
|
1429
1555
|
!input.alias.endsWith(`@${input.binding.hostId}`) ||
|
|
1430
1556
|
!this.config.allowedHosts.includes(input.binding.hostId) ||
|
|
1431
|
-
(input
|
|
1432
|
-
? input.registrationMode !== "selected_live_peer"
|
|
1433
|
-
: input.registrationMode !== "explicit_opt_in")) {
|
|
1557
|
+
!routeModeMatchesHost(input, localHost)) {
|
|
1434
1558
|
throw new BridgeError("INVALID_ROUTE_BINDING", "The logical route binding is invalid for this gateway.");
|
|
1435
1559
|
}
|
|
1436
1560
|
const prefix = gatewayRegistrationIngressPrefixes[input.binding.provider];
|
|
@@ -1469,10 +1593,10 @@ export class GatewayStore {
|
|
|
1469
1593
|
}
|
|
1470
1594
|
if (left === undefined ||
|
|
1471
1595
|
right === undefined ||
|
|
1472
|
-
left.binding.provider === right.binding.provider
|
|
1473
|
-
|
|
1596
|
+
(left.binding.provider === right.binding.provider &&
|
|
1597
|
+
left.binding.hostId === right.binding.hostId)) {
|
|
1474
1598
|
if (required) {
|
|
1475
|
-
throw new BridgeError("INVALID_CONSENT_EDGE", "Consent requires
|
|
1599
|
+
throw new BridgeError("INVALID_CONSENT_EDGE", "Consent requires distinct providers or distinct configured hosts.");
|
|
1476
1600
|
}
|
|
1477
1601
|
return undefined;
|
|
1478
1602
|
}
|
|
@@ -1633,6 +1757,16 @@ export class GatewayStore {
|
|
|
1633
1757
|
: { supersededSettlement }),
|
|
1634
1758
|
};
|
|
1635
1759
|
}
|
|
1760
|
+
terminalizeConsentEdge(state, edge, now) {
|
|
1761
|
+
return state.messages.filter((message) => message.state.phase !== "terminal" &&
|
|
1762
|
+
sameConsent(message.consentEdge, edge.endpoints)).map((message) => {
|
|
1763
|
+
const outcome = message.state.phase === "armed" ? "ambiguous" :
|
|
1764
|
+
message.state.phase === "accepted" ? message.state.lossOutcome : "cancelled";
|
|
1765
|
+
return this.finishMessage(state, message, outcome, now, outcome === "ambiguous"
|
|
1766
|
+
? "DISPATCH_OUTCOME_AMBIGUOUS" : outcome === "unconfirmed"
|
|
1767
|
+
? "DELIVERY_UNCONFIRMED" : "SENDER_NOT_PAIRED");
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1636
1770
|
allocateDeliveryToken(state) {
|
|
1637
1771
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
1638
1772
|
const token = `dlv_${createHash("sha256")
|
|
@@ -1709,11 +1843,16 @@ export class GatewayStore {
|
|
|
1709
1843
|
: { safeErrorCode: message.state.safeErrorCode }),
|
|
1710
1844
|
};
|
|
1711
1845
|
}
|
|
1846
|
+
const handedOff = outcome === "delivered" &&
|
|
1847
|
+
(message.state.phase === "armed" || message.state.phase === "accepted") &&
|
|
1848
|
+
message.state.prepared.kind === "peer_handoff";
|
|
1712
1849
|
if (message.state.phase === "queued") {
|
|
1713
1850
|
state.accounting.queuedBytes -= message.bytes;
|
|
1714
1851
|
}
|
|
1715
1852
|
message.state = terminalState(outcome, now, message.enqueuedAt, safeErrorCode);
|
|
1716
1853
|
this.touchMessage(state, message);
|
|
1854
|
+
if (handedOff)
|
|
1855
|
+
delete message.body;
|
|
1717
1856
|
state.accounting[outcome] += 1;
|
|
1718
1857
|
const target = state.routes.find((route) => route.binding.registrationId === message.targetRegistrationId);
|
|
1719
1858
|
if (target !== undefined)
|
|
@@ -1911,7 +2050,8 @@ export class GatewayStore {
|
|
|
1911
2050
|
}
|
|
1912
2051
|
assertConfiguredBounds(state) {
|
|
1913
2052
|
const active = state.messages.filter((message) => message.state.phase !== "terminal");
|
|
1914
|
-
if (state.routes.some((route) => !this.config.allowedHosts.includes(route.binding.hostId)
|
|
2053
|
+
if (state.routes.some((route) => !this.config.allowedHosts.includes(route.binding.hostId) ||
|
|
2054
|
+
!routeModeMatchesHost(route, this.config.hostId ?? this.config.allowedHosts[0])) ||
|
|
1915
2055
|
state.routes.length > this.config.limits.maxRoutes ||
|
|
1916
2056
|
state.consentEdges.length > this.config.limits.maxConsentEdges ||
|
|
1917
2057
|
active.length > this.config.limits.maxQueueMessages ||
|