From 64dd8659127df0e36817df6a7b4f2f182fd9cd80 Mon Sep 17 00:00:00 2001 From: Eugeniusz Gilewski Date: Tue, 28 Jul 2026 18:30:42 +0200 Subject: [PATCH] fix(deps): repair Google transitive security floors (#72108) Google API and authentication packages permit vulnerable httplib2 and pyasn1 transitives, while the Workspace and Google Chat runtime installers previously treated any importable version as sufficient. Existing environments could therefore remain vulnerable after the project dependency pins were repaired. Carry the fixed versions through the Google and Vertex extras, lazy feature requirements, lockfile, and both runtime installers. Route the documented Google Chat installation path through its maintained secure requirements instead of an unconstrained direct pip command. Detect stale distributions, install only unsatisfied requirements, and verify the result before continuing. Behavioral tests cover those repair invariants without freezing manifests, lockfiles, or complete package sets. Related #72108 Extracted from #72840 Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com> --- plugins/platforms/google_chat/oauth.py | 56 ++++++++---- pyproject.toml | 9 +- .../google-workspace/scripts/setup.py | 57 ++++++++---- .../test_google_chat_oauth_dependencies.py | 63 +++++++++++++ tests/skills/test_google_workspace_setup.py | 90 +++++++++++++++++++ tests/tools/test_lazy_deps.py | 53 +++++++++++ tools/lazy_deps.py | 11 ++- uv.lock | 30 ++++--- .../docs/user-guide/messaging/google_chat.md | 5 +- .../user-guide/messaging/google_chat.md | 6 +- 10 files changed, 320 insertions(+), 60 deletions(-) create mode 100644 tests/gateway/test_google_chat_oauth_dependencies.py create mode 100644 tests/skills/test_google_workspace_setup.py diff --git a/plugins/platforms/google_chat/oauth.py b/plugins/platforms/google_chat/oauth.py index 1b8c89cb2e4b4..65744d2570d55 100644 --- a/plugins/platforms/google_chat/oauth.py +++ b/plugins/platforms/google_chat/oauth.py @@ -65,9 +65,12 @@ import secrets import stat import subprocess import sys +from importlib.metadata import version as _distribution_version from pathlib import Path from typing import Any, List, Optional, Tuple +from packaging.requirements import Requirement + # Pin the legacy logger name so operator-side log filters keep matching # after the in-tree → plugin migration. See adapter.py for context. logger = logging.getLogger("gateway.platforms.google_chat_user_oauth") @@ -157,11 +160,15 @@ SCOPES: List[str] = [ "https://www.googleapis.com/auth/chat.messages.create", ] -# Pip packages required for the OAuth flow. +# Pip packages required by the Google Chat adapter and its OAuth flow. _REQUIRED_PACKAGES = [ - "google-api-python-client", - "google-auth-oauthlib", - "google-auth-httplib2", + "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", ] # Out-of-band redirect: Google deprecated the ``urn:ietf:wg:oauth:2.0:oob`` @@ -367,31 +374,44 @@ def _write_private_json(path: Path, data: Any) -> None: def _ensure_deps() -> None: - """Check deps available; install if not; exit on failure.""" - try: - import googleapiclient # noqa: F401 - import google_auth_oauthlib # noqa: F401 - except ImportError: - if not install_deps(): - sys.exit(1) + """Check exact dependency versions; install if stale; exit on failure.""" + if _missing_required_packages() and not install_deps(): + sys.exit(1) + + +def _missing_required_packages() -> List[str]: + """Return exact requirements absent or stale in this interpreter.""" + missing = [] + for spec in _REQUIRED_PACKAGES: + requirement = Requirement(spec) + try: + installed = _distribution_version(requirement.name) + satisfied = requirement.specifier.contains(installed, prereleases=True) + except Exception: + satisfied = False + if not satisfied: + missing.append(spec) + return missing def install_deps() -> bool: - try: - import googleapiclient # noqa: F401 - import google_auth_oauthlib # noqa: F401 + missing = _missing_required_packages() + if not missing: print("Dependencies already installed.") return True - except ImportError: - pass - print("Installing Google Chat OAuth dependencies...") + print("Installing Google Chat dependencies...") try: from hermes_cli.tools_config import _pip_install - result = _pip_install(["--quiet"] + _REQUIRED_PACKAGES) + result = _pip_install(["--quiet"] + missing) if result.returncode != 0: raise RuntimeError((result.stderr or "install failed").strip()[:300]) + remaining = _missing_required_packages() + if remaining: + raise RuntimeError( + "dependencies remain stale after install: " + " ".join(remaining) + ) print("Dependencies installed.") return True except Exception as exc: diff --git a/pyproject.toml b/pyproject.toml index aaa49c78d16a4..35d4949a531d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -286,12 +286,15 @@ google = [ # and packagers ship them without hitting runtime `pip install` paths that # fail in environments without pip (e.g. Nix-managed Python). "google-api-python-client==2.194.0", + "google-auth==2.55.1", "google-auth-oauthlib==1.3.1", "google-auth-httplib2==0.3.1", - # Transitive via google-api-python-client/google-auth-httplib2; keep explicit - # so fresh google-extra installs do not resolve vulnerable httplib2 0.31.2 - # (GHSA-j5g9-f88f-gfj3 decompression bomb DoS). + # The Google SDKs permit older vulnerable transitives, so unlocked installs + # must carry the same fixed floors as uv.lock and the runtime installers: + # httplib2 0.32.0 (GHSA-j5g9-f88f-gfj3 decompression bomb DoS), + # pyasn1 0.6.4, google-auth 2.55.1. "httplib2==0.32.0", + "pyasn1==0.6.4", ] youtube = [ # Required by skills/media/youtube-content and diff --git a/skills/productivity/google-workspace/scripts/setup.py b/skills/productivity/google-workspace/scripts/setup.py index 803d3d34fe132..375fcd9737dc9 100644 --- a/skills/productivity/google-workspace/scripts/setup.py +++ b/skills/productivity/google-workspace/scripts/setup.py @@ -29,6 +29,7 @@ import os import shutil import subprocess import sys +from importlib.metadata import version as _distribution_version from pathlib import Path # Ensure sibling modules (_hermes_home) are importable when run standalone. @@ -56,14 +57,17 @@ SCOPES = [ # Exact pins: keep in sync with pyproject.toml [project.optional-dependencies].google # and tools/lazy_deps.py LAZY_DEPS['skill.google_workspace']. -# Pinning all three protects against version drift and ensures the httplib2 -# GHSA-j5g9-f88f-gfj3 security fix is honoured regardless of install path. +# Pinning all protects against version drift and ensures the security floors +# (httplib2 GHSA-j5g9-f88f-gfj3, stale pyasn1/google-auth) are honoured +# regardless of install path. REQUIRED_PACKAGES = [ "google-api-python-client==2.194.0", + "google-auth==2.55.1", "google-auth-oauthlib==1.3.1", "google-auth-httplib2==0.3.1", # GHSA-j5g9-f88f-gfj3 — Decompression Bomb DoS via unbounded gzip/deflate "httplib2==0.32.0", + "pyasn1==0.6.4", ] # OAuth redirect for "out of band" manual code copy flow. @@ -103,24 +107,43 @@ def _format_missing_scopes(missing_scopes: list[str]) -> str: ) +def _missing_required_packages() -> list[str]: + """Return exact requirements absent or stale in this interpreter. + + All REQUIRED_PACKAGES entries are exact ``name==version`` pins, so a + direct version comparison is sufficient — no ``packaging`` dependency + needed in this standalone script. + """ + missing = [] + for spec in REQUIRED_PACKAGES: + name, _, wanted = spec.partition("==") + try: + if _distribution_version(name) != wanted: + missing.append(spec) + except Exception: + missing.append(spec) + return missing + + def install_deps(): - """Install Google API packages if missing. Returns True on success.""" - try: - import googleapiclient # noqa: F401 - import google_auth_oauthlib # noqa: F401 + """Install missing or stale Google API packages. Returns True on success.""" + missing = _missing_required_packages() + if not missing: print("Dependencies already installed.") return True - except ImportError: - pass print("Installing Google API dependencies...") # First choice: pip in the current interpreter. Works for most installs. try: subprocess.check_call( - [sys.executable, "-m", "pip", "install", "--quiet"] + REQUIRED_PACKAGES, + [sys.executable, "-m", "pip", "install", "--quiet"] + missing, stdout=subprocess.DEVNULL, ) + remaining = _missing_required_packages() + if remaining: + print(f"ERROR: Dependencies remain stale after pip install: {' '.join(remaining)}") + return False print("Dependencies installed.") return True except subprocess.CalledProcessError as e: @@ -136,9 +159,13 @@ def install_deps(): try: subprocess.check_call( [uv, "pip", "install", "--python", sys.executable, "--quiet"] - + REQUIRED_PACKAGES, + + missing, stdout=subprocess.DEVNULL, ) + remaining = _missing_required_packages() + if remaining: + print(f"ERROR: Dependencies remain stale after uv install: {' '.join(remaining)}") + return False print("Dependencies installed.") return True except subprocess.CalledProcessError as e: @@ -157,13 +184,9 @@ def install_deps(): def _ensure_deps(): - """Check deps are available, install if not, exit on failure.""" - try: - import googleapiclient # noqa: F401 - import google_auth_oauthlib # noqa: F401 - except ImportError: - if not install_deps(): - sys.exit(1) + """Check exact dependency versions, install if stale, exit on failure.""" + if _missing_required_packages() and not install_deps(): + sys.exit(1) def check_auth_live(): diff --git a/tests/gateway/test_google_chat_oauth_dependencies.py b/tests/gateway/test_google_chat_oauth_dependencies.py new file mode 100644 index 0000000000000..43b4227b2c45a --- /dev/null +++ b/tests/gateway/test_google_chat_oauth_dependencies.py @@ -0,0 +1,63 @@ +"""Security-floor tests for the Google Chat runtime installer.""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError +from types import SimpleNamespace + +from plugins.platforms.google_chat import oauth + + +def test_stale_google_transitives_are_reported_missing(monkeypatch): + installed = { + "google-cloud-pubsub": "2.39.0", + "google-api-python-client": "2.194.0", + "google-auth": "2.55.0", + "google-auth-oauthlib": "1.3.1", + "google-auth-httplib2": "0.3.1", + "httplib2": "0.31.2", + "pyasn1": "0.6.3", + } + + def fake_version(name): + try: + return installed[name] + except KeyError: + raise PackageNotFoundError(name) from None + + monkeypatch.setattr(oauth, "_distribution_version", fake_version) + + assert oauth._missing_required_packages() == [ + "google-auth==2.55.1", + "httplib2==0.32.0", + "pyasn1==0.6.4", + ] + + +def test_installer_repairs_stale_transitives(monkeypatch): + states = iter( + [ + [ + "google-auth==2.55.1", + "httplib2==0.32.0", + "pyasn1==0.6.4", + ], + [], + ] + ) + monkeypatch.setattr(oauth, "_missing_required_packages", lambda: next(states)) + calls = [] + monkeypatch.setattr( + "hermes_cli.tools_config._pip_install", + lambda argv: 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", + ] + ] diff --git a/tests/skills/test_google_workspace_setup.py b/tests/skills/test_google_workspace_setup.py new file mode 100644 index 0000000000000..385a68fb6c61f --- /dev/null +++ b/tests/skills/test_google_workspace_setup.py @@ -0,0 +1,90 @@ +"""Security-floor tests for the Google Workspace runtime installer.""" + +from __future__ import annotations + +import importlib.util +from importlib.metadata import PackageNotFoundError +from pathlib import Path + +import pytest + + +SETUP_PATH = ( + Path(__file__).resolve().parents[2] + / "skills/productivity/google-workspace/scripts/setup.py" +) + + +@pytest.fixture() +def setup_module(): + spec = importlib.util.spec_from_file_location( + "test_google_workspace_setup_module", + SETUP_PATH, + ) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_stale_google_transitives_are_reported_missing(setup_module, monkeypatch): + installed = { + "google-api-python-client": "2.194.0", + "google-auth": "2.55.0", + "google-auth-oauthlib": "1.3.1", + "google-auth-httplib2": "0.3.1", + "httplib2": "0.31.2", + "pyasn1": "0.6.3", + } + + def fake_version(name): + try: + return installed[name] + except KeyError: + raise PackageNotFoundError(name) from None + + monkeypatch.setattr(setup_module, "_distribution_version", fake_version) + + assert setup_module._missing_required_packages() == [ + "google-auth==2.55.1", + "httplib2==0.32.0", + "pyasn1==0.6.4", + ] + + +def test_installer_repairs_stale_transitives(setup_module, monkeypatch): + states = iter( + [ + [ + "google-auth==2.55.1", + "httplib2==0.32.0", + "pyasn1==0.6.4", + ], + [], + ] + ) + monkeypatch.setattr( + setup_module, + "_missing_required_packages", + lambda: next(states), + ) + calls = [] + monkeypatch.setattr( + setup_module.subprocess, + "check_call", + lambda argv, **kwargs: calls.append(argv), + ) + + assert setup_module.install_deps() is True + assert calls == [ + [ + setup_module.sys.executable, + "-m", + "pip", + "install", + "--quiet", + "google-auth==2.55.1", + "httplib2==0.32.0", + "pyasn1==0.6.4", + ] + ] diff --git a/tests/tools/test_lazy_deps.py b/tests/tools/test_lazy_deps.py index 9316d9ef90553..61b1dcb790b2e 100644 --- a/tests/tools/test_lazy_deps.py +++ b/tests/tools/test_lazy_deps.py @@ -225,6 +225,59 @@ class TestIsSatisfiedVersionAware: assert ld._is_satisfied(spec) is True assert ld.feature_missing("tool.trace_upload") == () + @pytest.mark.parametrize( + ("feature", "installed_versions", "expected_repairs"), + [ + ( + "skill.google_workspace", + { + "google-api-python-client": "2.194.0", + "google-auth": "2.55.0", + "google-auth-oauthlib": "1.3.1", + "google-auth-httplib2": "0.3.1", + "httplib2": "0.31.2", + "pyasn1": "0.6.3", + }, + ( + "google-auth==2.55.1", + "httplib2==0.32.0", + "pyasn1==0.6.4", + ), + ), + ( + "provider.vertex", + { + "google-auth": "2.55.1", + "pyasn1": "0.6.3", + }, + ("pyasn1==0.6.4",), + ), + ], + ) + def test_google_features_repair_stale_transitives( + self, + monkeypatch, + feature, + installed_versions, + expected_repairs, + ): + self._fake_version(monkeypatch, installed_versions) + monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: True) + installed = [] + + def fake_install(specs, **kwargs): + installed.extend(specs) + for spec in specs: + package, wanted = spec.split("==", 1) + installed_versions[package] = wanted + return ld._InstallResult(True, "ok", "") + + monkeypatch.setattr(ld, "_venv_pip_install", fake_install) + + ld.ensure(feature, prompt=False) + + assert tuple(installed) == expected_repairs + # --------------------------------------------------------------------------- # active_features + refresh_active_features (Piece A — hermes update wiring) diff --git a/tools/lazy_deps.py b/tools/lazy_deps.py index 29dde793f60fe..86332391043de 100644 --- a/tools/lazy_deps.py +++ b/tools/lazy_deps.py @@ -104,7 +104,10 @@ LAZY_DEPS: dict[str, tuple[str, ...]] = { # Google Vertex AI provider — OAuth2 token minting for the Gemini # OpenAI-compatible endpoint. Only loaded when provider=vertex is selected; # google-auth is NOT in [all] so plain installs don't carry it. - "provider.vertex": ("google-auth==2.55.1",), + "provider.vertex": ( + "google-auth==2.55.1", + "pyasn1==0.6.4", + ), # Microsoft Foundry — Entra ID auth (managed identity, workload identity, # service principal, az login, VS Code, azd, PowerShell). Only loaded # when model.auth_mode=entra_id is selected; key-based azure-foundry @@ -257,12 +260,14 @@ LAZY_DEPS: dict[str, tuple[str, ...]] = { # ─── Skills ──────────────────────────────────────────────────────────── "skill.google_workspace": ( "google-api-python-client==2.194.0", + "google-auth==2.55.1", "google-auth-oauthlib==1.3.1", "google-auth-httplib2==0.3.1", # Transitive via google-api-python-client/google-auth-httplib2; keep explicit - # so lazy installs do not resolve vulnerable httplib2 0.31.2 - # (GHSA-j5g9-f88f-gfj3 decompression bomb DoS). + # so lazy installs do not resolve vulnerable transitives: httplib2 0.31.2 + # (GHSA-j5g9-f88f-gfj3 decompression bomb DoS), stale pyasn1/google-auth. "httplib2==0.32.0", + "pyasn1==0.6.4", ), "skill.youtube": ("youtube-transcript-api==1.2.4",), diff --git a/uv.lock b/uv.lock index 4097be434e394..b6f906e4719b9 100644 --- a/uv.lock +++ b/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ ] [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-07-18T05:39:53.300543645Z" exclude-newer-span = "P14D" [options.exclude-newer-package] @@ -1458,9 +1458,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424, upload-time = "2026-06-26T18:20:31.469Z" }, { url = "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523, upload-time = "2026-06-26T19:07:08.859Z" }, { url = "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315, upload-time = "2026-06-26T19:10:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a0/68afd1ebad40db87dac0a28ffa120726b98bf9c7c40c481b0f63c105d298/greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb", size = 626155, upload-time = "2026-06-26T19:24:14.44Z" }, { url = "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381, upload-time = "2026-06-26T18:32:16.077Z" }, - { url = "https://files.pythonhosted.org/packages/07/7f/e327d912239ec4b3b49999e3967389bcf1ee8722b9ee9194d2752ecd558a/greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c", size = 421083, upload-time = "2026-06-26T19:25:35.804Z" }, { url = "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771, upload-time = "2026-06-26T19:09:01.537Z" }, { url = "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048, upload-time = "2026-06-26T18:31:42.996Z" }, { url = "https://files.pythonhosted.org/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137, upload-time = "2026-06-26T18:23:21.664Z" }, @@ -1468,9 +1466,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, - { url = "https://files.pythonhosted.org/packages/fb/fb/d97dc261209c80744b7c8132693a30d70ec6e7315e632cb0a10b3fec94dd/greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23", size = 622351, upload-time = "2026-06-26T19:24:16.32Z" }, { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/e5fee13cbbd0e8de312d9a146584b8a51891c68847330ef9dc8b5109d23f/greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c", size = 425395, upload-time = "2026-06-26T19:25:37.144Z" }, { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, @@ -1478,9 +1474,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/2f5b1adf256d039f5dab8005de8d3d7ad2b0070a3219c0e036b3fbfeb440/greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1", size = 625580, upload-time = "2026-06-26T19:24:18.344Z" }, { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/ab7fc9e543e44d6879b0a6ef9a4b2188940fd180cc65d6f646883ddf7201/greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda", size = 427933, upload-time = "2026-06-26T19:25:38.219Z" }, { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, @@ -1611,10 +1605,12 @@ all = [ { name = "aiohttp" }, { name = "fastapi" }, { name = "google-api-python-client" }, + { name = "google-auth" }, { name = "google-auth-httplib2" }, { name = "google-auth-oauthlib" }, { name = "httplib2" }, { name = "mcp" }, + { name = "pyasn1" }, { name = "python-multipart" }, { name = "simple-term-menu" }, { name = "starlette" }, @@ -1673,9 +1669,11 @@ firecrawl = [ ] google = [ { name = "google-api-python-client" }, + { name = "google-auth" }, { name = "google-auth-httplib2" }, { name = "google-auth-oauthlib" }, { name = "httplib2" }, + { name = "pyasn1" }, ] hindsight = [ { name = "hindsight-client" }, @@ -1750,11 +1748,13 @@ termux-all = [ { name = "aiohttp" }, { name = "fastapi" }, { name = "google-api-python-client" }, + { name = "google-auth" }, { name = "google-auth-httplib2" }, { name = "google-auth-oauthlib" }, { name = "honcho-ai" }, { name = "httplib2" }, { name = "mcp" }, + { name = "pyasn1" }, { name = "python-multipart" }, { name = "python-telegram-bot", extra = ["webhooks"] }, { name = "simple-term-menu" }, @@ -1835,6 +1835,7 @@ requires-dist = [ { name = "fire", specifier = "==0.7.1" }, { name = "firecrawl-py", marker = "extra == 'firecrawl'", specifier = "==4.17.0" }, { name = "google-api-python-client", marker = "extra == 'google'", specifier = "==2.194.0" }, + { name = "google-auth", marker = "extra == 'google'", 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-oauthlib", marker = "extra == 'google'", specifier = "==1.3.1" }, @@ -1890,6 +1891,7 @@ requires-dist = [ { name = "psutil", specifier = "==7.2.2" }, { 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 = "pydantic", specifier = "==2.13.4" }, { name = "pyjwt", extras = ["crypto"], specifier = "==2.13.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==9.1.1" }, @@ -3995,7 +3997,7 @@ resolution-markers = [ "python_full_version < '3.12'", ] dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "python_full_version < '3.12'" }, ] 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 = [ @@ -4050,7 +4052,7 @@ resolution-markers = [ "python_full_version == '3.12.*'", ] dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "python_full_version >= '3.12'" }, ] 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 = [ @@ -4689,11 +4691,11 @@ name = "vercel-workers" version = "0.0.25" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "vercel" }, + { 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'" }, ] 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 = [ diff --git a/website/docs/user-guide/messaging/google_chat.md b/website/docs/user-guide/messaging/google_chat.md index 881fac6acd218..e613331a4de8a 100644 --- a/website/docs/user-guide/messaging/google_chat.md +++ b/website/docs/user-guide/messaging/google_chat.md @@ -166,10 +166,11 @@ GOOGLE_CHAT_MAX_BYTES=16777216 # 16 MiB — cap on in-flight me The project ID also falls back to `GOOGLE_CLOUD_PROJECT`, and the SA path falls back to `GOOGLE_APPLICATION_CREDENTIALS` — use whichever convention you prefer. -Install the dependencies the Google Chat adapter needs (no Hermes extra is currently published — install them directly): +Install the Google Chat adapter dependencies through its maintained installer. +It applies the same pinned security floors used by the runtime checks: ```bash -pip install google-cloud-pubsub google-api-python-client google-auth google-auth-oauthlib +python -m plugins.platforms.google_chat.oauth --install-deps ``` Start the gateway: diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/google_chat.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/google_chat.md index 98c5585b6b3d5..722cf35fccefe 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/google_chat.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/google_chat.md @@ -138,10 +138,10 @@ GOOGLE_CHAT_MAX_BYTES=16777216 # 16 MiB — 在途消息字节 项目 ID 也可回退到 `GOOGLE_CLOUD_PROJECT`,SA 路径可回退到 `GOOGLE_APPLICATION_CREDENTIALS`——使用你偏好的约定即可。 -安装 Google Chat 适配器所需的依赖(目前没有发布 Hermes extra,请直接安装): +通过适配器维护的安装程序安装 Google Chat 依赖。该程序会应用与运行时检查相同的固定安全版本: ```bash -pip install google-cloud-pubsub google-api-python-client google-auth google-auth-oauthlib +python -m plugins.platforms.google_chat.oauth --install-deps ``` 启动 gateway(网关): @@ -278,4 +278,4 @@ auth code 是一次性的且有效期很短(通常几分钟)。发送 `/setu - **附件下载保护**:Hermes 只会将 SA bearer token 附加到主机名匹配 Google 自有域名短名单的 URL(`googleapis.com`、`drive.google.com`、`lh[3-6].googleusercontent.com` 等)。其他主机在发起 HTTP 请求前即被拒绝,以防范 SSRF 场景——即精心构造的事件将 bearer token 重定向到 GCE 元数据服务。 - **脱敏处理**:Service Account 邮箱、订阅路径和 topic 路径会被 `agent/redact.py` 从日志输出中剥离。调试信封转储(`GOOGLE_CHAT_DEBUG_RAW=1`)经过同一脱敏过滤器,以 DEBUG 级别记录。 - **合规性**:如果你计划将此机器人接入受监管的 Workspace(任何有数据驻留或 AI 治理政策的环境),请在首次安装前获得相应审批。 -- **用户 OAuth scope**:每用户附件流程*仅*请求 `chat.messages.create`——覆盖 `media.upload` 及后续 `messages.create` 所需的最小权限。token 以明文 JSON 形式持久化在 `~/.hermes/google_chat_user_tokens/.json`(文件系统权限是保护手段——与 SA 密钥文件采用相同模型)。每个 token 归属于唯一一位用户;撤销操作仅限于该用户。 \ No newline at end of file +- **用户 OAuth scope**:每用户附件流程*仅*请求 `chat.messages.create`——覆盖 `media.upload` 及后续 `messages.create` 所需的最小权限。token 以明文 JSON 形式持久化在 `~/.hermes/google_chat_user_tokens/.json`(文件系统权限是保护手段——与 SA 密钥文件采用相同模型)。每个 token 归属于唯一一位用户;撤销操作仅限于该用户。