@agent-native/core 0.157.9 → 0.157.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 (107) hide show
  1. package/corpus/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +5 -2
  2. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +3 -1
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +18 -6
  4. package/corpus/templates/analytics/server/plugins/db.ts +139 -0
  5. package/corpus/templates/slides/AGENTS.md +14 -10
  6. package/corpus/templates/slides/actions/export-pptx.ts +1094 -94
  7. package/corpus/templates/slides/actions/import-file.ts +16 -1
  8. package/corpus/templates/slides/actions/import-pptx.ts +61 -43
  9. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +222 -29
  10. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +71 -0
  11. package/corpus/templates/slides/app/global.css +19 -1
  12. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +44 -5
  13. package/corpus/templates/slides/app/lib/export-pdf-client.ts +13 -6
  14. package/corpus/templates/slides/app/lib/export-pptx-client.ts +556 -25
  15. package/corpus/templates/slides/app/pages/DeckEditor.tsx +18 -1
  16. package/corpus/templates/slides/server/handlers/import/html-converter.ts +1176 -26
  17. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +13 -0
  18. package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +24 -0
  19. package/corpus/templates/slides/shared/aspect-ratios.ts +13 -1
  20. package/dist/agent/harness/migrations.js +24 -3
  21. package/dist/agent/production-agent.js +11 -0
  22. package/dist/agent/run-loop-with-resume.js +21 -4
  23. package/dist/automations/service.d.ts +6 -0
  24. package/dist/automations/service.js +35 -0
  25. package/dist/cli/code-agent-connector.d.ts +4 -1
  26. package/dist/cli/code-agent-connector.js +154 -23
  27. package/dist/cli/code-agent-executor.js +255 -13
  28. package/dist/cli/code-agent-runs.d.ts +1 -0
  29. package/dist/cli/code-agent-runs.js +2 -1
  30. package/dist/cli/portal-workspace.d.ts +49 -0
  31. package/dist/cli/portal-workspace.js +320 -0
  32. package/dist/client/AssistantChat.d.ts +1 -1
  33. package/dist/client/AssistantChat.js +39 -61
  34. package/dist/client/ConnectBuilderCard.js +33 -5
  35. package/dist/client/agent-chat-adapter.js +4 -0
  36. package/dist/client/analytics-platform.d.ts +2 -0
  37. package/dist/client/analytics-platform.js +12 -0
  38. package/dist/client/analytics.d.ts +3 -0
  39. package/dist/client/analytics.js +10 -1
  40. package/dist/client/chat/index.d.ts +1 -0
  41. package/dist/client/chat/index.js +1 -0
  42. package/dist/client/chat/run-recovery.d.ts +2 -1
  43. package/dist/client/chat/run-recovery.js +2 -2
  44. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  45. package/dist/client/chat-first/apps-rail.js +11 -11
  46. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  47. package/dist/client/chat-first/surface-tabs.js +38 -36
  48. package/dist/client/chat-first/types.d.ts +5 -0
  49. package/dist/client/chat-first.d.ts +1 -1
  50. package/dist/client/chat-first.js +4 -4
  51. package/dist/client/desktop-local-code-change.d.ts +10 -0
  52. package/dist/client/desktop-local-code-change.js +16 -0
  53. package/dist/client/index.d.ts +1 -0
  54. package/dist/client/index.js +1 -0
  55. package/dist/client/observability/ThumbsFeedback.js +21 -7
  56. package/dist/client/terminal/AgentTerminal.d.ts +9 -1
  57. package/dist/client/terminal/AgentTerminal.js +38 -3
  58. package/dist/client/terminal/index.d.ts +1 -1
  59. package/dist/client/terminal/index.js +1 -1
  60. package/dist/client/track.js +9 -1
  61. package/dist/client/use-action.js +3 -0
  62. package/dist/collab/struct-routes.d.ts +1 -1
  63. package/dist/deploy/build.js +1 -1
  64. package/dist/ingestion/pptx.d.ts +78 -0
  65. package/dist/ingestion/pptx.js +991 -107
  66. package/dist/integrations/index.d.ts +3 -3
  67. package/dist/integrations/index.js +2 -2
  68. package/dist/integrations/plugin.js +95 -6
  69. package/dist/integrations/remote-commands-store.d.ts +5 -0
  70. package/dist/integrations/remote-commands-store.js +63 -22
  71. package/dist/integrations/remote-devices-store.d.ts +2 -1
  72. package/dist/integrations/remote-devices-store.js +46 -0
  73. package/dist/integrations/remote-types.d.ts +20 -0
  74. package/dist/jobs/actions/list-recurring-jobs.d.ts +3 -0
  75. package/dist/jobs/actions/list-recurring-jobs.js +5 -1
  76. package/dist/jobs/frontmatter.d.ts +16 -0
  77. package/dist/jobs/frontmatter.js +53 -0
  78. package/dist/jobs/remote-execution.d.ts +42 -0
  79. package/dist/jobs/remote-execution.js +287 -0
  80. package/dist/jobs/scheduler.js +102 -3
  81. package/dist/jobs/tools.js +50 -3
  82. package/dist/notifications/routes.d.ts +4 -4
  83. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  84. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  85. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  86. package/dist/resources/handlers.d.ts +1 -1
  87. package/dist/server/action-routes.js +4 -2
  88. package/dist/server/agent-chat-plugin.js +6 -0
  89. package/dist/server/agent-run-context.d.ts +1 -0
  90. package/dist/server/agent-run-context.js +9 -0
  91. package/dist/server/core-routes-plugin.js +8 -1
  92. package/dist/server/request-context.d.ts +2 -0
  93. package/dist/shared/analytics-platform.d.ts +6 -0
  94. package/dist/shared/analytics-platform.js +14 -0
  95. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  96. package/dist/shared/mcp-embed-headers.js +1 -1
  97. package/dist/styles/agent-native.css +83 -103
  98. package/dist/terminal/cli-registry.js +5 -0
  99. package/dist/tracking/registry.js +9 -1
  100. package/dist/triggers/actions/list-automations.d.ts +3 -0
  101. package/dist/triggers/actions/list-automations.js +3 -0
  102. package/dist/triggers/actions/manage-automation.d.ts +9 -0
  103. package/dist/triggers/actions/manage-automation.js +14 -2
  104. package/dist/triggers/actions.js +82 -7
  105. package/docs/content/template-content-developers.mdx +48 -0
  106. package/docs/content/toolkit-ui.mdx +57 -0
  107. package/package.json +2 -2
