agent-embassy 1.9.5 → 3.0.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.
Files changed (150) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/CONTRIBUTING.md +19 -34
  3. package/README.md +110 -221
  4. package/SECURITY.md +55 -89
  5. package/dist/src/errors.d.ts +10 -1
  6. package/dist/src/errors.js +3 -1
  7. package/dist/src/errors.js.map +1 -1
  8. package/dist/src/gateway/claude-helper-protocol.d.ts +9 -12
  9. package/dist/src/gateway/claude-helper-protocol.js +15 -13
  10. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  11. package/dist/src/gateway/claude-helper-supervisor.d.ts +2 -6
  12. package/dist/src/gateway/claude-helper-supervisor.js +10 -9
  13. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  14. package/dist/src/gateway/claude-helper.js +18 -10
  15. package/dist/src/gateway/claude-helper.js.map +1 -1
  16. package/dist/src/gateway/claude-peer.d.ts +0 -3
  17. package/dist/src/gateway/claude-peer.js +6 -18
  18. package/dist/src/gateway/claude-peer.js.map +1 -1
  19. package/dist/src/gateway/cli.d.ts +18 -13
  20. package/dist/src/gateway/cli.js +942 -298
  21. package/dist/src/gateway/cli.js.map +1 -1
  22. package/dist/src/gateway/codex-local-transport.d.ts +1 -1
  23. package/dist/src/gateway/codex-local-transport.js +2 -2
  24. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  25. package/dist/src/gateway/codex-socket-holder.d.ts +26 -0
  26. package/dist/src/gateway/codex-socket-holder.js +76 -0
  27. package/dist/src/gateway/codex-socket-holder.js.map +1 -0
  28. package/dist/src/gateway/codex-stateless-transport.js +1 -1
  29. package/dist/src/gateway/codex-stateless-transport.js.map +1 -1
  30. package/dist/src/gateway/config.d.ts +5 -13
  31. package/dist/src/gateway/config.js +7 -19
  32. package/dist/src/gateway/config.js.map +1 -1
  33. package/dist/src/gateway/control.d.ts +55 -108
  34. package/dist/src/gateway/control.js +95 -222
  35. package/dist/src/gateway/control.js.map +1 -1
  36. package/dist/src/gateway/federation-nodes.d.ts +29 -3
  37. package/dist/src/gateway/federation-nodes.js +200 -15
  38. package/dist/src/gateway/federation-nodes.js.map +1 -1
  39. package/dist/src/gateway/peer-client.d.ts +4 -3
  40. package/dist/src/gateway/peer-client.js +22 -13
  41. package/dist/src/gateway/peer-client.js.map +1 -1
  42. package/dist/src/gateway/peer-protocol.d.ts +8 -11
  43. package/dist/src/gateway/peer-protocol.js +5 -12
  44. package/dist/src/gateway/peer-protocol.js.map +1 -1
  45. package/dist/src/gateway/provenance-envelope.d.ts +0 -1
  46. package/dist/src/gateway/provenance-envelope.js +4 -19
  47. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  48. package/dist/src/gateway/providers.d.ts +10 -7
  49. package/dist/src/gateway/providers.js +21 -28
  50. package/dist/src/gateway/providers.js.map +1 -1
  51. package/dist/src/gateway/server.d.ts +4 -14
  52. package/dist/src/gateway/server.js +33 -59
  53. package/dist/src/gateway/server.js.map +1 -1
  54. package/dist/src/gateway/service-agent.d.ts +187 -0
  55. package/dist/src/gateway/service-agent.js +758 -0
  56. package/dist/src/gateway/service-agent.js.map +1 -0
  57. package/dist/src/gateway/service.d.ts +117 -32
  58. package/dist/src/gateway/service.js +525 -576
  59. package/dist/src/gateway/service.js.map +1 -1
  60. package/dist/src/gateway/status-view.d.ts +167 -0
  61. package/dist/src/gateway/status-view.js +488 -0
  62. package/dist/src/gateway/status-view.js.map +1 -0
  63. package/dist/src/gateway/store.d.ts +103 -21
  64. package/dist/src/gateway/store.js +473 -547
  65. package/dist/src/gateway/store.js.map +1 -1
  66. package/dist/src/gateway/types.d.ts +51 -102
  67. package/dist/src/gateway/types.js +15 -52
  68. package/dist/src/gateway/types.js.map +1 -1
  69. package/docs/CONFIGURATION.md +194 -59
  70. package/docs/DELIVERY.md +11 -11
  71. package/docs/GATEWAY-ARCHITECTURE.md +286 -394
  72. package/package.json +4 -12
  73. package/skills/embassy-peer/SKILL.md +72 -92
  74. package/skills/embassy-peer/agents/openai.yaml +1 -1
  75. package/README.zh-CN.md +0 -269
  76. package/assets/live-dashboard/app.css +0 -1619
  77. package/assets/vendor/react/LICENSE +0 -21
  78. package/assets/vendor/react/react-dom.production.min.js +0 -267
  79. package/assets/vendor/react/react.production.min.js +0 -31
  80. package/dist/src/gateway/acp-client.d.ts +0 -110
  81. package/dist/src/gateway/acp-client.js +0 -407
  82. package/dist/src/gateway/acp-client.js.map +0 -1
  83. package/dist/src/gateway/acp-provider.d.ts +0 -66
  84. package/dist/src/gateway/acp-provider.js +0 -275
  85. package/dist/src/gateway/acp-provider.js.map +0 -1
  86. package/dist/src/gateway/cli-copy.d.ts +0 -8
  87. package/dist/src/gateway/cli-copy.en.d.ts +0 -17
  88. package/dist/src/gateway/cli-copy.en.js +0 -58
  89. package/dist/src/gateway/cli-copy.en.js.map +0 -1
  90. package/dist/src/gateway/cli-copy.js +0 -22
  91. package/dist/src/gateway/cli-copy.js.map +0 -1
  92. package/dist/src/gateway/cli-copy.zh-CN.d.ts +0 -17
  93. package/dist/src/gateway/cli-copy.zh-CN.js +0 -58
  94. package/dist/src/gateway/cli-copy.zh-CN.js.map +0 -1
  95. package/dist/src/gateway/codex-doctor.d.ts +0 -36
  96. package/dist/src/gateway/codex-doctor.js +0 -127
  97. package/dist/src/gateway/codex-doctor.js.map +0 -1
  98. package/dist/src/gateway/dashboard-copy.d.ts +0 -7
  99. package/dist/src/gateway/dashboard-copy.en.d.ts +0 -504
  100. package/dist/src/gateway/dashboard-copy.en.js +0 -505
  101. package/dist/src/gateway/dashboard-copy.en.js.map +0 -1
  102. package/dist/src/gateway/dashboard-copy.js +0 -514
  103. package/dist/src/gateway/dashboard-copy.js.map +0 -1
  104. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +0 -504
  105. package/dist/src/gateway/dashboard-copy.zh-CN.js +0 -505
  106. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +0 -1
  107. package/dist/src/gateway/dashboard-model.d.ts +0 -343
  108. package/dist/src/gateway/dashboard-model.js +0 -1061
  109. package/dist/src/gateway/dashboard-model.js.map +0 -1
  110. package/dist/src/gateway/dashboard.d.ts +0 -20
  111. package/dist/src/gateway/dashboard.js +0 -874
  112. package/dist/src/gateway/dashboard.js.map +0 -1
  113. package/dist/src/gateway/deepseek-detect.d.ts +0 -14
  114. package/dist/src/gateway/deepseek-detect.js +0 -41
  115. package/dist/src/gateway/deepseek-detect.js.map +0 -1
  116. package/dist/src/gateway/live-dashboard-app/app.js +0 -2385
  117. package/dist/src/gateway/live-dashboard-assets.d.ts +0 -10
  118. package/dist/src/gateway/live-dashboard-assets.js +0 -74
  119. package/dist/src/gateway/live-dashboard-assets.js.map +0 -1
  120. package/dist/src/gateway/live-dashboard-command.d.ts +0 -58
  121. package/dist/src/gateway/live-dashboard-command.js +0 -334
  122. package/dist/src/gateway/live-dashboard-command.js.map +0 -1
  123. package/dist/src/gateway/live-dashboard-http.d.ts +0 -39
  124. package/dist/src/gateway/live-dashboard-http.js +0 -383
  125. package/dist/src/gateway/live-dashboard-http.js.map +0 -1
  126. package/dist/src/gateway/live-dashboard-protocol.d.ts +0 -34
  127. package/dist/src/gateway/live-dashboard-protocol.js +0 -114
  128. package/dist/src/gateway/live-dashboard-protocol.js.map +0 -1
  129. package/dist/src/gateway/live-dashboard-server.d.ts +0 -33
  130. package/dist/src/gateway/live-dashboard-server.js +0 -144
  131. package/dist/src/gateway/live-dashboard-server.js.map +0 -1
  132. package/dist/src/gateway/live-dashboard-stream.d.ts +0 -46
  133. package/dist/src/gateway/live-dashboard-stream.js +0 -234
  134. package/dist/src/gateway/live-dashboard-stream.js.map +0 -1
  135. package/dist/src/gateway/live-dashboard.d.ts +0 -28
  136. package/dist/src/gateway/live-dashboard.js +0 -154
  137. package/dist/src/gateway/live-dashboard.js.map +0 -1
  138. package/dist/src/gateway/locale.d.ts +0 -4
  139. package/dist/src/gateway/locale.js +0 -10
  140. package/dist/src/gateway/locale.js.map +0 -1
  141. package/dist/src/gateway/progress-watch-machine.d.ts +0 -45
  142. package/dist/src/gateway/progress-watch-machine.js +0 -70
  143. package/dist/src/gateway/progress-watch-machine.js.map +0 -1
  144. package/dist/src/gateway/state-v2-to-v3.d.ts +0 -24
  145. package/dist/src/gateway/state-v2-to-v3.js +0 -1034
  146. package/dist/src/gateway/state-v2-to-v3.js.map +0 -1
  147. package/docs/CONFIGURATION.zh-CN.md +0 -93
  148. package/docs/DASHBOARD.md +0 -98
  149. package/docs/DASHBOARD.zh-CN.md +0 -49
  150. package/docs/DELIVERY.zh-CN.md +0 -55
