@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
@@ -2,17 +2,24 @@ import {
2
2
  type CapabilityActionWording,
3
3
  capabilityActionOutcome,
4
4
  capabilityActionSubject,
5
- resolveCapabilityActionSentences,
5
+ capabilityReviewSentence,
6
+ capabilitySentenceText,
6
7
  } from "../_internal/capability-sentences";
7
- import { displayToolName, isRecord } from "./chat/message-utils";
8
+ import { CapabilityActionReceiptSchema } from "../contracts/capabilities";
9
+ import { type AiStoredActionReview, isLegacyActionReview, storedActionReview } from "./action-review";
10
+ import { isRecord } from "./chat/message-utils";
8
11
  import type { AiTurnBlock } from "./protocol";
9
- import { CLOUD_AI_TOOL_PRESENTATION } from "./tool-sentences";
12
+ import { cloudToolReview } from "./tool-sentences";
10
13
 
11
14
  type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
12
15
 
16
+ /** The review Cloud stored for the call, else the one its open approval carries. */
17
+ const reviewOf = (block: ToolBlock): AiStoredActionReview | null =>
18
+ block.review ?? (block.approval?.review ? storedActionReview(block.approval.review) : null);
19
+
13
20
  /**
14
21
  * How the chat words one call that acts on something: an app Action through its saved presentation, a
15
- * Cloud tool through Cloud's own catalog of the same shape. `null` for calls that only read.
22
+ * Cloud tool through its per-call review. `null` for calls that only read.
16
23
  */
17
24
  export const toolWording = (block: ToolBlock, locale: string): CapabilityActionWording | null => {
18
25
  const presentation = block.presentation;
@@ -24,21 +31,42 @@ export const toolWording = (block: ToolBlock, locale: string): CapabilityActionW
24
31
  ...(presentation.fields ? { fields: presentation.fields } : {}),
25
32
  };
26
33
  }
27
- const sentences = resolveCapabilityActionSentences(block.name, CLOUD_AI_TOOL_PRESENTATION, locale);
28
- return sentences ? { title: displayToolName(block.name, locale), sentences } : null;
34
+ const review = cloudToolReview(block.name, block.args, locale);
35
+ return review
36
+ ? { title: capabilitySentenceText(review.sentence.action, review.slots, { locale: review.locale, timeZone: "UTC" }) }
37
+ : null;
29
38
  };
30
39
 
31
- /** What the call does or would do, as one sentence; `null` for calls that only read. */
40
+ /**
41
+ * What the call does or would do, as one sentence; `null` for calls that only read. A stored review's action sentence
42
+ * wins, so it reads the same after a reload and after the app changed. An old review's message is the question of the
43
+ * card, not the call's short subject: steps and receipts keep the app's catalog sentence.
44
+ */
32
45
  export const toolSubject = (block: ToolBlock, locale: string, timeZone: string): string | null => {
46
+ const review = reviewOf(block);
47
+ if (review && !isLegacyActionReview(review))
48
+ return capabilitySentenceText(review.sentence.action, review.slots, { locale: review.locale, timeZone });
49
+ const cloudReview = cloudToolReview(block.name, block.args, locale);
50
+ if (cloudReview) return capabilitySentenceText(cloudReview.sentence.action, cloudReview.slots, { locale: cloudReview.locale, timeZone });
33
51
  const wording = toolWording(block, locale);
34
52
  return wording ? capabilityActionSubject(wording, block.args, { locale, timeZone }) : null;
35
53
  };
36
54
 
37
55
  /** The sentence for one outcome, or `null` when the tool has none that renders for this call. */
38
56
  export const toolOutcome = (block: ToolBlock, key: "done" | "rejected" | "notRun", locale: string, timeZone: string): string | null => {
57
+ const result = isRecord(block.result) ? block.result : undefined;
58
+ const review = reviewOf(block);
59
+ if (review && !isLegacyActionReview(review)) {
60
+ const receipt = key === "done" ? CapabilityActionReceiptSchema.safeParse(result?.receipt) : null;
61
+ if (receipt?.success) return capabilitySentenceText(receipt.data.text, receipt.data.slots ?? {}, { locale: review.locale, timeZone });
62
+ return capabilityReviewSentence(review, key, { timeZone });
63
+ }
64
+ if (review && key === "done" && review.legacy.summary) return review.legacy.summary;
65
+ const cloudReview = cloudToolReview(block.name, block.args, locale);
66
+ // Only a form the tool names; otherwise the card keeps Cloud's own outcome words for the call.
67
+ if (cloudReview) return key === "done" || cloudReview.sentence[key] ? capabilityReviewSentence(cloudReview, key, { timeZone }) : null;
39
68
  const wording = toolWording(block, locale);
40
69
  if (!wording) return null;
41
- const result = isRecord(block.result) ? block.result : undefined;
42
70
  // App Actions return their data inside the result envelope; Cloud tools return the data itself.
43
71
  const data = block.presentation?.kind === "capability" ? result?.data : block.result;
44
72
  return capabilityActionOutcome(wording, key, { input: block.args, data }, { locale, timeZone });
package/src/ai/tools.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Tool, ToolContext } from "@k2b/nessi";
2
2
  import { defineTool as defineNessiTool } from "@k2b/nessi";
