@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,21 +1,29 @@
1
1
  import { type SQL, type SQLQuery, sql } from "bun";
2
2
  import type { CloudResourceRef } from "../contracts/capabilities";
3
3
  import { AuthenticatedPrincipalSchema, type ServiceAccountKind } from "../contracts/shared";
4
- import type { AccessSubject } from "../server";
5
4
  import {
6
5
  buildAccessPrincipalCondition,
7
6
  createAccess,
8
7
  deleteAccess,
8
+ ensureManagerRemains,
9
9
  hasPermission,
10
10
  type PermissionLevel,
11
11
  type Principal,
12
12
  } from "../server/services/access";
13
13
  import { stripImageMetadata } from "../services/image-metadata";
14
14
  import { toPgUuidArray } from "../services/postgres";
15
+ import { type AiModelRequest, AiModelRequestSchema } from "../shared/ai-models";
15
16
  import { AiFileVersionConflict, AiFileWriteError, aiFileContentVersion } from "./file-content-version";
16
17
  import { isAiImage } from "./file-media-type";
17
18
  import { mountAiProjectFilePath } from "./file-mount";
18
19
  import { pickStoredAiFilePath } from "./files-store";
20
+ import {
21
+ type AiResourceContext,
22
+ aiResourceAccessSubject,
23
+ aiResourcePermissionCap,
24
+ capAiResourcePermission,
25
+ requireAiResourceCreation,
26
+ } from "./projects-authorization";
19
27
  import { withAiShortIdForDb } from "./short-id";
20
28
  import type { AiProjectPromptSnapshot } from "./types";
21
29
 
