{"repo":"NousResearch/hermes-agent","public_id":"16452","display_label":"google-workspace required_credential_files and OAuth file access in remote terminals","summary":"Three members relate to the google-workspace skill: two pull request titles about restoring required_credential_files in SKILL.md, and one bug report about being unable to read an OAuth file in remote terminals. The titles suggest a shared theme around credential file handling, but the link between the bug and the fix is inferred from titles alone.","subsystem":"skills","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":19886,"merged_at":"2026-05-04T19:43:14+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-04T19:43:14+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":16452,"pr_number":19886,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-04T19:43:14+00:00"}],"members":[{"kind":"issue","number":16452,"title":"[Bug]: google-workspace can't read oauth file in remote terminals","url":null,"state":"closed"},{"kind":"pr","number":16470,"title":"fix(google-workspace): restore required_credential_files in SKILL.md (#16452)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19886","number":19886}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":19886,"title":"fix(google-workspace): restore required_credential_files in SKILL.md","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["skills/productivity/google-workspace/SKILL.md","tests/skills/test_google_workspace_credential_files.py"],"fix_hunks":[{"file":"skills/productivity/google-workspace/SKILL.md","hunks":["@@ -1,9 +1,14 @@\n ---\n name: google-workspace\n description: \"Gmail, Calendar, Drive, Docs, Sheets via gws CLI or Python.\"\n-version: 1.0.0\n+version: 1.0.1\n author: Nous Research\n license: MIT\n+required_credential_files:\n+  - path: google_token.json\n+    description: Google OAuth2 token (created by setup script)\n+  - path: google_client_secret.json\n+    description: Google OAuth2 client credentials (downloaded from Google Cloud Console)\n metadata:\n   hermes:\n     tags: [Google, Gmail, Calendar, Drive, Sheets, Docs, Contacts, Email, OAuth]"]},{"file":"tests/skills/test_google_workspace_credential_files.py","hunks":["@@ -0,0 +1,102 @@\n+\"\"\"Regression test: google-workspace SKILL.md must declare required_credential_files.\n+\n+PR #9931 accidentally removed the required_credential_files header, which broke\n+credential file mounting in Docker/Modal remote backends (#16452). This test\n+prevents the regression from silently reappearing.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import os\n+from pathlib import Path\n+from unittest.mock import patch\n+\n+import pytest\n+\n+SKILL_MD = (\n+    Path(__file__).resolve().parents[2]\n+    / \"skills/productivity/google-workspace/SKILL.md\"\n+)\n+\n+_EXPECTED_PATHS = {\"google_token.json\", \"google_client_secret.json\"}\n+\n+\n+def _parse_frontmatter(content: str) -> dict:\n+    from agent.skill_utils import parse_frontmatter\n+\n+    fm, _ = parse_frontmatter(content)\n+    return fm\n+\n+\n+class TestGoogleWorkspaceCredentialFiles:\n+    def test_required_credential_files_present_in_skill_md(self):\n+        content = SKILL_MD.read_text(encoding=\"utf-8\")\n+        fm = _parse_frontmatter(content)\n+        entries = fm.get(\"required_credential_files\")\n+        assert entries, \"required_credential_files missing from google-workspace SKILL.md\"\n+        assert isinstance(entries, list), \"required_credential_files must be a list\"\n+        paths = {\n+            (e[\"path\"] if isinstance(e, dict) else e)\n+            for e in entries\n+        }\n+        assert _EXPECTED_PATHS <= paths, (\n+            f\"Missing entries in required_credential_files: [redacted:secret-assignment] - paths}\"\n+        )\n+\n+    def test_entries_are_registered_when_files_exist(self, tmp_path):\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir()\n+        (hermes_home / \"google_token.json\").write_text(\"{}\")\n+        (hermes_home / \"google_client_secret.json\").write_text(\"{}\")\n+\n+        from tools.credential_files import (\n+            clear_credential_files,\n+            get_credential_file_mounts,\n+            register_credential_files,\n+        )\n+\n+        clear_credential_files()\n+        try:\n+            content = SKILL_MD.read_text(encoding=\"utf-8\")\n+            fm = _parse_frontmatter(content)\n+            entries = fm.get(\"required_credential_files\", [])\n+\n+            with patch.dict(os.environ, {\"HERMES_HOME\": str(hermes_home)}):\n+                missing = register_credential_files(entries)\n+\n+            assert missing == [], f\"Unexpected missing files: {missing}\"\n+            mounts = get_credential_file_mounts()\n+            container_paths = {m[\"container_path\"] for m in mounts}\n+            assert \"/root/.hermes/google_token.json\" in container_paths\n+            assert \"/root/.hermes/google_client_secret.json\" in container_paths\n+        finally:\n+            clear_credential_files()\n+\n+    def test_missing_token_is_reported(self, tmp_path):\n+        \"\"\"google_token.json absent (first-time setup) — reported as missing, client secret still mounts.\"\"\"\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir()\n+        (hermes_home / \"google_client_secret.json\").write_text(\"{}\")\n+\n+        from tools.credential_files import (\n+            clear_credential_files,\n+            get_credential_file_mounts,\n+            register_credential_files,\n+        )\n+\n+        clear_credential_files()\n+        try:\n+            content = SKILL_MD.read_text(encoding=\"utf-8\")\n+            fm = _parse_frontmatter(content)\n+            entries = fm.get(\"required_credential_files\", [])\n+\n+            with patch.dict(os.environ, {\"HERMES_HOME\": str(hermes_home)}):\n+                missing = register_credential_files(entries)\n+\n+            assert \"google_token.json\" in missing\n+            mounts = get_credential_file_mounts()\n+            container_paths = {m[\"container_path\"] for m in mounts}\n+            assert \"/root/.hermes/google_client_secret.json\" in container_paths\n+            assert \"/root/.hermes/google_token.json\" not in container_paths\n+        finally:\n+            clear_credential_files()\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}