@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
@@ -0,0 +1,262 @@
1
+ import { z } from "zod";
2
+ import { readBoundedJson } from "../_internal/bounded-json";
3
+ import { getCapability } from "../_internal/registry";
4
+ import { recordCapabilityExecution } from "../capabilities/executions";
5
+ import { exactStream } from "../capabilities/stream-body";
6
+ import type { CapabilityStream } from "../contracts/capabilities";
7
+ import {
8
+ CAPABILITY_MAX_RESULT_BYTES,
9
+ CAPABILITY_ORIGIN_HEADER,
10
+ CapabilityErrorSchema,
11
+ CapabilityOriginSchema,
12
+ CapabilityStreamSchema,
13
+ CapabilityStreamStatusSchema,
14
+ capabilityResultSchema,
15
+ } from "../contracts/capabilities";
16
+ import type { RequestAuthority } from "../server";
17
+ import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
18
+ import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
19
+ import { signInvocationToken } from "../services/identity/invocation-token";
20
+ import { withActiveIdentitySigner } from "../services/identity/key-ring";
21
+ import { logger } from "../services/logging";
22
+ import { decryptSecret, encryptSecret } from "../services/secrets";
23
+ import { LOCALE_HEADER } from "../shared/locale";
24
+ import type { CapabilityDispatchDependencies } from "./capabilities";
25
+
26
+ const log = logger("capability-streams");
27
+
28
+ const Grant = z.object({
29
+ purpose: z.literal("capability-stream-v2"),
30
+ continuation: z.string().nullable(),
31
+ origin: CapabilityOriginSchema.default("http"),
32
+ appId: z.string(),
33
+ kind: z.enum(["queries", "actions"]),
34
+ capabilityId: z.string(),
35
+ schemaHash: z.string(),
36
+ authority: z.string(),
37
+ stream: CapabilityStreamSchema,
38
+ requestId: z.string(),
39
+ });
40
+ const binding = (authority: RequestAuthority) => JSON.stringify(invocationAuthorityFromRequest(authority));
41
+ export async function sealCapabilityStream(
42
+ stream: CapabilityStream,
43
+ params: {
44
+ appId: string;
45
+ kind: "queries" | "actions";
46
+ capabilityId: string;
47
+ schemaHash: string;
48
+ authority: RequestAuthority;
49
+ requestId: string;
50
+ origin?: z.infer<typeof CapabilityOriginSchema>;
51
+ continuation?: string;
52
+ },
53
+ ): Promise<CapabilityStream> {
54
+ if (Buffer.byteLength(stream.id) > 2048) throw new Error("Provider stream ID exceeds 2 KiB");
55
+ const grant = Grant.parse({
56
+ ...params,
57
+ purpose: "capability-stream-v2",
58
+ continuation: params.continuation ?? null,
59
+ authority: binding(params.authority),
60
+ stream,
61
+ });
62
+ return CapabilityStreamSchema.parse({ ...stream, id: await encryptSecret(grant) });
63
+ }
64
+ const denied = (status = 403, code = "STREAM_DENIED") =>
65
+ Response.json({ code, message: "Stream unavailable or not authorized" }, { status });
66
+
67
+ /** A transfer continues an already-authorized operation; it cannot manufacture a new Action. */
68
+ export async function dispatchCapabilityStream(
69
+ request: Request,
70
+ authority: RequestAuthority,
71
+ verb: string,
72
+ dependencies: CapabilityDispatchDependencies & {
73
+ recordStreamExecution?: typeof recordCapabilityExecution;
74
+ continuation?: string;
75
+ allow?: (operation: { appId: string; capabilityId: string }) => Promise<boolean>;
76
+ } = {},
77
+ ): Promise<Response> {
78
+ const id = request.headers.get("x-cloud-stream-id");
79
+ if (!id || id.length > 8192 || !["read", "write", "status", "abort"].includes(verb)) return denied(400);
80
+ let grant: z.infer<typeof Grant>;
81
+ try {
82
+ grant = Grant.parse(await decryptSecret(id));
83
+ } catch {
84
+ return denied();
85
+ }
86
+ try {
87
+ if (grant.authority !== binding(authority)) return denied();
88
+ } catch {
89
+ return denied();
90
+ }
91
+ if (grant.continuation !== (dependencies.continuation ?? null)) return denied();
92
+ if (dependencies.allow && !(await dependencies.allow(grant))) return denied();
93
+ if (Date.parse(grant.stream.expiresAt) <= Date.now()) return denied(410, "STREAM_EXPIRED");
94
+ if ((verb === "read") !== (grant.stream.direction === "read")) return denied();
95
+ if (
96
+ authority.credentialKind === "oauth" &&
97
+ !authority.scopes.some((scope) => scope === "admin" || scope === (grant.kind === "actions" ? "write" : "read"))
98
+ )
99
+ return denied();
100
+ const startedAt = new Date();
101
+ let recorded = false;
102
+ let destructive = false;
103
+ const record = async (status: number, errorCode: string | null = null) => {
104
+ if (recorded || (verb !== "read" && verb !== "write")) return;
105
+ recorded = true;
106
+ const actor = authority.actor;
107
+ const actorId = actor.kind === "user" ? actor.user.id : actor.serviceAccount.id;
108
+ const subject = authority.accessSubject;
109
+ const subjectId = subject.type === "user" ? subject.userId : subject.serviceAccountId;
110
+ await (dependencies.recordStreamExecution ?? recordCapabilityExecution)({
111
+ requestId: grant.requestId,
112
+ origin: grant.origin,
113
+ appId: grant.appId,
114
+ capability: `${grant.appId}.${grant.capabilityId}`,
115
+ kind: grant.kind === "queries" ? "query" : "action",
116
+ destructive,
117
+ actorKind: actor.kind,
118
+ actorId,
119
+ userId: actor.kind === "user" ? actor.user.id : (actor.delegatedUser?.id ?? null),
120
+ accessSubject: subjectId !== actorId ? { type: subject.type, id: subjectId } : null,
121
+ status: status < 400 ? "succeeded" : status === 403 ? "denied" : status === 504 ? "timed_out" : "failed",
122
+ errorCode,
123
+ inputMeta: { type: "object", keys: [`stream.${verb}`], omittedKeys: 0 },
124
+ outputMeta: status < 400 ? { type: "number", value: grant.stream.size } : null,
125
+ startedAt,
126
+ completedAt: new Date(),
127
+ }).catch(() => log.error("Stream execution could not be recorded", { requestId: grant.requestId }));
128
+ };
129
+ const signal = AbortSignal.any([request.signal, AbortSignal.timeout(300_000)]);
130
+ try {
131
+ const entry = await (dependencies.getCapability ?? getCapability)(grant.appId);
132
+ const operation = entry?.manifest[grant.kind].find((item) => item.localId === grant.capabilityId);
133
+ if (
134
+ !entry ||
135
+ !operation ||
136
+ operation.schemaHash !== grant.schemaHash ||
137
+ operation.stream?.direction !== grant.stream.direction ||
138
+ grant.stream.size > operation.stream.maxBytes
139
+ )
140
+ return denied(409, "SCHEMA_MISMATCH");
141
+ destructive = "destructive" in operation && operation.destructive;
142
+ const signed = await (dependencies.withActiveSigner ?? withActiveIdentitySigner)(
143
+ "invocation",
144
+ (signer) =>
145
+ (dependencies.signInvocation ?? signInvocationToken)({
146
+ targetAppId: grant.appId,
147
+ callingAppId: "core",
148
+ operation: capabilityInvocationOperation(grant.kind, grant.capabilityId),
149
+ schemaHash: operation.schemaHash,
150
+ authority: invocationAuthorityFromRequest(authority),
151
+ requestId: grant.requestId,
152
+ signer,
153
+ issuer: signer.issuer,
154
+ }),
155
+ { signal, timeoutMs: 30_000 },
156
+ );
157
+ const headers = new Headers({
158
+ authorization: `Bearer ${signed.token}`,
159
+ "x-request-id": grant.requestId,
160
+ "x-cloud-capability-schema-hash": operation.schemaHash,
161
+ "x-cloud-stream-offer": Buffer.from(JSON.stringify(grant.stream)).toString("base64url"),
162
+ "content-type": "application/octet-stream",
163
+ });
164
+ headers.set(CAPABILITY_ORIGIN_HEADER, grant.origin);
165
+ if (request.headers.has(LOCALE_HEADER)) headers.set(LOCALE_HEADER, request.headers.get(LOCALE_HEADER)!);
166
+ if (request.headers.has("accept-language")) headers.set("accept-language", request.headers.get("accept-language")!);
167
+ const body =
168
+ verb === "write"
169
+ ? exactStream(
170
+ request.body ??
171
+ new ReadableStream({
172
+ start(c) {
173
+ c.close();
174
+ },
175
+ }),
176
+ grant.stream.size,
177
+ )
178
+ : undefined;
179
+ const response = await (dependencies.fetch ?? fetch)(
180
+ `${entry.endpoint}/streams/${grant.kind}/${encodeURIComponent(grant.capabilityId)}/${verb}`,
181
+ {
182
+ method: "POST",
183
+ headers,
184
+ body,
185
+ signal,
186
+ redirect: "error",
187
+ // @ts-expect-error Bun and Node stream request bodies use duplex.
188
+ duplex: body ? "half" : undefined,
189
+ },
190
+ );
191
+ log.info("Capability stream response", {
192
+ requestId: grant.requestId,
193
+ appId: grant.appId,
194
+ capabilityId: grant.capabilityId,
195
+ verb,
196
+ status: response.status,
197
+ bytes: grant.stream.size,
198
+ });
199
+ if (verb === "read" && response.ok) {
200
+ if (!response.body) return denied(502, "STREAM_FAILED");
201
+ const reader = exactStream(response.body, grant.stream.size).getReader();
202
+ return new Response(
203
+ new ReadableStream({
204
+ async pull(controller) {
205
+ try {
206
+ const chunk = await reader.read();
207
+ if (chunk.done) {
208
+ await record(200);
209
+ controller.close();
210
+ } else controller.enqueue(chunk.value);
211
+ } catch {
212
+ await record(502, "STREAM_FAILED");
213
+ controller.error(new Error("Incomplete capability stream"));
214
+ }
215
+ },
216
+ async cancel(reason) {
217
+ await reader.cancel(reason).catch(() => undefined);
218
+ await record(499, "REQUEST_CANCELLED");
219
+ },
220
+ }),
221
+ {
222
+ headers: {
223
+ "content-type": grant.stream.mediaType,
224
+ "content-length": String(grant.stream.size),
225
+ "cache-control": "no-store",
226
+ "x-content-type-options": "nosniff",
227
+ },
228
+ },
229
+ );
230
+ }
231
+ const receipt = await readBoundedJson(response, CAPABILITY_MAX_RESULT_BYTES);
232
+ if (!receipt.ok) {
233
+ await record(502, "STREAM_FAILED");
234
+ return denied(502, "STREAM_FAILED");
235
+ }
236
+ if (response.ok) {
237
+ const parsed = (verb === "write" ? capabilityResultSchema(z.unknown()) : CapabilityStreamStatusSchema).safeParse(receipt.data);
238
+ if (!parsed.success) {
239
+ await record(502, "STREAM_FAILED");
240
+ return denied(502, "STREAM_FAILED");
241
+ }
242
+ const nested =
243
+ verb === "write" ? parsed.data : "state" in parsed.data && parsed.data.state === "completed" ? parsed.data.result : null;
244
+ if (nested && typeof nested === "object" && "stream" in nested) {
245
+ await record(502, "STREAM_FAILED");
246
+ return denied(502, "STREAM_FAILED");
247
+ }
248
+ }
249
+ if (!response.ok) {
250
+ const error = CapabilityErrorSchema.safeParse(receipt.data);
251
+ const status = response.status === 401 ? 502 : response.status;
252
+ await record(status, error.success ? error.data.code : "STREAM_FAILED");
253
+ if (!error.success || response.status === 401) return denied(status, "STREAM_FAILED");
254
+ } else await record(response.status);
255
+ return Response.json(receipt.data, { status: response.status, headers: { "cache-control": "no-store" } });
256
+ } catch {
257
+ const status = request.signal.aborted ? 499 : signal.aborted ? 504 : 502;
258
+ const code = status === 499 ? "REQUEST_CANCELLED" : status === 504 ? "DEADLINE_EXCEEDED" : "STREAM_FAILED";
259
+ await record(status, code);
260
+ return Response.json({ code, message: "Transfer interrupted; inspect write status before retrying" }, { status });
261
+ }
262
+ }
package/src/api/help.ts CHANGED
@@ -1,78 +1,42 @@
1
1
  import { Hono, type MiddlewareHandler } from "hono";
