@livedesk/hub 0.1.21 → 0.1.23

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.21",
3
+ "version": "0.1.23",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
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
  : '';
@@ -5458,11 +5459,13 @@ export function createRemoteHub(options = {}) {
5458
5459
  }
5459
5460
 
5460
5461
  function sendCommand(deviceId, command) {
5461
- const device = devices.get(String(deviceId || ''));
5462
+ const device = devices.get(String(deviceId || ''));
5462
5463
  const commandName = safeString(command?.command || 'ping', 80);
5463
- const requiredPermission = commandName === 'input.control'
5464
- ? 'allowControl'
5465
- : commandName.startsWith('file.transfer')
5464
+ const requiredPermission = commandName === 'input.control'
5465
+ ? 'allowControl'
5466
+ : commandName === 'system.power'
5467
+ ? 'allowPowerActions'
5468
+ : commandName.startsWith('file.transfer')
5466
5469
  ? 'allowFileTransfer'
5467
5470
  : commandName === 'audio.start'
5468
5471
  ? 'allowRemoteAudio'
@@ -5521,8 +5524,30 @@ export function createRemoteHub(options = {}) {
5521
5524
  command: payload.command,
5522
5525
  channel: dedicatedFileSocket ? 'file' : 'control'
5523
5526
  });
5524
- return { ok: true, commandId };
5525
- }
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
+ }
5526
5551
 
5527
5552
  // Client updates are an explicit Hub maintenance action, not an Agent task.
5528
5553
  // This legacy path is kept for clients that predate the dedicated update command.
