fix(google-chat): install optional deps into the sealed-image lazy target

Hosted/Docker images lock /opt/hermes/.venv, so --install-deps writing
site-packages fails with Permission denied and the adapter never starts.
Route Google Chat through lazy_deps (HERMES_LAZY_INSTALL_TARGET) and bake
the extra into the published image so a configured gateway can connect.
This commit is contained in:
xxxigm
2026-09-12 19:21:01 +07:00
committed by Teknium
parent 6ba92758bc
commit f3dbb19707
7 changed files with 152 additions and 23 deletions

View File

@@ -261,10 +261,15 @@ RUN cd plugins/platforms/photon/sidecar && \
# avoids the cross-platform failures that kept [matrix] out of [all]
# while still making Matrix work in the published container. Fixes #30399.
#
# Google Chat's [google-chat] extra (google-cloud-pubsub + Chat API clients)
# is baked so hosted/immutable images can enable the adapter without writing
# the sealed venv. Runtime --install-deps still routes through lazy_deps into
# HERMES_LAZY_INSTALL_TARGET when the extra is not present.
#
# The editable link is created after the source copy below.
COPY pyproject.toml uv.lock ./
RUN touch ./README.md
RUN uv sync --frozen --no-install-project --extra all --extra messaging --extra otlp --extra anthropic --extra bedrock --extra azure-identity --extra hindsight --extra matrix
RUN uv sync --frozen --no-install-project --extra all --extra messaging --extra otlp --extra anthropic --extra bedrock --extra azure-identity --extra hindsight --extra matrix --extra google-chat
# ---------- Frontend build (cached independently from Python source) ----------
# Copy only the frontend source trees first so that Python-only changes don't

View File

@@ -185,7 +185,26 @@ def _is_retryable_error(exc: BaseException) -> bool:
def check_google_chat_requirements() -> bool:
"""Canonical "are the optional deps available" probe; triggers the lazy import."""
"""PASSIVE deps probe; must never install. Registry ``check_fn`` uses this via ``_check_for_registry``."""
return _load_google_modules()
def ensure_google_chat_deps() -> bool:
"""ACTIVE installer (registry ``ensure_deps_fn``).
Routes through ``tools.lazy_deps`` so sealed hosted/Docker images write
``HERMES_LAZY_INSTALL_TARGET`` instead of the read-only venv. Resets the
failed-import cache so ``create_adapter()`` can load modules after install.
"""
global _google_modules_loaded, GOOGLE_CHAT_AVAILABLE
if GOOGLE_CHAT_AVAILABLE:
return True
try:
from tools.lazy_deps import ensure as _lazy_ensure
_lazy_ensure("platform.google_chat", prompt=False)
except Exception:
return False
_google_modules_loaded = False
return _load_google_modules()
@@ -1704,6 +1723,7 @@ def register(ctx) -> None:
label="Google Chat",
adapter_factory=lambda cfg: GoogleChatAdapter(cfg),
check_fn=_check_for_registry,
ensure_deps_fn=ensure_google_chat_deps,
validate_config=_validate_config,
is_connected=_is_connected,
required_env=["GOOGLE_CHAT_SERVICE_ACCOUNT_JSON"],

View File

@@ -237,16 +237,20 @@ def install_deps() -> bool:
return True
print("Installing Google Chat dependencies...")
try:
from hermes_cli.tools_config import _pip_install
from tools.lazy_deps import FeatureUnavailable, ensure as _lazy_ensure
result = _pip_install(["--quiet"] + missing)
if result.returncode != 0:
raise RuntimeError((result.stderr or "install failed").strip()[:300])
# lazy_deps honors HERMES_LAZY_INSTALL_TARGET on sealed hosted images;
# _pip_install always writes the venv and Permission-denied there.
_lazy_ensure("platform.google_chat", prompt=False)
remaining = _missing_required_packages()
if remaining:
raise RuntimeError("dependencies remain stale after install: " + " ".join(remaining))
print("Dependencies installed.")
return True
except FeatureUnavailable as exc:
print(f"ERROR: Failed to install dependencies: {exc.reason}")
print("Run `hermes setup` to repair the managed installation, then retry.")
return False
except Exception as exc:
print(f"ERROR: Failed to install dependencies: {exc}")
print("Run `hermes setup` to repair the managed installation, then retry.")

