@k2b/cloud 0.43.0 → 0.44.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 (267) hide show
  1. package/package.json +7 -2
  2. package/src/_internal/app-access-request.ts +13 -0
  3. package/src/_internal/app-access-resources.ts +141 -0
  4. package/src/_internal/app-event-catalog.ts +40 -0
  5. package/src/_internal/app-event-schemas.ts +127 -0
  6. package/src/_internal/application-package.ts +40 -0
  7. package/src/_internal/built-in-app-ids.json +27 -0
  8. package/src/_internal/capabilities.ts +158 -12
  9. package/src/_internal/capability-origin.ts +4 -0
  10. package/src/_internal/capability-streams.ts +7 -5
  11. package/src/_internal/define-app.ts +57 -6
  12. package/src/_internal/object-store-area.ts +9 -0
  13. package/src/_internal/object-store-config.ts +101 -0
  14. package/src/_internal/object-store-key.ts +14 -0
  15. package/src/_internal/process-identity.ts +7 -2
  16. package/src/_internal/registry-validation.ts +11 -1
  17. package/src/_internal/registry.ts +8 -1
  18. package/src/_internal/runtime-context.ts +8 -1
  19. package/src/access/AppAccessEditor.tsx +2 -0
  20. package/src/access/PermissionEditor.tsx +9 -3
  21. package/src/access/PrincipalPicker.tsx +14 -4
  22. package/src/access/ResourceApiKeys.tsx +3 -4
  23. package/src/access/app-access-messages.ts +6 -0
  24. package/src/account/ProfilePictureDialog.tsx +699 -0
  25. package/src/account/profile-picture-messages.ts +269 -0
  26. package/src/account/ui.ts +7 -0
  27. package/src/ai/admin.ts +3 -0
  28. package/src/ai/app-frame.ts +12 -0
  29. package/src/ai/application-tasks.ts +101 -0
  30. package/src/ai/assistant-models.ts +126 -64
  31. package/src/ai/capabilities.ts +19 -4
  32. package/src/ai/chart-block.ts +63 -30
  33. package/src/ai/chat/chart-block.tsx +12 -3
  34. package/src/ai/chat/composer-adapter.ts +8 -7
  35. package/src/ai/chat/message-actions.tsx +1 -1
  36. package/src/ai/chat/message-utils.ts +2 -2
  37. package/src/ai/chat/presentation.tsx +22 -1
  38. package/src/ai/chat-quotas.ts +5 -4
  39. package/src/ai/chat-tasks.ts +2 -2
  40. package/src/ai/client/controller.ts +11 -10
  41. package/src/ai/client/projection.ts +3 -3
  42. package/src/ai/compaction.ts +3 -3
  43. package/src/ai/credentials.ts +13 -114
  44. package/src/ai/default-tool-contracts.ts +4 -3
  45. package/src/ai/dictation-runtime.ts +4 -4
  46. package/src/ai/enrich.ts +2 -2
  47. package/src/ai/executor.ts +31 -22
  48. package/src/ai/http.ts +17 -29
  49. package/src/ai/index.ts +11 -5
  50. package/src/ai/inference-calls.ts +5 -4
  51. package/src/ai/maintenance.ts +1 -1
  52. package/src/ai/memory-learning-runs.ts +5 -5
  53. package/src/ai/memory-learning.ts +3 -3
  54. package/src/ai/message-queue.ts +1 -2
  55. package/src/ai/migrate.ts +75 -42
  56. package/src/ai/model-access-migrate.ts +3 -17
  57. package/src/ai/model-access.ts +44 -85
  58. package/src/ai/model-card.tsx +134 -0
  59. package/src/ai/model-catalog.ts +359 -0
  60. package/src/ai/model-chooser-messages.ts +88 -0
  61. package/src/ai/model-chooser.tsx +577 -0
  62. package/src/ai/model-delete.ts +234 -0
  63. package/src/ai/model-fixture.ts +39 -0
  64. package/src/ai/model-icon.tsx +36 -0
  65. package/src/ai/model-overview.ts +53 -0
  66. package/src/ai/model-usage.ts +45 -0
  67. package/src/ai/openai-api-messages.ts +0 -4
  68. package/src/ai/openai-api.ts +26 -9
  69. package/src/ai/personal-agent.ts +1 -1
  70. package/src/ai/prefs.ts +12 -10
  71. package/src/ai/projects-routes.ts +2 -1
  72. package/src/ai/projects.ts +13 -12
  73. package/src/ai/protocol.ts +2 -2
  74. package/src/ai/provider-connections.ts +161 -0
  75. package/src/ai/provider-errors.ts +55 -6
  76. package/src/ai/provider-fetch.ts +4 -0
  77. package/src/ai/provider.ts +1 -1
  78. package/src/ai/quota-limits.ts +36 -0
  79. package/src/ai/quota-provider.ts +4 -2
  80. package/src/ai/quota-report.ts +7 -4
  81. package/src/ai/quotas.ts +19 -8
  82. package/src/ai/recent-resources.ts +6 -0
  83. package/src/ai/request-headers.ts +14 -87
  84. package/src/ai/resource-refs.ts +6 -2
  85. package/src/ai/routes.ts +17 -18
  86. package/src/ai/runtime.ts +50 -22
  87. package/src/ai/settings.ts +93 -580
  88. package/src/ai/store.ts +45 -23
  89. package/src/ai/stream.ts +1 -1
  90. package/src/ai/structured-runs.ts +2 -2
  91. package/src/ai/structured.ts +21 -44
  92. package/src/ai/task-contracts.ts +25 -5
  93. package/src/ai/task-execution.ts +15 -6
  94. package/src/ai/task-model.ts +9 -0
  95. package/src/ai/transcription.ts +27 -22
  96. package/src/ai/types.ts +50 -62
  97. package/src/ai/ui.tsx +13 -0
  98. package/src/ai/usage.ts +8 -8
  99. package/src/ai/validate.ts +8 -7
  100. package/src/ai/view-block.ts +2 -2
  101. package/src/api/accounts-entities.ts +16 -0
  102. package/src/api/admin-ai-quotas.ts +34 -56
  103. package/src/api/admin-ai-usage.ts +1 -1
  104. package/src/api/admin-ai.ts +173 -0
  105. package/src/api/admin-core-settings.ts +68 -244
  106. package/src/api/app-approval.ts +3 -1
  107. package/src/api/app-event-delivery.ts +51 -0
  108. package/src/api/app-events.ts +123 -0
  109. package/src/api/app-resource-access.ts +48 -0
  110. package/src/api/auth/account-mismatch.ts +24 -0
  111. package/src/api/auth.ts +26 -3
  112. package/src/api/capabilities.ts +28 -4
  113. package/src/api/index.ts +7 -1
  114. package/src/api/mcp.ts +175 -2
  115. package/src/api/me.ts +6 -4
  116. package/src/api/pwa-phone.ts +75 -26
  117. package/src/api/pwa.ts +34 -8
  118. package/src/api/resource-links.ts +83 -6
  119. package/src/api/resource-resolve.ts +50 -0
  120. package/src/api/search/schemas.ts +3 -1
  121. package/src/api/search.ts +9 -7
  122. package/src/app-pwa/contracts.ts +4 -0
  123. package/src/app-pwa/core-query-error.ts +20 -0
  124. package/src/app-pwa/index.ts +1 -0
  125. package/src/app-pwa/service.ts +91 -0
  126. package/src/app-pwa/transport.ts +22 -0
  127. package/src/approvals/messages.ts +2 -2
  128. package/src/browser/cloud-id.ts +86 -0
  129. package/src/browser/resource-clipboard.ts +4 -0
  130. package/src/browser/resource-links.ts +52 -0
  131. package/src/capabilities/server-invoke.ts +2 -11
  132. package/src/capabilities/testing.ts +5 -2
  133. package/src/cli/access.ts +6 -5
  134. package/src/cli/admin/ai-models.ts +199 -0
  135. package/src/cli/admin/ai-quotas.ts +6 -102
  136. package/src/cli/admin/ai-usage.ts +4 -4
  137. package/src/cli/admin/app-credentials.ts +5 -3
  138. package/src/cli/admin/index.ts +5 -2
  139. package/src/cli/apps.ts +89 -26
  140. package/src/cli/capabilities.ts +22 -1
  141. package/src/cli/events.ts +320 -0
  142. package/src/config/env.ts +40 -0
  143. package/src/contracts/app-access.ts +26 -2
  144. package/src/contracts/app-details.ts +89 -0
  145. package/src/contracts/app-events.ts +51 -0
  146. package/src/contracts/app.ts +21 -5
  147. package/src/contracts/capabilities.ts +143 -3
  148. package/src/contracts/index.ts +2 -0
  149. package/src/contracts/notification-types.ts +36 -9
  150. package/src/contracts/pwa.ts +17 -1
  151. package/src/contracts/registry.ts +6 -0
  152. package/src/contracts/resource-clipboard.ts +35 -0
  153. package/src/contracts/resource-pages.ts +140 -0
  154. package/src/contracts/resource-resolution.ts +13 -0
  155. package/src/contracts/shared.ts +4 -0
  156. package/src/contracts/user-notifications.ts +2 -0
  157. package/src/events/app-event-delivery.ts +100 -0
  158. package/src/events/app-events.testing.ts +65 -0
  159. package/src/events/app-events.ts +134 -0
  160. package/src/events/core-user-created.ts +24 -0
  161. package/src/events/index.ts +9 -0
  162. package/src/events/testing.ts +2 -0
  163. package/src/index.ts +1 -1
  164. package/src/server/api/respond.ts +8 -3
  165. package/src/server/index.ts +1 -0
  166. package/src/server/middleware/app-access.ts +1 -1
  167. package/src/server/middleware/auth.ts +21 -7
  168. package/src/server/middleware/index.ts +1 -0
  169. package/src/server/middleware/invocation.ts +2 -0
  170. package/src/server/public-storage.ts +106 -0
  171. package/src/server/services/access.ts +8 -0
  172. package/src/services/accounts/app.ts +6 -0
  173. package/src/services/accounts/entities.ts +14 -3
  174. package/src/services/accounts/groups.ts +3 -1
  175. package/src/services/accounts/identities.ts +62 -10
  176. package/src/services/accounts/users.ts +7 -6
  177. package/src/services/app-access.ts +25 -7
  178. package/src/services/app-details.ts +224 -0
  179. package/src/services/app-discovery.ts +2 -1
  180. package/src/services/app-event-shapes.ts +139 -0
  181. package/src/services/app-events/accept.ts +116 -0
  182. package/src/services/app-events/admin.ts +250 -0
  183. package/src/services/app-events/declarations.ts +187 -0
  184. package/src/services/app-events/deliver.ts +218 -0
  185. package/src/services/app-events.ts +92 -0
  186. package/src/services/config-conflict.ts +11 -0
  187. package/src/services/core-user-events.ts +1 -0
  188. package/src/services/freeipa-config.ts +9 -0
  189. package/src/services/geo/ledger.ts +28 -10
  190. package/src/services/geo/service.ts +43 -24
  191. package/src/services/identity/core-origin.ts +12 -0
  192. package/src/services/identity/invocation-actor.ts +20 -2
  193. package/src/services/identity/invocation-authority.ts +4 -0
  194. package/src/services/identity/invocation-token.ts +238 -56
  195. package/src/services/identity/issue-invocation.ts +6 -1
  196. package/src/services/image-metadata.ts +65 -1
  197. package/src/services/index.ts +13 -1
  198. package/src/services/ipa/sync.ts +6 -2
  199. package/src/services/ipa/users.ts +4 -1
  200. package/src/services/mandates/policy.ts +19 -12
  201. package/src/services/notifications/browser.ts +4 -0
  202. package/src/services/notifications/catalog.ts +5 -2
  203. package/src/services/notifications/platform.ts +7 -2
  204. package/src/services/notifications/user.ts +18 -7
  205. package/src/services/oauth-tokens.ts +28 -2
  206. package/src/services/object-store.ts +217 -0
  207. package/src/services/providers/local/users.ts +5 -0
  208. package/src/services/pwa-devices.ts +147 -37
  209. package/src/services/session/index.ts +14 -0
  210. package/src/services/settings/core-settings.ts +71 -43
  211. package/src/services/settings/index.ts +8 -0
  212. package/src/services/settings/versions.ts +26 -0
  213. package/src/shared/ai-models.ts +274 -0
  214. package/src/shared/ai-quotas.ts +2 -2
  215. package/src/shared/ai-usage.ts +2 -2
  216. package/src/shared/index.ts +2 -0
  217. package/src/shared-text/documents.ts +25 -8
  218. package/src/shared-text/log.ts +1 -1
  219. package/src/shared-text/text-diff.ts +75 -0
  220. package/src/ssr/AdminArea.tsx +131 -0
  221. package/src/ssr/AdminLayout.tsx +9 -3
  222. package/src/ssr/AdminSidebar.tsx +7 -7
  223. package/src/ssr/AppAccessDenied.tsx +2 -0
  224. package/src/ssr/AppBadge.tsx +14 -0
  225. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  226. package/src/ssr/CloudShell.island.tsx +25 -4
  227. package/src/ssr/Layout.tsx +8 -6
  228. package/src/ssr/PwaLayout.tsx +16 -1
  229. package/src/ssr/PwaPartCount.island.tsx +23 -0
  230. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  231. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  232. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  233. package/src/ssr/admin-active-link.ts +16 -0
  234. package/src/ssr/admin-navigation.ts +25 -20
  235. package/src/ssr/app-badges.ts +31 -7
  236. package/src/ssr/index.ts +8 -0
  237. package/src/ssr/page-trail-runtime.ts +3 -1
  238. package/src/ssr/platform-messages.ts +9 -1
  239. package/src/ssr/pwa-messages.ts +2 -2
  240. package/src/ssr/rail-navigation.ts +5 -1
  241. package/src/ssr/settings-draft.ts +153 -0
  242. package/src/ssr/shell-data.tsx +2 -0
  243. package/src/styles/global.css +1 -0
  244. package/src/styles/profile-picture.css +270 -0
  245. package/src/styles/utilities-navigation.css +6 -0
  246. package/src/workflows/ai/policy.ts +49 -0
  247. package/src/workflows/ai/runtime.ts +131 -21
  248. package/src/workflows/ai/store.ts +283 -35
  249. package/src/workflows/ai/types.ts +8 -2
  250. package/src/workflows/ai-actions.ts +148 -120
  251. package/src/workflows/authoring.ts +31 -0
  252. package/src/workflows/contracts.ts +1 -0
  253. package/src/workflows/definition.ts +2 -0
  254. package/src/workflows/editor.ts +15 -0
  255. package/src/workflows/index.ts +2 -1
  256. package/src/workflows/language/compiler.ts +34 -3
  257. package/src/workflows/runtime/executor.ts +132 -33
  258. package/src/workflows/runtime/index.ts +2 -0
  259. package/src/workflows/runtime/ports.ts +5 -1
  260. package/src/workflows/store/actions.ts +1 -1
  261. package/src/workflows/store/definitions.ts +5 -0
  262. package/src/workflows/store/runs.ts +5 -3
  263. package/src/workflows/store/worker.ts +7 -2
  264. package/src/ai/model-pricing.ts +0 -43
  265. package/src/ai/model-request-settings.ts +0 -98
  266. package/src/ai/settings-messages.ts +0 -129
  267. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -0,0 +1,269 @@
