@caelo-cms/provisioning 0.11.8 → 0.11.10

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 (261) hide show
  1. package/dist/admin-domain.d.ts +10 -1
  2. package/dist/admin-domain.d.ts.map +1 -1
  3. package/dist/admin-domain.js +13 -3
  4. package/dist/admin-domain.js.map +1 -1
  5. package/dist/adopt-existing.d.ts +6 -1
  6. package/dist/adopt-existing.d.ts.map +1 -1
  7. package/dist/adopt-existing.js +86 -0
  8. package/dist/adopt-existing.js.map +1 -1
  9. package/dist/agent/command.d.ts +20 -0
  10. package/dist/agent/command.d.ts.map +1 -1
  11. package/dist/agent/command.js +32 -9
  12. package/dist/agent/command.js.map +1 -1
  13. package/dist/agent/run.d.ts +2 -0
  14. package/dist/agent/run.d.ts.map +1 -1
  15. package/dist/agent/run.js +5 -1
  16. package/dist/agent/run.js.map +1 -1
  17. package/dist/agent-db.js +65 -3
  18. package/dist/aws-task-roles.d.ts +4 -2
  19. package/dist/aws-task-roles.d.ts.map +1 -1
  20. package/dist/aws-task-roles.js +4 -1
  21. package/dist/aws-task-roles.js.map +1 -1
  22. package/dist/caddy.d.ts +21 -4
  23. package/dist/caddy.d.ts.map +1 -1
  24. package/dist/caddy.js +27 -3
  25. package/dist/caddy.js.map +1 -1
  26. package/dist/cli.js +12 -3
  27. package/dist/cli.js.map +1 -1
  28. package/dist/cloudsql-tls.d.ts +63 -0
  29. package/dist/cloudsql-tls.d.ts.map +1 -0
  30. package/dist/cloudsql-tls.js +289 -0
  31. package/dist/cloudsql-tls.js.map +1 -0
  32. package/dist/compose.d.ts +29 -6
  33. package/dist/compose.d.ts.map +1 -1
  34. package/dist/compose.js +95 -23
  35. package/dist/compose.js.map +1 -1
  36. package/dist/converge-checkpoint.d.ts +1 -0
  37. package/dist/converge-checkpoint.d.ts.map +1 -1
  38. package/dist/converge-checkpoint.js +8 -1
  39. package/dist/converge-checkpoint.js.map +1 -1
  40. package/dist/converge-job-gcp.d.ts.map +1 -1
  41. package/dist/converge-job-gcp.js +2 -11
  42. package/dist/converge-job-gcp.js.map +1 -1
  43. package/dist/deployed-release.d.ts +4 -1
  44. package/dist/deployed-release.d.ts.map +1 -1
  45. package/dist/deployed-release.js +18 -8
  46. package/dist/deployed-release.js.map +1 -1
  47. package/dist/gcp-names.d.ts +21 -0
  48. package/dist/gcp-names.d.ts.map +1 -1
  49. package/dist/gcp-names.js +24 -0
  50. package/dist/gcp-names.js.map +1 -1
  51. package/dist/gcp-url-map.d.ts.map +1 -1
  52. package/dist/gcp-url-map.js +6 -2
  53. package/dist/gcp-url-map.js.map +1 -1
  54. package/dist/index.d.ts +1 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/infra-contract.d.ts +1 -1
  59. package/dist/infra-contract.d.ts.map +1 -1
  60. package/dist/infra-contract.js +24 -1
  61. package/dist/infra-contract.js.map +1 -1
  62. package/dist/install-state.d.ts +6 -0
  63. package/dist/install-state.d.ts.map +1 -1
  64. package/dist/install-state.js +17 -4
  65. package/dist/install-state.js.map +1 -1
  66. package/dist/lifecycle.d.ts.map +1 -1
  67. package/dist/lifecycle.js +17 -8
  68. package/dist/lifecycle.js.map +1 -1
  69. package/dist/mcp-auth-proxy-setup.d.ts +48 -0
  70. package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
  71. package/dist/mcp-auth-proxy-setup.js +280 -0
  72. package/dist/mcp-auth-proxy-setup.js.map +1 -0
  73. package/dist/release-manifest.d.ts +25 -1
  74. package/dist/release-manifest.d.ts.map +1 -1
  75. package/dist/release-manifest.js +36 -3
  76. package/dist/release-manifest.js.map +1 -1
  77. package/dist/remote-mcp.d.ts +55 -18
  78. package/dist/remote-mcp.d.ts.map +1 -1
  79. package/dist/remote-mcp.js +307 -77
  80. package/dist/remote-mcp.js.map +1 -1
  81. package/dist/runtime-secrets.d.ts +9 -0
  82. package/dist/runtime-secrets.d.ts.map +1 -1
  83. package/dist/runtime-secrets.js +21 -10
  84. package/dist/runtime-secrets.js.map +1 -1
  85. package/dist/seal-key-rotation.d.ts +7 -7
  86. package/dist/seal-key-rotation.d.ts.map +1 -1
  87. package/dist/seal-key-rotation.js +22 -22
  88. package/dist/seal-key-rotation.js.map +1 -1
  89. package/dist/self-hosted-db-tls.d.ts +37 -0
  90. package/dist/self-hosted-db-tls.d.ts.map +1 -0
  91. package/dist/self-hosted-db-tls.js +220 -0
  92. package/dist/self-hosted-db-tls.js.map +1 -0
  93. package/dist/self-hosted-release.d.ts +3 -1
  94. package/dist/self-hosted-release.d.ts.map +1 -1
  95. package/dist/self-hosted-release.js +33 -9
  96. package/dist/self-hosted-release.js.map +1 -1
  97. package/dist/self-hosted-roles.d.ts +34 -0
  98. package/dist/self-hosted-roles.d.ts.map +1 -1
  99. package/dist/self-hosted-roles.js +52 -0
  100. package/dist/self-hosted-roles.js.map +1 -1
  101. package/dist/self-hosted-start.d.ts +0 -21
  102. package/dist/self-hosted-start.d.ts.map +1 -1
  103. package/dist/self-hosted-start.js +39 -1
  104. package/dist/self-hosted-start.js.map +1 -1
  105. package/dist/self-hosted-upgrade.d.ts +15 -6
  106. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  107. package/dist/self-hosted-upgrade.js +72 -23
  108. package/dist/self-hosted-upgrade.js.map +1 -1
  109. package/dist/stack-contract.d.ts +170 -25
  110. package/dist/stack-contract.d.ts.map +1 -1
  111. package/dist/stack-contract.js +314 -37
  112. package/dist/stack-contract.js.map +1 -1
  113. package/dist/stack-converge.d.ts +31 -0
  114. package/dist/stack-converge.d.ts.map +1 -1
  115. package/dist/stack-converge.js +71 -30
  116. package/dist/stack-converge.js.map +1 -1
  117. package/dist/upgrade/app-roll.d.ts +25 -4
  118. package/dist/upgrade/app-roll.d.ts.map +1 -1
  119. package/dist/upgrade/app-roll.js +63 -17
  120. package/dist/upgrade/app-roll.js.map +1 -1
  121. package/dist/upgrade/converge.d.ts.map +1 -1
  122. package/dist/upgrade/converge.js +99 -12
  123. package/dist/upgrade/converge.js.map +1 -1
  124. package/dist/upgrade/health.d.ts +31 -7
  125. package/dist/upgrade/health.d.ts.map +1 -1
  126. package/dist/upgrade/health.js +41 -17
  127. package/dist/upgrade/health.js.map +1 -1
  128. package/dist/upgrade/image-forms.d.ts +1 -1
  129. package/dist/upgrade/image-forms.d.ts.map +1 -1
  130. package/dist/upgrade/image-forms.js +24 -7
  131. package/dist/upgrade/image-forms.js.map +1 -1
  132. package/dist/upgrade/release-images.d.ts +12 -3
  133. package/dist/upgrade/release-images.d.ts.map +1 -1
  134. package/dist/upgrade/release-images.js +13 -4
  135. package/dist/upgrade/release-images.js.map +1 -1
  136. package/dist/upgrade/upgrade.d.ts +13 -3
  137. package/dist/upgrade/upgrade.d.ts.map +1 -1
  138. package/dist/upgrade/upgrade.js +34 -2
  139. package/dist/upgrade/upgrade.js.map +1 -1
  140. package/dist/upgrader-gcp.d.ts +12 -0
  141. package/dist/upgrader-gcp.d.ts.map +1 -1
  142. package/dist/upgrader-gcp.js +49 -1
  143. package/dist/upgrader-gcp.js.map +1 -1
  144. package/dist/upgrader-iam-preflight.d.ts.map +1 -1
  145. package/dist/upgrader-iam-preflight.js +6 -1
  146. package/dist/upgrader-iam-preflight.js.map +1 -1
  147. package/dist/upgrader-job.d.ts +1 -1
  148. package/dist/upgrader-job.d.ts.map +1 -1
  149. package/dist/upgrader-job.js +20 -14
  150. package/dist/upgrader-job.js.map +1 -1
  151. package/dist/upgrader-records.d.ts +1 -1
  152. package/dist/upgrader-records.d.ts.map +1 -1
  153. package/dist/upgrader-records.js +26 -7
  154. package/dist/upgrader-records.js.map +1 -1
  155. package/dist/upgrader-setup.d.ts.map +1 -1
  156. package/dist/upgrader-setup.js +10 -5
  157. package/dist/upgrader-setup.js.map +1 -1
  158. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  159. package/dist/wizards/gcp-cost.js +22 -7
  160. package/dist/wizards/gcp-cost.js.map +1 -1
  161. package/dist/wizards/gcp.d.ts.map +1 -1
  162. package/dist/wizards/gcp.js +43 -17
  163. package/dist/wizards/gcp.js.map +1 -1
  164. package/package.json +2 -2
  165. package/src/admin-domain.test.ts +9 -8
  166. package/src/admin-domain.ts +13 -3
  167. package/src/admin-memory.test.ts +1 -0
  168. package/src/adopt-existing.test.ts +39 -0
  169. package/src/adopt-existing.ts +83 -1
  170. package/src/agent/command.test.ts +49 -1
  171. package/src/agent/command.ts +36 -9
  172. package/src/agent/run.test.ts +19 -0
  173. package/src/agent/run.ts +11 -2
  174. package/src/aws-task-roles.ts +5 -2
  175. package/src/caddy-redirects.docker.test.ts +2 -0
  176. package/src/caddy-redirects.test.ts +3 -0
  177. package/src/caddy-validate.test.ts +7 -1
  178. package/src/caddy.test.ts +39 -1
  179. package/src/caddy.ts +36 -4
  180. package/src/checkpoint-secrets.test.ts +14 -6
  181. package/src/cli.ts +12 -1
  182. package/src/cloudsql-tls.test.ts +225 -0
  183. package/src/cloudsql-tls.ts +352 -0
  184. package/src/compose.ts +125 -24
  185. package/src/converge-checkpoint.ts +8 -1
  186. package/src/converge-job-gcp.ts +2 -11
  187. package/src/database-tls-builders.test.ts +55 -0
  188. package/src/deployed-release.test.ts +104 -40
  189. package/src/deployed-release.ts +30 -9
  190. package/src/gateway-env-isolation.test.ts +10 -5
  191. package/src/gcp-names.ts +31 -0
  192. package/src/gcp-url-map.test.ts +6 -8
  193. package/src/gcp-url-map.ts +6 -2
  194. package/src/index.test.ts +7 -1
  195. package/src/index.ts +1 -0
  196. package/src/infra-contract.test.ts +5 -3
  197. package/src/infra-contract.ts +24 -1
  198. package/src/install-state.ts +26 -4
  199. package/src/lifecycle.ts +20 -11
  200. package/src/mcp-auth-proxy-setup.test.ts +305 -0
  201. package/src/mcp-auth-proxy-setup.ts +374 -0
  202. package/src/mcp-iap-stacks.test.ts +1 -1
  203. package/src/media-volume.test.ts +1 -1
  204. package/src/operator-access-stacks.test.ts +1 -1
  205. package/src/operator-access.test.ts +5 -4
  206. package/src/pool-budget-stacks.test.ts +1 -1
  207. package/src/release-manifest.test.ts +25 -0
  208. package/src/release-manifest.ts +59 -4
  209. package/src/remote-mcp-stack-config.test.ts +19 -9
  210. package/src/remote-mcp.test.ts +252 -57
  211. package/src/remote-mcp.ts +336 -77
  212. package/src/runtime-secrets.test.ts +20 -7
  213. package/src/runtime-secrets.ts +50 -9
  214. package/src/seal-key-rotation.test.ts +2 -2
  215. package/src/seal-key-rotation.ts +25 -25
  216. package/src/self-hosted-compose.test.ts +108 -5
  217. package/src/self-hosted-db-tls.test.ts +111 -0
  218. package/src/self-hosted-db-tls.ts +287 -0
  219. package/src/self-hosted-release-resolve.test.ts +71 -0
  220. package/src/self-hosted-release.ts +41 -11
  221. package/src/self-hosted-roles.test.ts +54 -0
  222. package/src/self-hosted-roles.ts +58 -0
  223. package/src/self-hosted-start.test.ts +34 -2
  224. package/src/self-hosted-start.ts +20 -2
  225. package/src/self-hosted-upgrade.test.ts +103 -14
  226. package/src/self-hosted-upgrade.ts +115 -29
  227. package/src/stack-contract-parity.test.ts +79 -12
  228. package/src/stack-contract.test.ts +169 -29
  229. package/src/stack-contract.ts +362 -42
  230. package/src/stack-converge-secrets.test.ts +26 -13
  231. package/src/stack-converge.test.ts +199 -26
  232. package/src/stack-converge.ts +92 -30
  233. package/src/state-store.test.ts +2 -1
  234. package/src/upgrade/app-roll.ts +97 -19
  235. package/src/upgrade/converge.ts +120 -11
  236. package/src/upgrade/health.test.ts +63 -25
  237. package/src/upgrade/health.ts +54 -22
  238. package/src/upgrade/image-forms.test.ts +34 -0
  239. package/src/upgrade/image-forms.ts +23 -8
  240. package/src/upgrade/release-images.test.ts +5 -1
  241. package/src/upgrade/release-images.ts +22 -6
  242. package/src/upgrade/upgrade.test.ts +353 -44
  243. package/src/upgrade/upgrade.ts +55 -6
  244. package/src/upgrader-gcp.ts +65 -3
  245. package/src/upgrader-iam-preflight.test.ts +19 -0
  246. package/src/upgrader-iam-preflight.ts +6 -0
  247. package/src/upgrader-job.test.ts +101 -27
  248. package/src/upgrader-job.ts +27 -14
  249. package/src/upgrader-records.test.ts +97 -12
  250. package/src/upgrader-records.ts +28 -11
  251. package/src/upgrader-setup.test.ts +2 -0
  252. package/src/upgrader-setup.ts +10 -4
  253. package/src/wizards/gcp-cost.test.ts +50 -0
  254. package/src/wizards/gcp-cost.ts +24 -7
  255. package/src/wizards/gcp.ts +55 -14
  256. package/stacks/aws/index.ts +32 -14
  257. package/stacks/azure/index.ts +57 -18
  258. package/stacks/gcp/index.ts +83 -23
  259. package/stacks/gcp-firebase/index.ts +162 -33
  260. package/stacks/self-hosted/README.md +2 -1
  261. package/stacks/self-hosted/index.ts +11 -1