@@ -42,7 +50,7 @@ export type AiProject = {
42
50
  description: string;
43
51
  icon: string;
44
52
  instructions: string;
45
- defaultModelProfileId: string | null;
53
+ defaultModel: AiModelRequest | null;
46
54
  permission: AiProjectPermission;
47
55
  revision: number;
48
56
  createdAt: string;
@@ -85,7 +93,7 @@ export type AiProjectAccess = {
85
93
  permission: AiProjectPermission;
86
94
  displayName?: string;
87
95
  avatarHash?: string | null;
88
- /** Kind of a `service_account` principal; presentation only. */
96
+ /** Kind of a `service_account` principal, including manager eligibility. */
89
97
  serviceAccountKind?: ServiceAccountKind;
90
98
  createdAt: string;
91
99
  };
@@ -115,7 +123,7 @@ type ProjectRow = {
115
123
  description: string;
116
124
  icon: string;
117
125
  instructions: string;
118
- default_model_profile_id: string | null;
126
+ default_model: AiModelRequest | null;
119
127
  revision: number;
120
128
  created_at: Date | string;
121
129
  updated_at: Date | string;
@@ -170,15 +178,21 @@ type AdminProjectRow = ProjectRow & {
170
178
  };
171
179
 
172
180
  const iso = (value: Date | string): string => (value instanceof Date ? value.toISOString() : new Date(value).toISOString());
173
- const actorUserId = (subject: AccessSubject | null): string | null => (subject?.type === "user" ? subject.userId : null);
181
+ const actorUserId = (input: AiResourceContext | null): string | null => {
182
+ const subject = aiResourceAccessSubject(input);
183
+ return subject?.type === "user" ? subject.userId : null;
184
+ };
174
185
 
175
- const principalForSubject = (subject: AccessSubject): Principal =>
176
- subject.type === "user"
186
+ const principalForSubject = (input: AiResourceContext): Principal => {
187
+ const subject = aiResourceAccessSubject(input)!;
188
+ return subject.type === "user"
177
189
  ? { type: "user", userId: subject.userId }
178
190
  : { type: "service_account", serviceAccountId: subject.serviceAccountId };
191
+ };
179
192
 
180
- const accessMatch = (subject: AccessSubject | null): SQLQuery =>
181
- subject === null
193
+ const accessMatch = (input: AiResourceContext | null): SQLQuery => {
194
+ const subject = aiResourceAccessSubject(input);
195
+ return subject === null || aiResourcePermissionCap(input) === "none"
182
196
  ? sql`FALSE`
183
197
  : buildAccessPrincipalCondition({
184
198
  subject,
@@ -189,8 +203,9 @@ const accessMatch = (subject: AccessSubject | null): SQLQuery =>
189
203
  authenticatedOnly: sql`access.authenticated_only`,
190
204
  },
191
205
  });
206
+ };
192
207
 
193
- const permissionFor = async (row: ProjectRow, subject: AccessSubject | null, db: SQL = sql): Promise<AiProjectPermission | "none"> => {
208
+ const permissionFor = async (row: ProjectRow, subject: AiResourceContext | null, db: SQL = sql): Promise<AiProjectPermission | "none"> => {
194
209
  const match = accessMatch(subject);
195
210
  const rows = await db<{ permission: AiProjectPermission }[]>`
196
211
  SELECT access.permission
@@ -200,10 +215,10 @@ const permissionFor = async (row: ProjectRow, subject: AccessSubject | null, db:
200
215
  ORDER BY CASE access.permission WHEN 'admin' THEN 3 WHEN 'write' THEN 2 WHEN 'read' THEN 1 ELSE 0 END DESC
201
216
  LIMIT 1
202
217
  `;
203
- return rows[0]?.permission ?? "none";
218
+ return capAiResourcePermission(rows[0]?.permission ?? "none", subject);
204
219
  };
205
220
 
206
- const toProject = async (row: ProjectRow, subject: AccessSubject | null, db: SQL = sql): Promise<AiProject | null> => {
221
+ const toProject = async (row: ProjectRow, subject: AiResourceContext | null, db: SQL = sql): Promise<AiProject | null> => {
207
222
  const permission = await permissionFor(row, subject, db);
208
223
  if (permission === "none") return null;
209
224
  return {
@@ -213,7 +228,7 @@ const toProject = async (row: ProjectRow, subject: AccessSubject | null, db: SQL
213
228
  description: row.description,
214
229
  icon: row.icon,
215
230
  instructions: row.instructions,
216
- defaultModelProfileId: row.default_model_profile_id,
231
+ defaultModel: row.default_model ? AiModelRequestSchema.parse(row.default_model) : null,
217
232
  permission,
218
233
  revision: Number(row.revision),
219
234
  createdAt: iso(row.created_at),
@@ -330,41 +345,34 @@ const createProjectAccess = async (
330
345
  };
331
346
 
332
347
  const updateProjectAccess = async (projectId: string, accessId: string, permission: AiProjectPermission, db: SQL): Promise<boolean> => {
333
- const [bound] = await db<{ id: string; permission: AiProjectPermission }[]>`
334
- SELECT access.id, access.permission
348
+ const [bound] = await db<{ id: string }[]>`
349
+ SELECT access.id
335
350
  FROM ai.project_access project_access
336
351
  JOIN auth.access access ON access.id = project_access.access_id
337
352
  WHERE project_access.project_id = ${projectId}::uuid AND project_access.short_id = ${accessId}
338
353
  `;
339
354
  if (!bound) return false;
340
- if (bound.permission === "admin" && permission !== "admin") {
341
- const [admins] = await db<{ count: number }[]>`
342
- SELECT count(*)::int AS count FROM ai.project_access project_access
343
- JOIN auth.access access ON access.id = project_access.access_id
344
- WHERE project_access.project_id = ${projectId}::uuid AND access.permission = 'admin'
345
- `;
346
- if ((admins?.count ?? 0) <= 1) throw new AiProjectLastAdminError();
347
- }
355
+ const before = await listProjectAccess(projectId, db);
356
+ const guarded = ensureManagerRemains({
357
+ before,
358
+ after: before.map((entry) => (entry.shortId === accessId ? { ...entry, permission } : entry)),
359
+ });
360
+ if (!guarded.ok) throw new AiProjectLastAdminError();
348
361
  const result = await db`UPDATE auth.access SET permission = ${permission}::auth.permission_level WHERE id = ${bound.id}::uuid`;
349
362
  return result.count === 1;
350
363
  };
351
364
 
352
365
  const revokeProjectAccess = async (projectId: string, accessId: string, db: SQL): Promise<boolean> => {
353
- const [bound] = await db<{ access_id: string; permission: AiProjectPermission }[]>`
354
- SELECT project_access.access_id, access.permission
366
+ const [bound] = await db<{ access_id: string }[]>`
367
+ SELECT project_access.access_id
355
368
  FROM ai.project_access project_access
356
369
  JOIN auth.access access ON access.id = project_access.access_id
357
370
  WHERE project_access.project_id = ${projectId}::uuid AND project_access.short_id = ${accessId}
358
371
  `;
359
372
  if (!bound) return false;
360
- if (bound.permission === "admin") {
361
- const [admins] = await db<{ count: number }[]>`
362
- SELECT count(*)::int AS count FROM ai.project_access project_access
363
- JOIN auth.access access ON access.id = project_access.access_id
364
- WHERE project_access.project_id = ${projectId}::uuid AND access.permission = 'admin'
365
- `;
366
- if ((admins?.count ?? 0) <= 1) throw new AiProjectLastAdminError();
367
- }
373
+ const before = await listProjectAccess(projectId, db);
374
+ const guarded = ensureManagerRemains({ before, after: before.filter((entry) => entry.shortId !== accessId) });
375
+ if (!guarded.ok) throw new AiProjectLastAdminError();
368
376
  const deleted = await deleteAccess({ id: bound.access_id }, db);
369
377
  if (!deleted.ok) throw new Error(deleted.error.message);
370
378
  return true;
@@ -372,7 +380,7 @@ const revokeProjectAccess = async (projectId: string, accessId: string, db: SQL)
372
380
 
373
381
  const requireProject = async (
374
382
  projectId: string,
375
- subject: AccessSubject | null,
383
+ subject: AiResourceContext | null,
376
384
  permission: AiProjectPermission = "read",
377
385
  ): Promise<AiProject | null> => {
378
386
  const row = await getRow(projectId);
@@ -383,7 +391,7 @@ const requireProject = async (
383
391
 
384
392
  const requireProjectByShortId = async (
385
393
  shortId: string,
386
- subject: AccessSubject | null,
394
+ subject: AiResourceContext | null,
387
395
  permission: AiProjectPermission = "read",
388
396
  ): Promise<AiProject | null> => {
389
397
  const row = await getRowByShortId(shortId);
@@ -412,22 +420,23 @@ const projectSearchPattern = (value: string | undefined): string | null => {
412
420
 
413
421
  export const aiProjects = {
414
422
  async create(input: {
415
- subject: AccessSubject;
423
+ subject: AiResourceContext;
416
424
  name: string;
417
425
  description?: string;
418
426
  icon?: string;
419
427
  instructions?: string;
420
- defaultModelProfileId?: string | null;
428
+ defaultModel?: AiModelRequest | null;
421
429
  }): Promise<AiProject> {
430
+ requireAiResourceCreation(input.subject);
422
431
  return sql.begin(async (tx) => {
423
432
  const rows = await withAiShortIdForDb(
424
433
  tx,
425
434
  "idx_ai_projects_short_id",
426
435
  (attempt, shortId) => attempt<ProjectRow[]>`
427
- INSERT INTO ai.projects (short_id, name, description, icon, instructions, default_model_profile_id)
436
+ INSERT INTO ai.projects (short_id, name, description, icon, instructions, default_model)
428
437
  VALUES (
429
438
  ${shortId}, ${input.name.trim()}, ${input.description?.trim() ?? ""}, ${input.icon?.trim() || "ti ti-folders"},
430
- ${input.instructions?.trim() ?? ""}, ${input.defaultModelProfileId?.trim() || null}
439
+ ${input.instructions?.trim() ?? ""}, ${input.defaultModel ? JSON.stringify(AiModelRequestSchema.parse(input.defaultModel)) : null}::text::jsonb
431
440
  )
432
441
  RETURNING *
433
442
  `,
@@ -442,11 +451,11 @@ export const aiProjects = {
442
451
  VALUES (${rows[0]!.id}::uuid, ${created.data.id}::uuid, ${shortId})
443
452
  `,
444
453
  );
445
- return { ...(await toProject(rows[0]!, input.subject, tx))!, permission: "admin" };
454
+ return (await toProject(rows[0]!, input.subject, tx))!;
446
455
  });
447
456
  },
448
457
 
449
- async list(subject: AccessSubject | null): Promise<AiProject[]> {
458
+ async list(subject: AiResourceContext | null): Promise<AiProject[]> {
450
459
  const match = accessMatch(subject);
451
460
  const rows = await sql<ProjectRow[]>`
452
461
  SELECT DISTINCT project.*, lower(project.name) AS sort_name
@@ -459,7 +468,7 @@ export const aiProjects = {
459
468
  return (await Promise.all(rows.map((row) => toProject(row, subject)))).filter((project): project is AiProject => Boolean(project));
460
469
  },
461
470
 
462
- async resolveShortIds(projectIds: readonly string[], subject: AccessSubject | null): Promise<Map<string, string>> {
471
+ async resolveShortIds(projectIds: readonly string[], subject: AiResourceContext | null): Promise<Map<string, string>> {
463
472
  const ids = [...new Set(projectIds)];
464
473
  if (!ids.length) return new Map();
465
474
  const match = accessMatch(subject);
@@ -474,7 +483,7 @@ export const aiProjects = {
474
483
  return new Map(rows.map((row) => [row.id, row.short_id]));
475
484
  },
476
485
 
477
- async scopeVersion(subject: AccessSubject | null): Promise<string> {
486
+ async scopeVersion(subject: AiResourceContext | null): Promise<string> {
478
487
  const projects = await this.list(subject);
479
488
  return projects
480
489
  .map((project) => `${project.shortId}:${project.permission}`)
@@ -487,30 +496,34 @@ export const aiProjects = {
487
496
 
488
497
  async update(
489
498
  projectId: string,
490
- subject: AccessSubject | null,
491
- input: Partial<Pick<AiProject, "name" | "description" | "icon" | "instructions" | "defaultModelProfileId">>,
499
+ subject: AiResourceContext | null,
500
+ input: Partial<Pick<AiProject, "name" | "description" | "icon" | "instructions" | "defaultModel">>,
492
501
  ): Promise<AiProject | null> {
493
- const changesTrustedInstructions = Object.hasOwn(input, "instructions") || Object.hasOwn(input, "defaultModelProfileId");
494
- if (!(await requireProject(projectId, subject, changesTrustedInstructions ? "admin" : "write"))) return null;
495
- const rows = await sql<ProjectRow[]>`
496
- UPDATE ai.projects
497
- SET name = COALESCE(${input.name?.trim() ?? null}, name),
498
- description = COALESCE(${input.description?.trim() ?? null}, description),
499
- icon = COALESCE(${input.icon?.trim() ?? null}, icon),
500
- instructions = COALESCE(${input.instructions?.trim() ?? null}, instructions),
501
- default_model_profile_id = CASE
502
- WHEN ${Object.hasOwn(input, "defaultModelProfileId")} THEN ${input.defaultModelProfileId?.trim() || null}
503
- ELSE default_model_profile_id
504
- END,
505
- revision = revision + 1,
506
- updated_at = now()
507
- WHERE id = ${projectId}::uuid
508
- RETURNING *
509
- `;
510
- return rows[0] ? toProject(rows[0], subject) : null;
502
+ const changesTrustedInstructions = Object.hasOwn(input, "instructions") || Object.hasOwn(input, "defaultModel");
503
+ return sql.begin(async (tx) => {
504
+ const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
505
+ if (!project || !hasPermission(await permissionFor(project, subject, tx), changesTrustedInstructions ? "admin" : "write"))
506
+ return null;
507
+ const rows = await tx<ProjectRow[]>`
508
+ UPDATE ai.projects
509
+ SET name = COALESCE(${input.name?.trim() ?? null}, name),
510
+ description = COALESCE(${input.description?.trim() ?? null}, description),
511
+ icon = COALESCE(${input.icon?.trim() ?? null}, icon),
512
+ instructions = COALESCE(${input.instructions?.trim() ?? null}, instructions),
513
+ default_model = CASE
514
+ WHEN ${Object.hasOwn(input, "defaultModel")} THEN ${input.defaultModel ? JSON.stringify(AiModelRequestSchema.parse(input.defaultModel)) : null}::text::jsonb
515
+ ELSE default_model
516
+ END,
517
+ revision = revision + 1,
518
+ updated_at = now()
519
+ WHERE id = ${projectId}::uuid
520
+ RETURNING *
521
+ `;
522
+ return rows[0] ? toProject(rows[0], subject, tx) : null;
523
+ });
511
524
  },
512
525
 
513
- async delete(projectId: string, subject: AccessSubject | null): Promise<boolean> {
526
+ async delete(projectId: string, subject: AiResourceContext | null): Promise<boolean> {
514
527
  return sql.begin(async (tx) => {
515
528
  const [project] = await tx<ProjectRow[]>`
516
529
  SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
@@ -525,7 +538,7 @@ export const aiProjects = {
525
538
  });
526
539
  },
527
540
 
528
- async snapshot(projectId: string, subject: AccessSubject | null): Promise<AiProjectPromptSnapshot | null> {
541
+ async snapshot(projectId: string, subject: AiResourceContext | null): Promise<AiProjectPromptSnapshot | null> {
529
542
  return sql.begin(async (tx) => {
530
543
  const [row] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR SHARE`;
531
544
  if (!row || !hasPermission(await permissionFor(row, subject, tx), "read")) return null;
@@ -563,19 +576,19 @@ export const aiProjects = {
563
576
  instructions: project.instructions,
564
577
  context: lines.join("\n").slice(0, AI_PROJECT_PROMPT_MANIFEST_MAX_CHARS),
565
578
  references: references.map((reference) => reference.ref),
566
- defaultModelProfileId: project.defaultModelProfileId,
579
+ defaultModel: project.defaultModel,
567
580
  };
568
581
  });
569
582
  },
570
583
 
571
- async listAccess(projectId: string, subject: AccessSubject | null): Promise<AiProjectAccess[] | null> {
584
+ async listAccess(projectId: string, subject: AiResourceContext | null): Promise<AiProjectAccess[] | null> {
572
585
  if (!(await requireProject(projectId, subject, "admin"))) return null;
573
586
  return listProjectAccess(projectId);
574
587
  },
575
588
 
576
589
  async grantAccess(
577
590
  projectId: string,
578
- subject: AccessSubject | null,
591
+ subject: AiResourceContext | null,
579
592
  input: { principal: Principal; permission: AiProjectPermission },
580
593
  ): Promise<AiProjectAccess | null> {
581
594
  return sql.begin(async (tx) => {
@@ -590,7 +603,7 @@ export const aiProjects = {
590
603
  async updateAccess(
591
604
  projectId: string,
592
605
  accessId: string,
593
- subject: AccessSubject | null,
606
+ subject: AiResourceContext | null,
594
607
  permission: AiProjectPermission,
595
608
  ): Promise<boolean> {
596
609
  return sql.begin(async (tx) => {
@@ -602,7 +615,7 @@ export const aiProjects = {
602
615
  });
603
616
  },
604
617
 
605
- async revokeAccess(projectId: string, accessId: string, subject: AccessSubject | null): Promise<boolean> {
618
+ async revokeAccess(projectId: string, accessId: string, subject: AiResourceContext | null): Promise<boolean> {
606
619
  return sql.begin(async (tx) => {
607
620
  const [project] = await tx<ProjectRow[]>`
608
621
  SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
@@ -726,7 +739,7 @@ export const aiProjects = {
726
739
  },
727
740
  },
728
741
 
729
- async listKnowledge(projectId: string, subject: AccessSubject | null, query?: string): Promise<AiProjectKnowledge[]> {
742
+ async listKnowledge(projectId: string, subject: AiResourceContext | null, query?: string): Promise<AiProjectKnowledge[]> {
730
743
  if (!(await requireProject(projectId, subject, "read"))) return [];
731
744
  const q = query?.trim() || null;
732
745
  const pattern = q ? `%${q.toLowerCase().replace(/[\\%_]/g, (char) => `\\${char}`)}%` : null;
@@ -741,7 +754,7 @@ export const aiProjects = {
741
754
  return rows.map(toKnowledge);
742
755
  },
743
756
 
744
- async getKnowledgeByShortId(projectId: string, shortId: string, subject: AccessSubject | null): Promise<AiProjectKnowledge | null> {
757
+ async getKnowledgeByShortId(projectId: string, shortId: string, subject: AiResourceContext | null): Promise<AiProjectKnowledge | null> {
745
758
  if (!(await requireProject(projectId, subject, "read"))) return null;
746
759
  const rows = await sql<KnowledgeRow[]>`
747
760
  SELECT * FROM ai.project_knowledge
@@ -753,11 +766,12 @@ export const aiProjects = {
753
766
 
754
767
  async createKnowledge(
755
768
  projectId: string,
756
- subject: AccessSubject | null,
769
+ subject: AiResourceContext | null,
757
770
  input: { title: string; content: string },
758
771
  ): Promise<AiProjectKnowledge | null> {
759
- if (!(await requireProject(projectId, subject, "write"))) return null;
760
772
  return sql.begin(async (tx) => {
773
+ const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
774
+ if (!project || !hasPermission(await permissionFor(project, subject, tx), "write")) return null;
761
775
  const rows = await withAiShortIdForDb(
762
776
  tx,
763
777
  "idx_ai_project_knowledge_short_id",
@@ -775,11 +789,12 @@ export const aiProjects = {
775
789
  async updateKnowledge(
776
790
  projectId: string,
777
791
  knowledgeId: string,
778
- subject: AccessSubject | null,
792
+ subject: AiResourceContext | null,
779
793
  input: { title?: string; content?: string },
780
794
  ): Promise<AiProjectKnowledge | null> {
781
- if (!(await requireProject(projectId, subject, "write"))) return null;
782
795
  return sql.begin(async (tx) => {
796
+ const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
797
+ if (!project || !hasPermission(await permissionFor(project, subject, tx), "write")) return null;
783
798
  const rows = await tx<KnowledgeRow[]>`
784
799
  UPDATE ai.project_knowledge
785
800
  SET title = COALESCE(${input.title?.trim() ?? null}, title), content = COALESCE(${input.content?.trim() ?? null}, content), updated_at = now()
@@ -791,9 +806,10 @@ export const aiProjects = {
791
806
  });
792
807
  },
793
808
 
794
- async deleteKnowledge(projectId: string, knowledgeId: string, subject: AccessSubject | null): Promise<boolean> {
795
- if (!(await requireProject(projectId, subject, "write"))) return false;
809
+ async deleteKnowledge(projectId: string, knowledgeId: string, subject: AiResourceContext | null): Promise<boolean> {
796
810
  return sql.begin(async (tx) => {
811
+ const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
812
+ if (!project || !hasPermission(await permissionFor(project, subject, tx), "write")) return false;
797
813
  const rows = await tx<{ id: string }[]>`
798
814
  DELETE FROM ai.project_knowledge WHERE short_id = ${knowledgeId} AND project_id = ${projectId}::uuid RETURNING id
799
815
  `;
@@ -802,7 +818,11 @@ export const aiProjects = {
802
818
  });
803
819
  },
804
820
 
805
- async listFiles(projectId: string, subject: AccessSubject | null, page?: { after?: string; limit: number }): Promise<AiProjectFile[]> {
821
+ async listFiles(
822
+ projectId: string,
823
+ subject: AiResourceContext | null,
824
+ page?: { after?: string; limit: number },
825
+ ): Promise<AiProjectFile[]> {
806
826
  if (!(await requireProject(projectId, subject, "read"))) return [];
807
827
  if (page && (!Number.isInteger(page.limit) || page.limit < 1 || page.limit > 1000)) throw new Error("Invalid file page limit");
808
828
  return (
@@ -814,7 +834,7 @@ export const aiProjects = {
814
834
 
815
835
  async writeFile(
816
836
  projectId: string,
817
- subject: AccessSubject | null,
837
+ subject: AiResourceContext | null,
818
838
  input: { path: string; mediaType: string; bytes: Uint8Array; expectedVersion?: string | null },
819
839
  ): Promise<AiProjectFile | null> {
820
840
  if (!(await requireProject(projectId, subject, "write"))) return null;
@@ -853,7 +873,7 @@ export const aiProjects = {
853
873
  async readFile(
854
874
  projectId: string,
855
875
  fileId: string,
856
- subject: AccessSubject | null,
876
+ subject: AiResourceContext | null,
857
877
  ): Promise<(AiProjectFile & { bytes: Uint8Array }) | null> {
858
878
  if (!(await requireProject(projectId, subject, "read"))) return null;
859
879
  const rows = await sql<(FileRow & { bytes: Uint8Array })[]>`
@@ -866,7 +886,7 @@ export const aiProjects = {
866
886
  async readFileByPath(
867
887
  projectId: string,
868
888
  path: string,
869
- subject: AccessSubject | null,
889
+ subject: AiResourceContext | null,
870
890
  ): Promise<(AiProjectFile & { bytes: Uint8Array }) | null> {
871
891
  if (!(await requireProject(projectId, subject, "read"))) return null;
872
892
  const normalized = normalizeProjectPath(path);
@@ -883,7 +903,7 @@ export const aiProjects = {
883
903
  return rows[0] ? { ...toFile(rows[0]), bytes: rows[0].bytes } : null;
884
904
  },
885
905
 
886
- async deleteFile(projectId: string, fileId: string, subject: AccessSubject | null): Promise<boolean> {
906
+ async deleteFile(projectId: string, fileId: string, subject: AiResourceContext | null): Promise<boolean> {
887
907
  if (!(await requireProject(projectId, subject, "write"))) return false;
888
908
  return sql.begin(async (tx) => {
889
909
  const [row] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id=${projectId}::uuid FOR UPDATE`;
@@ -897,7 +917,7 @@ export const aiProjects = {
897
917
  });
898
918
  },
899
919
 
900
- async listReferences(projectId: string, subject: AccessSubject | null): Promise<AiProjectReference[]> {
920
+ async listReferences(projectId: string, subject: AiResourceContext | null): Promise<AiProjectReference[]> {
901
921
  if (!(await requireProject(projectId, subject, "read"))) return [];
902
922
  return (
903
923
  await sql<
@@ -908,11 +928,12 @@ export const aiProjects = {
908
928
 
909
929
  async createReference(
910
930
  projectId: string,
911
- subject: AccessSubject | null,
931
+ subject: AiResourceContext | null,
912
932
  input: { ref: CloudResourceRef; label?: string },
913
933
  ): Promise<AiProjectReference | null> {
914
- if (!(await requireProject(projectId, subject, "write"))) return null;
915
934
  return sql.begin(async (tx) => {
935
+ const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
936
+ if (!project || !hasPermission(await permissionFor(project, subject, tx), "write")) return null;
916
937
  const rows = await withAiShortIdForDb(
917
938
  tx,
918
939
  "idx_ai_project_resource_refs_short_id",
@@ -927,9 +948,10 @@ export const aiProjects = {
927
948
  });
928
949
  },
929
950
 
930
- async deleteReference(projectId: string, referenceId: string, subject: AccessSubject | null): Promise<boolean> {
931
- if (!(await requireProject(projectId, subject, "write"))) return false;
951
+ async deleteReference(projectId: string, referenceId: string, subject: AiResourceContext | null): Promise<boolean> {
932
952
  return sql.begin(async (tx) => {
953
+ const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
954
+ if (!project || !hasPermission(await permissionFor(project, subject, tx), "write")) return false;
933
955
  const rows = await tx<{ id: string }[]>`
934
956
  DELETE FROM ai.project_resource_refs WHERE short_id = ${referenceId} AND project_id = ${projectId}::uuid RETURNING id
935
957
  `;
@@ -90,7 +90,7 @@ export type AiTurnFinishedStatus = "completed" | "failed" | "aborted";
90
90
  export type AiWireEvent =
91
91
  | (AiWireEventBase & {
92
92
  type: "turn_started";
93
- modelProfileId: string;
93
+ modelId: string;
94
94
  providerModel: string;
95
95
  /** Authoritative ordered baseline for this attempt. Optional for older senders. */
96
96
  blocks?: AiTurnBlock[];
@@ -116,7 +116,7 @@ export type AiTurnSnapshot = {
116
116
  /** Last wire seq reflected in `blocks` (attempt-scoped). */
117
117
  seq: number;
118
118
  blocks: AiTurnBlock[];
119
- modelProfileId: string | null;
119
+ modelId: string | null;
120
120
  createdAt: string;
121
121
  /** Time the turn already waited for answered user actions such as approvals. Absent from older servers. */
122
122
  actionWaitMs?: number;