@k2b/cloud 0.40.0 → 0.42.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 (279) hide show
  1. package/package.json +9 -5
  2. package/src/_internal/bounded-json.ts +14 -2
  3. package/src/_internal/capabilities.ts +87 -8
  4. package/src/_internal/capability-reviews.ts +6 -0
  5. package/src/_internal/capability-sentences.ts +2 -1
  6. package/src/_internal/capability-timeout.ts +71 -0
  7. package/src/_internal/define-app.ts +158 -92
  8. package/src/_internal/heartbeat.ts +203 -14
  9. package/src/_internal/http-drain.ts +56 -0
  10. package/src/_internal/registry-validation.ts +3 -0
  11. package/src/_internal/registry.ts +28 -1
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/_internal/setup-lock.ts +103 -0
  14. package/src/_internal/status-preserving-ssr.ts +2 -0
  15. package/src/access/PermissionEditor.tsx +16 -17
  16. package/src/access/PrincipalPicker.tsx +45 -10
  17. package/src/access/messages.ts +8 -0
  18. package/src/access/principal-label.ts +18 -0
  19. package/src/access/ui.ts +1 -0
  20. package/src/account/EntitySearch.tsx +5 -4
  21. package/src/ai/action-review.ts +2 -1
  22. package/src/ai/app-skill-store.ts +1 -1
  23. package/src/ai/capabilities.ts +21 -3
  24. package/src/ai/capability-execution.ts +14 -12
  25. package/src/ai/chat/action-card.ts +76 -4
  26. package/src/ai/chat/action-messages.ts +14 -0
  27. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  28. package/src/ai/chat/action-question.tsx +214 -23
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/tool-action-words.ts +31 -0
  31. package/src/ai/chat/tool-action.tsx +1 -26
  32. package/src/ai/chat-tasks.ts +2 -1
  33. package/src/ai/client/controller.ts +150 -32
  34. package/src/ai/client/transport.ts +14 -2
  35. package/src/ai/code-capability-routes.ts +27 -23
  36. package/src/ai/code-runtime-tools.ts +14 -16
  37. package/src/ai/code-source-tools.ts +16 -18
  38. package/src/ai/executor.ts +64 -21
  39. package/src/ai/fetch-file-tool.ts +67 -76
  40. package/src/ai/firecrawl-tools.ts +15 -2
  41. package/src/ai/http.ts +1 -0
  42. package/src/ai/index.ts +1 -0
  43. package/src/ai/migrate.ts +28 -0
  44. package/src/ai/open-questions.ts +168 -0
  45. package/src/ai/projects.ts +1 -2
  46. package/src/ai/protocol.ts +2 -2
  47. package/src/ai/question-text.ts +8 -0
  48. package/src/ai/recent-resources.ts +12 -5
  49. package/src/ai/review-content.ts +77 -0
  50. package/src/ai/routes.ts +119 -35
  51. package/src/ai/runtime.ts +1 -0
  52. package/src/ai/skill-tool.ts +17 -6
  53. package/src/ai/skills-routes.ts +72 -27
  54. package/src/ai/skills.ts +139 -60
  55. package/src/ai/store.ts +184 -31
  56. package/src/ai/testing.ts +36 -0
  57. package/src/ai/tool-sentences.ts +152 -34
  58. package/src/ai/tool-wording.ts +12 -6
  59. package/src/ai/tools.ts +8 -3
  60. package/src/ai/types.ts +25 -4
  61. package/src/ai/usage.ts +2 -2
  62. package/src/api/admin-core-settings.ts +15 -2
  63. package/src/api/admin-geo.ts +54 -0
  64. package/src/api/admin-linux-identities.ts +6 -3
  65. package/src/api/admin-outgoing-mail.ts +29 -15
  66. package/src/api/app-approval-actions.ts +31 -7
  67. package/src/api/approvals-loader.ts +3 -3
  68. package/src/api/approvals.ts +51 -0
  69. package/src/api/apps.ts +4 -2
  70. package/src/api/auth/schemas.ts +3 -0
  71. package/src/api/auth.ts +54 -18
  72. package/src/api/capabilities.ts +258 -181
  73. package/src/api/capability-streams.ts +21 -13
  74. package/src/api/cli-plugins.ts +4 -1
  75. package/src/api/geo-glyphs.ts +11 -0
  76. package/src/api/geo.ts +128 -0
  77. package/src/api/help.ts +12 -4
  78. package/src/api/index.ts +5 -0
  79. package/src/api/mcp.ts +73 -41
  80. package/src/api/me-panel-consent.ts +8 -5
  81. package/src/api/me.ts +79 -3
  82. package/src/api/search.ts +21 -14
  83. package/src/api/sync-ops.ts +19 -13
  84. package/src/api/widgets.ts +42 -18
  85. package/src/approvals/Approvals.tsx +30 -66
  86. package/src/approvals/approval-card.ts +68 -0
  87. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  88. package/src/browser/CloudResourceSearch.tsx +3 -13
  89. package/src/browser/PanelLeaveNotice.tsx +18 -0
  90. package/src/browser/navigation-search.ts +78 -25
  91. package/src/browser/panel-leave.ts +69 -0
  92. package/src/browser/panel-messages.ts +12 -0
  93. package/src/browser/panel-slot.ts +173 -24
  94. package/src/browser/panel.ts +3 -0
  95. package/src/browser/resource-search-messages.ts +2 -0
  96. package/src/capabilities/client.ts +28 -1
  97. package/src/capabilities/executions.ts +22 -3
  98. package/src/capabilities/geo-client.ts +57 -0
  99. package/src/capabilities/response.ts +2 -1
  100. package/src/capabilities/review-storage.ts +24 -0
  101. package/src/capabilities/reviews.ts +17 -3
  102. package/src/capabilities/server.ts +36 -16
  103. package/src/cli/access.ts +15 -23
  104. package/src/cli/admin/linux.ts +13 -2
  105. package/src/cli/admin/outgoing-mail.ts +26 -4
  106. package/src/cli/capabilities.ts +19 -0
  107. package/src/config/env.ts +0 -6
  108. package/src/contracts/app-access.ts +48 -0
  109. package/src/contracts/app-approval.ts +3 -3
  110. package/src/contracts/app.ts +4 -1
  111. package/src/contracts/approvals.ts +2 -2
  112. package/src/contracts/avatar.ts +186 -0
  113. package/src/contracts/capabilities.ts +149 -55
  114. package/src/contracts/geo-admin.ts +127 -0
  115. package/src/contracts/geo-map.ts +19 -0
  116. package/src/contracts/geo-route.ts +168 -0
  117. package/src/contracts/geo.ts +248 -0
  118. package/src/contracts/index.ts +4 -0
  119. package/src/contracts/outgoing-mail.ts +17 -4
  120. package/src/contracts/registry.ts +2 -0
  121. package/src/contracts/shared.ts +3 -0
  122. package/src/events/app-access.ts +68 -0
  123. package/src/events/live-delivery.ts +128 -0
  124. package/src/events/live.ts +14 -130
  125. package/src/geo/CloudMap.tsx +95 -0
  126. package/src/geo/map-messages.ts +17 -0
  127. package/src/geo/ui.ts +1 -0
  128. package/src/index.ts +2 -0
  129. package/src/server/client-ip.ts +36 -0
  130. package/src/server/index.ts +3 -4
  131. package/src/server/middleware/app-access.ts +312 -0
  132. package/src/server/middleware/auth.ts +5 -3
  133. package/src/server/middleware/invocation.ts +11 -0
  134. package/src/server/middleware/rate-limit.ts +4 -4
  135. package/src/server/services/access.ts +13 -9
  136. package/src/server/services/index.ts +0 -2
  137. package/src/server/services/services.ts +0 -2
  138. package/src/services/accounts/app.ts +23 -3
  139. package/src/services/accounts/posix.ts +21 -7
  140. package/src/services/accounts/users.ts +12 -1
  141. package/src/services/allowed-network-fetch.ts +16 -10
  142. package/src/services/app-access-metrics.ts +24 -0
  143. package/src/services/app-access.ts +123 -0
  144. package/src/services/app-discovery.ts +27 -0
  145. package/src/services/approvals/content.ts +40 -0
  146. package/src/services/approvals/device.ts +49 -33
  147. package/src/services/approvals/index.ts +15 -6
  148. package/src/services/approvals/list.ts +6 -11
  149. package/src/services/approvals/question.ts +72 -0
  150. package/src/services/audit/index.ts +1 -0
  151. package/src/services/auth-flows/ipa.ts +39 -2
  152. package/src/services/auth-flows/password-reset.ts +32 -6
  153. package/src/services/downstream-budget.ts +87 -0
  154. package/src/services/geo/admin.ts +262 -0
  155. package/src/services/geo/bucket.ts +67 -0
  156. package/src/services/geo/cache.ts +57 -0
  157. package/src/services/geo/costs.ts +5 -0
  158. package/src/services/geo/errors.ts +41 -0
  159. package/src/services/geo/fixtures/README.md +17 -0
  160. package/src/services/geo/fixtures/along.json +355 -0
  161. package/src/services/geo/fixtures/autocomplete.json +218 -0
  162. package/src/services/geo/fixtures/geocode.json +129 -0
  163. package/src/services/geo/fixtures/places.json +355 -0
  164. package/src/services/geo/fixtures/reverse.json +61 -0
  165. package/src/services/geo/fixtures/routing.json +57 -0
  166. package/src/services/geo/geoapify-route.ts +93 -0
  167. package/src/services/geo/geoapify.ts +281 -0
  168. package/src/services/geo/ledger.ts +122 -0
  169. package/src/services/geo/map.ts +160 -0
  170. package/src/services/geo/migrate.ts +25 -0
  171. package/src/services/geo/polyline.ts +184 -0
  172. package/src/services/geo/route-chain.ts +145 -0
  173. package/src/services/geo/route-costs.ts +9 -0
  174. package/src/services/geo/route-geometry.ts +39 -0
  175. package/src/services/geo/route-plan.ts +172 -0
  176. package/src/services/geo/route-service.ts +433 -0
  177. package/src/services/geo/service.ts +300 -0
  178. package/src/services/geo/settings.ts +49 -0
  179. package/src/services/help/index.ts +16 -7
  180. package/src/services/help/types.ts +6 -1
  181. package/src/services/identity/invocation-actor.ts +20 -2
  182. package/src/services/identity/issue-invocation.ts +67 -0
  183. package/src/services/identity/key-ring.ts +5 -3
  184. package/src/services/identity/runtime-config.ts +23 -0
  185. package/src/services/index.ts +11 -0
  186. package/src/services/mandates/index.ts +40 -3
  187. package/src/services/network-security.ts +6 -3
  188. package/src/services/network-transport.ts +99 -0
  189. package/src/services/notifications/catalog.ts +3 -2
  190. package/src/services/notifications/runtime.ts +1 -0
  191. package/src/services/notifications/web-push-transport.ts +45 -38
  192. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  193. package/src/services/outgoing-mail/index.ts +55 -1
  194. package/src/services/outgoing-mail/messages.ts +82 -3
  195. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  196. package/src/services/outgoing-mail/retention.ts +13 -1
  197. package/src/services/outgoing-mail/store.ts +36 -6
  198. package/src/services/outgoing-mail/test-send.ts +34 -4
  199. package/src/services/outgoing-mail/transport.ts +1 -1
  200. package/src/services/pdf/gotenberg.ts +11 -38
  201. package/src/services/pdf/markdown.ts +160 -2
  202. package/src/services/pdf/template-preview.ts +1 -1
  203. package/src/services/public-http.ts +28 -40
  204. package/src/services/session/index.ts +203 -29
  205. package/src/services/settings/app.ts +4 -1
  206. package/src/services/settings/core-settings.ts +88 -0
  207. package/src/services/settings/defaults.ts +8 -2
  208. package/src/services/settings/errors.ts +11 -0
  209. package/src/services/settings/geo-budget.ts +40 -0
  210. package/src/services/settings/index.ts +4 -3
  211. package/src/services/settings/store.ts +25 -17
  212. package/src/services/weather/geo.ts +29 -111
  213. package/src/services/weather/index.ts +4 -3
  214. package/src/services/weather/locations.ts +8 -5
  215. package/src/services/weather/migrate.ts +1 -0
  216. package/src/services/weather/settings.ts +0 -16
  217. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  218. package/src/shared/avatar-art.ts +400 -0
  219. package/src/shared/index.ts +2 -0
  220. package/src/shared/network-address.ts +22 -3
  221. package/src/shared/template-rendering.ts +36 -4
  222. package/src/shared-text/documents.ts +1 -1
  223. package/src/shared-text/index.ts +2 -0
  224. package/src/shared-text/log.ts +51 -3
  225. package/src/shared-text/snapshots.ts +340 -0
  226. package/src/ssr/AdminLayout.tsx +4 -2
  227. package/src/ssr/CloudShell.island.tsx +6 -0
  228. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  229. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  230. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  231. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  232. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  233. package/src/ssr/HelpPanelView.island.tsx +27 -0
  234. package/src/ssr/Layout.tsx +65 -15
  235. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  236. package/src/ssr/LayoutHelp.tsx +185 -542
  237. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
  238. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  239. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  240. package/src/ssr/PanelRuntime.island.tsx +11 -7
  241. package/src/ssr/PwaLayout.tsx +2 -2
  242. package/src/ssr/RailApps.tsx +6 -0
  243. package/src/ssr/ShellRuntime.island.tsx +23 -7
  244. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  245. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  246. package/src/ssr/admin-navigation.ts +1 -0
  247. package/src/ssr/app-badges.ts +6 -1
  248. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  249. package/src/ssr/help.ts +7 -5
  250. package/src/ssr/index.ts +2 -1
  251. package/src/ssr/islands/index.ts +3 -4
  252. package/src/ssr/layout-context.ts +2 -0
  253. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  254. package/src/ssr/layout-help-panel-document.ts +67 -0
  255. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  256. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  257. package/src/ssr/layout-help-panel-modules.ts +7 -0
  258. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  259. package/src/ssr/layout-help-panel-search.tsx +69 -0
  260. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  261. package/src/ssr/layout-help-panel-state.ts +141 -0
  262. package/src/ssr/layout-help-panel-types.ts +19 -0
  263. package/src/ssr/layout-help-views.tsx +484 -0
  264. package/src/ssr/mobile-menu-history.ts +4 -0
  265. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  266. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  267. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  268. package/src/ssr/page-swap.ts +28 -0
  269. package/src/ssr/page-trail-runtime.ts +32 -13
  270. package/src/ssr/panel-consent.ts +2 -2
  271. package/src/ssr/panel-idle.ts +9 -0
  272. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  273. package/src/ssr/platform-messages.ts +8 -0
  274. package/src/ssr/runtime.ts +17 -0
  275. package/src/ssr/shell-data.tsx +10 -2
  276. package/src/styles/approvals.css +6 -0
  277. package/src/styles/effects.css +15 -0
  278. package/src/styles/global.css +70 -6
  279. package/src/server/services/geo.ts +0 -215
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k2b/cloud",
3
- "version": "0.40.0",
3
+ "version": "0.42.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": {
@@ -29,6 +29,7 @@
29
29
  "./desktop/solid": "./src/desktop/solid.tsx",
30
30
  "./server": "./src/server/index.ts",
31
31
  "./server/*": "./src/server/*",
32
+ "./geo/ui": "./src/geo/ui.ts",
32
33
  "./browser": "./src/server/api-client.ts",
33
34
  "./browser/app-approval": "./src/browser/app-approval.ts",
34
35
  "./browser/app-badges": "./src/browser/app-badges.ts",
@@ -67,10 +68,12 @@
67
68
  "./ai": "./src/ai/index.ts",
68
69
  "./ai/admin": "./src/ai/admin.ts",
69
70
  "./ai/browser": "./src/ai/browser.ts",
71
+ "./ai/question-text": "./src/ai/question-text.ts",
70
72
  "./ai/live": "./src/ai/live.ts",
71
73
  "./ai/live-events": "./src/ai/live-events.ts",
72
74
  "./ai/runtime": "./src/ai/runtime.ts",
73
75
  "./ai/solid": "./src/ai/solid.ts",
76
+ "./ai/testing": "./src/ai/testing.ts",
74
77
  "./ai/tools": "./src/ai/runtime-tools.ts",
75
78
  "./ai/ui": "./src/ai/ui.tsx",
76
79
  "./account/ui": "./src/account/ui.ts",
@@ -107,13 +110,13 @@
107
110
  "@firecrawl/anydoc": "0.2.4",
108
111
  "@napi-rs/canvas": "1.0.9",
109
112
  "pdfjs-dist": "6.3.289",
110
- "sharp": "0.35.4",
113
+ "sharp": "0.35.5",
111
114
  "@simplewebauthn/server": "14.0.3",
112
115
  "@tabler/icons": "3.48.0",
113
116
  "@tailwindcss/typography": "0.5.20",
114
117
  "@k2b/nessi": "0.18.0",
115
- "@k2b/ssr": "0.18.0",
116
- "@k2b/ui": "0.30.0",
118
+ "@k2b/ssr": "0.20.0",
119
+ "@k2b/ui": "0.32.0",
117
120
  "@k2b/stdlib": "0.30.0",
118
121
  "@k2b/sync": "7.0.0",
119
122
  "@nats-io/jetstream": "3.4.0",
@@ -121,6 +124,7 @@
121
124
  "@modelcontextprotocol/sdk": "1.32.1",
122
125
  "bun-plugin-tailwind": "0.1.2",
123
126
  "hono-openapi": "1.3.3",
127
+ "entities": "8.0.0",
124
128
  "htmlparser2": "12.0.0",
125
129
  "jose": "6.2.12",
126
130
  "katex": "0.18.9",
@@ -149,7 +153,7 @@
149
153
  "babel-preset-solid": "1.9.15",
150
154
  "hono": "4.13.9",
151
155
  "playwright": "1.64.1",
152
- "solid-js": "1.9.15",
156
+ "solid-js": "1.9.17",
153
157
  "typescript": "7.0.2",
154
158
  "zod": "4.6.5"
155
159
  },