2
- import { createHelpCatalog, findHelpDocument, resolveAppHelp, searchHelpCatalog } from "../_internal/help-catalog";
3
- import { getApp, getHelp } from "../_internal/registry";
4
- import type { AppRegistryEntry, HelpRegistryEntry } from "../contracts/registry";
5
2
  import { type AuthContext, auth, getLocale } from "../server";
3
+ import { createHelpReader, type HelpReaderFactory } from "../services/help";
6
4
  import type { HelpDocumentPayload, HelpSearchPayload } from "../shared/help";
7
5
  import { renderHelpMarkdown } from "../shared/markdown";
8
- import { type HelpApiMessages, helpApiMessages } from "./help-messages";
6
+ import { helpApiMessages } from "./help-messages";
9
7
 
10
8
  export type HelpRouteDependencies = {
11
- getApp?: (appId: string) => Promise<AppRegistryEntry | null>;
12
- getHelp?: (appId: string) => Promise<HelpRegistryEntry | null>;
9
+ help?: HelpReaderFactory;
13
10
  authenticate?: MiddlewareHandler<AuthContext>;
14
11
  renderMarkdown?: (markdown: string) => string;
15
12
  };
16
13
 
17
- const resolveHelp = async (appId: string, dependencies: HelpRouteDependencies, t: HelpApiMessages) => {
18
- const resolved = await resolveAppHelp(appId, { getApp: dependencies.getApp ?? getApp, getHelp: dependencies.getHelp ?? getHelp });
19
- if (resolved.status === "missing") {
20
- return Response.json({ code: "APP_UNAVAILABLE", message: t.unavailable({ appId }) }, { status: 404 });
21
- }
22
- if (resolved.status === "stale") {
23
- return Response.json({ code: "HELP_STALE", message: t.stale({ appId }) }, { status: 503 });
24
- }
25
- return resolved;
26
- };
27
-
28
14
  export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
29
- const renderedByApp = new Map<string, { manifestHash: string; documents: Map<string, string> }>();
30
- const renderMarkdown = dependencies.renderMarkdown ?? renderHelpMarkdown;
31
- const renderDocument = (help: HelpRegistryEntry, locale: string, documentId: string, markdown: string): string => {
32
- let cached = renderedByApp.get(help.appId);
33
- if (!cached || cached.manifestHash !== help.manifestHash) {
34
- cached = { manifestHash: help.manifestHash, documents: new Map() };
35
- renderedByApp.set(help.appId, cached);
36
- }
37
- const cacheKey = `${locale}:${documentId}`;
38
- const html = cached.documents.get(cacheKey);
39
- if (html !== undefined) return html;
40
- const rendered = renderMarkdown(markdown);
41
- cached.documents.set(cacheKey, rendered);
42
- return rendered;
43
- };
44
-
15
+ const reader = dependencies.help ?? createHelpReader;
45
16
  return new Hono<AuthContext>()
46
17
  .use(dependencies.authenticate ?? auth.requireRole("*"))
47
18
  .get("/help/v1/:appId/search", async (c) => {
48
19
  const locale = getLocale(c);
49
- const { t } = helpApiMessages.resolve([locale]);
50
- const resolved = await resolveHelp(c.req.param("appId"), dependencies, t);
51
- if (resolved instanceof Response) return resolved;
52
20
  const query = c.req.query("q")?.trim().slice(0, 200) ?? "";
53
- const catalog = createHelpCatalog([resolved.help], locale);
54
21
  const payload: HelpSearchPayload = {
55
- locale: catalog[0]?.locale ?? resolved.help.baseLocale ?? "en",
56
- ids: query ? searchHelpCatalog(catalog, { query, appId: resolved.app.id, limit: 25 }).map((document) => document.documentId) : [],
22
+ locale,
23
+ ids: (await reader(locale).search({ query, appId: c.req.param("appId"), limit: 25 })).map((document) => document.documentId),
57
24
  };
58
25
  return c.json(payload);
59
26
  })
60
27
  .get("/help/v1/:appId/documents/:documentId", async (c) => {
61
28
  const locale = getLocale(c);
62
29
  const { t } = helpApiMessages.resolve([locale]);
63
- const resolved = await resolveHelp(c.req.param("appId"), dependencies, t);
64
- if (resolved instanceof Response) return resolved;
65
- const document = findHelpDocument(createHelpCatalog([resolved.help], locale), resolved.app.id, c.req.param("documentId"));
30
+ const document = await reader(locale).read({ appId: c.req.param("appId"), documentId: c.req.param("documentId") });
66
31
  if (!document) return c.json({ code: "HELP_NOT_FOUND", message: t.notFound }, 404);
67
32
  const payload: HelpDocumentPayload = {
68
33
  locale: document.locale,
69
34
  id: document.documentId,
70
35
  title: document.title,
71
36
  markdown: document.markdown,
72
- html: renderDocument(resolved.help, document.locale, document.documentId, document.markdown),
37
+ html: (dependencies.renderMarkdown ?? renderHelpMarkdown)(document.markdown),
73
38
  };
74
39
  return c.json(payload);
75
40
  });
76
41
  };
77
-
78
42
  export type HelpApiType = ReturnType<typeof createHelpRoutes>;
package/src/api/index.ts CHANGED
@@ -16,21 +16,24 @@
16
16
  import { Hono } from "hono";
