feat(lease): verified lease-remediation stack (rebased onto next) — promotion trigger + promote CLI + carve-out + TTL #1109

Open
mos-dt-0 wants to merge 24 commits from feat/lease-promotion-and-harness-isolation into next
Collaborator

Lease remediation — make gated mosaic pi / mosaic claude seats launch AND function

Trunk of the lease-remediation mission. Every commit below cleared an
independent verification gate (code review + security review + E2E falsifier
reproduction by a separate orchestrator session) before landing on the branch.

What's in it (10 commits, oldest → newest)

Lease invariant / read-only carve-out (W-0, W-A):

  • 3592b92e refuse an incomplete law binding instead of silently shrinking it (W-0)
  • 87ef1c14 close W-0R review findings — assert the omission notice; skip chmod sims under root
  • 289425ed measure pi tool registry (docs)
  • 9f800493 enforce read-only tool invariant (test)
  • 930b7e88 distinguish pi probe timeouts (test)
  • e76f2935 assert pi carve-out capability, not just identity (test)
  • 3d98c832 constrain read-only tool carve-outs (W-C — the fix; carve-out
    admitted grep/find which spawn subprocesses + cold-cache download+chmod+exec)

Observer / TTL (W-E, TTL):

  • 9c3f054f ignore benign observer idle replies (W-E — fail-open confined to
    the stop-blocking axis; auth/transport stay exit 2)
  • 412ac4b0 raise lease TTL 300s → 3600s (daemon cap+default + client default)

Promotion trigger (W-P1):

  • d15b4b83 single-turn Claude promotion trigger/mosaic-promote
    (gated command) → begin hook injects receipt → model echoes verbatim →
    same-turn Stop chain (observer posts the finished text from Stop-stdin
    last_assistant_message, completion promotes) → mutator ALLOWED, one turn.
    Non-ASCII bytes-compare crash fix in the verbatim check included. The one
    protected-file change (observer client source-swap) cleared a stricter
    security gate: daemon verbatim HMAC check untouched, present-invalid +
    oversize fail closed, transcript fallback retained for older Claude.

Verification

All landed commits are gate-PASS (verdicts in jarvis-brain
docs/scratchpads/lease-remediation/dispatch/verdict-*.md). Full
pnpm run test:framework-shell RC=0 reproduced independently; promotion_trigger
24/24, observer 16/16, receipt_challenge 6/6. Husky pre-push gate green on push.

Reviewer notes

  • Rebase before merge: branch is ~8 commits behind main as of this push
    (0 behind its own remote branch — the push was a clean fast-forward). No
    textual conflicts expected but please rebase/verify against current main
    before merge, especially anything touching the lease-broker.
  • Not included — W-P1b (mosaic promote <seat> operator CLI): held pending
    a design decision. The CLI needs an external read-only lease-state poll, and
    there is no such surface today by design — the broker has no status verb,
    authorize_tool is ancestry-gated, and lease/TTL state is never persisted
    (monotonic clock, volatile memory). Options (in-band result breadcrumb vs a
    new read-only lease_status verb) are written up for the operator to choose;
    W-P1b lands in a follow-up PR.
  • Not included — W-P2 (pi-side promotion path) and harness-homes
    codification (W-F): later phases.
