@k2b/cloud 0.41.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 (227) hide show
  1. package/package.json +6 -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-timeout.ts +71 -0
  6. package/src/_internal/define-app.ts +145 -89
  7. package/src/_internal/http-drain.ts +56 -0
  8. package/src/_internal/registry-validation.ts +3 -0
  9. package/src/_internal/runtime-context.ts +1 -0
  10. package/src/_internal/status-preserving-ssr.ts +2 -0
  11. package/src/access/PermissionEditor.tsx +16 -17
  12. package/src/access/PrincipalPicker.tsx +45 -10
  13. package/src/access/messages.ts +8 -0
  14. package/src/access/principal-label.ts +18 -0
  15. package/src/access/ui.ts +1 -0
  16. package/src/account/EntitySearch.tsx +5 -4
  17. package/src/ai/action-review.ts +2 -1
  18. package/src/ai/app-skill-store.ts +1 -1
  19. package/src/ai/capabilities.ts +21 -3
  20. package/src/ai/capability-execution.ts +14 -12
  21. package/src/ai/chat/action-card.ts +7 -1
  22. package/src/ai/chat/action-messages.ts +14 -0
  23. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  24. package/src/ai/chat/action-question.tsx +202 -22
  25. package/src/ai/chat/action-text.ts +1 -1
  26. package/src/ai/chat-tasks.ts +2 -1
  27. package/src/ai/client/controller.ts +150 -32
  28. package/src/ai/client/transport.ts +14 -2
  29. package/src/ai/code-capability-routes.ts +27 -23
  30. package/src/ai/code-runtime-tools.ts +14 -16
  31. package/src/ai/code-source-tools.ts +16 -18
  32. package/src/ai/executor.ts +56 -21
  33. package/src/ai/fetch-file-tool.ts +67 -76
  34. package/src/ai/http.ts +1 -0
  35. package/src/ai/migrate.ts +13 -0
  36. package/src/ai/open-questions.ts +4 -8
  37. package/src/ai/projects.ts +1 -2
  38. package/src/ai/protocol.ts +2 -2
  39. package/src/ai/recent-resources.ts +12 -5
  40. package/src/ai/review-content.ts +77 -0
  41. package/src/ai/routes.ts +69 -31
  42. package/src/ai/runtime.ts +1 -0
  43. package/src/ai/skill-tool.ts +17 -6
  44. package/src/ai/skills-routes.ts +72 -27
  45. package/src/ai/skills.ts +139 -60
  46. package/src/ai/store.ts +168 -33
  47. package/src/ai/types.ts +10 -3
  48. package/src/ai/usage.ts +2 -2
  49. package/src/api/admin-core-settings.ts +15 -2
  50. package/src/api/admin-geo.ts +54 -0
  51. package/src/api/admin-linux-identities.ts +6 -3
  52. package/src/api/admin-outgoing-mail.ts +29 -15
  53. package/src/api/approvals-loader.ts +3 -3
  54. package/src/api/approvals.ts +51 -0
  55. package/src/api/apps.ts +4 -2
  56. package/src/api/capabilities.ts +258 -181
  57. package/src/api/capability-streams.ts +21 -13
  58. package/src/api/cli-plugins.ts +4 -1
  59. package/src/api/geo-glyphs.ts +11 -0
  60. package/src/api/geo.ts +128 -0
  61. package/src/api/help.ts +12 -4
  62. package/src/api/index.ts +5 -0
  63. package/src/api/mcp.ts +46 -38
  64. package/src/api/me-panel-consent.ts +8 -5
  65. package/src/api/search.ts +21 -14
  66. package/src/api/sync-ops.ts +19 -13
  67. package/src/api/widgets.ts +42 -18
  68. package/src/approvals/Approvals.tsx +5 -3
  69. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  70. package/src/browser/CloudResourceSearch.tsx +3 -13
  71. package/src/browser/PanelLeaveNotice.tsx +18 -0
  72. package/src/browser/navigation-search.ts +78 -25
  73. package/src/browser/panel-leave.ts +69 -0
  74. package/src/browser/panel-messages.ts +12 -0
  75. package/src/browser/panel-slot.ts +173 -24
  76. package/src/browser/panel.ts +3 -0
  77. package/src/browser/resource-search-messages.ts +2 -0
  78. package/src/capabilities/client.ts +28 -1
  79. package/src/capabilities/executions.ts +22 -3
  80. package/src/capabilities/geo-client.ts +57 -0
  81. package/src/capabilities/response.ts +2 -1
  82. package/src/capabilities/review-storage.ts +24 -0
  83. package/src/capabilities/reviews.ts +17 -3
  84. package/src/capabilities/server.ts +36 -16
  85. package/src/cli/access.ts +15 -23
  86. package/src/cli/admin/linux.ts +13 -2
  87. package/src/cli/admin/outgoing-mail.ts +26 -4
  88. package/src/cli/capabilities.ts +19 -0
  89. package/src/config/env.ts +0 -6
  90. package/src/contracts/app-access.ts +48 -0
  91. package/src/contracts/app.ts +4 -1
  92. package/src/contracts/approvals.ts +2 -2
  93. package/src/contracts/avatar.ts +186 -0
  94. package/src/contracts/capabilities.ts +148 -54
  95. package/src/contracts/geo-admin.ts +127 -0
  96. package/src/contracts/geo-map.ts +19 -0
  97. package/src/contracts/geo-route.ts +168 -0
  98. package/src/contracts/geo.ts +248 -0
  99. package/src/contracts/index.ts +4 -0
  100. package/src/contracts/outgoing-mail.ts +17 -4
  101. package/src/contracts/registry.ts +2 -0
  102. package/src/events/app-access.ts +68 -0
  103. package/src/events/live-delivery.ts +128 -0
  104. package/src/events/live.ts +14 -130
  105. package/src/geo/CloudMap.tsx +95 -0
  106. package/src/geo/map-messages.ts +17 -0
  107. package/src/geo/ui.ts +1 -0
  108. package/src/index.ts +2 -0
  109. package/src/server/client-ip.ts +36 -0
  110. package/src/server/index.ts +3 -4
  111. package/src/server/middleware/app-access.ts +312 -0
  112. package/src/server/middleware/auth.ts +1 -1
  113. package/src/server/middleware/invocation.ts +11 -0
  114. package/src/server/middleware/rate-limit.ts +4 -4
  115. package/src/server/services/access.ts +13 -9
  116. package/src/server/services/index.ts +0 -2
  117. package/src/server/services/services.ts +0 -2
  118. package/src/services/accounts/posix.ts +21 -7
  119. package/src/services/allowed-network-fetch.ts +16 -10
  120. package/src/services/app-access-metrics.ts +24 -0
  121. package/src/services/app-access.ts +123 -0
  122. package/src/services/app-discovery.ts +27 -0
  123. package/src/services/approvals/content.ts +40 -0
  124. package/src/services/approvals/index.ts +14 -6
  125. package/src/services/approvals/list.ts +6 -11
  126. package/src/services/approvals/question.ts +11 -7
  127. package/src/services/downstream-budget.ts +87 -0
  128. package/src/services/geo/admin.ts +262 -0
  129. package/src/services/geo/bucket.ts +67 -0
  130. package/src/services/geo/cache.ts +57 -0
  131. package/src/services/geo/costs.ts +5 -0
  132. package/src/services/geo/errors.ts +41 -0
  133. package/src/services/geo/fixtures/README.md +17 -0
  134. package/src/services/geo/fixtures/along.json +355 -0
  135. package/src/services/geo/fixtures/autocomplete.json +218 -0
  136. package/src/services/geo/fixtures/geocode.json +129 -0
  137. package/src/services/geo/fixtures/places.json +355 -0
  138. package/src/services/geo/fixtures/reverse.json +61 -0
  139. package/src/services/geo/fixtures/routing.json +57 -0
  140. package/src/services/geo/geoapify-route.ts +93 -0
  141. package/src/services/geo/geoapify.ts +281 -0
  142. package/src/services/geo/ledger.ts +122 -0
  143. package/src/services/geo/map.ts +160 -0
  144. package/src/services/geo/migrate.ts +25 -0
  145. package/src/services/geo/polyline.ts +184 -0
  146. package/src/services/geo/route-chain.ts +145 -0
  147. package/src/services/geo/route-costs.ts +9 -0
  148. package/src/services/geo/route-geometry.ts +39 -0
  149. package/src/services/geo/route-plan.ts +172 -0
  150. package/src/services/geo/route-service.ts +433 -0
  151. package/src/services/geo/service.ts +300 -0
  152. package/src/services/geo/settings.ts +49 -0
  153. package/src/services/help/index.ts +16 -7
  154. package/src/services/help/types.ts +6 -1
  155. package/src/services/identity/invocation-actor.ts +20 -2
  156. package/src/services/identity/issue-invocation.ts +67 -0
  157. package/src/services/identity/key-ring.ts +5 -3
  158. package/src/services/identity/runtime-config.ts +23 -0
  159. package/src/services/index.ts +9 -0
  160. package/src/services/mandates/index.ts +40 -3
  161. package/src/services/network-security.ts +6 -3
  162. package/src/services/network-transport.ts +99 -0
  163. package/src/services/notifications/catalog.ts +3 -2
  164. package/src/services/notifications/runtime.ts +1 -0
  165. package/src/services/notifications/web-push-transport.ts +45 -38
  166. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  167. package/src/services/outgoing-mail/index.ts +55 -1
  168. package/src/services/outgoing-mail/messages.ts +82 -3
  169. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  170. package/src/services/outgoing-mail/retention.ts +13 -1
  171. package/src/services/outgoing-mail/store.ts +36 -6
  172. package/src/services/outgoing-mail/test-send.ts +34 -4
  173. package/src/services/outgoing-mail/transport.ts +1 -1
  174. package/src/services/pdf/gotenberg.ts +11 -38
  175. package/src/services/pdf/template-preview.ts +1 -1
  176. package/src/services/public-http.ts +28 -40
  177. package/src/services/settings/app.ts +4 -1
  178. package/src/services/settings/core-settings.ts +88 -0
  179. package/src/services/settings/defaults.ts +8 -2
  180. package/src/services/settings/errors.ts +11 -0
  181. package/src/services/settings/geo-budget.ts +40 -0
  182. package/src/services/settings/index.ts +4 -3
  183. package/src/services/settings/store.ts +25 -17
  184. package/src/services/weather/geo.ts +29 -111
  185. package/src/services/weather/index.ts +4 -3
  186. package/src/services/weather/locations.ts +8 -5
  187. package/src/services/weather/migrate.ts +1 -0
  188. package/src/services/weather/settings.ts +0 -16
  189. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  190. package/src/shared/avatar-art.ts +400 -0
  191. package/src/shared/index.ts +2 -0
  192. package/src/shared/network-address.ts +22 -3
  193. package/src/shared/template-rendering.ts +36 -4
  194. package/src/shared-text/documents.ts +1 -1
  195. package/src/shared-text/index.ts +2 -0
  196. package/src/shared-text/log.ts +51 -3
  197. package/src/shared-text/snapshots.ts +340 -0
  198. package/src/ssr/AdminLayout.tsx +4 -2
  199. package/src/ssr/CloudShell.island.tsx +6 -0
  200. package/src/ssr/Layout.tsx +65 -15
  201. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  202. package/src/ssr/LayoutHelp.tsx +3 -2
  203. package/src/ssr/PanelRuntime.island.tsx +11 -7
  204. package/src/ssr/PwaLayout.tsx +2 -2
  205. package/src/ssr/RailApps.tsx +6 -0
  206. package/src/ssr/ShellRuntime.island.tsx +23 -7
  207. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  208. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
  209. package/src/ssr/admin-navigation.ts +1 -0
  210. package/src/ssr/app-badges.ts +6 -1
  211. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  212. package/src/ssr/help.ts +3 -2
  213. package/src/ssr/index.ts +1 -1
  214. package/src/ssr/layout-context.ts +2 -0
  215. package/src/ssr/mobile-menu-history.ts +4 -0
  216. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  217. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  218. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  219. package/src/ssr/page-swap.ts +28 -0
  220. package/src/ssr/page-trail-runtime.ts +32 -13
  221. package/src/ssr/panel-consent.ts +2 -2
  222. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  223. package/src/ssr/platform-messages.ts +8 -0
  224. package/src/ssr/runtime.ts +17 -0
  225. package/src/ssr/shell-data.tsx +10 -2
  226. package/src/styles/global.css +70 -6
  227. package/src/server/services/geo.ts +0 -215
