@k2b/cloud 0.43.0 → 0.45.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 (308) hide show
  1. package/package.json +11 -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/GroupCoverage.tsx +20 -5
  21. package/src/access/PermissionEditor.tsx +20 -5
  22. package/src/access/PrincipalAvatar.tsx +30 -21
  23. package/src/access/PrincipalPicker.tsx +14 -4
  24. package/src/access/ResourceApiKeys.tsx +3 -4
  25. package/src/access/app-access-messages.ts +6 -0
  26. package/src/access/messages.ts +4 -4
  27. package/src/account/EntitySearch.tsx +12 -0
  28. package/src/account/ProfilePictureDialog.tsx +699 -0
  29. package/src/account/profile-picture-messages.ts +269 -0
  30. package/src/account/ui.ts +7 -0
  31. package/src/ai/admin.ts +3 -0
  32. package/src/ai/app-frame.ts +12 -0
  33. package/src/ai/application-tasks.ts +101 -0
  34. package/src/ai/assistant-models.ts +126 -64
  35. package/src/ai/capabilities.ts +19 -4
  36. package/src/ai/chart-block.ts +63 -30
  37. package/src/ai/chat/chart-block.tsx +12 -3
  38. package/src/ai/chat/composer-adapter.ts +8 -7
  39. package/src/ai/chat/message-actions.tsx +1 -1
  40. package/src/ai/chat/message-utils.ts +2 -2
  41. package/src/ai/chat/presentation.tsx +22 -1
  42. package/src/ai/chat-quotas.ts +5 -4
  43. package/src/ai/chat-tasks.ts +2 -2
  44. package/src/ai/client/controller.ts +11 -10
  45. package/src/ai/client/projection.ts +3 -3
  46. package/src/ai/compaction.ts +3 -3
  47. package/src/ai/credentials.ts +13 -114
  48. package/src/ai/default-tool-contracts.ts +4 -3
  49. package/src/ai/dictation-runtime.ts +10 -6
  50. package/src/ai/enrich.ts +2 -2
  51. package/src/ai/executor.ts +40 -25
  52. package/src/ai/http.ts +17 -29
  53. package/src/ai/index.ts +12 -5
  54. package/src/ai/inference-calls.ts +5 -4
  55. package/src/ai/maintenance.ts +1 -1
  56. package/src/ai/memory-learning-runs.ts +5 -5
  57. package/src/ai/memory-learning.ts +3 -3
  58. package/src/ai/message-queue.ts +1 -2
  59. package/src/ai/migrate.ts +75 -42
  60. package/src/ai/model-access-migrate.ts +3 -17
  61. package/src/ai/model-access.ts +44 -85
  62. package/src/ai/model-card.tsx +134 -0
  63. package/src/ai/model-catalog.ts +359 -0
  64. package/src/ai/model-chooser-messages.ts +88 -0
  65. package/src/ai/model-chooser.tsx +577 -0
  66. package/src/ai/model-delete.ts +234 -0
  67. package/src/ai/model-fixture.ts +39 -0
  68. package/src/ai/model-icon.tsx +36 -0
  69. package/src/ai/model-overview.ts +53 -0
  70. package/src/ai/model-usage.ts +45 -0
  71. package/src/ai/openai-api-messages.ts +0 -4
  72. package/src/ai/openai-api.ts +26 -9
  73. package/src/ai/personal-agent.ts +1 -1
  74. package/src/ai/prefs.ts +12 -10
  75. package/src/ai/project-tool.ts +4 -4
  76. package/src/ai/projects-authorization.ts +53 -0
  77. package/src/ai/projects-routes.ts +58 -47
  78. package/src/ai/projects.ts +110 -88
  79. package/src/ai/protocol.ts +2 -2
  80. package/src/ai/provider-connections.ts +161 -0
  81. package/src/ai/provider-errors.ts +55 -6
  82. package/src/ai/provider-fetch.ts +4 -0
  83. package/src/ai/provider.ts +1 -1
  84. package/src/ai/quota-limits.ts +36 -0
  85. package/src/ai/quota-provider.ts +4 -2
  86. package/src/ai/quota-report.ts +7 -4
  87. package/src/ai/quotas.ts +19 -8
  88. package/src/ai/recent-resources.ts +9 -3
  89. package/src/ai/request-headers.ts +14 -87
  90. package/src/ai/resource-refs.ts +6 -2
  91. package/src/ai/routes.ts +47 -48
  92. package/src/ai/runtime.ts +50 -22
  93. package/src/ai/settings.ts +93 -580
  94. package/src/ai/skill-tool.ts +4 -4
  95. package/src/ai/skills-routes.ts +27 -26
  96. package/src/ai/skills.ts +120 -81
  97. package/src/ai/store.ts +45 -23
  98. package/src/ai/stream.ts +1 -1
  99. package/src/ai/structured-runs.ts +2 -2
  100. package/src/ai/structured.ts +21 -44
  101. package/src/ai/task-contracts.ts +25 -5
  102. package/src/ai/task-execution.ts +15 -6
  103. package/src/ai/task-model.ts +9 -0
  104. package/src/ai/transcription.ts +27 -22
  105. package/src/ai/types.ts +50 -62
  106. package/src/ai/ui.tsx +13 -0
  107. package/src/ai/usage.ts +8 -8
  108. package/src/ai/validate.ts +8 -7
  109. package/src/ai/view-block.ts +2 -2
  110. package/src/api/accounts-entities.ts +22 -3
  111. package/src/api/admin-ai-quotas.ts +34 -56
  112. package/src/api/admin-ai-skills.ts +4 -1
  113. package/src/api/admin-ai-usage.ts +1 -1
  114. package/src/api/admin-ai.ts +173 -0
  115. package/src/api/admin-core-settings.ts +68 -244
  116. package/src/api/app-approval.ts +3 -1
  117. package/src/api/app-event-delivery.ts +51 -0
  118. package/src/api/app-events.ts +123 -0
  119. package/src/api/app-resource-access.ts +48 -0
  120. package/src/api/auth/account-mismatch.ts +24 -0
  121. package/src/api/auth.ts +35 -5
  122. package/src/api/capabilities.ts +28 -4
  123. package/src/api/index.ts +7 -1
  124. package/src/api/mcp.ts +175 -2
  125. package/src/api/me.ts +6 -4
  126. package/src/api/pwa-phone.ts +75 -26
  127. package/src/api/pwa.ts +34 -8
  128. package/src/api/resource-links.ts +83 -6
  129. package/src/api/resource-resolve.ts +50 -0
  130. package/src/api/search/schemas.ts +3 -1
  131. package/src/api/search.ts +9 -7
  132. package/src/app-pwa/contracts.ts +4 -0
  133. package/src/app-pwa/core-query-error.ts +20 -0
  134. package/src/app-pwa/index.ts +1 -0
  135. package/src/app-pwa/service.ts +91 -0
  136. package/src/app-pwa/transport.ts +22 -0
  137. package/src/approvals/messages.ts +2 -2
  138. package/src/browser/cloud-id.ts +86 -0
  139. package/src/browser/resource-clipboard.ts +4 -0
  140. package/src/browser/resource-links.ts +52 -0
  141. package/src/browser/session.ts +39 -0
  142. package/src/browser/shared-text-history-validation.ts +151 -0
  143. package/src/browser/shared-text-history.ts +464 -0
  144. package/src/browser/shared-text-offline-authorized.ts +29 -0
  145. package/src/browser/shared-text-offline.ts +351 -0
  146. package/src/browser/shared-text-provider.ts +714 -0
  147. package/src/browser/shared-text-table-contract.ts +150 -0
  148. package/src/browser/shared-text.ts +19 -0
  149. package/src/capabilities/server-invoke.ts +2 -11
  150. package/src/capabilities/testing.ts +5 -2
  151. package/src/cli/access.ts +6 -5
  152. package/src/cli/admin/ai-models.ts +199 -0
  153. package/src/cli/admin/ai-quotas.ts +6 -102
  154. package/src/cli/admin/ai-usage.ts +4 -4
  155. package/src/cli/admin/app-credentials.ts +5 -3
  156. package/src/cli/admin/index.ts +5 -2
  157. package/src/cli/apps.ts +89 -26
  158. package/src/cli/capabilities.ts +22 -1
  159. package/src/cli/events.ts +320 -0
  160. package/src/config/env.ts +40 -0
  161. package/src/contracts/app-access.ts +26 -2
  162. package/src/contracts/app-details.ts +89 -0
  163. package/src/contracts/app-events.ts +51 -0
  164. package/src/contracts/app.ts +21 -5
  165. package/src/contracts/capabilities.ts +143 -3
  166. package/src/contracts/index.ts +2 -0
  167. package/src/contracts/notification-types.ts +36 -9
  168. package/src/contracts/pwa.ts +17 -1
  169. package/src/contracts/registry.ts +6 -0
  170. package/src/contracts/resource-clipboard.ts +35 -0
  171. package/src/contracts/resource-pages.ts +140 -0
  172. package/src/contracts/resource-resolution.ts +13 -0
  173. package/src/contracts/shared.ts +6 -1
  174. package/src/contracts/user-notifications.ts +2 -0
  175. package/src/events/app-event-delivery.ts +100 -0
  176. package/src/events/app-events.testing.ts +65 -0
  177. package/src/events/app-events.ts +134 -0
  178. package/src/events/core-user-created.ts +24 -0
  179. package/src/events/index.ts +9 -0
  180. package/src/events/testing.ts +2 -0
  181. package/src/index.ts +1 -1
  182. package/src/server/api/respond.ts +17 -4
  183. package/src/server/index.ts +2 -0
  184. package/src/server/middleware/app-access.ts +1 -1
  185. package/src/server/middleware/auth.ts +21 -7
  186. package/src/server/middleware/index.ts +1 -0
  187. package/src/server/middleware/invocation.ts +2 -0
  188. package/src/server/public-storage.ts +106 -0
  189. package/src/server/services/access.ts +31 -10
  190. package/src/server/services/index.ts +1 -0
  191. package/src/services/accounts/app.ts +36 -13
  192. package/src/services/accounts/entities.ts +43 -19
  193. package/src/services/accounts/group-sql.ts +23 -0
  194. package/src/services/accounts/groups.ts +19 -14
  195. package/src/services/accounts/identities.ts +70 -11
  196. package/src/services/accounts/identity-reconciliation.ts +3 -1
  197. package/src/services/accounts/local-groups.ts +69 -4
  198. package/src/services/accounts/users.ts +45 -14
  199. package/src/services/app-access.ts +25 -7
  200. package/src/services/app-details.ts +224 -0
  201. package/src/services/app-discovery.ts +2 -1
  202. package/src/services/app-event-shapes.ts +139 -0
  203. package/src/services/app-events/accept.ts +116 -0
  204. package/src/services/app-events/admin.ts +250 -0
  205. package/src/services/app-events/declarations.ts +187 -0
  206. package/src/services/app-events/deliver.ts +218 -0
  207. package/src/services/app-events.ts +92 -0
  208. package/src/services/config-conflict.ts +11 -0
  209. package/src/services/core-user-events.ts +1 -0
  210. package/src/services/freeipa-config.ts +9 -0
  211. package/src/services/geo/ledger.ts +28 -10
  212. package/src/services/geo/service.ts +43 -24
  213. package/src/services/identity/core-origin.ts +12 -0
  214. package/src/services/identity/invocation-actor.ts +20 -2
  215. package/src/services/identity/invocation-authority.ts +4 -0
  216. package/src/services/identity/invocation-token.ts +238 -56
  217. package/src/services/identity/issue-invocation.ts +6 -1
  218. package/src/services/image-metadata.ts +65 -1
  219. package/src/services/index.ts +13 -1
  220. package/src/services/ipa/sync.ts +6 -2
  221. package/src/services/ipa/users.ts +4 -1
  222. package/src/services/mandates/index.ts +2 -2
  223. package/src/services/mandates/policy.ts +20 -12
  224. package/src/services/notifications/browser.ts +4 -0
  225. package/src/services/notifications/catalog.ts +5 -2
  226. package/src/services/notifications/platform.ts +7 -2
  227. package/src/services/notifications/user.ts +18 -7
  228. package/src/services/oauth-tokens.ts +28 -2
  229. package/src/services/object-store.ts +217 -0
  230. package/src/services/providers/local/users.ts +5 -0
  231. package/src/services/pwa-devices.ts +147 -37
  232. package/src/services/session/index.ts +26 -3
  233. package/src/services/settings/core-settings.ts +71 -43
  234. package/src/services/settings/index.ts +8 -0
  235. package/src/services/settings/versions.ts +26 -0
  236. package/src/shared/ai-models.ts +274 -0
  237. package/src/shared/ai-quotas.ts +2 -2
  238. package/src/shared/ai-usage.ts +2 -2
  239. package/src/shared/email-html.ts +1 -0
  240. package/src/shared/index.ts +2 -0
  241. package/src/shared/markdown/extensions/katex.ts +8 -1
  242. package/src/shared/markdown/extensions/tables.ts +14 -1
  243. package/src/shared/markdown/index.ts +22 -11
  244. package/src/shared-text/anchors.ts +29 -0
  245. package/src/shared-text/awareness.ts +87 -0
  246. package/src/shared-text/changes.ts +27 -0
  247. package/src/shared-text/documents.ts +433 -65
  248. package/src/shared-text/events.ts +12 -2
  249. package/src/shared-text/index.ts +16 -1
  250. package/src/shared-text/log.ts +24 -5
  251. package/src/shared-text/snapshots.ts +69 -23
  252. package/src/shared-text/structural-history.ts +377 -0
  253. package/src/shared-text/table-actions.ts +432 -0
  254. package/src/shared-text/text-diff.ts +75 -0
  255. package/src/shared-text/types.ts +24 -5
  256. package/src/ssr/AdminArea.tsx +131 -0
  257. package/src/ssr/AdminLayout.tsx +9 -3
  258. package/src/ssr/AdminSidebar.tsx +7 -7
  259. package/src/ssr/AppAccessDenied.tsx +2 -0
  260. package/src/ssr/AppBadge.tsx +14 -0
  261. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  262. package/src/ssr/CloudShell.island.tsx +44 -7
  263. package/src/ssr/Layout.tsx +9 -7
  264. package/src/ssr/MobileProfileActions.tsx +2 -2
  265. package/src/ssr/ProfilePreferences.tsx +2 -1
  266. package/src/ssr/PwaLayout.tsx +16 -1
  267. package/src/ssr/PwaPartCount.island.tsx +23 -0
  268. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  269. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  270. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  271. package/src/ssr/admin-active-link.ts +16 -0
  272. package/src/ssr/admin-navigation.ts +26 -31
  273. package/src/ssr/app-badges.ts +31 -7
  274. package/src/ssr/app-launchpad.tsx +6 -1
  275. package/src/ssr/index.ts +8 -0
  276. package/src/ssr/page-trail-runtime.ts +3 -1
  277. package/src/ssr/platform-messages.ts +11 -1
  278. package/src/ssr/profile-actions.ts +3 -6
  279. package/src/ssr/pwa-messages.ts +2 -2
  280. package/src/ssr/rail-navigation.ts +5 -1
  281. package/src/ssr/settings-draft.ts +153 -0
  282. package/src/ssr/shell-data.tsx +3 -1
  283. package/src/styles/global.css +1 -0
  284. package/src/styles/profile-picture.css +270 -0
  285. package/src/styles/utilities-navigation.css +6 -0
  286. package/src/workflows/ai/policy.ts +49 -0
  287. package/src/workflows/ai/runtime.ts +131 -21
  288. package/src/workflows/ai/store.ts +283 -35
  289. package/src/workflows/ai/types.ts +8 -2
  290. package/src/workflows/ai-actions.ts +148 -120
  291. package/src/workflows/authoring.ts +31 -0
  292. package/src/workflows/contracts.ts +1 -0
  293. package/src/workflows/definition.ts +2 -0
  294. package/src/workflows/editor.ts +15 -0
  295. package/src/workflows/index.ts +2 -1
  296. package/src/workflows/language/compiler.ts +34 -3
  297. package/src/workflows/language/expressions.ts +3 -2
  298. package/src/workflows/runtime/executor.ts +134 -34
  299. package/src/workflows/runtime/index.ts +2 -0
  300. package/src/workflows/runtime/ports.ts +5 -1
  301. package/src/workflows/store/actions.ts +1 -1
  302. package/src/workflows/store/definitions.ts +5 -0
  303. package/src/workflows/store/runs.ts +5 -3
  304. package/src/workflows/store/worker.ts +7 -2
  305. package/src/ai/model-pricing.ts +0 -43
  306. package/src/ai/model-request-settings.ts +0 -98
  307. package/src/ai/settings-messages.ts +0 -129
  308. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -1,14 +1,13 @@
