diff --git a/batch_runner.py b/batch_runner.py index 48551d0e2c..4c947831dc 100644 --- a/batch_runner.py +++ b/batch_runner.py @@ -490,13 +490,13 @@ class BatchRunner: try: entry = json.loads(line) - if 'prompt' not in entry: - print(f"⚠️ Warning: Line {line_num} missing 'prompt' field, skipping") - continue - dataset.append(entry) except json.JSONDecodeError as e: print(f"⚠️ Warning: Invalid JSON on line {line_num}: {e}") continue + if not isinstance(entry, dict) or 'prompt' not in entry: + print(f"⚠️ Warning: Line {line_num} missing 'prompt' field, skipping") + continue + dataset.append(entry) if not dataset: raise ValueError(f"No valid entries found in dataset file: {self.dataset_file}") @@ -552,7 +552,7 @@ class BatchRunner: for line in f: try: entry = json.loads(line.strip()) - if entry.get("failed", False): + if not isinstance(entry, dict) or entry.get("failed", False): continue prompt_text = _entry_prompt_text(entry) if prompt_text: @@ -722,6 +722,9 @@ class BatchRunner: try: data = json.loads(line) + if not isinstance(data, dict): + filtered_entries += 1 + continue if data.get("discarded"): tombstone_entries += 1 continue diff --git a/hermes_cli/local_runtime/binaries.py b/hermes_cli/local_runtime/binaries.py index c304f06ffa..11844a6dab 100644 --- a/hermes_cli/local_runtime/binaries.py +++ b/hermes_cli/local_runtime/binaries.py @@ -66,9 +66,10 @@ def runtimes_root() -> Path: def manifest_verified(manifest: Path) -> bool: """True when an install manifest records a verified_version (missing/damaged -> False).""" try: - return bool(json.loads(manifest.read_text(encoding="utf-8")).get("verified_version")) + data = json.loads(manifest.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError): return False + return isinstance(data, dict) and bool(data.get("verified_version")) def _release_number(tag: str) -> int: diff --git a/plugins/platforms/a2a/protocol.py b/plugins/platforms/a2a/protocol.py index a4cbcfe083..04484dc176 100644 --- a/plugins/platforms/a2a/protocol.py +++ b/plugins/platforms/a2a/protocol.py @@ -456,9 +456,11 @@ def load_conversation(context_id: str, limit: int = 50) -> list[dict]: for line in lines: if line.strip(): try: - out.append(json.loads(line)) + entry = json.loads(line) except json.JSONDecodeError: - pass + continue + if isinstance(entry, dict): + out.append(entry) return out[-limit:] diff --git a/tools/bot_relay.py b/tools/bot_relay.py index 63cb5a31f4..fc70687730 100644 --- a/tools/bot_relay.py +++ b/tools/bot_relay.py @@ -241,6 +241,8 @@ def _expire_if_stale(root: Path | str, path: Path, ttl: float, now: float) -> bo reply so the sender's waiter resolves (best effort). Unreadable envelopes are left for the claim.""" try: env = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(env, dict): + raise ValueError(f"expected a JSON object, got {type(env).__name__}") created = float(env.get("created_at") or path.stat().st_mtime) except (OSError, ValueError): return False @@ -287,7 +289,10 @@ def claim_pending_envelopes(root: Path | str) -> list[dict]: claimed = base / CLAIMED_DIR / path.name with contextlib.suppress(OSError, ValueError): os.replace(path, claimed) # atomic claim - out.append(json.loads(claimed.read_text(encoding="utf-8"))) + envelope = json.loads(claimed.read_text(encoding="utf-8")) + if not isinstance(envelope, dict): + raise ValueError(f"expected a JSON object, got {type(envelope).__name__}") + out.append(envelope) return out diff --git a/tools/browser_lightpanda.py b/tools/browser_lightpanda.py index 90c563c166..8fe03531e7 100644 --- a/tools/browser_lightpanda.py +++ b/tools/browser_lightpanda.py @@ -316,6 +316,8 @@ def reap_orphaned_lightpanda() -> int: session_name = record_path.stem try: record = json.loads(record_path.read_text(encoding="utf-8")) + if not isinstance(record, dict): + raise ValueError(f"expected a JSON object, got {type(record).__name__}") except (OSError, ValueError): record_path.unlink(missing_ok=True) continue diff --git a/tools/write_approval.py b/tools/write_approval.py index c519bf939a..d767256187 100644 --- a/tools/write_approval.py +++ b/tools/write_approval.py @@ -93,7 +93,10 @@ def list_pending(subsystem: str) -> List[Dict[str, Any]]: records: List[Dict[str, Any]] = [] for p in _pending_files(subsystem): try: - records.append(json.loads(p.read_text(encoding="utf-8"))) + record = json.loads(p.read_text(encoding="utf-8")) + if not isinstance(record, dict): + raise ValueError(f"expected a JSON object, got {type(record).__name__}") + records.append(record) except Exception: logger.warning("Skipping unreadable pending record: %s", p) records.sort(key=lambda r: r.get("created_at", 0)) @@ -106,7 +109,8 @@ def get_pending(subsystem: str, pending_id: str) -> Optional[Dict[str, Any]]: if not path.exists(): return None with suppress(Exception): - return json.loads(path.read_text(encoding="utf-8")) + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else None return None diff --git a/trajectory_compressor.py b/trajectory_compressor.py index 19892bb80f..d7c4f1fc70 100644 --- a/trajectory_compressor.py +++ b/trajectory_compressor.py @@ -572,7 +572,7 @@ Write only the summary, starting with "[CONTEXT SUMMARY]:" prefix.""" async def process_entry_async(self, entry: Dict[str, Any]) -> Tuple[Dict[str, Any], TrajectoryMetrics]: """Compress one JSONL entry's ``conversations``; attach metrics when compressed.""" - if "conversations" not in entry: + if not isinstance(entry, dict) or "conversations" not in entry: return entry, TrajectoryMetrics() compressed_trajectory, metrics = await self.compress_trajectory_async(entry["conversations"]) result = dict(entry, conversations=compressed_trajectory) diff --git a/tui_gateway/methods_session.py b/tui_gateway/methods_session.py index d916aac71d..729478d9e8 100644 --- a/tui_gateway/methods_session.py +++ b/tui_gateway/methods_session.py @@ -2160,6 +2160,8 @@ def _legacy_spawn_tree_entry(p, session_dir_name: str) -> dict | None: raw = {} with contextlib.suppress(Exception): raw = json.loads(p.read_text(encoding="utf-8")) + if not isinstance(raw, dict): + raw = {} subagents = raw.get("subagents") or [] return {"path": str(p), "session_id": raw.get("session_id") or session_dir_name, "finished_at": raw.get("finished_at") or stat.st_mtime, "started_at": raw.get("started_at"), @@ -2198,6 +2200,8 @@ def _(rid, params: dict) -> dict: payload = json.loads(resolved.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: return _err(rid, 5000, f"spawn_tree.load failed: {exc}") + if not isinstance(payload, dict): + return _err(rid, 5000, "spawn_tree.load failed: snapshot is not a JSON object") return _ok(rid, payload)