@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
@@ -1,20 +1,37 @@
1
+ import { sealCapabilityStream, dispatchCapabilityStream } from "./capability-streams";
2
+ import { CapabilityStreamSchema } from "../contracts/capability-streams";
1
3
  import { Hono, type MiddlewareHandler } from "hono";
2
4
  import { describeRoute } from "hono-openapi";
3
5
  import { z } from "zod";
4
6
  import { readBoundedJson } from "../_internal/bounded-json";
5
7
  import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
6
8
  import { getCapability, listApps } from "../_internal/registry";
9
+ import {
10
+ type CapabilityClaimScope,
11
+ type CapabilityExecutionStatus,
12
+ capabilityIdempotencyKeyHash,
13
+ capabilityRequestHash,
14
+ capabilityValueMeta,
15
+ claimCapabilityIdempotency,
16
+ completeCapabilityClaim,
17
+ markCapabilityClaimUncertain,
18
+ recordCapabilityExecution,
19
+ releaseCapabilityClaim,
20
+ resolveCapabilityClaim,
21
+ } from "../capabilities/executions";
7
22
  import {
8
23
  CAPABILITY_FRAMEWORK_ERROR_CODES,
9
24
  CAPABILITY_MAX_CATALOG_BYTES,
10
25
  CAPABILITY_MAX_REQUEST_BYTES,
11
26
  CAPABILITY_MAX_RESULT_BYTES,
27
+ CAPABILITY_ORIGIN_HEADER,
12
28
  CAPABILITY_PROTOCOL_VERSION,
13
29
  CapabilityActionReviewSchema,
14
30
  type CapabilityCatalog,
15
31
  CapabilityCatalogSchema,
16
32
  CapabilityErrorSchema,
17
33
  CapabilityIdempotencyKeySchema,
34
+ type CapabilityOrigin,
18
35
  capabilityResultJsonSchema,
19
36
  } from "../contracts/capabilities";
20
37
  import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
@@ -30,7 +47,9 @@ import {
30
47
  requiresAuth,
31
48
  v,
32
49
  } from "../server";
33
- import { logger } from "../services";
50
+ import { logger, settings } from "../services";
51
+ import { resolveCommandLink } from "../capabilities/command-link";
52
+ import { CommandRequestSchema, CommandLinkSchema } from "../contracts/commands";
34
53
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
35
54
  import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
36
55
  import type { InvocationAuthority } from "../services/identity/invocation-token";
@@ -83,6 +102,7 @@ export type CapabilityRouteDependencies = {
83
102
  withMandateIssueAuthority?: typeof withMandateIssueAuthority;
84
103
  queryTimeoutMs?: number;
85
104
  actionTimeoutMs?: number;
105
+ appUrl?: () => Promise<string>;
86
106
  };
87
107
 
88
108
  export type CapabilityDispatchDependencies = Pick<
@@ -203,12 +223,9 @@ const waitWithin = <T>(value: Promise<T>, signal: AbortSignal): Promise<T> =>
203
223
  );
204
224
  });
205
225
 
