agent-embassy 1.9.4 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +28 -21
  4. package/README.zh-CN.md +26 -20
  5. package/dist/src/gateway/claude-helper-protocol.d.ts +1 -1
  6. package/dist/src/gateway/claude-helper-protocol.js +3 -2
  7. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  8. package/dist/src/gateway/claude-helper-supervisor.d.ts +1 -1
  9. package/dist/src/gateway/claude-helper-supervisor.js +1 -1
  10. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  11. package/dist/src/gateway/claude-helper.js +12 -4
  12. package/dist/src/gateway/claude-helper.js.map +1 -1
  13. package/dist/src/gateway/cli-copy.d.ts +1 -1
  14. package/dist/src/gateway/cli-copy.en.d.ts +9 -4
  15. package/dist/src/gateway/cli-copy.en.js +10 -6
  16. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  17. package/dist/src/gateway/cli-copy.js +5 -0
  18. package/dist/src/gateway/cli-copy.js.map +1 -1
  19. package/dist/src/gateway/cli-copy.zh-CN.d.ts +9 -4
  20. package/dist/src/gateway/cli-copy.zh-CN.js +10 -6
  21. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  22. package/dist/src/gateway/cli.d.ts +3 -9
  23. package/dist/src/gateway/cli.js +56 -63
  24. package/dist/src/gateway/cli.js.map +1 -1
  25. package/dist/src/gateway/codex-local-transport.d.ts +1 -1
  26. package/dist/src/gateway/codex-local-transport.js +2 -2
  27. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  28. package/dist/src/gateway/config.d.ts +4 -3
  29. package/dist/src/gateway/config.js +5 -11
  30. package/dist/src/gateway/config.js.map +1 -1
  31. package/dist/src/gateway/control.d.ts +20 -41
  32. package/dist/src/gateway/control.js +54 -96
  33. package/dist/src/gateway/control.js.map +1 -1
  34. package/dist/src/gateway/dashboard-copy.en.js +2 -2
  35. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  36. package/dist/src/gateway/dashboard-copy.zh-CN.js +2 -2
  37. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  38. package/dist/src/gateway/federation-nodes.d.ts +1 -2
  39. package/dist/src/gateway/federation-nodes.js +29 -14
  40. package/dist/src/gateway/federation-nodes.js.map +1 -1
  41. package/dist/src/gateway/live-dashboard-app/app.js +2 -2
  42. package/dist/src/gateway/live-dashboard-command.d.ts +3 -1
  43. package/dist/src/gateway/live-dashboard-command.js +1 -1
  44. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  45. package/dist/src/gateway/peer-protocol.d.ts +1 -0
  46. package/dist/src/gateway/peer-protocol.js +5 -0
  47. package/dist/src/gateway/peer-protocol.js.map +1 -1
  48. package/dist/src/gateway/providers.d.ts +3 -3
  49. package/dist/src/gateway/providers.js +7 -10
  50. package/dist/src/gateway/providers.js.map +1 -1
  51. package/dist/src/gateway/server.d.ts +1 -2
  52. package/dist/src/gateway/server.js +6 -13
  53. package/dist/src/gateway/server.js.map +1 -1
  54. package/dist/src/gateway/service.d.ts +3 -1
  55. package/dist/src/gateway/service.js +93 -39
  56. package/dist/src/gateway/service.js.map +1 -1
  57. package/dist/src/gateway/store.d.ts +1 -1
  58. package/dist/src/gateway/store.js +30 -29
  59. package/dist/src/gateway/store.js.map +1 -1
  60. package/dist/src/gateway/types.d.ts +4 -4
  61. package/docs/CONFIGURATION.md +27 -21
  62. package/docs/CONFIGURATION.zh-CN.md +19 -15
  63. package/docs/DASHBOARD.md +1 -1
  64. package/docs/DASHBOARD.zh-CN.md +1 -1
  65. package/docs/DELIVERY.md +2 -2
  66. package/docs/DELIVERY.zh-CN.md +2 -2
  67. package/docs/GATEWAY-ARCHITECTURE.md +48 -58
  68. package/package.json +1 -1
  69. package/skills/embassy-peer/SKILL.md +20 -15
  70. package/dist/src/gateway/state-v2-to-v3.d.ts +0 -24
  71. package/dist/src/gateway/state-v2-to-v3.js +0 -1034
  72. package/dist/src/gateway/state-v2-to-v3.js.map +0 -1