1
+ import { i18n } from "@k2b/stdlib";
2
+
3
+ type OptionLabels = Record<string, Record<string, string>>;
4
+
5
+ const optionLabel = (labels: OptionLabels, part: string, id: string): string => labels[part]?.[id] ?? id;
6
+
7
+ const OPTIONS_EN = {
8
+ face: { round: "Round", oval: "Oval", square: "Square" },
9
+ skin: { s1: "Light", s2: "Light medium", s3: "Medium", s4: "Medium dark", s5: "Dark", s6: "Very dark" },
10
+ hair: {
11
+ none: "None",
12
+ short: "Short",
13
+ buzz: "Very short",
14
+ curly: "Curls",
15
+ afro: "Afro",
16
+ long: "Long",
17
+ bob: "Bob",
18
+ bun: "Bun",
19
+ locs: "Locs",
20
+ },
21
+ hairColor: {
22
+ black: "Black",
23
+ dark: "Dark brown",
24
+ brown: "Brown",
25
+ blond: "Blond",
26
+ red: "Red",
27
+ grey: "Grey",
28
+ white: "White",
29
+ blue: "Blue",
30
+ pink: "Pink",
31
+ },
32
+ eyes: { dots: "Dots", ovals: "Oval", happy: "Happy", wink: "Wink", calm: "Calm" },
33
+ brows: { none: "None", straight: "Straight", arched: "Arched", thick: "Strong", raised: "Raised" },
34
+ mouth: { smile: "Smile", grin: "Laugh", neutral: "Neutral", oh: "Surprised", smirk: "Smirk" },
35
+ beard: { none: "None", stubble: "Stubble", moustache: "Moustache", goatee: "Goatee", full: "Full beard" },
36
+ glasses: { none: "None", round: "Round", square: "Square", half: "Half rim", sun: "Sunglasses" },
37
+ head: { none: "None", beanie: "Beanie", cap: "Cap", hijab: "Headscarf", turban: "Turban", kippa: "Kippah", phones: "Headphones" },
38
+ extra: { none: "None", freckles: "Freckles", blush: "Cheeks", earrings: "Earrings", mole: "Beauty mark" },
39
+ bg: { sky: "Sky", mint: "Mint", sand: "Sand", rose: "Rose", lilac: "Lilac", stone: "Stone", peach: "Peach", teal: "Teal" },
40
+ robotHead: { square: "Square", round: "Round", capsule: "Capsule", tv: "Screen" },
41
+ antenna: { none: "None", ball: "Ball", double: "Double", dish: "Dish" },
42
+ robotEyes: { round: "Round", visor: "Visor", pixel: "Pixel", cyclops: "One eye", happy: "Happy" },
43
+ robotMouth: { grill: "Grille", smile: "Smile", led: "Lights", line: "Straight", none: "None" },
44
+ body: { plain: "Plain", panel: "Panel", light: "Light", buttons: "Buttons" },
45
+ color: {
46
+ steel: "Steel",
47
+ white: "White",
48
+ blue: "Blue",
49
+ teal: "Turquoise",
50
+ coral: "Coral",
51
+ yellow: "Yellow",
52
+ purple: "Purple",
53
+ graphite: "Graphite",
54
+ },
55
+ } as const satisfies OptionLabels;
56
+
57
+ const OPTIONS_DE = {
58
+ face: { round: "Rund", oval: "Oval", square: "Eckig" },
59
+ skin: { s1: "Hell", s2: "Hell-mittel", s3: "Mittel", s4: "Mittel-dunkel", s5: "Dunkel", s6: "Sehr dunkel" },
60
+ hair: {
61
+ none: "Ohne",
62
+ short: "Kurz",
63
+ buzz: "Sehr kurz",
64
+ curly: "Locken",
65
+ afro: "Afro",
66
+ long: "Lang",
67
+ bob: "Bob",
68
+ bun: "Dutt",
69
+ locs: "Locs",
70
+ },
71
+ hairColor: {
72
+ black: "Schwarz",
73
+ dark: "Dunkelbraun",
74
+ brown: "Braun",
75
+ blond: "Blond",
76
+ red: "Rot",
77
+ grey: "Grau",
78
+ white: "Weiß",
79
+ blue: "Blau",
80
+ pink: "Pink",
81
+ },
82
+ eyes: { dots: "Punkte", ovals: "Oval", happy: "Fröhlich", wink: "Zwinkern", calm: "Ruhig" },
83
+ brows: { none: "Ohne", straight: "Gerade", arched: "Bogen", thick: "Kräftig", raised: "Fragend" },
84
+ mouth: { smile: "Lächeln", grin: "Lachen", neutral: "Neutral", oh: "Staunen", smirk: "Schmunzeln" },
85
+ beard: { none: "Ohne", stubble: "Stoppeln", moustache: "Schnurrbart", goatee: "Kinnbart", full: "Vollbart" },
86
+ glasses: { none: "Ohne", round: "Rund", square: "Eckig", half: "Halbrand", sun: "Sonnenbrille" },
87
+ head: { none: "Ohne", beanie: "Mütze", cap: "Kappe", hijab: "Kopftuch", turban: "Turban", kippa: "Kippa", phones: "Kopfhörer" },
88
+ extra: { none: "Ohne", freckles: "Sommersprossen", blush: "Wangen", earrings: "Ohrringe", mole: "Muttermal" },
89
+ bg: { sky: "Himmel", mint: "Minze", sand: "Sand", rose: "Rose", lilac: "Flieder", stone: "Stein", peach: "Pfirsich", teal: "Petrol" },
90
+ robotHead: { square: "Eckig", round: "Rund", capsule: "Kapsel", tv: "Bildschirm" },
91
+ antenna: { none: "Ohne", ball: "Kugel", double: "Doppelt", dish: "Schüssel" },
92
+ robotEyes: { round: "Rund", visor: "Visier", pixel: "Pixel", cyclops: "Ein Auge", happy: "Fröhlich" },
93
+ robotMouth: { grill: "Gitter", smile: "Lächeln", led: "Lichter", line: "Gerade", none: "Ohne" },
94
+ body: { plain: "Schlicht", panel: "Klappe", light: "Licht", buttons: "Knöpfe" },
95
+ color: {
96
+ steel: "Stahl",
97
+ white: "Weiß",
98
+ blue: "Blau",
99
+ teal: "Türkis",
100
+ coral: "Koralle",
101
+ yellow: "Gelb",
102
+ purple: "Lila",
103
+ graphite: "Graphit",
104
+ },
105
+ } as const satisfies OptionLabels;
106
+
107
+ /** Words of the profile picture dialog; option names follow the shape set ids (`option({ part, id })`). */
108
+ export const profilePictureMessages = i18n.define({
109
+ baseLocale: "en",
110
+ messages: {
111
+ en: {
112
+ title: "Change profile picture",
113
+ close: "Close",
114
+ ways: "Kind of profile picture",
115
+ photo: "Photo",
116
+ avatar: "Avatar",
117
+ robot: "Robot",
118
+ initials: "Initials",
119
+ active: "Active",
120
+ cancel: "Cancel",
121
+ save: "Save",
122
+ saving: "Saving",
123
+ retry: "Retry",
124
+ unsaved: "Not saved yet",
125
+ visiblePerson: "Visible to everyone signed in to this Cloud.",
126
+ visibleAgent: "Visible to everyone who can see this agent.",
127
+ saveFailed: "Not saved. The previous profile picture stays, your choice is still here.",
128
+ loading: "Loading profile picture",
129
+ loadFailed: "The profile picture could not be loaded.",
130
+ discardTitle: "Discard changes?",
131
+ discardBody: "Your changes are not saved. The previous profile picture stays.",
132
+ discardConfirm: "Discard",
133
+ discardCancel: "Keep editing",
134
+ preview: "Preview",
135
+ previewSizes: "Preview in Cloud's sizes",
136
+ previewDark: "Preview in the dark theme",
137
+ random: "Random",
138
+ undo: "Undo",
139
+ reset: "Reset",
140
+ parts: "Parts",
141
+ chosen: ({ name }: { name: string }) => `${name} chosen`,
142
+ randomized: "Random choice",
143
+ undone: "Undone",
144
+ resetDone: "Reset",
145
+ hairHidden: ({ cover }: { cover: string }) => `${cover} covers the hair. Your choice stays saved.`,
146
+ beardColour: "The beard has your hair colour.",
147
+ photoDrop: "Drop a photo here",
148
+ photoFormats: "PNG, JPEG, or WebP. Then you crop the picture.",
149
+ photoChoose: "Choose photo",
150
+ photoOther: "Other photo",
151
+ photoDelete: "Delete photo",
152
+ photoDeleted: "Photo deleted",
153
+ photoNew: "New photo, cropped. Save makes it your profile picture.",
154
+ photoActive: "Your photo is your profile picture.",
155
+ photoHidden: "Your photo is stored but not visible right now. Others see only the active profile picture.",
156
+ photoDeleteFailed: "The photo could not be deleted.",
157
+ photoTypeInvalid: "Choose a PNG, JPEG, or WebP image.",
158
+ photoTooLarge: "Choose an image smaller than 32 MB.",
159
+ photoEmpty: "This image is empty. Choose another one.",
160
+ photoUnsupported: "This browser cannot prepare the image. Try another browser.",
161
+ photoProcessFailed: "This image could not be prepared as a small picture. Try a simpler image.",
162
+ initialsInfo: "Cloud builds the initials from your name. They change when your name changes.",
163
+ // Parts
164
+ face: "Face",
165
+ faceShape: "Face shape",
166
+ skin: "Skin tone",
167
+ hair: "Hair",
168
+ hairstyle: "Hairstyle",
169
+ hairColor: "Hair colour",
170
+ eyes: "Eyes",
171
+ brows: "Brows",
172
+ eyebrows: "Eyebrows",
173
+ mouth: "Mouth",
174
+ beard: "Beard",
175
+ glasses: "Glasses",
176
+ head: "Head",
177
+ headwear: "Headwear",
178
+ extra: "Extras",
179
+ colour: "Colour",
180
+ backgroundAndClothes: "Background and clothes",
181
+ antenna: "Antenna",
182
+ body: "Body",
183
+ colours: "Colours",
184
+ casing: "Casing",
185
+ background: "Background",
186
+ option: ({ part, id }: { part: string; id: string }) => optionLabel(OPTIONS_EN, part, id),
187
+ hijabCover: "The headscarf",
188
+ turbanCover: "The turban",
189
+ },
190
+ de: {
191
+ title: "Profilbild ändern",
192
+ close: "Schließen",
193
+ ways: "Art des Profilbilds",
194
+ photo: "Foto",
195
+ avatar: "Avatar",
196
+ robot: "Roboter",
197
+ initials: "Initialen",
198
+ active: "Aktiv",
199
+ cancel: "Abbrechen",
200
+ save: "Speichern",
201
+ saving: "Wird gespeichert",
202
+ retry: "Erneut versuchen",
203
+ unsaved: "Noch nicht gespeichert",
204
+ visiblePerson: "Sichtbar für alle angemeldeten Personen dieser Cloud.",
205
+ visibleAgent: "Sichtbar für alle, die diesen Agenten sehen.",
206
+ saveFailed: "Nicht gespeichert. Das bisherige Profilbild bleibt, deine Auswahl ist noch da.",
207
+ loading: "Profilbild wird geladen",
208
+ loadFailed: "Das Profilbild konnte nicht geladen werden.",
209
+ discardTitle: "Änderungen verwerfen?",
210
+ discardBody: "Deine Änderungen sind nicht gespeichert. Das bisherige Profilbild bleibt.",
211
+ discardConfirm: "Verwerfen",
212
+ discardCancel: "Weiter bearbeiten",
213
+ preview: "Vorschau",
214
+ previewSizes: "Vorschau in den Größen von Cloud",
215
+ previewDark: "Vorschau im dunklen Design",
216
+ random: "Zufall",
217
+ undo: "Rückgängig",
218
+ reset: "Zurücksetzen",
219
+ parts: "Teile",
220
+ chosen: ({ name }: { name: string }) => `${name} gewählt`,
221
+ randomized: "Zufällige Auswahl",
222
+ undone: "Rückgängig gemacht",
223
+ resetDone: "Zurückgesetzt",
224
+ hairHidden: ({ cover }: { cover: string }) => `${cover} verdeckt die Haare. Deine Wahl bleibt gespeichert.`,
225
+ beardColour: "Der Bart hat deine Haarfarbe.",
226
+ photoDrop: "Foto hierher ziehen",
227
+ photoFormats: "PNG, JPEG oder WebP. Danach schneidest du den Ausschnitt zu.",
228
+ photoChoose: "Foto auswählen",
229
+ photoOther: "Anderes Foto",
230
+ photoDelete: "Foto löschen",
231
+ photoDeleted: "Foto gelöscht",
232
+ photoNew: "Neues Foto, zugeschnitten. Mit „Speichern“ wird es dein Profilbild.",
233
+ photoActive: "Dein Foto ist dein Profilbild.",
234
+ photoHidden: "Dein Foto ist gespeichert, aber gerade nicht sichtbar. Andere sehen nur das aktive Profilbild.",
235
+ photoDeleteFailed: "Das Foto konnte nicht gelöscht werden.",
236
+ photoTypeInvalid: "Wähle ein PNG-, JPEG- oder WebP-Bild.",
237
+ photoTooLarge: "Wähle ein Bild unter 32 MB.",
238
+ photoEmpty: "Dieses Bild ist leer. Wähle ein anderes.",
239
+ photoUnsupported: "Dieser Browser kann das Bild nicht vorbereiten. Versuche einen anderen Browser.",
240
+ photoProcessFailed: "Dieses Bild konnte nicht als kleines Bild vorbereitet werden. Versuche ein einfacheres Bild.",
241
+ initialsInfo: "Cloud bildet die Initialen aus deinem Namen. Sie ändern sich, wenn sich dein Name ändert.",
242
+ face: "Gesicht",
243
+ faceShape: "Gesichtsform",
244
+ skin: "Hautton",
245
+ hair: "Haare",
246
+ hairstyle: "Frisur",
247
+ hairColor: "Haarfarbe",
248
+ eyes: "Augen",
249
+ brows: "Brauen",
250
+ eyebrows: "Augenbrauen",
251
+ mouth: "Mund",
252
+ beard: "Bart",
253
+ glasses: "Brille",
254
+ head: "Kopf",
255
+ headwear: "Kopfbedeckung",
256
+ extra: "Extras",
257
+ colour: "Farbe",
258
+ backgroundAndClothes: "Hintergrund und Kleidung",
259
+ antenna: "Antenne",
260
+ body: "Körper",
261
+ colours: "Farben",
262
+ casing: "Gehäuse",
263
+ background: "Hintergrund",
264
+ option: ({ part, id }: { part: string; id: string }) => optionLabel(OPTIONS_DE, part, id),
265
+ hijabCover: "Das Kopftuch",
266
+ turbanCover: "Der Turban",
267
+ },
268
+ },
269
+ });
package/src/account/ui.ts CHANGED
@@ -1,3 +1,10 @@
1
1
  export { CloudAvatar, type CloudAvatarKind, type CloudAvatarProps, cloudAvatarImage, cloudAvatarSource } from "./Avatar";
