diff --git a/SKALD.md b/SKALD.md index 7453015..8be4b84 100644 --- a/SKALD.md +++ b/SKALD.md @@ -130,9 +130,7 @@ Configurazione tecnica per l'attivazione del connector. ], "setup_instructions": [ "Install dependencies: pip install -r requirements.txt", - "Create secrets/google_oauth_client.json with {\"client_id\": \"...\", \"client_secret\": \"...\"}", - "Run: python3 gmail_oauth_setup.py (opens browser for OAuth)", - "Set GMAIL_CREDS_PATH env var or place token at secrets/gmail_creds.json" + "Run: python3 gmail_oauth_setup.py (optional, for standalone use — Skald handles OAuth)" ], "docs": [ { @@ -149,17 +147,14 @@ Configurazione tecnica per l'attivazione del connector. "https://www.googleapis.com/auth/gmail.labels" ], "deliver": { - "as": "file", + "as": "env", "format": "google_authorized_user", - "path": "{secrets}/gmail_creds.json" + "env": "GMAIL_CREDS_JSON" } }, "mcp_config": { "command": "python3", - "args": ["gmail_mcp_server.py"], - "env": { - "GMAIL_CREDS_PATH": "{secrets}/gmail_creds.json" - } + "args": ["gmail_mcp_server.py"] }, "homepage": "https://mail.google.com", "icon_small": "icon_sm.svg", @@ -233,7 +228,11 @@ Struttura che descrive come il connector gestisce l'autenticazione: // OAuth2 — provider SOLO slug (Skald risolve endpoint + client secrets) {"type": "oauth2", "provider": "google", "scopes": ["...", "..."]} -// OAuth2 con deliver (come Skald consegna la credenziale al processo MCP) +// OAuth2 con deliver (Skald inietta il JSON authorized_user via env var) +{"type": "oauth2", "provider": "google", "scopes": ["..."], + "deliver": {"as": "env", "format": "google_authorized_user", "env": "GMAIL_CREDS_JSON"}} + +// OAuth2 con deliver su file (legacy) {"type": "oauth2", "provider": "google", "scopes": ["..."], "deliver": {"as": "file", "format": "google_authorized_user", "path": "{secrets}/gmail_creds.json"}} @@ -253,6 +252,7 @@ Dichiara **come** Skald consegna la credenziale OAuth ottenuta al processo del s | `as` | ✅ | `"file"` (su disco) o `"env"` (variabile d'ambiente) | | `format` | ✅ | Nome della serializzazione — es. `"google_authorized_user"` (JSON Google che `from_authorized_user_file` legge), `"refresh_token"`, `"access_token"` | | `path` | solo `as=file` | Path con placeholder `{secrets}` (Skald lo espande a dir per-utente a runtime). DEVE matchare il path in `mcp_config.env`. | +| `env` | solo `as=env` | Nome della variabile d'ambiente in cui Skald inietta l'intero JSON authorized_user. **Non va dichiarata in `mcp_config.env`** — Skald la inietta a runtime. | Il feed NON contiene MAI: `client_id`, `client_secret`, `endpoint` URL, `redirect_uri`. Questi sono risolti lato Skald a partire dal nome del `provider`. @@ -439,10 +439,11 @@ sudo find /var/www/connectors.skaldagent.net/ -type f -exec chmod 644 {} \; | ID | Nome | Tipo | Scope | Auth | Verify | |----|------|------|-------|------|--------| | `tavily` | Tavily | `mcp_remote` | `global` | api_key (`{SECRET:tavilyApiKey}` in URL) | `verify.py` (HTTP probe `/search`) | -| `gmail` | Gmail | `mcp_local` | `user` | oauth2 (Google) + deliver: `file/google_authorized_user` | ⏳ Fase 2 — OAuth via loopback listener | -| `gcal` | Google Calendar | `mcp_local` | `user` | oauth2 (Google) + deliver: `file/google_authorized_user` | `verify.py` (creds load + API probe) | +| `gmail` | Gmail | `mcp_local` | `user` | oauth2 (Google) + deliver: `env/google_authorized_user` (via `GMAIL_CREDS_JSON`) | ⏳ Fase 2 — OAuth via loopback listener | +| `gcal` | Google Calendar | `mcp_local` | `user` | oauth2 (Google) + deliver: `env/google_authorized_user` (via `GCAL_CREDS_JSON`) | `verify.py` (creds load + API probe) | | `email` | Email (IMAP/SMTP) | `mcp_local` | `user` | password (env) | `verify.py` (IMAP+SMTP probe) | | `ssh` | SSH Remote Access | `mcp_local` | `user` | none (auth runtime per-alias) | — (nessun setup credential) | **Stato del verify-before-save in skald**: `email`, `tavily`, e `gcal` hanno `verify` completo (script + JSON output); `gmail` aspetta la Fase 2 (OAuth via loopback listener). +(script + JSON output); `gmail` aspetta la Fase 2 (OAuth via loopback listener). Un connector senza `verify` viene attivato senza test — vedi § Senza verify. diff --git a/connectors/connectors.json b/connectors/connectors.json index a67b374..36e12de 100644 --- a/connectors/connectors.json +++ b/connectors/connectors.json @@ -31,8 +31,8 @@ "files": [ { "path": "gmail_mcp_server.py", - "sha256": "a50d4da9621f7a4b092f4e3c5ae85dec5f466783372f78ca7e792046dc01c673", - "size": 46772 + "sha256": "53e6cdbc4ff40a095d29f7538ec23ac8fc10fb83cb28496bb28a83e7c49deaae", + "size": 47833 }, { "path": "gmail_oauth_setup.py", @@ -85,8 +85,8 @@ "files": [ { "path": "gcal_mcp_server.py", - "sha256": "0d1450ae552aba8035e608fa7785487459b9ecf0b05064c15fb5511d93ac92c1", - "size": 37210 + "sha256": "87b44a407feac90ed073997bd0a558c7002eec0c408d50c45da7d99011dd6107", + "size": 38203 }, { "path": "gcal_oauth_setup.py", @@ -95,8 +95,8 @@ }, { "path": "verify.py", - "sha256": "3bbc8c32bd320ed772972798371a39b03c3e52ce1dfd66a1493cda9e78e428e5", - "size": 2328 + "sha256": "d994ae435d8a5a3d9cdc85c8f73805a877c1a8818637b2912f6c31c693fdded4", + "size": 3429 }, { "path": "icon_sm.svg", diff --git a/connectors/gcal/connector.json b/connectors/gcal/connector.json index 7a2ce87..4355bb2 100644 --- a/connectors/gcal/connector.json +++ b/connectors/gcal/connector.json @@ -15,9 +15,7 @@ ], "setup_instructions": [ "Install dependencies: pip install -r requirements.txt", - "Create secrets/google_oauth_client.json with {\"client_id\": \"...\", \"client_secret\": \"...\"}", - "Run: python3 gcal_oauth_setup.py (opens browser for OAuth)", - "Set GOOGLE_CREDS_PATH env var or place token at secrets/google_creds.json" + "Run: python3 gcal_oauth_setup.py (optional, for standalone use — Skald handles OAuth)" ], "docs": [ { @@ -33,17 +31,14 @@ "https://www.googleapis.com/auth/calendar" ], "deliver": { - "as": "file", + "as": "env", "format": "google_authorized_user", - "path": "{secrets}/google_creds.json" + "env": "GCAL_CREDS_JSON" } }, "mcp_config": { "command": "python3", - "args": ["gcal_mcp_server.py"], - "env": { - "GOOGLE_CREDS_PATH": "{secrets}/google_creds.json" - } + "args": ["gcal_mcp_server.py"] }, "verify": { "command": "python3 verify.py", diff --git a/connectors/gcal/gcal_mcp_server.py b/connectors/gcal/gcal_mcp_server.py index 52f7003..5a8c263 100755 --- a/connectors/gcal/gcal_mcp_server.py +++ b/connectors/gcal/gcal_mcp_server.py @@ -11,14 +11,13 @@ Capabilities (callable as `mcp__gcal__`): delete_event — permanently delete an event respond_to_event — set RSVP / attendance response -Credentials are read from ./secrets/google_creds.json by default. -Override with GOOGLE_CREDS_PATH env var. +Skald mode: Skald injects credentials via GCAL_CREDS_JSON env var (authorized_user JSON). +Standalone mode: reads from GOOGLE_CREDS_PATH or ./secrets/google_creds.json. +Run scripts/gcal_oauth_setup.py to (re-)authenticate (standalone). Required OAuth scopes: https://www.googleapis.com/auth/calendar (or https://www.googleapis.com/auth/calendar.events for events-only) - -Run scripts/gcal_oauth_setup.py to (re-)authenticate. """ from __future__ import annotations @@ -138,7 +137,14 @@ def _persist_creds() -> None: def _build_service() -> Any: - """Build and return a Google Calendar service object, or None on failure.""" + """Build and return a Google Calendar service object, or None on failure. + + Credentials are loaded in priority order: + 1. GCAL_CREDS_JSON env var — full authorized_user JSON injected by Skald + (Credentials.from_authorized_user_info) + 2. GOOGLE_CREDS_PATH env var → file on disk (standalone/legacy) + 3. Default path ./secrets/google_creds.json (standalone use) + """ global _init_error, _creds, _creds_path try: from google.auth.transport.requests import Request @@ -149,25 +155,40 @@ def _build_service() -> Any: log(_init_error) return None - _creds_path = os.environ.get( - "GOOGLE_CREDS_PATH", - os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "secrets", "google_creds.json"), - ) + _SCOPES = [ + "https://www.googleapis.com/auth/calendar", + ] - if not os.path.exists(_creds_path): - _init_error = ( - f"Credentials file not found at {_creds_path}. " - "Run scripts/gcal_oauth_setup.py to authenticate, or set GOOGLE_CREDS_PATH." + raw = os.environ.get("GCAL_CREDS_JSON") + if raw: + try: + creds = Credentials.from_authorized_user_info(json.loads(raw), _SCOPES) + _creds_path = None # no file, persisted only in env + log("Credentials loaded from GCAL_CREDS_JSON env var.") + except Exception as e: + _init_error = f"Failed to load credentials from GCAL_CREDS_JSON: {e}" + log(_init_error) + return None + else: + _creds_path = os.environ.get( + "GOOGLE_CREDS_PATH", + os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "secrets", "google_creds.json"), ) - log(_init_error) - return None - try: - creds = Credentials.from_authorized_user_file(_creds_path) - except Exception as e: - _init_error = f"Failed to load credentials from {_creds_path}: {e}" - log(_init_error) - return None + if not os.path.exists(_creds_path): + _init_error = ( + f"Credentials not found. Set GCAL_CREDS_JSON env var (Skald mode) or " + f"place credentials file at {_creds_path} (standalone mode)." + ) + log(_init_error) + return None + + try: + creds = Credentials.from_authorized_user_file(_creds_path) + except Exception as e: + _init_error = f"Failed to load credentials from {_creds_path}: {e}" + log(_init_error) + return None # Publish creds globally so _persist_creds / _call can see them. _creds = creds @@ -176,7 +197,8 @@ def _build_service() -> Any: if creds.expired and creds.refresh_token: try: creds.refresh(Request()) - _persist_creds() + if _creds_path: + _persist_creds() log("Token refreshed and saved.") except Exception as e: log(f"Token refresh failed: {e}") @@ -188,7 +210,7 @@ def _build_service() -> Any: log(_init_error) return None - log(f"Calendar service built successfully (creds: {_creds_path})") + log(f"Calendar service built successfully (creds: env var or {_creds_path})") return service @@ -319,8 +341,8 @@ def _gcal_status(args: dict | None = None) -> str: if svc is None: return _status_report("❌", "NOT_CONFIGURED", "action needed", f"The Google Calendar service could not be built: {_init_error or 'unknown error'}.", - ["Run scripts/gcal_oauth_setup.py to authenticate and create secrets/google_creds.json.", - "Or set the GOOGLE_CREDS_PATH env var to point at an existing credentials file."]) + ["Run scripts/gcal_oauth_setup.py to authenticate (standalone).", + "For Skald mode, ensure GCAL_CREDS_JSON env var is set."]) # Step 2: live probe — refresh-on-auth-error is handled inside _call. try: diff --git a/connectors/gcal/verify.py b/connectors/gcal/verify.py index 80bfd06..55926ae 100644 --- a/connectors/gcal/verify.py +++ b/connectors/gcal/verify.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 """Verify Google Calendar OAuth credentials are valid. -Reads GOOGLE_CREDS_PATH env var (or falls back to secrets/google_creds.json), -loads the authorized_user credentials, and does one cheap API probe -(calendarList.list(maxResults=1)). +Priority: +1. GCAL_CREDS_JSON env var — authorized_user JSON injected by Skald. +2. GOOGLE_CREDS_PATH env var → file on disk (standalone). +3. Default path ./secrets/google_creds.json (standalone). + +Loads credentials and does one cheap API probe (calendarList.list(maxResults=1)). Output: single JSON object on stdout: {"ok": true, "message": "..."} @@ -17,40 +20,20 @@ import os import sys -def main() -> None: - creds_path = os.environ.get( - "GOOGLE_CREDS_PATH", - os.path.join(os.path.dirname(__file__), "secrets", "google_creds.json"), - ) - - if not os.path.exists(creds_path): - _fail(f"Credentials file not found at {creds_path}. Run gcal_oauth_setup.py first.") - return - +def _check_api(creds) -> None: + """Perform a live Calendar API probe with the given credentials.""" try: from google.auth.transport.requests import Request - from google.oauth2.credentials import Credentials from googleapiclient.discovery import build except ImportError as e: _fail(f"Missing dependencies: {e}. Install google-api-python-client and google-auth.") return - try: - creds = Credentials.from_authorized_user_file( - creds_path, - ["https://www.googleapis.com/auth/calendar"], - ) - except Exception as e: - _fail(f"Failed to load credentials: {e}") - return - if creds.expired and creds.refresh_token: try: creds.refresh(Request()) - with open(creds_path, "w") as f: - f.write(creds.to_json()) except Exception as e: - _fail(f"Token refresh failed: {e}. Re-run gcal_oauth_setup.py.") + _fail(f"Token refresh failed: {e}") return try: @@ -64,12 +47,60 @@ def main() -> None: _fail(f"API probe failed: {e}") +def main() -> None: + # Priority 1: env var with full JSON (Skald mode) + raw = os.environ.get("GCAL_CREDS_JSON") + if raw: + try: + from google.oauth2.credentials import Credentials + creds = Credentials.from_authorized_user_info( + json.loads(raw), + ["https://www.googleapis.com/auth/calendar"], + ) + _check_api(creds) + return + except ImportError as e: + _fail(f"Missing dependencies: {e}. Install google-api-python-client and google-auth.") + return + except Exception as e: + _fail(f"Failed to load credentials from GCAL_CREDS_JSON: {e}") + return + + # Priority 2/3: file-based (standalone use) + creds_path = os.environ.get( + "GOOGLE_CREDS_PATH", + os.path.join(os.path.dirname(__file__), "secrets", "google_creds.json"), + ) + + if not os.path.exists(creds_path): + _fail(f"Credentials not found. Set GCAL_CREDS_JSON env var (Skald mode) or " + f"place credentials file at {creds_path} (standalone mode).") + return + + try: + from google.oauth2.credentials import Credentials + creds = Credentials.from_authorized_user_file( + creds_path, + ["https://www.googleapis.com/auth/calendar"], + ) + except ImportError as e: + _fail(f"Missing dependencies: {e}. Install google-api-python-client and google-auth.") + return + except Exception as e: + _fail(f"Failed to load credentials: {e}") + return + + _check_api(creds) + + def _ok(message: str) -> None: json.dump({"ok": True, "message": message}, sys.stdout) + sys.stdout.write("\n") def _fail(message: str) -> None: json.dump({"ok": False, "message": message}, sys.stdout) + sys.stdout.write("\n") sys.exit(1) diff --git a/connectors/gmail/connector.json b/connectors/gmail/connector.json index ed84b4e..452fe97 100644 --- a/connectors/gmail/connector.json +++ b/connectors/gmail/connector.json @@ -16,9 +16,7 @@ ], "setup_instructions": [ "Install dependencies: pip install -r requirements.txt", - "Create secrets/google_oauth_client.json with {\"client_id\": \"...\", \"client_secret\": \"...\"}", - "Run: python3 gmail_oauth_setup.py (opens browser for OAuth)", - "Set GMAIL_CREDS_PATH env var or place token at secrets/gmail_creds.json" + "Run: python3 gmail_oauth_setup.py (optional, for standalone use — Skald handles OAuth)" ], "docs": [ { @@ -31,10 +29,7 @@ "command": "python3", "args": [ "gmail_mcp_server.py" - ], - "env": { - "GMAIL_CREDS_PATH": "{secrets}/gmail_creds.json" - } + ] }, "homepage": "https://mail.google.com", "icon_small": "icon_sm.svg", @@ -54,9 +49,9 @@ "https://www.googleapis.com/auth/gmail.labels" ], "deliver": { - "as": "file", + "as": "env", "format": "google_authorized_user", - "path": "{secrets}/gmail_creds.json" + "env": "GMAIL_CREDS_JSON" } } } \ No newline at end of file diff --git a/connectors/gmail/gmail_mcp_server.py b/connectors/gmail/gmail_mcp_server.py index a8adad5..0fbf9b9 100644 --- a/connectors/gmail/gmail_mcp_server.py +++ b/connectors/gmail/gmail_mcp_server.py @@ -15,10 +15,9 @@ Capabilities (callable as `mcp__gmail__`): Provides read, modify, and send access to Gmail via the Gmail API v1. -Credentials are read from ./secrets/gmail_creds.json by default. -Override with GMAIL_CREDS_PATH env var. - -Run scripts/gmail_oauth_setup.py first to generate the OAuth token. +Skald mode: Skald injects credentials via GMAIL_CREDS_JSON env var (authorized_user JSON). +Standalone mode: reads from GMAIL_CREDS_PATH or ./secrets/gmail_creds.json. +Run scripts/gmail_oauth_setup.py first to generate the OAuth token (standalone). """ from __future__ import annotations @@ -156,7 +155,14 @@ def _persist_creds() -> None: def _build_service() -> Any: - """Build and return a Gmail service object, or None on failure.""" + """Build and return a Gmail service object, or None on failure. + + Credentials are loaded in priority order: + 1. GMAIL_CREDS_JSON env var — full authorized_user JSON injected by Skald + (Credentials.from_authorized_user_info) + 2. GMAIL_CREDS_PATH env var → file on disk (standalone/legacy) + 3. Default path ./secrets/gmail_creds.json (standalone use) + """ global _init_error, _creds, _creds_path try: from google.auth.transport.requests import Request @@ -167,25 +173,41 @@ def _build_service() -> Any: log(_init_error) return None - _creds_path = os.environ.get( - "GMAIL_CREDS_PATH", - os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "secrets", "gmail_creds.json"), - ) + _SCOPES = [ + "https://www.googleapis.com/auth/gmail.modify", + "https://www.googleapis.com/auth/gmail.labels", + ] - if not os.path.exists(_creds_path): - _init_error = ( - f"Credentials file not found at {_creds_path}. " - "Run scripts/gmail_oauth_setup.py first, or set GMAIL_CREDS_PATH." + raw = os.environ.get("GMAIL_CREDS_JSON") + if raw: + try: + creds = Credentials.from_authorized_user_info(json.loads(raw), _SCOPES) + _creds_path = None # no file, persisted only in env + log("Credentials loaded from GMAIL_CREDS_JSON env var.") + except Exception as e: + _init_error = f"Failed to load credentials from GMAIL_CREDS_JSON: {e}" + log(_init_error) + return None + else: + _creds_path = os.environ.get( + "GMAIL_CREDS_PATH", + os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "secrets", "gmail_creds.json"), ) - log(_init_error) - return None - try: - creds = Credentials.from_authorized_user_file(_creds_path) - except Exception as e: - _init_error = f"Failed to load credentials from {_creds_path}: {e}" - log(_init_error) - return None + if not os.path.exists(_creds_path): + _init_error = ( + f"Credentials not found. Set GMAIL_CREDS_JSON env var (Skald mode) or " + f"place credentials file at {_creds_path} (standalone mode)." + ) + log(_init_error) + return None + + try: + creds = Credentials.from_authorized_user_file(_creds_path) + except Exception as e: + _init_error = f"Failed to load credentials from {_creds_path}: {e}" + log(_init_error) + return None # Publish creds globally so _persist_creds / _call can see them. _creds = creds @@ -195,10 +217,11 @@ def _build_service() -> Any: if not creds.valid: if creds.expired and creds.refresh_token: creds.refresh(Request()) - _persist_creds() + if _creds_path: + _persist_creds() log("Token refreshed and saved.") else: - _init_error = "Credentials invalid and cannot be refreshed. Re-run scripts/gmail_oauth_setup.py." + _init_error = "Credentials invalid and cannot be refreshed. Re-run OAuth setup." log(_init_error) return None except Exception as e: @@ -213,7 +236,7 @@ def _build_service() -> Any: log(_init_error) return None - log(f"Gmail service built successfully (creds: {_creds_path})") + log(f"Gmail service built successfully (creds: env var or {_creds_path})") return service @@ -450,8 +473,8 @@ def _gmail_status(args: dict | None = None) -> str: if svc is None: return _status_report("❌", "NOT_CONFIGURED", "action needed", f"The Gmail service could not be built: {_init_error or 'unknown error'}.", - ["Run scripts/gmail_oauth_setup.py to authenticate and create secrets/gmail_creds.json.", - "Or set the GMAIL_CREDS_PATH env var to point at an existing credentials file."]) + ["Run scripts/gmail_oauth_setup.py to authenticate (standalone).", + "For Skald mode, ensure GMAIL_CREDS_JSON env var is set."]) # Step 2: live probe — refresh-on-auth-error is handled inside _call. try: