@k2b/cloud 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/README.md +2 -2
  2. package/package.json +22 -11
  3. package/scripts/README.md +10 -0
  4. package/scripts/browser-performance.ts +14 -0
  5. package/scripts/build-pdf-renderer.ts +39 -0
  6. package/scripts/build.ts +20 -35
  7. package/scripts/preload.ts +16 -12
  8. package/scripts/sync-recovery-smoke.ts +22 -6
  9. package/src/_internal/app-presentation.ts +14 -2
  10. package/src/_internal/build-metadata.ts +8 -1
  11. package/src/_internal/capabilities.ts +131 -7
  12. package/src/_internal/capability-streams.ts +81 -0
  13. package/src/_internal/define-app.ts +80 -37
  14. package/src/_internal/heartbeat.ts +4 -0
  15. package/src/_internal/help-catalog.ts +27 -172
  16. package/src/_internal/help.ts +13 -76
  17. package/src/_internal/page-responses.ts +2 -0
  18. package/src/_internal/postgres-application-name.ts +30 -0
  19. package/src/_internal/process-identity.ts +15 -0
  20. package/src/_internal/process-sync.ts +1 -1
  21. package/src/_internal/readiness.ts +4 -1
  22. package/src/_internal/registry-validation.ts +29 -25
  23. package/src/_internal/registry.ts +2 -53
  24. package/src/_internal/runtime-context.ts +5 -16
  25. package/src/_internal/server-timing.ts +13 -0
  26. package/src/_internal/status-preserving-ssr.ts +18 -6
  27. package/src/_internal/web-vitals-asset.ts +10 -0
  28. package/src/access/PermissionEditor.tsx +18 -151
  29. package/src/access/PrincipalPicker.tsx +110 -0
  30. package/src/access/ResourceApiKeys.tsx +1 -1
  31. package/src/access/ui.ts +1 -0
  32. package/src/ai/admin.ts +14 -13
  33. package/src/ai/assistant-models.ts +12 -1
  34. package/src/ai/audio-format.ts +34 -0
  35. package/src/ai/audio-tool.ts +90 -0
  36. package/src/ai/browser-code-contracts.ts +201 -0
  37. package/src/ai/browser.ts +4 -0
  38. package/src/ai/capabilities.ts +55 -36
  39. package/src/ai/capability-execution.ts +47 -12
  40. package/src/ai/chat/blocks.tsx +229 -90
  41. package/src/ai/chat/builtin-tools.tsx +2 -4
  42. package/src/ai/chat/capability-result.ts +15 -0
  43. package/src/ai/chat/capability-table.tsx +2 -10
  44. package/src/ai/chat/composer-adapter.ts +83 -3
  45. package/src/ai/chat/message-actions.tsx +8 -7
  46. package/src/ai/chat/message-utils.ts +25 -1
  47. package/src/ai/chat/messages.ts +2 -0
  48. package/src/ai/chat/presentation.tsx +31 -47
  49. package/src/ai/chat/primitives.tsx +43 -1
  50. package/src/ai/chat/tool-disclosure.tsx +21 -2
  51. package/src/ai/chat/tool-groups.ts +58 -0
  52. package/src/ai/chat-quotas.ts +25 -0
  53. package/src/ai/chat-task-contracts.ts +14 -10
  54. package/src/ai/chat-tasks.ts +297 -43
  55. package/src/ai/client/controller.ts +165 -39
  56. package/src/ai/client/file-source.ts +12 -6
  57. package/src/ai/client/live-connection.ts +1 -0
  58. package/src/ai/client/projection.ts +29 -11
  59. package/src/ai/client/transport.ts +6 -0
  60. package/src/ai/code-capability-routes.ts +105 -0
  61. package/src/ai/code-capability-transport.ts +39 -0
  62. package/src/ai/code-execution.ts +53 -0
  63. package/src/ai/code-mode-skill.ts +118 -0
  64. package/src/ai/code-runtime-tools.ts +179 -0
  65. package/src/ai/code-source-contracts.ts +270 -0
  66. package/src/ai/code-source-tools.ts +188 -0
  67. package/src/ai/compaction.ts +27 -6
  68. package/src/ai/data-analysis-skill.ts +10 -0
  69. package/src/ai/default-tools.ts +99 -3
  70. package/src/ai/dictation-runtime.ts +171 -0
  71. package/src/ai/dictations.ts +188 -0
  72. package/src/ai/draft-content.ts +49 -0
  73. package/src/ai/enrich.ts +6 -0
  74. package/src/ai/executor.ts +189 -33
  75. package/src/ai/fetch-file-tool.ts +2 -2
  76. package/src/ai/file-content-version.ts +27 -0
  77. package/src/ai/file-context.ts +2 -1
  78. package/src/ai/file-media-type.ts +12 -0
  79. package/src/ai/file-reference-contracts.ts +15 -0
  80. package/src/ai/file-tools.ts +8 -1
  81. package/src/ai/files-store.ts +212 -71
  82. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  83. package/src/ai/grids-skill.ts +42 -10
  84. package/src/ai/http.ts +26 -8
  85. package/src/ai/index.ts +26 -3
  86. package/src/ai/inference-calls.ts +197 -0
  87. package/src/ai/live-events.ts +5 -1
  88. package/src/ai/live.ts +1 -1
  89. package/src/ai/markdown-pdf-tool.ts +2 -2
  90. package/src/ai/memories.ts +30 -27
  91. package/src/ai/memory-learning-runs.ts +1 -1
  92. package/src/ai/memory-learning.ts +64 -32
  93. package/src/ai/memory-tool.ts +1 -5
  94. package/src/ai/memory-workflow-evidence.ts +9 -4
  95. package/src/ai/message-queue.ts +178 -0
  96. package/src/ai/migrate.ts +189 -11
  97. package/src/ai/model-pricing.ts +43 -0
  98. package/src/ai/pdf-render-worker.ts +75 -0
  99. package/src/ai/pdf-render.fixture.ts +28 -0
  100. package/src/ai/pdf-render.ts +77 -0
  101. package/src/ai/projects-routes.ts +18 -43
  102. package/src/ai/projects.ts +33 -14
  103. package/src/ai/protocol.ts +3 -0
  104. package/src/ai/provider.ts +1 -8
  105. package/src/ai/quota-provider.ts +192 -0
  106. package/src/ai/quota-report.ts +152 -0
  107. package/src/ai/quotas-migrate.ts +45 -0
  108. package/src/ai/quotas.ts +168 -0
  109. package/src/ai/routes.ts +241 -11
  110. package/src/ai/run-timeout.ts +12 -0
  111. package/src/ai/runtime-tools.ts +7 -5
  112. package/src/ai/runtime.ts +74 -17
  113. package/src/ai/settings.ts +36 -8
  114. package/src/ai/short-id.ts +4 -5
  115. package/src/ai/skill-catalog.ts +0 -10
  116. package/src/ai/skill-search.ts +58 -0
  117. package/src/ai/skill-seeds.ts +114 -20
  118. package/src/ai/skill-tool.ts +21 -5
  119. package/src/ai/skills-routes.ts +49 -4
  120. package/src/ai/skills.ts +358 -163
  121. package/src/ai/store.ts +416 -201
  122. package/src/ai/stream.ts +5 -0
  123. package/src/ai/structured-runs.ts +4 -3
  124. package/src/ai/structured.ts +12 -1
  125. package/src/ai/system-prompt.ts +14 -2
  126. package/src/ai/task-contracts.ts +84 -0
  127. package/src/ai/task-execution.ts +92 -0
  128. package/src/ai/timeline.ts +2 -5
  129. package/src/ai/todo-contracts.ts +32 -0
  130. package/src/ai/todo-tool.ts +19 -0
  131. package/src/ai/tool-audit.ts +30 -40
  132. package/src/ai/tools.ts +6 -0
  133. package/src/ai/transcription.ts +202 -0
  134. package/src/ai/turn-timing.ts +114 -0
  135. package/src/ai/types.ts +58 -10
  136. package/src/ai/ui.tsx +3 -0
  137. package/src/ai/usage.ts +118 -118
  138. package/src/ai/vision-tool.ts +60 -10
  139. package/src/api/admin-ai-projects.ts +2 -2
  140. package/src/api/admin-ai-quotas.ts +80 -0
  141. package/src/api/admin-ai-skills.ts +48 -5
  142. package/src/api/admin-ai-usage.ts +2 -2
  143. package/src/api/admin-core-settings.ts +25 -0
  144. package/src/api/admin-identity.ts +91 -51
  145. package/src/api/admin-linux-identities.ts +10 -4
  146. package/src/api/admin-rail.ts +58 -0
  147. package/src/api/announcements.ts +14 -0
  148. package/src/api/app-approval.ts +2 -2
  149. package/src/api/auth.ts +1 -1
  150. package/src/api/capabilities.ts +324 -43
  151. package/src/api/capability-streams.ts +262 -0
  152. package/src/api/help.ts +8 -44
  153. package/src/api/index.ts +8 -2
  154. package/src/api/mcp.ts +45 -34
  155. package/src/api/me-web-vitals.ts +35 -0
  156. package/src/api/me.ts +2 -0
  157. package/src/api/search/schemas.ts +26 -15
  158. package/src/api/search.bench.ts +1 -1
  159. package/src/api/search.ts +54 -17
  160. package/src/browser/CloudResourceSearch.tsx +640 -702
  161. package/src/browser/app-approval-vault.ts +19 -7
  162. package/src/browser/command-bridge.ts +109 -0
  163. package/src/browser/command-shortcuts.ts +76 -0
  164. package/src/browser/commands.ts +67 -0
  165. package/src/browser/navigation-search.ts +48 -0
  166. package/src/browser/notifications.ts +1 -1
  167. package/src/browser/resource-picker.tsx +5 -18
  168. package/src/browser/resource-search-dialog.ts +9 -0
  169. package/src/browser/resource-search-input.ts +46 -0
  170. package/src/browser/resource-search-messages.ts +76 -40
  171. package/src/browser/resource-search.ts +7 -0
  172. package/src/browser/search-bridge.ts +102 -0
  173. package/src/browser/search-commands.ts +59 -0
  174. package/src/browser/search.ts +13 -0
  175. package/src/browser/spotlight-position.ts +208 -0
  176. package/src/browser/testing.ts +6 -0
  177. package/src/browser/web-vitals.ts +25 -0
  178. package/src/capabilities/claims.ts +179 -0
  179. package/src/capabilities/client.ts +34 -0
  180. package/src/capabilities/command-link.ts +16 -0
  181. package/src/capabilities/executions.ts +298 -0
  182. package/src/capabilities/migrate.ts +81 -0
  183. package/src/capabilities/server.ts +116 -35
  184. package/src/capabilities/stream-body.ts +16 -0
  185. package/src/capabilities/streams.ts +28 -0
  186. package/src/cli/admin/ai-quotas.ts +161 -0
  187. package/src/cli/admin/ai-skills.ts +64 -0
  188. package/src/cli/admin/ai-usage.ts +53 -30
  189. package/src/cli/admin/app-credentials.ts +77 -0
  190. package/src/cli/admin/data.ts +17 -0
  191. package/src/cli/admin/index.ts +10 -1
  192. package/src/cli/admin/instance.ts +2 -2
  193. package/src/cli/admin/linux.ts +4 -2
  194. package/src/cli/admin/sync.ts +1 -1
  195. package/src/cli/capabilities.ts +42 -0
  196. package/src/cli/capability-streams.ts +25 -0
  197. package/src/cli/index.ts +3 -2
  198. package/src/config/define-env.ts +97 -0
  199. package/src/config/env.ts +147 -51
  200. package/src/config/index.ts +3 -1
  201. package/src/contracts/app.ts +23 -3
  202. package/src/contracts/capabilities.ts +75 -5
  203. package/src/contracts/capability-streams.ts +46 -0
  204. package/src/contracts/commands.ts +49 -0
  205. package/src/contracts/index.ts +5 -2
  206. package/src/contracts/rail-admin.ts +38 -0
  207. package/src/contracts/rail-preferences.ts +16 -0
  208. package/src/contracts/registry.ts +5 -27
  209. package/src/contracts/settings-types.ts +1 -1
  210. package/src/contracts/shared.ts +18 -5
  211. package/src/contracts/web-vitals.ts +27 -0
  212. package/src/desktop/index.ts +1 -0
  213. package/src/desktop/solid.tsx +1 -1
  214. package/src/index.ts +8 -6
  215. package/src/server/index.ts +3 -0
  216. package/src/server/middleware/auth.ts +3 -2
  217. package/src/server/middleware/route-template.ts +9 -3
  218. package/src/server/middleware/runtime.ts +2 -1
  219. package/src/server/middleware/settings.ts +17 -11
  220. package/src/server/ratelimit.ts +1 -1
  221. package/src/server/services/access-revision.ts +10 -0
  222. package/src/server/services/access.ts +3 -1
  223. package/src/server/services/index.ts +1 -0
  224. package/src/services/account-category-policy.ts +9 -0
  225. package/src/services/account-lifecycle/audit.ts +1 -1
  226. package/src/services/accounts/app.ts +12 -2
  227. package/src/services/accounts/email-write.ts +23 -0
  228. package/src/services/accounts/groups.ts +15 -4
  229. package/src/services/accounts/identities.ts +249 -0
  230. package/src/services/accounts/identity-reconciliation.ts +87 -0
  231. package/src/services/accounts/ipa-data.ts +2 -2
  232. package/src/services/accounts/local-groups.ts +2 -2
  233. package/src/services/accounts/notification-sender.ts +12 -2
  234. package/src/services/accounts/posix.ts +45 -29
  235. package/src/services/accounts/users.ts +2 -2
  236. package/src/services/announcements/index.ts +69 -8
  237. package/src/services/app-approval.ts +11 -10
  238. package/src/services/audit/index.ts +1 -1
  239. package/src/services/cache-fill.ts +35 -0
  240. package/src/services/help/index.ts +109 -0
  241. package/src/services/help/maintenance.ts +25 -0
  242. package/src/services/help/store.ts +180 -0
  243. package/src/services/help/types.ts +46 -0
  244. package/src/services/identity/invocation-actor.ts +2 -2
  245. package/src/services/identity/key-config.ts +8 -5
  246. package/src/services/identity/key-ring.ts +1 -1
  247. package/src/services/identity/runtime-config.ts +6 -5
  248. package/src/services/index.ts +15 -0
  249. package/src/services/ipa/posix.ts +1 -1
  250. package/src/services/ipa/sync.ts +1 -1
  251. package/src/services/ipa/users.ts +268 -221
  252. package/src/services/legal-consent.ts +1 -1
  253. package/src/services/logging/index.ts +15 -13
  254. package/src/services/logging/trace.ts +2 -1
  255. package/src/services/logging/web-vitals.ts +81 -0
  256. package/src/services/mandates/index.ts +21 -4
  257. package/src/services/mandates/policy.ts +165 -2
  258. package/src/services/notifications/batches.ts +125 -57
  259. package/src/services/notifications/browser.ts +12 -0
  260. package/src/services/notifications/index.ts +0 -2
  261. package/src/services/notifications/platform.ts +0 -4
  262. package/src/services/oauth-tokens.ts +3 -3
  263. package/src/services/pdf/gotenberg.ts +131 -72
  264. package/src/services/pdf/index.ts +11 -8
  265. package/src/services/providers/local/users.ts +25 -16
  266. package/src/services/public-http.ts +101 -0
  267. package/src/services/rail-shortcuts.ts +95 -0
  268. package/src/services/rail-snapshot.ts +53 -0
  269. package/src/services/request-cache-redis.ts +29 -0
  270. package/src/services/session/index.ts +15 -2
  271. package/src/services/session/user.ts +62 -37
  272. package/src/services/settings/app.ts +5 -1
  273. package/src/services/settings/core-settings.ts +26 -35
  274. package/src/services/settings/defaults.ts +5 -0
  275. package/src/services/settings/index.ts +3 -3
  276. package/src/services/settings/store.ts +58 -84
  277. package/src/services/webauthn.ts +1 -1
  278. package/src/shared/ai-costs.ts +42 -0
  279. package/src/shared/ai-platform-prompt.ts +1 -1
  280. package/src/shared/ai-quotas.ts +136 -0
  281. package/src/shared/ai-usage.ts +6 -16
  282. package/src/shared/app-presentation.ts +21 -15
  283. package/src/shared/capability-messages.ts +14 -0
  284. package/src/shared/help.ts +6 -42
  285. package/src/shared/index.ts +3 -0
  286. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  287. package/src/ssr/AdminLayout.tsx +7 -5
  288. package/src/ssr/AdminSidebar.tsx +30 -16
  289. package/src/ssr/AppLaunchpad.island.tsx +33 -136
  290. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  291. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  292. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  293. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  294. package/src/ssr/Layout.tsx +17 -6
  295. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  296. package/src/ssr/LayoutFooter.tsx +1 -1
  297. package/src/ssr/LayoutHeader.tsx +46 -14
  298. package/src/ssr/LayoutHelp.tsx +22 -23
  299. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  300. package/src/ssr/LayoutRail.tsx +17 -9
  301. package/src/ssr/MinimalLayout.tsx +1 -1
  302. package/src/ssr/MobileNavigation.tsx +71 -0
  303. package/src/ssr/MobileProfileActions.tsx +49 -0
  304. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  305. package/src/ssr/RailApps.island.tsx +2 -2
  306. package/src/ssr/RailEditor.tsx +17 -2
  307. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  308. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  309. package/src/ssr/admin-navigation.ts +11 -1
  310. package/src/ssr/help.ts +29 -0
  311. package/src/ssr/islands/SearchBar.tsx +67 -0
  312. package/src/ssr/islands/index.ts +3 -1
  313. package/src/ssr/layout-context.ts +2 -2
  314. package/src/ssr/layout-help-search.ts +15 -0
  315. package/src/ssr/mobile-menu-history.ts +70 -0
  316. package/src/ssr/platform-messages.ts +16 -8
  317. package/src/ssr/profile-actions.ts +42 -0
  318. package/src/ssr/rail-context.ts +8 -4
  319. package/src/ssr/rail-messages.ts +2 -0
  320. package/src/ssr/rail-navigation.ts +3 -3
  321. package/src/ssr/workspace-navigation.ts +75 -0
  322. package/src/styles/effects.css +6 -6
  323. package/src/styles/global.css +54 -0
  324. package/src/styles/input.css +43 -41
  325. package/src/styles/resource-search.css +505 -0
  326. package/src/styles/utilities-feedback.css +13 -0
  327. package/src/styles/utilities-layout.css +1 -5
  328. package/src/styles/utilities-navigation.css +3 -39
  329. package/src/types/ambient.d.ts +5 -0
  330. package/src/workflows/ai/index.ts +2 -2
  331. package/src/workflows/ai/runtime.ts +19 -90
  332. package/src/workflows/ai/store.ts +2 -0
  333. package/src/workflows/ai/types.ts +6 -82
  334. package/src/workflows/ai-actions.ts +16 -13
  335. package/src/workflows/definition.ts +10 -5
  336. package/src/workflows/store/actions.ts +44 -20
  337. package/src/workflows/store/index.ts +4 -2
  338. package/src/workflows/store/runs.ts +36 -0
  339. package/src/workflows/store/worker-pool.ts +55 -0
  340. package/src/workflows/store/worker-runtime.ts +90 -0
  341. package/src/ai/kit-skill.ts +0 -22
  342. package/src/ai/usage-migrate.ts +0 -81
  343. package/src/browser/notification-target.ts +0 -27
  344. package/src/contracts/notification-live.ts +0 -66
  345. package/src/server/services/freeipa/test-certificates.ts +0 -44
  346. package/src/services/notifications/live.ts +0 -50
  347. package/src/services/session/test-fixture.ts +0 -18
  348. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  349. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -4,19 +4,37 @@ import { z } from "zod";
