@a11ign/screenreader-fleet 0.0.0-reserved.0 → 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 (147) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +94 -2
  3. package/dist/capture-client.d.mts +49 -0
  4. package/dist/capture-client.d.mts.map +1 -0
  5. package/dist/capture-client.mjs +352 -0
  6. package/dist/capture-client.mjs.map +1 -0
  7. package/dist/check-worker-code.d.mts +34 -0
  8. package/dist/check-worker-code.d.mts.map +1 -0
  9. package/dist/check-worker-code.mjs +173 -0
  10. package/dist/check-worker-code.mjs.map +1 -0
  11. package/dist/cli-flags.d.mts +71 -0
  12. package/dist/cli-flags.d.mts.map +1 -0
  13. package/dist/cli-flags.mjs +207 -0
  14. package/dist/cli-flags.mjs.map +1 -0
  15. package/dist/code-drift.d.mts +140 -0
  16. package/dist/code-drift.d.mts.map +1 -0
  17. package/dist/code-drift.mjs +284 -0
  18. package/dist/code-drift.mjs.map +1 -0
  19. package/dist/command-line-census.d.mts +33 -0
  20. package/dist/command-line-census.d.mts.map +1 -0
  21. package/dist/command-line-census.mjs +96 -0
  22. package/dist/command-line-census.mjs.map +1 -0
  23. package/dist/compare-workers.d.mts +3 -0
  24. package/dist/compare-workers.d.mts.map +1 -0
  25. package/dist/compare-workers.mjs +332 -0
  26. package/dist/compare-workers.mjs.map +1 -0
  27. package/dist/control-plane-isolation.d.mts +45 -0
  28. package/dist/control-plane-isolation.d.mts.map +1 -0
  29. package/dist/control-plane-isolation.mjs +67 -0
  30. package/dist/control-plane-isolation.mjs.map +1 -0
  31. package/dist/deploy-worker.d.mts +3 -0
  32. package/dist/deploy-worker.d.mts.map +1 -0
  33. package/dist/deploy-worker.mjs +333 -0
  34. package/dist/deploy-worker.mjs.map +1 -0
  35. package/dist/doctor.d.mts +216 -0
  36. package/dist/doctor.d.mts.map +1 -0
  37. package/dist/doctor.mjs +962 -0
  38. package/dist/doctor.mjs.map +1 -0
  39. package/dist/fleet-consistency.d.mts +235 -0
  40. package/dist/fleet-consistency.d.mts.map +1 -0
  41. package/dist/fleet-consistency.mjs +436 -0
  42. package/dist/fleet-consistency.mjs.map +1 -0
  43. package/dist/fleet-env.d.mts +228 -0
  44. package/dist/fleet-env.d.mts.map +1 -0
  45. package/dist/fleet-env.mjs +509 -0
  46. package/dist/fleet-env.mjs.map +1 -0
  47. package/dist/fleet-scripts.d.mts +11 -0
  48. package/dist/fleet-scripts.d.mts.map +1 -0
  49. package/dist/fleet-scripts.mjs +41 -0
  50. package/dist/fleet-scripts.mjs.map +1 -0
  51. package/dist/git-safe-env.d.mts +10 -0
  52. package/dist/git-safe-env.d.mts.map +1 -0
  53. package/dist/git-safe-env.mjs +44 -0
  54. package/dist/git-safe-env.mjs.map +1 -0
  55. package/dist/guest-run.d.mts +26 -0
  56. package/dist/guest-run.d.mts.map +1 -0
  57. package/dist/guest-run.mjs +164 -0
  58. package/dist/guest-run.mjs.map +1 -0
  59. package/dist/host-address.d.mts +33 -0
  60. package/dist/host-address.d.mts.map +1 -0
  61. package/dist/host-address.mjs +105 -0
  62. package/dist/host-address.mjs.map +1 -0
  63. package/dist/host-capacity.d.mts +64 -0
  64. package/dist/host-capacity.d.mts.map +1 -0
  65. package/dist/host-capacity.mjs +152 -0
  66. package/dist/host-capacity.mjs.map +1 -0
  67. package/dist/host-metrics.d.mts +116 -0
  68. package/dist/host-metrics.d.mts.map +1 -0
  69. package/dist/host-metrics.mjs +201 -0
  70. package/dist/host-metrics.mjs.map +1 -0
  71. package/dist/index.d.ts +23 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +25 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/local-vm.d.ts +125 -0
  76. package/dist/local-vm.d.ts.map +1 -0
  77. package/dist/local-vm.js +360 -0
  78. package/dist/local-vm.js.map +1 -0
  79. package/dist/measure-guard.d.mts +34 -0
  80. package/dist/measure-guard.d.mts.map +1 -0
  81. package/dist/measure-guard.mjs +73 -0
  82. package/dist/measure-guard.mjs.map +1 -0
  83. package/dist/normalise-fleet.d.mts +2 -0
  84. package/dist/normalise-fleet.d.mts.map +1 -0
  85. package/dist/normalise-fleet.mjs +76 -0
  86. package/dist/normalise-fleet.mjs.map +1 -0
  87. package/dist/npm-cli-executable.d.mts +42 -0
  88. package/dist/npm-cli-executable.d.mts.map +1 -0
  89. package/dist/npm-cli-executable.mjs +159 -0
  90. package/dist/npm-cli-executable.mjs.map +1 -0
  91. package/dist/probe-outcome.d.mts +89 -0
  92. package/dist/probe-outcome.d.mts.map +1 -0
  93. package/dist/probe-outcome.mjs +104 -0
  94. package/dist/probe-outcome.mjs.map +1 -0
  95. package/dist/protocol-guard.d.mts +34 -0
  96. package/dist/protocol-guard.d.mts.map +1 -0
  97. package/dist/protocol-guard.mjs +121 -0
  98. package/dist/protocol-guard.mjs.map +1 -0
  99. package/dist/source-walk.d.mts +12 -0
  100. package/dist/source-walk.d.mts.map +1 -0
  101. package/dist/source-walk.mjs +56 -0
  102. package/dist/source-walk.mjs.map +1 -0
  103. package/dist/transient-fault.d.mts +6 -0
  104. package/dist/transient-fault.d.mts.map +1 -0
  105. package/dist/transient-fault.mjs +86 -0
  106. package/dist/transient-fault.mjs.map +1 -0
  107. package/dist/utm-deprecated.d.mts +6 -0
  108. package/dist/utm-deprecated.d.mts.map +1 -0
  109. package/dist/utm-deprecated.mjs +23 -0
  110. package/dist/utm-deprecated.mjs.map +1 -0
  111. package/dist/worker-code-check.d.mts +29 -0
  112. package/dist/worker-code-check.d.mts.map +1 -0
  113. package/dist/worker-code-check.mjs +78 -0
  114. package/dist/worker-code-check.mjs.map +1 -0
  115. package/dist/worker-health.d.mts +56 -0
  116. package/dist/worker-health.d.mts.map +1 -0
  117. package/dist/worker-health.mjs +73 -0
  118. package/dist/worker-health.mjs.map +1 -0
  119. package/dist/worker-http.d.mts +103 -0
  120. package/dist/worker-http.d.mts.map +1 -0
  121. package/dist/worker-http.mjs +277 -0
  122. package/dist/worker-http.mjs.map +1 -0
  123. package/dist/worker-stats.d.mts +66 -0
  124. package/dist/worker-stats.d.mts.map +1 -0
  125. package/dist/worker-stats.mjs +143 -0
  126. package/dist/worker-stats.mjs.map +1 -0
  127. package/package.json +96 -4
  128. package/src/local-worker/autounattend.xml +280 -0
  129. package/src/local-worker/build-vm.sh +218 -0
  130. package/src/local-worker/clone-worker.sh +141 -0
  131. package/src/local-worker/create-utm-vm.sh +202 -0
  132. package/src/local-worker/fetch-windows-iso.sh +238 -0
  133. package/src/local-worker/first-boot.cmd +58 -0
  134. package/src/local-worker/worker-ctl.sh +442 -0
  135. package/src/provisioning/README.md +28 -0
  136. package/src/provisioning/apply-foreground-lock-timeout.ps1 +71 -0
  137. package/src/provisioning/bare-metal/README.md +213 -0
  138. package/src/provisioning/bare-metal/a11y-bootstrap.service +58 -0
  139. package/src/provisioning/bare-metal/autounattend.xml +428 -0
  140. package/src/provisioning/bare-metal/serve-bootstrap.sh +86 -0
  141. package/src/provisioning/bootstrap-control-plane.sh +463 -0
  142. package/src/provisioning/bootstrap-windows-worker.ps1 +649 -0
  143. package/src/provisioning/build-lean-worker-image.ps1 +275 -0
  144. package/src/provisioning/diagnose-nvda-worker.ps1 +174 -0
  145. package/src/provisioning/provision-nvda-worker.ps1 +827 -0
  146. package/src/provisioning/set-display-mode.ps1 +411 -0
  147. package/src/provisioning/stamp-provision-revision.ps1 +184 -0
