@linkshell/gateway 0.3.9 → 0.4.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/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 +85 -161
- 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 +11978 -3423
- package/dist/shared-protocol/src/index.js +114 -163
- 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 +97 -193
- 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,85 @@ 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":
|
|
186
|
+
case "agent.v2.notice":
|
|
283
187
|
// Multi-terminal: host → clients
|
|
284
188
|
case "terminal.spawned":
|
|
285
189
|
case "terminal.list":
|
|
286
190
|
case "terminal.browse.result":
|
|
287
191
|
case "terminal.file.read.result":
|
|
288
|
-
broadcastToClients(session, envelope
|
|
192
|
+
broadcastToClients(session, envelope);
|
|
289
193
|
break;
|
|
290
194
|
// Structured status from hooks
|
|
291
195
|
case "terminal.status":
|
|
292
196
|
sessions.cacheStatus(session.id, envelope);
|
|
293
|
-
broadcastToClients(session, envelope
|
|
197
|
+
broadcastToClients(session, envelope);
|
|
294
198
|
break;
|
|
295
199
|
default:
|
|
296
|
-
broadcastToClients(session, envelope
|
|
200
|
+
broadcastToClients(session, envelope);
|
|
297
201
|
break;
|
|
298
202
|
}
|
|
299
203
|
}
|
|
300
204
|
|
|
301
205
|
function handleClientMessage(
|
|
302
206
|
envelope: Envelope,
|
|
303
|
-
raw: string,
|
|
304
207
|
socket: WebSocket,
|
|
305
|
-
session: ReturnType<
|
|
208
|
+
session: ReturnType<SessionManager["get"]> & {},
|
|
306
209
|
deviceId: string,
|
|
307
|
-
sessions:
|
|
210
|
+
sessions: SessionManager,
|
|
308
211
|
): void {
|
|
309
212
|
const requireController = (): boolean => {
|
|
310
|
-
if (
|
|
311
|
-
|
|
213
|
+
if (session.controllerId === deviceId) return true;
|
|
214
|
+
socket.send(
|
|
215
|
+
serializeEnvelope(
|
|
216
|
+
createEnvelope({
|
|
217
|
+
type: "session.error",
|
|
218
|
+
sessionId: session.id,
|
|
219
|
+
payload: {
|
|
220
|
+
code: "control_conflict",
|
|
221
|
+
message: "Not the controller",
|
|
222
|
+
},
|
|
223
|
+
}),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
312
226
|
return false;
|
|
313
227
|
};
|
|
314
228
|
|
|
315
229
|
switch (envelope.type) {
|
|
316
230
|
case "terminal.input": {
|
|
317
231
|
if (!requireController()) return;
|
|
318
|
-
sendToHost(session, envelope
|
|
232
|
+
sendToHost(session, envelope);
|
|
319
233
|
break;
|
|
320
234
|
}
|
|
321
235
|
case "terminal.resize": {
|
|
322
236
|
if (!requireController()) return;
|
|
323
|
-
sendToHost(session, envelope
|
|
237
|
+
sendToHost(session, envelope);
|
|
324
238
|
break;
|
|
325
239
|
}
|
|
326
|
-
case "
|
|
240
|
+
case "session.ack": {
|
|
327
241
|
// Forward ACK to host
|
|
328
|
-
sendToHost(session, envelope
|
|
242
|
+
sendToHost(session, envelope);
|
|
329
243
|
break;
|
|
330
244
|
}
|
|
331
|
-
case "
|
|
332
|
-
const p = parseTypedPayload("
|
|
245
|
+
case "session.resume": {
|
|
246
|
+
const p = parseTypedPayload("session.resume", envelope.payload);
|
|
333
247
|
// Replay from gateway buffer first
|
|
334
248
|
const replay = sessions.getReplayFrom(
|
|
335
249
|
session.id,
|
|
@@ -338,24 +252,22 @@ function handleClientMessage(
|
|
|
338
252
|
);
|
|
339
253
|
for (const msg of replay) {
|
|
340
254
|
const payload = msg.payload as Record<string, unknown>;
|
|
341
|
-
|
|
342
|
-
socket,
|
|
255
|
+
socket.send(
|
|
343
256
|
serializeEnvelope(
|
|
344
257
|
createEnvelope({
|
|
345
258
|
type: "terminal.output",
|
|
346
|
-
|
|
259
|
+
sessionId: session.id,
|
|
347
260
|
terminalId: msg.terminalId,
|
|
348
261
|
seq: msg.seq,
|
|
349
262
|
payload: { ...payload, isReplay: true },
|
|
350
263
|
}),
|
|
351
264
|
),
|
|
352
|
-
session.id,
|
|
353
265
|
);
|
|
354
266
|
}
|
|
355
267
|
// Replay last terminal.status for each terminal
|
|
356
268
|
const statusReplay = sessions.getStatusReplay(session.id);
|
|
357
269
|
for (const statusMsg of statusReplay) {
|
|
358
|
-
|
|
270
|
+
socket.send(serializeEnvelope(statusMsg));
|
|
359
271
|
}
|
|
360
272
|
// Also forward resume to host so it can fill gaps beyond gateway buffer.
|
|
361
273
|
sendToHost(session, session.machineId
|
|
@@ -373,7 +285,7 @@ function handleClientMessage(
|
|
|
373
285
|
sessions.claimControl(session.id, deviceId);
|
|
374
286
|
const grantMsg = createEnvelope({
|
|
375
287
|
type: "control.grant",
|
|
376
|
-
|
|
288
|
+
sessionId: session.id,
|
|
377
289
|
payload: { deviceId },
|
|
378
290
|
});
|
|
379
291
|
// Broadcast to ALL clients so previous controller updates its state
|
|
@@ -385,86 +297,78 @@ function handleClientMessage(
|
|
|
385
297
|
sessions.releaseControl(session.id, deviceId);
|
|
386
298
|
const releaseMsg = createEnvelope({
|
|
387
299
|
type: "control.release",
|
|
388
|
-
|
|
300
|
+
sessionId: session.id,
|
|
389
301
|
payload: { deviceId },
|
|
390
302
|
});
|
|
391
303
|
broadcastToClients(session, releaseMsg);
|
|
392
304
|
sendToHost(session, releaseMsg);
|
|
393
305
|
break;
|
|
394
306
|
}
|
|
395
|
-
case "
|
|
307
|
+
case "session.heartbeat":
|
|
396
308
|
break;
|
|
397
309
|
// Screen sharing: client → host
|
|
398
310
|
case "screen.start":
|
|
399
311
|
case "screen.stop":
|
|
400
312
|
case "screen.answer":
|
|
401
313
|
case "screen.ice":
|
|
314
|
+
case "agent.session.new":
|
|
315
|
+
case "agent.session.load":
|
|
316
|
+
case "agent.prompt":
|
|
317
|
+
case "agent.cancel":
|
|
318
|
+
case "agent.permission.response":
|
|
402
319
|
case "agent.v2.conversation.open":
|
|
403
320
|
case "agent.v2.prompt":
|
|
404
321
|
case "agent.v2.command.execute":
|
|
405
322
|
case "agent.v2.cancel":
|
|
406
323
|
case "agent.v2.permission.respond":
|
|
407
324
|
case "agent.v2.structured_input.respond":
|
|
408
|
-
// Multi-terminal: client → host
|
|
325
|
+
// Multi-terminal write ops: client → host (require controller)
|
|
409
326
|
case "terminal.spawn":
|
|
410
327
|
case "terminal.kill":
|
|
411
|
-
case "terminal.list":
|
|
412
|
-
case "terminal.browse":
|
|
413
|
-
case "terminal.file.read":
|
|
414
328
|
case "terminal.mkdir":
|
|
415
|
-
case "terminal.history.request":
|
|
416
329
|
case "file.upload":
|
|
417
330
|
case "permission.decision":
|
|
418
331
|
if (!requireController()) return;
|
|
419
|
-
sendToHost(session, envelope
|
|
332
|
+
sendToHost(session, envelope);
|
|
420
333
|
break;
|
|
334
|
+
// Read-only ops: any client may issue (no controller gate)
|
|
335
|
+
case "terminal.list":
|
|
336
|
+
case "terminal.browse":
|
|
337
|
+
case "terminal.file.read":
|
|
338
|
+
case "terminal.history.request":
|
|
339
|
+
case "agent.initialize":
|
|
340
|
+
case "agent.session.list":
|
|
421
341
|
case "agent.v2.capabilities.request":
|
|
422
342
|
case "agent.v2.conversation.list":
|
|
423
343
|
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);
|
|
344
|
+
sendToHost(session, envelope);
|
|
430
345
|
break;
|
|
431
346
|
default:
|
|
432
|
-
sendToHost(session, envelope
|
|
347
|
+
sendToHost(session, envelope);
|
|
433
348
|
break;
|
|
434
349
|
}
|
|
435
350
|
}
|
|
436
351
|
|
|
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
352
|
function broadcastToClients(
|
|
452
|
-
session: ReturnType<
|
|
353
|
+
session: ReturnType<SessionManager["get"]> & {},
|
|
453
354
|
envelope: Envelope,
|
|
454
|
-
raw?: string,
|
|
455
355
|
): void {
|
|
456
|
-
const data =
|
|
356
|
+
const data = serializeEnvelope(envelope);
|
|
457
357
|
for (const [, client] of session.clients) {
|
|
458
|
-
|
|
358
|
+
if (client.socket.readyState === client.socket.OPEN) {
|
|
359
|
+
client.socket.send(data);
|
|
360
|
+
}
|
|
459
361
|
}
|
|
460
362
|
}
|
|
461
363
|
|
|
462
364
|
function sendToHost(
|
|
463
|
-
session: ReturnType<
|
|
365
|
+
session: ReturnType<SessionManager["get"]> & {},
|
|
464
366
|
envelope: Envelope,
|
|
465
|
-
raw?: string,
|
|
466
367
|
): void {
|
|
467
|
-
if (
|
|
468
|
-
|
|
368
|
+
if (
|
|
369
|
+
session.host &&
|
|
370
|
+
session.host.socket.readyState === session.host.socket.OPEN
|
|
371
|
+
) {
|
|
372
|
+
session.host.socket.send(serializeEnvelope(envelope));
|
|
469
373
|
}
|
|
470
374
|
}
|