fix(#829): guard prefixed runtime variable launches
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
This commit is contained in:
@@ -24,6 +24,7 @@ interface BrokerPaths {
|
||||
const frameworkRoot = new URL('../../framework/', import.meta.url).pathname;
|
||||
const daemonPath = join(frameworkRoot, 'tools/lease-broker/daemon.py');
|
||||
const gatePath = join(frameworkRoot, 'tools/lease-broker/mutator-gate.py');
|
||||
const launchGuardPath = join(frameworkRoot, 'tools/lease-broker/check-runtime-launches.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');
|
||||
@@ -282,6 +283,39 @@ describe('whole mutator-class lease gate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('non-dangerous parser residual is denied by the global all-tools hook without a lease', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'mosaic-parser-residual-'));
|
||||
temporaryRoots.push(root);
|
||||
const source = join(root, 'packages/probe/launch.sh');
|
||||
await mkdir(join(root, 'packages/probe'), { recursive: true });
|
||||
await writeFile(source, 'alias hidden_runtime=claude\nhidden_runtime -p x\n');
|
||||
|
||||
const parserResult = spawnSync('python3', [launchGuardPath, '--root', root, '--json'], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
expect(parserResult.status).toBe(0);
|
||||
expect(JSON.parse(parserResult.stdout)).toMatchObject({ gated: 0, total: 0 });
|
||||
|
||||
const settings = JSON.parse(await readFile(claudeSettingsPath, 'utf8')) as {
|
||||
hooks: { PreToolUse: Array<{ matcher: string; hooks: Array<{ command: string }> }> };
|
||||
};
|
||||
const allToolsHook = settings.hooks.PreToolUse.find((hook) => hook.matcher === '.*');
|
||||
expect(allToolsHook?.hooks[0]?.command).toContain('mutator-gate.py --runtime claude');
|
||||
|
||||
const environment = { ...process.env };
|
||||
delete environment['MOSAIC_LEASE_SESSION_ID'];
|
||||
delete environment['MOSAIC_LEASE_BROKER_SOCKET'];
|
||||
for (const toolName of ['Bash', 'Read', 'mcp__provider__custom']) {
|
||||
const gateResult = spawnSync('python3', [gatePath, '--runtime', 'claude'], {
|
||||
input: `${JSON.stringify({ tool_name: toolName })}\n`,
|
||||
encoding: 'utf8',
|
||||
env: environment,
|
||||
});
|
||||
expect(gateResult.status, toolName).toBe(2);
|
||||
expect(gateResult.stderr, toolName).toContain('GATE_UNAVAILABLE');
|
||||
}
|
||||
});
|
||||
|
||||
test('T-B raw and custom mutator tools are default-denied without shell parsing', async () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
|
||||
@@ -114,6 +114,15 @@ class RuntimeLaunchGuardTest(unittest.TestCase):
|
||||
with self.subTest(filename=filename):
|
||||
self.assertEqual(GUARD.classify_text(Path(filename), source), [])
|
||||
|
||||
def test_dangerous_primitive_backstops_parser_exotic_alias_indirection(self) -> None:
|
||||
source = (
|
||||
"alias hidden_runtime=claude\n"
|
||||
"hidden_runtime --dangerously-skip-permissions -p x\n"
|
||||
)
|
||||
sites = GUARD.scan_text(Path("alias-launch.sh"), source)
|
||||
self.assertEqual(len(sites), 1)
|
||||
self.assertEqual(sites[0].classification, "dangerous-primitive")
|
||||
|
||||
def test_dangerous_primitive_is_owned_only_by_the_choke_point(self) -> None:
|
||||
primitive = "--dangerously-skip-permissions"
|
||||
self.assertNotEqual(GUARD.scan_text(Path("caller.ts"), f"args = ['{primitive}'];\n"), [])
|
||||
|
||||
Reference in New Issue
Block a user