@app-vitals/agent-admin 0.0.1 → 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 +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,685 @@
1
+ /**
2
+ * admin/src/agent-manifest.ts
3
+ *
4
+ * Pure builders for the Kubernetes manifests that provision a single Shipwright
5
+ * agent: a per-agent Deployment, its Opaque token Secret, and the RFC1123 name
6
+ * derivation that ties them together.
7
+ *
8
+ * These functions are PURE — no fs, no network, no clock. They only shape wire
9
+ * objects. The actual k8s API calls live in ./kubernetes-client.ts; this module
10
+ * returns the same `KubernetesDeployment` / `KubernetesSecret` wire types so the
11
+ * two stay structurally consistent.
12
+ */
13
+
14
+ import { createHash } from "node:crypto";
15
+ import type {
16
+ KubernetesDeployment,
17
+ KubernetesEnvVar,
18
+ KubernetesPvc,
19
+ KubernetesSecret,
20
+ } from "./kubernetes-client.ts";
21
+ import type { ServiceEnvSpec } from "./service-integration.ts";
22
+
23
+ // ─── Constants ──────────────────────────────────────────────────────────────
24
+
25
+ /** Agent health/liveness port (matches the agent's `SHIPWRIGHT_HEALTH_PORT`). */
26
+ export const AGENT_HEALTH_PORT = 3459;
27
+
28
+ /** Where the agent's persistent home volume is mounted in the container. */
29
+ export const AGENT_HOME_MOUNT_PATH = "/data/agent-home";
30
+
31
+ /**
32
+ * Repo clones and git worktrees must live on the PVC. The plugin commands
33
+ * default to `$HOME/src` / `$HOME/worktrees`, which sit on the container's
34
+ * ephemeral overlay layer — a single worktree with node_modules can exceed
35
+ * the pod's ephemeral-storage limit and get the pod evicted.
36
+ */
37
+ const AGENT_REPO_DIR = `${AGENT_HOME_MOUNT_PATH}/workspace/repos`;
38
+ const AGENT_WORKTREE_DIR = `${AGENT_HOME_MOUNT_PATH}/workspace/worktrees`;
39
+
40
+ /**
41
+ * Optional per-field overrides for the agent container's resources. Any field
42
+ * left unset falls back to the default in `resolveAgentContainerResources`.
43
+ */
44
+ export interface AgentContainerResourceOverrides {
45
+ /** CPU request, e.g. "500m". */
46
+ cpuRequest?: string;
47
+ /**
48
+ * CPU limit, e.g. "2". Applied to tenant pods ONLY (agents with an
49
+ * accountId); platform agents never get a CPU limit.
50
+ */
51
+ cpuLimit?: string;
52
+ /** Memory request, e.g. "2Gi". */
53
+ memoryRequest?: string;
54
+ /** Memory limit, e.g. "8Gi". */
55
+ memoryLimit?: string;
56
+ /**
57
+ * ephemeral-storage, applied to BOTH request and limit (kept equal — see
58
+ * rationale below). e.g. "4Gi".
59
+ */
60
+ ephemeralStorage?: string;
61
+ }
62
+
63
+ /** Default CPU limit for tenant pods (agents with an accountId). */
64
+ const DEFAULT_TENANT_CPU_LIMIT = "2";
65
+
66
+ /**
67
+ * Resolve the agent container's resources.requests/limits, applying any
68
+ * caller-supplied overrides on top of today's defaults (500m cpu / 2Gi memory
69
+ * request / 8Gi memory limit / 4Gi ephemeral storage, no CPU limit). Called
70
+ * from `buildAgentDeploymentManifest` with `opts.resources` — omitted or
71
+ * partial overrides fall back field-by-field to the default below.
72
+ *
73
+ * Precedence (highest wins): specialty override > env override > default.
74
+ * `overrides` here is the ENV layer (populated in admin/src/main.ts's
75
+ * buildProvisioner from SHIPWRIGHT_K8S_AGENT_* vars). The per-type
76
+ * SpecialtyResourcesSchema in specialty-registry.ts is already parsed as
77
+ * part of SpecialtyManifestSchema.resources but is NOT consumed here or by
78
+ * any provisioner — it stays inert until a future task wires specialty-level
79
+ * resource resolution in ahead of this function's env layer.
80
+ *
81
+ * Defaults: requests mirror the GKE Autopilot defaults the agent ran with
82
+ * before these were explicit. The memory limit exists to contain a runaway
83
+ * Claude run to its own container (OOM-kill) rather than letting it grow
84
+ * until the kubelet evicts neighbouring pods under node memory pressure
85
+ * (observed: ~11.6Gi used against the 2Gi request). No CPU limit for
86
+ * platform agents — CPU contention throttles instead of evicting. Tenant pods
87
+ * (`tenant: true`) get `DEFAULT_TENANT_CPU_LIMIT` (or `overrides.cpuLimit`) so
88
+ * one account's agent cannot starve neighbours in the shared namespace; with
89
+ * no ResourceQuota, per-account bounds are maxAgents x these fixed limits.
90
+ *
91
+ * ephemeral-storage defaults to 4Gi, not the 1Gi Autopilot default: 1Gi left
92
+ * no room for a single build step's scratch space, and agents were evicted
93
+ * mid-run ("Pod ephemeral local storage usage exceeds the total limit of
94
+ * containers 1Gi"), losing the in-flight task. Tool caches are pinned to the
95
+ * PVC in the agent's runMiseStartup, so this budget only has to cover
96
+ * genuinely transient writes — e.g. a scan skill staging tool binaries in a
97
+ * mktemp dir. Requests and limits are kept equal because Autopilot bills the
98
+ * request either way.
99
+ */
100
+ export function resolveAgentContainerResources(
101
+ overrides?: AgentContainerResourceOverrides,
102
+ tenant = false,
103
+ ): { requests: Record<string, string>; limits: Record<string, string> } {
104
+ const ephemeralStorage = overrides?.ephemeralStorage ?? "4Gi";
105
+ return {
106
+ requests: {
107
+ cpu: overrides?.cpuRequest ?? "500m",
108
+ memory: overrides?.memoryRequest ?? "2Gi",
109
+ "ephemeral-storage": ephemeralStorage,
110
+ },
111
+ limits: {
112
+ ...(tenant
113
+ ? { cpu: overrides?.cpuLimit ?? DEFAULT_TENANT_CPU_LIMIT }
114
+ : {}),
115
+ memory: overrides?.memoryLimit ?? "8Gi",
116
+ "ephemeral-storage": ephemeralStorage,
117
+ },
118
+ };
119
+ }
120
+
121
+ /** Non-root uid/gid the agent runs as (matches the agent image). */
122
+ const AGENT_RUN_AS = 1000;
123
+
124
+ /** RFC1123 label length cap. */
125
+ const MAX_NAME_LEN = 63;
126
+
127
+ /** Length of the appended disambiguation hash suffix. */
128
+ const HASH_SUFFIX_LEN = 8;
129
+
130
+ const NAME_LABEL = "app.kubernetes.io/name";
131
+ const INSTANCE_LABEL = "app.kubernetes.io/instance";
132
+ const MANAGED_BY_LABEL = "app.kubernetes.io/managed-by";
133
+ const AGENT_ID_LABEL = "shipwright.dev/agent-id";
134
+ /** Marks pods belonging to a tenant account (set only when accountId is present). */
135
+ export const TENANT_LABEL = "shipwright.dev/tenant";
136
+ const AGENT_APP_NAME = "shipwright-agent";
137
+
138
+ /** Volume name, mount dir, and ConfigMap key for the Claude Code managed policy. */
139
+ export const CLAUDE_POLICY_VOLUME = "claude-policy";
140
+ export const CLAUDE_POLICY_MOUNT_PATH = "/etc/claude-code";
141
+ const CLAUDE_POLICY_FILE = "managed-settings.json";
142
+ /** Pod-template annotation carrying the policy hash so a policy change rolls pods. */
143
+ export const CLAUDE_POLICY_HASH_ANNOTATION =
144
+ "shipwright.dev/claude-policy-hash";
145
+ /** 0444 — policy is read-only to the agent user. */
146
+ const CLAUDE_POLICY_FILE_MODE = 292;
147
+
148
+ /** Volume name and mount dir for the projected Anthropic identity token. */
149
+ export const ANTHROPIC_TOKEN_VOLUME = "anthropic-token";
150
+ export const ANTHROPIC_TOKEN_MOUNT_PATH = "/var/run/secrets/anthropic.com";
151
+ const ANTHROPIC_TOKEN_FILE = "token";
152
+
153
+ /**
154
+ * Every env var the federation wiring may inject, in manifest order. The
155
+ * reconcile patch walks this list so a var that is no longer desired (feature
156
+ * off, or workspace id unset) is deleted from live Deployments.
157
+ */
158
+ export const ANTHROPIC_FEDERATION_ENV_NAMES = [
159
+ "ANTHROPIC_IDENTITY_TOKEN_FILE",
160
+ "ANTHROPIC_FEDERATION_RULE_ID",
161
+ "ANTHROPIC_ORGANIZATION_ID",
162
+ "ANTHROPIC_SERVICE_ACCOUNT_ID",
163
+ "ANTHROPIC_WORKSPACE_ID",
164
+ ] as const;
165
+
166
+ // ─── Name sanitization ──────────────────────────────────────────────────────
167
+
168
+ /**
169
+ * Derive an RFC1123-compliant, collision-resistant Kubernetes object name from
170
+ * an arbitrary agent id.
171
+ *
172
+ * RFC1123 labels must be lowercase alphanumerics or `-`, start/end alphanumeric,
173
+ * and be ≤63 chars.
174
+ *
175
+ * Collision strategy: sanitization is inherently lossy — `agent_1`, `agent.1`,
176
+ * and `Agent-1` all reduce to the same `agent-1` base, and long ids must be
177
+ * truncated. Whenever the cleaned base is NOT a byte-for-byte match of the
178
+ * original id (i.e. any lowercasing, char replacement, collapsing, trimming, or
179
+ * truncation happened), we append `-<8-hex>` where the hex is the leading bytes
180
+ * of `sha256(originalId)`. The hash is over the FULL original id, so two ids
181
+ * that share a cleaned prefix still get distinct suffixes. A lossless id (already
182
+ * valid RFC1123) is returned unchanged. The base is truncated as needed so that
183
+ * `base + "-" + suffix` still fits within 63 chars.
184
+ */
185
+ export function sanitizeAgentName(id: string): string {
186
+ const base = cleanBase(id);
187
+
188
+ // Lossless: the id was already a valid RFC1123 label — return as-is.
189
+ if (base === id && base.length > 0 && base.length <= MAX_NAME_LEN) {
190
+ return base;
191
+ }
192
+
193
+ const suffix = createHash("sha256")
194
+ .update(id)
195
+ .digest("hex")
196
+ .slice(0, HASH_SUFFIX_LEN);
197
+
198
+ // Reserve room for "-" + suffix, then re-trim any dash exposed at the new end.
199
+ const room = MAX_NAME_LEN - (suffix.length + 1);
200
+ const truncated = trimDashes(base.slice(0, Math.max(0, room)));
201
+ const prefix = truncated.length > 0 ? `${truncated}-` : "";
202
+ return `${prefix}${suffix}`;
203
+ }
204
+
205
+ /** Lowercase, replace invalid runs with a single dash, trim dash padding. */
206
+ function cleanBase(id: string): string {
207
+ const lowered = id.toLowerCase().replace(/[^a-z0-9]+/g, "-");
208
+ return trimDashes(lowered);
209
+ }
210
+
211
+ function trimDashes(s: string): string {
212
+ return s.replace(/^-+/, "").replace(/-+$/, "");
213
+ }
214
+
215
+ // ─── PVC builder ────────────────────────────────────────────────────────────
216
+
217
+ export interface AgentPvcOpts {
218
+ name: string;
219
+ namespace: string;
220
+ sizeGi: number;
221
+ storageClassName?: string;
222
+ }
223
+
224
+ export function buildAgentPvcManifest(opts: AgentPvcOpts): KubernetesPvc {
225
+ const manifest: KubernetesPvc = {
226
+ apiVersion: "v1",
227
+ kind: "PersistentVolumeClaim",
228
+ metadata: {
229
+ name: opts.name,
230
+ namespace: opts.namespace,
231
+ },
232
+ spec: {
233
+ accessModes: ["ReadWriteOnce"],
234
+ resources: { requests: { storage: `${opts.sizeGi}Gi` } },
235
+ },
236
+ };
237
+ if (opts.storageClassName !== undefined) {
238
+ manifest.spec.storageClassName = opts.storageClassName;
239
+ }
240
+ return manifest;
241
+ }
242
+
243
+ // ─── Deployment builder ─────────────────────────────────────────────────────
244
+
245
+ export interface AgentDeploymentOpts {
246
+ /** Original agent id (used verbatim in env + label, sanitized for the name). */
247
+ agentId: string;
248
+ namespace: string;
249
+ /** Agent container image (without tag). */
250
+ image: string;
251
+ /** Image tag, joined as `image:tag`. */
252
+ imageTag: string;
253
+ /** In-cluster admin/API base URL the agent calls home to. */
254
+ apiUrl: string;
255
+ /** Name of the pre-provisioned PersistentVolumeClaim for the agent home. */
256
+ pvcName: string;
257
+ /** Name of the Secret holding the per-agent token. */
258
+ secretName: string;
259
+ /** Key within the Secret's data that holds the token. Defaults to "token". */
260
+ tokenSecretKey?: string;
261
+ /** Replica count. Defaults to 1. */
262
+ replicas?: number;
263
+ /**
264
+ * Owning account. When set the pod is a tenant pod: the tenant label is added
265
+ * to the Deployment and pod template, and a CPU limit is applied. Omit for
266
+ * platform agents (manifest unchanged).
267
+ */
268
+ accountId?: string | null;
269
+ /**
270
+ * Optional container resource overrides, merged field-by-field over the
271
+ * defaults in `resolveAgentContainerResources`. Omit (or leave individual
272
+ * fields unset) to keep today's values for those fields.
273
+ */
274
+ resources?: AgentContainerResourceOverrides;
275
+ /**
276
+ * Optional agent-voice (STT/TTS) configuration. When omitted, the provisioned
277
+ * agent pod gets only the 3 base env vars (voice disabled). When present, the
278
+ * relevant vars are appended after the base vars:
279
+ * - whisperServiceUrl → WHISPER_SERVICE_URL (provider=whisper)
280
+ * - groqApiKey → GROQ_API_KEY (provider=groq)
281
+ * - elevenLabsApiKey → ELEVENLABS_API_KEY (TTS, either provider)
282
+ * - voiceId → ELEVENLABS_VOICE_ID (optional TTS voice override)
283
+ * Only the keys that are set are injected — this mirrors the agent's own
284
+ * env contract (agent/src/runtime/config.ts).
285
+ */
286
+ voice?: AgentVoiceEnv;
287
+ /**
288
+ * Optional service integrations (see service-integration.ts), in order. Each
289
+ * entry with a `url` injects its `tokenEnv` (secretKeyRef from the agent
290
+ * Secret, key `secretKey`) then its `urlEnv` (plain value). Placed after the
291
+ * base vars and before the built-in chat-service env vars.
292
+ */
293
+ serviceIntegrations?: ServiceEnvSpec[];
294
+ /**
295
+ * When set, inject SHIPWRIGHT_CHAT_SERVICE_TOKEN (via secretKeyRef from the
296
+ * agent Secret) and SHIPWRIGHT_CHAT_SERVICE_URL (as a plain value) into the
297
+ * agent Deployment. Placed after the service-integration env vars and before voice env vars.
298
+ */
299
+ chatServiceUrl?: string;
300
+ /**
301
+ * Key within the agent Secret under which the chat-service token is stored.
302
+ * Defaults to "chat-service-token".
303
+ */
304
+ chatServiceTokenSecretKey?: string;
305
+ /**
306
+ * When set, mount the named ConfigMap's `managed-settings.json` read-only at
307
+ * /etc/claude-code (Claude Code's managed-policy directory). `hash` is
308
+ * optional: when non-empty it is stamped on the pod template as an annotation
309
+ * so a policy change rolls the pods; when absent/empty no annotation is added.
310
+ * Omit entirely for an unchanged manifest.
311
+ */
312
+ claudePolicy?: { configMapName: string; hash?: string };
313
+ /**
314
+ * When set, the pod runs as `serviceAccountName` and carries a projected
315
+ * service-account token (volume `anthropic-token`, mounted read-only at
316
+ * /var/run/secrets/anthropic.com) plus the env the Anthropic client needs to
317
+ * exchange that token for a short-lived access token (workload identity
318
+ * federation). Omit for an unchanged manifest.
319
+ */
320
+ anthropicFederation?: AgentAnthropicFederation;
321
+ }
322
+
323
+ /**
324
+ * Anthropic Workload Identity Federation settings for provisioned agent pods.
325
+ * Parsed from the admin's SHIPWRIGHT_K8S_AGENT_* env in main.ts.
326
+ */
327
+ export interface AgentAnthropicFederation {
328
+ /** Kubernetes ServiceAccount the pod runs as (the token's subject). */
329
+ serviceAccountName: string;
330
+ /** → ANTHROPIC_FEDERATION_RULE_ID */
331
+ ruleId: string;
332
+ /** → ANTHROPIC_ORGANIZATION_ID */
333
+ organizationId: string;
334
+ /** → ANTHROPIC_SERVICE_ACCOUNT_ID */
335
+ serviceAccountId: string;
336
+ /** → ANTHROPIC_WORKSPACE_ID (only injected when set). */
337
+ workspaceId?: string;
338
+ /** Audience of the projected token. */
339
+ audience: string;
340
+ /** Lifetime of the projected token; Kubernetes requires >= 600. */
341
+ expirationSeconds: number;
342
+ }
343
+
344
+ /**
345
+ * Build the federation env entries, in ANTHROPIC_FEDERATION_ENV_NAMES order.
346
+ * Returns an empty array when federation is disabled.
347
+ */
348
+ function anthropicFederationEnvEntries(
349
+ fed: AgentAnthropicFederation | undefined,
350
+ ): { name: string; value: string }[] {
351
+ if (!fed) return [];
352
+ return [
353
+ {
354
+ name: "ANTHROPIC_IDENTITY_TOKEN_FILE",
355
+ value: `${ANTHROPIC_TOKEN_MOUNT_PATH}/${ANTHROPIC_TOKEN_FILE}`,
356
+ },
357
+ { name: "ANTHROPIC_FEDERATION_RULE_ID", value: fed.ruleId },
358
+ { name: "ANTHROPIC_ORGANIZATION_ID", value: fed.organizationId },
359
+ { name: "ANTHROPIC_SERVICE_ACCOUNT_ID", value: fed.serviceAccountId },
360
+ ...(fed.workspaceId
361
+ ? [{ name: "ANTHROPIC_WORKSPACE_ID", value: fed.workspaceId }]
362
+ : []),
363
+ ];
364
+ }
365
+
366
+ /**
367
+ * The subset of voice env an admin provisioner flows into a provisioned agent
368
+ * pod. The admin reads these from its OWN env (sourced from the chart's voice
369
+ * Secret + whisper Service URL); this builder only shapes the pod env from them.
370
+ */
371
+ export interface AgentVoiceEnv {
372
+ /** http(s) URL of the self-hosted Whisper pod (provider=whisper). */
373
+ whisperServiceUrl?: string;
374
+ /** Groq API key (provider=groq). */
375
+ groqApiKey?: string;
376
+ /** ElevenLabs API key (TTS). */
377
+ elevenLabsApiKey?: string;
378
+ /** Optional ElevenLabs voice id override. */
379
+ voiceId?: string;
380
+ }
381
+
382
+ /**
383
+ * Build the ordered voice env entries from a partial voice config. Only set
384
+ * keys are emitted; an empty/undefined config yields no entries (voice
385
+ * disabled). Order is stable for deterministic manifests/tests.
386
+ */
387
+ function voiceEnvEntries(
388
+ voice: AgentVoiceEnv | undefined,
389
+ ): { name: string; value: string }[] {
390
+ if (!voice) return [];
391
+ const entries: { name: string; value: string }[] = [];
392
+ if (voice.whisperServiceUrl) {
393
+ entries.push({
394
+ name: "WHISPER_SERVICE_URL",
395
+ value: voice.whisperServiceUrl,
396
+ });
397
+ }
398
+ if (voice.groqApiKey) {
399
+ entries.push({ name: "GROQ_API_KEY", value: voice.groqApiKey });
400
+ }
401
+ if (voice.elevenLabsApiKey) {
402
+ entries.push({ name: "ELEVENLABS_API_KEY", value: voice.elevenLabsApiKey });
403
+ }
404
+ if (voice.voiceId) {
405
+ entries.push({ name: "ELEVENLABS_VOICE_ID", value: voice.voiceId });
406
+ }
407
+ return entries;
408
+ }
409
+
410
+ /**
411
+ * Build env entries for the optional service integrations, in list order. Each
412
+ * configured integration (url set) yields two entries: its token env
413
+ * (secretKeyRef from the agent Secret) then its URL env (plain value).
414
+ * Unconfigured integrations yield nothing.
415
+ */
416
+ function serviceIntegrationEnvEntries(
417
+ opts: AgentDeploymentOpts,
418
+ ): KubernetesEnvVar[] {
419
+ return (opts.serviceIntegrations ?? []).flatMap((svc) => {
420
+ if (!svc.url) return [];
421
+ return [
422
+ {
423
+ name: svc.tokenEnv,
424
+ valueFrom: {
425
+ secretKeyRef: { name: opts.secretName, key: svc.secretKey },
426
+ },
427
+ },
428
+ { name: svc.urlEnv, value: svc.url },
429
+ ];
430
+ });
431
+ }
432
+
433
+ /**
434
+ * Build chat-service env entries when chatServiceUrl is configured. Returns two
435
+ * entries: SHIPWRIGHT_CHAT_SERVICE_TOKEN (secretKeyRef from the agent Secret)
436
+ * and SHIPWRIGHT_CHAT_SERVICE_URL (plain value). Returns an empty array when
437
+ * chatServiceUrl is absent (chat-service wiring disabled).
438
+ */
439
+ function chatServiceEnvEntries(opts: AgentDeploymentOpts): KubernetesEnvVar[] {
440
+ if (!opts.chatServiceUrl) return [];
441
+ const csKey = opts.chatServiceTokenSecretKey ?? "chat-service-token";
442
+ return [
443
+ {
444
+ name: "SHIPWRIGHT_CHAT_SERVICE_TOKEN",
445
+ valueFrom: {
446
+ secretKeyRef: { name: opts.secretName, key: csKey },
447
+ },
448
+ },
449
+ {
450
+ name: "SHIPWRIGHT_CHAT_SERVICE_URL",
451
+ value: opts.chatServiceUrl,
452
+ },
453
+ ];
454
+ }
455
+
456
+ export function buildAgentDeploymentManifest(
457
+ opts: AgentDeploymentOpts,
458
+ ): KubernetesDeployment {
459
+ const name = sanitizeAgentName(opts.agentId);
460
+ const tokenKey = opts.tokenSecretKey ?? "token";
461
+ const volumeName = "agent-home";
462
+
463
+ const labels: Record<string, string> = {
464
+ [NAME_LABEL]: AGENT_APP_NAME,
465
+ [INSTANCE_LABEL]: name,
466
+ [MANAGED_BY_LABEL]: "shipwright-admin",
467
+ [AGENT_ID_LABEL]: opts.agentId,
468
+ };
469
+
470
+ const tenant = Boolean(opts.accountId);
471
+ if (tenant) labels[TENANT_LABEL] = "true";
472
+
473
+ // Selectors are immutable post-create and must not include volatile values;
474
+ // the instance label is the stable per-agent identity.
475
+ const selectorLabels: Record<string, string> = {
476
+ [NAME_LABEL]: AGENT_APP_NAME,
477
+ [INSTANCE_LABEL]: name,
478
+ };
479
+
480
+ const policy = opts.claudePolicy;
481
+ const federation = opts.anthropicFederation;
482
+
483
+ return {
484
+ apiVersion: "apps/v1",
485
+ kind: "Deployment",
486
+ metadata: {
487
+ name,
488
+ namespace: opts.namespace,
489
+ labels,
490
+ },
491
+ spec: {
492
+ replicas: opts.replicas ?? 1,
493
+ // Recreate: Slack Socket Mode allows only one active connection per token.
494
+ // RollingUpdate would briefly create a second pod that fights for the socket.
495
+ strategy: { type: "Recreate" },
496
+ selector: { matchLabels: selectorLabels },
497
+ template: {
498
+ metadata: {
499
+ labels: tenant
500
+ ? { ...selectorLabels, [TENANT_LABEL]: "true" }
501
+ : selectorLabels,
502
+ ...(policy?.hash
503
+ ? { annotations: { [CLAUDE_POLICY_HASH_ANNOTATION]: policy.hash } }
504
+ : {}),
505
+ },
506
+ spec: {
507
+ // Federation: the projected token's subject is this ServiceAccount.
508
+ ...(federation
509
+ ? { serviceAccountName: federation.serviceAccountName }
510
+ : {}),
511
+ securityContext: {
512
+ fsGroup: AGENT_RUN_AS,
513
+ fsGroupChangePolicy: "OnRootMismatch",
514
+ runAsNonRoot: true,
515
+ runAsUser: AGENT_RUN_AS,
516
+ },
517
+ // 120s grace period mirrors the Helm-managed agent template and gives
518
+ // the agent time to finish an in-flight Claude response before SIGKILL.
519
+ terminationGracePeriodSeconds: 120,
520
+ volumes: [
521
+ {
522
+ name: volumeName,
523
+ persistentVolumeClaim: { claimName: opts.pvcName },
524
+ },
525
+ ...(policy
526
+ ? [
527
+ {
528
+ name: CLAUDE_POLICY_VOLUME,
529
+ configMap: {
530
+ name: policy.configMapName,
531
+ items: [
532
+ { key: CLAUDE_POLICY_FILE, path: CLAUDE_POLICY_FILE },
533
+ ],
534
+ defaultMode: CLAUDE_POLICY_FILE_MODE,
535
+ },
536
+ },
537
+ ]
538
+ : []),
539
+ ...(federation
540
+ ? [
541
+ {
542
+ name: ANTHROPIC_TOKEN_VOLUME,
543
+ projected: {
544
+ sources: [
545
+ {
546
+ serviceAccountToken: {
547
+ audience: federation.audience,
548
+ expirationSeconds: federation.expirationSeconds,
549
+ path: ANTHROPIC_TOKEN_FILE,
550
+ },
551
+ },
552
+ ],
553
+ },
554
+ },
555
+ ]
556
+ : []),
557
+ ],
558
+ containers: [
559
+ {
560
+ name: AGENT_APP_NAME,
561
+ image: `${opts.image}:${opts.imageTag}`,
562
+ ports: [{ containerPort: AGENT_HEALTH_PORT, protocol: "TCP" }],
563
+ resources: resolveAgentContainerResources(opts.resources, tenant),
564
+ env: [
565
+ { name: "SHIPWRIGHT_AGENT_ID", value: opts.agentId },
566
+ { name: "SHIPWRIGHT_API_URL", value: opts.apiUrl },
567
+ {
568
+ name: "SHIPWRIGHT_AGENT_API_KEY",
569
+ valueFrom: {
570
+ secretKeyRef: { name: opts.secretName, key: tokenKey },
571
+ },
572
+ },
573
+ // Tell the agent where its persistent home directory is mounted.
574
+ { name: "AGENT_HOME", value: AGENT_HOME_MOUNT_PATH },
575
+ // Route repo clones and worktrees to the PVC (see constants).
576
+ { name: "SHIPWRIGHT_REPO_DIR", value: AGENT_REPO_DIR },
577
+ { name: "SHIPWRIGHT_WORKTREE_DIR", value: AGENT_WORKTREE_DIR },
578
+ // Service-integration env (e.g. task-store, registered by the
579
+ // coding extension) — injected after the base vars for each
580
+ // configured integration; omitted entirely when none are.
581
+ ...serviceIntegrationEnvEntries(opts),
582
+ // Chat-service env — injected after integration vars when chat
583
+ // service is configured; omitted entirely when chatServiceUrl is absent.
584
+ ...chatServiceEnvEntries(opts),
585
+ // Voice (STT/TTS) env — appended only for the keys that are set
586
+ // (none when voice is disabled, preserving the 6 base vars).
587
+ ...voiceEnvEntries(opts.voice),
588
+ // Workload identity federation env — none when disabled.
589
+ ...anthropicFederationEnvEntries(federation),
590
+ ],
591
+ volumeMounts: [
592
+ { name: volumeName, mountPath: AGENT_HOME_MOUNT_PATH },
593
+ // Directory mount (not subPath) so ConfigMap updates propagate.
594
+ ...(policy
595
+ ? [
596
+ {
597
+ name: CLAUDE_POLICY_VOLUME,
598
+ mountPath: CLAUDE_POLICY_MOUNT_PATH,
599
+ readOnly: true,
600
+ },
601
+ ]
602
+ : []),
603
+ ...(federation
604
+ ? [
605
+ {
606
+ name: ANTHROPIC_TOKEN_VOLUME,
607
+ mountPath: ANTHROPIC_TOKEN_MOUNT_PATH,
608
+ readOnly: true,
609
+ },
610
+ ]
611
+ : []),
612
+ ],
613
+ // Gate liveness/readiness until the agent's health server binds.
614
+ // Startup runs `mise install` + plugin install, which can exceed a
615
+ // minute on a cold/contended node; without this, liveness would
616
+ // restart the container (~75s in) before startup ever finished.
617
+ // 18 × 10s = 180s grace, aligned with the entrypoint startup
618
+ // watchdog (DEFAULT_STARTUP_TIMEOUT_MS in agent/src/runtime/entrypoint.ts).
619
+ startupProbe: {
620
+ httpGet: { path: "/health", port: AGENT_HEALTH_PORT },
621
+ periodSeconds: 10,
622
+ failureThreshold: 18,
623
+ },
624
+ livenessProbe: {
625
+ httpGet: { path: "/health", port: AGENT_HEALTH_PORT },
626
+ initialDelaySeconds: 15,
627
+ periodSeconds: 30,
628
+ // Explicit, not the 1s k8s default — the health server shares
629
+ // its process with claude -p child spawns and check-helpers.ts's
630
+ // spawnSync("gh", ...) calls, which block the whole process for
631
+ // the duration of the gh CLI call (network round-trip to GitHub,
632
+ // not just local CPU work). 15s is a bridge until SIO-1.2 converts
633
+ // those to async spawn; no fixed timeout is truly safe against a
634
+ // slow gh response, so this isn't a permanent fix.
635
+ timeoutSeconds: 15,
636
+ failureThreshold: 3,
637
+ },
638
+ readinessProbe: {
639
+ httpGet: { path: "/health", port: AGENT_HEALTH_PORT },
640
+ initialDelaySeconds: 10,
641
+ periodSeconds: 10,
642
+ timeoutSeconds: 15,
643
+ failureThreshold: 3,
644
+ },
645
+ securityContext: {
646
+ runAsNonRoot: true,
647
+ runAsUser: AGENT_RUN_AS,
648
+ allowPrivilegeEscalation: false,
649
+ },
650
+ },
651
+ ],
652
+ },
653
+ },
654
+ },
655
+ };
656
+ }
657
+
658
+ // ─── Secret builder ─────────────────────────────────────────────────────────
659
+
660
+ export interface AgentSecretOpts {
661
+ name: string;
662
+ namespace: string;
663
+ /** Plaintext per-agent token; base64-encoded into the Secret's `data`. */
664
+ token: string;
665
+ /** Key under which the token is stored. Defaults to "token". */
666
+ tokenKey?: string;
667
+ }
668
+
669
+ export function buildAgentSecretManifest(
670
+ opts: AgentSecretOpts,
671
+ ): KubernetesSecret {
672
+ const tokenKey = opts.tokenKey ?? "token";
673
+ return {
674
+ apiVersion: "v1",
675
+ kind: "Secret",
676
+ type: "Opaque",
677
+ metadata: {
678
+ name: opts.name,
679
+ namespace: opts.namespace,
680
+ },
681
+ data: {
682
+ [tokenKey]: Buffer.from(opts.token).toString("base64"),
683
+ },
684
+ };
685
+ }