test(#829): regress ungated runtime launch entries

This commit is contained in:
ms-lead-reviewer
2026-07-17 23:29:05 -05:00
parent 6a3505dfbc
commit 7f3418fa8c
3 changed files with 162 additions and 0 deletions

View File

@@ -82,3 +82,10 @@ Carried authority chain also verified/read for the locked T-B gate contract: SPE
- Fresh full repository suite: `43/43` Turbo tasks; `@mosaicstack/mosaic` `72/72` files and `1,381/1,381` tests.
- Fresh root gates: typecheck `42/42`; lint `23/23`; format and `git diff --check` GREEN.
- PR #837 remains open and unmerged. Terra CODE and Opus SECREV must both rerun from zero on the exact remediated head before coordinator-owned merge authorization.
## Remediation round 3 — terra CODE comment 18099 + binding upgrade
- Locked-good surfaces: Claudex gating and B2 per-executable coverage are verified; do not regress them. Broker state-transition/lock ordering remains untouched.
- Mechanical repository sweep found direct executing Claude entries in PRDY init, PRDY update, QA remediation, and `@mosaicstack/coord` task launch. It also found a direct Claude command rendered into the QA report template and documentation examples. Existing Mosaic CLI Claude/Pi/Claudex, orchestrator session-run, and fleet starts already reach the gated boundary.
- Elevated hard requirements: ship a permanent suite/CI guard that scans production source and fails on any direct Claude/Pi launch; route every executing entry through one common gated wrapper; add real-broker RED/GREEN tests for PRDY init/update and QA; preserve each environment and denial behavior; independently measure all new executable coverage at ≥85%.
- Round-3 plan: first commit RED behavioral and scanner-contract tests; then add one framework `launch-runtime.sh` choke-point over `launch-runtime.py`, make Mosaic CLI and shell launchers use it, make coord route through `mosaic`, and wire the permanent guard into package tests. Update all discovered operator-facing direct-launch examples so the scanner inventory remains complete.

View File

@@ -27,6 +27,9 @@ const gatePath = join(frameworkRoot, 'tools/lease-broker/mutator-gate.py');
const launcherPath = join(frameworkRoot, 'tools/lease-broker/launch-runtime.py');
const claudeSettingsPath = join(frameworkRoot, 'runtime/claude/settings.json');
const piExtensionPath = join(frameworkRoot, 'runtime/pi/mosaic-extension.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');
const children: ChildProcess[] = [];
const temporaryRoots: string[] = [];
@@ -77,6 +80,82 @@ async function startBroker(): Promise<BrokerPaths> {
return { socket };
}
interface RuntimeLaunchEntry {
name: string;
script: string;
prepare(root: string): Promise<string[]>;
}
const runtimeLaunchEntries: RuntimeLaunchEntry[] = [
{
name: 'prdy-init',
script: prdyInitPath,
prepare: async (root) => ['--project', root, '--name', 'Gate Test'],
},
{
name: 'prdy-update',
script: prdyUpdatePath,
prepare: async (root) => {
await mkdir(join(root, 'docs'), { recursive: true });
await writeFile(join(root, 'docs/PRD.md'), '# Existing PRD\n');
return ['--project', root];
},
},
{
name: 'qa-remediation',
script: remediationHandlerPath,
prepare: async (root) => {
const pending = join(root, 'reports/pending');
await mkdir(pending, { recursive: true });
const report = join(pending, 'gate_remediation_needed.md');
await writeFile(report, '# remediation\n');
return [report];
},
},
];
async function runRuntimeLaunchEntry(entry: RuntimeLaunchEntry, socket: string) {
const root = await mkdtemp(join(tmpdir(), `mosaic-${entry.name}-gate-`));
temporaryRoots.push(root);
const binDir = join(root, 'bin');
await mkdir(binDir, { recursive: true });
const fakeClaude = join(binDir, 'claude');
await writeFile(
fakeClaude,
`#!/usr/bin/env python3
import json
import os
import subprocess
session_id = os.environ.get("MOSAIC_LEASE_SESSION_ID", "")
denied = subprocess.run(
["python3", ${JSON.stringify(gatePath)}, "--runtime", "claude"],
input=json.dumps({"tool_name": "Bash"}) + "\\n",
text=True,
capture_output=True,
env=os.environ,
).returncode == 2
print("RUNTIME_PROBE=" + json.dumps({"session_id": session_id, "denied": denied}))
raise SystemExit(0 if len(session_id) == 64 and denied else 1)
`,
{ mode: 0o700 },
);
await chmod(fakeClaude, 0o700);
const args = await entry.prepare(root);
return spawnSync('bash', [entry.script, ...args], {
cwd: root,
encoding: 'utf8',
env: {
...process.env,
PATH: `${binDir}:${process.env.PATH ?? ''}`,
MOSAIC_HOME: frameworkRoot,
MOSAIC_PRDY_RUNTIME: 'claude',
MOSAIC_LEASE_BROKER_SOCKET: socket,
MOSAIC_RUNTIME_GENERATION: '1',
},
});
}
async function register(socket: string, runtime_generation = 1): Promise<string> {
const reply = await request(socket, { action: 'register_anchor', runtime_generation });
expect(reply.ok).toBe(true);
@@ -386,6 +465,26 @@ describe('whole mutator-class lease gate', () => {
expect(unavailable.stdout).not.toContain('EXECUTED');
});
test.each(runtimeLaunchEntries)(
'$name registers before launch, denies an unverified mutator, and fails closed without broker',
async (entry) => {
const missingSocket = join(tmpdir(), `missing-${entry.name}-${process.pid}.sock`);
const unavailable = await runRuntimeLaunchEntry(entry, missingSocket);
expect(unavailable.status).not.toBe(0);
expect(`${unavailable.stdout}${unavailable.stderr}`).not.toContain('RUNTIME_PROBE=');
const { socket } = await startBroker();
const launched = await runRuntimeLaunchEntry(entry, socket);
expect(launched.status, launched.stderr).toBe(0);
const match = /RUNTIME_PROBE=(\{[^\n]+\})/.exec(`${launched.stdout}${launched.stderr}`);
expect(match).not.toBeNull();
expect(JSON.parse(match![1]!)).toEqual({
session_id: expect.stringMatching(/^[a-f0-9]{64}$/),
denied: true,
});
},
);
test.each([
{ command: 'mosaic claudex', yolo: false },
{ command: 'mosaic yolo claudex', yolo: true },

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Contract tests for the permanent consequential-runtime launch guard."""
from __future__ import annotations
import importlib.util
import unittest
from pathlib import Path
MOSAIC_ROOT = Path(__file__).parents[2]
REPO_ROOT = Path(__file__).parents[4]
GUARD_PATH = MOSAIC_ROOT / "framework/tools/lease-broker/check-runtime-launches.py"
SPEC = importlib.util.spec_from_file_location("runtime_launch_guard", GUARD_PATH)
if SPEC is None or SPEC.loader is None:
raise RuntimeError("unable to load runtime launch guard")
GUARD = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(GUARD)
class RuntimeLaunchGuardTest(unittest.TestCase):
def test_detects_direct_shell_and_process_api_launches(self) -> None:
cases = {
"shell-exec.sh": 'exec claude --dangerously-skip-permissions "prompt"\n',
"shell-print.sh": 'claude -p "prompt" | tee report.log\n',
"typescript.ts": "spawn('pi', ['--print', prompt]);\n",
"python.py": "subprocess.run(['claude', '-p', prompt])\n",
"dynamic.ts": "return [runtime, '-p', prompt];\n",
}
for filename, source in cases.items():
with self.subTest(filename=filename):
violations = GUARD.scan_text(Path(filename), source)
self.assertNotEqual(violations, [], source)
def test_allows_only_explicit_gated_boundaries(self) -> None:
cases = {
"shell-helper.sh": 'exec "$GATED_RUNTIME" claude -- claude -p "prompt"\n',
"mosaic.sh": 'exec mosaic yolo "$runtime" "prompt"\n',
"launch.ts": "execLeaseGatedRuntime('claude', args);\n",
"coord.ts": "return ['mosaic', runtime, '-p', prompt];\n",
}
for filename, source in cases.items():
with self.subTest(filename=filename):
self.assertEqual(GUARD.scan_text(Path(filename), source), [])
def test_repository_has_no_ungated_consequential_runtime_launch(self) -> None:
violations = GUARD.scan_repository(REPO_ROOT)
self.assertEqual(
violations,
[],
"\n".join(GUARD.format_violation(violation) for violation in violations),
)
if __name__ == "__main__":
unittest.main()