@@ -6,15 +6,15 @@ export { resolveDefaultIntegrationExecutionContext } from "./identity.js";
6
6
  export { upsertVerifiedIntegrationIdentity, type IntegrationIdentityLink, } from "./identity-links-store.js";
7
7
  export { BUILT_IN_INTEGRATION_CATALOG, INTEGRATION_CATEGORIES, getIntegrationCatalogEntry, listBuiltInChannelIntegrations, listIntegrationCatalog, type BuiltInChannelId, type AutomationCapabilities, type ChannelCapabilities, type IntegrationAvailability, type IntegrationCatalogEntry, type IntegrationCategory, type IntegrationCredentialRequirement, type IntegrationIconKey, type IntegrationSupportMaturity, } from "./catalog.js";
8
8
  export { BUILT_IN_INTEGRATION_ADAPTER_FACTORIES, BUILT_IN_INTEGRATION_ADAPTER_IDS, createBuiltInIntegrationAdapters, createIntegrationsPlugin, defaultIntegrationsPlugin, enqueueRemoteCommand, } from "./plugin.js";
9
- export { createRemoteDevice, getRemoteComputerCapabilities, listRemoteDevicesForOwner, revokeRemoteDeviceForOwner, unregisterRemoteDevice, } from "./remote-devices-store.js";
10
- export { claimNextComputerCommand, enqueueComputerCommand, getRemoteCommand, listRemoteCommandsForOwner, } from "./remote-commands-store.js";
9
+ export { createRemoteDevice, getRemoteComputerCapabilities, getRemoteExecutionCapabilities, listRemoteDevicesForOwner, revokeRemoteDeviceForOwner, unregisterRemoteDevice, } from "./remote-devices-store.js";
10
+ export { claimNextComputerCommand, enqueueComputerCommand, getRemoteCommand, getRemoteCommandByIdempotencyKey, listRemoteCommandsForOwner, } from "./remote-commands-store.js";
11
11
  export { createRemoteBrowserActionEntries, type CreateRemoteBrowserActionEntriesOptions, } from "./remote-browser-actions.js";
12
12
  export { assertValidComputerCommandEnvelope, computeComputerActionHash, computerOperationRequiresApproval, ComputerSupervisionError, } from "./computer-supervision.js";
13
13
  export { authorizeComputerOperation, createComputerApprovalRequest, decideComputerApproval, getComputerApprovalForOwner, listComputerApprovalsForOwner, type ComputerApprovalRecord, type ComputerApprovalStatus, } from "./computer-supervision-store.js";
14
14
  export { insertRemoteLiveViewEvents } from "./remote-run-events-store.js";
15
15
  export { listRemotePushNotificationsForOwner, listRemotePushRegistrationsForOwner, queueRemotePushNotifications, toPublicRemotePushRegistration, unregisterRemotePushRegistrationForOwner, upsertRemotePushRegistration, } from "./remote-push-store.js";
16
16
  export { deliverPendingRemotePushNotifications } from "./remote-push-delivery.js";
17
- export type { ComputerApprovalScope, ComputerCommandAction, ComputerCommandEnvelope, ComputerOperationClass, PublicRemotePushRegistration, PublicRemoteDevice, RemoteComputerCapabilities, RemoteCommand, RemoteDevice, RemoteDeviceMetadata, RemoteLiveViewEvent, RemotePushNotification, RemotePushRegistration, RemoteRunEvent, } from "./remote-types.js";
17
+ export type { ComputerApprovalScope, ComputerCommandAction, ComputerCommandEnvelope, ComputerOperationClass, PublicRemotePushRegistration, PublicRemoteDevice, RemoteComputerCapabilities, RemoteExecutionCapabilities, RemoteExecutionBackend, RemoteExecutionPersistence, RemoteExecutionWorkload, RemoteCommand, RemoteDevice, RemoteDeviceMetadata, RemoteLiveViewEvent, RemotePushNotification, RemotePushRegistration, RemoteRunEvent, } from "./remote-types.js";
18
18
  export { resolveSlackBotTokenForIncoming, slackAdapter, type SlackAdapterOptions, } from "./adapters/slack.js";
19
19
  export { telegramAdapter } from "./adapters/telegram.js";
20
20
  export { whatsappAdapter } from "./adapters/whatsapp.js";
@@ -6,8 +6,8 @@ export { upsertVerifiedIntegrationIdentity, } from "./identity-links-store.js";
6
6
  export { BUILT_IN_INTEGRATION_CATALOG, INTEGRATION_CATEGORIES, getIntegrationCatalogEntry, listBuiltInChannelIntegrations, listIntegrationCatalog, } from "./catalog.js";
7
7
  // Plugin
8
8
  export { BUILT_IN_INTEGRATION_ADAPTER_FACTORIES, BUILT_IN_INTEGRATION_ADAPTER_IDS, createBuiltInIntegrationAdapters, createIntegrationsPlugin, defaultIntegrationsPlugin, enqueueRemoteCommand, } from "./plugin.js";