2
2
  export { type AvatarUploadDialogOptions, openAvatarUploadDialog } from "./avatar-upload-dialog";
3
3
  export { default as EntitySearch, type EntitySearchPrincipal } from "./EntitySearch";
4
+ export {
5
+ openProfilePictureDialog,
6
+ type ProfilePictureChange,
7
+ type ProfilePictureDialogOptions,
8
+ type ProfilePictureState,
9
+ } from "./ProfilePictureDialog";
10
+ export { profilePictureMessages } from "./profile-picture-messages";
package/src/ai/admin.ts CHANGED
@@ -1,3 +1,6 @@
1
+ export { aiModelCatalog } from "./model-catalog";
2
+ export { type AiAdminOverview, readAiAdminOverview } from "./model-overview";
3
+ export { aiModelUsage } from "./model-usage";
1
4
  export { quotaAdminConfig, quotaReport } from "./quota-report";
2
5
  export { AiQuotaError, aiQuotas } from "./quotas";
3
6
  export { listAiRequestHeaderNames } from "./request-headers";
@@ -0,0 +1,12 @@
1
+ // The chart, view and task contracts that an HTML app frame draws and checks. Its prelude is inlined in every app
2
+ // document, so it imports this entry instead of `./browser`, whose schemas and skill parser (with YAML) it never uses.
3
+ export {
4
+ type CloudAiChartTable,
5
+ cloudAiChartMarkKey,
6
+ cloudAiChartRenderOptions,
7
+ cloudAiChartRows,
8
+ cloudAiChartTooltip,
9
+ } from "./chart-block";
10
+ export { type CloudAiChartInput, type CloudAiFormat, type CloudAiNumberFormat, cloudAiAppChartSchema } from "./default-tool-contracts";
11
+ export { type AiTaskRequestInput, AiTaskRequestSchema } from "./task-contracts";
12
+ export { CloudAiViewSummarySchema } from "./view-block";
@@ -0,0 +1,101 @@
1
+ import type { SQL } from "bun";
2
+ import { env } from "../config/env";
3
+ import type { AccessSubject } from "../server/services/access";
4
+ import { WorkflowAiAdmissionError, type WorkflowAiScope, workflowAiHash, workflowAiModelHash } from "../workflows/ai/policy";
5
+ import { submitWorkflowAiTask } from "../workflows/ai/runtime";
6
+ import {
7
+ createWorkflowAiTask,
8
+ getWorkflowAiTask,
9
+ getWorkflowAiTaskByEffectKey,
10
+ releaseApplicationAiTaskByEffectKey,
11
+ } from "../workflows/ai/store";
12
+ import { type AiTaskRequestInput, assertAiTaskRequestSize } from "./task-contracts";
13
+ import { resolveApplicationAiTaskModel } from "./task-model";
14
+
15
+ export type ApplicationAiTaskInput = {
16
+ appId: string;
17
+ scopeId: string;
18
+ effectKey: string;
19
+ task: string;
20
+ request: AiTaskRequestInput;
21
+ source: WorkflowAiScope;
22
+ usageSubject?: AccessSubject;
23
+ signal: AbortSignal;
24
+ };
25
+
26
+ export type ApplicationAiTaskKey = Pick<ApplicationAiTaskInput, "appId" | "scopeId" | "effectKey" | "usageSubject">;
27
+
28
+ const applicationEffectKey = (input: ApplicationAiTaskKey): string =>
29
+ `application:${workflowAiHash({ app: input.appId, scope: input.scopeId, subject: input.usageSubject ?? null, effect: input.effectKey })}`;
30
+
31
+ /** Call in the transaction settling the application request, including discarded or failed requests. */
32
+ export const releaseApplicationAiTask = (input: ApplicationAiTaskKey, db?: SQL): Promise<boolean> =>
33
+ releaseApplicationAiTaskByEffectKey({ appId: input.appId, scopeId: input.scopeId, effectKey: applicationEffectKey(input) }, db);
34
+
35
+ export { deleteApplicationAiTasks, listUnreleasedApplicationAiTaskSources } from "../workflows/ai/store";
36
+
37
+ const waitForTask = (signal: AbortSignal, delayMs = 250) =>
38
+ new Promise<void>((resolve, reject) => {
39
+ const abort = () => {
40
+ clearTimeout(timer);
41
+ reject(signal.reason);
42
+ };
43
+ const timer = setTimeout(() => {
44
+ signal.removeEventListener("abort", abort);
45
+ resolve();
46
+ }, delayMs);
47
+ signal.addEventListener("abort", abort, { once: true });
48
+ if (signal.aborted) abort();
49
+ });
50
+
51
+ /** One durable inference in the shared AI runtime, without creating a workflow. */
52
+ export const runApplicationAiTask = async (input: ApplicationAiTaskInput) => {
53
+ input.signal.throwIfAborted();
54
+ assertAiTaskRequestSize(input.request);
55
+ const effectKey = applicationEffectKey(input);
56
+ let task = await getWorkflowAiTaskByEffectKey(effectKey);
57
+ if (!task) {
58
+ const resolved = await resolveApplicationAiTaskModel(input.usageSubject, input.request.model);
59
+ const admissionSignal = AbortSignal.any([input.signal, AbortSignal.timeout(env.WORKFLOW_AI_QUEUE_TIMEOUT_MS)]);
60
+ for (;;) {
61
+ input.signal.throwIfAborted();
62
+ if (admissionSignal.aborted) throw new WorkflowAiAdmissionError("WORKFLOW_AI_QUEUE_TIMEOUT");
63
+ try {
64
+ task = (
65
+ await createWorkflowAiTask({
66
+ owner: { appId: input.appId, scopeId: input.scopeId, usageSubject: input.usageSubject },
67
+ stepKey: input.task,
68
+ effectKey,
69
+ request: input.request,
70
+ modelId: resolved.profile.id,
71
+ modelRef: resolved.model,
72
+ modelConfigHash: workflowAiModelHash(resolved.profile),
73
+ scope: input.source,
74
+ })
75
+ ).task;
76
+ break;
77
+ } catch (error) {
78
+ if (!(error instanceof WorkflowAiAdmissionError) || !error.retryable) throw error;
79
+ await waitForTask(admissionSignal, 500).catch(() => {
80
+ input.signal.throwIfAborted();
81
+ throw new WorkflowAiAdmissionError("WORKFLOW_AI_QUEUE_TIMEOUT");
82
+ });
83
+ }
84
+ }
85
+ }
86
+
87
+ if (task.status === "queued") await submitWorkflowAiTask(task.id);
88
+ let current = task;
89
+ for (;;) {
90
+ input.signal.throwIfAborted();
91
+ if (current.status === "succeeded") return { output: current.output, usage: current.usage, input: current.request.input };
92
+ if (current.status === "failed" || current.status === "canceled")
93
+ throw Object.assign(new Error("Application AI task did not complete. Retry the operation or inspect AI availability."), {
94
+ code: current.errorCode ?? "WORKFLOW_AI_FAILED",
95
+ });
96
+ await waitForTask(input.signal);
97
+ const latest = await getWorkflowAiTask(task.id);
98
+ if (!latest) throw new WorkflowAiAdmissionError("WORKFLOW_AI_SOURCE_UNAVAILABLE");
99
+ current = latest;
100
+ }
101
+ };
@@ -1,93 +1,155 @@
1
1
  import type { AccessSubject, RequestActor } from "../server";
