""" opax_mcp_client.py — REST-klient for opax-mcp Cloud Run service. Kopiert inn i core-logic/ slik at Vertex AI Agent Engine finn den ved deploy. Auth-lag: 1. Cloud Run IAM : Authorization: Bearer (automatisk) 2. Tool-level : X-MCP-Secret: (frå env) URL: https://opax-mcp-zjbqp3prqq-uc.a.run.app """ import os import httpx import google.auth import google.auth.transport.requests OPAX_MCP_URL = os.environ.get("MCP_SERVER_URL", "https://opax-mcp-zjbqp3prqq-uc.a.run.app") MCP_SECRET = os.environ.get("MCP_SECRET", "") def _identity_token() -> str: metadata_url = ( "http://metadata.google.internal/computeMetadata/v1/instance" f"/service-accounts/default/identity?audience={OPAX_MCP_URL}&format=full" ) try: resp = httpx.get(metadata_url, headers={"Metadata-Flavor": "Google"}, timeout=5) if resp.status_code == 200 and resp.text.strip(): return resp.text.strip() except Exception: pass credentials, _ = google.auth.default() credentials.refresh(google.auth.transport.requests.Request()) return credentials.token def _headers() -> dict: return { "Authorization": f"Bearer {_identity_token()}", "X-MCP-Secret": MCP_SECRET, "Content-Type": "application/json", } def call_tool(tool: str, params: dict = None) -> dict: resp = httpx.post( f"{OPAX_MCP_URL}/tools/call", headers=_headers(), json={"tool": tool, "params": params or {}}, timeout=30, ) resp.raise_for_status() return resp.json() # --------------------------------------------------------------------------- # Billing # --------------------------------------------------------------------------- def get_billing_summary() -> dict: """Hent billing-oversikt for OSVauco (GCP-kostnader, token-forbruk).""" return call_tool("get_billing_summary") def get_billing_forecast() -> dict: """Hent token-estimat og kostnadsframskriving.""" return call_tool("get_billing_forecast") def get_billing_credits() -> dict: """Hent gjenværende GCP-kreditter og burn-rate.""" return call_tool("get_billing_credits") def get_billing_anomalies() -> dict: """Sjekk for kostnadanomalier og uventede spiker.""" return call_tool("get_billing_anomalies") def get_billing_history() -> dict: """Hent historisk kostnadsdata (90 dagar).""" return call_tool("get_billing_history") def get_billing_budget() -> dict: """Hent gjeldande budsjett for OSVauco.""" return call_tool("get_billing_budget") def set_billing_budget(amount: float) -> dict: """Sett nytt budsjettgrense i NOK/USD. amount er beløpet.""" return call_tool("set_billing_budget", {"amount": amount}) # --------------------------------------------------------------------------- # Onboarding # --------------------------------------------------------------------------- def create_invite(email: str, company: str = "", tier: str = "starter") -> dict: """Opprett invite-link for ny kunde. tier: starter | guard | shield | enterprise.""" return call_tool("create_invite", {"email": email, "company": company, "tier": tier}) def list_customers() -> dict: """List alle onboardede kunder og systemtilstand.""" return call_tool("list_customers") # --------------------------------------------------------------------------- # Notify # --------------------------------------------------------------------------- def send_webhook(message: str, url: str = "", title: str = "OPAX varsel") -> dict: """Send webhook-varsling til Slack/Teams/Discord. url er valgfri override.""" return call_tool("send_webhook", {"message": message, "url": url, "title": title}) def send_email(to: str, subject: str, body: str = "") -> dict: """Send e-post via SendGrid. to er mottaker-adresse.""" return call_tool("send_email", {"to": to, "subject": subject, "body": body}) def send_sms(to: str, message: str, tier: str = "guard") -> dict: """Send SMS via Twilio. Krev Guard+-tier. to er telefonnummer med landkode.""" return call_tool("send_sms", {"to": to, "message": message, "tier": tier}) def get_notify_channels() -> dict: """List konfigurerte varslingskanalar (e-post, SMS, webhook).""" return call_tool("get_notify_channels") # --------------------------------------------------------------------------- # Agents # --------------------------------------------------------------------------- def run_jason(prompt: str) -> dict: """Kall Jason-agenten med ein prompt via OPAX /run.""" return call_tool("run_jason", {"prompt": prompt}) def run_emma(prompt: str) -> dict: """Kall Emma-agenten (Gemma lokal) med ein prompt via OPAX /emma.""" return call_tool("run_emma", {"prompt": prompt}) # --------------------------------------------------------------------------- # Platform # --------------------------------------------------------------------------- def get_health() -> dict: """Sjekk helsestatus for OPAX-plattformen.""" return call_tool("get_health") def get_build_status() -> dict: """Hent status på siste Cloud Build-kjøring.""" return call_tool("get_build_status") def get_state() -> dict: """Hent gjeldande systemtilstand og agent-statistikk for OSVauco.""" return call_tool("get_state") def get_telemetry() -> dict: """Hent token-forbruk og ytingsdata per modul.""" return call_tool("get_telemetry") def run_terminal(command: str) -> dict: """Kjør whitelist-kommando i OPAX terminal. Gyldige: health, billing, build, logs, help.""" return call_tool("run_terminal", {"command": command}) # --------------------------------------------------------------------------- # Gitea (repo-tools) # --------------------------------------------------------------------------- def list_commits(limit: int = 10) -> dict: """List siste commits i OSVauco-repoet på Gitea.""" return call_tool("list_commits", {"limit": limit}) def get_file(path: str) -> dict: """Hent innhald i ein fil frå Gitea-repoet. path er relativ til rot.""" return call_tool("get_file", {"path": path}) def list_open_issues() -> dict: """List alle opne issues i OSVauco Gitea-repoet.""" return call_tool("list_open_issues", {}) def create_issue(title: str, body: str = "") -> dict: """Opprett nytt issue i Gitea-repoet.""" return call_tool("create_issue", {"title": title, "body": body}) def push_file(path: str, content: str, message: str = "", sha: str = "") -> dict: """Push/oppdater ein fil i Gitea. sha krevst ved oppdatering av eksisterande fil.""" return call_tool("push_file", {"path": path, "content": content, "message": message, "sha": sha}) from pydantic import BaseModel, Field from typing import Optional class TicketSource(BaseModel): reporter: str trigger: str affected_service: str project_id: str class ProposedAction(BaseModel): execution_tool: str tool_parameters: dict parameter_hash: Optional[str] = None class Context(BaseModel): summary: str user_justification: str conversation_history: list class Governance(BaseModel): approval_status: str = "PENDING" authorized_approver: str requires_mfa: bool = True timeout_minutes: int = 15 class A2H2ATicket(BaseModel): ticket_id: Optional[str] = None source: TicketSource proposed_action: ProposedAction context: Context severity: str governance: Governance # --------------------------------------------------------------------------- # A2H2A (Human-in-the-Loop) # --------------------------------------------------------------------------- def create_a2h2a_ticket(ticket: A2H2ATicket) -> dict: """Create a new A2H2A ticket for a sensitive operation.""" resp = httpx.post( f"{OPAX_MCP_URL}/api/v1/a2h2a/tickets", headers=_headers(), json=ticket.dict(), timeout=30, ) resp.raise_for_status() return resp.json() # --------------------------------------------------------------------------- # ADK FunctionTools — for bruk i agent.py # --------------------------------------------------------------------------- def get_all_function_tools() -> list: """Returner alle tools som ADK FunctionTool-liste for Jason.""" from google.adk.tools import FunctionTool return [ FunctionTool(func=get_billing_summary), FunctionTool(func=get_billing_forecast), FunctionTool(func=get_billing_credits), FunctionTool(func=get_billing_anomalies), FunctionTool(func=get_billing_history), FunctionTool(func=get_billing_budget), FunctionTool(func=set_billing_budget), FunctionTool(func=create_invite), FunctionTool(func=list_customers), FunctionTool(func=send_webhook), FunctionTool(func=send_email), FunctionTool(func=send_sms), FunctionTool(func=get_notify_channels), FunctionTool(func=run_jason), FunctionTool(func=run_emma), FunctionTool(func=get_health), FunctionTool(func=get_build_status), FunctionTool(func=get_state), FunctionTool(func=get_telemetry), FunctionTool(func=run_terminal), FunctionTool(func=list_commits), FunctionTool(func=get_file), FunctionTool(func=list_open_issues), FunctionTool(func=create_issue), FunctionTool(func=push_file), FunctionTool(func=create_a2h2a_ticket), ]