@caelo-cms/provisioning 0.10.29 → 0.10.30

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 (82) hide show
  1. package/dist/admin-domain.d.ts +54 -0
  2. package/dist/admin-domain.d.ts.map +1 -0
  3. package/dist/admin-domain.js +122 -0
  4. package/dist/admin-domain.js.map +1 -0
  5. package/dist/cli.js +65 -6
  6. package/dist/cli.js.map +1 -1
  7. package/dist/compose.d.ts +7 -9
  8. package/dist/compose.d.ts.map +1 -1
  9. package/dist/compose.js +17 -1
  10. package/dist/compose.js.map +1 -1
  11. package/dist/deployed-release.d.ts +67 -0
  12. package/dist/deployed-release.d.ts.map +1 -0
  13. package/dist/deployed-release.js +144 -0
  14. package/dist/deployed-release.js.map +1 -0
  15. package/dist/gcloud-retry.d.ts +16 -0
  16. package/dist/gcloud-retry.d.ts.map +1 -0
  17. package/dist/gcloud-retry.js +17 -0
  18. package/dist/gcloud-retry.js.map +1 -0
  19. package/dist/gcloud.d.ts.map +1 -1
  20. package/dist/gcloud.js +2 -0
  21. package/dist/gcloud.js.map +1 -1
  22. package/dist/gcp-names.d.ts +58 -0
  23. package/dist/gcp-names.d.ts.map +1 -0
  24. package/dist/gcp-names.js +88 -0
  25. package/dist/gcp-names.js.map +1 -0
  26. package/dist/install-state.d.ts +31 -0
  27. package/dist/install-state.d.ts.map +1 -1
  28. package/dist/install-state.js +38 -0
  29. package/dist/install-state.js.map +1 -1
  30. package/dist/lifecycle.d.ts.map +1 -1
  31. package/dist/lifecycle.js +314 -46
  32. package/dist/lifecycle.js.map +1 -1
  33. package/dist/mcp-iap.d.ts +5 -26
  34. package/dist/mcp-iap.d.ts.map +1 -1
  35. package/dist/mcp-iap.js +6 -18
  36. package/dist/mcp-iap.js.map +1 -1
  37. package/dist/migration-runner.d.ts +25 -0
  38. package/dist/migration-runner.d.ts.map +1 -1
  39. package/dist/migration-runner.js +60 -19
  40. package/dist/migration-runner.js.map +1 -1
  41. package/dist/operator-access.d.ts +83 -0
  42. package/dist/operator-access.d.ts.map +1 -0
  43. package/dist/operator-access.js +562 -0
  44. package/dist/operator-access.js.map +1 -0
  45. package/dist/runtime-secrets.d.ts +128 -0
  46. package/dist/runtime-secrets.d.ts.map +1 -0
  47. package/dist/runtime-secrets.js +444 -0
  48. package/dist/runtime-secrets.js.map +1 -0
  49. package/dist/stack-contract.d.ts +231 -0
  50. package/dist/stack-contract.d.ts.map +1 -0
  51. package/dist/stack-contract.js +455 -0
  52. package/dist/stack-contract.js.map +1 -0
  53. package/dist/stack-converge.d.ts +183 -0
  54. package/dist/stack-converge.d.ts.map +1 -0
  55. package/dist/stack-converge.js +513 -0
  56. package/dist/stack-converge.js.map +1 -0
  57. package/dist/wizard.d.ts.map +1 -1
  58. package/dist/wizard.js +8 -1
  59. package/dist/wizard.js.map +1 -1
  60. package/dist/wizards/gcp-cost.d.ts +6 -10
  61. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  62. package/dist/wizards/gcp-cost.js +34 -2
  63. package/dist/wizards/gcp-cost.js.map +1 -1
  64. package/dist/wizards/gcp-pulumi.d.ts +32 -1
  65. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  66. package/dist/wizards/gcp-pulumi.js +60 -5
  67. package/dist/wizards/gcp-pulumi.js.map +1 -1
  68. package/dist/wizards/gcp.d.ts.map +1 -1
  69. package/dist/wizards/gcp.js +79 -16
  70. package/dist/wizards/gcp.js.map +1 -1
  71. package/package.json +3 -3
  72. package/stacks/aws/Pulumi.yaml +4 -0
  73. package/stacks/aws/index.ts +11 -0
  74. package/stacks/azure/Pulumi.yaml +4 -0
  75. package/stacks/azure/index.ts +10 -1
  76. package/stacks/gcp/Pulumi.yaml +4 -3
  77. package/stacks/gcp/README.md +13 -2
  78. package/stacks/gcp/index.ts +200 -140
  79. package/stacks/gcp-firebase/Pulumi.yaml +4 -0
  80. package/stacks/gcp-firebase/index.ts +165 -75
  81. package/stacks/self-hosted/Pulumi.yaml +4 -0
  82. package/stacks/self-hosted/index.ts +3 -0