3
3
  import type { z } from "zod";
4
+ import type { CapabilityActionCallReview } from "../contracts/capabilities";
4
5
  import type { RequestActor } from "../server";
5
6
  import { aiToolNeedsApproval } from "./approvals";
6
7
  import type {
@@ -64,7 +65,7 @@ export const defineAiTool = <TInput extends z.ZodType, TOutput extends z.ZodType
64
65
  /** Localized short status; never include payloads or secrets. */
65
66
  reportProgress?: (message: string) => Promise<void>;
66
67
  /** Asks like `requestApproval`, and lets the person remember the approval of `target`. */
67
- requestApprovalFor?: (message: string, target: AiApprovalTarget) => Promise<boolean>;
68
+ requestApprovalFor?: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) => Promise<boolean>;
68
69
  /** Relays a decision for an effect another platform component consumes and executes. */
69
70
  requestRelayedApproval?: (message: string, options: { approvalId?: string; target?: AiApprovalTarget }) => Promise<boolean>;
70
71
  /** Shows requests a website approval let through as they go out, so the chat keeps them whatever the outcome. */
@@ -122,6 +123,8 @@ export type AiToolPreparationContext = {
122
123
  linkApproval?: (approvalCallId: string, approvalId: string) => void;
123
124
  /** Hears what a nested approval asks for before the approval is requested. */
124
125
  describeApproval?: (approvalCallId: string, target: AiApprovalTarget) => void;
126
+ /** Registers the deterministic review before requesting or consuming a nested approval. */
127
+ describeReview?: (approvalCallId: string, review: CapabilityActionCallReview) => void | Promise<void>;
125
128
  reportWebsiteReceipts?: (callId: string, receipts: AiWebsiteReceipt[]) => Promise<void>;
126
129
  };
127
130
 
