cloudfall 0.2.0__py3-none-any.whl

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 (165) hide show
  1. cloudfall/__init__.py +19 -0
  2. cloudfall/__main__.py +5 -0
  3. cloudfall/_bundled/schemas/v1/alert-rule.schema.json +63 -0
  4. cloudfall/_bundled/schemas/v1/application.schema.json +60 -0
  5. cloudfall/_bundled/schemas/v1/artifact.schema.json +71 -0
  6. cloudfall/_bundled/schemas/v1/backup-receipt.schema.json +58 -0
  7. cloudfall/_bundled/schemas/v1/common.schema.json +115 -0
  8. cloudfall/_bundled/schemas/v1/component.schema.json +234 -0
  9. cloudfall/_bundled/schemas/v1/deployment-receipt.schema.json +53 -0
  10. cloudfall/_bundled/schemas/v1/domain.schema.json +208 -0
  11. cloudfall/_bundled/schemas/v1/environment-receipt.schema.json +55 -0
  12. cloudfall/_bundled/schemas/v1/logging-stack.schema.json +473 -0
  13. cloudfall/_bundled/schemas/v1/observed-domain.schema.json +158 -0
  14. cloudfall/_bundled/schemas/v1/observed-server.schema.json +627 -0
  15. cloudfall/_bundled/schemas/v1/operator-policy.schema.json +97 -0
  16. cloudfall/_bundled/schemas/v1/operator-proposal.schema.json +253 -0
  17. cloudfall/_bundled/schemas/v1/release-receipt.schema.json +66 -0
  18. cloudfall/_bundled/schemas/v1/server-type.schema.json +289 -0
  19. cloudfall/_bundled/schemas/v1/server.schema.json +146 -0
  20. cloudfall/_bundled/schemas/v1/service.schema.json +222 -0
  21. cloudfall/_bundled/schemas/v1/ssh-public-key.schema.json +55 -0
  22. cloudfall/agent_tools.py +868 -0
  23. cloudfall/arguments.py +92 -0
  24. cloudfall/audit.py +905 -0
  25. cloudfall/authoring.py +341 -0
  26. cloudfall/cli.py +1748 -0
  27. cloudfall/commands.py +257 -0
  28. cloudfall/cutover.py +370 -0
  29. cloudfall/dashboard.py +407 -0
  30. cloudfall/dashboard_server.py +201 -0
  31. cloudfall/domain.py +1157 -0
  32. cloudfall/importer.py +906 -0
  33. cloudfall/inventory.py +2116 -0
  34. cloudfall/lifecycle.py +874 -0
  35. cloudfall/mcp_server.py +704 -0
  36. cloudfall/migrate.py +730 -0
  37. cloudfall/observation.py +143 -0
  38. cloudfall/operations.py +1228 -0
  39. cloudfall/operator.py +1099 -0
  40. cloudfall/project.py +841 -0
  41. cloudfall/py.typed +0 -0
  42. cloudfall/render_api.py +338 -0
  43. cloudfall/resources.py +67 -0
  44. cloudfall/secrets.py +262 -0
  45. cloudfall/service_evidence.py +659 -0
  46. cloudfall/validation.py +1340 -0
  47. cloudfall-0.2.0.dist-info/METADATA +408 -0
  48. cloudfall-0.2.0.dist-info/RECORD +165 -0
  49. cloudfall-0.2.0.dist-info/WHEEL +4 -0
  50. cloudfall-0.2.0.dist-info/entry_points.txt +4 -0
  51. cloudfall-0.2.0.dist-info/licenses/LICENSE +661 -0
  52. cloudfall_engine/__init__.py +5 -0
  53. cloudfall_engine/__main__.py +5 -0
  54. cloudfall_engine/_bundled/ansible/ansible.cfg +7 -0
  55. cloudfall_engine/_bundled/ansible/playbooks/backup.yml +86 -0
  56. cloudfall_engine/_bundled/ansible/playbooks/baseline.yml +35 -0
  57. cloudfall_engine/_bundled/ansible/playbooks/bootstrap.yml +24 -0
  58. cloudfall_engine/_bundled/ansible/playbooks/data.yml +28 -0
  59. cloudfall_engine/_bundled/ansible/playbooks/deploy.yml +35 -0
  60. cloudfall_engine/_bundled/ansible/playbooks/domains.yml +21 -0
  61. cloudfall_engine/_bundled/ansible/playbooks/health.yml +31 -0
  62. cloudfall_engine/_bundled/ansible/playbooks/inspect.yml +15 -0
  63. cloudfall_engine/_bundled/ansible/playbooks/logging.yml +35 -0
  64. cloudfall_engine/_bundled/ansible/playbooks/restart.yml +31 -0
  65. cloudfall_engine/_bundled/ansible/playbooks/rollback.yml +33 -0
  66. cloudfall_engine/_bundled/ansible/playbooks/services.yml +37 -0
  67. cloudfall_engine/_bundled/ansible/playbooks/time.yml +9 -0
  68. cloudfall_engine/_bundled/ansible/roles/cloudfall_access/defaults/main.yml +2 -0
  69. cloudfall_engine/_bundled/ansible/roles/cloudfall_access/handlers/main.yml +5 -0
  70. cloudfall_engine/_bundled/ansible/roles/cloudfall_access/meta/argument_specs.yml +34 -0
  71. cloudfall_engine/_bundled/ansible/roles/cloudfall_access/meta/main.yml +13 -0
  72. cloudfall_engine/_bundled/ansible/roles/cloudfall_access/tasks/main.yml +59 -0
  73. cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/defaults/main.yml +11 -0
  74. cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +52 -0
  75. cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/meta/main.yml +13 -0
  76. cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/tasks/main.yml +99 -0
  77. cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/defaults/main.yml +15 -0
  78. cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +36 -0
  79. cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/meta/main.yml +13 -0
  80. cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/tasks/main.yml +201 -0
  81. cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/defaults/main.yml +10 -0
  82. cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +125 -0
  83. cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/meta/main.yml +13 -0
  84. cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/health.yml +3 -0
  85. cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +37 -0
  86. cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/main.yml +285 -0
  87. cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/restart.yml +17 -0
  88. cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/rollback.yml +52 -0
  89. cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/templates/component.service.j2 +24 -0
  90. cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/defaults/main.yml +2 -0
  91. cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/handlers/main.yml +5 -0
  92. cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +33 -0
  93. cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/meta/main.yml +13 -0
  94. cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/tasks/main.yml +51 -0
  95. cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +31 -0
  96. cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/defaults/main.yml +6 -0
  97. cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +24 -0
  98. cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/meta/main.yml +13 -0
  99. cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/tasks/main.yml +643 -0
  100. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/defaults/main.yml +6 -0
  101. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/handlers/main.yml +29 -0
  102. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +227 -0
  103. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/meta/main.yml +11 -0
  104. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/tasks/main.yml +487 -0
  105. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +2 -0
  106. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +33 -0
  107. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +20 -0
  108. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +11 -0
  109. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +3 -0
  110. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +49 -0
  111. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +45 -0
  112. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +11 -0
  113. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +2 -0
  114. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +20 -0
  115. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +24 -0
  116. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/defaults/main.yml +7 -0
  117. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/handlers/main.yml +6 -0
  118. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +122 -0
  119. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/meta/main.yml +11 -0
  120. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/tasks/main.yml +224 -0
  121. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +3 -0
  122. cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +127 -0
  123. cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/defaults/main.yml +4 -0
  124. cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/handlers/main.yml +5 -0
  125. cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +69 -0
  126. cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/meta/main.yml +13 -0
  127. cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/tasks/main.yml +212 -0
  128. cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +54 -0
  129. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/defaults/main.yml +6 -0
  130. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/handlers/main.yml +9 -0
  131. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +91 -0
  132. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/meta/main.yml +13 -0
  133. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/tasks/main.yml +295 -0
  134. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +11 -0
  135. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +17 -0
  136. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +10 -0
  137. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +3 -0
  138. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +11 -0
  139. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +27 -0
  140. cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +10 -0
  141. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/defaults/main.yml +2 -0
  142. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/handlers/main.yml +6 -0
  143. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/meta/argument_specs.yml +68 -0
  144. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/meta/main.yml +13 -0
  145. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/tasks/main.yml +148 -0
  146. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/backup.service.j2 +11 -0
  147. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +14 -0
  148. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +10 -0
  149. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +6 -0
  150. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +11 -0
  151. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +15 -0
  152. cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +10 -0
  153. cloudfall_engine/_bundled/ansible/roles/cloudfall_time/defaults/main.yml +2 -0
  154. cloudfall_engine/_bundled/ansible/roles/cloudfall_time/meta/argument_specs.yml +10 -0
  155. cloudfall_engine/_bundled/ansible/roles/cloudfall_time/meta/main.yml +13 -0
  156. cloudfall_engine/_bundled/ansible/roles/cloudfall_time/tasks/main.yml +119 -0
  157. cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +3 -0
  158. cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +8 -0
  159. cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +13 -0
  160. cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +37 -0
  161. cloudfall_engine/ansible_inventory.py +253 -0
  162. cloudfall_engine/artifact.py +258 -0
  163. cloudfall_engine/cli.py +273 -0
  164. cloudfall_engine/playbook.py +175 -0
  165. cloudfall_engine/py.typed +0 -0