2
+ import {
3
+ AI_MODEL_CLASSES,
4
+ type AiModelRequest,
5
+ type AiModelUnavailableReason,
6
+ type AiPeopleModelIcon,
7
+ type AiPeopleModels,
8
+ aiProviderMark,
9
+ } from "../shared/ai-models";
2
10
  import { aiModelAccess } from "./model-access";
11
+ import { aiModelCatalog } from "./model-catalog";
3
12
  import { personalAiModelPolicy } from "./personal-agent";
4
- import { listAiModels, toPublicAiSettingsState } from "./settings";
13
+ import { aiUserPrefs } from "./prefs";
14
+ import { aiQuotas } from "./quotas";
15
+ import { listAiModels, readAiSettingsState, resolveAiModelRef, selectAiModelProfile, toPublicAiSettingsState } from "./settings";
5
16
  import { type AiResolvedAudioModel, AiTranscriptionError, resolveAiAudioModel } from "./transcription";
6
- import type { AiChatTurnRunConfig, AiDataBoundary, AiModelPolicy, AiSettingsError } from "./types";
17
+ import type { AiChatTurnRunConfig, AiDataBoundary, AiModelPolicy, AiSettingsState } from "./types";
7
18
  import { isAiSettingsError } from "./validate";
8
19
 