1
1
  import { openAICompatibleTranscription, type TranscriptionProvider } from "@k2b/nessi/ai";
2
- import { coreSettings } from "../services";
3
2
  import { logger, type TraceContext, trace } from "../services/logging";
4
3
  import { AI_AUDIO_MAX_BYTES, inspectAiAudio } from "./audio-format";
5
4
  import { getAiCredential } from "./credentials";
6
5
  import { beginAiCall, finishAiCall } from "./inference-calls";
7
- import { readAiSettingsState, selectAiModelProfile } from "./settings";
6
+ import { classifyProviderFailure, recordAiProviderStatus } from "./provider-connections";
7
+ import { readAiSettingsState, resolveAiModelRef, selectAiModelProfile } from "./settings";
8
8
  import { type AiUsageAttribution, safelyRecordStructuredRun } from "./structured-runs";
9
9
  import type { AiDataBoundary, AiModelProfile } from "./types";
10
10
 
11
- export const AI_AUDIO_MODEL_SETTING_KEY = "ai.audio_model_id";
12
11
  /** Matches the existing chat turn execution budget; callers may cancel sooner. */
13
12
  export const AI_TRANSCRIPTION_TIMEOUT_MS = 10 * 60_000;
14
13
 
@@ -37,36 +36,42 @@ export const createAiTranscriptionProvider = (profile: AiModelProfile, apiKey?:
37
36
  model: provider.model,
38
37
  transcribe: async (request) => {
39
38
  try {
40
- return await provider.transcribe(request);
39
+ const result = await provider.transcribe(request);
40
+ await recordAiProviderStatus(profile.providerId, { status: "ok", message: "The provider connection works." });
41
+ return result;
41
42
  } catch (error) {
42
- throw describeTranscriptionFailure(error, "provider", request.signal?.aborted);
43
+ const failure = describeTranscriptionFailure(error, "provider", request.signal?.aborted);
44
+ if (!request.signal?.aborted)
45
+ await recordAiProviderStatus(
46
+ profile.providerId,
47
+ failure.httpStatus
48
+ ? classifyProviderFailure({ status: failure.httpStatus })
49
+ : failure.code === "transcription_connection_failed"
50
+ ? classifyProviderFailure(new TypeError())
51
+ : classifyProviderFailure(error),
52
+ );
53
+ throw failure;
43
54
  }
44
55
  },
45
56
  };