17
17
  import { prettyJSON } from "hono/pretty-json";
18
18
  import { createAiApprovalPreferenceRoutes } from "../ai/approval-routes";
19
+ import { createCodeCapabilityRoutes } from "../ai/code-capability-routes";
19
20
  import { aiProjectsRoutes } from "../ai/projects-routes";
20
21
  import { aiRoutes } from "../ai/routes";
21
22
  import { aiSkillsRoutes } from "../ai/skills-routes";
22
23
  import accountsEntitiesRoutes from "./accounts-entities";
23
- import adminAiUsageRoutes from "./admin-ai-usage";
24
24
  import adminAiProjectsRoutes from "./admin-ai-projects";
25
+ import adminAiQuotaRoutes from "./admin-ai-quotas";
25
26
  import adminAiSkillsRoutes from "./admin-ai-skills";
27
+ import adminAiUsageRoutes from "./admin-ai-usage";
26
28
  import adminCoreSettingsRoutes from "./admin-core-settings";
27
29
  import adminIdentityRoutes from "./admin-identity";
28
30
  import adminLifecycleRoutes from "./admin-lifecycle";
29
31
  import adminLinuxIdentityRoutes from "./admin-linux-identities";
32
+ import adminRailRoutes from "./admin-rail";
30
33
  import { adminAnnouncementRoutes, announcementRoutes } from "./announcements";
34
+ import { createAppApprovalRoutes } from "./app-approval";
31
35
  import { appDiscoveryRoutes } from "./apps";
32
36
  import { createAuthRoutes } from "./auth";
33
- import { createAppApprovalRoutes } from "./app-approval";
34
37
  import { createCapabilityRoutes } from "./capabilities";
35
38
  import { createHelpRoutes } from "./help";
36
39
  import { createMcpRoutes } from "./mcp";
@@ -66,10 +69,12 @@ const buildCoreApi = (options: CoreApiOptions) => {
66
69
  .route("/apps", appDiscoveryRoutes)
67
70
  .route("/announcements", announcementRoutes)
68
71
  .route("/admin/core/ai-usage", adminAiUsageRoutes)
72
+ .route("/admin/core/ai-quotas", adminAiQuotaRoutes)
69
73
  .route("/admin/core/ai-projects", adminAiProjectsRoutes)
70
74
  .route("/admin/core/ai-skills", adminAiSkillsRoutes)
71
75
  .route("/admin/core/announcements", adminAnnouncementRoutes)
72
76
  .route("/admin/core/settings", adminCoreSettingsRoutes)
77
+ .route("/admin/core/rail", adminRailRoutes)
73
78
  .route("/admin/core/linux-identities", adminLinuxIdentityRoutes)
74
79
  .route("/admin/identity", adminIdentityRoutes)
75
80
  .route("/admin/sync", createSyncOpsProxyRoutes())
@@ -77,6 +82,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
77
82
  .route("/ai/approval-preferences", approvalPreferenceRoutes)
78
83
  .route("/ai/projects", aiProjectsRoutes)
79
84
  .route("/ai/skills", aiSkillsRoutes)
85
+ .route("/_internal/ai/code", createCodeCapabilityRoutes())
80
86
  .route("/ai", aiRoutes)
81
87
  .route("/", helpRoutes)
82
88
  .route("/", capabilityRoutes)
package/src/api/mcp.ts CHANGED
@@ -15,17 +15,9 @@ import {
15
15
  import { Hono, type MiddlewareHandler } from "hono";
16
16
  import { z } from "zod";
17
17
  import { readBoundedJson } from "../_internal/bounded-json";
18
- import {
19
- findHelpDocument,
20
- HELP_SEARCH_MAX_LIMIT,
21
- type HelpCatalogDocument,
22
- helpResourceUri,
23
- loadHelpCatalog,
24
- parseHelpResourceUri,
25
- readHelpCatalog,
26
- searchHelpCatalog,
27
- } from "../_internal/help-catalog";
28
- import { getCapability, listApps, listHelp } from "../_internal/registry";
18
+ import { HELP_SEARCH_MAX_LIMIT, helpResourceUri, parseHelpResourceUri, readHelpArticle } from "../_internal/help-catalog";
19
+ import { getCapability, listApps } from "../_internal/registry";
20
+ import { capabilityValueMeta, recordCapabilityExecution } from "../capabilities/executions";
29
21
  import {
30
22
  CAPABILITY_FRAMEWORK_ERROR_CODES,
31
23
  CAPABILITY_MAX_REQUEST_BYTES,
@@ -39,8 +31,10 @@ import {
39
31
  cloudResourceRefAppId,
40
32
  resolveCapabilityResourceReader,
41
33
  } from "../contracts/capabilities";
42
- import type { AppRegistryEntry, CapabilityRegistryEntry, HelpRegistryEntry } from "../contracts/registry";
34
+ import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
43
35
  import { type AuthContext, auth, type RequestAuthority, rateLimit, rejectReservedWorkloadCredential, resolveLocale } from "../server";
36
+ import { createHelpReader, type HelpArticle, type HelpMetadata, type HelpReader, type HelpReaderFactory } from "../services/help";
37
+ import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
44
38
  import { logger } from "../services/logging";
45
39
  import { get } from "../services/settings";
46
40
  import { cloudMcpResourceUri, publicCloudOrigin } from "../shared/app-url";
@@ -61,7 +55,7 @@ const log = logger("mcp");
61
55
 
62
56
  type McpRouteDependencies = CapabilityDispatchDependencies & {
63
57
  listApps?: () => Promise<AppRegistryEntry[]>;
64
- listHelp?: () => Promise<HelpRegistryEntry[]>;
58
+ help?: HelpReaderFactory;
65
59
  getOperatorLocale?: () => Promise<string | undefined>;
66
60
  getAppUrl?: () => Promise<string>;
67
61
  authenticate?: MiddlewareHandler<AuthContext>;
@@ -375,13 +369,12 @@ const semanticResourceLinks = (body: Record<string, unknown>, origin: string): C
375
369
  }));
376
370
  };