View File

@@ -344,6 +344,19 @@ google = [
"httplib2==0.32.0",
"pyasn1==0.6.4",
]
google-chat = [
# Google Chat adapter (Pub/Sub inbound + Chat REST). Kept out of [all] so a
# quarantined google-cloud-pubsub cannot break every fresh install; Docker
# bakes `--extra google-chat` and lazy_deps installs into
# HERMES_LAZY_INSTALL_TARGET on sealed hosted images.
"google-cloud-pubsub==2.39.0",
"google-api-python-client==2.194.0",
"google-auth==2.55.1",
"google-auth-oauthlib==1.3.1",
"google-auth-httplib2==0.3.1",
"httplib2==0.32.0",
"pyasn1==0.6.4",
]
youtube = [
# Required by skills/media/youtube-content and
# optional-skills/productivity/memento-flashcards (youtube_quiz.py).
@@ -498,6 +511,7 @@ google-api-python-client = false
google-auth = false
google-auth-httplib2 = false
google-auth-oauthlib = false
google-cloud-pubsub = false
h2 = false
hindsight-client = false
honcho-ai = false

View File

@@ -47,17 +47,17 @@ def test_installer_repairs_stale_transitives(monkeypatch):
)
monkeypatch.setattr(oauth, "_missing_required_packages", lambda: next(states))
calls = []
pip_calls = []
def fake_ensure(feature, prompt=False):
calls.append((feature, prompt))
monkeypatch.setattr("tools.lazy_deps.ensure", fake_ensure)
monkeypatch.setattr(
"hermes_cli.tools_config._pip_install",
lambda argv: calls.append(argv) or SimpleNamespace(returncode=0, stderr=""),
lambda argv: pip_calls.append(argv) or SimpleNamespace(returncode=0, stderr=""),
)
assert oauth.install_deps() is True
assert calls == [
[
"--quiet",
"google-auth==2.55.1",
"httplib2==0.32.0",
"pyasn1==0.6.4",
]
]
assert calls == [("platform.google_chat", False)]
assert pip_calls == []

View File

