A slot acts on its launchd label or systemd unit only while the service manager holds that name from the slot's own file #188

Merged
agra merged 4 commits from task/3803-a-run-on-a-runner-host-replaced-the-host into master 2026-10-30 07:03:04 +02:00
Owner

Tracker: #3803

What reached the real agent

Review run 718 (stream 8029) ran one command last, at seq 271, 23:52:07Z. It copied m5's installed 0.3.6/sak-runner, gave it a scratch HOME and XDG_CONFIG_HOME whose configuration named no slot and no service, and ran it with --slot a. A slot that its configuration's slot line does not name leaves. Leaving runs launchctl bootout gui/<uid>/<label>, and with no service line the label is the default com.swipelab.sak-runner. launchd's gui/<uid> domain is one per user, whatever HOME a process runs with. So the scratch copy booted out m5's own slot a, pid 17272, and runs 715 and 718 lost their pump. The stream ends at that command, because its runner was the process it ended. The runAH.sh arms ran tests/runner_launchd.sx, whose labels are com.swipelab.sak-runner-test-<6>, and mutA.py only took two checks out of that test. Neither touched the host's label. The scratch tree, the harness's own transcript and the unified log of that minute were all gone by 2026-10-30, so what bootstrapped the agent again at 23:53:12Z is not recoverable.

The same holds for any run: a run's HOME is its own directory, so sak-runner --slot a typed in a run reads a configuration that names no slot. It boots out the runner that runs it, and sak-runner --start-slot a replaces it. On Linux, systemctl --user acts on a unit name for every XDG_CONFIG_HOME alike, with the same effect.

What it delivers

  • A slot is its name and its file. runner/slot.sx asks the service manager which file it holds a slot's name from: the path of launchctl print gui/<uid>/<label>, or systemctl --user show --property=FragmentPath --value <unit>. A slot acts on the name only when that file is its own, compared with each directory's links resolved, since launchd records the resolved path. A label launchd holds with no path in its record counts as another install's.
    • --start-slot refuses a name held from another install's file. It writes nothing and fails with "the service manager holds the slot's name from another install's file". From its own file it starts the slot in place of its process, as before.
    • Leaving and rolling back unload the name only when it is held from the slot's own file, and remove the slot's own file either way.
    • "The other slot still finishes its steps" (step 3 of Updates) counts only a name held from that slot's own file.
  • tests/runner_launchd.sx
    • check_other_install: a second scratch install, whose configuration names slot a's label and no slot, as run 718's setup did. Its --slot a and its --start-slot a leave slot a's agent loaded from its plist with the same pid.
    • The test's last check also holds the record of the host's own slot agent that runs the test, its path and pid. The old check read only $HOME, which in a gate is the run's home.
    • The cleanup boots out every agent loaded from the sandbox, the default labels among them, and no other.
    • A failing scene no longer aborts on a null run home in check_b_to_a, which used to skip that cleanup and leave the sandbox behind.
  • tests/runner_install.sx: the fake launchctl and systemctl answer the file they hold a name from. On Linux, --start-slot refuses a unit the manager holds from another file and leaves it running, and a leaving slot does not disable it.
  • tests/runner_handover.sx: the fake systemctl answers show. check_found_taken's hand-run --slot a gets slot a's unit file first, as a process systemd started has it.
  • runner/README.md's The slots and Updates say the rule once, as when a slot is loaded.

Regressions, failing before

I ran these from a git archive arm of master 39594b31, with the head's test files and dist/sak-runner built from master's runner. All ran on m5.

  • tests/runner_launchd.sx failed:

    • FAIL another install's --slot a, its configuration naming slot a's label and no slot, leaves slot a's agent running from its plist
    • FAIL another install's --start-slot a fails, writing no agent, and slot a's agent runs on as it was

    The other install's leave booted the test's slot a out, so most of the hand-over checks after them fail too: RUNNER LAUNCHD FAIL (22 checks).

  • tests/runner_install.sx failed:

    • FAIL ci-linux: another install's --start-slot a fails and writes no unit, sak-runner.service running on from the first install's file
    • FAIL ci-linux: another install's --slot a, its configuration naming no slot, leaves without disabling sak-runner.service

    It also failed the three call logs that now open with the manager read: m5: slot b unloaded itself, ci-linux: slot b unloaded itself, and Linux's install log. That is RUNNER INSTALL FAIL (5 checks).

  • At the head, all three files pass.

Mutation arms of runner/slot.sx at 1b974e05, each failing only what it pins:

Mutation runner_launchd runner_install
Leave unloads whoever holds the name the --slot a check, then the cascade the Linux leave check
Start without the refusal the --start-slot a check alone the Linux start check, then its leave check
Paths compared as written, links unresolved the scratch runner's own hand-over, from "a to b: slot a's label is unloaded and its plist gone once its step ends" on, because the sandbox lies under /tmp → /private/tmp passes (the fakes record paths as written)

