@k2b/cloud 0.6.0-rc.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 (540) hide show
  1. package/README.md +98 -0
  2. package/package.json +128 -0
  3. package/public/logo.svg +1 -0
  4. package/scripts/app-favicon.test.ts +49 -0
  5. package/scripts/app-favicon.ts +51 -0
  6. package/scripts/build.ts +205 -0
  7. package/scripts/preload.ts +164 -0
  8. package/scripts/sync-recovery-smoke.ts +200 -0
  9. package/src/_internal/app-favicon.ts +2 -0
  10. package/src/_internal/app-presentation.ts +61 -0
  11. package/src/_internal/app-workspace-state.ts +24 -0
  12. package/src/_internal/bounded-json.ts +47 -0
  13. package/src/_internal/build-metadata.ts +9 -0
  14. package/src/_internal/capabilities.ts +1118 -0
  15. package/src/_internal/define-app.ts +801 -0
  16. package/src/_internal/heartbeat.ts +167 -0
  17. package/src/_internal/help-catalog.ts +241 -0
  18. package/src/_internal/help.ts +95 -0
  19. package/src/_internal/nats-connection.ts +34 -0
  20. package/src/_internal/page-responses.ts +40 -0
  21. package/src/_internal/process-sync.ts +117 -0
  22. package/src/_internal/readiness.ts +3 -0
  23. package/src/_internal/registry-validation.ts +136 -0
  24. package/src/_internal/registry-watch.ts +52 -0
  25. package/src/_internal/registry.ts +338 -0
  26. package/src/_internal/runtime-compatibility.ts +64 -0
  27. package/src/_internal/runtime-context.ts +84 -0
  28. package/src/_internal/runtime-watcher.ts +78 -0
  29. package/src/_internal/static-assets.ts +86 -0
  30. package/src/_internal/status-preserving-ssr.ts +49 -0
  31. package/src/access/PermissionEditor.tsx +486 -0
  32. package/src/access/ResourceApiKeys.tsx +271 -0
  33. package/src/access/messages.ts +121 -0
  34. package/src/access/ui.ts +16 -0
  35. package/src/account/Avatar.tsx +28 -0
  36. package/src/account/EntitySearch.tsx +363 -0
  37. package/src/account/avatar-upload-dialog.tsx +197 -0
  38. package/src/account/avatar-upload.ts +149 -0
  39. package/src/account/ui.ts +3 -0
  40. package/src/ai/admin.ts +47 -0
  41. package/src/ai/approval-routes.ts +130 -0
  42. package/src/ai/approvals.ts +139 -0
  43. package/src/ai/assistant-models.ts +61 -0
  44. package/src/ai/attachments.ts +40 -0
  45. package/src/ai/browser.ts +92 -0
  46. package/src/ai/capabilities.ts +714 -0
  47. package/src/ai/capability-execution.ts +151 -0
  48. package/src/ai/chat/blocks.render.test.tsx +1108 -0
  49. package/src/ai/chat/blocks.tsx +730 -0
  50. package/src/ai/chat/builtin-tools.tsx +428 -0
  51. package/src/ai/chat/capability-table.tsx +87 -0
  52. package/src/ai/chat/composer-adapter.ts +169 -0
  53. package/src/ai/chat/file-tools.tsx +81 -0
  54. package/src/ai/chat/message-actions.tsx +370 -0
  55. package/src/ai/chat/message-utils.ts +479 -0
  56. package/src/ai/chat/messages.ts +43 -0
  57. package/src/ai/chat/presentation.tsx +395 -0
  58. package/src/ai/chat/primitives.tsx +3 -0
  59. package/src/ai/chat/tool-disclosure.tsx +41 -0
  60. package/src/ai/chat/turn-navigator-utils.ts +28 -0
  61. package/src/ai/chat/turn-navigator.tsx +164 -0
  62. package/src/ai/chat/user-message.tsx +149 -0
  63. package/src/ai/chat/visual-tools.tsx +435 -0
  64. package/src/ai/chat/web-tools.tsx +202 -0
  65. package/src/ai/chat-task-contracts.ts +113 -0
  66. package/src/ai/chat-tasks.ts +952 -0
  67. package/src/ai/client/controller.ts +1211 -0
  68. package/src/ai/client/file-source.ts +95 -0
  69. package/src/ai/client/live-connection.ts +140 -0
  70. package/src/ai/client/projection.ts +178 -0
  71. package/src/ai/client/transport.ts +111 -0
  72. package/src/ai/compaction.ts +182 -0
  73. package/src/ai/credentials.ts +124 -0
  74. package/src/ai/default-tool-contracts.ts +108 -0
  75. package/src/ai/default-tools.ts +127 -0
  76. package/src/ai/enrich.ts +280 -0
  77. package/src/ai/executor.ts +1624 -0
  78. package/src/ai/fetch-file-tool.ts +313 -0
  79. package/src/ai/file-context.ts +92 -0
  80. package/src/ai/file-media-type.ts +28 -0
  81. package/src/ai/file-mount.ts +16 -0
  82. package/src/ai/file-tools.ts +566 -0
  83. package/src/ai/files-store.ts +390 -0
  84. package/src/ai/firecrawl-tools.ts +305 -0
  85. package/src/ai/grids-skill.ts +126 -0
  86. package/src/ai/http.ts +265 -0
  87. package/src/ai/index.ts +337 -0
  88. package/src/ai/kit-skill.ts +22 -0
  89. package/src/ai/limits.ts +5 -0
  90. package/src/ai/live-events.ts +289 -0
  91. package/src/ai/live-messages.ts +45 -0
  92. package/src/ai/live-outbox.ts +102 -0
  93. package/src/ai/live-routes.ts +412 -0
  94. package/src/ai/live.ts +2 -0
  95. package/src/ai/maintenance.ts +302 -0
  96. package/src/ai/markdown-pdf-tool.ts +95 -0
  97. package/src/ai/memories.ts +605 -0
  98. package/src/ai/memory-learning-runs.ts +222 -0
  99. package/src/ai/memory-learning.ts +619 -0
  100. package/src/ai/memory-tool.ts +120 -0
  101. package/src/ai/memory-workflow-evidence.ts +158 -0
  102. package/src/ai/migrate.ts +2082 -0
  103. package/src/ai/model-access-migrate.ts +30 -0
  104. package/src/ai/model-access.ts +191 -0
  105. package/src/ai/personal-agent.ts +6 -0
  106. package/src/ai/prefs.ts +72 -0
  107. package/src/ai/project-tool.ts +94 -0
  108. package/src/ai/projects-routes.ts +277 -0
  109. package/src/ai/projects.ts +902 -0
  110. package/src/ai/protocol.ts +279 -0
  111. package/src/ai/provider.ts +73 -0
  112. package/src/ai/public-projection.ts +25 -0
  113. package/src/ai/resource-markers.ts +31 -0
  114. package/src/ai/resource-refs.ts +70 -0
  115. package/src/ai/routes.ts +1131 -0
  116. package/src/ai/runtime-tools.ts +39 -0
  117. package/src/ai/runtime.ts +544 -0
  118. package/src/ai/settings.ts +529 -0
  119. package/src/ai/short-id.ts +45 -0
  120. package/src/ai/skill-catalog.ts +73 -0
  121. package/src/ai/skill-format.ts +145 -0
  122. package/src/ai/skill-seeds.ts +430 -0
  123. package/src/ai/skill-tool.ts +81 -0
  124. package/src/ai/skills-routes.ts +143 -0
  125. package/src/ai/skills.ts +934 -0
  126. package/src/ai/solid.ts +27 -0
  127. package/src/ai/store.ts +3113 -0
  128. package/src/ai/stream.ts +308 -0
  129. package/src/ai/structured-runs.ts +66 -0
  130. package/src/ai/structured.ts +186 -0
  131. package/src/ai/system-prompt.ts +170 -0
  132. package/src/ai/task-prompt.ts +22 -0
  133. package/src/ai/timeline.ts +148 -0
  134. package/src/ai/tool-audit.ts +177 -0
  135. package/src/ai/tool-result-budget.ts +16 -0
  136. package/src/ai/tools.ts +155 -0
  137. package/src/ai/types.ts +967 -0
  138. package/src/ai/ui.tsx +39 -0
  139. package/src/ai/usage-migrate.ts +81 -0
  140. package/src/ai/usage.ts +315 -0
  141. package/src/ai/validate.ts +54 -0
  142. package/src/ai/vision-tool.ts +79 -0
  143. package/src/api/accounts-entities.ts +174 -0
  144. package/src/api/admin-ai-projects.ts +74 -0
  145. package/src/api/admin-ai-skills.ts +76 -0
  146. package/src/api/admin-ai-usage.ts +42 -0
  147. package/src/api/admin-core-settings.ts +508 -0
  148. package/src/api/admin-identity.ts +148 -0
  149. package/src/api/admin-lifecycle.ts +206 -0
  150. package/src/api/admin-linux-identities.ts +66 -0
  151. package/src/api/announcements.ts +131 -0
  152. package/src/api/app-approval.ts +129 -0
  153. package/src/api/apps.ts +51 -0
  154. package/src/api/auth/schemas.ts +54 -0
  155. package/src/api/auth.ts +370 -0
  156. package/src/api/capabilities.ts +658 -0
  157. package/src/api/help-messages.ts +19 -0
  158. package/src/api/help.ts +78 -0
  159. package/src/api/index.ts +108 -0
  160. package/src/api/mcp.ts +744 -0
  161. package/src/api/me-rail.ts +45 -0
  162. package/src/api/me.ts +753 -0
  163. package/src/api/search/schemas.ts +79 -0
  164. package/src/api/search.bench.ts +185 -0
  165. package/src/api/search.ts +407 -0
  166. package/src/api/sync-ops.ts +79 -0
  167. package/src/api/widgets.ts +143 -0
  168. package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
  169. package/src/browser/CloudResourceSearch.tsx +791 -0
  170. package/src/browser/app-approval-vault.ts +305 -0
  171. package/src/browser/app-approval.ts +234 -0
  172. package/src/browser/live-websocket.ts +268 -0
  173. package/src/browser/locale-preference.ts +25 -0
  174. package/src/browser/notification-target.ts +27 -0
  175. package/src/browser/notification-timeout.ts +14 -0
  176. package/src/browser/notifications.ts +181 -0
  177. package/src/browser/resource-clipboard.ts +70 -0
  178. package/src/browser/resource-picker.tsx +39 -0
  179. package/src/browser/resource-search-messages.ts +55 -0
  180. package/src/browser/resource-search.ts +27 -0
  181. package/src/capabilities/client.ts +149 -0
  182. package/src/capabilities/response.ts +39 -0
  183. package/src/capabilities/server.ts +215 -0
  184. package/src/capabilities/signals.ts +4 -0
  185. package/src/capabilities/testing.ts +12 -0
  186. package/src/capabilities/types.ts +32 -0
  187. package/src/cli/access.ts +499 -0
  188. package/src/cli/account.ts +382 -0
  189. package/src/cli/admin/account-administration.ts +45 -0
  190. package/src/cli/admin/account-categories.ts +49 -0
  191. package/src/cli/admin/ai-usage.ts +118 -0
  192. package/src/cli/admin/app-sign-in.ts +49 -0
  193. package/src/cli/admin/data.ts +343 -0
  194. package/src/cli/admin/documentation.ts +34 -0
  195. package/src/cli/admin/gateway.ts +155 -0
  196. package/src/cli/admin/index.ts +61 -0
  197. package/src/cli/admin/instance.ts +250 -0
  198. package/src/cli/admin/jobs.ts +223 -0
  199. package/src/cli/admin/legal.ts +122 -0
  200. package/src/cli/admin/linux.ts +53 -0
  201. package/src/cli/admin/logs.ts +324 -0
  202. package/src/cli/admin/metrics.ts +142 -0
  203. package/src/cli/admin/nats.ts +167 -0
  204. package/src/cli/admin/notifications.ts +516 -0
  205. package/src/cli/admin/shared.ts +113 -0
  206. package/src/cli/admin/sync.ts +188 -0
  207. package/src/cli/admin/telemetry.ts +314 -0
  208. package/src/cli/admin/webhooks.ts +222 -0
  209. package/src/cli/admin/workflows.ts +367 -0
  210. package/src/cli/apps.ts +40 -0
  211. package/src/cli/capabilities.ts +239 -0
  212. package/src/cli/commands.ts +604 -0
  213. package/src/cli/index.ts +61 -0
  214. package/src/cli/locale.ts +31 -0
  215. package/src/cli/output.ts +58 -0
  216. package/src/clients/core.ts +20 -0
  217. package/src/config/env.ts +54 -0
  218. package/src/config/index.ts +6 -0
  219. package/src/contracts/account-categories.ts +62 -0
  220. package/src/contracts/announcements.ts +121 -0
  221. package/src/contracts/app-approval.ts +197 -0
  222. package/src/contracts/app.ts +162 -0
  223. package/src/contracts/capabilities.ts +484 -0
  224. package/src/contracts/index.ts +14 -0
  225. package/src/contracts/notification-live.ts +66 -0
  226. package/src/contracts/notification-types.ts +235 -0
  227. package/src/contracts/posix.ts +69 -0
  228. package/src/contracts/profile.ts +85 -0
  229. package/src/contracts/rail-preferences.ts +53 -0
  230. package/src/contracts/registry.ts +110 -0
  231. package/src/contracts/resource-clipboard.ts +33 -0
  232. package/src/contracts/settings-types.ts +98 -0
  233. package/src/contracts/shared.ts +408 -0
  234. package/src/contracts/user-notifications.ts +101 -0
  235. package/src/contracts/widgets.ts +183 -0
  236. package/src/desktop/index.ts +704 -0
  237. package/src/desktop/solid.tsx +938 -0
  238. package/src/index.ts +58 -0
  239. package/src/server/actor.ts +41 -0
  240. package/src/server/api/index.ts +2 -0
  241. package/src/server/api/respond.ts +96 -0
  242. package/src/server/api-client.ts +54 -0
  243. package/src/server/app-context.ts +39 -0
  244. package/src/server/help.ts +187 -0
  245. package/src/server/index.ts +89 -0
  246. package/src/server/locale.ts +55 -0
  247. package/src/server/middleware/auth.ts +386 -0
  248. package/src/server/middleware/index.ts +28 -0
  249. package/src/server/middleware/invocation.ts +55 -0
  250. package/src/server/middleware/middleware.ts +65 -0
  251. package/src/server/middleware/openapi.ts +126 -0
  252. package/src/server/middleware/rate-limit.ts +125 -0
  253. package/src/server/middleware/request-logger.ts +41 -0
  254. package/src/server/middleware/route-template.ts +46 -0
  255. package/src/server/middleware/runtime.ts +22 -0
  256. package/src/server/middleware/settings.ts +55 -0
  257. package/src/server/middleware/validator.ts +49 -0
  258. package/src/server/middleware/workload.ts +22 -0
  259. package/src/server/ratelimit.ts +151 -0
  260. package/src/server/request-cookies.ts +18 -0
  261. package/src/server/services/access.ts +644 -0
  262. package/src/server/services/freeipa/client.ts +137 -0
  263. package/src/server/services/freeipa/index.ts +9 -0
  264. package/src/server/services/freeipa/session.ts +123 -0
  265. package/src/server/services/freeipa/test-certificates.ts +44 -0
  266. package/src/server/services/freeipa/tls.ts +44 -0
  267. package/src/server/services/freeipa/transport.ts +116 -0
  268. package/src/server/services/freeipa/util.ts +59 -0
  269. package/src/server/services/geo.ts +198 -0
  270. package/src/server/services/index.ts +48 -0
  271. package/src/server/services/pagination.ts +22 -0
  272. package/src/server/services/services.ts +13 -0
  273. package/src/server/time.ts +33 -0
  274. package/src/services/account-category-policy.ts +51 -0
  275. package/src/services/account-lifecycle/audit.ts +41 -0
  276. package/src/services/account-lifecycle/index.ts +940 -0
  277. package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
  278. package/src/services/account-lifecycle/notification-sender.ts +13 -0
  279. package/src/services/account-lifecycle/scheduler.ts +506 -0
  280. package/src/services/account-model.ts +22 -0
  281. package/src/services/accounts/app.ts +1671 -0
  282. package/src/services/accounts/authz.ts +44 -0
  283. package/src/services/accounts/avatar.ts +120 -0
  284. package/src/services/accounts/base-group.ts +11 -0
  285. package/src/services/accounts/base-user.ts +46 -0
  286. package/src/services/accounts/duplicate-emails.ts +82 -0
  287. package/src/services/accounts/entities.ts +622 -0
  288. package/src/services/accounts/group-sql.ts +106 -0
  289. package/src/services/accounts/groups.ts +272 -0
  290. package/src/services/accounts/index.ts +14 -0
  291. package/src/services/accounts/ipa-data.ts +66 -0
  292. package/src/services/accounts/lifecycle.ts +2 -0
  293. package/src/services/accounts/local-groups.ts +499 -0
  294. package/src/services/accounts/model.ts +129 -0
  295. package/src/services/accounts/notification-sender.ts +31 -0
  296. package/src/services/accounts/posix.ts +386 -0
  297. package/src/services/accounts/request-policy.ts +11 -0
  298. package/src/services/accounts/switching.ts +126 -0
  299. package/src/services/accounts/users.ts +745 -0
  300. package/src/services/announcements/index.ts +224 -0
  301. package/src/services/app-approval.ts +498 -0
  302. package/src/services/audit/index.ts +438 -0
  303. package/src/services/auth-flows/index.ts +15 -0
  304. package/src/services/auth-flows/ipa.ts +197 -0
  305. package/src/services/auth-flows/magic-link.ts +185 -0
  306. package/src/services/auth-flows/notification-sender.ts +10 -0
  307. package/src/services/auth-flows/password-reset.ts +268 -0
  308. package/src/services/auth-flows/proxy-return.ts +49 -0
  309. package/src/services/document-extraction.ts +148 -0
  310. package/src/services/freeipa-config.ts +179 -0
  311. package/src/services/gateway.ts +186 -0
  312. package/src/services/identity/constants.ts +18 -0
  313. package/src/services/identity/index.ts +52 -0
  314. package/src/services/identity/invocation-actor.ts +120 -0
  315. package/src/services/identity/invocation-authority.ts +53 -0
  316. package/src/services/identity/invocation-operations.ts +15 -0
  317. package/src/services/identity/invocation-token.ts +276 -0
  318. package/src/services/identity/key-config.ts +41 -0
  319. package/src/services/identity/key-ring.ts +530 -0
  320. package/src/services/identity/metrics.ts +29 -0
  321. package/src/services/identity/routes.ts +18 -0
  322. package/src/services/identity/runtime-config.ts +88 -0
  323. package/src/services/identity/session-token.ts +152 -0
  324. package/src/services/identity/signer-refresh.ts +44 -0
  325. package/src/services/identity/workload-auth.ts +64 -0
  326. package/src/services/index.ts +187 -0
  327. package/src/services/ipa/auth.ts +216 -0
  328. package/src/services/ipa/connection.ts +57 -0
  329. package/src/services/ipa/effective-groups.ts +70 -0
  330. package/src/services/ipa/groups.ts +722 -0
  331. package/src/services/ipa/guard.ts +17 -0
  332. package/src/services/ipa/index.ts +17 -0
  333. package/src/services/ipa/posix.ts +19 -0
  334. package/src/services/ipa/profile.ts +132 -0
  335. package/src/services/ipa/runtime.ts +28 -0
  336. package/src/services/ipa/search.ts +154 -0
  337. package/src/services/ipa/service-account.ts +15 -0
  338. package/src/services/ipa/sync-planning.ts +100 -0
  339. package/src/services/ipa/sync.ts +862 -0
  340. package/src/services/ipa/users.ts +806 -0
  341. package/src/services/legal-consent.ts +76 -0
  342. package/src/services/logging/index.ts +477 -0
  343. package/src/services/logging/redaction.ts +20 -0
  344. package/src/services/logging/trace.ts +1231 -0
  345. package/src/services/mandates/index.ts +855 -0
  346. package/src/services/mandates/policy.ts +85 -0
  347. package/src/services/network-security.ts +31 -0
  348. package/src/services/notifications/batches.ts +722 -0
  349. package/src/services/notifications/browser.ts +252 -0
  350. package/src/services/notifications/catalog.ts +113 -0
  351. package/src/services/notifications/channels.ts +104 -0
  352. package/src/services/notifications/dispatcher.ts +139 -0
  353. package/src/services/notifications/email.ts +118 -0
  354. package/src/services/notifications/index.ts +633 -0
  355. package/src/services/notifications/live.ts +50 -0
  356. package/src/services/notifications/observability.ts +407 -0
  357. package/src/services/notifications/platform.ts +421 -0
  358. package/src/services/notifications/runtime.ts +120 -0
  359. package/src/services/notifications/user.ts +248 -0
  360. package/src/services/notifications/web-push-transport.ts +135 -0
  361. package/src/services/oauth-tokens.ts +209 -0
  362. package/src/services/outbox.ts +187 -0
  363. package/src/services/pdf/gotenberg.ts +290 -0
  364. package/src/services/pdf/index.ts +43 -0
  365. package/src/services/pdf/markdown.ts +209 -0
  366. package/src/services/pdf/template-preview.ts +122 -0
  367. package/src/services/postgres.ts +76 -0
  368. package/src/services/providers/index.ts +27 -0
  369. package/src/services/providers/local/auth.ts +51 -0
  370. package/src/services/providers/local/index.ts +4 -0
  371. package/src/services/providers/local/users.ts +263 -0
  372. package/src/services/rail-preferences.ts +27 -0
  373. package/src/services/runtime-lifecycle.ts +191 -0
  374. package/src/services/secrets.ts +10 -0
  375. package/src/services/service-account-credentials.ts +891 -0
  376. package/src/services/service-accounts.ts +184 -0
  377. package/src/services/session/index.ts +236 -0
  378. package/src/services/session/test-fixture.ts +18 -0
  379. package/src/services/session/user.ts +144 -0
  380. package/src/services/settings/api.ts +61 -0
  381. package/src/services/settings/app.ts +86 -0
  382. package/src/services/settings/core-settings.ts +796 -0
  383. package/src/services/settings/crypto.ts +69 -0
  384. package/src/services/settings/defaults.ts +374 -0
  385. package/src/services/settings/index.ts +215 -0
  386. package/src/services/settings/namespace.ts +9 -0
  387. package/src/services/settings/snapshot.ts +49 -0
  388. package/src/services/settings/store.ts +241 -0
  389. package/src/services/settings/templates.ts +4 -0
  390. package/src/services/sync-ops.ts +403 -0
  391. package/src/services/topic-cursor.ts +22 -0
  392. package/src/services/weather/forecast.ts +323 -0
  393. package/src/services/weather/geo.ts +119 -0
  394. package/src/services/weather/index.ts +67 -0
  395. package/src/services/weather/location.ts +24 -0
  396. package/src/services/weather/locations.ts +133 -0
  397. package/src/services/weather/migrate.ts +45 -0
  398. package/src/services/weather/settings.ts +66 -0
  399. package/src/services/weather/short-id.ts +57 -0
  400. package/src/services/weather/types.ts +61 -0
  401. package/src/services/weather/ui.ts +50 -0
  402. package/src/services/webauthn.ts +385 -0
  403. package/src/shared/account-action-notice.ts +70 -0
  404. package/src/shared/account-display.ts +15 -0
  405. package/src/shared/account-session.ts +15 -0
  406. package/src/shared/ai-platform-prompt.ts +134 -0
  407. package/src/shared/ai-usage.ts +49 -0
  408. package/src/shared/app-presentation.ts +48 -0
  409. package/src/shared/app-url.ts +12 -0
  410. package/src/shared/branding.ts +14 -0
  411. package/src/shared/capability-messages.ts +70 -0
  412. package/src/shared/email-html.ts +54 -0
  413. package/src/shared/format.ts +91 -0
  414. package/src/shared/help.ts +68 -0
  415. package/src/shared/icons.ts +400 -0
  416. package/src/shared/index.ts +54 -0
  417. package/src/shared/locale.ts +48 -0
  418. package/src/shared/login-method.ts +44 -0
  419. package/src/shared/markdown/client.ts +130 -0
  420. package/src/shared/markdown/extensions/code.ts +105 -0
  421. package/src/shared/markdown/extensions/guided-help.ts +100 -0
  422. package/src/shared/markdown/extensions/images.ts +79 -0
  423. package/src/shared/markdown/extensions/info-blocks.ts +97 -0
  424. package/src/shared/markdown/extensions/katex.ts +120 -0
  425. package/src/shared/markdown/extensions/links.ts +43 -0
  426. package/src/shared/markdown/extensions/mark.ts +48 -0
  427. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  428. package/src/shared/markdown/extensions/tables.ts +109 -0
  429. package/src/shared/markdown/extensions/task-list.ts +53 -0
  430. package/src/shared/markdown/formula.ts +1187 -0
  431. package/src/shared/markdown/index.ts +193 -0
  432. package/src/shared/markdown/shared.ts +36 -0
  433. package/src/shared/mock-cover.ts +130 -0
  434. package/src/shared/network-address.ts +106 -0
  435. package/src/shared/redirect.ts +56 -0
  436. package/src/shared/template-rendering.ts +140 -0
  437. package/src/shared/theme.ts +68 -0
  438. package/src/shared/time.ts +13 -0
  439. package/src/ssr/AdminLayout.tsx +53 -0
  440. package/src/ssr/AdminSidebar.tsx +50 -0
  441. package/src/ssr/AppLaunchpad.island.tsx +236 -0
  442. package/src/ssr/AppWorkspaceController.island.tsx +21 -0
  443. package/src/ssr/BrowserNotifications.island.tsx +135 -0
  444. package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
  445. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
  446. package/src/ssr/GlobalSearchDialog.tsx +35 -0
  447. package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
  448. package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
  449. package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
  450. package/src/ssr/Layout.render.test.tsx +148 -0
  451. package/src/ssr/Layout.tsx +292 -0
  452. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  453. package/src/ssr/LayoutFooter.tsx +22 -0
  454. package/src/ssr/LayoutHeader.tsx +81 -0
  455. package/src/ssr/LayoutHelp.tsx +869 -0
  456. package/src/ssr/LayoutPreferences.island.tsx +36 -0
  457. package/src/ssr/LayoutRail.tsx +58 -0
  458. package/src/ssr/MinimalLayout.tsx +46 -0
  459. package/src/ssr/PageError.tsx +29 -0
  460. package/src/ssr/ProfilePreferences.island.tsx +92 -0
  461. package/src/ssr/RailApps.island.tsx +61 -0
  462. package/src/ssr/RailEditor.tsx +293 -0
  463. package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
  464. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  465. package/src/ssr/admin-active-link.ts +47 -0
  466. package/src/ssr/admin-navigation.ts +78 -0
  467. package/src/ssr/app-appearance.ts +23 -0
  468. package/src/ssr/app-navigation.ts +41 -0
  469. package/src/ssr/help-messages.ts +104 -0
  470. package/src/ssr/index.ts +14 -0
  471. package/src/ssr/islands/SearchBar.island.tsx +80 -0
  472. package/src/ssr/islands/index.ts +16 -0
  473. package/src/ssr/layout-context.ts +17 -0
  474. package/src/ssr/layout-help-markdown.ts +9 -0
  475. package/src/ssr/layout-help-navigation.ts +24 -0
  476. package/src/ssr/layout-help-url.ts +5 -0
  477. package/src/ssr/layout-runtime.ts +22 -0
  478. package/src/ssr/page-error-messages.ts +25 -0
  479. package/src/ssr/platform-messages.ts +135 -0
  480. package/src/ssr/preference-controller.ts +44 -0
  481. package/src/ssr/profile-preferences-messages.ts +29 -0
  482. package/src/ssr/rail-context.ts +21 -0
  483. package/src/ssr/rail-messages.ts +66 -0
  484. package/src/ssr/rail-navigation.ts +60 -0
  485. package/src/ssr/runtime.ts +31 -0
  486. package/src/ssr/url-filter.ts +127 -0
  487. package/src/styles/base-popover.css +36 -0
  488. package/src/styles/effects.css +851 -0
  489. package/src/styles/global.css +268 -0
  490. package/src/styles/input.css +58 -0
  491. package/src/styles/tokens.css +230 -0
  492. package/src/styles/utilities-buttons.css +405 -0
  493. package/src/styles/utilities-data.css +33 -0
  494. package/src/styles/utilities-feedback.css +232 -0
  495. package/src/styles/utilities-layout.css +603 -0
  496. package/src/styles/utilities-markdown-editor.css +525 -0
  497. package/src/styles/utilities-markdown-table.css +234 -0
  498. package/src/styles/utilities-navigation.css +1054 -0
  499. package/src/types/ambient.d.ts +24 -0
  500. package/src/workflows/ai/index.ts +17 -0
  501. package/src/workflows/ai/runtime.ts +286 -0
  502. package/src/workflows/ai/store.ts +314 -0
  503. package/src/workflows/ai/types.ts +111 -0
  504. package/src/workflows/ai-actions.ts +269 -0
  505. package/src/workflows/authoring.ts +88 -0
  506. package/src/workflows/builtins.ts +193 -0
  507. package/src/workflows/contracts.ts +280 -0
  508. package/src/workflows/definition.ts +334 -0
  509. package/src/workflows/editor.ts +113 -0
  510. package/src/workflows/index.ts +71 -0
  511. package/src/workflows/language/binder.ts +45 -0
  512. package/src/workflows/language/canonical.ts +29 -0
  513. package/src/workflows/language/compiler.ts +396 -0
  514. package/src/workflows/language/expressions.ts +34 -0
  515. package/src/workflows/language/index.ts +20 -0
  516. package/src/workflows/language/references.ts +55 -0
  517. package/src/workflows/language/schema.ts +134 -0
  518. package/src/workflows/language/strict-yaml.ts +131 -0
  519. package/src/workflows/manifest.ts +24 -0
  520. package/src/workflows/module.ts +12 -0
  521. package/src/workflows/runtime/coordinator.ts +115 -0
  522. package/src/workflows/runtime/executor.ts +1003 -0
  523. package/src/workflows/runtime/index.ts +60 -0
  524. package/src/workflows/runtime/instant.ts +8 -0
  525. package/src/workflows/runtime/ports.ts +213 -0
  526. package/src/workflows/runtime/schedule.ts +203 -0
  527. package/src/workflows/runtime/trigger.ts +37 -0
  528. package/src/workflows/runtime/wake.ts +13 -0
  529. package/src/workflows/store/actions.ts +438 -0
  530. package/src/workflows/store/budget.ts +145 -0
  531. package/src/workflows/store/definitions.ts +345 -0
  532. package/src/workflows/store/events.ts +383 -0
  533. package/src/workflows/store/index.ts +107 -0
  534. package/src/workflows/store/observability.ts +594 -0
  535. package/src/workflows/store/runs.ts +975 -0
  536. package/src/workflows/store/transaction.ts +23 -0
  537. package/src/workflows/store/worker.ts +439 -0
  538. package/src/workflows/testing/conformance.ts +72 -0
  539. package/src/workflows/testing/index.ts +11 -0
  540. package/src/workflows/testing/process-fixtures.ts +275 -0
