@agent-native/core 0.157.9 → 0.157.11

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 (114) 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 +1215 -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/server/routes/api/exports/google-slides.post.ts +12 -4
  20. package/corpus/templates/slides/shared/aspect-ratios.ts +13 -1
  21. package/dist/agent/harness/migrations.js +24 -3
  22. package/dist/agent/production-agent.js +11 -0
  23. package/dist/agent/run-loop-with-resume.js +21 -4
  24. package/dist/automations/service.d.ts +6 -0
  25. package/dist/automations/service.js +35 -0
  26. package/dist/cli/code-agent-connector.d.ts +4 -1
  27. package/dist/cli/code-agent-connector.js +154 -23
  28. package/dist/cli/code-agent-executor.js +255 -13
  29. package/dist/cli/code-agent-runs.d.ts +1 -0
  30. package/dist/cli/code-agent-runs.js +2 -1
  31. package/dist/cli/portal-workspace.d.ts +49 -0
  32. package/dist/cli/portal-workspace.js +320 -0
  33. package/dist/client/AssistantChat.d.ts +1 -1
  34. package/dist/client/AssistantChat.js +39 -61
  35. package/dist/client/ConnectBuilderCard.js +33 -5
  36. package/dist/client/agent-chat-adapter.js +4 -0
  37. package/dist/client/analytics-platform.d.ts +2 -0
  38. package/dist/client/analytics-platform.js +12 -0
  39. package/dist/client/analytics.d.ts +3 -0
  40. package/dist/client/analytics.js +10 -1
  41. package/dist/client/chat/index.d.ts +1 -0
  42. package/dist/client/chat/index.js +1 -0
  43. package/dist/client/chat/run-recovery.d.ts +2 -1
  44. package/dist/client/chat/run-recovery.js +2 -2
  45. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  46. package/dist/client/chat-first/apps-rail.js +11 -11
  47. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  48. package/dist/client/chat-first/surface-tabs.js +38 -36
  49. package/dist/client/chat-first/types.d.ts +5 -0
  50. package/dist/client/chat-first.d.ts +1 -1
  51. package/dist/client/chat-first.js +4 -4
  52. package/dist/client/desktop-local-code-change.d.ts +10 -0
  53. package/dist/client/desktop-local-code-change.js +16 -0
  54. package/dist/client/index.d.ts +1 -0
  55. package/dist/client/index.js +1 -0
  56. package/dist/client/observability/ThumbsFeedback.js +21 -7
  57. package/dist/client/terminal/AgentTerminal.d.ts +9 -1
  58. package/dist/client/terminal/AgentTerminal.js +38 -3
  59. package/dist/client/terminal/index.d.ts +1 -1
  60. package/dist/client/terminal/index.js +1 -1
  61. package/dist/client/track.js +9 -1
  62. package/dist/client/use-action.js +3 -0
  63. package/dist/collab/awareness.d.ts +2 -2
  64. package/dist/collab/struct-routes.d.ts +1 -1
  65. package/dist/db/client.js +1 -0
  66. package/dist/deploy/build.js +1 -1
  67. package/dist/ingestion/pptx.d.ts +78 -0
  68. package/dist/ingestion/pptx.js +991 -107
  69. package/dist/integrations/index.d.ts +3 -3
  70. package/dist/integrations/index.js +2 -2
  71. package/dist/integrations/plugin.js +95 -6
  72. package/dist/integrations/remote-commands-store.d.ts +5 -0
  73. package/dist/integrations/remote-commands-store.js +63 -22
  74. package/dist/integrations/remote-devices-store.d.ts +2 -1
  75. package/dist/integrations/remote-devices-store.js +46 -0
  76. package/dist/integrations/remote-types.d.ts +20 -0
  77. package/dist/jobs/actions/list-recurring-jobs.d.ts +3 -0
  78. package/dist/jobs/actions/list-recurring-jobs.js +5 -1
  79. package/dist/jobs/frontmatter.d.ts +16 -0
  80. package/dist/jobs/frontmatter.js +53 -0
  81. package/dist/jobs/remote-execution.d.ts +42 -0
  82. package/dist/jobs/remote-execution.js +287 -0
  83. package/dist/jobs/scheduler.js +102 -3
  84. package/dist/jobs/tools.js +50 -3
  85. package/dist/notifications/routes.d.ts +4 -4
  86. package/dist/observability/routes.d.ts +5 -5
  87. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  88. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  89. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  90. package/dist/resources/handlers.d.ts +1 -1
  91. package/dist/secrets/routes.d.ts +9 -9
  92. package/dist/server/action-routes.js +4 -2
  93. package/dist/server/agent-chat-plugin.js +6 -0
  94. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  95. package/dist/server/agent-run-context.d.ts +1 -0
  96. package/dist/server/agent-run-context.js +9 -0
  97. package/dist/server/core-routes-plugin.js +8 -1
  98. package/dist/server/request-context.d.ts +2 -0
  99. package/dist/server/transcribe-voice.d.ts +1 -1
  100. package/dist/shared/analytics-platform.d.ts +6 -0
  101. package/dist/shared/analytics-platform.js +14 -0
  102. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  103. package/dist/shared/mcp-embed-headers.js +1 -1
  104. package/dist/styles/agent-native.css +83 -103
  105. package/dist/terminal/cli-registry.js +5 -0
  106. package/dist/tracking/registry.js +9 -1
  107. package/dist/triggers/actions/list-automations.d.ts +3 -0
  108. package/dist/triggers/actions/list-automations.js +3 -0
  109. package/dist/triggers/actions/manage-automation.d.ts +9 -0
  110. package/dist/triggers/actions/manage-automation.js +14 -2
  111. package/dist/triggers/actions.js +82 -7
  112. package/docs/content/template-content-developers.mdx +48 -0
  113. package/docs/content/toolkit-ui.mdx +57 -0
  114. package/package.json +2 -2