@@ -30,6 +30,23 @@ import * as pulumi from "@pulumi/pulumi";
30
30
  import * as random from "@pulumi/random";
31
31
  import type { CloudAdapterOutputs, DnsRecord } from "../../dist/adapter.js";
32
32
  import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
33
+ import {
34
+ gatewayServiceAccountId,
35
+ gcpBucketName,
36
+ gcpSecretId,
37
+ MCP_IAP_SERVICE_ACCOUNT_ID,
38
+ runServiceAccountId,
39
+ } from "../../dist/gcp-names.js";
40
+ import {
41
+ ADMIN_MEMORY_DEFAULT,
42
+ adminEnvContract,
43
+ type CloudRunEnvVar,
44
+ type CloudRunSlug,
45
+ databaseUrls,
46
+ gatewayEnvContract,
47
+ type RuntimeSecret,
48
+ runtimeSecretBindings,
49
+ } from "../../dist/stack-contract.js";
33
50
 
34
51
  const cfg = new pulumi.Config();
35
52
  const domain = cfg.require("domain");
@@ -57,6 +74,9 @@ const cloudSqlHa = cfg.getBoolean("cloudSqlHa") ?? false;
57
74
  const cloudSqlEdition = cfg.get("cloudSqlEdition") ?? "ENTERPRISE";
58
75
  const deletionProtection = cfg.getBoolean("deletionProtection") ?? false;
59
76
  const adminMinInstances = Number.parseInt(cfg.get("adminMinInstances") ?? "0", 10);
77
+ // #553 — the admin also runs the Lighthouse quality audit; same knob and
78
+ // default on every adapter (stack-contract.ts ADMIN_MEMORY_DEFAULT).
79
+ const adminMemory = cfg.get("adminMemory") ?? ADMIN_MEMORY_DEFAULT;
60
80
  const adminMaxInstances = Number.parseInt(cfg.get("adminMaxInstances") ?? "5", 10);
61
81
  const gatewayMinInstances = Number.parseInt(cfg.get("gatewayMinInstances") ?? "0", 10);
62
82
  const maxConnections = Number.parseInt(cfg.get("maxConnections") ?? "100", 10);
