agent-embassy 1.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 (145) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/CONTRIBUTING.md +122 -0
  3. package/LICENSE +21 -0
  4. package/README.md +185 -0
  5. package/README.zh-CN.md +185 -0
  6. package/SECURITY.md +224 -0
  7. package/assets/live-dashboard/app.css +1610 -0
  8. package/assets/mark-seal.svg +7 -0
  9. package/assets/mark.svg +7 -0
  10. package/assets/social-preview.png +0 -0
  11. package/assets/vendor/react/LICENSE +21 -0
  12. package/assets/vendor/react/react-dom.production.min.js +267 -0
  13. package/assets/vendor/react/react.production.min.js +31 -0
  14. package/dist/src/errors.d.ts +5 -0
  15. package/dist/src/errors.js +11 -0
  16. package/dist/src/errors.js.map +1 -0
  17. package/dist/src/gateway/claude-compatibility-scratch.d.ts +18 -0
  18. package/dist/src/gateway/claude-compatibility-scratch.js +158 -0
  19. package/dist/src/gateway/claude-compatibility-scratch.js.map +1 -0
  20. package/dist/src/gateway/claude-helper-client.d.ts +43 -0
  21. package/dist/src/gateway/claude-helper-client.js +212 -0
  22. package/dist/src/gateway/claude-helper-client.js.map +1 -0
  23. package/dist/src/gateway/claude-helper-protocol.d.ts +172 -0
  24. package/dist/src/gateway/claude-helper-protocol.js +373 -0
  25. package/dist/src/gateway/claude-helper-protocol.js.map +1 -0
  26. package/dist/src/gateway/claude-helper-supervisor.d.ts +74 -0
  27. package/dist/src/gateway/claude-helper-supervisor.js +497 -0
  28. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -0
  29. package/dist/src/gateway/claude-helper.d.ts +1 -0
  30. package/dist/src/gateway/claude-helper.js +269 -0
  31. package/dist/src/gateway/claude-helper.js.map +1 -0
  32. package/dist/src/gateway/claude-peer.d.ts +273 -0
  33. package/dist/src/gateway/claude-peer.js +2359 -0
  34. package/dist/src/gateway/claude-peer.js.map +1 -0
  35. package/dist/src/gateway/claude-runtime.d.ts +40 -0
  36. package/dist/src/gateway/claude-runtime.js +302 -0
  37. package/dist/src/gateway/claude-runtime.js.map +1 -0
  38. package/dist/src/gateway/cli-copy.d.ts +8 -0
  39. package/dist/src/gateway/cli-copy.en.d.ts +13 -0
  40. package/dist/src/gateway/cli-copy.en.js +43 -0
  41. package/dist/src/gateway/cli-copy.en.js.map +1 -0
  42. package/dist/src/gateway/cli-copy.js +19 -0
  43. package/dist/src/gateway/cli-copy.js.map +1 -0
  44. package/dist/src/gateway/cli-copy.zh-CN.d.ts +13 -0
  45. package/dist/src/gateway/cli-copy.zh-CN.js +43 -0
  46. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -0
  47. package/dist/src/gateway/cli.d.ts +44 -0
  48. package/dist/src/gateway/cli.js +969 -0
  49. package/dist/src/gateway/cli.js.map +1 -0
  50. package/dist/src/gateway/codex-app-server.d.ts +310 -0
  51. package/dist/src/gateway/codex-app-server.js +1746 -0
  52. package/dist/src/gateway/codex-app-server.js.map +1 -0
  53. package/dist/src/gateway/codex-local-transport.d.ts +81 -0
  54. package/dist/src/gateway/codex-local-transport.js +617 -0
  55. package/dist/src/gateway/codex-local-transport.js.map +1 -0
  56. package/dist/src/gateway/codex-registration-generation.d.ts +5 -0
  57. package/dist/src/gateway/codex-registration-generation.js +19 -0
  58. package/dist/src/gateway/codex-registration-generation.js.map +1 -0
  59. package/dist/src/gateway/codex-registration-succession.d.ts +209 -0
  60. package/dist/src/gateway/codex-registration-succession.js +588 -0
  61. package/dist/src/gateway/codex-registration-succession.js.map +1 -0
  62. package/dist/src/gateway/compatibility.d.ts +70 -0
  63. package/dist/src/gateway/compatibility.js +252 -0
  64. package/dist/src/gateway/compatibility.js.map +1 -0
  65. package/dist/src/gateway/config.d.ts +24 -0
  66. package/dist/src/gateway/config.js +130 -0
  67. package/dist/src/gateway/config.js.map +1 -0
  68. package/dist/src/gateway/control.d.ts +308 -0
  69. package/dist/src/gateway/control.js +1573 -0
  70. package/dist/src/gateway/control.js.map +1 -0
  71. package/dist/src/gateway/dashboard-copy.d.ts +7 -0
  72. package/dist/src/gateway/dashboard-copy.en.d.ts +506 -0
  73. package/dist/src/gateway/dashboard-copy.en.js +507 -0
  74. package/dist/src/gateway/dashboard-copy.en.js.map +1 -0
  75. package/dist/src/gateway/dashboard-copy.js +516 -0
  76. package/dist/src/gateway/dashboard-copy.js.map +1 -0
  77. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +506 -0
  78. package/dist/src/gateway/dashboard-copy.zh-CN.js +507 -0
  79. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -0
  80. package/dist/src/gateway/dashboard-model.d.ts +207 -0
  81. package/dist/src/gateway/dashboard-model.js +720 -0
  82. package/dist/src/gateway/dashboard-model.js.map +1 -0
  83. package/dist/src/gateway/dashboard.d.ts +26 -0
  84. package/dist/src/gateway/dashboard.js +941 -0
  85. package/dist/src/gateway/dashboard.js.map +1 -0
  86. package/dist/src/gateway/delivery-machine.d.ts +235 -0
  87. package/dist/src/gateway/delivery-machine.js +540 -0
  88. package/dist/src/gateway/delivery-machine.js.map +1 -0
  89. package/dist/src/gateway/instance-lease.d.ts +21 -0
  90. package/dist/src/gateway/instance-lease.js +549 -0
  91. package/dist/src/gateway/instance-lease.js.map +1 -0
  92. package/dist/src/gateway/live-dashboard-app/app.js +2914 -0
  93. package/dist/src/gateway/live-dashboard-assets.d.ts +10 -0
  94. package/dist/src/gateway/live-dashboard-assets.js +73 -0
  95. package/dist/src/gateway/live-dashboard-assets.js.map +1 -0
  96. package/dist/src/gateway/live-dashboard-bootstrap.d.ts +36 -0
  97. package/dist/src/gateway/live-dashboard-bootstrap.js +176 -0
  98. package/dist/src/gateway/live-dashboard-bootstrap.js.map +1 -0
  99. package/dist/src/gateway/live-dashboard-command.d.ts +54 -0
  100. package/dist/src/gateway/live-dashboard-command.js +324 -0
  101. package/dist/src/gateway/live-dashboard-command.js.map +1 -0
  102. package/dist/src/gateway/live-dashboard-http.d.ts +41 -0
  103. package/dist/src/gateway/live-dashboard-http.js +445 -0
  104. package/dist/src/gateway/live-dashboard-http.js.map +1 -0
  105. package/dist/src/gateway/live-dashboard-protocol.d.ts +38 -0
  106. package/dist/src/gateway/live-dashboard-protocol.js +156 -0
  107. package/dist/src/gateway/live-dashboard-protocol.js.map +1 -0
  108. package/dist/src/gateway/live-dashboard-server.d.ts +33 -0
  109. package/dist/src/gateway/live-dashboard-server.js +130 -0
  110. package/dist/src/gateway/live-dashboard-server.js.map +1 -0
  111. package/dist/src/gateway/live-dashboard-stream.d.ts +46 -0
  112. package/dist/src/gateway/live-dashboard-stream.js +233 -0
  113. package/dist/src/gateway/live-dashboard-stream.js.map +1 -0
  114. package/dist/src/gateway/live-dashboard.d.ts +31 -0
  115. package/dist/src/gateway/live-dashboard.js +190 -0
  116. package/dist/src/gateway/live-dashboard.js.map +1 -0
  117. package/dist/src/gateway/locale.d.ts +4 -0
  118. package/dist/src/gateway/locale.js +10 -0
  119. package/dist/src/gateway/locale.js.map +1 -0
  120. package/dist/src/gateway/progress-watch-machine.d.ts +97 -0
  121. package/dist/src/gateway/progress-watch-machine.js +172 -0
  122. package/dist/src/gateway/progress-watch-machine.js.map +1 -0
  123. package/dist/src/gateway/providers.d.ts +287 -0
  124. package/dist/src/gateway/providers.js +2451 -0
  125. package/dist/src/gateway/providers.js.map +1 -0
  126. package/dist/src/gateway/server.d.ts +57 -0
  127. package/dist/src/gateway/server.js +264 -0
  128. package/dist/src/gateway/server.js.map +1 -0
  129. package/dist/src/gateway/service.d.ts +449 -0
  130. package/dist/src/gateway/service.js +4831 -0
  131. package/dist/src/gateway/service.js.map +1 -0
  132. package/dist/src/gateway/store.d.ts +340 -0
  133. package/dist/src/gateway/store.js +4047 -0
  134. package/dist/src/gateway/store.js.map +1 -0
  135. package/dist/src/gateway/types.d.ts +514 -0
  136. package/dist/src/gateway/types.js +414 -0
  137. package/dist/src/gateway/types.js.map +1 -0
  138. package/dist/src/mutex.d.ts +4 -0
  139. package/dist/src/mutex.js +23 -0
  140. package/dist/src/mutex.js.map +1 -0
  141. package/docs/DESIGN.md +134 -0
  142. package/docs/GATEWAY-ARCHITECTURE.md +858 -0
  143. package/package.json +80 -0
  144. package/skills/embassy-peer/SKILL.md +198 -0
  145. package/skills/embassy-peer/agents/openai.yaml +4 -0
