@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
package/Dockerfile ADDED
@@ -0,0 +1,43 @@
1
+ # Admin service image (ghcr.io/app-vitals/agent-admin)
2
+ #
3
+ # Build from the repository root: docker build -f admin/Dockerfile .
4
+
5
+ FROM oven/bun:1-slim AS deps
6
+
7
+ WORKDIR /app
8
+
9
+ COPY package.json bunfig.toml bun.lock* ./
10
+ COPY admin/package.json ./admin/
11
+ COPY admin/prisma ./admin/prisma/
12
+ COPY admin/prisma.config.ts ./admin/
13
+ COPY runtime/package.json ./runtime/
14
+ COPY lib/package.json ./lib/
15
+ COPY patches/ ./patches/
16
+
17
+ RUN if [ -f bun.lock ]; then bun install --frozen-lockfile; else bun install; fi
18
+
19
+ FROM oven/bun:1-slim AS production
20
+
21
+ RUN apt-get update && apt-get install -y --no-install-recommends \
22
+ ca-certificates \
23
+ && rm -rf /var/lib/apt/lists/*
24
+
25
+ WORKDIR /app
26
+
27
+ ENV NODE_ENV=production
28
+
29
+ COPY --from=deps /app/node_modules ./node_modules
30
+ COPY --from=deps /app/admin/node_modules ./admin/node_modules
31
+ COPY --from=deps /app/lib/node_modules ./lib/node_modules
32
+
33
+ # .dockerignore keeps node_modules, .git and build output out of the context.
34
+ COPY . .
35
+
36
+ # The generated Prisma client is not committed.
37
+ RUN cd admin && bunx prisma generate --schema=prisma/schema
38
+
39
+ EXPOSE 3000
40
+
41
+ USER 1000
42
+
43
+ ENTRYPOINT ["bun", "run", "admin/src/main.ts"]
package/README.md CHANGED
@@ -1,3 +1,73 @@
1
- # @app-vitals/agent-admin
1
+ # Shipwright Admin Service
2
2
 
3
- Placeholder reserving this name. Real releases are published from https://github.com/app-vitals/agent.
3
+ The admin service provides a web UI and CRUD API for managing Shipwright agents — env vars, cron jobs, tokens, tools, and plugins. It also exposes the runtime API used by deployed agents to fetch their configuration.
4
+
5
+ The HTTP server is implemented in `admin/src/main.ts`, which composes the admin UI, admin CRUD API, and agent runtime API into a single Hono app running on port 3000.
6
+
7
+ ## Running Locally
8
+
9
+ ```bash
10
+ # From repo root — install all workspace deps
11
+ bun install
12
+
13
+ # Export required env vars (see Environment Variables table below)
14
+ export DATABASE_URL_SHIPWRIGHT_ADMIN=postgresql://...
15
+ export SHIPWRIGHT_SESSION_SECRET=...
16
+ # ... (remaining vars)
17
+
18
+ # Run the admin server
19
+ bun run admin/src/main.ts
20
+ ```
21
+
22
+ The service listens on `http://localhost:3000` by default. Visit `/admin` for the UI.
23
+
24
+ ## Environment Variables
25
+
26
+ | Variable | Required | Description |
27
+ |---|---|---|
28
+ | `DATABASE_URL_SHIPWRIGHT_ADMIN` | Yes | PostgreSQL connection string for the admin Prisma schema |
29
+ | `SHIPWRIGHT_SESSION_SECRET` | Yes | JWT cookie secret for admin UI sessions |
30
+ | `GOOGLE_CLIENT_ID` | Yes | Google OAuth 2.0 client ID for admin UI login |
31
+ | `GOOGLE_CLIENT_SECRET` | Yes | Google OAuth 2.0 client secret |
32
+ | `SHIPWRIGHT_ADMIN_ALLOWED_EMAILS` | Yes | Comma-separated list of emails permitted to log in |
33
+ | `SHIPWRIGHT_ADMIN_APP_BASE_URL` | Yes | Public base URL used for OAuth redirect (defaults to `http://localhost:3000`) |
34
+ | `SHIPWRIGHT_ADMIN_API_KEYS` | No | Comma-separated `name:token:scope` tuples for bearer token auth. Scope is `*`, an agent id, `platform.read` or `platform.ops` (no colons in scopes) — see [docs/agent-api.md](../docs/agent-api.md) |
35
+ | `SHIPWRIGHT_ENCRYPTION_KEY` | Recommended | 64-char hex key for AES-256-GCM encryption of stored secrets |
36
+ | `PORT` | No | HTTP server port (defaults to `3000`) |
37
+
38
+ ## Database
39
+
40
+ The admin service uses its own PostgreSQL database. Run migrations before starting:
41
+
42
+ ```bash
43
+ export DATABASE_URL_SHIPWRIGHT_ADMIN=postgres://...
44
+ cd admin && bunx prisma migrate deploy --schema=prisma/schema
45
+ ```
46
+
47
+ ## Building the Docker Image
48
+
49
+ Build from the repo root (the Dockerfile uses the full monorepo context):
50
+
51
+ ```bash
52
+ docker build -f admin/Dockerfile -t shipwright-admin .
53
+ ```
54
+
55
+ Run the image:
56
+
57
+ ```bash
58
+ docker run \
59
+ -e DATABASE_URL_SHIPWRIGHT_ADMIN=postgres://... \
60
+ -e SHIPWRIGHT_SESSION_SECRET=... \
61
+ -e GOOGLE_CLIENT_ID=... \
62
+ -e GOOGLE_CLIENT_SECRET=... \
63
+ -e SHIPWRIGHT_ADMIN_ALLOWED_EMAILS=admin@example.com \
64
+ -e SHIPWRIGHT_ADMIN_APP_BASE_URL=https://admin.example.com \
65
+ -p 3000:3000 \
66
+ shipwright-admin
67
+ ```
68
+
69
+ ## Deployment
70
+
71
+ A GKE Deployment + Service manifest is provided at [`admin/deploy/deployment.yaml`](./deploy/deployment.yaml).
72
+
73
+ It expects a Kubernetes Secret named `shipwright-admin-secret` containing all required env vars. See the comments at the top of the manifest for setup instructions.
@@ -0,0 +1,338 @@
1
+ /**
2
+ * admin/e2e/agents-page.e2e.ts
3
+ * Admin UI — Agents Page E2E Tests (ADM-3.2)
4
+ *
5
+ * Tests the agent detail page at GET /admin/agents/:id, covering:
6
+ * 1. Unauthenticated GET /admin/agents → redirects to /admin/login
7
+ * 2. Authenticated → page title contains agent name
8
+ * 3. Authenticated → h1 contains agent name
9
+ * 4. Authenticated → agent card shows agent name "Test Agent"
10
+ * 5. Env var key "SLACK_BOT_TOKEN" is visible
11
+ * 6. Env var value is masked (••••••••), raw secret not present in page
12
+ * 7. Cron job schedule "0 9 * * 1" is visible
13
+ * 8. Cron job prompt is visible
14
+ * 9. Token label "Production token" is visible
15
+ * 10. Env var add form is present (action contains /envs)
16
+ * 11. Add cron form is present (action contains /crons)
17
+ * 12. Token form is present (action contains /tokens)
18
+ * 13. Mobile viewport (375px) — no horizontal overflow
19
+ *
20
+ * Architecture:
21
+ * - Spawns admin/e2e/test-server.ts via Bun as a child process.
22
+ * - Uses hono/jwt sign() to mint valid session cookies directly.
23
+ * - No real DB or OAuth flow is initiated.
24
+ */
25
+
26
+ import { type ChildProcess, spawn } from "node:child_process";
27
+ import { dirname, resolve } from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+ import { type BrowserContext, type Page, expect, test } from "@playwright/test";
30
+ import { sign } from "hono/jwt";
31
+
32
+ // ─── Constants ────────────────────────────────────────────────────────────────
33
+
34
+ const PORT = 3491;
35
+ const BASE_URL = `http://localhost:${PORT}`;
36
+ const SESSION_SECRET = "e2e-admin-test-secret-32chars!!!";
37
+ const SESSION_COOKIE = "admin_session";
38
+ const AGENT_ID = "agent-e2e-1";
39
+
40
+ const __dirname = dirname(fileURLToPath(import.meta.url));
41
+
42
+ // ─── Server lifecycle ─────────────────────────────────────────────────────────
43
+
44
+ let serverProcess: ChildProcess | null = null;
45
+
46
+ async function waitForServer(url: string, maxWaitMs: number): Promise<void> {
47
+ const start = Date.now();
48
+ while (Date.now() - start < maxWaitMs) {
49
+ try {
50
+ const res = await fetch(url);
51
+ if (res.status < 500) return;
52
+ } catch {
53
+ // Not ready yet
54
+ }
55
+ await new Promise((r) => setTimeout(r, 300));
56
+ }
57
+ throw new Error(`Server at ${url} did not start within ${maxWaitMs}ms`);
58
+ }
59
+
60
+ test.beforeAll(async () => {
61
+ const serverScript = resolve(__dirname, "test-server.ts");
62
+
63
+ serverProcess = spawn("bun", ["run", serverScript], {
64
+ env: {
65
+ ...process.env,
66
+ ADMIN_E2E_PORT: String(PORT),
67
+ ADMIN_E2E_SESSION_SECRET: SESSION_SECRET,
68
+ },
69
+ stdio: "pipe",
70
+ cwd: resolve(__dirname, "../.."),
71
+ });
72
+
73
+ serverProcess.stderr?.on("data", (data: Buffer) => {
74
+ const msg = data.toString();
75
+ if (msg.trim()) console.error("[admin-e2e-agents-server]", msg.trim());
76
+ });
77
+
78
+ serverProcess.stdout?.on("data", (data: Buffer) => {
79
+ const msg = data.toString();
80
+ if (msg.trim()) console.log("[admin-e2e-agents-server]", msg.trim());
81
+ });
82
+
83
+ await waitForServer(`${BASE_URL}/health`, 15_000);
84
+ });
85
+
86
+ test.afterAll(() => {
87
+ if (serverProcess) {
88
+ serverProcess.kill("SIGTERM");
89
+ serverProcess = null;
90
+ }
91
+ });
92
+
93
+ // ─── Session cookie helper ────────────────────────────────────────────────────
94
+
95
+ async function mintSession(
96
+ userId = "google-sub-e2e",
97
+ email = "admin@example.com",
98
+ ): Promise<string> {
99
+ const nowSec = Math.floor(Date.now() / 1000);
100
+ return sign(
101
+ { userId, email, isAdmin: true, iat: nowSec, exp: nowSec + 3600 },
102
+ SESSION_SECRET,
103
+ "HS256",
104
+ );
105
+ }
106
+
107
+ // ─── Helper: load agent detail page with auth ─────────────────────────────────
108
+
109
+ async function loadAgentDetailPage(
110
+ page: Page,
111
+ context: BrowserContext,
112
+ ): Promise<void> {
113
+ const token = await mintSession();
114
+ await context.addCookies([
115
+ {
116
+ name: SESSION_COOKIE,
117
+ value: token,
118
+ domain: "localhost",
119
+ path: "/",
120
+ httpOnly: true,
121
+ sameSite: "Lax",
122
+ },
123
+ ]);
124
+ await page.goto(`${BASE_URL}/admin/agents/${AGENT_ID}`);
125
+ }
126
+
127
+ // ─── 1. Unauthenticated redirect ──────────────────────────────────────────────
128
+
129
+ test.describe("GET /admin/agents — unauthenticated", () => {
130
+ test("redirects to /admin/login when no session cookie", async ({ page }) => {
131
+ await page.goto(`${BASE_URL}/admin/agents`);
132
+ await expect(page).toHaveURL(/\/admin\/login/);
133
+ });
134
+ });
135
+
136
+ // ─── 2–13. Authenticated tests ────────────────────────────────────────────────
137
+
138
+ test.describe("GET /admin/agents/:id — authenticated", () => {
139
+ test("page title contains agent name", async ({ page, context }) => {
140
+ await loadAgentDetailPage(page, context);
141
+ await expect(page).toHaveTitle(/Test Agent/);
142
+ });
143
+
144
+ test("h1 contains agent name", async ({ page, context }) => {
145
+ await loadAgentDetailPage(page, context);
146
+ await expect(page.locator("h1")).toContainText("Test Agent");
147
+ });
148
+
149
+ test('agent name "Test Agent" is visible on the page', async ({
150
+ page,
151
+ context,
152
+ }) => {
153
+ await loadAgentDetailPage(page, context);
154
+ await expect(page.locator("body")).toContainText("Test Agent");
155
+ });
156
+
157
+ test('env var key "SLACK_BOT_TOKEN" is visible', async ({
158
+ page,
159
+ context,
160
+ }) => {
161
+ await loadAgentDetailPage(page, context);
162
+ await expect(page.locator("body")).toContainText("SLACK_BOT_TOKEN");
163
+ });
164
+
165
+ test("env var value is masked — shows •••••••• and not the raw secret", async ({
166
+ page,
167
+ context,
168
+ }) => {
169
+ await loadAgentDetailPage(page, context);
170
+ const body = await page.locator("body").textContent();
171
+ expect(body).toContain("••••••••");
172
+ expect(body).not.toContain("xoxb-secret-value");
173
+ });
174
+
175
+ test('cron job schedule "0 9 * * 1" is visible', async ({
176
+ page,
177
+ context,
178
+ }) => {
179
+ await loadAgentDetailPage(page, context);
180
+ await expect(page.locator("body")).toContainText("0 9 * * 1");
181
+ });
182
+
183
+ test("cron job prompt is visible", async ({ page, context }) => {
184
+ await loadAgentDetailPage(page, context);
185
+ await expect(page.locator("body")).toContainText("Send the weekly standup");
186
+ });
187
+
188
+ test('token label "Production token" is visible', async ({
189
+ page,
190
+ context,
191
+ }) => {
192
+ await loadAgentDetailPage(page, context);
193
+ await expect(page.locator("body")).toContainText("Production token");
194
+ });
195
+
196
+ test("env var add form is present (action contains /envs)", async ({
197
+ page,
198
+ context,
199
+ }) => {
200
+ await loadAgentDetailPage(page, context);
201
+ const envForm = page.locator('form[action*="/envs"]').first();
202
+ await expect(envForm).toBeAttached();
203
+ });
204
+
205
+ test("add cron form is present (action contains /crons)", async ({
206
+ page,
207
+ context,
208
+ }) => {
209
+ await loadAgentDetailPage(page, context);
210
+ const cronForm = page.locator('form[action*="/crons"]').first();
211
+ await expect(cronForm).toBeAttached();
212
+ });
213
+
214
+ test("token form is present (action contains /tokens)", async ({
215
+ page,
216
+ context,
217
+ }) => {
218
+ await loadAgentDetailPage(page, context);
219
+ const tokenForm = page.locator('form[action*="/tokens"]').first();
220
+ await expect(tokenForm).toBeAttached();
221
+ });
222
+
223
+ test("mobile viewport (375px) — page renders without horizontal overflow", async ({
224
+ page,
225
+ context,
226
+ }) => {
227
+ await page.setViewportSize({ width: 375, height: 812 });
228
+ await loadAgentDetailPage(page, context);
229
+
230
+ const docScrollWidth = await page.evaluate(
231
+ () => document.documentElement.scrollWidth,
232
+ );
233
+ const innerWidth = await page.evaluate(() => window.innerWidth);
234
+ expect(docScrollWidth).toBeLessThanOrEqual(innerWidth + 1);
235
+ });
236
+
237
+ test("specialty name is visible on the detail page", async ({
238
+ page,
239
+ context,
240
+ }) => {
241
+ await loadAgentDetailPage(page, context);
242
+ await expect(page.locator("body")).toContainText("coding");
243
+ });
244
+
245
+ // CFB-3.1: .data-table .btn must stay at its current ~36px height at mobile
246
+ // widths — the mobile-wide .btn min-height:44px rule must not cascade
247
+ // through to buttons inside a .data-table (e.g. the env-var "Delete"
248
+ // button), or the tasks/PRs/env tables would become absurdly tall.
249
+ test("at 375px, a .data-table .btn (env var Delete button) stays near its current ~36px height, not 44px", async ({
250
+ page,
251
+ context,
252
+ }) => {
253
+ await page.setViewportSize({ width: 375, height: 812 });
254
+ await loadAgentDetailPage(page, context);
255
+
256
+ // Env Vars now lives inside a collapsed accordion group (AGA-1.1) —
257
+ // expand it before asserting on the visibility of anything inside.
258
+ await page
259
+ .locator("#env-vars")
260
+ .locator("xpath=ancestor::details[contains(@class, 'group')]")
261
+ .first()
262
+ .evaluate((details) => {
263
+ (details as HTMLDetailsElement).open = true;
264
+ });
265
+
266
+ const deleteBtn = page
267
+ .locator("#env-vars .data-table .btn", { hasText: "Delete" })
268
+ .first();
269
+ await expect(deleteBtn).toBeVisible();
270
+
271
+ const height = await deleteBtn.evaluate(
272
+ (el) => el.getBoundingClientRect().height,
273
+ );
274
+ expect(height).toBeLessThan(44);
275
+ expect(height).toBeGreaterThan(0);
276
+ });
277
+ });
278
+
279
+ // ─── New-specialty picker (ATS-4.1) ──────────────────────────────────────────
280
+
281
+ async function loadNewAgentPage(
282
+ page: Page,
283
+ context: BrowserContext,
284
+ ): Promise<void> {
285
+ const token = await mintSession();
286
+ await context.addCookies([
287
+ {
288
+ name: SESSION_COOKIE,
289
+ value: token,
290
+ domain: "localhost",
291
+ path: "/",
292
+ httpOnly: true,
293
+ sameSite: "Lax",
294
+ },
295
+ ]);
296
+ await page.goto(`${BASE_URL}/admin/agents/new`);
297
+ }
298
+
299
+ test.describe("GET /admin/agents/new — type picker", () => {
300
+ test("a required select[name=type] is present", async ({ page, context }) => {
301
+ await loadNewAgentPage(page, context);
302
+ const typeSelect = page.locator('select[name="type"]');
303
+ await expect(typeSelect).toBeAttached();
304
+ await expect(typeSelect).toHaveAttribute("required", "");
305
+ });
306
+
307
+ test('the select lists "Coding Agent" (value coding)', async ({
308
+ page,
309
+ context,
310
+ }) => {
311
+ await loadNewAgentPage(page, context);
312
+ const codingOption = page.locator(
313
+ 'select[name="type"] option[value="coding"]',
314
+ );
315
+ await expect(codingOption).toBeAttached();
316
+ await expect(codingOption).toHaveText("Coding Agent");
317
+ });
318
+ });
319
+
320
+ test.describe("POST /admin/agents — create-with-type journey", () => {
321
+ test("filling the form (incl. type) and submitting lands on the new agent's detail page showing its type", async ({
322
+ page,
323
+ context,
324
+ }) => {
325
+ await loadNewAgentPage(page, context);
326
+
327
+ await page.fill('input[name="name"]', "My New Coding Agent");
328
+ await page.selectOption('select[name="type"]', "coding");
329
+ // Scoped to the new-agent form specifically — the page also has a
330
+ // "Sign out" button[type="submit"] in the toolbar, and an unscoped
331
+ // selector matches that one first (it's earlier in the DOM), which
332
+ // logs the session out instead of submitting this form.
333
+ await page.click('form[action="/admin/agents"] button[type="submit"]');
334
+
335
+ await page.waitForURL(/\/admin\/agents\/[^/]+$/);
336
+ await expect(page.locator("body")).toContainText("coding");
337
+ });
338
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * admin/e2e/chat-degraded.e2e.ts
3
+ * Admin UI — Chat Degraded Mode E2E Tests (CFB-2.3)
4
+ *
5
+ * Proves that `chatClient` is a genuinely injectable dependency of the test
6
+ * server, not a hardcoded mock — and that the three other chat e2e suites
7
+ * (chat-page.e2e.ts, chat-thread-page.e2e.ts, chat-progress.e2e.ts) are
8
+ * exercising the real chat UI rather than silently passing against a
9
+ * degraded fallback page.
10
+ *
11
+ * When the server is spawned with ADMIN_E2E_CHAT_CLIENT_ABSENT=1, test-server.ts
12
+ * builds AdminUIDeps with chatClient: undefined, which is the same condition
13
+ * production hits when SHIPWRIGHT_CHAT_SERVICE_URL /
14
+ * SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN are unset. Both /admin/chat and the
15
+ * per-thread route must then render the "Chat service not configured." alert
16
+ * instead of the real thread list / message UI.
17
+ *
18
+ * Architecture mirrors chat-progress.e2e.ts: spawns admin/e2e/test-server.ts
19
+ * via Bun as a child process, mints session cookies via hono/jwt.
20
+ */
21
+
22
+ import { type ChildProcess, spawn } from "node:child_process";
23
+ import { dirname, resolve } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { type BrowserContext, expect, test } from "@playwright/test";
26
+ import { sign } from "hono/jwt";
27
+
28
+ // ─── Constants ────────────────────────────────────────────────────────────────
29
+
30
+ const PORT = 3495;
31
+ const BASE_URL = `http://localhost:${PORT}`;
32
+ const SESSION_SECRET = "e2e-admin-test-secret-32chars!!!";
33
+ const SESSION_COOKIE = "admin_session";
34
+ const AGENT_ID = "agent-e2e-1";
35
+ // Matches MOCK_CHAT_THREAD.id in test-server.ts.
36
+ const THREAD_ID = "thread-e2e-1";
37
+
38
+ const __dirname = dirname(fileURLToPath(import.meta.url));
39
+
40
+ // ─── Server lifecycle ─────────────────────────────────────────────────────────
41
+
42
+ let serverProcess: ChildProcess | null = null;
43
+
44
+ async function waitForServer(url: string, maxWaitMs: number): Promise<void> {
45
+ const start = Date.now();
46
+ while (Date.now() - start < maxWaitMs) {
47
+ try {
48
+ const res = await fetch(url);
49
+ if (res.status < 500) return;
50
+ } catch {
51
+ // Not ready yet
52
+ }
53
+ await new Promise((r) => setTimeout(r, 300));
54
+ }
55
+ throw new Error(`Server at ${url} did not start within ${maxWaitMs}ms`);
56
+ }
57
+
58
+ test.beforeAll(async () => {
59
+ const serverScript = resolve(__dirname, "test-server.ts");
60
+
61
+ serverProcess = spawn("bun", ["run", serverScript], {
62
+ env: {
63
+ ...process.env,
64
+ ADMIN_E2E_PORT: String(PORT),
65
+ ADMIN_E2E_SESSION_SECRET: SESSION_SECRET,
66
+ // Configure this spawned server to run with no chatClient at all, so
67
+ // the /admin/chat* routes fall back to degraded-mode rendering.
68
+ ADMIN_E2E_CHAT_CLIENT_ABSENT: "1",
69
+ },
70
+ stdio: "pipe",
71
+ cwd: resolve(__dirname, "../.."),
72
+ });
73
+
74
+ serverProcess.stderr?.on("data", (data: Buffer) => {
75
+ const msg = data.toString();
76
+ if (msg.trim()) console.error("[admin-e2e-chat-degraded-server]", msg.trim());
77
+ });
78
+
79
+ serverProcess.stdout?.on("data", (data: Buffer) => {
80
+ const msg = data.toString();
81
+ if (msg.trim()) console.log("[admin-e2e-chat-degraded-server]", msg.trim());
82
+ });
83
+
84
+ await waitForServer(`${BASE_URL}/health`, 15_000);
85
+ });
86
+
87
+ test.afterAll(() => {
88
+ if (serverProcess) {
89
+ serverProcess.kill("SIGTERM");
90
+ serverProcess = null;
91
+ }
92
+ });
93
+
94
+ // ─── Session cookie helper ────────────────────────────────────────────────────
95
+
96
+ async function mintSession(
97
+ userId = "google-sub-e2e",
98
+ email = "admin@example.com",
99
+ ): Promise<string> {
100
+ const nowSec = Math.floor(Date.now() / 1000);
101
+ return sign(
102
+ { userId, email, isAdmin: true, iat: nowSec, exp: nowSec + 3600 },
103
+ SESSION_SECRET,
104
+ "HS256",
105
+ );
106
+ }
107
+
108
+ async function authenticate(context: BrowserContext): Promise<void> {
109
+ const token = await mintSession();
110
+ await context.addCookies([
111
+ {
112
+ name: SESSION_COOKIE,
113
+ value: token,
114
+ domain: "localhost",
115
+ path: "/",
116
+ httpOnly: true,
117
+ sameSite: "Lax",
118
+ },
119
+ ]);
120
+ }
121
+
122
+ // ─── Tests ────────────────────────────────────────────────────────────────────
123
+
124
+ test.describe("GET /admin/chat* — CFB-2.3 degraded mode (chatClient absent)", () => {
125
+ test("the chat list page renders the degraded alert instead of the thread list", async ({
126
+ page,
127
+ context,
128
+ }) => {
129
+ await authenticate(context);
130
+ await page.goto(`${BASE_URL}/admin/chat?agentId=${AGENT_ID}`);
131
+
132
+ await expect(page.locator(".alert.alert-error")).toContainText(
133
+ "Chat service not configured.",
134
+ );
135
+ await expect(page.locator(".thread-pane-list")).toHaveCount(0);
136
+ });
137
+
138
+ test("the thread detail page renders the degraded alert instead of the message UI", async ({
139
+ page,
140
+ context,
141
+ }) => {
142
+ await authenticate(context);
143
+ await page.goto(
144
+ `${BASE_URL}/admin/chat/${AGENT_ID}/threads/${THREAD_ID}`,
145
+ );
146
+
147
+ await expect(page.locator(".alert.alert-error")).toContainText(
148
+ "Chat service not configured.",
149
+ );
150
+ await expect(page.locator("#live-status-bubble")).toHaveCount(0);
151
+ await expect(page.locator("#messages-container")).toHaveCount(0);
152
+ });
153
+ });