@@ -4,10 +4,10 @@ import { chmod, lstat, mkdtemp, realpath, rename, rmdir, unlink } from "node:fs/
4
4
  import net from "node:net";
5
5
  import path from "node:path";
6
6
  import { TextDecoder } from "node:util";
7
- import { CONNECTOR_OBSERVATION_STALE_AFTER_MS, GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayProviders, gatewayPublicSnapshotLimits, isGatewayProvider, isMessageDirection, isPublicRegistryObservationSnapshot, } from "./types.js";
7
+ import { CONNECTOR_OBSERVATION_STALE_AFTER_MS, GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, isGatewayProvider, isMessageDirection, isPublicRegistryObservationSnapshot, } from "./types.js";
8
8
  import { decodePeerParams, decodePeerResult } from "./peer-protocol.js";
9
9
  import { isPeerMailboxAwaitResult } from "./peer-mailbox.js";
10
- export const GATEWAY_CONTROL_PROTOCOL_VERSION = 1;
10
+ export const GATEWAY_CONTROL_PROTOCOL_VERSION = 3;
11
11
  export const GATEWAY_CONTROL_MAX_FRAME_BYTES = 32 * 1024;
12
12
  export const GATEWAY_CONTROL_MAX_RESPONSE_BYTES = 256 * 1024;
13
13
  export const GATEWAY_CONTROL_MAX_MESSAGE_BYTES = 16 * 1024;
@@ -16,7 +16,6 @@ export const GATEWAY_CONTROL_MAX_CONNECTIONS = 32;
16
16
  const MAX_SOCKET_PATH_BYTES = 100;
17
17
  const MAX_REPLY_ADDRESS_BYTES = 256;
18
18
  const MAX_REVISION = Number.MAX_SAFE_INTEGER;
19
- const DEFAULT_HOST_ID = "this-mac";
20
19
  const ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
21
20
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
22
21
  const CONVERSATION_ID_PATTERN = /^conv_[A-Za-z0-9_-]{16,64}$/;
@@ -31,21 +30,15 @@ const MESSAGE_SUFFIX_PATTERN = /^[0-9a-f]{8}$/;
31
30
  const CONVERSATION_SUFFIX_PATTERN = /^[A-Za-z0-9_-]{8}$/;
32
31
  const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
33
32
  export const gatewayControlMethods = [
34
- "health", "register_codex", "unregister_codex", "remove_codex_registration",
35
- "select_claude", "unselect_claude", "pair", "unpair", "list_snapshot",
36
- "observe_snapshot", "delivery_status", "untrack", "send_to_claude",
37
- "send_to_codex", "reply", "refresh_dashboard", "peer_catalog", "peer_handoff",
33
+ "health", "register_codex", "unregister_codex", "list_snapshot",
34
+ "observe_snapshot", "delivery_status", "send",
35
+ "refresh_discovery", "peer_catalog", "peer_handoff",
38
36
  "register_peer", "unregister_peer", "await_peer", "peer_receipt",
39
37
  ];
