@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,690 @@
1
+ /**
2
+ * admin/src/admin-boot.ts
3
+ *
4
+ * Admin platform boot sequence, exported as startAdmin(extensions). A consumer
5
+ * entry (admin/src/main.ts for Shipwright) passes its AdminExtensions; with
6
+ * none (or hookless ones) this is exactly the bare admin platform. See
7
+ * admin-extension.ts for the seam and where each hook lands in the sequence.
8
+ *
9
+ * Boot order (pinned by admin-boot.boot-order.unit.test.ts):
10
+ * Sentry → migrations → Prisma → platform services → resolve extensions →
11
+ * routes (health, runtime API, admin API, accounts API, extension APIs,
12
+ * admin UI, extension UI) → platform sweepers → extension background
13
+ * passes → Bun.serve → graceful shutdown → startup policy reconcile.
14
+ *
15
+ * Route mount order (important — avoids shadowing):
16
+ * GET /health — liveness check (no auth, DB-independent)
17
+ * GET /health/ready — readiness check (no auth, DB-aware)
18
+ * * /agents/* — runtime API (admin key | per-agent bearer | session JWT)
19
+ * + admin CRUD API (admin key | per-agent bearer | session JWT)
20
+ * * /admin/* — admin UI (session JWT)
21
+ *
22
+ * Both runtimeApp and adminApiApp serve routes under /agents/*.
23
+ * They use non-overlapping sub-paths so Hono resolves them correctly:
24
+ * runtime → GET /agents/:id/config, GET /agents/:id/crons
25
+ * admin → GET /agents/:id/crons/summary (enriched with run stats)
26
+ * POST/PATCH/DELETE /agents/:id/envs, /crons, /tools, /tokens, /plugins
27
+ *
28
+ * This file must not import any coding module — main.ts is the only file
29
+ * that wires a concrete extension in.
30
+ */
31
+
32
+ import { join } from "node:path";
33
+ import { registerGracefulShutdown } from "@app-vitals/agent-lib/graceful-shutdown";
34
+ import {
35
+ type PoolLike,
36
+ routeContextMiddleware,
37
+ } from "@app-vitals/agent-lib/pool-observability";
38
+ import {
39
+ buildSentryInitOptions,
40
+ initSentry,
41
+ } from "@app-vitals/agent-lib/sentry";
42
+ import * as Sentry from "@sentry/bun";
43
+ import { sentry } from "@sentry/hono/bun";
44
+ import { Hono } from "hono";
45
+ import { createAccountCreatedNotifier } from "./account-created-notifier.ts";
46
+ import { AccountInviteService } from "./account-invites.ts";
47
+ import { AccountLifecycle } from "./account-lifecycle.ts";
48
+ import { AccountMemberService } from "./account-members.ts";
49
+ import { AccountOnboardingService } from "./account-onboarding.ts";
50
+ import { AccountTrialExpirySweeper } from "./account-trial-expiry-sweeper.ts";
51
+ import { AccountTrialWarningSweeper } from "./account-trial-warning-sweeper.ts";
52
+ import { AccountService } from "./accounts.ts";
53
+ import { createAccountsApp } from "./accounts-api.ts";
54
+ import {
55
+ buildDeletionClients,
56
+ buildProvisioner,
57
+ checkDbReady,
58
+ DEFAULT_PORT,
59
+ DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS,
60
+ isAnthropicFederationEnabled,
61
+ resolveTrialExpiryWarningDays,
62
+ runMigrations,
63
+ } from "./admin-boot-config.ts";
64
+ import {
65
+ type AdminExtension,
66
+ type AdminExtensionContext,
67
+ collectServiceIntegrations,
68
+ mountRouteContributions,
69
+ resolveAdminExtensions,
70
+ startBackgroundPasses,
71
+ } from "./admin-extension.ts";
72
+ import { createAdminUIApp } from "./admin-ui.ts";
73
+ import { AgentAskService } from "./agent-ask.ts";
74
+ import { parseAgentAskConfig } from "./agent-ask-config.ts";
75
+ import { AgentChatTokenService } from "./agent-chat-tokens.ts";
76
+ import { AgentCronJobService } from "./agent-cron-jobs.ts";
77
+ import { AgentCronRunStatsService } from "./agent-cron-run-stats.ts";
78
+ import { AgentCronRunService } from "./agent-cron-runs.ts";
79
+ import { AgentEnvService } from "./agent-envs.ts";
80
+ import { AgentGitHubInstallationsService } from "./agent-github-installations.ts";
81
+ import { AgentMemberService } from "./agent-members.ts";
82
+ import { AgentPluginService } from "./agent-plugins.ts";
83
+ import { AgentTokenService } from "./agent-tokens.ts";
84
+ import { AgentToolService } from "./agent-tools.ts";
85
+ import { AgentService } from "./agents.ts";
86
+ import { createAdminApp, parseAdminApiKeys } from "./agents-api.ts";
87
+ import { createAgentRuntimeApp } from "./api.ts";
88
+ import { createAdminAuthMiddleware } from "./api-auth.ts";
89
+ import {
90
+ callerScopeDepsFromServices,
91
+ createCallerScopeResolver,
92
+ } from "./caller-scope.ts";
93
+ import { parseChatCredentialConfig } from "./chat-credential-config.ts";
94
+ import { HttpChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
95
+ import { isDevAuthAllowed } from "./dev-auth-guard.ts";
96
+ import { HttpGithubAppProvisioningClient } from "./github-app-provisioning-client.ts";
97
+ import { HttpGoogleAuthClient } from "./google-auth-client.ts";
98
+ import { HttpChatClient } from "./http-chat-client.ts";
99
+ import { HttpOktaAuthClient } from "./okta-auth-client.ts";
100
+ import { mountPlatformDoc } from "./platform-doc-route.ts";
101
+ import { createAdminPrismaClient } from "./prisma-client.ts";
102
+ import { isPushEnabled } from "./push-sender.ts";
103
+ import { PushService } from "./push-service.ts";
104
+ import { parseSelfServeConfig } from "./self-serve-config.ts";
105
+ import { HttpSlackProvisioningClient } from "./slack-provisioning-client.ts";
106
+ import { SpecialtyRegistry } from "./specialty-manifest-loader.ts";
107
+ import { runStartupPolicyReconcile } from "./startup-policy-reconcile.ts";
108
+ import { makeTokenCrypto } from "./token-crypto.ts";
109
+ import {
110
+ resolveTrialExpirySweepIntervalMs,
111
+ TrialExpirySweeper,
112
+ } from "./trial-expiry-sweeper.ts";
113
+ import { TrialExpiryWarningSweeper } from "./trial-expiry-warning-sweeper.ts";
114
+
115
+ // startAdmin() is the process boot: it runs the real migration preflight,
116
+ // constructs a real PrismaClient (live DB connection), calls Bun.serve to bind
117
+ // an actual socket, and mounts every sub-app. This is process-wiring / real-I/O
118
+ // by design — its pure logic lives in admin-boot-config.ts (unit-tested via
119
+ // main.unit.test.ts) and admin-extension.ts (admin-extension.unit.test.ts),
120
+ // and its sequence is pinned structurally by admin-boot.boot-order.unit.test.ts.
121
+ // It is exercised in deployed environments (and via the smoke/integration
122
+ // suites of the services it wires together), not by a unit test — forcing a
123
+ // mock-heavy unit test here would mean re-implementing
124
+ // Bun.serve/PrismaClient/Hono routing as fakes without gaining real coverage
125
+ // of the wiring behavior itself.
126
+ export async function startAdmin(
127
+ extensions: readonly AdminExtension[] = [],
128
+ ): Promise<void> {
129
+ // Initializes Sentry (no-op when SENTRY_DSN is unset) so that startup
130
+ // failures below (e.g. a failed migration) are captured too. See
131
+ // task-store's main.ts for the rationale on why re-initializing is safe.
132
+ initSentry({ service: "admin" });
133
+
134
+ const port = Number(process.env.PORT ?? DEFAULT_PORT);
135
+
136
+ console.log(`[admin] starting admin service on port ${port}`);
137
+
138
+ // Run DB migrations as idempotent preflight
139
+ await runMigrations();
140
+
141
+ // Construct PrismaClient once at boot. Prisma 7 has no built-in query
142
+ // engine, so this is a PrismaPg driver adapter over a pg.Pool built from
143
+ // DATABASE_URL_SHIPWRIGHT_ADMIN — see prisma-client.ts. An unset URL throws
144
+ // there (an empty connection string would otherwise make pg fall back to the
145
+ // PG* env vars or localhost, silently pointing the service at a different
146
+ // database); the top-level catch turns that into a named
147
+ // "[admin] fatal startup error" and exit 1, matching how Prisma 6 refused to
148
+ // construct a client without the env var.
149
+ // Pool observability (PLO-1.3): hold tracking + waiting-clients monitor on
150
+ // the admin pool; stopped on shutdown below.
151
+ let adminPool: PoolLike | undefined;
152
+ let stopPoolObservability: () => void = () => {};
153
+ const prisma = createAdminPrismaClient(
154
+ process.env.DATABASE_URL_SHIPWRIGHT_ADMIN ?? "",
155
+ {
156
+ onPoolObserved: ({ pool, stop }) => {
157
+ adminPool = pool;
158
+ stopPoolObservability = stop;
159
+ },
160
+ },
161
+ );
162
+
163
+ // Construct TokenCrypto — reads SHIPWRIGHT_ENCRYPTION_KEY at call time
164
+ const crypto = makeTokenCrypto();
165
+
166
+ // Construct all services with injected deps
167
+ const agentService = new AgentService(prisma);
168
+ const anthropicFederationEnabled = isAnthropicFederationEnabled(process.env);
169
+ const agentEnvService = new AgentEnvService(
170
+ prisma,
171
+ crypto,
172
+ undefined,
173
+ anthropicFederationEnabled,
174
+ );
175
+ const agentCronJobService = new AgentCronJobService(prisma);
176
+ const agentCronRunService = new AgentCronRunService(prisma);
177
+ const agentToolService = new AgentToolService(prisma);
178
+ const agentTokenService = new AgentTokenService(prisma);
179
+ const agentPluginService = new AgentPluginService(prisma);
180
+ const agentMemberService = new AgentMemberService(prisma);
181
+ // Account-aware caller scope (SSP-2.1). Account membership contributes
182
+ // agents only when the self-serve flag is on; off → AgentMember-only.
183
+ const accountService = new AccountService(prisma);
184
+ const callerScopeResolver = createCallerScopeResolver(
185
+ callerScopeDepsFromServices({
186
+ accountService,
187
+ agentMemberService,
188
+ }),
189
+ process.env.SHIPWRIGHT_SELF_SERVE_ENABLED === "enabled",
190
+ );
191
+ const agentChatTokenService = new AgentChatTokenService(prisma);
192
+ const agentCronRunStatsService = new AgentCronRunStatsService(prisma);
193
+ const agentGitHubInstallationsService = new AgentGitHubInstallationsService(
194
+ prisma,
195
+ );
196
+
197
+ // Extension-registered service integrations (task-store, via the coding
198
+ // extension): env-only, so resolved before the provisioner that mints their
199
+ // tokens and the agent deletion that revokes them.
200
+ const serviceIntegrations = collectServiceIntegrations(
201
+ extensions,
202
+ process.env,
203
+ );
204
+
205
+ // Real K8s provisioner when SHIPWRIGHT_K8S_PROVISIONING=enabled, else Noop.
206
+ const provisioner = buildProvisioner(
207
+ process.env,
208
+ agentTokenService,
209
+ prisma,
210
+ serviceIntegrations,
211
+ );
212
+
213
+ // Read config values at call time (no module-level env reads)
214
+ const sessionSecret = process.env.SHIPWRIGHT_SESSION_SECRET ?? "";
215
+ const googleClientId = process.env.GOOGLE_CLIENT_ID ?? "";
216
+ const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET ?? "";
217
+ const oktaIssuer = process.env.OKTA_ISSUER ?? "";
218
+ const oktaClientId = process.env.OKTA_CLIENT_ID ?? "";
219
+ const oktaClientSecret = process.env.OKTA_CLIENT_SECRET ?? "";
220
+ const adminAllowedEmails = (process.env.SHIPWRIGHT_ADMIN_ALLOWED_EMAILS ?? "")
221
+ .split(",")
222
+ .map((e) => e.trim())
223
+ .filter(Boolean);
224
+ const appBaseUrl =
225
+ process.env.SHIPWRIGHT_ADMIN_APP_BASE_URL ?? `http://localhost:${port}`;
226
+ // Read once at startup (not per-request) and interpolated into the
227
+ // service worker's cache name — see admin/src/pwa.ts's
228
+ // buildServiceWorkerBody. Falls back to "0.0.0" if version.txt is
229
+ // missing so a broken read never blocks server startup.
230
+ const appVersion = await Bun.file(
231
+ join(import.meta.dir, "..", "..", "version.txt"),
232
+ )
233
+ .text()
234
+ .then((v) => v.trim())
235
+ .catch(() => "0.0.0");
236
+ const adminApiKeys = parseAdminApiKeys(process.env.SHIPWRIGHT_ADMIN_API_KEYS);
237
+ // Self-serve provisioning config (SSP-1.3) — read once at startup and
238
+ // injected as deps; no consumers yet.
239
+ const selfServe = parseSelfServeConfig(process.env);
240
+ // Account status transitions → cron lockdown/restore (SSP-8.2). Shared by
241
+ // the /accounts PATCH handler, the admin UI and the account sweepers.
242
+ const accountLifecycle = new AccountLifecycle({
243
+ accounts: accountService,
244
+ cronJobs: agentCronJobService,
245
+ });
246
+
247
+ const googleClient = new HttpGoogleAuthClient();
248
+ // Constructed only when an issuer is configured — HttpOktaAuthClient requires
249
+ // `issuer` in its constructor, and an empty issuer would produce nonsensical
250
+ // (double-slash-free but bogus) endpoint URLs. When unset, oktaClient stays
251
+ // undefined and both Okta routes redirect to /admin/login?error=server_error
252
+ // without attempting any external call (see admin-ui.ts's AdminUIDeps doc).
253
+ const oktaClient = oktaIssuer
254
+ ? new HttpOktaAuthClient({ issuer: oktaIssuer })
255
+ : undefined;
256
+ const slackClient = new HttpSlackProvisioningClient();
257
+ const githubAppClient = new HttpGithubAppProvisioningClient();
258
+
259
+ // Chat-service client for deleteAgentFully() cleanup, shared by both delete
260
+ // entry points (DELETE /agents/:id and the admin-ui danger zone) —
261
+ // independent of SHIPWRIGHT_K8S_PROVISIONING (see buildDeletionClients doc
262
+ // comment). Extension service integrations are cleaned up before it.
263
+ const { chatService: deletionChatService } = buildDeletionClients(
264
+ process.env,
265
+ );
266
+
267
+ // Chat-credential routes need a real chat-service client; the no-op
268
+ // deletion client would mint nothing, so it stays absent (→ 503) unless
269
+ // both env vars are set.
270
+ const chatCredentialsClient =
271
+ process.env.SHIPWRIGHT_CHAT_SERVICE_URL &&
272
+ process.env.SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN
273
+ ? new HttpChatServiceProvisioningClient(
274
+ process.env.SHIPWRIGHT_CHAT_SERVICE_URL,
275
+ process.env.SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN,
276
+ )
277
+ : undefined;
278
+
279
+ // Thread/message chat client, shared by the /admin/chat UI and the agent
280
+ // ask route (POST /agents/:id/ask). Built only when both
281
+ // SHIPWRIGHT_CHAT_SERVICE_URL and SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN are
282
+ // set; when absent the /admin/chat routes render in degraded mode and the
283
+ // ask route 503s.
284
+ const chatServiceUrl = process.env.SHIPWRIGHT_CHAT_SERVICE_URL;
285
+ const chatServiceAdminToken = process.env.SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN;
286
+ const chatClient =
287
+ chatServiceUrl && chatServiceAdminToken
288
+ ? new HttpChatClient(chatServiceUrl, chatServiceAdminToken)
289
+ : undefined;
290
+
291
+ // Agent-to-agent ask (COS-31.1). Throws at boot on a malformed allowlist.
292
+ const agentAskConfig = parseAgentAskConfig(process.env);
293
+
294
+ // Web Push (CFB-4.2). Enabled ONLY when public + private + subject are all
295
+ // set — a partial config must not half-enable the feature. When disabled,
296
+ // pushService is undefined, the push routes 503, and the toggle renders "".
297
+ const vapidConfig = {
298
+ publicKey: process.env.SHIPWRIGHT_ADMIN_VAPID_PUBLIC_KEY ?? "",
299
+ privateKey: process.env.SHIPWRIGHT_ADMIN_VAPID_PRIVATE_KEY ?? "",
300
+ subject: process.env.SHIPWRIGHT_ADMIN_VAPID_SUBJECT ?? "",
301
+ };
302
+ const pushWebhookToken = process.env.SHIPWRIGHT_ADMIN_PUSH_WEBHOOK_TOKEN;
303
+ // Operator ceiling for how much detail a notification may reveal on a locked
304
+ // screen. Defaults to "title" per the content policy.
305
+ const pushMaxDetail = process.env.SHIPWRIGHT_ADMIN_PUSH_MAX_DETAIL ?? "title";
306
+ const pushService = isPushEnabled(vapidConfig)
307
+ ? new PushService(prisma as never, vapidConfig, fetch, pushMaxDetail)
308
+ : undefined;
309
+
310
+ // AEP-2.1: resolve the extensions once, after every platform service they
311
+ // may build on exists and before any route is mounted. With no extensions
312
+ // (or hookless ones) every contribution below is empty and the admin is the
313
+ // bare platform.
314
+ const extensionContext: AdminExtensionContext = {
315
+ env: process.env,
316
+ prisma,
317
+ sessionSecret,
318
+ adminApiKeys,
319
+ adminAllowedEmails,
320
+ appBaseUrl,
321
+ ...(process.env.SENTRY_DSN ? { sentryClient: Sentry } : {}),
322
+ ...(adminPool ? { pool: adminPool } : {}),
323
+ ...(process.env.SHIPWRIGHT_ADMIN_TZ
324
+ ? { timezone: process.env.SHIPWRIGHT_ADMIN_TZ }
325
+ : {}),
326
+ ...(pushService
327
+ ? {
328
+ push: {
329
+ service: pushService,
330
+ maxDetail: pushMaxDetail,
331
+ vapidPublicKey: vapidConfig.publicKey,
332
+ },
333
+ }
334
+ : {}),
335
+ services: {
336
+ agentService,
337
+ agentEnvService,
338
+ agentCronJobService,
339
+ agentCronRunService,
340
+ agentGitHubInstallationsService,
341
+ agentTokenService,
342
+ agentMemberService,
343
+ accountService,
344
+ callerScopeResolver,
345
+ provisioner,
346
+ },
347
+ };
348
+ const resolvedExtensions = resolveAdminExtensions(
349
+ extensions,
350
+ extensionContext,
351
+ );
352
+
353
+ // Reassigned below, after Bun.serve() + registerGracefulShutdown() run —
354
+ // the /health/ready handler only calls this at request time (well after
355
+ // startAdmin() finishes its synchronous setup), so it always sees the
356
+ // real graceful-shutdown flag once one exists.
357
+ let isShuttingDown = (): boolean => false;
358
+
359
+ const root = new Hono();
360
+
361
+ // Records method + route per request so held connections are attributable
362
+ // (covers every sub-app, including admin UI and extension routes).
363
+ root.use("*", routeContextMiddleware());
364
+
365
+ // Only mounted when SENTRY_DSN is set — a complete no-op otherwise, matching
366
+ // the app's behavior with Sentry absent. `@sentry/hono`'s `sentry()`
367
+ // middleware performs its own `Sentry.init` call internally, so this stays
368
+ // the only Hono-level init site; it's mounted on the root app (rather than
369
+ // inside a single sub-app) since root is the actual process entry composing
370
+ // the full route tree (health, runtime API, admin API, admin UI).
371
+ const sentryInitOptions = buildSentryInitOptions({ service: "admin" });
372
+ if (sentryInitOptions) {
373
+ root.use("*", sentry(root, sentryInitOptions));
374
+ }
375
+
376
+ // 1. Health checks — no auth
377
+ // /health — liveness: process-alive only, independent of DB state.
378
+ // A transient DB blip must never trigger a
379
+ // liveness-driven restart.
380
+ // /health/ready — readiness: DB-aware. Used by the chart's readinessProbe
381
+ // so Kubernetes doesn't route traffic to this pod before
382
+ // the Cloud SQL proxy sidecar has finished connecting.
383
+ root.get("/health", (c) => c.json({ status: "ok" }));
384
+ root.get("/health/ready", async (c) => {
385
+ // Once a shutdown signal has been received, fail readiness immediately
386
+ // (no DB round-trip) so Kubernetes pulls this pod from Service endpoints
387
+ // as early as possible — see lib/graceful-shutdown.ts.
388
+ if (isShuttingDown()) {
389
+ return c.json({ status: "unavailable" }, 503);
390
+ }
391
+ const ready = await checkDbReady(prisma);
392
+ return c.json({ status: ready ? "ok" : "unavailable" }, ready ? 200 : 503);
393
+ });
394
+
395
+ // Redirect bare root to the login page — no handler exists at "/" otherwise.
396
+ root.get("/", (c) => c.redirect("/admin/login", 302));
397
+
398
+ // 2. Runtime API — admin key | per-agent bearer | session JWT
399
+ // Mounted via root.route("/agents", runtimeApp). Hono v4 strips the prefix
400
+ // before dispatching, so runtimeApp routes are registered as /:id/config
401
+ // and /:id/crons (without the /agents prefix) and resolve correctly at
402
+ // GET /agents/:id/config and GET /agents/:id/crons from root.
403
+ const runtimeApp = createAgentRuntimeApp({
404
+ agentEnvService,
405
+ agentCronJobService,
406
+ agentService,
407
+ agentPluginService,
408
+ ...(resolvedExtensions.agentPhaseMethodologyService
409
+ ? {
410
+ agentPhaseMethodologyService:
411
+ resolvedExtensions.agentPhaseMethodologyService,
412
+ }
413
+ : {}),
414
+ sessionSecret,
415
+ adminApiKeys,
416
+ agentTokenService,
417
+ anthropicFederationEnabled,
418
+ });
419
+
420
+ root.route("/agents", runtimeApp);
421
+
422
+ // 3. Admin CRUD API — /agents/:id/* — admin key | per-agent bearer | session JWT
423
+ // Routes are now at /agents/:id/* (same prefix as runtime, different sub-paths).
424
+ // MUST be mounted before admin-ui (/admin/*) to avoid shadowing.
425
+ const adminApiApp = createAdminApp({
426
+ agentService,
427
+ agentEnvService,
428
+ agentCronJobService,
429
+ agentCronRunService,
430
+ agentCronRunStatsService,
431
+ agentToolService,
432
+ agentTokenService,
433
+ agentPluginService,
434
+ agentMemberService,
435
+ callerScopeResolver,
436
+ specialtyRegistry: new SpecialtyRegistry(),
437
+ agentChatTokenService,
438
+ agentGitHubInstallationsService,
439
+ prisma,
440
+ provisioner,
441
+ serviceIntegrations,
442
+ chatService: deletionChatService,
443
+ slack: slackClient,
444
+ decrypt: crypto.decrypt,
445
+ sessionSecret,
446
+ adminApiKeys,
447
+ sentryClient: process.env.SENTRY_DSN ? Sentry : undefined,
448
+ pool: adminPool,
449
+ selfServe,
450
+ anthropicFederationEnabled,
451
+ chatCredentialsEnabled: parseChatCredentialConfig(process.env).enabled,
452
+ ...(chatCredentialsClient
453
+ ? { chatCredentials: chatCredentialsClient }
454
+ : {}),
455
+ agentAsk: {
456
+ config: agentAskConfig,
457
+ store: new AgentAskService(prisma),
458
+ },
459
+ ...(chatClient ? { chatClient } : {}),
460
+ ...resolvedExtensions.apiDeps,
461
+ });
462
+ root.route("/", adminApiApp);
463
+
464
+ // 3a. Platform OpenAPI document — GET /doc, flag-gated (not mounted when off)
465
+ mountPlatformDoc(root, {
466
+ env: process.env,
467
+ auth: createAdminAuthMiddleware({
468
+ sessionSecret,
469
+ agentTokenService,
470
+ adminApiKeys,
471
+ agentMemberService,
472
+ callerScopeResolver,
473
+ }),
474
+ });
475
+
476
+ // 3b. Accounts API — /accounts/* — admin-only, flag-gated (SSP-5.1)
477
+ root.route(
478
+ "/",
479
+ createAccountsApp({
480
+ accountService: new AccountService(prisma),
481
+ accountLifecycle,
482
+ selfServe,
483
+ sessionSecret,
484
+ adminApiKeys,
485
+ agentTokenService,
486
+ }),
487
+ );
488
+
489
+ // 3c. Extension API routes — after every platform API, before the admin UI
490
+ // (same no-shadowing rule as the admin CRUD API above).
491
+ mountRouteContributions(root, resolvedExtensions.apiRoutes);
492
+
493
+ // 4. Admin UI — /admin/* — session JWT
494
+ // Validate SHIPWRIGHT_ADMIN_TZ at startup — toLocaleDateString/toLocaleString
495
+ // will throw RangeError at render time if the timezone is invalid, causing a 500
496
+ // on every admin page. Catch it early with a clean error and exit.
497
+ const adminTz = process.env.SHIPWRIGHT_ADMIN_TZ;
498
+ if (adminTz) {
499
+ try {
500
+ Intl.DateTimeFormat(undefined, { timeZone: adminTz });
501
+ } catch {
502
+ console.error("[admin] Invalid SHIPWRIGHT_ADMIN_TZ:", adminTz);
503
+ process.exit(1);
504
+ }
505
+ }
506
+
507
+ const adminUIApp = createAdminUIApp({
508
+ prisma: prisma as never,
509
+ agentEnvService,
510
+ agentCronJobService,
511
+ agentToolService,
512
+ agentTokenService,
513
+ agentPluginService,
514
+ agentMemberService,
515
+ callerScopeResolver,
516
+ agentService,
517
+ provisioner,
518
+ anthropicFederationEnabled,
519
+ serviceIntegrations,
520
+ chatService: deletionChatService,
521
+ slack: slackClient,
522
+ decrypt: crypto.decrypt,
523
+ sessionSecret,
524
+ googleClientId,
525
+ googleClientSecret,
526
+ adminAllowedEmails,
527
+ googleClient,
528
+ oktaClientId,
529
+ oktaClientSecret,
530
+ oktaIssuer,
531
+ ...(oktaClient ? { oktaClient } : {}),
532
+ slackClient,
533
+ githubAppClient,
534
+ appBaseUrl,
535
+ appVersion,
536
+ devAuthEnabled: isDevAuthAllowed(process.env),
537
+ selfServe,
538
+ accountService,
539
+ accountOnboarding: new AccountOnboardingService(
540
+ prisma,
541
+ selfServe.defaultMaxAgents,
542
+ ),
543
+ ...(() => {
544
+ const accountCreatedNotifier = createAccountCreatedNotifier({
545
+ ...(pushService ? { pushService } : {}),
546
+ adminEmails: adminAllowedEmails,
547
+ });
548
+ return accountCreatedNotifier ? { accountCreatedNotifier } : {};
549
+ })(),
550
+ accountServices: {
551
+ accounts: new AccountService(prisma),
552
+ members: new AccountMemberService(prisma),
553
+ invites: new AccountInviteService(prisma),
554
+ lifecycle: accountLifecycle,
555
+ },
556
+ timezone: adminTz,
557
+ ...(chatClient ? { chatClient } : {}),
558
+ ...(pushService
559
+ ? {
560
+ pushService,
561
+ vapidPublicKey: vapidConfig.publicKey,
562
+ ...(pushWebhookToken ? { pushWebhookToken } : {}),
563
+ }
564
+ : {}),
565
+ ...(resolvedExtensions.agentDetailCards.length > 0
566
+ ? { agentDetailCards: resolvedExtensions.agentDetailCards }
567
+ : {}),
568
+ ...resolvedExtensions.uiDeps,
569
+ });
570
+ root.route("/", adminUIApp);
571
+
572
+ // 4b. Extension admin UI routes — after the platform UI.
573
+ mountRouteContributions(root, resolvedExtensions.adminUiRoutes);
574
+
575
+ // Trial-expiry warning sweeper (ATE-2.1) — warns an agent's own Slack alert
576
+ // channel once, shortly BEFORE its trial lapses. Registered HERE, never
577
+ // inside createAdminUIApp()/createAdminApp(), which must stay
578
+ // side-effect-free (same rule as the session alert sweeper's registration).
579
+ // Unlike the session alert sweeper, this one needs no external service
580
+ // config to be meaningful — its only dependencies (Prisma, AgentService,
581
+ // AgentEnvService) are always constructed above — so it always starts.
582
+ {
583
+ const trialExpiryWarningSweeper = new TrialExpiryWarningSweeper({
584
+ prisma: prisma as never,
585
+ agentService,
586
+ agentEnvService,
587
+ warningDays: resolveTrialExpiryWarningDays(process.env),
588
+ });
589
+ setInterval(() => {
590
+ trialExpiryWarningSweeper
591
+ .tick()
592
+ .catch((err) =>
593
+ console.error("[trial-expiry-warning-sweeper] tick error:", err),
594
+ );
595
+ }, DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS);
596
+ console.log(
597
+ `[admin] trial expiry warning sweeper started (interval: ${DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS}ms)`,
598
+ );
599
+ }
600
+
601
+ // Trial-expiry sweeper (ATE-3.1) — the lockdown half, running AFTER the
602
+ // warning sweeper above has had its say: disables every enabled AgentCronJob
603
+ // belonging to an agent whose trialExpiresAt has passed. Registered HERE,
604
+ // never inside createAdminUIApp(), same rule as the session alert sweeper
605
+ // above. Reuses the same agentCronJobService instance constructed earlier
606
+ // in this function — no HTTP self-call, just the service's setEnabled(),
607
+ // the identical method the PATCH /agents/:id/crons/:cronId route calls.
608
+ // Deliberately does NOT deprovision the agent (no deleteAgentFully()) — see
609
+ // trial-expiry-sweeper.ts's header comment.
610
+ {
611
+ const trialExpirySweeper = new TrialExpirySweeper({ agentCronJobService });
612
+ const trialExpirySweepIntervalMs = resolveTrialExpirySweepIntervalMs(
613
+ process.env,
614
+ );
615
+ setInterval(() => {
616
+ trialExpirySweeper
617
+ .tick()
618
+ .catch((err) =>
619
+ console.error("[trial-expiry-sweeper] tick error:", err),
620
+ );
621
+ }, trialExpirySweepIntervalMs);
622
+ console.log(
623
+ `[admin] trial expiry sweeper started (interval: ${trialExpirySweepIntervalMs}ms)`,
624
+ );
625
+ }
626
+
627
+ // Account trial sweepers (SSP-8.2) — the account-level counterparts of the
628
+ // two per-agent sweepers above (which keep running unchanged). Flag-gated:
629
+ // accounts only exist when self-serve is enabled. Same cadences as the
630
+ // per-agent pair: hourly warning, minutely expiry/lockdown. Registered
631
+ // HERE, never inside an app factory. No deleteAgentFully call anywhere.
632
+ if (selfServe.enabled) {
633
+ const accountTrialWarningSweeper = new AccountTrialWarningSweeper({
634
+ accounts: accountService,
635
+ agentEnvService,
636
+ warningDays: resolveTrialExpiryWarningDays(process.env),
637
+ });
638
+ setInterval(() => {
639
+ accountTrialWarningSweeper
640
+ .tick()
641
+ .catch((err) =>
642
+ console.error("[account-trial-warning-sweeper] tick error:", err),
643
+ );
644
+ }, DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS);
645
+
646
+ const accountTrialExpirySweeper = new AccountTrialExpirySweeper({
647
+ accounts: accountService,
648
+ lifecycle: accountLifecycle,
649
+ });
650
+ const accountTrialExpiryIntervalMs = resolveTrialExpirySweepIntervalMs(
651
+ process.env,
652
+ );
653
+ setInterval(() => {
654
+ accountTrialExpirySweeper
655
+ .tick()
656
+ .catch((err) =>
657
+ console.error("[account-trial-expiry-sweeper] tick error:", err),
658
+ );
659
+ }, accountTrialExpiryIntervalMs);
660
+ console.log(
661
+ `[admin] account trial sweepers started (warning: ${DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS}ms, expiry: ${accountTrialExpiryIntervalMs}ms)`,
662
+ );
663
+ }
664
+
665
+ // Extension background passes (AEP-2.1) — after every platform sweeper,
666
+ // same "registered here, never inside an app factory" rule.
667
+ startBackgroundPasses(resolvedExtensions.backgroundPasses);
668
+
669
+ const server = Bun.serve({ fetch: root.fetch, port });
670
+
671
+ const shutdown = registerGracefulShutdown({
672
+ server,
673
+ cleanup: [async () => stopPoolObservability(), () => prisma.$disconnect()],
674
+ serviceName: "admin",
675
+ });
676
+ isShuttingDown = shutdown.isShuttingDown;
677
+
678
+ console.log(`[admin] admin service listening on port ${port}`);
679
+
680
+ // Policy-only reconcile (ACP-1.3): a Claude policy change restarts admin via
681
+ // the chart's policy-hash env var, so rolling only the agents whose policy
682
+ // wiring drifted happens here. Runs even with no policy configured so agents
683
+ // lose the mount once it is disabled. Fire-and-forget: it never throws and
684
+ // must not delay serving.
685
+ void runStartupPolicyReconcile({
686
+ agentService,
687
+ provisioner,
688
+ logger: console,
689
+ });
690
+ }