@@ -139,15 +159,9 @@ const sqlPeering = new gcp.servicenetworking.Connection(
139
159
  // =========================================================================
140
160
 
141
161
  const pgPasswordBytes = new random.RandomBytes(`${namePrefix}-pg-password-bytes`, { length: 32 });
142
- const csrfSecretBytes = new random.RandomBytes(`${namePrefix}-csrf-secret-bytes`, { length: 32 });
143
- const cookieSecretBytes = new random.RandomBytes(`${namePrefix}-cookie-secret-bytes`, {
144
- length: 32,
145
- });
146
162
  const kekBytes = new random.RandomBytes(`${namePrefix}-secret-kek-bytes`, { length: 32 });
147
163
 
148
164
  const postgresPassword = pulumi.secret(pgPasswordBytes.hex);
149
- const csrfSecret = pulumi.secret(csrfSecretBytes.hex);
150
- const cookieSecret = pulumi.secret(cookieSecretBytes.hex);
151
165
  const caeloSecretKek = pulumi.secret(kekBytes.hex);
152
166
  // v0.3.2 — anthropic-api-key Secret + Pulumi config dropped. The
153
167
  // runtime AI provider configuration lives at /security/ai (key
@@ -162,19 +176,26 @@ const caeloSecretKek = pulumi.secret(kekBytes.hex);
162
176
  // in v0.3.0 but never wired to runtime (transport.ts reads from
163
177
  // cfg.config.apiKey, never from Secret Manager). Drop to reduce
164
178
  // surface area + monthly cost.
179
+ // csrf-secret + cookie-secret were dropped for the same reason: CSRF
180
+ // secrets are per session (in cms_admin) and the gateway's cookie secret
181
+ // lives in site_settings — nothing read the Secret Manager copies.
182
+ // `pulumi up` deletes them; `cms-provision upgrade` leaves them in place.
183
+ //
184
+ // internal-secret (CAELO_INTERNAL_SECRET) and tool-approval-secret
185
+ // (CAELO_TOOL_APPROVAL_SECRET) are generated by the CLI, not here (see the
186
+ // same note in stacks/gcp/index.ts); this program references them by id.
165
187
 
166
188
  interface MadeSecret {
167
189
  resource: gcp.secretmanager.Secret;
168
- version: gcp.secretmanager.SecretVersion | null;
190
+ version: gcp.secretmanager.SecretVersion;
169
191
  }
170
192
 
171
- function makeSecret(name: string, value: pulumi.Output<string> | null): MadeSecret {
193
+ function makeSecret(name: string, value: pulumi.Output<string>): MadeSecret {
172
194
  const resource = new gcp.secretmanager.Secret(
173
195
  `${namePrefix}-${name}`,
174
- { secretId: `${namePrefix}-${name}`, replication: secretReplication },
196
+ { secretId: gcpSecretId(env, name), replication: secretReplication },
175
197
  opts,
176
198
  );
177
- if (value === null) return { resource, version: null };
178
199
  const version = new gcp.secretmanager.SecretVersion(
179
200
  `${namePrefix}-${name}-v1`,
180
201
  { secret: resource.name, secretData: value },
@@ -184,10 +205,17 @@ function makeSecret(name: string, value: pulumi.Output<string> | null): MadeSecr
184
205
  }
185
206
 
186
207
  const pgSecret = makeSecret("postgres-password", postgresPassword);
187
- const csrfSecretRes = makeSecret("csrf-secret", csrfSecret);
188
- const cookieSecretRes = makeSecret("cookie-secret", cookieSecret);
189
208
  const kekSecret = makeSecret("secret-kek", caeloSecretKek);
190
209
 
210
+ // The id each runtime secret is bound by: the Pulumi resource where this
211
+ // program owns the secret, the CLI-generated id otherwise.
212
+ const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
213
+ "postgres-password": pgSecret.resource.secretId,
214
+ "secret-kek": kekSecret.resource.secretId,
215
+ "internal-secret": gcpSecretId(env, "internal-secret"),
216
+ "tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
217
+ };
218
+
191
219
  // =========================================================================
192
220
  // Tier 4 — Cloud SQL Postgres (private IP only; HA configurable)
193
221
  // =========================================================================
@@ -216,7 +244,12 @@ const sqlInstance = new gcp.sql.DatabaseInstance(
216
244
  privateNetwork: network.id,
217
245
  enablePrivatePathForGoogleCloudServices: true,
218
246
  },
219
- databaseFlags: [{ name: "max_connections", value: maxConnections.toString() }],
247
+ databaseFlags: [
248
+ { name: "max_connections", value: maxConnections.toString() },
249
+ // The operator-access sync job logs in as its service account (Cloud
250
+ // SQL IAM database authentication, no password). No restart needed.
251
+ { name: "cloudsql.iam_authentication", value: "on" },
252
+ ],
220
253
  },
221
254
  deletionProtection,
222
255
  },
@@ -245,9 +278,9 @@ new gcp.sql.User(
245
278
  opts,
246
279
  );
247
280
 