@@ -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,
@@ -16,6 +16,7 @@ import type {
16
16
  CapabilityAskReason,
17
17
  CapabilityConsequence,
18
18
  CapabilityPreview,
19
+ CapabilityReviewContentRef,
19
20
  CapabilitySemanticLink,
20
21
  CapabilitySlot,
21
22
  } from "../../contracts/capabilities";
@@ -78,6 +79,7 @@ export type ActionCard = {
78
79
  preview: ActionPreviewModel | null;
79
80
  /** Opens exactly this version of the thing. */
80
81
  readMore?: string;
82
+ contentRef?: CapabilityReviewContentRef;
81
83
  /** The review's other links, such as editing the draft, each with its words. */
82
84
  links: { href: string; label: string }[];
83
85
  /** Where the thing itself opens, for an outcome Cloud could not see. */
@@ -247,7 +249,10 @@ const slotValues = (slots: CapabilitySlot[], context: { locale: string; timeZone
247
249
  unlisted: slots.some((slot) => slot.kind === "person" && (slot.total ?? slot.people.length) > slot.people.length),
248
250
  });
249
251
 
250
- export const reviewActionCard = (review: CapabilityActionCallReview, context: ActionCardContext): ActionCard => {
252
+ export const reviewActionCard = (
253
+ review: CapabilityActionCallReview & { contentRef?: CapabilityReviewContentRef },
254
+ context: ActionCardContext,
255
+ ): ActionCard => {
251
256
  const locale = review.locale;
252
257
  const tz = { timeZone: context.timeZone };
253
258
  const parts = (form: Parameters<typeof capabilityReviewSentenceParts>[1]) =>
@@ -287,6 +292,7 @@ export const reviewActionCard = (review: CapabilityActionCallReview, context: Ac
287
292
  : {}),
288
293
  }
289
294
  : null,
295
+ contentRef: review.contentRef,
290
296
  readMore: firstLink(review.links, ["open", "preview"]),
291
297
  links: otherLinks(review.links, locale, context.app.name),
292
298
  open: firstLink(review.links, ["open", "edit", "status"]) ?? firstObjectHref(review),
@@ -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")