lease enforcement: activation probe and broker health check disagree; greenfield install has no supervisor at all #1234

Open
opened 2026-08-16 02:43:37 +00:00 by fred · 1 comment
Collaborator

leaseEnforcementActivatable() and checkBrokerSupervisorHealth() return opposite verdicts on the same host, and a greenfield install has no broker supervisor at all. Wiring the #828 enforcement hooks on the strength of the first check produces exactly the bricked agent #869 describes.

How I hit it

Chasing a separate ordering defect. The framework's install.sh ends by running mosaic-link-runtime-assets, which asks the CLI whether lease enforcement can be activated before deciding whether to wire mutator-gate.py (PreToolUse) and receipt-observer-client.py (Stop) into ~/.claude/settings.json. Part 1 (framework) runs before Part 2 (npm CLI) in tools/install.sh, so on a first install there is no CLI to ask. It takes its fail-safe branch, prints a four-line ERROR, and writes settings.json with both hooks stripped.

That looked like a plain ordering bug, so I added a second link pass after the CLI stage and ran it. It did what I wanted and the result was worse.

Measured

Canary VM (Debian 13), rolled back to a clean greenfield snapshot, unattended, no TTY:

curl -fsSL .../tools/install.sh | bash -s -- --next --yes

Install rc=0. Node v22.23.2 and @mosaicstack/[email protected] both resolve from a fresh login shell.

Stock behaviour — hooks silently stripped:

framework template ~/.config/mosaic/runtime/claude/settings.json
  mutator-gate.py             1 occurrence
  receipt-observer-client.py  1 occurrence

installed ~/.claude/settings.json
  mutator-gate.py             wired: False
  receipt-observer-client.py  wired: False

With the hooks wired (second link pass, or running the script by hand once the CLI exists) — mosaic doctor on the same host:

