fix: oppdater Gitea IP fra 34.158.137.245 til 136.111.198.14

This commit is contained in:
Chris Christiansen 2026-07-05 11:57:05 +00:00
parent e88881be19
commit ef02198d02
7 changed files with 191 additions and 27 deletions

View File

@ -23,7 +23,7 @@ SSH_CONFIG_PATH="/mnt/c/Users/Cvias/.ssh/config"
SSH_KEY_PATH="C:\Users\Cvias\.ssh\google_compute_engine"
# Git Remotes for Verification on GPU VM
GITEA_REMOTE="http://34.158.137.245:3000/chris/osvauco.git"
GITEA_REMOTE="http://136.111.198.14:3000/chris/osvauco.git"
GITHUB_REMOTE="git@github.com:vauco/osvauco.git"
# --- Helper Functions ---

View File

@ -12,7 +12,7 @@ Stabilisere dev-serveren som aktiv plattform nå, ferdigstille sikker og robust
## Fase 1 — Stabilisering nå
### 1. Statisk IP på osvauco-dev-vm (✅ Allerede utført)
- En statisk IP (`34.158.137.245`) med navnet `osvauco-dev-ip` er allerede reservert og i bruk av `osvauco-dev-vm`.
- En statisk IP (`136.111.198.14`) med navnet `osvauco-dev-ip` er allerede reservert og i bruk av `osvauco-dev-vm`.
### 2. Oppdater koblinger mot dev-vm
- Oppdater `GITEA_URL` til ny statisk IP
@ -96,7 +96,7 @@ Stabilisere dev-serveren som aktiv plattform nå, ferdigstille sikker og robust
---
## Neste konkrete steg
1. Oppdater `GITEA_URL` til statisk IP (`34.158.137.245`)
1. Oppdater `GITEA_URL` til statisk IP (`136.111.198.14`)
2. Test `push_file` ende til ende
3. Legg inn SendGrid/Twilio secrets
4. Start `emma-gpu-vm` (fra TRUTH.md TODO)

View File

@ -31,7 +31,7 @@
| Navn | Sone | Status | Ekstern IP |
|---|---|---|---|
| `osvauco-dev-vm` | europe-west4-c | RUNNING | 34.158.137.245 (Statisk: osvauco-dev-ip) |
| `osvauco-dev-vm` | europe-west4-c | RUNNING | 136.111.198.14 (Statisk: osvauco-dev-ip) |
| `osvauco-dev-vm` | us-central1-b | TERMINATED | - |
| `emma-gpu-vm` | europe-west4-a | TERMINATED | - |

View File

@ -72,7 +72,7 @@ EMMA_MODEL = os.environ.get("EMMA_MODEL", "gemini-2.5-pro")
# ── JASON: backend-flagg ────────────────────────────────────────────────────
JASON_BACKEND = os.environ.get("JASON_BACKEND", "vertex")
JASON_LOCAL_URL = os.environ.get("JASON_LOCAL_URL", "http://34.158.137.245:11434/api/generate")
JASON_LOCAL_URL = os.environ.get("JASON_LOCAL_URL", "http://136.111.198.14:11434/api/generate")
JASON_MODEL = os.environ.get("JASON_MODEL", "gemma3:4b")
async def _jason_local(message: str) -> str:

View File

@ -40,6 +40,8 @@ spec:
- env:
- name: GOOGLE_CLOUD_PROJECT
value: propane-will-491900-m5
- name: INTERNAL_API_KEY
value: 8yHesvMPYZuKbsPmR01NFvBjNRGLytYWnM9yoVQl-MQ
- name: STATIC_BUCKET
value: opax-vauco-static
- name: CLOUD_RUN_SERVICE
@ -49,7 +51,7 @@ spec:
- name: OPAX_IAP_CLIENT_ID
value: 357036551735-kq8nt7ld38hfqlcfb3n52ef7tala4meo.apps.googleusercontent.com
- name: GITEA_URL
value: http://34.158.137.245:3000
value: http://136.111.198.14:3000
- name: GITEA_TOKEN
valueFrom:
secretKeyRef:

View File

@ -2,4 +2,6 @@ fastapi>=0.111.0
uvicorn>=0.30.0
httpx>=0.27.0
google-auth>=2.29.0
google-api-python-client>=2.120.0
google-cloud-firestore>=2.16.0
google-cloud-secret-manager>=2.18.0

