cloudfall 0.2.0__tar.gz → 0.2.1__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 (282) hide show
  1. {cloudfall-0.2.0 → cloudfall-0.2.1}/CHANGELOG.md +24 -0
  2. {cloudfall-0.2.0 → cloudfall-0.2.1}/PKG-INFO +25 -14
  3. {cloudfall-0.2.0 → cloudfall-0.2.1}/README.md +24 -13
  4. {cloudfall-0.2.0 → cloudfall-0.2.1}/pyproject.toml +1 -1
  5. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/cli.py +14 -9
  6. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/project.py +158 -39
  7. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_agent_tools.py +8 -2
  8. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_authoring.py +8 -2
  9. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_project.py +69 -11
  10. {cloudfall-0.2.0 → cloudfall-0.2.1}/uv.lock +1 -1
  11. {cloudfall-0.2.0 → cloudfall-0.2.1}/.ansible-lint +0 -0
  12. {cloudfall-0.2.0 → cloudfall-0.2.1}/.github/workflows/ci.yml +0 -0
  13. {cloudfall-0.2.0 → cloudfall-0.2.1}/.github/workflows/release.yml +0 -0
  14. {cloudfall-0.2.0 → cloudfall-0.2.1}/.gitignore +0 -0
  15. {cloudfall-0.2.0 → cloudfall-0.2.1}/ARCHITECTURE.md +0 -0
  16. {cloudfall-0.2.0 → cloudfall-0.2.1}/CONTRIBUTING.md +0 -0
  17. {cloudfall-0.2.0 → cloudfall-0.2.1}/LICENSE +0 -0
  18. {cloudfall-0.2.0 → cloudfall-0.2.1}/MANIFESTO.md +0 -0
  19. {cloudfall-0.2.0 → cloudfall-0.2.1}/ROADMAP.md +0 -0
  20. {cloudfall-0.2.0 → cloudfall-0.2.1}/SECURITY.md +0 -0
  21. {cloudfall-0.2.0 → cloudfall-0.2.1}/Taskfile.yml +0 -0
  22. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/README.md +0 -0
  23. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/alert-rules/postgresql-down.yaml +0 -0
  24. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/applications/crm.yaml +0 -0
  25. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/components/crm-backend.yaml +0 -0
  26. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/domains/crm-site.yaml +0 -0
  27. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/logging-stacks/operations.yaml +0 -0
  28. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/operator-policies/production.yaml +0 -0
  29. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/server-types/debian-application.yaml +0 -0
  30. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/servers/h1.yaml +0 -0
  31. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/servers/h2.yaml +0 -0
  32. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/services/postgresql-main.yaml +0 -0
  33. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/services/redis-cache.yaml +0 -0
  34. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/ssh-public-keys/example-admin.yaml +0 -0
  35. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/alert-rule.schema.json +0 -0
  36. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/application.schema.json +0 -0
  37. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/artifact.schema.json +0 -0
  38. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/backup-receipt.schema.json +0 -0
  39. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/common.schema.json +0 -0
  40. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/component.schema.json +0 -0
  41. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/deployment-receipt.schema.json +0 -0
  42. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/domain.schema.json +0 -0
  43. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/environment-receipt.schema.json +0 -0
  44. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/logging-stack.schema.json +0 -0
  45. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/observed-domain.schema.json +0 -0
  46. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/observed-server.schema.json +0 -0
  47. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/operator-policy.schema.json +0 -0
  48. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/operator-proposal.schema.json +0 -0
  49. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/release-receipt.schema.json +0 -0
  50. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/server-type.schema.json +0 -0
  51. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/server.schema.json +0 -0
  52. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/service.schema.json +0 -0
  53. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/ssh-public-key.schema.json +0 -0
  54. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/invalid/missing-server/applications/crm.yaml +0 -0
  55. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/invalid/missing-server/components/crm-backend.yaml +0 -0
  56. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/invalid/missing-server/server-types/debian-application.yaml +0 -0
  57. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/invalid/missing-server/servers/h1.yaml +0 -0
  58. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/invalid/schema/components/component-unknown-field.yaml +0 -0
  59. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/observed/compliant/h1.json +0 -0
  60. {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/observed/compliant/h2.json +0 -0
  61. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/ansible-context.md +0 -0
  62. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/availability-design.md +0 -0
  63. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/brownfield-design.md +0 -0
  64. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/decisions/2026-09-14-no-ansible-replacement.md +0 -0
  65. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/decisions/2026-09-14-no-nixos-base.md +0 -0
  66. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/decisions/2026-09-14-no-rust-rewrite.md +0 -0
  67. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/fleet-goals.md +0 -0
  68. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/hybrid-storage-design.md +0 -0
  69. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/landscape.md +0 -0
  70. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/logging-service-guide.md +0 -0
  71. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/neon-migration-guide.md +0 -0
  72. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/new-server-storage-guide.md +0 -0
  73. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/operator-guide.md +0 -0
  74. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-debian-container-m1.md +0 -0
  75. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-dns-cutover.md +0 -0
  76. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-guided-data-migration.md +0 -0
  77. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-hetzner-m1.md +0 -0
  78. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-hetzner-m2-m3.md +0 -0
  79. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-hetzner-m4-m5.md +0 -0
  80. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-render-cutover.md +0 -0
  81. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-10-hetzner-m7.md +0 -0
  82. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-10-hetzner-m8.md +0 -0
  83. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-10-hetzner-m9.md +0 -0
  84. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-11-hetzner-m10.md +0 -0
  85. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/render-migration-guide.md +0 -0
  86. {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/secrets-guide.md +0 -0
  87. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/README.md +0 -0
  88. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/ansible.cfg +0 -0
  89. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/backup.yml +0 -0
  90. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/baseline.yml +0 -0
  91. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/bootstrap.yml +0 -0
  92. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/data.yml +0 -0
  93. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/deploy.yml +0 -0
  94. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/domains.yml +0 -0
  95. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/health.yml +0 -0
  96. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/inspect.yml +0 -0
  97. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/logging.yml +0 -0
  98. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/restart.yml +0 -0
  99. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/rollback.yml +0 -0
  100. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/services.yml +0 -0
  101. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/time.yml +0 -0
  102. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_access/defaults/main.yml +0 -0
  103. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_access/handlers/main.yml +0 -0
  104. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_access/meta/argument_specs.yml +0 -0
  105. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_access/meta/main.yml +0 -0
  106. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_access/tasks/main.yml +0 -0
  107. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_bootstrap/defaults/main.yml +0 -0
  108. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +0 -0
  109. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_bootstrap/meta/main.yml +0 -0
  110. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_bootstrap/tasks/main.yml +0 -0
  111. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml +0 -0
  112. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +0 -0
  113. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_data_migration/meta/main.yml +0 -0
  114. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_data_migration/tasks/main.yml +0 -0
  115. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/defaults/main.yml +0 -0
  116. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +0 -0
  117. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/meta/main.yml +0 -0
  118. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/tasks/health.yml +0 -0
  119. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +0 -0
  120. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/tasks/main.yml +0 -0
  121. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/tasks/restart.yml +0 -0
  122. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/tasks/rollback.yml +0 -0
  123. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/templates/component.service.j2 +0 -0
  124. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/defaults/main.yml +0 -0
  125. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/handlers/main.yml +0 -0
  126. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +0 -0
  127. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/meta/main.yml +0 -0
  128. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/tasks/main.yml +0 -0
  129. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +0 -0
  130. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_inspect/defaults/main.yml +0 -0
  131. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +0 -0
  132. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_inspect/meta/main.yml +0 -0
  133. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_inspect/tasks/main.yml +0 -0
  134. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/defaults/main.yml +0 -0
  135. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/handlers/main.yml +0 -0
  136. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +0 -0
  137. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/meta/main.yml +0 -0
  138. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/tasks/main.yml +0 -0
  139. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +0 -0
  140. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +0 -0
  141. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +0 -0
  142. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +0 -0
  143. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +0 -0
  144. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +0 -0
  145. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +0 -0
  146. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +0 -0
  147. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +0 -0
  148. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +0 -0
  149. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +0 -0
  150. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/defaults/main.yml +0 -0
  151. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/handlers/main.yml +0 -0
  152. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +0 -0
  153. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/meta/main.yml +0 -0
  154. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/tasks/main.yml +0 -0
  155. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +0 -0
  156. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +0 -0
  157. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/defaults/main.yml +0 -0
  158. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/handlers/main.yml +0 -0
  159. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +0 -0
  160. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/meta/main.yml +0 -0
  161. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/tasks/main.yml +0 -0
  162. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +0 -0
  163. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/defaults/main.yml +0 -0
  164. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/handlers/main.yml +0 -0
  165. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +0 -0
  166. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/meta/main.yml +0 -0
  167. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/tasks/main.yml +0 -0
  168. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +0 -0
  169. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +0 -0
  170. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +0 -0
  171. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +0 -0
  172. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +0 -0
  173. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +0 -0
  174. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +0 -0
  175. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/defaults/main.yml +0 -0
  176. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/handlers/main.yml +0 -0
  177. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/meta/argument_specs.yml +0 -0
  178. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/meta/main.yml +0 -0
  179. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/tasks/main.yml +0 -0
  180. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/backup.service.j2 +0 -0
  181. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +0 -0
  182. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +0 -0
  183. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +0 -0
  184. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +0 -0
  185. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +0 -0
  186. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +0 -0
  187. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_time/defaults/main.yml +0 -0
  188. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_time/meta/argument_specs.yml +0 -0
  189. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_time/meta/main.yml +0 -0
  190. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_time/tasks/main.yml +0 -0
  191. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +0 -0
  192. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +0 -0
  193. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +0 -0
  194. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +0 -0
  195. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/__init__.py +0 -0
  196. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/__main__.py +0 -0
  197. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/ansible_inventory.py +0 -0
  198. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/artifact.py +0 -0
  199. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/cli.py +0 -0
  200. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/playbook.py +0 -0
  201. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/py.typed +0 -0
  202. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_ansible_inventory.py +0 -0
  203. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_artifact_builder.py +0 -0
  204. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_deploy_templates.py +0 -0
  205. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_firewall_templates.py +0 -0
  206. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_logging_templates.py +0 -0
  207. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_nginx_site_templates.py +0 -0
  208. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_playbook.py +0 -0
  209. {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_postgresql_templates.py +0 -0
  210. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/README.md +0 -0
  211. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/environment.md +0 -0
  212. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/findings.md +0 -0
  213. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/issues.md +0 -0
  214. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/linkedin.md +0 -0
  215. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/readiness.md +0 -0
  216. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/report-agent-dev.md +0 -0
  217. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/report-dev.md +0 -0
  218. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/report-index.md +0 -0
  219. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/report-issues.md +0 -0
  220. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/report-runtime.md +0 -0
  221. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/trace.md +0 -0
  222. {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/x.md +0 -0
  223. {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/README.md +0 -0
  224. {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/pyproject.toml +0 -0
  225. {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/render.yaml +0 -0
  226. {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/src/demo/__init__.py +0 -0
  227. {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/src/demo/asgi.py +0 -0
  228. {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/uv.lock +0 -0
  229. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/README.md +0 -0
  230. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/__init__.py +0 -0
  231. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/__main__.py +0 -0
  232. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/agent_tools.py +0 -0
  233. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/arguments.py +0 -0
  234. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/audit.py +0 -0
  235. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/authoring.py +0 -0
  236. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/commands.py +0 -0
  237. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/cutover.py +0 -0
  238. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/dashboard.py +0 -0
  239. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/dashboard_server.py +0 -0
  240. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/domain.py +0 -0
  241. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/importer.py +0 -0
  242. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/inventory.py +0 -0
  243. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/lifecycle.py +0 -0
  244. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/mcp_server.py +0 -0
  245. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/migrate.py +0 -0
  246. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/observation.py +0 -0
  247. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/operations.py +0 -0
  248. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/operator.py +0 -0
  249. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/py.typed +0 -0
  250. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/render_api.py +0 -0
  251. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/resources.py +0 -0
  252. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/secrets.py +0 -0
  253. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/service_evidence.py +0 -0
  254. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/validation.py +0 -0
  255. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_arguments.py +0 -0
  256. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_audit.py +0 -0
  257. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_commands.py +0 -0
  258. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_cutover.py +0 -0
  259. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_dashboard_server.py +0 -0
  260. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_data_migration.py +0 -0
  261. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_importer.py +0 -0
  262. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_inventory.py +0 -0
  263. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_lifecycle.py +0 -0
  264. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_migrate.py +0 -0
  265. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_operations_dashboard.py +0 -0
  266. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_operator.py +0 -0
  267. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_render_api.py +0 -0
  268. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_resources.py +0 -0
  269. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_secrets.py +0 -0
  270. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_service_evidence.py +0 -0
  271. {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_validation.py +0 -0
  272. {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/apple-touch-icon.png +0 -0
  273. {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/favicon-32.png +0 -0
  274. {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/favicon.svg +0 -0
  275. {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/icon-192.png +0 -0
  276. {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/icon-512.png +0 -0
  277. {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/index.html +0 -0
  278. {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/manifesto.html +0 -0
  279. {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/og.png +0 -0
  280. {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/site.webmanifest +0 -0
  281. {cloudfall-0.2.0 → cloudfall-0.2.1}/site/src/worker.js +0 -0
  282. {cloudfall-0.2.0 → cloudfall-0.2.1}/site/wrangler.jsonc +0 -0
@@ -6,6 +6,30 @@ Notable changes to Cloudfall. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.1] — 2026-09-20
10
+
11
+ ### Fixed
12
+
13
+ - `cloudfall init` works from a PyPI install. An install from the package
14
+ index records no origin to read a commit from, so `init` failed with
15
+ `project_revision_unresolved` on the first command a new user runs, and
16
+ `--rev` only worked around it by writing a project that installed
17
+ Cloudfall from git instead of from the index. A project now pins whatever
18
+ ran `init`: `cloudfall==<version>` with no `[tool.uv.sources]` table for a
19
+ released install, the commit and repository for a git or source install
20
+ - The project README links the secrets guide and the reference examples at
21
+ the release tag when the pin is a released version, rather than at a
22
+ commit the install does not know
23
+
24
+ ### Changed
25
+
26
+ - `InitOptions` and the `cloudfall init` envelope carry one `pin` instead of
27
+ a `revision` and a `source`: `{"kind": "index", "version": ...}` or
28
+ `{"kind": "git", "revision": ..., "source": ...}`. `resolve_installed_pin`
29
+ replaces `resolve_installed_revision`
30
+ - `--rev` documents that it pins a commit from `--source`, which is now the
31
+ explicit alternative to the default rather than the only mechanism
32
+
9
33
  ## [0.2.0] — 2026-09-20
10
34
 
11
35
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cloudfall
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: AI-native control plane for dedicated Debian application fleets
5
5
  Project-URL: Homepage, https://cloudfall.dev
6
6
  Project-URL: Repository, https://github.com/romamo/cloudfall
@@ -160,10 +160,18 @@ is implemented yet; everything below this line is.
160
160
 
161
161
  ## Quickstart
162
162
 
163
- Cloudfall is not on PyPI yet; run it from a clone of this repository. The
164
- only prerequisite is [`uv`](https://docs.astral.sh/uv/): it provisions the
165
- required Python 3.14 interpreter and all dependencies automatically, so you
166
- do not need Python 3.14 preinstalled.
163
+ Cloudfall is on PyPI. The only prerequisite is
164
+ [`uv`](https://docs.astral.sh/uv/): it provisions the required Python 3.14
165
+ interpreter and all dependencies automatically, so you do not need Python
166
+ 3.14 preinstalled.
167
+
168
+ ```console
169
+ uv tool install cloudfall
170
+ cloudfall --help
171
+ ```
172
+
173
+ The reference resource set lives in this repository, so validating it means
174
+ a clone:
167
175
 
168
176
  ```console
169
177
  git clone https://github.com/romamo/cloudfall.git
@@ -189,13 +197,14 @@ uv run cloudfall-engine inventory render --project config/examples
189
197
  Your servers, applications, evidence, and any playbooks of your own belong in
190
198
  a project: a directory of its own, kept private. Cloudfall's wheel bundles the
191
199
  schema catalog and the Ansible engine, so the project needs no checkout of
192
- this repository. `cloudfall init` is the first command; run it straight from
193
- git or from a clone (`uv run cloudfall init ../my-project`), and it pins the
194
- project to the exact Cloudfall commit it ran from. Without a directory
195
- argument it initializes the current directory, which must be empty:
200
+ this repository. `cloudfall init` is the first command, and it pins the
201
+ project to the Cloudfall that ran it: the released version when that came
202
+ from PyPI, the exact commit when it came from git or a clone. Without a
203
+ directory argument it initializes the current directory, which must be
204
+ empty:
196
205
 
197
206
  ```console
198
- uvx --from git+https://github.com/romamo/cloudfall.git cloudfall init my-project
207
+ uvx cloudfall init my-project
199
208
  cd my-project
200
209
  uv sync
201
210
  ```
@@ -215,15 +224,17 @@ repository, and a `pyproject.toml`:
215
224
  name = "my-project"
216
225
  version = "0"
217
226
  requires-python = ">=3.14"
218
- dependencies = ["cloudfall"]
227
+ dependencies = ["cloudfall==<version>"]
219
228
 
220
229
  [tool.uv]
221
230
  package = false
222
-
223
- [tool.uv.sources]
224
- cloudfall = { git = "https://github.com/romamo/cloudfall.git", rev = "<commit>" }
225
231
  ```
226
232
 
233
+ Bump that pin and run `uv sync` to move the project to a newer Cloudfall. A
234
+ project initialized from a git or source install pins a commit instead, with
235
+ a `[tool.uv.sources]` table naming the repository and revision; `--rev` asks
236
+ for that explicitly.
237
+
227
238
  Only the kind directories are read as resources, so playbooks, roles, docs,
228
239
  and tooling files may live anywhere else in the project. Commands find the
229
240
  project on their own: the current directory when it is one, else
@@ -134,10 +134,18 @@ is implemented yet; everything below this line is.
134
134
 
135
135
  ## Quickstart
136
136
 
137
- Cloudfall is not on PyPI yet; run it from a clone of this repository. The
138
- only prerequisite is [`uv`](https://docs.astral.sh/uv/): it provisions the
139
- required Python 3.14 interpreter and all dependencies automatically, so you
140
- do not need Python 3.14 preinstalled.
137
+ Cloudfall is on PyPI. The only prerequisite is
138
+ [`uv`](https://docs.astral.sh/uv/): it provisions the required Python 3.14
139
+ interpreter and all dependencies automatically, so you do not need Python
140
+ 3.14 preinstalled.
141
+
142
+ ```console
143
+ uv tool install cloudfall
144
+ cloudfall --help
145
+ ```
146
+
147
+ The reference resource set lives in this repository, so validating it means
148
+ a clone:
141
149
 
142
150
  ```console
143
151
  git clone https://github.com/romamo/cloudfall.git
@@ -163,13 +171,14 @@ uv run cloudfall-engine inventory render --project config/examples
163
171
  Your servers, applications, evidence, and any playbooks of your own belong in
164
172
  a project: a directory of its own, kept private. Cloudfall's wheel bundles the
165
173
  schema catalog and the Ansible engine, so the project needs no checkout of
166
- this repository. `cloudfall init` is the first command; run it straight from
167
- git or from a clone (`uv run cloudfall init ../my-project`), and it pins the
168
- project to the exact Cloudfall commit it ran from. Without a directory
169
- argument it initializes the current directory, which must be empty:
174
+ this repository. `cloudfall init` is the first command, and it pins the
175
+ project to the Cloudfall that ran it: the released version when that came
176
+ from PyPI, the exact commit when it came from git or a clone. Without a
177
+ directory argument it initializes the current directory, which must be
178
+ empty:
170
179
 
171
180
  ```console
172
- uvx --from git+https://github.com/romamo/cloudfall.git cloudfall init my-project
181
+ uvx cloudfall init my-project
173
182
  cd my-project
174
183
  uv sync
175
184
  ```
@@ -189,15 +198,17 @@ repository, and a `pyproject.toml`:
189
198
  name = "my-project"
190
199
  version = "0"
191
200
  requires-python = ">=3.14"
192
- dependencies = ["cloudfall"]
201
+ dependencies = ["cloudfall==<version>"]
193
202
 
194
203
  [tool.uv]
195
204
  package = false
196
-
197
- [tool.uv.sources]
198
- cloudfall = { git = "https://github.com/romamo/cloudfall.git", rev = "<commit>" }
199
205
  ```
200
206
 
207
+ Bump that pin and run `uv sync` to move the project to a newer Cloudfall. A
208
+ project initialized from a git or source install pins a commit instead, with
209
+ a `[tool.uv.sources]` table naming the repository and revision; `--rev` asks
210
+ for that explicitly.
211
+
201
212
  Only the kind directories are read as resources, so playbooks, roles, docs,
202
213
  and tooling files may live anywhere else in the project. Commands find the
203
214
  project on their own: the current directory when it is one, else
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cloudfall"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "AI-native control plane for dedicated Debian application fleets"
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -98,6 +98,7 @@ from cloudfall.operator import (
98
98
  from cloudfall.project import (
99
99
  DEFAULT_SOURCE_URL,
100
100
  PROJECT_DIRECTORY_VARIABLE,
101
+ GitPin,
101
102
  GitRevision,
102
103
  GitSourceUrl,
103
104
  InitOptions,
@@ -106,7 +107,7 @@ from cloudfall.project import (
106
107
  ProjectName,
107
108
  init_project,
108
109
  project_context,
109
- resolve_installed_revision,
110
+ resolve_installed_pin,
110
111
  )
111
112
  from cloudfall.render_api import (
112
113
  HttpRenderApiClient,
@@ -202,8 +203,9 @@ def _add_init_parser(
202
203
  init_parser.add_argument(
203
204
  "--rev",
204
205
  help=(
205
- "Cloudfall commit to pin (default: the commit the running "
206
- "cloudfall was installed from)"
206
+ "Cloudfall commit to pin, installed from --source (default: the "
207
+ "released version of the running cloudfall, or the commit it was "
208
+ "installed from when that was a git or source install)"
207
209
  ),
208
210
  )
209
211
  init_parser.add_argument(
@@ -217,7 +219,8 @@ def _add_init_parser(
217
219
  "--source",
218
220
  default=DEFAULT_SOURCE_URL,
219
221
  help=(
220
- f"git location of Cloudfall to install from (default: {DEFAULT_SOURCE_URL})"
222
+ "git location of Cloudfall to install from, with --rev "
223
+ f"(default: {DEFAULT_SOURCE_URL})"
221
224
  ),
222
225
  )
223
226
 
@@ -1030,16 +1033,18 @@ def _run_init(arguments: Namespace) -> int:
1030
1033
  if arguments.name is not None
1031
1034
  else ProjectName.from_directory(directory)
1032
1035
  )
1033
- revision = (
1034
- GitRevision.from_boundary(arguments.rev)
1036
+ pin = (
1037
+ GitPin(
1038
+ GitRevision.from_boundary(arguments.rev),
1039
+ GitSourceUrl.from_boundary(arguments.source),
1040
+ )
1035
1041
  if arguments.rev is not None
1036
- else resolve_installed_revision()
1042
+ else resolve_installed_pin()
1037
1043
  )
1038
1044
  options = InitOptions(
1039
1045
  directory=directory,
1040
1046
  name=name,
1041
- revision=revision,
1042
- source=GitSourceUrl.from_boundary(arguments.source),
1047
+ pin=pin,
1043
1048
  description=(
1044
1049
  ProjectDescription.from_boundary(arguments.description)
1045
1050
  if arguments.description is not None
@@ -4,7 +4,8 @@ A project is one directory holding everything Cloudfall needs to run one
4
4
  user's stack: the fleet (servers, server types, SSH public keys), the
5
5
  applications (applications, components, services, domains), the operating
6
6
  declarations (alert rules, operator policies, logging stacks), and the
7
- ``pyproject.toml`` that pins the Cloudfall revision the project runs with.
7
+ ``pyproject.toml`` that pins the Cloudfall the project runs with: a
8
+ released version from the package index, or a commit in the repository.
8
9
  Every ``cloudfall`` command runs inside a project: the current directory
9
10
  when it is one, else ``--project``, else ``CLOUDFALL_PROJECT``.
10
11
  Evidence, receipts, and rendered files land under ``tmp/`` inside the
@@ -35,6 +36,9 @@ if TYPE_CHECKING:
35
36
 
36
37
  _GIT_REVISION_PATTERN = re.compile(r"^[0-9a-f]{40}$")
37
38
  _PROJECT_NAME_PATTERN = re.compile(r"^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$")
39
+ _RELEASE_VERSION_PATTERN = re.compile(
40
+ r"^\d+(?:\.\d+)*(?:(?:a|b|rc)\d+)?(?:\.post\d+)?(?:\.dev\d+)?$"
41
+ )
38
42
  _SCP_LIKE_SOURCE_PATTERN = re.compile(
39
43
  r"^(?P<user>[\w.-]+)@(?P<host>[\w.-]+):(?P<path>[\w./-]+)$"
40
44
  )
@@ -69,6 +73,7 @@ _ERROR_DIRECTORY_UNRESOLVED = "project_directory_unresolved"
69
73
  _ERROR_REVISION_UNRESOLVED = "project_revision_unresolved"
70
74
  _ERROR_REVISION_UNCOMMITTED = "project_revision_uncommitted"
71
75
  _ERROR_REVISION_UNPUBLISHED = "project_revision_unpublished"
76
+ _ERROR_VERSION_UNRESOLVED = "project_version_unresolved"
72
77
  _ERROR_NOT_A_DIRECTORY = "project_directory_not_a_directory"
73
78
  _ERROR_NOT_EMPTY = "project_directory_not_empty"
74
79
  _ERROR_GIT_UNAVAILABLE = "project_git_unavailable"
@@ -169,6 +174,109 @@ class GitSourceUrl:
169
174
  return self.value
170
175
 
171
176
 
177
+ @dataclass(frozen=True, slots=True)
178
+ class ReleaseVersion:
179
+ """Released Cloudfall version a project pins from the package index."""
180
+
181
+ value: str
182
+
183
+ def __post_init__(self) -> None:
184
+ """Enforce the release-version invariant at construction time."""
185
+ if not _RELEASE_VERSION_PATTERN.fullmatch(self.value):
186
+ message = f"release version must be a PEP 440 release: {self.value!r}"
187
+ raise ValueError(message)
188
+
189
+ @classmethod
190
+ def from_boundary(cls, value: object) -> ReleaseVersion:
191
+ """Coerce a boundary value while keeping internal APIs strictly typed."""
192
+ if not isinstance(value, str):
193
+ message = f"release version must be a string, got {type(value).__name__}"
194
+ raise TypeError(message)
195
+ return cls(value.strip())
196
+
197
+ def __str__(self) -> str:
198
+ """Return the serialized version."""
199
+ return self.value
200
+
201
+
202
+ @dataclass(frozen=True, slots=True)
203
+ class GitPin:
204
+ """A project that installs Cloudfall from a commit in the repository."""
205
+
206
+ revision: GitRevision
207
+ source: GitSourceUrl = GitSourceUrl(DEFAULT_SOURCE_URL)
208
+
209
+ @property
210
+ def requirement(self) -> str:
211
+ """Return the dependency specifier for the project's ``pyproject``."""
212
+ return "cloudfall"
213
+
214
+ @property
215
+ def sources_table(self) -> str:
216
+ """Return the ``[tool.uv.sources]`` table that resolves the commit."""
217
+ return f"""
218
+ # Cloudfall is an installed package: the schema catalog and the Ansible engine
219
+ # ship inside the wheel. Bump `rev` to move this project to a newer Cloudfall
220
+ # commit, then run `uv sync`. Add the `mcp` extra (`cloudfall[mcp]`) to run
221
+ # the `cloudfall-mcp` agent server.
222
+ [tool.uv.sources]
223
+ cloudfall = {{ git = "{self.source}", rev = "{self.revision}" }}
224
+ """
225
+
226
+ @property
227
+ def browse_url(self) -> str:
228
+ """Return the https location of the repository the pin reads from."""
229
+ return self.source.browse_url
230
+
231
+ @property
232
+ def browse_ref(self) -> str:
233
+ """Return the git ref whose files match the pinned Cloudfall."""
234
+ return str(self.revision)
235
+
236
+ def as_dict(self) -> dict[str, object]:
237
+ """Serialize the pin for system boundaries."""
238
+ return {
239
+ "kind": "git",
240
+ "revision": str(self.revision),
241
+ "source": str(self.source),
242
+ }
243
+
244
+
245
+ @dataclass(frozen=True, slots=True)
246
+ class IndexPin:
247
+ """A project that installs a released Cloudfall from the package index."""
248
+
249
+ version: ReleaseVersion
250
+
251
+ @property
252
+ def requirement(self) -> str:
253
+ """Return the dependency specifier for the project's ``pyproject``."""
254
+ return f"cloudfall=={self.version}"
255
+
256
+ @property
257
+ def sources_table(self) -> str:
258
+ """Return nothing: a released version needs no source override."""
259
+ return ""
260
+
261
+ @property
262
+ def browse_url(self) -> str:
263
+ """Return the https location of the repository the release came from."""
264
+ return GitSourceUrl(DEFAULT_SOURCE_URL).browse_url
265
+
266
+ @property
267
+ def browse_ref(self) -> str:
268
+ """Return the release tag whose files match the pinned Cloudfall."""
269
+ return f"v{self.version}"
270
+
271
+ def as_dict(self) -> dict[str, object]:
272
+ """Serialize the pin for system boundaries."""
273
+ return {"kind": "index", "version": str(self.version)}
274
+
275
+
276
+ type ProjectPin = GitPin | IndexPin
277
+ """What a project pins Cloudfall to: a released version, or a commit."""
278
+
279
+
172
280
  @dataclass(frozen=True, slots=True)
173
281
  class ProjectName:
174
282
  """Package name of a project, as written into its ``pyproject.toml``."""
@@ -255,8 +363,7 @@ class InitOptions:
255
363
 
256
364
  directory: Path
257
365
  name: ProjectName
258
- revision: GitRevision
259
- source: GitSourceUrl = GitSourceUrl(DEFAULT_SOURCE_URL)
366
+ pin: ProjectPin
260
367
  description: ProjectDescription | None = None
261
368
 
262
369
 
@@ -276,8 +383,7 @@ class ProjectScaffold:
276
383
 
277
384
  directory: Path
278
385
  name: ProjectName
279
- revision: GitRevision
280
- source: GitSourceUrl
386
+ pin: ProjectPin
281
387
  files: tuple[str, ...]
282
388
  git: GitSetup
283
389
 
@@ -288,8 +394,7 @@ class ProjectScaffold:
288
394
  "project": {
289
395
  "directory": str(self.directory),
290
396
  "name": str(self.name),
291
- "revision": str(self.revision),
292
- "source": str(self.source),
397
+ "pin": self.pin.as_dict(),
293
398
  "git": self.git.value,
294
399
  },
295
400
  "files": list(self.files),
@@ -388,33 +493,32 @@ def project_path(value: str) -> Path:
388
493
  return path
389
494
 
390
495
 
391
- def resolve_installed_revision(
496
+ def resolve_installed_pin(
392
497
  read_direct_url: Callable[[], str | None] | None = None,
393
498
  inspect_checkout: Callable[[Path], CheckoutState] | None = None,
394
- ) -> GitRevision:
395
- """Return the commit the running Cloudfall was installed from.
396
-
397
- A wheel installed from git records its commit in ``direct_url.json``; an
398
- editable source checkout records the checkout location, whose ``HEAD`` is
399
- the commit, provided the checkout is clean (else ``HEAD`` is not the code
400
- that is running) and ``HEAD`` is on a remote branch (else ``uv sync``
401
- cannot fetch it). Anything else cannot be pinned and must be given
402
- explicitly.
499
+ read_version: Callable[[], str] | None = None,
500
+ ) -> ProjectPin:
501
+ """Return what the running Cloudfall pins a new project to.
502
+
503
+ An install from the package index records no origin, and its version is
504
+ the pin: the project depends on ``cloudfall==<version>`` and resolves it
505
+ the ordinary way. A wheel installed from git records its commit in
506
+ ``direct_url.json``; an editable source checkout records the checkout
507
+ location, whose ``HEAD`` is the commit, provided the checkout is clean
508
+ (else ``HEAD`` is not the code that is running) and ``HEAD`` is on a
509
+ remote branch (else ``uv sync`` cannot fetch it). Anything else cannot be
510
+ pinned and must be given explicitly.
403
511
  """
404
512
  direct_url = (read_direct_url or _installed_direct_url)()
405
513
  if direct_url is None:
406
- message = (
407
- "the installed cloudfall distribution records no origin; "
408
- "pass --rev with the commit to pin"
409
- )
410
- raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
514
+ return IndexPin(_installed_release_version(read_version))
411
515
  record = json.loads(direct_url)
412
516
  if not isinstance(record, dict):
413
517
  message = f"direct_url.json is not an object: {direct_url!r}"
414
518
  raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
415
519
  vcs_info = record.get("vcs_info")
416
520
  if isinstance(vcs_info, dict) and "commit_id" in vcs_info:
417
- return GitRevision.from_boundary(vcs_info["commit_id"])
521
+ return GitPin(GitRevision.from_boundary(vcs_info["commit_id"]))
418
522
  dir_info = record.get("dir_info")
419
523
  url = record.get("url")
420
524
  if (
@@ -424,7 +528,7 @@ def resolve_installed_revision(
424
528
  ):
425
529
  checkout = Path(urlparse(url).path)
426
530
  inspect = inspect_checkout or _inspect_checkout
427
- return _pinnable_head(checkout, inspect(checkout))
531
+ return GitPin(_pinnable_head(checkout, inspect(checkout)))
428
532
  message = (
429
533
  "the installed cloudfall distribution is neither a git install nor a "
430
534
  f"source checkout ({url!r}); pass --rev with the commit to pin"
@@ -449,6 +553,25 @@ def _pinnable_head(checkout: Path, state: CheckoutState) -> GitRevision:
449
553
  return head
450
554
 
451
555
 
556
+ def _installed_release_version(
557
+ read_version: Callable[[], str] | None = None,
558
+ ) -> ReleaseVersion:
559
+ raw = (read_version or _installed_version)()
560
+ try:
561
+ return ReleaseVersion.from_boundary(raw)
562
+ except ValueError as error:
563
+ message = (
564
+ "the installed cloudfall distribution records no origin and its "
565
+ f"version {raw!r} is not a release, so there is nothing to pin; "
566
+ "pass --rev with the commit to pin"
567
+ )
568
+ raise ProjectError(_ERROR_VERSION_UNRESOLVED, message) from error
569
+
570
+
571
+ def _installed_version() -> str:
572
+ return metadata.version("cloudfall")
573
+
574
+
452
575
  def _installed_direct_url() -> str | None:
453
576
  return metadata.distribution("cloudfall").read_text("direct_url.json")
454
577
 
@@ -543,8 +666,7 @@ def init_project(
543
666
  return ProjectScaffold(
544
667
  directory=directory,
545
668
  name=options.name,
546
- revision=options.revision,
547
- source=options.source,
669
+ pin=options.pin,
548
670
  files=tuple(files),
549
671
  git=git,
550
672
  )
@@ -561,18 +683,15 @@ name = "{options.name}"
561
683
  version = "0"
562
684
  description = "{description}"
563
685
  requires-python = ">=3.14"
564
- dependencies = ["cloudfall"]
686
+ # Cloudfall is an installed package: the schema catalog and the Ansible engine
687
+ # ship inside the wheel. Bump this pin to move the project to a newer
688
+ # Cloudfall, then run `uv sync`. Add the `mcp` extra (`cloudfall[mcp]`) to run
689
+ # the `cloudfall-mcp` agent server.
690
+ dependencies = ["{options.pin.requirement}"]
565
691
 
566
692
  [tool.uv]
567
693
  package = false
568
-
569
- # Cloudfall is an installed package: the schema catalog and the Ansible engine
570
- # ship inside the wheel. Bump `rev` to move this project to a newer Cloudfall
571
- # commit, then run `uv sync`. Add the `mcp` extra (`cloudfall[mcp]`) to run
572
- # the `cloudfall-mcp` agent server.
573
- [tool.uv.sources]
574
- cloudfall = {{ git = "{options.source}", rev = "{options.revision}" }}
575
- """
694
+ {options.pin.sources_table}"""
576
695
 
577
696
 
578
697
  _GITIGNORE = f"""# Runtime state: evidence, receipts, rendered inventory, built
@@ -603,10 +722,10 @@ creation_rules:
603
722
  def _readme(options: InitOptions, git: GitSetup) -> str:
604
723
  tmp = RUNTIME_DIRECTORY
605
724
  secrets = SECRETS_DIRECTORY
606
- browse = options.source.browse_url
607
- revision = options.revision
608
- secrets_guide = f"{browse}/blob/{revision}/{SECRETS_GUIDE_PATH}"
609
- examples = f"{browse}/tree/{revision}/{EXAMPLES_PATH}"
725
+ browse = options.pin.browse_url
726
+ ref = options.pin.browse_ref
727
+ secrets_guide = f"{browse}/blob/{ref}/{SECRETS_GUIDE_PATH}"
728
+ examples = f"{browse}/tree/{ref}/{EXAMPLES_PATH}"
610
729
  about = (
611
730
  str(options.description)
612
731
  if options.description is not None
@@ -8,7 +8,13 @@ from pathlib import Path
8
8
  import pytest
9
9
  import yaml
10
10
  from cloudfall.agent_tools import AgentConfig, AgentToolset
11
- from cloudfall.project import GitRevision, InitOptions, ProjectName, init_project
11
+ from cloudfall.project import (
12
+ GitPin,
13
+ GitRevision,
14
+ InitOptions,
15
+ ProjectName,
16
+ init_project,
17
+ )
12
18
 
13
19
  ROOT = Path(__file__).parents[2]
14
20
  SCHEMAS = ROOT / "config" / "schemas" / "v1"
@@ -141,7 +147,7 @@ def _fresh_project(tmp_path: Path) -> AgentToolset:
141
147
  InitOptions(
142
148
  directory=directory,
143
149
  name=ProjectName("project"),
144
- revision=GitRevision(REVISION),
150
+ pin=GitPin(GitRevision(REVISION)),
145
151
  )
146
152
  )
147
153
  return AgentToolset(_config(tmp_path, project_directory=directory))
@@ -17,7 +17,13 @@ from cloudfall.authoring import (
17
17
  )
18
18
  from cloudfall.cli import main
19
19
  from cloudfall.domain import ConnectionAddress, LinuxUser, ResourceId, TcpPort
20
- from cloudfall.project import GitRevision, InitOptions, ProjectName, init_project
20
+ from cloudfall.project import (
21
+ GitPin,
22
+ GitRevision,
23
+ InitOptions,
24
+ ProjectName,
25
+ init_project,
26
+ )
21
27
  from cloudfall.validation import ConfigValidationError, validate_config
22
28
 
23
29
  ROOT = Path(__file__).parents[2]
@@ -46,7 +52,7 @@ def _project(tmp_path: Path) -> Path:
46
52
  InitOptions(
47
53
  directory=directory,
48
54
  name=ProjectName("project"),
49
- revision=GitRevision(REVISION),
55
+ pin=GitPin(GitRevision(REVISION)),
50
56
  )
51
57
  )
52
58
  return directory