@linkshell/gateway 0.3.9 → 0.3.10
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/Dockerfile +1 -3
- package/README.md +13 -14
- package/dist/gateway/src/agent-permission-http.d.ts +74 -19
- package/dist/gateway/src/agent-permission-http.js +56 -16
- package/dist/gateway/src/agent-permission-http.js.map +1 -1
- package/dist/gateway/src/embedded.js +61 -153
- package/dist/gateway/src/embedded.js.map +1 -1
- package/dist/gateway/src/index.js +98 -193
- package/dist/gateway/src/index.js.map +1 -1
- package/dist/gateway/src/pairings.d.ts +3 -3
- package/dist/gateway/src/pairings.js +5 -4
- package/dist/gateway/src/pairings.js.map +1 -1
- package/dist/gateway/src/relay.d.ts +2 -2
- package/dist/gateway/src/relay.js +78 -156
- package/dist/gateway/src/relay.js.map +1 -1
- package/dist/gateway/src/sessions.d.ts +28 -42
- package/dist/gateway/src/sessions.js +145 -200
- package/dist/gateway/src/sessions.js.map +1 -1
- package/dist/gateway/src/state-store.d.ts +6 -9
- package/dist/gateway/src/state-store.js +19 -26
- package/dist/gateway/src/state-store.js.map +1 -1
- package/dist/gateway/src/tokens.d.ts +7 -27
- package/dist/gateway/src/tokens.js +60 -86
- package/dist/gateway/src/tokens.js.map +1 -1
- package/dist/gateway/src/tunnel.d.ts +13 -11
- package/dist/gateway/src/tunnel.js +36 -36
- package/dist/gateway/src/tunnel.js.map +1 -1
- package/dist/gateway/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +11940 -3423
- package/dist/shared-protocol/src/index.js +98 -164
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +11 -11
- package/src/agent-permission-http.ts +63 -20
- package/src/embedded.ts +60 -158
- package/src/index.ts +98 -199
- package/src/pairings.ts +7 -6
- package/src/relay.ts +90 -188
- package/src/sessions.ts +150 -213
- package/src/state-store.ts +25 -41
- package/src/tokens.ts +63 -109
- package/src/tunnel.ts +43 -49
package/src/relay.ts
CHANGED
|
@@ -2,12 +2,13 @@ import type WebSocket from "ws";
|
|
|
2
2
|
import {
|
|
3
3
|
parseEnvelope,
|
|
4
4
|
parseTypedPayload,
|
|
5
|
+
protocolMessageSchemas,
|
|
5
6
|
serializeEnvelope,
|
|
6
7
|
createEnvelope,
|
|
7
8
|
} from "@linkshell/protocol";
|
|
8
9
|
import type { Envelope, ProtocolMessageType } from "@linkshell/protocol";
|
|
9
10
|
import { ZodError } from "zod";
|
|
10
|
-
import type {
|
|
11
|
+
import type { SessionManager, ConnectedDevice } from "./sessions.js";
|
|
11
12
|
import {
|
|
12
13
|
handleTunnelResponse,
|
|
13
14
|
handleTunnelWsData,
|
|
@@ -15,88 +16,40 @@ import {
|
|
|
15
16
|
} from "./tunnel.js";
|
|
16
17
|
import { resolveAgentPermissionHttpAck } from "./agent-permission-http.js";
|
|
17
18
|
|
|
18
|
-
const AGENT_SNAPSHOT_WARN_BYTES = Number(
|
|
19
|
-
process.env.AGENT_SNAPSHOT_WARN_BYTES ?? 1024 * 1024,
|
|
20
|
-
);
|
|
21
|
-
const DEFAULT_WS_BUFFERED_AMOUNT_LIMIT = 16 * 1024 * 1024;
|
|
22
|
-
|
|
23
|
-
function wsBufferedAmountLimit(): number {
|
|
24
|
-
return Number(process.env.WS_BUFFERED_AMOUNT_LIMIT ?? DEFAULT_WS_BUFFERED_AMOUNT_LIMIT);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
export function handleSocketMessage(
|
|
28
20
|
socket: WebSocket,
|
|
29
21
|
raw: string,
|
|
30
22
|
role: "host" | "client",
|
|
31
|
-
|
|
23
|
+
sessionId: string,
|
|
32
24
|
deviceId: string,
|
|
33
|
-
sessions:
|
|
25
|
+
sessions: SessionManager,
|
|
34
26
|
): void {
|
|
35
|
-
const codexHeader = tryParseCodexRpcHeader(raw);
|
|
36
|
-
if (codexHeader && codexHeader.type === "agent.codex.rpc") {
|
|
37
|
-
if (codexHeader.hostDeviceId !== hostDeviceId) {
|
|
38
|
-
sendSessionError(
|
|
39
|
-
socket,
|
|
40
|
-
hostDeviceId,
|
|
41
|
-
"invalid_message",
|
|
42
|
-
"Envelope hostDeviceId does not match connection hostDeviceId",
|
|
43
|
-
);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const session = sessions.get(hostDeviceId);
|
|
48
|
-
if (!session) {
|
|
49
|
-
sendSessionError(socket, hostDeviceId, "device_not_found", "Device not found");
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (role === "client" && !clientHasControl(session, deviceId)) {
|
|
54
|
-
sendControlConflict(socket, hostDeviceId);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const routeEnvelope: Envelope = {
|
|
59
|
-
id: "",
|
|
60
|
-
type: "agent.codex.rpc",
|
|
61
|
-
hostDeviceId,
|
|
62
|
-
timestamp: new Date().toISOString(),
|
|
63
|
-
payload: {},
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
if (role === "host") {
|
|
67
|
-
broadcastToClients(session, routeEnvelope, raw);
|
|
68
|
-
} else {
|
|
69
|
-
sendToHost(session, routeEnvelope, raw);
|
|
70
|
-
}
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
27
|
let envelope: Envelope;
|
|
75
28
|
try {
|
|
76
29
|
envelope = parseEnvelope(raw);
|
|
77
30
|
} catch {
|
|
78
|
-
sendSessionError(socket,
|
|
31
|
+
sendSessionError(socket, sessionId, "invalid_message", "Failed to parse envelope");
|
|
79
32
|
return;
|
|
80
33
|
}
|
|
81
34
|
|
|
82
|
-
if (envelope.
|
|
35
|
+
if (envelope.sessionId !== sessionId) {
|
|
83
36
|
sendSessionError(
|
|
84
37
|
socket,
|
|
85
|
-
|
|
38
|
+
sessionId,
|
|
86
39
|
"invalid_message",
|
|
87
|
-
"Envelope
|
|
40
|
+
"Envelope sessionId does not match connection sessionId",
|
|
88
41
|
);
|
|
89
42
|
return;
|
|
90
43
|
}
|
|
91
44
|
|
|
92
|
-
const session = sessions.get(
|
|
45
|
+
const session = sessions.get(sessionId);
|
|
93
46
|
if (!session) {
|
|
94
|
-
sendSessionError(socket,
|
|
47
|
+
sendSessionError(socket, sessionId, "session_not_found", "Session not found");
|
|
95
48
|
return;
|
|
96
49
|
}
|
|
97
50
|
|
|
98
51
|
try {
|
|
99
|
-
if (
|
|
52
|
+
if (isProtocolMessageType(envelope.type)) {
|
|
100
53
|
envelope = {
|
|
101
54
|
...envelope,
|
|
102
55
|
payload: parseTypedPayload(envelope.type, envelope.payload),
|
|
@@ -104,73 +57,31 @@ export function handleSocketMessage(
|
|
|
104
57
|
}
|
|
105
58
|
|
|
106
59
|
if (role === "host") {
|
|
107
|
-
handleHostMessage(envelope,
|
|
60
|
+
handleHostMessage(envelope, session, sessions);
|
|
108
61
|
} else {
|
|
109
|
-
handleClientMessage(envelope,
|
|
62
|
+
handleClientMessage(envelope, socket, session, deviceId, sessions);
|
|
110
63
|
}
|
|
111
64
|
} catch (error) {
|
|
112
65
|
if (error instanceof ZodError) {
|
|
113
66
|
sendSessionError(
|
|
114
67
|
socket,
|
|
115
|
-
|
|
68
|
+
sessionId,
|
|
116
69
|
"invalid_message",
|
|
117
|
-
error.
|
|
70
|
+
error.errors[0]?.message ?? "Invalid message payload",
|
|
118
71
|
);
|
|
119
72
|
return;
|
|
120
73
|
}
|
|
121
|
-
sendSessionError(socket,
|
|
74
|
+
sendSessionError(socket, sessionId, "invalid_message", "Failed to handle message");
|
|
122
75
|
}
|
|
123
76
|
}
|
|
124
77
|
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
if (!trimmed.startsWith("{")) return;
|
|
128
|
-
const snippet = trimmed.slice(0, 8192);
|
|
129
|
-
const hasType = /"type"\s*:\s*"agent\.codex\.rpc"/.test(snippet);
|
|
130
|
-
if (!hasType) return;
|
|
131
|
-
const hostMatch = /"hostDeviceId"\s*:\s*"([^"\\]*?)"/.exec(snippet);
|
|
132
|
-
if (!hostMatch?.[1]) return;
|
|
133
|
-
return { type: "agent.codex.rpc", hostDeviceId: hostMatch[1] };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function clientHasControl(session: ReturnType<DeviceManager["get"]> & {}, deviceId: string): boolean {
|
|
137
|
-
return session.controllerId === deviceId;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function sendControlConflict(socket: WebSocket, hostDeviceId: string): void {
|
|
141
|
-
safeSend(
|
|
142
|
-
socket,
|
|
143
|
-
serializeEnvelope(
|
|
144
|
-
createEnvelope({
|
|
145
|
-
type: "device.error",
|
|
146
|
-
hostDeviceId,
|
|
147
|
-
payload: {
|
|
148
|
-
code: "control_conflict",
|
|
149
|
-
message: "Not the controller",
|
|
150
|
-
},
|
|
151
|
-
}),
|
|
152
|
-
),
|
|
153
|
-
hostDeviceId,
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function shouldValidatePayload(type: string): type is ProtocolMessageType {
|
|
158
|
-
return (
|
|
159
|
-
type === "device.connect" ||
|
|
160
|
-
type === "device.ack" ||
|
|
161
|
-
type === "device.resume" ||
|
|
162
|
-
type === "terminal.input" ||
|
|
163
|
-
type === "terminal.resize" ||
|
|
164
|
-
type === "permission.decision" ||
|
|
165
|
-
type === "permission.decision.result" ||
|
|
166
|
-
type === "control.claim" ||
|
|
167
|
-
type === "control.release"
|
|
168
|
-
);
|
|
78
|
+
function isProtocolMessageType(type: string): type is ProtocolMessageType {
|
|
79
|
+
return Object.prototype.hasOwnProperty.call(protocolMessageSchemas, type);
|
|
169
80
|
}
|
|
170
81
|
|
|
171
82
|
function sendSessionError(
|
|
172
83
|
socket: WebSocket,
|
|
173
|
-
|
|
84
|
+
sessionId: string,
|
|
174
85
|
code: string,
|
|
175
86
|
message: string,
|
|
176
87
|
): void {
|
|
@@ -178,8 +89,8 @@ function sendSessionError(
|
|
|
178
89
|
socket.send(
|
|
179
90
|
serializeEnvelope(
|
|
180
91
|
createEnvelope({
|
|
181
|
-
type: "
|
|
182
|
-
|
|
92
|
+
type: "session.error",
|
|
93
|
+
sessionId,
|
|
183
94
|
payload: { code, message },
|
|
184
95
|
}),
|
|
185
96
|
),
|
|
@@ -188,44 +99,42 @@ function sendSessionError(
|
|
|
188
99
|
|
|
189
100
|
function handleHostMessage(
|
|
190
101
|
envelope: Envelope,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
sessions: DeviceManager,
|
|
102
|
+
session: ReturnType<SessionManager["get"]> & {},
|
|
103
|
+
sessions: SessionManager,
|
|
194
104
|
): void {
|
|
195
105
|
switch (envelope.type) {
|
|
196
|
-
case "
|
|
106
|
+
case "session.connect": {
|
|
197
107
|
// Extract metadata from host's connect message
|
|
198
|
-
const p = parseTypedPayload("
|
|
199
|
-
if (p.machineId || p.hostname || p.platform || p.cwd || p.
|
|
108
|
+
const p = parseTypedPayload("session.connect", envelope.payload);
|
|
109
|
+
if (p.provider || p.machineId || p.hostname || p.platform || p.cwd || p.projectName) {
|
|
200
110
|
sessions.setMetadata(
|
|
201
111
|
session.id,
|
|
202
|
-
undefined,
|
|
112
|
+
p.provider ?? undefined,
|
|
203
113
|
p.machineId ?? undefined,
|
|
204
114
|
p.hostname ?? undefined,
|
|
205
115
|
p.platform ?? undefined,
|
|
206
116
|
p.cwd ?? undefined,
|
|
207
|
-
undefined,
|
|
208
|
-
p.capabilities ?? undefined,
|
|
117
|
+
p.projectName ?? undefined,
|
|
209
118
|
);
|
|
210
119
|
}
|
|
211
120
|
break;
|
|
212
121
|
}
|
|
213
122
|
case "terminal.output": {
|
|
214
123
|
sessions.bufferOutput(session.id, envelope);
|
|
215
|
-
broadcastToClients(session, envelope
|
|
124
|
+
broadcastToClients(session, envelope);
|
|
216
125
|
break;
|
|
217
126
|
}
|
|
218
127
|
case "terminal.exit": {
|
|
219
128
|
// Don't terminate session — other terminals may still be running
|
|
220
|
-
broadcastToClients(session, envelope
|
|
129
|
+
broadcastToClients(session, envelope);
|
|
221
130
|
break;
|
|
222
131
|
}
|
|
223
|
-
case "
|
|
132
|
+
case "session.heartbeat":
|
|
224
133
|
break;
|
|
225
134
|
case "permission.decision.result": {
|
|
226
135
|
const p = parseTypedPayload("permission.decision.result", envelope.payload);
|
|
227
136
|
resolveAgentPermissionHttpAck({
|
|
228
|
-
|
|
137
|
+
sessionId: session.id,
|
|
229
138
|
ack: {
|
|
230
139
|
requestId: p.requestId,
|
|
231
140
|
decision: p.decision,
|
|
@@ -235,7 +144,7 @@ function handleHostMessage(
|
|
|
235
144
|
message: p.message,
|
|
236
145
|
},
|
|
237
146
|
});
|
|
238
|
-
broadcastToClients(session, envelope
|
|
147
|
+
broadcastToClients(session, envelope);
|
|
239
148
|
break;
|
|
240
149
|
}
|
|
241
150
|
// Tunnel: host → gateway (not broadcast to clients)
|
|
@@ -256,80 +165,84 @@ function handleHostMessage(
|
|
|
256
165
|
}
|
|
257
166
|
case "control.grant":
|
|
258
167
|
case "control.reject":
|
|
259
|
-
broadcastToClients(session, envelope
|
|
168
|
+
broadcastToClients(session, envelope);
|
|
260
169
|
break;
|
|
261
170
|
// Screen sharing: host → clients
|
|
262
171
|
case "screen.frame":
|
|
263
172
|
case "screen.status":
|
|
264
173
|
case "screen.offer":
|
|
265
174
|
case "screen.ice":
|
|
266
|
-
//
|
|
267
|
-
case "agent.
|
|
268
|
-
|
|
175
|
+
// Agent GUI: host → clients
|
|
176
|
+
case "agent.capabilities":
|
|
177
|
+
case "agent.update":
|
|
178
|
+
case "agent.permission.request":
|
|
179
|
+
case "agent.snapshot":
|
|
269
180
|
case "agent.v2.capabilities":
|
|
270
181
|
case "agent.v2.conversation.opened":
|
|
271
182
|
case "agent.v2.conversation.list.result":
|
|
272
183
|
case "agent.v2.event":
|
|
273
184
|
case "agent.v2.snapshot":
|
|
274
|
-
if (envelope.type === "agent.v2.snapshot" && Buffer.byteLength(raw, "utf8") > AGENT_SNAPSHOT_WARN_BYTES) {
|
|
275
|
-
process.stderr.write(`[gateway:warn] oversized agent snapshot host=${session.id} bytes=${Buffer.byteLength(raw, "utf8")}\n`);
|
|
276
|
-
}
|
|
277
|
-
broadcastToClients(session, envelope, raw);
|
|
278
|
-
break;
|
|
279
|
-
case "agent.v2.history.page":
|
|
280
|
-
case "agent.v2.delta":
|
|
281
|
-
case "agent.v2.running_state":
|
|
282
185
|
case "agent.v2.permission.request":
|
|
283
186
|
// Multi-terminal: host → clients
|
|
284
187
|
case "terminal.spawned":
|
|
285
188
|
case "terminal.list":
|
|
286
189
|
case "terminal.browse.result":
|
|
287
190
|
case "terminal.file.read.result":
|
|
288
|
-
broadcastToClients(session, envelope
|
|
191
|
+
broadcastToClients(session, envelope);
|
|
289
192
|
break;
|
|
290
193
|
// Structured status from hooks
|
|
291
194
|
case "terminal.status":
|
|
292
195
|
sessions.cacheStatus(session.id, envelope);
|
|
293
|
-
broadcastToClients(session, envelope
|
|
196
|
+
broadcastToClients(session, envelope);
|
|
294
197
|
break;
|
|
295
198
|
default:
|
|
296
|
-
broadcastToClients(session, envelope
|
|
199
|
+
broadcastToClients(session, envelope);
|
|
297
200
|
break;
|
|
298
201
|
}
|
|
299
202
|
}
|
|
300
203
|
|
|
301
204
|
function handleClientMessage(
|
|
302
205
|
envelope: Envelope,
|
|
303
|
-
raw: string,
|
|
304
206
|
socket: WebSocket,
|
|
305
|
-
session: ReturnType<
|
|
207
|
+
session: ReturnType<SessionManager["get"]> & {},
|
|
306
208
|
deviceId: string,
|
|
307
|
-
sessions:
|
|
209
|
+
sessions: SessionManager,
|
|
308
210
|
): void {
|
|
309
211
|
const requireController = (): boolean => {
|
|
310
|
-
if (
|
|
311
|
-
|
|
212
|
+
if (session.controllerId === deviceId) return true;
|
|
213
|
+
socket.send(
|
|
214
|
+
serializeEnvelope(
|
|
215
|
+
createEnvelope({
|
|
216
|
+
type: "session.error",
|
|
217
|
+
sessionId: session.id,
|
|
218
|
+
payload: {
|
|
219
|
+
code: "control_conflict",
|
|
220
|
+
message: "Not the controller",
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
),
|
|
224
|
+
);
|
|
312
225
|
return false;
|
|
313
226
|
};
|
|
314
227
|
|
|
315
228
|
switch (envelope.type) {
|
|
316
229
|
case "terminal.input": {
|
|
317
230
|
if (!requireController()) return;
|
|
318
|
-
sendToHost(session, envelope
|
|
231
|
+
sendToHost(session, envelope);
|
|
319
232
|
break;
|
|
320
233
|
}
|
|
321
234
|
case "terminal.resize": {
|
|
322
235
|
if (!requireController()) return;
|
|
323
|
-
sendToHost(session, envelope
|
|
236
|
+
sendToHost(session, envelope);
|
|
324
237
|
break;
|
|
325
238
|
}
|
|
326
|
-
case "
|
|
239
|
+
case "session.ack": {
|
|
327
240
|
// Forward ACK to host
|
|
328
|
-
sendToHost(session, envelope
|
|
241
|
+
sendToHost(session, envelope);
|
|
329
242
|
break;
|
|
330
243
|
}
|
|
331
|
-
case "
|
|
332
|
-
const p = parseTypedPayload("
|
|
244
|
+
case "session.resume": {
|
|
245
|
+
const p = parseTypedPayload("session.resume", envelope.payload);
|
|
333
246
|
// Replay from gateway buffer first
|
|
334
247
|
const replay = sessions.getReplayFrom(
|
|
335
248
|
session.id,
|
|
@@ -338,24 +251,22 @@ function handleClientMessage(
|
|
|
338
251
|
);
|
|
339
252
|
for (const msg of replay) {
|
|
340
253
|
const payload = msg.payload as Record<string, unknown>;
|
|
341
|
-
|
|
342
|
-
socket,
|
|
254
|
+
socket.send(
|
|
343
255
|
serializeEnvelope(
|
|
344
256
|
createEnvelope({
|
|
345
257
|
type: "terminal.output",
|
|
346
|
-
|
|
258
|
+
sessionId: session.id,
|
|
347
259
|
terminalId: msg.terminalId,
|
|
348
260
|
seq: msg.seq,
|
|
349
261
|
payload: { ...payload, isReplay: true },
|
|
350
262
|
}),
|
|
351
263
|
),
|
|
352
|
-
session.id,
|
|
353
264
|
);
|
|
354
265
|
}
|
|
355
266
|
// Replay last terminal.status for each terminal
|
|
356
267
|
const statusReplay = sessions.getStatusReplay(session.id);
|
|
357
268
|
for (const statusMsg of statusReplay) {
|
|
358
|
-
|
|
269
|
+
socket.send(serializeEnvelope(statusMsg));
|
|
359
270
|
}
|
|
360
271
|
// Also forward resume to host so it can fill gaps beyond gateway buffer.
|
|
361
272
|
sendToHost(session, session.machineId
|
|
@@ -373,7 +284,7 @@ function handleClientMessage(
|
|
|
373
284
|
sessions.claimControl(session.id, deviceId);
|
|
374
285
|
const grantMsg = createEnvelope({
|
|
375
286
|
type: "control.grant",
|
|
376
|
-
|
|
287
|
+
sessionId: session.id,
|
|
377
288
|
payload: { deviceId },
|
|
378
289
|
});
|
|
379
290
|
// Broadcast to ALL clients so previous controller updates its state
|
|
@@ -385,20 +296,25 @@ function handleClientMessage(
|
|
|
385
296
|
sessions.releaseControl(session.id, deviceId);
|
|
386
297
|
const releaseMsg = createEnvelope({
|
|
387
298
|
type: "control.release",
|
|
388
|
-
|
|
299
|
+
sessionId: session.id,
|
|
389
300
|
payload: { deviceId },
|
|
390
301
|
});
|
|
391
302
|
broadcastToClients(session, releaseMsg);
|
|
392
303
|
sendToHost(session, releaseMsg);
|
|
393
304
|
break;
|
|
394
305
|
}
|
|
395
|
-
case "
|
|
306
|
+
case "session.heartbeat":
|
|
396
307
|
break;
|
|
397
308
|
// Screen sharing: client → host
|
|
398
309
|
case "screen.start":
|
|
399
310
|
case "screen.stop":
|
|
400
311
|
case "screen.answer":
|
|
401
312
|
case "screen.ice":
|
|
313
|
+
case "agent.session.new":
|
|
314
|
+
case "agent.session.load":
|
|
315
|
+
case "agent.prompt":
|
|
316
|
+
case "agent.cancel":
|
|
317
|
+
case "agent.permission.response":
|
|
402
318
|
case "agent.v2.conversation.open":
|
|
403
319
|
case "agent.v2.prompt":
|
|
404
320
|
case "agent.v2.command.execute":
|
|
@@ -416,55 +332,41 @@ function handleClientMessage(
|
|
|
416
332
|
case "file.upload":
|
|
417
333
|
case "permission.decision":
|
|
418
334
|
if (!requireController()) return;
|
|
419
|
-
sendToHost(session, envelope
|
|
335
|
+
sendToHost(session, envelope);
|
|
420
336
|
break;
|
|
337
|
+
case "agent.initialize":
|
|
338
|
+
case "agent.session.list":
|
|
421
339
|
case "agent.v2.capabilities.request":
|
|
422
340
|
case "agent.v2.conversation.list":
|
|
423
341
|
case "agent.v2.snapshot.request":
|
|
424
|
-
|
|
425
|
-
case "agent.v2.delta.request":
|
|
426
|
-
// Codex app-server JSON-RPC: client → host.
|
|
427
|
-
case "agent.codex.rpc":
|
|
428
|
-
if (!requireController()) return;
|
|
429
|
-
sendToHost(session, envelope, raw);
|
|
342
|
+
sendToHost(session, envelope);
|
|
430
343
|
break;
|
|
431
344
|
default:
|
|
432
|
-
sendToHost(session, envelope
|
|
345
|
+
sendToHost(session, envelope);
|
|
433
346
|
break;
|
|
434
347
|
}
|
|
435
348
|
}
|
|
436
349
|
|
|
437
|
-
function safeSend(socket: WebSocket, data: string, hostDeviceId: string): boolean {
|
|
438
|
-
if (socket.readyState !== socket.OPEN) return false;
|
|
439
|
-
const limit = wsBufferedAmountLimit();
|
|
440
|
-
const frameBytes = Buffer.byteLength(data);
|
|
441
|
-
const queuedBytes = socket.bufferedAmount + frameBytes;
|
|
442
|
-
if (queuedBytes > limit) {
|
|
443
|
-
process.stderr.write(`[gateway:warn] closing slow socket host=${hostDeviceId} buffered=${socket.bufferedAmount} frame=${frameBytes} limit=${limit}\n`);
|
|
444
|
-
socket.close(1013, "client too slow");
|
|
445
|
-
return false;
|
|
446
|
-
}
|
|
447
|
-
socket.send(data);
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
350
|
function broadcastToClients(
|
|
452
|
-
session: ReturnType<
|
|
351
|
+
session: ReturnType<SessionManager["get"]> & {},
|
|
453
352
|
envelope: Envelope,
|
|
454
|
-
raw?: string,
|
|
455
353
|
): void {
|
|
456
|
-
const data =
|
|
354
|
+
const data = serializeEnvelope(envelope);
|
|
457
355
|
for (const [, client] of session.clients) {
|
|
458
|
-
|
|
356
|
+
if (client.socket.readyState === client.socket.OPEN) {
|
|
357
|
+
client.socket.send(data);
|
|
358
|
+
}
|
|
459
359
|
}
|
|
460
360
|
}
|
|
461
361
|
|
|
462
362
|
function sendToHost(
|
|
463
|
-
session: ReturnType<
|
|
363
|
+
session: ReturnType<SessionManager["get"]> & {},
|
|
464
364
|
envelope: Envelope,
|
|
465
|
-
raw?: string,
|
|
466
365
|
): void {
|
|
467
|
-
if (
|
|
468
|
-
|
|
366
|
+
if (
|
|
367
|
+
session.host &&
|
|
368
|
+
session.host.socket.readyState === session.host.socket.OPEN
|
|
369
|
+
) {
|
|
370
|
+
session.host.socket.send(serializeEnvelope(envelope));
|
|
469
371
|
}
|
|
470
372
|
}
|