@k2b/cloud 0.38.0 → 0.40.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 (244) hide show
  1. package/README.md +8 -0
  2. package/package.json +12 -5
  3. package/scripts/build-canvas-workers.ts +109 -29
  4. package/scripts/build.ts +3 -5
  5. package/src/_internal/app-favicon.ts +4 -2
  6. package/src/_internal/app-path.ts +25 -0
  7. package/src/_internal/app-presentation.ts +4 -0
  8. package/src/_internal/canvas-worker.ts +5 -3
  9. package/src/_internal/capabilities.ts +72 -48
  10. package/src/_internal/capability-reviews.ts +81 -0
  11. package/src/_internal/capability-schemas.ts +29 -0
  12. package/src/_internal/capability-sentences.ts +216 -1
  13. package/src/_internal/capability-streams.ts +7 -0
  14. package/src/_internal/define-app.ts +63 -16
  15. package/src/_internal/font-preloads.ts +2 -1
  16. package/src/_internal/panel-cookie.ts +39 -0
  17. package/src/_internal/registry-validation.ts +21 -26
  18. package/src/_internal/runtime-context.ts +1 -0
  19. package/src/_internal/runtime-watcher.ts +6 -0
  20. package/src/_internal/static-assets.ts +52 -3
  21. package/src/_internal/status-preserving-ssr.ts +2 -0
  22. package/src/access/PermissionEditor.browser-harness.tsx +57 -38
  23. package/src/access/PermissionEditor.tsx +32 -7
  24. package/src/access/ResourceApiKeys.tsx +5 -1
  25. package/src/account/EntitySearch.tsx +1 -1
  26. package/src/ai/action-review.ts +101 -0
  27. package/src/ai/approval-routes.ts +5 -2
  28. package/src/ai/approvals.ts +139 -28
  29. package/src/ai/background-review.ts +72 -0
  30. package/src/ai/browser.ts +10 -0
  31. package/src/ai/capabilities.ts +29 -11
  32. package/src/ai/capability-execution.ts +76 -43
  33. package/src/ai/capability-result.ts +95 -0
  34. package/src/ai/chat/action-card.ts +398 -0
  35. package/src/ai/chat/action-messages.ts +129 -0
  36. package/src/ai/chat/action-question.browser-harness.tsx +245 -0
  37. package/src/ai/chat/action-question.fixtures.ts +733 -0
  38. package/src/ai/chat/action-question.tsx +669 -0
  39. package/src/ai/chat/action-sentences.ts +1 -44
  40. package/src/ai/chat/blocks.tsx +19 -25
  41. package/src/ai/chat/message-actions.tsx +6 -0
  42. package/src/ai/chat/tool-action.tsx +481 -0
  43. package/src/ai/chat/turn-view.tsx +45 -163
  44. package/src/ai/chat/view-block.tsx +22 -7
  45. package/src/ai/chat-task-contracts.ts +31 -3
  46. package/src/ai/chat-task-reasons-migrate.ts +75 -0
  47. package/src/ai/chat-task-reasons.ts +92 -0
  48. package/src/ai/chat-tasks.ts +438 -78
  49. package/src/ai/client/controller.ts +59 -0
  50. package/src/ai/default-tools.ts +9 -1
  51. package/src/ai/executor.ts +190 -39
  52. package/src/ai/index.ts +14 -0
  53. package/src/ai/migrate.ts +39 -4
  54. package/src/ai/projects-routes.ts +3 -0
  55. package/src/ai/protocol.ts +14 -2
  56. package/src/ai/recent-resources.ts +240 -0
  57. package/src/ai/recent-sent-refs.ts +38 -0
  58. package/src/ai/request-context.ts +7 -1
  59. package/src/ai/routes.ts +172 -2
  60. package/src/ai/runtime.ts +13 -27
  61. package/src/ai/skills-routes.ts +3 -0
  62. package/src/ai/skills.ts +10 -1
  63. package/src/ai/store.ts +232 -27
  64. package/src/ai/timeline.ts +9 -1
  65. package/src/ai/tool-audit.ts +8 -0
  66. package/src/ai/tool-wording.ts +67 -0
  67. package/src/ai/turn-abort.ts +33 -0
  68. package/src/ai/types.ts +40 -4
  69. package/src/api/accounts-entities.ts +2 -1
  70. package/src/api/admin-ai-projects.ts +2 -0
  71. package/src/api/admin-ai-skills.ts +2 -0
  72. package/src/api/admin-ai-usage.ts +2 -0
  73. package/src/api/admin-core-settings.ts +190 -139
  74. package/src/api/admin-geoip.ts +80 -0
  75. package/src/api/admin-identity.ts +3 -0
  76. package/src/api/admin-lifecycle.ts +3 -0
  77. package/src/api/admin-outgoing-mail.ts +6 -2
  78. package/src/api/app-approval.ts +54 -6
  79. package/src/api/approvals-loader.ts +23 -1
  80. package/src/api/approvals.ts +3 -1
  81. package/src/api/auth.ts +25 -6
  82. package/src/api/capabilities.ts +44 -11
  83. package/src/api/capability-streams.ts +12 -0
  84. package/src/api/help.ts +4 -28
  85. package/src/api/index.ts +7 -1
  86. package/src/api/lasting-access-messages.ts +17 -0
  87. package/src/api/mcp.ts +3 -0
  88. package/src/api/me-panel-consent.ts +71 -0
  89. package/src/api/me.ts +80 -33
  90. package/src/api/search.ts +3 -1
  91. package/src/api/sync-ops.ts +2 -0
  92. package/src/api/widgets.ts +3 -0
  93. package/src/approvals/Approvals.tsx +180 -238
  94. package/src/approvals/messages.ts +12 -24
  95. package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
  96. package/src/browser/CloudResourceSearch.tsx +10 -6
  97. package/src/browser/app-approval.ts +60 -6
  98. package/src/browser/commands.ts +22 -6
  99. package/src/browser/composer-catalog.ts +145 -0
  100. package/src/browser/composer-insert-messages.ts +31 -0
  101. package/src/browser/composer-insert.ts +160 -0
  102. package/src/browser/composer-open-messages.ts +17 -0
  103. package/src/browser/composer-open.ts +110 -0
  104. package/src/browser/discoverable-command.ts +6 -0
  105. package/src/browser/hints.ts +6 -0
  106. package/src/browser/live-connection.ts +26 -2
  107. package/src/browser/navigation-apps.ts +7 -0
  108. package/src/browser/panel-context-bridge.ts +30 -0
  109. package/src/browser/panel-context-receiver.ts +53 -0
  110. package/src/browser/panel-menu.tsx +56 -0
  111. package/src/browser/panel-messages.ts +35 -0
  112. package/src/browser/panel-shortcuts.ts +31 -0
  113. package/src/browser/panel-slot.ts +756 -0
  114. package/src/browser/panel.ts +65 -0
  115. package/src/browser/resource-picker.tsx +12 -1
  116. package/src/browser/search-commands.ts +6 -3
  117. package/src/capabilities/client.ts +8 -2
  118. package/src/capabilities/reviews.ts +205 -0
  119. package/src/capabilities/server.ts +10 -3
  120. package/src/capabilities/testing.ts +47 -0
  121. package/src/capabilities/types.ts +2 -2
  122. package/src/cli/account.ts +13 -3
  123. package/src/cli/admin/geoip.ts +115 -0
  124. package/src/cli/admin/index.ts +3 -0
  125. package/src/contracts/app-approval.ts +159 -12
  126. package/src/contracts/app.ts +14 -0
  127. package/src/contracts/approvals.ts +2 -2
  128. package/src/contracts/capabilities.ts +410 -18
  129. package/src/contracts/geoip.ts +42 -0
  130. package/src/contracts/index.ts +3 -0
  131. package/src/contracts/panel-context-plain.ts +99 -0
  132. package/src/contracts/panel-context.ts +93 -0
  133. package/src/contracts/registry.ts +3 -1
  134. package/src/contracts/seen-hints.ts +35 -0
  135. package/src/contracts/shared.ts +2 -0
  136. package/src/server/client-ip.ts +5 -0
  137. package/src/server/help-payload.ts +38 -0
  138. package/src/server/middleware/auth.ts +11 -1
  139. package/src/server/middleware/middleware.ts +9 -0
  140. package/src/server/middleware/no-transform.ts +19 -0
  141. package/src/server/middleware/rate-limit.ts +4 -6
  142. package/src/server/request-context/address.ts +45 -0
  143. package/src/server/request-context/geoip.ts +380 -0
  144. package/src/server/request-context/index.ts +33 -0
  145. package/src/server/request-context/user-agent.ts +95 -0
  146. package/src/server/services/access.ts +5 -1
  147. package/src/services/accounts/posix.ts +2 -1
  148. package/src/services/app-approval.ts +533 -90
  149. package/src/services/approvals/device.ts +9 -5
  150. package/src/services/approvals/index.ts +6 -5
  151. package/src/services/approvals/list.ts +26 -5
  152. package/src/services/auth-flows/notification-sender.ts +3 -0
  153. package/src/services/geoip/index.ts +164 -0
  154. package/src/services/geoip/store.ts +30 -0
  155. package/src/services/geoip/upload.ts +150 -0
  156. package/src/services/identity/routes.ts +5 -1
  157. package/src/services/index.ts +32 -3
  158. package/src/services/known-browsers.ts +312 -0
  159. package/src/services/login-address-store.ts +51 -0
  160. package/src/services/login-context.ts +118 -0
  161. package/src/services/mandates/index.ts +3 -0
  162. package/src/services/mandates/policy.ts +15 -3
  163. package/src/services/notifications/browser.ts +16 -5
  164. package/src/services/notifications/email-frame.ts +1 -1
  165. package/src/services/oauth-tokens.ts +16 -2
  166. package/src/services/panel-context-consent.ts +58 -0
  167. package/src/services/pdf/image-protocol.ts +135 -0
  168. package/src/services/pdf/image-variants.ts +124 -0
  169. package/src/services/pdf/image-worker.ts +31 -0
  170. package/src/services/pdf/image.ts +109 -0
  171. package/src/services/pdf/index.ts +9 -0
  172. package/src/services/service-account-credentials.ts +21 -0
  173. package/src/services/session/index.ts +97 -16
  174. package/src/services/session/recent.ts +13 -0
  175. package/src/services/settings/api.ts +11 -6
  176. package/src/services/settings/app.ts +5 -4
  177. package/src/services/settings/core-settings.ts +4 -3
  178. package/src/services/settings/index.ts +19 -17
  179. package/src/services/settings/namespace.ts +2 -1
  180. package/src/services/settings/store.ts +83 -11
  181. package/src/services/webauthn.ts +12 -0
  182. package/src/shared/ai-platform-prompt.ts +1 -1
  183. package/src/shared/app-presentation.ts +1 -0
  184. package/src/shared/email-html.ts +2 -0
  185. package/src/shared/help.ts +3 -0
  186. package/src/shared/redirect.ts +12 -0
  187. package/src/shared-text/cursor.ts +22 -0
  188. package/src/shared-text/documents.ts +142 -0
  189. package/src/shared-text/events.ts +43 -0
  190. package/src/shared-text/index.ts +12 -0
  191. package/src/shared-text/log.ts +48 -0
  192. package/src/shared-text/text-diff.ts +23 -0
  193. package/src/shared-text/types.ts +41 -0
  194. package/src/ssr/AdminLayout.tsx +2 -0
  195. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  196. package/src/ssr/CloudShell.island.tsx +95 -10
  197. package/src/ssr/Layout.tsx +66 -10
  198. package/src/ssr/LayoutHeader.tsx +15 -9
  199. package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
  200. package/src/ssr/LayoutHelp.tsx +435 -274
  201. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
  202. package/src/ssr/LayoutHelpTrigger.tsx +27 -2
  203. package/src/ssr/LayoutRail.tsx +2 -2
  204. package/src/ssr/LayoutTabBar.tsx +29 -0
  205. package/src/ssr/MobileNavigation.tsx +2 -2
  206. package/src/ssr/PanelLayout.tsx +35 -0
  207. package/src/ssr/PanelRuntime.island.tsx +94 -0
  208. package/src/ssr/PanelSlot.tsx +158 -0
  209. package/src/ssr/PanelTrigger.tsx +49 -0
  210. package/src/ssr/ShellRuntime.island.tsx +69 -5
  211. package/src/ssr/app-appearance.ts +3 -2
  212. package/src/ssr/app-color.ts +105 -0
  213. package/src/ssr/app-launchpad.tsx +18 -1
  214. package/src/ssr/app-navigation.ts +28 -0
  215. package/src/ssr/compact-shell.baseline.json +5270 -0
  216. package/src/ssr/compact-shell.browser-harness.ts +439 -0
  217. package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
  218. package/src/ssr/help-messages.ts +4 -0
  219. package/src/ssr/help.ts +20 -0
  220. package/src/ssr/hints.ts +25 -0
  221. package/src/ssr/index.ts +5 -0
  222. package/src/ssr/islands/index.ts +4 -3
  223. package/src/ssr/layout-context.ts +2 -0
  224. package/src/ssr/layout-help-article.server.ts +50 -0
  225. package/src/ssr/layout-help-start.ts +33 -7
  226. package/src/ssr/panel-consent.ts +29 -0
  227. package/src/ssr/panel-context.ts +10 -0
  228. package/src/ssr/panel-slot.browser-harness.tsx +159 -0
  229. package/src/ssr/platform-messages.ts +48 -0
  230. package/src/ssr/rail-navigation.ts +44 -0
  231. package/src/ssr/shell-data.tsx +5 -0
  232. package/src/ssr/shell-shortcuts.ts +13 -2
  233. package/src/styles/effects.css +201 -11
  234. package/src/styles/file-chooser.css +2 -1
  235. package/src/styles/global.css +314 -63
  236. package/src/styles/panel.css +416 -0
  237. package/src/styles/resource-search.css +6 -2
  238. package/src/styles/tokens.css +36 -13
  239. package/src/styles/utilities-buttons.css +3 -2
  240. package/src/styles/utilities-data.css +2 -1
  241. package/src/styles/utilities-feedback.css +2 -2
  242. package/src/styles/utilities-layout.css +3 -13
  243. package/src/styles/utilities-markdown-editor.css +6 -6
  244. package/src/styles/utilities-navigation.css +22 -43
@@ -17,6 +17,7 @@ export const CAPABILITY_MAX_RESULT_BYTES = 256 * 1024;
17
17
  export const CAPABILITY_MAX_CATALOG_BYTES = 2 * 1024 * 1024;
18
18
  export const CAPABILITY_QUALIFIED_ID_MAX_LENGTH = 180;
19
19
  export const CLOUD_RESOURCE_ID_MAX_LENGTH = 512;
20
+ export const CAPABILITY_APP_ID_MAX_LENGTH = 80;
20
21
 
21
22
  export const CAPABILITY_FRAMEWORK_ERROR_CODES = {
22
23
  appUnavailable: "APP_UNAVAILABLE",
@@ -147,6 +148,7 @@ const CapabilityActionReviewDetailSchema = z
147
148
  }
148
149
  });
149
150
 
151
+ /** @deprecated the next major release replaces this shape; return CapabilityActionCallReview. */
150
152
  export const CapabilityActionReviewSchema = z
151
153
  .object({
152
154
  message: z.string().min(1).max(1000).describe("Plain-text description of the concrete Action consequence."),
@@ -156,8 +158,344 @@ export const CapabilityActionReviewSchema = z
156
158
  })
157
159
  .strict();
158
160
 
161
+ /** @deprecated the next major release replaces this shape; return CapabilityActionCallReview. */
159
162
  export type CapabilityActionReview = z.infer<typeof CapabilityActionReviewSchema>;
160
163
 
164
+ export const CAPABILITY_ASK_REASONS = [
165
+ "sends",
166
+ "deletes",
167
+ "cannot_undo",
168
+ "involves_people",
169
+ "changes_access",
170
+ "keeps_working",
171
+ "sensitive",
172
+ "app_asks",
173
+ ] as const;
174
+ export type CapabilityAskReason = (typeof CAPABILITY_ASK_REASONS)[number];
175
+ export const CAPABILITY_REACHES = ["you", "draft", "shared"] as const;
176
+ export type CapabilityReach = (typeof CAPABILITY_REACHES)[number];
177
+ export const CAPABILITY_ACTION_OUTCOMES = ["running", "done", "rejected", "not_run", "unknown"] as const;
178
+ export type CapabilityActionOutcome = (typeof CAPABILITY_ACTION_OUTCOMES)[number];
179
+ export const CAPABILITY_MAX_REVIEW_BYTES = 32 * 1024;
180
+ export const CapabilityActionPolicySchema = z.union([
181
+ z.object({ silent: z.enum(CAPABILITY_REACHES), routines: z.literal(false).optional() }).strict(),
182
+ z.object({ ask: z.enum(CAPABILITY_ASK_REASONS) }).strict(),
183
+ ]);
184
+ export type CapabilityActionPolicy = z.infer<typeof CapabilityActionPolicySchema>;
185
+
186
+ const reviewLine = (max: number, min = 1) =>
187
+ z
188
+ .string()
189
+ .min(min)
190
+ .max(max)
191
+ .refine((text) => !/[\p{Cc}\p{Cf}\u2028\u2029]/u.test(text), "Contains unsafe characters");
192
+ const reviewBlock = (max: number) =>
193
+ z
194
+ .string()
195
+ .max(max)
196
+ .refine((text) => !/[\p{Cc}\p{Cf}\u2028\u2029]/u.test(text.replace(/[\n\t]/g, "")), "Contains unsafe characters");
197
+ export const CapabilityPersonSchema = z
198
+ .object({ name: reviewLine(120).optional(), address: reviewLine(320).optional(), userId: z.uuid().optional() })
199
+ .strict()
200
+ .refine((person) => person.name !== undefined || person.address !== undefined, "A person needs a name or address");
201
+ export type CapabilityPerson = z.infer<typeof CapabilityPersonSchema>;
202
+ const reviewTime = z.iso.datetime({ offset: true });
203
+ const reviewDay = z.iso.date();
204
+ const reviewIcon = z.string().regex(/^ti ti-[a-z0-9-]+$/);
205
+ export const CapabilitySlotSchema = z.discriminatedUnion("kind", [
206
+ z
207
+ .object({
208
+ kind: z.literal("object"),
209
+ title: reviewLine(200),
210
+ ref: CloudResourceRefSchema.optional(),
211
+ icon: reviewIcon.optional(),
212
+ gone: z.literal(true).optional(),
213
+ })
214
+ .strict(),
215
+ z
216
+ .object({ kind: z.literal("place"), title: reviewLine(200), ref: CloudResourceRefSchema.optional(), icon: reviewIcon.optional() })
217
+ .strict(),
218
+ z
219
+ .object({
220
+ kind: z.literal("person"),
221
+ people: z.array(CapabilityPersonSchema).min(1).max(20),
222
+ total: z.number().int().min(1).max(100_000).optional(),
223
+ })
224
+ .strict()
225
+ .refine((slot) => slot.total === undefined || slot.total >= slot.people.length, "total is smaller than people"),
226
+ z
227
+ .object({ kind: z.literal("time"), start: z.string(), end: z.string().optional(), format: z.enum(["date", "date-time"]) })
228
+ .strict()
229
+ .superRefine((slot, ctx) => {
230
+ const schema = slot.format === "date" ? reviewDay : reviewTime;
231
+ for (const key of ["start", "end"] as const)
232
+ if (slot[key] !== undefined && !schema.safeParse(slot[key]).success)
233
+ ctx.addIssue({ code: "custom", path: [key], message: "Invalid date or time" });
234
+ if (slot.end && (slot.format === "date" ? slot.end < slot.start : Date.parse(slot.end) < Date.parse(slot.start)))
235
+ ctx.addIssue({ code: "custom", path: ["end"], message: "end precedes start" });
236
+ }),
237
+ z
238
+ .object({
239
+ kind: z.literal("count"),
240
+ value: z.number().int().min(0).max(1_000_000_000),
241
+ noun: z
242
+ .object({
243
+ zero: reviewLine(40).optional(),
244
+ one: reviewLine(40).optional(),
245
+ two: reviewLine(40).optional(),
246
+ few: reviewLine(40).optional(),
247
+ many: reviewLine(40).optional(),
248
+ other: reviewLine(40),
249
+ })
250
+ .strict(),
251
+ })
252
+ .strict(),
253
+ z.object({ kind: z.literal("text"), value: reviewLine(200) }).strict(),
254
+ ]);
255
+ export type CapabilitySlot = z.infer<typeof CapabilitySlotSchema>;
256
+ const reviewSlots = z
257
+ .record(z.string().regex(/^[a-z][A-Za-z0-9]{0,31}$/), CapabilitySlotSchema)
258
+ .refine((slots) => Object.keys(slots).length <= 6, "At most 6 slots");
259
+ const reviewTemplate = reviewLine(200).refine(
260
+ (text) => !/[{}]/.test(text.replace(/\{[a-z][A-Za-z0-9]{0,31}\}/g, "")),
261
+ "Invalid placeholder",
262
+ );
263
+ /** Context-free sentence rules, also used for consequence and rule sentences. */
264
+ export const capabilitySentenceIssues = (
265
+ text: string,
266
+ slots: Readonly<Record<string, CapabilitySlot>>,
267
+ complete = true,
268
+ chips = true,
269
+ ): string[] => {
270
+ const names = [...text.matchAll(/\{([a-z][A-Za-z0-9]{0,31})\}/g)].map((match) => match[1]!);
271
+ const issues: string[] = [];
272
+ if (/[{}]/.test(text.replace(/\{[a-z][A-Za-z0-9]{0,31}\}/g, ""))) issues.push("Invalid placeholder");
273
+ for (const name of names) if (!Object.hasOwn(slots, name)) issues.push(`Unknown slot ${name}`);
274
+ if (complete) for (const name of Object.keys(slots)) if (!names.includes(name)) issues.push(`Missing slot ${name}`);
275
+ if (
276
+ chips &&
277
+ names.filter((name) => {
278
+ const slot = Object.hasOwn(slots, name) ? slots[name] : undefined;
279
+ return slot && slot.kind !== "text" && slot.kind !== "count";
280
+ }).length > 3
281
+ )
282
+ issues.push("At most 3 chip slots");
283
+ return issues;
284
+ };
285
+ const sentenceShape = { text: reviewTemplate, slots: reviewSlots.readonly().optional() };
286
+ const sentenceRefinement = (sentence: { text: string; slots?: Readonly<Record<string, CapabilitySlot>> }, ctx: z.RefinementCtx) => {
287
+ for (const message of capabilitySentenceIssues(sentence.text, sentence.slots ?? {})) ctx.addIssue({ code: "custom", message });
288
+ };
289
+ export const CapabilitySentenceSchema = z.object(sentenceShape).strict().superRefine(sentenceRefinement);
290
+ export type CapabilitySentence = z.infer<typeof CapabilitySentenceSchema>;
291
+ export const CapabilityConsequenceSchema = z
292
+ .object({ ...sentenceShape, mark: z.enum(["added", "removed"]).optional() })
293
+ .strict()
294
+ .superRefine(sentenceRefinement);
295
+ export type CapabilityConsequence = z.infer<typeof CapabilityConsequenceSchema>;
296
+ const previewPeople = (max: number) => z.array(CapabilityPersonSchema).max(max);
297
+ const extraCount = z.number().int().min(0).max(100_000);
298
+ const previewZone = reviewLine(200).refine((zone) => {
299
+ try {
300
+ new Intl.DateTimeFormat("en", { timeZone: zone });
301
+ return true;
302
+ } catch {
303
+ return false;
304
+ }
305
+ }, "Invalid time zone");
306
+ export const CapabilityPreviewSchema = z.discriminatedUnion("kind", [
307
+ z
308
+ .object({
309
+ kind: z.literal("message"),
310
+ from: CapabilityPersonSchema.optional(),
311
+ to: previewPeople(50),
312
+ cc: previewPeople(50).optional(),
313
+ bcc: previewPeople(50).optional(),
314
+ more: z.object({ to: extraCount.optional(), cc: extraCount.optional(), bcc: extraCount.optional() }).strict().optional(),
315
+ subject: reviewLine(998, 0),
316
+ body: reviewBlock(10_000),
317
+ format: z.enum(["plain", "markdown"]),
318
+ truncated: z.boolean().optional(),
319
+ attachments: z
320
+ .array(
321
+ z
322
+ .object({ name: reviewLine(255), size: z.number().int().nonnegative().optional(), ref: CloudResourceRefSchema.optional() })
323
+ .strict(),
324
+ )
325
+ .max(50)
326
+ .optional(),
327
+ moreAttachments: extraCount.optional(),
328
+ sendAt: reviewTime.optional(),
329
+ warnings: z.array(reviewLine(300)).max(5).optional(),
330
+ })
331
+ .strict()
332
+ .refine(
333
+ (preview) =>
334
+ preview.to.length +
335
+ (preview.cc?.length ?? 0) +
336
+ (preview.bcc?.length ?? 0) +
337
+ (preview.more?.to ?? 0) +
338
+ (preview.more?.cc ?? 0) +
339
+ (preview.more?.bcc ?? 0) >
340
+ 0,
341
+ "A message needs a recipient",
342
+ ),
343
+ z
344
+ .object({
345
+ kind: z.literal("event"),
346
+ title: reviewLine(200),
347
+ start: z.union([reviewDay, reviewTime]),
348
+ end: z.union([reviewDay, reviewTime]).optional(),
349
+ allDay: z.boolean().optional(),
350
+ timeZone: previewZone.optional(),
351
+ place: reviewLine(200).optional(),
352
+ location: reviewLine(200).optional(),
353
+ recurrence: reviewLine(200).optional(),
354
+ attendees: previewPeople(50).optional(),
355
+ description: reviewBlock(2000).optional(),
356
+ })
357
+ .strict(),
358
+ z
359
+ .object({
360
+ kind: z.literal("task"),
361
+ title: reviewLine(200),
362
+ place: reviewLine(200).optional(),
363
+ due: z.union([reviewDay, reviewTime]).optional(),
364
+ assignees: previewPeople(20).optional(),
365
+ status: reviewLine(60).optional(),
366
+ checklist: z
367
+ .array(z.object({ label: reviewLine(200), done: z.boolean() }).strict())
368
+ .max(20)
369
+ .optional(),
370
+ tags: z.array(reviewLine(60)).max(10).optional(),
371
+ description: reviewBlock(2000).optional(),
372
+ })
373
+ .strict(),
374
+ z
375
+ .object({
376
+ kind: z.literal("page"),
377
+ title: reviewLine(200),
378
+ place: reviewLine(200).optional(),
379
+ body: reviewBlock(10_000),
380
+ format: z.enum(["plain", "markdown"]),
381
+ truncated: z.boolean().optional(),
382
+ changes: z
383
+ .array(z.object({ change: z.enum(["added", "removed"]), text: reviewBlock(2000) }).strict())
384
+ .max(20)
385
+ .optional(),
386
+ })
387
+ .strict(),
388
+ z
389
+ .object({
390
+ kind: z.literal("file"),
391
+ name: reviewLine(255),
392
+ place: reviewLine(200).optional(),
393
+ path: reviewLine(1024).optional(),
394
+ size: z.number().int().nonnegative().optional(),
395
+ mediaType: reviewLine(120).optional(),
396
+ to: reviewLine(1024).optional(),
397
+ })
398
+ .strict(),
399
+ z
400
+ .object({
401
+ kind: z.literal("rule"),
402
+ when: CapabilitySentenceSchema,
403
+ does: reviewBlock(4000),
404
+ may: z.array(CapabilityConsequenceSchema).max(12),
405
+ seesChat: z.boolean(),
406
+ })
407
+ .strict(),
408
+ z
409
+ .object({
410
+ kind: z.literal("fields"),
411
+ rows: z
412
+ .array(
413
+ z
414
+ .object({
415
+ label: reviewLine(60),
416
+ value: reviewBlock(2000),
417
+ before: reviewBlock(2000).optional(),
418
+ format: z.enum(["date", "date-time"]).optional(),
419
+ })
420
+ .strict()
421
+ .superRefine((row, context) => {
422
+ const schema =
423
+ row.format === "date" ? CapabilityReviewDateSchema : row.format === "date-time" ? CapabilityReviewDateTimeSchema : null;
424
+ for (const key of ["value", "before"] as const)
425
+ if (row[key] !== undefined && schema?.safeParse(row[key]).success === false)
426
+ context.addIssue({ code: "custom", path: [key], message: "Invalid date or time" });
427
+ }),
428
+ )
429
+ .min(1)
430
+ .max(12),
431
+ })
432
+ .strict(),
433
+ ]);
434
+ export type CapabilityPreview = z.infer<typeof CapabilityPreviewSchema>;
435
+ export const CapabilityActionCallReviewSchema = z
436
+ .object({
437
+ locale: z.string(),
438
+ sentence: z
439
+ .object({
440
+ action: reviewTemplate,
441
+ running: reviewTemplate,
442
+ done: reviewTemplate,
443
+ confirm: reviewTemplate,
444
+ decline: reviewTemplate,
445
+ rejected: reviewTemplate.optional(),
446
+ notRun: reviewTemplate.optional(),
447
+ short: reviewTemplate.refine((text) => text.length <= 60, "short exceeds 60 characters").optional(),
448
+ })
449
+ .strict(),
450
+ slots: reviewSlots.readonly(),
451
+ preview: CapabilityPreviewSchema.optional(),
452
+ consequences: z.array(CapabilityConsequenceSchema).max(6).optional(),
453
+ ask: z.enum(CAPABILITY_ASK_REASONS).optional(),
454
+ reach: z.enum(CAPABILITY_REACHES).optional(),
455
+ effects: z.number().int().min(1).max(1000).optional(),
456
+ check: z
457
+ .object({ query: CapabilityLocalIdSchema, input: z.record(z.string(), z.unknown()) })
458
+ .strict()
459
+ .optional(),
460
+ scope: z.string().min(1).max(200).optional(),
461
+ links: z.array(CapabilitySemanticLinkSchema).max(10).optional(),
462
+ })
463
+ .strict()
464
+ .superRefine((review, ctx) => {
465
+ for (const [form, text] of Object.entries(review.sentence)) {
466
+ if (text === undefined) continue;
467
+ const complete = form === "action" || form === "running" || form === "done";
468
+ for (const message of capabilitySentenceIssues(text, review.slots, complete, form === "action"))
469
+ ctx.addIssue({ code: "custom", path: ["sentence", form], message });
470
+ if ((form === "confirm" || form === "decline") && [...text.matchAll(/\{[a-z][A-Za-z0-9]{0,31}\}/g)].length > 1)
471
+ ctx.addIssue({ code: "custom", path: ["sentence", form], message: "Buttons name at most one slot" });
472
+ }
473
+ });
474
+ export type CapabilityActionCallReview = z.infer<typeof CapabilityActionCallReviewSchema>;
475
+ export type CapabilityActionReviewData = CapabilityActionCallReview | CapabilityActionReview;
476
+ export const CapabilityActionReviewDataSchema = z
477
+ .unknown()
478
+ .transform((value, ctx) => {
479
+ const schema =
480
+ value !== null && typeof value === "object" && Object.hasOwn(value, "sentence")
481
+ ? CapabilityActionCallReviewSchema
482
+ : CapabilityActionReviewSchema;
483
+ const parsed = schema.safeParse(value);
484
+ if (parsed.success) return parsed.data;
485
+ for (const issue of parsed.error.issues) ctx.addIssue({ code: "custom", path: issue.path, message: issue.message });
486
+ return z.NEVER;
487
+ })
488
+ .pipe(z.union([CapabilityActionCallReviewSchema, CapabilityActionReviewSchema]));
489
+ export const CapabilityActionReceiptSchema = z
490
+ .object({ ...sentenceShape, preview: CapabilityPreviewSchema.optional() })
491
+ .strict()
492
+ .superRefine(sentenceRefinement);
493
+ export type CapabilityActionReceipt = z.infer<typeof CapabilityActionReceiptSchema>;
494
+ export const CapabilityActionCheckSchema = z
495
+ .object({ outcome: z.enum(["done", "not_done", "pending"]), receipt: CapabilityActionReceiptSchema.optional() })
496
+ .strict();
497
+ export type CapabilityActionCheck = z.infer<typeof CapabilityActionCheckSchema>;
498
+
161
499
  export const CapabilityPageSchema = z.discriminatedUnion("hasMore", [
162
500
  z
163
501
  .object({
@@ -215,15 +553,19 @@ export type CapabilityResult<T> = {
215
553
  stream?: CapabilityStream;
216
554
  /** Provider-authored, user-facing summary of the successful result. Render as escaped plain text. */
217
555
  summary?: string;
556
+ receipt?: CapabilityActionReceipt;
218
557
  refs?: CloudResourceReference[];
219
558
  page?: CapabilityPage;
220
559
  links?: CapabilitySemanticLink[];
221
560
  presentation?: CapabilityTablePresentation;
222
561
  };
223
562
 
224
- export const capabilityResultSchema = <T extends z.ZodType>(
563
+ // Published JSON schemas (Core's result check, each MCP tool's outputSchema) describe a receipt as an opaque object,
564
+ // so they stay as small as before receipts existed; Core then checks it with the shared receipt rules. Zod readers
565
+ // (the producer, stream results, consumers) keep the full receipt shape.
566
+ const resultSchema = <T extends z.ZodType>(
225
567
  data: T,
226
- options?: { consumer?: boolean },
568
+ options: { consumer?: boolean; opaqueReceipt: boolean },
227
569
  ): z.ZodType<CapabilityResult<z.output<T>>> =>
228
570
  z
229
571
  .object({
@@ -231,19 +573,24 @@ export const capabilityResultSchema = <T extends z.ZodType>(
231
573
  stream: CapabilityStreamSchema.optional(),
232
574
  summary: z.string().trim().min(1).max(500).optional(),
233
575
  refs: z
234
- .array(options?.consumer ? LinkedCloudResourceReferenceSchema : CloudResourceReferenceSchema)
576
+ .array(options.consumer ? LinkedCloudResourceReferenceSchema : CloudResourceReferenceSchema)
235
577
  .max(100)
236
578
  .optional(),
237
579
  page: CapabilityPageSchema.optional(),
238
580
  links: z.array(CapabilitySemanticLinkSchema).max(20).optional(),
239
- presentation: options?.consumer
581
+ receipt: options.opaqueReceipt
582
+ ? z.record(z.string(), z.unknown()).optional()
583
+ : options.consumer
584
+ ? CapabilityActionReceiptSchema.optional().catch(undefined)
585
+ : CapabilityActionReceiptSchema.optional(),
586
+ presentation: options.consumer
240
587
  ? CapabilityTablePresentationSchema.optional().catch(undefined)
241
588
  : CapabilityTablePresentationSchema.optional(),
242
589
  })
243
590
  .strict()
244
591
  .superRefine((result, context) => {
245
592
  if (
246
- !options?.consumer &&
593
+ !options.consumer &&
247
594
  result.presentation &&
248
595
  !Array.isArray(capabilityDataAtPath(capabilityDataAtPath(result, ["data"]), result.presentation.rowsPath))
249
596
  ) {
@@ -255,14 +602,18 @@ export const capabilityResultSchema = <T extends z.ZodType>(
255
602
  }
256
603
  }) as unknown as z.ZodType<CapabilityResult<z.output<T>>>;
257
604
 
605
+ export const capabilityResultSchema = <T extends z.ZodType>(
606
+ data: T,
607
+ options?: { consumer?: boolean },
608
+ ): z.ZodType<CapabilityResult<z.output<T>>> => resultSchema(data, { consumer: options?.consumer, opaqueReceipt: false });
609
+
258
610
  export const capabilityResultJsonSchema = (
259
611
  dataSchema: Record<string, unknown>,
260
612
  options?: { consumer?: boolean },
261
613
  ): Record<string, unknown> =>
262
- z.toJSONSchema(capabilityResultSchema(z.fromJSONSchema(structuredClone(dataSchema)), options), { io: "output" }) as Record<
263
- string,
264
- unknown
265
- >;
614
+ z.toJSONSchema(resultSchema(z.fromJSONSchema(structuredClone(dataSchema)), { consumer: options?.consumer, opaqueReceipt: true }), {
615
+ io: "output",
616
+ }) as Record<string, unknown>;
266
617
 
267
618
  export const CapabilityErrorSchema = z
268
619
  .object({
@@ -281,7 +632,7 @@ export type CapabilityError = {
281
632
  details?: Record<string, unknown>;
282
633
  };
283
634
  export type CapabilityInvocationResult<T> = { ok: true; data: CapabilityResult<T> } | { ok: false; error: CapabilityError };
284
- export type CapabilityActionReviewResult = { ok: true; data: CapabilityActionReview } | { ok: false; error: CapabilityError };
635
+ export type CapabilityActionReviewResult = { ok: true; data: CapabilityActionReviewData } | { ok: false; error: CapabilityError };
285
636
 
286
637
  export const CloudResourceViewSchema = z
287
638
  .object({
@@ -370,6 +721,8 @@ export type CapabilityUniversalSearchDefinition = {
370
721
  export type CapabilitySchemaPresentation = Readonly<Record<string, string>>;
371
722
 
372
723
  /**
724
+ * @deprecated removed with the next major; return per-call CapabilityActionCallReview sentence forms.
725
+ *
373
726
  * What one Action does, in words people read in approvals, receipts, and step rows. Each sentence is a
374
727
  * template: `{input.to}` inserts a field of the validated call input by its dotted schema path, and
375
728
  * `done` may also read the Action's result data with `{data.id}`. Cloud inserts values as escaped,
@@ -398,7 +751,7 @@ export type CapabilityOperationPresentationTranslation = {
398
751
  data?: CapabilitySchemaPresentation;
399
752
  /** Search-tag presentation keyed by the stable tag token. */
400
753
  searchTags?: Readonly<Record<string, { title?: string; description?: string }>>;
401
- /** Actions only: the complete set of sentences in this locale. */
754
+ /** @deprecated removed with the next major; return per-call review forms. */
402
755
  sentences?: CapabilityActionSentences;
403
756
  };
404
757
 
@@ -406,13 +759,21 @@ export type CapabilityPresentationTranslation = {
406
759
  types?: Readonly<Record<string, { title?: string; description?: string }>>;
407
760
  queries?: Readonly<Record<string, CapabilityOperationPresentationTranslation>>;
408
761
  actions?: Readonly<Record<string, CapabilityOperationPresentationTranslation>>;
409
- commands?: Readonly<Record<string, Pick<CapabilityOperationPresentationTranslation, "title" | "description" | "input">>>;
762
+ commands?: Readonly<
763
+ Record<
764
+ string,
765
+ Pick<CapabilityOperationPresentationTranslation, "title" | "description" | "input"> & {
766
+ /** Translated search words, with the same bounds as the base Command keywords. */
767
+ keywords?: readonly string[];
768
+ }
769
+ >
770
+ >;
410
771
  };
411
772
 
412
773
  export type CapabilityPresentationCatalog = {
413
774
  /** Locale of the complete Type, Query, Action, search-tag, and schema presentation. */
414
775
  baseLocale: string;
415
- /** Optional Action sentences in the base locale, keyed by Action local ID. */
776
+ /** @deprecated removed with the next major; return per-call review forms. */
416
777
  sentences?: Readonly<Record<string, CapabilityActionSentences>>;
417
778
  /** Partial presentation overlays with exact -> ancestor -> base fallback. */
418
779
  translations: Readonly<Record<string, CapabilityPresentationTranslation>>;
@@ -444,7 +805,9 @@ export type CapabilityActionDefinition<Input extends z.ZodType = z.ZodType<any>,
444
805
  openWorld: boolean;
445
806
  stream?: CapabilityStreamDefinition;
446
807
  idempotency: CapabilityIdempotencyPolicy;
808
+ /** @deprecated removed with the next major; declare policy. */
447
809
  approval?: CapabilityActionApprovalPolicy;
810
+ policy?: CapabilityActionPolicy;
448
811
  review?: (
449
812
  input: z.output<Input>,
450
813
  context: CapabilityExecutionContext,
@@ -549,7 +912,7 @@ export const CapabilityActionManifestSchema = CapabilityOperationManifestBaseSch
549
912
  export const CapabilityAppIdSchema = z
550
913
  .string()
551
914
  .min(1)
552
- .max(80)
915
+ .max(CAPABILITY_APP_ID_MAX_LENGTH)
553
916
  .regex(/^[a-z][a-z0-9-]*$/);
554
917
 
555
918
  const MAX_MANIFEST_ENTRIES = 200;
@@ -571,8 +934,9 @@ const readEntries = <T extends z.ZodType>(values: unknown, entry: T): z.output<T
571
934
  /**
572
935
  * Reads what this release understands from a manifest that a newer release may have produced.
573
936
  *
574
- * - Unknown top-level fields are ignored. A newer release may therefore only add something at the top
575
- * level; anything that restricts an existing entry belongs inside that entry.
937
+ * - Unknown top-level fields are ignored. Additions there must retain conservative behavior through
938
+ * existing fields for older readers; Action policies carry the corresponding derived approval.
939
+ * Other restrictions belong inside their entry.
576
940
  * - An entry with a field or value from a newer release is left out rather than stripped: its unknown
577
941
  * part may change how the entry runs or who may use it, so guessing could widen access.
578
942
  * - Entries that depend on an entry that is not there follow it: a Query scoped to a missing type is
@@ -580,6 +944,9 @@ const readEntries = <T extends z.ZodType>(values: unknown, entry: T): z.output<T
580
944
  * - A file provider with a field from a newer release is left out. Whether its operations are still
581
945
  * there and match the contract is `fileProviderIssues`' job, which Core and consumers run.
582
946
  *
947
+ * - An unreadable policy or one inconsistent with derived approval, review, or effect flags leaves
948
+ * out its Action. Policies for unknown Actions are dropped.
949
+ *
583
950
  * Every other entry stays available.
584
951
  */
585
952
  const readableManifest = (value: unknown): unknown => {
@@ -593,7 +960,27 @@ const readableManifest = (value: unknown): unknown => {
593
960
  const typeIds = new Set(types.map((type) => type.localId));
594
961
  const readableQueries = queries.filter((query) => query.universalSearch?.scopeTypes?.every((type) => typeIds.has(type)) ?? true);
595
962
  const queryIds = new Set(readableQueries.map((query) => query.localId));
596
- const { fileProvider: sentProvider, ...rest } = sent;
963
+ const policies: Record<string, CapabilityActionPolicy> = {};
964
+ const sentPolicies = sent.policies;
965
+ // Not a plain policy map: leave the value to the manifest schema, which rejects it.
966
+ if (
967
+ sentPolicies !== undefined &&
968
+ (sentPolicies === null ||
969
+ typeof sentPolicies !== "object" ||
970
+ (Object.getPrototypeOf(sentPolicies) !== Object.prototype && Object.getPrototypeOf(sentPolicies) !== null))
971
+ )
972
+ return value;
973
+ const readableActions = actions.filter((action) => {
974
+ if (!sentPolicies || typeof sentPolicies !== "object" || !Object.hasOwn(sentPolicies, action.localId)) return true;
975
+ const parsed = CapabilityActionPolicySchema.safeParse(Object.getOwnPropertyDescriptor(sentPolicies, action.localId)?.value);
976
+ if (!parsed.success) return false;
977
+ const policy = parsed.data;
978
+ const approval = "silent" in policy ? "rememberable" : undefined;
979
+ if (action.approval !== approval || !action.review || ("silent" in policy && (action.destructive || action.openWorld))) return false;
980
+ policies[action.localId] = policy;
981
+ return true;
982
+ });
983
+ const { fileProvider: sentProvider, policies: _policies, ...rest } = sent;
597
984
  const provider = CapabilityFileProviderManifestSchema.safeParse(sentProvider);
598
985
  return {
599
986
  ...rest,
@@ -604,7 +991,8 @@ const readableManifest = (value: unknown): unknown => {
604
991
  return withoutReader;
605
992
  }),
606
993
  queries: readableQueries,
607
- actions,
994
+ actions: readableActions,
995
+ ...(sentPolicies !== undefined ? { policies } : {}),
608
996
  commands,
609
997
  };
610
998
  };
@@ -625,6 +1013,7 @@ export const CapabilityManifestSchema = z.preprocess(
625
1013
  commands: z.array(CapabilityCommandManifestSchema).max(MAX_MANIFEST_ENTRIES),
626
1014
  // Optional without a default: a manifest without a provider keeps the shape and hash of earlier releases.
627
1015
  fileProvider: CapabilityFileProviderManifestSchema.optional(),
1016
+ policies: z.record(CapabilityLocalIdSchema, CapabilityActionPolicySchema).optional(),
628
1017
  }),
629
1018
  );
630
1019
 
@@ -634,6 +1023,9 @@ export type CapabilityActionManifest = z.infer<typeof CapabilityActionManifestSc
634
1023
  export type CapabilitySearchTagManifest = z.infer<typeof CapabilitySearchTagManifestSchema>;
635
1024
  export type CapabilityManifest = z.infer<typeof CapabilityManifestSchema>;
636
1025
 
1026
+ export const capabilityActionPolicy = (manifest: CapabilityManifest, localId: string): CapabilityActionPolicy | undefined =>
1027
+ manifest.policies && Object.hasOwn(manifest.policies, localId) ? manifest.policies[localId] : undefined;
1028
+
637
1029
  export const cloudResourceRefAppId = (ref: CloudResourceRef): string => ref.type.slice(0, ref.type.indexOf("."));
638
1030
 
639
1031
  /** Resolves a resource reference against one current, validated app manifest. */
@@ -0,0 +1,42 @@
1
+ export type GeoIpKind = "city" | "asn";
2
+ export const GEOIP_MAX_DATABASE_BYTES = 192 * 1024 * 1024;
3
+ export const GEOIP_MAX_REQUEST_BYTES = 128 * 1024 * 1024;
4
+ export const DBIP_ATTRIBUTION = {
5
+ text: "IP Geolocation by DB-IP",
6
+ url: "https://db-ip.com",
7
+ license: "CC BY 4.0",
8
+ licenseUrl: "https://creativecommons.org/licenses/by/4.0/",
9
+ } as const;
10
+
11
+ export type GeoIpAdminDatabase = {
12
+ kind: GeoIpKind;
13
+ source: "uploaded" | "bundled" | null;
14
+ /** A bundled snapshot exists for this type, so a reset returns to it. */
15
+ bundled: boolean;
16
+ databaseType: string | null;
17
+ buildDate: string | null;
18
+ stale: boolean;
19
+ dbIp: boolean;
20
+ upload: {
21
+ sha256: string;
22
+ size: number;
23
+ uploadedAt: string;
24
+ uploadedBy: { id: string; name: string } | null;
25
+ } | null;
26
+ };
27
+ export type GeoIpAdminStatus = {
28
+ databases: [GeoIpAdminDatabase, GeoIpAdminDatabase];
29
+ attribution: typeof DBIP_ATTRIBUTION | null;
30
+ limits: { maxDatabaseBytes: number; maxRequestBytes: number };
31
+ };
32
+
33
+ export type GeoIpUploadResult = GeoIpAdminStatus & { kind: GeoIpKind };
34
+ export type GeoIpResetResult = GeoIpAdminStatus & { changed: boolean };
35
+
36
+ export type GeoIpErrorCode =
37
+ | "geoip_request_too_large"
38
+ | "geoip_too_large"
39
+ | "geoip_empty"
40
+ | "geoip_invalid"
41
+ | "geoip_unsupported_type"
42
+ | "geoip_upload_busy";
@@ -7,8 +7,10 @@ export * from "./capability-compatibility";
7
7
  export * from "./commands";
8
8
  export * from "./contact-directory";
9
9
  export * from "./file-provider";
10
+ export * from "./geoip";
10
11
  export * from "./notification-types";
11
12
  export * from "./outgoing-mail";
13
+ export * from "./panel-context";
12
14
  export * from "./posix";
13
15
  export * from "./profile";
14
16
  export * from "./pwa";
@@ -17,6 +19,7 @@ export { RailAccessSchema, type RailAdminEntry, RailAdminInputSchema, RailAdminS
17
19
  export type { RailShortcut } from "./rail-preferences";
18
20
  export * from "./registry";
19
21
  export * from "./resource-clipboard";
22
+ export * from "./seen-hints";
20
23
  export * from "./shared";
21
24
  export * from "./user-notifications";
22
25
  export * from "./widgets";