@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,236 @@
1
+ /**
2
+ * admin/src/specialty-registry.ts
3
+ *
4
+ * Specialty manifest schema — the single source of truth for the
5
+ * `shipwright.dev/v1alpha1` / `Specialty` YAML manifest that declares an
6
+ * specialty's identity, crons, plugins, tools, env contract, membership,
7
+ * repos, and chat/voice toggles.
8
+ *
9
+ * Named "specialty-registry" (not "manifest") to avoid colliding with the
10
+ * two existing "manifest" concepts in this repo: the Kubernetes manifests in
11
+ * ./agent-manifest.ts and the Slack app manifest. This module is unrelated
12
+ * to either — it's a declarative spec fed into scripts/CLI tooling, not a
13
+ * wire type, so it uses plain `zod` rather than `@hono/zod-openapi`'s
14
+ * `.openapi()`-augmented `z` (kept simple for JSON Schema conversion).
15
+ *
16
+ * Manifests are treated as a trust boundary: they may originate from
17
+ * third-party contributors, so every object schema is `.strict()` (unknown
18
+ * keys rejected, not silently stripped) and the env contract is
19
+ * deliberately keys-only — no `value`/`default` field is permitted on an
20
+ * env entry, so a manifest can declare *that* a var is needed without ever
21
+ * carrying a real value.
22
+ */
23
+
24
+ import { isOrgRepo } from "@app-vitals/agent-lib/org-repo";
25
+ import { parse as parseYaml } from "yaml";
26
+ import { z } from "zod";
27
+
28
+ /** RFC1123 label pattern (see admin/src/agent-manifest.ts's sanitizeAgentName). */
29
+ const RFC1123_NAME = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/;
30
+
31
+ // ─── metadata.skills[] (A2A-aligned) ───────────────────────────────────────
32
+
33
+ /**
34
+ * Minimal A2A (Agent2Agent) "skill" shape: what the specialty can do, as a
35
+ * small catalog entry. Kept intentionally narrow — id/name/description plus
36
+ * optional free-form tags — since this repo has no existing A2A convention
37
+ * to align with beyond the general shape of an agent capability card.
38
+ */
39
+ const SpecialtySkillSchema = z
40
+ .object({
41
+ id: z.string().min(1),
42
+ name: z.string().min(1),
43
+ description: z.string().min(1),
44
+ tags: z.array(z.string()).optional(),
45
+ })
46
+ .strict();
47
+
48
+ // ─── metadata ───────────────────────────────────────────────────────────────
49
+
50
+ const SpecialtyMetadataSchema = z
51
+ .object({
52
+ name: z
53
+ .string()
54
+ .regex(RFC1123_NAME, "metadata.name must be a valid RFC1123 label"),
55
+ displayName: z.string().min(1),
56
+ description: z.string().min(1),
57
+ version: z.string().min(1),
58
+ skills: z.array(SpecialtySkillSchema),
59
+ })
60
+ .strict();
61
+
62
+ // ─── identity ───────────────────────────────────────────────────────────────
63
+
64
+ const SpecialtyIdentitySchema = z
65
+ .object({
66
+ templatesDir: z.string().min(1),
67
+ })
68
+ .strict();
69
+
70
+ // ─── crons[] ────────────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * A single cron template declared inside a Specialty manifest. Distinct
74
+ * from the runtime, DB-backed AgentCronJobSchema (admin/src/openapi-schemas.ts)
75
+ * and CreateAgentCronJobInput (admin/src/agent-cron-jobs.ts) — those describe
76
+ * per-agent-instance rows already created in Postgres; this describes a
77
+ * *template* inside a versioned spec. `parentCron` here is a name reference
78
+ * resolved within this manifest's own crons[] array (validated by the
79
+ * top-level schema's superRefine below), not a DB id reference.
80
+ */
81
+ const SpecialtyCronSchema = z
82
+ .object({
83
+ name: z.string().min(1),
84
+ schedule: z.string().min(1),
85
+ prompt: z.string().min(1),
86
+ preCheck: z.string().optional(),
87
+ silent: z.boolean().optional().default(false),
88
+ enabled: z.boolean().optional().default(true),
89
+ parentCron: z.string().optional(),
90
+ })
91
+ .strict();
92
+
93
+ // ─── env contract ───────────────────────────────────────────────────────────
94
+
95
+ /**
96
+ * A single env contract entry. Keys-only by design: a manifest declares that
97
+ * a var is needed (and whether it's secret) — it never carries a `value` or
98
+ * `default`. `.strict()` ensures an unexpected `value` key is rejected, not
99
+ * silently stripped, since manifests are potential third-party input.
100
+ */
101
+ const SpecialtyEnvEntrySchema = z
102
+ .object({
103
+ key: z.string().min(1),
104
+ description: z.string().min(1),
105
+ secret: z.boolean(),
106
+ })
107
+ .strict();
108
+
109
+ const SpecialtyEnvSchema = z
110
+ .object({
111
+ required: z.array(SpecialtyEnvEntrySchema),
112
+ optional: z.array(SpecialtyEnvEntrySchema),
113
+ })
114
+ .strict();
115
+
116
+ // ─── resources override (optional) ─────────────────────────────────────────
117
+
118
+ /**
119
+ * Optional override of the default agent container resources (see
120
+ * resolveAgentContainerResources in admin/src/agent-manifest.ts). All fields
121
+ * optional — a manifest may override just one of requests/limits.
122
+ *
123
+ * Not yet wired to any provisioner — parsed as part of
124
+ * SpecialtyManifestSchema.resources but inert until a future task consumes it
125
+ * ahead of resolveAgentContainerResources's env-override layer (see that
126
+ * function's precedence-rule doc comment).
127
+ */
128
+ const SpecialtyResourceListSchema = z
129
+ .object({
130
+ cpu: z.string().optional(),
131
+ memory: z.string().optional(),
132
+ "ephemeral-storage": z.string().optional(),
133
+ })
134
+ .strict();
135
+
136
+ const SpecialtyResourcesSchema = z
137
+ .object({
138
+ requests: SpecialtyResourceListSchema.optional(),
139
+ limits: SpecialtyResourceListSchema.optional(),
140
+ })
141
+ .strict();
142
+
143
+ // ─── Top-level Specialty manifest ──────────────────────────────────────────
144
+
145
+ export const SpecialtyManifestSchema = z
146
+ .object({
147
+ apiVersion: z.literal("shipwright.dev/v1alpha1"),
148
+ kind: z.literal("Specialty"),
149
+ metadata: SpecialtyMetadataSchema,
150
+ identity: SpecialtyIdentitySchema,
151
+ crons: z.array(SpecialtyCronSchema),
152
+ plugins: z.array(z.string()),
153
+ tools: z.array(z.string()),
154
+ env: SpecialtyEnvSchema,
155
+ members: z.array(z.string()),
156
+ repos: z.array(
157
+ z.string().refine(isOrgRepo, {
158
+ message: "each repo must be in org/repo format",
159
+ }),
160
+ ),
161
+ chat: z.boolean(),
162
+ voice: z.boolean(),
163
+ resources: SpecialtyResourcesSchema.optional(),
164
+ })
165
+ .strict()
166
+ .superRefine((manifest, ctx) => {
167
+ const cronNames = new Set(manifest.crons.map((c) => c.name));
168
+ manifest.crons.forEach((cron, index) => {
169
+ if (cron.parentCron === undefined) return;
170
+ if (cron.parentCron === cron.name) {
171
+ ctx.addIssue({
172
+ code: "custom",
173
+ path: ["crons", index, "parentCron"],
174
+ message: `parentCron "${cron.parentCron}" cannot reference its own cron entry`,
175
+ });
176
+ return;
177
+ }
178
+ if (!cronNames.has(cron.parentCron)) {
179
+ ctx.addIssue({
180
+ code: "custom",
181
+ path: ["crons", index, "parentCron"],
182
+ message: `parentCron "${cron.parentCron}" does not match any cron name in this manifest`,
183
+ });
184
+ }
185
+ });
186
+ });
187
+
188
+ export type SpecialtyManifest = z.infer<typeof SpecialtyManifestSchema>;
189
+
190
+ // ─── Parsing ────────────────────────────────────────────────────────────────
191
+
192
+ /**
193
+ * Parse and validate a YAML Specialty manifest string.
194
+ *
195
+ * Pure — no I/O. `content` is the already-read YAML text; throws a
196
+ * descriptive Error (with the underlying ZodError's field-path issues
197
+ * embedded in `message`, and the ZodError itself reachable via `cause`) if
198
+ * the manifest fails schema validation.
199
+ */
200
+ export function parseSpecialtyManifest(content: string): SpecialtyManifest {
201
+ const raw: unknown = parseYaml(content);
202
+ const result = SpecialtyManifestSchema.safeParse(raw);
203
+ if (!result.success) {
204
+ const details = result.error.issues
205
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
206
+ .join("; ");
207
+ throw new Error(`invalid Specialty manifest — ${details}`, {
208
+ cause: result.error,
209
+ });
210
+ }
211
+ return result.data;
212
+ }
213
+
214
+ // ─── JSON Schema generation ─────────────────────────────────────────────────
215
+
216
+ /**
217
+ * Build the JSON-serializable JSON Schema object for SpecialtyManifestSchema.
218
+ *
219
+ * Pure — no file I/O — so both scripts/generate-specialty-schema.ts's CLI
220
+ * entrypoint and its content test can call this directly instead of shelling
221
+ * out. Uses zod 4's built-in `z.toJSONSchema` (draft-07 target, to stay
222
+ * contract-compatible with the previous zod-to-json-schema output).
223
+ */
224
+ export function buildSpecialtyJsonSchema(): Record<string, unknown> {
225
+ const { $schema, ...definition } = z.toJSONSchema(SpecialtyManifestSchema, {
226
+ target: "draft-7",
227
+ io: "input",
228
+ });
229
+ // Wrap as a named definition (`$ref` + `definitions`) — the shape the
230
+ // previous zod-to-json-schema output used, so consumers see no change.
231
+ return {
232
+ $ref: "#/definitions/SpecialtyManifest",
233
+ definitions: { SpecialtyManifest: definition },
234
+ $schema,
235
+ };
236
+ }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * admin/src/specialty-registry.unit.test.ts
3
+ * Unit tests for the pure Specialty manifest schema + parser. No I/O, no
4
+ * network, no fs — parseSpecialtyManifest takes a YAML string in memory and
5
+ * returns/throws in-process.
6
+ */
7
+
8
+ import { describe, expect, it } from "bun:test";
9
+ import { stringify as stringifyYaml } from "yaml";
10
+ import {
11
+ SpecialtyManifestSchema,
12
+ parseSpecialtyManifest,
13
+ } from "./specialty-registry.ts";
14
+
15
+ // ─── Fixtures ───────────────────────────────────────────────────────────────
16
+
17
+ /** A minimal valid manifest object. Pass overrides for the fields a test cares about. */
18
+ function baseManifest(overrides: Record<string, unknown> = {}) {
19
+ return {
20
+ apiVersion: "shipwright.dev/v1alpha1",
21
+ kind: "Specialty",
22
+ metadata: {
23
+ name: "dude-agent",
24
+ displayName: "The Dude",
25
+ description: "A laid-back team member agent.",
26
+ version: "1.0.0",
27
+ skills: [],
28
+ },
29
+ identity: { templatesDir: "templates/dude" },
30
+ crons: [],
31
+ plugins: [],
32
+ tools: [],
33
+ env: { required: [], optional: [] },
34
+ members: [],
35
+ repos: [],
36
+ chat: true,
37
+ voice: false,
38
+ ...overrides,
39
+ };
40
+ }
41
+
42
+ /** Round-trips a manifest object through YAML so tests exercise the real parse path. */
43
+ function parseManifestYaml(manifest: Record<string, unknown>) {
44
+ return parseSpecialtyManifest(stringifyYaml(manifest));
45
+ }
46
+
47
+ function expectThrowsWithMessage(manifest: Record<string, unknown>, ...fragments: string[]) {
48
+ expect(() => parseManifestYaml(manifest)).toThrow();
49
+ try {
50
+ parseManifestYaml(manifest);
51
+ throw new Error("expected parseSpecialtyManifest to throw");
52
+ } catch (err) {
53
+ const message = (err as Error).message;
54
+ for (const fragment of fragments) {
55
+ expect(message).toContain(fragment);
56
+ }
57
+ }
58
+ }
59
+
60
+ // ─── Valid manifest ─────────────────────────────────────────────────────────
61
+
62
+ describe("parseSpecialtyManifest — valid manifest", () => {
63
+ it("parses a valid minimal manifest successfully", () => {
64
+ const manifest = parseManifestYaml(
65
+ baseManifest({
66
+ metadata: {
67
+ name: "dude-agent",
68
+ displayName: "The Dude",
69
+ description: "A laid-back team member agent.",
70
+ version: "1.0.0",
71
+ skills: [
72
+ {
73
+ id: "chill",
74
+ name: "Stay Chill",
75
+ description: "Keeps things mellow.",
76
+ tags: ["vibe"],
77
+ },
78
+ ],
79
+ },
80
+ crons: [
81
+ {
82
+ name: "morning-brief",
83
+ schedule: "0 9 * * 1-5",
84
+ prompt: "Run the morning brief.",
85
+ },
86
+ ],
87
+ plugins: ["@shipwright/plugin"],
88
+ tools: ["Read", "Write"],
89
+ env: {
90
+ required: [
91
+ {
92
+ key: "SHIPWRIGHT_API_URL",
93
+ description: "Base URL of the admin API.",
94
+ secret: false,
95
+ },
96
+ ],
97
+ optional: [
98
+ {
99
+ key: "GROQ_API_KEY",
100
+ description: "Optional Groq API key for voice.",
101
+ secret: true,
102
+ },
103
+ ],
104
+ },
105
+ members: ["alice"],
106
+ repos: ["my-org/my-repo"],
107
+ }),
108
+ );
109
+ expect(manifest.apiVersion).toBe("shipwright.dev/v1alpha1");
110
+ expect(manifest.kind).toBe("Specialty");
111
+ expect(manifest.metadata.name).toBe("dude-agent");
112
+ expect(manifest.identity.templatesDir).toBe("templates/dude");
113
+ expect(manifest.crons).toHaveLength(1);
114
+ expect(manifest.chat).toBe(true);
115
+ expect(manifest.voice).toBe(false);
116
+ });
117
+
118
+ it("accepts a cron whose parentCron resolves to a sibling cron name", () => {
119
+ const manifest = parseManifestYaml(
120
+ baseManifest({
121
+ crons: [
122
+ {
123
+ name: "parent-cron",
124
+ schedule: "0 9 * * 1-5",
125
+ prompt: "Parent prompt.",
126
+ },
127
+ {
128
+ name: "child-cron",
129
+ schedule: "0 10 * * 1-5",
130
+ prompt: "Child prompt.",
131
+ parentCron: "parent-cron",
132
+ },
133
+ ],
134
+ }),
135
+ );
136
+ expect(manifest.crons[1].parentCron).toBe("parent-cron");
137
+ });
138
+ });
139
+
140
+ // ─── Acceptance criterion 1 — failure cases ────────────────────────────────
141
+
142
+ describe("parseSpecialtyManifest — invalid manifests fail with field-path errors", () => {
143
+ it("rejects a cron missing its required schedule field", () => {
144
+ expectThrowsWithMessage(
145
+ baseManifest({
146
+ crons: [{ name: "bad-cron", prompt: "Missing schedule." }],
147
+ }),
148
+ "crons",
149
+ "schedule",
150
+ );
151
+ });
152
+
153
+ it("rejects an unknown top-level field", () => {
154
+ expectThrowsWithMessage(
155
+ baseManifest({ unknownField: "surprise" }),
156
+ "unknownField",
157
+ );
158
+ });
159
+
160
+ it("rejects an env entry carrying a value key (trust boundary)", () => {
161
+ expectThrowsWithMessage(
162
+ baseManifest({
163
+ env: {
164
+ required: [
165
+ {
166
+ key: "SOME_KEY",
167
+ description: "has a value, which is forbidden",
168
+ secret: false,
169
+ value: "should not be allowed",
170
+ },
171
+ ],
172
+ optional: [],
173
+ },
174
+ }),
175
+ "env",
176
+ "value",
177
+ );
178
+ });
179
+
180
+ it("rejects a dangling parentCron reference", () => {
181
+ expectThrowsWithMessage(
182
+ baseManifest({
183
+ crons: [
184
+ {
185
+ name: "only-cron",
186
+ schedule: "0 9 * * 1-5",
187
+ prompt: "Prompt.",
188
+ parentCron: "does-not-exist",
189
+ },
190
+ ],
191
+ }),
192
+ "parentCron",
193
+ );
194
+ });
195
+
196
+ it("rejects a non-RFC1123 metadata.name", () => {
197
+ expectThrowsWithMessage(
198
+ baseManifest({
199
+ metadata: {
200
+ name: "Dude_Agent!",
201
+ displayName: "The Dude",
202
+ description: "desc",
203
+ version: "1.0.0",
204
+ skills: [],
205
+ },
206
+ }),
207
+ "metadata",
208
+ "name",
209
+ );
210
+ });
211
+ });
212
+
213
+ // ─── Schema-level checks (direct safeParse, matching repo convention) ──────
214
+
215
+ describe("SpecialtyManifestSchema.safeParse", () => {
216
+ it("rejects a self-referencing parentCron", () => {
217
+ const result = SpecialtyManifestSchema.safeParse(
218
+ baseManifest({
219
+ crons: [
220
+ {
221
+ name: "self-cron",
222
+ schedule: "0 9 * * 1-5",
223
+ prompt: "Prompt.",
224
+ parentCron: "self-cron",
225
+ },
226
+ ],
227
+ }),
228
+ );
229
+ expect(result.success).toBe(false);
230
+ });
231
+
232
+ it("defaults silent/enabled on a cron entry when omitted", () => {
233
+ const result = SpecialtyManifestSchema.safeParse(
234
+ baseManifest({
235
+ crons: [
236
+ { name: "morning-brief", schedule: "0 9 * * 1-5", prompt: "Prompt." },
237
+ ],
238
+ }),
239
+ );
240
+ expect(result.success).toBe(true);
241
+ if (result.success) {
242
+ expect(result.data.crons[0].silent).toBe(false);
243
+ expect(result.data.crons[0].enabled).toBe(true);
244
+ }
245
+ });
246
+
247
+ it("rejects a bad apiVersion literal", () => {
248
+ const result = SpecialtyManifestSchema.safeParse(
249
+ baseManifest({ apiVersion: "shipwright.dev/v1" }),
250
+ );
251
+ expect(result.success).toBe(false);
252
+ });
253
+
254
+ it("rejects a repos entry not in org/repo format", () => {
255
+ const result = SpecialtyManifestSchema.safeParse(
256
+ baseManifest({ repos: ["not-a-valid-repo"] }),
257
+ );
258
+ expect(result.success).toBe(false);
259
+ });
260
+
261
+ it("accepts an optional resources override", () => {
262
+ const result = SpecialtyManifestSchema.safeParse(
263
+ baseManifest({
264
+ resources: {
265
+ requests: { cpu: "500m", memory: "2Gi", "ephemeral-storage": "1Gi" },
266
+ limits: { memory: "8Gi", "ephemeral-storage": "1Gi" },
267
+ },
268
+ }),
269
+ );
270
+ expect(result.success).toBe(true);
271
+ });
272
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * admin/src/startup-policy-reconcile.ts
3
+ *
4
+ * One-shot policy-only reconcile run on admin startup (ACP-1.3).
5
+ *
6
+ * The chart passes the Claude policy hash to admin as an env var, so a policy
7
+ * change restarts admin. On boot this pass walks every managed agent through
8
+ * `provisioner.reconcile(..., { policyOnly: true })`, which patches only the
9
+ * policy wiring (volume, mount, hash annotation) — so only agents whose policy
10
+ * actually changed roll. It runs whether or not a policy is configured, so
11
+ * agents lose the mount after the policy is disabled. The same pass applies
12
+ * the Anthropic workload identity federation wiring (ServiceAccount, projected
13
+ * token volume, mount, federation env), so turning federation on or off and
14
+ * restarting admin corrects existing agents.
15
+ *
16
+ * The startup pass never moves agents onto their own per-agent identity: that
17
+ * creates Anthropic and Kubernetes resources, which should not happen as a side
18
+ * effect of a boot. Operators run POST /agents/reconcile (with dryRun=true
19
+ * first); this pass only reports how many agents have no owner (noOwner).
20
+ *
21
+ * Never throws: a Kubernetes or database failure is logged and admin keeps
22
+ * serving. Called once from main.ts after Bun.serve.
23
+ */
24
+
25
+ import type { AgentProvisioner, ReconcileTarget } from "./agent-provisioner.ts";
26
+ import type { AgentService, AgentSummary } from "./agents.ts";
27
+
28
+ interface StartupPolicyReconcileDeps {
29
+ agentService: Pick<AgentService, "list">;
30
+ provisioner: Pick<AgentProvisioner, "canProvision" | "reconcile">;
31
+ logger: Pick<Console, "log" | "error">;
32
+ }
33
+
34
+ /**
35
+ * The reconcile targets for a list of agents. Self-hosted agents manage their
36
+ * own workloads, so they are excluded from K8s reconciliation. Shared with
37
+ * POST /agents/reconcile so both paths select agents the same way.
38
+ */
39
+ export function managedReconcileTargets(
40
+ agents: AgentSummary[],
41
+ ): ReconcileTarget[] {
42
+ return agents
43
+ .filter((a) => !a.selfHosted)
44
+ .map((a) => ({
45
+ id: a.id,
46
+ slug: a.name,
47
+ accountId: a.accountId,
48
+ ownerEmail: a.ownerEmail,
49
+ anthropicIdentityName: a.anthropicIdentityName,
50
+ anthropicServiceAccountId: a.anthropicServiceAccountId,
51
+ anthropicFederationRuleId: a.anthropicFederationRuleId,
52
+ k8sServiceAccountName: a.k8sServiceAccountName,
53
+ }));
54
+ }
55
+
56
+ export async function runStartupPolicyReconcile({
57
+ agentService,
58
+ provisioner,
59
+ logger,
60
+ }: StartupPolicyReconcileDeps): Promise<void> {
61
+ if (!provisioner.canProvision) return;
62
+
63
+ try {
64
+ const targets = managedReconcileTargets(await agentService.list());
65
+ const result = await provisioner.reconcile(targets, { policyOnly: true });
66
+ logger.log(
67
+ `[startup-policy-reconcile] agents=${targets.length} updated=${result.updated.length} recreated=${result.recreated.length} orphans=${result.orphans.length} failed=${result.failed.length} noOwner=${result.noOwner.length}`,
68
+ );
69
+ for (const { agentId, error } of result.failed) {
70
+ logger.error(
71
+ `[startup-policy-reconcile] agent ${agentId} failed: ${error}`,
72
+ );
73
+ }
74
+ } catch (err) {
75
+ logger.error("[startup-policy-reconcile] reconcile failed:", err);
76
+ }
77
+ }