40
- export function pairParamAliases(params) {
41
- return "aliases" in params ? params.aliases : [params.claudeAlias, params.codexAlias];
42
- }
43
- export function pairParamThreadAttestation(params) {
44
- if ("aliases" in params)
45
- return params.threadAttestation;
46
- return params.codexThreadId === undefined
47
- ? undefined : { alias: params.codexAlias, threadId: params.codexThreadId };
48
- }
38
+ /** Bounds on an ephemeral peer registration's lifetime, in milliseconds. */
39
+ export const GATEWAY_EPHEMERAL_PEER_TTL_DEFAULT_MS = 300_000;
40
+ export const GATEWAY_EPHEMERAL_PEER_TTL_MIN_MS = 1_000;
41
+ export const GATEWAY_EPHEMERAL_PEER_TTL_MAX_MS = 3_600_000;
49
42
  const WIRE_ERROR_MESSAGES = {
50
43
  INVALID_JSON: "The control frame is not valid JSON.", FRAME_TOO_LARGE: "The control frame exceeds the size limit.",
51
44
  INVALID_REQUEST: "The control request is invalid.", UNSUPPORTED_VERSION: "The control protocol version is unsupported.",
@@ -66,9 +59,13 @@ const TRANSPORT_MESSAGES = {
66
59
  SOCKET_PERMISSION_FAILED: "The gateway control socket permissions could not be secured.",
67
60
  SOCKET_CLEANUP_CONFLICT: "The gateway control socket path changed during cleanup.",
68
61
  CONTROL_CONNECT_FAILED: "The gateway control socket could not be reached.",
62
+ CONTROL_CONNECT_DENIED: "The gateway control socket connection was denied by local policy.",
63
+ CONTROL_SOCKET_MISSING: "The gateway control socket does not exist at the configured path.",
64
+ CONTROL_LISTENER_UNAVAILABLE: "Nothing is listening on the gateway control socket.",
69
65
  CONTROL_TIMEOUT: "The gateway control request timed out.",
70
66
  CONTROL_RESPONSE_TOO_LARGE: "The gateway control response exceeds the client limit.",
71
67
  CONTROL_INVALID_RESPONSE: "The gateway returned an invalid control response. Restart the broker, then retry.",
68
+ CONTROL_VERSION_MISMATCH: "The gateway control protocol version does not match this client.",
72
69
  CONTROL_CONNECTION_CLOSED: "The gateway closed before returning a control response.",
73
70
  CONTROL_OUTCOME_AMBIGUOUS: "The gateway may have applied the control mutation before the response was lost; do not retry automatically.",
74
71
  };
@@ -135,12 +132,6 @@ function isNonNegativeInteger(value) {
135
132
  value >= 0 &&
136
133
  value <= MAX_REVISION);
137
134
  }
138
- function isTrackIdleMinutes(value) {
139
- return (typeof value === "number" &&
140
- Number.isSafeInteger(value) &&
141
- value >= 1 &&
142
- value <= 24 * 60);
143
- }
144
135
  function isIsoTimestamp(value) {
145
136
  if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value)) {
146
137
  return false;
@@ -165,7 +156,6 @@ function isMessageText(value) {
165
156
  }
166
157
  const exact = hasExactKeys;
167
158
  const nonNegative = isNonNegativeInteger;
168
- const trackMinutes = isTrackIdleMinutes;
169
159
  const iso = isIsoTimestamp;
170
160
  const uuid = isUuid;
171
161
  const alias = isAlias;
@@ -206,9 +196,20 @@ function decodePeerHandoff(value) {
206
196
  }
207
197
  }
208
198
  function decodeRegisterPeer(value) {
209
- if (!shape(value, { alias: peerAlias }, { token: peerToken }))
199
+ // `ephemeral` belongs only to a first registration: re-presenting a token
200
+ // proves an existing route, and an existing route's lifetime is already
201
+ // fixed. `ttlMs` without `ephemeral` would name a lifetime nothing honours.
202
+ if (!shape(value, { alias: peerAlias }, { token: peerToken, ephemeral: oneOf(true),
203
+ ttlMs: (item) => Number.isSafeInteger(item) &&
204
+ Number(item) >= GATEWAY_EPHEMERAL_PEER_TTL_MIN_MS &&
205
+ Number(item) <= GATEWAY_EPHEMERAL_PEER_TTL_MAX_MS }) ||
206
+ (value.ephemeral !== undefined && value.token !== undefined) ||
207
+ (value.ttlMs !== undefined && value.ephemeral === undefined))
210
208
  invalid();
211
- return { alias: value.alias, ...(value.token === undefined ? {} : { token: value.token }) };
209
+ return { alias: value.alias,
210
+ ...(value.token === undefined ? {} : { token: value.token }),
211
+ ...(value.ephemeral === undefined ? {} : { ephemeral: true }),
212
+ ...(value.ttlMs === undefined ? {} : { ttlMs: value.ttlMs }) };
212
213
  }
213
214
  function decodePeerPrincipal(value) {
214
215
  if (!shape(value, { alias: peerAlias, token: peerToken }))
@@ -240,142 +241,52 @@ const isPeerHandoffResult = (value) => {
240
241
  }
241
242
  };
242
243
  function decodeRegister(value) {
243
- if (!isRecord(value) || !exact(value, ["alias", "threadId"], ["hostId", "busyPolicy", "succeedsAlias"]) ||
244
+ if (!isRecord(value) || !exact(value, ["alias", "threadId", "hostId", "busyPolicy"], ["succeedsAlias"]) ||
244
245
  !alias(value.alias) || !value.alias.startsWith("codex-") || !uuid(value.threadId) ||
245
- (value.hostId !== undefined && !host(value.hostId)) ||
246
- (value.busyPolicy !== undefined && value.busyPolicy !== "queue") ||
246
+ !host(value.hostId) || value.busyPolicy !== "queue" ||
247
247
  (value.succeedsAlias !== undefined && (!alias(value.succeedsAlias) || !value.succeedsAlias.startsWith("codex-"))))
248
248
  invalid();
249
- const hostId = value.hostId ?? DEFAULT_HOST_ID;
250
- if (!value.alias.endsWith(`@${hostId}`) ||
249
+ if (!value.alias.endsWith(`@${value.hostId}`) ||
251
250
  (value.succeedsAlias !== undefined &&
252
- (value.succeedsAlias === value.alias || !value.succeedsAlias.endsWith(`@${hostId}`))))
251
+ (value.succeedsAlias === value.alias || !value.succeedsAlias.endsWith(`@${value.hostId}`))))
253
252
  invalid();
254
- return { alias: value.alias, threadId: value.threadId.toLowerCase(), hostId,
255
- busyPolicy: "queue", ...(value.succeedsAlias === undefined ? {} : { succeedsAlias: value.succeedsAlias }) };
253
+ return { alias: value.alias, threadId: value.threadId.toLowerCase(), hostId: value.hostId,
254
+ busyPolicy: value.busyPolicy, ...(value.succeedsAlias === undefined ? {} : { succeedsAlias: value.succeedsAlias }) };
256
255
  }