248
- const adminDatabaseUrl = pulumi.interpolate`postgres://admin_role:${postgresPassword}@${sqlInstance.privateIpAddress}:5432/cms_admin?sslmode=require`;
249
- const publicAdminDatabaseUrl = pulumi.interpolate`postgres://admin_role:${postgresPassword}@${sqlInstance.privateIpAddress}:5432/cms_public?sslmode=require`;
250
- const publicDatabaseUrl = pulumi.interpolate`postgres://public_role:${postgresPassword}@${sqlInstance.privateIpAddress}:5432/cms_public?sslmode=require`;
281
+ // Password-less connection URLs (stack-contract.ts databaseUrls); the
282
+ // services get the password as a Secret Manager reference.
283
+ const dbUrls = sqlInstance.privateIpAddress.apply(databaseUrls);
251
284
 
252
285
  // =========================================================================
253
286
  // Media bucket — private, signed-URL access only (same as `gcp` stack)
@@ -256,7 +289,7 @@ const publicDatabaseUrl = pulumi.interpolate`postgres://public_role:${postgresPa
256
289
  const mediaBucket = new gcp.storage.Bucket(
257
290
  `${namePrefix}-media`,
258
291
  {
259
- name: `${project}-${namePrefix}-media`,
292
+ name: gcpBucketName(project, env, "media"),
260
293
  location: region.toUpperCase(),
261
294
  uniformBucketLevelAccess: true,
262
295
  forceDestroy: env !== "production",
@@ -272,26 +305,47 @@ const mediaBucket = new gcp.storage.Bucket(
272
305
  const runSa = new gcp.serviceaccount.Account(
273
306
  `${namePrefix}-run-sa`,
274
307
  {
275
- accountId: `${namePrefix}-run-sa`,
308
+ accountId: runServiceAccountId(env),
276
309
  displayName: `Caelo ${env} Cloud Run service account`,
277
310
  },
278
311
  opts,
279
312
  );
280
313
 
281
- for (const made of [
282
- { name: "postgres-password", made: pgSecret },
283
- { name: "csrf-secret", made: csrfSecretRes },
284
- { name: "cookie-secret", made: cookieSecretRes },
285
- { name: "secret-kek", made: kekSecret },
286
- ]) {
287
- new gcp.secretmanager.SecretIamMember(
288
- `${namePrefix}-${made.name}-binding`,
289
- {
290
- secretId: made.made.resource.secretId,
291
- role: "roles/secretmanager.secretAccessor",
292
- member: pulumi.interpolate`serviceAccount:${runSa.email}`,
293
- },
294
- { ...opts, dependsOn: [made.made.resource] },
314
+ // The gateway runs as its own SA so it can read only the secrets it needs
315
+ // (stack-contract.ts SERVICE_SECRET_ENV). `cms-provision upgrade` creates it
316
+ // on older installs, hence createIgnoreAlreadyExists.
317
+ const gatewaySa = new gcp.serviceaccount.Account(
318
+ `${namePrefix}-gateway-sa`,
319
+ {
320
+ accountId: gatewayServiceAccountId(env),
321
+ displayName: `Caelo ${env} gateway`,
322
+ createIgnoreAlreadyExists: true,
323
+ },
324
+ opts,
325
+ );
326
+ const serviceSa: Record<CloudRunSlug, gcp.serviceaccount.Account> = {
327
+ admin: runSa,
328
+ gateway: gatewaySa,
329
+ };
330
+
331
+ // secretAccessor for each service's SA on exactly the secrets its env reads.
332
+ // Cloud Run refuses to deploy a revision whose SA can't read a referenced
333
+ // secret, so each service waits for its bindings.
334
+ const secretBindings: Record<CloudRunSlug, gcp.secretmanager.SecretIamMember[]> = {
335
+ admin: [],
336
+ gateway: [],
337
+ };
338
+ for (const binding of runtimeSecretBindings()) {
339
+ secretBindings[binding.service].push(
340
+ new gcp.secretmanager.SecretIamMember(
341
+ `${namePrefix}-${binding.stackResource}`,
342
+ {
343
+ secretId: runtimeSecretIds[binding.secret],
344
+ role: "roles/secretmanager.secretAccessor",
345
+ member: pulumi.interpolate`serviceAccount:${serviceSa[binding.service].email}`,
346
+ },
347
+ opts,
348
+ ),
295
349
  );
296
350
  }
297
351
 
@@ -346,6 +400,24 @@ new gcp.projects.IAMMember(
346
400
  },
347
401
  opts,
348
402
  );
403
+ new gcp.projects.IAMMember(
404
+ `${namePrefix}-gateway-log-writer`,
405
+ {
406
+ project,
407
+ role: "roles/logging.logWriter",
408
+ member: pulumi.interpolate`serviceAccount:${gatewaySa.email}`,
409
+ },
410
+ opts,
411
+ );
412
+ new gcp.projects.IAMMember(
413
+ `${namePrefix}-gateway-metric-writer`,
414
+ {
415
+ project,
416
+ role: "roles/monitoring.metricWriter",
417
+ member: pulumi.interpolate`serviceAccount:${gatewaySa.email}`,
418
+ },
419
+ opts,
420
+ );
349
421
 
350
422
  // =========================================================================
351
423
  // Tier 1 — Firebase Hosting site
@@ -394,12 +466,16 @@ const firebaseCustomDomain = new gcp.firebase.HostingCustomDomain(
394
466
  // =========================================================================
395
467
 
396
468
  interface CloudRunArgs {
397
- readonly serviceName: string;
469
+ readonly serviceName: "admin" | "gateway";
398
470
  readonly minInstances: number;
399
471
  readonly maxInstances: number;
400
472
  readonly memory: string;
401
473
  readonly timeout: string;
402
- readonly extraEnv?: ReadonlyArray<{ name: string; value: pulumi.Input<string> }>;
474
+ /**
475
+ * The service's complete env — plain values and Secret Manager
476
+ * references: its env contract (src/stack-contract.ts).
477
+ */
478
+ readonly contractEnv: ReadonlyArray<CloudRunEnvVar<pulumi.Input<string>>>;
403
479
  /** v0.3.1 — admin: ALL (so IAP gates internet traffic). gateway:
404
480
  * INTERNAL_LOAD_BALANCER (locked down; only Firebase Hosting
405
481
  * rewrites + run.invoker-authorised callers can reach it). */
@@ -434,7 +510,7 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
434
510
  iapEnabled: args.iapEnabled ?? false,
435
511
  deletionProtection,
436
512
  template: {
437
- serviceAccount: runSa.email,
513
+ serviceAccount: serviceSa[args.serviceName].email,
438
514
  scaling: {
439
515
  minInstanceCount: args.minInstances,
440
516
  maxInstanceCount: args.maxInstances,
@@ -443,19 +519,7 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
443
519
  containers: [
444
520
  {
445
521
  image: imageRef(args.serviceName),
446
- envs: [
447
- { name: "CAELO_PROVIDER", value: "gcp-firebase" },
448
- { name: "CAELO_ENV", value: env },
449
- { name: "ADMIN_DATABASE_URL", value: adminDatabaseUrl },
450
- { name: "MEDIA_STORAGE_URL", value: pulumi.interpolate`gs://${mediaBucket.name}` },
451
- {
452
- name: "CAELO_SECRET_KEK",
453
- valueSource: {
454
- secretKeyRef: { secret: kekSecret.resource.secretId, version: "latest" },
455
- },
456
- },
457
- ...(args.extraEnv ?? []),
458
- ],
522
+ envs: [...args.contractEnv],
459
523
  resources: { limits: { cpu: "1", memory: args.memory } },
460
524
  },
461
525
  ],
@@ -471,13 +535,31 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
471
535
  pgAdminUser,
472
536
  cmsAdminDb,
473
537
  cmsPublicDb,
474
- ...(pgSecret.version ? [pgSecret.version] : []),
475
- ...(kekSecret.version ? [kekSecret.version] : []),
538
+ // The env contract names the media bucket as a string; keep it
539
+ // ordered before the services that use it.
540
+ mediaBucket,
541
+ // Referenced secrets must have a version, and the service's SA
542
+ // must be able to read them, before Cloud Run deploys a revision.
543
+ pgSecret.version,
544
+ kekSecret.version,
545
+ ...secretBindings[args.serviceName],
476
546
  ],
477
547
  },
478
548
  );
479
549
  }
480
550
 
551
+ // Inputs of the admin + gateway env contracts. The contracts are shared with
552
+ // `cms-provision upgrade`, which applies them to installs it rolls — add new
553
+ // env vars and secrets there, not here.
554
+ const envContractInputs = {
555
+ provider: "gcp-firebase",
556
+ projectId: project,
557
+ env,
558
+ domain,
559
+ region,
560
+ databaseUrls: { admin: dbUrls.admin, publicAdmin: dbUrls.publicAdmin, public: dbUrls.public },
561
+ } as const;
562
+
481
563
  // Gateway provisioned first so we can pass its name/region into the
482
564
  // admin's env (the Firebase publisher needs them to wire rewrites).
483
565
  const gatewaySvc = cloudRunService({
@@ -490,39 +572,27 @@ const gatewaySvc = cloudRunService({
490
572
  // rewrites or any caller holding roles/run.invoker on the
491
573
  // service. Anonymous internet traffic gets a Cloud Run 403.
492
574
  ingress: "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER",
493
- extraEnv: [{ name: "PUBLIC_DATABASE_URL", value: publicDatabaseUrl }],
575
+ contractEnv: gatewayEnvContract(envContractInputs),
494
576
  });
495
577
 
496
- // Issue #37 — deterministic, so the admin env can carry it as a plain string.
497
- const mcpServiceAccountEmail = `caelo-mcp@${project}.iam.gserviceaccount.com`;
498
-
499
578
  const adminSvc = cloudRunService({
500
579
  serviceName: "admin",
501
580
  minInstances: adminMinInstances,
502
581
  maxInstances: adminMaxInstances,
503
- memory: "1Gi",
582
+ memory: adminMemory,
504
583
  timeout: "3600s",
505
584
  // v0.3.1 — admin is internet-reachable but gated by Cloud Run
506
585
  // native IAP. iapEnabled flips the IAP integration on the service.
507
586
  ingress: "INGRESS_TRAFFIC_ALL",
508
587
  iapEnabled: true,
509
- extraEnv: [
510
- { name: "PUBLIC_ADMIN_DATABASE_URL", value: publicAdminDatabaseUrl },
511
- // Use the input string (known at config time) — resource output
512
- // is Output<string | undefined> which Input<string> rejects.
513
- // #551 — the public site URL; the admin seeds site_defaults.site_base_url
514
- // from it (canonical, og:url, sitemap) when it is not configured yet.
515
- { name: "CAELO_SITE_URL", value: `https://${domain}` },
516
- { name: "CAELO_FIREBASE_SITE", value: firebaseSiteId },
517
- { name: "CAELO_GENERATOR_CLI", value: "/app/apps/static-generator/src/cli.ts" },
518
- // v0.3.1 — Firebase publisher needs the gateway service name +
519
- // region to declare the /api/** rewrite when creating each
520
- // version.
521
- { name: "CAELO_GATEWAY_SERVICE", value: gatewaySvc.name },
522
- { name: "CAELO_GATEWAY_REGION", value: region },
523
- // Issue #37 — shown in the /security/mcp `claude mcp add` command.
524
- { name: "CAELO_MCP_IAP_SERVICE_ACCOUNT", value: mcpServiceAccountEmail },
525
- ],
588
+ // Use the input string (known at config time) for the site — the
589
+ // resource output is Output<string | undefined>, which Input<string>
590
+ // rejects.
591
+ contractEnv: adminEnvContract({
592
+ ...envContractInputs,
593
+ firebaseSiteId,
594
+ gatewayService: gatewaySvc.name,
595
+ }),
526
596
  });
527
597
 
528
598
  // =========================================================================
@@ -613,6 +683,11 @@ new gcp.cloudrunv2.ServiceIamMember(
613
683
  // (`https://<admin-svc>-<hash>-<region>.a.run.app`). They can flip
614
684
  // the knob + verify the domain at https://search.google.com/search-console
615
685
  // later and run `pulumi up` to bind admin.<domain> → Cloud Run.
686
+ //
687
+ // The supported path is `cms-provision admin-domain enable`
688
+ // (src/admin-domain.ts): it creates the same mapping as the operator's own
689
+ // gcloud identity — the verified domain owner — which the provisioner SA
690
+ // running this stack never is. Leave this knob false after using it.
616
691
  const provisionAdminDomain = cfg.getBoolean("provisionAdminDomain") ?? false;
617
692
  const adminDomainMapping = provisionAdminDomain
618
693
  ? new gcp.cloudrun.DomainMapping(
@@ -737,7 +812,7 @@ const mcpServiceAccount = new gcp.serviceaccount.Account(
737
812
  `${namePrefix}-mcp-sa`,
738
813
  {
739
814
  project,
740
- accountId: "caelo-mcp",
815
+ accountId: MCP_IAP_SERVICE_ACCOUNT_ID,
741
816
  displayName: "Caelo MCP (IAP ingress)",
742
817
  description:
743
818
  "Signs the IAP credential external MCP clients use to reach the admin (issue #37).",
@@ -774,6 +849,20 @@ new gcp.iap.WebCloudRunServiceIamMember(
774
849
  opts,
775
850
  );
776
851
 
852
+ // =========================================================================
853
+ // Operator access — deliberately NOT here
854
+ // =========================================================================
855
+ //
856
+ // Who passes IAP beyond the allowlist above follows the Caelo user list. A
857
+ // Cloud Run job with its own service account keeps the IAP binding and the
858
+ // caelo-mcp token-creator binding in step (admin-core
859
+ // security/operator-access/sync-job.ts); the admin's run SA holds no IAM
860
+ // policy rights and may only start that job. The CLI owns the job, its
861
+ // account, its grants and its schedule (packages/provisioning/src/
862
+ // operator-access.ts, run by the wizard after migrations and by `upgrade`),
863
+ // because the job's database user can only be assigned its role after the
864
+ // migrations have created that role.
865
+
777
866
  // =========================================================================
778
867
  // Outputs — DNS records the operator wires manually
779
868
  // =========================================================================
@@ -835,8 +924,9 @@ const tokenInfo = generateBootstrapToken();
835
924
  // plain strings in stack outputs at runtime; the cast bridges the
836
925
  // build-time type. Same pattern as packages/provisioning/stacks/gcp/index.ts.
837
926
  const out: CloudAdapterOutputs = {
838
- adminDatabaseUrl: adminDatabaseUrl as unknown as string,
839
- publicDatabaseUrl: publicDatabaseUrl as unknown as string,
927
+ // Password-less — the password lives only in Secret Manager.
928
+ adminDatabaseUrl: dbUrls.admin as unknown as string,
929
+ publicDatabaseUrl: dbUrls.public as unknown as string,
840
930
  mediaStorageUrl: pulumi.interpolate`gs://${mediaBucket.name}` as unknown as string,
841
931
  // Firebase Hosting serves media via its own CDN; we proxy through
842
932
  // the Firebase site's apex domain for canonical URLs.
@@ -25,3 +25,7 @@ config:
25
25
  type: string
26
26
  default: ./.caelo
27
27
  description: Where the generated docker-compose.yml + Caddyfile + secrets live.
28
+ caelo-self-hosted:adminMemory:
29
+ type: string
30
+ default: "2Gi"
31
+ description: Memory reserved for the admin container (it runs the Lighthouse quality audit).
@@ -31,6 +31,8 @@ const cfg = new pulumi.Config();
31
31
  const domain = cfg.require("domain");
32
32
  const ownerEmail = cfg.require("ownerEmail");
33
33
  const caeloDir = cfg.get("caeloDir") ?? "./.caelo";
34
+ // #553 — same knob as every adapter; undefined = stack-contract default.
35
+ const adminMemory = cfg.get("adminMemory");
34
36
 
35
37
  const composePath = resolve(caeloDir, "docker-compose.yml");
36
38
  const caddyPath = resolve(caeloDir, "Caddyfile");
@@ -52,6 +54,7 @@ const composeYaml = pulumi.all([postgresPassword, minioRootPassword]).apply(([pg
52
54
  anthropicApiKey: process.env.ANTHROPIC_API_KEY,
53
55
  resendApiKey: process.env.RESEND_API_KEY,
54
56
  diskSize: "20Gi",
57
+ ...(adminMemory ? { adminMemory } : {}),
55
58
  }),
56
59
  );
57
60