@caelo-cms/provisioning 0.10.55 → 0.10.57
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.
- package/dist/agent-db.js +76 -19
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/compose.js +5 -0
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +66 -0
- package/dist/converge-checkpoint.d.ts.map +1 -0
- package/dist/converge-checkpoint.js +61 -0
- package/dist/converge-checkpoint.js.map +1 -0
- package/dist/converge-job-gcp.d.ts +46 -0
- package/dist/converge-job-gcp.d.ts.map +1 -0
- package/dist/converge-job-gcp.js +368 -0
- package/dist/converge-job-gcp.js.map +1 -0
- package/dist/converge-job-setup.d.ts +62 -0
- package/dist/converge-job-setup.d.ts.map +1 -0
- package/dist/converge-job-setup.js +228 -0
- package/dist/converge-job-setup.js.map +1 -0
- package/dist/converge-job.d.ts +141 -0
- package/dist/converge-job.d.ts.map +1 -0
- package/dist/converge-job.js +325 -0
- package/dist/converge-job.js.map +1 -0
- package/dist/gcp-names.d.ts +9 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +13 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +22 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +11 -3
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +22 -12
- package/dist/install-job.js.map +1 -1
- package/dist/lifecycle.d.ts +13 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +115 -58
- package/dist/lifecycle.js.map +1 -1
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +13 -3
- package/dist/operator-access.js.map +1 -1
- package/dist/release-contract.d.ts +3 -2
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +5 -2
- package/dist/release-contract.js.map +1 -1
- package/dist/release-manifest.d.ts +2 -0
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp-edge.d.ts +114 -0
- package/dist/remote-mcp-edge.d.ts.map +1 -0
- package/dist/remote-mcp-edge.js +116 -0
- package/dist/remote-mcp-edge.js.map +1 -0
- package/dist/remote-mcp.d.ts +117 -0
- package/dist/remote-mcp.d.ts.map +1 -0
- package/dist/remote-mcp.js +379 -0
- package/dist/remote-mcp.js.map +1 -0
- package/dist/runtime-secrets.d.ts +12 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +32 -4
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/seal-key-rotation.d.ts +38 -0
- package/dist/seal-key-rotation.d.ts.map +1 -0
- package/dist/seal-key-rotation.js +206 -0
- package/dist/seal-key-rotation.js.map +1 -0
- package/dist/stack-contract.d.ts +167 -3
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +171 -3
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +38 -1
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +74 -4
- package/dist/stack-converge.js.map +1 -1
- package/dist/state-lock-lease.d.ts +142 -0
- package/dist/state-lock-lease.d.ts.map +1 -0
- package/dist/state-lock-lease.js +347 -0
- package/dist/state-lock-lease.js.map +1 -0
- package/dist/state-store.d.ts +66 -9
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +249 -30
- package/dist/state-store.js.map +1 -1
- package/dist/upgrade/app-roll.d.ts +36 -7
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +53 -19
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +111 -9
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +27 -10
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +46 -7
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/record-rolled.d.ts +40 -0
- package/dist/upgrade/record-rolled.d.ts.map +1 -0
- package/dist/upgrade/record-rolled.js +46 -0
- package/dist/upgrade/record-rolled.js.map +1 -0
- package/dist/upgrade/upgrade.d.ts +45 -1
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +60 -17
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +15 -1
- package/dist/upgrader-assess.d.ts.map +1 -1
- package/dist/upgrader-assess.js +36 -6
- package/dist/upgrader-assess.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +15 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +42 -7
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts +123 -0
- package/dist/upgrader-iam-preflight.d.ts.map +1 -0
- package/dist/upgrader-iam-preflight.js +414 -0
- package/dist/upgrader-iam-preflight.js.map +1 -0
- package/dist/upgrader-job.d.ts +33 -9
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +169 -27
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +58 -0
- package/dist/upgrader-records.d.ts.map +1 -0
- package/dist/upgrader-records.js +87 -0
- package/dist/upgrader-records.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +3 -2
- package/dist/upgrader-reporter.d.ts.map +1 -1
- package/dist/upgrader-reporter.js +10 -2
- package/dist/upgrader-reporter.js.map +1 -1
- package/dist/upgrader-setup.d.ts +25 -3
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +123 -11
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +11 -3
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +99 -4
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/agent/run.test.ts +14 -1
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +4 -1
- package/src/compose.ts +5 -0
- package/src/converge-checkpoint.ts +75 -0
- package/src/converge-entrypoint.test.ts +213 -0
- package/src/converge-job-gcp.test.ts +222 -0
- package/src/converge-job-gcp.ts +443 -0
- package/src/converge-job-setup.test.ts +152 -0
- package/src/converge-job-setup.ts +268 -0
- package/src/converge-job.test.ts +486 -0
- package/src/converge-job.ts +542 -0
- package/src/gateway-env-isolation.test.ts +8 -1
- package/src/gcp-names.ts +15 -0
- package/src/gcp-url-map.test.ts +9 -1
- package/src/index.ts +8 -0
- package/src/infra-contract.test.ts +7 -4
- package/src/infra-contract.ts +22 -1
- package/src/install-job.test.ts +3 -1
- package/src/install-job.ts +31 -14
- package/src/lifecycle.ts +137 -71
- package/src/operator-access.test.ts +14 -0
- package/src/operator-access.ts +13 -3
- package/src/release-contract.ts +5 -2
- package/src/release-manifest.test.ts +42 -0
- package/src/release-manifest.ts +2 -0
- package/src/remote-mcp-edge.ts +137 -0
- package/src/remote-mcp.test.ts +517 -0
- package/src/remote-mcp.ts +460 -0
- package/src/runtime-secrets.test.ts +31 -6
- package/src/runtime-secrets.ts +41 -3
- package/src/seal-key-rotation.test.ts +131 -0
- package/src/seal-key-rotation.ts +258 -0
- package/src/stack-contract-parity.test.ts +20 -3
- package/src/stack-contract.test.ts +30 -2
- package/src/stack-contract.ts +291 -3
- package/src/stack-converge-secrets.test.ts +3 -0
- package/src/stack-converge.test.ts +21 -5
- package/src/stack-converge.ts +101 -7
- package/src/state-attach.test.ts +7 -1
- package/src/state-lock-lease.test.ts +302 -0
- package/src/state-lock-lease.ts +423 -0
- package/src/state-lock.test.ts +568 -0
- package/src/state-store.ts +347 -27
- package/src/upgrade/app-roll.ts +96 -21
- package/src/upgrade/converge.ts +130 -8
- package/src/upgrade/gcp-lookup.test.ts +80 -0
- package/src/upgrade/gcp-lookup.ts +65 -8
- package/src/upgrade/record-rolled.test.ts +105 -0
- package/src/upgrade/record-rolled.ts +91 -0
- package/src/upgrade/upgrade.test.ts +203 -3
- package/src/upgrade/upgrade.ts +120 -24
- package/src/upgrader-assess.test.ts +92 -3
- package/src/upgrader-assess.ts +58 -9
- package/src/upgrader-gcp.ts +47 -8
- package/src/upgrader-iam-preflight.test.ts +393 -0
- package/src/upgrader-iam-preflight.ts +510 -0
- package/src/upgrader-job.test.ts +586 -15
- package/src/upgrader-job.ts +237 -53
- package/src/upgrader-records.ts +151 -0
- package/src/upgrader-reporter.test.ts +6 -0
- package/src/upgrader-reporter.ts +7 -2
- package/src/upgrader-setup.test.ts +3 -3
- package/src/upgrader-setup.ts +128 -11
- package/src/wizards/gcp-cost.ts +12 -3
- package/src/wizards/gcp.ts +125 -4
- package/stacks/aws/index.ts +10 -0
- package/stacks/azure/index.ts +9 -0
- package/stacks/gcp/index.ts +46 -7
- package/stacks/gcp-firebase/index.ts +37 -6
package/src/upgrader-setup.ts
CHANGED
|
@@ -17,9 +17,19 @@
|
|
|
17
17
|
* The job's own service account, `caelo-<env>-upgrader`, may do exactly
|
|
18
18
|
* what an app-roll needs (upgrade/app-roll.ts):
|
|
19
19
|
*
|
|
20
|
-
* - `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* - the custom role `caeloUpgraderRun` on the project
|
|
21
|
+
* ({@link UPGRADER_RUN_ROLE}): deploy admin/gateway revisions, move
|
|
22
|
+
* traffic, recreate and run the migration job, update its own job image.
|
|
23
|
+
* NOT `roles/run.developer`: that role holds `run.jobs.runWithOverrides`
|
|
24
|
+
* on every job of the project, including the converge job and the
|
|
25
|
+
* install job, which run as `caelo-provisioner` — the upgrader could run
|
|
26
|
+
* them with its own args (destroy, owner-reset) or env (CLOUDSDK_*,
|
|
27
|
+
* PULUMI_*) and so act as the provisioner (#883 review). The custom role
|
|
28
|
+
* has no `runWithOverrides` and no `*.setIamPolicy`. Updating another
|
|
29
|
+
* job (the converge job's image, say) additionally needs `actAs` on its
|
|
30
|
+
* service account, which the upgrader holds only on the admin, gateway
|
|
31
|
+
* and its own. A grant of `roles/run.developer` left by an earlier
|
|
32
|
+
* setup is removed.
|
|
23
33
|
* - `roles/iam.serviceAccountUser` on exactly the admin's and the
|
|
24
34
|
* gateway's runtime service accounts (a revision runs as them), on
|
|
25
35
|
* itself (its own job) and on the quality-audit job's account (the
|
|
@@ -46,6 +56,19 @@
|
|
|
46
56
|
* is a converge, which only the operator runs ("Run in Cloud Shell").
|
|
47
57
|
* {@link UPGRADER_FORBIDDEN_ROLES} names that line; the tests assert no
|
|
48
58
|
* grant crosses it.
|
|
59
|
+
*
|
|
60
|
+
* Direct VPC egress (#818): the migration job and both services run with
|
|
61
|
+
* `--network/--subnet`. Google documents the compute permissions that needs
|
|
62
|
+
* (`compute.subnetworks.use`, `compute.addresses.createInternal`, …) on the
|
|
63
|
+
* Cloud Run service agent, which `roles/run.serviceAgent` already gives it
|
|
64
|
+
* (cloud.google.com/run/docs/configuring/vpc-direct-vpc), not on the
|
|
65
|
+
* principal that deploys. So the upgrader gets no compute role; the
|
|
66
|
+
* real-GCP upgrader smoke test (.github/workflows/upgrader-smoke.yml) shows
|
|
67
|
+
* it once a stable release was installed on the smoke install through
|
|
68
|
+
* Settings → Updates (the next tag cannot ship before). Every permission
|
|
69
|
+
* the job needs is checked by the job itself before it changes anything
|
|
70
|
+
* (upgrader-iam-preflight.ts);
|
|
71
|
+
* a grant added or removed here must keep that list in step.
|
|
49
72
|
*/
|
|
50
73
|
|
|
51
74
|
import { ensureCloudSqlIamUser } from "./cloudsql-iam-user.js";
|
|
@@ -91,22 +114,72 @@ export const UPGRADER_OPS_LIST_ROLE: CustomRoleSpec = {
|
|
|
91
114
|
permissions: ["storage.objects.list"],
|
|
92
115
|
};
|
|
93
116
|
|
|
94
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* What an app-roll does on Cloud Run (upgrade/app-roll.ts, migration-runner.ts,
|
|
119
|
+
* the self-update, the #815 lock lease) — `roles/run.developer` minus
|
|
120
|
+
* everything else, above all `run.jobs.runWithOverrides` (see the file
|
|
121
|
+
* header). Keep in step with upgrader-iam-preflight.ts.
|
|
122
|
+
*/
|
|
123
|
+
export const UPGRADER_RUN_ROLE: CustomRoleSpec = {
|
|
124
|
+
roleId: "caeloUpgraderRun",
|
|
125
|
+
title: "Caelo upgrader: roll Cloud Run",
|
|
126
|
+
description:
|
|
127
|
+
"Lets the Caelo upgrader job deploy admin/gateway revisions, run the migration job and update its own image. Cannot start any job with changed arguments or env, cannot change IAM.",
|
|
128
|
+
permissions: [
|
|
129
|
+
"run.configurations.get",
|
|
130
|
+
"run.configurations.list",
|
|
131
|
+
"run.executions.get",
|
|
132
|
+
"run.executions.list",
|
|
133
|
+
"run.jobs.create",
|
|
134
|
+
"run.jobs.delete",
|
|
135
|
+
"run.jobs.get",
|
|
136
|
+
"run.jobs.list",
|
|
137
|
+
"run.jobs.run",
|
|
138
|
+
"run.jobs.update",
|
|
139
|
+
"run.operations.get",
|
|
140
|
+
"run.revisions.get",
|
|
141
|
+
"run.revisions.list",
|
|
142
|
+
"run.routes.get",
|
|
143
|
+
"run.routes.list",
|
|
144
|
+
"run.services.get",
|
|
145
|
+
"run.services.list",
|
|
146
|
+
"run.services.update",
|
|
147
|
+
"run.tasks.get",
|
|
148
|
+
"run.tasks.list",
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** Project-level predefined roles of the upgrader's service account. */
|
|
95
153
|
export const UPGRADER_PROJECT_ROLES = [
|
|
96
|
-
"roles/run.developer",
|
|
97
154
|
"roles/cloudsql.instanceUser",
|
|
98
155
|
"roles/logging.logWriter",
|
|
99
156
|
] as const;
|
|
100
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Predefined roles that hold `run.jobs.runWithOverrides`. Neither the
|
|
160
|
+
* upgrader nor the admin may hold any of them anywhere: with it, either
|
|
161
|
+
* could run the converge or install job — which run as caelo-provisioner —
|
|
162
|
+
* with its own arguments or environment (#883 review).
|
|
163
|
+
*/
|
|
164
|
+
export const RUN_WITH_OVERRIDES_ROLES = [
|
|
165
|
+
"roles/owner",
|
|
166
|
+
"roles/editor",
|
|
167
|
+
"roles/run.admin",
|
|
168
|
+
"roles/run.developer",
|
|
169
|
+
"roles/run.invoker",
|
|
170
|
+
"roles/run.jobsExecutorWithOverrides",
|
|
171
|
+
] as const;
|
|
172
|
+
|
|
173
|
+
/** A project-level grant an earlier setup gave the upgrader; removed on every setup. */
|
|
174
|
+
const LEGACY_UPGRADER_PROJECT_ROLES = ["roles/run.developer"] as const;
|
|
175
|
+
|
|
101
176
|
/**
|
|
102
177
|
* Roles the upgrader must never hold anywhere — they would let it change
|
|
103
178
|
* IAM, manage service accounts or secrets, or touch compute/projects.
|
|
104
179
|
* Prefix matches (`roles/compute.` = every compute role).
|
|
105
180
|
*/
|
|
106
181
|
export const UPGRADER_FORBIDDEN_ROLES = [
|
|
107
|
-
|
|
108
|
-
"roles/editor",
|
|
109
|
-
"roles/run.admin",
|
|
182
|
+
...RUN_WITH_OVERRIDES_ROLES,
|
|
110
183
|
"roles/iam.serviceAccountAdmin",
|
|
111
184
|
"roles/iam.serviceAccountKeyAdmin",
|
|
112
185
|
"roles/iam.securityAdmin",
|
|
@@ -124,8 +197,14 @@ export const UPGRADER_FORBIDDEN_ROLES = [
|
|
|
124
197
|
/** Database role migration 0267 creates for the job. */
|
|
125
198
|
export const UPGRADER_DB_ROLE = "platform_updater";
|
|
126
199
|
|
|
127
|
-
/**
|
|
128
|
-
|
|
200
|
+
/**
|
|
201
|
+
* The job's task timeout in minutes: enough for a backup + roll; the job
|
|
202
|
+
* ends within it. It is also the lease on the state lock the job takes
|
|
203
|
+
* (#815, state-lock-lease.ts): after it, Cloud Run has killed the run.
|
|
204
|
+
*/
|
|
205
|
+
export const UPGRADER_TASK_TIMEOUT_MINUTES = 60;
|
|
206
|
+
|
|
207
|
+
const JOB_TASK_TIMEOUT = `${UPGRADER_TASK_TIMEOUT_MINUTES}m`;
|
|
129
208
|
|
|
130
209
|
export interface UpgraderSetupTarget {
|
|
131
210
|
readonly projectId: string;
|
|
@@ -197,7 +276,7 @@ export async function ensureUpgraderJob(
|
|
|
197
276
|
}
|
|
198
277
|
|
|
199
278
|
// 1. The backup and ops-bucket-list roles.
|
|
200
|
-
for (const role of [UPGRADER_SQL_BACKUP_ROLE, UPGRADER_OPS_LIST_ROLE]) {
|
|
279
|
+
for (const role of [UPGRADER_RUN_ROLE, UPGRADER_SQL_BACKUP_ROLE, UPGRADER_OPS_LIST_ROLE]) {
|
|
201
280
|
const roleError = await ensureCustomRole(run, projectId, role);
|
|
202
281
|
if (roleError) return { ok: false, done, error: roleError };
|
|
203
282
|
done.push(`custom role ${role.roleId}`);
|
|
@@ -229,6 +308,7 @@ export async function ensureUpgraderJob(
|
|
|
229
308
|
// 3. Project-level roles.
|
|
230
309
|
for (const role of [
|
|
231
310
|
...UPGRADER_PROJECT_ROLES,
|
|
311
|
+
customRoleName(projectId, UPGRADER_RUN_ROLE.roleId),
|
|
232
312
|
customRoleName(projectId, UPGRADER_SQL_BACKUP_ROLE.roleId),
|
|
233
313
|
]) {
|
|
234
314
|
const r = await retry([
|
|
@@ -245,6 +325,34 @@ export async function ensureUpgraderJob(
|
|
|
245
325
|
done.push(`upgrader: ${role} on the project`);
|
|
246
326
|
}
|
|
247
327
|
|
|
328
|
+
// 3b. The project-wide roles/run.developer an earlier setup granted holds
|
|
329
|
+
// run.jobs.runWithOverrides on the provisioner's jobs: take it away.
|
|
330
|
+
// Only after the custom role above is bound, so a running upgrade
|
|
331
|
+
// never loses its Cloud Run rights in between.
|
|
332
|
+
const policy = await run(["projects", "get-iam-policy", projectId, "--format=json"]);
|
|
333
|
+
if (!policy.ok) return fail("read the project's IAM policy", policy);
|
|
334
|
+
for (const role of LEGACY_UPGRADER_PROJECT_ROLES) {
|
|
335
|
+
let held: boolean;
|
|
336
|
+
try {
|
|
337
|
+
held = projectPolicyHas(policy.stdout, role, member);
|
|
338
|
+
} catch (e) {
|
|
339
|
+
return { ok: false, done, error: `read the project's IAM policy: ${String(e)}` };
|
|
340
|
+
}
|
|
341
|
+
if (!held) continue;
|
|
342
|
+
const r = await retry([
|
|
343
|
+
"projects",
|
|
344
|
+
"remove-iam-policy-binding",
|
|
345
|
+
projectId,
|
|
346
|
+
`--member=${member}`,
|
|
347
|
+
`--role=${role}`,
|
|
348
|
+
"--all",
|
|
349
|
+
"--quiet",
|
|
350
|
+
"--format=none",
|
|
351
|
+
]);
|
|
352
|
+
if (!r.ok) return fail(`remove the upgrader's ${role}`, r);
|
|
353
|
+
done.push(`upgrader: removed ${role} on the project`);
|
|
354
|
+
}
|
|
355
|
+
|
|
248
356
|
// 4. Resource-level grants on single service accounts.
|
|
249
357
|
const onAccounts: { account: string; role: string; why: string }[] = [
|
|
250
358
|
{
|
|
@@ -373,6 +481,15 @@ export async function ensureUpgraderJob(
|
|
|
373
481
|
return { ok: true, done };
|
|
374
482
|
}
|
|
375
483
|
|
|
484
|
+
/** Whether a gcloud IAM policy (JSON) grants `role` to `member`. */
|
|
485
|
+
export function projectPolicyHas(policyJson: string, role: string, member: string): boolean {
|
|
486
|
+
// Throws on unreadable JSON: the caller reports it, never guesses "absent".
|
|
487
|
+
const policy = JSON.parse(policyJson.trim() || "{}") as {
|
|
488
|
+
bindings?: { role: string; members?: string[] }[];
|
|
489
|
+
};
|
|
490
|
+
return (policy.bindings ?? []).some((b) => b.role === role && (b.members ?? []).includes(member));
|
|
491
|
+
}
|
|
492
|
+
|
|
376
493
|
/**
|
|
377
494
|
* {@link ensureUpgraderJob}'s target, with the VPC read from the deployed
|
|
378
495
|
* admin service (the job reaches Cloud SQL's private IP the same way).
|
package/src/wizards/gcp-cost.ts
CHANGED
|
@@ -138,6 +138,14 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
|
|
|
138
138
|
? "Adaptive protection (ML-based bot mitigation)"
|
|
139
139
|
: "Free tier — rate limit + OWASP basic rules",
|
|
140
140
|
},
|
|
141
|
+
{
|
|
142
|
+
// #806 — the remote-MCP throttle (src/remote-mcp-edge.ts) is one
|
|
143
|
+
// more rule on the gateway's policy; Cloud Armor Standard bills
|
|
144
|
+
// each rule at about $1/mo.
|
|
145
|
+
name: "Cloud Armor rule: remote MCP throttle",
|
|
146
|
+
monthlyUsd: 1,
|
|
147
|
+
notes: "Per-IP throttle for /api/mcp/* (remote MCP clients)",
|
|
148
|
+
},
|
|
141
149
|
] satisfies CostLine[])
|
|
142
150
|
: ([
|
|
143
151
|
{
|
|
@@ -155,9 +163,10 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
|
|
|
155
163
|
notes: "~5 GB storage + low egress; growth roughly $0.02/GB",
|
|
156
164
|
},
|
|
157
165
|
{
|
|
158
|
-
name: "Secret Manager (
|
|
159
|
-
monthlyUsd: 0,
|
|
160
|
-
notes:
|
|
166
|
+
name: "Secret Manager (runtime secrets)",
|
|
167
|
+
monthlyUsd: 0.2,
|
|
168
|
+
notes:
|
|
169
|
+
"6 active versions free, then ~$0.06 each; remote MCP (#806) adds the OAuth client and its seal key",
|
|
161
170
|
},
|
|
162
171
|
{
|
|
163
172
|
name: "BigQuery edge log sink",
|
package/src/wizards/gcp.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { checkAdcImpersonation } from "../adc-impersonation.js";
|
|
|
36
36
|
import { ensureAuditJob, resolveAdminImage } from "../audit-job-setup.js";
|
|
37
37
|
import { CERT_WAIT_TIMEOUT_MS, lbCertProbe, waitForCert } from "../cert-wait.js";
|
|
38
38
|
import { redactCredentials } from "../checkpoint-secrets.js";
|
|
39
|
+
import { ensureConvergeJob } from "../converge-job-setup.js";
|
|
39
40
|
import { removeDefaultFirewallRules } from "../default-firewall.js";
|
|
40
41
|
import {
|
|
41
42
|
checkDeployedRuntimeEnv,
|
|
@@ -101,6 +102,7 @@ import {
|
|
|
101
102
|
} from "../provisioner-identity.js";
|
|
102
103
|
import { revokePublicRoleMemberships } from "../public-role-isolation.js";
|
|
103
104
|
import { decideRegion, detectCliRegion, installableRegions, suggestRegion } from "../regions.js";
|
|
105
|
+
import { ensureRemoteMcpEdge, ensureRemoteMcpIdentityEnv } from "../remote-mcp.js";
|
|
104
106
|
import type { RunMode } from "../run-mode.js";
|
|
105
107
|
import { selfCommand } from "../self-command.js";
|
|
106
108
|
import { ABSENT_DEFAULT_FIREWALL_RULES } from "../stack-contract.js";
|
|
@@ -115,6 +117,7 @@ import {
|
|
|
115
117
|
resolveBackend,
|
|
116
118
|
} from "../state-backend.js";
|
|
117
119
|
import { acquireStateLock, GcsStateStore } from "../state-store.js";
|
|
120
|
+
import { resolveGcpResourceName } from "../upgrade/gcp-lookup.js";
|
|
118
121
|
import {
|
|
119
122
|
releaseVersionForDigest,
|
|
120
123
|
resolveOpsImageRef,
|
|
@@ -464,6 +467,8 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
464
467
|
const releaseVersion = await releaseVersionForDigest(gcloud, "admin", imageDigests.admin);
|
|
465
468
|
const upgraderReady = await stepUpgraderJob(projectId, region, releaseVersion);
|
|
466
469
|
const keepSigner = upgraderReady || (await upgraderExists(projectId));
|
|
470
|
+
// === 11.45. The converge job: releases that change the cloud setup (#819) ===
|
|
471
|
+
const convergeReady = await stepConvergeJob(projectId, region, releaseVersion);
|
|
467
472
|
|
|
468
473
|
// === 11.5. Operator access: IAP follows the Caelo user list ===
|
|
469
474
|
await stepOperatorAccessSync(
|
|
@@ -474,11 +479,18 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
474
479
|
keepSigner ? [`serviceAccount:${upgraderServiceAccountEmail(projectId, GCP_STACK_ENV)}`] : [],
|
|
475
480
|
);
|
|
476
481
|
|
|
482
|
+
// === 11.6. Remote MCP (#806): LB rules (gcp) + the identity env ===
|
|
483
|
+
// After `pulumi up` on every pass: the identity env names services Pulumi
|
|
484
|
+
// generated, so the stack cannot declare it, and an `up` that changed a
|
|
485
|
+
// service's template dropped it (remote-mcp.ts).
|
|
486
|
+
const remoteMcpReady = await stepRemoteMcp(projectId, region, provider, domain);
|
|
487
|
+
|
|
477
488
|
// A fresh install ran every converge step above, so it is at this CLI's
|
|
478
|
-
// infrastructure contract — recorded only when the upgrader
|
|
479
|
-
// (the
|
|
480
|
-
//
|
|
481
|
-
|
|
489
|
+
// infrastructure contract — recorded only when the upgrader and the
|
|
490
|
+
// converge job are in place (the steps contract bumps 2 and 9 added) and
|
|
491
|
+
// remote MCP's steps (#806, bump 10) succeeded. With the release version,
|
|
492
|
+
// the admin can install the next update itself.
|
|
493
|
+
if (upgraderReady && convergeReady && remoteMcpReady) {
|
|
482
494
|
const version = releaseVersion;
|
|
483
495
|
recordImageDigests(installId, imageDigests, version);
|
|
484
496
|
recordInfraContractVersion(installId, INFRA_CONTRACT_VERSION);
|
|
@@ -981,6 +993,63 @@ async function stepUpgraderJob(
|
|
|
981
993
|
return true;
|
|
982
994
|
}
|
|
983
995
|
|
|
996
|
+
/**
|
|
997
|
+
* Set up the converge job (converge-job-setup.ts, #819): what the admin's
|
|
998
|
+
* "Install update" starts for a release that changes the cloud setup. Same
|
|
999
|
+
* shape as {@link stepUpgraderJob}: after migrations (its database login
|
|
1000
|
+
* gets migration 0267's role), only a verified ops image, and a failure
|
|
1001
|
+
* only warns — such releases then install from a terminal until a later
|
|
1002
|
+
* `upgrade` sets the job up. Idempotent.
|
|
1003
|
+
*
|
|
1004
|
+
* @returns whether the job is ready
|
|
1005
|
+
*/
|
|
1006
|
+
async function stepConvergeJob(
|
|
1007
|
+
projectId: string,
|
|
1008
|
+
region: string,
|
|
1009
|
+
releaseVersion: string | null,
|
|
1010
|
+
): Promise<boolean> {
|
|
1011
|
+
const opsImageRef = releaseVersion ? await resolveOpsImageRef(releaseVersion) : null;
|
|
1012
|
+
const digest = opsImageRef ? /@(sha256:[0-9a-f]{64})$/.exec(opsImageRef)?.[1] : undefined;
|
|
1013
|
+
// The job runs as the provisioner: only a verified image, ever.
|
|
1014
|
+
if (digest && !(await verifyCosignAll([{ slug: "ops", digest }]))) {
|
|
1015
|
+
log.warn(
|
|
1016
|
+
`The converge job is not set up: its image's signature did not verify. The install works; ${bold(selfCommand("upgrade"))} sets it up later.`,
|
|
1017
|
+
);
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
const s = spinner();
|
|
1021
|
+
s.start("Setting up the converge job (cloud-setup updates from the admin)...");
|
|
1022
|
+
const resolved = opsImageRef
|
|
1023
|
+
? await resolveUpgraderTarget({ projectId, region, env: GCP_STACK_ENV, opsImageRef })
|
|
1024
|
+
: {
|
|
1025
|
+
ok: false as const,
|
|
1026
|
+
error: releaseVersion
|
|
1027
|
+
? `release ${releaseVersion} has no caelo-ops image in the registry`
|
|
1028
|
+
: "the release this install runs could not be told from its images",
|
|
1029
|
+
};
|
|
1030
|
+
const r = resolved.ok
|
|
1031
|
+
? await ensureConvergeJob(resolved.target)
|
|
1032
|
+
: { ok: false as const, done: [], error: resolved.error };
|
|
1033
|
+
if (!r.ok && r.reason === "no-provisioner") {
|
|
1034
|
+
// #883 review — not needed for the contract: such an install installs
|
|
1035
|
+
// cloud-setup releases from a terminal, and its admin says so.
|
|
1036
|
+
s.stop(yellow("No converge job: this install has no caelo-provisioner to run it as"));
|
|
1037
|
+
log.info(
|
|
1038
|
+
"Releases that change the cloud setup install from a terminal; Settings → Updates says so when one comes.",
|
|
1039
|
+
);
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
if (!r.ok) {
|
|
1043
|
+
s.stop(yellow(`Converge job not set up: ${r.error}`));
|
|
1044
|
+
log.warn(
|
|
1045
|
+
`The install works; releases that change the cloud setup install from a terminal until ${bold(selfCommand("upgrade"))} sets the job up.`,
|
|
1046
|
+
);
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
s.stop(green(`Converge job ready (${r.done.length} steps)`));
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
984
1053
|
/**
|
|
985
1054
|
* Set up the quality-audit job (audit-job-setup.ts) on the deployed admin's
|
|
986
1055
|
* image. A failure stops the wizard: without the job every Stage audit
|
|
@@ -1064,6 +1133,58 @@ async function stepOperatorAccessSync(
|
|
|
1064
1133
|
s.stop(green(`Operator-access sync job ready (${r.done.length} steps)`));
|
|
1065
1134
|
}
|
|
1066
1135
|
|
|
1136
|
+
/**
|
|
1137
|
+
* #806 — the CLI's part of remote MCP (remote-mcp.ts): on `gcp` the load
|
|
1138
|
+
* balancer's discovery routes and Cloud Armor rule, on both providers the
|
|
1139
|
+
* env vars naming the admin URL, the broker's public URL and the IAP
|
|
1140
|
+
* audience. Remote MCP stays off until the Owner sets it up under Settings →
|
|
1141
|
+
* MCP, so a failure does not stop the install: it warns, and the next pass
|
|
1142
|
+
* or `upgrade` retries. Idempotent.
|
|
1143
|
+
*
|
|
1144
|
+
* @returns whether every part is in place
|
|
1145
|
+
*/
|
|
1146
|
+
async function stepRemoteMcp(
|
|
1147
|
+
projectId: string,
|
|
1148
|
+
region: string,
|
|
1149
|
+
provider: "gcp" | "gcp-firebase",
|
|
1150
|
+
domain: string,
|
|
1151
|
+
): Promise<boolean> {
|
|
1152
|
+
const s = spinner();
|
|
1153
|
+
s.start(
|
|
1154
|
+
"Preparing remote MCP (ChatGPT / claude.ai connectors; off until the Owner enables it)...",
|
|
1155
|
+
);
|
|
1156
|
+
const name = (slug: "admin" | "gateway") =>
|
|
1157
|
+
resolveGcpResourceName(
|
|
1158
|
+
gcloud,
|
|
1159
|
+
"run-service",
|
|
1160
|
+
`caelo-${GCP_STACK_ENV}-${slug}`,
|
|
1161
|
+
projectId,
|
|
1162
|
+
region,
|
|
1163
|
+
);
|
|
1164
|
+
const [admin, gateway] = [await name("admin"), await name("gateway")];
|
|
1165
|
+
const failed = (error: string) => {
|
|
1166
|
+
s.stop(yellow(`Remote MCP not prepared: ${error}`));
|
|
1167
|
+
log.warn(
|
|
1168
|
+
`The install works; Settings → MCP reports remote MCP as unavailable until ${bold(selfCommand("upgrade"))} prepares it.`,
|
|
1169
|
+
);
|
|
1170
|
+
return false;
|
|
1171
|
+
};
|
|
1172
|
+
if (!admin || !gateway) return failed("the admin or gateway Cloud Run service was not found");
|
|
1173
|
+
const edge = await ensureRemoteMcpEdge({ provider, projectId, env: GCP_STACK_ENV, domain });
|
|
1174
|
+
if (!edge.ok) return failed(edge.error);
|
|
1175
|
+
const env = await ensureRemoteMcpIdentityEnv({
|
|
1176
|
+
provider,
|
|
1177
|
+
projectId,
|
|
1178
|
+
region,
|
|
1179
|
+
env: GCP_STACK_ENV,
|
|
1180
|
+
domain,
|
|
1181
|
+
services: { admin, gateway },
|
|
1182
|
+
});
|
|
1183
|
+
if (!env.ok) return failed(env.error);
|
|
1184
|
+
s.stop(green(`Remote MCP prepared (${env.identity.mcpPublicUrl})`));
|
|
1185
|
+
return true;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1067
1188
|
/**
|
|
1068
1189
|
* Delete the firewall rules GCP pre-populates on the `default` network
|
|
1069
1190
|
* (SSH, RDP, ICMP, internal; default-firewall.ts). A customised rule of the
|
package/stacks/aws/index.ts
CHANGED
|
@@ -357,10 +357,20 @@ new aws.cloudwatch.LogDelivery(
|
|
|
357
357
|
// exists the admin records every Lighthouse audit as failed with that gap
|
|
358
358
|
// (quality/lighthouse-runner-select.ts) instead of measuring on its own
|
|
359
359
|
// shared CPU. Nothing is staged on AWS before #749 either.
|
|
360
|
+
//
|
|
361
|
+
// FIXME(issue #696): no in-cloud upgrader or converge job (#819) yet. On
|
|
362
|
+
// GCP Settings → Updates hands app-roll releases to the upgrader job and
|
|
363
|
+
// cloud-setup releases to the converge job; on AWS the admin refuses a
|
|
364
|
+
// cloud-setup release with the CLI command that installs it (admin-core
|
|
365
|
+
// platform-updates/update-route.ts) until this adapter gains both jobs.
|
|
360
366
|
const cluster = new aws.ecs.Cluster(`${namePrefix}-ecs`, {
|
|
361
367
|
settings: [{ name: "containerInsights", value: "enabled" }],
|
|
362
368
|
});
|
|
363
369
|
|
|
370
|
+
// #806 — no remote MCP on AWS: it rides Google IAP (gcp stacks only). The
|
|
371
|
+
// gateway task definition, once it exists, sets none of its env, so /api/mcp/*
|
|
372
|
+
// answers 503 remote_mcp_not_configured and the admin reports it unavailable.
|
|
373
|
+
//
|
|
364
374
|
// One task role per service (#635): the gateway's may read only the
|
|
365
375
|
// public_role password, never a secret that unlocks a cms_admin login
|
|
366
376
|
// (src/aws-task-roles.ts AWS_SERVICE_SECRETS, asserted by
|
package/stacks/azure/index.ts
CHANGED
|
@@ -339,6 +339,12 @@ const logWorkspace = new azure.operationalinsights.Workspace(`${namePrefix}-logs
|
|
|
339
339
|
// audit as failed with that gap (quality/lighthouse-runner-select.ts)
|
|
340
340
|
// instead of measuring on its own shared CPU. Nothing is staged on Azure
|
|
341
341
|
// before #750 either.
|
|
342
|
+
//
|
|
343
|
+
// FIXME(issue #696): no in-cloud upgrader or converge job (#819) yet. On
|
|
344
|
+
// GCP Settings → Updates hands app-roll releases to the upgrader job and
|
|
345
|
+
// cloud-setup releases to the converge job; on Azure the admin refuses a
|
|
346
|
+
// cloud-setup release with the CLI command that installs it (admin-core
|
|
347
|
+
// platform-updates/update-route.ts) until this adapter gains both jobs.
|
|
342
348
|
const cappEnv = new azure.app.ManagedEnvironment(`${namePrefix}-capp-env`, {
|
|
343
349
|
resourceGroupName: rg.name,
|
|
344
350
|
environmentName: `${namePrefix}-capp-env`,
|
|
@@ -430,6 +436,9 @@ const _gatewayApp = containerApp({
|
|
|
430
436
|
serviceName: "gateway",
|
|
431
437
|
databaseEnv: [{ name: "PUBLIC_DATABASE_URL", value: publicDatabaseUrl }],
|
|
432
438
|
// Which X-Forwarded-For entry is the visitor (api-gateway client-ip.ts).
|
|
439
|
+
// #806 — no remote-MCP env: remote MCP rides Google IAP (gcp stacks
|
|
440
|
+
// only); unset, the gateway answers /api/mcp/* with 503
|
|
441
|
+
// remote_mcp_not_configured and the admin reports it unavailable.
|
|
433
442
|
extraEnv: [{ name: TRUSTED_PROXY_HOPS_ENV, value: String(GATEWAY_TRUSTED_PROXY_HOPS.azure) }],
|
|
434
443
|
});
|
|
435
444
|
const _orchestratorApp = containerApp({
|
package/stacks/gcp/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
staticPublisherServiceAccountId,
|
|
32
32
|
} from "../../dist/gcp-names.js";
|
|
33
33
|
import { createSiteUrlMap } from "../../dist/gcp-url-map.js";
|
|
34
|
+
import { mcpArmorRuleArgs } from "../../dist/remote-mcp-edge.js";
|
|
34
35
|
import {
|
|
35
36
|
ADMIN_MEMORY_DEFAULT,
|
|
36
37
|
adminEnvContract,
|
|
@@ -40,10 +41,12 @@ import {
|
|
|
40
41
|
type CloudRunSlug,
|
|
41
42
|
databaseUrls,
|
|
42
43
|
defaultStagingHost,
|
|
44
|
+
GATEWAY_TIMEOUT_SECONDS,
|
|
43
45
|
gatewayEnvContract,
|
|
44
46
|
type RuntimeSecret,
|
|
45
47
|
runtimeSecretBindings,
|
|
46
48
|
STATIC_CDN_POLICY,
|
|
49
|
+
secretFileVolumesTemplate,
|
|
47
50
|
} from "../../dist/stack-contract.js";
|
|
48
51
|
|
|
49
52
|
const cfg = new pulumi.Config();
|
|
@@ -251,6 +254,10 @@ const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
|
|
|
251
254
|
"tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
|
|
252
255
|
"staging-gate-secret": gcpSecretId(env, "staging-gate-secret"),
|
|
253
256
|
"public-role-password": gcpSecretId(env, "public-role-password"),
|
|
257
|
+
// #806 — remote MCP: the Owner-set Google OAuth client (a placeholder
|
|
258
|
+
// until then) and the broker's seal key, both CLI-generated.
|
|
259
|
+
"mcp-oauth-client": gcpSecretId(env, "mcp-oauth-client"),
|
|
260
|
+
"mcp-oauth-seal-key": gcpSecretId(env, "mcp-oauth-seal-key"),
|
|
254
261
|
};
|
|
255
262
|
|
|
256
263
|
// #613 — public_role's own password (CLI-generated, stack-contract.ts
|
|
@@ -472,6 +479,20 @@ for (const binding of runtimeSecretBindings()) {
|
|
|
472
479
|
);
|
|
473
480
|
}
|
|
474
481
|
|
|
482
|
+
// #806 — the Owner's remote-MCP setup (Settings → MCP → Remote) stores the
|
|
483
|
+
// Google OAuth client in this one secret. Add-version only: the admin can
|
|
484
|
+
// replace the client the gateway reads, never read it back or touch any
|
|
485
|
+
// other secret (stack-contract.ts GCP_COMMON says why).
|
|
486
|
+
new gcp.secretmanager.SecretIamMember(
|
|
487
|
+
`${namePrefix}-admin-mcp-oauth-client-version-adder`,
|
|
488
|
+
{
|
|
489
|
+
secretId: runtimeSecretIds["mcp-oauth-client"],
|
|
490
|
+
role: "roles/secretmanager.secretVersionAdder",
|
|
491
|
+
member: pulumi.interpolate`serviceAccount:${runSa.email}`,
|
|
492
|
+
},
|
|
493
|
+
opts,
|
|
494
|
+
);
|
|
495
|
+
|
|
475
496
|
new gcp.storage.BucketIAMMember(
|
|
476
497
|
`${namePrefix}-media-rw`,
|
|
477
498
|
{
|
|
@@ -627,8 +648,8 @@ interface CloudRunArgs {
|
|
|
627
648
|
* Per-request timeout. Cloud Run's default is 300s (5min), which is too
|
|
628
649
|
* short for the admin's SSE chat-stream — a multi-tool compose-page session
|
|
629
650
|
* regularly runs 5–15 min. Pass an explicit "<n>s" Pulumi-style duration
|
|
630
|
-
* string.
|
|
631
|
-
*
|
|
651
|
+
* string. The gateway gets the same (#806): it proxies remote-MCP chat
|
|
652
|
+
* streams to the admin.
|
|
632
653
|
*/
|
|
633
654
|
readonly timeout?: string;
|
|
634
655
|
/**
|
|
@@ -689,16 +710,23 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
|
689
710
|
},
|
|
690
711
|
timeout: args.timeout,
|
|
691
712
|
...(args.mediaVolume
|
|
692
|
-
? {
|
|
693
|
-
executionEnvironment: args.mediaVolume.executionEnvironment,
|
|
694
|
-
volumes: [...args.mediaVolume.volumes],
|
|
695
|
-
}
|
|
713
|
+
? { executionEnvironment: args.mediaVolume.executionEnvironment }
|
|
696
714
|
: {}),
|
|
715
|
+
// The admin's media bucket, plus each service's secret files (#806:
|
|
716
|
+
// the gateway's remote-MCP OAuth client) — both from the contract
|
|
717
|
+
// upgrade applies to existing installs.
|
|
718
|
+
volumes: [
|
|
719
|
+
...(args.mediaVolume?.volumes ?? []),
|
|
720
|
+
...secretFileVolumesTemplate(args.serviceName, env).volumes,
|
|
721
|
+
],
|
|
697
722
|
containers: [
|
|
698
723
|
{
|
|
699
724
|
image: imageTag(args.serviceName),
|
|
700
725
|
envs: [...args.contractEnv],
|
|
701
|
-
|
|
726
|
+
volumeMounts: [
|
|
727
|
+
...(args.mediaVolume?.volumeMounts ?? []),
|
|
728
|
+
...secretFileVolumesTemplate(args.serviceName, env).volumeMounts,
|
|
729
|
+
],
|
|
702
730
|
resources: { limits: { cpu: "1", memory: args.memory } },
|
|
703
731
|
},
|
|
704
732
|
],
|
|
@@ -749,6 +777,11 @@ const gatewaySvc = cloudRunService({
|
|
|
749
777
|
minInstances: gatewayMinInstances,
|
|
750
778
|
maxInstances: 100,
|
|
751
779
|
memory: "512Mi",
|
|
780
|
+
// #806 — remote MCP: chat turns stream through the gateway for minutes,
|
|
781
|
+
// so it gets the admin's 3600 s instead of Cloud Run's 300 s default
|
|
782
|
+
// (stack-contract.ts GATEWAY_TIMEOUT_SECONDS). The LB in front imposes no
|
|
783
|
+
// shorter limit on serverless NEG backends.
|
|
784
|
+
timeout: `${GATEWAY_TIMEOUT_SECONDS}s`,
|
|
752
785
|
// public_role on cms_public only: no cms_admin login of any role
|
|
753
786
|
// (#613, #635, CLAUDE.md §2).
|
|
754
787
|
contractEnv: gatewayEnvContract(envContractInputs),
|
|
@@ -784,6 +817,12 @@ const wafPolicy = new gcp.compute.SecurityPolicy(
|
|
|
784
817
|
{
|
|
785
818
|
description: `Caelo ${env} Cloud Armor — rate limit + OWASP`,
|
|
786
819
|
rules: [
|
|
820
|
+
// #806 — remote MCP (/api/mcp/*): a per-IP throttle sized for agent
|
|
821
|
+
// traffic behind shared connector IPs, evaluated first; below it the
|
|
822
|
+
// rule allows, so the OWASP packs (MCP bodies carry HTML) and the
|
|
823
|
+
// general ban below do not apply there. src/remote-mcp-edge.ts says
|
|
824
|
+
// why; `cms-provision upgrade` adds the same rule to existing installs.
|
|
825
|
+
mcpArmorRuleArgs(),
|
|
787
826
|
// Rate limit per source IP — 100 req / min sliding window.
|
|
788
827
|
{
|
|
789
828
|
action: "rate_based_ban",
|
|
@@ -46,9 +46,11 @@ import {
|
|
|
46
46
|
type CloudRunSlug,
|
|
47
47
|
databaseUrls,
|
|
48
48
|
defaultStagingHost,
|
|
49
|
+
GATEWAY_TIMEOUT_SECONDS,
|
|
49
50
|
gatewayEnvContract,
|
|
50
51
|
type RuntimeSecret,
|
|
51
52
|
runtimeSecretBindings,
|
|
53
|
+
secretFileVolumesTemplate,
|
|
52
54
|
} from "../../dist/stack-contract.js";
|
|
53
55
|
|
|
54
56
|
const cfg = new pulumi.Config();
|
|
@@ -224,6 +226,10 @@ const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
|
|
|
224
226
|
"tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
|
|
225
227
|
"staging-gate-secret": gcpSecretId(env, "staging-gate-secret"),
|
|
226
228
|
"public-role-password": gcpSecretId(env, "public-role-password"),
|
|
229
|
+
// #806 — remote MCP: the Owner-set Google OAuth client (a placeholder
|
|
230
|
+
// until then) and the broker's seal key, both CLI-generated.
|
|
231
|
+
"mcp-oauth-client": gcpSecretId(env, "mcp-oauth-client"),
|
|
232
|
+
"mcp-oauth-seal-key": gcpSecretId(env, "mcp-oauth-seal-key"),
|
|
227
233
|
};
|
|
228
234
|
|
|
229
235
|
// #613 — public_role's own password (CLI-generated, stack-contract.ts
|
|
@@ -376,6 +382,20 @@ for (const binding of runtimeSecretBindings()) {
|
|
|
376
382
|
);
|
|
377
383
|
}
|
|
378
384
|
|
|
385
|
+
// #806 — the Owner's remote-MCP setup (Settings → MCP → Remote) stores the
|
|
386
|
+
// Google OAuth client in this one secret. Add-version only: the admin can
|
|
387
|
+
// replace the client the gateway reads, never read it back or touch any
|
|
388
|
+
// other secret (stack-contract.ts GCP_COMMON says why).
|
|
389
|
+
new gcp.secretmanager.SecretIamMember(
|
|
390
|
+
`${namePrefix}-admin-mcp-oauth-client-version-adder`,
|
|
391
|
+
{
|
|
392
|
+
secretId: runtimeSecretIds["mcp-oauth-client"],
|
|
393
|
+
role: "roles/secretmanager.secretVersionAdder",
|
|
394
|
+
member: pulumi.interpolate`serviceAccount:${runSa.email}`,
|
|
395
|
+
},
|
|
396
|
+
opts,
|
|
397
|
+
);
|
|
398
|
+
|
|
379
399
|
new gcp.storage.BucketIAMMember(
|
|
380
400
|
`${namePrefix}-media-rw`,
|
|
381
401
|
{
|
|
@@ -551,16 +571,23 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
|
551
571
|
},
|
|
552
572
|
timeout: args.timeout,
|
|
553
573
|
...(args.mediaVolume
|
|
554
|
-
? {
|
|
555
|
-
executionEnvironment: args.mediaVolume.executionEnvironment,
|
|
556
|
-
volumes: [...args.mediaVolume.volumes],
|
|
557
|
-
}
|
|
574
|
+
? { executionEnvironment: args.mediaVolume.executionEnvironment }
|
|
558
575
|
: {}),
|
|
576
|
+
// The admin's media bucket, plus each service's secret files (#806:
|
|
577
|
+
// the gateway's remote-MCP OAuth client) — both from the contract
|
|
578
|
+
// upgrade applies to existing installs.
|
|
579
|
+
volumes: [
|
|
580
|
+
...(args.mediaVolume?.volumes ?? []),
|
|
581
|
+
...secretFileVolumesTemplate(args.serviceName, env).volumes,
|
|
582
|
+
],
|
|
559
583
|
containers: [
|
|
560
584
|
{
|
|
561
585
|
image: imageRef(args.serviceName),
|
|
562
586
|
envs: [...args.contractEnv],
|
|
563
|
-
|
|
587
|
+
volumeMounts: [
|
|
588
|
+
...(args.mediaVolume?.volumeMounts ?? []),
|
|
589
|
+
...secretFileVolumesTemplate(args.serviceName, env).volumeMounts,
|
|
590
|
+
],
|
|
564
591
|
resources: { limits: { cpu: "1", memory: args.memory } },
|
|
565
592
|
},
|
|
566
593
|
],
|
|
@@ -613,7 +640,11 @@ const gatewaySvc = cloudRunService({
|
|
|
613
640
|
minInstances: gatewayMinInstances,
|
|
614
641
|
maxInstances: 100,
|
|
615
642
|
memory: "512Mi",
|
|
616
|
-
|
|
643
|
+
// #806 — remote MCP: chat turns stream through the gateway for minutes
|
|
644
|
+
// (stack-contract.ts GATEWAY_TIMEOUT_SECONDS). The Firebase Hosting
|
|
645
|
+
// rewrite keeps its own 60 s cut; MCP clients use the gateway's Cloud Run
|
|
646
|
+
// URL directly.
|
|
647
|
+
timeout: `${GATEWAY_TIMEOUT_SECONDS}s`,
|
|
617
648
|
// #650 — all traffic: the gateway also serves staging.<domain> through
|
|
618
649
|
// a Cloud Run domain mapping (`cms-provision staging-domain enable`),
|
|
619
650
|
// and Cloud Run refuses domain-mapped requests under an internal-only
|