@k2b/cloud 0.39.0 → 0.41.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 (237) hide show
  1. package/README.md +8 -0
  2. package/package.json +11 -4
  3. package/src/_internal/app-favicon.ts +4 -2
  4. package/src/_internal/capabilities.ts +72 -48
  5. package/src/_internal/capability-reviews.ts +81 -0
  6. package/src/_internal/capability-schemas.ts +29 -0
  7. package/src/_internal/capability-sentences.ts +217 -1
  8. package/src/_internal/capability-streams.ts +7 -0
  9. package/src/_internal/define-app.ts +49 -18
  10. package/src/_internal/font-preloads.ts +2 -1
  11. package/src/_internal/heartbeat.ts +203 -14
  12. package/src/_internal/registry.ts +28 -1
  13. package/src/_internal/runtime-watcher.ts +6 -0
  14. package/src/_internal/setup-lock.ts +103 -0
  15. package/src/_internal/static-assets.ts +52 -3
  16. package/src/_internal/status-preserving-ssr.ts +2 -0
  17. package/src/access/PermissionEditor.tsx +2 -2
  18. package/src/account/EntitySearch.tsx +1 -1
  19. package/src/ai/action-review.ts +101 -0
  20. package/src/ai/background-review.ts +72 -0
  21. package/src/ai/capabilities.ts +29 -11
  22. package/src/ai/capability-execution.ts +76 -43
  23. package/src/ai/capability-result.ts +95 -0
  24. package/src/ai/chat/action-card.ts +464 -0
  25. package/src/ai/chat/action-messages.ts +129 -0
  26. package/src/ai/chat/action-question.browser-harness.tsx +245 -0
  27. package/src/ai/chat/action-question.fixtures.ts +733 -0
  28. package/src/ai/chat/action-question.tsx +680 -0
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/blocks.tsx +19 -25
  31. package/src/ai/chat/message-actions.tsx +6 -0
  32. package/src/ai/chat/tool-action-words.ts +31 -0
  33. package/src/ai/chat/tool-action.tsx +456 -0
  34. package/src/ai/chat/turn-view.tsx +10 -160
  35. package/src/ai/chat/view-block.tsx +15 -5
  36. package/src/ai/chat-task-contracts.ts +4 -1
  37. package/src/ai/chat-task-reasons-migrate.ts +7 -0
  38. package/src/ai/chat-task-reasons.ts +1 -1
  39. package/src/ai/chat-tasks.ts +55 -25
  40. package/src/ai/client/controller.ts +59 -0
  41. package/src/ai/default-tools.ts +9 -1
  42. package/src/ai/executor.ts +198 -39
  43. package/src/ai/firecrawl-tools.ts +15 -2
  44. package/src/ai/index.ts +3 -0
  45. package/src/ai/migrate.ts +26 -2
  46. package/src/ai/open-questions.ts +172 -0
  47. package/src/ai/projects-routes.ts +3 -0
  48. package/src/ai/protocol.ts +14 -2
  49. package/src/ai/question-text.ts +8 -0
  50. package/src/ai/recent-resources.ts +240 -0
  51. package/src/ai/recent-sent-refs.ts +38 -0
  52. package/src/ai/routes.ts +195 -5
  53. package/src/ai/runtime.ts +1 -0
  54. package/src/ai/skills-routes.ts +3 -0
  55. package/src/ai/skills.ts +10 -1
  56. package/src/ai/store.ts +245 -24
  57. package/src/ai/testing.ts +36 -0
  58. package/src/ai/tool-audit.ts +8 -0
  59. package/src/ai/tool-sentences.ts +152 -34
  60. package/src/ai/tool-wording.ts +36 -8
  61. package/src/ai/tools.ts +8 -3
  62. package/src/ai/types.ts +43 -4
  63. package/src/api/accounts-entities.ts +2 -1
  64. package/src/api/admin-ai-projects.ts +2 -0
  65. package/src/api/admin-ai-skills.ts +2 -0
  66. package/src/api/admin-ai-usage.ts +2 -0
  67. package/src/api/admin-geoip.ts +80 -0
  68. package/src/api/admin-identity.ts +3 -0
  69. package/src/api/admin-lifecycle.ts +3 -0
  70. package/src/api/admin-outgoing-mail.ts +2 -0
  71. package/src/api/app-approval-actions.ts +31 -7
  72. package/src/api/app-approval.ts +46 -5
  73. package/src/api/approvals-loader.ts +23 -1
  74. package/src/api/approvals.ts +2 -0
  75. package/src/api/auth/schemas.ts +3 -0
  76. package/src/api/auth.ts +67 -12
  77. package/src/api/capabilities.ts +44 -11
  78. package/src/api/capability-streams.ts +12 -0
  79. package/src/api/help.ts +4 -28
  80. package/src/api/index.ts +6 -1
  81. package/src/api/lasting-access-messages.ts +17 -0
  82. package/src/api/mcp.ts +30 -3
  83. package/src/api/me-panel-consent.ts +71 -0
  84. package/src/api/me.ts +159 -36
  85. package/src/api/search.ts +3 -1
  86. package/src/api/sync-ops.ts +2 -0
  87. package/src/api/widgets.ts +3 -0
  88. package/src/approvals/Approvals.tsx +166 -262
  89. package/src/approvals/approval-card.ts +68 -0
  90. package/src/approvals/messages.ts +12 -24
  91. package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
  92. package/src/browser/app-approval.ts +54 -5
  93. package/src/browser/commands.ts +22 -6
  94. package/src/browser/composer-catalog.ts +45 -3
  95. package/src/browser/composer-insert.ts +10 -0
  96. package/src/browser/composer-open-messages.ts +17 -0
  97. package/src/browser/composer-open.ts +110 -0
  98. package/src/browser/discoverable-command.ts +6 -0
  99. package/src/browser/hints.ts +6 -0
  100. package/src/browser/live-connection.ts +26 -2
  101. package/src/browser/navigation-apps.ts +7 -0
  102. package/src/browser/panel-context-bridge.ts +30 -0
  103. package/src/browser/panel-context-receiver.ts +53 -0
  104. package/src/browser/panel-menu.tsx +56 -0
  105. package/src/browser/panel-messages.ts +11 -0
  106. package/src/browser/panel-shortcuts.ts +31 -0
  107. package/src/browser/panel-slot.ts +412 -10
  108. package/src/browser/panel.ts +37 -0
  109. package/src/browser/search-commands.ts +6 -3
  110. package/src/capabilities/client.ts +8 -2
  111. package/src/capabilities/reviews.ts +205 -0
  112. package/src/capabilities/server.ts +10 -3
  113. package/src/capabilities/testing.ts +47 -0
  114. package/src/capabilities/types.ts +2 -2
  115. package/src/cli/account.ts +13 -3
  116. package/src/cli/admin/geoip.ts +115 -0
  117. package/src/cli/admin/index.ts +3 -0
  118. package/src/contracts/app-approval.ts +123 -16
  119. package/src/contracts/approvals.ts +2 -2
  120. package/src/contracts/capabilities.ts +410 -18
  121. package/src/contracts/geoip.ts +42 -0
  122. package/src/contracts/index.ts +3 -0
  123. package/src/contracts/panel-context-plain.ts +99 -0
  124. package/src/contracts/panel-context.ts +93 -0
  125. package/src/contracts/seen-hints.ts +35 -0
  126. package/src/contracts/shared.ts +3 -0
  127. package/src/server/client-ip.ts +5 -0
  128. package/src/server/help-payload.ts +38 -0
  129. package/src/server/middleware/auth.ts +13 -1
  130. package/src/server/middleware/middleware.ts +9 -0
  131. package/src/server/middleware/no-transform.ts +19 -0
  132. package/src/server/middleware/rate-limit.ts +4 -6
  133. package/src/server/request-context/address.ts +45 -0
  134. package/src/server/request-context/geoip.ts +380 -0
  135. package/src/server/request-context/index.ts +33 -0
  136. package/src/server/request-context/user-agent.ts +95 -0
  137. package/src/services/accounts/app.ts +23 -3
  138. package/src/services/accounts/users.ts +12 -1
  139. package/src/services/app-approval.ts +382 -59
  140. package/src/services/approvals/device.ts +49 -31
  141. package/src/services/approvals/index.ts +7 -5
  142. package/src/services/approvals/list.ts +27 -6
  143. package/src/services/approvals/question.ts +68 -0
  144. package/src/services/audit/index.ts +1 -0
  145. package/src/services/auth-flows/ipa.ts +39 -2
  146. package/src/services/auth-flows/notification-sender.ts +3 -0
  147. package/src/services/auth-flows/password-reset.ts +32 -6
  148. package/src/services/geoip/index.ts +164 -0
  149. package/src/services/geoip/store.ts +30 -0
  150. package/src/services/geoip/upload.ts +150 -0
  151. package/src/services/identity/routes.ts +5 -1
  152. package/src/services/index.ts +13 -0
  153. package/src/services/known-browsers.ts +312 -0
  154. package/src/services/login-address-store.ts +51 -0
  155. package/src/services/login-context.ts +118 -0
  156. package/src/services/mandates/index.ts +3 -0
  157. package/src/services/mandates/policy.ts +15 -3
  158. package/src/services/notifications/email-frame.ts +1 -1
  159. package/src/services/oauth-tokens.ts +16 -2
  160. package/src/services/panel-context-consent.ts +58 -0
  161. package/src/services/pdf/markdown.ts +160 -2
  162. package/src/services/service-account-credentials.ts +21 -0
  163. package/src/services/session/index.ts +296 -41
  164. package/src/services/session/recent.ts +13 -0
  165. package/src/services/webauthn.ts +12 -0
  166. package/src/shared/email-html.ts +2 -0
  167. package/src/shared/help.ts +3 -0
  168. package/src/shared-text/cursor.ts +22 -0
  169. package/src/shared-text/documents.ts +142 -0
  170. package/src/shared-text/events.ts +43 -0
  171. package/src/shared-text/index.ts +12 -0
  172. package/src/shared-text/log.ts +48 -0
  173. package/src/shared-text/text-diff.ts +23 -0
  174. package/src/shared-text/types.ts +41 -0
  175. package/src/ssr/AdminLayout.tsx +2 -0
  176. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  177. package/src/ssr/CloudShell.island.tsx +10 -2
  178. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  179. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  180. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  181. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  182. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  183. package/src/ssr/HelpPanelView.island.tsx +27 -0
  184. package/src/ssr/Layout.tsx +25 -2
  185. package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
  186. package/src/ssr/LayoutHelp.tsx +222 -419
  187. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
  188. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  189. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  190. package/src/ssr/LayoutHelpTrigger.tsx +27 -2
  191. package/src/ssr/MobileNavigation.tsx +2 -2
  192. package/src/ssr/PanelLayout.tsx +9 -1
  193. package/src/ssr/PanelRuntime.island.tsx +7 -0
  194. package/src/ssr/PanelSlot.tsx +53 -3
  195. package/src/ssr/ShellRuntime.island.tsx +11 -2
  196. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  197. package/src/ssr/app-color.ts +14 -9
  198. package/src/ssr/app-navigation.ts +16 -3
  199. package/src/ssr/compact-shell.browser-harness.ts +30 -22
  200. package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
  201. package/src/ssr/help-messages.ts +4 -0
  202. package/src/ssr/help.ts +21 -0
  203. package/src/ssr/hints.ts +25 -0
  204. package/src/ssr/index.ts +3 -0
  205. package/src/ssr/layout-context.ts +2 -0
  206. package/src/ssr/layout-help-article.server.ts +50 -0
  207. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  208. package/src/ssr/layout-help-panel-document.ts +67 -0
  209. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  210. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  211. package/src/ssr/layout-help-panel-modules.ts +7 -0
  212. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  213. package/src/ssr/layout-help-panel-search.tsx +69 -0
  214. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  215. package/src/ssr/layout-help-panel-state.ts +141 -0
  216. package/src/ssr/layout-help-panel-types.ts +19 -0
  217. package/src/ssr/layout-help-start.ts +33 -7
  218. package/src/ssr/layout-help-views.tsx +484 -0
  219. package/src/ssr/panel-consent.ts +29 -0
  220. package/src/ssr/panel-context.ts +10 -0
  221. package/src/ssr/panel-idle.ts +9 -0
  222. package/src/ssr/panel-slot.browser-harness.tsx +53 -4
  223. package/src/ssr/platform-messages.ts +18 -0
  224. package/src/ssr/shell-data.tsx +2 -1
  225. package/src/styles/approvals.css +6 -0
  226. package/src/styles/effects.css +213 -9
  227. package/src/styles/file-chooser.css +2 -1
  228. package/src/styles/global.css +49 -12
  229. package/src/styles/panel.css +47 -4
  230. package/src/styles/resource-search.css +6 -2
  231. package/src/styles/tokens.css +17 -11
  232. package/src/styles/utilities-buttons.css +3 -2
  233. package/src/styles/utilities-data.css +2 -1
  234. package/src/styles/utilities-feedback.css +2 -2
  235. package/src/styles/utilities-layout.css +3 -13
  236. package/src/styles/utilities-markdown-editor.css +6 -6
  237. package/src/styles/utilities-navigation.css +17 -22