257
256
  function decodeUnregister(value) {
258
257
  if (!shape(value, { alias, threadId: uuid }) || !value.alias.startsWith("codex-"))
259
258
  invalid();
260
259
  return { alias: value.alias, threadId: value.threadId.toLowerCase() };
261
260
  }
262
- function decodeRemoval(value) {
263
- if (!shape(value, { alias }) || !value.alias.startsWith("codex-"))
264
- invalid();
265
- return { alias: value.alias };
266
- }
267
- function decodeSelection(value) {
268
- if (!isRecord(value) || !exact(value, ["alias"], ["codexThreadId"]) ||
269
- typeof value.alias !== "string" || !isClaudeSessionSelector(value.alias) ||
270
- (value.codexThreadId !== undefined && !uuid(value.codexThreadId)))
271
- invalid();
272
- return { alias: UUID_PATTERN.test(value.alias) ? value.alias.toLowerCase() : value.alias,
273
- ...(value.codexThreadId === undefined ? {} : { codexThreadId: value.codexThreadId.toLowerCase() }) };
274
- }
275
- function decodePair(value) {
276
- if (!isRecord(value))
277
- invalid();
278
- if (Object.hasOwn(value, "aliases")) {
279
- if (!exact(value, ["aliases"], ["threadAttestation"]) || !Array.isArray(value.aliases) ||
280
- value.aliases.length !== 2 || !alias(value.aliases[0]) || !alias(value.aliases[1]) ||
281
- value.aliases[0] === value.aliases[1])
282
- invalid();
283
- let threadAttestation;
284
- if (value.threadAttestation !== undefined) {
285
- if (!shape(value.threadAttestation, { alias, threadId: uuid }) ||
286
- !value.aliases.includes(value.threadAttestation.alias))
287
- invalid();
288
- threadAttestation = { alias: value.threadAttestation.alias,
289
- threadId: value.threadAttestation.threadId.toLowerCase() };
290
- }
291
- return { aliases: [value.aliases[0], value.aliases[1]],
292
- ...(threadAttestation === undefined ? {} : { threadAttestation }) };
293
- }
294
- if (!exact(value, ["claudeAlias", "codexAlias"], ["codexThreadId"]) ||
295
- typeof value.claudeAlias !== "string" || !isClaudeSessionSelector(value.claudeAlias) ||
296
- !alias(value.codexAlias) || !value.codexAlias.startsWith("codex-") ||
297
- (value.codexThreadId !== undefined && !uuid(value.codexThreadId)) ||
298
- value.claudeAlias === value.codexAlias ||
299
- (alias(value.claudeAlias) && value.claudeAlias.slice(value.claudeAlias.lastIndexOf("@") + 1) !==
300
- value.codexAlias.slice(value.codexAlias.lastIndexOf("@") + 1)))
301
- invalid();
302
- return { claudeAlias: UUID_PATTERN.test(value.claudeAlias) ? value.claudeAlias.toLowerCase() : value.claudeAlias,
303
- codexAlias: value.codexAlias,
304
- ...(value.codexThreadId === undefined ? {} : { codexThreadId: value.codexThreadId.toLowerCase() }) };
305
- }
306
- function commonSend(value, required, optional) {
307
- if (!isRecord(value) || !exact(value, required, optional) || !alias(value.fromAlias) ||
308
- !alias(value.toAlias) || !messageText(value.text) ||
309
- (value.expectsReply !== undefined && typeof value.expectsReply !== "boolean") ||
310
- (value.trackIdleMinutes !== undefined && !trackMinutes(value.trackIdleMinutes)))
311
- invalid();
312
- return value;
313
- }
314
- function decodeSendClaude(value) {
315
- if (!isRecord(value) || !exact(value, ["fromAlias", "toAlias", "text"], ["threadId", "peerToken", "expectsReply", "trackIdleMinutes"]) ||
316
- !alias(value.fromAlias) || (value.threadId === undefined) === (value.peerToken === undefined) ||
261
+ function decodeSend(value) {
262
+ if (!isRecord(value) || !exact(value, ["fromAlias", "text"], ["toAlias", "conversationId", "threadId", "replyAddress", "peerToken", "expectsReply"]) ||
263
+ !alias(value.fromAlias) || !messageText(value.text) ||
264
+ [value.toAlias, value.conversationId].filter((item) => item !== undefined).length !== 1 ||
265
+ (value.toAlias !== undefined && (typeof value.toAlias !== "string" || !isClaudeSessionSelector(value.toAlias))) ||
266
+ (value.conversationId !== undefined && (typeof value.conversationId !== "string" || !isGatewayConversationId(value.conversationId))) ||
267
+ [value.threadId, value.replyAddress, value.peerToken].filter((item) => item !== undefined).length !== 1 ||
317
268
  (value.threadId !== undefined && !uuid(value.threadId)) ||
318
- (value.peerToken !== undefined && !peerToken(value.peerToken)) || typeof value.toAlias !== "string" ||
319
- !isClaudeSessionSelector(value.toAlias) || !messageText(value.text) ||
320
- (value.expectsReply !== undefined && typeof value.expectsReply !== "boolean") ||
321
- (value.trackIdleMinutes !== undefined && !trackMinutes(value.trackIdleMinutes)))
322
- invalid();
323
- return { fromAlias: value.fromAlias,
324
- toAlias: UUID_PATTERN.test(value.toAlias) ? value.toAlias.toLowerCase() : value.toAlias,
325
- text: value.text, expectsReply: value.expectsReply ?? false,
326
- ...(typeof value.threadId === "string" ? { threadId: value.threadId.toLowerCase() } : { peerToken: value.peerToken }),
327
- ...(value.trackIdleMinutes === undefined ? {} : { trackIdleMinutes: value.trackIdleMinutes }) };
328
- }
329
- function decodeSendCodex(value) {
330
- const row = commonSend(value, ["fromAlias", "toAlias", "text"], ["replyAddress", "peerToken", "expectsReply", "trackIdleMinutes"]);
331
- if ((row.replyAddress === undefined) === (row.peerToken === undefined) ||
332
- (row.replyAddress !== undefined && (typeof row.replyAddress !== "string" || !isGatewayReplyAddress(row.replyAddress))) ||
333
- (row.peerToken !== undefined && !peerToken(row.peerToken)))
334
- invalid();
335
- const replyAddress = row.replyAddress !== undefined;
336
- return { fromAlias: row.fromAlias, toAlias: row.toAlias, text: row.text,
337
- ...(replyAddress ? { replyAddress: row.replyAddress } : { peerToken: row.peerToken }),
338
- expectsReply: row.expectsReply === true,
339
- ...(row.trackIdleMinutes === undefined ? {} : { trackIdleMinutes: row.trackIdleMinutes }) };
340
- }
341
- function normalizeReplyCaller(value) {
342
- if (!isRecord(value))
343
- invalid();
344
- if (value.kind === "codex" && shape(value, { kind: oneOf("codex"), alias, threadId: uuid }))
345
- return { kind: "codex", alias: value.alias,
346
- threadId: value.threadId.toLowerCase() };
347
- if (value.kind === "claude" && isRecord(value) && exact(value, ["kind", "alias"], ["replyAddress"]) &&
348
- alias(value.alias) && (value.replyAddress === undefined ||
349
- (typeof value.replyAddress === "string" && isGatewayReplyAddress(value.replyAddress))))
350
- return { kind: "claude", alias: value.alias,
351
- ...(value.replyAddress === undefined ? {} : { replyAddress: value.replyAddress }) };
352
- if (value.kind === "peer" && shape(value, { kind: oneOf("peer"), alias: peerAlias, token: peerToken }))
353
- return { kind: "peer", alias: value.alias, token: value.token };
354
- return invalid();
355
- }
356
- function decodeReply(value) {
357
- if (!isRecord(value) || !exact(value, ["conversationId", "text", "caller"], ["trackIdleMinutes"]) ||
358
- typeof value.conversationId !== "string" || !isGatewayConversationId(value.conversationId) ||
359
- !messageText(value.text) ||
360
- (value.trackIdleMinutes !== undefined && !trackMinutes(value.trackIdleMinutes)))
269
+ (value.replyAddress !== undefined && (typeof value.replyAddress !== "string" || !isGatewayReplyAddress(value.replyAddress))) ||
270
+ (value.peerToken !== undefined && !peerToken(value.peerToken)) ||
271
+ (value.expectsReply !== undefined &&
272
+ (typeof value.expectsReply !== "boolean" || value.conversationId !== undefined)))
361
273
  invalid();
362
- return { conversationId: value.conversationId, text: value.text, caller: normalizeReplyCaller(value.caller),
363
- ...(value.trackIdleMinutes === undefined ? {} : { trackIdleMinutes: value.trackIdleMinutes }) };
274
+ const authority = value.threadId !== undefined ? { threadId: value.threadId.toLowerCase() }
275
+ : value.replyAddress !== undefined ? { replyAddress: value.replyAddress }
276
+ : { peerToken: value.peerToken };
277
+ const target = value.conversationId !== undefined
278
+ ? { conversationId: value.conversationId }
279
+ : { toAlias: UUID_PATTERN.test(value.toAlias) ? value.toAlias.toLowerCase() : value.toAlias,
280
+ expectsReply: value.expectsReply === true };
281
+ return { fromAlias: value.fromAlias, text: value.text, ...target, ...authority };
364
282
  }
365
283
  function decodeDeliveryStatus(value) {
366
284
  if (!shape(value, { token: (item) => typeof item === "string" && DELIVERY_TOKEN_PATTERN.test(item) }))
367
285
  invalid();
368
286
  return { token: value.token };
369
287
  }
370
- function decodeUntrack(value) {
371
- if (!shape(value, { conversationId: (item) => typeof item === "string" && CONVERSATION_ID_PATTERN.test(item) }))
372
- invalid();
373
- return { conversationId: value.conversationId };
374
- }
375
288
  function isDecision(value) {
376
- return shape(value, { accepted: (item) => typeof item === "boolean", code: oneOf("ok", "not_found", "conflict", "watch_owner_conflict", "route_mismatch", "busy", "unavailable", "rejected") }, { ownerHost: host }) && (value.accepted === true
377
- ? value.code === "ok" && value.ownerHost === undefined
378
- : value.code !== "ok" && (value.ownerHost === undefined || value.code === "conflict"));
289
+ return shape(value, { accepted: (item) => typeof item === "boolean", code: oneOf("ok", "not_found", "conflict", "route_mismatch", "busy", "unavailable", "rejected") }) && (value.accepted === true ? value.code === "ok" : value.code !== "ok");
379
290
  }
380
291
  function isRegisterPeerResult(value) {
381
292
  return isDecision(value) || shape(value, { accepted: oneOf(true), code: oneOf("ok"), token: peerToken });
@@ -384,14 +295,15 @@ function isHealthResult(value) {
384
295
  return shape(value, { status: oneOf("ok", "degraded"), revision: nonNegative });
385
296
  }
386
297
  function isSendResult(value) {
387
- if (!isRecord(value) || value.accepted !== true)
388
- return isDecision(value);
298
+ if (!isRecord(value) || value.accepted !== true) {
299
+ return shape(value, { accepted: oneOf(false), code: oneOf("not_found", "conflict", "route_mismatch", "busy", "unavailable", "rejected") }, { reason: safeCode });
300
+ }
389
301
  return shape(value, { accepted: oneOf(true), code: oneOf("ok"),
390
302
  conversationId: (item) => typeof item === "string" && CONVERSATION_ID_PATTERN.test(item),
391
303
  deliveryToken: (item) => typeof item === "string" && DELIVERY_TOKEN_PATTERN.test(item) });
392
304
  }
393
305
  function isRefreshResult(value) {
394
- return shape(value, { accepted: (item) => typeof item === "boolean", code: oneOf("ok", "not_found", "conflict", "watch_owner_conflict", "route_mismatch", "busy", "unavailable", "rejected"), revision: nonNegative }) && isDecision({ accepted: value.accepted, code: value.code });
306
+ return shape(value, { accepted: (item) => typeof item === "boolean", code: oneOf("ok", "not_found", "conflict", "route_mismatch", "busy", "unavailable", "rejected"), revision: nonNegative }) && isDecision({ accepted: value.accepted, code: value.code });
395
307
  }
396
308
  const TERMINAL = new Set(["delivered", "unconfirmed", "expired", "failed", "ambiguous", "cancelled"]);
397
309
  function isDeliveryStatusResult(value) {
@@ -411,13 +323,9 @@ function isConnectorSnapshot(value) {
411
323
  if (!shape(value, { provider: isGatewayProvider, host, health: isConnectorHealth,
412
324
  protocol: (item) => typeof item === "string" && PROTOCOL_PATTERN.test(item),
413
325
  protocolVersion: (item) => typeof item === "string" && PROTOCOL_VERSION_PATTERN.test(item) }, { lastSeenAt: iso, observationAgeMs: nonNegative, safeErrorCode: safeCode,
414
- registry: isPublicRegistryObservationSnapshot, codexDoctor: (item) => shape(item, {
415
- conditions: (rows) => Array.isArray(rows) && rows.length > 0 && rows.length <= 2 &&
416
- rows.every(oneOf("split_brain", "orphaned", "attached", "observation_stale", "managed_layout_missing", "unknown")),
417
- }) }))
326
+ registry: isPublicRegistryObservationSnapshot }))
418
327
  return false;
419
- return (value.codexDoctor === undefined || value.provider === "codex") &&
420
- (value.registry === undefined || value.provider === "claude");
328
+ return value.registry === undefined || value.provider === "claude";
421
329
  }
