Commit Graph
841 Commits
Author SHA1 Message Date
Jason Woltje a3d9bd890c lease probe: fix the same 2s budget on the activation half
The enforcement half was fixed in 9efd903c. The activation half
(`defaultCapabilityProbe`) had the identical hardcoded 2000 ms budget
against the identical CLI boot, so half the defect was still shipping.

Measured cost of the exact call this makes: 1.04-1.11 s on an idle
developer host against `node -e 0` at 0.055 s, and 3.0-3.7 s on a 4-core
VM / 3.55-3.61 s on web1 when reached through the `mosaic` shim. The
budget was below the real cost on two production hosts and inside the
noise band on a third.

Because the probe is fail-closed, an expiry is indistinguishable from
"this build has no activation capability", so it surfaced as a
framework/CLI version-skew error that no upgrade could satisfy.

This was not theoretical: install-ordering-guard.spec.ts failed
intermittently in the full suite (3819 ms) while passing alone (2199 ms)
— the budget expiring under parallel load. That failure is gone.

- named constant + env override, mirroring the enforcement half
- override rejects non-finite/non-positive values rather than unbounding
  the probe, so a bad value cannot hang a launch
- a test asserts the two halves stay numerically equal, so the tighter
  one can never silently become the real budget again

Verified: build rc=0; new tests red against the old constant (2 fail),
green after; full vitest down to the 4 pre-existing
mutator-gate.acceptance failures that are also red on origin/main.
2026-08-14 23:49:15 -05:00
Jason Woltje 9efd903c16 lease gate: stop denying every seat launch on a 2s probe budget
The activation-capability probe spawns the whole Node CLI rather than
exec'ing a binary. Measured 3.0-3.7s on an idle 4-core VM and 3.55-3.61s
on web1, against `node -e 0` at 0.05s. The budget was 2.0s, so the probe
timed out on every call on both hosts.

The gate is fail-closed, and an expiry is indistinguishable from "no
capability", so every fleet seat launch was denied with a version-skew
message telling the operator to "upgrade both as one unit" -- advice that
cannot fix a timeout. This is why web1 shows roster seats with no live
sessions.

Raise the budget to 20s, well clear of the measured range, and add
MOSAIC_LEASE_VERSION_PROBE_TIMEOUT_SECONDS for slower hosts. Unusable
override values fall back to the default rather than removing the bound.

Also fix _resolve_probe_command ignoring the environ it is handed:
shutil.which was called without path=, so it read the ambient PATH. That
made test_returns_none_when_mosaic_is_not_resolvable_on_path pass only
because the 2.0s budget expired first -- right answer, wrong reason, and
it masked the timeout defect. The suite's runtime drops from 2.0s to
0.002s, which is that accidental timeout leaving.

Verified: 18/18 version_coupling_unittest (new tests red against the old
gate: 2 failures + 1 error), tsc build clean, test-start-agent-session.sh
and test-fleet-units.sh rc=0. invariant_r_unittest fails identically with
and without this change (pinned pi 0.84.1 vs installed 0.84.2).
2026-08-14 23:41:15 -05:00
Jason Woltje 47c5476430 merge origin/main into feat/wf5-securestorage
Brings MOSAIC_GIT_IDENTITY (per-seat git authorship) onto the delivery
branch, which had none of it. The branch carried W-F7's FLEET_SEAT seam in
the same launcher file; the two auto-merged cleanly.

Three single-hunk conflicts resolved:
- pr-merge.sh: kept branch policy allowing main OR next (next is the
  release stream).
- test-ci-queue-wait-tristate.sh: kept the branch's 4 added merge-readiness
  assertions; merge base and main both had zero, so nothing of main's is
  reverted.
- package.json test:framework-shell: union of both enumerations, 50 entries,
  no test dropped from either side. Picks up main's test-start-agent-session.sh
  and test-fleet-units.sh, which are the guards for the identity key.
