@livedesk/hub 0.1.20 → 0.1.22

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.20",
3
+ "version": "0.1.22",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -13,6 +13,15 @@ function publicCodexStatus(status = {}) {
13
13
  };
14
14
  }
15
15
 
16
+ function publicCodexConnection(result = {}) {
17
+ const latencyMs = Number(result.latencyMs);
18
+ return {
19
+ ok: result.ok === true,
20
+ latencyMs: Number.isFinite(latencyMs) ? Math.max(0, Math.round(latencyMs)) : 0,
21
+ threadId: String(result.threadId || '').slice(0, 200)
22
+ };
23
+ }
24
+
16
25
  export function createAgentManager({
17
26
  dataDir = path.join(os.homedir(), '.livedesk'),
18
27
  settingsStore,
@@ -56,7 +65,7 @@ export function createAgentManager({
56
65
  async testConnection() {
57
66
  if (!runtime) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
58
67
  statusCache = { expiresAt: 0, value: null };
59
- return runtime.testConnection();
68
+ return publicCodexConnection(await runtime.testConnection());
60
69
  },
61
70
  async createSummary(input) {
62
71
  const source = input && typeof input === 'object' ? input : {};
package/src/remote-hub.js CHANGED
@@ -658,7 +658,8 @@ function getRetiredAgentTaskError(options = {}) {
658
658
  const approvalLevel = safeString(options?.approvalLevel, 80).toLowerCase();
659
659
  const hasModelSelection = options
660
660
  && typeof options === 'object'
661
- && Object.prototype.hasOwnProperty.call(options, 'model');
661
+ && ['model', 'aiModel', 'aiProvider', 'provider']
662
+ .some(key => Object.prototype.hasOwnProperty.call(options, key));
662
663
  return approvalLevel === 'ai-assist' || hasModelSelection
663
664
  ? RETIRED_AGENT_TASK_ERROR
664
665
  : '';
@@ -3892,6 +3893,13 @@ export function createRemoteHub(options = {}) {
3892
3893
  agentQueueBeforePaceMs: Number.isFinite(Number(message.agentQueueBeforePaceMs)) ? Number(message.agentQueueBeforePaceMs) : 0,
3893
3894
  agentFrameAgeMs: Number.isFinite(Number(message.agentFrameAgeMs)) ? Number(message.agentFrameAgeMs) : 0,
3894
3895
  nativeCaptureTelemetryAvailable: message.nativeCaptureTelemetryAvailable === true,
3896
+ nativeCaptureTelemetryVersion: Number.isFinite(Number(message.nativeCaptureTelemetryVersion)) ? Number(message.nativeCaptureTelemetryVersion) : 0,
3897
+ nativeCaptureTelemetryAgeMs: message.nativeCaptureTelemetryAgeMs === null
3898
+ || message.nativeCaptureTelemetryAgeMs === undefined
3899
+ ? undefined
3900
+ : Number.isFinite(Number(message.nativeCaptureTelemetryAgeMs))
3901
+ ? Math.max(0, Number(message.nativeCaptureTelemetryAgeMs))
3902
+ : undefined,
3895
3903
  nativeEncoderHardwareAccelerationKnown: message.nativeEncoderHardwareAccelerationKnown === true,
3896
3904
  nativeEncoderHardwareAccelerated: typeof message.nativeEncoderHardwareAccelerated === 'boolean'
3897
3905
  ? message.nativeEncoderHardwareAccelerated
@@ -4281,6 +4289,13 @@ export function createRemoteHub(options = {}) {
4281
4289
  agentQueueBeforePaceMs: Number.isFinite(Number(message.agentQueueBeforePaceMs)) ? Number(message.agentQueueBeforePaceMs) : 0,
4282
4290
  agentFrameAgeMs: Number.isFinite(Number(message.agentFrameAgeMs)) ? Number(message.agentFrameAgeMs) : 0,
4283
4291
  nativeCaptureTelemetryAvailable: message.nativeCaptureTelemetryAvailable === true,
4292
+ nativeCaptureTelemetryVersion: Number.isFinite(Number(message.nativeCaptureTelemetryVersion)) ? Number(message.nativeCaptureTelemetryVersion) : 0,
4293
+ nativeCaptureTelemetryAgeMs: message.nativeCaptureTelemetryAgeMs === null
4294
+ || message.nativeCaptureTelemetryAgeMs === undefined
4295
+ ? undefined
4296
+ : Number.isFinite(Number(message.nativeCaptureTelemetryAgeMs))
4297
+ ? Math.max(0, Number(message.nativeCaptureTelemetryAgeMs))
4298
+ : undefined,
4284
4299
  nativeEncoderHardwareAccelerationKnown: message.nativeEncoderHardwareAccelerationKnown === true,
4285
4300
  nativeEncoderHardwareAccelerated: typeof message.nativeEncoderHardwareAccelerated === 'boolean'
4286
4301
  ? message.nativeEncoderHardwareAccelerated
@@ -5444,11 +5459,13 @@ export function createRemoteHub(options = {}) {
5444
5459
  }
5445
5460
 
5446
5461
  function sendCommand(deviceId, command) {
5447
- const device = devices.get(String(deviceId || ''));
5462
+ const device = devices.get(String(deviceId || ''));
5448
5463
  const commandName = safeString(command?.command || 'ping', 80);
5449
- const requiredPermission = commandName === 'input.control'
5450
- ? 'allowControl'
5451
- : commandName.startsWith('file.transfer')
5464
+ const requiredPermission = commandName === 'input.control'
5465
+ ? 'allowControl'
5466
+ : commandName === 'system.power'
5467
+ ? 'allowPowerActions'
5468
+ : commandName.startsWith('file.transfer')
5452
5469
  ? 'allowFileTransfer'
5453
5470
  : commandName === 'audio.start'
5454
5471
  ? 'allowRemoteAudio'
@@ -5507,8 +5524,30 @@ export function createRemoteHub(options = {}) {
5507
5524
  command: payload.command,
5508
5525
  channel: dedicatedFileSocket ? 'file' : 'control'
5509
5526
  });
5510
- return { ok: true, commandId };
5511
- }
5527
+ return { ok: true, commandId };
5528
+ }
5529
+
5530
+ function refreshDevicePolicies(deviceIds = undefined) {
5531
+ const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
5532
+ ? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
5533
+ : null;
5534
+ let updated = 0;
5535
+ let total = 0;
5536
+ for (const device of devices.values()) {
5537
+ if (requestedIds && !requestedIds.has(device.deviceId)) continue;
5538
+ if (!device?.socket || device.socket.destroyed || !device.connected) continue;
5539
+ total += 1;
5540
+ const effectivePolicy = getDevicePolicy(device);
5541
+ if (writeJsonLine(device.socket, {
5542
+ type: 'policy.update',
5543
+ effectivePolicy,
5544
+ updatedAt: new Date().toISOString()
5545
+ })) {
5546
+ updated += 1;
5547
+ }
5548
+ }
5549
+ return { ok: updated === total, updated, total };
5550
+ }
5512
5551
 
5513
5552
  // Client updates are an explicit Hub maintenance action, not an Agent task.
5514
5553
  // This legacy path is kept for clients that predate the dedicated update command.
@@ -7382,6 +7421,7 @@ export function createRemoteHub(options = {}) {
7382
7421
  disconnectDevice,
7383
7422
  assignDeviceSlot,
7384
7423
  sendCommand,
7424
+ refreshDevicePolicies,
7385
7425
  sendLegacyClientUpdate,
7386
7426
  sendInputControl,
7387
7427
  releaseInputOwner,
package/src/server.js CHANGED
@@ -1450,7 +1450,8 @@ function normalizeDeviceIds(value) {
1450
1450
  function rejectRetiredAgentTaskRequest(req, res) {
1451
1451
  const body = req.body && typeof req.body === 'object' ? req.body : {};
1452
1452
  const approvalLevel = String(body.approvalLevel || '').trim().toLowerCase();
1453
- const hasModelSelection = Object.prototype.hasOwnProperty.call(body, 'model');
1453
+ const hasModelSelection = ['model', 'aiModel', 'aiProvider', 'provider']
1454
+ .some(key => Object.prototype.hasOwnProperty.call(body, key));
1454
1455
  if (approvalLevel !== 'ai-assist' && !hasModelSelection) {
1455
1456
  return false;
1456
1457
  }
@@ -2102,6 +2103,13 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
2102
2103
  agentQueueBeforePaceMs: Number(frame.agentQueueBeforePaceMs || 0) || 0,
2103
2104
  agentFrameAgeMs: Number(frame.agentFrameAgeMs || 0) || 0,
2104
2105
  nativeCaptureTelemetryAvailable: frame.nativeCaptureTelemetryAvailable === true,
2106
+ nativeCaptureTelemetryVersion: Number(frame.nativeCaptureTelemetryVersion || 0) || 0,
2107
+ nativeCaptureTelemetryAgeMs: frame.nativeCaptureTelemetryAgeMs === null
2108
+ || frame.nativeCaptureTelemetryAgeMs === undefined
2109
+ ? undefined
2110
+ : Number.isFinite(Number(frame.nativeCaptureTelemetryAgeMs))
2111
+ ? Math.max(0, Number(frame.nativeCaptureTelemetryAgeMs))
2112
+ : undefined,
2105
2113
  nativeEncoderHardwareAccelerationKnown: frame.nativeEncoderHardwareAccelerationKnown === true,
2106
2114
  nativeEncoderHardwareAccelerated: typeof frame.nativeEncoderHardwareAccelerated === 'boolean'
2107
2115
  ? frame.nativeEncoderHardwareAccelerated
@@ -2708,6 +2716,7 @@ app.patch('/api/settings', async (req, res) => {
2708
2716
  if (patch.agent && Object.prototype.hasOwnProperty.call(patch.agent, 'enabled')) {
2709
2717
  await agentSettingsStore.update({ enabled: record.settings.agent?.enabled === true });
2710
2718
  }
2719
+ remoteHub.refreshDevicePolicies();
2711
2720
  res.json({ ok: true, revision: record.revision, updatedAt: record.updatedAt, settings: record.settings });
2712
2721
  } catch (error) {
2713
2722
  if (error instanceof SettingsConflictError) {
@@ -2965,6 +2974,9 @@ app.post('/api/internal/agent-mcp/tool', async (req, res) => {
2965
2974
 
2966
2975
  app.post('/api/settings/agent/run', async (req, res) => {
2967
2976
  noStore(res);
2977
+ if (rejectRetiredAgentTaskRequest(req, res)) {
2978
+ return;
2979
+ }
2968
2980
  try {
2969
2981
  if (!(await synchronizeAgentEnablement())) {
2970
2982
  throw new AgentRuntimeError('agent-disabled', 'Enable Codex Agent in Settings before running commands.', { status: 409 });
@@ -3999,11 +4011,12 @@ app.post('/api/remote/devices/:deviceId/quick-actions', requireHubFeatureAccess,
3999
4011
  }
4000
4012
 
4001
4013
  const settings = await liveDeskSettingsStore.get();
4002
- if (settings.agent?.enabled !== true) {
4014
+ const policy = effectiveDevicePolicy(settings);
4015
+ if (policy.accessMode !== 'trusted-only') {
4003
4016
  res.status(403).json({ ok: false, error: 'remote-actions-disabled' });
4004
4017
  return;
4005
4018
  }
4006
- if (settings.agent?.allowPowerActions !== true) {
4019
+ if (policy.allowPowerActions !== true) {
4007
4020
  res.status(403).json({ ok: false, error: 'power-actions-disabled' });
4008
4021
  return;
4009
4022
  }
@@ -4020,14 +4033,18 @@ app.post('/api/remote/devices/:deviceId/quick-actions', requireHubFeatureAccess,
4020
4033
  return;
4021
4034
  }
4022
4035
 
4023
- const label = action === 'shutdown' ? 'Shut down' : `${action.slice(0, 1).toUpperCase()}${action.slice(1)}`;
4024
- const result = remoteHub.requestAgentTask(req.params.deviceId, {
4025
- instruction: `${label} this Client. This action was explicitly requested and confirmed from the LiveDesk Hub action panel.`,
4026
- title: `${label} Client`,
4027
- operation: 'system.power',
4028
- approvalLevel: 'task-only',
4029
- toolArguments: { action, delaySec: 0 },
4030
- permissionMode: 'ask'
4036
+ // The Client enforces the same policy. Queue the current policy immediately
4037
+ // before the command so enabling this visible setting works without forcing
4038
+ // a reconnect, while TCP ordering keeps the command behind the policy update.
4039
+ remoteHub.refreshDevicePolicies([req.params.deviceId]);
4040
+ const result = remoteHub.sendCommand(req.params.deviceId, {
4041
+ command: 'system.power',
4042
+ payload: {
4043
+ action,
4044
+ delaySec: 0,
4045
+ permissionMode: 'manual-confirmed',
4046
+ requestSource: 'hub-action-dock'
4047
+ }
4031
4048
  });
4032
4049
  recordAgentAudit({
4033
4050
  event: 'manual-action-dispatched',
@@ -4036,7 +4053,7 @@ app.post('/api/remote/devices/:deviceId/quick-actions', requireHubFeatureAccess,
4036
4053
  category: 'systemPower',
4037
4054
  decision: result.ok ? 'allow' : 'deny',
4038
4055
  status: result.ok ? 'queued' : 'failed',
4039
- details: { action, taskId: result.taskId || '', error: result.error || '' }
4056
+ details: { action, commandId: result.commandId || '', error: result.error || '' }
4040
4057
  });
4041
4058
  if (!result.ok) {
4042
4059
  res.status(result.error === 'device-not-found' ? 404 : 409).json(result);
@@ -251,6 +251,7 @@ export function effectiveDevicePolicy(settings = DEFAULT_LIVEDESK_SETTINGS) {
251
251
  allowUnencryptedLanFallback: normalized.security.allowUnencryptedLanFallback,
252
252
  allowControl: allowMutation && normalized.control.allowKeyboardMouse,
253
253
  allowClipboardText: allowMutation && normalized.control.allowClipboardText,
254
+ allowPowerActions: allowMutation && normalized.control.allowRemoteRestart,
254
255
  allowFileTransfer: allowMutation && normalized.filesAudio.allowFileTransfer,
255
256
  allowRemoteAudio: accessMode !== 'block-remote-access' && normalized.filesAudio.allowRemoteAudio,
256
257
  allowAgent: allowMutation && normalized.agent.enabled