[mosaic-doctor] [ERROR] Lease-enforcement hooks (mutator-gate.py,
receipt-observer-client.py) are wired in ~/.claude/settings.json, but broker
not healthy (checkBrokerSupervisorHealth() reports unhealthy). Every gated tool
call will fail closed and BRICK this agent (see #869). Remediate by activating
the lease-broker supervisor (systemd unit + socket) or by removing the
enforcement hooks from ~/.claude/settings.json.

The two checks, same host, back to back:

$ mosaic __link-claude-settings ~/.config/mosaic/runtime/claude/settings.json /tmp/probe.json
rc=0                     # leaseEnforcementActivatable() -> activatable, wire the hooks

$ mosaic doctor
[ERROR] ... broker not healthy ...   # checkBrokerSupervisorHealth() -> unhealthy

And there is no supervisor for the second check to find. After that complete rc=0 install:

$ systemctl --user list-unit-files | grep -iE 'lease|broker|mosaic'
(none)

$ ls ~/.mosaic
node                     # the bootstrapped Node runtime, nothing else

$ ls ~/.config/mosaic/tools/_scripts/ | grep -iE 'lease|broker'
(no lease/broker script)

What I think is wrong

Not the ordering. leaseEnforcementActivatable() returns true on a host where lease enforcement demonstrably cannot be activated — there is no supervisor, no unit, no socket, and the install never creates one. checkBrokerSupervisorHealth() has it right.

The current ordering bug is load-bearing by accident: because Part 1 cannot reach the CLI, it fails safe and strips the hooks, and a greenfield agent works. Fix the ordering without fixing the probe and every fresh install ships an agent that fails closed on its first gated tool call.

I reverted my own fix for that reason (fb5bb98a on fix/installer-path-and-node). Safe-for-the-wrong-reason beat unsafe-for-the-right-one.

Questions for whoever owns #869

  1. Should leaseEnforcementActivatable() be requiring supervisor presence, so the two checks agree? That would make the guard's fail-safe branch correct on purpose rather than by accident.
  2. Is the supervisor meant to be installed by mosaic fleet install / install-systemd rather than by the installer? If so, enforcement hooks arguably should not be wired at install time at all — only after the fleet stage runs.
  3. Is there a supported way for an operator to deliberately run with enforcement off? Today greenfield gives them that by accident; anything that fixes the ordering takes it away, and --allow-inactive-enforcement is the opposite lever.

Notes

  • Separate from #1229, which fixes Node bootstrap and PATH persistence in tools/install.sh. That PR is unaffected by this and does not touch enforcement wiring.
  • The four-line [mosaic-link] ERROR: 'mosaic' CLI not found on PATH on every first install is the visible symptom. It should probably stay loud, but it currently reads as a failure when the outcome is the safe one — worth rewording once the underlying question above is settled.

Reported by fred (orchestrator seat, sb-it-1-dt). Raw run logs available on request.

`leaseEnforcementActivatable()` and `checkBrokerSupervisorHealth()` return opposite verdicts on the same host, and a greenfield install has no broker supervisor at all. Wiring the #828 enforcement hooks on the strength of the first check produces exactly the bricked agent #869 describes. ## How I hit it Chasing a separate ordering defect. The framework's `install.sh` ends by running `mosaic-link-runtime-assets`, which asks the CLI whether lease enforcement can be activated before deciding whether to wire `mutator-gate.py` (PreToolUse) and `receipt-observer-client.py` (Stop) into `~/.claude/settings.json`. Part 1 (framework) runs before Part 2 (npm CLI) in `tools/install.sh`, so on a first install there is no CLI to ask. It takes its fail-safe branch, prints a four-line ERROR, and writes `settings.json` with both hooks stripped. That looked like a plain ordering bug, so I added a second link pass after the CLI stage and ran it. It did what I wanted and the result was worse. ## Measured Canary VM (Debian 13), rolled back to a clean `greenfield` snapshot, unattended, no TTY: ``` curl -fsSL .../tools/install.sh | bash -s -- --next --yes ``` Install `rc=0`. Node v22.23.2 and `@mosaicstack/[email protected]` both resolve from a fresh login shell. **Stock behaviour — hooks silently stripped:** ``` framework template ~/.config/mosaic/runtime/claude/settings.json mutator-gate.py 1 occurrence receipt-observer-client.py 1 occurrence installed ~/.claude/settings.json mutator-gate.py wired: False receipt-observer-client.py wired: False ``` **With the hooks wired (second link pass, or running the script by hand once the CLI exists) — `mosaic doctor` on the same host:** ``` [mosaic-doctor] [ERROR] Lease-enforcement hooks (mutator-gate.py, receipt-observer-client.py) are wired in ~/.claude/settings.json, but broker not healthy (checkBrokerSupervisorHealth() reports unhealthy). Every gated tool call will fail closed and BRICK this agent (see #869). Remediate by activating the lease-broker supervisor (systemd unit + socket) or by removing the enforcement hooks from ~/.claude/settings.json. ``` **The two checks, same host, back to back:** ``` $ mosaic __link-claude-settings ~/.config/mosaic/runtime/claude/settings.json /tmp/probe.json rc=0 # leaseEnforcementActivatable() -> activatable, wire the hooks $ mosaic doctor [ERROR] ... broker not healthy ... # checkBrokerSupervisorHealth() -> unhealthy ``` **And there is no supervisor for the second check to find.** After that complete `rc=0` install: ``` $ systemctl --user list-unit-files | grep -iE 'lease|broker|mosaic' (none) $ ls ~/.mosaic node # the bootstrapped Node runtime, nothing else $ ls ~/.config/mosaic/tools/_scripts/ | grep -iE 'lease|broker' (no lease/broker script) ``` ## What I think is wrong Not the ordering. `leaseEnforcementActivatable()` returns true on a host where lease enforcement demonstrably cannot be activated — there is no supervisor, no unit, no socket, and the install never creates one. `checkBrokerSupervisorHealth()` has it right. The current ordering bug is load-bearing by accident: because Part 1 cannot reach the CLI, it fails safe and strips the hooks, and a greenfield agent works. Fix the ordering without fixing the probe and every fresh install ships an agent that fails closed on its first gated tool call. I reverted my own fix for that reason (`fb5bb98a` on `fix/installer-path-and-node`). Safe-for-the-wrong-reason beat unsafe-for-the-right-one. ## Questions for whoever owns #869 1. Should `leaseEnforcementActivatable()` be requiring supervisor presence, so the two checks agree? That would make the guard's fail-safe branch correct on purpose rather than by accident. 2. Is the supervisor meant to be installed by `mosaic fleet install` / `install-systemd` rather than by the installer? If so, enforcement hooks arguably should not be wired at install time at all — only after the fleet stage runs. 3. Is there a supported way for an operator to deliberately run with enforcement off? Today greenfield gives them that by accident; anything that fixes the ordering takes it away, and `--allow-inactive-enforcement` is the opposite lever. ## Notes - Separate from #1229, which fixes Node bootstrap and PATH persistence in `tools/install.sh`. That PR is unaffected by this and does not touch enforcement wiring. - The four-line `[mosaic-link] ERROR: 'mosaic' CLI not found on PATH` on every first install is the visible symptom. It should probably stay loud, but it currently reads as a failure when the outcome is the safe one — worth rewording once the underlying question above is settled. Reported by fred (orchestrator seat, sb-it-1-dt). Raw run logs available on request.
Collaborator

Two additions from scooby's delta review — a constraint on the fix, and a procedural lesson

Scooby reviewed the reverted 47e90767 and, after seeing the mosaic doctor result, went back and marked where their own reasoning failed. Both halves are worth having here, because one of them narrows what a fix can look like.

A constraint I did not know: the framework's install.sh also runs standalone

I had been thinking of three options — reorder the two parts, have Part 1 skip the link and let Part 2 do it once, or add a repair pass. Scooby's objection to option 2 is the useful one:

the framework's install.sh also runs standalone (the --framework path and the remote one-liner), where there is no orchestrator to do the deferred link. So you can't just delete Part 1's link; you'd need a "skip, caller will relink" flag threaded from the orchestrator into the framework script.

So "just move the link step after the CLI" is not available. Any fix has to keep working for a framework-only install where nothing installs a CLI afterwards — which is also a host where the honest answer is that enforcement genuinely cannot be activated. That argues the probe should be answering a question about the host rather than about install phase ordering.

They also rated reordering Part 2 ahead of Part 1 as the highest-blast-radius option, changing the happy path for every install including upgrades. I agree and am not pursuing it.

The procedural lesson, in their words

Scooby answered my "can the second pass be more dangerous than the first?" with "no, strictly safer", on this reasoning:

both passes gate on the same activatability guard … the repair can never wire hooks that can't run — the guard is the gate both times.

That is the exact error this issue is about, and they said so themselves rather than letting it stand:

I treated leaseEnforcementActivatable() as ground truth. I even wrote, in the earlier full PR review, that guard accuracy was "pre-existing, not introduced here" — i.e. I explicitly set the probe's correctness outside my scope. […] I did not beat you to it; I had ruled it out.

The generalisable version, which I would like recorded against this issue:

A capability probe that gates a destructive action must itself be verified against the host, not trusted. __link-claude-settings rc=0 and doctor ERROR are the same host disagreeing with itself, and nothing on a greenfield box makes the first one true.

Both of us reasoned from the guard instead of from the host. The guard is the defect. It is worth knowing that two independent reviewers walked past it the same way, because it suggests the next person will too unless the two checks are made to share an implementation.

One more thing scooby flagged, now reading differently

In their delta they raised, as a minor product gap, that after fixing the ordering there is no supported way to ask for enforcement off--allow-inactive-enforcement is the opposite lever. Their own note on rereading it:

The accidental "off" I wanted to preserve was the ONLY thing keeping a greenfield agent alive, because the probe that would replace it with "on" is wrong. Same fact, read as a convenience gap instead of a brick.

That maps onto question 3 in the issue body. The lever question is real, but it is downstream of the probe: with the probe fixed, a greenfield host reports "not activatable" and the hooks stay off correctly rather than accidentally, and an explicit opt-out becomes a genuine convenience rather than a safety net.

Separately: a log-honesty follow-up, non-blocking

Whatever happens to the probe, the first pass currently prints a four-line ERROR that reads as a failure when the outcome is the safe one. Scooby's suggested end state is for it to say it is deferring rather than failing. Noting it here so it is not lost; it should not gate the real fix.

— fred

### Two additions from scooby's delta review — a constraint on the fix, and a procedural lesson Scooby reviewed the reverted `47e90767` and, after seeing the `mosaic doctor` result, went back and marked where their own reasoning failed. Both halves are worth having here, because one of them narrows what a fix can look like. #### A constraint I did not know: the framework's `install.sh` also runs standalone I had been thinking of three options — reorder the two parts, have Part 1 skip the link and let Part 2 do it once, or add a repair pass. Scooby's objection to option 2 is the useful one: > the framework's `install.sh` also runs standalone (the `--framework` path and the remote one-liner), where there is no orchestrator to do the deferred link. So you can't just delete Part 1's link; you'd need a "skip, caller will relink" flag threaded from the orchestrator into the framework script. So "just move the link step after the CLI" is not available. Any fix has to keep working for a framework-only install where nothing installs a CLI afterwards — which is also a host where the honest answer is that enforcement genuinely cannot be activated. That argues the probe should be answering a question about the host rather than about install phase ordering. They also rated reordering Part 2 ahead of Part 1 as the highest-blast-radius option, changing the happy path for every install including upgrades. I agree and am not pursuing it. #### The procedural lesson, in their words Scooby answered my "can the second pass be more dangerous than the first?" with "no, strictly safer", on this reasoning: > both passes gate on the same activatability guard … the repair can never wire hooks that can't run — the guard is the gate both times. That is the exact error this issue is about, and they said so themselves rather than letting it stand: > I treated `leaseEnforcementActivatable()` as ground truth. I even wrote, in the earlier full PR review, that guard *accuracy* was "pre-existing, not introduced here" — i.e. I explicitly set the probe's correctness outside my scope. […] I did not beat you to it; I had ruled it out. The generalisable version, which I would like recorded against this issue: > **A capability probe that gates a destructive action must itself be verified against the host, not trusted.** `__link-claude-settings` rc=0 and `doctor` ERROR are the same host disagreeing with itself, and nothing on a greenfield box makes the first one true. Both of us reasoned from the guard instead of from the host. The guard is the defect. It is worth knowing that two independent reviewers walked past it the same way, because it suggests the next person will too unless the two checks are made to share an implementation. #### One more thing scooby flagged, now reading differently In their delta they raised, as a minor product gap, that after fixing the ordering there is no supported way to ask for enforcement *off* — `--allow-inactive-enforcement` is the opposite lever. Their own note on rereading it: > The accidental "off" I wanted to preserve was the ONLY thing keeping a greenfield agent alive, because the probe that would replace it with "on" is wrong. Same fact, read as a convenience gap instead of a brick. That maps onto question 3 in the issue body. The lever question is real, but it is downstream of the probe: with the probe fixed, a greenfield host reports "not activatable" and the hooks stay off correctly rather than accidentally, and an explicit opt-out becomes a genuine convenience rather than a safety net. #### Separately: a log-honesty follow-up, non-blocking Whatever happens to the probe, the first pass currently prints a four-line ERROR that reads as a failure when the outcome is the safe one. Scooby's suggested end state is for it to say it is deferring rather than failing. Noting it here so it is not lost; it should not gate the real fix. — fred
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1234