cloudfall 0.8.0rc13__tar.gz → 0.8.0rc15__tar.gz

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 (487) hide show
  1. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/PKG-INFO +8 -3
  2. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/README.md +7 -2
  3. cloudfall-0.8.0rc15/changelog.d/106-enforce-manual-approval.md +3 -0
  4. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/160-release-approve.md +1 -1
  5. cloudfall-0.8.0rc15/changelog.d/165-migrate-approval-pause.md +3 -0
  6. cloudfall-0.8.0rc15/changelog.d/174-no-templating-config-values.md +3 -0
  7. cloudfall-0.8.0rc15/changelog.d/181-catalog-inputs-as-data.md +3 -0
  8. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/ci-deploy-guide.md +86 -19
  9. cloudfall-0.8.0rc15/docs/decisions/2026-11-02-isolation-without-containers.md +155 -0
  10. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/decisions.md +8 -0
  11. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/operator-guide.md +5 -2
  12. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/render-migration-guide.md +15 -1
  13. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/008-enforce-manual-approval.md +20 -1
  14. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/cli.py +14 -3
  15. cloudfall-0.8.0rc15/engine/tests/golden/examples/ansible-inventory.json +1 -0
  16. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_ansible_inventory.py +4 -4
  17. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_examples_render_unchanged.py +6 -1
  18. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_services_passwords.py +10 -3
  19. cloudfall-0.8.0rc15/engine/tests/test_unsafe_config_values.py +348 -0
  20. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/pyproject.toml +1 -1
  21. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/README.md +11 -0
  22. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/agent_server.py +16 -0
  23. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/agent_tools.py +3 -0
  24. cloudfall-0.8.0rc15/sdk/src/cloudfall/ansible_data.py +45 -0
  25. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/app.py +101 -6
  26. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/commands.py +28 -7
  27. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/decision.py +5 -3
  28. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/lifecycle.py +9 -0
  29. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/migrate.py +88 -20
  30. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/observe.py +5 -1
  31. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/recording.py +8 -1
  32. cloudfall-0.8.0rc15/sdk/src/cloudfall/release_approval.py +609 -0
  33. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_agent_server.py +85 -0
  34. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_decision.py +67 -1
  35. cloudfall-0.8.0rc15/sdk/tests/test_deploy_approval.py +556 -0
  36. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_host_stats.py +24 -0
  37. cloudfall-0.8.0rc15/sdk/tests/test_migrate_approval.py +320 -0
  38. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_observe.py +3 -2
  39. cloudfall-0.8.0rc13/engine/tests/golden/examples/ansible-inventory.json +0 -1
  40. cloudfall-0.8.0rc13/sdk/src/cloudfall/release_approval.py +0 -286
  41. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/.ansible-lint +0 -0
  42. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/.gitignore +0 -0
  43. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/ARCHITECTURE.md +0 -0
  44. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/CHANGELOG.md +0 -0
  45. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/CODE_OF_CONDUCT.md +0 -0
  46. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/CONTRIBUTING.md +0 -0
  47. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/LICENSE +0 -0
  48. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/MANIFESTO.md +0 -0
  49. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/ROADMAP.md +0 -0
  50. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/SECURITY.md +0 -0
  51. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/Taskfile.yml +0 -0
  52. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/113-component-build-validation.md +0 -0
  53. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/114-refuse-node-runtimes.md +0 -0
  54. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/115-component-build-steps.md +0 -0
  55. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/116-converge-one-domain.md +0 -0
  56. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/117-domains-apply.md +0 -0
  57. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/118-domains-observe.md +0 -0
  58. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/119-release-command.md +0 -0
  59. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/120-release-command-plan.md +0 -0
  60. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/121-owned-postgresql-validation.md +0 -0
  61. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/122-owned-postgresql-converge.md +0 -0
  62. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/123-postgresql-remote-access.md +0 -0
  63. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/124-data-copy-schema.md +0 -0
  64. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/125-data-copy-preflight.md +0 -0
  65. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/126-source-schema-checks.md +0 -0
  66. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/140-scram-verifier.md +0 -0
  67. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/143-audit-remote-access.md +0 -0
  68. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/144-pg-hba-reload-and-names.md +0 -0
  69. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/151-keep-hand-made-unit.md +0 -0
  70. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/159-domain-routes-validation.md +0 -0
  71. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/161-command-health-check-schema.md +0 -0
  72. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/162-domain-routes-render.md +0 -0
  73. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/164-command-health-gate.md +0 -0
  74. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/166-command-check-restart-rollback-health.md +0 -0
  75. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/173-preflight-extension-arrays.md +0 -0
  76. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/183-migrate-observations-missing.md +0 -0
  77. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/89-ci-deploy-guide.md +0 -0
  78. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/90-domain-upstream-early.md +0 -0
  79. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/96-healthcheck-none.md +0 -0
  80. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/97-stale-proposals.md +0 -0
  81. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/README.md +0 -0
  82. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/README.md +0 -0
  83. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/alert-rules/postgresql-down.yaml +0 -0
  84. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/applications/crm.yaml +0 -0
  85. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/components/crm-backend.yaml +0 -0
  86. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/domains/crm-site.yaml +0 -0
  87. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/logging-stacks/operations.yaml +0 -0
  88. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/operator-policies/production.yaml +0 -0
  89. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/server-types/debian-application.yaml +0 -0
  90. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/servers/h1.yaml +0 -0
  91. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/servers/h2.yaml +0 -0
  92. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/services/postgresql-main.yaml +0 -0
  93. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/services/redis-cache.yaml +0 -0
  94. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/ssh-public-keys/example-admin.yaml +0 -0
  95. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/agent-investigation.schema.json +0 -0
  96. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/alert-rule.schema.json +0 -0
  97. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/application.schema.json +0 -0
  98. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/artifact.schema.json +0 -0
  99. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/backup-receipt.schema.json +0 -0
  100. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/common.schema.json +0 -0
  101. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/component.schema.json +0 -0
  102. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/deployment-receipt.schema.json +0 -0
  103. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/domain.schema.json +0 -0
  104. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/environment-receipt.schema.json +0 -0
  105. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/logging-stack.schema.json +0 -0
  106. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/observed-domain.schema.json +0 -0
  107. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/observed-server.schema.json +0 -0
  108. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/operation-decision.schema.json +0 -0
  109. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/operation.schema.json +0 -0
  110. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/operator-policy.schema.json +0 -0
  111. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/operator-proposal.schema.json +0 -0
  112. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/release-approval.schema.json +0 -0
  113. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/release-receipt.schema.json +0 -0
  114. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/server-type.schema.json +0 -0
  115. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/server.schema.json +0 -0
  116. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/service.schema.json +0 -0
  117. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/ssh-public-key.schema.json +0 -0
  118. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/invalid/missing-server/applications/crm.yaml +0 -0
  119. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/invalid/missing-server/components/crm-backend.yaml +0 -0
  120. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/invalid/missing-server/server-types/debian-application.yaml +0 -0
  121. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/invalid/missing-server/servers/h1.yaml +0 -0
  122. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/invalid/schema/components/component-unknown-field.yaml +0 -0
  123. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/observed/compliant/h1.json +0 -0
  124. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/observed/compliant/h2.json +0 -0
  125. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/README.md +0 -0
  126. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/.gitignore +0 -0
  127. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/ansible.cfg +0 -0
  128. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/inventories/production/hosts.yml +0 -0
  129. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/converge.yml +0 -0
  130. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/disk-report.yml +0 -0
  131. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/journal-vacuum.yml +0 -0
  132. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/postgresql-reinit.yml +0 -0
  133. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/postgresql-restart.yml +0 -0
  134. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/service-logs.yml +0 -0
  135. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/shop-logrotate.yml +0 -0
  136. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/disk-report.yml +0 -0
  137. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/journal-vacuum.yml +0 -0
  138. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/postgresql-reinit.yml +0 -0
  139. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/postgresql-restart.yml +0 -0
  140. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/postgresql.yml +0 -0
  141. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/service-logs.yml +0 -0
  142. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/shop-logrotate.yml +0 -0
  143. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/shop.yml +0 -0
  144. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/disk-report-20261008170744.diff +0 -0
  145. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/disk-report-20261008170744.json +0 -0
  146. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/journal-vacuum-20261008171016.diff +0 -0
  147. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/journal-vacuum-20261008171016.json +0 -0
  148. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/postgresql-reinit-20261008171010.diff +0 -0
  149. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/postgresql-reinit-20261008171010.json +0 -0
  150. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/postgresql-restart-20261008170624.diff +0 -0
  151. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/postgresql-restart-20261008170624.json +0 -0
  152. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170612.diff +0 -0
  153. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170612.json +0 -0
  154. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170740.diff +0 -0
  155. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170740.json +0 -0
  156. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170742.diff +0 -0
  157. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170742.json +0 -0
  158. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170838.diff +0 -0
  159. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170838.json +0 -0
  160. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170920.diff +0 -0
  161. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170920.json +0 -0
  162. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/shop-logrotate-20261008170752.diff +0 -0
  163. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/shop-logrotate-20261008170752.json +0 -0
  164. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/disk-report-20261008204348.diff +0 -0
  165. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/disk-report-20261008204348.json +0 -0
  166. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/disk-report-20261008204421.diff +0 -0
  167. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/disk-report-20261008204421.json +0 -0
  168. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/postgresql-restart-20261008204358.diff +0 -0
  169. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/postgresql-restart-20261008204358.json +0 -0
  170. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/shop-logrotate-20261008204351.diff +0 -0
  171. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/shop-logrotate-20261008204351.json +0 -0
  172. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/scenario/break.yml +0 -0
  173. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/site.yml +0 -0
  174. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/pyproject.toml +0 -0
  175. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/src/cloudfall_demo/__init__.py +0 -0
  176. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/src/cloudfall_demo/server.py +0 -0
  177. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/src/cloudfall_demo/static/evaluation.json +0 -0
  178. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/src/cloudfall_demo/static/index.html +0 -0
  179. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/tests/test_demo.py +0 -0
  180. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/uv.lock +0 -0
  181. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/ansible-context.md +0 -0
  182. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/assets/logo.svg +0 -0
  183. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/availability-design.md +0 -0
  184. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/brownfield-design.md +0 -0
  185. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/decisions/2026-09-14-no-ansible-replacement.md +0 -0
  186. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/decisions/2026-09-14-no-nixos-base.md +0 -0
  187. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/decisions/2026-09-14-no-rust-rewrite.md +0 -0
  188. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/fleet-goals.md +0 -0
  189. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/getting-started.md +0 -0
  190. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/hybrid-storage-design.md +0 -0
  191. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/images/demo.png +0 -0
  192. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/landscape.md +0 -0
  193. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/logging-service-guide.md +0 -0
  194. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/neon-migration-guide.md +0 -0
  195. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/new-server-storage-guide.md +0 -0
  196. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/product.md +0 -0
  197. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-debian-container-m1.md +0 -0
  198. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-dns-cutover.md +0 -0
  199. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-guided-data-migration.md +0 -0
  200. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-hetzner-m1.md +0 -0
  201. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-hetzner-m2-m3.md +0 -0
  202. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-hetzner-m4-m5.md +0 -0
  203. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-render-cutover.md +0 -0
  204. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-10-hetzner-m7.md +0 -0
  205. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-10-hetzner-m8.md +0 -0
  206. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-10-hetzner-m9.md +0 -0
  207. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-11-hetzner-m10.md +0 -0
  208. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-10-08-disk-full-agent.md +0 -0
  209. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/secrets-guide.md +0 -0
  210. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/001-stale-proposals.md +0 -0
  211. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/002-component-build-steps.md +0 -0
  212. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/003-domain-routes.md +0 -0
  213. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/004-domains-apply.md +0 -0
  214. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/005-release-command.md +0 -0
  215. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/006-standalone-postgresql.md +0 -0
  216. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/007-data-copy-schemas.md +0 -0
  217. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/009-command-health-check.md +0 -0
  218. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/README.md +0 -0
  219. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/ansible.cfg +0 -0
  220. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/backup.yml +0 -0
  221. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/baseline.yml +0 -0
  222. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/bootstrap.yml +0 -0
  223. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/data.yml +0 -0
  224. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/deploy.yml +0 -0
  225. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/domains-observe.yml +0 -0
  226. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/domains.yml +0 -0
  227. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/health.yml +0 -0
  228. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/inspect.yml +0 -0
  229. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/logging.yml +0 -0
  230. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/restart.yml +0 -0
  231. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/rollback.yml +0 -0
  232. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/services.yml +0 -0
  233. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/time.yml +0 -0
  234. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_access/defaults/main.yml +0 -0
  235. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_access/handlers/main.yml +0 -0
  236. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_access/meta/argument_specs.yml +0 -0
  237. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_access/meta/main.yml +0 -0
  238. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_access/tasks/main.yml +0 -0
  239. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_bootstrap/defaults/main.yml +0 -0
  240. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +0 -0
  241. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_bootstrap/meta/main.yml +0 -0
  242. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_bootstrap/tasks/main.yml +0 -0
  243. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml +0 -0
  244. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +0 -0
  245. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_data_migration/meta/main.yml +0 -0
  246. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_data_migration/tasks/main.yml +0 -0
  247. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/defaults/main.yml +0 -0
  248. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +0 -0
  249. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/meta/main.yml +0 -0
  250. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/command_health_check.yml +0 -0
  251. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/health.yml +0 -0
  252. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +0 -0
  253. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/main.yml +0 -0
  254. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/release_command.yml +0 -0
  255. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/release_receipt.yml +0 -0
  256. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/restart.yml +0 -0
  257. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/rollback.yml +0 -0
  258. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/templates/component.service.j2 +0 -0
  259. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/defaults/main.yml +0 -0
  260. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/handlers/main.yml +0 -0
  261. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +0 -0
  262. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/meta/main.yml +0 -0
  263. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/tasks/main.yml +0 -0
  264. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +0 -0
  265. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_inspect/defaults/main.yml +0 -0
  266. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +0 -0
  267. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_inspect/meta/main.yml +0 -0
  268. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_inspect/tasks/main.yml +0 -0
  269. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/defaults/main.yml +0 -0
  270. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/handlers/main.yml +0 -0
  271. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +0 -0
  272. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/meta/main.yml +0 -0
  273. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/tasks/main.yml +0 -0
  274. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +0 -0
  275. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +0 -0
  276. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +0 -0
  277. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +0 -0
  278. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +0 -0
  279. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +0 -0
  280. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +0 -0
  281. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +0 -0
  282. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +0 -0
  283. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +0 -0
  284. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +0 -0
  285. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/defaults/main.yml +0 -0
  286. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/handlers/main.yml +0 -0
  287. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +0 -0
  288. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/meta/main.yml +0 -0
  289. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/tasks/main.yml +0 -0
  290. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +0 -0
  291. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +0 -0
  292. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/defaults/main.yml +0 -0
  293. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/handlers/main.yml +0 -0
  294. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +0 -0
  295. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/meta/main.yml +0 -0
  296. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/tasks/apply_virtual_host.yml +0 -0
  297. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/tasks/main.yml +0 -0
  298. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/tasks/static_access.yml +0 -0
  299. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +0 -0
  300. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/defaults/main.yml +0 -0
  301. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/handlers/main.yml +0 -0
  302. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +0 -0
  303. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/meta/main.yml +0 -0
  304. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/tasks/main.yml +0 -0
  305. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/tasks/owner_password.yml +0 -0
  306. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +0 -0
  307. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +0 -0
  308. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +0 -0
  309. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +0 -0
  310. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/pg_hba-remote-access.j2 +0 -0
  311. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +0 -0
  312. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +0 -0
  313. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +0 -0
  314. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/defaults/main.yml +0 -0
  315. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/handlers/main.yml +0 -0
  316. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/meta/argument_specs.yml +0 -0
  317. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/meta/main.yml +0 -0
  318. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/tasks/main.yml +0 -0
  319. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/backup.service.j2 +0 -0
  320. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +0 -0
  321. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +0 -0
  322. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +0 -0
  323. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +0 -0
  324. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +0 -0
  325. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +0 -0
  326. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_time/defaults/main.yml +0 -0
  327. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_time/meta/argument_specs.yml +0 -0
  328. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_time/meta/main.yml +0 -0
  329. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_time/tasks/main.yml +0 -0
  330. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +0 -0
  331. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +0 -0
  332. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +0 -0
  333. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +0 -0
  334. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/__init__.py +0 -0
  335. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/__main__.py +0 -0
  336. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/ansible_inventory.py +0 -0
  337. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/artifact.py +0 -0
  338. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/playbook.py +0 -0
  339. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/py.typed +0 -0
  340. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/examples/h1/nftables.conf +0 -0
  341. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/examples/h1/postgresql-main/backup.sh +0 -0
  342. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/examples/h1/postgresql-main/cloudfall.conf +0 -0
  343. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/examples/h1/postgresql-main/restore-check.sh +0 -0
  344. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/examples/h2/nftables.conf +0 -0
  345. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/nginx-site/http-1.22.1.conf +0 -0
  346. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/nginx-site/http-1.26.3.conf +0 -0
  347. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/nginx-site/tls-1.22.1.conf +0 -0
  348. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/nginx-site/tls-1.26.3.conf +0 -0
  349. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_artifact_build_steps.py +0 -0
  350. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_artifact_builder.py +0 -0
  351. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_command_health_gate.py +0 -0
  352. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_data_migration_role.py +0 -0
  353. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_deploy_role.py +0 -0
  354. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_deploy_templates.py +0 -0
  355. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_domains_certificate.py +0 -0
  356. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_domains_one_domain.py +0 -0
  357. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_domains_playbook.py +0 -0
  358. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_firewall_templates.py +0 -0
  359. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_logging_templates.py +0 -0
  360. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_nginx_site_role.py +0 -0
  361. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_nginx_site_routes.py +0 -0
  362. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_nginx_site_templates.py +0 -0
  363. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_playbook.py +0 -0
  364. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_postgresql_owner_role.py +0 -0
  365. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_postgresql_remote_access.py +0 -0
  366. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_postgresql_templates.py +0 -0
  367. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_release_command.py +0 -0
  368. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_time_role.py +0 -0
  369. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/.gitignore +0 -0
  370. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/README.md +0 -0
  371. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/ansible.cfg +0 -0
  372. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/inventories/production/hosts.yml +0 -0
  373. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/converge.yml +0 -0
  374. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/disk-report.yml +0 -0
  375. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/journal-vacuum.yml +0 -0
  376. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/postgresql-reinit.yml +0 -0
  377. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/postgresql-restart.yml +0 -0
  378. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/service-logs.yml +0 -0
  379. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/shop-logrotate.yml +0 -0
  380. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/disk-report.yml +0 -0
  381. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/journal-vacuum.yml +0 -0
  382. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/postgresql-reinit.yml +0 -0
  383. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/postgresql-restart.yml +0 -0
  384. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/postgresql.yml +0 -0
  385. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/service-logs.yml +0 -0
  386. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/shop-logrotate.yml +0 -0
  387. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/shop.yml +0 -0
  388. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/disk-report-20261008170744.diff +0 -0
  389. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/disk-report-20261008170744.json +0 -0
  390. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/journal-vacuum-20261008171016.diff +0 -0
  391. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/journal-vacuum-20261008171016.json +0 -0
  392. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/postgresql-reinit-20261008171010.diff +0 -0
  393. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/postgresql-reinit-20261008171010.json +0 -0
  394. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/postgresql-restart-20261008170624.diff +0 -0
  395. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/postgresql-restart-20261008170624.json +0 -0
  396. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170612.diff +0 -0
  397. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170612.json +0 -0
  398. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170740.diff +0 -0
  399. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170740.json +0 -0
  400. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170742.diff +0 -0
  401. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170742.json +0 -0
  402. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170838.diff +0 -0
  403. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170838.json +0 -0
  404. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170920.diff +0 -0
  405. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170920.json +0 -0
  406. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/shop-logrotate-20261008170752.diff +0 -0
  407. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/shop-logrotate-20261008170752.json +0 -0
  408. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204348.diff +0 -0
  409. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204348.json +0 -0
  410. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204421.diff +0 -0
  411. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204421.json +0 -0
  412. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/postgresql-restart-20261008204358.diff +0 -0
  413. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/postgresql-restart-20261008204358.json +0 -0
  414. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/shop-logrotate-20261008204351.diff +0 -0
  415. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/shop-logrotate-20261008204351.json +0 -0
  416. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/scenario/break.yml +0 -0
  417. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/site.yml +0 -0
  418. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/README.md +0 -0
  419. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/pyproject.toml +0 -0
  420. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/render.yaml +0 -0
  421. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/src/demo/__init__.py +0 -0
  422. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/src/demo/asgi.py +0 -0
  423. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/uv.lock +0 -0
  424. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/__init__.py +0 -0
  425. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/__main__.py +0 -0
  426. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/ansible_api.py +0 -0
  427. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/ansible_reader.py +0 -0
  428. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/audit.py +0 -0
  429. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/authoring.py +0 -0
  430. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/callback_plugins/__init__.py +0 -0
  431. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/callback_plugins/cloudfall_host_stats.py +0 -0
  432. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/catalog.py +0 -0
  433. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/cli.py +0 -0
  434. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/cutover.py +0 -0
  435. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/dashboard.py +0 -0
  436. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/dashboard_server.py +0 -0
  437. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/domain.py +0 -0
  438. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/importer.py +0 -0
  439. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/inventory.py +0 -0
  440. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/investigation.py +0 -0
  441. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/observation.py +0 -0
  442. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/operations.py +0 -0
  443. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/operator.py +0 -0
  444. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/project.py +0 -0
  445. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/py.typed +0 -0
  446. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/render_api.py +0 -0
  447. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/resources.py +0 -0
  448. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/secrets.py +0 -0
  449. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/service_evidence.py +0 -0
  450. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/validation.py +0 -0
  451. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/why.py +0 -0
  452. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_ansible_reader.py +0 -0
  453. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_app.py +0 -0
  454. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_arguments.py +0 -0
  455. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_audit.py +0 -0
  456. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_authoring.py +0 -0
  457. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_catalog.py +0 -0
  458. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_command_health_check.py +0 -0
  459. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_commands.py +0 -0
  460. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_component_build_validation.py +0 -0
  461. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_cutover.py +0 -0
  462. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_dashboard_server.py +0 -0
  463. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_data_copy_preflight.py +0 -0
  464. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_data_migration.py +0 -0
  465. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_domain_routes.py +0 -0
  466. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_domains_apply.py +0 -0
  467. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_domains_converge.py +0 -0
  468. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_domains_observe.py +0 -0
  469. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_importer.py +0 -0
  470. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_inventory.py +0 -0
  471. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_investigation.py +0 -0
  472. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_lifecycle.py +0 -0
  473. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_migrate.py +0 -0
  474. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_operations_dashboard.py +0 -0
  475. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_operator.py +0 -0
  476. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_owned_postgresql.py +0 -0
  477. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_owned_postgresql_converge.py +0 -0
  478. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_project.py +0 -0
  479. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_release_approval.py +0 -0
  480. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_release_command_plan.py +0 -0
  481. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_render_api.py +0 -0
  482. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_resources.py +0 -0
  483. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_secrets.py +0 -0
  484. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_service_evidence.py +0 -0
  485. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_stale.py +0 -0
  486. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_validation.py +0 -0
  487. {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_why.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cloudfall
3
- Version: 0.8.0rc13
3
+ Version: 0.8.0rc15
4
4
  Summary: The operator's record for self-hosted servers run by an AI agent: Ansible executes, Cloudfall gates, verifies and keeps the receipts
5
5
  Project-URL: Homepage, https://cloudfall.dev
6
6
  Project-URL: Repository, https://github.com/cloudfall-dev/cloudfall
@@ -252,14 +252,19 @@ and drift audit to the live dashboard, is in
252
252
  ```console
253
253
  uv run cloudfall import render render.yaml --application acme --server h1
254
254
  uv run cloudfall migrate --build web=main # show the plan
255
- uv run cloudfall migrate --build web=main --yes # run it
255
+ uv run cloudfall migrate --build web=main --yes # run it; pauses at deploy:web
256
+ uv run cloudfall release approve web --release <release-id> # a person, at a terminal
257
+ uv run cloudfall migrate --yes # resume: deploy, routes, TLS
256
258
  ```
257
259
 
258
260
  The importer never guesses silently: `IMPORT-REPORT.md` records what was not
259
261
  imported, every assumption and every step required before cutover. The
260
262
  migration runs baseline, services, builds, health-gated deploys, routes, a
261
263
  DNS-verification pause, TLS and a final evidence pass, and resumes exactly
262
- where it stopped. Databases move with `cloudfall data copy`, which verifies
264
+ where it stopped. An imported application declares `approval: manual`, so
265
+ the run pauses before its deploy until a person approves the built release
266
+ with `cloudfall release approve`; the resumed run deploys that release
267
+ without building again. Databases move with `cloudfall data copy`, which verifies
263
268
  per-table row counts and writes a receipt. See the
264
269
  [Render migration guide](docs/render-migration-guide.md) and the
265
270
  [Neon migration guide](docs/neon-migration-guide.md).
@@ -224,14 +224,19 @@ and drift audit to the live dashboard, is in
224
224
  ```console
225
225
  uv run cloudfall import render render.yaml --application acme --server h1
226
226
  uv run cloudfall migrate --build web=main # show the plan
227
- uv run cloudfall migrate --build web=main --yes # run it
227
+ uv run cloudfall migrate --build web=main --yes # run it; pauses at deploy:web
228
+ uv run cloudfall release approve web --release <release-id> # a person, at a terminal
229
+ uv run cloudfall migrate --yes # resume: deploy, routes, TLS
228
230
  ```
229
231
 
230
232
  The importer never guesses silently: `IMPORT-REPORT.md` records what was not
231
233
  imported, every assumption and every step required before cutover. The
232
234
  migration runs baseline, services, builds, health-gated deploys, routes, a
233
235
  DNS-verification pause, TLS and a final evidence pass, and resumes exactly
234
- where it stopped. Databases move with `cloudfall data copy`, which verifies
236
+ where it stopped. An imported application declares `approval: manual`, so
237
+ the run pauses before its deploy until a person approves the built release
238
+ with `cloudfall release approve`; the resumed run deploys that release
239
+ without building again. Databases move with `cloudfall data copy`, which verifies
235
240
  per-table row counts and writes a receipt. See the
236
241
  [Render migration guide](docs/render-migration-guide.md) and the
237
242
  [Neon migration guide](docs/neon-migration-guide.md).
@@ -0,0 +1,3 @@
1
+ ### Breaking
2
+
3
+ - `deploy --yes` and `migrate --yes` on an application that declares `approval: manual` now need a person's approval of the release: without a `cloudfall release approve` record that counts (the same archive SHA-256, exactly the component's declared servers, not expired), `deploy --yes` exits 4 `PRECONDITION` with `deployment_approval_missing` before any playbook runs, naming the `release approve` command, and lists each record that doesn't count with `digest_mismatch`, `servers_changed` or `expired`. This hits every project that declares `manual` and deploys today, the example `crm.yaml` and every Render import included. Two ways out: keep `manual` and approve each release first (build it, run `cloudfall release approve` at a terminal, commit the record under `decisions/releases/` when CI deploys, then deploy), or switch the application to `approval: automatic` when another gate, such as GitHub's required reviewers, is the real one. The `deploy` plan and result gain `approval` (`policy`, and for `manual` the `status`, the counting record's `path`, `approver` and `expires_at` or `approved_at`, the records that don't count and the `command` to run), `deploy` gains `--approvals` (default `decisions/releases`) and the `RECORD_INVALID` exit for an approvals file that isn't a valid `ReleaseApproval`, and `cloudfall mcp serve` binds the approvals folder (its own `--approvals`) so a `deploy` call can't name its own; an `automatic` application's deploy never reads the folder (#163)
@@ -1,3 +1,3 @@
1
1
  ### Added