@@ -621,6 +621,19 @@ export function detectBlockAlignment(
621
621
  blockRight: number,
622
622
  ): "left" | "center" | "right" {
623
623
  if (blockLines.length < 2) return "left";
624
+ // The block bounds come from these same lines. Equal-width lines therefore
625
+ // have identical left edges, right edges, and midpoints whether they were
626
+ // left-aligned or centered; keep that ambiguous case left-aligned rather
627
+ // than moving ordinary paragraphs to the center.
628
+ const widths = blockLines.map((line) => line.right - line.left);
629
+ const minWidth = Math.min(...widths);
630
+ const maxWidth = Math.max(...widths);
631
+ const sameBounds = blockLines.every(
632
+ (line) => line.left === blockLeft && line.right === blockRight,
633
+ );
634
+ if (maxWidth - minWidth <= TEXT_ALIGNMENT_TOLERANCE_PT && sameBounds) {
635
+ return "left";
636
+ }
624
637
  const midpoint = (blockLeft + blockRight) / 2;
625
638
  const centered = blockLines.every(
626
639
  (line) =>
@@ -12,6 +12,30 @@ const BROWSER_RENDERABLE_IMAGE_MIME_TYPES = new Set([
12
12
  "image/bmp",
13
13
  ]);
14
14
 
15
+ /**
16
+ * Validate every image before any upload starts. PPTX imports reject partial
17
+ * fidelity, so an unsupported image must not leave earlier slide uploads
18
+ * orphaned when the action eventually throws.
19
+ */
20
+ export function assertPptxImagesRenderable(
21
+ slides: readonly ParsedSlide[],
22
+ ): void {
23
+ const unsupported = new Set<string>();
24
+ for (const slide of slides) {
25
+ for (const element of slide.elements ?? []) {
26
+ if (element.kind !== "image" || !element.image) continue;
27
+ if (!BROWSER_RENDERABLE_IMAGE_MIME_TYPES.has(element.image.mimeType)) {
28
+ unsupported.add(element.image.mimeType || "unknown");
29
+ }
30
+ }
31
+ }
32
+ if (unsupported.size > 0) {
33
+ throw new Error(
34
+ `Source-faithful PPTX import cannot preserve unsupported image type(s): ${[...unsupported].join(", ")}. No images were uploaded. Re-export the deck with browser-renderable images or use a PDF export for page-faithful preservation.`,
35
+ );
36
+ }
37
+ }
38
+
15
39
  export async function uploadPptxSlideImages(args: {
16
40
  slide: ParsedSlide;
17
41
  slideIndex: number;
@@ -14,10 +14,18 @@ const UPLOAD_URL =
14
14
  "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,webViewLink";
15
15
 
16
16
  /**
17
- * Uploads a browser-generated PPTX into the user's Drive, letting Drive convert
18
- * it to a native Google Slides deck. The PPTX comes from the client because the
19
- * browser export renders the real slide DOM — the server-side pptxgenjs export
20
- * is a lower-fidelity fallback and would ship a visibly worse deck to Google.
17
+ * Uploads a PPTX into the user's Drive, letting Drive convert it to a native
18
+ * Google Slides deck. The caller decides which exporter produced those bytes,
19
+ * and which one is higher fidelity depends on the deck:
20
+ *
21
+ * - Source-imported decks go through the server `export-pptx`, which writes the
22
+ * source's own geometry as real vector shapes. The browser exporter cannot —
23
+ * `dom-to-pptx` has no custom-geometry support at all and rasterizes every
24
+ * vector shape to a bitmap, so Google receives silhouettes instead of curves.
25
+ * - Editor-authored decks go through the browser exporter, the only place
26
+ * geometry positioned in the DOM is measurable.
27
+ *
28
+ * See "Export Behavior" in `templates/slides/AGENTS.md` for the routing rule.
21
29
  */
22
30
  export default defineEventHandler(async (event) => {
23
31
  const session = await getSession(event).catch(() => null);
@@ -23,10 +23,22 @@ export const ASPECT_RATIOS = {
23
23
  label: "4:5",
24
24
  pptxInches: { w: 8, h: 10 },
25
25
  },
26
+ "4:3": {
27
+ width: 960,
28
+ height: 720,
29
+ label: "4:3",
30
+ pptxInches: { w: 10, h: 7.5 },
31
+ },
26
32
  } as const;
27
33
 
28
34
  export type AspectRatio = keyof typeof ASPECT_RATIOS;
29
- export const ASPECT_RATIO_VALUES = ["16:9", "1:1", "9:16", "4:5"] as const;
35
+ export const ASPECT_RATIO_VALUES = [
36
+ "16:9",
37
+ "1:1",
38
+ "9:16",
39
+ "4:5",
40
+ "4:3",
41
+ ] as const;
30
42
  export const DEFAULT_ASPECT_RATIO: AspectRatio = "16:9";
31
43
 
32
44
  export function getAspectRatioDims(ratio: AspectRatio | undefined | null) {
@@ -10,8 +10,8 @@ export const AGENT_HARNESS_SESSION_MIGRATIONS = [
10
10
  harness_name TEXT NOT NULL,
11
11
  thread_id TEXT NOT NULL,
12
12
  status TEXT NOT NULL DEFAULT 'idle',
13
- created_at INTEGER NOT NULL,
14
- updated_at INTEGER NOT NULL
13
+ created_at BIGINT NOT NULL,
14
+ updated_at BIGINT NOT NULL
15
15
  )
16
16
  `,
17
17
  },
@@ -27,7 +27,7 @@ export const AGENT_HARNESS_SESSION_MIGRATIONS = [
27
27
  ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS resolved_approval_ids TEXT;
28
28
  ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS owner_email TEXT;
29
29
  ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS org_id TEXT;
30
- ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS stopped_at INTEGER;
30
+ ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS stopped_at BIGINT;
31
31
  CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_thread
32
32
  ON agent_harness_sessions (thread_id, updated_at);
33
33
  CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_status
@@ -36,4 +36,25 @@ export const AGENT_HARNESS_SESSION_MIGRATIONS = [
36
36
  ON agent_harness_sessions (owner_email, updated_at)
37
37
  `,
38
38
  },
39
+ {
40
+ version: 3,
41
+ name: "agent-harness-sessions-widen-millisecond-timestamps",
42
+ // Existing release migrations may have created these columns as Postgres
43
+ // int4 before the timestamp contract was widened. The type change is
44
+ // additive and keeps Date.now() values representable on those databases.
45
+ sql: {
46
+ postgres: `
47
+ -- guard:allow-destructive-ddl — widen legacy int4 timestamp storage to preserve Date.now() values
48
+ ALTER TABLE agent_harness_sessions
49
+ ALTER COLUMN created_at TYPE BIGINT;
50
+ -- guard:allow-destructive-ddl — widen legacy int4 timestamp storage to preserve Date.now() values
51
+ ALTER TABLE agent_harness_sessions
52
+ ALTER COLUMN updated_at TYPE BIGINT;
53
+ -- guard:allow-destructive-ddl — widen legacy int4 timestamp storage to preserve Date.now() values
54
+ ALTER TABLE agent_harness_sessions
55
+ ALTER COLUMN stopped_at TYPE BIGINT
56
+ `,
57
+ sqlite: "",
58
+ },
59
+ },
39
60
  ];
@@ -19,6 +19,7 @@ import { readBody } from "../server/h3-helpers.js";
19
19
  import { resolveHostedHarnessPolicy } from "../server/hosted-harness-policy.js";
20
20
  import { assertRequestActionSurfaceIsolation, getRequestRunContext, ensureRequestRunContext, getRequestContext, getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../server/request-context.js";
21
21
  import { fireInternalDispatch } from "../server/self-dispatch.js";
22
+ import { ANALYTICS_CLIENT_PLATFORM_BODY_FIELD } from "../shared/analytics-platform.js";
22
23
  import { isReasoningEffort, normalizeReasoningEffortForRequest, stepDownReasoningEffort, } from "../shared/reasoning-effort.js";
23
24
  import { actionPreparationContinuationNote } from "./action-continuation-guidance.js";
24
25
  import { drainAgentWarnings, formatAgentWarningsForToolResult, } from "./action-warnings.js";
@@ -6226,6 +6227,16 @@ export function createProductionAgentHandler(options) {
6226
6227
  isAgentChatDurableBackgroundEnabled({
6227
6228
  appOptIn: options.durableBackgroundRuns,
6228
6229
  });
6230
+ const mutableBody = body;
6231
+ if (!isBackgroundWorker) {
6232
+ delete mutableBody[ANALYTICS_CLIENT_PLATFORM_BODY_FIELD];
6233
+ }
6234
+ if (dispatchToBackground) {
6235
+ const clientPlatform = getRequestContext()?.clientPlatform;
6236
+ if (clientPlatform) {
6237
+ mutableBody[ANALYTICS_CLIENT_PLATFORM_BODY_FIELD] = clientPlatform;
6238
+ }
6239
+ }
6229
6240
  const requestBrowserTabId = normalizeBrowserTabId(browserTabId);
6230
6241
  const requestChatScope = normalizeChatScope(scope);
6231
6242
  const requestRunCtx = ensureRequestRunContext();
@@ -407,7 +407,11 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
407
407
  const internalContinuationReason = internalContinuationReasonForAttempt(attemptEvents);
408
408
  if (internalContinuationReason && !upstreamSignal.aborted) {
409
409
  lastAttemptWasUnfinishedContinuation = true;
410
- await appendContinuationAndJournal(opts.messages, internalContinuationReason, opts.threadId, opts.turnId, localTurnEvents, attemptEvents);
410
+ const continuationEvents = [...localTurnEvents];
411
+ if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, continuationEvents)) === "none") {
412
+ opts.send({ type: "clear" });
413
+ }
414
+ await appendContinuationAndJournal(opts.messages, internalContinuationReason, opts.threadId, opts.turnId, continuationEvents, attemptEvents);
411
415
  continue;
412
416
  }
413
417
  if (softTimedOut && !upstreamSignal.aborted) {
@@ -422,7 +426,12 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
422
426
  }
423
427
  catch (err) {
424
428
  if (softTimedOut && !upstreamSignal.aborted) {
429
+ // Clear partial text the client received before the abort so the
430
+ // resumed model doesn't re-emit it and produce duplicated output.
425
431
  lastAttemptWasUnfinishedContinuation = true;
432
+ if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, localTurnEvents)) === "none") {
433
+ opts.send({ type: "clear" });
434
+ }
426
435
  await appendContinuationAndJournal(opts.messages, "run_timeout", opts.threadId, opts.turnId, localTurnEvents, localTurnEvents.slice(attemptStartIndex));
427
436
  continue;
428
437
  }
@@ -439,6 +448,9 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
439
448
  rateLimitRetryFitsBudget) {
440
449
  lastAttemptWasUnfinishedContinuation = true;
441
450
  backgroundRateLimitContinuations++;
451
+ if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, localTurnEvents)) === "none") {
452
+ opts.send({ type: "clear" });
453
+ }
442
454
  await appendContinuationAndJournal(opts.messages, "rate_limited", opts.threadId, opts.turnId, localTurnEvents, localTurnEvents.slice(attemptStartIndex));