4
4
  import { readBoundedJson } from "../_internal/bounded-json";
5
5
  import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
6
6
  import { getCapability, listApps } from "../_internal/registry";
7
+ import { resolveCommandLink } from "../capabilities/command-link";
8
+ import {
9
+ type CapabilityClaimScope,
10
+ type CapabilityExecutionStatus,
11
+ capabilityIdempotencyKeyHash,
12
+ capabilityRequestHash,
13
+ capabilityValueMeta,
14
+ claimCapabilityIdempotency,
15
+ completeCapabilityClaim,
16
+ markCapabilityClaimUncertain,
17
+ recordCapabilityExecution,
18
+ releaseCapabilityClaim,
19
+ resolveCapabilityClaim,
20
+ } from "../capabilities/executions";
7
21
  import {
8
22
  CAPABILITY_FRAMEWORK_ERROR_CODES,
9
23
  CAPABILITY_MAX_CATALOG_BYTES,
10
24
  CAPABILITY_MAX_REQUEST_BYTES,
11
25
  CAPABILITY_MAX_RESULT_BYTES,
26
+ CAPABILITY_ORIGIN_HEADER,
12
27
  CAPABILITY_PROTOCOL_VERSION,
13
28
  CapabilityActionReviewSchema,
14
29
  type CapabilityCatalog,
15
30
  CapabilityCatalogSchema,
16
31
  CapabilityErrorSchema,
17
32
  CapabilityIdempotencyKeySchema,
33
+ type CapabilityOrigin,
18
34
  capabilityResultJsonSchema,
19
35
  } from "../contracts/capabilities";