Before the null-home fix, master's arm and the leave arm both ended runner_launchd.sx:195: force-unwrap of a null optional, leaving /tmp/sak-runner_launchd-<6>. With it, master's arm runs to RUNNER LAUNCHD FAIL (22 checks) and removes its sandbox. In every arm the host's own com.swipelab.sak-runner-b stayed loaded from its plist with the same pid.

Gates

On m5:

  • full (gate run 891) passed at the head, 8c1343a8. It covered the setup, the three sx builds, npm run build, all 69 tests/*.sx files (runner_launchd 112 s, runner_handover 170 s, runner_install 11 s), release.sh with step9, and the deploy contract. The main Playwright config gave 777 passed and 1 skipped, and the runner config gave 10 passed.
  • From a git archive arm of 8c1343a8, tests/run.sh passed runner_launchd (118 s), runner_install (14 s) and runner_handover (186 s).
  • The head's sak-runner was run against the host's real labels with a fake launchctl first on PATH, which passes print to /bin/launchctl and refuses every other subcommand. Its --slot b and --start-slot b called only print gui/501/com.swipelab.sak-runner-b. --start-slot b exited 1 ("holds the slot's name from another install's file") and wrote no plist. The host's com.swipelab.sak-runner-b stayed loaded with the same pid. Two more fakes covered print itself. One answered a record with no path line, and --start-slot b was refused as another install's. The other answered that no such label is held, and --start-slot b went straight to bootstrap with no bootout.
  • Earlier, at beba3b26: fast (gate run 881) passed. full (gate run 884) failed only memories.spec.ts:315 phone-dark, a known race of that shot, and this branch changes no file under web/.

Recovery

This PR changes code only. The recovery the task names had already happened by 2026-10-30, checked on m5 and on the tracker:

  • the harnesses 80521 and 82015 and runAH.sh 96635 no longer exist;
  • launchctl list shows no com.swipelab.sak-runner-test-* agent;
  • runs 715 and 718 read failed · runner_lost.
Tracker: [#3803](https://sak.swipelab.com/tasks/3803) ## What reached the real agent Review run 718 (stream 8029) ran one command last, at seq 271, 23:52:07Z. It copied m5's installed `0.3.6/sak-runner`, gave it a scratch `HOME` and `XDG_CONFIG_HOME` whose configuration named no `slot` and no `service`, and ran it with `--slot a`. A slot that its configuration's `slot` line does not name leaves. Leaving runs `launchctl bootout gui/<uid>/<label>`, and with no `service` line the label is the default `com.swipelab.sak-runner`. launchd's `gui/<uid>` domain is one per user, whatever `HOME` a process runs with. So the scratch copy booted out m5's own slot a, pid 17272, and runs 715 and 718 lost their pump. The stream ends at that command, because its runner was the process it ended. The `runAH.sh` arms ran `tests/runner_launchd.sx`, whose labels are `com.swipelab.sak-runner-test-<6>`, and `mutA.py` only took two checks out of that test. Neither touched the host's label. The scratch tree, the harness's own transcript and the unified log of that minute were all gone by 2026-10-30, so what bootstrapped the agent again at 23:53:12Z is not recoverable. The same holds for any run: a run's `HOME` is its own directory, so `sak-runner --slot a` typed in a run reads a configuration that names no slot. It boots out the runner that runs it, and `sak-runner --start-slot a` replaces it. On Linux, `systemctl --user` acts on a unit name for every `XDG_CONFIG_HOME` alike, with the same effect. ## What it delivers - **A slot is its name and its file.** `runner/slot.sx` asks the service manager which file it holds a slot's name from: the `path` of `launchctl print gui/<uid>/<label>`, or `systemctl --user show --property=FragmentPath --value <unit>`. A slot acts on the name only when that file is its own, compared with each directory's links resolved, since launchd records the resolved path. A label launchd holds with no `path` in its record counts as another install's. - `--start-slot` refuses a name held from another install's file. It writes nothing and fails with "the service manager holds the slot's name from another install's file". From its own file it starts the slot in place of its process, as before. - Leaving and rolling back unload the name only when it is held from the slot's own file, and remove the slot's own file either way. - "The other slot still finishes its steps" (step 3 of Updates) counts only a name held from that slot's own file. - **`tests/runner_launchd.sx`** - `check_other_install`: a second scratch install, whose configuration names slot a's label and no slot, as run 718's setup did. Its `--slot a` and its `--start-slot a` leave slot a's agent loaded from its plist with the same pid. - The test's last check also holds the record of the host's own slot agent that runs the test, its path and pid. The old check read only `$HOME`, which in a gate is the run's home. - The cleanup boots out every agent loaded from the sandbox, the default labels among them, and no other. - A failing scene no longer aborts on a null run home in `check_b_to_a`, which used to skip that cleanup and leave the sandbox behind. - **`tests/runner_install.sx`**: the fake launchctl and systemctl answer the file they hold a name from. On Linux, `--start-slot` refuses a unit the manager holds from another file and leaves it running, and a leaving slot does not disable it. - **`tests/runner_handover.sx`**: the fake systemctl answers `show`. `check_found_taken`'s hand-run `--slot a` gets slot a's unit file first, as a process systemd started has it. - runner/README.md's The slots and Updates say the rule once, as when a slot is loaded. ## Regressions, failing before I ran these from a `git archive` arm of master 39594b31, with the head's test files and `dist/sak-runner` built from master's runner. All ran on m5. - `tests/runner_launchd.sx` failed: - `FAIL another install's --slot a, its configuration naming slot a's label and no slot, leaves slot a's agent running from its plist` - `FAIL another install's --start-slot a fails, writing no agent, and slot a's agent runs on as it was` The other install's leave booted the test's slot a out, so most of the hand-over checks after them fail too: RUNNER LAUNCHD FAIL (22 checks). - `tests/runner_install.sx` failed: - `FAIL ci-linux: another install's --start-slot a fails and writes no unit, sak-runner.service running on from the first install's file` - `FAIL ci-linux: another install's --slot a, its configuration naming no slot, leaves without disabling sak-runner.service` It also failed the three call logs that now open with the manager read: `m5: slot b unloaded itself`, `ci-linux: slot b unloaded itself`, and Linux's install log. That is RUNNER INSTALL FAIL (5 checks). - At the head, all three files pass. Mutation arms of `runner/slot.sx` at 1b974e05, each failing only what it pins: | Mutation | runner_launchd | runner_install | | --- | --- | --- | | Leave unloads whoever holds the name | the `--slot a` check, then the cascade | the Linux leave check | | Start without the refusal | the `--start-slot a` check alone | the Linux start check, then its leave check | | Paths compared as written, links unresolved | the scratch runner's own hand-over, from "a to b: slot a's label is unloaded and its plist gone once its step ends" on, because the sandbox lies under `/tmp` → `/private/tmp` | passes (the fakes record paths as written) | Before the null-home fix, master's arm and the leave arm both ended `runner_launchd.sx:195: force-unwrap of a null optional`, leaving `/tmp/sak-runner_launchd-<6>`. With it, master's arm runs to RUNNER LAUNCHD FAIL (22 checks) and removes its sandbox. In every arm the host's own `com.swipelab.sak-runner-b` stayed loaded from its plist with the same pid. ## Gates On m5: - **`full` (gate run 891) passed at the head, 8c1343a8.** It covered the setup, the three sx builds, `npm run build`, all 69 `tests/*.sx` files (runner_launchd 112 s, runner_handover 170 s, runner_install 11 s), release.sh with step9, and the deploy contract. The main Playwright config gave 777 passed and 1 skipped, and the runner config gave 10 passed. - From a `git archive` arm of 8c1343a8, `tests/run.sh` passed runner_launchd (118 s), runner_install (14 s) and runner_handover (186 s). - The head's `sak-runner` was run against the host's real labels with a fake `launchctl` first on PATH, which passes `print` to `/bin/launchctl` and refuses every other subcommand. Its `--slot b` and `--start-slot b` called only `print gui/501/com.swipelab.sak-runner-b`. `--start-slot b` exited 1 ("holds the slot's name from another install's file") and wrote no plist. The host's `com.swipelab.sak-runner-b` stayed loaded with the same pid. Two more fakes covered `print` itself. One answered a record with no `path` line, and `--start-slot b` was refused as another install's. The other answered that no such label is held, and `--start-slot b` went straight to `bootstrap` with no `bootout`. - Earlier, at beba3b26: `fast` (gate run 881) passed. `full` (gate run 884) failed only `memories.spec.ts:315` phone-dark, a known race of that shot, and this branch changes no file under `web/`. ## Recovery This PR changes code only. The recovery the task names had already happened by 2026-10-30, checked on m5 and on the tracker: - the harnesses 80521 and 82015 and `runAH.sh` 96635 no longer exist; - `launchctl list` shows no `com.swipelab.sak-runner-test-*` agent; - runs 715 and 718 read `failed · runner_lost`.
launchd's gui/<uid> domain and systemd's user manager hold one name for
every HOME of the user. A sak-runner run under another home, by a run or
by a test, therefore named the host's own labels: its --slot a, with a
configuration naming no slot, booted the host's slot a out, and its
--start-slot a replaced it. The slot now reads which file the manager
holds its name from (launchctl print's path, systemd's FragmentPath),
refuses a start over another install's name, and unloads, leaves or
disables only a name held from its own file.
In a gate the test's HOME is the run's, so reading the host's install
under $HOME checked nothing of the runner the gate runs on. The test now
also reads launchd's record of the host's slot whose process is an
ancestor of it, its plist and pid, before and after. Its cleanup boots
out each of the scratch and the host's labels that launchd holds from a
file under the test's directory, and no other.
When slot b never ran its step, check_b_to_a unwrapped a null home and
the test aborted before it booted its agents out and removed its
sandbox.
agra merged commit f42d8e301e into master 2026-10-30 07:03:04 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sx/sak!188
No description provided.