test(#829): regress ungated runtime launch entries
This commit is contained in:
@@ -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 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -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 launcherPath = join(frameworkRoot, 'tools/lease-broker/launch-runtime.py');
|
||||||
const claudeSettingsPath = join(frameworkRoot, 'runtime/claude/settings.json');
|
const claudeSettingsPath = join(frameworkRoot, 'runtime/claude/settings.json');
|
||||||
const piExtensionPath = join(frameworkRoot, 'runtime/pi/mosaic-extension.ts');
|
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 children: ChildProcess[] = [];
|
||||||
const temporaryRoots: string[] = [];
|
const temporaryRoots: string[] = [];
|
||||||
|
|
||||||
@@ -77,6 +80,82 @@ async function startBroker(): Promise<BrokerPaths> {
|
|||||||
return { socket };
|
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> {
|
async function register(socket: string, runtime_generation = 1): Promise<string> {
|
||||||
const reply = await request(socket, { action: 'register_anchor', runtime_generation });
|
const reply = await request(socket, { action: 'register_anchor', runtime_generation });
|
||||||
expect(reply.ok).toBe(true);
|
expect(reply.ok).toBe(true);
|
||||||
@@ -386,6 +465,26 @@ describe('whole mutator-class lease gate', () => {
|
|||||||
expect(unavailable.stdout).not.toContain('EXECUTED');
|
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([
|
test.each([
|
||||||
{ command: 'mosaic claudex', yolo: false },
|
{ command: 'mosaic claudex', yolo: false },
|
||||||
{ command: 'mosaic yolo claudex', yolo: true },
|
{ command: 'mosaic yolo claudex', yolo: true },
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user