9
- export { createRemoteDevice, getRemoteComputerCapabilities, listRemoteDevicesForOwner, revokeRemoteDeviceForOwner, unregisterRemoteDevice, } from "./remote-devices-store.js";
10
- export { claimNextComputerCommand, enqueueComputerCommand, getRemoteCommand, listRemoteCommandsForOwner, } from "./remote-commands-store.js";
9
+ export { createRemoteDevice, getRemoteComputerCapabilities, getRemoteExecutionCapabilities, listRemoteDevicesForOwner, revokeRemoteDeviceForOwner, unregisterRemoteDevice, } from "./remote-devices-store.js";
10
+ export { claimNextComputerCommand, enqueueComputerCommand, getRemoteCommand, getRemoteCommandByIdempotencyKey, listRemoteCommandsForOwner, } from "./remote-commands-store.js";
11
11
  export { createRemoteBrowserActionEntries, } from "./remote-browser-actions.js";
12
12
  export { assertValidComputerCommandEnvelope, computeComputerActionHash, computerOperationRequiresApproval, ComputerSupervisionError, } from "./computer-supervision.js";
13
13
  export { authorizeComputerOperation, createComputerApprovalRequest, decideComputerApproval, getComputerApprovalForOwner, listComputerApprovalsForOwner, } from "./computer-supervision-store.js";
@@ -40,7 +40,7 @@ import { extractBearerToken, verifyInternalToken } from "./internal-token.js";
40
40
  import { retryStuckPendingTasks, startPendingTasksRetryJob, } from "./pending-tasks-retry-job.js";
41
41
  import { claimPendingTask, getNextPendingTaskForThread, getPendingTask, insertPendingTask, isDuplicateEventError, MAX_PENDING_TASK_ATTEMPTS, markTaskCompleted, markTaskDeliveryRetryable, markTaskFailed, markTaskRetryable, stageTaskDeliveryPayload, } from "./pending-tasks-store.js";
42
42
  import { claimNextComputerCommand, claimNextRemoteCommand, enqueueComputerCommand, enqueueRemoteCommand as enqueueRemoteCommandRow, isRemoteCommandKind, listRemoteCommandsForOwner, updateRemoteCommandResult, } from "./remote-commands-store.js";
43
- import { authenticateRemoteDeviceToken, createRemoteDevice, getRemoteComputerCapabilities, getRemoteDeviceForOwner, listRemoteDevicesForOwner, revokeRemoteDeviceForOwner, toPublicRemoteDevice, unregisterRemoteDevice, updateRemoteDeviceDetails, } from "./remote-devices-store.js";
43
+ import { authenticateRemoteDeviceToken, createRemoteDevice, getRemoteComputerCapabilities, getRemoteExecutionCapabilities, getRemoteDeviceForOwner, listRemoteDevicesForOwner, revokeRemoteDeviceForOwner, toPublicRemoteDevice, unregisterRemoteDevice, updateRemoteDeviceDetails, } from "./remote-devices-store.js";
44
44
  import { startRemotePushDeliveryJob } from "./remote-push-delivery-job.js";
45
45
  import { listRemotePushNotificationsForOwner, listRemotePushRegistrationsForOwner, queueRemotePushNotifications, toPublicRemotePushRegistration, unregisterRemotePushRegistrationForOwner, upsertRemotePushRegistration, } from "./remote-push-store.js";
46
46
  import { startRemoteCommandsRetryJob } from "./remote-retry-job.js";
@@ -290,9 +290,17 @@ export async function enqueueRemoteCommand(envelope) {
290
290
  limit: 10,
291
291
  });
292
292
  const requestedDeviceId = readString(command.hostId) ?? readString(command.deviceId);
