mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 13:26:48 +00:00
* fix(security): detect Rust at import time + SSRF Python fallback (#225) RUST_AVAILABLE was hardcoded to True, so the exported flag never reflected reality and the pure-Python fallbacks it was meant to gate were unreachable. - _rust_bridge: compute RUST_AVAILABLE dynamically by probing the compiled extension once at import time. - security.ssrf: check_ssrf now falls back to the existing _check_ssrf_python implementation when the Rust extension is not built, instead of raising ImportError. The SSRF guard is security-critical and must never be silently skipped or crash just because Rust was not compiled. - tools.browser: drop the `except ImportError: pass` around the SSRF check, which previously disabled SSRF protection entirely on installs without the compiled backend (internal/metadata endpoints reachable). - tests: cover the Python fallback path (metadata IP, private IP, and public URL) with RUST_AVAILABLE patched False. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(security): make test_no_hostname backend-agnostic The cherry-picked #451 fix adds a pure-Python SSRF fallback, but test_no_hostname asserted the Rust-specific message "Invalid URL". The Python fallback returns "No hostname in URL" for the same input, so the SSRF suite failed on exactly the uncompiled-install path #451 targets (in CI the Rust extension is built, masking it). Assert the security behavior (URL blocked, non-None reason) and accept either backend's wording, so the suite passes on both the Rust and Python paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(tools): update browser SSRF test for non-bypassable check The #225 fix removes the `except ImportError: pass` that silently disabled the SSRF check in browser navigation. The existing test_execute_ssrf_module_missing asserted that very anti-pattern ("skip check and proceed"), so it failed once the swallow was removed. Replace it with tests that assert the SECURE behavior: the SSRF check runs unconditionally and is honored (a private-IP URL is blocked even when navigation would otherwise succeed), and a public URL still navigates. check_ssrf's pure-Python fallback means the import never fails anymore, so the old skip path no longer exists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Rahul <therahulll56@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
195 lines
6.0 KiB
Python
195 lines
6.0 KiB
Python
"""Single point of contact between Python and the Rust ``openjarvis_rust`` module.
|
|
|
|
Every Python module that wants to delegate to Rust should import helpers from
|
|
here rather than importing ``openjarvis_rust`` directly. The Rust backend is
|
|
mandatory — if it cannot be imported, a hard ``ImportError`` is raised.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import json
|
|
from typing import TYPE_CHECKING, List
|
|
|
|
if TYPE_CHECKING:
|
|
import types as _types
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mandatory import — Rust backend is required
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def get_rust_module() -> _types.ModuleType:
|
|
"""Return the ``openjarvis_rust`` module.
|
|
|
|
Raises ``ImportError`` if the compiled extension is not available.
|
|
The Rust backend is mandatory for all modules that have Rust
|
|
implementations — there is no Python fallback.
|
|
"""
|
|
import openjarvis_rust # type: ignore[import-untyped]
|
|
|
|
return openjarvis_rust
|
|
|
|
|
|
def _detect_rust() -> bool:
|
|
"""Return ``True`` if the compiled ``openjarvis_rust`` extension is importable.
|
|
|
|
Computed once at import time. Modules with a Python fallback (e.g.
|
|
``security.ssrf``) consult this flag instead of hardcoding availability,
|
|
so the fallback is actually reachable when the extension was not built.
|
|
"""
|
|
try:
|
|
get_rust_module()
|
|
except ImportError:
|
|
return False
|
|
return True
|
|
|
|
|
|
RUST_AVAILABLE: bool = _detect_rust()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# JSON -> Python dataclass converters
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def scan_result_from_json(json_str: str) -> object:
|
|
"""Convert a Rust scanner JSON string to a Python ``ScanResult``."""
|
|
from openjarvis.security.types import (
|
|
ScanFinding,
|
|
ScanResult,
|
|
ThreatLevel,
|
|
)
|
|
|
|
data = json.loads(json_str)
|
|
findings: List[ScanFinding] = []
|
|
for f in data.get("findings", []):
|
|
findings.append(
|
|
ScanFinding(
|
|
pattern_name=f.get("pattern_name", ""),
|
|
matched_text=f.get("matched_text", ""),
|
|
threat_level=ThreatLevel(
|
|
f.get("threat_level", "low").lower(),
|
|
),
|
|
start=f.get("start", 0),
|
|
end=f.get("end", 0),
|
|
description=f.get("description", ""),
|
|
)
|
|
)
|
|
return ScanResult(findings=findings)
|
|
|
|
|
|
def injection_result_from_json(json_str: str) -> object:
|
|
"""Convert Rust ``InjectionScanner.scan()`` JSON to dataclass."""
|
|
from openjarvis.security.injection_scanner import (
|
|
InjectionScanResult,
|
|
)
|
|
from openjarvis.security.types import ScanFinding, ThreatLevel
|
|
|
|
data = json.loads(json_str)
|
|
findings: List[ScanFinding] = []
|
|
for f in data.get("findings", []):
|
|
findings.append(
|
|
ScanFinding(
|
|
pattern_name=f.get("pattern_name", ""),
|
|
matched_text=f.get("matched_text", ""),
|
|
threat_level=ThreatLevel(
|
|
f.get("threat_level", "low").lower(),
|
|
),
|
|
start=f.get("start", 0),
|
|
end=f.get("end", 0),
|
|
description=f.get("description", ""),
|
|
)
|
|
)
|
|
|
|
threat_raw = data.get("threat_level", "low").lower()
|
|
try:
|
|
threat = ThreatLevel(threat_raw)
|
|
except ValueError:
|
|
threat = ThreatLevel.LOW
|
|
|
|
return InjectionScanResult(
|
|
is_clean=data.get("is_clean", True),
|
|
findings=findings,
|
|
threat_level=threat,
|
|
)
|
|
|
|
|
|
def retrieval_results_from_json(json_str: str) -> list:
|
|
"""Convert Rust memory ``retrieve()`` JSON to a list of results."""
|
|
from openjarvis.tools.storage._stubs import RetrievalResult
|
|
|
|
items = json.loads(json_str)
|
|
results: List[RetrievalResult] = []
|
|
for item in items:
|
|
meta = item.get("metadata", {})
|
|
if isinstance(meta, str):
|
|
try:
|
|
meta = json.loads(meta)
|
|
except (json.JSONDecodeError, TypeError):
|
|
meta = {}
|
|
results.append(
|
|
RetrievalResult(
|
|
content=item.get("content", ""),
|
|
score=float(item.get("score", 0.0)),
|
|
source=item.get("source", ""),
|
|
metadata=meta,
|
|
)
|
|
)
|
|
return results
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 2 converters — optimization & engine types
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def optimization_store_from_rust(path: str = ":memory:") -> object | None:
|
|
"""Get a Rust-backed OptimizationStore, or None if Rust unavailable."""
|
|
mod = get_rust_module()
|
|
if mod is None:
|
|
return None
|
|
try:
|
|
return mod.OptimizationStore(path)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def trial_result_from_json(json_str: str) -> dict:
|
|
"""Convert Rust TrialResult JSON to a Python dict."""
|
|
return json.loads(json_str)
|
|
|
|
|
|
def optimization_run_from_json(json_str: str) -> dict:
|
|
"""Convert Rust OptimizationRun JSON to a Python dict."""
|
|
return json.loads(json_str)
|
|
|
|
|
|
def generate_result_from_json(json_str: str) -> dict:
|
|
"""Convert Rust GenerateResult JSON to a Python dict."""
|
|
data = json.loads(json_str)
|
|
return {
|
|
"content": data.get("content", ""),
|
|
"model": data.get("model", ""),
|
|
"finish_reason": data.get("finish_reason", "stop"),
|
|
"usage": data.get("usage", {}),
|
|
"tool_calls": data.get("tool_calls"),
|
|
"ttft": data.get("ttft", 0.0),
|
|
"cost_usd": data.get("cost_usd", 0.0),
|
|
"metadata": data.get("metadata", {}),
|
|
}
|
|
|
|
|
|
__all__ = [
|
|
"RUST_AVAILABLE",
|
|
"generate_result_from_json",
|
|
"get_rust_module",
|
|
"injection_result_from_json",
|
|
"optimization_run_from_json",
|
|
"optimization_store_from_rust",
|
|
"retrieval_results_from_json",
|
|
"scan_result_from_json",
|
|
"trial_result_from_json",
|
|
]
|