@k2b/cloud 0.6.0 → 0.8.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 (349) hide show
  1. package/README.md +2 -2
  2. package/package.json +22 -11
  3. package/scripts/README.md +10 -0
  4. package/scripts/browser-performance.ts +14 -0
  5. package/scripts/build-pdf-renderer.ts +39 -0
  6. package/scripts/build.ts +20 -35
  7. package/scripts/preload.ts +16 -12
  8. package/scripts/sync-recovery-smoke.ts +22 -6
  9. package/src/_internal/app-presentation.ts +14 -2
  10. package/src/_internal/build-metadata.ts +8 -1
  11. package/src/_internal/capabilities.ts +131 -7
  12. package/src/_internal/capability-streams.ts +81 -0
  13. package/src/_internal/define-app.ts +80 -37
  14. package/src/_internal/heartbeat.ts +4 -0
  15. package/src/_internal/help-catalog.ts +27 -172
  16. package/src/_internal/help.ts +13 -76
  17. package/src/_internal/page-responses.ts +2 -0
  18. package/src/_internal/postgres-application-name.ts +30 -0
  19. package/src/_internal/process-identity.ts +15 -0
  20. package/src/_internal/process-sync.ts +1 -1
  21. package/src/_internal/readiness.ts +4 -1
  22. package/src/_internal/registry-validation.ts +29 -25
  23. package/src/_internal/registry.ts +2 -53
  24. package/src/_internal/runtime-context.ts +5 -16
  25. package/src/_internal/server-timing.ts +13 -0
  26. package/src/_internal/status-preserving-ssr.ts +18 -6
  27. package/src/_internal/web-vitals-asset.ts +10 -0
  28. package/src/access/PermissionEditor.tsx +18 -151
  29. package/src/access/PrincipalPicker.tsx +110 -0
  30. package/src/access/ResourceApiKeys.tsx +1 -1
  31. package/src/access/ui.ts +1 -0
  32. package/src/ai/admin.ts +14 -13
  33. package/src/ai/assistant-models.ts +12 -1
  34. package/src/ai/audio-format.ts +34 -0
  35. package/src/ai/audio-tool.ts +90 -0
  36. package/src/ai/browser-code-contracts.ts +201 -0
  37. package/src/ai/browser.ts +4 -0
  38. package/src/ai/capabilities.ts +55 -36
  39. package/src/ai/capability-execution.ts +47 -12
  40. package/src/ai/chat/blocks.tsx +229 -90
  41. package/src/ai/chat/builtin-tools.tsx +2 -4
  42. package/src/ai/chat/capability-result.ts +15 -0
  43. package/src/ai/chat/capability-table.tsx +2 -10
  44. package/src/ai/chat/composer-adapter.ts +83 -3
  45. package/src/ai/chat/message-actions.tsx +8 -7
  46. package/src/ai/chat/message-utils.ts +25 -1
  47. package/src/ai/chat/messages.ts +2 -0
  48. package/src/ai/chat/presentation.tsx +31 -47
  49. package/src/ai/chat/primitives.tsx +43 -1
  50. package/src/ai/chat/tool-disclosure.tsx +21 -2
  51. package/src/ai/chat/tool-groups.ts +58 -0
  52. package/src/ai/chat-quotas.ts +25 -0
  53. package/src/ai/chat-task-contracts.ts +14 -10
  54. package/src/ai/chat-tasks.ts +297 -43
  55. package/src/ai/client/controller.ts +165 -39
  56. package/src/ai/client/file-source.ts +12 -6
  57. package/src/ai/client/live-connection.ts +1 -0
  58. package/src/ai/client/projection.ts +29 -11
  59. package/src/ai/client/transport.ts +6 -0
  60. package/src/ai/code-capability-routes.ts +105 -0
  61. package/src/ai/code-capability-transport.ts +39 -0
  62. package/src/ai/code-execution.ts +53 -0
  63. package/src/ai/code-mode-skill.ts +118 -0
  64. package/src/ai/code-runtime-tools.ts +179 -0
  65. package/src/ai/code-source-contracts.ts +270 -0
  66. package/src/ai/code-source-tools.ts +188 -0
  67. package/src/ai/compaction.ts +27 -6
  68. package/src/ai/data-analysis-skill.ts +10 -0
  69. package/src/ai/default-tools.ts +99 -3
  70. package/src/ai/dictation-runtime.ts +171 -0
  71. package/src/ai/dictations.ts +188 -0
  72. package/src/ai/draft-content.ts +49 -0
  73. package/src/ai/enrich.ts +6 -0
  74. package/src/ai/executor.ts +189 -33
  75. package/src/ai/fetch-file-tool.ts +2 -2
  76. package/src/ai/file-content-version.ts +27 -0
  77. package/src/ai/file-context.ts +2 -1
  78. package/src/ai/file-media-type.ts +12 -0
  79. package/src/ai/file-reference-contracts.ts +15 -0
  80. package/src/ai/file-tools.ts +8 -1
  81. package/src/ai/files-store.ts +212 -71
  82. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  83. package/src/ai/grids-skill.ts +42 -10
  84. package/src/ai/http.ts +26 -8
  85. package/src/ai/index.ts +26 -3
  86. package/src/ai/inference-calls.ts +197 -0
  87. package/src/ai/live-events.ts +5 -1
  88. package/src/ai/live.ts +1 -1
  89. package/src/ai/markdown-pdf-tool.ts +2 -2
  90. package/src/ai/memories.ts +30 -27
  91. package/src/ai/memory-learning-runs.ts +1 -1
  92. package/src/ai/memory-learning.ts +64 -32
  93. package/src/ai/memory-tool.ts +1 -5
  94. package/src/ai/memory-workflow-evidence.ts +9 -4
  95. package/src/ai/message-queue.ts +178 -0
  96. package/src/ai/migrate.ts +189 -11
  97. package/src/ai/model-pricing.ts +43 -0
  98. package/src/ai/pdf-render-worker.ts +75 -0
  99. package/src/ai/pdf-render.fixture.ts +28 -0
  100. package/src/ai/pdf-render.ts +77 -0
  101. package/src/ai/projects-routes.ts +18 -43
  102. package/src/ai/projects.ts +33 -14
  103. package/src/ai/protocol.ts +3 -0
  104. package/src/ai/provider.ts +1 -8
  105. package/src/ai/quota-provider.ts +192 -0
  106. package/src/ai/quota-report.ts +152 -0
  107. package/src/ai/quotas-migrate.ts +45 -0
  108. package/src/ai/quotas.ts +168 -0
  109. package/src/ai/routes.ts +241 -11
  110. package/src/ai/run-timeout.ts +12 -0
  111. package/src/ai/runtime-tools.ts +7 -5
  112. package/src/ai/runtime.ts +74 -17
  113. package/src/ai/settings.ts +36 -8
  114. package/src/ai/short-id.ts +4 -5
  115. package/src/ai/skill-catalog.ts +0 -10
  116. package/src/ai/skill-search.ts +58 -0
  117. package/src/ai/skill-seeds.ts +114 -20
  118. package/src/ai/skill-tool.ts +21 -5
  119. package/src/ai/skills-routes.ts +49 -4
  120. package/src/ai/skills.ts +358 -163
  121. package/src/ai/store.ts +416 -201
  122. package/src/ai/stream.ts +5 -0
  123. package/src/ai/structured-runs.ts +4 -3
  124. package/src/ai/structured.ts +12 -1
  125. package/src/ai/system-prompt.ts +14 -2
  126. package/src/ai/task-contracts.ts +84 -0
  127. package/src/ai/task-execution.ts +92 -0
  128. package/src/ai/timeline.ts +2 -5
  129. package/src/ai/todo-contracts.ts +32 -0
  130. package/src/ai/todo-tool.ts +19 -0
  131. package/src/ai/tool-audit.ts +30 -40
  132. package/src/ai/tools.ts +6 -0
  133. package/src/ai/transcription.ts +202 -0
  134. package/src/ai/turn-timing.ts +114 -0
  135. package/src/ai/types.ts +58 -10
  136. package/src/ai/ui.tsx +3 -0
  137. package/src/ai/usage.ts +118 -118
  138. package/src/ai/vision-tool.ts +60 -10
  139. package/src/api/admin-ai-projects.ts +2 -2
  140. package/src/api/admin-ai-quotas.ts +80 -0
  141. package/src/api/admin-ai-skills.ts +48 -5
  142. package/src/api/admin-ai-usage.ts +2 -2
  143. package/src/api/admin-core-settings.ts +25 -0
  144. package/src/api/admin-identity.ts +91 -51
  145. package/src/api/admin-linux-identities.ts +10 -4
  146. package/src/api/admin-rail.ts +58 -0
  147. package/src/api/announcements.ts +14 -0
  148. package/src/api/app-approval.ts +2 -2
  149. package/src/api/auth.ts +1 -1
  150. package/src/api/capabilities.ts +324 -43
  151. package/src/api/capability-streams.ts +262 -0
  152. package/src/api/help.ts +8 -44
  153. package/src/api/index.ts +8 -2
  154. package/src/api/mcp.ts +45 -34
  155. package/src/api/me-web-vitals.ts +35 -0
  156. package/src/api/me.ts +2 -0
  157. package/src/api/search/schemas.ts +26 -15
  158. package/src/api/search.bench.ts +1 -1
  159. package/src/api/search.ts +54 -17
  160. package/src/browser/CloudResourceSearch.tsx +640 -702
  161. package/src/browser/app-approval-vault.ts +19 -7
  162. package/src/browser/command-bridge.ts +109 -0
  163. package/src/browser/command-shortcuts.ts +76 -0
  164. package/src/browser/commands.ts +67 -0
  165. package/src/browser/navigation-search.ts +48 -0
  166. package/src/browser/notifications.ts +1 -1
  167. package/src/browser/resource-picker.tsx +5 -18
  168. package/src/browser/resource-search-dialog.ts +9 -0
  169. package/src/browser/resource-search-input.ts +46 -0
  170. package/src/browser/resource-search-messages.ts +76 -40
  171. package/src/browser/resource-search.ts +7 -0
  172. package/src/browser/search-bridge.ts +102 -0
  173. package/src/browser/search-commands.ts +59 -0
  174. package/src/browser/search.ts +13 -0
  175. package/src/browser/spotlight-position.ts +208 -0
  176. package/src/browser/testing.ts +6 -0
  177. package/src/browser/web-vitals.ts +25 -0
  178. package/src/capabilities/claims.ts +179 -0
  179. package/src/capabilities/client.ts +34 -0
  180. package/src/capabilities/command-link.ts +16 -0
  181. package/src/capabilities/executions.ts +298 -0
  182. package/src/capabilities/migrate.ts +81 -0
  183. package/src/capabilities/server.ts +116 -35
  184. package/src/capabilities/stream-body.ts +16 -0
  185. package/src/capabilities/streams.ts +28 -0
  186. package/src/cli/admin/ai-quotas.ts +161 -0
  187. package/src/cli/admin/ai-skills.ts +64 -0
  188. package/src/cli/admin/ai-usage.ts +53 -30
  189. package/src/cli/admin/app-credentials.ts +77 -0
  190. package/src/cli/admin/data.ts +17 -0
  191. package/src/cli/admin/index.ts +10 -1
  192. package/src/cli/admin/instance.ts +2 -2
  193. package/src/cli/admin/linux.ts +4 -2
  194. package/src/cli/admin/sync.ts +1 -1
  195. package/src/cli/capabilities.ts +42 -0
  196. package/src/cli/capability-streams.ts +25 -0
  197. package/src/cli/index.ts +3 -2
  198. package/src/config/define-env.ts +97 -0
  199. package/src/config/env.ts +147 -51
  200. package/src/config/index.ts +3 -1
  201. package/src/contracts/app.ts +23 -3
  202. package/src/contracts/capabilities.ts +75 -5
  203. package/src/contracts/capability-streams.ts +46 -0
  204. package/src/contracts/commands.ts +49 -0
  205. package/src/contracts/index.ts +5 -2
  206. package/src/contracts/rail-admin.ts +38 -0
  207. package/src/contracts/rail-preferences.ts +16 -0
  208. package/src/contracts/registry.ts +5 -27
  209. package/src/contracts/settings-types.ts +1 -1
  210. package/src/contracts/shared.ts +18 -5
  211. package/src/contracts/web-vitals.ts +27 -0
  212. package/src/desktop/index.ts +1 -0
  213. package/src/desktop/solid.tsx +1 -1
  214. package/src/index.ts +8 -6
  215. package/src/server/index.ts +3 -0
  216. package/src/server/middleware/auth.ts +3 -2
  217. package/src/server/middleware/route-template.ts +9 -3
  218. package/src/server/middleware/runtime.ts +2 -1
  219. package/src/server/middleware/settings.ts +17 -11
  220. package/src/server/ratelimit.ts +1 -1
  221. package/src/server/services/access-revision.ts +10 -0
  222. package/src/server/services/access.ts +3 -1
  223. package/src/server/services/index.ts +1 -0
  224. package/src/services/account-category-policy.ts +9 -0
  225. package/src/services/account-lifecycle/audit.ts +1 -1
  226. package/src/services/accounts/app.ts +12 -2
  227. package/src/services/accounts/email-write.ts +23 -0
  228. package/src/services/accounts/groups.ts +15 -4
  229. package/src/services/accounts/identities.ts +249 -0
  230. package/src/services/accounts/identity-reconciliation.ts +87 -0
  231. package/src/services/accounts/ipa-data.ts +2 -2
  232. package/src/services/accounts/local-groups.ts +2 -2
  233. package/src/services/accounts/notification-sender.ts +12 -2
  234. package/src/services/accounts/posix.ts +45 -29
  235. package/src/services/accounts/users.ts +2 -2
  236. package/src/services/announcements/index.ts +69 -8
  237. package/src/services/app-approval.ts +11 -10
  238. package/src/services/audit/index.ts +1 -1
  239. package/src/services/cache-fill.ts +35 -0
  240. package/src/services/help/index.ts +109 -0
  241. package/src/services/help/maintenance.ts +25 -0
  242. package/src/services/help/store.ts +180 -0
  243. package/src/services/help/types.ts +46 -0
  244. package/src/services/identity/invocation-actor.ts +2 -2
  245. package/src/services/identity/key-config.ts +8 -5
  246. package/src/services/identity/key-ring.ts +1 -1
  247. package/src/services/identity/runtime-config.ts +6 -5
  248. package/src/services/index.ts +15 -0
  249. package/src/services/ipa/posix.ts +1 -1
  250. package/src/services/ipa/sync.ts +1 -1
  251. package/src/services/ipa/users.ts +268 -221
  252. package/src/services/legal-consent.ts +1 -1
  253. package/src/services/logging/index.ts +15 -13
  254. package/src/services/logging/trace.ts +2 -1
  255. package/src/services/logging/web-vitals.ts +81 -0
  256. package/src/services/mandates/index.ts +21 -4
  257. package/src/services/mandates/policy.ts +165 -2
  258. package/src/services/notifications/batches.ts +125 -57
  259. package/src/services/notifications/browser.ts +12 -0
  260. package/src/services/notifications/index.ts +0 -2
  261. package/src/services/notifications/platform.ts +0 -4
  262. package/src/services/oauth-tokens.ts +3 -3
  263. package/src/services/pdf/gotenberg.ts +131 -72
  264. package/src/services/pdf/index.ts +11 -8
  265. package/src/services/providers/local/users.ts +25 -16
  266. package/src/services/public-http.ts +101 -0
  267. package/src/services/rail-shortcuts.ts +95 -0
  268. package/src/services/rail-snapshot.ts +53 -0
  269. package/src/services/request-cache-redis.ts +29 -0
  270. package/src/services/session/index.ts +15 -2
  271. package/src/services/session/user.ts +62 -37
  272. package/src/services/settings/app.ts +5 -1
  273. package/src/services/settings/core-settings.ts +26 -35
  274. package/src/services/settings/defaults.ts +5 -0
  275. package/src/services/settings/index.ts +3 -3
  276. package/src/services/settings/store.ts +58 -84
  277. package/src/services/webauthn.ts +1 -1
  278. package/src/shared/ai-costs.ts +42 -0
  279. package/src/shared/ai-platform-prompt.ts +1 -1
  280. package/src/shared/ai-quotas.ts +136 -0
  281. package/src/shared/ai-usage.ts +6 -16
  282. package/src/shared/app-presentation.ts +21 -15
  283. package/src/shared/capability-messages.ts +14 -0
  284. package/src/shared/help.ts +6 -42
  285. package/src/shared/index.ts +3 -0
  286. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  287. package/src/ssr/AdminLayout.tsx +7 -5
  288. package/src/ssr/AdminSidebar.tsx +30 -16
  289. package/src/ssr/AppLaunchpad.island.tsx +33 -136
  290. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  291. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  292. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  293. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  294. package/src/ssr/Layout.tsx +17 -6
  295. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  296. package/src/ssr/LayoutFooter.tsx +1 -1
  297. package/src/ssr/LayoutHeader.tsx +46 -14
  298. package/src/ssr/LayoutHelp.tsx +22 -23
  299. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  300. package/src/ssr/LayoutRail.tsx +17 -9
  301. package/src/ssr/MinimalLayout.tsx +1 -1
  302. package/src/ssr/MobileNavigation.tsx +71 -0
  303. package/src/ssr/MobileProfileActions.tsx +49 -0
  304. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  305. package/src/ssr/RailApps.island.tsx +2 -2
  306. package/src/ssr/RailEditor.tsx +17 -2
  307. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  308. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  309. package/src/ssr/admin-navigation.ts +11 -1
  310. package/src/ssr/help.ts +29 -0
  311. package/src/ssr/islands/SearchBar.tsx +67 -0
  312. package/src/ssr/islands/index.ts +3 -1
  313. package/src/ssr/layout-context.ts +2 -2
  314. package/src/ssr/layout-help-search.ts +15 -0
  315. package/src/ssr/mobile-menu-history.ts +70 -0
  316. package/src/ssr/platform-messages.ts +16 -8
  317. package/src/ssr/profile-actions.ts +42 -0
  318. package/src/ssr/rail-context.ts +8 -4
  319. package/src/ssr/rail-messages.ts +2 -0
  320. package/src/ssr/rail-navigation.ts +3 -3
  321. package/src/ssr/workspace-navigation.ts +75 -0
  322. package/src/styles/effects.css +6 -6
  323. package/src/styles/global.css +54 -0
  324. package/src/styles/input.css +43 -41
  325. package/src/styles/resource-search.css +505 -0
  326. package/src/styles/utilities-feedback.css +13 -0
  327. package/src/styles/utilities-layout.css +1 -5
  328. package/src/styles/utilities-navigation.css +3 -39
  329. package/src/types/ambient.d.ts +5 -0
  330. package/src/workflows/ai/index.ts +2 -2
  331. package/src/workflows/ai/runtime.ts +19 -90
  332. package/src/workflows/ai/store.ts +2 -0
  333. package/src/workflows/ai/types.ts +6 -82
  334. package/src/workflows/ai-actions.ts +16 -13
  335. package/src/workflows/definition.ts +10 -5
  336. package/src/workflows/store/actions.ts +44 -20
  337. package/src/workflows/store/index.ts +4 -2
  338. package/src/workflows/store/runs.ts +36 -0
  339. package/src/workflows/store/worker-pool.ts +55 -0
  340. package/src/workflows/store/worker-runtime.ts +90 -0
  341. package/src/ai/kit-skill.ts +0 -22
  342. package/src/ai/usage-migrate.ts +0 -81
  343. package/src/browser/notification-target.ts +0 -27
  344. package/src/contracts/notification-live.ts +0 -66
  345. package/src/server/services/freeipa/test-certificates.ts +0 -44
  346. package/src/services/notifications/live.ts +0 -50
  347. package/src/services/session/test-fixture.ts +0 -18
  348. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  349. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -4,6 +4,7 @@ import { escapeLikePattern, parsePgJsonRecord, toPgTextArray } from "../postgres