@@ -0,0 +1,704 @@
1
+ """MCP server exposing the Cloudfall agent toolset over stdio.
2
+
3
+ Requires the ``cloudfall[mcp]`` extra. Read-only evidence tools carry a
4
+ read-only annotation; anything that changes servers is annotated
5
+ destructive and demands the toolset's explicit confirmation handshake.
6
+ The fleet-declaring tools (``add_ssh_key``, ``add_server_type``,
7
+ ``add_server``) write resources into the project and are neither: they
8
+ never touch a server and never overwrite an existing resource.
9
+
10
+ ``cloudfall init`` is deliberately CLI-only. The server is started inside
11
+ a project (``--project``, ``CLOUDFALL_PROJECT``, or the current directory)
12
+ and refuses to start outside one, so by the time an agent can call a tool
13
+ the project already exists; a tool that lays out a new project elsewhere
14
+ would escape the directory every other tool is confined to.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import sys
22
+ from pathlib import Path
23
+ from typing import TYPE_CHECKING
24
+
25
+ from mcp.server.mcpserver import MCPServer
26
+ from mcp.types import ToolAnnotations
27
+
28
+ from cloudfall.agent_tools import AgentConfig, AgentToolset
29
+ from cloudfall.arguments import (
30
+ StrictArgumentParser,
31
+ parse_arguments,
32
+ project_path_argument,
33
+ )
34
+ from cloudfall.project import (
35
+ PROJECT_DIRECTORY_VARIABLE,
36
+ ProjectError,
37
+ project_context,
38
+ )
39
+ from cloudfall.resources import default_engine_directory, default_schema_directory
40
+
41
+ if TYPE_CHECKING:
42
+ import argparse
43
+ from collections.abc import Callable, Sequence
44
+
45
+ _Registration = tuple[Callable[..., str], str, str, ToolAnnotations | None]
46
+
47
+ _INSTRUCTIONS = """\
48
+ Cloudfall manages declarative infrastructure state for dedicated Debian
49
+ servers. Read-only tools validate state and derive evidence; destructive
50
+ tools change servers and require calling twice: the first call returns a
51
+ confirmation-required preview, the second call with confirm=true executes.
52
+ Every mutation writes receipts; nothing reports success it cannot prove.
53
+ The add_ssh_key, add_server_type, and add_server tools declare the fleet:
54
+ they write schema-validated resource files into the project (never onto a
55
+ server), re-validate the whole project afterwards, and remove what they
56
+ wrote when that validation fails. They never overwrite an existing
57
+ resource. The project itself is laid out beforehand with `cloudfall init`
58
+ on the CLI; this server always runs inside an existing project.
59
+ """
60
+
61
+
62
+ def create_server(config: AgentConfig) -> MCPServer:
63
+ """Create the MCP server and register every agent tool."""
64
+ server = MCPServer(name="cloudfall", instructions=_INSTRUCTIONS)
65
+ toolset = AgentToolset(config)
66
+ registrations = (
67
+ *_evidence_registrations(toolset),
68
+ *_authoring_registrations(toolset),
69
+ *_build_registrations(toolset),
70
+ *_mutation_registrations(toolset),
71
+ *_backup_registrations(toolset),
72
+ *_operator_registrations(toolset),
73
+ )
74
+ for handler, name, description, tool_annotations in registrations:
75
+ server.add_tool(
76
+ handler,
77
+ name=name,
78
+ description=description,
79
+ annotations=tool_annotations,
80
+ )
81
+ return server
82
+
83
+
84
+ def _evidence_registrations(
85
+ toolset: AgentToolset,
86
+ ) -> tuple[_Registration, ...]:
87
+ read_only = ToolAnnotations(read_only_hint=True)
88
+
89
+ def validate_config() -> str:
90
+ return _dump(toolset.validate())
91
+
92
+ def show_inventory() -> str:
93
+ return _dump(toolset.inventory())
94
+
95
+ def audit_servers() -> str:
96
+ return _dump(toolset.audit())
97
+
98
+ def services_status() -> str:
99
+ return _dump(toolset.services_status())
100
+
101
+ def component_health(component: str) -> str:
102
+ return _dump(toolset.component_health(component))
103
+
104
+ def inspect_servers() -> str:
105
+ return _dump(toolset.inspect_servers())
106
+
107
+ def inspect_services() -> str:
108
+ return _dump(toolset.inspect_services())
109
+
110
+ return (
111
+ (
112
+ validate_config,
113
+ "validate_config",
114
+ "Validate declared config and resource references",
115
+ read_only,
116
+ ),
117
+ (
118
+ show_inventory,
119
+ "show_inventory",
120
+ "Return the non-secret typed platform inventory",
121
+ read_only,
122
+ ),
123
+ (
124
+ audit_servers,
125
+ "audit_servers",
126
+ "Compare desired state with collected server evidence",
127
+ read_only,
128
+ ),
129
+ (
130
+ services_status,
131
+ "services_status",
132
+ "Derive the evidence-based public service lifecycle",
133
+ read_only,
134
+ ),
135
+ (
136
+ component_health,
137
+ "component_health",
138
+ "Probe one component's declared health check on its servers",
139
+ read_only,
140
+ ),
141
+ (
142
+ inspect_servers,
143
+ "inspect_servers",
144
+ "Collect read-only server evidence snapshots over SSH",
145
+ read_only,
146
+ ),
147
+ (
148
+ inspect_services,
149
+ "inspect_services",
150
+ "Collect DNS, TLS, origin, and public route evidence",
151
+ read_only,
152
+ ),
153
+ )
154
+
155
+
156
+ def _authoring_registrations(
157
+ toolset: AgentToolset,
158
+ ) -> tuple[_Registration, ...]:
159
+ writes_project = ToolAnnotations(
160
+ read_only_hint=False, destructive_hint=False, idempotent_hint=False
161
+ )
162
+
163
+ def add_ssh_key(
164
+ key_file: str,
165
+ owner: str,
166
+ id: str | None = None, # noqa: A002 - mirrors `cloudfall add --id`.
167
+ environment: str = "production",
168
+ description: str | None = None,
169
+ ) -> str:
170
+ return _dump(
171
+ toolset.add_ssh_key(key_file, owner, id, environment, description)
172
+ )
173
+
174
+ def add_server_type(
175
+ id: str, # noqa: A002 - mirrors the `cloudfall add server-type` id.
176
+ description: str | None = None,
177
+ ) -> str:
178
+ return _dump(toolset.add_server_type(id, description))
179
+
180
+ def add_server( # noqa: PLR0913 - boundary signature mirrors the CLI.
181
+ id: str, # noqa: A002 - mirrors the `cloudfall add server` id.
182
+ address: str,
183
+ type: str = "debian-application", # noqa: A002 - mirrors `--type`.
184
+ environment: str = "production",
185
+ hostname: str | None = None,
186
+ ssh_user: str = "root",
187
+ ssh_port: int = 22,
188
+ description: str | None = None,
189
+ ) -> str:
190
+ return _dump(
191
+ toolset.add_server(
192
+ id,
193
+ address,
194
+ type,
195
+ environment,
196
+ hostname,
197
+ ssh_user,
198
+ ssh_port,
199
+ description,
200
+ )
201
+ )
202
+
203
+ return (
204
+ (
205
+ add_ssh_key,
206
+ "add_ssh_key",
207
+ "Declare an SshPublicKey resource read from a controller-side "
208
+ "public key file (e.g. ~/.ssh/id_ed25519.pub); writes into the "
209
+ "project and re-validates it, never overwrites",
210
+ writes_project,
211
+ ),
212
+ (
213
+ add_server_type,
214
+ "add_server_type",
215
+ "Declare a ServerType resource from the bundled Debian 13 "
216
+ "baseline (no software RAID, default-deny firewall with "
217
+ "22/80/443); writes into the project and re-validates it, "
218
+ "never overwrites",
219
+ writes_project,
220
+ ),
221
+ (
222
+ add_server,
223
+ "add_server",
224
+ "Declare a Server resource reachable at an address, creating "
225
+ "its baseline ServerType when the project lacks it; writes "
226
+ "into the project and re-validates it, never overwrites",
227
+ writes_project,
228
+ ),
229
+ )
230
+
231
+
232
+ def _build_registrations(toolset: AgentToolset) -> tuple[_Registration, ...]:
233
+ def import_render(
234
+ blueprint: str,
235
+ application: str,
236
+ server_id: str,
237
+ output_directory: str = "tmp/import/config",
238
+ environment_directory: str = "tmp/import/env",
239
+ ) -> str:
240
+ return _dump(
241
+ toolset.import_render(
242
+ blueprint,
243
+ application,
244
+ server_id,
245
+ output_directory,
246
+ environment_directory,
247
+ )
248
+ )
249
+
250
+ def import_render_api(
251
+ api_key_file: str,
252
+ application: str,
253
+ server_id: str,
254
+ output_directory: str = "tmp/import/config",
255
+ environment_directory: str = "tmp/import/env",
256
+ ) -> str:
257
+ return _dump(
258
+ toolset.import_render_api(
259
+ api_key_file,
260
+ application,
261
+ server_id,
262
+ output_directory,
263
+ environment_directory,
264
+ )
265
+ )
266
+
267
+ def render_secrets(component: str) -> str:
268
+ return _dump(toolset.render_secrets(component))
269
+
270
+ def build_artifact(component: str, git_ref: str) -> str:
271
+ return _dump(toolset.build_artifact(component, git_ref))
272
+
273
+ return (
274
+ (
275
+ import_render,
276
+ "import_render",
277
+ "Map a render.yaml blueprint onto Cloudfall config fragments "
278
+ "with a structured gap report",
279
+ None,
280
+ ),
281
+ (
282
+ import_render_api,
283
+ "import_render_api",
284
+ "Map a live Render workspace onto Cloudfall config fragments "
285
+ "through the Render API; the api_key_file is a controller-side "
286
+ "file containing only the API key",
287
+ None,
288
+ ),
289
+ (
290
+ render_secrets,
291
+ "render_secrets",
292
+ "Resolve a component's declared secret references from the "
293
+ "sops-encrypted secrets directory into its 0600 environment "
294
+ "file; the envelope carries key names and a hash, never values",
295
+ None,
296
+ ),
297
+ (
298
+ build_artifact,
299
+ "build_artifact",
300
+ "Clone a component repository at a git ref and package a "
301
+ "hashed release artifact",
302
+ None,
303
+ ),
304
+ )
305
+
306
+
307
+ def _mutation_registrations(
308
+ toolset: AgentToolset,
309
+ ) -> tuple[_Registration, ...]:
310
+ destructive = ToolAnnotations(read_only_hint=False, destructive_hint=True)
311
+
312
+ def deploy_component(
313
+ component: str,
314
+ release: str,
315
+ environment_file: str | None = None,
316
+ confirm: bool = False, # noqa: FBT001, FBT002 - explicit agent gate.
317
+ ) -> str:
318
+ return _dump(
319
+ toolset.deploy_component(
320
+ component, release, environment_file, confirm=confirm
321
+ )
322
+ )
323
+
324
+ def rollback_component(
325
+ component: str,
326
+ release: str,
327
+ confirm: bool = False, # noqa: FBT001, FBT002 - explicit agent gate.
328
+ ) -> str:
329
+ return _dump(toolset.rollback_component(component, release, confirm=confirm))
330
+
331
+ def restart_component(
332
+ component: str,
333
+ confirm: bool = False, # noqa: FBT001, FBT002 - explicit agent gate.
334
+ ) -> str:
335
+ return _dump(toolset.restart_component(component, confirm=confirm))
336
+
337
+ def migrate_database(
338
+ service: str,
339
+ database: str,
340
+ source_url_file: str,
341
+ confirm: bool = False, # noqa: FBT001, FBT002 - explicit agent gate.
342
+ ) -> str:
343
+ return _dump(
344
+ toolset.migrate_database(
345
+ service, database, source_url_file, confirm=confirm
346
+ )
347
+ )
348
+
349
+ def converge_baseline(
350
+ confirm: bool = False, # noqa: FBT001, FBT002 - explicit agent gate.
351
+ ) -> str:
352
+ return _dump(toolset.converge_baseline(confirm=confirm))
353
+
354
+ def converge_services(
355
+ confirm: bool = False, # noqa: FBT001, FBT002 - explicit agent gate.
356
+ ) -> str:
357
+ return _dump(toolset.converge_services(confirm=confirm))
358
+
359
+ def converge_domains(
360
+ issue_certificates: bool = False, # noqa: FBT001, FBT002 - explicit.
361
+ confirm: bool = False, # noqa: FBT001, FBT002 - explicit agent gate.
362
+ ) -> str:
363
+ return _dump(
364
+ toolset.converge_domains(
365
+ issue_certificates=issue_certificates, confirm=confirm
366
+ )
367
+ )
368
+
369
+ def migrate( # noqa: PLR0913 - boundary signature mirrors the toolset.
370
+ builds: dict[str, str] | None = None,
371
+ releases: dict[str, str] | None = None,
372
+ environment_files: dict[str, str] | None = None,
373
+ data_migrations: dict[str, str] | None = None,
374
+ plan_file: str = "tmp/migrate/plan.json",
375
+ restart_plan: bool = False, # noqa: FBT001, FBT002 - explicit gate.
376
+ confirm: bool = False, # noqa: FBT001, FBT002 - explicit agent gate.
377
+ ) -> str:
378
+ return _dump(
379
+ toolset.migrate(
380
+ builds,
381
+ releases,
382
+ environment_files,
383
+ data_migrations,
384
+ plan_file,
385
+ restart_plan=restart_plan,
386
+ confirm=confirm,
387
+ )
388
+ )
389
+
390
+ return (
391
+ (
392
+ deploy_component,
393
+ "deploy_component",
394
+ "Deploy one built release with digest verification, a health "
395
+ "gate, and automatic rollback; requires confirm=true",
396
+ destructive,
397
+ ),
398
+ (
399
+ rollback_component,
400
+ "rollback_component",
401
+ "Activate an already-retained release behind the health gate; "
402
+ "requires confirm=true",
403
+ destructive,
404
+ ),
405
+ (
406
+ restart_component,
407
+ "restart_component",
408
+ "Restart one component behind its health check; requires confirm=true",
409
+ destructive,
410
+ ),
411
+ (
412
+ migrate_database,
413
+ "migrate_database",
414
+ "Dump an external PostgreSQL database and restore it into a "
415
+ "declared service with per-table row-count verification; the "
416
+ "source_url_file is a controller-side file containing only the "
417
+ "connection URL; requires confirm=true",
418
+ destructive,
419
+ ),
420
+ (
421
+ converge_baseline,
422
+ "converge_baseline",
423
+ "Converge every server to the managed baseline; requires confirm=true",
424
+ destructive,
425
+ ),
426
+ (
427
+ converge_services,
428
+ "converge_services",
429
+ "Converge every declared infrastructure service; requires confirm=true",
430
+ destructive,
431
+ ),
432
+ (
433
+ converge_domains,
434
+ "converge_domains",
435
+ "Converge every declared public domain route; requires confirm=true",
436
+ destructive,
437
+ ),
438
+ (
439
+ migrate,
440
+ "migrate",
441
+ "Preview (without confirm) or execute (confirm=true) the "
442
+ "resumable end-to-end migration plan: baseline, services, "
443
+ "builds, deployments, routes, DNS verification, TLS, and a "
444
+ "final compliance-proving evidence pass",
445
+ destructive,
446
+ ),
447
+ )
448
+
449
+
450
+ def _backup_registrations(
451
+ toolset: AgentToolset,
452
+ ) -> tuple[_Registration, ...]:
453
+ destructive = ToolAnnotations(read_only_hint=False, destructive_hint=True)
454
+
455
+ def backup_service(
456
+ service: str,
457
+ confirm: bool = False, # noqa: FBT001, FBT002 - explicit agent gate.
458
+ ) -> str:
459
+ return _dump(toolset.backup_service(service, confirm=confirm))
460
+
461
+ def verify_backup(
462
+ service: str,
463
+ confirm: bool = False, # noqa: FBT001, FBT002 - explicit agent gate.
464
+ ) -> str:
465
+ return _dump(toolset.verify_backup(service, confirm=confirm))
466
+
467
+ return (
468
+ (
469
+ backup_service,
470
+ "backup_service",
471
+ "Run the declared backup for one service on its server, "
472
+ "writing a schema-valid receipt; requires confirm=true",
473
+ destructive,
474
+ ),
475
+ (
476
+ verify_backup,
477
+ "verify_backup",
478
+ "Prove the newest backup of one service restores (dump "
479
+ "restore-check or snapshot integrity), receipted; requires "
480
+ "confirm=true",
481
+ destructive,
482
+ ),
483
+ )
484
+
485
+
486
+ def _operator_registrations(
487
+ toolset: AgentToolset,
488
+ ) -> tuple[_Registration, ...]:
489
+ read_only = ToolAnnotations(read_only_hint=True)
490
+ destructive = ToolAnnotations(read_only_hint=False, destructive_hint=True)
491
+
492
+ def operator_proposals() -> str:
493
+ return _dump(toolset.operator_proposals())
494
+
495
+ def operator_watch(
496
+ drift: bool = False, # noqa: FBT001, FBT002 - explicit agent flag.
497
+ ) -> str:
498
+ return _dump(toolset.operator_watch(drift=drift))
499
+
500
+ def operator_approve(
501
+ proposal: str,
502
+ confirm: bool = False, # noqa: FBT001, FBT002 - explicit agent gate.
503
+ ) -> str:
504
+ return _dump(toolset.operator_approve(proposal, confirm=confirm))
505
+
506
+ return (
507
+ (
508
+ operator_proposals,
509
+ "operator_proposals",
510
+ "List every operator proposal receipt: trigger evidence, "
511
+ "diagnosis, proposed operation, and outcome",
512
+ read_only,
513
+ ),
514
+ (
515
+ operator_watch,
516
+ "operator_watch",
517
+ "Run one operator watch pass: fetch firing declared alerts "
518
+ "through the mTLS gateway (and audit for drift when "
519
+ "drift=true) and write proposals for anything new",
520
+ read_only,
521
+ ),
522
+ (
523
+ operator_approve,
524
+ "operator_approve",
525
+ "Execute one operator proposal's declared remediation and "
526
+ "verify its trigger evidence resolves; requires confirm=true",
527
+ destructive,
528
+ ),
529
+ )
530
+
531
+
532
+ def _dump(payload: dict[str, object]) -> str:
533
+ return json.dumps(payload, sort_keys=True)
534
+
535
+
536
+ def _parser() -> StrictArgumentParser:
537
+ parser = StrictArgumentParser(
538
+ prog="cloudfall-mcp",
539
+ description=(
540
+ "Expose the Cloudfall agent toolset over MCP stdio. Read-only"
541
+ " evidence tools are available freely; every server-changing"
542
+ " tool requires an explicit two-step confirmation handshake."
543
+ ),
544
+ )
545
+ parser.add_argument(
546
+ "--project",
547
+ type=Path,
548
+ default=None,
549
+ help=(
550
+ f"project directory to run in (default: ${PROJECT_DIRECTORY_VARIABLE}, "
551
+ "else the current directory when it is a project)"
552
+ ),
553
+ )
554
+ parser.add_argument(
555
+ "--schemas",
556
+ type=Path,
557
+ default=default_schema_directory(),
558
+ help=(
559
+ "JSON Schema directory used to validate the project "
560
+ "(default: %(default)s)"
561
+ ),
562
+ )
563
+ parser.add_argument(
564
+ "--engine",
565
+ type=Path,
566
+ default=default_engine_directory(),
567
+ help="engine module directory containing the Ansible playbooks"
568
+ " (default: %(default)s)",
569
+ )
570
+ parser.add_argument(
571
+ "--inventory-file",
572
+ type=project_path_argument,
573
+ default=Path("tmp/ansible-inventory.json"),
574
+ help="path where the rendered Ansible inventory is written"
575
+ " (default: %(default)s)",
576
+ )
577
+ parser.add_argument(
578
+ "--observed",
579
+ type=project_path_argument,
580
+ default=Path("tmp/observed"),
581
+ help="directory holding read-only server observation snapshots"
582
+ " (default: %(default)s)",
583
+ )
584
+ parser.add_argument(
585
+ "--service-observed",
586
+ type=project_path_argument,
587
+ default=Path("tmp/observed-services"),
588
+ help="directory holding DNS, TLS, origin, and public route evidence"
589
+ " (default: %(default)s)",
590
+ )
591
+ parser.add_argument(
592
+ "--deployments",
593
+ type=project_path_argument,
594
+ default=Path("tmp/deployments"),
595
+ help="directory holding domain deployment receipts (default: %(default)s)",
596
+ )
597
+ parser.add_argument(
598
+ "--releases",
599
+ type=project_path_argument,
600
+ default=Path("tmp/releases"),
601
+ help="directory holding component release receipts (default: %(default)s)",
602
+ )
603
+ parser.add_argument(
604
+ "--artifacts",
605
+ type=project_path_argument,
606
+ default=Path("tmp/artifacts"),
607
+ help="directory holding built release artifacts (default: %(default)s)",
608
+ )
609
+ parser.add_argument(
610
+ "--data-migrations",
611
+ type=project_path_argument,
612
+ default=Path("tmp/data-migrations"),
613
+ help="directory holding data-migration receipts (default: %(default)s)",
614
+ )
615
+ parser.add_argument(
616
+ "--secrets-dir",
617
+ type=Path,
618
+ default=Path("secrets"),
619
+ help="sops-encrypted secrets directory (default: %(default)s)",
620
+ )
621
+ parser.add_argument(
622
+ "--env-dir",
623
+ type=project_path_argument,
624
+ default=Path("tmp/env"),
625
+ help="rendered environment file directory (default: %(default)s)",
626
+ )
627
+ parser.add_argument(
628
+ "--env-receipts",
629
+ type=project_path_argument,
630
+ default=Path("tmp/env-receipts"),
631
+ help="rendered environment receipt directory (default: %(default)s)",
632
+ )
633
+ parser.add_argument(
634
+ "--backups",
635
+ type=project_path_argument,
636
+ default=Path("tmp/backups"),
637
+ help="directory holding backup operation receipts (default: %(default)s)",
638
+ )
639
+ parser.add_argument(
640
+ "--proposals",
641
+ type=project_path_argument,
642
+ default=Path("tmp/operator/proposals"),
643
+ help="directory holding operator proposal receipts (default: %(default)s)",
644
+ )
645
+ parser.add_argument(
646
+ "--gateway-ca",
647
+ type=Path,
648
+ default=None,
649
+ help="CA bundle for the logging gateway's alerts route",
650
+ )
651
+ parser.add_argument(
652
+ "--gateway-cert",
653
+ type=Path,
654
+ default=None,
655
+ help="client certificate for the logging gateway's alerts route",
656
+ )
657
+ parser.add_argument(
658
+ "--gateway-key",
659
+ type=Path,
660
+ default=None,
661
+ help="client key for the logging gateway's alerts route",
662
+ )
663
+ return parser
664
+
665
+
666
+ def main(argv: Sequence[str] | None = None) -> int:
667
+ """Run the MCP server over stdio."""
668
+ arguments = parse_arguments(_parser(), argv)
669
+ try:
670
+ with project_context(arguments.project, os.environ) as project_directory:
671
+ _serve(arguments, project_directory)
672
+ except ProjectError as error:
673
+ sys.stderr.write(f"{_dump(error.as_dict())}\n")
674
+ return 2
675
+ return 0
676
+
677
+
678
+ def _serve(arguments: argparse.Namespace, project_directory: Path) -> None:
679
+ config = AgentConfig(
680
+ project_directory=project_directory,
681
+ schema_directory=Path(arguments.schemas),
682
+ engine_directory=Path(arguments.engine),
683
+ inventory_file=Path(arguments.inventory_file),
684
+ observed_directory=Path(arguments.observed),
685
+ service_observed_directory=Path(arguments.service_observed),
686
+ deployments_directory=Path(arguments.deployments),
687
+ releases_directory=Path(arguments.releases),
688
+ artifacts_directory=Path(arguments.artifacts),
689
+ data_migrations_directory=Path(arguments.data_migrations),
690
+ backups_directory=Path(arguments.backups),
691
+ secrets_directory=Path(arguments.secrets_dir),
692
+ environment_directory=Path(arguments.env_dir),
693
+ environment_receipts_directory=Path(arguments.env_receipts),
694
+ proposals_directory=Path(arguments.proposals),
695
+ gateway_ca_path=arguments.gateway_ca,
696
+ gateway_certificate_path=arguments.gateway_cert,
697
+ gateway_key_path=arguments.gateway_key,
698
+ )
699
+ create_server(config).run(transport="stdio")
700
+
701
+
702
+ def run() -> None:
703
+ """Installed console-script entry point."""
704
+ raise SystemExit(main())