@k2b/cloud 0.29.1 → 0.31.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 (136) hide show
  1. package/package.json +4 -2
  2. package/src/_internal/app-presentation.ts +22 -1
  3. package/src/_internal/app-roles.ts +14 -0
  4. package/src/_internal/capabilities.ts +113 -5
  5. package/src/_internal/capability-sentences.ts +413 -0
  6. package/src/_internal/define-app.ts +24 -5
  7. package/src/_internal/registry-validation.ts +5 -0
  8. package/src/_internal/registry.ts +50 -6
  9. package/src/_internal/widget-declarations.ts +53 -0
  10. package/src/access/PermissionEditor.tsx +43 -24
  11. package/src/ai/app-skill-store.ts +298 -0
  12. package/src/ai/app-skills.ts +105 -0
  13. package/src/ai/approval-routes.ts +40 -4
  14. package/src/ai/approvals.ts +68 -10
  15. package/src/ai/browser.ts +10 -0
  16. package/src/ai/capabilities.ts +68 -9
  17. package/src/ai/chart-block.ts +380 -0
  18. package/src/ai/chat/action-sentences.ts +49 -0
  19. package/src/ai/chat/blocks.tsx +97 -40
  20. package/src/ai/chat/chart-block.tsx +79 -0
  21. package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
  22. package/src/ai/chat/message-actions.tsx +3 -1
  23. package/src/ai/chat/message-utils.ts +3 -0
  24. package/src/ai/chat/messages.ts +26 -2
  25. package/src/ai/chat/tool-groups.ts +66 -12
  26. package/src/ai/chat/tool-names.ts +6 -2
  27. package/src/ai/chat/turn-layout.ts +10 -2
  28. package/src/ai/chat/turn-view.tsx +123 -36
  29. package/src/ai/chat/web-tools.tsx +95 -1
  30. package/src/ai/client/controller.ts +6 -3
  31. package/src/ai/code-runtime-tools.ts +109 -61
  32. package/src/ai/code-source-contracts.ts +25 -21
  33. package/src/ai/code-source-tools.ts +3 -1
  34. package/src/ai/default-tool-contracts.ts +198 -0
  35. package/src/ai/default-tools.ts +25 -1
  36. package/src/ai/executor.ts +189 -35
  37. package/src/ai/fetch-file-tool.ts +41 -4
  38. package/src/ai/firecrawl-tools.ts +87 -4
  39. package/src/ai/index.ts +17 -1
  40. package/src/ai/migrate.ts +172 -14
  41. package/src/ai/protocol.ts +22 -1
  42. package/src/ai/routes.ts +11 -0
  43. package/src/ai/runtime-tools.ts +1 -0
  44. package/src/ai/runtime.ts +46 -7
  45. package/src/ai/skill-content.ts +54 -0
  46. package/src/ai/skills-routes.ts +22 -13
  47. package/src/ai/skills.ts +177 -231
  48. package/src/ai/store.ts +66 -1
  49. package/src/ai/tool-audit.ts +11 -1
  50. package/src/ai/tool-sentences.ts +38 -0
  51. package/src/ai/tools.ts +91 -3
  52. package/src/ai/types.ts +47 -1
  53. package/src/ai/web-provenance.ts +103 -0
  54. package/src/api/admin-ai-skills.ts +35 -16
  55. package/src/api/capabilities.ts +3 -2
  56. package/src/api/fanout.ts +128 -0
  57. package/src/api/help.ts +8 -3
  58. package/src/api/mcp.ts +4 -1
  59. package/src/api/search.ts +31 -106
  60. package/src/api/widgets.ts +291 -101
  61. package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
  62. package/src/browser/CloudResourceSearch.tsx +1 -1
  63. package/src/browser/command-shortcuts.ts +21 -6
  64. package/src/browser/ndjson.ts +34 -0
  65. package/src/browser/reload.ts +46 -0
  66. package/src/browser/search-stream.ts +4 -26
  67. package/src/browser/spotlight-position.ts +4 -1
  68. package/src/browser/widgets.ts +30 -0
  69. package/src/capabilities/executions.ts +3 -0
  70. package/src/capabilities/server.ts +20 -2
  71. package/src/cli/access.ts +48 -14
  72. package/src/cli/admin/ai-skills.ts +59 -39
  73. package/src/cli/commands.ts +20 -0
  74. package/src/cli/index.ts +16 -0
  75. package/src/cli/plugin.ts +30 -0
  76. package/src/contracts/announcement-cookie.ts +62 -0
  77. package/src/contracts/announcements.ts +14 -52
  78. package/src/contracts/app.ts +34 -2
  79. package/src/contracts/capabilities.ts +24 -0
  80. package/src/contracts/pwa-paths.ts +7 -2
  81. package/src/contracts/pwa.ts +2 -7
  82. package/src/contracts/registry.ts +10 -1
  83. package/src/contracts/shared.ts +5 -0
  84. package/src/contracts/widgets.ts +67 -2
  85. package/src/desktop/solid.tsx +6 -7
  86. package/src/index.ts +1 -0
  87. package/src/server/help.ts +8 -1
  88. package/src/server/index.ts +1 -0
  89. package/src/server/widget-request.ts +27 -0
  90. package/src/services/accounts/users.ts +15 -0
  91. package/src/services/approvals/index.ts +225 -0
  92. package/src/services/approvals/migrate.ts +33 -0
  93. package/src/services/help/index.ts +18 -5
  94. package/src/services/help/types.ts +3 -2
  95. package/src/services/identity/constants.ts +2 -0
  96. package/src/services/identity/key-ring.ts +123 -42
  97. package/src/services/identity/runtime-config.ts +13 -2
  98. package/src/shared/app-presentation.ts +11 -0
  99. package/src/shared/email-html.ts +43 -8
  100. package/src/ssr/AdminLayout.tsx +4 -7
  101. package/src/ssr/AppLaunchpad.island.tsx +1 -1
  102. package/src/ssr/AppLaunchpadPanel.tsx +12 -3
  103. package/src/ssr/GlobalAnnouncements.island.tsx +2 -2
  104. package/src/ssr/GlobalSearchDialog.tsx +1 -43
  105. package/src/ssr/GlobalSearchTrigger.island.tsx +12 -3
  106. package/src/ssr/Layout.tsx +38 -14
  107. package/src/ssr/LayoutFooter.tsx +1 -1
  108. package/src/ssr/LayoutHeader.tsx +17 -20
  109. package/src/ssr/LayoutHelp.tsx +1 -1
  110. package/src/ssr/LayoutHelpTrigger.island.tsx +1 -1
  111. package/src/ssr/LayoutRail.tsx +1 -2
  112. package/src/ssr/PageTrail.tsx +23 -0
  113. package/src/ssr/PageTrailRow.island.tsx +20 -0
  114. package/src/ssr/PageTrailRuntime.island.tsx +26 -0
  115. package/src/ssr/ProfilePreferences.island.tsx +7 -3
  116. package/src/ssr/PwaRuntime.island.tsx +1 -1
  117. package/src/ssr/app-navigation.ts +5 -7
  118. package/src/ssr/global-search-host.tsx +55 -0
  119. package/src/ssr/help.ts +3 -3
  120. package/src/ssr/islands/index.ts +1 -0
  121. package/src/ssr/layout-runtime.ts +60 -0
  122. package/src/ssr/mobile-menu-history.ts +1 -1
  123. package/src/ssr/page-trail-runtime.ts +313 -0
  124. package/src/ssr/page-trail.browser-harness.tsx +216 -0
  125. package/src/ssr/page-trail.ts +59 -0
  126. package/src/ssr/platform-messages.ts +4 -0
  127. package/src/ssr/rail-context.ts +4 -3
  128. package/src/ssr/runtime.ts +14 -1
  129. package/src/styles/effects.css +21 -4
  130. package/src/styles/global.css +35 -2
  131. package/src/styles/utilities-layout.css +24 -1
  132. package/src/styles/utilities-navigation.css +6 -18
  133. package/src/ai/code-mode-skill.ts +0 -114
  134. package/src/ai/data-analysis-skill.ts +0 -10
  135. package/src/ai/grids-skill.ts +0 -158
  136. package/src/ai/skill-seeds.ts +0 -560
@@ -5,8 +5,15 @@ import { listCapabilities } from "../_internal/registry";
5
5
  import { ErrorResponseSchema } from "../contracts";
6
6
  import type { CapabilityRegistryEntry } from "../contracts/registry";
7
7
  import { type AuthContext, auth, expectUserBackedActor, getLocale, jsonResponse, rateLimit, requiresAuth, v } from "../server";
8
- import { type AiToolApprovalPreference, listAiToolApprovalPreferences, revokeAiToolApprovalPreference } from "./approvals";
8
+ import {
9
+ AI_WEBSITE_APPROVAL_TOOL,
10
+ type AiToolApprovalPreference,
11
+ listAiToolApprovalPreferences,
12
+ parseAiWebsiteApprovalScope,
13
+ revokeAiToolApprovalPreference,
14
+ } from "./approvals";
9
15
  import { buildAiCapabilityCatalog } from "./capabilities";
16
+ import { aiConversations } from "./store";
10
17
 
11
18
  export type AiApprovalPreferenceView = AiToolApprovalPreference & {
12
19
  title: string;
@@ -16,6 +23,8 @@ export type AiApprovalPreferenceView = AiToolApprovalPreference & {
16
23
  icon: string;
17
24
  accent?: string;
18
25
  } | null;
26
+ /** For a website approval: the exact origin it lets code read, and the Studio app it is limited to, if any. */
27
+ website: { origin: string; resourceId: string | null } | null;
19
28
  };
20
29
 
21
30
  const AiApprovalPreferenceSchema = z.object({
@@ -24,6 +33,7 @@ const AiApprovalPreferenceSchema = z.object({
24
33
  approvalScope: z.string(),
25
34
  createdAt: z.iso.datetime(),
26
35
  lastUsedAt: z.iso.datetime().nullable(),
36
+ conversationId: z.uuid().nullable(),
27
37
  expiresAt: z.iso.datetime().nullable(),
28
38
  title: z.string(),
29
39
  app: z
@@ -34,20 +44,29 @@ const AiApprovalPreferenceSchema = z.object({
34
44
  accent: z.string().optional(),
35
45
  })
36
46
  .nullable(),
47
+ website: z.object({ origin: z.string(), resourceId: z.string().nullable() }).nullable(),
37
48
  });
38
49
 
39
50
  const AiApprovalPreferenceListSchema = z.object({ approvals: z.array(AiApprovalPreferenceSchema) });
40
51
  const AiApprovalPreferenceParamSchema = z.object({ preferenceId: z.uuid() });
52
+ const AiApprovalPreferenceQuerySchema = z.object({ conversation: z.string().min(1).max(80).optional() });
41
53
  const AiApprovalPreferenceDeleteSchema = z.object({ deleted: z.literal(true) });
42
54
 
43
55
  type AiApprovalRouteDependencies = {
44
56
  limit?: MiddlewareHandler<AuthContext>;
45
57
  authenticate?: MiddlewareHandler<AuthContext>;
46
- listPreferences?: (actorUserId: string) => Promise<AiToolApprovalPreference[]>;
58
+ listPreferences?: (actorUserId: string, options: { conversationId?: string }) => Promise<AiToolApprovalPreference[]>;
47
59
  revokePreference?: (actorUserId: string, preferenceId: string) => Promise<boolean>;
48
60
  listCapabilities?: () => Promise<CapabilityRegistryEntry[]>;
61
+ /** Resolves a chat the user owns by its ID or short ID. */
62
+ findConversation?: (ownerUserId: string, conversation: string) => Promise<{ id: string } | null>;
49
63
  };
50
64
 
65
+ const findOwnConversation = async (ownerUserId: string, conversation: string) =>
66
+ z.uuid().safeParse(conversation).success
67
+ ? aiConversations.getConversation({ conversationId: conversation, ownerUserId })
68
+ : aiConversations.getConversationByShortId({ shortId: conversation, ownerUserId });
69
+
51
70
  const fallbackTitle = (toolName: string): string => {
52
71
  const label = toolName
53
72
  .replaceAll("__", " ")
@@ -61,6 +80,7 @@ export const createAiApprovalPreferenceRoutes = (dependencies: AiApprovalRouteDe
61
80
  const listPreferences = dependencies.listPreferences ?? listAiToolApprovalPreferences;
62
81
  const revokePreference = dependencies.revokePreference ?? revokeAiToolApprovalPreference;
63
82
  const registry = dependencies.listCapabilities ?? listCapabilities;
83
+ const findConversation = dependencies.findConversation ?? findOwnConversation;
64
84
 
65
85
  return new Hono<AuthContext>()
66
86
  .use(dependencies.limit ?? rateLimit())
@@ -70,25 +90,41 @@ export const createAiApprovalPreferenceRoutes = (dependencies: AiApprovalRouteDe
70
90
  describeRoute({
71
91
  tags: ["AI"],
72
92
  summary: "List remembered AI approvals",
73
- description: "Lists AI tool approvals remembered for the current user, with titles in the request locale.",
93
+ description:
94
+ "Lists AI tool approvals remembered for the current user, with titles in the request locale. Without `conversation`, it lists the approvals that apply everywhere; with it, those limited to that chat.",
74
95
  ...requiresAuth,
75
96
  responses: {
76
97
  200: jsonResponse(AiApprovalPreferenceListSchema, "Remembered approvals"),
77
98
  401: jsonResponse(ErrorResponseSchema, "Authentication required"),
78
99
  403: jsonResponse(ErrorResponseSchema, "User-backed actor required"),
100
+ 404: jsonResponse(ErrorResponseSchema, "Chat not found"),
79
101
  },
80
102
  }),
103
+ v("query", AiApprovalPreferenceQuerySchema),
81
104
  async (c) => {
82
105
  const user = expectUserBackedActor(c);
106
+ const requested = c.req.valid("query").conversation;
107
+ const conversation = requested ? await findConversation(user.id, requested) : null;
108
+ if (requested && !conversation) return c.json({ message: "Chat not found", code: "NOT_FOUND" }, 404);
83
109
  const [preferences, capabilities] = await Promise.all([
84
- listPreferences(user.id),
110
+ listPreferences(user.id, conversation ? { conversationId: conversation.id } : {}),
85
111
  registry().catch(() => [] as CapabilityRegistryEntry[]),
86
112
  ]);
87
113
  const catalogByName = new Map(buildAiCapabilityCatalog(capabilities, getLocale(c)).map((entry) => [entry.name, entry]));
88
114
  const approvals: AiApprovalPreferenceView[] = preferences.map((preference) => {
115
+ if (preference.toolName === AI_WEBSITE_APPROVAL_TOOL) {
116
+ const website = parseAiWebsiteApprovalScope(preference.approvalScope);
117
+ return {
118
+ ...preference,
119
+ title: URL.canParse(website.origin) ? new URL(website.origin).host : website.origin,
120
+ app: null,
121
+ website,
122
+ };
123
+ }
89
124
  const capability = catalogByName.get(preference.toolName);
90
125
  return {
91
126
  ...preference,
127
+ website: null,
92
128
  title: capability?.display.title ?? fallbackTitle(preference.toolName),
93
129
  app: capability
94
130
  ? {
@@ -5,14 +5,41 @@ import type { AiToolApprovalPolicy, AiTurnRunConfig } from "./types";
5
5
  export const aiTurnAllowsRememberedApprovals = (config: AiTurnRunConfig | null | undefined): boolean =>
6
6
  config == null || config.kind === "compact" || config.mandate === undefined;
7
7
 
8
+ /**
9
+ * Reserved remembered-approval name for reading one website in one chat or one managed app. No capability or tool can
10
+ * collide with it: tool names never contain a colon.
11
+ */
12
+ export const AI_WEBSITE_APPROVAL_TOOL = "website:read";
13
+
14
+ /** The remembered scope of a website approval: one exact origin, in a chat or for one managed Studio app. */
15
+ export const aiWebsiteApprovalScope = (origin: string, resourceId?: string): string =>
16
+ resourceId ? `${origin} resource:${resourceId}` : origin;
17
+
18
+ export const parseAiWebsiteApprovalScope = (scope: string): { origin: string; resourceId: string | null } => {
19
+ const [origin = scope, resource] = scope.split(" ");
20
+ return { origin, resourceId: resource?.startsWith("resource:") ? resource.slice("resource:".length) : null };
21
+ };
22
+
23
+ /**
24
+ * Website approvals apply only to a turn a person started in a signed-in browser session: never to a scheduled task,
25
+ * a mandate, `cld`, an API key, or another delegated credential.
26
+ */
27
+ export const aiTurnAllowsWebsiteApprovals = (config: AiTurnRunConfig | null | undefined): boolean =>
28
+ config != null && config.kind !== "compact" && config.signedInSession === true && aiTurnAllowsRememberedApprovals(config);
29
+
8
30
  export type AiToolApprovalContext = {
9
31
  actorUserId: string;
10
32
  };
11
33
 
34
+ /** Where a remembered approval applies: in one chat until it is deleted, or always. */
35
+ export type AiToolApprovalRemember = "chat" | "always";
36
+
12
37
  export type AiToolApprovalPreference = {
13
38
  id: string;
14
39
  toolName: string;
15
40
  approvalScope: string;
41
+ /** The chat this approval is limited to; null for an approval that applies everywhere. */
42
+ conversationId: string | null;
16
43
  createdAt: string;
17
44
  lastUsedAt: string | null;
18
45
  expiresAt: string | null;
@@ -22,6 +49,7 @@ type AiToolApprovalPreferenceRow = {
22
49
  id: string;
23
50
  tool_name: string;
24
51
  approval_scope: string;
52
+ conversation_id: string | null;
25
53
  created_at: Date | string;
26
54
  last_used_at: Date | string | null;
27
55
  expires_at: Date | string | null;
@@ -37,36 +65,52 @@ export const aiToolNeedsApproval = (policy: AiToolApprovalPolicy | undefined): b
37
65
  export const aiToolAllowsAlways = (policy: AiToolApprovalPolicy | undefined): boolean =>
38
66
  policy === "always" || (typeof policy === "object" && policy.kind === "user-configurable");
39
67
 
40
- export const hasRememberedAiToolApproval = async (
68
+ /**
69
+ * The remembered approval that covers this tool and scope: one that applies everywhere, or one limited to
70
+ * `conversationId`; with `chatOnly`, only the latter. Returns its ID and marks it used.
71
+ */
72
+ export const findRememberedAiToolApproval = async (
41
73
  context: AiToolApprovalContext,
42
- input: { toolName: string; approvalScope: string },
43
- ): Promise<boolean> => {
74
+ input: { toolName: string; approvalScope: string; conversationId?: string | null; chatOnly?: boolean },
75
+ ): Promise<string | null> => {
76
+ const conversationId = input.conversationId ?? null;
77
+ if (input.chatOnly && !conversationId) return null;
44
78
  const rows = await sql<{ id: string }[]>`
45
79
  SELECT id
46
80
  FROM ai.tool_approval_preferences
47
81
  WHERE actor_user_id = ${context.actorUserId}
48
82
  AND tool_name = ${input.toolName}
49
83
  AND approval_scope = ${input.approvalScope}
84
+ AND (conversation_id = ${conversationId}::uuid OR (conversation_id IS NULL AND NOT ${input.chatOnly === true}))
50
85
  AND (expires_at IS NULL OR expires_at > now())
86
+ ORDER BY conversation_id NULLS LAST
51
87
  LIMIT 1
52
88
  `;
53
89
 
54
90
  const id = rows[0]?.id;
55
- if (!id) return false;
91
+ if (!id) return null;
56
92
  await sql`UPDATE ai.tool_approval_preferences SET last_used_at = now() WHERE id = ${id}`;
57
- return true;
93
+ return id;
58
94
  };
59
95
 
96
+ export const hasRememberedAiToolApproval = async (
97
+ context: AiToolApprovalContext,
98
+ input: { toolName: string; approvalScope: string; conversationId?: string | null },
99
+ ): Promise<boolean> => (await findRememberedAiToolApproval(context, input)) !== null;
100
+
101
+ /** Remembers an approval everywhere, or in one chat when `conversationId` is set; a chat approval ends with its chat. */
60
102
  export const rememberAiToolApproval = async (
61
103
  context: AiToolApprovalContext,
62
- input: { toolName: string; approvalScope: string; expiresAt?: Date | null },
104
+ input: { toolName: string; approvalScope: string; conversationId?: string | null; expiresAt?: Date | null },
63
105
  ): Promise<void> => {
106
+ const conversationId = input.conversationId ?? null;
64
107
  await sql.begin(async () => {
65
108
  await sql`
66
109
  DELETE FROM ai.tool_approval_preferences
67
110
  WHERE actor_user_id = ${context.actorUserId}
68
111
  AND tool_name = ${input.toolName}
69
112
  AND approval_scope = ${input.approvalScope}
113
+ AND conversation_id IS NOT DISTINCT FROM ${conversationId}::uuid
70
114
  `;
71
115
 
72
116
  await sql`
@@ -74,6 +118,7 @@ export const rememberAiToolApproval = async (
74
118
  actor_user_id,
75
119
  tool_name,
76
120
  approval_scope,
121
+ conversation_id,
77
122
  last_used_at,
78
123
  expires_at
79
124
  )
@@ -81,6 +126,7 @@ export const rememberAiToolApproval = async (
81
126
  ${context.actorUserId},
82
127
  ${input.toolName},
83
128
  ${input.approvalScope},
129
+ ${conversationId}::uuid,
84
130
  now(),
85
131
  ${input.expiresAt ?? null}
86
132
  )
@@ -88,16 +134,20 @@ export const rememberAiToolApproval = async (
88
134
  });
89
135
  };
90
136
 
137
+ /** Forgets the approval with exactly this reach: everywhere, or in the one chat `conversationId` names. */
91
138
  export const forgetAiToolApproval = async (
92
139
  context: AiToolApprovalContext,
93
- input: { toolName: string; approvalScope: string },
94
- ): Promise<void> => {
95
- await sql`
140
+ input: { toolName: string; approvalScope: string; conversationId?: string | null },
141
+ ): Promise<boolean> => {
142
+ const rows = await sql<{ id: string }[]>`
96
143
  DELETE FROM ai.tool_approval_preferences
97
144
  WHERE actor_user_id = ${context.actorUserId}
98
145
  AND tool_name = ${input.toolName}
99
146
  AND approval_scope = ${input.approvalScope}
147
+ AND conversation_id IS NOT DISTINCT FROM ${input.conversationId ?? null}::uuid
148
+ RETURNING id
100
149
  `;
150
+ return rows.length > 0;
101
151
  };
102
152
 
103
153
  const toIsoString = (value: Date | string): string => (value instanceof Date ? value : new Date(value)).toISOString();
@@ -106,22 +156,30 @@ const approvalPreferenceFromRow = (row: AiToolApprovalPreferenceRow): AiToolAppr
106
156
  id: row.id,
107
157
  toolName: row.tool_name,
108
158
  approvalScope: row.approval_scope,
159
+ conversationId: row.conversation_id,
109
160
  createdAt: toIsoString(row.created_at),
110
161
  lastUsedAt: row.last_used_at ? toIsoString(row.last_used_at) : null,
111
162
  expiresAt: row.expires_at ? toIsoString(row.expires_at) : null,
112
163
  });
113
164
 
114
- export const listAiToolApprovalPreferences = async (actorUserId: string): Promise<AiToolApprovalPreference[]> => {
165
+ /** Lists the approvals that apply everywhere, or only those of one chat when `conversationId` is set. */
166
+ export const listAiToolApprovalPreferences = async (
167
+ actorUserId: string,
168
+ options: { conversationId?: string } = {},
169
+ ): Promise<AiToolApprovalPreference[]> => {
170
+ const conversationId = options.conversationId ?? null;
115
171
  const rows = await sql<AiToolApprovalPreferenceRow[]>`
116
172
  SELECT
117
173
  id,
118
174
  tool_name,
119
175
  approval_scope,
176
+ conversation_id,
120
177
  created_at,
121
178
  last_used_at,
122
179
  expires_at
123
180
  FROM ai.tool_approval_preferences
124
181
  WHERE actor_user_id = ${actorUserId}
182
+ AND conversation_id IS NOT DISTINCT FROM ${conversationId}::uuid
125
183
  AND (expires_at IS NULL OR expires_at > now())
126
184
  ORDER BY last_used_at DESC NULLS LAST, created_at DESC, id
127
185
  `;
package/src/ai/browser.ts CHANGED
@@ -13,13 +13,23 @@ export { type AiTodoPlan, parseAiTodoPlan } from "./todo-contracts";
13
13
  import { AI_TURN_ATTACHMENT_MAX_ITEMS } from "./limits";
14
14
  import type { AiConversation, AiDraftContentPart } from "./types";
15
15
 
16
+ export {
17
+ type CloudAiChartColumn,
18
+ type CloudAiChartRow,
19
+ type CloudAiChartTable,
20
+ cloudAiChartTable,
21
+ } from "./chart-block";
16
22
  export { parseAiSse } from "./client/transport";
17
23
  export {
24
+ CLOUD_AI_CHART_MAX_SERIES,
25
+ CLOUD_AI_CHART_MAX_VALUES,
18
26
  CLOUD_AI_TEXT_EDITOR_MAX_CHARS,
19
27
  CloudAiCardInputSchema,
28
+ CloudAiChartInputSchema,
20
29
  CloudAiLocalBashInputSchema,
21
30
  CloudAiSurveyInputSchema,
22
31
  CloudAiTextEditorInputSchema,
32
+ parseCloudAiChartInput,
23
33
  } from "./default-tool-contracts";
24
34
  export { guessAiMediaType } from "./file-media-type";
25
35
  export { AI_IMAGE_INPUT_MAX_BYTES, AI_TURN_ATTACHMENT_MAX_ITEMS, AI_TURN_IMAGE_MAX_TOTAL_BYTES } from "./limits";
@@ -1,7 +1,15 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { Tool, ToolContext, ToolResolver } from "@k2b/nessi";
3
3
  import { z } from "zod";
4
- import { resolveCapabilityOperationTitle } from "../_internal/capabilities";
4
+ import { resolveCapabilityActionWording, resolveCapabilityOperationTitle } from "../_internal/capabilities";
5
+ import {
6
+ CAPABILITY_APPROVAL_REASON_FIELD,
7
+ CAPABILITY_APPROVAL_REASON_MAX_CHARS,
8
+ type CapabilityActionWording,
9
+ capabilityActionSubject,
10
+ capabilityApprovalReason,
11
+ capabilityApprovalText,
12
+ } from "../_internal/capability-sentences";
5
13
  import { HELP_READ_MAX_CHARS, HELP_SEARCH_MAX_LIMIT, readHelpArticle } from "../_internal/help-catalog";
6
14
  import {
7
15
  type CapabilityActionManifest,
@@ -13,9 +21,10 @@ import {
13
21
  } from "../contracts/capabilities";
14
22
  import type { CapabilityRegistryEntry } from "../contracts/registry";
15
23
  import type { RequestActor } from "../server";
16
- import { createHelpReader, type HelpReaderFactory } from "../services/help";
24
+ import type { HelpReader, HelpReaderFactory } from "../services/help";
17
25
  import { type MandatePolicyV1, mandatePolicyCanPermitCapability } from "../services/mandates/policy";
18
26
  import { resolveAppIdentityPresentation } from "../shared/app-presentation";
27
+ import { DEFAULT_LOCALE } from "../shared/locale";
19
28
  import { recordRejectedAiCapability } from "./capability-execution";
20
29
  import { CODE_SOURCE_TOOLS } from "./code-source-contracts";
21
30
  import { createCodeSourceTool } from "./code-source-tools";
@@ -337,9 +346,7 @@ const AiHelpDocumentSchema = AiHelpCatalogItemSchema.extend({
337
346
  }).strict();
338
347
 
339
348
  /** Search and read published Help without loading one tool per article. */
340
- export const createAiHelpTools = (help: HelpReaderFactory = createHelpReader, locale = "en"): AiRuntimeTool[] => {
341
- const reader = help(locale);
342
-
349
+ export const createAiHelpTools = (reader: HelpReader): AiRuntimeTool[] => {
343
350
  const search = defineAiTool({
344
351
  name: "search_help",
345
352
  description:
@@ -453,6 +460,39 @@ export const reduceAiCapabilityInputSchema = (schema: Record<string, unknown>):
453
460
  export const aiCapabilityInputSchema = (schema: Record<string, unknown>): z.ZodType =>
454
461
  z.fromJSONSchema(reduceAiCapabilityInputSchema(schema));
455
462
 
463
+ /** How people read calls of one loaded Action in the turn's locale; derived from the live catalog only. */
464
+ export const aiCapabilityActionWording = (entry: AiCapabilityCatalogEntry, locale: string | undefined): CapabilityActionWording =>
465
+ resolveCapabilityActionWording(entry.operation as CapabilityActionManifest, entry.app.presentation, locale ?? DEFAULT_LOCALE);
466
+
467
+ /**
468
+ * An Action that waits for approval lets the model say why it wants it. The field exists only for the model:
469
+ * Cloud removes it before review, authorization, and execution. An app field of the same name wins.
470
+ */
471
+ const offersApprovalReason = (entry: AiCapabilityCatalogEntry): boolean => {
472
+ if (entry.kind !== "action" || (entry.operation as CapabilityActionManifest).approval === "none") return false;
473
+ const properties = entry.operation.inputSchema.properties;
474
+ return Boolean(properties && typeof properties === "object" && !Object.hasOwn(properties, CAPABILITY_APPROVAL_REASON_FIELD));
475
+ };
476
+
477
+ const withApprovalReason = (schema: Record<string, unknown>): Record<string, unknown> => ({
478
+ ...schema,
479
+ properties: {
480
+ ...(schema.properties as Record<string, unknown>),
481
+ [CAPABILITY_APPROVAL_REASON_FIELD]: {
482
+ type: "string",
483
+ maxLength: CAPABILITY_APPROVAL_REASON_MAX_CHARS,
484
+ description:
485
+ "Optional: one short sentence in the user's language on why this call is needed now. Shown as your reason beside the approval; never put Action input here.",
486
+ },
487
+ },
488
+ });
489
+
490
+ const withoutApprovalReason = (entry: AiCapabilityCatalogEntry, args: unknown): unknown => {
491
+ if (!offersApprovalReason(entry) || !args || typeof args !== "object" || Array.isArray(args)) return args;
492
+ const { [CAPABILITY_APPROVAL_REASON_FIELD]: _reason, ...appArgs } = args as Record<string, unknown>;
493
+ return appArgs;
494
+ };
495
+
456
496
  const ToolCatalogItemSchema = z
457
497
  .object({
458
498
  name: z.string(),
@@ -600,6 +640,8 @@ export const createLoadedAiCapabilityTools = (input: {
600
640
  catalog: readonly AiCapabilityCatalogEntry[];
601
641
  loadedNames: readonly string[];
602
642
  actor: RequestActor;
643
+ /** Locale of the turn; the approval text a person reads is worded in it. */
644
+ locale?: string;
603
645
  review?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<CapabilityActionReview | null>;
604
646
  authorizeBackground?: (entry: AiCapabilityCatalogEntry, args: unknown) => Promise<void>;
605
647
  onReview?: (callId: string, review: CapabilityActionReview) => void;
@@ -614,17 +656,28 @@ export const createLoadedAiCapabilityTools = (input: {
614
656
  name: entry.providerName,
615
657
  canonicalName: entry.name,
616
658
  description: `${entry.title}. ${entry.description} Never retry ACTION_OUTCOME_UNKNOWN. Do not retry unchanged after INTERNAL or INVALID_APP_RESPONSE; report the provider error.`,
617
- inputSchema: aiCapabilityInputSchema(entry.operation.inputSchema),
659
+ inputSchema: aiCapabilityInputSchema(
660
+ offersApprovalReason(entry) ? withApprovalReason(entry.operation.inputSchema) : entry.operation.inputSchema,
661
+ ),
618
662
  outputSchema: z.unknown(),
619
663
  // Capability Actions request a custom approval after their optional
620
664
  // live review has resolved. The review may supply an app-owned scope.
621
665
  approval: "never",
622
- }).server(async (args, context) => {
666
+ }).server(async (modelArgs, context) => {
667
+ const args = withoutApprovalReason(entry, modelArgs);
623
668
  await input.authorizeBackground?.(entry, args);
624
669
  if (!input.authorizeBackground && entry.kind === "action" && (entry.operation as CapabilityActionManifest).approval !== "none") {
625
670
  const review = (await input.review?.(entry, args, context)) ?? null;
626
671
  if (review && context.callId) input.onReview?.(context.callId, review);
627
- const message = review?.message ?? `${entry.display.appName}: ${entry.display.title}`;
672
+ // Text-only readers such as the CLI read what the card shows: the app's sentence, its review, and the model's reason.
673
+ const locale = input.locale ?? DEFAULT_LOCALE;
674
+ const message = capabilityApprovalText({
675
+ appName: entry.display.appName,
676
+ subject: capabilityActionSubject(aiCapabilityActionWording(entry, locale), args, { locale, timeZone: context.timeZone }),
677
+ review: review?.message,
678
+ reason: offersApprovalReason(entry) ? capabilityApprovalReason(modelArgs) : null,
679
+ locale,
680
+ });
628
681
  if (!(await context.requestApproval(message))) {
629
682
  if (input.actor.kind === "user") {
630
683
  await recordRejectedAiCapability({ entry, actor: input.actor, args }).catch(() => undefined);
@@ -743,7 +796,8 @@ export const createAiToolResolver =
743
796
  fullCapabilityCatalog.filter((entry) => !mandateAllows(entry)).flatMap((entry) => [entry.name, entry.providerName]),
744
797
  );
745
798
  const capabilityCatalog = fullCapabilityCatalog.filter((entry) => (!allowed || allowed.has(entry.name)) && mandateAllows(entry));
746
- const helpTools = input.help ? createAiHelpTools(input.help, input.locale) : [];
799
+ // The turn's actor decides which apps' Help exists for the model, like every other Help surface.
800
+ const helpTools = input.help ? createAiHelpTools(input.help(input.locale ?? "en", input.actor)) : [];
747
801
  const resourceTool =
748
802
  input.execute && (capabilityCatalog.length > 0 || input.staticTools.some((tool) => tool.def.name === "code_read"))
749
803
  ? createAiResourceReaderTool({ apps: registry, catalog: capabilityCatalog, execute: input.execute })
@@ -805,6 +859,7 @@ export const createAiToolResolver =
805
859
  catalog: capabilityCatalog,
806
860
  loadedNames,
807
861
  actor: input.actor,
862
+ locale: input.locale,
808
863
  review: input.review,
809
864
  onReview: input.onReview,
810
865
  authorizeBackground: input.authorizeBackground,
@@ -824,6 +879,7 @@ export const createAiToolResolver =
824
879
  for (const name of loadedNames) {
825
880
  const entry = catalogByName.get(name);
826
881
  if (!entry) continue;
882
+ const wording = entry.kind === "action" ? aiCapabilityActionWording(entry, input.locale) : null;
827
883
  presentations.set(entry.providerName, {
828
884
  kind: "capability",
829
885
  appId: entry.appId,
@@ -832,6 +888,9 @@ export const createAiToolResolver =
832
888
  appAccent: entry.app.appAccent,
833
889
  title: entry.display.title,
834
890
  capabilityKind: entry.kind,
891
+ ...(wording?.sentences ? { sentences: wording.sentences } : {}),
892
+ ...(wording?.fields?.length ? { fields: [...wording.fields] } : {}),
893
+ ...(offersApprovalReason(entry) ? { approvalReason: true as const } : {}),
835
894
  });
836
895
  // Rememberable scopes are resolved by the owning app for each concrete
837
896
  // call and attached when its live review completes.