2
2
 
3
- - `cloudfall release approve <component> --release <release>` records a person's approval of one built release as a `ReleaseApproval` (new `release-approval.schema.json`) under `decisions/releases/`: the application, component, release, archive SHA-256, git commit, servers, approver, `approvedAt`, `expiresAt` (`--valid-for` hours, 1 to 168, default 24) and `via: terminal`. It verifies the artifact first, refuses a component of an `automatic` application with `release_approval_not_required`, and writes the record only once a person typed the release id at a terminal; `--yes` never answers it, and it is no MCP tool. It deploys nothing, and `deploy` does not read the record yet (#160)
3
+ - `cloudfall release approve <component> --release <release>` records a person's approval of one built release as a `ReleaseApproval` (new `release-approval.schema.json`) under `decisions/releases/`: the application, component, release, archive SHA-256, git commit, servers, approver, `approvedAt`, `expiresAt` (`--valid-for` hours, 1 to 168, default 24) and `via: terminal`. It verifies the artifact first, refuses a component of an `automatic` application with `release_approval_not_required`, and writes the record only once a person typed the release id at a terminal; `--yes` never answers it, and it is no MCP tool. It deploys nothing (#160)
@@ -0,0 +1,3 @@
1
+ ### Changed
2
+
3
+ - `migrate --yes` checks a `deploy:<component>` step's approval as `deploy --yes` does: for an application that declares `approval: manual` and no approval that counts, it pauses at that step with `PARTIAL_FAILURE`, `context.code: deployment_approval_missing`, the built `release` and the `cloudfall release approve` `command`, and once a person approved it, `migrate --yes` resumes there with the release the completed `build:<component>` step recorded, without building again and without needing `--build` again. The deploy step's detail gains `approval` (the record it went on), `migrate` gains `--approvals` (default `decisions/releases`, bound by `cloudfall mcp serve`) and the `RECORD_INVALID` exit for an approvals file that isn't a valid `ReleaseApproval`. `rollback` needs no approval: its plan gains `approval`, `{"policy": "manual", "required": false, "reason": ...}` for a `manual` application and `{"policy": "automatic"}` otherwise (#165)
@@ -0,0 +1,3 @@
1
+ ### Security
2
+
3
+ - A declared config value is never run as an Ansible template: `cloudfall-engine inventory render` and the overlay `cloudfall observe` writes mark every string unsafe (`{"__ansible_unsafe": ...}`), so a value holding `{{ ... }}` reaches the unit file, the release command and the health check as written. Before, a `service.command`, `healthCheck.command` or `release.command` argument such as `{{ lookup('pipe', '...') }}` ran on the management host during `cloudfall health` or `cloudfall deploy`, and an honest one such as `docker inspect --format {{.State.Health.Status}} app` failed with a template syntax error. A declared value that relied on Ansible evaluating `{{ ... }}` is now literal, and the rendered inventory's strings read as `{"__ansible_unsafe": "..."}` in its JSON and in `ansible-inventory --list` (#174)
@@ -0,0 +1,3 @@
1
+ ### Security
2
+
3
+ - A catalog operation's inputs are never run as an Ansible template: `cloudfall operations propose`, `cloudfall decisions approve` and `cloudfall agent investigate` hand them to the team's playbook in `--extra-vars` with every string marked unsafe (`{"__ansible_unsafe": ...}`), so an input holding `{{ ... }}` reaches the playbook as written, and integers and booleans keep their types. Before, an input such as `{{ lookup('pipe', '...') }}` ran on the management host with the operator's rights when the playbook started, and an honest input holding `{{` failed with a template syntax error. A team playbook that relied on an input being evaluated as Jinja now gets the literal text. The decision record's `inputs` are unchanged, and recordings made before this change replay as before (#181)
@@ -325,25 +325,92 @@ naming `production` still waits for its reviewers.
325
325
 
326
326
  ## Approval: who presses the button
327
327
 
328
- Every `Application` declares `spec.approval: manual` or `automatic`.
329
- Cloudfall validates the field and writes it into the rendered inventory, but
330
- no command acts on it today: `cloudfall deploy --yes` deploys an application
331
- marked `manual` exactly as it deploys one marked `automatic`. The only gate
332
- inside `deploy` is `--yes`, and a workflow that passes `--yes` has approved
333
- its own deploy.
334
-
335
- So in CI, the approval comes from GitHub, not from Cloudfall:
336
-
337
- - Give every environment that holds an application marked `manual` required
338
- reviewers, and turn on "Prevent self-review", so the person who pushed the
339
- change cannot approve its deploy
340
- - The job waits before its first step until a reviewer approves, so it never
341
- sees the SSH key or the age key of that environment without approval
342
- - Leave `staging` without reviewers if its applications are `automatic`
343
-
344
- Read `approval: manual` as your declaration that such a gate must exist; the
345
- environment's reviewers are what enforce it. The approval itself lands in
346
- GitHub's deployment history, not in Cloudfall's record.
328
+ Every `Application` declares `spec.approval: manual` or `automatic`, and
329
+ `cloudfall deploy` enforces it:
330
+
331
+ - `automatic`: `deploy --yes` deploys, as the workflow above does. Keep
332
+ staging's applications `automatic` and its one job stays as it is
333
+ - `manual`: `deploy --yes` deploys a release only when a person approved
334
+ that release with `cloudfall release approve`. Without an approval that
335
+ counts it exits 4 `PRECONDITION` with `deployment_approval_missing`, before
336
+ any playbook runs, and names the command to run. `deploy` without `--yes`
337
+ shows the approval in its plan: `approval.status` is `approved` or
338
+ `missing`
339
+
340
+ An approval is a record under `decisions/releases/` in the project. It names
341
+ the archive's SHA-256, the component's servers and when it expires (24 hours
342
+ by default, `--valid-for` up to a week). It counts only while the archive
343
+ being deployed has that digest, the component is declared on exactly those
344
+ servers, and it has not expired. A failed health gate does not use it up, so
345
+ the same release can be retried until it expires.
346
+
347
+ A person approves at a terminal by typing the release id back; no flag, and
348
+ no CI job, answers that prompt. So a production deploy of a `manual`
349
+ application is two jobs with a person between them, and approval needs the
350
+ exact archive the deploy ships:
351
+
352
+ 1. A build job builds the release and uploads `tmp/artifacts/<component>/`
353
+ as a workflow artifact
354
+ 2. The person downloads that artifact into `tmp/artifacts/<component>/` of
355
+ their checkout and approves it:
356
+
357
+ ```sh
358
+ gh run download <run-id> --name cloudfall-artifact-crm-backend --dir tmp/artifacts/crm-backend
359
+ uv run cloudfall release approve crm-backend --release 20260101T000000Z-abcdef0
360
+ git add decisions/releases
361
+ git commit -m "Approve crm-backend 20260101T000000Z-abcdef0"
362
+ git push
363
+ ```
364
+
365
+ 3. They dispatch the deploy job with the build's run id and the release. It
366
+ checks out the commit holding the record, downloads the same artifact and
367
+ deploys it:
368
+
369
+ ```yaml
370
+ build:
371
+ runs-on: ubuntu-24.04
372
+ steps:
373
+ # checkout, uv, uv sync --locked, as in the workflow above
374
+ - name: Build the artifact
375
+ id: build
376
+ run: |
377
+ uv run cloudfall-engine artifact build "$COMPONENT" --ref "$REF" --format json > "$RUNNER_TEMP/build.json"
378
+ echo "release=$(jq -r '.data.release' "$RUNNER_TEMP/build.json")" >> "$GITHUB_OUTPUT"
379
+ - uses: actions/upload-artifact@v7
380
+ with:
381
+ name: cloudfall-artifact-${{ env.COMPONENT }}
382
+ path: tmp/artifacts/${{ env.COMPONENT }}/
383
+ ```
384
+
385
+ ```yaml
386
+ deploy:
387
+ runs-on: ubuntu-24.04
388
+ environment: production
389
+ steps:
390
+ # checkout, uv, sops, the SSH and age keys, secrets render, as above
391
+ - uses: actions/download-artifact@v7
392
+ with:
393
+ name: cloudfall-artifact-${{ env.COMPONENT }}
394
+ path: tmp/artifacts/${{ env.COMPONENT }}/
395
+ run-id: ${{ inputs.build_run_id }}
396
+ github-token: ${{ github.token }}
397
+ - name: Deploy the approved release
398
+ run: uv run cloudfall deploy "$COMPONENT" --release "$RELEASE" --env-file "tmp/env/$COMPONENT.env" --yes
399
+ ```
400
+
401
+ The deploy job reads the record from its checkout, so dispatch it on the
402
+ commit that holds the approval. A rebuilt release has a new archive, and
403
+ its old approval no longer counts.
404
+
405
+ GitHub's required reviewers remain a useful outer gate: give the
406
+ `production` environment reviewers and turn on "Prevent self-review", and
407
+ the deploy job never sees that environment's SSH key or age key until a
408
+ reviewer lets it start. Their approval lands in GitHub's deployment
409
+ history; the `release approve` record is the one in Cloudfall's.
410
+
411
+ When the reviewers are the real gate and nobody will run `release approve`,
412
+ switch the application to `approval: automatic`, so the project no longer
413
+ claims a gate Cloudfall does not hold.
347
414
 
348
415
  ## Keeping the receipts
349
416
 
@@ -0,0 +1,155 @@
1
+ # Decision: isolation without containers or self-run VMs
2
+
3
+ Date: 2026-11-02. Status: decided, revisit on the triggers listed below.
4
+
5
+ ## Question
6
+
7
+ How does Cloudfall keep components, runners and environments apart, and does
8
+ that need containers or virtual machines it runs itself?
9
+
10
+ ## Decision
11
+
12
+ No containers and no hypervisor of Cloudfall's own. Isolation has two
13
+ boundaries:
14
+
15
+ - **Inside a server**, the boundary is the systemd sandbox of each unit, a
16
+ database role per component, and cgroup limits. This is required on every
17
+ server
18
+ - **Between trust levels**, the stronger boundary is a separate server: its
19
+ own kernel. A VPS counts as a server; its hypervisor belongs to the
20
+ provider, not to Cloudfall. This is recommended, not required
21
+
22
+ A team may run everything on one server, several independent projects
23
+ included, when it accepts the shared-kernel risk. The acceptance is declared
24
+ in the plan, never implied: Cloudfall warns about a placement that puts two
25
+ trust levels on one server unless the plan declares it.
26
+
27
+ A separate Unix user per component is necessary but is not a boundary on its
28
+ own.
29
+
30
+ ## Why a Unix user is not enough
31
+
32
+ A user keeps files and signals apart. It does not stop:
33
+
34
+ - a local privilege escalation in the shared kernel, which reaches every
35
+ component on the host
36
+ - a database, cache or queue on localhost that trusts every local user, or
37
+ that components share one account of
38
+ - membership in the `docker` group or a broad sudo rule, either of which is
39
+ root
40
+ - a runner that executes code from a pull request or a compromised
41
+ dependency, which gets every key the runner holds; the keys, not the uid,
42
+ set the blast radius
43
+ - reading other processes' command lines in `/proc`, world-readable home
44
+ directories, and one component exhausting memory, disk or processes for
45
+ the rest
46
+
47
+ ## Why not containers
48
+
49
+ Docker and Podman isolate with the same kernel mechanisms systemd exposes
50
+ directly: namespaces, cgroups, seccomp and capabilities. A container shares
51
+ the kernel exactly as a sandboxed unit does, so it adds no boundary. What it
52
+ adds is packaging (images, independent library versions) and a runtime to
53
+ operate. Cloudfall's base is Debian and systemd
54
+ ([`2026-09-14-no-nixos-base.md`](2026-09-14-no-nixos-base.md)), container
55
+ orchestration is a non-goal ([`../product.md`](../product.md), N-1), and the
56
+ exit test is that any Linux admin can take over a server with Debian
57
+ knowledge.
58
+
59
+ ## Why not self-run VMs
60
+
61
+ A hypervisor on Cloudfall's hosts (Proxmox, KVM, Firecracker) is a second
62
+ infrastructure layer to provision, patch, back up and explain to whoever
63
+ takes over. A separate small server gives the same kernel boundary for a
64
+ few euros a month and fits the model Cloudfall already manages.
65
+
66
+ ## The target model
67
+
68
+ The first four parts are required on every server, one server or many. The
69
+ last is a recommendation.
70
+
71
+ **Each unit is sandboxed by systemd.** The component unit gets, by default:
72
+
73
+ - its own user (or `DynamicUser=`), `NoNewPrivileges=` and an empty
74
+ `CapabilityBoundingSet=`
75
+ - `ProtectSystem=strict`, `ProtectHome=`, `PrivateTmp=`, `PrivateDevices=`,
76
+ writes only to `ReadWritePaths=` and `StateDirectory=`
77
+ - `ProtectProc=invisible`, `SystemCallFilter=@system-service`,
78
+ `RestrictNamespaces=`, `RestrictAddressFamilies=`
79
+ - `IPAddressAllow=` and `IPAddressDeny=` where the component's peers are
80
+ known
81
+ - `MemoryMax=`, `CPUQuota=`, `TasksMax=` from the component's declared
82
+ envelope ([`../fleet-goals.md`](../fleet-goals.md), goal 8)
83
+ - secrets through `LoadCredentialEncrypted=` rather than `EnvironmentFile=`,
84
+ so a value is readable by that unit only and never sits in the process
85
+ environment
86
+
87
+ `systemd-analyze security` scores a unit; a score threshold can become a
88
+ deploy gate.
89
+
90
+ **Shared services authenticate per component.** Each component gets its own
91
+ PostgreSQL role, preferably through `peer` authentication over the Unix
92
+ socket, so the unit's user is the role and no password exists. A service
93
+ without authentication is never shared between components of different
94
+ owners.
95
+
96
+ **Runners are ephemeral and sandboxed.** A runner executes untrusted code.
97
+ It runs one job per transient unit started with `systemd-run` under the same
98
+ sandbox and `DynamicUser=`, its state removed after the job, and its network
99
+ open only for the dependency step. On a server that also runs production
100
+ components, the runner's unit is also denied the production components'
101
+ directories and network peers (`InaccessiblePaths=`, `IPAddressDeny=`) and
102
+ holds no production keys. This is the most exposed point of a one-server
103
+ placement.
104
+
105
+ **Access is scoped by environment.** A runner holds the SSH and age keys of
106
+ one environment ([`../ci-deploy-guide.md`](../ci-deploy-guide.md)). Deploys
107
+ log in as a dedicated user with sudo limited to the commands a deploy runs,
108
+ not as root.
109
+
110
+ **Separate servers separate trust levels (recommended).** Where the budget
111
+ allows, these do not share a kernel:
112
+
113
+ - production and staging
114
+ - build and CI, and production; production servers then receive built
115
+ artifacts only
116
+ - components of different customers or owners
117
+
118
+ A team can only accept risk for its own projects. Placing components of
119
+ different owners on one server puts the risk on owners who did not accept
120
+ it, so Cloudfall keeps a separate, stronger warning for it. Cloudfall cannot
121
+ verify ownership, so this stays a warning, not a refusal.
122
+
123
+ ## Where Cloudfall is today
124
+
125
+ - Each component already runs as its own application user with
126
+ `NoNewPrivileges=true` and `PrivateTmp=true`
127
+ (`engine/ansible/roles/cloudfall_deploy/templates/component.service.j2`);
128
+ the rest of the sandbox is missing
129
+ - SSH keys are scoped by environment, but the baseline installs them into
130
+ root's `authorized_keys`, so a runner's key is root on every server of its
131
+ environment
132
+ - Secrets reach units through `EnvironmentFile=`
133
+ - Resource envelopes are not declared or enforced yet
134
+
135
+ ## Triggers to revisit
136
+
137
+ - **A customer needs untrusted code from several tenants on one server**,
138
+ where a separate server per tenant does not pay; then a stronger sandbox
139
+ (gVisor, microVMs) is the question, not containers
140
+ - **A required workload ships only as a container image** with no reasonable
141
+ Debian install path
142
+ - **systemd sandboxing proves insufficient** for a concrete attack that a
143
+ container runtime would have stopped
144
+
145
+ ## Consequences
146
+
147
+ - The component unit template gains the sandbox directives above as
148
+ defaults, with per-component opt-outs that are declared, not silent
149
+ - No container runtime, image build or registry appears in Cloudfall's
150
+ engine or docs as the way to isolate components
151
+ - One server is a supported placement, not a degraded one; the required
152
+ parts above carry the whole isolation there
153
+ - A plan that puts two trust levels on one server declares it as an
154
+ accepted risk; Cloudfall warns when it is undeclared, and warns again,
155
+ more strongly, when the trust levels belong to different owners
@@ -75,3 +75,11 @@ entry that supersedes it, never by editing an old one.
75
75
  - Why: psql substitutes a variable into the statement text, so ALTER ROLE ... PASSWORD with the plaintext puts the password in the server's log under log_statement ddl or all, or on a failing statement; PostgreSQL stores a pre-hashed verifier as it is
76
76
  - Applies to: engine/ansible/roles/cloudfall_postgresql/tasks/owner_password.yml, sdk/src/cloudfall/secrets.py, engine/src/cloudfall_engine/cli.py
77
77
  - Enforced by: a regression test that the staged file holds a verifier and never the password
78
+
79
+ ## D-10: Components are isolated by the systemd sandbox, and separate servers are recommended, not required
80
+
81
+ - Decided: 2026-11-02, by the maintainer; reasoning in docs/decisions/2026-11-02-isolation-without-containers.md
82
+ - Rule: On every server, components are kept apart by each unit's systemd sandbox (own user, ProtectSystem, ProtectHome, PrivateTmp, ProtectProc, SystemCallFilter, capability and address-family limits, cgroup limits), a database role per component, secrets through systemd credentials, keys scoped by environment and a non-root deploy user; runners are ephemeral sandboxed units, denied production directories, network peers and keys when they share a server with production; separate servers for different trust levels (production and staging, build or CI and production, different owners) are recommended, and a team may place everything on one server, several independent projects included, by declaring the accepted risk in the plan; Cloudfall warns about an undeclared placement of two trust levels on one server, and warns more strongly when they belong to different owners; Cloudfall adds no container runtime and no hypervisor of its own
83
+ - Why: A Unix user alone keeps only files and signals apart; containers use the same kernel mechanisms systemd exposes and add no boundary, only a runtime to operate; a separate kernel is the one stronger boundary and comes from a separate server, but requiring it would rule out the one-server teams Cloudfall serves, and a team can accept that risk for its own projects, not for other owners
84
+ - Applies to: engine/ansible/roles/cloudfall_deploy/templates/component.service.j2, engine/ansible/roles/cloudfall_access, server placement in plans, runner and CI setup
85
+ - Enforced by: review
@@ -192,8 +192,11 @@ proposals it executed and which it withheld, with reasons.
192
192
  Approval is not a tool. A person approves a proposal by running
193
193
  `cloudfall operator approve` from a shell, as in
194
194
  [Reviewing and approving](#reviewing-and-approving); an agent that could
195
- call it would run and verify the change it proposed itself. The watch
196
- loop is not a tool either: it runs until stopped, as the service above.
195
+ call it would run and verify the change it proposed itself. The same holds
196
+ for `cloudfall release approve`: the `deploy` tool deploys a release of an
197
+ application that declares `approval: manual` only once a person approved
198
+ it at a terminal, and no tool writes that approval. The watch loop is not a
199
+ tool either: it runs until stopped, as the service above.
197
200
 
198
201
  The agent is the brain here and the operator is the record. The agent can
199
202
  read the proposals, relay the diagnosis and recommend one; a person
@@ -127,6 +127,8 @@ and validation rejects them if they do.
127
127
  ```console
128
128
  uv run cloudfall migrate --build acme-api=main
129
129
  uv run cloudfall migrate --build acme-api=main --yes
130
+ uv run cloudfall release approve acme-api --release <release-id>
131
+ uv run cloudfall migrate --yes
130
132
  ```
131
133
 
132
134
  Without `--yes` the command shows the plan. With it, the plan executes step
@@ -134,7 +136,19 @@ by step: server baseline and firewall, infrastructure services, artifact
134
136
  builds, health-gated deployments, HTTP routes, a DNS-verification pause at
135
137
  the cutover moment, TLS issuance, and a final evidence pass requiring a
136
138
  compliant audit and healthy routes. Progress persists after every step, so
137
- a failure or the DNS pause resumes exactly where it stopped.
139
+ a failure or a pause resumes exactly where it stopped.
140
+
141
+ The importer writes `approval: manual` for every application, so the run
142
+ pauses at `deploy:acme-api` (exit 3, `PARTIAL_FAILURE`, with
143
+ `context.code: deployment_approval_missing`) once the release is built. The
144
+ answer names the built release and the exact `cloudfall release approve`
145
+ command. A person runs it at a terminal: it shows the git commit, the archive
146
+ SHA-256 and the servers, and records the approval under
147
+ `decisions/releases/` once the release id is typed back. `migrate --yes` then
148
+ resumes at the deploy step with the release the build step recorded,
149
+ without building again. When another gate, such as GitHub's required
150
+ reviewers, is the real one, switch the application to `approval: automatic`
151
+ and the run deploys without stopping.
138
152
 
139
153
  ## Step 6: migrate the data
140
154
 
@@ -1,6 +1,6 @@
1
1
  # S-008: Enforce manual approval
2
2
 
3
- status: approved
3
+ status: built
4
4
  serves: G-2
5
5
 
6
6
  ## Problem
@@ -186,3 +186,22 @@ Touches `sdk/src/cloudfall/app.py` (`deploy`, `rollback`, `migrate`, new `releas
186
186
  - cloudfall-dev/cloudfall#165: S-008-13, S-008-14
187
187
 
188
188
  ## Verification
189
+
190
+ Every criterion ran through the CLI (or `cloudfall mcp serve`'s tool call) against a copy of the example project, with an engine whose playbooks stand in for the real ones: they run `ansible-playbook` on localhost and leave a marker and a release receipt, so no host was contacted. The S-008-14 artifact was built for real by `cloudfall-engine artifact build` from a local git repository. The terminal attestation ran with a stream that reports itself a terminal; no person typed at a real one, and no deploy reached a real server.
191
+
192
+ - S-008-1: `test_s008_1_a_manual_application_without_approval_is_refused`, exit 4 `PRECONDITION` with `deployment_approval_missing`, the message named `crm`, `crm-backend`, the release and the command, `context.command` was the exact `release approve` command, and no marker, inventory or receipt was written
193
+ - S-008-2: `test_s008_2_a_person_approves_a_release_by_typing_its_id` and `test_s008_2_the_record_id_is_an_approval_time_and_16_hex`, exit 0 and one file under `decisions/releases/` that validated against `release-approval.schema.json` with every named field, `expiresAt` 24 hours after `approvedAt` and `via: terminal`
194
+ - S-008-3: `test_s008_3_an_approved_release_deploys_and_names_its_approval`, the deploy playbook ran and the result's `approval` carried the record's `path`, `approver` and `approved_at`
195
+ - S-008-4: `test_s008_4_off_a_terminal_nothing_is_approved` and `test_s008_4_a_mistyped_release_id_approves_nothing`, exit 4 `PERSON_REQUIRED` and `ATTESTATION_MISMATCH` with and without `--yes`, and the approvals folder stayed empty
196
+ - S-008-5: `test_s008_5_the_deploy_tool_refuses_an_unapproved_manual_release` and `test_s008_5_the_project_instructions_name_release_approve`, no `release_approve` tool was listed and the `deploy` tool with `yes: true` returned `PRECONDITION` with `deployment_approval_missing`
197
+ - S-008-6: `test_s008_6_a_record_for_another_archive_does_not_count`, `test_s008_6_a_record_for_other_servers_does_not_count`, `test_s008_6_an_expired_record_does_not_count` and `test_s008_6_the_servers_are_compared_as_a_set`, each refusal listed the record with `digest_mismatch`, `servers_changed` or `expired`, and servers in another order still counted
198
+ - S-008-7: `test_s008_7_a_record_for_another_release_or_component_does_not_count`, refused with `deployment_approval_missing` and neither record listed
199
+ - S-008-8: `test_s008_8_the_plan_shows_the_approval_before_and_after_it_is_given` and `test_s008_8_the_latest_counting_approval_is_shown`, exit 0 with no playbook run, `status: missing` with `command` before the approval and `status: approved` with `path`, `approver` and `expires_at` after it, the latest of two counting records shown
200
+ - S-008-9: `test_s008_9_an_automatic_application_needs_no_record`, plan and `--yes` both exit 0 with `approval: {"policy": "automatic"}` beside an approvals file that isn't JSON, and the deploy ran
201
+ - S-008-10: `test_s008_10_a_broken_record_stops_a_manual_deploy` and `test_s008_10_deploy_declares_record_invalid`, exit 86 `RECORD_INVALID` naming the file for invalid JSON, a schema mismatch and a non-object, plan and `--yes` alike, and `deploy --schema` declares the exit
202
+ - S-008-11: `test_s008_11_an_automatic_application_needs_no_approval`, `test_s008_11_a_missing_artifact_is_refused_before_asking` and `test_s008_11_a_tampered_artifact_is_refused_before_asking`, `PRECONDITION` with `release_approval_not_required`, `lifecycle_artifact_missing` and `lifecycle_artifact_digest_mismatch`, no prompt and no file
203
+ - S-008-12: `test_s008_12_a_validity_out_of_range_is_a_usage_error` and `test_s008_12_a_validity_of_two_hours_expires_two_hours_later`, `--valid-for 0` and `169` were usage errors naming `--valid-for`, and `2` wrote `expiresAt` two hours after `approvedAt`
204
+ - S-008-13: `test_s008_13_rollback_needs_no_approval`, for a `manual` application the plan showed `approval` with `policy: manual`, `required: false` and a reason, and `rollback --yes` ran the rollback playbook with no approvals folder at all; an `automatic` application's plan showed `{"policy": "automatic"}`
205
+ - S-008-14: `test_s008_14_migrate_pauses_for_an_approval_and_resumes_without_a_rebuild`, `migrate --build crm-backend=main --yes` exited 3 `PARTIAL_FAILURE` at `deploy:crm-backend` with `deployment_approval_missing`, the release the completed build step recorded and the `release approve` command, no deploy playbook ran; after `release approve`, with the source repository deleted so a second build could not succeed, `migrate --yes` completed `deploy:crm-backend` with that release and the record as its `approval`, and the artifacts folder held the same files. `test_s008_14_a_broken_approval_record_stops_the_deploy_step` (exit 86 `RECORD_INVALID`) and `test_s008_14_an_automatic_application_migrates_without_a_record` cover the step's other answers
206
+ - S-008-15: `test_s008_15_a_failed_health_gate_keeps_the_approval_for_a_retry`, the failed deploy exited `ENGINE_STEP_FAILED`, the record's bytes were unchanged and the retry deployed on the same record
207
+ - S-008-16: `test_s008_16_the_changelog_and_ci_guide_say_deploy_enforces_manual`, the fragment's `### Breaking` entry names `deploy --yes`, `migrate --yes`, `approval: manual`, `cloudfall release approve` and `approval: automatic`, and the CI guide names `cloudfall release approve` and no longer says no command acts on `approval`
@@ -17,6 +17,7 @@ from importlib.metadata import version
17
17
  from pathlib import Path
18
18
  from typing import TYPE_CHECKING, Self, cast
19
19
 
20
+ from cloudfall.ansible_data import as_ansible_data, unmarked
20
21
  from cloudfall.commands import snake_case_keys
21
22
  from cloudfall.domain import (
22
23
  ARTIFACT_CLONE_TIMEOUT_SECONDS,
@@ -223,8 +224,14 @@ class RenderedInventory:
223
224
  ],
224
225
  )
225
226
  def render(args: RenderArgs, _ctx: Ctx, project: Project) -> RenderedInventory:
226
- """Render the inventory, and write it when ``--output`` names a file."""
227
- rendered = render_ansible_inventory(project.inventory)
227
+ """Render the inventory, and write it when ``--output`` names a file.
228
+
229
+ Every declared string is marked unsafe, so Ansible never templates it.
230
+ """
231
+ rendered = cast(
232
+ "dict[str, object]",
233
+ as_ansible_data(render_ansible_inventory(project.inventory)),
234
+ )
228
235
  if args.output is None:
229
236
  return RenderedInventory(rendered)
230
237
  output = project.path(args.output)
@@ -595,8 +602,12 @@ def _rendered_owned_databases(
595
602
  if not inventory_file.is_file():
596
603
  return ()
597
604
  try:
605
+ # The rendered strings are marked unsafe for Ansible (#174).
598
606
  document = cast(
599
- "object", json.loads(inventory_file.read_text(encoding="utf-8"))
607
+ "object",
608
+ json.loads(
609
+ inventory_file.read_text(encoding="utf-8"), object_hook=unmarked
610
+ ),
600
611
  )
601
612
  except (UnicodeDecodeError, json.JSONDecodeError):
602
613
  return ()
@@ -0,0 +1 @@
1
+ {"all": {"children": {"cloudfall_applications": {"children": {"application_crm": {"hosts": {"h1": {}, "h2": {}}}}}, "cloudfall_components": {"children": {"component_crm_backend": {"hosts": {"h1": {}, "h2": {}}}}}, "cloudfall_domain_origins": {"hosts": {"h1": {}}}, "cloudfall_environments": {"children": {"environment_production": {"hosts": {"h1": {}, "h2": {}}}}}, "cloudfall_logging_backends": {"hosts": {"h1": {}}}, "cloudfall_logging_collectors": {"hosts": {"h1": {}, "h2": {}}}, "cloudfall_release_servers": {"hosts": {}}, "cloudfall_servers": {"hosts": {"h1": {"ansible_host": {"__ansible_unsafe": "h1.example.internal"}, "ansible_port": 22, "ansible_user": {"__ansible_unsafe": "cloudfall"}, "cloudfall_alert_rules": [{"environment": {"__ansible_unsafe": "production"}, "expr": {"__ansible_unsafe": "pg_up{service=\"postgresql-main\"} == 0"}, "for": {"__ansible_unsafe": "1m"}, "id": {"__ansible_unsafe": "postgresql-down"}, "severity": {"__ansible_unsafe": "critical"}, "summary": {"__ansible_unsafe": "PostgreSQL postgresql-main is not answering its exporter"}}], "cloudfall_applications": [{"approval": {"__ansible_unsafe": "manual"}, "id": {"__ansible_unsafe": "crm"}, "user": {"__ansible_unsafe": "crm"}}], "cloudfall_components": [{"application": {"__ansible_unsafe": "crm"}, "application_user": {"__ansible_unsafe": "crm"}, "healthCheck": {"attempts": 5, "expectedStatuses": [200], "path": {"__ansible_unsafe": "/health"}, "port": 8100, "scheme": {"__ansible_unsafe": "http"}, "timeoutSeconds": 5, "type": {"__ansible_unsafe": "http"}}, "id": {"__ansible_unsafe": "crm-backend"}, "install_root": {"__ansible_unsafe": "/srv/apps/crm/backend"}, "repository": {"url": {"__ansible_unsafe": "https://github.com/example/crm-backend.git"}}, "retainUntilCleanup": true, "runtime": {"packageManager": {"__ansible_unsafe": "uv"}, "type": {"__ansible_unsafe": "python"}, "version": {"__ansible_unsafe": "3.14"}}, "service": {"command": [{"__ansible_unsafe": ".venv/bin/uvicorn"}, {"__ansible_unsafe": "crm.asgi:application"}, {"__ansible_unsafe": "--host"}, {"__ansible_unsafe": "127.0.0.1"}, {"__ansible_unsafe": "--port"}, {"__ansible_unsafe": "8100"}], "manager": {"__ansible_unsafe": "systemd"}, "name": {"__ansible_unsafe": "crm-backend"}}}], "cloudfall_domains": [], "cloudfall_environment": {"__ansible_unsafe": "production"}, "cloudfall_firewall": {"allowedInbound": [{"description": {"__ansible_unsafe": "ssh"}, "port": 22, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "http"}, "port": 80, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "https"}, "port": 443, "protocol": {"__ansible_unsafe": "tcp"}}], "policy": {"__ansible_unsafe": "default-deny"}}, "cloudfall_hostname": {"__ansible_unsafe": "h1.example.internal"}, "cloudfall_labels": {"region": {"__ansible_unsafe": "eu-central"}, "workload": {"__ansible_unsafe": "application"}}, "cloudfall_lifecycle": {"__ansible_unsafe": "active"}, "cloudfall_logging_backend": {"alerting": {"alertmanager": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9093}, "receivers": [{"id": {"__ansible_unsafe": "operations-webhook"}, "webhook": {"url": {"__ansible_unsafe": "https://hooks.example.internal/cloudfall"}}}, {"email": {"authPasswordFile": {"__ansible_unsafe": "/etc/cloudfall/logging/smtp-password"}, "authUsername": {"__ansible_unsafe": "alerts@example.internal"}, "from": {"__ansible_unsafe": "alerts@example.internal"}, "smarthost": {"__ansible_unsafe": "smtp.example.internal:587"}, "to": {"__ansible_unsafe": "oncall@example.internal"}}, "id": {"__ansible_unsafe": "operations-email"}}]}, "backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3100, "retentionHours": 720, "server": {"__ansible_unsafe": "h1"}, "storage": {"path": {"__ansible_unsafe": "/var/lib/loki-cloudfall"}, "type": {"__ansible_unsafe": "filesystem"}}}, "collectors": {"clientTls": {"caPath": {"__ansible_unsafe": "/etc/cloudfall/logging/ca.crt"}, "certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.key"}}, "files": [{"application": {"__ansible_unsafe": "crm"}, "component": {"__ansible_unsafe": "crm-backend"}, "id": {"__ansible_unsafe": "crm-backend"}, "path": {"__ansible_unsafe": "/var/log/crm/backend/*.log"}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}], "journal": {"enabled": true, "maxAgeHours": 24}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}, "environment": {"__ansible_unsafe": "production"}, "gateway": {"listenAddress": {"__ansible_unsafe": "0.0.0.0"}, "port": 3101, "serverName": {"__ansible_unsafe": "logs.example.internal"}, "tls": {"certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.crt"}, "clientCaPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client-ca.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.key"}}}, "grafana": {"adminPasswordPath": {"__ansible_unsafe": "/etc/cloudfall/logging/grafana-admin-password"}, "listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3000}, "id": {"__ansible_unsafe": "operations"}, "metrics": {"backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9090, "retentionHours": 360, "storagePath": {"__ansible_unsafe": "/var/lib/prometheus-cloudfall"}}, "collection": {"intervalSeconds": 60}}, "migration": {"mode": {"__ansible_unsafe": "parallel"}, "preserveLegacyAgents": true}, "software": {"alloyPackageVersion": {"__ansible_unsafe": "1.17.1-1"}, "grafanaPackageVersion": {"__ansible_unsafe": "13.1.0"}, "lokiPackageVersion": {"__ansible_unsafe": "3.7.2"}}}, "cloudfall_logging_collector": {"alerting": {"alertmanager": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9093}, "receivers": [{"id": {"__ansible_unsafe": "operations-webhook"}, "webhook": {"url": {"__ansible_unsafe": "https://hooks.example.internal/cloudfall"}}}, {"email": {"authPasswordFile": {"__ansible_unsafe": "/etc/cloudfall/logging/smtp-password"}, "authUsername": {"__ansible_unsafe": "alerts@example.internal"}, "from": {"__ansible_unsafe": "alerts@example.internal"}, "smarthost": {"__ansible_unsafe": "smtp.example.internal:587"}, "to": {"__ansible_unsafe": "oncall@example.internal"}}, "id": {"__ansible_unsafe": "operations-email"}}]}, "backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3100, "retentionHours": 720, "server": {"__ansible_unsafe": "h1"}, "storage": {"path": {"__ansible_unsafe": "/var/lib/loki-cloudfall"}, "type": {"__ansible_unsafe": "filesystem"}}}, "collectors": {"clientTls": {"caPath": {"__ansible_unsafe": "/etc/cloudfall/logging/ca.crt"}, "certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.key"}}, "files": [{"application": {"__ansible_unsafe": "crm"}, "component": {"__ansible_unsafe": "crm-backend"}, "id": {"__ansible_unsafe": "crm-backend"}, "path": {"__ansible_unsafe": "/var/log/crm/backend/*.log"}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}], "journal": {"enabled": true, "maxAgeHours": 24}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}, "environment": {"__ansible_unsafe": "production"}, "gateway": {"listenAddress": {"__ansible_unsafe": "0.0.0.0"}, "port": 3101, "serverName": {"__ansible_unsafe": "logs.example.internal"}, "tls": {"certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.crt"}, "clientCaPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client-ca.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.key"}}}, "grafana": {"adminPasswordPath": {"__ansible_unsafe": "/etc/cloudfall/logging/grafana-admin-password"}, "listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3000}, "id": {"__ansible_unsafe": "operations"}, "metrics": {"backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9090, "retentionHours": 360, "storagePath": {"__ansible_unsafe": "/var/lib/prometheus-cloudfall"}}, "collection": {"intervalSeconds": 60}}, "migration": {"mode": {"__ansible_unsafe": "parallel"}, "preserveLegacyAgents": true}, "software": {"alloyPackageVersion": {"__ansible_unsafe": "1.17.1-1"}, "grafanaPackageVersion": {"__ansible_unsafe": "13.1.0"}, "lokiPackageVersion": {"__ansible_unsafe": "3.7.2"}}}, "cloudfall_origin_domains": [{"aliases": [{"__ansible_unsafe": "www.crm.example.test"}], "edge": {"mode": {"__ansible_unsafe": "dns-only"}, "provider": {"__ansible_unsafe": "cloudflare"}}, "healthCheck": {"expectedStatuses": [200], "path": {"__ansible_unsafe": "/health"}, "scheme": {"__ansible_unsafe": "https"}, "timeoutSeconds": 5}, "id": {"__ansible_unsafe": "crm-site"}, "origin": {"configurationPath": {"__ansible_unsafe": "/etc/nginx/sites-available/crm-origin.conf"}, "port": 8100, "scheme": {"__ansible_unsafe": "http"}, "server": {"__ansible_unsafe": "h1"}, "serverName": {"__ansible_unsafe": "crm.example.test"}, "service": {"__ansible_unsafe": "nginx.service"}}, "primaryName": {"__ansible_unsafe": "crm.example.test"}, "proxy": {"configurationPath": {"__ansible_unsafe": "/etc/nginx/sites-available/crm.example.test.conf"}, "server": {"__ansible_unsafe": "h2"}, "service": {"__ansible_unsafe": "nginx.service"}, "upstream": {"address": {"__ansible_unsafe": "127.0.0.1"}, "port": 8100}}, "tls": {"mode": {"__ansible_unsafe": "required"}}}], "cloudfall_server_id": {"__ansible_unsafe": "h1"}, "cloudfall_server_type": {"configuration": {"files": [{"capture": {"__ansible_unsafe": "hash"}, "group": {"__ansible_unsafe": "root"}, "mode": {"__ansible_unsafe": "0644"}, "owner": {"__ansible_unsafe": "root"}, "path": {"__ansible_unsafe": "/etc/ssh/sshd_config"}}]}, "firewall": {"allowedInbound": [{"description": {"__ansible_unsafe": "http"}, "port": 80, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "https"}, "port": 443, "protocol": {"__ansible_unsafe": "tcp"}}], "policy": {"__ansible_unsafe": "default-deny"}}, "id": {"__ansible_unsafe": "debian-application"}, "os": {"distribution": {"__ansible_unsafe": "Debian"}, "serviceManager": {"__ansible_unsafe": "systemd"}, "versions": [{"__ansible_unsafe": "12"}, {"__ansible_unsafe": "13"}]}, "packages": {"forbidden": [{"__ansible_unsafe": "telnetd"}], "required": [{"name": {"__ansible_unsafe": "acl"}}, {"name": {"__ansible_unsafe": "ca-certificates"}}, {"name": {"__ansible_unsafe": "curl"}}, {"name": {"__ansible_unsafe": "git"}}, {"name": {"__ansible_unsafe": "rsync"}}, {"name": {"__ansible_unsafe": "unzip"}}]}, "services": {"required": [{"name": {"__ansible_unsafe": "ssh.service"}, "state": {"__ansible_unsafe": "running"}, "status": {"__ansible_unsafe": "enabled"}}, {"name": {"__ansible_unsafe": "apt-daily.timer"}, "state": {"__ansible_unsafe": "running"}, "status": {"__ansible_unsafe": "enabled"}}]}, "storage": {"mounts": [{"filesystem": {"__ansible_unsafe": "ext4"}, "minimumBytes": 850000000000, "path": {"__ansible_unsafe": "/"}}], "softwareRaid": {"level": {"__ansible_unsafe": "raid1"}, "minimumActiveDevices": 2, "minimumUsableBytes": 900000000000}}}, "cloudfall_services": [{"backup": {"directory": {"__ansible_unsafe": "/var/backups/cloudfall/postgresql-main"}, "onCalendar": {"__ansible_unsafe": "*-*-* 02:00:00 UTC"}, "restoreCheckOnCalendar": {"__ansible_unsafe": "*-*-* 05:00:00 UTC"}, "retentionDays": 14}, "bind": {"address": {"__ansible_unsafe": "127.0.0.1"}, "port": 5432}, "environment": {"__ansible_unsafe": "production"}, "id": {"__ansible_unsafe": "postgresql-main"}, "metrics": {"enabled": true}, "postgresql": {"databases": [{"application": {"__ansible_unsafe": "crm"}, "name": {"__ansible_unsafe": "crm"}, "owner": {"__ansible_unsafe": "crm"}}], "majorVersion": {"__ansible_unsafe": "17"}}, "server": {"__ansible_unsafe": "h1"}, "serviceKind": {"__ansible_unsafe": "postgresql"}}, {"backup": {"directory": {"__ansible_unsafe": "/var/backups/cloudfall/redis-cache"}, "onCalendar": {"__ansible_unsafe": "*-*-* 03:00:00 UTC"}, "restoreCheckOnCalendar": {"__ansible_unsafe": "*-*-* 05:30:00 UTC"}, "retentionDays": 7}, "bind": {"address": {"__ansible_unsafe": "127.0.0.1"}, "port": 6379}, "environment": {"__ansible_unsafe": "production"}, "id": {"__ansible_unsafe": "redis-cache"}, "metrics": {"enabled": true}, "redis": {"appendOnly": true, "maxmemoryMb": 256}, "server": {"__ansible_unsafe": "h1"}, "serviceKind": {"__ansible_unsafe": "redis"}}], "cloudfall_ssh_public_keys": [{"algorithm": {"__ansible_unsafe": "ssh-rsa"}, "environment": {"__ansible_unsafe": "production"}, "id": {"__ansible_unsafe": "example-admin"}, "lifecycle": {"__ansible_unsafe": "active"}, "owner": {"__ansible_unsafe": "example-admin"}, "publicKey": {"__ansible_unsafe": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqmOqPPapyG6hL5FlR/KOK2ENEvi/2qlhoRtrgK7JfafHXb1i4WJwf5FMGrPE4DBRfDQDwokABdvUPITZOymFMgiGMmFKGnp0xq+/B0wTTOtdnr8GC3sCmSRwuFDrroVWwfjIqyC/wlpoPXpSvyc1VKBuXOe/gLv1tTzDH3Jc+3fUYaUCj+uIVO65n0N2ajNg9ibBsCjiUp9CrWEoprqxR4lz3TaiPuRtMxWQlQ2NKPSTdUZWqg9KVXm3TS8xgf4Ox2dFwedVMcucC8UBBAwetgylXNZMv1yAMYD3QDokLi0AC+XnNR+UhtFXr9Y3VveQfH4YKnv0Fhr2opCyN99Lr example"}}]}, "h2": {"ansible_host": {"__ansible_unsafe": "h2.example.internal"}, "ansible_port": 22, "ansible_user": {"__ansible_unsafe": "cloudfall"}, "cloudfall_applications": [{"approval": {"__ansible_unsafe": "manual"}, "id": {"__ansible_unsafe": "crm"}, "user": {"__ansible_unsafe": "crm"}}], "cloudfall_components": [{"application": {"__ansible_unsafe": "crm"}, "application_user": {"__ansible_unsafe": "crm"}, "healthCheck": {"attempts": 5, "expectedStatuses": [200], "path": {"__ansible_unsafe": "/health"}, "port": 8100, "scheme": {"__ansible_unsafe": "http"}, "timeoutSeconds": 5, "type": {"__ansible_unsafe": "http"}}, "id": {"__ansible_unsafe": "crm-backend"}, "install_root": {"__ansible_unsafe": "/srv/apps/crm/backend"}, "repository": {"url": {"__ansible_unsafe": "https://github.com/example/crm-backend.git"}}, "retainUntilCleanup": true, "runtime": {"packageManager": {"__ansible_unsafe": "uv"}, "type": {"__ansible_unsafe": "python"}, "version": {"__ansible_unsafe": "3.14"}}, "service": {"command": [{"__ansible_unsafe": ".venv/bin/uvicorn"}, {"__ansible_unsafe": "crm.asgi:application"}, {"__ansible_unsafe": "--host"}, {"__ansible_unsafe": "127.0.0.1"}, {"__ansible_unsafe": "--port"}, {"__ansible_unsafe": "8100"}], "manager": {"__ansible_unsafe": "systemd"}, "name": {"__ansible_unsafe": "crm-backend"}}}], "cloudfall_domains": [{"aliases": [{"__ansible_unsafe": "www.crm.example.test"}], "edge": {"mode": {"__ansible_unsafe": "dns-only"}, "provider": {"__ansible_unsafe": "cloudflare"}}, "healthCheck": {"expectedStatuses": [200], "path": {"__ansible_unsafe": "/health"}, "scheme": {"__ansible_unsafe": "https"}, "timeoutSeconds": 5}, "id": {"__ansible_unsafe": "crm-site"}, "origin": {"configurationPath": {"__ansible_unsafe": "/etc/nginx/sites-available/crm-origin.conf"}, "port": 8100, "scheme": {"__ansible_unsafe": "http"}, "server": {"__ansible_unsafe": "h1"}, "serverName": {"__ansible_unsafe": "crm.example.test"}, "service": {"__ansible_unsafe": "nginx.service"}}, "primaryName": {"__ansible_unsafe": "crm.example.test"}, "proxy": {"configurationPath": {"__ansible_unsafe": "/etc/nginx/sites-available/crm.example.test.conf"}, "server": {"__ansible_unsafe": "h2"}, "service": {"__ansible_unsafe": "nginx.service"}, "upstream": {"address": {"__ansible_unsafe": "127.0.0.1"}, "port": 8100}}, "tls": {"mode": {"__ansible_unsafe": "required"}}}], "cloudfall_environment": {"__ansible_unsafe": "production"}, "cloudfall_firewall": {"allowedInbound": [{"description": {"__ansible_unsafe": "ssh"}, "port": 22, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "http"}, "port": 80, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "https"}, "port": 443, "protocol": {"__ansible_unsafe": "tcp"}}], "policy": {"__ansible_unsafe": "default-deny"}}, "cloudfall_hostname": {"__ansible_unsafe": "h2.example.internal"}, "cloudfall_labels": {"region": {"__ansible_unsafe": "eu-central"}, "workload": {"__ansible_unsafe": "application"}}, "cloudfall_lifecycle": {"__ansible_unsafe": "active"}, "cloudfall_logging_collector": {"alerting": {"alertmanager": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9093}, "receivers": [{"id": {"__ansible_unsafe": "operations-webhook"}, "webhook": {"url": {"__ansible_unsafe": "https://hooks.example.internal/cloudfall"}}}, {"email": {"authPasswordFile": {"__ansible_unsafe": "/etc/cloudfall/logging/smtp-password"}, "authUsername": {"__ansible_unsafe": "alerts@example.internal"}, "from": {"__ansible_unsafe": "alerts@example.internal"}, "smarthost": {"__ansible_unsafe": "smtp.example.internal:587"}, "to": {"__ansible_unsafe": "oncall@example.internal"}}, "id": {"__ansible_unsafe": "operations-email"}}]}, "backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3100, "retentionHours": 720, "server": {"__ansible_unsafe": "h1"}, "storage": {"path": {"__ansible_unsafe": "/var/lib/loki-cloudfall"}, "type": {"__ansible_unsafe": "filesystem"}}}, "collectors": {"clientTls": {"caPath": {"__ansible_unsafe": "/etc/cloudfall/logging/ca.crt"}, "certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.key"}}, "files": [{"application": {"__ansible_unsafe": "crm"}, "component": {"__ansible_unsafe": "crm-backend"}, "id": {"__ansible_unsafe": "crm-backend"}, "path": {"__ansible_unsafe": "/var/log/crm/backend/*.log"}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}], "journal": {"enabled": true, "maxAgeHours": 24}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}, "environment": {"__ansible_unsafe": "production"}, "gateway": {"listenAddress": {"__ansible_unsafe": "0.0.0.0"}, "port": 3101, "serverName": {"__ansible_unsafe": "logs.example.internal"}, "tls": {"certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.crt"}, "clientCaPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client-ca.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.key"}}}, "grafana": {"adminPasswordPath": {"__ansible_unsafe": "/etc/cloudfall/logging/grafana-admin-password"}, "listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3000}, "id": {"__ansible_unsafe": "operations"}, "metrics": {"backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9090, "retentionHours": 360, "storagePath": {"__ansible_unsafe": "/var/lib/prometheus-cloudfall"}}, "collection": {"intervalSeconds": 60}}, "migration": {"mode": {"__ansible_unsafe": "parallel"}, "preserveLegacyAgents": true}, "software": {"alloyPackageVersion": {"__ansible_unsafe": "1.17.1-1"}, "grafanaPackageVersion": {"__ansible_unsafe": "13.1.0"}, "lokiPackageVersion": {"__ansible_unsafe": "3.7.2"}}}, "cloudfall_origin_domains": [], "cloudfall_server_id": {"__ansible_unsafe": "h2"}, "cloudfall_server_type": {"configuration": {"files": [{"capture": {"__ansible_unsafe": "hash"}, "group": {"__ansible_unsafe": "root"}, "mode": {"__ansible_unsafe": "0644"}, "owner": {"__ansible_unsafe": "root"}, "path": {"__ansible_unsafe": "/etc/ssh/sshd_config"}}]}, "firewall": {"allowedInbound": [{"description": {"__ansible_unsafe": "http"}, "port": 80, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "https"}, "port": 443, "protocol": {"__ansible_unsafe": "tcp"}}], "policy": {"__ansible_unsafe": "default-deny"}}, "id": {"__ansible_unsafe": "debian-application"}, "os": {"distribution": {"__ansible_unsafe": "Debian"}, "serviceManager": {"__ansible_unsafe": "systemd"}, "versions": [{"__ansible_unsafe": "12"}, {"__ansible_unsafe": "13"}]}, "packages": {"forbidden": [{"__ansible_unsafe": "telnetd"}], "required": [{"name": {"__ansible_unsafe": "acl"}}, {"name": {"__ansible_unsafe": "ca-certificates"}}, {"name": {"__ansible_unsafe": "curl"}}, {"name": {"__ansible_unsafe": "git"}}, {"name": {"__ansible_unsafe": "rsync"}}, {"name": {"__ansible_unsafe": "unzip"}}]}, "services": {"required": [{"name": {"__ansible_unsafe": "ssh.service"}, "state": {"__ansible_unsafe": "running"}, "status": {"__ansible_unsafe": "enabled"}}, {"name": {"__ansible_unsafe": "apt-daily.timer"}, "state": {"__ansible_unsafe": "running"}, "status": {"__ansible_unsafe": "enabled"}}]}, "storage": {"mounts": [{"filesystem": {"__ansible_unsafe": "ext4"}, "minimumBytes": 850000000000, "path": {"__ansible_unsafe": "/"}}], "softwareRaid": {"level": {"__ansible_unsafe": "raid1"}, "minimumActiveDevices": 2, "minimumUsableBytes": 900000000000}}}, "cloudfall_services": [], "cloudfall_ssh_public_keys": [{"algorithm": {"__ansible_unsafe": "ssh-rsa"}, "environment": {"__ansible_unsafe": "production"}, "id": {"__ansible_unsafe": "example-admin"}, "lifecycle": {"__ansible_unsafe": "active"}, "owner": {"__ansible_unsafe": "example-admin"}, "publicKey": {"__ansible_unsafe": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqmOqPPapyG6hL5FlR/KOK2ENEvi/2qlhoRtrgK7JfafHXb1i4WJwf5FMGrPE4DBRfDQDwokABdvUPITZOymFMgiGMmFKGnp0xq+/B0wTTOtdnr8GC3sCmSRwuFDrroVWwfjIqyC/wlpoPXpSvyc1VKBuXOe/gLv1tTzDH3Jc+3fUYaUCj+uIVO65n0N2ajNg9ibBsCjiUp9CrWEoprqxR4lz3TaiPuRtMxWQlQ2NKPSTdUZWqg9KVXm3TS8xgf4Ox2dFwedVMcucC8UBBAwetgylXNZMv1yAMYD3QDokLi0AC+XnNR+UhtFXr9Y3VveQfH4YKnv0Fhr2opCyN99Lr example"}}]}}}, "ungrouped": {"hosts": {}}}}}
@@ -359,10 +359,10 @@ def test_engine_cli_emits_ansible_json(
359
359
  assert document["ok"] is True
360
360
  assert document["data"]["output"] is None
361
361
  inventory = document["data"]["inventory"]
362
- assert (
363
- inventory["all"]["children"]["cloudfall_servers"]["hosts"]["h1"]["ansible_user"]
364
- == "cloudfall"
365
- )
362
+ h1 = inventory["all"]["children"]["cloudfall_servers"]["hosts"]["h1"]
363
+ # Declared strings are marked unsafe so Ansible never templates them (#174).
364
+ assert h1["ansible_user"] == {"__ansible_unsafe": "cloudfall"}
365
+ assert h1["ansible_port"] == 22
366
366
  assert (
367
367
  "component_crm_backend"
368
368
  in inventory["all"]["children"]["cloudfall_components"]["children"]
@@ -11,6 +11,7 @@ import json
11
11
  from pathlib import Path
12
12
  from typing import TYPE_CHECKING, cast
13
13
 
14
+ from cloudfall.ansible_data import unmarked
14
15
  from cloudfall_engine.cli import main
15
16
  from jinja2 import Environment, FileSystemLoader, StrictUndefined
16
17
 
@@ -58,7 +59,11 @@ def render_examples(output: Path) -> dict[str, str]:
58
59
  assert exit_code == 0
59
60
  inventory_text = inventory_path.read_text(encoding="utf-8")
60
61
  rendered = {"ansible-inventory.json": inventory_text}
61
- document = cast("dict[str, dict[str, object]]", json.loads(inventory_text))
62
+ # Read the values back as Ansible's loader does: an unsafe string is text.
63
+ document = cast(
64
+ "dict[str, dict[str, object]]",
65
+ json.loads(inventory_text, object_hook=unmarked),
66
+ )
62
67
  servers = cast(
63
68
  "dict[str, dict[str, dict[str, dict[str, object]]]]",
64
69
  document["all"]["children"],