@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/src/ai/routes.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import { type Context, Hono } from "hono";
2
2
  import { bodyLimit } from "hono/body-limit";
3
+ import { describeRoute } from "hono-openapi";
3
4
  import { z } from "zod";
4
- import { listCapabilities } from "../_internal/registry";
5
+ import { getApp, listCapabilities } from "../_internal/registry";
6
+ import { QUERY_TIMEOUT_MS } from "../api/capabilities";
7
+ import { ErrorResponseSchema } from "../contracts";
5
8
  import { APPROVAL_CLIENT_HEADER } from "../contracts/approvals";
9
+ import { capabilityActionPolicy } from "../contracts/capabilities";
6
10
  import {
7
11
  type AccessSubject,
8
12
  type AuthContext,
@@ -11,22 +15,30 @@ import {
11
15
  fail,
12
16
  getLocale,
13
17
  getTimeZone,
18
+ jsonResponse,
14
19
  ok,
15
20
  type RequestActor,
16
21
  rateLimit,
22
+ requiresAuth,
17
23
  respond,
18
24
  v,
19
25
  } from "../server";
20
26
  import { isAppSessionActor } from "../server/actor";
21
27
  import { isRequestCredentialCurrent } from "../server/middleware/auth";
22
- import { approvalDecisionChannel } from "../services/approvals";
28
+ import { noTransform } from "../server/middleware/no-transform";
29
+ import { REQUEST_IDLE_TIMEOUT_SECONDS, setRequestTimeout } from "../server/request-timeout";
30
+ import { approvalDecisionChannel, approvals } from "../services/approvals";
31
+ import { pendingApprovalReview } from "../services/approvals/question";
23
32
  import { ImageMetadataError } from "../services/image-metadata";
24
33
  import { logger } from "../services/logging";
25
34
  import { coreSettings } from "../services/settings/api";
35
+ import { resolveAppIdentityPresentation } from "../shared/app-presentation";
26
36
  import { readThemeFromCookieHeader } from "../shared/theme";
37
+ import { type AiStoredActionReview, isLegacyActionReview, isUnknownActionOutcome, storedActionReview } from "./action-review";
27
38
  import { assistantAiSettingsState, listAssistantAiModels, resolveAssistantAudioModel, selectAssistantAiModelId } from "./assistant-models";
28
39
  import { AI_AUDIO_MAX_BYTES } from "./audio-format";
29
40
  import { buildAiCapabilityCatalog } from "./capabilities";
41
+ import { checkAiCapability, resolveAiCapabilityActor } from "./capability-execution";
30
42
  import { aiChatMessages } from "./chat/messages";
31
43
  import { getAiChatQuotas } from "./chat-quotas";
32
44
  import { aiChatTasks } from "./chat-tasks";
@@ -67,6 +79,8 @@ import {
67
79
  import { aiActorUser, aiPrefsUserId, aiUserPrefs } from "./prefs";
68
80
  import { aiProjects } from "./projects";
69
81
  import { projectPublicAiStoredMessages, publicAiStoredMessages } from "./public-projection";
82
+ import { AiRecentResourceSchema, listAiRecentResources } from "./recent-resources";
83
+ import { RECENT_SENT_REF_LIMIT } from "./recent-sent-refs";
70
84
  import { type AiChatRequestContext, resolveAiChatRequestContext as resolveContext } from "./request-context";
71
85
  import { aiResourceMarker } from "./resource-markers";
72
86
  import { isConversationResourceCursor } from "./resource-refs";
@@ -85,9 +99,10 @@ import { AI_SHORT_ID_PATTERN } from "./short-id";
85
99
  import { selectAiSkillCatalog } from "./skill-catalog";
86
100
  import { createCloudAiLoadSkillTool, createCloudAiSearchSkillsTool } from "./skill-tool";
87
101
  import { aiSkills } from "./skills";
88
- import { aiConversations } from "./store";
102
+ import { AI_TOOL_CHECK_STALE_MS, aiConversations, claimAiToolCheck, readAiToolCheck, settleAiToolCheck } from "./store";
89
103
  import { createAiConversationStreamResponse, loadAiStreamState } from "./stream";
90
104
  import { composeAiSystemPrompt } from "./system-prompt";
105
+ import { aiToolAudit } from "./tool-audit";
91
106
  import { aiToolPromptHints } from "./tools";
92
107
  import type { AiConversation, AiTurn } from "./types";
93
108
 
@@ -305,6 +320,7 @@ export const aiRoutes = (() => {
305
320
 
306
321
  return (
307
322
  new Hono<AuthContext>()
323
+ .use("*", noTransform())
308
324
  .use(rateLimit())
309
325
  .use("*", auth.requireRole("authenticated"))
310
326
  .get("/status", async (c) => {
@@ -459,6 +475,42 @@ export const aiRoutes = (() => {
459
475
  });
460
476
  return respond(c, ok({ prompt, renderedAt: new Date().toISOString() }));
461
477
  })
478
+ .get(
479
+ "/resources/recent",
480
+ describeRoute({
481
+ tags: ["AI"],
482
+ summary: "List recently sent resources",
483
+ description: "Up to eight recently sent refs, with current reader-authorized metadata. Unreadable resources are omitted.",
484
+ ...requiresAuth,
485
+ responses: {
486
+ 200: jsonResponse(
487
+ z.object({ resources: z.array(AiRecentResourceSchema).max(RECENT_SENT_REF_LIMIT) }),
488
+ "Current recent resources",
489
+ ),
490
+ 401: jsonResponse(ErrorResponseSchema, "Authentication required"),
491
+ 403: jsonResponse(ErrorResponseSchema, "AI context unavailable"),
492
+ },
493
+ }),
494
+ // Titles come from app readers, so a scoped token needs the read scope the capability routes ask for.
495
+ auth.requireOAuthScope("read", "admin"),
496
+ async (c) => {
497
+ c.header("Cache-Control", "no-store");
498
+ const ctx = await resolveContext(c);
499
+ if (ctx instanceof Response) return ctx;
500
+ setRequestTimeout(c, Math.ceil(QUERY_TIMEOUT_MS / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
501
+ try {
502
+ const resources = await listAiRecentResources({
503
+ ownerUserId: ctx.ownerUserId,
504
+ authority: auth.getAuthority(c),
505
+ locale: getLocale(c),
506
+ signal: c.req.raw.signal,
507
+ });
508
+ return respond(c, ok({ resources }));
509
+ } finally {
510
+ setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_SECONDS);
511
+ }
512
+ },
513
+ )
462
514
  .get("/resources", v("query", UserResourcesQuerySchema), async (c) => {
463
515
  const ctx = await resolveContext(c);
464
516
  if (ctx instanceof Response) return ctx;
@@ -1158,6 +1210,101 @@ export const aiRoutes = (() => {
1158
1210
  await abortAiTurn({ conversationId: conversation.id, turnId: turn.id });
1159
1211
  return respond(c, ok({ ok: true }));
1160
1212
  })
1213
+ .post("/conversations/:conversationId/turns/:turnId/calls/:callId/check", async (c) => {
1214
+ const ctx = await resolveContext(c);
1215
+ if (ctx instanceof Response) return ctx;
1216
+ // The check runs with the person's session authority, so a scoped token or another client cannot borrow it.
1217
+ if (ctx.actor.kind !== "user" || !ctx.signedInSession)
1218
+ return respond(c, fail(err.forbidden("Action outcome checks require a signed-in browser session.")));
1219
+ const conversation = await loadConversation(c, ctx);
1220
+ if (!conversation) return notFound(c);
1221
+ const turn = await loadTurn(conversation, c.req.param("turnId"));
1222
+ const callId = c.req.param("callId");
1223
+ if (!turn || !callId) return notFound(c);
1224
+ const target = { conversationId: conversation.id, turnId: turn.id, callId };
1225
+ // The app's Query may take its whole budget; the request must outlast it.
1226
+ setRequestTimeout(c, Math.ceil(QUERY_TIMEOUT_MS / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
1227
+ try {
1228
+ const messages = await aiConversations.listTurnMessages({
1229
+ conversationId: conversation.id,
1230
+ loopId: turn.id,
1231
+ includeCompacted: true,
1232
+ });
1233
+ const message = messages.find(
1234
+ ({ message }) =>
1235
+ message.role === "assistant" && message.content.some((block) => block.type === "tool_call" && block.id === callId),
1236
+ );
1237
+ if (!message) return notFound(c);
1238
+ const review = message.meta?.toolReviews?.[callId];
1239
+ const previous = message.meta?.toolChecks?.[callId];
1240
+ if (!review || !("sentence" in review) || !review.check) return respond(c, fail(err.conflict("This call has no outcome check.")));
1241
+ if (!previous) {
1242
+ // An unknown tool result ends executor recovery for this call, even while its turn is live. Without
1243
+ // a result, only a terminal cut-off call may be checked. Both paths require evidence of dispatch.
1244
+ const terminal = turn.status === "completed" || turn.status === "failed" || turn.status === "aborted";
1245
+ const result = messages.find(({ message }) => message.role === "tool_result" && message.callId === callId)?.message;
1246
+ const eligible = result
1247
+ ? result.role === "tool_result" && result.isError === true && isUnknownActionOutcome(result.result)
1248
+ : terminal;
1249
+ if (!eligible || !(await aiToolAudit.hasCapabilityDispatch(target)))
1250
+ return respond(c, fail(err.conflict("This call has no outcome check.")));
1251
+ }
1252
+ // A settled check answers again as it was stored, so a client whose last response got lost can recover
1253
+ // without running the Query again.
1254
+ if (previous?.state === "done" || previous?.state === "not_done") return respond(c, ok(previous));
1255
+ const block =
1256
+ message.message.role === "assistant"
1257
+ ? message.message.content.find((block) => block.type === "tool_call" && block.id === callId)
1258
+ : undefined;
1259
+ const catalog = buildAiCapabilityCatalog(await listCapabilities());
1260
+ const presentation = message.meta?.toolPresentations?.[callId];
1261
+ const entry = catalog.find(
1262
+ (entry) =>
1263
+ entry.kind === "action" &&
1264
+ (entry.name === (block?.type === "tool_call" ? block.name : undefined) ||
1265
+ entry.providerName === (block?.type === "tool_call" ? block.name : undefined)) &&
1266
+ (!presentation || presentation.appId === entry.appId),
1267
+ );
1268
+ if (!entry) return respond(c, fail(err.conflict("The Action's app is unavailable. Try the check again when it returns.")));
1269
+ const authority = await resolveAiCapabilityActor({
1270
+ conversationId: conversation.id,
1271
+ persistedActor: ctx.actor,
1272
+ store: aiConversations,
1273
+ });
1274
+ // One check at a time per call, across Core replicas and the executor: the claim is one conditional write,
1275
+ // and no transaction stays open while the app's Query runs.
1276
+ const claimedAt = new Date().toISOString();
1277
+ const claimed = await claimAiToolCheck({
1278
+ ...target,
1279
+ first: !previous,
1280
+ at: claimedAt,
1281
+ staleBefore: new Date(Date.now() - AI_TOOL_CHECK_STALE_MS).toISOString(),
1282
+ });
1283
+ if (!claimed) {
1284
+ const stored = await readAiToolCheck(target);
1285
+ if (stored?.state === "done" || stored?.state === "not_done") return respond(c, ok(stored));
1286
+ return respond(c, fail(err.conflict("An outcome check is already running for this call.")));
1287
+ }
1288
+ const answered = await checkAiCapability({
1289
+ ...target,
1290
+ entry,
1291
+ check: review.check,
1292
+ catalog,
1293
+ authority,
1294
+ // The receipt joins the stored review's sentence, so it is asked for in that language.
1295
+ locale: review.locale,
1296
+ context: { callId, signal: c.req.raw.signal },
1297
+ });
1298
+ if (await settleAiToolCheck({ ...target, claimedAt, check: answered })) return respond(c, ok(answered));
1299
+ // The claim went stale and a newer check took over; its stored result is the answer.
1300
+ const stored = await readAiToolCheck(target);
1301
+ return stored && stored.state !== "checking"
1302
+ ? respond(c, ok(stored))
1303
+ : respond(c, fail(err.conflict("An outcome check is already running for this call.")));
1304
+ } finally {
1305
+ setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_SECONDS);
1306
+ }
1307
+ })
1161
1308
  .post(
1162
1309
  "/conversations/:conversationId/turns/:turnId/actions/:callId",
1163
1310
  bodyLimit({ maxSize: AI_CLIENT_TOOL_ACTION_MAX_BYTES }),
@@ -1208,8 +1355,51 @@ export const aiRoutes = (() => {
1208
1355
  if (!conversation) return notFound(c);
1209
1356
  const turn = await loadTurn(conversation, c.req.param("turnId"));
1210
1357
  if (!turn) return notFound(c);
1211
- const actions = await listPendingAiTurnActions({ conversationId: conversation.id, turnId: turn.id });
1212
- return respond(c, ok(actions.map((action) => publicPendingAction(action, conversation.shortId, turn.shortId))));
1358
+ const scope = { conversationId: conversation.id, turnId: turn.id };
1359
+ const actions = await listPendingAiTurnActions(scope);
1360
+ if (!actions.some((action) => action.type === "approval_request"))
1361
+ return respond(c, ok(actions.map((action) => publicPendingAction(action, conversation.shortId, turn.shortId))));
1362
+ const [records, reviews, config] = await Promise.all([
1363
+ aiConversations.listPendingActionRecords(scope),
1364
+ aiConversations.listTurnToolReviews(scope),
1365
+ aiConversations.getTurnRunConfig(scope),
1366
+ ]);
1367
+ const locale = config?.kind !== "compact" ? (config?.locale ?? getLocale(c)) : getLocale(c);
1368
+ let capabilities: Promise<Awaited<ReturnType<typeof listCapabilities>> | null> | undefined;
1369
+ const presented = await Promise.all(
1370
+ actions.map(async (action) => {
1371
+ const result = publicPendingAction(action, conversation.shortId, turn.shortId);
1372
+ if (action.type !== "approval_request") return result;
1373
+ const record = records.find((record) => record.callId === action.callId);
1374
+ const request = record?.approvalId ? await approvals.get(record.approvalId) : null;
1375
+ const reviewCallId = record?.kind === "custom_approval" ? action.callId.replace(/-approval-[0-9]+$/, "") : action.callId;
1376
+ const stored = reviews.get(reviewCallId);
1377
+ let review: AiStoredActionReview | null = stored && !isLegacyActionReview(stored) ? stored : null;
1378
+ if (!review && request) {
1379
+ review = pendingApprovalReview({ storedReview: null, details: request.details });
1380
+ if (review && !isLegacyActionReview(review) && !review.ask) {
1381
+ // As the approvals page: the declared reason when the review names none, read once per listing.
1382
+ capabilities ??= listCapabilities().catch(() => null);
1383
+ const manifest = (await capabilities)?.find((entry) => entry.appId === request.appId)?.manifest;
1384
+ review = storedActionReview(review, manifest ? capabilityActionPolicy(manifest, request.action) : undefined);
1385
+ }
1386
+ }
1387
+ const appId = request?.appId;
1388
+ // The name is presentation: a registry that cannot answer leaves the app ID, never the waiting approvals unread.
1389
+ const app = appId && appId !== "assistant" ? await getApp(appId).catch(() => null) : null;
1390
+ return {
1391
+ ...result,
1392
+ ...(review && !isLegacyActionReview(review) ? { review } : {}),
1393
+ app: {
1394
+ id: appId ?? "assistant",
1395
+ name: app ? resolveAppIdentityPresentation(app, locale).name : appId && appId !== "assistant" ? appId : "Cloud",
1396
+ },
1397
+ chatTitle: conversation.title,
1398
+ ...(request ? { expiresAt: request.expiresAt.toISOString() } : {}),
1399
+ };
1400
+ }),
1401
+ );
1402
+ return respond(c, ok(presented));
1213
1403
  })
1214
1404
  .get("/conversations/:conversationId/stream", async (c) => {
1215
1405
  const ctx = await resolveContext(c);
package/src/ai/runtime.ts CHANGED
@@ -47,6 +47,7 @@ import type {
47
47
  import { isAiImageMediaType } from "./types";
48
48
  import { validateAiTurnRequest } from "./validate";
49
49
 
50
+ export { createAiTurnCodeAndClientTools } from "./default-tools";
50
51
  export type { ValidateAiTurnInput } from "./validate";
51
52
  export { isAiSettingsError, validateAiTurnRequest } from "./validate";
52
53
 
@@ -3,6 +3,7 @@ import { HTTPException } from "hono/http-exception";
3
3
  import { z } from "zod";
4
4
  import { AuthenticatedPrincipalSchema } from "../contracts/shared";
5
5
  import { type AuthContext, auth, err, fail, ok, rateLimit, respond, v } from "../server";
6
+ import { noTransform } from "../server/middleware/no-transform";
6
7
  import { localizedAppName } from "../ssr/runtime";
7
8
  import { aiProjects } from "./projects";
8
9
  import {
@@ -63,6 +64,8 @@ type AiSkillsRouteDependencies = {
63
64
 
64
65
  const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
65
66
  new Hono<AuthContext>()
67
+ // The list combines skills from separate grants; the writer of one can rename it next to another skill's text.
68
+ .use("*", noTransform())
66
69
  .onError((error, c) => {
67
70
  if (error instanceof AiSkillAppForbiddenError) return respond(c, fail(err.forbidden(error.message)));
68
71
  if (error instanceof HTTPException) return error.getResponse();
package/src/ai/skills.ts CHANGED
@@ -522,7 +522,15 @@ const skillSnapshotFiles = (skill: AiSkill): { path: string; content: string }[]
522
522
  ...skill.references.map((reference) => ({ path: `${skill.name}/${reference.path}`, content: reference.content })),
523
523
  ];
524
524
 
525
- type SkillListOptions = { name?: string; query?: string; enabledOnly?: boolean; modelInvocableOnly?: boolean; limit?: number };
525
+ type SkillListOptions = {
526
+ name?: string;
527
+ /** Only these Skills, by short ID, such as the ones a person recently sent. */
528
+ shortIds?: readonly string[];
529
+ query?: string;
530
+ enabledOnly?: boolean;
531
+ modelInvocableOnly?: boolean;
532
+ limit?: number;
533
+ };
526
534
  async function listSkillSummaries(
527
535
  subject: AccessSubject | null,
528
536
  options: SkillListOptions & {
@@ -551,6 +559,7 @@ async function listSkillSummaries(
551
559
  ON disabled.skill_id = skill.id AND disabled.user_id = ${userId}::uuid
552
560
  WHERE access.permission <> 'none'
553
561
  AND ${options.name === undefined ? sql`true` : sql`skill.name = ${options.name}`}
562
+ AND ${options.shortIds === undefined ? sql`true` : sql`skill.short_id = ANY(${toPgTextArray([...options.shortIds])}::text[])`}
554
563
  AND (${!options.manageOnly} OR access.permission='admin')
555
564
  AND ${options.project ? sql`EXISTS(SELECT 1 FROM ai.project_skills link WHERE link.skill_id=skill.id AND link.project_id=${options.project.id}::uuid) = ${options.project.linked}` : sql`true`}
556
565
  AND (${!options.modelInvocableOnly} OR NOT manual_only)