443
455
  await waitForBackgroundRateLimitCooldown(upstreamSignal);
444
456
  continue;
@@ -451,11 +463,16 @@ export async function runAgentLoopDirectWithSoftTimeout(opts, softTimeoutMs, tim
451
463
  // another LLM call. Anthropic's prompt cache makes the resume call much
452
464
  // faster than the cold first attempt.
453
465
  //
454
- // Keep streamed text visible while the resumed model finishes from the
455
- // internal checkpoint. The continuation prompt tells the model not to
456
- // repeat that prefix, so clearing it would leave only the suffix visible.
466
+ // Emit 'clear' so any partial streamed text is discarded on the client
467
+ // before the model resumes. Without this the model restarts its sentence
468
+ // from scratch and the fold produces duplicated text in one message
469
+ // (the partial text was already sent to the client but is now retained
470
+ // only as an internal checkpoint so the next attempt can finish it).
457
471
  if (!upstreamSignal.aborted && isResumableEngineError(err)) {
458
472
  lastAttemptWasUnfinishedContinuation = true;
473
+ if ((await completedSideEffectInCurrentTurn(opts.threadId, opts.turnId, localTurnEvents)) === "none") {
474
+ opts.send({ type: "clear" });
475
+ }
459
476
  await appendContinuationAndJournal(opts.messages, continuationReasonForResumableError(err), opts.threadId, opts.turnId, localTurnEvents, localTurnEvents.slice(attemptStartIndex));
460
477
  continue;
461
478
  }
@@ -38,6 +38,9 @@ export interface DefineAutomationInput {
38
38
  domain?: string;
39
39
  delegatedPolicyId?: string;
40
40
  model?: string;
41
+ executionHostId?: string;
42
+ executionEngine?: string;
43
+ executionCwd?: string;
41
44
  mcpTools?: unknown;
42
45
  delivery?: AutomationDelivery;
43
46
  }
@@ -52,6 +55,9 @@ export interface UpdateAutomationInput {
52
55
  schedule?: string;
53
56
  timezone?: string;
54
57
  model?: string | null;
58
+ executionHostId?: string | null;
59
+ executionEngine?: string | null;
60
+ executionCwd?: string | null;
55
61
  mcpTools?: unknown;
56
62
  }
57
63
  /**
@@ -9,6 +9,20 @@ export const DEFAULT_AUTOMATION_SCHEDULE = "0 * * * *";
9
9
  function httpError(message, statusCode) {
10
10
  return Object.assign(new Error(message), { statusCode });
11
11
  }
12
+ function normalizeExecutionTarget(value, label, options = {}) {
13
+ if (value === null || value === undefined)
14
+ return undefined;
15
+ const normalized = value.trim();
16
+ if (!normalized)
17
+ return undefined;
18
+ const max = options.max ?? (options.opaque ? 128 : 1024);
19
+ if (normalized.length > max ||
20
+ /[\r\n]/.test(normalized) ||
21
+ (options.opaque && !/^[a-z0-9][a-z0-9._:-]{0,127}$/i.test(normalized))) {
22
+ throw httpError(`${label} must be a bounded identifier${options.opaque ? " using letters, numbers, dots, underscores, colons, or hyphens" : ""}.`, 400);
23
+ }
24
+ return normalized;
25
+ }
12
26
  function normalizeActor(actor) {
13
27
  const userEmail = actor.userEmail.trim().toLowerCase();
14
28
  if (!userEmail)
@@ -204,6 +218,12 @@ export async function defineAutomation(actorInput, input) {
204
218
  ? input.timezone || (await resolveUserSchedulingTimezone(actor.userEmail))
205
219
  : undefined;
206
220
  const mcpTools = normalizeJobMcpTools(input.mcpTools);
221
+ const executionHostId = normalizeExecutionTarget(input.executionHostId, "execution_host_id", { opaque: true });
222
+ const executionEngine = normalizeExecutionTarget(input.executionEngine, "execution_engine", { opaque: true });
223
+ const executionCwd = normalizeExecutionTarget(input.executionCwd, "execution_cwd");
224
+ if (executionHostId && input.triggerType !== "schedule") {
225
+ throw httpError("Execution hosts are currently supported for scheduled code automations only.", 400);
226
+ }
207
227
  const meta = {
208
228
  schedule: input.triggerType === "schedule" ? schedule : "",
209
229
  timezone,
@@ -222,6 +242,9 @@ export async function defineAutomation(actorInput, input) {
222
242
  ? nextOccurrence(schedule, undefined, timezone).toISOString()
223
243
  : undefined,
224
244
  model: input.model?.trim() || undefined,
245
+ executionHostId,
246
+ executionEngine,
247
+ executionCwd,
225
248
  mcpTools: mcpTools?.length ? mcpTools : undefined,
226
249
  originScopeId: input.delivery?.originScopeId,
227
250
  deliveryPlatform: input.delivery?.platform,
@@ -283,6 +306,18 @@ export async function updateAutomation(actorInput, input) {
283
306
  if (input.model !== undefined) {
284
307
  meta.model = input.model?.trim() || undefined;
285
308
  }
309
+ if (input.executionHostId !== undefined) {
310
+ if (input.executionHostId && meta.triggerType !== "schedule") {
311
+ throw httpError("Execution hosts are currently supported for scheduled code automations only.", 400);
312
+ }
313
+ meta.executionHostId = normalizeExecutionTarget(input.executionHostId, "execution_host_id", { opaque: true });
314
+ }
315
+ if (input.executionEngine !== undefined) {
316
+ meta.executionEngine = normalizeExecutionTarget(input.executionEngine, "execution_engine", { opaque: true });
317
+ }
318
+ if (input.executionCwd !== undefined) {
319
+ meta.executionCwd = normalizeExecutionTarget(input.executionCwd, "execution_cwd");
320
+ }
286
321
  if (input.mcpTools !== undefined) {
287
322
  const mcpTools = normalizeJobMcpTools(input.mcpTools);
288
323
  meta.mcpTools = mcpTools?.length ? mcpTools : undefined;
@@ -1,9 +1,11 @@
1
- import type { RemoteComputerCapabilities } from "../integrations/remote-types.js";
1
+ import type { RemoteComputerCapabilities, RemoteExecutionCapabilities } from "../integrations/remote-types.js";
2
2
  export interface RemoteCodeAgentDeviceConfig {
3
3
  token: string;
4
4
  relayUrl?: string;
5
5
  deviceId?: string;
6
6
  deviceName?: string;
7
+ /** Default host-local workspace used when a command omits cwd. */
8
+ workspacePath?: string;
7
9
  pollIntervalMs?: number;
8
10
  }
