@caelo-cms/provisioning 0.11.9 → 0.11.11

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 (352) hide show
  1. package/dist/adapter.d.ts +6 -0
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/admin-domain.d.ts +10 -1
  4. package/dist/admin-domain.d.ts.map +1 -1
  5. package/dist/admin-domain.js +13 -3
  6. package/dist/admin-domain.js.map +1 -1
  7. package/dist/adopt-existing.d.ts +12 -3
  8. package/dist/adopt-existing.d.ts.map +1 -1
  9. package/dist/adopt-existing.js +143 -0
  10. package/dist/adopt-existing.js.map +1 -1
  11. package/dist/agent/command.d.ts +23 -0
  12. package/dist/agent/command.d.ts.map +1 -1
  13. package/dist/agent/command.js +32 -9
  14. package/dist/agent/command.js.map +1 -1
  15. package/dist/agent/run.d.ts +2 -0
  16. package/dist/agent/run.d.ts.map +1 -1
  17. package/dist/agent/run.js +5 -1
  18. package/dist/agent/run.js.map +1 -1
  19. package/dist/agent-db.js +69 -10
  20. package/dist/aws-task-roles.d.ts +4 -2
  21. package/dist/aws-task-roles.d.ts.map +1 -1
  22. package/dist/aws-task-roles.js +4 -1
  23. package/dist/aws-task-roles.js.map +1 -1
  24. package/dist/caddy.d.ts +21 -4
  25. package/dist/caddy.d.ts.map +1 -1
  26. package/dist/caddy.js +27 -3
  27. package/dist/caddy.js.map +1 -1
  28. package/dist/cli-routing.js +1 -1
  29. package/dist/cli-routing.js.map +1 -1
  30. package/dist/cli.js +68 -7
  31. package/dist/cli.js.map +1 -1
  32. package/dist/cloudsql-tls.d.ts +93 -0
  33. package/dist/cloudsql-tls.d.ts.map +1 -0
  34. package/dist/cloudsql-tls.js +295 -0
  35. package/dist/cloudsql-tls.js.map +1 -0
  36. package/dist/compose.d.ts +43 -6
  37. package/dist/compose.d.ts.map +1 -1
  38. package/dist/compose.js +148 -23
  39. package/dist/compose.js.map +1 -1
  40. package/dist/converge-checkpoint.d.ts +1 -0
  41. package/dist/converge-checkpoint.d.ts.map +1 -1
  42. package/dist/converge-checkpoint.js +8 -1
  43. package/dist/converge-checkpoint.js.map +1 -1
  44. package/dist/converge-db-trust.d.ts +44 -0
  45. package/dist/converge-db-trust.d.ts.map +1 -0
  46. package/dist/converge-db-trust.js +158 -0
  47. package/dist/converge-db-trust.js.map +1 -0
  48. package/dist/converge-job-gcp.d.ts +5 -2
  49. package/dist/converge-job-gcp.d.ts.map +1 -1
  50. package/dist/converge-job-gcp.js +11 -13
  51. package/dist/converge-job-gcp.js.map +1 -1
  52. package/dist/converge-job-preflight.d.ts.map +1 -1
  53. package/dist/converge-job-preflight.js +8 -0
  54. package/dist/converge-job-preflight.js.map +1 -1
  55. package/dist/converge-job.d.ts +17 -0
  56. package/dist/converge-job.d.ts.map +1 -1
  57. package/dist/converge-job.js +12 -0
  58. package/dist/converge-job.js.map +1 -1
  59. package/dist/deployed-release.d.ts +4 -1
  60. package/dist/deployed-release.d.ts.map +1 -1
  61. package/dist/deployed-release.js +18 -8
  62. package/dist/deployed-release.js.map +1 -1
  63. package/dist/gcp-names.d.ts +34 -0
  64. package/dist/gcp-names.d.ts.map +1 -1
  65. package/dist/gcp-names.js +45 -0
  66. package/dist/gcp-names.js.map +1 -1
  67. package/dist/gcp-url-map.d.ts.map +1 -1
  68. package/dist/gcp-url-map.js +6 -2
  69. package/dist/gcp-url-map.js.map +1 -1
  70. package/dist/index.d.ts +3 -2
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/infra-contract.d.ts +1 -1
  75. package/dist/infra-contract.d.ts.map +1 -1
  76. package/dist/infra-contract.js +45 -1
  77. package/dist/infra-contract.js.map +1 -1
  78. package/dist/install-job.d.ts +4 -2
  79. package/dist/install-job.d.ts.map +1 -1
  80. package/dist/install-job.js +4 -1
  81. package/dist/install-job.js.map +1 -1
  82. package/dist/install-state.d.ts +13 -0
  83. package/dist/install-state.d.ts.map +1 -1
  84. package/dist/install-state.js +17 -4
  85. package/dist/install-state.js.map +1 -1
  86. package/dist/lifecycle.d.ts.map +1 -1
  87. package/dist/lifecycle.js +20 -8
  88. package/dist/lifecycle.js.map +1 -1
  89. package/dist/mcp-auth-proxy-setup.d.ts +48 -0
  90. package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
  91. package/dist/mcp-auth-proxy-setup.js +280 -0
  92. package/dist/mcp-auth-proxy-setup.js.map +1 -0
  93. package/dist/migration-runner.d.ts.map +1 -1
  94. package/dist/migration-runner.js +8 -3
  95. package/dist/migration-runner.js.map +1 -1
  96. package/dist/migration-targets.d.ts +11 -0
  97. package/dist/migration-targets.d.ts.map +1 -0
  98. package/dist/migration-targets.js +12 -0
  99. package/dist/migration-targets.js.map +1 -0
  100. package/dist/operator-access.d.ts +10 -0
  101. package/dist/operator-access.d.ts.map +1 -1
  102. package/dist/operator-access.js +5 -0
  103. package/dist/operator-access.js.map +1 -1
  104. package/dist/pool-budget-stack.d.ts +13 -1
  105. package/dist/pool-budget-stack.d.ts.map +1 -1
  106. package/dist/pool-budget-stack.js +11 -1
  107. package/dist/pool-budget-stack.js.map +1 -1
  108. package/dist/public-role-isolation.d.ts +7 -3
  109. package/dist/public-role-isolation.d.ts.map +1 -1
  110. package/dist/public-role-isolation.js +25 -13
  111. package/dist/public-role-isolation.js.map +1 -1
  112. package/dist/release-contract.d.ts.map +1 -1
  113. package/dist/release-contract.js +3 -0
  114. package/dist/release-contract.js.map +1 -1
  115. package/dist/release-manifest.d.ts +25 -1
  116. package/dist/release-manifest.d.ts.map +1 -1
  117. package/dist/release-manifest.js +36 -3
  118. package/dist/release-manifest.js.map +1 -1
  119. package/dist/remote-mcp.d.ts +55 -18
  120. package/dist/remote-mcp.d.ts.map +1 -1
  121. package/dist/remote-mcp.js +307 -77
  122. package/dist/remote-mcp.js.map +1 -1
  123. package/dist/runtime-secrets.d.ts +11 -0
  124. package/dist/runtime-secrets.d.ts.map +1 -1
  125. package/dist/runtime-secrets.js +40 -25
  126. package/dist/runtime-secrets.js.map +1 -1
  127. package/dist/scheduler-setup.d.ts +75 -0
  128. package/dist/scheduler-setup.d.ts.map +1 -0
  129. package/dist/scheduler-setup.js +331 -0
  130. package/dist/scheduler-setup.js.map +1 -0
  131. package/dist/seal-key-rotation.d.ts +7 -7
  132. package/dist/seal-key-rotation.d.ts.map +1 -1
  133. package/dist/seal-key-rotation.js +22 -22
  134. package/dist/seal-key-rotation.js.map +1 -1
  135. package/dist/self-hosted-db-tls.d.ts +37 -0
  136. package/dist/self-hosted-db-tls.d.ts.map +1 -0
  137. package/dist/self-hosted-db-tls.js +220 -0
  138. package/dist/self-hosted-db-tls.js.map +1 -0
  139. package/dist/self-hosted-release.d.ts +3 -1
  140. package/dist/self-hosted-release.d.ts.map +1 -1
  141. package/dist/self-hosted-release.js +33 -9
  142. package/dist/self-hosted-release.js.map +1 -1
  143. package/dist/self-hosted-roles.d.ts +43 -2
  144. package/dist/self-hosted-roles.d.ts.map +1 -1
  145. package/dist/self-hosted-roles.js +87 -1
  146. package/dist/self-hosted-roles.js.map +1 -1
  147. package/dist/self-hosted-scheduler-interval.d.ts +21 -0
  148. package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
  149. package/dist/self-hosted-scheduler-interval.js +68 -0
  150. package/dist/self-hosted-scheduler-interval.js.map +1 -0
  151. package/dist/self-hosted-start.d.ts +0 -21
  152. package/dist/self-hosted-start.d.ts.map +1 -1
  153. package/dist/self-hosted-start.js +39 -1
  154. package/dist/self-hosted-start.js.map +1 -1
  155. package/dist/self-hosted-upgrade.d.ts +19 -7
  156. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  157. package/dist/self-hosted-upgrade.js +78 -25
  158. package/dist/self-hosted-upgrade.js.map +1 -1
  159. package/dist/stack-contract.d.ts +234 -28
  160. package/dist/stack-contract.d.ts.map +1 -1
  161. package/dist/stack-contract.js +397 -39
  162. package/dist/stack-contract.js.map +1 -1
  163. package/dist/stack-converge.d.ts +31 -0
  164. package/dist/stack-converge.d.ts.map +1 -1
  165. package/dist/stack-converge.js +71 -30
  166. package/dist/stack-converge.js.map +1 -1
  167. package/dist/staging-database.d.ts +22 -0
  168. package/dist/staging-database.d.ts.map +1 -0
  169. package/dist/staging-database.js +117 -0
  170. package/dist/staging-database.js.map +1 -0
  171. package/dist/upgrade/app-roll.d.ts +25 -4
  172. package/dist/upgrade/app-roll.d.ts.map +1 -1
  173. package/dist/upgrade/app-roll.js +63 -17
  174. package/dist/upgrade/app-roll.js.map +1 -1
  175. package/dist/upgrade/converge.d.ts +5 -0
  176. package/dist/upgrade/converge.d.ts.map +1 -1
  177. package/dist/upgrade/converge.js +207 -20
  178. package/dist/upgrade/converge.js.map +1 -1
  179. package/dist/upgrade/health.d.ts +31 -7
  180. package/dist/upgrade/health.d.ts.map +1 -1
  181. package/dist/upgrade/health.js +41 -17
  182. package/dist/upgrade/health.js.map +1 -1
  183. package/dist/upgrade/image-forms.d.ts +1 -1
  184. package/dist/upgrade/image-forms.d.ts.map +1 -1
  185. package/dist/upgrade/image-forms.js +24 -7
  186. package/dist/upgrade/image-forms.js.map +1 -1
  187. package/dist/upgrade/release-images.d.ts +12 -3
  188. package/dist/upgrade/release-images.d.ts.map +1 -1
  189. package/dist/upgrade/release-images.js +13 -4
  190. package/dist/upgrade/release-images.js.map +1 -1
  191. package/dist/upgrade/upgrade.d.ts +19 -3
  192. package/dist/upgrade/upgrade.d.ts.map +1 -1
  193. package/dist/upgrade/upgrade.js +37 -2
  194. package/dist/upgrade/upgrade.js.map +1 -1
  195. package/dist/upgrader-gcp.d.ts +13 -0
  196. package/dist/upgrader-gcp.d.ts.map +1 -1
  197. package/dist/upgrader-gcp.js +50 -1
  198. package/dist/upgrader-gcp.js.map +1 -1
  199. package/dist/upgrader-iam-preflight.d.ts.map +1 -1
  200. package/dist/upgrader-iam-preflight.js +6 -1
  201. package/dist/upgrader-iam-preflight.js.map +1 -1
  202. package/dist/upgrader-job.d.ts +1 -1
  203. package/dist/upgrader-job.d.ts.map +1 -1
  204. package/dist/upgrader-job.js +20 -14
  205. package/dist/upgrader-job.js.map +1 -1
  206. package/dist/upgrader-records.d.ts +1 -1
  207. package/dist/upgrader-records.d.ts.map +1 -1
  208. package/dist/upgrader-records.js +26 -7
  209. package/dist/upgrader-records.js.map +1 -1
  210. package/dist/upgrader-setup.d.ts.map +1 -1
  211. package/dist/upgrader-setup.js +10 -5
  212. package/dist/upgrader-setup.js.map +1 -1
  213. package/dist/wizard.d.ts +7 -0
  214. package/dist/wizard.d.ts.map +1 -1
  215. package/dist/wizard.js +11 -0
  216. package/dist/wizard.js.map +1 -1
  217. package/dist/wizards/gcp-cost.d.ts +31 -0
  218. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  219. package/dist/wizards/gcp-cost.js +103 -18
  220. package/dist/wizards/gcp-cost.js.map +1 -1
  221. package/dist/wizards/gcp.d.ts.map +1 -1
  222. package/dist/wizards/gcp.js +129 -27
  223. package/dist/wizards/gcp.js.map +1 -1
  224. package/package.json +2 -2
  225. package/src/adapter.ts +6 -0
  226. package/src/admin-domain.test.ts +9 -8
  227. package/src/admin-domain.ts +13 -3
  228. package/src/admin-memory.test.ts +2 -1
  229. package/src/adopt-existing.test.ts +85 -0
  230. package/src/adopt-existing.ts +141 -3
  231. package/src/agent/command.test.ts +49 -1
  232. package/src/agent/command.ts +39 -9
  233. package/src/agent/run.test.ts +19 -0
  234. package/src/agent/run.ts +11 -2
  235. package/src/aws-task-roles.ts +5 -2
  236. package/src/caddy-redirects.docker.test.ts +2 -0
  237. package/src/caddy-redirects.test.ts +4 -1
  238. package/src/caddy-validate.test.ts +7 -1
  239. package/src/caddy.test.ts +39 -1
  240. package/src/caddy.ts +36 -4
  241. package/src/checkpoint-secrets.test.ts +14 -6
  242. package/src/cli-routing.test.ts +1 -1
  243. package/src/cli-routing.ts +1 -1
  244. package/src/cli.ts +71 -5
  245. package/src/cloudsql-tls.test.ts +255 -0
  246. package/src/cloudsql-tls.ts +359 -0
  247. package/src/compose.ts +193 -24
  248. package/src/converge-checkpoint.ts +8 -1
  249. package/src/converge-db-trust.test.ts +237 -0
  250. package/src/converge-db-trust.ts +212 -0
  251. package/src/converge-job-gcp.ts +11 -12
  252. package/src/converge-job-preflight.test.ts +5 -0
  253. package/src/converge-job-preflight.ts +8 -0
  254. package/src/converge-job.test.ts +70 -0
  255. package/src/converge-job.ts +32 -0
  256. package/src/database-tls-builders.test.ts +56 -0
  257. package/src/deployed-release.test.ts +107 -40
  258. package/src/deployed-release.ts +30 -9
  259. package/src/gateway-env-isolation.test.ts +50 -12
  260. package/src/gcp-names.ts +56 -0
  261. package/src/gcp-url-map.test.ts +6 -8
  262. package/src/gcp-url-map.ts +6 -2
  263. package/src/index.test.ts +10 -4
  264. package/src/index.ts +7 -0
  265. package/src/infra-contract.test.ts +7 -3
  266. package/src/infra-contract.ts +45 -1
  267. package/src/install-job.ts +4 -1
  268. package/src/install-state.ts +33 -4
  269. package/src/lifecycle.ts +23 -11
  270. package/src/mcp-auth-proxy-setup.test.ts +305 -0
  271. package/src/mcp-auth-proxy-setup.ts +374 -0
  272. package/src/mcp-iap-stacks.test.ts +1 -1
  273. package/src/media-volume.test.ts +1 -1
  274. package/src/migration-runner.ts +14 -9
  275. package/src/migration-targets.ts +12 -0
  276. package/src/operator-access-stacks.test.ts +1 -1
  277. package/src/operator-access.test.ts +20 -4
  278. package/src/operator-access.ts +14 -0
  279. package/src/pool-budget-stack.ts +15 -0
  280. package/src/pool-budget-stacks.test.ts +39 -14
  281. package/src/public-role-isolation.test.ts +18 -8
  282. package/src/public-role-isolation.ts +31 -17
  283. package/src/release-contract.ts +3 -0
  284. package/src/release-manifest.test.ts +25 -0
  285. package/src/release-manifest.ts +59 -4
  286. package/src/remote-mcp-stack-config.test.ts +19 -9
  287. package/src/remote-mcp.test.ts +252 -57
  288. package/src/remote-mcp.ts +336 -77
  289. package/src/runtime-secrets.test.ts +37 -14
  290. package/src/runtime-secrets.ts +87 -27
  291. package/src/scheduler-setup.test.ts +201 -0
  292. package/src/scheduler-setup.ts +375 -0
  293. package/src/seal-key-rotation.test.ts +2 -2
  294. package/src/seal-key-rotation.ts +25 -25
  295. package/src/self-hosted-backup.test.ts +1 -0
  296. package/src/self-hosted-compose.test.ts +180 -6
  297. package/src/self-hosted-db-tls.test.ts +111 -0
  298. package/src/self-hosted-db-tls.ts +287 -0
  299. package/src/self-hosted-release-resolve.test.ts +71 -0
  300. package/src/self-hosted-release.ts +41 -11
  301. package/src/self-hosted-roles.integration.test.ts +2 -0
  302. package/src/self-hosted-roles.test.ts +87 -3
  303. package/src/self-hosted-roles.ts +101 -3
  304. package/src/self-hosted-scheduler-interval.test.ts +66 -0
  305. package/src/self-hosted-scheduler-interval.ts +79 -0
  306. package/src/self-hosted-start.test.ts +35 -2
  307. package/src/self-hosted-start.ts +20 -2
  308. package/src/self-hosted-upgrade.test.ts +104 -14
  309. package/src/self-hosted-upgrade.ts +123 -31
  310. package/src/stack-contract-parity.test.ts +79 -12
  311. package/src/stack-contract.test.ts +199 -31
  312. package/src/stack-contract.ts +472 -45
  313. package/src/stack-converge-secrets.test.ts +28 -13
  314. package/src/stack-converge.test.ts +203 -28
  315. package/src/stack-converge.ts +92 -30
  316. package/src/staging-database.test.ts +114 -0
  317. package/src/staging-database.ts +148 -0
  318. package/src/state-store.test.ts +2 -1
  319. package/src/upgrade/app-roll.ts +97 -19
  320. package/src/upgrade/converge.ts +249 -19
  321. package/src/upgrade/health.test.ts +63 -25
  322. package/src/upgrade/health.ts +54 -22
  323. package/src/upgrade/image-forms.test.ts +34 -0
  324. package/src/upgrade/image-forms.ts +23 -8
  325. package/src/upgrade/release-images.test.ts +5 -1
  326. package/src/upgrade/release-images.ts +22 -6
  327. package/src/upgrade/upgrade.test.ts +438 -46
  328. package/src/upgrade/upgrade.ts +64 -6
  329. package/src/upgrader-gcp.ts +66 -3
  330. package/src/upgrader-iam-preflight.test.ts +19 -0
  331. package/src/upgrader-iam-preflight.ts +6 -0
  332. package/src/upgrader-job.test.ts +101 -27
  333. package/src/upgrader-job.ts +27 -14
  334. package/src/upgrader-records.test.ts +97 -12
  335. package/src/upgrader-records.ts +28 -11
  336. package/src/upgrader-setup.test.ts +2 -0
  337. package/src/upgrader-setup.ts +10 -4
  338. package/src/wizard.ts +19 -0
  339. package/src/wizards/gcp-cost.test.ts +103 -0
  340. package/src/wizards/gcp-cost.ts +125 -18
  341. package/src/wizards/gcp.ts +164 -22
  342. package/stacks/aws/Pulumi.yaml +1 -1
  343. package/stacks/aws/index.ts +72 -14
  344. package/stacks/azure/index.ts +108 -21
  345. package/stacks/gcp/Pulumi.yaml +1 -1
  346. package/stacks/gcp/README.md +1 -0
  347. package/stacks/gcp/index.ts +133 -24
  348. package/stacks/gcp-firebase/Pulumi.yaml +1 -1
  349. package/stacks/gcp-firebase/index.ts +212 -34
  350. package/stacks/self-hosted/Pulumi.yaml +3 -0
  351. package/stacks/self-hosted/README.md +6 -1
  352. package/stacks/self-hosted/index.ts +40 -5