2026-08-14 23:15:56 -05:00
terra bf6b245f3c fleet: move directories off managed paths instead of refusing forever
Launch will not delete a real directory sitting where it expects a managed
link -- an auth/<harness>/primary that someone logged into by hand, or a
plugin directory a seat acquired before the central store existed. That
refusal is right and it is also a dead end: the operator gets a composition
error and no way forward.

`mosaic fleet adopt` is the way forward. Bare, it lists every such directory
and the command that resolves it. With a verb, it moves one where it belongs.

Nothing here deletes. A promotion is a rename; an occupied destination is a
refusal, not a merge; a cross-device rename is reported rather than retried as
copy-then-delete, because a copy-then-delete is a delete.

Store adoption stops at the move and does not install the link. The seat's
.mosaic-managed-links.json belongs to launch, and a link written behind it
fails the next composition as an unrecorded symlink -- one refusal traded for
another. The next launch installs and records it when the profile lists the
entry; whether a seat gets a plugin stays `mosaic fleet plugin`'s decision.

W-F3 of docs/plans/2026-08-14_fleet-seats-on-web1.md.
2026-08-14 19:38:41 -05:00
terraandClaude Opus 5 478e925041 fleet: give one host several accounts per harness, and peg each seat to one
`mosaic auth enroll | assign | list | default` (W-F5). Until now a host had one
account per harness, so an author seat and a reviewer seat were the same
principal wearing two names, and a review carried out under that arrangement is
self-review. Bundles under ~/.mosaic/auth/<harness>/<bundle>/ are what a seat's
profile.json points at, so two seats on one host can hold genuinely different
accounts.

Enroll does not reimplement any harness's login. It creates the bundle
directory owner-only, points the harness's own home at it by environment, runs
the harness, and then checks what landed: credential present, permissions
tightened, and the account recorded. Claude is reached through
CLAUDE_SECURESTORAGE_CONFIG_DIR rather than a symlink because it writes by
rename(2), which replaces a symlink instead of following it. --no-login prints
the environment for an operator who would rather run the login themselves.

The check worth naming is identity: enroll reads the account back out of what
the harness wrote and refuses quietly to accept a bundle named for one account
that holds another. That mistake is otherwise silent -- an operator enrolling
the reviewer bundle logs in out of habit as the author, both seats collapse to
one principal, and nothing else in the system notices.

Assign re-parses a seat's profile before rewriting its bundle, so an already
broken profile is reported here rather than re-serialized into something that
looks repaired and still fails at launch. An unenrolled bundle is assigned but
said out loud, because the seat will refuse to launch until the account exists.

registerAuthCommand now returns its Command so these local verbs can hang off
it. They never talk to the gateway and work on a host where it is down.

41 tests. Each of the load-bearing checks was mutation-tested: nine mutations,
each killing exactly the one test that covers it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WYgWocp36goy8hj2ui6ps1
2026-08-14 19:23:19 -05:00
terraandClaude Opus 5 309a99a600 fleet: fix four defects that made no seat launchable on a clean install
Found by rehearsing the full install on a greenfield Debian 13 VM
(mosaic-sbx-dev) rather than on a host that already had a working Mosaic
tree. Each one is invisible on a developer machine and fatal on a new host.

1. Required system settings layer. The framework ships runtime/<harness>/
   for claude, codex, opencode and pi but a settings.json only for claude,
   so requiring the file made every pi, codex and opencode seat refuse to
   compose. The system layer is now optional; what must exist is the
   harness runtime directory, which is the thing that actually proves the
   framework is installed and carries that harness.

2. Required mcpServers in canonical Claude settings. The shipped
   settings.json has no such key, so `fleet agent new` refused to scaffold
   any Claude seat. Absent now means the same as empty. A present but
   wrong-typed value is still an error.

3. Never-enrolled hosts were told their auth directory "must be a real,
   non-symlink directory", which reads as a tampering report when the real
   situation is that nobody has logged in yet. Absent and wrong-shaped are
   now separate messages, and the absent one names `mosaic auth enroll`.