@@ -7396,6 +7421,7 @@ export function createRemoteHub(options = {}) {
7396
7421
  disconnectDevice,
7397
7422
  assignDeviceSlot,
7398
7423
  sendCommand,
7424
+ refreshDevicePolicies,
7399
7425
  sendLegacyClientUpdate,
7400
7426
  sendInputControl,
7401
7427
  releaseInputOwner,
package/src/server.js CHANGED
@@ -1229,11 +1229,12 @@ hubTransferJobs = createHubTransferJobs({
1229
1229
  maxConcurrent: Number(process.env.LIVEDESK_MAX_FILE_JOBS || 2),
1230
1230
  commandResultTimeoutMs: readPositiveIntegerEnv('LIVEDESK_FILE_CHUNK_ACK_TIMEOUT_MS', 30_000)
1231
1231
  });
1232
- const hubSharedFolders = createHubSharedFolders({
1233
- filesystem: hubFilesystem,
1234
- transferJobs: hubTransferJobs,
1235
- remoteHub
1236
- });
1232
+ const hubSharedFolders = createHubSharedFolders({
1233
+ filesystem: hubFilesystem,
1234
+ transferJobs: hubTransferJobs,
1235
+ remoteHub,
1236
+ dataDir: agentDataDir
1237
+ });
1237
1238
 
1238
1239
  const app = express();
1239
1240
  const httpServer = createServer(app);
@@ -1450,7 +1451,8 @@ function normalizeDeviceIds(value) {
1450
1451
  function rejectRetiredAgentTaskRequest(req, res) {
1451
1452
  const body = req.body && typeof req.body === 'object' ? req.body : {};
1452
1453
  const approvalLevel = String(body.approvalLevel || '').trim().toLowerCase();
1453
- const hasModelSelection = Object.prototype.hasOwnProperty.call(body, 'model');
1454
+ const hasModelSelection = ['model', 'aiModel', 'aiProvider', 'provider']
1455
+ .some(key => Object.prototype.hasOwnProperty.call(body, key));
1454
1456
  if (approvalLevel !== 'ai-assist' && !hasModelSelection) {
1455
1457
  return false;
1456
1458
  }
@@ -2715,6 +2717,7 @@ app.patch('/api/settings', async (req, res) => {
2715
2717
  if (patch.agent && Object.prototype.hasOwnProperty.call(patch.agent, 'enabled')) {
2716
2718
  await agentSettingsStore.update({ enabled: record.settings.agent?.enabled === true });
2717
2719
  }
2720
+ remoteHub.refreshDevicePolicies();
2718
2721
  res.json({ ok: true, revision: record.revision, updatedAt: record.updatedAt, settings: record.settings });
2719
2722
  } catch (error) {
2720
2723
  if (error instanceof SettingsConflictError) {
@@ -2972,6 +2975,9 @@ app.post('/api/internal/agent-mcp/tool', async (req, res) => {
2972
2975
 
2973
2976
  app.post('/api/settings/agent/run', async (req, res) => {
2974
2977
  noStore(res);
2978
+ if (rejectRetiredAgentTaskRequest(req, res)) {
2979
+ return;
2980
+ }
2975
2981
  try {
2976
2982
  if (!(await synchronizeAgentEnablement())) {
2977
2983
  throw new AgentRuntimeError('agent-disabled', 'Enable Codex Agent in Settings before running commands.', { status: 409 });
@@ -4006,11 +4012,12 @@ app.post('/api/remote/devices/:deviceId/quick-actions', requireHubFeatureAccess,
4006
4012
  }
4007
4013
 
4008
4014
  const settings = await liveDeskSettingsStore.get();
4009
- if (settings.agent?.enabled !== true) {
4015
+ const policy = effectiveDevicePolicy(settings);
4016
+ if (policy.accessMode !== 'trusted-only') {
4010
4017
  res.status(403).json({ ok: false, error: 'remote-actions-disabled' });
4011
4018
  return;
4012
4019
  }
4013
- if (settings.agent?.allowPowerActions !== true) {
4020
+ if (policy.allowPowerActions !== true) {
4014
4021
  res.status(403).json({ ok: false, error: 'power-actions-disabled' });
4015
4022
  return;
4016
4023
  }
@@ -4027,14 +4034,18 @@ app.post('/api/remote/devices/:deviceId/quick-actions', requireHubFeatureAccess,
4027
4034
  return;
4028
4035
  }
4029
4036
 
4030
- const label = action === 'shutdown' ? 'Shut down' : `${action.slice(0, 1).toUpperCase()}${action.slice(1)}`;
4031
- const result = remoteHub.requestAgentTask(req.params.deviceId, {
4032
- instruction: `${label} this Client. This action was explicitly requested and confirmed from the LiveDesk Hub action panel.`,
4033
- title: `${label} Client`,
4034
- operation: 'system.power',
4035
- approvalLevel: 'task-only',
4036
- toolArguments: { action, delaySec: 0 },
4037
- permissionMode: 'ask'
4037
+ // The Client enforces the same policy. Queue the current policy immediately
4038
+ // before the command so enabling this visible setting works without forcing
4039
+ // a reconnect, while TCP ordering keeps the command behind the policy update.
4040
+ remoteHub.refreshDevicePolicies([req.params.deviceId]);
4041
+ const result = remoteHub.sendCommand(req.params.deviceId, {
4042
+ command: 'system.power',
4043
+ payload: {
4044
+ action,
4045
+ delaySec: 0,
4046
+ permissionMode: 'manual-confirmed',
4047
+ requestSource: 'hub-action-dock'
4048
+ }
4038
4049
  });
4039
4050
  recordAgentAudit({
4040
4051
  event: 'manual-action-dispatched',
@@ -4043,7 +4054,7 @@ app.post('/api/remote/devices/:deviceId/quick-actions', requireHubFeatureAccess,
4043
4054
  category: 'systemPower',
4044
4055
  decision: result.ok ? 'allow' : 'deny',
4045
4056
  status: result.ok ? 'queued' : 'failed',
4046
- details: { action, taskId: result.taskId || '', error: result.error || '' }
4057
+ details: { action, commandId: result.commandId || '', error: result.error || '' }
4047
4058
  });
4048
4059
  if (!result.ok) {
4049
4060
  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