@@ -7,8 +7,10 @@
7
7
  * - Static site: Firebase Hosting site (no LB, no BackendBucket)
8
8
  * - Admin: Cloud Run + custom domain mapping (admin.<domain>)
9
9
  * + IAP-on-Cloud-Run (no LB IAP)
10
- * - Gateway: Cloud Run + Firebase Hosting `rewrites` (no LB,
11
- * no Cloud Armor)
10
+ * - Gateway: Cloud Run `caelo-<env>-data-gateway` + Firebase
11
+ * Hosting `rewrites` (no LB, no Cloud Armor)
12
+ * - MCP-Auth-Proxy: Cloud Run on its own run.app URL (#1328; the
13
+ * service that was the gateway, E13 a of #763)
12
14
  * - Database: Cloud SQL Postgres on private VPC IP only (same
13
15
  * as `gcp/`)
14
16
  * - Workers: share admin Cloud Run (same as `gcp/`)
@@ -35,11 +37,14 @@ import {
35
37
  gcpBucketName,
36
38
  gcpSecretId,
37
39
  MCP_IAP_SERVICE_ACCOUNT_ID,
40
+ mcpAuthProxyServiceAccountId,
38
41
  runServiceAccountId,
39
42
  } from "../../dist/gcp-names.js";
40
43
  import {
44
+ DEFAULT_ADMIN_MAX_INSTANCES,
41
45
  DEFAULT_GATEWAY_MAX_INSTANCES,
42
46
  DEFAULT_MAX_CONNECTIONS,
47
+ DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
43
48
  stackPoolBudget,
44
49
  } from "../../dist/pool-budget-stack.js";
45
50
  import {
@@ -48,17 +53,27 @@ import {
48
53
  adminMediaVolume,
49
54
  adminMediaVolumeTemplate,
50
55
  CLOUD_RUN_IGNORE_CHANGES,
56
+ CLOUD_SQL_PSA_CONNECTION_TYPE,
57
+ CLOUD_SQL_SERVER_CA_MODE,
58
+ CLOUD_SQL_SSL_MODE,
51
59
  type CloudRunEnvVar,
52
60
  type CloudRunSlug,
61
+ cloudSqlDnsZoneName,
62
+ cloudSqlPrivateDnsName,
63
+ DEFAULT_MCP_AUTH_PROXY_MAX_INSTANCES,
53
64
  databaseUrls,
54
65
  defaultStagingHost,
55
- GATEWAY_TIMEOUT_SECONDS,
66
+ fixedCloudRunServiceName,
56
67
  gatewayEnvContract,
68
+ INTERIM_FORWARD_ORIGIN_ENV,
69
+ MCP_AUTH_PROXY_TIMEOUT_SECONDS,
70
+ mcpAuthProxyEnvContract,
57
71
  REMOTE_MCP_IDENTITY_CONFIG,
58
72
  type RuntimeSecret,
59
73
  remoteMcpIdentityStackEnv,
60
74
  runtimeSecretBindings,
61
75
  SERVICE_CPU_IDLE,
76
+ SERVICE_VPC_EGRESS,
62
77
  secretFileVolumesTemplate,
63
78
  } from "../../dist/stack-contract.js";
64
79
 
@@ -96,7 +111,10 @@ const adminMinInstances = Number.parseInt(cfg.get("adminMinInstances") ?? "0", 1
96
111
  // #553 — the admin also runs the Lighthouse quality audit; same knob and
97
112
  // default on every adapter (stack-contract.ts ADMIN_MEMORY_DEFAULT).
98
113
  const adminMemory = cfg.get("adminMemory") ?? ADMIN_MEMORY_DEFAULT;
99
- const adminMaxInstances = Number.parseInt(cfg.get("adminMaxInstances") ?? "5", 10);
114
+ const adminMaxInstances = Number.parseInt(
115
+ cfg.get("adminMaxInstances") ?? String(DEFAULT_ADMIN_MAX_INSTANCES),
116
+ 10,
117
+ );
100
118
  const gatewayMinInstances = Number.parseInt(cfg.get("gatewayMinInstances") ?? "0", 10);
101
119
  // #1254 — part of the connection budget (stacks/gcp has the long comment).
102
120
  const gatewayMaxInstances = Number.parseInt(
@@ -112,6 +130,8 @@ const poolBudget = stackPoolBudget("gcp-firebase", {
112
130
  maxConnections,
113
131
  adminMaxInstances,
114
132
  gatewayMaxInstances,
133
+ // #1329 — the staging gateway (P3b-2) is budgeted from this release on.
134
+ stagingGatewayMaxInstances: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
115
135
  });
116
136
  const backupRetentionDays = Number.parseInt(cfg.get("backupRetentionDays") ?? "7", 10);
117
137
  const iapAllowlistRaw = cfg.get("iapAllowlist");
@@ -249,6 +269,8 @@ const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
249
269
  "tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
250
270
  "staging-gate-secret": gcpSecretId(env, "staging-gate-secret"),
251
271
  "public-role-password": gcpSecretId(env, "public-role-password"),
272
+ // #1329 — the staging gateway's login, never the live one's.
273
+ "public-staging-role-password": gcpSecretId(env, "public-staging-role-password"),
252
274
  // #914 / #806 — the Owner-set Google sign-in client (a placeholder until
253
275
  // then; IAP login, IAP programmatic access and the remote-MCP broker) and
254
276
  // the broker's seal key, both CLI-generated.
@@ -265,6 +287,14 @@ const publicRolePassword = pulumi.secret(
265
287
  { provider: gcpProvider },
266
288
  ).secretData,
267
289
  );
290
+ // #1329 — public_staging_role's own password (CLI-generated like the one
291
+ // above): the staging gateway's only login, on cms_public_staging only.
292
+ const publicStagingRolePassword = pulumi.secret(
293
+ gcp.secretmanager.getSecretVersionOutput(
294
+ { project, secret: gcpSecretId(env, "public-staging-role-password") },
295
+ { provider: gcpProvider },
296
+ ).secretData,
297
+ );
268
298
 
269
299
  // =========================================================================
270
300
  // Tier 4 — Cloud SQL Postgres (private IP only; HA configurable)
@@ -293,6 +323,11 @@ const sqlInstance = new gcp.sql.DatabaseInstance(
293
323
  ipv4Enabled: false,
294
324
  privateNetwork: network.id,
295
325
  enablePrivatePathForGoogleCloudServices: true,
326
+ // #1338 — the shared CA names the instance's DNS name in its server
327
+ // certificate, so every client verifies it (sslmode=verify-full);
328
+ // unencrypted connections are refused outright.
329
+ serverCaMode: CLOUD_SQL_SERVER_CA_MODE,
330
+ sslMode: CLOUD_SQL_SSL_MODE,
296
331
  },
297
332
  databaseFlags: [
298
333
  { name: "max_connections", value: maxConnections.toString() },
@@ -316,6 +351,13 @@ const cmsPublicDb = new gcp.sql.Database(
316
351
  { instance: sqlInstance.name, name: "cms_public" },
317
352
  opts,
318
353
  );
354
+ // #1329 — staging's own data: the twin of cms_public on the same instance
355
+ // (no extra cost), reached by the staging gateway only.
356
+ const cmsPublicStagingDb = new gcp.sql.Database(
357
+ `${namePrefix}-cms-public-staging`,
358
+ { instance: sqlInstance.name, name: "cms_public_staging" },
359
+ opts,
360
+ );
319
361
 
320
362
  const pgAdminUser = new gcp.sql.User(
321
363
  `${namePrefix}-admin-user`,
@@ -334,10 +376,58 @@ new gcp.sql.User(
334
376
  { instance: sqlInstance.name, name: "public_role", password: publicRolePassword },
335
377
  opts,
336
378
  );
379
+ // #1329 — the staging gateway's login: an Admin API user like public_role,
380
+ // so the same membership revoke (public-role-isolation.ts) and the same
381
+ // audit apply; migration 0530 gives it CONNECT on cms_public_staging only.
382
+ new gcp.sql.User(
383
+ `${namePrefix}-public-staging-user`,
384
+ { instance: sqlInstance.name, name: "public_staging_role", password: publicStagingRolePassword },
385
+ opts,
386
+ );
387
+
388
+ // #1338 — the instance's DNS name, resolved to its private IP by a private
389
+ // zone in the VPC (Cloud SQL assigns the name, it does not resolve it).
390
+ // Every client dials this name with sslmode=verify-full; the certificate
391
+ // names no IP. `cms-provision upgrade` converges the same zone and record
392
+ // through gcloud (cloudsql-tls.ts); the installer adopts them (#1079).
393
+ const sqlDnsName = sqlInstance.dnsNames.apply((names) => {
394
+ const name = cloudSqlPrivateDnsName(names);
395
+ if (!name) {
396
+ throw new Error(
397
+ `Cloud SQL instance has no ${CLOUD_SQL_PSA_CONNECTION_TYPE} DNS name; it needs the shared server CA (${CLOUD_SQL_SERVER_CA_MODE}) for verify-full connections (#1338)`,
398
+ );
399
+ }
400
+ return name;
401
+ });
402
+ const sqlDnsZone = new gcp.dns.ManagedZone(
403
+ `${namePrefix}-pg-dns`,
404
+ {
405
+ name: cloudSqlDnsZoneName(env),
406
+ dnsName: pulumi.interpolate`${sqlDnsName}.`,
407
+ visibility: "private",
408
+ privateVisibilityConfig: { networks: [{ networkUrl: network.id }] },
409
+ description: `Caelo ${env}: the Cloud SQL instance's DNS name inside the VPC (#1338)`,
410
+ },
411
+ opts,
412
+ );
413
+ const sqlDnsRecord = new gcp.dns.RecordSet(
414
+ `${namePrefix}-pg-dns-a`,
415
+ {
416
+ // Literal names, not the zone's outputs: the installer can then adopt
417
+ // a record an upgrade created (adopt-existing.ts) from the preview.
418
+ managedZone: cloudSqlDnsZoneName(env),
419
+ name: pulumi.interpolate`${sqlDnsName}.`,
420
+ type: "A",
421
+ ttl: 300,
422
+ rrdatas: [sqlInstance.privateIpAddress],
423
+ },
424
+ { ...opts, dependsOn: [sqlDnsZone] },
425
+ );
337
426
 
338
427
  // Password-less connection URLs (stack-contract.ts databaseUrls); the
339
- // services get the password as a Secret Manager reference.
340
- const dbUrls = sqlInstance.privateIpAddress.apply(databaseUrls);
428
+ // services get the password as a Secret Manager reference. Tied to the
429
+ // record so no revision boots before its database host resolves.
430
+ const dbUrls = pulumi.all([sqlDnsName, sqlDnsRecord.id]).apply(([host]) => databaseUrls(host));
341
431
 
342
432
  // =========================================================================
343
433
  // Media bucket — private, signed-URL access only (same as `gcp` stack)
@@ -380,9 +470,22 @@ const gatewaySa = new gcp.serviceaccount.Account(
380
470
  },
381
471
  opts,
382
472
  );
473
+ // #1328 — the MCP-Auth-Proxy's own SA: secretAccessor on its two secrets
474
+ // below and nothing else — no project role, not even the log writer (Cloud
475
+ // Run ships a container's stdout to Cloud Logging without one).
476
+ const mcpAuthProxySa = new gcp.serviceaccount.Account(
477
+ `${namePrefix}-mcp-auth-proxy-sa`,
478
+ {
479
+ accountId: mcpAuthProxyServiceAccountId(env),
480
+ displayName: `Caelo ${env} MCP-Auth-Proxy`,
481
+ createIgnoreAlreadyExists: true,
482
+ },
483
+ opts,
484
+ );
383
485
  const serviceSa: Record<CloudRunSlug, gcp.serviceaccount.Account> = {
384
486
  admin: runSa,
385
487
  gateway: gatewaySa,
488
+ "mcp-auth-proxy": mcpAuthProxySa,
386
489
  };
387
490
 
388
491
  // secretAccessor for each service's SA on exactly the secrets its env reads.
@@ -391,6 +494,7 @@ const serviceSa: Record<CloudRunSlug, gcp.serviceaccount.Account> = {
391
494
  const secretBindings: Record<CloudRunSlug, gcp.secretmanager.SecretIamMember[]> = {
392
495
  admin: [],
393
496
  gateway: [],
497
+ "mcp-auth-proxy": [],
394
498
  };
395
499
  for (const binding of runtimeSecretBindings()) {
396
500
  secretBindings[binding.service].push(
@@ -533,11 +637,19 @@ const firebaseCustomDomain = new gcp.firebase.HostingCustomDomain(
533
637
  );
534
638
 
535
639
  // =========================================================================
536
- // Cloud Run services — admin + gateway
640
+ // Cloud Run services — admin, data gateway, MCP-Auth-Proxy
537
641
  // =========================================================================
538
642
 
539
643
  interface CloudRunArgs {
540
- readonly serviceName: "admin" | "gateway";
644
+ readonly serviceName: CloudRunSlug;
645
+ /**
646
+ * The Pulumi resource name, when it is not `${namePrefix}-<serviceName>`.
647
+ * #1328 E13 a: the proxy keeps the logical name the gateway had, so Pulumi
648
+ * keeps the same resource and its auto-generated service name — an alias
649
+ * under a new name would let the provider pick a new name, and a new
650
+ * service means a new run.app URL.
651
+ */
652
+ readonly resourceName?: string;
541
653
  readonly minInstances: number;
542
654
  readonly maxInstances: number;
543
655
  readonly memory: string;
@@ -582,9 +694,13 @@ function imageRef(service: string): string {
582
694
  const remoteMcpEnv = remoteMcpIdentityStackEnv(cfg.get(REMOTE_MCP_IDENTITY_CONFIG));
583
695
 
584
696
  function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
697
+ // #1328 — the data gateway has a fixed name: the converge creates it on
698
+ // installs whose stack predates it, and the installer adopts that one.
699
+ const fixedName = fixedCloudRunServiceName("gcp-firebase", env, args.serviceName);
585
700
  return new gcp.cloudrunv2.Service(
586
- `${namePrefix}-${args.serviceName}`,
701
+ args.resourceName ?? `${namePrefix}-${args.serviceName}`,
587
702
  {
703
+ ...(fixedName ? { name: fixedName } : {}),
588
704
  location: region,
589
705
  ingress: args.ingress,
590
706
  // v0.3.1 — Cloud Run native IAP. With this on, every request
@@ -604,8 +720,8 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
604
720
  ? { executionEnvironment: args.mediaVolume.executionEnvironment }
605
721
  : {}),
606
722
  // The admin's media bucket, plus each service's secret files (#806:
607
- // the gateway's remote-MCP OAuth client) — both from the contract
608
- // upgrade applies to existing installs.
723
+ // the MCP-Auth-Proxy's remote-MCP OAuth client) — both from the
724
+ // contract upgrade applies to existing installs.
609
725
  volumes: [
610
726
  ...(args.mediaVolume?.volumes ?? []),
611
727
  ...secretFileVolumesTemplate(args.serviceName, env).volumes,
@@ -625,10 +741,16 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
625
741
  },
626
742
  },
627
743
  ],
628
- vpcAccess: {
629
- networkInterfaces: [{ network: network.name, subnetwork: subnet.name }],
630
- egress: "PRIVATE_RANGES_ONLY",
631
- },
744
+ // #1328 — the MCP-Auth-Proxy gets no route into the VPC (and so
745
+ // none to Cloud SQL's private IP): SERVICE_VPC_EGRESS.
746
+ ...(SERVICE_VPC_EGRESS[args.serviceName]
747
+ ? {
748
+ vpcAccess: {
749
+ networkInterfaces: [{ network: network.name, subnetwork: subnet.name }],
750
+ egress: "PRIVATE_RANGES_ONLY",
751
+ },
752
+ }
753
+ : {}),
632
754
  },
633
755
  },
634
756
  {
@@ -639,6 +761,7 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
639
761
  pgAdminUser,
640
762
  cmsAdminDb,
641
763
  cmsPublicDb,
764
+ cmsPublicStagingDb,
642
765
  // The env contract names the media bucket as a string; keep it
643
766
  // ordered before the services that use it.
644
767
  mediaBucket,
@@ -666,33 +789,58 @@ const envContractInputs = {
666
789
  admin: dbUrls.admin,
667
790
  publicAdmin: dbUrls.publicAdmin,
668
791
  public: dbUrls.public,
792
+ publicStagingAdmin: dbUrls.publicStagingAdmin,
793
+ publicStaging: dbUrls.publicStaging,
669
794
  },
670
795
  } as const;
671
796
 
672
- // Gateway provisioned first so we can pass its name/region into the
673
- // admin's env (the Firebase publisher needs them to wire rewrites).
797
+ // The data gateway is provisioned first so its name/region can go into the
798
+ // admin's env (the Firebase publisher wires the /api/** rewrite to it).
799
+ // #1328 — a service of its own (`caelo-<env>-data-gateway`); before, the
800
+ // gateway was also the remote-MCP broker.
674
801
  const gatewaySvc = cloudRunService({
675
802
  serviceName: "gateway",
676
803
  minInstances: gatewayMinInstances,
677
804
  maxInstances: gatewayMaxInstances,
678
805
  memory: "512Mi",
679
- // #806 — remote MCP: chat turns stream through the gateway for minutes
680
- // (stack-contract.ts GATEWAY_TIMEOUT_SECONDS). The Firebase Hosting
681
- // rewrite keeps its own 60 s cut; MCP clients use the gateway's Cloud Run
682
- // URL directly.
683
- timeout: `${GATEWAY_TIMEOUT_SECONDS}s`,
684
- // #650 — all traffic: the gateway also serves staging.<domain> through
685
- // a Cloud Run domain mapping (`cms-provision staging-domain enable`),
686
- // and Cloud Run refuses domain-mapped requests under an internal-only
687
- // ingress. Exposure is unchanged in substance: the service was already
688
- // invokable by allUsers (gateway-public-invoker below) and every /api
689
- // request passes its rate limits, captcha and honeypot; staging requests
690
- // pass the staging access gate.
806
+ // Firebase Hosting cuts a rewritten request at 60 s anyway.
807
+ timeout: "300s",
808
+ // #650 — all traffic: `cms-provision staging-domain enable` maps the
809
+ // staging host onto the data gateway (Cloud Run refuses domain-mapped
810
+ // requests under an internal-only ingress), and it is invokable by
811
+ // allUsers anyway (data-gateway-public-invoker below): every /api request
812
+ // passes its rate limits, captcha and honeypot; staging requests pass the
813
+ // staging access gate.
814
+ ingress: "INGRESS_TRAFFIC_ALL",
815
+ contractEnv: [...gatewayEnvContract(envContractInputs), ...poolBudget.env.gateway],
816
+ });
817
+
818
+ // #1328 — the MCP-Auth-Proxy: remote MCP's sign-in broker and its stream to
819
+ // the IAP-protected admin, with no database, no VPC route and an account
820
+ // that reads two secrets. E13 (a) of #763: it is the resource that was the
821
+ // gateway (same logical name), so its service and run.app URL — the address every
822
+ // connector in claude.ai / ChatGPT holds, and the Google sign-in client's
823
+ // registered redirect URI — stay as they were. Always deployed at minimum
824
+ // zero; while remote MCP is not set up it answers a fixed 404 (E14 a).
825
+ const mcpAuthProxySvc = cloudRunService({
826
+ serviceName: "mcp-auth-proxy",
827
+ resourceName: `${namePrefix}-gateway`,
828
+ minInstances: 0,
829
+ maxInstances: DEFAULT_MCP_AUTH_PROXY_MAX_INSTANCES,
830
+ memory: "512Mi",
831
+ // #806 — chat turns stream for minutes (MCP_AUTH_PROXY_TIMEOUT_SECONDS);
832
+ // MCP clients call its run.app URL, never the Firebase rewrite (which
833
+ // buffers streams and cuts at 60 s).
834
+ timeout: `${MCP_AUTH_PROXY_TIMEOUT_SECONDS}s`,
835
+ // Public: MCP clients reach it directly; until #1331 the staging host's
836
+ // domain mapping (#650) still points at this service too, and the proxy
837
+ // forwards that traffic to the data gateway (INTERIM_FORWARD_ORIGIN_ENV).
691
838
  ingress: "INGRESS_TRAFFIC_ALL",
692
839
  contractEnv: [
693
- ...gatewayEnvContract(envContractInputs),
694
- ...remoteMcpEnv.gateway,
695
- ...poolBudget.env.gateway,
840
+ ...mcpAuthProxyEnvContract({ provider: "gcp-firebase", env }),
841
+ ...remoteMcpEnv["mcp-auth-proxy"],
842
+ // FIXME(issue #1331): goes with the staging cutover.
843
+ { name: INTERIM_FORWARD_ORIGIN_ENV, value: gatewaySvc.uri },
696
844
  ],
697
845
  });
698
846
 
@@ -751,7 +899,7 @@ const adminSvc = cloudRunService({
751
899
  // authentication remain the real security boundary, exactly as
752
900
  // designed.
753
901
  new gcp.cloudrunv2.ServiceIamMember(
754
- `${namePrefix}-gateway-public-invoker`,
902
+ `${namePrefix}-data-gateway-public-invoker`,
755
903
  {
756
904
  project,
757
905
  location: region,
@@ -762,6 +910,21 @@ new gcp.cloudrunv2.ServiceIamMember(
762
910
  opts,
763
911
  );
764
912
 
913
+ // #1328 — the MCP-Auth-Proxy: MCP clients sign in and call it without a
914
+ // Google identity of their own. The binding the service had as the gateway,
915
+ // under its old logical name, so it never lapses.
916
+ new gcp.cloudrunv2.ServiceIamMember(
917
+ `${namePrefix}-gateway-public-invoker`,
918
+ {
919
+ project,
920
+ location: region,
921
+ name: mcpAuthProxySvc.name,
922
+ role: "roles/run.invoker",
923
+ member: "allUsers",
924
+ },
925
+ opts,
926
+ );
927
+
765
928
  // v0.6.5 — `run.services.get` on the gateway for the admin runtime
766
929
  // SA. Required at DEPLOY TIME (not runtime): when the static
767
930
  // publisher creates a Firebase Hosting version with a Cloud Run
@@ -781,7 +944,7 @@ new gcp.cloudrunv2.ServiceIamMember(
781
944
  // explicit cross-service grant because the admin's runSa is a
782
945
  // DIFFERENT-resource caller from the gateway's perspective.
783
946
  new gcp.cloudrunv2.ServiceIamMember(
784
- `${namePrefix}-gateway-viewer-for-admin-sa`,
947
+ `${namePrefix}-data-gateway-viewer-for-admin-sa`,
785
948
  {
786
949
  project,
787
950
  location: region,
@@ -1001,6 +1164,18 @@ new gcp.iap.WebCloudRunServiceIamMember(
1001
1164
  // (packages/provisioning/src/capture-job-setup.ts), for the same reason as
1002
1165
  // the audit job above.
1003
1166
 
1167
+ // =========================================================================
1168
+ // Scheduler tick — deliberately NOT here
1169
+ // =========================================================================
1170
+ //
1171
+ // #1140 — one Cloud Scheduler job starts a Cloud Workflows execution every
1172
+ // `schedulerIntervalMinutes` (1–15, default 5); the workflow signs an IAP
1173
+ // credential as the scheduler's own service account and POSTs the admin's
1174
+ // /api/internal/scheduler/tick, where the admin runs its periodic work and
1175
+ // plugin workers from its due-table. The CLI owns the account, its grants,
1176
+ // the workflow and the job (packages/provisioning/src/scheduler-setup.ts),
1177
+ // for the same reason as the audit job above.
1178
+
1004
1179
  // =========================================================================
1005
1180
  // Operator access — deliberately NOT here
1006
1181
  // =========================================================================
@@ -1085,6 +1260,7 @@ const out: CloudAdapterOutputs = {
1085
1260
  // Password-less — the password lives only in Secret Manager.
1086
1261
  adminDatabaseUrl: dbUrls.admin as unknown as string,
1087
1262
  publicDatabaseUrl: dbUrls.public as unknown as string,
1263
+ publicStagingDatabaseUrl: dbUrls.publicStaging as unknown as string,
1088
1264
  mediaStorageUrl: pulumi.interpolate`gs://${mediaBucket.name}` as unknown as string,
1089
1265
  // Firebase Hosting serves media via its own CDN; we proxy through
1090
1266
  // the Firebase site's apex domain for canonical URLs.
@@ -1101,6 +1277,7 @@ const out: CloudAdapterOutputs = {
1101
1277
 
1102
1278
  export const adminDatabaseUrlOut = out.adminDatabaseUrl;
1103
1279
  export const publicDatabaseUrlOut = out.publicDatabaseUrl;
1280
+ export const publicStagingDatabaseUrlOut = out.publicStagingDatabaseUrl;
1104
1281
  export const mediaStorageUrlOut = out.mediaStorageUrl;
1105
1282
  export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
1106
1283
  export const bootstrapUrlOut = out.bootstrapUrl;
@@ -1110,6 +1287,7 @@ export const providerOut = out.provider;
1110
1287
  export const environmentOut = out.environment;
1111
1288
  export const adminCloudRunUrlOut = adminSvc.uri;
1112
1289
  export const gatewayCloudRunUrlOut = gatewaySvc.uri;
1290
+ export const mcpAuthProxyCloudRunUrlOut = mcpAuthProxySvc.uri;
1113
1291
  export const adminDomainOut = adminDomain;
1114
1292
  export const firebaseSiteIdOut = firebaseSiteId;
1115
1293
  export const bootstrapTokenExpiresAtOut = tokenInfo.expiresAt;
@@ -29,3 +29,6 @@ config:
29
29
  type: string
30
30
  default: "2Gi"
31
31
  description: Memory reserved for the admin container (it runs the Lighthouse quality audit).
32
+ caelo-self-hosted:schedulerIntervalMinutes:
33
+ type: integer
34
+ description: Minutes between scheduler ticks (1-15, default 5; #1140). On an install `cms-provision init` set up, `.caelo/config.json` records it and this key must match.
@@ -20,10 +20,11 @@ pulumi config set caelo-self-hosted:ownerEmail me@example.com
20
20
  # verified and recorded in .caelo/release.json:
21
21
  pulumi config set caelo-self-hosted:adminImage ghcr.io/caelo-cms/admin@sha256:<digest>
22
22
  pulumi config set caelo-self-hosted:gatewayImage ghcr.io/caelo-cms/gateway@sha256:<digest>
23
+ pulumi config set caelo-self-hosted:mcpAuthProxyImage ghcr.io/caelo-cms/mcp-auth-proxy@sha256:<digest>
23
24
  pulumi up
24
25
  ```
25
26
 
26
- Pulumi mints the postgres and role passwords and the KEK, generates `docker-compose.yml` + `Caddyfile` + `bootstrap.sh` + `pending-token.json` under `.caelo/`, starts Postgres, runs the migrations and runs `docker compose up -d`. Point DNS for `<domain>`, `admin.<domain>` and `staging.<domain>` at the host. After the run:
27
+ Pulumi mints the postgres and role passwords and the KEK, generates `docker-compose.yml` + `Caddyfile` + `bootstrap.sh` + `pending-token.json` under `.caelo/`, starts Postgres, runs the migrations and runs `docker compose up -d`. Point DNS for `<domain>`, `admin.<domain>` and `staging.<domain>` at the host. The MCP sign-in service (`caelo-mcp-auth-proxy`) runs without a public host: remote MCP needs an identity proxy in front of the admin, which self-hosted installs do not have yet, so it only answers 404 and Caddy does not expose it. After the run:
27
28
 
28
29
  ```bash
29
30
  pulumi stack output bootstrapUrl
@@ -32,6 +33,10 @@ pulumi stack output bootstrapUrl
32
33
 
33
34
  Open that URL once Caddy has issued the cert (~30 s).
34
35
 
36
+ ## Scheduler tick
37
+
38
+ The Compose file runs a `caelo-scheduler` container (the admin image, no database access, only the internal secret it signs with) that POSTs the admin's `/api/internal/scheduler/tick` every `schedulerIntervalMinutes` (#1140; 1–15, default 5). The admin runs its periodic work and plugin workers on that tick. Set the interval with `cms-provision init --scheduler-interval-minutes 2` (recorded in `.caelo/config.json`, which every CLI render reads) or, on an install without a `config.json`, with `pulumi config set caelo-self-hosted:schedulerIntervalMinutes 2`. The stack reads `.caelo/config.json` too, so a `pulumi up` keeps the recorded interval; a Pulumi key that disagrees with it is refused. An install from before the scheduler gets the container on its next `upgrade`, which rewrites the Compose file.
39
+
35
40
  ## Connection budget
36
41
 
37
42
  The generated Compose file starts Postgres with `max_connections=100` and sizes every pool from it (#1254, docs/dev/connection-budget.md): the admin gets a cms_admin pool of 77 and a cms_public pool of 4, the gateway a pool of 2, and static builds 2 per pool.
@@ -29,11 +29,17 @@ import { generateCaddyfile, selfHostedAdminHost, selfHostedSeedDomains } from ".
29
29
  import {
30
30
  COMPOSE_ADMIN_UPSTREAM,
31
31
  COMPOSE_GATEWAY_UPSTREAM,
32
+ COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
32
33
  composeProjectName,
33
34
  generateDockerCompose,
34
35
  SITE_OUTPUT_ROOT,
35
36
  } from "../../dist/compose.js";
37
+ import { MIGRATION_TARGETS } from "../../dist/migration-targets.js";
36
38
  import { selfHostedBootstrapScript } from "../../dist/self-hosted-roles.js";
39
+ import {
40
+ recordedSchedulerIntervalMinutes,
41
+ selfHostedSchedulerIntervalMinutes,
42
+ } from "../../dist/self-hosted-scheduler-interval.js";
37
43
 
38
44
  const cfg = new pulumi.Config();
39
45
  const domain = cfg.require("domain");
@@ -43,12 +49,26 @@ const ownerEmail = cfg.require("ownerEmail");
43
49
  const caeloDir = cfg.get("caeloDir") ?? "./.caelo";
44
50
  // #553 — same knob as every adapter; undefined = stack-contract default.
45
51
  const adminMemory = cfg.get("adminMemory");
52
+ // #1140 — how often the scheduler container calls the admin's tick (1–15
53
+ // minutes, default 5). `init --scheduler-interval-minutes` records it in
54
+ // <caeloDir>/config.json, which the CLI renders from; the stack reads the
55
+ // same record so a `pulumi up` does not put it back to the default, and
56
+ // refuses a Pulumi key that disagrees with it.
57
+ const caeloConfigPath = resolve(caeloDir, "config.json");
58
+ const schedulerIntervalMinutes = selfHostedSchedulerIntervalMinutes(
59
+ cfg.getNumber("schedulerIntervalMinutes"),
60
+ recordedSchedulerIntervalMinutes(caeloConfigPath),
61
+ caeloConfigPath,
62
+ );
46
63
  // #696 — the prebuilt, cosign-verified release images, pinned by digest
47
64
  // (`ghcr.io/caelo-cms/<svc>@sha256:…`; the generator refuses anything else).
48
65
  // `cms-provision init` resolves + verifies them and records them in
49
66
  // .caelo/release.json; copy those values here.
50
67
  const adminImage = cfg.require("adminImage");
51
68
  const gatewayImage = cfg.require("gatewayImage");
69
+ // #1328 — the MCP-Auth-Proxy (remote MCP's sign-in service, its own
70
+ // container and its own host `mcp.<domain>`).
71
+ const mcpAuthProxyImage = cfg.require("mcpAuthProxyImage");
52
72
 
53
73
  const composePath = resolve(caeloDir, "docker-compose.yml");
54
74
  const caddyPath = resolve(caeloDir, "Caddyfile");
@@ -65,22 +85,36 @@ const postgresPassword = pulumi.secret(randomHex(32));
65
85
  // public_role one (#635: no cms_admin login).
66
86
  const adminRolePassword = pulumi.secret(randomHex(32));
67
87
  const publicRolePassword = pulumi.secret(randomHex(32));
88
+ // #1329 — the staging gateway's own login on cms_public_staging.
89
+ const publicStagingRolePassword = pulumi.secret(randomHex(32));
68
90
  // P18 — the KEK that encrypts stored provider keys.
69
91
  const caeloSecretKek = pulumi.secret(randomHex(32));
70
92
 
71
93
  const composeYaml = pulumi
72
- .all([postgresPassword, adminRolePassword, publicRolePassword, caeloSecretKek])
73
- .apply(([pgPw, adminPw, publicPw, kek]) =>
94
+ .all([
95
+ postgresPassword,
96
+ adminRolePassword,
97
+ publicRolePassword,
98
+ publicStagingRolePassword,
99
+ caeloSecretKek,
100
+ ])
101
+ .apply(([pgPw, adminPw, publicPw, publicStagingPw, kek]) =>
74
102
  generateDockerCompose({
75
103
  domain,
76
104
  postgresPassword: pgPw,
77
- rolePasswords: { admin: adminPw, public: publicPw },
105
+ rolePasswords: { admin: adminPw, public: publicPw, publicStaging: publicStagingPw },
78
106
  caeloSecretKek: kek,
79
107
  anthropicApiKey: process.env.ANTHROPIC_API_KEY,
80
108
  resendApiKey: process.env.RESEND_API_KEY,
81
109
  diskSize: "20Gi",
82
110
  ...(adminMemory ? { adminMemory } : {}),
83
- images: { mode: "release", admin: adminImage, gateway: gatewayImage },
111
+ ...(schedulerIntervalMinutes !== undefined ? { schedulerIntervalMinutes } : {}),
112
+ images: {
113
+ mode: "release",
114
+ admin: adminImage,
115
+ gateway: gatewayImage,
116
+ "mcp-auth-proxy": mcpAuthProxyImage,
117
+ },
84
118
  ports: { http: 80, https: 443 },
85
119
  project: composeProjectName(domain),
86
120
  }),
@@ -95,6 +129,7 @@ const caddyConf = generateCaddyfile({
95
129
  stagingSiteRoot: `${SITE_OUTPUT_ROOT}/output/staging/current`,
96
130
  adminUpstream: COMPOSE_ADMIN_UPSTREAM,
97
131
  gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
132
+ mcpAuthProxyUpstream: COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
98
133
  domains: selfHostedSeedDomains(domain, stagingHost),
99
134
  });
100
135
  const bootstrapScript = selfHostedBootstrapScript({ agent: false });
@@ -134,7 +169,7 @@ const composeUp = new local.Command(
134
169
  // install's admin image, then everything else.
135
170
  create: pulumi.interpolate`set -e
136
171
  docker compose -f ${composePath} up -d --wait postgres
137
- for target in admin public; do
172
+ for target in ${MIGRATION_TARGETS.join(" ")}; do
138
173
  docker compose -f ${composePath} run --rm --no-deps -T caelo-admin bun --bun /app/packages/migrations/src/migrate.ts "$target"
139
174
  done
140
175
  docker compose -f ${composePath} up -d --remove-orphans`,