From a7e92cfba79c0335417df0e4bad595f26dc07866 Mon Sep 17 00:00:00 2001 From: chrischristiansen-glitch Date: Mon, 15 Jun 2026 20:44:59 +0200 Subject: [PATCH 1/8] fix(deploy): legg til extra_packages for opax_mcp_client --- agents/core-logic/deploy_agent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/agents/core-logic/deploy_agent.py b/agents/core-logic/deploy_agent.py index 9ceb465..082d102 100644 --- a/agents/core-logic/deploy_agent.py +++ b/agents/core-logic/deploy_agent.py @@ -30,6 +30,7 @@ def deploy(project: str, region: str, display_name: str, staging_bucket: str): "httpx>=0.27.0", "google-auth>=2.29.0", ], + extra_packages=["./opax_mcp_client"], display_name=display_name, ) print(f"\n✅ Agent deployet!") From 61d1fa47ed82d704e6c694a22bf81ac933d7c908 Mon Sep 17 00:00:00 2001 From: chrischristiansen-glitch Date: Mon, 15 Jun 2026 21:07:16 +0200 Subject: [PATCH 2/8] =?UTF-8?q?fix(deploy):=20extra=5Fpackages=20peikar=20?= =?UTF-8?q?p=C3=A5=20.py-fil,=20ikkje=20mappe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/core-logic/deploy_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agents/core-logic/deploy_agent.py b/agents/core-logic/deploy_agent.py index 082d102..ba47cab 100644 --- a/agents/core-logic/deploy_agent.py +++ b/agents/core-logic/deploy_agent.py @@ -30,7 +30,7 @@ def deploy(project: str, region: str, display_name: str, staging_bucket: str): "httpx>=0.27.0", "google-auth>=2.29.0", ], - extra_packages=["./opax_mcp_client"], + extra_packages=["./opax_mcp_client.py"], display_name=display_name, ) print(f"\n✅ Agent deployet!") From 31f58e4ce06775906c9465763ed07b1a962b9685 Mon Sep 17 00:00:00 2001 From: chrischristiansen-glitch Date: Mon, 15 Jun 2026 21:19:03 +0200 Subject: [PATCH 3/8] fix(deploy): gjer opax_mcp_client til Python-pakke med __init__.py for Vertex AI --- agents/core-logic/opax_mcp_client/__init__.py | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 agents/core-logic/opax_mcp_client/__init__.py diff --git a/agents/core-logic/opax_mcp_client/__init__.py b/agents/core-logic/opax_mcp_client/__init__.py new file mode 100644 index 0000000..64dff59 --- /dev/null +++ b/agents/core-logic/opax_mcp_client/__init__.py @@ -0,0 +1,212 @@ +""" +opax_mcp_client — REST-klient for opax-mcp Cloud Run service. +Pakke-struktur for Vertex AI Agent Engine extra_packages-kompatibilitet. + +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}) + + +# --------------------------------------------------------------------------- +# 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), + ] From 294f455d67ff2ff7aa2a20a7ec054768b9b136a0 Mon Sep 17 00:00:00 2001 From: chrischristiansen-glitch Date: Mon, 15 Jun 2026 21:32:21 +0200 Subject: [PATCH 4/8] =?UTF-8?q?fix(agent):=20inline=20opax=5Fmcp=5Fclient?= =?UTF-8?q?=20direkte=20i=20agent.py=20=E2=80=94=20fjern=20ekstern=20impor?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/core-logic/agent.py | 256 +++++++++++++++++++++++++------------ 1 file changed, 175 insertions(+), 81 deletions(-) diff --git a/agents/core-logic/agent.py b/agents/core-logic/agent.py index 740840c..ba0d78d 100644 --- a/agents/core-logic/agent.py +++ b/agents/core-logic/agent.py @@ -1,24 +1,20 @@ #!/usr/bin/env python3 """ agent.py — OSVauco OPAX agent (Jason Vauger). - -Modes: - light — gemini-2.5-flash, $1/task - heavy — gemini-2.5-pro, $3/task - -Legacy 'A' / 'A+' normaliseres automatisk. -Autoriserte brukere for heavy: opax, admin - -NOTE: opax_mcp_client.py ligg i same mappe (core-logic/) for Vertex AI-kompatibilitet. +opax_mcp_client er inlina direkte her for Vertex AI Agent Engine-kompatibilitet. """ import asyncio import os import logging import uuid +import httpx +import google.auth +import google.auth.transport.requests from typing import Literal from google.adk.agents import Agent +from google.adk.tools import FunctionTool from google.adk.runners import Runner from google.adk.sessions.in_memory_session_service import InMemorySessionService from google.genai import types @@ -66,6 +62,161 @@ except ImportError as _e: create_bq_table_if_not_exists() +# --------------------------------------------------------------------------- +# OPAX MCP Client (inlina) +# --------------------------------------------------------------------------- +_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 _mcp_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=_mcp_headers(), + json={"tool": tool, "params": params or {}}, + timeout=30, + ) + resp.raise_for_status() + return resp.json() + + +def get_billing_summary() -> dict: + """Hent billing-oversikt for OSVauco (GCP-kostnader, token-forbruk).""" + return _call_tool("get_billing_summary") + +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. amount er beløpet i USD.""" + return _call_tool("set_billing_budget", {"amount": amount}) + +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") + +def send_webhook(message: str, url: str = "", title: str = "OPAX varsel") -> dict: + """Send webhook-varsling til Slack/Teams/Discord.""" + 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.""" + return _call_tool("send_email", {"to": to, "subject": subject, "body": body}) + +def get_notify_channels() -> dict: + """List konfigurerte varslingskanalar.""" + return _call_tool("get_notify_channels") + +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}) + +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.""" + 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.""" + return _call_tool("push_file", {"path": path, "content": content, "message": message, "sha": sha}) + + +OPAX_TOOLS = [ + FunctionTool(func=get_billing_summary), + 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=get_notify_channels), + 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), +] + + +# --------------------------------------------------------------------------- +# Agent logic +# --------------------------------------------------------------------------- def _normalize_mode(mode: str) -> str: mapping = {"A": "light", "A+": "heavy", "light": "light", "heavy": "heavy"} if mode not in mapping: @@ -79,37 +230,16 @@ def authorize_mode(user_id: str, mode: str) -> None: raise PermissionError(f"User '{user_id}' is not authorized for heavy mode.") -# —— OPAX-MCP FunctionTools —— -# opax_mcp_client.py er i same mappe — Vertex AI finn den alltid -opax_tools = [] -try: - from opax_mcp_client import get_all_function_tools - opax_tools = get_all_function_tools() - logger.info(f"OPAX-MCP tools lastet: {len(opax_tools)} tools") -except Exception as e: - logger.warning(f"OPAX-MCP tools ikkje tilgjengeleg: {e}") - - def get_models_for_mode(mode: Mode) -> dict: if mode == "heavy": - return { - "orchestrator": HEAVY_ORCHESTRATOR, - "subagent": HEAVY_SUBAGENT, - "reasoning": HEAVY_REASONING, - "budget_usd": BUDGET_HEAVY, - } - return { - "orchestrator": ORCHESTRATOR_MODEL, - "subagent": SUBAGENT_MODEL, - "reasoning": REASONING_MODEL, - "budget_usd": BUDGET_LIGHT, - } + return {"orchestrator": HEAVY_ORCHESTRATOR, "subagent": HEAVY_SUBAGENT, "reasoning": HEAVY_REASONING, "budget_usd": BUDGET_HEAVY} + return {"orchestrator": ORCHESTRATOR_MODEL, "subagent": SUBAGENT_MODEL, "reasoning": REASONING_MODEL, "budget_usd": BUDGET_LIGHT} _INSTRUCTION_TEMPLATE = ( "Du er Jason Vauger — OPAX-agenten for Vauco AS. Modus: {mode}. Modell: {model}. " "Du har tilgang til alle OPAX-tools via opax-mcp. " - "Tools du kan bruke: list_commits, get_file, push_file, create_issue, list_open_issues (Gitea), " + "Tools: list_commits, get_file, push_file, create_issue, list_open_issues (Gitea), " "get_billing_summary, get_billing_credits, get_billing_anomalies, " "get_build_status, get_state, get_telemetry, run_terminal, " "create_invite, list_customers, send_webhook, send_email, get_notify_channels. " @@ -122,19 +252,14 @@ _INSTRUCTION_TEMPLATE = ( def build_agent(mode: str = "light", message: str = "") -> tuple: mode = _normalize_mode(mode) models = get_models_for_mode(mode) - actual_model = route_model( - message=message, - mode=mode, - flash_model=ORCHESTRATOR_MODEL, - pro_model=models["orchestrator"], - ) + actual_model = route_model(message=message, mode=mode, flash_model=ORCHESTRATOR_MODEL, pro_model=models["orchestrator"]) instruction = _INSTRUCTION_TEMPLATE.format(mode=mode, model=actual_model) agent = Agent( model=actual_model, name="jason_vauger", description=f"Jason Vauger — OPAX enterprise agent [{mode}]", instruction=instruction, - tools=opax_tools, + tools=OPAX_TOOLS, ) return agent, actual_model, instruction @@ -142,16 +267,9 @@ def build_agent(mode: str = "light", message: str = "") -> tuple: root_agent, _, _ = build_agent(mode="light") -async def _run_async( - message: str, - user_id: str, - session_id: str, - mode: str, - caller_type: str = "agent", -) -> str: +async def _run_async(message: str, user_id: str, session_id: str, mode: str, caller_type: str = "agent") -> str: mode = _normalize_mode(mode) agent, actual_model, instruction = build_agent(mode=mode, message=message) - models = get_models_for_mode(mode) module_name = f"jason/{mode}" try: @@ -160,9 +278,7 @@ async def _run_async( return f"⚠️ Token-budsjett overskredet: {e}" session_service = InMemorySessionService() - session = await session_service.create_session( - app_name=APP_NAME, user_id=user_id, session_id=session_id, - ) + session = await session_service.create_session(app_name=APP_NAME, user_id=user_id, session_id=session_id) runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service) new_message = types.Content(role="user", parts=[types.Part(text=message)]) final_text = "" @@ -170,9 +286,7 @@ async def _run_async( output_tokens = 0 request_id = str(uuid.uuid4()) - async for event in runner.run_async( - user_id=user_id, session_id=session.id, new_message=new_message, - ): + async for event in runner.run_async(user_id=user_id, session_id=session.id, new_message=new_message): if event.is_final_response() and event.content and event.content.parts: final_text = event.content.parts[0].text or "" if hasattr(event, "usage_metadata") and event.usage_metadata: @@ -182,39 +296,19 @@ async def _run_async( if input_tokens > 0 or output_tokens > 0: log_token_usage( - agent_name=module_name, - model_name=actual_model, - input_tokens=input_tokens, - output_tokens=output_tokens, - request_id=request_id, - module_name=module_name, - caller_type=caller_type, - session_id=session_id, + agent_name=module_name, model_name=actual_model, + input_tokens=input_tokens, output_tokens=output_tokens, + request_id=request_id, module_name=module_name, + caller_type=caller_type, session_id=session_id, ) - - logger.info( - f"[agent] {mode}/{actual_model} in={input_tokens} out={output_tokens} " - f"caller={caller_type} session={session_id}" - ) + logger.info(f"[agent] {mode}/{actual_model} in={input_tokens} out={output_tokens} caller={caller_type} session={session_id}") return final_text -def run( - message: str, - user_id: str = "opax", - session_id: str = "default", - mode: str = "light", - caller_type: str = "agent", -) -> str: +def run(message: str, user_id: str = "opax", session_id: str = "default", mode: str = "light", caller_type: str = "agent") -> str: mode = _normalize_mode(mode) authorize_mode(user_id, mode) - return asyncio.run(_run_async( - message=message, - user_id=user_id, - session_id=session_id, - mode=mode, - caller_type=caller_type, - )) + return asyncio.run(_run_async(message=message, user_id=user_id, session_id=session_id, mode=mode, caller_type=caller_type)) if __name__ == "__main__": From 9627da37aed3c79de2b9d736601915a9afe4b4df Mon Sep 17 00:00:00 2001 From: chrischristiansen-glitch Date: Wed, 17 Jun 2026 17:59:05 +0200 Subject: [PATCH 5/8] =?UTF-8?q?fix(agent):=20legg=20til=20vertexai.init()?= =?UTF-8?q?=20=E2=80=94=20ADK=20m=C3=A5=20initialiseras=20eksplisitt=20for?= =?UTF-8?q?=20Cloud=20Run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/core-logic/agent.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/agents/core-logic/agent.py b/agents/core-logic/agent.py index ba0d78d..354f9a1 100644 --- a/agents/core-logic/agent.py +++ b/agents/core-logic/agent.py @@ -13,6 +13,7 @@ import google.auth import google.auth.transport.requests from typing import Literal +import vertexai from google.adk.agents import Agent from google.adk.tools import FunctionTool from google.adk.runners import Runner @@ -22,7 +23,10 @@ from google.genai import types logger = logging.getLogger(__name__) PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5") -LOCATION = os.environ.get("GOOGLE_CLOUD_LOCATION", "global") +LOCATION = os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1") + +# Initialiser Vertex AI eksplisitt — ADK krev dette for Cloud Run +vertexai.init(project=PROJECT_ID, location=LOCATION) ORCHESTRATOR_MODEL = os.environ.get("ORCHESTRATOR_MODEL", "gemini-2.5-flash") SUBAGENT_MODEL = os.environ.get("SUBAGENT_MODEL", "gemini-2.5-flash") @@ -65,7 +69,7 @@ create_bq_table_if_not_exists() # --------------------------------------------------------------------------- # OPAX MCP Client (inlina) # --------------------------------------------------------------------------- -_OPAX_MCP_URL = os.environ.get("MCP_SERVER_URL", "https://opax-mcp-zjbqp3prqq-uc.a.run.app") +_OPAX_MCP_URL = os.environ.get("MCP_SERVER_URL", "https://opax-mcp-357036551735.us-central1.run.app") _MCP_SECRET = os.environ.get("MCP_SECRET", "") From 0ab044417eaaf76a25e02d66e36bcba2586f1aba Mon Sep 17 00:00:00 2001 From: chrischristiansen-glitch Date: Wed, 17 Jun 2026 18:39:12 +0200 Subject: [PATCH 6/8] fix(opax-mcp): legg til identity token auth i _opax_get/_opax_post mot opax.vauco.no --- opax-mcp/server.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/opax-mcp/server.py b/opax-mcp/server.py index a96a587..10061ba 100644 --- a/opax-mcp/server.py +++ b/opax-mcp/server.py @@ -5,6 +5,8 @@ Auth: Cloud Run IAM (Authorization header) + X-MCP-Secret header for tool-level import os import httpx import base64 +import google.auth +import google.auth.transport.requests from fastapi import FastAPI, HTTPException, Header from pydantic import BaseModel from typing import Optional, Any @@ -27,6 +29,30 @@ def _auth_check(x_mcp_secret: Optional[str]): raise HTTPException(status_code=401, detail="Unauthorized") +def _opax_identity_token() -> str: + """Hent identity token for opax.vauco.no (Cloud Run IAP/IAM).""" + metadata_url = ( + "http://metadata.google.internal/computeMetadata/v1/instance" + f"/service-accounts/default/identity?audience={OPAX_BASE_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 _opax_headers() -> dict: + return { + "Authorization": f"Bearer {_opax_identity_token()}", + "Content-Type": "application/json", + } + + # --------------------------------------------------------------------------- # Health # --------------------------------------------------------------------------- @@ -70,14 +96,14 @@ async def list_tools(x_mcp_secret: Optional[str] = Header(default=None)): async def _opax_get(path: str) -> Any: async with httpx.AsyncClient(timeout=30) as client: - r = await client.get(f"{OPAX_BASE_URL}{path}") + r = await client.get(f"{OPAX_BASE_URL}{path}", headers=_opax_headers()) r.raise_for_status() return r.json() async def _opax_post(path: str, body: dict) -> Any: async with httpx.AsyncClient(timeout=30) as client: - r = await client.post(f"{OPAX_BASE_URL}{path}", json=body) + r = await client.post(f"{OPAX_BASE_URL}{path}", json=body, headers=_opax_headers()) r.raise_for_status() return r.json() @@ -203,7 +229,7 @@ async def run_emma(p): # Platform tools # --------------------------------------------------------------------------- -async def get_health(p): return await _opax_get("/health") +async def get_health(p): return await _opax_get("/health") async def get_build_status(p): return await _opax_get("/opax/build-status") async def get_state(p): return await _opax_get("/state") async def get_telemetry(p): return await _opax_get("/telemetry/history") From 9542106ecd19f7c261a90b8c53764d06d9db8296 Mon Sep 17 00:00:00 2001 From: chrischristiansen-glitch Date: Thu, 18 Jun 2026 14:11:54 +0200 Subject: [PATCH 7/8] fix(opax-mcp): bruk OPAX_IAP_CLIENT_ID som IAP audience i identitytoken Update identity token retrieval to use OPAX_IAP_CLIENT_ID if available. --- opax-mcp/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opax-mcp/server.py b/opax-mcp/server.py index 10061ba..1fab9e0 100644 --- a/opax-mcp/server.py +++ b/opax-mcp/server.py @@ -14,6 +14,7 @@ from typing import Optional, Any app = FastAPI(title="opax-mcp", version="2.0.0") OPAX_BASE_URL = os.environ.get("OPAX_BASE_URL", "https://opax.vauco.no") +OPAX_IAP_CLIENT_ID = os.environ.get("OPAX_IAP_CLIENT_ID", "") MCP_SECRET = os.environ.get("MCP_SECRET", "") # Gitea @@ -33,7 +34,7 @@ def _opax_identity_token() -> str: """Hent identity token for opax.vauco.no (Cloud Run IAP/IAM).""" metadata_url = ( "http://metadata.google.internal/computeMetadata/v1/instance" - f"/service-accounts/default/identity?audience={OPAX_BASE_URL}&format=full" + f"/service-accounts/default/identity?audience={OPAX_IAP_CLIENT_ID or OPAX_BASE_URL}&format=full" ) try: resp = httpx.get(metadata_url, headers={"Metadata-Flavor": "Google"}, timeout=5) From dc9f6265be23446f41a30ade2bef8ffd9b4c17b8 Mon Sep 17 00:00:00 2001 From: chrischristiansen-glitch Date: Thu, 18 Jun 2026 14:46:19 +0200 Subject: [PATCH 8/8] =?UTF-8?q?docs(handoff):=20oppdater=20HANDOFF.md=20?= =?UTF-8?q?=E2=80=94=20IAP=20auth=20fix=20for=20opax-mcp=202026-06-18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated the last modified date and author in the HANDOFF document. Revised critical rules and completed tasks, including IAP Auth fixes for opax-mcp. --- docs/HANDOFF.md | 170 ++++++++++++++++++++++++++++++------------------ 1 file changed, 105 insertions(+), 65 deletions(-) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index cbd57ac..1cc79de 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,14 +1,14 @@ # HANDOFF — Vauco OS -**Sist oppdatert:** 2026-06-15 19:46 CEST -**Skrevet av:** Perplexity (Orchestrator) +**Sist oppdatert:** 2026-06-18 14:00 CEST +**Skrevet av:** Perplexity (Orchestrator) **Status:** Fase B — Gitea er primær Git. GitHub er fasa ut. --- ## ⚠️ KRITISKE REGLER — les alltid først -- **Hovudapp:** `main.py` i rot — IKKJE `agents/core-logic/app.py` +- **Hovedapp:** `main.py` i rot — IKKJE `agents/core-logic/app.py` - **Dockerfile:** `agents/core-logic/Dockerfile` — WORKDIR `/app`, CMD uvicorn main:app - **Region:** `us-central1` | **Service:** `osvauco-agent` | **Prosjekt:** `propane-will-491900-m5` - **Test via proxy:** `gcloud run services proxy osvauco-agent --region us-central1 --project propane-will-491900-m5 --port 8181` @@ -21,7 +21,20 @@ --- -## ✅ Fullført i dag (2026-06-15) +## ✅ Fullført i dag (2026-06-18) — IAP Auth fix for opax-mcp + +| Oppgave | Status | +|---------|--------| +| Identifisert rotårsak: feil IAP audience i identity token-kall | ✅ | +| Lagt til `roles/iap.httpsResourceAccessor` for `jason-vauger` SA | ✅ | +| Oppdatert `opax-mcp/server.py` — ny env var `OPAX_IAP_CLIENT_ID` som audience | ✅ | +| Lagt til `OPAX_IAP_CLIENT_ID` env var i Cloud Run (revisjon 00019 — feil navn `IAP_CLIENT_ID`) | ⚠️ | +| Rettet env var navn til `OPAX_IAP_CLIENT_ID` i Cloud Run — revisjon 00020 deployed | ✅ | +| Revisjon `opax-mcp-00020-w6j` aktiv med 100% trafikk | ✅ | + +--- + +## ✅ Fullført tidligere (2026-06-15) | Oppgave | Status | |---------|--------| @@ -30,7 +43,7 @@ | Jason deployet til Vertex AI Agent Engine | ✅ | | Jason svarer på norsk og kallar tools automatisk | ✅ | | Gitea installert på VM (`34.59.131.162:3000`) | ✅ | -| Gitea køyrer som systemd-teneste (overlever reboot) | ✅ | +| Gitea kjøyrer som systemd-teneste (overlever reboot) | ✅ | | Mirror-repo sletta og erstatta med vanleg repo | ✅ | | **2867 commits pusha frå GitHub → Gitea** | ✅ | | Gitea API-tokens i Secret Manager | ✅ | @@ -38,89 +51,116 @@ --- -## 🏗 Systemstatus +## 🖥 Systemstatus | Komponent | URL / Referanse | Status | |-----------|-----------------|--------| -| `osvauco-agent` Cloud Run | `us-central1` | ✅ Live | -| `opax-mcp` Cloud Run | `https://opax-mcp-zjbqp3prqq-uc.a.run.app` | ✅ Live (25 tools) | -| Jason Vertex AI Agent Engine | `reasoningEngines/237959055262351360` | ✅ Live | -| **Gitea (primær Git)** | `http://34.59.131.162:3000/chris/OSVauco` | ✅ Live + systemd | -| GitHub OSVauco | `https://github.com/vauco-saas/OSVauco` | ❌ Fasa ut | -| `opax.vauco.no` | — | ✅ Live | -| Emma (Gemma, lokal VM) | — | ⏳ Neste fase | +| osvauco-agent (Cloud Run) | https://osvauco-agent-zjbgp3prqg-uc.a.run.app | ✅ Live | +| opax-mcp (Cloud Run) | https://opax-mcp-357036551735.us-central1.run.app | ✅ Live (rev 00020) | +| opax frontend (IAP LB) | https://opax.vauco.no | ✅ Live | +| Jason (Vertex AI Agent) | Agent Engine, us-central1 | ✅ Live | +| Gitea (self-hosted) | http://34.59.131.162:3000 | ✅ Live | +| GitHub (legacy) | https://github.com/vauco-saas/OSVauco | ⚠️ Fasa ut | --- ## 🔑 Credentials -| System | Brukar | Verdi | -|--------|--------|-------| -| Gitea | `chris` | `OSVauco@Gitea2026!` | -| Gitea API token (jason-token2) | — | Secret Manager: `gitea-api-token` (`278c0b41...`) | -| Gitea API token (admin-token2) | — | `94527a0f5353c4b1db29a007b98f340b5fd1f385` | -| opax-mcp MCP_SECRET | — | Secret Manager: `mcp-server-key` | -| Jason Agent Engine | — | `reasoningEngines/237959055262351360` | +- **Gitea login:** `chris` / se Secret Manager +- **Gitea API-token:** Secret Manager → `gitea-api-token` +- **MCP-Secret:** Secret Manager → `mcp-secret` +- **Prosjekt:** `propane-will-491900-m5` +- **IAP OAuth Client ID (opax backend):** `357036551735-kq8nt71d38hfqlcfb3n52ef7ala4meo.apps.googleusercontent.com` +- **IAP Backend Service path:** `/projects/357036551735/global/backendServices/` (satt som `OPAX_IAP_CLIENT_ID` i Cloud Run) --- -## 🟠 Gjenstår (prioritert) +## 🎯 Gjenstår (prioritert) -| # | Oppgave | Notat | -|---|---------|-------| -| 1 | Oppdater `opax_mcp_client.py` GitHub-tools → Gitea API | Byt `api.github.com` → `34.59.131.162:3000/api/v1` | -| 2 | Cloud Build webhook → Gitea (ikkje GitHub) | Trigger på Gitea push | -| 3 | DNS `git.vauco.no` → `34.59.131.162` | Domene for Gitea | -| 4 | HTTPS/TLS for Gitea (Caddy) | Etter DNS | -| 5 | Emma — Gemma lokal på ML-3a GPU VM | Eiga fase | -| 6 | Sett manglande env-vars på `osvauco-agent` | `SENDGRID_API_KEY`, `TWILIO_*`, `NOTIFY_*` | +1. **KRITISK — Verifiser IAP auth end-to-end** — kjør verifikasjonskommando under og bekreft `HTTP/2 200` +2. Oppdater Gitea-klient-logikk i agenter (fra GitHub API → Gitea API) +3. DNS/TLS for Gitea (`git.vauco.no`) +4. GPU VM-arbeid (planlagt) +5. Smoke-test pipeline for opax-mcp etter IAP-fix --- -## 📌 Neste sesjon — start her +## 🚀 Neste sesjon — start her + +### 1. Verifiser IAP auth fix (VIKTIGST) + +Kjør fra `osvauco-dev-vm`: ```bash -# 1. Verifiser Gitea -curl -s http://34.59.131.162:3000/api/v1/repos/chris/OSVauco \ - -H "Authorization: token 94527a0f5353c4b1db29a007b98f340b5fd1f385" \ - | python3 -c "import sys,json; r=json.load(sys.stdin); print('mirror:', r['mirror'], '| commits OK')" +# Hent identity token med korrekt IAP audience +TOKEN=$(curl -s -H "Metadata-Flavor: Google" \ + "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=357036551735-kq8nt71d38hfqlcfb3n52ef7ala4meo.apps.googleusercontent.com&format=full") -# 2. Verifiser Jason -cd ~/OSVauco/agents/core-logic && source .venv/bin/activate -python3 - <<'EOF' -import vertexai -from vertexai import agent_engines -vertexai.init(project="propane-will-491900-m5", location="us-central1") -agent = agent_engines.get("projects/357036551735/locations/us-central1/reasoningEngines/237959055262351360") -session = agent.create_session(user_id="chris") -for event in agent.stream_query(user_id="chris", session_id=session["id"], message="Status?"): - if event.get("content"): - print(event["content"]["parts"][0].get("text", ""), end="") -EOF - -# 3. Neste: oppdater opax_mcp_client.py til Gitea API +# Test mot opax via IAP load balancer +curl -si https://opax.vauco.no/opax/build-status \ + -H "Authorization: Bearer $TOKEN" | head -5 ``` +Forventet: `HTTP/2 200` (tidligere: `HTTP/2 401`) + +### 2. Sjekk Jason sin status + +```bash +curl -s https://osvauco-agent-zjbgp3prqg-uc.a.run.app/health \ + -H "Authorization: Bearer $(gcloud auth print-identity-token)" +``` + +### 3. Hva som ble fikset i opax-mcp/server.py + +**Fil:** `opax-mcp/server.py` — commit `9542106` ("OPAX_IAP_CLIENT_ID som IAP audience i identitytoken") + +**Endring linje 17:** +```python +# FØR: +OPAX_BASE_URL = os.environ.get("OPAX_BASE_URL", "https://opax.vauco.no") + +# ETTER: +OPAX_BASE_URL = os.environ.get("OPAX_BASE_URL", "https://opax.vauco.no") +OPAX_IAP_CLIENT_ID = os.environ.get("OPAX_IAP_CLIENT_ID", "") +``` + +**Endring linje 37 (`_opax_identity_token`):** +```python +# FØR: +f"...identity?audience={OPAX_BASE_URL}&format=full" + +# ETTER: +f"...identity?audience={OPAX_IAP_CLIENT_ID or OPAX_BASE_URL}&format=full" +``` + +**Cloud Run env vars på opax-mcp (aktive i rev 00020):** + +| Navn | Verdi | +|------|-------| +| `OPAX_BASE_URL` | `https://opax.vauco.no` | +| `OPAX_IAP_CLIENT_ID` | `/projects/357036551735/global/backendServices/...` | +| `MCP_SECRET` | (fra Secret Manager) | +| `GOOGLE_CLOUD_PROJECT` | `propane-will-491900-m5` | + --- -## 🗺 Arkitektur +## 🏗 Arkitektur ``` -Chris (naturleg språk) - ↓ -Jason Vauger (Vertex AI Agent Engine — Gemini 2.5 Flash) - reasoningEngines/237959055262351360 - ↓ -opax-mcp (Cloud Run — 25 tools) - https://opax-mcp-zjbqp3prqq-uc.a.run.app - ↓ -┌─────────────────────────────────┐ -│ Gitea 34.59.131.162:3000 │ ← Eigen Git (PRIMÆR) -│ GCP Cloud Build / Run │ ← CI/CD + hosting -│ GitHub — FASA UT │ ← Ikkje i bruk lenger -└─────────────────────────────────┘ +Bruker + └─► Jason (Vertex AI Agent Engine, Gemini 2.5 Pro) + └─► osvauco-agent (Cloud Run) + └─► opax-mcp (Cloud Run) + └─► [IAP] opax.vauco.no + └─► OPAX backend (GCP) ``` ---- - -*Jason Vauger — han blir den du trenger han til å bli.* +**Auth-flyt for opax-mcp → opax.vauco.no:** +``` +opax-mcp container + → GCE Metadata Service + → identity token med audience = IAP_CLIENT_ID + → Authorization: Bearer + → IAP validerer token + → Request videresendt til OPAX backend +```