@k2b/cloud 0.6.0-rc.0

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 (540) hide show
  1. package/README.md +98 -0
  2. package/package.json +128 -0
  3. package/public/logo.svg +1 -0
  4. package/scripts/app-favicon.test.ts +49 -0
  5. package/scripts/app-favicon.ts +51 -0
  6. package/scripts/build.ts +205 -0
  7. package/scripts/preload.ts +164 -0
  8. package/scripts/sync-recovery-smoke.ts +200 -0
  9. package/src/_internal/app-favicon.ts +2 -0
  10. package/src/_internal/app-presentation.ts +61 -0
  11. package/src/_internal/app-workspace-state.ts +24 -0
  12. package/src/_internal/bounded-json.ts +47 -0
  13. package/src/_internal/build-metadata.ts +9 -0
  14. package/src/_internal/capabilities.ts +1118 -0
  15. package/src/_internal/define-app.ts +801 -0
  16. package/src/_internal/heartbeat.ts +167 -0
  17. package/src/_internal/help-catalog.ts +241 -0
  18. package/src/_internal/help.ts +95 -0
  19. package/src/_internal/nats-connection.ts +34 -0
  20. package/src/_internal/page-responses.ts +40 -0
  21. package/src/_internal/process-sync.ts +117 -0
  22. package/src/_internal/readiness.ts +3 -0
  23. package/src/_internal/registry-validation.ts +136 -0
  24. package/src/_internal/registry-watch.ts +52 -0
  25. package/src/_internal/registry.ts +338 -0
  26. package/src/_internal/runtime-compatibility.ts +64 -0
  27. package/src/_internal/runtime-context.ts +84 -0
  28. package/src/_internal/runtime-watcher.ts +78 -0
  29. package/src/_internal/static-assets.ts +86 -0
  30. package/src/_internal/status-preserving-ssr.ts +49 -0
  31. package/src/access/PermissionEditor.tsx +486 -0
  32. package/src/access/ResourceApiKeys.tsx +271 -0
  33. package/src/access/messages.ts +121 -0
  34. package/src/access/ui.ts +16 -0
  35. package/src/account/Avatar.tsx +28 -0
  36. package/src/account/EntitySearch.tsx +363 -0
  37. package/src/account/avatar-upload-dialog.tsx +197 -0
  38. package/src/account/avatar-upload.ts +149 -0
  39. package/src/account/ui.ts +3 -0
  40. package/src/ai/admin.ts +47 -0
  41. package/src/ai/approval-routes.ts +130 -0
  42. package/src/ai/approvals.ts +139 -0
  43. package/src/ai/assistant-models.ts +61 -0
  44. package/src/ai/attachments.ts +40 -0
  45. package/src/ai/browser.ts +92 -0
  46. package/src/ai/capabilities.ts +714 -0
  47. package/src/ai/capability-execution.ts +151 -0
  48. package/src/ai/chat/blocks.render.test.tsx +1108 -0
  49. package/src/ai/chat/blocks.tsx +730 -0
  50. package/src/ai/chat/builtin-tools.tsx +428 -0
  51. package/src/ai/chat/capability-table.tsx +87 -0
  52. package/src/ai/chat/composer-adapter.ts +169 -0
  53. package/src/ai/chat/file-tools.tsx +81 -0
  54. package/src/ai/chat/message-actions.tsx +370 -0
  55. package/src/ai/chat/message-utils.ts +479 -0
  56. package/src/ai/chat/messages.ts +43 -0
  57. package/src/ai/chat/presentation.tsx +395 -0
  58. package/src/ai/chat/primitives.tsx +3 -0
  59. package/src/ai/chat/tool-disclosure.tsx +41 -0
  60. package/src/ai/chat/turn-navigator-utils.ts +28 -0
  61. package/src/ai/chat/turn-navigator.tsx +164 -0
  62. package/src/ai/chat/user-message.tsx +149 -0
  63. package/src/ai/chat/visual-tools.tsx +435 -0
  64. package/src/ai/chat/web-tools.tsx +202 -0
  65. package/src/ai/chat-task-contracts.ts +113 -0
  66. package/src/ai/chat-tasks.ts +952 -0
  67. package/src/ai/client/controller.ts +1211 -0
  68. package/src/ai/client/file-source.ts +95 -0
  69. package/src/ai/client/live-connection.ts +140 -0
  70. package/src/ai/client/projection.ts +178 -0
  71. package/src/ai/client/transport.ts +111 -0
  72. package/src/ai/compaction.ts +182 -0
  73. package/src/ai/credentials.ts +124 -0
  74. package/src/ai/default-tool-contracts.ts +108 -0
  75. package/src/ai/default-tools.ts +127 -0
  76. package/src/ai/enrich.ts +280 -0
  77. package/src/ai/executor.ts +1624 -0
  78. package/src/ai/fetch-file-tool.ts +313 -0
  79. package/src/ai/file-context.ts +92 -0
  80. package/src/ai/file-media-type.ts +28 -0
  81. package/src/ai/file-mount.ts +16 -0
  82. package/src/ai/file-tools.ts +566 -0
  83. package/src/ai/files-store.ts +390 -0
  84. package/src/ai/firecrawl-tools.ts +305 -0
  85. package/src/ai/grids-skill.ts +126 -0
  86. package/src/ai/http.ts +265 -0
  87. package/src/ai/index.ts +337 -0
  88. package/src/ai/kit-skill.ts +22 -0
  89. package/src/ai/limits.ts +5 -0
  90. package/src/ai/live-events.ts +289 -0
  91. package/src/ai/live-messages.ts +45 -0
  92. package/src/ai/live-outbox.ts +102 -0
  93. package/src/ai/live-routes.ts +412 -0
  94. package/src/ai/live.ts +2 -0
  95. package/src/ai/maintenance.ts +302 -0
  96. package/src/ai/markdown-pdf-tool.ts +95 -0
  97. package/src/ai/memories.ts +605 -0
  98. package/src/ai/memory-learning-runs.ts +222 -0
  99. package/src/ai/memory-learning.ts +619 -0
  100. package/src/ai/memory-tool.ts +120 -0
  101. package/src/ai/memory-workflow-evidence.ts +158 -0
  102. package/src/ai/migrate.ts +2082 -0
  103. package/src/ai/model-access-migrate.ts +30 -0
  104. package/src/ai/model-access.ts +191 -0
  105. package/src/ai/personal-agent.ts +6 -0
  106. package/src/ai/prefs.ts +72 -0
  107. package/src/ai/project-tool.ts +94 -0
  108. package/src/ai/projects-routes.ts +277 -0
  109. package/src/ai/projects.ts +902 -0
  110. package/src/ai/protocol.ts +279 -0
  111. package/src/ai/provider.ts +73 -0
  112. package/src/ai/public-projection.ts +25 -0
  113. package/src/ai/resource-markers.ts +31 -0
  114. package/src/ai/resource-refs.ts +70 -0
  115. package/src/ai/routes.ts +1131 -0
  116. package/src/ai/runtime-tools.ts +39 -0
  117. package/src/ai/runtime.ts +544 -0
  118. package/src/ai/settings.ts +529 -0
  119. package/src/ai/short-id.ts +45 -0
  120. package/src/ai/skill-catalog.ts +73 -0
  121. package/src/ai/skill-format.ts +145 -0
  122. package/src/ai/skill-seeds.ts +430 -0
  123. package/src/ai/skill-tool.ts +81 -0
  124. package/src/ai/skills-routes.ts +143 -0
  125. package/src/ai/skills.ts +934 -0
  126. package/src/ai/solid.ts +27 -0
  127. package/src/ai/store.ts +3113 -0
  128. package/src/ai/stream.ts +308 -0
  129. package/src/ai/structured-runs.ts +66 -0
  130. package/src/ai/structured.ts +186 -0
  131. package/src/ai/system-prompt.ts +170 -0
  132. package/src/ai/task-prompt.ts +22 -0
  133. package/src/ai/timeline.ts +148 -0
  134. package/src/ai/tool-audit.ts +177 -0
  135. package/src/ai/tool-result-budget.ts +16 -0
  136. package/src/ai/tools.ts +155 -0
  137. package/src/ai/types.ts +967 -0
  138. package/src/ai/ui.tsx +39 -0
  139. package/src/ai/usage-migrate.ts +81 -0
  140. package/src/ai/usage.ts +315 -0
  141. package/src/ai/validate.ts +54 -0
  142. package/src/ai/vision-tool.ts +79 -0
  143. package/src/api/accounts-entities.ts +174 -0
  144. package/src/api/admin-ai-projects.ts +74 -0
  145. package/src/api/admin-ai-skills.ts +76 -0
  146. package/src/api/admin-ai-usage.ts +42 -0
  147. package/src/api/admin-core-settings.ts +508 -0
  148. package/src/api/admin-identity.ts +148 -0
  149. package/src/api/admin-lifecycle.ts +206 -0
  150. package/src/api/admin-linux-identities.ts +66 -0
  151. package/src/api/announcements.ts +131 -0
  152. package/src/api/app-approval.ts +129 -0
  153. package/src/api/apps.ts +51 -0
  154. package/src/api/auth/schemas.ts +54 -0
  155. package/src/api/auth.ts +370 -0
  156. package/src/api/capabilities.ts +658 -0
  157. package/src/api/help-messages.ts +19 -0
  158. package/src/api/help.ts +78 -0
  159. package/src/api/index.ts +108 -0
  160. package/src/api/mcp.ts +744 -0
  161. package/src/api/me-rail.ts +45 -0
  162. package/src/api/me.ts +753 -0
  163. package/src/api/search/schemas.ts +79 -0
  164. package/src/api/search.bench.ts +185 -0
  165. package/src/api/search.ts +407 -0
  166. package/src/api/sync-ops.ts +79 -0
  167. package/src/api/widgets.ts +143 -0
  168. package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
  169. package/src/browser/CloudResourceSearch.tsx +791 -0
  170. package/src/browser/app-approval-vault.ts +305 -0
  171. package/src/browser/app-approval.ts +234 -0
  172. package/src/browser/live-websocket.ts +268 -0
  173. package/src/browser/locale-preference.ts +25 -0
  174. package/src/browser/notification-target.ts +27 -0
  175. package/src/browser/notification-timeout.ts +14 -0
  176. package/src/browser/notifications.ts +181 -0
  177. package/src/browser/resource-clipboard.ts +70 -0
  178. package/src/browser/resource-picker.tsx +39 -0
  179. package/src/browser/resource-search-messages.ts +55 -0
  180. package/src/browser/resource-search.ts +27 -0
  181. package/src/capabilities/client.ts +149 -0
  182. package/src/capabilities/response.ts +39 -0
  183. package/src/capabilities/server.ts +215 -0
  184. package/src/capabilities/signals.ts +4 -0
  185. package/src/capabilities/testing.ts +12 -0
  186. package/src/capabilities/types.ts +32 -0
  187. package/src/cli/access.ts +499 -0
  188. package/src/cli/account.ts +382 -0
  189. package/src/cli/admin/account-administration.ts +45 -0
  190. package/src/cli/admin/account-categories.ts +49 -0
  191. package/src/cli/admin/ai-usage.ts +118 -0
  192. package/src/cli/admin/app-sign-in.ts +49 -0
  193. package/src/cli/admin/data.ts +343 -0
  194. package/src/cli/admin/documentation.ts +34 -0
  195. package/src/cli/admin/gateway.ts +155 -0
  196. package/src/cli/admin/index.ts +61 -0
  197. package/src/cli/admin/instance.ts +250 -0
  198. package/src/cli/admin/jobs.ts +223 -0
  199. package/src/cli/admin/legal.ts +122 -0
  200. package/src/cli/admin/linux.ts +53 -0
  201. package/src/cli/admin/logs.ts +324 -0
  202. package/src/cli/admin/metrics.ts +142 -0
  203. package/src/cli/admin/nats.ts +167 -0
  204. package/src/cli/admin/notifications.ts +516 -0
  205. package/src/cli/admin/shared.ts +113 -0
  206. package/src/cli/admin/sync.ts +188 -0
  207. package/src/cli/admin/telemetry.ts +314 -0
  208. package/src/cli/admin/webhooks.ts +222 -0
  209. package/src/cli/admin/workflows.ts +367 -0
  210. package/src/cli/apps.ts +40 -0
  211. package/src/cli/capabilities.ts +239 -0
  212. package/src/cli/commands.ts +604 -0
  213. package/src/cli/index.ts +61 -0
  214. package/src/cli/locale.ts +31 -0
  215. package/src/cli/output.ts +58 -0
  216. package/src/clients/core.ts +20 -0
  217. package/src/config/env.ts +54 -0
  218. package/src/config/index.ts +6 -0
  219. package/src/contracts/account-categories.ts +62 -0
  220. package/src/contracts/announcements.ts +121 -0
  221. package/src/contracts/app-approval.ts +197 -0
  222. package/src/contracts/app.ts +162 -0
  223. package/src/contracts/capabilities.ts +484 -0
  224. package/src/contracts/index.ts +14 -0
  225. package/src/contracts/notification-live.ts +66 -0
  226. package/src/contracts/notification-types.ts +235 -0
  227. package/src/contracts/posix.ts +69 -0
  228. package/src/contracts/profile.ts +85 -0
  229. package/src/contracts/rail-preferences.ts +53 -0
  230. package/src/contracts/registry.ts +110 -0
  231. package/src/contracts/resource-clipboard.ts +33 -0
  232. package/src/contracts/settings-types.ts +98 -0
  233. package/src/contracts/shared.ts +408 -0
  234. package/src/contracts/user-notifications.ts +101 -0
  235. package/src/contracts/widgets.ts +183 -0
  236. package/src/desktop/index.ts +704 -0
  237. package/src/desktop/solid.tsx +938 -0
  238. package/src/index.ts +58 -0
  239. package/src/server/actor.ts +41 -0
  240. package/src/server/api/index.ts +2 -0
  241. package/src/server/api/respond.ts +96 -0
  242. package/src/server/api-client.ts +54 -0
  243. package/src/server/app-context.ts +39 -0
  244. package/src/server/help.ts +187 -0
  245. package/src/server/index.ts +89 -0
  246. package/src/server/locale.ts +55 -0
  247. package/src/server/middleware/auth.ts +386 -0
  248. package/src/server/middleware/index.ts +28 -0
  249. package/src/server/middleware/invocation.ts +55 -0
  250. package/src/server/middleware/middleware.ts +65 -0
  251. package/src/server/middleware/openapi.ts +126 -0
  252. package/src/server/middleware/rate-limit.ts +125 -0
  253. package/src/server/middleware/request-logger.ts +41 -0
  254. package/src/server/middleware/route-template.ts +46 -0
  255. package/src/server/middleware/runtime.ts +22 -0
  256. package/src/server/middleware/settings.ts +55 -0
  257. package/src/server/middleware/validator.ts +49 -0
  258. package/src/server/middleware/workload.ts +22 -0
  259. package/src/server/ratelimit.ts +151 -0
  260. package/src/server/request-cookies.ts +18 -0
  261. package/src/server/services/access.ts +644 -0
  262. package/src/server/services/freeipa/client.ts +137 -0
  263. package/src/server/services/freeipa/index.ts +9 -0
  264. package/src/server/services/freeipa/session.ts +123 -0
  265. package/src/server/services/freeipa/test-certificates.ts +44 -0
  266. package/src/server/services/freeipa/tls.ts +44 -0
  267. package/src/server/services/freeipa/transport.ts +116 -0
  268. package/src/server/services/freeipa/util.ts +59 -0
  269. package/src/server/services/geo.ts +198 -0
  270. package/src/server/services/index.ts +48 -0
  271. package/src/server/services/pagination.ts +22 -0
  272. package/src/server/services/services.ts +13 -0
  273. package/src/server/time.ts +33 -0
  274. package/src/services/account-category-policy.ts +51 -0
  275. package/src/services/account-lifecycle/audit.ts +41 -0
  276. package/src/services/account-lifecycle/index.ts +940 -0
  277. package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
  278. package/src/services/account-lifecycle/notification-sender.ts +13 -0
  279. package/src/services/account-lifecycle/scheduler.ts +506 -0
  280. package/src/services/account-model.ts +22 -0
  281. package/src/services/accounts/app.ts +1671 -0
  282. package/src/services/accounts/authz.ts +44 -0
  283. package/src/services/accounts/avatar.ts +120 -0
  284. package/src/services/accounts/base-group.ts +11 -0
  285. package/src/services/accounts/base-user.ts +46 -0
  286. package/src/services/accounts/duplicate-emails.ts +82 -0
  287. package/src/services/accounts/entities.ts +622 -0
  288. package/src/services/accounts/group-sql.ts +106 -0
  289. package/src/services/accounts/groups.ts +272 -0
  290. package/src/services/accounts/index.ts +14 -0
  291. package/src/services/accounts/ipa-data.ts +66 -0
  292. package/src/services/accounts/lifecycle.ts +2 -0
  293. package/src/services/accounts/local-groups.ts +499 -0
  294. package/src/services/accounts/model.ts +129 -0
  295. package/src/services/accounts/notification-sender.ts +31 -0
  296. package/src/services/accounts/posix.ts +386 -0
  297. package/src/services/accounts/request-policy.ts +11 -0
  298. package/src/services/accounts/switching.ts +126 -0
  299. package/src/services/accounts/users.ts +745 -0
  300. package/src/services/announcements/index.ts +224 -0
  301. package/src/services/app-approval.ts +498 -0
  302. package/src/services/audit/index.ts +438 -0
  303. package/src/services/auth-flows/index.ts +15 -0
  304. package/src/services/auth-flows/ipa.ts +197 -0
  305. package/src/services/auth-flows/magic-link.ts +185 -0
  306. package/src/services/auth-flows/notification-sender.ts +10 -0
  307. package/src/services/auth-flows/password-reset.ts +268 -0
  308. package/src/services/auth-flows/proxy-return.ts +49 -0
  309. package/src/services/document-extraction.ts +148 -0
  310. package/src/services/freeipa-config.ts +179 -0
  311. package/src/services/gateway.ts +186 -0
  312. package/src/services/identity/constants.ts +18 -0
  313. package/src/services/identity/index.ts +52 -0
  314. package/src/services/identity/invocation-actor.ts +120 -0
  315. package/src/services/identity/invocation-authority.ts +53 -0
  316. package/src/services/identity/invocation-operations.ts +15 -0
  317. package/src/services/identity/invocation-token.ts +276 -0
  318. package/src/services/identity/key-config.ts +41 -0
  319. package/src/services/identity/key-ring.ts +530 -0
  320. package/src/services/identity/metrics.ts +29 -0
  321. package/src/services/identity/routes.ts +18 -0
  322. package/src/services/identity/runtime-config.ts +88 -0
  323. package/src/services/identity/session-token.ts +152 -0
  324. package/src/services/identity/signer-refresh.ts +44 -0
  325. package/src/services/identity/workload-auth.ts +64 -0
  326. package/src/services/index.ts +187 -0
  327. package/src/services/ipa/auth.ts +216 -0
  328. package/src/services/ipa/connection.ts +57 -0
  329. package/src/services/ipa/effective-groups.ts +70 -0
  330. package/src/services/ipa/groups.ts +722 -0
  331. package/src/services/ipa/guard.ts +17 -0
  332. package/src/services/ipa/index.ts +17 -0
  333. package/src/services/ipa/posix.ts +19 -0
  334. package/src/services/ipa/profile.ts +132 -0
  335. package/src/services/ipa/runtime.ts +28 -0
  336. package/src/services/ipa/search.ts +154 -0
  337. package/src/services/ipa/service-account.ts +15 -0
  338. package/src/services/ipa/sync-planning.ts +100 -0
  339. package/src/services/ipa/sync.ts +862 -0
  340. package/src/services/ipa/users.ts +806 -0
  341. package/src/services/legal-consent.ts +76 -0
  342. package/src/services/logging/index.ts +477 -0
  343. package/src/services/logging/redaction.ts +20 -0
  344. package/src/services/logging/trace.ts +1231 -0
  345. package/src/services/mandates/index.ts +855 -0
  346. package/src/services/mandates/policy.ts +85 -0
  347. package/src/services/network-security.ts +31 -0
  348. package/src/services/notifications/batches.ts +722 -0
  349. package/src/services/notifications/browser.ts +252 -0
  350. package/src/services/notifications/catalog.ts +113 -0
  351. package/src/services/notifications/channels.ts +104 -0
  352. package/src/services/notifications/dispatcher.ts +139 -0
  353. package/src/services/notifications/email.ts +118 -0
  354. package/src/services/notifications/index.ts +633 -0
  355. package/src/services/notifications/live.ts +50 -0
  356. package/src/services/notifications/observability.ts +407 -0
  357. package/src/services/notifications/platform.ts +421 -0
  358. package/src/services/notifications/runtime.ts +120 -0
  359. package/src/services/notifications/user.ts +248 -0
  360. package/src/services/notifications/web-push-transport.ts +135 -0
  361. package/src/services/oauth-tokens.ts +209 -0
  362. package/src/services/outbox.ts +187 -0
  363. package/src/services/pdf/gotenberg.ts +290 -0
  364. package/src/services/pdf/index.ts +43 -0
  365. package/src/services/pdf/markdown.ts +209 -0
  366. package/src/services/pdf/template-preview.ts +122 -0
  367. package/src/services/postgres.ts +76 -0
  368. package/src/services/providers/index.ts +27 -0
  369. package/src/services/providers/local/auth.ts +51 -0
  370. package/src/services/providers/local/index.ts +4 -0
  371. package/src/services/providers/local/users.ts +263 -0
  372. package/src/services/rail-preferences.ts +27 -0
  373. package/src/services/runtime-lifecycle.ts +191 -0
  374. package/src/services/secrets.ts +10 -0
  375. package/src/services/service-account-credentials.ts +891 -0
  376. package/src/services/service-accounts.ts +184 -0
  377. package/src/services/session/index.ts +236 -0
  378. package/src/services/session/test-fixture.ts +18 -0
  379. package/src/services/session/user.ts +144 -0
  380. package/src/services/settings/api.ts +61 -0
  381. package/src/services/settings/app.ts +86 -0
  382. package/src/services/settings/core-settings.ts +796 -0
  383. package/src/services/settings/crypto.ts +69 -0
  384. package/src/services/settings/defaults.ts +374 -0
  385. package/src/services/settings/index.ts +215 -0
  386. package/src/services/settings/namespace.ts +9 -0
  387. package/src/services/settings/snapshot.ts +49 -0
  388. package/src/services/settings/store.ts +241 -0
  389. package/src/services/settings/templates.ts +4 -0
  390. package/src/services/sync-ops.ts +403 -0
  391. package/src/services/topic-cursor.ts +22 -0
  392. package/src/services/weather/forecast.ts +323 -0
  393. package/src/services/weather/geo.ts +119 -0
  394. package/src/services/weather/index.ts +67 -0
  395. package/src/services/weather/location.ts +24 -0
  396. package/src/services/weather/locations.ts +133 -0
  397. package/src/services/weather/migrate.ts +45 -0
  398. package/src/services/weather/settings.ts +66 -0
  399. package/src/services/weather/short-id.ts +57 -0
  400. package/src/services/weather/types.ts +61 -0
  401. package/src/services/weather/ui.ts +50 -0
  402. package/src/services/webauthn.ts +385 -0
  403. package/src/shared/account-action-notice.ts +70 -0
  404. package/src/shared/account-display.ts +15 -0
  405. package/src/shared/account-session.ts +15 -0
  406. package/src/shared/ai-platform-prompt.ts +134 -0
  407. package/src/shared/ai-usage.ts +49 -0
  408. package/src/shared/app-presentation.ts +48 -0
  409. package/src/shared/app-url.ts +12 -0
  410. package/src/shared/branding.ts +14 -0
  411. package/src/shared/capability-messages.ts +70 -0
  412. package/src/shared/email-html.ts +54 -0
  413. package/src/shared/format.ts +91 -0
  414. package/src/shared/help.ts +68 -0
  415. package/src/shared/icons.ts +400 -0
  416. package/src/shared/index.ts +54 -0
  417. package/src/shared/locale.ts +48 -0
  418. package/src/shared/login-method.ts +44 -0
  419. package/src/shared/markdown/client.ts +130 -0
  420. package/src/shared/markdown/extensions/code.ts +105 -0
  421. package/src/shared/markdown/extensions/guided-help.ts +100 -0
  422. package/src/shared/markdown/extensions/images.ts +79 -0
  423. package/src/shared/markdown/extensions/info-blocks.ts +97 -0
  424. package/src/shared/markdown/extensions/katex.ts +120 -0
  425. package/src/shared/markdown/extensions/links.ts +43 -0
  426. package/src/shared/markdown/extensions/mark.ts +48 -0
  427. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  428. package/src/shared/markdown/extensions/tables.ts +109 -0
  429. package/src/shared/markdown/extensions/task-list.ts +53 -0
  430. package/src/shared/markdown/formula.ts +1187 -0
  431. package/src/shared/markdown/index.ts +193 -0
  432. package/src/shared/markdown/shared.ts +36 -0
  433. package/src/shared/mock-cover.ts +130 -0
  434. package/src/shared/network-address.ts +106 -0
  435. package/src/shared/redirect.ts +56 -0
  436. package/src/shared/template-rendering.ts +140 -0
  437. package/src/shared/theme.ts +68 -0
  438. package/src/shared/time.ts +13 -0
  439. package/src/ssr/AdminLayout.tsx +53 -0
  440. package/src/ssr/AdminSidebar.tsx +50 -0
  441. package/src/ssr/AppLaunchpad.island.tsx +236 -0
  442. package/src/ssr/AppWorkspaceController.island.tsx +21 -0
  443. package/src/ssr/BrowserNotifications.island.tsx +135 -0
  444. package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
  445. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
  446. package/src/ssr/GlobalSearchDialog.tsx +35 -0
  447. package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
  448. package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
  449. package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
  450. package/src/ssr/Layout.render.test.tsx +148 -0
  451. package/src/ssr/Layout.tsx +292 -0
  452. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  453. package/src/ssr/LayoutFooter.tsx +22 -0
  454. package/src/ssr/LayoutHeader.tsx +81 -0
  455. package/src/ssr/LayoutHelp.tsx +869 -0
  456. package/src/ssr/LayoutPreferences.island.tsx +36 -0
  457. package/src/ssr/LayoutRail.tsx +58 -0
  458. package/src/ssr/MinimalLayout.tsx +46 -0
  459. package/src/ssr/PageError.tsx +29 -0
  460. package/src/ssr/ProfilePreferences.island.tsx +92 -0
  461. package/src/ssr/RailApps.island.tsx +61 -0
  462. package/src/ssr/RailEditor.tsx +293 -0
  463. package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
  464. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  465. package/src/ssr/admin-active-link.ts +47 -0
  466. package/src/ssr/admin-navigation.ts +78 -0
  467. package/src/ssr/app-appearance.ts +23 -0
  468. package/src/ssr/app-navigation.ts +41 -0
  469. package/src/ssr/help-messages.ts +104 -0
  470. package/src/ssr/index.ts +14 -0
  471. package/src/ssr/islands/SearchBar.island.tsx +80 -0
  472. package/src/ssr/islands/index.ts +16 -0
  473. package/src/ssr/layout-context.ts +17 -0
  474. package/src/ssr/layout-help-markdown.ts +9 -0
  475. package/src/ssr/layout-help-navigation.ts +24 -0
  476. package/src/ssr/layout-help-url.ts +5 -0
  477. package/src/ssr/layout-runtime.ts +22 -0
  478. package/src/ssr/page-error-messages.ts +25 -0
  479. package/src/ssr/platform-messages.ts +135 -0
  480. package/src/ssr/preference-controller.ts +44 -0
  481. package/src/ssr/profile-preferences-messages.ts +29 -0
  482. package/src/ssr/rail-context.ts +21 -0
  483. package/src/ssr/rail-messages.ts +66 -0
  484. package/src/ssr/rail-navigation.ts +60 -0
  485. package/src/ssr/runtime.ts +31 -0
  486. package/src/ssr/url-filter.ts +127 -0
  487. package/src/styles/base-popover.css +36 -0
  488. package/src/styles/effects.css +851 -0
  489. package/src/styles/global.css +268 -0
  490. package/src/styles/input.css +58 -0
  491. package/src/styles/tokens.css +230 -0
  492. package/src/styles/utilities-buttons.css +405 -0
  493. package/src/styles/utilities-data.css +33 -0
  494. package/src/styles/utilities-feedback.css +232 -0
  495. package/src/styles/utilities-layout.css +603 -0
  496. package/src/styles/utilities-markdown-editor.css +525 -0
  497. package/src/styles/utilities-markdown-table.css +234 -0
  498. package/src/styles/utilities-navigation.css +1054 -0
  499. package/src/types/ambient.d.ts +24 -0
  500. package/src/workflows/ai/index.ts +17 -0
  501. package/src/workflows/ai/runtime.ts +286 -0
  502. package/src/workflows/ai/store.ts +314 -0
  503. package/src/workflows/ai/types.ts +111 -0
  504. package/src/workflows/ai-actions.ts +269 -0
  505. package/src/workflows/authoring.ts +88 -0
  506. package/src/workflows/builtins.ts +193 -0
  507. package/src/workflows/contracts.ts +280 -0
  508. package/src/workflows/definition.ts +334 -0
  509. package/src/workflows/editor.ts +113 -0
  510. package/src/workflows/index.ts +71 -0
  511. package/src/workflows/language/binder.ts +45 -0
  512. package/src/workflows/language/canonical.ts +29 -0
  513. package/src/workflows/language/compiler.ts +396 -0
  514. package/src/workflows/language/expressions.ts +34 -0
  515. package/src/workflows/language/index.ts +20 -0
  516. package/src/workflows/language/references.ts +55 -0
  517. package/src/workflows/language/schema.ts +134 -0
  518. package/src/workflows/language/strict-yaml.ts +131 -0
  519. package/src/workflows/manifest.ts +24 -0
  520. package/src/workflows/module.ts +12 -0
  521. package/src/workflows/runtime/coordinator.ts +115 -0
  522. package/src/workflows/runtime/executor.ts +1003 -0
  523. package/src/workflows/runtime/index.ts +60 -0
  524. package/src/workflows/runtime/instant.ts +8 -0
  525. package/src/workflows/runtime/ports.ts +213 -0
  526. package/src/workflows/runtime/schedule.ts +203 -0
  527. package/src/workflows/runtime/trigger.ts +37 -0
  528. package/src/workflows/runtime/wake.ts +13 -0
  529. package/src/workflows/store/actions.ts +438 -0
  530. package/src/workflows/store/budget.ts +145 -0
  531. package/src/workflows/store/definitions.ts +345 -0
  532. package/src/workflows/store/events.ts +383 -0
  533. package/src/workflows/store/index.ts +107 -0
  534. package/src/workflows/store/observability.ts +594 -0
  535. package/src/workflows/store/runs.ts +975 -0
  536. package/src/workflows/store/transaction.ts +23 -0
  537. package/src/workflows/store/worker.ts +439 -0
  538. package/src/workflows/testing/conformance.ts +72 -0
  539. package/src/workflows/testing/index.ts +11 -0
  540. package/src/workflows/testing/process-fixtures.ts +275 -0
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Turns an app's declared actions into something the executor can run.
3
+ *
4
+ * This is what makes "an app brings only actions" true rather than aspirational.
5
+ * Without it, every app would hand-wire the same four things around every
6
+ * external call: resolve the config, charge the budget, mark the effect as
7
+ * started, settle it afterwards. Both apps did, slightly differently, which is
8
+ * how one of them ended up with no budget at all.
9
+ *
10
+ * The budget is charged from the action's own `plan` hook — the same hook a dry
11
+ * run uses. That is the point: preflight and execution cannot disagree about
12
+ * what an action costs, because one function answers for both. Mail's known
13
+ * defect was exactly that divergence, and here it is not a bug to fix but a
14
+ * shape that cannot occur.
15
+ */
16
+ import type { SQL } from "bun";
17
+ import { type WorkflowDependency, type WorkflowJsonValue, type WorkflowStepOutcome, workflowPathKey } from "../contracts";
18
+ import type { ErasedWorkflowAction, WorkflowActionContext } from "../definition";
19
+ import type { DefinedWorkflowModule } from "../module";
20
+ import type {
21
+ WorkflowActionStep,
22
+ WorkflowDryRunActionContext,
23
+ WorkflowDryRunActionPort,
24
+ WorkflowExecuteActionContext,
25
+ WorkflowExecuteActionPort,
26
+ } from "../runtime/ports";
27
+ import { budgetError, budgetRootRunId, chargeWorkflowEffectBudget } from "./budget";
28
+ import { beginWorkflowEffect, readWorkflowEffect, recordWorkflowEffect, settleWorkflowEffect } from "./runs";
29
+ import { withTransaction } from "./transaction";
30
+
31
+ /**
32
+ * The key an idempotent effect deduplicates on.
33
+ *
34
+ * Derived from the run and step rather than generated, so a replay after a
35
+ * crash presents the provider with the same key and gets the same answer
36
+ * instead of performing the work twice.
37
+ */
38
+ export const workflowEffectKey = (runId: string, stepKey: string): string => `workflow:${runId}:step:${stepKey}`;
39
+
40
+ /** Unwinds a transactional action's transaction while keeping how it failed. */
41
+ class WorkflowTransactionalFailure extends Error {
42
+ constructor(readonly failure: { message: string; code?: string; retryable?: boolean }) {
43
+ super(failure.message);
44
+ }
45
+ }
46
+
47
+ /** Rolls a transactional action back before parking its run. */
48
+ class WorkflowTransactionalWaiting extends Error {
49
+ constructor(readonly dependency: WorkflowDependency) {
50
+ super("workflow action is waiting");
51
+ }
52
+ }
53
+
54
+ type StepError = Extract<WorkflowStepOutcome, { state: "failed" }>["error"];
55
+
56
+ const asError = (message: string, retryable = false, code = "WORKFLOW_ACTION_ERROR"): StepError => ({ code, message, retryable });
57
+
58
+ /** Carries the action's own code and retryability into the run, where both are read. */
59
+ const reportedError = (result: { message: string; code?: string; retryable?: boolean }): StepError => ({
60
+ code: result.code ?? "WORKFLOW_ACTION_ERROR",
61
+ message: result.message,
62
+ retryable: result.retryable ?? false,
63
+ });
64
+
65
+ /**
66
+ * Resolves a step's written config into the values the implementation receives.
67
+ *
68
+ * References in the config — `{{ steps.foo.output }}` and friends — are what the
69
+ * workflow language exists for, so they have to be evaluated before the action
70
+ * ever sees them. The action's parameter type is derived from its schema, which
71
+ * is what keeps the two honest.
72
+ */
73
+ const resolveConfig = async (
74
+ ctx: { evaluate(value: WorkflowJsonValue, path?: Array<string | number>): Promise<WorkflowJsonValue> },
75
+ step: WorkflowActionStep,
76
+ ): Promise<Record<string, WorkflowJsonValue>> => {
77
+ const resolved = await ctx.evaluate(step.config as WorkflowJsonValue, [...step.sourcePath, step.action]);
78
+ return (resolved && typeof resolved === "object" && !Array.isArray(resolved) ? resolved : {}) as Record<string, WorkflowJsonValue>;
79
+ };
80
+
81
+ /** The context an action implementation receives, built once per invocation. */
82
+ const actionContext = (
83
+ ctx: WorkflowExecuteActionContext | WorkflowDryRunActionContext,
84
+ step: WorkflowActionStep,
85
+ effectKey: string,
86
+ tx?: SQL,
87
+ ) => {
88
+ // Both `binding` and `resolveReference` are relative to this step's own config,
89
+ // so an implementation names the key it wrote rather than reassembling the
90
+ // plan-wide path the compiler used.
91
+ const configPath = (path: Array<string | number>) => [...step.sourcePath, step.action, ...path];
92
+ return {
93
+ runId: ctx.run.runId,
94
+ stepKey: ctx.step.key,
95
+ invocation: ctx.invocation,
96
+ effectKey,
97
+ ...(tx ? { tx } : {}),
98
+ binding: (...path: Array<string | number>): WorkflowJsonValue | undefined => ctx.plan.bindings[workflowPathKey(configPath(path))],
99
+ resolveReference: (reference: string, ...path: Array<string | number>): Promise<WorkflowJsonValue | undefined> =>
100
+ ctx.resolveReference(reference, configPath(path)),
101
+ variableSnapshot: (): Record<string, WorkflowJsonValue> => ctx.variables.snapshot?.() ?? {},
102
+ heartbeat: async (): Promise<void> => {
103
+ await ctx.heartbeat();
104
+ },
105
+ };
106
+ };
107
+
108
+ /** Refused access is a failure of this step, not of the whole run. */
109
+ const denied = (): WorkflowStepOutcome => ({
110
+ state: "failed",
111
+ error: { code: "FORBIDDEN", message: "not authorized to perform this action", retryable: false },
112
+ });
113
+
114
+ /**
115
+ * Stores a step's output under the name its config gives.
116
+ *
117
+ * A plan refers to an earlier step's result by name, so something has to put it
118
+ * there. Both apps did it inside every action — which meant every action also
119
+ * had to remember to do it again when a replay restored a recorded outcome, and
120
+ * an action that forgot produced a plan that silently resolved to nothing.
121
+ */
122
+ const applySaveAs = (
123
+ ctx: { variables: { set(name: string, value: WorkflowJsonValue): void } },
124
+ step: WorkflowActionStep,
125
+ output: WorkflowJsonValue | undefined,
126
+ ): void => {
127
+ const name = step.config.saveAs;
128
+ if (typeof name === "string" && name && output !== undefined) ctx.variables.set(name, output);
129
+ };
130
+
131
+ /** The step an effect belongs to. Its generation fences a stale worker's writes. */
132
+ export type WorkflowEffectJournalStep = { runId: string; key: string; executionGeneration: number };
133
+
134
+ /**
135
+ * Where the evidence that an effect happened is written.
136
+ *
137
+ * The kernel's own journal is the default and the destination. The seam exists
138
+ * because an app adopts declared actions before its runs live in
139
+ * `workflows.run`, and during that window the evidence has to land in the table
140
+ * that does hold its runs. The ordering guarantees are the port's, not the
141
+ * table's, so they hold either way.
142
+ */
143
+ export type WorkflowEffectJournalPort = {
144
+ /** What an earlier attempt recorded about this step's effect, if anything. */
145
+ read(step: WorkflowEffectJournalStep): Promise<{ key: string; state: string; output: WorkflowJsonValue } | null>;
146
+ /** Marks an effect as started, before it is performed. */
147
+ begin(step: WorkflowEffectJournalStep, effectKey: string): Promise<void>;
148
+ /** Records a completed effect on the handle that performed it, so both commit together. */
149
+ record(tx: SQL, step: WorkflowEffectJournalStep, effectKey: string, output: WorkflowJsonValue): Promise<void>;
150
+ /** Settles an effect once its fate is known. `ambiguous` is a real answer, not a failure. */
151
+ settle(step: WorkflowEffectJournalStep, state: "succeeded" | "ambiguous" | "failed"): Promise<void>;
152
+ };
153
+
154
+ const kernelEffectJournal = (db?: SQL): WorkflowEffectJournalPort => ({
155
+ read: (step) => readWorkflowEffect(step, { ...(db ? { db } : {}) }),
156
+ begin: (step, effectKey) => beginWorkflowEffect(step, effectKey, { ...(db ? { db } : {}) }),
157
+ record: (tx, step, effectKey, output) => recordWorkflowEffect(tx, step, effectKey, output),
158
+ settle: (step, state) => settleWorkflowEffect(step, state, { ...(db ? { db } : {}) }),
159
+ });
160
+
161
+ export type WorkflowActionPortOptions = {
162
+ /** Charged against the root of a fan-out, so children share one allowance. */
163
+ budget?: boolean;
164
+ db?: SQL;
165
+ journal?: WorkflowEffectJournalPort;
166
+ /** App-owned authorization applied to selected shared actions at execution time. */
167
+ authorize?: (context: WorkflowActionContext, step: WorkflowActionStep) => Promise<boolean>;
168
+ };
169
+
170
+ /**
171
+ * Runs one declared action, with everything its effect class implies.
172
+ *
173
+ * The order is the contract: plan, then charge, then mark, then act, then
174
+ * settle. Charging before acting is the only ordering that cannot overspend,
175
+ * and marking before acting is the only one that leaves evidence when the
176
+ * process dies mid-effect.
177
+ */
178
+ const runDeclaredAction = async (
179
+ action: ErasedWorkflowAction,
180
+ ctx: WorkflowExecuteActionContext,
181
+ step: WorkflowActionStep,
182
+ options: WorkflowActionPortOptions,
183
+ ): Promise<WorkflowStepOutcome> => {
184
+ const config = await resolveConfig(ctx, step);
185
+ const journalStep = { runId: ctx.run.runId, key: ctx.step.key, executionGeneration: ctx.run.executionGeneration };
186
+ const effectKey = workflowEffectKey(ctx.run.runId, ctx.step.key);
187
+ const journal = options.journal ?? kernelEffectJournal(options.db);
188
+
189
+ /**
190
+ * Asks the action what it would do, and charges the run for it.
191
+ *
192
+ * Only reached when there is a budget to charge: the answer is otherwise
193
+ * discarded, and `plan` is allowed to be expensive — an HTTP action resolves
194
+ * its target to check it is safe to call.
195
+ */
196
+ const charge = async (db?: SQL): Promise<Extract<WorkflowStepOutcome, { state: "failed" }> | null> => {
197
+ if (options.budget === false || !action.plan) return null;
198
+ const planned = await action.plan(actionContext(ctx, step, effectKey), config as never);
199
+ if (!planned.consumes || Object.keys(planned.consumes).length === 0) return null;
200
+ const handle = db ?? options.db;
201
+ const root = await budgetRootRunId(ctx.run.runId, { ...(handle ? { db: handle } : {}) });
202
+ const charged = await chargeWorkflowEffectBudget(root, planned.consumes, { ...(handle ? { db: handle } : {}) });
203
+ if (charged.state !== "exceeded") return null;
204
+ const error = budgetError(charged);
205
+ return {
206
+ state: "failed",
207
+ error: {
208
+ ...asError(error.message, false, "WORKFLOW_BUDGET_EXCEEDED"),
209
+ details: {
210
+ dimension: error.dimension,
211
+ limit: error.limit,
212
+ used: error.used,
213
+ requested: error.requested,
214
+ },
215
+ },
216
+ };
217
+ };
218
+
219
+ /*
220
+ * An earlier attempt of this step may have escaped without telling us how it
221
+ * ended. Re-running is how the same message goes out twice, so ask the
222
+ * external system instead — that is the entire reason `reconcile` is
223
+ * mandatory for this class.
224
+ *
225
+ * Only ambiguous actions need it. A transactional effect was undone by the
226
+ * crash that interrupted it, and an idempotent one is safe to repeat under
227
+ * the same key.
228
+ */
229
+ if (action.effect === "ambiguous" && action.reconcile) {
230
+ const prior = await journal.read(journalStep);
231
+ if (prior && (prior.state === "executing" || prior.state === "ambiguous")) {
232
+ let verdict: Awaited<ReturnType<NonNullable<typeof action.reconcile>>>;
233
+ try {
234
+ verdict = await action.reconcile(actionContext(ctx, step, prior.key), prior.key);
235
+ } catch (error) {
236
+ return {
237
+ state: "failed",
238
+ error: asError(error instanceof Error ? error.message : String(error), true, "WORKFLOW_EFFECT_RECONCILE_FAILED"),
239
+ };
240
+ }
241
+ if (verdict.state === "succeeded") {
242
+ await journal.settle(journalStep, "succeeded");
243
+ return { state: "completed", output: (verdict.output ?? null) as WorkflowJsonValue };
244
+ }
245
+ if (verdict.state === "failed") {
246
+ await journal.settle(journalStep, "failed");
247
+ return { state: "failed", error: reportedError(verdict) };
248
+ }
249
+ // Still unknown. A human decides; nothing is repeated on their behalf.
250
+ await journal.settle(journalStep, "ambiguous");
251
+ return { state: "needs_attention", error: reportedError(verdict) };
252
+ }
253
+ }
254
+
255
+ /*
256
+ * A transactional action performs its work and records that it happened in
257
+ * one transaction, so a crash leaves neither. A replay therefore finds the
258
+ * recorded outcome and returns it instead of doing the work a second time.
259
+ */
260
+ if (action.effect === "transactional") {
261
+ const prior = await journal.read(journalStep);
262
+ if (prior?.state === "succeeded") return { state: "completed", output: prior.output };
263
+
264
+ return withTransaction(options.db, async (tx) => {
265
+ const txCtx = actionContext(ctx, step, effectKey, tx);
266
+ // Checked on the transaction's own handle: access can be revoked between
267
+ // queueing and running, and a check on another connection is checking a
268
+ // world this write will not see.
269
+ if (options.authorize && !(await options.authorize(txCtx, step))) return denied();
270
+ if (action.authorize && !(await action.authorize(txCtx, config as never))) return denied();
271
+ const overspent = await charge(tx);
272
+ if (overspent) return overspent;
273
+
274
+ const result = await action.run(txCtx, config as never);
275
+ // Let the transaction unwind: the effect did not happen.
276
+ if (result.state === "waiting") throw new WorkflowTransactionalWaiting(result.dependency);
277
+ if (result.state !== "succeeded") throw new WorkflowTransactionalFailure(result);
278
+ await journal.record(tx, journalStep, effectKey, (result.output ?? null) as WorkflowJsonValue);
279
+ return { state: "completed", output: (result.output ?? null) as WorkflowJsonValue } satisfies WorkflowStepOutcome;
280
+ }).catch((error) => {
281
+ if (error instanceof WorkflowTransactionalWaiting)
282
+ return { state: "waiting", dependency: error.dependency } satisfies WorkflowStepOutcome;
283
+ if (error instanceof WorkflowTransactionalFailure)
284
+ return { state: "failed", error: reportedError(error.failure) } satisfies WorkflowStepOutcome;
285
+ throw error;
286
+ });
287
+ }
288
+
289
+ const context = actionContext(ctx, step, effectKey);
290
+ if (options.authorize && !(await options.authorize(context, step))) return denied();
291
+ if (action.authorize && !(await action.authorize(context, config as never))) return denied();
292
+
293
+ if (action.effect !== "pure") {
294
+ // Charged per attempt. A replayed in-flight step charges twice, which is
295
+ // conservative — it can only refuse more, never permit more.
296
+ const overspent = await charge();
297
+ if (overspent) return overspent;
298
+ }
299
+
300
+ // Only an ambiguous effect needs evidence that it started: the others are
301
+ // either safe to repeat or undone by the crash that interrupted them.
302
+ if (action.effect === "ambiguous") await journal.begin(journalStep, effectKey);
303
+
304
+ let result: Awaited<ReturnType<typeof action.run>>;
305
+ try {
306
+ result = await action.run(context, config as never);
307
+ } catch (error) {
308
+ if (action.effect !== "ambiguous") throw error;
309
+ // The exception happened after the external effect was marked executing.
310
+ // Its fate is unknown, so never turn it into a retryable ordinary failure.
311
+ await journal.settle(journalStep, "ambiguous");
312
+ return {
313
+ state: "needs_attention",
314
+ error: asError(error instanceof Error ? error.message : String(error), false, "WORKFLOW_EFFECT_OUTCOME_UNKNOWN"),
315
+ };
316
+ }
317
+
318
+ if (action.effect === "ambiguous") {
319
+ await journal.settle(journalStep, result.state === "succeeded" ? "succeeded" : result.state === "ambiguous" ? "ambiguous" : "failed");
320
+ }
321
+
322
+ switch (result.state) {
323
+ case "succeeded":
324
+ return { state: "completed", output: (result.output ?? null) as WorkflowJsonValue };
325
+ case "failed":
326
+ return { state: "failed", error: reportedError(result) };
327
+ case "waiting":
328
+ return { state: "waiting", dependency: result.dependency };
329
+ case "ambiguous":
330
+ // Not a failure: "the send may have gone through" needs a human, and
331
+ // treating it as failure either loses messages or sends them twice.
332
+ return { state: "needs_attention", error: reportedError(result) };
333
+ }
334
+ throw new Error("workflow action returned an unknown state");
335
+ };
336
+
337
+ /**
338
+ * The port the worker executes with, built from an app's `workflows.ts`.
339
+ *
340
+ * ```ts
341
+ * import { workflows } from "./workflows";
342
+ * tickWorkflows({ worker, appId: "grids", actions: createWorkflowActionPort(workflows) });
343
+ * ```
344
+ */
345
+ export const createWorkflowActionPort = (
346
+ module: DefinedWorkflowModule,
347
+ options: WorkflowActionPortOptions = {},
348
+ ): WorkflowExecuteActionPort => {
349
+ const actions = module.actions;
350
+ return {
351
+ get: (name) => {
352
+ const action = actions[name];
353
+ if (!action) return undefined;
354
+ return {
355
+ execute: async (ctx, step) => {
356
+ const outcome = await runDeclaredAction(action, ctx, step, options);
357
+ if (outcome.state === "completed") applySaveAs(ctx, step, outcome.output);
358
+ return outcome;
359
+ },
360
+ // A restored outcome has to land in the same variable the first attempt
361
+ // would have written, or every step after it resolves to nothing.
362
+ restoreCompleted: (ctx, step, outcome) => applySaveAs(ctx, step, outcome.output),
363
+ };
364
+ },
365
+ };
366
+ };
367
+
368
+ /**
369
+ * The port a dry run executes with, built from the same declarations.
370
+ *
371
+ * A dry run and a preflight are the same operation: run the plan with impure
372
+ * steps reporting what they *would* do. Because both come from one `plan` hook,
373
+ * what a dry run promises and what execution charges cannot drift apart.
374
+ *
375
+ * A pure action is simply run — it is deterministic and touches nothing, which
376
+ * is what makes its dry run exact rather than a description.
377
+ */
378
+ export const createWorkflowDryRunPort = (module: DefinedWorkflowModule): WorkflowDryRunActionPort => {
379
+ const actions = module.actions;
380
+ return {
381
+ get: (name) => {
382
+ const action = actions[name];
383
+ if (!action) return undefined;
384
+ return {
385
+ restoreCompleted: (ctx, step, outcome) => applySaveAs(ctx, step, outcome.output),
386
+ plan: async (ctx: WorkflowDryRunActionContext, step: WorkflowActionStep) => {
387
+ const config = await resolveConfig(ctx, step);
388
+ const context = actionContext(ctx, step, workflowEffectKey(ctx.run.runId, ctx.step.key));
389
+
390
+ if (action.effect === "pure") {
391
+ const result = await action.run(context, config as never);
392
+ if (result.state === "waiting") {
393
+ return {
394
+ state: "indeterminate" as const,
395
+ reason: `dependency ${result.dependency.kind}:${result.dependency.key} is not satisfied`,
396
+ };
397
+ }
398
+ if (result.state !== "succeeded") {
399
+ return { state: "terminal" as const, status: "failed" as const, message: result.message, effects: [] };
400
+ }
401
+ applySaveAs(ctx, step, (result.output ?? undefined) as WorkflowJsonValue | undefined);
402
+ return { state: "planned" as const, output: (result.output ?? null) as WorkflowJsonValue, effects: [] };
403
+ }
404
+
405
+ if (!action.plan) return { state: "unsupported" as const, reason: `${name} cannot be planned` };
406
+
407
+ const planned = await action.plan(context, config as never);
408
+ applySaveAs(ctx, step, (planned.output ?? undefined) as WorkflowJsonValue | undefined);
409
+ return {
410
+ state: "planned" as const,
411
+ output: (planned.output ?? null) as WorkflowJsonValue,
412
+ effects: [
413
+ { action: name, summary: planned.summary, ...(planned.consumes ? { consumes: planned.consumes } : {}) } as WorkflowJsonValue,
414
+ ],
415
+ // An issue names the step it belongs to, so the dry-run view can point
416
+ // at what could not be determined rather than listing loose strings.
417
+ ...(planned.issues?.length
418
+ ? {
419
+ issues: planned.issues.map((reason) => ({
420
+ state: "indeterminate" as const,
421
+ reason,
422
+ step: {
423
+ key: ctx.step.key,
424
+ sourcePath: ctx.step.sourcePath,
425
+ iterationPath: ctx.step.iterationPath,
426
+ path: ctx.step.path,
427
+ kind: ctx.step.kind,
428
+ ...(ctx.step.action ? { action: ctx.step.action } : {}),
429
+ },
430
+ })),
431
+ }
432
+ : {}),
433
+ };
434
+ },
435
+ };
436
+ },
437
+ };
438
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Effect budgets.
3
+ *
4
+ * A dry run and a preflight are the same operation: execute the plan with
5
+ * impure steps reporting what they *would* do instead of doing it. Preflight is
6
+ * that, with the reported effects counted against a cap.
7
+ *
8
+ * Grids had no cap at all — a workflow with an email action inside a loop over
9
+ * ten thousand records was bounded only by `maxLoopItems`, which limits
10
+ * iterations, not messages. Mail had budgets but checked them once, before
11
+ * execution, so a run could be approved for one set of effects and go on to
12
+ * perform a different one.
13
+ *
14
+ * Hence the rule here: the budget is charged at the moment of the effect, not
15
+ * only ahead of it. That makes the preflight/execution divergence harmless
16
+ * rather than something a hash has to detect — whatever the plan turns out to
17
+ * do, it still cannot exceed what was approved.
18
+ */
19
+ import { type SQL, sql } from "bun";
20
+ import { withTransaction } from "./transaction";
21
+
22
+ /** Caps keyed by dimension — `{ emails: 100, httpRequests: 50 }`. */
23
+ export type WorkflowEffectBudget = Record<string, number>;
24
+
25
+ /** What one step reports consuming, matching an action's `plan` hook. */
26
+ export type WorkflowEffectCharge = Record<string, number>;
27
+
28
+ export type WorkflowBudgetOutcome =
29
+ | { state: "ok"; used: WorkflowEffectBudget }
30
+ | { state: "exceeded"; dimension: string; limit: number; used: number; requested: number };
31
+
32
+ /** The error a budget rejection settles a run with — structured, so the UI can badge it. */
33
+ export type WorkflowBudgetError = {
34
+ kind: "budget_exceeded";
35
+ dimension: string;
36
+ limit: number;
37
+ used: number;
38
+ requested: number;
39
+ message: string;
40
+ };
41
+
42
+ /** Refuses values JSON accepts but arithmetic cannot safely enforce. */
43
+ export const validateWorkflowEffectBudget = (value: Record<string, number>, label = "workflow effect budget"): void => {
44
+ for (const [dimension, amount] of Object.entries(value)) {
45
+ if (!dimension || !Number.isFinite(amount) || amount < 0) {
46
+ throw new TypeError(`${label} ${JSON.stringify(dimension)} must be a finite non-negative number`);
47
+ }
48
+ }
49
+ };
50
+
51
+ export const budgetError = (outcome: Extract<WorkflowBudgetOutcome, { state: "exceeded" }>): WorkflowBudgetError => ({
52
+ kind: "budget_exceeded",
53
+ dimension: outcome.dimension,
54
+ limit: outcome.limit,
55
+ used: outcome.used,
56
+ requested: outcome.requested,
57
+ message: `effect budget for ${outcome.dimension} exhausted: ${outcome.used} of ${outcome.limit} used, ${outcome.requested} more requested`,
58
+ });
59
+
60
+ /** Adds up what a dry run said it would do, so preflight can compare it to the cap. */
61
+ export const totalPlannedEffects = (planned: readonly { consumes?: WorkflowEffectCharge }[]): WorkflowEffectBudget => {
62
+ const total: WorkflowEffectBudget = {};
63
+ for (const effect of planned) {
64
+ validateWorkflowEffectBudget(effect.consumes ?? {}, "workflow effect charge");
65
+ for (const [dimension, amount] of Object.entries(effect.consumes ?? {})) {
66
+ total[dimension] = (total[dimension] ?? 0) + amount;
67
+ }
68
+ }
69
+ return total;
70
+ };
71
+
72
+ /** Whether a planned total fits, without touching the database. Used by preflight. */
73
+ export const checkEffectBudget = (budget: WorkflowEffectBudget, planned: WorkflowEffectBudget): WorkflowBudgetOutcome => {
74
+ validateWorkflowEffectBudget(budget);
75
+ validateWorkflowEffectBudget(planned, "planned workflow effect charge");
76
+ for (const [dimension, requested] of Object.entries(planned)) {
77
+ const limit = budget[dimension];
78
+ if (limit !== undefined && requested > limit) {
79
+ return { state: "exceeded", dimension, limit, used: 0, requested };
80
+ }
81
+ }
82
+ return { state: "ok", used: planned };
83
+ };
84
+
85
+ /**
86
+ * Charges a run's budget for an effect it is about to perform.
87
+ *
88
+ * One statement, so two workers on child runs of the same parent cannot both
89
+ * read "99 of 100 used" and both proceed. A dimension the budget does not
90
+ * mention is uncapped and still counted, because the count is what the operator
91
+ * looks at afterwards.
92
+ */
93
+ export const chargeWorkflowEffectBudget = async (
94
+ runId: string,
95
+ charge: WorkflowEffectCharge,
96
+ options: { db?: SQL } = {},
97
+ ): Promise<WorkflowBudgetOutcome> => {
98
+ validateWorkflowEffectBudget(charge, "workflow effect charge");
99
+ if (Object.keys(charge).length === 0) return { state: "ok", used: {} };
100
+ return withTransaction(options.db, async (tx) => {
101
+ const [row] = await tx<{ effects_used: WorkflowEffectBudget; effect_budget: WorkflowEffectBudget }[]>`
102
+ SELECT r.effects_used, v.effect_budget
103
+ FROM workflows.run AS r
104
+ JOIN workflows.version AS v ON v.id = r.workflow_version_id
105
+ WHERE r.id = ${runId}::uuid
106
+ FOR UPDATE OF r
107
+ `;
108
+ if (!row) throw new Error(`workflow run ${runId} does not exist`);
109
+ validateWorkflowEffectBudget(row.effect_budget);
110
+ validateWorkflowEffectBudget(row.effects_used, "workflow effects used");
111
+
112
+ const next: WorkflowEffectBudget = { ...row.effects_used };
113
+ for (const [dimension, amount] of Object.entries(charge)) {
114
+ const used = next[dimension] ?? 0;
115
+ const limit = row.effect_budget[dimension];
116
+ if (limit !== undefined && used + amount > limit) {
117
+ return { state: "exceeded", dimension, limit, used, requested: amount };
118
+ }
119
+ next[dimension] = used + amount;
120
+ }
121
+
122
+ await tx`UPDATE workflows.run SET effects_used = ${next}, updated_at = now() WHERE id = ${runId}::uuid`;
123
+ return { state: "ok", used: next };
124
+ });
125
+ };
126
+
127
+ /**
128
+ * Charges the *parent* when a run is one of many children.
129
+ *
130
+ * A budget that only applied per child would be no budget at all: fanning out
131
+ * over ten thousand records would authorise ten thousand times the cap. The
132
+ * root run of a fan-out owns the allowance.
133
+ */
134
+ export const budgetRootRunId = async (runId: string, options: { db?: SQL } = {}): Promise<string> => {
135
+ const db = options.db ?? sql;
136
+ const [row] = await db<{ root: string }[]>`
137
+ WITH RECURSIVE ancestry AS (
138
+ SELECT id, parent_run_id FROM workflows.run WHERE id = ${runId}::uuid
139
+ UNION ALL
140
+ SELECT r.id, r.parent_run_id FROM workflows.run AS r JOIN ancestry AS a ON r.id = a.parent_run_id
141
+ )
142
+ SELECT id AS root FROM ancestry WHERE parent_run_id IS NULL
143
+ `;
144
+ return row?.root ?? runId;
145
+ };