mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-27 01:11:40 +00:00
fix(google-workspace): normalize authorized user token writes
This commit is contained in:
parent
f726b9b843
commit
daef0519e9
5 changed files with 92 additions and 5 deletions
|
|
@ -47,6 +47,13 @@ SCOPES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_authorized_user_payload(payload: dict) -> dict:
|
||||||
|
normalized = dict(payload)
|
||||||
|
if not normalized.get("type"):
|
||||||
|
normalized["type"] = "authorized_user"
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
def _ensure_authenticated():
|
def _ensure_authenticated():
|
||||||
if not TOKEN_PATH.exists():
|
if not TOKEN_PATH.exists():
|
||||||
print("Not authenticated. Run the setup script first:", file=sys.stderr)
|
print("Not authenticated. Run the setup script first:", file=sys.stderr)
|
||||||
|
|
@ -170,7 +177,12 @@ def get_credentials():
|
||||||
creds = Credentials.from_authorized_user_file(str(TOKEN_PATH), _stored_token_scopes())
|
creds = Credentials.from_authorized_user_file(str(TOKEN_PATH), _stored_token_scopes())
|
||||||
if creds.expired and creds.refresh_token:
|
if creds.expired and creds.refresh_token:
|
||||||
creds.refresh(Request())
|
creds.refresh(Request())
|
||||||
TOKEN_PATH.write_text(creds.to_json())
|
TOKEN_PATH.write_text(
|
||||||
|
json.dumps(
|
||||||
|
_normalize_authorized_user_payload(json.loads(creds.to_json())),
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
if not creds.valid:
|
if not creds.valid:
|
||||||
print("Token is invalid. Re-run setup.", file=sys.stderr)
|
print("Token is invalid. Re-run setup.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ def get_token_path() -> Path:
|
||||||
return get_hermes_home() / "google_token.json"
|
return get_hermes_home() / "google_token.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_authorized_user_payload(payload: dict) -> dict:
|
||||||
|
normalized = dict(payload)
|
||||||
|
if not normalized.get("type"):
|
||||||
|
normalized["type"] = "authorized_user"
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
def refresh_token(token_data: dict) -> dict:
|
def refresh_token(token_data: dict) -> dict:
|
||||||
"""Refresh the access token using the refresh token."""
|
"""Refresh the access token using the refresh token."""
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
|
@ -55,7 +62,9 @@ def refresh_token(token_data: dict) -> dict:
|
||||||
tz=timezone.utc,
|
tz=timezone.utc,
|
||||||
).isoformat()
|
).isoformat()
|
||||||
|
|
||||||
get_token_path().write_text(json.dumps(token_data, indent=2))
|
get_token_path().write_text(
|
||||||
|
json.dumps(_normalize_authorized_user_payload(token_data), indent=2)
|
||||||
|
)
|
||||||
return token_data
|
return token_data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,13 @@ REQUIRED_PACKAGES = ["google-api-python-client", "google-auth-oauthlib", "google
|
||||||
REDIRECT_URI = "http://localhost:1"
|
REDIRECT_URI = "http://localhost:1"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_authorized_user_payload(payload: dict) -> dict:
|
||||||
|
normalized = dict(payload)
|
||||||
|
if not normalized.get("type"):
|
||||||
|
normalized["type"] = "authorized_user"
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
def _load_token_payload(path: Path = TOKEN_PATH) -> dict:
|
def _load_token_payload(path: Path = TOKEN_PATH) -> dict:
|
||||||
try:
|
try:
|
||||||
return json.loads(path.read_text())
|
return json.loads(path.read_text())
|
||||||
|
|
@ -151,7 +158,12 @@ def check_auth():
|
||||||
if creds.expired and creds.refresh_token:
|
if creds.expired and creds.refresh_token:
|
||||||
try:
|
try:
|
||||||
creds.refresh(Request())
|
creds.refresh(Request())
|
||||||
TOKEN_PATH.write_text(creds.to_json())
|
TOKEN_PATH.write_text(
|
||||||
|
json.dumps(
|
||||||
|
_normalize_authorized_user_payload(json.loads(creds.to_json())),
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
missing_scopes = _missing_scopes_from_payload(_load_token_payload(TOKEN_PATH))
|
missing_scopes = _missing_scopes_from_payload(_load_token_payload(TOKEN_PATH))
|
||||||
if missing_scopes:
|
if missing_scopes:
|
||||||
print(f"AUTHENTICATED (partial): Token refreshed but missing {len(missing_scopes)} scopes:")
|
print(f"AUTHENTICATED (partial): Token refreshed but missing {len(missing_scopes)} scopes:")
|
||||||
|
|
@ -313,7 +325,7 @@ def exchange_auth_code(code: str):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
creds = flow.credentials
|
creds = flow.credentials
|
||||||
token_payload = json.loads(creds.to_json())
|
token_payload = _normalize_authorized_user_payload(json.loads(creds.to_json()))
|
||||||
|
|
||||||
# Store only the scopes actually granted by the user, not what was requested.
|
# Store only the scopes actually granted by the user, not what was requested.
|
||||||
# creds.to_json() writes the requested scopes, which causes refresh to fail
|
# creds.to_json() writes the requested scopes, which causes refresh to fail
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,9 @@ class TestExchangeAuthCode:
|
||||||
assert flow.state == "saved-state"
|
assert flow.state == "saved-state"
|
||||||
assert flow.code_verifier == "saved-verifier"
|
assert flow.code_verifier == "saved-verifier"
|
||||||
assert flow.fetch_token_calls == [{"code": "4/test-auth-code"}]
|
assert flow.fetch_token_calls == [{"code": "4/test-auth-code"}]
|
||||||
assert json.loads(setup_module.TOKEN_PATH.read_text())["token"] == "access-token"
|
saved = json.loads(setup_module.TOKEN_PATH.read_text())
|
||||||
|
assert saved["token"] == "access-token"
|
||||||
|
assert saved["type"] == "authorized_user"
|
||||||
assert not setup_module.PENDING_AUTH_PATH.exists()
|
assert not setup_module.PENDING_AUTH_PATH.exists()
|
||||||
|
|
||||||
def test_extracts_code_from_redirect_url_and_checks_state(self, setup_module):
|
def test_extracts_code_from_redirect_url_and_checks_state(self, setup_module):
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ def test_bridge_refreshes_expired_token(bridge_module, tmp_path):
|
||||||
# Verify persisted
|
# Verify persisted
|
||||||
saved = json.loads(token_path.read_text())
|
saved = json.loads(token_path.read_text())
|
||||||
assert saved["token"] == "ya29.refreshed"
|
assert saved["token"] == "ya29.refreshed"
|
||||||
|
assert saved["type"] == "authorized_user"
|
||||||
|
|
||||||
|
|
||||||
def test_bridge_exits_on_missing_token(bridge_module):
|
def test_bridge_exits_on_missing_token(bridge_module):
|
||||||
|
|
@ -182,3 +183,54 @@ def test_api_calendar_list_respects_date_range(api_module):
|
||||||
params = json.loads(cmd[params_idx + 1])
|
params = json.loads(cmd[params_idx + 1])
|
||||||
assert params["timeMin"] == "2026-04-01T00:00:00Z"
|
assert params["timeMin"] == "2026-04-01T00:00:00Z"
|
||||||
assert params["timeMax"] == "2026-04-07T23:59:59Z"
|
assert params["timeMax"] == "2026-04-07T23:59:59Z"
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_get_credentials_refresh_persists_authorized_user_type(api_module, monkeypatch):
|
||||||
|
token_path = api_module.TOKEN_PATH
|
||||||
|
_write_token(token_path, token="ya29.old")
|
||||||
|
|
||||||
|
class FakeCredentials:
|
||||||
|
def __init__(self):
|
||||||
|
self.expired = True
|
||||||
|
self.refresh_token = "1//refresh"
|
||||||
|
self.valid = True
|
||||||
|
|
||||||
|
def refresh(self, request):
|
||||||
|
self.expired = False
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
return json.dumps({
|
||||||
|
"token": "ya29.refreshed",
|
||||||
|
"refresh_token": "1//refresh",
|
||||||
|
"client_id": "123.apps.googleusercontent.com",
|
||||||
|
"client_secret": "secret",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
})
|
||||||
|
|
||||||
|
class FakeCredentialsModule:
|
||||||
|
@staticmethod
|
||||||
|
def from_authorized_user_file(filename, scopes):
|
||||||
|
assert filename == str(token_path)
|
||||||
|
assert scopes == api_module.SCOPES
|
||||||
|
return FakeCredentials()
|
||||||
|
|
||||||
|
google_module = types.ModuleType("google")
|
||||||
|
oauth2_module = types.ModuleType("google.oauth2")
|
||||||
|
credentials_module = types.ModuleType("google.oauth2.credentials")
|
||||||
|
credentials_module.Credentials = FakeCredentialsModule
|
||||||
|
transport_module = types.ModuleType("google.auth.transport")
|
||||||
|
requests_module = types.ModuleType("google.auth.transport.requests")
|
||||||
|
requests_module.Request = lambda: object()
|
||||||
|
|
||||||
|
monkeypatch.setitem(sys.modules, "google", google_module)
|
||||||
|
monkeypatch.setitem(sys.modules, "google.oauth2", oauth2_module)
|
||||||
|
monkeypatch.setitem(sys.modules, "google.oauth2.credentials", credentials_module)
|
||||||
|
monkeypatch.setitem(sys.modules, "google.auth.transport", transport_module)
|
||||||
|
monkeypatch.setitem(sys.modules, "google.auth.transport.requests", requests_module)
|
||||||
|
|
||||||
|
creds = api_module.get_credentials()
|
||||||
|
|
||||||
|
saved = json.loads(token_path.read_text())
|
||||||
|
assert isinstance(creds, FakeCredentials)
|
||||||
|
assert saved["token"] == "ya29.refreshed"
|
||||||
|
assert saved["type"] == "authorized_user"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue