@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
@@ -0,0 +1,65 @@
1
+ import type { PanelPageContext } from "../contracts/panel-context";
2
+ import {
3
+ PAGE_CONTEXT_CHANGED,
4
+ PAGE_CONTEXT_COLLECT,
5
+ PANEL_CONTEXT_CHANGED,
6
+ type PageContextCollection,
7
+ type PageContextSource,
8
+ readProviderContext,
9
+ } from "./panel-context-bridge";
10
+
11
+ export type PanelRequest = { action: "open"; provider: string; search?: string; accepted: boolean } | { action: "close" | "toggle" };
12
+
13
+ declare global {
14
+ interface WindowEventMap {
15
+ "cloud.panel.request": CustomEvent<PanelRequest>;
16
+ }
17
+ }
18
+
19
+ /** Opens a visible provider. False leaves navigation with the caller when this page has no matching slot. */
20
+ export function openPanel(provider: string, options?: { search?: string }): boolean {
21
+ const detail: PanelRequest = {
22
+ action: "open",
23
+ provider,
24
+ search: typeof options?.search === "string" && options.search.length <= 2048 ? options.search : undefined,
25
+ accepted: false,
26
+ };
27
+ window.dispatchEvent(new CustomEvent("cloud.panel.request", { detail }));
28
+ return detail.accepted;
29
+ }
30
+
31
+ /** Closes the panel when it is open. */
32
+ export function closePanel(): void {
33
+ if (window.parent !== window && document.querySelector("[data-cloud-panel-page]")) {
34
+ window.parent.postMessage({ type: "cloud.panel.close" }, location.origin);
35
+ return;
36
+ }
37
+ window.dispatchEvent(new CustomEvent<PanelRequest>("cloud.panel.request", { detail: { action: "close" } }));
38
+ }
39
+
40
+ /** Publish in a reactive effect; dispose the previous call when that source changes or unmounts. */
41
+ export function publishPageContext(context: PageContextSource): () => void {
42
+ const source = { ...context, ...(context.scope ? { scope: { ...context.scope } } : {}) };
43
+ const collect = (event: Event) => (event as CustomEvent<PageContextCollection>).detail.sources.push(source);
44
+ window.addEventListener(PAGE_CONTEXT_COLLECT, collect);
45
+ window.dispatchEvent(new Event(PAGE_CONTEXT_CHANGED));
46
+ let active = true;
47
+ return () => {
48
+ if (!active) return;
49
+ active = false;
50
+ window.removeEventListener(PAGE_CONTEXT_COLLECT, collect);
51
+ window.dispatchEvent(new Event(PAGE_CONTEXT_CHANGED));
52
+ };
53
+ }
54
+
55
+ /** The validated URL snapshot, then the most recent accepted host message. */
56
+ export function getContext(): PanelPageContext | null {
57
+ return readProviderContext();
58
+ }
59
+
60
+ /** Called on each accepted change, including null on close or withdrawal. */
61
+ export function onContextChange(cb: (context: PanelPageContext | null) => void): () => void {
62
+ const changed = () => cb(getContext());
63
+ window.addEventListener(PANEL_CONTEXT_CHANGED, changed);
64
+ return () => window.removeEventListener(PANEL_CONTEXT_CHANGED, changed);
65
+ }
@@ -6,10 +6,14 @@ import { resourceSearchDialogOptions } from "./resource-search-dialog";
6
6
  import { resourceSearchMessages } from "./resource-search-messages";
7
7
 
8
8
  export type CloudResourcePickerItem = SearchItem;
9
+ /** Narrows the search on the server to one app and, optionally, one of its search tags; shown as the removable chip. */
10
+ export type CloudResourcePickerScope = { appId: string; tag?: string; label: string; icon?: string };
9
11
  export type CloudResourcePickerOptions = {
10
12
  title?: string;
11
13
  placeholder?: string;
12
14
  initialAppId?: string;
15
+ scope?: CloudResourcePickerScope;
16
+ query?: string;
13
17
  excludeRefs?: readonly CloudResourceRef[];
14
18
  requireReader?: boolean;
15
19
  resourceType?: string;
@@ -20,7 +24,14 @@ export const openCloudResourcePicker = (options: CloudResourcePickerOptions = {}
20
24
  const t = resourceSearchMessages.resolve([locale]).t;
21
25
  return dialogCore.open<CloudResourcePickerItem>(
22
26
  (close) => (
23
- <CloudResourceSearch {...options} title={options.title ?? t.chooseResource} selectionMode onSelect={close} onClose={() => close()} />
27
+ <CloudResourceSearch
28
+ {...options}
29
+ request={options.scope !== undefined || options.query !== undefined ? { scope: options.scope, query: options.query } : undefined}
30
+ title={options.title ?? t.chooseResource}
31
+ selectionMode
32
+ onSelect={close}
33
+ onClose={() => close()}
34
+ />
24
35
  ),
25
36
  // A phone shows the picker as a full-screen sheet; global search keeps its floating card.
26
37
  {
@@ -1,12 +1,15 @@
1
- import { z } from "zod";
2
1
  import type { SearchItem } from "../api/search/schemas";
3
2
  import { listCapabilityCatalog } from "../capabilities/client";
4
3
  import { LOCALE_HEADER } from "../shared/locale";
5
4
  import type { ContextAwareCommand } from "./command-bridge";
6
5
 
6
+ import { discoverableCommandInput } from "./discoverable-command";
7
+ import { navigationApps } from "./navigation-apps";
8
+
7
9
  export type PaletteCommand = ContextAwareCommand & { appName?: string; keywords?: readonly string[]; context?: boolean };
8
10
  export const loadSearchCommands = async (locale: string, signal: AbortSignal): Promise<PaletteCommand[]> => {
9
11
  const commands: PaletteCommand[] = [];
12
+ const { appIds } = navigationApps();
10
13
  let cursor: string | undefined;
11
14
  do {
12
15
  const result = await listCapabilityCatalog({ limit: 25, cursor, signal, headers: { [LOCALE_HEADER]: locale } });
@@ -14,8 +17,8 @@ export const loadSearchCommands = async (locale: string, signal: AbortSignal): P
14
17
  for (const app of result.data.apps) {
15
18
  for (const command of app.manifest.commands) {
16
19
  // Only Commands with a valid empty invocation are globally discoverable.
17
- const input = z.fromJSONSchema(structuredClone(command.inputSchema)).safeParse({});
18
- if (!input.success) continue;
20
+ const input = discoverableCommandInput(command, app.appId, appIds);
21
+ if (!input?.success) continue;
19
22
  const id = `${app.appId}.${command.localId}`;
20
23
  commands.push({
21
24
  id,
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import {
3
3
  CAPABILITY_FRAMEWORK_ERROR_CODES,
4
4
  CAPABILITY_MAX_CATALOG_BYTES,
5
- CapabilityActionReviewSchema,
5
+ CapabilityActionReviewDataSchema,
6
6
  CapabilityCatalogSchema,
7
7
  capabilityResultSchema,
8
8
  } from "../contracts/capabilities";
@@ -142,7 +142,7 @@ export const reviewCapabilityAction = async <TInput = unknown>(
142
142
  body: JSON.stringify({ input: invocation.input }),
143
143
  signal: invocation.signal,
144
144
  });
145
- return readCapabilityResponse(response, CapabilityActionReviewSchema);
145
+ return readCapabilityResponse(response, CapabilityActionReviewDataSchema);
146
146
  } catch (cause) {
147
147
  return unavailable(cause, undefined, clientLocale(options.headers));
148
148
  }
@@ -180,4 +180,10 @@ export const resolveCommand = async (
180
180
  }
181
181
  };
182
182
 
183
+ export {
184
+ actionReview,
185
+ type CapabilityReviewForms,
186
+ capabilityReviewRememberScope,
187
+ capabilityReviewView,
188
+ } from "./reviews";
183
189
  export { type CapabilityStreamVerb, transferCapabilityStream } from "./streams";
@@ -0,0 +1,205 @@
1
+ import { capabilityPersonText, capabilityReviewSentence, capabilitySentenceText } from "../_internal/capability-sentences";
2
+ import {
3
+ CAPABILITY_MAX_REVIEW_BYTES,
4
+ type CapabilityActionCallReview,
5
+ CapabilityActionCallReviewSchema,
6
+ type CapabilityActionReview,
7
+ type CapabilityActionReviewData,
8
+ type CapabilityPreview,
9
+ type CapabilitySemanticLink,
10
+ type CapabilitySlot,
11
+ } from "../contracts/capabilities";
12
+ import { canonicalLocale } from "../shared/locale";
13
+ export const capabilityPreviewIssues = (
14
+ preview: CapabilityPreview | undefined,
15
+ links: readonly CapabilitySemanticLink[] | undefined,
16
+ sends = false,
17
+ ): string[] => {
18
+ if (!preview) return [];
19
+ const issues: string[] = [];
20
+ const open = links?.some((link) => link.rel === "open");
21
+ if ("truncated" in preview && preview.truncated && !open) issues.push("A truncated preview requires an open link");
22
+ if (preview.kind === "message") {
23
+ if ((Object.values(preview.more ?? {}).some((count) => count > 0) || (preview.moreAttachments ?? 0) > 0) && !open)
24
+ issues.push("Unlisted recipients or attachments require an open link");
25
+ if (sends && !preview.from) issues.push("A sends message requires from");
26
+ }
27
+ return issues;
28
+ };
29
+ /** Context-free rules shared by the builder and runtime validation. */
30
+ export const capabilityReviewFormIssues = (review: CapabilityActionCallReview): string[] => {
31
+ const issues: string[] = [];
32
+ if (canonicalLocale(review.locale) !== review.locale) return ["locale must be a canonical BCP 47 tag"];
33
+ for (const form of ["confirm", "decline"] as const)
34
+ if (Array.from(capabilityReviewSentence(review, form, { timeZone: "UTC" })).length > 40)
35
+ issues.push(`${form} exceeds 40 rendered characters`);
36
+ issues.push(...capabilityPreviewIssues(review.preview, review.links, review.ask === "sends"));
37
+ try {
38
+ if (new TextEncoder().encode(JSON.stringify(review)).byteLength > CAPABILITY_MAX_REVIEW_BYTES) issues.push("Review exceeds 32 KiB");
39
+ } catch {
40
+ issues.push("Review is not JSON serializable");
41
+ }
42
+ return issues;
43
+ };
44
+ export type CapabilityReviewForms = Pick<CapabilityActionCallReview, "locale" | "sentence">;
45
+ export type SlotNames<Text extends string> = string extends Text
46
+ ? string
47
+ : Text extends `${string}{${infer Name}}${infer Rest}`
48
+ ? Name | SlotNames<Rest>
49
+ : never;
50
+ type FormNames<Forms extends CapabilityReviewForms> = SlotNames<Extract<Forms["sentence"][keyof Forms["sentence"]], string>>;
51
+ type CheckedForms<Forms extends CapabilityReviewForms> =
52
+ string extends FormNames<Forms>
53
+ ? unknown
54
+ : Exclude<FormNames<Forms>, SlotNames<Forms["sentence"]["action"]>> extends never
55
+ ? unknown
56
+ : never;
57
+ type ActionSlotNames<Forms extends CapabilityReviewForms> =
58
+ string extends FormNames<Forms> ? string : SlotNames<Forms["sentence"]["action"]>;
59
+ /** Build a review without I/O. Literal forms enforce the exact slot set at compile time. */
60
+ export const actionReview = <const Forms extends CapabilityReviewForms, const Slots extends Record<string, CapabilitySlot>>(
61
+ forms: Forms & CheckedForms<Forms>,
62
+ slots: Slots & Record<ActionSlotNames<Forms>, CapabilitySlot> & Record<Exclude<keyof Slots, ActionSlotNames<Forms>>, never>,
63
+ extra?: Omit<CapabilityActionCallReview, "locale" | "sentence" | "slots">,
64
+ ): CapabilityActionCallReview => {
65
+ const review = CapabilityActionCallReviewSchema.parse({ ...forms, slots, ...extra });
66
+ const issues = capabilityReviewFormIssues(review);
67
+ if (issues.length) throw new Error(`Invalid Action review:\n- ${issues.join("\n- ")}`);
68
+ return review;
69
+ };
70
+ export const capabilityReviewRememberScope = (
71
+ review: CapabilityActionReviewData,
72
+ approval: "rememberable" | "none" | undefined,
73
+ ): string | undefined =>
74
+ approval !== "rememberable"
75
+ ? undefined
76
+ : "sentence" in review
77
+ ? review.ask === undefined
78
+ ? review.scope
79
+ : undefined
80
+ : review.approvalScope;
81
+
82
+ type Detail = NonNullable<CapabilityActionReview["details"]>[number];
83
+ /** Today's surfaces read this view; storage and digests always retain the original review. */
84
+ export const capabilityReviewView = (
85
+ review: CapabilityActionReviewData,
86
+ context: { timeZone: string },
87
+ ): Pick<CapabilityActionReview, "message" | "details" | "links"> => {
88
+ if (!("sentence" in review)) return review;
89
+ const de = review.locale.toLowerCase().startsWith("de");
90
+ const details: Detail[] = [];
91
+ const row = (en: string, german: string, value: string | number | boolean | undefined, format?: Detail["format"], block = false) => {
92
+ if (value !== undefined)
93
+ details.push({
94
+ label: de ? german : en,
95
+ value: String(value),
96
+ ...(format ? { format } : {}),
97
+ ...(block ? { display: "block" as const } : {}),
98
+ });
99
+ };
100
+ const people = (en: string, german: string, persons: Parameters<typeof capabilityPersonText>[0][] | undefined, more = 0) => {
101
+ if (persons === undefined && !more) return;
102
+ const values = (persons ?? []).map(capabilityPersonText);
103
+ if (more) values.push(de ? `und ${more} weitere` : `and ${more} more`);
104
+ row(en, german, values.join(", "));
105
+ };
106
+ const sentence = (value: { text: string; slots?: Readonly<Record<string, CapabilitySlot>> }) =>
107
+ capabilitySentenceText(value.text, value.slots ?? {}, { locale: review.locale, timeZone: context.timeZone });
108
+ const consequence = (value: NonNullable<CapabilityActionCallReview["consequences"]>[number]) =>
109
+ row(
110
+ value.mark === "added" ? "Added" : value.mark === "removed" ? "Removed" : "Consequence",
111
+ value.mark === "added" ? "Neu" : value.mark === "removed" ? "Entfernt" : "Folge",
112
+ sentence(value),
113
+ );
114
+ const p = review.preview;
115
+ if (p)
116
+ switch (p.kind) {
117
+ case "message": {
118
+ if (p.from) row("From", "Von", capabilityPersonText(p.from));
119
+ people("To", "An", p.to, p.more?.to);
120
+ people("Cc", "Cc", p.cc, p.more?.cc);
121
+ people("Bcc", "Bcc", p.bcc, p.more?.bcc);
122
+ row("Subject", "Betreff", p.subject);
123
+ const attachments = (p.attachments ?? []).map((a) => `${a.name}${a.size === undefined ? "" : ` (${a.size} B)`}`);
124
+ if (p.moreAttachments) attachments.push(de ? `und ${p.moreAttachments} weitere` : `and ${p.moreAttachments} more`);
125
+ if (p.attachments || p.moreAttachments) row("Attachments", "Anhänge", attachments.join(", "));
126
+ row("Send at", "Sendezeit", p.sendAt, "date-time");
127
+ for (const warning of p.warnings ?? []) row("Warning", "Warnung", warning);
128
+ row("Body", "Text", p.body, p.format === "markdown" ? "markdown" : undefined, true);
129
+ if (p.truncated)
130
+ row(
131
+ "Shortened",
132
+ "Gekürzt",
133
+ de ? "Den ganzen Text öffnest du über den Link unten." : "Open the full text through the link below.",
134
+ );
135
+ break;
136
+ }
137
+ case "event":
138
+ row("Title", "Titel", p.title);
139
+ row("Start", "Beginn", p.start, p.allDay || p.start.length === 10 ? "date" : "date-time");
140
+ row("End", "Ende", p.end, p.allDay || p.end?.length === 10 ? "date" : "date-time");
141
+ row("All day", "Ganztägig", p.allDay === undefined ? undefined : p.allDay ? (de ? "Ja" : "Yes") : de ? "Nein" : "No");
142
+ row("Time zone", "Zeitzone", p.timeZone);
143
+ row("Place", "Ort", p.place);
144
+ row("Location", "Adresse", p.location);
145
+ row("Recurrence", "Wiederholung", p.recurrence);
146
+ people("Attendees", "Teilnehmende", p.attendees);
147
+ row("Description", "Beschreibung", p.description, undefined, true);
148
+ break;
149
+ case "task":
150
+ row("Title", "Titel", p.title);
151
+ row("Place", "Ort", p.place);
152
+ row("Due", "Fällig", p.due, p.due?.length === 10 ? "date" : "date-time");
153
+ people("Assignees", "Zuständig", p.assignees);
154
+ row("Status", "Status", p.status);
155
+ for (const item of p.checklist ?? []) row(item.done ? "Done" : "Open", item.done ? "Erledigt" : "Offen", item.label);
156
+ if (p.tags) row("Tags", "Tags", p.tags.join(", "));
157
+ row("Description", "Beschreibung", p.description, undefined, true);
158
+ break;
159
+ case "page":
160
+ row("Title", "Titel", p.title);
161
+ row("Place", "Ort", p.place);
162
+ row("Body", "Text", p.body, p.format === "markdown" ? "markdown" : undefined, true);
163
+ if (p.truncated)
164
+ row(
165
+ "Shortened",
166
+ "Gekürzt",
167
+ de ? "Den ganzen Text öffnest du über den Link unten." : "Open the full text through the link below.",
168
+ );
169
+ for (const change of p.changes ?? [])
170
+ row(
171
+ change.change === "added" ? "Added" : "Removed",
172
+ change.change === "added" ? "Neu" : "Entfernt",
173
+ change.text,
174
+ undefined,
175
+ true,
176
+ );
177
+ break;
178
+ case "file":
179
+ row("Name", "Name", p.name);
180
+ row("Place", "Ort", p.place);
181
+ row("Path", "Pfad", p.path);
182
+ row("Size", "Größe", p.size);
183
+ row("Media type", "Medientyp", p.mediaType);
184
+ row("To", "Ziel", p.to);
185
+ break;
186
+ case "rule":
187
+ row("When", "Wenn", sentence(p.when));
188
+ row("Does", "Tut", p.does, undefined, true);
189
+ for (const right of p.may) consequence(right);
190
+ row("Sees this chat", "Kennt diesen Chat", p.seesChat ? (de ? "Ja" : "Yes") : de ? "Nein" : "No");
191
+ break;
192
+ case "fields":
193
+ for (const field of p.rows) {
194
+ row(field.label, field.label, field.value, field.format);
195
+ if (field.before !== undefined) row(`${field.label} (before)`, `${field.label} (vorher)`, field.before, field.format);
196
+ }
197
+ break;
198
+ }
199
+ for (const value of review.consequences ?? []) consequence(value);
200
+ return {
201
+ message: capabilitySentenceText(review.sentence.action, review.slots, { locale: review.locale, timeZone: context.timeZone }),
202
+ ...(details.length ? { details } : {}),
203
+ ...(review.links ? { links: review.links } : {}),
204
+ };
205
+ };
@@ -6,7 +6,7 @@ import { dispatchCapability, loadCapabilityCatalogPage } from "../api/capabiliti
6
6
  import { env } from "../config/env";
7
7
  import {
8
8
  CAPABILITY_FRAMEWORK_ERROR_CODES,
9
- CapabilityActionReviewSchema,
9
+ CapabilityActionReviewDataSchema,
10
10
  type CapabilityOrigin,
11
11
  capabilityResultSchema,
12
12
  } from "../contracts/capabilities";
@@ -263,7 +263,7 @@ export const reviewCapabilityAction = async <TInput = unknown>(
263
263
  locale: caller.locale ?? undefined,
264
264
  origin: caller.origin ?? "app",
265
265
  });
266
- return readCapabilityResponse(outcome, CapabilityActionReviewSchema);
266
+ return readCapabilityResponse(outcome, CapabilityActionReviewDataSchema);
267
267
  }
268
268
  const response = await (caller.transport ?? fetch)(
269
269
  new Request(
@@ -276,7 +276,7 @@ export const reviewCapabilityAction = async <TInput = unknown>(
276
276
  { body: JSON.stringify({ input: invocation.input }) },
277
277
  ),
278
278
  );
279
- return readCapabilityResponse(response, CapabilityActionReviewSchema);
279
+ return readCapabilityResponse(response, CapabilityActionReviewDataSchema);
280
280
  } catch (cause) {
281
281
  return unavailable(cause, undefined, caller.locale ?? undefined);
282
282
  }
@@ -312,3 +312,10 @@ export async function transferCapabilityStream(
312
312
  }
313
313
  return caller.transport ? caller.transport(request) : fetch(request, { redirect: "error" });
314
314
  }
315
+
316
+ export {
317
+ actionReview,
318
+ type CapabilityReviewForms,
319
+ capabilityReviewRememberScope,
320
+ capabilityReviewView,
321
+ } from "./reviews";
@@ -1,9 +1,19 @@
1
1
  import { capabilityManifestEvolutionIssues, compileCapabilities } from "../_internal/capabilities";
2
2
  import {
3
+ type CapabilityReviewContext,
4
+ type CapabilityReviewForms,
5
+ capabilityActionReviewIssues,
6
+ capabilityReviewFormIssues,
7
+ } from "../_internal/capability-reviews";
8
+ import { capabilitySentenceParts } from "../_internal/capability-sentences";
9
+ import {
10
+ CapabilityActionCallReviewSchema,
11
+ CapabilityActionReviewDataSchema,
3
12
  type CapabilityDefinitions,
4
13
  type CapabilityManifest,
5
14
  type CapabilityResult,
6
15
  CapabilitySemanticLinkSchema,
16
+ type CapabilitySlot,
7
17
  } from "../contracts/capabilities";
8
18
 
9
19
  /** Compile and validate the public manifest an app would register at startup. */
@@ -22,3 +32,40 @@ export const assertResourceOpenLink = (result: Pick<CapabilityResult<unknown>, "
22
32
  if (!link) throw new Error("Resource reader must return a top-level open link");
23
33
  return CapabilitySemanticLinkSchema.parse(link);
24
34
  };
35
+
36
+ /** Validate each localized review in application tests, with optional declaration context. */
37
+ export const assertCapabilityReview = (review: unknown, context?: CapabilityReviewContext): void => {
38
+ const parsed = CapabilityActionReviewDataSchema.safeParse(review);
39
+ const issues = context
40
+ ? capabilityActionReviewIssues(review, context)
41
+ : !parsed.success
42
+ ? parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
43
+ : "sentence" in parsed.data
44
+ ? capabilityReviewFormIssues(parsed.data)
45
+ : [];
46
+ if (issues.length) throw new Error(`Invalid Capability review:\n- ${issues.join("\n- ")}`);
47
+ };
48
+
49
+ /** Catalog check: the 80-character question bound is guidance for app messages, not a runtime restriction. */
50
+ export const checkActionReviewMessages = (input: {
51
+ locales: readonly string[];
52
+ forms: (locale: string) => CapabilityReviewForms;
53
+ slots: Readonly<Record<string, CapabilitySlot>>;
54
+ }): string[] =>
55
+ input.locales.flatMap((locale) => {
56
+ try {
57
+ const review = CapabilityActionCallReviewSchema.safeParse({ ...input.forms(locale), slots: input.slots });
58
+ if (!review.success) return review.error.issues.map((issue) => `${locale} ${issue.path.join(".")}: ${issue.message}`);
59
+ const issues = capabilityReviewFormIssues(review.data).map((issue) => `${locale}: ${issue}`);
60
+ const visible = capabilitySentenceParts(review.data.sentence.action, review.data.slots, {
61
+ locale: review.data.locale,
62
+ timeZone: "UTC",
63
+ })
64
+ .map((part) => (part.kind === "text" ? part.text : part.label))
65
+ .join("");
66
+ if (Array.from(`${visible}?`).length > 80) issues.push(`${locale}: question exceeds 80 visible characters`);
67
+ return issues;
68
+ } catch (error) {
69
+ return [`${locale}: ${error instanceof Error ? error.message : String(error)}`];
70
+ }
71
+ });
@@ -1,11 +1,11 @@
1
1
  import type { z } from "zod";
2
- import type { CapabilityActionReview, CapabilityCatalog, CapabilityErrorSchema, CapabilityResult } from "../contracts/capabilities";
2
+ import type { CapabilityActionReviewData, CapabilityCatalog, CapabilityErrorSchema, CapabilityResult } from "../contracts/capabilities";
3
3
 
4
4
  export type CapabilityKind = "query" | "action";
5
5
  export type CapabilityClientError = z.infer<typeof CapabilityErrorSchema> & { status: number };
6
6
  export type CapabilityResultState<T> = { ok: true; data: T } | { ok: false; error: CapabilityClientError };
7
7
  export type CapabilityClientResult<T> = CapabilityResultState<CapabilityResult<T>>;
8
- export type CapabilityReviewClientResult = CapabilityResultState<CapabilityActionReview>;
8
+ export type CapabilityReviewClientResult = CapabilityResultState<CapabilityActionReviewData>;
9
9
  export type CapabilityCatalogClientResult = CapabilityResultState<CapabilityCatalog>;
10
10
  export type CapabilityCatalogApp = CapabilityCatalog["apps"][number];
11
11
  export type CapabilityCatalogAppClientResult = CapabilityResultState<CapabilityCatalogApp | null>;
@@ -301,11 +301,21 @@ export default defineCliCommands({
301
301
  access: flag.enum(["full", "models"], { default: "full", description: "Full access or models-only inference access" }),
302
302
  },
303
303
  run: async ({ ctx, args, flags }) => {
304
- const result = await readApi<{ credential: ApiKey; token: string }>(
305
- ctx,
306
- "/api-keys",
304
+ const response = await ctx.fetch(
305
+ apiPath("/api-keys"),
307
306
  jsonRequest("POST", { name: args.name, expiresAt: parseExpiry(flags.expires), access: flags.access }),
308
307
  );
308
+ if (response.status === 403) {
309
+ const body: unknown = await response
310
+ .clone()
311
+ .json()
312
+ .catch(() => null);
313
+ const code = body && typeof body === "object" && "code" in body ? body.code : null;
314
+ const href = new URL("/me/developer", ctx.options.server).href;
315
+ if (code === "BROWSER_SIGN_IN_REQUIRED") throw new Error(`Create API keys in the browser: ${href}`);
316
+ if (code === "REAUTHENTICATE") throw new Error(`Sign in again in the browser, then create the API key: ${href}`);
317
+ }
318
+ const result = await ctx.readJson<{ credential: ApiKey; token: string }>(response);
309
319
  const modelsOnly = isModelsOnlyApiKey(result.credential);
310
320
  if (flags.access === "models" && !modelsOnly) {
311
321
  try {
@@ -0,0 +1,115 @@
1
+ import type { GeoIpAdminStatus, GeoIpResetResult } from "../../contracts/geoip";
2
+ import { arg, type CloudCliContext, cliText, command, confirmFlag, printStructured } from "../index";
3
+
4
+ const path = "/api/admin/core/geoip";
5
+ const messages = {
6
+ geoip_too_large: {
7
+ en: "The database exceeds 192 MiB unpacked. Try a smaller database.",
8
+ de: "Die Datenbank überschreitet 192 MiB entpackt. Nutze eine kleinere Datenbank.",
9
+ },
10
+ geoip_request_too_large: {
11
+ en: "The compressed upload exceeds 128 MiB. Try a smaller database.",
12
+ de: "Der komprimierte Upload überschreitet 128 MiB. Nutze eine kleinere Datenbank.",
13
+ },
14
+ geoip_empty: { en: "The uploaded file is empty.", de: "Die hochgeladene Datei ist leer." },
15
+ geoip_invalid: {
16
+ en: "Upload a valid IPv6 MMDB database with a build date.",
17
+ de: "Lade eine gültige IPv6-MMDB-Datenbank mit Erstellungsdatum hoch.",
18
+ },
19
+ geoip_unsupported_type: { en: "Only City and ASN databases are supported.", de: "Nur City- und ASN-Datenbanken werden unterstützt." },
20
+ geoip_upload_busy: {
21
+ en: "Another upload is running. Try again shortly.",
22
+ de: "Ein anderer Upload läuft. Versuche es gleich noch einmal.",
23
+ },
24
+ } as const;
25
+ const readStatus = async <T extends GeoIpAdminStatus = GeoIpAdminStatus>(ctx: CloudCliContext, response: Response): Promise<T> => {
26
+ if (!response.ok) {
27
+ const value: unknown = await response
28
+ .clone()
29
+ .json()
30
+ .catch(() => null);
31
+ if (typeof value === "object" && value !== null && "code" in value) {
32
+ for (const [code, message] of Object.entries(messages)) if (value.code === code) throw new Error(cliText(ctx, message));
33
+ }
34
+ }
35
+ return ctx.readJson<T>(response);
36
+ };
37
+ const printStatus = (ctx: CloudCliContext, status: GeoIpAdminStatus) => {
38
+ if (printStructured(ctx, status)) return;
39
+ ctx.table(
40
+ status.databases.map((database) => ({
41
+ type: database.kind,
42
+ source: database.source ?? "-",
43
+ databaseType: database.databaseType ?? "-",
44
+ built: database.buildDate ?? "-",
45
+ stale: database.stale,
46
+ uploadedBy: database.upload?.uploadedBy?.name ?? "-",
47
+ uploadedAt: database.upload?.uploadedAt ?? "-",
48
+ })),
49
+ [
50
+ { key: "type" },
51
+ { key: "source" },
52
+ { key: "databaseType" },
53
+ { key: "built" },
54
+ { key: "stale" },
55
+ { key: "uploadedBy" },
56
+ { key: "uploadedAt" },
57
+ ],
58
+ );
59
+ if (status.attribution) ctx.print(`${status.attribution.text} (${status.attribution.url}), ${status.attribution.license}`);
60
+ };
61
+ export const geoipCommands = [
62
+ command("geoip status", {
63
+ summary: "Show sign-in location databases and their build dates",
64
+ async run({ ctx }) {
65
+ printStatus(ctx, await readStatus(ctx, await ctx.fetch(path)));
66
+ },
67
+ }),
68
+ command("geoip upload", {
69
+ summary: "Replace a sign-in location database from an MMDB or gzip file",
70
+ args: { file: arg.required({ valueLabel: "file" }) },
71
+ async run({ ctx, args }) {
72
+ const file = Bun.file(args.file);
73
+ if (!(await file.exists())) throw new Error(cliText(ctx, { en: "File not found.", de: "Datei nicht gefunden." }));
74
+ // Refresh credentials on a replayable request before sending a one-shot stream. A token with only the write
75
+ // scope may not read the status; the upload's own answer reports whether it is allowed.
76
+ const preflight = await ctx.fetch(path);
77
+ if (preflight.status === 403) await preflight.body?.cancel();
78
+ else await readStatus(ctx, preflight);
79
+ const magic = new Uint8Array(await file.slice(0, 2).arrayBuffer());
80
+ const gzip = magic[0] === 0x1f && magic[1] === 0x8b;
81
+ const body = gzip ? file.stream() : file.stream().pipeThrough(new CompressionStream("gzip"));
82
+ try {
83
+ const init: RequestInit & { duplex: "half" } = {
84
+ method: "PUT",
85
+ headers: { "Content-Type": "application/gzip" },
86
+ body,
87
+ duplex: "half",
88
+ };
89
+ printStatus(ctx, await readStatus(ctx, await ctx.fetch(path, init)));
90
+ } finally {
91
+ if (!body.locked) await body.cancel().catch(() => {});
92
+ }
93
+ },
94
+ }),
95
+ command("geoip reset", {
96
+ summary: "Return one or both sign-in location databases to the bundled snapshot",
97
+ args: { kind: arg.optional({ valueLabel: "city|asn" }) },
98
+ flags: { yes: confirmFlag("Reset sign-in location databases") },
99
+ async run({ ctx, args, flags }) {
100
+ if (!flags.yes) throw new Error(cliText(ctx, { en: "Refusing to reset without --yes.", de: "Ohne --yes wird nicht zurückgesetzt." }));
101
+ if (args.kind !== undefined && args.kind !== "city" && args.kind !== "asn")
102
+ throw new Error(cliText(ctx, { en: "Choose city or asn.", de: "Wähle city oder asn." }));
103
+ let status: GeoIpResetResult | undefined;
104
+ let changed = false;
105
+ for (const kind of args.kind ? [args.kind] : ["city", "asn"]) {
106
+ status = await readStatus<GeoIpResetResult>(ctx, await ctx.fetch(`${path}/${kind}`, { method: "DELETE" }));
107
+ changed ||= status.changed;
108
+ }
109
+ if (status) {
110
+ status.changed = changed;
111
+ printStatus(ctx, status);
112
+ }
113
+ },
114
+ }),
115
+ ];
@@ -17,6 +17,7 @@ import { appSignInCommands } from "./app-sign-in";
17
17
  import { dataCommands } from "./data";
18
18
  import { documentationCommands } from "./documentation";
19
19
  import { gatewayCommands } from "./gateway";
20
+ import { geoipCommands } from "./geoip";
20
21
  import { instanceCommands } from "./instance";
21
22
  import { jobCommands } from "./jobs";
22
23
  import { legalCommands } from "./legal";
@@ -49,6 +50,7 @@ export default defineCliCommands({
49
50
  "app-sign-in": "Manage app sign-in and pairing.",
50
51
  apps: "Inspect and remove registered apps.",
51
52
  documentation: "Manage the administration documentation website.",
53
+ geoip: "Replace and inspect sign-in location databases.",
52
54
  jobs: "Inspect background jobs and their runs.",
53
55
  logs: "Search, inspect, and clean up retained logs.",
54
56
  metrics: "Inspect collectors and manage metrics access.",
@@ -99,6 +101,7 @@ export default defineCliCommands({
99
101
  ...telemetryCommands,
100
102
  ...jobCommands,
101
103
  ...legalCommands,
104
+ ...geoipCommands,
102
105
  ...linuxCommands,
103
106
  ...workflowCommands,
104
107
  ...dataCommands,