@app-vitals/agent-admin 0.0.1 → 0.1.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 (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,574 @@
1
+ /**
2
+ * admin/src/admin-boot-config.ts
3
+ *
4
+ * Env-driven configuration helpers for the admin platform boot (admin-boot.ts):
5
+ * the migration preflight, provisioner selection, Anthropic federation parsing,
6
+ * agent-deletion client selection, the readiness DB check and the sweeper
7
+ * interval resolvers. Pure (or early-return) logic kept apart from
8
+ * startAdmin()'s process wiring so it is unit-testable in isolation;
9
+ * main.ts re-exports the public ones (main.unit.test.ts imports them there).
10
+ */
11
+
12
+ import { join } from "node:path";
13
+ import type { PrismaClient } from "../prisma/client/client.ts";
14
+ import { AgentAnthropicIdentityService } from "./agent-anthropic-identity.ts";
15
+ import type { AgentAnthropicFederation } from "./agent-manifest.ts";
16
+ import type {
17
+ AgentProvisioner,
18
+ PerAgentIdentityConfig,
19
+ } from "./agent-provisioner.ts";
20
+ import {
21
+ KubernetesAgentProvisioner,
22
+ NoopAgentProvisioner,
23
+ } from "./agent-provisioner.ts";
24
+ import type { AgentTokenService } from "./agent-tokens.ts";
25
+ import { FederatedTokenProvider } from "./anthropic-federated-token-provider.ts";
26
+ import { HttpAnthropicIdentityClient } from "./anthropic-identity-client.ts";
27
+ import { AnthropicIdentityNameAllocator } from "./anthropic-identity-name.ts";
28
+ import type { ChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
29
+ import {
30
+ HttpChatServiceProvisioningClient,
31
+ NoopChatServiceProvisioningClient,
32
+ } from "./chat-service-provisioning-client.ts";
33
+ import { HttpKubernetesClient } from "./kubernetes-client.ts";
34
+ import type { ServiceIntegration } from "./service-integration.ts";
35
+
36
+ // ─── Migration preflight ──────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Runs `prisma migrate deploy` as a boot preflight.
40
+ * Idempotent — safe to call on every startup. Throws on migration failure.
41
+ *
42
+ * Exported for the early-return branch (DATABASE_URL_SHIPWRIGHT_ADMIN unset) to
43
+ * be unit-tested directly. The Bun.spawn path below it requires a real `bunx
44
+ * prisma migrate deploy` process and a live Postgres — that's process-wiring /
45
+ * real-I/O, exercised in deployed environments and the integration DB rather
46
+ * than a unit test.
47
+ */
48
+ export async function runMigrations(): Promise<void> {
49
+ const databaseUrl = process.env.DATABASE_URL_SHIPWRIGHT_ADMIN;
50
+ if (!databaseUrl) {
51
+ console.warn(
52
+ "[admin] DATABASE_URL_SHIPWRIGHT_ADMIN not set — skipping prisma migrate deploy",
53
+ );
54
+ return;
55
+ }
56
+
57
+ console.log("[admin] running prisma migrate deploy...");
58
+
59
+ const proc = Bun.spawn(
60
+ ["bunx", "prisma", "migrate", "deploy", "--schema=prisma/schema"],
61
+ {
62
+ cwd: join(import.meta.dir, ".."),
63
+ env: { ...process.env, DATABASE_URL_SHIPWRIGHT_ADMIN: databaseUrl },
64
+ stdout: "pipe",
65
+ stderr: "pipe",
66
+ },
67
+ );
68
+
69
+ const [stdout, stderr] = await Promise.all([
70
+ new Response(proc.stdout).text(),
71
+ new Response(proc.stderr).text(),
72
+ ]);
73
+
74
+ await proc.exited;
75
+
76
+ if (proc.exitCode !== 0) {
77
+ console.error("[admin] prisma migrate deploy failed:");
78
+ console.error(stderr);
79
+ throw new Error(`prisma migrate deploy exited with code ${proc.exitCode}`);
80
+ }
81
+
82
+ if (stdout.trim()) {
83
+ console.log("[admin]", stdout.trim());
84
+ }
85
+
86
+ console.log("[admin] migrations complete");
87
+ }
88
+
89
+ // ─── Provisioner selection ────────────────────────────────────────────────────
90
+
91
+ /**
92
+ * Select the agent provisioner from the environment.
93
+ *
94
+ * When `SHIPWRIGHT_K8S_PROVISIONING=enabled`, construct a real
95
+ * `KubernetesAgentProvisioner` that mints a per-agent token and creates the
96
+ * backing Secret + Deployment in the cluster. Otherwise (unset / any other
97
+ * value) return a `NoopAgentProvisioner` so create/delete behave exactly as
98
+ * before — no cluster required. This keeps the new wiring safe to deploy
99
+ * standalone behind the flag's default.
100
+ *
101
+ * Exported so its many env-driven branches (K8s provisioning flag, voice env
102
+ * passthrough, chat-service client construction, PVC name template)
103
+ * can be unit-tested directly with an injected/stub AgentTokenService, without
104
+ * needing a real Prisma connection or K8s cluster.
105
+ *
106
+ * `serviceIntegrations` are the extension-registered integrations (see
107
+ * service-integration.ts); the platform names none of them.
108
+ */
109
+ export function buildProvisioner(
110
+ env: NodeJS.ProcessEnv,
111
+ agentTokenService: AgentTokenService,
112
+ prisma?: PrismaClient,
113
+ serviceIntegrations: readonly ServiceIntegration[] = [],
114
+ ): AgentProvisioner {
115
+ if (env.SHIPWRIGHT_K8S_PROVISIONING !== "enabled") {
116
+ return new NoopAgentProvisioner();
117
+ }
118
+
119
+ const namespace = env.SHIPWRIGHT_K8S_NAMESPACE ?? "default";
120
+ const image = env.SHIPWRIGHT_AGENT_IMAGE ?? "";
121
+ const imageTag = env.SHIPWRIGHT_AGENT_IMAGE_TAG ?? "latest";
122
+ const apiUrl = env.SHIPWRIGHT_API_URL ?? "";
123
+ const replicasRaw = env.SHIPWRIGHT_AGENT_REPLICAS;
124
+ const replicas = replicasRaw ? Number(replicasRaw) : undefined;
125
+ const pvcStorageGiRaw = env.SHIPWRIGHT_AGENT_PVC_STORAGE_GI;
126
+ const pvcStorageGi = pvcStorageGiRaw ? Number(pvcStorageGiRaw) : undefined;
127
+ const pvcNameTemplate = env.SHIPWRIGHT_AGENT_PVC_NAME_TEMPLATE;
128
+
129
+ // Optional per-field container resource overrides. Resource quantities are
130
+ // strings (e.g. "500m", "2Gi") — no numeric parsing needed, unlike replicas/
131
+ // pvcStorageGi above. Each is passed through only when set (undefined, not
132
+ // an empty string, when unset) so resolveAgentContainerResources's default
133
+ // fallback applies field-by-field.
134
+ const cpuRequest = env.SHIPWRIGHT_K8S_AGENT_CPU_REQUEST;
135
+ const cpuLimit = env.SHIPWRIGHT_K8S_AGENT_CPU_LIMIT;
136
+ const memoryRequest = env.SHIPWRIGHT_K8S_AGENT_MEMORY_REQUEST;
137
+ const memoryLimit = env.SHIPWRIGHT_K8S_AGENT_MEMORY_LIMIT;
138
+ const ephemeralStorage = env.SHIPWRIGHT_K8S_AGENT_EPHEMERAL_STORAGE;
139
+ const resources = {
140
+ ...(cpuRequest ? { cpuRequest } : {}),
141
+ ...(cpuLimit ? { cpuLimit } : {}),
142
+ ...(memoryRequest ? { memoryRequest } : {}),
143
+ ...(memoryLimit ? { memoryLimit } : {}),
144
+ ...(ephemeralStorage ? { ephemeralStorage } : {}),
145
+ };
146
+
147
+ // Agent-voice (STT/TTS) env flowed into provisioned agent pods. The chart's
148
+ // voice Secret + Whisper Service URL land in the admin's env when
149
+ // agent.voice.enabled; absent → voice disabled and no voice env is injected.
150
+ const voice = {
151
+ ...(env.WHISPER_SERVICE_URL
152
+ ? { whisperServiceUrl: env.WHISPER_SERVICE_URL }
153
+ : {}),
154
+ ...(env.GROQ_API_KEY ? { groqApiKey: env.GROQ_API_KEY } : {}),
155
+ ...(env.ELEVENLABS_API_KEY
156
+ ? { elevenLabsApiKey: env.ELEVENLABS_API_KEY }
157
+ : {}),
158
+ ...(env.ELEVENLABS_VOICE_ID ? { voiceId: env.ELEVENLABS_VOICE_ID } : {}),
159
+ };
160
+
161
+ // Claude Code managed-policy ConfigMap. Empty/unset name disables the
162
+ // feature; the hash is optional and only stamped on pods when non-empty.
163
+ const claudePolicyConfigMap =
164
+ env.SHIPWRIGHT_K8S_AGENT_CLAUDE_POLICY_CONFIGMAP;
165
+ const claudePolicyHash = env.SHIPWRIGHT_K8S_AGENT_CLAUDE_POLICY_HASH;
166
+ const claudePolicy = claudePolicyConfigMap
167
+ ? {
168
+ configMapName: claudePolicyConfigMap,
169
+ ...(claudePolicyHash ? { hash: claudePolicyHash } : {}),
170
+ }
171
+ : undefined;
172
+
173
+ // Anthropic workload identity federation. In shared mode an unset rule ID
174
+ // disables it; a partial config throws here so admin startup fails with a
175
+ // clear message. Shared mode passes the one shared identity; perAgent mode
176
+ // passes what the provisioner needs to create each agent's own identity.
177
+ const parsedFederation = parseAnthropicFederation(env);
178
+ let anthropicFederation: AgentAnthropicFederation | undefined;
179
+ let perAgentIdentity: PerAgentIdentityConfig | undefined;
180
+ if (parsedFederation?.mode === "shared") {
181
+ const { mode: _mode, ...shared } = parsedFederation;
182
+ anthropicFederation = shared;
183
+ } else if (parsedFederation?.mode === "perAgent") {
184
+ perAgentIdentity = buildPerAgentIdentity(env, parsedFederation, prisma);
185
+ }
186
+
187
+ const chatServiceUrl = env.SHIPWRIGHT_CHAT_SERVICE_URL;
188
+ const chatServiceAdminToken = env.SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN;
189
+ const chatService =
190
+ chatServiceUrl && chatServiceAdminToken
191
+ ? new HttpChatServiceProvisioningClient(
192
+ chatServiceUrl,
193
+ chatServiceAdminToken,
194
+ )
195
+ : undefined;
196
+
197
+ const k8s = new HttpKubernetesClient();
198
+
199
+ return new KubernetesAgentProvisioner(k8s, agentTokenService, {
200
+ namespace,
201
+ image,
202
+ imageTag,
203
+ apiUrl,
204
+ ...(replicas !== undefined && Number.isFinite(replicas)
205
+ ? { replicas }
206
+ : {}),
207
+ ...(pvcStorageGi !== undefined && Number.isFinite(pvcStorageGi)
208
+ ? { pvcStorageGi }
209
+ : {}),
210
+ ...(Object.keys(resources).length > 0 ? { resources } : {}),
211
+ ...(Object.keys(voice).length > 0 ? { voice } : {}),
212
+ ...(claudePolicy ? { claudePolicy } : {}),
213
+ ...(anthropicFederation ? { anthropicFederation } : {}),
214
+ ...(perAgentIdentity ? { perAgentIdentity } : {}),
215
+ // When SHIPWRIGHT_AGENT_PVC_NAME_TEMPLATE is set (e.g. "acme-agent-{name}-home"),
216
+ // substitute {name} with the pre-sanitized name resolved by pvcNameFor()
217
+ // (slug sanitized via sanitizeAgentName, or falls back to resourceName).
218
+ // When unset, the provisioner uses the default `${resourceName}-home` naming.
219
+ ...(pvcNameTemplate
220
+ ? {
221
+ pvcName: (name: string) => pvcNameTemplate.replace("{name}", name),
222
+ }
223
+ : {}),
224
+ // Extension-registered service integrations (e.g. task-store): each one
225
+ // with a URL mints a per-agent token on provision (before chat) and injects
226
+ // it into the agent Secret + Deployment.
227
+ ...(serviceIntegrations.length > 0 ? { serviceIntegrations } : {}),
228
+ // Chat-service token minting: when both SHIPWRIGHT_CHAT_SERVICE_URL and
229
+ // SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN are set, mint a per-agent token on
230
+ // provision and inject it into the agent Secret + Deployment.
231
+ ...(chatService ? { chatService, chatServiceUrl } : {}),
232
+ });
233
+ }
234
+
235
+ /**
236
+ * The perAgent provisioner deps. The admin pod signs in to the Anthropic Admin
237
+ * API by federation with its own projected token and bootstrap rule (the
238
+ * chart renders ANTHROPIC_IDENTITY_TOKEN_FILE / _FEDERATION_RULE_ID /
239
+ * _ORGANIZATION_ID, plus _SERVICE_ACCOUNT_ID / _WORKSPACE_ID, in perAgent
240
+ * mode); identity IDs and names are kept on the Agent row.
241
+ * @throws when the admin sign-in env or the database client is missing
242
+ */
243
+ function buildPerAgentIdentity(
244
+ env: NodeJS.ProcessEnv,
245
+ federation: PerAgentAnthropicFederation,
246
+ prisma: PrismaClient | undefined,
247
+ ): PerAgentIdentityConfig {
248
+ const required = [
249
+ "ANTHROPIC_IDENTITY_TOKEN_FILE",
250
+ "ANTHROPIC_FEDERATION_RULE_ID",
251
+ "ANTHROPIC_ORGANIZATION_ID",
252
+ ] as const;
253
+ const missing = required.filter((name) => !env[name]);
254
+ if (missing.length > 0) {
255
+ throw new Error(
256
+ `${FEDERATION_ENV_PREFIX}IDENTITY_MODE is perAgent but the admin's own Anthropic sign-in is not configured: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} missing.`,
257
+ );
258
+ }
259
+ if (!prisma) {
260
+ throw new Error(
261
+ "perAgent identity mode needs a database client to save agent identities.",
262
+ );
263
+ }
264
+ const tokenProvider = new FederatedTokenProvider({
265
+ fetchFn: fetch,
266
+ tokenFilePath: env.ANTHROPIC_IDENTITY_TOKEN_FILE as string,
267
+ federationRuleId: env.ANTHROPIC_FEDERATION_RULE_ID as string,
268
+ organizationId: env.ANTHROPIC_ORGANIZATION_ID as string,
269
+ ...(env.ANTHROPIC_SERVICE_ACCOUNT_ID
270
+ ? { serviceAccountId: env.ANTHROPIC_SERVICE_ACCOUNT_ID }
271
+ : {}),
272
+ ...(env.ANTHROPIC_WORKSPACE_ID
273
+ ? { workspaceId: env.ANTHROPIC_WORKSPACE_ID }
274
+ : {}),
275
+ });
276
+ return {
277
+ client: new HttpAnthropicIdentityClient({ fetchFn: fetch, tokenProvider }),
278
+ identities: new AgentAnthropicIdentityService(prisma),
279
+ names: new AnthropicIdentityNameAllocator(prisma, {
280
+ suffix: federation.nameSuffix,
281
+ }),
282
+ organizationId: federation.organizationId,
283
+ ...(federation.workspaceId ? { workspaceId: federation.workspaceId } : {}),
284
+ issuerId: federation.issuerId,
285
+ audience: federation.audience,
286
+ expirationSeconds: federation.expirationSeconds,
287
+ };
288
+ }
289
+
290
+ const FEDERATION_ENV_PREFIX = "SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_";
291
+
292
+ /** Resolve the identity mode (default "shared"); unknown values throw. */
293
+ function resolveFederationIdentityMode(
294
+ env: NodeJS.ProcessEnv,
295
+ ): "shared" | "perAgent" {
296
+ const modeName = `${FEDERATION_ENV_PREFIX}IDENTITY_MODE`;
297
+ const mode = env[modeName] || "shared";
298
+ if (mode !== "shared" && mode !== "perAgent") {
299
+ throw new Error(
300
+ `${modeName} must be "shared" or "perAgent", got "${mode}".`,
301
+ );
302
+ }
303
+ return mode;
304
+ }
305
+
306
+ /**
307
+ * Whether Anthropic workload identity federation is active for provisioned
308
+ * agent pods (WIF-2.1, PAI-2.1). Derived from the identity mode, mirroring
309
+ * parseAnthropicFederation's gating without its completeness checks (those
310
+ * throw at buildProvisioner time): a deployment that provisions pods AND is in
311
+ * perAgent mode, or in shared mode with the rule ID set, federates agents.
312
+ * Gates the Anthropic credential write/read guards.
313
+ */
314
+ export function isAnthropicFederationEnabled(env: NodeJS.ProcessEnv): boolean {
315
+ if (env.SHIPWRIGHT_K8S_PROVISIONING !== "enabled") return false;
316
+ return (
317
+ resolveFederationIdentityMode(env) === "perAgent" ||
318
+ Boolean(env[`${FEDERATION_ENV_PREFIX}RULE_ID`])
319
+ );
320
+ }
321
+
322
+ /** Kubernetes rejects projected service-account tokens shorter than this. */
323
+ const MIN_FEDERATION_TOKEN_EXPIRATION_SECONDS = 600;
324
+
325
+ /** Charset for the per-agent name suffix (Kubernetes / Anthropic-safe). */
326
+ const FEDERATION_NAME_SUFFIX_PATTERN = /^[a-z0-9-]*$/;
327
+
328
+ /** perAgent identity mode: every agent gets its own SA and federation rule. */
329
+ export interface PerAgentAnthropicFederation {
330
+ mode: "perAgent";
331
+ organizationId: string;
332
+ workspaceId?: string;
333
+ /** Anthropic issuer ID of this cluster (the per-agent rules' issuer). */
334
+ issuerId: string;
335
+ /** Appended to per-agent resource names so clusters sharing an org don't collide. */
336
+ nameSuffix?: string;
337
+ audience: string;
338
+ expirationSeconds: number;
339
+ }
340
+
341
+ /** Discriminated by `mode`; `undefined` (from the parser) means disabled. */
342
+ export type ParsedAnthropicFederation =
343
+ | ({ mode: "shared" } & AgentAnthropicFederation)
344
+ | PerAgentAnthropicFederation;
345
+
346
+ /**
347
+ * Parse the Anthropic Workload Identity Federation config for provisioned agent
348
+ * pods. SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_IDENTITY_MODE selects the
349
+ * mode (default "shared"; unknown values throw).
350
+ *
351
+ * shared: an unset/empty RULE_ID disables the feature (returns undefined, other
352
+ * federation vars ignored). With the rule ID set, the agent ServiceAccount,
353
+ * organization ID and service account ID are required.
354
+ *
355
+ * perAgent: requires ORGANIZATION_ID and ISSUER_ID; the shared rule/service
356
+ * account are not used (per-agent identities are created at provision time).
357
+ *
358
+ * A partial config throws so startup fails loudly instead of provisioning pods
359
+ * that cannot authenticate.
360
+ */
361
+ export function parseAnthropicFederation(
362
+ env: NodeJS.ProcessEnv,
363
+ ): ParsedAnthropicFederation | undefined {
364
+ const prefix = FEDERATION_ENV_PREFIX;
365
+ const mode = resolveFederationIdentityMode(env);
366
+
367
+ const ruleId = env[`${prefix}RULE_ID`];
368
+ if (mode === "shared" && !ruleId) return undefined;
369
+
370
+ const required =
371
+ mode === "shared"
372
+ ? ({
373
+ serviceAccountName: "SHIPWRIGHT_K8S_AGENT_SERVICE_ACCOUNT",
374
+ organizationId: `${prefix}ORGANIZATION_ID`,
375
+ serviceAccountId: `${prefix}SERVICE_ACCOUNT_ID`,
376
+ } as const)
377
+ : ({
378
+ organizationId: `${prefix}ORGANIZATION_ID`,
379
+ issuerId: `${prefix}ISSUER_ID`,
380
+ } as const);
381
+ const missing = Object.values(required).filter((name) => !env[name]);
382
+ if (missing.length > 0) {
383
+ const verb = missing.length === 1 ? "is" : "are";
384
+ throw new Error(
385
+ mode === "shared"
386
+ ? `Anthropic workload identity federation is partially configured: ${prefix}RULE_ID is set but ${missing.join(", ")} ${verb} missing. Set all of them, or unset ${prefix}RULE_ID to disable federation.`
387
+ : `Anthropic workload identity federation is partially configured: ${prefix}IDENTITY_MODE is perAgent but ${missing.join(", ")} ${verb} missing.`,
388
+ );
389
+ }
390
+
391
+ const expirationName = `${prefix}TOKEN_EXPIRATION_SECONDS`;
392
+ const expirationRaw = env[expirationName];
393
+ const expirationSeconds = expirationRaw
394
+ ? Number(expirationRaw)
395
+ : MIN_FEDERATION_TOKEN_EXPIRATION_SECONDS;
396
+ if (
397
+ !Number.isInteger(expirationSeconds) ||
398
+ expirationSeconds < MIN_FEDERATION_TOKEN_EXPIRATION_SECONDS
399
+ ) {
400
+ throw new Error(
401
+ `${expirationName} must be an integer >= ${MIN_FEDERATION_TOKEN_EXPIRATION_SECONDS} (Kubernetes minimum for projected tokens), got "${expirationRaw}".`,
402
+ );
403
+ }
404
+
405
+ const workspaceId = env[`${prefix}WORKSPACE_ID`];
406
+ const audience = env[`${prefix}AUDIENCE`] || "https://api.anthropic.com";
407
+
408
+ if (mode === "perAgent") {
409
+ const nameSuffix = env[`${prefix}NAME_SUFFIX`];
410
+ if (nameSuffix && !FEDERATION_NAME_SUFFIX_PATTERN.test(nameSuffix)) {
411
+ throw new Error(
412
+ `${prefix}NAME_SUFFIX must match ${FEDERATION_NAME_SUFFIX_PATTERN}, got "${nameSuffix}".`,
413
+ );
414
+ }
415
+ return {
416
+ mode,
417
+ organizationId: env[`${prefix}ORGANIZATION_ID`] as string,
418
+ ...(workspaceId ? { workspaceId } : {}),
419
+ issuerId: env[`${prefix}ISSUER_ID`] as string,
420
+ ...(nameSuffix ? { nameSuffix } : {}),
421
+ audience,
422
+ expirationSeconds,
423
+ };
424
+ }
425
+
426
+ return {
427
+ mode,
428
+ serviceAccountName: env.SHIPWRIGHT_K8S_AGENT_SERVICE_ACCOUNT as string,
429
+ ruleId: ruleId as string,
430
+ organizationId: env[`${prefix}ORGANIZATION_ID`] as string,
431
+ serviceAccountId: env[`${prefix}SERVICE_ACCOUNT_ID`] as string,
432
+ ...(workspaceId ? { workspaceId } : {}),
433
+ audience,
434
+ expirationSeconds,
435
+ };
436
+ }
437
+
438
+ // ─── Agent-deletion client selection ──────────────────────────────────────────
439
+
440
+ /**
441
+ * Select the chat-service client used by DELETE /agents/:id's
442
+ * `deleteAgentFully()` orchestration (agent-deletion.ts).
443
+ *
444
+ * Independent of `buildProvisioner`/`SHIPWRIGHT_K8S_PROVISIONING`: unlike the
445
+ * token-minting clients constructed inside that function's closure (only
446
+ * built when K8s provisioning is enabled), agent deletion must be able to
447
+ * revoke chat-service tokens regardless of whether K8s
448
+ * provisioning is on — e.g. a local dev setup with provisioning disabled but
449
+ * a real chat-service still running. Extension-registered integrations
450
+ * (task-store) come from AdminExtension.serviceIntegrations instead.
451
+ *
452
+ * Gated on the same env-var pairs as `buildProvisioner` (URL + admin token
453
+ * both set) so the "unconfigured" case is unambiguous. When unconfigured,
454
+ * returns the No-op client rather than `undefined` — constructing the
455
+ * client is cheap (no I/O happens until a call is made), so this avoids
456
+ * making `chatService` optional in `AdminDeps`/`DeleteAgentFullyDeps`
457
+ * for the common case (K8s provisioning disabled, e.g. local dev): a
458
+ * deleteAgentFully() call with nothing to revoke (listTokensForAgent → [])
459
+ * completes cleanly instead of surfacing a new error path.
460
+ */
461
+ export function buildDeletionClients(env: NodeJS.ProcessEnv): {
462
+ chatService: ChatServiceProvisioningClient;
463
+ } {
464
+ const chatServiceUrl = env.SHIPWRIGHT_CHAT_SERVICE_URL;
465
+ const chatServiceAdminToken = env.SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN;
466
+ const chatService =
467
+ chatServiceUrl && chatServiceAdminToken
468
+ ? new HttpChatServiceProvisioningClient(
469
+ chatServiceUrl,
470
+ chatServiceAdminToken,
471
+ )
472
+ : new NoopChatServiceProvisioningClient();
473
+
474
+ return { chatService };
475
+ }
476
+
477
+ // ─── Readiness check ──────────────────────────────────────────────────────────
478
+
479
+ /**
480
+ * DB-aware readiness check backing GET /health/ready. Runs a lightweight
481
+ * `SELECT 1` and reports whether Postgres is actually reachable — unlike
482
+ * GET /health (liveness), which stays DB-independent so a transient DB blip
483
+ * never triggers a liveness-driven pod restart. Takes only the `$queryRaw`
484
+ * slice of PrismaClient so it's unit-testable with a mocked/injected client,
485
+ * no real I/O.
486
+ */
487
+ export async function checkDbReady(prisma: {
488
+ $queryRaw: (
489
+ query: TemplateStringsArray,
490
+ ...values: unknown[]
491
+ ) => Promise<unknown>;
492
+ }): Promise<boolean> {
493
+ try {
494
+ await prisma.$queryRaw`SELECT 1`;
495
+ return true;
496
+ } catch {
497
+ return false;
498
+ }
499
+ }
500
+
501
+ // ─── Server config ────────────────────────────────────────────────────────────
502
+
503
+ export const DEFAULT_PORT = 3000;
504
+
505
+ /**
506
+ * Resolve the public read-only task board repo from the environment.
507
+ * Reads SHIPWRIGHT_ADMIN_PUBLIC_REPO; trims whitespace so a blank/whitespace
508
+ * value behaves like unset (returns undefined → board stays in degraded mode)
509
+ * rather than producing an empty repo filter.
510
+ */
511
+ export function resolvePublicRepo(
512
+ env: Record<string, string | undefined>,
513
+ ): string | undefined {
514
+ return env.SHIPWRIGHT_ADMIN_PUBLIC_REPO?.trim() || undefined;
515
+ }
516
+
517
+ /** Default cadence of the session-alert sweeper — matches chat's stall reaper. */
518
+ export const DEFAULT_SESSION_ALERT_INTERVAL_MS = 60_000;
519
+
520
+ /**
521
+ * Hard timeout on the sweeper's task-store `GET /sessions` calls. Longer than
522
+ * the repo's usual 5s auth-lookup timeout because this fetches up to 500
523
+ * sessions, but still well inside the default 60s tick so a stalled
524
+ * task-store can't wedge the sweep loop behind its in-flight guard.
525
+ */
526
+ export const SESSION_ALERT_FETCH_TIMEOUT_MS = 10_000;
527
+
528
+ /**
529
+ * Resolve the session-alert sweeper's tick interval from the environment.
530
+ * Reads SHIPWRIGHT_ADMIN_SESSION_ALERT_INTERVAL_MS; anything unset, blank,
531
+ * non-numeric, or non-positive falls back to the default rather than
532
+ * producing a setInterval that spins (0/NaN) or never fires.
533
+ */
534
+ export function resolveSessionAlertIntervalMs(
535
+ env: Record<string, string | undefined>,
536
+ ): number {
537
+ const raw = env.SHIPWRIGHT_ADMIN_SESSION_ALERT_INTERVAL_MS?.trim();
538
+ if (!raw) return DEFAULT_SESSION_ALERT_INTERVAL_MS;
539
+ const parsed = Number(raw);
540
+ if (!Number.isFinite(parsed) || parsed <= 0) {
541
+ return DEFAULT_SESSION_ALERT_INTERVAL_MS;
542
+ }
543
+ return parsed;
544
+ }
545
+
546
+ /**
547
+ * Cadence of the trial-expiry *warning* sweeper (ATE-2.1). Coarser than the
548
+ * session-alert sweeper's 60s default: a trial-expiry warning window is
549
+ * measured in days, not seconds, so hourly polling still leaves enormous
550
+ * margin against ever missing the window.
551
+ *
552
+ * Distinct from trial-expiry-sweeper.ts's own
553
+ * DEFAULT_TRIAL_EXPIRY_SWEEP_INTERVAL_MS (60s), which paces the ATE-3.1
554
+ * lockdown sweeper — that one should react promptly once a trial lapses,
555
+ * whereas this one only has to land somewhere inside a multi-day window.
556
+ */
557
+ export const DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS = 60 * 60 * 1000;
558
+
559
+ /**
560
+ * Resolve the trial-expiry sweeper's warning window (in days) from the
561
+ * environment. Reads SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_WARNING_DAYS; anything
562
+ * unset, blank, non-numeric, or non-positive falls back to the sweeper's own
563
+ * default (DEFAULT_TRIAL_EXPIRY_WARNING_DAYS) rather than disabling the
564
+ * check or warning agents on every single tick.
565
+ */
566
+ export function resolveTrialExpiryWarningDays(
567
+ env: Record<string, string | undefined>,
568
+ ): number | undefined {
569
+ const raw = env.SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_WARNING_DAYS?.trim();
570
+ if (!raw) return undefined;
571
+ const parsed = Number(raw);
572
+ if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
573
+ return parsed;
574
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { buildDeletionClients } from "./admin-boot-config.ts";
3
+ import {
4
+ HttpChatServiceProvisioningClient,
5
+ NoopChatServiceProvisioningClient,
6
+ } from "./chat-service-provisioning-client.ts";
7
+
8
+ describe("buildDeletionClients", () => {
9
+ it("returns a no-op client when nothing is configured", () => {
10
+ const { chatService } = buildDeletionClients({});
11
+ expect(chatService).toBeInstanceOf(NoopChatServiceProvisioningClient);
12
+ });
13
+
14
+ it("needs both the URL and the admin token for the real client", () => {
15
+ const { chatService } = buildDeletionClients({
16
+ SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN: "chat-admin",
17
+ });
18
+ expect(chatService).toBeInstanceOf(NoopChatServiceProvisioningClient);
19
+ });
20
+
21
+ it("builds the real client when fully configured, independent of K8s provisioning", () => {
22
+ const { chatService } = buildDeletionClients({
23
+ SHIPWRIGHT_CHAT_SERVICE_URL: "http://chat.test",
24
+ SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN: "chat-admin",
25
+ });
26
+ expect(chatService).toBeInstanceOf(HttpChatServiceProvisioningClient);
27
+ });
28
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { join } from "node:path";
3
+
4
+ // admin-boot.ts holds the boot sequence and main.ts is the composition root
5
+ // that runs it. Calling startAdmin() would run migrations, open a DB pool and
6
+ // bind a socket, so both are asserted structurally against their source text
7
+ // (mirrors agent/src/index.boot-order.unit.test.ts).
8
+ const source = await Bun.file(join(import.meta.dir, "admin-boot.ts")).text();
9
+ const entrySource = await Bun.file(join(import.meta.dir, "main.ts")).text();
10
+
11
+ function position(marker: string): number {
12
+ const at = source.indexOf(marker);
13
+ expect(
14
+ at,
15
+ `boot marker missing from admin-boot.ts: ${marker}`,
16
+ ).toBeGreaterThan(-1);
17
+ return at;
18
+ }
19
+
20
+ function assertOrdered(markers: string[]): void {
21
+ const positions = markers.map(position);
22
+ expect(positions).toEqual([...positions].sort((a, b) => a - b));
23
+ }
24
+
25
+ describe("admin-boot.ts boot sequence", () => {
26
+ it("keeps the documented boot order: DB, services, routes, sweepers, serve, startup policy reconcile", () => {
27
+ assertOrdered([
28
+ 'initSentry({ service: "admin" })',
29
+ "await runMigrations();",
30
+ "createAdminPrismaClient(",
31
+ "makeTokenCrypto()",
32
+ "new AgentService(prisma)",
33
+ "const provisioner = buildProvisioner(",
34
+ "const resolvedExtensions = resolveAdminExtensions(",
35
+ 'root.get("/health"',
36
+ 'root.route("/agents", runtimeApp)',
37
+ 'root.route("/", adminApiApp)',
38
+ "createAccountsApp(",
39
+ "mountRouteContributions(root, resolvedExtensions.apiRoutes)",
40
+ 'root.route("/", adminUIApp)',
41
+ "mountRouteContributions(root, resolvedExtensions.adminUiRoutes)",
42
+ "new TrialExpiryWarningSweeper(",
43
+ "new TrialExpirySweeper(",
44
+ "new AccountTrialWarningSweeper(",
45
+ "new AccountTrialExpirySweeper(",
46
+ "startBackgroundPasses(resolvedExtensions.backgroundPasses)",
47
+ "const server = Bun.serve(",
48
+ "const shutdown = registerGracefulShutdown(",
49
+ "void runStartupPolicyReconcile(",
50
+ ]);
51
+ });
52
+
53
+ it("feeds extension service deps into the platform app factories", () => {
54
+ expect(source).toContain("...resolvedExtensions.apiDeps");
55
+ expect(source).toContain("...resolvedExtensions.uiDeps");
56
+ });
57
+
58
+ it("only passes agent-detail card slots when an extension contributed some", () => {
59
+ expect(source).toContain("resolvedExtensions.agentDetailCards.length > 0");
60
+ });
61
+
62
+ it("keeps the platform boot free of any coding extension import", () => {
63
+ expect(source).not.toContain("coding-admin-extension");
64
+ expect(source).not.toContain("./coding/");
65
+ expect(source).not.toContain("AgentPhaseMethodologyService");
66
+ });
67
+
68
+ it("exports startAdmin(extensions) from admin-boot.ts", () => {
69
+ expect(source).toContain("export async function startAdmin(");
70
+ });
71
+ });
72
+
73
+ describe("main.ts composition root", () => {
74
+ it("builds no services, routes or sweepers itself", () => {
75
+ for (const forbidden of [
76
+ "new Hono(",
77
+ "const server = Bun.serve(",
78
+ "setInterval(",
79
+ "createAdminPrismaClient(",
80
+ "new AgentService(",
81
+ ]) {
82
+ expect(entrySource).not.toContain(forbidden);
83
+ }
84
+ expect(entrySource.split("\n").length).toBeLessThan(80);
85
+ });
86
+
87
+ it("runs only when invoked as the main entry (import-safe)", () => {
88
+ expect(entrySource).toContain("if (import.meta.main) {");
89
+ expect(entrySource).toContain('"[admin] fatal startup error:"');
90
+ });
91
+ });