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 归属于唯一一位用户;撤销操作仅限于该用户。