@@ -168,10 +171,11 @@ export const prepareAiTools = (input: AiToolPreparationContext & { tools?: AiRun
168
171
  }
169
172
  // Nessi numbers the approvals of one execution in request order; count them to name each one's call ID.
170
173
  let approvals = 0;
171
- const requestApproval = async (message: string, target?: AiApprovalTarget): Promise<boolean> => {
174
+ const requestApproval = async (message: string, target?: AiApprovalTarget, review?: CapabilityActionCallReview): Promise<boolean> => {
172
175
  while (true) {
173
176
  const approvalCallId = `${callId}-approval-${approvals++}`;
174
177
  if (target) input.describeApproval?.(approvalCallId, target);
178
+ if (review) await input.describeReview?.(approvalCallId, review);
175
179
  if (!(await ctx.requestApproval(message))) return false;
176
180
  const used = await use(approvalCallId);
177
181
  if (used === "approved") return true;
@@ -182,7 +186,8 @@ export const prepareAiTools = (input: AiToolPreparationContext & { tools?: AiRun
182
186
  return {
183
187
  ...ctx,
184
188
  requestApproval,
185
- requestApprovalFor: (message: string, target: AiApprovalTarget) => requestApproval(message, target),
189
+ requestApprovalFor: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) =>
190
+ requestApproval(message, target, review),
186
191
  requestRelayedApproval: async (message: string, options: { approvalId?: string; target?: AiApprovalTarget }) => {
187
192
  const approvalCallId = `${callId}-approval-${approvals++}`;
188
193
  if (options.approvalId) input.linkApproval?.(approvalCallId, options.approvalId);
package/src/ai/types.ts CHANGED
@@ -13,11 +13,37 @@ import type {
13
13
  import type { Usage } from "@k2b/nessi/ai";
14
14
  import type { z } from "zod";
15
15
  import type { CapabilitySentenceField } from "../_internal/capability-sentences";
16
- import type { CapabilityActionReview, CapabilityActionSentences, CloudResourceRef } from "../contracts/capabilities";
16
+ import type {
17
+ CapabilityActionCallReview,
18
+ CapabilityActionReviewData,
19
+ CapabilityActionSentences,
20
+ CloudResourceRef,
21
+ } from "../contracts/capabilities";
17
22
  import type { RequestActor } from "../server";
23
+ import type { AiActionCheck, AiStoredActionReview } from "./action-review";
18
24
  import type { AiLocalClient, AiLocalContext, AiLocalToolName } from "./local-tool-contracts";
19
25
  import type { AiTurnBlock } from "./protocol";
20
26
 
27
+ /** Current reader-confirmed metadata for a recently sent resource. */
28
+ export type AiRecentResource = {
29
+ ref: CloudResourceRef;
30
+ title: string;
31
+ icon?: string;
32
+ href?: string;
33
+ kind: string;
34
+ preview?: string;
35
+ appName: string;
36
+ count: number;
37
+ };
38
+
39
+ export type AiRecentSentResourceRef = {
40
+ ref: CloudResourceRef;
41
+ count: number;
42
+ lastSentAt: string;
43
+ /** Internal conversation UUID, only for authorized Studio reads. */
44
+ conversationId: string;
45
+ };
46
+
21
47
  export const AI_MODEL_CAPABILITIES = ["streaming", "tools", "vision", "transcription"] as const;
22
48
  export type AiModelCapability = (typeof AI_MODEL_CAPABILITIES)[number];
23
49
 
@@ -308,6 +334,8 @@ export type AiStoredMessage = {
308
334
  trigger: "scheduled" | "manual";
309
335
  };
310
336
  toolPresentations?: Record<string, AiToolPresentation>;
337
+ toolReviews?: Record<string, AiStoredActionReview>;
338
+ toolChecks?: Record<string, AiActionCheck>;
311
339
  /** How the person decided an approval; `expired` when the turn ended while it still waited. */
312
340
  toolOutcomes?: Record<string, "rejected" | "approved" | "expired">;
313
341
  /** Requests a website approval let through, by tool call and then by `AiWebsiteReceipt.index`. */
@@ -428,7 +456,14 @@ export type AiPendingTurnAction =
428
456
  name: string;
429
457
  args: unknown;
430
458
  message?: string;
431
- review?: CapabilityActionReview;
459
+ /** Stored review, including the declared ask reason for new-shape reviews. */
460
+ review?: CapabilityActionReviewData;
461
+ /** Owning application, with its registry name in the turn's request locale. */
462
+ app?: { id: string; name: string };
463
+ /** The chat's current title, when known. */
464
+ chatTitle?: string | null;
465
+ /** Expiry of the platform approval request, as an ISO timestamp. */
466
+ expiresAt?: string;
432
467
  allowAlways: boolean;
433
468
  /** Whether the approval can be remembered for this chat. */
434
469
  allowChat: boolean;
@@ -464,7 +499,7 @@ export type AiPendingTurnActionRecord = {
464
499
  name: string;
465
500
  args: unknown;
466
501
  message?: string;
467
- review?: CapabilityActionReview;
502
+ review?: CapabilityActionReviewData;
468
503
  approvalScope: string;
469
504
  allowAlways: boolean;
470
505
  /** Whether the approval can be remembered for this chat; defaults to `allowAlways`. */
@@ -621,6 +656,7 @@ export type AiBackgroundRunContext = {
621
656
  trigger?: "scheduled" | "manual";
622
657
  timezone?: string;
623
658
  stopDeliveredAt?: string;
659
+ needsApproval?: { action: string };
624
660
  taskId: string;
625
661
  occurrenceId: string;
626
662
  context: Array<{ seq: number; kind: "message" | "summary"; message: Message }>;
@@ -765,6 +801,7 @@ export type AiConversationService = {
765
801
  before?: string;
766
802
  limit?: number;
767
803
  }): Promise<{ resources: AiConversationResourceRef[]; nextCursor?: string }>;
804
+ listRecentSentResourceRefs(input: { ownerUserId: string }): Promise<AiRecentSentResourceRef[]>;
768
805
  listUserConversationResources(input: {
769
806
  ownerUserId: string;
770
807
  search?: string;
@@ -1007,6 +1044,8 @@ export type AiConversationService = {
1007
1044
  getPendingTurnAction(input: { conversationId: string; turnId: string; callId: string }): Promise<AiPendingTurnActionRecord | null>;
1008
1045
  findPendingTurnActionByApproval(input: { approvalId: string; ownerUserId: string }): Promise<AiPendingTurnActionRecord | null>;
1009
1046
  listPendingActionRecords(input: { conversationId: string; turnId: string }): Promise<AiPendingTurnActionRecord[]>;
1047
+ /** The reviews Cloud stored with a turn's tool calls, keyed by call ID; the newest message wins. */
1048
+ listTurnToolReviews(input: { conversationId: string; turnId: string }): Promise<Map<string, AiStoredActionReview>>;
1010
1049
  listResolvedPendingActions(input: { conversationId: string; turnId: string }): Promise<AiPendingTurnActionRecord[]>;
1011
1050
  resolvePendingTurnAction(input: {
1012
1051
  conversationId: string;
@@ -1126,7 +1165,7 @@ export type AiToolRuntime<TInput extends z.ZodType = z.ZodType, TOutput extends
1126
1165
  /** Localized short status; never include payloads or secrets. */
1127
1166
  reportProgress?: (message: string) => Promise<void>;
1128
1167
  /** Asks like `requestApproval`, and lets the person remember the approval of `target`. */
1129
- requestApprovalFor?: (message: string, target: AiApprovalTarget) => Promise<boolean>;
1168
+ requestApprovalFor?: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) => Promise<boolean>;
1130
1169
  /** Relays a decision for an effect another platform component consumes and executes. */
1131
1170
  requestRelayedApproval?: (message: string, options: { approvalId?: string; target?: AiApprovalTarget }) => Promise<boolean>;
1132
1171
  /** Shows requests a website approval let through as they go out, so the chat keeps them whatever the outcome. */
@@ -14,6 +14,7 @@ import {
14
14
  UserProviderSchema,
15
15
  } from "../contracts";
16
16
  import { type AuthContext, auth, expectUserBackedActor, jsonResponse, requiresAuth, respond, v } from "../server";
17
+ import { noTransform } from "../server/middleware/no-transform";
17
18
  import { accountsAppService as accountsService } from "../services";
18
19
 
19
20
  const EntitiesListResponseSchema = z.object({
@@ -91,7 +92,7 @@ type AccountsEntitiesRouteDependencies = {
91
92
  };
92
93
 
93
94
  export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRouteDependencies = {}) =>
94
- new Hono<AuthContext>().get(
95
+ new Hono<AuthContext>().use("/entities/*", noTransform()).get(
95
96
  "/entities",
96
97
  dependencies.authenticate ?? auth.requireRole("authenticated"),
97
98
  dependencies.requireUser ?? auth.requireUser(),
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { AiProjectLastAdminError, aiProjects } from "../ai/projects";
4
4
  import { AuthenticatedPrincipalSchema } from "../contracts/shared";
5
5
  import { type AuthContext, auth, err, fail, ok, respond, v } from "../server";
6
+ import { noTransform } from "../server/middleware/no-transform";
6
7
 
7
8
  const ListProjectsSchema = z
8
9
  .object({
@@ -20,6 +21,7 @@ const lastAdminConflict = (c: Parameters<typeof respond>[0], error: AiProjectLas
20
21
 
21
22
  export const createAdminAiProjectsRoutes = (authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin")) =>
22
23
  new Hono<AuthContext>()
24
+ .use("*", noTransform())
23
25
  .use("*", authenticate)
24
26
  .get("/", v("query", ListProjectsSchema), async (c) => {
25
27
  const query = c.req.valid("query");
@@ -4,6 +4,7 @@ import { aiAppSkills } from "../ai/app-skill-store";
4
4
  import { AiSkillInputError, AiSkillLastAdminError, AiSkillRevisionConflictError, aiSkills } from "../ai/skills";
5
5
  import { AuthenticatedPrincipalSchema } from "../contracts/shared";
6
6
  import { type AuthContext, auth, err, fail, ok, respond, v } from "../server";
7
+ import { noTransform } from "../server/middleware/no-transform";
7
8
 
8
9
  const ListSkillsSchema = z
9
10
  .object({
@@ -32,6 +33,7 @@ const installAppSkill = async (c: Parameters<typeof respond>[0], install: () =>
32
33
 
33
34
  export const createAdminAiSkillsRoutes = (authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin")) =>
34
35
  new Hono<AuthContext>()
36
+ .use("*", noTransform())
35
37
  .use("*", authenticate)
36
38
  .get("/", v("query", ListSkillsSchema), async (c) => {
37
39
  const query = c.req.valid("query");
@@ -3,6 +3,7 @@ import { describeRoute } from "hono-openapi";
3
3
  import { z } from "zod";
4
4
  import { aiUsage } from "../ai/usage";
5
5
  import { type AuthContext, auth, err, fail, ok, respond, v } from "../server";
6
+ import { noTransform } from "../server/middleware/no-transform";
6
7
  import { AiUsageQuerySchema } from "../shared/ai-usage";
7
8
 
8
9
  export const createAdminAiUsageRoutes = (
@@ -10,6 +11,7 @@ export const createAdminAiUsageRoutes = (
10
11
  service: typeof aiUsage = aiUsage,
11
12
  ) =>
12
13
  new Hono<AuthContext>()
14
+ .use("*", noTransform())
13
15
  .use("*", authenticate)
14
16
  .get(
15
17
  "/report",
@@ -0,0 +1,80 @@
1
+ import { Hono, type MiddlewareHandler } from "hono";
2
+ import { describeRoute } from "hono-openapi";
3
+ import { z } from "zod";
4
+ import { GEOIP_MAX_DATABASE_BYTES, GEOIP_MAX_REQUEST_BYTES } from "../contracts/geoip";
5
+ import { type AuthContext, auth, v } from "../server";
6
+ import { REQUEST_IDLE_TIMEOUT_MAX_SECONDS, REQUEST_IDLE_TIMEOUT_SECONDS, setRequestTimeout } from "../server/request-timeout";
7
+ import { GeoIpError, geoip } from "../services/geoip";
8
+
9
+ export const createAdminGeoIpRoutes = (
10
+ authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
11
+ service: typeof geoip = geoip,
12
+ ) =>
13
+ new Hono<AuthContext>()
14
+ .use(authenticate)
15
+ .onError((error, c) => {
16
+ if (error instanceof GeoIpError)
17
+ return c.json(
18
+ {
19
+ code: error.code,
20
+ ...(error.code === "geoip_too_large" ? { maxDatabaseBytes: GEOIP_MAX_DATABASE_BYTES } : {}),
21
+ ...(error.code === "geoip_request_too_large" ? { maxRequestBytes: GEOIP_MAX_REQUEST_BYTES } : {}),
22
+ },
23
+ error.status,
24
+ );
25
+ if (c.req.raw.signal.aborted) return new Response(null, { status: 499 });
26
+ throw error;
27
+ })
28
+ .get(
29
+ "/",
30
+ describeRoute({ tags: ["Sign-in locations"], summary: "Show active sign-in location databases and upload limits" }),
31
+ auth.requireOAuthScope("read", "admin"),
32
+ async (c) => c.json(await service.status(c.get("user"))),
33
+ )
34
+ .put(
35
+ "/",
36
+ describeRoute({
37
+ tags: ["Sign-in locations"],
38
+ summary: "Replace a City or ASN database with a raw or gzip MMDB upload",
39
+ requestBody: {
40
+ required: true,
41
+ content: {
42
+ "application/octet-stream": { schema: { type: "string", format: "binary" } },
43
+ "application/gzip": { schema: { type: "string", format: "binary" } },
44
+ },
45
+ },
46
+ }),
47
+ auth.requireOAuthScope("write", "admin"),
48
+ async (c) => {
49
+ setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_MAX_SECONDS);
50
+ try {
51
+ const length = c.req.header("Content-Length");
52
+ return c.json(
53
+ await service.upload(
54
+ c.get("user"),
55
+ c.req.raw.body ??
56
+ new ReadableStream({
57
+ start(controller) {
58
+ controller.close();
59
+ },
60
+ }),
61
+ {
62
+ contentLength: length === undefined ? undefined : Number(length),
63
+ signal: c.req.raw.signal,
64
+ },
65
+ ),
66
+ );
67
+ } finally {
68
+ setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_SECONDS);
69
+ }
70
+ },
71
+ )
72
+ .delete(
73
+ "/:kind",
74
+ describeRoute({ tags: ["Sign-in locations"], summary: "Reset one sign-in database to the bundled snapshot" }),
75
+ auth.requireOAuthScope("write", "admin"),
76
+ v("param", z.object({ kind: z.enum(["city", "asn"]) })),
77
+ async (c) => c.json(await service.reset(c.get("user"), c.req.valid("param").kind)),
78
+ );
79
+
80
+ export default createAdminGeoIpRoutes();
@@ -7,6 +7,7 @@ import { listApps } from "../_internal/registry";
7
7
  import { CapabilityAppIdSchema } from "../contracts/capabilities";
8
8
  import { AI_INFERENCE_SCOPE } from "../contracts/shared";
9
9
  import { type AuthContext, auth, err, respond, v } from "../server";
10
+ import { noTransform } from "../server/middleware/no-transform";
10
11
  import { audit } from "../services/audit";
11
12
  import { getIdentitySigningKeyStatus, identityMetrics, revokeIdentitySigningKey, rewrapIdentitySigningKeys } from "../services/identity";
12
13
  import { WORKLOAD_SCOPES } from "../services/identity/workload-auth";
@@ -110,6 +111,8 @@ export const createAdminIdentityRoutes = (
110
111
  service: IdentityAdminDependencies = dependencies,
111
112
  ) =>
112
113
  new Hono<AuthContext>()
114
+ // Mandate lists hold every user's grant inputs, which users write at will and which can carry private text.
115
+ .use("*", noTransform())
113
116
  .use(authenticate)
114
117
  .use("*", async (c, next) => {
115
118
  c.header("Cache-Control", "no-store");
@@ -4,6 +4,7 @@ import { describeRoute } from "hono-openapi";
4
4
  import { z } from "zod";
5
5
  import { createPagination, ErrorResponseSchema, MessageResponseSchema, PaginationQuerySchema, parsePagination } from "../contracts";
6
6
  import { type AuthContext, auth, jsonResponse, requiresAdmin, respond, v } from "../server";
7
+ import { noTransform } from "../server/middleware/no-transform";
7
8
  import { accountLifecycle, lifecycleJobs } from "../services";
8
9
 
9
10
  const DeletedAccountSchema = z.object({
@@ -63,7 +64,9 @@ const TriggerJobResponseSchema = z.object({
63
64
  jobId: z.string(),
64
65
  });
65
66
 
67
+ // Deleted-account and reminder reports list other accounts' personal data next to requester-controlled identity text.
66
68
  const app = new Hono<AuthContext>()
69
+ .use("*", noTransform())
67
70
  .use(auth.requireRole("admin"))
68
71
  .get(
69
72
  "/deleted-accounts",
@@ -14,6 +14,7 @@ import {
14
14
  MailStatusSchema,
15
15
  } from "../contracts/outgoing-mail";
16
16
  import { type AuthContext, auth, jsonResponse, requiresAdmin } from "../server";
17
+ import { noTransform } from "../server/middleware/no-transform";
17
18
  import { audit } from "../services/audit";
18
19
  import { outgoingMailLog } from "../services/outgoing-mail/admin";
19
20
  import { MAIL_RETENTION_KEYS, readMailRetention } from "../services/outgoing-mail/retention";
@@ -96,6 +97,7 @@ const contentResponse = z.union([
96
97
  ]);
97
98
  export const createAdminOutgoingMailRoutes = (authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin")) =>
98
99
  new Hono<AuthContext>()
100
+ .use("*", noTransform())
99
101
  .use("*", authenticate)
100
102
  .use("*", async (c, next) => {
101
103
  c.header("Cache-Control", "no-store");
@@ -1,20 +1,44 @@
1
1
  import { submitAiTurnAction } from "../ai/runtime";
2
2
  import { aiConversations } from "../ai/store";
3
- import { APP_ACTION_LIMITS } from "../contracts/app-approval";
3
+ import { APP_ACTION_LIMITS, type AppActionSummary } from "../contracts/app-approval";
4
+ import { APPROVALS_PAGE_SIZE } from "../contracts/approvals";
4
5
  import type { AppDeviceActions } from "../services/app-approval";
5
- import { listPending } from "../services/approvals";
6
- import { appActionSummary, appActionView } from "../services/approvals/device";
6
+ import { listPending, loadApprovalActionPolicies, pendingApprovalCard } from "../services/approvals";
7
+ import { appActionOffered, appActionSummary, appActionView } from "../services/approvals/device";
8
+
9
+ /** Pages of open requests one inbox poll reads at most while it skips browser-only questions. */
10
+ const INBOX_PAGES = 5;
7
11
 
8
12
  export const createDeviceActions = (appName: (appId: string, locale: string) => string): AppDeviceActions => {
9
13
  const view: AppDeviceActions["view"] = async ({ userId, issuer, actionId }) => {
10
14
  const { items } = await listPending({ approverUserId: userId, id: actionId, limit: 1 });
11
- return items[0] ? appActionView(items[0], { issuer, appName }) : null;
15
+ const row = items[0];
16
+ if (!row) return null;
17
+ const policy = (await loadApprovalActionPolicies([row]))(row);
18
+ const mapped = await appActionView(row, { issuer, appName, policy });
19
+ return appActionOffered(mapped, pendingApprovalCard(row, appName, policy), policy) ? mapped : null;
12
20
  };
13
21
  return {
14
22
  inbox: async ({ userId, issuer }) => {
15
- const { items, count } = await listPending({ approverUserId: userId, limit: APP_ACTION_LIMITS.inboxActions });
16
- const views = await Promise.all(items.map((row) => appActionView(row, { issuer, appName })));
17
- return { actions: views.map(appActionSummary), moreActions: Math.max(0, count - items.length) };
23
+ // Browser-only questions are skipped, so read on past them. One poll reads at most this many pages of open
24
+ // requests, newest first, which keeps each 5-second poll bounded.
25
+ const actions: AppActionSummary[] = [];
26
+ let count = 0;
27
+ let cursor: string | undefined;
28
+ for (let page = 0; page < INBOX_PAGES && actions.length < APP_ACTION_LIMITS.inboxActions; page += 1) {
29
+ const result = await listPending({ approverUserId: userId, limit: APPROVALS_PAGE_SIZE, ...(cursor ? { cursor } : {}) });
30
+ count = result.count;
31
+ const policyOf = await loadApprovalActionPolicies(result.items);
32
+ for (const row of result.items) {
33
+ const policy = policyOf(row);
34
+ const mapped = await appActionView(row, { issuer, appName, policy });
35
+ if (appActionOffered(mapped, pendingApprovalCard(row, appName, policy), policy)) actions.push(appActionSummary(mapped));
36
+ if (actions.length === APP_ACTION_LIMITS.inboxActions) break;
37
+ }
38
+ if (!result.nextCursor) break;
39
+ cursor = result.nextCursor;
40
+ }
41
+ return { actions, moreActions: Math.max(0, count - actions.length) };
18
42
  },
19
43
  view,
20
44
  decide: async ({ userId, deviceId, issuer, actionId, digest, approved }) => {
@@ -5,6 +5,8 @@ import { describeRoute } from "hono-openapi";
5
5
  import { z } from "zod";
6
6
  import {
7
7
  APP_APPROVAL_LIMITS,
8
+ AppDeviceChangeLookupSchema,
9
+ AppDeviceChangeReferenceSchema,
8
10
  AppDeviceMutationSchema,
9
11
  AppDeviceRequestSchema,
10
12
  AppLoginReferenceSchema,
@@ -15,6 +17,8 @@ import {
15
17
  AppPairingStartSchema,
16
18
  } from "../contracts/app-approval";
17
19
  import { type AuthContext, auth, rateLimit, v } from "../server";
20
+ import { noTransform } from "../server/middleware/no-transform";
21
+ import { addressPrefix, clientIp, describeRequest } from "../server/request-context";
18
22
  import {
19
23
  type AppApprovalActor,
20
24
  AppApprovalError,
@@ -22,6 +26,8 @@ import {
22
26
  appApproval,
23
27
  type createAppApprovalService,
24
28
  } from "../services/app-approval";
29
+ import { type NewBrowserSignInNotifier, readBrowserCookie } from "../services/known-browsers";
30
+ import { SessionAppDeviceInactiveError, SessionEpochChangedError } from "../services/session";
25
31
  import { appNameIn } from "../ssr/runtime";
26
32
  import { createDeviceActions } from "./app-approval-actions";
27
33
 
@@ -30,7 +36,12 @@ const DevicePageSchema = z.object({ after: z.string().uuid().optional() }).stric
30
36
  type Service = ReturnType<typeof createAppApprovalService>;
31
37
 
32
38
  /** This is a separate credential surface, never a user API-key or OAuth scope. */
33
- export const createAppApprovalRoutes = (service: Service = appApproval, actions = createDeviceActions, terminal?: AppTerminalLogins) => {
39
+ export const createAppApprovalRoutes = (
40
+ options: { notify: NewBrowserSignInNotifier },
41
+ service: Service = appApproval,
42
+ actions = createDeviceActions,
43
+ terminal?: AppTerminalLogins,
44
+ ) => {
34
45
  const actor = async (c: Parameters<typeof auth.session.getToken>[0]): Promise<AppApprovalActor> => {
35
46
  // The web session (or an explicit bearer session), never the mobile app's session: a phone
36
47
  // that pairs sign-in devices would hand out authority that outlives its removal.
@@ -42,6 +53,8 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
42
53
  return { userId: session.user.id, sid: session.data.sid, admin: session.user.roles.includes("admin") };
43
54
  };
44
55
  return new Hono<AuthContext>()
56
+ .use("/device", noTransform())
57
+ .use("/device/*", noTransform())
45
58
  .onError((error, c) => {
46
59
  if (error instanceof AppApprovalError) return c.json({ code: error.code, message: error.code }, error.status);
47
60
  if (error instanceof HTTPException && error.status < 500) return c.json({ code: "INVALID_REQUEST" }, error.status);
@@ -95,7 +108,19 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
95
108
  )
96
109
  .post("/manage/devices/update", v("json", AppDeviceMutationSchema), async (c) => {
97
110
  const input = c.req.valid("json");
98
- await service.mutateDevice(await actor(c), input.deviceId, input.operation === "rename" ? input.name : undefined);
111
+ const result = await service.mutateDevice(
112
+ await actor(c),
113
+ input.deviceId,
114
+ input.operation === "rename" ? input.name : undefined,
115
+ input.operation === "revoke" ? input.endSessions : undefined,
116
+ );
117
+ return result ? c.json(result) : c.body(null, 204);
118
+ })
119
+ .post("/manage/device-changes/status", v("json", AppDeviceChangeLookupSchema), async (c) => {
120
+ return c.json(await service.findRemoval(await actor(c), c.req.valid("json")));
121
+ })
122
+ .post("/manage/device-changes/cancel", v("json", AppDeviceChangeReferenceSchema), async (c) => {
123
+ await service.cancelChange(await actor(c), c.req.valid("json").changeId);
99
124
  return c.body(null, 204);
100
125
  })
101
126
  .post(
@@ -118,6 +143,7 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
118
143
  c.req.valid("json"),
119
144
  actions((appId, locale) => appNameIn(c, appId, locale, appId)),
120
145
  terminal,
146
+ { address: clientIp(c) },
121
147
  ),
122
148
  ),
123
149
  )
@@ -127,7 +153,10 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
127
153
  v("json", AppLoginStartSchema),
128
154
  async (c) => {
129
155
  const input = c.req.valid("json");
130
- return c.json(await service.startLogin(input.identifier, input.category), 202);
156
+ const description = describeRequest(c);
157
+ const cookie = readBrowserCookie(c);
158
+ const prefix = addressPrefix(clientIp(c));
159
+ return c.json(await service.startLogin(input.identifier, input.category, { ...description, cookie, prefix }), 202);
131
160
  },
132
161
  )
133
162
  .post("/login/status", v("json", AppLoginReferenceSchema), async (c) => {
@@ -143,8 +172,20 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
143
172
  v("json", AppLoginReferenceSchema),
144
173
  async (c) => {
145
174
  const input = c.req.valid("json");
146
- const userId = await service.consumeLogin(input.requestId, input.browserSecret);
147
- await auth.session.create(c, userId);
175
+ const { userId, authEpoch, deviceId, deviceName } = await service.consumeLogin(input.requestId, input.browserSecret);
176
+ try {
177
+ await auth.session.create(c, userId, {
178
+ method: "cloud_login",
179
+ device: deviceName,
180
+ notify: options.notify,
181
+ authEpoch,
182
+ appDeviceId: deviceId,
183
+ });
184
+ } catch (error) {
185
+ if (error instanceof SessionEpochChangedError || error instanceof SessionAppDeviceInactiveError)
186
+ throw new AppApprovalError("CONFLICT", 409);
187
+ throw error;
188
+ }
148
189
  return c.body(null, 204);
149
190
  },
150
191
  );
@@ -1,9 +1,12 @@
1
1
  import type { Context } from "hono";
2
2
  import { HTTPException } from "hono/http-exception";
3
+ import { listCapabilities } from "../_internal/registry";
3
4
  import { resolveAiChatRequestContext } from "../ai/request-context";
4
5
  import type { ApprovalListQuery, ApprovalPage } from "../contracts/approvals";
6
+ import { CapabilityActionCallReviewSchema, capabilityActionPolicy } from "../contracts/capabilities";
5
7
  import type { AuthContext } from "../server/middleware/auth";
6
8
  import { listPending, presentPendingApproval } from "../services/approvals/list";
9
+ import { logger } from "../services/logging";
7
10
  import { getLocalizedRuntimeContext } from "../ssr/runtime";
8
11
 
9
12
  export const loadPendingApprovals = async <T extends AuthContext>(c: Context<T>, query: ApprovalListQuery): Promise<ApprovalPage> => {
@@ -11,5 +14,24 @@ export const loadPendingApprovals = async <T extends AuthContext>(c: Context<T>,
11
14
  if (ctx instanceof Response) throw new HTTPException(403, { res: ctx });
12
15
  const page = await listPending({ approverUserId: ctx.ownerUserId, appId: query.app, link: query.link, cursor: query.cursor });
13
16
  const apps = getLocalizedRuntimeContext(c).apps;
14
- return { ...page, items: page.items.map((row) => presentPendingApproval(row, apps)) };
17
+ const needsPolicy = page.items.some((row) => {
18
+ if (row.storedReview && "sentence" in row.storedReview) return false;
19
+ const review = CapabilityActionCallReviewSchema.safeParse(row.details);
20
+ return review.success && !review.data.ask;
21
+ });
22
+ const capabilities = needsPolicy
23
+ ? await listCapabilities().catch((error: unknown) => {
24
+ logger("approvals").warn("Approval capability registry unavailable", {
25
+ error: error instanceof Error ? error.message : String(error),
26
+ });
27
+ return [];
28
+ })
29
+ : [];
30
+ return {
31
+ ...page,
32
+ items: page.items.map((row) => {
33
+ const manifest = capabilities.find((entry) => entry.appId === row.appId)?.manifest;
34
+ return presentPendingApproval(row, apps, manifest ? capabilityActionPolicy(manifest, row.action) : undefined);
35
+ }),
36
+ };
15
37
  };
@@ -18,6 +18,7 @@ import { ErrorResponseSchema } from "../contracts/shared";
18
18
  import { rateLimit } from "../server";
19
19
  import { respond } from "../server/api";
20
20
  import { type AuthContext, auth } from "../server/middleware/auth";
21
+ import { noTransform } from "../server/middleware/no-transform";
21
22
  import { jsonResponse, requiresAuth } from "../server/middleware/openapi";
22
23
  import { v } from "../server/middleware/validator";
23
24
  import { ok } from "../server/services";
@@ -26,6 +27,7 @@ import { decodeApprovalCursor } from "../services/approvals/cursor";
26
27
  import { loadPendingApprovals } from "./approvals-loader";
27
28
 
28
29
  export const approvalRoutes = new Hono<AuthContext>()
30
+ .use("*", noTransform())
29
31
  .use(rateLimit())
30
32
  .use(auth.requireRole("authenticated"))
31
33
  .get(