377
371
 
378
- const helpResource = (document: HelpCatalogDocument): Resource => ({
372
+ const helpResource = (document: HelpMetadata): Resource => ({
379
373
  uri: helpResourceUri(document.appId, document.documentId),
380
374
  name: `${document.appName}: ${document.title}`,
381
375
  title: document.title,
382
376
  description: document.description ?? `Product Help from ${document.appName}.`,
383
377
  mimeType: "text/markdown",
384
- size: new TextEncoder().encode(document.markdown).byteLength,
385
378
  annotations: { audience: ["assistant"], priority: 0.8 },
386
379
  _meta: {
387
380
  "cloud/appId": document.appId,
@@ -391,10 +384,10 @@ const helpResource = (document: HelpCatalogDocument): Resource => ({
391
384
  },
392
385
  });
393
386
 
394
- const helpResourcePage = (catalog: readonly HelpCatalogDocument[], cursor?: string): { resources: Resource[]; nextCursor?: string } => {
387
+ const helpResourcePage = (catalog: readonly HelpMetadata[], cursor?: string): { resources: Resource[]; nextCursor?: string } => {
395
388
  const resources = catalog
396
389
  .map(helpResource)
397
- .sort((left, right) => left.uri.localeCompare(right.uri))
390
+ .sort((left, right) => (left.uri < right.uri ? -1 : left.uri > right.uri ? 1 : 0))
398
391
  .filter((resource) => !cursor || resource.uri > cursor)
399
392
  .slice(0, MCP_RESOURCE_PAGE_SIZE);
400
393
  const hasMore =
@@ -412,7 +405,7 @@ const hasOnlyKeys = (value: Record<string, unknown>, keys: readonly string[]): b
412
405
  const validationToolError = (message: string): CallToolResult =>
413
406
  boundedToolResult({ code: CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, message }, true);
414
407
 
415
- const callHelpTool = async (name: string, argsValue: unknown, catalog: readonly HelpCatalogDocument[]): Promise<CallToolResult | null> => {
408
+ const callHelpTool = async (name: string, argsValue: unknown, reader: HelpReader): Promise<CallToolResult | null> => {
416
409
  const args = helpArguments(argsValue);
417
410
  if (name === HELP_SEARCH_TOOL) {
418
411
  const query = typeof args.query === "string" ? args.query.trim() : "";
@@ -428,7 +421,7 @@ const callHelpTool = async (name: string, argsValue: unknown, catalog: readonly
428
421
  ) {
429
422
  return validationToolError("cloud__help__search requires query (1-200 characters) and an optional limit from 1 to 25");
430
423
  }
431
- const documents = searchHelpCatalog(catalog, { query, appId, limit });
424
+ const documents = await reader.search({ query, appId, limit });
432
425
  const result = boundedToolResult({ documents }, false);
433
426
  result.content.push(
434
427
  ...documents.map((document) => ({
@@ -457,7 +450,8 @@ const callHelpTool = async (name: string, argsValue: unknown, catalog: readonly
457
450
  "cloud__help__read requires exact appId and documentId values and accepts an optional query up to 200 characters",
458
451
  );
459
452
  }
460
- const document = readHelpCatalog(catalog, { appId, documentId, query });
453
+ const article = await reader.read({ appId, documentId });
454
+ const document = article ? readHelpArticle(article, query) : null;
461
455
  if (!document) return boundedToolResult({ code: "HELP_NOT_FOUND", message: "Help document is not in the current live catalog" }, true);
462
456
  const result = boundedToolResult({ document }, false);
463
457
  result.content.push({
@@ -486,8 +480,8 @@ const createMcpServer = (
486
480
  (helpLocale ??= (dependencies.getOperatorLocale ?? (() => get<string>("app.locale")))().then((operatorDefault) =>
487
481
  resolveLocale(request.headers, operatorDefault),
488
482
  ));
489
- const helpCatalog = async () =>
490
- loadHelpCatalog({ listApps: registry, listHelp: dependencies.listHelp ?? listHelp }, await resolveHelpLocale());
483
+ const helpReader = async () =>
484
+ (dependencies.help ?? ((locale) => createHelpReader(locale, { listApps: registry })))(await resolveHelpLocale());
491
485
  const hasScope = (scope: "read" | "write"): boolean =>
492
486
  oauthScopes === null || oauthScopes.includes(scope) || oauthScopes.includes("admin");
493
487
  const requireScope = (scope: "read" | "write"): void => {
@@ -507,10 +501,10 @@ const createMcpServer = (
507
501
  throw new McpError(ErrorCode.InvalidParams, "Invalid Cloud Help resource cursor");
508
502
  }
509
503
  try {
510
- return helpResourcePage(await helpCatalog(), message.params?.cursor);
504
+ return helpResourcePage(await (await helpReader()).list(message.params?.cursor), message.params?.cursor);
511
505
  } catch (error) {
512
506
  log.error("Failed to list MCP Help resources", { error: error instanceof Error ? error.message : String(error) });
513
- throw new McpError(ErrorCode.InternalError, "Help registry is currently unavailable", {
507
+ throw new McpError(ErrorCode.InternalError, "Help is currently unavailable", {
514
508
  code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
515
509
  });
516
510
  }
@@ -520,16 +514,15 @@ const createMcpServer = (
520
514
  requireScope("read");
521
515
  const identity = parseHelpResourceUri(message.params.uri);
522
516
  if (!identity) throw new McpError(ErrorCode.InvalidParams, "Unknown Cloud Help resource URI");
523
- let catalog: HelpCatalogDocument[];
517
+ let document: HelpArticle | null;
524
518
  try {
525
- catalog = await helpCatalog();
519
+ document = await (await helpReader()).read(identity);
526
520
  } catch (error) {
527
- log.error("Failed to read MCP Help catalog", { error: error instanceof Error ? error.message : String(error) });
528
- throw new McpError(ErrorCode.InternalError, "Help registry is currently unavailable", {
521
+ log.error("Failed to read Help", { error: error instanceof Error ? error.message : String(error) });
522
+ throw new McpError(ErrorCode.InternalError, "Help is currently unavailable", {
529
523
  code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
530
524
  });
531
525
  }
532
- const document = findHelpDocument(catalog, identity.appId, identity.documentId);
533
526
  if (!document) throw new McpError(ErrorCode.InvalidParams, "Help resource is not in the current live catalog");
534
527
  return {
535
528
  contents: [
@@ -567,13 +560,10 @@ const createMcpServer = (
567
560
  return boundedToolResult({ code: "FORBIDDEN", message: "OAuth scope read is required" }, true);
568
561
  }
569
562
  try {
570
- return (await callHelpTool(message.params.name, message.params.arguments, await helpCatalog()))!;
563
+ return (await callHelpTool(message.params.name, message.params.arguments, await helpReader()))!;
571
564
  } catch (error) {
572
565
  log.error("Failed to call MCP Help tool", { error: error instanceof Error ? error.message : String(error) });
573
- return boundedToolResult(
574
- { code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, message: "Help registry is currently unavailable" },
575
- true,
576
- );
566
+ return boundedToolResult({ code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, message: "Help is currently unavailable" }, true);
577
567
  }
578
568
  }
579
569
  let selected: CapabilityTool | null;
@@ -615,6 +605,26 @@ const createMcpServer = (
615
605
  }
616
606
  const requiredScope = selected.kind === "actions" ? "write" : "read";
617
607
  if (!hasScope(requiredScope)) {
608
+ // Denied before the dispatcher, so this surface records the same row the
609
+ // dispatcher would have written.
610
+ const at = new Date();
611
+ const actor = authority?.actor;
612
+ await recordCapabilityExecution({
613
+ requestId: normalizeInvocationRequestId(request.headers.get("x-request-id")) ?? crypto.randomUUID(),
614
+ origin: "mcp",
615
+ appId: selected.app.appId,
616
+ capability: `${selected.app.appId}.${selected.operation.localId}`,
617
+ kind: selected.kind === "actions" ? "action" : "query",
618
+ destructive: "destructive" in selected.operation ? selected.operation.destructive : false,
619
+ actorKind: actor?.kind ?? null,
620
+ actorId: actor?.kind === "user" ? actor.user.id : (actor?.serviceAccount.id ?? null),
621
+ userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? null),
622
+ status: "denied",
623
+ errorCode: "FORBIDDEN",
624
+ inputMeta: capabilityValueMeta(message.params.arguments ?? {}),
625
+ startedAt: at,
626
+ completedAt: at,
627
+ }).catch((error) => log.error("MCP denial could not be recorded", { error: error instanceof Error ? error.message : String(error) }));
618
628
  return boundedToolResult({ code: "FORBIDDEN", message: `OAuth scope ${requiredScope} is required` }, true);
619
629
  }
620
630
 
@@ -626,6 +636,7 @@ const createMcpServer = (
626
636
  const response = await dispatchCapability({
627
637
  request: new Request(request.url, { method: "POST", headers, signal: request.signal }),
628
638
  kind: selected.kind,
639
+ origin: "mcp",
629
640
  appId: selected.app.appId,
630
641
  capabilityId: selected.operation.localId,
631
642
  input: args,
@@ -0,0 +1,35 @@
1
+ import { Hono } from "hono";
2
+ import { bodyLimit } from "hono/body-limit";
3
+ import { describeRoute } from "hono-openapi";
4
+ import { WEB_VITALS_MAX_BYTES, type WebVitalsReport, WebVitalsReportSchema } from "../contracts/web-vitals";
5
+ import { type AuthContext, requiresAuth, v } from "../server";
6
+ import { logger } from "../services/logging";
7
+ import { get } from "../services/settings";
8
+
9
+ const log = logger("web-vitals");
10
+
11
+ /** Mounted below /me's authentication, user guard and rate limit. Client reports are diagnostics, not audit facts. */
12
+ export const createMeWebVitalsRoutes = (
13
+ record: (report: WebVitalsReport) => void = (report) => log.info(report.name, report),
14
+ isEnabled: () => Promise<boolean> = async () => (await get("observability.web_vitals.enabled")) === true,
15
+ ) =>
16
+ new Hono<AuthContext>().post(
17
+ "/web-vitals",
18
+ describeRoute({
19
+ tags: ["Me"],
20
+ summary: "Report browser page performance",
21
+ ...requiresAuth,
22
+ responses: {
23
+ 204: { description: "Report processed; discarded when collection is disabled" },
24
+ 400: { description: "Invalid report" },
25
+ 413: { description: "Report too large" },
26
+ },
27
+ }),
28
+ bodyLimit({ maxSize: WEB_VITALS_MAX_BYTES }),
29
+ v("json", WebVitalsReportSchema),
30
+ async (c) => {
31
+ if (!c.get("user")) return c.json({ message: "Authentication required" }, 401);
32
+ if (await isEnabled()) record(c.req.valid("json"));
33
+ return c.body(null, 204);
34
+ },
35
+ );
package/src/api/me.ts CHANGED
@@ -50,6 +50,7 @@ import {
50
50
  import { MandatePolicyV1Schema } from "../services/mandates";
51
51
 
52
52
  import { createMeRailRoutes } from "./me-rail";
53
+ import { createMeWebVitalsRoutes } from "./me-web-vitals";
53
54
 
54
55
  const toAccountsActor = (user: AuthContext["Variables"]["user"]) => ({
55
56
  userId: user.id,
@@ -187,6 +188,7 @@ const app = new Hono<AuthContext>()
187
188
  .use(auth.requireUser())
188
189
  .route("/", mandateRoutes)
189
190
  .route("/", createMeRailRoutes())
191
+ .route("/", createMeWebVitalsRoutes())
190
192
 
191
193
  .get(
192
194
  "/activity",