@@ -1,7 +1,8 @@
1
1
  export type BoundedJsonResult = { ok: true; data: unknown } | { ok: false; reason: "too_large" | "invalid_json" };
2
2
 
3
3
  /** Reads JSON without allowing a missing or forged Content-Length to bypass the byte cap. */
4
- export const readBoundedJson = async (message: Request | Response, maxBytes: number): Promise<BoundedJsonResult> => {
4
+ export const readBoundedJson = async (message: Request | Response, maxBytes: number, signal?: AbortSignal): Promise<BoundedJsonResult> => {
5
+ signal?.throwIfAborted();
5
6
  const declared = message.headers.get("content-length");
6
7
  if (declared !== null) {
7
8
  const bytes = Number(declared);
@@ -13,6 +14,12 @@ export const readBoundedJson = async (message: Request | Response, maxBytes: num
13
14
 
14
15
  const reader = message.body?.getReader();
15
16
  if (!reader) return { ok: false, reason: "invalid_json" };
17
+ const aborted = () => {
18
+ // Releasing the caller must not wait for a stalled source's cancellation hook.
19
+ void reader.cancel(signal?.reason).catch(() => undefined);
20
+ };
21
+ signal?.addEventListener("abort", aborted, { once: true });
22
+ if (signal?.aborted) aborted();
16
23
 
17
24
  const chunks: Uint8Array[] = [];
18
25
  let total = 0;
@@ -28,10 +35,15 @@ export const readBoundedJson = async (message: Request | Response, maxBytes: num
28
35
  chunks.push(next.value);
29
36
  }
30
37
  } catch {
31
- await reader.cancel().catch(() => undefined);
38
+ if (!signal?.aborted) await reader.cancel().catch(() => undefined);
32
39
  return { ok: false, reason: "invalid_json" };
40
+ } finally {
41
+ signal?.removeEventListener("abort", aborted);
42
+ reader.releaseLock();
33
43
  }
34
44
 
45
+ signal?.throwIfAborted();
46
+
35
47
  const bytes = new Uint8Array(total);
36
48
  let offset = 0;
37
49
  for (const chunk of chunks) {
@@ -1,3 +1,4 @@
1
+ import { capabilityMessages } from "../shared/capability-messages";
1
2
  import { capabilityHash, projectSchema, stableJsonValue } from "./capability-schemas";
2
3
 
3
4
  export { capabilityHash } from "./capability-schemas";
@@ -8,6 +9,7 @@ import {
8
9
  CAPABILITY_ERROR_STATUSES,
9
10
  CAPABILITY_FRAMEWORK_ERROR_CODES,
10
11
  CAPABILITY_MAX_RESULT_BYTES,
12
+ CAPABILITY_MAX_REVIEW_RESPONSE_BYTES,
11
13
  CAPABILITY_PROTOCOL_VERSION,
12
14
  type CapabilityActionDefinition,
13
15
  type CapabilityActionManifest,
@@ -193,7 +195,7 @@ const providerPayload = (code: string, message: string, details?: Record<string,
193
195
  /** Serializes one framework-owned provider response exactly once and enforces the public byte bound. */
194
196
  export const serializeCapabilityProviderResult = (
195
197
  result: CapabilityProviderResult,
196
- options: { nonIdempotentAction?: boolean } = {},
198
+ options: { nonIdempotentAction?: boolean; review?: boolean } = {},
197
199
  ): { body: string; status: number } => {
198
200
  if (options.nonIdempotentAction && !result.ok && result.error.code === CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse) {
199
201
  const unknown = providerPayload(
@@ -206,7 +208,9 @@ export const serializeCapabilityProviderResult = (
206
208
  const payload = result.ok ? result.data : providerPayload(result.error.code, result.error.message, result.error.details);
207
209
  try {
208
210
  const body = JSON.stringify(payload);
209
- if (new TextEncoder().encode(body).byteLength <= CAPABILITY_MAX_RESULT_BYTES) {
211
+ if (
212
+ new TextEncoder().encode(body).byteLength <= (options.review ? CAPABILITY_MAX_REVIEW_RESPONSE_BYTES : CAPABILITY_MAX_RESULT_BYTES)
213
+ ) {
210
214
  return { body, status: result.ok ? 200 : result.error.status };
211
215
  }
212
216
  } catch {
@@ -1091,7 +1095,48 @@ const schemaSemantics = (value: unknown): unknown => {
1091
1095
  const sameSchema = (left: unknown, right: unknown): boolean =>
1092
1096
  JSON.stringify(stableJsonValue(schemaSemantics(left))) === JSON.stringify(stableJsonValue(schemaSemantics(right)));
1093
1097
 
1098
+ const isSchemaObject = (value: unknown): value is JsonSchema => typeof value === "object" && value !== null && !Array.isArray(value);
1099
+
1100
+ // Input recursion visits schema nodes explicitly. Never strip annotation names from
1101
+ // property maps or literal enum/const values when proving that old inputs survive.
1102
+ const inputConstraints = (schema: unknown): unknown =>
1103
+ isSchemaObject(schema) ? Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "title" && key !== "description")) : schema;
1104
+ const sameInputSchema = (left: unknown, right: unknown): boolean =>
1105
+ JSON.stringify(stableJsonValue(inputConstraints(left))) === JSON.stringify(stableJsonValue(inputConstraints(right)));
1106
+
1107
+ /** Only finite scalar tags: object/array equality and general schema exclusion are outside this proof. */
1108
+ const discriminatorValues = (schema: unknown): unknown[] | undefined => {
1109
+ if (!isSchemaObject(schema)) return;
1110
+ const values = Object.hasOwn(schema, "const") ? [schema.const] : schema.enum;
1111
+ if (
1112
+ Array.isArray(values) &&
1113
+ values.length > 0 &&
1114
+ values.every(
1115
+ (value) =>
1116
+ value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value)),
1117
+ )
1118
+ )
1119
+ return values;
1120
+ };
1121
+
1122
+ const disjointObjectBranches = (left: unknown, right: unknown): boolean => {
1123
+ if (!isSchemaObject(left) || !isSchemaObject(right) || left.type !== "object" || right.type !== "object") return false;
1124
+ const leftProperties = left.properties;
1125
+ const rightProperties = right.properties;
1126
+ const leftRequired = left.required;
1127
+ const rightRequired = right.required;
1128
+ if (!isSchemaObject(leftProperties) || !isSchemaObject(rightProperties) || !Array.isArray(leftRequired) || !Array.isArray(rightRequired))
1129
+ return false;
1130
+ return leftRequired.some((name) => {
1131
+ if (typeof name !== "string" || !rightRequired.includes(name)) return false;
1132
+ const leftValues = discriminatorValues(leftProperties[name]);
1133
+ const rightValues = discriminatorValues(rightProperties[name]);
1134
+ return Boolean(leftValues && rightValues && leftValues.every((value) => !rightValues.includes(value)));
1135
+ });
1136
+ };
1137
+
1094
1138
  const schemaEvolutionIssues = (previous: JsonSchema, next: JsonSchema, path: string, direction: "input" | "data"): string[] => {
1139
+ const schemasMatch = direction === "input" ? sameInputSchema : sameSchema;
1095
1140
  for (const keyword of ["oneOf", "anyOf"] as const) {
1096
1141
  const previousBranches = previous[keyword];
1097
1142
  const nextBranches = next[keyword];
@@ -1100,9 +1145,25 @@ const schemaEvolutionIssues = (previous: JsonSchema, next: JsonSchema, path: str
1100
1145
  const nextRest = { ...next };
1101
1146
  delete previousRest[keyword];
1102
1147
  delete nextRest[keyword];
1103
- if (!sameSchema(previousRest, nextRest) || previousBranches.length !== nextBranches.length) {
1104
- return [`${path} changed`];
1148
+ if (!schemasMatch(previousRest, nextRest)) return [`${path} changed`];
1149
+ if (direction === "input") {
1150
+ if (!previousBranches.every(isSchemaObject) || !nextBranches.every(isSchemaObject)) return [`${path} changed`];
1151
+ if (nextBranches.length < previousBranches.length) return [`${path} changed`];
1152
+ // anyOf is monotonic under branch additions. oneOf is not: a newly overlapping
1153
+ // branch rejects previously valid input, even when the branch count stays the same.
1154
+ if (
1155
+ keyword === "oneOf" &&
1156
+ !sameInputSchema(previousBranches, nextBranches) &&
1157
+ !nextBranches.every((branch, index) => nextBranches.slice(index + 1).every((other) => disjointObjectBranches(branch, other)))
1158
+ )
1159
+ return [`${path} oneOf branches are not provably disjoint`];
1160
+ return previousBranches.flatMap((branch, index) =>
1161
+ nextBranches.some((candidate) => schemaEvolutionIssues(branch, candidate, path, direction).length === 0)
1162
+ ? []
1163
+ : [`${path} ${keyword}[${index}] was removed or narrowed`],
1164
+ );
1105
1165
  }
1166
+ if (previousBranches.length !== nextBranches.length) return [`${path} changed`];
1106
1167
  return previousBranches.flatMap((branch, index) =>
1107
1168
  schemaEvolutionIssues(branch as JsonSchema, nextBranches[index] as JsonSchema, `${path} ${keyword}[${index}]`, direction),
1108
1169
  );
@@ -1114,16 +1175,16 @@ const schemaEvolutionIssues = (previous: JsonSchema, next: JsonSchema, path: str
1114
1175
  const nextItems = nextRest.items;
1115
1176
  delete previousRest.items;
1116
1177
  delete nextRest.items;
1117
- const issues = sameSchema(previousRest, nextRest) ? [] : [`${path} constraints changed`];
1178
+ const issues = schemasMatch(previousRest, nextRest) ? [] : [`${path} constraints changed`];
1118
1179
  if (previousItems && nextItems) {
1119
1180
  issues.push(...schemaEvolutionIssues(previousItems as JsonSchema, nextItems as JsonSchema, `${path} items`, direction));
1120
- } else if (!sameSchema(previousItems, nextItems)) {
1181
+ } else if (!schemasMatch(previousItems, nextItems)) {
1121
1182
  issues.push(`${path} items changed`);
1122
1183
  }
1123
1184
  return issues;
1124
1185
  }
1125
1186
  if (previous.type !== "object" || next.type !== "object") {
1126
- return sameSchema(previous, next) ? [] : [`${path} changed`];
1187
+ return schemasMatch(previous, next) ? [] : [`${path} changed`];
1127
1188
  }
1128
1189
  const issues: string[] = [];
1129
1190
  const previousRest = { ...previous };
@@ -1132,10 +1193,15 @@ const schemaEvolutionIssues = (previous: JsonSchema, next: JsonSchema, path: str
1132
1193
  delete previousRest.required;
1133
1194
  delete nextRest.properties;
1134
1195
  delete nextRest.required;
1135
- if (!sameSchema(previousRest, nextRest)) issues.push(`${path} constraints changed`);
1196
+ if (!schemasMatch(previousRest, nextRest)) issues.push(`${path} constraints changed`);
1136
1197
 
1137
1198
  const previousProperties = (previous.properties ?? {}) as Record<string, JsonSchema>;
1138
1199
  const nextProperties = (next.properties ?? {}) as Record<string, JsonSchema>;
1200
+ if (direction === "input" && (previous.additionalProperties !== false || previous.patternProperties !== undefined)) {
1201
+ for (const name of Object.keys(nextProperties)) {
1202
+ if (!Object.hasOwn(previousProperties, name)) issues.push(`${path}.${name} may constrain previously allowed additional input`);
1203
+ }
1204
+ }
1139
1205
  for (const [name, schema] of Object.entries(previousProperties)) {
1140
1206
  const nextSchema = nextProperties[name];
1141
1207
  if (!nextSchema) issues.push(`${path}.${name} was removed`);
@@ -1302,6 +1368,15 @@ export const validateCapabilityResult = (
1302
1368
  return { ok: true, data: parsed.data };
1303
1369
  };
1304
1370
 
1371
+ const cancelledInvocation = (locale: string) => ({
1372
+ ok: false as const,
1373
+ error: {
1374
+ code: CAPABILITY_FRAMEWORK_ERROR_CODES.requestCancelled,
1375
+ message: capabilityMessages(locale).requestCancelled,
1376
+ status: 499 as const,
1377
+ },
1378
+ });
1379
+
1305
1380
  export const invokeCompiledCapability = async (params: {
1306
1381
  compiled: CompiledCapabilities;
1307
1382
  kind: "query" | "action";
@@ -1403,6 +1478,9 @@ export const invokeCompiledCapability = async (params: {
1403
1478
  )
1404
1479
  return { ok: false, error: { code: "VALIDATION_FAILED", message: "Unsupported search scope", status: 400 } };
1405
1480
  }
1481
+ // Preparation can finish after the forwarding request was cancelled. Never
1482
+ // cross into application code for the first time with an aborted invocation.
1483
+ if (context.signal.aborted) return cancelledInvocation(context.locale);
1406
1484
  try {
1407
1485
  const invoked = await operation.definition.run(input.data, context);
1408
1486
  if (!invoked.ok) {
@@ -1496,6 +1574,7 @@ export const reviewCompiledCapability = async (params: {
1496
1574
  },
1497
1575
  };
1498
1576
  }
1577
+ if (params.context.signal.aborted) return cancelledInvocation(params.context.locale);
1499
1578
  try {
1500
1579
  const reviewed = await operation.definition.review(input.data, params.context);
1501
1580
  if (!reviewed.ok) {
@@ -67,8 +67,14 @@ export const capabilityActionReviewIssues = (review: unknown, context: Capabilit
67
67
  data.links,
68
68
  // A per-call reason never lifts the declared `sends` completeness rule.
69
69
  data.ask === "sends" || ("ask" in context.policy && context.policy.ask === "sends"),
70
+ Boolean(data.content),
70
71
  ),
71
72
  );
73
+ if (data.content?.kind === "message" && "ask" in context.policy && context.policy.ask === "sends") {
74
+ if (!data.content.from) issues.push("Full sends content requires from");
75
+ if (!data.content.to.length && !data.content.cc?.length && !data.content.bcc?.length)
76
+ issues.push("Full sends content requires a recipient");
77
+ }
72
78
  return [...new Set(issues)];
73
79
  };
74
80
  /** A receipt's own rules; completeness links are the result envelope's top-level `links`. */
@@ -465,7 +465,8 @@ const slotText = (slot: CapabilitySlot, context: CapabilitySlotSentenceContext,
465
465
  }
466
466
  case "person": {
467
467
  const total = slot.total ?? slot.people.length;
468
- if (mode === "button") return total === 1 ? (slot.people[0]!.name ?? slot.people[0]!.address ?? "") : t.people({ count: total });
468
+ if (mode === "button")
469
+ return total === 1 ? plain(slot.people[0]!.name ?? slot.people[0]!.address ?? "", 24, "middle") : t.people({ count: total });
469
470
  const shown = mode === "full" ? slot.people : slot.people.slice(0, 3);
470
471
  const list = shown.map((person) =>
471
472
  mode === "chip" ? plain(person.name ?? person.address ?? "", 40, "middle") : capabilityPersonText(person),
@@ -0,0 +1,71 @@
1
+ import type { Context } from "hono";
2
+ import { REQUEST_IDLE_TIMEOUT_SECONDS, setRequestTimeout } from "../server/request-timeout";
3
+
4
+ type InvocationKind = "query" | "action" | "review";
5
+ export type CapabilityTimeoutOverrides = { queryTimeoutMs?: number; actionTimeoutMs?: number; reviewTimeoutMs?: number };
6
+
7
+ /** Discovery and recent-resource reads keep their short, independent budget. */
8
+ export const CAPABILITY_DISCOVERY_TIMEOUT_MS = 15_000;
9
+
10
+ export const capabilityTimeoutMs = (kind: InvocationKind, overrides: CapabilityTimeoutOverrides = {}): number => {
11
+ if (kind === "review") return overrides.reviewTimeoutMs ?? 15_000;
12
+ return kind === "query" ? (overrides.queryTimeoutMs ?? 45_000) : (overrides.actionTimeoutMs ?? 30_000);
13
+ };
14
+
15
+ /** Only JSON request owners use this; streaming responses own their idle lifecycle. */
16
+ export const withCapabilityRequestTimeout = async <T>(c: Context, kind: InvocationKind, run: () => Promise<T>): Promise<T> => {
17
+ setRequestTimeout(c, Math.ceil(capabilityTimeoutMs(kind) / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
18
+ try {
19
+ return await run();
20
+ } finally {
21
+ setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_SECONDS);
22
+ }
23
+ };
24
+
25
+ /** One monotonic budget, including preparation. Waiting reads never start after it expires. */
26
+ export const createCapabilityDeadline = (timeoutMs: number, caller: AbortSignal) => {
27
+ const controller = new AbortController();
28
+ const expiresAt = performance.now() + timeoutMs;
29
+ const expire = () => controller.abort(new DOMException("Capability deadline exceeded", "TimeoutError"));
30
+ const timer = setTimeout(expire, timeoutMs);
31
+ const signal = AbortSignal.any([caller, controller.signal]);
32
+ const check = () => {
33
+ if (performance.now() >= expiresAt) expire();
34
+ signal.throwIfAborted();
35
+ };
36
+ const wait = <T>(read: () => Promise<T>): Promise<T> => {
37
+ check();
38
+ return new Promise<T>((resolve, reject) => {
39
+ const aborted = () => reject(signal.reason);
40
+ signal.addEventListener("abort", aborted, { once: true });
41
+ // Observe both outcomes even after cancellation; a late read cannot resume dispatch.
42
+ Promise.resolve()
43
+ .then(() => {
44
+ check();
45
+ return read();
46
+ })
47
+ .then(
48
+ (result) => {
49
+ signal.removeEventListener("abort", aborted);
50
+ try {
51
+ check();
52
+ resolve(result);
53
+ } catch (error) {
54
+ reject(error);
55
+ }
56
+ },
57
+ (error) => {
58
+ signal.removeEventListener("abort", aborted);
59
+ reject(error);
60
+ },
61
+ );
62
+ });
63
+ };
64
+ return {
65
+ signal,
66
+ check,
67
+ wait,
68
+ remainingMs: () => Math.max(1, Math.ceil(expiresAt - performance.now())),
69
+ clear: () => clearTimeout(timer),
70
+ };
71
+ };