@agent-native/core 0.146.2 → 0.146.4

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 (213) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +13 -10
  3. package/corpus/templates/analytics/.agents/skills/self-modifying-code/SKILL.md +7 -3
  4. package/corpus/templates/analytics/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  5. package/corpus/templates/analytics/actions/compose-dashboard.ts +53 -6
  6. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +8 -2
  7. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +3 -1
  8. package/corpus/templates/analytics/actions/mutate-dashboard.ts +48 -9
  9. package/corpus/templates/analytics/actions/update-dashboard.ts +80 -6
  10. package/corpus/templates/analytics/changelog/2026-08-07-dashboard-creation-and-edits-now-save-faster-with-reliable-s.md +6 -0
  11. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +19 -19
  12. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +21 -2
  13. package/corpus/templates/analytics/server/lib/bigquery.ts +59 -24
  14. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +44 -8
  15. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +3 -5
  16. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +33 -69
  17. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +90 -24
  18. package/corpus/templates/analytics/server/lib/real-data-actions.ts +1 -1
  19. package/corpus/templates/analytics/server/plugins/agent-chat.ts +17 -2
  20. package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +2 -0
  21. package/corpus/templates/assets/.agents/skills/self-modifying-code/SKILL.md +7 -3
  22. package/corpus/templates/assets/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  23. package/corpus/templates/assets/netlify.toml +1 -1
  24. package/corpus/templates/assets/package.json +1 -0
  25. package/corpus/templates/assets/scripts/migrate-production.ts +24 -0
  26. package/corpus/templates/assets/server/plugins/db.ts +1 -1
  27. package/corpus/templates/assets/tsconfig.json +1 -0
  28. package/corpus/templates/brain/.agents/skills/self-modifying-code/SKILL.md +7 -3
  29. package/corpus/templates/brain/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  30. package/corpus/templates/brain/netlify.toml +1 -1
  31. package/corpus/templates/brain/package.json +2 -0
  32. package/corpus/templates/brain/scripts/migrate-production.ts +24 -0
  33. package/corpus/templates/brain/server/plugins/db.ts +1 -1
  34. package/corpus/templates/brain/tsconfig.json +1 -0
  35. package/corpus/templates/calendar/.agents/skills/self-modifying-code/SKILL.md +7 -3
  36. package/corpus/templates/calendar/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  37. package/corpus/templates/calendar/netlify.toml +1 -1
  38. package/corpus/templates/calendar/package.json +1 -0
  39. package/corpus/templates/calendar/scripts/migrate-production.ts +24 -0
  40. package/corpus/templates/calendar/server/plugins/db.ts +1 -1
  41. package/corpus/templates/calendar/tsconfig.json +1 -0
  42. package/corpus/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
  43. package/corpus/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  44. package/corpus/templates/chat/netlify.toml +1 -1
  45. package/corpus/templates/chat/package.json +1 -0
  46. package/corpus/templates/chat/scripts/migrate-production.ts +31 -0
  47. package/corpus/templates/chat/tsconfig.json +1 -0
  48. package/corpus/templates/clips/.agents/skills/self-modifying-code/SKILL.md +7 -3
  49. package/corpus/templates/clips/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  50. package/corpus/templates/clips/netlify.toml +1 -1
  51. package/corpus/templates/clips/package.json +1 -0
  52. package/corpus/templates/clips/scripts/migrate-production.ts +24 -0
  53. package/corpus/templates/clips/server/plugins/db.ts +1 -1
  54. package/corpus/templates/clips/tsconfig.json +1 -0
  55. package/corpus/templates/content/.agents/skills/self-modifying-code/SKILL.md +7 -3
  56. package/corpus/templates/content/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  57. package/corpus/templates/content/netlify.toml +1 -1
  58. package/corpus/templates/content/package.json +2 -0
  59. package/corpus/templates/content/scripts/migrate-production.ts +28 -0
  60. package/corpus/templates/content/server/plugins/db.ts +2 -2
  61. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  62. package/corpus/templates/crm/netlify.toml +1 -1
  63. package/corpus/templates/crm/package.json +2 -0
  64. package/corpus/templates/crm/scripts/migrate-production.ts +24 -0
  65. package/corpus/templates/crm/server/plugins/db.ts +1 -1
  66. package/corpus/templates/crm/tsconfig.json +1 -0
  67. package/corpus/templates/design/.agents/skills/self-modifying-code/SKILL.md +7 -3
  68. package/corpus/templates/design/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  69. package/corpus/templates/design/netlify.toml +1 -1
  70. package/corpus/templates/design/package.json +1 -0
  71. package/corpus/templates/design/scripts/migrate-production.ts +24 -0
  72. package/corpus/templates/design/server/plugins/db.ts +1 -1
  73. package/corpus/templates/design/tsconfig.json +1 -0
  74. package/corpus/templates/dispatch/.agents/skills/self-modifying-code/SKILL.md +7 -3
  75. package/corpus/templates/dispatch/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  76. package/corpus/templates/dispatch/netlify.toml +1 -1
  77. package/corpus/templates/dispatch/package.json +1 -0
  78. package/corpus/templates/dispatch/scripts/migrate-production.ts +23 -0
  79. package/corpus/templates/dispatch/tsconfig.json +1 -0
  80. package/corpus/templates/factory/.agents/skills/self-modifying-code/SKILL.md +7 -3
  81. package/corpus/templates/factory/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  82. package/corpus/templates/factory/netlify.toml +1 -1
  83. package/corpus/templates/factory/package.json +1 -0
  84. package/corpus/templates/factory/scripts/migrate-production.ts +31 -0
  85. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +2 -0
  86. package/corpus/templates/factory/tsconfig.json +1 -0
  87. package/corpus/templates/forms/.agents/skills/self-modifying-code/SKILL.md +7 -3
  88. package/corpus/templates/forms/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  89. package/corpus/templates/forms/netlify.toml +1 -1
  90. package/corpus/templates/forms/package.json +2 -0
  91. package/corpus/templates/forms/scripts/migrate-production.ts +24 -0
  92. package/corpus/templates/forms/server/plugins/db.ts +1 -1
  93. package/corpus/templates/forms/tsconfig.json +1 -0
  94. package/corpus/templates/macros/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  95. package/corpus/templates/macros/netlify.toml +1 -1
  96. package/corpus/templates/macros/package.json +1 -0
  97. package/corpus/templates/macros/scripts/migrate-production.ts +24 -0
  98. package/corpus/templates/macros/server/plugins/db.ts +1 -1
  99. package/corpus/templates/macros/tsconfig.json +1 -0
  100. package/corpus/templates/mail/.agents/skills/self-modifying-code/SKILL.md +7 -3
  101. package/corpus/templates/mail/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  102. package/corpus/templates/mail/netlify.toml +1 -1
  103. package/corpus/templates/mail/package.json +1 -0
  104. package/corpus/templates/mail/scripts/migrate-production.ts +24 -0
  105. package/corpus/templates/mail/server/plugins/db.ts +1 -1
  106. package/corpus/templates/mail/tsconfig.json +1 -0
  107. package/corpus/templates/plan/.agents/skills/self-modifying-code/SKILL.md +7 -3
  108. package/corpus/templates/plan/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  109. package/corpus/templates/plan/netlify.toml +1 -1
  110. package/corpus/templates/plan/package.json +1 -0
  111. package/corpus/templates/plan/scripts/migrate-production.ts +24 -0
  112. package/corpus/templates/plan/server/plugins/00-mcp.ts +3 -2
  113. package/corpus/templates/plan/server/plugins/db.ts +1 -1
  114. package/corpus/templates/plan/tsconfig.json +1 -0
  115. package/corpus/templates/slides/.agents/skills/self-modifying-code/SKILL.md +7 -3
  116. package/corpus/templates/slides/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  117. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +56 -23
  118. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -0
  119. package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +4 -1
  120. package/corpus/templates/slides/app/context/DeckContext.tsx +91 -25
  121. package/corpus/templates/slides/app/hooks/use-deck-role.ts +14 -4
  122. package/corpus/templates/slides/app/i18n/en-US.ts +2 -0
  123. package/corpus/templates/slides/app/pages/DeckEditor.tsx +19 -7
  124. package/corpus/templates/slides/app/pages/Index.tsx +11 -2
  125. package/corpus/templates/slides/app/root.tsx +5 -1
  126. package/corpus/templates/slides/netlify.toml +1 -1
  127. package/corpus/templates/slides/package.json +1 -0
  128. package/corpus/templates/slides/scripts/migrate-production.ts +24 -0
  129. package/corpus/templates/slides/server/plugins/db.ts +1 -1
  130. package/corpus/templates/slides/tsconfig.json +1 -0
  131. package/corpus/templates/tasks/.agents/skills/self-modifying-code/SKILL.md +7 -3
  132. package/corpus/templates/tasks/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  133. package/corpus/templates/tasks/netlify.toml +1 -1
  134. package/corpus/templates/tasks/package.json +1 -0
  135. package/corpus/templates/tasks/scripts/migrate-production.ts +24 -0
  136. package/corpus/templates/tasks/server/plugins/db.ts +1 -1
  137. package/corpus/templates/tasks/tsconfig.json +1 -0
  138. package/dist/action.d.ts +2 -0
  139. package/dist/agent/production-agent.d.ts +1 -0
  140. package/dist/agent/production-agent.js +2 -0
  141. package/dist/agent/thread-data-builder.d.ts +4 -0
  142. package/dist/agent/thread-data-builder.js +44 -0
  143. package/dist/automations/service.d.ts +1 -0
  144. package/dist/automations/service.js +21 -2
  145. package/dist/cli/create.js +20 -2
  146. package/dist/client/agent-page/AgentJobsTab.js +3 -1
  147. package/dist/client/agent-page/AutomationDetailsDialog.js +9 -2
  148. package/dist/collab/awareness.d.ts +2 -2
  149. package/dist/collab/struct-routes.d.ts +1 -1
  150. package/dist/db/migrations.js +7 -13
  151. package/dist/jobs/actions/list-automation-runs.d.ts +1 -0
  152. package/dist/jobs/actions/list-automation-runs.js +9 -2
  153. package/dist/jobs/actions/list-recurring-jobs.js +3 -1
  154. package/dist/jobs/actions/manage-recurring-job.js +1 -1
  155. package/dist/jobs/actions/run-automation-now.js +1 -0
  156. package/dist/jobs/background-automation-runner.js +2 -0
  157. package/dist/jobs/frontmatter.d.ts +2 -0
  158. package/dist/jobs/frontmatter.js +4 -0
  159. package/dist/jobs/run-history.d.ts +4 -0
  160. package/dist/jobs/run-history.js +28 -8
  161. package/dist/jobs/run-now.d.ts +4 -1
  162. package/dist/jobs/run-now.js +4 -3
  163. package/dist/jobs/scheduler.js +18 -0
  164. package/dist/jobs/tools.d.ts +3 -2
  165. package/dist/jobs/tools.js +26 -12
  166. package/dist/localization/default-messages.d.ts +1 -0
  167. package/dist/localization/default-messages.js +1 -0
  168. package/dist/observability/routes.d.ts +3 -3
  169. package/dist/secrets/routes.d.ts +6 -6
  170. package/dist/server/action-routes.d.ts +2 -0
  171. package/dist/server/action-routes.js +1 -0
  172. package/dist/server/agent-chat-plugin.js +3 -2
  173. package/dist/server/builder-browser.js +24 -8
  174. package/dist/server/transcribe-voice.d.ts +1 -1
  175. package/dist/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
  176. package/dist/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  177. package/dist/templates/chat/netlify.toml +1 -1
  178. package/dist/templates/chat/package.json +1 -0
  179. package/dist/templates/chat/scripts/migrate-production.ts +31 -0
  180. package/dist/templates/chat/tsconfig.json +1 -0
  181. package/dist/templates/default/.agents/skills/self-modifying-code/SKILL.md +7 -3
  182. package/dist/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  183. package/dist/templates/default/package.json +1 -0
  184. package/dist/templates/default/scripts/migrate-production.ts +31 -0
  185. package/dist/templates/default/tsconfig.json +1 -0
  186. package/dist/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  187. package/dist/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +7 -3
  188. package/dist/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  189. package/dist/templates/workspace-root/AGENTS.md +2 -1
  190. package/dist/triggers/actions/list-automations.js +1 -1
  191. package/dist/triggers/actions/manage-automation.js +5 -1
  192. package/dist/triggers/actions.d.ts +1 -1
  193. package/dist/triggers/actions.js +21 -15
  194. package/dist/triggers/dispatcher.js +17 -2
  195. package/dist/triggers/routes.d.ts +1 -0
  196. package/dist/triggers/routes.js +1 -0
  197. package/docs/content/docs-components.mdx +122 -11
  198. package/package.json +1 -1
  199. package/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
  200. package/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  201. package/src/templates/chat/netlify.toml +1 -1
  202. package/src/templates/chat/package.json +1 -0
  203. package/src/templates/chat/scripts/migrate-production.ts +31 -0
  204. package/src/templates/chat/tsconfig.json +1 -0
  205. package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +7 -3
  206. package/src/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  207. package/src/templates/default/package.json +1 -0
  208. package/src/templates/default/scripts/migrate-production.ts +31 -0
  209. package/src/templates/default/tsconfig.json +1 -0
  210. package/src/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  211. package/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +7 -3
  212. package/src/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  213. package/src/templates/workspace-root/AGENTS.md +2 -1
