@app-vitals/agent-admin 0.0.1 → 0.1.1

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 (325) hide show
  1. package/Dockerfile +43 -0
  2. package/README.md +72 -2
  3. package/e2e/agents-page.e2e.ts +338 -0
  4. package/e2e/chat-degraded.e2e.ts +153 -0
  5. package/e2e/chat-page.e2e.ts +169 -0
  6. package/e2e/chat-progress.e2e.ts +206 -0
  7. package/e2e/chat-thread-page.e2e.ts +444 -0
  8. package/e2e/login-page.e2e.ts +148 -0
  9. package/e2e/test-server.ts +427 -0
  10. package/openapi.json +6098 -0
  11. package/openapi.platform.json +5553 -0
  12. package/package.json +62 -4
  13. package/playwright.config.ts +25 -0
  14. package/prisma/client/browser.ts +134 -0
  15. package/prisma/client/client.ts +158 -0
  16. package/prisma/client/commonInputTypes.ts +578 -0
  17. package/prisma/client/enums.ts +15 -0
  18. package/prisma/client/internal/class.ts +424 -0
  19. package/prisma/client/internal/prismaNamespace.ts +2898 -0
  20. package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
  21. package/prisma/client/models/Account.ts +1774 -0
  22. package/prisma/client/models/AccountInvite.ts +1386 -0
  23. package/prisma/client/models/AccountMember.ts +1351 -0
  24. package/prisma/client/models/Agent.ts +4554 -0
  25. package/prisma/client/models/AgentAsk.ts +1373 -0
  26. package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
  27. package/prisma/client/models/AgentCronJob.ts +2416 -0
  28. package/prisma/client/models/AgentCronRun.ts +2858 -0
  29. package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
  30. package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
  31. package/prisma/client/models/AgentEnv.ts +1421 -0
  32. package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
  33. package/prisma/client/models/AgentMember.ts +1316 -0
  34. package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
  35. package/prisma/client/models/AgentPlugin.ts +1421 -0
  36. package/prisma/client/models/AgentToken.ts +1380 -0
  37. package/prisma/client/models/AgentTool.ts +1351 -0
  38. package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
  39. package/prisma/client/models/ChatThreadWatch.ts +1183 -0
  40. package/prisma/client/models/PushSubscription.ts +1233 -0
  41. package/prisma/client/models/SessionAlertState.ts +1187 -0
  42. package/prisma/client/models/SessionFollow.ts +1187 -0
  43. package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
  44. package/prisma/client/models.ts +34 -0
  45. package/prisma/migrations/20260608000000_init/migration.sql +112 -0
  46. package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
  47. package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
  48. package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
  49. package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
  50. package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
  51. package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
  52. package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
  53. package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
  54. package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
  55. package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
  56. package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
  57. package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
  58. package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
  59. package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
  60. package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
  61. package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
  62. package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
  63. package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
  64. package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
  65. package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
  66. package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
  67. package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
  68. package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
  69. package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
  70. package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
  71. package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
  72. package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
  73. package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
  74. package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
  75. package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
  76. package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
  77. package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
  78. package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
  79. package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
  80. package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
  81. package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
  82. package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
  83. package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
  84. package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
  85. package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
  86. package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
  87. package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
  88. package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
  89. package/prisma/migrations/migration_lock.toml +3 -0
  90. package/prisma/schema/coding.prisma +85 -0
  91. package/prisma/schema/schema.prisma +506 -0
  92. package/prisma.config.ts +34 -0
  93. package/pwa-assets/icons/apple-touch-icon.png +0 -0
  94. package/pwa-assets/icons/favicon-32.png +0 -0
  95. package/pwa-assets/icons/icon-192.png +0 -0
  96. package/pwa-assets/icons/icon-512.png +0 -0
  97. package/pwa-assets/icons/icon-maskable-192.png +0 -0
  98. package/pwa-assets/icons/icon-maskable-512.png +0 -0
  99. package/src/account-agent-create.ts +42 -0
  100. package/src/account-agent-create.unit.test.ts +28 -0
  101. package/src/account-created-notifier.ts +71 -0
  102. package/src/account-created-notifier.unit.test.ts +70 -0
  103. package/src/account-email.ts +9 -0
  104. package/src/account-email.unit.test.ts +21 -0
  105. package/src/account-invites.ts +70 -0
  106. package/src/account-lifecycle.integration.test.ts +329 -0
  107. package/src/account-lifecycle.ts +206 -0
  108. package/src/account-lifecycle.unit.test.ts +108 -0
  109. package/src/account-members.ts +128 -0
  110. package/src/account-onboarding.integration.test.ts +101 -0
  111. package/src/account-onboarding.ts +95 -0
  112. package/src/account-trial-expiry-sweeper.ts +85 -0
  113. package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
  114. package/src/account-trial-warning-sweeper.ts +216 -0
  115. package/src/accounts-api.integration.test.ts +196 -0
  116. package/src/accounts-api.smoke.test.ts +292 -0
  117. package/src/accounts-api.ts +289 -0
  118. package/src/accounts-api.unit.test.ts +93 -0
  119. package/src/accounts.integration.test.ts +178 -0
  120. package/src/accounts.ts +208 -0
  121. package/src/admin-boot-config.ts +574 -0
  122. package/src/admin-boot-config.unit.test.ts +28 -0
  123. package/src/admin-boot.boot-order.unit.test.ts +91 -0
  124. package/src/admin-boot.ts +690 -0
  125. package/src/admin-dev-login.smoke.test.ts +317 -0
  126. package/src/admin-extension.ts +432 -0
  127. package/src/admin-extension.unit.test.ts +532 -0
  128. package/src/admin-local-agent.smoke.test.ts +1643 -0
  129. package/src/admin-spec.smoke.test.ts +96 -0
  130. package/src/admin-spec.ts +186 -0
  131. package/src/admin-ui-account-nav.ts +20 -0
  132. package/src/admin-ui-account-pages.ts +131 -0
  133. package/src/admin-ui-account-pages.unit.test.ts +112 -0
  134. package/src/admin-ui-account.smoke.test.ts +286 -0
  135. package/src/admin-ui-account.ts +185 -0
  136. package/src/admin-ui-accounts-pages.ts +202 -0
  137. package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
  138. package/src/admin-ui-accounts.smoke.test.ts +434 -0
  139. package/src/admin-ui-accounts.ts +266 -0
  140. package/src/admin-ui-layout.ts +60 -0
  141. package/src/admin-ui-layout.unit.test.ts +140 -0
  142. package/src/admin-ui-pages.ts +3489 -0
  143. package/src/admin-ui-pages.unit.test.ts +4355 -0
  144. package/src/admin-ui-push-test.smoke.test.ts +125 -0
  145. package/src/admin-ui-push-test.ts +102 -0
  146. package/src/admin-ui-styles.ts +656 -0
  147. package/src/admin-ui-styles.unit.test.ts +103 -0
  148. package/src/admin-ui.fixtures.ts +457 -0
  149. package/src/admin-ui.smoke.test.ts +7227 -0
  150. package/src/admin-ui.ts +3824 -0
  151. package/src/agent-account-assignment.ts +49 -0
  152. package/src/agent-account-assignment.unit.test.ts +99 -0
  153. package/src/agent-anthropic-identity.integration.test.ts +99 -0
  154. package/src/agent-anthropic-identity.ts +78 -0
  155. package/src/agent-anthropic-identity.unit.test.ts +77 -0
  156. package/src/agent-ask-config.ts +59 -0
  157. package/src/agent-ask-config.unit.test.ts +70 -0
  158. package/src/agent-ask.integration.test.ts +175 -0
  159. package/src/agent-ask.smoke.test.ts +457 -0
  160. package/src/agent-ask.ts +378 -0
  161. package/src/agent-chat-tokens.integration.test.ts +420 -0
  162. package/src/agent-chat-tokens.smoke.test.ts +551 -0
  163. package/src/agent-chat-tokens.ts +259 -0
  164. package/src/agent-cron-jobs.integration.test.ts +582 -0
  165. package/src/agent-cron-jobs.ts +615 -0
  166. package/src/agent-cron-jobs.unit.test.ts +420 -0
  167. package/src/agent-cron-run-stats.integration.test.ts +1551 -0
  168. package/src/agent-cron-run-stats.smoke.test.ts +953 -0
  169. package/src/agent-cron-run-stats.ts +752 -0
  170. package/src/agent-cron-runs.integration.test.ts +1491 -0
  171. package/src/agent-cron-runs.ts +489 -0
  172. package/src/agent-deletion-checklist.ts +128 -0
  173. package/src/agent-deletion-checklist.unit.test.ts +201 -0
  174. package/src/agent-deletion-per-agent.integration.test.ts +323 -0
  175. package/src/agent-deletion.integration.test.ts +323 -0
  176. package/src/agent-deletion.ts +329 -0
  177. package/src/agent-deletion.unit.test.ts +412 -0
  178. package/src/agent-envs.integration.test.ts +323 -0
  179. package/src/agent-envs.ts +244 -0
  180. package/src/agent-github-installations.integration.test.ts +115 -0
  181. package/src/agent-github-installations.ts +52 -0
  182. package/src/agent-manifest.ts +685 -0
  183. package/src/agent-manifest.unit.test.ts +870 -0
  184. package/src/agent-members.ts +69 -0
  185. package/src/agent-members.unit.test.ts +250 -0
  186. package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
  187. package/src/agent-plugins.integration.test.ts +157 -0
  188. package/src/agent-plugins.ts +95 -0
  189. package/src/agent-plugins.unit.test.ts +43 -0
  190. package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
  191. package/src/agent-provisioner.integration.test.ts +1245 -0
  192. package/src/agent-provisioner.ts +1650 -0
  193. package/src/agent-provisioner.unit.test.ts +1852 -0
  194. package/src/agent-tokens.integration.test.ts +150 -0
  195. package/src/agent-tokens.ts +112 -0
  196. package/src/agent-tools.integration.test.ts +129 -0
  197. package/src/agent-tools.ts +111 -0
  198. package/src/agents-api.integration.test.ts +705 -0
  199. package/src/agents-api.sentry.smoke.test.ts +478 -0
  200. package/src/agents-api.smoke.test.ts +5986 -0
  201. package/src/agents-api.ts +2826 -0
  202. package/src/agents.integration.test.ts +512 -0
  203. package/src/agents.ts +1097 -0
  204. package/src/agents.unit.test.ts +1736 -0
  205. package/src/anthropic-credential-guard.ts +44 -0
  206. package/src/anthropic-credential-guard.unit.test.ts +88 -0
  207. package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
  208. package/src/anthropic-federated-token-provider.ts +249 -0
  209. package/src/anthropic-identity-client.fixtures.ts +237 -0
  210. package/src/anthropic-identity-client.integration.test.ts +342 -0
  211. package/src/anthropic-identity-client.ts +473 -0
  212. package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
  213. package/src/anthropic-identity-name.integration.test.ts +83 -0
  214. package/src/anthropic-identity-name.ts +163 -0
  215. package/src/anthropic-identity-name.unit.test.ts +345 -0
  216. package/src/anthropic-openapi-contract.fixtures.ts +192 -0
  217. package/src/api-auth.ts +383 -0
  218. package/src/api-auth.unit.test.ts +837 -0
  219. package/src/api.smoke.test.ts +1225 -0
  220. package/src/api.ts +376 -0
  221. package/src/attachment-validation.ts +124 -0
  222. package/src/attachment-validation.unit.test.ts +218 -0
  223. package/src/caller-scope.ts +131 -0
  224. package/src/caller-scope.unit.test.ts +116 -0
  225. package/src/chat-credential-config.ts +22 -0
  226. package/src/chat-credentials-logging.unit.test.ts +92 -0
  227. package/src/chat-markers.ts +114 -0
  228. package/src/chat-markers.unit.test.ts +116 -0
  229. package/src/chat-service-provisioning-client.integration.test.ts +412 -0
  230. package/src/chat-service-provisioning-client.ts +221 -0
  231. package/src/chat.smoke.test.ts +1875 -0
  232. package/src/clock.ts +47 -0
  233. package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
  234. package/src/coding-task-store-provisioning-client.ts +156 -0
  235. package/src/crypto.ts +83 -0
  236. package/src/dev-auth-guard.ts +29 -0
  237. package/src/dev-auth-guard.unit.test.ts +51 -0
  238. package/src/errors.ts +77 -0
  239. package/src/errors.unit.test.ts +52 -0
  240. package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
  241. package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
  242. package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
  243. package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
  244. package/src/fixtures/github-app-manifest-cassette.json +44 -0
  245. package/src/fixtures/google-auth-cassette.json +36 -0
  246. package/src/fixtures/http-chat-client-cassette.json +226 -0
  247. package/src/fixtures/k8s-cassette.json +470 -0
  248. package/src/fixtures/okta-auth-cassette.json +36 -0
  249. package/src/fixtures/slack-provision-cassette.json +12 -0
  250. package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
  251. package/src/fixtures/task-store-provisioning-cassette.json +85 -0
  252. package/src/github-app-provisioning-client.integration.test.ts +149 -0
  253. package/src/github-app-provisioning-client.ts +161 -0
  254. package/src/github-app-provisioning-client.unit.test.ts +114 -0
  255. package/src/github-provisioning-service.ts +569 -0
  256. package/src/github-provisioning-service.unit.test.ts +123 -0
  257. package/src/google-auth-client.integration.test.ts +195 -0
  258. package/src/google-auth-client.ts +121 -0
  259. package/src/http-chat-client.integration.test.ts +554 -0
  260. package/src/http-chat-client.ts +533 -0
  261. package/src/http-chat-client.unit.test.ts +119 -0
  262. package/src/import-boundary.unit.test.ts +240 -0
  263. package/src/kubernetes-client.integration.test.ts +697 -0
  264. package/src/kubernetes-client.ts +1027 -0
  265. package/src/kubernetes-client.unit.test.ts +726 -0
  266. package/src/main.smoke.test.ts +91 -0
  267. package/src/main.ts +21 -0
  268. package/src/main.unit.test.ts +801 -0
  269. package/src/okta-auth-client.integration.test.ts +263 -0
  270. package/src/okta-auth-client.ts +152 -0
  271. package/src/openapi-schemas.ts +1310 -0
  272. package/src/openapi-schemas.unit.test.ts +348 -0
  273. package/src/per-agent-identity.fixtures.ts +305 -0
  274. package/src/platform-doc-route.ts +40 -0
  275. package/src/prisma-client.integration.test.ts +57 -0
  276. package/src/prisma-client.ts +187 -0
  277. package/src/prisma-client.unit.test.ts +178 -0
  278. package/src/prisma-tx.ts +23 -0
  279. package/src/push-content.ts +177 -0
  280. package/src/push-content.unit.test.ts +213 -0
  281. package/src/push-crypto.ts +135 -0
  282. package/src/push-crypto.unit.test.ts +66 -0
  283. package/src/push-sender.ts +169 -0
  284. package/src/push-sender.unit.test.ts +107 -0
  285. package/src/push-service.ts +332 -0
  286. package/src/push-service.unit.test.ts +679 -0
  287. package/src/push-toggle.ts +135 -0
  288. package/src/push-toggle.unit.test.ts +78 -0
  289. package/src/pwa-https-gating.smoke.test.ts +229 -0
  290. package/src/pwa-icons.unit.test.ts +41 -0
  291. package/src/pwa-routes.smoke.test.ts +276 -0
  292. package/src/pwa.ts +380 -0
  293. package/src/pwa.unit.test.ts +321 -0
  294. package/src/schema.unit.test.ts +122 -0
  295. package/src/self-serve-config.ts +52 -0
  296. package/src/self-serve-config.unit.test.ts +79 -0
  297. package/src/server.smoke.test.ts +273 -0
  298. package/src/server.ts +50 -0
  299. package/src/service-integration.ts +67 -0
  300. package/src/service-integration.unit.test.ts +129 -0
  301. package/src/slack-provisioning-client.integration.test.ts +308 -0
  302. package/src/slack-provisioning-client.ts +411 -0
  303. package/src/slack-provisioning-client.unit.test.ts +80 -0
  304. package/src/slack-provisioning-service.integration.test.ts +637 -0
  305. package/src/slack-provisioning-service.ts +451 -0
  306. package/src/slack-provisioning.integration.test.ts +395 -0
  307. package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
  308. package/src/specialty-manifest-loader.ts +255 -0
  309. package/src/specialty-manifest-loader.unit.test.ts +301 -0
  310. package/src/specialty-registry.schema.unit.test.ts +35 -0
  311. package/src/specialty-registry.ts +236 -0
  312. package/src/specialty-registry.unit.test.ts +272 -0
  313. package/src/startup-policy-reconcile.ts +77 -0
  314. package/src/startup-policy-reconcile.unit.test.ts +251 -0
  315. package/src/theme-tokens.json +12 -0
  316. package/src/token-crypto.ts +60 -0
  317. package/src/token-crypto.unit.test.ts +182 -0
  318. package/src/trial-expiry-sweeper.integration.test.ts +318 -0
  319. package/src/trial-expiry-sweeper.ts +132 -0
  320. package/src/trial-expiry-sweeper.unit.test.ts +391 -0
  321. package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
  322. package/src/trial-expiry-warning-sweeper.ts +447 -0
  323. package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
  324. package/src/voice-contract.unit.test.ts +102 -0
  325. package/tsconfig.json +9 -0
