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:
ARIA committed 2026-08-21 18:47:03 +02:00
1 parent 9f3f9842c8
commit 678c0344c8
27 files changed
+928 -454

No files matched your search

+53 -51
View File
@@ -20,12 +20,14 @@ Storage: ``get_hermes_home()/"android"/channels.db``.
Milestone M3.
"""
import builtins
import contextlib
import logging
import sqlite3
import threading
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any
logger = logging.getLogger(__name__)
@@ -73,9 +75,7 @@ class ChannelDirectory:
"""
)
# Migrate existing DBs: add the cosmetic columns if missing.
existing = {
row[1] for row in self._conn.execute("PRAGMA table_info(channels)")
}
existing = {row[1] for row in self._conn.execute("PRAGMA table_info(channels)")}
if "favorite" not in existing:
self._conn.execute(
"ALTER TABLE channels ADD COLUMN favorite INTEGER NOT NULL DEFAULT 0"
@@ -120,7 +120,7 @@ class ChannelDirectory:
# ── default channel ───────────────────────────────────────────────────
def ensure_default(self, chat_id: str, name: str) -> Dict[str, Any]:
def ensure_default(self, chat_id: str, name: str) -> dict[str, Any]:
"""Ensure the default (home) channel exists. Idempotent.
If a row already exists for *chat_id* it is kept (name refreshed only
@@ -152,16 +152,18 @@ class ChannelDirectory:
(KIND_DEFAULT, chat_id),
)
# Exactly one default: clear any other default flag.
self._conn.execute(
"UPDATE channels SET is_default = 0 WHERE chat_id != ?", (chat_id,)
)
self._conn.execute("UPDATE channels SET is_default = 0 WHERE chat_id != ?", (chat_id,))
self._conn.commit()
entry = self.get(chat_id)
if entry is not None:
return entry
return {
"chat_id": chat_id, "name": name, "kind": KIND_DEFAULT,
"parent_chat_id": None, "is_default": True, "archived": False,
"chat_id": chat_id,
"name": name,
"kind": KIND_DEFAULT,
"parent_chat_id": None,
"is_default": True,
"archived": False,
"created": time.time(),
}
@@ -171,8 +173,8 @@ class ChannelDirectory:
self,
name: str,
kind: str = KIND_CHANNEL,
parent_chat_id: Optional[str] = None,
) -> Dict[str, Any]:
parent_chat_id: str | None = None,
) -> dict[str, Any]:
"""Mint a new channel (or thread) and store it. Returns the entry."""
name = (name or "").strip()
if not name:
@@ -194,12 +196,16 @@ class ChannelDirectory:
if entry is not None:
return entry
return {
"chat_id": chat_id, "name": name, "kind": kind,
"parent_chat_id": parent_chat_id, "is_default": False,
"archived": False, "created": now,
"chat_id": chat_id,
"name": name,
"kind": kind,
"parent_chat_id": parent_chat_id,
"is_default": False,
"archived": False,
"created": now,
}
def rename(self, chat_id: str, name: str) -> Optional[Dict[str, Any]]:
def rename(self, chat_id: str, name: str) -> dict[str, Any] | None:
name = (name or "").strip()
if not name:
raise ValueError("channel name required")
@@ -213,7 +219,7 @@ class ChannelDirectory:
return None
return self.get(chat_id)
def set_default(self, chat_id: str) -> Optional[Dict[str, Any]]:
def set_default(self, chat_id: str) -> dict[str, Any] | None:
"""Mark *chat_id* as the default channel (clears the previous one).
The default channel is the user's chat surface, so the automation
@@ -228,14 +234,13 @@ class ChannelDirectory:
return None
self._conn.execute("UPDATE channels SET is_default = 0")
self._conn.execute(
"UPDATE channels SET is_default = 1, automation = 0 "
"WHERE chat_id = ?",
"UPDATE channels SET is_default = 1, automation = 0 WHERE chat_id = ?",
(chat_id,),
)
self._conn.commit()
return self.get(chat_id)
def set_favorite(self, chat_id: str, on: bool) -> Optional[Dict[str, Any]]:
def set_favorite(self, chat_id: str, on: bool) -> dict[str, Any] | None:
"""Toggle the cosmetic favorite flag (sorts to the top of the list)."""
with self._lock:
cur = self._conn.execute(
@@ -247,7 +252,7 @@ class ChannelDirectory:
return None
return self.get(chat_id)
def set_icon(self, chat_id: str, icon: Optional[str], color: Optional[str]) -> Optional[Dict[str, Any]]:
def set_icon(self, chat_id: str, icon: str | None, color: str | None) -> dict[str, Any] | None:
"""Set the channel's cosmetic icon (base64 image) and/or avatar color.
``icon`` is a base64-encoded image (or ``None`` to clear it); ``color``
@@ -264,7 +269,7 @@ class ChannelDirectory:
return None
return self.get(chat_id)
def set_automation(self, chat_id: str, on: bool) -> Optional[Dict[str, Any]]:
def set_automation(self, chat_id: str, on: bool) -> dict[str, Any] | None:
"""Mark *chat_id* as an automation channel (or clear the flag).
Automation channels are read-only for the user: they only receive
@@ -287,7 +292,7 @@ class ChannelDirectory:
self._conn.commit()
return self.get(chat_id)
def delete(self, chat_id: str) -> Optional[Dict[str, Any]]:
def delete(self, chat_id: str) -> dict[str, Any] | None:
"""Soft-delete (archive) a channel. History stays for search.
The default channel cannot be deleted. Returns the (archived) entry,
@@ -299,39 +304,40 @@ class ChannelDirectory:
).fetchone()
if row is None or row["is_default"]:
return None
self._conn.execute(
"UPDATE channels SET archived = 1 WHERE chat_id = ?", (chat_id,)
)
self._conn.execute("UPDATE channels SET archived = 1 WHERE chat_id = ?", (chat_id,))
self._conn.commit()
return self.get(chat_id)
# ── reads ─────────────────────────────────────────────────────────────
def get(self, chat_id: str) -> Optional[Dict[str, Any]]:
def get(self, chat_id: str) -> dict[str, Any] | None:
with self._lock:
row = self._conn.execute(
"SELECT * FROM channels WHERE chat_id = ?", (chat_id,)
).fetchone()
return _row_to_entry(row) if row else None
def list(self, include_archived: bool = False) -> List[Dict[str, Any]]:
def list(self, include_archived: bool = False) -> list[dict[str, Any]]:
"""Directory listing. Default first, then favorites, then creation order."""
sql = "SELECT * FROM channels"
if not include_archived:
sql += " WHERE archived = 0"
sql += " ORDER BY is_default DESC, favorite DESC, created ASC"
with self._lock:
# Safe: fully static SQL (no user data); the variable is only to
# toggle the optional archived filter.
# pi-lens-ignore: python-sql-injection
rows = self._conn.execute(sql).fetchall()
return [_row_to_entry(r) for r in rows]
def default(self) -> Optional[Dict[str, Any]]:
def default(self) -> dict[str, Any] | None:
with self._lock:
row = self._conn.execute(
"SELECT * FROM channels WHERE is_default = 1 LIMIT 1"
).fetchone()
return _row_to_entry(row) if row else None
def threads_for(self, chat_id: str) -> List[Dict[str, Any]]:
def threads_for(self, chat_id: str) -> builtins.list[dict[str, Any]]:
"""All (non-archived) threads under *chat_id*, oldest first."""
with self._lock:
rows = self._conn.execute(
@@ -341,7 +347,7 @@ class ChannelDirectory:
).fetchall()
return [_row_to_entry(r) for r in rows]
def resolve_entry(self, name: str) -> Optional[Dict[str, Any]]:
def resolve_entry(self, name: str) -> dict[str, Any] | None:
"""Resolve a friendly name to a directory entry (case-insensitive).
Matches non-archived channels/threads by exact name first, then by
@@ -352,23 +358,19 @@ class ChannelDirectory:
if not query:
return None
with self._lock:
rows = self._conn.execute(
"SELECT * FROM channels WHERE archived = 0"
).fetchall()
rows = self._conn.execute("SELECT * FROM channels WHERE archived = 0").fetchall()
entries = [_row_to_entry(r) for r in rows]
exact = [e for e in entries if (e["name"] or "").strip().lower() == query]
if len(exact) == 1:
return exact[0]
if len(exact) > 1:
return None
prefix = [
e for e in entries if (e["name"] or "").strip().lower().startswith(query)
]
prefix = [e for e in entries if (e["name"] or "").strip().lower().startswith(query)]
if len(prefix) == 1:
return prefix[0]
return None
def resolve_name(self, name: str) -> Optional[str]:
def resolve_name(self, name: str) -> str | None:
"""Resolve a friendly name to a valid chat_id (case-insensitive).
For a thread, returns the *parent* chat_id (the thread's session lane
@@ -383,14 +385,12 @@ class ChannelDirectory:
return entry["chat_id"]
def close(self) -> None:
with self._lock:
try:
self._conn.close()
except Exception:
pass
with self._lock, contextlib.suppress(Exception):
# Best-effort: a close failure on shutdown is not actionable.
self._conn.close()
def _row_to_entry(row: sqlite3.Row) -> Dict[str, Any]:
def _row_to_entry(row: sqlite3.Row) -> dict[str, Any]:
return {
"chat_id": row["chat_id"],
"name": row["name"],
@@ -415,24 +415,26 @@ def _row_to_entry(row: sqlite3.Row) -> Dict[str, Any]:
# Keyed on ``get_hermes_home()`` so a profile switch rebuilds it.
# ---------------------------------------------------------------------------
_directory: Optional[ChannelDirectory] = None
_directory_home: Optional[Path] = None
_directory: ChannelDirectory | None = None
_directory_home: Path | None = None
_directory_lock = threading.Lock()
def get_directory() -> ChannelDirectory:
"""Return the process-wide channel directory for the active profile."""
global _directory, _directory_home
# Module-level singleton keyed on the active profile; the global is the
# intended pattern here (see the block comment above).
global _directory, _directory_home # noqa: PLW0603
from hermes_constants import get_hermes_home
home = Path(get_hermes_home())
with _directory_lock:
if _directory is None or _directory_home != home:
if _directory is not None:
try:
# Best-effort: the old directory is being replaced; a close
# failure is not actionable.
with contextlib.suppress(Exception):
_directory.close()
except Exception:
pass
_directory = ChannelDirectory(home / "android" / "channels.db")
_directory_home = home
return _directory
return _directory