@crvouga/dev-fleet 0.0.0-stage → 0.1.0

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 (333) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +157 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +128 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +17 -0
  33. package/ansible/monitoring/README.md +154 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +134 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/lock.json +11 -0
  40. package/ansible/net/remote-probe.sh +13 -0
  41. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  42. package/ansible/ubuntu/baseline.yml +89 -0
  43. package/ansible/ubuntu/disk-guard.py +104 -0
  44. package/ansible/ubuntu/policy.json +43 -0
  45. package/ansible/ubuntu/recovery.yml +15 -0
  46. package/ansible/ubuntu/t3/policy.json +7 -0
  47. package/ansible/ubuntu/t3-desktop.yml +220 -0
  48. package/ansible/ubuntu/t3-health.sh +33 -0
  49. package/ansible/ubuntu/t3-policy.yml +76 -0
  50. package/ansible/ubuntu/t3-route.yml +28 -0
  51. package/ansible/ubuntu/tailscale.yml +75 -0
  52. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  53. package/ansible/ubuntu/tasks/docker.yml +31 -0
  54. package/ansible/ubuntu/tasks/packages.yml +56 -0
  55. package/ansible/ubuntu/tasks/power.yml +51 -0
  56. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  57. package/ansible/ubuntu/tasks/resources.yml +252 -0
  58. package/ansible/ubuntu/tasks/services.yml +31 -0
  59. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  60. package/ansible/ubuntu/tasks/user.yml +120 -0
  61. package/bin/devfleet.mjs +36 -0
  62. package/bootstrap/bootstrap.ps1 +50 -0
  63. package/bootstrap/bootstrap.sh +73 -0
  64. package/bootstrap/devfleet +7 -0
  65. package/bootstrap/docker-desktop.sh +33 -0
  66. package/bootstrap/doctor.ts +189 -0
  67. package/bootstrap/fleet-manager-run.sh +76 -0
  68. package/bootstrap/fleet-remote-bootstrap.sh +185 -0
  69. package/bootstrap/global-config.ts +113 -0
  70. package/bootstrap/install-launcher.sh +7 -0
  71. package/bootstrap/native-aws.ts +86 -0
  72. package/bootstrap/native-t3.ts +154 -0
  73. package/bootstrap/npm.ts +350 -0
  74. package/bootstrap/setup-status.ts +51 -0
  75. package/bootstrap/setup.ts +88 -0
  76. package/bootstrap/shell-path.ts +105 -0
  77. package/bootstrap/verify-t3-archive.py +49 -0
  78. package/bootstrap/worker-auth.ts +65 -0
  79. package/bootstrap/worker-github.ts +47 -0
  80. package/bootstrap/worker-tools.ts +24 -0
  81. package/cliproxy/AGENTS.md +11 -0
  82. package/cliproxy/README.md +269 -0
  83. package/cliproxy/clients/bootstrap.py +143 -0
  84. package/cliproxy/clients/launcher.py +57 -0
  85. package/cliproxy/compose.yaml +90 -0
  86. package/cliproxy/config.template.yaml +70 -0
  87. package/cliproxy/docs/cloud-adoption.md +16 -0
  88. package/cliproxy/docs/deployment.png +0 -0
  89. package/cliproxy/docs/deployment.svg +109 -0
  90. package/cliproxy/docs/health-workflow.example.yml +185 -0
  91. package/cliproxy/docs/migration.md +131 -0
  92. package/cliproxy/docs/model-health-report.schema.json +364 -0
  93. package/cliproxy/docs/model-health.md +393 -0
  94. package/cliproxy/image.lock.json +5 -0
  95. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  96. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  97. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  98. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  99. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  101. package/cliproxy/models.json +1618 -0
  102. package/cliproxy/requirements.lock +78 -0
  103. package/cliproxy/scripts/accounts.py +38 -0
  104. package/cliproxy/scripts/audit.py +123 -0
  105. package/cliproxy/scripts/backup +234 -0
  106. package/cliproxy/scripts/check.py +34 -0
  107. package/cliproxy/scripts/checkpoint.py +226 -0
  108. package/cliproxy/scripts/cli.py +64 -0
  109. package/cliproxy/scripts/cliproxy_host.py +257 -0
  110. package/cliproxy/scripts/cloud_state.py +150 -0
  111. package/cliproxy/scripts/container-health +6 -0
  112. package/cliproxy/scripts/docker +24 -0
  113. package/cliproxy/scripts/external-health.py +79 -0
  114. package/cliproxy/scripts/fleet_config.py +43 -0
  115. package/cliproxy/scripts/fleet_sync.py +274 -0
  116. package/cliproxy/scripts/initialize +84 -0
  117. package/cliproxy/scripts/install-dependencies +48 -0
  118. package/cliproxy/scripts/install.py +406 -0
  119. package/cliproxy/scripts/model_health.py +16 -0
  120. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  121. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  122. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  123. package/cliproxy/scripts/modelhealth/config.py +186 -0
  124. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  125. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  126. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  127. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  128. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  129. package/cliproxy/scripts/modelhealth/report.py +150 -0
  130. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  131. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  132. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  133. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  134. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  135. package/cliproxy/scripts/monitor +68 -0
  136. package/cliproxy/scripts/ops +527 -0
  137. package/cliproxy/scripts/public-endpoint +127 -0
  138. package/cliproxy/scripts/recover_node.py +177 -0
  139. package/cliproxy/scripts/restore-verify +60 -0
  140. package/cliproxy/scripts/retention.py +144 -0
  141. package/cliproxy/scripts/service-start +7 -0
  142. package/cliproxy/scripts/validate-config +56 -0
  143. package/cliproxy/scripts/vault-access.py +57 -0
  144. package/cliproxy/scripts/vault-backup-login +55 -0
  145. package/cliproxy/scripts/vault-recovery +56 -0
  146. package/cliproxy/scripts/vault-renew +30 -0
  147. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  148. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  149. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  150. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  151. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  152. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  153. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  155. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  156. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  157. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  158. package/cliproxy/vendor/LICENSE +22 -0
  159. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  160. package/examples/fleet.json +95 -0
  161. package/examples/roster.json +13 -0
  162. package/mise.lock +1492 -0
  163. package/mise.toml +99 -0
  164. package/package.json +65 -4
  165. package/schema/fleet.schema.json +364 -0
  166. package/schema/roster.schema.json +77 -0
  167. package/src/agents/account-relay.ts +111 -0
  168. package/src/agents/accounts.ts +85 -0
  169. package/src/agents/audit.ts +77 -0
  170. package/src/agents/cli.ts +13 -0
  171. package/src/agents/command.ts +204 -0
  172. package/src/agents/converge.ts +123 -0
  173. package/src/agents/doctor.ts +107 -0
  174. package/src/agents/errors.ts +2 -0
  175. package/src/agents/files.ts +73 -0
  176. package/src/agents/fleet-verify.ts +156 -0
  177. package/src/agents/launch.ts +290 -0
  178. package/src/agents/native-verify.ts +226 -0
  179. package/src/agents/provider-errors.ts +50 -0
  180. package/src/agents/registry.ts +55 -0
  181. package/src/agents/render.ts +110 -0
  182. package/src/agents/round-trip.ts +192 -0
  183. package/src/agents/secrets.ts +13 -0
  184. package/src/agents/toolchain.ts +34 -0
  185. package/src/agents/verify-cache.ts +103 -0
  186. package/src/agents/verify.ts +127 -0
  187. package/src/auth-checks.ts +163 -0
  188. package/src/cli/agent-guide.ts +95 -0
  189. package/src/cli/app.ts +115 -0
  190. package/src/cli/cli.ts +21 -0
  191. package/src/cli/command-index.ts +50 -0
  192. package/src/cli/contracts.ts +25 -0
  193. package/src/cli/fleet-reference.ts +17 -0
  194. package/src/cli/fleet-sync-checkout.ts +91 -0
  195. package/src/cli/fleet-sync-enrollment.ts +144 -0
  196. package/src/cli/fleet-sync-forward.ts +134 -0
  197. package/src/cli/fleet-sync-manager.ts +193 -0
  198. package/src/cli/fleet-sync-monitoring.ts +185 -0
  199. package/src/cli/fleet-sync-output.ts +314 -0
  200. package/src/cli/fleet-sync-privilege.ts +27 -0
  201. package/src/cli/fleet-sync-progress.ts +109 -0
  202. package/src/cli/fleet-sync-remote.ts +236 -0
  203. package/src/cli/fleet-sync-report.ts +26 -0
  204. package/src/cli/fleet-sync-schedule.ts +138 -0
  205. package/src/cli/fleet-sync-vault.ts +166 -0
  206. package/src/cli/fleet-sync.ts +309 -0
  207. package/src/cli/fleet.ts +163 -0
  208. package/src/cli/managed-health.ts +219 -0
  209. package/src/cli/node-deployment.ts +34 -0
  210. package/src/cli/node-sync.ts +114 -0
  211. package/src/cli/node-t3.ts +31 -0
  212. package/src/cli/options.ts +141 -0
  213. package/src/cli/probe-source.ts +7 -0
  214. package/src/cli/program.ts +169 -0
  215. package/src/cli/register-fleet.ts +246 -0
  216. package/src/cli/registry.ts +158 -0
  217. package/src/cli/repair-apply.ts +67 -0
  218. package/src/cli/repair-guard.ts +11 -0
  219. package/src/cli/repair-inspect.ts +150 -0
  220. package/src/cli/repair-script.ts +76 -0
  221. package/src/cli/repair.ts +209 -0
  222. package/src/cli/roster.ts +89 -0
  223. package/src/cli/search.ts +35 -0
  224. package/src/cli/settings.ts +64 -0
  225. package/src/cli/source-operations.ts +78 -0
  226. package/src/cli/t3/command.ts +90 -0
  227. package/src/cli/t3/register.ts +55 -0
  228. package/src/cli/tailnet.ts +127 -0
  229. package/src/cli/ui-actions.ts +133 -0
  230. package/src/cli/ui-controller.ts +113 -0
  231. package/src/cli/ui-data.ts +106 -0
  232. package/src/cli/ui-execute.ts +163 -0
  233. package/src/cli/ui-repairs.ts +34 -0
  234. package/src/cli/ui-reports.ts +211 -0
  235. package/src/cli/ui-targets.ts +100 -0
  236. package/src/cli/ui-terminal.ts +109 -0
  237. package/src/cli/ui-types.ts +49 -0
  238. package/src/cli/ui-wizard.ts +101 -0
  239. package/src/cli/ui.ts +9 -0
  240. package/src/cli/worker-health-output.ts +37 -0
  241. package/src/config.ts +186 -0
  242. package/src/configure.ts +255 -0
  243. package/src/definition.ts +260 -0
  244. package/src/diagnostics.ts +92 -0
  245. package/src/doctor-check.ts +10 -0
  246. package/src/errors.ts +1 -0
  247. package/src/fingerprint.ts +120 -0
  248. package/src/fleet-config.ts +88 -0
  249. package/src/index.ts +6 -0
  250. package/src/input-hashes.ts +73 -0
  251. package/src/machine/local-source.ts +52 -0
  252. package/src/machine/lock-error.ts +2 -0
  253. package/src/machine/platform.ts +44 -0
  254. package/src/machine/source.ts +68 -0
  255. package/src/machine/state.ts +185 -0
  256. package/src/machine-health.ts +95 -0
  257. package/src/managed-source.ts +47 -0
  258. package/src/manager-failure.ts +48 -0
  259. package/src/manager-sync.ts +400 -0
  260. package/src/manager.ts +62 -0
  261. package/src/monitoring/apply.ts +171 -0
  262. package/src/monitoring/beszel.ts +191 -0
  263. package/src/monitoring/enroll.ts +110 -0
  264. package/src/monitoring/hub-access.ts +113 -0
  265. package/src/monitoring/hub-viewer.ts +142 -0
  266. package/src/monitoring/hub.ts +93 -0
  267. package/src/monitoring/manager-agent.ts +73 -0
  268. package/src/monitoring/manager-hub.ts +232 -0
  269. package/src/monitoring/remote.ts +65 -0
  270. package/src/network-definition.ts +83 -0
  271. package/src/node-converge-linux.ts +372 -0
  272. package/src/node-converge-shared.ts +83 -0
  273. package/src/node-converge.ts +40 -0
  274. package/src/onboard.ts +35 -0
  275. package/src/platform/ansible-receipt.ts +128 -0
  276. package/src/platform/ansible.ts +62 -0
  277. package/src/platform/darwin.ts +203 -0
  278. package/src/platform/detect.ts +11 -0
  279. package/src/platform/fallback.ts +110 -0
  280. package/src/platform/index.ts +38 -0
  281. package/src/platform/launchd.ts +98 -0
  282. package/src/platform/linux.ts +139 -0
  283. package/src/platform/ps.ts +45 -0
  284. package/src/platform/resource-verify.ts +128 -0
  285. package/src/platform/systemd-user.ts +95 -0
  286. package/src/platform/t3-policy.ts +137 -0
  287. package/src/platform/t3-route.ts +68 -0
  288. package/src/platform/t3-service.ts +75 -0
  289. package/src/platform/types.ts +123 -0
  290. package/src/platform/ubuntu-audit.ts +63 -0
  291. package/src/platform/ubuntu-verify.ts +105 -0
  292. package/src/platform/windows.ts +148 -0
  293. package/src/power-recovery.ts +55 -0
  294. package/src/probe.ts +51 -0
  295. package/src/protocol.ts +119 -0
  296. package/src/proxy-converge.ts +85 -0
  297. package/src/remote-bootstrap.ts +212 -0
  298. package/src/remote-entry.ts +89 -0
  299. package/src/remote-lock.ts +12 -0
  300. package/src/repair-catalog.ts +206 -0
  301. package/src/repair-plan.ts +78 -0
  302. package/src/repair-runner.ts +84 -0
  303. package/src/report.ts +71 -0
  304. package/src/runner.ts +104 -0
  305. package/src/secrets.ts +169 -0
  306. package/src/service-auth.ts +115 -0
  307. package/src/ssh.ts +135 -0
  308. package/src/sync.ts +104 -0
  309. package/src/t3/accounts.ts +81 -0
  310. package/src/t3/desired.ts +165 -0
  311. package/src/t3/files.ts +81 -0
  312. package/src/t3/names-check.ts +53 -0
  313. package/src/t3/names.ts +155 -0
  314. package/src/t3/plan.ts +82 -0
  315. package/src/t3/providers.ts +74 -0
  316. package/src/t3/reconcile.ts +123 -0
  317. package/src/t3-endpoint.ts +48 -0
  318. package/src/t3-linked-check.ts +101 -0
  319. package/src/tailnet-health.ts +231 -0
  320. package/src/tailnet-matching.ts +37 -0
  321. package/src/tailscale.ts +141 -0
  322. package/src/test-fixtures.ts +36 -0
  323. package/src/toolchain-checks.ts +83 -0
  324. package/src/topology.ts +24 -0
  325. package/src/transport.ts +122 -0
  326. package/src/types.ts +95 -0
  327. package/src/vault-config.ts +38 -0
  328. package/src/worker-access-checks.ts +115 -0
  329. package/src/worker-capacity.ts +34 -0
  330. package/src/worker-host-checks.ts +176 -0
  331. package/src/worker-policy.ts +64 -0
  332. package/src/worker-readiness.ts +64 -0
  333. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,75 @@
