@k2b/cloud 0.6.0-rc.0 → 0.7.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 (315) hide show
  1. package/README.md +2 -2
  2. package/package.json +30 -17
  3. package/scripts/browser-performance.ts +14 -0
  4. package/scripts/build-pdf-renderer.ts +39 -0
  5. package/scripts/build.ts +15 -35
  6. package/scripts/preload.ts +9 -12
  7. package/scripts/sync-recovery-smoke.ts +2 -1
  8. package/src/_internal/app-presentation.ts +14 -2
  9. package/src/_internal/capabilities.ts +108 -7
  10. package/src/_internal/capability-streams.ts +81 -0
  11. package/src/_internal/define-app.ts +62 -36
  12. package/src/_internal/heartbeat.ts +4 -0
  13. package/src/_internal/help-catalog.ts +27 -172
  14. package/src/_internal/help.ts +13 -76
  15. package/src/_internal/page-responses.ts +2 -0
  16. package/src/_internal/postgres-application-name.ts +30 -0
  17. package/src/_internal/process-identity.ts +15 -0
  18. package/src/_internal/process-sync.ts +1 -1
  19. package/src/_internal/registry-validation.ts +29 -25
  20. package/src/_internal/registry.ts +2 -53
  21. package/src/_internal/runtime-context.ts +5 -16
  22. package/src/_internal/server-timing.ts +13 -0
  23. package/src/_internal/status-preserving-ssr.ts +18 -6
  24. package/src/_internal/web-vitals-asset.ts +10 -0
  25. package/src/access/PermissionEditor.tsx +14 -153
  26. package/src/access/PrincipalPicker.tsx +104 -0
  27. package/src/access/ui.ts +2 -0
  28. package/src/ai/admin.ts +14 -13
  29. package/src/ai/assistant-models.ts +12 -1
  30. package/src/ai/audio-format.ts +34 -0
  31. package/src/ai/audio-tool.ts +90 -0
  32. package/src/ai/browser-code-contracts.ts +79 -0
  33. package/src/ai/browser.ts +2 -0
  34. package/src/ai/capabilities.ts +55 -36
  35. package/src/ai/capability-execution.ts +47 -12
  36. package/src/ai/chat/blocks.tsx +157 -95
  37. package/src/ai/chat/capability-result.ts +15 -0
  38. package/src/ai/chat/capability-table.tsx +2 -10
  39. package/src/ai/chat/composer-adapter.ts +48 -3
  40. package/src/ai/chat/message-actions.tsx +8 -7
  41. package/src/ai/chat/message-utils.ts +12 -1
  42. package/src/ai/chat/messages.ts +2 -0
  43. package/src/ai/chat/presentation.tsx +19 -48
  44. package/src/ai/chat/primitives.tsx +43 -1
  45. package/src/ai/chat/tool-disclosure.tsx +17 -2
  46. package/src/ai/chat/tool-groups.ts +52 -0
  47. package/src/ai/chat-quotas.ts +25 -0
  48. package/src/ai/chat-task-contracts.ts +9 -1
  49. package/src/ai/chat-tasks.ts +276 -43
  50. package/src/ai/client/controller.ts +110 -33
  51. package/src/ai/client/file-source.ts +12 -6
  52. package/src/ai/client/live-connection.ts +1 -0
  53. package/src/ai/client/projection.ts +22 -11
  54. package/src/ai/client/transport.ts +4 -0
  55. package/src/ai/code-capability-routes.ts +110 -0
  56. package/src/ai/code-capability-transport.ts +38 -0
  57. package/src/ai/code-mode-skill.ts +114 -0
  58. package/src/ai/code-runtime-tools.ts +166 -0
  59. package/src/ai/code-source-contracts.ts +202 -0
  60. package/src/ai/code-source-tools.ts +145 -0
  61. package/src/ai/compaction.ts +27 -6
  62. package/src/ai/data-analysis-skill.ts +10 -0
  63. package/src/ai/default-tools.ts +25 -2
  64. package/src/ai/dictation-runtime.ts +171 -0
  65. package/src/ai/dictations.ts +188 -0
  66. package/src/ai/draft-content.ts +34 -0
  67. package/src/ai/enrich.ts +6 -0
  68. package/src/ai/executor.ts +191 -31
  69. package/src/ai/file-content-version.ts +22 -0
  70. package/src/ai/file-context.ts +2 -1
  71. package/src/ai/file-media-type.ts +12 -0
  72. package/src/ai/file-reference-contracts.ts +15 -0
  73. package/src/ai/file-tools.ts +2 -1
  74. package/src/ai/files-store.ts +174 -71
  75. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  76. package/src/ai/grids-skill.ts +40 -10
  77. package/src/ai/http.ts +15 -8
  78. package/src/ai/index.ts +26 -2
  79. package/src/ai/inference-calls.ts +198 -0
  80. package/src/ai/live-events.ts +5 -1
  81. package/src/ai/memory-learning.ts +60 -28
  82. package/src/ai/message-queue.ts +167 -0
  83. package/src/ai/migrate.ts +187 -11
  84. package/src/ai/model-pricing.ts +43 -0
  85. package/src/ai/pdf-render-worker.ts +74 -0
  86. package/src/ai/pdf-render.fixture.ts +28 -0
  87. package/src/ai/pdf-render.ts +77 -0
  88. package/src/ai/projects-routes.ts +3 -3
  89. package/src/ai/projects.ts +20 -6
  90. package/src/ai/protocol.ts +3 -0
  91. package/src/ai/provider.ts +1 -8
  92. package/src/ai/quota-provider.ts +191 -0
  93. package/src/ai/quota-report.ts +152 -0
  94. package/src/ai/quotas-migrate.ts +45 -0
  95. package/src/ai/quotas.ts +168 -0
  96. package/src/ai/routes.ts +215 -11
  97. package/src/ai/run-timeout.ts +10 -0
  98. package/src/ai/runtime-tools.ts +3 -0
  99. package/src/ai/runtime.ts +55 -16
  100. package/src/ai/settings.ts +36 -8
  101. package/src/ai/short-id.ts +2 -2
  102. package/src/ai/skill-catalog.ts +0 -10
  103. package/src/ai/skill-search.ts +47 -0
  104. package/src/ai/skill-seeds.ts +105 -20
  105. package/src/ai/skill-tool.ts +16 -5
  106. package/src/ai/skills-routes.ts +32 -4
  107. package/src/ai/skills.ts +324 -155
  108. package/src/ai/store.ts +373 -191
  109. package/src/ai/stream.ts +5 -0
  110. package/src/ai/structured-runs.ts +4 -3
  111. package/src/ai/structured.ts +8 -1
  112. package/src/ai/system-prompt.ts +12 -2
  113. package/src/ai/timeline.ts +2 -5
  114. package/src/ai/todo-contracts.ts +20 -0
  115. package/src/ai/todo-tool.ts +16 -0
  116. package/src/ai/tool-audit.ts +30 -40
  117. package/src/ai/tools.ts +6 -0
  118. package/src/ai/transcription.ts +202 -0
  119. package/src/ai/turn-timing.ts +114 -0
  120. package/src/ai/types.ts +58 -10
  121. package/src/ai/ui.tsx +2 -0
  122. package/src/ai/usage.ts +118 -118
  123. package/src/ai/vision-tool.ts +39 -10
  124. package/src/api/admin-ai-projects.ts +2 -2
  125. package/src/api/admin-ai-quotas.ts +74 -0
  126. package/src/api/admin-ai-skills.ts +48 -5
  127. package/src/api/admin-ai-usage.ts +2 -2
  128. package/src/api/admin-core-settings.ts +25 -0
  129. package/src/api/admin-identity.ts +91 -51
  130. package/src/api/admin-rail.ts +58 -0
  131. package/src/api/announcements.ts +14 -0
  132. package/src/api/capabilities.ts +292 -43
  133. package/src/api/capability-streams.ts +262 -0
  134. package/src/api/help.ts +8 -44
  135. package/src/api/index.ts +6 -0
  136. package/src/api/mcp.ts +44 -26
  137. package/src/api/me-web-vitals.ts +35 -0
  138. package/src/api/me.ts +2 -0
  139. package/src/api/search/schemas.ts +26 -15
  140. package/src/api/search.bench.ts +1 -1
  141. package/src/api/search.ts +54 -17
  142. package/src/browser/CloudResourceSearch.tsx +640 -702
  143. package/src/browser/app-approval-vault.ts +19 -7
  144. package/src/browser/command-bridge.ts +109 -0
  145. package/src/browser/command-shortcuts.ts +76 -0
  146. package/src/browser/commands.ts +66 -0
  147. package/src/browser/navigation-search.ts +48 -0
  148. package/src/browser/notifications.ts +1 -1
  149. package/src/browser/resource-picker.tsx +5 -18
  150. package/src/browser/resource-search-dialog.ts +9 -0
  151. package/src/browser/resource-search-input.ts +46 -0
  152. package/src/browser/resource-search-messages.ts +76 -40
  153. package/src/browser/resource-search.ts +7 -0
  154. package/src/browser/search-bridge.ts +102 -0
  155. package/src/browser/search-commands.ts +59 -0
  156. package/src/browser/search.ts +13 -0
  157. package/src/browser/spotlight-position.ts +208 -0
  158. package/src/browser/testing.ts +5 -0
  159. package/src/browser/web-vitals.ts +25 -0
  160. package/src/capabilities/claims.ts +179 -0
  161. package/src/capabilities/client.ts +34 -0
  162. package/src/capabilities/command-link.ts +16 -0
  163. package/src/capabilities/executions.ts +298 -0
  164. package/src/capabilities/migrate.ts +81 -0
  165. package/src/capabilities/server.ts +114 -34
  166. package/src/capabilities/stream-body.ts +16 -0
  167. package/src/capabilities/streams.ts +28 -0
  168. package/src/cli/admin/ai-quotas.ts +161 -0
  169. package/src/cli/admin/ai-skills.ts +64 -0
  170. package/src/cli/admin/ai-usage.ts +53 -30
  171. package/src/cli/admin/app-credentials.ts +79 -0
  172. package/src/cli/admin/data.ts +17 -0
  173. package/src/cli/admin/index.ts +9 -0
  174. package/src/cli/capabilities.ts +29 -0
  175. package/src/cli/capability-streams.ts +25 -0
  176. package/src/cli/index.ts +3 -2
  177. package/src/config/env.ts +11 -8
  178. package/src/contracts/app.ts +23 -3
  179. package/src/contracts/capabilities.ts +68 -5
  180. package/src/contracts/capability-streams.ts +46 -0
  181. package/src/contracts/commands.ts +49 -0
  182. package/src/contracts/index.ts +6 -1
  183. package/src/contracts/rail-admin.ts +38 -0
  184. package/src/contracts/rail-preferences.ts +16 -0
  185. package/src/contracts/registry.ts +3 -27
  186. package/src/contracts/settings-types.ts +1 -1
  187. package/src/contracts/shared.ts +9 -5
  188. package/src/contracts/web-vitals.ts +27 -0
  189. package/src/desktop/solid.tsx +3 -3
  190. package/src/index.ts +0 -4
  191. package/src/server/index.ts +3 -0
  192. package/src/server/middleware/auth.ts +3 -2
  193. package/src/server/middleware/route-template.ts +9 -3
  194. package/src/server/middleware/runtime.ts +2 -1
  195. package/src/server/middleware/settings.ts +17 -11
  196. package/src/server/services/access-revision.ts +10 -0
  197. package/src/server/services/access.ts +1 -1
  198. package/src/server/services/index.ts +1 -0
  199. package/src/services/account-category-policy.ts +9 -0
  200. package/src/services/account-lifecycle/audit.ts +1 -1
  201. package/src/services/accounts/app.ts +12 -2
  202. package/src/services/accounts/email-write.ts +23 -0
  203. package/src/services/accounts/groups.ts +15 -4
  204. package/src/services/accounts/identities.ts +249 -0
  205. package/src/services/accounts/identity-reconciliation.ts +87 -0
  206. package/src/services/accounts/ipa-data.ts +2 -2
  207. package/src/services/accounts/local-groups.ts +2 -2
  208. package/src/services/accounts/posix.ts +45 -29
  209. package/src/services/accounts/users.ts +2 -2
  210. package/src/services/announcements/index.ts +69 -8
  211. package/src/services/audit/index.ts +1 -1
  212. package/src/services/cache-fill.ts +35 -0
  213. package/src/services/help/index.ts +109 -0
  214. package/src/services/help/maintenance.ts +25 -0
  215. package/src/services/help/store.ts +180 -0
  216. package/src/services/help/types.ts +46 -0
  217. package/src/services/identity/invocation-actor.ts +2 -2
  218. package/src/services/identity/key-config.ts +2 -1
  219. package/src/services/identity/key-ring.ts +1 -1
  220. package/src/services/index.ts +15 -0
  221. package/src/services/ipa/users.ts +268 -221
  222. package/src/services/legal-consent.ts +1 -1
  223. package/src/services/logging/index.ts +15 -13
  224. package/src/services/logging/trace.ts +2 -1
  225. package/src/services/logging/web-vitals.ts +81 -0
  226. package/src/services/mandates/index.ts +18 -4
  227. package/src/services/mandates/policy.ts +65 -2
  228. package/src/services/notifications/batches.ts +124 -57
  229. package/src/services/notifications/browser.ts +12 -0
  230. package/src/services/notifications/index.ts +0 -2
  231. package/src/services/notifications/platform.ts +0 -4
  232. package/src/services/oauth-tokens.ts +3 -3
  233. package/src/services/pdf/gotenberg.ts +107 -84
  234. package/src/services/pdf/index.ts +3 -0
  235. package/src/services/providers/local/users.ts +25 -16
  236. package/src/services/public-http.ts +101 -0
  237. package/src/services/rail-shortcuts.ts +95 -0
  238. package/src/services/rail-snapshot.ts +53 -0
  239. package/src/services/request-cache-redis.ts +28 -0
  240. package/src/services/session/index.ts +15 -2
  241. package/src/services/session/user.ts +62 -37
  242. package/src/services/settings/core-settings.ts +21 -28
  243. package/src/services/settings/defaults.ts +5 -0
  244. package/src/services/settings/store.ts +58 -84
  245. package/src/shared/ai-costs.ts +42 -0
  246. package/src/shared/ai-platform-prompt.ts +1 -1
  247. package/src/shared/ai-quotas.ts +136 -0
  248. package/src/shared/ai-usage.ts +6 -16
  249. package/src/shared/app-presentation.ts +21 -15
  250. package/src/shared/capability-messages.ts +14 -0
  251. package/src/shared/help.ts +6 -42
  252. package/src/shared/index.ts +6 -0
  253. package/src/ssr/AdminLayout.tsx +7 -5
  254. package/src/ssr/AdminSidebar.tsx +29 -15
  255. package/src/ssr/AppLaunchpad.island.tsx +24 -137
  256. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  257. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  258. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  259. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  260. package/src/ssr/Layout.tsx +17 -6
  261. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  262. package/src/ssr/LayoutFooter.tsx +1 -1
  263. package/src/ssr/LayoutHeader.tsx +46 -14
  264. package/src/ssr/LayoutHelp.tsx +23 -22
  265. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  266. package/src/ssr/LayoutRail.tsx +17 -9
  267. package/src/ssr/MobileNavigation.tsx +71 -0
  268. package/src/ssr/MobileProfileActions.tsx +49 -0
  269. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  270. package/src/ssr/RailApps.island.tsx +2 -2
  271. package/src/ssr/RailEditor.tsx +17 -2
  272. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  273. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  274. package/src/ssr/admin-navigation.ts +7 -1
  275. package/src/ssr/help.ts +29 -0
  276. package/src/ssr/islands/SearchBar.tsx +67 -0
  277. package/src/ssr/islands/index.ts +3 -1
  278. package/src/ssr/layout-context.ts +2 -2
  279. package/src/ssr/layout-help-search.ts +15 -0
  280. package/src/ssr/mobile-menu-history.ts +70 -0
  281. package/src/ssr/platform-messages.ts +16 -8
  282. package/src/ssr/profile-actions.ts +42 -0
  283. package/src/ssr/rail-context.ts +8 -4
  284. package/src/ssr/rail-messages.ts +2 -0
  285. package/src/ssr/rail-navigation.ts +3 -3
  286. package/src/ssr/workspace-navigation.ts +75 -0
  287. package/src/styles/effects.css +6 -6
  288. package/src/styles/global.css +54 -0
  289. package/src/styles/input.css +43 -41
  290. package/src/styles/resource-search.css +505 -0
  291. package/src/styles/utilities-feedback.css +13 -0
  292. package/src/styles/utilities-layout.css +1 -5
  293. package/src/styles/utilities-navigation.css +3 -39
  294. package/src/types/ambient.d.ts +5 -0
  295. package/src/workflows/ai/runtime.ts +5 -1
  296. package/src/workflows/ai/store.ts +2 -0
  297. package/src/workflows/ai-actions.ts +16 -13
  298. package/src/workflows/definition.ts +10 -5
  299. package/src/workflows/store/actions.ts +44 -20
  300. package/src/workflows/store/index.ts +2 -0
  301. package/src/workflows/store/runs.ts +36 -0
  302. package/src/workflows/store/worker-pool.ts +55 -0
  303. package/src/workflows/store/worker-runtime.ts +90 -0
  304. package/scripts/app-favicon.test.ts +0 -49
  305. package/src/ai/chat/blocks.render.test.tsx +0 -1108
  306. package/src/ai/kit-skill.ts +0 -22
  307. package/src/ai/usage-migrate.ts +0 -81
  308. package/src/browser/CloudResourceSearch.behavior.test.tsx +0 -74
  309. package/src/browser/notification-target.ts +0 -27
  310. package/src/contracts/notification-live.ts +0 -66
  311. package/src/services/notifications/live.ts +0 -50
  312. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  313. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +0 -96
  314. package/src/ssr/Layout.render.test.tsx +0 -148
  315. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -11,6 +11,8 @@ import {
11
11
  type CapabilityActionReviewResult,
12
12
  CapabilityActionReviewSchema,
13
13
  type CapabilityDefinitions,
14
+ type CapabilityCommandManifest,
15
+ CapabilityCommandManifestSchema,
14
16
  type CapabilityError,
15
17
  CapabilityErrorSchema,
16
18
  type CapabilityExecutionContext,
@@ -190,12 +192,14 @@ const compileSchemaPresentation = (
190
192
 
191
193
  const compileOperationPresentation = (
192
194
  value: unknown,
193
- operation: CapabilityQueryManifest | CapabilityActionManifest,
195
+ operation: CapabilityQueryManifest | CapabilityActionManifest | CapabilityCommandManifest,
194
196
  label: string,
195
197
  ): CapabilityOperationPresentationTranslation => {
196
198
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
197
199
  const translation = value as Record<string, unknown>;
198
- const allowed = new Set(["title", "description", "input", "data", "searchTags"]);
200
+ const allowed = new Set(
201
+ "path" in operation ? ["title", "description", "input"] : ["title", "description", "input", "data", "searchTags"],
202
+ );
199
203
  const extra = Object.keys(translation).find((key) => !allowed.has(key));
200
204
  if (extra) throw new Error(`${label} contains unsupported field "${extra}"`);
201
205
  if (translation.title !== undefined) {
@@ -238,7 +242,7 @@ const compileOperationPresentation = (
238
242
  );
239
243
  }
240
244
  const input = compileSchemaPresentation(translation.input, operation.inputSchema, `${label}.input`);
241
- const data = compileSchemaPresentation(translation.data, operation.dataSchema, `${label}.data`);
245
+ const data = "dataSchema" in operation ? compileSchemaPresentation(translation.data, operation.dataSchema, `${label}.data`) : undefined;
242
246
  return {
243
247
  ...(typeof translation.title === "string" ? { title: translation.title.trim() } : {}),
244
248
  ...(typeof translation.description === "string" ? { description: translation.description.trim() } : {}),
@@ -263,6 +267,7 @@ export const compileCapabilityPresentation = (manifest: CapabilityManifest, valu
263
267
  const types = new Map(manifest.types.map((type) => [type.localId, type]));
264
268
  const queries = new Map(manifest.queries.map((operation) => [operation.localId, operation]));
265
269
  const actions = new Map(manifest.actions.map((operation) => [operation.localId, operation]));
270
+ const commands = new Map(manifest.commands.map((operation) => [operation.localId, operation]));
266
271
  const translations: Record<string, CapabilityPresentationTranslation> = {};
267
272
  for (const [locale, rawTranslation] of Object.entries(catalog.translations as Record<string, unknown>)) {
268
273
  const canonical = canonicalLocale(locale);
@@ -273,7 +278,7 @@ export const compileCapabilityPresentation = (manifest: CapabilityManifest, valu
273
278
  throw new Error(`Capability presentation translation ${canonical} must be an object`);
274
279
  }
275
280
  const raw = rawTranslation as Record<string, unknown>;
276
- const extra = Object.keys(raw).find((key) => key !== "types" && key !== "queries" && key !== "actions");
281
+ const extra = Object.keys(raw).find((key) => key !== "types" && key !== "queries" && key !== "actions" && key !== "commands");
277
282
  if (extra) throw new Error(`Capability presentation translation ${canonical} contains unsupported field "${extra}"`);
278
283
  const compileGroup = <T>(
279
284
  group: unknown,
@@ -311,10 +316,12 @@ export const compileCapabilityPresentation = (manifest: CapabilityManifest, valu
311
316
  });
312
317
  const translatedQueries = compileGroup(raw.queries, queries, "Query", compileOperationPresentation);
313
318
  const translatedActions = compileGroup(raw.actions, actions, "Action", compileOperationPresentation);
319
+ const translatedCommands = compileGroup(raw.commands, commands, "Command", compileOperationPresentation);
314
320
  translations[canonical] = {
315
321
  ...(translatedTypes ? { types: translatedTypes } : {}),
316
322
  ...(translatedQueries ? { queries: translatedQueries } : {}),
317
323
  ...(translatedActions ? { actions: translatedActions } : {}),
324
+ ...(translatedCommands ? { commands: translatedCommands } : {}),
318
325
  };
319
326
  }
320
327
  return { baseLocale, translations };
@@ -406,7 +413,7 @@ const normalizeSearchTags = (definition: CapabilityQueryDefinition, label: strin
406
413
  ...(aliases.length > 0 ? { aliases } : {}),
407
414
  };
408
415
  });
409
- return { tags };
416
+ return { tags, ...(definition.universalSearch.scopeTypes ? { scopeTypes: [...new Set(definition.universalSearch.scopeTypes)] } : {}) };
410
417
  };
411
418
 
412
419
  const compileOperationSchemas = (definition: CapabilityQueryDefinition | CapabilityActionDefinition, label: string) => {
@@ -426,7 +433,7 @@ const compileOperationSchemas = (definition: CapabilityQueryDefinition | Capabil
426
433
  inputSchema,
427
434
  dataSchema,
428
435
  resultZodSchema,
429
- schemaHash: capabilityHash({ inputSchema, dataSchema }),
436
+ schemaHash: capabilityHash({ inputSchema, dataSchema, ...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}) }),
430
437
  };
431
438
  };
