@k2b/cloud 0.40.0 → 0.42.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 (279) hide show
  1. package/package.json +9 -5
  2. package/src/_internal/bounded-json.ts +14 -2
  3. package/src/_internal/capabilities.ts +87 -8
  4. package/src/_internal/capability-reviews.ts +6 -0
  5. package/src/_internal/capability-sentences.ts +2 -1
  6. package/src/_internal/capability-timeout.ts +71 -0
  7. package/src/_internal/define-app.ts +158 -92
  8. package/src/_internal/heartbeat.ts +203 -14
  9. package/src/_internal/http-drain.ts +56 -0
  10. package/src/_internal/registry-validation.ts +3 -0
  11. package/src/_internal/registry.ts +28 -1
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/_internal/setup-lock.ts +103 -0
  14. package/src/_internal/status-preserving-ssr.ts +2 -0
  15. package/src/access/PermissionEditor.tsx +16 -17
  16. package/src/access/PrincipalPicker.tsx +45 -10
  17. package/src/access/messages.ts +8 -0
  18. package/src/access/principal-label.ts +18 -0
  19. package/src/access/ui.ts +1 -0
  20. package/src/account/EntitySearch.tsx +5 -4
  21. package/src/ai/action-review.ts +2 -1
  22. package/src/ai/app-skill-store.ts +1 -1
  23. package/src/ai/capabilities.ts +21 -3
  24. package/src/ai/capability-execution.ts +14 -12
  25. package/src/ai/chat/action-card.ts +76 -4
  26. package/src/ai/chat/action-messages.ts +14 -0
  27. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  28. package/src/ai/chat/action-question.tsx +214 -23
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/tool-action-words.ts +31 -0
  31. package/src/ai/chat/tool-action.tsx +1 -26
  32. package/src/ai/chat-tasks.ts +2 -1
  33. package/src/ai/client/controller.ts +150 -32
  34. package/src/ai/client/transport.ts +14 -2
  35. package/src/ai/code-capability-routes.ts +27 -23
  36. package/src/ai/code-runtime-tools.ts +14 -16
  37. package/src/ai/code-source-tools.ts +16 -18
  38. package/src/ai/executor.ts +64 -21
  39. package/src/ai/fetch-file-tool.ts +67 -76
  40. package/src/ai/firecrawl-tools.ts +15 -2
  41. package/src/ai/http.ts +1 -0
  42. package/src/ai/index.ts +1 -0
  43. package/src/ai/migrate.ts +28 -0
  44. package/src/ai/open-questions.ts +168 -0
  45. package/src/ai/projects.ts +1 -2
  46. package/src/ai/protocol.ts +2 -2
  47. package/src/ai/question-text.ts +8 -0
  48. package/src/ai/recent-resources.ts +12 -5
  49. package/src/ai/review-content.ts +77 -0
  50. package/src/ai/routes.ts +119 -35
  51. package/src/ai/runtime.ts +1 -0
  52. package/src/ai/skill-tool.ts +17 -6
  53. package/src/ai/skills-routes.ts +72 -27
  54. package/src/ai/skills.ts +139 -60
  55. package/src/ai/store.ts +184 -31
  56. package/src/ai/testing.ts +36 -0
  57. package/src/ai/tool-sentences.ts +152 -34
  58. package/src/ai/tool-wording.ts +12 -6
  59. package/src/ai/tools.ts +8 -3
  60. package/src/ai/types.ts +25 -4
  61. package/src/ai/usage.ts +2 -2
  62. package/src/api/admin-core-settings.ts +15 -2
  63. package/src/api/admin-geo.ts +54 -0
  64. package/src/api/admin-linux-identities.ts +6 -3
  65. package/src/api/admin-outgoing-mail.ts +29 -15
  66. package/src/api/app-approval-actions.ts +31 -7
  67. package/src/api/approvals-loader.ts +3 -3
  68. package/src/api/approvals.ts +51 -0
  69. package/src/api/apps.ts +4 -2
  70. package/src/api/auth/schemas.ts +3 -0
  71. package/src/api/auth.ts +54 -18
  72. package/src/api/capabilities.ts +258 -181
  73. package/src/api/capability-streams.ts +21 -13
  74. package/src/api/cli-plugins.ts +4 -1
  75. package/src/api/geo-glyphs.ts +11 -0
  76. package/src/api/geo.ts +128 -0
  77. package/src/api/help.ts +12 -4
  78. package/src/api/index.ts +5 -0
  79. package/src/api/mcp.ts +73 -41
  80. package/src/api/me-panel-consent.ts +8 -5
  81. package/src/api/me.ts +79 -3
  82. package/src/api/search.ts +21 -14
  83. package/src/api/sync-ops.ts +19 -13
  84. package/src/api/widgets.ts +42 -18
  85. package/src/approvals/Approvals.tsx +30 -66
  86. package/src/approvals/approval-card.ts +68 -0
  87. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  88. package/src/browser/CloudResourceSearch.tsx +3 -13
  89. package/src/browser/PanelLeaveNotice.tsx +18 -0
  90. package/src/browser/navigation-search.ts +78 -25
  91. package/src/browser/panel-leave.ts +69 -0
  92. package/src/browser/panel-messages.ts +12 -0
  93. package/src/browser/panel-slot.ts +173 -24
  94. package/src/browser/panel.ts +3 -0
  95. package/src/browser/resource-search-messages.ts +2 -0
  96. package/src/capabilities/client.ts +28 -1
  97. package/src/capabilities/executions.ts +22 -3
  98. package/src/capabilities/geo-client.ts +57 -0
  99. package/src/capabilities/response.ts +2 -1
  100. package/src/capabilities/review-storage.ts +24 -0
  101. package/src/capabilities/reviews.ts +17 -3
  102. package/src/capabilities/server.ts +36 -16
  103. package/src/cli/access.ts +15 -23
  104. package/src/cli/admin/linux.ts +13 -2
  105. package/src/cli/admin/outgoing-mail.ts +26 -4
  106. package/src/cli/capabilities.ts +19 -0
  107. package/src/config/env.ts +0 -6
  108. package/src/contracts/app-access.ts +48 -0
  109. package/src/contracts/app-approval.ts +3 -3
  110. package/src/contracts/app.ts +4 -1
  111. package/src/contracts/approvals.ts +2 -2
  112. package/src/contracts/avatar.ts +186 -0
  113. package/src/contracts/capabilities.ts +149 -55
  114. package/src/contracts/geo-admin.ts +127 -0
  115. package/src/contracts/geo-map.ts +19 -0
  116. package/src/contracts/geo-route.ts +168 -0
  117. package/src/contracts/geo.ts +248 -0
  118. package/src/contracts/index.ts +4 -0
  119. package/src/contracts/outgoing-mail.ts +17 -4
  120. package/src/contracts/registry.ts +2 -0
  121. package/src/contracts/shared.ts +3 -0
  122. package/src/events/app-access.ts +68 -0
  123. package/src/events/live-delivery.ts +128 -0
  124. package/src/events/live.ts +14 -130
  125. package/src/geo/CloudMap.tsx +95 -0
  126. package/src/geo/map-messages.ts +17 -0
  127. package/src/geo/ui.ts +1 -0
  128. package/src/index.ts +2 -0
  129. package/src/server/client-ip.ts +36 -0
  130. package/src/server/index.ts +3 -4
  131. package/src/server/middleware/app-access.ts +312 -0
  132. package/src/server/middleware/auth.ts +5 -3
  133. package/src/server/middleware/invocation.ts +11 -0
  134. package/src/server/middleware/rate-limit.ts +4 -4
  135. package/src/server/services/access.ts +13 -9
  136. package/src/server/services/index.ts +0 -2
  137. package/src/server/services/services.ts +0 -2
  138. package/src/services/accounts/app.ts +23 -3
  139. package/src/services/accounts/posix.ts +21 -7
  140. package/src/services/accounts/users.ts +12 -1
  141. package/src/services/allowed-network-fetch.ts +16 -10
  142. package/src/services/app-access-metrics.ts +24 -0
  143. package/src/services/app-access.ts +123 -0
  144. package/src/services/app-discovery.ts +27 -0
  145. package/src/services/approvals/content.ts +40 -0
  146. package/src/services/approvals/device.ts +49 -33
  147. package/src/services/approvals/index.ts +15 -6
  148. package/src/services/approvals/list.ts +6 -11
  149. package/src/services/approvals/question.ts +72 -0
  150. package/src/services/audit/index.ts +1 -0
  151. package/src/services/auth-flows/ipa.ts +39 -2
  152. package/src/services/auth-flows/password-reset.ts +32 -6
  153. package/src/services/downstream-budget.ts +87 -0
  154. package/src/services/geo/admin.ts +262 -0
  155. package/src/services/geo/bucket.ts +67 -0
  156. package/src/services/geo/cache.ts +57 -0
  157. package/src/services/geo/costs.ts +5 -0
  158. package/src/services/geo/errors.ts +41 -0
  159. package/src/services/geo/fixtures/README.md +17 -0
  160. package/src/services/geo/fixtures/along.json +355 -0
  161. package/src/services/geo/fixtures/autocomplete.json +218 -0
  162. package/src/services/geo/fixtures/geocode.json +129 -0
  163. package/src/services/geo/fixtures/places.json +355 -0
  164. package/src/services/geo/fixtures/reverse.json +61 -0
  165. package/src/services/geo/fixtures/routing.json +57 -0
  166. package/src/services/geo/geoapify-route.ts +93 -0
  167. package/src/services/geo/geoapify.ts +281 -0
  168. package/src/services/geo/ledger.ts +122 -0
  169. package/src/services/geo/map.ts +160 -0
  170. package/src/services/geo/migrate.ts +25 -0
  171. package/src/services/geo/polyline.ts +184 -0
  172. package/src/services/geo/route-chain.ts +145 -0
  173. package/src/services/geo/route-costs.ts +9 -0
  174. package/src/services/geo/route-geometry.ts +39 -0
  175. package/src/services/geo/route-plan.ts +172 -0
  176. package/src/services/geo/route-service.ts +433 -0
  177. package/src/services/geo/service.ts +300 -0
  178. package/src/services/geo/settings.ts +49 -0
  179. package/src/services/help/index.ts +16 -7
  180. package/src/services/help/types.ts +6 -1
  181. package/src/services/identity/invocation-actor.ts +20 -2
  182. package/src/services/identity/issue-invocation.ts +67 -0
  183. package/src/services/identity/key-ring.ts +5 -3
  184. package/src/services/identity/runtime-config.ts +23 -0
  185. package/src/services/index.ts +11 -0
  186. package/src/services/mandates/index.ts +40 -3
  187. package/src/services/network-security.ts +6 -3
  188. package/src/services/network-transport.ts +99 -0
  189. package/src/services/notifications/catalog.ts +3 -2
  190. package/src/services/notifications/runtime.ts +1 -0
  191. package/src/services/notifications/web-push-transport.ts +45 -38
  192. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  193. package/src/services/outgoing-mail/index.ts +55 -1
  194. package/src/services/outgoing-mail/messages.ts +82 -3
  195. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  196. package/src/services/outgoing-mail/retention.ts +13 -1
  197. package/src/services/outgoing-mail/store.ts +36 -6
  198. package/src/services/outgoing-mail/test-send.ts +34 -4
  199. package/src/services/outgoing-mail/transport.ts +1 -1
  200. package/src/services/pdf/gotenberg.ts +11 -38
  201. package/src/services/pdf/markdown.ts +160 -2
  202. package/src/services/pdf/template-preview.ts +1 -1
  203. package/src/services/public-http.ts +28 -40
  204. package/src/services/session/index.ts +203 -29
  205. package/src/services/settings/app.ts +4 -1
  206. package/src/services/settings/core-settings.ts +88 -0
  207. package/src/services/settings/defaults.ts +8 -2
  208. package/src/services/settings/errors.ts +11 -0
  209. package/src/services/settings/geo-budget.ts +40 -0
  210. package/src/services/settings/index.ts +4 -3
  211. package/src/services/settings/store.ts +25 -17
  212. package/src/services/weather/geo.ts +29 -111
  213. package/src/services/weather/index.ts +4 -3
  214. package/src/services/weather/locations.ts +8 -5
  215. package/src/services/weather/migrate.ts +1 -0
  216. package/src/services/weather/settings.ts +0 -16
  217. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  218. package/src/shared/avatar-art.ts +400 -0
  219. package/src/shared/index.ts +2 -0
  220. package/src/shared/network-address.ts +22 -3
  221. package/src/shared/template-rendering.ts +36 -4
  222. package/src/shared-text/documents.ts +1 -1
  223. package/src/shared-text/index.ts +2 -0
  224. package/src/shared-text/log.ts +51 -3
  225. package/src/shared-text/snapshots.ts +340 -0
  226. package/src/ssr/AdminLayout.tsx +4 -2
  227. package/src/ssr/CloudShell.island.tsx +6 -0
  228. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  229. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  230. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  231. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  232. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  233. package/src/ssr/HelpPanelView.island.tsx +27 -0
  234. package/src/ssr/Layout.tsx +65 -15
  235. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  236. package/src/ssr/LayoutHelp.tsx +185 -542
  237. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
  238. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  239. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  240. package/src/ssr/PanelRuntime.island.tsx +11 -7
  241. package/src/ssr/PwaLayout.tsx +2 -2
  242. package/src/ssr/RailApps.tsx +6 -0
  243. package/src/ssr/ShellRuntime.island.tsx +23 -7
  244. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  245. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  246. package/src/ssr/admin-navigation.ts +1 -0
  247. package/src/ssr/app-badges.ts +6 -1
  248. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  249. package/src/ssr/help.ts +7 -5
  250. package/src/ssr/index.ts +2 -1
  251. package/src/ssr/islands/index.ts +3 -4
  252. package/src/ssr/layout-context.ts +2 -0
  253. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  254. package/src/ssr/layout-help-panel-document.ts +67 -0
  255. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  256. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  257. package/src/ssr/layout-help-panel-modules.ts +7 -0
  258. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  259. package/src/ssr/layout-help-panel-search.tsx +69 -0
  260. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  261. package/src/ssr/layout-help-panel-state.ts +141 -0
  262. package/src/ssr/layout-help-panel-types.ts +19 -0
  263. package/src/ssr/layout-help-views.tsx +484 -0
  264. package/src/ssr/mobile-menu-history.ts +4 -0
  265. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  266. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  267. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  268. package/src/ssr/page-swap.ts +28 -0
  269. package/src/ssr/page-trail-runtime.ts +32 -13
  270. package/src/ssr/panel-consent.ts +2 -2
  271. package/src/ssr/panel-idle.ts +9 -0
  272. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  273. package/src/ssr/platform-messages.ts +8 -0
  274. package/src/ssr/runtime.ts +17 -0
  275. package/src/ssr/shell-data.tsx +10 -2
  276. package/src/styles/approvals.css +6 -0
  277. package/src/styles/effects.css +15 -0
  278. package/src/styles/global.css +70 -6
  279. package/src/server/services/geo.ts +0 -215