9
20
  export const aiChatAccessSubject = (actor: RequestActor | undefined): AccessSubject | null => {
10
21
  if (!actor) return null;
11
22
  if (actor.kind === "user") return { type: "user", userId: actor.user.id };
12
- if (actor.delegatedUser) {
13
- return { type: "user", userId: actor.delegatedUser.id, delegatedByServiceAccountId: actor.serviceAccount.id };
14
- }
23
+ if (actor.delegatedUser) return { type: "user", userId: actor.delegatedUser.id, delegatedByServiceAccountId: actor.serviceAccount.id };
15
24
  return { type: "service_account", serviceAccountId: actor.serviceAccount.id };
16
25
  };
17
-
18
- /** Background chat deliveries also use default tools; only the explicit HTTP marker identifies interactive turns. */
19
26
  export const isAssistantChatTurn = (config: AiChatTurnRunConfig): boolean => config.assistantChat === true && !config.mandate;
20
-
21
- export const listAssistantAiModels = async (subject: AccessSubject | null, policy: AiModelPolicy = personalAiModelPolicy) =>
22
- aiModelAccess.filterModels(await listAiModels(policy), subject);
23
-
24
- /** The one audio check behind the composer microphone, dictation, and offering and running transcribe_audio. */
27
+ /** Server-side list for permission-aware execution and the developer API. */
28
+ export const listAssistantAiModels = async (
29
+ subject: AccessSubject | null,
30
+ policy: AiModelPolicy = personalAiModelPolicy,
31
+ state?: AiSettingsState,
32
+ ) => aiModelAccess.filterModels(await listAiModels(policy, state), subject);
33
+ export const peopleAiModels = async (
34
+ subject: AccessSubject | null,
35
+ policy: AiModelPolicy = personalAiModelPolicy,
36
+ ): Promise<AiPeopleModels> => {
37
+ const [state, providers, prefs, config] = await Promise.all([
38
+ readAiSettingsState(),
39
+ aiModelCatalog.providers(undefined, false),
40
+ subject?.type === "user" ? aiUserPrefs.get(subject.userId) : null,
41
+ aiQuotas.config(),
42
+ ]);
43
+ const allowed = await listAssistantAiModels(subject, policy, state);
44
+ const quotaByModel = new Map<string, Awaited<ReturnType<typeof aiQuotas.snapshot>>>();
45
+ const options: AiPeopleModels["options"] = [];
46
+ const unavailable = new Map<string, AiModelUnavailableReason>();
47
+ for (const profile of allowed) {
48
+ const provider = providers.find((provider) => provider.id === profile.providerId);
49
+ let reason: AiModelUnavailableReason | undefined =
50
+ provider?.status === "key_rejected"
51
+ ? { code: "key_rejected" }
52
+ : provider?.status === "unreachable"
53
+ ? { code: "provider_unreachable" }
54
+ : undefined;
55
+ if (!reason && subject) {
56
+ let snapshot = quotaByModel.get(profile.modelId);
57
+ if (!snapshot) {
58
+ snapshot = await aiQuotas.snapshot(subject, profile.modelId, undefined, undefined, { state, config });
59
+ quotaByModel.set(profile.modelId, snapshot);
60
+ }
61
+ const exhausted = snapshot.balances.find(
62
+ (balance) => !balance.bypassed && balance.limit !== null && (balance.unknown > 0 || balance.used >= balance.limit),
63
+ );
64
+ if (exhausted) reason = { code: "budget_exhausted", resetsAt: exhausted.resetsAt };
65
+ }
66
+ if (reason) unavailable.set(profile.id, reason);
67
+ if (!profile.showInPicker) continue;
68
+ const icon: AiPeopleModelIcon =
69
+ profile.icon.kind === "image"
70
+ ? { kind: "image", src: profile.icon.dataUrl }
71
+ : profile.icon.kind === "emoji"
72
+ ? profile.icon
73
+ : { kind: "mark", ...aiProviderMark(provider ?? { id: profile.providerId, label: "AI" }) };
74
+ options.push({
75
+ id: profile.id,
76
+ label: profile.label,
77
+ ...(profile.id !== profile.modelId ? { variantLabel: profile.variantLabel } : {}),
78
+ description: profile.description,
79
+ icon,
80
+ classes: AI_MODEL_CLASSES.filter((modelClass) =>
81
+ state.assignments.some((assignment) => assignment.slot === modelClass && assignment.variantId === profile.id),
82
+ ),
83
+ ...(reason ? { unavailable: reason } : {}),
84
+ });
85
+ }
86
+ const storedSelection = prefs?.selection;
87
+ const selection =
88
+ storedSelection && "id" in storedSelection && !state.profiles.some((profile) => profile.id === storedSelection.id)
89
+ ? null
90
+ : (storedSelection ?? null);
91
+ return {
92
+ status: "ok",
93
+ selection,
94
+ effectiveDefault: { class: "balanced" },
95
+ classes: AI_MODEL_CLASSES.flatMap((modelClass) => {
96
+ const id = state.assignments.find((assignment) => assignment.slot === modelClass)?.variantId;
97
+ return id
98
+ ? [
99
+ {
100
+ class: modelClass,
101
+ variantId: id,
102
+ ...(!allowed.some((model) => model.id === id)
103
+ ? { unavailable: { code: "access_revoked" } as const }
104
+ : unavailable.has(id)
105
+ ? { unavailable: unavailable.get(id) }
106
+ : {}),
107
+ },
108
+ ]
109
+ : [];
110
+ }),
111
+ options,
112
+ };
113
+ };
25
114
  export const resolveAssistantAudioModel = async (
26
115
  subject: AccessSubject | null,
27
116
  allowedDataBoundaries?: AiDataBoundary[],
28
117
  ): Promise<AiResolvedAudioModel> => {
29
118
  const model = await resolveAiAudioModel({ allowedDataBoundaries });
30
119
  try {
31
- await aiModelAccess.assertAllowed(model.profile.id, subject);
120
+ await aiModelAccess.assertAllowed(model.profile.modelId, subject);
32
121
  } catch (error) {
33
- if (!isAiSettingsError(error) || error.aiError.code !== "model_access_denied") throw error;
34
- throw Object.assign(
35
- new AiTranscriptionError(
36
- "transcription_access_denied",
37
- "Audio transcription is not available to you: you do not have access to the audio model. An administrator can grant access in the AI settings.",
38
- ),
39
- { aiError: error.aiError },
40
- );
122
+ if (!isAiSettingsError(error)) throw error;
123
+ throw Object.assign(new AiTranscriptionError("transcription_access_denied", "Audio transcription is not available to you."), {
124
+ aiError: error.aiError,
125
+ });
41
126
  }
42
127
  return model;
43
128
  };