@@ -0,0 +1,213 @@
1
+ # Zero-touch worker install (bare metal, x64)
2
+
3
+ PXE-boot a mini PC and it joins the fleet: Windows installs, `witness` logs in, sshd comes up with your
4
+ key already installed, and the worker serves `/health`. **No console visit.**
5
+
6
+ **This path is for this project's own fleet, not for an outside contributor.** It PXE-boots against
7
+ infrastructure only this project runs — Proxmox CT 110 `iventoy-pxe`, the fleet-control container
8
+ serving the bootstrap payload below, and `inventory.yml`/`ansible.cfg` wiring that live outside this
9
+ checkout. A stranger with a spare Windows box and none of that cannot follow this start to finish; they
10
+ would need to first stand up equivalent PXE/control-plane infrastructure of their own, which nothing
11
+ here asks them to do because it isn't written for that reader. If that's you, see
12
+ [`../README.md`](../README.md) for the routes that are: your own Windows machine
13
+ (`bootstrap-windows-worker.ps1`) or the GitHub Action (no machine at all).
14
+
15
+ ## Detach the network until first boot has pinned Edge
16
+
17
+ **Learned on a11y-worker-7, 2026-09-04, and it cost a reimage.** Nothing in first boot mentioned Edge
18
+ until then. The box installed Windows, came up with a network, and Edge updated itself to 152 while
19
+ `npm install` was still running. `roles/worker/tasks/edge-version.yml` then refused, correctly:
20
+
21
+ > a11y-worker-7 is on Edge 152.0.4191.62, NEWER than the pinned 151.0.4129.107. Chromium will not install
22
+ > over a newer build and Windows will not let it be uninstalled, so this role cannot bring it back.
23
+
24
+ `browserVersion` is FIRST in `fleet-consistency`'s MUST_MATCH list and part of the capture cache key, so a
25
+ box on a different Edge build does not merely underperform — **every capture run refuses to start, on
26
+ every box.** The only remedies are reimaging that one machine or moving the pin and recapturing the whole
27
+ corpus.
28
+
29
+ Bootstrap step 5 now stops the Edge updater and installs the pinned build, and it runs BEFORE the handoff
30
+ to provisioning. That closes the window in the normal case. It cannot close it entirely: the box needs a
31
+ network for step 3 to fetch the payload, and Edge can begin updating in the minutes before step 5 lands.
32
+
33
+ So on a slow box, or one that sat at the desktop before you got to it, **check before assuming**:
34
+
35
+ ```powershell
36
+ (Get-Item 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe').VersionInfo.ProductVersion
37
+ ```
38
+
39
+ Newer than the pin means reimage now, while it is cheap. Step 5 says so itself rather than trying and
40
+ reporting success.
41
+
42
+ ## How a box gets installed, end to end
43
+
44
+ ```
45
+ PXE -> iVentoy serves the Windows ISO + autounattend.xml
46
+ -> Windows installs unattended, creates `witness`, auto-logs on
47
+ -> FirstLogonCommands fetches 3 files over HTTP from the control plane
48
+ -> first-boot.cmd waits for DHCP, stages the key, runs bootstrap elevated
49
+ -> bootstrap installs node/git/sshd, sets DefaultShell, installs the operator key
50
+ -> the box is reachable by Ansible. No console visit after the firmware step.
51
+ ```
52
+
53
+ ### 1. At the machine, once — do both while you are there
54
+
55
+ Nothing can automate this: the box is off and has no OS to ask.
56
+
57
+ - **Enable Wake-on-LAN** in firmware. Usually under Power Management; may be called *Wake on LAN/WLAN*,
58
+ *Power On By PCIe*, or *Resume by PCI-E Device*. On HP and Dell business desktops **Deep Sleep must
59
+ also be disabled** — it cuts the NIC's standby power and silently defeats WoL.
60
+ - **Note the MAC.** It goes in `inventory.yml` and is the one fact about a box that cannot be discovered
61
+ while it is off.
62
+ - Set it to network-boot.
63
+
64
+ ### 2. On the control plane, serve the payload
65
+
66
+ ```bash
67
+ ./serve-bootstrap.sh ~/.ssh/<fleet key filename>.pub # port 8099
68
+ ```
69
+
70
+ It refuses a private key, and it is not a service — Ctrl-C when the box is up.
71
+
72
+ ### 3. In iVentoy
73
+
74
+ The PXE server is **Proxmox CT 110 `iventoy-pxe`** (address in `inventory.yml`), web UI on `:26000`.
75
+
76
+ Point the Windows 11 x64 ISO's **Auto Install Script** at this directory's `autounattend.xml`, and set a
77
+ non-zero boot timeout so the install starts without a keypress.
78
+
79
+ **Check the PXE service is started** — the container running is not the same as the service running, and
80
+ with it stopped a machine set to network-boot finds nothing at all, which looks exactly like a firmware
81
+ boot-order mistake and is not one.
82
+
83
+ Ask iVentoy, do not probe the port:
84
+
85
+ ```bash
86
+ curl -s -X POST -H 'Content-Type: application/json' \
87
+ -d '{"method":"query_status"}' http://<the pxe host>:26000/iventoy/json # {"status": "running"}
88
+ ```
89
+
90
+ **TFTP is UDP.** A TCP connect to port 69 fails whether the service is up or down, so it cannot
91
+ distinguish the two — which is this repo's recurring rule (a check that shares a failure mode with the
92
+ thing it checks verifies nothing) in its cheapest possible form. It was got wrong here first time and
93
+ reported a running server as stopped. `nc -zvu` is the port-level check if you want one; `query_status`
94
+ is better, because it asks the service rather than the socket.
95
+
96
+ The same endpoint drives the rest: `start_server`, `stop_server`, `sys_ip_list`, `get_img_tree`,
97
+ `img_add_auto_script`, `img_set_auto_id`, `img_set_auto_timeout`.
98
+
99
+ **Check the address in `autounattend.xml`.** It has `http://<fleet-control host>:8099` baked in, and that must be
100
+ the machine running `serve-bootstrap.sh`.
101
+
102
+ That address is the **fleet-control container**, not the iVentoy host, and the reason is that
103
+ `serve-bootstrap.sh` needs two things that live there and nowhere else: the repo (it copies
104
+ `first-boot.cmd` and `bootstrap-windows-worker.ps1` out of the checkout) and the fleet public key. Serving
105
+ from the PXE host instead would mean cloning the repo onto it and copying a key across, which puts fleet
106
+ material on a second box to save nothing. Both are on the same bridge, so a machine that can PXE-boot from
107
+ one can certainly fetch three files over HTTP from the other.
108
+
109
+ ### Why the files are fetched rather than injected
110
+
111
+ iVentoy's file injection decompresses into **`X:`** — the WinPE RAM disk, which is gone by the time
112
+ `FirstLogonCommands` runs. The UTM path can scan drive letters because its support ISO stays attached;
113
+ nothing stays attached here. Rebuilding the install ISO to carry the files is the other option, and this
114
+ project has already spent a day on El Torito boot catalogues and `0xc0000225` to earn the opinion that it
115
+ is not worth it for three small files.
116
+
117
+ ### 4. Then, entirely remotely
118
+
119
+ ```bash
120
+ npm run fleet:discover -- --enroll # finds the box, reads its MAC from ARP, adds it
121
+ cd ../../../ansible
122
+ ansible a11y_workers -m ansible.windows.win_ping -l a11y-worker-N
123
+ ansible-playbook provision-role.yml -l a11y-worker-N --check --diff
124
+ ansible-playbook provision-role.yml -l a11y-worker-N
125
+ ansible-playbook sleep.yml -l a11y-worker-N && ansible-playbook wake.yml -l a11y-worker-N
126
+ ```
127
+
128
+ That last line is the point of the firmware step: WoL is only really verified by powering a box down and
129
+ getting it back, and it is far better to find a wrong firmware setting while you are still next to the
130
+ machine.
131
+
132
+ ## Four things this file does differently from the UTM one, and why
133
+
134
+ The sibling at `../../local-worker/autounattend.xml` is the proven recipe. This is that recipe on real
135
+ hardware and a real network, and the differences are not cosmetic:
136
+
137
+ - **No plaintext password.** The arm64 file embeds `witness`/`witness` and justifies it as *"a disposable
138
+ local VM behind QEMU user-mode networking"*. None of that is true of a mini PC on your LAN. The account
139
+ is created **blank** instead, which `LimitBlankPasswordUse=1` confines to console logon — no SMB, no
140
+ RDP, no password SSH. Strictly narrower than a password, and nothing secret is committed.
141
+ - **No VirtIO drivers.** Those paths point at UTM's support ISO; real hardware has inbox drivers, and
142
+ leaving them in fails the windowsPE pass looking for a drive that is not there.
143
+ - **`ComputerName` is `*`.** Twelve machines answering to `A11Y-WORKER` is a NetBIOS conflict. Workers are
144
+ addressed by IP from the inventory anyway.
145
+ - **The TPM/CPU bypasses are load-bearing, not a VM workaround.** The 6th- and 7th-gen boxes in this fleet
146
+ are not on Windows 11's supported-CPU list and Setup refuses them without these.
147
+
148
+ ## It installs on unsupported hardware, deliberately
149
+
150
+ The fleet is second-hand mini PCs, and several generations of them are not on Windows 11's supported-CPU
151
+ list at all. The bypasses are therefore load-bearing rather than a lab convenience, and they are applied
152
+ in **two places**, because one is not enough:
153
+
154
+ | where | what it covers |
155
+ |---|---|
156
+ | `windowsPE` → `HKLM\System\Setup\LabConfig` | the install itself: CPU, TPM, Secure Boot, RAM, storage |
157
+ | `specialize` → `HKLM\SYSTEM\Setup\LabConfig` + `MoSetup` | the INSTALLED system, so later feature updates do not refuse |
158
+
159
+ The second one is the easy thing to miss. The WinPE keys live in the installer's registry and do not
160
+ survive into the running OS, so a box installs happily and then, months later on a machine nobody is
161
+ watching, declines a feature update with "this PC doesn't meet the minimum requirements".
162
+ `AllowUpgradesWithUnsupportedTPMOrCPU` under `MoSetup` is what covers the upgrade path specifically.
163
+
164
+ Two names, deliberately both: `BypassStorageCheck` is what Setup reads; `BypassDiskCheck` was inherited
165
+ from the arm64 file and appears in no Microsoft documentation. An ignored registry value costs nothing
166
+ and a missing one costs the install, so both are written.
167
+
168
+ `BypassNRO` removes 24H2's "connect to the internet and sign in with a Microsoft account" wall at OOBE.
169
+ `HideOnlineAccountScreens` plus a local account usually carries it — but *usually* is not a property you
170
+ want on a fleet whose screens you cannot see.
171
+
172
+ ## It wipes disk 0 without asking
173
+
174
+ That is what makes it hands-off, and it is why the PXE entry serving this must be aimed at machines you
175
+ have set aside — not set as a default-for-everything boot option.
176
+
177
+ ## The media is VERSIONED — refresh `autounattend.xml` before every install
178
+
179
+ This file is copied onto the boot media (iVentoy's auto-install script, or a Ventoy USB stick), so a
180
+ stick prepared once carries whatever this file said that day. It has been fixed **three times since the
181
+ fleet was built**, and each fix came from a real install that failed:
182
+
183
+ | | |
184
+ |---|---|
185
+ | `279e161` 15 Aug | aims the payload fetch at fleet-control (`<fleet-control host>:8099`) rather than the PXE host |
186
+ | `5d4b877` 15 Aug | the install asked for a locale the media does not carry |
187
+ | `4fff4c6` 23 Aug | the product key stopped matching the edition once `install.wim` was split |
188
+
189
+ **But do NOT reflash a stick that has installed boxes successfully, and the third row is why.**
190
+
191
+ The product-key fix is conditional on the MEDIA, not on the box. The generic KMS key worked while the ISO
192
+ shipped a single `install.wim`, and only began failing once `install.wim` was SPLIT into `install.swm`
193
+ parts to fit a file-size limit — Setup then validates the key against the edition it resolved from the
194
+ split image and the two disagree. A stick whose ISO is unsplit never meets that.
195
+
196
+ Measured on this fleet: all four workers first-booted between 15 and 16 August, at commits that are
197
+ ancestors of the 23 August fix. So they were installed WITH the KMS key and they installed fine, which is
198
+ positive evidence that stick's ISO is unsplit. The two 15 August fixes (locale, payload address) are
199
+ already in it.
200
+
201
+ So the honest rule is **check what changed and why, rather than refreshing on principle**. The current
202
+ file is the more portable one — no key at all works on split and unsplit media alike — but that
203
+ combination has been proven zero times on unsplit media, and the stick's has been proven four times.
204
+ Swapping a proven combination for an untested one, in the one phase of the process with no remote
205
+ diagnosis, is the wrong trade. Refresh when a fix applies to your media, or when you change the ISO.
206
+
207
+ `serve-bootstrap.sh` must be RUNNING on the control plane while the box installs: the media fetches three
208
+ files over HTTP and is not self-contained (see *Why the files are fetched rather than injected*).
209
+
210
+ This section previously read "Untested end to end — this x64 adaptation has not been PXE-booted yet."
211
+ That is stale: the three fixes above are what a real install produces, and the fleet is four boxes. It is
212
+ not a claim of a clean unattended run start to finish, which nobody has recorded. The place to look when
213
+ it stalls is still `C:\a11y-first-boot.log` on the box, written before anything else can fail.
@@ -0,0 +1,58 @@
1
+ # The bootstrap payload server, as a service on the control plane.
2
+ #
3
+ # Install (on fleet-control, as root):
4
+ #
5
+ # cp a11y-bootstrap.service /etc/systemd/system/
6
+ # systemctl daemon-reload && systemctl enable --now a11y-bootstrap
7
+ # systemctl status a11y-bootstrap
8
+ #
9
+ # ## Why this exists
10
+ #
11
+ # `serve-bootstrap.sh` used to be started by hand and stopped with Ctrl-C, which is fine for one install
12
+ # you are watching and wrong for a fleet meant to install itself. It was found running with PPID 1 and no
13
+ # unit: a reboot of this container would have removed it silently, and the NEXT box to install would have
14
+ # come up perfectly, logged on, retried three fetches sixty times each, given up after ~15 minutes, and sat
15
+ # at a working desktop with no node, no sshd, no key and no worker. From outside that reads as a bad image,
16
+ # and the only evidence is C:\a11y-first-boot.log on a machine you cannot SSH to. Hence Restart=always.
17
+ #
18
+ # ## The paths are this box's, deliberately
19
+ #
20
+ # `inventory.yml` hardcodes addresses for the same reason: there is one control plane, and a layer of
21
+ # indirection to make a single-instance path configurable buys nothing but a second place to be wrong.
22
+ # If the checkout or the key moves, edit them here.
23
+ #
24
+ # ## RESTART AFTER CHANGING THE PAYLOAD
25
+ #
26
+ # `serve-bootstrap.sh` copies first-boot.cmd and bootstrap-windows-worker.ps1 into a `mktemp -d` at START
27
+ # and serves that snapshot -- so a long-running service serves whatever the checkout held when it last
28
+ # started, not what it holds now. That is the one way this unit can be quietly wrong. After a `git pull`
29
+ # that touches either script:
30
+ #
31
+ # systemctl restart a11y-bootstrap
32
+ #
33
+ # ## It serves a PUBLIC key continuously
34
+ #
35
+ # The script's own header called it "a small exposure and a real one" and told you to Ctrl-C when the box
36
+ # was up. Running always widens that from minutes to permanently. It is a public key, and the two scripts
37
+ # are in this repo -- but this was a deliberate choice to trade that for installs that do not depend on
38
+ # somebody having remembered to start a server.
39
+
40
+ [Unit]
41
+ Description=a11ign worker bootstrap payload (first-boot.cmd, bootstrap script, operator public key)
42
+ Documentation=file:///root/a11y-witness/packages/worker-fleet/src/provisioning/bare-metal/README.md
43
+ After=network-online.target
44
+ Wants=network-online.target
45
+
46
+ [Service]
47
+ Type=simple
48
+ ExecStart=/root/a11y-witness/packages/worker-fleet/src/provisioning/bare-metal/serve-bootstrap.sh /root/.ssh/a11y-witness_ed25519.pub 8099
49
+ Restart=always
50
+ RestartSec=5
51
+
52
+ # The script refuses a private key and refuses a file that is not a public key, so a misconfigured
53
+ # ExecStart fails fast rather than serving the wrong thing. These stop it becoming anything more.
54
+ NoNewPrivileges=yes
55
+ PrivateTmp=yes
56
+
57
+ [Install]
58
+ WantedBy=multi-user.target