@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
@@ -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
@@ -1,3 +1,4 @@
1
+ import adminAiQuotaRoutes from "./admin-ai-quotas";
1
2
  /**
2
3
  * Core platform API surface — owned by cloud-lib, mounted by the core-app.
3
4
  *
@@ -17,12 +18,14 @@ import { Hono } from "hono";
17
18
  import { prettyJSON } from "hono/pretty-json";
18
19
  import { createAiApprovalPreferenceRoutes } from "../ai/approval-routes";
19
20
  import { aiProjectsRoutes } from "../ai/projects-routes";
21
+ import { createCodeCapabilityRoutes } from "../ai/code-capability-routes";
20
22
  import { aiRoutes } from "../ai/routes";
21
23
  import { aiSkillsRoutes } from "../ai/skills-routes";
22
24
  import accountsEntitiesRoutes from "./accounts-entities";
23
25
  import adminAiUsageRoutes from "./admin-ai-usage";
24
26
  import adminAiProjectsRoutes from "./admin-ai-projects";
25
27
  import adminAiSkillsRoutes from "./admin-ai-skills";
28
+ import adminRailRoutes from "./admin-rail";
26
29
  import adminCoreSettingsRoutes from "./admin-core-settings";
27
30
  import adminIdentityRoutes from "./admin-identity";
28
31
  import adminLifecycleRoutes from "./admin-lifecycle";
@@ -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
@@ -1,3 +1,4 @@
1
+ import { createHelpReader, type HelpReader, type HelpReaderFactory, type HelpMetadata, type HelpArticle } from "../services/help";
1
2
  import { createHash } from "node:crypto";
2
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
4
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
@@ -16,16 +17,13 @@ import { Hono, type MiddlewareHandler } from "hono";
16
17
  import { z } from "zod";
17
18
  import { readBoundedJson } from "../_internal/bounded-json";
18
19
  import {
19
- findHelpDocument,
20
20
  HELP_SEARCH_MAX_LIMIT,
21
- type HelpCatalogDocument,
22
21
  helpResourceUri,
23
- loadHelpCatalog,
24
22
  parseHelpResourceUri,
25
- readHelpCatalog,
26
- searchHelpCatalog,
23
+ readHelpArticle,
27
24
  } from "../_internal/help-catalog";
28
- import { getCapability, listApps, listHelp } from "../_internal/registry";
25
+ import { getCapability, listApps } from "../_internal/registry";
26
+ import { capabilityValueMeta, recordCapabilityExecution } from "../capabilities/executions";
29
27
  import {
30
28
  CAPABILITY_FRAMEWORK_ERROR_CODES,
31
29
  CAPABILITY_MAX_REQUEST_BYTES,
@@ -39,8 +37,9 @@ import {
39
37
  cloudResourceRefAppId,
40
38
  resolveCapabilityResourceReader,
41
39
  } from "../contracts/capabilities";
42
- import type { AppRegistryEntry, CapabilityRegistryEntry, HelpRegistryEntry } from "../contracts/registry";
40
+ import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
43
41
  import { type AuthContext, auth, type RequestAuthority, rateLimit, rejectReservedWorkloadCredential, resolveLocale } from "../server";
42
+ import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
44
43
  import { logger } from "../services/logging";
45
44
  import { get } from "../services/settings";
46
45
  import { cloudMcpResourceUri, publicCloudOrigin } from "../shared/app-url";
@@ -61,7 +60,7 @@ const log = logger("mcp");
61
60
 
62
61
  type McpRouteDependencies = CapabilityDispatchDependencies & {
63
62
  listApps?: () => Promise<AppRegistryEntry[]>;
64
- listHelp?: () => Promise<HelpRegistryEntry[]>;
63
+ help?: HelpReaderFactory;
65
64
  getOperatorLocale?: () => Promise<string | undefined>;
66
65
  getAppUrl?: () => Promise<string>;
67
66
  authenticate?: MiddlewareHandler<AuthContext>;
@@ -375,13 +374,12 @@ const semanticResourceLinks = (body: Record<string, unknown>, origin: string): C
375
374
  }));
376
375
  };
377
376
 
378
- const helpResource = (document: HelpCatalogDocument): Resource => ({
377
+ const helpResource = (document: HelpMetadata): Resource => ({
379
378
  uri: helpResourceUri(document.appId, document.documentId),
380
379
  name: `${document.appName}: ${document.title}`,
381
380
  title: document.title,
382
381
  description: document.description ?? `Product Help from ${document.appName}.`,
383
382
  mimeType: "text/markdown",
384
- size: new TextEncoder().encode(document.markdown).byteLength,
385
383
  annotations: { audience: ["assistant"], priority: 0.8 },
386
384
  _meta: {
387
385
  "cloud/appId": document.appId,
@@ -391,10 +389,10 @@ const helpResource = (document: HelpCatalogDocument): Resource => ({
391
389
  },
392
390
  });
393
391
 
394
- const helpResourcePage = (catalog: readonly HelpCatalogDocument[], cursor?: string): { resources: Resource[]; nextCursor?: string } => {
392
+ const helpResourcePage = (catalog: readonly HelpMetadata[], cursor?: string): { resources: Resource[]; nextCursor?: string } => {
395
393
  const resources = catalog
396
394
  .map(helpResource)
397
- .sort((left, right) => left.uri.localeCompare(right.uri))
395
+ .sort((left, right) => (left.uri < right.uri ? -1 : left.uri > right.uri ? 1 : 0))
398
396
  .filter((resource) => !cursor || resource.uri > cursor)
399
397
  .slice(0, MCP_RESOURCE_PAGE_SIZE);
400
398
  const hasMore =
@@ -412,7 +410,7 @@ const hasOnlyKeys = (value: Record<string, unknown>, keys: readonly string[]): b
412
410
  const validationToolError = (message: string): CallToolResult =>
413
411
  boundedToolResult({ code: CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, message }, true);
414
412
 
415
- const callHelpTool = async (name: string, argsValue: unknown, catalog: readonly HelpCatalogDocument[]): Promise<CallToolResult | null> => {
413
+ const callHelpTool = async (name: string, argsValue: unknown, reader: HelpReader): Promise<CallToolResult | null> => {
416
414
  const args = helpArguments(argsValue);
417
415
  if (name === HELP_SEARCH_TOOL) {
418
416
  const query = typeof args.query === "string" ? args.query.trim() : "";
@@ -428,7 +426,7 @@ const callHelpTool = async (name: string, argsValue: unknown, catalog: readonly
428
426
  ) {
429
427
  return validationToolError("cloud__help__search requires query (1-200 characters) and an optional limit from 1 to 25");
430
428
  }
431
- const documents = searchHelpCatalog(catalog, { query, appId, limit });
429
+ const documents = await reader.search({ query, appId, limit });
432
430
  const result = boundedToolResult({ documents }, false);
433
431
  result.content.push(
434
432
  ...documents.map((document) => ({
@@ -457,7 +455,8 @@ const callHelpTool = async (name: string, argsValue: unknown, catalog: readonly
457
455
  "cloud__help__read requires exact appId and documentId values and accepts an optional query up to 200 characters",
458
456
  );
459
457
  }
460
- const document = readHelpCatalog(catalog, { appId, documentId, query });
458
+ const article = await reader.read({ appId, documentId });
459
+ const document = article ? readHelpArticle(article, query) : null;
461
460
  if (!document) return boundedToolResult({ code: "HELP_NOT_FOUND", message: "Help document is not in the current live catalog" }, true);
462
461
  const result = boundedToolResult({ document }, false);
463
462
  result.content.push({
@@ -486,8 +485,7 @@ const createMcpServer = (
486
485
  (helpLocale ??= (dependencies.getOperatorLocale ?? (() => get<string>("app.locale")))().then((operatorDefault) =>
487
486
  resolveLocale(request.headers, operatorDefault),
488
487
  ));
489
- const helpCatalog = async () =>
490
- loadHelpCatalog({ listApps: registry, listHelp: dependencies.listHelp ?? listHelp }, await resolveHelpLocale());
488
+ const helpReader = async () => (dependencies.help ?? ((locale) => createHelpReader(locale, { listApps: registry })))(await resolveHelpLocale());
491
489
  const hasScope = (scope: "read" | "write"): boolean =>
492
490
  oauthScopes === null || oauthScopes.includes(scope) || oauthScopes.includes("admin");
493
491
  const requireScope = (scope: "read" | "write"): void => {
@@ -507,10 +505,10 @@ const createMcpServer = (
507
505
  throw new McpError(ErrorCode.InvalidParams, "Invalid Cloud Help resource cursor");
508
506
  }
509
507
  try {
510
- return helpResourcePage(await helpCatalog(), message.params?.cursor);
508
+ return helpResourcePage(await (await helpReader()).list(message.params?.cursor), message.params?.cursor);
511
509
  } catch (error) {
512
510
  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", {
511
+ throw new McpError(ErrorCode.InternalError, "Help is currently unavailable", {
514
512
  code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
515
513
  });
516
514
  }
@@ -520,16 +518,15 @@ const createMcpServer = (
520
518
  requireScope("read");
521
519
  const identity = parseHelpResourceUri(message.params.uri);
522
520
  if (!identity) throw new McpError(ErrorCode.InvalidParams, "Unknown Cloud Help resource URI");
523
- let catalog: HelpCatalogDocument[];
521
+ let document: HelpArticle | null;
524
522
  try {
525
- catalog = await helpCatalog();
523
+ document = await (await helpReader()).read(identity);
526
524
  } 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", {
525
+ log.error("Failed to read Help", { error: error instanceof Error ? error.message : String(error) });
526
+ throw new McpError(ErrorCode.InternalError, "Help is currently unavailable", {
529
527
  code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
530
528
  });
531
529
  }
532
- const document = findHelpDocument(catalog, identity.appId, identity.documentId);
533
530
  if (!document) throw new McpError(ErrorCode.InvalidParams, "Help resource is not in the current live catalog");
534
531
  return {
535
532
  contents: [
@@ -567,11 +564,11 @@ const createMcpServer = (
567
564
  return boundedToolResult({ code: "FORBIDDEN", message: "OAuth scope read is required" }, true);
568
565
  }
569
566
  try {
570
- return (await callHelpTool(message.params.name, message.params.arguments, await helpCatalog()))!;
567
+ return (await callHelpTool(message.params.name, message.params.arguments, await helpReader()))!;
571
568
  } catch (error) {
572
569
  log.error("Failed to call MCP Help tool", { error: error instanceof Error ? error.message : String(error) });
573
570
  return boundedToolResult(
574
- { code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, message: "Help registry is currently unavailable" },
571
+ { code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, message: "Help is currently unavailable" },
575
572
  true,
576
573
  );
577
574
  }
@@ -615,6 +612,26 @@ const createMcpServer = (
615
612
  }
616
613
  const requiredScope = selected.kind === "actions" ? "write" : "read";
617
614
  if (!hasScope(requiredScope)) {
615
+ // Denied before the dispatcher, so this surface records the same row the
616
+ // dispatcher would have written.
617
+ const at = new Date();
618
+ const actor = authority?.actor;
619
+ await recordCapabilityExecution({
620
+ requestId: normalizeInvocationRequestId(request.headers.get("x-request-id")) ?? crypto.randomUUID(),
621
+ origin: "mcp",
622
+ appId: selected.app.appId,
623
+ capability: `${selected.app.appId}.${selected.operation.localId}`,
624
+ kind: selected.kind === "actions" ? "action" : "query",
625
+ destructive: "destructive" in selected.operation ? selected.operation.destructive : false,
626
+ actorKind: actor?.kind ?? null,
627
+ actorId: actor?.kind === "user" ? actor.user.id : (actor?.serviceAccount.id ?? null),
628
+ userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? null),
629
+ status: "denied",
630
+ errorCode: "FORBIDDEN",
631
+ inputMeta: capabilityValueMeta(message.params.arguments ?? {}),
632
+ startedAt: at,
633
+ completedAt: at,
634
+ }).catch((error) => log.error("MCP denial could not be recorded", { error: error instanceof Error ? error.message : String(error) }));
618
635
  return boundedToolResult({ code: "FORBIDDEN", message: `OAuth scope ${requiredScope} is required` }, true);
619
636
  }
620
637
 
@@ -626,6 +643,7 @@ const createMcpServer = (
626
643
  const response = await dispatchCapability({
627
644
  request: new Request(request.url, { method: "POST", headers, signal: request.signal }),
628
645
  kind: selected.kind,
646
+ origin: "mcp",
629
647
  appId: selected.app.appId,
630
648
  capabilityId: selected.operation.localId,
631
649
  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",