36
+ import { CapabilityStreamSchema } from "../contracts/capability-streams";
37
+ import { CommandLinkSchema, CommandRequestSchema } from "../contracts/commands";
20
38
  import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
21
39
  import {
22
40
  type AuthContext,
@@ -30,7 +48,7 @@ import {
30
48
  requiresAuth,
31
49
  v,
32
50
  } from "../server";
33
- import { logger } from "../services";
51
+ import { logger, settings } from "../services";
34
52
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
35
53
  import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
36
54
  import type { InvocationAuthority } from "../services/identity/invocation-token";
@@ -39,6 +57,7 @@ import { withActiveIdentitySigner } from "../services/identity/key-ring";
39
57
  import { withMandateIssueAuthority } from "../services/mandates";
40
58
  import { resolveAppPresentation } from "../shared/app-presentation";
41
59
  import { capabilityMessages } from "../shared/capability-messages";
60
+ import { dispatchCapabilityStream, sealCapabilityStream } from "./capability-streams";
42
61
 
43
62
  const log = logger("capabilities");
44
63
  const QUERY_TIMEOUT_MS = 15_000;
@@ -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,50 @@ 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)
344
+ return errorResponse("STREAM_UNSUPPORTED_AUTHORITY", "Streams require an interactive user or service-account request", 403);
345
+
299
346
  let requestBody: string;
