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>
This commit is contained in:
Eugeniusz Gilewski 2026-07-28 18:30:42 +02:00 committed by Teknium
parent e008b62a72
commit 64dd865912
10 changed files with 320 additions and 60 deletions

View File

@ -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:

View File

@ -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

View File

@ -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():

View File

@ -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",
]
]

View File

@ -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",
]
]

View File

@ -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)

View File

@ -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",),

30
uv.lock
View File

@ -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 = [

View File

@ -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:

View File

@ -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/<sanitized_email>.json`(文件系统权限是保护手段——与 SA 密钥文件采用相同模型)。每个 token 归属于唯一一位用户;撤销操作仅限于该用户。
- **用户 OAuth scope**:每用户附件流程*仅*请求 `chat.messages.create`——覆盖 `media.upload` 及后续 `messages.create` 所需的最小权限。token 以明文 JSON 形式持久化在 `~/.hermes/google_chat_user_tokens/<sanitized_email>.json`(文件系统权限是保护手段——与 SA 密钥文件采用相同模型)。每个 token 归属于唯一一位用户;撤销操作仅限于该用户。