46
57
  };
47
58
 
48
59
  export const resolveAiAudioModel = async (
49
- input: { requestedModelId?: string; allowedDataBoundaries?: AiDataBoundary[] } = {},
60
+ input: { model?: import("../shared/ai-models").AiModelRequest; allowedDataBoundaries?: AiDataBoundary[] } = {},
50
61
  ): Promise<AiResolvedAudioModel> => {
51
62
  const state = await readAiSettingsState();
52
- if (!state.ok)
53
- throw Object.assign(
54
- new AiTranscriptionError(
55
- "transcription_configuration_failed",
56
- `Audio transcription is not available: ${state.error.message} An administrator can fix this in the AI settings.`,
57
- ),
58
- { aiError: state.error },
59
- );
60
63
  if (!state.enabled)
61
64
  throw new AiTranscriptionError(
62
65
  "transcription_configuration_failed",
63
66
  "Audio transcription is not available: AI is disabled. An administrator can enable AI in the AI settings.",
64
67
  );
65
- const modelId = input.requestedModelId ?? String((await coreSettings.get<string>(AI_AUDIO_MODEL_SETTING_KEY)) ?? "").trim();
68
+ const modelId = input.model
69
+ ? resolveAiModelRef(state, input.model).id
70
+ : state.assignments.find((assignment) => assignment.slot === "audio")?.variantId;
66
71
  if (!modelId)
67
72
  throw new AiTranscriptionError(
68
73
  "transcription_configuration_failed",
69
- "Audio transcription is not set up: no audio model is configured. An administrator can configure an audio model in the AI settings.",
74
+ "Audio transcription is not set up: no audio model is assigned. An administrator can configure an audio model in the AI settings.",
70
75
  );
71
76
  let profile: AiModelProfile;
72
77
  try {
@@ -93,7 +98,7 @@ export const resolveAiAudioModel = async (
93
98
  error instanceof Error && "aiError" in error ? { aiError: error.aiError } : {},
94
99
  );
95
100
  }
96
- const credential = await getAiCredential(profile.id);
101
+ const credential = await getAiCredential(profile.providerId);
97
102
  if (profile.provider === "openai" && !credential?.trim())
98
103
  throw new AiTranscriptionError(
99
104
  "transcription_configuration_failed",
@@ -108,7 +113,7 @@ export type RunAiTranscriptionInput = {
108
113
  filename: string;
109
114
  language?: string;
110
115
  prompt?: string;
111
- requestedModelId?: string;
116
+ model?: import("../shared/ai-models").AiModelRequest;
112
117
  allowedDataBoundaries?: AiDataBoundary[];
113
118
  signal?: AbortSignal;
114
119
  appId?: string;
@@ -119,7 +124,7 @@ export type RunAiTranscriptionInput = {
119
124
  };
120
125
 
121
126
  /** One provider call. Durable execution and input authorization belong to the caller. */
122
- export const runAiTranscription = async (input: RunAiTranscriptionInput): Promise<{ text: string; modelProfileId: string }> => {
127
+ export const runAiTranscription = async (input: RunAiTranscriptionInput): Promise<{ text: string; modelId: string }> => {
123
128
  return trace.withSpan(
124
129
  {
125
130
  name: `ai.transcription.${input.task}`,
@@ -165,13 +170,13 @@ export const runAiTranscription = async (input: RunAiTranscriptionInput): Promis
165
170
  });
166
171
  signal.throwIfAborted();
167
172
  succeeded = true;
168
- return { text: result.text, modelProfileId: resolved.profile.id };
173
+ return { text: result.text, modelId: resolved.profile.id };
169
174
  } catch (error) {
170
175
  failure = describeTranscriptionFailure(error, stage, signal.aborted);
171
176
  logger("ai:transcription")[input.signal?.aborted ? "info" : "error"](failure.message, {
172
177
  traceId: span.traceId,
173
178
  task: input.task,
174
- modelProfileId: resolved?.profile.id ?? input.requestedModelId,
179
+ modelId: resolved?.profile.id ?? input.model,
175
180
  providerModel: resolved?.profile.model,
176
181
  stage,
177
182
  errorCode: failure.code,
@@ -191,7 +196,7 @@ export const runAiTranscription = async (input: RunAiTranscriptionInput): Promis
191
196
  task: input.task,
192
197
  appId: input.appId,
193
198
  attribution: input.attribution,
194
- modelProfileId: resolved?.profile.id,
199
+ modelId: resolved?.profile.id,
195
200
  providerModel: resolved?.profile.model,
196
201
  mode: "transcription",
197
202
  attempts: resolved ? 1 : 0,
package/src/ai/types.ts CHANGED
@@ -20,6 +20,7 @@ import type {
20
20
  CloudResourceRef,
21
21
  } from "../contracts/capabilities";
22
22
  import type { RequestActor } from "../server";
23
+ import type { AiModelRequest, AiResolvedModelRef } from "../shared/ai-models";
23
24
  import type { AiActionCheck, AiStoredActionReview } from "./action-review";
24
25
  import type { AiLocalClient, AiLocalContext, AiLocalToolName } from "./local-tool-contracts";
25
26
  import type { AiTurnBlock } from "./protocol";
@@ -27,6 +28,8 @@ import type { AiTurnBlock } from "./protocol";
27
28
  /** Current reader-confirmed metadata for a recently sent resource. */
28
29
  export type AiRecentResource = {
29
30
  ref: CloudResourceRef;
31
+ context?: string;
32
+ status?: string;
30
33
  title: string;
31
34
  icon?: string;
32
35
  href?: string;
@@ -44,7 +47,7 @@ export type AiRecentSentResourceRef = {
44
47
  conversationId: string;
45
48
  };
46
49
 
47
- export const AI_MODEL_CAPABILITIES = ["streaming", "tools", "vision", "transcription"] as const;
50
+ export const AI_MODEL_CAPABILITIES = ["tools", "vision", "transcription"] as const;
48
51
  export type AiModelCapability = (typeof AI_MODEL_CAPABILITIES)[number];
49
52
 
50
53
  export const AI_IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"] as const;
@@ -62,11 +65,15 @@ export type AiDataPolicy = AiDataBoundary;
62
65
  export type AiModelProfile = {
63
66
  id: string;
64
67
  label: string;
68
+ modelId: string;
69
+ providerId: string;
70
+ variantLabel: string;
71
+ description: string;
72
+ icon: import("../shared/ai-models").AiModelIcon;
73
+ showInPicker: boolean;
65
74
  provider: AiProviderId;
66
75
  model: string;
67
76
  enabled: boolean;
68
- /** Small logo (data URL, hard-compressed) shown in the admin card and the composer model picker. */
69
- image?: string;
70
77
  capabilities: AiModelCapability[];
71
78
  dataBoundary: AiDataBoundary;
72
79
  baseURL?: string;
@@ -82,67 +89,34 @@ export type AiModelProfile = {
82
89
  pricing?: import("../shared/ai-costs").AiModelPricing;
83
90
  };
84
91
 
85
- export type AiPublicModelProfile = Pick<
86
- AiModelProfile,
87
- "id" | "label" | "provider" | "model" | "image" | "capabilities" | "dataBoundary" | "contextWindow"
88
- >;
89
-
90
92
  export type AiUserContentPart = ContentPart;
91
93
 
92
- export type AiSettingsErrorCode =
93
- | "ai_disabled"
94
- | "invalid_model_profiles"
95
- | "missing_default_model"
96
- | "default_model_disabled"
97
- | "missing_provider_credential"
98
- | "model_policy_mismatch"
99
- | "model_access_denied";
100
-
101
- export type AiSettingsError = {
102
- code: AiSettingsErrorCode;
103
- message: string;
104
- fields?: Record<string, string>;
94
+ export type AiSettingsErrorCode = "ai_disabled" | "ai_no_model" | "model_policy_mismatch";
95
+ export type AiSettingsError = { code: AiSettingsErrorCode; message: string; fields?: Record<string, string> };
96
+ export type AiSettingsState = {
97
+ ok: true;
98
+ enabled: boolean;
99
+ assignments: import("../shared/ai-models").AiModelAssignment[];
100
+ globalInstructions: string;
101
+ localClientInstructions: string;
102
+ compactionInstructions: string;
103
+ maxToolResultChars: number;
104
+ firecrawlConfigured: boolean;
105
+ profiles: AiModelProfile[];
105
106
  };
106
107
 
107
- export type AiSettingsState =
108
- | {
109
- ok: true;
110
- enabled: boolean;
111
- defaultModelId: string;
112
- visionModelId?: string;
113
- globalInstructions: string;
114
- localClientInstructions: string;
115
- compactionInstructions: string;
116
- maxToolResultChars: number;
117
- firecrawlConfigured: boolean;
118
- profiles: AiModelProfile[];
119
- }
120
- | {
121
- ok: false;
122
- enabled: boolean;
123
- defaultModelId: string;
124
- visionModelId?: string;
125
- globalInstructions: string;
126
- localClientInstructions: string;
127
- compactionInstructions: string;
128
- maxToolResultChars: number;
129
- firecrawlConfigured: boolean;
130
- profiles: AiModelProfile[];
131
- error: AiSettingsError;
132
- };
133
-
134
108
  export type AiModelPolicy =
135
109
  | { kind: "platform-default"; allowedDataBoundaries?: AiDataBoundary[]; requiredCapabilities?: AiModelCapability[] }
136
110
  | { kind: "locked"; modelId: string; allowedDataBoundaries?: AiDataBoundary[]; requiredCapabilities?: AiModelCapability[] }
137
111
  | {
138
112
  kind: "selectable";
139
- defaultModelId?: string;
140
113
  allowedModelIds?: string[];
141
114
  allowedDataBoundaries?: AiDataBoundary[];
142
115
  requiredCapabilities?: AiModelCapability[];
143
116
  };
144
117
 
145
118
  export type AiResolvedModel = {
119
+ model: import("../shared/ai-models").AiResolvedModelRef;
146
120
  profile: AiModelProfile;
147
121
  provider: Provider;
148
122
  };
@@ -230,7 +204,7 @@ export type AiEnrichmentRun = {
230
204
  conversationId: string;
231
205
  status: AiEnrichmentRunStatus;
232
206
  trigger: AiEnrichmentTrigger;
233
- modelProfileId: string | null;
207
+ modelId: string | null;
234
208
  /** nessi structured mode of the successful call: native | fallback | repair. */
235
209
  mode: string | null;
236
210
  durationMs: number | null;
@@ -298,7 +272,7 @@ export type AiStoredMessage = {
298
272
  kind: "message" | "summary";
299
273
  message: Message;
300
274
  loopId: string | null;
301
- modelProfileId: string | null;
275
+ modelId: string | null;
302
276
  providerModel: string | null;
303
277
  usage: Usage | null;
304
278
  stopReason: string | null;
@@ -313,6 +287,9 @@ export type AiStoredMessage = {
313
287
  meta: {
314
288
  /** The atomically consumed composer revision, used to confirm a local send. */
315
289
  submittedDraftRevision?: number;
290
+ modelRequest?: AiModelRequest;
291
+ modelResolved?: AiResolvedModelRef;
292
+ modelReplacement?: { kind: "model_replaced"; previousLabel: string; nextLabel: string };
316
293
  compactedCount?: number;
317
294
  todoPlan?: import("./todo-contracts").AiTodoPlan;
318
295
  steerId?: string;
@@ -353,6 +330,7 @@ export type AiConversationTimelineEntry = {
353
330
  id: string;
354
331
  seq: number;
355
332
  loopId: string | null;
333
+ modelReplacement?: { kind: "model_replaced"; previousLabel: string; nextLabel: string };
356
334
  userPreview: string;
357
335
  assistantPreview: string;
358
336
  isSteer: boolean;
@@ -418,7 +396,7 @@ export type AiTurn = {
418
396
  conversationId: string;
419
397
  status: AiTurnStatus;
420
398
  attempt: number;
421
- modelProfileId: string | null;
399
+ modelId: string | null;
422
400
  createdAt: string;
423
401
  completedAt: string | null;
424
402
  error: string | null;
@@ -534,7 +512,7 @@ export type AiProjectPromptSnapshot = {
534
512
  instructions: string;
535
513
  context: string;
536
514
  references: CloudResourceRef[];
537
- defaultModelProfileId: string | null;
515
+ defaultModel: import("../shared/ai-models").AiModelRequest | null;
538
516
  };
539
517
 
540
518
  export type AiProjectFileToolStat = {
@@ -569,6 +547,8 @@ export type AiSkillFileToolSource = {
569
547
 
570
548
  export type AiConversationResourceRef = {
571
549
  ref: CloudResourceRef;
550
+ context?: string | null;
551
+ status?: string | null;
572
552
  title: string | null;
573
553
  preview: string | null;
574
554
  icon: string | null;
@@ -581,6 +561,8 @@ export type AiConversationResourceRef = {
581
561
 
582
562
  export type AiConversationResourceObservation = {
583
563
  ref: CloudResourceRef;
564
+ context?: string;
565
+ status?: string;
584
566
  title?: string;
585
567
  preview?: string;
586
568
  icon?: string;
@@ -601,6 +583,8 @@ export type AiConversationResourceOccurrence = AiConversationResourceRef & {
601
583
  export type AiConversationSourceKind = "result" | "web" | "file" | "resource" | "activity";
602
584
 
603
585
  export type AiConversationSource = {
586
+ context?: string | null;
587
+ status?: string | null;
604
588
  kind: AiConversationSourceKind;
605
589
  key: string;
606
590
  title: string;
@@ -683,7 +667,10 @@ export type AiChatTurnRunConfig = {
683
667
  /** Request locale persisted with the turn so async execution keeps the caller preference. */
684
668
  locale?: string;
685
669
  modelPolicy?: AiModelPolicy;
686
- requestedModelId?: string;
670
+ model?: AiModelRequest;
671
+ modelResolved?: AiResolvedModelRef;
672
+ modelLabel?: string;
673
+ modelReplacement?: { kind: "model_replaced"; previousLabel: string; nextLabel: string };
687
674
  /** Optional instructions that apply only to this turn. */
688
675
  systemPrompt?: string;
689
676
  project?: AiProjectPromptSnapshot;
@@ -708,7 +695,8 @@ export type AiCompactionTurnRunConfig = {
708
695
  kind: "compact";
709
696
  actor?: RequestActor;
710
697
  modelPolicy?: AiModelPolicy;
711
- requestedModelId?: string;
698
+ model?: AiModelRequest;
699
+ modelResolved?: AiResolvedModelRef;
712
700
  };
713
701
 
714
702
  export type AiTurnRunConfig = AiChatTurnRunConfig | AiCompactionTurnRunConfig;
@@ -851,7 +839,7 @@ export type AiConversationService = {
851
839
  failInterChatMessage(input: { messageId: string; error: string }): Promise<boolean>;
852
840
  deliverInterChatMessage(input: {
853
841
  messageId: string;
854
- modelProfileId: string;
842
+ modelId: string;
855
843
  runConfig: AiChatTurnRunConfig;
856
844
  userMessage: Message;
857
845
  sourceHref?: string;
@@ -909,7 +897,7 @@ export type AiConversationService = {
909
897
  conversationId: string;
910
898
  status: AiEnrichmentRunStatus;
911
899
  trigger: AiEnrichmentTrigger;
912
- modelProfileId?: string;
900
+ modelId?: string;
913
901
  mode?: string;
914
902
  durationMs?: number;
915
903
  titleUpdated?: boolean;
@@ -954,7 +942,7 @@ export type AiConversationService = {
954
942
  turnId?: string;
955
943
  checkpointSeq: number;
956
944
  summary: Message;
957
- modelProfileId?: string | null;
945
+ modelId?: string | null;
958
946
  }): Promise<void>;
959
947
  listTurnMessages(input: { conversationId: string; loopId: string; includeCompacted?: boolean }): Promise<AiStoredMessage[]>;
960
948
  setMessageFeedback(input: {
@@ -964,12 +952,12 @@ export type AiConversationService = {
964
952
  }): Promise<AiMessageFeedback | null>;
965
953
  clearMessageFeedback(input: { conversationId: string; messageShortId: string }): Promise<boolean>;
966
954
  /** Create a queued compaction turn. Chat turns must use submitChatTurn. */
967
- createCompactionTurn(input: { conversationId: string; modelProfileId: string; runConfig: AiCompactionTurnRunConfig }): Promise<AiTurn>;
955
+ createCompactionTurn(input: { conversationId: string; modelId: string; runConfig: AiCompactionTurnRunConfig }): Promise<AiTurn>;
968
956
  /** Persist the user message and create its turn in one transaction. */
969
957
  submitChatTurn(input: {
970
958
  queuedMessageId?: string;
971
959
  conversationId: string;
972
- modelProfileId: string;
960
+ modelId: string;
973
961
  runConfig: AiChatTurnRunConfig;
974
962
  userMessage: Message;
975
963
  /** Atomically consume this saved draft revision when present. */
@@ -984,7 +972,7 @@ export type AiConversationService = {
984
972
  retrySourceTurnId?: string;
985
973
  }): Promise<{ turn: AiTurn; message: AiStoredMessage }>;
986
974
  getTurnRunConfig(input: { conversationId: string; turnId: string }): Promise<AiTurnRunConfig | null>;
987
- getLatestTurn(input: { conversationId: string }): Promise<AiTurn | null>;
975
+ getLatestTurn(input: { conversationId: string; kind?: AiTurnRunConfig["kind"] }): Promise<AiTurn | null>;
988
976
  getTurn(input: { conversationId: string; turnId: string }): Promise<AiTurn | null>;
989
977
  getTurnByShortId(input: { conversationId: string; shortId: string }): Promise<AiTurn | null>;
990
978
  getActiveTurn(input: { conversationId: string }): Promise<{
@@ -1076,7 +1064,7 @@ export type AiConversationService = {
1076
1064
  /** Called after a durable append, for incremental usage/history projection. */
1077
1065
  onMessage?: (message: AiStoredMessage) => Promise<void>;
1078
1066
  conversationId: string;
1079
- modelProfileId?: string | null;
1067
+ modelId?: string | null;
1080
1068
  turnId?: string | null;
1081
1069
  leaseOwner?: string | null;
1082
1070
  /** Ephemeral provider input for this turn; user-message persistence remains reference-only. */
package/src/ai/ui.tsx CHANGED
@@ -1,4 +1,17 @@
1
1
  export { AiTurnBlockView } from "./chat/blocks";
2
+ export { AiModelCard, type AiModelCardStop, aiModelClassLabel, aiModelClassSubtitle } from "./model-card";
3
+ export {
4
+ AiModelChooser,
5
+ type AiModelChooserState,
6
+ AiModelField,
7
+ AiModelNote,
8
+ aiModelBlocksSending,
9
+ aiModelRequestFor,
10
+ aiModelUnavailableText,
11
+ resolveAiModelChoice,
12
+ } from "./model-chooser";
13
+ export { aiModelChooserMessages } from "./model-chooser-messages";
14
+ export { AiModelIcon, aiModelDisplayIcon } from "./model-icon";
2
15
 
3
16
  import { latestLoopUsage, latestUsage, latestUsageSnapshot, textFromMessage } from "./chat/message-utils";
4
17
 
package/src/ai/usage.ts CHANGED
@@ -12,7 +12,7 @@ export type AiUsageRun = {
12
12
  createdAt: string;
13
13
  userId: string | null;
14
14
  userLabel: string | null;
15
- modelProfileId: string | null;
15
+ modelId: string | null;
16
16
  providerModel: string | null;
17
17
  appId: string | null;
18
18
  conversationId: string | null;
@@ -42,7 +42,7 @@ export type AiUsageFeedback = {
42
42
  conversationTitle: string;
43
43
  userId: string | null;
44
44
  userLabel: string | null;
45
- modelProfileId: string | null;
45
+ modelId: string | null;
46
46
  providerModel: string | null;
47
47
  rating: "up" | "down";
48
48
  reasons: string[];
@@ -137,7 +137,7 @@ const events = (since: Date, until: Date) => sql`
137
137
  `;
138
138
  const globalFilter = (q: AiUsageQuery) => sql`
139
139
  (${q.userId ?? null}::text IS NULL OR (${q.userId === "unassigned"} AND e.user_id IS NULL) OR e.user_id::text=${q.userId ?? null})
140
- AND (${q.modelProfileId ?? null}::text IS NULL OR e.model_profile_id=${q.modelProfileId ?? null})
140
+ AND (${q.modelId ?? null}::text IS NULL OR e.model_profile_id=${q.modelId ?? null})
141
141
  AND (${q.providerModel ?? null}::text IS NULL OR e.provider_model=${q.providerModel ?? null})
142
142
  AND (${q.workflowId ?? null}::uuid IS NULL OR e.workflow_id=${q.workflowId ?? null}::uuid)
143
143
  AND (${q.workflowRunId ?? null}::uuid IS NULL OR e.workflow_run_id=${q.workflowRunId ?? null}::uuid)
@@ -187,7 +187,7 @@ const pageOf = async <T>(projection: ReturnType<typeof events>, q: AiUsageQuery)
187
187
  return { items, total, page, perPage: q.perPage };
188
188
  };
189
189
  const runColumns = () => sql`e.id, e.kind, e.task, e.status, e.created_at::text AS "createdAt", e.user_id::text AS "userId",
190
- COALESCE(NULLIF(u.display_name,''),u.uid,sa.name) AS "userLabel",e.model_profile_id AS "modelProfileId",e.provider_model AS "providerModel",
190
+ COALESCE(NULLIF(u.display_name,''),u.uid,sa.name) AS "userLabel",e.model_profile_id AS "modelId",e.provider_model AS "providerModel",
191
191
  e.app_id AS "appId",e.conversation_id::text AS "conversationId",e.turn_id::text AS "turnId",e.workflow_run_id::text AS "workflowRunId",
192
192
  e.workflow_id::text AS "workflowId",e.workflow_name AS "workflowName",e.estimated,e.trace_id AS "traceId",e.tokens,e.cost::float8 AS cost,e.duration_ms AS "durationMs",e.error_code AS "errorCode",e.error,e.attempts,
193
193
  e.request_started_at::text AS "requestStartedAt",e.generation_ms AS "generationMs",e.headers_ms AS "headersMs",e.first_byte_ms AS "firstByteMs",
@@ -235,7 +235,7 @@ export const aiUsage = {
235
235
  // The response period is authoritative for both summary and feedback. Editing a rating never moves its response into a different period.
236
236
  const feedbackRows = sql`SELECT m.id::text AS id,c.id::text AS "conversationId",c.title AS "conversationTitle",
237
237
  e.user_id::text AS "userId",COALESCE(NULLIF(u.display_name,''),u.uid,sa.name) AS "userLabel",
238
- e.model_profile_id AS "modelProfileId",e.provider_model AS "providerModel",
238
+ e.model_profile_id AS "modelId",e.provider_model AS "providerModel",
239
239
  CASE WHEN m.feedback_rating=1 THEN 'up' ELSE 'down' END AS rating,
240
240
  m.feedback_reasons AS reasons,m.feedback_comment AS comment,m.created_at::text AS "createdAt",m.feedback_updated_at::text AS "updatedAt"
241
241
  FROM (${filtered}) e JOIN ai.messages m ON m.loop_id=e.turn_id::text AND m.conversation_id=e.conversation_id
@@ -266,7 +266,7 @@ export const aiUsage = {
266
266
  FROM ai.conversations c WHERE c.created_at >= ${since} AND c.created_at <= ${until} AND c.launched_by_app_id IS NOT NULL
267
267
  AND (${q.userId ?? null}::text IS NULL OR (${q.userId === "unassigned"} AND c.created_by_user_id IS NULL) OR c.created_by_user_id::text=${q.userId ?? null})
268
268
  AND (${q.appId ?? null}::text IS NULL OR c.launched_by_app_id=${q.appId ?? null})
269
- AND ((${q.modelProfileId ?? null}::text IS NULL AND ${q.providerModel ?? null}::text IS NULL) OR EXISTS (
269
+ AND ((${q.modelId ?? null}::text IS NULL AND ${q.providerModel ?? null}::text IS NULL) OR EXISTS (
270
270
  SELECT 1 FROM (${filtered}) e WHERE e.kind='chat' AND e.conversation_id=c.id
271
271
  )) GROUP BY c.launched_by_app_id ORDER BY chats DESC,c.launched_by_app_id`,
272
272
  q,
@@ -305,12 +305,12 @@ export const aiUsage = {
305
305
  LEFT JOIN auth.users u ON u.id=e.user_id LEFT JOIN auth.service_accounts sa ON sa.id=e.user_id WHERE e.kind=${kind} AND e.id=${id}`;
306
306
  return row ?? null;
307
307
  },
308
- facets: async (field: "userId" | "modelProfileId" | "providerModel" | "appId" | "task", search: string, input: Partial<AiUsageQuery>) => {
308
+ facets: async (field: "userId" | "modelId" | "providerModel" | "appId" | "task", search: string, input: Partial<AiUsageQuery>) => {
309
309
  const { query, since, until } = period(input);
310
310
  const key =
311
311
  field === "userId"
312
312
  ? sql`e.user_id::text`
313
- : field === "modelProfileId"
313
+ : field === "modelId"
314
314
  ? sql`e.model_profile_id`
315
315
  : field === "providerModel"
316
316
  ? sql`e.provider_model`
@@ -1,9 +1,10 @@
1
1
  import type { Input } from "@k2b/nessi";
2
+ import type { AiModelRequest } from "../shared/ai-models";
2
3
  import { readAiSettingsState, resolveAiModelFromState } from "./settings";
3
4
  import type { AiModelPolicy, AiSettingsError, AiSettingsState } from "./types";
4
5
 
5
6
  export const isAiSettingsError = (error: unknown): error is Error & { aiError: AiSettingsError } =>
6
- error instanceof Error && typeof (error as Error & { aiError?: unknown }).aiError === "object";
7
+ error instanceof Error && "aiError" in error && typeof error.aiError === "object";
7
8
 
8
9
  const inputIncludesFiles = (input: Input): boolean =>
9
10
  Array.isArray(input) && input.some((part) => typeof part === "object" && part.type === "file");
@@ -13,27 +14,27 @@ export type ValidateAiTurnInput = {
13
14
  hasImageAttachments?: boolean;
14
15
  canInspectAttachedImages?: boolean;
15
16
  modelPolicy?: AiModelPolicy;
16
- requestedModelId?: string;
17
+ model?: AiModelRequest;
17
18
  };
18
19
 
19
20
  export const validateAiTurnRequest = async (
20
21
  input: ValidateAiTurnInput,
22
+ providedState?: AiSettingsState,
21
23
  ): Promise<{ settings: Extract<AiSettingsState, { ok: true }>; resolved: Awaited<ReturnType<typeof resolveAiModelFromState>> }> => {
22
- const settings = await readAiSettingsState();
23
- if (!settings.ok) throw Object.assign(new Error(settings.error.message), { aiError: settings.error });
24
+ const settings = providedState ?? (await readAiSettingsState());
24
25
  if (!settings.enabled) {
25
26
  throw Object.assign(new Error("AI is disabled."), {
26
27
  aiError: { code: "ai_disabled", message: "AI is disabled." } satisfies AiSettingsError,
27
28
  });
28
29
  }
29
30
 
30
- const resolved = await resolveAiModelFromState(settings, input.modelPolicy ?? { kind: "platform-default" }, input.requestedModelId);
31
+ const resolved = await resolveAiModelFromState(settings, input.modelPolicy ?? { kind: "platform-default" }, input.model);
31
32
  if (inputIncludesFiles(input.input) && !resolved.profile.capabilities.includes("vision")) {
32
33
  throw Object.assign(new Error(`AI model "${resolved.profile.id}" does not support image input.`), {
33
34
  aiError: {
34
35
  code: "model_policy_mismatch",
35
36
  message: `AI model "${resolved.profile.label}" does not support image input.`,
36
- fields: { modelProfileId: "Choose a model with vision support." },
37
+ fields: { modelId: "Choose a model with vision support." },
37
38
  } satisfies AiSettingsError,
38
39
  });
39
40
  }
@@ -44,7 +45,7 @@ export const validateAiTurnRequest = async (
44
45
  aiError: {
45
46
  code: "model_policy_mismatch",
46
47
  message: `AI model "${resolved.profile.label}" needs Vision support or Tools with a configured Vision tool model.`,
47
- fields: { modelProfileId: "Choose a Vision model, or configure the view_image fallback." },
48
+ fields: { modelId: "Choose a Vision model, or configure the view_image fallback." },
48
49
  } satisfies AiSettingsError,
49
50
  });
50
51
  }
@@ -41,7 +41,7 @@ export const CLOUD_AI_DETAILS_VIEWPORT_ITEMS = 12;
41
41
  // the viewport at any width and its length counts no further.
42
42
  export const CLOUD_AI_DETAILS_MAX_CHARS = 4096;
43
43
 
44
- const referenceSchema = () => CloudResourceReferenceSchema.pick({ type: true, id: true, title: true });
44
+ const referenceSchema = () => CloudResourceReferenceSchema.pick({ type: true, id: true, title: true, context: true, status: true });
45
45
  const ReferenceSchema = /* @__PURE__ */ referenceSchema();
46
46
  export type CloudAiViewReference = z.output<typeof ReferenceSchema>;
47
47
  const cellTypeSchema = () => z.enum(["text", "number", "date", "datetime", "ref"]);
@@ -371,7 +371,7 @@ const ExplorerViewSchema = ExplorerDataSchema.superRefine((value, ctx) => {
371
371
  };
372
372
  }
373
373
  const domains = [
374
- ...("xAxis" in input && input.xAxis?.domain ? [input.xAxis.domain] : []),
374
+ ...("xAxis" in input && input.kind !== "bar" && input.xAxis?.domain ? [input.xAxis.domain] : []),
375
375
  ...("yAxis" in input && input.yAxis?.domain ? [input.yAxis.domain] : []),
376
376
  ];
377
377
  const stepping = domains.some(
@@ -10,6 +10,8 @@ import {
10
10
  PaginationQuerySchema,
11
11
  PaginationResponseSchema,
12
12
  parsePagination,
13
+ ServiceAccountKindSchema,
14
+ ServiceAccountStatusSchema,
13
15
  UserProfileSchema,
14
16
  UserProviderSchema,
15
17
  } from "../contracts";
@@ -34,6 +36,9 @@ const QuerySchema = z
34
36
  profile: UserProfileSchema.optional(),
35
37
  user_ids: z.string().optional(),
36
38
  group_ids: z.string().optional(),
39
+ service_account_ids: z.string().optional(),
40
+ service_account_kinds: z.string().optional(),
41
+ service_account_status: ServiceAccountStatusSchema.optional(),
37
42
  exclude_user_ids: z.string().optional(),
38
43
  exclude_group_ids: z.string().optional(),
39
44
  exclude_service_account_ids: z.string().optional(),
@@ -43,7 +48,6 @@ const QuerySchema = z
43
48
  parent_group_id: z.uuid().optional(),
44
49
  managed_by_user_id: z.uuid().optional(),
45
50
  recursive: z.enum(["true", "false"]).optional(),
46
- include_personal: z.enum(["true", "false"]).optional(),
47
51
  })
48
52
  .refine(
49
53
  (value) => {
@@ -73,6 +77,14 @@ const parseKinds = (value?: string) => {
73
77
  return { ok: false as const, message: "Invalid kinds query parameter." };
74
78
  };
75
79
 
80
+ const parseServiceAccountKinds = (value?: string) => {
81
+ const kinds = parseCsv(value);
82
+ if (kinds.length === 0) return { ok: true as const, value: undefined };
83
+ const parsed = z.array(ServiceAccountKindSchema).safeParse([...new Set(kinds)]);
84
+ if (parsed.success) return { ok: true as const, value: parsed.data };
85
+ return { ok: false as const, message: "Invalid service_account_kinds query parameter." };
86
+ };
87
+
76
88
  const parseUuidList = (value: string | undefined, label: string) => {
77
89
  const ids = parseCsv(value);
78
90
  if (ids.length === 0) return { ok: true as const, value: undefined };
@@ -100,7 +112,7 @@ export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRoute
100
112
  tags: ["Accounts"],
101
113
  summary: "List visible directory entries",
102
114
  description:
103
- "List visible users and groups with SQL-backed filtering and pagination. Guest accounts see only themselves and their effective groups; relation filters require a full user account. Standalone and agent accounts with read scope see only explicitly shared directory entries and cannot query relationships. Personal Linux groups (a user's private primary group, flagged with `personalOwner`) are left out unless `include_personal=true`; exact `group_ids` lookups and relation filters always return them.",
115
+ "List visible users and groups with SQL-backed filtering and pagination. Guest accounts see only themselves and their effective groups; relation filters require a full user account. Standalone and agent accounts with read scope see only explicitly shared directory entries and cannot query relationships. Personal Linux groups are excluded, including exact group IDs and relation filters. Inspect the owning user's Linux identity for diagnostic attributes.",
104
116
  ...requiresAuth,
105
117
  responses: {
106
118
  200: jsonResponse(EntitiesListResponseSchema, "Paginated mixed entity list"),
@@ -126,6 +138,8 @@ export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRoute
126
138
  const params = parsePagination(query);
127
139
  const kinds = parseKinds(query.kinds);
128
140
  if (!kinds.ok) return respond(c, fail(err.badInput(kinds.message)));
141
+ const serviceAccountKinds = parseServiceAccountKinds(query.service_account_kinds);
142
+ if (!serviceAccountKinds.ok) return respond(c, fail(err.badInput(serviceAccountKinds.message)));
129
143
  const excludeUserIds = parseUuidList(query.exclude_user_ids, "exclude_user_ids");
130
144
  if (!excludeUserIds.ok) return respond(c, fail(err.badInput(excludeUserIds.message)));
131
145
  const excludeGroupIds = parseUuidList(query.exclude_group_ids, "exclude_group_ids");
@@ -139,6 +153,9 @@ export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRoute
139
153
  const groupIds = parseUuidList(query.group_ids, "group_ids");
140
154
  if (!groupIds.ok) return respond(c, fail(err.badInput(groupIds.message)));
141
155
 
156
+ const serviceAccountIds = parseUuidList(query.service_account_ids, "service_account_ids");
157
+ if (!serviceAccountIds.ok) return respond(c, fail(err.badInput(serviceAccountIds.message)));
158
+
142
159
  const filters = {
143
160
  search: query.search,
144
161
  kinds: kinds.value,
@@ -146,6 +163,9 @@ export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRoute
146
163
  profile: query.profile,
147
164
  userIds: userIds.value,
148
165
  groupIds: groupIds.value,
166
+ serviceAccountIds: serviceAccountIds.value,
167
+ serviceAccountKinds: serviceAccountKinds.value,
168
+ serviceAccountStatus: query.service_account_status,
149
169
  excludeUserIds: excludeUserIds.value,
150
170
  excludeGroupIds: excludeGroupIds.value,
151
171
  excludeServiceAccountIds: excludeServiceAccountIds.value,
@@ -155,7 +175,6 @@ export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRoute
155
175
  parentGroupId: query.parent_group_id,
156
176
  managedByUserId: query.managed_by_user_id,
157
177
  recursive: query.recursive === "true",
158
- includePersonal: query.include_personal === "true",
159
178
  };
160
179
  if (!user) {
161
180
  const result = await (dependencies.searchShares ?? directoryShares.search)({