1
+ ---
2
+ - name: Converge native Tailscale without replacing its device identity
3
+ hosts: localhost
4
+ connection: local
5
+ become: true
6
+ gather_facts: true
7
+ tasks:
8
+ - name: Require supported Ubuntu
9
+ ansible.builtin.assert:
10
+ that:
11
+ - ansible_facts.distribution == 'Ubuntu'
12
+ - ansible_facts.distribution_version == '26.04'
13
+ - name: Inspect installed packages before possible network interruption
14
+ ansible.builtin.package_facts:
15
+ manager: apt
16
+ - name: Require maintenance for an installed version change
17
+ ansible.builtin.assert:
18
+ that: "'tailscale' not in ansible_facts.packages or ansible_facts.packages.tailscale[0].version == '1.104.5'"
19
+ fail_msg: TAILSCALE_UPGRADE_PENDING; upgrading an active VPN can disconnect SSH. Review an explicit maintenance operation.
20
+ - name: Inspect existing package-source ownership before writing
21
+ ansible.builtin.find:
22
+ paths: /etc/apt/sources.list.d
23
+ patterns: ['*.list', '*.sources']
24
+ contains: '.*pkgs.tailscale.com.*'
25
+ read_whole_file: true
26
+ register: existing_sources
27
+ - name: Refuse duplicate or custom sources instead of breaking APT
28
+ ansible.builtin.assert:
29
+ that:
30
+ - existing_sources.matched <= 1
31
+ - existing_sources.matched == 0 or existing_sources.files[0].path == '/etc/apt/sources.list.d/tailscale.list'
32
+ fail_msg: TAILSCALE_SOURCE_OWNER_CONFLICT; review the existing signed source and its migration before creating another repository entry.
33
+ - name: Install pinned upstream signing key
34
+ ansible.builtin.get_url:
35
+ url: https://pkgs.tailscale.com/stable/ubuntu/resolute.noarmor.gpg
36
+ dest: /usr/share/keyrings/tailscale-archive-keyring.gpg
37
+ checksum: sha256:3e03dacf222698c60b8e2f990b809ca1b3e104de127767864284e6c228f1fb39
38
+ owner: root
39
+ group: root
40
+ mode: '0644'
41
+ timeout: 30
42
+ - name: Declare the signed upstream package source
43
+ ansible.builtin.apt_repository:
44
+ repo: deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/ubuntu resolute main
45
+ filename: tailscale
46
+ state: present
47
+ update_cache: false
48
+ register: tailscale_source
49
+ - name: Install the approved version only when absent
50
+ ansible.builtin.apt:
51
+ name: tailscale=1.104.5
52
+ state: present
53
+ update_cache: true
54
+ cache_valid_time: 0
55
+ lock_timeout: 120
56
+ allow_downgrade: false
57
+ allow_unauthenticated: false
58
+ when: "'tailscale' not in ansible_facts.packages"
59
+ - name: Enable and start the native daemon, preserving existing identity
60
+ ansible.builtin.systemd_service:
61
+ name: tailscaled.service
62
+ enabled: true
63
+ state: started
64
+ - name: Inspect native authentication without prompting or opening a browser
65
+ ansible.builtin.command:
66
+ argv: [tailscale, status, '--json']
67
+ changed_when: false
68
+ failed_when: false
69
+ register: tailscale_status
70
+ no_log: true
71
+ - name: Require genuine device authentication before claiming connectivity
72
+ ansible.builtin.assert:
73
+ that: (tailscale_status.stdout | from_json).BackendState == 'Running'
74
+ fail_msg: TAILSCALE_LOGIN_REQUIRED; authorize this device through its native login. Existing state is never copied or reset.
75
+ no_log: true
@@ -0,0 +1,47 @@
1
+ ---
2
+ - name: Create persistent worker credential renewal units
3
+ ansible.builtin.file:
4
+ path: '{{ development_home }}/.config/systemd/user'
5
+ state: directory
6
+ owner: '{{ development_user }}'
7
+ mode: '0700'
8
+ - name: Declare native self-token renewal without copying credentials
9
+ ansible.builtin.copy:
10
+ dest: '{{ development_home }}/.config/systemd/user/personal-worker-auth.service'
11
+ owner: '{{ development_user }}'
12
+ mode: '0600'
13
+ content: |
14
+ [Unit]
15
+ Description=Renew this worker's own Vault authorization
16
+ [Service]
17
+ Type=oneshot
18
+ WorkingDirectory={{ development_source }}
19
+ ExecStart={{ development_home }}/.local/bin/mise exec -- bun bootstrap/worker-auth.ts
20
+ Environment="PATH={{ development_home }}/.local/share/mise/shims:{{ development_home }}/.local/bin:/usr/local/bin:/usr/bin:/bin"
21
+ UMask=0077
22
+ TimeoutStartSec=45
23
+ - name: Declare bounded persistent renewal schedule
24
+ ansible.builtin.copy:
25
+ dest: '{{ development_home }}/.config/systemd/user/personal-worker-auth.timer'
26
+ owner: '{{ development_user }}'
27
+ mode: '0600'
28
+ content: |
29
+ [Unit]
30
+ Description=Keep enrolled worker Vault authorization renewed
31
+ [Timer]
32
+ OnBootSec=2min
33
+ OnUnitActiveSec=10min
34
+ Persistent=true
35
+ [Install]
36
+ WantedBy=timers.target
37
+ - name: Enable this user's credential renewal timer
38
+ become: true
39
+ become_user: '{{ development_user }}'
40
+ environment:
41
+ XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
42
+ ansible.builtin.systemd_service:
43
+ scope: user
44
+ name: personal-worker-auth.timer
45
+ daemon_reload: true
46
+ enabled: true
47
+ state: started
@@ -0,0 +1,31 @@
1
+ ---
2
+ - name: Rotate container logs by default (new containers, after the next Docker restart)
3
+ ansible.builtin.copy:
4
+ dest: /etc/docker/daemon.json
5
+ force: false
6
+ owner: root
7
+ group: root
8
+ mode: '0644'
9
+ content: |
10
+ {
11
+ "log-driver": "json-file",
12
+ "log-opts": { "max-size": "{{ resourceSafety.dockerLogMaxSize }}", "max-file": "{{ resourceSafety.dockerLogMaxFiles }}" }
13
+ }
14
+ validate: dockerd --validate --config-file %s
15
+ - name: Start the native Docker engine without restarting existing workloads
16
+ ansible.builtin.systemd_service:
17
+ name: docker.service
18
+ enabled: true
19
+ state: started
20
+ - name: Grant the designated developer access to the local Docker engine
21
+ ansible.builtin.user:
22
+ name: '{{ development_user }}'
23
+ groups: docker
24
+ append: true
25
+ - name: Verify native Docker Compose and Buildx installation
26
+ ansible.builtin.command:
27
+ argv: '{{ item }}'
28
+ loop:
29
+ - [docker, compose, version]
30
+ - [docker, buildx, version]
31
+ changed_when: false
@@ -0,0 +1,56 @@
1
+ ---
2
+ - name: Create owned APT configuration directory
3
+ ansible.builtin.file:
4
+ path: /etc/personal-fleet
5
+ state: directory
6
+ owner: root
7
+ group: root
8
+ mode: '0755'
9
+ - name: Declare snapshot sources used only for managed package transactions
10
+ ansible.builtin.copy:
11
+ dest: /etc/personal-fleet/ubuntu.sources
12
+ owner: root
13
+ group: root
14
+ mode: '0644'
15
+ backup: true
16
+ content: |
17
+ Types: deb
18
+ URIs: https://snapshot.ubuntu.com/ubuntu/{{ snapshot }}/
19
+ Suites: resolute resolute-updates resolute-security
20
+ Components: main universe restricted multiverse
21
+ Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
22
+ register: snapshot_sources
23
+ - name: Isolate snapshot selection from unrelated APT operations
24
+ ansible.builtin.copy:
25
+ dest: /etc/personal-fleet/apt.conf
26
+ owner: root
27
+ group: root
28
+ mode: '0644'
29
+ content: |
30
+ Dir::Etc::sourcelist "/etc/personal-fleet/ubuntu.sources";
31
+ Dir::Etc::sourceparts "-";
32
+ DPkg::Lock::Timeout "{{ aptLockTimeoutSeconds }}";
33
+ - name: Inspect immutable snapshot package index
34
+ ansible.builtin.stat:
35
+ path: '/var/lib/apt/lists/snapshot.ubuntu.com_ubuntu_{{ snapshot }}_dists_resolute_InRelease'
36
+ register: snapshot_index
37
+ - name: Fetch signed snapshot indexes after source change or interrupted setup
38
+ ansible.builtin.apt:
39
+ update_cache: true
40
+ cache_valid_time: 0
41
+ lock_timeout: '{{ aptLockTimeoutSeconds }}'
42
+ environment:
43
+ APT_CONFIG: /etc/personal-fleet/apt.conf
44
+ when: snapshot_sources.changed or not snapshot_index.stat.exists
45
+ - name: Install exact managed packages without downgrading or restarting services
46
+ ansible.builtin.apt:
47
+ name: '{{ item.key }}={{ item.value }}'
48
+ state: present
49
+ lock_timeout: '{{ aptLockTimeoutSeconds }}'
50
+ allow_downgrade: false
51
+ allow_unauthenticated: false
52
+ fail_on_autoremove: true
53
+ policy_rc_d: 101
54
+ environment:
55
+ APT_CONFIG: /etc/personal-fleet/apt.conf
56
+ loop: '{{ packages | dict2items }}'
@@ -0,0 +1,51 @@
1
+ ---
2
+ - name: Ensure systemd policy directories exist
3
+ ansible.builtin.file:
4
+ path: '{{ item }}'
5
+ state: directory
6
+ owner: root
7
+ group: root
8
+ mode: '0755'
9
+ loop:
10
+ - /etc/systemd/logind.conf.d
11
+ - /etc/systemd/sleep.conf.d
12
+ - name: Declare idle and lid policy without restarting login sessions
13
+ ansible.builtin.copy:
14
+ dest: /etc/systemd/logind.conf.d/60-personal-fleet.conf
15
+ owner: root
16
+ group: root
17
+ mode: '0644'
18
+ backup: true
19
+ content: |
20
+ # Managed by personal fleet-sync. Effective after logind reload/reboot.
21
+ [Login]
22
+ HandleLidSwitch=ignore
23
+ HandleLidSwitchExternalPower=ignore
24
+ HandleLidSwitchDocked=ignore
25
+ IdleAction=ignore
26
+ KillUserProcesses=no
27
+ StopIdleSessionSec=infinity
28
+ - name: Declare supported sleep policy
29
+ ansible.builtin.copy:
30
+ dest: /etc/systemd/sleep.conf.d/60-personal-fleet.conf
31
+ owner: root
32
+ group: root
33
+ mode: '0644'
34
+ backup: true
35
+ content: |
36
+ [Sleep]
37
+ AllowSuspend=no
38
+ AllowHibernation=no
39
+ AllowSuspendThenHibernate=no
40
+ AllowHybridSleep=no
41
+ - name: Mask sleep transitions without stopping running user sessions
42
+ ansible.builtin.systemd_service:
43
+ name: '{{ item }}'
44
+ masked: true
45
+ daemon_reload: true
46
+ loop:
47
+ - sleep.target
48
+ - suspend.target
49
+ - hibernate.target
50
+ - hybrid-sleep.target
51
+ - suspend-then-hibernate.target
@@ -0,0 +1,39 @@
1
+ ---
2
+ - name: Declare bounded recovery of the worker T3 desktop app
3
+ ansible.builtin.copy:
4
+ dest: '{{ development_home }}/.config/systemd/user/personal-worker-recovery.service'
5
+ owner: '{{ development_user }}'
6
+ mode: '0600'
7
+ content: |
8
+ # Managed by personal fleet-sync: ansible/ubuntu/t3-health.sh
9
+ [Unit]
10
+ Description=Keep the worker T3 desktop app running and responsive
11
+ [Service]
12
+ Type=oneshot
13
+ ExecStart=/bin/sh {{ development_source }}/ansible/ubuntu/t3-health.sh
14
+ TimeoutStartSec=90
15
+ - name: Declare persistent service recovery checks
16
+ ansible.builtin.copy:
17
+ dest: '{{ development_home }}/.config/systemd/user/personal-worker-recovery.timer'
18
+ owner: '{{ development_user }}'
19
+ mode: '0600'
20
+ content: |
21
+ [Unit]
22
+ Description=Check the worker T3 desktop app every minute
23
+ [Timer]
24
+ OnBootSec=1min
25
+ OnUnitActiveSec=1min
26
+ Persistent=true
27
+ [Install]
28
+ WantedBy=timers.target
29
+ - name: Enable bounded T3 recovery without restarting active sessions
30
+ become: true
31
+ become_user: '{{ development_user }}'
32
+ environment:
33
+ XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
34
+ ansible.builtin.systemd_service:
35
+ scope: user
36
+ name: personal-worker-recovery.timer
37
+ daemon_reload: true
38
+ enabled: true
39
+ state: started
@@ -0,0 +1,252 @@
1
+ ---
2
+ # Memory and disk safety: a runaway workload is stopped on its own before the
3
+ # machine, SSH, Tailscale or the T3 server run out of memory or disk. These
4
+ # tasks run before the capacity preflight because they matter most when a disk
5
+ # is nearly full; they only write small files and never delete user data.
6
+ - name: Inspect active swap
7
+ ansible.builtin.command:
8
+ argv: [swapon, --show=NAME, --noheadings]
9
+ changed_when: false
10
+ check_mode: false
11
+ register: active_swap
12
+ - name: Measure root free space before creating swap
13
+ ansible.builtin.command:
14
+ argv: [df, --output=avail, -BM, /]
15
+ changed_when: false
16
+ check_mode: false
17
+ register: swap_capacity
18
+ when: active_swap.stdout | trim == ''
19
+ - name: Refuse to create swap on a nearly full disk
20
+ ansible.builtin.assert:
21
+ that: (swap_capacity.stdout_lines | last | trim | regex_replace('M$', '') | int) >= resourceSafety.swapFileMiB + minimumFreeDiskMiB
22
+ fail_msg: SWAP_CAPACITY_REQUIRED; free disk space so a swap file can be created. No swap is active.
23
+ when: active_swap.stdout | trim == ''
24
+ - name: Create a swap file when the node has no swap
25
+ ansible.builtin.command:
26
+ argv: [fallocate, -l, '{{ resourceSafety.swapFileMiB }}M', /swap.fleet]
27
+ creates: /swap.fleet
28
+ when: active_swap.stdout | trim == ''
29
+ - name: Restrict the fleet swap file
30
+ ansible.builtin.file:
31
+ path: /swap.fleet
32
+ owner: root
33
+ group: root
34
+ mode: '0600'
35
+ when: active_swap.stdout | trim == ''
36
+ - name: Format the fleet swap file
37
+ ansible.builtin.command:
38
+ argv: [mkswap, /swap.fleet]
39
+ when: active_swap.stdout | trim == ''
40
+ - name: Activate the fleet swap file
41
+ ansible.builtin.command:
42
+ argv: [swapon, /swap.fleet]
43
+ when: active_swap.stdout | trim == ''
44
+ - name: Activate the fleet swap file at boot
45
+ ansible.builtin.lineinfile:
46
+ path: /etc/fstab
47
+ line: /swap.fleet none swap sw 0 0
48
+ when: active_swap.stdout | trim == ''
49
+
50
+ - name: Install the systemd userspace OOM killer
51
+ ansible.builtin.apt:
52
+ name: systemd-oomd
53
+ state: present
54
+ - name: Create memory and disk policy directories
55
+ ansible.builtin.file:
56
+ path: '{{ item }}'
57
+ state: directory
58
+ owner: root
59
+ group: root
60
+ mode: '0755'
61
+ loop:
62
+ - /etc/systemd/oomd.conf.d
63
+ - /etc/systemd/system/-.slice.d
64
+ - /etc/systemd/system/user.slice.d
65
+ - /etc/systemd/user/t3code-.scope.d
66
+ - /etc/systemd/journald.conf.d
67
+ - /etc/systemd/system/systemd-tmpfiles-clean.timer.d
68
+ - /etc/tmpfiles.d
69
+ - name: Declare systemd-oomd limits
70
+ ansible.builtin.copy:
71
+ dest: /etc/systemd/oomd.conf.d/60-personal-fleet.conf
72
+ owner: root
73
+ group: root
74
+ mode: '0644'
75
+ content: |
76
+ # Managed by personal fleet-sync: kill one cgroup before swap or memory pressure stalls the node.
77
+ [OOM]
78
+ SwapUsedLimit=90%
79
+ DefaultMemoryPressureLimit=60%
80
+ DefaultMemoryPressureDurationSec=20s
81
+ register: oomd_config
82
+ - name: Declare memory reserve and OOM policy for system and user slices
83
+ ansible.builtin.copy:
84
+ dest: '{{ item.dest }}'
85
+ owner: root
86
+ group: root
87
+ mode: '0644'
88
+ content: '{{ item.content }}'
89
+ loop:
90
+ - dest: /etc/systemd/system/-.slice.d/60-personal-fleet.conf
91
+ content: |
92
+ # Managed by personal fleet-sync: when swap is exhausted, oomd kills the largest swap user.
93
+ [Slice]
94
+ ManagedOOMSwap=kill
95
+ - dest: /etc/systemd/system/user.slice.d/60-personal-fleet.conf
96
+ content: |
97
+ # Managed by personal fleet-sync: user workloads can never take the memory SSH, Tailscale and system services need.
98
+ [Slice]
99
+ MemoryMax={{ resourceSafety.userMemoryMax }}
100
+ loop_control:
101
+ label: '{{ item.dest }}'
102
+ register: slice_policy
103
+ - name: Bound each T3 agent so a runaway loses one process, not the session or the node
104
+ ansible.builtin.copy:
105
+ dest: /etc/systemd/user/t3code-.scope.d/60-personal-fleet.conf
106
+ owner: root
107
+ group: root
108
+ mode: '0644'
109
+ content: |
110
+ # Managed by personal fleet-sync. Applies to every t3code-*.scope T3 starts for an agent.
111
+ # OOMPolicy=continue: the kernel kills the largest process in the agent; the agent itself keeps running.
112
+ [Scope]
113
+ MemoryMax={{ resourceSafety.agentMemoryMax }}
114
+ MemorySwapMax={{ resourceSafety.agentSwapMax }}
115
+ OOMPolicy=continue
116
+ register: agent_policy
117
+ - name: Apply slice limits without restarting services
118
+ ansible.builtin.systemd_service:
119
+ daemon_reload: true
120
+ when: slice_policy.changed
121
+ - name: Load the agent limits into the running user manager for new agents
122
+ become: true
123
+ become_user: '{{ development_user }}'
124
+ environment:
125
+ XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
126
+ ansible.builtin.systemd_service:
127
+ scope: user
128
+ daemon_reload: true
129
+ when: agent_policy.changed
130
+ failed_when: false
131
+ - name: Run systemd-oomd
132
+ ansible.builtin.systemd_service:
133
+ name: systemd-oomd.service
134
+ enabled: true
135
+ state: '{{ "restarted" if oomd_config.changed else "started" }}'
136
+
137
+ - name: Cap the system journal
138
+ ansible.builtin.copy:
139
+ dest: /etc/systemd/journald.conf.d/60-personal-fleet.conf
140
+ owner: root
141
+ group: root
142
+ mode: '0644'
143
+ content: |
144
+ # Managed by personal fleet-sync.
145
+ [Journal]
146
+ SystemMaxUse={{ resourceSafety.journalMaxUse }}
147
+ SystemKeepFree={{ resourceSafety.journalKeepFree }}
148
+ RuntimeMaxUse=256M
149
+ register: journal_policy
150
+ - name: Apply the journal cap
151
+ ansible.builtin.systemd_service:
152
+ name: systemd-journald.service
153
+ state: restarted
154
+ when: journal_policy.changed
155
+ - name: Age out abandoned temporary files
156
+ ansible.builtin.copy:
157
+ dest: /etc/tmpfiles.d/tmp.conf
158
+ owner: root
159
+ group: root
160
+ mode: '0644'
161
+ content: |
162
+ # Managed by personal fleet-sync; overrides /usr/lib/tmpfiles.d/tmp.conf.
163
+ q /tmp 1777 root root {{ resourceSafety.tmpAge }}
164
+ q /var/tmp 1777 root root {{ resourceSafety.varTmpAge }}
165
+ - name: Clean temporary files hourly instead of daily
166
+ ansible.builtin.copy:
167
+ dest: /etc/systemd/system/systemd-tmpfiles-clean.timer.d/60-personal-fleet.conf
168
+ owner: root
169
+ group: root
170
+ mode: '0644'
171
+ content: |
172
+ # Managed by personal fleet-sync.
173
+ [Timer]
174
+ OnUnitActiveSec=1h
175
+ register: tmpfiles_timer
176
+ - name: Inspect the RAM-backed /tmp unit
177
+ ansible.builtin.stat:
178
+ path: /usr/lib/systemd/system/tmp.mount
179
+ register: tmp_mount
180
+ - name: Create the /tmp mount policy directory
181
+ ansible.builtin.file:
182
+ path: /etc/systemd/system/tmp.mount.d
183
+ state: directory
184
+ owner: root
185
+ group: root
186
+ mode: '0755'
187
+ when: tmp_mount.stat.exists
188
+ - name: Bound RAM-backed /tmp (effective after the next reboot)
189
+ ansible.builtin.copy:
190
+ dest: /etc/systemd/system/tmp.mount.d/60-personal-fleet.conf
191
+ owner: root
192
+ group: root
193
+ mode: '0644'
194
+ content: |
195
+ # Managed by personal fleet-sync: /tmp is RAM; keep it from taking half of memory.
196
+ [Mount]
197
+ Options=mode=1777,strictatime,nosuid,nodev,size={{ resourceSafety.tmpMaxSize }},nr_inodes=1m,x-systemd.graceful-option=usrquota
198
+ when: tmp_mount.stat.exists
199
+ register: tmp_policy
200
+ - name: Reload the cleanup timer and /tmp definitions
201
+ ansible.builtin.systemd_service:
202
+ daemon_reload: true
203
+ when: tmpfiles_timer.changed or tmp_policy.changed
204
+
205
+ - name: Read the tracked disk guard
206
+ ansible.builtin.stat:
207
+ path: '{{ development_source }}/ansible/ubuntu/disk-guard.py'
208
+ checksum_algorithm: sha256
209
+ register: disk_guard_source
210
+ when: development_source is defined
211
+ - name: Declare the disk guard service
212
+ ansible.builtin.copy:
213
+ dest: /etc/systemd/system/personal-fleet-disk-guard.service
214
+ owner: root
215
+ group: root
216
+ mode: '0644'
217
+ content: |
218
+ # Managed by personal fleet-sync: ansible/ubuntu/disk-guard.py
219
+ # Source sha256 {{ disk_guard_source.stat.checksum }}
220
+ [Unit]
221
+ Description=Personal Fleet disk guard (stops a runaway user writer before the disk fills)
222
+ StartLimitIntervalSec=0
223
+ [Service]
224
+ Type=simple
225
+ ExecStart=/usr/bin/python3 -I -B {{ disk_guard_source.stat.path }} --path / --path {{ development_home }} --floor-mib {{ resourceSafety.diskGuardFloorMiB }} --write-mib {{ resourceSafety.diskGuardWriteMiB }} --interval {{ resourceSafety.diskGuardIntervalSeconds }}
226
+ Restart=always
227
+ RestartSec=5
228
+ OOMScoreAdjust=-1000
229
+ MemoryMax=64M
230
+ MemorySwapMax=0
231
+ CPUQuota=10%
232
+ TasksMax=4
233
+ NoNewPrivileges=true
234
+ PrivateTmp=true
235
+ PrivateNetwork=true
236
+ ProtectSystem=strict
237
+ ProtectHome=read-only
238
+ ReadWritePaths=/sys/fs/cgroup
239
+ ProtectKernelModules=true
240
+ LogRateLimitIntervalSec=60
241
+ LogRateLimitBurst=20
242
+ [Install]
243
+ WantedBy=multi-user.target
244
+ when: development_source is defined and disk_guard_source.stat.exists
245
+ register: disk_guard_unit
246
+ - name: Run the disk guard
247
+ ansible.builtin.systemd_service:
248
+ name: personal-fleet-disk-guard.service
249
+ daemon_reload: '{{ disk_guard_unit.changed }}'
250
+ enabled: true
251
+ state: '{{ "restarted" if disk_guard_unit.changed else "started" }}'
252
+ when: development_source is defined and disk_guard_source.stat.exists
@@ -0,0 +1,31 @@
1
+ ---
2
+ # Always-on recovery for the system services every worker depends on. Drop-ins
3
+ # change restart policy only; running services are never restarted here.
4
+ - name: Create recovery drop-in directories for required system services
5
+ ansible.builtin.file:
6
+ path: '/etc/systemd/system/{{ item }}.service.d'
7
+ state: directory
8
+ owner: root
9
+ group: root
10
+ mode: '0755'
11
+ loop: '{{ alwaysOnServices }}'
12
+ - name: Declare unlimited automatic restart for required system services
13
+ ansible.builtin.copy:
14
+ dest: '/etc/systemd/system/{{ item }}.service.d/60-personal-fleet.conf'
15
+ owner: root
16
+ group: root
17
+ mode: '0644'
18
+ backup: true
19
+ content: |
20
+ # Managed by personal fleet-sync: keep this worker service always on.
21
+ [Unit]
22
+ StartLimitIntervalSec=0
23
+ [Service]
24
+ Restart=always
25
+ RestartSec=5
26
+ loop: '{{ alwaysOnServices }}'
27
+ register: recovery_dropins
28
+ - name: Reload unit definitions without restarting running services
29
+ ansible.builtin.systemd_service:
30
+ daemon_reload: true
31
+ when: recovery_dropins.changed
@@ -0,0 +1,71 @@
1
+ ---
2
+ - name: Inspect effective SSH configuration before changes
3
+ ansible.builtin.command: /usr/sbin/sshd -T
4
+ changed_when: false
5
+ register: effective_ssh
6
+ - name: Inspect any existing managed SSH fragment
7
+ ansible.builtin.stat:
8
+ path: /etc/ssh/sshd_config.d/60-personal-fleet.conf
9
+ register: ssh_owned_file
10
+ - name: Read previous fragment for validation rollback
11
+ ansible.builtin.slurp:
12
+ src: /etc/ssh/sshd_config.d/60-personal-fleet.conf
13
+ when: ssh_owned_file.stat.exists
14
+ register: ssh_previous
15
+ no_log: true
16
+ - name: Refuse to overwrite unmanaged SSH configuration
17
+ ansible.builtin.assert:
18
+ that:
19
+ - not ssh_owned_file.stat.exists or (ssh_previous.content | b64decode).startswith('# Managed by personal Fleet')
20
+ fail_msg: SSH_UNMANAGED_CONFIG_CONFLICT; review the existing fragment before adoption.
21
+ - name: Stage and validate forwarding policy while preserving listeners and authentication
22
+ block:
23
+ - name: Write forwarding-only owned fragment
24
+ ansible.builtin.copy:
25
+ dest: /etc/ssh/sshd_config.d/60-personal-fleet.conf
26
+ owner: root
27
+ group: root
28
+ mode: '0644'
29
+ backup: true
30
+ content: |
31
+ # Managed by personal Fleet; listeners and authentication remain explicit policy.
32
+ AllowTcpForwarding yes
33
+ ClientAliveInterval 30
34
+ ClientAliveCountMax 3
35
+ register: ssh_fragment
36
+ - name: Validate complete SSH configuration
37
+ ansible.builtin.command: /usr/sbin/sshd -t
38
+ changed_when: false
39
+ rescue:
40
+ - name: Restore previous fragment after validation failure
41
+ ansible.builtin.copy:
42
+ content: '{{ ssh_previous.content | b64decode }}'
43
+ dest: /etc/ssh/sshd_config.d/60-personal-fleet.conf
44
+ owner: root
45
+ group: root
46
+ mode: '0644'
47
+ when: ssh_owned_file.stat.exists
48
+ no_log: true
49
+ - name: Remove rejected new fragment
50
+ ansible.builtin.file:
51
+ path: /etc/ssh/sshd_config.d/60-personal-fleet.conf
52
+ state: absent
53
+ when: not ssh_owned_file.stat.exists
54
+ - name: Report validation failure without reloading sshd
55
+ ansible.builtin.fail:
56
+ msg: SSH_CONFIG_REJECTED; previous configuration restored, active service untouched.
57
+ - name: Enable existing SSH service at boot
58
+ ansible.builtin.systemd_service:
59
+ name: ssh.service
60
+ enabled: true
61
+ state: started
62
+ - name: Reload validated forwarding policy without terminating sessions
63
+ ansible.builtin.systemd_service:
64
+ name: ssh.service
65
+ state: reloaded
66
+ when: ssh_fragment.changed
67
+ - name: Verify effective forwarding after reload
68
+ ansible.builtin.command: /usr/sbin/sshd -T
69
+ changed_when: false
70
+ register: ssh_verified
71
+ failed_when: "'allowtcpforwarding yes' not in ssh_verified.stdout"