nullforge 0.2.0__tar.gz → 0.3.0__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 (230) hide show
  1. {nullforge-0.2.0 → nullforge-0.3.0}/.github/labeler.yaml +2 -0
  2. {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/ci-code-quality.yaml +8 -2
  3. nullforge-0.3.0/.github/workflows/ci-docs.yaml +37 -0
  4. {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/ci-tests.yaml +2 -2
  5. nullforge-0.3.0/.github/workflows/deploy-docs.yaml +62 -0
  6. {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/release-please.yaml +3 -3
  7. nullforge-0.3.0/.markdownlint-cli2.jsonc +25 -0
  8. {nullforge-0.2.0 → nullforge-0.3.0}/.pre-commit-config.yaml +8 -3
  9. nullforge-0.3.0/.release-please-manifest.json +3 -0
  10. {nullforge-0.2.0 → nullforge-0.3.0}/CHANGELOG.md +19 -0
  11. nullforge-0.3.0/PKG-INFO +46 -0
  12. nullforge-0.3.0/README.md +29 -0
  13. nullforge-0.3.0/docs/concepts/architecture.md +61 -0
  14. nullforge-0.3.0/docs/concepts/inventories.md +165 -0
  15. nullforge-0.3.0/docs/concepts/molds.md +69 -0
  16. nullforge-0.3.0/docs/concepts/runes.md +74 -0
  17. nullforge-0.3.0/docs/contributing/conventions.md +72 -0
  18. nullforge-0.3.0/docs/contributing/development.md +68 -0
  19. nullforge-0.3.0/docs/features/base.md +71 -0
  20. nullforge-0.3.0/docs/features/containers.md +46 -0
  21. nullforge-0.3.0/docs/features/dns.md +58 -0
  22. nullforge-0.3.0/docs/features/haproxy.md +32 -0
  23. nullforge-0.3.0/docs/features/index.md +29 -0
  24. nullforge-0.3.0/docs/features/monitoring.md +60 -0
  25. nullforge-0.3.0/docs/features/netsec.md +97 -0
  26. nullforge-0.3.0/docs/features/profiles.md +56 -0
  27. nullforge-0.3.0/docs/features/telemt.md +62 -0
  28. nullforge-0.3.0/docs/features/tor.md +29 -0
  29. nullforge-0.3.0/docs/features/users.md +54 -0
  30. nullforge-0.3.0/docs/features/warp.md +56 -0
  31. nullforge-0.3.0/docs/features/xray.md +30 -0
  32. nullforge-0.3.0/docs/features/zerotrust.md +41 -0
  33. nullforge-0.3.0/docs/getting-started/cli.md +85 -0
  34. nullforge-0.3.0/docs/getting-started/installation.md +40 -0
  35. nullforge-0.3.0/docs/getting-started/quickstart.md +75 -0
  36. nullforge-0.3.0/docs/guides/custom-runes.md +79 -0
  37. nullforge-0.3.0/docs/guides/version-pinning.md +60 -0
  38. nullforge-0.3.0/docs/index.md +72 -0
  39. nullforge-0.3.0/nullforge/__init__.py +1 -0
  40. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/containers.py +8 -6
  41. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/profiles.py +11 -9
  42. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/xray.py +12 -5
  43. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/cloudflare.py +3 -2
  44. nullforge-0.3.0/nullforge/smithy/http.py +186 -0
  45. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/install.py +2 -3
  46. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/monitoring/nezha/deploy.py +2 -2
  47. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/versions.py +3 -3
  48. nullforge-0.3.0/pyproject.toml +136 -0
  49. nullforge-0.3.0/tests/__init__.py +0 -0
  50. nullforge-0.3.0/tests/cli/__init__.py +0 -0
  51. nullforge-0.3.0/tests/cli/components/__init__.py +0 -0
  52. nullforge-0.3.0/tests/cli/components/completion/__init__.py +0 -0
  53. nullforge-0.3.0/tests/cli/components/completion/test_controller.py +77 -0
  54. nullforge-0.3.0/tests/cli/components/completion/test_powershell.py +97 -0
  55. nullforge-0.3.0/tests/cli/components/foundry/__init__.py +0 -0
  56. nullforge-0.3.0/tests/cli/components/foundry/test_controller.py +141 -0
  57. nullforge-0.3.0/tests/cli/conftest.py +20 -0
  58. nullforge-0.3.0/tests/cli/core/__init__.py +0 -0
  59. nullforge-0.3.0/tests/cli/core/test_application.py +120 -0
  60. nullforge-0.3.0/tests/cli/test_types.py +127 -0
  61. nullforge-0.3.0/tests/conftest.py +50 -0
  62. nullforge-0.3.0/tests/test_foundry.py +56 -0
  63. nullforge-0.3.0/tests/test_foundry_cast.py +60 -0
  64. nullforge-0.3.0/tests/test_foundry_pyinfra.py +94 -0
  65. nullforge-0.3.0/tests/test_models_dns.py +42 -0
  66. nullforge-0.3.0/tests/test_molds_dns.py +50 -0
  67. nullforge-0.3.0/tests/test_molds_monitoring.py +78 -0
  68. nullforge-0.3.0/tests/test_molds_netsec.py +44 -0
  69. nullforge-0.3.0/tests/test_molds_telemt.py +65 -0
  70. nullforge-0.3.0/tests/test_molds_user.py +21 -0
  71. nullforge-0.3.0/tests/test_molds_utils.py +177 -0
  72. nullforge-0.3.0/tests/test_runes_dns.py +19 -0
  73. nullforge-0.3.0/tests/test_runes_meta.py +163 -0
  74. nullforge-0.3.0/tests/test_runes_misc.py +33 -0
  75. nullforge-0.3.0/tests/test_runes_netsec.py +204 -0
  76. nullforge-0.3.0/tests/test_runes_profiles.py +114 -0
  77. nullforge-0.3.0/tests/test_runes_telemt.py +105 -0
  78. nullforge-0.3.0/tests/test_runes_users.py +62 -0
  79. nullforge-0.3.0/tests/test_smithy_arch.py +53 -0
  80. nullforge-0.3.0/tests/test_smithy_github.py +153 -0
  81. nullforge-0.3.0/tests/test_smithy_http.py +312 -0
  82. nullforge-0.3.0/tests/test_smithy_install.py +119 -0
  83. nullforge-0.3.0/tests/test_smithy_network.py +128 -0
  84. nullforge-0.3.0/tests/test_smithy_nezha.py +44 -0
  85. nullforge-0.3.0/tests/test_smithy_nezha_dashboard.py +84 -0
  86. nullforge-0.3.0/tests/test_smithy_packages.py +44 -0
  87. nullforge-0.3.0/tests/test_smithy_service.py +61 -0
  88. nullforge-0.3.0/tests/test_smithy_sni.py +224 -0
  89. nullforge-0.3.0/tests/test_smithy_swap.py +54 -0
  90. nullforge-0.3.0/tests/test_smithy_system.py +101 -0
  91. nullforge-0.3.0/tests/test_smithy_versions.py +178 -0
  92. nullforge-0.3.0/tests/test_templates.py +19 -0
  93. nullforge-0.3.0/tests/test_templates_telemt.py +108 -0
  94. {nullforge-0.2.0 → nullforge-0.3.0}/uv.lock +328 -63
  95. nullforge-0.3.0/zensical.toml +110 -0
  96. nullforge-0.2.0/.release-please-manifest.json +0 -3
  97. nullforge-0.2.0/PKG-INFO +0 -68
  98. nullforge-0.2.0/README.md +0 -52
  99. nullforge-0.2.0/nullforge/__init__.py +0 -1
  100. nullforge-0.2.0/nullforge/smithy/http.py +0 -85
  101. nullforge-0.2.0/pyproject.toml +0 -45
  102. {nullforge-0.2.0 → nullforge-0.3.0}/.gitattributes +0 -0
  103. {nullforge-0.2.0 → nullforge-0.3.0}/.github/CODEOWNERS +0 -0
  104. {nullforge-0.2.0 → nullforge-0.3.0}/.github/CONTRIBUTING.md +0 -0
  105. {nullforge-0.2.0 → nullforge-0.3.0}/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml +0 -0
  106. {nullforge-0.2.0 → nullforge-0.3.0}/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yaml +0 -0
  107. {nullforge-0.2.0 → nullforge-0.3.0}/.github/ISSUE_TEMPLATE/config.yaml +0 -0
  108. {nullforge-0.2.0 → nullforge-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  109. {nullforge-0.2.0 → nullforge-0.3.0}/.github/dependabot.yml +0 -0
  110. {nullforge-0.2.0 → nullforge-0.3.0}/.github/labels.yaml +0 -0
  111. {nullforge-0.2.0 → nullforge-0.3.0}/.github/release.yaml +0 -0
  112. {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/check-commit-message-pr.yaml +0 -0
  113. {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/label-prs.yaml +0 -0
  114. {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/labels.yaml +0 -0
  115. {nullforge-0.2.0 → nullforge-0.3.0}/.gitignore +0 -0
  116. {nullforge-0.2.0 → nullforge-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  117. {nullforge-0.2.0 → nullforge-0.3.0}/LICENSE +0 -0
  118. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/__init__.py +0 -0
  119. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/__main__.py +0 -0
  120. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/app.py +0 -0
  121. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/__init__.py +0 -0
  122. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/completion/__init__.py +0 -0
  123. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/completion/component.py +0 -0
  124. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/completion/controller.py +0 -0
  125. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/completion/errors.py +0 -0
  126. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/completion/powershell.py +0 -0
  127. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/foundry/__init__.py +0 -0
  128. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/foundry/component.py +0 -0
  129. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/foundry/controller.py +0 -0
  130. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/foundry/errors.py +0 -0
  131. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/runes/__init__.py +0 -0
  132. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/runes/component.py +0 -0
  133. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/runes/controller.py +0 -0
  134. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/__init__.py +0 -0
  135. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/application.py +0 -0
  136. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/component.py +0 -0
  137. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/controller.py +0 -0
  138. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/errors.py +0 -0
  139. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/types.py +0 -0
  140. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/display.py +0 -0
  141. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/types.py +0 -0
  142. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/foundry/README.md +0 -0
  143. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/foundry/__init__.py +0 -0
  144. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/foundry/_pyinfra.py +0 -0
  145. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/foundry/cast.py +0 -0
  146. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/foundry/full_cast.py +0 -0
  147. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/inventories/README.md +0 -0
  148. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/inventories/example.py +0 -0
  149. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/__init__.py +0 -0
  150. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/containers.py +0 -0
  151. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/dns.py +0 -0
  152. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/monitoring/__init__.py +0 -0
  153. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/monitoring/base.py +0 -0
  154. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/monitoring/nezha.py +0 -0
  155. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/netsec.py +0 -0
  156. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/profiles.py +0 -0
  157. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/system.py +0 -0
  158. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/users.py +0 -0
  159. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/warp.py +0 -0
  160. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/zerotrust.py +0 -0
  161. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/__init__.py +0 -0
  162. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/base_mold.py +0 -0
  163. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/containers.py +0 -0
  164. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/defaults.py +0 -0
  165. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/dns.py +0 -0
  166. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/features.py +1 -1
  167. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/haproxy.py +0 -0
  168. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/monitoring/__init__.py +0 -0
  169. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/monitoring/nezha.py +0 -0
  170. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/netsec.py +0 -0
  171. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/profiles.py +0 -0
  172. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/system.py +0 -0
  173. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/telemt.py +0 -0
  174. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/tor.py +0 -0
  175. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/user.py +0 -0
  176. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/utils.py +0 -0
  177. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/warp.py +0 -0
  178. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/xray.py +0 -0
  179. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/zerotrust.py +0 -0
  180. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/__init__.py +0 -0
  181. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/base.py +0 -0
  182. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/dns.py +0 -0
  183. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/haproxy.py +0 -0
  184. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/monitoring.py +0 -0
  185. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/netsec.py +0 -0
  186. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/prepare.py +0 -0
  187. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/telemt.py +0 -0
  188. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/tor.py +0 -0
  189. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/users.py +0 -0
  190. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/warp.py +0 -0
  191. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/zerotrust.py +0 -0
  192. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/__init__.py +0 -0
  193. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/admin.py +0 -0
  194. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/arch.py +0 -0
  195. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/blocky.py +0 -0
  196. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/github.py +0 -0
  197. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/monitoring/__init__.py +0 -0
  198. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/monitoring/nezha/__init__.py +0 -0
  199. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/monitoring/nezha/agent.py +0 -0
  200. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/monitoring/nezha/dashboard.py +0 -0
  201. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/network.py +0 -0
  202. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/packages.py +0 -0
  203. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/service.py +0 -0
  204. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/sni.py +0 -0
  205. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/swap.py +0 -0
  206. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/system.py +0 -0
  207. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/__init__.py +0 -0
  208. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/cloudflared/tunnel.yml.j2 +0 -0
  209. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/dns/blocky.yaml.j2 +0 -0
  210. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/dns/dns.yaml.j2 +0 -0
  211. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/dns/resolv.conf.j2 +0 -0
  212. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/dns/resolved.conf.j2 +0 -0
  213. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/etc/default/zramswap.j2 +0 -0
  214. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/nvim/nvim_patch.lua.j2 +0 -0
  215. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/profiles/direnv.toml +0 -0
  216. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/profiles/starship.toml +0 -0
  217. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/profiles/tmux.conf +0 -0
  218. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/profiles/zshrc.j2 +0 -0
  219. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/scripts/telemt-synfix.sh +0 -0
  220. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/scripts/teleproxy-warp.sh +0 -0
  221. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/scripts/warp-v6-policy.sh +0 -0
  222. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/scripts/zt-tunnel-warp.sh +0 -0
  223. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/systemd/blocky.service.j2 +0 -0
  224. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/systemd/cloudflare-tunnel.service.j2 +0 -0
  225. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/systemd/cloudflare-warp.service.j2 +0 -0
  226. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/systemd/dns-internal.service.j2 +0 -0
  227. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/systemd/telemt.service.j2 +0 -0
  228. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/telemt/telemt.toml.j2 +0 -0
  229. {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/tor/torrc.j2 +0 -0
  230. {nullforge-0.2.0 → nullforge-0.3.0}/release-please-config.json +0 -0
@@ -19,9 +19,11 @@ labels:
19
19
  files:
20
20
  - '^README\.md$'
21
21
  - "^docs/"
22
+ - '^zensical\.toml$'
22
23
  - label: "ci-cd"
23
24
  files:
24
25
  - '^\.github/'
26
+ - '^\.markdownlint-cli2\.jsonc$'
25
27
  - label: "tests"
26
28
  files:
27
29
  - "^tests/"
@@ -25,7 +25,7 @@ jobs:
25
25
  with:
26
26
  persist-credentials: false
27
27
 
28
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
28
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
29
29
  with:
30
30
  enable-cache: true
31
31
  cache-dependency-glob: uv.lock
@@ -41,6 +41,12 @@ jobs:
41
41
  - name: Type check
42
42
  run: uv run ty check
43
43
 
44
+ - name: Check import contracts
45
+ run: uv run lint-imports
46
+
47
+ - name: Check dependency hygiene
48
+ run: uv run deptry .
49
+
44
50
  hooks:
45
51
  name: 🪝 prek hooks
46
52
  runs-on: ubuntu-latest
@@ -50,7 +56,7 @@ jobs:
50
56
  with:
51
57
  persist-credentials: false
52
58
 
53
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
59
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
54
60
  with:
55
61
  enable-cache: true
56
62
  cache-dependency-glob: uv.lock
@@ -0,0 +1,37 @@
1
+ name: 📚 Docs build check
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - "docs/**"
9
+ - "zensical.toml"
10
+ - "pyproject.toml"
11
+ - "uv.lock"
12
+ - ".github/workflows/ci-docs.yaml"
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ concurrency:
18
+ group: ${{ github.workflow }}-${{ github.ref }}
19
+ cancel-in-progress: true
20
+
21
+ jobs:
22
+ build:
23
+ name: 📚 Build documentation
24
+ runs-on: ubuntu-latest
25
+ timeout-minutes: 10
26
+ steps:
27
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
28
+ with:
29
+ persist-credentials: false
30
+
31
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
32
+ with:
33
+ enable-cache: true
34
+ cache-dependency-glob: uv.lock
35
+
36
+ - name: Build documentation
37
+ run: uv run --group docs zensical build --clean --strict
@@ -25,7 +25,7 @@ jobs:
25
25
  with:
26
26
  persist-credentials: false
27
27
 
28
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
28
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
29
29
  with:
30
30
  enable-cache: true
31
31
  cache-dependency-glob: uv.lock
@@ -43,7 +43,7 @@ jobs:
43
43
  with:
44
44
  persist-credentials: false
45
45
 
46
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
46
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
47
47
  with:
48
48
  enable-cache: true
49
49
  cache-dependency-glob: uv.lock
@@ -0,0 +1,62 @@
1
+ name: 📚 Deploy documentation
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - "docs/**"
9
+ - "zensical.toml"
10
+ - "pyproject.toml"
11
+ - "uv.lock"
12
+ - ".github/workflows/deploy-docs.yaml"
13
+ workflow_dispatch:
14
+
15
+ concurrency:
16
+ group: pages
17
+ cancel-in-progress: false
18
+
19
+ permissions:
20
+ contents: read
21
+
22
+ jobs:
23
+ build:
24
+ name: 📚 Build documentation
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 10
27
+ permissions:
28
+ contents: read
29
+ pages: read
30
+ steps:
31
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32
+ with:
33
+ persist-credentials: false
34
+
35
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
36
+ with:
37
+ enable-cache: true
38
+ cache-dependency-glob: uv.lock
39
+
40
+ - name: Build documentation
41
+ run: uv run --group docs zensical build --clean --strict
42
+
43
+ - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
44
+
45
+ - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
46
+ with:
47
+ path: site
48
+
49
+ deploy:
50
+ name: 🚀 Deploy to GitHub Pages
51
+ needs: build
52
+ runs-on: ubuntu-latest
53
+ timeout-minutes: 10
54
+ permissions:
55
+ pages: write
56
+ id-token: write
57
+ environment:
58
+ name: github-pages
59
+ url: ${{ steps.deployment.outputs.page_url }}
60
+ steps:
61
+ - id: deployment
62
+ uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
@@ -52,7 +52,7 @@ jobs:
52
52
  with:
53
53
  persist-credentials: false
54
54
 
55
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
55
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
56
56
  with:
57
57
  enable-cache: false
58
58
 
@@ -95,7 +95,7 @@ jobs:
95
95
  path: dist/
96
96
 
97
97
  - name: Generate artifact attestations
98
- uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
98
+ uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
99
99
  with:
100
100
  subject-path: dist/*
101
101
 
@@ -111,7 +111,7 @@ jobs:
111
111
  permissions:
112
112
  id-token: write
113
113
  steps:
114
- - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
114
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
115
115
 
116
116
  - name: Download distribution artifacts
117
117
  uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -0,0 +1,25 @@
1
+ {
2
+ "gitignore": true,
3
+
4
+ // Generated, boilerplate and vendored - not authored to this style.
5
+ "ignores": [
6
+ "CHANGELOG.md",
7
+ ".github/PULL_REQUEST_TEMPLATE.md",
8
+ ".github/ISSUE_TEMPLATE/**",
9
+ ],
10
+
11
+ "config": {
12
+ "default": true,
13
+
14
+ // Prose is one sentence per line; tables pad cells but not the delimiter row.
15
+ "MD013": false,
16
+ "MD060": false,
17
+
18
+ "MD046": false,
19
+
20
+ // mkdocs admonition blocks
21
+ "MD033": {
22
+ "allowed_elements": ["div"],
23
+ },
24
+ },
25
+ }
@@ -5,7 +5,7 @@ default_install_hook_types:
5
5
  - post-rewrite
6
6
  repos:
7
7
  - repo: https://github.com/tox-dev/pyproject-fmt
8
- rev: v2.25.3
8
+ rev: v2.28.0
9
9
  hooks:
10
10
  - id: pyproject-fmt
11
11
 
@@ -19,12 +19,12 @@ repos:
19
19
  - id: end-of-file-fixer
20
20
 
21
21
  - repo: https://github.com/astral-sh/uv-pre-commit
22
- rev: 0.11.32
22
+ rev: 0.12.5
23
23
  hooks:
24
24
  - id: uv-lock
25
25
 
26
26
  - repo: https://github.com/astral-sh/ruff-pre-commit
27
- rev: v0.16.0
27
+ rev: v0.16.3
28
28
  hooks:
29
29
  - id: ruff-check
30
30
  args: [--fix]
@@ -40,3 +40,8 @@ repos:
40
40
  - id: python-no-log-warn
41
41
  - id: python-use-type-annotations
42
42
  - id: text-unicode-replacement-char
43
+
44
+ - repo: https://github.com/DavidAnson/markdownlint-cli2
45
+ rev: v0.23.2
46
+ hooks:
47
+ - id: markdownlint-cli2
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.3.0"
3
+ }
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0](https://github.com/wlix13/NullForge/compare/v0.2.0...v0.3.0) (2026-08-28)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **molds:** deploy warp before other features
9
+ * **smithy:** fall back to WARP interface for filtered downloads
10
+
11
+ ### Features
12
+
13
+ * **molds:** deploy warp before other features ([a97cbfe](https://github.com/wlix13/NullForge/commit/a97cbfe9020bf0708acec4adf370cfa7113ea964))
14
+ * **smithy:** fall back to WARP interface for filtered downloads ([25f0b28](https://github.com/wlix13/NullForge/commit/25f0b28cb7d0efb89cf7710aa922d52a9651c985))
15
+
16
+
17
+ ### Documentation
18
+
19
+ * describe WARP download fallback ([8b57441](https://github.com/wlix13/NullForge/commit/8b57441c8ecc234db4f9131e5ab9a603e3384b4c))
20
+ * **site:** add documentation ([d6b8575](https://github.com/wlix13/NullForge/commit/d6b857541d8ed35a5fe8fa3a32cdef95d8d8bf8c))
21
+
3
22
  ## [0.2.0](https://github.com/wlix13/NullForge/compare/v0.1.0...v0.2.0) (2026-07-25)
4
23
 
5
24
 
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.5
2
+ Name: nullforge
3
+ Version: 0.3.0
4
+ Summary: Forge the server's baseline from null
5
+ Author-email: wlix13 <github@wlix13.dev>
6
+ Maintainer-email: wlix13 <github@wlix13.dev>
7
+ License-File: LICENSE
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Requires-Python: <3.14,>=3.13
11
+ Requires-Dist: click
12
+ Requires-Dist: pydantic
13
+ Requires-Dist: pyinfra
14
+ Requires-Dist: rich
15
+ Requires-Dist: rich-click
16
+ Description-Content-Type: text/markdown
17
+
18
+ # NullForge
19
+
20
+ [![PyPI](https://img.shields.io/pypi/v/nullforge?logo=pypi&logoColor=white)](https://pypi.org/project/nullforge/)
21
+ ![Python](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=white)
22
+ ![Build](https://img.shields.io/github/actions/workflow/status/wlix13/NullForge/ci-tests.yaml?label=build&logo=github)
23
+ ![Lint](https://img.shields.io/github/actions/workflow/status/wlix13/NullForge/ci-code-quality.yaml?label=lint&logo=github)
24
+ ![License](https://img.shields.io/badge/license-MIT-green)
25
+ ![uv](https://img.shields.io/badge/package%20manager-uv-blueviolet?logo=astral)
26
+ ![Ruff](https://img.shields.io/badge/linter-ruff-orange?logo=ruff)
27
+ [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-0075ca?logo=materialformkdocs&logoColor=white)](https://wlix13.github.io/NullForge/)
28
+
29
+ Forge the server's baseline from null - an infrastructure-as-code framework built on [pyinfra](https://pyinfra.com), themed around a blacksmith's forge: **inventories** define hosts, **molds** shape the configuration, **runes** are idempotent operation sets, the **foundry** casts them onto targets, and the **smithy** holds cross-distro helpers.
30
+
31
+ > [!WARNING]
32
+ > **NullForge is in active development.** Until the `v1.0.0` release, the CLI, mold schemas and deploy behaviour may change at any time - breaking changes can land in **any** release, including patch versions. Pin an exact version (e.g. `nullforge==0.2.0`) and check the [release notes](https://github.com/wlix13/NullForge/releases) before upgrading.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ uv tool install nullforge
38
+ ```
39
+
40
+ ## Documentation
41
+
42
+ Everything else - getting started, concepts, the full feature reference, and guides - lives at **[wlix13.github.io/NullForge](https://wlix13.github.io/NullForge/)**.
43
+
44
+ ## Contributing
45
+
46
+ See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for the full setup, commit conventions and pull request flow.
@@ -0,0 +1,29 @@
1
+ # NullForge
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/nullforge?logo=pypi&logoColor=white)](https://pypi.org/project/nullforge/)
4
+ ![Python](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=white)
5
+ ![Build](https://img.shields.io/github/actions/workflow/status/wlix13/NullForge/ci-tests.yaml?label=build&logo=github)
6
+ ![Lint](https://img.shields.io/github/actions/workflow/status/wlix13/NullForge/ci-code-quality.yaml?label=lint&logo=github)
7
+ ![License](https://img.shields.io/badge/license-MIT-green)
8
+ ![uv](https://img.shields.io/badge/package%20manager-uv-blueviolet?logo=astral)
9
+ ![Ruff](https://img.shields.io/badge/linter-ruff-orange?logo=ruff)
10
+ [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-0075ca?logo=materialformkdocs&logoColor=white)](https://wlix13.github.io/NullForge/)
11
+
12
+ Forge the server's baseline from null - an infrastructure-as-code framework built on [pyinfra](https://pyinfra.com), themed around a blacksmith's forge: **inventories** define hosts, **molds** shape the configuration, **runes** are idempotent operation sets, the **foundry** casts them onto targets, and the **smithy** holds cross-distro helpers.
13
+
14
+ > [!WARNING]
15
+ > **NullForge is in active development.** Until the `v1.0.0` release, the CLI, mold schemas and deploy behaviour may change at any time - breaking changes can land in **any** release, including patch versions. Pin an exact version (e.g. `nullforge==0.2.0`) and check the [release notes](https://github.com/wlix13/NullForge/releases) before upgrading.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ uv tool install nullforge
21
+ ```
22
+
23
+ ## Documentation
24
+
25
+ Everything else - getting started, concepts, the full feature reference, and guides - lives at **[wlix13.github.io/NullForge](https://wlix13.github.io/NullForge/)**.
26
+
27
+ ## Contributing
28
+
29
+ See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for the full setup, commit conventions and pull request flow.
@@ -0,0 +1,61 @@
1
+ # Architecture
2
+
3
+ NullForge is a thin, opinionated layer over pyinfra.
4
+ Every layer has one job, and imports only flow downward.
5
+
6
+ ## Data flow
7
+
8
+ ```mermaid
9
+ flowchart TB
10
+ subgraph plan [Control node]
11
+ CLI[cli - nullforge cast] --> FY["foundry - full_cast.py / cast.py"]
12
+ INV[inventories] --> FY
13
+ FY --> RN["runes/*.py"]
14
+ RN --> SM[smithy]
15
+ RN --> TP["templates (Jinja2)"]
16
+ MD["molds (Pydantic)"] --> INV
17
+ MD --> RN
18
+ ML["models (domain types)"] --> MD
19
+ end
20
+ RN -->|pyinfra operations over SSH| HOSTS[(Target hosts)]
21
+ ```
22
+
23
+ 1. **Inventories** define hosts and attach `system` and `features` data, built by [merging layers](inventories.md) over the defaults.
24
+ 2. The **foundry** is the deploy entry point.
25
+ `full_cast.py` coerces inventory data through the molds, always includes `prepare` then `base`, and then includes the rune of every active feature.
26
+ `cast.py` is the selective variant used by `nullforge cast -r ...`.
27
+ 3. **Runes** are self-contained pyinfra operation sets - one file per concern.
28
+ They read validated configuration from `host.data` and emit idempotent operations.
29
+ 4. **Molds** are Pydantic schemas for all configuration; [`FeaturesMold`](molds.md) composes the per-feature sub-molds.
30
+ 5. **Models** hold pure domain types and constants consumed by molds.
31
+ 6. The **smithy** provides cross-distro abstractions: package-name mapping (apt/dnf), release-binary installs with checksum verification, version pinning, networking facts, swap, service users.
32
+ 7. **Templates** are Jinja2 files for systemd units, service configs, and shell profiles.
33
+
34
+ ## Layer contracts
35
+
36
+ The layering is enforced by import contracts:
37
+
38
+ | Contract | Meaning |
39
+ | --- | --- |
40
+ | Deploy spine | `cli` -> `foundry` -> `runes` -> `smithy` -> `molds`/`templates` -> `models`; a layer may import only layers below it |
41
+ | Models are pure | `models` imports no other NullForge package |
42
+ | Templates are a leaf | `templates` imports no other NullForge package, `models` included - its spine position only says which layers may import *it* |
43
+ | Molds describe, never provision | `molds` cannot import runes, smithy, or templates |
44
+ | Runes are independent | no rune imports another rune |
45
+
46
+ Why rune independence matters - and how runes coordinate without it - is covered in [Runes](runes.md#independence).
47
+
48
+ ## Execution model
49
+
50
+ pyinfra runs in two phases:
51
+
52
+ 1. **Plan** - fact gathering and operation collection on the control node.
53
+ Python-level branching (`if host.get_fact(...)`) happens here.
54
+ 2. **Execute** - the collected operations run against each host, in a deterministic order shared by all hosts.
55
+
56
+ The [conventions](../contributing/conventions.md) around `host.loop` and change detection exist to keep that shared ordering stable.
57
+
58
+ ## The CLI wrapper
59
+
60
+ `nullforge cast` is a thin planner around pyinfra: it resolves the [cast stages](../getting-started/cli.md#stages) and hands them to pyinfra.
61
+ Everything else - connections, facts, operations, parallelism - is stock pyinfra, which is why unknown CLI options are proxied through verbatim.
@@ -0,0 +1,165 @@
1
+ # Inventories
2
+
3
+ An inventory is a standard [pyinfra inventory](https://docs.pyinfra.com/en/3.x/inventory-data.html): a Python file with a `hosts` list.
4
+ Each host is either a bare address or an `(address, data)` tuple.
5
+ NullForge reads two data keys - `system` and `features` - plus an optional `versions` map for [pin overrides](../guides/version-pinning.md).
6
+
7
+ ```python
8
+ from nullforge.molds.defaults import BASE_FEATURES, BASE_SYSTEM
9
+ from nullforge.molds.utils import merge_features, merge_system
10
+
11
+ hosts = [
12
+ (
13
+ "203.0.113.10",
14
+ {
15
+ "system": merge_system(BASE_SYSTEM, {"hostname": "node1.example.com"}),
16
+ "features": merge_features(BASE_FEATURES, ...),
17
+ },
18
+ ),
19
+ ]
20
+ ```
21
+
22
+ ## Defaults and layering
23
+
24
+ `BASE_SYSTEM` and `BASE_FEATURES` (from `nullforge.molds.defaults`) are plain default-constructed molds.
25
+ `merge_system` / `merge_features` (from `nullforge.molds.utils`) start from a base and deep-merge each layer in order - later layers win.
26
+
27
+ A features layer can be any of:
28
+
29
+ | Layer type | Example |
30
+ | --- | --- |
31
+ | Full `FeaturesMold` | another host's merged result |
32
+ | Any feature sub-mold | `WarpMold(install=True)` - merged under its own key |
33
+ | `dict` fragment | `{"warp": {"install": True}}` (sub-molds allowed as values) |
34
+ | `None` | skipped; useful for conditional layers |
35
+
36
+ ```python
37
+ from nullforge.molds import DnsMold, UserMold, WarpMold
38
+
39
+ common = (
40
+ UserMold(manage=True, name="core"),
41
+ WarpMold(install=True),
42
+ )
43
+
44
+ features = merge_features(BASE_FEATURES, *common, {"dns": {"ecs": True}})
45
+ ```
46
+
47
+ The set of allowed sub-mold types is derived from `FeaturesMold.model_fields`, so a new feature is mergeable without touching the merge logic.
48
+
49
+ !!! warning "Deep-merge is per-key"
50
+
51
+ Dictionaries merge recursively, but any non-dict value - including lists - replaces the previous value outright.
52
+ A layer that sets `netsec.firewall_rules` replaces the whole rule list; extend `BASE_FEATURES.netsec.firewall_rules` in Python if you want "default rules plus mine".
53
+
54
+ Because inventories are Python, you can factor shared preset tuples, per-environment modules, or host loops however you like - the only contract is the final `hosts` list.
55
+
56
+ ## Validation and coercion
57
+
58
+ The foundry coerces whatever the inventory provided - `None`, a `dict`, or a mold instance - into validated `SystemMold` / `FeaturesMold` objects before any rune runs (`ensure_system` / `ensure_features`).
59
+ Missing keys fall back to mold defaults; unknown keys are rejected because every mold forbids extra fields.
60
+ A typo like `{"warp": {"instal": True}}` fails the cast at validation time, before anything touches a host.
61
+
62
+ ## Scaling to a fleet
63
+
64
+ Inventories are Python, so fleet structure is ordinary code.
65
+ A pattern that works well in practice: per-zone feature presets, a YAML host registry, and a small builder that turns them into pyinfra groups.
66
+
67
+ Shared mold instances compose into one `FeaturesMold` per zone:
68
+
69
+ ```python title="inventory/zones.py"
70
+ from nullforge.models.netsec import SshHostKeyType
71
+ from nullforge.molds import ContainersMold, FeaturesMold, FirewallRule, HaproxyMold, NetSecMold, SshMold, UserMold
72
+ from nullforge.molds.defaults import BASE_FEATURES
73
+ from nullforge.molds.utils import merge_features
74
+
75
+ netsec = NetSecMold(
76
+ firewall_rules=[
77
+ FirewallRule(port=443, comment="HTTPS"),
78
+ FirewallRule(port=22, proto="tcp", from_ip="203.0.113.7", comment="Admin SSH"),
79
+ ],
80
+ ssh=SshMold(host_keys=[SshHostKeyType.ED25519]),
81
+ )
82
+
83
+ users = UserMold(name="ops", fetch_key_from_github="your-github-username")
84
+
85
+ ZONE_FEATURES: dict[str, FeaturesMold] = {
86
+ "web": merge_features(BASE_FEATURES, netsec, users, HaproxyMold(install=True)),
87
+ "workers": merge_features(BASE_FEATURES, netsec, users, ContainersMold(install=True)),
88
+ }
89
+ ```
90
+
91
+ Host membership lives in data, not code - one YAML entry per host, with an optional `overrides` fragment deep-merged onto the zone preset:
92
+
93
+ ```yaml title="inventory/hosts.yaml"
94
+ web:
95
+ - address: 203.0.113.10
96
+ hostname: web1.example.com
97
+ - address: 203.0.113.11
98
+ hostname: web2.example.com
99
+ overrides:
100
+ netsec:
101
+ firewall: false
102
+ workers:
103
+ - address: 203.0.113.20
104
+ hostname: worker1.example.com
105
+ ```
106
+
107
+ The builder exposes one module-level list per zone - pyinfra treats each as a named group, so `--limit web` targets a whole zone:
108
+
109
+ ```python title="inventory/main.py"
110
+ from pathlib import Path
111
+
112
+ import yaml
113
+
114
+ from nullforge.molds.defaults import BASE_SYSTEM
115
+ from nullforge.molds.utils import merge_features, merge_system
116
+
117
+ from inventory.zones import ZONE_FEATURES
118
+
119
+ DATA: dict = yaml.safe_load((Path(__file__).parent / "hosts.yaml").read_text()) or {}
120
+
121
+
122
+ def _build(zone: str) -> list:
123
+ rows = []
124
+ for entry in DATA.get(zone, []):
125
+ rows.append(
126
+ (
127
+ entry["address"],
128
+ {
129
+ "zone": zone,
130
+ "system": merge_system(BASE_SYSTEM, {"hostname": entry["hostname"]}),
131
+ "features": merge_features(ZONE_FEATURES[zone], entry.get("overrides")),
132
+ },
133
+ )
134
+ )
135
+ return rows
136
+
137
+
138
+ for zone in ZONE_FEATURES:
139
+ globals()[zone] = _build(zone)
140
+ ```
141
+
142
+ ```bash
143
+ nullforge cast -i inventory/main.py --limit web --dry
144
+ ```
145
+
146
+ Details worth stealing:
147
+
148
+ - `entry.get("overrides")` is either a dict fragment or `None` - both are valid `merge_features` layers, so per-host overrides cost one line and still go through mold validation.
149
+ - Extra data keys (like `zone` above) ride along on `host.data` untouched; [custom runes](../guides/custom-runes.md) can branch on them.
150
+ - Adding a host is a YAML edit, reviewable in a PR and scriptable from CI.
151
+
152
+ This is the pattern behind the deploy repo of the **Conglomerate** proxy fleet: zone presets over NullForge molds, a YAML host registry edited from CI workflows, and per-zone `--limit` casts.
153
+
154
+ ## Secrets in inventories
155
+
156
+ Inventories are code; secrets in them (tunnel tokens, proxy user secrets) end up on disk.
157
+ Keep real inventories out of public repos, or load secrets from the environment.
158
+
159
+ Mold fields marked sensitive (user password, Zero Trust token, Nezha secrets, Telemt users) are redacted as `***` in pyinfra's `--debug-inventory` output, so inspecting a plan does not leak them.
160
+
161
+ ## Debugging
162
+
163
+ ```bash
164
+ nullforge cast -i inventory.py --debug-inventory # dump merged host data (pass-through to pyinfra)
165
+ ```
@@ -0,0 +1,69 @@
1
+ # Molds
2
+
3
+ Molds are Pydantic models that shape and validate every piece of NullForge configuration.
4
+ If a value reaches a rune, it went through a mold first.
5
+
6
+ ## BaseMold
7
+
8
+ Every mold extends `BaseMold` (`nullforge/molds/base_mold.py`), which sets the shared behaviour:
9
+
10
+ - **`extra="forbid"`** - unknown keys are validation errors, so typos fail the cast instead of silently deploying defaults.
11
+ - **`is_active`** - the activation protocol used for [rune dispatch](runes.md#dispatch); each feature sub-mold implements it.
12
+ - **`to_json()`** - JSON-mode serialization for pyinfra's `--debug-inventory`, with `_sensitive_fields` values redacted as `***` (recursively, through nested molds, lists and dicts).
13
+
14
+ ## The top-level molds
15
+
16
+ Two molds cover a host:
17
+
18
+ - **`SystemMold`** - base system state: packages, locales, timezone, hostname, swap, IPv6.
19
+ Consumed by the always-on [base rune](../features/base.md).
20
+ - **`FeaturesMold`** - one field per feature, each a sub-mold with its own defaults:
21
+
22
+ ```python
23
+ class FeaturesMold(BaseMold):
24
+ warp: WarpMold = Field(default_factory=WarpMold)
25
+ dns: DnsMold = Field(default_factory=DnsMold)
26
+ users: UserMold = Field(default_factory=UserMold)
27
+ netsec: NetSecMold = Field(default_factory=NetSecMold)
28
+ profiles: ProfilesMold = Field(default_factory=ProfilesMold)
29
+ zerotrust: ZeroTrustTunnelMold = Field(default_factory=ZeroTrustTunnelMold)
30
+ containers: ContainersMold = Field(default_factory=ContainersMold)
31
+ monitoring: MonitoringMold = Field(default_factory=MonitoringMold)
32
+ haproxy: HaproxyMold = Field(default_factory=HaproxyMold)
33
+ xray: XrayCoreMold = Field(default_factory=XrayCoreMold)
34
+ tor: TorMold = Field(default_factory=TorMold)
35
+ telemt: TelemtMold = Field(default_factory=TelemtMold)
36
+ ```
37
+
38
+ Field order is deploy order.
39
+ Everything else is derived from these fields - the allowed [merge layers](inventories.md), the feature-to-mold mapping, and rune dispatch - so adding a feature means adding a field, not editing plumbing.
40
+
41
+ ## Activation
42
+
43
+ Every feature sub-mold implements `is_active`.
44
+ Most simply return `self.install`; the exceptions encode their own semantics:
45
+
46
+ - `DnsMold` - active while `mode != "none"`.
47
+ - `UserMold` - active while `manage` is true.
48
+ - `ProfilesMold` - active while `for_root` or `for_user` is true.
49
+
50
+ A sub-mold may also rename its rune or opt out of automatic dispatch - see [dispatch](runes.md#dispatch).
51
+
52
+ ## Validation beyond types
53
+
54
+ Molds validate coherence, not just shapes - a bad combination fails at plan time with a readable error, never mid-deploy.
55
+ Each [feature page](../features/index.md) documents its own validators.
56
+
57
+ ## Models vs molds
58
+
59
+ Domain types and constants - `DnsMode`, `Shell`, `WarpEngineType`, `SwapType`, ... - live in `nullforge.models.<domain>` and are imported from there directly:
60
+
61
+ ```python
62
+ from nullforge.models.dns import DnsMode
63
+ from nullforge.molds import DnsMold
64
+
65
+ dns = DnsMold(mode=DnsMode.DOT_RESOLVED)
66
+ ```
67
+
68
+ `nullforge.molds` exports only the molds.
69
+ The split is enforced by an import contract: models import nothing from the rest of the package.