@@ -2593,7 +2593,11 @@ function rewriteNetlifyToml(appDir, appName, mode) {
2593
2593
  return;
2594
2594
  try {
2595
2595
  let content = fs.readFileSync(netlifyPath, "utf-8");
2596
- const originalCommand = content.match(/^\s*command = "([^"]*)"$/m)?.[1];
2596
+ // Tolerate escaped quotes inside the command. Every template's build
2597
+ // command now contains `\"` (the release-migration step's CONTEXT test),
2598
+ // and a naive [^"]* stops at the first one — which silently dropped the
2599
+ // NETLIFY_DATABASE_URL_UNPOOLED override for the four templates that use it.
2600
+ const originalCommand = content.match(/^\s*command = "((?:[^"\\]|\\.)*)"$/m)?.[1];
2597
2601
  const usesUnpooledDatabase = originalCommand?.includes("NETLIFY_DATABASE_URL_UNPOOLED") ?? false;
2598
2602
  const buildCommand = mode === "workspace"
2599
2603
  ? `APP_BASE_PATH=/${appName} VITE_APP_BASE_PATH=/${appName} NITRO_PRESET=netlify pnpm --filter ${appName} build`
@@ -2602,7 +2606,21 @@ function rewriteNetlifyToml(appDir, appName, mode) {
2602
2606
  const buildDatabasePrefix = usesUnpooledDatabase
2603
2607
  ? 'DATABASE_URL=\\"${NETLIFY_DATABASE_URL_UNPOOLED:-$DATABASE_URL}\\" '
2604
2608
  : "";
2605
- const command = `${databaseSetup} && ${buildDatabasePrefix}${buildCommand}`;
2609
+ const releaseDatabasePrefix = usesUnpooledDatabase
2610
+ ? 'DATABASE_URL=\\"${NETLIFY_DATABASE_URL_UNPOOLED:-$DATABASE_URL}\\" '
2611
+ : "";
2612
+ // Migrate at RELEASE, never on the request path. On serverless "migrate on
2613
+ // first use" means migrate on every cold start; a production incident
2614
+ // traced a multi-hour outage to schema introspection running concurrently
2615
+ // on requests. Generated for every app so a fresh `create` + Netlify
2616
+ // connect just works, with no flag to remember.
2617
+ const releaseMigrations = ' && if [ \\"${CONTEXT:-}\\" = \\"production\\" ]; then ' +
2618
+ releaseDatabasePrefix +
2619
+ (mode === "workspace"
2620
+ ? `pnpm --filter ${appName} migrate:production`
2621
+ : "pnpm migrate:production") +
2622
+ "; fi";
2623
+ const command = `${databaseSetup} && ${buildDatabasePrefix}${buildCommand}${releaseMigrations}`;
2606
2624
  const publishPath = mode === "workspace" ? `apps/${appName}/dist` : "dist";
2607
2625
  const functionsPath = mode === "workspace"
2608
2626
  ? `apps/${appName}/.netlify/functions-internal`
@@ -213,7 +213,9 @@ export function AgentJobsTab({ canManageOrg = false, hideHeader = false, }) {
213
213
  : t("jobs.paused", { defaultValue: "Paused" }) }), resource.lastStatus ? (_jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground", children: resource.lastStatus })) : null] }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: triggerDescription }), _jsx("p", { className: "hidden", children: instructions }), lastRun || nextRun || lastCheck ? (_jsxs("div", { className: "hidden", children: [nextRun ? (_jsxs("span", { children: [t("jobs.nextRun", { defaultValue: "Next run" }), ":", " ", nextRun] })) : null, _jsxs("span", { children: [t("jobs.lastRun", { defaultValue: "Last run" }), ":", " ", lastRun ??
214
214
  t("jobs.neverRan", { defaultValue: "Never" })] }), !lastRun && lastCheck ? (_jsxs("span", { children: [t("jobs.lastChecked", {
215
215
  defaultValue: "Last checked",
216
- }), ": ", lastCheck] })) : null] })) : null, resource.lastError ? (_jsxs("p", { className: "mt-2 flex items-start gap-1.5 text-[11px] text-destructive", children: [_jsx(IconAlertTriangle, { className: "mt-px size-3 shrink-0" }), _jsx("span", { className: "min-w-0 break-words", children: resource.lastError })] })) : null] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [resource.canUpdate ? (_jsx("button", { type: "button", role: "switch", "aria-checked": resource.enabled, "aria-label": resource.enabled
216
+ }), ": ", lastCheck] })) : null] })) : null, resource.lastError ? (_jsxs("div", { className: "mt-2 flex items-start gap-1.5 text-[11px] text-destructive", children: [_jsx(IconAlertTriangle, { className: "mt-px size-3 shrink-0" }), _jsx("span", { className: "min-w-0 break-words", children: resource.lastError }), _jsx("button", { type: "button", className: "shrink-0 font-medium text-foreground underline-offset-4 hover:underline", onClick: () => setDetailsTarget(entry), children: t("jobs.viewDetails", {
217
+ defaultValue: "View details",
218
+ }) })] })) : null] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [resource.canUpdate ? (_jsx("button", { type: "button", role: "switch", "aria-checked": resource.enabled, "aria-label": resource.enabled
217
219
  ? "Pause automation"
218
220
  : "Resume automation", disabled: mutationPending, onClick: () => mutateEntry(entry, "update", {
219
221
  enabled: !resource.enabled,
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { IconAlertTriangle, IconLoader2 } from "@tabler/icons-react";
2
+ import { Button } from "@agent-native/toolkit/ui/button";
3
+ import { IconAlertTriangle, IconExternalLink, IconLoader2, } from "@tabler/icons-react";
4
+ import { requestAgentChatThreadOpen } from "../agent-chat.js";
3
5
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "../components/ui/dialog.js";
4
6
  import { useT } from "../i18n.js";
5
7
  import { useAutomationRuns } from "./use-jobs.js";
@@ -28,5 +30,10 @@ export function AutomationDetailsDialog({ open, name, triggerSummary, fields, co
28
30
  defaultValue: "Could not load run history.",
29
31
  }) })) : runs.length === 0 ? (_jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t("jobs.noRuns", {
30
32
  defaultValue: "This automation has not executed yet. Only real executions are recorded here.",
31
- }) })) : (_jsx("ul", { className: "mt-2 divide-y divide-border/60 border-y border-border/60", children: runs.map((run) => (_jsxs("li", { className: "flex items-center gap-2 py-1.5 text-xs", children: [_jsx(RunStatusDot, { status: run.status }), _jsx("span", { className: "shrink-0 text-muted-foreground", children: formatTimestamp(run.startedAt) }), _jsx("span", { className: "shrink-0 font-medium", children: run.status }), run.error ? (_jsx("span", { className: "min-w-0 flex-1 truncate text-muted-foreground", title: run.error, children: run.error })) : (_jsx("span", { className: "min-w-0 flex-1" })), run.finishedAt ? (_jsxs("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: [Math.round((run.finishedAt - run.startedAt) / 1000), "s"] })) : null] }, run.id))) }))] })] })] }) }));
33
+ }) })) : (_jsx("ul", { className: "mt-2 divide-y divide-border/60 border-y border-border/60", children: runs.map((run) => (_jsxs("li", { className: "flex items-center gap-2 py-1.5 text-xs", children: [_jsx(RunStatusDot, { status: run.status }), _jsx("span", { className: "shrink-0 text-muted-foreground", children: formatTimestamp(run.startedAt) }), _jsx("span", { className: "shrink-0 font-medium", children: run.status }), run.error ? (_jsx("span", { className: "min-w-0 flex-1 truncate text-muted-foreground", title: run.error, children: run.error })) : (_jsx("span", { className: "min-w-0 flex-1" })), run.finishedAt ? (_jsxs("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: [Math.round((run.finishedAt - run.startedAt) / 1000), "s"] })) : null, run.error && run.threadId ? (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-6 shrink-0 gap-1 px-1.5 text-[11px]", onClick: () => {
34
+ requestAgentChatThreadOpen({
35
+ threadId: run.threadId,
36
+ });
37
+ onClose();
38
+ }, children: [_jsx(IconExternalLink, { className: "size-3" }), "Open thread"] })) : null] }, run.id))) }))] })] })] }) }));
32
39
  }
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  states: {
66
67
  clientId: number;
67
68
  state: string;
68
69
  }[];