206
- /**
207
- * Dispatch one already-parsed capability invocation through the live registry.
208
- * HTTP, CLI, and MCP callers share this exact app lookup, credential forwarding,
209
- * schema pinning, timeout, and response validation path.
210
- */
211
- export const dispatchCapability = async (params: {
226
+ export type CapabilityDispatchParams = {
227
+ /** Trusted Core continuation binding; never read from client headers. */
228
+ continuation?: string;
212
229
  request: Request;
213
230
  kind: "queries" | "actions";
214
231
  review?: boolean;
@@ -216,11 +233,37 @@ export const dispatchCapability = async (params: {
216
233
  capabilityId: string;
217
234
  input: unknown;
218
235
  locale?: string;
236
+ /** Cloud surface this invocation came from; recorded on the execution row. */
237
+ origin: CapabilityOrigin;
219
238
  authority?: RequestAuthority;
220
239
  mandate?: MandatedCapabilityAuthority;
221
240
  callingAppId?: string;
222
241
  dependencies?: CapabilityDispatchDependencies;
223
- }): Promise<Response> => {
242
+ };
243
+
244
+ type CapabilityDispatchOutcome = { body: unknown; status: number; replayed?: boolean };
245
+
246
+ /** Principal resolved while dispatching. A mandate only reveals it during issuance. */
247
+ type CapabilityDispatchObservation = {
248
+ subject: { type: "user" | "service_account"; id: string } | null;
249
+ destructive: boolean;
250
+ };
251
+
252
+ type CapabilityPrincipal = { type: "user" | "service_account"; id: string } | null;
253
+
254
+ /** Acting principal for the execution row and the idempotency claim scope. A mandate reveals it during issuance. */
255
+ const dispatchPrincipal = (params: CapabilityDispatchParams, observed: CapabilityDispatchObservation): CapabilityPrincipal => {
256
+ const actor = params.authority?.actor;
257
+ if (actor?.kind === "user") return { type: "user", id: actor.user.id };
258
+ if (actor?.kind === "service_account") return { type: "service_account", id: actor.serviceAccount.id };
259
+ return observed.subject;
260
+ };
261
+
262
+ const runCapabilityDispatch = async (
263
+ params: CapabilityDispatchParams,
264
+ requestId: string,
265
+ observed: CapabilityDispatchObservation,
266
+ ): Promise<CapabilityDispatchOutcome> => {
224
267
  const messages = capabilityMessages(params.locale ?? preferredLocale(params.request.headers));
225
268
  const registryEntry = params.dependencies?.getCapability ?? getCapability;
226
269
  const fetchUpstream = params.dependencies?.fetch ?? globalThis.fetch;
@@ -233,15 +276,16 @@ export const dispatchCapability = async (params: {
233
276
  error: error instanceof Error ? error.message : String(error),
234
277
  });
235
278
  const unavailable = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, messages.registryUnavailable, 503);
236
- return capabilityJsonResponse(unavailable.body, unavailable.status);
279
+ return unavailable;
237
280
  }
238
281
  if (!entry) {
239
282
  const error = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, messages.appUnavailable({ appId: params.appId }), 503);
240
- return capabilityJsonResponse(error.body, error.status);
283
+ return error;
241
284
  }
242
285
 
243
286
  const operations = params.kind === "queries" ? entry.manifest.queries : entry.manifest.actions;
244
287
  const operation = operations.find((candidate) => candidate.localId === params.capabilityId);
288
+ if (operation && "destructive" in operation) observed.destructive = operation.destructive;
245
289
  if (!operation) {
246
290
  const label = params.kind === "queries" ? messages.query : messages.action;
247
291
  const error = errorResponse(
@@ -249,7 +293,7 @@ export const dispatchCapability = async (params: {
249
293
  messages.notFound({ kind: label, reference: `${params.appId}.${params.capabilityId}` }),
250
294
  404,
251
295
  );
252
- return capabilityJsonResponse(error.body, error.status);
296
+ return error;
253
297
  }
254
298
  if (params.review && (params.kind !== "actions" || !("review" in operation) || operation.review !== true)) {
255
299
  const error = errorResponse(
@@ -257,7 +301,7 @@ export const dispatchCapability = async (params: {
257
301
  messages.notFound({ kind: messages.review, reference: `${params.appId}.${params.capabilityId}` }),
258
302
  404,
259
303
  );
260
- return capabilityJsonResponse(error.body, error.status);
304
+ return error;
261
305
  }
262
306
 
263
307
  const rawIdempotencyKey = params.request.headers.get("idempotency-key");
@@ -265,47 +309,49 @@ export const dispatchCapability = async (params: {
265
309
  const action = operation as (typeof entry.manifest.actions)[number];
266
310
  if (action.idempotency === "required" && rawIdempotencyKey === null) {
267
311
  const error = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyKeyRequired, messages.idempotencyRequired, 400);
268
- return capabilityJsonResponse(error.body, error.status);
312
+ return error;
269
313
  }
270
314
  if (action.idempotency === "none" && rawIdempotencyKey !== null) {
271
315
  const error = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyKeyNotAllowed, messages.idempotencyNotAllowed, 400);
272
- return capabilityJsonResponse(error.body, error.status);
316
+ return error;
273
317
  }
274
318
  } else if (rawIdempotencyKey !== null) {
275
319
  const error = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyKeyNotAllowed, messages.idempotencyOnlyActions, 400);
276
- return capabilityJsonResponse(error.body, error.status);
320
+ return error;
277
321
  }
278
322
  const idempotencyKey = rawIdempotencyKey === null ? null : CapabilityIdempotencyKeySchema.safeParse(rawIdempotencyKey);
279
323
  if (idempotencyKey && !idempotencyKey.success) {
280
324
  const error = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, messages.idempotencyInvalid, 400, {
281
325
  issues: idempotencyKey.error.issues.map((issue) => ({ message: issue.message })),
282
326
  });
283
- return capabilityJsonResponse(error.body, error.status);
327
+ return error;
284
328
  }
285
329
 
286
330
  const inputValidator = schemaValidator(`${operation.schemaHash}:input`, operation.inputSchema);
287
331
  if (!inputValidator) {
288
332
  const invalid = errorResponse("INVALID_APP_RESPONSE", messages.unsupportedSchema({ appId: params.appId }), 502);
289
- return capabilityJsonResponse(invalid.body, invalid.status);
333
+ return invalid;
290
334
  }
291
335
  const input = inputValidator.safeParse(params.input);
292
336
  if (!input.success) {
293
337
  const invalid = errorResponse("VALIDATION_FAILED", messages.inputSchemaMismatch, 400, {
294
338
  issues: input.error.issues.slice(0, 20).map((issue) => ({ path: issue.path.join("."), message: issue.message })),
295
339
  });
296
- return capabilityJsonResponse(invalid.body, invalid.status);
340
+ return invalid;
297
341
  }
298
342
 
343
+ if (params.mandate && operation.stream) return errorResponse("STREAM_UNSUPPORTED_AUTHORITY", "Streams require an interactive user or service-account request", 403);
344
+
299
345
  let requestBody: string;
300
346
  try {
301
347
  requestBody = JSON.stringify({ input: input.data });
302
348
  } catch {
303
349
  const invalid = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, messages.inputNotSerializable, 400);
304
- return capabilityJsonResponse(invalid.body, invalid.status);
350
+ return invalid;
305
351
  }
306
352
  if (new TextEncoder().encode(requestBody).byteLength > CAPABILITY_MAX_REQUEST_BYTES) {
307
353
  const invalid = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, messages.requestTooLarge, 400);
308
- return capabilityJsonResponse(invalid.body, invalid.status);
354
+ return invalid;
309
355
  }
