Compare commits

..
Author SHA1 Message Date
ms-lead-reviewerandClaude Opus 4.8 c5a2bcc516 fix(mosaic): DI-inject CLI-entry resolver so tests never touch real dist/ (#869 C1 review fix R3)
ci/woodpecker/pr/ci Pipeline was successful
Round-2 review found the positive-path test's writeFileSync() staged a
stub cli.js/index.js at the package's REAL resolved dist/ path, and
afterEach only removed files that had NOT pre-existed — never restoring
original CONTENT for files that had. On a host with a real pre-built
dist/cli.js (ordinary `pnpm build && pnpm test`, and CI: this package's
turbo.json overrides the `test` task to depend on `build`, so CI always
builds a real dist/cli.js before running vitest), the test would silently
overwrite the real ~26KB compiled CLI with an 87-byte stub and still
report PASS. Confirmed as the exact root cause of CI1972's red `test`
step: src/cli-smoke.spec.ts execs the real dist/cli.js in the same vitest
process/run, so the clobber surfaced there.

Fix (dependency injection, not snapshot/restore):
- defaultCapabilityProbe() now takes an injectable CapabilityProbeDeps
  ({ resolveCliEntry }), defaulting to the real defaultResolveCliEntry()
  in production — no change to the real-artifact-read guarantee.
- defaultResolveCliEntry() itself now takes an injectable ModuleResolver
  (defaults to the real require.resolve), so its resolution CHOICE (bare
  "@mosaicstack/mosaic" specifier vs the buggy "./package.json" subpath)
  can be tested in complete isolation from real package/build state.
- The positive-path test now stages its stub cli.js in an mkdtempSync()
  scratch directory and injects resolveCliEntry to point there — it never
  calls the default resolver, so it structurally cannot touch the real
  package's dist/. It also asserts the real dist/ path's existence is
  unchanged by the test.
- The "returns null when unbuilt" test now injects a resolver pointing at
  a path that cannot exist, instead of relying on this checkout happening
  to be unbuilt (deterministic regardless of ambient host build state).

Verified:
- Reintroduced the R1 bug in defaultResolveCliEntry() and confirmed the
  new resolver-choice test fails red against it; restored the fix (byte-
  identical diff against the pre-revert file) and confirmed green.
- Built a real dist/cli.js (~26KB) via `turbo run build`, ran the targeted
  specs against it, then ran the FULL `turbo run test --filter=@mosaicstack/mosaic`
  CI-parity path (which builds dist/ itself per this package's turbo.json
  override before vitest runs, exactly matching Woodpecker's `test` step):
  77/77 test files, 1451/1451 tests passed, including cli-smoke.spec.ts
  (22/22) and lease-activation-probe.spec.ts (15/15) in the SAME run.
  sha256 of dist/cli.js before and after that full run: identical
  (e61d8de7a2223b6578a2b733edd927707830e011f44b9d20901194c23c4a5272,
  26317 bytes) — the real build artifact is untouched byte-for-byte.
- python3 -m unittest runtime_tools_unittest: 25/25 pass, including both
  C-REGRESS-locked fail-closed cases.
- typecheck/lint/format:check all pass via turbo --filter=@mosaicstack/mosaic.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 14:24:14 -05:00
ms-lead-reviewerandClaude Opus 4.8 ac44a1aea7 fix(mosaic): resolve lease-activation CLI via exported "." entry, not "./package.json" (#869 C1 review fix)
ci/woodpecker/pr/ci Pipeline failed
Independent review of PR #870 found defaultCapabilityProbe() resolved the
installed CLI via req.resolve('@mosaicstack/mosaic/package.json') — a
subpath NOT present in package.json's `exports` map. Node throws
ERR_PACKAGE_PATH_NOT_EXPORTED for that on every real install, which the
catch-all silently turned into an always-null probe: leaseEnforcementActivatable()
could never return true anywhere, defeating the card's purpose.

Fix: resolve via the already-exported "." entry instead
(require.resolve('@mosaicstack/mosaic') -> dist/index.js), then take
cli.js as its sibling in the same built dist/ directory — matching
package.json's bin.mosaic mapping. No change to the exports map itself
(that would mask a separate, out-of-scope pre-existing issue in
resolveTool()/launch.ts per review guidance).

Adds a positive-path test that stages a realistic fake dist/index.js +
dist/cli.js at the package's real resolved location and asserts
defaultCapabilityProbe() returns the actual {name, version} capability
object with zero mocking of the resolver. Verified red against the prior
(reverted-and-restored) buggy resolution before landing the fix, and green
after — the previous only-unmocked test asserted null for the wrong
reason (masking this bug) and is left in place alongside the new one.

Re-verified: launch.spec.ts (30), fail-closed-regression.spec.ts (2), and
runtime_tools_unittest.py (25, including both C-REGRESS-locked fail-closed
cases) all still green. typecheck/lint/format:check pass via
`turbo run ... --filter=@mosaicstack/mosaic`.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 13:57:17 -05:00
ms-lead-reviewerandClaude Opus 4.8 c64a04e7dd chore(orchestrator): fix pre-existing Prettier drift in README.md
ci/woodpecker/pr/ci Pipeline was successful
Whitespace-only markdown table/spacing fix. Pre-existing on origin/main
(introduced by #868), unrelated to #869 C1 — fixed only because the repo's
pre-push hook runs a full-repo \`pnpm format:check\` and was blocking this
branch's push. No functional change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 13:38:20 -05:00
ms-lead-reviewerandClaude Opus 4.8 763cecc381 feat(mosaic): leaseEnforcementActivatable() capability probe (#869 C1)
Adds a real activation-capability probe so a downstream install-ordering
guard (C2, out of scope here) can refuse to wire lease-broker enforcement
(PreToolUse/Stop hooks) on a host that cannot actually activate it — the
root cause of #828's version-skew brick, where the published CLI tarball
lagged the enforcement reseed and every tool call denied with
GATE_UNAVAILABLE.

- LEASE_ACTIVATION_CAPABILITY {name, version}: versioned signal owned by
  the activation half (execLeaseGatedRuntime), independent of npm semver.
- Hidden `mosaic __lease-capability` subcommand: prints that capability
  from the actually-resolvable built CLI artifact, not source-tree
  presence.
- leaseEnforcementActivatable(): pure predicate, true iff a compatible
  capability is advertised AND the broker supervisor (launcher + daemon.py
  artifacts, socket path) resolves. Detection only — never starts the
  broker. Both inputs are injectable for testing.
- C-REGRESS: added a vitest spec that runs the two test-locked fail-closed
  gate cases in runtime_tools_unittest.py directly, proving the gate's
  fail-closed-on-absent-identity behavior is unchanged by this card.

Part of #869 (Point-1 C1).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 13:36:29 -05:00
10 changed files with 535 additions and 307 deletions
-8
View File
@@ -4,14 +4,6 @@ pnpm-lock.yaml
**/node_modules
**/drizzle
**/.next
# Python build/test artifacts — same category as node_modules/dist/.next above.
# Prettier must never scan generated trees; without these a local venv poisons
# `pnpm format:check` with thousands of third-party files.
**/venv
**/__pycache__
**/.mypy_cache
**/.pytest_cache
**/htmlcov
.claude/
docs/tess/TASKS.md
docs/scratchpads/
-76
View File
@@ -1,76 +0,0 @@
# mos-remediation — LIVE BOARD (keep < 8 KB)
**Phase:** PLANNING — adversarial task decomposition IN FLIGHT.
**Updated:** 2026-07-31 (mos-remediation orchestrator; seat active on `mosaic-fleet`).
## Head
- Mission charter + 15 decisions + 4-build plan: PERSISTED (`docs/remediation/MISSION.md`).
- HOLD lifted for this workstream (Jason 2026-07-31). Nothing implemented yet — planning first.
- Orchestrator seat `mos-remediation` is LIVE and owns the mission. Residency attestation: PASS.
- TASK-0 (env + push mission package) IN PROGRESS — checkout deps repaired, gates being verified honestly.
- TASK-1 (adversarial decomp) DISPATCHED for real, both planners on GUARANTEED-CLEAN context.
## In-flight
| Task | Owner | State |
| ---------------------------------------------------- | --------------- | --------------------------------------------- |
| TASK-0 env repair + push `remediation/mission-setup` | mos-remediation | IN PROGRESS — deps installed; gates verifying |
| Decomp (robustness side) → `DECOMP-OPUS.md` | planner-opus | WORKING (clean session) |
| Decomp (pragmatic side) → `DECOMP-SOL.md` | planner-sol | WORKING (reset to 0.0% ctx before dispatch) |
| Reconcile both decomps → `docs/remediation/TASKS.md` | mos-remediation | BLOCKED on the two decomps |
## Fleet seats
- mos-remediation — project orchestrator (Claude, /src/mosaic-stack, socket `mosaic-fleet`) — ACTIVE
- planner-opus — adversarial planner (robustness), Opus 5, socket `default` — WORKING
- planner-sol — adversarial planner (pragmatic), gpt-5.6-sol, socket `default` — WORKING
- rev-974 — mosaicstack reviewer identity (id 16, write:repository) — idle, on call
- Mos (mos-claude) — lead coordinator, socket `default` — relay path to Jason
## Gate status
- Delivery gates active: author≠reviewer, diff-blind pre-registered checks, CI-green, merged-PR completion.
- Freeze: LIFTED for this workstream only.
- Git identity: `MOSAIC_GIT_IDENTITY=mos-dt-0` INTERIM. Mos ruled gate-16 HOLDS (author≠reviewer is what
gate-16 protects; rev-974 reviews, mos-dt-0 never self-reviews). Dedicated identity TRACKED, Mos provisions.
- Remote control: native `/remote-control` NOT wired in this runtime. Path is **Mos-relay**
(Jason ↔ mos-claude via Discord ↔ mos-remediation via agent-send). Not a blocker.
## Sequencing (from MISSION.md)
1. Spine + choke-point service (MACP wiring @ mosaic_orchestrator.py::run_single_task) + PG/Redis
2. Rotation daemon (finish Mission Control Plane, reuse packages/coord)
3. Comms service (envelope→service→PG/Redis→adapters)
4. Hygiene + conformance harness
Cross-cutting retirements: flat-file tracking, 3 MACP islands, silent MOSAIC BYPASS.
## Dogfood evidence captured this session (live failure classes, not hypotheticals)
- **D-1 / P-ACTIVATION + hygiene — committed `.npmrc` hard-pins `store-dir=/root/.local/share/pnpm/store`.**
Correct for the CI container (runs as root), fatal for EVERY non-root local checkout: `EACCES` on
`/root/.local/share/pnpm/store/v10/server/server.json`. A committed config that only works on one
runtime is exactly the activation-skew class. Fix candidate: make store-dir env-overridable, not hardcoded.
- **D-2 / hygiene — husky `prepare` fails `EPERM` copying into root-owned `.husky/_/`.** Repo working
tree has root-owned dirs (`.husky/`, repo root) under a non-root agent. Worked around with the
intended `HUSKY=0` escape hatch (does NOT disable the existing pre-commit/pre-push hooks).
- **D-3 / P-FLEET-001 — the seats running this mission are UNMANAGED.** `mos-remediation`, `rev-974`,
`planner-opus`, `planner-sol` appear in NO roster (`~/.config/mosaic/fleet/roster.yaml`, `agents/`).
Planners run on socket `default`; the roster declares `mosaic-fleet`. This is the exact
"one roster-owned socket/host + quarantine unmanaged + stale GC" failure P-FLEET-001 indicts —
observed on the remediation mission's own fleet. Prerequisite for INBOX identity-addressing.
- **D-4 / P-LIFECYCLE + hygiene — a dispatched agent silently IGNORED an in-message context reset.**
planner-sol was at 64.3%/372k; the brief asked it to reset first; it began work on dirty context anyway.
Only an out-of-band `/new` driven by the orchestrator guaranteed clean state. Confirms the postmortem
thesis: **instructions are not enforcement.** Reset must be a mechanical pre-dispatch step, not a request.
## Decisions log
- 2026-07-31 — Mission set up by Mos post-postmortem (15/15 decided). Dogfood posture active.
- 2026-07-31 — Mos: stale `.mosaic/orchestrator/mission.json` is RESIDUE of the disabled Python
orchestrator rail that this plan RETIRES. Do NOT invest in it; do NOT build on that rail. The 0/0
milestone banner is cosmetic. (Supersedes any plan to repair it.)
- 2026-07-31 — Mos: planners must be dispatched with GUARANTEED clean context, not requested-clean.
Prior default-socket planner sessions predate this mission; dirty context is the indicted hygiene.
- 2026-07-31 — mos-remediation: worker briefs forbid all git ops and restrict each worker to a single
named output file, so two planners can share one checkout without a branch race (M2-era incident doctrine).
-44
View File
@@ -1,44 +0,0 @@
# mos-remediation — Orchestrator Kickstart / Compaction-Survival Resume
**You are `mos-remediation`, the project orchestrator for the Mosaic Stack remediation, launched in `/src/mosaic-stack`.**
This file is your fail-closed resume procedure. Read it on EVERY fresh/cleared session and on the FIRST turn
after any compaction. This mission's whole point is that manual compaction-survival is fragile — so follow this
mechanically until Build 3 (rotation) makes it automatic.
## On resume (do in order, before any orchestration action)
1. `cd /src/mosaic-stack` and confirm you are on the remediation working branch.
2. Read `docs/remediation/MISSION.md` — the charter (goal, 4 builds, 15 decisions, sequencing, directives).
3. Read `docs/remediation/BOARD.md` — the LIVE state: current phase, in-flight tasks, fleet seat assignments,
gate status. This is your single source of in-flight truth (kept small).
4. Read the discussion checkpoint for full rationale if needed:
`../jarvis-brain/docs/scratchpads/postmortem/REMEDIATION-DISCUSSION-STATE.md` (or the jarvis-brain repo path).
5. **Residency attestation (fail-closed):** restate from the reloaded files — (a) the goal in one line, (b) the
current build/phase, (c) the BOARD head (in-flight tasks + who owns them). If you cannot, HALT and re-read.
Do NOT act on memory alone; a compaction may have dropped context silently.
## Standing invariants (never violate)
- **North star:** deterministic-right-answer → code/gate; LLM only for judgment.
- **Delivery gates:** author≠reviewer; PRE-REGISTERED diff-blind checks committed before reading the diff;
CI terminal-green; completion = merged PR + closed issue. rev-974 = the mosaicstack reviewer identity.
- **Dogfooding:** every fix validated against its live seed case (MISSION.md lists them).
- **Tracking → DB** (hard cutover); do NOT re-invest in flat-file tracking. jarvis-brain PDA is off-limits.
- **Git identity:** export `MOSAIC_GIT_IDENTITY=<your-seat>` so wrappers author correctly and survive respawn.
## After every significant event
Overwrite stale lines in `BOARD.md`, keep it < 8 KB, commit + push. The board IS your checkpoint until the
DB-backed rotation daemon (Build 3) exists. Persist typed state (phase, tasks, owners, gates) — never the transcript.
## Fleet
- Adversarial planners: `planner-opus` (robustness), `planner-sol` (pragmatic) — dispatch for task decomposition; reconcile their oppositional decomps.
- Coders/reviewers: dispatch per roster + delivery gates. Comms: `~/.config/mosaic/tools/tmux/agent-send.sh`
(`-L <socket> -s <dst> -S <yourhost>:<yourseat> --class <class>`); always pass `-S`.
- Lead coordinator: Mos (`mos-claude`). Escalate only on the Constitution's escalation triggers.
## Remote control
On first startup, activate remote control for this session (`/remote-control`) so Jason can reach/drive you while
away. If the command is unavailable in this runtime, report it to Mos and continue — it is not a blocker.
-98
View File
@@ -1,98 +0,0 @@
# MACP wiring investigation
**Scope:** `/src/mosaic-stack` inspected at HEAD `b79336a8c11e2a4646a47ff8d295a226e0c71404`; read-only. Existing dirty/untracked state was not touched.
## Verdict
**(c) STRANDED.** `packages/macp` is exported, unit-tested, and registered as a CLI command group, but no production dispatch/execution code invokes its credential resolver, gate runner, or event emitter.
A separate MACP-named OpenClaw/orchestrator rail exists, but it redefines task/result types and gate/event logic instead of importing `@mosaicstack/macp`; direct `mosaic yolo|claude|codex|opencode|pi` also bypasses it.
## 1. Production call sites vs tests
### Production references to `@mosaicstack/macp`
| Surface | Evidence | Actual use |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Unified CLI | `packages/mosaic/src/cli.ts:8,385` | Imports and registers `registerMacpCommand`; no task/gate/event execution. |
| Forge | `packages/forge/src/types.ts:1,17,68,79` | **Type-only** imports of `GateEntry` and `TaskResult`. Pipeline calls an injected abstract executor at `packages/forge/src/pipeline-runner.ts:189-190,299-300`, not MACP. |
| Mosaic package metadata | `packages/mosaic/package.json:36`; `packages/mosaic/src/runtime/update-checker.ts:172` | Dependency/update inventory only. |
| Agent | No match under production `packages/agent/src/**` | No MACP import/call. |
| Coord | No match under production `packages/coord/src/**`; dependency list is only `@mosaicstack/types` at `packages/coord/package.json:25-27` | No MACP import/call. |
| Plugins | No `@mosaicstack/macp` import under `plugins/**` | No package use; the MACP-named plugin is an independent implementation (below). |
**Repository-wide production call-site search result:** excluding `packages/macp/**`, tests, worktrees, and build output, there are **zero** calls to `runGate`, `runGates`, `emitEvent`, `appendEvent`, or `resolveCredentials`.
### `packages/macp` implementation is internally connected only
- Public exports: `packages/macp/src/index.ts:1-48` exports Task/GateEntry/MACPEvent/TaskResult, credential resolution, `runGate(s)`, risk-floor, and event emission.
- Gate runner calls its own event emitter: `packages/macp/src/gate-runner.ts:187-236`.
- Event persistence implementation appends NDJSON to a caller-supplied path: `packages/macp/src/event-emitter.ts:11-27`.
- There is **no exported programmatic `submit` implementation** in `packages/macp/src/index.ts:1-48`; only the CLI placeholder named `submit`.
### Test-only invocations
- Gate runner: `packages/macp/__tests__/gate-runner.test.ts:96-242` invokes `runGate/runGates`.
- Event ledger: `packages/macp/__tests__/event-emitter.test.ts:46-133` invokes `appendEvent/emitEvent` against temporary `events.ndjson` files.
- Credential resolver: `packages/macp/__tests__/credential-resolver.test.ts` exercises resolver behavior.
- CLI tests only verify command registration: `packages/macp/src/cli.spec.ts:37-73`; `packages/mosaic/src/cli-smoke.spec.ts:8` imports registration.
## 2. Gate on the live dispatch path
### Direct Mosaic runtime launch bypasses MACP
- Runtime commands dispatch directly to harness launch: `packages/mosaic/src/commands/launch.ts:730-801`.
- Claude/Pi go through the lease broker, then spawn the runtime: `packages/mosaic/src/commands/launch.ts:817-843`.
- Commander wiring sends `mosaic yolo <runtime>` and direct runtime commands to `launchRuntime`: `packages/mosaic/src/commands/launch.ts:1102-1157,1165-1167`.
- None of those ranges imports/calls `@mosaicstack/macp`, `runGates`, or `emitEvent`.
**Result:** a direct `mosaic yolo`, `mosaic claude/codex/opencode/pi`, or underlying exec does not create a typed MACP Task, run the package gate-runner, or append a package MACPEvent.
### Coord bypasses MACP
- Coord reads/updates `docs/TASKS.md`: `packages/coord/src/runner.ts:6,306-386`; parser/writer is `packages/coord/src/tasks-file.ts:326-377`.
- Coord launches a child process directly: `packages/coord/src/runner.ts:397-427`.
- Mission state is its own `.mosaic/orchestrator/mission.json`/`next-task.json`: `packages/coord/src/mission.ts:8-12`; `packages/coord/src/runner.ts:15-16,355-384`.
**Result:** Coord task execution has no MACP Task validation, package gate runner, or event append.
### Forge bypasses MACP execution
- Forge defines its own `ForgeTask` and abstract `TaskExecutor`: `packages/forge/src/types.ts:48-80`.
- The production CLI injects a **stub executor** that immediately reports completion with empty gates: `packages/forge/src/cli.ts:13-31,167,185`.
**Result:** even `mosaic forge run` does not execute MACP gates or persist MACP events.
### Separate MACP-named rail is not `packages/macp`
- OpenClaw plugin registers an ACP backend named `macp`: `plugins/macp/src/index.ts:1-18,72-102`.
- It locally redefines `OrchestratorTask`, `TaskResult`, and gate-result shapes instead of importing package types: `plugins/macp/src/macp-runtime.ts:43-77`.
- It appends directly to `.mosaic/orchestrator/tasks.json`, triggers an external controller, and polls `results/<task>.json`: `plugins/macp/src/macp-runtime.ts:290-329,437-483`.
- The controller independently implements `append_event`, `emit_event`, shell execution, gate execution, and results: `packages/mosaic/framework/tools/orchestrator-matrix/controller/mosaic_orchestrator.py:29-91,126-276`.
- Its gate loop runs raw string gates after worker success: `mosaic_orchestrator.py:213-235`; it does not support the package's structured `GateEntry`/AI-review behavior.
- Current checkout disables this controller: `.mosaic/orchestrator/config.json:2` (`"enabled": false`).
- Plugin references `tools/macp/dispatcher/pi_runner.ts` at `plugins/macp/src/macp-runtime.ts:85-91`, but `tools/macp/` does not exist in this checkout.
**Result:** there is a parallel, optionally enabled MACP-shaped rail, not package integration. It cannot make `packages/macp` the enforced path.
## 3. Event ledger status
- Package persistence exists only as a library primitive: `packages/macp/src/event-emitter.ts:11-27` appends JSON lines to an arbitrary `eventsPath`.
- Package event emission is reached only from package `runGates`: `packages/macp/src/gate-runner.ts:204-236`.
- No production caller invokes package `runGates/emitEvent/appendEvent`; therefore no runtime destination path is configured for the package ledger.
- Test-only ledgers use temp paths: `packages/macp/__tests__/event-emitter.test.ts:35-133`; gate tests use temp `events.ndjson`: `packages/macp/__tests__/gate-runner.test.ts:171-242`.
- The separate Python controller writes `.mosaic/orchestrator/events.ndjson`: `mosaic_orchestrator.py:129-133,159-161,219-235`; the Mosaic Framework plugin only **reads** that file for context at `plugins/mosaic-framework/src/index.ts:279-316,430-438`.
- In this checkout, `.mosaic/orchestrator/events.ndjson` is absent and the controller is disabled (`.mosaic/orchestrator/config.json:2`).
**Conclusion:** `MACPEvent` from `packages/macp` is defined/tested but not emitted or persisted by live production call sites. The similarly shaped Python ledger is a duplicate island.
## 4. Coord link
- `packages/coord` has no `@mosaicstack/macp` dependency/import: `packages/coord/package.json:25-27`; no matches in `packages/coord/src/**`.
- Coord's task model is Markdown `docs/TASKS.md` plus mission/session JSON: `packages/coord/src/tasks-file.ts:1-10,257-377`; `packages/coord/src/mission.ts:8-12`; `packages/coord/src/runner.ts:306-427`.
- It does not consume `.mosaic/orchestrator/events.ndjson`, MACP Task, MACPEvent, GateEntry, or TaskResult.
**Conclusion:** Coord and `packages/macp` are disconnected islands.
## Shortest wiring gap
**Single integration point:** replace the duplicated execution/gate/event block in `mosaic_orchestrator.py::run_single_task` (`:126-276`) with one production Node `TaskExecutor` backed by `@mosaicstack/macp` (typed Task validation + `resolveCredentials` + `runGates` + `emitEvent`), and make Coord/Forge/OpenClaw submit through that executor. This queue/controller choke point is where `yolo|acp|exec` worker outcomes can be gated and journaled before completion is recorded.
-79
View File
@@ -1,79 +0,0 @@
# Mosaic Stack Remediation — Mission Charter
**Owner:** project orchestrator `mos-remediation` (Claude, launched in `/src/mosaic-stack`).
**Origin:** 2026-07-16..31 fleet lifecycle postmortem. **Status:** PLANNING (task decomposition).
**HOLD lifted** for this workstream by Jason, 2026-07-31 — "begin full mosaic fleet operation on this."
## Goal
Convert the 15 accepted postmortem remediation proposals into a working, **dogfooded** implementation.
**North star:** anything with a deterministic right answer moves OUT of the LLM into a deterministic
gate/program; the LLM handles only genuine judgment.
## Decision record (authoritative, immutable)
- **15/15 proposals decided: 13 accept, 2 modify (P-AUTHORITY-001, P-INBOX-001), 0 reject.**
- Site + `annotations.json`: `jarvis-brain/docs/postmortem-spec/site/` (committed, origin/main).
- Discussion checkpoint (rich rationale per proposal): `jarvis-brain/docs/scratchpads/postmortem/REMEDIATION-DISCUSSION-STATE.md`.
- Postmortem report: mosaicstack/stack PR #107 (merged 88f4ee04).
- MACP wiring scout (verdict c=STRANDED): `/tmp/macp-wiring-investigation.md` (copy into this dir — see TODO).
## The plan — 15 proposals collapse to 4 builds + hygiene
| Build | Absorbs | What it is |
| ------------------------------------------------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **1. One choke-point service** (mechanical enforcer) | MISSION, STATE, AUDIT, WRAPPER, QUEUE | Deterministic program every task/data mutation flows through. **Wire the stranded `@mosaicstack/macp`** in at `mosaic_orchestrator.py::run_single_task` — typed tasks, gate-runner, event ledger, credential binding, tri-state write outcomes. |
| **2. One durable spine + hot path** | (storage under everything) | **PG system-of-record + Redis hot queue** (transactional-outbox). Mission/tasks/state-claims/audit-ledger/comms-inbox all land here. |
| **3. Rotation lifecycle** (finish the Mission Control Plane) | LIFECYCLE, CONTRACT, GUIDE, RECOVERY | Coordinator daemon: contract-hash binding, compaction-detected → rotate-not-compact, checkpoint→fresh-session→rehydrate, broker-independent recovery. Deterministic, not an LLM. Reuse `packages/coord`; existing PRD at `docs/mission-control/`. |
| **4. Comms service** | AUTHORITY, INBOX (+ versioning roadmap) | Envelope (comms/v1) → sole-path service → PG/Redis → pluggable adapters (tmux→Matrix/Discord/Slack/Telegram). Version the protocol, not participants. |
| **+ Hygiene & proof** | FLEET, WORKFLOW, CONFORMANCE | One roster-owned socket/host + stale GC; allowlist auto-sync; the conformance harness that fault-injects the failure classes and proves builds 14 hold. |
## The finding that sets the cost
**Built-but-unwired disease.** `@mosaicstack/macp` is stranded (nothing calls it); `packages/coord` primitives
exist; the Mission Control PRD exists; PG + Redis already run in-stack. Three duplicate MACP islands, an
orphaned context loader, a fail-open bypass. **Work = wire + consolidate + retire, NOT greenfield. "Finish, don't re-spec."**
## Sequencing (skeleton — adversarial decomposition refines this)
1. **Spine + choke-point service** (builds 1+2) — foundation; unlocks MISSION/STATE/AUDIT/WRAPPER/QUEUE at one integration point.
2. **Rotation daemon** (build 3) on that spine — the drift fix proper.
3. **Comms service** (build 4) — envelope → service → PG/Redis → adapters; retire direct-tmux.
4. **Hygiene + conformance** (build 5) — fleet convergence, allowlist sync, dogfood harness.
- **Cross-cutting retirements:** flat-file orchestration tracking (hard cutover to DB), the 3 duplicate MACP islands, the silent `MOSAIC BYPASS`.
## Standing directives (Jason, 2026-07-31)
- **Dogfooding:** validate EACH fix against the live fleet failure that motivated it. Seed acceptance tests:
Pi brick (RECOVERY), scout-bounce (INBOX/FLEET), gate-6 inert + #1019 recursion (QUEUE), identity drift
(WRAPPER), auto-sync sweep (WORKFLOW), #1018 stale-consumed (INBOX). The fleet is its own test bed.
- **Orchestration tracking → DB**, hard cutover ("rip off the bandaid"), NO flat-file interim. jarvis-brain
PDA flat-files untouched. Current flat-file tracking runs as-is/unhardened until DB tracking is real, then one clean replace.
## The 15 decisions (one-line; full rationale in the checkpoint)
1. **P-ACTIVATION-001** accept — transactional CLI+hooks+broker+version release; block launch on skew, fail-SAFE.
2. **P-AUTHORITY-001** MODIFY — structured authenticated inbox; envelope carries comms-PROTOCOL version; version the protocol not participants; N-version window.
3. **P-LIFECYCLE-001** accept — rotation not recursive compaction; pre-empt at token threshold; enforcer = deterministic coordinator; = finish Mission Control Plane.
4. **P-MISSION-001** accept — bind lanes to mission+task ledger; convention exists, ENFORCEMENT is the gap; mission+tasks → DB spine (hard cutover).
5. **P-QUEUE-001** accept — repair queue transport + exit-asserting non-null-case tests (gate-6 was INERT fleet-wide; #1019 fix recursed the same bug).
6. **P-STATE-001** accept — typed claims (source/confidence/TTL) not prose blob; MACP typed record; integrity fail-closed HMAC; don't fork a 4th island.
7. **P-AUDIT-001** accept — MACPEvent lifecycle ledger; EXTEND enum to lifecycle events; runtime-neutral (executor-emitted); retire duplicate Python ledger.
8. **P-WRAPPER-001** accept — identity derives from seat name + survives respawn; tri-state write outcomes MANDATORY; name safe target metadata.
9. **P-CONTRACT-001** accept — bind session to contract hash; re-anchor on policy-change OR compaction-detected; stale generation loses authority MECHANICALLY.
10. **P-INBOX-001** MODIFY — sole-path comms SERVICE; PG durable SoR + Redis hot queue (outbox, reconciliation sweeper); pluggable adapters; protocol-first, PG-first-then-Redis.
11. **P-RECOVERY-001** accept — broker-independent bootstrap recovery; honest capability labeling; break-glass LOUD+AUDITED+TEMPORARY not silent permanent bypass.
12. **P-GUIDE-001** accept — delete `/compact and continue` from orchestrator path (keep for ephemeral); removal = substitution (wire rotation trigger).
13. **P-FLEET-001** accept — one roster-owned socket/host; quarantine unmanaged; stale-session GC; prerequisite for INBOX identity-addressing.
14. **P-WORKFLOW-001** accept — auto-sync ALLOWLIST not denylist; worktree/lease isolation for agent docs/source; DB-tracking obviates the flat-file-sweep criterion.
15. **P-CONFORMANCE-001** accept — fleet lifecycle harness on REAL runtime artifacts + fault injection; the 100-rotations-lossless bar is a test; target the DB substrate.
## Fleet operating model
- **Project orchestrator** `mos-remediation` (this seat) owns the mission; coordinates under Mos (lead).
- **Adversarial task decomposition:** `planner-opus` (robustness) + `planner-sol` (pragmatic) each decompose
the plan independently; orchestrator reconciles into `TASKS.md`/DB tasks. Oppositional by design.
- **Delivery gates (non-negotiable):** author≠reviewer, PRE-REGISTERED diff-blind acceptance checks committed
before reading the diff, CI terminal-green, completion = merged PR + closed issue. rev-974 = mosaicstack reviewer.
- **Compaction survival:** see `KICKSTART.md` in this dir — the resume procedure. Persist typed state, not transcript.
+5
View File
@@ -21,6 +21,7 @@ import { registerRestoreCommand } from './commands/restore.js';
import { registerSkillCommand } from './commands/skill.js';
// prdy is registered via launch.ts
import { registerLaunchCommands } from './commands/launch.js';
import { registerLeaseCapabilityProbe } from './commands/lease-activation-probe.js';
import { registerAuthCommand } from './commands/auth.js';
import { registerFederationCommand } from './commands/federation.js';
import { registerGatewayCommand } from './commands/gateway.js';
@@ -78,6 +79,10 @@ Command Groups:
registerLaunchCommands(program);
// ─── lease activation capability probe (hidden; #869 Point-1 C1) ────────
registerLeaseCapabilityProbe(program);
// ─── login ──────────────────────────────────────────────────────────────
program
+14 -2
View File
@@ -806,7 +806,14 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
process.exit(0); // Unreachable but satisfies never
}
function defaultLeaseBrokerSocket(env: NodeJS.ProcessEnv = process.env): string {
/**
* Resolve the lease broker's control socket path. Exported (in addition to
* being used internally by execLeaseGatedRuntime) so the C1 activation probe
* (lease-activation-probe.ts) can perform the same resolution when checking
* whether the broker supervisor is reachable — detection only, this never
* connects to the socket itself.
*/
export function defaultLeaseBrokerSocket(env: NodeJS.ProcessEnv = process.env): string {
if (env['MOSAIC_LEASE_BROKER_SOCKET']) return env['MOSAIC_LEASE_BROKER_SOCKET'];
const runtimeDir = env['XDG_RUNTIME_DIR'];
if (runtimeDir) return join(runtimeDir, 'mosaic-lease', 'broker.sock');
@@ -895,7 +902,12 @@ function delegateToScript(scriptPath: string, args: string[], env?: Record<strin
* bundled in the @mosaicstack/mosaic npm package (always matches the installed
* CLI version) over the deployed copy in ~/.config/mosaic/ (may be stale).
*/
function resolveTool(...segments: string[]): string {
/**
* Exported so the C1 activation probe (lease-activation-probe.ts) can resolve
* the same lease-broker launcher/daemon artifacts execLeaseGatedRuntime()
* uses, for detection-only supervisor presence checks.
*/
export function resolveTool(...segments: string[]): string {
try {
const req = createRequire(import.meta.url);
const mosaicPkg = dirname(req.resolve('@mosaicstack/mosaic/package.json'));
@@ -0,0 +1,243 @@
import { describe, it, expect } from 'vitest';
import { Command } from 'commander';
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import {
LEASE_ACTIVATION_CAPABILITY,
LEASE_CAPABILITY_PROBE_COMMAND,
defaultCapabilityProbe,
defaultResolveCliEntry,
defaultSupervisorProbe,
leaseEnforcementActivatable,
registerLeaseCapabilityProbe,
type LeaseActivationCapability,
type SupervisorProbeResult,
} from './lease-activation-probe.js';
/**
* Red-first tests for issue #869 Point-1 C1 — leaseEnforcementActivatable().
*
* Root cause under test: #828 shipped the lease broker's ENFORCEMENT half
* (hooks) and ACTIVATION half (execLeaseGatedRuntime + a running daemon.py
* broker) on different channels, and they drifted — the published CLI
* tarball lacked the activation half even though it existed in source. The
* predicate here must say NO when either half of activation is unavailable,
* and only YES when both are genuinely present — never based on "does the
* source file exist", but on a real capability signal + real supervisor
* detection.
*/
const compatibleCapability: LeaseActivationCapability = { ...LEASE_ACTIVATION_CAPABILITY };
const presentSupervisor: SupervisorProbeResult = {
supervisorPresent: true,
socketPath: '/run/user/1000/mosaic-lease/broker.sock',
};
describe('leaseEnforcementActivatable', () => {
it('is false when the activation capability is absent (null)', () => {
const result = leaseEnforcementActivatable({
getCapability: () => null,
probeSupervisor: () => presentSupervisor,
});
expect(result).toBe(false);
});
it('is false when the activation capability name does not match', () => {
const result = leaseEnforcementActivatable({
getCapability: () => ({
name: 'some-other-capability',
version: LEASE_ACTIVATION_CAPABILITY.version,
}),
probeSupervisor: () => presentSupervisor,
});
expect(result).toBe(false);
});
it('is false when the activation capability version is incompatible (stale/newer build)', () => {
const result = leaseEnforcementActivatable({
getCapability: () => ({
name: LEASE_ACTIVATION_CAPABILITY.name,
version: LEASE_ACTIVATION_CAPABILITY.version + 1,
}),
probeSupervisor: () => presentSupervisor,
});
expect(result).toBe(false);
});
it('is false when the supervisor artifacts (launcher/daemon) are not present', () => {
const result = leaseEnforcementActivatable({
getCapability: () => compatibleCapability,
probeSupervisor: () => ({
supervisorPresent: false,
socketPath: presentSupervisor.socketPath,
}),
});
expect(result).toBe(false);
});
it('is false when the supervisor socket path is not resolvable', () => {
const result = leaseEnforcementActivatable({
getCapability: () => compatibleCapability,
probeSupervisor: () => ({ supervisorPresent: true, socketPath: null }),
});
expect(result).toBe(false);
});
it('is false when BOTH capability and supervisor are absent', () => {
const result = leaseEnforcementActivatable({
getCapability: () => null,
probeSupervisor: () => ({ supervisorPresent: false, socketPath: null }),
});
expect(result).toBe(false);
});
it('is true when a compatible capability AND a resolvable supervisor are both present', () => {
const result = leaseEnforcementActivatable({
getCapability: () => compatibleCapability,
probeSupervisor: () => presentSupervisor,
});
expect(result).toBe(true);
});
it('uses the real default probes when no deps are injected (does not throw)', () => {
// No live broker / built CLI is guaranteed in a test environment, so this
// only asserts the predicate degrades to a safe boolean rather than
// throwing — the fail-closed behavior itself is covered by the injected
// cases above.
expect(() => leaseEnforcementActivatable()).not.toThrow();
expect(typeof leaseEnforcementActivatable()).toBe('boolean');
});
});
describe('defaultCapabilityProbe', () => {
it('returns null (fail-closed) when no built CLI artifact is resolvable', () => {
// Deterministic regardless of ambient host state (e.g. a host that has
// already run `pnpm build`, which would otherwise make this pass or fail
// depending on whether dist/cli.js happens to exist) — inject a resolver
// pointing at a path that cannot exist, rather than relying on this
// checkout being unbuilt. The probe must report "no capability" rather
// than fabricate one from source-tree presence — this is the exact
// distinction #828's version skew needed: source existing is not the
// same as the published artifact advertising the capability.
const result = defaultCapabilityProbe({
resolveCliEntry: () => '/nonexistent/mosaic-lease-activation-probe-test/cli.js',
});
expect(result).toBeNull();
});
describe('positive path — injected resolver, isolated scratch dir (never the real dist/)', () => {
// A prior version of this test staged the stub cli.js at the package's
// REAL resolved dist/ path and relied on afterEach to clean up "only
// what it created" — which meant a host with a real pre-built
// dist/cli.js (ordinary `pnpm build && pnpm test`) would have its real
// ~26KB compiled CLI silently overwritten by an 87-byte stub, with no
// restoration of the original content. That is exactly the kind of
// build-artifact corruption #869 exists to prevent. This version uses
// dependency injection exclusively: defaultCapabilityProbe() is never
// called with its default resolver here, so it can never touch the real
// package dist/ at all — proven below by asserting that path's
// existence is unchanged by the test.
it('returns the real {name, version} capability from a stub cli.js in a temp dir, and leaves the real dist/ untouched', () => {
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const realDistDir = join(packageRoot, 'dist');
const realDistPreexisted = existsSync(realDistDir);
const scratchDir = mkdtempSync(join(tmpdir(), 'mosaic-lease-capability-probe-'));
try {
const scratchCliPath = join(scratchDir, 'cli.js');
// Minimal stand-in for the built CLI's hidden __lease-capability
// subcommand — prints exactly what registerLeaseCapabilityProbe()
// wires the real `mosaic __lease-capability` command to print.
writeFileSync(
scratchCliPath,
`process.stdout.write(JSON.stringify(${JSON.stringify(LEASE_ACTIVATION_CAPABILITY)}));\n`,
);
const result = defaultCapabilityProbe({ resolveCliEntry: () => scratchCliPath });
expect(result).toEqual(LEASE_ACTIVATION_CAPABILITY);
// The real package dist/ must be byte-for-byte untouched: this test
// never invokes the default resolver, so the path's mere existence
// (created or not) must be unchanged by having run this test.
expect(existsSync(realDistDir)).toBe(realDistPreexisted);
} finally {
rmSync(scratchDir, { recursive: true, force: true });
}
});
});
});
describe('defaultResolveCliEntry', () => {
it('resolves the bare "@mosaicstack/mosaic" specifier (the exported "." entry), never the non-exported "./package.json" subpath', () => {
// Fully isolated from the real filesystem/package state (no dependency
// on whether @mosaicstack/mosaic has been built on this host) via an
// injected fake resolver that mirrors Node's real behavior: the "."
// export resolves fine, but "./package.json" is NOT in package.json's
// `exports` map, so real `require.resolve` throws
// ERR_PACKAGE_PATH_NOT_EXPORTED for it. This is genuinely red-first
// against the reviewer-found bug: the old implementation resolved the
// "./package.json" subpath here, which this fake throws on — the new
// implementation must resolve only the bare specifier.
const requestedSpecifiers: string[] = [];
const fakeResolve = (specifier: string): string => {
requestedSpecifiers.push(specifier);
if (specifier === '@mosaicstack/mosaic') return '/fake/pkg/dist/index.js';
throw new Error(`ERR_PACKAGE_PATH_NOT_EXPORTED: ${specifier}`);
};
const result = defaultResolveCliEntry(fakeResolve);
expect(result).toBe(join('/fake/pkg/dist', 'cli.js'));
expect(requestedSpecifiers).toEqual(['@mosaicstack/mosaic']);
});
});
describe('defaultSupervisorProbe', () => {
it('returns a well-shaped result without starting or connecting to anything', () => {
const result = defaultSupervisorProbe({});
expect(typeof result.supervisorPresent).toBe('boolean');
expect(result.socketPath === null || typeof result.socketPath === 'string').toBe(true);
});
it('resolves a socket path from an explicit MOSAIC_LEASE_BROKER_SOCKET override', () => {
const result = defaultSupervisorProbe({ MOSAIC_LEASE_BROKER_SOCKET: '/tmp/explicit.sock' });
expect(result.socketPath).toBe('/tmp/explicit.sock');
});
});
describe('registerLeaseCapabilityProbe', () => {
it('registers a hidden subcommand named __lease-capability', () => {
const program = new Command();
program.exitOverride();
registerLeaseCapabilityProbe(program);
const registered = program.commands.find((c) => c.name() === LEASE_CAPABILITY_PROBE_COMMAND);
expect(registered).toBeDefined();
// Commander exposes "hidden" only as help-output suppression (no public
// getter) — assert the observable behavior instead of a private field.
expect(program.helpInformation()).not.toContain(LEASE_CAPABILITY_PROBE_COMMAND);
});
it('prints the capability constant as JSON when invoked', () => {
const program = new Command();
program.exitOverride();
registerLeaseCapabilityProbe(program);
let written = '';
const originalWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = ((chunk: string) => {
written += chunk;
return true;
}) as typeof process.stdout.write;
try {
program.parse(['node', 'mosaic', LEASE_CAPABILITY_PROBE_COMMAND]);
} finally {
process.stdout.write = originalWrite;
}
expect(JSON.parse(written)).toEqual(LEASE_ACTIVATION_CAPABILITY);
});
});
@@ -0,0 +1,232 @@
/**
* Lease-enforcement activation probe (issue #869, Point-1 card C1).
*
* Root cause this exists to guard against (#828 version skew): the
* ENFORCEMENT half of the lease broker (PreToolUse/Stop hooks —
* `mutator-gate.py`, `receipt-observer-client.py` — wired via the framework
* reseed) and the ACTIVATION half (`execLeaseGatedRuntime()` in `launch.ts`,
* which chains the runtime through `launch-runtime.py`, injects
* `MOSAIC_LEASE_*`, and requires a running `daemon.py` broker) ship on
* different channels. When the published CLI tarball lags behind an
* enforcement reseed, the gate correctly fails CLOSED on absent identity —
* but every tool call then denies with GATE_UNAVAILABLE. That fail-closed
* behavior is intentional and must not change (see the C-REGRESS note in
* `runtime_tools_unittest.py`); this module exists so a downstream
* install-ordering guard (C2, out of scope here) can refuse to WIRE
* enforcement in the first place on a host that cannot ACTIVATE it.
*
* `leaseEnforcementActivatable()` answers one narrow question: "if
* enforcement were wired right now, could activation actually satisfy it?"
* It is a real capability probe — not a "does the source file exist" check
* — and both of its inputs are injectable so tests can drive every branch
* without a live broker or an installed CLI on PATH.
*/
import { execFileSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import type { Command } from 'commander';
import { defaultLeaseBrokerSocket, resolveTool } from './launch.js';
// ─── Capability signal (owned by the activation half) ──────────────────────
/**
* Versioned identity for the activation contract `execLeaseGatedRuntime()`
* implements. OWNED by the activation half of the lease broker. Bump
* `version` only when the activation contract itself changes (env vars
* injected, chaining behavior, socket protocol, etc.) — deliberately
* independent of the package's npm semver, because #828 happened precisely
* because the npm version was NOT bumped even though the shipped artifact
* fell out of sync. A build that cannot advertise this exact
* `{ name, version }` pair does not implement the contract a caller is
* relying on, whatever its package.json claims.
*/
export interface LeaseActivationCapability {
readonly name: string;
readonly version: number;
}
export const LEASE_ACTIVATION_CAPABILITY: LeaseActivationCapability = {
name: 'lease-runtime-activation',
version: 1,
};
/** Hidden CLI probe subcommand name — wired via {@link registerLeaseCapabilityProbe}. */
export const LEASE_CAPABILITY_PROBE_COMMAND = '__lease-capability';
function capabilityMatches(candidate: LeaseActivationCapability | null): boolean {
return (
candidate !== null &&
candidate.name === LEASE_ACTIVATION_CAPABILITY.name &&
candidate.version === LEASE_ACTIVATION_CAPABILITY.version
);
}
/**
* Register the hidden `__lease-capability` probe subcommand. Prints the
* capability this BUILD advertises as compact JSON to stdout and exits 0.
* Deliberately undocumented (hidden from `--help`): it is an internal signal
* for {@link defaultCapabilityProbe}, not a user-facing command.
*/
export function registerLeaseCapabilityProbe(program: Command): void {
program
.command(LEASE_CAPABILITY_PROBE_COMMAND, { hidden: true })
.description('Internal: print the lease-activation capability this build advertises')
.action(() => {
process.stdout.write(JSON.stringify(LEASE_ACTIVATION_CAPABILITY));
});
}
/** Injectable Node module resolver — matches `require.resolve`'s signature
* narrowly (specifier in, absolute path out, or throws). Defaults to the
* real `createRequire(import.meta.url).resolve`. Injectable so tests can
* exercise WHICH specifier {@link defaultResolveCliEntry} resolves (the
* reviewer-found bug was resolving the wrong one) without depending on
* whether `@mosaicstack/mosaic` has actually been built on the test host —
* and without ever touching the real package's `dist/` to find out. */
export type ModuleResolver = (specifier: string) => string;
/**
* Resolve the CLI's built entrypoint (`dist/cli.js`). Resolves via the
* package's "." export (already present in package.json's `exports` map)
* rather than a "./package.json" subpath — the latter is NOT exported, so
* `require.resolve('@mosaicstack/mosaic/package.json')` throws
* ERR_PACKAGE_PATH_NOT_EXPORTED on every real install. The "." export
* resolves to `dist/index.js`; `cli.js` is its sibling in the same built
* `dist/` directory (see package.json's `bin.mosaic`).
*
* Exported standalone (and injectable via {@link CapabilityProbeDeps}) so
* tests can exercise this resolution logic in isolation, or point
* {@link defaultCapabilityProbe} at a scratch directory instead of ever
* touching the real installed package's `dist/` — a test corrupting a real
* build artifact is exactly the artifact-integrity failure class this card
* exists to prevent (#828).
*/
export function defaultResolveCliEntry(
resolve: ModuleResolver = createRequire(import.meta.url).resolve,
): string {
const mainEntry = resolve('@mosaicstack/mosaic');
return join(dirname(mainEntry), 'cli.js');
}
/** Injectable inputs for {@link defaultCapabilityProbe}. */
export interface CapabilityProbeDeps {
/** Resolve the CLI entrypoint (`cli.js`) to probe. Defaults to
* {@link defaultResolveCliEntry}. Inject to point at an isolated scratch
* location in tests — never at the real package's `dist/`. */
resolveCliEntry?: () => string;
}
/**
* Real capability lookup. Resolves the installed `@mosaicstack/mosaic`
* package's BUILT entrypoint (`dist/cli.js` — the published artifact a user
* actually runs, not this TypeScript source file) and executes its hidden
* `__lease-capability` probe subcommand out-of-process. A build that lacks
* the subcommand, fails to execute, or reports an incompatible
* `{ name, version }` is treated as having NO activation capability.
*
* This is the check that would have caught #828's version skew: the
* source-tree activation half existed, but the published tarball's `dist/`
* did not carry it, so this probe — reading the actually-resolvable built
* artifact rather than trusting source-tree presence — would report null.
*/
export function defaultCapabilityProbe(
deps: CapabilityProbeDeps = {},
): LeaseActivationCapability | null {
try {
const resolveCliEntry = deps.resolveCliEntry ?? defaultResolveCliEntry;
const cliEntry = resolveCliEntry();
if (!existsSync(cliEntry)) return null;
const output = execFileSync(process.execPath, [cliEntry, LEASE_CAPABILITY_PROBE_COMMAND], {
encoding: 'utf-8',
timeout: 2000,
stdio: ['ignore', 'pipe', 'ignore'],
});
const parsed: unknown = JSON.parse(output);
if (
typeof parsed !== 'object' ||
parsed === null ||
typeof (parsed as Record<string, unknown>)['name'] !== 'string' ||
typeof (parsed as Record<string, unknown>)['version'] !== 'number'
) {
return null;
}
const candidate = parsed as { name: string; version: number };
return { name: candidate.name, version: candidate.version };
} catch {
return null;
}
}
// ─── Supervisor / socket resolution (detection only) ───────────────────────
/** Detection-only supervisor/socket probe result. Never starts the broker
* and never connects to the socket — presence and path resolution only. */
export interface SupervisorProbeResult {
/** The lease-broker supervisor artifacts (launcher + daemon) are present. */
readonly supervisorPresent: boolean;
/** Resolved broker socket path, or null if it could not be resolved. */
readonly socketPath: string | null;
}
/**
* Real supervisor/socket resolution: checks that the lease-broker's launcher
* (`launch-runtime.py`) and supervisor (`daemon.py`) artifacts resolve on
* disk via the same tool-resolution `execLeaseGatedRuntime()` uses, and that
* a broker socket path resolves via the same logic as
* `defaultLeaseBrokerSocket()`. Detection only — this never starts the
* daemon and never connects to the socket.
*/
export function defaultSupervisorProbe(
env: NodeJS.ProcessEnv = process.env,
): SupervisorProbeResult {
const launcherPath = resolveTool('lease-broker', 'launch-runtime.py');
const daemonPath = resolveTool('lease-broker', 'daemon.py');
const supervisorPresent = existsSync(launcherPath) && existsSync(daemonPath);
let socketPath: string | null = null;
try {
const resolved = defaultLeaseBrokerSocket(env);
socketPath = resolved.trim().length > 0 ? resolved : null;
} catch {
socketPath = null;
}
return { supervisorPresent, socketPath };
}
// ─── Predicate ───────────────────────────────────────────────────────────
/** Injectable inputs for {@link leaseEnforcementActivatable}, so tests (and
* downstream callers such as the C2 install-ordering guard) can drive every
* branch without a live broker or an installed CLI on PATH. */
export interface ActivationProbeDeps {
getCapability?: () => LeaseActivationCapability | null;
probeSupervisor?: () => SupervisorProbeResult;
}
/**
* True IFF lease enforcement can actually be ACTIVATED on this host:
*
* (a) the resolvable CLI advertises a {@link LeaseActivationCapability}
* compatible with {@link LEASE_ACTIVATION_CAPABILITY}, AND
* (b) the broker supervisor is resolvable — launcher + `daemon.py`
* artifacts present AND a broker socket path resolves.
*
* Pure/testable: both probes default to the real, side-effect-free lookups
* above but can be injected, so this predicate never itself starts a broker
* or performs enforcement — it only reports whether activation *could*
* satisfy enforcement if wired.
*/
export function leaseEnforcementActivatable(deps: ActivationProbeDeps = {}): boolean {
const getCapability = deps.getCapability ?? defaultCapabilityProbe;
const probeSupervisor = deps.probeSupervisor ?? defaultSupervisorProbe;
if (!capabilityMatches(getCapability())) return false;
const supervisor = probeSupervisor();
return supervisor.supervisorPresent && supervisor.socketPath !== null;
}
@@ -0,0 +1,41 @@
import { spawnSync } from 'node:child_process';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
/**
* C-REGRESS (issue #869, Point-1) — proves the fail-closed gate is untouched
* by the C1 activation probe added alongside this test.
*
* `mutator-gate.py`'s fail-closed-on-absent-identity behavior is INTENTIONAL
* and TEST-LOCKED: #869 C1 gates the WIRING decision for enforcement (via
* `leaseEnforcementActivatable()`), it does not — and must not — touch the
* gate's own runtime denial behavior. This spec runs the two test-locked
* cases from `runtime_tools_unittest.py` directly (rather than merely
* re-asserting the same logic in TypeScript) so a regression in the actual
* Python gate is caught here too, not just documented in prose.
*/
const MUTATOR_GATE_DIR = new URL('.', import.meta.url).pathname;
const UNITTEST_FILE = join(MUTATOR_GATE_DIR, 'runtime_tools_unittest.py');
const LOCKED_TEST_CASES = [
'ExecutableEntrypointTest.test_gate_entrypoint_denies_when_identity_environment_is_absent',
'MutatorGateTest.test_environment_generation_and_request_failures_deny',
] as const;
describe('mutator-gate fail-closed behavior (C-REGRESS, unchanged by #869 C1)', () => {
it.each(LOCKED_TEST_CASES)('%s still passes', (testCase) => {
const result = spawnSync('python3', ['-m', 'unittest', `${moduleName()}.${testCase}`, '-v'], {
cwd: MUTATOR_GATE_DIR,
encoding: 'utf-8',
});
expect(result.status, `stderr:\n${result.stderr}`).toBe(0);
});
});
function moduleName(): string {
// runtime_tools_unittest.py, addressed as a bare module name for `python3 -m unittest`.
return UNITTEST_FILE.split('/').pop()!.replace(/\.py$/, '');
}