69
- error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
+ error?: undefined;
80
81
  users: {
81
82
  clientId: number;
82
83
  lastSeen: number;
83
84
  }[];
84
- error?: undefined;
85
85
  }>>;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -245,19 +245,8 @@ function isServerlessRequestRuntime() {
245
245
  process.env.VERCEL === "1");
246
246
  }
247
247
  /**
248
- * Whether this app migrates at RELEASE time, so skipping on the request path
249
- * is safe.
250
- *
251
- * Without this, "skip in serverless" silently means "never migrate" for any app
252
- * that has no release entrypoint — and at the time this was written exactly one
253
- * of seventeen templates had one. Existing tables keep working, so nothing
254
- * fails; a newly added migration just never applies, and a fresh deploy comes
255
- * up with missing tables. That is a skip indistinguishable from success, which
256
- * is the failure mode this repo bans.
257
- *
258
- * So the request-path skip is opt-in per app. An app sets this once it owns a
259
- * release migration step (see templates/analytics/scripts/migrate-production.ts
260
- * and the `migrate:production` build step in its netlify.toml).
248
+ * Whether this deployment has a release-time migration runner. Request-path
249
+ * migration skipping is safe only when that runner owns schema setup.
261
250
  */
262
251
  function appMigratesAtRelease() {
263
252
  const raw = process.env.AGENT_NATIVE_RELEASE_MIGRATIONS?.trim();
@@ -675,6 +664,11 @@ export function runMigrations(migrations, options) {
675
664
  !!globalThis.process?.env?.VERCEL ||
676
665
  "__cf_env" in globalThis ||
677
666
  "__env__" in globalThis;
667
+ // A release migration runs in the same Netlify environment as a request,
668
+ // but it must fail the deploy when DDL fails instead of publishing an
669
+ // app against an incomplete schema.
670
+ if (isMigrationAuthorizedRuntime())
671
+ throw err;
678
672
  if (typeof globalThis.process?.exit === "function" && !isServerless) {
679
673
  process.exit(1);
680
674
  }
@@ -2,6 +2,7 @@ import { type AutomationRun } from "../run-history.js";
2
2
  declare const _default: import("../../action.js").ActionDefinition<{
3
3
  name: string;
4
4
  scope?: "organization" | "personal";
5
+ appId?: string;
5
6
  limit?: number;
6
7
  }, AutomationRun[]>;
7
8
  export default _default;
@@ -9,12 +9,13 @@ export default defineAction({
9
9
  schema: z.object({
10
10
  name: z.string().min(1),
11
11
  scope: scopeSchema.default("personal"),
12
+ appId: z.string().trim().min(1).optional(),
12
13
  limit: z.number().int().min(1).max(100).optional(),
13
14
  }),
14
15
  http: { method: "GET" },
15
16
  readOnly: true,
16
17
  parallelSafe: true,
17
- run: async ({ name, scope, limit }, ctx) => {
18
+ run: async ({ name, scope, appId: requestedAppId, limit }, ctx) => {
18
19
  const userEmail = ctx?.userEmail;
19
20
  if (!userEmail)
20
21
  throw new Error("Not authenticated.");
@@ -23,6 +24,12 @@ export default defineAction({
23
24
  const owner = scope === "organization"
24
25
  ? organizationResourceOwner(ctx.orgId)
25
26
  : userEmail;
26
- return listAutomationRuns({ owners: [owner], automation: name, limit });
27
+ const appId = ctx?.appId === "dispatch" ? requestedAppId : ctx?.appId;
28
+ return listAutomationRuns({
29
+ owners: [owner],
30
+ automation: name,
31
+ appId,
32
+ limit,
33
+ });
27
34
  },
28
35
  });
@@ -4,7 +4,7 @@ import { organizationResourceOwner, resourceGetByPath, resourceList, } from "../
4
4
  import { describeCron, effectiveTimezone, isValidCron, nextOccurrence, } from "../cron.js";
5
5
  import { classifyJobResource } from "../frontmatter.js";
6
6
  import { parseJobFrontmatter } from "../scheduler.js";
7
- import { authorizeJobMutation } from "../tools.js";
7
+ import { authorizeJobMutation, jobBelongsToApp } from "../tools.js";
8
8
  const scopeSchema = z.enum(["personal", "organization"]);
9
9
  function jobName(path) {
10
10
  return path.replace(/^jobs\//, "").replace(/\.md$/, "");
@@ -57,6 +57,8 @@ export default defineAction({
57
57
  continue;
58
58
  }
59
59
  const { meta, body } = parseJobFrontmatter(full.content);
60
+ if (!jobBelongsToApp(meta, ctx?.appId))
61
+ continue;
60
62
  const canUpdate = scope === "personal" || !(await authorizeJobMutation(owner, meta));
61
63
  jobs.push({
62
64
  id: full.id,
@@ -41,7 +41,7 @@ export default defineAction({
41
41
  statusCode: 400,
42
42
  });
43
43
  }
44
- const denied = await authorizeJobMutation(resource.owner, meta);
44
+ const denied = await authorizeJobMutation(resource.owner, meta, ctx.appId);
45
45
  if (denied)
46
46
  throw Object.assign(new Error(denied), { statusCode: 403 });
47
47
  if (operation === "delete") {
@@ -14,6 +14,7 @@ export default defineAction({
14
14
  return queueAutomationRunNow({
15
15
  userEmail: ctx.userEmail,
16
16
  orgId: ctx.orgId,
17
+ appId: ctx.appId,
17
18
  scope,
18
19
  name,
19
20
  });
@@ -168,6 +168,7 @@ export async function runBackgroundAutomation(options, deps) {
168
168
  path: automation.resource.path,
169
169
  scope: options.orgId ? "organization" : "personal",
170
170
  orgId: options.orgId ?? null,
171
+ appId: deps.appId,
171
172
  });
172
173
  }
173
174
  catch (err) {
@@ -307,6 +308,7 @@ async function executeBackgroundAutomation(options, deps, historyId) {
307
308
  threadId: thread.id,
308
309
  ownerEmail,
309
310
  orgId,
311
+ appId: deps.appId,
310
312
  actionCaller: options.actionCaller,
311
313
  automation: options.actionAutomation,
312
314
  runId,
@@ -46,6 +46,8 @@ export interface JobFrontmatter {
46
46
  mode?: JobExecutionMode;
47
47
  /** Domain tag for filtering in per-template UIs. */
48
48
  domain?: string;
49
+ /** Explicit application owner used by the recurring-job scheduler. */
50
+ appId?: string;
49
51
  /**
50
52
  * Optional application-owned policy id carried into actions by the trusted
51
53
  * trigger dispatcher. It is not model-supplied action input.
@@ -130,6 +130,9 @@ function parseKnownField(meta, key, rawValue) {
130
130
  case "domain":
131
131
  meta.domain = value;
132
132
  break;
133
+ case "appId":
134
+ meta.appId = value || undefined;
135
+ break;
133
136
  case "delegatedPolicyId":
134
137
  meta.delegatedPolicyId = value || undefined;
135
138
  break;
@@ -194,6 +197,7 @@ export function buildJobResourceContent(meta, body) {
194
197
  if (meta.mode)
195
198
  lines.push(`mode: ${meta.mode}`);
196
199
  pushString(lines, "domain", meta.domain);
200
+ pushString(lines, "appId", meta.appId);
197
201
  pushString(lines, "delegatedPolicyId", meta.delegatedPolicyId);
198
202
  // Keep the long-standing human-readable owner shape used by existing
199
203
  // resources and diagnostics; values that can contain free-form text use
@@ -13,6 +13,7 @@ export interface AutomationRun {
13
13
  path: string;
14
14
  scope: string | null;
15
15
  orgId: string | null;
16
+ appId: string | null;
16
17
  runId: string | null;
17
18
  threadId: string | null;
18
19
  status: AutomationRunStatus;
@@ -26,6 +27,7 @@ export interface StartAutomationRunInput {
26
27
  path: string;
27
28
  scope?: string | null;
28
29
  orgId?: string | null;
30
+ appId?: string | null;
29
31
  runId?: string | null;
30
32
  threadId?: string | null;
31
33
  /** A pre-created row still waiting for its background worker handoff. */
@@ -49,6 +51,7 @@ export declare function claimAutomationRun(id: string): Promise<boolean>;
49
51
  * redeliver it because claimAutomationRun is an atomic CAS.
50
52
  */
51
53
  export declare function listUnclaimedAutomationRuns(options?: {
54
+ appId?: string | null;
52
55
  olderThanMs?: number;
53
56
  limit?: number;
54
57
  }): Promise<AutomationRun[]>;
@@ -69,5 +72,6 @@ export declare function deleteAutomationRuns(owner: string, automation: string):
69
72
  export declare function listAutomationRuns(options: {
70
73
  owners: string[];
71
74
  automation: string;
75
+ appId?: string | null;
72
76
  limit?: number;
73
77
  }): Promise<AutomationRun[]>;
@@ -46,6 +46,7 @@ export const AUTOMATION_RUN_MIGRATIONS = [
46
46
  path TEXT NOT NULL,
47
47
  scope TEXT,
48
48
  org_id TEXT,
49
+ app_id TEXT,
49
50
  run_id TEXT,
50
51
  thread_id TEXT,
51
52
  status TEXT NOT NULL DEFAULT 'running',
@@ -70,6 +71,11 @@ export const AUTOMATION_RUN_MIGRATIONS = [
70
71
  sql: `ALTER TABLE ${TABLE}
71
72
  ADD COLUMN IF NOT EXISTS dispatch_pending INTEGER NOT NULL DEFAULT 0`,
72
73
  },
74
+ {
75
+ version: 4,
76
+ name: "automation-runs-app-id",
77
+ sql: `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS app_id TEXT`,
78
+ },
73
79
  ];
74
80
  export async function runAutomationRunMigrations(nitroApp) {
75
81
  await runMigrations(AUTOMATION_RUN_MIGRATIONS, {
@@ -86,10 +92,11 @@ async function ensureTable() {
86
92
  id TEXT PRIMARY KEY,
87
93
  owner TEXT NOT NULL,
88
94
  automation TEXT NOT NULL,
89
- path TEXT NOT NULL,
90
- scope TEXT,
91
- org_id TEXT,
92
- run_id TEXT,
95
+ path TEXT NOT NULL,
96
+ scope TEXT,
97
+ org_id TEXT,
98
+ app_id TEXT,
99
+ run_id TEXT,
93
100
  thread_id TEXT,
94
101
  status TEXT NOT NULL DEFAULT 'running',
95
102
  started_at ${intType()} NOT NULL,
@@ -113,6 +120,7 @@ async function ensureTable() {
113
120
  for (const [name, definition] of [
114
121
  ["claimed_at", `${intType()}`],
115
122
  ["dispatch_pending", `${intType()} NOT NULL DEFAULT 0`],
123
+ ["app_id", "TEXT"],
116
124
  ]) {
117
125
  if (columns.has(name))
118
126
  continue;
@@ -148,6 +156,7 @@ function toRun(row, now) {
148
156
  path: String(row.path),
149
157
  scope: row.scope == null ? null : String(row.scope),
150
158
  orgId: row.org_id == null ? null : String(row.org_id),
159
+ appId: row.app_id == null ? null : String(row.app_id),
151
160
  runId: row.run_id == null ? null : String(row.run_id),
152
161
  threadId: row.thread_id == null ? null : String(row.thread_id),
153
162
  status,
@@ -169,8 +178,8 @@ export async function startAutomationRun(input) {
169
178
  await ensureTable();
170
179
  const id = randomUUID();
171
180
  await getDbExec().execute({
172
- sql: `INSERT INTO ${TABLE} (id, owner, automation, path, scope, org_id, run_id, thread_id, status, started_at, dispatch_pending)
173
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?)`,
181
+ sql: `INSERT INTO ${TABLE} (id, owner, automation, path, scope, org_id, app_id, run_id, thread_id, status, started_at, dispatch_pending)
182
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?)`,
174
183
  args: [
175
184
  id,
176
185
  input.owner,
@@ -178,6 +187,7 @@ export async function startAutomationRun(input) {
178
187
  input.path,
179
188
  input.scope ?? null,
180
189
  input.orgId ?? null,
190
+ input.appId ?? null,
181
191
  input.runId ?? null,
182
192
  input.threadId ?? null,
183
193
  Date.now(),
@@ -213,14 +223,21 @@ export async function claimAutomationRun(id) {
213
223
  */
214
224
  export async function listUnclaimedAutomationRuns(options) {
215
225
  await ensureTable();
226
+ const appId = options?.appId?.trim() || null;
216
227
  const olderThanMs = Math.max(options?.olderThanMs ?? 10_000, 0);
217
228
  const limit = Math.min(Math.max(options?.limit ?? 50, 1), 100);
229
+ const appFilter = appId ? " AND (app_id = ? OR app_id IS NULL)" : "";
218
230
  const result = await getDbExec().execute({
219
231
  sql: `SELECT * FROM ${TABLE}
220
232
  WHERE dispatch_pending = 1 AND (claimed_at IS NULL OR claimed_at <= ?) AND status = 'running'
233
+ ${appFilter}
221
234
  AND started_at <= ?
222
235
  ORDER BY started_at ASC LIMIT ${limit}`,
223
- args: [Date.now() - CLAIM_LEASE_MS, Date.now() - olderThanMs],
236
+ args: [
237
+ Date.now() - CLAIM_LEASE_MS,
238
+ ...(appId ? [appId] : []),
239
+ Date.now() - olderThanMs,
240
+ ],
224
241
  });
225
242
  const now = Date.now();
226
243
  return (result.rows ?? []).map((row) => toRun(row, now));
@@ -314,11 +331,14 @@ export async function listAutomationRuns(options) {
314
331
  if (!owners.length)
315
332
  return [];
316
333
  const limit = Math.min(Math.max(options.limit ?? 20, 1), 100);
334
+ const appId = options.appId?.trim() || null;
317
335
  const placeholders = owners.map(() => "?").join(", ");
336
+ const appFilter = appId ? " AND (app_id = ? OR app_id IS NULL)" : "";
318
337
  const result = await getDbExec().execute({
319
338
  sql: `SELECT * FROM ${TABLE} WHERE owner IN (${placeholders}) AND automation = ?
339
+ ${appFilter}
320
340
  ORDER BY started_at DESC LIMIT ${limit}`,
321
- args: [...owners, options.automation],
341
+ args: [...owners, options.automation, ...(appId ? [appId] : [])],
322
342
  });
323
343
  const now = Date.now();
324
344
  return (result.rows ?? []).map((row) => toRun(row, now));
@@ -2,6 +2,7 @@ import { type AutomationScope } from "../automations/service.js";
2
2
  export interface RunAutomationNowInput {
3
3
  userEmail: string;
4
4
  orgId?: string | null;
5
+ appId?: string | null;
5
6
  scope: AutomationScope;
6
7
  name: string;
7
8
  }
@@ -16,4 +17,6 @@ export declare function queueAutomationRunNow(input: RunAutomationNowInput): Pro
16
17
  * This is intentionally a redelivery, not a second execution: the worker's
17
18
  * claim CAS decides which request owns the run.
18
19
  */
19
- export declare function redispatchUnclaimedAutomationRuns(): Promise<number>;
20
+ export declare function redispatchUnclaimedAutomationRuns(options?: {
21
+ appId?: string | null;
22
+ }): Promise<number>;
@@ -56,7 +56,7 @@ export async function queueAutomationRunNow(input) {
56
56
  }
57
57
  // A manual-run request is a guaranteed app request even on hosts without a
58
58
  // durable timer. Use it to recover older rows before adding the new one.
59
- await redispatchUnclaimedAutomationRuns().catch((error) => {
59
+ await redispatchUnclaimedAutomationRuns({ appId: input.appId }).catch((error) => {
60
60
  console.warn("[automations] Could not sweep queued runs before run-now:", error);
61
61
  });
62
62
  const historyId = await startAutomationRun({
@@ -65,6 +65,7 @@ export async function queueAutomationRunNow(input) {
65
65
  path: resource.path,
66
66
  scope: input.scope,
67
67
  orgId: input.scope === "organization" ? input.orgId : null,
68
+ appId: input.appId,
68
69
  dispatchPending: true,
69
70
  });
70
71
  try {
@@ -82,8 +83,8 @@ export async function queueAutomationRunNow(input) {
82
83
  * This is intentionally a redelivery, not a second execution: the worker's
83
84
  * claim CAS decides which request owns the run.
84
85
  */
85
- export async function redispatchUnclaimedAutomationRuns() {
86
- const runs = await listUnclaimedAutomationRuns();
86
+ export async function redispatchUnclaimedAutomationRuns(options) {
87
+ const runs = await listUnclaimedAutomationRuns({ appId: options?.appId });
87
88
  let attempted = 0;
88
89
  for (const run of runs) {
89
90
  try {
@@ -18,6 +18,13 @@ const MAX_IDENTITY_PREFLIGHTS_PER_TICK = MAX_CONCURRENT_SCHEDULED_JOBS * 4;
18
18
  const IDENTITY_FAILURE_RETRY_MS = 5 * 60_000;
19
19
  const _activeScheduledJobs = new Set();
20
20
  const _preflightingScheduledJobs = new Set();
21
+ function jobBelongsToApp(meta, appId) {
22
+ const ownerAppId = meta.appId?.trim();
23
+ if (!ownerAppId)
24
+ return true;
25
+ const schedulerAppId = appId?.trim();
26
+ return Boolean(schedulerAppId && ownerAppId === schedulerAppId);
27
+ }
21
28
  // Skip the DB query on every tick if we recently confirmed no jobs exist.
22
29
  // `_hasJobsCache` is invalidated whenever a `jobs/*` resource is written or
23
30
  // deleted (subscribed below), and refreshed at most every 5 minutes.
@@ -106,6 +113,12 @@ export async function processRecurringJobs(deps) {
106
113
  if (resource.path.endsWith(".keep"))
107
114
  continue;
108
115
  const { meta, body } = parseJobFrontmatter(resource.content);
116
+ // Jobs written before app ownership was persisted remain compatible with
117
+ // the shared scheduler. Once a job declares an owner, only that app may
118
+ // evaluate or execute it. Without this boundary every app's scheduled
119
+ // worker can claim the same organization resource.
120
+ if (!jobBelongsToApp(meta, deps.appId))
121
+ continue;
109
122
  healthOrgIds.add(meta.orgId ?? null);
110
123
  // Skip disabled or missing schedule
111
124
  if (!meta.enabled || !meta.schedule)
@@ -422,6 +435,11 @@ export async function runQueuedAutomation(historyId, deps) {
422
435
  const queued = await getAutomationRun(historyId);
423
436
  if (!queued)
424
437
  throw new Error(`Automation run "${historyId}" not found.`);
438
+ const queuedAppId = queued.appId?.trim() || null;
439
+ const workerAppId = deps.appId?.trim() || null;
440
+ if (queuedAppId && queuedAppId !== workerAppId) {
441
+ return { skipped: true };
442
+ }
425
443
  if (!(await claimAutomationRun(historyId))) {
426
444
  return { skipped: true };
427
445
  }
@@ -1,5 +1,6 @@
1
1
  import type { ActionEntry } from "../agent/production-agent.js";
2
2
  import { type JobFrontmatter } from "./scheduler.js";
3
+ export declare function jobBelongsToApp(meta: JobFrontmatter, appId?: string): boolean;
3
4
  /**
4
5
  * Authorise a mutation (update / delete) against a job resource. When the
5
6
  * job is in the SHARED scope the caller must either be the original
@@ -11,5 +12,5 @@ import { type JobFrontmatter } from "./scheduler.js";
11
12
  * Returns null when the mutation is allowed, or an error string suitable
12
13
  * for returning to the caller when not.
13
14
  */
14
- export declare function authorizeJobMutation(resourceOwner: string, meta: JobFrontmatter): Promise<string | null>;
15
- export declare function createJobTools(): Record<string, ActionEntry>;
15
+ export declare function authorizeJobMutation(resourceOwner: string, meta: JobFrontmatter, appId?: string): Promise<string | null>;
16
+ export declare function createJobTools(appId?: string): Record<string, ActionEntry>;
@@ -14,6 +14,12 @@ function getOwner() {
14
14
  function getSharedOwner() {
15
15
  return sharedResourceOwner(getRequestOrgId());
16
16
  }
17
+ export function jobBelongsToApp(meta, appId) {
18
+ const ownerAppId = meta.appId?.trim();
19
+ if (!ownerAppId)
20
+ return true;
21
+ return ownerAppId === appId?.trim();
22
+ }
17
23
  /**
18
24
  * Determine if the current request's user is an org owner/admin in the
19
25
  * given org. Used to allow privileged users to update or delete shared
@@ -52,7 +58,10 @@ async function isCurrentUserOrgAdmin(orgId) {
52
58
  * Returns null when the mutation is allowed, or an error string suitable
53
59
  * for returning to the caller when not.
54
60
  */
55
- export async function authorizeJobMutation(resourceOwner, meta) {
61
+ export async function authorizeJobMutation(resourceOwner, meta, appId) {
62
+ if (!jobBelongsToApp(meta, appId)) {
63
+ return "This job belongs to another app and cannot be changed here.";
64
+ }
56
65
  const resourceOrgId = organizationIdFromResourceOwner(resourceOwner);
57
66
  if (resourceOwner !== SHARED_OWNER && !resourceOrgId) {
58
67
  // Personal-scope job — owner is the request's user. resourceGetByPath is
@@ -69,7 +78,7 @@ export async function authorizeJobMutation(resourceOwner, meta) {
69
78
  return null;
70
79
  return "Only the job's creator (or an org admin) can update or delete it.";
71
80
  }
72
- async function runCreate(args) {
81
+ async function runCreate(args, appId) {
73
82
  const { name, schedule, instructions, scope, runAs, model } = args;
74
83
  const requestedTimezone = args.timezone;
75
84
  if (!name || !schedule || !instructions) {
@@ -111,6 +120,7 @@ async function runCreate(args) {
111
120
  enabled: true,
112
121
  createdBy: getOwner(),
113
122
  orgId: getRequestOrgId() || undefined,
123
+ appId: appId?.trim() || undefined,
114
124
  runAs: runAs === "shared" ? "shared" : "creator",
115
125
  nextRun: next.toISOString(),
116
126
  ...(integration?.scopeId ? { originScopeId: integration.scopeId } : {}),
@@ -143,7 +153,7 @@ async function runCreate(args) {
143
153
  ...(mcpTools?.length ? { mcpTools } : {}),
144
154
  });
145
155
  }
146
- async function runList(args) {
156
+ async function runList(args, appId) {
147
157
  const owner = getOwner();
148
158
  const sharedOwner = getSharedOwner();
149
159
  // Fetch only current user's and shared jobs (not other users')
@@ -164,6 +174,8 @@ async function runList(args) {
164
174
  if (classifyJobResource(full.content).kind === "automation")
165
175
  return null;
166
176
  const { meta } = parseJobFrontmatter(full.content);
177
+ if (!jobBelongsToApp(meta, appId))
178
+ return null;
167
179
  return {
168
180
  name: r.path.replace(/^jobs\//, "").replace(/\.md$/, ""),
169
181
  path: r.path,
@@ -191,7 +203,7 @@ async function runList(args) {
191
203
  }
192
204
  return JSON.stringify(scheduledJobs, null, 2);
193
205
  }
194
- async function runUpdate(args) {
206
+ async function runUpdate(args, appId) {
195
207
  const { name, schedule, instructions, enabled, scope, runAs, model } = args;
196
208
  const path = `jobs/${name}.md`;
197
209
  // Try to find the resource
@@ -213,10 +225,12 @@ async function runUpdate(args) {
213
225
  // `createdBy` is alice@…, and the next cron tick would run the
214
226
  // attacker's instructions as alice (creator-runAs schedules in
215
227
  // jobs/scheduler.ts line 273-278).
216
- const denied = await authorizeJobMutation(resource.owner, meta);
228
+ const denied = await authorizeJobMutation(resource.owner, meta, appId);
217
229
  if (denied) {
218
230
  return JSON.stringify({ error: denied });
219
231
  }
232
+ if (!meta.appId && appId?.trim())
233
+ meta.appId = appId.trim();
220
234
  if (schedule) {
221
235
  if (!isValidCron(schedule)) {
222
236
  return JSON.stringify({
@@ -273,7 +287,7 @@ async function runUpdate(args) {
273
287
  mcpTools: meta.mcpTools || [],
274
288
  });
275
289
  }
276
- async function runDelete(args) {
290
+ async function runDelete(args, appId) {
277
291
  const { name, scope } = args;
278
292
  const path = `jobs/${name}.md`;
279
293
  let resource = await resourceGetByPath(getSharedOwner(), path);
@@ -292,14 +306,14 @@ async function runDelete(args) {
292
306
  error: `"${name}" is an automation. Use manage-automations to delete it.`,
293
307
  });
294
308
  }
295
- const denied = await authorizeJobMutation(resource.owner, meta);
309
+ const denied = await authorizeJobMutation(resource.owner, meta, appId);
296
310
  if (denied) {
297
311
  return JSON.stringify({ error: denied });
298
312
  }
299
313
  await resourceDelete(resource.id);
300
314
  return JSON.stringify({ deleted: true, name });
301
315
  }
302
- export function createJobTools() {
316
+ export function createJobTools(appId) {
303
317
  return {
304
318
  "manage-jobs": {
305
319
  tool: {
@@ -374,13 +388,13 @@ For jobs that use a connected MCP, pass the exact tool names in mcpTools. This b
374
388
  run: async (args) => {
375
389
  switch (args.action) {
376
390
  case "create":
377
- return runCreate(args);
391
+ return runCreate(args, appId);
378
392
  case "list":
379
- return runList(args);
393
+ return runList(args, appId);
380
394
  case "update":
381
- return runUpdate(args);
395
+ return runUpdate(args, appId);
382
396
  case "delete":
383
- return runDelete(args);
397
+ return runDelete(args, appId);
384
398
  default:
385
399
  return JSON.stringify({
386
400
  error: `Unknown action "${args.action}". Use "create", "list", or "update".`,
@@ -616,6 +616,7 @@ declare const messages: {
616
616
  cronFieldCount: string;
617
617
  saveSchedule: string;
618
618
  details: string;
619
+ viewDetails: string;
619
620
  pause: string;
620
621
  resume: string;
621
622
  delete: string;
@@ -622,6 +622,7 @@ const messages = {
622
622
  cronFieldCount: "A cron expression needs exactly 5 fields.",
623
623
  saveSchedule: "Save schedule",
624
624
  details: "Details",
625
+ viewDetails: "View details",
625
626
  pause: "Pause",
626
627
  resume: "Resume",
627
628
  delete: "Delete",