@app-vitals/agent-admin 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/Dockerfile +43 -0
  2. package/README.md +72 -2
  3. package/e2e/agents-page.e2e.ts +338 -0
  4. package/e2e/chat-degraded.e2e.ts +153 -0
  5. package/e2e/chat-page.e2e.ts +169 -0
  6. package/e2e/chat-progress.e2e.ts +206 -0
  7. package/e2e/chat-thread-page.e2e.ts +444 -0
  8. package/e2e/login-page.e2e.ts +148 -0
  9. package/e2e/test-server.ts +427 -0
  10. package/openapi.json +6098 -0
  11. package/openapi.platform.json +5553 -0
  12. package/package.json +64 -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/src/api.ts ADDED
@@ -0,0 +1,376 @@
1
+ /**
2
+ * agent/src/api.ts
3
+ * Hono runtime API — GET /:id/config and GET /:id/crons.
4
+ *
5
+ * Mounted at /agents/* via root.route("/agents", runtimeApp).
6
+ * Hono v4's .route() strips the prefix before dispatching — routes must be
7
+ * registered without the /agents prefix so they resolve correctly at
8
+ * GET /agents/:id/config and GET /agents/:id/crons from the root.
9
+ *
10
+ * Auth: same admin-key / per-agent-token / session-cookie middleware as the
11
+ * CRUD routes (SHIPWRIGHT_INTERNAL_API_KEY removed in UNI-1.2).
12
+ * This is the endpoint the harness polls every 60s.
13
+ *
14
+ * NOTE: Auth middleware is scoped per-route (not global app.use("*")).
15
+ * Using app.use("*") in a sub-app mounted via root.route("/agents", runtimeApp)
16
+ * causes Hono v4 to hoist the middleware as a /agents/* guard in root — which
17
+ * blocks all admin CRUD requests (POST/PATCH/DELETE /agents/:id/*) before they
18
+ * reach the admin handlers. Per-route middleware confines the check to only the
19
+ * two routes that need it.
20
+ */
21
+
22
+ import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
23
+ import type { AgentCronJob } from "./agent-cron-jobs.ts";
24
+ import type { AgentEnvBundle } from "./agent-envs.ts";
25
+ import type { AgentTokenService } from "./agent-tokens.ts";
26
+ import { anthropicCredentialKeysIn } from "./anthropic-credential-guard.ts";
27
+ import { type AdminApiKey, createAdminAuthMiddleware } from "./api-auth.ts";
28
+ import {
29
+ AGENT_PHASES,
30
+ AgentConfigResponseSchema,
31
+ AgentCronJobSchema,
32
+ AgentIdParamSchema,
33
+ RuntimeErrorSchema,
34
+ } from "./openapi-schemas.ts";
35
+
36
+ // ─── Types ────────────────────────────────────────────────────────────────────
37
+
38
+ export interface AgentPlugin {
39
+ marketplace: string;
40
+ plugin: string;
41
+ }
42
+
43
+ export interface AgentConfigResponse {
44
+ env: Record<string, string>;
45
+ allowedTools: string[];
46
+ plugins: AgentPlugin[];
47
+ repos: string[];
48
+ reviewAuthorAllowlist: string[];
49
+ patchAuthorAllowlist: string[];
50
+ restrictSlackToMembers: boolean;
51
+ memberEmails: string[];
52
+ /**
53
+ * ATE-3.1: ISO timestamp of trial expiry, or null/absent when no trial is
54
+ * configured. Optional (rather than a required nullable field) so existing
55
+ * fixtures/doubles built against this interface before ATE-3.1 keep
56
+ * compiling unchanged. Read by agent/src/index.ts's syncConfig() to sync
57
+ * agent-trial-expiry-ref.ts's live ref, which agent/src/runtime/slack.ts's
58
+ * isTrialExpired() gate reads to block Slack access post-expiry.
59
+ */
60
+ trialExpiresAt?: string | null;
61
+ /**
62
+ * SSP-8.3: status of the agent's owning Account ('active' | 'suspended' |
63
+ * 'trial_expired'), or null/absent when the agent has no account. Optional
64
+ * for the same fixture-compat reason as trialExpiresAt. Read by
65
+ * agent/src/index.ts's syncConfig() to drive slack.ts's isAccountPaused().
66
+ */
67
+ accountStatus?: string | null;
68
+ /**
69
+ * ARD-6.1: the agent's Specialty name (e.g. "coding"). Additive and
70
+ * optional — fixtures/doubles built before this field keep compiling, and
71
+ * the agent runtime treats an absent value as "coding".
72
+ */
73
+ typeName?: string;
74
+ /**
75
+ * PMC-1.1: phase -> subagentType (or null) map, covering all six pipeline
76
+ * phases regardless of whether the agent has an explicit override row for
77
+ * each — phases without a row default to null. The live handler always
78
+ * populates this (see AgentConfigResponseSchema, which requires it), but
79
+ * the TS interface field stays optional — mirroring trialExpiresAt above —
80
+ * so existing fixtures/doubles built against this interface before PMC-1.1
81
+ * (and lib/admin-types.ts's openapi-fetch `paths` type, regenerated
82
+ * separately from admin/openapi.json) keep compiling unchanged.
83
+ */
84
+ phaseMethodology?: Record<string, string | null>;
85
+ /**
86
+ * APM-1.5: direct passthrough of the 6 agent-policy fields APM-1.1 added
87
+ * to the Agent row. Required fields — no prior-fixture-compat concern
88
+ * since they didn't exist before this task.
89
+ */
90
+ autoPostReviews: boolean;
91
+ allowSelfReview: boolean;
92
+ minConfidence: number;
93
+ maxFindings: number;
94
+ cleanupMergedWorktrees: boolean;
95
+ cleanupAfterDays: number;
96
+ }
97
+
98
+ interface AgentEnvServiceLike {
99
+ getConfigBundle(agentId: string): Promise<AgentEnvBundle | null>;
100
+ }
101
+
102
+ interface AgentCronJobServiceLike {
103
+ list(agentId: string): Promise<AgentCronJob[]>;
104
+ listWithRunSummary?(agentId: string): Promise<
105
+ Array<
106
+ AgentCronJob & {
107
+ lastRun: null | {
108
+ startedAt: Date;
109
+ completedAt: Date | null;
110
+ skipped: boolean;
111
+ outcome: string | null;
112
+ };
113
+ runCountToday: number;
114
+ }
115
+ >
116
+ >;
117
+ }
118
+
119
+ interface AgentServiceLike {
120
+ getById(agentId: string): Promise<{
121
+ id: string;
122
+ repos: string[];
123
+ reviewAuthorAllowlist: string[];
124
+ patchAuthorAllowlist: string[];
125
+ restrictSlackToMembers: boolean;
126
+ memberEmails: string[];
127
+ /** ATE-3.1: optional so pre-existing test doubles built against this interface keep compiling unchanged. */
128
+ trialExpiresAt?: Date | null;
129
+ /** SSP-8.3: owning account status; optional for pre-existing test doubles. */
130
+ accountStatus?: string | null;
131
+ typeName?: string;
132
+ /** WIF-2.1: optional so pre-existing test doubles keep compiling; absent is treated as not self-hosted. */
133
+ selfHosted?: boolean;
134
+ /** APM-1.5: the 6 agent-policy fields, direct-passthrough into the response. */
135
+ autoPostReviews: boolean;
136
+ allowSelfReview: boolean;
137
+ minConfidence: number;
138
+ maxFindings: number;
139
+ cleanupMergedWorktrees: boolean;
140
+ cleanupAfterDays: number;
141
+ } | null>;
142
+ }
143
+
144
+ interface AgentPluginServiceLike {
145
+ listEnabled(agentId: string): Promise<Array<{ name: string }>>;
146
+ }
147
+
148
+ export interface AgentPhaseMethodologyServiceLike {
149
+ list(
150
+ agentId: string,
151
+ ): Promise<Array<{ phase: string; subagentType: string | null }>>;
152
+ }
153
+
154
+ export interface AgentRuntimeDeps {
155
+ agentEnvService: AgentEnvServiceLike;
156
+ agentCronJobService: AgentCronJobServiceLike;
157
+ agentService: AgentServiceLike;
158
+ agentPluginService: AgentPluginServiceLike;
159
+ /**
160
+ * Optional: supplied by an AdminExtension (the coding extension). Absent →
161
+ * GET /agents/:id/config omits `phaseMethodology`.
162
+ */
163
+ agentPhaseMethodologyService?: AgentPhaseMethodologyServiceLike;
164
+ /** Session secret for cookie auth (SHIPWRIGHT_SESSION_SECRET). */
165
+ sessionSecret: string;
166
+ /** Parsed SHIPWRIGHT_ADMIN_API_KEYS — optional; absent means env key auth is disabled. */
167
+ adminApiKeys?: Map<string, AdminApiKey>;
168
+ /** Token service for per-agent bearer token validation. */
169
+ agentTokenService: Pick<AgentTokenService, "validate">;
170
+ /** WIF-2.1: Anthropic federation is enabled; defaults to false. */
171
+ anthropicFederationEnabled?: boolean;
172
+ }
173
+
174
+ // ─── Route definitions ────────────────────────────────────────────────────────
175
+
176
+ const getConfigRoute = createRoute({
177
+ method: "get",
178
+ path: "/:id/config",
179
+ tags: ["runtime"],
180
+ summary: "Get agent config bundle",
181
+ description:
182
+ "Returns the agent's full runtime config bundle — decrypted env vars, allowed-tools patterns, installed plugins (with derived marketplace), scoped repos, review/patch author allowlists, and Slack membership-restriction settings. Polled by the agent harness on startup and during its config sync loop. Returns 404 if the agent doesn't exist.",
183
+ security: [{ bearerAuth: [] }],
184
+ request: {
185
+ params: AgentIdParamSchema,
186
+ },
187
+ responses: {
188
+ 200: {
189
+ description: "Agent config bundle",
190
+ content: { "application/json": { schema: AgentConfigResponseSchema } },
191
+ },
192
+ 401: {
193
+ description: "Unauthorized",
194
+ content: { "application/json": { schema: RuntimeErrorSchema } },
195
+ },
196
+ 404: {
197
+ description: "Agent not found",
198
+ content: { "application/json": { schema: RuntimeErrorSchema } },
199
+ },
200
+ },
201
+ });
202
+
203
+ const getCronsRoute = createRoute({
204
+ method: "get",
205
+ path: "/:id/crons",
206
+ tags: ["runtime"],
207
+ summary: "List agent cron jobs",
208
+ description:
209
+ "Returns the agent's cron jobs as a plain array (not wrapped), used by the agent harness's scheduler. Returns 404 if the agent doesn't exist.",
210
+ security: [{ bearerAuth: [] }],
211
+ request: {
212
+ params: AgentIdParamSchema,
213
+ },
214
+ responses: {
215
+ 200: {
216
+ description: "Array of cron jobs",
217
+ content: {
218
+ "application/json": { schema: z.array(AgentCronJobSchema) },
219
+ },
220
+ },
221
+ 401: {
222
+ description: "Unauthorized",
223
+ content: { "application/json": { schema: RuntimeErrorSchema } },
224
+ },
225
+ 404: {
226
+ description: "Agent not found",
227
+ content: { "application/json": { schema: RuntimeErrorSchema } },
228
+ },
229
+ },
230
+ });
231
+
232
+ // ─── Factory ──────────────────────────────────────────────────────────────────
233
+
234
+ /**
235
+ * Creates the OpenAPIHono runtime API app.
236
+ *
237
+ * Inject real services for production; inject mocks for tests.
238
+ */
239
+ export function createAgentRuntimeApp(deps: AgentRuntimeDeps): OpenAPIHono {
240
+ const {
241
+ agentEnvService,
242
+ agentCronJobService,
243
+ agentService,
244
+ agentPluginService,
245
+ agentPhaseMethodologyService,
246
+ } = deps;
247
+
248
+ const app = new OpenAPIHono();
249
+
250
+ // Auth middleware — applied per-route, NOT as app.use("*").
251
+ // See file-level comment for why global middleware is avoided here.
252
+ const requireAuth = createAdminAuthMiddleware({
253
+ sessionSecret: deps.sessionSecret,
254
+ adminApiKeys: deps.adminApiKeys,
255
+ agentTokenService: deps.agentTokenService,
256
+ });
257
+
258
+ // ─── GET /:id/config ─────────────────────────────────────────────────────
259
+ // Reachable from root as GET /agents/:id/config (Hono v4 strips prefix)
260
+
261
+ app.use("/:id/config", requireAuth);
262
+
263
+ app.openapi(getConfigRoute, async (c) => {
264
+ const { id } = c.req.valid("param");
265
+
266
+ // Check agent existence
267
+ const agent = await agentService.getById(id);
268
+ if (!agent) {
269
+ return c.json({ error: "Not found" }, 404);
270
+ }
271
+
272
+ // Fetch env bundle, plugins, and phase-methodology overrides in parallel
273
+ const [bundle, plugins, phaseMethodologyRows] = await Promise.all([
274
+ agentEnvService.getConfigBundle(id),
275
+ agentPluginService.listEnabled(id),
276
+ agentPhaseMethodologyService?.list(id),
277
+ ]);
278
+
279
+ // WIF-2.1: with federation enabled, a non-self-hosted agent must never be
280
+ // handed Anthropic credentials, even ones already stored in its bundle
281
+ // (the agent re-reads this endpoint every 60s). Log names only.
282
+ let env = bundle?.env ?? {};
283
+ if (deps.anthropicFederationEnabled && !agent.selfHosted) {
284
+ const blocked = anthropicCredentialKeysIn(Object.keys(env));
285
+ if (blocked.length > 0) {
286
+ console.warn(
287
+ `[api] stripped Anthropic credentials from agent ${id} config under federation: ${blocked.join(", ")}`,
288
+ );
289
+ env = Object.fromEntries(
290
+ Object.entries(env).filter(([k]) => !blocked.includes(k)),
291
+ );
292
+ }
293
+ }
294
+
295
+ // Every phase defaults to null; override rows (if any) fill in the rest.
296
+ const phaseMethodology: Record<string, string | null> = Object.fromEntries(
297
+ AGENT_PHASES.map((phase) => [phase, null]),
298
+ );
299
+ for (const row of phaseMethodologyRows ?? []) {
300
+ phaseMethodology[row.phase] = row.subagentType;
301
+ }
302
+
303
+ // Not annotated as `: AgentConfigResponse` — that interface's
304
+ // phaseMethodology field is deliberately optional (see its doc comment)
305
+ // for downstream consumer/fixture compatibility, but this handler always
306
+ // populates every field, so leaving the literal un-annotated lets it
307
+ // infer the fully-required shape the route's Zod response schema (which
308
+ // requires phaseMethodology) expects. `c.json()` below still structurally
309
+ // checks this literal against that schema-derived type.
310
+ const response = {
311
+ env,
312
+ allowedTools: bundle?.allowedTools ?? [],
313
+ plugins: plugins.map((p) => {
314
+ // The stored name is the canonical Claude plugin spec — exactly what
315
+ // you'd pass to `claude plugin install`: "<plugin>@<marketplace>"
316
+ // (e.g. "shipwright@shipwright"), or a bare "<plugin>" that defaults to
317
+ // the bundled "shipwright" marketplace. The harness reassembles
318
+ // "<plugin>@<marketplace>" from these two fields.
319
+ const at = p.name.indexOf("@");
320
+ return at === -1
321
+ ? { marketplace: "shipwright", plugin: p.name }
322
+ : { plugin: p.name.slice(0, at), marketplace: p.name.slice(at + 1) };
323
+ }),
324
+ repos: agent.repos,
325
+ reviewAuthorAllowlist: agent.reviewAuthorAllowlist,
326
+ patchAuthorAllowlist: agent.patchAuthorAllowlist,
327
+ restrictSlackToMembers: agent.restrictSlackToMembers ?? false,
328
+ memberEmails: agent.memberEmails ?? [],
329
+ trialExpiresAt: agent.trialExpiresAt
330
+ ? agent.trialExpiresAt.toISOString()
331
+ : null,
332
+ accountStatus: agent.accountStatus ?? null,
333
+ typeName: agent.typeName,
334
+ // Omitted entirely when no phase-methodology service was supplied
335
+ // (platform-only admin, no coding extension).
336
+ ...(agentPhaseMethodologyService ? { phaseMethodology } : {}),
337
+ // APM-1.5: direct passthrough — the DB column always has a value
338
+ // (Prisma schema default), so no `??` fallback is needed.
339
+ autoPostReviews: agent.autoPostReviews,
340
+ allowSelfReview: agent.allowSelfReview,
341
+ minConfidence: agent.minConfidence,
342
+ maxFindings: agent.maxFindings,
343
+ cleanupMergedWorktrees: agent.cleanupMergedWorktrees,
344
+ cleanupAfterDays: agent.cleanupAfterDays,
345
+ };
346
+
347
+ // The route's Zod schema requires phaseMethodology; a platform-only admin
348
+ // omits it, so the response is asserted to the schema-derived shape.
349
+ return c.json(
350
+ response as typeof response & {
351
+ phaseMethodology: Record<string, string | null>;
352
+ },
353
+ 200,
354
+ );
355
+ });
356
+
357
+ // ─── GET /:id/crons ──────────────────────────────────────────────────────
358
+ // Reachable from root as GET /agents/:id/crons (Hono v4 strips prefix)
359
+
360
+ app.use("/:id/crons", requireAuth);
361
+
362
+ app.openapi(getCronsRoute, async (c) => {
363
+ const { id } = c.req.valid("param");
364
+
365
+ // Check agent existence
366
+ const agent = await agentService.getById(id);
367
+ if (!agent) {
368
+ return c.json({ error: "Not found" }, 404);
369
+ }
370
+
371
+ const crons = await agentCronJobService.list(id);
372
+ return c.json(crons, 200);
373
+ });
374
+
375
+ return app;
376
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * admin/src/attachment-validation.ts
3
+ *
4
+ * Pure, I/O-free validation for chat message attachments. Enforces the size cap
5
+ * and an allowlist of MIME types before an upload is ever handed to the chat
6
+ * service. Kept side-effect-free so it is trivially unit-testable.
7
+ */
8
+
9
+ /** Maximum allowed attachment size (10 MB) — mirrors the chat service guard. */
10
+ export const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
11
+
12
+ /** MIME type prefixes accepted wholesale (e.g. any text/* file). */
13
+ const ALLOWED_MIME_PREFIXES = ["text/"];
14
+
15
+ /** Exact MIME types accepted beyond the prefix list. */
16
+ const ALLOWED_MIME_EXACT = new Set([
17
+ "image/jpeg",
18
+ "image/png",
19
+ "image/gif",
20
+ "image/webp",
21
+ "image/svg+xml",
22
+ "image/heic",
23
+ "image/heif",
24
+ "application/pdf",
25
+ "application/json",
26
+ // VM-3.1: mic-recorded blobs are submitted through this same upload path —
27
+ // these are the exact MIME strings Chrome/Firefox's MediaRecorder emits
28
+ // (browser/codec dependent, so all three are allowed).
29
+ "audio/webm",
30
+ "audio/webm;codecs=opus",
31
+ "audio/ogg",
32
+ // Safari/iOS MediaRecorder output, plus common TTS/upload audio formats.
33
+ "audio/mp4",
34
+ "audio/mpeg",
35
+ "audio/wav",
36
+ ]);
37
+
38
+ /**
39
+ * Filename extensions rendered as an inline <audio> player instead of a
40
+ * plain filename badge (VM-3.1). No MIME type is persisted end-to-end on a
41
+ * ChatMessage — the chat service only stores attachmentFilename/attachmentSize
42
+ * — so audio-ness is inferred from the filename extension. Covers both
43
+ * MediaRecorder-authored user attachments (.webm/.ogg, this task) and
44
+ * TTS-authored assistant attachments (.wav/.mp3, from VM-2.1's chat-poller).
45
+ */
46
+ const AUDIO_FILE_EXTENSIONS = [".wav", ".mp3", ".webm", ".ogg", ".m4a"];
47
+
48
+ const AUDIO_CONTENT_TYPES: Record<string, string> = {
49
+ ".wav": "audio/wav",
50
+ ".mp3": "audio/mpeg",
51
+ ".webm": "audio/webm",
52
+ ".ogg": "audio/ogg",
53
+ ".m4a": "audio/mp4",
54
+ };
55
+
56
+ /**
57
+ * Map an audio filename's extension to a real `audio/*` Content-Type, for
58
+ * the admin attachment-proxy route to serve. The chat service's own
59
+ * GET /:id/attachment always streams `application/octet-stream`
60
+ * (chat/src/routes/messages.ts), which most browsers refuse to play inline
61
+ * in an <audio> element — the proxy route re-labels the response using this
62
+ * mapping instead. Returns null for a non-audio (or unrecognized) filename.
63
+ */
64
+ export function audioContentTypeForFilename(
65
+ filename: string | null | undefined,
66
+ ): string | null {
67
+ if (!filename) return null;
68
+ const lower = filename.toLowerCase();
69
+ const ext = AUDIO_FILE_EXTENSIONS.find((e) => lower.endsWith(e));
70
+ return ext ? (AUDIO_CONTENT_TYPES[ext] ?? null) : null;
71
+ }
72
+
73
+ /** Whether an attachment's filename indicates audio content. */
74
+ export function isAudioFilename(filename: string | null | undefined): boolean {
75
+ return audioContentTypeForFilename(filename) !== null;
76
+ }
77
+
78
+ type AttachmentValidationResult =
79
+ | { ok: false; error: string; status: 413 | 415 }
80
+ | { ok: true; filename: string; size: number };
81
+
82
+ /**
83
+ * Validate an attachment by filename, byte size, and MIME type.
84
+ * Returns a discriminated result: on failure it carries a clear error message
85
+ * and the appropriate HTTP status (413 too large, 415 unsupported type).
86
+ */
87
+ export function validateAttachment(
88
+ filename: string,
89
+ size: number,
90
+ mimeType: string,
91
+ ): AttachmentValidationResult {
92
+ if (size > MAX_ATTACHMENT_BYTES) {
93
+ const mb = Math.round((size / 1024 / 1024) * 10) / 10;
94
+ return {
95
+ ok: false,
96
+ status: 413,
97
+ error: `Attachment exceeds the 10 MB limit (received ~${mb} MB)`,
98
+ };
99
+ }
100
+
101
+ // Strip parameters (";codecs=opus") and case before matching; the exact set
102
+ // still lists the codecs variant for backward compatibility.
103
+ const baseType = mimeType.split(";")[0].trim().toLowerCase();
104
+ // Chromium reports audio-only MediaRecorder output as video/webm — accept it
105
+ // only for .webm filenames so arbitrary video uploads stay rejected.
106
+ const isWebmRecording =
107
+ baseType === "video/webm" && filename.toLowerCase().endsWith(".webm");
108
+
109
+ const isAllowed =
110
+ ALLOWED_MIME_EXACT.has(mimeType) ||
111
+ ALLOWED_MIME_EXACT.has(baseType) ||
112
+ isWebmRecording ||
113
+ ALLOWED_MIME_PREFIXES.some((prefix) => baseType.startsWith(prefix));
114
+
115
+ if (!isAllowed) {
116
+ return {
117
+ ok: false,
118
+ status: 415,
119
+ error: `Attachment type "${mimeType || "unknown"}" is not allowed`,
120
+ };
121
+ }
122
+
123
+ return { ok: true, filename, size };
124
+ }