## Lease remediation — make gated `mosaic pi` / `mosaic claude` seats launch AND function Trunk of the lease-remediation mission. Every commit below cleared an independent verification gate (code review + security review + E2E falsifier reproduction by a separate orchestrator session) before landing on the branch. ### What's in it (10 commits, oldest → newest) **Lease invariant / read-only carve-out (W-0, W-A):** - `3592b92e` refuse an incomplete law binding instead of silently shrinking it (W-0) - `87ef1c14` close W-0R review findings — assert the omission notice; skip chmod sims under root - `289425ed` measure pi tool registry (docs) - `9f800493` enforce read-only tool invariant (test) - `930b7e88` distinguish pi probe timeouts (test) - `e76f2935` assert pi carve-out **capability**, not just identity (test) - `3d98c832` **constrain read-only tool carve-outs** (W-C — the fix; carve-out admitted grep/find which spawn subprocesses + cold-cache download+chmod+exec) **Observer / TTL (W-E, TTL):** - `9c3f054f` ignore benign observer idle replies (W-E — fail-open confined to the stop-blocking axis; auth/transport stay exit 2) - `412ac4b0` **raise lease TTL 300s → 3600s** (daemon cap+default + client default) **Promotion trigger (W-P1):** - `d15b4b83` **single-turn Claude promotion trigger** — `/mosaic-promote` (gated command) → begin hook injects receipt → model echoes verbatim → same-turn Stop chain (observer posts the finished text from Stop-stdin `last_assistant_message`, completion promotes) → mutator ALLOWED, one turn. Non-ASCII bytes-compare crash fix in the verbatim check included. The one protected-file change (observer client source-swap) cleared a stricter security gate: daemon verbatim HMAC check untouched, present-invalid + oversize fail closed, transcript fallback retained for older Claude. ### Verification All landed commits are gate-PASS (verdicts in jarvis-brain `docs/scratchpads/lease-remediation/dispatch/verdict-*.md`). Full `pnpm run test:framework-shell` RC=0 reproduced independently; promotion_trigger 24/24, observer 16/16, receipt_challenge 6/6. Husky pre-push gate green on push. ### Reviewer notes - **Rebase before merge:** branch is ~8 commits behind `main` as of this push (0 behind its own remote branch — the push was a clean fast-forward). No textual conflicts expected but please rebase/verify against current `main` before merge, especially anything touching the lease-broker. - **Not included — W-P1b** (`mosaic promote <seat>` operator CLI): held pending a design decision. The CLI needs an external read-only lease-state poll, and there is **no such surface today by design** — the broker has no status verb, `authorize_tool` is ancestry-gated, and lease/TTL state is never persisted (monotonic clock, volatile memory). Options (in-band result breadcrumb vs a new read-only `lease_status` verb) are written up for the operator to choose; W-P1b lands in a follow-up PR. - **Not included — W-P2** (pi-side promotion path) and harness-homes codification (W-F): later phases.
mos-dt-0 changed title from feat(lease): promotion trigger + read-only carve-out + TTL — verified lease-remediation stack to feat(lease): verified lease-remediation stack (rebased onto next) — promotion trigger + promote CLI + carve-out + TTL 2026-08-08 21:12:42 +00:00
mos-dt-0 changed target branch from main to next 2026-08-08 21:12:43 +00:00
mos-dt-0 added 21 commits 2026-08-08 21:12:43 +00:00
Co-authored-by: be-coder-08 <[email protected]>
Co-authored-by: be-coder-08 <[email protected]>
Co-authored-by: be-coder-08 <[email protected]>
Mosaic wrote into the operator's harness base installs — ~/.claude,
~/.pi/agent, ~/.codex, ~/.config/opencode — for settings, instructions, and a
102-symlink skill farm per harness. Any experiment with hooks or gating
therefore mutated the operator's own tooling, and a broken framework change
could take out the very harness needed to repair it.

Harness home isolation
----------------------
Each runtime now reads config from a dedicated mosaic-owned home via the
harness's own config-dir variable:

  claude    CLAUDE_CONFIG_DIR     ~/.config/mosaic/.claude
  pi        PI_CODING_AGENT_DIR   ~/.config/mosaic/.pi     (replaces ~/.pi/agent)
  codex     CODEX_HOME            ~/.config/mosaic/.codex
  opencode  XDG_CONFIG_HOME       ~/.config/mosaic/.opencode