4
4
  import { registerSettings } from "../settings/defaults";
5
5
  import { redactMetadata } from "./redaction";
6
6
  import { observeSyncEvent, TRACE_STUCK_AFTER_MS, trace } from "./trace";
7
+ import { readWebVitals } from "./web-vitals";
7
8
 
8
9
  export type {
9
10
  TraceAttributes,
@@ -129,7 +130,7 @@ function write(params: WriteParams): void {
129
130
  ${params.level},
130
131
  ${params.source},
131
132
  ${params.message},
132
- ${safeMetadata ? JSON.stringify(safeMetadata) : null}::jsonb
133
+ (${safeMetadata ? JSON.stringify(safeMetadata) : null}::text)::jsonb
133
134
  )
134
135
  `.catch((err: Error) => console.error("[logging] DB write failed:", err.message));
135
136
  }
@@ -194,7 +195,7 @@ const list = async (
194
195
  WHERE source = ${filterSource}
195
196
  AND (${filterLevel}::text IS NULL OR level = ${filterLevel})
196
197
  AND (${filterSinceHours}::int IS NULL OR created_at >= now() - (${filterSinceHours}::int * INTERVAL '1 hour'))
197
- AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\')
198
+ AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\\')
198
199
  `;
199
200
 
200
201
  dataRows = await sql`
@@ -203,7 +204,7 @@ const list = async (
203
204
  WHERE source = ${filterSource}
204
205
  AND (${filterLevel}::text IS NULL OR level = ${filterLevel})
205
206
  AND (${filterSinceHours}::int IS NULL OR created_at >= now() - (${filterSinceHours}::int * INTERVAL '1 hour'))
206
- AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\')
207
+ AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\\')
207
208
  ORDER BY created_at DESC
208
209
  LIMIT ${perPage} OFFSET ${offset}
209
210
  `;
@@ -214,7 +215,7 @@ const list = async (
214
215
  WHERE source = ANY(${filterSourcesLiteral}::text[])
215
216
  AND (${filterLevel}::text IS NULL OR level = ${filterLevel})
216
217
  AND (${filterSinceHours}::int IS NULL OR created_at >= now() - (${filterSinceHours}::int * INTERVAL '1 hour'))
217
- AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\')
218
+ AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\\')
218
219
  `;
219
220
 
220
221
  dataRows = await sql`
@@ -223,7 +224,7 @@ const list = async (
223
224
  WHERE source = ANY(${filterSourcesLiteral}::text[])
224
225
  AND (${filterLevel}::text IS NULL OR level = ${filterLevel})
225
226
  AND (${filterSinceHours}::int IS NULL OR created_at >= now() - (${filterSinceHours}::int * INTERVAL '1 hour'))
226
- AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\')
227
+ AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\\')
227
228
  ORDER BY created_at DESC
228
229
  LIMIT ${perPage} OFFSET ${offset}
229
230
  `;
@@ -233,7 +234,7 @@ const list = async (
233
234
  FROM logging.entries
234
235
  WHERE (${filterLevel}::text IS NULL OR level = ${filterLevel})
235
236
  AND (${filterSinceHours}::int IS NULL OR created_at >= now() - (${filterSinceHours}::int * INTERVAL '1 hour'))
236
- AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\')
237
+ AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\\')
237
238
  `;
238
239
 
239
240
  dataRows = await sql`
@@ -241,7 +242,7 @@ const list = async (
241
242
  FROM logging.entries
242
243
  WHERE (${filterLevel}::text IS NULL OR level = ${filterLevel})
243
244
  AND (${filterSinceHours}::int IS NULL OR created_at >= now() - (${filterSinceHours}::int * INTERVAL '1 hour'))
244
- AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\')
245
+ AND (${searchPattern}::text IS NULL OR message ILIKE ${searchPattern} ESCAPE '\\' OR metadata::text ILIKE ${searchPattern} ESCAPE '\\')
245
246
  ORDER BY created_at DESC
246
247
  LIMIT ${perPage} OFFSET ${offset}
247
248
  `;
@@ -321,8 +322,8 @@ const timeseries = async (
321
322
  AND (${level}::text IS NULL OR entries.level = ${level})
322
323
  AND (
323
324
  ${searchPattern}::text IS NULL
324
- OR entries.message ILIKE ${searchPattern} ESCAPE '\'
325
- OR entries.metadata::text ILIKE ${searchPattern} ESCAPE '\'
325
+ OR entries.message ILIKE ${searchPattern} ESCAPE '\\'
326
+ OR entries.metadata::text ILIKE ${searchPattern} ESCAPE '\\'
326
327
  )
327
328
  GROUP BY at, entries.level
328
329
  )
@@ -369,8 +370,8 @@ const statsBy = async (
369
370
  AND (${level}::text IS NULL OR level = ${level})
370
371
  AND (
371
372
  ${searchPattern}::text IS NULL
372
- OR message ILIKE ${searchPattern} ESCAPE '\'
373
- OR metadata::text ILIKE ${searchPattern} ESCAPE '\'
373
+ OR message ILIKE ${searchPattern} ESCAPE '\\'
374
+ OR metadata::text ILIKE ${searchPattern} ESCAPE '\\'
374
375
  )
375
376
  GROUP BY level
376
377
  ORDER BY count DESC, key ASC
@@ -387,8 +388,8 @@ const statsBy = async (
387
388
  AND (${level}::text IS NULL OR level = ${level})
388
389
  AND (
389
390
  ${searchPattern}::text IS NULL
390
- OR message ILIKE ${searchPattern} ESCAPE '\'
391
- OR metadata::text ILIKE ${searchPattern} ESCAPE '\'
391
+ OR message ILIKE ${searchPattern} ESCAPE '\\'
392
+ OR metadata::text ILIKE ${searchPattern} ESCAPE '\\'
392
393
  )
393
394
  GROUP BY source
394
395
  ORDER BY count DESC, key ASC
@@ -466,6 +467,7 @@ const summary = async (): Promise<LogSummary> => {
466
467
 
467
468
  /** Admin service object for querying/managing logs. */
468
469
  export const logging = {
470
+ webVitals: readWebVitals,
469
471
  list,
470
472
  getById,
471
473
  getSources,
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import type { SyncEvent } from "@k2b/sync";
3
3
  import { sql } from "bun";
4
+ import { getProcessApplicationId } from "../../_internal/process-identity";
4
5
  import type { PaginationParams } from "../../contracts/shared";
5
6
  import { escapeLikePattern, parsePgJsonRecord, toPgTextArray } from "../postgres";
6
7
  import { isSensitiveMetadataKey, REDACTED, redactMetadata } from "./redaction";
@@ -1034,7 +1035,7 @@ const detailAttributes = (detail: Record<string, unknown>): TraceAttributes => {
1034
1035
  };
1035
1036
 
1036
1037
  /** The app hosting this process; Sync events carry no app name. */
1037
- const processAppId = (): string | undefined => process.env.APP_ID?.trim() || undefined;
1038
+ const processAppId = getProcessApplicationId;
1038
1039
 
1039
1040
  const traceSyncEvent = async (event: SyncEvent, application?: string): Promise<void> => {
1040
1041
  const resource = event.resource;
@@ -0,0 +1,81 @@
1
+ import { sql } from "bun";
2
+ import { z } from "zod";
3
+
4
+ const ranges = {
5
+ "1h": { hours: 1, bucket: 60 },
6
+ "6h": { hours: 6, bucket: 300 },
7
+ "24h": { hours: 24, bucket: 900 },
8
+ "7d": { hours: 168, bucket: 3600 },
9
+ "30d": { hours: 720, bucket: 21600 },
10
+ } as const;
11
+ const querySchema = z.object({
12
+ range: z.enum(["1h", "6h", "24h", "7d", "30d"]).default("24h"),
13
+ appId: z.string().max(64).default(""),
14
+ route: z.string().max(200).default(""),
15
+ page: z.number().int().min(1).max(100000).default(1),
16
+ });
17
+ export type WebVitalsQuery = z.input<typeof querySchema>;
18
+ export type WebVitalName = "LCP" | "INP" | "CLS";
19
+ export type WebVitalMetric = { name: WebVitalName; count: number; p75: number };
20
+ export type WebVitalsOverview = {
21
+ summary: WebVitalMetric[];
22
+ series: (WebVitalMetric & { bucket: number })[];
23
+ routes: (WebVitalMetric & { appId: string; route: string })[];
24
+ totalRoutes: number;
25
+ page: number;
26
+ perPage: number;
27
+ };
28
+
29
+ /** Admin diagnostics. Deduplicate the last valid report inside [since, until).
30
+ * Attribute it to its last receipt time; this is not navigation-start time.
31
+ * Only aggregates leave Postgres. Retention applies before this read.
32
+ */
33
+ export async function readWebVitals(input: WebVitalsQuery, until = new Date()): Promise<WebVitalsOverview> {
34
+ const query = querySchema.parse(input);
35
+ const { hours, bucket } = ranges[query.range];
36
+ const since = new Date(until.getTime() - hours * 3600000);
37
+ const perPage = 50;
38
+ const [result] = await sql<Omit<WebVitalsOverview, "page" | "perPage">[]>`
39
+ WITH stored AS MATERIALIZED (
40
+ SELECT id, created_at,
41
+ logging.object_metadata(metadata) AS metadata
42
+ FROM logging.entries
43
+ WHERE source = 'web-vitals' AND level = 'info' AND created_at >= ${since} AND created_at < ${until}
44
+ ), candidates AS MATERIALIZED (
45
+ SELECT id, created_at, metadata->>'appId' AS app, metadata->>'routeTemplate' AS route,
46
+ metadata->>'name' AS name, metadata->>'id' AS metric_id,
47
+ CASE WHEN jsonb_typeof(metadata->'value') = 'number' THEN (metadata->>'value')::numeric END AS value
48
+ FROM stored
49
+ WHERE jsonb_typeof(metadata->'appId') = 'string' AND metadata->>'appId' ~ '^[a-z0-9-]{1,64}$'
50
+ AND jsonb_typeof(metadata->'routeTemplate') = 'string'
51
+ AND length(metadata->>'routeTemplate') BETWEEN 1 AND 200 AND metadata->>'routeTemplate' ~ '^/[a-zA-Z0-9_/:.*-]*$'
52
+ AND jsonb_typeof(metadata->'id') = 'string' AND length(metadata->>'id') BETWEEN 1 AND 100
53
+ AND metadata->>'name' IN ('LCP','INP','CLS')
54
+ AND (${query.appId} = '' OR metadata->>'appId' = ${query.appId})
55
+ AND (${query.route} = '' OR metadata->>'routeTemplate' = ${query.route})
56
+ ), latest AS MATERIALIZED (
57
+ SELECT DISTINCT ON (app, route, name, metric_id) app, route, name, value::double precision AS value, created_at
58
+ FROM candidates WHERE value >= 0 AND value <= 1.7976931348623157e308::numeric
59
+ ORDER BY app, route, name, metric_id, created_at DESC, id DESC
60
+ ), route_stats AS MATERIALIZED (
61
+ SELECT app, route, name, count(*)::int AS count,
62
+ percentile_cont(0.75) WITHIN GROUP (ORDER BY value) AS p75
63
+ FROM latest GROUP BY app, route, name
64
+ ), route_keys AS MATERIALIZED (
65
+ SELECT DISTINCT app, route FROM route_stats ORDER BY app, route LIMIT ${perPage} OFFSET ${(query.page - 1) * perPage}
66
+ )
67
+ SELECT
68
+ COALESCE((SELECT jsonb_agg(s) FROM (SELECT name, count(*)::int AS count, percentile_cont(0.75) WITHIN GROUP (ORDER BY value) AS p75 FROM latest GROUP BY name ORDER BY name) s), '[]'::jsonb) AS summary,
69
+ COALESCE((SELECT jsonb_agg(s ORDER BY s.bucket, s.name) FROM (SELECT name,
70
+ floor(extract(epoch FROM created_at) / ${bucket}) * ${bucket} * 1000 AS bucket,
71
+ count(*)::int AS count, percentile_cont(0.75) WITHIN GROUP (ORDER BY value) AS p75
72
+ FROM latest GROUP BY name, bucket) s), '[]'::jsonb) AS series,
73
+ COALESCE((SELECT jsonb_agg(s ORDER BY s."appId", s.route, s.name) FROM (
74
+ SELECT l.app AS "appId", l.route, l.name, l.count, l.p75
75
+ FROM route_stats l JOIN route_keys k USING (app, route)
76
+ ) s), '[]'::jsonb) AS routes,
77
+ (SELECT count(*)::int FROM (SELECT DISTINCT app, route FROM route_stats) r) AS "totalRoutes"
78
+ `;
79
+ if (!result) throw new Error("Web Vitals aggregation returned no result");
80
+ return { ...result, page: query.page, perPage };
81
+ }
@@ -9,6 +9,10 @@ import { parsePgJsonValue } from "../postgres";
9
9
  import { isMandatePolicyNarrowing, type MandatePolicyV1, MandatePolicyV1Schema, mandatePolicyAllows, parseMandatePolicy } from "./policy";
10
10
 
11
11
  export {
12
+ type CapabilityGrant,
13
+ CapabilityGrantSchema,
14
+ CapabilityGrantsSchema,
15
+ capabilityGrantAllows,
12
16
  isMandatePolicyNarrowing,
13
17
  MANDATE_POLICY_MAX_IDENTIFIERS,
14
18
  MANDATE_POLICY_VERSION,
@@ -16,6 +20,9 @@ export {
16
20
  MandatePolicyV1Schema,
17
21
  mandatePolicyAllows,
18
22
  parseMandatePolicy,
23
+ type TaskGrant,
24
+ TaskGrantSchema,
25
+ TaskGrantsSchema,
19
26
  } from "./policy";
20
27
 
21
28
  export type MandateState = "active" | "paused" | "revoked";
@@ -176,7 +183,7 @@ const mapMandate = (row: MandateRow): Mandate => ({
176
183
  revokeReason: row.revoke_reason,
177
184
  });
178
185
 
179
- const selectMandate = sql`
186
+ const selectMandate = () => sql`
180
187
  SELECT id, subject_kind, subject_user_id, subject_service_account_id,
181
188
  owner_app_id, workload_type, workload_id, policy, state, revision, expires_at,
182
189
  confirmed_at, confirmation_deadline,
@@ -224,7 +231,7 @@ const parsePolicyForWorkload = (
224
231
 
225
232
  const load = async (mandateId: string, db: SQL): Promise<Mandate | null> => {
226
233
  if (!UUID.safeParse(mandateId).success) return null;
227
- const [row] = await db<MandateRow[]>`${selectMandate} WHERE id = ${mandateId}::uuid`;
234
+ const [row] = await db<MandateRow[]>`${selectMandate()} WHERE id = ${mandateId}::uuid`;
228
235
  return row ? mapMandate(row) : null;
229
236
  };
230
237
 
@@ -437,7 +444,7 @@ export const listMandates = async (config: {
437
444
  const where = conditions.slice(1).reduce((combined, condition) => sql`${combined} AND ${condition}`, conditions[0]!);
438
445
  const [countRows, rows] = await Promise.all([
439
446
  db<{ count: number }[]>`SELECT COUNT(*)::int AS count FROM auth.mandates WHERE ${where}`,
440
- db<MandateRow[]>`${selectMandate} WHERE ${where} ORDER BY created_at DESC, id DESC LIMIT ${perPage} OFFSET ${offset}`,
447
+ db<MandateRow[]>`${selectMandate()} WHERE ${where} ORDER BY created_at DESC, id DESC LIMIT ${perPage} OFFSET ${offset}`,
441
448
  ]);
442
449
  const total = countRows[0]?.count ?? 0;
443
450
  return { items: rows.map(mapMandate), page, perPage, total, hasNext: page * perPage < total };
@@ -616,6 +623,8 @@ export const validateMandateIssueAuthority = async (
616
623
  targetAppId: string;
617
624
  operation: string;
618
625
  actionApproval?: "none" | "approved";
626
+ input?: unknown;
627
+ capabilityApproval?: "none" | "rememberable" | "always";
619
628
  expectedRevision?: number;
620
629
  requestId?: string;
621
630
  },
@@ -669,7 +678,15 @@ export const validateMandateIssueAuthority = async (
669
678
  } catch {
670
679
  return denyMandateIssue(err.forbidden("Mandate policy is invalid"));
671
680
  }
672
- if (!mandatePolicyAllows(policy, { appId: target.data, operation: operation.data, actionApproval: input.actionApproval ?? "none" })) {
681
+ if (
682
+ !mandatePolicyAllows(policy, {
683
+ appId: target.data,
684
+ operation: operation.data,
685
+ actionApproval: input.actionApproval ?? "none",
686
+ input: input.input,
687
+ capabilityApproval: input.capabilityApproval,
688
+ })
689
+ ) {
673
690
  return denyMandateIssue(err.forbidden("Mandate policy does not allow this operation"));
674
691
  }
675
692
  const mandate = mapMandate(row);
@@ -12,6 +12,8 @@ const identifierList = <T extends z.ZodType<string>>(schema: T) =>
12
12
 
13
13
  const operationSchema = z.union([
14
14
  z.literal("search.query"),
15
+ z.literal("runtime.http"),
16
+ z.literal("runtime.database"),
15
17
  z.string().refine((value) => {
16
18
  const [prefix, localId, extra] = value.split(":");
17
19
  return (
@@ -25,12 +27,121 @@ const operationSchema = z.union([
25
27
  }, "Expected a canonical mandate operation"),
26
28
  ]);
27
29
 
30
+ export const CapabilityGrantSchema = z
31
+ .object({
32
+ appId: CapabilityAppIdSchema.describe("Exact discovered application ID."),
33
+ capabilityId: CapabilityLocalIdSchema.describe("Exact local capability ID from discovery, without application prefix."),
34
+ kind: z.enum(["query", "action"]).describe("Discovered capability kind."),
35
+ fixedInput: z.record(z.string(), z.json()).default({}).describe("Exact top-level input values. Omitted fields remain unrestricted."),
36
+ })
37
+ .strict();
38
+ export const CapabilityGrantsSchema = z.array(CapabilityGrantSchema).max(MANDATE_POLICY_MAX_IDENTIFIERS / 2);
39
+ export type CapabilityGrant = z.output<typeof CapabilityGrantSchema>;
40
+
41
+ const httpsUrl = z
42
+ .string()
43
+ .url()
44
+ .max(2000)
45
+ .refine((value) => {
46
+ const url = new URL(value);
47
+ return url.protocol === "https:" && !url.username && !url.password && !url.hash;
48
+ }, "Use HTTPS without credentials or fragment");
49
+ export const TaskGrantSchema = z.union([
50
+ CapabilityGrantSchema,
51
+ z
52
+ .object({
53
+ kind: z.literal("http").describe("Authorize scheduled Code Mode HTTP requests."),
54
+ fixedInput: z
55
+ .object({
56
+ origin: httpsUrl
57
+ .refine((value) => new URL(value).origin === value, "Use an exact origin without path")
58
+ .optional()
59
+ .describe("Exact HTTPS origin, without path, wildcard or trailing slash."),
60
+ url: httpsUrl.optional().describe("Exact full URL, including query parameters."),
61
+ method: z
62
+ .enum(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
63
+ .optional()
64
+ .describe("Exact HTTP method; omit to permit every supported method."),
65
+ })
66
+ .strict()
67
+ .default({})
68
+ .describe("Fixed input values. Omitted fields are unrestricted; empty input permits every supported target and operation."),
69
+ })
70
+ .strict(),
71
+ z
72
+ .object({
73
+ kind: z.literal("database").describe("Authorize scheduled Code Mode RSQL operations."),
74
+ fixedInput: z
75
+ .object({
76
+ resourceId: z.string().min(1).max(80).optional().describe("Exact Studio app ID whose database may be used."),
77
+ operation: z
78
+ .enum([
79
+ "connect",
80
+ "status",
81
+ "export",
82
+ "clear",
83
+ "reset",
84
+ "tables.list",
85
+ "tables.create",
86
+ "tables.update",
87
+ "tables.delete",
88
+ "schema.get",
89
+ "rows.list",
90
+ "rows.get",
91
+ "rows.insert",
92
+ "rows.update",
93
+ "rows.delete",
94
+ "query",
95
+ ])
96
+ .optional()
97
+ .describe("Exact database operation; connect needs its own grant when operation is fixed."),
98
+ table: z
99
+ .string()
100
+ .min(1)
101
+ .max(63)
102
+ .optional()
103
+ .describe("Exact table; applies to row and schema operations, not arbitrary SQL queries."),
104
+ })
105
+ .strict()
106
+ .default({})
107
+ .describe("Fixed input values. Omitted fields are unrestricted; empty input permits every supported target and operation."),
108
+ })
109
+ .strict(),
110
+ ]);
111
+ export const TaskGrantsSchema = z.array(TaskGrantSchema).max(MANDATE_POLICY_MAX_IDENTIFIERS / 2);
112
+ export type TaskGrant = z.output<typeof TaskGrantSchema>;
113
+
114
+ const equalJson = (a: unknown, b: unknown): boolean => {
115
+ if (a === b) return true;
116
+ if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
117
+ if (Array.isArray(a) || Array.isArray(b))
118
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => equalJson(v, b[i]));
119
+ const ak = Object.keys(a),
120
+ bk = Object.keys(b);
121
+ return ak.length === bk.length && ak.every((key) => Object.hasOwn(b, key) && equalJson(Reflect.get(a, key), Reflect.get(b, key)));
122
+ };
123
+ export const capabilityGrantAllows = (
124
+ grant: CapabilityGrant,
125
+ input: { appId: string; capabilityId: string; kind: "query" | "action"; input: unknown },
126
+ ): boolean =>
127
+ grant.appId === input.appId &&
128
+ grant.capabilityId === input.capabilityId &&
129
+ grant.kind === input.kind &&
130
+ Object.entries(grant.fixedInput).every(
131
+ ([key, value]) =>
132
+ input.input !== null &&
133
+ typeof input.input === "object" &&
134
+ Object.hasOwn(input.input, key) &&
135
+ equalJson(value, Reflect.get(input.input, key)),
136
+ );
137
+
28
138
  export const MandatePolicyV1Schema = z
29
139
  .object({
30
140
  version: z.literal(MANDATE_POLICY_VERSION),
31
141
  apps: z.union([z.literal("*"), identifierList(CapabilityAppIdSchema)]),
32
142
  operations: z.union([z.literal("*"), identifierList(operationSchema)]),
33
143
  actions: z.enum(["deny", "require_approval", "preapproved"]),
144
+ grants: TaskGrantsSchema.optional(),
34
145
  })
35
146
  .strict()
36
147
  .superRefine((policy, context) => {
@@ -70,14 +181,66 @@ const ACTION_AUTHORITY = { deny: 0, require_approval: 1, preapproved: 2 } as con
70
181
  export const isMandatePolicyNarrowing = (current: MandatePolicyV1, next: MandatePolicyV1): boolean =>
71
182
  setAllows(current.apps, next.apps) &&
72
183
  setAllows(current.operations, next.operations) &&
73
- ACTION_AUTHORITY[next.actions] <= ACTION_AUTHORITY[current.actions];
184
+ ACTION_AUTHORITY[next.actions] <= ACTION_AUTHORITY[current.actions] &&
185
+ (current.grants === undefined
186
+ ? !next.grants?.some((grant) => grant.kind === "http" || grant.kind === "database")
187
+ : next.grants !== undefined &&
188
+ next.grants.every((grant) =>
189
+ current.grants!.some((existing) => {
190
+ if (existing.kind !== grant.kind) return false;
191
+ if ("appId" in existing && "appId" in grant && (existing.appId !== grant.appId || existing.capabilityId !== grant.capabilityId))
192
+ return false;
193
+ return Object.entries(existing.fixedInput).every(([key, value]) => equalJson(value, Reflect.get(grant.fixedInput, key)));
194
+ }),
195
+ ));
74
196
 
75
197
  export const mandatePolicyAllows = (
76
198
  policy: MandatePolicyV1,
77
- input: { appId: string; operation: string; actionApproval: "none" | "approved" },
199
+ input: {
200
+ appId: string;
201
+ operation: string;
202
+ actionApproval: "none" | "approved";
203
+ input?: unknown;
204
+ capabilityApproval?: "none" | "rememberable" | "always";
205
+ },
78
206
  ): boolean => {
79
207
  if (policy.apps !== "*" && !policy.apps.includes(input.appId)) return false;
80
208
  if (policy.operations !== "*" && !policy.operations.includes(input.operation)) return false;
209
+ if (input.operation === "runtime.http" || input.operation === "runtime.database") {
210
+ const kind = input.operation === "runtime.http" ? "http" : "database";
211
+ return (
212
+ input.appId === "assistant" &&
213
+ !!policy.grants?.some(
214
+ (grant) =>
215
+ grant.kind === kind &&
216
+ Object.entries(grant.fixedInput).every(
217
+ ([key, value]) =>
218
+ input.input !== null &&
219
+ typeof input.input === "object" &&
220
+ Object.hasOwn(input.input, key) &&
221
+ equalJson(value, Reflect.get(input.input, key)),
222
+ ),
223
+ )
224
+ );
225
+ }
226
+ if (policy.grants !== undefined) {
227
+ const [operation, capabilityId] = input.operation.split(":");
228
+ const kind =
229
+ operation === "capability.query"
230
+ ? "query"
231
+ : operation === "capability.action.run" || operation === "capability.action.review"
232
+ ? "action"
233
+ : null;
234
+ if (
235
+ !kind ||
236
+ !capabilityId ||
237
+ !policy.grants.some(
238
+ (grant) => "appId" in grant && capabilityGrantAllows(grant, { appId: input.appId, capabilityId, kind, input: input.input }),
239
+ )
240
+ )
241
+ return false;
242
+ if (kind === "action" && input.capabilityApproval !== "none" && input.capabilityApproval !== "rememberable") return false;
243
+ }
81
244
  if (!input.operation.startsWith("capability.action.run:")) return true;
82
245
  if (policy.actions === "deny") return false;
83
246
  if (policy.actions === "require_approval") return input.actionApproval === "approved";