422
330
  function isRouteSnapshot(value) {
423
331
  if (!shape(value, { alias, provider: isGatewayProvider, host, enabled: (item) => typeof item === "boolean",
@@ -428,24 +336,10 @@ function isRouteSnapshot(value) {
428
336
  return row.alias.endsWith(`@${row.host}`) &&
429
337
  (row.oldestQueuedAt === undefined ? row.queueDepth === 0 : row.queueDepth > 0);
430
338
  }
431
- function compareConsentEndpoints(left, right) {
432
- return gatewayProviders.indexOf(left.provider) -
433
- gatewayProviders.indexOf(right.provider) || left.alias.localeCompare(right.alias);
434
- }
435
- function isConsentEdgeSnapshot(value) {
436
- const endpoint = (item) => shape(item, { alias, provider: isGatewayProvider });
437
- if (!shape(value, { endpoints: (rows) => Array.isArray(rows) && rows.length === 2 && rows.every(endpoint),
438
- host, counters: isRouteCounters }, { mutable: (item) => typeof item === "boolean" }))
439
- return false;
440
- const [left, right] = value.endpoints;
441
- const hosts = [left.alias.slice(left.alias.lastIndexOf("@") + 1), right.alias.slice(right.alias.lastIndexOf("@") + 1)];
442
- return left.alias !== right.alias && compareConsentEndpoints(left, right) < 0 &&
443
- (left.provider !== right.provider || hosts[0] !== hosts[1]) && value.host === [...hosts].sort()[0];
444
- }
445
339
  function isAvailablePeerSnapshot(value) {
446
340
  return shape(value, { alias, provider: oneOf("claude"), host,
447
341
  state: oneOf("idle", "busy", "awaiting_approval", "offline"),
448
- validated: (item) => typeof item === "boolean", selected: (item) => typeof item === "boolean" }, { lastSeenAt: iso, safeErrorCode: safeCode }) &&
342
+ validated: (item) => typeof item === "boolean", routed: (item) => typeof item === "boolean" }, { lastSeenAt: iso, safeErrorCode: safeCode }) &&
449
343
  value.alias.endsWith(`@${String(value.host)}`);
450
344
  }
451
345
  function isNormalizedMessageEvent(value) {
@@ -467,40 +361,17 @@ function isAccounting(value) {
467
361
  function isSafeAlert(value) {
468
362
  return shape(value, { code: safeCode, severity: oneOf("info", "warning", "error"), timestamp: iso }, { provider: isGatewayProvider, host, alias });
469
363
  }
470
- function isProgressWatchSnapshot(value) {
471
- return shape(value, {
472
- conversationIdSuffix: (item) => typeof item === "string" && CONVERSATION_SUFFIX_PATTERN.test(item),
473
- ownerAlias: alias, workerAlias: alias, lastActivityAt: iso, nextActionAt: iso,
474
- nudgeCount: oneOf(0, 1, 2),
475
- }) && value.ownerAlias !== value.workerAlias;
476
- }
477
- function isProgressWatchEventSnapshot(value) {
478
- if (!shape(value, { sequence: nonNegative, timestamp: iso,
479
- conversationIdSuffix: (item) => typeof item === "string" && CONVERSATION_SUFFIX_PATTERN.test(item),
480
- ownerAlias: alias, workerAlias: alias, kind: oneOf("opened", "replaced", "settled"),
481
- actor: oneOf("owner", "worker", "operator", "gateway", "unknown") }, { reason: oneOf("done", "untracked", "idle_timeout", "tracking_disabled", "endpoint_retired", "pair_removed") }))
482
- return false;
483
- if (value.kind === "opened")
484
- return value.actor === "owner" && value.reason === undefined;
485
- if (value.kind === "replaced")
486
- return (value.actor === "owner" || value.actor === "unknown") && value.reason === undefined;
487
- return (value.reason === "done" && (value.actor === "owner" || value.actor === "worker")) ||
488
- (value.reason === "untracked" && value.actor === "operator") ||
489
- ((value.reason === "idle_timeout" || value.reason === "tracking_disabled") && value.actor === "gateway") ||
490
- (value.reason === "endpoint_retired" && (value.actor === "gateway" || value.actor === "operator")) ||
491
- (value.reason === "pair_removed" && value.actor === "operator");
492
- }
493
364
  function isGatewayActivityEvent(value) {
494
365
  if (!shape(value, { sequence: positive, timestamp: iso,
495
- kind: oneOf("discovery", "selection", "registration", "pairing", "watch"),
496
- action: oneOf("discovery_refreshed", "claude_selected", "claude_unselected", "codex_registered", "codex_succeeded", "codex_unregistered", "routes_paired", "routes_unpaired", "watch_ended"),
366
+ kind: oneOf("discovery", "registration"),
367
+ action: oneOf("discovery_refreshed", "codex_registered", "codex_succeeded", "codex_unregistered", "claude_route_installed", "claude_route_retired"),
497
368
  outcome: oneOf("accepted", "rejected"), aliases: (rows) => arrayOf(rows, 2, alias),
498
369
  operatorAction: oneOf(true) }, { safeErrorCode: safeCode }))
499
370
  return false;
500
371
  const allowed = {
501
- discovery: ["discovery_refreshed"], selection: ["claude_selected", "claude_unselected"],
502
- registration: ["codex_registered", "codex_succeeded", "codex_unregistered"],
503
- pairing: ["routes_paired", "routes_unpaired"], watch: ["watch_ended"],
372
+ discovery: ["discovery_refreshed"],
373
+ registration: ["codex_registered", "codex_succeeded", "codex_unregistered",
374
+ "claude_route_installed", "claude_route_retired"],
504
375
  };
505
376
  return new Set(value.aliases).size === value.aliases.length &&
506
377
  (allowed[value.kind]?.includes(value.action) ?? false);
@@ -518,23 +389,20 @@ function isDeadlinePressure(value) {
518
389
  rows.reduce((sum, row) => sum + Number(row.expired), 0) === value.expiredEvents;
519
390
  }
520
391
  function isSnapshotTruncation(value) {
521
- const required = ["connectors", "availablePeers", "routes", "consentEdges", "messages", "alerts"];
522
- const optional = ["progressWatches", "progressWatchEvents", "activityEvents"];
392
+ const required = ["connectors", "availablePeers", "routes", "messages", "alerts"];
393
+ const optional = ["activityEvents"];
523
394
  return isRecord(value) && exact(value, required, optional) &&
524
395
  [...required, ...optional].every((key) => value[key] === undefined || nonNegative(value[key]));
525
396
  }
526
397
  export function isGatewaySnapshot(value) {
527
- if (!shape(value, { schemaVersion: oneOf(2), generatedAt: iso, inboundMode: oneOf("paired", "open"),
398
+ if (!shape(value, { schemaVersion: oneOf(2), generatedAt: iso,
528
399
  health: isConnectorHealth,
529
400
  connectors: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.connectors, isConnectorSnapshot),
530
401
  availablePeers: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.availablePeers, isAvailablePeerSnapshot),
531
402
  routes: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.routes, isRouteSnapshot),
532
- consentEdges: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.consentEdges, isConsentEdgeSnapshot),
533
403
  messages: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.messages, isNormalizedMessageEvent),
534
404
  accounting: isAccounting, alerts: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.alerts, isSafeAlert),
535
- truncation: isSnapshotTruncation }, { progressWatches: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.progressWatches, isProgressWatchSnapshot),
536
- progressWatchEvents: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.progressWatchEvents, isProgressWatchEventSnapshot),
537
- activityEvents: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.activityEvents, isGatewayActivityEvent),
405
+ truncation: isSnapshotTruncation }, { activityEvents: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.activityEvents, isGatewayActivityEvent),
538
406
  deadlinePressure: isDeadlinePressure }))