4. A fleet seat whose host had no system SOUL.md reached checkSoul(),
   which spawns the interactive `mosaic wizard` with inherited stdio. On a
   detached tmux seat that parks the pane on a menu with nobody at it: the
   session is live, the systemd unit reports fine, and no agent ever
   starts. A seat's identity is its own SOUL.md, written by `fleet agent
   new`, so the fleet path checks that and fails loudly instead.

Each fix has a regression test verified red against the unfixed source.
The launch.spec.ts seat fixtures gained a SOUL.md they always should have
had -- without it those tests were satisfied by whatever SOUL.md the
developer's real ~/.config/mosaic happened to contain.

Full suite before and after: the same 5 pre-existing failures in
mutator-gate.acceptance.spec.ts and install-ordering-guard.spec.ts,
1585 -> 1591 passing. typecheck and eslint clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WYgWocp36goy8hj2ui6ps1
2026-08-14 19:03:30 -05:00
terra c1a42cdb81 fleet: start a roster pane through its seat when one is scaffolded
The roster lane and the harness-homes lane did not touch. start-agent-session.sh
ran `mosaic yolo "$RUNTIME"` with HOME set to the operator's home, so every fleet
seat on a host shared the operator's harness home and, for Claude, the operator's
own ~/.claude credentials. Nothing in framework/ called `mosaic fleet launch` at
all, which meant ~/.mosaic was a directory nothing read.

The pane now runs `mosaic fleet launch "$AGENT_NAME"` when a scaffolded seat
exists at $PANE_HOME/.mosaic/fleet/agents/<name>/profile.json, and the historical
command otherwise. Detection uses $PANE_HOME/.mosaic rather than MOSAIC_DATA_HOME
because the pane environment is cleared with env -i; the composition resolves the
same root from HOME, so the two cannot disagree.

Additive by construction: a host with no scaffolded seats launches exactly as
before, so this can land ahead of any seat being enrolled.

- fleet launch gains --dangerous, threaded to launchFleetRuntime. Without it a
  seat launched from the roster would drop the permissions footing `mosaic yolo`
  gave it and prompt at a pane with nobody at it. The roster launcher asks for it
  explicitly so it stays visible in the process table instead of becoming a
  profile default.
- A caller's --model replaces the profile's instead of being appended after it.
  The roster carries a model per seat and is the surface operators edit; emitting
  both flags would leave the choice to each harness's argument parser.
- Claude workdir trust is written into the seat's .claude.json when the pane will
  run in a seat home. It previously always went to the operator's ~/.claude.json,
  which would leave the seat prompting on its first turn.

Covers Jason's scope amendment for web1: without this seam, "multiple
authentication accounts and agent pegging to auth" cannot be demonstrated on a
roster-managed seat.
2026-08-14 18:35:11 -05:00
terra a12eeb4786 fleet: share Claude credentials by directory env, not a seat symlink
Claude Code saves credentials by writing a sibling temp file and rename()-ing
it over the target. rename(2) replaces a symlink rather than following it, so
the managed link W-F1/W-F2 planted at <seat>/.claude/.credentials.json is
destroyed by the first token refresh and the seat silently forks its
credentials. The in-place fallback arm opens with O_NOFOLLOW and would refuse
the link anyway. Evidence, quoting the 2.1.232 binary:
docs/reports/harness/claude-credential-write-path-2026-08-14.md (jarvis-brain).

CLAUDE_SECURESTORAGE_CONFIG_DIR resolves the credential directory
independently of CLAUDE_CONFIG_DIR, so the temp file and the rename both land
inside the bundle. That is the property the design wanted -- share the
credential, never the transcripts -- with no symlink and no privileges.

- new fleet/credential-sharing.ts owns the harness -> credential-file and
  harness -> credential-directory-variable maps, so scaffold and launch cannot
  disagree about the mechanism. It also removes the duplicate credential-file
  name table the two already carried.
- launch composes CLAUDE_SECURESTORAGE_CONFIG_DIR from the resolved bundle
  directory and plans no credential link for Claude. The value is always the
  absolute bundle path: Claude reads an empty value as ~/.claude, which is the
  operator's own account.
- scaffold stops emitting the credential symlink and its manifest entry for
  Claude, and tolerates one left by an earlier scaffold rather than reporting
  it as a foreign file or rewriting it.
- FIRST_AUTH_REFUSAL still fires when a real file occupies the seat path.
- Harnesses absent from the map (pi, codex, opencode) keep managed links; the
  containment specs now exercise them on pi.

Answers promotion gate #1 negatively for the frozen mechanism and positively
for the replacement. E3.3 (two seats refreshing one bundle at once) is still
open.
2026-08-14 18:20:54 -05:00
terra 326a1a58b5 fix(fleet): harden managed launch composition
AMD1213-C: repair stale array consumer, fail closed on foreign link provenance, validate manifests before mutation, and exercise the fleet MCP preflight call path.
2026-08-13 15:37:32 -05:00
terra 2755f86f7b fix(fleet): seed seat MCP preflight config
AMD1213-B5: derive Claude seat MCP configuration from the active installed runtime base and inspect the isolated seat during fleet launch.
2026-08-13 14:38:25 -05:00
terra fe2cf19461 fix(fleet): preserve managed link provenance
AMD1213-B3: record Mosaic-owned links and refuse foreign or retargeted symlink mutations. Out-of-scope review follow-up: settings output/snapshot apply-time TOCTOU remains reported, not patched.
2026-08-13 14:38:25 -05:00
terra 4fde3f622d fix(fleet): contain credential trust roots
AMD1213-B4: reject symlinked auth ancestry and group/world-readable credential artifacts before composition can write.
2026-08-13 14:38:25 -05:00
terra 9de9ffa56b fix(lease): restore uniform settings array replacement
AMD1213-B1: preserve the gated Claude hook composition explicitly in the lease overlay while restoring last-layer-wins arrays and null tombstones.
2026-08-13 14:38:19 -05:00
Jason Woltje cb960237d3 test(lease): assert promotion wiring against composed base+overlay template
ci/woodpecker/pr/ci Pipeline was successful
The lease-overlay split (a42d5e2e) moved the promotion hooks out of the
base Claude settings template; the wiring test still read the base alone
and failed on the absent UserPromptSubmit event, stopping the whole
test:framework-shell chain. The test now composes base + lease overlay
the way a launched seat does (hook event arrays concatenate, base
first) and asserts the same wiring contract against that view.

Reported-by: goals (clean-head probe on 5e154310)
2026-08-13 12:17:10 -05:00
Jason WoltjeandClaude Fable 5 5e15431027 fix(fleet): tolerate harness metadata files in the managed install root
ci/woodpecker/pr/ci Pipeline was canceled
Claude Code writes installed_plugins.json and other metadata files into
the seat's plugins directory during a session, so refusing every real
entry made composition fail on each seat's second launch. Only a real
directory is an unmanaged entry the pruner would orphan; plain files are
harness state and pass through untouched. Found by the in-box hour-gate
relaunch of the probe seat.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
2026-08-13 11:59:31 -05:00
Jason WoltjeandClaude Fable 5 c16256d48c fix(fleet): compose system settings from the installed flattened home layout
The installed ~/.config/mosaic home flattens the repo's
packages/mosaic/framework/ prefix: the real file is
<home>/runtime/<harness>/settings.json, exactly as launch.ts already
resolves it everywhere. The fleet launch composition leaked the repo
layout (framework/runtime/...) into the system layer path, so a real
installed home failed with COMPOSITION_FAILED while the temp-fixture
specs (which mirrored the same wrong prefix) stayed green. Found by the
in-box hour-gate dry-run against the installed mos-dev-stage home.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
2026-08-13 11:52:00 -05:00
Jason WoltjeandClaude Fable 5 92e790ae9d fix(fleet): additive hook-event merge and gated-composition acceptance reads
Integration adjudication (fred, W-F1): the general arrays-replace merge rule
conflicts with the gap-7 base/overlay split — base and lease overlay share
the PreToolUse and Stop events, so replace semantics would silently drop the
base QA hooks from every gated seat. Ruling: hook event arrays directly
under the top-level hooks key concatenate (base first); all other arrays
keep replace semantics; null tombstones still delete an event.

- mutator-gate acceptance now asserts lease wiring against the COMPOSED
  gated settings (base + lease-overlay via the launcher's own merge),
  matching the post-split contract.
- fleet subcommand canary gains the intended new 'agent' surface from T3.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
2026-08-13 11:44:58 -05:00
Jason WoltjeandClaude Fable 5 0fdcfa0ff4 fix(fleet): unify user data-home seam and add actionable unscaffolded-agent error
Integration reconciliation of T2/T3 seams on feat/wf-fleet-mvp:
- fleet launch now resolves the user root through defaultFleetDataHome()
  (MOSAIC_DATA_HOME), the same seam fleet agent new uses, instead of a
  divergent MOSAIC_USER_HOME variable.
- Launching an unscaffolded name raises AGENT_NOT_SCAFFOLDED with the
  actionable message pointing at 'mosaic fleet agent new <name>' (acceptance
  carried over from the T3 card after the roster-v2 reconciliation moved it
  onto the launch path).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
2026-08-13 11:39:44 -05:00
Jason Woltje 4e2f9888a0 Merge branch 'feat/wf-fleet-t2-launch' into feat/wf-fleet-mvp 2026-08-13 11:36:32 -05:00
Jason Woltje 9a92bb64ff Merge branch 'feat/wf-fleet-t3-scaffold' into feat/wf-fleet-mvp 2026-08-13 11:36:32 -05:00
Jason Woltje fe26b37e81 Merge branch 'feat/wf-fleet-t1-base' into feat/wf-fleet-mvp 2026-08-13 11:36:32 -05:00
Jason Woltje 378c227cbb feat(fleet): compose and launch profile-backed seats 2026-08-13 11:30:53 -05:00
Jason Woltje 4522adaa5e feat(fleet): scaffold user-owned agent homes 2026-08-13 11:25:25 -05:00
Jason Woltje a42d5e2ee5 feat(mosaic): split Claude lease overlay from base 2026-08-13 11:21:51 -05:00
coder2andMos 7102ccb93e docs(tools): index pull request edit wrapper (#1200)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: coder2 <[email protected]>
2026-08-13 14:50:38 +00:00
Mos afdaa6d0e6 framework: make tool discoverability, workspace placement and model tiering mechanical (#1174)
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline was successful
2026-08-13 14:21:22 +00:00
coder3andMos 41749bbd33 fix(framework): detect installed tool drift (#1194) (#1195)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: coder3 <[email protected]>
2026-08-13 10:43:11 +00:00
Mos 120af4e193 feat(git-tools): add pull request edit wrapper (#1080) (#1173)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
2026-08-13 06:28:01 +00:00
shaggyandmos-dt-0 216cd72226 refactor(chat): route browser chat through one runtime (P3 Slice-Zero Task 5) (#1172)
ci/woodpecker/push/publish Pipeline failed
Co-authored-by: shaggy <[email protected]>
2026-08-12 20:11:12 +00:00
mos-dt-0 6a8ce66702 Merge pull request 'feat(lease): verified lease-remediation stack (rebased onto next) — promotion trigger + promote CLI + carve-out + TTL' (#1109) from feat/lease-promotion-and-harness-isolation into next
ci/woodpecker/push/publish Pipeline failed
2026-08-12 03:07:32 +00:00
jason.woltjeandMos 9cd9409089 P3 Slice Zero, Task 4 — replace Web free-text selection with the structured harness catalog (#1170)
ci/woodpecker/push/publish Pipeline failed
Co-authored-by: Jason Woltje <[email protected]>
2026-08-12 02:50:18 +00:00
Jason WoltjeandClaude Fable 5 13c70a7a10 test(mutator-gate): keep launch ledger out of the shipped framework tree
ci/woodpecker/pr/ci Pipeline was successful
runRuntimeLaunchEntry set MOSAIC_HOME to the shipped framework root, so
launch-runtime.py appended its launch ledger to
framework/fleet/run/sessions/events.ndjson — polluting the tree that
manifest.spec.ts walks and failing its completeness check in CI.

Point MOSAIC_HOME at the per-entry temp root instead. Nothing in the
launch chain resolves tools via MOSAIC_HOME (entry scripts resolve via
SCRIPT_DIR); the ledger is its only consumer here.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
2026-08-11 20:51:03 -05:00
dd6357e670 test(skill): install linker asserts harness-home link topology
The two install-linker-compatibility tests still asserted the pre-isolation
behavior (mosaic skill links planted in $HOME/.claude/skills). This branch
deliberately moved the link farm into the mosaic-owned harness homes
($MOSAIC_HOME/.claude/skills) and demoted the base-install dirs to
cleanup-only legacy targets, so the tests now assert the new topology:
the skill links appear under the harness home, foreign links in the legacy
dir are preserved, and no new mosaic link is planted in the base install.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
2026-08-11 20:51:03 -05:00
Jason Woltje 709a23d08c feat(mosaic): mechanically authorize lease promotion 2026-08-11 20:51:03 -05:00
Jason WoltjeandClaude Opus 4.8 239a2a93f1 test(lease): #1124 regression uses node pane command (real field topology per scooby)
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
2026-08-11 20:51:03 -05:00
Jason WoltjeandClaude Opus 4.8 ea1f058022 fix(lease): resolve lease session id from the claude child, not the tmux pane pid (#1124)
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
2026-08-11 20:51:03 -05:00
Jason Woltje 1fde450ff1 test(lease): align mutator carve-out acceptance 2026-08-11 20:51:03 -05:00
Jason Woltje c136baa052 fix(mosaic): bound promotion transport delivery 2026-08-11 20:51:03 -05:00
Jason Woltje 4f7f6b3281 feat(mosaic): add correlated lease promotion CLI 2026-08-11 20:51:03 -05:00
Jason Woltje 77edb0dea2 feat(lease): add single-turn Claude promotion trigger 2026-08-11 20:51:03 -05:00
Jason WoltjeandClaude Fable 5 3676180ae8 fix(lease): raise lease TTL 300s -> 3600s
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
2026-08-11 20:51:03 -05:00
Jason Woltje 0e938b66ed fix(lease): ignore benign observer idle replies 2026-08-11 20:51:03 -05:00
Jason Woltje f0fef26eb7 fix(lease): constrain read-only tool carve-outs 2026-08-11 20:51:03 -05:00
Jason Woltje c9bccd4aae test(lease): assert pi carve-out capability 2026-08-11 20:51:03 -05:00
Jason Woltje 8ef2e5b91d test(lease): distinguish pi probe timeouts 2026-08-11 20:51:03 -05:00
Jason Woltje 4cab6c09fe test(lease): enforce read-only tool invariant 2026-08-11 20:51:03 -05:00
Jason Woltje 239fc6d03c docs: measure pi tool registry 2026-08-11 20:51:03 -05:00
Jason WoltjeandClaude Fable 5 d085182dc1 test: close W-0R review findings — assert the omission notice, skip chmod simulations under root
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
2026-08-11 20:51:03 -05:00
Jason WoltjeandClaude Opus 5 e949fa3767 fix(lease): refuse an incomplete law binding instead of silently shrinking it
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
2026-08-11 20:51:03 -05:00
Jason Woltje f1761c91be Revert "feat(pi): wire lazy lease promotion into the mutator gate"
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.
2026-08-11 20:51:03 -05:00