300
347
  try {
301
348
  requestBody = JSON.stringify({ input: input.data });
302
349
  } catch {
303
350
  const invalid = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, messages.inputNotSerializable, 400);
304
- return capabilityJsonResponse(invalid.body, invalid.status);
351
+ return invalid;
305
352
  }
306
353
  if (new TextEncoder().encode(requestBody).byteLength > CAPABILITY_MAX_REQUEST_BYTES) {
307
354
  const invalid = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, messages.requestTooLarge, 400);
308
- return capabilityJsonResponse(invalid.body, invalid.status);
355
+ return invalid;
309
356
  }
310
357
 
311
358
  const invocationOperation = capabilityInvocationOperation(params.kind, params.capabilityId, params.review);
@@ -332,17 +379,20 @@ export const dispatchCapability = async (params: {
332
379
  targetAppId: params.appId,
333
380
  operation: invocationOperation,
334
381
  actionApproval: mandate.actionApproval ?? "none",
335
- requestId: normalizeInvocationRequestId(params.request.headers.get("x-request-id")),
382
+ input: params.input,
383
+ capabilityApproval: "approval" in operation ? (operation.approval ?? "always") : "always",
384
+ requestId,
336
385
  },
337
386
  async (mandateAuthority) => {
338
387
  signal.throwIfAborted();
388
+ observed.subject = mandateAuthority.subject;
339
389
  return (params.dependencies?.signInvocation ?? signInvocationToken)({
340
390
  targetAppId: params.appId,
341
391
  callingAppId: mandate.ownerAppId,
342
392
  operation: invocationOperation,
343
393
  schemaHash: operation.schemaHash,
344
394
  authority: invocationAuthorityFromMandate(mandateAuthority),
345
- requestId: normalizeInvocationRequestId(params.request.headers.get("x-request-id")),
395
+ requestId,
346
396
  signer,
347
397
  issuer: signer.issuer,
348
398
  });
@@ -376,7 +426,7 @@ export const dispatchCapability = async (params: {
376
426
  operation: invocationOperation,
377
427
  schemaHash: operation.schemaHash,
378
428
  authority: invocationAuthorityFromRequest(authority),
379
- requestId: normalizeInvocationRequestId(params.request.headers.get("x-request-id")),
429
+ requestId,
380
430
  signer,
381
431
  issuer: signer.issuer,
382
432
  }),
@@ -397,7 +447,7 @@ export const dispatchCapability = async (params: {
397
447
  error.status === 409 ? "Mandate revision changed" : "Mandate does not authorize this capability invocation",
398
448
  error.status,
399
449
  );
400
- return capabilityJsonResponse(denied.body, denied.status);
450
+ return denied;
401
451
  }
402
452
  if (params.request.signal.aborted || timeout.aborted) {
403
453
  const cancelled = params.request.signal.aborted;
@@ -407,15 +457,16 @@ export const dispatchCapability = async (params: {
407
457
  cancelled ? 499 : 504,
408
458
  { retrySafe: true },
409
459
  );
410
- return capabilityJsonResponse(failure.body, failure.status);
460
+ return failure;
411
461
  }
412
462
  console.error("[capabilities] Invocation issuance failed", { appId: params.appId, operation: invocationOperation });
413
463
  const failure = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.internal, messages.issuanceFailed, 500, { retrySafe: true });
414
- return capabilityJsonResponse(failure.body, failure.status);
464
+ return failure;
415
465
  }
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);
466
+ headers.set("x-request-id", requestId);
467
+ headers.set(CAPABILITY_ORIGIN_HEADER, params.origin);
468
+ for (const name of ["traceparent", "tracestate"] as const) {
469
+ const value = params.request.headers.get(name);
419
470
  if (value) headers.set(name, value);
420
471
  }
