agent-embassy 1.7.0 → 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 +32 -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.en.d.ts +2 -2
- package/dist/src/gateway/cli-copy.en.js +4 -2
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +2 -2
- package/dist/src/gateway/cli-copy.zh-CN.js +4 -2
- 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 +277 -575
- 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 +36 -0
- package/dist/src/gateway/codex-doctor.js +127 -0
- package/dist/src/gateway/codex-doctor.js.map +1 -0
- package/dist/src/gateway/codex-local-transport.d.ts +6 -6
- package/dist/src/gateway/codex-local-transport.js +13 -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 +465 -1123
- 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 +8 -26
- package/dist/src/gateway/dashboard-copy.en.js +19 -37
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +8 -26
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +8 -26
- package/dist/src/gateway/dashboard-copy.zh-CN.js +19 -37
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +8 -22
- package/dist/src/gateway/dashboard-model.js +44 -130
- 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 +3 -7
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/live-dashboard-app/app.js +20 -34
- 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 +58 -191
- package/dist/src/gateway/providers.js +637 -2434
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +16 -25
- package/dist/src/gateway/server.js +165 -240
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +149 -427
- package/dist/src/gateway/service.js +1639 -5662
- 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 -3510
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +181 -225
- package/dist/src/gateway/types.js +115 -251
- 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 { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayProviders, gatewayPublicSnapshotLimits, isGatewayProvider, isMessageDirection, isPublicRegistryObservationSnapshot, } from "./types.js";
|
|
7
|
+
import { CONNECTOR_OBSERVATION_STALE_AFTER_MS, GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, 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,1047 +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
|
-
isConnectorHealth(value.health) &&
|
|
590
|
-
typeof value.protocol === "string" &&
|
|
591
|
-
PROTOCOL_PATTERN.test(value.protocol) &&
|
|
592
|
-
typeof value.protocolVersion === "string" &&
|
|
593
|
-
PROTOCOL_VERSION_PATTERN.test(value.protocolVersion) &&
|
|
594
|
-
(value.lastSeenAt === undefined || isIsoTimestamp(value.lastSeenAt)) &&
|
|
595
|
-
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)) &&
|
|
596
|
-
(value.registry === undefined ||
|
|
597
|
-
(value.provider === "claude" &&
|
|
598
|
-
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");
|
|
599
355
|
}
|
|
600
356
|
function isRouteSnapshot(value) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
"busyPolicy",
|
|
609
|
-
"queueDepth",
|
|
610
|
-
"counters",
|
|
611
|
-
], ["lastSeenAt", "oldestQueuedAt", "safeErrorCode"]) &&
|
|
612
|
-
isAlias(value.alias) &&
|
|
613
|
-
isGatewayProvider(value.provider) &&
|
|
614
|
-
isHostId(value.host) &&
|
|
615
|
-
value.alias.endsWith(`@${value.host}`) &&
|
|
616
|
-
typeof value.enabled === "boolean" &&
|
|
617
|
-
(value.state === "stale" ||
|
|
618
|
-
value.state === "idle" ||
|
|
619
|
-
value.state === "busy" ||
|
|
620
|
-
value.state === "awaiting_approval" ||
|
|
621
|
-
value.state === "offline" ||
|
|
622
|
-
value.state === "disabled") &&
|
|
623
|
-
value.busyPolicy === "queue" &&
|
|
624
|
-
(value.lastSeenAt === undefined || isIsoTimestamp(value.lastSeenAt)) &&
|
|
625
|
-
isNonNegativeInteger(value.queueDepth) &&
|
|
626
|
-
(value.oldestQueuedAt === undefined
|
|
627
|
-
? value.queueDepth === 0
|
|
628
|
-
: value.queueDepth > 0 && isIsoTimestamp(value.oldestQueuedAt)) &&
|
|
629
|
-
isRouteCounters(value.counters) &&
|
|
630
|
-
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)));
|
|
631
|
-
}
|
|
632
|
-
function isConsentEndpoint(value) {
|
|
633
|
-
return (isRecord(value) &&
|
|
634
|
-
hasExactKeys(value, ["alias", "provider"]) &&
|
|
635
|
-
isAlias(value.alias) &&
|
|
636
|
-
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);
|
|
637
364
|
}
|
|
638
365
|
function compareConsentEndpoints(left, right) {
|
|
639
|
-
|
|
640
|
-
gatewayProviders.indexOf(right.provider);
|
|
641
|
-
return providerDelta || left.alias.localeCompare(right.alias);
|
|
366
|
+
return gatewayProviders.indexOf(left.provider) -
|
|
367
|
+
gatewayProviders.indexOf(right.provider) || left.alias.localeCompare(right.alias);
|
|
642
368
|
}
|
|
643
369
|
function isConsentEdgeSnapshot(value) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
value.endpoints.length !== 2 ||
|
|
648
|
-
!isConsentEndpoint(value.endpoints[0]) ||
|
|
649
|
-
!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 }))
|
|
650
373
|
return false;
|
|
651
|
-
}
|
|
652
374
|
const [left, right] = value.endpoints;
|
|
653
|
-
return
|
|
654
|
-
left.alias
|
|
655
|
-
compareConsentEndpoints(left, right) < 0 &&
|
|
656
|
-
isHostId(value.host) &&
|
|
657
|
-
left.alias.endsWith(`@${value.host}`) &&
|
|
658
|
-
right.alias.endsWith(`@${value.host}`) &&
|
|
659
|
-
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}`);
|
|
660
377
|
}
|
|
661
378
|
function isAvailablePeerSnapshot(value) {
|
|
662
|
-
return (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
"host",
|
|
667
|
-
"state",
|
|
668
|
-
"validated",
|
|
669
|
-
"selected",
|
|
670
|
-
], ["lastSeenAt", "safeErrorCode"]) &&
|
|
671
|
-
isAlias(value.alias) &&
|
|
672
|
-
value.provider === "claude" &&
|
|
673
|
-
isHostId(value.host) &&
|
|
674
|
-
value.alias.endsWith(`@${value.host}`) &&
|
|
675
|
-
(value.state === "idle" ||
|
|
676
|
-
value.state === "busy" ||
|
|
677
|
-
value.state === "awaiting_approval" ||
|
|
678
|
-
value.state === "offline") &&
|
|
679
|
-
typeof value.validated === "boolean" &&
|
|
680
|
-
typeof value.selected === "boolean" &&
|
|
681
|
-
(value.lastSeenAt === undefined || isIsoTimestamp(value.lastSeenAt)) &&
|
|
682
|
-
(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)}`);
|
|
683
383
|
}
|
|
684
384
|
function isNormalizedMessageEvent(value) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
"bytes",
|
|
695
|
-
], ["conversationIdSuffix", "body", "latencyMs", "safeErrorCode", "steer"]) &&
|
|
696
|
-
isNonNegativeInteger(value.sequence) &&
|
|
697
|
-
isIsoTimestamp(value.timestamp) &&
|
|
698
|
-
typeof value.messageIdSuffix === "string" &&
|
|
699
|
-
MESSAGE_SUFFIX_PATTERN.test(value.messageIdSuffix) &&
|
|
700
|
-
(value.conversationIdSuffix === undefined ||
|
|
701
|
-
(typeof value.conversationIdSuffix === "string" &&
|
|
702
|
-
CONVERSATION_SUFFIX_PATTERN.test(value.conversationIdSuffix))) &&
|
|
703
|
-
isMessageDirection(value.direction) &&
|
|
704
|
-
isAlias(value.sourceAlias) &&
|
|
705
|
-
isAlias(value.targetAlias) &&
|
|
706
|
-
(value.state === "queued" ||
|
|
707
|
-
value.state === "duplicate" ||
|
|
708
|
-
value.state === "dispatching" ||
|
|
709
|
-
value.state === "transport_written" ||
|
|
710
|
-
value.state === "held" ||
|
|
711
|
-
value.state === "delivered" ||
|
|
712
|
-
value.state === "unconfirmed" ||
|
|
713
|
-
value.state === "failed" ||
|
|
714
|
-
value.state === "ambiguous" ||
|
|
715
|
-
value.state === "expired" ||
|
|
716
|
-
value.state === "cancelled" ||
|
|
717
|
-
value.state === "abandoned" ||
|
|
718
|
-
value.state === "rejected") &&
|
|
719
|
-
isNonNegativeInteger(value.bytes) &&
|
|
720
|
-
(value.body === undefined ||
|
|
721
|
-
(typeof value.body === "string" &&
|
|
722
|
-
value.body.length > 0 &&
|
|
723
|
-
!value.body.includes("\u0000") &&
|
|
724
|
-
Buffer.byteLength(value.body, "utf8") === value.bytes)) &&
|
|
725
|
-
(value.latencyMs === undefined || isNonNegativeInteger(value.latencyMs)) &&
|
|
726
|
-
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)) &&
|
|
727
|
-
(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;
|
|
728
394
|
}
|
|
729
395
|
function isAccounting(value) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
"duplicates",
|
|
734
|
-
"delivered",
|
|
735
|
-
"unconfirmed",
|
|
736
|
-
"failed",
|
|
737
|
-
"ambiguous",
|
|
738
|
-
"expired",
|
|
739
|
-
"cancelled",
|
|
740
|
-
"abandoned",
|
|
741
|
-
"rejected",
|
|
742
|
-
"bytesAccepted",
|
|
743
|
-
"queuedBytes",
|
|
744
|
-
])) {
|
|
745
|
-
return false;
|
|
746
|
-
}
|
|
747
|
-
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]));
|
|
748
399
|
}
|
|
749
400
|
function isSafeAlert(value) {
|
|
750
|
-
return (
|
|
751
|
-
hasExactKeys(value, ["code", "severity", "timestamp"], [
|
|
752
|
-
"provider",
|
|
753
|
-
"host",
|
|
754
|
-
"alias",
|
|
755
|
-
]) &&
|
|
756
|
-
isSafeCode(value.code) &&
|
|
757
|
-
(value.severity === "info" ||
|
|
758
|
-
value.severity === "warning" ||
|
|
759
|
-
value.severity === "error") &&
|
|
760
|
-
isIsoTimestamp(value.timestamp) &&
|
|
761
|
-
(value.provider === undefined || isGatewayProvider(value.provider)) &&
|
|
762
|
-
(value.host === undefined || isHostId(value.host)) &&
|
|
763
|
-
(value.alias === undefined || isAlias(value.alias)));
|
|
401
|
+
return shape(value, { code: safeCode, severity: oneOf("info", "warning", "error"), timestamp: iso }, { provider: isGatewayProvider, host, alias });
|
|
764
402
|
}
|
|
765
403
|
function isProgressWatchSnapshot(value) {
|
|
766
|
-
return (
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
"lastActivityAt",
|
|
772
|
-
"nextActionAt",
|
|
773
|
-
"nudgeCount",
|
|
774
|
-
]) &&
|
|
775
|
-
typeof value.conversationIdSuffix === "string" &&
|
|
776
|
-
CONVERSATION_SUFFIX_PATTERN.test(value.conversationIdSuffix) &&
|
|
777
|
-
isAlias(value.ownerAlias) &&
|
|
778
|
-
isAlias(value.workerAlias) &&
|
|
779
|
-
value.ownerAlias !== value.workerAlias &&
|
|
780
|
-
isIsoTimestamp(value.lastActivityAt) &&
|
|
781
|
-
isIsoTimestamp(value.nextActionAt) &&
|
|
782
|
-
(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;
|
|
783
409
|
}
|
|
784
410
|
function isProgressWatchEventSnapshot(value) {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
isAlias(value.ownerAlias) &&
|
|
800
|
-
isAlias(value.workerAlias) &&
|
|
801
|
-
((value.kind === "opened" &&
|
|
802
|
-
value.actor === "owner" &&
|
|
803
|
-
value.reason === undefined) ||
|
|
804
|
-
(value.kind === "replaced" &&
|
|
805
|
-
(value.actor === "owner" || value.actor === "unknown") &&
|
|
806
|
-
value.reason === undefined) ||
|
|
807
|
-
(value.kind === "settled" &&
|
|
808
|
-
((value.reason === "done" &&
|
|
809
|
-
(value.actor === "owner" || value.actor === "worker")) ||
|
|
810
|
-
(value.reason === "untracked" && value.actor === "operator") ||
|
|
811
|
-
((value.reason === "idle_timeout" ||
|
|
812
|
-
value.reason === "tracking_disabled") &&
|
|
813
|
-
value.actor === "gateway") ||
|
|
814
|
-
(value.reason === "endpoint_retired" &&
|
|
815
|
-
(value.actor === "gateway" || value.actor === "operator")) ||
|
|
816
|
-
(value.reason === "pair_removed" &&
|
|
817
|
-
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");
|
|
818
425
|
}
|
|
819
426
|
function isGatewayActivityEvent(value) {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
"action",
|
|
826
|
-
"outcome",
|
|
827
|
-
"aliases",
|
|
828
|
-
"operatorAction",
|
|
829
|
-
], ["safeErrorCode"]) &&
|
|
830
|
-
isNonNegativeInteger(value.sequence) &&
|
|
831
|
-
value.sequence > 0 &&
|
|
832
|
-
isIsoTimestamp(value.timestamp) &&
|
|
833
|
-
isGatewayActivityIdentity(value.kind, value.action, value.operatorAction) &&
|
|
834
|
-
(value.outcome === "accepted" || value.outcome === "rejected") &&
|
|
835
|
-
Array.isArray(value.aliases) &&
|
|
836
|
-
value.aliases.length <= 2 &&
|
|
837
|
-
value.aliases.every(isAlias) &&
|
|
838
|
-
new Set(value.aliases).size === value.aliases.length &&
|
|
839
|
-
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)));
|
|
840
|
-
}
|
|
841
|
-
function isGatewayActivityIdentity(kind, action, operatorAction) {
|
|
842
|
-
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 }))
|
|
843
432
|
return false;
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
return ((action === "codex_registered" ||
|
|
852
|
-
action === "codex_succeeded" ||
|
|
853
|
-
action === "codex_unregistered") &&
|
|
854
|
-
operatorAction);
|
|
855
|
-
case "pairing":
|
|
856
|
-
return ((action === "routes_paired" || action === "routes_unpaired") &&
|
|
857
|
-
operatorAction);
|
|
858
|
-
case "watch":
|
|
859
|
-
return action === "watch_ended" && operatorAction;
|
|
860
|
-
case "endpoint":
|
|
861
|
-
return action === "endpoint_refreshed" && !operatorAction;
|
|
862
|
-
case "recovery":
|
|
863
|
-
return action === "codex_orphan_removed" && operatorAction;
|
|
864
|
-
default:
|
|
865
|
-
return false;
|
|
866
|
-
}
|
|
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);
|
|
867
440
|
}
|
|
868
441
|
function isDeadlinePressure(value) {
|
|
869
|
-
const names = [
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
value.configuredDeadlineMs > 0 &&
|
|
880
|
-
(value.retainedSince === undefined || isIsoTimestamp(value.retainedSince)) &&
|
|
881
|
-
isNonNegativeInteger(value.terminalEvents) &&
|
|
882
|
-
isNonNegativeInteger(value.expiredEvents) &&
|
|
883
|
-
value.expiredEvents <= value.terminalEvents &&
|
|
884
|
-
Array.isArray(value.buckets) &&
|
|
885
|
-
value.buckets.length === names.length &&
|
|
886
|
-
value.buckets.every((bucket, index) => isRecord(bucket) &&
|
|
887
|
-
hasExactKeys(bucket, ["bucket", "settled", "expired"]) &&
|
|
888
|
-
bucket.bucket === names[index] &&
|
|
889
|
-
isNonNegativeInteger(bucket.settled) &&
|
|
890
|
-
isNonNegativeInteger(bucket.expired) &&
|
|
891
|
-
bucket.expired <= bucket.settled) &&
|
|
892
|
-
value.buckets.reduce((sum, bucket) => sum + Number(bucket.settled), 0) ===
|
|
893
|
-
value.terminalEvents &&
|
|
894
|
-
value.buckets.reduce((sum, bucket) => sum + Number(bucket.expired), 0) ===
|
|
895
|
-
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;
|
|
896
452
|
}
|
|
897
453
|
function isSnapshotTruncation(value) {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
"routes",
|
|
903
|
-
"consentEdges",
|
|
904
|
-
"messages",
|
|
905
|
-
"alerts",
|
|
906
|
-
], [
|
|
907
|
-
"progressWatches",
|
|
908
|
-
"progressWatchEvents",
|
|
909
|
-
"activityEvents",
|
|
910
|
-
]) &&
|
|
911
|
-
isNonNegativeInteger(value.connectors) &&
|
|
912
|
-
isNonNegativeInteger(value.availablePeers) &&
|
|
913
|
-
isNonNegativeInteger(value.routes) &&
|
|
914
|
-
isNonNegativeInteger(value.consentEdges) &&
|
|
915
|
-
isNonNegativeInteger(value.messages) &&
|
|
916
|
-
isNonNegativeInteger(value.alerts) &&
|
|
917
|
-
(value.progressWatches === undefined ||
|
|
918
|
-
isNonNegativeInteger(value.progressWatches)) &&
|
|
919
|
-
(value.progressWatchEvents === undefined ||
|
|
920
|
-
isNonNegativeInteger(value.progressWatchEvents)) &&
|
|
921
|
-
(value.activityEvents === undefined ||
|
|
922
|
-
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]));
|
|
923
458
|
}
|
|
924
459
|
export function isGatewaySnapshot(value) {
|
|
925
|
-
if (!
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
"alerts",
|
|
938
|
-
"truncation",
|
|
939
|
-
], [
|
|
940
|
-
"progressWatches",
|
|
941
|
-
"progressWatchEvents",
|
|
942
|
-
"activityEvents",
|
|
943
|
-
"deadlinePressure",
|
|
944
|
-
]) ||
|
|
945
|
-
value.schemaVersion !== 2 ||
|
|
946
|
-
!isIsoTimestamp(value.generatedAt) ||
|
|
947
|
-
(value.inboundMode !== "paired" && value.inboundMode !== "open") ||
|
|
948
|
-
!isConnectorHealth(value.health) ||
|
|
949
|
-
!Array.isArray(value.connectors) ||
|
|
950
|
-
value.connectors.length > gatewayPublicSnapshotLimits.connectors ||
|
|
951
|
-
!value.connectors.every(isConnectorSnapshot) ||
|
|
952
|
-
!Array.isArray(value.availablePeers) ||
|
|
953
|
-
value.availablePeers.length > gatewayPublicSnapshotLimits.availablePeers ||
|
|
954
|
-
!value.availablePeers.every(isAvailablePeerSnapshot) ||
|
|
955
|
-
!Array.isArray(value.routes) ||
|
|
956
|
-
value.routes.length > gatewayPublicSnapshotLimits.routes ||
|
|
957
|
-
!value.routes.every(isRouteSnapshot) ||
|
|
958
|
-
!Array.isArray(value.consentEdges) ||
|
|
959
|
-
value.consentEdges.length > gatewayPublicSnapshotLimits.consentEdges ||
|
|
960
|
-
!value.consentEdges.every(isConsentEdgeSnapshot) ||
|
|
961
|
-
(value.progressWatches !== undefined &&
|
|
962
|
-
(!Array.isArray(value.progressWatches) ||
|
|
963
|
-
value.progressWatches.length >
|
|
964
|
-
gatewayPublicSnapshotLimits.progressWatches ||
|
|
965
|
-
!value.progressWatches.every(isProgressWatchSnapshot))) ||
|
|
966
|
-
(value.progressWatchEvents !== undefined &&
|
|
967
|
-
(!Array.isArray(value.progressWatchEvents) ||
|
|
968
|
-
value.progressWatchEvents.length >
|
|
969
|
-
gatewayPublicSnapshotLimits.progressWatchEvents ||
|
|
970
|
-
!value.progressWatchEvents.every(isProgressWatchEventSnapshot))) ||
|
|
971
|
-
(value.activityEvents !== undefined &&
|
|
972
|
-
(!Array.isArray(value.activityEvents) ||
|
|
973
|
-
value.activityEvents.length >
|
|
974
|
-
gatewayPublicSnapshotLimits.activityEvents ||
|
|
975
|
-
!value.activityEvents.every(isGatewayActivityEvent))) ||
|
|
976
|
-
(value.deadlinePressure !== undefined &&
|
|
977
|
-
!isDeadlinePressure(value.deadlinePressure)) ||
|
|
978
|
-
!Array.isArray(value.messages) ||
|
|
979
|
-
value.messages.length > gatewayPublicSnapshotLimits.messages ||
|
|
980
|
-
!value.messages.every(isNormalizedMessageEvent) ||
|
|
981
|
-
!isAccounting(value.accounting) ||
|
|
982
|
-
!Array.isArray(value.alerts) ||
|
|
983
|
-
value.alerts.length > gatewayPublicSnapshotLimits.alerts ||
|
|
984
|
-
!value.alerts.every(isSafeAlert) ||
|
|
985
|
-
!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 }))
|
|
986
472
|
return false;
|
|
987
|
-
|
|
988
|
-
const
|
|
989
|
-
const
|
|
990
|
-
const
|
|
991
|
-
const
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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));
|
|
486
|
+
});
|
|
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;
|
|
1003
490
|
}
|
|
1004
491
|
function isSnapshotObservation(value) {
|
|
1005
|
-
return (
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
return isSendResult(value);
|
|
1034
|
-
case "refresh_dashboard":
|
|
1035
|
-
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);
|
|
1036
520
|
}
|
|
1037
521
|
}
|
|
1038
522
|
function errorResponse(code) {
|
|
1039
|
-
return {
|
|
1040
|
-
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
1041
|
-
ok: false,
|
|
1042
|
-
error: { code, message: WIRE_ERROR_MESSAGES[code] },
|
|
1043
|
-
};
|
|
523
|
+
return { protocolVersion: 1, ok: false, error: { code, message: WIRE_ERROR_MESSAGES[code] } };
|
|
1044
524
|
}
|
|
1045
525
|
async function dispatch(request, handlers) {
|
|
1046
|
-
|
|
526
|
+
const descriptor = descriptors[request.method];
|
|
527
|
+
const handler = handlers[descriptor.handler];
|
|
1047
528
|
try {
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
result = await handlers.health();
|
|
1051
|
-
break;
|
|
1052
|
-
case "register_codex":
|
|
1053
|
-
result = await handlers.registerCodex(request.params);
|
|
1054
|
-
break;
|
|
1055
|
-
case "unregister_codex":
|
|
1056
|
-
result = await handlers.unregisterCodex(request.params);
|
|
1057
|
-
break;
|
|
1058
|
-
case "remove_stale_codex_registration":
|
|
1059
|
-
result = await handlers.removeStaleCodexRegistration(request.params);
|
|
1060
|
-
break;
|
|
1061
|
-
case "select_claude":
|
|
1062
|
-
result = await handlers.selectClaude(request.params);
|
|
1063
|
-
break;
|
|
1064
|
-
case "unselect_claude":
|
|
1065
|
-
result = await handlers.unselectClaude(request.params);
|
|
1066
|
-
break;
|
|
1067
|
-
case "pair":
|
|
1068
|
-
result = await handlers.pair(request.params);
|
|
1069
|
-
break;
|
|
1070
|
-
case "unpair":
|
|
1071
|
-
result = await handlers.unpair(request.params);
|
|
1072
|
-
break;
|
|
1073
|
-
case "list_snapshot":
|
|
1074
|
-
result = await handlers.listSnapshot();
|
|
1075
|
-
break;
|
|
1076
|
-
case "observe_snapshot":
|
|
1077
|
-
result = await handlers.observeSnapshot();
|
|
1078
|
-
break;
|
|
1079
|
-
case "delivery_status":
|
|
1080
|
-
result = await handlers.deliveryStatus(request.params);
|
|
1081
|
-
break;
|
|
1082
|
-
case "untrack":
|
|
1083
|
-
result = await handlers.untrack(request.params);
|
|
1084
|
-
break;
|
|
1085
|
-
case "send_to_claude":
|
|
1086
|
-
result = await handlers.sendToClaude(request.params);
|
|
1087
|
-
break;
|
|
1088
|
-
case "send_to_codex":
|
|
1089
|
-
result = await handlers.sendToCodex(request.params);
|
|
1090
|
-
break;
|
|
1091
|
-
case "reply":
|
|
1092
|
-
result = await handlers.reply(request.params);
|
|
1093
|
-
break;
|
|
1094
|
-
case "refresh_dashboard":
|
|
1095
|
-
result = await handlers.refreshDashboard();
|
|
1096
|
-
break;
|
|
1097
|
-
}
|
|
529
|
+
const result = await (Object.keys(request.params).length === 0 ? handler() : handler(request.params));
|
|
530
|
+
return { protocolVersion: 1, ok: true, result };
|
|
1098
531
|
}
|
|
1099
532
|
catch {
|
|
1100
533
|
return errorResponse("HANDLER_FAILURE");
|
|
1101
534
|
}
|
|
1102
|
-
if (!isResultForMethod(request.method, result)) {
|
|
1103
|
-
return errorResponse("INVALID_HANDLER_RESPONSE");
|
|
1104
|
-
}
|
|
1105
|
-
return {
|
|
1106
|
-
protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
|
|
1107
|
-
ok: true,
|
|
1108
|
-
result,
|
|
1109
|
-
};
|
|
1110
535
|
}
|
|
1111
536
|
function serializeResponse(response) {
|
|
1112
|
-
const encoded = Buffer.from(`${JSON.stringify(response)}\n`, "utf8");
|
|
1113
|
-
if (encoded.length <= GATEWAY_CONTROL_MAX_RESPONSE_BYTES)
|
|
1114
|
-
return encoded;
|
|
1115
|
-
return Buffer.from(`${JSON.stringify(errorResponse("RESPONSE_TOO_LARGE"))}\n`, "utf8");
|
|
1116
|
-
}
|
|
1117
|
-
function respond(socket, response) {
|
|
1118
|
-
if (socket.destroyed)
|
|
1119
|
-
return;
|
|
1120
|
-
let encoded;
|
|
1121
537
|
try {
|
|
1122
|
-
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`);
|
|
1123
540
|
}
|
|
1124
541
|
catch {
|
|
1125
|
-
|
|
542
|
+
return Buffer.from(`${JSON.stringify(errorResponse("INVALID_HANDLER_RESPONSE"))}\n`);
|
|
1126
543
|
}
|
|
1127
|
-
socket.end(encoded);
|
|
1128
544
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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")));
|
|
1144
583
|
});
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
if (newline < 0)
|
|
1155
|
-
return;
|
|
1156
|
-
const trailing = buffered.subarray(newline + 1).toString("utf8");
|
|
1157
|
-
if (trailing.trim().length > 0) {
|
|
1158
|
-
complete(errorResponse("MULTIPLE_FRAMES"));
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
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);
|
|
1162
593
|
let request;
|
|
1163
594
|
try {
|
|
1164
|
-
request =
|
|
595
|
+
request = parseRequestObject(decodeJson(frame, "INVALID_JSON"));
|
|
1165
596
|
}
|
|
1166
597
|
catch (error) {
|
|
1167
|
-
|
|
1168
|
-
complete(errorResponse(code));
|
|
598
|
+
respond(socket, errorResponse(error instanceof ProtocolFault ? error.code : "INVALID_REQUEST"));
|
|
1169
599
|
return;
|
|
1170
600
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
|
1178
611
|
socket.destroy();
|
|
1179
612
|
});
|
|
1180
613
|
}
|
|
1181
|
-
function controlTransportError(code, ambiguous = false) {
|
|
1182
|
-
const messages = {
|
|
1183
|
-
UNSUPPORTED_PLATFORM: "Unix-domain gateway control sockets are unavailable.",
|
|
1184
|
-
INVALID_STATE_DIR: "The gateway state directory is invalid.",
|
|
1185
|
-
INSECURE_STATE_DIR: "The gateway state directory is not private to this user.",
|
|
1186
|
-
INVALID_SOCKET_PATH: "The gateway control socket path is invalid.",
|
|
1187
|
-
UNSAFE_SOCKET_TARGET: "The gateway control socket target is unsafe.",
|
|
1188
|
-
SOCKET_IN_USE: "The gateway control socket is already served by a live process.",
|
|
1189
|
-
SOCKET_PROBE_FAILED: "The gateway control socket could not be checked safely.",
|
|
1190
|
-
SOCKET_BIND_FAILED: "The gateway control socket could not be bound.",
|
|
1191
|
-
SOCKET_PERMISSION_FAILED: "The gateway control socket permissions could not be secured.",
|
|
1192
|
-
SOCKET_CLEANUP_CONFLICT: "The gateway control socket path changed during cleanup.",
|
|
1193
|
-
CONTROL_CONNECT_FAILED: "The gateway control socket could not be reached.",
|
|
1194
|
-
CONTROL_TIMEOUT: "The gateway control request timed out.",
|
|
1195
|
-
CONTROL_RESPONSE_TOO_LARGE: "The gateway control response exceeds the client limit.",
|
|
1196
|
-
CONTROL_INVALID_RESPONSE: "The gateway returned an invalid control response. Restart the broker, then retry.",
|
|
1197
|
-
CONTROL_CONNECTION_CLOSED: "The gateway closed before returning a control response.",
|
|
1198
|
-
CONTROL_OUTCOME_AMBIGUOUS: "The gateway may have applied the control mutation before the response was lost; do not retry automatically.",
|
|
1199
|
-
};
|
|
1200
|
-
return new GatewayControlTransportError(code, messages[code] ?? "The gateway control transport failed.", ambiguous);
|
|
1201
|
-
}
|
|
1202
|
-
function isNonIdempotentControlMethod(method) {
|
|
1203
|
-
return (method === "register_codex" ||
|
|
1204
|
-
method === "unregister_codex" ||
|
|
1205
|
-
method === "remove_stale_codex_registration" ||
|
|
1206
|
-
method === "select_claude" ||
|
|
1207
|
-
method === "unselect_claude" ||
|
|
1208
|
-
method === "pair" ||
|
|
1209
|
-
method === "unpair" ||
|
|
1210
|
-
method === "send_to_claude" ||
|
|
1211
|
-
method === "send_to_codex" ||
|
|
1212
|
-
method === "reply");
|
|
1213
|
-
}
|
|
1214
614
|
async function optionalLstat(target) {
|
|
1215
615
|
try {
|
|
1216
616
|
return await lstat(target);
|
|
@@ -1221,15 +621,19 @@ async function optionalLstat(target) {
|
|
|
1221
621
|
throw error;
|
|
1222
622
|
}
|
|
1223
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
|
+
}
|
|
1224
631
|
async function validateSocketLocation(stateDir, socketPath) {
|
|
1225
632
|
if (process.platform === "win32") {
|
|
1226
633
|
throw controlTransportError("UNSUPPORTED_PLATFORM");
|
|
1227
634
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
!path.isAbsolute(socketPath) ||
|
|
1231
|
-
path.resolve(socketPath) !== socketPath ||
|
|
1232
|
-
Buffer.byteLength(socketPath, "utf8") > MAX_SOCKET_PATH_BYTES) {
|
|
635
|
+
assertSocketPath(socketPath);
|
|
636
|
+
if (!path.isAbsolute(stateDir) || path.resolve(stateDir) !== stateDir) {
|
|
1233
637
|
throw controlTransportError("INVALID_SOCKET_PATH");
|
|
1234
638
|
}
|
|
1235
639
|
let stateInfo;
|
|
@@ -1351,8 +755,7 @@ async function removeOwnedSocket(socketPath, identity) {
|
|
|
1351
755
|
await unlink(socketPath);
|
|
1352
756
|
}
|
|
1353
757
|
catch {
|
|
1354
|
-
//
|
|
1355
|
-
// broaden cleanup to any replacement path.
|
|
758
|
+
// Never broaden cleanup to a replacement path.
|
|
1356
759
|
}
|
|
1357
760
|
}
|
|
1358
761
|
async function protectSocketReplacement(socketPath, identity) {
|
|
@@ -1405,24 +808,17 @@ async function restoreSocketReplacement(socketPath, replacement) {
|
|
|
1405
808
|
throw controlTransportError("SOCKET_CLEANUP_CONFLICT");
|
|
1406
809
|
}
|
|
1407
810
|
}
|
|
1408
|
-
function validServerTimeout(value) {
|
|
1409
|
-
return Number.isInteger(value) && value >= 50 && value <= 30_000;
|
|
1410
|
-
}
|
|
1411
|
-
function validClientTimeout(value) {
|
|
1412
|
-
return Number.isInteger(value) && value >= 50 && value <= 90_000;
|
|
1413
|
-
}
|
|
1414
811
|
export async function startGatewayControlServer(options) {
|
|
1415
812
|
await validateSocketLocation(options.stateDir, options.socketPath);
|
|
1416
813
|
await prepareSocketTarget(options.socketPath);
|
|
1417
814
|
const requestTimeoutMs = options.requestTimeoutMs ?? GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
|
|
1418
|
-
if (!
|
|
815
|
+
if (!Number.isInteger(requestTimeoutMs) ||
|
|
816
|
+
requestTimeoutMs < 50 ||
|
|
817
|
+
requestTimeoutMs > 30_000) {
|
|
1419
818
|
throw controlTransportError("INVALID_SOCKET_PATH");
|
|
1420
819
|
}
|
|
1421
820
|
const connections = new Set();
|
|
1422
821
|
const server = net.createServer({ allowHalfOpen: true }, (socket) => {
|
|
1423
|
-
// Directory ownership proves same-user access, not that every caller is a
|
|
1424
|
-
// trusted agent. Bound idle and in-flight peers before allocating parser or
|
|
1425
|
-
// handler state so a local peer cannot grow this set without limit.
|
|
1426
822
|
if (connections.size >= GATEWAY_CONTROL_MAX_CONNECTIONS) {
|
|
1427
823
|
socket.once("error", () => socket.destroy());
|
|
1428
824
|
socket.end(serializeResponse(errorResponse("SERVER_BUSY")), () => socket.destroy());
|
|
@@ -1461,8 +857,8 @@ export async function startGatewayControlServer(options) {
|
|
|
1461
857
|
}
|
|
1462
858
|
}
|
|
1463
859
|
catch {
|
|
1464
|
-
for (const
|
|
1465
|
-
|
|
860
|
+
for (const socket of connections)
|
|
861
|
+
socket.destroy();
|
|
1466
862
|
await closeNativeServer(server);
|
|
1467
863
|
if (identity)
|
|
1468
864
|
await removeOwnedSocket(options.socketPath, identity);
|
|
@@ -1474,8 +870,8 @@ export async function startGatewayControlServer(options) {
|
|
|
1474
870
|
const close = () => {
|
|
1475
871
|
closePromise ??= (async () => {
|
|
1476
872
|
const replacement = await protectSocketReplacement(options.socketPath, ownedIdentity);
|
|
1477
|
-
for (const
|
|
1478
|
-
|
|
873
|
+
for (const socket of connections)
|
|
874
|
+
socket.destroy();
|
|
1479
875
|
await closeNativeServer(server);
|
|
1480
876
|
await removeOwnedSocket(options.socketPath, ownedIdentity);
|
|
1481
877
|
await restoreSocketReplacement(options.socketPath, replacement);
|
|
@@ -1491,117 +887,63 @@ export async function startGatewayControlServer(options) {
|
|
|
1491
887
|
close,
|
|
1492
888
|
};
|
|
1493
889
|
}
|
|
1494
|
-
function
|
|
1495
|
-
if (!isRecord(value) || value.protocolVersion !== 1)
|
|
890
|
+
function decodeResponse(method, value) {
|
|
891
|
+
if (!isRecord(value) || value.protocolVersion !== 1)
|
|
1496
892
|
throw controlTransportError("CONTROL_INVALID_RESPONSE");
|
|
1497
|
-
}
|
|
1498
893
|
if (value.ok === false) {
|
|
1499
|
-
if (!
|
|
1500
|
-
|
|
1501
|
-
!
|
|
1502
|
-
typeof value.error.code !== "string" ||
|
|
1503
|
-
!Object.hasOwn(WIRE_ERROR_MESSAGES, value.error.code) ||
|
|
1504
|
-
value.error.message !==
|
|
1505
|
-
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])
|
|
1506
897
|
throw controlTransportError("CONTROL_INVALID_RESPONSE");
|
|
1507
|
-
}
|
|
1508
898
|
return value;
|
|
1509
899
|
}
|
|
1510
|
-
if (value
|
|
1511
|
-
!hasExactKeys(value, ["protocolVersion", "ok", "result"]) ||
|
|
1512
|
-
!isResultForMethod(method, value.result)) {
|
|
900
|
+
if (!shape(value, { protocolVersion: oneOf(1), ok: oneOf(true), result: descriptors[method].result }))
|
|
1513
901
|
throw controlTransportError("CONTROL_INVALID_RESPONSE");
|
|
1514
|
-
}
|
|
1515
902
|
return value;
|
|
1516
903
|
}
|
|
1517
904
|
export async function sendGatewayControlRequest(options) {
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
const timeoutMs = options.timeoutMs ?? GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
|
|
1524
|
-
const maxResponseBytes = options.maxResponseBytes ?? GATEWAY_CONTROL_MAX_RESPONSE_BYTES;
|
|
1525
|
-
if (!validClientTimeout(timeoutMs) ||
|
|
1526
|
-
!Number.isInteger(maxResponseBytes) ||
|
|
1527
|
-
maxResponseBytes < 256 ||
|
|
1528
|
-
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)
|
|
1529
910
|
throw controlTransportError("CONTROL_INVALID_RESPONSE");
|
|
1530
|
-
|
|
1531
|
-
const
|
|
1532
|
-
if (
|
|
1533
|
-
throw new ProtocolFault("INVALID_REQUEST");
|
|
1534
|
-
}
|
|
1535
|
-
const requestFrame = Buffer.from(`${JSON.stringify(normalized)}\n`, "utf8");
|
|
1536
|
-
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)
|
|
1537
914
|
throw new ProtocolFault("FRAME_TOO_LARGE");
|
|
1538
|
-
}
|
|
1539
915
|
return await new Promise((resolve, reject) => {
|
|
1540
916
|
const socket = net.createConnection(options.socketPath);
|
|
1541
917
|
let settled = false;
|
|
1542
918
|
let writeStarted = false;
|
|
1543
|
-
|
|
1544
|
-
const mutation = isNonIdempotentControlMethod(normalized.method);
|
|
919
|
+
const mutation = descriptors[request.method].mutation;
|
|
1545
920
|
const fail = (error) => {
|
|
1546
921
|
if (settled)
|
|
1547
922
|
return;
|
|
1548
923
|
settled = true;
|
|
1549
924
|
socket.destroy();
|
|
1550
|
-
reject(mutation && writeStarted
|
|
1551
|
-
? controlTransportError("CONTROL_OUTCOME_AMBIGUOUS", true)
|
|
1552
|
-
: error);
|
|
1553
|
-
};
|
|
1554
|
-
const succeed = (response) => {
|
|
1555
|
-
if (settled)
|
|
1556
|
-
return;
|
|
1557
|
-
settled = true;
|
|
1558
|
-
socket.destroy();
|
|
1559
|
-
resolve(response);
|
|
925
|
+
reject(mutation && writeStarted ? controlTransportError("CONTROL_OUTCOME_AMBIGUOUS", true) : error);
|
|
1560
926
|
};
|
|
1561
|
-
socket.setTimeout(
|
|
1562
|
-
socket.once("connect", () => {
|
|
1563
|
-
|
|
1564
|
-
// the frame. Losing the response to a mutation is therefore ambiguous,
|
|
1565
|
-
// never a safe retry signal.
|
|
1566
|
-
writeStarted = true;
|
|
1567
|
-
socket.write(requestFrame);
|
|
1568
|
-
});
|
|
1569
|
-
socket.once("timeout", () => fail(controlTransportError("CONTROL_TIMEOUT")));
|
|
1570
|
-
socket.once("error", () => fail(controlTransportError("CONTROL_CONNECT_FAILED")));
|
|
1571
|
-
socket.on("data", (chunk) => {
|
|
1572
|
-
if (settled)
|
|
1573
|
-
return;
|
|
1574
|
-
if (buffered.length + chunk.length > maxResponseBytes) {
|
|
1575
|
-
fail(controlTransportError("CONTROL_RESPONSE_TOO_LARGE"));
|
|
1576
|
-
return;
|
|
1577
|
-
}
|
|
1578
|
-
buffered = Buffer.concat([buffered, chunk]);
|
|
1579
|
-
const newline = buffered.indexOf(0x0a);
|
|
1580
|
-
if (newline < 0)
|
|
1581
|
-
return;
|
|
1582
|
-
if (buffered.subarray(newline + 1).toString("utf8").trim().length > 0) {
|
|
1583
|
-
fail(controlTransportError("CONTROL_INVALID_RESPONSE"));
|
|
1584
|
-
return;
|
|
1585
|
-
}
|
|
1586
|
-
let parsed;
|
|
927
|
+
socket.setTimeout(timeout);
|
|
928
|
+
socket.once("connect", () => { writeStarted = true; socket.write(frame); });
|
|
929
|
+
void readOneFrame(socket, maximum).then((responseFrame) => {
|
|
1587
930
|
try {
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
try {
|
|
1595
|
-
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
|
+
}
|
|
1596
937
|
}
|
|
1597
938
|
catch {
|
|
1598
939
|
fail(controlTransportError("CONTROL_INVALID_RESPONSE"));
|
|
1599
940
|
}
|
|
1600
|
-
})
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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));
|
|
1605
947
|
});
|
|
1606
948
|
});
|
|
1607
949
|
}
|