@@ -1,1034 +0,0 @@
1
- import { Buffer } from "node:buffer";
2
- import { createHash, randomUUID } from "node:crypto";
3
- import { constants } from "node:fs";
4
- import { lstat, open, realpath, rename as renameFile, unlink, } from "node:fs/promises";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import { BridgeError } from "../errors.js";
8
- import { isGatewayPersistedStateV3 } from "./store.js";
9
- const STATE_MARKER = ".agent-embassy-state";
10
- const STATE_MARKER_BODY = "agent-embassy-state-v1\n";
11
- const STATE_FILE = "gateway-state.json";
12
- const BACKUP_FILE = "gateway-state.v2.backup.json";
13
- const V3_BACKUP_FILE = "gateway-state.v3.backup.json";
14
- const CONTROLLER_LOCK = ".gateway-controller.lock";
15
- const MAX_STATE_BYTES = 8 * 1024 * 1024;
16
- const MAX_MARKER_BYTES = 128;
17
- const MAX_LOCK_BYTES = 4 * 1024;
18
- const PROVIDERS = new Set(["claude", "codex", "deepseek", "grok"]);
19
- const ROUTE_STATES = new Set([
20
- "stale", "idle", "busy", "awaiting_approval", "offline", "disabled",
21
- ]);
22
- const REGISTRATION_MODES = new Set(["explicit_opt_in", "selected_live_peer"]);
23
- const CONNECTOR_HEALTH = new Set(["offline", "connecting", "healthy", "degraded"]);
24
- const DELIVERY_STATES = new Set([
25
- "queued", "duplicate", "dispatching", "transport_written", "held",
26
- "delivered", "unconfirmed", "failed", "ambiguous", "expired",
27
- "cancelled", "abandoned", "rejected",
28
- ]);
29
- const ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
30
- const HOST = /^[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
31
- const PRIVATE_TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
32
- const SAFE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
33
- const MESSAGE_ID = /^msg_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
34
- const MESSAGE_SUFFIX = /^[0-9a-f]{8}$/;
35
- const CONVERSATION_SUFFIX = /^[A-Za-z0-9_-]{8}$/;
36
- const FINGERPRINT = /^[A-Za-z0-9_-]{43}$/;
37
- const PROTOCOL = /^[a-z0-9][a-z0-9._-]{0,63}$/;
38
- const PROTOCOL_VERSION = /^[A-Za-z0-9][A-Za-z0-9._:+-]{0,63}$/;
39
- const INGRESS_PREFIX = {
40
- claude: undefined,
41
- codex: "codex-",
42
- deepseek: "dsh-",
43
- grok: "grok-",
44
- peer: "peer-",
45
- };
46
- export const gatewayStateV2ToV3FaultStages = [
47
- "before_backup_write",
48
- "before_backup_file_sync",
49
- "before_backup_directory_sync",
50
- "before_backup_readback",
51
- "before_target_write",
52
- "before_target_file_sync",
53
- "before_target_rename",
54
- "after_target_rename",
55
- "before_target_directory_sync",
56
- "before_target_readback",
57
- ];
58
- class ConversionCommitError extends BridgeError {
59
- constructor(verified) {
60
- super("GATEWAY_STATE_COMMIT_OUTCOME_UNKNOWN", verified
61
- ? "The v3 state rename was installed and read back exactly, but its directory durability was not confirmed; retry is forbidden."
62
- : "The v3 state rename crossed the commit point but exact readback failed; retry and automatic rollback are forbidden.", false);
63
- }
64
- }
65
- /**
66
- * Release-owned offline conversion and bounded host reconciliation. Callers must resolve the normal
67
- * configured state directory before entering; this API never starts providers,
68
- * helpers, discovery, the control socket, or the broker runtime.
69
- */
70
- export async function convertGatewayStateV2ToV3(options, dependencies = {}) {
71
- const now = dependencies.now ?? (() => new Date());
72
- const randomId = dependencies.randomId ?? randomUUID;
73
- const fault = dependencies.fault ?? (() => undefined);
74
- const renameState = dependencies.renameState ?? renameFile;
75
- const root = await validateStateRoot(options.stateDir);
76
- const marker = await readPrivateFile(path.join(root, STATE_MARKER), MAX_MARKER_BYTES);
77
- if (marker.toString("utf8") !== STATE_MARKER_BODY) {
78
- throw conversionError("GATEWAY_STATE_DIRECTORY_NOT_OWNED", "The gateway state ownership marker is not recognized.");
79
- }
80
- const lock = await acquireOfflineLock(root, nextRandomId(randomId));
81
- try {
82
- const statePath = path.join(root, STATE_FILE);
83
- const installedSource = await readPrivateFile(statePath, MAX_STATE_BYTES);
84
- const installedValue = parseJson(installedSource);
85
- const hostId = options.hostId ?? "this-mac";
86
- const nativeV3 = isGatewayPersistedStateV3(installedValue);
87
- const backupPath = path.join(root, nativeV3 ? V3_BACKUP_FILE : BACKUP_FILE);
88
- let converted;
89
- if (nativeV3) {
90
- const routes = reconcileHostRoutes(installedValue.routes, hostId, { ...installedValue, routes: [] });
91
- if (routes.length === installedValue.routes.length)
92
- throw conversionError("GATEWAY_STATE_CONVERSION_ALREADY_APPLIED", "The gateway state is already native schema v3; a second conversion is forbidden.");
93
- converted = { ...installedValue, routes, updatedAt: now().toISOString(),
94
- commit: { sequence: installedValue.commit.sequence + 1, id: nextRandomId(randomId) } };
95
- }
96
- else {
97
- const v2 = decodeGatewayStateV2(installedSource);
98
- if (v2.codexSuccession !== null)
99
- throw conversionError("GATEWAY_STATE_CONVERSION_SUCCESSION_ACTIVE", "State conversion requires the Codex succession journal to be inactive.");
100
- converted = convertState({ ...v2, routes: reconcileHostRoutes(v2.routes, hostId, { ...v2, routes: [] }) }, now(), nextRandomId(randomId));
101
- }
102
- if (!isGatewayPersistedStateV3(converted)) {
103
- throw conversionError("CORRUPT_GATEWAY_STATE", "The strict v2 state could not be represented as valid native v3 state.");
104
- }
105
- const body = Buffer.from(`${JSON.stringify(converted, null, 2)}\n`, "utf8");
106
- if (body.byteLength > MAX_STATE_BYTES) {
107
- throw conversionError("GATEWAY_STATE_FILE_TOO_LARGE", "The converted native state exceeds its strict byte limit.");
108
- }
109
- if (nativeV3) {
110
- try {
111
- const existing = await readPrivateFile(backupPath, MAX_STATE_BYTES);
112
- if (!existing.equals(installedSource))
113
- throw conversionError("GATEWAY_STATE_BACKUP_MISMATCH", "The fixed v3 recovery backup does not match the installed source.");
114
- }
115
- catch (error) {
116
- if (nodeCode(error) !== "ENOENT")
117
- throw error;
118
- await writeExclusivePrivateFile(backupPath, installedSource, "backup", fault);
119
- }
120
- }
121
- else
122
- await writeExclusivePrivateFile(backupPath, installedSource, "backup", fault);
123
- await fault("before_backup_directory_sync");
124
- await syncDirectory(root);
125
- await fault("before_backup_readback");
126
- const backup = await readPrivateFile(backupPath, MAX_STATE_BYTES);
127
- if (!backup.equals(installedSource) ||
128
- sha256(backup) !== sha256(installedSource)) {
129
- throw conversionError("GATEWAY_STATE_BACKUP_MISMATCH", "The byte-identical state backup could not be verified; the target was not mutated.");
130
- }
131
- const temporary = path.join(root, `.gateway-state-v3-${nextRandomId(randomId)}.tmp`);
132
- let temporaryHandle;
133
- let renamed = false;
134
- try {
135
- temporaryHandle = await openExclusivePrivate(temporary);
136
- await fault("before_target_write");
137
- await temporaryHandle.writeFile(body);
138
- await temporaryHandle.chmod(0o600);
139
- await fault("before_target_file_sync");
140
- await temporaryHandle.sync();
141
- await temporaryHandle.close();
142
- temporaryHandle = undefined;
143
- // The converter owns the ordinary controller lock, but re-read the
144
- // source immediately before rename so an uncooperative writer cannot be
145
- // overwritten using stale evidence.
146
- await fault("before_target_rename");
147
- const current = await readPrivateFile(statePath, MAX_STATE_BYTES);
148
- if (!current.equals(installedSource) || sha256(current) !== sha256(installedSource)) {
149
- throw conversionError("GATEWAY_STATE_SOURCE_CHANGED", "The state source changed after backup verification; conversion was not installed.");
150
- }
151
- try {
152
- await renameState(temporary, statePath);
153
- renamed = true;
154
- }
155
- catch (renameError) {
156
- const outcome = await reconcileRenameError(statePath, installedSource, body, converted);
157
- if (outcome === "source")
158
- throw renameError;
159
- if (outcome === "unknown")
160
- throw new ConversionCommitError(false);
161
- renamed = true;
162
- }
163
- await fault("after_target_rename");
164
- await fault("before_target_directory_sync");
165
- await syncDirectory(root);
166
- await fault("before_target_readback");
167
- const installed = await readPrivateFile(statePath, MAX_STATE_BYTES);
168
- const parsed = parseJson(installed);
169
- if (!installed.equals(body) ||
170
- !isGatewayPersistedStateV3(parsed) ||
171
- parsed.commit.id !== converted.commit.id ||
172
- parsed.commit.sequence !== converted.commit.sequence) {
173
- throw new ConversionCommitError(false);
174
- }
175
- }
176
- catch (error) {
177
- if (!renamed)
178
- throw error;
179
- let verified = false;
180
- try {
181
- const installed = await readPrivateFile(statePath, MAX_STATE_BYTES);
182
- const parsed = parseJson(installed);
183
- verified =
184
- installed.equals(body) &&
185
- isGatewayPersistedStateV3(parsed) &&
186
- parsed.commit.id === converted.commit.id &&
187
- parsed.commit.sequence === converted.commit.sequence;
188
- }
189
- catch {
190
- // Crossing rename forbids automatic rollback even when readback fails.
191
- }
192
- if (error instanceof ConversionCommitError)
193
- throw error;
194
- throw new ConversionCommitError(verified);
195
- }
196
- finally {
197
- await temporaryHandle?.close().catch(() => undefined);
198
- if (!renamed)
199
- await unlink(temporary).catch(() => undefined);
200
- }
201
- return {
202
- backupFile: path.basename(backupPath),
203
- commitId: converted.commit.id,
204
- commitSequence: converted.commit.sequence,
205
- };
206
- }
207
- finally {
208
- await releaseOfflineLock(root, lock);
209
- }
210
- }
211
- function reconcileHostRoutes(routes, hostId, remainder) {
212
- const foreign = routes.filter((route) => route.registrationMode !== "federated_peer" && route.binding.hostId !== hostId);
213
- if (foreign.length === 0)
214
- return [...routes];
215
- const referenced = JSON.stringify(remainder);
216
- const droppable = new Set(foreign.filter((route) => Object.values(route.counters).every((count) => count === 0) &&
217
- ((route.binding.provider === "deepseek" && route.alias === `dsh-main@${route.binding.hostId}`) ||
218
- (route.binding.provider === "grok" && route.alias === `grok-main@${route.binding.hostId}`)) &&
219
- !referenced.includes(JSON.stringify(route.alias))).map((route) => route.alias));
220
- if (droppable.size !== foreign.length)
221
- throw conversionError("CORRUPT_GATEWAY_STATE", "Host reconciliation can remove only unused, unreferenced config-declared lazy routes.");
222
- return routes.filter((route) => !droppable.has(route.alias));
223
- }
224
- function convertState(v2, convertedAt, commitId) {
225
- const timestamp = convertedAt.toISOString();
226
- const routes = v2.routes.map((route) => ({
227
- alias: route.alias,
228
- binding: {
229
- provider: route.binding.provider,
230
- hostId: route.binding.hostId,
231
- routeHandle: route.binding.routeHandle,
232
- registrationId: route.binding.ownerLease,
233
- },
234
- registrationMode: route.registrationMode,
235
- enabled: route.enabled,
236
- busyPolicy: route.busyPolicy,
237
- registeredAt: route.registeredAt,
238
- updatedAt: route.updatedAt,
239
- counters: { ...route.counters },
240
- }));
241
- const routeByAlias = new Map(routes.map((route) => [route.alias, route]));
242
- const consentEdges = v2.consentEdges.map((edge) => ({
243
- endpoints: edge.endpoints.map((endpoint) => ({
244
- alias: endpoint.alias,
245
- provider: endpoint.provider,
246
- registrationId: endpoint.ownerLease,
247
- })),
248
- createdAt: edge.createdAt,
249
- updatedAt: edge.updatedAt,
250
- counters: { ...edge.counters },
251
- }));
252
- let nextSequence = v2.eventSequence;
253
- const message = (item, state) => {
254
- const source = routeByAlias.get(item.sourceAlias);
255
- const target = routeByAlias.get(item.targetAlias);
256
- const edge = item.pair === true
257
- ? consentEdges.find((candidate) => candidate.endpoints.some((endpoint) => endpoint.alias === item.sourceAlias) &&
258
- candidate.endpoints.some((endpoint) => endpoint.alias === item.targetAlias))
259
- : undefined;
260
- return {
261
- sequence: ++nextSequence,
262
- messageId: item.messageId,
263
- messageIdSuffix: item.messageIdSuffix,
264
- ...(item.conversationIdSuffix === undefined ? {} : { conversationIdSuffix: item.conversationIdSuffix }),
265
- direction: item.direction,
266
- sourceAlias: item.sourceAlias,
267
- targetAlias: item.targetAlias,
268
- enqueuedAt: item.enqueuedAt,
269
- deadlineAt: item.deadlineAt,
270
- bytes: item.bytes,
271
- ...(item.body === undefined ? {} : { body: item.body }),
272
- ...(item.pair === true ? { pair: true } : {}),
273
- ...(item.transientTarget === true ? { transientTarget: true } : {}),
274
- ...(item.steer === true ? { steer: true } : {}),
275
- sourceRegistrationId: source?.binding.registrationId ?? null,
276
- targetRegistrationId: target?.binding.registrationId ?? null,
277
- consentEdge: edge?.endpoints ?? null,
278
- state,
279
- };
280
- };
281
- const messages = [];
282
- const accounting = { ...v2.accounting };
283
- const recordTerminal = (item, outcome) => {
284
- accounting[outcome] += 1;
285
- const target = routeByAlias.get(item.targetAlias);
286
- if (target !== undefined)
287
- target.counters[outcome] += 1;
288
- if (item.pair === true) {
289
- const edge = consentEdges.find((candidate) => candidate.endpoints.some((endpoint) => endpoint.alias === item.sourceAlias) &&
290
- candidate.endpoints.some((endpoint) => endpoint.alias === item.targetAlias));
291
- if (edge !== undefined) {
292
- edge.counters[outcome] += 1;
293
- edge.updatedAt = timestamp;
294
- }
295
- }
296
- };
297
- for (const item of v2.queue) {
298
- const expired = Date.parse(item.deadlineAt) <= convertedAt.getTime();
299
- const targetMissing = !routeByAlias.has(item.targetAlias);
300
- const terminal = expired || targetMissing || item.body === undefined || item.transientTarget === true;
301
- const outcome = expired ? "expired" : "abandoned";
302
- const record = message(item, terminal
303
- ? {
304
- phase: "terminal",
305
- outcome,
306
- terminalAt: timestamp,
307
- safeErrorCode: expired ? "MESSAGE_EXPIRED" : "CONTROLLER_RESTARTED",
308
- latencyMs: Math.max(0, convertedAt.getTime() - Date.parse(item.enqueuedAt)),
309
- }
310
- : { phase: "queued", attemptCount: 0 });
311
- messages.push(record);
312
- if (terminal)
313
- recordTerminal(item, outcome);
314
- }
315
- for (const item of v2.inFlight) {
316
- const record = message(item, {
317
- phase: "terminal",
318
- outcome: "ambiguous",
319
- terminalAt: timestamp,
320
- safeErrorCode: "CONTROLLER_RESTARTED",
321
- latencyMs: Math.max(0, convertedAt.getTime() - Date.parse(item.enqueuedAt)),
322
- });
323
- messages.push(record);
324
- recordTerminal(item, "ambiguous");
325
- }
326
- const queuedBytes = messages.reduce((total, item) => total + (item.state.phase === "queued" ? item.bytes : 0), 0);
327
- const activity = v2.events.map((event) => ({
328
- type: "legacy_message",
329
- event: { ...event },
330
- }));
331
- return {
332
- schemaVersion: 3,
333
- commit: { sequence: 0, id: commitId },
334
- createdAt: v2.createdAt,
335
- updatedAt: timestamp,
336
- eventSequence: nextSequence,
337
- routes,
338
- consentEdges,
339
- messages,
340
- dedupe: v2.dedupe.map((record) => ({ ...record })),
341
- rateBuckets: v2.rateBuckets.map((bucket) => ({ ...bucket })),
342
- activity,
343
- accounting: { ...accounting, queuedBytes },
344
- };
345
- }
346
- function decodeGatewayStateV2(body) {
347
- const value = parseJson(body);
348
- if (isGatewayPersistedStateV3(value)) {
349
- throw conversionError("GATEWAY_STATE_CONVERSION_ALREADY_APPLIED", "The gateway state is already native schema v3; a second conversion is forbidden.");
350
- }
351
- if (!isV2State(value)) {
352
- throw conversionError("CORRUPT_GATEWAY_STATE", "The offline converter requires one strict, internally consistent native v2 state file.");
353
- }
354
- if (value.codexSuccession !== null) {
355
- throw conversionError("GATEWAY_STATE_CONVERSION_SUCCESSION_ACTIVE", "State conversion requires the Codex succession journal to be inactive.");
356
- }
357
- return value;
358
- }
359
- async function reconcileRenameError(statePath, source, body, converted) {
360
- try {
361
- const current = await readPrivateFile(statePath, MAX_STATE_BYTES);
362
- if (current.equals(source) && sha256(current) === sha256(source))
363
- return "source";
364
- if (!current.equals(body))
365
- return "unknown";
366
- const parsed = parseJson(current);
367
- return isGatewayPersistedStateV3(parsed) &&
368
- parsed.commit.id === converted.commit.id &&
369
- parsed.commit.sequence === converted.commit.sequence
370
- ? "installed"
371
- : "unknown";
372
- }
373
- catch {
374
- return "unknown";
375
- }
376
- }
377
- function parseJson(body) {
378
- try {
379
- return JSON.parse(body.toString("utf8"));
380
- }
381
- catch {
382
- throw conversionError("CORRUPT_GATEWAY_STATE", "The gateway state is not strict JSON.");
383
- }
384
- }
385
- function isV2State(value) {
386
- if (!isRecord(value) ||
387
- !exactKeys(value, [
388
- "schemaVersion", "createdAt", "updatedAt", "eventSequence", "routes",
389
- "consentEdges", "connectors", "queue", "inFlight", "events", "dedupe",
390
- "rateBuckets", "accounting", "watchSequence", "progressWatches",
391
- "progressWatchEvents", "codexEndpointRefreshSequence",
392
- "codexEndpointRefreshEvents", "codexOrphanRemovalSequence",
393
- "codexOrphanRemovalEvents", "codexSuccession",
394
- ]) ||
395
- value.schemaVersion !== 2 ||
396
- !iso(value.createdAt) ||
397
- !iso(value.updatedAt) ||
398
- !nonNegativeInteger(value.eventSequence) ||
399
- !nonNegativeInteger(value.watchSequence) ||
400
- !nonNegativeInteger(value.codexEndpointRefreshSequence) ||
401
- !nonNegativeInteger(value.codexOrphanRemovalSequence) ||
402
- !Array.isArray(value.routes) || !value.routes.every(isV2Route) ||
403
- !Array.isArray(value.consentEdges) || value.consentEdges.length > 256 ||
404
- !value.consentEdges.every(isV2ConsentEdge) ||
405
- !Array.isArray(value.connectors) || !value.connectors.every(isV2Connector) ||
406
- !Array.isArray(value.queue) || !value.queue.every(isV2QueuedMessage) ||
407
- !Array.isArray(value.inFlight) || !value.inFlight.every(isV2InFlightMessage) ||
408
- !Array.isArray(value.events) || value.events.length > 1_024 ||
409
- !value.events.every(isV2Event) ||
410
- !Array.isArray(value.dedupe) || !value.dedupe.every(isV2Dedupe) ||
411
- !Array.isArray(value.rateBuckets) || !value.rateBuckets.every(isV2RateBucket) ||
412
- !isCounters(value.accounting, true) ||
413
- !Array.isArray(value.progressWatches) || value.progressWatches.length > 256 ||
414
- !value.progressWatches.every(isV2ProgressWatch) ||
415
- !Array.isArray(value.progressWatchEvents) || value.progressWatchEvents.length > 1_024 ||
416
- !value.progressWatchEvents.every(isV2ProgressWatchEvent) ||
417
- !Array.isArray(value.codexEndpointRefreshEvents) ||
418
- value.codexEndpointRefreshEvents.length > 256 ||
419
- !value.codexEndpointRefreshEvents.every(isV2EndpointRefreshEvent) ||
420
- !Array.isArray(value.codexOrphanRemovalEvents) ||
421
- value.codexOrphanRemovalEvents.length > 256 ||
422
- !value.codexOrphanRemovalEvents.every(isV2OrphanEvent) ||
423
- (value.codexSuccession !== null && !isV2CodexSuccession(value.codexSuccession)))
424
- return false;
425
- // The active journal is deliberately refused rather than interpreted by the
426
- // converter. Validate the rest of the graph before returning that safe code.
427
- const routes = value.routes;
428
- const edges = value.consentEdges;
429
- const queue = value.queue;
430
- const inFlight = value.inFlight;
431
- const aliases = new Set(routes.map((route) => route.alias));
432
- const leases = new Set(routes.map((route) => route.binding.ownerLease));
433
- const targets = new Set(routes.map((route) => `${route.binding.provider}\0${route.binding.hostId}\0${route.binding.endpointGeneration}\0${route.binding.routeHandle}`));
434
- const routeByAlias = new Map(routes.map((route) => [route.alias, route]));
435
- const edgeKeys = edges.map((edge) => edge.endpoints
436
- .map((endpoint) => `${endpoint.provider}\0${endpoint.alias}\0${endpoint.ownerLease}`)
437
- .join("\0"));
438
- const messageIds = [...queue, ...inFlight].map((item) => item.messageId);
439
- const expectedQueuedBytes = queue.reduce((total, item) => total + item.bytes, 0);
440
- const connectors = value.connectors;
441
- const events = value.events;
442
- const progressEvents = value.progressWatchEvents;
443
- const refreshEvents = value.codexEndpointRefreshEvents;
444
- const orphanEvents = value.codexOrphanRemovalEvents;
445
- const watches = value.progressWatches;
446
- if (aliases.size !== routes.length ||
447
- leases.size !== routes.length ||
448
- targets.size !== routes.length ||
449
- new Set(edgeKeys).size !== edges.length ||
450
- new Set(messageIds).size !== messageIds.length ||
451
- new Set(connectors.map((item) => `${item.provider}\0${item.hostId}`)).size !== connectors.length ||
452
- new Set(value.dedupe.map((item) => item.fingerprint)).size !== value.dedupe.length ||
453
- new Set(value.rateBuckets.map((item) => item.sourceAlias)).size !== value.rateBuckets.length ||
454
- new Set(watches.map((watch) => watch.conversationId)).size !== watches.length ||
455
- new Set(watches.map(v2ProgressWatchPairKey)).size !== watches.length ||
456
- value.accounting.queuedBytes !== expectedQueuedBytes)
457
- return false;
458
- for (const edge of edges) {
459
- const [left, right] = edge.endpoints;
460
- const leftRoute = routeByAlias.get(left.alias);
461
- const rightRoute = routeByAlias.get(right.alias);
462
- if (leftRoute?.binding.provider !== left.provider ||
463
- rightRoute?.binding.provider !== right.provider ||
464
- leftRoute.binding.ownerLease !== left.ownerLease ||
465
- rightRoute.binding.ownerLease !== right.ownerLease ||
466
- left.provider === right.provider ||
467
- leftRoute.binding.hostId !== rightRoute.binding.hostId ||
468
- compareV2Endpoints(left, right) >= 0)
469
- return false;
470
- }
471
- for (const route of routes) {
472
- if (!route.alias.endsWith(`@${route.binding.hostId}`) ||
473
- (route.binding.provider === "claude") !==
474
- (route.registrationMode === "selected_live_peer") ||
475
- (INGRESS_PREFIX[route.binding.provider] !== undefined &&
476
- !route.alias.startsWith(INGRESS_PREFIX[route.binding.provider])) ||
477
- route.queueDepth !== queue.filter((item) => item.targetAlias === route.alias).length)
478
- return false;
479
- }
480
- for (const watch of watches) {
481
- const owner = routeByAlias.get(String(watch.ownerAlias));
482
- const worker = routeByAlias.get(String(watch.workerAlias));
483
- if (owner === undefined || worker === undefined ||
484
- owner.binding.ownerLease !== watch.ownerLease ||
485
- worker.binding.ownerLease !== watch.workerLease ||
486
- owner.binding.provider === worker.binding.provider ||
487
- owner.binding.hostId !== worker.binding.hostId)
488
- return false;
489
- const expected = [owner, worker]
490
- .map((route) => ({
491
- alias: route.alias,
492
- provider: route.binding.provider,
493
- ownerLease: route.binding.ownerLease,
494
- }))
495
- .sort(compareV2Endpoints);
496
- if (!edges.some((edge) => edge.endpoints.every((endpoint, index) => endpoint.alias === expected[index].alias &&
497
- endpoint.provider === expected[index].provider &&
498
- endpoint.ownerLease === expected[index].ownerLease)))
499
- return false;
500
- }
501
- const messageGraphValid = [...queue, ...inFlight].every((item) => {
502
- const separator = item.direction.indexOf("_to_");
503
- const sourceProvider = item.direction.slice(0, separator);
504
- const targetProvider = item.direction.slice(separator + 4);
505
- const source = routeByAlias.get(item.sourceAlias);
506
- const target = routeByAlias.get(item.targetAlias);
507
- const claudeHosts = new Set(connectors
508
- .filter((connector) => connector.provider === "claude")
509
- .map((connector) => connector.hostId));
510
- const sourceValid = source?.binding.provider === sourceProvider ||
511
- (source === undefined && sourceProvider === "claude" && target !== undefined &&
512
- claudeHosts.has(target.binding.hostId) && aliasHost(item.sourceAlias) === target.binding.hostId);
513
- const targetValid = target?.binding.provider === targetProvider ||
514
- (target === undefined && targetProvider === "claude" && source !== undefined &&
515
- claudeHosts.has(source.binding.hostId) && aliasHost(item.targetAlias) === source.binding.hostId);
516
- const consentValid = item.pair !== true || edges.some((edge) => edge.endpoints.some((endpoint) => endpoint.alias === item.sourceAlias) &&
517
- edge.endpoints.some((endpoint) => endpoint.alias === item.targetAlias));
518
- return item.messageIdSuffix === item.messageId.replaceAll("-", "").slice(-8).toLowerCase() &&
519
- sourceValid && targetValid && consentValid &&
520
- (source === undefined || target === undefined || source.binding.hostId === target.binding.hostId);
521
- });
522
- const metadataGraphValid = value.dedupe.every((item) => {
523
- const separator = String(item.direction).indexOf("_to_");
524
- const sourceProvider = String(item.direction).slice(0, separator);
525
- const targetProvider = String(item.direction).slice(separator + 4);
526
- const source = routeByAlias.get(String(item.sourceAlias));
527
- const target = routeByAlias.get(String(item.targetAlias));
528
- const claudeHosts = new Set(connectors
529
- .filter((connector) => connector.provider === "claude")
530
- .map((connector) => connector.hostId));
531
- const sourceValid = source?.binding.provider === sourceProvider ||
532
- (source === undefined && sourceProvider === "claude" && target !== undefined &&
533
- claudeHosts.has(target.binding.hostId) && aliasHost(String(item.sourceAlias)) === target.binding.hostId);
534
- const targetValid = target?.binding.provider === targetProvider ||
535
- (target === undefined && targetProvider === "claude" && source !== undefined &&
536
- claudeHosts.has(source.binding.hostId) && aliasHost(String(item.targetAlias)) === source.binding.hostId);
537
- const consentValid = item.pair !== true || edges.some((edge) => edge.endpoints.some((endpoint) => endpoint.alias === item.sourceAlias) &&
538
- edge.endpoints.some((endpoint) => endpoint.alias === item.targetAlias));
539
- return sourceValid && targetValid && consentValid &&
540
- (source === undefined || target === undefined || source.binding.hostId === target.binding.hostId);
541
- });
542
- const rateGraphValid = value.rateBuckets.every((bucket) => {
543
- const alias = String(bucket.sourceAlias);
544
- if (routeByAlias.has(alias))
545
- return true;
546
- const host = aliasHost(alias);
547
- return connectors.some((connector) => connector.provider === "claude" && connector.hostId === host) &&
548
- routes.some((route) => route.binding.hostId === host);
549
- });
550
- const strictSequence = (items, ceiling) => items.every((item, index) => typeof item.sequence === "number" && item.sequence <= ceiling &&
551
- (index === 0 || item.sequence > Number(items[index - 1].sequence)));
552
- const contiguousJournal = (items, sequence) => items.length === 0
553
- ? sequence === 0
554
- : items.at(-1)?.sequence === sequence && items.every((item, index) => index === 0 || item.sequence === Number(items[index - 1].sequence) + 1);
555
- return messageGraphValid && metadataGraphValid && rateGraphValid &&
556
- strictSequence(events, value.eventSequence) &&
557
- strictSequence(progressEvents, value.watchSequence) &&
558
- contiguousJournal(refreshEvents, value.codexEndpointRefreshSequence) &&
559
- contiguousJournal(orphanEvents, value.codexOrphanRemovalSequence) &&
560
- isV2SuccessionConsistent(value);
561
- }
562
- function isV2Route(value) {
563
- if (!isRecord(value) || !exactKeys(value, ["alias", "binding", "registrationMode", "enabled", "state", "busyPolicy", "registeredAt", "updatedAt", "queueDepth", "counters"], ["lastSeenAt", "safeErrorCode"]))
564
- return false;
565
- return typeof value.alias === "string" && ALIAS.test(value.alias) &&
566
- isV2Binding(value.binding) && typeof value.registrationMode === "string" &&
567
- REGISTRATION_MODES.has(value.registrationMode) && typeof value.enabled === "boolean" &&
568
- typeof value.state === "string" && ROUTE_STATES.has(value.state) &&
569
- value.busyPolicy === "queue" && iso(value.registeredAt) && iso(value.updatedAt) &&
570
- (value.lastSeenAt === undefined || iso(value.lastSeenAt)) &&
571
- nonNegativeInteger(value.queueDepth) && isCounters(value.counters) &&
572
- (value.safeErrorCode === undefined || safeCode(value.safeErrorCode));
573
- }
574
- function isV2Binding(value) {
575
- return isRecord(value) && exactKeys(value, ["provider", "hostId", "endpointGeneration", "routeHandle", "ownerLease"]) &&
576
- provider(value.provider) && typeof value.hostId === "string" && HOST.test(value.hostId) &&
577
- token(value.endpointGeneration) && token(value.routeHandle) && token(value.ownerLease);
578
- }
579
- function isV2ConsentEndpoint(value) {
580
- return isRecord(value) && exactKeys(value, ["alias", "provider", "ownerLease"]) &&
581
- typeof value.alias === "string" && ALIAS.test(value.alias) && provider(value.provider) &&
582
- token(value.ownerLease);
583
- }
584
- function isV2ConsentEdge(value) {
585
- return isRecord(value) && exactKeys(value, ["endpoints", "createdAt", "updatedAt", "counters"]) &&
586
- Array.isArray(value.endpoints) && value.endpoints.length === 2 &&
587
- value.endpoints.every(isV2ConsentEndpoint) && iso(value.createdAt) &&
588
- iso(value.updatedAt) && isCounters(value.counters);
589
- }
590
- function isV2Connector(value) {
591
- return isRecord(value) && exactKeys(value, ["provider", "hostId", "endpointGeneration", "health", "protocol", "protocolVersion", "updatedAt"], ["lastSeenAt", "safeErrorCode"]) && provider(value.provider) &&
592
- typeof value.hostId === "string" && HOST.test(value.hostId) && token(value.endpointGeneration) &&
593
- typeof value.protocol === "string" && PROTOCOL.test(value.protocol) &&
594
- typeof value.protocolVersion === "string" && PROTOCOL_VERSION.test(value.protocolVersion) &&
595
- typeof value.health === "string" &&
596
- CONNECTOR_HEALTH.has(value.health) && iso(value.updatedAt) &&
597
- (value.lastSeenAt === undefined || iso(value.lastSeenAt)) &&
598
- (value.safeErrorCode === undefined || safeCode(value.safeErrorCode));
599
- }
600
- function isV2QueuedMessage(value) {
601
- if (!isRecord(value) || !exactKeys(value, ["messageId", "messageIdSuffix", "direction", "sourceAlias", "targetAlias", "enqueuedAt", "deadlineAt", "bytes"], ["conversationIdSuffix", "body", "pair", "transientTarget", "steer"]))
602
- return false;
603
- return typeof value.messageId === "string" && MESSAGE_ID.test(value.messageId) &&
604
- typeof value.messageIdSuffix === "string" && MESSAGE_SUFFIX.test(value.messageIdSuffix) &&
605
- (value.conversationIdSuffix === undefined || (typeof value.conversationIdSuffix === "string" && CONVERSATION_SUFFIX.test(value.conversationIdSuffix))) &&
606
- direction(value.direction) && typeof value.sourceAlias === "string" && ALIAS.test(value.sourceAlias) &&
607
- typeof value.targetAlias === "string" && ALIAS.test(value.targetAlias) &&
608
- iso(value.enqueuedAt) && iso(value.deadlineAt) && positiveInteger(value.bytes) &&
609
- (value.body === undefined || (typeof value.body === "string" && value.body.length > 0 &&
610
- !value.body.includes("\0") && Buffer.byteLength(value.body, "utf8") === value.bytes)) &&
611
- (value.pair === undefined || value.pair === true) &&
612
- (value.transientTarget === undefined || value.transientTarget === true) &&
613
- (value.steer === undefined || value.steer === true);
614
- }
615
- function isV2InFlightMessage(value) {
616
- if (!isRecord(value) || !iso(value.dispatchedAt))
617
- return false;
618
- const { dispatchedAt: _ignored, ...queued } = value;
619
- return isV2QueuedMessage(queued);
620
- }
621
- function isV2Event(value) {
622
- if (!isRecord(value) || !exactKeys(value, ["sequence", "timestamp", "messageIdSuffix", "direction", "sourceAlias", "targetAlias", "state", "bytes"], ["conversationIdSuffix", "body", "latencyMs", "safeErrorCode", "steer"]))
623
- return false;
624
- return positiveInteger(value.sequence) && iso(value.timestamp) &&
625
- typeof value.messageIdSuffix === "string" && MESSAGE_SUFFIX.test(value.messageIdSuffix) &&
626
- (value.conversationIdSuffix === undefined || (typeof value.conversationIdSuffix === "string" && CONVERSATION_SUFFIX.test(value.conversationIdSuffix))) &&
627
- direction(value.direction) && typeof value.sourceAlias === "string" && ALIAS.test(value.sourceAlias) &&
628
- typeof value.targetAlias === "string" && ALIAS.test(value.targetAlias) &&
629
- typeof value.state === "string" && DELIVERY_STATES.has(value.state) && positiveInteger(value.bytes) &&
630
- (value.body === undefined || (typeof value.body === "string" && value.body.length > 0 &&
631
- !value.body.includes("\0") && Buffer.byteLength(value.body, "utf8") === value.bytes)) &&
632
- (value.latencyMs === undefined || nonNegativeInteger(value.latencyMs)) &&
633
- (value.safeErrorCode === undefined || safeCode(value.safeErrorCode)) &&
634
- (value.steer === undefined || value.steer === true);
635
- }
636
- function isV2Dedupe(value) {
637
- return isRecord(value) && exactKeys(value, ["fingerprint", "messageIdSuffix", "sourceAlias", "targetAlias", "direction", "firstSeenAt", "expiresAt"], ["conversationIdSuffix", "pair"]) && typeof value.fingerprint === "string" &&
638
- FINGERPRINT.test(value.fingerprint) && typeof value.messageIdSuffix === "string" &&
639
- MESSAGE_SUFFIX.test(value.messageIdSuffix) &&
640
- (value.conversationIdSuffix === undefined || (typeof value.conversationIdSuffix === "string" && CONVERSATION_SUFFIX.test(value.conversationIdSuffix))) &&
641
- typeof value.sourceAlias === "string" && ALIAS.test(value.sourceAlias) &&
642
- typeof value.targetAlias === "string" && ALIAS.test(value.targetAlias) && direction(value.direction) &&
643
- (value.pair === undefined || value.pair === true) && iso(value.firstSeenAt) && iso(value.expiresAt);
644
- }
645
- function isV2RateBucket(value) {
646
- return isRecord(value) && exactKeys(value, ["sourceAlias", "windowStartedAt", "count"]) &&
647
- typeof value.sourceAlias === "string" && ALIAS.test(value.sourceAlias) &&
648
- iso(value.windowStartedAt) && nonNegativeInteger(value.count);
649
- }
650
- function isV2ProgressWatch(value) {
651
- if (!isRecord(value))
652
- return false;
653
- const modern = exactKeys(value, [
654
- "conversationId", "ownerAlias", "workerAlias", "ownerLease", "workerLease",
655
- "lastActivityAt", "idleMs", "nudgeCount", "nextActionAt",
656
- ]);
657
- const legacy = exactKeys(value, [
658
- "conversationId", "ownerAlias", "workerAlias", "ownerLease", "workerLease",
659
- "createdAt", "updatedAt", "lastActivityAt", "idleMs", "phase", "nudgeCount",
660
- "nextActionAt", "capability", "degradedNoticeSent",
661
- ]);
662
- if (!modern && !legacy)
663
- return false;
664
- if (!conversationId(value.conversationId) ||
665
- typeof value.ownerAlias !== "string" || !ALIAS.test(value.ownerAlias) ||
666
- typeof value.workerAlias !== "string" || !ALIAS.test(value.workerAlias) ||
667
- value.ownerAlias === value.workerAlias || !token(value.ownerLease) ||
668
- !token(value.workerLease) || value.ownerLease === value.workerLease ||
669
- !iso(value.lastActivityAt) || !positiveInteger(value.idleMs) ||
670
- value.idleMs < 60_000 || value.idleMs > 86_400_000 ||
671
- ![0, 1, 2].includes(value.nudgeCount) || !iso(value.nextActionAt))
672
- return false;
673
- if (modern)
674
- return true;
675
- return iso(value.createdAt) && iso(value.updatedAt) &&
676
- (value.phase === "quiet" || value.phase === "episode") &&
677
- (value.phase === "quiet"
678
- ? value.nudgeCount === 0
679
- : value.nudgeCount === 1 || value.nudgeCount === 2) &&
680
- (value.capability === "conversation" || value.capability === "route") &&
681
- typeof value.degradedNoticeSent === "boolean";
682
- }
683
- function isV2ProgressWatchEvent(value) {
684
- if (!isRecord(value) || !positiveInteger(value.sequence) || !iso(value.timestamp) ||
685
- !conversationId(value.conversationId) || typeof value.ownerAlias !== "string" ||
686
- !ALIAS.test(value.ownerAlias) || typeof value.workerAlias !== "string" ||
687
- !ALIAS.test(value.workerAlias) || value.ownerAlias === value.workerAlias ||
688
- typeof value.kind !== "string")
689
- return false;
690
- if (exactKeys(value, ["sequence", "timestamp", "conversationId", "ownerAlias", "workerAlias", "kind", "actor"], ["reason"])) {
691
- if (value.kind === "opened")
692
- return value.actor === "owner" && value.reason === undefined;
693
- if (value.kind === "replaced") {
694
- return (value.actor === "owner" || value.actor === "unknown") && value.reason === undefined;
695
- }
696
- if (value.kind !== "settled")
697
- return false;
698
- if (value.reason === "done")
699
- return value.actor === "owner" || value.actor === "worker";
700
- if (value.reason === "untracked" || value.reason === "pair_removed")
701
- return value.actor === "operator";
702
- if (value.reason === "endpoint_retired")
703
- return value.actor === "gateway" || value.actor === "operator";
704
- return (value.reason === "idle_timeout" || value.reason === "tracking_disabled") && value.actor === "gateway";
705
- }
706
- const legacyKinds = new Set([
707
- "opened", "replaced", "activity", "nudge", "worker_reported_complete",
708
- "capability_degraded", "conversation_rebound", "done", "unresponsive",
709
- "pair_removed", "endpoint_retired", "disabled",
710
- ]);
711
- return exactKeys(value, ["sequence", "timestamp", "conversationId", "ownerAlias", "workerAlias", "kind"], ["nudgeNumber"]) && legacyKinds.has(value.kind) &&
712
- (value.kind === "nudge"
713
- ? value.nudgeNumber === 1 || value.nudgeNumber === 2
714
- : value.nudgeNumber === undefined);
715
- }
716
- function isV2EndpointRefreshEvent(value) {
717
- return isRecord(value) && exactKeys(value, ["sequence", "timestamp", "alias", "hostId", "threadId", "oldEndpointGeneration", "newEndpointGeneration"], ["reason"]) && positiveInteger(value.sequence) && iso(value.timestamp) &&
718
- typeof value.alias === "string" && ALIAS.test(value.alias) && value.alias.startsWith("codex-") &&
719
- typeof value.hostId === "string" && HOST.test(value.hostId) && value.alias.endsWith(`@${value.hostId}`) &&
720
- token(value.threadId) && token(value.oldEndpointGeneration) && token(value.newEndpointGeneration) &&
721
- (value.reason === undefined
722
- ? value.oldEndpointGeneration !== value.newEndpointGeneration
723
- : value.reason === "boot_reactivation");
724
- }
725
- function isV2OrphanEvent(value) {
726
- return isRecord(value) && exactKeys(value, ["sequence", "timestamp", "alias", "hostId"]) &&
727
- positiveInteger(value.sequence) && iso(value.timestamp) && typeof value.alias === "string" &&
728
- ALIAS.test(value.alias) && value.alias.startsWith("codex-") &&
729
- typeof value.hostId === "string" && HOST.test(value.hostId) &&
730
- value.alias.endsWith(`@${value.hostId}`);
731
- }
732
- function isV2CodexSuccession(value) {
733
- if (!isRecord(value) || !exactKeys(value, ["schemaVersion", "stage", "old", "new"], ["safeErrorCode"]) ||
734
- value.schemaVersion !== 1 || typeof value.stage !== "string" ||
735
- !new Set(["prepared", "publication_armed", "published", "activated", "recovery_forbidden"]).has(value.stage) ||
736
- !isV2CodexSuccessionIdentity(value.old) || !isV2CodexSuccessionIdentity(value.new)) {
737
- return false;
738
- }
739
- const oldIdentity = value.old;
740
- const newIdentity = value.new;
741
- if (oldIdentity.hostId !== newIdentity.hostId || oldIdentity.alias === newIdentity.alias ||
742
- oldIdentity.threadId === newIdentity.threadId || oldIdentity.generation === newIdentity.generation ||
743
- oldIdentity.binding.ownerLease === newIdentity.binding.ownerLease)
744
- return false;
745
- return value.stage === "recovery_forbidden"
746
- ? safeCode(value.safeErrorCode)
747
- : value.safeErrorCode === undefined;
748
- }
749
- function isV2CodexSuccessionIdentity(value) {
750
- return isRecord(value) && exactKeys(value, ["alias", "threadId", "hostId", "generation", "binding"]) &&
751
- typeof value.alias === "string" && ALIAS.test(value.alias) && value.alias.startsWith("codex-") &&
752
- typeof value.hostId === "string" && HOST.test(value.hostId) &&
753
- value.alias.endsWith(`@${value.hostId}`) && token(value.threadId) &&
754
- typeof value.generation === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/.test(value.generation) &&
755
- isV2Binding(value.binding) && value.binding.provider === "codex" &&
756
- value.binding.hostId === value.hostId && value.binding.routeHandle === value.threadId;
757
- }
758
- function v2ProgressWatchPairKey(watch) {
759
- return [
760
- `${String(watch.ownerAlias)}\0${String(watch.ownerLease)}`,
761
- `${String(watch.workerAlias)}\0${String(watch.workerLease)}`,
762
- ].sort().join("\0");
763
- }
764
- function sameV2Endpoint(left, right) {
765
- return left.provider === right.provider && left.hostId === right.hostId &&
766
- left.endpointGeneration === right.endpointGeneration;
767
- }
768
- function sameV2Binding(left, right) {
769
- return sameV2Endpoint(left, right) && left.routeHandle === right.routeHandle &&
770
- left.ownerLease === right.ownerLease;
771
- }
772
- function sameV2RouteTarget(left, right) {
773
- return sameV2Endpoint(left, right) && left.routeHandle === right.routeHandle;
774
- }
775
- function v2RouteMatchesSuccessionIdentity(route, identity) {
776
- const binding = identity.binding;
777
- return route.alias === identity.alias && route.registrationMode === "explicit_opt_in" &&
778
- route.binding.provider === "codex" && route.binding.hostId === identity.hostId &&
779
- route.binding.routeHandle === identity.threadId && sameV2Binding(route.binding, binding);
780
- }
781
- function isV2SuccessionConsistent(state) {
782
- if (state.codexSuccession === null)
783
- return true;
784
- const journal = state.codexSuccession;
785
- const oldIdentity = journal.old;
786
- const newIdentity = journal.new;
787
- const newBinding = newIdentity.binding;
788
- const matchingRoutes = state.routes.filter((route) => v2RouteMatchesSuccessionIdentity(route, oldIdentity) ||
789
- v2RouteMatchesSuccessionIdentity(route, newIdentity));
790
- if (matchingRoutes.length !== 1)
791
- return false;
792
- const route = matchingRoutes[0];
793
- const aliases = new Set([String(oldIdentity.alias), String(newIdentity.alias)]);
794
- if ([...state.queue, ...state.inFlight].some((item) => aliases.has(item.sourceAlias) || aliases.has(item.targetAlias)) ||
795
- route.queueDepth !== 0 ||
796
- !state.connectors.some((connector) => sameV2Endpoint(connector, newBinding)) ||
797
- state.routes.some((candidate) => candidate !== route &&
798
- (candidate.alias === newIdentity.alias ||
799
- sameV2RouteTarget(candidate.binding, newBinding) ||
800
- candidate.binding.ownerLease === newBinding.ownerLease)))
801
- return false;
802
- const oldMatches = v2RouteMatchesSuccessionIdentity(route, oldIdentity);
803
- const newMatches = v2RouteMatchesSuccessionIdentity(route, newIdentity);
804
- const zeroCounters = Object.values(route.counters).every((value) => value === 0);
805
- if (journal.stage === "activated")
806
- return newMatches && zeroCounters;
807
- if (journal.stage === "recovery_forbidden")
808
- return oldMatches || (newMatches && zeroCounters);
809
- return oldMatches;
810
- }
811
- function isCounters(value, accounting = false) {
812
- const routeKeys = [
813
- "accepted", "delivered", "unconfirmed", "failed", "ambiguous", "expired",
814
- "cancelled", "abandoned", "rejected", "bytesAccepted",
815
- ];
816
- const keys = accounting
817
- ? ["accepted", "duplicates", ...routeKeys.slice(1), "queuedBytes"]
818
- : routeKeys;
819
- return isRecord(value) && exactKeys(value, keys) &&
820
- Object.values(value).every(nonNegativeInteger);
821
- }
822
- function compareV2Endpoints(left, right) {
823
- const providerOrder = ["claude", "codex", "deepseek", "grok"];
824
- const providerDifference = providerOrder.indexOf(left.provider) - providerOrder.indexOf(right.provider);
825
- return providerDifference !== 0 ? providerDifference : left.alias.localeCompare(right.alias);
826
- }
827
- async function validateStateRoot(candidate) {
828
- if (typeof candidate !== "string" || !path.isAbsolute(candidate)) {
829
- throw conversionError("UNSAFE_GATEWAY_STATE_DIRECTORY", "The offline converter requires the configured absolute gateway state directory.");
830
- }
831
- const requested = path.resolve(candidate);
832
- const info = await lstat(requested);
833
- if (info.isSymbolicLink() || !info.isDirectory()) {
834
- throw conversionError("UNSAFE_GATEWAY_STATE_DIRECTORY", "The gateway state path must be a real directory.");
835
- }
836
- assertOwnedMode(info.uid, info.mode, 0o700, "state directory");
837
- const root = await realpath(requested);
838
- if (root !== requested) {
839
- throw conversionError("UNSAFE_GATEWAY_STATE_DIRECTORY", "The gateway state directory changed while it was being attested.");
840
- }
841
- const home = await realpath(os.homedir()).catch(() => path.resolve(os.homedir()));
842
- const temporaryRoot = await realpath(os.tmpdir()).catch(() => path.resolve(os.tmpdir()));
843
- if (root === path.parse(root).root || root === home || root === temporaryRoot) {
844
- throw conversionError("UNSAFE_GATEWAY_STATE_DIRECTORY", "The gateway state directory must be a dedicated private leaf.");
845
- }
846
- return root;
847
- }
848
- async function readPrivateFile(filePath, maximumBytes) {
849
- const before = await lstat(filePath);
850
- if (before.isSymbolicLink() || !before.isFile()) {
851
- throw conversionError("UNSAFE_GATEWAY_STATE_FILE", "A gateway controller file is not a regular file.");
852
- }
853
- assertOwnedMode(before.uid, before.mode, 0o600, "state file");
854
- if (before.size > maximumBytes) {
855
- throw conversionError("GATEWAY_STATE_FILE_TOO_LARGE", "A gateway controller file exceeds its strict byte limit.");
856
- }
857
- const handle = await open(filePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
858
- try {
859
- const opened = await handle.stat();
860
- if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino ||
861
- opened.size !== before.size || opened.size > maximumBytes) {
862
- throw conversionError("UNSAFE_GATEWAY_STATE_FILE", "A gateway controller file changed during its bounded no-follow read.");
863
- }
864
- assertOwnedMode(opened.uid, opened.mode, 0o600, "state file");
865
- const buffer = Buffer.alloc(maximumBytes + 1);
866
- let offset = 0;
867
- while (offset < buffer.byteLength) {
868
- const result = await handle.read(buffer, offset, buffer.byteLength - offset, offset);
869
- if (result.bytesRead === 0)
870
- break;
871
- offset += result.bytesRead;
872
- }
873
- if (offset > maximumBytes) {
874
- throw conversionError("GATEWAY_STATE_FILE_TOO_LARGE", "A gateway controller file exceeds its strict byte limit.");
875
- }
876
- const after = await handle.stat();
877
- if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size ||
878
- after.mtimeMs !== opened.mtimeMs) {
879
- throw conversionError("UNSAFE_GATEWAY_STATE_FILE", "A gateway controller file changed during its bounded no-follow read.");
880
- }
881
- return buffer.subarray(0, offset);
882
- }
883
- finally {
884
- await handle.close();
885
- }
886
- }
887
- async function openExclusivePrivate(filePath) {
888
- return await open(filePath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY |
889
- (constants.O_NOFOLLOW ?? 0), 0o600);
890
- }
891
- async function writeExclusivePrivateFile(filePath, body, kind, fault) {
892
- let handle;
893
- try {
894
- handle = await openExclusivePrivate(filePath);
895
- }
896
- catch (error) {
897
- if (nodeCode(error) === "EEXIST") {
898
- throw conversionError("GATEWAY_STATE_BACKUP_EXISTS", "The fixed v2 backup already exists; conversion will not overwrite recovery evidence.");
899
- }
900
- throw error;
901
- }
902
- try {
903
- await fault(`before_${kind}_write`);
904
- await handle.writeFile(body);
905
- await handle.chmod(0o600);
906
- await fault(`before_${kind}_file_sync`);
907
- await handle.sync();
908
- }
909
- finally {
910
- await handle.close();
911
- }
912
- }
913
- async function syncDirectory(root) {
914
- const directory = await open(root, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
915
- try {
916
- await directory.sync();
917
- }
918
- finally {
919
- await directory.close();
920
- }
921
- }
922
- async function acquireOfflineLock(root, tokenValue) {
923
- const lockPath = path.join(root, CONTROLLER_LOCK);
924
- let handle;
925
- try {
926
- handle = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR |
927
- (constants.O_NOFOLLOW ?? 0), 0o600);
928
- }
929
- catch (error) {
930
- if (nodeCode(error) === "EEXIST") {
931
- throw conversionError("GATEWAY_STATE_IN_USE", "The offline converter requires the broker to be stopped and the state lock to be free.");
932
- }
933
- throw error;
934
- }
935
- try {
936
- const tokenValueBody = `${JSON.stringify({
937
- schemaVersion: 1,
938
- pid: process.pid,
939
- hostname: os.hostname(),
940
- token: tokenValue,
941
- })}\n`;
942
- await handle.writeFile(tokenValueBody, "utf8");
943
- await handle.chmod(0o600);
944
- await handle.sync();
945
- await syncDirectory(root);
946
- return { handle, token: tokenValue };
947
- }
948
- catch (error) {
949
- await handle.close().catch(() => undefined);
950
- await unlink(lockPath).catch(() => undefined);
951
- throw error;
952
- }
953
- }
954
- async function releaseOfflineLock(root, lock) {
955
- await lock.handle.close().catch(() => undefined);
956
- const lockPath = path.join(root, CONTROLLER_LOCK);
957
- try {
958
- const body = await readPrivateFile(lockPath, MAX_LOCK_BYTES);
959
- const value = JSON.parse(body.toString("utf8"));
960
- if (isRecord(value) && value.token === lock.token) {
961
- await unlink(lockPath);
962
- await syncDirectory(root);
963
- }
964
- }
965
- catch {
966
- // Never remove a controller lock whose exact ownership cannot be proven.
967
- }
968
- }
969
- function assertOwnedMode(uid, mode, expected, kind) {
970
- if (typeof process.getuid === "function" && uid !== process.getuid()) {
971
- throw conversionError("UNSAFE_GATEWAY_STATE_DIRECTORY", `The gateway ${kind} is not owned by the current process user.`);
972
- }
973
- if ((mode & 0o777) !== expected) {
974
- throw conversionError("UNSAFE_GATEWAY_STATE_DIRECTORY", `The gateway ${kind} must have exact mode ${expected.toString(8)}.`);
975
- }
976
- }
977
- function sha256(body) {
978
- return createHash("sha256").update(body).digest("hex");
979
- }
980
- function nextRandomId(randomId) {
981
- const value = randomId();
982
- if (!token(value) || value.length > 128) {
983
- throw conversionError("GATEWAY_STATE_RANDOM_ID_INVALID", "The offline converter could not allocate a bounded private commit identifier.");
984
- }
985
- return value;
986
- }
987
- function conversionError(code, message) {
988
- return new BridgeError(code, message, true);
989
- }
990
- function nodeCode(error) {
991
- return error instanceof Error && "code" in error && typeof error.code === "string"
992
- ? error.code
993
- : undefined;
994
- }
995
- function isRecord(value) {
996
- return typeof value === "object" && value !== null && !Array.isArray(value);
997
- }
998
- function exactKeys(value, required, optional = []) {
999
- const keys = Object.keys(value);
1000
- return required.every((key) => Object.hasOwn(value, key)) &&
1001
- keys.every((key) => required.includes(key) || optional.includes(key));
1002
- }
1003
- function nonNegativeInteger(value) {
1004
- return Number.isSafeInteger(value) && value >= 0;
1005
- }
1006
- function positiveInteger(value) {
1007
- return Number.isSafeInteger(value) && value > 0;
1008
- }
1009
- function iso(value) {
1010
- return typeof value === "string" && value.length >= 20 && value.length <= 35 &&
1011
- Number.isFinite(Date.parse(value));
1012
- }
1013
- function provider(value) {
1014
- return typeof value === "string" && PROVIDERS.has(value);
1015
- }
1016
- function token(value) {
1017
- return typeof value === "string" && PRIVATE_TOKEN.test(value);
1018
- }
1019
- function safeCode(value) {
1020
- return typeof value === "string" && SAFE_CODE.test(value);
1021
- }
1022
- function conversationId(value) {
1023
- return typeof value === "string" && /^conv_[A-Za-z0-9_-]{16,64}$/.test(value);
1024
- }
1025
- function aliasHost(alias) {
1026
- return alias.slice(alias.lastIndexOf("@") + 1);
1027
- }
1028
- function direction(value) {
1029
- if (typeof value !== "string")
1030
- return false;
1031
- const [source, target, extra] = value.split("_to_");
1032
- return extra === undefined && provider(source) && provider(target) && source !== target;
1033
- }
1034
- //# sourceMappingURL=state-v2-to-v3.js.map