44
-
45
129
  export const assistantAiSettingsState = async (subject: AccessSubject | null) => {
46
- const [status, models, audioModelConfigured] = await Promise.all([
130
+ const [status, choices, audioModelConfigured] = await Promise.all([
47
131
  toPublicAiSettingsState(personalAiModelPolicy.allowedDataBoundaries),
48
- listAssistantAiModels(subject),
49
- (async () => {
50
- try {
51
- await resolveAssistantAudioModel(subject, personalAiModelPolicy.allowedDataBoundaries);
52
- return true;
53
- } catch {
54
- return false;
55
- }
56
- })(),
132
+ peopleAiModels(subject),
133
+ resolveAssistantAudioModel(subject, personalAiModelPolicy.allowedDataBoundaries).then(
134
+ () => true,
135
+ () => false,
136
+ ),
57
137
  ]);
58
- return {
59
- ...status,
60
- audioModelConfigured,
61
- models,
62
- defaultModelId: models.some((model) => model.id === status.defaultModelId) ? status.defaultModelId : (models[0]?.id ?? ""),
63
- };
138
+ return { ...status, ...choices, audioModelConfigured };
64
139
  };
65
-
66
- /** Explicit and project selections never silently switch models. */
67
- export const selectAssistantAiModelId = async (
140
+ /** Class fallback only considers empty assignments, and access is checked on the owning model. */
141
+ export const selectAssistantAiModel = async (
68
142
  subject: AccessSubject | null,
69
- requestedModelId?: string,
143
+ model?: AiModelRequest,
70
144
  policy: AiModelPolicy = personalAiModelPolicy,
71
- ): Promise<string> => {
72
- if (policy.kind === "locked") {
73
- await aiModelAccess.assertAllowed(policy.modelId, subject);
74
- return policy.modelId;
75
- }
76
- if (policy.kind === "platform-default") {
77
- const status = await toPublicAiSettingsState();
78
- await aiModelAccess.assertAllowed(status.defaultModelId, subject);
79
- return status.defaultModelId;
80
- }
81
- if (requestedModelId) {
82
- await aiModelAccess.assertAllowed(requestedModelId, subject);
83
- return requestedModelId;
84
- }
85
- const [status, models] = await Promise.all([toPublicAiSettingsState(), listAssistantAiModels(subject, policy)]);
86
- const preferred = policy.defaultModelId;
87
- const model = models.find((candidate) => candidate.id === (preferred ?? status.defaultModelId)) ?? models[0];
88
- if (!model) {
89
- const message = "No AI model is available for your Assistant access. Contact an administrator.";
90
- throw Object.assign(new Error(message), { aiError: { code: "model_access_denied", message } satisfies AiSettingsError });
91
- }
92
- return model.id;
145
+ providedState?: AiSettingsState,
146
+ ): Promise<AiModelRequest> => {
147
+ const state = providedState ?? (await readAiSettingsState());
148
+ const stored = !model && subject?.type === "user" ? (await aiUserPrefs.get(subject.userId)).selection : null;
149
+ const selection = stored && "id" in stored && !state.profiles.some((profile) => profile.id === stored.id) ? null : stored;
150
+ const requested = model ?? selection ?? { class: "balanced" };
151
+ const profile = selectAiModelProfile(state, policy, requested);
152
+ await aiModelAccess.assertAllowed(profile.modelId, subject);
153
+ resolveAiModelRef(state, requested);
154
+ return requested;
93
155
  };