539
407
  return false;
540
408
  const snapshot = value;
@@ -543,16 +411,13 @@ export function isGatewaySnapshot(value) {
543
411
  const connectorKeys = snapshot.connectors.map((row) => `${row.provider}@${row.host}`);
544
412
  const aliases = snapshot.routes.map((row) => row.alias);
545
413
  const peers = snapshot.availablePeers.map((row) => row.alias);
546
- const edgeKeys = snapshot.consentEdges.map((row) => `${row.endpoints[0].alias}\0${row.endpoints[1].alias}`);
547
- const routeByAlias = new Map(snapshot.routes.map((row) => [row.alias, row]));
548
414
  const honest = snapshot.connectors.every((row) => {
549
415
  const seen = row.lastSeenAt === undefined ? undefined : Date.parse(row.lastSeenAt);
550
416
  const age = seen === undefined ? undefined : Math.min(MAX_REVISION, Math.max(0, generated - seen));
551
417
  return (row.observationAgeMs === undefined || row.observationAgeMs === age) &&
552
418
  (row.health !== "healthy" || (age !== undefined && age <= CONNECTOR_OBSERVATION_STALE_AFTER_MS));
553
419
  });
554
- return honest && unique(connectorKeys) && unique(aliases) && unique(peers) && unique(edgeKeys) &&
555
- snapshot.consentEdges.every((edge) => edge.endpoints.every((endpoint) => routeByAlias.get(endpoint.alias)?.provider === endpoint.provider)) &&
420
+ return honest && unique(connectorKeys) && unique(aliases) && unique(peers) &&
556
421
  Buffer.byteLength(JSON.stringify(snapshot), "utf8") <= GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET;
557
422
  }