9
11
  export interface RunCodeAgentConnectorOptions {
@@ -17,6 +19,7 @@ export interface LocalComputerBridgeConfig {
17
19
  token: string;
18
20
  capabilities: RemoteComputerCapabilities;
19
21
  }
22
+ export declare const DEFAULT_REMOTE_EXECUTION_CAPABILITIES: RemoteExecutionCapabilities;
20
23
  export declare function remoteDeviceConfigPath(): string;
21
24
  export declare function loadRemoteCodeAgentDeviceConfig(configPath?: string): RemoteCodeAgentDeviceConfig | null;
22
25
  export declare function runCodeAgentConnector(options?: RunCodeAgentConnectorOptions): Promise<number>;
@@ -6,14 +6,25 @@ import { assertValidComputerCommandEnvelope } from "../integrations/computer-sup
6
6
  import { serializeBoundedRemoteJson } from "../integrations/remote-json-safety.js";
7
7
  import { executeDenyCodeAgentApproval, executePendingCodeAgentApproval, } from "./code-agent-executor.js";
8
8
  import { appendCodeAgentTranscriptEvent, codeAgentRunTranscriptPath, codeAgentStoreRoot, createCodeAgentRunRecord, getCodeAgentRunRecord, listCodeAgentRunRecords, normalizeCodeAgentPermissionMode, queueCodeAgentFollowUp, updateCodeAgentRunRecord, } from "./code-agent-runs.js";
9
+ import { loadPortalEnvironment, preparePortalWorkspace, parsePortalHandoff, } from "./portal-workspace.js";
9
10
  const DEVICE_PATH_ENV = "AGENT_NATIVE_REMOTE_DEVICE_PATH";
10
11
  const COMPUTER_BRIDGE_URL_ENV = "AGENT_NATIVE_COMPUTER_BRIDGE_URL";
11
12
  const COMPUTER_BRIDGE_TOKEN_ENV = "AGENT_NATIVE_COMPUTER_BRIDGE_TOKEN";
12
13
  const COMPUTER_CAPABILITIES_ENV = "AGENT_NATIVE_COMPUTER_CAPABILITIES";
14
+ const REMOTE_ENGINES_ENV = "AGENT_NATIVE_REMOTE_ENGINES";
13
15
  const DEFAULT_POLL_INTERVAL_MS = 2_000;
14
16
  const MAX_POLL_INTERVAL_MS = 30_000;
15
17
  const MAX_TRANSCRIPT_EVENTS_PER_BATCH = 50;
16
18
  const activeRunners = new Map();
19
+ export const DEFAULT_REMOTE_EXECUTION_CAPABILITIES = {
20
+ backend: "desktop",
21
+ workloads: ["code-agent", "scheduled-code"],
22
+ engines: ["codex-cli", "claude-cli", "pi-cli", "opencode-cli"],
23
+ acceptsScheduledWork: true,
24
+ acceptsPortalHandoffs: true,
25
+ persistence: "local-files",
26
+ adapters: ["agent-native-code"],
27
+ };
17
28
  export function remoteDeviceConfigPath() {
18
29
  return path.resolve(process.env[DEVICE_PATH_ENV] ??
19
30
  path.join(os.homedir(), ".agent-native", "remote-device.json"));
@@ -32,6 +43,7 @@ export function loadRemoteCodeAgentDeviceConfig(configPath = remoteDeviceConfigP
32
43
  relayUrl: firstStringValue(raw.relayUrl, raw.url, raw.baseUrl),
33
44
  deviceId: firstStringValue(raw.deviceId, raw.id),
34
45
  deviceName: firstStringValue(raw.deviceName, raw.name),
46
+ workspacePath: firstStringValue(raw.workspacePath, raw.workspace, raw.cwd, raw.projectPath),
35
47
  pollIntervalMs: Number.isFinite(pollIntervalMs) && pollIntervalMs > 0
36
48
  ? Math.min(Math.max(pollIntervalMs, 500), MAX_POLL_INTERVAL_MS)
37
49
  : undefined,
@@ -77,6 +89,7 @@ class RemoteCodeAgentConnector {
77
89
  output;
78
90
  transcriptCursors = new Map();
79
91
  remoteRunIds = new Set();
92
+ terminalCommandIds = new Set();
80
93
  stopped = false;
81
94
  computerBridge = loadLocalComputerBridgeConfig();
82
95
  constructor(config, relayUrl, output) {
@@ -140,6 +153,7 @@ class RemoteCodeAgentConnector {
140
153
  "run-events",
141
154
  ],
142
155
  computerCapabilities: this.computerBridge?.capabilities ?? {},
156
+ executionCapabilities: remoteExecutionCapabilities(),
143
157
  activeRunIds: Array.from(this.remoteRunIds),
144
158
  });
145
159
  const commands = normalizeCommands(response);
@@ -180,25 +194,65 @@ class RemoteCodeAgentConnector {
180
194
  async computerOperation(command) {
181
195
  return dispatchComputerOperationToLocalBridge(this.computerBridge, command.params.envelope);
182
196
  }
183
- createRun(command) {
197
+ async createRun(command) {
184
198
  const prompt = firstTextValue(command.params.prompt, command.params.message, command.params.input);
185
199
  if (!prompt) {
186
200
  return { ok: false, error: "Missing prompt." };
187
201
  }
188
202
  const goalId = firstStringValue(command.params.goalId) ?? "task";
189
- const cwd = resolveCommandCwd(command.params.cwd);
203
+ const metadata = isObject(command.params.metadata)
204
+ ? command.params.metadata
205
+ : {};
206
+ let cwd = resolveCommandCwd(command.params.cwd ?? this.config.workspacePath);
207
+ let portalWorkspace;
208
+ let portalEnvironment;
209
+ if (metadata.portal !== undefined) {
210
+ try {
211
+ portalWorkspace = await preparePortalWorkspace({
212
+ handoff: parsePortalHandoff(metadata.portal),
213
+ workspacePath: this.config.workspacePath,
214
+ });
215
+ portalEnvironment = loadPortalEnvironment(portalWorkspace.environmentRoot);
216
+ cwd = portalWorkspace.workspacePath;
217
+ }
218
+ catch (error) {
219
+ return {
220
+ ok: false,
221
+ error: error instanceof Error ? error.message : String(error),
222
+ };
223
+ }
224
+ }
225
+ const requestedRunId = firstStringValue(command.params.runId);
226
+ if (portalWorkspace && requestedRunId && !isSafeRunId(requestedRunId)) {
227
+ return { ok: false, error: "Portal run id is invalid." };
228
+ }
229
+ if (portalWorkspace && requestedRunId) {
230
+ const existing = getCodeAgentRunRecord(requestedRunId);
231
+ const existingRemote = isObject(existing?.metadata?.remote)
232
+ ? existing.metadata.remote
233
+ : {};
234
+ if (existing &&
235
+ firstStringValue(existingRemote.commandId) === command.id) {
236
+ this.remoteRunIds.add(existing.id);
237
+ this.transcriptCursors.set(existing.id, { offset: 0, seq: 0 });
238
+ if (!activeRunners.has(existing.id)) {
239
+ this.spawnRunner(existing.id, existing.cwd, existing.permissionMode, portalEnvironment?.values);
240
+ }
241
+ return { ok: true, runId: existing.id, run: existing, resumed: true };
242
+ }
243
+ }
190
244
  const permissionMode = normalizeCodeAgentPermissionMode(command.params.permissionMode) ??
191
245
  "full-auto";
192
246
  const engine = firstStringValue(command.params.engine);
193
247
  const model = firstStringValue(command.params.model);
194
248
  const effort = firstStringValue(command.params.effort, command.params.reasoningEffort);
195
- const metadata = isObject(command.params.metadata)
196
- ? command.params.metadata
197
- : {};
198
249
  const run = createCodeAgentRunRecord({
250
+ ...(portalWorkspace && requestedRunId ? { id: requestedRunId } : {}),
199
251
  goalId,
200
252
  title: firstStringValue(command.params.title) ?? titleFromPrompt(prompt),
201
- subtitle: "Remote coding task",
253
+ subtitle: portalWorkspace
254
+ ? `Portal on ${this.config.deviceName ?? "paired computer"}`
255
+ : "Remote coding task",
202
256
  status: "queued",
203
257
  phase: "queued",
204
258
  permissionMode,
@@ -213,6 +267,20 @@ class RemoteCodeAgentConnector {
213
267
  { label: "Prompt", value: truncateForDisplay(prompt, 160) },
214
268
  { label: "Agent", value: "Remote connector" },
215
269
  { label: "Mode", value: permissionMode },
270
+ ...(portalWorkspace
271
+ ? [
272
+ {
273
+ label: "Workspace",
274
+ value: `Portal · ${portalWorkspace.workspacePath}`,
275
+ },
276
+ {
277
+ label: "Environment",
278
+ value: portalEnvironment?.files.length
279
+ ? `Loaded ${portalEnvironment.files.join(", ")}`
280
+ : "Using paired computer environment",
281
+ },
282
+ ]
283
+ : []),
216
284
  ],
217
285
  metadata: {
218
286
  ...metadata,
@@ -225,7 +293,26 @@ class RemoteCodeAgentConnector {
225
293
  commandId: command.id,
226
294
  deviceId: this.config.deviceId,
227
295
  relayUrl: this.relayUrl,
296
+ ...(portalWorkspace ? { remoteRunId: run.id } : {}),
228
297
  },
298
+ ...(portalWorkspace
299
+ ? {
300
+ portal: portalWorkspace.handoff,
301
+ executionResidence: {
302
+ schemaVersion: 1,
303
+ kind: "portal",
304
+ hostId: this.config.deviceId,
305
+ hostLabel: this.config.deviceName ?? os.hostname(),
306
+ workspacePath: portalWorkspace.workspacePath,
307
+ sourceBranch: portalWorkspace.handoff.sourceBranch,
308
+ sourceCommit: portalWorkspace.handoff.commit,
309
+ handoffId: portalWorkspace.handoff.handoffId,
310
+ envPolicy: portalWorkspace.handoff.envPolicy,
311
+ environmentFiles: portalEnvironment?.files ?? [],
312
+ environmentLoadedAt: new Date().toISOString(),
313
+ },
314
+ }
315
+ : {}),
229
316
  },
230
317
  });
231
318
  appendCodeAgentTranscriptEvent({
@@ -242,7 +329,7 @@ class RemoteCodeAgentConnector {
242
329
  });
243
330
  this.remoteRunIds.add(run.id);
244
331
  this.transcriptCursors.set(run.id, { offset: 0, seq: 0 });
245
- this.spawnRunner(run.id, cwd, permissionMode);
332
+ this.spawnRunner(run.id, cwd, permissionMode, portalEnvironment?.values);
246
333
  return { ok: true, runId: run.id, run };
247
334
  }
248
335
  appendFollowUp(command) {
@@ -424,7 +511,7 @@ class RemoteCodeAgentConnector {
424
511
  activeRunIds: Array.from(activeRunners.keys()),
425
512
  };
426
513
  }
427
- spawnRunner(runId, cwd, permissionMode) {
514
+ spawnRunner(runId, cwd, permissionMode, portalEnvironment) {
428
515
  if (activeRunners.has(runId))
429
516
  return;
430
517
  const invocation = resolveCodeAgentCliInvocation();
@@ -434,6 +521,7 @@ class RemoteCodeAgentConnector {
434
521
  stdio: ["ignore", "pipe", "pipe"],
435
522
  env: {
436
523
  ...process.env,
524
+ ...portalEnvironment,
437
525
  AGENT_NATIVE_CODE_AGENTS_HOME: codeAgentStoreRoot(),
438
526
  ...(permissionMode
439
527
  ? { AGENT_NATIVE_CODE_AGENT_PERMISSION_MODE: permissionMode }
@@ -484,28 +572,50 @@ class RemoteCodeAgentConnector {
484
572
  for (const runId of this.remoteRunIds) {
485
573
  const cursor = this.transcriptCursors.get(runId) ?? { offset: 0, seq: 0 };
486
574
  const batch = readTranscriptBatch(runId, cursor.offset);
487
- if (batch.events.length === 0) {
488
- this.transcriptCursors.set(runId, {
489
- offset: batch.nextOffset,
490
- seq: cursor.seq,
575
+ if (batch.events.length > 0) {
576
+ await this.postJson("/_agent-native/integrations/remote/run-events", {
577
+ deviceId: this.config.deviceId,
578
+ remoteRunId: runId,
579
+ cursor: { offset: batch.nextOffset },
580
+ events: batch.events.map((event, index) => ({
581
+ seq: cursor.seq + index,
582
+ event,
583
+ })),
491
584
  });
492
- continue;
493
585
  }
494
- await this.postJson("/_agent-native/integrations/remote/run-events", {
495
- deviceId: this.config.deviceId,
496
- remoteRunId: runId,
497
- cursor: { offset: batch.nextOffset },
498
- events: batch.events.map((event, index) => ({
499
- seq: cursor.seq + index,
500
- event,
501
- })),
502
- });
503
586
  this.transcriptCursors.set(runId, {
504
587
  offset: batch.nextOffset,
505
588
  seq: cursor.seq + batch.events.length,
506
589
  });
590
+ await this.reportRemoteRunTerminal(runId);
507
591
  }
508
592
  }
593
+ async reportRemoteRunTerminal(runId) {
594
+ const run = getCodeAgentRunRecord(runId);
595
+ if (!run || (run.status !== "completed" && run.status !== "errored")) {
596
+ return;
597
+ }
598
+ const remote = isObject(run.metadata?.remote) ? run.metadata.remote : {};
599
+ const commandId = firstStringValue(remote.commandId);
600
+ if (!commandId || this.terminalCommandIds.has(commandId))
601
+ return;
602
+ const errorMessage = firstStringValue(run.metadata?.error, run.metadata?.runnerError);
603
+ await this.postJson("/_agent-native/integrations/remote/result", {
604
+ deviceId: this.config.deviceId,
605
+ commandId,
606
+ kind: "create-run",
607
+ ok: run.status === "completed",
608
+ status: run.status === "completed" ? "completed" : "failed",
609
+ result: {
610
+ ok: run.status === "completed",
611
+ run,
612
+ },
613
+ errorMessage: run.status === "errored"
614
+ ? (errorMessage ?? "Remote code-agent run failed.")
615
+ : undefined,
616
+ });
617
+ this.terminalCommandIds.add(commandId);
618
+ }
509
619
  async postCommandResult(command, result) {
510
620
  let safeResult;
511
621
  try {
@@ -519,12 +629,17 @@ class RemoteCodeAgentConnector {
519
629
  : "Connector result rejected as unsafe.",
520
630
  };
521
631
  }
632
+ const isAcceptedCreateRun = normalizeKind(command.kind) === "create-run" && safeResult.ok !== false;
522
633
  await this.postJson("/_agent-native/integrations/remote/result", {
523
634
  deviceId: this.config.deviceId,
524
635
  commandId: command.id,
525
636
  kind: command.kind,
526
637
  ok: safeResult.ok !== false,
527
- status: safeResult.ok === false ? "failed" : "completed",
638
+ status: safeResult.ok === false
639
+ ? "failed"
640
+ : isAcceptedCreateRun
641
+ ? "running"
642
+ : "completed",
528
643
  result: safeResult,
529
644
  errorMessage: typeof safeResult.error === "string" ? safeResult.error : undefined,
530
645
  });
@@ -535,6 +650,7 @@ class RemoteCodeAgentConnector {
535
650
  method: "POST",
536
651
  headers: {
537
652
  authorization: `Bearer ${this.config.token}`,
653
+ "x-agent-native-device-token": this.config.token,
538
654
  "content-type": "application/json",
539
655
  },
540
656
  body: JSON.stringify(body),
@@ -712,6 +828,21 @@ function resolveCommandCwd(value) {
712
828
  const cwd = firstStringValue(value);
713
829
  return cwd ? path.resolve(cwd) : process.cwd();
714
830
  }
831
+ function isSafeRunId(value) {
832
+ return value.length <= 160 && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value);
833
+ }
834
+ function remoteExecutionCapabilities() {
835
+ const configured = process.env[REMOTE_ENGINES_ENV]
836
+ ?.split(",")
837
+ .map((engine) => engine.trim())
838
+ .filter(Boolean);
839
+ return {
840
+ ...DEFAULT_REMOTE_EXECUTION_CAPABILITIES,
841
+ ...(configured?.length
842
+ ? { engines: [...new Set(configured)].slice(0, 32) }
843
+ : {}),
844
+ };
845
+ }
715
846
  function normalizeRelayUrl(value) {
716
847
  if (!value)
717
848
  return null;