293
- const device = (requestedDeviceId
293
+ const device = requestedDeviceId
294
294
  ? devices.find((candidate) => candidate.id === requestedDeviceId)
295
- : undefined) ?? devices[0];
295
+ : devices[0];
296
+ if (requestedDeviceId && !device) {
297
+ return {
298
+ ok: false,
299
+ hostOnline: false,
300
+ hostStatus: "offline",
301
+ error: "The requested execution host is not paired with this account.",
302
+ };
303
+ }
296
304
  if (!device) {
297
305
  return {
298
306
  ok: false,
@@ -353,6 +361,13 @@ function remoteCodeCommandParams(command) {
353
361
  cwd: readString(command.cwd),
354
362
  goalId: readString(command.goalId) ?? "task",
355
363
  permissionMode: readString(command.permissionMode),
364
+ engine: readString(command.engine),
365
+ model: readString(command.model),
366
+ effort: readString(command.effort),
367
+ reasoningEffort: readString(command.reasoningEffort),
368
+ runId: readString(command.runId),
369
+ workload: readString(command.workload) ?? "code-agent",
370
+ metadata: readObject(command.metadata) ?? undefined,
356
371
  };
357
372
  }
358
373
  if (type === "continue") {
@@ -389,6 +404,13 @@ function enqueueBodyToRemoteCodeCommand(body) {
389
404
  cwd: payload.cwd,
390
405
  goalId: payload.goalId,
391
406
  permissionMode: payload.permissionMode,
407
+ engine: payload.engine,
408
+ model: payload.model,
409
+ effort: payload.effort,
410
+ reasoningEffort: payload.reasoningEffort,
411
+ runId: payload.runId,
412
+ workload: payload.workload,
413
+ metadata: payload.metadata,
392
414
  };
393
415
  }
394
416
  if (operation === "code-agent.run.follow-up") {
@@ -473,6 +495,14 @@ async function hasOnlineRemoteDevice(ownerEmail, orgId) {
473
495
  }
474
496
  function remoteDeviceToHost(device) {
475
497
  const online = device.status === "active" && isRemoteDeviceOnline(device);
498
+ const executionCapabilities = getRemoteExecutionCapabilities(device);
499
+ const capabilities = [
500
+ ...(executionCapabilities?.workloads ?? []).map((workload) => `workload:${workload}`),
501
+ ...(executionCapabilities?.engines ?? []).map((engine) => `engine:${engine}`),
502
+ ...(executionCapabilities?.acceptsScheduledWork ? ["scheduled-task"] : []),
503
+ ...(executionCapabilities?.acceptsPortalHandoffs ? ["portal"] : []),
504
+ ...(device.metadata?.computerCapabilities ? ["computer"] : []),
505
+ ];
476
506
  return {
477
507
  id: device.id,
478
508
  name: device.label,
@@ -484,6 +514,8 @@ function remoteDeviceToHost(device) {
484
514
  platform: device.platform ?? "desktop",
485
515
  appVersion: device.appVersion ?? undefined,
486
516
  hostName: device.hostName ?? undefined,
517
+ capabilities,
518
+ executionCapabilities: executionCapabilities ?? undefined,
487
519
  metadata: device.metadata ?? undefined,
488
520
  device: toPublicRemoteDevice(device),
489
521
  };
@@ -672,7 +704,13 @@ export function createIntegrationsPlugin(options) {
672
704
  return runWithRequestContext(context, fn);
673
705
  }
674
706
  async function requireRemoteDevice(event) {
675
- const token = extractBearerToken(getRequestHeader(event, "authorization"));
707
+ // Some managed proxies omit Authorization before a serverless function
708
+ // sees the request. Keep the device secret in a dedicated TLS-only
709
+ // header as a transport fallback; the value is still hashed and looked
710
+ // up by authenticateRemoteDeviceToken, never persisted raw.
711
+ const token = extractBearerToken(getRequestHeader(event, "authorization")) ??
712
+ (getRequestHeader(event, "x-agent-native-device-token")?.trim() ||
713
+ null);
676
714
  const device = await authenticateRemoteDeviceToken(token);
677
715
  if (device)
678
716
  return device;
@@ -785,6 +823,8 @@ export function createIntegrationsPlugin(options) {
785
823
  const label = typeof body.label === "string" && body.label.trim()
786
824
  ? body.label.trim().slice(0, 200)
787
825
  : "Remote device";
826
+ const metadata = readObject(body.metadata);
827
+ const hasExecutionCapabilities = Object.prototype.hasOwnProperty.call(body, "executionCapabilities");
788
828
  const { device, token } = await createRemoteDevice({
789
829
  ownerEmail: ctx.ownerEmail,
790
830
  orgId: ctx.orgId,
@@ -792,7 +832,16 @@ export function createIntegrationsPlugin(options) {
792
832
  platform: readString(body.platform),
793
833
  appVersion: readString(body.appVersion) ?? readString(body.version),
794
834
  hostName: readString(body.hostName) ?? readString(body.hostname),
795
- metadata: readObject(body.metadata),
835
+ metadata: metadata || hasExecutionCapabilities
836
+ ? {
837
+ ...(metadata ?? {}),
838
+ ...(hasExecutionCapabilities
839
+ ? {
840
+ executionCapabilities: readRemoteExecutionCapabilities(body.executionCapabilities),
841
+ }
842
+ : {}),
843
+ }
844
+ : null,
796
845
  });
797
846
  return { device: toPublicRemoteDevice(device), token };
798
847
  }));
@@ -1224,7 +1273,8 @@ export function createIntegrationsPlugin(options) {
1224
1273
  let pollingDevice = device;
1225
1274
  if (method === "POST" &&
1226
1275
  (Object.prototype.hasOwnProperty.call(body, "computerCapabilities") ||
1227
- Object.prototype.hasOwnProperty.call(body, "browserSession"))) {
1276
+ Object.prototype.hasOwnProperty.call(body, "browserSession") ||
1277
+ Object.prototype.hasOwnProperty.call(body, "executionCapabilities"))) {
1228
1278
  const updated = await updateRemoteDeviceDetails({
1229
1279
  id: device.id,
1230
1280
  metadata: {
@@ -1234,6 +1284,11 @@ export function createIntegrationsPlugin(options) {
1234
1284
  computerCapabilities: readComputerCapabilities(body.computerCapabilities),
1235
1285
  }
1236
1286
  : {}),
1287
+ ...(Object.prototype.hasOwnProperty.call(body, "executionCapabilities")
1288
+ ? {
1289
+ executionCapabilities: readRemoteExecutionCapabilities(body.executionCapabilities),
1290
+ }
1291
+ : {}),
1237
1292
  ...(Object.prototype.hasOwnProperty.call(body, "browserSession")
1238
1293
  ? { browserSession: readBrowserSession(body.browserSession) }
1239
1294
  : {}),
@@ -2753,6 +2808,40 @@ function readComputerCapabilities(value) {
2753
2808
  desktop: readSurface(input?.desktop, true),
2754
2809
  };
2755
2810
  }
2811
+ function readRemoteExecutionCapabilities(value) {
2812
+ const input = readObject(value);
2813
+ if (!input)
2814
+ return {};
2815
+ const backend = readString(input.backend);
2816
+ const persistence = readString(input.persistence);
2817
+ const list = (candidate, max) => [
2818
+ ...new Set(Array.isArray(candidate)
2819
+ ? candidate
2820
+ .filter((entry) => typeof entry === "string")
2821
+ .map((entry) => entry.trim().slice(0, 120))
2822
+ .filter(Boolean)
2823
+ : []),
2824
+ ].slice(0, max);
2825
+ return {
2826
+ ...(backend === "desktop" ||
2827
+ backend === "container" ||
2828
+ backend === "kubernetes" ||
2829
+ backend === "external"
2830
+ ? { backend }
2831
+ : {}),
2832
+ workloads: list(input.workloads, 16),
2833
+ engines: list(input.engines, 32),
2834
+ ...(typeof input.acceptsScheduledWork === "boolean"
2835
+ ? { acceptsScheduledWork: input.acceptsScheduledWork }
2836
+ : {}),
2837
+ ...(persistence === "local-files" ||
2838
+ persistence === "persistent-volume" ||
2839
+ persistence === "ephemeral"
2840
+ ? { persistence }
2841
+ : {}),
2842
+ adapters: list(input.adapters, 32),
2843
+ };
2844
+ }
2756
2845
  function readBrowserSession(value) {
2757
2846
  if (value === null)
2758
2847
  return null;
@@ -8,6 +8,7 @@ export declare function enqueueRemoteCommand(input: {
8
8
  params?: unknown;
9
9
  platform?: string | null;
10
10
  externalThreadId?: string | null;
11
+ idempotencyKey?: string | null;
11
12
  nextCheckAt?: number;
12
13
  }): Promise<RemoteCommand>;
13
14
  export declare function enqueueComputerCommand(input: {
@@ -18,6 +19,10 @@ export declare function enqueueComputerCommand(input: {
18
19
  platform?: string | null;
19
20
  }): Promise<RemoteCommand>;
20
21
  export declare function getRemoteCommand(id: string): Promise<RemoteCommand | null>;
22
+ export declare function getRemoteCommandByIdempotencyKey(input: {
23
+ deviceId: string;
24
+ idempotencyKey: string;
25
+ }): Promise<RemoteCommand | null>;
21
26
  export declare function listRemoteCommandsForOwner(input: {
22
27
  ownerEmail: string;
23
28
  orgId?: string | null;
@@ -83,6 +83,7 @@ function rowToCommand(row) {
83
83
  result: parseJson(row.result_json, null),
84
84
  platform: row.platform ?? null,
85
85
  externalThreadId: row.external_thread_id ?? null,
86
+ idempotencyKey: row.idempotency_key ?? null,
86
87
  computerOperation: row.kind === "computer-operation"
87
88
  ? (params?.envelope ?? null)
88
89
  : null,
@@ -151,28 +152,52 @@ export async function enqueueRemoteCommand(input) {
151
152
  const client = getDbExec();
152
153
  const now = Date.now();
153
154
  const id = `remote-command-${now}-${randomHex(8)}`;
154
- await client.execute({
155
- sql: `INSERT INTO integration_remote_commands
156
- (id, device_id, owner_email, org_id, kind, params_json, status, result_json,
157
- platform, external_thread_id, attempts, next_check_at, created_at, updated_at)
158
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
159
- args: [
160
- id,
161
- input.deviceId,
162
- input.ownerEmail,
163
- input.orgId ?? null,
164
- input.kind,
165
- JSON.stringify(input.params ?? {}),
166
- "pending",
167
- null,
168
- input.platform ?? null,
169
- input.externalThreadId ?? null,
170
- 0,
171
- input.nextCheckAt ?? now,
172
- now,
173
- now,
174
- ],
175
- });
155
+ const idempotencyKey = normalizeIdempotencyKey(input.idempotencyKey);
156
+ if (idempotencyKey) {
157
+ const existing = await getRemoteCommandByIdempotencyKey({
158
+ deviceId: input.deviceId,
159
+ idempotencyKey,
160
+ });
161
+ if (existing)
162
+ return existing;
163
+ }
164
+ try {
165
+ await client.execute({
166
+ sql: `INSERT INTO integration_remote_commands
167
+ (id, device_id, owner_email, org_id, kind, params_json, status, result_json,
168
+ platform, external_thread_id, idempotency_key, attempts, next_check_at,
169
+ created_at, updated_at)
170
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
171
+ args: [
172
+ id,
173
+ input.deviceId,
174
+ input.ownerEmail,
175
+ input.orgId ?? null,
176
+ input.kind,
177
+ JSON.stringify(input.params ?? {}),
178
+ "pending",
179
+ null,
180
+ input.platform ?? null,
181
+ input.externalThreadId ?? null,
182
+ idempotencyKey,
183
+ 0,
184
+ input.nextCheckAt ?? now,
185
+ now,
186
+ now,
187
+ ],
188
+ });
189
+ }
190
+ catch (error) {
191
+ if (idempotencyKey && isUniqueConstraintError(error)) {
192
+ const existing = await getRemoteCommandByIdempotencyKey({
193
+ deviceId: input.deviceId,
194
+ idempotencyKey,
195
+ });
196
+ if (existing)
197
+ return existing;
198
+ }
199
+ throw error;
200
+ }
176
201
  const command = await getRemoteCommand(id);
177
202
  if (!command)
178
203
  throw new Error("remote command insert failed");
@@ -259,6 +284,16 @@ export async function getRemoteCommand(id) {
259
284
  });
260
285
  return rows[0] ? rowToCommand(rows[0]) : null;
261
286
  }
287
+ export async function getRemoteCommandByIdempotencyKey(input) {
288
+ await ensureTable();
289
+ const { rows } = await getDbExec().execute({
290
+ sql: `SELECT * FROM integration_remote_commands
291
+ WHERE device_id = ? AND idempotency_key = ?
292
+ LIMIT 1`,
293
+ args: [input.deviceId, input.idempotencyKey],
294
+ });
295
+ return rows[0] ? rowToCommand(rows[0]) : null;
296
+ }
262
297
  export async function listRemoteCommandsForOwner(input) {
263
298
  await ensureTable();
264
299
  const limit = Math.max(1, Math.min(input.limit ?? 100, 250));
@@ -529,6 +564,12 @@ function randomHex(byteLength) {
529
564
  .map((byte) => byte.toString(16).padStart(2, "0"))
530
565
  .join("");
531
566
  }
567
+ function normalizeIdempotencyKey(value) {
568
+ if (typeof value !== "string")
569
+ return null;
570
+ const normalized = value.trim();
571
+ return normalized ? normalized.slice(0, 512) : null;
572
+ }
532
573
  function affectedRows(result) {
533
574
  return (result.rowsAffected ??
534
575
  Number(result.rowCount ?? 0));
@@ -1,6 +1,7 @@
1
- import type { PublicRemoteDevice, RemoteComputerCapabilities, RemoteDevice, RemoteDeviceMetadata } from "./remote-types.js";
1
+ import type { PublicRemoteDevice, RemoteComputerCapabilities, RemoteDevice, RemoteDeviceMetadata, RemoteExecutionCapabilities } from "./remote-types.js";
2
2
  export declare function toPublicRemoteDevice(device: RemoteDevice): PublicRemoteDevice;
3
3
  export declare function getRemoteComputerCapabilities(device: Pick<RemoteDevice, "metadata">): RemoteComputerCapabilities | null;
4
+ export declare function getRemoteExecutionCapabilities(device: Pick<RemoteDevice, "metadata">): RemoteExecutionCapabilities | null;
4
5
  export declare function createRemoteDevice(input: {
5
6
  ownerEmail: string;
6
7
  orgId?: string | null;
@@ -115,6 +115,12 @@ export function getRemoteComputerCapabilities(device) {
115
115
  return null;
116
116
  return normalizeComputerCapabilities(value);
117
117
  }
118
+ export function getRemoteExecutionCapabilities(device) {
119
+ const value = device.metadata?.executionCapabilities;
120
+ if (!value || typeof value !== "object")
121
+ return null;
122
+ return normalizeExecutionCapabilities(value);
123
+ }
118
124
  export async function createRemoteDevice(input) {
119
125
  await ensureTable();
120
126
  const client = getDbExec();
@@ -334,6 +340,9 @@ function serializeRemoteDeviceMetadata(metadata) {
334
340
  if (metadata.computerCapabilities !== undefined) {
335
341
  normalized.computerCapabilities = normalizeComputerCapabilities(metadata.computerCapabilities);
336
342
  }
343
+ if (metadata.executionCapabilities !== undefined) {
344
+ normalized.executionCapabilities = normalizeExecutionCapabilities(metadata.executionCapabilities);
345
+ }
337
346
  const json = JSON.stringify(normalized);
338
347
  if (new TextEncoder().encode(json).byteLength > 32_768) {
339
348
  throw new Error("Remote device metadata exceeds 32 KiB");
@@ -365,6 +374,43 @@ function normalizeComputerCapabilities(value) {
365
374
  }
366
375
  return result;
367
376
  }
377
+ function normalizeExecutionCapabilities(value) {
378
+ const result = {};
379
+ if (value.backend === "desktop" ||
380
+ value.backend === "container" ||
381
+ value.backend === "kubernetes" ||
382
+ value.backend === "external") {
383
+ result.backend = value.backend;
384
+ }
385
+ if (Array.isArray(value.workloads)) {
386
+ result.workloads = normalizeStringList(value.workloads, 16).filter((entry) => entry === "code-agent" ||
387
+ entry === "scheduled-code" ||
388
+ entry === "external-agent");
389
+ }
390
+ if (Array.isArray(value.engines)) {
391
+ result.engines = normalizeStringList(value.engines, 32);
392
+ }
393
+ if (typeof value.acceptsScheduledWork === "boolean") {
394
+ result.acceptsScheduledWork = value.acceptsScheduledWork;
395
+ }
396
+ if (value.persistence === "local-files" ||
397
+ value.persistence === "persistent-volume" ||
398
+ value.persistence === "ephemeral") {
399
+ result.persistence = value.persistence;
400
+ }
401
+ if (Array.isArray(value.adapters)) {
402
+ result.adapters = normalizeStringList(value.adapters, 32);
403
+ }
404
+ return result;
405
+ }
406
+ function normalizeStringList(value, max) {
407
+ return [
408
+ ...new Set(value
409
+ .filter((entry) => typeof entry === "string")
410
+ .map((entry) => entry.trim().slice(0, 120))
411
+ .filter(Boolean)),
412
+ ].slice(0, max);
413
+ }
368
414
  function looksLikeLargeBase64(value) {
369
415
  return value.length > 4_096 && /[A-Za-z0-9+/]{4_096,}={0,2}/.test(value);
370
416
  }
@@ -1,4 +1,22 @@
1
1
  export type RemoteCommandKind = "create-run" | "list-runs" | "get-run" | "append-followup" | "approve" | "deny" | "stop" | "status" | "computer-operation";
2
+ /** Workloads a paired execution host may accept from the relay. */
3
+ export type RemoteExecutionWorkload = "code-agent" | "scheduled-code" | "external-agent";
4
+ export type RemoteExecutionBackend = "desktop" | "container" | "kubernetes" | "external";
5
+ export type RemoteExecutionPersistence = "local-files" | "persistent-volume" | "ephemeral";
6
+ /**
7
+ * Non-secret capabilities advertised by a host during its heartbeat. The
8
+ * relay uses this to reject a requested workload before queueing it, while
9
+ * leaving room for a future Kubernetes or external-agent adapter.
10
+ */
11
+ export interface RemoteExecutionCapabilities {
12
+ backend?: RemoteExecutionBackend;
13
+ workloads?: RemoteExecutionWorkload[];
14
+ engines?: string[];
15
+ acceptsScheduledWork?: boolean;
16
+ acceptsPortalHandoffs?: boolean;
17
+ persistence?: RemoteExecutionPersistence;
18
+ adapters?: string[];
19
+ }
2
20
  export type ComputerOperationClass = "browser.observe" | "browser.control" | "desktop.observe" | "desktop.control";
3
21
  export type ComputerApprovalScope = "once" | "run" | "task";
4
22
  export interface ComputerCommandAction {
@@ -40,6 +58,7 @@ export interface RemoteComputerCapabilities {
40
58
  }
41
59
  export interface RemoteDeviceMetadata extends Record<string, unknown> {
42
60
  computerCapabilities?: RemoteComputerCapabilities;
61
+ executionCapabilities?: RemoteExecutionCapabilities;
43
62
  }
44
63
  export type RemoteCommandStatus = "pending" | "claimed" | "running" | "completed" | "failed";
45
64
  export type RemoteDeviceStatus = "active" | "inactive";
@@ -85,6 +104,7 @@ export interface RemoteCommand {
85
104
  result: unknown;
86
105
  platform: string | null;
87
106
  externalThreadId: string | null;
107
+ idempotencyKey?: string | null;
88
108
  computerOperation?: ComputerCommandEnvelope | null;
89
109
  attempts: number;
90
110
  nextCheckAt: number;
@@ -14,6 +14,9 @@ export interface RecurringJobActionItem {
14
14
  lastError: string | null;
15
15
  nextRun: string | null;
16
16
  createdBy: string | null;
17
+ executionHostId: string | null;
18
+ executionEngine: string | null;
19
+ executionCwd: string | null;
17
20
  mcpTools: string[];
18
21
  canUpdate: boolean;
19
22
  }
@@ -59,7 +59,8 @@ export default defineAction({
59
59
  const { meta, body } = parseJobFrontmatter(full.content);
60
60
  if (!jobBelongsToApp(meta, ctx?.appId))
61
61
  continue;
62
- const canUpdate = scope === "personal" || !(await authorizeJobMutation(owner, meta));
62
+ const canUpdate = scope === "personal" ||
63
+ (await authorizeJobMutation(owner, meta)) === null;
63
64
  jobs.push({
64
65
  id: full.id,
65
66
  name: jobName(full.path),
@@ -78,6 +79,9 @@ export default defineAction({
78
79
  lastError: meta.lastError ?? null,
79
80
  nextRun: nextRun(meta),
80
81
  createdBy: meta.createdBy ?? null,
82
+ executionHostId: meta.executionHostId ?? null,
83
+ executionEngine: meta.executionEngine ?? null,
84
+ executionCwd: meta.executionCwd ?? null,
81
85
  mcpTools: meta.mcpTools ?? [],
82
86
  canUpdate,
83
87
  });
@@ -52,6 +52,22 @@ export interface JobFrontmatter {
52
52
  domain?: string;
53
53
  /** Explicit application owner used by the recurring-job scheduler. */
54
54
  appId?: string;
55
+ /** Optional paired execution host for code-agent work. */
56
+ executionHostId?: string;
57
+ /** Optional engine id understood by the selected execution host. */
58
+ executionEngine?: string;
59
+ /** Optional host-local workspace path used by code-agent work. */
60
+ executionCwd?: string;
61
+ /** Stable remote dispatch key for a running host-targeted job. */
62
+ remoteRequestId?: string;
63
+ /** Durable relay command id for a running host-targeted job. */
64
+ remoteCommandId?: string;
65
+ /** Durable remote code-agent run id, when the host has started one. */
66
+ remoteRunId?: string;
67
+ /** Durable automation history row associated with the remote dispatch. */
68
+ remoteAutomationRunId?: string;
69
+ /** Whether a completed remote dispatch should advance the cron schedule. */
70
+ remoteAdvanceSchedule?: boolean;
55
71
  /**
56
72
  * Optional application-owned policy id carried into actions by the trusted
57
73
  * trigger dispatcher. It is not model-supplied action input.
@@ -18,6 +18,15 @@ const MAX_JOB_MCP_TOOLS = 64;
18
18
  const JOB_MCP_TOOL_NAME_RE = /^mcp__[^\s]+__[^\s]+$/;
19
19
  const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n)?([\s\S]*)$/;
20
20
  const DELEGATED_POLICY_ID_RE = /^[a-z0-9][a-z0-9._:-]{0,127}$/i;
21
+ const EXECUTION_ID_RE = /^[a-z0-9][a-z0-9._:-]{0,127}$/i;
22
+ const REMOTE_ID_RE = /^[a-z0-9][a-z0-9@+._:/-]{0,511}$/i;
23
+ function assertBoundedFrontmatterValue(value, label, pattern) {
24
+ if (value === undefined)
25
+ return;
26
+ if (!pattern.test(value)) {
27
+ throw new Error(`${label} must be a bounded opaque identifier.`);
28
+ }
29
+ }
21
30
  /**
22
31
  * Normalize the non-secret MCP capability references persisted with a job.
23
32
  * Tool names are opaque framework identifiers; URLs and credentials never
@@ -159,6 +168,30 @@ function parseKnownField(meta, key, rawValue) {
159
168
  case "appId":
160
169
  meta.appId = value || undefined;
161
170
  break;
171
+ case "executionHostId":
172
+ meta.executionHostId = value || undefined;
173
+ break;
174
+ case "executionEngine":
175
+ meta.executionEngine = value || undefined;
176
+ break;
177
+ case "executionCwd":
178
+ meta.executionCwd = value || undefined;
179
+ break;
180
+ case "remoteRequestId":
181
+ meta.remoteRequestId = value || undefined;
182
+ break;
183
+ case "remoteCommandId":
184
+ meta.remoteCommandId = value || undefined;
185
+ break;
186
+ case "remoteRunId":
187
+ meta.remoteRunId = value || undefined;
188
+ break;
189
+ case "remoteAutomationRunId":
190
+ meta.remoteAutomationRunId = value || undefined;
191
+ break;
192
+ case "remoteAdvanceSchedule":
193
+ meta.remoteAdvanceSchedule = value !== "false";
194
+ break;
162
195
  case "delegatedPolicyId":
163
196
  meta.delegatedPolicyId = value || undefined;
164
197
  break;
@@ -211,6 +244,16 @@ export function buildJobResourceContent(meta, body) {
211
244
  !DELEGATED_POLICY_ID_RE.test(meta.delegatedPolicyId)) {
212
245
  throw new Error("Delegated automation policy IDs must be 1-128 letters, numbers, dots, underscores, colons, or hyphens.");
213
246
  }
247
+ assertBoundedFrontmatterValue(meta.executionHostId, "Execution host IDs", EXECUTION_ID_RE);
248
+ assertBoundedFrontmatterValue(meta.executionEngine, "Execution engine IDs", EXECUTION_ID_RE);
249
+ assertBoundedFrontmatterValue(meta.remoteRequestId, "Remote request IDs", REMOTE_ID_RE);
250
+ assertBoundedFrontmatterValue(meta.remoteCommandId, "Remote command IDs", REMOTE_ID_RE);
251
+ assertBoundedFrontmatterValue(meta.remoteRunId, "Remote run IDs", REMOTE_ID_RE);
252
+ assertBoundedFrontmatterValue(meta.remoteAutomationRunId, "Remote automation run IDs", REMOTE_ID_RE);
253
+ if (meta.executionCwd !== undefined &&
254
+ (meta.executionCwd.length > 1024 || /[\r\n]/.test(meta.executionCwd))) {
255
+ throw new Error("Execution workspace paths must be at most 1024 characters.");
256
+ }
214
257
  const lines = [
215
258
  "---",
216
259
  `schedule: ${JSON.stringify(meta.schedule)}`,
@@ -225,6 +268,16 @@ export function buildJobResourceContent(meta, body) {
225
268
  pushString(lines, "domain", meta.domain);
226
269
  pushString(lines, "appId", meta.appId);
227
270
  pushString(lines, "delegatedPolicyId", meta.delegatedPolicyId);
271
+ pushString(lines, "executionHostId", meta.executionHostId);
272
+ pushString(lines, "executionEngine", meta.executionEngine);
273
+ pushString(lines, "executionCwd", meta.executionCwd);
274
+ pushString(lines, "remoteRequestId", meta.remoteRequestId);
275
+ pushString(lines, "remoteCommandId", meta.remoteCommandId);
276
+ pushString(lines, "remoteRunId", meta.remoteRunId);
277
+ pushString(lines, "remoteAutomationRunId", meta.remoteAutomationRunId);
278
+ if (meta.remoteAdvanceSchedule !== undefined) {
279
+ lines.push(`remoteAdvanceSchedule: ${meta.remoteAdvanceSchedule}`);
280
+ }
228
281
  // Keep the long-standing human-readable owner shape used by existing
229
282
  // resources and diagnostics; values that can contain free-form text use
230
283
  // JSON quoting below.
@@ -0,0 +1,42 @@
1
+ import type { RemoteCommand } from "../integrations/remote-types.js";
2
+ import { type Resource } from "../resources/store.js";
3
+ import { type JobFrontmatter } from "./frontmatter.js";
4
+ /** A remote run is allowed to be long-lived, but never indefinitely invisible. */
5
+ export declare const REMOTE_AUTOMATION_MAX_ACTIVE_MS: number;
6
+ export type RemoteAutomationState = "not-remote" | "active" | "completed" | "failed";
7
+ export interface RemoteAutomationStatus {
8
+ state: RemoteAutomationState;
9
+ command?: RemoteCommand;
10
+ error?: string;
11
+ }
12
+ export interface RemoteAutomationDispatchInput {
13
+ resource: Resource;
14
+ meta: JobFrontmatter;
15
+ body: string;
16
+ ownerEmail: string;
17
+ orgId?: string;
18
+ appId?: string;
19
+ prompt: string;
20
+ title: string;
21
+ historyId?: string;
22
+ /** Whether completing this remote dispatch should advance its cron schedule. */
23
+ advanceSchedule?: boolean;
24
+ now?: Date;
25
+ }
26
+ /**
27
+ * Queue one host-targeted automation. The request key is derived from the
28
+ * persisted start timestamp, so a worker crash between the queue write and
29
+ * resource bookkeeping cannot create a second remote run.
30
+ */
31
+ export declare function dispatchRemoteAutomation(input: RemoteAutomationDispatchInput): Promise<{
32
+ command: RemoteCommand;
33
+ requestId: string;
34
+ automationRunId?: string;
35
+ }>;
36
+ export declare function getRemoteAutomationStatus(input: {
37
+ meta: JobFrontmatter;
38
+ ownerEmail: string;
39
+ orgId?: string;
40
+ now?: Date;
41
+ }): Promise<RemoteAutomationStatus>;
42
+ export declare function finishRemoteAutomationHistory(meta: Pick<JobFrontmatter, "remoteAutomationRunId">, state: "completed" | "failed", error?: string): Promise<void>;