@@ -0,0 +1,902 @@
1
+ import { type SQL, type SQLQuery, sql } from "bun";
2
+ import type { CloudResourceRef } from "../contracts/capabilities";
3
+ import type { AccessSubject } from "../server";
4
+ import {
5
+ buildAccessPrincipalCondition,
6
+ createAccess,
7
+ deleteAccess,
8
+ hasPermission,
9
+ type PermissionLevel,
10
+ type Principal,
11
+ } from "../server/services/access";
12
+ import { toPgUuidArray } from "../services/postgres";
13
+ import { mountAiProjectFilePath } from "./file-mount";
14
+ import { withAiShortIdForDb } from "./short-id";
15
+ import type { AiProjectPromptSnapshot } from "./types";
16
+
17
+ export const AI_PROJECT_NAME_MAX_CHARS = 120;
18
+ export const AI_PROJECT_DESCRIPTION_MAX_CHARS = 500;
19
+ export const AI_PROJECT_INSTRUCTIONS_MAX_CHARS = 16_000;
20
+ export const AI_PROJECT_KNOWLEDGE_MAX_CHARS = 100_000;
21
+ export const AI_PROJECT_FILE_MAX_BYTES = 10 * 1024 * 1024;
22
+ const AI_PROJECT_PROMPT_MANIFEST_MAX_CHARS = 20_000;
23
+
24
+ export type AiProjectPermission = Exclude<PermissionLevel, "none">;
25
+
26
+ export class AiProjectLastAdminError extends Error {
27
+ constructor() {
28
+ super("A Project must keep at least one admin access entry.");
29
+ this.name = "AiProjectLastAdminError";
30
+ }
31
+ }
32
+
33
+ export type AiProject = {
34
+ id: string;
35
+ shortId: string;
36
+ name: string;
37
+ description: string;
38
+ icon: string;
39
+ instructions: string;
40
+ defaultModelProfileId: string | null;
41
+ permission: AiProjectPermission;
42
+ revision: number;
43
+ createdAt: string;
44
+ updatedAt: string;
45
+ };
46
+
47
+ export type AiProjectKnowledge = {
48
+ id: string;
49
+ shortId: string;
50
+ projectId: string;
51
+ title: string;
52
+ content: string;
53
+ createdAt: string;
54
+ updatedAt: string;
55
+ };
56
+
57
+ export type AiProjectFile = {
58
+ id: string;
59
+ shortId: string;
60
+ projectId: string;
61
+ path: string;
62
+ mediaType: string;
63
+ size: number;
64
+ updatedAt: string;
65
+ };
66
+
67
+ export type AiProjectReference = {
68
+ id: string;
69
+ shortId: string;
70
+ projectId: string;
71
+ ref: CloudResourceRef;
72
+ label: string;
73
+ createdAt: string;
74
+ };
75
+
76
+ export type AiProjectAccess = {
77
+ id: string;
78
+ shortId: string;
79
+ principal: Principal;
80
+ permission: AiProjectPermission;
81
+ displayName?: string;
82
+ createdAt: string;
83
+ };
84
+
85
+ export type AiProjectAdminListItem = {
86
+ id: string;
87
+ shortId: string;
88
+ name: string;
89
+ description: string;
90
+ icon: string;
91
+ accessCount: number;
92
+ adminCount: number;
93
+ createdAt: string;
94
+ updatedAt: string;
95
+ };
96
+
97
+ export type AiProjectAdminSummary = {
98
+ total: number;
99
+ unmanaged: number;
100
+ totalAccess: number;
101
+ };
102
+
103
+ type ProjectRow = {
104
+ id: string;
105
+ short_id: string;
106
+ name: string;
107
+ description: string;
108
+ icon: string;
109
+ instructions: string;
110
+ default_model_profile_id: string | null;
111
+ revision: number;
112
+ created_at: Date | string;
113
+ updated_at: Date | string;
114
+ };
115
+
116
+ type KnowledgeRow = {
117
+ id: string;
118
+ short_id: string;
119
+ project_id: string;
120
+ title: string;
121
+ content: string;
122
+ created_at: Date | string;
123
+ updated_at: Date | string;
124
+ };
125
+
126
+ type FileRow = {
127
+ id: string;
128
+ short_id: string;
129
+ project_id: string;
130
+ path: string;
131
+ media_type: string;
132
+ size: number;
133
+ updated_at: Date | string;
134
+ };
135
+
136
+ type ReferenceRow = {
137
+ id: string;
138
+ short_id: string;
139
+ project_id: string;
140
+ resource_type: string;
141
+ resource_id: string;
142
+ label: string;
143
+ created_at: Date | string;
144
+ };
145
+
146
+ type ProjectAccessRow = {
147
+ short_id: string;
148
+ user_id: string | null;
149
+ group_id: string | null;
150
+ service_account_id: string | null;
151
+ authenticated_only: boolean;
152
+ permission: AiProjectPermission;
153
+ created_at: Date | string;
154
+ display_name: string | null;
155
+ };
156
+
157
+ type AdminProjectRow = ProjectRow & {
158
+ access_count: number;
159
+ admin_count: number;
160
+ };
161
+
162
+ const iso = (value: Date | string): string => (value instanceof Date ? value.toISOString() : new Date(value).toISOString());
163
+ const actorUserId = (subject: AccessSubject | null): string | null => (subject?.type === "user" ? subject.userId : null);
164
+
165
+ const principalForSubject = (subject: AccessSubject): Principal =>
166
+ subject.type === "user"
167
+ ? { type: "user", userId: subject.userId }
168
+ : { type: "service_account", serviceAccountId: subject.serviceAccountId };
169
+
170
+ const accessMatch = (subject: AccessSubject | null): SQLQuery =>
171
+ buildAccessPrincipalCondition({
172
+ subject,
173
+ columns: {
174
+ userId: sql`access.user_id`,
175
+ groupId: sql`access.group_id`,
176
+ serviceAccountId: sql`access.service_account_id`,
177
+ authenticatedOnly: sql`access.authenticated_only`,
178
+ },
179
+ });
180
+
181
+ const permissionFor = async (row: ProjectRow, subject: AccessSubject | null, db: SQL = sql): Promise<AiProjectPermission | "none"> => {
182
+ const match = accessMatch(subject);
183
+ const rows = await db<{ permission: AiProjectPermission }[]>`
184
+ SELECT access.permission
185
+ FROM ai.project_access project_access
186
+ JOIN auth.access access ON access.id = project_access.access_id
187
+ WHERE project_access.project_id = ${row.id}::uuid AND ${match}
188
+ ORDER BY CASE access.permission WHEN 'admin' THEN 3 WHEN 'write' THEN 2 WHEN 'read' THEN 1 ELSE 0 END DESC
189
+ LIMIT 1
190
+ `;
191
+ return rows[0]?.permission ?? "none";
192
+ };
193
+
194
+ const toProject = async (row: ProjectRow, subject: AccessSubject | null, db: SQL = sql): Promise<AiProject | null> => {
195
+ const permission = await permissionFor(row, subject, db);
196
+ if (permission === "none") return null;
197
+ return {
198
+ id: row.id,
199
+ shortId: row.short_id,
200
+ name: row.name,
201
+ description: row.description,
202
+ icon: row.icon,
203
+ instructions: row.instructions,
204
+ defaultModelProfileId: row.default_model_profile_id,
205
+ permission,
206
+ revision: Number(row.revision),
207
+ createdAt: iso(row.created_at),
208
+ updatedAt: iso(row.updated_at),
209
+ };
210
+ };
211
+
212
+ const toKnowledge = (row: KnowledgeRow): AiProjectKnowledge => ({
213
+ id: row.id,
214
+ shortId: row.short_id,
215
+ projectId: row.project_id,
216
+ title: row.title,
217
+ content: row.content,
218
+ createdAt: iso(row.created_at),
219
+ updatedAt: iso(row.updated_at),
220
+ });
221
+
222
+ const toFile = (row: FileRow): AiProjectFile => ({
223
+ id: row.id,
224
+ shortId: row.short_id,
225
+ projectId: row.project_id,
226
+ path: row.path,
227
+ mediaType: row.media_type,
228
+ size: Number(row.size),
229
+ updatedAt: iso(row.updated_at),
230
+ });
231
+
232
+ const toReference = (row: ReferenceRow): AiProjectReference => ({
233
+ id: row.id,
234
+ shortId: row.short_id,
235
+ projectId: row.project_id,
236
+ ref: { type: row.resource_type, id: row.resource_id },
237
+ label: row.label,
238
+ createdAt: iso(row.created_at),
239
+ });
240
+
241
+ const toProjectAccess = (row: ProjectAccessRow): AiProjectAccess => ({
242
+ id: row.short_id,
243
+ shortId: row.short_id,
244
+ principal: row.user_id
245
+ ? { type: "user", userId: row.user_id }
246
+ : row.group_id
247
+ ? { type: "group", groupId: row.group_id }
248
+ : row.service_account_id
249
+ ? { type: "service_account", serviceAccountId: row.service_account_id }
250
+ : row.authenticated_only
251
+ ? { type: "authenticated" }
252
+ : { type: "public" },
253
+ permission: row.permission,
254
+ displayName: row.display_name ?? undefined,
255
+ createdAt: iso(row.created_at),
256
+ });
257
+
258
+ const toAdminProject = (row: AdminProjectRow): AiProjectAdminListItem => ({
259
+ id: row.id,
260
+ shortId: row.short_id,
261
+ name: row.name,
262
+ description: row.description,
263
+ icon: row.icon,
264
+ accessCount: Number(row.access_count),
265
+ adminCount: Number(row.admin_count),
266
+ createdAt: iso(row.created_at),
267
+ updatedAt: iso(row.updated_at),
268
+ });
269
+
270
+ const getRow = async (projectId: string): Promise<ProjectRow | null> => {
271
+ const rows = await sql<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid`;
272
+ return rows[0] ?? null;
273
+ };
274
+
275
+ const getRowByShortId = async (shortId: string): Promise<ProjectRow | null> => {
276
+ const rows = await sql<ProjectRow[]>`SELECT * FROM ai.projects WHERE short_id = ${shortId}`;
277
+ return rows[0] ?? null;
278
+ };
279
+
280
+ const listProjectAccess = async (projectId: string, db: SQL = sql): Promise<AiProjectAccess[]> => {
281
+ const rows = await db<ProjectAccessRow[]>`
282
+ SELECT project_access.short_id, access.user_id, access.group_id, access.service_account_id, access.authenticated_only,
283
+ access.permission, access.created_at,
284
+ COALESCE(users.display_name, groups.name, service_accounts.name,
285
+ CASE WHEN access.authenticated_only THEN 'All authenticated users' ELSE 'Public' END) AS display_name
286
+ FROM ai.project_access project_access
287
+ JOIN auth.access access ON access.id = project_access.access_id
288
+ LEFT JOIN auth.users users ON users.id = access.user_id
289
+ LEFT JOIN auth.groups groups ON groups.id = access.group_id
290
+ LEFT JOIN auth.service_accounts service_accounts ON service_accounts.id = access.service_account_id
291
+ WHERE project_access.project_id = ${projectId}::uuid
292
+ ORDER BY access.created_at, access.id
293
+ `;
294
+ return rows.map(toProjectAccess);
295
+ };
296
+
297
+ const createProjectAccess = async (
298
+ projectId: string,
299
+ input: { principal: Principal; permission: AiProjectPermission },
300
+ db: SQL,
301
+ ): Promise<AiProjectAccess> => {
302
+ const created = await createAccess(input, db);
303
+ if (!created.ok) throw new Error(created.error.message);
304
+ let shortId = "";
305
+ await withAiShortIdForDb(db, "idx_ai_project_access_short_id", (attempt, candidate) => {
306
+ shortId = candidate;
307
+ return attempt`
308
+ INSERT INTO ai.project_access (project_id, access_id, short_id)
309
+ VALUES (${projectId}::uuid, ${created.data.id}::uuid, ${candidate})
310
+ `;
311
+ });
312
+ const entry = (await listProjectAccess(projectId, db)).find((item) => item.shortId === shortId);
313
+ if (!entry) throw new Error("Failed to read the created Project access entry.");
314
+ return entry;
315
+ };
316
+
317
+ const updateProjectAccess = async (projectId: string, accessId: string, permission: AiProjectPermission, db: SQL): Promise<boolean> => {
318
+ const [bound] = await db<{ id: string; permission: AiProjectPermission }[]>`
319
+ SELECT access.id, access.permission
320
+ FROM ai.project_access project_access
321
+ JOIN auth.access access ON access.id = project_access.access_id
322
+ WHERE project_access.project_id = ${projectId}::uuid AND project_access.short_id = ${accessId}
323
+ `;
324
+ if (!bound) return false;
325
+ if (bound.permission === "admin" && permission !== "admin") {
326
+ const [admins] = await db<{ count: number }[]>`
327
+ SELECT count(*)::int AS count FROM ai.project_access project_access
328
+ JOIN auth.access access ON access.id = project_access.access_id
329
+ WHERE project_access.project_id = ${projectId}::uuid AND access.permission = 'admin'
330
+ `;
331
+ if ((admins?.count ?? 0) <= 1) throw new AiProjectLastAdminError();
332
+ }
333
+ const result = await db`UPDATE auth.access SET permission = ${permission}::auth.permission_level WHERE id = ${bound.id}::uuid`;
334
+ return result.count === 1;
335
+ };
336
+
337
+ const revokeProjectAccess = async (projectId: string, accessId: string, db: SQL): Promise<boolean> => {
338
+ const [bound] = await db<{ access_id: string; permission: AiProjectPermission }[]>`
339
+ SELECT project_access.access_id, access.permission
340
+ FROM ai.project_access project_access
341
+ JOIN auth.access access ON access.id = project_access.access_id
342
+ WHERE project_access.project_id = ${projectId}::uuid AND project_access.short_id = ${accessId}
343
+ `;
344
+ if (!bound) return false;
345
+ if (bound.permission === "admin") {
346
+ const [admins] = await db<{ count: number }[]>`
347
+ SELECT count(*)::int AS count FROM ai.project_access project_access
348
+ JOIN auth.access access ON access.id = project_access.access_id
349
+ WHERE project_access.project_id = ${projectId}::uuid AND access.permission = 'admin'
350
+ `;
351
+ if ((admins?.count ?? 0) <= 1) throw new AiProjectLastAdminError();
352
+ }
353
+ const deleted = await deleteAccess({ id: bound.access_id }, db);
354
+ if (!deleted.ok) throw new Error(deleted.error.message);
355
+ return true;
356
+ };
357
+
358
+ const requireProject = async (
359
+ projectId: string,
360
+ subject: AccessSubject | null,
361
+ permission: AiProjectPermission = "read",
362
+ ): Promise<AiProject | null> => {
363
+ const row = await getRow(projectId);
364
+ if (!row) return null;
365
+ const project = await toProject(row, subject);
366
+ return project && hasPermission(project.permission, permission) ? project : null;
367
+ };
368
+
369
+ const requireProjectByShortId = async (
370
+ shortId: string,
371
+ subject: AccessSubject | null,
372
+ permission: AiProjectPermission = "read",
373
+ ): Promise<AiProject | null> => {
374
+ const row = await getRowByShortId(shortId);
375
+ if (!row) return null;
376
+ const project = await toProject(row, subject);
377
+ return project && hasPermission(project.permission, permission) ? project : null;
378
+ };
379
+
380
+ const touchProject = async (db: SQL, projectId: string): Promise<void> => {
381
+ await db`UPDATE ai.projects SET revision = revision + 1, updated_at = now() WHERE id = ${projectId}::uuid`;
382
+ };
383
+
384
+ const normalizeProjectPath = (value: string): string => {
385
+ const path = value.trim().replace(/\\/g, "/").replace(/^\/+/, "");
386
+ if (!path || path.length > 500 || path.split("/").some((part) => !part || part === "." || part === "..")) {
387
+ throw new Error("Invalid project file path.");
388
+ }
389
+ return path;
390
+ };
391
+
392
+ const projectSearchPattern = (value: string | undefined): string | null => {
393
+ const query = value?.trim().toLowerCase();
394
+ return query ? `%${query.replace(/[\\%_]/g, (character) => `\\${character}`)}%` : null;
395
+ };
396
+
397
+ export const aiProjects = {
398
+ async create(input: {
399
+ subject: AccessSubject;
400
+ name: string;
401
+ description?: string;
402
+ icon?: string;
403
+ instructions?: string;
404
+ defaultModelProfileId?: string | null;
405
+ }): Promise<AiProject> {
406
+ return sql.begin(async (tx) => {
407
+ const rows = await withAiShortIdForDb(
408
+ tx,
409
+ "idx_ai_projects_short_id",
410
+ (attempt, shortId) => attempt<ProjectRow[]>`
411
+ INSERT INTO ai.projects (short_id, name, description, icon, instructions, default_model_profile_id)
412
+ VALUES (
413
+ ${shortId}, ${input.name.trim()}, ${input.description?.trim() ?? ""}, ${input.icon?.trim() || "ti ti-folders"},
414
+ ${input.instructions?.trim() ?? ""}, ${input.defaultModelProfileId?.trim() || null}
415
+ )
416
+ RETURNING *
417
+ `,
418
+ );
419
+ const created = await createAccess({ principal: principalForSubject(input.subject), permission: "admin" }, tx);
420
+ if (!created.ok) throw new Error(created.error.message);
421
+ await withAiShortIdForDb(
422
+ tx,
423
+ "idx_ai_project_access_short_id",
424
+ (attempt, shortId) => attempt`
425
+ INSERT INTO ai.project_access (project_id, access_id, short_id)
426
+ VALUES (${rows[0]!.id}::uuid, ${created.data.id}::uuid, ${shortId})
427
+ `,
428
+ );
429
+ return { ...(await toProject(rows[0]!, input.subject, tx))!, permission: "admin" };
430
+ });
431
+ },
432
+
433
+ async list(subject: AccessSubject | null): Promise<AiProject[]> {
434
+ const match = accessMatch(subject);
435
+ const rows = await sql<ProjectRow[]>`
436
+ SELECT DISTINCT project.*, lower(project.name) AS sort_name
437
+ FROM ai.projects project
438
+ JOIN ai.project_access project_access ON project_access.project_id = project.id
439
+ JOIN auth.access access ON access.id = project_access.access_id
440
+ WHERE access.permission <> 'none' AND ${match}
441
+ ORDER BY lower(project.name), project.id
442
+ LIMIT 200
443
+ `;
444
+ return (await Promise.all(rows.map((row) => toProject(row, subject)))).filter((project): project is AiProject => Boolean(project));
445
+ },
446
+
447
+ async resolveShortIds(projectIds: readonly string[], subject: AccessSubject | null): Promise<Map<string, string>> {
448
+ const ids = [...new Set(projectIds)];
449
+ if (!ids.length) return new Map();
450
+ const match = accessMatch(subject);
451
+ const rows = await sql<{ id: string; short_id: string }[]>`
452
+ SELECT DISTINCT project.id, project.short_id
453
+ FROM ai.projects project
454
+ JOIN ai.project_access project_access ON project_access.project_id = project.id
455
+ JOIN auth.access access ON access.id = project_access.access_id
456
+ WHERE project.id = ANY(${toPgUuidArray(ids)}::uuid[])
457
+ AND access.permission <> 'none' AND ${match}
458
+ `;
459
+ return new Map(rows.map((row) => [row.id, row.short_id]));
460
+ },
461
+
462
+ async scopeVersion(subject: AccessSubject | null): Promise<string> {
463
+ const projects = await this.list(subject);
464
+ return projects
465
+ .map((project) => `${project.shortId}:${project.permission}`)
466
+ .sort()
467
+ .join("|");
468
+ },
469
+
470
+ get: requireProject,
471
+ getByShortId: requireProjectByShortId,
472
+
473
+ async update(
474
+ projectId: string,
475
+ subject: AccessSubject | null,
476
+ input: Partial<Pick<AiProject, "name" | "description" | "icon" | "instructions" | "defaultModelProfileId">>,
477
+ ): Promise<AiProject | null> {
478
+ const changesTrustedInstructions = Object.hasOwn(input, "instructions") || Object.hasOwn(input, "defaultModelProfileId");
479
+ if (!(await requireProject(projectId, subject, changesTrustedInstructions ? "admin" : "write"))) return null;
480
+ const rows = await sql<ProjectRow[]>`
481
+ UPDATE ai.projects
482
+ SET name = COALESCE(${input.name?.trim() ?? null}, name),
483
+ description = COALESCE(${input.description?.trim() ?? null}, description),
484
+ icon = COALESCE(${input.icon?.trim() ?? null}, icon),
485
+ instructions = COALESCE(${input.instructions?.trim() ?? null}, instructions),
486
+ default_model_profile_id = CASE
487
+ WHEN ${Object.hasOwn(input, "defaultModelProfileId")} THEN ${input.defaultModelProfileId?.trim() || null}
488
+ ELSE default_model_profile_id
489
+ END,
490
+ revision = revision + 1,
491
+ updated_at = now()
492
+ WHERE id = ${projectId}::uuid
493
+ RETURNING *
494
+ `;
495
+ return rows[0] ? toProject(rows[0], subject) : null;
496
+ },
497
+
498
+ async delete(projectId: string, subject: AccessSubject | null): Promise<boolean> {
499
+ return sql.begin(async (tx) => {
500
+ const [project] = await tx<ProjectRow[]>`
501
+ SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
502
+ `;
503
+ if (!project || !hasPermission(await permissionFor(project, subject, tx), "admin")) return false;
504
+ const accessIds = (
505
+ await tx<{ access_id: string }[]>`SELECT access_id FROM ai.project_access WHERE project_id = ${projectId}::uuid`
506
+ ).map((row) => row.access_id);
507
+ await tx`DELETE FROM ai.projects WHERE id = ${projectId}::uuid`;
508
+ if (accessIds.length) await tx`DELETE FROM auth.access WHERE id = ANY(${toPgUuidArray(accessIds)}::uuid[])`;
509
+ return true;
510
+ });
511
+ },
512
+
513
+ async snapshot(projectId: string, subject: AccessSubject | null): Promise<AiProjectPromptSnapshot | null> {
514
+ return sql.begin(async (tx) => {
515
+ const [row] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR SHARE`;
516
+ if (!row || !hasPermission(await permissionFor(row, subject, tx), "read")) return null;
517
+ const project = await toProject(row, subject, tx);
518
+ if (!project) return null;
519
+ const [knowledgeRows, fileRows, referenceRows] = await Promise.all([
520
+ tx<KnowledgeRow[]>`SELECT * FROM ai.project_knowledge WHERE project_id = ${projectId}::uuid ORDER BY updated_at DESC, id LIMIT 100`,
521
+ tx<
522
+ FileRow[]
523
+ >`SELECT id, short_id, project_id, path, media_type, octet_length(bytes)::int AS size, updated_at FROM ai.project_files WHERE project_id = ${projectId}::uuid ORDER BY updated_at DESC, id`,
524
+ tx<ReferenceRow[]>`SELECT * FROM ai.project_resource_refs WHERE project_id = ${projectId}::uuid ORDER BY created_at, id LIMIT 500`,
525
+ ]);
526
+ const knowledge = knowledgeRows.map(toKnowledge);
527
+ const files = fileRows.map(toFile);
528
+ const references = referenceRows.map(toReference);
529
+ const lines = [
530
+ `Project: ${project.name} (${project.shortId}, revision ${project.revision})`,
531
+ project.description ? `Description: ${project.description}` : null,
532
+ knowledge.length ? `Knowledge entries:\n${knowledge.map((item) => `- ${item.title} [${item.shortId}]`).join("\n")}` : null,
533
+ files.length
534
+ ? `Files (read-only below /project):\n${files
535
+ .map((file) => `- ${mountAiProjectFilePath(file.path)} (${file.mediaType}, ${file.size} bytes) [${file.shortId}]`)
536
+ .join("\n")}`
537
+ : null,
538
+ references.length
539
+ ? `Cloud references (metadata only; use authorized app capabilities to read the source):\n${references
540
+ .map((reference) => `- ${reference.label || reference.ref.id}: ${reference.ref.type}/${reference.ref.id}`)
541
+ .join("\n")}`
542
+ : null,
543
+ ].filter(Boolean);
544
+ return {
545
+ id: project.shortId,
546
+ name: project.name,
547
+ revision: project.revision,
548
+ instructions: project.instructions,
549
+ context: lines.join("\n").slice(0, AI_PROJECT_PROMPT_MANIFEST_MAX_CHARS),
550
+ references: references.map((reference) => reference.ref),
551
+ defaultModelProfileId: project.defaultModelProfileId,
552
+ };
553
+ });
554
+ },
555
+
556
+ async listAccess(projectId: string, subject: AccessSubject | null): Promise<AiProjectAccess[] | null> {
557
+ if (!(await requireProject(projectId, subject, "admin"))) return null;
558
+ return listProjectAccess(projectId);
559
+ },
560
+
561
+ async grantAccess(
562
+ projectId: string,
563
+ subject: AccessSubject | null,
564
+ input: { principal: Principal; permission: AiProjectPermission },
565
+ ): Promise<AiProjectAccess | null> {
566
+ return sql.begin(async (tx) => {
567
+ const [row] = await tx<ProjectRow[]>`
568
+ SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
569
+ `;
570
+ if (!row || !hasPermission(await permissionFor(row, subject, tx), "admin")) return null;
571
+ return createProjectAccess(projectId, input, tx);
572
+ });
573
+ },
574
+
575
+ async updateAccess(
576
+ projectId: string,
577
+ accessId: string,
578
+ subject: AccessSubject | null,
579
+ permission: AiProjectPermission,
580
+ ): Promise<boolean> {
581
+ return sql.begin(async (tx) => {
582
+ const [project] = await tx<ProjectRow[]>`
583
+ SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
584
+ `;
585
+ if (!project || !hasPermission(await permissionFor(project, subject, tx), "admin")) return false;
586
+ return updateProjectAccess(projectId, accessId, permission, tx);
587
+ });
588
+ },
589
+
590
+ async revokeAccess(projectId: string, accessId: string, subject: AccessSubject | null): Promise<boolean> {
591
+ return sql.begin(async (tx) => {
592
+ const [project] = await tx<ProjectRow[]>`
593
+ SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
594
+ `;
595
+ if (!project || !hasPermission(await permissionFor(project, subject, tx), "admin")) return false;
596
+ return revokeProjectAccess(projectId, accessId, tx);
597
+ });
598
+ },
599
+
600
+ admin: {
601
+ async list(params: {
602
+ search?: string;
603
+ page?: number;
604
+ perPage?: number;
605
+ }): Promise<{ items: AiProjectAdminListItem[]; total: number; page: number; perPage: number }> {
606
+ const perPage = Math.min(Math.max(params.perPage ?? 100, 1), 500);
607
+ const page = Math.max(params.page ?? 1, 1);
608
+ const offset = (page - 1) * perPage;
609
+ const pattern = projectSearchPattern(params.search);
610
+ const [countRow] = await sql<{ total: number }[]>`
611
+ SELECT count(*)::int AS total
612
+ FROM ai.projects project
613
+ WHERE ${pattern}::text IS NULL
614
+ OR lower(project.name) LIKE ${pattern} ESCAPE '\\'
615
+ OR lower(project.description) LIKE ${pattern} ESCAPE '\\'
616
+ OR lower(project.short_id) LIKE ${pattern} ESCAPE '\\'
617
+ `;
618
+ const rows = await sql<AdminProjectRow[]>`
619
+ SELECT project.*,
620
+ count(project_access.access_id)::int AS access_count,
621
+ count(project_access.access_id) FILTER (WHERE access.permission = 'admin')::int AS admin_count
622
+ FROM ai.projects project
623
+ LEFT JOIN ai.project_access project_access ON project_access.project_id = project.id
624
+ LEFT JOIN auth.access access ON access.id = project_access.access_id
625
+ WHERE ${pattern}::text IS NULL
626
+ OR lower(project.name) LIKE ${pattern} ESCAPE '\\'
627
+ OR lower(project.description) LIKE ${pattern} ESCAPE '\\'
628
+ OR lower(project.short_id) LIKE ${pattern} ESCAPE '\\'
629
+ GROUP BY project.id
630
+ ORDER BY project.updated_at DESC, project.id
631
+ LIMIT ${perPage} OFFSET ${offset}
632
+ `;
633
+ return { items: rows.map(toAdminProject), total: countRow?.total ?? 0, page, perPage };
634
+ },
635
+
636
+ async summary(params: { search?: string } = {}): Promise<AiProjectAdminSummary> {
637
+ const pattern = projectSearchPattern(params.search);
638
+ const [row] = await sql<{ total: number; unmanaged: number; total_access: number }[]>`
639
+ WITH filtered AS (
640
+ SELECT project.id,
641
+ count(project_access.access_id)::int AS access_count,
642
+ count(project_access.access_id) FILTER (WHERE access.permission = 'admin')::int AS admin_count
643
+ FROM ai.projects project
644
+ LEFT JOIN ai.project_access project_access ON project_access.project_id = project.id
645
+ LEFT JOIN auth.access access ON access.id = project_access.access_id
646
+ WHERE ${pattern}::text IS NULL
647
+ OR lower(project.name) LIKE ${pattern} ESCAPE '\\'
648
+ OR lower(project.description) LIKE ${pattern} ESCAPE '\\'
649
+ OR lower(project.short_id) LIKE ${pattern} ESCAPE '\\'
650
+ GROUP BY project.id
651
+ )
652
+ SELECT count(*)::int AS total,
653
+ count(*) FILTER (WHERE admin_count = 0)::int AS unmanaged,
654
+ coalesce(sum(access_count), 0)::int AS total_access
655
+ FROM filtered
656
+ `;
657
+ return { total: row?.total ?? 0, unmanaged: row?.unmanaged ?? 0, totalAccess: row?.total_access ?? 0 };
658
+ },
659
+
660
+ async getByShortId(shortId: string): Promise<AiProjectAdminListItem | null> {
661
+ const rows = await sql<AdminProjectRow[]>`
662
+ SELECT project.*,
663
+ count(project_access.access_id)::int AS access_count,
664
+ count(project_access.access_id) FILTER (WHERE access.permission = 'admin')::int AS admin_count
665
+ FROM ai.projects project
666
+ LEFT JOIN ai.project_access project_access ON project_access.project_id = project.id
667
+ LEFT JOIN auth.access access ON access.id = project_access.access_id
668
+ WHERE project.short_id = ${shortId}
669
+ GROUP BY project.id
670
+ `;
671
+ return rows[0] ? toAdminProject(rows[0]) : null;
672
+ },
673
+
674
+ listAccess: listProjectAccess,
675
+
676
+ async grantAccess(
677
+ projectId: string,
678
+ input: { principal: Principal; permission: AiProjectPermission },
679
+ ): Promise<AiProjectAccess | null> {
680
+ return sql.begin(async (tx) => {
681
+ const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
682
+ return project ? createProjectAccess(projectId, input, tx) : null;
683
+ });
684
+ },
685
+
686
+ async updateAccess(projectId: string, accessId: string, permission: AiProjectPermission): Promise<boolean> {
687
+ return sql.begin(async (tx) => {
688
+ const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
689
+ return project ? updateProjectAccess(projectId, accessId, permission, tx) : false;
690
+ });
691
+ },
692
+
693
+ async revokeAccess(projectId: string, accessId: string): Promise<boolean> {
694
+ return sql.begin(async (tx) => {
695
+ const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
696
+ return project ? revokeProjectAccess(projectId, accessId, tx) : false;
697
+ });
698
+ },
699
+
700
+ async delete(projectId: string): Promise<boolean> {
701
+ return sql.begin(async (tx) => {
702
+ const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
703
+ if (!project) return false;
704
+ const accessIds = (
705
+ await tx<{ access_id: string }[]>`SELECT access_id FROM ai.project_access WHERE project_id = ${projectId}::uuid`
706
+ ).map((row) => row.access_id);
707
+ await tx`DELETE FROM ai.projects WHERE id = ${projectId}::uuid`;
708
+ if (accessIds.length) await tx`DELETE FROM auth.access WHERE id = ANY(${toPgUuidArray(accessIds)}::uuid[])`;
709
+ return true;
710
+ });
711
+ },
712
+ },
713
+
714
+ async listKnowledge(projectId: string, subject: AccessSubject | null, query?: string): Promise<AiProjectKnowledge[]> {
715
+ if (!(await requireProject(projectId, subject, "read"))) return [];
716
+ const q = query?.trim() || null;
717
+ const pattern = q ? `%${q.toLowerCase().replace(/[\\%_]/g, (char) => `\\${char}`)}%` : null;
718
+ const rows = await sql<KnowledgeRow[]>`
719
+ SELECT * FROM ai.project_knowledge
720
+ WHERE project_id = ${projectId}::uuid
721
+ AND (${q}::text IS NULL OR lower(title) LIKE ${pattern} ESCAPE '\\' OR lower(content) LIKE ${pattern} ESCAPE '\\'
722
+ OR search_document @@ websearch_to_tsquery('simple', ${q ?? ""}))
723
+ ORDER BY updated_at DESC, id
724
+ LIMIT 100
725
+ `;
726
+ return rows.map(toKnowledge);
727
+ },
728
+
729
+ async getKnowledgeByShortId(projectId: string, shortId: string, subject: AccessSubject | null): Promise<AiProjectKnowledge | null> {
730
+ if (!(await requireProject(projectId, subject, "read"))) return null;
731
+ const rows = await sql<KnowledgeRow[]>`
732
+ SELECT * FROM ai.project_knowledge
733
+ WHERE project_id = ${projectId}::uuid AND short_id = ${shortId}
734
+ LIMIT 1
735
+ `;
736
+ return rows[0] ? toKnowledge(rows[0]) : null;
737
+ },
738
+
739
+ async createKnowledge(
740
+ projectId: string,
741
+ subject: AccessSubject | null,
742
+ input: { title: string; content: string },
743
+ ): Promise<AiProjectKnowledge | null> {
744
+ if (!(await requireProject(projectId, subject, "write"))) return null;
745
+ return sql.begin(async (tx) => {
746
+ const rows = await withAiShortIdForDb(
747
+ tx,
748
+ "idx_ai_project_knowledge_short_id",
749
+ (attempt, shortId) => attempt<KnowledgeRow[]>`
750
+ INSERT INTO ai.project_knowledge (short_id, project_id, title, content, created_by_user_id)
751
+ VALUES (${shortId}, ${projectId}::uuid, ${input.title.trim()}, ${input.content.trim()}, ${actorUserId(subject)}::uuid)
752
+ RETURNING *
753
+ `,
754
+ );
755
+ await touchProject(tx, projectId);
756
+ return toKnowledge(rows[0]!);
757
+ });
758
+ },
759
+
760
+ async updateKnowledge(
761
+ projectId: string,
762
+ knowledgeId: string,
763
+ subject: AccessSubject | null,
764
+ input: { title?: string; content?: string },
765
+ ): Promise<AiProjectKnowledge | null> {
766
+ if (!(await requireProject(projectId, subject, "write"))) return null;
767
+ return sql.begin(async (tx) => {
768
+ const rows = await tx<KnowledgeRow[]>`
769
+ UPDATE ai.project_knowledge
770
+ SET title = COALESCE(${input.title?.trim() ?? null}, title), content = COALESCE(${input.content?.trim() ?? null}, content), updated_at = now()
771
+ WHERE short_id = ${knowledgeId} AND project_id = ${projectId}::uuid
772
+ RETURNING *
773
+ `;
774
+ if (rows[0]) await touchProject(tx, projectId);
775
+ return rows[0] ? toKnowledge(rows[0]) : null;
776
+ });
777
+ },
778
+
779
+ async deleteKnowledge(projectId: string, knowledgeId: string, subject: AccessSubject | null): Promise<boolean> {
780
+ if (!(await requireProject(projectId, subject, "write"))) return false;
781
+ return sql.begin(async (tx) => {
782
+ const rows = await tx<{ id: string }[]>`
783
+ DELETE FROM ai.project_knowledge WHERE short_id = ${knowledgeId} AND project_id = ${projectId}::uuid RETURNING id
784
+ `;
785
+ if (rows[0]) await touchProject(tx, projectId);
786
+ return Boolean(rows[0]);
787
+ });
788
+ },
789
+
790
+ async listFiles(projectId: string, subject: AccessSubject | null): Promise<AiProjectFile[]> {
791
+ if (!(await requireProject(projectId, subject, "read"))) return [];
792
+ return (
793
+ await sql<
794
+ FileRow[]
795
+ >`SELECT id, short_id, project_id, path, media_type, size, updated_at FROM ai.project_files WHERE project_id = ${projectId}::uuid ORDER BY path LIMIT 500`
796
+ ).map(toFile);
797
+ },
798
+
799
+ async writeFile(
800
+ projectId: string,
801
+ subject: AccessSubject | null,
802
+ input: { path: string; mediaType: string; bytes: Uint8Array },
803
+ ): Promise<AiProjectFile | null> {
804
+ if (!(await requireProject(projectId, subject, "write"))) return null;
805
+ if (input.bytes.byteLength > AI_PROJECT_FILE_MAX_BYTES) throw new Error("Project file exceeds the size limit.");
806
+ const path = normalizeProjectPath(input.path);
807
+ return sql.begin(async (tx) => {
808
+ const rows = await withAiShortIdForDb(
809
+ tx,
810
+ "idx_ai_project_files_short_id",
811
+ (attempt, shortId) => attempt<FileRow[]>`
812
+ INSERT INTO ai.project_files (short_id, project_id, path, media_type, bytes, size, created_by_user_id)
813
+ VALUES (${shortId}, ${projectId}::uuid, ${path}, ${input.mediaType.trim() || "application/octet-stream"}, ${input.bytes}, ${input.bytes.byteLength}, ${actorUserId(subject)}::uuid)
814
+ ON CONFLICT (project_id, path) DO UPDATE SET
815
+ media_type = EXCLUDED.media_type, bytes = EXCLUDED.bytes, size = EXCLUDED.size, updated_at = now()
816
+ RETURNING id, short_id, project_id, path, media_type, size, updated_at
817
+ `,
818
+ );
819
+ await touchProject(tx, projectId);
820
+ return toFile(rows[0]!);
821
+ });
822
+ },
823
+
824
+ async readFile(
825
+ projectId: string,
826
+ fileId: string,
827
+ subject: AccessSubject | null,
828
+ ): Promise<(AiProjectFile & { bytes: Uint8Array }) | null> {
829
+ if (!(await requireProject(projectId, subject, "read"))) return null;
830
+ const rows = await sql<(FileRow & { bytes: Uint8Array })[]>`
831
+ SELECT id, short_id, project_id, path, media_type, size, updated_at, bytes FROM ai.project_files
832
+ WHERE short_id = ${fileId} AND project_id = ${projectId}::uuid
833
+ `;
834
+ return rows[0] ? { ...toFile(rows[0]), bytes: rows[0].bytes } : null;
835
+ },
836
+
837
+ async readFileByPath(
838
+ projectId: string,
839
+ path: string,
840
+ subject: AccessSubject | null,
841
+ ): Promise<(AiProjectFile & { bytes: Uint8Array }) | null> {
842
+ if (!(await requireProject(projectId, subject, "read"))) return null;
843
+ const normalized = normalizeProjectPath(path);
844
+ const rows = await sql<(FileRow & { bytes: Uint8Array })[]>`
845
+ SELECT id, short_id, project_id, path, media_type, size, updated_at, bytes FROM ai.project_files
846
+ WHERE path = ${normalized} AND project_id = ${projectId}::uuid
847
+ `;
848
+ return rows[0] ? { ...toFile(rows[0]), bytes: rows[0].bytes } : null;
849
+ },
850
+
851
+ async deleteFile(projectId: string, fileId: string, subject: AccessSubject | null): Promise<boolean> {
852
+ if (!(await requireProject(projectId, subject, "write"))) return false;
853
+ return sql.begin(async (tx) => {
854
+ const rows = await tx<{ id: string }[]>`
855
+ DELETE FROM ai.project_files WHERE short_id = ${fileId} AND project_id = ${projectId}::uuid RETURNING id
856
+ `;
857
+ if (rows[0]) await touchProject(tx, projectId);
858
+ return Boolean(rows[0]);
859
+ });
860
+ },
861
+
862
+ async listReferences(projectId: string, subject: AccessSubject | null): Promise<AiProjectReference[]> {
863
+ if (!(await requireProject(projectId, subject, "read"))) return [];
864
+ return (
865
+ await sql<
866
+ ReferenceRow[]
867
+ >`SELECT * FROM ai.project_resource_refs WHERE project_id = ${projectId}::uuid ORDER BY created_at, id LIMIT 500`
868
+ ).map(toReference);
869
+ },
870
+
871
+ async createReference(
872
+ projectId: string,
873
+ subject: AccessSubject | null,
874
+ input: { ref: CloudResourceRef; label?: string },
875
+ ): Promise<AiProjectReference | null> {
876
+ if (!(await requireProject(projectId, subject, "write"))) return null;
877
+ return sql.begin(async (tx) => {
878
+ const rows = await withAiShortIdForDb(
879
+ tx,
880
+ "idx_ai_project_resource_refs_short_id",
881
+ (attempt, shortId) => attempt<ReferenceRow[]>`
882
+ INSERT INTO ai.project_resource_refs (short_id, project_id, resource_type, resource_id, label)
883
+ VALUES (${shortId}, ${projectId}::uuid, ${input.ref.type}, ${input.ref.id}, ${input.label?.trim() ?? ""})
884
+ RETURNING *
885
+ `,
886
+ );
887
+ await touchProject(tx, projectId);
888
+ return toReference(rows[0]!);
889
+ });
890
+ },
891
+
892
+ async deleteReference(projectId: string, referenceId: string, subject: AccessSubject | null): Promise<boolean> {
893
+ if (!(await requireProject(projectId, subject, "write"))) return false;
894
+ return sql.begin(async (tx) => {
895
+ const rows = await tx<{ id: string }[]>`
896
+ DELETE FROM ai.project_resource_refs WHERE short_id = ${referenceId} AND project_id = ${projectId}::uuid RETURNING id
897
+ `;
898
+ if (rows[0]) await touchProject(tx, projectId);
899
+ return Boolean(rows[0]);
900
+ });
901
+ },
902
+ };