@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.
Files changed (41) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +13 -14
  3. package/dist/gateway/src/agent-permission-http.d.ts +74 -19
  4. package/dist/gateway/src/agent-permission-http.js +56 -16
  5. package/dist/gateway/src/agent-permission-http.js.map +1 -1
  6. package/dist/gateway/src/embedded.js +61 -153
  7. package/dist/gateway/src/embedded.js.map +1 -1
  8. package/dist/gateway/src/index.js +98 -193
  9. package/dist/gateway/src/index.js.map +1 -1
  10. package/dist/gateway/src/pairings.d.ts +3 -3
  11. package/dist/gateway/src/pairings.js +5 -4
  12. package/dist/gateway/src/pairings.js.map +1 -1
  13. package/dist/gateway/src/relay.d.ts +2 -2
  14. package/dist/gateway/src/relay.js +63 -76
  15. package/dist/gateway/src/relay.js.map +1 -1
  16. package/dist/gateway/src/sessions.d.ts +28 -42
  17. package/dist/gateway/src/sessions.js +145 -196
  18. package/dist/gateway/src/sessions.js.map +1 -1
  19. package/dist/gateway/src/state-store.d.ts +6 -9
  20. package/dist/gateway/src/state-store.js +19 -26
  21. package/dist/gateway/src/state-store.js.map +1 -1
  22. package/dist/gateway/src/tokens.d.ts +7 -27
  23. package/dist/gateway/src/tokens.js +60 -86
  24. package/dist/gateway/src/tokens.js.map +1 -1
  25. package/dist/gateway/src/tunnel.d.ts +13 -11
  26. package/dist/gateway/src/tunnel.js +36 -36
  27. package/dist/gateway/src/tunnel.js.map +1 -1
  28. package/dist/gateway/tsconfig.tsbuildinfo +1 -1
  29. package/dist/shared-protocol/src/index.d.ts +11940 -3451
  30. package/dist/shared-protocol/src/index.js +98 -172
  31. package/dist/shared-protocol/src/index.js.map +1 -1
  32. package/package.json +11 -11
  33. package/src/agent-permission-http.ts +63 -20
  34. package/src/embedded.ts +60 -158
  35. package/src/index.ts +98 -199
  36. package/src/pairings.ts +7 -6
  37. package/src/relay.ts +70 -92
  38. package/src/sessions.ts +150 -210
  39. package/src/state-store.ts +25 -41
  40. package/src/tokens.ts +63 -109
  41. 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 { DeviceManager, ConnectedDevice } from "./sessions.js";
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
- hostDeviceId: string,
23
+ sessionId: string,
27
24
  deviceId: string,
28
- sessions: DeviceManager,
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, hostDeviceId, "invalid_message", "Failed to parse envelope");
31
+ sendSessionError(socket, sessionId, "invalid_message", "Failed to parse envelope");
35
32
  return;
36
33
  }
37
34
 
38
- if (envelope.hostDeviceId !== hostDeviceId) {
35
+ if (envelope.sessionId !== sessionId) {
39
36
  sendSessionError(
40
37
  socket,
41
- hostDeviceId,
38
+ sessionId,
42
39
  "invalid_message",
43
- "Envelope hostDeviceId does not match connection hostDeviceId",
40
+ "Envelope sessionId does not match connection sessionId",
44
41
  );
45
42
  return;
46
43
  }
47
44
 
48
- const session = sessions.get(hostDeviceId);
45
+ const session = sessions.get(sessionId);
49
46
  if (!session) {
50
- sendSessionError(socket, hostDeviceId, "device_not_found", "Device not found");
47
+ sendSessionError(socket, sessionId, "session_not_found", "Session not found");
51
48
  return;
52
49
  }
53
50
 
54
51
  try {
55
- if (shouldValidatePayload(envelope.type)) {
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, raw, session, sessions);
60
+ handleHostMessage(envelope, session, sessions);
64
61
  } else {
65
- handleClientMessage(envelope, raw, socket, session, deviceId, sessions);
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
- hostDeviceId,
68
+ sessionId,
72
69
  "invalid_message",
73
- error.issues[0]?.message ?? "Invalid message payload",
70
+ error.errors[0]?.message ?? "Invalid message payload",
74
71
  );
75
72
  return;
76
73
  }
77
- sendSessionError(socket, hostDeviceId, "invalid_message", "Failed to handle message");
74
+ sendSessionError(socket, sessionId, "invalid_message", "Failed to handle message");
78
75
  }
79
76
  }
80
77
 
81
- function shouldValidatePayload(type: string): type is ProtocolMessageType {
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
- hostDeviceId: string,
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: "device.error",
107
- hostDeviceId,
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
- raw: string,
117
- session: ReturnType<DeviceManager["get"]> & {},
118
- sessions: DeviceManager,
102
+ session: ReturnType<SessionManager["get"]> & {},
103
+ sessions: SessionManager,
119
104
  ): void {
120
105
  switch (envelope.type) {
121
- case "device.connect": {
106
+ case "session.connect": {
122
107
  // Extract metadata from host's connect message
123
- const p = parseTypedPayload("device.connect", envelope.payload);
124
- if (p.machineId || p.hostname || p.platform || p.cwd || p.capabilities) {
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, raw);
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, raw);
129
+ broadcastToClients(session, envelope);
146
130
  break;
147
131
  }
148
- case "device.heartbeat":
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
- hostDeviceId: session.id,
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, raw);
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, raw);
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
- // Codex app-server JSON-RPC: host → clients.
192
- case "agent.codex.rpc":
193
- // Agent Workspace: host → clients
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, raw);
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, raw);
196
+ broadcastToClients(session, envelope);
219
197
  break;
220
198
  default:
221
- broadcastToClients(session, envelope, raw);
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<DeviceManager["get"]> & {},
207
+ session: ReturnType<SessionManager["get"]> & {},
231
208
  deviceId: string,
232
- sessions: DeviceManager,
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: "device.error",
240
- hostDeviceId: session.id,
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, raw);
231
+ sendToHost(session, envelope);
255
232
  break;
256
233
  }
257
234
  case "terminal.resize": {
258
235
  if (!requireController()) return;
259
- sendToHost(session, envelope, raw);
236
+ sendToHost(session, envelope);
260
237
  break;
261
238
  }
262
- case "device.ack": {
239
+ case "session.ack": {
263
240
  // Forward ACK to host
264
- sendToHost(session, envelope, raw);
241
+ sendToHost(session, envelope);
265
242
  break;
266
243
  }
267
- case "device.resume": {
268
- const p = parseTypedPayload("device.resume", envelope.payload);
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
- hostDeviceId: session.id,
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
- hostDeviceId: session.id,
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
- hostDeviceId: session.id,
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 "device.heartbeat":
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, raw);
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
- case "agent.v2.history.request":
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, raw);
345
+ sendToHost(session, envelope);
366
346
  break;
367
347
  }
368
348
  }
369
349
 
370
350
  function broadcastToClients(
371
- session: ReturnType<DeviceManager["get"]> & {},
351
+ session: ReturnType<SessionManager["get"]> & {},
372
352
  envelope: Envelope,
373
- raw?: string,
374
353
  ): void {
375
- const data = raw ?? serializeEnvelope(envelope);
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<DeviceManager["get"]> & {},
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(raw ?? serializeEnvelope(envelope));
370
+ session.host.socket.send(serializeEnvelope(envelope));
393
371
  }
394
372
  }