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:
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 == []
|
||||
|
||||
@@ -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
91
uv.lock
generated
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user