432
439
 
@@ -462,6 +469,8 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
462
469
  for (const [localId, definition] of Object.entries(definitions.queries ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
463
470
  registerLocalId(localId, "Query");
464
471
  const label = `Query ${localId}`;
472
+ if (definition.stream && ((definition.stream.direction === "write" && !("idempotency" in definition)) || definition.stream.maxBytes <= 0)) throw new Error(`${label}: write streams require an Action and a positive byte budget`);
473
+ if (definition.stream?.direction === "write" && "idempotency" in definition && definition.idempotency !== "required") throw new Error(`${label}: write streams require idempotency`);
465
474
  const schemas = compileOperationSchemas(definition, label);
466
475
  if (definition.universalSearch) {
467
476
  const expectedInput = projectSchema(UniversalSearchInputSchema, "Universal Search input", "input");
@@ -480,9 +489,13 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
480
489
  inputSchema: schemas.inputSchema,
481
490
  dataSchema: schemas.dataSchema,
482
491
  schemaHash: schemas.schemaHash,
492
+ ...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
483
493
  openWorld: definition.openWorld,
484
494
  universalSearch: normalizeSearchTags(definition, label),
485
495
  } satisfies CapabilityQueryManifest;
496
+ for (const type of manifest.universalSearch?.scopeTypes ?? []) {
497
+ if (!typeIds.has(qualifiedId(appId, type))) throw new Error(`${label} declares an unknown search scope type ${type}`);
498
+ }
486
499
  queries.set(localId, {
487
500
  definition,
488
501
  manifest,
@@ -499,12 +512,36 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
499
512
  for (const [localId, definition] of Object.entries(definitions.actions ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
500
513
  registerLocalId(localId, "Action");
501
514
  const label = `Action ${localId}`;
515
+ // Declaration rule, documented in docs/platform/capabilities.md:
516
+ // destructive means irreversible or externally visible. An irreversible or
517
+ // open-world effect must always be reviewable and safely repeatable, and
518
+ // must never be approved once and then replayed silently.
519
+ if (definition.destructive && !definition.review) {
520
+ throw new Error(`${label} is destructive and must declare review()`);
521
+ }
522
+ if (definition.destructive && definition.idempotency !== "required") {
523
+ throw new Error(`${label} is destructive and must declare idempotency: "required"`);
524
+ }
525
+ if (definition.openWorld && !definition.review) {
526
+ throw new Error(`${label} is open-world and must declare review()`);
527
+ }
528
+ if (definition.openWorld && definition.idempotency !== "required") {
529
+ throw new Error(`${label} is open-world and must declare idempotency: "required"`);
530
+ }
502
531
  if (definition.approval === "rememberable" && definition.openWorld) {
503
532
  throw new Error(`${label} cannot remember approval for an open-world effect`);
504
533
  }
534
+ if (definition.approval === "rememberable" && definition.destructive) {
535
+ throw new Error(`${label} cannot remember approval for a destructive effect`);
536
+ }
537
+ if (definition.approval === "none" && (definition.destructive || definition.openWorld)) {
538
+ throw new Error(`${label} cannot skip approval for destructive or open-world effects`);
539
+ }
505
540
  if (definition.approval === "rememberable" && !definition.review) {
506
541
  throw new Error(`${label} must provide a review before approval can be remembered`);
507
542
  }
543
+ if (definition.stream && ((definition.stream.direction === "write" && !("idempotency" in definition)) || definition.stream.maxBytes <= 0)) throw new Error(`${label}: write streams require an Action and a positive byte budget`);
544
+ if (definition.stream?.direction === "write" && "idempotency" in definition && definition.idempotency !== "required") throw new Error(`${label}: write streams require idempotency`);
508
545
  const schemas = compileOperationSchemas(definition, label);
509
546
  const manifest = {
510
547
  localId,
@@ -513,6 +550,7 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
513
550
  inputSchema: schemas.inputSchema,
514
551
  dataSchema: schemas.dataSchema,
515
552
  schemaHash: schemas.schemaHash,
553
+ ...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
516
554
  destructive: definition.destructive,
517
555
  openWorld: definition.openWorld,
518
556
  idempotency: definition.idempotency,
@@ -526,12 +564,30 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
526
564
  });
527
565
  }
528
566
 
567
+ const commands = Object.entries(definitions.commands ?? {})
568
+ .sort(([a], [b]) => a.localeCompare(b))
569
+ .map(([localId, definition]) => {
570
+ registerLocalId(localId, "Command");
571
+ const inputSchema = projectSchema(definition.input, `Command ${localId} input`, "input");
572
+ assertClosedObjectInput(inputSchema, `Command ${localId} input`);
573
+ return CapabilityCommandManifestSchema.parse({
574
+ localId,
575
+ title: definition.title.trim(),
576
+ description: definition.description.trim(),
577
+ icon: definition.icon,
578
+ keywords: [...(definition.keywords ?? [])],
579
+ inputSchema,
580
+ path: definition.path,
581
+ });
582
+ });
583
+
529
584
  const manifestBase = {
530
585
  protocolVersion: CAPABILITY_PROTOCOL_VERSION,
531
586
  appId,
532
587
  types,
533
588
  queries: [...queries.values()].map((entry) => entry.manifest),
534
589
  actions: [...actions.values()].map((entry) => entry.manifest),
590
+ commands,
535
591
  };
536
592
  const manifest = CapabilityManifestSchema.parse({
537
593
  ...manifestBase,
@@ -614,6 +670,7 @@ export const resolveCapabilityManifestPresentation = (
614
670
  ...(operation.universalSearch
615
671
  ? {
616
672
  universalSearch: {
673
+ ...operation.universalSearch,
617
674
  tags: operation.universalSearch.tags.map((tag) => {
618
675
  const tagCopy = copy.searchTags?.[tag.tag];
619
676
  return tagCopy
@@ -625,6 +682,17 @@ export const resolveCapabilityManifestPresentation = (
625
682
  : {}),
626
683
  };
627
684
  }),
685
+ commands: current.commands.map((operation) => {
686
+ const copy = translation.commands?.[operation.localId];
687
+ return copy
688
+ ? {
689
+ ...operation,
690
+ title: copy.title ?? operation.title,
691
+ description: copy.description ?? operation.description,
692
+ inputSchema: applySchemaPresentation(operation.inputSchema, copy.input),
693
+ }
694
+ : operation;
695
+ }),
628
696
  actions: current.actions.map((operation) => {
629
697
  const copy = translation.actions?.[operation.localId];
630
698
  return copy
@@ -668,10 +736,19 @@ export const parseCapabilityManifest = (value: unknown, expectedAppId: string):
668
736
  );
669
737
  }
670
738
  assertClosedObjectInput(inputSchema, `Operation ${operation.localId} input`);
671
- const expectedSchemaHash = capabilityHash({ inputSchema: operation.inputSchema, dataSchema: operation.dataSchema });
739
+ if (
740
+ operation.stream?.direction === "write" &&
741
+ (!("idempotency" in operation) || operation.idempotency !== "required")
742
+ ) {
743
+ throw new Error(`Operation ${operation.localId} write streams require an idempotent Action`);
744
+ }
745
+ const expectedSchemaHash = capabilityHash({ inputSchema: operation.inputSchema, dataSchema: operation.dataSchema, ...(operation.stream ? { stream: operation.stream } : {}) });
672
746
  if (operation.schemaHash !== expectedSchemaHash)
673
747
  throw new Error(`Operation ${operation.localId} schemaHash does not match its schemas`);
674
748
  if ("universalSearch" in operation && operation.universalSearch) {
749
+ for (const type of operation.universalSearch.scopeTypes ?? []) {
750
+ if (!manifest.types.some((entry) => entry.localId === type)) throw new Error(`Unknown search scope type ${type}`);
751
+ }
675
752
  const expectedInput = projectSchema(UniversalSearchInputSchema, "Universal Search input", "input");
676
753
  const expectedData = projectSchema(UniversalSearchDataSchema, "Universal Search data", "output");
677
754
  if (
@@ -682,6 +759,11 @@ export const parseCapabilityManifest = (value: unknown, expectedAppId: string):
682
759
  }
683
760
  }
684
761
  }
762
+ for (const command of manifest.commands) {
763
+ registerLocalId(command.localId, "Command");
764
+ z.fromJSONSchema(structuredClone(command.inputSchema));
765
+ assertClosedObjectInput(command.inputSchema, `Command ${command.localId} input`);
766
+ }
685
767
  const queries = new Map(manifest.queries.map((query) => [query.localId, query]));
686
768
  for (const type of manifest.types) {
687
769
  if (!type.reader) continue;
@@ -787,6 +869,12 @@ export const capabilityManifestEvolutionIssues = (previous: CapabilityManifest,
787
869
  else if (type.reader && current.reader !== type.reader) issues.push(`Type ${type.localId} reader changed`);
788
870
  }
789
871
 
872
+ const nextCommands = new Map(next.commands.map((command) => [command.localId, command]));
873
+ for (const command of previous.commands) {
874
+ const current = nextCommands.get(command.localId);
875
+ if (!current) issues.push(`Command ${command.localId} was removed`);
876
+ else issues.push(...schemaEvolutionIssues(command.inputSchema, current.inputSchema, `Command ${command.localId} input`, "input"));
877
+ }
790
878
  const nextQueries = new Map(next.queries.map((operation) => [operation.localId, operation]));
791
879
  const nextActions = new Map(next.actions.map((operation) => [operation.localId, operation]));
792
880
  for (const operation of previous.queries) {
@@ -982,11 +1070,24 @@ export const invokeCompiledCapability = async (params: {
982
1070
  },
983
1071
  };
984
1072
  }
1073
+ if ("universalSearch" in operation.manifest && operation.manifest.universalSearch) {
1074
+ const search = UniversalSearchInputSchema.parse(input.data);
1075
+ if (
1076
+ search.scope &&
1077
+ !(operation.manifest.universalSearch.scopeTypes ?? []).some(
1078
+ (type) => qualifiedId(params.compiled.manifest.appId, type) === search.scope!.type,
1079
+ )
1080
+ )
1081
+ return { ok: false, error: { code: "VALIDATION_FAILED", message: "Unsupported search scope", status: 400 } };
1082
+ }
985
1083
  try {
986
1084
  const invoked = await operation.definition.run(input.data, context);
987
1085
  if (!invoked.ok) {
988
1086
  return normalizeProviderError(invoked.error, "Capability returned an invalid error", params.onUnexpectedError);
989
1087
  }
1088
+ const stream = invoked.data.stream;
1089
+ if (stream && (Buffer.byteLength(stream.id) > 2048 || !operation.definition.stream || stream.direction !== operation.definition.stream.direction || stream.size > operation.definition.stream.maxBytes || Date.parse(stream.expiresAt) <= Date.now() || Date.parse(stream.expiresAt) > Date.now() + 86_400_000))
1090
+ return invalidProviderError("Stream is outside its declared policy or 24 hour lifetime");
990
1091
  const validated = validateCapabilityResult(params.compiled, operation, invoked.data, params.onUnexpectedError);
991
1092
  return validated.ok
992
1093
  ? ({
@@ -0,0 +1,81 @@
1
+ import { isServiceError } from "@k2b/stdlib";
2
+ import { exactStream } from "../capabilities/stream-body";
3
+ import type { CapabilityExecutionContext } from "../contracts/capabilities";
4
+ import {
5
+ CAPABILITY_MAX_RESULT_BYTES,
6
+ CapabilityStreamSchema,
7
+ CapabilityStreamStatusSchema,
8
+ capabilityResultSchema,
9
+ } from "../contracts/capabilities";
10
+ import type { CompiledCapabilities } from "./capabilities";
11
+
12
+ const failure = (status: number, code: string, message: string) => Response.json({ code, message }, { status });
13
+ export async function invokeCapabilityStream(params: {
14
+ compiled: CompiledCapabilities;
15
+ kind: "queries" | "actions";
16
+ localId: string;
17
+ verb: string;
18
+ request: Request;
19
+ context: CapabilityExecutionContext;
20
+ }): Promise<Response> {
21
+ const operation = params.compiled[params.kind].get(params.localId);
22
+ const definition = operation?.definition.stream;
23
+ if (!operation || !definition) return failure(404, "STREAM_NOT_FOUND", "Stream operation not found");
24
+ const offer = params.request.headers.get("x-cloud-stream-offer");
25
+ if (!offer || offer.length > 24000) return failure(400, "STREAM_INVALID", "Invalid stream offer");
26
+ const receipt = (value: unknown) => {
27
+ const parsed = capabilityResultSchema(operation.definition.data).parse(value);
28
+ if (parsed.stream) throw new Error("Stream receipts cannot issue another stream");
29
+ return parsed;
30
+ };
31
+ try {
32
+ const stream = CapabilityStreamSchema.parse(JSON.parse(Buffer.from(offer, "base64url").toString()));
33
+ if (stream.direction !== definition.direction || stream.size > definition.maxBytes || Date.parse(stream.expiresAt) <= Date.now())
34
+ return failure(410, "STREAM_EXPIRED", "Stream expired or outside its byte budget");
35
+ if (definition.direction === "read") {
36
+ if (params.verb !== "read") return failure(403, "STREAM_DENIED", "Read-only stream");
37
+ const response = await definition.read(stream, params.context);
38
+ if (!response.ok || !response.body) {
39
+ await response.body?.cancel();
40
+ return failure(502, "STREAM_FAILED", "Source could not be read");
41
+ }
42
+ return new Response(exactStream(response.body, stream.size), {
43
+ headers: { "content-type": stream.mediaType, "cache-control": "no-store" },
44
+ });
45
+ }
46
+ let result: unknown;
47
+ if (params.verb === "write") {
48
+ const body = exactStream(
49
+ params.request.body ??
50
+ new ReadableStream({
51
+ start(c) {
52
+ c.close();
53
+ },
54
+ }),
55
+ stream.size,
56
+ );
57
+ try {
58
+ const value = await definition.write(stream, body, params.context);
59
+ result = receipt(value);
60
+ } finally {
61
+ if (!body.locked) await body.cancel().catch(() => undefined);
62
+ }
63
+ } else if (params.verb === "status") {
64
+ const status = CapabilityStreamStatusSchema.parse(await definition.status(stream, params.context));
65
+ if (status.state === "completed") receipt(status.result);
66
+ result = status;
67
+ } else if (params.verb === "abort") {
68
+ await definition.abort(stream, params.context);
69
+ const status = CapabilityStreamStatusSchema.parse(await definition.status(stream, params.context));
70
+ if (status.state === "completed") receipt(status.result);
71
+ result = status;
72
+ } else return failure(400, "STREAM_INVALID", "Unknown stream operation");
73
+ const json = JSON.stringify(result);
74
+ if (Buffer.byteLength(json) > CAPABILITY_MAX_RESULT_BYTES)
75
+ return failure(502, "STREAM_FAILED", "Stream receipt is too large; inspect status before retrying");
76
+ return new Response(json, { headers: { "content-type": "application/json", "cache-control": "no-store" } });
77
+ } catch (error) {
78
+ if (isServiceError(error)) return failure(error.status, error.code, error.message);
79
+ return failure(409, "STREAM_FAILED", "Transfer failed; inspect status before retrying a write");
80
+ }
81
+ }
@@ -1,3 +1,7 @@
1
+ import { invokeCapabilityStream } from "./capability-streams";
2
+ import { registerHelp } from "../services/help";
3
+ import { preloadLayoutHelp } from "../ssr/help";
4
+ import { bindProcessApplicationId, clearProcessApplicationId } from "./process-identity";
1
5
  /**
2
6
  * defineApp() — The single entry point for every cloud app.
3
7
  *
@@ -5,6 +9,8 @@
5
9
  * Returns `{ ssr, plugin, config, meta, start }`.
6
10
  */
7
11
 
12
+ import { resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
8
14
  import type { SsrConfig } from "@k2b/ssr";
9
15
  import { createConfig as createSsrConfig } from "@k2b/ssr";
10
16
  import { routes } from "@k2b/ssr/hono";
@@ -18,10 +24,17 @@ import type {
18
24
  AppLifecycle,
19
25
  AppMeta,
20
26
  AppPresentationCatalog,
27
+ AppSearchLink,
21
28
  CloudLifecycleContext,
22
29
  WidgetEndpoint,
23
30
  } from "../contracts/app";
24
- import { CAPABILITY_FRAMEWORK_ERROR_CODES, CAPABILITY_MAX_REQUEST_BYTES, type CapabilityDefinitions } from "../contracts/capabilities";
31
+ import {
32
+ CAPABILITY_FRAMEWORK_ERROR_CODES,
33
+ CAPABILITY_MAX_REQUEST_BYTES,
34
+ CAPABILITY_ORIGIN_HEADER,
35
+ type CapabilityDefinitions,
36
+ CapabilityOriginSchema,
37
+ } from "../contracts/capabilities";
25
38
  import { type BoundNotificationMap, bindNotificationDefinitions, type NotificationDefinitionMap } from "../contracts/notification-types";
26
39
  import type { AppRegistryEntry } from "../contracts/registry";
27
40
  import type { AppSettingsMap, KindToType } from "../contracts/settings-types";
@@ -30,18 +43,19 @@ import type { HelpDefinition } from "../server/help";
30
43
  import { getLocale, resolveLocale } from "../server/locale";
31
44
  import { type AuthContext, auth } from "../server/middleware/auth";
32
45
  import { requireInvocation } from "../server/middleware/invocation";
33
- import { routeTemplate } from "../server/middleware/route-template";
46
+ import { matchedRouteTemplate, routeTemplate } from "../server/middleware/route-template";
34
47
  import { runtime as runtimeMiddleware } from "../server/middleware/runtime";
35
- import { settings as settingsMiddleware } from "../server/middleware/settings";
48
+ import { preloadLayoutAnnouncements, settings as settingsMiddleware } from "../server/middleware/settings";
36
49
  import {
37
50
  capabilityInvocationOperation,
38
51
  searchInvocationOperation,
39
52
  syncInvocationOperation,
40
53
  widgetInvocationOperation,
41
54
  } from "../services/identity/invocation-operations";
55
+ import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
42
56
  import { logger } from "../services/logging";
43
57
  import { startNotificationDefinitionRegistration } from "../services/notifications/catalog";
44
- import { railPreferences } from "../services/rail-preferences";
58
+ import { readRailSnapshot } from "../services/rail-snapshot";
45
59
  import { get, loadCache as loadSettingsCache, set } from "../services/settings";
46
60
  import { createSettingsAPI, type SettingsAPI } from "../services/settings/api";
47
61
  import { registerSettings, toLegacySettingDefs } from "../services/settings/defaults";
@@ -59,11 +73,13 @@ import { createHeartbeat } from "./heartbeat";
59
73
  import { compileHelp } from "./help";
60
74
  import { createPageResponses } from "./page-responses";
61
75
  import { getProcessSync, startProcessSync } from "./process-sync";
76
+ import { configurePostgresApplicationName } from "./postgres-application-name";
62
77
  import { APP_READINESS_PATH, appReadinessResponse } from "./readiness";
63
- import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry, helpRegistry } from "./registry";
78
+ import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry } from "./registry";
64
79
  import { ensureRuntimeWatcher, getCurrentRuntime, stopRuntimeWatcher } from "./runtime-watcher";
65
80
  import { servePublicAsset } from "./static-assets";
66
81
  import { createStatusPreservingSsrHandler } from "./status-preserving-ssr";
82
+ import { WEB_VITALS_ASSET_HREF } from "./web-vitals-asset";
67
83
 
68
84
  /** Cache-busting version stamp — changes on every server start / rebuild. */
69
85
  const v = Date.now();
@@ -76,6 +92,8 @@ export type PageOptions = {
76
92
  lang?: string;
77
93
  };
78
94
 
95
+ type ResolvedPageOptions = PageOptions & { performanceRoute?: string };
96
+
79
97
  // ── Public types ────────────────────────────────────────────────────────────
80
98
 
81
99
  /**
@@ -103,6 +121,7 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
103
121
  adminHref?: string;
104
122
  /** Multi-link admin navigation contributed by this app. */
105
123
  adminNav?: ReadonlyArray<{
124
+ section?: "ai";
106
125
  id?: string;
107
126
  label: string;
108
127
  links: ReadonlyArray<{ label: string; href: string; icon: string }>;
@@ -136,6 +155,8 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
136
155
  * always open in a new tab from the login footer.
137
156
  */
138
157
  legalLinks?: ReadonlyArray<{ label: string; href: string; icon?: string }>;
158
+ /** Static destinations for the global search UI; no capabilities are created. */
159
+ searchLinks?: readonly AppSearchLink[];
139
160
  /**
140
161
  * Dashboard widget endpoints this app exposes. Each entry references an
141
162
  * HTTP path on this app that returns a `WidgetResponse`. The dashboard
@@ -177,9 +198,10 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
177
198
  */
178
199
  openapi?: string;
179
200
  /**
180
- * Project root used by the SSR plugin to discover island/client files.
201
+ * Project root used for SSR component IDs and development assets.
181
202
  * Defaults to `process.cwd()`. Override only if you run the entrypoint
182
- * from a directory other than the project root.
203
+ * from a directory other than the project root. Discovery uses APP_DIR/src
204
+ * (or appRoot/src) plus the installed Cloud framework source.
183
205
  */
184
206
  appRoot?: string;
185
207
  };
@@ -280,6 +302,7 @@ export const defineApp = <
280
302
  >(
281
303
  opts: AppOptions<S, N, AppId>,
282
304
  ): AppDefinition<S, N, AppId> => {
305
+ configurePostgresApplicationName(opts.id);
283
306
  const isDevelopment = process.env.NODE_ENV === "development";
284
307
  const notifications = bindNotificationDefinitions(opts.id, opts.notifications);
285
308
 
@@ -293,12 +316,14 @@ export const defineApp = <
293
316
  }
294
317
 
295
318
  // ── 1. SSR config ─────────────────────────────────────────────────────
296
- const { config, plugin, html } = createSsrConfig<PageOptions>({
319
+ const rootDir = opts.appRoot ?? process.cwd();
320
+ const { config, plugin, html } = createSsrConfig<ResolvedPageOptions>({
297
321
  dev: isDevelopment,
298
322
  verbose: true,
299
- rootDir: opts.appRoot ?? process.cwd(),
323
+ rootDir,
324
+ componentRoots: [resolve(process.env.APP_DIR ?? rootDir, "src"), fileURLToPath(new URL("../", import.meta.url))],
300
325
  basePath: opts.basePath,
301
- template: ({ body, scripts, title, description, theme, lang }) => {
326
+ template: ({ body, scripts, title, description, theme, lang, performanceRoute }) => {
302
327
  const themeFixed = theme !== undefined;
303
328
  return `<!DOCTYPE html>
304
329
  <html lang="${normalizeLocale(lang)}" class="${theme ?? "light"}"${themeFixed ? " data-theme-fixed" : ""}>
@@ -321,6 +346,7 @@ export const defineApp = <
321
346
  ${body}
322
347
  </body>
323
348
  ${scripts}
349
+ ${performanceRoute ? `<script type="module" src="${WEB_VITALS_ASSET_HREF}" data-cloud-web-vitals data-app-id="${escapeHtml(opts.id)}" data-route-template="${escapeHtml(performanceRoute)}"></script>` : ""}
324
350
  </html>`;
325
351
  },
326
352
  });
@@ -336,10 +362,16 @@ export const defineApp = <
336
362
  // `getDateConfig` share one canonical `getLocale(c)` that page handlers
337
363
  // cannot override.
338
364
  const ssr = Object.assign(
339
- createStatusPreservingSsrHandler<PageOptions>(html, async (c) => {
365
+ createStatusPreservingSsrHandler<ResolvedPageOptions>(html, async (c) => {
340
366
  c.get("page").lang = getLocale(c);
341
367
  const user = c.get("user");
342
- if (user) c.set("railPreferences", await railPreferences.get(user.id));
368
+ c.get("page").performanceRoute =
369
+ user && c.get("settings")?.observability?.web_vitals?.enabled === true ? (matchedRouteTemplate(c) ?? undefined) : undefined;
370
+ await Promise.all([
371
+ preloadLayoutAnnouncements(c),
372
+ preloadLayoutHelp(c),
373
+ user ? readRailSnapshot(user).then((snapshot) => c.set("railPreferences", snapshot)) : undefined,
374
+ ]);
343
375
  }),
344
376
  createPageResponses(html),
345
377
  );
@@ -354,6 +386,7 @@ export const defineApp = <
354
386
  adminHref: opts.adminHref,
355
387
  adminNav: opts.adminNav?.map(
356
388
  (group): AppAdminNavigationGroup => ({
389
+ ...(group.section ? { section: group.section } : {}),
357
390
  id: group.id,
358
391
  label: group.label,
359
392
  links: group.links.map((link) => ({ ...link })),
@@ -362,6 +395,7 @@ export const defineApp = <
362
395
  routes: [...opts.routes],
363
396
  nav: opts.nav,
364
397
  legalLinks: opts.legalLinks ? [...opts.legalLinks] : undefined,
398
+ searchLinks: opts.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
365
399
  widgets: opts.widgets ? opts.widgets.map((w) => ({ ...w })) : undefined,
366
400
  settingKeys: opts.settings ? Object.keys(opts.settings) : undefined,
367
401
  openapi: opts.openapi,
@@ -400,6 +434,8 @@ export const defineApp = <
400
434
  }
401
435
  })());
402
436
  try {
437
+ bindProcessApplicationId(meta.id);
438
+ cleanup.unshift(clearProcessApplicationId);
403
439
  const startedAt = Date.now();
404
440
 
405
441
  // OpenAPI advertised in the registry only when there's a router to
@@ -410,8 +446,6 @@ export const defineApp = <
410
446
  const compiledHelp = startOpts.help
411
447
  ? compileHelp({
412
448
  appId: meta.id,
413
- appName: meta.name,
414
- appIcon: meta.icon,
415
449
  basePath: opts.basePath,
416
450
  definition: startOpts.help,
417
451
  })
@@ -441,6 +475,7 @@ export const defineApp = <
441
475
  }
442
476
  : undefined,
443
477
  adminNav: meta.adminNav?.map((group) => ({
478
+ ...(group.section ? { section: group.section } : {}),
444
479
  id: group.id,
445
480
  label: group.label,
446
481
  links: group.links.map((link) => ({ ...link })),
@@ -453,6 +488,7 @@ export const defineApp = <
453
488
  : undefined,
454
489
  help: compiledHelp?.summary,
455
490
  legalLinks: meta.legalLinks ? meta.legalLinks.map((l) => ({ ...l })) : undefined,
491
+ searchLinks: meta.searchLinks,
456
492
  widgets: meta.widgets ? meta.widgets.map((w) => ({ ...w })) : undefined,
457
493
  settingKeys: meta.settingKeys ? [...meta.settingKeys] : undefined,
458
494
  openapi: advertiseOpenapi ? opts.openapi : undefined,
@@ -461,6 +497,7 @@ export const defineApp = <
461
497
  // Heartbeat
462
498
  const heartbeat = createHeartbeat(meta.id, entry, {
463
499
  registry: appRegistry(),
500
+ beforeWrite: compiledHelp ? () => registerHelp(compiledHelp.corpus) : undefined,
464
501
  onError: (error) =>
465
502
  log.error("Registry heartbeat failed", {
466
503
  appId: meta.id,
@@ -499,27 +536,6 @@ export const defineApp = <
499
536
  },
500
537
  })
501
538
  : undefined;
502
- const helpHeartbeat = compiledHelp
503
- ? createHeartbeat(meta.id, compiledHelp.registryEntry, {
504
- key: `help/${meta.id}`,
505
- registry: helpRegistry(),
506
- onError: (error) =>
507
- log.error("Help registry heartbeat failed", {
508
- appId: meta.id,
509
- error: error instanceof Error ? error.message : String(error),
510
- }),
511
- onStale: (error) => {
512
- log.error("Help registry lease expired; restarting", {
513
- appId: meta.id,
514
- error: error instanceof Error ? error.message : String(error),
515
- });
516
- process.exit(1);
517
- },
518
- })
519
- : undefined;
520
- cleanup.push(async () => {
521
- await helpHeartbeat?.stop();
522
- });
523
539
  cleanup.push(async () => {
524
540
  await capabilityHeartbeat?.stop();
525
541
  });
@@ -614,6 +630,8 @@ export const defineApp = <
614
630
  user,
615
631
  idempotencyKey,
616
632
  locale: requestLocale,
633
+ requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(),
634
+ origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)),
617
635
  signal: c.req.raw.signal,
618
636
  },
619
637
  onUnexpectedError: (error: unknown) =>
@@ -654,6 +672,15 @@ export const defineApp = <
654
672
  schemaHash: operation.manifest.schemaHash,
655
673
  };
656
674
  });
675
+ for (const kind of ["queries", "actions"] as const) {
676
+ server.post(`/api/_internal/capabilities/v1/streams/${kind}/:capabilityId/:verb`, capabilityAuth(kind), auth.requireOAuthScope(kind === "queries" ? "read" : "write", "admin"), async c => {
677
+ const actor = c.get("actor");
678
+ return invokeCapabilityStream({ compiled: compiledCapabilities, kind, localId: c.req.param("capabilityId") ?? "", verb: c.req.param("verb") ?? "", request: c.req.raw,
679
+ context: { actor, accessSubject: c.get("accessSubject"), user: actor.kind === "user" ? actor.user : actor.delegatedUser,
680
+ locale: resolveLocale(c.req.raw.headers, await get<string>("app.locale")),
681
+ requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(), origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)), signal: c.req.raw.signal } });
682
+ });
683
+ }
657
684
  const capabilityReadScope = auth.requireOAuthScope("read", "admin");
658
685
  const capabilityWriteScope = auth.requireOAuthScope("write", "admin");
659
686
  server.post("/api/_internal/capabilities/v1/queries/:capabilityId", capabilityAuth("queries"), capabilityReadScope, (c) =>
@@ -759,7 +786,6 @@ export const defineApp = <
759
786
 
760
787
  // Only advertise after setup, workers, and their declared resources are ready.
761
788
  // Publish supporting entries first; the app entry makes them discoverable.
762
- await helpHeartbeat?.start();
763
789
  await capabilityHeartbeat?.start();
764
790
  await heartbeat.start();
765
791
  log.info(`Registered "${meta.id}"`, { baseUrl });
@@ -18,6 +18,8 @@ type HeartbeatRegistry<T> = {
18
18
  type HeartbeatOptions<T> = {
19
19
  registry: HeartbeatRegistry<T>;
20
20
  key?: string;
21
+ /** Publish dependent data before advertising or renewing the app. */
22
+ beforeWrite?: () => Promise<void>;
21
23
  intervalMs?: number;
22
24
  retryMs?: number;
23
25
  staleAfterMs?: number;
@@ -53,6 +55,7 @@ export const createHeartbeat = <T>(appId: string, entry: T, options: HeartbeatOp
53
55
  };
54
56
 
55
57
  const register = async (): Promise<void> => {
58
+ await options.beforeWrite?.();
56
59
  await registry.upsert({ key, value: entry });
57
60
  noteSuccess();
58
61
  };
@@ -61,6 +64,7 @@ export const createHeartbeat = <T>(appId: string, entry: T, options: HeartbeatOp
61
64
  // Touch renews the existing entry (and emits a registry invalidation).
62
65
  // Avoid rebuilding the manifest here. A missing entry is repaired
63
66
  // immediately, preserving restart-free registry recovery.
67
+ await options.beforeWrite?.();
64
68
  const touched = await registry.touch({ key });
65
69
  if (!touched) await registry.upsert({ key, value: entry });
66
70
  noteSuccess();