@livedesk/hub 0.1.16 → 0.1.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -94,15 +94,6 @@ export function createAgentManager({
94
94
  async deleteApiKey() {
95
95
  return publicSettings();
96
96
  },
97
- async getModels() {
98
- return [{
99
- id: '',
100
- name: 'Codex default',
101
- protocol: 'codex-sdk',
102
- available: true,
103
- capabilities: { supportsReasoningLevel: true, supportsStreaming: true, supportsTools: true }
104
- }];
105
- },
106
97
  async testConnection() {
107
98
  if (!runtime) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
108
99
  statusCache = { expiresAt: 0, value: null };
@@ -4,14 +4,11 @@ import path from 'node:path';
4
4
 
5
5
  export const AGENT_PROVIDER_CODEX = 'codex';
6
6
  export const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_CODEX;
7
- export const DEFAULT_CODEX_MODEL_ID = '';
8
7
  export const DEFAULT_AGENT_WORKSPACE = path.join(os.homedir(), '.livedesk', 'agent-workspace');
9
8
  export const DEFAULT_AGENT_SETTINGS = Object.freeze({
10
- settingsVersion: 3,
9
+ settingsVersion: 4,
11
10
  enabled: true,
12
11
  provider: DEFAULT_AGENT_PROVIDER,
13
- codexModelId: DEFAULT_CODEX_MODEL_ID,
14
- codexReasoningEffort: 'medium',
15
12
  codexSandboxMode: 'read-only',
16
13
  codexApprovalPolicy: 'never',
17
14
  codexWorkingDirectory: DEFAULT_AGENT_WORKSPACE,
@@ -25,20 +22,6 @@ export const DEFAULT_AGENT_SETTINGS = Object.freeze({
25
22
  maxConcurrentRequests: 2
26
23
  });
27
24
 
28
- const REASONING_EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
29
- const SANDBOX_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
30
- const APPROVAL_POLICIES = new Set(['never', 'on-request', 'on-failure', 'untrusted']);
31
- const WEB_SEARCH_MODES = new Set(['disabled', 'cached', 'live']);
32
-
33
- export class AgentSettingsError extends Error {
34
- constructor(code, message) {
35
- super(message);
36
- this.name = 'AgentSettingsError';
37
- this.code = code;
38
- this.status = 400;
39
- }
40
- }
41
-
42
25
  function booleanValue(value, fallback) {
43
26
  return typeof value === 'boolean' ? value : fallback;
44
27
  }
@@ -50,18 +33,7 @@ function numberValue(value, min, max, fallback, integer = false) {
50
33
  return integer ? Math.round(clamped) : clamped;
51
34
  }
52
35
 
53
- function stringValue(value, fallback, maxLength = 240) {
54
- if (typeof value !== 'string') return fallback;
55
- return value.trim().slice(0, maxLength);
56
- }
57
-
58
- function enumValue(value, allowed, fallback, code) {
59
- const normalized = stringValue(value, fallback, 80).toLowerCase();
60
- if (!allowed.has(normalized)) throw new AgentSettingsError(code, `Invalid agent setting: ${normalized}.`);
61
- return normalized;
62
- }
63
-
64
- function normalizeWorkingDirectory(value) {
36
+ function normalizeWorkingDirectory() {
65
37
  // The agent runtime owns this directory. Do not allow a browser/API caller
66
38
  // to move Codex into a user project or an arbitrary filesystem root.
67
39
  return DEFAULT_AGENT_WORKSPACE;
@@ -70,18 +42,16 @@ function normalizeWorkingDirectory(value) {
70
42
  export function normalizeAgentSettings(value = {}) {
71
43
  const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
72
44
  return {
73
- settingsVersion: 3,
45
+ settingsVersion: 4,
74
46
  enabled: booleanValue(source.enabled, DEFAULT_AGENT_SETTINGS.enabled),
75
47
  // LiveDesk has one Agent runtime. Old provider values are deliberately
76
48
  // normalized to Codex so an upgrade cannot reactivate a retired provider.
77
49
  provider: AGENT_PROVIDER_CODEX,
78
- codexModelId: stringValue(source.codexModelId, DEFAULT_AGENT_SETTINGS.codexModelId, 160),
79
- codexReasoningEffort: enumValue(source.codexReasoningEffort, REASONING_EFFORTS, DEFAULT_AGENT_SETTINGS.codexReasoningEffort, 'agent-invalid-reasoning-effort'),
80
50
  // These are safety settings, not user-controlled execution toggles. They
81
51
  // are accepted for compatibility but always normalized to the safe floor.
82
52
  codexSandboxMode: 'read-only',
83
53
  codexApprovalPolicy: 'never',
84
- codexWorkingDirectory: normalizeWorkingDirectory(source.codexWorkingDirectory),
54
+ codexWorkingDirectory: normalizeWorkingDirectory(),
85
55
  codexTaskTimeoutMs: numberValue(source.codexTaskTimeoutMs, 30000, 1800000, DEFAULT_AGENT_SETTINGS.codexTaskTimeoutMs, true),
86
56
  codexMaxTurns: numberValue(source.codexMaxTurns, 1, 40, DEFAULT_AGENT_SETTINGS.codexMaxTurns, true),
87
57
  codexMaxToolCalls: numberValue(source.codexMaxToolCalls, 1, 100, DEFAULT_AGENT_SETTINGS.codexMaxToolCalls, true),
@@ -66,15 +66,13 @@ function normalizeCodexError(error) {
66
66
  return new AgentProviderError('codex-run-failed', message, { status: 502, retryable: true });
67
67
  }
68
68
 
69
- function codexThreadOptions(settings, workspace) {
70
- return {
71
- model: settings.codexModelId || undefined,
72
- sandboxMode: 'read-only',
73
- approvalPolicy: 'never',
74
- workingDirectory: workspace,
75
- skipGitRepoCheck: true,
76
- modelReasoningEffort: settings.codexReasoningEffort,
77
- networkAccessEnabled: false,
69
+ function codexThreadOptions(workspace) {
70
+ return {
71
+ sandboxMode: 'read-only',
72
+ approvalPolicy: 'never',
73
+ workingDirectory: workspace,
74
+ skipGitRepoCheck: true,
75
+ networkAccessEnabled: false,
78
76
  webSearchMode: 'disabled',
79
77
  webSearchEnabled: false,
80
78
  additionalDirectories: []
@@ -417,10 +415,10 @@ export function createCodexAgentRuntime({
417
415
  const controller = new AbortController();
418
416
  const timer = setTimeout(() => controller.abort(), Math.min(30000, settings.codexTaskTimeoutMs));
419
417
  try {
420
- const thread = codex.startThread(codexThreadOptions(settings, workspace));
421
- const turn = await thread.run('Reply with the single word READY. Do not use tools.', { signal: controller.signal });
422
- if (!String(turn.finalResponse || '').trim()) throw new AgentProviderError('codex-empty-response', 'Codex returned an empty response.', { status: 502 });
423
- return { ok: true, modelId: settings.codexModelId || 'Codex default', modelName: settings.codexModelId || 'Codex default', latencyMs: Date.now() - startedAt, threadId: thread.id || '' };
418
+ const thread = codex.startThread(codexThreadOptions(workspace));
419
+ const turn = await thread.run('Reply with the single word READY. Do not use tools.', { signal: controller.signal });
420
+ if (!String(turn.finalResponse || '').trim()) throw new AgentProviderError('codex-empty-response', 'Codex returned an empty response.', { status: 502 });
421
+ return { ok: true, latencyMs: Date.now() - startedAt, threadId: thread.id || '' };
424
422
  } catch (error) {
425
423
  throw normalizeCodexError(error);
426
424
  } finally {
@@ -531,7 +529,7 @@ export function createCodexAgentRuntime({
531
529
  if (run.abortReason === 'timeout') throw codexAbortError('codex-run-timeout', 'The Codex task exceeded its timeout.');
532
530
  throw Object.assign(new Error('The Codex task was cancelled.'), { name: 'AbortError' });
533
531
  }
534
- const threadOptions = codexThreadOptions(settings, workspace);
532
+ const threadOptions = codexThreadOptions(workspace);
535
533
  run.status = 'running';
536
534
  run.updatedAt = new Date().toISOString();
537
535
  for (let attempt = 0; attempt < 2; attempt += 1) {
package/src/remote-hub.js CHANGED
@@ -4232,9 +4232,11 @@ export function createRemoteHub(options = {}) {
4232
4232
  const captureTimingAvailable = hasFrameCaptureTiming(message);
4233
4233
  const videoTransport = transport === 'udp-p2p'
4234
4234
  ? 'p2p-udp'
4235
- : transport === 'ws-binary'
4236
- ? 'direct-ws'
4237
- : 'direct-tcp';
4235
+ : transport === 'relay-binary'
4236
+ ? 'encrypted-relay'
4237
+ : transport === 'ws-binary'
4238
+ ? 'direct-ws'
4239
+ : 'direct-tcp';
4238
4240
  device.latestLiveFrame = {
4239
4241
  streamId,
4240
4242
  frameSeq,
@@ -4322,9 +4324,16 @@ export function createRemoteHub(options = {}) {
4322
4324
  deltaRefreshTileCount: Number.isFinite(Number(message.deltaRefreshTileCount)) ? Number(message.deltaRefreshTileCount) : 0,
4323
4325
  deltaTotalTiles: Number.isFinite(Number(message.deltaTotalTiles)) ? Number(message.deltaTotalTiles) : 0,
4324
4326
  videoTransport,
4325
- frameTransportActive: safeString(message.frameTransportActive, 20) || (videoTransport === 'p2p-udp' ? 'p2p' : 'direct'),
4327
+ frameTransportActive: safeString(message.frameTransportActive, 20)
4328
+ || (videoTransport === 'p2p-udp' ? 'p2p' : videoTransport === 'encrypted-relay' ? 'relay' : 'direct'),
4326
4329
  frameTransportProtocol: safeString(message.frameTransportProtocol, 20)
4327
- || (videoTransport === 'p2p-udp' ? 'udp' : videoTransport === 'direct-ws' ? 'ws' : 'tcp'),
4330
+ || (videoTransport === 'p2p-udp'
4331
+ ? 'udp'
4332
+ : videoTransport === 'encrypted-relay'
4333
+ ? 'relay'
4334
+ : videoTransport === 'direct-ws'
4335
+ ? 'ws'
4336
+ : 'tcp'),
4328
4337
  frameTransportState: safeString(message.frameTransportState, 20) || 'active',
4329
4338
  frameTransportP2pReady: message.frameTransportP2pReady === true,
4330
4339
  frameTransportReason: safeString(message.frameTransportReason, 240),
@@ -4518,7 +4527,11 @@ export function createRemoteHub(options = {}) {
4518
4527
  }
4519
4528
 
4520
4529
  const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
4521
- const binaryTransport = state.binaryTransport === 'ws-binary' ? 'ws-binary' : 'binary';
4530
+ const binaryTransport = socket.__liveDeskRelayControl === true
4531
+ ? 'relay-binary'
4532
+ : state.binaryTransport === 'ws-binary'
4533
+ ? 'ws-binary'
4534
+ : 'binary';
4522
4535
  if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
4523
4536
  applyThumbnailFrame(device, header, framePayload, binaryTransport);
4524
4537
  return;
package/src/server.js CHANGED
@@ -2127,9 +2127,11 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
2127
2127
  transport: frame.transport || '',
2128
2128
  videoTransport: frame.videoTransport || (frame.transport === 'udp-p2p'
2129
2129
  ? 'p2p-udp'
2130
- : frame.transport === 'ws-binary'
2131
- ? 'direct-ws'
2132
- : 'direct-tcp'),
2130
+ : frame.transport === 'relay-binary'
2131
+ ? 'encrypted-relay'
2132
+ : frame.transport === 'ws-binary'
2133
+ ? 'direct-ws'
2134
+ : 'direct-tcp'),
2133
2135
  frameTransportActive: frame.frameTransportActive || '',
2134
2136
  frameTransportProtocol: frame.frameTransportProtocol || '',
2135
2137
  frameTransportState: frame.frameTransportState || '',
@@ -2955,14 +2957,14 @@ app.delete('/api/settings/agent/api-key', async (_req, res) => {
2955
2957
  }
2956
2958
  });
2957
2959
 
2958
- app.get('/api/settings/agent/models', async (_req, res) => {
2959
- noStore(res);
2960
- try {
2961
- res.json({ ok: true, models: await agentManager.getModels() });
2962
- } catch (error) {
2963
- sendAgentError(res, error);
2964
- }
2965
- });
2960
+ app.get('/api/settings/agent/models', async (_req, res) => {
2961
+ noStore(res);
2962
+ res.status(410).json({
2963
+ ok: false,
2964
+ error: 'agent-model-catalog-retired',
2965
+ message: 'LiveDesk uses the Codex SDK defaults and does not manage models.'
2966
+ });
2967
+ });
2966
2968
 
2967
2969
  app.post('/api/settings/agent/test', async (_req, res) => {
2968
2970
  noStore(res);
@@ -2,10 +2,10 @@ import crypto from 'node:crypto';
2
2
  import { lookup } from 'node:dns/promises';
3
3
  import dgram from 'node:dgram';
4
4
  import {
5
- UDP_MAX_DATAGRAM_BYTES,
6
- UDP_P2P_PROTOCOL,
7
- UdpFrameReassembler,
8
- UdpReplayWindow,
5
+ UDP_MAX_DATAGRAM_BYTES,
6
+ UDP_P2P_PROTOCOL,
7
+ UdpFrameReassembler,
8
+ UdpReplayWindow,
9
9
  decodeRendezvousMessage,
10
10
  decodeUdpPacket,
11
11
  encodeRendezvousMessage,
@@ -52,7 +52,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
52
52
  const socket = dgram.createSocket('udp4');
53
53
  const sessions = new Map();
54
54
  let frameHandler = () => {};
55
- let eventHandler = () => {};
55
+ let eventHandler = () => {};
56
56
  let started = false;
57
57
  let boundPort = requestedPort;
58
58
  let sequence = crypto.randomInt(1, 0x7fffffff);
@@ -65,20 +65,20 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
65
65
  if (trace) logEvent('udp', `${type} ${JSON.stringify({ deviceId: event.deviceId || '', state: event.state || '', reason: event.reason || '' })}`);
66
66
  }
67
67
 
68
- function nextSequence() {
69
- sequence = (sequence + 1) >>> 0;
70
- return sequence;
71
- }
72
-
68
+ function nextSequence() {
69
+ sequence = (sequence + 1) >>> 0;
70
+ return sequence;
71
+ }
72
+
73
73
  function sendPacket(session, type, header = {}, payload = Buffer.alloc(0), address = session.clientEndpoint || session.peerEndpoint) {
74
74
  if (!address || !session) return false;
75
- let packet;
76
- try {
77
- packet = encodeUdpPacket({ type, sessionId: session.sessionId, key: session.key, sequence: nextSequence(), header, payload });
78
- } catch (error) {
79
- logWarn('udp', `UDP packet rejected device=${session.deviceId}: ${error?.message || error}`);
80
- return false;
81
- }
75
+ let packet;
76
+ try {
77
+ packet = encodeUdpPacket({ type, sessionId: session.sessionId, key: session.key, sequence: nextSequence(), header, payload });
78
+ } catch (error) {
79
+ logWarn('udp', `UDP packet rejected device=${session.deviceId}: ${error?.message || error}`);
80
+ return false;
81
+ }
82
82
  socket.send(packet, address.port, address.address);
83
83
  return true;
84
84
  }
@@ -288,9 +288,9 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
288
288
  keyBase64: key.toString('base64'),
289
289
  rendezvousToken: crypto.randomBytes(16).toString('hex'),
290
290
  sendControl,
291
- clientEndpoint: null,
292
- peerEndpoint: null,
293
- replay: new UdpReplayWindow(),
291
+ clientEndpoint: null,
292
+ peerEndpoint: null,
293
+ replay: new UdpReplayWindow(),
294
294
  reassembler: new UdpFrameReassembler({ maxPending: maxPendingFrames, timeoutMs: frameTimeoutMs }),
295
295
  ready: false,
296
296
  createdAt: Date.now(),
@@ -333,8 +333,8 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
333
333
  return {
334
334
  enabled,
335
335
  preferred: preferP2p,
336
- state: session.ready ? 'udp-ready' : 'udp-session-created',
337
- ready: session.ready,
336
+ state: session.ready ? 'udp-ready' : 'udp-session-created',
337
+ ready: session.ready,
338
338
  sessionId: session.sessionId,
339
339
  lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
340
340
  framesReceived: session.framesReceived,
@@ -2,9 +2,11 @@ import crypto from 'node:crypto';
2
2
  import { inflateRawSync } from 'node:zlib';
3
3
 
4
4
  export const UDP_P2P_PROTOCOL = 'livedesk.udp.p2p.v1';
5
- export const UDP_P2P_VERSION = 1;
6
- export const UDP_MAX_DATAGRAM_BYTES = 1200;
7
- export const UDP_PACKET_TYPES = Object.freeze({
5
+ export const UDP_P2P_VERSION = 1;
6
+ export const UDP_MAX_DATAGRAM_BYTES = 1200;
7
+ const UDP_SERIAL_MODULUS = 0x1_0000_0000;
8
+ const UDP_SERIAL_HALF_RANGE = 0x8000_0000;
9
+ export const UDP_PACKET_TYPES = Object.freeze({
8
10
  probe: 1,
9
11
  'probe-ack': 2,
10
12
  ready: 3,
@@ -83,13 +85,21 @@ function decodeFramedPayload(payload, legacyHeader = null) {
83
85
  }
84
86
  }
85
87
 
86
- function packetTypeCode(type) {
88
+ function packetTypeCode(type) {
87
89
  const code = typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()];
88
90
  if (!code || !UDP_PACKET_TYPE_NAMES.has(code)) throw new Error('invalid-udp-packet-type');
89
91
  return code;
90
- }
91
-
92
- function encodeHeader(header) {
92
+ }
93
+
94
+ function normalizeSequence(value) {
95
+ const sequence = Number(value);
96
+ if (!Number.isInteger(sequence) || sequence < 0 || sequence >= UDP_SERIAL_MODULUS) {
97
+ throw new Error('invalid-udp-packet-sequence');
98
+ }
99
+ return sequence;
100
+ }
101
+
102
+ function encodeHeader(header) {
93
103
  const bytes = Buffer.from(JSON.stringify(header && typeof header === 'object' ? header : {}), 'utf8');
94
104
  if (bytes.length > UDP_MAX_HEADER_BYTES) throw new Error('udp-packet-header-too-large');
95
105
  return bytes;
@@ -103,9 +113,9 @@ export function createUdpSessionKey() {
103
113
  return crypto.randomBytes(32);
104
114
  }
105
115
 
106
- export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {}, payload = Buffer.alloc(0) }) {
107
- const normalizedSessionId = normalizeSessionId(sessionId);
108
- const sessionBytes = Buffer.from(normalizedSessionId, 'utf8');
116
+ export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {}, payload = Buffer.alloc(0) }) {
117
+ const normalizedSessionId = normalizeSessionId(sessionId);
118
+ const sessionBytes = Buffer.from(normalizedSessionId, 'utf8');
109
119
  const headerBytes = encodeHeader(header);
110
120
  const payloadBytes = asBuffer(payload);
111
121
  if (payloadBytes.length > UDP_MAX_PAYLOAD_BYTES) throw new Error('udp-packet-payload-too-large');
@@ -117,8 +127,8 @@ export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {
117
127
  preamble[6] = sessionBytes.length;
118
128
  preamble.writeUInt16BE(headerBytes.length, 7);
119
129
  preamble.writeUInt16BE(payloadBytes.length, 9);
120
- preamble.writeUInt32BE((Number(sequence) >>> 0), 11);
121
- const nonce = crypto.randomBytes(UDP_NONCE_BYTES);
130
+ preamble.writeUInt32BE((Number(sequence) >>> 0), 11);
131
+ const nonce = crypto.randomBytes(UDP_NONCE_BYTES);
122
132
  const associatedData = Buffer.concat([preamble, nonce, sessionBytes, headerBytes]);
123
133
  const cipher = crypto.createCipheriv('aes-256-gcm', normalizeKey(key), nonce);
124
134
  cipher.setAAD(associatedData);
@@ -169,26 +179,41 @@ export function decodeUdpPacket(packet, key) {
169
179
  }
170
180
  }
171
181
 
172
- export class UdpReplayWindow {
182
+ export class UdpReplayWindow {
173
183
  constructor(size = 512) {
174
184
  this.size = Math.max(32, Math.min(4096, Math.floor(Number(size) || 512)));
175
185
  this.highest = -1;
176
186
  this.seen = new Set();
177
187
  }
178
188
 
179
- accept(sequence) {
180
- const value = Number(sequence) >>> 0;
181
- if (this.highest >= 0 && value + this.size < this.highest) return false;
182
- if (this.seen.has(value)) return false;
183
- this.seen.add(value);
184
- if (this.highest < 0 || value > this.highest) this.highest = value;
185
- const minimum = Math.max(0, this.highest - this.size);
186
- for (const item of this.seen) {
187
- if (item < minimum) this.seen.delete(item);
188
- }
189
- return true;
190
- }
191
- }
189
+ accept(sequence) {
190
+ let value;
191
+ try {
192
+ value = normalizeSequence(sequence);
193
+ } catch {
194
+ return false;
195
+ }
196
+ if (this.seen.has(value)) return false;
197
+ if (this.highest >= 0) {
198
+ const forward = (value - this.highest + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
199
+ if (forward === 0 || forward === UDP_SERIAL_HALF_RANGE) return false;
200
+ if (forward > UDP_SERIAL_HALF_RANGE) {
201
+ const age = (this.highest - value + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
202
+ if (age > this.size) return false;
203
+ } else {
204
+ this.highest = value;
205
+ }
206
+ } else {
207
+ this.highest = value;
208
+ }
209
+ this.seen.add(value);
210
+ for (const item of this.seen) {
211
+ const age = (this.highest - item + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
212
+ if (age > this.size) this.seen.delete(item);
213
+ }
214
+ return true;
215
+ }
216
+ }
192
217
 
193
218
  export class UdpFrameReassembler {
194
219
  constructor({ maxPending = 2, timeoutMs = 500, maxFrameBytes = 8 * 1024 * 1024 } = {}) {