@@ -1,7 +1,7 @@
1
1
  import { Combobox, type ComboboxOption, useLocale } from "@k2b/ui";
2
2
  import type { Principal, ServiceAccountKind } from "../contracts/shared";
3
- import { groupDisplayName } from "../shared/account-display";
4
3
  import { accessMessages } from "./messages";
4
+ import { principalLabel } from "./principal-label";
5
5
  import { serviceAccountKindDisplay } from "./service-account-kind";
6
6
 
7
7
  export const principalKey = (p: Principal) =>
@@ -13,7 +13,7 @@ export const principalKey = (p: Principal) =>
13
13
  ? `s:${p.serviceAccountId}`
14
14
  : p.type;
15
15
  type Entity =
16
- | { kind: "user"; user: { id: string; displayName: string; uid: string; mail: string | null } }
16
+ | { kind: "user"; user: { id: string; displayName: string; uid: string; mail: string | null; avatarHash?: string | null } }
17
17
  | { kind: "group"; group: { id: string; name: string; description: string | null } }
18
18
  | {
19
19
  kind: "service_account";
@@ -35,17 +35,36 @@ export default function PrincipalPicker(props: {
35
35
  disabled?: boolean;
36
36
  readOnly?: boolean;
37
37
  /** `serviceAccountKind` is set for service accounts so a new row can show the same kind as the result. */
38
- onSelect: (principal: Principal, display: { displayName: string; serviceAccountKind?: ServiceAccountKind }) => void;
38
+ onSelect: (
39
+ principal: Principal,
40
+ display: { displayName: string; avatarHash?: string | null; serviceAccountKind?: ServiceAccountKind },
41
+ ) => void;
39
42
  }) {
40
43
  const locale = useLocale(),
41
44
  t = () => accessMessages.resolve([locale()]).t;
42
- type Entry = { principal: Principal; label: string; icon: string; description?: string; serviceAccountKind?: ServiceAccountKind };
45
+ type Entry = {
46
+ principal: Principal;
47
+ label: string;
48
+ icon: string;
49
+ description?: string;
50
+ avatarHash?: string | null;
51
+ serviceAccountKind?: ServiceAccountKind;
52
+ };
43
53
  let selectable = new Map<string, Entry>();
44
54
  const load = async (search: string, signal: AbortSignal): Promise<ComboboxOption[]> => {
45
55
  const entries: Entry[] = [];
46
56
  if (props.allowAuthenticated !== false)
47
- entries.push({ principal: { type: "authenticated" }, label: t().allUsers, icon: "ti ti-lock-open-2" });
48
- if (props.allowPublic) entries.push({ principal: { type: "public" }, label: t().public, icon: "ti ti-world" });
57
+ entries.push({
58
+ principal: { type: "authenticated" },
59
+ label: principalLabel({ principal: { type: "authenticated" } }, locale()),
60
+ icon: "ti ti-lock-open-2",
61
+ });
62
+ if (props.allowPublic)
63
+ entries.push({
64
+ principal: { type: "public" },
65
+ label: principalLabel({ principal: { type: "public" } }, locale()),
66
+ icon: "ti ti-world",
67
+ });
49
68
  if (search.length >= 2) {
50
69
  const url = new URL("/api/accounts/entities", window.location.origin);
51
70
  url.searchParams.set("search", search);
@@ -64,14 +83,18 @@ export default function PrincipalPicker(props: {
64
83
  if (e.kind === "user")
65
84
  entries.push({
66
85
  principal: { type: "user", userId: e.user.id },
67
- label: e.user.displayName || e.user.uid,
86
+ label: principalLabel(
87
+ { principal: { type: "user", userId: e.user.id }, displayName: e.user.displayName || e.user.uid },
88
+ locale(),
89
+ ),
90
+ avatarHash: e.user.avatarHash,
68
91
  icon: "ti ti-user",
69
92
  description: e.user.mail ?? e.user.uid,
70
93
  });
71
94
  else if (e.kind === "group")
72
95
  entries.push({
73
96
  principal: { type: "group", groupId: e.group.id },
74
- label: groupDisplayName(e.group.name, locale()),
97
+ label: principalLabel({ principal: { type: "group", groupId: e.group.id }, displayName: e.group.name }, locale()),
75
98
  icon: "ti ti-users-group",
76
99
  description: e.group.description ?? undefined,
77
100
  });
@@ -80,7 +103,14 @@ export default function PrincipalPicker(props: {
80
103
  const kind = serviceAccountKindDisplay(account.kind, t());
81
104
  entries.push({
82
105
  principal: { type: "service_account", serviceAccountId: account.id },
83
- label: account.name,
106
+ label: principalLabel(
107
+ {
108
+ principal: { type: "service_account", serviceAccountId: account.id },
109
+ displayName: account.name,
110
+ serviceAccountKind: account.kind,
111
+ },
112
+ locale(),
113
+ ),
84
114
  // Resource-bound accounts often share a name; their binding tells them apart.
85
115
  description:
86
116
  account.kind === "resource_bound"
@@ -114,7 +144,12 @@ export default function PrincipalPicker(props: {
114
144
  fetchData={load}
115
145
  onSelect={(option) => {
116
146
  const entry = selectable.get(option.id);
117
- if (entry) props.onSelect(entry.principal, { displayName: entry.label, serviceAccountKind: entry.serviceAccountKind });
147
+ if (entry)
148
+ props.onSelect(entry.principal, {
149
+ displayName: entry.label,
150
+ avatarHash: entry.avatarHash,
151
+ serviceAccountKind: entry.serviceAccountKind,
152
+ });
118
153
  }}
119
154
  />
120
155
  );
@@ -8,6 +8,10 @@ export const accessMessages = i18n.define({
8
8
  edit: "Edit",
9
9
  manage: "Manage",
10
10
  noAccess: "No access",
11
+ unknownPerson: "Unknown person",
12
+ deletedGroup: "Deleted group",
13
+ unknownServiceAccount: "Unknown service account",
14
+ unknownAgent: "Unknown agent",
11
15
  allUsers: "All users (including guests)",
12
16
  public: "Public",
13
17
  signedInDescription: "Anyone signed in to Cloud",
@@ -87,6 +91,10 @@ export const accessMessages = i18n.define({
87
91
  edit: "Bearbeiten",
88
92
  manage: "Verwalten",
89
93
  noAccess: "Kein Zugriff",
94
+ unknownPerson: "Unbekannte Person",
95
+ deletedGroup: "Gelöschte Gruppe",
96
+ unknownServiceAccount: "Unbekanntes Dienstkonto",
97
+ unknownAgent: "Unbekannter Agent",
90
98
  allUsers: "Alle Benutzer einschließlich Gäste",
91
99
  public: "Öffentlich",
92
100
  signedInDescription: "Alle bei Cloud angemeldeten Personen",
@@ -0,0 +1,18 @@
1
+ import type { Principal, ServiceAccountKind } from "../contracts/shared";
2
+ import { groupDisplayName } from "../shared/account-display";
3
+ import { accessMessages } from "./messages";
4
+
5
+ /** A human label, never an identity. Resolve names only after authorizing the resource operation. */
6
+ export const principalLabel = (
7
+ entry: { principal: Principal; displayName?: string | null; serviceAccountKind?: ServiceAccountKind },
8
+ locale: string | null | undefined,
9
+ ): string => {
10
+ const { t } = accessMessages.resolve([locale ?? "en"]);
11
+ const { principal, displayName } = entry;
12
+ if (principal.type === "authenticated") return t.allUsers;
13
+ if (principal.type === "public") return t.public;
14
+ if (displayName) return principal.type === "group" ? groupDisplayName(displayName, locale) : displayName;
15
+ if (principal.type === "user") return t.unknownPerson;
16
+ if (principal.type === "group") return t.deletedGroup;
17
+ return entry.serviceAccountKind === "agent" ? t.unknownAgent : t.unknownServiceAccount;
18
+ };
package/src/access/ui.ts CHANGED
@@ -10,6 +10,7 @@ export { isManagerEntry, type ManagerCandidate } from "./managers";
10
10
  export type { AllowedLevel, GrantableLevel } from "./PermissionEditor";
11
11
  export { default as PermissionEditor } from "./PermissionEditor";
12
12
  export { default as PrincipalPicker, principalKey } from "./PrincipalPicker";
13
+ export { principalLabel } from "./principal-label";
13
14
  export type {
14
15
  ResourceApiKey,
15
16
  ResourceApiKeyPermissionOption,
@@ -3,6 +3,7 @@ import { Button, ScrollArea, TextInput, useLocale } from "@k2b/ui";
3
3
  import type { JSX } from "solid-js";
4
4
  import { createSignal, For, Show } from "solid-js";
5
5
  import { accessMessages } from "../access/messages";
6
+ import { principalLabel } from "../access/principal-label";
6
7
  import { serviceAccountKindDisplay } from "../access/service-account-kind";
7
8
  import { groupDisplayName } from "../shared/account-display";
8
9
  import { CloudAvatar } from "./Avatar";
@@ -233,8 +234,8 @@ const EntitySearch = (props: EntitySearchProps) => {
233
234
  <Show when={props.includeAuthenticated}>
234
235
  <ResultRow
235
236
  icon="ti-lock-open-2"
236
- title="All users (incl. guests)"
237
- subtitle="Anyone signed in to the cloud"
237
+ title={principalLabel({ principal: { type: "authenticated" } }, locale())}
238
+ subtitle={accessMessages.resolve([locale()]).t.signedInDescription}
238
239
  disabled={props.disabled}
239
240
  onSelect={() => props.onSelect({ type: "authenticated" })}
240
241
  />
@@ -242,8 +243,8 @@ const EntitySearch = (props: EntitySearchProps) => {
242
243
  <Show when={props.includePublic}>
243
244
  <ResultRow
244
245
  icon="ti-world"
245
- title="Public"
246
- subtitle="Anyone with the link, even unauthenticated"
246
+ title={principalLabel({ principal: { type: "public" } }, locale())}
247
+ subtitle={accessMessages.resolve([locale()]).t.publicDescription}
247
248
  disabled={props.disabled}
248
249
  onSelect={() => props.onSelect({ type: "public" })}
249
250
  />
@@ -4,6 +4,7 @@ import type {
4
4
  CapabilityActionReceipt,
5
5
  CapabilityActionReview,
6
6
  CapabilityActionReviewData,
7
+ CapabilityReviewContentRef,
7
8
  CapabilitySemanticLink,
8
9
  } from "../contracts/capabilities";
9
10
 
@@ -32,7 +33,7 @@ export type AiLegacyActionReview = {
32
33
  };
33
34
 
34
35
  /** A new review, with `ask` filled from the declared policy when the call itself names no reason. */
35
- export type AiStoredActionReview = CapabilityActionCallReview | AiLegacyActionReview;
36
+ export type AiStoredActionReview = (CapabilityActionCallReview & { contentRef?: CapabilityReviewContentRef }) | AiLegacyActionReview;
36
37
 
37
38
  export const isLegacyActionReview = (review: AiStoredActionReview): review is AiLegacyActionReview => "legacy" in review;
38
39
 
@@ -79,7 +79,7 @@ const reconcileApp = async (app: AppRegistryEntry, tx: TransactionSQL): Promise<
79
79
  { skills: unknown }[]
80
80
  >`SELECT skills FROM ai.app_skill_catalogs WHERE app_id=${app.id} AND manifest_hash=${manifest}`;
81
81
  if (!catalog) return false; // Publication may still be in flight: keep the last known state.
82
- const roles = appAudienceRoles(app.nav);
82
+ const roles = app.access === undefined ? appAudienceRoles(app.nav) : undefined;
83
83
  const roleArray = roles === undefined ? null : toPgTextArray([...roles]);
84
84
  const [state] = await tx<{ current: boolean }[]>`SELECT
85
85
  EXISTS(SELECT 1 FROM ai.app_skills WHERE app_id=${app.id}) AND
@@ -12,6 +12,7 @@ import {
12
12
  capabilityApprovalText,
13
13
  } from "../_internal/capability-sentences";
14
14
  import { HELP_READ_MAX_CHARS, HELP_SEARCH_MAX_LIMIT, readHelpArticle } from "../_internal/help-catalog";
15
+ import type { listApps } from "../_internal/registry";
15
16
  import {
16
17
  type CapabilityActionManifest,
17
18
  type CapabilityActionReviewData,
@@ -23,6 +24,8 @@ import {
23
24
  } from "../contracts/capabilities";
24
25
  import type { CapabilityRegistryEntry } from "../contracts/registry";
25
26
  import type { RequestActor } from "../server";
27
+ import type { RequestAuthority } from "../server/middleware/auth";
28
+ import { discoverableAppEntries } from "../services/app-discovery";
26
29
  import type { HelpReader, HelpReaderFactory } from "../services/help";
27
30
  import { type MandatePolicyV1, mandatePolicyCanPermitCapability } from "../services/mandates/policy";
28
31
  import { resolveAppIdentityPresentation } from "../shared/app-presentation";
@@ -869,6 +872,7 @@ export const createAiToolResolver =
869
872
  (input: {
870
873
  conversationId: string;
871
874
  actor: RequestActor;
875
+ authority?: RequestAuthority | null;
872
876
  staticTools: AiRuntimeTool[];
873
877
  allowedTools?: readonly string[] | null;
874
878
  /** Undefined for interactive runs; null fails closed when a background mandate cannot be loaded. */
@@ -880,6 +884,7 @@ export const createAiToolResolver =
880
884
  runtimeContext?: Omit<AiToolPreparationContext, "actor" | "conversationId">;
881
885
  store: Pick<AiConversationService, "getLoadedTools" | "loadTools">;
882
886
  listRegistry?: () => Promise<CapabilityRegistryEntry[]>;
887
+ listApps?: typeof listApps;
883
888
  onCapabilityRegistryError?: (error: unknown) => void;
884
889
  help?: HelpReaderFactory;
885
890
  locale?: string;
@@ -904,7 +909,17 @@ export const createAiToolResolver =
904
909
  input.listRegistry ? resolveCapabilityRegistry(input.listRegistry, input.onCapabilityRegistryError) : [],
905
910
  input.store.getLoadedTools({ conversationId: input.conversationId }),
906
911
  ]);
907
- const registry = liveRegistry ?? [];
912
+ const registry =
913
+ (await resolveCapabilityRegistry(
914
+ () => discoverableAppEntries(input.authority ?? null, liveRegistry ?? [], input.listApps),
915
+ input.onCapabilityRegistryError,
916
+ )) ?? [];
917
+ const visibleAppIds = new Set(registry.map((app) => app.appId));
918
+ const hiddenNames = new Set(
919
+ buildAiCapabilityCatalog(liveRegistry ?? [])
920
+ .filter((entry) => !visibleAppIds.has(entry.appId))
921
+ .flatMap((entry) => [entry.name, entry.providerName]),
922
+ );
908
923
  const configuredLimit = Math.floor(input.maxLoadedTools ?? 0);
909
924
  const loadedNames = configuredLimit > 0 ? persistedLoadedNames.slice(-configuredLimit) : persistedLoadedNames;
910
925
  if (loadedNames.length !== persistedLoadedNames.length) {
@@ -931,7 +946,9 @@ export const createAiToolResolver =
931
946
  );
932
947
  const capabilityCatalog = fullCapabilityCatalog.filter((entry) => (!allowed || allowed.has(entry.name)) && mandateAllows(entry));
933
948
  // The turn's actor decides which apps' Help exists for the model, like every other Help surface.
934
- const helpTools = input.help ? createAiHelpTools(input.help(input.locale ?? "en", input.actor)) : [];
949
+ const helpTools = input.help
950
+ ? createAiHelpTools(input.help(input.locale ?? "en", input.actor, { authority: input.authority ?? null }))
951
+ : [];
935
952
  const resourceTool =
936
953
  input.execute && (capabilityCatalog.length > 0 || input.staticTools.some((tool) => tool.def.name === "code_read"))
937
954
  ? createAiResourceReaderTool({ apps: registry, catalog: capabilityCatalog, execute: input.execute })
@@ -940,7 +957,7 @@ export const createAiToolResolver =
940
957
  const builtIns = allBuiltIns.filter((tool) => !allowed || allowed.has(tool.def.name));
941
958
  const catalog = buildAiToolCatalog(builtIns, capabilityCatalog);
942
959
  const catalogNames = new Set(catalog.map((entry) => entry.name));
943
- const unavailableLoadedNames = loadedNames.filter((name) => !catalogNames.has(name));
960
+ const unavailableLoadedNames = loadedNames.filter((name) => !catalogNames.has(name) && !hiddenNames.has(name));
944
961
  const outOfScope = new Set([
945
962
  ...allBuiltIns.map((tool) => tool.def.name),
946
963
  ...fullCapabilityCatalog.flatMap((entry) => [entry.name, entry.providerName]),
@@ -959,6 +976,7 @@ export const createAiToolResolver =
959
976
  const unavailableReason = (requested: string): AiToolUnavailableReason => {
960
977
  if (input.skillNames?.includes(requested)) return "skill";
961
978
  const name = loadedByProviderName.get(requested) ?? requested;
979
+ if (hiddenNames.has(requested) || hiddenNames.has(name)) return "unknown";
962
980
  if (mandateExcluded.has(name)) return "not_allowed";
963
981
  if (allowed && outOfScope.has(name) && !allowed.has(name)) return "not_allowed";
964
982
  if (unoffered.has(name)) return "not_offered_in_turn";
@@ -10,8 +10,7 @@ import {
10
10
  type CapabilityActionReviewData,
11
11
  CapabilityActionReviewDataSchema,
12
12
  } from "../contracts/capabilities";
13
- import type { RequestActor } from "../server";
14
- import type { AccessSubject } from "../server/services/access";
13
+ import type { RequestActor, RequestAuthority } from "../server";
15
14
  import { isAccountCategoryAllowed } from "../services/account-category-policy";
16
15
  import { isAccountExpired } from "../services/account-model";
17
16
  import { accounts } from "../services/accounts";
@@ -26,13 +25,14 @@ import type { AiConversationService } from "./types";
26
25
  export { AiCapabilityExecutionError, aiCapabilityCheckResult } from "./capability-result";
27
26
 
28
27
  type CapabilityActor = Extract<RequestActor, { kind: "user" }>;
28
+ type AiCapabilityAuthority = RequestAuthority & { actor: CapabilityActor };
29
29
 
30
30
  export const resolveAiCapabilityActor = async (input: {
31
31
  conversationId: string;
32
32
  persistedActor?: RequestActor;
33
33
  store: Pick<AiConversationService, "getConversation">;
34
34
  getUser?: typeof accounts.users.get;
35
- }): Promise<{ actor: CapabilityActor; accessSubject: AccessSubject }> => {
35
+ }): Promise<AiCapabilityAuthority> => {
36
36
  if (input.persistedActor?.kind !== "user") throw new Error("Cloud capabilities require a current user-backed actor.");
37
37
  const conversation = await input.store.getConversation({ conversationId: input.conversationId });
38
38
  if (!conversation?.createdByUserId || conversation.createdByUserId !== input.persistedActor.user.id) {
@@ -44,9 +44,16 @@ export const resolveAiCapabilityActor = async (input: {
44
44
  if (!(await isAccountCategoryAllowed(user))) throw new Error("Cloud capability actor category is disabled.");
45
45
  // A turn started in the mobile app keeps its limits: no administrator role, and invocations say so.
46
46
  if (input.persistedActor.sessionKind === "app") {
47
- return { actor: { kind: "user", user: withoutAdminRole(user), sessionKind: "app" }, accessSubject: { type: "user", userId: user.id } };
47
+ return {
48
+ actor: { kind: "user", user: withoutAdminRole(user), sessionKind: "app" },
49
+ accessSubject: { type: "user", userId: user.id },
50
+ credentialKind: "session",
51
+ scopes: [],
52
+ };
48
53
  }
49
- return { actor: { kind: "user", user }, accessSubject: { type: "user", userId: user.id } };
54
+ // Assistant persists a user actor, not a reusable transport credential. Its supported
55
+ // delegation is the current conversation owner's session authority.
56
+ return { actor: { kind: "user", user }, accessSubject: { type: "user", userId: user.id }, credentialKind: "session", scopes: [] };
50
57
  };
51
58
 
52
59
  const idempotencyKey = (conversationId: string, callId: string): string =>
@@ -83,7 +90,7 @@ const parseCapabilityResponse = async (response: Response, dispatched: boolean):
83
90
  type AiCapabilityCall = {
84
91
  conversationId: string;
85
92
  turnId?: string;
86
- authority: { actor: CapabilityActor; accessSubject: AccessSubject };
93
+ authority: AiCapabilityAuthority;
87
94
  locale?: string;
88
95
  entry: AiCapabilityCatalogEntry;
89
96
  args: unknown;
@@ -140,12 +147,7 @@ const dispatchAiCapability = async (
140
147
  appId: input.entry.appId,
141
148
  capabilityId: input.entry.operation.localId,
142
149
  input: input.args,
143
- authority: {
144
- actor: input.authority.actor,
145
- accessSubject: input.authority.accessSubject,
146
- credentialKind: "session",
147
- scopes: [],
148
- },
150
+ authority: input.authority,
149
151
  mandate: input.mandate
150
152
  ? {
151
153
  mandateId: input.mandate.id,
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  type CapabilitySentencePart,
3
3
  capabilityAskReasonText,
4
+ capabilityPersonText,
4
5
  capabilityQuestionText,
5
6
  capabilityReceiptParts,
6
7
  capabilityReviewSentence,
7
8
  capabilityReviewSentenceParts,
9
+ capabilitySentenceParts,
8
10
  capabilitySentenceText,
9
11
  } from "../../_internal/capability-sentences";
10
12
  import type {
@@ -14,6 +16,7 @@ import type {
14
16
  CapabilityAskReason,
15
17
  CapabilityConsequence,
16
18
  CapabilityPreview,
19
+ CapabilityReviewContentRef,
17
20
  CapabilitySemanticLink,
18
21
  CapabilitySlot,
19
22
  } from "../../contracts/capabilities";
@@ -55,11 +58,13 @@ export type ActionPreviewModel =
55
58
  locale: string;
56
59
  timeZone: string;
57
60
  preview?: CapabilityPreview;
58
- consequences: { text: ActionText; mark?: "added" | "removed" }[];
61
+ /** Original slots for plain rule sentences, retaining people’s addresses. */
62
+ slots?: CapabilityActionCallReview["slots"];
63
+ consequences: { text: ActionText; plain: string; mark?: "added" | "removed" }[];
59
64
  /** The rule sentence of a `rule` preview, with its chips. */
60
65
  when?: ActionText;
61
66
  /** Each `may` line of a `rule` preview. */
62
- may?: { text: ActionText; mark?: "added" | "removed" }[];
67
+ may?: { text: ActionText; plain: string; mark?: "added" | "removed" }[];
63
68
  }
64
69
  | { kind: "legacy"; rows: AiLegacyReviewRow[]; markdown?: string; input?: unknown };
65
70
 
@@ -74,6 +79,7 @@ export type ActionCard = {
74
79
  preview: ActionPreviewModel | null;
75
80
  /** Opens exactly this version of the thing. */
76
81
  readMore?: string;
82
+ contentRef?: CapabilityReviewContentRef;
77
83
  /** The review's other links, such as editing the draft, each with its words. */
78
84
  links: { href: string; label: string }[];
79
85
  /** Where the thing itself opens, for an outcome Cloud could not see. */
@@ -96,6 +102,16 @@ export type ActionCard = {
96
102
  hasCheck: boolean;
97
103
  /** The receipt text of an old review's success summary. */
98
104
  summary?: string;
105
+ /**
106
+ * For plain text: every value a new review's slots stand for (each person with address, title, text, time, count),
107
+ * also in consequences and rule lines, and whether the app left people out. Plain text that does not show each value
108
+ * whole is incomplete.
109
+ */
110
+ slotValues?: {
111
+ values: string[];
112
+ times: { text: string; start: string; end?: string; format: "date" | "date-time" }[];
113
+ unlisted: boolean;
114
+ };
99
115
  };
100
116
 
101
117
  const REASON_LOOK: Record<CapabilityAskReason, Omit<ActionReason, "code" | "text">> = {
@@ -174,6 +190,19 @@ export const actionPlainText = (text: ActionText): string =>
174
190
  })
175
191
  .join("");
176
192
 
193
+ /** The short template, else the action sentence, read as the card's chips without person addresses or title quotes. */
194
+ export const reviewShortText = (review: CapabilityActionCallReview, context: ActionCardContext): string =>
195
+ actionPlainText(
196
+ partsText(
197
+ capabilitySentenceParts(review.sentence.short ?? review.sentence.action, review.slots, {
198
+ locale: review.locale,
199
+ timeZone: context.timeZone,
200
+ }),
201
+ review.locale,
202
+ context.app,
203
+ ),
204
+ );
205
+
177
206
  const firstLink = (links: readonly CapabilitySemanticLink[] | undefined, rels: CapabilitySemanticLink["rel"][]) =>
178
207
  rels.map((rel) => links?.find((link) => link.rel === rel)?.href).find(Boolean);
179
208
 
@@ -192,7 +221,38 @@ const firstObjectHref = (review: CapabilityActionCallReview): string | undefined
192
221
  .map((slot) => ((slot.kind === "object" || slot.kind === "place") && slot.ref ? cloudResourceHref(slot.ref) : undefined))
193
222
  .find(Boolean);
194
223
 
195
- export const reviewActionCard = (review: CapabilityActionCallReview, context: ActionCardContext): ActionCard => {
224
+ const slotValues = (slots: CapabilitySlot[], context: { locale: string; timeZone: string }) => ({
225
+ values: slots.flatMap((slot) =>
226
+ slot.kind === "person"
227
+ ? slot.people.map(capabilityPersonText)
228
+ : slot.kind === "object" || slot.kind === "place"
229
+ ? [slot.title]
230
+ : slot.kind === "text"
231
+ ? [slot.value]
232
+ : slot.kind === "count"
233
+ ? [capabilitySentenceText("{value}", { value: slot }, context)]
234
+ : [],
235
+ ),
236
+ // A time is shown whole as its words, or as typed rows that hold its start and end with at least its precision.
237
+ times: slots.flatMap((slot) =>
238
+ slot.kind === "time"
239
+ ? [
240
+ {
241
+ text: capabilitySentenceText("{value}", { value: slot }, context),
242
+ start: slot.start,
243
+ ...(slot.end ? { end: slot.end } : {}),
244
+ format: slot.format,
245
+ },
246
+ ]
247
+ : [],
248
+ ),
249
+ unlisted: slots.some((slot) => slot.kind === "person" && (slot.total ?? slot.people.length) > slot.people.length),
250
+ });
251
+
252
+ export const reviewActionCard = (
253
+ review: CapabilityActionCallReview & { contentRef?: CapabilityReviewContentRef },
254
+ context: ActionCardContext,
255
+ ): ActionCard => {
196
256
  const locale = review.locale;
197
257
  const tz = { timeZone: context.timeZone };
198
258
  const parts = (form: Parameters<typeof capabilityReviewSentenceParts>[1]) =>
@@ -206,6 +266,7 @@ export const reviewActionCard = (review: CapabilityActionCallReview, context: Ac
206
266
  );
207
267
  const consequence = (entry: CapabilityConsequence) => ({
208
268
  text: sentence(entry.text, entry.slots),
269
+ plain: capabilitySentenceText(entry.text, entry.slots ?? review.slots, { locale, timeZone: context.timeZone }),
209
270
  ...(entry.mark ? { mark: entry.mark } : {}),
210
271
  });
211
272
  const code = review.ask ?? "app_asks";
@@ -224,12 +285,14 @@ export const reviewActionCard = (review: CapabilityActionCallReview, context: Ac
224
285
  locale,
225
286
  timeZone: context.timeZone,
226
287
  ...(preview ? { preview } : {}),
288
+ slots: review.slots,
227
289
  consequences: (review.consequences ?? []).map(consequence),
228
290
  ...(preview?.kind === "rule"
229
291
  ? { when: sentence(preview.when.text, preview.when.slots), may: preview.may.map(consequence) }
230
292
  : {}),
231
293
  }
232
294
  : null,
295
+ contentRef: review.contentRef,
233
296
  readMore: firstLink(review.links, ["open", "preview"]),
234
297
  links: otherLinks(review.links, locale, context.app.name),
235
298
  open: firstLink(review.links, ["open", "edit", "status"]) ?? firstObjectHref(review),
@@ -255,6 +318,14 @@ export const reviewActionCard = (review: CapabilityActionCallReview, context: Ac
255
318
  : plainText(context.app.name),
256
319
  short: capabilityReviewSentence(review, "short", tz),
257
320
  hasCheck: Boolean(review.check),
321
+ slotValues: slotValues(
322
+ [
323
+ review.slots,
324
+ ...(review.consequences ?? []).map((entry) => entry.slots ?? {}),
325
+ ...(preview?.kind === "rule" ? [preview.when.slots ?? {}, ...preview.may.map((entry) => entry.slots ?? {})] : []),
326
+ ].flatMap((slots) => Object.values(slots)),
327
+ { locale, timeZone: context.timeZone },
328
+ ),
258
329
  };
259
330
  };
260
331
 
@@ -340,7 +411,8 @@ const messageRows = (block: ToolBlock): AiLegacyReviewRow[] => {
340
411
  for (const line of message.split("\n").map((entry) => entry.trim())) {
341
412
  if (!line || /^Review the validated arguments below before running this action\.$/i.test(line)) continue;
342
413
  const heading = /^([^:]{1,40}):$/.exec(line);
343
- const match = /^([^:]{1,40}):\s+(.+)$/.exec(line);
414
+ // The value starts at its first visible character, so the pattern never backtracks over the white space before it.
415
+ const match = /^([^:]{1,40}):\s+(\S.*)$/.exec(line);
344
416
  if (heading) {
345
417
  open = { label: heading[1]!, value: "" };
346
418
  rows.push(open);
@@ -36,6 +36,13 @@ const messages = i18n.define({
36
36
  unavailable: "You can't answer this question here.",
37
37
  stopping: "The response is stopping.",
38
38
  submitFailed: "Your answer didn't go through. Try again.",
39
+ fullReview: "Read the complete proposal",
40
+ baseRevision: ({ revision }: { revision: number }) => `Based on revision ${revision}`,
41
+ fullReviewLoading: "Loading the complete proposal …",
42
+ fullReviewFailed: "The complete proposal could not be loaded. Try again before approving.",
43
+ fullReviewRequired: "Read the complete proposal before approving.",
44
+ fullReviewRetry: "Try again",
45
+ fullReviewClose: "Close",
39
46
  // Preview
40
47
  from: "From",
41
48
  to: "To",
@@ -94,6 +101,13 @@ const messages = i18n.define({
94
101
  unavailable: "Diese Frage kannst du hier nicht beantworten.",
95
102
  stopping: "Die Antwort wird angehalten.",
96
103
  submitFailed: "Deine Antwort ging nicht durch. Versuche es noch einmal.",
104
+ fullReview: "Vollständigen Vorschlag lesen",
105
+ baseRevision: ({ revision }: { revision: number }) => `Auf Basis von Version ${revision}`,
106
+ fullReviewLoading: "Der vollständige Vorschlag wird geladen …",
107
+ fullReviewFailed: "Der vollständige Vorschlag konnte nicht geladen werden. Versuche es vor der Freigabe noch einmal.",
108
+ fullReviewRequired: "Lies vor der Freigabe den vollständigen Vorschlag.",
109
+ fullReviewRetry: "Noch einmal versuchen",
110
+ fullReviewClose: "Schließen",
97
111
  from: "Von",
98
112
  to: "An",
99
113
  cc: "Cc",
@@ -22,6 +22,18 @@ type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
22
22
  const params = new URLSearchParams(location.search);
23
23
  const lang = (params.get("lang") === "en" ? "en" : "de") as ActionFixtureLocale;
24
24
  const fixtures = actionFixtures(lang);
25
+ if (params.has("full")) {
26
+ const block = fixtures.mail!.block;
27
+ if (block.review && "sentence" in block.review) {
28
+ block.review = {
29
+ ...block.review,
30
+ contentRef: {
31
+ href: "/api/approvals/11111111-1111-4111-8111-111111111111/content",
32
+ digest: "a".repeat(64),
33
+ },
34
+ };
35
+ }
36
+ }
25
37
  const state = params.get("state") ?? "open";
26
38
  /** Each entry is `fixture` or `fixture:state`; its id is unique even when one fixture shows in several states. */
27
39
  const entries = (params.get("scenarios") ?? "mail")