310
356
 
311
357
  const invocationOperation = capabilityInvocationOperation(params.kind, params.capabilityId, params.review);
@@ -332,17 +378,20 @@ export const dispatchCapability = async (params: {
332
378
  targetAppId: params.appId,
333
379
  operation: invocationOperation,
334
380
  actionApproval: mandate.actionApproval ?? "none",
335
- requestId: normalizeInvocationRequestId(params.request.headers.get("x-request-id")),
381
+ input: params.input,
382
+ capabilityApproval: "approval" in operation ? (operation.approval ?? "always") : "always",
383
+ requestId,
336
384
  },
337
385
  async (mandateAuthority) => {
338
386
  signal.throwIfAborted();
387
+ observed.subject = mandateAuthority.subject;
339
388
  return (params.dependencies?.signInvocation ?? signInvocationToken)({
340
389
  targetAppId: params.appId,
341
390
  callingAppId: mandate.ownerAppId,
342
391
  operation: invocationOperation,
343
392
  schemaHash: operation.schemaHash,
344
393
  authority: invocationAuthorityFromMandate(mandateAuthority),
345
- requestId: normalizeInvocationRequestId(params.request.headers.get("x-request-id")),
394
+ requestId,
346
395
  signer,
347
396
  issuer: signer.issuer,
348
397
  });
@@ -376,7 +425,7 @@ export const dispatchCapability = async (params: {
376
425
  operation: invocationOperation,
377
426
  schemaHash: operation.schemaHash,
378
427
  authority: invocationAuthorityFromRequest(authority),
379
- requestId: normalizeInvocationRequestId(params.request.headers.get("x-request-id")),
428
+ requestId,
380
429
  signer,
381
430
  issuer: signer.issuer,
382
431
  }),
@@ -397,7 +446,7 @@ export const dispatchCapability = async (params: {
397
446
  error.status === 409 ? "Mandate revision changed" : "Mandate does not authorize this capability invocation",
398
447
  error.status,
399
448
  );
400
- return capabilityJsonResponse(denied.body, denied.status);
449
+ return denied;
401
450
  }
402
451
  if (params.request.signal.aborted || timeout.aborted) {
403
452
  const cancelled = params.request.signal.aborted;
@@ -407,15 +456,16 @@ export const dispatchCapability = async (params: {
407
456
  cancelled ? 499 : 504,
408
457
  { retrySafe: true },
409
458
  );
410
- return capabilityJsonResponse(failure.body, failure.status);
459
+ return failure;
411
460
  }
412
461
  console.error("[capabilities] Invocation issuance failed", { appId: params.appId, operation: invocationOperation });
413
462
  const failure = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.internal, messages.issuanceFailed, 500, { retrySafe: true });
414
- return capabilityJsonResponse(failure.body, failure.status);
463
+ return failure;
415
464
  }
416
- for (const name of ["x-request-id", "traceparent", "tracestate"] as const) {
417
- const value =
418
- name === "x-request-id" ? normalizeInvocationRequestId(params.request.headers.get(name)) : params.request.headers.get(name);
465
+ headers.set("x-request-id", requestId);
466
+ headers.set(CAPABILITY_ORIGIN_HEADER, params.origin);
467
+ for (const name of ["traceparent", "tracestate"] as const) {
468
+ const value = params.request.headers.get(name);
419
469
  if (value) headers.set(name, value);
420
470
  }
421
471
  const invocationLocale = preferredLocale(params.request.headers);
@@ -424,11 +474,74 @@ export const dispatchCapability = async (params: {
424
474
  headers.set("x-cloud-capability-schema-hash", operation.schemaHash);
425
475
  const actionWithoutRetrySafety =
426
476
  params.kind === "actions" && !params.review && "idempotency" in operation && operation.idempotency === "none";
427
- const outcomeUnknown = (): Response => {
477
+ const outcomeUnknown = (): CapabilityDispatchOutcome => {
428
478
  const unknown = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.actionOutcomeUnknown, messages.actionOutcomeUnknown, 502, {
429
479
  retrySafe: false,
430
480
  });
431
- return capabilityJsonResponse(unknown.body, unknown.status);
481
+ return unknown;
482
+ };
483
+
484
+ const claimPrincipal = dispatchPrincipal(params, observed);
485
+ const claimScope: CapabilityClaimScope | null =
486
+ params.kind === "actions" &&
487
+ !params.review &&
488
+ "idempotency" in operation &&
489
+ operation.idempotency === "required" &&
490
+ idempotencyKey?.success
491
+ ? {
492
+ appId: params.appId,
493
+ capability: `${params.appId}.${params.capabilityId}`,
494
+ principal: claimPrincipal ? `${claimPrincipal.type}:${claimPrincipal.id}` : "anonymous",
495
+ keyHash: capabilityIdempotencyKeyHash(idempotencyKey.data),
496
+ }
497
+ : null;
498
+ if (claimScope) {
499
+ let claim: Awaited<ReturnType<typeof claimCapabilityIdempotency>>;
500
+ try {
501
+ claim = await claimCapabilityIdempotency(claimScope, capabilityRequestHash(input.data));
502
+ } catch (error) {
503
+ // Nothing has been forwarded yet, so refusing the call is the safe answer.
504
+ log.error("Capability idempotency claim failed", {
505
+ appId: params.appId,
506
+ capabilityId: params.capabilityId,
507
+ error: error instanceof Error ? error.message : String(error),
508
+ });
509
+ return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.internal, messages.cloudUnavailable, 503, { retrySafe: true });
510
+ }
511
+ if (claim.state === "replay") return { body: claim.body, status: claim.status, replayed: true };
512
+ if (claim.state === "conflict")
513
+ return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyConflict, messages.idempotencyConflict, 409);
514
+ if (claim.state === "in_flight") {
515
+ return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyInProgress, messages.idempotencyInProgress, 409, {
516
+ retrySafe: false,
517
+ });
518
+ }
519
+ if (claim.state === "uncertain") {
520
+ return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyUncertain, messages.idempotencyUncertain, 409, { retrySafe: false });
521
+ }
522
+ if (claim.state === "not_retained") {
523
+ return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyUncertain, messages.idempotencyResultNotRetained, 409, {
524
+ retrySafe: false,
525
+ });
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Resolve the claim once the outcome is known: a definitive success is
531
+ * replayable, a failure that proves nothing happened frees the key for a
532
+ * corrected retry, and anything ambiguous freezes the key.
533
+ */
534
+ const settle = async (outcome: CapabilityDispatchOutcome, disposition: "succeeded" | "nothing_happened" | "uncertain") => {
535
+ if (!claimScope) return outcome;
536
+ await resolveCapabilityClaim(
537
+ disposition === "succeeded"
538
+ ? completeCapabilityClaim(claimScope, outcome.status, outcome.body)
539
+ : disposition === "nothing_happened"
540
+ ? releaseCapabilityClaim(claimScope)
541
+ : markCapabilityClaimUncertain(claimScope),
542
+ claimScope,
543
+ );
544
+ return outcome;
432
545
  };
433
546
 
434
547
  let response: Response;
@@ -449,18 +562,18 @@ export const dispatchCapability = async (params: {
449
562
  kind: params.kind,
450
563
  error: error instanceof Error ? error.message : String(error),
451
564
  });
452
- if (actionWithoutRetrySafety) return outcomeUnknown();
565
+ if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
453
566
  if (params.request.signal.aborted) {
454
567
  const cancelled = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.requestCancelled, messages.requestCancelled, 499, {
455
568
  retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required"),
456
569
  });
457
- return capabilityJsonResponse(cancelled.body, cancelled.status);
570
+ return settle(cancelled, "uncertain");
458
571
  }