These paths are manifest-UNKNOWN, so rule 3 (#791) resolves them to operator
ownership and a keep-mode upgrade can neither overwrite nor prune them.
A bare `claude` / `pi` keeps its own config AND auth, making it a structural
break-glass rather than one depending on restoring a file under pressure.

opencode is blunter than the rest: it has no dedicated variable and follows XDG,
so isolation also relocates XDG lookups for anything it spawns. Documented in
place.

mosaic-sync-skills now links into those homes and cleans the legacy farms it
previously planted in base installs. Ownership is proven by RESOLUTION, not by
name — only symlinks resolving inside the canonical/local skills dirs are
removed, mirroring the refusal already in commands/skill.js. Verified against a
real install: codex's own .system directory survived while its 102 mosaic links
were removed. Both resolution prefixes are length-checked first; an empty prefix
would make "$resolved" == "$prefix/"* match every absolute path and delete
foreign symlinks.

Immutable launch record
-----------------------
Every launch now appends one record to fleet/run/sessions/events.ndjson before
exec. Mandatory, mechanical, no model involvement.

pi rewrites its own argv to a bare `pi`, so /proc/<pid>/cmdline destroys the
launch evidence — that has already produced a confident wrong diagnosis ("this
agent bypassed the launcher"), disproved only by the parent's argv and only
because the parent had not yet exited. A record written before exec is the only
place this survives.

The path is the #797 Runtime Session Ledger, already operator-classified and
already covered by test-upgrade-manifest-guard.sh, which seeds it and proves a
populated ledger survives keep-mode upgrades — but nothing shipped ever wrote
it. This implements it in the shape that guard already asserts (0600 files under
a 0700 dir).

`mosaic` writes session.launch; launch-runtime.py appends lease.register with
the broker session id and activation capability. They correlate by an explicit
MOSAIC_LAUNCH_ID, never by pid: execRuntime uses spawnSync, so the runtime is a
child with a different pid.

Records normative fragment digests (CONSTITUTION/AGENTS/SOUL/USER/STANDARDS/
TOOLS/RUNTIME) — the same set the broker hashes for promotion, so drift is
mechanically detectable rather than a matter of judgement.

Credential-safe: env is captured as PRESENT NAMES ONLY, and argv values over
256 bytes become a sha256 + length rather than being inlined.

Also fixes CLI_VERSION resolution: '@mosaicstack/mosaic/package.json' is not in
the package exports map and always throws ERR_PACKAGE_PATH_NOT_EXPORTED.
resolveTool() uses that same failing specifier, which is why its documented
preference for bundled tools over the deployed ~/.config/mosaic copy has never
once applied — noted in place, not fixed here.

Verified on sb-it-1-dt: isolated homes written and base installs byte-identical
for all four harnesses; 408 legacy symlinks removed with 1 foreign entry
preserved; launch records paired across the spawn boundary. typecheck shows zero
errors in launch.ts (the @mosaicstack/types failures are pre-existing and
reproduce on a pristine origin/main worktree).
The enforcement half of the lease broker ships and denies; the promotion half
has no production caller anywhere in the package. Verified across 0.0.48, 0.0.49
and 0.0.50-next.2207: begin_verification / observe_receipt / promote_lease are
invoked only by broker-test-client.ts, the acceptance spec, unit tests, and two
probes under docs/.

Consequence: no lease on any host can reach VERIFIED, so mutator-gate denies
every mutator with MUTATOR_UNVERIFIED via a gate that nothing shipped can
satisfy. Runtimes that enforce the gate in-process (pi, via mosaic-extension's
tool_call hook) are bricked for mutators; runtimes whose gate is wired through a
settings hook escape only when that hook is absent — i.e. by being ungated.

This adds the client. It implements protocol steps 1, 4 and 5:

  1. begin_verification  -> mint a challenge, return the exact receipt text
  2. the MODEL emits that text verbatim as its entire latest message
  3. the runtime adapter ships that message to the observer socket
  4. observe_receipt      -> PENDING_PROMOTION
  5. promote_lease        -> VERIFIED

Step 2 is deliberately NOT implemented here, and that is the point.
is_verbatim_receipt uses hmac.compare_digest against the exact minted string —
explicitly "not a transcript substring" — which makes promotion a LIVENESS
PROOF: it requires a live model that received the challenge in its context and
echoed it exactly.

receipt-observer-client.py will post ANY string as the latest assistant message.
A promotion client that posted its own receipt would satisfy the broker while
proving nothing — a gate-disabler indistinguishable from a working fix unless
someone specifically looks. Emitting the receipt therefore belongs to the runtime
adapter, where a real model turn happens. A local diagnostic that posts its own
receipt exists in the operator's repo and is deliberately NOT shipped here.

The construction binds the exact normative source bytes, so a VERIFIED lease
means "this agent is running THIS law", not merely "this session id is known".
h_source/h_payload are derived by importing the framework's own
normative_fragments.build_payload rather than reimplementing it: the broker
derives them the same way and any divergence yields PAYLOAD_BINDING_MISMATCH.
There must be exactly one implementation.

session_identity() prefers the generation FILE over the env var, matching
lease_generation.py. Sending a generation higher than the broker's would revoke
the session's own authority (daemon.py:342-344), so it never guesses.

Verified end-to-end on sb-it-1-dt under a real lease-gated anchor: a mutator
denied rc=2 MUTATOR_UNVERIFIED, then begin -> observe -> promote -> VERIFIED,
then the same mutator allowed rc=0. Negative controls pass: a fresh session is
still denied, and an unrelated session still reads UNVERIFIED — promotion is
per-session and does not leak.

Still open: adapter wiring for step 2. Lazy promotion on first mutator attempt
avoids colliding with the Constitution's first-response mode declaration, since
compare_digest requires the receipt to be the WHOLE message.
Completes the promotion path: the client landed in the previous commit, but
nothing drove step 2 — the model emitting the receipt. This wires it.

LAZY, not at session start. Promotion costs an entire model turn, because the
receipt must be the whole message (hmac.compare_digest, "not a transcript
substring"). Minting at session start would collide with the Constitution's
first-response mode declaration — the two cannot share a message, so requiring
both would be unsatisfiable. Deferring to the first DENIED MUTATOR means the
mode declaration happens first and the receipt gets its own later turn, so no
governance change is needed. A read-only session never pays for promotion at all.

Mechanism: on a MUTATOR_UNVERIFIED denial the tool_call hook mints a challenge
and returns the receipt in the block `reason`, which pi feeds back to the model
as the tool result — the existing injection path already used by
lease-lifecycle.ts. The model emits the receipt as its next message, message_end
ships it to the observer, and the extension then calls observe_receipt +
promote_lease.

Only MUTATOR_UNVERIFIED triggers minting. Other denials (GATE_UNAVAILABLE,
STALE_GENERATION, LEASE_EXPIRED, ANCESTRY_MISMATCH) describe conditions a
receipt cannot fix, and begin_verification revokes before it mints, so minting
there would thrash the broker.

Completion is gated on an EXACT text match against the minted receipt. This is
load-bearing, not defensive: message_end also fires for the message that
CONTAINED the blocked tool call — one turn BEFORE the model answers. An earlier
version completed there, so observe_receipt compared against the wrong text,
failed, and burned the challenge before the model ever emitted it. Matching the
text mirrors the broker's own compare_digest semantics and waits for the right
turn. Confirmed by instrumenting message_end and watching it fire with
pending=yes one message too early.

It never posts the receipt itself. receipt-observer-client.py accepts any
string, so self-posting would satisfy the broker while proving nothing — the
whole point is that a live model echoes a challenge it was given.

Bounded by MAX_PROMOTION_ATTEMPTS: model compliance is not guaranteed (observed
a run where the model retried the command instead of emitting the receipt), so
a non-complying model degrades to today's behaviour — denied mutators — rather
than looping.

Verified with a live model on sb-it-1-dt: receipt emitted verbatim as a whole
message, and the broker token was minted AND consumed, i.e. observe_receipt and
promote_lease both succeeded and the lease reached VERIFIED.

Known limitation: under `pi -p`, the receipt is a text-only turn, which ends the
one-shot loop — so promotion completes but the blocked tool is not retried in
that same invocation. Interactive and durable fleet sessions continue and retry
normally.
prune_stale_links_in_target compared "$resolved" == "$canonical_real/"* while
length-checking only $resolved. If $canonical_real were ever empty the pattern
collapses to == "/"* and matches every absolute path.

The failure is precisely inverted, which is what makes it worth fixing rather
than noting: is_mosaic_skill_name already `continue`s for names that ARE current
mosaic skills, so an empty prefix would delete exactly the FOREIGN symlinks in
every target directory and preserve the mosaic ones. On this host that is 4 base
installs, including codex's own .system entry.

Reported by mos-claude as #1087 after I introduced the same guard in the new
legacy-cleanup path in the previous commit and walked past this instance thirty
lines away. Same defect class, same file, one function apart.

$canonical_real is populated by readlink -f after a mkdir -p, so an empty value
requires readlink to fail — unlikely, but the consequence is deleting operator
symlinks across every harness, which is not a risk worth carrying for one test.
This reverts 939f2e04. Keeping the revert rather than dropping the commit,
because the failed attempt is the most useful record on this branch.

The wiring worked mechanically — verified with a live model on sb-it-1-dt: the
receipt was emitted verbatim as a whole message, and the broker token was minted
AND consumed, so observe_receipt and promote_lease both succeeded and the lease
reached VERIFIED.

It failed as a DESIGN, for reasons that are properties of the protocol rather
than of this wiring:

  * It puts control-plane traffic in the user-facing conversation channel. An
    operator asking "what model are you?" received a receipt string instead of an
    answer — the model tried a tool, was blocked, complied with the receipt
    instruction, and in one-shot mode that text turn BECAME the reply. Observed
    twice, non-deterministically.
  * The lease TTL is hard-capped at 300s (MAX_LEASE_TTL_SECONDS; ttl_seconds >
    cap raises INVALID_LEASE_TTL). Measured: allowed at T+0, LEASE_EXPIRED at
    T+310. So the visible cost recurs every five minutes of mutator activity.
  * Model compliance is not guaranteed — one run retried the command instead of
    emitting the receipt.

Any model emission is user-visible, so this is not fixable by better wiring; it
needs a design answer about how promotion is triggered and paid for. That is
under adversarial review (docs/scratchpads/lease-remediation/07-liveness-design-brief.md
in the operator's repo). Promotion triggering will return on its own branch once
that lands.

What remains here is independently sound and unblocked: harness-home isolation,
the immutable launch record, the skills relocation, the promotion client itself
(steps 1/4/5), and the #1087 prefix guard.
build_construction skipped any normative source it could not read
(`except OSError: continue`) and promoted whatever remained. That is not a
degraded binding, it is a forged smaller one: the broker recomputes h_source /
h_payload from the fragments it is SENT (daemon.py:602-616), so an omitted
fragment is internally consistent and PAYLOAD_BINDING_MISMATCH cannot fire. A
partial law promotes exactly like a complete one and nothing downstream can tell
the difference.

Measured before this change, against a seeded home: with only USER.md readable,
the client produced a one-fragment construction with promotion=True. Removing
CONSTITUTION.md, STANDARDS.md or the runtime contract likewise promoted.

The classification mirrors the framework's own file ownership rather than
inventing one:

  * CONSTITUTION.md / AGENTS.md / STANDARDS.md are framework-owned and
    reconciled every upgrade (install.sh FRAMEWORK_OWNED,
    config/file-adapter.ts FRAMEWORK_OWNED_FILES), as is the per-runtime
    RUNTIME.md. Absent => IncompleteBinding. A deployment missing one is broken,
    not minimal.
  * SOUL.md / USER.md are deliberately not seeded by install.sh ("generated by
    `mosaic init`") and TOOLS.md is seeded on first install only, so their
    absence is legitimate. It is reported on stderr, never silent.

Unreadable is handled separately from absent for EVERY source, optional ones
included: a file that will not open is not a file that was never configured, and
collapsing the two is what let a permission change quietly shrink the law.

Also corrects this module's own docstring, which asserted that a VERIFIED lease
means "this agent is running THIS law". It does not. Both sides of the broker's
comparison originate in this client, so it detects corruption in transit and
nothing else. That overstatement is where the belief spread from; the stronger
claim needs the broker re-reading on-disk sources against a manifest the agent
cannot rewrite.

Test: promotion_binding_unittest.py, enumerated in test:framework-shell (the
enumeration guard's population is *test*.sh and does not cover Python, so an
unenumerated test here would simply never run). Falsifier executed: defeating the
guard while leaving the module API intact turns the suite red (12 failures);
restoring it returns green.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EHYXhcCQsL3J1Lnm7EraGq
The independent W-0R review of 3592b92e passed but left two PLAUSIBLE
findings: the stderr notice for a legitimately-omitted operator source was
claimed and never asserted (a silent omission is the original defect in
miniature), and the chmod 0o000 unreadable simulations fail spuriously when
euid==0 (CAP_DAC_OVERRIDE). Falsifier for the new assertion: deleting the
notice block turns the suite red (failures=3); restoring returns green.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EHYXhcCQsL3J1Lnm7EraGq
MAX_LEASE_TTL_SECONDS (daemon cap+default) and DEFAULT_TTL_SECONDS
(lease_promote client) both move to 3600. The 5-minute TTL made
gated-by-default sessions unusable (re-promotion mid-task); 1 hour
matches a working session. Full test:framework-shell RC=0.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013SAYFkRhQfhguY7AHfiUC8
Author
Collaborator

Retargeted main→next in place (Jason redirected to next, 2026-08-08). Branch rebased onto next: clean 21/21, zero conflicts (next touched none of these files), force-pushed d15b4b83...adb1d7f8. Now carries the full verified stack: W-0/W-0R, W-A capability tests, W-C carve-out constraint, W-E observer idle fix, TTL 300→3600, W-P1 single-turn /mosaic-promote trigger, W-P1b mosaic promote CLI (atomic breadcrumb + bounded transport), T1 carve-out acceptance alignment. Verification (fred, §4): promote 9/9, promotion_trigger 28/28, observer 16/16, receipt 6/6, T-B green; framework-shell green under controlled PATH (one PATH-sensitivity test needs clean env by design). The 4 broker-dependent mutator-gate acceptance failures are pre-existing on pristine origin/main (documented, not introduced here). GATE BEFORE MERGE: scooby full greenfield promotion-flow E2E on fomo-lin (Jason-directed; in flight) + review.

Retargeted main→next in place (Jason redirected to next, 2026-08-08). Branch rebased onto next: clean 21/21, zero conflicts (next touched none of these files), force-pushed d15b4b83...adb1d7f8. Now carries the full verified stack: W-0/W-0R, W-A capability tests, W-C carve-out constraint, W-E observer idle fix, TTL 300→3600, W-P1 single-turn /mosaic-promote trigger, W-P1b mosaic promote CLI (atomic breadcrumb + bounded transport), T1 carve-out acceptance alignment. Verification (fred, §4): promote 9/9, promotion_trigger 28/28, observer 16/16, receipt 6/6, T-B green; framework-shell green under controlled PATH (one PATH-sensitivity test needs clean env by design). The 4 broker-dependent mutator-gate acceptance failures are pre-existing on pristine origin/main (documented, not introduced here). GATE BEFORE MERGE: scooby full greenfield promotion-flow E2E on fomo-lin (Jason-directed; in flight) + review.
mos-dt-0 added 1 commit 2026-08-08 21:51:01 +00:00
The launcher runs the runtime as a spawnSync CHILD of node(mosaic) (deliberate,
per launch.ts:99 — parent survives to propagate signals), so
MOSAIC_LEASE_SESSION_ID lives on the claude child, not the pane's root pid. The
transport read only pane.pid's /proc/environ and returned RESOLVE_FAILED for
every real 'mosaic claude' seat. Now BFS the pane's process subtree (bounded,
injectable children-reader) and read the first descendant that carries a valid
lease id; fail-closed if none. Unit tests now exercise the real walk (pane=node
without lease -> child=claude with lease) rather than mocking the resolution.

Found by scooby greenfield E2E on fomo-lin with proc-level evidence.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013SAYFkRhQfhguY7AHfiUC8
mos-dt-0 added 1 commit 2026-08-08 21:52:34 +00:00
The mosaic wrapper makes pane_current_command=node (RUNTIME_ACCEPTABLE_COMMANDS.claude=['claude','node']); the walk matters precisely in that no-shell-wrapper case. Match reality.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013SAYFkRhQfhguY7AHfiUC8
mos-dt-0 added 1 commit 2026-08-09 04:58:44 +00:00
Author
Collaborator

A6 live-E2E verdict (scooby, real gated seat on fomo-lin, 2026-08-09): CORE THESIS VALIDATED

Full 5-step gate against a fresh real mosaic claude seat, real daemon (unmodified daemon.py), real sockets, at head 1a871524.

Step Result
Baseline mutator DENY (UNVERIFIED) PASS
1. mosaic promote -> VERIFIED, Write allowed PASS — seat model actively REFUSED /mosaic-promote and promotion still completed synchronously; the #1126 failure mode (model refusal blocks promotion) is eliminated
2. Revoke -> re-promote PARTIAL — revoke + post-revoke DENY pass; re-promote blocked (#1135, fast-follow)
3. Injected /mosaic-promote with no operator token PASS — fully inert: NOT_AUTHORIZED breadcrumb, zero generation change, zero broker promotion traffic
4. In-seat CLI refusal PASS
5. #1125 command recognized PASS with caveat (installer seeding not independently verified; clean-host check queued)

Merge gate — one fix required on this branch (F1)

W-P1c dropped the #1123 slow-host probe-timeout bump: lease-activation-probe.js ships timeout: 2000 (was 15000). fomo-lin cold node-spawn of mosaic __lease-capability measures ~2.4 s, so gated-seat launch fails spuriously on modest hardware ("activation capability unreadable / version skew"). Python activation_version_gate.py PROBE_TIMEOUT_SECONDS = 2.0 has the same ceiling. Plan: restore the 15000 ms JS timeout + align the Python ceiling on this branch, scooby re-verifies a clean seat launch (no probe-command override), then merge. While the fix is in, commands/mosaic-promote.md should also drop the echo-era wording ("follow the ... injected receipt confirmation instruction exactly") for mechanical framing — that text sends seat models into refusal loops.

Tracked, not gating

  • #1135 — revoke -> re-promote generation drift (frozen MOSAIC_RUNTIME_GENERATION env vs live generation file), root-caused, likely pre-existing; fast-follow.
  • #1125 installer-seed verification on a clean host (scooby).
## A6 live-E2E verdict (scooby, real gated seat on fomo-lin, 2026-08-09): CORE THESIS VALIDATED Full 5-step gate against a fresh real `mosaic claude` seat, real daemon (unmodified daemon.py), real sockets, at head `1a871524`. | Step | Result | |---|---| | Baseline mutator DENY (UNVERIFIED) | PASS | | 1. `mosaic promote` -> VERIFIED, Write allowed | PASS — seat model actively REFUSED `/mosaic-promote` and promotion still completed synchronously; the #1126 failure mode (model refusal blocks promotion) is eliminated | | 2. Revoke -> re-promote | PARTIAL — revoke + post-revoke DENY pass; re-promote blocked (#1135, fast-follow) | | 3. Injected `/mosaic-promote` with no operator token | PASS — fully inert: NOT_AUTHORIZED breadcrumb, zero generation change, zero broker promotion traffic | | 4. In-seat CLI refusal | PASS | | 5. #1125 command recognized | PASS with caveat (installer seeding not independently verified; clean-host check queued) | ### Merge gate — one fix required on this branch (F1) W-P1c dropped the #1123 slow-host probe-timeout bump: `lease-activation-probe.js` ships `timeout: 2000` (was 15000). fomo-lin cold node-spawn of `mosaic __lease-capability` measures ~2.4 s, so gated-seat launch fails spuriously on modest hardware ("activation capability unreadable / version skew"). Python `activation_version_gate.py` `PROBE_TIMEOUT_SECONDS = 2.0` has the same ceiling. **Plan: restore the 15000 ms JS timeout + align the Python ceiling on this branch, scooby re-verifies a clean seat launch (no probe-command override), then merge.** While the fix is in, `commands/mosaic-promote.md` should also drop the echo-era wording ("follow the ... injected receipt confirmation instruction exactly") for mechanical framing — that text sends seat models into refusal loops. ### Tracked, not gating - #1135 — revoke -> re-promote generation drift (frozen `MOSAIC_RUNTIME_GENERATION` env vs live generation file), root-caused, likely pre-existing; fast-follow. - #1125 installer-seed verification on a clean host (scooby).
Author
Collaborator

Correction narrowing the merge gate (scooby, clean-host verification):

  • F3 is already fixed on this branch — the refusal-loop wording seen in the A6 E2E was a stale pre-install file on the test host (dated Aug 8, never overwritten because the A6 install was surgical). commands/mosaic-promote.md at head 1a871524 (blob b80c0924) already carries the mechanical framing. No wording change needed in the F1 commit.
  • #1125 installer-seed verified on a clean host: the seeding block in mosaic-link-runtime-assets (unconditional copy loop, independent of the settings-guard) reproduced on a scratch HOME with no .claude/commands/ — seeded file is byte-identical to the source blob.

The merge gate is therefore F1 only: restore the #1123 probe timeout (lease-activation-probe.js 2000 → 15000) + align the Python PROBE_TIMEOUT_SECONDS ceiling, then a clean-launch re-verify on fomo-lin.

**Correction narrowing the merge gate (scooby, clean-host verification):** - **F3 is already fixed on this branch** — the refusal-loop wording seen in the A6 E2E was a stale pre-install file on the test host (dated Aug 8, never overwritten because the A6 install was surgical). `commands/mosaic-promote.md` at head `1a871524` (blob `b80c0924`) already carries the mechanical framing. No wording change needed in the F1 commit. - **#1125 installer-seed verified on a clean host**: the seeding block in `mosaic-link-runtime-assets` (unconditional copy loop, independent of the settings-guard) reproduced on a scratch HOME with no `.claude/commands/` — seeded file is byte-identical to the source blob. **The merge gate is therefore F1 only**: restore the #1123 probe timeout (`lease-activation-probe.js` 2000 → 15000) + align the Python `PROBE_TIMEOUT_SECONDS` ceiling, then a clean-launch re-verify on fomo-lin.
Some required checks failed
ci/woodpecker/pr/ci Pipeline failed
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/lease-promotion-and-harness-isolation:feat/lease-promotion-and-harness-isolation
git checkout feat/lease-promotion-and-harness-isolation
Sign in to join this conversation.