Repository navigation
fork fleet: desk setup pack kit (D8) - #225
scuttletoby wants to merge 5 commits into
Conversation
374ba43 to
9336f15
Compare
fork-kit/desk: bootstrap, install, PASS/FAIL check and uninstall for a desktop's side of a desk pool (runner users, launcher, walls, desk agent, gate hook, settings, key, units), from one release commit. Safe to run again and to stop at any step; refuses an install it did not make; uninstall removes only what it made. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9336f15 to
04a398d
Compare
|
Review round 1 at 04a398d: two independent lenses (security, correctness), both SAFE WITH NITS. Fix round to follow on this branch. Security (lens225s): no way out for a hostile job as
Correctness (lens225c): install/check/uninstall agree; 19 tests pass and CI lists them. Findings:
Both lenses ran the state question (runs again, stopped at each step, old/foreign/empty state); everything not listed above is handled. |
- install refuses a systemd drop-in it did not write for its units; check fails on one - re-run restarts the agent only when a file, setting or key changed - check compares the settings files with the machine file - uninstall stops every runner, keeps the nft table, users and record while one still runs, removes every fork-fleet-* namespace and ffh- link, removes only what the kit made (deny files it created, its users' deny lines), names what it leaves, and removes its record last - the record is read only when it has this kit's shape - --release-fleet-config goes back to the release's fleet config - bootstrap parses --fleet-config like the other flags Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e ip (D8) Uninstall now deletes the namespaces ip netns list shows, not one per recorded user; the older test listed none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A whole-folder removal took the record first only for some directory orders, so the test passed on the old code by luck. Listing by name puts the record first, which the old order fails every time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Fix round 1 (commits 5f64eb9, 7e73543, 99bf235; head 99bf235). Tests: Security (lens225s):
Correctness (lens225c):
Fails before the fix: the new test file against the round-0 installer: 15 tests fail (every MUST test above). Two follow-up test commits: 7e73543 (the uninstall test gives its namespace through the fake PR body updated (uninstall, re-run, drop-ins, size, test count). Still draft. |
|
Re-check lens 225c r1 (correctness) at 99bf235: SAFE WITH NITS Earlier findings: S1, S2/C4, S4, S5, S6, S7, C1, C2, C3, C5, C6, C7, C8 FIXED (new tests fail on round-0: 14 fail + 3 errors); S3 New:
self.assertEqual(self.install()[0], 0)
self.machine({'version': 1, 'desk': {**MACHINE['desk'], 'host': 'other-host.example'}})
self.install(stop_after='settings')
self.clear_calls(); self.assertEqual(self.install()[0], 0)
self.assertIn('systemctl restart fork-fleet-desk-agent.service', self.calls()) # fails
State question: re-run ( |
|
Re-check lens 225s r1 (security) at 99bf235: SAFE WITH NITS No way out of New:
Probe tests (fail at head): def test_a_change_written_by_a_killed_run_still_restarts_the_agent(self):
self.assertEqual(self.install()[0], 0)
self.machine({'version': 1, 'desk': {**MACHINE['desk'], 'host': 'new-host.example'}})
self.assertIn('stopped after', str(self.install(stop_after='settings')[0]))
self.clear_calls(); self.assertEqual(self.install()[0], 0)
self.assertIn('systemctl restart fork-fleet-desk-agent.service', self.calls())
def test_check_fails_when_a_unit_is_not_the_release_unit(self):
self.assertEqual(self.install()[0], 0); self.template()
new = self.release('release2', {'.github/scripts/fork-fleet-desk-agent.service': '# v2\n[Service]\n'})
self.install(sha=SHA2, source=new, stop_after='settings')
self.assertEqual(self.run_main('check')[0], 1)Checked and holding: bootstrap runs only curl + stdlib before the sha check (merge base, protected branch, no redirects, |
…et config) (D8) - A restart-pending mark in the record, saved before the first change the agent reads (files, settings, key, units) and cleared after the restart: a run stopped in between restarts the agent on the next run; check names it. - check compares each installed unit with the header plus the release's /opt/fork-fleet/units/UNIT. - The record lists every deny line the kit added (also for runner users that were there before); uninstall removes them all. - The fleet config choice (and the project's copy in /etc/fork-fleet/fleet-config.json) is saved before the swap. - --fleet-config is refused in a folder others or a runner user may write. - check names a kit-made runner user that a smaller slots no longer uses. - The bootstrap test gives a good --sha/--repo before --fleet-config, so it fails on the round-0 bootstrap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Fix round 2 at 4a13be5 (one commit on top of 99bf235) Lens 225c r1 (correctness) + 225s r1 (security):
State question: runs again (a cleared mark gives Red-check: the new test file against the round-1 installer: 8 fail (both restart tests, unit check, fewer slots, open fleet config, fleet config both ways, deny lines). Head: install.test.py 33 OK. |
Row D8 of the desktop pool plan: a setup pack for a desktop that runs fork fleet jobs over the desk link (D1-D7).
Why
Today a desktop joins a desk pool through about twenty hand steps in the runbook (launcher, walls, desk agent, gate
hook, runner users, settings files, key, units). That is slow and easy to get subtly wrong (a missing wall, a
foreign launcher, a lingering user). This kit does all of it from one release commit, checks every part with one
PASS/FAIL list, and removes it again.
fork-kit/desk/bootstrap.sh: same pattern as the other kits. It checks the sha is on the protectedci-releasebranch, downloads that commit and runs the installer from it.
fork-kit/desk/sbin/desk-install:install: reads thedesksection of the machine file (pool, always-on host and its host key, slots,reserve). It makes
fork-runner-Nusers with no login, no linger, and no cron or at. It stages the release'sfleet files, tests them, and moves them into
/opt/fork-fleet. Then it writes the settings files and makes thedesk key once. Last it writes the units, starts the refresh timer (the first template is built in the
background) and starts the agent.
check: one PASS/FAIL line per part, including every wall of this OS (fork-fleet-walls.py describe). Itonly reads.
uninstall: stops every runner (launcher, thensystemctl stop); while one still runs it stops there and keepsthe nft table, users and record. Then it removes every
fork-fleet-*namespace andffh-*link, the nfttable, and only what the kit made: its units, the files of its manifest, template, settings, key, its users and
the deny files it created. Anything else in its folders is kept and named; the record goes last.
desksection in the example machine file, a row in the kit table, a pointer in the runbook.Not here: the Windows starter (D9), the "free" script (D10), seeds (D11), the fork's own pool switch (D12).
What happens when this runs again, stops half-way, or meets existing, old or empty state?
kept, and the agent is neither stopped nor restarted (a test checks no
systemctl restartis run). After a changeto a file, setting, key or unit the agent is restarted. A test compares the whole fake root before and after.
The agent reads its settings only when it starts, so the record gets a restart-pending mark before the first
change and loses it after the restart: a run stopped in between (or refused by the agent's settings check)
restarts the agent on the next run, and
checkfails until then.The result equals a full install every time.
/opt/fork-fleet/.kit-stage-*and tested first: the walls know this OS, the gateloads with the fleet config, the pool is in it,
bash -n, and the agent's--help. A release that fails isnever moved in. The next run removes a leftover stage folder.
MANIFEST.sha256and thenSOURCEare written last,so
checkshows a half-done swap.useradd, so uninstall still removes a user from a run that stoppedright there.
is refused with a fix.
/etc/fork-fleet/fleet-config.json) is recorded before theswap, so a run stopped right after the swap keeps what was asked for.
finishes; a runner that will not stop keeps the nft table, users and record in place.
launch.conf,desk.conf, desk key, unit or systemd drop-in for one ofthe kit's units that the kit did not write (an older hand install) is refused before anything changes;
checkfails on a drop-in that appears later, and on an installed unit that is not the release's (header plus
/opt/fork-fleet/units/UNIT). The kit never adopts or overwrites it. Type-wideservice.d//timer.d/drop-insare not checked: they are the distribution's defaults for every unit and only root can write them. Users that
already exist are used but not recorded, so uninstall leaves them (their namespaces and links are still removed);
the cron/at deny lines the kit added for them are recorded and removed. A user that a smaller
slotsno longeruses stays (no login, unused) and
checknames it; uninstall removes it.A record of another shape is refused, not trusted. An upgrade removes files the old release had
and the new one has not.
checkwith nothing installed lists FAILs and writes nothing.uninstallwith no record doesnothing and exits 0. An OS with no walls file (walls exit 3) is refused before anything is written.
General vs project
General. The kit names no project, person or machine. Pool, host, host key and slots come from the private
machine file on that desktop. The pools come from the release's fleet config or a project's
--fleet-config,which is remembered for later upgrades (
--release-fleet-configgoes back to the release's own). A--fleet-configin a folder that others or a runner user may write is refused. Example values are made up (fleet-host.example).Upstream fit
This touches only fork-only folders (
fork-kit/,.github/fork-*). Nothing in upstream T3 changes.Size
After fix round 1 the PR has about 765 added lines that are not tests (installer, bootstrap, README, example),
over the 400 guideline; round 1 added about 180 installer lines (uninstall that removes only its own, drop-ins,
restart only on change, settings check). Splitting off check or
uninstall would leave a PR that cannot be tested on its own state rules.
Layout differs from the other kits on purpose (README, "Why one flat folder"). The desk agent refuses a launcher or
walls path with a link in it, so there is no "versions + one link". Files are renamed into
/opt/fork-fleetandrecorded in a manifest instead.
Tests
fork-kit/desk/tests/install.test.py(listed in.github/fork-ci-tests.txt) has 33 tests. They run on a fakeroot in a temporary folder, with fake
useradd,systemctl,ssh-keygen,ip,nftand so on, and stub fleetfiles. Nothing touches the real system, and no root is needed. Beyond the first 19 (fresh install, re-run, a stop
after each step, upgrade, refusals, bad releases, keys, existing users, check, uninstall, bootstrap arguments),
round 1 added: a changed setting restarts the agent (and a plain re-run does not), drop-ins refused at install and
failed by check, settings that the machine file no longer gives, uninstall keeps what the kit did not make, removes
the deny files it made, keeps the table while a runner still runs, removes every runner namespace, removes its
record last, refuses a record of another shape,
--release-fleet-config, and bootstrap flags after--fleet-config.Run with
python3 -I -B -W error::ResourceWarning fork-kit/desk/tests/install.test.py.Round 2 added: a change written by a killed run still restarts the agent (and
checkfails until then), a refusedagent check keeps the restart, the fleet config choice outlives a run killed right after the swap (both ways),
checkfails on a unit that is not the release's,checknames a user left by fewer slots, uninstall removes thedeny lines it added for users that were there, a fleet config others may write is refused, and the bootstrap test
now gives a good
--sha/--repobefore--fleet-config.Local run (Python 3.12): 33 tests, OK;
fork-kit/common/tests/machine_file.test.py17 OK.Red-checks
Each rule below was broken on purpose in a copy of the installer, one at a time, and the named test failed (all red;
the full run is OK again after the restore):
useradd->test_a_user_made_by_a_run_that_died_is_removed_by_uninstalltest_runs_again_and_changes_nothingtest_refusals_change_nothingtest_bad_release_is_not_installedtest_existing_user_is_used_not_recordedtest_key_is_made_once_and_a_half_key_is_refusedtest_refusals_change_nothingtest_agent_refusing_its_settings_starts_nothingRound 1: the new test file run against the round-0 installer: 15 tests fail, among them every round-1 MUST test
(drop-ins, re-run without restart, uninstall keeps what it did not make, deny files, table kept while a runner runs,
every namespace, record last, record shape, bootstrap flags). The record-last test lists folders by name, so a
whole-folder removal fails it whatever order the file system gives.
Round 2: the new test file run against the round-1 installer: 8 tests fail (restart after a killed run, restart
after a refused agent check, unit check, fewer slots, fleet config others may write, the fleet config choice both
ways, deny lines for users that were there); the bootstrap test fails on the round-0 bootstrap.
🤖 Generated with Claude Code