421
472
  const invocationLocale = preferredLocale(params.request.headers);
@@ -424,11 +475,74 @@ export const dispatchCapability = async (params: {
424
475
  headers.set("x-cloud-capability-schema-hash", operation.schemaHash);
425
476
  const actionWithoutRetrySafety =
426
477
  params.kind === "actions" && !params.review && "idempotency" in operation && operation.idempotency === "none";
427
- const outcomeUnknown = (): Response => {
478
+ const outcomeUnknown = (): CapabilityDispatchOutcome => {
428
479
  const unknown = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.actionOutcomeUnknown, messages.actionOutcomeUnknown, 502, {
429
480
  retrySafe: false,
430
481
  });
431
- return capabilityJsonResponse(unknown.body, unknown.status);
482
+ return unknown;
483
+ };
484
+
485
+ const claimPrincipal = dispatchPrincipal(params, observed);
486
+ const claimScope: CapabilityClaimScope | null =
487
+ params.kind === "actions" &&
488
+ !params.review &&
489
+ "idempotency" in operation &&
490
+ operation.idempotency === "required" &&
491
+ idempotencyKey?.success
492
+ ? {
493
+ appId: params.appId,
494
+ capability: `${params.appId}.${params.capabilityId}`,
495
+ principal: claimPrincipal ? `${claimPrincipal.type}:${claimPrincipal.id}` : "anonymous",
496
+ keyHash: capabilityIdempotencyKeyHash(idempotencyKey.data),
497
+ }
498
+ : null;
499
+ if (claimScope) {
500
+ let claim: Awaited<ReturnType<typeof claimCapabilityIdempotency>>;
501
+ try {
502
+ claim = await claimCapabilityIdempotency(claimScope, capabilityRequestHash(input.data));
503
+ } catch (error) {
504
+ // Nothing has been forwarded yet, so refusing the call is the safe answer.
505
+ log.error("Capability idempotency claim failed", {
506
+ appId: params.appId,
507
+ capabilityId: params.capabilityId,
508
+ error: error instanceof Error ? error.message : String(error),
509
+ });
510
+ return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.internal, messages.cloudUnavailable, 503, { retrySafe: true });
511
+ }
512
+ if (claim.state === "replay") return { body: claim.body, status: claim.status, replayed: true };
513
+ if (claim.state === "conflict")
514
+ return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyConflict, messages.idempotencyConflict, 409);
515
+ if (claim.state === "in_flight") {
516
+ return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyInProgress, messages.idempotencyInProgress, 409, {
517
+ retrySafe: false,
518
+ });
519
+ }
520
+ if (claim.state === "uncertain") {
521
+ return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyUncertain, messages.idempotencyUncertain, 409, { retrySafe: false });
522
+ }
523
+ if (claim.state === "not_retained") {
524
+ return errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.idempotencyUncertain, messages.idempotencyResultNotRetained, 409, {
525
+ retrySafe: false,
526
+ });
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Resolve the claim once the outcome is known: a definitive success is
532
+ * replayable, a failure that proves nothing happened frees the key for a
533
+ * corrected retry, and anything ambiguous freezes the key.
534
+ */
535
+ const settle = async (outcome: CapabilityDispatchOutcome, disposition: "succeeded" | "nothing_happened" | "uncertain") => {
536
+ if (!claimScope) return outcome;
537
+ await resolveCapabilityClaim(
538
+ disposition === "succeeded"
539
+ ? completeCapabilityClaim(claimScope, outcome.status, outcome.body)
540
+ : disposition === "nothing_happened"
541
+ ? releaseCapabilityClaim(claimScope)
542
+ : markCapabilityClaimUncertain(claimScope),
543
+ claimScope,
544
+ );
545
+ return outcome;
432
546
  };