package/README.md CHANGED
@@ -83,6 +83,14 @@ Application setup, public contracts, and deployment guidance:
83
83
  - **[Create the first application](https://github.com/k2b-dev/cloud/blob/main/docs-site/docs/en/build/getting-started.md)** — standalone project setup and a verified API endpoint
84
84
  - **[github.com/k2b-dev/cloud](https://github.com/k2b-dev/cloud)** — the platform monorepo (gateway, core, all platform apps)
85
85
 
86
+ ## Security
87
+
88
+ Report a vulnerability privately through
89
+ [GitHub Security Advisories](https://github.com/k2b-dev/cloud/security/advisories/new),
90
+ never in a public issue. Security fixes appear in this package's changelog,
91
+ and a published advisory names the first patched version. See the
92
+ [security policy](https://github.com/k2b-dev/cloud/blob/main/SECURITY.md).
93
+
86
94
  ## License
87
95
 
88
96
  GNU Affero General Public License v3.0 or later.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k2b/cloud",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "description": "Application platform library for independently deployed Hono and SolidJS services behind a dynamic gateway.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "repository": {
@@ -34,6 +34,7 @@
34
34
  "./browser/app-badges": "./src/browser/app-badges.ts",
35
35
  "./browser/live": "./src/browser/live.ts",
36
36
  "./browser/mermaid": "./src/browser/mermaid.ts",
37
+ "./browser/hints": "./src/browser/hints.ts",
37
38
  "./browser/reload": "./src/browser/reload.ts",
38
39
  "./browser/notifications": "./src/browser/notifications.ts",
39
40
  "./browser/resource-clipboard": "./src/browser/resource-clipboard.ts",
@@ -56,6 +57,7 @@
56
57
  "./cli/capabilities": "./src/cli/capabilities.ts",
57
58
  "./cli/admin": "./src/cli/admin/index.ts",
58
59
  "./shared": "./src/shared/index.ts",
60
+ "./shared-text": "./src/shared-text/index.ts",
59
61
  "./services": "./src/services/index.ts",
60
62
  "./services/image-metadata": "./src/services/image-metadata.ts",
61
63
  "./services/document-extraction": "./src/services/document-extraction.ts",
@@ -65,10 +67,12 @@
65
67
  "./ai": "./src/ai/index.ts",
66
68
  "./ai/admin": "./src/ai/admin.ts",
67
69
  "./ai/browser": "./src/ai/browser.ts",
70
+ "./ai/question-text": "./src/ai/question-text.ts",
68
71
  "./ai/live": "./src/ai/live.ts",
69
72
  "./ai/live-events": "./src/ai/live-events.ts",
70
73
  "./ai/runtime": "./src/ai/runtime.ts",
71
74
  "./ai/solid": "./src/ai/solid.ts",
75
+ "./ai/testing": "./src/ai/testing.ts",
72
76
  "./ai/tools": "./src/ai/runtime-tools.ts",
73
77
  "./ai/ui": "./src/ai/ui.tsx",
74
78
  "./account/ui": "./src/account/ui.ts",
@@ -110,21 +114,23 @@
110
114
  "@tabler/icons": "3.48.0",
111
115
  "@tailwindcss/typography": "0.5.20",
112
116
  "@k2b/nessi": "0.18.0",
113
- "@k2b/ssr": "0.17.0",
114
- "@k2b/ui": "0.29.0",
115
- "@k2b/stdlib": "0.29.0",
117
+ "@k2b/ssr": "0.19.0",
118
+ "@k2b/ui": "0.31.0",
119
+ "@k2b/stdlib": "0.30.0",
116
120
  "@k2b/sync": "7.0.0",
117
121
  "@nats-io/jetstream": "3.4.0",
118
122
  "@nats-io/transport-node": "3.4.0",
119
123
  "@modelcontextprotocol/sdk": "1.32.1",
120
124
  "bun-plugin-tailwind": "0.1.2",
121
125
  "hono-openapi": "1.3.3",
126
+ "entities": "8.0.0",
122
127
  "htmlparser2": "12.0.0",
123
128
  "jose": "6.2.12",
124
129
  "katex": "0.18.9",
125
130
  "liquidjs": "10.29.0",
126
131
  "marked": "18.0.14",
127
132
  "mermaid": "12.0.0",
133
+ "mmdb-lib": "3.0.3",
128
134
  "nodemailer": "10.0.10",
129
135
  "postcss": "8.5.28",
130
136
  "reflect-metadata": "0.2.2",
@@ -134,6 +140,7 @@
134
140
  "web-push": "3.6.7",
135
141
  "web-vitals": "6.2.2",
136
142
  "yaml": "2.9.1",
143
+ "yjs": "13.6.33",
137
144
  "hash-wasm": "4.12.0",
138
145
  "@types/sanitize-html": "2.16.1",
139
146
  "@types/web-push": "3.6.4"
@@ -1,2 +1,4 @@
1
- export const appFaviconHref = (appId: string, version: number): string =>
2
- appId === "core" ? "/branding/favicon" : `/public/${appId}/favicon.svg?v=${version}`;
1
+ export const appFaviconHref = (appId: string, version: string | number | undefined): string =>
2
+ appId === "core"
3
+ ? "/branding/favicon"
4
+ : `/public/${appId}/favicon.svg${version === undefined ? "" : `?v=${encodeURIComponent(version)}`}`;
@@ -1,4 +1,7 @@
1
- import { createHash } from "node:crypto";
1
+ import { capabilityHash, projectSchema, stableJsonValue } from "./capability-schemas";
2
+
3
+ export { capabilityHash } from "./capability-schemas";
4
+
2
5
  import { isServiceError, type Result, type ServiceError } from "@k2b/stdlib";
3
6
  import { z } from "zod";
4
7
  import {
@@ -8,8 +11,8 @@ import {
8
11
  CAPABILITY_PROTOCOL_VERSION,
9
12
  type CapabilityActionDefinition,
10
13
  type CapabilityActionManifest,
14
+ CapabilityActionPolicySchema,
11
15
  type CapabilityActionReviewResult,
12
- CapabilityActionReviewSchema,
13
16
  type CapabilityActionSentences,
14
17
  type CapabilityCommandDefinition,
15
18
  type CapabilityCommandManifest,
@@ -33,12 +36,14 @@ import {
33
36
  type CapabilitySearchTagDefinition,
34
37
  type CapabilityStreamDefinition,
35
38
  type CapabilityUniversalSearchDefinition,
39
+ capabilityActionPolicy,
36
40
  capabilityResultSchema,
37
41
  UniversalSearchDataSchema,
38
42
  UniversalSearchInputSchema,
39
43
  } from "../contracts/capabilities";
40
44
  import { fileProviderIssues } from "../contracts/file-provider";
41
45
  import { canonicalLocale, localeFallbackChain, normalizeLocale } from "../shared/locale";
46
+ import { capabilityActionReceiptIssues, capabilityActionReviewIssues, capabilityQueryChecksActionOutcome } from "./capability-reviews";
42
47
  import {
43
48
  CAPABILITY_SENTENCE_KEYS,
44
49
  CAPABILITY_SENTENCE_MAX_CHARS,
@@ -94,6 +99,7 @@ const ACTION_FIELDS = {
94
99
  stream: true,
95
100
  idempotency: true,
96
101
  approval: true,
102
+ policy: true,
97
103
  review: true,
98
104
  run: true,
99
105
  } satisfies DefinedFields<CapabilityActionDefinition>;
@@ -221,21 +227,6 @@ export const serializeCapabilityProviderResult = (
221
227
  return { body: JSON.stringify(fallback), status: options.nonIdempotentAction ? 502 : 500 };
222
228
  };
223
229
 
224
- const stableJsonValue = (value: unknown): unknown => {
225
- if (Array.isArray(value)) return value.map(stableJsonValue);
226
- if (typeof value !== "object" || value === null) return value;
227
- return Object.fromEntries(
228
- Object.entries(value as Record<string, unknown>)
229
- .sort(([left], [right]) => left.localeCompare(right))
230
- .map(([key, entry]) => [key, stableJsonValue(entry)]),
231
- );
232
- };
233
-
234
- export const capabilityHash = (value: unknown): string =>
235
- createHash("sha256")
236
- .update(JSON.stringify(stableJsonValue(value)))
237
- .digest("hex");
238
-
239
230
  const assertText = (value: string, label: string, max: number): void => {
240
231
  if (!value.trim()) throw new Error(`${label} is required`);
241
232
  if (value.length > max) throw new Error(`${label} must be at most ${max} characters`);
@@ -338,12 +329,12 @@ const compileOperationPresentation = (
338
329
  operation: CapabilityQueryManifest | CapabilityActionManifest | CapabilityCommandManifest,
339
330
  label: string,
340
331
  strict = true,
341
- ): CapabilityOperationPresentationTranslation => {
332
+ ): CapabilityOperationPresentationTranslation & { keywords?: readonly string[] } => {
342
333
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
343
334
  const translation = value as Record<string, unknown>;
344
335
  const allowed = new Set(
345
336
  "path" in operation
346
- ? ["title", "description", "input"]
337
+ ? ["title", "description", "input", "keywords"]
347
338
  : ["title", "description", "input", "data", "searchTags", ...(isAction(operation) ? ["sentences"] : [])],
348
339
  );
349
340
  const extra = Object.keys(translation).find((key) => !allowed.has(key));
@@ -356,6 +347,10 @@ const compileOperationPresentation = (
356
347
  if (typeof translation.description !== "string") throw new Error(`${label}.description must be text`);
357
348
  assertText(translation.description, `${label}.description`, 1000);
358
349
  }
350
+ const keywords =
351
+ "path" in operation && translation.keywords !== undefined
352
+ ? CapabilityCommandManifestSchema.shape.keywords.parse(translation.keywords)
353
+ : undefined;
359
354
  let searchTags: CapabilityOperationPresentationTranslation["searchTags"];
360
355
  const universalSearch = "universalSearch" in operation ? operation.universalSearch : undefined;
361
356
  // A reader skips search-tag copy for an operation without Universal Search instead of losing its other copy.
@@ -398,6 +393,7 @@ const compileOperationPresentation = (
398
393
  return {
399
394
  ...(typeof translation.title === "string" ? { title: translation.title.trim() } : {}),
400
395
  ...(typeof translation.description === "string" ? { description: translation.description.trim() } : {}),
396
+ ...(keywords ? { keywords } : {}),
401
397
  ...(input ? { input } : {}),
402
398
  ...(data ? { data } : {}),
403
399
  ...(searchTags ? { searchTags } : {}),
@@ -517,17 +513,6 @@ export const compileCapabilityPresentation = (
517
513
  return { baseLocale, ...(sentences && Object.keys(sentences).length > 0 ? { sentences } : {}), translations };
518
514
  };
519
515
 
520
- const projectSchema = (schema: z.ZodType, label: string, io: "input" | "output"): JsonSchema => {
521
- try {
522
- const projected = z.toJSONSchema(schema, { io }) as JsonSchema;
523
- z.fromJSONSchema(projected);
524
- return projected;
525
- } catch (error) {
526
- const message = error instanceof Error ? error.message : String(error);
527
- throw new Error(`${label} is not JSON-Schema-round-trippable: ${message}`);
528
- }
529
- };
530
-
531
516
  const assertPropertyDescriptions = (schema: unknown, label: string, path: readonly string[] = []): void => {
532
517
  if (!schema || typeof schema !== "object" || Array.isArray(schema)) return;
533
518
  const value = schema as Record<string, unknown>;
@@ -720,6 +705,20 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
720
705
  const label = `Action ${localId}`;
721
706
  assertDefinedFields(definition, ACTION_FIELDS, label);
722
707
  assertDefinedStreamFields(definition.stream, label);
708
+ if (definition.policy !== undefined) {
709
+ const parsedPolicy = CapabilityActionPolicySchema.safeParse(definition.policy);
710
+ if (!parsedPolicy.success) throw new Error(`${label} has invalid policy: ${parsedPolicy.error.message}`);
711
+ if (definition.approval !== undefined) throw new Error(`${label} policy and approval are mutually exclusive`);
712
+ if (!definition.review) throw new Error(`${label} policy requires review()`);
713
+ if ("silent" in parsedPolicy.data && (definition.destructive || definition.openWorld))
714
+ throw new Error(`${label} destructive or open-world policy must ask`);
715
+ if (
716
+ "ask" in parsedPolicy.data &&
717
+ (parsedPolicy.data.ask === "sends" || parsedPolicy.data.ask === "deletes") &&
718
+ definition.idempotency !== "required"
719
+ )
720
+ throw new Error(`${label} sends or deletes policy requires idempotency: "required"`);
721
+ }
723
722
  // Declaration rule, documented in docs/platform/capabilities.md:
724
723
  // destructive means irreversible or externally visible. An irreversible or
725
724
  // open-world effect must always be reviewable and safely repeatable, and
@@ -767,7 +766,13 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
767
766
  destructive: definition.destructive,
768
767
  openWorld: definition.openWorld,
769
768
  idempotency: definition.idempotency,
770
- ...(definition.approval ? { approval: definition.approval } : {}),
769
+ ...(definition.policy
770
+ ? "silent" in definition.policy
771
+ ? { approval: "rememberable" as const }
772
+ : {}
773
+ : definition.approval
774
+ ? { approval: definition.approval }
775
+ : {}),
771
776
  ...(definition.review ? { review: true as const } : {}),
772
777
  } satisfies CapabilityActionManifest;
773
778
  actions.set(localId, {
@@ -795,6 +800,9 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
795
800
  });
796
801
  });
797
802
 
803
+ const policies = Object.fromEntries(
804
+ [...actions].flatMap(([localId, action]) => (action.definition.policy ? [[localId, action.definition.policy]] : [])),
805
+ );
798
806
  const fileProvider = definitions.fileProvider;
799
807
  const manifestBase = {
800
808
  protocolVersion: CAPABILITY_PROTOCOL_VERSION,
@@ -803,6 +811,7 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
803
811
  queries: [...queries.values()].map((entry) => entry.manifest),
804
812
  actions: [...actions.values()].map((entry) => entry.manifest),
805
813
  commands,
814
+ ...(Object.keys(policies).length ? { policies } : {}),
806
815
  // Omitted when unused so manifests without a provider keep their earlier hash.
807
816
  ...(fileProvider
808
817
  ? { fileProvider: { list: fileProvider.list, read: fileProvider.read, ...(fileProvider.save ? { save: fileProvider.save } : {}) } }
@@ -954,6 +963,7 @@ export const resolveCapabilityManifestPresentation = (
954
963
  title: copy.title ?? operation.title,
955
964
  description: copy.description ?? operation.description,
956
965
  inputSchema: applySchemaPresentation(operation.inputSchema, copy.input),
966
+ keywords: copy.keywords ? [...copy.keywords] : operation.keywords,
957
967
  }
958
968
  : operation;
959
969
  }),
@@ -970,6 +980,7 @@ export const resolveCapabilityManifestPresentation = (
970
980
  : operation;
971
981
  }),
972
982
  ...(current.fileProvider ? { fileProvider: current.fileProvider } : {}),
983
+ ...(current.policies ? { policies: current.policies } : {}),
973
984
  };
974
985
  current = CapabilityManifestSchema.parse({ ...manifestBase, manifestHash: capabilityHash(manifestBase) });
975
986
  }
@@ -1213,6 +1224,15 @@ const resultRefs = (result: unknown): Array<{ type: string; id: string }> => {
1213
1224
  }>;
1214
1225
  };
1215
1226
 
1227
+ const compiledReviewContext = (compiled: CompiledCapabilities, localId: string) => ({
1228
+ policy: capabilityActionPolicy(compiled.manifest, localId),
1229
+ approval: compiled.actions.get(localId)?.manifest.approval,
1230
+ queries: compiled.manifest.queries,
1231
+ validateQueryInput: (query: CapabilityQueryManifest, input: Record<string, unknown>) =>
1232
+ compiled.queries.get(query.localId)?.definition.input.safeParse(input).success === true,
1233
+ ...(compiled.presentation ? { locales: [compiled.presentation.baseLocale, ...Object.keys(compiled.presentation.translations)] } : {}),
1234
+ });
1235
+
1216
1236
  export const validateCapabilityResult = (
1217
1237
  compiled: CompiledCapabilities,
1218
1238
  operation: CompiledCapabilityQuery | CompiledCapabilityAction,
@@ -1220,12 +1240,25 @@ export const validateCapabilityResult = (
1220
1240
  onInvalidResult?: (error: unknown) => void,
1221
1241
  ): Result<unknown, ServiceError> => {
1222
1242
  const parsed = operation.resultSchema.safeParse(result);
1223
- if (!parsed.success) {
1243
+ const field = (value: unknown, key: string): unknown =>
1244
+ value !== null && typeof value === "object" ? Object.getOwnPropertyDescriptor(value, key)?.value : undefined;
1245
+ const receipt = field(result, "receipt");
1246
+ const links = field(result, "links");
1247
+ const receiptIssues =
1248
+ receipt === undefined
1249
+ ? []
1250
+ : "idempotency" in operation.manifest
1251
+ ? capabilityActionReceiptIssues(receipt, links)
1252
+ : ["Queries cannot return a receipt"];
1253
+ const nestedReceipt = field(field(result, "data"), "receipt");
1254
+ if (!("idempotency" in operation.manifest) && nestedReceipt !== undefined && capabilityQueryChecksActionOutcome(operation.manifest))
1255
+ receiptIssues.push(...capabilityActionReceiptIssues(nestedReceipt, links));
1256
+ if (!parsed.success || receiptIssues.length > 0) {
1224
1257
  try {
1225
1258
  onInvalidResult?.(
1226
1259
  new Error(
1227
1260
  `Capability ${operation.manifest.localId} returned data outside its registered schema: ${JSON.stringify(
1228
- parsed.error.issues.map(({ code, path, message }) => ({ code, path, message })),
1261
+ parsed.success ? receiptIssues : parsed.error.issues.map(({ code, path, message }) => ({ code, path, message })),
1229
1262
  )}`,
1230
1263
  ),
1231
1264
  );
@@ -1468,27 +1501,18 @@ export const reviewCompiledCapability = async (params: {
1468
1501
  if (!reviewed.ok) {
1469
1502
  return normalizeProviderError(reviewed.error, "Capability review returned an invalid error", params.onUnexpectedError);
1470
1503
  }
1471
- const parsed = CapabilityActionReviewSchema.safeParse(reviewed.data);
1472
- // A rememberable Action may leave out its scope for arguments that should ask every time; any other Action has none.
1473
- const approvalScopeIsValid =
1474
- parsed.success && (operation.manifest.approval === "rememberable" || parsed.data.approvalScope === undefined);
1475
- if (!parsed.success || !approvalScopeIsValid) {
1504
+ const issues = capabilityActionReviewIssues(reviewed.data, compiledReviewContext(params.compiled, params.localId));
1505
+ if (issues.length > 0) {
1476
1506
  try {
1477
1507
  params.onUnexpectedError?.(
1478
- new Error(
1479
- parsed.success
1480
- ? `Capability ${operation.manifest.localId} review returned an approval scope inconsistent with its manifest`
1481
- : `Capability ${operation.manifest.localId} review returned data outside its registered schema: ${JSON.stringify(
1482
- parsed.error.issues.map(({ code, path, message }) => ({ code, path, message })),
1483
- )}`,
1484
- ),
1508
+ new Error(`Capability ${operation.manifest.localId} review returned an invalid result: ${issues.join("; ")}`),
1485
1509
  );
1486
1510
  } catch {
1487
- // Observability must not change the public failure contract.
1511
+ /* Observability must not change the public failure contract. */
1488
1512
  }
1489
1513
  }
1490
- return parsed.success && approvalScopeIsValid
1491
- ? { ok: true, data: parsed.data }
1514
+ return issues.length === 0
1515
+ ? { ok: true, data: reviewed.data }
1492
1516
  : {
1493
1517
  ok: false,
1494
1518
  error: {
@@ -0,0 +1,81 @@
1
+ import { z } from "zod";
2
+ import { capabilityPreviewIssues, capabilityReviewFormIssues } from "../capabilities/reviews";
3
+ import {
4
+ CAPABILITY_REACHES,
5
+ CapabilityActionCheckSchema,
6
+ type CapabilityActionPolicy,
7
+ CapabilityActionReceiptSchema,
8
+ CapabilityActionReviewDataSchema,
9
+ type CapabilityQueryManifest,
10
+ CapabilitySemanticLinkSchema,
11
+ } from "../contracts/capabilities";
12
+ import { localeFallbackChain } from "../shared/locale";
13
+ import { capabilityHash, projectSchema } from "./capability-schemas";
14
+
15
+ export {
16
+ actionReview,
17
+ type CapabilityReviewForms,
18
+ capabilityReviewFormIssues,
19
+ capabilityReviewRememberScope,
20
+ capabilityReviewView,
21
+ type SlotNames,
22
+ } from "../capabilities/reviews";
23
+
24
+ export { CAPABILITY_MAX_REVIEW_BYTES } from "../contracts/capabilities";
25
+ export type CapabilityReviewContext = {
26
+ policy?: CapabilityActionPolicy;
27
+ approval?: "rememberable" | "none";
28
+ queries: readonly CapabilityQueryManifest[];
29
+ validateQueryInput: (query: CapabilityQueryManifest, input: Record<string, unknown>) => boolean;
30
+ locales?: readonly string[];
31
+ };
32
+ const schemaIssues = (error: z.ZodError): string[] => error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
33
+ let actionCheckDataHash: string | undefined;
34
+ /** Whether a Query answers an Action's `check`: its data schema is exactly the projected `CapabilityActionCheckSchema`. */
35
+ export const capabilityQueryChecksActionOutcome = (query: CapabilityQueryManifest): boolean => {
36
+ actionCheckDataHash ??= capabilityHash(projectSchema(CapabilityActionCheckSchema, "Action check", "output"));
37
+ return capabilityHash(query.dataSchema) === actionCheckDataHash;
38
+ };
39
+
40
+ export const capabilityActionReviewIssues = (review: unknown, context: CapabilityReviewContext): string[] => {
41
+ const parsed = CapabilityActionReviewDataSchema.safeParse(review);
42
+ if (!parsed.success) return schemaIssues(parsed.error);
43
+ const data = parsed.data;
44
+ if (!("sentence" in data))
45
+ return context.policy
46
+ ? ["A policy Action must return a new review"]
47
+ : data.approvalScope !== undefined && context.approval !== "rememberable"
48
+ ? ["approvalScope requires rememberable approval"]
49
+ : [];
50
+ if (!context.policy) return ["An Action without policy must return a legacy review"];
51
+ const issues = capabilityReviewFormIssues(data);
52
+ if (
53
+ data.reach !== undefined &&
54
+ (!("silent" in context.policy) || CAPABILITY_REACHES.indexOf(data.reach) > CAPABILITY_REACHES.indexOf(context.policy.silent))
55
+ )
56
+ issues.push("reach exceeds the declared silent reach");
57
+ if (context.locales && !localeFallbackChain(data.locale, data.locale).some((locale) => context.locales?.includes(locale)))
58
+ issues.push("locale is not supported by the app");
59
+ if (data.check) {
60
+ const query = context.queries.find((query) => query.localId === data.check?.query);
61
+ if (!query || !capabilityQueryChecksActionOutcome(query)) issues.push("check.query must return CapabilityActionCheckSchema");
62
+ else if (!context.validateQueryInput(query, data.check.input)) issues.push("check.input does not match the Query");
63
+ }
64
+ issues.push(
65
+ ...capabilityPreviewIssues(
66
+ data.preview,
67
+ data.links,
68
+ // A per-call reason never lifts the declared `sends` completeness rule.
69
+ data.ask === "sends" || ("ask" in context.policy && context.policy.ask === "sends"),
70
+ ),
71
+ );
72
+ return [...new Set(issues)];
73
+ };
74
+ /** A receipt's own rules; completeness links are the result envelope's top-level `links`. */
75
+ export const capabilityActionReceiptIssues = (receipt: unknown, links: unknown): string[] => {
76
+ const parsed = CapabilityActionReceiptSchema.safeParse(receipt);
77
+ const resultLinks = z.array(CapabilitySemanticLinkSchema).safeParse(links);
78
+ return parsed.success
79
+ ? capabilityPreviewIssues(parsed.data.preview, resultLinks.success ? resultLinks.data : undefined)
80
+ : schemaIssues(parsed.error);
81
+ };
@@ -0,0 +1,29 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+
4
+ type JsonSchema = Record<string, unknown>;
5
+ export const stableJsonValue = (value: unknown): unknown => {
6
+ if (Array.isArray(value)) return value.map(stableJsonValue);
7
+ if (typeof value !== "object" || value === null) return value;
8
+ return Object.fromEntries(
9
+ Object.entries(value)
10
+ .sort(([left], [right]) => left.localeCompare(right))
11
+ .map(([key, entry]) => [key, stableJsonValue(entry)]),
12
+ );
13
+ };
14
+
15
+ export const capabilityHash = (value: unknown): string =>
16
+ createHash("sha256")
17
+ .update(JSON.stringify(stableJsonValue(value)))
18
+ .digest("hex");
19
+
20
+ export const projectSchema = (schema: z.ZodType, label: string, io: "input" | "output"): JsonSchema => {
21
+ try {
22
+ const projected = z.toJSONSchema(schema, { io });
23
+ z.fromJSONSchema(projected);
24
+ return projected;
25
+ } catch (error) {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ throw new Error(`${label} is not JSON-Schema-round-trippable: ${message}`);
28
+ }
29
+ };