459
572
  if (timeout.aborted) {
460
573
  const deadline = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.deadlineExceeded, messages.deadlineExceeded, 504, {
461
574
  retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required"),
462
575
  });
463
- return capabilityJsonResponse(deadline.body, deadline.status);
576
+ return settle(deadline, "uncertain");
464
577
  }
465
578
  const unavailable = errorResponse(
466
579
  CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
@@ -468,28 +581,30 @@ export const dispatchCapability = async (params: {
468
581
  503,
469
582
  { retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required") },
470
583
  );
471
- return capabilityJsonResponse(unavailable.body, unavailable.status);
584
+ return settle(unavailable, "uncertain");
472
585
  }
473
586
 
474
587
  const upstreamBody = await readBoundedJson(response, CAPABILITY_MAX_RESULT_BYTES);
475
588
  if (!upstreamBody.ok) {
476
- if (actionWithoutRetrySafety) return outcomeUnknown();
589
+ if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
477
590
  const code =
478
591
  upstreamBody.reason === "too_large"
479
592
  ? CAPABILITY_FRAMEWORK_ERROR_CODES.responseTooLarge
480
593
  : CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse;
481
594
  const invalid = errorResponse(code, messages.invalidCapabilityJson({ appId: params.appId }), 502);
482
- return capabilityJsonResponse(invalid.body, invalid.status);
595
+ return settle(invalid, "uncertain");
483
596
  }
484
597
 
485
598
  if (!response.ok) {
486
599
  const parsed = CapabilityErrorSchema.safeParse(upstreamBody.data);
487
600
  if (parsed.success && response.status >= 400 && response.status <= 599) {
488
- return capabilityJsonResponse(parsed.data, response.status);
601
+ // A 4xx from the owning app is a decision about the request itself and
602
+ // proves the effect did not happen; a 5xx proves nothing.
603
+ return settle({ body: parsed.data, status: response.status }, response.status < 500 ? "nothing_happened" : "uncertain");
489
604
  }
490
- if (actionWithoutRetrySafety) return outcomeUnknown();
605
+ if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
491
606
  const invalid = errorResponse("INVALID_APP_RESPONSE", messages.invalidCapabilityError({ appId: params.appId }), 502);
492
- return capabilityJsonResponse(invalid.body, invalid.status);
607
+ return settle(invalid, "uncertain");
493
608
  }
494
609
 
495
610
  const resultValidator = params.review
@@ -503,12 +618,93 @@ export const dispatchCapability = async (params: {
503
618
  !parsedReview?.success ||
504
619
  (operation.approval === "rememberable" ? parsedReview.data.approvalScope !== undefined : parsedReview.data.approvalScope === undefined);
505
620
  if (!resultValidator || !parsedResult?.success || !reviewApprovalScopeIsValid) {
506
- if (actionWithoutRetrySafety) return outcomeUnknown();
621
+ if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
507
622
  const invalid = errorResponse("INVALID_APP_RESPONSE", messages.outsideResultSchema({ appId: params.appId }), 502);
508
- return capabilityJsonResponse(invalid.body, invalid.status);
623
+ return settle(invalid, "uncertain");
624
+ }
625
+
626
+ const body = upstreamBody.data as Record<string, unknown>;
627
+ if (!params.review && body.stream !== undefined) {
628
+ const stream = CapabilityStreamSchema.safeParse(body.stream);
629
+ if (!stream.success || !operation.stream || stream.data.direction !== operation.stream.direction || stream.data.size > operation.stream.maxBytes || Date.parse(stream.data.expiresAt) <= Date.now() || Date.parse(stream.data.expiresAt) > Date.now() + 86_400_000 || !params.authority)
630
+ return settle(errorResponse("INVALID_APP_RESPONSE", "Invalid stream offer", 502), "uncertain");
631
+ try {
632
+ body.stream = await sealCapabilityStream(stream.data, { appId: params.appId, kind: params.kind, capabilityId: params.capabilityId, schemaHash: operation.schemaHash, authority: params.authority, requestId, origin: params.origin, continuation: params.continuation });
633
+ if (Buffer.byteLength(JSON.stringify(body)) > CAPABILITY_MAX_RESULT_BYTES) throw new Error("Stream result too large");
634
+ } catch { return settle(errorResponse("INVALID_APP_RESPONSE", "Invalid stream offer", 502), "uncertain"); }
509
635
  }
636
+ return settle({ body, status: 200 }, "succeeded");
637
+ };
638
+
639
+ const executionStatus = (status: number, code: string | null): CapabilityExecutionStatus => {
640
+ if (status < 400) return "succeeded";
641
+ if (status === 400) return "invalid_input";
642
+ if (status === 401 || status === 403) return "denied";
643
+ if (status === 504 || code === CAPABILITY_FRAMEWORK_ERROR_CODES.deadlineExceeded) return "timed_out";
644
+ return "failed";
645
+ };
646
+
647
+ const bodyErrorCode = (body: unknown): string | null =>
648
+ body && typeof body === "object" && "code" in body && typeof body.code === "string" ? body.code : null;
510
649
 
511
- return capabilityJsonResponse(upstreamBody.data, 200);
650
+ const resultDataMeta = (body: unknown) =>
651
+ capabilityValueMeta(body && typeof body === "object" && "data" in body ? (body as { data: unknown }).data : body);
652
+
653
+ /**
654
+ * Dispatch one already-parsed capability invocation through the live registry.
655
+ * HTTP, MCP, and assistant callers share this exact app lookup, credential
656
+ * forwarding, schema pinning, timeout, and response validation path.
657
+ *
658
+ * One invocation
659
+ * produces exactly one execution row, whatever its outcome. Binary stream
660
+ * continuations record their own row under the same request ID. Action reviews are a
661
+ * read-only preview and are not executions, so they are not recorded.
662
+ */
663
+ export const dispatchCapability = async (params: CapabilityDispatchParams): Promise<Response> => {
664
+ const requestId = normalizeInvocationRequestId(params.request.headers.get("x-request-id")) ?? crypto.randomUUID();
665
+ const observed: CapabilityDispatchObservation = { subject: null, destructive: false };
666
+ const startedAt = new Date();
667
+ const outcome = await runCapabilityDispatch(params, requestId, observed);
668
+ if (!params.review) {
669
+ const actor = params.authority?.actor;
670
+ const principal = dispatchPrincipal(params, observed);
671
+ const accessSubject = params.authority?.accessSubject;
672
+ const subject =
673
+ accessSubject?.type === "user"
674
+ ? ({ type: "user", id: accessSubject.userId } as const)
675
+ : accessSubject?.type === "service_account"
676
+ ? ({ type: "service_account", id: accessSubject.serviceAccountId } as const)
677
+ : null;
678
+ const errorCode = outcome.status >= 400 ? bodyErrorCode(outcome.body) : null;
679
+ await recordCapabilityExecution({
680
+ requestId,
681
+ origin: params.origin,
682
+ appId: params.appId,
683
+ capability: `${params.appId}.${params.capabilityId}`,
684
+ kind: params.kind === "queries" ? "query" : "action",
685
+ destructive: observed.destructive,
686
+ actorKind: principal?.type ?? null,
687
+ actorId: principal?.id ?? null,
688
+ userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? (principal?.type === "user" ? principal.id : null)),
689
+ accessSubject: subject && subject.id !== principal?.id ? subject : null,
690
+ status: executionStatus(outcome.status, errorCode),
691
+ errorCode,
692
+ inputMeta: capabilityValueMeta(params.input),
693
+ outputMeta: outcome.status < 400 ? resultDataMeta(outcome.body) : null,
694
+ idempotencyKey: CapabilityIdempotencyKeySchema.safeParse(params.request.headers.get("idempotency-key")).data ?? null,
695
+ replayed: outcome.replayed === true,
696
+ startedAt,
697
+ completedAt: new Date(),
698
+ }).catch((error) => {
699
+ // History is operational evidence; losing a row must not fail the call.
700
+ log.error("Capability execution could not be recorded", {
701
+ appId: params.appId,
702
+ capabilityId: params.capabilityId,
703
+ error: error instanceof Error ? error.message : String(error),
704
+ });
705
+ });
706
+ }
707
+ return capabilityJsonResponse(outcome.body, outcome.status);
512
708
  };
513
709
 
514
710
  class MandateDispatchError extends Error {
@@ -526,6 +722,11 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
526
722
  return new Hono<AuthContext>()
527
723
  .use("/capabilities/v1/*", dependencies.authenticate ?? auth.requireRole("authenticated"))
528
724
  .use("/capabilities/v1/*", rejectReservedWorkloadCredential)
725
+ .post("/capabilities/v1/streams/:verb", describeRoute({
726
+ tags:["Capabilities"],summary:"Transfer or inspect one authorized binary stream",...requiresAuth,
727
+ description:"POST read/write/status/abort with x-cloud-stream-id. Write sends raw bytes; read returns raw bytes. Status and abort return the upload state. No automatic retries.",
728
+ responses:{200:{description:"Binary content, write receipt, or transfer state"},403:{description:"Stream does not belong to this caller"},409:{description:"Transfer or contract changed"},410:{description:"Stream expired"}},
729
+ }), async c => dispatchCapabilityStream(c.req.raw, auth.getAuthority(c), c.req.param("verb"), dependencies))
529
730
  .get(
530
731
  "/capabilities/v1/catalog",
531
732
  describeRoute({
@@ -554,6 +755,52 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
554
755
  }
555
756
  },
556
757
  )
758
+ .post(
759
+ "/capabilities/v1/commands/:appId/:capabilityId",
760
+ describeRoute({
761
+ tags: ["Capabilities"],
762
+ summary: "Resolve an interactive Command link",
763
+ ...requiresAuth,
764
+ responses: {
765
+ 200: jsonResponse(CommandLinkSchema, "Command entry URL"),
766
+ 400: jsonResponse(CapabilityErrorSchema, "Invalid command input"),
767
+ 404: jsonResponse(CapabilityErrorSchema, "Command unavailable"),
768
+ 503: jsonResponse(CapabilityErrorSchema, "Route owner unavailable"),
769
+ },
770
+ }),
771
+ requireReadScope,
772
+ async (c) => {
773
+ const body = await readBoundedJson(c.req.raw, CAPABILITY_MAX_REQUEST_BYTES);
774
+ const request = body.ok ? CommandRequestSchema.safeParse(body.data) : null;
775
+ if (!request?.success) return c.json({ code: "VALIDATION_FAILED", message: capabilityMessages(getLocale(c)).requestBodyJson }, 400);
776
+ const appId = c.req.param("appId");
777
+ const entry = await (dependencies.getCapability ?? getCapability)(appId);
778
+ const command = entry?.manifest.commands.find((command) => command.localId === c.req.param("capabilityId"));
779
+ if (!command)
780
+ return c.json(
781
+ {
782
+ code: "CAPABILITY_NOT_FOUND",
783
+ message: capabilityMessages(getLocale(c)).notFound({ kind: "Command", reference: `${appId}.${c.req.param("capabilityId")}` }),
784
+ },
785
+ 404,
786
+ );
787
+ const apps = await (dependencies.listApps ?? listApps)();
788
+ const pathname = new URL(command.path, "https://cloud.invalid").pathname;
789
+ const owner = apps
790
+ .flatMap((app) => app.routes.map((prefix) => ({ app, prefix })))
791
+ .filter(({ prefix }) => pathname === prefix || pathname.startsWith(`${prefix}/`))
792
+ .sort((a, b) => b.prefix.length - a.prefix.length)[0]?.app;
793
+ if (owner?.id !== appId)
794
+ return c.json({ code: "APP_UNAVAILABLE", message: capabilityMessages(getLocale(c)).appUnavailable({ appId }) }, 503);
795
+ try {
796
+ const appUrl = await (dependencies.appUrl ?? (() => settings.get<string>("app.url")))();
797
+ return c.json(resolveCommandLink(appId, command, request.data.input, { returnTo: request.data.returnTo }, appUrl));
798
+ } catch (error) {
799
+ if (!(error instanceof z.ZodError) && !(error instanceof Error && error.message === "Command link is too large")) throw error;
800
+ return c.json({ code: "VALIDATION_FAILED", message: capabilityMessages(getLocale(c)).inputSchemaMismatch }, 400);
801
+ }
802
+ },
803
+ )
557
804
  .post(
558
805
  "/capabilities/v1/actions/:appId/:capabilityId/review",
559
806
  describeRoute({
@@ -594,6 +841,7 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
594
841
  request: c.req.raw,
595
842
  kind: "actions",
596
843
  review: true,
844
+ origin: "http",
597
845
  appId: c.req.param("appId") ?? "",
598
846
  capabilityId: c.req.param("capabilityId") ?? "",
599
847
  input: request.data.input,
@@ -644,6 +892,7 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
644
892
  return dispatchCapability({
645
893
  request: c.req.raw,
646
894
  kind: c.req.param("kind") as "queries" | "actions",
895
+ origin: "http",
647
896
  appId: c.req.param("appId") ?? "",
648
897
  capabilityId: c.req.param("capabilityId") ?? "",
649
898
  input: request.data.input,