433
547
 
434
548
  let response: Response;
@@ -449,18 +563,18 @@ export const dispatchCapability = async (params: {
449
563
  kind: params.kind,
450
564
  error: error instanceof Error ? error.message : String(error),
451
565
  });
452
- if (actionWithoutRetrySafety) return outcomeUnknown();
566
+ if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
453
567
  if (params.request.signal.aborted) {
454
568
  const cancelled = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.requestCancelled, messages.requestCancelled, 499, {
455
569
  retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required"),
456
570
  });
457
- return capabilityJsonResponse(cancelled.body, cancelled.status);
571
+ return settle(cancelled, "uncertain");
458
572
  }
459
573
  if (timeout.aborted) {
460
574
  const deadline = errorResponse(CAPABILITY_FRAMEWORK_ERROR_CODES.deadlineExceeded, messages.deadlineExceeded, 504, {
461
575
  retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required"),
462
576
  });
463
- return capabilityJsonResponse(deadline.body, deadline.status);
577
+ return settle(deadline, "uncertain");
464
578
  }
465
579
  const unavailable = errorResponse(
466
580
  CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable,
@@ -468,28 +582,30 @@ export const dispatchCapability = async (params: {
468
582
  503,
469
583
  { retrySafe: params.kind === "queries" || ("idempotency" in operation && operation.idempotency === "required") },
470
584
  );
471
- return capabilityJsonResponse(unavailable.body, unavailable.status);
585
+ return settle(unavailable, "uncertain");
472
586
  }
473
587
 
474
588
  const upstreamBody = await readBoundedJson(response, CAPABILITY_MAX_RESULT_BYTES);
475
589
  if (!upstreamBody.ok) {
476
- if (actionWithoutRetrySafety) return outcomeUnknown();
590
+ if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
477
591
  const code =
478
592
  upstreamBody.reason === "too_large"
479
593
  ? CAPABILITY_FRAMEWORK_ERROR_CODES.responseTooLarge
480
594
  : CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse;
481
595
  const invalid = errorResponse(code, messages.invalidCapabilityJson({ appId: params.appId }), 502);
482
- return capabilityJsonResponse(invalid.body, invalid.status);
596
+ return settle(invalid, "uncertain");
483
597
  }
484
598
 
485
599
  if (!response.ok) {
486
600
  const parsed = CapabilityErrorSchema.safeParse(upstreamBody.data);
487
601
  if (parsed.success && response.status >= 400 && response.status <= 599) {
488
- return capabilityJsonResponse(parsed.data, response.status);
602
+ // A 4xx from the owning app is a decision about the request itself and
603
+ // proves the effect did not happen; a 5xx proves nothing.
604
+ return settle({ body: parsed.data, status: response.status }, response.status < 500 ? "nothing_happened" : "uncertain");
489
605
  }
490
- if (actionWithoutRetrySafety) return outcomeUnknown();
606
+ if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
491
607
  const invalid = errorResponse("INVALID_APP_RESPONSE", messages.invalidCapabilityError({ appId: params.appId }), 502);
492
- return capabilityJsonResponse(invalid.body, invalid.status);
608
+ return settle(invalid, "uncertain");
493
609
  }
494
610
 
495
611
  const resultValidator = params.review
@@ -503,12 +619,112 @@ export const dispatchCapability = async (params: {
503
619
  !parsedReview?.success ||
504
620
  (operation.approval === "rememberable" ? parsedReview.data.approvalScope !== undefined : parsedReview.data.approvalScope === undefined);
505
621
  if (!resultValidator || !parsedResult?.success || !reviewApprovalScopeIsValid) {
506
- if (actionWithoutRetrySafety) return outcomeUnknown();
622
+ if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
507
623
  const invalid = errorResponse("INVALID_APP_RESPONSE", messages.outsideResultSchema({ appId: params.appId }), 502);
508
- return capabilityJsonResponse(invalid.body, invalid.status);
624
+ return settle(invalid, "uncertain");
509
625
  }
510
626
 
511
- return capabilityJsonResponse(upstreamBody.data, 200);
627
+ const body = upstreamBody.data as Record<string, unknown>;
628
+ if (!params.review && body.stream !== undefined) {
629
+ const stream = CapabilityStreamSchema.safeParse(body.stream);
630
+ if (
631
+ !stream.success ||
632
+ !operation.stream ||
633
+ stream.data.direction !== operation.stream.direction ||
634
+ stream.data.size > operation.stream.maxBytes ||
635
+ Date.parse(stream.data.expiresAt) <= Date.now() ||
636
+ Date.parse(stream.data.expiresAt) > Date.now() + 86_400_000 ||
637
+ !params.authority
638
+ )
639
+ return settle(errorResponse("INVALID_APP_RESPONSE", "Invalid stream offer", 502), "uncertain");
640
+ try {
641
+ body.stream = await sealCapabilityStream(stream.data, {
642
+ appId: params.appId,
643
+ kind: params.kind,
644
+ capabilityId: params.capabilityId,
645
+ schemaHash: operation.schemaHash,
646
+ authority: params.authority,
647
+ requestId,
648
+ origin: params.origin,
649
+ continuation: params.continuation,
650
+ });
651
+ if (Buffer.byteLength(JSON.stringify(body)) > CAPABILITY_MAX_RESULT_BYTES) throw new Error("Stream result too large");
652
+ } catch {
653
+ return settle(errorResponse("INVALID_APP_RESPONSE", "Invalid stream offer", 502), "uncertain");
654
+ }
655
+ }
656
+ return settle({ body, status: 200 }, "succeeded");
657
+ };
658
+
659
+ const executionStatus = (status: number, code: string | null): CapabilityExecutionStatus => {
660
+ if (status < 400) return "succeeded";
661
+ if (status === 400) return "invalid_input";
662
+ if (status === 401 || status === 403) return "denied";
663
+ if (status === 504 || code === CAPABILITY_FRAMEWORK_ERROR_CODES.deadlineExceeded) return "timed_out";
664
+ return "failed";
665
+ };
666
+
667
+ const bodyErrorCode = (body: unknown): string | null =>
668
+ body && typeof body === "object" && "code" in body && typeof body.code === "string" ? body.code : null;
669
+
670
+ const resultDataMeta = (body: unknown) =>
671
+ capabilityValueMeta(body && typeof body === "object" && "data" in body ? (body as { data: unknown }).data : body);
672
+
673
+ /**
674
+ * Dispatch one already-parsed capability invocation through the live registry.
675
+ * HTTP, MCP, and assistant callers share this exact app lookup, credential
676
+ * forwarding, schema pinning, timeout, and response validation path.
677
+ *
678
+ * One invocation
679
+ * produces exactly one execution row, whatever its outcome. Binary stream
680
+ * continuations record their own row under the same request ID. Action reviews are a
681
+ * read-only preview and are not executions, so they are not recorded.
682
+ */
683
+ export const dispatchCapability = async (params: CapabilityDispatchParams): Promise<Response> => {
684
+ const requestId = normalizeInvocationRequestId(params.request.headers.get("x-request-id")) ?? crypto.randomUUID();
685
+ const observed: CapabilityDispatchObservation = { subject: null, destructive: false };
686
+ const startedAt = new Date();
687
+ const outcome = await runCapabilityDispatch(params, requestId, observed);
688
+ if (!params.review) {
689
+ const actor = params.authority?.actor;
690
+ const principal = dispatchPrincipal(params, observed);
691
+ const accessSubject = params.authority?.accessSubject;
692
+ const subject =
693
+ accessSubject?.type === "user"
694
+ ? ({ type: "user", id: accessSubject.userId } as const)
695
+ : accessSubject?.type === "service_account"
696
+ ? ({ type: "service_account", id: accessSubject.serviceAccountId } as const)
697
+ : null;
698
+ const errorCode = outcome.status >= 400 ? bodyErrorCode(outcome.body) : null;
699
+ await recordCapabilityExecution({
700
+ requestId,
701
+ origin: params.origin,
702
+ appId: params.appId,
703
+ capability: `${params.appId}.${params.capabilityId}`,
704
+ kind: params.kind === "queries" ? "query" : "action",
705
+ destructive: observed.destructive,
706
+ actorKind: principal?.type ?? null,
707
+ actorId: principal?.id ?? null,
708
+ userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? (principal?.type === "user" ? principal.id : null)),
709
+ accessSubject: subject && subject.id !== principal?.id ? subject : null,
710
+ status: executionStatus(outcome.status, errorCode),
711
+ errorCode,
712
+ inputMeta: capabilityValueMeta(params.input),
713
+ outputMeta: outcome.status < 400 ? resultDataMeta(outcome.body) : null,
714
+ idempotencyKey: CapabilityIdempotencyKeySchema.safeParse(params.request.headers.get("idempotency-key")).data ?? null,
715
+ replayed: outcome.replayed === true,
716
+ startedAt,
717
+ completedAt: new Date(),
718
+ }).catch((error) => {
719
+ // History is operational evidence; losing a row must not fail the call.
720
+ log.error("Capability execution could not be recorded", {
721
+ appId: params.appId,
722
+ capabilityId: params.capabilityId,
723
+ error: error instanceof Error ? error.message : String(error),
724
+ });
725
+ });
726
+ }
727
+ return capabilityJsonResponse(outcome.body, outcome.status);
512
728
  };
513
729
 
514
730
  class MandateDispatchError extends Error {
@@ -526,6 +742,23 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
526
742
  return new Hono<AuthContext>()
527
743
  .use("/capabilities/v1/*", dependencies.authenticate ?? auth.requireRole("authenticated"))
528
744
  .use("/capabilities/v1/*", rejectReservedWorkloadCredential)
745
+ .post(
746
+ "/capabilities/v1/streams/:verb",
747
+ describeRoute({
748
+ tags: ["Capabilities"],
749
+ summary: "Transfer or inspect one authorized binary stream",
750
+ ...requiresAuth,
751
+ description:
752
+ "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.",
753
+ responses: {
754
+ 200: { description: "Binary content, write receipt, or transfer state" },
755
+ 403: { description: "Stream does not belong to this caller" },
756
+ 409: { description: "Transfer or contract changed" },
757
+ 410: { description: "Stream expired" },
758
+ },
759
+ }),
760
+ async (c) => dispatchCapabilityStream(c.req.raw, auth.getAuthority(c), c.req.param("verb"), dependencies),
761
+ )
529
762
  .get(
530
763
  "/capabilities/v1/catalog",
531
764
  describeRoute({
@@ -554,6 +787,52 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
554
787
  }
555
788
  },
556
789
  )
790
+ .post(
791
+ "/capabilities/v1/commands/:appId/:capabilityId",
792
+ describeRoute({
793
+ tags: ["Capabilities"],
794
+ summary: "Resolve an interactive Command link",
795
+ ...requiresAuth,
796
+ responses: {
797
+ 200: jsonResponse(CommandLinkSchema, "Command entry URL"),
798
+ 400: jsonResponse(CapabilityErrorSchema, "Invalid command input"),
799
+ 404: jsonResponse(CapabilityErrorSchema, "Command unavailable"),
800
+ 503: jsonResponse(CapabilityErrorSchema, "Route owner unavailable"),
801
+ },
802
+ }),
803
+ requireReadScope,
804
+ async (c) => {
805
+ const body = await readBoundedJson(c.req.raw, CAPABILITY_MAX_REQUEST_BYTES);
806
+ const request = body.ok ? CommandRequestSchema.safeParse(body.data) : null;
807
+ if (!request?.success) return c.json({ code: "VALIDATION_FAILED", message: capabilityMessages(getLocale(c)).requestBodyJson }, 400);
808
+ const appId = c.req.param("appId");
809
+ const entry = await (dependencies.getCapability ?? getCapability)(appId);
810
+ const command = entry?.manifest.commands.find((command) => command.localId === c.req.param("capabilityId"));
811
+ if (!command)
812
+ return c.json(
813
+ {
814
+ code: "CAPABILITY_NOT_FOUND",
815
+ message: capabilityMessages(getLocale(c)).notFound({ kind: "Command", reference: `${appId}.${c.req.param("capabilityId")}` }),
816
+ },
817
+ 404,
818
+ );
819
+ const apps = await (dependencies.listApps ?? listApps)();
820
+ const pathname = new URL(command.path, "https://cloud.invalid").pathname;
821
+ const owner = apps
822
+ .flatMap((app) => app.routes.map((prefix) => ({ app, prefix })))
823
+ .filter(({ prefix }) => pathname === prefix || pathname.startsWith(`${prefix}/`))
824
+ .sort((a, b) => b.prefix.length - a.prefix.length)[0]?.app;
825
+ if (owner?.id !== appId)
826
+ return c.json({ code: "APP_UNAVAILABLE", message: capabilityMessages(getLocale(c)).appUnavailable({ appId }) }, 503);
827
+ try {
828
+ const appUrl = await (dependencies.appUrl ?? (() => settings.get<string>("app.url")))();
829
+ return c.json(resolveCommandLink(appId, command, request.data.input, { returnTo: request.data.returnTo }, appUrl));
830
+ } catch (error) {
831
+ if (!(error instanceof z.ZodError) && !(error instanceof Error && error.message === "Command link is too large")) throw error;
832
+ return c.json({ code: "VALIDATION_FAILED", message: capabilityMessages(getLocale(c)).inputSchemaMismatch }, 400);
833
+ }
834
+ },
835
+ )
557
836
  .post(
558
837
  "/capabilities/v1/actions/:appId/:capabilityId/review",
559
838
  describeRoute({
@@ -594,6 +873,7 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
594
873
  request: c.req.raw,
595
874
  kind: "actions",
596
875
  review: true,
876
+ origin: "http",
597
877
  appId: c.req.param("appId") ?? "",
598
878
  capabilityId: c.req.param("capabilityId") ?? "",
599
879
  input: request.data.input,
@@ -644,6 +924,7 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
644
924
  return dispatchCapability({
645
925
  request: c.req.raw,
646
926
  kind: c.req.param("kind") as "queries" | "actions",
927
+ origin: "http",
647
928
  appId: c.req.param("appId") ?? "",
648
929
  capabilityId: c.req.param("capabilityId") ?? "",
649
930
  input: request.data.input,