agent-embassy 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/CONTRIBUTING.md +51 -47
- package/README.md +26 -9
- package/README.zh-CN.md +22 -9
- package/SECURITY.md +48 -35
- package/dist/src/gateway/acp-client.d.ts +12 -19
- package/dist/src/gateway/acp-client.js +90 -65
- package/dist/src/gateway/acp-client.js.map +1 -1
- package/dist/src/gateway/acp-provider.d.ts +11 -10
- package/dist/src/gateway/acp-provider.js +165 -53
- package/dist/src/gateway/acp-provider.js.map +1 -1
- package/dist/src/gateway/claude-helper-client.d.ts +2 -43
- package/dist/src/gateway/claude-helper-client.js +1 -211
- package/dist/src/gateway/claude-helper-client.js.map +1 -1
- package/dist/src/gateway/claude-helper-protocol.d.ts +16 -74
- package/dist/src/gateway/claude-helper-protocol.js +99 -378
- package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
- package/dist/src/gateway/claude-helper-supervisor.d.ts +56 -44
- package/dist/src/gateway/claude-helper-supervisor.js +265 -456
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
- package/dist/src/gateway/claude-helper.js +184 -210
- package/dist/src/gateway/claude-helper.js.map +1 -1
- package/dist/src/gateway/claude-peer.d.ts +20 -87
- package/dist/src/gateway/claude-peer.js +310 -1009
- package/dist/src/gateway/claude-peer.js.map +1 -1
- package/dist/src/gateway/claude-runtime.d.ts +13 -15
- package/dist/src/gateway/claude-runtime.js +14 -168
- package/dist/src/gateway/claude-runtime.js.map +1 -1
- package/dist/src/gateway/cli-copy.d.ts +1 -1
- package/dist/src/gateway/cli-copy.en.d.ts +2 -4
- package/dist/src/gateway/cli-copy.en.js +3 -4
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.js +0 -2
- package/dist/src/gateway/cli-copy.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +2 -4
- package/dist/src/gateway/cli-copy.zh-CN.js +3 -4
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +7 -2
- package/dist/src/gateway/cli.js +273 -617
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +5 -227
- package/dist/src/gateway/codex-app-server.js +18 -1463
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-doctor.d.ts +6 -3
- package/dist/src/gateway/codex-doctor.js +77 -98
- package/dist/src/gateway/codex-doctor.js.map +1 -1
- package/dist/src/gateway/codex-local-transport.d.ts +1 -6
- package/dist/src/gateway/codex-local-transport.js +8 -10
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/codex-stateless-transport.d.ts +114 -0
- package/dist/src/gateway/codex-stateless-transport.js +1108 -0
- package/dist/src/gateway/codex-stateless-transport.js.map +1 -0
- package/dist/src/gateway/config.d.ts +2 -9
- package/dist/src/gateway/config.js +62 -100
- package/dist/src/gateway/config.js.map +1 -1
- package/dist/src/gateway/control.d.ts +36 -90
- package/dist/src/gateway/control.js +464 -1159
- package/dist/src/gateway/control.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.d.ts +1 -1
- package/dist/src/gateway/dashboard-copy.en.d.ts +3 -26
- package/dist/src/gateway/dashboard-copy.en.js +14 -37
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +3 -26
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +3 -26
- package/dist/src/gateway/dashboard-copy.zh-CN.js +14 -37
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +6 -23
- package/dist/src/gateway/dashboard-model.js +37 -131
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.d.ts +0 -6
- package/dist/src/gateway/dashboard.js +0 -6
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/live-dashboard-app/app.js +9 -32
- package/dist/src/gateway/live-dashboard-command.js +2 -2
- package/dist/src/gateway/live-dashboard-command.js.map +1 -1
- package/dist/src/gateway/live-dashboard-http.d.ts +1 -1
- package/dist/src/gateway/live-dashboard-http.js +2 -2
- package/dist/src/gateway/live-dashboard-http.js.map +1 -1
- package/dist/src/gateway/progress-watch-machine.d.ts +1 -37
- package/dist/src/gateway/progress-watch-machine.js +4 -15
- package/dist/src/gateway/progress-watch-machine.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +55 -195
- package/dist/src/gateway/providers.js +608 -2465
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +16 -25
- package/dist/src/gateway/server.js +164 -251
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +146 -448
- package/dist/src/gateway/service.js +1670 -6142
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/state-v2-to-v3.d.ts +23 -0
- package/dist/src/gateway/state-v2-to-v3.js +994 -0
- package/dist/src/gateway/state-v2-to-v3.js.map +1 -0
- package/dist/src/gateway/store.d.ts +54 -309
- package/dist/src/gateway/store.js +1560 -3524
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +175 -228
- package/dist/src/gateway/types.js +114 -252
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +22 -9
- package/docs/CONFIGURATION.zh-CN.md +21 -9
- package/docs/DASHBOARD.md +10 -10
- package/docs/DASHBOARD.zh-CN.md +3 -3
- package/docs/DELIVERY.md +4 -4
- package/docs/DELIVERY.zh-CN.md +4 -4
- package/docs/GATEWAY-ARCHITECTURE.md +158 -187
- package/package.json +1 -1
- package/skills/embassy-peer/SKILL.md +28 -22
- package/dist/src/gateway/codex-registration-generation.d.ts +0 -5
- package/dist/src/gateway/codex-registration-generation.js +0 -19
- package/dist/src/gateway/codex-registration-generation.js.map +0 -1
- package/dist/src/gateway/codex-registration-succession.d.ts +0 -209
- package/dist/src/gateway/codex-registration-succession.js +0 -588
- package/dist/src/gateway/codex-registration-succession.js.map +0 -1
- package/dist/src/gateway/compatibility.d.ts +0 -22
- package/dist/src/gateway/compatibility.js +0 -23
- package/dist/src/gateway/compatibility.js.map +0 -1
- package/dist/src/gateway/delivery-machine.d.ts +0 -235
- package/dist/src/gateway/delivery-machine.js +0 -540
- package/dist/src/gateway/delivery-machine.js.map +0 -1
|
@@ -1,18 +1,10 @@
|
|
|
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
|
-
*/
|
|
1
|
+
/** Private, bounded JSONL control transport over one same-user Unix socket. */
|
|
10
2
|
import { randomBytes } from "node:crypto";
|
|
11
|
-
import { chmod, lstat, mkdtemp, realpath, rename, rmdir, unlink
|
|
3
|
+
import { chmod, lstat, mkdtemp, realpath, rename, rmdir, unlink } from "node:fs/promises";
|
|
12
4
|
import net from "node:net";
|
|
13
5
|
import path from "node:path";
|
|
14
6
|
import { TextDecoder } from "node:util";
|
|
15
|
-
import {
|
|
7
|
+
import { CONNECTOR_OBSERVATION_STALE_AFTER_MS, GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayProviders, gatewayPublicSnapshotLimits, isGatewayProvider, isMessageDirection, isPublicRegistryObservationSnapshot, } from "./types.js";
|
|
16
8
|
export const GATEWAY_CONTROL_PROTOCOL_VERSION = 1;
|
|
17
9
|
export const GATEWAY_CONTROL_MAX_FRAME_BYTES = 32 * 1024;
|
|
18
10
|
export const GATEWAY_CONTROL_MAX_RESPONSE_BYTES = 256 * 1024;
|
|
@@ -23,7 +15,6 @@ const MAX_SOCKET_PATH_BYTES = 100;
|
|
|
23
15
|
const MAX_REPLY_ADDRESS_BYTES = 256;
|
|
24
16
|
const MAX_REVISION = Number.MAX_SAFE_INTEGER;
|
|
25
17
|
const DEFAULT_HOST_ID = "this-mac";
|
|
26
|
-
const DEFAULT_BUSY_POLICY = "queue";
|
|
27
18
|
const ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
28
19
|
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;
|
|
29
20
|
const CONVERSATION_ID_PATTERN = /^conv_[A-Za-z0-9_-]{16,64}$/;
|
|
@@ -36,48 +27,46 @@ const MESSAGE_SUFFIX_PATTERN = /^[0-9a-f]{8}$/;
|
|
|
36
27
|
const CONVERSATION_SUFFIX_PATTERN = /^[A-Za-z0-9_-]{8}$/;
|
|
37
28
|
const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
38
29
|
export const gatewayControlMethods = [
|
|
39
|
-
"health",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"select_claude",
|
|
44
|
-
"unselect_claude",
|
|
45
|
-
"pair",
|
|
46
|
-
"unpair",
|
|
47
|
-
"list_snapshot",
|
|
48
|
-
"observe_snapshot",
|
|
49
|
-
"delivery_status",
|
|
50
|
-
"untrack",
|
|
51
|
-
"send_to_claude",
|
|
52
|
-
"send_to_codex",
|
|
53
|
-
"reply",
|
|
54
|
-
"refresh_dashboard",
|
|
30
|
+
"health", "register_codex", "unregister_codex", "remove_codex_registration",
|
|
31
|
+
"select_claude", "unselect_claude", "pair", "unpair", "list_snapshot",
|
|
32
|
+
"observe_snapshot", "delivery_status", "untrack", "send_to_claude",
|
|
33
|
+
"send_to_codex", "reply", "refresh_dashboard",
|
|
55
34
|
];
|
|
56
35
|
export function pairParamAliases(params) {
|
|
57
|
-
return "aliases" in params
|
|
58
|
-
? params.aliases
|
|
59
|
-
: [params.claudeAlias, params.codexAlias];
|
|
36
|
+
return "aliases" in params ? params.aliases : [params.claudeAlias, params.codexAlias];
|
|
60
37
|
}
|
|
61
38
|
export function pairParamThreadAttestation(params) {
|
|
62
39
|
if ("aliases" in params)
|
|
63
40
|
return params.threadAttestation;
|
|
64
41
|
return params.codexThreadId === undefined
|
|
65
|
-
? undefined
|
|
66
|
-
: { alias: params.codexAlias, threadId: params.codexThreadId };
|
|
42
|
+
? undefined : { alias: params.codexAlias, threadId: params.codexThreadId };
|
|
67
43
|
}
|
|
68
44
|
const WIRE_ERROR_MESSAGES = {
|
|
69
|
-
INVALID_JSON: "The control frame is not valid JSON.",
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
MULTIPLE_FRAMES: "Only one control request is allowed per connection.",
|
|
75
|
-
SERVER_BUSY: "The gateway control server is at its connection limit.",
|
|
76
|
-
REQUEST_TIMEOUT: "The control request timed out.",
|
|
77
|
-
HANDLER_FAILURE: "The gateway could not complete the control request.",
|
|
78
|
-
INVALID_HANDLER_RESPONSE: "The gateway produced an invalid control response.",
|
|
45
|
+
INVALID_JSON: "The control frame is not valid JSON.", FRAME_TOO_LARGE: "The control frame exceeds the size limit.",
|
|
46
|
+
INVALID_REQUEST: "The control request is invalid.", UNSUPPORTED_VERSION: "The control protocol version is unsupported.",
|
|
47
|
+
UNKNOWN_METHOD: "The control method is unsupported.", MULTIPLE_FRAMES: "Only one control request is allowed per connection.",
|
|
48
|
+
SERVER_BUSY: "The gateway control server is at its connection limit.", REQUEST_TIMEOUT: "The control request timed out.",
|
|
49
|
+
HANDLER_FAILURE: "The gateway could not complete the control request.", INVALID_HANDLER_RESPONSE: "The gateway produced an invalid control response.",
|
|
79
50
|
RESPONSE_TOO_LARGE: "The control response exceeds the size limit.",
|
|
80
51
|
};
|
|
52
|
+
const TRANSPORT_MESSAGES = {
|
|
53
|
+
UNSUPPORTED_PLATFORM: "Unix-domain gateway control sockets are unavailable.",
|
|
54
|
+
INVALID_STATE_DIR: "The gateway state directory is invalid.",
|
|
55
|
+
INSECURE_STATE_DIR: "The gateway state directory is not private to this user.",
|
|
56
|
+
INVALID_SOCKET_PATH: "The gateway control socket path is invalid.",
|
|
57
|
+
UNSAFE_SOCKET_TARGET: "The gateway control socket target is unsafe.",
|
|
58
|
+
SOCKET_IN_USE: "The gateway control socket is already served by a live process.",
|
|
59
|
+
SOCKET_PROBE_FAILED: "The gateway control socket could not be checked safely.",
|
|
60
|
+
SOCKET_BIND_FAILED: "The gateway control socket could not be bound.",
|
|
61
|
+
SOCKET_PERMISSION_FAILED: "The gateway control socket permissions could not be secured.",
|
|
62
|
+
SOCKET_CLEANUP_CONFLICT: "The gateway control socket path changed during cleanup.",
|
|
63
|
+
CONTROL_CONNECT_FAILED: "The gateway control socket could not be reached.",
|
|
64
|
+
CONTROL_TIMEOUT: "The gateway control request timed out.",
|
|
65
|
+
CONTROL_RESPONSE_TOO_LARGE: "The gateway control response exceeds the client limit.",
|
|
66
|
+
CONTROL_INVALID_RESPONSE: "The gateway returned an invalid control response. Restart the broker, then retry.",
|
|
67
|
+
CONTROL_CONNECTION_CLOSED: "The gateway closed before returning a control response.",
|
|
68
|
+
CONTROL_OUTCOME_AMBIGUOUS: "The gateway may have applied the control mutation before the response was lost; do not retry automatically.",
|
|
69
|
+
};
|
|
81
70
|
export class GatewayControlTransportError extends Error {
|
|
82
71
|
code;
|
|
83
72
|
ambiguous;
|
|
@@ -98,34 +87,27 @@ class ProtocolFault extends Error {
|
|
|
98
87
|
this.code = code;
|
|
99
88
|
}
|
|
100
89
|
}
|
|
90
|
+
function controlTransportError(code, ambiguous = false) {
|
|
91
|
+
return new GatewayControlTransportError(code, TRANSPORT_MESSAGES[code] ?? "The gateway control transport failed.", ambiguous);
|
|
92
|
+
}
|
|
93
|
+
function invalid() { throw new ProtocolFault("INVALID_REQUEST"); }
|
|
101
94
|
export function createGatewayConversationId() {
|
|
102
95
|
return `conv_${randomBytes(18).toString("base64url")}`;
|
|
103
96
|
}
|
|
104
|
-
export function isGatewayAlias(value) {
|
|
105
|
-
return ALIAS_PATTERN.test(value);
|
|
106
|
-
}
|
|
97
|
+
export function isGatewayAlias(value) { return ALIAS_PATTERN.test(value); }
|
|
107
98
|
export function isClaudeSessionSelector(value) {
|
|
108
99
|
return isGatewayAlias(value) || UUID_PATTERN.test(value);
|
|
109
100
|
}
|
|
110
|
-
export function isGatewayHostId(value) {
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
export function isGatewayConversationId(value) {
|
|
114
|
-
return CONVERSATION_ID_PATTERN.test(value);
|
|
115
|
-
}
|
|
116
|
-
export function isGatewayDeliveryToken(value) {
|
|
117
|
-
return DELIVERY_TOKEN_PATTERN.test(value);
|
|
118
|
-
}
|
|
101
|
+
export function isGatewayHostId(value) { return HOST_PATTERN.test(value); }
|
|
102
|
+
export function isGatewayConversationId(value) { return CONVERSATION_ID_PATTERN.test(value); }
|
|
103
|
+
export function isGatewayDeliveryToken(value) { return DELIVERY_TOKEN_PATTERN.test(value); }
|
|
119
104
|
export function isGatewayReplyAddress(value) {
|
|
120
|
-
if (value.includes("\0") ||
|
|
121
|
-
|
|
122
|
-
Buffer.byteLength(value, "utf8") > MAX_REPLY_ADDRESS_BYTES) {
|
|
105
|
+
if (value.includes("\0") || !value.startsWith("uds:") ||
|
|
106
|
+
Buffer.byteLength(value, "utf8") > MAX_REPLY_ADDRESS_BYTES)
|
|
123
107
|
return false;
|
|
124
|
-
}
|
|
125
108
|
const socketPath = value.slice(4);
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
Buffer.byteLength(socketPath, "utf8") <= MAX_SOCKET_PATH_BYTES);
|
|
109
|
+
return path.isAbsolute(socketPath) && path.resolve(socketPath) === socketPath &&
|
|
110
|
+
Buffer.byteLength(socketPath, "utf8") <= MAX_SOCKET_PATH_BYTES;
|
|
129
111
|
}
|
|
130
112
|
function isRecord(value) {
|
|
131
113
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -170,1084 +152,465 @@ function isAlias(value) {
|
|
|
170
152
|
function isHostId(value) {
|
|
171
153
|
return typeof value === "string" && isGatewayHostId(value);
|
|
172
154
|
}
|
|
173
|
-
function isConversationId(value) {
|
|
174
|
-
return typeof value === "string" && isGatewayConversationId(value);
|
|
175
|
-
}
|
|
176
155
|
function isMessageText(value) {
|
|
177
156
|
return (typeof value === "string" &&
|
|
178
157
|
value.trim().length > 0 &&
|
|
179
158
|
!value.includes("\0") &&
|
|
180
159
|
Buffer.byteLength(value, "utf8") <= GATEWAY_CONTROL_MAX_MESSAGE_BYTES);
|
|
181
160
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
161
|
+
const exact = hasExactKeys;
|
|
162
|
+
const nonNegative = isNonNegativeInteger;
|
|
163
|
+
const trackMinutes = isTrackIdleMinutes;
|
|
164
|
+
const iso = isIsoTimestamp;
|
|
165
|
+
const uuid = isUuid;
|
|
166
|
+
const alias = isAlias;
|
|
167
|
+
const host = isHostId;
|
|
168
|
+
function shape(value, required, optional = {}) {
|
|
169
|
+
if (!isRecord(value) || !exact(value, Object.keys(required), Object.keys(optional)))
|
|
170
|
+
return false;
|
|
171
|
+
return Object.entries(required).every(([key, check]) => check(value[key])) &&
|
|
172
|
+
Object.entries(optional).every(([key, check]) => value[key] === undefined || check(value[key]));
|
|
173
|
+
}
|
|
174
|
+
function oneOf(...values) { return (value) => values.includes(value); }
|
|
175
|
+
function arrayOf(value, maximum, check) {
|
|
176
|
+
return Array.isArray(value) && value.length <= maximum && value.every(check);
|
|
177
|
+
}
|
|
178
|
+
function positive(value) { return nonNegative(value) && value > 0; }
|
|
179
|
+
function safeCode(value) { return typeof value === "string" && SAFE_CODE_PATTERN.test(value); }
|
|
180
|
+
const messageText = isMessageText;
|
|
181
|
+
function emptyParams(value) {
|
|
182
|
+
if (!isRecord(value) || !exact(value, []))
|
|
183
|
+
invalid();
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
function decodeRegister(value) {
|
|
187
|
+
if (!isRecord(value) || !exact(value, ["alias", "threadId"], ["hostId", "busyPolicy", "succeedsAlias"]) ||
|
|
188
|
+
!alias(value.alias) || !value.alias.startsWith("codex-") || !uuid(value.threadId) ||
|
|
189
|
+
(value.hostId !== undefined && !host(value.hostId)) ||
|
|
190
|
+
(value.busyPolicy !== undefined && value.busyPolicy !== "queue") ||
|
|
191
|
+
(value.succeedsAlias !== undefined && (!alias(value.succeedsAlias) || !value.succeedsAlias.startsWith("codex-"))))
|
|
192
|
+
invalid();
|
|
193
|
+
const hostId = value.hostId ?? DEFAULT_HOST_ID;
|
|
194
|
+
if (!value.alias.endsWith(`@${hostId}`) ||
|
|
195
|
+
(value.succeedsAlias !== undefined &&
|
|
196
|
+
(value.succeedsAlias === value.alias || !value.succeedsAlias.endsWith(`@${hostId}`))))
|
|
197
|
+
invalid();
|
|
198
|
+
return { alias: value.alias, threadId: value.threadId.toLowerCase(), hostId,
|
|
199
|
+
busyPolicy: "queue", ...(value.succeedsAlias === undefined ? {} : { succeedsAlias: value.succeedsAlias }) };
|
|
200
|
+
}
|
|
201
|
+
function decodeUnregister(value) {
|
|
202
|
+
if (!shape(value, { alias, threadId: uuid }) || !value.alias.startsWith("codex-"))
|
|
203
|
+
invalid();
|
|
204
|
+
return { alias: value.alias, threadId: value.threadId.toLowerCase() };
|
|
205
|
+
}
|
|
206
|
+
function decodeRemoval(value) {
|
|
207
|
+
if (!shape(value, { alias }) || !value.alias.startsWith("codex-"))
|
|
208
|
+
invalid();
|
|
209
|
+
return { alias: value.alias };
|
|
210
|
+
}
|
|
211
|
+
function decodeSelection(value) {
|
|
212
|
+
if (!isRecord(value) || !exact(value, ["alias"], ["codexThreadId"]) ||
|
|
213
|
+
typeof value.alias !== "string" || !isClaudeSessionSelector(value.alias) ||
|
|
214
|
+
(value.codexThreadId !== undefined && !uuid(value.codexThreadId)))
|
|
215
|
+
invalid();
|
|
216
|
+
return { alias: UUID_PATTERN.test(value.alias) ? value.alias.toLowerCase() : value.alias,
|
|
217
|
+
...(value.codexThreadId === undefined ? {} : { codexThreadId: value.codexThreadId.toLowerCase() }) };
|
|
218
|
+
}
|
|
219
|
+
function decodePair(value) {
|
|
217
220
|
if (!isRecord(value))
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
return { token: value.token };
|
|
235
|
-
}
|
|
236
|
-
case "untrack": {
|
|
237
|
-
if (!hasExactKeys(value, ["conversationId"]) ||
|
|
238
|
-
!isConversationId(value.conversationId)) {
|
|
239
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
240
|
-
}
|
|
241
|
-
return { conversationId: value.conversationId };
|
|
221
|
+
invalid();
|
|
222
|
+
if (Object.hasOwn(value, "aliases")) {
|
|
223
|
+
if (!exact(value, ["aliases"], ["threadAttestation"]) || !Array.isArray(value.aliases) ||
|
|
224
|
+
value.aliases.length !== 2 || !alias(value.aliases[0]) || !alias(value.aliases[1]) ||
|
|
225
|
+
value.aliases[0] === value.aliases[1] ||
|
|
226
|
+
value.aliases[0].slice(value.aliases[0].lastIndexOf("@") + 1) !==
|
|
227
|
+
value.aliases[1].slice(value.aliases[1].lastIndexOf("@") + 1))
|
|
228
|
+
invalid();
|
|
229
|
+
let threadAttestation;
|
|
230
|
+
if (value.threadAttestation !== undefined) {
|
|
231
|
+
if (!shape(value.threadAttestation, { alias, threadId: uuid }) ||
|
|
232
|
+
!value.aliases.includes(value.threadAttestation.alias))
|
|
233
|
+
invalid();
|
|
234
|
+
threadAttestation = { alias: value.threadAttestation.alias,
|
|
235
|
+
threadId: value.threadAttestation.threadId.toLowerCase() };
|
|
242
236
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
threadId: value.threadId.toLowerCase(),
|
|
266
|
-
hostId,
|
|
267
|
-
busyPolicy: value.busyPolicy ?? DEFAULT_BUSY_POLICY,
|
|
268
|
-
...(value.succeedsAlias === undefined
|
|
269
|
-
? {}
|
|
270
|
-
: { succeedsAlias: value.succeedsAlias }),
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
case "unregister_codex": {
|
|
274
|
-
if (!hasExactKeys(value, ["alias", "threadId"]) ||
|
|
275
|
-
!isAlias(value.alias) ||
|
|
276
|
-
!value.alias.startsWith("codex-") ||
|
|
277
|
-
!isUuid(value.threadId)) {
|
|
278
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
279
|
-
}
|
|
280
|
-
return {
|
|
281
|
-
alias: value.alias,
|
|
282
|
-
threadId: value.threadId.toLowerCase(),
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
case "remove_stale_codex_registration": {
|
|
286
|
-
if (!hasExactKeys(value, ["alias"]) ||
|
|
287
|
-
!isAlias(value.alias) ||
|
|
288
|
-
!value.alias.startsWith("codex-")) {
|
|
289
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
290
|
-
}
|
|
291
|
-
return { alias: value.alias };
|
|
292
|
-
}
|
|
293
|
-
case "select_claude":
|
|
294
|
-
case "unselect_claude": {
|
|
295
|
-
if (!hasExactKeys(value, ["alias"], ["codexThreadId"]) ||
|
|
296
|
-
typeof value.alias !== "string" ||
|
|
297
|
-
!isClaudeSessionSelector(value.alias) ||
|
|
298
|
-
(value.codexThreadId !== undefined && !isUuid(value.codexThreadId))) {
|
|
299
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
300
|
-
}
|
|
301
|
-
return {
|
|
302
|
-
alias: UUID_PATTERN.test(value.alias)
|
|
303
|
-
? value.alias.toLowerCase()
|
|
304
|
-
: value.alias,
|
|
305
|
-
...(value.codexThreadId === undefined
|
|
306
|
-
? {}
|
|
307
|
-
: { codexThreadId: value.codexThreadId.toLowerCase() }),
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
case "pair":
|
|
311
|
-
case "unpair": {
|
|
312
|
-
if (Object.hasOwn(value, "aliases")) {
|
|
313
|
-
if (!hasExactKeys(value, ["aliases"], ["threadAttestation"]) ||
|
|
314
|
-
!Array.isArray(value.aliases) ||
|
|
315
|
-
value.aliases.length !== 2 ||
|
|
316
|
-
!isAlias(value.aliases[0]) ||
|
|
317
|
-
!isAlias(value.aliases[1]) ||
|
|
318
|
-
value.aliases[0] === value.aliases[1] ||
|
|
319
|
-
value.aliases[0].slice(value.aliases[0].lastIndexOf("@") + 1) !==
|
|
320
|
-
value.aliases[1].slice(value.aliases[1].lastIndexOf("@") + 1)) {
|
|
321
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
322
|
-
}
|
|
323
|
-
let threadAttestation;
|
|
324
|
-
if (value.threadAttestation !== undefined) {
|
|
325
|
-
if (!isRecord(value.threadAttestation) ||
|
|
326
|
-
!hasExactKeys(value.threadAttestation, ["alias", "threadId"]) ||
|
|
327
|
-
!isAlias(value.threadAttestation.alias) ||
|
|
328
|
-
!value.aliases.includes(value.threadAttestation.alias) ||
|
|
329
|
-
!isUuid(value.threadAttestation.threadId)) {
|
|
330
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
331
|
-
}
|
|
332
|
-
threadAttestation = {
|
|
333
|
-
alias: value.threadAttestation.alias,
|
|
334
|
-
threadId: value.threadAttestation.threadId.toLowerCase(),
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
return {
|
|
338
|
-
aliases: [value.aliases[0], value.aliases[1]],
|
|
339
|
-
...(threadAttestation === undefined ? {} : { threadAttestation }),
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
if (!hasExactKeys(value, ["claudeAlias", "codexAlias"], ["codexThreadId"]) ||
|
|
343
|
-
typeof value.claudeAlias !== "string" ||
|
|
344
|
-
!isClaudeSessionSelector(value.claudeAlias) ||
|
|
345
|
-
!isAlias(value.codexAlias) ||
|
|
346
|
-
!value.codexAlias.startsWith("codex-") ||
|
|
347
|
-
(value.codexThreadId !== undefined && !isUuid(value.codexThreadId)) ||
|
|
348
|
-
value.claudeAlias === value.codexAlias ||
|
|
349
|
-
(isAlias(value.claudeAlias) &&
|
|
350
|
-
value.claudeAlias.slice(value.claudeAlias.lastIndexOf("@") + 1) !==
|
|
351
|
-
value.codexAlias.slice(value.codexAlias.lastIndexOf("@") + 1))) {
|
|
352
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
353
|
-
}
|
|
354
|
-
return {
|
|
355
|
-
claudeAlias: UUID_PATTERN.test(value.claudeAlias)
|
|
356
|
-
? value.claudeAlias.toLowerCase()
|
|
357
|
-
: value.claudeAlias,
|
|
358
|
-
codexAlias: value.codexAlias,
|
|
359
|
-
...(value.codexThreadId === undefined
|
|
360
|
-
? {}
|
|
361
|
-
: { codexThreadId: value.codexThreadId.toLowerCase() }),
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
case "send_to_claude": {
|
|
365
|
-
if (!hasExactKeys(value, ["fromAlias", "threadId", "toAlias", "text"], ["expectsReply", "trackIdleMinutes"]) ||
|
|
366
|
-
!isAlias(value.fromAlias) ||
|
|
367
|
-
!isUuid(value.threadId) ||
|
|
368
|
-
(typeof value.toAlias !== "string" ||
|
|
369
|
-
!isClaudeSessionSelector(value.toAlias)) ||
|
|
370
|
-
!isMessageText(value.text) ||
|
|
371
|
-
(value.expectsReply !== undefined &&
|
|
372
|
-
typeof value.expectsReply !== "boolean") ||
|
|
373
|
-
(value.trackIdleMinutes !== undefined &&
|
|
374
|
-
!isTrackIdleMinutes(value.trackIdleMinutes))) {
|
|
375
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
376
|
-
}
|
|
377
|
-
return {
|
|
378
|
-
fromAlias: value.fromAlias,
|
|
379
|
-
threadId: value.threadId.toLowerCase(),
|
|
380
|
-
toAlias: UUID_PATTERN.test(value.toAlias)
|
|
381
|
-
? value.toAlias.toLowerCase()
|
|
382
|
-
: value.toAlias,
|
|
383
|
-
text: value.text,
|
|
384
|
-
expectsReply: value.expectsReply ?? false,
|
|
385
|
-
...(value.trackIdleMinutes === undefined
|
|
386
|
-
? {}
|
|
387
|
-
: { trackIdleMinutes: value.trackIdleMinutes }),
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
case "send_to_codex": {
|
|
391
|
-
if (!hasExactKeys(value, ["fromAlias", "toAlias", "text"], ["replyAddress", "expectsReply", "trackIdleMinutes"]) ||
|
|
392
|
-
!isAlias(value.fromAlias) ||
|
|
393
|
-
!isAlias(value.toAlias) ||
|
|
394
|
-
!isMessageText(value.text) ||
|
|
395
|
-
(value.replyAddress !== undefined &&
|
|
396
|
-
(typeof value.replyAddress !== "string" ||
|
|
397
|
-
!isGatewayReplyAddress(value.replyAddress))) ||
|
|
398
|
-
(value.expectsReply !== undefined &&
|
|
399
|
-
typeof value.expectsReply !== "boolean") ||
|
|
400
|
-
(value.trackIdleMinutes !== undefined &&
|
|
401
|
-
!isTrackIdleMinutes(value.trackIdleMinutes))) {
|
|
402
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
403
|
-
}
|
|
404
|
-
return {
|
|
405
|
-
fromAlias: value.fromAlias,
|
|
406
|
-
toAlias: value.toAlias,
|
|
407
|
-
text: value.text,
|
|
408
|
-
...(value.replyAddress === undefined
|
|
409
|
-
? {}
|
|
410
|
-
: { replyAddress: value.replyAddress }),
|
|
411
|
-
expectsReply: value.expectsReply ?? false,
|
|
412
|
-
...(value.trackIdleMinutes === undefined
|
|
413
|
-
? {}
|
|
414
|
-
: { trackIdleMinutes: value.trackIdleMinutes }),
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
case "reply": {
|
|
418
|
-
if (!hasExactKeys(value, ["conversationId", "text", "caller"], ["trackIdleMinutes"]) ||
|
|
419
|
-
!isConversationId(value.conversationId) ||
|
|
420
|
-
!isMessageText(value.text) ||
|
|
421
|
-
(value.trackIdleMinutes !== undefined &&
|
|
422
|
-
!isTrackIdleMinutes(value.trackIdleMinutes))) {
|
|
423
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
424
|
-
}
|
|
425
|
-
return {
|
|
426
|
-
conversationId: value.conversationId,
|
|
427
|
-
text: value.text,
|
|
428
|
-
caller: normalizeReplyCaller(value.caller),
|
|
429
|
-
...(value.trackIdleMinutes === undefined
|
|
430
|
-
? {}
|
|
431
|
-
: { trackIdleMinutes: value.trackIdleMinutes }),
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
}
|
|
237
|
+
return { aliases: [value.aliases[0], value.aliases[1]],
|
|
238
|
+
...(threadAttestation === undefined ? {} : { threadAttestation }) };
|
|
239
|
+
}
|
|
240
|
+
if (!exact(value, ["claudeAlias", "codexAlias"], ["codexThreadId"]) ||
|
|
241
|
+
typeof value.claudeAlias !== "string" || !isClaudeSessionSelector(value.claudeAlias) ||
|
|
242
|
+
!alias(value.codexAlias) || !value.codexAlias.startsWith("codex-") ||
|
|
243
|
+
(value.codexThreadId !== undefined && !uuid(value.codexThreadId)) ||
|
|
244
|
+
value.claudeAlias === value.codexAlias ||
|
|
245
|
+
(alias(value.claudeAlias) && value.claudeAlias.slice(value.claudeAlias.lastIndexOf("@") + 1) !==
|
|
246
|
+
value.codexAlias.slice(value.codexAlias.lastIndexOf("@") + 1)))
|
|
247
|
+
invalid();
|
|
248
|
+
return { claudeAlias: UUID_PATTERN.test(value.claudeAlias) ? value.claudeAlias.toLowerCase() : value.claudeAlias,
|
|
249
|
+
codexAlias: value.codexAlias,
|
|
250
|
+
...(value.codexThreadId === undefined ? {} : { codexThreadId: value.codexThreadId.toLowerCase() }) };
|
|
251
|
+
}
|
|
252
|
+
function commonSend(value, required, optional) {
|
|
253
|
+
if (!isRecord(value) || !exact(value, required, optional) || !alias(value.fromAlias) ||
|
|
254
|
+
!alias(value.toAlias) || !messageText(value.text) ||
|
|
255
|
+
(value.expectsReply !== undefined && typeof value.expectsReply !== "boolean") ||
|
|
256
|
+
(value.trackIdleMinutes !== undefined && !trackMinutes(value.trackIdleMinutes)))
|
|
257
|
+
invalid();
|
|
258
|
+
return value;
|
|
435
259
|
}
|
|
436
|
-
function
|
|
437
|
-
if (!isRecord(value) ||
|
|
438
|
-
!
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
260
|
+
function decodeSendClaude(value) {
|
|
261
|
+
if (!isRecord(value) || !exact(value, ["fromAlias", "threadId", "toAlias", "text"], ["expectsReply", "trackIdleMinutes"]) ||
|
|
262
|
+
!alias(value.fromAlias) || !uuid(value.threadId) || typeof value.toAlias !== "string" ||
|
|
263
|
+
!isClaudeSessionSelector(value.toAlias) || !messageText(value.text) ||
|
|
264
|
+
(value.expectsReply !== undefined && typeof value.expectsReply !== "boolean") ||
|
|
265
|
+
(value.trackIdleMinutes !== undefined && !trackMinutes(value.trackIdleMinutes)))
|
|
266
|
+
invalid();
|
|
267
|
+
return { fromAlias: value.fromAlias, threadId: value.threadId.toLowerCase(),
|
|
268
|
+
toAlias: UUID_PATTERN.test(value.toAlias) ? value.toAlias.toLowerCase() : value.toAlias,
|
|
269
|
+
text: value.text, expectsReply: value.expectsReply ?? false,
|
|
270
|
+
...(value.trackIdleMinutes === undefined ? {} : { trackIdleMinutes: value.trackIdleMinutes }) };
|
|
271
|
+
}
|
|
272
|
+
function decodeSendCodex(value) {
|
|
273
|
+
const row = commonSend(value, ["fromAlias", "toAlias", "text"], ["replyAddress", "expectsReply", "trackIdleMinutes"]);
|
|
274
|
+
if (row.replyAddress !== undefined &&
|
|
275
|
+
(typeof row.replyAddress !== "string" || !isGatewayReplyAddress(row.replyAddress)))
|
|
276
|
+
invalid();
|
|
277
|
+
return { fromAlias: row.fromAlias, toAlias: row.toAlias, text: row.text,
|
|
278
|
+
...(row.replyAddress === undefined ? {} : { replyAddress: row.replyAddress }),
|
|
279
|
+
expectsReply: row.expectsReply === true,
|
|
280
|
+
...(row.trackIdleMinutes === undefined ? {} : { trackIdleMinutes: row.trackIdleMinutes }) };
|
|
455
281
|
}
|
|
456
|
-
function
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
282
|
+
function normalizeReplyCaller(value) {
|
|
283
|
+
if (!isRecord(value))
|
|
284
|
+
invalid();
|
|
285
|
+
if (value.kind === "codex" && shape(value, { kind: oneOf("codex"), alias, threadId: uuid }))
|
|
286
|
+
return { kind: "codex", alias: value.alias,
|
|
287
|
+
threadId: value.threadId.toLowerCase() };
|
|
288
|
+
if (value.kind === "claude" && isRecord(value) && exact(value, ["kind", "alias"], ["replyAddress"]) &&
|
|
289
|
+
alias(value.alias) && (value.replyAddress === undefined ||
|
|
290
|
+
(typeof value.replyAddress === "string" && isGatewayReplyAddress(value.replyAddress))))
|
|
291
|
+
return { kind: "claude", alias: value.alias,
|
|
292
|
+
...(value.replyAddress === undefined ? {} : { replyAddress: value.replyAddress }) };
|
|
293
|
+
return invalid();
|
|
294
|
+
}
|
|
295
|
+
function decodeReply(value) {
|
|
296
|
+
if (!isRecord(value) || !exact(value, ["conversationId", "text", "caller"], ["trackIdleMinutes"]) ||
|
|
297
|
+
typeof value.conversationId !== "string" || !isGatewayConversationId(value.conversationId) ||
|
|
298
|
+
!messageText(value.text) ||
|
|
299
|
+
(value.trackIdleMinutes !== undefined && !trackMinutes(value.trackIdleMinutes)))
|
|
300
|
+
invalid();
|
|
301
|
+
return { conversationId: value.conversationId, text: value.text, caller: normalizeReplyCaller(value.caller),
|
|
302
|
+
...(value.trackIdleMinutes === undefined ? {} : { trackIdleMinutes: value.trackIdleMinutes }) };
|
|
303
|
+
}
|
|
304
|
+
function decodeDeliveryStatus(value) {
|
|
305
|
+
if (!shape(value, { token: (item) => typeof item === "string" && DELIVERY_TOKEN_PATTERN.test(item) }))
|
|
306
|
+
invalid();
|
|
307
|
+
return { token: value.token };
|
|
308
|
+
}
|
|
309
|
+
function decodeUntrack(value) {
|
|
310
|
+
if (!shape(value, { conversationId: (item) => typeof item === "string" && CONVERSATION_ID_PATTERN.test(item) }))
|
|
311
|
+
invalid();
|
|
312
|
+
return { conversationId: value.conversationId };
|
|
465
313
|
}
|
|
466
314
|
function isDecision(value) {
|
|
467
|
-
|
|
468
|
-
!hasExactKeys(value, ["accepted", "code"]) ||
|
|
469
|
-
typeof value.accepted !== "boolean" ||
|
|
470
|
-
typeof value.code !== "string") {
|
|
471
|
-
return false;
|
|
472
|
-
}
|
|
473
|
-
if (value.accepted)
|
|
474
|
-
return value.code === "ok";
|
|
475
|
-
return (value.code === "not_found" ||
|
|
476
|
-
value.code === "conflict" ||
|
|
477
|
-
value.code === "watch_owner_conflict" ||
|
|
478
|
-
value.code === "route_mismatch" ||
|
|
479
|
-
value.code === "busy" ||
|
|
480
|
-
value.code === "unavailable" ||
|
|
481
|
-
value.code === "rejected");
|
|
315
|
+
return shape(value, { accepted: (item) => typeof item === "boolean", code: oneOf("ok", "not_found", "conflict", "watch_owner_conflict", "route_mismatch", "busy", "unavailable", "rejected") }) && (value.accepted === true ? value.code === "ok" : value.code !== "ok");
|
|
482
316
|
}
|
|
483
317
|
function isHealthResult(value) {
|
|
484
|
-
return (
|
|
485
|
-
hasExactKeys(value, ["status", "revision"]) &&
|
|
486
|
-
(value.status === "ok" || value.status === "degraded") &&
|
|
487
|
-
isNonNegativeInteger(value.revision));
|
|
318
|
+
return shape(value, { status: oneOf("ok", "degraded"), revision: nonNegative });
|
|
488
319
|
}
|
|
489
320
|
function isSendResult(value) {
|
|
490
|
-
if (!isRecord(value))
|
|
491
|
-
return
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
"code",
|
|
496
|
-
"conversationId",
|
|
497
|
-
"deliveryToken",
|
|
498
|
-
]) &&
|
|
499
|
-
value.code === "ok" &&
|
|
500
|
-
isConversationId(value.conversationId) &&
|
|
501
|
-
typeof value.deliveryToken === "string" &&
|
|
502
|
-
isGatewayDeliveryToken(value.deliveryToken));
|
|
503
|
-
}
|
|
504
|
-
return isDecision(value);
|
|
321
|
+
if (!isRecord(value) || value.accepted !== true)
|
|
322
|
+
return isDecision(value);
|
|
323
|
+
return shape(value, { accepted: oneOf(true), code: oneOf("ok"),
|
|
324
|
+
conversationId: (item) => typeof item === "string" && CONVERSATION_ID_PATTERN.test(item),
|
|
325
|
+
deliveryToken: (item) => typeof item === "string" && DELIVERY_TOKEN_PATTERN.test(item) });
|
|
505
326
|
}
|
|
506
327
|
function isRefreshResult(value) {
|
|
507
|
-
|
|
508
|
-
!hasExactKeys(value, ["accepted", "code", "revision"]) ||
|
|
509
|
-
!isNonNegativeInteger(value.revision)) {
|
|
510
|
-
return false;
|
|
511
|
-
}
|
|
512
|
-
const decision = { accepted: value.accepted, code: value.code };
|
|
513
|
-
return isDecision(decision);
|
|
514
|
-
}
|
|
515
|
-
function isConnectorHealth(value) {
|
|
516
|
-
return (value === "offline" ||
|
|
517
|
-
value === "connecting" ||
|
|
518
|
-
value === "healthy" ||
|
|
519
|
-
value === "degraded");
|
|
520
|
-
}
|
|
521
|
-
function isSafeCode(value) {
|
|
522
|
-
return typeof value === "string" && SAFE_CODE_PATTERN.test(value);
|
|
523
|
-
}
|
|
524
|
-
const TERMINAL_DELIVERY_STATUS_STATES = new Set([
|
|
525
|
-
"delivered",
|
|
526
|
-
"unconfirmed",
|
|
527
|
-
"expired",
|
|
528
|
-
"failed",
|
|
529
|
-
"ambiguous",
|
|
530
|
-
"cancelled",
|
|
531
|
-
]);
|
|
532
|
-
function isDeliveryStatusState(value) {
|
|
533
|
-
return (value === "queued" ||
|
|
534
|
-
value === "stalled" ||
|
|
535
|
-
value === "delivered" ||
|
|
536
|
-
value === "unconfirmed" ||
|
|
537
|
-
value === "expired" ||
|
|
538
|
-
value === "failed" ||
|
|
539
|
-
value === "ambiguous" ||
|
|
540
|
-
value === "cancelled");
|
|
328
|
+
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 });
|
|
541
329
|
}
|
|
330
|
+
const TERMINAL = new Set(["delivered", "unconfirmed", "expired", "failed", "ambiguous", "cancelled"]);
|
|
542
331
|
function isDeliveryStatusResult(value) {
|
|
543
332
|
if (!isRecord(value) || typeof value.found !== "boolean")
|
|
544
333
|
return false;
|
|
545
334
|
if (!value.found)
|
|
546
|
-
return
|
|
547
|
-
|
|
548
|
-
!isDeliveryStatusState(value.state) ||
|
|
549
|
-
typeof value.terminal !== "boolean" ||
|
|
550
|
-
value.terminal !== TERMINAL_DELIVERY_STATUS_STATES.has(value.state) ||
|
|
551
|
-
!isIsoTimestamp(value.updatedAt) ||
|
|
552
|
-
!isIsoTimestamp(value.deadlineAt) ||
|
|
553
|
-
(value.pendingForMs !== undefined &&
|
|
554
|
-
!isNonNegativeInteger(value.pendingForMs)) ||
|
|
555
|
-
(value.safeErrorCode !== undefined && !isSafeCode(value.safeErrorCode))) {
|
|
556
|
-
return false;
|
|
557
|
-
}
|
|
558
|
-
return true;
|
|
335
|
+
return exact(value, ["found"]);
|
|
336
|
+
return shape(value, { found: oneOf(true), state: oneOf("queued", "stalled", "delivered", "unconfirmed", "expired", "failed", "ambiguous", "cancelled"), terminal: (item) => typeof item === "boolean", updatedAt: iso, deadlineAt: iso }, { pendingForMs: nonNegative, safeErrorCode: safeCode }) && value.terminal === TERMINAL.has(value.state);
|
|
559
337
|
}
|
|
560
338
|
function isRouteCounters(value) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
"delivered",
|
|
565
|
-
"unconfirmed",
|
|
566
|
-
"failed",
|
|
567
|
-
"ambiguous",
|
|
568
|
-
"expired",
|
|
569
|
-
"cancelled",
|
|
570
|
-
"abandoned",
|
|
571
|
-
"rejected",
|
|
572
|
-
"bytesAccepted",
|
|
573
|
-
])) {
|
|
574
|
-
return false;
|
|
575
|
-
}
|
|
576
|
-
return Object.values(value).every(isNonNegativeInteger);
|
|
339
|
+
const keys = ["accepted", "delivered", "unconfirmed", "failed", "ambiguous", "expired",
|
|
340
|
+
"cancelled", "abandoned", "rejected", "bytesAccepted"];
|
|
341
|
+
return isRecord(value) && exact(value, keys) && keys.every((key) => nonNegative(value[key]));
|
|
577
342
|
}
|
|
343
|
+
const isConnectorHealth = oneOf("offline", "connecting", "healthy", "degraded");
|
|
578
344
|
function isConnectorSnapshot(value) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
"codexDoctor",
|
|
590
|
-
"safeErrorCode",
|
|
591
|
-
"registry",
|
|
592
|
-
]) &&
|
|
593
|
-
isGatewayProvider(value.provider) &&
|
|
594
|
-
isHostId(value.host) &&
|
|
595
|
-
isConnectorHealth(value.health) &&
|
|
596
|
-
typeof value.protocol === "string" &&
|
|
597
|
-
PROTOCOL_PATTERN.test(value.protocol) &&
|
|
598
|
-
typeof value.protocolVersion === "string" &&
|
|
599
|
-
PROTOCOL_VERSION_PATTERN.test(value.protocolVersion) &&
|
|
600
|
-
(value.lastSeenAt === undefined || isIsoTimestamp(value.lastSeenAt)) &&
|
|
601
|
-
(value.observationAgeMs === undefined ||
|
|
602
|
-
isNonNegativeInteger(value.observationAgeMs)) &&
|
|
603
|
-
(value.codexDoctor === undefined ||
|
|
604
|
-
(value.provider === "codex" &&
|
|
605
|
-
isRecord(value.codexDoctor) &&
|
|
606
|
-
hasExactKeys(value.codexDoctor, ["conditions"]) &&
|
|
607
|
-
Array.isArray(value.codexDoctor.conditions) &&
|
|
608
|
-
value.codexDoctor.conditions.length > 0 &&
|
|
609
|
-
value.codexDoctor.conditions.length <= 2 &&
|
|
610
|
-
value.codexDoctor.conditions.every((condition) => [
|
|
611
|
-
"split_brain",
|
|
612
|
-
"orphaned",
|
|
613
|
-
"attached",
|
|
614
|
-
"observation_stale",
|
|
615
|
-
"unknown",
|
|
616
|
-
].includes(condition)))) &&
|
|
617
|
-
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)) &&
|
|
618
|
-
(value.registry === undefined ||
|
|
619
|
-
(value.provider === "claude" &&
|
|
620
|
-
isPublicRegistryObservationSnapshot(value.registry))));
|
|
345
|
+
if (!shape(value, { provider: isGatewayProvider, host, health: isConnectorHealth,
|
|
346
|
+
protocol: (item) => typeof item === "string" && PROTOCOL_PATTERN.test(item),
|
|
347
|
+
protocolVersion: (item) => typeof item === "string" && PROTOCOL_VERSION_PATTERN.test(item) }, { lastSeenAt: iso, observationAgeMs: nonNegative, safeErrorCode: safeCode,
|
|
348
|
+
registry: isPublicRegistryObservationSnapshot, codexDoctor: (item) => shape(item, {
|
|
349
|
+
conditions: (rows) => Array.isArray(rows) && rows.length > 0 && rows.length <= 2 &&
|
|
350
|
+
rows.every(oneOf("split_brain", "orphaned", "attached", "observation_stale", "managed_layout_missing", "unknown")),
|
|
351
|
+
}) }))
|
|
352
|
+
return false;
|
|
353
|
+
return (value.codexDoctor === undefined || value.provider === "codex") &&
|
|
354
|
+
(value.registry === undefined || value.provider === "claude");
|
|
621
355
|
}
|
|
622
356
|
function isRouteSnapshot(value) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
"busyPolicy",
|
|
631
|
-
"queueDepth",
|
|
632
|
-
"counters",
|
|
633
|
-
], ["lastSeenAt", "oldestQueuedAt", "safeErrorCode"]) &&
|
|
634
|
-
isAlias(value.alias) &&
|
|
635
|
-
isGatewayProvider(value.provider) &&
|
|
636
|
-
isHostId(value.host) &&
|
|
637
|
-
value.alias.endsWith(`@${value.host}`) &&
|
|
638
|
-
typeof value.enabled === "boolean" &&
|
|
639
|
-
(value.state === "stale" ||
|
|
640
|
-
value.state === "idle" ||
|
|
641
|
-
value.state === "busy" ||
|
|
642
|
-
value.state === "awaiting_approval" ||
|
|
643
|
-
value.state === "offline" ||
|
|
644
|
-
value.state === "disabled") &&
|
|
645
|
-
value.busyPolicy === "queue" &&
|
|
646
|
-
(value.lastSeenAt === undefined || isIsoTimestamp(value.lastSeenAt)) &&
|
|
647
|
-
isNonNegativeInteger(value.queueDepth) &&
|
|
648
|
-
(value.oldestQueuedAt === undefined
|
|
649
|
-
? value.queueDepth === 0
|
|
650
|
-
: value.queueDepth > 0 && isIsoTimestamp(value.oldestQueuedAt)) &&
|
|
651
|
-
isRouteCounters(value.counters) &&
|
|
652
|
-
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)));
|
|
653
|
-
}
|
|
654
|
-
function isConsentEndpoint(value) {
|
|
655
|
-
return (isRecord(value) &&
|
|
656
|
-
hasExactKeys(value, ["alias", "provider"]) &&
|
|
657
|
-
isAlias(value.alias) &&
|
|
658
|
-
isGatewayProvider(value.provider));
|
|
357
|
+
if (!shape(value, { alias, provider: isGatewayProvider, host, enabled: (item) => typeof item === "boolean",
|
|
358
|
+
state: oneOf("stale", "idle", "busy", "awaiting_approval", "offline", "disabled"),
|
|
359
|
+
busyPolicy: oneOf("queue"), queueDepth: nonNegative, counters: isRouteCounters }, { lastSeenAt: iso, oldestQueuedAt: iso, safeErrorCode: safeCode }))
|
|
360
|
+
return false;
|
|
361
|
+
const row = value;
|
|
362
|
+
return row.alias.endsWith(`@${row.host}`) &&
|
|
363
|
+
(row.oldestQueuedAt === undefined ? row.queueDepth === 0 : row.queueDepth > 0);
|
|
659
364
|
}
|
|
660
365
|
function compareConsentEndpoints(left, right) {
|
|
661
|
-
|
|
662
|
-
gatewayProviders.indexOf(right.provider);
|
|
663
|
-
return providerDelta || left.alias.localeCompare(right.alias);
|
|
366
|
+
return gatewayProviders.indexOf(left.provider) -
|
|
367
|
+
gatewayProviders.indexOf(right.provider) || left.alias.localeCompare(right.alias);
|
|
664
368
|
}
|
|
665
369
|
function isConsentEdgeSnapshot(value) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
value.endpoints.length !== 2 ||
|
|
670
|
-
!isConsentEndpoint(value.endpoints[0]) ||
|
|
671
|
-
!isConsentEndpoint(value.endpoints[1])) {
|
|
370
|
+
const endpoint = (item) => shape(item, { alias, provider: isGatewayProvider });
|
|
371
|
+
if (!shape(value, { endpoints: (rows) => Array.isArray(rows) && rows.length === 2 && rows.every(endpoint),
|
|
372
|
+
host, counters: isRouteCounters }))
|
|
672
373
|
return false;
|
|
673
|
-
}
|
|
674
374
|
const [left, right] = value.endpoints;
|
|
675
|
-
return
|
|
676
|
-
left.alias
|
|
677
|
-
compareConsentEndpoints(left, right) < 0 &&
|
|
678
|
-
isHostId(value.host) &&
|
|
679
|
-
left.alias.endsWith(`@${value.host}`) &&
|
|
680
|
-
right.alias.endsWith(`@${value.host}`) &&
|
|
681
|
-
isRouteCounters(value.counters));
|
|
375
|
+
return left.provider !== right.provider && left.alias !== right.alias && compareConsentEndpoints(left, right) < 0 &&
|
|
376
|
+
left.alias.endsWith(`@${value.host}`) && right.alias.endsWith(`@${value.host}`);
|
|
682
377
|
}
|
|
683
378
|
function isAvailablePeerSnapshot(value) {
|
|
684
|
-
return (
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
"host",
|
|
689
|
-
"state",
|
|
690
|
-
"validated",
|
|
691
|
-
"selected",
|
|
692
|
-
], ["lastSeenAt", "safeErrorCode"]) &&
|
|
693
|
-
isAlias(value.alias) &&
|
|
694
|
-
value.provider === "claude" &&
|
|
695
|
-
isHostId(value.host) &&
|
|
696
|
-
value.alias.endsWith(`@${value.host}`) &&
|
|
697
|
-
(value.state === "idle" ||
|
|
698
|
-
value.state === "busy" ||
|
|
699
|
-
value.state === "awaiting_approval" ||
|
|
700
|
-
value.state === "offline") &&
|
|
701
|
-
typeof value.validated === "boolean" &&
|
|
702
|
-
typeof value.selected === "boolean" &&
|
|
703
|
-
(value.lastSeenAt === undefined || isIsoTimestamp(value.lastSeenAt)) &&
|
|
704
|
-
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)));
|
|
379
|
+
return shape(value, { alias, provider: oneOf("claude"), host,
|
|
380
|
+
state: oneOf("idle", "busy", "awaiting_approval", "offline"),
|
|
381
|
+
validated: (item) => typeof item === "boolean", selected: (item) => typeof item === "boolean" }, { lastSeenAt: iso, safeErrorCode: safeCode }) &&
|
|
382
|
+
value.alias.endsWith(`@${String(value.host)}`);
|
|
705
383
|
}
|
|
706
384
|
function isNormalizedMessageEvent(value) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
"bytes",
|
|
717
|
-
], ["conversationIdSuffix", "body", "latencyMs", "safeErrorCode", "steer"]) &&
|
|
718
|
-
isNonNegativeInteger(value.sequence) &&
|
|
719
|
-
isIsoTimestamp(value.timestamp) &&
|
|
720
|
-
typeof value.messageIdSuffix === "string" &&
|
|
721
|
-
MESSAGE_SUFFIX_PATTERN.test(value.messageIdSuffix) &&
|
|
722
|
-
(value.conversationIdSuffix === undefined ||
|
|
723
|
-
(typeof value.conversationIdSuffix === "string" &&
|
|
724
|
-
CONVERSATION_SUFFIX_PATTERN.test(value.conversationIdSuffix))) &&
|
|
725
|
-
isMessageDirection(value.direction) &&
|
|
726
|
-
isAlias(value.sourceAlias) &&
|
|
727
|
-
isAlias(value.targetAlias) &&
|
|
728
|
-
(value.state === "queued" ||
|
|
729
|
-
value.state === "duplicate" ||
|
|
730
|
-
value.state === "dispatching" ||
|
|
731
|
-
value.state === "transport_written" ||
|
|
732
|
-
value.state === "held" ||
|
|
733
|
-
value.state === "delivered" ||
|
|
734
|
-
value.state === "unconfirmed" ||
|
|
735
|
-
value.state === "failed" ||
|
|
736
|
-
value.state === "ambiguous" ||
|
|
737
|
-
value.state === "expired" ||
|
|
738
|
-
value.state === "cancelled" ||
|
|
739
|
-
value.state === "abandoned" ||
|
|
740
|
-
value.state === "rejected") &&
|
|
741
|
-
isNonNegativeInteger(value.bytes) &&
|
|
742
|
-
(value.body === undefined ||
|
|
743
|
-
(typeof value.body === "string" &&
|
|
744
|
-
value.body.length > 0 &&
|
|
745
|
-
!value.body.includes("\u0000") &&
|
|
746
|
-
Buffer.byteLength(value.body, "utf8") === value.bytes)) &&
|
|
747
|
-
(value.latencyMs === undefined || isNonNegativeInteger(value.latencyMs)) &&
|
|
748
|
-
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)) &&
|
|
749
|
-
(value.steer === undefined || value.steer === true));
|
|
385
|
+
if (!shape(value, { sequence: nonNegative, timestamp: iso,
|
|
386
|
+
messageIdSuffix: (item) => typeof item === "string" && MESSAGE_SUFFIX_PATTERN.test(item),
|
|
387
|
+
direction: isMessageDirection, sourceAlias: alias, targetAlias: alias,
|
|
388
|
+
state: oneOf("queued", "duplicate", "dispatching", "transport_written", "held", "delivered", "unconfirmed", "failed", "ambiguous", "expired", "cancelled", "abandoned", "rejected"),
|
|
389
|
+
bytes: nonNegative }, { conversationIdSuffix: (item) => typeof item === "string" && CONVERSATION_SUFFIX_PATTERN.test(item),
|
|
390
|
+
body: (item) => typeof item === "string" && item.length > 0 && !item.includes("\0"),
|
|
391
|
+
latencyMs: nonNegative, safeErrorCode: safeCode, steer: oneOf(true) }))
|
|
392
|
+
return false;
|
|
393
|
+
return value.body === undefined || Buffer.byteLength(value.body, "utf8") === value.bytes;
|
|
750
394
|
}
|
|
751
395
|
function isAccounting(value) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
"duplicates",
|
|
756
|
-
"delivered",
|
|
757
|
-
"unconfirmed",
|
|
758
|
-
"failed",
|
|
759
|
-
"ambiguous",
|
|
760
|
-
"expired",
|
|
761
|
-
"cancelled",
|
|
762
|
-
"abandoned",
|
|
763
|
-
"rejected",
|
|
764
|
-
"bytesAccepted",
|
|
765
|
-
"queuedBytes",
|
|
766
|
-
])) {
|
|
767
|
-
return false;
|
|
768
|
-
}
|
|
769
|
-
return Object.values(value).every(isNonNegativeInteger);
|
|
396
|
+
const keys = ["accepted", "duplicates", "delivered", "unconfirmed", "failed", "ambiguous",
|
|
397
|
+
"expired", "cancelled", "abandoned", "rejected", "bytesAccepted", "queuedBytes"];
|
|
398
|
+
return isRecord(value) && exact(value, keys) && keys.every((key) => nonNegative(value[key]));
|
|
770
399
|
}
|
|
771
400
|
function isSafeAlert(value) {
|
|
772
|
-
return (
|
|
773
|
-
hasExactKeys(value, ["code", "severity", "timestamp"], [
|
|
774
|
-
"provider",
|
|
775
|
-
"host",
|
|
776
|
-
"alias",
|
|
777
|
-
]) &&
|
|
778
|
-
isSafeCode(value.code) &&
|
|
779
|
-
(value.severity === "info" ||
|
|
780
|
-
value.severity === "warning" ||
|
|
781
|
-
value.severity === "error") &&
|
|
782
|
-
isIsoTimestamp(value.timestamp) &&
|
|
783
|
-
(value.provider === undefined || isGatewayProvider(value.provider)) &&
|
|
784
|
-
(value.host === undefined || isHostId(value.host)) &&
|
|
785
|
-
(value.alias === undefined || isAlias(value.alias)));
|
|
401
|
+
return shape(value, { code: safeCode, severity: oneOf("info", "warning", "error"), timestamp: iso }, { provider: isGatewayProvider, host, alias });
|
|
786
402
|
}
|
|
787
403
|
function isProgressWatchSnapshot(value) {
|
|
788
|
-
return (
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
"lastActivityAt",
|
|
794
|
-
"nextActionAt",
|
|
795
|
-
"nudgeCount",
|
|
796
|
-
]) &&
|
|
797
|
-
typeof value.conversationIdSuffix === "string" &&
|
|
798
|
-
CONVERSATION_SUFFIX_PATTERN.test(value.conversationIdSuffix) &&
|
|
799
|
-
isAlias(value.ownerAlias) &&
|
|
800
|
-
isAlias(value.workerAlias) &&
|
|
801
|
-
value.ownerAlias !== value.workerAlias &&
|
|
802
|
-
isIsoTimestamp(value.lastActivityAt) &&
|
|
803
|
-
isIsoTimestamp(value.nextActionAt) &&
|
|
804
|
-
(value.nudgeCount === 0 || value.nudgeCount === 1 || value.nudgeCount === 2));
|
|
404
|
+
return shape(value, {
|
|
405
|
+
conversationIdSuffix: (item) => typeof item === "string" && CONVERSATION_SUFFIX_PATTERN.test(item),
|
|
406
|
+
ownerAlias: alias, workerAlias: alias, lastActivityAt: iso, nextActionAt: iso,
|
|
407
|
+
nudgeCount: oneOf(0, 1, 2),
|
|
408
|
+
}) && value.ownerAlias !== value.workerAlias;
|
|
805
409
|
}
|
|
806
410
|
function isProgressWatchEventSnapshot(value) {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
isAlias(value.ownerAlias) &&
|
|
822
|
-
isAlias(value.workerAlias) &&
|
|
823
|
-
((value.kind === "opened" &&
|
|
824
|
-
value.actor === "owner" &&
|
|
825
|
-
value.reason === undefined) ||
|
|
826
|
-
(value.kind === "replaced" &&
|
|
827
|
-
(value.actor === "owner" || value.actor === "unknown") &&
|
|
828
|
-
value.reason === undefined) ||
|
|
829
|
-
(value.kind === "settled" &&
|
|
830
|
-
((value.reason === "done" &&
|
|
831
|
-
(value.actor === "owner" || value.actor === "worker")) ||
|
|
832
|
-
(value.reason === "untracked" && value.actor === "operator") ||
|
|
833
|
-
((value.reason === "idle_timeout" ||
|
|
834
|
-
value.reason === "tracking_disabled") &&
|
|
835
|
-
value.actor === "gateway") ||
|
|
836
|
-
(value.reason === "endpoint_retired" &&
|
|
837
|
-
(value.actor === "gateway" || value.actor === "operator")) ||
|
|
838
|
-
(value.reason === "pair_removed" &&
|
|
839
|
-
value.actor === "operator")))));
|
|
411
|
+
if (!shape(value, { sequence: nonNegative, timestamp: iso,
|
|
412
|
+
conversationIdSuffix: (item) => typeof item === "string" && CONVERSATION_SUFFIX_PATTERN.test(item),
|
|
413
|
+
ownerAlias: alias, workerAlias: alias, kind: oneOf("opened", "replaced", "settled"),
|
|
414
|
+
actor: oneOf("owner", "worker", "operator", "gateway", "unknown") }, { reason: oneOf("done", "untracked", "idle_timeout", "tracking_disabled", "endpoint_retired", "pair_removed") }))
|
|
415
|
+
return false;
|
|
416
|
+
if (value.kind === "opened")
|
|
417
|
+
return value.actor === "owner" && value.reason === undefined;
|
|
418
|
+
if (value.kind === "replaced")
|
|
419
|
+
return (value.actor === "owner" || value.actor === "unknown") && value.reason === undefined;
|
|
420
|
+
return (value.reason === "done" && (value.actor === "owner" || value.actor === "worker")) ||
|
|
421
|
+
(value.reason === "untracked" && value.actor === "operator") ||
|
|
422
|
+
((value.reason === "idle_timeout" || value.reason === "tracking_disabled") && value.actor === "gateway") ||
|
|
423
|
+
(value.reason === "endpoint_retired" && (value.actor === "gateway" || value.actor === "operator")) ||
|
|
424
|
+
(value.reason === "pair_removed" && value.actor === "operator");
|
|
840
425
|
}
|
|
841
426
|
function isGatewayActivityEvent(value) {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
"action",
|
|
848
|
-
"outcome",
|
|
849
|
-
"aliases",
|
|
850
|
-
"operatorAction",
|
|
851
|
-
], ["safeErrorCode"]) &&
|
|
852
|
-
isNonNegativeInteger(value.sequence) &&
|
|
853
|
-
value.sequence > 0 &&
|
|
854
|
-
isIsoTimestamp(value.timestamp) &&
|
|
855
|
-
isGatewayActivityIdentity(value.kind, value.action, value.operatorAction) &&
|
|
856
|
-
(value.outcome === "accepted" || value.outcome === "rejected") &&
|
|
857
|
-
Array.isArray(value.aliases) &&
|
|
858
|
-
value.aliases.length <= 2 &&
|
|
859
|
-
value.aliases.every(isAlias) &&
|
|
860
|
-
new Set(value.aliases).size === value.aliases.length &&
|
|
861
|
-
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)));
|
|
862
|
-
}
|
|
863
|
-
function isGatewayActivityIdentity(kind, action, operatorAction) {
|
|
864
|
-
if (typeof operatorAction !== "boolean")
|
|
427
|
+
if (!shape(value, { sequence: positive, timestamp: iso,
|
|
428
|
+
kind: oneOf("discovery", "selection", "registration", "pairing", "watch"),
|
|
429
|
+
action: oneOf("discovery_refreshed", "claude_selected", "claude_unselected", "codex_registered", "codex_succeeded", "codex_unregistered", "routes_paired", "routes_unpaired", "watch_ended"),
|
|
430
|
+
outcome: oneOf("accepted", "rejected"), aliases: (rows) => arrayOf(rows, 2, alias),
|
|
431
|
+
operatorAction: oneOf(true) }, { safeErrorCode: safeCode }))
|
|
865
432
|
return false;
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
return ((action === "codex_registered" ||
|
|
874
|
-
action === "codex_succeeded" ||
|
|
875
|
-
action === "codex_unregistered") &&
|
|
876
|
-
operatorAction);
|
|
877
|
-
case "pairing":
|
|
878
|
-
return ((action === "routes_paired" || action === "routes_unpaired") &&
|
|
879
|
-
operatorAction);
|
|
880
|
-
case "watch":
|
|
881
|
-
return action === "watch_ended" && operatorAction;
|
|
882
|
-
case "endpoint":
|
|
883
|
-
return action === "endpoint_refreshed" && !operatorAction;
|
|
884
|
-
case "recovery":
|
|
885
|
-
return action === "codex_orphan_removed" && operatorAction;
|
|
886
|
-
default:
|
|
887
|
-
return false;
|
|
888
|
-
}
|
|
433
|
+
const allowed = {
|
|
434
|
+
discovery: ["discovery_refreshed"], selection: ["claude_selected", "claude_unselected"],
|
|
435
|
+
registration: ["codex_registered", "codex_succeeded", "codex_unregistered"],
|
|
436
|
+
pairing: ["routes_paired", "routes_unpaired"], watch: ["watch_ended"],
|
|
437
|
+
};
|
|
438
|
+
return new Set(value.aliases).size === value.aliases.length &&
|
|
439
|
+
(allowed[value.kind]?.includes(value.action) ?? false);
|
|
889
440
|
}
|
|
890
441
|
function isDeadlinePressure(value) {
|
|
891
|
-
const names = [
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
value.configuredDeadlineMs > 0 &&
|
|
902
|
-
(value.retainedSince === undefined || isIsoTimestamp(value.retainedSince)) &&
|
|
903
|
-
isNonNegativeInteger(value.terminalEvents) &&
|
|
904
|
-
isNonNegativeInteger(value.expiredEvents) &&
|
|
905
|
-
value.expiredEvents <= value.terminalEvents &&
|
|
906
|
-
Array.isArray(value.buckets) &&
|
|
907
|
-
value.buckets.length === names.length &&
|
|
908
|
-
value.buckets.every((bucket, index) => isRecord(bucket) &&
|
|
909
|
-
hasExactKeys(bucket, ["bucket", "settled", "expired"]) &&
|
|
910
|
-
bucket.bucket === names[index] &&
|
|
911
|
-
isNonNegativeInteger(bucket.settled) &&
|
|
912
|
-
isNonNegativeInteger(bucket.expired) &&
|
|
913
|
-
bucket.expired <= bucket.settled) &&
|
|
914
|
-
value.buckets.reduce((sum, bucket) => sum + Number(bucket.settled), 0) ===
|
|
915
|
-
value.terminalEvents &&
|
|
916
|
-
value.buckets.reduce((sum, bucket) => sum + Number(bucket.expired), 0) ===
|
|
917
|
-
value.expiredEvents);
|
|
442
|
+
const names = ["under_1m", "1m_to_5m", "5m_to_15m", "15m_to_60m", "over_60m"];
|
|
443
|
+
if (!shape(value, { configuredDeadlineMs: positive, terminalEvents: nonNegative,
|
|
444
|
+
expiredEvents: nonNegative, buckets: (rows) => Array.isArray(rows) && rows.length === names.length }, { retainedSince: iso }) || Number(value.expiredEvents) > Number(value.terminalEvents))
|
|
445
|
+
return false;
|
|
446
|
+
const rows = value.buckets;
|
|
447
|
+
if (!rows.every((row, index) => shape(row, { bucket: oneOf(names[index]), settled: nonNegative, expired: nonNegative }) &&
|
|
448
|
+
row.expired <= row.settled))
|
|
449
|
+
return false;
|
|
450
|
+
return rows.reduce((sum, row) => sum + Number(row.settled), 0) === value.terminalEvents &&
|
|
451
|
+
rows.reduce((sum, row) => sum + Number(row.expired), 0) === value.expiredEvents;
|
|
918
452
|
}
|
|
919
453
|
function isSnapshotTruncation(value) {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
"routes",
|
|
925
|
-
"consentEdges",
|
|
926
|
-
"messages",
|
|
927
|
-
"alerts",
|
|
928
|
-
], [
|
|
929
|
-
"progressWatches",
|
|
930
|
-
"progressWatchEvents",
|
|
931
|
-
"activityEvents",
|
|
932
|
-
]) &&
|
|
933
|
-
isNonNegativeInteger(value.connectors) &&
|
|
934
|
-
isNonNegativeInteger(value.availablePeers) &&
|
|
935
|
-
isNonNegativeInteger(value.routes) &&
|
|
936
|
-
isNonNegativeInteger(value.consentEdges) &&
|
|
937
|
-
isNonNegativeInteger(value.messages) &&
|
|
938
|
-
isNonNegativeInteger(value.alerts) &&
|
|
939
|
-
(value.progressWatches === undefined ||
|
|
940
|
-
isNonNegativeInteger(value.progressWatches)) &&
|
|
941
|
-
(value.progressWatchEvents === undefined ||
|
|
942
|
-
isNonNegativeInteger(value.progressWatchEvents)) &&
|
|
943
|
-
(value.activityEvents === undefined ||
|
|
944
|
-
isNonNegativeInteger(value.activityEvents)));
|
|
454
|
+
const required = ["connectors", "availablePeers", "routes", "consentEdges", "messages", "alerts"];
|
|
455
|
+
const optional = ["progressWatches", "progressWatchEvents", "activityEvents"];
|
|
456
|
+
return isRecord(value) && exact(value, required, optional) &&
|
|
457
|
+
[...required, ...optional].every((key) => value[key] === undefined || nonNegative(value[key]));
|
|
945
458
|
}
|
|
946
459
|
export function isGatewaySnapshot(value) {
|
|
947
|
-
if (!
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
"alerts",
|
|
960
|
-
"truncation",
|
|
961
|
-
], [
|
|
962
|
-
"progressWatches",
|
|
963
|
-
"progressWatchEvents",
|
|
964
|
-
"activityEvents",
|
|
965
|
-
"deadlinePressure",
|
|
966
|
-
]) ||
|
|
967
|
-
value.schemaVersion !== 2 ||
|
|
968
|
-
!isIsoTimestamp(value.generatedAt) ||
|
|
969
|
-
(value.inboundMode !== "paired" && value.inboundMode !== "open") ||
|
|
970
|
-
!isConnectorHealth(value.health) ||
|
|
971
|
-
!Array.isArray(value.connectors) ||
|
|
972
|
-
value.connectors.length > gatewayPublicSnapshotLimits.connectors ||
|
|
973
|
-
!value.connectors.every(isConnectorSnapshot) ||
|
|
974
|
-
!Array.isArray(value.availablePeers) ||
|
|
975
|
-
value.availablePeers.length > gatewayPublicSnapshotLimits.availablePeers ||
|
|
976
|
-
!value.availablePeers.every(isAvailablePeerSnapshot) ||
|
|
977
|
-
!Array.isArray(value.routes) ||
|
|
978
|
-
value.routes.length > gatewayPublicSnapshotLimits.routes ||
|
|
979
|
-
!value.routes.every(isRouteSnapshot) ||
|
|
980
|
-
!Array.isArray(value.consentEdges) ||
|
|
981
|
-
value.consentEdges.length > gatewayPublicSnapshotLimits.consentEdges ||
|
|
982
|
-
!value.consentEdges.every(isConsentEdgeSnapshot) ||
|
|
983
|
-
(value.progressWatches !== undefined &&
|
|
984
|
-
(!Array.isArray(value.progressWatches) ||
|
|
985
|
-
value.progressWatches.length >
|
|
986
|
-
gatewayPublicSnapshotLimits.progressWatches ||
|
|
987
|
-
!value.progressWatches.every(isProgressWatchSnapshot))) ||
|
|
988
|
-
(value.progressWatchEvents !== undefined &&
|
|
989
|
-
(!Array.isArray(value.progressWatchEvents) ||
|
|
990
|
-
value.progressWatchEvents.length >
|
|
991
|
-
gatewayPublicSnapshotLimits.progressWatchEvents ||
|
|
992
|
-
!value.progressWatchEvents.every(isProgressWatchEventSnapshot))) ||
|
|
993
|
-
(value.activityEvents !== undefined &&
|
|
994
|
-
(!Array.isArray(value.activityEvents) ||
|
|
995
|
-
value.activityEvents.length >
|
|
996
|
-
gatewayPublicSnapshotLimits.activityEvents ||
|
|
997
|
-
!value.activityEvents.every(isGatewayActivityEvent))) ||
|
|
998
|
-
(value.deadlinePressure !== undefined &&
|
|
999
|
-
!isDeadlinePressure(value.deadlinePressure)) ||
|
|
1000
|
-
!Array.isArray(value.messages) ||
|
|
1001
|
-
value.messages.length > gatewayPublicSnapshotLimits.messages ||
|
|
1002
|
-
!value.messages.every(isNormalizedMessageEvent) ||
|
|
1003
|
-
!isAccounting(value.accounting) ||
|
|
1004
|
-
!Array.isArray(value.alerts) ||
|
|
1005
|
-
value.alerts.length > gatewayPublicSnapshotLimits.alerts ||
|
|
1006
|
-
!value.alerts.every(isSafeAlert) ||
|
|
1007
|
-
!isSnapshotTruncation(value.truncation)) {
|
|
460
|
+
if (!shape(value, { schemaVersion: oneOf(2), generatedAt: iso, inboundMode: oneOf("paired", "open"),
|
|
461
|
+
health: isConnectorHealth,
|
|
462
|
+
connectors: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.connectors, isConnectorSnapshot),
|
|
463
|
+
availablePeers: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.availablePeers, isAvailablePeerSnapshot),
|
|
464
|
+
routes: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.routes, isRouteSnapshot),
|
|
465
|
+
consentEdges: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.consentEdges, isConsentEdgeSnapshot),
|
|
466
|
+
messages: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.messages, isNormalizedMessageEvent),
|
|
467
|
+
accounting: isAccounting, alerts: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.alerts, isSafeAlert),
|
|
468
|
+
truncation: isSnapshotTruncation }, { progressWatches: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.progressWatches, isProgressWatchSnapshot),
|
|
469
|
+
progressWatchEvents: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.progressWatchEvents, isProgressWatchEventSnapshot),
|
|
470
|
+
activityEvents: (rows) => arrayOf(rows, gatewayPublicSnapshotLimits.activityEvents, isGatewayActivityEvent),
|
|
471
|
+
deadlinePressure: isDeadlinePressure }))
|
|
1008
472
|
return false;
|
|
1009
|
-
|
|
1010
|
-
const
|
|
1011
|
-
const
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
(
|
|
1022
|
-
(derivedAgeMs !== undefined &&
|
|
1023
|
-
derivedAgeMs <= CONNECTOR_OBSERVATION_STALE_AFTER_MS)));
|
|
473
|
+
const snapshot = value;
|
|
474
|
+
const unique = (rows) => new Set(rows).size === rows.length;
|
|
475
|
+
const generated = Date.parse(snapshot.generatedAt);
|
|
476
|
+
const connectorKeys = snapshot.connectors.map((row) => `${row.provider}@${row.host}`);
|
|
477
|
+
const aliases = snapshot.routes.map((row) => row.alias);
|
|
478
|
+
const peers = snapshot.availablePeers.map((row) => row.alias);
|
|
479
|
+
const edgeKeys = snapshot.consentEdges.map((row) => `${row.endpoints[0].alias}\0${row.endpoints[1].alias}`);
|
|
480
|
+
const routeByAlias = new Map(snapshot.routes.map((row) => [row.alias, row]));
|
|
481
|
+
const honest = snapshot.connectors.every((row) => {
|
|
482
|
+
const seen = row.lastSeenAt === undefined ? undefined : Date.parse(row.lastSeenAt);
|
|
483
|
+
const age = seen === undefined ? undefined : Math.min(MAX_REVISION, Math.max(0, generated - seen));
|
|
484
|
+
return (row.observationAgeMs === undefined || row.observationAgeMs === age) &&
|
|
485
|
+
(row.health !== "healthy" || (age !== undefined && age <= CONNECTOR_OBSERVATION_STALE_AFTER_MS));
|
|
1024
486
|
});
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const routeByAlias = new Map(value.routes.map((route) => [route.alias, route]));
|
|
1029
|
-
return (connectorObservationsHonest &&
|
|
1030
|
-
new Set(connectorKeys).size === connectorKeys.length &&
|
|
1031
|
-
new Set(aliases).size === aliases.length &&
|
|
1032
|
-
new Set(peerAliases).size === peerAliases.length &&
|
|
1033
|
-
new Set(consentEdgeKeys).size === consentEdgeKeys.length &&
|
|
1034
|
-
value.consentEdges.every((edge) => routeByAlias.get(edge.endpoints[0].alias)?.provider ===
|
|
1035
|
-
edge.endpoints[0].provider &&
|
|
1036
|
-
routeByAlias.get(edge.endpoints[1].alias)?.provider ===
|
|
1037
|
-
edge.endpoints[1].provider) &&
|
|
1038
|
-
Buffer.byteLength(JSON.stringify(value), "utf8") <=
|
|
1039
|
-
GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET);
|
|
487
|
+
return honest && unique(connectorKeys) && unique(aliases) && unique(peers) && unique(edgeKeys) &&
|
|
488
|
+
snapshot.consentEdges.every((edge) => edge.endpoints.every((endpoint) => routeByAlias.get(endpoint.alias)?.provider === endpoint.provider)) &&
|
|
489
|
+
Buffer.byteLength(JSON.stringify(snapshot), "utf8") <= GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET;
|
|
1040
490
|
}
|
|
1041
491
|
function isSnapshotObservation(value) {
|
|
1042
|
-
return (
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return isSendResult(value);
|
|
1071
|
-
case "refresh_dashboard":
|
|
1072
|
-
return isRefreshResult(value);
|
|
492
|
+
return shape(value, { snapshotRevision: nonNegative, snapshot: isGatewaySnapshot });
|
|
493
|
+
}
|
|
494
|
+
const descriptors = {
|
|
495
|
+
health: { handler: "health", decode: emptyParams, result: isHealthResult, mutation: false }, register_codex: { handler: "registerCodex", decode: decodeRegister, result: isDecision, mutation: true },
|
|
496
|
+
unregister_codex: { handler: "unregisterCodex", decode: decodeUnregister, result: isDecision, mutation: true }, remove_codex_registration: { handler: "removeCodexRegistration", decode: decodeRemoval, result: isDecision, mutation: true },
|
|
497
|
+
select_claude: { handler: "selectClaude", decode: decodeSelection, result: isDecision, mutation: true }, unselect_claude: { handler: "unselectClaude", decode: decodeSelection, result: isDecision, mutation: true },
|
|
498
|
+
pair: { handler: "pair", decode: decodePair, result: isDecision, mutation: true }, unpair: { handler: "unpair", decode: decodePair, result: isDecision, mutation: true },
|
|
499
|
+
list_snapshot: { handler: "listSnapshot", decode: emptyParams, result: isGatewaySnapshot, mutation: false }, observe_snapshot: { handler: "observeSnapshot", decode: emptyParams, result: isSnapshotObservation, mutation: false },
|
|
500
|
+
delivery_status: { handler: "deliveryStatus", decode: decodeDeliveryStatus, result: isDeliveryStatusResult, mutation: false }, untrack: { handler: "untrack", decode: decodeUntrack, result: isDecision, mutation: false },
|
|
501
|
+
send_to_claude: { handler: "sendToClaude", decode: decodeSendClaude, result: isSendResult, mutation: true }, send_to_codex: { handler: "sendToCodex", decode: decodeSendCodex, result: isSendResult, mutation: true },
|
|
502
|
+
reply: { handler: "reply", decode: decodeReply, result: isSendResult, mutation: true }, refresh_dashboard: { handler: "refreshDashboard", decode: emptyParams, result: isRefreshResult, mutation: false },
|
|
503
|
+
};
|
|
504
|
+
function parseRequestObject(value) {
|
|
505
|
+
if (!isRecord(value) || !exact(value, ["protocolVersion", "method", "params"]))
|
|
506
|
+
invalid();
|
|
507
|
+
if (value.protocolVersion !== 1)
|
|
508
|
+
throw new ProtocolFault("UNSUPPORTED_VERSION");
|
|
509
|
+
if (typeof value.method !== "string" || !Object.hasOwn(descriptors, value.method))
|
|
510
|
+
throw new ProtocolFault("UNKNOWN_METHOD");
|
|
511
|
+
const method = value.method;
|
|
512
|
+
return { protocolVersion: 1, method, params: descriptors[method].decode(value.params) };
|
|
513
|
+
}
|
|
514
|
+
function decodeJson(frame, invalidCode) {
|
|
515
|
+
try {
|
|
516
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(frame));
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
throw new ProtocolFault(invalidCode);
|
|
1073
520
|
}
|
|
1074
521
|
}
|
|
1075
522
|
function errorResponse(code) {
|
|
1076
|
-
return {
|
|
1077
|
-
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
1078
|
-
ok: false,
|
|
1079
|
-
error: { code, message: WIRE_ERROR_MESSAGES[code] },
|
|
1080
|
-
};
|
|
523
|
+
return { protocolVersion: 1, ok: false, error: { code, message: WIRE_ERROR_MESSAGES[code] } };
|
|
1081
524
|
}
|
|
1082
525
|
async function dispatch(request, handlers) {
|
|
1083
|
-
|
|
526
|
+
const descriptor = descriptors[request.method];
|
|
527
|
+
const handler = handlers[descriptor.handler];
|
|
1084
528
|
try {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
result = await handlers.health();
|
|
1088
|
-
break;
|
|
1089
|
-
case "register_codex":
|
|
1090
|
-
result = await handlers.registerCodex(request.params);
|
|
1091
|
-
break;
|
|
1092
|
-
case "unregister_codex":
|
|
1093
|
-
result = await handlers.unregisterCodex(request.params);
|
|
1094
|
-
break;
|
|
1095
|
-
case "remove_stale_codex_registration":
|
|
1096
|
-
result = await handlers.removeStaleCodexRegistration(request.params);
|
|
1097
|
-
break;
|
|
1098
|
-
case "select_claude":
|
|
1099
|
-
result = await handlers.selectClaude(request.params);
|
|
1100
|
-
break;
|
|
1101
|
-
case "unselect_claude":
|
|
1102
|
-
result = await handlers.unselectClaude(request.params);
|
|
1103
|
-
break;
|
|
1104
|
-
case "pair":
|
|
1105
|
-
result = await handlers.pair(request.params);
|
|
1106
|
-
break;
|
|
1107
|
-
case "unpair":
|
|
1108
|
-
result = await handlers.unpair(request.params);
|
|
1109
|
-
break;
|
|
1110
|
-
case "list_snapshot":
|
|
1111
|
-
result = await handlers.listSnapshot();
|
|
1112
|
-
break;
|
|
1113
|
-
case "observe_snapshot":
|
|
1114
|
-
result = await handlers.observeSnapshot();
|
|
1115
|
-
break;
|
|
1116
|
-
case "delivery_status":
|
|
1117
|
-
result = await handlers.deliveryStatus(request.params);
|
|
1118
|
-
break;
|
|
1119
|
-
case "untrack":
|
|
1120
|
-
result = await handlers.untrack(request.params);
|
|
1121
|
-
break;
|
|
1122
|
-
case "send_to_claude":
|
|
1123
|
-
result = await handlers.sendToClaude(request.params);
|
|
1124
|
-
break;
|
|
1125
|
-
case "send_to_codex":
|
|
1126
|
-
result = await handlers.sendToCodex(request.params);
|
|
1127
|
-
break;
|
|
1128
|
-
case "reply":
|
|
1129
|
-
result = await handlers.reply(request.params);
|
|
1130
|
-
break;
|
|
1131
|
-
case "refresh_dashboard":
|
|
1132
|
-
result = await handlers.refreshDashboard();
|
|
1133
|
-
break;
|
|
1134
|
-
}
|
|
529
|
+
const result = await (Object.keys(request.params).length === 0 ? handler() : handler(request.params));
|
|
530
|
+
return { protocolVersion: 1, ok: true, result };
|
|
1135
531
|
}
|
|
1136
532
|
catch {
|
|
1137
533
|
return errorResponse("HANDLER_FAILURE");
|
|
1138
534
|
}
|
|
1139
|
-
if (!isResultForMethod(request.method, result)) {
|
|
1140
|
-
return errorResponse("INVALID_HANDLER_RESPONSE");
|
|
1141
|
-
}
|
|
1142
|
-
return {
|
|
1143
|
-
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
1144
|
-
ok: true,
|
|
1145
|
-
result,
|
|
1146
|
-
};
|
|
1147
535
|
}
|
|
1148
536
|
function serializeResponse(response) {
|
|
1149
|
-
const encoded = Buffer.from(`${JSON.stringify(response)}\n`, "utf8");
|
|
1150
|
-
if (encoded.length <= GATEWAY_CONTROL_MAX_RESPONSE_BYTES)
|
|
1151
|
-
return encoded;
|
|
1152
|
-
return Buffer.from(`${JSON.stringify(errorResponse("RESPONSE_TOO_LARGE"))}\n`, "utf8");
|
|
1153
|
-
}
|
|
1154
|
-
function respond(socket, response) {
|
|
1155
|
-
if (socket.destroyed)
|
|
1156
|
-
return;
|
|
1157
|
-
let encoded;
|
|
1158
537
|
try {
|
|
1159
|
-
encoded =
|
|
538
|
+
const encoded = Buffer.from(`${JSON.stringify(response)}\n`, "utf8");
|
|
539
|
+
return encoded.length <= GATEWAY_CONTROL_MAX_RESPONSE_BYTES ? encoded : Buffer.from(`${JSON.stringify(errorResponse("RESPONSE_TOO_LARGE"))}\n`);
|
|
1160
540
|
}
|
|
1161
541
|
catch {
|
|
1162
|
-
|
|
542
|
+
return Buffer.from(`${JSON.stringify(errorResponse("INVALID_HANDLER_RESPONSE"))}\n`);
|
|
1163
543
|
}
|
|
1164
|
-
socket.end(encoded);
|
|
1165
544
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
545
|
+
class FrameFault extends Error {
|
|
546
|
+
kind;
|
|
547
|
+
constructor(kind) {
|
|
548
|
+
super(kind);
|
|
549
|
+
this.kind = kind;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function readOneFrame(socket, maximum) {
|
|
553
|
+
return new Promise((resolve, reject) => {
|
|
554
|
+
let buffered = Buffer.alloc(0);
|
|
555
|
+
let settled = false;
|
|
556
|
+
const finish = (value) => {
|
|
557
|
+
if (settled)
|
|
558
|
+
return;
|
|
559
|
+
settled = true;
|
|
560
|
+
socket.pause();
|
|
561
|
+
socket.off("data", onData);
|
|
562
|
+
socket.off("end", onEnd);
|
|
563
|
+
socket.off("timeout", onTimeout);
|
|
564
|
+
value instanceof FrameFault ? reject(value) : resolve(value);
|
|
565
|
+
};
|
|
566
|
+
const onData = (chunk) => {
|
|
567
|
+
if (buffered.length + chunk.length > maximum)
|
|
568
|
+
return finish(new FrameFault("too_large"));
|
|
569
|
+
buffered = Buffer.concat([buffered, chunk]);
|
|
570
|
+
const newline = buffered.indexOf(0x0a);
|
|
571
|
+
if (newline < 0)
|
|
572
|
+
return;
|
|
573
|
+
if (buffered.subarray(newline + 1).toString("utf8").trim().length > 0)
|
|
574
|
+
return finish(new FrameFault("multiple"));
|
|
575
|
+
finish(buffered.subarray(0, newline));
|
|
576
|
+
};
|
|
577
|
+
const onEnd = () => finish(new FrameFault("closed"));
|
|
578
|
+
const onTimeout = () => finish(new FrameFault("timeout"));
|
|
579
|
+
socket.on("data", onData);
|
|
580
|
+
socket.once("end", onEnd);
|
|
581
|
+
socket.once("timeout", onTimeout);
|
|
582
|
+
socket.once("error", () => finish(new FrameFault("error")));
|
|
1181
583
|
});
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
if (newline < 0)
|
|
1192
|
-
return;
|
|
1193
|
-
const trailing = buffered.subarray(newline + 1).toString("utf8");
|
|
1194
|
-
if (trailing.trim().length > 0) {
|
|
1195
|
-
complete(errorResponse("MULTIPLE_FRAMES"));
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
const frame = buffered.subarray(0, newline);
|
|
584
|
+
}
|
|
585
|
+
function respond(socket, response) {
|
|
586
|
+
if (!socket.destroyed)
|
|
587
|
+
socket.end(serializeResponse(response));
|
|
588
|
+
}
|
|
589
|
+
function handleConnection(socket, handlers, timeoutMs) {
|
|
590
|
+
socket.setTimeout(timeoutMs);
|
|
591
|
+
void readOneFrame(socket, GATEWAY_CONTROL_MAX_FRAME_BYTES).then(async (frame) => {
|
|
592
|
+
socket.setTimeout(0);
|
|
1199
593
|
let request;
|
|
1200
594
|
try {
|
|
1201
|
-
request =
|
|
595
|
+
request = parseRequestObject(decodeJson(frame, "INVALID_JSON"));
|
|
1202
596
|
}
|
|
1203
597
|
catch (error) {
|
|
1204
|
-
|
|
1205
|
-
complete(errorResponse(code));
|
|
598
|
+
respond(socket, errorResponse(error instanceof ProtocolFault ? error.code : "INVALID_REQUEST"));
|
|
1206
599
|
return;
|
|
1207
600
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
601
|
+
respond(socket, await dispatch(request, handlers));
|
|
602
|
+
}, (error) => {
|
|
603
|
+
const kind = error instanceof FrameFault ? error.kind : "error";
|
|
604
|
+
if (kind === "too_large")
|
|
605
|
+
respond(socket, errorResponse("FRAME_TOO_LARGE"));
|
|
606
|
+
else if (kind === "multiple")
|
|
607
|
+
respond(socket, errorResponse("MULTIPLE_FRAMES"));
|
|
608
|
+
else if (kind === "timeout")
|
|
609
|
+
respond(socket, errorResponse("REQUEST_TIMEOUT"));
|
|
610
|
+
else
|
|
1215
611
|
socket.destroy();
|
|
1216
612
|
});
|
|
1217
613
|
}
|
|
1218
|
-
function controlTransportError(code, ambiguous = false) {
|
|
1219
|
-
const messages = {
|
|
1220
|
-
UNSUPPORTED_PLATFORM: "Unix-domain gateway control sockets are unavailable.",
|
|
1221
|
-
INVALID_STATE_DIR: "The gateway state directory is invalid.",
|
|
1222
|
-
INSECURE_STATE_DIR: "The gateway state directory is not private to this user.",
|
|
1223
|
-
INVALID_SOCKET_PATH: "The gateway control socket path is invalid.",
|
|
1224
|
-
UNSAFE_SOCKET_TARGET: "The gateway control socket target is unsafe.",
|
|
1225
|
-
SOCKET_IN_USE: "The gateway control socket is already served by a live process.",
|
|
1226
|
-
SOCKET_PROBE_FAILED: "The gateway control socket could not be checked safely.",
|
|
1227
|
-
SOCKET_BIND_FAILED: "The gateway control socket could not be bound.",
|
|
1228
|
-
SOCKET_PERMISSION_FAILED: "The gateway control socket permissions could not be secured.",
|
|
1229
|
-
SOCKET_CLEANUP_CONFLICT: "The gateway control socket path changed during cleanup.",
|
|
1230
|
-
CONTROL_CONNECT_FAILED: "The gateway control socket could not be reached.",
|
|
1231
|
-
CONTROL_TIMEOUT: "The gateway control request timed out.",
|
|
1232
|
-
CONTROL_RESPONSE_TOO_LARGE: "The gateway control response exceeds the client limit.",
|
|
1233
|
-
CONTROL_INVALID_RESPONSE: "The gateway returned an invalid control response. Restart the broker, then retry.",
|
|
1234
|
-
CONTROL_CONNECTION_CLOSED: "The gateway closed before returning a control response.",
|
|
1235
|
-
CONTROL_OUTCOME_AMBIGUOUS: "The gateway may have applied the control mutation before the response was lost; do not retry automatically.",
|
|
1236
|
-
};
|
|
1237
|
-
return new GatewayControlTransportError(code, messages[code] ?? "The gateway control transport failed.", ambiguous);
|
|
1238
|
-
}
|
|
1239
|
-
function isNonIdempotentControlMethod(method) {
|
|
1240
|
-
return (method === "register_codex" ||
|
|
1241
|
-
method === "unregister_codex" ||
|
|
1242
|
-
method === "remove_stale_codex_registration" ||
|
|
1243
|
-
method === "select_claude" ||
|
|
1244
|
-
method === "unselect_claude" ||
|
|
1245
|
-
method === "pair" ||
|
|
1246
|
-
method === "unpair" ||
|
|
1247
|
-
method === "send_to_claude" ||
|
|
1248
|
-
method === "send_to_codex" ||
|
|
1249
|
-
method === "reply");
|
|
1250
|
-
}
|
|
1251
614
|
async function optionalLstat(target) {
|
|
1252
615
|
try {
|
|
1253
616
|
return await lstat(target);
|
|
@@ -1258,15 +621,19 @@ async function optionalLstat(target) {
|
|
|
1258
621
|
throw error;
|
|
1259
622
|
}
|
|
1260
623
|
}
|
|
624
|
+
function assertSocketPath(socketPath) {
|
|
625
|
+
if (!path.isAbsolute(socketPath) ||
|
|
626
|
+
path.resolve(socketPath) !== socketPath ||
|
|
627
|
+
Buffer.byteLength(socketPath, "utf8") > MAX_SOCKET_PATH_BYTES) {
|
|
628
|
+
throw controlTransportError("INVALID_SOCKET_PATH");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
1261
631
|
async function validateSocketLocation(stateDir, socketPath) {
|
|
1262
632
|
if (process.platform === "win32") {
|
|
1263
633
|
throw controlTransportError("UNSUPPORTED_PLATFORM");
|
|
1264
634
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
!path.isAbsolute(socketPath) ||
|
|
1268
|
-
path.resolve(socketPath) !== socketPath ||
|
|
1269
|
-
Buffer.byteLength(socketPath, "utf8") > MAX_SOCKET_PATH_BYTES) {
|
|
635
|
+
assertSocketPath(socketPath);
|
|
636
|
+
if (!path.isAbsolute(stateDir) || path.resolve(stateDir) !== stateDir) {
|
|
1270
637
|
throw controlTransportError("INVALID_SOCKET_PATH");
|
|
1271
638
|
}
|
|
1272
639
|
let stateInfo;
|
|
@@ -1388,8 +755,7 @@ async function removeOwnedSocket(socketPath, identity) {
|
|
|
1388
755
|
await unlink(socketPath);
|
|
1389
756
|
}
|
|
1390
757
|
catch {
|
|
1391
|
-
//
|
|
1392
|
-
// broaden cleanup to any replacement path.
|
|
758
|
+
// Never broaden cleanup to a replacement path.
|
|
1393
759
|
}
|
|
1394
760
|
}
|
|
1395
761
|
async function protectSocketReplacement(socketPath, identity) {
|
|
@@ -1442,24 +808,17 @@ async function restoreSocketReplacement(socketPath, replacement) {
|
|
|
1442
808
|
throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
|
|
1443
809
|
}
|
|
1444
810
|
}
|
|
1445
|
-
function validServerTimeout(value) {
|
|
1446
|
-
return Number.isInteger(value) && value >= 50 && value <= 30_000;
|
|
1447
|
-
}
|
|
1448
|
-
function validClientTimeout(value) {
|
|
1449
|
-
return Number.isInteger(value) && value >= 50 && value <= 90_000;
|
|
1450
|
-
}
|
|
1451
811
|
export async function startGatewayControlServer(options) {
|
|
1452
812
|
await validateSocketLocation(options.stateDir, options.socketPath);
|
|
1453
813
|
await prepareSocketTarget(options.socketPath);
|
|
1454
814
|
const requestTimeoutMs = options.requestTimeoutMs ?? GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
|
|
1455
|
-
if (!
|
|
815
|
+
if (!Number.isInteger(requestTimeoutMs) ||
|
|
816
|
+
requestTimeoutMs < 50 ||
|
|
817
|
+
requestTimeoutMs > 30_000) {
|
|
1456
818
|
throw controlTransportError("INVALID_SOCKET_PATH");
|
|
1457
819
|
}
|
|
1458
820
|
const connections = new Set();
|
|
1459
821
|
const server = net.createServer({ allowHalfOpen: true }, (socket) => {
|
|
1460
|
-
// Directory ownership proves same-user access, not that every caller is a
|
|
1461
|
-
// trusted agent. Bound idle and in-flight peers before allocating parser or
|
|
1462
|
-
// handler state so a local peer cannot grow this set without limit.
|
|
1463
822
|
if (connections.size >= GATEWAY_CONTROL_MAX_CONNECTIONS) {
|
|
1464
823
|
socket.once("error", () => socket.destroy());
|
|
1465
824
|
socket.end(serializeResponse(errorResponse("SERVER_BUSY")), () => socket.destroy());
|
|
@@ -1498,8 +857,8 @@ export async function startGatewayControlServer(options) {
|
|
|
1498
857
|
}
|
|
1499
858
|
}
|
|
1500
859
|
catch {
|
|
1501
|
-
for (const
|
|
1502
|
-
|
|
860
|
+
for (const socket of connections)
|
|
861
|
+
socket.destroy();
|
|
1503
862
|
await closeNativeServer(server);
|
|
1504
863
|
if (identity)
|
|
1505
864
|
await removeOwnedSocket(options.socketPath, identity);
|
|
@@ -1511,8 +870,8 @@ export async function startGatewayControlServer(options) {
|
|
|
1511
870
|
const close = () => {
|
|
1512
871
|
closePromise ??= (async () => {
|
|
1513
872
|
const replacement = await protectSocketReplacement(options.socketPath, ownedIdentity);
|
|
1514
|
-
for (const
|
|
1515
|
-
|
|
873
|
+
for (const socket of connections)
|
|
874
|
+
socket.destroy();
|
|
1516
875
|
await closeNativeServer(server);
|
|
1517
876
|
await removeOwnedSocket(options.socketPath, ownedIdentity);
|
|
1518
877
|
await restoreSocketReplacement(options.socketPath, replacement);
|
|
@@ -1528,117 +887,63 @@ export async function startGatewayControlServer(options) {
|
|
|
1528
887
|
close,
|
|
1529
888
|
};
|
|
1530
889
|
}
|
|
1531
|
-
function
|
|
1532
|
-
if (!isRecord(value) || value.protocolVersion !== 1)
|
|
890
|
+
function decodeResponse(method, value) {
|
|
891
|
+
if (!isRecord(value) || value.protocolVersion !== 1)
|
|
1533
892
|
throw controlTransportError("CONTROL_INVALID_RESPONSE");
|
|
1534
|
-
}
|
|
1535
893
|
if (value.ok === false) {
|
|
1536
|
-
if (!
|
|
1537
|
-
|
|
1538
|
-
!
|
|
1539
|
-
typeof value.error.code !== "string" ||
|
|
1540
|
-
!Object.hasOwn(WIRE_ERROR_MESSAGES, value.error.code) ||
|
|
1541
|
-
value.error.message !==
|
|
1542
|
-
WIRE_ERROR_MESSAGES[value.error.code]) {
|
|
894
|
+
if (!shape(value, { protocolVersion: oneOf(1), ok: oneOf(false), error: (item) => shape(item, { code: (code) => typeof code === "string" && Object.hasOwn(WIRE_ERROR_MESSAGES, code),
|
|
895
|
+
message: (message) => typeof message === "string" }) }) ||
|
|
896
|
+
!isRecord(value.error) || value.error.message !== WIRE_ERROR_MESSAGES[value.error.code])
|
|
1543
897
|
throw controlTransportError("CONTROL_INVALID_RESPONSE");
|
|
1544
|
-
}
|
|
1545
898
|
return value;
|
|
1546
899
|
}
|
|
1547
|
-
if (value
|
|
1548
|
-
!hasExactKeys(value, ["protocolVersion", "ok", "result"]) ||
|
|
1549
|
-
!isResultForMethod(method, value.result)) {
|
|
900
|
+
if (!shape(value, { protocolVersion: oneOf(1), ok: oneOf(true), result: descriptors[method].result }))
|
|
1550
901
|
throw controlTransportError("CONTROL_INVALID_RESPONSE");
|
|
1551
|
-
}
|
|
1552
902
|
return value;
|
|
1553
903
|
}
|
|
1554
904
|
export async function sendGatewayControlRequest(options) {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
const timeoutMs = options.timeoutMs ?? GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
|
|
1561
|
-
const maxResponseBytes = options.maxResponseBytes ?? GATEWAY_CONTROL_MAX_RESPONSE_BYTES;
|
|
1562
|
-
if (!validClientTimeout(timeoutMs) ||
|
|
1563
|
-
!Number.isInteger(maxResponseBytes) ||
|
|
1564
|
-
maxResponseBytes < 256 ||
|
|
1565
|
-
maxResponseBytes > GATEWAY_CONTROL_MAX_RESPONSE_BYTES) {
|
|
905
|
+
assertSocketPath(options.socketPath);
|
|
906
|
+
const timeout = options.timeoutMs ?? GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
|
|
907
|
+
const maximum = options.maxResponseBytes ?? GATEWAY_CONTROL_MAX_RESPONSE_BYTES;
|
|
908
|
+
if (!Number.isInteger(timeout) || timeout < 50 || timeout > 90_000 ||
|
|
909
|
+
!Number.isInteger(maximum) || maximum < 256 || maximum > GATEWAY_CONTROL_MAX_RESPONSE_BYTES)
|
|
1566
910
|
throw controlTransportError("CONTROL_INVALID_RESPONSE");
|
|
1567
|
-
|
|
1568
|
-
const
|
|
1569
|
-
if (
|
|
1570
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
1571
|
-
}
|
|
1572
|
-
const requestFrame = Buffer.from(`${JSON.stringify(normalized)}\n`, "utf8");
|
|
1573
|
-
if (requestFrame.length > GATEWAY_CONTROL_MAX_FRAME_BYTES) {
|
|
911
|
+
const request = parseRequestObject(options.request);
|
|
912
|
+
const frame = Buffer.from(`${JSON.stringify(request)}\n`, "utf8");
|
|
913
|
+
if (frame.length > GATEWAY_CONTROL_MAX_FRAME_BYTES)
|
|
1574
914
|
throw new ProtocolFault("FRAME_TOO_LARGE");
|
|
1575
|
-
}
|
|
1576
915
|
return await new Promise((resolve, reject) => {
|
|
1577
916
|
const socket = net.createConnection(options.socketPath);
|
|
1578
917
|
let settled = false;
|
|
1579
918
|
let writeStarted = false;
|
|
1580
|
-
|
|
1581
|
-
const mutation = isNonIdempotentControlMethod(normalized.method);
|
|
919
|
+
const mutation = descriptors[request.method].mutation;
|
|
1582
920
|
const fail = (error) => {
|
|
1583
921
|
if (settled)
|
|
1584
922
|
return;
|
|
1585
923
|
settled = true;
|
|
1586
924
|
socket.destroy();
|
|
1587
|
-
reject(mutation && writeStarted
|
|
1588
|
-
? controlTransportError("CONTROL_OUTCOME_AMBIGUOUS", true)
|
|
1589
|
-
: error);
|
|
1590
|
-
};
|
|
1591
|
-
const succeed = (response) => {
|
|
1592
|
-
if (settled)
|
|
1593
|
-
return;
|
|
1594
|
-
settled = true;
|
|
1595
|
-
socket.destroy();
|
|
1596
|
-
resolve(response);
|
|
925
|
+
reject(mutation && writeStarted ? controlTransportError("CONTROL_OUTCOME_AMBIGUOUS", true) : error);
|
|
1597
926
|
};
|
|
1598
|
-
socket.setTimeout(
|
|
1599
|
-
socket.once("connect", () => {
|
|
1600
|
-
|
|
1601
|
-
// the frame. Losing the response to a mutation is therefore ambiguous,
|
|
1602
|
-
// never a safe retry signal.
|
|
1603
|
-
writeStarted = true;
|
|
1604
|
-
socket.write(requestFrame);
|
|
1605
|
-
});
|
|
1606
|
-
socket.once("timeout", () => fail(controlTransportError("CONTROL_TIMEOUT")));
|
|
1607
|
-
socket.once("error", () => fail(controlTransportError("CONTROL_CONNECT_FAILED")));
|
|
1608
|
-
socket.on("data", (chunk) => {
|
|
1609
|
-
if (settled)
|
|
1610
|
-
return;
|
|
1611
|
-
if (buffered.length + chunk.length > maxResponseBytes) {
|
|
1612
|
-
fail(controlTransportError("CONTROL_RESPONSE_TOO_LARGE"));
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
buffered = Buffer.concat([buffered, chunk]);
|
|
1616
|
-
const newline = buffered.indexOf(0x0a);
|
|
1617
|
-
if (newline < 0)
|
|
1618
|
-
return;
|
|
1619
|
-
if (buffered.subarray(newline + 1).toString("utf8").trim().length > 0) {
|
|
1620
|
-
fail(controlTransportError("CONTROL_INVALID_RESPONSE"));
|
|
1621
|
-
return;
|
|
1622
|
-
}
|
|
1623
|
-
let parsed;
|
|
927
|
+
socket.setTimeout(timeout);
|
|
928
|
+
socket.once("connect", () => { writeStarted = true; socket.write(frame); });
|
|
929
|
+
void readOneFrame(socket, maximum).then((responseFrame) => {
|
|
1624
930
|
try {
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
try {
|
|
1632
|
-
succeed(validateResponseForMethod(options.request.method, parsed));
|
|
931
|
+
const response = decodeResponse(options.request.method, decodeJson(responseFrame, "INVALID_JSON"));
|
|
932
|
+
if (!settled) {
|
|
933
|
+
settled = true;
|
|
934
|
+
socket.destroy();
|
|
935
|
+
resolve(response);
|
|
936
|
+
}
|
|
1633
937
|
}
|
|
1634
938
|
catch {
|
|
1635
939
|
fail(controlTransportError("CONTROL_INVALID_RESPONSE"));
|
|
1636
940
|
}
|
|
1637
|
-
})
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
941
|
+
}, (error) => {
|
|
942
|
+
const kind = error instanceof FrameFault ? error.kind : "error";
|
|
943
|
+
const code = kind === "timeout" ? "CONTROL_TIMEOUT" :
|
|
944
|
+
kind === "too_large" ? "CONTROL_RESPONSE_TOO_LARGE" :
|
|
945
|
+
kind === "closed" ? "CONTROL_CONNECTION_CLOSED" : "CONTROL_CONNECT_FAILED";
|
|
946
|
+
fail(controlTransportError(code));
|
|
1642
947
|
});
|
|
1643
948
|
});
|
|
1644
949
|
}
|