@@ -150,6 +150,17 @@ LAZY_DEPS: dict[str, tuple[str, ...]] = {
"platform.wecom_callback": ("defusedxml==0.7.1",),
# Teams pulls a heavy tree (msal, dependency-injector); also the `teams` extra.
"platform.teams": ("microsoft-teams-apps==2.0.13.4", "aiohttp==3.14.3"),
# Google Chat — Pub/Sub + Chat API. Not in [all]; Docker bakes `--extra google-chat`
# so hosted/immutable images do not have to write the sealed venv.
"platform.google_chat": (
"google-cloud-pubsub==2.39.0",
"google-api-python-client==2.194.0",
"google-auth==2.55.1",
"google-auth-oauthlib==1.3.1",
"google-auth-httplib2==0.3.1",
"httplib2==0.32.0",
"pyasn1==0.6.4",
),
# ─── Terminal backends ─────────────────────────────────────────────────
"terminal.modal": ("modal==1.3.4",),

91
uv.lock generated
View File

@@ -1493,6 +1493,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" },
]
[package.optional-dependencies]
grpc = [
{ name = "grpcio" },
{ name = "grpcio-status" },
]
[[package]]
name = "google-api-python-client"
version = "2.194.0"
@@ -1548,6 +1554,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e0/cb454a95f460903e39f101e950038ec24a072ca69d0a294a6df625cc1627/google_auth_oauthlib-1.3.1-py3-none-any.whl", hash = "sha256:1a139ef23f1318756805b0e95f655c238bffd29655329a2978218248da4ee7f8", size = 19247, upload-time = "2026-03-30T20:02:23.894Z" },
]
[[package]]
name = "google-cloud-pubsub"
version = "2.39.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core", extra = ["grpc"] },
{ name = "google-auth" },
{ name = "grpc-google-iam-v1" },
{ name = "grpcio" },
{ name = "grpcio-status" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-sdk" },
{ name = "proto-plus" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/11/2b/4bf2c17e319ff65340389565b0e1b4d72696d87802b2f5f94390fbefa73c/google_cloud_pubsub-2.39.0.tar.gz", hash = "sha256:eed65e25f57f95bf3e02d96d7ee171688b23922471f9f21b5a91ed90e1282c0f", size = 402096, upload-time = "2026-06-03T15:28:26.396Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/20/dd0b27d4ad4577c062e77ff968ca3e2d404186cd78c8a2a53a0ef5fe5389/google_cloud_pubsub-2.39.0-py3-none-any.whl", hash = "sha256:7210d691a46d7a66559696899ebe6eb731e63de29b624964b3be4dd2d12d3e19", size = 324665, upload-time = "2026-06-03T15:27:41.119Z" },
]
[[package]]
name = "googleapis-common-protos"
version = "1.73.0"
@@ -1560,6 +1586,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" },
]
[package.optional-dependencies]
grpc = [
{ name = "grpcio" },
]
[[package]]
name = "greenlet"
version = "3.5.3"
@@ -1592,6 +1623,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" },
]
[[package]]
name = "grpc-google-iam-v1"
version = "0.14.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos", extra = ["grpc"] },
{ name = "grpcio" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d2/d0/fa5bdd5f3f421bb68dc6dc162e9caaf942897ca41ce7255b524723c80f0b/grpc_google_iam_v1-0.14.5.tar.gz", hash = "sha256:07fd3a9fafb586588e771831fbfc8f6597050181d0c3b45e039d18b8fdc1aab5", size = 23736, upload-time = "2026-08-06T06:24:54.489Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/ab/be3ad0d46cffe35fd1e7cc3f9947edd6cb3c552229de3be2742f15f7ea47/grpc_google_iam_v1-0.14.5-py3-none-any.whl", hash = "sha256:0f5e680b20aa0a9441e68c769da04d94d70fca4e43751a82d8abb8aa6a7181ca", size = 32674, upload-time = "2026-08-06T06:23:49.467Z" },
]
[[package]]
name = "grpcio"
version = "1.81.1"
@@ -1633,6 +1678,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:edb59506291b647a30884b1d51a599d605f40b20af4a7dc3d33786a47a31de60", size = 4928648, upload-time = "2026-06-11T12:46:17.823Z" },
]
[[package]]
name = "grpcio-status"
version = "1.81.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "grpcio" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/32/26/0aa9168c87882381fd810d140c279a2490ed6aee655f0515d6f56c5ca404/grpcio_status-1.81.1.tar.gz", hash = "sha256:9389a03e746017b10f0630c064289201458f3ce01f5d7ef4b0bebc1ef6cf82ad", size = 13923, upload-time = "2026-06-11T12:58:48.636Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/5e/5abfec5f7e89d3b7993d57cfb025ca5f968a2c18656d7fcda2b6919440b9/grpcio_status-1.81.1-py3-none-any.whl", hash = "sha256:08072fa9995f4a95c647fc6f4f85e2411573d00087bcabdf30f260114338f232", size = 14638, upload-time = "2026-06-11T12:58:31.982Z" },
]
[[package]]
name = "grpclib"
version = "0.4.9"
@@ -1788,6 +1847,15 @@ google = [
{ name = "httplib2" },
{ name = "pyasn1" },
]
google-chat = [
{ name = "google-api-python-client" },
{ name = "google-auth" },
{ name = "google-auth-httplib2" },
{ name = "google-auth-oauthlib" },
{ name = "google-cloud-pubsub" },
{ name = "httplib2" },
{ name = "pyasn1" },
]
hindsight = [
{ name = "hindsight-client" },
]
@@ -1950,10 +2018,15 @@ requires-dist = [
{ name = "firecrawl-anydoc", specifier = "==0.2.4" },
{ name = "firecrawl-py", marker = "extra == 'firecrawl'", specifier = "==4.17.0" },
{ name = "google-api-python-client", marker = "extra == 'google'", specifier = "==2.194.0" },
{ name = "google-api-python-client", marker = "extra == 'google-chat'", specifier = "==2.194.0" },
{ name = "google-auth", marker = "extra == 'google'", specifier = "==2.55.1" },
{ name = "google-auth", marker = "extra == 'google-chat'", specifier = "==2.55.1" },
{ name = "google-auth", marker = "extra == 'vertex'", specifier = "==2.55.1" },
{ name = "google-auth-httplib2", marker = "extra == 'google'", specifier = "==0.3.1" },
{ name = "google-auth-httplib2", marker = "extra == 'google-chat'", specifier = "==0.3.1" },
{ name = "google-auth-oauthlib", marker = "extra == 'google'", specifier = "==1.3.1" },
{ name = "google-auth-oauthlib", marker = "extra == 'google-chat'", specifier = "==1.3.1" },
{ name = "google-cloud-pubsub", marker = "extra == 'google-chat'", specifier = "==2.39.0" },
{ name = "hermes-agent", extras = ["acp"], marker = "extra == 'all'" },
{ name = "hermes-agent", extras = ["acp"], marker = "extra == 'termux'" },
{ name = "hermes-agent", extras = ["cron"], marker = "extra == 'all'" },
@@ -1976,6 +2049,7 @@ requires-dist = [
{ name = "hindsight-client", marker = "extra == 'hindsight'", specifier = "==0.6.1" },
{ name = "honcho-ai", marker = "extra == 'honcho'", specifier = "==2.2.0" },
{ name = "httplib2", marker = "extra == 'google'", specifier = "==0.32.0" },
{ name = "httplib2", marker = "extra == 'google-chat'", specifier = "==0.32.0" },
{ name = "httpx", extras = ["socks"], specifier = "==0.28.1" },
{ name = "httpx2", marker = "extra == 'computer-use'", specifier = "==2.7.0" },
{ name = "httpx2", marker = "extra == 'dev'", specifier = "==2.7.0" },
@@ -2009,6 +2083,7 @@ requires-dist = [
{ name = "ptyprocess", marker = "sys_platform != 'win32'", specifier = ">=0.7.0,<1" },
{ name = "pvporcupine", marker = "extra == 'wake'", specifier = "==4.0.3" },
{ name = "pyasn1", marker = "extra == 'google'", specifier = "==0.6.4" },
{ name = "pyasn1", marker = "extra == 'google-chat'", specifier = "==0.6.4" },
{ name = "pydantic", specifier = "==2.13.4" },
{ name = "pyjwt", extras = ["crypto"], specifier = "==2.13.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = "==9.1.1" },
@@ -2053,7 +2128,7 @@ requires-dist = [
{ name = "websockets", specifier = "==15.0.1" },
{ name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = "==1.2.4" },
]
provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "vercel", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "wecom", "tts-premium", "voice", "wake", "honcho", "supermemory", "mem0", "vision", "pty", "mcp", "nemo-relay", "homeassistant", "sms", "teams", "computer-use", "acp", "mistral", "otlp", "bedrock", "vertex", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "vercel", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "wecom", "tts-premium", "voice", "wake", "honcho", "supermemory", "mem0", "vision", "pty", "mcp", "nemo-relay", "homeassistant", "sms", "teams", "computer-use", "acp", "mistral", "otlp", "bedrock", "vertex", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "google-chat", "youtube", "web", "all"]
[[package]]
name = "hf-xet"
@@ -4197,7 +4272,7 @@ resolution-markers = [
"python_full_version < '3.12'",
]
dependencies = [
{ name = "numpy", marker = "python_full_version < '3.12'" },
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
wheels = [
@@ -4252,7 +4327,7 @@ resolution-markers = [
"python_full_version == '3.12.*'",
]
dependencies = [
{ name = "numpy", marker = "python_full_version >= '3.12'" },
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" }
wheels = [
@@ -4900,11 +4975,11 @@ name = "vercel-workers"
version = "0.0.25"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio", marker = "python_full_version >= '3.12'" },
{ name = "httpx", marker = "python_full_version >= '3.12'" },
{ name = "pydantic", marker = "python_full_version >= '3.12'" },
{ name = "python-dotenv", marker = "python_full_version >= '3.12'" },
{ name = "vercel", marker = "python_full_version >= '3.12'" },
{ name = "anyio" },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "vercel" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/df/04d37021ad7ca53b7599c313e411d91623c7a005c741f491d1eefb7a9f0c/vercel_workers-0.0.25.tar.gz", hash = "sha256:212ded01400b524be51d251df49f801caf115ad7d48cca7eb168cbeceda3def3", size = 64149, upload-time = "2026-06-20T19:26:27.177Z" }
wheels = [