@@ -0,0 +1,506 @@
1
+ // Shipwright — Admin Service Schema
2
+ // Owns: Agent, AgentEnv, AgentCronJob, AgentTool, AgentToken, AgentPlugin,
3
+ // plus the coding-owned models in coding.prisma (AgentWorkQueueSnapshot,
4
+ // AgentPhaseMethodology, SessionFollow, SessionAlertState).
5
+ // Agent is a first-class model (standalone, not embedded in a User model).
6
+ //
7
+ // WARNING: This schema uses DATABASE_URL_SHIPWRIGHT_ADMIN — a dedicated database
8
+ // for the admin service. Do NOT point this at a shared database.
9
+
10
+ // Prisma 7: the `prisma-client` generator (ESM, driver-adapter based) replaces
11
+ // `prisma-client-js`, and the datasource block no longer accepts `url` — the
12
+ // connection string for CLI/Migrate lives in ../../prisma.config.ts, and the
13
+ // runtime client connects through a PrismaPg driver adapter (see
14
+ // admin/src/prisma-client.ts).
15
+ generator client {
16
+ provider = "prisma-client"
17
+ output = "../client"
18
+ }
19
+
20
+ datasource db {
21
+ provider = "postgresql"
22
+ }
23
+
24
+ // ─── Agent ────────────────────────────────────────────────────────────────────
25
+ // A Shipwright agent: an autonomous runner that picks tasks, builds, and ships PRs.
26
+
27
+ model Agent {
28
+ id String @id @default(cuid())
29
+ name String
30
+ slackId String? @unique
31
+ slackBotToken String? // AES-256-GCM encrypted at the service layer
32
+ anthropicApiKey String? // AES-256-GCM encrypted at the service layer
33
+ selfHosted Boolean @default(false)
34
+ repos String[] @default([])
35
+ // DBR-2.1→2.4: renamed from authorAllowlist. DBR-2.1 added this column as
36
+ // a dual-written/dual-read twin of the old authorAllowlist column and
37
+ // backfilled it; DBR-2.2/2.3 migrated the admin UI and the agent's
38
+ // runtime read path onto this field exclusively; DBR-2.4 dropped the old
39
+ // authorAllowlist column now that nothing reads or writes it.
40
+ // CODING-OWNED (AEP-3.4): reviewAuthorAllowlist and patchAuthorAllowlist are
41
+ // read by the coding review/patch flows. Kept on Agent (no column move).
42
+ reviewAuthorAllowlist String[] @default([])
43
+ patchAuthorAllowlist String[] @default([])
44
+ restrictSlackToMembers Boolean @default(false)
45
+ typeName String @default("coding")
46
+ // SSP-1.2: owning account. Nullable/additive — existing agents keep NULL.
47
+ accountId String?
48
+ account Account? @relation(fields: [accountId], references: [id], onDelete: Restrict)
49
+ // ATE-1.1: trial expiry tracking. Both nullable/additive — null means "no
50
+ // trial configured" / "no warning sent yet". trialExpiresAt is settable via
51
+ // PATCH /agents/:id like other nullable scalar fields; trialExpiryWarnedAt
52
+ // is written internally by ATE-2.1's warning check (not user-editable via
53
+ // the PATCH route) but is included in GET/PATCH responses.
54
+ trialExpiresAt DateTime?
55
+ trialExpiryWarnedAt DateTime?
56
+ // PAI-1.1: owner and Anthropic identity. All nullable/additive — existing
57
+ // agents keep NULL. anthropicIdentityName is unique across agents.
58
+ ownerEmail String?
59
+ anthropicIdentityName String? @unique
60
+ anthropicServiceAccountId String?
61
+ anthropicFederationRuleId String?
62
+ k8sServiceAccountName String?
63
+ // APM-1.1: the 6 agent policy fields (auto_post_reviews, allow_self_review,
64
+ // min_confidence, max_findings, cleanup_merged_worktrees,
65
+ // cleanup_after_days). Originally mirrored from the legacy
66
+ // state/agent-policy.md; the DB is now the only source and each column
67
+ // carries its documented default.
68
+ // CODING-OWNED (AEP-3.4): autoPostReviews, allowSelfReview, minConfidence,
69
+ // maxFindings configure the coding review flow; cleanupMergedWorktrees and
70
+ // cleanupAfterDays below are runtime-owned.
71
+ autoPostReviews Boolean @default(true)
72
+ allowSelfReview Boolean @default(false)
73
+ minConfidence Int @default(75)
74
+ maxFindings Int @default(5)
75
+ cleanupMergedWorktrees Boolean @default(true)
76
+ cleanupAfterDays Int @default(14)
77
+ createdAt DateTime @default(now())
78
+ updatedAt DateTime @updatedAt
79
+
80
+ envVars AgentEnv[]
81
+ cronJobs AgentCronJob[]
82
+ tools AgentTool[]
83
+ tokens AgentToken[]
84
+ plugins AgentPlugin[]
85
+ members AgentMember[]
86
+ chatTokenUsage AgentChatTokenUsageDailyByModel[]
87
+ workQueueSnapshot AgentWorkQueueSnapshot?
88
+ githubInstallationsSnapshot AgentGitHubInstallationsSnapshot?
89
+ phaseMethodologies AgentPhaseMethodology[]
90
+
91
+ @@index([accountId])
92
+ }
93
+
94
+ // ─── Agent Env ────────────────────────────────────────────────────────────────
95
+ // Generic key/value env var store for deployed agents.
96
+ // Keys are env var names (e.g. ANTHROPIC_API_KEY). Values are AES-256-GCM encrypted.
97
+
98
+ model AgentEnv {
99
+ id String @id @default(cuid())
100
+ agentId String
101
+ agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
102
+ key String // env var name
103
+ value String // AES-256-GCM encrypted
104
+ secret Boolean @default(false) // when true, GET /agents/:id/envs masks the value as ***
105
+ createdAt DateTime @default(now())
106
+ updatedAt DateTime @updatedAt
107
+
108
+ @@unique([agentId, key])
109
+ }
110
+
111
+ // ─── Agent Cron Jobs ──────────────────────────────────────────────────────────
112
+ // Scheduled prompts per agent.
113
+ // Delivery target is either a Slack channel ID or a Slack user ID (DM).
114
+ // Validation that exactly one of channel/user is set happens in the service layer.
115
+
116
+ model AgentCronJob {
117
+ id String @id @default(cuid())
118
+ agentId String
119
+ agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
120
+ schedule String // cron expression e.g. "0 9 * * 1-5"
121
+ prompt String
122
+ channel String? // Slack channel ID (mutually exclusive with user)
123
+ user String? // Slack user ID for DMs
124
+ silent Boolean @default(false)
125
+ enabled Boolean @default(true)
126
+ preCheck String? // optional pre-check script e.g. "shipwright:check-docs-freshness.ts"
127
+ name String? // stable key for system crons; null for user-created crons
128
+ system Boolean @default(false)
129
+ // Set when the trial-expiry lockdown (not the user) disabled this cron, so
130
+ // restoreLockdownDisabled() can re-enable exactly those crons. Null for
131
+ // enabled crons and for crons the user disabled manually.
132
+ lockdownDisabledAt DateTime?
133
+ createdAt DateTime @default(now())
134
+ updatedAt DateTime @updatedAt
135
+
136
+ // Self-relation: a parent cron can have child "phase" crons (e.g. dev-task/
137
+ // review/patch/deploy phases of a single logical loop). Null for top-level
138
+ // crons. Deleting a parent cascades to its phase children.
139
+ parentCronId String?
140
+ parentCron AgentCronJob? @relation("CronPhases", fields: [parentCronId], references: [id], onDelete: Cascade)
141
+ phases AgentCronJob[] @relation("CronPhases")
142
+
143
+ runs AgentCronRun[]
144
+ phaseRuns AgentCronRun[] @relation("PhaseRuns")
145
+
146
+ @@index([parentCronId])
147
+ }
148
+
149
+ // ─── Agent Tools ──────────────────────────────────────────────────────────────
150
+ // Allowed tool patterns per agent. Each row grants an agent permission to use
151
+ // a specific tool pattern (e.g. "Read", "Bash", "Write"). The unique constraint
152
+ // on [agentId, pattern] prevents duplicates.
153
+
154
+ model AgentTool {
155
+ id String @id @default(cuid())
156
+ agentId String
157
+ agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
158
+ pattern String
159
+ enabled Boolean @default(true)
160
+ createdAt DateTime @default(now())
161
+
162
+ @@unique([agentId, pattern])
163
+ }
164
+
165
+ // ─── Agent Tokens ─────────────────────────────────────────────────────────────
166
+ // Scoped API tokens for agent authentication. The raw token is returned once at
167
+ // creation; only the SHA-256 hash is stored. Token validation is O(1) via the
168
+ // hash index.
169
+
170
+ model AgentToken {
171
+ id String @id @default(cuid())
172
+ agentId String
173
+ agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
174
+ token String @unique // SHA-256 hash of the raw token (hex)
175
+ label String?
176
+ createdAt DateTime @default(now())
177
+ revokedAt DateTime?
178
+ }
179
+
180
+ // ─── Agent Plugins ────────────────────────────────────────────────────────────
181
+ // Claude Code plugins installed per agent. Each row represents one installed
182
+ // plugin package. The unique constraint on [agentId, name] prevents duplicates.
183
+
184
+ model AgentPlugin {
185
+ id String @id @default(cuid())
186
+ agentId String
187
+ agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
188
+ name String // plugin package name e.g. "@shipwright/plugin"
189
+ version String? // pinned version; null = latest
190
+ enabled Boolean @default(true)
191
+ createdAt DateTime @default(now())
192
+ updatedAt DateTime @updatedAt
193
+
194
+ @@unique([agentId, name])
195
+ }
196
+
197
+ // ─── Agent Members ────────────────────────────────────────────────────────────
198
+ // Non-admin users granted web UI access to a specific agent.
199
+ // Admins (SHIPWRIGHT_ADMIN_ALLOWED_EMAILS) bypass this table entirely.
200
+ // Members can view and edit only the agents they are listed on.
201
+
202
+ model AgentMember {
203
+ id String @id @default(cuid())
204
+ agentId String
205
+ email String
206
+ createdAt DateTime @default(now())
207
+ agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
208
+
209
+ @@unique([agentId, email])
210
+ @@index([email])
211
+ }
212
+
213
+ // ─── Accounts ─────────────────────────────────────────────────────────────────
214
+ // SSP-1.2: self-serve agent provisioning. An Account owns agents and has
215
+ // members (email-keyed) and pending invites. Phase 1: one account per email
216
+ // (UNIQUE(email) on AccountMember). Enum-like strings are validated in the
217
+ // service layer, matching the rest of this schema.
218
+
219
+ model Account {
220
+ id String @id @default(cuid())
221
+ name String
222
+ status String @default("active") // 'active' | 'suspended' | 'trial_expired'
223
+ maxAgents Int @default(0)
224
+ plan String?
225
+ trialExpiresAt DateTime?
226
+ trialExpiryWarnedAt DateTime?
227
+ createdAt DateTime @default(now())
228
+ updatedAt DateTime @updatedAt
229
+
230
+ agents Agent[]
231
+ members AccountMember[]
232
+ invites AccountInvite[]
233
+ }
234
+
235
+ model AccountMember {
236
+ id String @id @default(cuid())
237
+ accountId String
238
+ email String @unique // lowercased; one account per email in Phase 1
239
+ role String // 'owner' | 'member'
240
+ createdAt DateTime @default(now())
241
+ account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
242
+
243
+ @@unique([accountId, email])
244
+ }
245
+
246
+ model AccountInvite {
247
+ id String @id @default(cuid())
248
+ accountId String
249
+ email String // lowercased
250
+ invitedBy String
251
+ createdAt DateTime @default(now())
252
+ acceptedAt DateTime?
253
+ account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
254
+
255
+ @@unique([accountId, email])
256
+ @@index([email])
257
+ }
258
+
259
+ // ─── Agent Cron Runs ──────────────────────────────────────────────────────────
260
+ // Execution history for AgentCronJob firings. Each row records one cron invocation:
261
+ // whether it was skipped (pre-check returned false), the outcome, and any error.
262
+ // agentId is denormalized for efficient agent-level history queries.
263
+
264
+ model AgentCronRun {
265
+ id String @id @default(cuid())
266
+ cronId String
267
+ cron AgentCronJob @relation(fields: [cronId], references: [id], onDelete: Cascade)
268
+ agentId String // denormalized from cron.agentId for efficient queries
269
+ startedAt DateTime
270
+ completedAt DateTime?
271
+ skipped Boolean @default(false)
272
+ skipReason String?
273
+ outcome String?
274
+ error String?
275
+ itemType String? // work item type this run was dispatched against ("task" | "pr");
276
+ // null when the tick had no dispatch (skipped tick, empty queue)
277
+ itemId String? // work item id this run was dispatched against (e.g. "WLS-2.2" or
278
+ // "acme/x#123"); null when the tick had no dispatch
279
+ commandVariant String? // which slash-command variant a loop dispatch ran ("default" | "alternate");
280
+ // null for runs with no variant attribution
281
+ sessionId String? // Claude session id this cron run corresponds to
282
+ lastHeartbeatAt DateTime? // Most recent debounced progress-push (recordProgress())
283
+ // timestamp, sourced from the agent's injected Clock. Null
284
+ // for runs that never reported progress (short/legacy runs).
285
+ // Lets the admin UI show a "last checked in" signal — and a
286
+ // best-effort in-progress duration — for a run that stalls
287
+ // or crashes before reaching completedAt. Additive/nullable;
288
+ // no backfill for existing rows.
289
+ createdAt DateTime @default(now())
290
+
291
+ // Secondary/denormalized pointer to the phase cron (a child AgentCronJob)
292
+ // this run was dispatched by, distinct from cronId which always points at
293
+ // the top-level cron. Null until phase crons are wired up (LPC-1.2+).
294
+ // SetNull (not Cascade) on delete: deleting a phase cron shouldn't delete
295
+ // the parent cron's run history, which cronId's own Cascade already owns.
296
+ phaseId String?
297
+ phaseCron AgentCronJob? @relation("PhaseRuns", fields: [phaseId], references: [id], onDelete: SetNull)
298
+
299
+ // ── Run telemetry (prompt-audit metrics; all additive/nullable) ──────────
300
+ // First-turn context baseline: the usage of the run's first assistant
301
+ // message. baselineContextTokens = input + cacheCreation + cacheRead of
302
+ // that turn — the full always-loaded context (system prompt, tool schemas,
303
+ // CLAUDE.md/rules, skill listing, cron prompt) independent of cache
304
+ // warmth. Null for resumed sessions (their first turn replays history)
305
+ // and for runs reported by older agent builds.
306
+ baselineModel String?
307
+ baselineContextTokens Int?
308
+ baselineInputTokens Int?
309
+ baselineCacheCreationTokens Int?
310
+ baselineCacheReadTokens Int?
311
+ // Distinct usage-bearing assistant turns / distinct tool_use blocks.
312
+ turns Int?
313
+ toolCalls Int?
314
+ // sha256[:12] over the workspace's always-loaded markdown (CLAUDE.md, its
315
+ // @imports, no-`paths` rules) + plugin version — groups runs by what the
316
+ // model was given, so before/after series for a markdown change key on
317
+ // content rather than merge date. Computed agent-side.
318
+ contextFingerprint String?
319
+ pluginVersion String?
320
+ claudeCodeVersion String?
321
+
322
+ modelBreakdown AgentCronRunModelBreakdown[]
323
+ skillUsage AgentCronRunSkillUsage[]
324
+
325
+ @@index([cronId])
326
+ @@index([agentId, startedAt])
327
+ @@index([phaseId])
328
+ @@index([startedAt])
329
+ @@index([contextFingerprint])
330
+ }
331
+
332
+ // ─── Agent Cron Run Skill Usage ──────────────────────────────────────────────
333
+ // Per-skill / per-subagent token attribution for a cron run, derived from the
334
+ // Claude CLI stream by the agent. `kind` is "skill" (a plugin
335
+ // skill invoked via the Skill tool), "agent" (a subagent type invoked via the
336
+ // Agent tool), or "root" (turns before any skill was invoked). Attribution is
337
+ // last-wins: every turn after an invoke is charged to the most recently
338
+ // invoked skill. invokeContextDelta is input + cacheCreation of the first
339
+ // turn after the skill's first invoke — the new context admitted when its
340
+ // body loaded. The unique constraint on [cronRunId, kind, name] allows upsert.
341
+
342
+ model AgentCronRunSkillUsage {
343
+ id String @id @default(cuid())
344
+ cronRunId String
345
+ kind String
346
+ name String
347
+ invocations Int @default(0)
348
+ turns Int @default(0)
349
+ inputTokens Int @default(0)
350
+ outputTokens Int @default(0)
351
+ cacheReadTokens Int @default(0)
352
+ cacheCreationTokens Int @default(0)
353
+ invokeContextDelta Int?
354
+ cronRun AgentCronRun @relation(fields: [cronRunId], references: [id], onDelete: Cascade)
355
+
356
+ @@unique([cronRunId, kind, name])
357
+ @@index([kind, name])
358
+ }
359
+
360
+ // ─── Agent Cron Run Model Breakdown ──────────────────────────────────────────
361
+ // Per-model token breakdown for a cron run. Populated when sub-agents use
362
+ // different models in a single run. The unique constraint on [cronRunId, model]
363
+ // allows upsert semantics.
364
+
365
+ model AgentCronRunModelBreakdown {
366
+ id String @id @default(cuid())
367
+ cronRunId String
368
+ model String
369
+ inputTokens Int @default(0)
370
+ outputTokens Int @default(0)
371
+ cacheReadTokens Int @default(0)
372
+ cacheCreationTokens Int @default(0)
373
+ costUsd Float @default(0)
374
+ cronRun AgentCronRun @relation(fields: [cronRunId], references: [id], onDelete: Cascade)
375
+
376
+ @@unique([cronRunId, model])
377
+ }
378
+
379
+ // ─── Agent Chat Token Usage (Daily, per Model) ───────────────────────────────
380
+ // Daily rollup table for Slack/chat session token usage per agent per model.
381
+ // Accumulated atomically via INSERT ... ON CONFLICT ... DO UPDATE (no read-modify-write).
382
+ // Totals and byAgent aggregations are computed by summing across models.
383
+
384
+ model AgentChatTokenUsageDailyByModel {
385
+ id String @id @default(cuid())
386
+ agentId String
387
+ agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
388
+ date String // YYYY-MM-DD
389
+ model String // e.g. "claude-sonnet-4-5"
390
+ inputTokens BigInt @default(0)
391
+ outputTokens BigInt @default(0)
392
+ cacheReadTokens BigInt @default(0)
393
+ cacheCreationTokens BigInt @default(0)
394
+ costUsd Float @default(0)
395
+ createdAt DateTime @default(now())
396
+ updatedAt DateTime @updatedAt
397
+
398
+ @@unique([agentId, date, model])
399
+ }
400
+
401
+ // ─── Agent Ask ──────────────────────────────────────────────────────────────
402
+ // Audit row per agent-to-agent ask (COS-31.1, POST /agents/:id/ask). Written
403
+ // before the chat call and updated after it. The question text is never
404
+ // stored. No relation to Agent on purpose: the audit trail outlives either
405
+ // agent. threadId is null until the chat thread is created (and stays null
406
+ // when that call fails). status: created | pending | replied | failed.
407
+
408
+ model AgentAsk {
409
+ id String @id @default(cuid())
410
+ fromAgentId String
411
+ toAgentId String
412
+ threadId String?
413
+ depth Int
414
+ waitMs Int
415
+ ttlSeconds Int
416
+ status String
417
+ callerLabel String
418
+ createdAt DateTime @default(now())
419
+ repliedAt DateTime?
420
+
421
+ @@index([toAgentId, createdAt])
422
+ @@index([fromAgentId, createdAt])
423
+ }
424
+
425
+ // ─── Agent GitHub Installations Snapshot ────────────────────────────────────
426
+ // One row per agent holding the agent's most recently reported GitHub App
427
+ // installations (owner, installation id, state, sanitized lastError). PUT
428
+ // replaces the whole list — a dropped installation disappears. No tokens or
429
+ // other credentials are ever stored here.
430
+
431
+ model AgentGitHubInstallationsSnapshot {
432
+ id String @id @default(cuid())
433
+ agentId String @unique
434
+ agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
435
+ reportedAt DateTime
436
+ installations Json // GitHubInstallation[]
437
+ createdAt DateTime @default(now())
438
+ }
439
+
440
+ // ─── Push Subscription ──────────────────────────────────────────────────────
441
+ // A single browser/device Web Push subscription (RFC 8291) belonging to a
442
+ // user. One user may have many rows (multi-device). Keyed by the unique push
443
+ // `endpoint`; `p256dh` and `auth` are the subscription's public key + auth
444
+ // secret used to encrypt the payload. `detailOptIn` is the USER'S opt-in for
445
+ // how much notification detail they want on a locked screen — the effective
446
+ // level is min(operator ceiling SHIPWRIGHT_ADMIN_PUSH_MAX_DETAIL, detailOptIn).
447
+ // Values: "generic" | "title" | "preview". Never carries an agentId — targeting
448
+ // is by userEmail via ChatThreadWatch, so a subscription is agent-agnostic.
449
+ model PushSubscription {
450
+ id String @id @default(cuid())
451
+ userEmail String
452
+ endpoint String @unique
453
+ p256dh String // base64url of the UA public key
454
+ auth String // base64url of the UA auth secret
455
+ detailOptIn String @default("generic") // user opt-in: generic | title | preview
456
+ createdAt DateTime @default(now())
457
+ updatedAt DateTime @updatedAt
458
+
459
+ @@index([userEmail])
460
+ }
461
+
462
+ // ─── Chat Thread Watch ──────────────────────────────────────────────────────
463
+ // Records that a user is "watching" a chat thread so the agent's reply can be
464
+ // targeted to exactly the right subscriber(s) without fanning out to every
465
+ // member of the agent. Upserted whenever a user sends a message in a thread.
466
+ // The chat Message model has no author column, and admin is the only web front
467
+ // door that knows both userEmail and threadId at send time — so authorship
468
+ // targeting lives here, not in the chat service.
469
+ model ChatThreadWatch {
470
+ id String @id @default(cuid())
471
+ userEmail String
472
+ threadId String
473
+ agentId String
474
+ createdAt DateTime @default(now())
475
+ updatedAt DateTime @updatedAt
476
+
477
+ @@unique([userEmail, threadId])
478
+ @@index([threadId])
479
+ }
480
+
481
+ // ─── User Notification Prefs ─────────────────────────────────────────────────
482
+ // One row per user, holding their notification preferences. `userEmail` is
483
+ // the primary key (one row per user, not per session). `autoFollowSessions`
484
+ // controls whether sessions *visible to* the user (any session in an agent or
485
+ // repo they can see — not only sessions they started) are auto-followed by
486
+ // session-alert-sweeper.ts; `autoFollowSince` records when that behavior was
487
+ // turned on (null if never explicitly set), and the sweeper skips
488
+ // auto-following sessions that were already waiting before that moment. When
489
+ // it is null the sweeper falls back to this row's `createdAt` as the boundary
490
+ // — `autoFollowSessions` defaults to true and a row is created just by loading
491
+ // the settings page, so "null" must not mean "no boundary", or the first sweep
492
+ // would back-follow and immediately push the whole waiting backlog. Rows that
493
+ // predate the sweeper were stamped once by
494
+ // 20260911000000_backfill_auto_follow_since.
495
+ // `reminderHourLocal` is validated to be an integer in
496
+ // [0, 23] at the service layer (session-follow-service.ts), not here — Prisma
497
+ // has no native CHECK constraint support in this schema style.
498
+ model UserNotificationPrefs {
499
+ userEmail String @id
500
+ autoFollowSessions Boolean @default(true)
501
+ reminderHourLocal Int @default(9)
502
+ autoFollowSince DateTime?
503
+ createdAt DateTime @default(now())
504
+ updatedAt DateTime @updatedAt
505
+ }
506
+
@@ -0,0 +1,34 @@
1
+ /**
2
+ * admin/prisma.config.ts
3
+ *
4
+ * Prisma 7 CLI configuration for the admin service.
5
+ *
6
+ * Prisma 7 removed `url` from the `datasource` block in the schema (prisma/schema/), so the
7
+ * connection string used by CLI commands (`prisma migrate deploy|dev`,
8
+ * `prisma db ...`) is declared here instead. The *runtime* client does not read
9
+ * this file at all — it connects through a `PrismaPg` driver adapter built in
10
+ * admin/src/prisma-client.ts.
11
+ *
12
+ * `datasource.url` is read straight off `process.env` rather than Prisma's
13
+ * `env()` helper on purpose: `env()` throws when the variable is unset, which
14
+ * would break `prisma generate` (run in `postinstall` and in admin/Dockerfile)
15
+ * in environments that legitimately have no database — generate needs the
16
+ * schema, not a connection. Leaving it `undefined` defers the failure to the
17
+ * commands that actually need a URL, which report it themselves.
18
+ *
19
+ * Paths are relative to this file (admin/), matching the existing
20
+ * `--schema=prisma/schema` invocations in package.json, Taskfile.yml,
21
+ * admin/Dockerfile, scripts/hitl.ts, and scripts/dev-tmux.ts.
22
+ */
23
+
24
+ import { defineConfig } from "prisma/config";
25
+
26
+ export default defineConfig({
27
+ schema: "prisma/schema",
28
+ migrations: {
29
+ path: "prisma/migrations",
30
+ },
31
+ datasource: {
32
+ url: process.env.DATABASE_URL_SHIPWRIGHT_ADMIN,
33
+ },
34
+ });
Binary file
Binary file
Binary file
@@ -0,0 +1,42 @@
1
+ /**
2
+ * admin/src/account-agent-create.ts
3
+ *
4
+ * Shared rules for self-serve agent creation by account users (SSP-4.2),
5
+ * used by both the admin UI form handler and the JSON API route so the two
6
+ * cannot drift: account users always create "coding" agents stamped with
7
+ * THEIR account (never a client-supplied one), and quota / account-status
8
+ * failures surface as clear messages.
9
+ */
10
+
11
+ import type { CallerScope } from "./caller-scope.ts";
12
+ import { resolveDefaultSpecialtyName } from "./specialty-manifest-loader.ts";
13
+
14
+ /**
15
+ * The only specialty account users may create: SHIPWRIGHT_SELF_SERVE_AGENT_TYPE
16
+ * when set, else the platform's default specialty (SHIPWRIGHT_DEFAULT_AGENT_TYPE,
17
+ * "coding" when unset).
18
+ */
19
+ export function getSelfServeSpecialty(
20
+ env: Record<string, string | undefined> = process.env,
21
+ ): string {
22
+ return (
23
+ env.SHIPWRIGHT_SELF_SERVE_AGENT_TYPE || resolveDefaultSpecialtyName(env)
24
+ );
25
+ }
26
+
27
+ /** Account the caller may create agents in, or null when they have none. */
28
+ export function accountIdFromScope(scope: CallerScope): string | null {
29
+ return scope.kind === "scoped" ? scope.accountId : null;
30
+ }
31
+
32
+ /** Clear, user-facing messages for the account-specific createAgent() errors. */
33
+ export const ACCOUNT_CREATE_ERROR_MESSAGES = {
34
+ quota_exceeded: "This account has reached its agent limit.",
35
+ account_inactive: "This account is not active.",
36
+ } as const;
37
+
38
+ export function isAccountCreateErrorCode(
39
+ code: string,
40
+ ): code is keyof typeof ACCOUNT_CREATE_ERROR_MESSAGES {
41
+ return code in ACCOUNT_CREATE_ERROR_MESSAGES;
42
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * admin/src/account-agent-create.unit.test.ts
3
+ * Unit tests for self-serve specialty resolution (AEP-1.2).
4
+ */
5
+
6
+ import { describe, expect, it } from "bun:test";
7
+ import { getSelfServeSpecialty } from "./account-agent-create.ts";
8
+
9
+ describe("getSelfServeSpecialty", () => {
10
+ it("defaults to coding", () => {
11
+ expect(getSelfServeSpecialty({})).toBe("coding");
12
+ expect(
13
+ getSelfServeSpecialty({ SHIPWRIGHT_DEFAULT_AGENT_TYPE: "fixture" }),
14
+ ).toBe("fixture");
15
+ expect(
16
+ getSelfServeSpecialty({
17
+ SHIPWRIGHT_DEFAULT_AGENT_TYPE: "fixture",
18
+ SHIPWRIGHT_SELF_SERVE_AGENT_TYPE: "other",
19
+ }),
20
+ ).toBe("other");
21
+ });
22
+
23
+ it("honours SHIPWRIGHT_SELF_SERVE_AGENT_TYPE", () => {
24
+ expect(
25
+ getSelfServeSpecialty({ SHIPWRIGHT_SELF_SERVE_AGENT_TYPE: "base" }),
26
+ ).toBe("base");
27
+ });
28
+ });