@@ -27,7 +27,11 @@
27
27
  * is to wire those env vars to AWS-native resources.
28
28
  */
29
29
 
30
- import { cloudFrontRedirectFunctionCode } from "@caelo-cms/shared";
30
+ import {
31
+ AWS_RDS_CA_BUNDLE,
32
+ cloudFrontRedirectFunctionCode,
33
+ verifyFullQuery,
34
+ } from "@caelo-cms/shared";
31
35
  import * as aws from "@pulumi/aws";
32
36
  import * as awsx from "@pulumi/awsx";
33
37
  import * as pulumi from "@pulumi/pulumi";
@@ -171,9 +175,15 @@ const dbParameters = new aws.rds.ParameterGroup(`${namePrefix}-pg-params`, {
171
175
  family: "postgres16",
172
176
  parameters: [
173
177
  { name: "max_connections", value: String(maxConnections), applyMethod: "pending-reboot" },
178
+ // #1338 — refuse plaintext; every client connects with verify-full.
179
+ { name: "rds.force_ssl", value: "1", applyMethod: "pending-reboot" },
174
180
  ],
175
181
  });
176
182
 
183
+ // #1338 — verify-full against Amazon's RDS CA bundle (shipped in every image,
184
+ // shared database-tls.ts); the RDS certificate names the endpoint host.
185
+ const RDS_TLS = verifyFullQuery(AWS_RDS_CA_BUNDLE);
186
+
177
187
  const db = new aws.rds.Instance(`${namePrefix}-pg`, {
178
188
  engine: "postgres",
179
189
  engineVersion: "16.4",
@@ -196,14 +206,12 @@ const db = new aws.rds.Instance(`${namePrefix}-pg`, {
196
206
  const adminDatabaseUrl = pulumi
197
207
  .all([db.address, db.port, postgresPassword])
198
208
  .apply(
199
- ([host, port, pw]) =>
200
- `postgresql://caelo_admin:${pw}@${host}:${port}/cms_admin?sslmode=require`,
209
+ ([host, port, pw]) => `postgresql://caelo_admin:${pw}@${host}:${port}/cms_admin?${RDS_TLS}`,
201
210
  );
202
211
  const publicDatabaseUrl = pulumi
203
212
  .all([db.address, db.port, publicRolePassword])
204
213
  .apply(
205
- ([host, port, pw]) =>
206
- `postgresql://public_role:${pw}@${host}:${port}/cms_public?sslmode=require`,
214
+ ([host, port, pw]) => `postgresql://public_role:${pw}@${host}:${port}/cms_public?${RDS_TLS}`,
207
215
  );
208
216
 
209
217
  // === 4. S3 buckets ===
@@ -450,9 +458,12 @@ const cluster = new aws.ecs.Cluster(`${namePrefix}-ecs`, {
450
458
  settings: [{ name: "containerInsights", value: "enabled" }],
451
459
  });
452
460
 
453
- // #806 — no remote MCP on AWS: it rides Google IAP (gcp stacks only). The
454
- // gateway task definition, once it exists, sets none of its env, so /api/mcp/*
455
- // answers 503 remote_mcp_not_configured and the admin reports it unavailable.
461
+ // #1328 — the MCP-Auth-Proxy (remote MCP's sign-in service) is a service of
462
+ // its own on every provider, with its own task role that may read NO secret.
463
+ // Remote MCP rides Google IAP (gcp stacks only), so on AWS it would run with
464
+ // CAELO_REMOTE_MCP=unavailable and answer a fixed 404. Like the admin and
465
+ // gateway it has no task definition yet: the service lands with #749, in the
466
+ // same shape (own origin, zero minimum tasks, no database env).
456
467
  //
457
468
  // One task role per service (#635): the gateway's may read only the
458
469
  // public_role password, never a secret that unlocks a cms_admin login
@@ -482,18 +493,23 @@ function taskRole(service: AwsTaskService): aws.iam.Role {
482
493
  ],
483
494
  }),
484
495
  });
