@linkshell/gateway 0.3.8 → 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 +63 -76
- 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 -196
- 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 -3451
- package/dist/shared-protocol/src/index.js +98 -172
- 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 +70 -92
- package/src/sessions.ts +150 -210
- 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,44 +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
|
-
|
|
22
19
|
export function handleSocketMessage(
|
|
23
20
|
socket: WebSocket,
|
|
24
21
|
raw: string,
|
|
25
22
|
role: "host" | "client",
|
|
26
|
-
|
|
23
|
+
sessionId: string,
|
|
27
24
|
deviceId: string,
|
|
28
|
-
sessions:
|
|
25
|
+
sessions: SessionManager,
|
|
29
26
|
): void {
|
|
30
27
|
let envelope: Envelope;
|
|
31
28
|
try {
|
|
32
29
|
envelope = parseEnvelope(raw);
|
|
33
30
|
} catch {
|
|
34
|
-
sendSessionError(socket,
|
|
31
|
+
sendSessionError(socket, sessionId, "invalid_message", "Failed to parse envelope");
|
|
35
32
|
return;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
if (envelope.
|
|
35
|
+
if (envelope.sessionId !== sessionId) {
|
|
39
36
|
sendSessionError(
|
|
40
37
|
socket,
|
|
41
|
-
|
|
38
|
+
sessionId,
|
|
42
39
|
"invalid_message",
|
|
43
|
-
"Envelope
|
|
40
|
+
"Envelope sessionId does not match connection sessionId",
|
|
44
41
|
);
|
|
45
42
|
return;
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
const session = sessions.get(
|
|
45
|
+
const session = sessions.get(sessionId);
|
|
49
46
|
if (!session) {
|
|
50
|
-
sendSessionError(socket,
|
|
47
|
+
sendSessionError(socket, sessionId, "session_not_found", "Session not found");
|
|
51
48
|
return;
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
try {
|
|
55
|
-
if (
|
|
52
|
+
if (isProtocolMessageType(envelope.type)) {
|
|
56
53
|
envelope = {
|
|
57
54
|
...envelope,
|
|
58
55
|
payload: parseTypedPayload(envelope.type, envelope.payload),
|
|
@@ -60,42 +57,31 @@ export function handleSocketMessage(
|
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
if (role === "host") {
|
|
63
|
-
handleHostMessage(envelope,
|
|
60
|
+
handleHostMessage(envelope, session, sessions);
|
|
64
61
|
} else {
|
|
65
|
-
handleClientMessage(envelope,
|
|
62
|
+
handleClientMessage(envelope, socket, session, deviceId, sessions);
|
|
66
63
|
}
|
|
67
64
|
} catch (error) {
|
|
68
65
|
if (error instanceof ZodError) {
|
|
69
66
|
sendSessionError(
|
|
70
67
|
socket,
|
|
71
|
-
|
|
68
|
+
sessionId,
|
|
72
69
|
"invalid_message",
|
|
73
|
-
error.
|
|
70
|
+
error.errors[0]?.message ?? "Invalid message payload",
|
|
74
71
|
);
|
|
75
72
|
return;
|
|
76
73
|
}
|
|
77
|
-
sendSessionError(socket,
|
|
74
|
+
sendSessionError(socket, sessionId, "invalid_message", "Failed to handle message");
|
|
78
75
|
}
|
|
79
76
|
}
|
|
80
77
|
|
|
81
|
-
function
|
|
82
|
-
return (
|
|
83
|
-
type === "device.connect" ||
|
|
84
|
-
type === "device.ack" ||
|
|
85
|
-
type === "device.resume" ||
|
|
86
|
-
type === "terminal.input" ||
|
|
87
|
-
type === "terminal.resize" ||
|
|
88
|
-
type === "agent.codex.rpc" ||
|
|
89
|
-
type === "permission.decision" ||
|
|
90
|
-
type === "permission.decision.result" ||
|
|
91
|
-
type === "control.claim" ||
|
|
92
|
-
type === "control.release"
|
|
93
|
-
);
|
|
78
|
+
function isProtocolMessageType(type: string): type is ProtocolMessageType {
|
|
79
|
+
return Object.prototype.hasOwnProperty.call(protocolMessageSchemas, type);
|
|
94
80
|
}
|
|
95
81
|
|
|
96
82
|
function sendSessionError(
|
|
97
83
|
socket: WebSocket,
|
|
98
|
-
|
|
84
|
+
sessionId: string,
|
|
99
85
|
code: string,
|
|
100
86
|
message: string,
|
|
101
87
|
): void {
|
|
@@ -103,8 +89,8 @@ function sendSessionError(
|
|
|
103
89
|
socket.send(
|
|
104
90
|
serializeEnvelope(
|
|
105
91
|
createEnvelope({
|
|
106
|
-
type: "
|
|
107
|
-
|
|
92
|
+
type: "session.error",
|
|
93
|
+
sessionId,
|
|
108
94
|
payload: { code, message },
|
|
109
95
|
}),
|
|
110
96
|
),
|
|
@@ -113,44 +99,42 @@ function sendSessionError(
|
|
|
113
99
|
|
|
114
100
|
function handleHostMessage(
|
|
115
101
|
envelope: Envelope,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
sessions: DeviceManager,
|
|
102
|
+
session: ReturnType<SessionManager["get"]> & {},
|
|
103
|
+
sessions: SessionManager,
|
|
119
104
|
): void {
|
|
120
105
|
switch (envelope.type) {
|
|
121
|
-
case "
|
|
106
|
+
case "session.connect": {
|
|
122
107
|
// Extract metadata from host's connect message
|
|
123
|
-
const p = parseTypedPayload("
|
|
124
|
-
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) {
|
|
125
110
|
sessions.setMetadata(
|
|
126
111
|
session.id,
|
|
127
|
-
undefined,
|
|
112
|
+
p.provider ?? undefined,
|
|
128
113
|
p.machineId ?? undefined,
|
|
129
114
|
p.hostname ?? undefined,
|
|
130
115
|
p.platform ?? undefined,
|
|
131
116
|
p.cwd ?? undefined,
|
|
132
|
-
undefined,
|
|
133
|
-
p.capabilities ?? undefined,
|
|
117
|
+
p.projectName ?? undefined,
|
|
134
118
|
);
|
|
135
119
|
}
|
|
136
120
|
break;
|
|
137
121
|
}
|
|
138
122
|
case "terminal.output": {
|
|
139
123
|
sessions.bufferOutput(session.id, envelope);
|
|
140
|
-
broadcastToClients(session, envelope
|
|
124
|
+
broadcastToClients(session, envelope);
|
|
141
125
|
break;
|
|
142
126
|
}
|
|
143
127
|
case "terminal.exit": {
|
|
144
128
|
// Don't terminate session — other terminals may still be running
|
|
145
|
-
broadcastToClients(session, envelope
|
|
129
|
+
broadcastToClients(session, envelope);
|
|
146
130
|
break;
|
|
147
131
|
}
|
|
148
|
-
case "
|
|
132
|
+
case "session.heartbeat":
|
|
149
133
|
break;
|
|
150
134
|
case "permission.decision.result": {
|
|
151
135
|
const p = parseTypedPayload("permission.decision.result", envelope.payload);
|
|
152
136
|
resolveAgentPermissionHttpAck({
|
|
153
|
-
|
|
137
|
+
sessionId: session.id,
|
|
154
138
|
ack: {
|
|
155
139
|
requestId: p.requestId,
|
|
156
140
|
decision: p.decision,
|
|
@@ -160,7 +144,7 @@ function handleHostMessage(
|
|
|
160
144
|
message: p.message,
|
|
161
145
|
},
|
|
162
146
|
});
|
|
163
|
-
broadcastToClients(session, envelope
|
|
147
|
+
broadcastToClients(session, envelope);
|
|
164
148
|
break;
|
|
165
149
|
}
|
|
166
150
|
// Tunnel: host → gateway (not broadcast to clients)
|
|
@@ -181,63 +165,56 @@ function handleHostMessage(
|
|
|
181
165
|
}
|
|
182
166
|
case "control.grant":
|
|
183
167
|
case "control.reject":
|
|
184
|
-
broadcastToClients(session, envelope
|
|
168
|
+
broadcastToClients(session, envelope);
|
|
185
169
|
break;
|
|
186
170
|
// Screen sharing: host → clients
|
|
187
171
|
case "screen.frame":
|
|
188
172
|
case "screen.status":
|
|
189
173
|
case "screen.offer":
|
|
190
174
|
case "screen.ice":
|
|
191
|
-
//
|
|
192
|
-
case "agent.
|
|
193
|
-
|
|
175
|
+
// Agent GUI: host → clients
|
|
176
|
+
case "agent.capabilities":
|
|
177
|
+
case "agent.update":
|
|
178
|
+
case "agent.permission.request":
|
|
179
|
+
case "agent.snapshot":
|
|
194
180
|
case "agent.v2.capabilities":
|
|
195
181
|
case "agent.v2.conversation.opened":
|
|
196
182
|
case "agent.v2.conversation.list.result":
|
|
197
183
|
case "agent.v2.event":
|
|
198
184
|
case "agent.v2.snapshot":
|
|
199
|
-
if (envelope.type === "agent.v2.snapshot" && Buffer.byteLength(raw, "utf8") > AGENT_SNAPSHOT_WARN_BYTES) {
|
|
200
|
-
process.stderr.write(`[gateway:warn] oversized agent snapshot host=${session.id} bytes=${Buffer.byteLength(raw, "utf8")}\n`);
|
|
201
|
-
}
|
|
202
|
-
broadcastToClients(session, envelope, raw);
|
|
203
|
-
break;
|
|
204
|
-
case "agent.v2.history.page":
|
|
205
|
-
case "agent.v2.delta":
|
|
206
|
-
case "agent.v2.running_state":
|
|
207
185
|
case "agent.v2.permission.request":
|
|
208
186
|
// Multi-terminal: host → clients
|
|
209
187
|
case "terminal.spawned":
|
|
210
188
|
case "terminal.list":
|
|
211
189
|
case "terminal.browse.result":
|
|
212
190
|
case "terminal.file.read.result":
|
|
213
|
-
broadcastToClients(session, envelope
|
|
191
|
+
broadcastToClients(session, envelope);
|
|
214
192
|
break;
|
|
215
193
|
// Structured status from hooks
|
|
216
194
|
case "terminal.status":
|
|
217
195
|
sessions.cacheStatus(session.id, envelope);
|
|
218
|
-
broadcastToClients(session, envelope
|
|
196
|
+
broadcastToClients(session, envelope);
|
|
219
197
|
break;
|
|
220
198
|
default:
|
|
221
|
-
broadcastToClients(session, envelope
|
|
199
|
+
broadcastToClients(session, envelope);
|
|
222
200
|
break;
|
|
223
201
|
}
|
|
224
202
|
}
|
|
225
203
|
|
|
226
204
|
function handleClientMessage(
|
|
227
205
|
envelope: Envelope,
|
|
228
|
-
raw: string,
|
|
229
206
|
socket: WebSocket,
|
|
230
|
-
session: ReturnType<
|
|
207
|
+
session: ReturnType<SessionManager["get"]> & {},
|
|
231
208
|
deviceId: string,
|
|
232
|
-
sessions:
|
|
209
|
+
sessions: SessionManager,
|
|
233
210
|
): void {
|
|
234
211
|
const requireController = (): boolean => {
|
|
235
212
|
if (session.controllerId === deviceId) return true;
|
|
236
213
|
socket.send(
|
|
237
214
|
serializeEnvelope(
|
|
238
215
|
createEnvelope({
|
|
239
|
-
type: "
|
|
240
|
-
|
|
216
|
+
type: "session.error",
|
|
217
|
+
sessionId: session.id,
|
|
241
218
|
payload: {
|
|
242
219
|
code: "control_conflict",
|
|
243
220
|
message: "Not the controller",
|
|
@@ -251,21 +228,21 @@ function handleClientMessage(
|
|
|
251
228
|
switch (envelope.type) {
|
|
252
229
|
case "terminal.input": {
|
|
253
230
|
if (!requireController()) return;
|
|
254
|
-
sendToHost(session, envelope
|
|
231
|
+
sendToHost(session, envelope);
|
|
255
232
|
break;
|
|
256
233
|
}
|
|
257
234
|
case "terminal.resize": {
|
|
258
235
|
if (!requireController()) return;
|
|
259
|
-
sendToHost(session, envelope
|
|
236
|
+
sendToHost(session, envelope);
|
|
260
237
|
break;
|
|
261
238
|
}
|
|
262
|
-
case "
|
|
239
|
+
case "session.ack": {
|
|
263
240
|
// Forward ACK to host
|
|
264
|
-
sendToHost(session, envelope
|
|
241
|
+
sendToHost(session, envelope);
|
|
265
242
|
break;
|
|
266
243
|
}
|
|
267
|
-
case "
|
|
268
|
-
const p = parseTypedPayload("
|
|
244
|
+
case "session.resume": {
|
|
245
|
+
const p = parseTypedPayload("session.resume", envelope.payload);
|
|
269
246
|
// Replay from gateway buffer first
|
|
270
247
|
const replay = sessions.getReplayFrom(
|
|
271
248
|
session.id,
|
|
@@ -278,7 +255,7 @@ function handleClientMessage(
|
|
|
278
255
|
serializeEnvelope(
|
|
279
256
|
createEnvelope({
|
|
280
257
|
type: "terminal.output",
|
|
281
|
-
|
|
258
|
+
sessionId: session.id,
|
|
282
259
|
terminalId: msg.terminalId,
|
|
283
260
|
seq: msg.seq,
|
|
284
261
|
payload: { ...payload, isReplay: true },
|
|
@@ -307,7 +284,7 @@ function handleClientMessage(
|
|
|
307
284
|
sessions.claimControl(session.id, deviceId);
|
|
308
285
|
const grantMsg = createEnvelope({
|
|
309
286
|
type: "control.grant",
|
|
310
|
-
|
|
287
|
+
sessionId: session.id,
|
|
311
288
|
payload: { deviceId },
|
|
312
289
|
});
|
|
313
290
|
// Broadcast to ALL clients so previous controller updates its state
|
|
@@ -319,20 +296,25 @@ function handleClientMessage(
|
|
|
319
296
|
sessions.releaseControl(session.id, deviceId);
|
|
320
297
|
const releaseMsg = createEnvelope({
|
|
321
298
|
type: "control.release",
|
|
322
|
-
|
|
299
|
+
sessionId: session.id,
|
|
323
300
|
payload: { deviceId },
|
|
324
301
|
});
|
|
325
302
|
broadcastToClients(session, releaseMsg);
|
|
326
303
|
sendToHost(session, releaseMsg);
|
|
327
304
|
break;
|
|
328
305
|
}
|
|
329
|
-
case "
|
|
306
|
+
case "session.heartbeat":
|
|
330
307
|
break;
|
|
331
308
|
// Screen sharing: client → host
|
|
332
309
|
case "screen.start":
|
|
333
310
|
case "screen.stop":
|
|
334
311
|
case "screen.answer":
|
|
335
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":
|
|
336
318
|
case "agent.v2.conversation.open":
|
|
337
319
|
case "agent.v2.prompt":
|
|
338
320
|
case "agent.v2.command.execute":
|
|
@@ -350,29 +332,26 @@ function handleClientMessage(
|
|
|
350
332
|
case "file.upload":
|
|
351
333
|
case "permission.decision":
|
|
352
334
|
if (!requireController()) return;
|
|
353
|
-
sendToHost(session, envelope
|
|
335
|
+
sendToHost(session, envelope);
|
|
354
336
|
break;
|
|
337
|
+
case "agent.initialize":
|
|
338
|
+
case "agent.session.list":
|
|
355
339
|
case "agent.v2.capabilities.request":
|
|
356
340
|
case "agent.v2.conversation.list":
|
|
357
341
|
case "agent.v2.snapshot.request":
|
|
358
|
-
|
|
359
|
-
case "agent.v2.delta.request":
|
|
360
|
-
// Codex app-server JSON-RPC: client → host.
|
|
361
|
-
case "agent.codex.rpc":
|
|
362
|
-
sendToHost(session, envelope, raw);
|
|
342
|
+
sendToHost(session, envelope);
|
|
363
343
|
break;
|
|
364
344
|
default:
|
|
365
|
-
sendToHost(session, envelope
|
|
345
|
+
sendToHost(session, envelope);
|
|
366
346
|
break;
|
|
367
347
|
}
|
|
368
348
|
}
|
|
369
349
|
|
|
370
350
|
function broadcastToClients(
|
|
371
|
-
session: ReturnType<
|
|
351
|
+
session: ReturnType<SessionManager["get"]> & {},
|
|
372
352
|
envelope: Envelope,
|
|
373
|
-
raw?: string,
|
|
374
353
|
): void {
|
|
375
|
-
const data =
|
|
354
|
+
const data = serializeEnvelope(envelope);
|
|
376
355
|
for (const [, client] of session.clients) {
|
|
377
356
|
if (client.socket.readyState === client.socket.OPEN) {
|
|
378
357
|
client.socket.send(data);
|
|
@@ -381,14 +360,13 @@ function broadcastToClients(
|
|
|
381
360
|
}
|
|
382
361
|
|
|
383
362
|
function sendToHost(
|
|
384
|
-
session: ReturnType<
|
|
363
|
+
session: ReturnType<SessionManager["get"]> & {},
|
|
385
364
|
envelope: Envelope,
|
|
386
|
-
raw?: string,
|
|
387
365
|
): void {
|
|
388
366
|
if (
|
|
389
367
|
session.host &&
|
|
390
368
|
session.host.socket.readyState === session.host.socket.OPEN
|
|
391
369
|
) {
|
|
392
|
-
session.host.socket.send(
|
|
370
|
+
session.host.socket.send(serializeEnvelope(envelope));
|
|
393
371
|
}
|
|
394
372
|
}
|