View File

@ -10,15 +10,21 @@ import base64
import google.auth
import google.auth.transport.requests
import google.oauth2.id_token
from googleapiclient.discovery import build
from google.cloud import secretmanager
from google.oauth2 import service_account
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from email.mime.text import MIMEText
from email.mime.text import MIMEText
from datetime import datetime, timezone, timedelta
from typing import Any, Optional
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="opax-mcp", version="3.3.0") # BUMPED
app = FastAPI(title="opax-mcp", version="3.5.0") # BUMPED
# ── Service discovery ───────────────────────────────────────────────────────
OSVAUCO_AGENT_URL = os.environ.get("OSVAUCO_AGENT_URL", "") # f.eks. https://osvauco-agent-....run.app
@ -28,6 +34,7 @@ GITEA_URL = os.environ.get("GITEA_URL", "http://34.59.131.162:3000")
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
GITEA_REPO = os.environ.get("GITEA_REPO", "chris/OSVauco")
GOOGLE_CLOUD_PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
WORKSPACE_ADMIN_USER = "chris.christiansen@vauco.no"
# Emma-vm Ollama — direkte tilkobling
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://34.13.238.133:11434")
@ -85,6 +92,159 @@ async def _agent_post(path: str, body: dict) -> Any:
r.raise_for_status()
return r.json()
# ---------------------------------------------------------------------------
# Google Workspace Helpers
# ---------------------------------------------------------------------------
def _get_workspace_service(api: str, version: str, scopes: list):
"""
Creates an authenticated Google API service object using a service account
key from Secret Manager and impersonating the admin user.
"""
try:
client = secretmanager.SecretManagerServiceClient()
secret_name = "workspace-sa-key"
version_name = f"projects/{GOOGLE_CLOUD_PROJECT}/secrets/{secret_name}/versions/latest"
response = client.access_secret_version(request={"name": version_name})
secret_payload = response.payload.data.decode("UTF-8")
secret_info = json.loads(secret_payload)
creds = service_account.Credentials.from_service_account_info(
secret_info, scopes=scopes
)
delegated_creds = creds.with_subject(WORKSPACE_ADMIN_USER)
return build(api, version, credentials=delegated_creds, cache_discovery=False)
except Exception as e:
logger.error(f"Failed to get Workspace service: {e}", exc_info=True)
raise
async def create_email_alias(p: dict) -> dict:
"""Creates a new email alias for a Google Workspace user."""
user_key = p.get("user_key")
alias_email = p.get("alias")
if not user_key or not alias_email:
raise ValueError("Missing required parameters: 'user_key' and 'alias'")
service = _get_workspace_service('admin', 'directory_v1', ['https://www.googleapis.com/auth/admin.directory.user'])
alias_body = {'alias': alias_email}
logger.info(f"Creating alias '{alias_email}' for user '{user_key}'...")
result = service.users().aliases().insert(userKey=user_key, body=alias_body).execute()
logger.info(f"Successfully created alias.")
return {"status": "success", "alias": result}
async def list_user_aliases(p: dict) -> dict:
"""Lists all aliases for a Google Workspace user."""
user_key = p.get("user_key")
if not user_key:
raise ValueError("Missing required parameter: 'user_key'")
service = _get_workspace_service('admin', 'directory_v1', ['https://www.googleapis.com/auth/admin.directory.user'])
logger.info(f"Listing aliases for user '{user_key}'...")
result = service.users().aliases().list(userKey=user_key).execute()
logger.info(f"Successfully listed aliases.")
return {"status": "success", "aliases": result.get("aliases", [])}
async def delete_email_alias(p: dict) -> dict:
"""Deletes an email alias from a Google Workspace user."""
user_key = p.get("user_key")
alias_email = p.get("alias")
if not user_key or not alias_email:
raise ValueError("Missing required parameters: 'user_key' and 'alias'")
service = _get_workspace_service('admin', 'directory_v1', ['https://www.googleapis.com/auth/admin.directory.user'])
logger.info(f"Deleting alias '{alias_email}' for user '{user_key}'...")
service.users().aliases().delete(userKey=user_key, alias=alias_email).execute()
logger.info(f"Successfully deleted alias.")
return {"status": "success", "detail": f"Alias '{alias_email}' deleted."}
async def send_email_as(p: dict) -> dict:
"""Sends an email as a user or their alias, on their behalf."""
from_address, to_address, subject, body = p.get("from"), p.get("to"), p.get("subject"), p.get("body")
if not all([from_address, to_address, subject, body]):
raise ValueError("Missing required parameters: 'from', 'to', 'subject', 'body'")
service = _get_workspace_service('gmail', 'v1', ['https://www.googleapis.com/auth/gmail.send'])
message = MIMEText(body)
message['to'], message['from'], message['subject'] = to_address, from_address, subject
encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
create_message = {'raw': encoded_message}
logger.info(f"Sending email from '{from_address}' to '{to_address}'...")
send_message = service.users().messages().send(userId='me', body=create_message).execute()
logger.info(f"Successfully sent email with ID: {send_message['id']}")
return {"status": "success", "message_id": send_message['id']}
async def get_workspace_user(p: dict) -> dict:
"""Gets detailed information about a single user in Google Workspace."""
user_key = p.get("user_key")
if not user_key: raise ValueError("Missing required parameter: 'user_key'")
service = _get_workspace_service('admin', 'directory_v1', ['https://www.googleapis.com/auth/admin.directory.user'])
logger.info(f"Getting info for user '{user_key}'...")
user = service.users().get(userKey=user_key).execute()
logger.info(f"Successfully retrieved user info.")
return {"name": user.get("name", {}).get("fullName"), "email": user.get("primaryEmail"), "aliases": user.get("aliases", []), "suspended": user.get("suspended"), "lastLoginTime": user.get("lastLoginTime")}
async def list_workspace_users(p: dict) -> dict:
"""Lists all users in the Google Workspace domain."""
service = _get_workspace_service('admin', 'directory_v1', ['https://www.googleapis.com/auth/admin.directory.user'])
logger.info("Listing all workspace users...")
result = service.users().list(domain='vauco.no', maxResults=50, orderBy='email').execute()
users = result.get('users', [])
logger.info(f"Found {len(users)} users.")
return {"users": [{"email": user.get("primaryEmail"), "name": user.get("name", {}).get("fullName")} for user in users]}
async def get_emails(p: dict) -> dict:
"""Searches a user's mailbox and retrieves a list of emails."""
user_key, query, max_results = p.get('user_key'), p.get('query', ''), p.get('max_results', 10)
if not user_key: raise ValueError("Missing required parameter: 'user_key'")
service = _get_workspace_service('gmail', 'v1', ['https://www.googleapis.com/auth/gmail.modify'])
logger.info(f"Searching emails for user '{user_key}' with query '{query}'...")
list_result = service.users().messages().list(userId=user_key, q=query, maxResults=max_results).execute()
messages = list_result.get('messages', [])
if not messages: return {"messages": []}
email_details = []
for msg in messages:
detail = service.users().messages().get(userId=user_key, id=msg['id'], format='metadata', metadataHeaders=['subject', 'from']).execute()
headers = {h['name']: h['value'] for h in detail['payload']['headers']}
email_details.append({"id": msg['id'], "snippet": detail.get('snippet'), "subject": headers.get('Subject'), "from": headers.get('From')})
logger.info(f"Successfully retrieved {len(email_details)} emails.")
return {"messages": email_details}
async def list_calendar_events(p: dict) -> dict:
"""Lists events from a user's calendar."""
user_key, days_ahead = p.get('user_key'), p.get('days_ahead', 7)
if not user_key: raise ValueError("Missing required parameter: 'user_key'")
service = _get_workspace_service('calendar', 'v3', ['https://www.googleapis.com/auth/calendar'])
logger.info(f"Fetching calendar events for '{user_key}' for the next {days_ahead} days...")
now, time_min = datetime.now(timezone.utc), (datetime.now(timezone.utc)).isoformat()
time_max = (now + timedelta(days=days_ahead)).isoformat()
events_result = service.events().list(calendarId=user_key, timeMin=time_min, timeMax=time_max, singleEvents=True, orderBy='startTime').execute()
events = events_result.get('items', [])
formatted_events = [{"summary": event.get('summary'), "start": event.get('start', {}).get('dateTime', event.get('start', {}).get('date')), "end": event.get('end', {}).get('dateTime', event.get('end', {}).get('date')), "organizer": event.get('organizer', {}).get('email')} for event in events]
logger.info(f"Found {len(formatted_events)} events.")
return {"events": formatted_events}
async def trigger_build(p: dict) -> dict:
"""Trigger en Cloud Build manuelt ved å sende kildekode fra Gitea."""
import subprocess
repo = p.get("repo", "OSVauco")
branch = p.get("branch", "main")
config = p.get("config", "cloudbuild.mcp.yaml")
result = subprocess.run([
"gcloud", "builds", "submit",
f"/home/chris_christiansen/OSVauco",
f"--config={config}",
f"--project={GOOGLE_CLOUD_PROJECT}"
], capture_output=True, text=True)
if result.returncode != 0:
error_message = result.stderr.strip()
logger.error(f"Cloud Build trigger failed: {error_message}")
return {"status": "error", "message": error_message}
build_id = "Not found in output"
for line in result.stdout.split('\n'):
if "ID:" in line:
build_id = line.split("ID:")[1].strip()
break
return {"status": "triggered", "build_id": build_id}
# ---------------------------------------------------------------------------
# Ollama helpers — direkte mot emma-gpu-vm
# ---------------------------------------------------------------------------
@ -207,37 +367,37 @@ async def push_file(p):
# ---------------------------------------------------------------------------
TOOLS = {
# ... (alle verktøy beholdt, peker nå på de refaktorerte funksjonene)
# Billing
"get_billing_summary": (get_billing_summary, "Hent billing-sammendrag for OPAX", {}),
"get_billing_forecast": (get_billing_forecast, "Hent billing-prognose", {}),
"get_billing_credits": (get_billing_credits, "Hent gjenværende kreditter", {}),
"get_billing_anomalies": (get_billing_anomalies, "Hent billing-anomalier", {}),
"get_billing_history": (get_billing_history, "Hent billing-historikk", {}),
"get_billing_budget": (get_billing_budget, "Hent nåværende budsjett", {}),
"get_billing_tokens_by_module":(get_billing_tokens_by_module,"Hent token-forbruk per modul", {}),
"set_billing_budget": (set_billing_budget, "Sett månedlig budsjett", {"type":"object","properties":{"amount":{"type":"number"}}}),
# Onboarding
"create_invite": (create_invite, "Inviter ny kunde til OPAX", {"type":"object","properties":{"company":{"type":"string"},"email":{"type":"string"},"tier":{"type":"string"}},"required":["email"]}),
"list_customers": (list_customers, "List alle kunder", {}),
# Notifications
"send_webhook": (send_webhook, "Send webhook-varsling", {"type":"object","properties":{"url":{"type":"string"},"message":{"type":"string"}}}),
"send_email": (send_email, "Send e-post", {"type":"object","properties":{"to":{"type":"string"},"subject":{"type":"string"}},"required":["to","subject"]}),
"send_email": (send_email, "Send e-post via ekstern tjeneste", {"type":"object","properties":{"to":{"type":"string"},"subject":{"type":"string"}},"required":["to","subject"]}),
"send_sms": (send_sms, "Send SMS", {"type":"object","properties":{"to":{"type":"string"},"message":{"type":"string"}},"required":["to","message"]}),
"get_notify_channels": (get_notify_channels, "Hent varslingkanaler", {}),
# AI Agents
"run_jason": (run_jason, "Kjør Jason-agenten med en prompt", {"type":"object","properties":{"prompt":{"type":"string"},"mode":{"type":"string"}},"required":["prompt"]}),
"run_emma": (run_emma, "Emma Vauger (gemma3:27b) — primær lokal AI", {"type":"object","properties":{"prompt":{"type":"string"}}}),
"run_emma_fast": (run_emma_fast, "Emma Fast (gemma3:4b) — rask versjon", {"type":"object","properties":{"prompt":{"type":"string"}}}),
"run_qwen": (run_qwen, "Qwen 2.5 3B — lett hjelper", {"type":"object","properties":{"prompt":{"type":"string"}}}),
"list_emma_models": (list_emma_models, "List alle Ollama-modeller", {}),
# System & Ops
"get_health": (get_health, "Hent helsestatus for OPAX", {}),
"get_build_status": (get_build_status, "Hent siste build-status", {}),
"trigger_build": (trigger_build, "Trigger en Cloud Build manuelt", {"type":"object","properties":{"repo":{"type":"string"},"branch":{"type":"string"},"config":{"type":"string"}}}),
"get_state": (get_state, "Hent platform-tilstand", {}),
"get_telemetry": (get_telemetry, "Hent telemetri-historikk", {}),
"tui_command": (tui_command, "Kjør shell-kommando via TUI-bro", {"type":"object","properties":{"command":{"type":"string"}}}),
"list_customers": (list_customers, "List alle kunder (alias for get_state)", {}),
"run_terminal": (run_terminal, "Kjør terminalkommando på VM", {"type":"object","properties":{"command":{"type":"string"}}}),
# Gitea / VCS
"list_commits": (list_commits, "List siste commits i Gitea-repo", {}),
"get_file": (get_file, "Hent fil fra Gitea-repo", {"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}),
"list_open_issues": (list_open_issues, "List åpne issues i Gitea-repo", {}),
"create_issue": (create_issue, "Opprett nytt issue i Gitea-repo", {"type":"object","properties":{"title":{"type":"string"}}}),
"push_file": (push_file, "Opprett eller oppdater fil i Gitea-repo", {"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"]}),
# Google Workspace
"create_email_alias": (create_email_alias, "Opprett et nytt e-postalias", {"type":"object","properties":{"user_key":{"type":"string"},"alias":{"type":"string"}},"required":["user_key","alias"]}),
"list_user_aliases": (list_user_aliases, "List en brukers e-postaliaser", {"type":"object","properties":{"user_key":{"type":"string"}},"required":["user_key"]}),
"delete_email_alias": (delete_email_alias, "Slett et e-postalias", {"type":"object","properties":{"user_key":{"type":"string"},"alias":{"type":"string"}},"required":["user_key","alias"]}),
"send_email_as": (send_email_as, "Send en e-post på vegne av en bruker/alias", {"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"}},"required":["from","to","subject","body"]}),
"get_workspace_user": (get_workspace_user, "Hent detaljer for en Workspace-bruker", {"type":"object","properties":{"user_key":{"type":"string"}},"required":["user_key"]}),
"list_workspace_users": (list_workspace_users, "List alle brukere i Workspace-domenet", {}),
"get_emails": (get_emails, "Søk i en brukers e-post", {"type":"object","properties":{"user_key":{"type":"string"},"query":{"type":"string"}},"required":["user_key"]}),
"list_calendar_events": (list_calendar_events, "List en brukers kalenderhendelser", {"type":"object","properties":{"user_key":{"type":"string"},"days_ahead":{"type":"integer"}},"required":["user_key"]}),
}
# ---------------------------------------------------------------------------
@ -257,7 +417,7 @@ async def mcp_handler(request: Request):
except Exception: return JSONResponse(_jsonrpc_err(None, -32700, "Parse error"), status_code=400)
method, req_id, params = body.get("method", ""), body.get("id"), body.get("params", {})
if method == "initialize":
return JSONResponse(_jsonrpc_ok(req_id, {"protocolVersion": "2024-11-05", "capabilities": {"tools": {}},"serverInfo": {"name": "opax-mcp", "version": "3.3.0"}})) # BUMPED
return JSONResponse(_jsonrpc_ok(req_id, {"protocolVersion": "2024-11-05", "capabilities": {"tools": {}},"serverInfo": {"name": "opax-mcp", "version": "3.5.0"}})) # BUMPED
if method == "tools/list":
return JSONResponse(_jsonrpc_ok(req_id, {"tools": [
{"name": name, "description": desc, "inputSchema": schema if schema else {"type": "object", "properties": {}}}
@ -283,4 +443,4 @@ async def mcp_handler(request: Request):
@app.get("/health")
async def health():
return {"status": "ok", "service": "opax-mcp", "version": "3.3.0", "ollama": OLLAMA_BASE_URL} # BUMPED
return {"status": "ok", "service": "opax-mcp", "version": "3.5.0", "ollama": OLLAMA_BASE_URL} # BUMPED