@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,432 @@
1
+ /**
2
+ * admin/src/admin-extension.ts
3
+ *
4
+ * The extension seam between the generic admin platform (admin-boot.ts:
5
+ * agents, envs, crons, tokens, plugins, accounts, provisioning, the admin UI
6
+ * shell and its sweepers) and specialty-specific admin behavior (Shipwright:
7
+ * the coding extension). Mirrors the agent's seam in
8
+ * agent/src/runtime/agent-extension.ts.
9
+ *
10
+ * startAdmin(extensions) resolves every extension once, right after the
11
+ * platform services exist and before any route is mounted, calling each hook
12
+ * across all extensions in ADMIN_EXTENSION_HOOK_ORDER:
13
+ *
14
+ * serviceIntegrations(env) — optional per-agent service integrations
15
+ * (service-integration.ts). Unlike the hooks below
16
+ * it takes only env and is resolved BEFORE the
17
+ * provisioner is built, since the provisioner and
18
+ * agent deletion consume it
19
+ * services(ctx) — extra service deps spread into createAdminApp /
20
+ * createAdminUIApp (later extensions win)
21
+ * apiRoutes(ctx) — API sub-apps, mounted after the platform APIs and
22
+ * before the admin UI (so the UI never shadows them)
23
+ * adminUiRoutes(ctx) — admin UI sub-apps, mounted after the platform UI
24
+ * agentDetailCards(ctx) — extra cards on the agent detail page, each placed
25
+ * in the header or the Configuration group
26
+ * backgroundPasses(ctx) — interval loops, started after the platform sweepers
27
+ *
28
+ * openApi() is static (no context): scripts/generate-admin-spec.ts calls it to
29
+ * fold extension routes/schemas into admin/openapi.json without booting.
30
+ *
31
+ * Every hook is optional; no extension (or a hookless one) reproduces the bare
32
+ * platform exactly. This file must not import any coding module — main.ts
33
+ * (the composition root) is the only file that wires a concrete extension in.
34
+ */
35
+
36
+ import type { PoolLike } from "@app-vitals/agent-lib/pool-observability";
37
+ import type { ErrorCapturingClient } from "@app-vitals/agent-lib/sentry";
38
+ import type { Hono } from "hono";
39
+ import type { PrismaClient } from "../prisma/client/client.ts";
40
+ import type { AccountService } from "./accounts.ts";
41
+ import type { AdminUIDeps } from "./admin-ui.ts";
42
+ import type { AgentCronJobService } from "./agent-cron-jobs.ts";
43
+ import type { AgentCronRunService } from "./agent-cron-runs.ts";
44
+ import type { AgentEnvService } from "./agent-envs.ts";
45
+ import type { AgentGitHubInstallationsService } from "./agent-github-installations.ts";
46
+ import type { AgentMemberService } from "./agent-members.ts";
47
+ import type { AgentProvisioner } from "./agent-provisioner.ts";
48
+ import type { AgentTokenService } from "./agent-tokens.ts";
49
+ import type { AgentService } from "./agents.ts";
50
+ import type { AdminDeps, parseAdminApiKeys } from "./agents-api.ts";
51
+ import type { AgentPhaseMethodologyServiceLike } from "./api.ts";
52
+ import type { createCallerScopeResolver } from "./caller-scope.ts";
53
+ import type { PushService } from "./push-service.ts";
54
+ import type { ServiceIntegration } from "./service-integration.ts";
55
+
56
+ /** The platform services an extension may build on. */
57
+ export interface AdminPlatformServices {
58
+ agentService: AgentService;
59
+ agentEnvService: AgentEnvService;
60
+ agentCronJobService: AgentCronJobService;
61
+ agentCronRunService: AgentCronRunService;
62
+ agentGitHubInstallationsService: AgentGitHubInstallationsService;
63
+ agentTokenService: AgentTokenService;
64
+ agentMemberService: AgentMemberService;
65
+ accountService: AccountService;
66
+ callerScopeResolver: ReturnType<typeof createCallerScopeResolver>;
67
+ provisioner: AgentProvisioner;
68
+ }
69
+
70
+ /** Everything an extension's hooks may need from the platform. */
71
+ export interface AdminExtensionContext {
72
+ env: NodeJS.ProcessEnv;
73
+ prisma: PrismaClient;
74
+ sessionSecret: string;
75
+ adminApiKeys: ReturnType<typeof parseAdminApiKeys>;
76
+ /** SHIPWRIGHT_ADMIN_ALLOWED_EMAILS, parsed: operators who see everything. */
77
+ adminAllowedEmails: string[];
78
+ /** The admin UI's public base URL (SHIPWRIGHT_ADMIN_APP_BASE_URL). */
79
+ appBaseUrl: string;
80
+ /** SHIPWRIGHT_ADMIN_TZ, when set: the admin UI's display timezone. */
81
+ timezone?: string;
82
+ /** Web Push, present only when VAPID is fully configured. */
83
+ push?: { service: PushService; maxDetail: string; vapidPublicKey: string };
84
+ /** Sentry, present only when SENTRY_DSN is set (as the admin API gets it). */
85
+ sentryClient?: ErrorCapturingClient;
86
+ /** Admin's pg pool (PLO-1.3), for pool-timeout capture context. */
87
+ pool?: PoolLike;
88
+ services: AdminPlatformServices;
89
+ }
90
+
91
+ // Extension sub-apps carry their own Env/Schema generics; Hono's route() is
92
+ // generic over them, so the contribution type must accept any.
93
+ // biome-ignore lint/suspicious/noExplicitAny: see above
94
+ type AnyHono = Hono<any, any, any>;
95
+
96
+ /** A sub-app mounted on the root at `path` via root.route(path, app). */
97
+ export interface AdminRouteContribution {
98
+ path: string;
99
+ app: AnyHono;
100
+ }
101
+
102
+ /** Extra deps merged over the platform's createAdminApp / createAdminUIApp deps. */
103
+ export interface AdminExtensionServices {
104
+ api?: Partial<AdminDeps>;
105
+ ui?: Partial<AdminUIDeps>;
106
+ /**
107
+ * Supplies the runtime API's phase-methodology service (GET
108
+ * /agents/:id/config's `phaseMethodology`). Absent from every extension →
109
+ * the runtime app omits the field.
110
+ */
111
+ agentPhaseMethodologyService?: AgentPhaseMethodologyServiceLike;
112
+ }
113
+
114
+ export interface AgentDetailCardContext {
115
+ agentId: string;
116
+ userEmail: string;
117
+ isAdmin: boolean;
118
+ /** The agent's owning account; null for unassigned agents. */
119
+ accountId?: string | null;
120
+ /** The admin UI's display timezone (IANA name). */
121
+ timezone?: string;
122
+ }
123
+
124
+ /**
125
+ * Where a card lands on the agent detail page:
126
+ * header — above the stat strip, after the page's alerts (default)
127
+ * configuration — inside the Configuration group, after Repos
128
+ */
129
+ export type AgentDetailCardPlacement = "header" | "configuration";
130
+
131
+ /**
132
+ * One extra card on GET /admin/agents/:id. render() returns trusted HTML — the
133
+ * extension escapes its own content. A throwing render omits just that card.
134
+ */
135
+ export interface AgentDetailCardSlot {
136
+ name: string;
137
+ /** Defaults to "header". */
138
+ placement?: AgentDetailCardPlacement;
139
+ render(ctx: AgentDetailCardContext): Promise<string>;
140
+ }
141
+
142
+ /** An interval loop registered in startAdmin, never inside an app factory. */
143
+ export interface AdminBackgroundPass {
144
+ name: string;
145
+ /** Prefix for the non-fatal failure log: `${logPrefix} tick error:`. */
146
+ logPrefix: string;
147
+ intervalMs: number;
148
+ tick(): Promise<unknown>;
149
+ }
150
+
151
+ /** OpenAPI fragments folded into the admin document (paths use `{param}`). */
152
+ export interface AdminOpenApiContribution {
153
+ paths?: Record<string, Record<string, unknown>>;
154
+ schemas?: Record<string, unknown>;
155
+ /**
156
+ * Where new paths/schemas land in the document: contributed key → the key
157
+ * it is inserted directly after (a platform key, or a contributed key placed
158
+ * earlier). New keys without a placement are appended. Lets an extension's
159
+ * routes keep a stable position in the generated document.
160
+ */
161
+ placeAfter?: {
162
+ paths?: Record<string, string>;
163
+ schemas?: Record<string, string>;
164
+ };
165
+ }
166
+
167
+ export interface AdminExtension {
168
+ name?: string;
169
+ /** See the header: env-only, resolved before the provisioner is built. */
170
+ serviceIntegrations?(env: NodeJS.ProcessEnv): ServiceIntegration[];
171
+ services?(ctx: AdminExtensionContext): AdminExtensionServices;
172
+ apiRoutes?(ctx: AdminExtensionContext): AdminRouteContribution[];
173
+ adminUiRoutes?(ctx: AdminExtensionContext): AdminRouteContribution[];
174
+ agentDetailCards?(ctx: AdminExtensionContext): AgentDetailCardSlot[];
175
+ backgroundPasses?(ctx: AdminExtensionContext): AdminBackgroundPass[];
176
+ openApi?(): AdminOpenApiContribution;
177
+ }
178
+
179
+ /** The order resolveAdminExtensions calls the context hooks in. */
180
+ export const ADMIN_EXTENSION_HOOK_ORDER = [
181
+ "services",
182
+ "apiRoutes",
183
+ "adminUiRoutes",
184
+ "agentDetailCards",
185
+ "backgroundPasses",
186
+ ] as const;
187
+
188
+ export interface ResolvedAdminExtensions {
189
+ apiDeps: Partial<AdminDeps>;
190
+ uiDeps: Partial<AdminUIDeps>;
191
+ agentPhaseMethodologyService?: AgentPhaseMethodologyServiceLike;
192
+ apiRoutes: AdminRouteContribution[];
193
+ adminUiRoutes: AdminRouteContribution[];
194
+ agentDetailCards: AgentDetailCardSlot[];
195
+ backgroundPasses: AdminBackgroundPass[];
196
+ }
197
+
198
+ /**
199
+ * Calls every context hook once, hook by hook (ADMIN_EXTENSION_HOOK_ORDER),
200
+ * each across all extensions in order. Lists concatenate; service deps merge
201
+ * with later extensions winning.
202
+ */
203
+ export function resolveAdminExtensions(
204
+ extensions: readonly AdminExtension[],
205
+ ctx: AdminExtensionContext,
206
+ ): ResolvedAdminExtensions {
207
+ const services = extensions.map((ext) => ext.services?.(ctx) ?? {});
208
+ const collect = <T>(hook: (ext: AdminExtension) => T[] | undefined): T[] =>
209
+ extensions.flatMap((ext) => hook(ext) ?? []);
210
+ const apiRoutes = collect((ext) => ext.apiRoutes?.(ctx));
211
+ const adminUiRoutes = collect((ext) => ext.adminUiRoutes?.(ctx));
212
+ const agentDetailCards = collect((ext) => ext.agentDetailCards?.(ctx));
213
+ const backgroundPasses = collect((ext) => ext.backgroundPasses?.(ctx));
214
+ const agentPhaseMethodologyService = services
215
+ .map((s) => s.agentPhaseMethodologyService)
216
+ .filter((svc) => svc !== undefined)
217
+ .at(-1);
218
+ return {
219
+ ...(agentPhaseMethodologyService ? { agentPhaseMethodologyService } : {}),
220
+ apiDeps: Object.assign({}, ...services.map((s) => s.api ?? {})),
221
+ uiDeps: Object.assign({}, ...services.map((s) => s.ui ?? {})),
222
+ apiRoutes,
223
+ adminUiRoutes,
224
+ agentDetailCards,
225
+ backgroundPasses,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Merges every extension's static OpenAPI contribution, in order. Placements
231
+ * are included only when some extension declared one.
232
+ */
233
+ export function collectOpenApiContributions(
234
+ extensions: readonly AdminExtension[],
235
+ ): Required<Pick<AdminOpenApiContribution, "paths" | "schemas">> &
236
+ Pick<AdminOpenApiContribution, "placeAfter"> {
237
+ const paths: Record<string, Record<string, unknown>> = {};
238
+ const schemas: Record<string, unknown> = {};
239
+ const pathsAfter: Record<string, string> = {};
240
+ const schemasAfter: Record<string, string> = {};
241
+ for (const ext of extensions) {
242
+ const contribution = ext.openApi?.() ?? {};
243
+ for (const [path, methods] of Object.entries(contribution.paths ?? {})) {
244
+ paths[path] = { ...paths[path], ...methods };
245
+ }
246
+ Object.assign(schemas, contribution.schemas);
247
+ Object.assign(pathsAfter, contribution.placeAfter?.paths);
248
+ Object.assign(schemasAfter, contribution.placeAfter?.schemas);
249
+ }
250
+ const placed =
251
+ Object.keys(pathsAfter).length > 0 || Object.keys(schemasAfter).length > 0;
252
+ return {
253
+ paths,
254
+ schemas,
255
+ ...(placed
256
+ ? { placeAfter: { paths: pathsAfter, schemas: schemasAfter } }
257
+ : {}),
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Merges `extra` into `base` keeping base's key order: an existing key keeps
263
+ * its position (its value merged via `mergeValue`), a new key is inserted
264
+ * right after its `after` anchor or, unplaced, appended. An anchor that is not
265
+ * in the document is a programming error and throws.
266
+ */
267
+ function mergeOrdered<V>(
268
+ base: Record<string, V>,
269
+ extra: Record<string, V>,
270
+ after: Record<string, string> | undefined,
271
+ mergeValue: (existing: V, added: V) => V,
272
+ ): Record<string, V> {
273
+ const keys = Object.keys(base);
274
+ const values: Record<string, V> = { ...base };
275
+ for (const [key, value] of Object.entries(extra)) {
276
+ if (Object.hasOwn(values, key)) {
277
+ values[key] = mergeValue(values[key] as V, value);
278
+ continue;
279
+ }
280
+ const anchor = after?.[key];
281
+ if (anchor === undefined) {
282
+ keys.push(key);
283
+ } else {
284
+ const at = keys.indexOf(anchor);
285
+ if (at === -1) {
286
+ throw new Error(
287
+ `OpenAPI contribution places "${key}" after unknown "${anchor}"`,
288
+ );
289
+ }
290
+ keys.splice(at + 1, 0, key);
291
+ }
292
+ values[key] = value;
293
+ }
294
+ return Object.fromEntries(keys.map((key) => [key, values[key] as V]));
295
+ }
296
+
297
+ interface OpenApiDocLike {
298
+ paths?: Record<string, Record<string, unknown>>;
299
+ components?: { schemas?: Record<string, unknown> };
300
+ }
301
+
302
+ /**
303
+ * Folds a contribution into an OpenAPI document. An empty contribution returns
304
+ * the input itself, so the generated spec stays byte-identical. Paths merge at
305
+ * the method level (like the runtime/admin merge in generate-admin-spec.ts);
306
+ * new paths and schemas land at their `placeAfter` anchors, else at the end.
307
+ */
308
+ export function applyOpenApiContributions<T extends OpenApiDocLike>(
309
+ doc: T,
310
+ contribution: AdminOpenApiContribution,
311
+ ): T {
312
+ const extraPaths = Object.entries(contribution.paths ?? {});
313
+ const extraSchemas = contribution.schemas ?? {};
314
+ if (extraPaths.length === 0 && Object.keys(extraSchemas).length === 0) {
315
+ return doc;
316
+ }
317
+ const paths = mergeOrdered(
318
+ doc.paths ?? {},
319
+ Object.fromEntries(extraPaths),
320
+ contribution.placeAfter?.paths,
321
+ (existing, added) => ({ ...existing, ...added }),
322
+ );
323
+ const schemas = mergeOrdered(
324
+ doc.components?.schemas ?? {},
325
+ extraSchemas,
326
+ contribution.placeAfter?.schemas,
327
+ (_existing, added) => added,
328
+ );
329
+ return {
330
+ ...doc,
331
+ paths,
332
+ components: { ...(doc.components ?? {}), schemas },
333
+ };
334
+ }
335
+
336
+ /**
337
+ * Collects every extension's service integrations, in extension order. Resolved
338
+ * before the provisioner exists, so it sees only the environment.
339
+ */
340
+ export function collectServiceIntegrations(
341
+ extensions: readonly AdminExtension[],
342
+ env: NodeJS.ProcessEnv,
343
+ ): ServiceIntegration[] {
344
+ return extensions.flatMap((ext) => ext.serviceIntegrations?.(env) ?? []);
345
+ }
346
+
347
+ /** Mounts each contribution on root, in order. */
348
+ export function mountRouteContributions(
349
+ root: AnyHono,
350
+ routes: readonly AdminRouteContribution[],
351
+ ): void {
352
+ for (const { path, app } of routes) {
353
+ root.route(path, app);
354
+ }
355
+ }
356
+
357
+ export interface BackgroundPassRuntime {
358
+ schedule(fn: () => void, intervalMs: number): unknown;
359
+ log(message: string): void;
360
+ error(...args: unknown[]): void;
361
+ }
362
+
363
+ const defaultRuntime: BackgroundPassRuntime = {
364
+ schedule: (fn, intervalMs) => setInterval(fn, intervalMs),
365
+ log: (message) => console.log(message),
366
+ error: (...args) => console.error(...args),
367
+ };
368
+
369
+ /**
370
+ * Starts each pass on its own interval, same shape as the platform sweepers:
371
+ * a failing tick is logged with the pass's prefix and never throws.
372
+ */
373
+ export function startBackgroundPasses(
374
+ passes: readonly AdminBackgroundPass[],
375
+ runtime: BackgroundPassRuntime = defaultRuntime,
376
+ ): void {
377
+ for (const pass of passes) {
378
+ runtime.schedule(() => {
379
+ pass
380
+ .tick()
381
+ .catch((err) => runtime.error(`${pass.logPrefix} tick error:`, err));
382
+ }, pass.intervalMs);
383
+ runtime.log(
384
+ `[admin] ${pass.name} started (interval: ${pass.intervalMs}ms)`,
385
+ );
386
+ }
387
+ }
388
+
389
+ /** Renders card slots in order; a throwing slot is logged and omitted. */
390
+ export async function renderAgentDetailCards(
391
+ slots: readonly AgentDetailCardSlot[],
392
+ ctx: AgentDetailCardContext,
393
+ ): Promise<string[]> {
394
+ const rendered = await Promise.all(
395
+ slots.map((slot) =>
396
+ slot.render(ctx).catch((err) => {
397
+ console.error(
398
+ `[admin] agent detail card "${slot.name}" failed to render:`,
399
+ err,
400
+ );
401
+ return undefined;
402
+ }),
403
+ ),
404
+ );
405
+ return rendered.filter((html): html is string => html !== undefined);
406
+ }
407
+
408
+ /**
409
+ * Renders the slots (renderAgentDetailCards semantics: in order, a throwing
410
+ * slot omitted) and groups the HTML by placement. Slots whose placement is not
411
+ * in `placements` are not rendered: each holds its place as an empty string,
412
+ * so the page lays out exactly as a full render whose cards came back empty.
413
+ */
414
+ export async function renderAgentDetailCardSlots(
415
+ slots: readonly AgentDetailCardSlot[],
416
+ ctx: AgentDetailCardContext,
417
+ placements: readonly AgentDetailCardPlacement[] = ["header", "configuration"],
418
+ ): Promise<Record<AgentDetailCardPlacement, string[]>> {
419
+ const at = async (placement: AgentDetailCardPlacement) => {
420
+ const placed = slots.filter(
421
+ (slot) => (slot.placement ?? "header") === placement,
422
+ );
423
+ return placements.includes(placement)
424
+ ? renderAgentDetailCards(placed, ctx)
425
+ : placed.map(() => "");
426
+ };
427
+ const [header, configuration] = await Promise.all([
428
+ at("header"),
429
+ at("configuration"),
430
+ ]);
431
+ return { header, configuration };
432
+ }