558
423
  function isSnapshotObservation(value) {
@@ -560,13 +425,11 @@ function isSnapshotObservation(value) {
560
425
  }
561
426
  const descriptors = {
562
427
  health: { handler: "health", decode: emptyParams, result: isHealthResult, mutation: false }, register_codex: { handler: "registerCodex", decode: decodeRegister, result: isDecision, mutation: true },
563
- unregister_codex: { handler: "unregisterCodex", decode: decodeUnregister, result: isDecision, mutation: true }, remove_codex_registration: { handler: "removeCodexRegistration", decode: decodeRemoval, result: isDecision, mutation: true },
564
- select_claude: { handler: "selectClaude", decode: decodeSelection, result: isDecision, mutation: true }, unselect_claude: { handler: "unselectClaude", decode: decodeSelection, result: isDecision, mutation: true },
565
- pair: { handler: "pair", decode: decodePair, result: isDecision, mutation: true }, unpair: { handler: "unpair", decode: decodePair, result: isDecision, mutation: true },
428
+ unregister_codex: { handler: "unregisterCodex", decode: decodeUnregister, result: isDecision, mutation: true },
566
429
  list_snapshot: { handler: "listSnapshot", decode: emptyParams, result: isGatewaySnapshot, mutation: false }, observe_snapshot: { handler: "observeSnapshot", decode: emptyParams, result: isSnapshotObservation, mutation: false },
567
- delivery_status: { handler: "deliveryStatus", decode: decodeDeliveryStatus, result: isDeliveryStatusResult, mutation: false }, untrack: { handler: "untrack", decode: decodeUntrack, result: isDecision, mutation: false },
568
- send_to_claude: { handler: "sendToClaude", decode: decodeSendClaude, result: isSendResult, mutation: true }, send_to_codex: { handler: "sendToCodex", decode: decodeSendCodex, result: isSendResult, mutation: true },
569
- reply: { handler: "reply", decode: decodeReply, result: isSendResult, mutation: true }, refresh_dashboard: { handler: "refreshDashboard", decode: emptyParams, result: isRefreshResult, mutation: false },
430
+ delivery_status: { handler: "deliveryStatus", decode: decodeDeliveryStatus, result: isDeliveryStatusResult, mutation: false },
431
+ send: { handler: "send", decode: decodeSend, result: isSendResult, mutation: true },
432
+ refresh_discovery: { handler: "refreshDiscovery", decode: emptyParams, result: isRefreshResult, mutation: false },
570
433
  peer_catalog: { handler: "peerCatalog", decode: decodePeerCatalog, result: isPeerCatalog, mutation: false },
571
434
  peer_handoff: { handler: "peerHandoff", decode: decodePeerHandoff, result: isPeerHandoffResult, mutation: true },
572
435
  register_peer: { handler: "registerPeer", decode: decodeRegisterPeer, result: isRegisterPeerResult, mutation: true },
@@ -577,12 +440,12 @@ const descriptors = {
577
440
  function parseRequestObject(value) {
578
441
  if (!isRecord(value) || !exact(value, ["protocolVersion", "method", "params"]))
579
442
  invalid();
580
- if (value.protocolVersion !== 1)
443
+ if (value.protocolVersion !== GATEWAY_CONTROL_PROTOCOL_VERSION)
581
444
  throw new ProtocolFault("UNSUPPORTED_VERSION");
582
445
  if (typeof value.method !== "string" || !Object.hasOwn(descriptors, value.method))
583
446
  throw new ProtocolFault("UNKNOWN_METHOD");
584
447
  const method = value.method;
585
- return { protocolVersion: 1, method, params: descriptors[method].decode(value.params) };
448
+ return { protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION, method, params: descriptors[method].decode(value.params) };
586
449
  }
587
450
  function decodeJson(frame, invalidCode) {
588
451
  try {
@@ -593,14 +456,14 @@ function decodeJson(frame, invalidCode) {
593
456
  }
594
457
  }
595
458
  function errorResponse(code) {
596
- return { protocolVersion: 1, ok: false, error: { code, message: WIRE_ERROR_MESSAGES[code] } };
459
+ return { protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION, ok: false, error: { code, message: WIRE_ERROR_MESSAGES[code] } };
597
460
  }
598
461
  async function dispatch(request, handlers) {
599
462
  const descriptor = descriptors[request.method];
600
463
  const handler = handlers[descriptor.handler];
601
464
  try {
602
465
  const result = await (Object.keys(request.params).length === 0 ? handler() : handler(request.params));
603
- return { protocolVersion: 1, ok: true, result };
466
+ return { protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION, ok: true, result };
604
467
  }
605
468
  catch {
606
469
  return errorResponse("HANDLER_FAILURE");
@@ -617,9 +480,11 @@ function serializeResponse(response) {
617
480
  }
618
481
  class FrameFault extends Error {
619
482
  kind;
620
- constructor(kind) {
483
+ systemCode;
484
+ constructor(kind, systemCode) {
621
485
  super(kind);
622
486
  this.kind = kind;
487
+ this.systemCode = systemCode;
623
488
  }
624
489
  }
625
490
  function readOneFrame(socket, maximum) {
@@ -652,7 +517,7 @@ function readOneFrame(socket, maximum) {
652
517
  socket.on("data", onData);
653
518
  socket.once("end", onEnd);
654
519
  socket.once("timeout", onTimeout);
655
- socket.once("error", () => finish(new FrameFault("error")));
520
+ socket.once("error", (error) => finish(new FrameFault("error", error.code)));
656
521
  });
657
522
  }
658
523
  function respond(socket, response) {
@@ -961,16 +826,18 @@ export async function startGatewayControlServer(options) {
961
826
  };
962
827
  }
963
828
  function decodeResponse(method, params, value) {
964
- if (!isRecord(value) || value.protocolVersion !== 1)
829
+ if (!isRecord(value) || !Number.isSafeInteger(value.protocolVersion))
965
830
  throw controlTransportError("CONTROL_INVALID_RESPONSE");
831
+ if (value.protocolVersion !== GATEWAY_CONTROL_PROTOCOL_VERSION)
832
+ throw controlTransportError("CONTROL_VERSION_MISMATCH");
966
833
  if (value.ok === false) {
967
- if (!shape(value, { protocolVersion: oneOf(1), ok: oneOf(false), error: (item) => shape(item, { code: (code) => typeof code === "string" && Object.hasOwn(WIRE_ERROR_MESSAGES, code),
834
+ if (!shape(value, { protocolVersion: oneOf(GATEWAY_CONTROL_PROTOCOL_VERSION), ok: oneOf(false), error: (item) => shape(item, { code: (code) => typeof code === "string" && Object.hasOwn(WIRE_ERROR_MESSAGES, code),
968
835
  message: (message) => typeof message === "string" }) }) ||
969
836
  !isRecord(value.error) || value.error.message !== WIRE_ERROR_MESSAGES[value.error.code])
970
837
  throw controlTransportError("CONTROL_INVALID_RESPONSE");
971
838
  return value;
972
839
  }
973
- if (!shape(value, { protocolVersion: oneOf(1), ok: oneOf(true), result: descriptors[method].result }))
840
+ if (!shape(value, { protocolVersion: oneOf(GATEWAY_CONTROL_PROTOCOL_VERSION), ok: oneOf(true), result: descriptors[method].result }))
974
841
  throw controlTransportError("CONTROL_INVALID_RESPONSE");
975
842
  if (method === "register_peer" && isRecord(value.result) && value.result.accepted === true &&
976
843
  Object.hasOwn(params, "token") === Object.hasOwn(value.result, "token"))
@@ -1015,14 +882,20 @@ export async function sendGatewayControlRequest(options) {
1015
882
  resolve(response);
1016
883
  }
1017
884
  }
1018
- catch {
1019
- fail(controlTransportError("CONTROL_INVALID_RESPONSE"));
885
+ catch (error) {
886
+ fail(error instanceof GatewayControlTransportError
887
+ ? error : controlTransportError("CONTROL_INVALID_RESPONSE"));
1020
888
  }
1021
889
  }, (error) => {
1022
890
  const kind = error instanceof FrameFault ? error.kind : "error";
891
+ const systemCode = !writeStarted && error instanceof FrameFault ? error.systemCode : undefined;
1023
892
  const code = kind === "timeout" ? "CONTROL_TIMEOUT" :
1024
893
  kind === "too_large" ? "CONTROL_RESPONSE_TOO_LARGE" :
1025
- kind === "closed" ? "CONTROL_CONNECTION_CLOSED" : "CONTROL_CONNECT_FAILED";
894
+ kind === "closed" ? "CONTROL_CONNECTION_CLOSED" :
895
+ systemCode === "EPERM" || systemCode === "EACCES"
896
+ ? "CONTROL_CONNECT_DENIED" : systemCode === "ENOENT"
897
+ ? "CONTROL_SOCKET_MISSING" : systemCode === "ECONNREFUSED"
898
+ ? "CONTROL_LISTENER_UNAVAILABLE" : "CONTROL_CONNECT_FAILED";
1026
899
  fail(controlTransportError(code));
1027
900
  });
1028
901
  });