@@ -0,0 +1,1573 @@
1
+ /**
2
+ * Private, local gateway control transport.
3
+ *
4
+ * Every byte received here is untrusted even though the socket lives inside a
5
+ * same-UID state directory. The directory permissions are the authentication
6
+ * boundary; this module still validates every frame and field before calling a
7
+ * handler. It deliberately never logs, stores, or reflects task IDs or message
8
+ * text.
9
+ */
10
+ import { randomBytes } from "node:crypto";
11
+ import { chmod, lstat, mkdtemp, realpath, rename, rmdir, unlink, } from "node:fs/promises";
12
+ import net from "node:net";
13
+ import path from "node:path";
14
+ import { TextDecoder } from "node:util";
15
+ import { isCompatibilityAttestation, isCompatibilityCertificationReport, isCompatibilityCheckReport, } from "./compatibility.js";
16
+ import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, } from "./types.js";
17
+ export const GATEWAY_CONTROL_PROTOCOL_VERSION = 1;
18
+ export const GATEWAY_CONTROL_MAX_FRAME_BYTES = 32 * 1024;
19
+ export const GATEWAY_CONTROL_MAX_RESPONSE_BYTES = 256 * 1024;
20
+ export const GATEWAY_CONTROL_MAX_MESSAGE_BYTES = 16 * 1024;
21
+ export const GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS = 3_000;
22
+ export const GATEWAY_CONTROL_MAX_CONNECTIONS = 32;
23
+ const MAX_SOCKET_PATH_BYTES = 100;
24
+ const MAX_REPLY_ADDRESS_BYTES = 256;
25
+ const MAX_REVISION = Number.MAX_SAFE_INTEGER;
26
+ const DEFAULT_HOST_ID = "this-mac";
27
+ const DEFAULT_BUSY_POLICY = "queue";
28
+ const ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
29
+ 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;
30
+ const CONVERSATION_ID_PATTERN = /^conv_[A-Za-z0-9_-]{16,64}$/;
31
+ const DELIVERY_TOKEN_PATTERN = /^dlv_[A-Za-z0-9_-]{24}$/;
32
+ const HOST_PATTERN = /^[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
33
+ const SAFE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
34
+ const PROTOCOL_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
35
+ const PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:+-]{0,63}$/;
36
+ const MESSAGE_SUFFIX_PATTERN = /^[0-9a-f]{8}$/;
37
+ const CONVERSATION_SUFFIX_PATTERN = /^[A-Za-z0-9_-]{8}$/;
38
+ const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
39
+ export const gatewayControlMethods = [
40
+ "health",
41
+ "register_codex",
42
+ "unregister_codex",
43
+ "select_claude",
44
+ "unselect_claude",
45
+ "pair",
46
+ "unpair",
47
+ "list_snapshot",
48
+ "observe_snapshot",
49
+ "compat_check",
50
+ "compat_certify",
51
+ "delivery_status",
52
+ "untrack",
53
+ "send_to_claude",
54
+ "send_to_codex",
55
+ "reply",
56
+ "refresh_dashboard",
57
+ ];
58
+ const WIRE_ERROR_MESSAGES = {
59
+ INVALID_JSON: "The control frame is not valid JSON.",
60
+ FRAME_TOO_LARGE: "The control frame exceeds the size limit.",
61
+ INVALID_REQUEST: "The control request is invalid.",
62
+ UNSUPPORTED_VERSION: "The control protocol version is unsupported.",
63
+ UNKNOWN_METHOD: "The control method is unsupported.",
64
+ MULTIPLE_FRAMES: "Only one control request is allowed per connection.",
65
+ SERVER_BUSY: "The gateway control server is at its connection limit.",
66
+ REQUEST_TIMEOUT: "The control request timed out.",
67
+ HANDLER_FAILURE: "The gateway could not complete the control request.",
68
+ INVALID_HANDLER_RESPONSE: "The gateway produced an invalid control response.",
69
+ RESPONSE_TOO_LARGE: "The control response exceeds the size limit.",
70
+ };
71
+ export class GatewayControlTransportError extends Error {
72
+ code;
73
+ ambiguous;
74
+ recoverable;
75
+ constructor(code, message, ambiguous = false) {
76
+ super(message);
77
+ this.name = "GatewayControlTransportError";
78
+ this.code = code;
79
+ this.ambiguous = ambiguous;
80
+ this.recoverable = !ambiguous;
81
+ }
82
+ }
83
+ class ProtocolFault extends Error {
84
+ code;
85
+ constructor(code) {
86
+ super(WIRE_ERROR_MESSAGES[code]);
87
+ this.name = "ProtocolFault";
88
+ this.code = code;
89
+ }
90
+ }
91
+ export function createGatewayConversationId() {
92
+ return `conv_${randomBytes(18).toString("base64url")}`;
93
+ }
94
+ export function isGatewayAlias(value) {
95
+ return ALIAS_PATTERN.test(value);
96
+ }
97
+ export function isClaudeSessionSelector(value) {
98
+ return isGatewayAlias(value) || UUID_PATTERN.test(value);
99
+ }
100
+ export function isGatewayHostId(value) {
101
+ return HOST_PATTERN.test(value);
102
+ }
103
+ export function isGatewayConversationId(value) {
104
+ return CONVERSATION_ID_PATTERN.test(value);
105
+ }
106
+ export function isGatewayDeliveryToken(value) {
107
+ return DELIVERY_TOKEN_PATTERN.test(value);
108
+ }
109
+ export function isGatewayReplyAddress(value) {
110
+ if (value.includes("\0") ||
111
+ !value.startsWith("uds:") ||
112
+ Buffer.byteLength(value, "utf8") > MAX_REPLY_ADDRESS_BYTES) {
113
+ return false;
114
+ }
115
+ const socketPath = value.slice(4);
116
+ return (path.isAbsolute(socketPath) &&
117
+ path.resolve(socketPath) === socketPath &&
118
+ Buffer.byteLength(socketPath, "utf8") <= MAX_SOCKET_PATH_BYTES);
119
+ }
120
+ function isRecord(value) {
121
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
122
+ return false;
123
+ }
124
+ const prototype = Object.getPrototypeOf(value);
125
+ return prototype === Object.prototype || prototype === null;
126
+ }
127
+ function hasExactKeys(value, required, optional = []) {
128
+ const ownKeys = Reflect.ownKeys(value);
129
+ if (ownKeys.some((key) => typeof key !== "string"))
130
+ return false;
131
+ const allowed = new Set([...required, ...optional]);
132
+ return (required.every((key) => Object.hasOwn(value, key)) &&
133
+ ownKeys.every((key) => typeof key === "string" && allowed.has(key)));
134
+ }
135
+ function isNonNegativeInteger(value) {
136
+ return (typeof value === "number" &&
137
+ Number.isSafeInteger(value) &&
138
+ value >= 0 &&
139
+ value <= MAX_REVISION);
140
+ }
141
+ function isTrackIdleMinutes(value) {
142
+ return (typeof value === "number" &&
143
+ Number.isSafeInteger(value) &&
144
+ value >= 1 &&
145
+ value <= 24 * 60);
146
+ }
147
+ function isIsoTimestamp(value) {
148
+ if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value)) {
149
+ return false;
150
+ }
151
+ const parsed = Date.parse(value);
152
+ return Number.isFinite(parsed);
153
+ }
154
+ function isUuid(value) {
155
+ return typeof value === "string" && UUID_PATTERN.test(value);
156
+ }
157
+ function isAlias(value) {
158
+ return typeof value === "string" && isGatewayAlias(value);
159
+ }
160
+ function isHostId(value) {
161
+ return typeof value === "string" && isGatewayHostId(value);
162
+ }
163
+ function isConversationId(value) {
164
+ return typeof value === "string" && isGatewayConversationId(value);
165
+ }
166
+ function isMessageText(value) {
167
+ return (typeof value === "string" &&
168
+ value.trim().length > 0 &&
169
+ !value.includes("\0") &&
170
+ Buffer.byteLength(value, "utf8") <= GATEWAY_CONTROL_MAX_MESSAGE_BYTES);
171
+ }
172
+ function normalizeReplyCaller(value) {
173
+ if (!isRecord(value) || typeof value.kind !== "string") {
174
+ throw new ProtocolFault("INVALID_REQUEST");
175
+ }
176
+ if (value.kind === "codex") {
177
+ if (!hasExactKeys(value, ["kind", "alias", "threadId"]) ||
178
+ !isAlias(value.alias) ||
179
+ !isUuid(value.threadId)) {
180
+ throw new ProtocolFault("INVALID_REQUEST");
181
+ }
182
+ return {
183
+ kind: "codex",
184
+ alias: value.alias,
185
+ threadId: value.threadId.toLowerCase(),
186
+ };
187
+ }
188
+ if (value.kind === "claude") {
189
+ if (!hasExactKeys(value, ["kind", "alias"], ["replyAddress"]) ||
190
+ !isAlias(value.alias) ||
191
+ (value.replyAddress !== undefined &&
192
+ (typeof value.replyAddress !== "string" ||
193
+ !isGatewayReplyAddress(value.replyAddress)))) {
194
+ throw new ProtocolFault("INVALID_REQUEST");
195
+ }
196
+ return {
197
+ kind: "claude",
198
+ alias: value.alias,
199
+ ...(value.replyAddress === undefined
200
+ ? {}
201
+ : { replyAddress: value.replyAddress }),
202
+ };
203
+ }
204
+ throw new ProtocolFault("INVALID_REQUEST");
205
+ }
206
+ function normalizeParams(method, value) {
207
+ if (!isRecord(value))
208
+ throw new ProtocolFault("INVALID_REQUEST");
209
+ switch (method) {
210
+ case "health":
211
+ case "list_snapshot":
212
+ case "observe_snapshot":
213
+ case "compat_check":
214
+ case "refresh_dashboard":
215
+ if (!hasExactKeys(value, [])) {
216
+ throw new ProtocolFault("INVALID_REQUEST");
217
+ }
218
+ return {};
219
+ case "compat_certify": {
220
+ if (!hasExactKeys(value, ["withTurn"], ["codexAlias"]) ||
221
+ typeof value.withTurn !== "boolean" ||
222
+ (value.codexAlias !== undefined &&
223
+ (!isAlias(value.codexAlias) ||
224
+ !value.codexAlias.startsWith("codex-")))) {
225
+ throw new ProtocolFault("INVALID_REQUEST");
226
+ }
227
+ return {
228
+ withTurn: value.withTurn,
229
+ ...(value.codexAlias === undefined
230
+ ? {}
231
+ : { codexAlias: value.codexAlias }),
232
+ };
233
+ }
234
+ case "delivery_status": {
235
+ if (!hasExactKeys(value, ["token"]) ||
236
+ typeof value.token !== "string" ||
237
+ !isGatewayDeliveryToken(value.token)) {
238
+ throw new ProtocolFault("INVALID_REQUEST");
239
+ }
240
+ return { token: value.token };
241
+ }
242
+ case "untrack": {
243
+ if (!hasExactKeys(value, ["conversationId"]) ||
244
+ !isConversationId(value.conversationId)) {
245
+ throw new ProtocolFault("INVALID_REQUEST");
246
+ }
247
+ return { conversationId: value.conversationId };
248
+ }
249
+ case "register_codex": {
250
+ if (!hasExactKeys(value, ["alias", "threadId"], ["hostId", "busyPolicy", "succeedsAlias"]) ||
251
+ !isAlias(value.alias) ||
252
+ !value.alias.startsWith("codex-") ||
253
+ !isUuid(value.threadId) ||
254
+ (value.hostId !== undefined && !isHostId(value.hostId)) ||
255
+ (value.busyPolicy !== undefined &&
256
+ value.busyPolicy !== "queue") ||
257
+ (value.succeedsAlias !== undefined &&
258
+ (!isAlias(value.succeedsAlias) ||
259
+ !value.succeedsAlias.startsWith("codex-")))) {
260
+ throw new ProtocolFault("INVALID_REQUEST");
261
+ }
262
+ const hostId = value.hostId ?? DEFAULT_HOST_ID;
263
+ if (!value.alias.endsWith(`@${hostId}`) ||
264
+ (value.succeedsAlias !== undefined &&
265
+ (value.succeedsAlias === value.alias ||
266
+ !value.succeedsAlias.endsWith(`@${hostId}`)))) {
267
+ throw new ProtocolFault("INVALID_REQUEST");
268
+ }
269
+ return {
270
+ alias: value.alias,
271
+ threadId: value.threadId.toLowerCase(),
272
+ hostId,
273
+ busyPolicy: value.busyPolicy ?? DEFAULT_BUSY_POLICY,
274
+ ...(value.succeedsAlias === undefined
275
+ ? {}
276
+ : { succeedsAlias: value.succeedsAlias }),
277
+ };
278
+ }
279
+ case "unregister_codex": {
280
+ if (!hasExactKeys(value, ["alias", "threadId"]) ||
281
+ !isAlias(value.alias) ||
282
+ !value.alias.startsWith("codex-") ||
283
+ !isUuid(value.threadId)) {
284
+ throw new ProtocolFault("INVALID_REQUEST");
285
+ }
286
+ return {
287
+ alias: value.alias,
288
+ threadId: value.threadId.toLowerCase(),
289
+ };
290
+ }
291
+ case "select_claude":
292
+ case "unselect_claude": {
293
+ if (!hasExactKeys(value, ["alias"], ["codexThreadId"]) ||
294
+ typeof value.alias !== "string" ||
295
+ !isClaudeSessionSelector(value.alias) ||
296
+ (value.codexThreadId !== undefined && !isUuid(value.codexThreadId))) {
297
+ throw new ProtocolFault("INVALID_REQUEST");
298
+ }
299
+ return {
300
+ alias: UUID_PATTERN.test(value.alias)
301
+ ? value.alias.toLowerCase()
302
+ : value.alias,
303
+ ...(value.codexThreadId === undefined
304
+ ? {}
305
+ : { codexThreadId: value.codexThreadId.toLowerCase() }),
306
+ };
307
+ }
308
+ case "pair":
309
+ case "unpair": {
310
+ if (!hasExactKeys(value, ["claudeAlias", "codexAlias"], ["codexThreadId"]) ||
311
+ typeof value.claudeAlias !== "string" ||
312
+ !isClaudeSessionSelector(value.claudeAlias) ||
313
+ !isAlias(value.codexAlias) ||
314
+ !value.codexAlias.startsWith("codex-") ||
315
+ (value.codexThreadId !== undefined && !isUuid(value.codexThreadId)) ||
316
+ value.claudeAlias === value.codexAlias ||
317
+ (isAlias(value.claudeAlias) &&
318
+ value.claudeAlias.slice(value.claudeAlias.lastIndexOf("@") + 1) !==
319
+ value.codexAlias.slice(value.codexAlias.lastIndexOf("@") + 1))) {
320
+ throw new ProtocolFault("INVALID_REQUEST");
321
+ }
322
+ return {
323
+ claudeAlias: UUID_PATTERN.test(value.claudeAlias)
324
+ ? value.claudeAlias.toLowerCase()
325
+ : value.claudeAlias,
326
+ codexAlias: value.codexAlias,
327
+ ...(value.codexThreadId === undefined
328
+ ? {}
329
+ : { codexThreadId: value.codexThreadId.toLowerCase() }),
330
+ };
331
+ }
332
+ case "send_to_claude": {
333
+ if (!hasExactKeys(value, ["fromAlias", "threadId", "toAlias", "text"], ["expectsReply", "trackIdleMinutes"]) ||
334
+ !isAlias(value.fromAlias) ||
335
+ !isUuid(value.threadId) ||
336
+ (typeof value.toAlias !== "string" ||
337
+ !isClaudeSessionSelector(value.toAlias)) ||
338
+ !isMessageText(value.text) ||
339
+ (value.expectsReply !== undefined &&
340
+ typeof value.expectsReply !== "boolean") ||
341
+ (value.trackIdleMinutes !== undefined &&
342
+ !isTrackIdleMinutes(value.trackIdleMinutes))) {
343
+ throw new ProtocolFault("INVALID_REQUEST");
344
+ }
345
+ return {
346
+ fromAlias: value.fromAlias,
347
+ threadId: value.threadId.toLowerCase(),
348
+ toAlias: UUID_PATTERN.test(value.toAlias)
349
+ ? value.toAlias.toLowerCase()
350
+ : value.toAlias,
351
+ text: value.text,
352
+ expectsReply: value.expectsReply ?? false,
353
+ ...(value.trackIdleMinutes === undefined
354
+ ? {}
355
+ : { trackIdleMinutes: value.trackIdleMinutes }),
356
+ };
357
+ }
358
+ case "send_to_codex": {
359
+ if (!hasExactKeys(value, ["fromAlias", "toAlias", "text"], ["replyAddress", "expectsReply", "trackIdleMinutes"]) ||
360
+ !isAlias(value.fromAlias) ||
361
+ !isAlias(value.toAlias) ||
362
+ !isMessageText(value.text) ||
363
+ (value.replyAddress !== undefined &&
364
+ (typeof value.replyAddress !== "string" ||
365
+ !isGatewayReplyAddress(value.replyAddress))) ||
366
+ (value.expectsReply !== undefined &&
367
+ typeof value.expectsReply !== "boolean") ||
368
+ (value.trackIdleMinutes !== undefined &&
369
+ !isTrackIdleMinutes(value.trackIdleMinutes))) {
370
+ throw new ProtocolFault("INVALID_REQUEST");
371
+ }
372
+ return {
373
+ fromAlias: value.fromAlias,
374
+ toAlias: value.toAlias,
375
+ text: value.text,
376
+ ...(value.replyAddress === undefined
377
+ ? {}
378
+ : { replyAddress: value.replyAddress }),
379
+ expectsReply: value.expectsReply ?? false,
380
+ ...(value.trackIdleMinutes === undefined
381
+ ? {}
382
+ : { trackIdleMinutes: value.trackIdleMinutes }),
383
+ };
384
+ }
385
+ case "reply": {
386
+ if (!hasExactKeys(value, ["conversationId", "text", "caller"], ["trackIdleMinutes"]) ||
387
+ !isConversationId(value.conversationId) ||
388
+ !isMessageText(value.text) ||
389
+ (value.trackIdleMinutes !== undefined &&
390
+ !isTrackIdleMinutes(value.trackIdleMinutes))) {
391
+ throw new ProtocolFault("INVALID_REQUEST");
392
+ }
393
+ return {
394
+ conversationId: value.conversationId,
395
+ text: value.text,
396
+ caller: normalizeReplyCaller(value.caller),
397
+ ...(value.trackIdleMinutes === undefined
398
+ ? {}
399
+ : { trackIdleMinutes: value.trackIdleMinutes }),
400
+ };
401
+ }
402
+ }
403
+ }
404
+ function parseRequestObject(value) {
405
+ if (!isRecord(value) ||
406
+ !hasExactKeys(value, ["protocolVersion", "method", "params"])) {
407
+ throw new ProtocolFault("INVALID_REQUEST");
408
+ }
409
+ if (value.protocolVersion !== GATEWAY_CONTROL_PROTOCOL_VERSION) {
410
+ throw new ProtocolFault("UNSUPPORTED_VERSION");
411
+ }
412
+ if (typeof value.method !== "string" ||
413
+ !gatewayControlMethods.includes(value.method)) {
414
+ throw new ProtocolFault("UNKNOWN_METHOD");
415
+ }
416
+ const method = value.method;
417
+ const params = normalizeParams(method, value.params);
418
+ return {
419
+ protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
420
+ method,
421
+ params,
422
+ };
423
+ }
424
+ function parseRequestFrame(frame) {
425
+ let parsed;
426
+ try {
427
+ parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(frame));
428
+ }
429
+ catch {
430
+ throw new ProtocolFault("INVALID_JSON");
431
+ }
432
+ return parseRequestObject(parsed);
433
+ }
434
+ function isDecision(value) {
435
+ if (!isRecord(value) ||
436
+ !hasExactKeys(value, ["accepted", "code"]) ||
437
+ typeof value.accepted !== "boolean" ||
438
+ typeof value.code !== "string") {
439
+ return false;
440
+ }
441
+ if (value.accepted)
442
+ return value.code === "ok";
443
+ return (value.code === "not_found" ||
444
+ value.code === "conflict" ||
445
+ value.code === "route_mismatch" ||
446
+ value.code === "busy" ||
447
+ value.code === "unavailable" ||
448
+ value.code === "rejected");
449
+ }
450
+ function isHealthResult(value) {
451
+ return (isRecord(value) &&
452
+ hasExactKeys(value, ["status", "revision"]) &&
453
+ (value.status === "ok" || value.status === "degraded") &&
454
+ isNonNegativeInteger(value.revision));
455
+ }
456
+ function isSendResult(value) {
457
+ if (!isRecord(value))
458
+ return false;
459
+ if (value.accepted === true) {
460
+ return (hasExactKeys(value, [
461
+ "accepted",
462
+ "code",
463
+ "conversationId",
464
+ "deliveryToken",
465
+ ]) &&
466
+ value.code === "ok" &&
467
+ isConversationId(value.conversationId) &&
468
+ typeof value.deliveryToken === "string" &&
469
+ isGatewayDeliveryToken(value.deliveryToken));
470
+ }
471
+ return isDecision(value);
472
+ }
473
+ function isRefreshResult(value) {
474
+ if (!isRecord(value) ||
475
+ !hasExactKeys(value, ["accepted", "code", "revision"]) ||
476
+ !isNonNegativeInteger(value.revision)) {
477
+ return false;
478
+ }
479
+ const decision = { accepted: value.accepted, code: value.code };
480
+ return isDecision(decision);
481
+ }
482
+ function isProvider(value) {
483
+ return value === "codex" || value === "claude";
484
+ }
485
+ function isConnectorHealth(value) {
486
+ return (value === "offline" ||
487
+ value === "connecting" ||
488
+ value === "healthy" ||
489
+ value === "degraded" ||
490
+ value === "incompatible");
491
+ }
492
+ function isCompatibility(value) {
493
+ return (value === "unknown" ||
494
+ value === "compatible" ||
495
+ value === "incompatible" ||
496
+ value === "expired");
497
+ }
498
+ function isSafeCode(value) {
499
+ return typeof value === "string" && SAFE_CODE_PATTERN.test(value);
500
+ }
501
+ const TERMINAL_DELIVERY_STATUS_STATES = new Set([
502
+ "delivered",
503
+ "unconfirmed",
504
+ "expired",
505
+ "failed",
506
+ "ambiguous",
507
+ "cancelled",
508
+ ]);
509
+ function isDeliveryStatusState(value) {
510
+ return (value === "queued" ||
511
+ value === "stalled" ||
512
+ value === "delivered" ||
513
+ value === "unconfirmed" ||
514
+ value === "expired" ||
515
+ value === "failed" ||
516
+ value === "ambiguous" ||
517
+ value === "cancelled");
518
+ }
519
+ function isDeliveryStatusResult(value) {
520
+ if (!isRecord(value) || typeof value.found !== "boolean")
521
+ return false;
522
+ if (!value.found)
523
+ return hasExactKeys(value, ["found"]);
524
+ if (!hasExactKeys(value, ["found", "state", "terminal", "updatedAt", "deadlineAt"], ["pendingForMs", "safeErrorCode"]) ||
525
+ !isDeliveryStatusState(value.state) ||
526
+ typeof value.terminal !== "boolean" ||
527
+ value.terminal !== TERMINAL_DELIVERY_STATUS_STATES.has(value.state) ||
528
+ !isIsoTimestamp(value.updatedAt) ||
529
+ !isIsoTimestamp(value.deadlineAt) ||
530
+ (value.pendingForMs !== undefined &&
531
+ !isNonNegativeInteger(value.pendingForMs)) ||
532
+ (value.safeErrorCode !== undefined && !isSafeCode(value.safeErrorCode))) {
533
+ return false;
534
+ }
535
+ return true;
536
+ }
537
+ function isRouteCounters(value) {
538
+ if (!isRecord(value) ||
539
+ !hasExactKeys(value, [
540
+ "accepted",
541
+ "delivered",
542
+ "unconfirmed",
543
+ "failed",
544
+ "ambiguous",
545
+ "expired",
546
+ "cancelled",
547
+ "abandoned",
548
+ "rejected",
549
+ "bytesAccepted",
550
+ ])) {
551
+ return false;
552
+ }
553
+ return Object.values(value).every(isNonNegativeInteger);
554
+ }
555
+ function isConnectorSnapshot(value) {
556
+ return (isRecord(value) &&
557
+ hasExactKeys(value, [
558
+ "provider",
559
+ "host",
560
+ "health",
561
+ "compatibility",
562
+ "protocol",
563
+ "protocolVersion",
564
+ ], ["lastSeenAt", "safeErrorCode"]) &&
565
+ isProvider(value.provider) &&
566
+ isHostId(value.host) &&
567
+ isConnectorHealth(value.health) &&
568
+ isCompatibility(value.compatibility) &&
569
+ typeof value.protocol === "string" &&
570
+ PROTOCOL_PATTERN.test(value.protocol) &&
571
+ typeof value.protocolVersion === "string" &&
572
+ PROTOCOL_VERSION_PATTERN.test(value.protocolVersion) &&
573
+ (value.lastSeenAt === undefined || isIsoTimestamp(value.lastSeenAt)) &&
574
+ (value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)));
575
+ }
576
+ function isRouteSnapshot(value) {
577
+ return (isRecord(value) &&
578
+ hasExactKeys(value, [
579
+ "alias",
580
+ "provider",
581
+ "host",
582
+ "enabled",
583
+ "state",
584
+ "compatibility",
585
+ "busyPolicy",
586
+ "queueDepth",
587
+ "counters",
588
+ ], ["lastSeenAt", "oldestQueuedAt", "safeErrorCode"]) &&
589
+ isAlias(value.alias) &&
590
+ isProvider(value.provider) &&
591
+ isHostId(value.host) &&
592
+ value.alias.endsWith(`@${value.host}`) &&
593
+ typeof value.enabled === "boolean" &&
594
+ (value.state === "stale" ||
595
+ value.state === "idle" ||
596
+ value.state === "busy" ||
597
+ value.state === "awaiting_approval" ||
598
+ value.state === "offline" ||
599
+ value.state === "incompatible" ||
600
+ value.state === "disabled") &&
601
+ isCompatibility(value.compatibility) &&
602
+ value.busyPolicy === "queue" &&
603
+ (value.lastSeenAt === undefined || isIsoTimestamp(value.lastSeenAt)) &&
604
+ isNonNegativeInteger(value.queueDepth) &&
605
+ (value.oldestQueuedAt === undefined
606
+ ? value.queueDepth === 0
607
+ : value.queueDepth > 0 && isIsoTimestamp(value.oldestQueuedAt)) &&
608
+ isRouteCounters(value.counters) &&
609
+ (value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)));
610
+ }
611
+ function isPairSnapshot(value) {
612
+ return (isRecord(value) &&
613
+ hasExactKeys(value, ["claudeAlias", "codexAlias", "host", "counters"]) &&
614
+ isAlias(value.claudeAlias) &&
615
+ isAlias(value.codexAlias) &&
616
+ value.claudeAlias !== value.codexAlias &&
617
+ isHostId(value.host) &&
618
+ value.claudeAlias.endsWith(`@${value.host}`) &&
619
+ value.codexAlias.endsWith(`@${value.host}`) &&
620
+ isRouteCounters(value.counters));
621
+ }
622
+ function isAvailablePeerSnapshot(value) {
623
+ return (isRecord(value) &&
624
+ hasExactKeys(value, [
625
+ "alias",
626
+ "provider",
627
+ "host",
628
+ "state",
629
+ "compatibility",
630
+ "validated",
631
+ "selected",
632
+ ], ["lastSeenAt", "safeErrorCode"]) &&
633
+ isAlias(value.alias) &&
634
+ value.provider === "claude" &&
635
+ isHostId(value.host) &&
636
+ value.alias.endsWith(`@${value.host}`) &&
637
+ (value.state === "idle" ||
638
+ value.state === "busy" ||
639
+ value.state === "awaiting_approval" ||
640
+ value.state === "offline" ||
641
+ value.state === "incompatible") &&
642
+ isCompatibility(value.compatibility) &&
643
+ typeof value.validated === "boolean" &&
644
+ typeof value.selected === "boolean" &&
645
+ (value.lastSeenAt === undefined || isIsoTimestamp(value.lastSeenAt)) &&
646
+ (value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)));
647
+ }
648
+ function isNormalizedMessageEvent(value) {
649
+ return (isRecord(value) &&
650
+ hasExactKeys(value, [
651
+ "sequence",
652
+ "timestamp",
653
+ "messageIdSuffix",
654
+ "direction",
655
+ "sourceAlias",
656
+ "targetAlias",
657
+ "state",
658
+ "bytes",
659
+ "hopCount",
660
+ ], ["conversationIdSuffix", "latencyMs", "safeErrorCode", "steer"]) &&
661
+ isNonNegativeInteger(value.sequence) &&
662
+ isIsoTimestamp(value.timestamp) &&
663
+ typeof value.messageIdSuffix === "string" &&
664
+ MESSAGE_SUFFIX_PATTERN.test(value.messageIdSuffix) &&
665
+ (value.conversationIdSuffix === undefined ||
666
+ (typeof value.conversationIdSuffix === "string" &&
667
+ CONVERSATION_SUFFIX_PATTERN.test(value.conversationIdSuffix))) &&
668
+ (value.direction === "codex_to_claude" ||
669
+ value.direction === "claude_to_codex") &&
670
+ isAlias(value.sourceAlias) &&
671
+ isAlias(value.targetAlias) &&
672
+ (value.state === "queued" ||
673
+ value.state === "duplicate" ||
674
+ value.state === "dispatching" ||
675
+ value.state === "transport_written" ||
676
+ value.state === "held" ||
677
+ value.state === "delivered" ||
678
+ value.state === "unconfirmed" ||
679
+ value.state === "failed" ||
680
+ value.state === "ambiguous" ||
681
+ value.state === "expired" ||
682
+ value.state === "cancelled" ||
683
+ value.state === "abandoned" ||
684
+ value.state === "rejected") &&
685
+ isNonNegativeInteger(value.bytes) &&
686
+ isNonNegativeInteger(value.hopCount) &&
687
+ (value.latencyMs === undefined || isNonNegativeInteger(value.latencyMs)) &&
688
+ (value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)) &&
689
+ (value.steer === undefined || value.steer === true));
690
+ }
691
+ function isAccounting(value) {
692
+ if (!isRecord(value) ||
693
+ !hasExactKeys(value, [
694
+ "accepted",
695
+ "duplicates",
696
+ "delivered",
697
+ "unconfirmed",
698
+ "failed",
699
+ "ambiguous",
700
+ "expired",
701
+ "cancelled",
702
+ "abandoned",
703
+ "rejected",
704
+ "bytesAccepted",
705
+ "queuedBytes",
706
+ ])) {
707
+ return false;
708
+ }
709
+ return Object.values(value).every(isNonNegativeInteger);
710
+ }
711
+ function isSafeAlert(value) {
712
+ return (isRecord(value) &&
713
+ hasExactKeys(value, ["code", "severity", "timestamp"], [
714
+ "provider",
715
+ "host",
716
+ "alias",
717
+ ]) &&
718
+ isSafeCode(value.code) &&
719
+ (value.severity === "info" ||
720
+ value.severity === "warning" ||
721
+ value.severity === "error") &&
722
+ isIsoTimestamp(value.timestamp) &&
723
+ (value.provider === undefined || isProvider(value.provider)) &&
724
+ (value.host === undefined || isHostId(value.host)) &&
725
+ (value.alias === undefined || isAlias(value.alias)));
726
+ }
727
+ function isProgressWatchSnapshot(value) {
728
+ return (isRecord(value) &&
729
+ hasExactKeys(value, [
730
+ "conversationIdSuffix",
731
+ "ownerAlias",
732
+ "workerAlias",
733
+ "phase",
734
+ "capability",
735
+ "lastActivityAt",
736
+ "nextActionAt",
737
+ "idleMs",
738
+ "nudgeCount",
739
+ "workerReportedComplete",
740
+ ]) &&
741
+ typeof value.conversationIdSuffix === "string" &&
742
+ CONVERSATION_SUFFIX_PATTERN.test(value.conversationIdSuffix) &&
743
+ isAlias(value.ownerAlias) &&
744
+ isAlias(value.workerAlias) &&
745
+ value.ownerAlias !== value.workerAlias &&
746
+ (value.phase === "quiet" || value.phase === "episode") &&
747
+ (value.capability === "conversation" || value.capability === "route") &&
748
+ isIsoTimestamp(value.lastActivityAt) &&
749
+ isIsoTimestamp(value.nextActionAt) &&
750
+ typeof value.idleMs === "number" &&
751
+ isTrackIdleMinutes(value.idleMs / 60_000) &&
752
+ (value.nudgeCount === 0 || value.nudgeCount === 1 || value.nudgeCount === 2) &&
753
+ typeof value.workerReportedComplete === "boolean");
754
+ }
755
+ function isProgressWatchEventSnapshot(value) {
756
+ return (isRecord(value) &&
757
+ hasExactKeys(value, [
758
+ "sequence",
759
+ "timestamp",
760
+ "conversationIdSuffix",
761
+ "ownerAlias",
762
+ "workerAlias",
763
+ "kind",
764
+ ], ["nudgeNumber"]) &&
765
+ isNonNegativeInteger(value.sequence) &&
766
+ isIsoTimestamp(value.timestamp) &&
767
+ typeof value.conversationIdSuffix === "string" &&
768
+ CONVERSATION_SUFFIX_PATTERN.test(value.conversationIdSuffix) &&
769
+ isAlias(value.ownerAlias) &&
770
+ isAlias(value.workerAlias) &&
771
+ [
772
+ "opened",
773
+ "nudge",
774
+ "worker_reported_complete",
775
+ "capability_degraded",
776
+ "done",
777
+ "unresponsive",
778
+ "endpoint_retired",
779
+ "disabled",
780
+ ].includes(String(value.kind)) &&
781
+ (value.nudgeNumber === undefined ||
782
+ value.nudgeNumber === 1 ||
783
+ value.nudgeNumber === 2) &&
784
+ ((value.kind === "nudge") === (value.nudgeNumber !== undefined)));
785
+ }
786
+ function isGatewayActivityEvent(value) {
787
+ return (isRecord(value) &&
788
+ hasExactKeys(value, [
789
+ "sequence",
790
+ "timestamp",
791
+ "kind",
792
+ "action",
793
+ "outcome",
794
+ "aliases",
795
+ "operatorAction",
796
+ ], ["safeErrorCode"]) &&
797
+ isNonNegativeInteger(value.sequence) &&
798
+ value.sequence > 0 &&
799
+ isIsoTimestamp(value.timestamp) &&
800
+ ["discovery", "selection", "registration", "pairing", "watch"].includes(String(value.kind)) &&
801
+ [
802
+ "discovery_refreshed",
803
+ "claude_selected",
804
+ "claude_unselected",
805
+ "codex_registered",
806
+ "codex_succeeded",
807
+ "codex_unregistered",
808
+ "routes_paired",
809
+ "routes_unpaired",
810
+ "watch_ended",
811
+ ].includes(String(value.action)) &&
812
+ (value.outcome === "accepted" || value.outcome === "rejected") &&
813
+ Array.isArray(value.aliases) &&
814
+ value.aliases.length <= 2 &&
815
+ value.aliases.every(isAlias) &&
816
+ new Set(value.aliases).size === value.aliases.length &&
817
+ value.operatorAction === true &&
818
+ (value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)));
819
+ }
820
+ function isDeadlinePressure(value) {
821
+ const names = [
822
+ "under_1m",
823
+ "1m_to_5m",
824
+ "5m_to_15m",
825
+ "15m_to_60m",
826
+ "over_60m",
827
+ ];
828
+ return (isRecord(value) &&
829
+ hasExactKeys(value, ["configuredDeadlineMs", "terminalEvents", "expiredEvents", "buckets"], ["retainedSince"]) &&
830
+ isNonNegativeInteger(value.configuredDeadlineMs) &&
831
+ value.configuredDeadlineMs > 0 &&
832
+ (value.retainedSince === undefined || isIsoTimestamp(value.retainedSince)) &&
833
+ isNonNegativeInteger(value.terminalEvents) &&
834
+ isNonNegativeInteger(value.expiredEvents) &&
835
+ value.expiredEvents <= value.terminalEvents &&
836
+ Array.isArray(value.buckets) &&
837
+ value.buckets.length === names.length &&
838
+ value.buckets.every((bucket, index) => isRecord(bucket) &&
839
+ hasExactKeys(bucket, ["bucket", "settled", "expired"]) &&
840
+ bucket.bucket === names[index] &&
841
+ isNonNegativeInteger(bucket.settled) &&
842
+ isNonNegativeInteger(bucket.expired) &&
843
+ bucket.expired <= bucket.settled) &&
844
+ value.buckets.reduce((sum, bucket) => sum + Number(bucket.settled), 0) ===
845
+ value.terminalEvents &&
846
+ value.buckets.reduce((sum, bucket) => sum + Number(bucket.expired), 0) ===
847
+ value.expiredEvents);
848
+ }
849
+ function isSnapshotTruncation(value) {
850
+ return (isRecord(value) &&
851
+ hasExactKeys(value, [
852
+ "connectors",
853
+ "availablePeers",
854
+ "routes",
855
+ "pairs",
856
+ "messages",
857
+ "alerts",
858
+ ], [
859
+ "compatibilityChecks",
860
+ "progressWatches",
861
+ "progressWatchEvents",
862
+ "activityEvents",
863
+ ]) &&
864
+ isNonNegativeInteger(value.connectors) &&
865
+ isNonNegativeInteger(value.availablePeers) &&
866
+ isNonNegativeInteger(value.routes) &&
867
+ isNonNegativeInteger(value.pairs) &&
868
+ isNonNegativeInteger(value.messages) &&
869
+ isNonNegativeInteger(value.alerts) &&
870
+ (value.compatibilityChecks === undefined ||
871
+ isNonNegativeInteger(value.compatibilityChecks)) &&
872
+ (value.progressWatches === undefined ||
873
+ isNonNegativeInteger(value.progressWatches)) &&
874
+ (value.progressWatchEvents === undefined ||
875
+ isNonNegativeInteger(value.progressWatchEvents)) &&
876
+ (value.activityEvents === undefined ||
877
+ isNonNegativeInteger(value.activityEvents)));
878
+ }
879
+ export function isGatewaySnapshot(value) {
880
+ if (!isRecord(value) ||
881
+ !hasExactKeys(value, [
882
+ "schemaVersion",
883
+ "generatedAt",
884
+ "inboundMode",
885
+ "health",
886
+ "connectors",
887
+ "availablePeers",
888
+ "routes",
889
+ "pairs",
890
+ "messages",
891
+ "accounting",
892
+ "alerts",
893
+ "truncation",
894
+ ], [
895
+ "compatibilityChecks",
896
+ "progressWatches",
897
+ "progressWatchEvents",
898
+ "activityEvents",
899
+ "deadlinePressure",
900
+ ]) ||
901
+ value.schemaVersion !== 1 ||
902
+ !isIsoTimestamp(value.generatedAt) ||
903
+ (value.inboundMode !== "paired" && value.inboundMode !== "open") ||
904
+ !isConnectorHealth(value.health) ||
905
+ !Array.isArray(value.connectors) ||
906
+ value.connectors.length > gatewayPublicSnapshotLimits.connectors ||
907
+ !value.connectors.every(isConnectorSnapshot) ||
908
+ (value.compatibilityChecks !== undefined &&
909
+ (!Array.isArray(value.compatibilityChecks) ||
910
+ value.compatibilityChecks.length >
911
+ gatewayPublicSnapshotLimits.compatibilityChecks ||
912
+ !value.compatibilityChecks.every(isCompatibilityAttestation))) ||
913
+ !Array.isArray(value.availablePeers) ||
914
+ value.availablePeers.length > gatewayPublicSnapshotLimits.availablePeers ||
915
+ !value.availablePeers.every(isAvailablePeerSnapshot) ||
916
+ !Array.isArray(value.routes) ||
917
+ value.routes.length > gatewayPublicSnapshotLimits.routes ||
918
+ !value.routes.every(isRouteSnapshot) ||
919
+ !Array.isArray(value.pairs) ||
920
+ value.pairs.length > gatewayPublicSnapshotLimits.pairs ||
921
+ !value.pairs.every(isPairSnapshot) ||
922
+ (value.progressWatches !== undefined &&
923
+ (!Array.isArray(value.progressWatches) ||
924
+ value.progressWatches.length >
925
+ gatewayPublicSnapshotLimits.progressWatches ||
926
+ !value.progressWatches.every(isProgressWatchSnapshot))) ||
927
+ (value.progressWatchEvents !== undefined &&
928
+ (!Array.isArray(value.progressWatchEvents) ||
929
+ value.progressWatchEvents.length >
930
+ gatewayPublicSnapshotLimits.progressWatchEvents ||
931
+ !value.progressWatchEvents.every(isProgressWatchEventSnapshot))) ||
932
+ (value.activityEvents !== undefined &&
933
+ (!Array.isArray(value.activityEvents) ||
934
+ value.activityEvents.length >
935
+ gatewayPublicSnapshotLimits.activityEvents ||
936
+ !value.activityEvents.every(isGatewayActivityEvent))) ||
937
+ (value.deadlinePressure !== undefined &&
938
+ !isDeadlinePressure(value.deadlinePressure)) ||
939
+ !Array.isArray(value.messages) ||
940
+ value.messages.length > gatewayPublicSnapshotLimits.messages ||
941
+ !value.messages.every(isNormalizedMessageEvent) ||
942
+ !isAccounting(value.accounting) ||
943
+ !Array.isArray(value.alerts) ||
944
+ value.alerts.length > gatewayPublicSnapshotLimits.alerts ||
945
+ !value.alerts.every(isSafeAlert) ||
946
+ !isSnapshotTruncation(value.truncation)) {
947
+ return false;
948
+ }
949
+ const connectorKeys = value.connectors.map((connector) => `${connector.provider}@${connector.host}`);
950
+ const aliases = value.routes.map((route) => route.alias);
951
+ const peerAliases = value.availablePeers.map((peer) => peer.alias);
952
+ const pairKeys = value.pairs.map((pair) => `${pair.claudeAlias}\0${pair.codexAlias}`);
953
+ const routeByAlias = new Map(value.routes.map((route) => [route.alias, route]));
954
+ return (new Set(connectorKeys).size === connectorKeys.length &&
955
+ new Set(aliases).size === aliases.length &&
956
+ new Set(peerAliases).size === peerAliases.length &&
957
+ new Set(pairKeys).size === pairKeys.length &&
958
+ value.pairs.every((pair) => routeByAlias.get(pair.claudeAlias)?.provider === "claude" &&
959
+ routeByAlias.get(pair.codexAlias)?.provider === "codex") &&
960
+ Buffer.byteLength(JSON.stringify(value), "utf8") <=
961
+ GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET);
962
+ }
963
+ function isSnapshotObservation(value) {
964
+ return (isRecord(value) &&
965
+ hasExactKeys(value, ["snapshotRevision", "snapshot"]) &&
966
+ isNonNegativeInteger(value.snapshotRevision) &&
967
+ isGatewaySnapshot(value.snapshot));
968
+ }
969
+ function isResultForMethod(method, value) {
970
+ switch (method) {
971
+ case "health":
972
+ return isHealthResult(value);
973
+ case "register_codex":
974
+ case "unregister_codex":
975
+ case "select_claude":
976
+ case "unselect_claude":
977
+ case "pair":
978
+ case "unpair":
979
+ return isDecision(value);
980
+ case "list_snapshot":
981
+ return isGatewaySnapshot(value);
982
+ case "observe_snapshot":
983
+ return isSnapshotObservation(value);
984
+ case "compat_check":
985
+ return isCompatibilityCheckReport(value);
986
+ case "compat_certify":
987
+ return isCompatibilityCertificationReport(value);
988
+ case "delivery_status":
989
+ return isDeliveryStatusResult(value);
990
+ case "untrack":
991
+ return isDecision(value);
992
+ case "send_to_claude":
993
+ case "send_to_codex":
994
+ case "reply":
995
+ return isSendResult(value);
996
+ case "refresh_dashboard":
997
+ return isRefreshResult(value);
998
+ }
999
+ }
1000
+ function errorResponse(code) {
1001
+ return {
1002
+ protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
1003
+ ok: false,
1004
+ error: { code, message: WIRE_ERROR_MESSAGES[code] },
1005
+ };
1006
+ }
1007
+ async function dispatch(request, handlers) {
1008
+ let result;
1009
+ try {
1010
+ switch (request.method) {
1011
+ case "health":
1012
+ result = await handlers.health();
1013
+ break;
1014
+ case "register_codex":
1015
+ result = await handlers.registerCodex(request.params);
1016
+ break;
1017
+ case "unregister_codex":
1018
+ result = await handlers.unregisterCodex(request.params);
1019
+ break;
1020
+ case "select_claude":
1021
+ result = await handlers.selectClaude(request.params);
1022
+ break;
1023
+ case "unselect_claude":
1024
+ result = await handlers.unselectClaude(request.params);
1025
+ break;
1026
+ case "pair":
1027
+ result = await handlers.pair(request.params);
1028
+ break;
1029
+ case "unpair":
1030
+ result = await handlers.unpair(request.params);
1031
+ break;
1032
+ case "list_snapshot":
1033
+ result = await handlers.listSnapshot();
1034
+ break;
1035
+ case "observe_snapshot":
1036
+ result = await handlers.observeSnapshot();
1037
+ break;
1038
+ case "compat_check":
1039
+ result = await handlers.compatibilityCheck();
1040
+ break;
1041
+ case "compat_certify":
1042
+ result = await handlers.compatibilityCertify(request.params);
1043
+ break;
1044
+ case "delivery_status":
1045
+ result = await handlers.deliveryStatus(request.params);
1046
+ break;
1047
+ case "untrack":
1048
+ result = await handlers.untrack(request.params);
1049
+ break;
1050
+ case "send_to_claude":
1051
+ result = await handlers.sendToClaude(request.params);
1052
+ break;
1053
+ case "send_to_codex":
1054
+ result = await handlers.sendToCodex(request.params);
1055
+ break;
1056
+ case "reply":
1057
+ result = await handlers.reply(request.params);
1058
+ break;
1059
+ case "refresh_dashboard":
1060
+ result = await handlers.refreshDashboard();
1061
+ break;
1062
+ }
1063
+ }
1064
+ catch {
1065
+ return errorResponse("HANDLER_FAILURE");
1066
+ }
1067
+ if (!isResultForMethod(request.method, result)) {
1068
+ return errorResponse("INVALID_HANDLER_RESPONSE");
1069
+ }
1070
+ return {
1071
+ protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
1072
+ ok: true,
1073
+ result,
1074
+ };
1075
+ }
1076
+ function serializeResponse(response) {
1077
+ const encoded = Buffer.from(`${JSON.stringify(response)}\n`, "utf8");
1078
+ if (encoded.length <= GATEWAY_CONTROL_MAX_RESPONSE_BYTES)
1079
+ return encoded;
1080
+ return Buffer.from(`${JSON.stringify(errorResponse("RESPONSE_TOO_LARGE"))}\n`, "utf8");
1081
+ }
1082
+ function respond(socket, response) {
1083
+ if (socket.destroyed)
1084
+ return;
1085
+ let encoded;
1086
+ try {
1087
+ encoded = serializeResponse(response);
1088
+ }
1089
+ catch {
1090
+ encoded = Buffer.from(`${JSON.stringify(errorResponse("INVALID_HANDLER_RESPONSE"))}\n`, "utf8");
1091
+ }
1092
+ socket.end(encoded);
1093
+ }
1094
+ function handleConnection(socket, handlers, requestTimeoutMs) {
1095
+ let buffered = Buffer.alloc(0);
1096
+ let completed = false;
1097
+ const complete = (response) => {
1098
+ if (completed)
1099
+ return;
1100
+ completed = true;
1101
+ socket.pause();
1102
+ socket.setTimeout(0);
1103
+ respond(socket, response);
1104
+ };
1105
+ socket.setTimeout(requestTimeoutMs);
1106
+ socket.once("timeout", () => complete(errorResponse("REQUEST_TIMEOUT")));
1107
+ socket.on("error", () => {
1108
+ completed = true;
1109
+ });
1110
+ socket.on("data", (chunk) => {
1111
+ if (completed)
1112
+ return;
1113
+ if (buffered.length + chunk.length > GATEWAY_CONTROL_MAX_FRAME_BYTES) {
1114
+ complete(errorResponse("FRAME_TOO_LARGE"));
1115
+ return;
1116
+ }
1117
+ buffered = Buffer.concat([buffered, chunk]);
1118
+ const newline = buffered.indexOf(0x0a);
1119
+ if (newline < 0)
1120
+ return;
1121
+ const trailing = buffered.subarray(newline + 1).toString("utf8");
1122
+ if (trailing.trim().length > 0) {
1123
+ complete(errorResponse("MULTIPLE_FRAMES"));
1124
+ return;
1125
+ }
1126
+ const frame = buffered.subarray(0, newline);
1127
+ let request;
1128
+ try {
1129
+ request = parseRequestFrame(frame);
1130
+ }
1131
+ catch (error) {
1132
+ const code = error instanceof ProtocolFault ? error.code : "INVALID_REQUEST";
1133
+ complete(errorResponse(code));
1134
+ return;
1135
+ }
1136
+ completed = true;
1137
+ socket.pause();
1138
+ socket.setTimeout(0);
1139
+ void dispatch(request, handlers).then((response) => respond(socket, response), () => respond(socket, errorResponse("HANDLER_FAILURE")));
1140
+ });
1141
+ socket.once("end", () => {
1142
+ if (!completed)
1143
+ socket.destroy();
1144
+ });
1145
+ }
1146
+ function controlTransportError(code, ambiguous = false) {
1147
+ const messages = {
1148
+ UNSUPPORTED_PLATFORM: "Unix-domain gateway control sockets are unavailable.",
1149
+ INVALID_STATE_DIR: "The gateway state directory is invalid.",
1150
+ INSECURE_STATE_DIR: "The gateway state directory is not private to this user.",
1151
+ INVALID_SOCKET_PATH: "The gateway control socket path is invalid.",
1152
+ UNSAFE_SOCKET_TARGET: "The gateway control socket target is unsafe.",
1153
+ SOCKET_IN_USE: "The gateway control socket is already served by a live process.",
1154
+ SOCKET_PROBE_FAILED: "The gateway control socket could not be checked safely.",
1155
+ SOCKET_BIND_FAILED: "The gateway control socket could not be bound.",
1156
+ SOCKET_PERMISSION_FAILED: "The gateway control socket permissions could not be secured.",
1157
+ SOCKET_CLEANUP_CONFLICT: "The gateway control socket path changed during cleanup.",
1158
+ CONTROL_CONNECT_FAILED: "The gateway control socket could not be reached.",
1159
+ CONTROL_TIMEOUT: "The gateway control request timed out.",
1160
+ CONTROL_RESPONSE_TOO_LARGE: "The gateway control response exceeds the client limit.",
1161
+ CONTROL_INVALID_RESPONSE: "The gateway returned an invalid control response.",
1162
+ CONTROL_CONNECTION_CLOSED: "The gateway closed before returning a control response.",
1163
+ CONTROL_OUTCOME_AMBIGUOUS: "The gateway may have applied the control mutation before the response was lost; do not retry automatically.",
1164
+ };
1165
+ return new GatewayControlTransportError(code, messages[code] ?? "The gateway control transport failed.", ambiguous);
1166
+ }
1167
+ function isNonIdempotentControlMethod(method) {
1168
+ return (method === "register_codex" ||
1169
+ method === "unregister_codex" ||
1170
+ method === "select_claude" ||
1171
+ method === "unselect_claude" ||
1172
+ method === "pair" ||
1173
+ method === "unpair" ||
1174
+ method === "compat_certify" ||
1175
+ method === "send_to_claude" ||
1176
+ method === "send_to_codex" ||
1177
+ method === "reply");
1178
+ }
1179
+ async function optionalLstat(target) {
1180
+ try {
1181
+ return await lstat(target);
1182
+ }
1183
+ catch (error) {
1184
+ if (error.code === "ENOENT")
1185
+ return undefined;
1186
+ throw error;
1187
+ }
1188
+ }
1189
+ async function validateSocketLocation(stateDir, socketPath) {
1190
+ if (process.platform === "win32") {
1191
+ throw controlTransportError("UNSUPPORTED_PLATFORM");
1192
+ }
1193
+ if (!path.isAbsolute(stateDir) ||
1194
+ path.resolve(stateDir) !== stateDir ||
1195
+ !path.isAbsolute(socketPath) ||
1196
+ path.resolve(socketPath) !== socketPath ||
1197
+ Buffer.byteLength(socketPath, "utf8") > MAX_SOCKET_PATH_BYTES) {
1198
+ throw controlTransportError("INVALID_SOCKET_PATH");
1199
+ }
1200
+ let stateInfo;
1201
+ try {
1202
+ stateInfo = await lstat(stateDir);
1203
+ }
1204
+ catch {
1205
+ throw controlTransportError("INVALID_STATE_DIR");
1206
+ }
1207
+ if (stateInfo.isSymbolicLink() || !stateInfo.isDirectory()) {
1208
+ throw controlTransportError("INVALID_STATE_DIR");
1209
+ }
1210
+ const getuid = process.getuid;
1211
+ if ((getuid !== undefined && stateInfo.uid !== getuid.call(process)) ||
1212
+ (stateInfo.mode & 0o077) !== 0) {
1213
+ throw controlTransportError("INSECURE_STATE_DIR");
1214
+ }
1215
+ let stateReal;
1216
+ let parentReal;
1217
+ try {
1218
+ [stateReal, parentReal] = await Promise.all([
1219
+ realpath(stateDir),
1220
+ realpath(path.dirname(socketPath)),
1221
+ ]);
1222
+ }
1223
+ catch {
1224
+ throw controlTransportError("INVALID_SOCKET_PATH");
1225
+ }
1226
+ if (socketPath === stateDir ||
1227
+ (parentReal !== stateReal &&
1228
+ !parentReal.startsWith(`${stateReal}${path.sep}`))) {
1229
+ throw controlTransportError("INVALID_SOCKET_PATH");
1230
+ }
1231
+ }
1232
+ async function socketIsLive(socketPath) {
1233
+ return await new Promise((resolve, reject) => {
1234
+ const socket = net.createConnection(socketPath);
1235
+ let settled = false;
1236
+ const finish = (action) => {
1237
+ if (settled)
1238
+ return;
1239
+ settled = true;
1240
+ socket.destroy();
1241
+ action();
1242
+ };
1243
+ socket.setTimeout(250);
1244
+ socket.once("connect", () => finish(() => resolve(true)));
1245
+ socket.once("timeout", () => finish(() => reject(controlTransportError("SOCKET_PROBE_FAILED"))));
1246
+ socket.once("error", (error) => {
1247
+ if (error.code === "ECONNREFUSED" || error.code === "ENOENT") {
1248
+ finish(() => resolve(false));
1249
+ }
1250
+ else {
1251
+ finish(() => reject(controlTransportError("SOCKET_PROBE_FAILED")));
1252
+ }
1253
+ });
1254
+ });
1255
+ }
1256
+ function sameIdentity(info, identity) {
1257
+ return info.dev === identity.dev && info.ino === identity.ino;
1258
+ }
1259
+ async function prepareSocketTarget(socketPath) {
1260
+ let existing;
1261
+ try {
1262
+ existing = await optionalLstat(socketPath);
1263
+ }
1264
+ catch {
1265
+ throw controlTransportError("UNSAFE_SOCKET_TARGET");
1266
+ }
1267
+ if (!existing)
1268
+ return;
1269
+ if (existing.isSymbolicLink() || !existing.isSocket()) {
1270
+ throw controlTransportError("UNSAFE_SOCKET_TARGET");
1271
+ }
1272
+ const identity = { dev: existing.dev, ino: existing.ino };
1273
+ if (await socketIsLive(socketPath)) {
1274
+ throw controlTransportError("SOCKET_IN_USE");
1275
+ }
1276
+ let current;
1277
+ try {
1278
+ current = await lstat(socketPath);
1279
+ }
1280
+ catch {
1281
+ throw controlTransportError("UNSAFE_SOCKET_TARGET");
1282
+ }
1283
+ if (!current.isSocket() || !sameIdentity(current, identity)) {
1284
+ throw controlTransportError("UNSAFE_SOCKET_TARGET");
1285
+ }
1286
+ try {
1287
+ await unlink(socketPath);
1288
+ }
1289
+ catch {
1290
+ throw controlTransportError("UNSAFE_SOCKET_TARGET");
1291
+ }
1292
+ }
1293
+ async function closeNativeServer(server) {
1294
+ await new Promise((resolve) => {
1295
+ try {
1296
+ server.close(() => resolve());
1297
+ }
1298
+ catch {
1299
+ resolve();
1300
+ }
1301
+ });
1302
+ }
1303
+ async function removeOwnedSocket(socketPath, identity) {
1304
+ let current;
1305
+ try {
1306
+ current = await optionalLstat(socketPath);
1307
+ }
1308
+ catch {
1309
+ return;
1310
+ }
1311
+ if (!current)
1312
+ return;
1313
+ if (!current.isSocket() || !sameIdentity(current, identity))
1314
+ return;
1315
+ try {
1316
+ await unlink(socketPath);
1317
+ }
1318
+ catch {
1319
+ // The server is already closed. A failed best-effort exact unlink must not
1320
+ // broaden cleanup to any replacement path.
1321
+ }
1322
+ }
1323
+ async function protectSocketReplacement(socketPath, identity) {
1324
+ let current;
1325
+ try {
1326
+ current = await optionalLstat(socketPath);
1327
+ }
1328
+ catch {
1329
+ throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
1330
+ }
1331
+ if (!current || sameIdentity(current, identity))
1332
+ return undefined;
1333
+ let directory;
1334
+ try {
1335
+ directory = await mkdtemp(path.join(path.dirname(socketPath), ".gateway-control-close-"));
1336
+ }
1337
+ catch {
1338
+ throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
1339
+ }
1340
+ const backupPath = path.join(directory, "replacement");
1341
+ try {
1342
+ await rename(socketPath, backupPath);
1343
+ }
1344
+ catch (error) {
1345
+ await rmdir(directory).catch(() => undefined);
1346
+ if (error.code === "ENOENT")
1347
+ return undefined;
1348
+ throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
1349
+ }
1350
+ return { directory, backupPath };
1351
+ }
1352
+ async function restoreSocketReplacement(socketPath, replacement) {
1353
+ if (!replacement)
1354
+ return;
1355
+ let current;
1356
+ try {
1357
+ current = await optionalLstat(socketPath);
1358
+ }
1359
+ catch {
1360
+ throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
1361
+ }
1362
+ if (current) {
1363
+ throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
1364
+ }
1365
+ try {
1366
+ await rename(replacement.backupPath, socketPath);
1367
+ await rmdir(replacement.directory);
1368
+ }
1369
+ catch {
1370
+ throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
1371
+ }
1372
+ }
1373
+ function validServerTimeout(value) {
1374
+ return Number.isInteger(value) && value >= 50 && value <= 30_000;
1375
+ }
1376
+ function validClientTimeout(value) {
1377
+ return Number.isInteger(value) && value >= 50 && value <= 90_000;
1378
+ }
1379
+ export async function startGatewayControlServer(options) {
1380
+ await validateSocketLocation(options.stateDir, options.socketPath);
1381
+ await prepareSocketTarget(options.socketPath);
1382
+ const requestTimeoutMs = options.requestTimeoutMs ?? GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
1383
+ if (!validServerTimeout(requestTimeoutMs)) {
1384
+ throw controlTransportError("INVALID_SOCKET_PATH");
1385
+ }
1386
+ const connections = new Set();
1387
+ const server = net.createServer({ allowHalfOpen: true }, (socket) => {
1388
+ // Directory ownership proves same-user access, not that every caller is a
1389
+ // trusted agent. Bound idle and in-flight peers before allocating parser or
1390
+ // handler state so a local peer cannot grow this set without limit.
1391
+ if (connections.size >= GATEWAY_CONTROL_MAX_CONNECTIONS) {
1392
+ socket.once("error", () => socket.destroy());
1393
+ socket.end(serializeResponse(errorResponse("SERVER_BUSY")), () => socket.destroy());
1394
+ return;
1395
+ }
1396
+ connections.add(socket);
1397
+ socket.once("close", () => connections.delete(socket));
1398
+ handleConnection(socket, options.handlers, requestTimeoutMs);
1399
+ });
1400
+ try {
1401
+ await new Promise((resolve, reject) => {
1402
+ const onError = () => reject(controlTransportError("SOCKET_BIND_FAILED"));
1403
+ server.once("error", onError);
1404
+ server.listen(options.socketPath, () => {
1405
+ server.off("error", onError);
1406
+ resolve();
1407
+ });
1408
+ });
1409
+ }
1410
+ catch (error) {
1411
+ await closeNativeServer(server);
1412
+ throw error;
1413
+ }
1414
+ let identity;
1415
+ try {
1416
+ const info = await lstat(options.socketPath);
1417
+ if (!info.isSocket())
1418
+ throw new Error("not a socket");
1419
+ identity = { dev: info.dev, ino: info.ino };
1420
+ await chmod(options.socketPath, 0o600);
1421
+ const secured = await lstat(options.socketPath);
1422
+ if (!secured.isSocket() ||
1423
+ !sameIdentity(secured, identity) ||
1424
+ (secured.mode & 0o777) !== 0o600) {
1425
+ throw new Error("socket identity changed");
1426
+ }
1427
+ }
1428
+ catch {
1429
+ for (const connection of connections)
1430
+ connection.destroy();
1431
+ await closeNativeServer(server);
1432
+ if (identity)
1433
+ await removeOwnedSocket(options.socketPath, identity);
1434
+ throw controlTransportError("SOCKET_PERMISSION_FAILED");
1435
+ }
1436
+ const ownedIdentity = identity;
1437
+ let closed = false;
1438
+ let closePromise;
1439
+ const close = () => {
1440
+ closePromise ??= (async () => {
1441
+ const replacement = await protectSocketReplacement(options.socketPath, ownedIdentity);
1442
+ for (const connection of connections)
1443
+ connection.destroy();
1444
+ await closeNativeServer(server);
1445
+ await removeOwnedSocket(options.socketPath, ownedIdentity);
1446
+ await restoreSocketReplacement(options.socketPath, replacement);
1447
+ closed = true;
1448
+ })();
1449
+ return closePromise;
1450
+ };
1451
+ return {
1452
+ socketPath: options.socketPath,
1453
+ get closed() {
1454
+ return closed;
1455
+ },
1456
+ close,
1457
+ };
1458
+ }
1459
+ function validateResponseForMethod(method, value) {
1460
+ if (!isRecord(value) || value.protocolVersion !== 1) {
1461
+ throw controlTransportError("CONTROL_INVALID_RESPONSE");
1462
+ }
1463
+ if (value.ok === false) {
1464
+ if (!hasExactKeys(value, ["protocolVersion", "ok", "error"]) ||
1465
+ !isRecord(value.error) ||
1466
+ !hasExactKeys(value.error, ["code", "message"]) ||
1467
+ typeof value.error.code !== "string" ||
1468
+ !Object.hasOwn(WIRE_ERROR_MESSAGES, value.error.code) ||
1469
+ value.error.message !==
1470
+ WIRE_ERROR_MESSAGES[value.error.code]) {
1471
+ throw controlTransportError("CONTROL_INVALID_RESPONSE");
1472
+ }
1473
+ return value;
1474
+ }
1475
+ if (value.ok !== true ||
1476
+ !hasExactKeys(value, ["protocolVersion", "ok", "result"]) ||
1477
+ !isResultForMethod(method, value.result)) {
1478
+ throw controlTransportError("CONTROL_INVALID_RESPONSE");
1479
+ }
1480
+ return value;
1481
+ }
1482
+ export async function sendGatewayControlRequest(options) {
1483
+ if (!path.isAbsolute(options.socketPath) ||
1484
+ path.resolve(options.socketPath) !== options.socketPath ||
1485
+ Buffer.byteLength(options.socketPath, "utf8") > MAX_SOCKET_PATH_BYTES) {
1486
+ throw controlTransportError("INVALID_SOCKET_PATH");
1487
+ }
1488
+ const timeoutMs = options.timeoutMs ?? GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
1489
+ const maxResponseBytes = options.maxResponseBytes ?? GATEWAY_CONTROL_MAX_RESPONSE_BYTES;
1490
+ if (!validClientTimeout(timeoutMs) ||
1491
+ !Number.isInteger(maxResponseBytes) ||
1492
+ maxResponseBytes < 256 ||
1493
+ maxResponseBytes > GATEWAY_CONTROL_MAX_RESPONSE_BYTES) {
1494
+ throw controlTransportError("CONTROL_INVALID_RESPONSE");
1495
+ }
1496
+ const normalized = parseRequestObject(options.request);
1497
+ if (normalized.method !== options.request.method) {
1498
+ throw new ProtocolFault("INVALID_REQUEST");
1499
+ }
1500
+ const requestFrame = Buffer.from(`${JSON.stringify(normalized)}\n`, "utf8");
1501
+ if (requestFrame.length > GATEWAY_CONTROL_MAX_FRAME_BYTES) {
1502
+ throw new ProtocolFault("FRAME_TOO_LARGE");
1503
+ }
1504
+ return await new Promise((resolve, reject) => {
1505
+ const socket = net.createConnection(options.socketPath);
1506
+ let settled = false;
1507
+ let writeStarted = false;
1508
+ let buffered = Buffer.alloc(0);
1509
+ const mutation = isNonIdempotentControlMethod(normalized.method);
1510
+ const fail = (error) => {
1511
+ if (settled)
1512
+ return;
1513
+ settled = true;
1514
+ socket.destroy();
1515
+ reject(mutation && writeStarted
1516
+ ? controlTransportError("CONTROL_OUTCOME_AMBIGUOUS", true)
1517
+ : error);
1518
+ };
1519
+ const succeed = (response) => {
1520
+ if (settled)
1521
+ return;
1522
+ settled = true;
1523
+ socket.destroy();
1524
+ resolve(response);
1525
+ };
1526
+ socket.setTimeout(timeoutMs);
1527
+ socket.once("connect", () => {
1528
+ // From this point onward the peer may receive and execute all or part of
1529
+ // the frame. Losing the response to a mutation is therefore ambiguous,
1530
+ // never a safe retry signal.
1531
+ writeStarted = true;
1532
+ socket.write(requestFrame);
1533
+ });
1534
+ socket.once("timeout", () => fail(controlTransportError("CONTROL_TIMEOUT")));
1535
+ socket.once("error", () => fail(controlTransportError("CONTROL_CONNECT_FAILED")));
1536
+ socket.on("data", (chunk) => {
1537
+ if (settled)
1538
+ return;
1539
+ if (buffered.length + chunk.length > maxResponseBytes) {
1540
+ fail(controlTransportError("CONTROL_RESPONSE_TOO_LARGE"));
1541
+ return;
1542
+ }
1543
+ buffered = Buffer.concat([buffered, chunk]);
1544
+ const newline = buffered.indexOf(0x0a);
1545
+ if (newline < 0)
1546
+ return;
1547
+ if (buffered.subarray(newline + 1).toString("utf8").trim().length > 0) {
1548
+ fail(controlTransportError("CONTROL_INVALID_RESPONSE"));
1549
+ return;
1550
+ }
1551
+ let parsed;
1552
+ try {
1553
+ parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffered.subarray(0, newline)));
1554
+ }
1555
+ catch {
1556
+ fail(controlTransportError("CONTROL_INVALID_RESPONSE"));
1557
+ return;
1558
+ }
1559
+ try {
1560
+ succeed(validateResponseForMethod(options.request.method, parsed));
1561
+ }
1562
+ catch {
1563
+ fail(controlTransportError("CONTROL_INVALID_RESPONSE"));
1564
+ }
1565
+ });
1566
+ socket.once("close", () => {
1567
+ if (!settled) {
1568
+ fail(controlTransportError("CONTROL_CONNECTION_CLOSED"));
1569
+ }
1570
+ });
1571
+ });
1572
+ }
1573
+ //# sourceMappingURL=control.js.map