agent-embassy 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTING.md +61 -6
- package/README.md +6 -5
- package/README.zh-CN.md +4 -4
- package/SECURITY.md +128 -24
- package/dist/src/gateway/claude-peer.d.ts +7 -4
- package/dist/src/gateway/claude-peer.js +80 -36
- package/dist/src/gateway/claude-peer.js.map +1 -1
- package/dist/src/gateway/claude-runtime.d.ts +4 -0
- package/dist/src/gateway/claude-runtime.js +50 -39
- 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 +1 -1
- package/dist/src/gateway/cli-copy.en.js +1 -1
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.js +1 -1
- package/dist/src/gateway/cli-copy.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 +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +1 -1
- package/dist/src/gateway/cli.js +3 -7
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +2 -1
- package/dist/src/gateway/codex-app-server.js +1 -1
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-local-transport.d.ts +10 -6
- package/dist/src/gateway/codex-local-transport.js +101 -56
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/compatibility.d.ts +11 -0
- package/dist/src/gateway/compatibility.js +104 -22
- package/dist/src/gateway/compatibility.js.map +1 -1
- package/dist/src/gateway/control.js +28 -30
- 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 +36 -17
- package/dist/src/gateway/dashboard-copy.en.js +50 -31
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +36 -17
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +36 -17
- package/dist/src/gateway/dashboard-copy.zh-CN.js +50 -31
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +176 -8
- package/dist/src/gateway/dashboard-model.js +420 -70
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.js +48 -135
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/live-dashboard-app/app.js +148 -247
- package/dist/src/gateway/live-dashboard-assets.js +2 -1
- package/dist/src/gateway/live-dashboard-assets.js.map +1 -1
- package/dist/src/gateway/live-dashboard-stream.d.ts +2 -2
- package/dist/src/gateway/live-dashboard-stream.js +2 -2
- package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
- package/dist/src/gateway/progress-watch-machine.d.ts +50 -64
- package/dist/src/gateway/progress-watch-machine.js +42 -133
- package/dist/src/gateway/progress-watch-machine.js.map +1 -1
- package/dist/src/gateway/provenance-envelope.d.ts +2 -0
- package/dist/src/gateway/provenance-envelope.js +13 -3
- package/dist/src/gateway/provenance-envelope.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +69 -15
- package/dist/src/gateway/providers.js +456 -109
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +8 -5
- package/dist/src/gateway/server.js +94 -13
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +25 -1
- package/dist/src/gateway/service.js +252 -69
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +23 -28
- package/dist/src/gateway/store.js +479 -467
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +38 -11
- package/dist/src/gateway/types.js +141 -3
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +8 -8
- package/docs/CONFIGURATION.zh-CN.md +8 -8
- package/docs/DASHBOARD.md +29 -6
- package/docs/DASHBOARD.zh-CN.md +1 -1
- package/docs/GATEWAY-ARCHITECTURE.md +143 -57
- package/package.json +1 -1
- package/skills/embassy-peer/SKILL.md +2 -2
|
@@ -9,7 +9,7 @@ import { createCodexRegistrationSuccession, transitionCodexRegistrationSuccessio
|
|
|
9
9
|
import { createCodexRegistrationGeneration, isCodexRegistrationGeneration, } from "./codex-registration-generation.js";
|
|
10
10
|
import { PROGRESS_WATCH_DEFAULT_IDLE_MS } from "./progress-watch-machine.js";
|
|
11
11
|
import { GatewayStore, } from "./store.js";
|
|
12
|
-
import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, arePublicAvailablePeerSnapshots, projectGatewayPublicSnapshot, } from "./types.js";
|
|
12
|
+
import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, arePublicAvailablePeerSnapshots, projectPublicCompatibilityCheck, projectGatewayPublicSnapshot, } from "./types.js";
|
|
13
13
|
const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
14
14
|
const CLAUDE_SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
15
15
|
const SAFE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
@@ -22,10 +22,37 @@ const NATIVE_HELD_THRESHOLD_MS = 1_000;
|
|
|
22
22
|
const RUNTIME_ACTIVITY_CAPACITY = 256;
|
|
23
23
|
const DELIVERY_SETTLEMENT_RETRY_MS = 250;
|
|
24
24
|
const DELIVERY_DASHBOARD_REFRESH_MS = 15_000;
|
|
25
|
+
const BOOT_REACTIVATION_TRUST_FAILURE_CODES = new Set([
|
|
26
|
+
"HOME_INVALID",
|
|
27
|
+
"MANAGED_CODEX_INVALID",
|
|
28
|
+
"LOCAL_APP_SERVER_ENDPOINT_UNSAFE",
|
|
29
|
+
"APP_SERVER_VERSION_MISMATCH",
|
|
30
|
+
"ENDPOINT_GENERATION_CHANGED",
|
|
31
|
+
"CODEX_ENDPOINT_GENERATION_CHURN",
|
|
32
|
+
"CODEX_FACTORY_ATTESTATION_INVALID",
|
|
33
|
+
"CLEANUP_FAILED",
|
|
34
|
+
"FACTORY_CLOSED",
|
|
35
|
+
"INVALID_CONFIGURATION",
|
|
36
|
+
"CLAUDE_PEER_CALLBACK_UNSAFE",
|
|
37
|
+
"CODEX_ROUTE_CLEANUP_FAILED",
|
|
38
|
+
"CODEX_PROVIDER_CLEANUP_FAILED",
|
|
39
|
+
"CODEX_PROVIDER_CLEANUP_AMBIGUOUS",
|
|
40
|
+
"CODEX_PROVIDER_CLEANUP_TIMEOUT",
|
|
41
|
+
]);
|
|
42
|
+
function boundedErrorCode(error) {
|
|
43
|
+
if (typeof error !== "object" ||
|
|
44
|
+
error === null ||
|
|
45
|
+
!("code" in error) ||
|
|
46
|
+
typeof error.code !== "string" ||
|
|
47
|
+
!SAFE_CODE.test(error.code)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return error.code;
|
|
51
|
+
}
|
|
25
52
|
function progressWatchActivity(text, conversationId, actorAlias, explicitIdleMinutes) {
|
|
26
53
|
const prefixedOpen = text.startsWith("TRACK:");
|
|
27
54
|
const completionSignal = text.startsWith("DONE:");
|
|
28
|
-
if (completionSignal &&
|
|
55
|
+
if (completionSignal && explicitIdleMinutes !== undefined) {
|
|
29
56
|
throw new BridgeError("PROGRESS_WATCH_SIGNAL_CONFLICT", "A single message cannot both open and complete a progress watch.");
|
|
30
57
|
}
|
|
31
58
|
const openIdleMs = explicitIdleMinutes === undefined
|
|
@@ -133,6 +160,8 @@ export class GatewayService {
|
|
|
133
160
|
bindingAliases = new Map();
|
|
134
161
|
candidates = new Map();
|
|
135
162
|
availablePeers = [];
|
|
163
|
+
registryEvidenceByConnector = new Map();
|
|
164
|
+
registryAdaptersWithParseableRecords = new Set();
|
|
136
165
|
conversations = new Map();
|
|
137
166
|
messageContexts = new Map();
|
|
138
167
|
providerTurnContinuations = new Map();
|
|
@@ -145,6 +174,8 @@ export class GatewayService {
|
|
|
145
174
|
deliveryTokens = new Map();
|
|
146
175
|
runtimeAlerts = [];
|
|
147
176
|
activityEvents = [];
|
|
177
|
+
/** Startup probe failures that must preflight cross-provider mutations. */
|
|
178
|
+
incompatibleProviderSurfaces = new Map();
|
|
148
179
|
activeCodexIncompatibility;
|
|
149
180
|
activitySequence = 0;
|
|
150
181
|
detachedReceiptWrites = new Set();
|
|
@@ -260,6 +291,12 @@ export class GatewayService {
|
|
|
260
291
|
throw new BridgeError("GATEWAY_ADAPTER_NOT_ALLOWED", "A provider adapter is duplicated or outside the host allowlist.");
|
|
261
292
|
}
|
|
262
293
|
seen.add(key);
|
|
294
|
+
}
|
|
295
|
+
if (this.adapters.length > 0) {
|
|
296
|
+
await this.runAutomaticCompatibilityProbesLocked();
|
|
297
|
+
assertStartActive();
|
|
298
|
+
}
|
|
299
|
+
for (const adapter of this.adapters) {
|
|
263
300
|
const observation = await adapter.initialize(this.callbacksFor(adapter));
|
|
264
301
|
assertStartActive();
|
|
265
302
|
await this.store.observeConnector({
|
|
@@ -274,15 +311,6 @@ export class GatewayService {
|
|
|
274
311
|
});
|
|
275
312
|
assertStartActive();
|
|
276
313
|
}
|
|
277
|
-
if (this.adapters.length > 0 &&
|
|
278
|
-
this.adapters.every((adapter) => adapter.compatibilitySurface !== undefined &&
|
|
279
|
-
adapter.runCompatibilityProbes !== undefined)) {
|
|
280
|
-
const attestations = await this.runAutomaticCompatibilityProbesLocked();
|
|
281
|
-
const incompatible = attestations.find((surface) => surface.tier === "incompatible");
|
|
282
|
-
if (incompatible !== undefined) {
|
|
283
|
-
throw new BridgeError(incompatible.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED", "A local provider failed bounded compatibility attestation.");
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
314
|
await this.recoverCodexSuccessionAfterRestartLocked();
|
|
287
315
|
bootReactivatedIdleAliases =
|
|
288
316
|
await this.reactivateRetainedCodexRoutesAfterRestartLocked();
|
|
@@ -338,7 +366,7 @@ export class GatewayService {
|
|
|
338
366
|
try {
|
|
339
367
|
await control.close();
|
|
340
368
|
}
|
|
341
|
-
catch {
|
|
369
|
+
catch (error) {
|
|
342
370
|
closeFailed = true;
|
|
343
371
|
}
|
|
344
372
|
}
|
|
@@ -404,7 +432,7 @@ export class GatewayService {
|
|
|
404
432
|
await this.drainDetachedReceiptWritesLocked();
|
|
405
433
|
await this.publish();
|
|
406
434
|
}
|
|
407
|
-
catch {
|
|
435
|
+
catch (error) {
|
|
408
436
|
closeFailed = true;
|
|
409
437
|
}
|
|
410
438
|
const adapterResults = await Promise.allSettled(this.adapters.map(async (adapter) => adapter.close()));
|
|
@@ -427,6 +455,9 @@ export class GatewayService {
|
|
|
427
455
|
this.deliveryTrackers.clear();
|
|
428
456
|
this.deliveryTokens.clear();
|
|
429
457
|
this.runtimeAlerts.length = 0;
|
|
458
|
+
this.incompatibleProviderSurfaces.clear();
|
|
459
|
+
this.registryEvidenceByConnector.clear();
|
|
460
|
+
this.registryAdaptersWithParseableRecords.clear();
|
|
430
461
|
this.detachedReceiptWrites.clear();
|
|
431
462
|
this.nativeIngressByConversation.clear();
|
|
432
463
|
this.callbackQueue.length = 0;
|
|
@@ -511,10 +542,7 @@ export class GatewayService {
|
|
|
511
542
|
observeSnapshot: async () => await this.observeSnapshot(),
|
|
512
543
|
deliveryStatus: async (params) => await this.deliveryStatus(params.token),
|
|
513
544
|
untrack: async (params) => await this.exclusiveDecision(async () => {
|
|
514
|
-
const settled = await this.store.
|
|
515
|
-
conversationId: params.conversationId,
|
|
516
|
-
outcome: "done",
|
|
517
|
-
});
|
|
545
|
+
const settled = await this.store.endProgressWatch(params.conversationId);
|
|
518
546
|
if (!settled) {
|
|
519
547
|
throw new BridgeError("PROGRESS_WATCH_NOT_FOUND", "No active progress watch matches that conversation token.");
|
|
520
548
|
}
|
|
@@ -559,6 +587,7 @@ export class GatewayService {
|
|
|
559
587
|
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "A required compatibility surface is unavailable.");
|
|
560
588
|
}
|
|
561
589
|
const probes = await entry.adapter.runCompatibilityProbes();
|
|
590
|
+
this.captureRegistryObservation(entry.adapter, entry.adapter.latestRegistryObservation?.());
|
|
562
591
|
const attestation = evaluateCompatibilityAttestation({
|
|
563
592
|
surface,
|
|
564
593
|
version: entry.observation.version,
|
|
@@ -567,13 +596,82 @@ export class GatewayService {
|
|
|
567
596
|
probes,
|
|
568
597
|
});
|
|
569
598
|
await this.store.recordCompatibilityAttestation(attestation);
|
|
570
|
-
|
|
571
|
-
|
|
599
|
+
entry.adapter.acceptCompatibilityAttestation(attestation);
|
|
600
|
+
if (attestation.tier === "incompatible") {
|
|
601
|
+
const code = attestation.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED";
|
|
602
|
+
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey(surface, entry.adapter.identity.hostId), code);
|
|
603
|
+
if (surface === "codex") {
|
|
604
|
+
this.activeCodexIncompatibility = structuredClone(attestation);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else if (surface === "codex" && attestation.tier === "schema_attested") {
|
|
608
|
+
// A live but untested App Server schema is useful for observation only.
|
|
609
|
+
// Fence retained boot reactivation before it can trigger endpoint
|
|
610
|
+
// refresh churn; an explicit registration may still re-probe a later,
|
|
611
|
+
// certified generation through the provider's bounded refresh path.
|
|
612
|
+
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", entry.adapter.identity.hostId), "CODEX_MONITOR_ONLY");
|
|
572
613
|
}
|
|
573
614
|
attestations.push(attestation);
|
|
574
615
|
}
|
|
575
616
|
return attestations;
|
|
576
617
|
}
|
|
618
|
+
captureRegistryObservation(adapter, observation) {
|
|
619
|
+
if (adapter.identity.provider !== "claude" ||
|
|
620
|
+
observation === undefined ||
|
|
621
|
+
!Number.isSafeInteger(observation.entriesScanned) ||
|
|
622
|
+
observation.entriesScanned < 0 ||
|
|
623
|
+
!Number.isSafeInteger(observation.parseableRecords) ||
|
|
624
|
+
observation.parseableRecords < 0 ||
|
|
625
|
+
observation.parseableRecords > observation.entriesScanned ||
|
|
626
|
+
!Array.isArray(observation.rejected)) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const counts = new Map();
|
|
630
|
+
for (const row of observation.rejected) {
|
|
631
|
+
if (!row ||
|
|
632
|
+
typeof row !== "object" ||
|
|
633
|
+
typeof row.safeErrorCode !== "string" ||
|
|
634
|
+
!SAFE_CODE.test(row.safeErrorCode) ||
|
|
635
|
+
!Number.isSafeInteger(row.count) ||
|
|
636
|
+
row.count <= 0) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const next = (counts.get(row.safeErrorCode) ?? 0) + row.count;
|
|
640
|
+
counts.set(row.safeErrorCode, Math.min(Number.MAX_SAFE_INTEGER, next));
|
|
641
|
+
}
|
|
642
|
+
const ordered = [...counts]
|
|
643
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
644
|
+
.map(([safeErrorCode, count]) => ({ safeErrorCode, count }));
|
|
645
|
+
const rejected = ordered.slice(0, gatewayPublicSnapshotLimits.registryRejectionCodes);
|
|
646
|
+
if (observation.parseableRecords > 0) {
|
|
647
|
+
this.registryAdaptersWithParseableRecords.add(adapter);
|
|
648
|
+
}
|
|
649
|
+
const hasObservedParseableRecord = this.registryAdaptersWithParseableRecords.has(adapter);
|
|
650
|
+
const row = {
|
|
651
|
+
entriesScanned: observation.entriesScanned,
|
|
652
|
+
parseableRecords: observation.parseableRecords,
|
|
653
|
+
parseableRecordSeenSinceBoot: hasObservedParseableRecord,
|
|
654
|
+
rejected,
|
|
655
|
+
rejectedCodesOmitted: Math.max(0, ordered.length - rejected.length),
|
|
656
|
+
};
|
|
657
|
+
this.registryEvidenceByConnector.set(`${adapter.identity.provider}\0${adapter.identity.hostId}`, row);
|
|
658
|
+
}
|
|
659
|
+
connectorsWithRegistry(connectors) {
|
|
660
|
+
return connectors.map((connector) => {
|
|
661
|
+
const registry = this.registryEvidenceByConnector.get(`${connector.provider}\0${connector.host}`);
|
|
662
|
+
return {
|
|
663
|
+
...connector,
|
|
664
|
+
...(connector.provider !== "claude" || registry === undefined
|
|
665
|
+
? {}
|
|
666
|
+
: {
|
|
667
|
+
registry: {
|
|
668
|
+
...registry,
|
|
669
|
+
rejected: registry.rejected.map((row) => ({ ...row })),
|
|
670
|
+
},
|
|
671
|
+
}),
|
|
672
|
+
};
|
|
673
|
+
});
|
|
674
|
+
}
|
|
577
675
|
async observeSnapshot() {
|
|
578
676
|
return await this.mutex.run("service", async () => {
|
|
579
677
|
const changed = await this.processLifecycleLocked();
|
|
@@ -1149,6 +1247,23 @@ export class GatewayService {
|
|
|
1149
1247
|
throw new BridgeError("PROVIDER_UNAVAILABLE", "The selected provider is unavailable.", true);
|
|
1150
1248
|
return adapter;
|
|
1151
1249
|
}
|
|
1250
|
+
providerSurfaceKey(provider, host) {
|
|
1251
|
+
return `${provider}@${host}`;
|
|
1252
|
+
}
|
|
1253
|
+
incompatibleProviderCode(provider, host) {
|
|
1254
|
+
return this.incompatibleProviderSurfaces.get(this.providerSurfaceKey(provider, host));
|
|
1255
|
+
}
|
|
1256
|
+
/** Reject before either half of a cross-provider mutation can acquire state. */
|
|
1257
|
+
assertCrossProviderMutationCompatible(host, options = { allowCodexRefresh: false }) {
|
|
1258
|
+
const claudeCode = this.incompatibleProviderCode("claude", host);
|
|
1259
|
+
if (claudeCode !== undefined) {
|
|
1260
|
+
throw new BridgeError(claudeCode, "The Claude provider is observation-only until Embassy restarts with a compatible runtime.");
|
|
1261
|
+
}
|
|
1262
|
+
const codexCode = this.incompatibleProviderCode("codex", host);
|
|
1263
|
+
if (codexCode !== undefined && !options.allowCodexRefresh) {
|
|
1264
|
+
throw new BridgeError(codexCode, "The Codex provider is observation-only until a compatible endpoint is re-observed.");
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1152
1267
|
async assertClaudeWorkspaceDisjoint(adapter, routeHandle) {
|
|
1153
1268
|
if (adapter.identity.provider !== "claude") {
|
|
1154
1269
|
throw new BridgeError("CLAUDE_PROVIDER_MISMATCH", "The selected workspace guard does not belong to Claude.");
|
|
@@ -1219,6 +1334,13 @@ export class GatewayService {
|
|
|
1219
1334
|
this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
|
|
1220
1335
|
continue;
|
|
1221
1336
|
}
|
|
1337
|
+
if (this.incompatibleProviderCode("codex", route.binding.hostId) !==
|
|
1338
|
+
undefined ||
|
|
1339
|
+
this.incompatibleProviderCode("claude", route.binding.hostId) !==
|
|
1340
|
+
undefined) {
|
|
1341
|
+
this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1222
1344
|
let routeSelected = false;
|
|
1223
1345
|
let advertiseAttempted = false;
|
|
1224
1346
|
let storeReactivated = false;
|
|
@@ -1308,7 +1430,7 @@ export class GatewayService {
|
|
|
1308
1430
|
if (selected.state === "idle")
|
|
1309
1431
|
idleAliases.push(route.alias);
|
|
1310
1432
|
}
|
|
1311
|
-
catch {
|
|
1433
|
+
catch (error) {
|
|
1312
1434
|
let cleanupFailed = false;
|
|
1313
1435
|
if (advertiseAttempted) {
|
|
1314
1436
|
await claudeAdapter
|
|
@@ -1327,6 +1449,11 @@ export class GatewayService {
|
|
|
1327
1449
|
if (cleanupFailed || storeReactivated || deferredEndpointRefresh) {
|
|
1328
1450
|
throw new BridgeError("CODEX_BOOT_REACTIVATION_CLEANUP_FAILED", "A Codex boot reactivation crossed its durable boundary without exact live authority.");
|
|
1329
1451
|
}
|
|
1452
|
+
const failureCode = boundedErrorCode(error);
|
|
1453
|
+
if (failureCode !== undefined &&
|
|
1454
|
+
BOOT_REACTIVATION_TRUST_FAILURE_CODES.has(failureCode)) {
|
|
1455
|
+
throw error;
|
|
1456
|
+
}
|
|
1330
1457
|
this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
|
|
1331
1458
|
}
|
|
1332
1459
|
}
|
|
@@ -2119,6 +2246,9 @@ export class GatewayService {
|
|
|
2119
2246
|
if (!params.alias.startsWith("codex-")) {
|
|
2120
2247
|
throw new BridgeError("INVALID_CODEX_PEER_ALIAS", "A registered Codex alias must start with codex-.");
|
|
2121
2248
|
}
|
|
2249
|
+
this.assertCrossProviderMutationCompatible(params.hostId, {
|
|
2250
|
+
allowCodexRefresh: params.succeedsAlias === undefined,
|
|
2251
|
+
});
|
|
2122
2252
|
if (this.codexSuccessionPoisoned.has(params.alias) ||
|
|
2123
2253
|
this.codexOrphanCleanupPending.has(params.alias) ||
|
|
2124
2254
|
(params.succeedsAlias !== undefined &&
|
|
@@ -2338,7 +2468,7 @@ export class GatewayService {
|
|
|
2338
2468
|
unwrittenOutcome: "cancelled",
|
|
2339
2469
|
safeErrorCode: "ROUTE_UNREGISTERED",
|
|
2340
2470
|
});
|
|
2341
|
-
const settlements = await this.store.unregisterRoute(params.alias, lease, inFlightSettlements);
|
|
2471
|
+
const settlements = await this.store.unregisterRoute(params.alias, lease, inFlightSettlements, "operator");
|
|
2342
2472
|
for (const settlement of settlements) {
|
|
2343
2473
|
await this.applyTerminalSettlementLocked(settlement);
|
|
2344
2474
|
}
|
|
@@ -2573,6 +2703,7 @@ export class GatewayService {
|
|
|
2573
2703
|
const base = await this.store.publicSnapshot();
|
|
2574
2704
|
const snapshot = projectGatewayPublicSnapshot({
|
|
2575
2705
|
...base,
|
|
2706
|
+
connectors: this.connectorsWithRegistry(base.connectors),
|
|
2576
2707
|
availablePeers: this.availablePeers.map((peer) => ({ ...peer })),
|
|
2577
2708
|
});
|
|
2578
2709
|
return JSON.stringify({
|
|
@@ -2607,6 +2738,7 @@ export class GatewayService {
|
|
|
2607
2738
|
// authority. The provider still revalidates the exact target before
|
|
2608
2739
|
// any write.
|
|
2609
2740
|
}
|
|
2741
|
+
this.captureRegistryObservation(adapter, discovered.registry ?? adapter.latestRegistryObservation?.());
|
|
2610
2742
|
const grouped = new Map();
|
|
2611
2743
|
const byHandle = new Map();
|
|
2612
2744
|
for (const peer of discovered.peers) {
|
|
@@ -3094,6 +3226,32 @@ export class GatewayService {
|
|
|
3094
3226
|
await this.selectAndPairClaude(params.alias, await this.inferCodexAlias(params.codexThreadId));
|
|
3095
3227
|
}
|
|
3096
3228
|
async selectAndPairClaude(selector, codexAlias) {
|
|
3229
|
+
const codexBinding = this.routeBindings.get(codexAlias);
|
|
3230
|
+
const retainedCodexRoute = codexBinding?.provider === "codex"
|
|
3231
|
+
? undefined
|
|
3232
|
+
: await this.store.inspectPrivateRoute(codexAlias);
|
|
3233
|
+
const codexHost = codexBinding?.provider === "codex"
|
|
3234
|
+
? codexBinding.hostId
|
|
3235
|
+
: retainedCodexRoute?.binding.provider === "codex"
|
|
3236
|
+
? retainedCodexRoute.binding.hostId
|
|
3237
|
+
: undefined;
|
|
3238
|
+
if (codexHost !== undefined) {
|
|
3239
|
+
this.assertCrossProviderMutationCompatible(codexHost);
|
|
3240
|
+
}
|
|
3241
|
+
const codexAdapter = codexBinding?.provider === "codex"
|
|
3242
|
+
? this.adapters.find((adapter) => adapter.identity.provider === "codex" &&
|
|
3243
|
+
adapter.identity.hostId === codexBinding.hostId)
|
|
3244
|
+
: undefined;
|
|
3245
|
+
const codexState = this.routeStates.get(codexAlias);
|
|
3246
|
+
if (codexBinding?.provider !== "codex" ||
|
|
3247
|
+
codexAdapter === undefined ||
|
|
3248
|
+
codexAdapter.identity.endpointGeneration !==
|
|
3249
|
+
codexBinding.endpointGeneration ||
|
|
3250
|
+
(codexState !== "idle" &&
|
|
3251
|
+
codexState !== "busy" &&
|
|
3252
|
+
codexState !== "awaiting_approval")) {
|
|
3253
|
+
throw new BridgeError("CODEX_ROUTE_UNAVAILABLE", "Claude selection requires an exact positively observed live Codex route.");
|
|
3254
|
+
}
|
|
3097
3255
|
const discoveryChanged = await this.refreshClaudeDiscovery();
|
|
3098
3256
|
const candidate = this.claudeCandidate(selector);
|
|
3099
3257
|
if (candidate === undefined)
|
|
@@ -3924,14 +4082,7 @@ export class GatewayService {
|
|
|
3924
4082
|
return false;
|
|
3925
4083
|
}
|
|
3926
4084
|
const actions = await this.store.advanceDueProgressWatches();
|
|
3927
|
-
let changed = actions.length > 0;
|
|
3928
4085
|
for (const action of actions) {
|
|
3929
|
-
if (action.type === "settled") {
|
|
3930
|
-
this.addRuntimeAlert(action.outcome === "unresponsive"
|
|
3931
|
-
? "PROGRESS_WATCH_UNRESPONSIVE"
|
|
3932
|
-
: "PROGRESS_WATCH_ENDPOINT_RETIRED", action.outcome === "unresponsive" ? "error" : "warning", { alias: action.ownerAlias });
|
|
3933
|
-
continue;
|
|
3934
|
-
}
|
|
3935
4086
|
const text = `[Embassy automated liveness check — ${action.conversationId} / ` +
|
|
3936
4087
|
`Embassy 自动活跃检查 — ${action.conversationId}]\n` +
|
|
3937
4088
|
"Reply with the result or a status. / 请回复结果或状态。";
|
|
@@ -3945,20 +4096,13 @@ export class GatewayService {
|
|
|
3945
4096
|
nudgeNumber: action.nudgeNumber,
|
|
3946
4097
|
},
|
|
3947
4098
|
});
|
|
3948
|
-
this.addRuntimeAlert("PROGRESS_WATCH_NUDGE", "warning", {
|
|
3949
|
-
alias: action.ownerAlias,
|
|
3950
|
-
});
|
|
3951
4099
|
}
|
|
3952
4100
|
catch {
|
|
3953
|
-
await this.store.deferProgressWatchNudge(action.conversationId);
|
|
3954
|
-
this.addRuntimeAlert("PROGRESS_WATCH_NUDGE_DEFERRED", "warning", {
|
|
3955
|
-
alias: action.ownerAlias,
|
|
3956
|
-
});
|
|
3957
|
-
changed = true;
|
|
4101
|
+
await this.store.deferProgressWatchNudge(action.conversationId, action.nudgeNumber);
|
|
3958
4102
|
}
|
|
3959
4103
|
}
|
|
3960
4104
|
await this.refreshNextProgressWatchWakeLocked();
|
|
3961
|
-
return
|
|
4105
|
+
return actions.length > 0;
|
|
3962
4106
|
}
|
|
3963
4107
|
async processLifecycleLocked() {
|
|
3964
4108
|
const now = this.now().getTime();
|
|
@@ -4253,6 +4397,9 @@ export class GatewayService {
|
|
|
4253
4397
|
const conversationId = options.existingConversationId ?? createGatewayConversationId();
|
|
4254
4398
|
const conversation = this.conversations.get(conversationId) ?? {
|
|
4255
4399
|
id: conversationId,
|
|
4400
|
+
...(options.existingConversationId === undefined
|
|
4401
|
+
? {}
|
|
4402
|
+
: { dedupeScope: createGatewayConversationId() }),
|
|
4256
4403
|
sourceAlias,
|
|
4257
4404
|
targetAlias,
|
|
4258
4405
|
expectsReply,
|
|
@@ -4269,7 +4416,7 @@ export class GatewayService {
|
|
|
4269
4416
|
sourceAlias,
|
|
4270
4417
|
targetAlias,
|
|
4271
4418
|
body: text,
|
|
4272
|
-
dedupeKey: `${conversationId}:${sequence}`,
|
|
4419
|
+
dedupeKey: `${conversation.dedupeScope ?? conversationId}:${sequence}`,
|
|
4273
4420
|
conversationIdSuffix: conversationId.slice(-8),
|
|
4274
4421
|
deadlineAt,
|
|
4275
4422
|
...(options.steer === true ? { steer: true } : {}),
|
|
@@ -4348,7 +4495,7 @@ export class GatewayService {
|
|
|
4348
4495
|
binding: capability.binding,
|
|
4349
4496
|
},
|
|
4350
4497
|
body: text,
|
|
4351
|
-
dedupeKey: `${conversation.id}:${sequence}`,
|
|
4498
|
+
dedupeKey: `${conversation.dedupeScope ?? conversation.id}:${sequence}`,
|
|
4352
4499
|
conversationIdSuffix: conversation.id.slice(-8),
|
|
4353
4500
|
deadlineAt,
|
|
4354
4501
|
...(conversation.pair === true ? { pair: true } : {}),
|
|
@@ -4410,7 +4557,7 @@ export class GatewayService {
|
|
|
4410
4557
|
},
|
|
4411
4558
|
targetAlias: conversation.sourceAlias,
|
|
4412
4559
|
body: text,
|
|
4413
|
-
dedupeKey: `${conversation.id}:${sequence}`,
|
|
4560
|
+
dedupeKey: `${conversation.dedupeScope ?? conversation.id}:${sequence}`,
|
|
4414
4561
|
conversationIdSuffix: conversation.id.slice(-8),
|
|
4415
4562
|
deadlineAt,
|
|
4416
4563
|
authorizedPairTeardownReply: true,
|
|
@@ -4525,14 +4672,17 @@ export class GatewayService {
|
|
|
4525
4672
|
await this.publishNativeHeldLocked(afterCallbacks);
|
|
4526
4673
|
}
|
|
4527
4674
|
}
|
|
4528
|
-
restoreQueuedMessageContextLocked(item, binding) {
|
|
4675
|
+
restoreQueuedMessageContextLocked(item, binding, canonicalConversationId) {
|
|
4529
4676
|
this.pruneTransient();
|
|
4530
|
-
const
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
const conversation = {
|
|
4677
|
+
const conversationId = canonicalConversationId ??
|
|
4678
|
+
(item.conversationIdSuffix === undefined
|
|
4679
|
+
? createGatewayConversationId()
|
|
4680
|
+
: `${createGatewayConversationId().slice(0, -8)}${item.conversationIdSuffix}`);
|
|
4681
|
+
const conversation = this.conversations.get(conversationId) ?? {
|
|
4535
4682
|
id: conversationId,
|
|
4683
|
+
...(canonicalConversationId === undefined
|
|
4684
|
+
? {}
|
|
4685
|
+
: { dedupeScope: createGatewayConversationId() }),
|
|
4536
4686
|
sourceAlias: item.sourceAlias,
|
|
4537
4687
|
targetAlias: item.targetAlias,
|
|
4538
4688
|
expectsReply: false,
|
|
@@ -4589,6 +4739,7 @@ export class GatewayService {
|
|
|
4589
4739
|
return;
|
|
4590
4740
|
const selectedBinding = this.routeBindings.get(targetAlias);
|
|
4591
4741
|
let context = this.messageContexts.get(item.messageId);
|
|
4742
|
+
let progressWatchActive;
|
|
4592
4743
|
if (context === undefined && selectedBinding === undefined) {
|
|
4593
4744
|
const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
|
|
4594
4745
|
if (result.status === "settled") {
|
|
@@ -4598,7 +4749,15 @@ export class GatewayService {
|
|
|
4598
4749
|
return;
|
|
4599
4750
|
}
|
|
4600
4751
|
if (context === undefined && selectedBinding !== undefined) {
|
|
4601
|
-
|
|
4752
|
+
const resolution = item.pair === true && item.conversationIdSuffix !== undefined
|
|
4753
|
+
? await this.store.resolveProgressWatchDispatch({
|
|
4754
|
+
recoveredConversationIdSuffix: item.conversationIdSuffix,
|
|
4755
|
+
sourceAlias: item.sourceAlias,
|
|
4756
|
+
targetAlias: item.targetAlias,
|
|
4757
|
+
})
|
|
4758
|
+
: undefined;
|
|
4759
|
+
context = this.restoreQueuedMessageContextLocked(item, selectedBinding, resolution?.conversationId);
|
|
4760
|
+
progressWatchActive = resolution?.markerActive === true;
|
|
4602
4761
|
}
|
|
4603
4762
|
if (context === undefined) {
|
|
4604
4763
|
const result = await this.store.settleMessage({
|
|
@@ -4741,23 +4900,16 @@ export class GatewayService {
|
|
|
4741
4900
|
this.scheduleLifecycleWakeLocked();
|
|
4742
4901
|
return;
|
|
4743
4902
|
}
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
ownerAlias: item.sourceAlias,
|
|
4754
|
-
workerAlias: item.targetAlias,
|
|
4755
|
-
});
|
|
4756
|
-
progressWatches = await this.store.inspectProgressWatches();
|
|
4903
|
+
if (progressWatchActive === undefined) {
|
|
4904
|
+
progressWatchActive =
|
|
4905
|
+
item.pair === true
|
|
4906
|
+
? (await this.store.resolveProgressWatchDispatch({
|
|
4907
|
+
conversationId: context.conversationId,
|
|
4908
|
+
sourceAlias: item.sourceAlias,
|
|
4909
|
+
targetAlias: item.targetAlias,
|
|
4910
|
+
})).markerActive
|
|
4911
|
+
: false;
|
|
4757
4912
|
}
|
|
4758
|
-
const progressWatchActive = progressWatches.some((watch) => watch.capability === "conversation" &&
|
|
4759
|
-
watch.workerAlias === item.targetAlias &&
|
|
4760
|
-
watch.conversationId === context.conversationId);
|
|
4761
4913
|
let result;
|
|
4762
4914
|
try {
|
|
4763
4915
|
result = await this.awaitDispatchWithNativeHeldLocked(tracker, this.adapter(binding.provider, binding.hostId).dispatch({
|
|
@@ -4771,6 +4923,9 @@ export class GatewayService {
|
|
|
4771
4923
|
expectsReply: context.expectsReply,
|
|
4772
4924
|
deadlineAt: item.deadlineAt,
|
|
4773
4925
|
...(item.steer === true ? { steer: true } : {}),
|
|
4926
|
+
...(item.steer === true && item.queuedAhead !== undefined
|
|
4927
|
+
? { queuedAhead: item.queuedAhead }
|
|
4928
|
+
: {}),
|
|
4774
4929
|
...(progressWatchActive
|
|
4775
4930
|
? { progressWatchActive: true }
|
|
4776
4931
|
: {}),
|
|
@@ -5404,11 +5559,37 @@ export class GatewayService {
|
|
|
5404
5559
|
if (event.outcome === "incompatible") {
|
|
5405
5560
|
await this.requireEndpointRefreshUnconflicted();
|
|
5406
5561
|
this.activeCodexIncompatibility = structuredClone(event.attestation);
|
|
5562
|
+
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", event.previous.hostId), event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE");
|
|
5407
5563
|
this.addRuntimeAlert(event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE", "error", { provider: "codex", host: event.previous.hostId });
|
|
5408
5564
|
this.handledIncompatibleEndpointRefreshes.set(event.previous.hostId, transitionKey);
|
|
5409
5565
|
await this.changed();
|
|
5410
5566
|
return false;
|
|
5411
5567
|
}
|
|
5568
|
+
if (event.attestation.tier === "schema_attested") {
|
|
5569
|
+
// A passing untested-version schema is useful observation, but it is not
|
|
5570
|
+
// durable route or write authority. Settle the provider's pending probe
|
|
5571
|
+
// so a later generation can be examined, while leaving every G1 route
|
|
5572
|
+
// stale and refusing selector/registration activation.
|
|
5573
|
+
await this.requireEndpointRefreshUnconflicted();
|
|
5574
|
+
await this.store.observeConnector({
|
|
5575
|
+
identity: event.current,
|
|
5576
|
+
health: "degraded",
|
|
5577
|
+
compatibility: "unknown",
|
|
5578
|
+
protocol: adapter.protocol,
|
|
5579
|
+
protocolVersion: adapter.protocolVersion,
|
|
5580
|
+
safeErrorCode: "CODEX_MONITOR_ONLY",
|
|
5581
|
+
});
|
|
5582
|
+
adapter.acceptCompatibilityAttestation(event.attestation);
|
|
5583
|
+
this.activeCodexIncompatibility = undefined;
|
|
5584
|
+
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", event.current.hostId), "CODEX_MONITOR_ONLY");
|
|
5585
|
+
this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
|
|
5586
|
+
await this.changed();
|
|
5587
|
+
if (options.deferActivation === true ||
|
|
5588
|
+
options.requiredRoute !== undefined) {
|
|
5589
|
+
throw new BridgeError("CODEX_MONITOR_ONLY", "This Codex App Server passed read-only schema probes but cannot authorize a task route or message write.");
|
|
5590
|
+
}
|
|
5591
|
+
return false;
|
|
5592
|
+
}
|
|
5412
5593
|
await this.store.observeConnector({
|
|
5413
5594
|
identity: event.current,
|
|
5414
5595
|
health: "healthy",
|
|
@@ -5639,11 +5820,7 @@ export class GatewayService {
|
|
|
5639
5820
|
// Provider writes remain closed until every durable and native transition
|
|
5640
5821
|
// above is complete. This acknowledgement is deliberately the final
|
|
5641
5822
|
// fallible activation step.
|
|
5642
|
-
|
|
5643
|
-
if (accept === undefined) {
|
|
5644
|
-
throw new BridgeError("CODEX_ENDPOINT_REFRESH_ACTIVATION_UNAVAILABLE", "The Codex provider cannot acknowledge the validated endpoint transition.");
|
|
5645
|
-
}
|
|
5646
|
-
accept.call(adapter, event.attestation);
|
|
5823
|
+
adapter.acceptCompatibilityAttestation(event.attestation);
|
|
5647
5824
|
}
|
|
5648
5825
|
catch (error) {
|
|
5649
5826
|
try {
|
|
@@ -5665,6 +5842,7 @@ export class GatewayService {
|
|
|
5665
5842
|
}
|
|
5666
5843
|
this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
|
|
5667
5844
|
this.activeCodexIncompatibility = undefined;
|
|
5845
|
+
this.incompatibleProviderSurfaces.delete(this.providerSurfaceKey("codex", event.current.hostId));
|
|
5668
5846
|
return false;
|
|
5669
5847
|
}
|
|
5670
5848
|
async onRouteState(source, event) {
|
|
@@ -5891,7 +6069,12 @@ export class GatewayService {
|
|
|
5891
6069
|
});
|
|
5892
6070
|
return projectGatewayPublicSnapshot({
|
|
5893
6071
|
...base,
|
|
5894
|
-
|
|
6072
|
+
connectors: this.connectorsWithRegistry(base.connectors),
|
|
6073
|
+
...(compatibilityChecks.length === 0
|
|
6074
|
+
? {}
|
|
6075
|
+
: {
|
|
6076
|
+
compatibilityChecks: compatibilityChecks.map(projectPublicCompatibilityCheck),
|
|
6077
|
+
}),
|
|
5895
6078
|
availablePeers: peers,
|
|
5896
6079
|
activityEvents: this.activityEvents.map((event) => ({
|
|
5897
6080
|
...event,
|