485
- new aws.iam.RolePolicy(`${prefix}-task-secrets`, {
486
- role: role.id,
487
- policy: pulumi
488
- .all(AWS_SERVICE_SECRETS[service].map((name) => secretArns[name]))
489
- .apply(secretsReadPolicy),
490
- });
496
+ // A service that reads no secret gets no secrets policy at all (an empty
497
+ // policy is malformed, see secretsReadPolicy).
498
+ if (AWS_SERVICE_SECRETS[service].length > 0) {
499
+ new aws.iam.RolePolicy(`${prefix}-task-secrets`, {
500
+ role: role.id,
501
+ policy: pulumi
502
+ .all(AWS_SERVICE_SECRETS[service].map((name) => secretArns[name]))
503
+ .apply(secretsReadPolicy),
504
+ });
505
+ }
491
506
  return role;
492
507
  }
493
508
 
494
509
  const taskRoles: Record<AwsTaskService, aws.iam.Role> = {
495
510
  admin: taskRole("admin"),
496
511
  gateway: taskRole("gateway"),
512
+ "mcp-auth-proxy": taskRole("mcp-auth-proxy"),
497
513
  };
498
514
 
499
515
  const execRole = new aws.iam.Role(`${namePrefix}-exec-role`, {
@@ -563,6 +579,8 @@ export const rdsAddressOut = db.address;
563
579
  // The task roles the admin-side and gateway task definitions run as.
564
580
  export const adminTaskRoleArnOut = taskRoles.admin.arn;
565
581
  export const gatewayTaskRoleArnOut = taskRoles.gateway.arn;
582
+ // #1328 — the role the MCP-Auth-Proxy task definition will run as (#749).
583
+ export const mcpAuthProxyTaskRoleArnOut = taskRoles["mcp-auth-proxy"].arn;
566
584
  // #1254 — the pool env the admin and gateway task definitions set.
567
585
  export const poolBudgetEnvOut = poolBudget.env;
568
586
 
@@ -13,7 +13,8 @@
13
13
  * 4. Azure Front Door Standard serving the static website on the primary
14
14
  * domain with a managed certificate — files served as files, no compute
15
15
  * on the request path (CLAUDE.md §11.B Tier 1).
16
- * 5. Azure Container Apps (admin/gateway/orchestrator/runner),
16
+ * 5. Azure Container Apps (admin/gateway/orchestrator/runner, and the
17
+ * MCP-Auth-Proxy without any secret, #1328),
17
18
  * mounted with Key Vault secrets via managed identity.
18
19
  * 6. Key Vault entries (postgres-password, csrf-secret, cookie-secret,
19
20
  * anthropic-api-key, resend-api-key).
@@ -34,6 +35,7 @@
34
35
  * env vars wired by this stack.
35
36
  */
36
37
 
38
+ import { verifyFullQuery } from "@caelo-cms/shared";
37
39
  import * as azure from "@pulumi/azure-native";
38
40
  import * as pulumi from "@pulumi/pulumi";
39
41
  import type { CloudAdapterOutputs, DnsRecord } from "../../dist/adapter.js";
@@ -44,6 +46,9 @@ import {
44
46
  ADMIN_MEMORY_DEFAULT,
45
47
  azureContainerAppResources,
46
48
  GATEWAY_TRUSTED_PROXY_HOPS,
49
+ MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS,
50
+ REMOTE_MCP_MODE,
51
+ REMOTE_MCP_MODE_ENV,
47
52
  TRUSTED_PROXY_HOPS_ENV,
48
53
  } from "../../dist/stack-contract.js";
49
54
  import {
@@ -191,18 +196,23 @@ const _pgPublic = new azure.dbforpostgresql.Database(`${namePrefix}-cms-public-d
191
196
  charset: "UTF8",
192
197
  });
193
198
 
199
+ // #1338 — verify-full to the server's FQDN. Flexible Server's certificate chains
200
+ // to public roots (DigiCert Global Root G2, Microsoft RSA Root CA 2017) that
201
+ // Bun trusts out of the box, so no sslrootcert (shared database-tls.ts).
202
+ const AZURE_TLS = verifyFullQuery();
203
+
194
204
  const adminDatabaseUrl = pulumi
195
205
  .all([pgServer.fullyQualifiedDomainName, postgresPassword])
196
- .apply(([host, pw]) => `postgresql://caelo_admin:${pw}@${host}:5432/cms_admin?sslmode=require`);
206
+ .apply(([host, pw]) => `postgresql://caelo_admin:${pw}@${host}:5432/cms_admin?${AZURE_TLS}`);
197
207
  // What the admin connects to cms_public with (DDL, migrations, moderation).
198
208
  const publicAdminDatabaseUrl = pulumi
199
209
  .all([pgServer.fullyQualifiedDomainName, postgresPassword])
200
- .apply(([host, pw]) => `postgresql://caelo_admin:${pw}@${host}:5432/cms_public?sslmode=require`);
210
+ .apply(([host, pw]) => `postgresql://caelo_admin:${pw}@${host}:5432/cms_public?${AZURE_TLS}`);
201
211
  // The gateway's only login (#613, #635): public_role on cms_public. It has
202
212
  // no cms_admin login at all (CLAUDE.md §2).
203
213
  const publicDatabaseUrl = pulumi
204
214
  .all([pgServer.fullyQualifiedDomainName, publicRolePassword])
205
- .apply(([host, pw]) => `postgresql://public_role:${pw}@${host}:5432/cms_public?sslmode=require`);
215
+ .apply(([host, pw]) => `postgresql://public_role:${pw}@${host}:5432/cms_public?${AZURE_TLS}`);
206
216
 
207
217
  // === 4. Storage account + containers ===
208
218
  const storage = new azure.storage.StorageAccount(`${namePrefix}-st`, {
@@ -437,6 +447,12 @@ interface ContainerAppArgs {
437
447
  readonly extraEnv?: ReadonlyArray<{ name: string; value: pulumi.Input<string> }>;
438
448
  /** Defaults to 0.5 vCPU / 1 GiB. */
439
449
  readonly resources?: { readonly cpu: number; readonly memory: string };
450
+ /**
451
+ * #1328 — a service that reads nothing of the install: no CAELO_ENV, no
452
+ * media URL, only `CAELO_PROVIDER` plus {@link extraEnv}, and zero
453
+ * minimum replicas in every environment (E14 a of #763).
454
+ */
455
+ readonly isolated?: true;
440
456
  }
441
457
 
442
458
  function containerApp(args: ContainerAppArgs): azure.app.ContainerApp {
@@ -454,7 +470,10 @@ function containerApp(args: ContainerAppArgs): azure.app.ContainerApp {
454
470
  },
455
471
  },
456
472
  template: {
457
- scale: { minReplicas: env === "production" ? 1 : 0, maxReplicas: MAX_REPLICAS },
473
+ scale: {
474
+ minReplicas: args.isolated || env !== "production" ? 0 : 1,
475
+ maxReplicas: MAX_REPLICAS,
476
+ },
458
477
  containers: [
459
478
  {
460
479
  name: args.serviceName,
@@ -462,16 +481,18 @@ function containerApp(args: ContainerAppArgs): azure.app.ContainerApp {
462
481
  image:
463
482
  cfg.get(`image-${args.serviceName}`) ??
464
483
  `${namePrefix}.azurecr.io/${args.serviceName}:latest`,
465
- env: [
466
- { name: "CAELO_PROVIDER", value: "azure" },
467
- { name: "CAELO_ENV", value: env },
468
- ...args.databaseEnv,
469
- {
470
- name: "MEDIA_STORAGE_URL",
471
- value: pulumi.interpolate`https://${storage.name}.blob.core.windows.net/media`,
472
- },
473
- ...(args.extraEnv ?? []),
474
- ],
484
+ env: args.isolated
485
+ ? [{ name: "CAELO_PROVIDER", value: "azure" }, ...(args.extraEnv ?? [])]
486
+ : [
487
+ { name: "CAELO_PROVIDER", value: "azure" },
488
+ { name: "CAELO_ENV", value: env },
489
+ ...args.databaseEnv,
490
+ {
491
+ name: "MEDIA_STORAGE_URL",
492
+ value: pulumi.interpolate`https://${storage.name}.blob.core.windows.net/media`,
493
+ },
494
+ ...(args.extraEnv ?? []),
495
+ ],
475
496
  resources: args.resources ?? { cpu: 0.5, memory: "1.0Gi" },
476
497
  },
477
498
  ],
@@ -499,11 +520,29 @@ const _gatewayApp = containerApp({
499
520
  ...poolBudget.env.gateway,
500
521
  ],
501
522
  // Which X-Forwarded-For entry is the visitor (api-gateway client-ip.ts).
502
- // #806 — no remote-MCP env: remote MCP rides Google IAP (gcp stacks
503
- // only); unset, the gateway answers /api/mcp/* with 503
504
- // remote_mcp_not_configured and the admin reports it unavailable.
505
523
  extraEnv: [{ name: TRUSTED_PROXY_HOPS_ENV, value: String(GATEWAY_TRUSTED_PROXY_HOPS.azure) }],
506
524
  });
525
+ // #1328 — the MCP-Auth-Proxy, its own Container App and so its own FQDN:
526
+ // remote MCP's sign-in pages never share an origin with the site's module
527
+ // JS. Remote MCP needs the admin behind Google IAP, which Azure does not
528
+ // have, so it runs with CAELO_REMOTE_MCP=unavailable and answers everything
529
+ // but its health check (`/api/mcp/health`) with one fixed 404; the admin
530
+ // reports remote MCP as not available here. No database URL, no secret
531
+ // and no managed identity, because there is nothing it may read. Same
532
+ // shape as on every provider (E11 of #763), at zero replicas until a
533
+ // request comes.
534
+ const _mcpAuthProxyApp = containerApp({
535
+ serviceName: "mcp-auth-proxy",
536
+ databaseEnv: [],
537
+ isolated: true,
538
+ resources: { cpu: 0.25, memory: "0.5Gi" },
539
+ extraEnv: [
540
+ { name: REMOTE_MCP_MODE_ENV, value: REMOTE_MCP_MODE.azure },
541
+ { name: TRUSTED_PROXY_HOPS_ENV, value: String(MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS.azure) },
542
+ // The app's ingress targets 8080 (containerApp above).
543
+ { name: "PORT", value: "8080" },
544
+ ],
545
+ });
507
546
  const _orchestratorApp = containerApp({
508
547
  serviceName: "orchestrator",
509
548
  databaseEnv: adminDatabaseEnv,
@@ -27,6 +27,7 @@ import {
27
27
  gcpBucketName,
28
28
  gcpSecretId,
29
29
  MCP_IAP_SERVICE_ACCOUNT_ID,
30
+ mcpAuthProxyServiceAccountId,
30
31
  runServiceAccountId,
31
32
  staticPublisherServiceAccountId,
32
33
  } from "../../dist/gcp-names.js";
@@ -36,18 +37,21 @@ import {
36
37
  DEFAULT_MAX_CONNECTIONS,
37
38
  stackPoolBudget,
38
39
  } from "../../dist/pool-budget-stack.js";
39
- import { mcpArmorRuleArgs } from "../../dist/remote-mcp-edge.js";
40
40
  import {
41
41
  ADMIN_MEMORY_DEFAULT,
42
42
  adminEnvContract,
43
43
  adminMediaVolume,
44
44
  adminMediaVolumeTemplate,
45
45
  CLOUD_RUN_IGNORE_CHANGES,
46
+ CLOUD_SQL_PSA_CONNECTION_TYPE,
47
+ CLOUD_SQL_SERVER_CA_MODE,
48
+ CLOUD_SQL_SSL_MODE,
46
49
  type CloudRunEnvVar,
47
50
  type CloudRunSlug,
51
+ cloudSqlDnsZoneName,
52
+ cloudSqlPrivateDnsName,
48
53
  databaseUrls,
49
54
  defaultStagingHost,
50
- GATEWAY_TIMEOUT_SECONDS,
51
55
  gatewayEnvContract,
52
56
  REMOTE_MCP_IDENTITY_CONFIG,
53
57
  type RuntimeSecret,
@@ -331,6 +335,11 @@ const sqlInstance = new gcp.sql.DatabaseInstance(
331
335
  ipConfiguration: {
332
336
  ipv4Enabled: false,
333
337
  privateNetwork: network.id,
338
+ // #1338 — the shared CA names the instance's DNS name in its server
339
+ // certificate, so every client verifies it (sslmode=verify-full);
340
+ // unencrypted connections are refused outright.
341
+ serverCaMode: CLOUD_SQL_SERVER_CA_MODE,
342
+ sslMode: CLOUD_SQL_SSL_MODE,
334
343
  },
335
344
  deletionProtectionEnabled: deletionProtection,
336
345
  },
@@ -374,10 +383,50 @@ const cmsPublicDb = new gcp.sql.Database(
374
383
  opts,
375
384
  );
376
385
 
386
+ // #1338 — the instance's DNS name, resolved to its private IP by a private
387
+ // zone in the VPC (Cloud SQL assigns the name, it does not resolve it).
388
+ // Every client dials this name with sslmode=verify-full; the certificate
389
+ // names no IP. `cms-provision upgrade` converges the same zone and record
390
+ // through gcloud (cloudsql-tls.ts); the installer adopts them (#1079).
391
+ const sqlDnsName = sqlInstance.dnsNames.apply((names) => {
392
+ const name = cloudSqlPrivateDnsName(names);
393
+ if (!name) {
394
+ throw new Error(
395
+ `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)`,
396
+ );
397
+ }
398
+ return name;
399
+ });
400
+ const sqlDnsZone = new gcp.dns.ManagedZone(
401
+ `${namePrefix}-pg-dns`,
402
+ {
403
+ name: cloudSqlDnsZoneName(env),
404
+ dnsName: pulumi.interpolate`${sqlDnsName}.`,
405
+ visibility: "private",
406
+ privateVisibilityConfig: { networks: [{ networkUrl: network.id }] },
407
+ description: `Caelo ${env}: the Cloud SQL instance's DNS name inside the VPC (#1338)`,
408
+ },
409
+ opts,
410
+ );
411
+ const sqlDnsRecord = new gcp.dns.RecordSet(
412
+ `${namePrefix}-pg-dns-a`,
413
+ {
414
+ // Literal names, not the zone's outputs: the installer can then adopt
415
+ // a record an upgrade created (adopt-existing.ts) from the preview.
416
+ managedZone: cloudSqlDnsZoneName(env),
417
+ name: pulumi.interpolate`${sqlDnsName}.`,
418
+ type: "A",
419
+ ttl: 300,
420
+ rrdatas: [sqlInstance.privateIpAddress],
421
+ },
422
+ { ...opts, dependsOn: [sqlDnsZone] },
423
+ );
424
+
377
425
  // Password-less connection URLs (stack-contract.ts databaseUrls); the
378
426
  // services get the password as a Secret Manager reference
379
- // (SERVICE_SECRET_ENV), so no revision ever carries it.
380
- const dbUrls = sqlInstance.privateIpAddress.apply(databaseUrls);
427
+ // (SERVICE_SECRET_ENV), so no revision ever carries it. Tied to the record
428
+ // so no revision boots before its database host resolves.
429
+ const dbUrls = pulumi.all([sqlDnsName, sqlDnsRecord.id]).apply(([host]) => databaseUrls(host));
381
430
 
382
431
  // Reference pgPublicUser so Pulumi knows it's part of the dependency
383
432
  // graph (Cloud Run env-var deps don't see this directly).
@@ -475,9 +524,24 @@ const gatewaySa = new gcp.serviceaccount.Account(
475
524
  },
476
525
  opts,
477
526
  );
527
+ // #1328 — the MCP-Auth-Proxy's own SA and (below) its secretAccessor on its
528
+ // two secrets, nothing else. The proxy's service, NEG, backend and Cloud
529
+ // Armor policy are the CLI's (remote-mcp.ts ensureRemoteMcpEdge, after the
530
+ // first `up`), like the jobs: the converge creates the same on existing
531
+ // installs, and the URL map's path rules are the CLI's already.
532
+ const mcpAuthProxySa = new gcp.serviceaccount.Account(
533
+ `${namePrefix}-mcp-auth-proxy-sa`,
534
+ {
535
+ accountId: mcpAuthProxyServiceAccountId(env),
536
+ displayName: `Caelo ${env} MCP-Auth-Proxy`,
537
+ createIgnoreAlreadyExists: true,
538
+ },
539
+ opts,
540
+ );
478
541
  const serviceSa: Record<CloudRunSlug, gcp.serviceaccount.Account> = {
479
542
  admin: runSa,
480
543
  gateway: gatewaySa,
544
+ "mcp-auth-proxy": mcpAuthProxySa,
481
545
  };
482
546
 
483
547
  // secretAccessor for each service's SA on exactly the secrets its env reads.
@@ -486,6 +550,7 @@ const serviceSa: Record<CloudRunSlug, gcp.serviceaccount.Account> = {
486
550
  const secretBindings: Record<CloudRunSlug, gcp.secretmanager.SecretIamMember[]> = {
487
551
  admin: [],
488
552
  gateway: [],
553
+ "mcp-auth-proxy": [],
489
554
  };
490
555
  for (const binding of runtimeSecretBindings()) {
491
556
  secretBindings[binding.service].push(
@@ -662,7 +727,8 @@ function imageTag(service: string): pulumi.Output<string> {
662
727
  }
663
728
 
664
729
  interface CloudRunArgs {
665
- readonly serviceName: "admin" | "gateway";
730
+ /** The MCP-Auth-Proxy is CLI-owned on this stack (see mcpAuthProxySa). */
731
+ readonly serviceName: Exclude<CloudRunSlug, "mcp-auth-proxy">;
666
732
  readonly minInstances: number;
667
733
  readonly maxInstances: number;
668
734
  readonly memory: string;
@@ -670,8 +736,7 @@ interface CloudRunArgs {
670
736
  * Per-request timeout. Cloud Run's default is 300s (5min), which is too
671
737
  * short for the admin's SSE chat-stream — a multi-tool compose-page session
672
738
  * regularly runs 5–15 min. Pass an explicit "<n>s" Pulumi-style duration
673
- * string. The gateway gets the same (#806): it proxies remote-MCP chat
674
- * streams to the admin.
739
+ * string.
675
740
  */
676
741
  readonly timeout?: string;
677
742
  /**
@@ -816,18 +881,12 @@ const gatewaySvc = cloudRunService({
816
881
  minInstances: gatewayMinInstances,
817
882
  maxInstances: gatewayMaxInstances,
818
883
  memory: "512Mi",
819
- // #806 — remote MCP: chat turns stream through the gateway for minutes,
820
- // so it gets the admin's 3600 s instead of Cloud Run's 300 s default
821
- // (stack-contract.ts GATEWAY_TIMEOUT_SECONDS). The LB in front imposes no
822
- // shorter limit on serverless NEG backends.
823
- timeout: `${GATEWAY_TIMEOUT_SECONDS}s`,
884
+ // Visitor requests only (#1328 moved remote MCP's long streams to the
885
+ // MCP-Auth-Proxy): Cloud Run's default.
886
+ timeout: "300s",
824
887
  // public_role on cms_public only: no cms_admin login of any role
825
888
  // (#613, #635, CLAUDE.md §2).
826
- contractEnv: [
827
- ...gatewayEnvContract(envContractInputs),
828
- ...remoteMcpEnv.gateway,
829
- ...poolBudget.env.gateway,
830
- ],
889
+ contractEnv: [...gatewayEnvContract(envContractInputs), ...poolBudget.env.gateway],
831
890
  });
832
891
 
833
892
  // =========================================================================
@@ -838,6 +897,9 @@ const gatewaySvc = cloudRunService({
838
897
  // admin.caelo-cms.com. Three backends routed by Host header / path:
839
898
  // - host=caelo-cms.com, default → static bucket (Cloud CDN)
840
899
  // - host=caelo-cms.com, path=/api/* → gateway Cloud Run (Cloud Armor)
900
+ // - host=caelo-cms.com, path=/api/mcp/*, /.well-known/oauth-*
901
+ // → MCP-Auth-Proxy Cloud Run (its own
902
+ // Cloud Armor; CLI-owned, #1328)
841
903
  // - host=admin.caelo-cms.com, default → admin Cloud Run (IAP-gated)
842
904
  //
843
905
  // CLAUDE.md §11.B Tier 2: admin behind cloud-native identity proxy. IAP
@@ -860,12 +922,10 @@ const wafPolicy = new gcp.compute.SecurityPolicy(
860
922
  {
861
923
  description: `Caelo ${env} Cloud Armor — rate limit + OWASP`,
862
924
  rules: [
863
- // #806 — remote MCP (/api/mcp/*): a per-IP throttle sized for agent
864
- // traffic behind shared connector IPs, evaluated first; below it the
865
- // rule allows, so the OWASP packs (MCP bodies carry HTML) and the
866
- // general ban below do not apply there. src/remote-mcp-edge.ts says
867
- // why; `cms-provision upgrade` adds the same rule to existing installs.
868
- mcpArmorRuleArgs(),
925
+ // #1328 — no remote-MCP rule here any more: the URL map sends
926
+ // /api/mcp/* to the MCP-Auth-Proxy's backend, whose own policy carries
927
+ // it (remote-mcp-edge.ts, created by the CLI; `upgrade` drops the old
928
+ // rule from this policy).
869
929
  // Rate limit per source IP — 100 req / min sliding window.
870
930
  {
871
931
  action: "rate_based_ban",