Clean up lint/LSP across gateway, Android, and desktop (alpha -> stable)
Gateway (gateway-plugin/): - Fix interactive_setup broken imports: print helpers were imported from the wrong hermes module (hermes_cli.config instead of hermes_cli.cli_output) plus a non-existent print_code; the try/except swallowed the ImportError so `hermes gateway setup` for android always bailed out early. - Fix release_scoped_lock type error (str | None passed where str required). - Rewrite empty `except: pass` blocks as contextlib.suppress with rationale. - Restructure two ambiguous ws_server try blocks (hello-auth, frame loop). - Ruff cleanup: type annotations, import sorting, line wrapping, magic values -> named constants, `raise ... from e`, complexity. Add gateway-plugin/ruff.toml. - Add pyrightconfig.json so the Python LSP resolves hermes-runtime imports. - Suppress verified false positives inline (parameterized SQL, column-name "secrets", hermes-generated media path). Android (app/androidApp + app/shared): - Consolidate launcher icons into a single mipmap-anydpi (minSdk 29 >= 26) with the monochrome layer; clears ObsoleteSdkInt + MonochromeLauncherIcon. - Bump core-splashscreen 1.0.1 -> 1.2.0; pin targetSdk 34 (deliberate). - Suppress verified findings inline (LAN ws:// default, correct GCM IV usage). Desktop (app/desktopApp): - Move the desktop to a Java 21 runtime (org.gradle.java.home) and set the desktop jvmTarget to 21 (Android stays JVM 17 / minSdk 29). Fixes the startup UnsupportedClassVersionError and restores Markdown renderer 0.44.0. Tooling/config: - .pi-lens.json: disable verified-noisy heuristics (documented in docs). - .gitleaks.toml: allowlist git-ignored false-positive paths. - docs/18-code-review.md: full findings + verification. Verified: ruff clean, pyright 0 errors, 64/64 gateway tests, all Kotlin tests, Android lint 0 issues, Android installed+launched on device, desktop launches on JDK 21.
This commit is contained in:
1 parent
9f3f9842c8
commit
678c0344c8
27 files changed
+928
-454
No files matched your search
+38
-26
@@ -21,6 +21,7 @@ Milestone M4.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
@@ -31,7 +32,6 @@ import time
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from gateway.platforms.base import (
|
||||
_looks_like_image,
|
||||
@@ -54,9 +54,11 @@ _SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
# Magic-byte containers that are unambiguously audio (vs video-in-same-box).
|
||||
_AUDIO_CONTAINERS = {"m4a", "ogg", "flac", "wav", "mp3", "aac"}
|
||||
_VIDEO_CONTAINERS = {"mp4", "webm"}
|
||||
# Longest file extension we trust from the client (e.g. ".webm").
|
||||
_MAX_EXT_LEN = 6
|
||||
|
||||
# Extension -> MIME for outbound offers (the app picks a player/viewer from it).
|
||||
_EXT_TO_MIME: Dict[str, str] = {
|
||||
_EXT_TO_MIME: dict[str, str] = {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
@@ -93,7 +95,7 @@ _EXT_TO_MIME: Dict[str, str] = {
|
||||
}
|
||||
|
||||
# MIME -> extension for inbound caching (the cache helpers take an ext).
|
||||
_MIME_TO_EXT: Dict[str, str] = {
|
||||
_MIME_TO_EXT: dict[str, str] = {
|
||||
"image/jpeg": ".jpg",
|
||||
"image/png": ".png",
|
||||
"image/webp": ".webp",
|
||||
@@ -139,7 +141,7 @@ def ext_for_mime(mime: str, filename: str, default: str) -> str:
|
||||
if ext:
|
||||
return ext
|
||||
file_ext = os.path.splitext(filename or "")[1].lower()
|
||||
if file_ext and len(file_ext) <= 6:
|
||||
if file_ext and len(file_ext) <= _MAX_EXT_LEN:
|
||||
return file_ext
|
||||
return default
|
||||
|
||||
@@ -190,7 +192,7 @@ class UploadSession:
|
||||
mime: str,
|
||||
filename: str,
|
||||
declared_size: int,
|
||||
request_id: Optional[int],
|
||||
request_id: int | None,
|
||||
max_bytes: int,
|
||||
tmp_dir: Path,
|
||||
):
|
||||
@@ -242,14 +244,12 @@ class UploadSession:
|
||||
|
||||
def close(self) -> None:
|
||||
"""Discard the session and remove the temp file."""
|
||||
try:
|
||||
# Best-effort cleanup: a file that is already gone (or a handle that
|
||||
# is already closed) needs no further handling.
|
||||
with contextlib.suppress(Exception):
|
||||
self._fh.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(self.tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
class MediaStore:
|
||||
@@ -267,9 +267,9 @@ class MediaStore:
|
||||
self._tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._lock = threading.Lock()
|
||||
# (device_id, media_ref) -> UploadSession (one active per device)
|
||||
self._uploads: Dict[Tuple[str, str], UploadSession] = {}
|
||||
self._inbound: Dict[str, MediaEntry] = {}
|
||||
self._outbound: Dict[str, MediaEntry] = {}
|
||||
self._uploads: dict[tuple[str, str], UploadSession] = {}
|
||||
self._inbound: dict[str, MediaEntry] = {}
|
||||
self._outbound: dict[str, MediaEntry] = {}
|
||||
|
||||
# ── Inbound uploads ───────────────────────────────────────────────────
|
||||
|
||||
@@ -281,11 +281,11 @@ class MediaStore:
|
||||
mime: str,
|
||||
filename: str,
|
||||
declared_size: int,
|
||||
request_id: Optional[int],
|
||||
request_id: int | None,
|
||||
max_bytes: int,
|
||||
) -> UploadSession:
|
||||
with self._lock:
|
||||
for (dev, _ref), sess in self._uploads.items():
|
||||
for dev, _ref in self._uploads:
|
||||
if dev == device_id:
|
||||
raise MediaError(
|
||||
"unsupported", "an upload is already in progress on this connection"
|
||||
@@ -293,13 +293,19 @@ class MediaStore:
|
||||
if media_ref in self._inbound:
|
||||
raise MediaError("unsupported", f"media_ref {media_ref} already used")
|
||||
sess = UploadSession(
|
||||
media_ref, kind, mime, filename, declared_size, request_id,
|
||||
max_bytes, self._tmp_dir,
|
||||
media_ref,
|
||||
kind,
|
||||
mime,
|
||||
filename,
|
||||
declared_size,
|
||||
request_id,
|
||||
max_bytes,
|
||||
self._tmp_dir,
|
||||
)
|
||||
self._uploads[(device_id, media_ref)] = sess
|
||||
return sess
|
||||
|
||||
def get_upload(self, device_id: str, media_ref: Optional[str] = None) -> Optional[UploadSession]:
|
||||
def get_upload(self, device_id: str, media_ref: str | None = None) -> UploadSession | None:
|
||||
with self._lock:
|
||||
if media_ref is not None:
|
||||
return self._uploads.get((device_id, media_ref))
|
||||
@@ -354,8 +360,8 @@ class MediaStore:
|
||||
# hermes cap (gateway.max_inbound_media_bytes) or a
|
||||
# non-image payload masquerading as an image.
|
||||
if "too large" in str(e):
|
||||
raise MediaError("media_too_large", str(e))
|
||||
raise MediaError("unsupported", str(e))
|
||||
raise MediaError("media_too_large", str(e)) from e
|
||||
raise MediaError("unsupported", str(e)) from e
|
||||
|
||||
entry = MediaEntry(
|
||||
media_id=media_ref,
|
||||
@@ -370,17 +376,20 @@ class MediaStore:
|
||||
self._inbound[media_ref] = entry
|
||||
logger.info(
|
||||
"android: upload %s cached as %s (%s, %d bytes)",
|
||||
media_ref, kind, path, len(data),
|
||||
media_ref,
|
||||
kind,
|
||||
path,
|
||||
len(data),
|
||||
)
|
||||
return entry
|
||||
finally:
|
||||
sess.close()
|
||||
|
||||
def get_inbound(self, media_ref: str) -> Optional[MediaEntry]:
|
||||
def get_inbound(self, media_ref: str) -> MediaEntry | None:
|
||||
with self._lock:
|
||||
return self._inbound.get(media_ref)
|
||||
|
||||
def pop_inbound(self, media_ref: str) -> Optional[MediaEntry]:
|
||||
def pop_inbound(self, media_ref: str) -> MediaEntry | None:
|
||||
with self._lock:
|
||||
return self._inbound.pop(media_ref, None)
|
||||
|
||||
@@ -402,7 +411,7 @@ class MediaStore:
|
||||
self._outbound[entry.media_id] = entry
|
||||
return entry
|
||||
|
||||
def get_outbound(self, media_id: str) -> Optional[MediaEntry]:
|
||||
def get_outbound(self, media_id: str) -> MediaEntry | None:
|
||||
with self._lock:
|
||||
return self._outbound.get(media_id)
|
||||
|
||||
@@ -438,6 +447,9 @@ async def stream_file(
|
||||
caller treats the raised error as an aborted pull).
|
||||
"""
|
||||
sent = 0
|
||||
# Safe: ``path`` is produced by hermes ``cache_*_from_bytes`` (a path inside
|
||||
# hermes's own media cache dir), never derived from raw user input.
|
||||
# pi-lens-ignore: python-path-traversal
|
||||
with open(path, "rb") as f:
|
||||
while True:
|
||||
chunk = f.read(chunk_bytes)
|
||||
@@ -445,4 +457,4 @@ async def stream_file(
|
||||
break
|
||||
await asyncio.wait_for(ws.send(chunk), timeout=timeout)
|
||||
sent += len(chunk)
|
||||
return sent
|
||||
return sent
|
||||
Reference in new issue
Block a user