feat(869-c4): enforcement/activation version-coupling assertion
Some checks failed
ci/woodpecker/pr/ci Pipeline was canceled

Part of #869 (Point-1 C4). Locks the lease broker's ENFORCEMENT half
(launch-runtime.py) and ACTIVATION half (execLeaseGatedRuntime, C1's
LEASE_ACTIVATION_CAPABILITY) as one versioned unit, closing the #828
version-skew gap C1 only made observable.

- New packages/mosaic/framework/tools/lease-broker/activation_version_gate.py:
  EXPECTED_ACTIVATION_CAPABILITY (enforcement-owned, mirrors but is
  independent from C1's LEASE_ACTIVATION_CAPABILITY), a fail-closed
  subprocess probe of the CLI's hidden `mosaic __lease-capability`
  command, and an assertion that FAILS LOUD (VersionCouplingError with
  an actionable #869 remediation message) on mismatch OR absence —
  never a silent pass, never a dead gate.
- launch-runtime.py now runs this assertion first, before any broker
  registration, and denies with a dedicated exit code (65) distinct
  from its existing usage (64) and registration-failure (1) codes.
- Red-first tests: src/mutator-gate/version_coupling_unittest.py
  (module-level match/mismatch/name-mismatch/absent-capability cases,
  and seam-level LAUNCHER.main() cases proving the gate runs before
  broker registration and never silently passes).
- Existing fail-closed-on-absent-identity lock
  (runtime_tools_unittest.py) stays green: matching-capability fakes
  were injected into its pre-existing LAUNCHER.main() calls so the new
  gate runs alongside, not in place of, identity enforcement.
- mutator-gate.acceptance.spec.ts updated to supply a fake CLI-capability
  probe (matching the existing fake-broker/fake-runtime-binary test
  doubles already in that suite) everywhere it drives launch-runtime.py
  as a real subprocess.

launch.ts and lease-activation-probe.ts are untouched (C2/C5 avoidance,
C1 read-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mosaic-coder
2026-07-23 13:44:20 -05:00
parent 4422231bdb
commit eaeb633d24
6 changed files with 543 additions and 1 deletions

View File

@@ -47,6 +47,22 @@ const piLifecyclePath = join(frameworkRoot, 'runtime/pi/lease-lifecycle.ts');
const prdyInitPath = join(frameworkRoot, 'tools/prdy/prdy-init.sh');
const prdyUpdatePath = join(frameworkRoot, 'tools/prdy/prdy-update.sh');
const remediationHandlerPath = join(frameworkRoot, 'tools/qa/remediation-hook-handler.sh');
// C4 (#869 Point-1): launch-runtime.py now asserts, before anything else,
// that the CLI's advertised lease-activation capability (normally read via
// the hidden `mosaic __lease-capability` subcommand) matches what
// enforcement expects — see framework/tools/lease-broker/
// activation_version_gate.py. This suite drives launch-runtime.py directly
// as a subprocess (never through the real `mosaic` CLI), so — exactly like
// the fake broker (daemon.py) and fake `claude` binaries already used
// below — it must supply a fake activation-capability probe rather than
// depend on a real `mosaic` binary being on PATH. `MOSAIC_LEASE_VERSION_PROBE_COMMAND`
// is launch-runtime.py's injection point for that fake; this literal
// {name, version} pair must be kept in sync with
// `EXPECTED_ACTIVATION_CAPABILITY` (activation_version_gate.py) and
// `LEASE_ACTIVATION_CAPABILITY` (lease-activation-probe.ts) — all three
// currently agree on v1.
const leaseCapabilityProbeStub = `python3 -c "import json; print(json.dumps({'name': 'lease-runtime-activation', 'version': 1}))"`;
const children: ChildProcess[] = [];
const temporaryRoots: string[] = [];
@@ -184,6 +200,7 @@ raise SystemExit(0 if len(session_id) == 64 and denied else 1)
MOSAIC_PRDY_RUNTIME: 'claude',
MOSAIC_LEASE_BROKER_SOCKET: socket,
MOSAIC_RUNTIME_GENERATION: '1',
MOSAIC_LEASE_VERSION_PROBE_COMMAND: leaseCapabilityProbeStub,
},
});
}
@@ -743,6 +760,7 @@ describe('whole mutator-class lease gate', () => {
...process.env,
MOSAIC_LEASE_BROKER_SOCKET: socket,
MOSAIC_RUNTIME_GENERATION: '1',
MOSAIC_LEASE_VERSION_PROBE_COMMAND: leaseCapabilityProbeStub,
},
},
);
@@ -761,6 +779,7 @@ describe('whole mutator-class lease gate', () => {
...process.env,
MOSAIC_LEASE_BROKER_SOCKET: join(tmpdir(), 'missing-mosaic-broker.sock'),
MOSAIC_RUNTIME_GENERATION: '1',
MOSAIC_LEASE_VERSION_PROBE_COMMAND: leaseCapabilityProbeStub,
},
},
);
@@ -878,6 +897,7 @@ raise SystemExit(0 if len(session_id) == 64 and hook_present and observers_prese
PATH: `${binDir}:${process.env.PATH ?? ''}`,
MOSAIC_LEASE_BROKER_SOCKET: socket,
MOSAIC_RUNTIME_GENERATION: '1',
MOSAIC_LEASE_VERSION_PROBE_COMMAND: leaseCapabilityProbeStub,
},
proxyGate: () =>
Promise.resolve({