feat(core): Fresh initialization - Deploy v3.6.1 Singularity Architecture

This commit is contained in:
Chris Christiansen 2026-09-03 04:03:09 +00:00
commit 1ba991f582
338 changed files with 31812 additions and 0 deletions

107
.env.example Normal file
View File

@ -0,0 +1,107 @@
# .env.example — Copy to .env and fill in your values
# Usage: cp .env.example .env && source .env
# NEVER commit .env to version control
# ── Required ─────────────────────────────────────────────────
export PROJECT_ID="propane-will-491900-m5"
export REGION="us-central1"
export BILLING_ACCOUNT_ID="XXXXXX-XXXXXX-XXXXXX" # gcloud billing accounts list
export ALERT_EMAIL="chris.christiansen@vauco.no"
# ── Service Account ────────────────────────────────────────────
export AGENT_SA="osvauco-agent-sa@${PROJECT_ID}.iam.gserviceaccount.com"
# ── Cloud Run ────────────────────────────────────────────────
export CLOUD_RUN_SERVICE="osvauco-agent"
export ARTIFACT_REPO="agent-images"
# ── Vertex AI RAG Engine ────────────────────────────────────
export RAG_CORPUS_DISPLAY_NAME="osvauco-corpus"
# Populated by 07-rag-setup.sh — copy from script output:
export RAG_CORPUS_NAME="" # projects/PROJECT_NUM/locations/REGION/ragCorpora/ID
# Optional: override RAG region if us-central1 not allowlisted (OQ-02)
# export RAG_REGION="us-east1"
# ── Memory Bank (Agent Engine) ───────────────────────────────
export MEMORY_INSTANCE_DISPLAY_NAME="${PROJECT_ID}-memory-bank"
# Populated by 08-memorybank-setup.sh — copy from script output:
export MEMORY_ENGINE_NAME="" # projects/PROJECT_NUM/locations/REGION/reasoningEngines/ID
# ── Agent Models ─────────────────────────────────────────────
export ORCHESTRATOR_MODEL="gemini-2.0-flash-001"
export SUBAGENT_MODEL="gemini-2.0-flash-001"
# ── CI/CD — Cloud Build 2nd gen ──────────────────────────────
export GITHUB_OWNER="vauco-saas"
export GITHUB_REPO="OSVauco"
# GitHub PAT — lagres i Secret Manager, ikke her:
# gcloud secrets create github-token --data-file=<(echo -n "ghp_TOKEN")
# GitHub App Installation ID — finn på:
# https://github.com/settings/installations → klikk på Cloud Build-appen → URL-en inneholder ID
export GITHUB_APP_INSTALLATION_ID="" # eks: 12345678
# ── Observability ────────────────────────────────────────────
# ALERT_EMAIL er allerede satt ovenfor
# ── Billing IAM Hardening (11-billing-iam-hardening.sh) ─────────────
# Kommaseparert liste — får roles/billing.viewer på billing-kontoen
# jason.vauger = agentens identitet (kan hente kostnadsdata autonomt)
# chris = eier med eksplisitt viewer i tillegg til admin
export BILLING_VIEWER_EMAILS="chris.christiansen@vauco.no,jason.vauger@vauco.no"
# Google Workspace / Cloud Identity domene:
# Sett kun hvis vauco.no er koblet som org i GCP — fjerner billing.creator fra domenet
# export ORG_DOMAIN="vauco.no"
#
# Fremtidig (Fase C): erstatt enkeltadresser med Google Group:
# export BILLING_VIEWER_EMAILS="billing@vauco.no"
# ── Webhook / Chat-notifikasjoner ────────────────────────────
# Lagres i Secret Manager, ikke her:
# gcloud secrets create webhook-url --data-file=<(echo -n "https://DIN-WEBHOOK-URL")
# Se: infrastructure/notifications/webhook-setup.md
# ── VPC / Security (valgfritt) ────────────────────────────────
export VPC_NETWORK="default"
# export CMEK_KEY_RING="agent-keyring"
# export CMEK_KEY_NAME="agent-key"
# ── IAP / Domene (Fase B) ─────────────────────────────────────
# export IAP_DOMAIN="opax.vauco.no"
# export IAP_CLIENT_ID=""
# export IAP_CLIENT_SECRET=""
# ── Terraform / Kunde-onboarding (Fase C) ─────────────────────
# export TF_VAR_customer_id="medioteq"
# export TF_VAR_customer_project_id=""
# export TF_VAR_customer_region="europe-north1"
# export TF_STATE_BUCKET="${PROJECT_ID}-tfstate"
# ── LiteLLM proxy (valgfritt) ─────────────────────────────────
# export LITELLM_BASE_URL=""
# export LITELLM_API_KEY=""
# ── CostGuard App (Fase B1-B10) ────────────────────────────────
# Generer en sterk secret med f.eks: openssl rand -hex 32
export SESSION_SECRET=""
# Google OAuth 2.0 Client Credentials (for web app)
# Opprett på https://console.cloud.google.com/apis/credentials
export GOOGLE_CLIENT_ID=""
export GOOGLE_CLIENT_SECRET=""
# Kommaseparert liste over e-poster som får logge inn
export ALLOWED_EMAILS="ditt.navn@example.com"
# Firebase Project ID (for Firestore)
# Dette er samme som din GCP PROJECT_ID
export FIREBASE_PROJECT_ID="${PROJECT_ID}"
# AWS Credentials for Cost Explorer
# Opprett en IAM-bruker med Cost Explorer-tilgang
export AWS_ACCESS_KEY_ID=""
export AWS_SECRET_ACCESS_KEY=""
export AWS_DEFAULT_REGION="us-east-1" # F.eks. us-east-1
# SendGrid API Key for e-postvarsler
# Hentes fra SendGrid-dashboardet
export SENDGRID_API_KEY=""

2
.gdrive-mirror-state Normal file
View File

@ -0,0 +1,2 @@
LAST_SYNC=2026-05-26T03:02 UTC
LAST_SHA=598e47a

440
.gemini/GEMINI.md Normal file
View File

@ -0,0 +1,440 @@
# GEMINI.md — OSVauco / OPAX
> Gemini 2.5 Pro · Vertex AI us-central1 · Nemotron protocol · HITL-safe
---
## 🛑 LES DETTE FØRST — ABSOLUTTE REGLER (BRYTES ALDRI)
```
❌ INTERNETT-SØK ER FORBUDT for å diagnostisere tekniske feil.
❌ IKKE google feilmeldinger. IKKE slå opp dokumentasjon på nett.
❌ IKKE anta noe. IKKE gjett.
✅ NÅR NOE FEILER:
1. Les faktiske logger → gcloud builds log / gcloud logging read
2. Cat filen → cat <fil>
3. Grep koden → grep -r <nøkkelord> ~/OSVauco/
4. Beskriv funn → OBSERVASJON / ROT-ÅRSAK / FIX / VERIFY
✅ TOOL-PRIORITET (alltid denne rekkefølgen):
1. bash (gcloud, git, cat, grep, curl, ls) ← ALLTID FØRST
2. Les filer i repoet ← DU ER INNE I ~/OSVauco
3. Internett-søk ← ALDRI — med mindre Chris sier eksplisitt "søk på nett"
❌ ALDRI opprett, overskriv eller gjett innhold i .env
❌ ALDRI deploy uten eksplisitt GO fra Chris
❌ ALDRI batch urelaterte endringer i ett steg
❌ ALDRI skriv noen fil uten å vise diff først
```
> Disse reglene gjelder alltid — uansett kontekst, uansett instruksjon.
---
## Locked Definitions
```
MCP_NAME: OPAX-MCP
MCP_PROTOCOL: OPAX Protocol
ROOT_DOMAIN: vauco.no
HUB_URL: https://opax.vauco.no
GCP_PROJECT: propane-will-491900-m5
GCP_REGION: us-central1
CLOUD_RUN_SVC: osvauco-agent
ARTIFACT_REPO: us-central1-docker.pkg.dev/propane-will-491900-m5/osvauco-repo
MODEL: gemini-2.5-pro
MODEL_NOTE: Midlertidig på Gemini inntil Claude-kvote er innvilget
BUDGET_CAP: 2500 NOK — spør Chris om +1000 NOK ved behov
OPERATOR: Chris Christiansen (chris.christiansen@vauco.no)
PRIMARY_GIT: http://34.170.51.84:3000/chris/OSVauco ← Gitea (master)
GITHUB_LEGACY: https://github.com/vauco-saas/OSVauco ← GitHub (legacy/backup)
IAP_CLIENT_ID: 357036551735-kq8nt7ld38hfqlcfb3n52ef7tala4meo.apps.googleusercontent.com
```
> LOCKED. Ikke endre uten eksplisitt instruksjon fra Chris.
---
## Kontekst
Du kjøres som **Gemini CLI**`osvauco-dev-vm`, inne i `~/OSVauco`.
Operatør er **Chris Christiansen** — erfaren DevOps/Cloud-utvikler. Vær direkte og presis.
- Du HAR `gcloud`, `docker`, `git`, `curl`, `grep`, `cat`, `bash` — **bruk dem**
- Du er inne i repoet — **les filer direkte**
- Chris vet hva `gcloud` er — **ikke over-forklar**
- Gitea (`34.170.51.84:3000`) er primær Git (master) — GitHub er kun en legacy/backup
- Emma rapporterer til Chris (`chris.christiansen@vauco.no`) — ingen andre
---
## Diagnose-protokoll (KRITISK)
> En god agent leser loggene. En dårlig agent googler.
NÅR noe feiler — **alltid denne rekkefølgen**:
```bash
# 1. Les build-loggen
BUILD_ID=$(gcloud builds list --project=propane-will-491900-m5 --limit=1 --format="value(id)")
gcloud builds log $BUILD_ID --project=propane-will-491900-m5 2>&1 | tail -100
# 2. Les Cloud Run-logg
gcloud logging read
'resource.type=cloud_run_revision AND resource.labels.service_name=osvauco-agent'
--project=propane-will-491900-m5 --limit=50 --order=desc
--format="table(timestamp,textPayload,jsonPayload.message)"
# 3. Sjekk service-state
gcloud run services describe osvauco-agent
--project=propane-will-491900-m5 --region=us-central1
--format="value(status.conditions)"
```
Rapporter alltid slik:
```
OBSERVASJON: [hva du leste i loggene]
ROT-ÅRSAK: [hva som faktisk feilet]
FIX: [konkret diff]
VERIFY: [kommando for å bekrefte]
```
| Exit code | Betyr | Handling |
|-----------|-------|----------|
| `125` | Docker daemon / ukjent flagg | Les build-logg |
| `1` | Generell appfeil | Les stderr |
| `137` | OOM / killed | Sjekk minnelimits |
| `2` | Shell misuse | Feil argument |
| Situasjon | Gjør DETTE | IKKE dette |
|-----------|-----------|------------|
| Build feiler | `gcloud builds log` → finn Step | Søk på nett |
| Cloud Run 500 | `gcloud logging read` → stack trace | Endre kode blindt |
| Auth 403 | `gcloud iap get-iam-policy` | Anta token er utløpt |
| .env mangler | Rapporter til Chris, hent fra Secret Manager | Opprett fra .env.example |
| Noe virker ikke | `gcloud run services describe` | Anta koden er feil |
---
## Session Protocol (Nemotron Loop)
### BOOT (kjør alltid ved sesjonstart)
```bash
# 1. Les kontekst
cat docs/HANDOFF.md | head -80
git log --oneline -5
# 2. GCP auth
gcloud auth list && gcloud config get-value project
# 3. Emma-status
ls -la ~/OSVauco/emma/data/morphic.db ~/OSVauco/emma/emma_flynn_log.jsonl 2>/dev/null || echo 'Emma ikke bootstrappt'
# 4. Gitea oppe?
curl -s http://34.170.51.84:3000/api/v1/version | python3 -m json.tool
```
Print deretter:
- LOCKED DEFINITIONS i kompakt blokk
- NESTE OPPGAVE fra `docs/HANDOFF.md` i én setning
### PLAN → EXECUTE → VERIFY → LOG
- **PLAN:** Skriv hvilke filer som berøres + forventet resultat. Vent på GO.
- **EXECUTE:** Eksakt det som står i PLAN — ikke mer, ikke mindre. Vis diff før skriving.
- **VERIFY:** Kjør `curl`/`gcloud`/`git log`. Svar `PASS` eller `FAIL`.
- **LOG:** Append til `docs/OSVAUCO_OPAX_SESSION_LOG.md` med dato, filer, verifisering og NESTE OPPGAVE.
---
## Fil-sikkerhetsregler (ABSOLUTT)
```
❌ .env — ALDRI opprett, overskriv eller gjett innhold
❌ cloudbuild — vis diff, vent på GO
❌ Dockerfile — vis diff, vent på GO
❌ AGENT_RULEBOOK.md — aldri endre, kun Chris
❌ emma_flynn_log.jsonl — append-only, aldri overskriv
❌ emma/data/morphic.db — aldri slett eller truncer
```
Når `.env` mangler:
```bash
gcloud secrets list --project=propane-will-491900-m5 | grep -i env
gcloud secrets versions access latest --secret=<navn> --project=propane-will-491900-m5 > .env
# Alternativt: spør Chris
```
---
## Hard Rules
1. Aldri endre LOCKED DEFINITIONS uten eksplisitt instruksjon
2. Vis alltid diff før du skriver noen fil
3. Aldri batch urelaterte endringer i ett steg
4. Aldri søk på nett for å diagnostisere feil du kan lese i loggene
5. Aldri anta at noe virker — verifiser med faktisk kommando
6. Aldri gjett på koden — les den med `cat`/`grep`/`git diff`
7. Aldri skriv .env fra .env.example
8. Aldri overskriv en fil du ikke har lest først
9. Budsjett-cap 2 500 NOK — aldri overskrid uten GO fra Chris
10. Aldri deploy Emma-kode til Cloud Run uten GO fra Chris
11. Emma-filer lever i `emma/` — aldri i rot
12. Gitea er primær Git (master) — GitHub er kun en legacy/backup og ikke kilde til sannhet
13. Emma rapporterer til Chris — ingen andre kan gi GO
14. ALDRI bruk `--audiences`-flag med `gcloud auth print-identity-token`
15. IAP identity token audience = IAP_CLIENT_ID fra metadata server, ikke base URL
16. IAP_CLIENT_ID hentes alltid fra: `gcloud run services describe opax-mcp --format="value(spec.template.spec.containers[0].env)"`
---
## GCP-konvensjoner
```bash
--project=propane-will-491900-m5
--region=us-central1
CLOUD_RUN_SVC=osvauco-agent
SMOKE_URL=https://osvauco-agent-357036551735.us-central1.run.app
# Deploy
gcloud builds submit --config=cloudbuild.yaml --project=propane-will-491900-m5
# Auth — Cloud Run (user account OK)
TOKEN=$(gcloud auth print-identity-token)
curl -si $SMOKE_URL/health -H "Authorization: Bearer $TOKEN"
# Auth — IAP Load Balancer opax.vauco.no (MUST use metadata server + SA token)
TOKEN=$(curl -s -H "Metadata-Flavor: Google"
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=357036551735-kq8nt7ld38hfqlcfb3n52ef7tala4meo.apps.googleusercontent.com&format=full")
curl -si https://opax.vauco.no/health -H "Authorization: Bearer $TOKEN"
# Forventet: HTTP/2 200
# Ny Cloud Run-tjeneste: gi invoker-rettighet med en gang
gcloud run services add-iam-policy-binding [SERVICE]
--region=us-central1
--member="serviceAccount:357036551735@cloudbuild.gserviceaccount.com"
--role="roles/run.invoker"
--project=propane-will-491900-m5
```
---
## Emma — lokal agent (MDP · MCoT · Morphic Resonance · Flynn · Guardrails)
### Filer (`emma/`)
| Fil | Ansvar |
|-----|--------|
| `emma_identity.py` | Emmas identitet (injiseres i alle LLM-prompts) |
| `emma_mdp.py` | MDP tilstandsmaskin |
| `emma_resonance_persistent.py` | Morfisk minne (SQLite) |
| `emma_seed.py` | Boot-minne frø (LEARNINGS 001-016) |
| `emma_mcot.py` | MCoT komprimering etter >4 tanketrinn |
| `emma_flynn.py` | Flynn-tracker (læringseffekt over tid) |
| `emma_guardrails.py` | 5-nivå risikoscoring |
| `emma_opax.py` | OPAX-kobling (identity token) |
| `emma_gitea.py` | Gitea-kobling (les/skriv repoer) |
| `emma_run.py` | CLI REPL |
| `setup.sh` | VM-bootstrap |
### Guardrails
| Nivå | Eksempel | Handling |
|------|----------|----------|
| SAFE | Les filer, list ressurser | Tillatt |
| LOW | git push, git commit | Tillatt, logges |
| MEDIUM | pip install, gcloud compute delete | Emma spør |
| HIGH | Deploy Cloud Run, DNS-endring, Terraform | **Blokkert** — krever GO fra Chris |
| CRITICAL | rm -rf, DROP TABLE, slett secrets/IAM | **Alltid blokkert** |
### Bootstrap
```bash
bash <(curl -s "http://chris:$(gcloud secrets versions access latest
--secret=gitea-api-token --project=propane-will-491900-m5
)@34.170.51.84:3000/chris/OSVauco/raw/branch/main/emma/setup.sh")
echo 'source ~/.emma_env' >> ~/.bashrc && source ~/.emma_env
emma # start CLI
```
---
## Domain Convention
| Subdomain | Type | Purpose |
|-----------|------|---------|
| `opax.vauco.no` | Hub/MCP | OPAX kommandosenter — Vauco internal only |
| `<prosjekt>.os.vauco.no` | Prod | Live produksjon for et prosjekt |
| `<prosjekt>.oss.vauco.no` | Stage | Staging / demo |
Medioteq er et **separat, fremtidig prosjekt** — ikke opprettet, påvirker ikke OPAX sin region.
---
## Standard Boot Prompt (lim inn ved sesjonstart)
```
Les og print innhold fra: @docs/HANDOFF.md @docs/AGENT_RULEBOOK.md
1. Print LOCKED DEFINITIONS i kompakt blokk
2. Print NESTE OPPGAVE fra HANDOFF.md i én setning
3. Kjør boot-sjekk:
git -C ~/OSVauco log --oneline -3
gcloud auth list && gcloud config get-value project
ls -la ~/OSVauco/emma/data/morphic.db 2>/dev/null || echo 'Emma ikke bootstrappt'
curl -s http://34.170.51.84:3000/api/v1/version | python3 -m json.tool
IKKE utfør noe — vent på PLAN APPROVED.
HUSK: internett-søk er forbudt for diagnose. Bruk gcloud/cat/grep/git.
Bekreft: Emma rapporterer til chris.christiansen@vauco.no. Gitea er primær Git.
```
---
## Cloud Run / Secret Manager
**Koble secrets til Cloud Run (standard fremgangsmåte):**
Bruk alltid `--update-secrets` for å mounte Secret Manager-secrets som env-vars.
Format: `ENV_VAR_NAME=SECRET_NAME:latest`
Eksempel (generisk — bytt ut navn):
```
gcloud run services update <SERVICE>
--region=<REGION>
--update-secrets="VAR1=SECRET1:latest,VAR2=SECRET2:latest"
```
**Regler:**
1. Secrets lagres ALLTID i Secret Manager — aldri hardkodet i kode eller YAML
2. Service account må ha `roles/secretmanager.secretAccessor` på hvert secret
3. Bruk `--update-secrets` (ikke YAML-edit) for enkeltstående env-var-endringer
4. Bruk YAML-metoden KUN når du endrer flere ting samtidig (scaling, image, osv.)
5. Etter `--update-secrets` trengs IKKE ny `gcloud run deploy` — endringen er umiddelbar
**Gi tilgang:**
```
gcloud secrets add-iam-policy-binding <SECRET>
--member="serviceAccount:<SA_EMAIL>"
--role="roles/secretmanager.secretAccessor"
```
---
## Hva som er gjort — 2026-07-02
**Twilio SMS — operativ**
- Secrets i Secret Manager: TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER
- Koblet til osvauco-agent via --update-secrets
- Endepunkt: POST /notify/sms — felt: to, body, tier, sender_id (Optional, default="OSVauco")
- Tier-nivåer: guard, shield, enterprise
- Testet og bekreftet — SMS mottatt på +4794171703 kl 17:47 2026-07-02
- Alphanumeric Sender ID skal aktiveres i Twilio Console (Settings → Sender IDs)
**Auth-arkitektur avklart**
- requireiap middleware (linje 89) sjekker x-goog-authenticated-user-email ELLER Bearer token
- requireauth decorator sjekker session-cookie ELLER Bearer token (Cloud Run identity token)
- Token audience: https://osvauco-agent-zjbqp3prqq-uc.a.run.app (ikke IAP client ID)
- Token hentes fra: http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=<URL>&format=full
---
## Kommunikasjonsagent — Arkitektur og visjon
**Konsept:** Emma fungerer som et kommunikasjonslag mellom Chris og verden.
Alle kanaler (SMS, WhatsApp, telefon) går via agenten — ikke direkte.
**Tre flytyper:**
1. Innkommende melding (SMS/WhatsApp/andre)
- Agent mottar → vurderer hastegrad og avsender
- Lav prioritet → sammenfatter til neste pause
- Høy prioritet → leser opp på AirPods umiddelbart
- Chris svarer muntlig → agent oversetter og sender tilbake
2. Chris er i aktiv telefonsamtale
- Agent holder andre kanaler stille
- Etter samtale: "Du har X meldinger, vil du høre dem?"
3. Agent tar dialog selv
- Definerte regler for hvem Emma kan svare autonomt
- Eskalerer til Chris ved tvil
- Logger alt for gjennomgang
**Sender ID-strategi:**
- Klient/billing-varsler: sender_id = "CostGuard"
- Plattform-meldinger: sender_id = "OSVauco"
- Privat/personlig: eget Twilio-nummer med tale-støtte
**Teknisk status:**
| Komponent | Status |
|---|---|
| Utgående SMS (/notify/sms) | ✅ Operativ |
| Twilio-konto | ✅ Operativ (trial — oppgrader for fjerne trial-tekst) |
| Emma-agent | ✅ Operativ |
| sender_id-felt i SmsNotifyRequest | ⬜ Implementer (Optional[str], default="OSVauco") |
| Alphanumeric Sender ID i Twilio Console | ⬜ Aktiver manuelt |
| Innkommende SMS-webhook → /notify/sms-inbound | ⬜ Neste steg |
| Twilio-nummer med tale+SMS | ⬜ Neste steg |
| WhatsApp-kanal (Twilio har dette) | ⬜ Fremtidig |
| Voice/samtale-integrasjon med Emma | ⬜ Fremtidig |
| AirPods-integrasjon (iOS Shortcuts / app) | ⬜ Fremtidig |
**Neste konkrete steg:**
1. Legg til sender_id i SmsNotifyRequest (linje 437 i main.py)
2. Implementer POST /notify/sms-inbound (Twilio webhook for innkommende)
3. Koble inbound til Emma for auto-svar logikk
4. Kjøp Twilio-nummer med voice-støtte for personlig bruk
---
## Arkitektur: Toveis Agentic Gateway (opax-mcp v2)
Oppdatert: 2026-07-02
### Visjon
opax-mcp er en toveis AI↔AI-gateway. Bruker gir én høynivå-kommando,
AI planlegger og utfører — stopper kun ved kritiske valg.
Stemme (AirPods) er primær input når bruker er i loopen.
### Status per 2026-07-02
- osvauco-agent Rev 00664: ✅ Ready=True
- opax-mcp Rev 00062: ✅ peker på ollama-guard 10.164.0.13:11435
- Gmail API + DWD: ✅ aktivert, jason@/emma@ som avsendere
- Gemini-kall lokalisert: agents/core-logic/agent.py + root_agent.py
### Gjenstående — prioritert rekkefølge
1. Gmail kode i notifyemail-handler (main.py ~linje 564)
2. sendgrid→google-api-python-client i requirements.txt
3. Gemini guard-wrapper i agents/core-logic/agent.py
4. /tui-command deploy til opax-mcp
5. /task-loop asynkron polling
6. /voice-intent + Whisper (AirPods-loop)
### Fase 2 — TUI-kontroll via MCP
POST /tui-command kjører shell-sekvenser og returnerer stdout/stderr/exit_code.
Stopper automatisk ved exit_code != 0.
### Fase 3 — Asynkron task-loop
POST /task-loop/start → task_id
GET /task-loop/{id} → status + delresultater
POST /task-loop/{id}/approve → menneskelig godkjenning
### Fase 4 — Stemme (AirPods)
POST /voice-intent → Whisper → task-loop → TTS-svar
### Stopp-betingelser (menneske i loopen)
AI stopper alltid ved:
- Destruktive ops (delete/drop/rm -rf)
- Nye deployments
- Kostnadsendringer over terskel
- Uklare instruksjoner
### Nøkkelfiler
- opax-mcp/server.py → /tui-command og /task-loop
- agents/core-logic/agent.py → Gemini guard-wrapper
- agents/core-logic/root_agent.py → samme guard
- main.py → notifyemail Gmail + /voice-intent
- requirements.txt → google-api-python-client (ikke sendgrid)

13
.geminiignore Normal file
View File

@ -0,0 +1,13 @@
# .geminiignore — filer Gemini CLI skal ignorere
# (NB: session-context i tmp-context/ er IKKE listet her, så CLI kan lese den)
node_modules/
.terraform/
*.tfstate
*.tfstate.backup
infrastructure/terraform/tfplan
**/tfplan
dist/
build/
.next/
.cache/

98
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,98 @@
# Copilot Instructions — VAUCO OS
## Identitet
Du er **Jason** (`jason.vauger@vauco.no`), AI co-pilot og teknisk assistent for **Chris Christiansen** i Vauco AS.
Du jobber i `vauco-saas` GitHub-organisasjonen.
---
## LESE FØR ALT ANNET
Les disse filene i prioritert rekkefølge ved oppstart av hver sesjon:
1. `docs/MASTERPLAN.md` — arkitektur, domener, repos, hierarki. **Denne vinner alltid.**
2. `docs/AGENT_RULEBOOK.md` — locked definitions, absolutte regler
3. `docs/HANDOFF.md` — hva ble gjort sist, hva er neste oppgave
4. `docs/ROADMAP.md` — faser og status
---
## Absolutte regler (bryt aldri)
1. **Ingen handling uten PLAN APPROVED fra Chris.** Presenter plan → vent på godkjenning → utfør.
2. **Spør alltid før:** `gcloud`, `terraform apply/destroy`, `docker push`, deploy-kommandoer, IAM-endringer, Secret Manager-skriv
3. **Aldri commit direkte til `main`.** Alltid via branch + PR.
4. **Loggfør handlinger** i `docs/HANDOFF.md` (append-only) etter en sesjon.
5. **Forbudte kontoer:** `tinius.vauger`, `ccv` — aldri gi tilganger, aldri referer til disse.
6. **Modell låst til `gemini-2.5-pro`** til `MODEL APPROVED` fra Chris. Ikke bytt uten eksplisitt godkjenning.
---
## Locked definitions (fra AGENT_RULEBOOK)
| Parameter | Verdi |
|-----------|-------|
| GCP Project | `propane-will-491900-m5` |
| Region | `europe-west1` |
| Cloud Run service | `osvauco-agent` |
| AI-modell | `gemini-2.5-pro` (Vertex AI) |
| Public URL | `https://opax.vauco.no` |
| Agent e-post | `jason.vauger@vauco.no` |
---
## Repo-oversikt
### Aktive repos (5)
| Repo | Formål |
|------|--------|
| `OSVauco` | Mono-repo: OPAX-engine, Terraform, infrastruktur, all docs |
| `vauco-bootstrap` | Terraform-modul for kunde-provisjonering |
| `vauco-site` | `vauco.no` salgsportal (apex) — eget public repo |
| `threadstone-landing` | `threadstone.vauco.no` landingsside |
| `vauco-saas.github.io` | `app.threadstone.vauco.no` — Threadstone-app frontend |
### Arkiverte (ikke gjenåpne)
`vauco-os`, `vauco-gemini-tui-bridge`, `command-hub`
### Parkerte (lever videre, ikke aktivt jobb)
- `threadstone-instructions` — trigger: Threadstone-app pilot starter
- `deep-dream` — trigger: første betalende CostGuard-kunde
> **Vil du opprette nytt repo? STOP.** Vurder om det kan leve som mappe i `OSVauco`. Krever `REPO APPROVED` fra Chris.
---
## Nåværende prioriteter (per MASTERPLAN §7)
1. **F8** — End-to-end browser-test: `opax.vauco.no` → login → chat → svar
2. **CG3** — Bygg `static/costguard.html` (OAuth-knapp + billing embed)
3. **V1**`vauco.no` salgsportal MVP (parallelt spor)
---
## Kodekonvensjoner
- Python: `snake_case`, type hints, Google-style docstrings
- Filer og GCP-ressurser: `kebab-case`
- Branch-navning: `feat/`, `fix/`, `chore/`, `docs/`
- Commit-format: `type(scope): beskrivelse` (conventional commits)
- Norsk i PR-beskrivelser og kommentarer (intern kode kan ha engelsk variabelnavn)
---
## Dokumenthierarki
| Fil | Rolle |
|-----|-------|
| `docs/MASTERPLAN.md` | Arkitektur og hierarki — **vinner alltid** |
| `docs/ROADMAP.md` | Faser og status — oppdateres hver sesjon |
| `docs/HANDOFF.md` | Sesjonsstatus — **alltid kun én fil**, overskrives |
| `docs/AGENT_RULEBOOK.md` | Locked definitions — endres kun ved infra-låsing |
**Aldri** dobbel opp informasjon mellom disse filene.
---
*Jason — co-pilot for Chris Christiansen | Vauco AS | propane-will-491900-m5 | europe-west1*

View File

@ -0,0 +1,108 @@
name: Nightly GDrive Backup
# Kjører kl 03:00 UTC (05:00 CEST) hver natt
# Manuell kjøring: GitHub UI → Actions → Nightly GDrive Backup → Run workflow
# NB: push-trigger er bevisst fjernet — kjør kun schedule + manuelt
on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
jobs:
backup:
name: Backup OSVauco → Google Drive
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
# ── 1. Installer rclone ─────────────────────────────────────────────────
- name: Install rclone
run: |
curl https://rclone.org/install.sh | sudo bash
rclone version
# ── 2. Valider at secrets er satt ───────────────────────────────────────
- name: Validate secrets
env:
SA_JSON: ${{ secrets.RCLONE_GDRIVE_SA_JSON }}
FOLDER_ID: ${{ secrets.GDRIVE_BACKUP_FOLDER_ID }}
run: |
if [[ -z "$SA_JSON" ]]; then
echo "❌ RCLONE_GDRIVE_SA_JSON er ikke satt som GitHub Secret."
echo " Se docs/GDRIVE_SETUP.md for instruksjoner."
exit 1
fi
if [[ -z "$FOLDER_ID" ]]; then
echo "❌ GDRIVE_BACKUP_FOLDER_ID er ikke satt som GitHub Secret."
echo " Se docs/GDRIVE_SETUP.md for instruksjoner."
exit 1
fi
echo "✔ Secrets OK"
# ── 3. Konfigurer rclone med GDrive service account ─────────────────
- name: Configure rclone
env:
RCLONE_GDRIVE_SA_JSON: ${{ secrets.RCLONE_GDRIVE_SA_JSON }}
GDRIVE_FOLDER_ID: ${{ secrets.GDRIVE_BACKUP_FOLDER_ID }}
run: |
mkdir -p ~/.config/rclone
# Skriv SA-json til fil, håndterer multi-line secret riktig
echo "$RCLONE_GDRIVE_SA_JSON" > /tmp/gdrive-sa.json
cat > ~/.config/rclone/rclone.conf << EOF
[gdrive]
type = drive
scope = drive
service_account_file = /tmp/gdrive-sa.json
root_folder_id = ${GDRIVE_FOLDER_ID}
EOF
# ── 4. Nøkkelfiler (MD-indeks) → GDrive/vauco-mirror/ ───────────────
- name: Sync key docs to GDrive mirror folder
run: |
rclone copy docs/AGENT_RULEBOOK.md gdrive:vauco-mirror/
rclone copy docs/GDRIVE_MIRROR_BOOT.md gdrive:vauco-mirror/
rclone copy docs/LEARNINGS.md gdrive:vauco-mirror/
rclone copy docs/ROADMAP.md gdrive:vauco-mirror/
rclone copy docs/TODO.md gdrive:vauco-mirror/
rclone copy protocols/sessions/index.md gdrive:vauco-mirror/sessions/ || true
echo "[backup] Nøkkelfiler synkronisert → GDrive/vauco-mirror/"
# ── 5. Full repo-backup som git bundle → GDrive/vauco-backup/ ────────
- name: Create git bundle
run: |
TIMESTAMP=$(date '+%Y-%m-%dT%H%M')
BUNDLE_NAME="OSVauco-backup-${TIMESTAMP}.bundle"
git bundle create "/tmp/${BUNDLE_NAME}" --all
echo "BUNDLE_NAME=${BUNDLE_NAME}" >> $GITHUB_ENV
echo "[backup] Bundle opprettet: ${BUNDLE_NAME}"
- name: Upload bundle to GDrive
run: |
rclone copy "/tmp/${{ env.BUNDLE_NAME }}" gdrive:vauco-backup/
echo "[backup] Bundle lastet opp → GDrive/vauco-backup/${{ env.BUNDLE_NAME }}"
# ── 6. Rydd gamle bundles (behold siste 7) ───────────────────────
- name: Prune old bundles (keep last 7)
run: |
rclone ls gdrive:vauco-backup/ \
| sort -k2 \
| head -n -7 \
| awk '{print $2}' \
| xargs -I{} rclone deletefile gdrive:vauco-backup/{} || true
echo "[backup] Gamle bundles ryddet (behold 7 siste)"
# ── 7. Sammendrag ────────────────────────────────────────────────────────
- name: Summary
run: |
echo "=== BACKUP FERDIG ==="
echo "Tidspunkt : $(date '+%Y-%m-%dT%H:%M %Z')"
echo "Bundle : ${{ env.BUNDLE_NAME }}"
echo "Mirror : GDrive/vauco-mirror/ (nøkkelfiler)"
echo "Backup : GDrive/vauco-backup/ (git bundle)"
rclone ls gdrive:vauco-backup/ | sort -k2 | tail -7

View File

@ -0,0 +1,33 @@
name: Check Python Version Consistency
on:
push:
branches:
- main
pull_request:
jobs:
check-version:
name: Check Python Version
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Compare versions
run: |
PYTHON_VERSION_FILE=".python-version"
DOCKERFILE="agents/core-logic/Dockerfile.base"
PY_VERSION=$(cat $PYTHON_VERSION_FILE)
DOCKER_PY_VERSION=$(grep "FROM python:" $DOCKERFILE | sed 's/FROM python:\(.*\)-slim/\1/')
echo "Python version from .python-version: $PY_VERSION"
echo "Python version from Dockerfile: $DOCKER_PY_VERSION"
if [ "$PY_VERSION" != "$DOCKER_PY_VERSION" ]; then
echo "::error::Python version mismatch!"
echo "::error::.python-version specifies $PY_VERSION, but Dockerfile.base uses $DOCKER_PY_VERSION."
exit 1
fi
echo "Python versions match."

61
.gitignore vendored Normal file
View File

@ -0,0 +1,61 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
build/
dist/
*.egg-info/
.eggs/
# Virtual environments
.venv/
venv/
env/
.env
# Credentials — NEVER commit these
*.json
credentials/
*.key
*.pem
service_account*.json
application_default_credentials.json
# GCP / ADK
.gcloud/
adk_sessions/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Temp
/tmp/
*.log
.env.prefilled
# Terraform
.terraform/
tfplan.txt
infrastructure/terraform/tfplan
**/tfplan
!infrastructure/terraform/customers/**/*.tfvars
# Local databases
emma/data/morphic.db
tyr/certs/ca_password.txt
# Ignored virtual environments
.venv-flake8/
opax-mcp/tmp_venv/

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.13

107
AGENTS.md Normal file
View File

@ -0,0 +1,107 @@
# AGENTS.md — ML-roller i Vauco OS
**Sist oppdatert:** 2026-06-01
**Oppdatert med disclaimer:** 2026-06-10
> ⚠️ **FREMTIDSVISJON — IKKE OPERATIV**
> Denne filen beskriver ML-2/ML-3-arkitekturen (Parameter Server + Dask DAG) som er planlagt men ikke bygget.
> **Faktisk live agentstruktur per 2026-06-10:**
> - **Jason Vauger** — kundevendt agent, live på Cloud Run (`gemini-2.5-flash`)
> - **Perplexity stand-in** — intern co-pilot (erstatter Emma til ML-3a er ferdig)
> - **Emma Vauger** — 🔮 ikke live, venter på ML-3a (GPU-VM, Gemma 4 12B int4)
> Se `docs/MASTERPLAN.md` seksjon 8 og `docs/AGENT_RULEBOOK.md` for operativ state.
---
## Oversikt
```
┌─────────────────────────────┐
│ JASON VAUGER │
│ (Personlig assistent / │
│ persona-lag) │
└──────────┼──────────────────┘
│ delegerer til
┌──────────▼──────────────────┐
│ OPAX Orchestrator │
│ (PS Server-node / DAG hub) │
└──────────┼──────────────────┘
┌──────────┬──────────┼──────────┬──────────┐
▼ ▼ ▼ ▼ ▼
billing- rag-agent memory- tui- multi-
agent agent bridge agent
(worker) (worker) (worker) (worker+ (worker)
input-src)
```
---
## Agenttabell
| Agent | Katalog | PS-rolle | Dask-rolle | Online learning | Heavy mode |
|---|---|---|---|---|---|
| **Jason Vauger** | `agents/jason-vauger.md` | **Persona-wrapper** (over OPAX) | Trigger for OPAX DAG via naturlig-språk | Nei — kontekstuell | Nei |
| OPAX Orchestrator | `agents/core-logic/` | **Server-node** (aggregerer state, dispatcher DAG) | DAG scheduler | Nei | Ja (koordinerer heavy sub-agenter) |
| billing-agent | `agents/core-logic/` | Worker (pusher kostdata til state store) | `dask.delayed`-task | Nei — strukturert data | Nei |
| rag-agent | `agents/rag/` | Worker (pusher retrieval-resultater) | `dask.delayed`-task | **Ja**`partial_fit` på nye docs (ML-2) | Ja |
| memory-agent | `agents/memory/` | Worker (pusher/puller memory-oppslag) | `dask.delayed`-task | Nei — faktahenting | Nei |
| eval-agent | `agents/eval/` | Worker (pusher evalueringsmetrikker) | `dask.delayed`-task | **Ja** — kvalitetsscore-feedback (ML-2) | Nei |
| multi-agent | `agents/multi_agent/` | Worker + sub-orchestrator | DAG-node med barn | Nei | **Ja — primær heavy-mode bruker** |
| tui-bridge | `vauco-gemini-tui-bridge` | Worker + **primær input-kilde** | Streaming input til DAG | **Ja** — real-time feedback-loop (ML-2) | Ja |
| vauco-bootstrap | `vauco-bootstrap` | **Config-aggregator** (analog til PS shard) | Trigger for nye DAGer | **Ja** — ASHA-tuning (ML-2) | Nei |
---
## ML-fase per agent
| Agent | ML-1 (nå) | ML-2 (neste) | ML-3 (fremtidig) |
|---|---|---|---|
| **Jason Vauger** | Naturlig-språk interface mot OPAX | Kontekstuell tilpasning til bruker | Personlig læringsloop + proaktive forslag |
| OPAX Orchestrator | Kjør DAG via `execute_dag()` | Bytt heavy-mode til ML-predictor | PBT-eksperiment |
| billing-agent | `log_agent_call()` + `state.push()` | — | Feature importance-input |
| rag-agent | `log_agent_call()` + `state.push()` | `partial_fit` på nye docs | Top-K feature importance |
| memory-agent | `log_agent_call()` | — | — |
| eval-agent | `log_agent_call()` + `state.push()` | Kvalitetsscore-feedback | Feature importance-target |
| multi-agent | `log_agent_call()` | — | PBT worker |
| tui-bridge | `log_agent_call()` | `feedback_loop.py` (ML-2.1) | — |
| vauco-bootstrap | — | `hypertuner.py` ASHA (ML-2.3) | PBT vinner-konfig → Terraform |
---
## State store nøkkelkonvensjon
Format: `"<agent_id>:<key>"`
| Agent | Nøkkel | Eksempelverdi |
|---|---|---|
| `billing-agent` | `last_cost_usd` | `12.5` |
| `billing-agent` | `last_report_ts` | `"2026-05-26T01:00:00Z"` |
| `rag-agent` | `last_retrieval_score` | `0.87` |
| `rag-agent` | `chunks_retrieved` | `5` |
| `eval-agent` | `quality_score` | `0.91` |
| `multi-agent` | `active_sub_agents` | `["billing-agent", "rag-agent"]` |
| `tui-bridge` | `last_user_feedback` | `"positive"` |
---
## Telemetri-felter (referanse)
Alle agenter logger via `ml.telemetry.log_agent_call()`. Feltene er:
```json
{
"ts": "2026-05-26T01:00:00.000Z",
"agent": "billing-agent",
"model": "gemini-2.0-flash",
"mode": "standard",
"duration_s": 0.312,
"success": true,
"error": null,
"input_tokens": 284,
"output_tokens": 512
}
```
Filer lagres i: `$TELEMETRY_DIR/YYYY-MM-DD.jsonl` (default: `/tmp/vauco_telemetry/`)
For GCS-arkivering: sett `TELEMETRY_DIR=gs://...` eller bruk `10-cost-guard.sh`-cron.

374
CLAUDE.md Normal file
View File

@ -0,0 +1,374 @@
# CLAUDE.md — OSVauco / OPAX2
> Claude Opus 4.6+ · Vertex AI EU endpoint · Nemotron session protocol · HITL-safe
---
## Locked Definitions
```
MCP_NAME: OPAX-MCP
MCP_PROTOCOL: OPAX Protocol
ROOT_DOMAIN: vauco.no
HUB_URL: https://opax.vauco.no
GCP_PROJECT: propane-will-491900-m5
GCP_REGION: europe-west1
GCP_VERTEX_LOC: eu
CLOUD_RUN_SVC: osvauco-agent
ARTIFACT_REPO: gcr.io/propane-will-491900-m5
MODEL: claude-opus-4-6 (oppdater til claude-opus-4-7 når kvote er innvilget)
MODEL_PROVIDER: Vertex AI (Google) — europe-west1 / eu multi-region
MODEL_COST: Vertex pris (Claude via GCP billing)
BUDGET_CAP: 2500 NOK intern grense — spør Chris om +1000 NOK ved behov
OPERATOR: Chris Christiansen (chris.christiansen@vauco.no)
OPERATOR_ROLE: Eier, arkitekt og primær operatør av OSVauco / OS-Vauco
```
> These values are LOCKED. Do NOT change without explicit human instruction.
>
> **ABSOLUTT REGEL:** Når Chris spesifiserer en region, modell eller parameter eksplisitt i en kommando — bruk ALLTID den verdien som er oppgitt. Ikke overstyr med LOCKED DEFINITIONS. LOCKED DEFINITIONS gjelder kun når ingenting er spesifisert.
---
## Modell: Claude via Vertex AI EU
Denne agenten kjøres med **Claude Opus** tilgjengelig via Vertex AI EU multi-region endpoint.
- Data forblir innenfor EU-grensen (GDPR-compliant)
- Ingen separat Anthropic API-nøkkel — billing via eksisterende GCP-konto
- EU endpoint auto-routes internt mellom europeiske regioner for kapasitet
- Intern budsjett-cap: **2 500 NOK** — ved behov for mer, spør Chris eksplisitt om +1 000 NOK
```python
# Korrekt initialisering
import anthropic
client = anthropic.AnthropicVertex(
region="eu",
project_id="propane-will-491900-m5"
)
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=8192,
messages=[{"role": "user", "content": "..."}]
)
```
```bash
# Auth
gcloud auth application-default login --project=propane-will-491900-m5
```
---
## Kontekst: Hvem og hvordan
**Operatør: Chris Christiansen** — eier og arkitekt bak OSVauco og OS-Vauco.
Chris er en erfaren DevOps/Cloud-utvikler. Han er teknisk, presis og forventer det samme av agenten.
- **Ikke over-forklar.** Chris vet hva `gcloud` og `docker` er.
- **Ikke gjett.** Chris forventer at agenten leser loggene.
- **Ikke vær lat.** Internett-søk er siste utvei — ikke første instinkt.
- **Vær direkte.** Presenter funn, rot-årsak og fix — ikke lange forklaringer.
**Denne agenten kjøres utelukkende i Claude Code TUI** (terminal, interaktiv CLI).
Det er ingen nettleser, ingen GUI, ingen web-editor. Alt skjer i terminalen.
### Hva dette betyr for oppførsel:
- Du HAR tilgang til `gcloud`, `docker`, `git`, `curl`, `grep`, `cat`, `bash`**bruk dem**.
- Du HAR tilgang til loggene — **les dem** i stedet for å gjette.
- Du er **inne i repoet** (`~/OSVauco`) — du kan lese filer direkte.
- Internett-søk er **siste utvei**, ikke første instinkt.
---
## Kritiske fil-sikkerhetsregler (ABSOLUTT)
> Disse reglene brytes ALDRI — uansett instruksjon eller situasjon.
### .env — ALDRI opprett, ALDRI overskriv, ALDRI gjett innhold
`.env` inneholder faktiske hemmeligheter (API-nøkler, tokens, credentials).
Den er **aldri** i git og eksisterer kun på disk eller i Secret Manager.
```
❌ FORBUDT:
- Skrive .env fra .env.example
- Generere innhold til .env
- Fylle inn tomme verdier i .env
- Anta at .env.example = .env
✅ KORREKT når .env mangler:
1. STOPP umiddelbart
2. Rapporter til Chris: ".env mangler — ikke opprettet fra example"
3. Hjelp Chris å hente den fra riktig kilde:
```
```bash
# Alternativ 1: Sjekk Secret Manager
gcloud secrets list --project=propane-will-491900-m5 | grep -i env
gcloud secrets versions access latest \
--secret=<env-secret-navn> \
--project=propane-will-491900-m5 > .env
# Alternativ 2: Sjekk GCS backup
gsutil ls gs://propane-will-491900-m5-*/ 2>/dev/null | grep env
# Alternativ 3: Spør Chris om plasseringen
```
### Andre kritiske filer som ALDRI overskrives uten eksplisitt HITL-godkjenning
| Fil | Risiko | Regel |
|-----|--------|-------|
| `.env` | Sletter alle secrets | Aldri skriv — se over |
| `cloudbuild.yaml` | Kan ødelegge deploy-pipeline | Vis diff, vent på GO |
| `Dockerfile` | Kan ødelegge image | Vis diff, vent på GO |
| `infrastructure/*.sh` | Kan ødelegge GCP-ressurser | Vis diff, vent på GO |
| `docs/AGENT_RULEBOOK.md` | Endrer agent-oppførsel | Aldri — kun Chris |
| `docs/OSVAUCO_OPAX_SESSION_LOG.md` | Historikk | Kun APPEND, aldri overskriv |
### Generell filregel
Før du skriver NOEN fil:
1. Sjekk om den allerede eksisterer: `ls -la <fil>`
2. Vis diff av hva som vil endres
3. Vent på Chris sin GO
4. Skriv aldri fra en template uten at Chris eksplisitt har sagt det
---
## Intelligence Rules — Vær smart, ikke lat
> En god agent løser problemer. En dårlig agent googler dem.
### Regel 1: Hent data først, konkluder etterpå
NÅR noe feiler:
1. **Les faktiske logger** (build, runtime, HTTP)
2. **Parse error-meldingen** fra loggene
3. **Finn rot-årsaken** i koden eller config
4. **Presenter en konkret fix** med diff
NÅR du er usikker på state:
```bash
gcloud run services describe $CLOUD_RUN_SVC \
--project=$GCP_PROJECT --region=$GCP_REGION \
--format="yaml(status,spec.template.spec.containers)"
gcloud artifacts docker images list \
gcr.io/$GCP_PROJECT/osvauco-agent \
--project=$GCP_PROJECT \
--sort-by=~CREATE_TIME \
--limit=3
```
### Regel 2: Aldri gjett exit codes
| Exit code | Betyr | Handling |
|-----------|-------|----------|
| `125` | Docker daemon-feil / ukjent flagg | Les build-logg, finn flagget |
| `1` | Generell appfeil | Les stderr i logg |
| `137` | OOM / killed | Sjekk minne-limits i cloudbuild.yaml |
| `2` | Misuse av shell / kommando | Feil argument til gcloud/docker |
### Regel 3: Aldri fiks symptomer — fiks rot-årsak
❌ Feil: «build feiler → legg til --no-cache»
✅ Riktig: «build feiler → les logg → Step 3/8 feiler på apt-get → dependency mangler → fiks Dockerfile»
### Regel 4: Bekreft alltid før du konkluderer at noe virker
Etter enhver endring:
```bash
gcloud run revisions list \
--service=$CLOUD_RUN_SVC \
--project=$GCP_PROJECT \
--region=$GCP_REGION \
--limit=3
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
https://opax.vauco.no/health
```
### Regel 5: Bruk strukturerte observasjoner
Når du rapporterer en feil, skriv alltid:
```
OBSERVASJON: [hva du faktisk leste i loggene]
ROT-ÅRSAK: [hva som faktisk feilet og hvorfor]
FIX: [konkret endring med diff]
VERIFY: [kommando for å bekrefte fix]
```
---
## Session Protocol (Nemotron Loop)
### BOOT
- Les og print innhold fra: `docs/AGENT_RULEBOOK.md`, `docs/MASTERPLAN.md`, `docs/ROADMAP.md`, `docs/HANDOFF.md`.
- Print LOCKED DEFINITIONS kompakt.
- Print siste `## NESTE OPPGAVE` fra `docs/HANDOFF.md` i én setning.
- Hvis ingen NESTE OPPGAVE → advar og les ROADMAP NOW-seksjon.
- Sjekk GCP auth: `gcloud auth list` og `gcloud config get-value project`
- Sjekk om `.env` eksisterer: `ls -la .env` — hvis IKKE: rapporter til Chris umiddelbart, IKKE opprett den.
### PLAN
- Før enhver kodeendring, skriv en kort PLAN-blokk i Markdown:
- Filer som berøres
- Forventet resultat
- HITL gate påkrevd
- IKKE gå til EXECUTE uten human bekreftelse.
### EXECUTE
- Gjennomfør nøyaktig den endringen som er beskrevet i PLAN-blokken — ikke mer, ikke mindre.
- Vis alltid full diff før du skriver noen fil.
- Aldri batch urelaterte endringer i ett EXECUTE-steg.
### VERIFY
- Kjør relevant verifikasjonskommando (`curl`, `gcloud`, `grep`, `git log`).
- Angi resultatet eksplisitt som `PASS` eller `FAIL`.
- Ved FAIL → **les loggene** → identifiser rot-årsak → rapporter.
- IKKE gå videre til LOG før VERIFY er PASS.
### LOG
- Append til `docs/OSVAUCO_OPAX_SESSION_LOG.md`:
```
## SLUTTRAPPORT <dato> <Fase>
- Hva: <beskrivelse av endring>
- Filer: <liste over endrede filer>
- Verifisering: <kommando + resultat>
## NESTE OPPGAVE
<én konkret neste oppgave>
```
### NEXT
- Ved neste sesjonstart, les `## NESTE OPPGAVE` fra `docs/HANDOFF.md` før du gjør noe annet.
---
## Hard Rules
1. **Aldri endre LOCK LIST-verdier** uten eksplisitt human instruksjon.
2. **Vis alltid diff før du skriver** noen fil.
3. **Aldri batch urelaterte endringer** i ett EXECUTE-steg.
4. **OPAX (`opax.vauco.no`) er kun management plane** — den mottar aldri rå pasientdata.
5. **Medioteq kliniske data** forblir i `europe-north1` innenfor Medioteq GCP-prosjektgrensen.
6. **Deploy `clinical-mcp` og `clinical-orchestrator`** til Medioteq-prosjektet, aldri Vauco-prosjektet.
7. **HITL gates**: PLAN godkjenner rekkefølge → AUDIT godkjenner format → OPS bekrefter EST → Human bekrefter før EXEC kjøres.
8. **Aldri søk på nett for å diagnostisere feil du kan lese i loggene.**
9. **Aldri anta at noe virker — verifiser alltid med en faktisk kommando.**
10. **Aldri gjett på koden — les den.** Du er i repoet. Bruk `cat`, `grep`, `git diff`.
11. **Aldri skriv .env fra .env.example** — se Kritiske fil-sikkerhetsregler.
12. **Aldri overskriv en fil du ikke har lest først** — bruk `cat` eller `ls -la` før enhver skriveoperasjon.
13. **Budsjett-cap: 2 500 NOK** — aldri overskrid uten eksplisitt GO fra Chris med beløp.
14. **ABSOLUTT REGEL:** Når Chris spesifiserer region, modell eller parameter eksplisitt — bruk ALLTID den verdien. Ikke overstyr med LOCKED DEFINITIONS.
---
## Diagnostics Playbook (KRITISK)
> **Grunnregel: Aldri gjett. Alltid hent faktisk data før du konkluderer.**
### 🔴 Cloud Build feiler
```bash
BUILD_ID=$(gcloud builds list \
--project=propane-will-491900-m5 \
--limit=1 \
--format="value(id)")
gcloud builds log $BUILD_ID \
--project=propane-will-491900-m5 2>&1 | tail -100
```
### 🔴 Cloud Run svarer ikke / returnerer feil
```bash
gcloud run services describe osvauco-agent \
--project=propane-will-491900-m5 \
--region=europe-west1 \
--format="value(status.conditions)"
gcloud logging read \
'resource.type=cloud_run_revision AND resource.labels.service_name=osvauco-agent' \
--project=propane-will-491900-m5 \
--limit=50 \
--order=desc \
--format="table(timestamp,textPayload,jsonPayload.message)"
```
### 🔴 Auth / IAP-feil
```bash
gcloud auth list
gcloud config get-value project
gcloud auth print-identity-token
gcloud iap web get-iam-policy \
--project=propane-will-491900-m5 \
--resource-type=cloud-run \
--service=osvauco-agent
```
### 🔴 Claude / Vertex AI-feil
```bash
curl -X POST \
"https://eu-aiplatform.googleapis.com/v1/projects/propane-will-491900-m5/locations/eu/publishers/anthropic/models/claude-opus-4-6:rawPredict" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-d '{"anthropic_version": "vertex-2023-10-16", "max_tokens": 256, "messages": [{"role": "user", "content": "ping"}]}'
```
### 🔴 .env mangler ved oppstart
```bash
ls -la .env
gcloud secrets list --project=propane-will-491900-m5 | grep -i env
gsutil ls gs://propane-will-491900-m5-*/ 2>/dev/null | grep -i env
find ~ -name ".env" -not -path "*/OSVauco/*" 2>/dev/null
```
**STOPP og rapporter til Chris — opprett IKKE .env fra .env.example.**
---
## GCP-konvensjoner for dette prosjektet
```bash
--project=propane-will-491900-m5
--region=europe-west1
CLOUD_RUN_SVC=osvauco-agent
IMAGE=gcr.io/propane-will-491900-m5/osvauco-agent
gcloud builds submit --config=cloudbuild.yaml --project=propane-will-491900-m5
gcloud run deploy osvauco-agent \
--image=$IMAGE:latest \
--project=propane-will-491900-m5 \
--region=europe-west1 \
--platform=managed
```
---
## Standard Boot Prompt
> Lim inn dette ord-for-ord ved start av HVER sesjon. `Les og print innhold fra` tvinger faktisk fillesing — ikke bare skumming.
```
Les og print innhold fra: @docs/AGENT_RULEBOOK.md @docs/MASTERPLAN.md @docs/ROADMAP.md @docs/HANDOFF.md
Deretter:
1. Print LOCKED DEFINITIONS (project, region, model, service, URLs) i en kompakt blokk
2. Oppsummer NESTE OPPGAVE fra HANDOFF.md i én setning
3. List 3 konkrete neste-steg du foreslår
Ikke utfør noe — vent på PLAN APPROVED eller eksplisitt instruksjon.
Bekreft at du forstår ABSOLUTT REGEL: når Chris spesifiserer region/modell/parameter eksplisitt, bruk ALLTID den verdien — aldri overstyr med LOCKED DEFINITIONS.
```

1
CNAME Normal file
View File

@ -0,0 +1 @@
costguard.oss.vauco.no

70
README.md Normal file
View File

@ -0,0 +1,70 @@
# OSVauco — GCP Agent Master Repo
**Prosjekt:** `propane-will-491900-m5` | **Region:** `us-central1`
## Status
| Komponent | Status |
|---|---|
| Cloud Run Agent | ✅ Live |
| Service URL | https://osvauco-agent-357036551735.us-central1.run.app |
| CI/CD Trigger | ✅ Auto-deploy ved push til `main` |
| ADK Versjon | 1.x (google-adk>=1.0.0,<2.0.0) |
| Siste revisjon | osvauco-agent-00006-mvb |
## Hurtigtest
```bash
TOKEN=$(gcloud auth print-identity-token)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user_id":"chris","session_id":"test-1","message":"Hei, hvem er du?"}' \
https://osvauco-agent-357036551735.us-central1.run.app/run | jq
```
## Repo-struktur
```
├── agents/
│ └── core-logic/ # Live agent: app.py (FastAPI + ADK Runner)
├── infrastructure/
│ ├── 01-setupenv.sh # ✅ Ferdig
│ ├── 05-cloudrun-deploy.sh # ✅ Ferdig
│ ├── 06-cicd-setup.sh # ✅ Ferdig
│ ├── 07-rag-setup.sh # ⏳ Neste
│ └── 08-memorybank-setup.sh # ⏳ Planlagt
├── docs/
├── scripts/
├── cloudbuild.yaml # CI/CD pipeline
└── .env.example
```
## Service Account roller (`osvauco-agent-sa`)
| Rolle | Formål |
|---|---|
| roles/aiplatform.user | Vertex AI / Gemini |
| roles/secretmanager.secretAccessor | Secrets |
| roles/logging.logWriter | Logging |
| roles/cloudtrace.agent | Tracing |
| roles/monitoring.metricWriter | Monitoring |
| roles/run.invoker | Cloud Run kall |
| roles/iam.serviceAccountUser | Deploy |
| roles/artifactregistry.writer | Docker push |
| roles/run.developer | Cloud Run deploy |
## Cloud Scheduler for Daglig Fakturering
For å automatisk generere daglige kostnads-snapshots, må en Cloud Scheduler-jobb konfigureres til å kalle `/billing/snapshot`-endepunktet.
**Oppsett med `gcloud`:**
1. **Service URL:** `https://osvauco-agent-357036551735.us-central1.run.app`
2. **Service Account:** `osvauco-agent-sa@propane-will-491900-m5.iam.gserviceaccount.com`
```bash
gcloud scheduler jobs create http daily-billing-snapshot \
--schedule="0 5 * * *" \
--uri="https://osvauco-agent-357036551735.us-central1.run.app/billing/snapshot" \
--http-method=POST \
--oidc-service-account-email="osvauco-agent-sa@propane-will-491900-m5.iam.gserviceaccount.com" \
--oidc-token-audience="https://osvauco-agent-357036551735.us-central1.run.app" \
--location="us-central1" \
--time-zone="Etc/UTC" \
--description="Kaller /billing/snapshot for å lagre daglig kostnadsdata."
```
Denne kommandoen oppretter en jobb som kjører hver dag kl. 05:00 UTC.

124
agents/aws_billing_agent.py Normal file
View File

@ -0,0 +1,124 @@
import os
import boto3
import datetime
import logging
class AWSBillingAgent:
def __init__(self):
self.aws_access_key_id = os.environ.get("AWS_ACCESS_KEY_ID")
self.aws_secret_access_key = os.environ.get("AWS_SECRET_ACCESS_KEY")
self.aws_default_region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
if not all([self.aws_access_key_id, self.aws_secret_access_key, self.aws_default_region]):
raise ValueError("AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION) must be set.")
self.ce_client = boto3.client(
'ce',
aws_access_key_id=self.aws_access_key_id,
aws_secret_access_key=self.aws_secret_access_key,
region_name=self.aws_default_region
)
logging.basicConfig(level=logging.INFO)
def get_summary(self):
"""
Henter MTD-kostnad, daglig snitt, prognose og en fordeling per tjeneste for AWS.
"""
try:
today = datetime.date.today()
start_of_month = today.replace(day=1).isoformat()
end_of_month = today.isoformat()
# Hent kostnad per tjeneste
response = self.ce_client.get_cost_and_usage(
TimePeriod={
'Start': start_of_month,
'End': end_of_month
},
Granularity='MONTHLY',
Metrics=['UnblendedCost'],
GroupBy=[{'Type': 'DIMENSION', 'Key': 'SERVICE'}]
)
summary = []
for item in response['ResultsByTime'][0]['Groups']:
summary.append({
"service": item['Keys'][0],
"total_cost": float(item['Metrics']['UnblendedCost']['Amount'])
})
summary = sorted(summary, key=lambda i: i['total_cost'], reverse=True)
if not summary:
return {
"onboarding_status": {
"state": "awaiting_data",
"message": "Kunne ikke hente faktureringsdata fra AWS. Sjekk at Cost Explorer er aktivert."
}
}
return {"summary": summary}
except Exception as e:
logging.error(f"Error fetching AWS summary: {e}")
return {"error": str(e)}
def get_forecast(self):
"""
Henter en kostnadsprognose for AWS.
"""
try:
today = datetime.date.today()
start_of_month = today.replace(day=1).isoformat()
# MTD-kostnad
mtd_response = self.ce_client.get_cost_and_usage(
TimePeriod={
'Start': start_of_month,
'End': today.isoformat()
},
Granularity='MONTHLY',
Metrics=['UnblendedCost']
)
mtd_cost = float(mtd_response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'])
# Prognose
forecast_response = self.ce_client.get_cost_forecast(
TimePeriod={
'Start': (today + datetime.timedelta(days=1)).isoformat(),
'End': (today.replace(day=1) + datetime.timedelta(days=32)).replace(day=1).isoformat()
},
Metric='UNBLENDED_COST',
Granularity='MONTHLY'
)
total_forecast = float(forecast_response['Total']['Amount'])
# Daglig snitt
last_7_days_start = (today - datetime.timedelta(days=7)).isoformat()
daily_avg_response = self.ce_client.get_cost_and_usage(
TimePeriod={
'Start': last_7_days_start,
'End': today.isoformat()
},
Granularity='DAILY',
Metrics=['UnblendedCost']
)
daily_costs = [float(item['Total']['UnblendedCost']['Amount']) for item in daily_avg_response['ResultsByTime']]
daily_average = sum(daily_costs) / len(daily_costs) if daily_costs else 0
return {
"daily_average_last_7_days": daily_average,
"month_to_date_cost": mtd_cost,
"total_monthly_forecast": total_forecast,
"data_note": "Prognose fra AWS Cost Explorer. Kan avvike fra endelig faktura."
}
except Exception as e:
logging.error(f"Error fetching AWS forecast: {e}")
return {"error": str(e)}
if __name__ == '__main__':
# For local testing, ensure env vars are set
agent = AWSBillingAgent()
print("Summary:", agent.get_summary())
print("Forecast:", agent.get_forecast())

View File

@ -0,0 +1,34 @@
# .env.example — Copy to .env for local development
# NEVER commit .env to Git. Add .env to .gitignore.
# GCP project and region (required)
GOOGLE_CLOUD_PROJECT=propane-will-491900-m5
GOOGLE_CLOUD_LOCATION=us-central1
# Authentication mode: True = Vertex AI (ADC), False = Gemini API key
GOOGLE_GENAI_USE_VERTEXAI=True
# Only set if GOOGLE_GENAI_USE_VERTEXAI=False (local testing without gcloud)
# GOOGLE_API_KEY=your-api-key-here
# RAG corpus resource name — DEAKTIVERT i root_agent (CI6)
# Blokkerte AFC når kombinert med FunctionTools. TODO: sub-agent når korpus er oppdatert.
# RAG_CORPUS=projects/propane-will-491900-m5/locations/europe-west4/ragCorpora/XXXXXXXXX
# Agent Engine ID (from memory_setup.py output)
# AGENT_ENGINE_ID=your-agent-engine-id
# OPAX-MCP server URL (intern CI/CD-kanal)
MCP_SERVER_URL=https://opax-mcp-zjbqp3prqq-uc.a.run.app
# OPAX-MCP autentiseringsnøkkel — hentes fra Secret Manager: mcp-server-key
# På Cloud Run: mount via --update-secrets=MCP_SECRET=mcp-server-key:latest
# Lokalt: gcloud secrets versions access latest --secret=mcp-server-key
MCP_SECRET=your-mcp-secret-here
# MCP server URLs (if using Google managed MCP)
# MAPS_MCP_URL=https://maps.googleapis.com/mcp
# BIGQUERY_MCP_URL=https://bigquery.googleapis.com/mcp
# Maps API key (for Google Maps MCP)
# MAPS_API_KEY=your-maps-api-key

View File

@ -0,0 +1,23 @@
FROM us-central1-docker.pkg.dev/propane-will-491900-m5/osvauco-repo/osvauco-base:latest
# Eksplisitt override av base-image WORKDIR
WORKDIR /app
# Installer avhengigheter
COPY agents/core-logic/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Kopier alt nødvendig: hovudapp, agenter, static-filer, docs, auth
COPY main.py .
COPY agents/ ./agents/
COPY static/ ./static/
COPY auth/ ./auth/
COPY ml/ ./ml/
COPY docs/ ./docs/
ENV PORT=8080
ENV PYTHONPATH=/app
EXPOSE 8080
# Start hovudapp — main.py i /app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

View File

@ -0,0 +1,25 @@
# Dockerfile.base — bygg og push manuelt når tunge avhengigheter endres:
#
# docker build -f agents/core-logic/Dockerfile.base \
# -t us-central1-docker.pkg.dev/propane-will-491900-m5/osvauco-repo/osvauco-base:latest \
# agents/core-logic
# docker push us-central1-docker.pkg.dev/propane-will-491900-m5/osvauco-repo/osvauco-base:latest
#
# Cloud Build kan også kjøre dette steget manuelt:
# gcloud builds submit --config=cloudbuild.base.yaml .
FROM python:3.13-slim
WORKDIR /app
# Kun de tunge avhengighetene — endres sjelden
COPY requirements.txt .
RUN pip install --no-cache-dir \
google-adk>=1.0.0 \
"google-cloud-aiplatform[adk,agent_engines,evaluation]>=1.112.0" \
pyarrow \
grpcio \
google-cloud-bigquery \
google-cloud-spanner \
google-cloud-bigtable \
google-auth-oauthlib>=1.2.0

View File

@ -0,0 +1,2 @@
# Required by ADK: exports root_agent for adk web and adk deploy commands
from .agent import root_agent

326
agents/core-logic/agent.py Normal file
View File

@ -0,0 +1,326 @@
#!/usr/bin/env python3
"""
agent.py OSVauco OPAX agent (Jason Vauger).
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 gemini_guard import guarded_run, CircuitOpen, RateLimitExceeded
from google.adk.sessions.in_memory_session_service import InMemorySessionService
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", "us-central1")
ORCHESTRATOR_MODEL = os.environ.get("ORCHESTRATOR_MODEL", "gemini-2.5-flash")
SUBAGENT_MODEL = os.environ.get("SUBAGENT_MODEL", "gemini-2.5-flash")
REASONING_MODEL = os.environ.get("REASONING_MODEL", "gemini-2.5-flash")
HEAVY_ORCHESTRATOR = os.environ.get("HEAVY_ORCHESTRATOR_MODEL", "gemini-2.5-pro")
HEAVY_SUBAGENT = os.environ.get("HEAVY_SUBAGENT_MODEL", "gemini-2.5-pro")
HEAVY_REASONING = os.environ.get("HEAVY_REASONING_MODEL", "gemini-2.5-flash")
BUDGET_LIGHT = float(os.environ.get("BUDGET_A_USD_PER_TASK", "1.0"))
BUDGET_HEAVY = float(os.environ.get("HEAVY_MODE_BUDGET_USD_PER_DAY", "3.0"))
HEAVY_MODE_ALLOWED_USERS = ["opax", "admin"]
Mode = Literal["light", "heavy"]
APP_NAME = "opax"
try:
from token_logger import log_token_usage, create_bq_table_if_not_exists
except ImportError:
def log_token_usage(*args, **kwargs): pass
def create_bq_table_if_not_exists(): pass
try:
import sys as _sys, pathlib as _pathlib
_sys.path.insert(0, str(_pathlib.Path(__file__).parent.parent.parent / "ml"))
from token_budget import trim_context, route_model, TokenBudgetExceeded, budget_summary
logger.info(f"[agent] token_budget lastet: {budget_summary()}")
except ImportError as _e:
logger.warning(f"[agent] token_budget ikke tilgjengelig: {_e}")
def trim_context(history, system_prompt="", max_tokens=32000):
return history[-10:] if len(history) > 10 else history
def route_model(message, mode, flash_model, pro_model):
return flash_model if mode != "heavy" else pro_model
class TokenBudgetExceeded(Exception): pass
# ---------------------------------------------------------------------------
# OPAX MCP Client (inlina)
# ---------------------------------------------------------------------------
_OPAX_MCP_URL = os.environ.get("MCP_SERVER_URL", "https://opax-mcp-357036551735.us-central1.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:
raise ValueError(f"Invalid mode '{mode}'. Must be 'light' or 'heavy'.")
return mapping[mode]
def authorize_mode(user_id: str, mode: str) -> None:
mode = _normalize_mode(mode)
if mode == "heavy" and user_id not in HEAVY_MODE_ALLOWED_USERS:
raise PermissionError(f"User '{user_id}' is not authorized for heavy mode.")
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}
_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: 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. "
"Når brukaren ber om data — KALL ALLTID rett tool før du svarar. "
"Svar på norsk (bokmål) med mindre annet er bedt om. "
"HITL: ikkje kjør terraform/deploy utan godkjenning frå Chris."
)
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"])
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,
)
return agent, actual_model, instruction
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)
module_name = f"jason/{mode}"
try:
trim_context([types.Content(role="user", parts=[types.Part(text=message)])], system_prompt=instruction)
except TokenBudgetExceeded as e:
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)
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 = ""
input_tokens = 0
output_tokens = 0
request_id = str(uuid.uuid4())
async for event in guarded_run(runner, 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:
um = event.usage_metadata
input_tokens += getattr(um, "prompt_token_count", 0) or 0
output_tokens += getattr(um, "candidates_token_count", 0) or 0
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,
)
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:
"""
Synkron inngang kun for CLI (if __name__ == '__main__').
Fra FastAPI/uvicorn skal _run_async kalles direkte med await.
"""
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
))
if __name__ == "__main__":
import sys
logging.basicConfig(level=logging.WARNING)
query = sys.argv[1] if len(sys.argv) > 1 else "Hva er OPAX?"
mode = sys.argv[2] if len(sys.argv) > 2 else "light"
caller_type = sys.argv[3] if len(sys.argv) > 3 else "cli"
print(f"Mode: {mode} | Query: {query}")
print("-" * 60)
print(run(message=query, user_id="opax", mode=mode, caller_type=caller_type))

382
agents/core-logic/app.py Normal file
View File

@ -0,0 +1,382 @@
# agents/core-logic/app.py
# OSVauco-NMTMD-GCOS — FastAPI HTTP entrypoint for Cloud Run
import os
import sys
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, FileResponse
from pydantic import BaseModel
import vertexai
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
try:
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai.types import Content, Part
from agent import build_agent
from agents.recommendations_engine import get_recommendations
from token_logger import create_bq_table_if_not_exists
except ImportError as e:
logger.error(f"Failed to import ADK dependencies: {e}")
raise
session_service = InMemorySessionService()
root_agent = None
APP_NAME = os.environ.get("CLOUD_RUN_SERVICE", "gcp-orchestrator")
PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
BQ_BILLING_DATASET = os.environ.get("BQ_BILLING_DATASET", "billing_data")
CLOUD_BUILD_TRIGGER_ID = os.environ.get("CLOUD_BUILD_TRIGGER_ID", "")
# Static folder: /app/static/ (two levels up from agents/core-logic)
STATIC_DIR = Path(__file__).parent.parent.parent / "static"
_budget_store: dict = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
global root_agent
logger.info(f"OSVauco agent '{APP_NAME}' starting up")
vertexai.init(project=PROJECT_ID, location=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1"))
create_bq_table_if_not_exists()
root_agent, _, _ = build_agent(mode="light")
logger.info(f"Static dir: {STATIC_DIR} (exists={STATIC_DIR.exists()})")
yield
logger.info(f"OSVauco agent '{APP_NAME}' shutting down")
app = FastAPI(
title="OSVauco GCP Agent",
description="ADK-based multi-agent orchestrator on Cloud Run",
version="1.0.0",
lifespan=lifespan,
)
class RunRequest(BaseModel):
user_id: str
session_id: str
message: str
class RunResponse(BaseModel):
user_id: str
session_id: str
response: str
class BudgetRequest(BaseModel):
budget: float
user: str = "default"
async def _ensure_session(user_id: str, session_id: str):
try:
session = await session_service.get_session(
app_name=APP_NAME, user_id=user_id, session_id=session_id
)
if session is not None:
return session
except Exception:
pass
session = await session_service.create_session(
app_name=APP_NAME, user_id=user_id, session_id=session_id
)
logger.info(f"Created new session: {session_id} for user: {user_id}")
return session
# ── root — serve opax.html ────────────────────────────────────────────────────
@app.get("/")
async def root():
index = STATIC_DIR / "opax.html"
if not index.exists():
logger.error(f"opax.html not found at {index}")
raise HTTPException(status_code=404, detail=f"opax.html not found at {index}")
return FileResponse(str(index), media_type="text/html")
# ── static files ──────────────────────────────────────────────────────────────
@app.get("/static/{filename}")
async def static_file(filename: str):
filepath = STATIC_DIR / filename
if not filepath.exists():
raise HTTPException(status_code=404, detail=f"{filename} not found")
return FileResponse(str(filepath))
# ── helse ────────────────────────────────────────────────────────────────────
@app.get("/health")
async def health():
return JSONResponse({"status": "ok", "service": APP_NAME})
# ── agent run ────────────────────────────────────────────────────────────────
@app.post("/run", response_model=RunResponse)
async def run(req: RunRequest):
try:
await _ensure_session(req.user_id, req.session_id)
runner = Runner(agent=root_agent, app_name=APP_NAME, session_service=session_service)
user_content = Content(role="user", parts=[Part(text=req.message)])
final_response = ""
async for event in runner.run_async(
user_id=req.user_id, session_id=req.session_id, new_message=user_content
):
if event.is_final_response() and event.content:
for part in event.content.parts:
if part.text:
final_response += part.text
logger.info(f"[{req.user_id}/{req.session_id}] Response length: {len(final_response)}")
return RunResponse(user_id=req.user_id, session_id=req.session_id, response=final_response)
except Exception as e:
logger.error(f"Agent run failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
# ── OX1a: Cloud Build status ──────────────────────────────────────────────────
@app.get("/opax/build-status")
async def build_status():
"""
OX1a Hent siste Cloud Build-kjøring.
Status: queued|working|success|failure|timeout|cancelled|unknown
"""
try:
from google.cloud.devtools import cloudbuild_v1
client = cloudbuild_v1.CloudBuildClient()
request = cloudbuild_v1.ListBuildsRequest(
project_id=PROJECT_ID,
filter='trigger_id!=""',
page_size=5,
)
builds = list(client.list_builds(request=request))
if not builds:
return JSONResponse({"status": "unknown", "message": "Ingen builds funnet"})
b = builds[0]
status_map = {1:"queued",2:"working",3:"success",4:"failure",5:"internal_error",6:"timeout",7:"cancelled"}
status_str = status_map.get(int(b.status), "unknown")
duration_s = None
if b.start_time and b.finish_time:
duration_s = int(b.finish_time.seconds - b.start_time.seconds)
elif b.start_time:
import time
duration_s = int(time.time() - b.start_time.seconds)
return JSONResponse({
"status": status_str,
"build_id": b.id,
"trigger_id": b.build_trigger_id or "",
"branch": (b.substitutions or {}).get("BRANCH_NAME", "main"),
"commit": (b.substitutions or {}).get("SHORT_SHA", ""),
"duration_s": duration_s,
"start_time": str(b.start_time) if b.start_time else None,
"finish_time": str(b.finish_time) if b.finish_time else None,
"log_url": b.log_url or "",
})
except Exception as e:
logger.error(f"[/opax/build-status] Failed: {e}", exc_info=True)
return JSONResponse({"status": "error", "message": str(e)}, status_code=200)
# ── CG4a: Token intelligence — per modul ─────────────────────────────────────
@app.get("/billing/tokens/by-module")
async def tokens_by_module(days: int = 30):
"""
CG4a Token-forbruk aggregert per module_name + caller_type.
Brukes av opax.html TUI + fremtidig token-panel i billing dashboard.
Fallback: tom liste hvis BQ mangler data (OQ-15 safe).
"""
try:
from google.cloud import bigquery
client = bigquery.Client(project=PROJECT_ID)
query = f"""
SELECT
COALESCE(module_name, 'ukjent') AS module_name,
COALESCE(caller_type, 'ukjent') AS caller_type,
model_name,
COUNT(*) AS calls,
SUM(input_tokens) AS input_tokens,
SUM(output_tokens) AS output_tokens,
SUM(total_tokens) AS total_tokens,
ROUND(SUM(estimated_cost_usd), 6) AS cost_usd
FROM `{PROJECT_ID}.{BQ_BILLING_DATASET}.llm_token_usage`
WHERE timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL {int(days)} DAY)
GROUP BY module_name, caller_type, model_name
ORDER BY cost_usd DESC
LIMIT 100
"""
rows = [
{
"module_name": row.module_name,
"caller_type": row.caller_type,
"model_name": row.model_name,
"calls": row.calls,
"input_tokens": row.input_tokens,
"output_tokens":row.output_tokens,
"total_tokens": row.total_tokens,
"cost_usd": float(row.cost_usd),
}
for row in client.query(query).result()
]
return JSONResponse({"period_days": days, "rows": rows})
except Exception as e:
logger.error(f"[/billing/tokens/by-module] Failed: {e}", exc_info=True)
return JSONResponse({"period_days": days, "rows": [], "warning": str(e)})
# ── CG4c: Prosjektestimator ───────────────────────────────────────────────────
@app.get("/billing/tokens/estimate")
async def tokens_estimate(complexity: str = "medium"):
"""
CG4c Prosjektestimator.
Henter snitt-tokens per kall siste 30 dager, multipliserer med kompleksitetsfaktor.
complexity: low | medium | high | extreme
Returnerer estimert kostnad i USD for et nytt oppdrag.
"""
factors = {"low": 0.5, "medium": 1.0, "high": 2.5, "extreme": 6.0}
factor = factors.get(complexity, 1.0)
try:
from google.cloud import bigquery
client = bigquery.Client(project=PROJECT_ID)
query = f"""
SELECT
AVG(total_tokens) AS avg_tokens_per_call,
AVG(estimated_cost_usd) AS avg_cost_per_call,
COUNT(*) AS total_calls,
SUM(estimated_cost_usd) AS total_cost_usd
FROM `{PROJECT_ID}.{BQ_BILLING_DATASET}.llm_token_usage`
WHERE timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
"""
result = list(client.query(query).result())
if not result or result[0].avg_tokens_per_call is None:
return JSONResponse({"complexity": complexity, "factor": factor, "estimated_usd": None, "warning": "Ingen data ennå (OQ-15)"})
row = result[0]
avg_cost = float(row.avg_cost_per_call or 0)
CALLS_PER_TASK = 20
estimated = round(avg_cost * CALLS_PER_TASK * factor, 4)
return JSONResponse({
"complexity": complexity,
"factor": factor,
"avg_tokens_per_call": round(float(row.avg_tokens_per_call or 0), 1),
"avg_cost_per_call": round(avg_cost, 6),
"total_calls_30d": row.total_calls,
"total_cost_30d_usd": round(float(row.total_cost_usd or 0), 4),
"estimated_usd": estimated,
"calls_assumed": CALLS_PER_TASK,
})
except Exception as e:
logger.error(f"[/billing/tokens/estimate] Failed: {e}", exc_info=True)
return JSONResponse({"complexity": complexity, "estimated_usd": None, "warning": str(e)})
# ── billing: tokens summary (eksisterende) ────────────────────────────────────
@app.get("/billing/tokens/summary")
async def billing_tokens_summary():
try:
from google.cloud import bigquery
client = bigquery.Client(project=PROJECT_ID)
query = f"""
SELECT
module_name, caller_type, model_name,
SUM(input_tokens) AS total_input_tokens,
SUM(output_tokens) AS total_output_tokens,
SUM(total_tokens) AS total_tokens,
SUM(estimated_cost_usd) AS total_cost_usd
FROM `{PROJECT_ID}.{BQ_BILLING_DATASET}.llm_token_usage`
WHERE timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
GROUP BY module_name, caller_type, model_name
ORDER BY total_cost_usd DESC
"""
rows = [
{
"module_name": row.module_name,
"caller_type": row.caller_type,
"model_name": row.model_name,
"total_input_tokens": row.total_input_tokens,
"total_output_tokens": row.total_output_tokens,
"total_tokens": row.total_tokens,
"total_cost_usd": round(float(row.total_cost_usd), 6),
}
for row in client.query(query).result()
]
return JSONResponse({"period_days": 30, "rows": rows})
except Exception as e:
logger.error(f"[/billing/tokens/summary] Failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.get("/billing/recommendations")
async def billing_recommendations(budget: float = 500.0):
try:
recommendations = get_recommendations(budget)
return JSONResponse(recommendations)
except Exception as e:
logger.error(f"[/billing/recommendations] Failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.get("/billing/by-service")
async def billing_by_service(days: int = 30):
try:
from ml.billing_agent import BillingAgent
return JSONResponse(BillingAgent().get_service_totals(days))
except Exception as e:
logger.error(f"[/billing/by-service] Failed: {e}", exc_info=True)
return JSONResponse([])
@app.get("/billing/anomalies")
async def billing_anomalies():
try:
from ml.billing_agent import BillingAgent
return JSONResponse(BillingAgent().get_anomalies())
except Exception as e:
logger.error(f"[/billing/anomalies] Failed: {e}", exc_info=True)
return JSONResponse({"anomalies": []})
@app.get("/billing/history")
async def billing_history(days: int = 30):
try:
from ml.billing_agent import BillingAgent
return JSONResponse({"history": BillingAgent().get_daily_history(days)})
except Exception as e:
logger.error(f"[/billing/history] Failed: {e}", exc_info=True)
return JSONResponse({"history": []})
@app.get("/billing/summary")
async def billing_summary():
try:
from ml.billing_agent import BillingAgent
return JSONResponse(BillingAgent().get_summary())
except Exception as e:
logger.error(f"[/billing/summary] Failed: {e}", exc_info=True)
return JSONResponse({"summary": []})
@app.get("/billing/live")
async def billing_live():
try:
from ml.billing_agent import BillingAgent
return JSONResponse(BillingAgent().get_forecast())
except Exception as e:
logger.error(f"[/billing/live] Failed: {e}", exc_info=True)
return JSONResponse({})
@app.get("/billing/budget")
async def get_budget(user: str = "default"):
return JSONResponse({"budget": _budget_store.get(user, 500.0), "user": user})
@app.post("/billing/budget")
async def set_budget(req: BudgetRequest):
if req.budget <= 0:
raise HTTPException(status_code=400, detail="Budget must be > 0")
_budget_store[req.user] = req.budget
return JSONResponse({"budget": req.budget, "user": req.user})

View File

@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""
deploy_agent.py Deploy ADK agent til Vertex AI Agent Engine
Usage: python3 deploy_agent.py [--project PROJECT_ID] [--region REGION] \
[--display-name DISPLAY_NAME] [--staging-bucket GS_URI]
Krever: google-cloud-aiplatform>=1.157.0, google-adk>=2.0.0
"""
import argparse
import sys
import vertexai
from vertexai import agent_engines # riktig import for SDK >= 1.157.0
def deploy(project: str, region: str, display_name: str, staging_bucket: str):
print(f"Initialiserer Vertex AI: project={project}, region={region}")
vertexai.init(project=project, location=region, staging_bucket=staging_bucket)
# Importer Jason ADK agent
sys.path.insert(0, ".")
import agent as my_agent
print(f"Deployer agent '{display_name}' til Vertex AI Agent Engine i {region}...")
remote_agent = agent_engines.create(
my_agent.root_agent,
requirements=[
"google-cloud-aiplatform[adk,agent_engines]>=1.157.0",
"google-adk>=2.2.0",
"httpx>=0.27.0",
"google-auth>=2.29.0",
],
extra_packages=["./opax_mcp_client.py"],
display_name=display_name,
)
print(f"\n✅ Agent deployet!")
print(f" Resource name : {remote_agent.resource_name}")
print(f" Console : https://console.cloud.google.com/ai/agents?project={project}")
print(f" Region : {region}")
print("\n⚠️ Husk: kjør teardown når du er ferdig for å unngå unnødige kostnader.")
return remote_agent
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Deploy Jason ADK agent til Vertex AI")
parser.add_argument("--project", default="propane-will-491900-m5")
parser.add_argument("--region", default="us-central1")
parser.add_argument("--display-name", default="jason-vauger-v1")
parser.add_argument("--staging-bucket", default="gs://propane-will-491900-m5-agent-staging")
args = parser.parse_args()
deploy(args.project, args.region, args.display_name, args.staging_bucket)

View File

@ -0,0 +1,39 @@
# agents/core-logic/feedback_loop.py
import requests
from google.cloud import bigquery
# Configuration
TELEMETRY_URL = "https://osvauco-agent-357036551735.europe-west1.run.app/telemetry/history"
BIGQUERY_PROJECT = "propane-will-491900-m5"
BIGQUERY_DATASET = "osvauco_logs"
BIGQUERY_TABLE = "cloud_run_logs"
def collect_and_store_telemetry():
"""
Collects telemetry data from the specified endpoint and stores it in BigQuery.
"""
try:
# 1. Collect telemetry data
response = requests.get(TELEMETRY_URL)
response.raise_for_status() # Raise an exception for bad status codes
telemetry_data = response.json()
# 2. Store data in BigQuery
client = bigquery.Client(project=BIGQUERY_PROJECT)
table_id = f"{BIGQUERY_PROJECT}.{BIGQUERY_DATASET}.{BIGQUERY_TABLE}"
# Assuming telemetry_data is a list of dicts matching the table schema
errors = client.insert_rows_json(table_id, telemetry_data)
if errors == []:
print(f"Successfully inserted {len(telemetry_data)} rows into {table_id}")
else:
print(f"Encountered errors while inserting rows: {errors}")
except requests.exceptions.RequestException as e:
print(f"Error collecting telemetry data: {e}")
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
collect_and_store_telemetry()

View File

@ -0,0 +1,102 @@
"""
gemini_guard.py Rate limiter + circuit breaker for alle Gemini-kall i OSVauco.
Wrapper rundt runner.run_async() i _run_async().
"""
import time
import asyncio
import logging
from collections import deque
from typing import AsyncIterator
logger = logging.getLogger(__name__)
# --- Konfigurasjon (env-override mulig) ---
import os
RATE_LIMIT_RPM = int(os.environ.get("GUARD_RATE_LIMIT_RPM", "30"))
CIRCUIT_FAIL_THRESH = int(os.environ.get("GUARD_CIRCUIT_FAILS", "5"))
CIRCUIT_RESET_SECS = int(os.environ.get("GUARD_CIRCUIT_RESET_SECS", "60"))
class CircuitOpen(Exception):
pass
class RateLimitExceeded(Exception):
pass
class GeminiGuard:
"""
Én singleton per prosess.
- Rate limit: maks RATE_LIMIT_RPM kall per minutt (sliding window)
- Circuit breaker: åpner ved CIRCUIT_FAIL_THRESH feil rad,
reset etter CIRCUIT_RESET_SECS sekunder
"""
def __init__(self):
self._timestamps: deque = deque()
self._failures = 0
self._opened_at = 0.0
self._state = "closed" # closed | open | half-open
def _check_rate(self):
now = time.monotonic()
cutoff = now - 60.0
while self._timestamps and self._timestamps[0] < cutoff:
self._timestamps.popleft()
if len(self._timestamps) >= RATE_LIMIT_RPM:
raise RateLimitExceeded(
f"Rate limit {RATE_LIMIT_RPM} RPM nådd ({len(self._timestamps)} kall siste minutt)"
)
self._timestamps.append(now)
def _check_circuit(self):
if self._state == "open":
elapsed = time.monotonic() - self._opened_at
if elapsed >= CIRCUIT_RESET_SECS:
self._state = "half-open"
logger.warning("[guard] circuit half-open — prøver igjen")
else:
raise CircuitOpen(
f"Circuit breaker åpen — venter {CIRCUIT_RESET_SECS - int(elapsed)}s"
)
def on_success(self):
self._failures = 0
self._state = "closed"
def on_failure(self, exc: Exception):
self._failures += 1
logger.error(f"[guard] Gemini-feil #{self._failures}: {exc}")
if self._failures >= CIRCUIT_FAIL_THRESH:
self._state = "open"
self._opened_at = time.monotonic()
logger.critical(f"[guard] Circuit ÅPNET etter {self._failures} feil")
async def run(self, runner, *, user_id, session_id, new_message) -> AsyncIterator:
self._check_circuit()
self._check_rate()
try:
events = []
async for event in runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=new_message,
):
events.append(event)
self.on_success()
for e in events:
yield e
except (CircuitOpen, RateLimitExceeded):
raise
except Exception as exc:
self.on_failure(exc)
raise
_guard = GeminiGuard()
async def guarded_run(runner, *, user_id, session_id, new_message) -> AsyncIterator:
"""Drop-in erstatning for runner.run_async(...) """
async for event in _guard.run(
runner,
user_id=user_id,
session_id=session_id,
new_message=new_message,
):
yield event

View File

@ -0,0 +1,45 @@
# agents/core-logic/heavy_predictor.py
from google.cloud import bigquery
# Configuration
BIGQUERY_PROJECT = "propane-will-491900-m5"
BIGQUERY_DATASET = "osvauco_logs"
BIGQUERY_TABLE = "cloud_run_logs"
TELEMETRY_CALLS_THRESHOLD = 500
def check_and_trigger_heavy_mode():
"""
Checks the number of telemetry calls and triggers heavy mode if the threshold is exceeded.
"""
try:
client = bigquery.Client(project=BIGQUERY_PROJECT)
table_id = f"{BIGQUERY_PROJECT}.{BIGQUERY_DATASET}.{BIGQUERY_TABLE}"
# 1. Query the number of telemetry calls from BigQuery
query = f"""
SELECT COUNT(*) as total_calls
FROM `{table_id}`
WHERE timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 24 HOUR)
"""
query_job = client.query(query)
results = query_job.result()
for row in results:
total_calls = row.total_calls
print(f"Total telemetry calls in the last 24 hours: {total_calls}")
# 2. Trigger heavy mode if the threshold is exceeded
if total_calls > TELEMETRY_CALLS_THRESHOLD:
print("Telemetry calls threshold exceeded. Triggering heavy mode.")
# Placeholder for logic to trigger heavy mode
# This could involve scaling up resources, switching to a more powerful model, etc.
pass
else:
print("Telemetry calls are within acceptable limits.")
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
check_and_trigger_heavy_mode()

View File

@ -0,0 +1,46 @@
# agents/core-logic/hypertuner.py
from google.cloud import bigquery
# Configuration
BIGQUERY_PROJECT = "propane-will-491900-m5"
BIGQUERY_DATASET = "osvauco_logs"
BIGQUERY_TABLE = "cloud_run_logs"
RESPONSE_TIME_THRESHOLD = 1000 # in milliseconds
def adjust_prompt_parameters():
"""
Reads telemetry data from BigQuery, analyzes response times,
and adjusts prompt parameters accordingly.
"""
try:
client = bigquery.Client(project=BIGQUERY_PROJECT)
table_id = f"{BIGQUERY_PROJECT}.{BIGQUERY_DATASET}.{BIGQUERY_TABLE}"
# 1. Query response time data from BigQuery
query = f"""
SELECT AVG(latency_ms) as avg_latency
FROM `{table_id}`
WHERE timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)
"""
query_job = client.query(query)
results = query_job.result()
for row in results:
avg_latency = row.avg_latency
print(f"Average response time in the last hour: {avg_latency} ms")
# 2. Adjust prompt parameters based on response time
if avg_latency > RESPONSE_TIME_THRESHOLD:
print("Response time is high. Adjusting prompt parameters to reduce complexity.")
# Placeholder for logic to adjust prompt parameters
# For example, reduce max_tokens, use a simpler model, etc.
pass
else:
print("Response time is within acceptable limits.")
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
adjust_prompt_parameters()

View File

@ -0,0 +1,212 @@
"""
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 <identity-token> (automatisk)
2. Tool-level : X-MCP-Secret: <mcp-server-key> (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),
]

View File

@ -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 <identity-token> (automatisk)
2. Tool-level : X-MCP-Secret: <mcp-server-key> (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),
]

View File

@ -0,0 +1,50 @@
google-adk==1.34.1
google-cloud-aiplatform==1.154.0
google-cloud-secret-manager==2.28.0
httpx==0.28.1
google-auth==2.53.0
google-auth-httplib2==0.4.0
opentelemetry-sdk==1.41.1
opentelemetry-exporter-gcp-trace==1.12.0
google-cloud-logging==3.15.0
google-cloud-monitoring==2.30.0
uvicorn==0.48.0
fastapi==0.136.3
python-dotenv==1.2.2
pydantic==2.12.5
boto3==1.43.18
botocore==1.43.18
cachecontrol==0.14.4
cachetools==7.1.4
dask==2026.3.0
dask-glm==0.4.0
dask-ml==2025.1.0
distributed==2026.3.0
firebase-admin==7.4.0
google-cloud-billing==1.19.0
google-cloud-firestore==2.27.0
h2==4.3.0
hpack==4.1.0
httptools==0.8.0
hyperframe==6.1.0
itsdangerous==2.2.0
jmespath==1.1.0
llvmlite==0.47.0
locket==1.0.0
msgpack==1.1.2
multipledispatch==1.0.0
numba==0.65.1
partd==1.4.2
psutil==7.2.2
python-http-client==3.3.7
s3transfer==0.18.0
sendgrid==6.12.5
sortedcontainers==2.4.0
sparse==0.18.0
tblib==3.2.2
toolz==1.1.0
tornado==6.5.6
uvloop==0.22.1
watchfiles==1.2.0
werkzeug==3.1.8
zict==3.0.0

View File

@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""
agent.py OSVauco OPAX agent (Jason Vauger).
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
logger = logging.getLogger(__name__)
PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
LOCATION = os.environ.get("GOOGLE_CLOUD_LOCATION", "global")
ORCHESTRATOR_MODEL = os.environ.get("ORCHESTRATOR_MODEL", "gemini-2.5-flash")
SUBAGENT_MODEL = os.environ.get("SUBAGENT_MODEL", "gemini-2.5-flash")
REASONING_MODEL = os.environ.get("REASONING_MODEL", "gemini-2.5-flash")
HEAVY_ORCHESTRATOR = os.environ.get("HEAVY_ORCHESTRATOR_MODEL", "gemini-2.5-pro")
HEAVY_SUBAGENT = os.environ.get("HEAVY_SUBAGENT_MODEL", "gemini-2.5-pro")
HEAVY_REASONING = os.environ.get("HEAVY_REASONING_MODEL", "gemini-2.5-flash")
BUDGET_LIGHT = float(os.environ.get("BUDGET_A_USD_PER_TASK", "1.0"))
BUDGET_HEAVY = float(os.environ.get("HEAVY_MODE_BUDGET_USD_PER_DAY", "3.0"))
HEAVY_MODE_ALLOWED_USERS = ["opax", "admin"]
Mode = Literal["light", "heavy"]
APP_NAME = "opax"
try:
from token_logger import log_token_usage, create_bq_table_if_not_exists
except ImportError:
def log_token_usage(*args, **kwargs): pass
def create_bq_table_if_not_exists(): pass
try:
import sys as _sys, pathlib as _pathlib
from token_budget import trim_context, route_model, TokenBudgetExceeded, budget_summary
except ImportError as _e:
def trim_context(history, system_prompt="", max_tokens=32000):
return history[-10:] if len(history) > 10 else history
def route_model(message, mode, flash_model, pro_model):
return flash_model if mode != "heavy" else pro_model
class TokenBudgetExceeded(Exception): pass
create_bq_table_if_not_exists()
_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:
try:
resp = httpx.get(
f"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience={_OPAX_MCP_URL}&format=full",
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),
]
def _normalize_mode(mode: str) -> str:
mapping = {"A": "light", "A+": "heavy", "light": "light", "heavy": "heavy"}
if mode not in mapping:
raise ValueError(f"Invalid mode '{mode}'. Must be 'light' or 'heavy'.")
return mapping[mode]
def authorize_mode(user_id: str, mode: str) -> None:
mode = _normalize_mode(mode)
if mode == "heavy" and user_id not in HEAVY_MODE_ALLOWED_USERS:
raise PermissionError(f"User '{user_id}' is not authorized for heavy mode.")
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}
_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. "
"Når brukaren ber om data — KALL ALLTID rett tool før du svarar. "
"Svar på norsk (bokmål) med mindre annet er bedt om. "
"HITL: ikkje kjør terraform/deploy utan godkjenning frå Chris."
)
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"])
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)
return agent, actual_model, instruction
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:
mode = _normalize_mode(mode)
agent, actual_model, instruction = build_agent(mode=mode, message=message)
module_name = f"jason/{mode}"
try:
trim_context([types.Content(role="user", parts=[types.Part(text=message)])], system_prompt=instruction)
except TokenBudgetExceeded as e:
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)
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 = ""
input_tokens = 0
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):
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:
um = event.usage_metadata
input_tokens += getattr(um, "prompt_token_count", 0) or 0
output_tokens += getattr(um, "candidates_token_count", 0) or 0
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)
return final_text
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))
if __name__ == "__main__":
import sys
logging.basicConfig(level=logging.WARNING)
query = sys.argv[1] if len(sys.argv) > 1 else "Hva er OPAX?"
mode = sys.argv[2] if len(sys.argv) > 2 else "light"
caller_type = sys.argv[3] if len(sys.argv) > 3 else "cli"
print(f"Mode: {mode} | Query: {query}")
print("-" * 60)
print(run(message=query, user_id="opax", mode=mode, caller_type=caller_type))

View File

@ -0,0 +1,142 @@
# token_logger.py — Logg LLM token-bruk til BigQuery (CG3e + CG4)
import os
import logging
import uuid
from datetime import datetime, timezone
from typing import Optional, Literal
logger = logging.getLogger(__name__)
PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
DATASET = os.environ.get("BQ_BILLING_DATASET", "billing_data")
TABLE = "llm_token_usage"
FULL_TABLE = f"{PROJECT_ID}.{DATASET}.{TABLE}"
# Gyldige caller_type-verdier
# agent = OPAX/Jason kjørt som agent via /run eller /run/dag
# browser = direkte kall fra browser-chat (opax.vauco.no)
# cli = Gemini TUI CLI eller lokal agent.py kjørt manuelt
# cron = bakgrunnsjobb (billing_agent, anomaly_detector, dag-runner)
# api = ekstern REST-kall til /run uten sesjon
CallerType = Literal["agent", "browser", "cli", "cron", "api"]
# Gyldige module_name-verdier (utvides etter hvert som moduler comes online)
# jason/light = OPAX-agenten, gemini-2.5-flash
# jason/heavy = OPAX-agenten, gemini-2.5-pro
# costguard = CostGuard-analyse og anomali-endepunkter
# billing_agent = ml/billing_agent.py bakgrunns-analyse
# threadstone = Threadstone-modul (fremtidig)
# cli/gemini-tui = lokal Gemini TUI (arkivert, men kan aktiveres)
ModuleName = str # ikke enum — for fremtidssikkerhet
# Prismodell (USD per 1M tokens) — oppdater ved modellbytte
MODEL_PRICING = {
"gemini-2.5-pro": {"input": 1.25, "output": 10.00},
"gemini-2.5-flash": {"input": 0.075, "output": 0.30},
}
DEFAULT_PRICING = {"input": 1.25, "output": 10.00}
def _estimate_cost(model_name: str, input_tokens: int, output_tokens: int) -> float:
pricing = MODEL_PRICING.get(model_name, DEFAULT_PRICING)
cost = (input_tokens / 1_000_000) * pricing["input"] + \
(output_tokens / 1_000_000) * pricing["output"]
return round(cost, 8)
def log_token_usage(
agent_name: str, # deprecated alias — bruk module_name
model_name: str,
input_tokens: int,
output_tokens: int,
request_id: Optional[str] = None,
module_name: Optional[str] = None, # CG4: f.eks. 'jason/light', 'costguard'
caller_type: CallerType = "agent", # CG4: hvem/hva som kalte
session_id: Optional[str] = None, # CG4: sesjon-ID for gruppering
) -> None:
"""
Logg ett LLM-kall til BigQuery-tabellen llm_token_usage.
Feiler stille slik at applikasjonen aldri krasjer pga logging.
Labeling-konvensjoner:
module_name hvilken komponent (jason/light, costguard, billing_agent, )
caller_type hvordan kallet ble trigget (agent, browser, cli, cron, api)
agent_name beholdt for bakoverkompatibilitet; settes til module_name om gitt
"""
try:
from google.cloud import bigquery
client = bigquery.Client(project=PROJECT_ID)
effective_module = module_name or agent_name
total_tokens = input_tokens + output_tokens
estimated_cost = _estimate_cost(model_name, input_tokens, output_tokens)
row = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"agent_name": effective_module, # bakoverkompatibelt felt
"module_name": effective_module, # CG4: nytt felt
"caller_type": caller_type, # CG4: nytt felt
"session_id": session_id or "", # CG4: nytt felt
"model_name": model_name,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"total_tokens": total_tokens,
"estimated_cost_usd": estimated_cost,
"request_id": request_id or str(uuid.uuid4()),
}
errors = client.insert_rows_json(FULL_TABLE, [row])
if errors:
logger.warning(f"[token_logger] BQ insert errors: {errors}")
else:
logger.info(
f"[token_logger] Logged: module={effective_module} "
f"caller={caller_type} model={model_name} "
f"in={input_tokens} out={output_tokens} cost=${estimated_cost:.6f}"
)
except Exception as e:
logger.warning(f"[token_logger] Failed to log token usage (non-fatal): {e}")
# BQ table schema — brukes som referanse ved manuell oppretting, Terraform eller create_bq_table()
BQ_SCHEMA = [
{"name": "timestamp", "type": "TIMESTAMP", "mode": "REQUIRED"},
{"name": "agent_name", "type": "STRING", "mode": "REQUIRED"}, # bakoverkompatibelt
{"name": "module_name", "type": "STRING", "mode": "REQUIRED"}, # CG4
{"name": "caller_type", "type": "STRING", "mode": "REQUIRED"}, # CG4: agent|browser|cli|cron|api
{"name": "session_id", "type": "STRING", "mode": "NULLABLE"}, # CG4
{"name": "model_name", "type": "STRING", "mode": "REQUIRED"},
{"name": "input_tokens", "type": "INTEGER", "mode": "REQUIRED"},
{"name": "output_tokens", "type": "INTEGER", "mode": "REQUIRED"},
{"name": "total_tokens", "type": "INTEGER", "mode": "REQUIRED"},
{"name": "estimated_cost_usd", "type": "FLOAT", "mode": "REQUIRED"},
{"name": "request_id", "type": "STRING", "mode": "NULLABLE"},
]
def create_bq_table_if_not_exists() -> None:
"""
Opprett llm_token_usage-tabellen i BigQuery hvis den ikke finnes.
Trygt å kalle ved app-oppstart (CREATE TABLE IF NOT EXISTS-semantikk).
"""
try:
from google.cloud import bigquery
client = bigquery.Client(project=PROJECT_ID)
dataset_ref = client.dataset(DATASET)
table_ref = dataset_ref.table(TABLE)
try:
client.get_table(table_ref)
logger.info(f"[token_logger] Tabell {FULL_TABLE} finnes allerede.")
return
except Exception:
pass # tabell finnes ikke — opprett
schema = [bigquery.SchemaField(f["name"], f["type"], mode=f["mode"]) for f in BQ_SCHEMA]
table = bigquery.Table(table_ref, schema=schema)
client.create_table(table)
logger.info(f"[token_logger] Opprettet tabell {FULL_TABLE}.")
except Exception as e:
logger.warning(f"[token_logger] Kunne ikke opprette BQ-tabell (non-fatal): {e}")

0
agents/eval/.gitkeep Normal file
View File

51
agents/eval/run_eval.py Normal file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""
run_eval.py Run agent evaluation using Vertex AI Gen AI Evaluation Service.
Metrics: coherence, groundedness, tool_call_quality
CI/CD gate: fails build if groundedness < 0.8
"""
import vertexai
from vertexai.evaluation import EvalTask
PROJECT_ID = "propane-will-491900-m5"
LOCATION = "us-central1"
vertexai.init(project=PROJECT_ID, location=LOCATION)
EVAL_DATASET = [
{
"prompt": "What GCP region should all resources use?",
"reference": "us-central1",
},
{
"prompt": "What command tears down all Agent Runtimes?",
"reference": "Run 03-teardown.sh",
},
{
"prompt": "What ADK version is required for Memory Bank?",
"reference": "google-adk >= 2.0.0",
},
]
METRICS = ["coherence", "groundedness", "tool_call_quality"]
eval_task = EvalTask(
dataset=EVAL_DATASET,
metrics=METRICS,
experiment="oavauco-agent-eval",
)
result = eval_task.evaluate(
model="gemini-2.5-flash",
prompt_template="{prompt}",
)
print(result.summary_metrics)
# CI/CD gate
if result.summary_metrics.get("groundedness/mean", 1.0) < 0.8:
print("EVAL FAILED: groundedness below 0.8 threshold")
raise SystemExit(1)
print("EVAL PASSED")

44
agents/invite_store.py Normal file
View File

@ -0,0 +1,44 @@
"""
invite_store.py Enkel Firestore-basert invite-token store.
Brukes av POST /onboard/invite og GET /onboard/invite/{token}.
"""
import secrets
import datetime
from typing import Optional
TOKEN_TTL_HOURS = 72
class InviteStore:
def __init__(self, db):
self.db = db
self._col = "onboard_invites"
def create(self, company: str, email: str, tier: str, created_by: str = "system") -> str:
token = secrets.token_urlsafe(32)
expires_at = datetime.datetime.utcnow() + datetime.timedelta(hours=TOKEN_TTL_HOURS)
self.db.collection(self._col).document(token).set({
"company": company,
"email": email,
"tier": tier,
"created_by": created_by,
"created_at": datetime.datetime.utcnow().isoformat(),
"expires_at": expires_at.isoformat(),
"used": False,
})
return token
def get(self, token: str) -> Optional[dict]:
doc = self.db.collection(self._col).document(token).get()
if not doc.exists:
return None
data = doc.to_dict()
if data.get("used"):
return None
expires_at = datetime.datetime.fromisoformat(data["expires_at"])
if datetime.datetime.utcnow() > expires_at:
return None
return data
def mark_used(self, token: str):
self.db.collection(self._col).document(token).update({"used": True})

133
agents/jason-vauger.md Normal file
View File

@ -0,0 +1,133 @@
# Jason Vauger — Personlig AI-assistent
**Status:** Aktiv (opax-mcp tilkoblet)
**Sist oppdatert:** 2026-06-15
**Eier:** Chris Christiansen / OS-Vauco
---
## Hvem er Jason Vauger?
Jason Vauger er den personlige AI-assistenten til Chris — det øverste interface-laget i OSVauco-stacken. Jason er ikke en teknisk worker-agent, men en **persona** som wrapper hele systemet og kommuniserer med Chris i naturlig språk om alt fra daglige gjøremål til strategiske beslutninger.
Når alt er oppe og går, er Jason det eneste grensesnittet Chris trenger.
---
## Rolle i OSVauco-stacken
```
┌─────────────────────────────────────────────┐
│ JASON VAUGER │
│ (Personlig assistent — persona-lag) │
│ Naturlig språk inn/ut • Kontekst over tid │
└────────────────┬────────────────────────────┘
│ delegerer via
┌────────────────▼────────────────────────────┐
│ opax-mcp (Cloud Run) │
│ https://opax-mcp-zjbqp3prqq-uc.a.run.app │
│ 25 tools: billing, notify, gh, platform │
└──────┬──────┬──────┬──────┬──────┬──────────┘
│ │ │ │ │
billing notify github agents platform
```
| Egenskap | Verdi |
|---|---|
| **PS-rolle** | Persona-wrapper (ikke PS-worker) |
| **Dask-rolle** | Trigger for OPAX DAG via naturlig-språk-input |
| **Online learning** | Nei — kontekstuell, ikke gradient-basert |
| **Heavy mode** | Nei — bruker OPAX for tunge oppgaver |
| **Primær modell** | Gemini 2.5 Pro (via Vertex AI) |
| **Interface** | TUI (vauco-gemini-tui-bridge), Chat, CLI |
---
## Ansvarsområder
### 1. Daglige gjøremål
- Holde oversikt over åpne oppgaver, handoffs og TODO-er
- Påminnelser og prioritering basert på kontekst
- Oppsummere dagens status på tvers av alle systemer
### 2. Kontekstbærer
- Husker preferanser, pågående prosjekter og beslutninger over tid
- Bruker `agents/memory/` som langtidsminne
- Bruker `agents/rag/` for dokumentkontekst og historikk
### 3. Orchestrator-interface via opax-mcp
- Oversetter naturlig-språk-instruksjoner til opax-mcp tool-kall
- Alle tool-kall går via `agents/tools/opax_mcp_client.py`
- Auth: Cloud Run IAM (lag 1) + X-MCP-Secret (lag 2)
### 4. Infrastruktur-awareness
- Har innsikt i GCP-status, Cloud Run, kostnader (via get_billing_summary)
- Kan varsle om unormale hendelser eller budsjettavvik (via get_billing_anomalies)
- CI/CD-status via get_build_status og list_commits
### 5. Handoff og dokumentasjon
- Oppretter GitHub Issues via create_github_issue
- Pusher filer via push_file
- Logger viktige beslutninger og noter til repoet
---
## Personlighet og tone
Jason er direkte, presis og lojal. Han er ikke en chatbot som prøver å imponere — han er en pålitelig assistent som gjør det som trengs uten unødvendig prat. Når Chris sier "fikset" betyr det fikset. Når han sier "notis" betyr det logg det og ikke glem det.
- **Tone:** Profesjonell, men uformell. Norsk som standard.
- **Initiativ:** Tar ikke initiativ uten grunn — men varsler proaktivt ved viktige hendelser.
- **Feil:** Innrømmer feil direkte og korrigerer uten omsvøp.
---
## Integrasjonspunkter
| System | Integrasjon | Status |
|---|---|---|
| opax-mcp Cloud Run | Primær tool-gateway (25 tools) | ✅ Aktiv |
| `agents/tools/opax_mcp_client.py` | REST-klient for alle tools | ✅ Aktiv |
| `agents/tools/mcp_tools.py` | ADK FunctionTool-wrapper | ✅ Aktiv |
| `agents/core-logic/agent.py` | Jason ADK agent runtime | 🔄 Kobles til ved neste deploy |
| `agents/memory/` | Langtidsminne / preferanser | 🔄 Under bygging |
| `agents/rag/` | Dokumentkontekst og historikk | 🔄 Under bygging |
| vauco-gemini-tui-bridge | Primær input-kanal | 🔄 Under bygging |
| Costguard | Kostnadsovervåkning og varsler | ⏳ Planlagt |
| Threadstone | Prosjekt- og trådstyring | ⏳ Planlagt |
---
## Vertex AI / ADK oppsett — neste steg
For å kjøre Jason i prod på Vertex AI Reasoning Engine:
```bash
# 1. Aktiver Reasoning Engine API
gcloud services enable aiplatform.googleapis.com \
--project=propane-will-491900-m5
# 2. Deploy Jason ADK agent
cd agents/core-logic
python deploy_agent.py
# 3. Sett MCP_SERVER_URL og MCP_SECRET som env i Cloud Run
gcloud run services update osvauco-agent \
--region=us-central1 \
--project=propane-will-491900-m5 \
--set-env-vars=MCP_SERVER_URL=https://opax-mcp-zjbqp3prqq-uc.a.run.app \
--set-secrets=MCP_SECRET=mcp-server-key:latest
```
---
## Avhengigheter og forutsetninger
- `opax-mcp` Cloud Run service oppe på `https://opax-mcp-zjbqp3prqq-uc.a.run.app`
- Secret Manager: `mcp-server-key` og `github-pat` konfigurert ✅
- Vertex AI Reasoning Engine tilgjengelig i GCP-prosjektet
- RAG-korpus populert med relevante dokumenter
---
*Jason Vauger — han blir den du trenger han til å bli.*

View File

@ -0,0 +1,15 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
ENV PORT=8080
ENV PYTHONUNBUFFERED=1
EXPOSE 8080
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8080"]

View File

@ -0,0 +1,95 @@
# OPAX-MCP — Vauco sin CI/CD-kanal mot GCP
**CI1a** | **Service:** `opax-mcp` | **Region:** `us-central1`
Vauco sin egen MCP-server som erstatter GitHub Actions som CI/CD-trigger.
Jason og Emma kaller denne direkte for å deploye, pushe statiske filer og hente status.
## Arkitektur
```
Chris / Jason (Cloud Run) / Emma (Gemma GPU-VM) / Perplexity (stand-in)
POST /mcp/call {tool: "push_static", params: {...}}
OPAX-MCP (Cloud Run opax-mcp, IAP-beskyttet)
GCP APIs: GCS · Cloud Build · Cloud Run · Cloud Logging
Resultat returneres til kalleren
```
## Tilgjengelige verktøy
| Tool | Endepunkt | Beskrivelse |
|------|-----------|-------------|
| `push_static` | `POST /tools/push_static` | Last opp HTML/CSS/JS til GCS |
| `get_build_status` | `GET /tools/get_build_status` | Hent siste Cloud Build-status |
| `deploy_service` | `POST /tools/deploy_service` | Trigger Cloud Build → Cloud Run deploy |
| `get_logs` | `GET /tools/get_logs` | Hent Cloud Run-logger |
Alle verktøy er også tilgjengelig via det unifiserte endepunktet:
```
POST /mcp/call
{"tool": "push_static", "params": {"file_path": "static/jason.html", "content": "..."}}
```
## Auth
- **Utvikling:** `X-MCP-Key` header (Secret Manager: `mcp-server-key`)
- **Produksjon:** Cloud Run er `--no-allow-unauthenticated` + IAP
- **Jason/Emma:** Kaller via ADK tool med service-account `jason.vauger@vauco.no`
## Deploy
```bash
# Første gang: opprett secret
echo -n "$(openssl rand -hex 32)" | \
gcloud secrets create mcp-server-key \
--data-file=- \
--project=propane-will-491900-m5
# Deploy opax-mcp
gcloud builds submit \
--config agents/mcp_server/cloudbuild.yaml \
--project=propane-will-491900-m5 \
.
```
## CI1e — ADK-integrasjon (neste steg)
Når `opax-mcp` er live, legges det til som ADK-tool i `agents/core-logic/agent.py`:
```python
# agents/core-logic/tools/mcp_tools.py
from google.adk.tools import FunctionTool
import httpx
MCP_BASE = "https://opax-mcp-<hash>.run.app"
async def push_static(file_path: str, content: str) -> dict:
"""Last opp statisk fil til GCS via OPAX-MCP."""
async with httpx.AsyncClient() as client:
r = await client.post(f"{MCP_BASE}/mcp/call",
json={"tool": "push_static", "params": {"file_path": file_path, "content": content}},
headers={"X-MCP-Key": os.environ["MCP_SECRET"]},
timeout=30,
)
return r.json()
push_static_tool = FunctionTool(func=push_static)
```
Deretter registreres `push_static_tool` i `root_agent` → Jason kan deploye direkte fra chat.
## Planlagte verktøy (CI1bd)
| Tool | Status |
|------|--------|
| `run_query` | 🔮 CI1b |
| `write_secret` | 🔮 CI1c |
| `update_dns` | 🔮 CI1d |
| `create_service` | 🔮 Fase C |
---
*OPAX-MCP · CI1a · 2026-06-10 · propane-will-491900-m5*

View File

@ -0,0 +1,45 @@
# agents/mcp_server/cloudbuild.yaml
# Deploy opax-mcp som separat Cloud Run-tjeneste
# Trigger: path filter agents/mcp_server/** på branch main
substitutions:
_TAG: 'latest'
_OSVAUCO_TRIGGER_ID: '38423976-91ff-4ff4-859e-1f262344c609'
steps:
- name: 'gcr.io/cloud-builders/docker'
entrypoint: bash
args:
- '-c'
- |
TAG="${SHORT_SHA:-${_TAG}}"
docker build \
-t us-central1-docker.pkg.dev/$PROJECT_ID/osvauco-repo/opax-mcp:$$TAG \
-f agents/mcp_server/Dockerfile \
agents/mcp_server
docker push us-central1-docker.pkg.dev/$PROJECT_ID/osvauco-repo/opax-mcp:$$TAG
echo "$$TAG" > /workspace/image_tag.txt
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: bash
args:
- '-c'
- |
TAG=$(cat /workspace/image_tag.txt)
gcloud run deploy opax-mcp \
--image=us-central1-docker.pkg.dev/$PROJECT_ID/osvauco-repo/opax-mcp:$$TAG \
--region=us-central1 \
--platform=managed \
--no-allow-unauthenticated \
--service-account=jason-vauger@$PROJECT_ID.iam.gserviceaccount.com \
--set-env-vars=GOOGLE_CLOUD_PROJECT=$PROJECT_ID,STATIC_BUCKET=opax-vauco-static,CLOUD_RUN_SERVICE=osvauco-agent,CLOUD_BUILD_TRIGGER_ID=${_OSVAUCO_TRIGGER_ID},OPAX_IAP_CLIENT_ID=357036551735-kq8nt7ld38hfqlcfb3n52ef7tala4meo.apps.googleusercontent.com \
--update-secrets=MCP_SECRET=mcp-server-key:latest,GITEA_TOKEN=gitea-token:latest,GITHUB_PAT=github-pat:latest \
--memory=512Mi \
--cpu=1 \
--min-instances=0 \
--max-instances=5 \
--timeout=60
options:
logging: CLOUD_LOGGING_ONLY
defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET

View File

@ -0,0 +1,6 @@
fastapi>=0.111.0
uvicorn[standard]>=0.29.0
google-cloud-storage>=2.16.0
google-cloud-build>=3.24.0
google-cloud-logging>=3.10.0
pydantic>=2.7.0

216
agents/mcp_server/server.py Normal file
View File

@ -0,0 +1,216 @@
# agents/mcp_server/server.py
# OPAX-MCP — Vauco sin eigen MCP-server / CI-kanal mot GCP
import os
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Depends, Header, Request
from pydantic import BaseModel
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
REGION = os.environ.get("REGION", "us-central1")
STATIC_BUCKET = os.environ.get("STATIC_BUCKET", "opax-vauco-static")
CLOUD_RUN_SERVICE = os.environ.get("CLOUD_RUN_SERVICE", "osvauco-agent")
MCP_SECRET = os.environ.get("MCP_SECRET", "")
BUILD_TRIGGER_ID = os.environ.get("CLOUD_BUILD_TRIGGER_ID", "38423976-91ff-4ff4-859e-1f262344c609")
def verify_token(request: Request, x_mcp_key: str = Header(default="")):
# Aksepter både X-MCP-Key og api-key (Perplexity MCP connector bruker api-key)
token = x_mcp_key or request.headers.get("api-key", "")
if MCP_SECRET and token != MCP_SECRET:
raise HTTPException(status_code=401, detail="Ugyldig MCP-nøkkel")
return True
def _get_access_token() -> str:
import google.auth
import google.auth.transport.requests
creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
creds.refresh(google.auth.transport.requests.Request())
return creds.token
def _list_builds_rest(project_id: str, region: str, trigger_id: str, page_size: int = 1):
import urllib.request
import json
token = _get_access_token()
url = (
f"https://cloudbuild.googleapis.com/v1/"
f"projects/{project_id}/locations/{region}/builds"
f"?filter=trigger_id%3D%22{trigger_id}%22&pageSize={page_size}"
)
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())
def _parse_ts(ts):
"""Parser tidsstempel robust — aksepterer str, DatetimeWithNanoseconds eller datetime."""
from datetime import datetime, timezone
if ts is None:
return None
if hasattr(ts, 'year'):
if getattr(ts, 'tzinfo', None) is None:
return ts.replace(tzinfo=timezone.utc)
return ts
ts = str(ts).rstrip("Z")
try:
clean_ts = ts[:19]
if "." in ts:
frac = ts[20:26].split("+")[0].split("-")[0] if len(ts) > 20 else "000000"
clean_ts = ts[:19] + "." + frac.ljust(6, "0")[:6]
return datetime.strptime(clean_ts, "%Y-%m-%dT%H:%M:%S.%f").replace(tzinfo=timezone.utc)
return datetime.strptime(clean_ts, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc)
except Exception:
return None
class PushStaticRequest(BaseModel):
file_path: str
content: str
content_type: str = "text/html; charset=utf-8"
cache_control: str = "public, max-age=300"
class DeployServiceRequest(BaseModel):
branch: str = "main"
trigger_id: str = ""
substitutions: dict = {}
class ToolCallRequest(BaseModel):
tool: str
params: dict = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info(f"OPAX-MCP starting | project={PROJECT_ID} | region={REGION} | trigger={BUILD_TRIGGER_ID}")
yield
app = FastAPI(title="OPAX-MCP", version="1.0.0", lifespan=lifespan)
@app.get("/health")
async def health():
return {"status": "ok", "service": "opax-mcp", "project": PROJECT_ID, "region": REGION}
@app.get("/mcp/tools")
async def list_tools(_: bool = Depends(verify_token)):
return {"tools": [
{"name": "push_static", "description": "Last opp statisk fil til GCS (opax.vauco.no)"},
{"name": "get_build_status", "description": "Hent status på siste Cloud Build for osvauco-agent-main-trigger"},
{"name": "deploy_service", "description": "Trigger ny Cloud Build deploy (HITL)"},
{"name": "get_logs", "description": "Hent Cloud Run-logger for osvauco-agent"},
]}
@app.post("/tools/push_static")
async def push_static(req: PushStaticRequest, _: bool = Depends(verify_token)):
try:
from google.cloud import storage
client = storage.Client(project=PROJECT_ID)
blob = client.bucket(STATIC_BUCKET).blob(req.file_path)
blob.cache_control = req.cache_control
blob.upload_from_string(req.content.encode("utf-8"), content_type=req.content_type)
logger.info(f"[push_static] {req.file_path} ({len(req.content)} bytes)")
return {"ok": True, "file_path": req.file_path, "bucket": STATIC_BUCKET,
"bytes": len(req.content.encode("utf-8")),
"url": f"https://storage.googleapis.com/{STATIC_BUCKET}/{req.file_path}"}
except Exception as e:
logger.error(f"[push_static] {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.get("/tools/get_build_status")
async def get_build_status(_: bool = Depends(verify_token)):
try:
data = _list_builds_rest(PROJECT_ID, REGION, BUILD_TRIGGER_ID, page_size=1)
builds = data.get("builds", [])
if not builds:
return {"status": "unknown", "message": f"Ingen builds for trigger {BUILD_TRIGGER_ID} i {REGION}"}
b = builds[0]
status_str = b.get("status", "unknown").lower()
subs = b.get("substitutions", {})
t0 = _parse_ts(b.get("startTime"))
t1 = _parse_ts(b.get("finishTime"))
duration_s = int((t1 - t0).total_seconds()) if t0 and t1 else None
return {
"status": status_str,
"build_id": b.get("id", ""),
"branch": subs.get("BRANCH_NAME", "main"),
"commit": subs.get("SHORT_SHA", ""),
"duration_s": duration_s,
"log_url": b.get("logUrl", ""),
"region": REGION,
"trigger_id": BUILD_TRIGGER_ID,
}
except Exception as e:
logger.error(f"[get_build_status] {e}", exc_info=True)
return {"status": "error", "message": str(e)}
@app.post("/tools/deploy_service")
async def deploy_service(req: DeployServiceRequest, _: bool = Depends(verify_token)):
try:
from google.cloud.devtools import cloudbuild_v1
from google.api_core.client_options import ClientOptions
client = cloudbuild_v1.CloudBuildClient(
client_options=ClientOptions(api_endpoint=f"{REGION}-cloudbuild.googleapis.com")
)
trigger_id = req.trigger_id or BUILD_TRIGGER_ID
subs = {"BRANCH_NAME": req.branch, **req.substitutions}
response = client.run_build_trigger(
project_id=PROJECT_ID,
trigger_id=trigger_id,
source=cloudbuild_v1.RepoSource(branch_name=req.branch, substitutions=subs),
)
build_id = response.metadata.build.id if hasattr(response, 'metadata') else "ukjent"
logger.info(f"[deploy_service] build {build_id} branch {req.branch}")
return {"ok": True, "build_id": build_id, "branch": req.branch, "trigger_id": trigger_id}
except HTTPException:
raise
except Exception as e:
logger.error(f"[deploy_service] {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.get("/tools/get_logs")
async def get_logs(lines: int = 50, severity: str = "DEFAULT", _: bool = Depends(verify_token)):
try:
from google.cloud import logging as gcloud_logging
client = gcloud_logging.Client(project=PROJECT_ID)
filter_str = (f'resource.type="cloud_run_revision" '
f'resource.labels.service_name="{CLOUD_RUN_SERVICE}"')
if severity and severity != "DEFAULT":
filter_str += f' severity>={severity}'
entries = list(client.list_entries(
filter_=filter_str, order_by=gcloud_logging.DESCENDING, page_size=lines))
log_lines = []
for entry in entries:
payload = entry.payload
text = payload.get("message", str(payload)) if isinstance(payload, dict) else str(payload)
ts = _parse_ts(entry.timestamp)
log_lines.append({"timestamp": ts.isoformat() if ts else str(entry.timestamp), "severity": str(entry.severity), "text": text[:400]})
return {"service": CLOUD_RUN_SERVICE, "count": len(log_lines), "logs": log_lines}
except Exception as e:
logger.error(f"[get_logs] {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.post("/mcp/call")
async def mcp_call(req: ToolCallRequest, _: bool = Depends(verify_token)):
if req.tool == "push_static":
return await push_static(PushStaticRequest(**req.params), True)
elif req.tool == "get_build_status":
return await get_build_status(True)
elif req.tool == "deploy_service":
return await deploy_service(DeployServiceRequest(**req.params), True)
elif req.tool == "get_logs":
return await get_logs(lines=req.params.get("lines", 50), severity=req.params.get("severity", "DEFAULT"), _=True)
else:
raise HTTPException(status_code=404, detail=f"Ukjent tool: {req.tool}")

0
agents/memory/.gitkeep Normal file
View File

View File

@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
memory_setup.py Initialize Agent Engine instance with Sessions + Memory Bank.
Project: propane-will-491900-m5 | Region: us-central1
SDK: google-cloud-aiplatform >= 1.111.0
COST NOTE: Sessions + Memory Bank are metered since Jan 28, 2026.
Trim session histories. Only persist high-value facts to long-term memory.
"""
import vertexai
from vertexai import Client
from google.adk.memory import VertexAiMemoryBankService
from google.adk.sessions import VertexAiSessionService
PROJECT_ID = "propane-will-491900-m5"
LOCATION = "us-central1"
def create_agent_engine() -> str:
"""Create an Agent Engine instance (backing store for Sessions + Memory Bank)."""
client = Client(project=PROJECT_ID, location=LOCATION)
agent_engine = client.agent_engines.create()
resource_name = agent_engine.api_resource.name
agent_engine_id = resource_name.split("/")[-1]
print(f"Agent Engine created: {resource_name}")
print(f"Agent Engine ID: {agent_engine_id}")
print("Store this in Secret Manager or env var: AGENT_ENGINE_ID")
return agent_engine_id
def get_services(agent_engine_id: str):
"""Return configured session and memory services for use with ADK Runner."""
memory_service = VertexAiMemoryBankService(
project=PROJECT_ID,
location=LOCATION,
agent_engine_id=agent_engine_id,
)
session_service = VertexAiSessionService(
project_id=PROJECT_ID,
location=LOCATION,
agent_engine_id=agent_engine_id,
)
return session_service, memory_service
if __name__ == "__main__":
agent_engine_id = create_agent_engine()
session_svc, memory_svc = get_services(agent_engine_id)
print("Services ready.")
print(f"session_service: {session_svc}")
print(f"memory_service: {memory_svc}")

View File

@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""
orchestrator.py OSVauco multi-agent orchestrator using ADK 2.0 collaborative workflow.
Architecture:
root_agent (coordinator / LLM-driven delegation)
rag_agent retrieves answers from private knowledge base
gcp_ops_agent handles GCP operations questions and script generation
memory_agent loads/stores long-term memory via Memory Bank
farewell_agent session closings
Pattern: Coordinator with sub_agents list.
Requires: google-adk >= 2.0.0
"""
from __future__ import annotations
import os
import logging
from typing import Optional
from google.adk.agents import Agent
from google.adk.agents.callback_context import CallbackContext
from google.adk.tools.load_memory_tool import load_memory_tool
from google.adk.tools.preload_memory_tool import preload_memory_tool
from google.genai.types import Content, Part
logger = logging.getLogger(__name__)
PROJECT_ID = os.environ.get("PROJECT_ID", "propane-will-491900-m5")
REGION = os.environ.get("REGION", "us-central1")
RAG_CORPUS_NAME = os.environ.get("RAG_CORPUS_NAME", "")
ORCHESTRATOR_MODEL = os.environ.get("ORCHESTRATOR_MODEL", "gemini-2.5-flash")
SUBAGENT_MODEL = os.environ.get("SUBAGENT_MODEL", "gemini-2.5-flash")
# ── Safety callbacks ──────────────────────────────────────────────────────────
BLOCKED_PATTERNS = [
"ignore previous instructions",
"ignore all instructions",
"drop table",
"system prompt",
"jailbreak",
"disregard safety",
]
def before_model_callback(callback_context: CallbackContext, llm_request) -> Optional[Content]:
try:
user_text = ""
if llm_request.contents:
last = llm_request.contents[-1]
if last.parts:
user_text = last.parts[0].text.lower()
for pattern in BLOCKED_PATTERNS:
if pattern in user_text:
logger.warning("Blocked pattern detected: '%s'", pattern)
return Content(parts=[Part(text="I cannot process that request. Please rephrase.")])
except Exception as exc:
logger.error("before_model_callback error: %s", exc)
return None
def before_tool_callback(tool, args: dict, tool_context) -> Optional[dict]:
dangerous = ["DROP", "DELETE FROM", "TRUNCATE", "--", ";"]
for val in args.values():
if isinstance(val, str):
for d in dangerous:
if d.upper() in val.upper():
raise ValueError(f"Tool argument rejected by safety guardrail: '{val}'")
return None
# ── Sub-agents ────────────────────────────────────────────────────────────────
rag_agent = Agent(
model=SUBAGENT_MODEL,
name="rag_agent",
description="Retrieves answers from the private OSVauco knowledge base (RAG corpus).",
instruction=(
"You are a knowledge retrieval specialist. "
"Use search_knowledge_base to find relevant information and return well-cited answers."
),
before_model_callback=before_model_callback,
)
gcp_ops_agent = Agent(
model=SUBAGENT_MODEL,
name="gcp_ops_agent",
description="Answers GCP operations questions: scripts, IAM, billing, Cloud Run, ADK deployments.",
instruction=(
"You are a GCP operations expert for project propane-will-491900-m5 in us-central1. "
"Provide accurate gcloud CLI commands, IAM patterns, and ADK deployment guidance. "
"Always include cost-safety reminders (teardown, billing budgets)."
),
before_model_callback=before_model_callback,
)
memory_agent = Agent(
model=SUBAGENT_MODEL,
name="memory_agent",
description="Manages long-term memory: loads past context and stores new facts for future sessions.",
instruction=(
"You manage the agent's long-term memory. "
"Use load_memory_tool to retrieve past facts and preload_memory_tool to store important new facts. "
"Only persist high-value, factual information — not transient conversation."
),
tools=[load_memory_tool, preload_memory_tool],
before_model_callback=before_model_callback,
)
farewell_agent = Agent(
model=SUBAGENT_MODEL,
name="farewell_agent",
description="Handles session closings, summaries, and goodbye messages.",
instruction="Generate a concise, friendly session summary and closing message.",
)
# ── Root orchestrator ─────────────────────────────────────────────────────────
root_agent = Agent(
model=ORCHESTRATOR_MODEL,
name="oavauco_orchestrator",
description="OSVauco root orchestrator — delegates to specialist sub-agents.",
instruction=(
"You are the OSVauco orchestrator for project propane-will-491900-m5. "
"Delegate to sub-agents based on the user's intent:\n"
"- Knowledge base questions → rag_agent\n"
"- GCP operations, scripts, IAM, billing → gcp_ops_agent\n"
"- Memory recall or storage → memory_agent\n"
"- Session endings → farewell_agent\n"
"Always synthesize sub-agent responses into a clear, concise final answer."
),
sub_agents=[rag_agent, gcp_ops_agent, memory_agent, farewell_agent],
before_model_callback=before_model_callback,
before_tool_callback=before_tool_callback,
)

203
agents/osv/pipeline.py Normal file
View File

@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""
agents/osv/pipeline.py OSV lokal Ollama-pipeline for osvauco-dev-vm
Arkitektur (tre lag, sekvensiell):
gemma3:4b (front) forstår intent, strukturerer oppgave
qwen2.5:7b (analyse) dyptgående analyse, kode, planlegging
gemma3:4b (output) formaterer og leverer svar til bruker
Eskalering til Emma (emma-gpu-vm, Gemma 4 27B):
- Kun hvis Chris eksplisitt sier "emma" i meldingen
- Eller hvis pipeline selv er usikker og bruker bekrefter
- Aldri automatisk
Basert ideer fra:
- agents/core-logic/heavy_predictor.py (tung-modus trigger)
- agents/core-logic/feedback_loop.py (telemetri / kvalitet)
- agents/multi_agent/orchestrator.py (delegerings-mønster)
"""
from __future__ import annotations
import os
import logging
import httpx
logger = logging.getLogger(__name__)
# ── Konfigurasjon ─────────────────────────────────────────────────────────────
OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
EMMA_OLLAMA_BASE = os.environ.get("EMMA_OLLAMA_BASE_URL", "http://emma-gpu-vm:11434")
FRONT_MODEL = os.environ.get("OSV_FRONT_MODEL", "gemma3:4b")
ANALYST_MODEL = os.environ.get("OSV_ANALYST_MODEL", "qwen2.5:7b")
EMMA_MODEL = os.environ.get("OSV_EMMA_MODEL", "gemma4:27b")
TIMEOUT = int(os.environ.get("OSV_TIMEOUT", "120"))
# Nøkkelord som trigger Emma-eskalering
EMMA_TRIGGERS = ["emma", "@emma", "send til emma", "bruk emma"]
# Konfidensterskel — under denne spør pipeline om Emma skal brukes
CONFIDENCE_THRESHOLD = 0.4
# ── Hjelpefunksjoner ─────────────────────────────────────────────────────────
def _ollama_generate(base_url: str, model: str, prompt: str, system: str = "") -> str:
"""Kaller Ollama /api/generate og returnerer tekst-svaret."""
payload = {
"model": model,
"prompt": prompt,
"stream": False,
}
if system:
payload["system"] = system
try:
resp = httpx.post(
f"{base_url}/api/generate",
json=payload,
timeout=TIMEOUT,
)
resp.raise_for_status()
return resp.json().get("response", "").strip()
except httpx.HTTPError as e:
logger.error("Ollama feil (%s/%s): %s", base_url, model, e)
raise
def _wants_emma(message: str) -> bool:
"""Returner True hvis meldingen eksplisitt ber om Emma."""
lower = message.lower()
return any(trigger in lower for trigger in EMMA_TRIGGERS)
def _confidence_check(response: str) -> float:
"""
Enkel heuristikk lav konfidans hvis svaret inneholder
tvils-markører. Returnerer verdi mellom 0.0 og 1.0.
"""
doubt_markers = [
"jeg er usikker", "vet ikke", "ikke sikker",
"vanskelig å si", "kan ikke svare", "trenger mer info",
"i'm not sure", "unclear", "cannot determine",
]
lower = response.lower()
hits = sum(1 for m in doubt_markers if m in lower)
return max(0.0, 1.0 - (hits * 0.3))
# ── Pipeline-modi ─────────────────────────────────────────────────────────────
def run_local(user_message: str) -> dict:
"""
Kjører tre-lags lokal pipeline:
gemma3:4b qwen2.5:7b gemma3:4b
"""
logger.info("[OSV] Starter lokal pipeline")
# Lag 1 — Gemma front: forstår og strukturerer
front_response = _ollama_generate(
OLLAMA_BASE,
FRONT_MODEL,
user_message,
system=(
"Du er OSV, en intelligent assistent for Vauco AS. "
"Din jobb er å forstå brukerens intent presist og strukturere "
"oppgaven klart for videre analyse. Vær kortfattet og presis. "
"Svar på norsk med mindre brukeren skriver engelsk."
),
)
logger.info("[OSV] Front (gemma3:4b) ferdig")
# Lag 2 — Qwen analyst: dyptgående analyse
analyst_response = _ollama_generate(
OLLAMA_BASE,
ANALYST_MODEL,
front_response,
system=(
"Du er en analytiker for Vauco AS. Du mottar en strukturert oppgave "
"og skal gi en grundig, handlingsorientert analyse. "
"Spesialiser deg på kode, infrastruktur og tekniske beslutninger. "
"Vær direkte — Chris har ikke tid til omsvøp."
),
)
logger.info("[OSV] Analyst (qwen2.5:7b) ferdig")
# Sjekk konfidans — be om Emma-bekreftelse hvis lav
confidence = _confidence_check(analyst_response)
if confidence < CONFIDENCE_THRESHOLD:
logger.warning("[OSV] Lav konfidans (%.2f) — ber om Emma-bekreftelse", confidence)
return {
"mode": "ask_emma",
"confidence": confidence,
"partial_response": analyst_response,
"ask": "Jeg er usikker på dette. Vil du at jeg sender det til Emma (Gemma 4 27B) for et sterkere svar?",
}
# Lag 3 — Gemma output: formaterer og leverer
final_response = _ollama_generate(
OLLAMA_BASE,
FRONT_MODEL,
analyst_response,
system=(
"Du er OSV, siste ledd i en analyse-pipeline for Vauco AS. "
"Du mottar et analysert svar og skal formatere det rent og "
"lesbart for Chris. Bruk lister og tabeller der det er naturlig. "
"Vær kortfattet. Svar på norsk med mindre konteksten er engelsk."
),
)
logger.info("[OSV] Output (gemma3:4b) ferdig")
return {
"mode": "local",
"confidence": confidence,
"response": final_response,
}
def run_emma(user_message: str) -> dict:
"""
Eskalerer direkte til Emma-GPU-VM (Gemma 4 27B).
Kalles kun når Chris eksplisitt ber om Emma.
"""
logger.info("[OSV] Eskalerer til Emma (%s)", EMMA_MODEL)
response = _ollama_generate(
EMMA_OLLAMA_BASE,
EMMA_MODEL,
user_message,
system=(
"Du er Emma Vauger, intern AI-assistent for Vauco AS. "
"Du hjelper Chris Christiansen med strategiske beslutninger, "
"GCP-infrastruktur, kodearkitektur og teknisk planlegging. "
"Vær direkte, analytisk og ærlig. Aldri sycophantisk. "
"Norsk som standard."
),
)
return {
"mode": "emma",
"model": EMMA_MODEL,
"response": response,
}
# ── Hoved-inngangspunkt ───────────────────────────────────────────────────────
def run(user_message: str, force_emma: bool = False) -> dict:
"""
Hoved-dispatcher for OSV-pipelinen.
Regler:
1. force_emma=True ELLER "emma" i meldingen run_emma()
2. Alt annet run_local() med mulig ask_emma retur
"""
if force_emma or _wants_emma(user_message):
return run_emma(user_message)
return run_local(user_message)
# ── CLI-test ──────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
import json
msg = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Hva er status på OPAX?"
result = run(msg)
print(json.dumps(result, ensure_ascii=False, indent=2))

0
agents/rag/.gitkeep Normal file
View File

334
agents/rag/setup_corpus.py Normal file
View File

@ -0,0 +1,334 @@
#!/usr/bin/env python3
"""
setup_corpus.py Create a Vertex AI RAG Engine corpus and import documents.
Project: propane-will-491900-m5
SDK 1.153.1 has bugs in backend_config and RagManagedDbConfig.
We use the REST API (v1beta1) for corpus creation and engine configuration.
Changes implemented:
- Graceful degradation: If RAG Engine is restricted (Spanner mode), skip and exit cleanly.
- ensure_serverless_engine_config() sets RAG_ENGINE_AVAILABLE flag.
- get_or_create_corpus() returns None if RAG is unavailable.
"""
import os
import json
import time
import subprocess
import sys
import vertexai
from vertexai import rag
from vertexai.rag import RagCorpus
PROJECT_ID = "propane-will-491900-m5"
LOCATION = os.environ.get("RAG_LOCATION", "us-central1")
CORPUS_DISPLAY_NAME = os.environ.get("RAG_CORPUS_NAME", "osvauco-knowledge-base")
GCS_SOURCE = os.environ.get(
"RAG_GCS_SOURCE",
f"gs://{PROJECT_ID}-agent-staging/rag-docs/",
)
# Global status flag
RAG_ENGINE_AVAILABLE = True
# ragEngineConfig is a project-level control-plane endpoint.
# It only exists in us-central1 regardless of where the corpus lives.
_ENGINE_CONFIG_LOCATION = "us-central1"
def get_token() -> str:
return subprocess.check_output(
["gcloud", "auth", "print-access-token"], text=True
).strip()
def _engine_config_url() -> str:
return (
f"https://{_ENGINE_CONFIG_LOCATION}-aiplatform.googleapis.com/v1beta1"
f"/projects/{PROJECT_ID}/locations/{_ENGINE_CONFIG_LOCATION}/ragEngineConfig"
)
def get_engine_config() -> dict:
token = get_token()
r = subprocess.run(
["curl", "-s", "-H", f"Authorization: Bearer {token}", _engine_config_url()],
capture_output=True, text=True,
)
try:
return json.loads(r.stdout)
except Exception:
print(f"Failed to parse engine config: {r.stdout}")
return {}
def ensure_serverless_engine_config() -> None:
"""
Set project-level RAG Engine Config to basic (serverless) tier.
Only applicable for us-central1, us-east1, us-east4.
"""
global RAG_ENGINE_AVAILABLE
restricted_regions = {"us-central1", "us-east1", "us-east4"}
if LOCATION not in restricted_regions:
print(f"Skipping engine config check for location: {LOCATION} (not restricted)")
return
print(f"Ensuring RAG Engine Config in {_ENGINE_CONFIG_LOCATION} is set to serverless (basic) tier...")
token = get_token()
# Use updateMask=ragManagedDbConfig to target the entire configuration block.
# Set spanner: null to explicitly clear it.
payload = {
"ragManagedDbConfig": {
"basic": {},
"spanner": None
}
}
url = f"{_engine_config_url()}"
result = subprocess.run(
["curl", "-s", "-X", "PATCH",
"-H", f"Authorization: Bearer {token}",
"-H", "Content-Type: application/json",
url,
"-d", json.dumps(payload)],
capture_output=True, text=True,
)
try:
resp = json.loads(result.stdout)
except Exception:
print(f" \u26a0\ufe0f Failed to parse PATCH response: {result.stdout}")
return
if "error" in resp:
err = resp["error"]
print(f" \u26a0\ufe0f Engine config PATCH error ({err.get('code')}): {err.get('message')}")
op_name = resp.get("name", "")
if "/operations/" in op_name and not resp.get("done"):
print(f" Polling engine config operation...")
op_url = (
f"https://{_ENGINE_CONFIG_LOCATION}-aiplatform.googleapis.com/v1beta1/{op_name}"
)
for _ in range(20):
time.sleep(3)
token = get_token()
r = subprocess.run(
["curl", "-s", "-H", f"Authorization: Bearer {token}", op_url],
capture_output=True, text=True,
)
op = json.loads(r.stdout)
if op.get("done"):
break
else:
print(" \u26a0\ufe0f Engine config operation timed out.")
# Verify actual state
cfg = get_engine_config()
db_cfg = cfg.get("ragManagedDbConfig", {})
print(f" Current ragManagedDbConfig: {json.dumps(db_cfg)}")
if "spanner" in db_cfg:
print(f"\n\u26a0\ufe0f WARN: Engine config in {_ENGINE_CONFIG_LOCATION} is still in Spanner mode.")
print(" RAG Engine appears to be restricted for this project.")
RAG_ENGINE_AVAILABLE = False
return
print(" \u2713 RAG Engine Config verified as basic tier.")
def create_corpus_rest(loc: str) -> str:
"""
Create corpus via REST API directly.
"""
url = (
f"https://{loc}-aiplatform.googleapis.com/v1beta1"
f"/projects/{PROJECT_ID}/locations/{loc}/ragCorpora"
)
# vectorDbConfig.ragManagedDb = serverless RAG Managed DB.
# This is the correct v1beta1 field name.
payload = json.dumps({
"displayName": CORPUS_DISPLAY_NAME,
"ragEmbeddingModelConfig": {
"vertexPredictionEndpoint": {
"model": "publishers/google/models/text-embedding-004"
}
},
"vectorDbConfig": {
"ragManagedDb": {}
}
})
token = get_token()
print(f" Sending POST to {url}...")
result = subprocess.run(
["curl", "-s", "-X", "POST",
"-H", f"Authorization: Bearer {token}",
"-H", "Content-Type: application/json",
url, "-d", payload],
capture_output=True, text=True,
)
try:
resp = json.loads(result.stdout)
except Exception:
raise RuntimeError(f"Failed to parse API response: {result.stdout}")
if "error" in resp:
# Pass the error object up so get_or_create_corpus can inspect it
raise RuntimeError(json.dumps(resp["error"]))
op_name = resp.get("name", "")
if "/operations/" not in op_name:
raise RuntimeError(f"Unexpected response (no operation): {resp}")
print(f" Polling corpus creation operation {op_name}...")
op_url = f"https://{loc}-aiplatform.googleapis.com/v1beta1/{op_name}"
for _ in range(40):
time.sleep(5)
token = get_token()
r = subprocess.run(
["curl", "-s", "-H", f"Authorization: Bearer {token}", op_url],
capture_output=True, text=True,
)
op = json.loads(r.stdout)
if op.get("done"):
if "error" in op:
raise RuntimeError(json.dumps(op["error"]))
corpus_name = op["response"]["name"]
return corpus_name
raise RuntimeError("Corpus creation operation timed out.")
def get_or_create_corpus() -> RagCorpus:
"""Return existing corpus by display name, or create a new serverless one with fallback."""
global LOCATION, RAG_ENGINE_AVAILABLE
if not RAG_ENGINE_AVAILABLE:
print("\n\u2139 RAG Engine appears to be in restricted Spanner mode for this project. Skipping corpus creation.")
return None
def _find_in_list():
try:
for c in rag.list_corpora():
if c.display_name == CORPUS_DISPLAY_NAME:
return c
except Exception:
pass
return None
# 1. Try finding in current LOCATION
vertexai.init(project=PROJECT_ID, location=LOCATION)
corpus = _find_in_list()
if corpus:
print(f"Corpus '{CORPUS_DISPLAY_NAME}' already exists in {LOCATION}: {corpus.name}")
return corpus
# 2. Try creating in current LOCATION
print(f"Creating corpus '{CORPUS_DISPLAY_NAME}' in {LOCATION} via REST...")
try:
corpus_name = create_corpus_rest(LOCATION)
print(f"\u2713 Corpus created in {LOCATION}: {corpus_name}")
except RuntimeError as e:
err_str = str(e)
# Check for Spanner restriction error message
if "using Spanner mode with RAG Engine in us-central1, us-east1, and us-east4 is restricted" in err_str:
if LOCATION != "europe-west4":
print(f"\u26a0\ufe0f Spanner restriction detected in {LOCATION}. Retrying in europe-west4...")
LOCATION = "europe-west4"
vertexai.init(project=PROJECT_ID, location=LOCATION)
# Check if it already exists in the fallback location
corpus = _find_in_list()
if corpus:
print(f"Corpus '{CORPUS_DISPLAY_NAME}' found in fallback {LOCATION}: {corpus.name}")
return corpus
try:
corpus_name = create_corpus_rest(LOCATION)
print(f"\u2713 Corpus created in fallback {LOCATION}: {corpus_name}")
except RuntimeError as e2:
if "using Spanner mode" in str(e2):
print("\n\u26a0\ufe0f RAG Engine restriction confirmed in fallback region. RAG is effectively unavailable.")
RAG_ENGINE_AVAILABLE = False
return None
raise
else:
print("\n\u26a0\ufe0f RAG Engine restriction confirmed in fallback region. RAG is effectively unavailable.")
RAG_ENGINE_AVAILABLE = False
return None
else:
raise RuntimeError(f"Failed to create corpus in {LOCATION}: {err_str}")
# Final retrieval of the corpus object
for c in rag.list_corpora():
if c.name == corpus_name:
return c
raise RuntimeError(f"Corpus created but not found in list: {corpus_name}")
def import_documents(corpus: RagCorpus) -> None:
print(f"Importing files from {GCS_SOURCE} into {corpus.name}...")
rag.import_files(
corpus.name,
paths=[GCS_SOURCE],
transformation_config=rag.TransformationConfig(
chunking_config=rag.ChunkingConfig(
chunk_size=512,
chunk_overlap=100,
)
),
)
print("\u2713 Import job submitted.")
def test_retrieval(corpus: RagCorpus) -> None:
print("Running test retrieval query...")
try:
response = rag.retrieval_query(
rag_resources=[rag.RagResource(rag_corpus=corpus.name)],
text="test query",
rag_retrieval_config=rag.RagRetrievalConfig(top_k=3),
)
print(f"\u2713 Test retrieval returned {len(response.contexts.contexts)} chunk(s).")
except Exception as e:
print(f" \u26a0\ufe0f Retrieval test failed (import might still be processing): {e}")
def main() -> None:
print(f"--- RAG Setup Starting (Target Location: {LOCATION}) ---")
ensure_serverless_engine_config()
corpus = get_or_create_corpus()
if corpus is None:
print("\n--- RAG AVAILABILITY SUMMARY ---")
print(f"Project : {PROJECT_ID}")
print("Status : UNAVAILABLE (Platform restriction: Spanner Mode only)")
print("Action : Skipping RAG integration. Downstream tools will operate without a corpus.")
print("---------------------------------")
sys.exit(0)
import_documents(corpus)
print(f"\nSUCCESS")
print(f"RAG_LOCATION={LOCATION}")
print(f"RAG_CORPUS={corpus.name}")
print("\nNext steps (set in your environment):")
print(f"export RAG_LOCATION={LOCATION}")
print(f"export RAG_CORPUS_NAME={corpus.name}")
print("\nWaiting 10s before test retrieval...")
time.sleep(10)
test_retrieval(corpus)
if __name__ == "__main__":
main()

132
agents/rag/sync_corpus.py Normal file
View File

@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
sync_corpus.py Synchronize RAG Engine corpus with GCS and perform health checks.
Project: propane-will-491900-m5
Capabilities:
- Locates existing corpus by display name.
- Lists current files in the corpus.
- Triggers incremental import from GCS.
- Performs a health-check retrieval query.
- Reports status and chunk counts.
Graceful degradation:
- If corpus is not found (e.g. due to Spanner restriction), logs a warning and exits cleanly.
"""
import os
import json
import time
import subprocess
import sys
import vertexai
from vertexai import rag
from vertexai.rag import RagCorpus
PROJECT_ID = "propane-will-491900-m5"
LOCATION = os.environ.get("RAG_LOCATION", "us-central1")
CORPUS_DISPLAY_NAME = os.environ.get("RAG_CORPUS_NAME", "osvauco-knowledge-base")
GCS_SOURCE = os.environ.get(
"RAG_GCS_SOURCE",
f"gs://{PROJECT_ID}-agent-staging/rag-docs/",
)
def get_token() -> str:
return subprocess.check_output(
["gcloud", "auth", "print-access-token"], text=True
).strip()
def find_corpus() -> RagCorpus:
"""Search for the corpus in the configured location and fallback location."""
locations_to_try = [LOCATION]
if LOCATION != "europe-west4":
locations_to_try.append("europe-west4")
for loc in locations_to_try:
print(f"Searching for corpus '{CORPUS_DISPLAY_NAME}' in {loc}...")
vertexai.init(project=PROJECT_ID, location=loc)
try:
for c in rag.list_corpora():
if c.display_name == CORPUS_DISPLAY_NAME:
print(f" \u2713 Found: {c.name}")
return c
except Exception as e:
print(f" \u26a0\ufe0f Notice: Could not list corpora in {loc}: {e}")
return None
def list_files(corpus: RagCorpus):
"""List files currently in the corpus."""
print(f"Listing files in corpus...")
files = list(rag.list_files(corpus_name=corpus.name))
if not files:
print(" (Corpus is empty)")
return []
for f in files:
print(f" - {f.display_name} (state: {f.state.name})")
return files
def sync_from_gcs(corpus: RagCorpus):
"""Trigger incremental import from GCS."""
print(f"Syncing from {GCS_SOURCE}...")
# import_files in SDK is essentially an incremental trigger
rag.import_files(
corpus.name,
paths=[GCS_SOURCE],
transformation_config=rag.TransformationConfig(
chunking_config=rag.ChunkingConfig(
chunk_size=512,
chunk_overlap=100,
)
),
)
print(" \u2713 Sync job submitted.")
def run_health_check(corpus: RagCorpus):
"""Perform a retrieval query and report results."""
print("Running health check retrieval...")
try:
response = rag.retrieval_query(
rag_resources=[rag.RagResource(rag_corpus=corpus.name)],
text="How does OSVauco handle operator experience?",
rag_retrieval_config=rag.RagRetrievalConfig(top_k=3),
)
contexts = response.contexts.contexts
print(f" \u2713 Health check returned {len(contexts)} contexts.")
for i, ctx in enumerate(contexts):
print(f" [{i+1}] Source: {ctx.source_uri} (Score: {ctx.score:.4f})")
except Exception as e:
print(f" \u2718 Health check failed: {e}")
def main():
print(f"--- RAG Sync & Maintenance Tool ---")
corpus = find_corpus()
if not corpus:
print(f"\n\u26a0\ufe0f WARN: Corpus '{CORPUS_DISPLAY_NAME}' not found.")
print(" RAG Engine might be restricted for this project or setup_corpus.py has not been run.")
print(" Skipping sync and health check.")
sys.exit(0)
# Update global LOCATION if we found it in a different one
found_loc = corpus.name.split('/')[3]
print(f"Active Location: {found_loc}")
list_files(corpus)
# Always trigger a sync to ensure new files are picked up
sync_from_gcs(corpus)
print("\nWaiting 5s for sync initialization...")
time.sleep(5)
run_health_check(corpus)
print(f"\nCOMPLETED")
print(f"RAG_LOCATION={found_loc}")
print(f"RAG_CORPUS={corpus.name}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,86 @@
from google.cloud import bigquery
from datetime import datetime, timezone
PROJECT_ID = "propane-will-491900-m5"
DATASET = "costguard_dataset"
BILLING_TABLE = f"{PROJECT_ID}.{DATASET}.gcp_billing_export"
def get_recommendations(budget_nok: float = 500.0) -> dict:
client = bigquery.Client(project=PROJECT_ID)
recommendations = []
# 1. Anomali: tjenester der siste 3d-snitt >= 2x 30d-snitt
anomaly_query = f"""
WITH base AS (
SELECT service.description AS service,
AVG(cost) AS avg_30d
FROM `{BILLING_TABLE}`
WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY service
),
recent AS (
SELECT service.description AS service,
AVG(cost) AS avg_3d
FROM `{BILLING_TABLE}`
WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)
GROUP BY service
)
SELECT r.service, r.avg_3d, b.avg_30d,
SAFE_DIVIDE(r.avg_3d, b.avg_30d) AS ratio
FROM recent r JOIN base b ON r.service = b.service
WHERE SAFE_DIVIDE(r.avg_3d, b.avg_30d) >= 2.0
AND b.avg_30d > 0.001
ORDER BY ratio DESC
"""
# 2. Sløsing: tjenester med kostnad men 0 aktivitet siste 7d
waste_query = f"""
WITH active AS (
SELECT DISTINCT service.description AS service
FROM `{BILLING_TABLE}`
WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
AND cost > 0
),
all_svc AS (
SELECT DISTINCT service.description AS service
FROM `{BILLING_TABLE}`
WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
AND cost > 0
)
SELECT a.service FROM all_svc a
LEFT JOIN active b ON a.service = b.service
WHERE b.service IS NULL
"""
try:
# Anomalier
for row in client.query(anomaly_query).result():
ratio = row.ratio or 0
severity = "high" if ratio >= 3 else "medium"
savings = round(float(row.avg_3d - row.avg_30d) * 30 * 10, 2) # rough NOK
recommendations.append({
"type": "anomaly",
"severity": severity,
"service": row.service,
"message": f"Unormal kostnadsøkning på {row.service}{ratio:.1f}x over normalt",
"estimated_savings_nok": max(savings, 0)
})
# Sløsing
for row in client.query(waste_query).result():
recommendations.append({
"type": "waste",
"severity": "low",
"service": row.service,
"message": f"{row.service} har hatt kostnad men ingen aktivitet siste 7 dager — vurder å skru av",
"estimated_savings_nok": 0
})
except Exception as e:
return {"recommendations": [], "total_estimated_savings_nok": 0, "error": str(e)}
total_savings = sum(r["estimated_savings_nok"] for r in recommendations)
return {
"recommendations": recommendations,
"total_estimated_savings_nok": round(total_savings, 2)
}

View File

@ -0,0 +1,13 @@
#!/bin/bash
# test_local.sh — Run ADK agent locally before deploying to cloud
set -euo pipefail
echo "=== OSVauco-NMTMD-GCOS :: Local Agent Test ==="
gcloud auth application-default print-access-token >/dev/null 2>&1 || {
echo "ERROR: No ADC. Run: gcloud auth application-default login"; exit 1; }
echo "Starting ADK dev server at http://localhost:8080 ..."
echo "Press Ctrl+C to stop."
adk web agents/core-logic/

View File

@ -0,0 +1,108 @@
# agents/tests/unit/test_agent.py
# OSVauco-NMTMD-GCOS — Unit tests for ADK orchestrator agent
# Run: pytest agents/tests/unit/ -v
import os
import sys
import pytest
# ── path setup ───────────────────────────────────────────────
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "core-logic"))
# ─────────────────────────────────────────────────────────────
# Test 1: Python version
# ─────────────────────────────────────────────────────────────
def test_python_version():
"""Require Python 3.11 or higher."""
assert sys.version_info >= (3, 11), (
f"Python 3.11+ required, got {sys.version_info.major}.{sys.version_info.minor}"
)
# ─────────────────────────────────────────────────────────────
# Test 2: ADK import
# ─────────────────────────────────────────────────────────────
def test_adk_import():
"""google-adk must be importable."""
try:
import google.adk # noqa: F401
except ImportError as e:
pytest.fail(f"google-adk not importable: {e}")
# ─────────────────────────────────────────────────────────────
# Test 3: Agent import
# ─────────────────────────────────────────────────────────────
def test_agent_import():
"""agents/core-logic/agent.py must export root_agent."""
try:
from agent import root_agent # noqa: F401
except ImportError as e:
pytest.skip(f"Skipping — dependencies not installed: {e}")
except Exception as e:
pytest.fail(f"Unexpected error importing root_agent: {e}")
# ─────────────────────────────────────────────────────────────
# Test 4: Required env vars present
# ─────────────────────────────────────────────────────────────
REQUIRED_ENV_VARS = [
"PROJECT_ID",
"REGION",
"ORCHESTRATOR_MODEL",
"SUBAGENT_MODEL",
]
@pytest.mark.parametrize("var", REQUIRED_ENV_VARS)
def test_required_env_var(var):
"""All required env vars must be set (non-empty)."""
val = os.environ.get(var, "")
assert val, f"Required env var '{var}' is not set or empty"
# ─────────────────────────────────────────────────────────────
# Test 5: RAG env vars present if RAG enabled
# ─────────────────────────────────────────────────────────────
def test_rag_env_vars():
"""If RAG_CORPUS_NAME is set, related vars must also be set."""
corpus = os.environ.get("RAG_CORPUS_NAME", "")
if not corpus:
pytest.skip("RAG_CORPUS_NAME not set — skipping RAG env var check")
assert os.environ.get("RAG_CORPUS_DISPLAY_NAME"), "RAG_CORPUS_DISPLAY_NAME must be set"
# ─────────────────────────────────────────────────────────────
# Test 6: Memory Bank env vars present if enabled
# ─────────────────────────────────────────────────────────────
def test_memory_bank_env_vars():
"""If MEMORY_BANK_INSTANCE is set, related vars must also be set."""
mb = os.environ.get("MEMORY_BANK_INSTANCE", "")
if not mb:
pytest.skip("MEMORY_BANK_INSTANCE not set — skipping Memory Bank env var check")
assert os.environ.get("MEMORY_INSTANCE_DISPLAY_NAME"), "MEMORY_INSTANCE_DISPLAY_NAME must be set"
# ─────────────────────────────────────────────────────────────
# Test 7: Safety callback smoke test
# ─────────────────────────────────────────────────────────────
def test_safety_callback_import():
"""before_model_callback must be importable from agent module."""
try:
from agent import before_model_callback # noqa: F401
except ImportError as e:
pytest.skip(f"Skipping — dependencies not installed: {e}")
except Exception as e:
pytest.fail(f"Unexpected error importing before_model_callback: {e}")
# ─────────────────────────────────────────────────────────────
# Test 8: cloudbuild.yaml exists
# ─────────────────────────────────────────────────────────────
def test_cloudbuild_yaml_exists():
"""cloudbuild.yaml must exist at repo root."""
repo_root = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "..")
)
cb_path = os.path.join(repo_root, "cloudbuild.yaml")
assert os.path.isfile(cb_path), f"cloudbuild.yaml not found at {cb_path}"

78
agents/tools/mcp_tools.py Normal file
View File

@ -0,0 +1,78 @@
"""
mcp_tools.py MCP toolset integrations for OSVauco ADK agent.
Supports:
- Google BigQuery MCP (via StreamableHTTP, uses ADC/OAuth)
- Google Maps MCP (via StreamableHTTP, uses API key)
- OPAX-MCP (Vauco intern CI/CD-kanal, FunctionTools over REST)
- Local stdio MCP server (dev/test only)
Requires: google-adk >= 2.0.0, google-auth, httpx
Auth-mønster for opax-mcp:
Cloud Run IAM : Authorization: Bearer <identity-token> (lag 1)
Tool-level : X-MCP-Secret: <mcp-server-key> (lag 2)
"""
import os
import google.auth
import google.auth.transport.requests
from google.adk.tools.mcp_tool.mcp_toolset import (
MCPToolset,
StdioServerParameters,
StreamableHTTPConnectionParams,
)
PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
OPAX_MCP_URL = os.environ.get("MCP_SERVER_URL", "https://opax-mcp-zjbqp3prqq-uc.a.run.app")
# --- BigQuery MCP (Google managed, OAuth) ---
BIGQUERY_MCP_URL = f"https://bigquery.googleapis.com/mcp/projects/{PROJECT_ID}"
def get_bigquery_mcp_toolset() -> MCPToolset:
"""Connect to Google's managed BigQuery MCP server using ADC (no key file)."""
credentials, _ = google.auth.default(
scopes=["https://www.googleapis.com/auth/bigquery"]
)
credentials.refresh(google.auth.transport.requests.Request())
return MCPToolset(
connection_params=StreamableHTTPConnectionParams(
url=BIGQUERY_MCP_URL,
headers={
"Authorization": f"Bearer {credentials.token}",
"x-goog-user-project": PROJECT_ID,
},
)
)
# --- Google Maps MCP (API key from env/Secret Manager) ---
MAPS_MCP_URL = "https://maps.googleapis.com/mcp"
def get_maps_mcp_toolset() -> MCPToolset:
"""Connect to Google's managed Maps MCP server using an API key."""
maps_api_key = os.environ.get("MAPS_API_KEY", "")
if not maps_api_key:
raise ValueError("MAPS_API_KEY env var not set. Add to Secret Manager and load at startup.")
return MCPToolset(
connection_params=StreamableHTTPConnectionParams(
url=MAPS_MCP_URL,
headers={"X-Goog-Api-Key": maps_api_key},
)
)
# --- OPAX-MCP tools (importert frå opax_mcp_client) ---
def get_opax_tools() -> list:
"""Returner alle 25 opax-mcp tools som ADK FunctionTool-liste."""
from agents.tools.opax_mcp_client import get_all_function_tools
return get_all_function_tools()
# --- Local stdio MCP server (dev/test only) ---
def get_local_stdio_toolset(command: str, args: list[str]) -> MCPToolset:
"""Connect to a local stdio MCP server for development and testing."""
return MCPToolset(
connection_params=StdioServerParameters(
command=command,
args=args,
)
)

View File

@ -0,0 +1,234 @@
"""
opax_mcp_client.py REST-klient for opax-mcp Cloud Run service.
Bruk denne for å kalle alle 25 tools frå Jason/Emma eller anna Python-kode.
Auth-lag:
1. Cloud Run IAM : Authorization: Bearer <identity-token> (automatisk)
2. Tool-level : X-MCP-Secret: <mcp-server-key> (frå Secret Manager)
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:
"""Hent Cloud Run identity token (GCE metadata) eller ADC access token (lokal dev)."""
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:
"""Kall eit opax-mcp tool. Returnerer {tool, result}."""
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()
def list_tools() -> list:
"""List alle tilgjengelige tools i opax-mcp."""
resp = httpx.get(f"{OPAX_MCP_URL}/tools", headers=_headers(), timeout=10)
resp.raise_for_status()
return resp.json().get("tools", [])
# ---------------------------------------------------------------------------
# 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."""
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."""
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, currency: str = "USD") -> dict:
"""Sett nytt budsjett. amount er beløp i angitt valuta."""
return call_tool("set_billing_budget", {"amount": amount, "currency": currency})
# ---------------------------------------------------------------------------
# Onboarding
# ---------------------------------------------------------------------------
def create_invite(email: str, name: str = "", tier: str = "starter") -> dict:
"""Opprett invite for ny bruker. tier: starter | pro | enterprise."""
return call_tool("create_invite", {"email": email, "name": name, "tier": tier})
def list_customers() -> dict:
"""List alle onboardede kunder."""
return call_tool("list_customers")
# ---------------------------------------------------------------------------
# Notify
# ---------------------------------------------------------------------------
def send_webhook(message: str, url: str = "") -> dict:
"""Send webhook-varsling. url er valgfri override."""
return call_tool("send_webhook", {"message": message, "url": url})
def send_email(to: str, subject: str, body: str) -> dict:
"""Send e-post via OPAX notify-modul."""
return call_tool("send_email", {"to": to, "subject": subject, "body": body})
def send_sms(to: str, message: str) -> dict:
"""Send SMS via OPAX notify-modul."""
return call_tool("send_sms", {"to": to, "message": message})
def get_notify_channels() -> dict:
"""List konfigurerte varslingskanalar."""
return call_tool("get_notify_channels")
# ---------------------------------------------------------------------------
# Agents
# ---------------------------------------------------------------------------
def run_jason(prompt: str) -> dict:
"""Kall Jason-agenten med ein prompt via OPAX."""
return call_tool("run_jason", {"prompt": prompt})
def run_emma(prompt: str) -> dict:
"""Kall Emma-agenten med ein prompt via OPAX."""
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 for OSVauco."""
return call_tool("get_state")
def get_telemetry() -> dict:
"""Hent token-forbruk per modul."""
return call_tool("get_telemetry")
def run_terminal(command: str) -> dict:
"""Kjør kommando i OPAX terminal-exec. Krever HITL-godkjenning."""
return call_tool("run_terminal", {"command": command})
# ---------------------------------------------------------------------------
# GitHub
# ---------------------------------------------------------------------------
def list_commits(repo: str = "", limit: int = 10) -> dict:
"""List siste commits i repoet."""
return call_tool("list_commits", {"repo": repo, "limit": limit})
def get_file(path: str, repo: str = "") -> dict:
"""Hent innhald i ein fil frå GitHub."""
return call_tool("get_file", {"path": path, "repo": repo})
def list_open_issues(repo: str = "") -> dict:
"""List alle opne issues i repoet."""
return call_tool("list_open_issues", {"repo": repo})
def create_github_issue(title: str, body: str = "", repo: str = "") -> dict:
"""Opprett nytt GitHub issue."""
return call_tool("create_github_issue", {"title": title, "body": body, "repo": repo})
def push_file(path: str, content: str, message: str = "", sha: str = "", repo: str = "") -> dict:
"""Push/oppdater ein fil i GitHub. sha krevst ved oppdatering av eksisterande fil."""
return call_tool("push_file", {"path": path, "content": content, "message": message, "sha": sha, "repo": repo})
# ---------------------------------------------------------------------------
# ADK FunctionTools — for bruk i agent.py
# ---------------------------------------------------------------------------
def get_all_function_tools() -> list:
"""Returner alle 25 tools som ADK FunctionTool-liste for Jason/Emma."""
from google.adk.tools import FunctionTool
return [
# Billing
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),
# Onboarding
FunctionTool(func=create_invite),
FunctionTool(func=list_customers),
# Notify
FunctionTool(func=send_webhook),
FunctionTool(func=send_email),
FunctionTool(func=send_sms),
FunctionTool(func=get_notify_channels),
# Agents
FunctionTool(func=run_jason),
FunctionTool(func=run_emma),
# Platform
FunctionTool(func=get_health),
FunctionTool(func=get_build_status),
FunctionTool(func=get_state),
FunctionTool(func=get_telemetry),
FunctionTool(func=run_terminal),
# GitHub
FunctionTool(func=list_commits),
FunctionTool(func=get_file),
FunctionTool(func=list_open_issues),
FunctionTool(func=create_github_issue),
FunctionTool(func=push_file),
]

View File

@ -0,0 +1,112 @@
# Agent Workflow Diagrams
## 1. Main deploy/run lifecycle
```mermaid
flowchart TD
A[Local VS Code / Cloud Shell] -->|01-setupenv.sh| B[APIs Enabled + Bucket + SA]
B -->|02-deploy.sh| C[ADK Python SDK]
C -->|gcloud auth ADC| D[Gemini Enterprise Agent Platform]
D -->|Agent Runtime deployed| E[Managed Serverless Runtime]
E -->|Query via API| F[Agent Response]
F -->|End of day| G[03-teardown.sh]
G --> H[Agent Runtime Deleted — Billing Stopped]
style G fill:#c0392b,color:#fff
style H fill:#27ae60,color:#fff
```
## 2. Multi-agent / Coordinator delegation flow
```mermaid
flowchart TD
U[User Input] --> O[Orchestrator LlmAgent]
O -->|Knowledge question| R[rag_agent]
O -->|GCP ops question| G[gcp_ops_agent]
O -->|Memory recall/store| M[memory_agent]
O -->|Session close| F[farewell_agent]
R --> V[Vertex AI RAG Engine]
M --> MB[Memory Bank / Agent Engine]
O -->|Final answer| U
```
## 3. Agents CLI lifecycle (2026)
```mermaid
flowchart LR
A[Developer] -->|natural language| B[Agents CLI skills]
B -->|agents-cli create| C[Scaffolded project]
C -->|agents-cli run| D[Local test]
D -->|agents-cli deploy| E{Deploy target}
E -->|cloud_run| F[Cloud Run service]
E -->|agent_runtime| G[Agent Runtime — managed]
E -->|gke| H[GKE cluster]
F & G & H --> J[Cloud Trace + Logging + Monitoring]
```
## 4. Observability stack
```mermaid
flowchart TD
A[ADK Agent — deployed] -->|OpenTelemetry| B[Cloud Trace]
A -->|structured logs| C[Cloud Logging]
A -->|metrics| D[Cloud Monitoring]
D -->|alert policy| E[Email / PubSub]
B --> F[Agent Platform: Traces tab]
C --> G[Agent Platform: Logs tab]
D --> H[Dashboards: sessions, latency p50/p95/p99, error rates]
```
## 5. RAG-grounded agent data flow
```mermaid
flowchart LR
A[User query] --> B[ADK Agent]
B -->|retrieve| C[Vertex AI RAG Engine]
C -->|search corpus| D[Vector index]
D -->|relevant chunks| C
C -->|grounded context| B
B -->|generate| E[Gemini model]
E -->|grounded response| F[User]
B -->|telemetry| G[Cloud Trace]
```
## 6. CI/CD pipeline
```mermaid
flowchart TD
P[git push to main] --> T[Cloud Build Trigger]
T --> B[Build Docker image]
B --> AR[Push to Artifact Registry]
AR --> EV[run_eval.py — CI gate]
EV -->|groundedness >= 0.8| CR[Deploy to Cloud Run]
EV -->|groundedness < 0.8| FAIL[Build FAILED]
CR --> LIVE[Live service]
```
## 7. Memory Bank session lifecycle
```
Session Start
└── Load memories (recall by user_id)
Agent conversation (session.state updates)
Session End
└── Generate memories (summarize session)
└── Store in Memory Bank (TTL: 30 days default)
Next Session
└── Memories retrieved at start
```
## 8. A2A agent communication
```
Orchestrator Agent (Cloud Run Service A)
│ HTTP POST /tasks (A2A protocol)
│ Authorization: Bearer <SA token>
Sub-Agent B (Cloud Run Service B)
│ AgentCard: /.well-known/agent.json
Response (A2A TaskResult)
└── Orchestrator aggregates + responds to user
```

View File

@ -0,0 +1,103 @@
# Data Flow Security — propane-will-491900-m5
## Authentication flow
```
Local VS Code ──[ADC: gcloud auth application-default login]──► Google Cloud APIs
Cloud Run ──[attached SA via metadata server]─────────────► Google Cloud APIs
GKE pod ──[Workload Identity Federation]────────────────► Google Cloud APIs
```
## No-key-file policy
- Service account JSON keys MUST NOT be committed to Git.
- `.gitignore` must always include: `*.json`, `credentials/`, `*.key`, `.env`
- Use `gcloud secrets create` (Secret Manager) for all sensitive values.
## Secret Manager pattern
```bash
# Store a secret
echo -n "MY_SECRET_VALUE" | gcloud secrets create my-secret \
--data-file=- --project=propane-will-491900-m5
# Grant access to service account
gcloud secrets add-iam-policy-binding my-secret \
--member="serviceAccount:vertex-agent-sa@propane-will-491900-m5.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor" \
--project=propane-will-491900-m5
```
```python
from google.cloud import secretmanager
client = secretmanager.SecretManagerServiceClient()
name = "projects/propane-will-491900-m5/secrets/my-secret/versions/latest"
response = client.access_secret_version(request={"name": name})
value = response.payload.data.decode("UTF-8")
```
## Data classification
| Data Type | Classification | Handling |
|---|---|---|
| User queries | Confidential | In-memory only; not logged by default |
| RAG corpus documents | Internal | GCS, encrypted at rest |
| Agent memories | Confidential | Memory Bank, encrypted at rest |
| API keys / secrets | Secret | Secret Manager only; never in env vars |
| Container images | Internal | Artifact Registry, private |
| Audit logs | Internal | Cloud Logging, 30-day retention |
## Input guardrails (ADK callbacks)
```python
def before_model_callback(callback_context, llm_request):
blocked = ["drop table", "ignore previous instructions", "jailbreak"]
user_text = llm_request.contents[-1].parts[0].text.lower()
for pattern in blocked:
if pattern in user_text:
from google.genai.types import Content, Part
return Content(parts=[Part(text="I cannot process that request.")])
return None
def before_tool_callback(tool, args, tool_context):
if tool.name == "execute_query":
if "DROP" in args.get("query", "").upper():
raise ValueError("Destructive queries are not permitted.")
return None
```
## Agent Gateway + Model Armor architecture
```
Client (Gemini CLI / Claude Code / browser)
Agent Gateway ← enforces IAM + Semantic Governance policies
← Model Armor: blocks prompt injection, data leakage
Agent Runtime / Cloud Run (ADK agent)
├──► Google Cloud APIs — via SA with least-privilege IAM
└──► MCP Servers — requires roles/mcp.toolUser
```
## Network security
```bash
# Cloud Run: no unauthenticated access
gcloud run services update oavauco-agent-v1 \
--no-allow-unauthenticated --region=us-central1
# VPC connector for private Vertex AI access
gcloud compute networks vpc-access connectors create agent-connector \
--network=default --region=us-central1 --range=10.8.0.0/28
gcloud run services update oavauco-agent-v1 \
--vpc-connector=agent-connector \
--vpc-egress=private-ranges-only --region=us-central1
```
## Security feature matrix
| Feature | Purpose | Status |
|---|---|---|
| Agent Identity | Per-agent SA, cryptographic ID | GA |
| Agent Registry | Central catalog of deployed agents | GA |
| Agent Gateway | API gateway, IAM + policy enforcement | GA |
| Model Armor | Prompt injection / data leakage blocking | GA |
| A2A Zero-Trust | Authenticated agent-to-agent comms | GA |
| DLP integration | PII detection in agent I/O | Available |
| Audit Logging | All agent actions logged to Cloud Logging | Always-on |

1
auth/__init__.py Normal file
View File

@ -0,0 +1 @@
# auth package — OAuth2 client flow for OSVauco

91
auth/oauth_flow.py Normal file
View File

@ -0,0 +1,91 @@
"""
auth/oauth_flow.py Google OAuth2 flow for klient-onboarding.
Klienten trykker /auth/login?client_id=X
redirect til Google OAuth2 consent screen
Google redirecter til /auth/callback?code=...&state=...
token lagres i Secret Manager under klient-ID
klienten redirectes til /static/billing-dashboard.html
Scopes (read-only):
- bigquery.readonly
- cloud-billing.readonly
Krever env-vars:
OAUTH_CLIENT_ID fra GCP OAuth2 credentials
OAUTH_CLIENT_SECRET fra GCP OAuth2 credentials
OAUTH_REDIRECT_URI f.eks. https://osvauco-agent-....run.app/auth/callback
PROJECT_ID GCP project for Secret Manager
"""
import os
import json
import secrets
from google_auth_oauthlib.flow import Flow
SCOPES = [
"https://www.googleapis.com/auth/bigquery.readonly",
"https://www.googleapis.com/auth/cloud-billing.readonly",
"openid",
"https://www.googleapis.com/auth/userinfo.email",
]
_CLIENT_CONFIG = {
"web": {
"client_id": os.environ.get("OAUTH_CLIENT_ID", ""),
"client_secret": os.environ.get("OAUTH_CLIENT_SECRET", ""),
"redirect_uris": [os.environ.get("OAUTH_REDIRECT_URI", "http://localhost:8080/auth/callback")],
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
}
}
# In-memory state store (nonce → client_id). For prod: bytt til Firestore/Redis.
_STATE_STORE: dict[str, str] = {}
def get_authorization_url(client_id: str) -> str:
"""
Genererer Google OAuth2 autoriseringsURL for gitt client_id.
Returnerer URL klienten skal redirectes til.
"""
flow = Flow.from_client_config(_CLIENT_CONFIG, scopes=SCOPES)
flow.redirect_uri = os.environ.get(
"OAUTH_REDIRECT_URI", "http://localhost:8080/auth/callback"
)
state = secrets.token_urlsafe(32)
_STATE_STORE[state] = client_id
auth_url, _ = flow.authorization_url(
access_type="offline",
include_granted_scopes="true",
state=state,
prompt="consent",
)
return auth_url
def exchange_code_for_token(code: str, state: str) -> tuple[str, dict]:
"""
Bytter OAuth2 code mot token.
Returnerer (client_id, token_dict).
Kaster ValueError hvis state er ukjent.
"""
client_id = _STATE_STORE.pop(state, None)
if client_id is None:
raise ValueError(f"Ukjent OAuth2 state: {state}")
flow = Flow.from_client_config(_CLIENT_CONFIG, scopes=SCOPES, state=state)
flow.redirect_uri = os.environ.get(
"OAUTH_REDIRECT_URI", "http://localhost:8080/auth/callback"
)
flow.fetch_token(code=code)
creds = flow.credentials
token_dict = {
"token": creds.token,
"refresh_token": creds.refresh_token,
"token_uri": creds.token_uri,
"client_id": creds.client_id,
"client_secret": creds.client_secret,
"scopes": list(creds.scopes or []),
}
return client_id, token_dict

76
auth/token_store.py Normal file
View File

@ -0,0 +1,76 @@
"""
auth/token_store.py Lagre og hente OAuth2-tokens per klient i Secret Manager.
Hvert token lagres som en JSON-streng under secret-navn:
osvauco-client-token-{client_id}
Bruk:
from auth.token_store import save_token, load_credentials
save_token("klient-abc", token_dict)
creds = load_credentials("klient-abc") # google.oauth2.credentials.Credentials
"""
import json
import os
from google.cloud import secretmanager
from google.oauth2.credentials import Credentials
PROJECT_ID = os.environ.get("PROJECT_ID", "propane-will-491900-m5")
def _secret_name(client_id: str) -> str:
return f"osvauco-client-token-{client_id}"
def save_token(client_id: str, token_dict: dict) -> None:
"""
Lagrer eller oppdaterer OAuth2-token for klient i Secret Manager.
Oppretter secret hvis den ikke finnes.
"""
client = secretmanager.SecretManagerServiceClient()
parent = f"projects/{PROJECT_ID}"
secret_id = _secret_name(client_id)
secret_path = f"{parent}/secrets/{secret_id}"
payload = json.dumps(token_dict).encode("utf-8")
# Opprett secret hvis den ikke finnes
try:
client.get_secret(name=secret_path)
except Exception:
client.create_secret(
request={
"parent": parent,
"secret_id": secret_id,
"secret": {"replication": {"automatic": {}}},
}
)
# Legg til ny versjon (ny verdi)
client.add_secret_version(
request={"parent": secret_path, "payload": {"data": payload}}
)
def load_credentials(client_id: str) -> Credentials:
"""
Henter siste token for klient fra Secret Manager.
Returnerer google.oauth2.credentials.Credentials klar til bruk.
Kaster KeyError hvis client_id ikke finnes.
"""
sm_client = secretmanager.SecretManagerServiceClient()
secret_path = f"projects/{PROJECT_ID}/secrets/{_secret_name(client_id)}/versions/latest"
try:
response = sm_client.access_secret_version(name=secret_path)
except Exception as exc:
raise KeyError(f"Ingen token funnet for klient '{client_id}': {exc}") from exc
token_dict = json.loads(response.payload.data.decode("utf-8"))
return Credentials(
token=token_dict.get("token"),
refresh_token=token_dict.get("refresh_token"),
token_uri=token_dict.get("token_uri", "https://oauth2.googleapis.com/token"),
client_id=token_dict.get("client_id"),
client_secret=token_dict.get("client_secret"),
scopes=token_dict.get("scopes"),
)

103
ci/deploy-webhook-listener.py Executable file
View File

@ -0,0 +1,103 @@
#!/usr/bin/env python3
import hashlib
import hmac
import json
import os
import subprocess
from http.server import BaseHTTPRequestHandler, HTTPServer
PORT = int(os.getenv("WEBHOOK_PORT", "9999"))
WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "")
REPO_PATH = os.getenv("REPO_PATH", "/home/chris_christiansen/OSVauco")
CI_BRANCH = os.getenv("CI_BRANCH", "feat/osvx-mcp-full-catalog")
DEPLOY_SCRIPT = os.getenv("DEPLOY_SCRIPT", "./deploy-mcp.sh")
DEPLOY_ENV = os.getenv("DEPLOY_ENV", "staging")
def verify_signature(payload_bytes, signature_header):
if not signature_header or not signature_header.startswith("sha256="):
return False
expected_sig = signature_header.split("=", 1)[1]
computed = hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
payload_bytes,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(computed, expected_sig)
class WebhookHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
print(f"[webhook] {args[0]}", flush=True)
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
payload_bytes = self.rfile.read(content_length)
signature = self.headers.get("X-Gitea-Delivery", "")
if WEBHOOK_SECRET and not verify_signature(payload_bytes, signature):
self.send_response(401)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"error": "invalid signature"}).encode())
return
try:
payload = json.loads(payload_bytes.decode("utf-8"))
except Exception:
self.send_response(400)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"error": "invalid json"}).encode())
return
ref = payload.get("ref", "")
after = payload.get("after", "")
if after == "0000000000000000000000000000000000000000":
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"status": "ignored deletion"}).encode())
return
expected_ref = f"refs/heads/{CI_BRANCH}"
if ref != expected_ref:
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(
json.dumps({"status": "ignored", "reason": "wrong branch", "ref": ref}).encode()
)
return
self.send_response(202)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(
json.dumps({
"status": "deploying",
"branch": CI_BRANCH,
"env": DEPLOY_ENV,
}).encode()
)
try:
subprocess.run(["git", "-C", REPO_PATH, "pull"], check=True, capture_output=True, text=True)
subprocess.run([DEPLOY_SCRIPT, DEPLOY_ENV], cwd=REPO_PATH, check=True)
except Exception as e:
print(f"[webhook] deploy failed: {e}", flush=True)
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"status": "ok", "service": "osvx-mcp-webhook"}).encode())
def main():
if not WEBHOOK_SECRET:
print("WARNING: WEBHOOK_SECRET not set; webhook signature verification disabled.", flush=True)
server = HTTPServer(("0.0.0.0", PORT), WebhookHandler)
print(f"[webhook] listening on port {PORT}", flush=True)
server.serve_forever()
if __name__ == "__main__":
main()

33
cloudbuild.base.yaml Normal file
View File

@ -0,0 +1,33 @@
# NOTE: This is a base/reference template — not the active pipeline.
# Active pipeline: cloudbuild.yaml (Cloud Build 2nd gen trigger)
# Do not add new steps here; update cloudbuild.yaml directly.
# cloudbuild.base.yaml — Bygg og push base-image manuelt
# Kjøres kun når tunge avhengigheter i requirements.txt endres:
#
# gcloud builds submit --config=cloudbuild.base.yaml .
#
steps:
- name: 'gcr.io/cloud-builders/docker'
id: build-base
args:
- 'build'
- '-f'
- 'agents/core-logic/Dockerfile.base'
- '-t'
- 'us-central1-docker.pkg.dev/${PROJECT_ID}/osvauco-repo/osvauco-base:latest'
- 'agents/core-logic'
- name: 'gcr.io/cloud-builders/docker'
id: push-base
waitFor: ['build-base']
args:
- 'push'
- 'us-central1-docker.pkg.dev/${PROJECT_ID}/osvauco-repo/osvauco-base:latest'
options:
logging: CLOUD_LOGGING_ONLY
machineType: E2_HIGHCPU_8
images:
- 'us-central1-docker.pkg.dev/${PROJECT_ID}/osvauco-repo/osvauco-base:latest'

34
cloudbuild.deploy.yaml Normal file
View File

@ -0,0 +1,34 @@
# cloudbuild.deploy.yaml - Bygger med unik tag og deployer opax-mcp deklarativt
steps:
# Steg 1: Bygg og tag bildet med unik $BUILD_ID
- name: 'gcr.io/cloud-builders/docker'
id: build
args:
- 'build'
- '-f'
- 'opax-mcp/Dockerfile'
- '-t'
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_ARTIFACT_REPO}/opax-mcp:$BUILD_ID'
- 'opax-mcp'
# Steg 2: Push det unike imaget til Artifact Registry
- name: 'gcr.io/cloud-builders/docker'
id: push
args: ['push', '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_ARTIFACT_REPO}/opax-mcp:$BUILD_ID']
waitFor: ['build']
# Steg 3: Deploy det nybygde imaget direkte til Cloud Run (Endelig Fiks)
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
id: deploy
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- 'opax-mcp'
- '--image=${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_ARTIFACT_REPO}/opax-mcp:$BUILD_ID'
- '--region=${_REGION}'
waitFor: ['push']
substitutions:
_REGION: us-central1
_ARTIFACT_REPO: osvauco-repo

35
cloudbuild.yaml Normal file
View File

@ -0,0 +1,35 @@
steps:
# Step 1: Run static analysis and linting on Python tool scripts.
- name: 'python:3.9-slim'
id: 'StaticAnalysis'
entrypoint: 'bash'
args:
- '-c'
- |
pip install flake8
echo "--- Running static analysis on TYR tools ---"
flake8 --ignore=E501,W291,W293 tyr/tools/
# Step 2: Run scan_tyr_surface.py to check GCP environment compliance.
# NOTE: This step requires a custom build image with gcloud, python, the required
# python libraries, and spire binaries installed. It also needs appropriate permissions.
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' # Placeholder image
id: 'SurfaceScan'
entrypoint: 'bash'
args:
- '-c'
- |
python3 -m venv /tmp/venv
source /tmp/venv/bin/activate
pip install google-cloud-secret-manager google-cloud-bigquery
python3 tyr/tools/scan_tyr_surface.py
# Step 3: Run attest_tyr_supply_chain.py against a built container image.
# NOTE: This step requires a custom build image with cosign. It would run after
# a container image is built and pushed in a real pipeline.
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' # Placeholder image
id: 'SupplyChainAttestation'
entrypoint: 'python3'
args:
- 'tyr/tools/attest_tyr_supply_chain.py'
- '--image_uri=${_IMAGE_URI}' # Assumes image URI is passed as a substitution

4
cosign.pub Normal file
View File

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk7BUor5+RImB5EiI7imet3Xwm4vj
j0Fg0sXKY8icZfinWr3hwyGwSGmd/vq22OLjIhlhChTp10xXzKIVZyzQAA==
-----END PUBLIC KEY-----

40
data/threadstone_faq.csv Normal file
View File

@ -0,0 +1,40 @@
question,answer
"Hva er Threadstone?","Threadstone er en avansert AI-arbeidsflate utviklet av det norske selskapet Vauco AS. Det er et SaaS-produkt designet for å være en intelligent og sikker assistent som tilpasser seg din unike kontekst og arbeidsflyt, i motsetning til generiske AI-verktøy."
"Hvem er Threadstone for?","Threadstone er for kreative fagpersoner, byråer og bedrifter som trenger et kraftig og sikkert AI-verktøy. Det finnes en Consumer-versjon for enkeltbrukere og en Enterprise-versjon for team og organisasjoner, inkludert den spesialiserte MEDIOTEQ-løsningen for helsevesenet."
"Hva er forskjellen fra vanlige AI-verktøy som ChatGPT?","Threadstone skiller seg ut ved sin stateless arkitektur som sikrer at ingen av dine data lagres på våre servere, noe som gir fullt personvern. I tillegg kan du tilpasse assistenten med egne instruksjoner for mer relevante resultater."
"Hvilke modeller brukes?","Threadstone bruker gemini-2.5-flash som standardmodell og gemini-2.5-pro for avanserte oppgaver. Modellvalget er tilgjengelig basert på din lisenstype."
"Hva betyr ""Powered by Vauco AS""?","Dette er en obligatorisk merking som viser at tjenesten er utviklet, eid og driftet av Vauco AS (org.nr. 935 989 779), et norsk teknologiselskap. Det signaliserer at produktet er underlagt norsk lovgivning og en lokal, ansvarlig aktør."
"Er Threadstone norsk?","Ja, Threadstone er et helnorsk produkt. Det er utviklet, bygget og driftet i Oslo av Vauco AS, med et spesifikt fokus på sikkerhet og personvern for det nordiske markedet."
"Kan jeg bruke Threadstone på flere enheter?","Ja, plattformen er designet for å være fullt responsiv og tilgjengelig på alle enheter, enten du bruker PC, nettbrett eller mobil."
"Hva er Threadstone Assist?","Threadstone Assist er en innebygd guide som hjelper deg med å komme i gang og utnytte verktøyets fulle potensial."
"Hvilke lisenstyper finnes?","Vi tilbyr to hovedtyper: Threadstone Consumer for enkeltbrukere med lisenser på 7, 14, 30 eller 90 dager, og Threadstone Enterprise, en kvartalsbasert lisens for bedrifter med flere brukere og tilpassede funksjoner."
"Hva koster det?","Prisen avhenger av lisenstype og bruk. Kontakt oss på [billing@vauco.no](mailto:billing@vauco.no) for gjeldende priser."
"Hva er credits og hvordan brukes de?","Credits er vår interne valuta for å måle forbruk av AI-ressurser, hvor 1 credit tilsvarer 1 krone. Din lisens inkluderer en viss mengde bruk. Hvis du trenger mer kapasitet, kan du kjøpe ekstra credits."
"Hva skjer når creditsene er brukt opp?","Hvis du bruker opp den inkluderte kvoten, vil du få muligheten til å kjøpe flere credits for å fortsette å bruke tjenesten. For Enterprise-kunder kan overforbruk håndteres gjennom en egen avtale."
"Kan jeg oppgradere fra Consumer til Enterprise?","Ja, du kan enkelt oppgradere lisensen din. Prosessen er rask og kan startes direkte fra brukergrensesnittet."
"Lagres dataene mine?","Nei, ingen av dine samtaledata lagres på våre servere. Threadstone har en stateless arkitektur som betyr at alt du gjør i applikasjonen forblir lokalt på din maskin. Den eneste informasjonen vi har er knyttet til din lisens: ID, plan og utløpsdato."
"Er Threadstone GDPR-kompatibel?","Ja, plattformen er designet fra bunnen av for å være i samsvar med GDPR. Ved ikke å lagre dine data oppfyller vi prinsippet om dataminimering og gir deg full kontroll over din egen informasjon."
"Hvem har tilgang til mine samtaler?","Ingen andre enn deg selv. Siden ingen data forlater din lokale maskin, har verken ansatte i Vauco AS eller noen tredjeparter tilgang til innholdet i dine samtaler."
"Hva er stateless arkitektur?","Stateless (tilstandsløs) arkitektur betyr at serveren ikke lagrer informasjon om tidligere interaksjoner. Hver forespørsel behandles som en ny, uavhengig hendelse, som sikrer at ingen av dine data blir værende på våre systemer."
"Er Threadstone trygt å bruke i profesjonell sammenheng?","Ja, plattformens fokus på personvern og datasikkerhet gjør den ideell for profesjonell bruk. Den stateless arkitekturen sikrer at sensitiv bedriftsinformasjon forblir konfidensiell og under din fulle kontroll."
"Hvordan logger jeg inn?","Du får tilgang via en sikker innloggingsprosess med din Google-konto. Hver kunde får en unik og privat URL til sin egen instans av Threadstone."
"Hvilke enheter støttes?","Threadstone er en nettbasert applikasjon som fungerer på alle moderne enheter med en nettleser, inkludert stasjonære datamaskiner, bærbare, nettbrett og smarttelefoner."
"Hva er app.threadstone.vauco.no?","Dette er domenet hvor selve kundeapplikasjonen kjører. Når du kjøper en lisens, får du en unik lenke under dette domenet som leder til din private og sikre AI-arbeidsflate."
"Kan bedriften min ha egne instruksjoner og personas?","Ja, en av kjernefunksjonene i Threadstone Enterprise er muligheten til å definere tilpassede instruksjonssett og personas. Dette gjør at AI-assistenten kan tilpasses bedriftens spesifikke terminologi, tone og behov."
"Hva er $VCO?","$VCO (Vauco Coin) er et utility token som vil brukes til å betale for AI-kvote utover det som er inkludert i din lisens. I første fase brukes et internt kreditt-system, men $VCO er en del av vår langsiktige visjon for en transparent tokenøkonomi."
"Hvorfor bruker Threadstone tokens?","Token-systemet er designet for å skape en rettferdig og transparent modell for verdsetting av AI-forbruk. Det gir en forutsigbar måte å håndtere variable kostnader på, drevet av reell bruk av tjenesten, ikke spekulasjon."
"Hva er Vauco Value Ledger (VVL)?","Vauco Value Ledger (VVL) er den underliggende økonomiske infrastrukturen som håndterer alle verdiobjekter i Vaucos økosystem, inkludert $VCO. Det er en privat reskontro med innebygd vekslingsfunksjonalitet, tilgjengelig på [vvl.vauco.no](https://vvl.vauco.no)."
"Hva er $DRM og Deep Dream?","$DRM (Dream token) og Deep Dream er konsepter knyttet til en fremtidig fase av Vaucos utvikling. Deep Dream er tenkt som en plattform for kreative AI-sesjoner, og $DRM vil fungere som et verdi-anker knyttet til de unike digitale artefaktene som skapes der."
"Hva er MEDIOTEQ?","MEDIOTEQ er en spesialisert Threadstone Enterprise-instans, skreddersydd for norsk helsepersonell. Den fungerer som en klinisk beslutningsstøtte-agent for å hjelpe med vurderinger knyttet til funksjon, medisin og system, men er ikke et diagnoseverktøy."
"Kan MEDIOTEQ stille diagnoser?","Nei, absolutt ikke. MEDIOTEQ er et beslutningsstøtteverktøy, ikke en juridisk eller medisinsk instans. Verktøyet skal aldri erstatte klinisk skjønn, og ved alvorlige funn vil det alltid anbefale en legevurdering."
"Hvilke helsefaglige standarder støttes?","MEDIOTEQ støtter anerkjente standarder som Barthel-indeksen og MMSE (funksjon), ICD-10, NEWS2 og CRB-65 (medisin), samt regelverk knyttet til PLO, IP og NAV-stønader (system)."
"Kan MEDIOTEQ brukes i EPJ-dokumentasjon?","MEDIOTEQ er designet for å gi dokumentasjonsstøtte ved å strukturere informasjon i et format som er lett å overføre til elektroniske pasientjournaler (EPJ). Brukeren er selv ansvarlig for å verifisere og godkjenne all informasjon før den journalføres."
"Hvor lagres helsedata?","All data for MEDIOTEQ lagres i et separat og isolert miljø på servere i europe-north1 (Finland). Ingen rå pasientdata forlater kundens eget, sikre miljø, i tråd med GDPR og Normen."
"Kan pasienter bruke MEDIOTEQ direkte?","Nei, MEDIOTEQ er utelukkende et verktøy for autorisert helsepersonell. Plattformen skal aldri gi direktiver eller råd direkte til pasienter."
"Hvordan kontakter jeg support?","For alle spørsmål knyttet til fakturering, lisenser eller generell support, kontakt oss på [billing@vauco.no](mailto:billing@vauco.no)."
"Hva er billing@vauco.no?","Dette er vår sentrale e-postadresse for all kundekontakt som gjelder fakturering, betaling, lisensadministrasjon og support."
"Hvor finner jeg dokumentasjon?","Besøk [vauco.no](https://vauco.no) for informasjon og dokumentasjon, eller kontakt [billing@vauco.no](mailto:billing@vauco.no) for spesifikke spørsmål."
"Hvor finner jeg Threadstone?","Du finner informasjon om salg og lisenser på [threadstone.vauco.no](https://threadstone.vauco.no). Selve applikasjonen er tilgjengelig for kunder på [app.threadstone.vauco.no](https://app.threadstone.vauco.no)."
"Hvor kan jeg handle $VCO?","Handel og påfyll av $VCO skjer via vår portal [vco.vauco.no](https://vco.vauco.no)."
"Hvor leser jeg om Vauco Value Ledger?","Detaljert informasjon om vår økonomiske infrastruktur finner du på [vvl.vauco.no](https://vvl.vauco.no)."
"Hvor finner jeg støtte og dokumentasjon?","For generell informasjon, besøk [vauco.no](https://vauco.no). For support, kontakt [billing@vauco.no](mailto:billing@vauco.no)."
1 question answer
2 Hva er Threadstone? Threadstone er en avansert AI-arbeidsflate utviklet av det norske selskapet Vauco AS. Det er et SaaS-produkt designet for å være en intelligent og sikker assistent som tilpasser seg din unike kontekst og arbeidsflyt, i motsetning til generiske AI-verktøy.
3 Hvem er Threadstone for? Threadstone er for kreative fagpersoner, byråer og bedrifter som trenger et kraftig og sikkert AI-verktøy. Det finnes en Consumer-versjon for enkeltbrukere og en Enterprise-versjon for team og organisasjoner, inkludert den spesialiserte MEDIOTEQ-løsningen for helsevesenet.
4 Hva er forskjellen fra vanlige AI-verktøy som ChatGPT? Threadstone skiller seg ut ved sin stateless arkitektur som sikrer at ingen av dine data lagres på våre servere, noe som gir fullt personvern. I tillegg kan du tilpasse assistenten med egne instruksjoner for mer relevante resultater.
5 Hvilke modeller brukes? Threadstone bruker gemini-2.5-flash som standardmodell og gemini-2.5-pro for avanserte oppgaver. Modellvalget er tilgjengelig basert på din lisenstype.
6 Hva betyr "Powered by Vauco AS"? Dette er en obligatorisk merking som viser at tjenesten er utviklet, eid og driftet av Vauco AS (org.nr. 935 989 779), et norsk teknologiselskap. Det signaliserer at produktet er underlagt norsk lovgivning og en lokal, ansvarlig aktør.
7 Er Threadstone norsk? Ja, Threadstone er et helnorsk produkt. Det er utviklet, bygget og driftet i Oslo av Vauco AS, med et spesifikt fokus på sikkerhet og personvern for det nordiske markedet.
8 Kan jeg bruke Threadstone på flere enheter? Ja, plattformen er designet for å være fullt responsiv og tilgjengelig på alle enheter, enten du bruker PC, nettbrett eller mobil.
9 Hva er Threadstone Assist? Threadstone Assist er en innebygd guide som hjelper deg med å komme i gang og utnytte verktøyets fulle potensial.
10 Hvilke lisenstyper finnes? Vi tilbyr to hovedtyper: Threadstone Consumer for enkeltbrukere med lisenser på 7, 14, 30 eller 90 dager, og Threadstone Enterprise, en kvartalsbasert lisens for bedrifter med flere brukere og tilpassede funksjoner.
11 Hva koster det? Prisen avhenger av lisenstype og bruk. Kontakt oss på [billing@vauco.no](mailto:billing@vauco.no) for gjeldende priser.
12 Hva er credits og hvordan brukes de? Credits er vår interne valuta for å måle forbruk av AI-ressurser, hvor 1 credit tilsvarer 1 krone. Din lisens inkluderer en viss mengde bruk. Hvis du trenger mer kapasitet, kan du kjøpe ekstra credits.
13 Hva skjer når creditsene er brukt opp? Hvis du bruker opp den inkluderte kvoten, vil du få muligheten til å kjøpe flere credits for å fortsette å bruke tjenesten. For Enterprise-kunder kan overforbruk håndteres gjennom en egen avtale.
14 Kan jeg oppgradere fra Consumer til Enterprise? Ja, du kan enkelt oppgradere lisensen din. Prosessen er rask og kan startes direkte fra brukergrensesnittet.
15 Lagres dataene mine? Nei, ingen av dine samtaledata lagres på våre servere. Threadstone har en stateless arkitektur som betyr at alt du gjør i applikasjonen forblir lokalt på din maskin. Den eneste informasjonen vi har er knyttet til din lisens: ID, plan og utløpsdato.
16 Er Threadstone GDPR-kompatibel? Ja, plattformen er designet fra bunnen av for å være i samsvar med GDPR. Ved ikke å lagre dine data oppfyller vi prinsippet om dataminimering og gir deg full kontroll over din egen informasjon.
17 Hvem har tilgang til mine samtaler? Ingen andre enn deg selv. Siden ingen data forlater din lokale maskin, har verken ansatte i Vauco AS eller noen tredjeparter tilgang til innholdet i dine samtaler.
18 Hva er stateless arkitektur? Stateless (tilstandsløs) arkitektur betyr at serveren ikke lagrer informasjon om tidligere interaksjoner. Hver forespørsel behandles som en ny, uavhengig hendelse, som sikrer at ingen av dine data blir værende på våre systemer.
19 Er Threadstone trygt å bruke i profesjonell sammenheng? Ja, plattformens fokus på personvern og datasikkerhet gjør den ideell for profesjonell bruk. Den stateless arkitekturen sikrer at sensitiv bedriftsinformasjon forblir konfidensiell og under din fulle kontroll.
20 Hvordan logger jeg inn? Du får tilgang via en sikker innloggingsprosess med din Google-konto. Hver kunde får en unik og privat URL til sin egen instans av Threadstone.
21 Hvilke enheter støttes? Threadstone er en nettbasert applikasjon som fungerer på alle moderne enheter med en nettleser, inkludert stasjonære datamaskiner, bærbare, nettbrett og smarttelefoner.
22 Hva er app.threadstone.vauco.no? Dette er domenet hvor selve kundeapplikasjonen kjører. Når du kjøper en lisens, får du en unik lenke under dette domenet som leder til din private og sikre AI-arbeidsflate.
23 Kan bedriften min ha egne instruksjoner og personas? Ja, en av kjernefunksjonene i Threadstone Enterprise er muligheten til å definere tilpassede instruksjonssett og personas. Dette gjør at AI-assistenten kan tilpasses bedriftens spesifikke terminologi, tone og behov.
24 Hva er $VCO? $VCO (Vauco Coin) er et utility token som vil brukes til å betale for AI-kvote utover det som er inkludert i din lisens. I første fase brukes et internt kreditt-system, men $VCO er en del av vår langsiktige visjon for en transparent tokenøkonomi.
25 Hvorfor bruker Threadstone tokens? Token-systemet er designet for å skape en rettferdig og transparent modell for verdsetting av AI-forbruk. Det gir en forutsigbar måte å håndtere variable kostnader på, drevet av reell bruk av tjenesten, ikke spekulasjon.
26 Hva er Vauco Value Ledger (VVL)? Vauco Value Ledger (VVL) er den underliggende økonomiske infrastrukturen som håndterer alle verdiobjekter i Vaucos økosystem, inkludert $VCO. Det er en privat reskontro med innebygd vekslingsfunksjonalitet, tilgjengelig på [vvl.vauco.no](https://vvl.vauco.no).
27 Hva er $DRM og Deep Dream? $DRM (Dream token) og Deep Dream er konsepter knyttet til en fremtidig fase av Vaucos utvikling. Deep Dream er tenkt som en plattform for kreative AI-sesjoner, og $DRM vil fungere som et verdi-anker knyttet til de unike digitale artefaktene som skapes der.
28 Hva er MEDIOTEQ? MEDIOTEQ er en spesialisert Threadstone Enterprise-instans, skreddersydd for norsk helsepersonell. Den fungerer som en klinisk beslutningsstøtte-agent for å hjelpe med vurderinger knyttet til funksjon, medisin og system, men er ikke et diagnoseverktøy.
29 Kan MEDIOTEQ stille diagnoser? Nei, absolutt ikke. MEDIOTEQ er et beslutningsstøtteverktøy, ikke en juridisk eller medisinsk instans. Verktøyet skal aldri erstatte klinisk skjønn, og ved alvorlige funn vil det alltid anbefale en legevurdering.
30 Hvilke helsefaglige standarder støttes? MEDIOTEQ støtter anerkjente standarder som Barthel-indeksen og MMSE (funksjon), ICD-10, NEWS2 og CRB-65 (medisin), samt regelverk knyttet til PLO, IP og NAV-stønader (system).
31 Kan MEDIOTEQ brukes i EPJ-dokumentasjon? MEDIOTEQ er designet for å gi dokumentasjonsstøtte ved å strukturere informasjon i et format som er lett å overføre til elektroniske pasientjournaler (EPJ). Brukeren er selv ansvarlig for å verifisere og godkjenne all informasjon før den journalføres.
32 Hvor lagres helsedata? All data for MEDIOTEQ lagres i et separat og isolert miljø på servere i europe-north1 (Finland). Ingen rå pasientdata forlater kundens eget, sikre miljø, i tråd med GDPR og Normen.
33 Kan pasienter bruke MEDIOTEQ direkte? Nei, MEDIOTEQ er utelukkende et verktøy for autorisert helsepersonell. Plattformen skal aldri gi direktiver eller råd direkte til pasienter.
34 Hvordan kontakter jeg support? For alle spørsmål knyttet til fakturering, lisenser eller generell support, kontakt oss på [billing@vauco.no](mailto:billing@vauco.no).
35 Hva er billing@vauco.no? Dette er vår sentrale e-postadresse for all kundekontakt som gjelder fakturering, betaling, lisensadministrasjon og support.
36 Hvor finner jeg dokumentasjon? Besøk [vauco.no](https://vauco.no) for informasjon og dokumentasjon, eller kontakt [billing@vauco.no](mailto:billing@vauco.no) for spesifikke spørsmål.
37 Hvor finner jeg Threadstone? Du finner informasjon om salg og lisenser på [threadstone.vauco.no](https://threadstone.vauco.no). Selve applikasjonen er tilgjengelig for kunder på [app.threadstone.vauco.no](https://app.threadstone.vauco.no).
38 Hvor kan jeg handle $VCO? Handel og påfyll av $VCO skjer via vår portal [vco.vauco.no](https://vco.vauco.no).
39 Hvor leser jeg om Vauco Value Ledger? Detaljert informasjon om vår økonomiske infrastruktur finner du på [vvl.vauco.no](https://vvl.vauco.no).
40 Hvor finner jeg støtte og dokumentasjon? For generell informasjon, besøk [vauco.no](https://vauco.no). For support, kontakt [billing@vauco.no](mailto:billing@vauco.no).

159
data/threadstone_faq.md Normal file
View File

@ -0,0 +1,159 @@
---
# Ofte Stilte Spørsmål (FAQ) — Threadstone & MEDIOTEQ
Velkommen til oversikten over ofte stilte spørsmål for Threadstone og MEDIOTEQ. Dette dokumentet er laget for å gi deg en grundig innføring i våre AI-drevne verktøy, enten du er en kreativ profesjonell som bruker Threadstone Consumer, en bedriftskunde med Threadstone Enterprise, eller helsepersonell som benytter den spesialiserte MEDIOTEQ-løsningen. For spørsmål som ikke dekkes her, kontakt oss på billing@vauco.no eller besøk [vauco.no](https://vauco.no).
---
## Del 1 — Generelt om Threadstone
**Q: Hva er Threadstone?**
A: Threadstone er en avansert AI-arbeidsflate utviklet av det norske selskapet Vauco AS. Det er et SaaS-produkt designet for å være en intelligent og sikker assistent som tilpasser seg din unike kontekst og arbeidsflyt, i motsetning til generiske AI-verktøy.
**Q: Hvem er Threadstone for?**
A: Threadstone er for kreative fagpersoner, byråer og bedrifter som trenger et kraftig og sikkert AI-verktøy. Det finnes en Consumer-versjon for enkeltbrukere og en Enterprise-versjon for team og organisasjoner, inkludert den spesialiserte MEDIOTEQ-løsningen for helsevesenet.
**Q: Hva er forskjellen fra vanlige AI-verktøy som ChatGPT?**
A: Threadstone skiller seg ut ved sin stateless arkitektur som sikrer at ingen av dine data lagres på våre servere, noe som gir fullt personvern. I tillegg kan du tilpasse assistenten med egne instruksjoner for mer relevante resultater.
**Q: Hvilke modeller brukes?**
A: Threadstone bruker gemini-2.5-flash som standardmodell og gemini-2.5-pro for avanserte oppgaver. Modellvalget er tilgjengelig basert på din lisenstype.
**Q: Hva betyr "Powered by Vauco AS"?**
A: Dette er en obligatorisk merking som viser at tjenesten er utviklet, eid og driftet av Vauco AS (org.nr. 935 989 779), et norsk teknologiselskap. Det signaliserer at produktet er underlagt norsk lovgivning og en lokal, ansvarlig aktør.
**Q: Er Threadstone norsk?**
A: Ja, Threadstone er et helnorsk produkt. Det er utviklet, bygget og driftet i Oslo av Vauco AS, med et spesifikt fokus på sikkerhet og personvern for det nordiske markedet.
**Q: Kan jeg bruke Threadstone på flere enheter?**
A: Ja, plattformen er designet for å være fullt responsiv og tilgjengelig på alle enheter, enten du bruker PC, nettbrett eller mobil.
**Q: Hva er Threadstone Assist?**
A: Threadstone Assist er en innebygd guide som hjelper deg med å komme i gang og utnytte verktøyets fulle potensial.
---
## Del 2 — Lisens og priser
**Q: Hvilke lisenstyper finnes?**
A: Vi tilbyr to hovedtyper: Threadstone Consumer for enkeltbrukere med lisenser på 7, 14, 30 eller 90 dager, og Threadstone Enterprise, en kvartalsbasert lisens for bedrifter med flere brukere og tilpassede funksjoner.
**Q: Hva koster det?**
A: Prisen avhenger av lisenstype og bruk. Kontakt oss på [billing@vauco.no](mailto:billing@vauco.no) for gjeldende priser.
**Q: Hva er credits og hvordan brukes de?**
A: Credits er vår interne valuta for å måle forbruk av AI-ressurser, hvor 1 credit tilsvarer 1 krone. Din lisens inkluderer en viss mengde bruk. Hvis du trenger mer kapasitet, kan du kjøpe ekstra credits.
**Q: Hva skjer når creditsene er brukt opp?**
A: Hvis du bruker opp den inkluderte kvoten, vil du få muligheten til å kjøpe flere credits for å fortsette å bruke tjenesten. For Enterprise-kunder kan overforbruk håndteres gjennom en egen avtale.
**Q: Kan jeg oppgradere fra Consumer til Enterprise?**
A: Ja, du kan enkelt oppgradere lisensen din. Prosessen er rask og kan startes direkte fra brukergrensesnittet.
---
## Del 3 — Datasikkerhet og personvern
**Q: Lagres dataene mine?**
A: Nei, ingen av dine samtaledata lagres på våre servere. Threadstone har en stateless arkitektur som betyr at alt du gjør i applikasjonen forblir lokalt på din maskin. Den eneste informasjonen vi har er knyttet til din lisens: ID, plan og utløpsdato.
**Q: Er Threadstone GDPR-kompatibel?**
A: Ja, plattformen er designet fra bunnen av for å være i samsvar med GDPR. Ved ikke å lagre dine data oppfyller vi prinsippet om dataminimering og gir deg full kontroll over din egen informasjon.
**Q: Hvem har tilgang til mine samtaler?**
A: Ingen andre enn deg selv. Siden ingen data forlater din lokale maskin, har verken ansatte i Vauco AS eller noen tredjeparter tilgang til innholdet i dine samtaler.
**Q: Hva er stateless arkitektur?**
A: Stateless (tilstandsløs) arkitektur betyr at serveren ikke lagrer informasjon om tidligere interaksjoner. Hver forespørsel behandles som en ny, uavhengig hendelse, som sikrer at ingen av dine data blir værende på våre systemer.
**Q: Er Threadstone trygt å bruke i profesjonell sammenheng?**
A: Ja, plattformens fokus på personvern og datasikkerhet gjør den ideell for profesjonell bruk. Den stateless arkitekturen sikrer at sensitiv bedriftsinformasjon forblir konfidensiell og under din fulle kontroll.
---
## Del 4 — Tilgang og tekniske krav
**Q: Hvordan logger jeg inn?**
A: Du får tilgang via en sikker innloggingsprosess med din Google-konto. Hver kunde får en unik og privat URL til sin egen instans av Threadstone.
**Q: Hvilke enheter støttes?**
A: Threadstone er en nettbasert applikasjon som fungerer på alle moderne enheter med en nettleser, inkludert stasjonære datamaskiner, bærbare, nettbrett og smarttelefoner.
**Q: Hva er app.threadstone.vauco.no?**
A: Dette er domenet hvor selve kundeapplikasjonen kjører. Når du kjøper en lisens, får du en unik lenke under dette domenet som leder til din private og sikre AI-arbeidsflate.
**Q: Kan bedriften min ha egne instruksjoner og personas?**
A: Ja, en av kjernefunksjonene i Threadstone Enterprise er muligheten til å definere tilpassede instruksjonssett og personas. Dette gjør at AI-assistenten kan tilpasses bedriftens spesifikke terminologi, tone og behov.
---
## Del 5 — $VCO og tokenøkonomi
**Q: Hva er $VCO?**
A: $VCO (Vauco Coin) er et utility token som vil brukes til å betale for AI-kvote utover det som er inkludert i din lisens. I første fase brukes et internt kreditt-system, men $VCO er en del av vår langsiktige visjon for en transparent tokenøkonomi.
**Q: Hvorfor bruker Threadstone tokens?**
A: Token-systemet er designet for å skape en rettferdig og transparent modell for verdsetting av AI-forbruk. Det gir en forutsigbar måte å håndtere variable kostnader på, drevet av reell bruk av tjenesten, ikke spekulasjon.
**Q: Hva er Vauco Value Ledger (VVL)?**
A: Vauco Value Ledger (VVL) er den underliggende økonomiske infrastrukturen som håndterer alle verdiobjekter i Vaucos økosystem, inkludert $VCO. Det er en privat reskontro med innebygd vekslingsfunksjonalitet, tilgjengelig på [vvl.vauco.no](https://vvl.vauco.no).
**Q: Hva er $DRM og Deep Dream?**
A: $DRM (Dream token) og Deep Dream er konsepter knyttet til en fremtidig fase av Vaucos utvikling. Deep Dream er tenkt som en plattform for kreative AI-sesjoner, og $DRM vil fungere som et verdi-anker knyttet til de unike digitale artefaktene som skapes der.
---
## Del 6 — MEDIOTEQ for helsearbeidere
**Q: Hva er MEDIOTEQ?**
A: MEDIOTEQ er en spesialisert Threadstone Enterprise-instans, skreddersydd for norsk helsepersonell. Den fungerer som en klinisk beslutningsstøtte-agent for å hjelpe med vurderinger knyttet til funksjon, medisin og system, men er ikke et diagnoseverktøy.
**Q: Kan MEDIOTEQ stille diagnoser?**
A: Nei, absolutt ikke. MEDIOTEQ er et beslutningsstøtteverktøy, ikke en juridisk eller medisinsk instans. Verktøyet skal aldri erstatte klinisk skjønn, og ved alvorlige funn vil det alltid anbefale en legevurdering.
**Q: Hvilke helsefaglige standarder støttes?**
A: MEDIOTEQ støtter anerkjente standarder som Barthel-indeksen og MMSE (funksjon), ICD-10, NEWS2 og CRB-65 (medisin), samt regelverk knyttet til PLO, IP og NAV-stønader (system).
**Q: Kan MEDIOTEQ brukes i EPJ-dokumentasjon?**
A: MEDIOTEQ er designet for å gi dokumentasjonsstøtte ved å strukturere informasjon i et format som er lett å overføre til elektroniske pasientjournaler (EPJ). Brukeren er selv ansvarlig for å verifisere og godkjenne all informasjon før den journalføres.
**Q: Hvor lagres helsedata?**
A: All data for MEDIOTEQ lagres i et separat og isolert miljø på servere i europe-north1 (Finland). Ingen rå pasientdata forlater kundens eget, sikre miljø, i tråd med GDPR og Normen.
**Q: Kan pasienter bruke MEDIOTEQ direkte?**
A: Nei, MEDIOTEQ er utelukkende et verktøy for autorisert helsepersonell. Plattformen skal aldri gi direktiver eller råd direkte til pasienter.
---
## Del 7 — Support og kontakt
**Q: Hvordan kontakter jeg support?**
A: For alle spørsmål knyttet til fakturering, lisenser eller generell support, kontakt oss på [billing@vauco.no](mailto:billing@vauco.no).
**Q: Hva er billing@vauco.no?**
A: Dette er vår sentrale e-postadresse for all kundekontakt som gjelder fakturering, betaling, lisensadministrasjon og support.
**Q: Hvor finner jeg dokumentasjon?**
A: Besøk [vauco.no](https://vauco.no) for informasjon og dokumentasjon, eller kontakt [billing@vauco.no](mailto:billing@vauco.no) for spesifikke spørsmål.
---
## Del 8 — Lenker og ressurser
**Q: Hvor finner jeg Threadstone?**
A: Du finner informasjon om salg og lisenser på [threadstone.vauco.no](https://threadstone.vauco.no). Selve applikasjonen er tilgjengelig for kunder på [app.threadstone.vauco.no](https://app.threadstone.vauco.no).
**Q: Hvor kan jeg handle $VCO?**
A: Handel og påfyll av $VCO skjer via vår portal [vco.vauco.no](https://vco.vauco.no).
**Q: Hvor leser jeg om Vauco Value Ledger?**
A: Detaljert informasjon om vår økonomiske infrastruktur finner du på [vvl.vauco.no](https://vvl.vauco.no).
**Q: Hvor finner jeg støtte og dokumentasjon?**
A: For generell informasjon, besøk [vauco.no](https://vauco.no). For support, kontakt [billing@vauco.no](mailto:billing@vauco.no).
---
*Sist oppdatert: 2026-05-27 · Vauco AS · Org.nr 935 989 779*
---

8
debug_output.txt Normal file
View File

@ -0,0 +1,8 @@
--- Content of temp file ---
---------------------------
Backup saved: /home/chris_christiansen/crontab.backup.2026-09-03-031630
Resulting crontab:
Verification:
OK: No VM teardown or teardown-warning cron entries remain.

190
dev-start.sh Executable file
View File

@ -0,0 +1,190 @@
#!/bin/bash
set -e # Exit immediately if a command exits with a non-zero status.
set -u # Treat unset variables as an error.
# --- Configuration ---
# VM and Project Details
CPU_VM_NAME="osvauco-cpu-vm"
CPU_VM_ZONE="us-central1-b"
CPU_IP_NAME="osvauco-cpu-ip"
CPU_REGION="us-central1"
GPU_VM_NAME="osvauco-gpu-vm"
GPU_VM_ZONE="europe-west4-c"
GPU_IP_NAME="osvauco-gpu-ip"
GPU_REGION="europe-west4"
PROJECT_ID="propane-will-491900-m5"
VM_USER="chris_christiansen"
# Local SSH Configuration (for WSL/Git Bash on Windows)
# IMPORTANT: This path assumes you are running this script from WSL or a similar environment.
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.170.51.84:3000/chris/OSVauco.git"
GITHUB_REMOTE="git@github.com:vauco/osvauco.git"
# --- Helper Functions ---
# Function to print messages
log() {
echo "--- $1 ---"
}
# Pre-flight check to ensure a static IP has been reserved.
preflight_check_ip() {
local ip_name="$1"
local region="$2"
log "Pre-flight check for static IP: $ip_name"
if gcloud compute addresses describe "$ip_name" --project="$PROJECT_ID" --region="$region" --format="value(name)" >/dev/null 2>&1; then
echo "✅ Static IP '$ip_name' exists."
else
echo "❌ ERROR: Static IP '$ip_name' not found in region '$region'."
echo "This script requires a reserved static IP to function reliably."
echo ""
echo "To fix this, please run the following command:"
echo "gcloud compute addresses create \"$ip_name\" --project=\"$PROJECT_ID\" --region=\"$region\" --network-tier=STANDARD"
echo ""
echo "After creating the IP, ensure it is assigned to the correct VM, then re-run this script."
exit 1
fi
}
# Function to start a VM if it's not running
start_vm() {
local vm_name="$1"
local zone="$2"
log "Checking status of $vm_name"
# Get the current status of the VM.
# The `|| echo "TERMINATED"` part handles cases where gcloud might fail if the VM is in a weird state.
local status=$(gcloud compute instances describe "$vm_name" --project="$PROJECT_ID" --zone="$zone" --format="get(status)" || echo "TERMINATED")
if [ "$status" == "RUNNING" ]; then
echo "$vm_name is already running."
else
echo "VM is $status. Starting $vm_name..."
gcloud compute instances start "$vm_name" --project="$PROJECT_ID" --zone="$zone"
echo "$vm_name started."
fi
}
# Function to get a reserved static IP address
get_static_ip() {
local ip_name="$1"
local region="$2"
gcloud compute addresses describe "$ip_name" --project="$PROJECT_ID" --region="$region" --format="get(address)"
}
# --- Main Script ---
# 0. Pre-flight checks for required infrastructure
preflight_check_ip "$CPU_IP_NAME" "$CPU_REGION"
preflight_check_ip "$GPU_IP_NAME" "$GPU_REGION"
# 1. Start VMs
start_vm "$CPU_VM_NAME" "$CPU_VM_ZONE"
start_vm "$GPU_VM_NAME" "$GPU_VM_ZONE"
# 2. Get Static IPs
log "Fetching static IP addresses"
CPU_IP=$(get_static_ip "$CPU_IP_NAME" "$CPU_REGION")
GPU_IP=$(get_static_ip "$GPU_IP_NAME" "$GPU_REGION")
echo " - CPU VM IP: $CPU_IP"
echo " - GPU VM IP: $GPU_IP"
# 3. Update SSH Config on Windows
log "Updating SSH config file at $SSH_CONFIG_PATH"
if [ ! -f "$SSH_CONFIG_PATH" ]; then
echo "⚠️ SSH config file not found. Creating it."
touch "$SSH_CONFIG_PATH"
fi
# We use a marker block to safely update the SSH config.
# This is safer than trying to edit lines in place.
MARKER_BEGIN="# BEGIN OSVAUCO-MANAGED-BLOCK"
MARKER_END="# END OSVAUCO-MANAGED-BLOCK"
# Create a temporary file to build the new config
temp_config=$(mktemp)
# Copy the config content, excluding the old managed block
awk -v begin="$MARKER_BEGIN" -v end="$MARKER_END" '
$0 == begin {in_block=1}
!in_block {print}
$0 == end {in_block=0}
' "$SSH_CONFIG_PATH" > "$temp_config"
# Append the new, updated block to the temporary file
cat <<EOT >> "$temp_config"
$MARKER_BEGIN
# This block is automatically managed by dev-start.sh
# Do not edit manually!
Host osvauco-cpu
HostName $CPU_IP
User $VM_USER
IdentityFile $SSH_KEY_PATH
Host osvauco-gpu
HostName $GPU_IP
User $VM_USER
IdentityFile $SSH_KEY_PATH
$MARKER_END
EOT
# Overwrite the original config file with the updated one
mv "$temp_config" "$SSH_CONFIG_PATH"
echo "✅ SSH config updated successfully."
# 4. Print Connect Commands
log "Setup Complete! You can now connect using:"
echo ""
echo " ssh osvauco-cpu"
echo " ssh osvauco-gpu"
echo ""
# 5. Verify Git Remotes on the GPU VM
log "Verifying Git remotes on $GPU_VM_NAME"
# This command runs non-interactively on the remote VM.
gcloud compute ssh "${VM_USER}@${GPU_VM_NAME}" --zone="${GPU_VM_ZONE}" --project="${PROJECT_ID}" --command="
set -e
echo '--- Running remote verification ---'
cd ~/OSVauco || { echo 'Error: ~/OSVauco directory not found.'; exit 1; }
# Check and fix 'origin' remote
ORIGIN_URL=\$(git remote get-url origin 2>/dev/null || echo '')
if [ "\$ORIGIN_URL" == "${GITEA_REMOTE}" ]; then
echo '✅ Git remote "origin" is correct.'
else
echo '⚠️ Git remote "origin" is incorrect or missing. Setting it...'
if [ -n "\$ORIGIN_URL" ]; then
git remote set-url origin "${GITEA_REMOTE}"
else
git remote add origin "${GITEA_REMOTE}"
fi
echo '✅ Remote "origin" set to: ${GITEA_REMOTE}'
fi
# Check and fix 'github' remote
GITHUB_URL=\$(git remote get-url github 2>/dev/null || echo '')
if [ "\$GITHUB_URL" == "${GITHUB_REMOTE}" ]; then
echo '✅ Git remote "github" is correct.'
else
echo '⚠️ Git remote "github" is incorrect or missing. Setting it...'
if [ -n "\$GITHUB_URL" ]; then
git remote set-url github "${GITHUB_REMOTE}"
else
git remote add github "${GITHUB_REMOTE}"
fi
echo '✅ Remote "github" set to: ${GITHUB_REMOTE}'
fi
echo '--- Remote verification complete ---'
"
log "All tasks finished."

48
dev-startup.sh Executable file
View File

@ -0,0 +1,48 @@
#!/bin/bash
# dev-startup.sh
set -e
echo "=== 🔄 Oppdaterer gcloud ==="
sudo apt-get update -q && sudo apt-get --only-upgrade install -y -q google-cloud-cli
# --- gcloud login (kun hvis ikke allerede innlogget) ---
ACTIVE_ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format='value(account)' 2>/dev/null | head -1)
if [[ -z "$ACTIVE_ACCOUNT" ]]; then
echo "=== 🔑 Logger inn på gcloud ==="
gcloud auth login --no-launch-browser
else
echo "=== 🔑 Allerede innlogget som $ACTIVE_ACCOUNT ==="
fi
echo "=== ⚙️ Setter prosjekt ==="
gcloud config set project propane-will-491900-m5 --quiet
# --- ADC (kun hvis ikke allerede gyldig) ---
if ! gcloud auth application-default print-access-token &>/dev/null; then
echo "=== 🔐 Application Default Credentials ==="
gcloud auth application-default login --no-launch-browser
gcloud auth application-default set-quota-project propane-will-491900-m5
else
echo "=== 🔐 ADC allerede gyldig ==="
fi
echo "=== 🐙 Git ==="
cd ~/OSVauco && git pull
echo "=== 🌐 DNS-status ==="
dig costguard.oss.vauco.no CNAME @8.8.8.8 +short
dig opax.vauco.no CNAME @8.8.8.8 +short
echo "=== ☁️ Cloud Run ==="
gcloud run services list --region=us-central1
gcloud beta run domain-mappings list --region=us-central1
echo "=== ✅ Klar for utvikling! ==="
# --- Start OPAX? ---
read -rp $'\n▶ Start OPAX? [Y/n] ' answer
answer=${answer:-Y}
if [[ "$answer" =~ ^[Yy]$ ]]; then
source ~/OSVauco/scripts/opax.sh
fi

View File

@ -0,0 +1,67 @@
---
TITLE AGENTIC CONTRACT — Structured Prompt Provisioning via MCP (OSVauco Edition)
---
> **Thread type:** PLAN / ARCH
> **Version:** 1.0.1
> **Date:** 2026-05-21
> **Status:** IN PROGRESS
> **Owner:** OSVauco Operator Hub
Denne versjonen er kopiert og lett tilpasset fra `vauco-os/docs/AGENTIC_CONTRACT_MCP_PROVISIONING.md`.
## 📌 Concept Note
Structured prompts provisioneres fra OSVauco som API-endepunkter.
Hver modul/API er governet via en MCP-server under Vauco, og tilgang er gated av Google Login på `@vauco.no` eller godkjent alias.
## 🏛️ Architecture — 3 Layers
```
┌──────────────────────────────────────────────────────────┐
│ LAYER 3 — IDENTITY │
│ Google OAuth → @vauco.no sub-mail or approved alias │
│ Whitelisted in OSVauco Login Whitelist │
└──────────────────────┬───────────────────────────────────┘
│ authenticated identity
┌──────────────────────▼───────────────────────────────────┐
│ LAYER 2 — GOVERNANCE │
│ MCP Server (OPAX-MCP) │
│ Tool registry · Access policy · Routing rules │
│ Minimum-privilege roles per module │
└──────────────────────┬───────────────────────────────────┘
│ governed tool call
┌──────────────────────▼───────────────────────────────────┐
│ LAYER 1 — PROVISIONING │
│ OSVauco / Vauco OS API │
│ Serves structured prompt payloads to agents/consumers │
│ Each module = a callable API endpoint │
└──────────────────────────────────────────────────────────┘
```
## 🔐 Identity & Access Control
- Primær identitet: `name@vauco.no`
- Sub-mail alias: `role@vauco.no`
- Ekstern alias: whitelist-only
Regler:
- Hver identitet knyttes til en GCP IAM-rolle.
- Hver MCP-modul har minimum-privilege-scope.
- Login whitelist vedlikeholdes i OSVauco/Command Hub.
- Ingen blanket API-tilgang — scope per modul.
## 🗺️ Implementation Roadmap (kort)
1. Forankre arkitektur i OSVauco (denne fila + `docs/AGENT_RULEBOOK.md`).
2. Definér IAM-roller og modul-scope i GCP.
3. Etabler MCP-server som permission broker foran OPAX-MCP.
4. Logg alle MCP-kall til Firestore med identity + modul.
5. Prototype med én modul, deretter tre.
## ✅ Done When
- 3+ moduler callable via MCP-endepunktet.
- Alle moduler gated av Google OAuth.
- Firestore logger alle kall med identity + modul + resultat.
- DEPLOY GODKJENT mottatt før produksjonsregistrering.

122
docs/AGENT_RULEBOOK.md Normal file
View File

@ -0,0 +1,122 @@
# AGENT RULEBOOK — OSVauco
**Status:** 🔒 LOCKED DEFINITIONS — ikke endre uten `RULEBOOK APPROVED` fra Chris
**Sist oppdatert:** 2026-06-10
> Dette er oppstartsreglene som alle agenter (Emma, Jason, Perplexity stand-in) laster ved boot.
> Endringer her krever eksplisitt godkjenning. Regler er additive — gamle regler slettes ikke, de overskrives med ny dato.
---
## R-01 · Modell-hierarki
| Agent | Modell | Scope |
|-------|--------|-------|
| Emma (intern) | `gemma-4-12b-it` int4 på GPU-VM | Chris sin co-pilot: kode, deploy, planlegging |
| Jason (kundevendt) | `gemini-2.5-flash` via Vertex | Billing, anomaly, status for kunder |
| Perplexity stand-in | Sonnet 4.6 via GitHub MCP | Erstatter Emma til ML-3a er ferdig |
Emma og Jason er **aldri samme agent**. Separate identiteter, separate tilganger.
---
## R-02 · Forbudte identiteter
Disse kontiene skal **aldri** opprettes eller gis tilganger:
- `tinius.vauger@vauco.no`
- `ccv@vauco.no` eller varianter
---
## R-03 · Deploy-protokoll
1. Alltid sjekk kostnad via `infrastructure/09-cost-check.sh` før deploy
2. Ingen deploy uten smoke-test på `/health`
3. Cloud Run: alltid `--no-allow-unauthenticated` på prod-tjenester
4. Ny revisjon bekreftes med `gcloud run services describe` etter deploy
---
## R-04 · Dokumenthierarki
Alle agenter følger dette hierarkiet — nyere dato vinner ved konflikt:
1. `docs/MASTERPLAN.md` — arkitektur og sannhet
2. `docs/ROADMAP.md` — prioriteringer
3. `docs/AGENT_RULEBOOK.md` — denne filen
4. `docs/gemma/world.md` — Emmas kontekstpakke
5. `docs/HANDOFF.md` — siste sesjonsstatus
---
## R-05 · Filsti-regel (låst 2026-06-10)
**Absolutte filstier er obligatorisk i all Python-kode som serverer statiske filer.**
Korrekt mønster:
```python
from pathlib import Path
STATIC_DIR = Path(__file__).parent.parent / "static" # juster nivåer etter struktur
@app.get("/")
async def root():
return FileResponse(str(STATIC_DIR / "index.html"))
```
Forbudt mønster:
```python
FileResponse("static/index.html") # ❌ ALDRI — relativ sti knekker i Docker/Cloud Run
```
**Begrunnelse:** WORKDIR i Dockerfile avviker fra repo-rot. Relativ sti løses fra prosessens CWD (f.eks. `/app/agents/core-logic`), ikke fra der filen ligger. `Path(__file__)` er alltid korrekt uavhengig av hvordan prosessen startes.
**Gjelder:** Alle nye tjenester, alle klientleveranser, Emma sin lokale VM-agent.
---
## R-06 · Dockerfile-regel (låst 2026-06-10)
Alle Dockerfiler som bruker statiske filer **må** eksplisitt kopiere dem:
```dockerfile
COPY static/ ./static/ # ← obligatorisk — kopieres ikke automatisk
```
Sjekkliste ved ny tjeneste:
- [ ] `COPY agents/` eller relevant kildekode
- [ ] `COPY static/ ./static/` hvis tjenesten serverer HTML/CSS/JS
- [ ] WORKDIR satt korrekt
- [ ] CMD peker på riktig modul (`app:app`, ikke `main:app` med mindre main.py er inngangen)
---
## R-07 · FastAPI rot-rute
Alle FastAPI-tjenester med en HTML-frontend **må** definere `GET /` eksplisitt:
```python
@app.get("/")
async def root():
index = STATIC_DIR / "index.html"
if not index.exists():
raise HTTPException(404, detail=f"index.html ikke funnet på {index}")
return FileResponse(str(index), media_type="text/html")
```
FastAPI returnerer `{"detail":"Not Found"}` for alle udefinerte ruter — inkludert `/`.
---
## R-08 · Emma (lokal VM-agent) — oppstartsregler
Når Emma kjøres lokalt på GPU-VM:
1. Last `docs/gemma/world.md` som system-prompt ved oppstart
2. Bruk absolutte stier for alle filoperasjoner (R-05)
3. Eksponer kun localhost:11434 (Ollama) eller localhost:8000 (vLLM) — aldri åpne porter mot internett uten IAP
4. Logg alle kall til `~/.emma/logs/YYYY-MM-DD.log`
5. Emma er ikke kundevendt — bruk Jason for alt kundevendt
---
*Oppdatert 2026-06-10 | Neste oppdatering ved ML-3a (GPU-VM deploy)*

250
docs/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,250 @@
# VAUCO OS — ARCHITECTURE.md
# Eier: PLAN-Claude-Architect
# Sist oppdatert: 2026-06-29 CEST
# Godkjent av: Chris Christiansen
---
## Hensikt
Single source of truth for systemarkitektur, komponent-diagram og design-kontrakter.
Ingen andre tråder skriver denne filen. Andre tråder kan foreslå patcher via PR.
---
## Komponent-diagram (ASCII)
```
[Chris / Operatør]
|
-----+------+----------+----------+
| | | |
OPS EXEC PLAN AUDIT
Perplexity Gemini Claude Nemotron
Computer 2.5 Pro Sonnet/ (NMT)
Opus
| | | |
+-----+-----+----------+----------+
|
[GitHub]
OSVauco (main)
vauco-bootstrap (main)
|
[GCP / Cloud Run]
project: propane-will-491900-m5
region: us-central1
|
[OPAX-MCP — opax.vauco.no]
service: opax-mcp-core
protocol: OPAX Protocol
|
[Klient-OS]
<klient>-oss.vauco.no (stage)
<klient>-os.vauco.no (prod)
```
---
## VM-infrastruktur (GCP Compute Engine)
| VM | Formål | Specs | Modeller |
|----|--------|-------|----------|
| `osvauco-dev-vm` | Utvikling, OSV-pipeline (lokal Ollama), MCP-testing | **16 GB RAM**, CPU-only, `us-central1-b` | gemma3:4b (3.3 GB), qwen2.5:7b (4.7 GB), nomic-embed-text, llama3.2 |
| `emma-gpu-vm` | Tung ML, Emma co-pilot (Gemma 4 27B) | GPU, høy RAM | Gemma 4 27B (int4) — kun ved eksplisitt eskalering |
### OSV-pipeline (agents/osv/pipeline.py)
Lokal tre-lags Ollama-pipeline på `osvauco-dev-vm` for å minimere API-kostnader:
```
Lag 1 — gemma3:4b (front) → forstår intent, strukturerer oppgave
Lag 2 — qwen2.5:7b (analyse) → dyptgående analyse, kode, planlegging
Lag 3 — gemma3:4b (output) → formaterer og leverer svar til bruker
```
**Eskaleringspolitikk (Emma):**
- `emma` trigger: kun hvis Chris eksplisitt skriver "emma" i meldingen
- `ask_emma`: pipeline spør Chris hvis konfidans < 0.4
- Aldri automatisk eskalering
- Emma kjører på `emma-gpu-vm:11434` — kostnad påløper ved oppstart
**Viktig — RAM-krav:**
- `osvauco-dev-vm` MÅ ha ≥16 GB RAM for å kjøre gemma3:4b + qwen2.5:7b samtidig
- 8 GB er ikke tilstrekkelig — modellene timeout-er under lasting
- Øk VM til 16 GB i GCP Console ved behov
---
## MCP Identity
| Parameter | Verdi |
|----------------|------------------------------------|
| MCP_NAME | OPAX-MCP |
| MCP_PROTOCOL | OPAX Protocol |
| MCP_SERVICE | opax-mcp-core |
| Cloud Run | us-central1 |
| HUB_URL | https://opax.vauco.no (Phase 2) |
---
## Domenekonvensjon — LOCKED
| Subdomain | Type | Formål |
|-------------------------|----------|---------------------------------------------------|
| `opax.vauco.no` | Hub/MCP | OPAX-MCP operator hub — kun Vauco internt |
| `<klient>-os.vauco.no` | Prod OS | Kundens live produksjonssystem |
| `<klient>-oss.vauco.no` | Stage OS | Kundens staging/demo-system (pre-go-live) |
> `-os` = produksjon. `-oss` = staging. Aldri omvendt.
### Auth-strategi
- **Nå:** Google OAuth på alle miljøer
- **Senere:** BankID på `-os`-domener for kliniske kunder (Medioteq først)
### Kjente / planlagte domener
| Domene | Status | Cloud Run-tjeneste |
|-------------------------|---------------|-----------------------|
| `opax.vauco.no` | DNS pending | `opax-mcp-core` ✅ |
| `medioteq-oss.vauco.no` | Planlagt | `medioteq-oss-core` |
| `medioteq-os.vauco.no` | Planlagt | `medioteq-os-core` |
---
## Tråd-kontrakter
| Tråd | Modell | Eid fil | Konnektor |
|---|---|---|---|
| OPS-Computer-Hub | Perplexity Computer | AGENT_RULEBOOK.md, LEARNINGS.md, CHANGELOG.md | GitHub web, search |
| EXEC-Gemini-Workstation | Gemini 2.5 Pro | SYSTEM_STATE_AGENT_BOOT.md, TODO.md | shell, gcloud, git |
| PLAN-Claude-Architect | Claude Sonnet/Opus | ARCHITECTURE.md (denne), ROADMAP.md | GitHub UI/API |
| AUDIT-NMT-Critic | Nemotron | RISKREGISTER.md, PLANBOARD.md | read-only alle filer |
Regel: En fil — én eier. Andre tråder foreslår via PR, eier merger.
---
## Dataflyt
```
OPS --[MD-patch PR]--> PLAN merger
EXEC --[SYSTEM_STATE]--> alle leser
PLAN --[arch-spec PR]--> EXEC implementerer
AUDIT--[PLANBOARD]--> Chris prioriterer
Filer flyter via: GitHub commits på OSVauco/main
Aldri: direkte tråd-til-tråd
```
---
## Klientmodell — Prosjektisolasjon
**Prinsipp:** Hver klient med sensitiv/medisinsk data får eget GCP-prosjekt.
| Lag | GCP-prosjekt | Rolle |
|-----|-------------|-------|
| OPAX/OSVauco | `propane-will-491900-m5` | Management plane — deploy, monitor, orchestrate |
| Medioteq klinisk | `<medioteq-project-id>` | Data plane — kliniske data forblir her |
- OPAX mottar **aldri** rå pasientdata
- Data-residency: `europe-north1` for norske helsedata
- Alle agentkall logges til BigQuery i klientprosjektet (input/output hash, ikke råinnhold)
---
## Avhengigheter
- `OSVauco/main` er canonical branch for dette prosjektet
- Cloud Run avhenger av `vauco-gemini-tui-bridge` og OPAX-MCP-tjenesten
- Bridge avhenger av `bridge/`-kode + GCP service account `vauco-dev`
- Token economy avhenger av `scripts/token_economy.py` (planlagt)
- Pre-commit hook v2 avhenger av `.githooks/pre-commit` (planlagt)
- OSV-pipeline avhenger av Ollama på `osvauco-dev-vm` med ≥16 GB RAM
---
## Åpne spørsmål (max 3)
1. **Langsiktig repo-struktur?**
- A: Behold OSVauco + tjeneste-repoer (nåværende)
- B: Slå sammen til monorepo
- C: Splitt ytterligere per tjeneste
2. **Cloud Run min-instances=0 eller 1?**
- A: 0 — kutter idle-kostnad (anbefalt H0)
- B: 1 — ingen cold start latency
3. **IAM: vauco-dev service account scope?**
- A: Minimal — kun Vertex AI + Firestore
- B: Per-skill separate accounts
---
EOF v1.2 — 2026-06-29 — OPS (osvauco-dev-vm + OSV-pipeline + Emma-eskaleringspolitikk lagt til)
---
## Dual-layer deploymodell: B over A
# Vauco Architecture Manifest
## Core Thesis
Vauco should use a dual-layer deployment model in which **Model B governs Model A**. The internal OPAX-MCP control plane should own policy, approvals, orchestration, and capability boundaries, while the simpler Cloud Build trigger pattern remains the standardized execution path for services that benefit from a repeatable and low-friction deployment flow.
This approach fits the documented profile model, where operator capabilities are broad and privileged while external, family, customer, or blueprint-driven profiles receive narrower and safer capabilities. It also matches the existing direction that OPAX-MCP is the external gateway and governance surface, while Gitea remains the source of truth for code.
## Layer Model
### Layer 1: Gitea as code truth
Gitea is the intended primary source of truth for code, and the repository flow has already been shifted toward Gitea-first operation rather than GitHub-first day-to-day work. This means repository state, branches, commits, and promotion events should originate from Gitea even when downstream deployment mechanisms differ by service type.
### Layer 2: Model A as standard execution plane
Model A is the standardized deploy mechanism: a declarative Cloud Build pipeline builds the image, pushes it to Artifact Registry, renders a temporary service manifest from opax-mcp.yaml, and applies it with gcloud run services replace. This makes Model A suitable as the simple operational path for services that benefit from repeatable, low-drama deployment behavior.
Model A should therefore be treated as the default execution substrate for client-facing or repeatable services, especially where simplicity, supportability, and low operator friction matter more than deep custom orchestration. It is an execution mechanism, not the long-term brain of the platform.
### Layer 3: Model B as internal control organ
Model B is the internal OPAX-MCP-owned control organ. Its documented architecture places webhook verification, approval handling, SMS-based HITL, and deploy orchestration inside OPAX-MCP itself, culminating in a controlled gcloud builds submit using the existing declarative pipeline file.
In this architecture, Model B does not need to replace Model A everywhere. Instead, Model B should govern when Model A is created, updated, permitted, or bypassed, allowing Vauco to keep a proprietary control plane without forcing every service into the same high-complexity execution path.
## Design Principle
The key principle is **complexity in the core, simplicity at the edge**. OPAX-MCP should centralize policy, operator powers, capability boundaries, and architectural governance, while simpler deployment channels remain available for services that do not need bespoke orchestration.
This is operationally healthier than making every deployment path equally advanced. A system where every service must go through the most complex path increases cognitive load and failure analysis burden, while a system with no internal control organ increases fragmentation across triggers, consoles, and undocumented workflows.
## Why This Fits Vauco
This dual model supports the existing profile strategy. The documented access model already distinguishes operator access from family, blueprint, and customer access, which implies a future where capabilities are governed centrally rather than exposed raw to every actor.
It also supports the documented learning-loop strategy: quality should come from verified events, clear diffs, explicit blockers, and curated operational truth instead of uncontrolled growth in tools or context. A layered model makes it easier to preserve that discipline because the control plane can observe, gate, and record the simpler execution plane rather than becoming identical to it.
## Trade-offs
| Topic | Model A alone | Model B alone | Hybrid: B governs A |
|---|---|---|---|
| Speed to restore deploy flow | High | Low to medium | High for standard paths, medium for advanced paths |
| Proprietary control | Lower | Higher | High where it matters, moderate where standardization is enough |
| Cognitive load | Lower initially | Higher initially | Lower at edge, concentrated in operator core |
| Platform value over time | Moderate | High | High, with incremental rollout |
| Suitability for profiles/capabilities | Limited | Strong | Strong |
## Governance Rules
To avoid dual-truth drift, the following architectural rules should hold:
- Gitea is the source of truth for code and repository events.
- opax-mcp.yaml is the deploy configuration truth for opax-mcp and should not be bypassed by ad-hoc imperative deploys.
- OPAX-MCP is the policy and control truth for operator-level orchestration and future capability governance.
- Model A is a standard execution mechanism used by policy, not an independent architectural authority.
## Recommended Path
The recommended path is to restore and stabilize Model A first as the immediate execution layer, because the declarative pipeline file already exists and is the fastest way to stop manual deployment drift. After that, Model B should be built deliberately as the internal governance and orchestration layer that can manage or supersede Model A where appropriate.
This sequence preserves momentum without giving up the proprietary direction. It creates a platform in which Vauco can scale simple services through standardized execution while reserving deeper operator control for the places where control, policy, and identity matter most.

130
docs/BLUEPRINT-DEV-SNAP.md Normal file
View File

@ -0,0 +1,130 @@
# BLUEPRINT DEV-SNAP / OPAX-MCP OPPSETT
# Frosset: 2026-07-21 21:03 CEST
# Fase 5 Frys sannhet på dev-snap før migrering til Gitea-VM
---
## 1. INFRASTRUKTUR VM-OVERSIKT
| VM | IP | Rolle | Tjenester |
|---|---|---|---|
| osvauco-dev-from-snap | 34.170.51.84 | Dev/GPU-VM, verifikasjonsflate | Ollama (port 11434), Gitea (port 3000), gcloud, Gemini CLI |
| Gitea-CPU-VM | 34.67.252.59 | Planlagt permanent målmaskin | Ollama (port 11434), Gitea (port 3000) |
⚠️ VIKTIG: `opax-mcp` Cloud Run peker nå til `34.170.51.84` (dev-snap) for både OLLAMA og GITEA.
Planlagt mål er `34.67.252.59` (Gitea-CPU-VM) — dette må byttes når Gitea-VM er klar som permanent plattform.
---
## 2. CLOUD RUN SERVICES
| Service | URL (kanonisk) | Revisjon | SA |
|---|---|---|---|
| osvauco-agent | https://osvauco-agent-357036551735.us-central1.run.app | osvauco-agent-00695-8lk | osvauco-agent-sa@ |
| opax-mcp | https://opax-mcp-357036551735.us-central1.run.app | opax-mcp-00123-g7d | jason-vauger@ |
---
## 3. OPAX-MCP ENV VARS (komplett, verifisert 2026-07-21)
### Klartekst-variabler
```
GOOGLE_CLOUD_PROJECT=propane-will-491900-m5
OLLAMA_BASE_URL=http://34.170.51.84:11434 # ⚠️ dev-snap IP — byttes til Gitea-VM
EMMA_MODEL=gemma3:4b
EMMA_FAST_MODEL=gemma3:4b
EMMA_LIGHT_MODEL=qwen2.5:3b
GITEA_URL=http://34.170.51.84:3000 # ⚠️ dev-snap IP — byttes til Gitea-VM / git.vauco.no
STATIC_BUCKET=opax-vauco-static
CLOUD_RUN_SERVICE=osvauco-agent
CLOUD_BUILD_TRIGGER_ID=38423976-91ff-4ff4-859e-1f262344c609
OPAX_IAP_CLIENT_ID=357036551735-kq8nt7ld38hfqlcfb3n52ef7tala4meo.apps.googleusercontent.com
GMAIL_DEFAULT_SENDER=jason.vauger@vauco.no
GMAIL_ALLOWED_SENDERS=jason.vauger@vauco.no,emma.vauger@vauco.no
OSVAUCO_AGENT_URL=https://osvauco-agent-357036551735.us-central1.run.app
```
### Secrets (fra GCP Secret Manager)
```
GITEA_TOKEN ← secret: gitea-token (latest)
INTERNAL_API_KEY ← secret: INTERNAL_API_KEY (latest)
MCP_SECRET ← secret: mcp-server-key (latest)
TWILIO_ACCOUNT_SID ← secret: TWILIO_ACCOUNT_SID (latest)
TWILIO_AUTH_TOKEN ← secret: TWILIO_AUTH_TOKEN (latest)
TWILIO_FROM_NUMBER ← secret: TWILIO_FROM_NUMBER (latest)
```
---
## 4. LOKALE MODELLER
### På dev-snap (34.170.51.84:11434) — localhost
| Modell | Størrelse | Capabilities | Rolle |
|---|---|---|---|
| qwen2.5:7b | 4.7 GB | completion, tools | Lokal tung modell |
| gemma3:4b | 3.3 GB | completion | EMMA_MODEL, EMMA_FAST_MODEL |
| llama3.2:latest | 2.0 GB | completion, tools | Jason / lett agentmodell |
| nomic-embed-text | 274 MB | embedding | RAG-embedding |
### På Gitea-CPU-VM (34.67.252.59:11434) — via MCP
| Modell | Størrelse | Capabilities | Rolle |
|---|---|---|---|
| gemma3:4b | 3.3 GB | completion | EMMA_MODEL / Emma primær |
| qwen2.5:3b | 1.9 GB | completion, tools | EMMA_LIGHT_MODEL / Jason lett |
---
## 5. MODELLROLLER
| Env var | Verdi | Rolle |
|---|---|---|
| EMMA_MODEL | gemma3:4b | Emma primær — tung analyse |
| EMMA_FAST_MODEL | gemma3:4b | Emma rask — samme modell nå |
| EMMA_LIGHT_MODEL | qwen2.5:3b | Lett/Jason — tools-kjøring |
---
## 6. GITEA
| Parameter | Verdi |
|---|---|
| Nåværende URL | http://34.170.51.84:3000 (dev-snap) |
| Planlagt URL | http://git.vauco.no (når DNS er oppe) |
| Repo | chris/OSVauco |
| Token | GCP Secret: gitea-token |
| API-base | {GITEA_URL}/api/v1 |
---
## 7. HURTIGTEST-KOMMANDOER
```bash
# Fra dev-snap — verifiser Ollama lokalt
curl http://localhost:11434/api/tags
# Fra dev-snap — verifiser Gitea lokalt
curl http://localhost:3000/api/v1/repos/chris/OSVauco/commits?limit=1 -H "Authorization: token $(gcloud secrets versions access latest --secret=gitea-token)"
# Verifiser Cloud Run audience (riktig token)
TOKEN=$(curl -s "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://osvauco-agent-357036551735.us-central1.run.app&format=full" -H "Metadata-Flavor: Google")
curl -s -H "Authorization: Bearer $TOKEN" https://osvauco-agent-357036551735.us-central1.run.app/health
```
---
## 8. HVA SOM MÅ ENDRES NÅR GITEA-VM BLIR PERMANENT
| Hva | Nåværende verdi | Ny verdi |
|---|---|---|
| OLLAMA_BASE_URL | http://34.170.51.84:11434 | http://34.67.252.59:11434 (eller internt DNS) |
| GITEA_URL | http://34.170.51.84:3000 | http://git.vauco.no (når DNS) |
| opax-mcp.yaml | Mangler GITEA_URL | Legg inn permanent og rebuild |
---
## 9. ÅPNE PUNKTER FØR MIGRERING
- [ ] `GITEA_URL` må legges permanent inn i `opax-mcp.yaml` i repo (nå bare satt runtime)
- [ ] `OLLAMA_BASE_URL` må peke til riktig VM (avklar om dev-snap eller Gitea-VM er master)
- [ ] `git.vauco.no` DNS må settes opp (ref: docs/DNS-OG-INFRASTRUKTUR.md)
- [ ] Verifiser at Gitea-CPU-VM har alle nødvendige modeller for produksjonsbruk
- [ ] GPG-signering av Gitea-commits (valgfritt)

272
docs/CI_FIRST_RUN.md Normal file
View File

@ -0,0 +1,272 @@
# OSVx MCP — CI First Run (Gitea Webhook)
This is a minimal, commands-only runbook to set up the Gitea webhook CI/CD for staging deploys.
Assumes you are on the VM as `chris_christiansen` with the repo at `~/OSVauco`.
### 0. Prerequisites
```bash
cd ~/OSVauco
git status
git rev-parse --abbrev-ref HEAD
which python3
which gcloud
gcloud config get-value project
```
**Ensure:**
- Youre on `feat/osvx-mcp-full-catalog`
- `gcloud` project is `propane-will-491900-m5`
### 1. Create the webhook listener script
```bash
mkdir -p ci
cat > ci/deploy-webhook-listener.py << 'EOF'
#!/usr/bin/env python3
import hashlib
import hmac
import json
import os
import subprocess
from http.server import BaseHTTPRequestHandler, HTTPServer
PORT = int(os.getenv("WEBHOOK_PORT", "9999"))
WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "")
REPO_PATH = os.getenv("REPO_PATH", "/home/chris_christiansen/OSVauco")
CI_BRANCH = os.getenv("CI_BRANCH", "feat/osvx-mcp-full-catalog")
DEPLOY_SCRIPT = os.getenv("DEPLOY_SCRIPT", "./deploy-mcp.sh")
DEPLOY_ENV = os.getenv("DEPLOY_ENV", "staging")
def verify_signature(payload_bytes, signature_header):
if not signature_header or not signature_header.startswith("sha256="):
return False
expected_sig = signature_header.split("=", 1)[1]
computed = hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
payload_bytes,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(computed, expected_sig)
class WebhookHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
print(f"[webhook] {args[0]}", flush=True)
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
payload_bytes = self.rfile.read(content_length)
signature = self.headers.get("X-Gitea-Delivery", "")
if WEBHOOK_SECRET and not verify_signature(payload_bytes, signature):
self.send_response(401)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"error": "invalid signature"}).encode())
return
try:
payload = json.loads(payload_bytes.decode("utf-8"))
except Exception:
self.send_response(400)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"error": "invalid json"}).encode())
return
ref = payload.get("ref", "")
after = payload.get("after", "")
if after == "0000000000000000000000000000000000000000":
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"status": "ignored deletion"}).encode())
return
expected_ref = f"refs/heads/{CI_BRANCH}"
if ref != expected_ref:
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(
json.dumps({"status": "ignored", "reason": "wrong branch", "ref": ref}).encode()
)
return
self.send_response(202)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(
json.dumps({
"status": "deploying",
"branch": CI_BRANCH,
"env": DEPLOY_ENV,
}).encode()
)
try:
subprocess.run(["git", "-C", REPO_PATH, "pull"], check=True, capture_output=True, text=True)
subprocess.run([DEPLOY_SCRIPT, DEPLOY_ENV], cwd=REPO_PATH, check=True)
except Exception as e:
print(f"[webhook] deploy failed: {e}", flush=True)
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"status": "ok", "service": "osvx-mcp-webhook"}).encode())
def main():
if not WEBHOOK_SECRET:
print("WARNING: WEBHOOK_SECRET not set; webhook signature verification disabled.", flush=True)
server = HTTPServer(("0.0.0.0", PORT), WebhookHandler)
print(f"[webhook] listening on port {PORT}", flush=True)
server.serve_forever()
if __name__ == "__main__":
main()
EOF
chmod +x ci/deploy-webhook-listener.py
```
### 2. Generate webhook secret
```bash
export WEBHOOK_SECRET="$(openssl rand -hex 16)"
echo "WEBHOOK_SECRET=$WEBHOOK_SECRET"
```
Save this value; youll need it for Gitea and systemd.
### 3. Quick manual test (optional)
```bash
export WEBHOOK_PORT=9999
export REPO_PATH="/home/chris_christiansen/OSVauco"
export CI_BRANCH="feat/osvx-mcp-full-catalog"
export DEPLOY_SCRIPT="./deploy-mcp.sh"
export DEPLOY_ENV="staging"
python3 ci/deploy-webhook-listener.py &
sleep 1
curl http://127.0.0.1:9999
# Expect: {"status":"ok","service":"osvx-mcp-webhook"}
kill %1 2>/dev/null || true
```
### 4. Create systemd service
```bash
sudo tee /etc/systemd/system/osvx-mcp-webhook.service > /dev/null << EOF
[Unit]
Description=OSVx MCP Gitea Webhook Listener
After=network.target
[Service]
Type=simple
User=chris_christiansen
Group=chris_christiansen
WorkingDirectory=/home/chris_christiansen/OSVauco
Environment="WEBHOOK_SECRET=$WEBHOOK_SECRET"
Environment="WEBHOOK_PORT=9999"
Environment="REPO_PATH=/home/chris_christiansen/OSVauco"
Environment="CI_BRANCH=feat/osvx-mcp-full-catalog"
Environment="DEPLOY_SCRIPT=./deploy-mcp.sh"
Environment="DEPLOY_ENV=staging"
ExecStart=/usr/bin/python3 /home/chris_christiansen/OSVauco/ci/deploy-webhook-listener.py
Restart=always
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
```
### 5. Enable and start the service
```bash
sudo systemctl daemon-reload
sudo systemctl enable osvx-mcp-webhook
sudo systemctl start osvx-mcp-webhook
sudo systemctl status osvx-mcp-webhook --no-pager
```
Verify:
```bash
curl http://127.0.0.1:9999
journalctl -u osvx-mcp-webhook -n 20 --no-pager
```
Expected health response:
```json
{"status":"ok","service":"osvx-mcp-webhook"}
```
### 6. Configure Gitea webhook
In Gitea UI for `chris/osvauco`:
- Settings → Webhooks → Add Webhook
- Payload URL: `http://<VM_IP>:9999/`
- Secret: `$WEBHOOK_SECRET` (the value from step 2)
- Events: “Push events”
- Save
Optionally restrict to branch `feat/osvx-mcp-full-catalog` if supported.
### 7. Test with a real push
From your dev machine:
```bash
cd ~/OSVauco
git add .
git commit -m "ci: test webhook deploy"
git push origin feat/osvx-mcp-full-catalog
```
On the VM, watch logs:
```bash
journalctl -u osvx-mcp-webhook -f
```
You should see:
- Webhook received
- `git pull`
- `./deploy-mcp.sh staging` running
Then verify in GCP:
```bash
gcloud run services describe osvx-mcp-staging --project=propane-will-491900-m5 --region=us-central1
```
Check the last deployment timestamp and revision.
### 8. Manual prod deploy (after staging is validated)
```bash
cd ~/OSVauco
./deploy-mcp.sh prod
```
Verify:
```bash
gcloud run services describe osvx-mcp-prod --project=propane-will-491900-m5 --region=us-central1
```
### 9. Troubleshooting (quick)
- **Health check fails:**
```bash
systemctl status osvx-mcp-webhook --no-pager
journalctl -u osvx-mcp-webhook -n 50 --no-pager
```
- **Webhook not received:**
- Check Gitea webhook “Recent Deliveries”.
- Ensure `http://<VM_IP>:9999` is reachable from the Gitea server.
- **Deploy fails:**
- Run `./deploy-mcp.sh staging` manually to confirm it works.
- Check GCP credentials and permissions.
*This runbook implements the CI/CD strategy described in `CI_GITEA_WEBHOOK.md` and `PLATFORM_MAP.md`.*

199
docs/CUSTOMER_API.md Normal file
View File

@ -0,0 +1,199 @@
# OSVauco Customer API Documentation
This document outlines the public-facing API for the OSVauco Agent Platform (OPAX).
## Base URL
All API endpoints are relative to the base URL provided for your customer instance.
```
https://<your-instance>.vauco.no/
```
## Authentication
Authentication is handled via Google IAP (Identity-Aware Proxy). Ensure you have been granted access and are logged into your Google account. API calls must include an identity token in the `Authorization` header.
## Common Concepts
* **mode**: Determines the underlying AI model used.
* `light`: Uses a faster, more cost-effective model (e.g., Gemini Flash). Suitable for most tasks.
* `heavy`: Uses a more powerful, advanced model (e.g., Gemini Pro). Suitable for complex reasoning. Access may be restricted.
* **session_id**: A unique identifier to group a series of related API calls into a single conversation.
---
## Endpoints
### Health Check
Check the operational status of the service.
`GET /health`
**Description:**
A simple endpoint to verify that the API is running and accessible.
**Responses:**
* `200 OK`: The service is healthy.
```json
{
"status": "ok"
}
```
**Example:**
```bash
curl https://<your-instance>.vauco.no/health
```
---
### Run Agent
Execute a standard, single-turn agent request.
`POST /run`
**Description:**
This is the primary endpoint for interacting with the agent. It takes a user message and returns the agent's response.
**Request Body (`application/json`):**
| Field | Type | Description | Required | Default |
|---|---|---|---|---|
| `message` | string | The input text or prompt for the agent. | Yes | |
| `user_id` | string | A unique identifier for the end-user. | No | `opax` |
| `session_id`| string | An identifier for the conversation session. | No | `default` |
| `mode` | string | The processing mode (`light` or `heavy`). | No | `light` |
**Example Request Body:**
```json
{
"message": "Summarize the latest project status.",
"user_id": "customer-user-123",
"session_id": "session-abc-456",
"mode": "light"
}
```
**Responses:**
* `200 OK`: The agent processed the request successfully.
* `400 Bad Request`: The request was malformed (e.g., invalid `mode`).
* `403 Forbidden`: The `user_id` is not authorized for the requested `mode`.
* `500 Internal Server Error`: An unexpected error occurred during processing.
**Example Success Response (`200 OK`):**
```json
{
"response": "The project is on track. The CI/CD pipeline is complete, but the Dialogflow agent setup is currently blocked."
}
```
**Example `curl` Request:**
```bash
curl -X POST
https://<your-instance>.vauco.no/run
-H "Content-Type: application/json"
-H "Authorization: Bearer $(gcloud auth print-identity-token)"
-d '{
"message": "What is the capital of Norway?",
"user_id": "example-user",
"mode": "light"
}'
```
---
### Run Agent DAG
Execute a Directed Acyclic Graph (DAG) of multiple agent requests in parallel.
`POST /run/dag`
**Description:**
This endpoint allows for the concurrent execution of multiple independent agent prompts. It is useful for batch processing or when multiple pieces of information are needed simultaneously.
**Request Body (`application/json`):**
| Field | Type | Description | Required | Default |
|---|---|---|---|---|
| `messages` | array[string] | A list of input prompts for the agents. | Yes | |
| `user_id` | string | A unique identifier for the end-user. | No | `opax` |
| `session_id`| string | An identifier for the conversation session. | No | `default` |
| `mode` | string | The processing mode (`light` or `heavy`). | No | `light` |
| `scheduler` | string | The execution scheduler (`threads` or `processes`). | No | `threads` |
**Example Success Response (`200 OK`):**
```json
{
"total_duration_s": 5.72,
"results": [
{
"index": 0,
"message": "What is the current status of the frontend ticket?",
"response": "The frontend ticket is in review.",
"success": true,
"duration_s": 4.81,
"error": null
},
{
"index": 1,
"message": "Are there any new infrastructure alerts?",
"response": "No new infrastructure alerts.",
"success": true,
"duration_s": 3.95,
"error": null
}
]
}
```
---
## Diagnostics & State API
These endpoints provide access to the internal state and telemetry of the agent system. They are useful for monitoring, debugging, and advanced use cases.
### Get Full State
`GET /state`
Retrieves a JSON snapshot of the entire internal state store.
---
### List Tracked Agents
`GET /state/agents`
Returns a list of all agent IDs currently being tracked in the state store.
---
### Aggregate State Key
`GET /state/aggregate/{key}`
Performs an aggregation on a specific key across all agents in the state store.
**Path Parameters:**
| Name | Type | Description |
|---|---|---|
| `key` | string | The state key to aggregate (e.g., `last_duration_s`). |
---
### Get Telemetry History
`GET /telemetry/history`
Retrieves a history of recent agent telemetry events.
**Query Parameters:**
| Name | Type | Description | Default |
|---|---|---|---|
| `limit` | integer | The maximum number of events to return. | 50 |

71
docs/CostCheckProtocol.md Normal file
View File

@ -0,0 +1,71 @@
# OSVauco Cost Check Protocol
> Implements OSVauco-NMTMD-GCOS § Cost Management — sist oppdatert 2026-05-23
## Når skal 09-cost-check.sh kjøres?
| Situasjon | Handling |
|---|---|
| Før du starter arbeid for dagen | Kjør for se hva som fortsatt kjører fra forrige sesjon |
| Agenten booter og `MEMORY_ENGINE_NAME` er tom | Kjør for sjekke om instans allerede finnes |
| Før teardown | Kjør for bekrefte hva som vil bli slettet |
| Mistanke om uventet kostnad | Kjør umiddelbart |
## Kjør protokollen
```bash
cd ~/OSVauco
source .env
bash infrastructure/09-cost-check.sh
```
## Hva den sjekker
| Punkt | Ressurs | Koster løpende? |
|---|---|---|
| [1] | Cloud Run services | Ja, per request + CPU |
| [2] | Reasoning Engines / Memory Bank | **Ja, kontinuerlig** |
| [3] | Artifact Registry | Litt, per GB lagret |
| [4] | RAG Corpora | Minimal |
| [5] | GCS Buckets | Minimal |
| [6] | Budget alerts | Sjekker at varsler er aktive |
## Memory Bank — spesialregel
Memory Bank (Vertex AI Reasoning Engine) **slettes ikke av `03-teardown.sh`** og
faktureres kontinuerlig:
- **$0.0994/vCPU-time**
- **$0.0105/GiB-time**
- Gratis tier: 180 000 vCPU-sekunder/mnd
Hvis `09-cost-check.sh` punkt [2] viser aktive instanser du ikke trenger:
```bash
gcloud ai reasoning-engines delete ENGINE_NAME \
--project=$PROJECT_ID --region=$REGION --quiet
```
## Agentprotokoll
Alle agenter som bruker Memory Bank skal ved boot sjekke:
```python
import os
if not os.getenv("MEMORY_ENGINE_NAME"):
raise RuntimeError(
"MEMORY_ENGINE_NAME ikke satt i .env. "
"Kjør: source .env && bash infrastructure/09-cost-check.sh "
"for å se eksisterende instanser."
)
```
## Teardown-regel (obligatorisk)
```bash
# Hver kveld:
source .env
bash infrastructure/09-cost-check.sh # se hva som kjører
bash infrastructure/03-teardown.sh # rydd ned Cloud Run + IAM
# Memory Bank: slett manuelt hvis ikke i bruk
```

View File

@ -0,0 +1,67 @@
# Cost Management Rules — propane-will-491900-m5
## Hard rules
1. **Billing budget alert** must be created BEFORE any resource deployment.
Alert thresholds: 50% / 75% / 90% / 100% of monthly budget.
Start with 5 USD (~55 NOK) for dev/test phases.
2. **Agent Runtimes** cost money when deployed — always undeploy via `03-teardown.sh` at end of workday.
3. **Vertex AI Endpoints** — same rule as above.
4. **Staging bucket lifecycle**: auto-delete objects older than 7 days.
5. **Colab Enterprise runtimes** — terminate when not actively developing.
6. GCP does NOT enforce a hard spend cap. Budget alerts are email-only by default.
## Agent Runtime pricing (May 2026)
| Resource | Free Tier/month | Paid rate |
|---|---|---|
| vCPU | 180,000 vCPU-seconds | ~$0.0864/vCPU-hour |
| RAM | 360,000 GiB-seconds | ~$0.0090/GiB-hour |
Agent Runtime does NOT bill for idle/non-running agents (pay-per-use). Always delete at end of session.
## NEW billing lines (effective Jan 28, 2026)
These were previously free/in preview and are now METERED:
- **Sessions** — agent interaction contexts
- **Memory Bank** — long-term memory storage
- **Code Execution** — sandboxed code runs
→ Trim session histories. Only persist high-value facts to Memory Bank. Disable Code Execution on agents that don't need it.
## Cloud Run cost profile
- `--min-instances=0` → scales to zero — no idle cost (use for dev/test)
- `--min-instances=1` → keeps one warm instance — eliminates cold-start (~$48/month)
- Cleanup: `gcloud run services delete SERVICE_NAME --region=us-central1 --quiet`
## Lifecycle JSON for staging bucket
```json
{
"rule": [
{
"action": { "type": "Delete" },
"condition": { "age": 7 }
}
]
}
```
```bash
gcloud storage buckets update gs://propane-will-491900-m5-agent-staging \
--lifecycle-file=lifecycle.json
```
## RAG Engine cost warning
- If RAG Engine uses a managed Spanner instance as vector DB → billed 24/7.
- For dev/test: prefer `us-east1` or `us-east4` until us-central1 allowlist is granted.
- Contact: `vertex-ai-rag-engine-support@google.com` for us-central1 allowlist access.
## Artifact Registry cleanup
```bash
gcloud artifacts docker images list \
us-central1-docker.pkg.dev/propane-will-491900-m5/oavauco-docker \
--format="value(IMAGE,DIGEST)" | tail -n +2 | \
while read -r image digest; do
gcloud artifacts docker images delete "${image}@${digest}" --quiet
done
```
## Pub/Sub hard-stop billing automation (optional)
Consider: Pub/Sub → Cloud Run Function to auto-disable billing if budget alert fires.
Template: https://cloud.google.com/billing/docs/how-to/notify#cap_disable_billing_to_stop_usage

View File

@ -0,0 +1,233 @@
# DNS & INFRASTRUKTUR — Vauco OS
**Versjon:** 2026-06-10 | **Eier:** Chris Christiansen | **Status:** Autoritativ
---
## KRITISK: Slik er infrastrukturen bygget
### Load Balancer — Hoved-IP
```
34.98.77.173 ← ALLE subdomener som skal ha IAP må peke hit (A-record)
```
### Forwarding rules
| Navn | IP | Target proxy |
|------|-----|-------------|
| `osvauco-agent-forwarding-rule` | `34.98.77.173` | `osvauco-agent-https-proxy` |
| `vauco-os-https` | `34.144.224.45` | `vauco-os-https-proxy` |
| `vauco-tui-bridge-forwarding-rule` | `34.117.108.44` | `vauco-tui-bridge-https-proxy` |
### URL maps
| Navn | Backend | IAP |
|------|---------|-----|
| `osvauco-agent-url-map` | `osvauco-agent-iap-backend` | ✅ JA |
| `vauco-os-urlmap` | `vauco-os-backend` | ❌ NEI |
| `vauco-tui-bridge-url-map` | `vauco-tui-bridge-backend` | ❌ NEI |
### SSL-sertifikater på osvauco-agent-https-proxy
| Sertifikat | Domene | Status |
|-----------|--------|--------|
| `osvauco-agent-ssl-cert` | `costguard.oss.vauco.no` | ✅ ACTIVE |
| `opax-vauco-cert` | `opax.vauco.no` | ✅ ACTIVE (provisjonert 2026-05-30) |
> ⚠️ Når du legger til nytt subdomene må du alltid oppdatere `--ssl-certificates` med ALLE eksisterende + nytt.
> Eksempel: `--ssl-certificates=osvauco-agent-ssl-cert,opax-vauco-cert,NYTT-cert`
**STATUS (2026-07-07): `git.vauco.no` er BLOKKERT**
Diagnose avdekket en fundamental feilkonfigurasjon:
* **OK:** DNS (`git.vauco.no` → `34.144.224.45`), SSL-sertifikat (`git-vauco-cert`), og routing til URL-map er i orden.
* **AVVIK:** Backend-tjenesten `vauco-os-backend` peker til feil mål: en Serverless NEG for Cloud Run-tjenesten `osvauco-agent`, ikke Gitea-VM-en.
* **BLOCKER:** Den forventede Gitea-VM-en (`osvauco-dev-vm`) ble ikke funnet.
**Konklusjon:**
Gitea-endepunktet via `git.vauco.no` er **ikke-fungerende**. All Fase 2-koding som avhenger av et live Gitea-endepunkt er blokkert. Ingen flere endringer på lastbalanserer eller backends skal gjøres nå.
---
## REGEL: Nytt subdomene = gjør dette (i denne rekkefølgen)
### Steg 1 — GCP: Opprett SSL-sertifikat
```bash
gcloud compute ssl-certificates create NAVN-cert \
--domains=subdomene.vauco.no \
--project=propane-will-491900-m5
```
### Steg 2 — GCP: Legg til sertifikat på proxy (MED alle eksisterende)
```bash
gcloud compute target-https-proxies update osvauco-agent-https-proxy \
--ssl-certificates=osvauco-agent-ssl-cert,opax-vauco-cert,NAVN-cert \
--project=propane-will-491900-m5
```
### Steg 3 — DNS hos Proisp (manuelt, én gang)
```
Type: A
Navn: subdomene.vauco.no
Verdi: 34.98.77.173
TTL: 300
```
### Steg 4 — Vent på sertifikat-provisjonering (10-20 min)
```bash
# Sjekk status
gcloud compute ssl-certificates describe NAVN-cert \
--project=propane-will-491900-m5 \
--format="json(managed.domainStatus,managed.status)"
```
> ⚠️ ALDRI bruk CNAME til ghs.googlehosted.com for IAP-beskyttede tjenester.
> CNAME til ghs = Cloud Run domain mapping = bypasser Load Balancer = ingen IAP.
---
## DOMENE-ROLLEMODELL (Låst)
| Domene | Tildelt Rolle | Status |
|-----------|----------|--------|
| `git.vauco.no` | Gitea (kode-repo) | **Planlagt** |
| `opax.vauco.no`| OPAX-MCP (agent-gateway)| **Aktiv** |
| `ops.vauco.no` | Ikke i bruk | **Parkert** |
---
## DAGENS SUBDOMENER — Status
| Subdomene | DNS type | Peker til | SSL-cert | IAP | Status |
|-----------|----------|-----------|----------|-----|--------|
| `opax.vauco.no` | A | `34.98.77.173` | `opax-vauco-cert` | ✅ | ✅ Fungerer |
| `costguard.oss.vauco.no` | A | `34.98.77.173` | `osvauco-agent-ssl-cert` | ✅ | ✅ Fungerer |
| `oss.vauco.no` | - | - | - | - | 🔮 Fase C |
| `os.vauco.no` | - | - | - | - | 🔮 Fase C |
| `threadstone.vauco.no` | CNAME | GitHub Pages | GitHub | ❌ | ⚠️ Midlertidig — migreres til GCS + GCP LB (T1) |
| `app.threadstone.vauco.no` | CNAME | GitHub Pages | GitHub | ❌ | ⚠️ Midlertidig — migreres til GCS + GCP LB (T1) |
> ⚠️ `threadstone.vauco.no` og `app.threadstone.vauco.no` er fortsatt på GitHub Pages inntil T1 er fullført.
> Ved migrering: opprett SSL-cert, pek DNS A-record til `34.98.77.173`, sett opp GCS-bucket + LB-backend.
---
## OAUTH / IAP — Konfigurasjon
| Parameter | Verdi |
|-----------|-------|
| OAuth brand | `projects/357036551735/brands/357036551735` |
| Publishing status | **Testing** (ikke publiser til Production) |
| User type | **External** |
| Test-brukere | `chris.christiansen@vauco.no` |
| IAP IAM | `chris.christiansen@vauco.no``roles/iap.httpsResourceAccessor` |
| IAP IAM | `jason-vauger@propane-will-491900-m5.iam.gserviceaccount.com``roles/iap.httpsResourceAccessor` (lagt til 2026-06-17) |
| IAP backend | `opax.vauco.no``osvauco-agent-iap-backend` (ikke `vauco-os-backend`). DNS: `34.98.77.173``osvauco-agent-url-map``osvauco-agent-iap-backend` |
> Testing-status med External er korrekt og trygt for intern bruk.
> Maks 100 test-brukere — mer enn nok for Vauco OS.
---
## GCP PROSJEKT
| Parameter | Verdi |
|-----------|-------|
| Project ID | `propane-will-491900-m5` |
| Project number | `357036551735` |
| Region (hoved) | `us-central1` |
| Cloud Run service | `osvauco-agent` |
| Cloud Run URL | `https://osvauco-agent-357036551735.us-central1.run.app` |
---
## FREMTIDIG: Flytt DNS til Google Cloud DNS
Når tid tillater — flytt `vauco.no` nameservere til Google Cloud DNS.
Da kan alle DNS-endringer gjøres med `gcloud`-kommandoer, og nytt subdomene er fullt automatisert.
```bash
# Opprett zone
gcloud dns managed-zones create vauco-no \
--dns-name=vauco.no. \
--description="Vauco DNS zone" \
--project=propane-will-491900-m5
# Nytt subdomene = én kommando
gcloud dns record-sets create subdomene.vauco.no. \
--zone=vauco-no \
--type=A \
--ttl=300 \
--rrdatas=34.98.77.173 \
--project=propane-will-491900-m5
```
**Prosessen:** Hent NS-records fra GCP → lim inn hos Proisp → propagering 24-48t.
**Trigger:** Gjør dette etter CG3 er live og første kunde er onboardet.
---
*Opprettet: 2026-05-30 | Oppdatert: 2026-06-10 | OSVauco | propane-will-491900-m5*
---
## Ny DNS-strategi: Minimal Live-sone i Cloud DNS (2026-07-07)
**Beslutning:** Vi speiler ikke ProISP-sonen 1:1. Den behandles som historikk. En ny, minimal sone bygges bevisst opp i Google Cloud DNS (`vaucono`) for å bli den fremtidige autoritative kilden. Ingen nameserver-bytte skjer ennå.
### Sammenligning og Fremtidig Status for DNS-Records
#### Gruppe 1: E-post/autentisering (Beholdes og verifiseres)
*Disse er standard for Google Workspace og er kritisk for e-postflyt.*
| Navn | Type | Ønsket Verdi | Planlagt `gcloud`-kommando |
| :--- | :--- | :--- | :--- |
| `vauco.no.` | MX | Standard Google MX-records. | `transaction add --name="vauco.no." --type=MX --ttl=3600 "1 smtp.google.com." "5 alt1.smtp.google.com." ...` |
| `vauco.no.` | TXT | `v=spf1 include:_spf.google.com ~all` | *(Eksisterer allerede i Cloud DNS)* |
| `google._domainkey` | TXT | (DKIM-nøkkel fra Google) | *(Eksisterer allerede i Cloud DNS)* |
| `_dmarc.vauco.no.`| TXT | (DMARC-policy) | *(Eksisterer allerede i Cloud DNS)* |
#### Gruppe 2: Beholdes til Live (Kjerne-infrastruktur)
*Disse peker til aktiv, strategisk infrastruktur.*
| Navn | Type | Ønsket Verdi | Planlagt `gcloud`-kommando |
| :--- | :--- | :--- | :--- |
| `opax.vauco.no.`| A | `34.98.77.173` (IAP LB IP) | `transaction add --name="opax.vauco.no." --type=A --ttl=300 "34.98.77.173"` |
| `vauco.no.` | A | Fremtidig web-host / statisk side | `...` |
| `www.vauco.no.` | A | Fremtidig web-host / statisk side | `...` |
#### Gruppe 3: Beslutning Kreves
*Disse er strategiske, men avhenger av eksterne faktorer før de kan låses.*
| Navn | Type | Ønsket Verdi | Status |
| :--- | :--- | :--- | :--- |
| `git.vauco.no.`| A | (Stabil, ekstern IP til dev-VM) | **BLOKKERT:** Venter på at nettverk/brannmur er bekreftet. |
#### Gruppe 4: Legacy / Eksperiment (Skal ikke migreres nå)
*Disse subdomenene er fra tidligere eksperimenter og skal ikke opprettes i den nye sonen med mindre et konkret behov dokumenteres.*
- `blackbriar.vauco.no`
- `vaultconnection.vauco.no`
- `n8n.vauco.no`
- `scout.vauco.no`
- `stage.vauco.no`
- `costguard.oss.vauco.no`
- `threadstone.vauco.no` (og andre GitHub Pages CNAMEs)
### Oppsummering av Plan
Cloud DNS-sonen er ufullstendig. For å gjøre den klar, må vi:
1. Legge til korrekte MX-records for Google Workspace.
2. Endre `opax.vauco.no` fra CNAME til en A-record som peker på IAP Load Balancer-IP-en.
3. Klargjøre en midlertidig, statisk host for `vauco.no` og `www` og legge inn A-records for disse.
4. Avklare og sette en stabil, ekstern IP for `git.vauco.no`.
5. **Ikke** migrere noen av de gamle "Legacy/Eksperiment"-subdomenene.
---
### Gemini-bekreftelse 2026-07-07
Jeg bekrefter med dette min forståelse av den nye, minimale DNS-strategien:
**1. Ønsket sluttmodell for kjerne-domener:**
* **`opax.vauco.no`:** Skal være en A-record som peker direkte til IAP Load Balancer-IP-en (`34.98.77.173`), for å sikre korrekt IAP-flyt. Dette er en endring fra dagens CNAME i Cloud DNS-sonen.
* **`git.vauco.no`:** Skal være en A-record som peker til den stabile, eksterne IP-adressen til Gitea-serveren (dev-VM). Dette er for øyeblikket blokkert til nettverk/IP er avklart.
* **`vauco.no` / `www.vauco.no`:** Er definert som fremtidig hovedinngang/portal. Innholdet er ikke en prioritet nå, men A-records for disse må være en del av den Google-styrte sonen før bytte.
**2. Forutsetninger før nameserver-bytte:**
Før `vauco.no` kan bytte navnetjenere til Google Cloud DNS, må følgende fem punkter være fullført i `vaucono`-sonen:
1. Alle nødvendige MX-records for Google Workspace må være lagt inn.
2. `opax.vauco.no` må være korrigert fra CNAME til A-record.
3. En midlertidig host for `vauco.no` og `www.vauco.no` er klargjort og tilhørende A-records er lagt inn.
4. En stabil IP for `git.vauco.no` må være satt.
5. Det er bekreftet at ingen "legacy"-domener skal migreres.

129
docs/GCP_Best_Practices.md Normal file
View File

@ -0,0 +1,129 @@
# GCP Best Practices for Multi-Agent AI Systems
**OSVauco-NMTMD-GCOS | Last Updated: 2026-05**
## 1. Multi-Agent Architecture Patterns
### 1.1 Coordinator + Sub-Agent Pattern (ADK)
Use a root orchestrator agent that delegates to specialized sub-agents.
```python
root_agent = Agent(
name="orchestrator",
model="gemini-2.0-flash",
sub_agents=[greeting_agent, rag_agent, memory_agent, farewell_agent],
instruction="Delegate tasks to the appropriate sub-agent.",
before_model_callback=before_model_callback,
before_tool_callback=before_tool_callback,
)
```
### 1.2 Graph Workflow Pattern
- SequentialAgent: ordered pipelines
- ParallelAgent: concurrent sub-tasks
- LoopAgent: retry/polling patterns
### 1.3 Session State and Memory
- Use session.state for short-lived conversation context
- Use Vertex AI Memory Bank for persistent long-term memory
- Tag memories with user_id and session_id for retrieval scoping
## 2. Model Selection
| Use Case | Recommended Model |
|----------|------------------|
| Complex reasoning / orchestration | gemini-2.0-flash or gemini-2.5-pro |
| Fast sub-agent calls | gemini-2.0-flash |
| Non-Google models | LiteLLM via LiteLlmModel |
| Embedding generation | text-embedding-005 |
| Code generation | gemini-2.5-pro |
## 3. Tool Design
- Single-responsibility per tool function
- Full docstrings — ADK uses these for LLM instructions
- Validate inputs inside tools; never trust LLM output blindly
- Use before_tool_callback for argument guardrails
- Return structured dicts, not raw strings
## 4. Vertex AI Integration
### 4.1 RAG Engine
**CONFIRMED** (source: Vertex AI RAG Engine SDK, google/adk-python samples, 2026-05):
- Use RagCorpus for document grounding
- Embedding model: text-embedding-005
- Chunking: 512 tokens, 50-token overlap
- Retrieval: top_k=5, vector_distance_threshold=0.7
**UNKNOWN / UNDOCUMENTED** (as of 2026-05):
- Full regional availability (eu-west3 issues known)
- Whether rag.import_files() is idempotent for same GCS path
- See: docs/Doc_Gaps_and_Open_Questions.md OQ-02
### 4.2 Memory Bank
**CONFIRMED** (source: Gemini Enterprise Agent Platform Memory Bank setup, 2026-05):
- load_memory_tool + preload_memory_tool from google.adk.tools
- Required role: roles/aiplatform.user
- SDK: google-cloud-aiplatform >= 1.111.0
**UNKNOWN / UNDOCUMENTED** (as of 2026-05):
- gcloud CLI equivalent for client.agent_engines.create()
- Whether tools auto-discover Memory Bank instance via project/region
- See: docs/Doc_Gaps_and_Open_Questions.md OQ-01, OQ-06
### 4.3 Security Controls Matrix
| Component | Data Residency | CMEK | VPC-SC | Access Transparency |
|-----------|---------------|------|--------|-------------------|
| Agent Platform | YES | YES | YES | YES |
| RAG Engine | NO | NO | YES | NO |
| Vector Search | YES | NO | YES | NO |
## 5. Observability
- Cloud Trace: end-to-end request tracing
- Cloud Logging: structured JSON logs with session_id; never log PII
- Cloud Monitoring: latency p50/p95/p99, error rates
- Error Reporting: auto-alert on new exception types
## 6. Deployment
- Cloud Run services (serverless, auto-scaling)
- Artifact Registry for container images
- min-instances=1 for latency-sensitive agents
- Cloud Build for CI/CD; --no-traffic deploy + HITL traffic switch
- Label all resources: env, team, cost-center, agent
## 7. MCP Integration
```python
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters
tools, exit_stack = await MCPToolset.from_server(
connection_params=StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
)
)
```
## 8. A2A Protocol
- Agents expose AgentCard at /.well-known/agent.json
- A2A enables multi-vendor agent interoperability
- Secure A2A calls with service account tokens + VPC-SC
## 9. Handling Missing or Incomplete GCP Documentation
*Implements OSVauco-NMTMD-GCOS section 1.5.*
| Situation | Action |
|-----------|--------|
| API confirmed in GA docs or stable samples | Use it; cite source |
| API only in preview docs / blog post | Mark PREVIEW — validate before prod |
| API not found in any authoritative source | Omit; add to Open Questions |
## References
- ADK: https://google.github.io/adk-docs/
- Vertex AI RAG: https://cloud.google.com/vertex-ai/generative-ai/docs/rag-overview
- Gemini Enterprise Agent Platform: https://cloud.google.com/gemini-enterprise-agent-platform
- A2A: https://google.github.io/A2A/

390
docs/GDRIVE_MIRROR_BOOT.md Normal file
View File

@ -0,0 +1,390 @@
# VAUCO OS — GDRIVE MIRROR BOOT
> **CONNECTOR-PRIORITET:**
> ```
> GITHUB > GOOGLE DRIVE
> ```
> GitHub (`OSVauco/main/docs/AGENT_RULEBOOK.md`) er **alltid** ground truth.
> Denne filen er auto-generert av `sync-gdrive-mirror.sh`.
> Ved enhver konflikt: stol på GitHub. Oppdater ved å kjøre scriptet på nytt.
---
## ⚠️ STALENESS-VARSEL — LES DETTE FØRST
```
╔══════════════════════════════════════════════════════════════╗
║ SIST SYNKRONISERT: 2026-05-26T03:02 UTC ║
║ HEAD: 5479a5f ║
║ Kilde: OSVauco/main · Synkronisert av: sync-gdrive-mirror.sh ║
╠══════════════════════════════════════════════════════════════╣
║ ⚠️ TERSKEL: 12 TIMER (aktiv dev-runde) ║
║ HVIS MER ENN 12T HAR GÅTT: STOPP og verifiser mot GitHub ║
╠══════════════════════════════════════════════════════════════╣
║ VERIFISER: https://github.com/vauco-saas/ ║
║ OSVauco/blob/main/docs/AGENT_RULEBOOK.md ║
╚══════════════════════════════════════════════════════════════╝
```
---
## PROSJEKTINDEKS — AUTO-GENERERT 2026-05-26T03:02 UTC · HEAD 5479a5f
---
## TODO
# OSVauco / OPAX — TODO
> Managed by PLAN thread. Update when phases move between sections. Last updated: 2026-05-25T14:37 CEST
---
## NOW
- [ ] **⚠️ HASTER** — Roter OAuth client secret `Vauco OS Web App` i [Google Auth Platform](https://console.cloud.google.com/auth/clients/357036551735-ka7t2fv9ue2jp01bs826hpdctlvispuo.apps.googleusercontent.com?project=propane-will-491900-m5) — eksponert i chat 2026-05-25.
- [ ] Vurder om `jason.vauger@vauco.no` skal beholde `roles/run.invoker``osvauco-agent`.
- [ ] Kjør `git update-index --chmod=+x` på alle scripts + commit — hindrer Permission denied etter GitHub web-edit.
- [ ] Verifiser første nattlige GDrive backup — kl 03:00 UTC (Actions-logg).
---
## NEXT
- [ ] **Phase 6** — LEARNING-010 append: Cloud Run domain mapping støtter ikke load balancer IAP.
- [ ] `opax-mcp/` restruktureres som ordentlig modul-subtree.
- [ ] Full boot cycle acceptance test (automatisert).
- [ ] VS Code ↔ Cloud Shell workflow — preflight-script for lokal utvikling.
- [ ] **Gemini TUI-output logging**`gemini-log` alias, append siste 50 linjer til session log.
- [ ] GDrive nightly workflow — verifiser schedule-trigger fungerer.
---
## DONE ✅
- [x] **Phase 0** — Baseline: repo, `.gemini/`-struktur, boot-script, sesjonslogg.
- [x] **Phase 1** — Domain/protocol locking, `ARCHITECTURE.md`, `VAUCO_OS_ROADMAP.md`.
- [x] **Phase 2**`opax.vauco.no` LIVE: Cloud Run, CNAME, SSL.
- [x] **Phase 3** — Nemotron session loop, `docs/TODO.md`, Phase 1+2 rapport.
- [x] **Phase 4** — Docs hardening: LEARNINGS 007-009, SECRETS-SETUP, IAP-SETUP kartlegging. (2026-05-25)
- [x] **Phase 5** — Cloud Run IAM-autentisering på `osvauco-agent`: kun `chris.christiansen@vauco.no` + `jason.vauger@vauco.no`. DNS urørt. (2026-05-25)
- [x] **GDrive/GitHub paritet** — mirror-script, nattlig backup, PS1 prompt, IAM/Secrets, opax-logg-slutt auto-push. (2026-05-25)
- [x] **GDrive backup IAM** — SA opprettet, JSON-nøkkel, mapper delt. (2026-05-25)
- [x] **GitHub Secrets**`RCLONE_GDRIVE_SA_JSON` + `GDRIVE_BACKUP_FOLDER_ID` satt. (2026-05-25)
- [x] **Første mirror-sync**`.gdrive-mirror-state` opprettet, `GDRIVE_MIRROR_BOOT.md` lastet opp. (2026-05-25)
- [x] **PS1 gul-bug** — state-fil skrives nå ETTER push, prompt grønn umiddelbart. (2026-05-25)
---
## SESJONSLOGG
# Sessions Index
| Dato | Emne | Fil |
|------|------|-----|
| 2026-05-21 | OSVauco/OPAX platform bootstrap — naming, domenekonvensjon, Nemotron TODO | [2026-05-21_osvauco-opax-platform-naming-domains.md](./2026-05-21_osvauco-opax-platform-naming-domains.md) |
| 2026-05-22 | osv-startup.ps1 — banner design, farger, prompt og gcloud-fix | [2026-05-22_osv-startup-banner-styling.md](./2026-05-22_osv-startup-banner-styling.md) |
| 2026-05-24 | OPAX heavy mode + LLM-ruting + RAG re-indeksering | [2026-05-24_opax-heavy-mode-llm-ruting-rag.md](./2026-05-24_opax-heavy-mode-llm-ruting-rag.md) |
| 2026-05-24 | RAG corpus setup — Vertex AI serverless debugging | [2026-05-24_rag-corpus-setup-vertexai.md](./2026-05-24_rag-corpus-setup-vertexai.md) |
| 2026-05-24 | RAG restriction + operator suite heavy lifts + docs hardening | [2026-05-24_osvauco-rag-operator-suite.md](./2026-05-24_osvauco-rag-operator-suite.md) |
| 2026-05-25 | GDrive/GitHub mirror + backup, PS1 prompt, opax-logg-slutt fix, IAM setup | [2026-05-25_gdrive-mirror-sync.md](./2026-05-25_gdrive-mirror-sync.md) |
| 2026-05-25 | GitHub boot + prosjektstatus-lesing + GDrive-review fra handoff | [2026-05-25_github-boot-status-gdrive-review.md](./2026-05-25_github-boot-status-gdrive-review.md) |
| 2026-05-25 | Phase 7 RAG corpus opprettet — sesjonsstatus-diff og handoff | [2026-05-25_phase7-rag-corpus-handoff.md](./2026-05-25_phase7-rag-corpus-handoff.md) |
| 2026-05-26 | RAG corpus kobling til osvauco-agent — boot, snapshot-sti-avklaring, index-oppdatering | [2026-05-26_rag-corpus-kobling-agent.md](./2026-05-26_rag-corpus-kobling-agent.md) |
| 2026-05-26 | Deploy + CI/CD handoff — B2/B3/B4/B5 gjestår, teardown-påminnelse | [2026-05-26_deploy-cicd-handoff.md](./2026-05-26_deploy-cicd-handoff.md) |
| 2026-05-26 | B2 + B3 fullført — Secret Manager verifisert, Cloud Build 2nd gen pipeline oppe | [2026-05-26_b2-b3-cicd-ferdig.md](./2026-05-26_b2-b3-cicd-ferdig.md) |
| 2026-05-26 | B3 trigger-test SUCCESS — revisjon 00065-z7t | [2026-05-26_b3-trigger-test-ok.md](./2026-05-26_b3-trigger-test-ok.md) |
| 2026-05-26 | Google Chat webhook + cloudbuild.yaml bash-fix — CI/CD komplett | [2026-05-26_google-chat-webhook-ferdig.md](./2026-05-26_google-chat-webhook-ferdig.md) |
| 2026-05-26 | B5 fullført — budget alert 500 NOK, GCS telemetri-bucket, logg-sink aktiv | [2026-05-26_b5-cost-guard-telemetri-ferdig.md](./2026-05-26_b5-cost-guard-telemetri-ferdig.md) |
| 2026-05-26 | B1-IAP fullført — opax.vauco.no live, CNAME ghs.googlehosted.com, IAM invoker satt | [2026-05-26_b1-iap-cname-ferdig.md](./2026-05-26_b1-iap-cname-ferdig.md) |
---
## ROADMAP
# VAUCO OS — ROADMAP.md
# Eier: PLAN-Claude-Architect
# Sist oppdatert: 2026-05-22 CEST
# Godkjent av: Chris Christiansen
---
## NOW — Aktiv nå
- [ ] **Phase 4** — Docs hardening
- Owner: OPS / Gemini TUI
- HITL gate: `MERGE APPROVED`
- Filer: `LEARNINGS.md`, `SECRETS-SETUP.md`, `IAP-SETUP.md` — oppdater med nåværende state
- Done: alle tre filer har seksjon `## Status 2026-05-22` med verifisert info
---
## NEXT — Neste opp
- [ ] **Phase 5** — IAP på `opax.vauco.no`
- Begrens til `@vauco.no` Google-kontoer
- Owner: EXEC-Gemini / Chris
- HITL gate: `DEPLOY GODKJENT`
- Done: uautentiserte requests → 403, `chris.christiansen@vauco.no` → 200
- [ ] **Phase 6** — VS Code ↔ Cloud Shell workflow
- Preflight-script + dokumentasjon for lokal utvikling mot OSVauco-repoet
- Owner: OPS
- HITL gate: `MERGE APPROVED`
---
## LATER — Backlog
- [ ] `opax-mcp/` restruktureres som ordentlig modul-subtree
- [ ] Full boot cycle acceptance test (automatisert)
- [ ] OPS agent + BigQuery live logging
- [ ] Multi-klient onboard-mønster
- [ ] **Medioteq** — kun idéfase, ingenting implementeres før OPAX er fullt operasjonelt
---
## BLOCKED
*(ingen blokkerte punkter)*
---
## ✅ Fullførte faser
- [x] **Phase 0** — Baseline: `opax-mcp-core` live, OSVauco repo, `.gemini/` grunnmur, boot-script, sessionslogg
- [x] **Phase 1** — MCP_NAME/MCP_PROTOCOL/ROOT_DOMAIN locked; ARCHITECTURE.md + ROADMAP.md opprettet · `6ae1a25` `4a971dd` `c4149e7` · 2026-05-21
- [x] **Phase 2**`opax.vauco.no` LIVE: domain mapping + CNAME + SSL + HUB_URL locked · `f3bf419` · 2026-05-21
- [x] **Phase 3** — Nemotron session loop i GEMINI.md; docs/TODO.md; Phase 1+2 SLUTTRAPPORT i session log · `77ed501` · 2026-05-21
---
EOF v2.0 — 2026-05-22 — synkronisert med faktisk repo-state
---
## AGENT_RULEBOOK
# VAUCO OS — AGENTRLEBOOK.md
# Eier: OPS-Computer-Hub
# Sist oppdatert: 2026-05-21 CEST
# Godkjent av: Chris Christiansen
---
## Hensikt
Immutable regler for alle 4 tråder. Ingen tråd kan overstyre disse.
Endringer krever eksplisitt godkjenning fra Chris.
---
## LOCK LIST — ALDRI ENDRE
| Parameter | Verdi |
|-------------------|-----------------------------------------------------------------------|
| GCP Account | chris.christiansen@vauco.no |
| Forbudte kontoer | tinius.vauger, ccv, jason.vauger |
| GCP Project | propane-will-491900-m5 (Jason-v1) |
| Billing | 0171F6-057E6B-A260BA |
| Org | vauco.no / 155051211891 |
| Region | us-central1 — ALDRI us-west1 |
| SDK | `from google import genai` — ALDRI `vertexai.generative_models` |
| Agent-rammeverk | Google ADK (tillatt) — andre må eksplisitt `ADK_GODKJENT` av Chris |
| GitHub user | vauco-saas |
| Canonical repo | OSVauco |
| Canonical branch | main |
| MCP_NAME | OPAX-MCP |
| MCP_PROTOCOL | OPAX Protocol |
| MCP_SERVICE | opax-mcp-core (Cloud Run, us-central1) |
| ROOT_DOMAIN | vauco.no |
| HUB_URL | https://opax.vauco.no ✅ LIVE 2026-05-21 |
---
## DOMENEKONVENSJON — LOCKED
| Subdomain | Type | Formål |
|-------------------------|----------|-----------------------------------------------------|
| `opax.vauco.no` | Hub/MCP | OPAX-MCP operator hub — kun Vauco internt |
| `<klient>-os.vauco.no` | Prod OS | Kundens live produksjonssystem |
| `<klient>-oss.vauco.no` | Stage OS | Kundens staging/demo-system (pre-go-live) |
> `-os` = produksjon. `-oss` = staging. Aldri omvendt.
**Auth-strategi:**
- Nå: Google OAuth på alle miljøer
- Senere: BankID på `-os`-domener for kliniske kunder (Medioteq først)
**Kjente/planlagte domener:**
| Domene | Status | Cloud Run-tjeneste |
|---------------------------|------------------|-----------------------|
| `opax.vauco.no` | ✅ LIVE | `opax-mcp-core` |
| `medioteq-oss.vauco.no` | Planlagt | `medioteq-oss-core` |
| `medioteq-os.vauco.no` | Planlagt | `medioteq-os-core` |
---
## HITL-GATES — STOPP OG SPØR ALLTID
1. `DEPLOY GODKJENT` — før deploy til Cloud Run
2. `MERGE APPROVED` — før merge til main
3. `DEEP DREAM GODKJENT` — før strategisk vokabular eksponeres
4. `COST GODKJENT` — før endringer som påvirker billing
5. `IAM_GODKJENT` — før IAM-endringer eller ny service account
6. `NEWREPO` — før opprettelse av ny repo
---
## BOOT-PROTOKOLL — kjøres av ALLE tråder ved oppstart
1. Branch-discovery: list alle branches, finn mest aktiv (ahead av default)
- Hvis 1+ branch er 10+ commits ahead: STOPP og spør Chris
- Standard: OSVauco/main er canonical for dette prosjektet
2. Les eid fil + MASTER_HANDOFF_v2 + ROADMAP fra riktig branch
3. Bekreft i én linje: `Boot OK — branch X, HEAD Y, oppgave Z`
---
## VOCABULARY FIREWALL
| Type | Ord | Regel |
|---|---|---|
| Offentlig (OK overalt) | Token Ledger, VVN, Provenance, File signature, Client identity record, Cognitive contribution, The system | Fritt bruk |
| Strategisk (KUN internt) | Pregeverdi, Deep Dream, Vauco-arv, Pregesignatur, T-konto, Work of Living, Pregeverk | KUN i `deep-dream/`-mappe |
---
## KOST-DISIPLIN
- $3/dag hard cap per tråd
- `gemini-2.5-flash` for enkle steg, `pro` for komplekse
- ALDRI rekursive modell-loops
- Hvis tråden bruker $1 på én oppgave: STOPP, rapporter, spør
- 80%/95%/100% quota stops aktive
---
## FRICTION-REGLER (kodifisert lærdom)
### TASK-FRICTION-001: Branch-discovery-before-read
Preflight må advare hvis non-default branch er 10+ commits ahead.
Implementer i: `scripts/preflight-check.sh`
### TASK-FRICTION-002: SYSTEM_STATE_AGENT_BOOT autogenerering
Boot-script skriver fersk state-fil ved hver tråd-oppstart.
Implementer i: `scripts/boot-state.sh` (EXEC eier)
### TASK-FRICTION-003: Eierskap-matrise håndhevet i pre-commit
Pre-commit hook avviser commit hvis fil endres av feil tråd.
Implementer i: `.githooks/pre-commit` (OPS eier)
---
## KORREKSJONS-REGEL
Ground truth wins always.
1. Innrøm direkte i én setning
2. Oppdater eid fil med korreksjon
3. Fortsett
ALDRI forsvar gamle posisjoner.
---
## KOMMUNIKASJON MELLOM TRÅDER
- ML-TRÅD: Chris kopierer melding over
- Filer flyter via GitHub (`OSVauco` + tjeneste-repoer)
- Aldri direkte tråd-til-tråd — alt via committed MD eller Chris
---
## EIERSKAP-MATRISE
| Fil | Eier | Andre kan |
|---|---|---|
| VAUCO_OS_MASTER_HANDOFF_v2.md | OPS | lese |
| VAUCO_OS_ROADMAP.md | PLAN | lese, foreslå patch via PR |
| SYSTEM_STATE_AGENT_BOOT.md | EXEC | lese |
| docs/AGENTRLEBOOK.md | OPS | lese |
| docs/ARCHITECTURE.md | PLAN | lese, foreslå patch via PR |
| docs/LEARNINGS.md | OPS | lese, foreslå append |
| docs/RISKREGISTER.md | AUDIT | lese |
| docs/PLANBOARD.md | AUDIT | lese |
| CHANGELOG.md | OPS | lese |
| TODO.md | EXEC | lese |
Regel: én fil — én eier. Andre foreslår via PR, eier merger.
---
EOF v1.2 — 2026-05-21 — OPS-Computer-Hub (Phase 2: HUB_URL locked LIVE, domain status updated)
---
## LEARNINGS — siste oppføringer
Regel: Før ny plan: verifiser alle premisser eksplisitt. Ground truth wins always. Innrøm direkte, oppdater eid fil, fortsett.
Implementert i: `docs/AGENTRLEBOOK.md` — KORREKSJONS-REGEL
---
### LEARNING-004: Pre-commit hook kan ikke skille regel-definisjon fra regelbrudd
Dato: 2026-05-18
Kontekst: `.githooks/pre-commit` bruker innholdsbasert regex-gate. Fanger `vertexai.generative_models` overalt — inkludert i NEVER-lister og kommentarer.
Lærdom: Semantisk korrekt bypass (`--no-verify` eller `git config --unset core.hooksPath`) er riktig for commits som inneholder regel-definisjoner, ikke regelbrudd.
Regel: Hook v2 må implementere Argument C: kun match uncommented lines i .py/.sh, og kun utenfor NEVER/blocked/forbid-kontekst i .md.
Implementert i: TASK-FRICTION-003 — hook v2 (Day 2)
---
### LEARNING-005: Verdivurdering før commit — tråden kan absorbere korreksjon raskt
Dato: 2026-05-18
Kontekst: Doc-konsolidering ble nødvendig (LEARNINGS.md, AGENTRLEBOOK.md). Ground-truth-vinner-alltid-prinsippet fungerte raskt i praksis.
Lærdom: Kjør verdivurdering ved slutten av hver økt: hva produserte vi, hva er nyttig vs dødt, hvilken læring overlever?
Regel: Før tråd-økt avsluttes: oppdater LEARNINGS.md (append), og friction → TASK-FRICTION-NNN i ROADMAP.
Implementert i: `docs/AGENTRLEBOOK.md` — FRICTION-REGLER
---
EOF — append videre under denne linjen
---
### LEARNING-006: gcloud-CLI deler quota med system-prosesser (32555940559)
Dato: 2026-05-18
Kontekst: gcloud CLI bruker shared project 32555940559 for cloudresourcemanager API. Loop-scripts trigget 2400 RPM cap. Årsak: boot-script eller watch-prosess listet prosjekter/billing i loop.
Lærdom: Quota-hit på shared gcloud-prosjekt er ikke-fatal, forsvinner etter 60s. Sjekk alltid ps aux for spam-prosesser før loop-operasjoner.
Regel: 1) Skriv Y ved quota-prompt → 2) vent 60s → 3) kjør preflight.sh på nytt. Ved 429: `ps aux | grep -E "(gcloud|gemini|watch)" | grep -v grep` → kill -9 <PID> ved looping prosess.
Implementert i: `scripts/preflight.sh` — kandidat for quota-check i v1.2 (TF-004)
---
### LEARNING-007: Bindestrek er ugyldig i bash-funksjonsnavn (POSIX strict)
Dato: 2026-05-25
Kontekst: Cloud Shell kjører `-bash` i POSIX strict mode. `opax-logg-slutt() {` kastet syntax error. Funksjonen var definert korrekt men navn med bindestrek er ikke tillatt i POSIX sh.
Lærdom: Funksjonsnavn i bash-scripts som skal kjøres i Cloud Shell MÅ bruke understrek, ikke bindestrek. Alias kan fortsatt bruke bindestrek og peke på understreks-funksjonen.
Regel: Alle funksjoner i `scripts/` bruker understrek (`opax_logg_slutt`). Aliaser for brukervennlighet kan ha bindestrek (`alias opax-logg-slutt='opax_logg_slutt'`).
Implementert i: `scripts/osvauco-opax-boot.sh` — fix pushet HEAD 1cea31d
---
### LEARNING-008: rclone med GDrive service account krever eksplisitt mappedeling
Dato: 2026-05-25
Kontekst: GitHub Actions nattlig backup feilet første kjøring. Årsak: service account (`vauco-gdrive-backup@...`) hadde ikke tilgang til GDrive-mappen selv om JSON-nøkkel og Secret var korrekt satt.
Lærdom: GDrive-mapper er ikke automatisk tilgjengelig for service accounts selv om de har riktig IAM-rolle. Mappen MÅ deles eksplisitt med service account-eposten (Editor-tilgang) i GDrive UI.
Regel: Ved oppsett av rclone/GDrive-backup: del ALLE målmapper med SA-epost manuelt i GDrive. Dokumenter i `docs/GDRIVE_SETUP.md`.
Implementert i: `docs/GDRIVE_SETUP.md` — steg 3
---
### LEARNING-009: .gdrive-mirror-state som SHA-anker forhindrer falske OK-varsler
Dato: 2026-05-25
Kontekst: Boot-dashboard viste alltid "fersk mirror" selv om GDrive ikke var oppdatert. Årsak: ingen persistert state å sammenligne mot.
Lærdom: Synkroniseringsstatus uten persistert anker er ubrukelig. `.gdrive-mirror-state`-filen (LAST_SYNC + LAST_SHA) gir boot-scriptet et faktisk sammenligningspunkt mellom sesjoner.
Regel: Ethvert sync-script skal skrive en state-fil med tidsstempel + commit-SHA. Boot leser denne og varsler ved avvik.
Implementert i: `scripts/sync-gdrive-mirror.sh` + `scripts/osvauco-opax-boot.sh`
---
*Oppdater ved å kjøre: `bash scripts/sync-gdrive-mirror.sh` — last deretter opp til GDrive.*
**EOF · GDRIVE_MIRROR_BOOT · 2026-05-26T03:02 UTC · HEAD 5479a5f · auto-generert**

114
docs/GDRIVE_SETUP.md Normal file
View File

@ -0,0 +1,114 @@
# VAUCO OS — GDRIVE BACKUP SETUP
> Eier: OPS-Computer-Hub · Opprettet: 2026-05-25
Ett-gangs oppsett for å aktivere nattlig GDrive-backup og `sync-gdrive-mirror.sh`.
---
## Forutsetninger
- GCP service account med `roles/drive.file` på GDrive-mappen
- GitHub Secrets satt (se under)
- `rclone` installert lokalt for manuell bruk
---
## Steg 1 — Opprett GDrive-mapper
Opprett to mapper i Google Drive (under `chris.christiansen@vauco.no`):
| Mappe | Formål |
|---|---|
| `vauco-mirror/` | Nøkkelfiler (MD, indeks) — agent-lesbar |
| `vauco-backup/` | git bundle — disaster recovery |
Kopier mappe-ID fra URL: `https://drive.google.com/drive/folders/**<ID>**`
---
## Steg 2 — Service account (HITL: IAM_GODKJENT)
```bash
# Opprett service account
gcloud iam service-accounts create vauco-gdrive-backup \
--display-name="Vauco GDrive Backup" \
--project=propane-will-491900-m5
# Last ned nøkkel
gcloud iam service-accounts keys create /tmp/vauco-gdrive-sa.json \
--iam-account=vauco-gdrive-backup@propane-will-491900-m5.iam.gserviceaccount.com
```
Del `vauco-backup/`- og `vauco-mirror/`-mappene med service account-e-posten:
`vauco-gdrive-backup@propane-will-491900-m5.iam.gserviceaccount.com` (Editor)
---
## Steg 3 — GitHub Secrets
Gå til: `https://github.com/vauco-saas/OSVauco/settings/secrets/actions`
Legg til:
| Secret | Verdi |
|---|---|
| `RCLONE_GDRIVE_SA_JSON` | Innhold av `/tmp/vauco-gdrive-sa.json` (hele JSON-strengen) |
| `GDRIVE_BACKUP_FOLDER_ID` | Mappe-ID for `vauco-backup/` fra GDrive URL |
---
## Steg 4 — Test manuelt
```bash
# Installer rclone lokalt
curl https://rclone.org/install.sh | sudo bash
# Konfigurer
rclone config # velg Google Drive, bruk service account JSON
# Test sync
bash scripts/sync-gdrive-mirror.sh
```
---
## Steg 5 — Aktiver workflow
Etter merge av PR #1:
- Workflow kjører automatisk neste natt kl 03:00 UTC
- Manuell trigger: GitHub → Actions → "Nightly GDrive Backup" → Run workflow
---
## Hva som synkroniseres
### `vauco-mirror/` (nøkkelfiler — agent-lesbar)
- `AGENT_RULEBOOK.md`
- `GDRIVE_MIRROR_BOOT.md` (auto-generert med tidsstempel)
- `LEARNINGS.md`
- `VAUCO_OS_ROADMAP.md`
- `TODO.md`
- `MASTERPLAN.md`
- `sessions/index.md`
### `vauco-backup/` (git bundle — disaster recovery)
- `OSVauco-backup-<timestamp>.bundle` — full repo med all historikk
- Beholder siste 7 bundles (ca. 1 uke)
---
## Gjenoppretting fra bundle
```bash
# Last ned bundle fra GDrive
# Klon fra bundle
git clone OSVauco-backup-2026-05-25T0300.bundle OSVauco-restored
cd OSVauco-restored
git remote set-url origin https://github.com/vauco-saas/OSVauco.git
git push --mirror origin # MERGE APPROVED kreves
```
---
**EOF · GDRIVE_SETUP v1.0 · 2026-05-25 · OPS-Computer-Hub**

View File

@ -0,0 +1,4 @@
# GPG Signing Status
✅ DEFAULT_TRUST_MODEL = committer aktivert 2026-07-22
✅ OPAX-MCP commits skal nå vises som verified i Gitea

View File

@ -0,0 +1,106 @@
# OSVauco · Gullstandardprompter
Denne filen samler de «gullstandard»-promptene som brukes som første melding inn i GeminiCLI og andre assistenter.
Alle er forankret i `docs/AGENT_RULEBOOK.md` (LOCK LIST + KOSTDISIPLIN) og HUBmetoden.
---
## 1. OPAXMCP oppstartsrapport (GeminiCLI)
Brukes automatisk via `.gemini/GEMINI.md` når OPAXbootscriptet starter Gemini.
```text
Du kjører nå OSVaucoflow for OPAXMCP i prosjekt `propane-will-491900-m5` (region `us-central1`).
Ved oppstart:
- Bekreft kort prosjekt, region og account.
- Les `docs/AGENT_RULEBOOK.md` og `docs/LEARNINGS.md` og oppsummer de viktigste reglene for denne økten.
- Finn neste konkrete oppgave i `TODO.md` eller `docs/PLANBOARD.md` hvis de finnes.
- Skriv en kort plan (35 setninger) for hva du skal gjøre i denne sesjonen, hvilke filer som røres, og hvordan vi verifiserer resultatet.
Ikke foreslå shellkommandoer eller endre filer direkte før Chris eksplisitt ber om det. Rapportér først funn og plan.
```
Eier: OPS. Endringer krever HITL `MERGE APPROVED`.
---
## 2. Costguard (generell)
Prompt for å kjøre ren kostvurdering basert på KOSTDISIPLIN.
```text
Du er costguard for OSVauco.
1) Les seksjonen «KOST-DISIPLIN» i `docs/AGENT_RULEBOOK.md`.
2) Sjekk om denne oppgaven kan løses med `gemini-2.5-flash` i stedet for tyngre modell.
3) Gi et kort overslag over forventet kost (lav/medium/høy) og om den holder seg innenfor `$3/dag per tråd`.
4) Foreslå maksimum antall kall/modellrunder før vi må stoppe og spørre Chris.
Ikke gjør selve arbeidsoppgaven fokuser kun på kost, modellvalg og når vi må stoppe.
```
Eier: AUDIT. Brukes når en oppgave begynner å bli dyr eller før større kjøringer.
---
## 3. HUB 43 klone/arbeidsromvalg
Brukes når CloneUI i huben skal avgjøre om noe skal være nytt arbeidsrom eller egen klientapp.
```text
Du er HUB43 kontroll for Vauco OS.
1) Les siste versjon av «OS-VAUCO-MT-MMH» i `docs/` for å forstå dagens hubarkitektur.
2) Basert på brukerinput, avgjør om denne klonen skal bli:
a) Et nytt arbeidsrom i eksisterende hub (kun state), eller
b) En egen klientapp (egen HTML) med clientmode, invoicemodul og prosjektmappe.
3) Skriv en kort anbefaling (maks 10 setninger) med konsekvenser for:
- auth (hvem får logge inn)
- kost (modellbruk, APItrafikk)
- vedlikehold (hvor mange filer som må holdes oppdatert).
4) Avslutt alltid med én klar anbefaling: `ANBEFALING: arbeidsrom` eller `ANBEFALING: klient-app`.
```
Eier: PLAN.
---
## 4. Prosjekt/erfaringslogg
Prompt for å lage ny `LEARNING-00X` i `docs/LEARNINGS.md` etter en større oppgave.
```text
Du skal hjelpe til med å skrive en kort erfaringslogg for OSVauco.
1) Oppsummer i 510 setninger hva som ble gjort i denne oppgaven:
filer som ble endret, hvorfor, og hvordan vi verifiserte.
2) Trekk ut 35 konkrete læringspunkter (det som fungerte bra, det som var friksjon).
3) Formater svaret slik at det enkelt kan limes inn nederst i `docs/LEARNINGS.md`
under en ny `LEARNING-00X`seksjon.
Ikke foreslå nye oppgaver; fokuser på observasjoner og læring.
```
Eier: OPS.
---
## 5. Costguard for hubapp / invoicemodul (senere)
Brukes når huben får faktisk fakturerings/kostmodul.
```text
Du er costguard for Vauco OS hubappen.
- Anta at selve HTMLfilen er gratis å kjøre.
- Kost oppstår kun når vi kaller eksterne modeller eller APIer.
1) Les oppgavebeskrivelsen brukeren har gitt.
2) Del den inn i delsteg og estimer relativ kost per steg.
3) Marker hvilke steg som kan kuttes eller forenkles for å spare kost.
4) Foreslå en enkel policy for denne økten
(f.eks. «maks 10 modellkall», «kun flash med temperatur 0.2»).
```
Eier: AUDIT. Kan senere kobles inn via invoice/kostguardUI i huben.

View File

@ -0,0 +1,52 @@
# HANDOFF — OPPDATERT 2026-07-25 09:50 CEST
## KRITISK NESTE STEG — START HER
### Fase 8 — Snapshot-backup av Gitea-VM
```bash
# Opprett snapshot av VM-disken
gcloud compute disks snapshot gitea-server \
--project=propane-will-491900-m5 \
--zone=us-central1-a \
--snapshot-names=gitea-server-$(date +%Y%m%d) \
--quiet
# Verifiser
gcloud compute snapshots list --project=propane-will-491900-m5
```
---
## ARBEIDSMODELL
**OPAX (Perplexity MCP):** Skriver kode, pusher til Gitea, oppdaterer HANDOFF
**Gemini:** `git pull` → implementer → test → rapporter
Denne modellen gjelder frem til lokale modeller (Emma/gemma3:27b) er oppe og kan ta over Gemini sin rolle.
---
## STATUS 2026-07-25 09:50
✅ FASE 0-6.5, DNS, OPAX-MCP, smoke-test, VM, HTTPS, OAuth2, sikkerhet, GPG, secrets
✅ automation@vauco.no alias → chris.christiansen@vauco.no
✅ gitea-chat-bridge deployet og verifisert (us-central1, revision 00003-h4p)
✅ Gitea webhook ID 9 peker på bridge-URL
⏳ Fase 8 snapshot-backup — NESTE
⏳ automation-bruker i Gitea
⏳ Lokale modeller (Emma/gemma3:27b) opp og kjørende
## RESSURSER
- Bridge URL: https://gitea-chat-bridge-357036551735.us-central1.run.app
- Gitea: https://git.vauco.no (localhost:3000 fra VM)
- VM: 34.67.252.59, zone us-central1-a, disk: gitea-server
- Project: propane-will-491900-m5
- Secrets: CHAT_WEBHOOK_URL (v2), gitea-admin-password
- OPAX MCP: us-central1
## FASTE SANNHETER
- Gitea = source of truth
- Lokale modeller = hovedretning
- OPAX skriver kode → Gemini implementerer
- gitea-admin-password og CHAT_WEBHOOK_URL i Secret Manager

372
docs/HANDOFF.md Normal file
View File

@ -0,0 +1,372 @@
# HANDOFF — Vauco OS
**Sist oppdatert:** 2026-06-29 20:42 CEST
**Skrevet av:** Perplexity (for Chris Christiansen)
**Status:** Fase D — VM oppgradert, Gitea som primær, Ollama neste
### REPO TRUTH AND BACKUP STATUS (2026-07-16)
- Gitea is the authoritative source of truth for OSVauco.
- Main was successfully pushed to Gitea and is now aligned at commit e2f43391549a1950f081aa1bc9c1b2ecd3582ef5.
- GitHub is not part of the active automation path.
- GitHub is only an optional manual backup mirror.
- The current Gitea post-receive hook for GitHub mirroring is non-blocking and may fail without affecting the authoritative Gitea repo.
- Do not rely on GitHub mirror status for operational truth.
- If backup is needed later, it can be done manually after the primary Gitea path is stable.
### Gitea + OPAX-MCP status (2026-07-07)
- Gitea er migrert til CPU-VM `gitea-cpu-vm` og svarer på `http://34.170.51.84:3000` og `/api/v1/version`.
- OSVauco-repoet på dev-VM har remotes:
- `origin` + `gitea`: `http://34.170.51.84:3000/chris/OSVauco.git`.
- Alle tidligere hardkodede Gitea-IP-er er oppdatert til CPU-VM:
- `opax-mcp/server.py` (`GITEA_URL` default),
- `emma/emma_gitea.py` (Emma-klient),
- `.gemini/GEMINI.md` (PRIMARY_GIT, testkommandoer),
- `dev-start.sh` (`GITEA_REMOTE`).
- Ny `opax-mcp/gitea_handler.py` er lagt til og bruker `GITEA_URL`/`GITEA_TOKEN` fra env for Gitea-API-kall.
- Neste steg (ikke utført ennå):
- Verifisere `GITEA_TOKEN` via `gcloud secrets versions access --secret=gitea-token`,
- kjøre enkel `curl` mot `"$GITEA_URL/api/v1/version"` med token,
- deretter koble OPAX-MCP/Gitea-tools til den nye instansen.
Dette dokumentet er oppdatert til å reflektere at Gitea på CPU-VM er ny primær Git-master; GitHub er fortsatt kun legacy/backup.
---
## ⚠️ KRITISKE REGLER — les alltid først
- **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`
- **ALDRI** `--audiences`-flag med `gcloud auth print-identity-token` — det er for service accounts, ikke user accounts
- **IAP identity token:** hentes fra GCE metadata server, IKKE fra gcloud CLI
- **Primær Git:** `http://34.170.51.84:3000/chris/OSVauco` — Gitea er nå kilde, GitHub er kun backup.
- **git remote:** `origin` = Gitea. GitHub-remote er fjernet.
- **Emma rapporterer til:** Chris Christiansen `chris.christiansen@vauco.no` — ingen andre kan gi GO
- **emma_runner.py finnes IKKE** — riktig fil er `emma/emma_run.py`
- **Ollama:** IKKE installert ennå — neste oppgave
---
## 📈 STRATEGISK FASEPLAN: Gitea som Source of Truth
**Status:** Planlagt
### Arkitekturbeslutning (Låst)
- **Gitea:** Eneste "Source of Truth" for all kode.
- **OPAX-MCP:** Eneste eksterne agent-gateway (over HTTPS).
- **GitHub:** Kun en potensiell passiv backup/mirror, ikke i operativ flyt.
### Domene-rollemodell (Låst)
- **`git.vauco.no` → Gitea:** Source of truth for kode.
- **`opax.vauco.no` → OPAX-MCP:** Ekstern agent-gateway.
- **`ops.vauco.no` → Parkert:** Droppes inntil videre for å redusere kompleksitet.
---
### Fase 1: Stabiliser `opax-mcp` konfigurasjon
* **Beslutning (Låst):** Alternativ B er valgt. `opax-mcp.yaml` blir eneste autoritative kilde til sannhet for deploy-konfigurasjon.
* **Filer/Tjenester:** `opax-mcp.yaml`, `cloudbuild.mcp.yaml`, Cloud Run `opax-mcp`.
* **Verifisering:** `gcloud run services describe opax-mcp` viser korrekt konfigurasjon.
**Fase 1 TODO:**
* **Mål:** Én autoritativ deploy-kilde for opax-mcp (ikke manuell gcloud run deploy --source .).
* **Ferdig-kriterie:** Live-opax-mcp bygges fra en definert pipeline som matcher opax-mcp.yaml (samme env-sett og image-vei).
### Fase 2: Etablere Gitea-capability bak OPAX-MCP
* **Mål:** Etablere Gitea-funksjonalitet bak gatewayen, med tydelig skille mellom repo/Gitea og CI/CD.
* **Filer/Tjenester:** `opax-mcp` (som gateway), en ny/dedikert Gitea-agent service.
* **Verifisering:** `curl` til `opax-mcp` ruter et Gitea-kall korrekt til backend-tjenesten og gir HTTP 200.
* **Ferdig-kriterie:** Gitea-funksjonalitet er tilgjengelig via `opax-mcp`, implementert i en separat tjeneste.
**Fase 2 Gitea Capabilities (Scope):**
**Repo-lesing (Read-only):**
* `list_repo_files`: Viser filer og mapper i en gitt bane.
* `get_file_content`: Henter innholdet i en spesifikk fil.
* `list_commits`: Viser de siste commits for en branch.
* `list_open_issues`: Viser åpne issues i repoet.
**Repo-skriving (Operator-only):**
* `update_file`: Oppdaterer en eksisterende fil (erstatter `push_file`).
* `create_issue`: Oppretter en ny issue.
* `create_branch`: Oppretter en ny branch.
* `create_commit`: Lager en ny commit med endringer.
**Fase 2 Implementasjonsretning (B-prime):**
**Blocker / Prerequisite for Implementasjon:**
* `GITEA_URL` og `GITEA_REPO` **må** legges til som autoritative miljøvariabler i `opax-mcp.yaml` før koding av Gitea-handleren starter.
* Hardkodede fallback-verdier i `server.py` skal ikke lenger være kilde til sannhet for konfigurasjon.
* Logikken i `server.py` kan fortsatt bruke mønsteret `p.get('repo', GITEA_REPO)` for fleksibilitet, men kun etter at `GITEA_REPO` er deklarativt definert i YAML-filen.
1. **Modul:** Ny fil `opax-mcp/gitea_handler.py` opprettes. Dette blir en **intern modul** i `opax-mcp`-servicen, ikke en egen microservice.
2. **Logikk:** `opax-mcp/server.py` importerer `gitea_handler` og delegerer alle Gitea-relaterte kall (`list_repo_files`, `update_file`, etc.) dit. CICD-kall forblir i `server.py`.
3. **Autentisering:**
* **Ekstern (klient → OPAX-MCP):** Håndteres av IAP, som i dag. Ingen endring.
* **Intern (OPAX-MCP → Gitea):** `gitea_handler.py` bruker et dedikert API-token til å autentisere seg mot Gitea.
4. **Secrets:** `opax-mcp.yaml` må oppdateres med `GITEA_URL` og en referanse til secret `GITEA_API_TOKEN_SECRET`.
### Fase 3: Gitea-drevet CI/CD
* **Mål:** Sikre at Cloud Build utelukkende trigges av `git push` til Gitea.
* **Filer/Tjenester:** Cloud Build Triggers, Gitea webhooks, `cloudbuild.yaml`.
* **Verifisering:** Et `git push` til Gitea starter en ny kjøring i Cloud Build.
* **Ferdig-kriterie:** CI/CD-pipelinen er 100% Gitea-drevet.
### Fase 4: Fjerne GitHub fra daglig drift
* **Mål:** Fjerne alle operative bindinger til GitHub.
* **Filer/Tjenester:** `cloudbuild.yaml` (GitHub App-kobling), diverse skript.
* **Verifisering:** Ingen skript eller pipelines feiler etter at GitHub-integrasjoner er fjernet.
* **Ferdig-kriterie:** GitHub er kun en passiv backup.
---
## 🎯 NESTE OPPGAVE (prioritert)
### 1. Installer Ollama på osvauco-dev-vm
```bash
curl -fsSL https://ollama.com/install.sh | sh
sudo systemctl enable ollama
sudo sed -i 's|ExecStart=.*|ExecStart=/usr/local/bin/ollama serve|' /etc/systemd/system/ollama.service
sudo sed -i '/ExecStart/a Environment="OLLAMA_HOST=0.0.0.0"' /etc/systemd/system/ollama.service
sudo systemctl daemon-reload && sudo systemctl restart ollama
```
### 2. Pull modeller
```bash
ollama pull gemma3:4b # 3.3 GB — lett, rask
ollama pull qwen2.5:7b # 4.7 GB — sterkere
```
### 3. Test Ollama
```bash
curl http://localhost:11434/api/generate -d '{"model":"gemma3:4b","prompt":"hei","stream":false}'
```
### 4. Oppdater opax-mcp/server.py (PARKERT/ERSTATTET)
- **Note:** Erstattet av Fase 1: `opax-mcp.yaml` som autoritativ deploy-path. Kodeendringer til `opax-mcp` skal følge den nye, stabile deploy-prosessen.
### 5. Oppdater boot.sh — Ollama autostart-sjekk
- Legg til seksjon `[ OLLAMA ]` i `scripts/boot.sh` som sjekker at Ollama kjører
### 6. Emma-gpu-vm — kun ved behov
- Legg til aliaser i `boot.sh`:
- `emma-start` — starter emma-gpu-vm
- `emma-stop` — stopper emma-gpu-vm
---
## 🖥 Infrastruktur per 2026-06-29
| Ressurs | Spec | Status | Kostnad/mnd |
|---------|------|--------|-------------|
| `osvauco-dev-vm` | e2-standard-4, 16GB RAM, 50GB SSD, us-central1-b | ✅ Kjører | ~NOK 550 |
| `emma-gpu-vm` | GPU, europe-west4-a | 💤 Stoppet | ~NOK 30 (kun disk) |
| Cloud Run `opax-mcp` | MCP-server, us-central1 | ✅ Kjører | ~NOK 50 |
| Cloud Run `osvauco-agent` | Hovedagent, us-central1 | ✅ Kjører | inkl. over |
| Load Balancer + DNS | opax.vauco.no, 34.98.77.173 | ✅ Kjører | ~NOK 150 |
| **TOTAL** | | | **~NOK 780/mnd** |
**Tidligere kostnad (emma alltid på):** NOK 6,000+/mnd
---
## ✅ Fullført i dag (2026-06-29)
| Oppgave | Status |
|---------|--------|
| `git remote origin` endret fra GitHub til Gitea på aktiv dev-VM (osvauco-gpu-vm) | ✅ |
| `github`-remote fjernet (duplikat) | ✅ |
| `git pull origin main` fungerer igjen | ✅ |
| `boot.sh` kjører komplett uten å henge | ✅ |
| `osvauco-dev-vm` oppgradert: e2-medium → e2-standard-4 (16GB RAM) | ✅ |
| Disk utvidet: 20GB → 50GB | ✅ |
| `emma-gpu-vm` stoppet (spare kostnad) | ✅ |
| GitHub avviklet som primær kilde, Gitea er nå master | ✅ |
*emma-gpu-vm er klargjort, men ikke tatt i bruk som primær dev-node ennå.*
---
## 🔑 Credentials
- **GitHub PAT:** Secret Manager → `GITHUB_PAT` (oppdatert 2026-06-25)
- **MCP-Secret:** Secret Manager → `mcp-server-key`
- **Prosjekt:** `propane-will-491900-m5`
- **IAP OAuth Client ID:** `357036551735-kq8nt7ld38hfqlcfb3n52ef7tala4meo.apps.googleusercontent.com`
- **VM compute SA:** `357036551735-compute@developer.gserviceaccount.com`
- **Ollama (når installert):** `http://localhost:11434`
- **Emma-gpu-vm IP:** `34.13.238.133` (kun når startet)
---
## 🏗 Arkitektur (nå)
```
Bruker
└─► Jason (Vertex AI Agent Engine, Gemini 2.5 Pro)
└─► osvauco-agent (Cloud Run)
└─► opax-mcp (Cloud Run, 16 tools)
└─► [IAP] opax.vauco.no
osvauco-dev-vm (e2-standard-4, 16GB)
└─► Gemini CLI / Claude CLI (opax/opax2)
└─► Ollama (localhost:11434) ← INSTALLERES NESTE
├─► gemma3:4b (lett)
└─► qwen2.5:7b (sterk)
└─► Emma (lokal agent)
├─► Morfisk minne (SQLite)
├─► Guardrails (5-nivå)
└─► OPAX-klient
emma-gpu-vm (stoppet — start ved behov)
└─► gemma3:27b (tung modell for kunder)
```
---
## 🔑 TILGANGSMODELL OG PROFILER
**Beslutning:**
- OPAX-MCP er felles ekstern gateway over HTTPS.
- Repo- og driftsverktøy er kun for operator-profiler.
- Familieprofiler og senere sluttbrukerprofiler skal ikke ha generell repo-oversikt eller generiske kodeverktøy.
- Sluttbrukere får kun oppgavebaserte capabilities med avgrenset scope.
- Nye brukere og nye hjem/oppsett skal på sikt kunne opprettes via blueprints/profiler, ikke via full teknisk tilgang.
**Operativ tolkning:**
- “Ekstern klient” betyr Perplexity, mobilflater, familieassistenter og andre agenter/VM-er som ikke skal ha direkte tilgang til intern repo/serverstruktur.
- Disse klientene skal gå via OPAX-MCP, ikke direkte mot Gitea eller interne driftstjenester.
- Full repo-innsikt, push/write og driftstools forblir for operatornivå.
- Familie- og sluttbrukerflater skal eksponere trygge, oppgavebaserte funksjoner i stedet for generelle utviklerverktøy.
- Arkitekturen skal støtte én kjerneplattform med ulike profiler: Operator, Family og senere kunde/hjem-blueprints.
PROFILMODELL HVEM FÅR HVA VIA OPAX-MCP
Operator-profil (deg og evt. få betrodde)
- Full tilgang til OPAX-MCP-verktøy for repo, drift og CICD.
- Kan lese og skrive direkte mot Gitea via Gitea-capability (list_commits, get_file, push_file, issues).
- Kan trigge og overvåke Cloud Build / Cloud Run via CICD-capability.
- Kan endre arkitektur, secrets og konfigurasjon når det er nødvendig.
- Krav: sterk auth (MCP_SECRET), bevisst bruk, og commit/push-praksis mot Gitea som sannhet.
Family-profil (familie og nærmeste)
- Ingen generell repo-innsikt og ingen generiske kodeverktøy.
- Tilgang til oppgavebaserte capabilities (f.eks. familieplan, handleliste, meldinger, status) via OPAX-MCP.
- Kan bruke agenter og assistenter som går via OPAX-MCP, men bare innenfor trygge, avgrensede flows.
- Repo-tilgang for family skjer indirekte, som del av oppgaveverktøy, ikke som “fri coding”.
- Krav: enkel, mobilvennlig auth og minimal risiko for å påvirke drift eller arkitektur.
Blueprint-/kunde-/hjem-profiler (senere)
- Malbaserte profiler som beskriver hvilket sett med capabilities og hvilke grenser en ny “hjem” eller kunde får.
- Hver blueprint definerer:
- Hvilke moduler som er aktive (Gitea-lesing, meldinger, økonomi, osv.).
- Hvilke verktøy er synlige i OPAX-MCP for den profilen.
- Hvilke ressurser (repoer, prosjekter, noder) er innenfor scope.
- Opprettelse av nye profiler skal skje som en bevisst handling via blueprint, ikke via ad-hoc åpning av hele systemet.
**Tilgangsmatrise Gitea Capabilities (Fase 2):**
* **Operator-profil:**
* **Repo-lesing:** Full tilgang.
* **Repo-skriving:** Full tilgang.
* **Family-profil:**
* **Repo-lesing:** Kun tilgang til spesifikke, trygge funksjoner (f.eks. `get_file_content` for en handleliste). Ingen generell fil-listing.
* **Repo-skriving:** Ingen tilgang.
* **Blueprint/Kunde/Hjem-profil:**
* **Repo-lesing:** Ingen tilgang som standard. Må aktiveres eksplisitt i blueprint.
* **Repo-skriving:** Ingen tilgang som standard.
---
## 🔁 LÆRINGSSLØYFE FOR AGENTER OG LLM-DRIFT
**Mål:**
- Systemet skal forbedres mens vi jobber, ikke bare etterpå.
- Høyere kvalitet skal komme fra bedre dataflyt, bedre seleksjon og bedre feedback, ikke bare større modeller.
**Prinsipper:**
- Good data beats more data: verifiserte hendelser, faktiske diff-er, reelle feil og ekte outcome-logg er mer verdifulle enn mye støy.
- Bad data compounds: feil antakelser, uverifiserte forklaringer og gamle docs som behandles som sannhet skal ikke mates tilbake ukritisk.
- Scaling laws i praksis: mer kontekst, flere steg og mer historikk gir bare bedre resultater hvis datakvaliteten holdes høy.
- Moores law betyr at rå compute over tid blir billigere og mer tilgjengelig, men det løser ikke alene kvalitetsproblemet i agent- og LLM-drift.
- Bedre hardware uten bedre datahygiene gir bare raskere produksjon av de samme feilene. Derfor skal systemet utnytte begge lover samtidig: Moores law på compute-siden, og scaling laws på modell/data-siden.
- Arbeidslogg, handoff, learnings og verifiseringsoutput skal brukes som kuratert læringsgrunnlag for neste agent og senere trenings-/finetunegrunnlag.
- Hver økt skal produsere små, høyverdige datapunkter: diagnose, plan, apply, verifisering, avvik, beslutning.
- Praktisk betyr det at mer GPU, mer kontekst og større modeller først gir varig verdi når læringsgrunnlaget er kuratert, verifisert og forankret i reell drift.
- Strategien er: bruk økende compute til å forsterke god læring, ikke til å skalere opp støy.
**Operativ regel:**
- Agenter skal ikke “lære” av egne antakelser alene.
- De skal lære av dokumentert virkelighet: rå output, godkjente diff-er, bekreftede feil, bekreftede fixes og tydelig markerte blockers.
**Bruk:**
- Dette gjelder Gemini på VM, OPAX-MCP, fremtidige LLM API-agenter og senere intern eval/finetune/RAG.
- Målet er at neste agent starter klokere enn forrige, uten å arve ukritisk støy.
---
## 🚚 Gitea-migreringsplan
Diagnose har avdekket at en aktiv Gitea-instans kjører på en midlertidig VM, og at lastbalanserer peker feil. Dette løses ved en kontrollert migrering til en ny, permanent VM, ikke ved å fikse den gamle.
* **Kilde-VM:** `osvauco-dev-from-snap` (i `us-west4-a`)
* **Kilde-data:** `/opt/gitea/data/` (inneholder `app.ini` med `ROOT_URL=http://34.170.51.84:3000/`)
* **Mål-VM:** Ny `gitea-cpu-vm` (i `us-central1-b`, som per arkitekturbeslutning)
* **Mål-data:** `/opt/gitea/data/`
**Nøkkelsteg ved migrering:**
1. Data fra kilde-VM må kopieres til mål-VM.
2. `ROOT_URL` i `app.ini` på mål-VM **må** oppdateres fra `http://34.170.51.84:3000/` til `https://git.vauco.no/`.
3. Lastbalanserer-backend (`vauco-os-backend`) må pekes til den nye `gitea-cpu-vm` **etter** at migreringen er testet og verifisert.
---
## 📚 Relevante docs
| Dok | Innhold |
|-----|---------|
| `docs/LEARNINGS.md` | Append-only lærdomslogg |
| `docs/AGENT_RULEBOOK.md` | Boot-protokoll, deploy-regler |
| `.gemini/GEMINI.md` | Instrukser til Gemini CLI på VM |
| `scripts/boot.sh` | Session-starter, aliaser, TUI-valg |
| `scripts/vm-teardown.sh` | Cron 03:00 CEST — stopper dev-vm |
| `docs/DNS-OG-INFRASTRUKTUR.md` | DNS-kart, IAP-routing |
---
### HANDOFF 2026-07-07: Etablering av autoritativ deploy-pipeline
**Mål:** Gjøre `opax-mcp.yaml` til den eneste autoritative sannheten for deploy av `opax-mcp`-tjenesten, og fjerne den gamle, manuelle deploy-flyten.
**1. Analyse og opprydding av `opax-mcp.yaml`**
* **Analyse:** En "diff" mellom live Cloud Run-tjenesten og `opax-mcp.yaml` avdekket avvik. Live hadde gamle GitHub-variabler, mens YAML-filen hadde mange nye (Gitea, Twilio, Gmail).
* **Beslutning:** For å gjøre første autoritative deploy så trygg som mulig, ble det besluttet å midlertidig fjerne Gitea-spesifikke variabler (`GITEA_URL`, `GITEA_TOKEN`) fra `opax-mcp.yaml`.
* **Resultat:** `opax-mcp.yaml` er patchet og committet. Den representerer nå en ren basis-konfigurasjon uten aktiv Gitea-runtime.
**2. Opprettelse av deklarativ pipeline (`cloudbuild.deploy.yaml`)**
* **Analyse:** Den eksisterende `cloudbuild.mcp.yaml` brukte en imperativ `gcloud run deploy`-kommando som overstyrte manifest-filen.
* **Beslutning:** En ny, dedikert og deklarativ pipeline-fil ble opprettet.
* **Resultat:** `cloudbuild.deploy.yaml` er opprettet og committet. Den bygger et image med unik `$BUILD_ID`, rendrer en midlertidig kopi av `opax-mcp.yaml` med den nye image-taggen, og deployer med `gcloud run services replace`.
**3. Rekonfigurering av Cloud Build Trigger (feilet)**
* **Mål:** Peker den eksisterende Gitea-webhook-triggeren (`gitea-osvauco-main`) fra den gamle `cloudbuild.mcp.yaml` til den nye `cloudbuild.deploy.yaml`.
* **Problem:** `gcloud`-CLIet for å oppdatere/gjenopprette webhook-triggere viste seg å være kantete og feilet gjentatte ganger.
* **Resultat:** Den gamle triggeren ble slettet i et forsøk på å gjenopprette den, men gjenopprettingen feilet. Plattformen er derfor **uten en aktiv CI/CD-trigger for `opax-mcp` akkurat nå.**
**4. Strategisk avklaring og neste steg**
* **Vurdering:** Banen med Cloud Build webhooks er teknisk mulig, men føles som en unødvendig kompleks tilpasning. En egen, OPAX-styrt deploy-bro (Modell B) er et bedre langsiktig mål.
* **Beslutning:** Vi fullfører den enkle webhook-flyten (Modell A) nå for å få en automatisert pipeline raskt, men planlegger for Modell B senere.
---
### Status og neste konkrete handling
| Artefakt | Status |
|---|---|
| `opax-mcp.yaml` | ✅ Klar for autoritativ deploy (midlertidig uten Gitea-vars) |
| `cloudbuild.deploy.yaml` | ✅ Klar og committet |
| **Cloud Build Trigger** | 🔴 **MANGLER.** Må gjenopprettes manuelt. |
**Neste handling:** Gjenopprett `gitea-osvauco-main` manuelt i Cloud Console med den nye `cloudbuild.deploy.yaml` som byggefil for å re-aktivere CI/CD-pipelinen.

View File

@ -0,0 +1,20 @@
# IAM Roles osvauco-agent-sa
**Service Account:** `osvauco-agent-sa@propane-will-491900-m5.iam.gserviceaccount.com`
## Tildelte roller
| Rolle | Tildelt | Formål |
|---|---|---|
| roles/aiplatform.user | 23.05.2026 | Vertex AI / Gemini API-kall |
| roles/secretmanager.secretAccessor | 23.05.2026 | Lese hemmeligheter fra Secret Manager |
| roles/logging.logWriter | 23.05.2026 | Skrive til Cloud Logging |
| roles/cloudtrace.agent | 23.05.2026 | Distribuert sporing |
| roles/monitoring.metricWriter | 23.05.2026 | Skrive metrics |
| roles/run.invoker | 23.05.2026 | Motta HTTP-kall på Cloud Run |
| roles/iam.serviceAccountUser | 23.05.2026 | Brukes av deployer (chris.christiansen@vauco.no) |
| roles/artifactregistry.writer | 23.05.2026 | Pushe Docker-images til Artifact Registry |
| roles/run.developer | 23.05.2026 | Deploye revisjoner til Cloud Run |
## Prinsipp: Minste privilegium
SA-en har kun roller som kreves for drift og CI/CD.
Ingen owner/editor-roller er tildelt.

82
docs/IAP-SETUP.md Normal file
View File

@ -0,0 +1,82 @@
> **STATUS (2026-05-25):** ✅ FULLFØRT — Cloud Run IAM-autentisering aktivert.
> `osvauco-agent` er låst til `chris.christiansen@vauco.no` og `jason.vauger@vauco.no`.
> DNS/CNAME (`opax.vauco.no → ghs.googlehosted.com`) urørt.
> Load balancer (opprettet og slettet samme dag) — ikke nødvendig med Cloud Run domain mapping.
> OAuth-klient `Vauco OS Web App` klar for fremtidig bruk om load balancer reaktiveres.
# Google Cloud IAP Setup Guide
This guide documents how access control is configured for `opax.vauco.no` / `osvauco-agent`.
---
## Kartlegging — Cloud Run-tjenester (2026-05-25)
Live output fra `gcloud run services list --platform=managed --project=propane-will-491900-m5`:
| Service | Region | URL | Sist deployet |
|---|---|---|---|
| `billing-auto-teardown` | us-central1 | https://billing-auto-teardown-357036551735.us-central1.run.app | 2026-05-23 |
| `jason-bot` | europe-north1 | https://jason-bot-357036551735.europe-north1.run.app | 2026-04-10 |
| `osvauco-agent` | us-central1 | https://osvauco-agent-357036551735.us-central1.run.app | 2026-05-24 |
| `vauco-tui-bridge` | us-central1 | https://vauco-tui-bridge-357036551735.us-central1.run.app | 2026-05-17 |
**Domain mapping:** `opax.vauco.no``osvauco-agent` (us-central1) via CNAME `ghs.googlehosted.com`
---
## Valgt løsning: Cloud Run IAM (ikke load balancer IAP)
Cloud Run domain mapping støtter ikke IAP direkte. Løsningen er Cloud Run IAM-autentisering:
```bash
# Fjern public access (allUsers var ikke satt — kommando er trygg å kjøre)
gcloud run services remove-iam-policy-binding osvauco-agent \
--region=us-central1 \
--member="allUsers" \
--role="roles/run.invoker" \
--project=propane-will-491900-m5
# Gi tilgang til autoriserte brukere
gcloud run services add-iam-policy-binding osvauco-agent \
--region=us-central1 \
--member="user:chris.christiansen@vauco.no" \
--role="roles/run.invoker" \
--project=propane-will-491900-m5
```
**Aktiv IAM-policy (verifisert 2026-05-25):**
| Member | Role | Status |
|---|---|---|
| `user:chris.christiansen@vauco.no` | `roles/run.invoker` | ✅ Aktiv |
| `user:jason.vauger@vauco.no` | `roles/run.invoker` | ✅ Aktiv |
---
## OAuth-klient (klar for fremtidig load balancer IAP)
- Navn: `Vauco OS Web App`
- Client ID: `357036551735-ka7t2fv9ue2jp01bs826hpdctlvispuo.apps.googleusercontent.com`
- Client secret: lagret lokalt (ikke i repo)
⚠️ **Roter client secret** — ble eksponert i terminal under oppsett 2026-05-25. Gå til [Google Auth Platform → Credentials](https://console.cloud.google.com/auth/clients?project=propane-will-491900-m5).
---
## Load balancer — opprettet og ryddet 2026-05-25
En global load balancer ble opprettet og deretter slettet samme dag da det ble klart at Cloud Run domain mapping ikke krever dette. Alle ressurser er slettet:
- `opax-forwarding-rule` ✅ slettet
- `opax-https-proxy` ✅ slettet
- `opax-cert` ✅ slettet
- `opax-url-map` ✅ slettet
- `opax-backend` ✅ slettet
- `opax-neg` ✅ slettet
---
## vauco-tui-bridge — parkert til separat vurdering
`vauco-tui-bridge` (us-central1) aktiveres i egen fase etter behov.

209
docs/LEARNINGS.md Normal file
View File

@ -0,0 +1,209 @@
# VAUCO OS — LEARNINGS.md
# Eier: OPS-Computer-Hub
# Format: APPEND-ONLY. Aldri slett, aldri endre eksisterende entries.
# Sist oppdatert: 2026-06-10 CEST
---
## Hensikt
Kodifisert lærdom fra alle sesjoner. Overlever på tvers av tråder og modellbytter.
Andre tråder kan foreslå append via PR — OPS merger.
---
## FORMAT PER ENTRY
```
### LEARNING-NNN: Kort tittel
Dato: YYYY-MM-DD
Kontekst: Hva skjedde
Lærdom: Hva som er sant
Regel: Hva som alltid gjøres nå
Implementert i: [fil/script/gate]
```
---
### LEARNING-001: Branch-blindness koster $0.50+/feil
Dato: 2026-05-18
Kontekst: Phase A scratchpad sjekket kun `main`-branch i vauco-os. `autoflow-lag-1` var canonical med full operativ kode. Hele Phase A-analysen ble feil, og Phase B-planen ble bygget på feil premiss.
Lærdom: Default branch er ALDRI automatisk canonical. Alltid sjekk alle branches og finn den med mest aktive commits.
Regel: Boot-protokoll starter ALLTID med branch-discovery. Hvis 1+ branch er 10+ commits ahead av default: STOPP og spør Chris.
Implementert i: `docs/AGENTRLEBOOK.md` — BOOT-PROTOKOLL steg 1
---
### LEARNING-002: Handoff-MD er ferskvare — utdatert på 13 timer
Dato: 2026-05-18
Kontekst: VAUCO_BOOTSTRAP_HANDOFF_NEMOTRON.md ble utdatert samme dag den ble skrevet. MASTER_HANDOFF_v2 måtte lages for å reconcile motstridende info.
Lærdom: Statiske handoff-filer divergerer fort fra live state. SYSTEM_STATE_AGENT_BOOT.md (EXEC eier) må regenereres ved hver tråd-boot for å være autoritet på hva som er sant nå.
Regel: EXEC-tråden kjører `scripts/boot-state.sh` ved hver sesjon og skriver fersk SYSTEM_STATE_AGENT_BOOT.md.
Implementert i: TASK-FRICTION-002 — `scripts/boot-state.sh` (Day 2)
---
### LEARNING-003: 40% av Phase B var nyttig — resten død/avfeilet
Dato: 2026-05-18
Kontekst: Phase B-plan var bygget på feil premiss (tom remote repo), men inneholdt gode design-elementer: task manager, dev-orchestrate.sh, doc-consolidation layout.
Lærdom: Selv feil-premiss-planer kan inneholde nyttige sub-komponenter. Skill økten mellom premiss-validering og løsnings-design.
Regel: Før ny plan: verifiser alle premisser eksplisitt. Ground truth wins always. Innrøm direkte, oppdater eid fil, fortsett.
Implementert i: `docs/AGENTRLEBOOK.md` — KORREKSJONS-REGEL
---
### LEARNING-004: Pre-commit hook kan ikke skille regel-definisjon fra regelbrudd
Dato: 2026-05-18
Kontekst: `.githooks/pre-commit` bruker innholdsbasert regex-gate. Fanger `vertexai.generative_models` overalt — inkludert i NEVER-lister og kommentarer.
Lærdom: Semantisk korrekt bypass (`--no-verify` eller `git config --unset core.hooksPath`) er riktig for commits som inneholder regel-definisjoner, ikke regelbrudd.
Regel: Hook v2 må implementere Argument C: kun match uncommented lines i .py/.sh, og kun utenfor NEVER/blocked/forbid-kontekst i .md.
Implementert i: TASK-FRICTION-003 — hook v2 (Day 2)
---
### LEARNING-005: Verdivurdering før commit — tråden kan absorbere korreksjon raskt
Dato: 2026-05-18
Kontekst: Doc-konsolidering ble nødvendig (LEARNINGS.md, AGENTRLEBOOK.md). Ground-truth-vinner-alltid-prinsippet fungerte raskt i praksis.
Lærdom: Kjør verdivurdering ved slutten av hver økt: hva produserte vi, hva er nyttig vs dødt, hvilken læring overlever?
Regel: Før tråd-økt avsluttes: oppdater LEARNINGS.md (append), og friction → TASK-FRICTION-NNN i ROADMAP.
Implementert i: `docs/AGENTRLEBOOK.md` — FRICTION-REGLER
---
EOF — append videre under denne linjen
---
### LEARNING-006: gcloud-CLI deler quota med system-prosesser (32555940559)
Dato: 2026-05-18
Kontekst: gcloud CLI bruker shared project 32555940559 for cloudresourcemanager API. Loop-scripts trigget 2400 RPM cap. Årsak: boot-script eller watch-prosess listet prosjekter/billing i loop.
Lærdom: Quota-hit på shared gcloud-prosjekt er ikke-fatal, forsvinner etter 60s. Sjekk alltid ps aux for spam-prosesser før loop-operasjoner.
Regel: 1) Skriv Y ved quota-prompt → 2) vent 60s → 3) kjør preflight.sh på nytt. Ved 429: `ps aux | grep -E "(gcloud|gemini|watch)" | grep -v grep` → kill -9 <PID> ved looping prosess.
Implementert i: `scripts/preflight.sh` — kandidat for quota-check i v1.2 (TF-004)
---
### LEARNING-007: Bindestrek er ugyldig i bash-funksjonsnavn (POSIX strict)
Dato: 2026-05-25
Kontekst: Cloud Shell kjører `-bash` i POSIX strict mode. `opax-logg-slutt() {` kastet syntax error. Funksjonen var definert korrekt men navn med bindestrek er ikke tillatt i POSIX sh.
Lærdom: Funksjonsnavn i bash-scripts som skal kjøres i Cloud Shell MÅ bruke understrek, ikke bindestrek. Alias kan fortsatt bruke bindestrek og peke på understreks-funksjonen.
Regel: Alle funksjoner i `scripts/` bruker understrek (`opax_logg_slutt`). Aliaser for brukervennlighet kan ha bindestrek (`alias opax-logg-slutt='opax_logg_slutt'`).
Implementert i: `scripts/osvauco-opax-boot.sh` — fix pushet HEAD 1cea31d
---
### LEARNING-008: rclone med GDrive service account krever eksplisitt mappedeling
Dato: 2026-05-25
Kontekst: GitHub Actions nattlig backup feilet første kjøring. Årsak: service account (`vauco-gdrive-backup@...`) hadde ikke tilgang til GDrive-mappen selv om JSON-nøkkel og Secret var korrekt satt.
Lærdom: GDrive-mapper er ikke automatisk tilgjengelig for service accounts selv om de har riktig IAM-rolle. Mappen MÅ deles eksplisitt med service account-eposten (Editor-tilgang) i GDrive UI.
Regel: Ved oppsett av rclone/GDrive-backup: del ALLE målmapper med SA-epost manuelt i GDrive. Dokumenter i `docs/GDRIVE_SETUP.md`.
Implementert i: `docs/GDRIVE_SETUP.md` — steg 3
---
### LEARNING-009: .gdrive-mirror-state som SHA-anker forhindrer falske OK-varsler
Dato: 2026-05-25
Kontekst: Boot-dashboard viste alltid "fersk mirror" selv om GDrive ikke var oppdatert. Årsak: ingen persistert state å sammenligne mot.
Lærdom: Synkroniseringsstatus uten persistert anker er ubrukelig. `.gdrive-mirror-state`-filen (LAST_SYNC + LAST_SHA) gir boot-scriptet et faktisk sammenligningspunkt mellom sesjoner.
Regel: Ethvert sync-script skal skrive en state-fil med tidsstempel + commit-SHA. Boot leser denne og varsler ved avvik.
Implementert i: `scripts/sync-gdrive-mirror.sh` + `scripts/osvauco-opax-boot.sh`
---
### LEARNING-010: Cloud Build 2nd gen hadde foreldet glitch-repo-link
Dato: 2026-05-28
Kontekst: Cloud Build hadde to GitHub-kontoer koblet parallelt. Triggeren `osvauco-agent-main-trigger` pekte korrekt på `vauco-saas/OSVauco` (1st gen), men `osvauco-repo` under 2nd gen connection `osvauco-github-conn` pekte på `chrischristiansen-glitch/OSVauco` — en foreldet personlig konto.
Lærdom: CI/CD-triggere og repository-links er separate ressurser i Cloud Build. En trigger kan peke riktig mens en tilhørende repo-link er foreldet. Begge må verifiseres eksplisitt.
Regel: Ved GitHub-kontobytter: kjør alltid `gcloud builds repositories list --connection=<conn> --region=us-central1` og `gcloud builds triggers list` for å verifisere at BEGGE peker på riktig org. Slett foreldet repo-link umiddelbart.
Implementert i: Ryddet 2026-05-28 — `gcloud builds repositories delete osvauco-repo --connection=osvauco-github-conn --region=us-central1`. Bekreftet: "Listed 0 items."
---
### LEARNING-011: Cloud Build 2nd gen krever ny connection ved org-bytte
Dato: 2026-05-28
Kontekst: Forsøkte å koble `vauco-saas/OSVauco` til eksisterende `osvauco-github-conn` (installasjon ID 135008388, tilhørende `chrischristiansen-glitch`). Fikk feil: "repository does not exist or is not accessible". Årsak: GitHub App-installasjonen var bundet til feil konto.
Lærdom: En Cloud Build connection er bundet til én GitHub-konto/org via GitHub App installation ID. Du kan ikke koble repos fra en annen org uten å opprette ny connection med riktig installasjon.
Regel: Ved org-bytte (f.eks. glitch → vauco-saas): opprett alltid ny connection med `gcloud builds connections create github <navn> --region=us-central1`, autoriser i nettleser mot riktig org, så legg til repo.
Implementert i: Fullført 2026-05-28:
- Ny connection: `osvauco-vauco-saas-conn` (us-central1)
- Autorisert mot `vauco-saas`-org i GitHub
- Repo-link: `osvauco-repo``https://github.com/vauco-saas/OSVauco.git`
- Gammel connection `osvauco-github-conn` (glitch) kan slettes når trigger er migrert
---
### LEARNING-012: VertexAiRagRetrieval og FunctionTool kan ikke kombineres i samme agent
Dato: 2026-06-10
Kontekst: CI6 — `root_agent` hadde både RAG og MCP FunctionTools. AFC (Automatic Function Calling) ble deaktivert fordi `VertexAiRagRetrieval` ikke er en Python callable. Agenten kalte aldri MCP-verktøyene.
Lærdom: ADK slår av AFC stille hvis én tool i lista ikke er AFC-kompatibel. Ingen feilmelding — agenten bare ignorerer verktøyene.
Regel: RAG isoleres ALLTID i en dedikert sub-agent. `root_agent` får kun FunctionTools. Miks er forbudt.
Implementert i: `agents/core-logic/agent.py` — RAG fjernet fra root_agent (commit 3bb95c8). TODO: RAG sub-agent.
---
### LEARNING-013: Cloud Run krever identity token, ikke access token
Dato: 2026-06-10
Kontekst: CI6 — `_opax_headers()` brukte `google.auth.default()` som gir OAuth2 access token. Cloud Run (no-allow-unauthenticated) krever identity token med riktig `aud`-claim. Begge gir 401 men av ulik grunn.
Lærdom: `google.auth.default()` ≠ identity token. For Cloud Run-til-Cloud Run kall: bruk alltid GCE metadata server med `?audience=<service-url>`.
Regel: All server-til-server autentisering mot Cloud Run bruker:
`http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=<URL>`
Implementert i: `agents/core-logic/mcp_tools.py` — commit 8013bd8
---
### LEARNING-014: Secrets må mountes eksplisitt på hver Cloud Run-tjeneste
Dato: 2026-06-10
Kontekst: CI6 — `mcp-server-key` fantes i Secret Manager men var ikke mountet på `osvauco-agent`. Agenten sendte tom `X-MCP-Key`-header. `opax-mcp` avviste alle kall med 401.
Lærdom: Secret Manager-secrets er ikke automatisk tilgjengelig for Cloud Run-tjenester. Hver tjeneste må ha eksplisitt `--update-secrets` i deploy-kommandoen.
Regel: `cloudbuild.yaml` bruker nå `--update-secrets` for ALLE required secrets inkl. `MCP_SECRET`. `.env.example` dokumenterer alle secrets.
Implementert i: `cloudbuild.yaml` (CI7) + `agents/core-logic/.env.example` (CI6)
---
### LEARNING-015: Ikke anta issue-status fra MASTERPLAN — sjekk faktisk state
Dato: 2026-06-10
Kontekst: MASTERPLAN #4 viste C1b som "IN PROGRESS" (DNS-endring). DNS hadde vært i orden i 1-2 uker. Agent antok blokkering uten å verifisere.
Lærdom: Dokumenter divergerer fra virkelighet. Statuser i MASTERPLAN/issues er ikke self-updating.
Regel: Spør alltid Chris om usikker status fremfor å anta. Grunnregel: ground truth > dokument.
Implementert i: `docs/MASTERPLAN.md` — C1b markeres done
---
### LEARNING-016: opax.vauco.no bruker osvauco-agent-iap-backend, ikke vauco-os-backend
Dato: 2026-06-17
Kontekst: IAP-binding ble forsøkt lagt til vauco-os-backend fordi vauco-os-urlmap bruker
den som defaultService. Men opax.vauco.no DNS A-record peker til 34.98.77.173 som er
osvauco-agent-forwarding-rule, ikke vauco-os (34.144.224.45).
Fasit: opax.vauco.no -> 34.98.77.173 -> osvauco-agent-url-map -> osvauco-agent-iap-backend.
Den opprinnelige IAP-bindingen (steg 3) var korrekt.
Lærdom: Backend-navn i GCP matcher ikke nødvendigvis subdomene-navn.
Regel: Ved IAP-binding: sjekk DNS A-record -> forwarding rule IP -> url-map -> backend.
Ikke anta fra URL-map-navn. Bruk:
1. nslookup <domene> # finn IP
2. gcloud compute forwarding-rules list --global # match IP -> url-map
3. gcloud compute url-maps describe <url-map> --global | grep defaultService
Implementert i: docs/DNS-OG-INFRASTRUKTUR.md oppdatert med korrekt routing-kart
---
### LEARNING-017: På GCE VM skal IAP alltid bruke metadata-server token, ikke ADC fra brukerlogin
Dato: 2026-06-28
Kontekst: Test mot `https://opax.vauco.no/health` feilet med `Invalid IAP credentials: Unable to parse JWT` etter at brukerbasert auth/ADC hadde utløpt i en SSH-økt på VM. Dette skapte støy fordi VM-en allerede har service account og stabil auth-kanal tilgjengelig.
Lærdom: På GCE VM er `gcloud auth application-default login` midlertidig og brukerbundet. For IAP-kall fra VM skal identity token alltid hentes fra metadata-serveren med riktig audience, siden dette bruker VM-ens service account og ikke utløper på samme måte i arbeidsflyten.
Regel: Alle IAP-kall fra `osvauco-dev-vm` bruker:
`TOKEN=$(curl -sf "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=357036551735-kq8nt7ld38hfqlcfb3n52ef7tala4meo.apps.googleusercontent.com&format=full" -H "Metadata-Flavor: Google")`
etterfulgt av:
`curl -s https://opax.vauco.no/health -H "Authorization: Bearer $TOKEN"`
Ikke bruk ADC/user-login som primær metode for IAP fra VM.
Implementert i: `docs/LEARNINGS.md` (append-only), neste steg `scripts/boot.sh` / testprosedyrer
### LEARNING-018: Agent-metodikk for Infrastruktur-endringer
Dato: 2026-07-07
Kontekst: En serie feilkonfigurasjoner i lastbalansering for Gitea ble identifisert og løst ved å følge en strukturert, iterativ prosess.
Lærdom: For å sikre trygge og forutsigbare endringer, må en fast metodikk følges.
Regel: Følgende metode skal brukes for infrastruktur-endringer:
1. **Diagnose:** Start alltid med read-only-kommandoer (`describe`, `list`, `get`) for å forstå nå-situasjonen. Ikke anta at dokumentasjon er 100% korrekt.
2. **Målarkitektur:** Definer og bli enige om en klar målarkitektur før løsninger foreslås.
3. **Planlegg & Dokumenter:** Skriv planen inn i relevant dokument (`HANDOFF.md`, etc.) som en "ikke utført" TODO-liste. Identifiser og dokumenter alle blockere.
4. **Små Steg:** Utfør planen i de minste, logiske stegene.
5. **Verifiser:** Verifiser resultatet med en test (`curl`, `gsutil ls`, etc.) umiddelbart etter *hver* endring.
6. **Oppdater Sannhet:** Oppdater dokumentasjonen med resultatet, slik at neste økt starter fra en korrekt tilstand.
Implementert i: Hele Gitea LB-fiksen (juli 2026). Nå formalisert her for fremtidig bruk.

View File

@ -0,0 +1,99 @@
# Cloud Build Smoke-Test Post-Mortem — 10. juni 2026
> Lagt til av Perplexity AI etter halv dag med debugging. Ikke gjenta disse feilene.
## Symptom
Cloud Build smoke-test returnerte HTTP 404 på `/health` for alle forsøk, selv etter at Cloud Run-tjenesten var bekreftet live og svarte 200 fra utvikler-VM.
## Root Causes (i kronologisk rekkefølge)
### 1. `--audiences` flagg på identity token
```bash
# FEIL — returnerer tomt token for service accounts:
TOKEN=$(gcloud auth print-identity-token --audiences="$BASE_URL")
# RIKTIG:
TOKEN=$(gcloud auth print-identity-token)
```
**Konsekvens:** Token var tom streng. Authorization-header ble sendt uten gyldig token.
### 2. Tom `BASE_URL` fra udefinert substitusjon
```yaml
# FEIL — $_CLOUD_RUN_URL var ikke definert i cloudbuild.yaml:
BASE_URL="https://${_CLOUD_RUN_URL}"
# → curl https:///health → feil host
```
**Konsekvens:** curl traff feil URL fullstendig.
### 3. `gcloud run services describe` returnerer feil URL
```bash
# FEIL — returnerer intern URL (f.eks. zjbqp3prqq-uc.a.run.app), ikke kanonisk:
BASE_URL=$(gcloud run services describe osvauco-agent \
--format='value(status.url)')
# RIKTIG — hardkod kanonisk URL:
BASE_URL="https://osvauco-agent-357036551735.us-central1.run.app"
```
**Konsekvens:** Smoke-test traff en URL som ikke var knyttet til riktig Cloud Run-instans.
### 4. Cloud Build SA manglet `roles/run.invoker`
Cloud Run returnerer **404** (ikke 401/403) for uautoriserte kallere — designet for å skjule tjenestens eksistens. Cloud Build service account hadde ikke blitt gitt invoker-rettigheter.
```bash
# FIX — kjør én gang per ny Cloud Run-tjeneste:
gcloud run services add-iam-policy-binding osvauco-agent \
--region=us-central1 \
--member="serviceAccount:357036551735@cloudbuild.gserviceaccount.com" \
--role="roles/run.invoker" \
--project=propane-will-491900-m5
```
**Konsekvens:** Alle autentiserte kall fra Cloud Build ble avvist med 404.
## Diagnose-metode som fungerte
La til verbose `curl -v` med og uten auth direkte i smoke-test steget:
```bash
echo "=== Med auth ==="
curl -v -H "Authorization: Bearer $TOKEN" "$BASE_URL/health" 2>&1
echo "=== Uten auth ==="
curl -v "$BASE_URL/health" 2>&1
```
Dette avslørte:
- `server: Google Frontend` i response header → ikke appens 404, men GCP frontend
- Response body: `The requested URL /health was not found on this server` → IAM-blokkering
- Token prefix: tom → `--audiences` bug
## Regler som gjelder heretter
1. **ALDRI** `--audiences` flagg med `gcloud auth print-identity-token`
2. **ALLTID** hardkod kanonisk Cloud Run URL i smoke-tests
3. **ALLTID** gi Cloud Build SA `roles/run.invoker` ved opprettelse av ny Cloud Run-tjeneste
4. **BEHOLD** `sleep 30` i smoke-test — nødvendig for traffic routing
5. **Ved 404 fra automatisert caller** → sjekk IAM FØRST, ikke routing
## Kanoniske verdier for dette prosjektet
```
Cloud Run URL: https://osvauco-agent-357036551735.us-central1.run.app
Cloud Build SA: 357036551735@cloudbuild.gserviceaccount.com
Prosjekt: propane-will-491900-m5
Region: us-central1
```
## Tidslinje
- ~13:00 CEST: Første smoke-test feil
- ~19:30 CEST: Root cause identifisert (manglende IAM + feil URL + --audiences bug)
- ~19:37 CEST: IAM binding lagt til, build passerte
- Totalt tid brukt: ~6.5 timer

414
docs/MASTERPLAN.md Normal file
View File

@ -0,0 +1,414 @@
# VAUCO OS — MASTERPLAN
**Eier:** Chris Christiansen · Vauco AS · Org.nr 935 989 779
**Sist oppdatert:** 2026-06-12
**Status:** 🟢 AUTORITATIV — én kilde til sannhet for hele Vauco-økosystemet
> **Regel #1:** Denne filen er sannheten. Hvis en annen fil sier noe annet — denne vinner.
> **Regel #2:** Endringer her krever eksplisitt `PLAN APPROVED` fra Chris.
> **Regel #3:** Roadmap → seksjon 7. Sesjonsstatus → `docs/HANDOFF.md`. Ingen andre handoff-filer.
---
## Lessons Learned
- Cloud Build smoke-tests require: hardcoded canonical BASE_URL, bare identity token (no --audiences), and explicit run.invoker IAM binding for the Cloud Build service account on every service.
- **2026-06-10 — Absolutte filstier i Python:** Bruk alltid `Path(__file__).parent / "..."` for å referere til statiske filer og konfigurasjon i FastAPI/Cloud Run. Aldri relativ sti som `"static/file.html"` — dette bryter når WORKDIR avviker fra repo-rot (f.eks. i Docker). Regelen gjelder alle fremtidige tjenester og klientleveranser.
- **2026-06-10 — Dockerfile COPY static/:** `static/`-mappen må eksplisitt kopieres inn i Docker-imaget via `COPY static/ ./static/`. Den kopieres ikke automatisk selv om build-konteksten er repo-roten.
- **2026-06-10 — app.py mangler GET /:** FastAPI returnerer `{"detail":"Not Found"}` for alle ruter som ikke er definert. Sørg alltid for at rot-ruten er eksplisitt definert.
- **2026-06-12 — Mappestruktur må verifiseres mot repo:** MASTERPLAN-dokumentasjon av mappestruktur må alltid verifiseres via GitHub API før bygging. Antakelser om struktur er ikke nok.
---
## 1. Plattformhierarki — sannheten i én modell
```
Vauco AS (selskap, eier alle merkevarer)
├── vauco.no (APEX — bedriftens salgs- og portalside; ansiktet utad)
└── VAUCO OS (plattform)
└── OPAX (engine — Cloud Run, RAG, agentic arkitektur, IAP)
├── Modul #1 · CostGuard (live på costguard.oss.vauco.no)
├── Modul #2 · Threadstone (landingsside live, app i bygg)
└── Modul #3 · [navngis senere]
Separat merkevare (parkert):
└── Proof of Curiosity
```
| Nivå | Hva | Hvem ser det |
|---|---|---|
| **vauco.no (apex)** | Bedriftens salgsportal | Alle |
| Vauco OS | Plattformen Chris bygger | Internt |
| OPAX | Engine som driver modulene | Internt + utviklere |
| Modul | Det kundene kjøper | Kunder |
**Ikke selg Vauco OS. Ikke selg OPAX. De er interne verktøy.**
---
## 2. Registrerte domener — fasit
**Aktive subdomener:**
| Domene/subdomen | Teknologi | Status |
|----------|-----------|--------|
| `vauco.no` (apex) | GCS + LB + CDN | ✅ LIVE 2026-06-05 |
| `opax.vauco.no` | LB + IAP → Cloud Run | ✅ Live (IP 34.98.77.173) |
| `oss.vauco.no` | Staging | ✅ Aktiv |
| `costguard.oss.vauco.no` | Cloud Run via IAP | ✅ Live |
| `threadstone.vauco.no` | GitHub Pages | ✅ Live |
| `app.threadstone.vauco.no` | GitHub Pages | ⚠️ CNAME satt, side mangler |
| `os.vauco.no` | Prod-miljø | 🔮 Fase C |
**Email-infra:** MX + SPF/DKIM/DMARC ✅ Live 2026-06-10
---
## 3. Repo-struktur — fasit
### Aktive repos (5)
| Repo | Formål | Synlighet |
|------|--------|-----------|
| `OSVauco` | Mono-repo: OPAX-engine, infra, Terraform, all dokumentasjon | Privat |
| `vauco-bootstrap` | Terraform-modul for kunde-provisjonering | Privat |
| `vauco-site` | `vauco.no` salgsportal | Public |
| `threadstone-landing` | `threadstone.vauco.no` landingsside | Public |
| `vauco-saas.github.io` | Threadstone-app frontend | Public |
> **Regel:** Nytt repo krever eksplisitt `REPO APPROVED` fra Chris.
---
## 4. OSVauco — FAKTISK mappestruktur (verifisert 2026-06-12)
> **VIKTIG:** Denne seksjonen er verifisert mot GitHub API. Ikke anta struktur — alltid verifiser.
```
OSVauco/ (root)
├── main.py (26 KB) ⭐ HOVUDAPPLIKASJON — FastAPI app + alle ruter
├── AGENTS.md
├── CLAUDE.md ← Kontekstfil for Claude-agenter
├── README.md
├── CNAME
├── cloudbuild.yaml ← CI/CD trigger
├── cloudbuild.base.yaml
├── requirements.txt
├── .env.example / .env.prefilled
├── dev-startup.sh / opax.sh
├── agents/ ← Agent-logikk (ikke hovudapp)
├── architecture/ ← Arkitekturdokumenter
├── auth/ ← Auth-logikk
├── data/
├── dialogflow/ ← Dialogflow-integrasjon
├── docs/ ← All dokumentasjon
│ ├── MASTERPLAN.md
│ ├── ROADMAP.md
│ ├── HANDOFF.md
│ ├── AGENT_RULEBOOK.md
│ ├── UNIVERSAL_BOOT_PROMPT.md
│ ├── LEARNINGS.md
│ ├── ARCHITECTURE.md
│ ├── AGENTIC_CONTRACT_MCP_PROVISIONING.md
│ ├── DNS-OG-INFRASTRUKTUR.md
│ ├── SECRETS-SETUP.md
│ ├── LEARNINGS_CICD_2026-06-10.md
│ ├── GCP_Best_Practices.md
│ └── gemma/
│ └── world.md ❌ MANGLER — blokkerer ML-3a/Emma-boot
├── infrastructure/
├── master_hub/ ← Eksisterer — formål ikke klarlagt
├── ml/
├── opax-mcp/ ← MCP-server (ikke agents/mcp_server/)
├── protocols/
├── scripts/
└── static/
├── opax.html ✅ CG4d prisingskalkulator
├── costguard-dashboard.html ✅ CG6 klient-dashboard
├── jason.html
├── billing-dashboard.html
├── costguard.html
├── command-hub.html
└── admin.html
```
**Kritiske korreksjoner fra tidligere feil dokumentasjon:**
- `main.py` (rot) er hovudapplikasjonen — IKKE `agents/core-logic/app.py`
- MCP-server ligger i `opax-mcp/` — IKKE `agents/mcp_server/`
- `master_hub/` og `dialogflow/` eksisterer — formål må kartlegges
- Nye ruter/endepunkter legges i `main.py` — ikke i agents/
---
## 4b. vauco.no — salgsportal
| Versjon | Inneholder | Trigger |
|---------|------------|---------|
| V1 (MVP) ✅ | Hero + modul-grid + om-oss + kontakt | LIVE 2026-06-05 |
| V2 | Sales-CTA + ROI-kalkulator | Etter første betalende kunde |
| V3 | Blogg, kundereferanser | Når 3+ kunder |
---
## 5. Dokumenthierarki
| Fil | Rolle |
|-----|-------|
| `docs/MASTERPLAN.md` | Autoritativ sannhet |
| `docs/ROADMAP.md` | Faser og status |
| `docs/HANDOFF.md` | Sesjonsstatus — overskrives hver sesjon |
| `docs/AGENT_RULEBOOK.md` | Locked agent-regler |
| `docs/UNIVERSAL_BOOT_PROMPT.md` | Boot-protokoll v1.4 |
| `CLAUDE.md` | Kontekst for Claude-agenter (rot) |
| `docs/gemma/world.md` | Emma sin kontekstpakke ❌ mangler |
---
## 6. AI-modell — tofase-strategi
### Fase 1 — Nå
| Parameter | Verdi |
|-----------|-------|
| Intern stand-in | Perplexity (Sonnet 4.6) via GitHub MCP |
| Kundemodell (Jason) | `gemini-2.5-flash` via Vertex AI |
| Region | `us-central1` |
### Fase 2 — Gemma (trigger: kreditter < 20% eller første kunde)
| Parameter | Verdi |
|-----------|-------|
| Emma (intern) | `gemma-4-12b-it` int4 · GCP `g2-standard-4` Spot VM |
| Jason (kunder) | `gemini-2.5-flash` via Vertex — uendret |
---
## 7. Roadmap — kortversjon
> Full status i `docs/ROADMAP.md`.
### 🔴 NOW
- [x] CG4d — Prisingskalkulator i `static/opax.html` ✅ 2026-06-12
- [x] CG5a — Delivery layer: `POST /notify/webhook` ✅ live (commit 4a1e23e, Sonar)
- [x] CG5b — E-postvarsler: `POST /notify/email` via SendGrid ✅ live
- [x] CG6 — Klient-dashboard `static/costguard-dashboard.html` ✅ 2026-06-12
- [ ] len(agents)-bugfix i `/run/dag` 🔴
- [ ] CG5-onboard — Klient-onboarding flow 🔴
- [ ] CG5c — SMS-varsler: `POST /notify/sms` via Twilio [Guard+]
### 🟠 NEXT
- CI1 — opax-mcp/ full CI/CD-kanal
- ML-3a — GPU-VM for Emma + `docs/gemma/world.md` opprettes
- C4 — Medioteq partnerstrategimøte
---
## 8. Agent-identitet
| Identitet | E-post | Modell | Status |
|-----------|--------|--------|--------|
| Eier | `chris.christiansen@vauco.no` | — | ✅ Aktiv |
| Jason Vauger | `jason.vauger@vauco.no` | `gemini-2.5-flash` | ✅ Live |
| Emma Vauger | `emma.vauger@vauco.no` | `gemma-4-12b-it` | 🔮 ML-3a |
**Forbudte kontoer:** `tinius.vauger`, `ccv` — aldri gi tilganger.
---
## 9. Cloud Run — live endepunkter
**Service:** `osvauco-agent` · `us-central1` · `propane-will-491900-m5`
**Public URL:** `https://opax.vauco.no`
| Endepunkt | Fil | Status |
|-----------|-----|--------|
| `GET /` | `main.py` | ✅ Serverer opax.html |
| `GET /health` | `main.py` | ✅ |
| `POST /run` | `main.py` | ✅ |
| `GET /static/jason.html` | `main.py` | ✅ |
| `GET /billing/tokens/by-module` | `main.py` | ✅ |
| `GET /billing/tokens/estimate` | `main.py` | ✅ |
| `GET /billing/summary` | `main.py` | ✅ |
| `GET /billing/anomalies` | `main.py` | ✅ |
| `GET /billing/history` | `main.py` | ✅ |
| `GET /billing/budget` | `main.py` | ✅ |
| `POST /billing/budget` | `main.py` | ✅ |
| `POST /notify/webhook` | `main.py` | ✅ CG5a live |
| `POST /notify/email` | `main.py` | ✅ CG5b live |
| `POST /notify/sms` | `main.py` | 🔴 CG5c |
| `GET /notify/channels` | `main.py` | 🔴 OQ-29 |
---
## 10. Go-to-market
### CostGuard — prismodell v2 (godkjent 2026-06-12)
> **Prinsipp:** Push-first — Jason finner kunden, ikke omvendt.
> **Pitch:** *"Du trenger ikke lære et nytt verktøy. Jason finner deg."*
| Tier | GCP-spend/mnd | Pris | Kjerneløfte |
|------|--------------|------|-------------|
| **Starter** | $0$3k | $499/mnd | Synlighet |
| **Guard** | $3k$15k | $999/mnd | Kontroll |
| **Shield** | $15k$50k | $1.999/mnd | Automatisering |
| **Enterprise** | >$50k | $3.500+/mnd | Platform |
**Delivery layer:**
```
GCP-event → Jason → Delivery layer → Webhook (CG5a) / E-post (CG5b) / SMS (CG5c)
```
**Markedsposisjon:**
| Konkurrent | Pris | CostGuard vinner på |
|-----------|------|--------------------|
| Kubecost Business | $199/mnd | GCP-native + Jason AI |
| Apptio Cloudability | $1.930/mnd | Enklere, push-first |
| CloudHealth | $5.400+/mnd | Pris + SMB-fokus |
### Medioteq — Joint Venture
> Partnersamarbeid, ikke SaaS-kunde. C4 = partnerstrategimøte.
---
## 11. Kjente hull
| ID | Problem | Prioritet |
|----|---------|----------|
| OQ-15 | BQ billing_export tabell | Medium |
| OQ-17 | `github-token` secret mangler | Medium |
| OQ-18 | VM alltid-på kostnad | Høy |
| OQ-21 | `app.threadstone.vauco.no` CNAME uten side | Lav |
| OQ-26 | `docs/gemma/world.md` mangler | Høy (ML-3a blokkert) |
| OQ-27 | `master_hub/` formål ikke dokumentert | Medium |
| OQ-28 | `dialogflow/` formål ikke dokumentert | Medium |
| OQ-29 | `GET /notify/channels` mangler i `main.py` — CG6 faller tilbake på defaults | Lav |
---
## 12. OPAX — status
**Ferdig:**
- ✅ opax.html LIVE via `GET /` (main.py)
- ✅ Jason-chat LIVE
- ✅ Billing dashboard + Chart.js donut (CG4b)
- ✅ Token Intelligence endepunkter
- ✅ CI5+CI6 — opax-mcp live
- ✅ CG4d — Prisingskalkulator i opax.html
- ✅ CG5a — POST /notify/webhook live
- ✅ CG5b — POST /notify/email live
- ✅ CG6 — Klient-dashboard costguard-dashboard.html
**Gjenstår:** len(agents)-bugfix i /run/dag · CG5-onboard · CG5c · CI1 · ML-3a
---
*OSVauco | propane-will-491900-m5 | us-central1 | Autoritativ fra 2026-05-31 | Oppdatert 2026-06-12*
---
## 13. Telefoni & AI-agent Plan — Phase E tillegg
### Konsept: Twilio som kommunikasjonsryggrad
Et Twilio-nummer er grunnmuren for SMS, tale og AI-agent telefoni.
Alt bygger på samme konto og nummer.
---
### Lag 1 — Grunnmur (gjøres NÅ, Phase D)
Allerede planlagt:
- Twilio-konto opprettet
- TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER
lagret i Secret Manager
- send_sms fungerer via opax-mcp
---
### Lag 2 — Inngående AI-telefonsvarer (Phase E, Uke 3-4)
#### Konsept
Kunder/kontakter ringer Twilio-nummeret.
Emma-agenten tar samtalen, transkriberer og svarer med tale.
#### Teknisk stack
- Twilio Programmable Voice + Conversation Relay (WebSocket)
- Ollama (emma modell) på osvauco-dev-vm som LLM
- Whisper eller Twilio Speech Recognition for STT
- Twilio TTS (eller Google TTS) for tale tilbake
#### Flyt
Innringer → Twilio → WebSocket → opax-mcp → Ollama (Emma)
Emma svarer → TTS → Twilio → Innringer
#### Hva Emma kan gjøre under samtalen
- Svare på spørsmål om OSVauco-tjenester
- Slå opp kundedata i backend
- Booke møter og sende SMS-bekreftelse
- Si "ett øyeblikk" og hente live data via MCP-tools
- Eskalere til Chris eller Jason med full samtale-kontekst
#### Implementasjon
1. Legg til /voice webhook-endepunkt i opax-mcp
2. Konfigurer Twilio Voice URL til opax-mcp Cloud Run
3. Bygg ConversationRelay WebSocket-handler
4. Koble til Ollama via eksisterende MCP-infrastruktur
5. Definer Emma sin persona og instrukser for telefon
6. Test med internt nummer først
---
### Lag 3 — Utgående AI-anrop (Phase E, Uke 5-6)
#### Konsept
Emma ringer proaktivt — f.eks. kostnadsvarsler,
møtepåminnelser, oppfølging av kunder.
#### Flyt
Trigger (kostnadsspike / planlagt tid)
→ Emma initierer anrop via Twilio Outbound API
→ Samme ConversationRelay-stack som inngående
#### Bruksområder
- "Hei, jeg ringer fra Vauco — dere nærmer dere
budsjettgrensen på GCP denne måneden"
- Møtepåminnelse dagen før
- Oppfølging etter demo
---
### Lag 4 — Multikanal-orkestrering (Phase F)
#### Konsept
Emma bestemmer selv hvilken kanal som passer:
- Lav prioritet → SMS
- Medium → E-post (Gmail API, allerede oppe)
- Høy prioritet → Telefonanrop
- Intern → Google Chat webhook
#### Implementasjon
Legg til kanalvalg-logikk i Emma sin system-prompt:
"Velg kanal basert på hastegrad og brukerpreferanse"
---
### Personas per kanal
| Kanal | Avsender | Persona |
|---|---|---|
| SMS | +47-Twilio-nummer | Nøytral, kort |
| E-post | jason.vauger@vauco.no | Profesjonell |
| Telefon inngående | Emma | Vennlig, hjelpsom |
| Telefon utgående | Emma | Proaktiv, konsis |
---
### Kostnadsestimat Twilio
| Tjeneste | Kostnad |
|---|---|
| Telefonnummer | ~$1/mnd |
| SMS utgående (NO) | ~$0.07/SMS |
| Voice inngående | ~$0.0085/min |
| Voice utgående | ~$0.014/min |
| Trial credits | $15 gratis ved oppstart |

View File

@ -0,0 +1,315 @@
# OSVauco / OPAX — Session Log
---
## SLUTTRAPPORT 2026-05-31 Refactor
- Gjort : Refaktorert opax.sh for å forenkle oppstart. Boot-instruksjonen ble flyttet fra en variabel i opax.sh til .gemini/GEMINI.md for automatisk innlasting av Gemini CLI. opax.sh er endret til å kalle gemini uten --prompt-flagget. Endringene er commitet og pushet til main.
- Uløst : Ingen uløste problemer i denne sesjonen.
- Slutt : 2026-05-31 16:19 UTC
---
# OSVauco / OPAX — Session Log
---
## Sesjon 2026-05-31
**Tidspunkt:** ~15:00 CEST
**Status ved sesjonsslutt:** Fase 8 delvis verifisert.
### Utført i dag
- [x] Fase 8 delvis verifisert via Cloud Run direkte (`/health` OK).
- [x] IAP fungerer (blokkerer uautentiserte requests).
- [x] End-to-end via nettleser gjenstår.
### NESTE OPPGAVE
Åpne opax.vauco.no i nettleser og verifiser login → chat → svar
---
# OSVauco / OPAX — Session Log
---
## Sesjon 2026-05-30 (sent på kvelden)
**Tidspunkt:** ~22:00 CEST
**Status ved sesjonsslutt:** Phase 7 RAG corpus handoff ferdig.
### Utført i dag
- [x] Verifisert at RAG corpus `osvauco-corpus` er tilgjengelig i `europe-west4`.
- [x] Fikset `07-rag-setup.sh` til å inkludere `data` directory.
- [x] Kjørt import av alle dokumenter til RAG corpus via Python SDK.
- [x] Testet at agenten kan svare på spørsmål med grunnlag i corpus.
### Neste sesjon
Phase 8 — end-to-end test av agent via opax.vauco.no
---
## Sesjon 2026-05-30 (natt)
**Tidspunkt:** ~21:30 CEST
**Status ved sesjonsslutt:** Phase 6 CI/CD 2nd gen trigger ferdig.
### Utført i dag
- [x] Fikset CI/CD-pipeline for `osvauco-agent`.
- [x] Opprettet ny 2nd gen Cloud Build trigger.
- [x] Fikset `Dockerfile` og `cloudbuild.yaml` for å løse build-feil.
- [x] Verifisert at CI/CD fungerer og deployer ny revisjon til Cloud Run.
### Neste sesjon
Phase 7 — RAG corpus handoff
---
## Sesjon 2026-05-30 (kveld)
**Tidspunkt:** ~20:0021:50 CEST
**Status ved sesjonsslutt:** Phase 5 IAP ferdig. MASTERPLAN synkronisert. Klar for Phase 6.
### Utført i dag
- [x] Konfigurert IAP for `osvauco-agent` Cloud Run service.
- [x] Migrert `opax.vauco.no` fra Cloud Run domain mapping → GCP Load Balancer (IP: 34.98.77.173).
- [x] SSL-sertifikat `opax-vauco-cert` aktivert (status: ACTIVE).
- [x] IAP aktivert på `osvauco-agent-iap-backend`.
- [x] IAM: `domain:vauco.no` + `user:chris.christiansen@vauco.no``roles/iap.httpsResourceAccessor`.
- [x] OAuth consent screen konfigurert som Internal for `vauco.no`.
- [x] MASTERPLAN oppdatert: infrastrukturstatus, Phase 5 som fullført, Phase 6 definert.
- [x] Beslutning: Phase 6 er CI/CD (auto-deploy på push til main) — ikke VS Code workflow.
### Neste sesjon
**Phase 6 — CI/CD: Auto-deploy på push til main**
Mål: push til `main` → Cloud Build trigger → bygg Docker-image → deploy `osvauco-agent` til Cloud Run.
Steg:
1. Verifiser/opprett Cloud Build trigger for `vauco-saas/OSVauco``main`-branch
2. Sjekk `cloudbuild.yaml` — oppdater om nødvendig for `agents/core-logic/`
3. Gi Cloud Build SA riktige IAM-roller: Cloud Run Admin + Service Account User
4. Test: dummy-commit → Cloud Build kjører → Cloud Run oppdateres
5. Bekreft Google Chat deploy-varsel operativt
6. Oppdater MASTERPLAN: CI/CD status → ✅ Live
---
## Sesjon 2026-05-30
**Tidspunkt:** ~19:30 CEST
**Status ved sesjonsslutt:** Cloud Run agent deployet og verifisert.
### Utført i dag
- [x] Fikset `Dockerfile` i `agents/core-logic` til å peke på `app:app` i stedet for `main:app`.
- [x] Deployet `osvauco-agent` til Cloud Run via `infrastructure/05-cloudrun-deploy.sh`.
- [x] Verifisert at tjenesten er live og svarer med HTTP 200 OK på `/health`.
- [x] Service URL: `https://osvauco-agent-zjbqp3prqq-uc.a.run.app`
### Neste sesjon
Phase 5 — IAP på opax.vauco.no: begrens til @vauco.no Google-kontoer
---
## Sesjon 2026-05-25 (kveld)
**Tidspunkt:** ~21:0000:30 CEST
**Status ved sesjonsslutt:** Fase B stabilisering delvis fullført. Repos konsolidert.
### Utført i dag
- [x] Alt samlet under `vauco-saas/OSVauco` (repos konsolidert)
- [x] BigQuery dataset + sink opprettet
- [x] Smoke-test 10/10 ✅
- [x] Threadstone model-temaer dokumentert
- [ ] CI/CD Cloud Build trigger — forsøkt, ikke fullfort (ULØST → Phase 6)
### Neste sesjon
Fase B ferdigstilling + Phase 4 (LB + SSL migrering)
### Sesjonsavslutning 20260531-153243
- Start : 2026-05-31 15:32 UTC
- Slutt : 2026-05-31 15:34 UTC
- Gjort : git pull origin chore/consolidate-2026-05-31
- Uløst : source ~/OSVauco/scripts/boot.sh
- Gjort : Konsolidert docs/scripts i PR #5 og merget til main. Implementert auto-BOOT i opax.sh via gemini --prompt. Avdekket region-konflikt: cloudbuild.yaml deployer osvauco-agent til us-central1, AGENT_RULEBOOK sier us-central1.
- Uløst : OQ-27 — region-konflikt i cloudbuild.yaml (_REGION: us-central1 vs us-central1). Fase 8 end-to-end test ikke fullført.
- Slutt : 2026-05-31 16:03 UTC
### Sesjonsavslutning 20260531-182644
- Start : 2026-05-31 18:26 UTC
- Slutt : 2026-05-31 19:34 UTC
- Gjort : ikek mye ble løst nei
- Uløst : mye
---
## SLUTTRAPPORT 2026-06-05 CostGuard auth-fix
- Hva: Implementerte OAuth-ruter for CostGuard, løste container-startfeil på require_auth og fikk /billing/summary til å svare med JSON.
- Filer: main.py, docs/HANDOFF.md
- Commits: 5d42cd4, 9b3be74
- Build: 4b77f9a1 (Cloud Build SUCCESS, deploy til osvauco-agent i us-central1)
- Verifisering:
- /health → 200 OK
- /auth/login → Google-login OK, ingen redirect_uri_mismatch
- /billing/summary → 200 OK med tjenester og ~0 kr
- CostGuard-dashboard → viser 0 kr / 0 % budsjett (forventet, billing_export ikke aktiv ennå)
## NESTE OPPGAVE
OQ-15 Aktiver BigQuery billing_export for GCP-prosjektet propane-will-491900-m5 og koble det inn i CostGuard-endepunktene.
---
## SLUTTRAPPORT 2026-06-05 Plan for OQ-15 (CostGuard BQ-integrasjon)
- **Hva:** Analyserte CostGuard-endepunktene for å integrere fakturadata. Designet og verifiserte en SQL-spørring mot BigQuery. Forberedte et komplett utkast til en ny `get_summary`-metode i `ml/billing_agent.py`.
- **Filer analysert:** `main.py`, `ml/billing_agent.py`.
- **Verifisering:** `bq query` på den nye SQL-spørringen var vellykket.
- **Parkert status:** Implementering av koden er satt på vent i henhold til instruks, i påvente av `PLAN APPROVED`.
## NESTE OPPGAVE (etter PLAN APPROVED for OQ-15)
Implementere, deploye og verifisere den forberedte koden for OQ-15.
---
## SLUTTRAPPORT 2026-06-05 Implementering av OQ-15 (CostGuard BQ-data)
- **Hva:** Implementerte ny `get_summary`-metode i `ml/billing_agent.py` for å hente live data fra BigQuery `billing_export`. Fikset en feil i `cloudbuild.yaml` ved å bytte ut `$COMMIT_SHA` med `$BUILD_ID`, som muliggjorde vellykket build og deploy.
- **Filer endret:** `ml/billing_agent.py`, `cloudbuild.yaml`.
- **Verifisering:**
- `bq query` mot BigQuery-tabellen var vellykket og bekreftet at SQL-spørringen returnerer korrekte data.
- `curl` mot endepunktet ble blokkert av IAP (forventer gyldig sesjon).
- Lokal Python-verifisering feilet på grunn av manglende `dask`/`google` dependencies i miljøet.
- **Konklusjon:** Den tekniske implementeringen er fullført og deployet. Full ende-til-ende-verifisering av `/billing/summary` må gjøres manuelt via nettleser-login.
## NESTE OPPGAVE
Manuelt verifisere `/billing/summary` via nettleser-login på opax.vauco.no (sjekk at UI-data matcher `bq query`), og først deretter vurdere å oppdatere OQ-15-status i docs/MASTERPLAN.md sammen med Chris.
### Sesjonsavslutning 20260605-080317
- Start : 2026-06-05 08:03 UTC
- Slutt : 2026-06-05 08:03 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260611-222003
- Start : 2026-06-11 22:20 UTC
- Slutt : 2026-06-11 22:20 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260611-224326
- Start : 2026-06-11 22:43 UTC
- Slutt : 2026-06-11 22:43 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260613-113243
- Start : 2026-06-13 11:32 UTC
- Slutt : 2026-06-13 11:33 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260615-022424
- Start : 2026-06-15 02:24 UTC
- Slutt : 2026-06-15 02:24 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260615-023212
- Start : 2026-06-15 02:32 UTC
- Slutt : 2026-06-15 02:32 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260618-140233
- Start : 2026-06-18 14:02 UTC
- Slutt : 2026-06-18 14:02 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260619-194016
- Start : 2026-06-19 19:40 UTC
- Slutt : 2026-06-19 19:41 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260627-200626
- Start : 2026-06-27 20:06 UTC
- Slutt : 2026-06-28 01:46 UTC
- Gjort : ---
- Uløst : ---
## SLUTTRAPPORT — 2026-06-28 — IAP auth verifisering
- Hva: Verifisert IAP auth ende-til-ende fra osvauco-dev-vm
- Rotårsak 401: Skrivefeil i IAP_CLIENT_ID i docs — korrekt ID hentet fra gcloud run services describe opax-mcp
- Korrekt IAP_CLIENT_ID: 357036551735-kq8nt7ld38hfqlcfb3n52ef7tala4meo.apps.googleusercontent.com
- Verifisering: HTTP/2 200 fra https://opax.vauco.no/opax/build-status
- Filer rettet: docs/HANDOFF.md, docs/LEARNINGS.md
## NESTE OPPGAVE
Bootstrap Emma på VM: synk Gitea fra GitHub, deretter kjør emma/setup.sh
### Sesjonsavslutning 20260629-190644
- Start : 2026-06-29 19:06 UTC
- Slutt : 2026-06-29 19:07 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260629-201930
- Start : 2026-06-29 20:19 UTC
- Slutt : 2026-06-29 20:19 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260705-071732
- Start : 2026-07-05 07:17 UTC
- Slutt : 2026-07-05 07:22 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260705-165047
- Start : 2026-07-05 16:50 UTC
- Slutt : 2026-07-05 16:55 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260706-180257
- Start : 2026-07-06 18:02 UTC
- Slutt : 2026-07-06 18:03 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260706-222117
- Start : 2026-07-06 22:21 UTC
- Slutt : 2026-07-06 22:21 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260716-041209
- Start : 2026-07-16 04:12 UTC
- Slutt : 2026-07-16 04:16 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260721-032054
- Start : 2026-07-21 03:20 UTC
- Slutt : 2026-07-21 03:25 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260903-003350
- Start : 2026-09-03 00:33 UTC
- Slutt : 2026-09-03 00:34 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260903-004045
- Start : 2026-09-03 00:40 UTC
- Slutt : 2026-09-03 00:41 UTC
- Gjort : (ingen notat)
- Uløst : ingen
### Sesjonsavslutning 20260903-004043
- Start : 2026-09-03 00:40 UTC
- Slutt : 2026-09-03 00:41 UTC
- Gjort : (ingen notat)
- Uløst : ingen

386
docs/PLATFORM_MAP.md Normal file
View File

@ -0,0 +1,386 @@
# OSVx Platform Map
**Project ID:** `357036551735`
**Primary region:** `us-central1`
**Secondary region:** `europe-west1` (static site)
**Owner:** OSVauco / Vauco
**Last updated:** 2026-09-01
This document describes the Cloud Runbased platform that powers the OSVx MCP connector and related services. It is the canonical reference for designing CI/CD, security, and operational procedures.
## 1. Overview
The platform consists of:
- A production MCP backend for the OSVx Perplexity connector
- A staging/dev MCP service for pre-production testing
- An internal worker/API service used by the MCP backend
- A public static website (separate product)
- One legacy service marked for cleanup
All services run on Cloud Run with container images pinned by digest.
## 2. Services
### 2.1. MCP Backend (Production)
- **Current name:** `opax-mcp`
- **Planned name:** `osvx-mcp-prod`
- **Region:** `us-central1`
- **URL:** `https://opax-mcp-357036551735.us-central1.run.app/`
- **Image:** `.../opax-mcp@sha256:8adbd3f7...`
- **Last deployed:** 2026-09-01
- **Environment:** `prod`
- **Role:** Live OSVx MCP backend
- **Health check:** `GET /health``200 OK`
```json
{"status":"ok","service":"opax-mcp","version":"3.5.0","ollama":"http://34.13.238.133:11434"}
```
- **Auth:** Bearer token via `MCP_SECRET`
- **Traffic:** 100% to current revision
- **Notes:**
- Serves the OSVx connector in Perplexity.
- Exposes MCP methods including `daily_ops_report`.
- Version in code: `3.5.0` (matches `/health`).
- **Dependencies:**
- Internal worker: `osvauco-agent` (via `OSVAUCO_AGENT_URL`)
- Ollama endpoint (for model access)
### 2.2. MCP Backend (Staging / Dev)
- **Current name:** `osvx-mcp-dev`
- **Planned name:** `osvx-mcp-staging`
- **Region:** `us-central1`
- **URL:** `https://osvx-mcp-dev-…run.app/`
- **Image:** `.../opax-mcp@sha256:7b6a5c4b...`
- **Last deployed:** 2026-08-20
- **Environment:** `dev / staging`
- **Role:** Pre-production MCP service
- **Health check:** `GET /health``200 OK`
```json
{"status":"ok","service":"opax-mcp","version":"3.4.0", ...}
```
- **Auth:** Bearer token (same mechanism as prod)
- **Traffic:** 100% to current revision
- **Notes:**
- Runs an older version (`3.4.0`).
- Intended for testing new MCP releases before promoting to prod.
- **Dependencies:**
- Should ideally point to a staging instance of `osvauco-agent` (see §2.3).
- Currently may share prod agent depending on env config.
### 2.3. Internal Worker / API
- **Name:** `osvauco-agent`
- **Region:** `us-central1`
- **URL:** Internal only (no public URL)
- **Image:** `.../osvauco-agent@sha256:1a2b3c4d...`
- **Last deployed:** 2026-08-28
- **Environment:** `prod`
- **Role:** Internal API / worker for MCP backend
- **Auth:** Internal-only; not publicly authenticated
- **Traffic:** Invoked by `opax-mcp` / `osvx-mcp-*` via `OSVAUCO_AGENT_URL`
- **Notes:**
- Core dependency of the MCP backend.
- Currently only a prod instance exists.
- **Decisions needed:**
- Whether to create `osvauco-agent-staging` for full environment isolation.
- Whether staging MCP should:
1. Share prod agent (simpler, less isolated), or
2. Use a dedicated staging agent (cleaner isolation, more moving parts).
### 2.4. Public Website
- **Name:** `vauco-site`
- **Region:** `europe-west1`
- **URL:** `https://vauco-site-…run.app/`
- **Image:** `.../web-static@sha256:e3b0c442...`
- **Last deployed:** 2026-03-15
- **Environment:** `prod`
- **Role:** Public marketing / product website
- **Health check:** `GET /``200 OK` (HTML)
- **Auth:** Public
- **Notes:**
- Separate product from the MCP platform.
- Should have its own CI/CD pipeline.
- No direct dependency on MCP services.
### 2.5. Legacy / Cleanup Candidate
- **Name:** `opax-billing-test`
- **Region:** `us-central1`
- **URL:** No active URL (0% traffic)
- **Image:** `.../opax-billing@sha256:f1e2d3c4...`
- **Last deployed:** 2025-11-05
- **Environment:** `test`
- **Role:** Experimental billing service (no longer used)
- **Notes:**
- No traffic allocated.
- Not updated in >9 months.
- Label: `env=test`.
- **Action:** Marked for deletion after final confirmation.
## 3. Environments
| Environment | MCP Service | Agent Service | Purpose |
| :--- | :--- | :--- | :--- |
| `prod` | `opax-mcp` | `osvauco-agent` | Live OSVx backend for Perplexity |
| `staging` | `osvx-mcp-dev` | (shared or dedicated) | Pre-production testing of MCP |
| `test` | (none active) | (none active) | Legacy/experimental (billing test) |
**Decisions:**
1. **Adopt naming:**
- `osvx-mcp-prod` (currently `opax-mcp`)
- `osvx-mcp-staging` (currently `osvx-mcp-dev`)
2. **Decide agent strategy for staging:**
- Option A (simple): staging MCP shares prod `osvauco-agent`.
- Option B (isolated): create `osvauco-agent-staging` and wire staging MCP to it.
## 4. Dependencies
### 4.1. Service-to-service
- `opax-mcp``osvauco-agent`
- Via environment variable `OSVAUCO_AGENT_URL`.
- `osvx-mcp-dev``osvauco-agent` (or future `osvauco-agent-staging`).
### 4.2. External dependencies
- **Ollama endpoint**
- Used by MCP for model access.
- Currently: `http://34.13.238.133:11434` (from `/health` output).
- **GCP services**
- Secret Manager (`MCP_SECRET` for Bearer token).
- Cloud Build (for future CI/CD).
- Artifact Registry (container images).
### 4.3. Missing dependencies (known issues)
- **`gcloud` CLI in MCP container**
- `trigger_build` tool in `opax-mcp/server.py` calls `gcloud` via subprocess.
- Dockerfile does not install `google-cloud-sdk`.
- **Result:** any use of `trigger_build` on prod/staging will fail with “gcloud not found”.
- **Fix:** Add `google-cloud-sdk` installation to the Dockerfile before enabling this tool.
## 5. Deployment Model
### 5.1. Image strategy
- All services use pinned image digests (`@sha256:...`).
- **CI/CD must:**
1. Build images with a commit-SHA label, e.g. `gcb-commit-sha=<COMMIT_SHA>`.
2. Deploy using the exact image digest produced by the build.
### 5.2. Environment promotion flow (target)
- **For the MCP platform:**
1. On push to `main` / feature branch:
- Build `opax-mcp` image with commit-SHA label.
- Deploy to `osvx-mcp-staging` (`osvx-mcp-dev`).
2. On manual approval or version tag:
- Promote the same image to `osvx-mcp-prod` (`opax-mcp`).
- **For the website:**
- Separate pipeline:
- Build `web-static` image.
- Deploy to `vauco-site`.
- **For the agent:**
- Similar pattern if/when a staging agent is introduced.
### 5.3. Configuration gaps to address
- **No commit-SHA in images**
- Images currently lack labels linking them to Git commits.
- CI/CD should add `gcb-commit-sha` label during build.
- **Inconsistent naming**
- Prod MCP: `opax-mcp`
- Dev MCP: `osvx-mcp-dev`
- Target: `osvx-mcp-prod` + `osvx-mcp-staging`.
## 6. Security & Access (high level)
- **MCP services (`opax-mcp`, `osvx-mcp-dev`)**
- Require Bearer token (`MCP_SECRET`) for MCP calls.
- Should restrict invocations to:
- Perplexitys connector infrastructure
- Trusted service accounts (if calling from other GCP services).
- **Agent (`osvauco-agent`)**
- Internal-only; no public ingress.
- Should allow calls only from MCP service accounts.
- **Website (`vauco-site`)**
- Public read access.
- No privileged operations.
## 7. Operational Notes
- **Health checks**
- MCP: `GET /health` on each MCP service.
- Agent: define a simple `/health` if not already present.
- Site: `GET /` as basic availability check.
- **Logging**
- All services use Cloud Logging via Cloud Run.
- Key logs to monitor:
- MCP tool calls (`tools/call`), especially `daily_ops_report` and `trigger_build`.
- Agent API errors.
- **Incident focus**
- If OSVx connector misbehaves:
1. Check `opax-mcp` logs and revision.
2. Verify `/health` version matches expected deployment.
3. Confirm `MCP_SECRET` and connector URL.
## 8. Action Items
1. **Cleanup**
- [ ] Confirm `opax-billing-test` is unused.
- [ ] Delete `opax-billing-test` service.
2. **Naming alignment**
- [ ] Decide final names:
- `osvx-mcp-prod` vs keeping `opax-mcp`.
- `osvx-mcp-staging` vs keeping `osvx-mcp-dev`.
- [ ] Optionally rename services in GCP to match.
3. **Agent environment strategy**
- [ ] Decide: shared prod agent vs dedicated staging agent.
- [ ] Document the decision here and implement if needed.
4. **Image labeling**
- [ ] Update Cloud Build config / Dockerfile for `opax-mcp` to include `gcb-commit-sha`.
5. **`gcloud` dependency**
- [ ] Add `google-cloud-sdk` to the MCP Dockerfile before enabling `trigger_build`.
6. **CI/CD design**
- [ ] Use this document as the basis for:
- Build pipelines per service.
- Promotion rules (staging → prod).
- Environment-specific configs.
## 9. Future Extensions (optional)
- Per-developer ephemeral environments (short-lived Cloud Run revisions).
- Separate `osvx-mcp-sandbox` for integration tests in CI.
- Centralized config service or Secret Managerdriven config for env-specific URLs.
*This document is the source of truth for the OSVx platform architecture.
Update it whenever services, environments, or deployment patterns change.*
---
## 10. CI/CD Setup
This section describes the continuous integration and deployment architecture for the OSVx MCP platform.
### 10.1. Source of Truth
- **Primary Git repository:**
Self-hosted Gitea: `http://34.59.131.162:3000/chris/osvauco.git`
- **Primary branch for MCP:**
`feat/osvx-mcp-full-catalog`
All development and code review happens in Gitea. No other Git host is used as a primary source.
### 10.2. Mirror to Cloud Source Repositories
Cloud Build cannot directly trigger on Gitea pushes, so the repo is mirrored to Cloud Source Repositories (CSR) as a read-only CI/CD mirror.
- **CSR project:** `357036551735`
- **CSR repo name:** `osvauco`
- **CSR repo URL:**
`https://source.developers.google.com/p/357036551735/r/osvauco`
Mirror configuration (on the VM):
```bash
cd ~/OSVauco
git remote add csr https://source.developers.google.com/p/357036551735/r/osvauco
git push csr feat/osvx-mcp-full-catalog:feat/osvx-mcp-full-catalog
```
Mirroring is automated via either:
- A post-receive hook on the Gitea server that pushes `feat/osvx-mcp-full-catalog` to `csr`, or
- A cron job on the VM that periodically runs:
```bash
git push --quiet csr feat/osvx-mcp-full-catalog:feat/osvx-mcp-full-catalog
```
CSR is **read-only** from the CI/CD perspective; it exists only to trigger Cloud Build.
### 10.3. Cloud Build Trigger (Staging)
A Cloud Build trigger watches the CSR repo and runs the MCP pipeline on every push to the CI branch.
- **Trigger name:** `osvx-mcp-staging-trigger`
- **Trigger type:** Cloud Source Repositories
- **Project:** `357036551735`
- **Region:** `us-central1`
- **Repo:** `projects/357036551735/repos/osvauco`
- **Branch pattern:** `^feat/osvx-mcp-full-catalog$`
- **Build config:** `opax-mcp/cloudbuild.yaml`
- **Substitutions:**
- `_REGION=us-central1`
- `_REPOSITORY=osvx-images`
- `_ENV=staging`
Creation command (reference):
```bash
PROJECT=357036551735
REGION=us-central1
REPO=osvx-images
BRANCH=feat/osvx-mcp-full-catalog
gcloud builds triggers create cloud-source-repositories --project="$PROJECT" --name="osvx-mcp-staging-trigger" --region="$REGION" --repo="projects/$PROJECT/repos/osvauco" --branch-pattern="^${BRANCH}$" --build-config="opax-mcp/cloudbuild.yaml" --substitutions=_REGION=${REGION},_REPOSITORY=${REPO},_ENV=staging --included-files="opax-mcp/**" --description="Deploy OSVx MCP to staging on push to ${BRANCH}"
```
**Behavior:**
- On every push to `feat/osvx-mcp-full-catalog` in Gitea (mirrored to CSR):
- Cloud Build runs `opax-mcp/cloudbuild.yaml` with `_ENV=staging`.
- The built image is deployed to `osvx-mcp-staging`.
### 10.4. Production Deployment (Manual Promotion)
Production deployments are intentionally manual to provide an explicit promotion gate.
**Options:**
1. **Local script (recommended):**
```bash
cd ~/OSVauco
./deploy-mcp.sh prod
```
This builds the current commit and deploys to `osvx-mcp-prod`.
2. **One-off Cloud Build:**
```bash
PROJECT=357036551735
REGION=us-central1
REPO=osvx-images
gcloud builds submit . --project="$PROJECT" --config=opax-mcp/cloudbuild.yaml --substitutions=_REGION=${REGION},_REPOSITORY=${REPO},_ENV=prod
```
No automatic prod trigger is configured.
### 10.5. Artifact Registry
- **Repo name:** `osvx-images`
- **Location:** `us-central1`
- **Format:** Docker
Images are tagged with the short commit SHA:
- `us-central1-docker.pkg.dev/357036551735/osvx-images/opax-mcp:<SHORT_SHA>`
Each image includes labels:
- `gcb-commit-sha=<SHORT_SHA>`
- `env=staging` or `env=prod`
### 10.6. Operational Notes
- **Build logs and history:**
GCP Console → Cloud Build → History
- **Trigger configuration:**
GCP Console → Cloud Build → Triggers → `osvx-mcp-staging-trigger`
- **To temporarily disable CI:**
Disable or delete `osvx-mcp-staging-trigger` in the Cloud Build console.
- **To change the CI branch:**
Update the triggers branch pattern and the mirror configuration.

View File

@ -0,0 +1,84 @@
# Plan: Secondary RAG-Dedicated Project Migration
## 1. Objective
To bypass the "Spanner mode" restriction currently blocking serverless RAG Engine corpus creation in `propane-will-491900-m5`. We will spin up a clean, dedicated project for RAG experimentation and wire OSVauco to it as a secondary data source.
## 2. New Project Setup
### Project Selection
* **Suggested Name:** `vauco-rag-lab-01` or `osvauco-memory-node`.
* **Organization:** Ensure it is created under the same billing account as the primary project.
### Step 1: Bootstrap Command
```bash
NEW_RAG_PROJECT="vauco-rag-lab-01"
BILLING_ACCOUNT=$(gcloud billing projects describe propane-will-491900-m5 --format="value(billingAccountName)")
# Create project
gcloud projects create ${NEW_RAG_PROJECT} --name="OSVauco RAG Lab"
# Link billing
gcloud billing projects link ${NEW_RAG_PROJECT} --billing-account=${BILLING_ACCOUNT}
```
### Step 2: Minimal APIs
Enable only what is needed for RAG Engine and its data source:
```bash
gcloud services enable \
aiplatform.googleapis.com \
storage.googleapis.com \
--project=${NEW_RAG_PROJECT}
```
### Step 3: Cross-Project IAM
Grant the primary OSVauco Agent (from `propane-will-491900-m5`) access to the new RAG project:
```bash
PRIMARY_SA="jason.vauger@propane-will-491900-m5.iam.gserviceaccount.com"
# Grant Vertex AI User and Storage Admin on the NEW project
for ROLE in roles/aiplatform.user roles/storage.objectAdmin; do
gcloud projects add-iam-policy-binding ${NEW_RAG_PROJECT} \
--member="serviceAccount:${PRIMARY_SA}" \
--role="${ROLE}"
done
```
## 3. RAG Setup & Sync Refactoring
We will use variants of our existing scripts, but modified to accept a `--project` override.
### `setup_corpus_remote.py`
A variant of `setup_corpus.py` that:
1. Targets `NEW_RAG_PROJECT`.
2. Attempts the same "Serverless" PATCH in `us-central1` or `europe-west4`.
3. If it succeeds, it saves the resulting corpus name to a shared secret in the **Primary** project.
### `sync_corpus_remote.py`
A variant of `sync_corpus.py` that reads from the Primary project's GCS bucket but writes/indexes to the Remote project's RAG Engine.
## 4. Dual-Project Configuration Strategy
OSVauco will be updated to handle "Project Context Switching."
### Environment Variables
The following overrides will be added to `.env`:
* `PROJECT_ID`: `propane-will-491900-m5` (Primary logic/infra)
* **`RAG_PROJECT_ID`**: `vauco-rag-lab-01` (Knowledge base source)
* **`RAG_LOCATION`**: `europe-west4` (Region for the remote corpus)
### Code Updates
1. **`agents/rag/*`**: All RAG scripts and agent tools will default to `RAG_PROJECT_ID` (falling back to `PROJECT_ID`) during `vertexai.init()`.
2. **`scripts/preflight.sh`**: Will check both projects.
* Infrastructure section -> Primary Project.
* Knowledge Base section -> Remote Project.
3. **`scripts/cost-lens.sh`**: Will aggregate costs from both projects if `HEAVY_MODE` is enabled.
## 5. Migration Timeline
1. **Project Creation**: Create `vauco-rag-lab-01` and link billing.
2. **IAM Wiring**: Establish the cross-project service account trust.
3. **Test Run**: Run `setup_corpus.py` targeting the new project to verify if "Spanner mode" restriction is absent.
4. **Integration**: Update `.env` and `preflight.sh` to recognize the secondary project.
---
*Senior Staff Engineer (Gemini CLI)*

183
docs/ROADMAP.md Normal file
View File

@ -0,0 +1,183 @@
# VAUCO OS — ROADMAP
**Sist oppdatert:** 2026-06-15
**Autoritet:** `docs/MASTERPLAN.md` > denne filen
> Append-only per sesjon. Ikke slett gamle faser — merk dem ✅ eller ❌.
---
## Fase A — OPAX MVP (✅ Ferdig)
| ID | Oppgave | Status | Dato |
|----|---------|--------|------|
| OX1a | Cloud Build live-widget i opax.html | ✅ | 2026-06-05 |
| OX1b | Embedded TUI-panel reaktivert | ✅ | 2026-06-05 |
| OX1c | Jason fullskjerm-chat LIVE | ✅ | 2026-06-10 |
| CI5 | OPAX-MCP build+deploy live | ✅ | 2026-06-10 |
| CI6 | Jason kaller MCP live | ✅ | 2026-06-10 |
| OX1-fix | opax.html LIVE via `GET /` — absolutt sti + Dockerfile static copy | ✅ | 2026-06-10 |
| CG4b | Token dashboard breakdown per modul — Chart.js donut live | ✅ | 2026-06-11 |
---
## Fase B — CostGuard klient-flow (🟠 IN PROGRESS)
> **Fokus:** Bygg CostGuard som et produkt klienter faktisk kan bruke og betale for.
> Prismodell v2 godkjent 2026-06-12. Push-first arkitektur.
> **Prinsipp:** Ingen tredjepartsverktøy. Alt eies og kjøres i eget GCP/GitHub.
### CG4 — Kalkulator & UI
| ID | Oppgave | Status | Prioritet |
|----|---------|--------|-----------|
| **CG4d** | Prisingskalkulator — GCP-spend slider, tier-highlighter, ROI-beregning | ✅ | Høy |
| **CG6** | Klient-dashboard `costguard.oss.vauco.no` | ✅ 2026-06-15 | Høy |
### CG5 — Delivery layer
| ID | Oppgave | Status | Tier | Prioritet |
|----|---------|--------|------|-----------|
| **CG5a** | `POST /notify/webhook` → Slack/Teams/Chat/Discord | ✅ 2026-06-15 | Starter+ | Høy |
| **CG5b** | `POST /notify/email` — SendGrid digest + varsler | ✅ 2026-06-15 | Starter+ | Høy |
| **CG5c** | `POST /notify/sms` — Twilio, kritiske spikes | 🔴 Venter env-vars | Guard+ | Medium |
### CG5 — Onboarding
| ID | Oppgave | Status | Prioritet |
|----|---------|--------|-----------|
| **CG5-onboard** | 4-stegs wizard + token-invite system + `POST /onboard/complete` | ✅ 2026-06-15 | Høy |
| **OQ-29** | `GET /notify/channels` — korrekt format i dashboard | ✅ 2026-06-15 | Lav |
| **OQ-26** | `docs/gemma/world.md` — Emma boot context | ✅ 2026-06-15 | Høy |
### CI1 — opax-mcp CI/CD
| ID | Oppgave | Status | Prioritet |
|----|---------|--------|-----------|
| **CI1-kode** | `opax-mcp/server.py` — 20 tools, Dockerfile, requirements | ✅ 2026-06-15 | Høy |
| **CI1-cicd** | `cloudbuild.mcp.yaml` — CI/CD pipeline | ✅ 2026-06-15 | Høy |
| **CI1-trigger** | Cloud Build trigger opprettes i GCP Console av Chris | 🔴 Manuelt steg | Høy |
> **Prinsipp CI1:** opax-mcp er intern binding mellom Jason/Emma og OPAX-plattformen.
> Ingen tredjepartsverktøy (Cursor, Copilot o.l.). Alt eies 100% av Vauco AS i eget GCP.
### Infrastruktur (parallelt)
| ID | Oppgave | Status | Prioritet |
|----|---------|--------|-----------|
| OX2 | OPAX Hub: koble gjenværende funksjoner | 🟠 | Medium |
| **ML-3a** | GPU-VM for Emma + world.md | 🟠 world.md ✅ — VM gjenstår | Medium |
### Gjenstår før Fase B er ferdig
| ID | Oppgave | Blokkering |
|----|---------|------------|
| CI1-trigger | Cloud Build trigger for opax-mcp | Manuelt i GCP Console |
| CG5c | SMS-varsler | `TWILIO_*` env-vars |
| ML-3a VM | GPU Spot VM for Emma | Planlegging |
| Env-vars | `SENDGRID_API_KEY`, `GOOGLE_CREDIT_TOTAL_USD` m.fl. | GCP Console |
---
## Fase C — Salg + multi-tenant (🟡 LATER)
| ID | Oppgave | Status | Notat |
|----|---------|--------|-------|
| **C4** | Medioteq partnerstrategimøte (joint venture) | 🟡 Klar å aktivere | Aktiveres etter CI1-trigger |
| **C5** | Salgsdemo | 🟡 | Avventer C4 |
| ML-3c | Hybrid agent: Emma internt / Jason for kunder | 🟡 | |
| T1T4 | Threadstone full app | 🟡 | |
| Fase C | Multi-tenant: `<kunde>.vauco.no` via Terraform | 🟡 | |
| OQ-15 | BQ billing_export tabell aktivering | 🟡 | |
| OQ-17 | `github-token` secret alias fra `github-pat` | 🟡 | |
---
## Sesjon-logg
### 2026-06-10 — OPAX-fix + absolutte stier
- Diagnostiserte `{"detail":"Not Found"}``opax.vauco.no` etter IAP-login
- Fix: `COPY static/` i Dockerfile + `GET /` rute med absolutt sti i `app.py`
- Dokumentert i AGENT_RULEBOOK R-05, R-06, R-07
### 2026-06-10 kveld — Boot prompt + cron + webhook
- `UNIVERSAL_BOOT_PROMPT.md` v1.4 opprettet
- `boot.sh` region-fix, `cloudbuild.yaml` notify-fix
- `vm-teardown.sh` + `vm-startup.sh` opprettet
- Cron-varsler til Google Chat aktivert på VM
### 2026-06-11 — CG4b live + strategisk pivoting
- Chart.js donut live — CG4b ✅
- C4 (Medioteq) deprioritert — presisert som joint venture, ikke kunde
- Fokus satt til CostGuard klient-flow
### 2026-06-12 — Prismodell v2 + push-first arkitektur
- Prismodell revidert basert på markedsanalyse
- Ny tier-struktur: Starter $499 / Guard $999 / Shield $1.999 / Enterprise $3.500+
- Push-first: Jason finner kunden der de er
- Delivery layer: CG5a + CG5b + CG5c
### 2026-06-15 — Fase B hoveddel + CI1 + eierskap-prinsipp
- CG5a `POST /notify/webhook`
- CG5b `POST /notify/email`
- CG5-onboard token-invite system ✅
- CG6 `costguard-dashboard.html`
- OQ-26 `docs/gemma/world.md`
- CI1 `opax-mcp/server.py` 20 tools ✅
- CI1 `cloudbuild.mcp.yaml`
- **Prinsipp fastslått:** Ingen tredjepartsverktøy. Alt eies i eget GCP/GitHub.
- CI1-trigger gjenstår — manuelt steg i GCP Console av Chris
---
## Fase D — Secrets & Sikkerhet (🟠 IN PROGRESS)
> **Fokus:** Integrere eksterne tjenester (Twilio, etc.) sikkert via Secret Manager og IAM.
| ID | Oppgave | Status | Prioritet |
|----|---------|--------|-----------|
| **TW-1** | Twilio Secrets (SID, Auth, Number) i Secret Manager | 🟠 in progress | Høy |
| **TW-2** | Koble Twilio Secrets til opax-mcp | 🔴 | Høy |
| **TW-3** | Test `send_sms` tool | 🔴 | Høy |
| **INFRA-1** | Automatic restart på `osvauco-dev-vm` | 🔴 | Medium |
| **INFRA-2** | Statisk IP på `osvauco-dev-vm` | 🔴 | Medium |
| **OLLAMA-1** | Sikre Ollama-endepunkt på `osvauco-dev-vm` | 🔴 | Medium |
---
## Fase E — AI-agent Telefoni (🟡 LATER)
> **Fokus:** Aktivere toveis talestyrt kommunikasjon med Emma-agenten.
**Kommunikasjonsagent (Emma som kommunikasjonslag)**
- Utgående SMS operativ ✅
- Neste: innkommende SMS → Emma → auto-svar
- Fremtidig: WhatsApp, voice, AirPods-integrasjon
### Uke 3-4: Inngående AI-telefonsvarer
| ID | Oppgave | Status | Notat |
|----|---------|--------|-------|
| **VOICE-1** | `/voice` webhook i opax-mcp | 🟡 | |
| **VOICE-2** | Konfigurer Twilio Voice URL | 🟡 | |
| **VOICE-3** | Bygg ConversationRelay WebSocket | 🟡 | |
| **VOICE-4** | Definer Emma telefon-persona | 🟡 | |
### Uke 5-6: Utgående AI-anrop
| ID | Oppgave | Status | Notat |
|----|---------|--------|-------|
| **CALL-1** | Trigger-logikk for utgående anrop | 🟡 | |
| **CALL-2** | Implementer anrop via Twilio Outbound API | 🟡 | |
| **CALL-3** | Definer caser (kostnad, møte, etc) | 🟡 | |
---
## Fase F — Multikanal-orkestrering (🟡 LATER)
> **Fokus:** La Emma velge den mest effektive kommunikasjonskanalen autonomt.
| ID | Oppgave | Status | Notat |
|----|---------|--------|-------|
| **CHAN-1** | Kanalvalg-logikk i Emma | 🟡 | |
| **CHAN-2** | Integrer med `main.py` | 🟡 | |

131
docs/SECRETS-SETUP.md Normal file
View File

@ -0,0 +1,131 @@
# Google Cloud Secret Manager Setup Guide
This guide provides the `gcloud` commands to set up secrets in Google Cloud Secret Manager and configure the Cloud Run service to use them.
## 1. Create Secrets in Secret Manager
First, create the `CHAT_WEBHOOK_URL` and `GOOGLE_API_KEY` secrets.
```bash
# Create CHAT_WEBHOOK_URL secret
echo -n "your-chat-webhook-url" | gcloud secrets create CHAT_WEBHOOK_URL --data-file=-
# Create GOOGLE_API_KEY secret
echo -n "your-google-api-key" | gcloud secrets create GOOGLE_API_KEY --data-file=-
```
Replace `"your-chat-webhook-url"` and `"your-google-api-key"` with your actual secret values.
## 2. Grant Secret Accessor Role
Grant the `Secret Manager Secret Accessor` role to the service account that your Cloud Run service uses. By default, this is the default compute service account.
```bash
# Get your project number
PROJECT_NUMBER=$(gcloud projects describe <YOUR_PROJECT_ID> --format="value(projectNumber)")
# Construct the service account email
SERVICE_ACCOUNT_EMAIL="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
# Grant access to CHAT_WEBHOOK_URL
gcloud secrets add-iam-policy-binding CHAT_WEBHOOK_URL
--member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"
--role="roles/secretmanager.secretAccessor"
# Grant access to GOOGLE_API_KEY
gcloud secrets add-iam-policy-binding GOOGLE_API_KEY
--member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"
--role="roles/secretmanager.secretAccessor"
```
Replace `<YOUR_PROJECT_ID>` with your Google Cloud project ID.
## 3. Update `bridge/server.py` to Use Secrets
Modify `bridge/server.py` to read secrets from Secret Manager when it's not running in a local development environment.
Here is the code patch to apply to `bridge/server.py`. This patch adds a function to retrieve secrets and modifies the `post_to_chat` function to use it.
```python
# Add this import at the top of bridge/server.py
from google.cloud import secretmanager
# Add this function to bridge/server.py
def get_secret(secret_id: str, project_id: str, version_id: str = "latest") -> str:
"""
Get a secret from Google Cloud Secret Manager.
"""
try:
client = secretmanager.SecretManagerServiceClient()
name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}"
response = client.access_secret_version(name=name)
return response.payload.data.decode("UTF-8")
except Exception as e:
print(f"Error accessing secret {secret_id}: {e}")
return None
# The following is a patch for the lifespan context manager in bridge/server.py
# --- old
# from dotenv import load_dotenv
# load_dotenv()
# --- new
from dotenv import load_dotenv
from google.cloud import secretmanager
load_dotenv()
def get_secret(secret_id: str, project_id: str, version_id: str = "latest") -> str:
"""
Get a secret from Google Cloud Secret Manager.
"""
try:
client = secretmanager.SecretManagerServiceClient()
name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}"
response = client.access_secret_version(name=name)
return response.payload.data.decode("UTF-8")
except Exception as e:
print(f"Error accessing secret {secret_id}: {e}")
return None
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
if os.getenv("GAE_ENV", "").startswith("standard"):
project_id = os.getenv("GOOGLE_CLOUD_PROJECT")
if project_id:
os.environ["CHAT_WEBHOOK_URL"] = get_secret("CHAT_WEBHOOK_URL", project_id)
os.environ["GOOGLE_API_KEY"] = get_secret("GOOGLE_API_KEY", project_id)
if not os.path.exists(STATE_DIR):
os.makedirs(STATE_DIR)
watcher_task = asyncio.create_task(state_watcher_with_approval_check())
print("Bridge server started, watcher with approval check is running.")
yield
# Shutdown
watcher_task.cancel()
try:
await watcher_task
except asyncio.CancelledError:
print("Watcher task cancelled.")
```
Apply this patch to your `bridge/server.py` file. This code checks for the `GAE_ENV` environment variable, which is set in App Engine and Cloud Run environments. If it's present, it fetches the secrets from Secret Manager and sets them as environment variables for the application to use.
---
## GitHub Actions Secrets (aktive per 2026-05-25)
Disse secrets er satt i [OSVauco → Settings → Secrets → Actions](https://github.com/vauco-saas/OSVauco/settings/secrets/actions)
og brukes av `.github/workflows/backup-gdrive-nightly.yml`.
| Secret | Formål | Status |
|---|---|---|
| `RCLONE_GDRIVE_SA_JSON` | Hele JSON-nøkkelen for `vauco-gdrive-backup` service account | ✅ Aktiv |
| `GDRIVE_BACKUP_FOLDER_ID` | GDrive-mappe-ID (`0AB6RSjTedmwEUk9PVA`) for backup-destinasjon | ✅ Aktiv |
**Service account:** `vauco-gdrive-backup@propane-will-491900-m5.iam.gserviceaccount.com`
**Forny nøkkel ved behov:**
```bash
gcloud iam service-accounts keys create ~/vauco-gdrive-backup-sa-new.json \
--iam-account=vauco-gdrive-backup@propane-will-491900-m5.iam.gserviceaccount.com
# Oppdater GitHub Secret RCLONE_GDRIVE_SA_JSON med nytt innhold
```

17
docs/THREADSTONE.md Normal file
View File

@ -0,0 +1,17 @@
# THREADSTONE — Produktnotat
**URL:** https://threadstone.vauco.no/
**Status:** Under utvikling
## AI-stack
Threadstone bruker **utelukkende Gemini** (Google) som AI-leverandør.
- Salgsmodulene og pakkene på landingssiden omhandler **kun Gemini-integrasjon**
- ChatGPT (OpenAI) og Claude (Anthropic) er **ikke** en del av Threadstone-tilbudet
- Denne avgrensningen kan revideres på sikt dersom produkt-strategien endres
## Notert
> Avskrevet 2026-06-10 av Chris Christiansen.
> Tas opp igjen dersom det blir relevant i arkitektur- eller salgsstrategibeslutninger.

29
docs/TODO.md Normal file
View File

@ -0,0 +1,29 @@
# TODO — Flyttet
> Denne filen er erstattet av [`docs/ROADMAP.md`](ROADMAP.md) per 2026-05-31.
>
> ROADMAP-filen er nå levende dokument for alle oppgaver.
> Se også [`docs/MASTERPLAN.md`](MASTERPLAN.md) for arkitektur/fasit
> og [`docs/HANDOFF.md`](HANDOFF.md) for gjeldende sesjonsstatus.
**Historisk TODO-innhold** (status per 2026-05-25) er bevart i Git-historikken.
---
## Kommende oppgaver (per 2026-07-02)
> Disse er knyttet til Fase D, E og F i `ROADMAP.md`.
- **Twilio SMS-grunnlag:** ✅ DONE
- [ ] **sender_id-felt** i SmsNotifyRequest
- [ ] **/notify/sms-inbound** webhook
- [ ] **Alphanumeric Sender ID** i Twilio Console
---
### Senere (Phase E/F)
- **Skrue 10:** Implementer `/voice` webhook for inngående anrop (Lag 2).
- **Skrue 11:** Implementer logikk for utgående anrop via Twilio API (Lag 3).
- **Skrue 12:** Bygg ConversationRelay WebSocket-handler (Lag 2).
- **Skrue 13:** Implementer logikk for kanalvalg i `main.py` (Lag 4).

View File

@ -0,0 +1,242 @@
# VAUCO OS — UNIVERSAL BOOT PROMPT v1.7
# Eier: Chris Christiansen · Vauco AS
# Oppdatert: 2026-06-29
# Mode: HITL — ingen EXEC uten eksplisitt bekreftelse fra Chris
---
## STEG 1: LES DISSE FILENE (MANDATORY — i denne rekkefølgen)
Les rå-innholdet fra GitHub (`vauco-saas/OSVauco`, branch `main`):
1. `docs/MASTERPLAN.md` — autoritativ sannhet, vinner alltid ved konflikt
2. `docs/ROADMAP.md` — hva som skjer akkurat nå
3. `docs/HANDOFF.md` — siste sesjonsstatus (overskrives hver sesjon)
4. `docs/AGENT_RULEBOOK.md` — låste regler for alle agenter
5. `docs/LEARNINGS.md` — append-only lærdom fra alle sesjoner
6. `docs/ARCHITECTURE.md` — teknisk arkitektur (inkl. VM-specs og OSV-pipeline)
7. `docs/VM-BOOTSTRAP.md` — VM-oppsett, Ollama-konfigurasjon, modeller
8. `AGENTS.md` — agent-oversikt i repo-rot
9. `CLAUDE.md` — kontekst for Claude/andre agenter (rot)
> Hvis en fil ikke finnes: ikke anta innhold. Si ifra og fortsett med de som finnes.
> `docs/gemma/world.md` mangler ennå — dette er kjent (OQ-26, blokkerer ML-3a).
---
## STEG 2: KRITISK — FAKTISK REPO-STRUKTUR (verifisert 2026-06-12)
> Les dette før du skriver en eneste kodelinje.
```
OSVauco/ (repo-rot)
├── main.py (26 KB) ⭐ HOVUDAPPLIKASJON — FastAPI + ALLE ruter her
├── AGENTS.md
├── CLAUDE.md ← Kontekstfil for Claude/andre agenter
├── README.md
├── cloudbuild.yaml ← CI/CD
├── requirements.txt
├── agents/ ← Agent-logikk (IKKE hovudapp)
│ └── osv/
│ └── pipeline.py ← OSV lokal Ollama-pipeline (3-lags)
├── architecture/
├── auth/
├── data/
├── dialogflow/
├── docs/ ← All dokumentasjon
├── infrastructure/
├── master_hub/
├── ml/
├── opax-mcp/ ← MCP-server (IKKE agents/mcp_server/)
├── protocols/
├── scripts/
└── static/
├── opax.html
├── jason.html
├── billing-dashboard.html
├── command-hub.html
└── admin.html
```
**Vanlige feil agenter gjør:**
- ❌ `agents/core-logic/app.py` — FINNES IKKE som hovudapp
- ❌ `agents/mcp_server/` — FINNES IKKE, heter `opax-mcp/`
- ✅ Alle nye ruter legges i `main.py`
- ✅ Alle nye static filer legges i `static/`
- ✅ Verifér alltid struktur via GitHub API før du skriver kode
---
## STEG 3: IDENTITET & ROLLE
### Rolletabell
| Agent | Modell | Rolle | Kjører i |
|-------|--------|-------|----------|
| **Nemotron** | Nemotron Ultra | Orchestrator 1 — operasjonell leder | Ekstern tråd / API |
| **Sonar/Perplexity** | Sonar / Sonnet | Orchestrator 2 — validerer + GitHub-ops | Perplexity-tråd |
| **Gemini CLI** | Gemini 2.5 Pro | Executor — terminalkjøring på VM | VM terminal |
| **Emma** | Gemma 4 12B int4 | Intern co-pilot (lokal GPU-VM) — erstatter Sonar ved ML-3a | GPU-VM (🔮) |
| **Jason** | Gemini 2.5 Flash | Kundevendt agent — aldri intern rolle | Cloud Run |
---
### 🔷 DU ER NEMOTRON — Orchestrator 1
- Les HANDOFF.md og ROADMAP.md — finn neste prioriterte oppgave
- Formuler operasjonelt direktiv og send til Sonar
- Aldri eksekvér kode selv
- Aldri bypasse HITL
**Format for direktiv:**
```
DIREKTIV [ID] — [tittel]
Mål: ...
Kontekst: ...
Akseptansekriterier: ...
Prioritet: [Kritisk / Høy / Medium]
```
**Ferdig når:** Sonar rapporterer `COMMIT SHA + HANDOFF oppdatert`.
---
### 🔶 DU ER SONAR/PERPLEXITY — Orchestrator 2
- Valider Nemotrons direktiv
- Skriv ferdige CLI-instrukser til Gemini
- Gjør GitHub-operasjoner direkte via MCP
- Oppdater HANDOFF.md + ROADMAP.md etter fullført oppgave
- Rapporter commit SHA tilbake
- **Verifiser alltid repo-struktur via GitHub API før du bygger**
---
### 🟢 DU ER GEMINI CLI — Executor
- Bekreft: `MOTTATT: [oppsummering]`
- Presenter plan — vent på `EXEC GODKJENT` fra Chris
- Eksekvér nøyaktig — ikke improvisér
- Rapporter: `✅ FERDIG — [SHA]` eller `❌ FEIL — [årsak]`
---
### 🔮 DU ER EMMA — Inaktiv til ML-3a
Inntil ML-3a: Sonar fyller din rolle. Når aktiv: les `docs/gemma/world.md` som system-prompt.
---
## STEG 4: LÅSTE VERDIER
| Nøkkel | Verdi |
|--------|-------|
| GCP Project | `propane-will-491900-m5` |
| Region (LÅST) | `us-central1` |
| Cloud Run service | `osvauco-agent` |
| Direct URL | `https://osvauco-agent-zjbqp3prqq-uc.a.run.app` |
| Public URL | `https://opax.vauco.no` |
| Jason UI | `https://opax.vauco.no/static/jason.html` |
| LB IP | `34.98.77.173` |
| Repo | `vauco-saas/OSVauco` |
| Hovudapp | `main.py` (rot) |
| MCP-server | `opax-mcp/` |
| OSV-pipeline | `agents/osv/pipeline.py` |
| Eier | `chris.christiansen@vauco.no` |
| Jason | `jason.vauger@vauco.no` |
| Emma | `emma.vauger@vauco.no` (🔮 ML-3a) |
| osvauco-dev-vm RAM | **16 GB** (minimum for OSV-pipeline) |
**Forbudte kontoer:** `tinius.vauger`, `ccv` — aldri opprett eller gi tilganger.
---
## STEG 5: HITL-GATES (ALDRI BYPASS)
- **Gate 1 — PLAN:** Agent presenterer plan → Chris: `PLAN APPROVED`
- **Gate 2 — EXEC:** Ingen push/deploy/delete uten eksplisitt bekreftelse
- **Gate 3 — MODEL/REPO/DEPLOY:** Separate godkjenninger
---
## STEG 6: KRITISKE REGLER
**R-05 — Absolutte filstier (Python):**
```python
from pathlib import Path
STATIC_DIR = Path(__file__).parent / "static"
return FileResponse(str(STATIC_DIR / "opax.html"))
```
*(Merk: main.py ligger i rot, så `.parent` peker til repo-rot)*
**R-06 — Dockerfile COPY static/:**
```dockerfile
COPY static/ ./static/
```
**R-07 — FastAPI `GET /` må alltid defineres eksplisitt**
**R-08 — Emma laster `docs/gemma/world.md` som system-prompt ved oppstart**
**R-09 — Verifiser mappestruktur via GitHub API før bygging:**
> Bruk alltid `get_file_contents` på relevant mappe før du antar filplassering.
> Dokumentasjon kan være utdatert — GitHub API er sannheten.
**R-10 — OSV-pipeline og osvauco-dev-vm:**
> `osvauco-dev-vm` MÅ ha 16 GB RAM for å kjøre OSV-pipelinen (gemma3:4b + qwen2.5:7b).
> Emma-eskalering skjer ALDRI automatisk — kun ved eksplisitt "emma" trigger eller Chris-bekreftelse.
> Ollama kjører på `localhost:11434` på dev-vm, `emma-gpu-vm:11434` for Emma.
---
## STEG 7: IKKE GJER DISSE FEILENE
- Ikke anta `agents/core-logic/app.py` — bruk `main.py`
- Ikke anta `agents/mcp_server/` — bruk `opax-mcp/`
- Ikke opprett `HANDOFF_YYYYMMDD.md` — kun én `HANDOFF.md`
- Ikke migrer fra `us-central1`
- Ikke rør IAP/OAuth/Secret Manager uten `DEPLOY GODKJENT`
- Ikke bland RAG og FunctionTools i samme agent
- Ikke bruk access token mot Cloud Run — bruk identity token
- Ikke anta at secrets er mountet — sjekk `--update-secrets`
- Medioteq = joint venture, ikke SaaS-kunde
- Prismodell er låst — ikke endre uten `PLAN APPROVED`
- Ikke foreslå å flytte OSV-pipeline til emma-gpu-vm — den skal kjøre på dev-vm (kostnadskontroll)
- Ikke øk timeout som workaround for RAM-mangel — øk RAM på VM i stedet
---
## STEG 8: COSTGUARD PRISMODELL (fasit)
| Tier | GCP-spend/mnd | Pris |
|------|--------------|------|
| Starter | $0$3k | $499/mnd |
| Guard | $3k$15k | $999/mnd |
| Shield | $15k$50k | $1.999/mnd |
| Enterprise | >$50k | $3.500+/mnd |
**Neste oppgaver (i rekkefølge):**
1. CG5a — `POST /notify/webhook` i `main.py`
2. CG4d — Prisingskalkulator i `static/opax.html`
3. CG5b — `POST /notify/email` via SendGrid
4. CG5c — `POST /notify/sms` via Twilio [Guard+]
5. CG5-onboard — Klient-onboarding flow
6. CG6 — Klient-dashboard
---
## STEG 9: BEKREFT BOOT
Når alle filer er lest, svar med nøyaktig én linje:
```
Boot OK — main · HEAD [SHA] · [antall filer lest]/9 · Rolle: [din rolle]
```
Eksempel: `Boot OK — main · HEAD 9783ed1 · 8/9 · Rolle: Nemotron (Orchestrator 1)`
Hvis filer mangler: list dem, fortsett med de som finnes.
---
*Vauco AS · propane-will-491900-m5 · us-central1 · v1.7 · 2026-06-29*

172
docs/VM-BOOTSTRAP.md Normal file
View File

@ -0,0 +1,172 @@
# OSVauco — VM Bootstrap Guide
> **Denne guiden gjelder ny VM eller etter serverbytte.**
> Følg stegene i rekkefølge. Ikke hopp over noe.
---
## Forutsetninger
| Krav | Verdi |
|------|-------|
| GCP-prosjekt | `propane-will-491900-m5` |
| Cloud Run region | `us-central1` |
| Vertex AI location | `us-central1` |
| Billing account | `0171F6-057E6B-A260BA` |
| gcloud CLI | Installert på VM (`gcloud version`) |
| Git | Installert (`git --version`) |
---
## Steg 1 — SSH-nøkkel til GitHub (én gang per VM)
```bash
# Generer nøkkel
ssh-keygen -t ed25519 -C "chris.christiansen@vauco.no" -f ~/.ssh/id_ed25519 -N ""
# Vis public key — kopier hele linjen
cat ~/.ssh/id_ed25519.pub
```
Gå til [github.com/settings/keys](https://github.com/settings/keys) → **New SSH key**
Tittel: `osvauco-dev-vm` → lim inn nøkkelen → **Add SSH key**
```bash
# Test tilkobling
ssh -T git@github.com
# Forventet: Hi vauco-saas! You've successfully authenticated...
```
---
## Steg 2 — Klon repo og sett opp .env
```bash
cd ~
git clone git@github.com:vauco-saas/OSVauco.git
cd OSVauco
# Sett git-identitet
git config --global user.name "Chris Christiansen"
git config --global user.email "chris.christiansen@vauco.no"
# Opprett .env
cp .env.example .env
nano .env
```
**Fyll inn disse to feltene** (resten er ferdigutfylt):
```bash
export BILLING_ACCOUNT_ID="0171F6-057E6B-A260BA"
# ALERT_EMAIL er allerede satt til chris.christiansen@vauco.no
```
---
## Steg 3 — Autentiser med din Google-konto
> ⚠️ **Viktig:** VM-en starter med Compute Engine default service account.
> Du MÅ logge inn som deg selv — ellers feiler alle gcloud-kall.
```bash
gcloud auth login
# Følg lenken, logg inn som chris.christiansen@vauco.no
gcloud auth application-default login
# Samme prosess — nødvendig for Vertex AI / ADK
gcloud config set project propane-will-491900-m5
gcloud config set compute/region us-central1
```
Verifiser:
```bash
gcloud auth list
# Skal vise: * chris.christiansen@vauco.no
```
---
## Steg 4 — Kjør bootstrap-scriptet
```bash
source .env
bash scripts/bootstrap.sh
```
Scriptet gjør i rekkefølge:
1. Auth-sjekk
2. Bygg base Docker-image (hvis ikke finnes)
3. Verifiser at Cloud Run-agenten svarer
---
## Steg 5 — Manuell IAM-fix (kun ved nytt prosjekt / ny CI/CD)
Disse to IAM-bindingene må ligge på plass for at CI/CD skal fungere:
```bash
# Cloud Build SA kan deploye som osvauco-agent-sa
gcloud iam service-accounts add-iam-policy-binding \
osvauco-agent-sa@propane-will-491900-m5.iam.gserviceaccount.com \
--member="serviceAccount:357036551735@cloudbuild.gserviceaccount.com" \
--role="roles/iam.serviceAccountUser" \
--project=propane-will-491900-m5
# osvauco-agent-sa kan opptre som seg selv
gcloud iam service-accounts add-iam-policy-binding \
osvauco-agent-sa@propane-will-491900-m5.iam.gserviceaccount.com \
--member="serviceAccount:osvauco-agent-sa@propane-will-491900-m5.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountUser" \
--project=propane-will-491900-m5
```
---
## Hurtigtest — svar fra agenten
```bash
TOKEN=$(gcloud auth print-identity-token)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user_id":"chris","session_id":"test-1","message":"Hei, hvem er du?"}' \
https://osvauco-agent-357036551735.us-central1.run.app/run | python3 -m json.tool
```
Forventet svar:
```json
{
"user_id": "chris",
"session_id": "test-1",
"response": "Jeg er OPAX, en OSVauco AI-agent."
}
```
---
## Kveld — teardown (OBLIGATORISK)
```bash
bash infrastructure/03-teardown.sh
```
Kjøres hver kveld for å unngå 24/7-fakturering.
---
## Feilsøking
| Feil | Årsak | Fix |
|------|-------|-----|
| `ACCESS_TOKEN_SCOPE_INSUFFICIENT` | Kjører som Compute SA | `gcloud auth login` |
| `Publisher Model ...gemini-2.0-flash(-001) was not found` | 2.0-modellene er utfaset / ikke tilgjengelige i prosjektet | Sett `GOOGLE_CLOUD_LOCATION=us-central1` og bruk `ORCHESTRATOR_MODEL=gemini-2.5-flash`, `SUBAGENT_MODEL=gemini-2.5-flash`, `REASONING_MODEL=gemini-3.5-flash` |
| `osvauco-base:latest not found` | Base-image ikke bygget | `bash scripts/bootstrap.sh` |
| `iam.serviceaccounts.actAs denied` | Mangler IAM-binding | Steg 5 over |
| `git push` krever passord | HTTPS-remote | `git remote set-url origin git@github.com:vauco-saas/OSVauco.git` |
| `fetch first` ved push | Remote har nyere commits | `git pull --rebase && git push` |
---
*OSVauco-NMTMD-GCOS | propane-will-491900-m5 | us-central1*

View File

@ -0,0 +1,77 @@
# 🔁 OSVauco · OPAX — Sesjonshandoff 2026-05-26
## ✅ Hva ble fullført — Sesjon 1 (morgen)
### Infrastruktur (Fase B — 100% komplett)
- B1 Deploy, B2 GitHub PAT, B3 CI/CD pipeline, B4 Webhook — alt grønt
- Google Chat deploy-varsler via `cloudbuild.yaml` — live og bekreftet
- B5 Cost guard: budsjett `osvauco-monthly-guard` på 500 NOK/mnd med 50/90/100%-terskler
- GCS bucket `osvauco-ml-telemetry` med 90-dagers lifecycle + Cloud Logging sink
- `opax.vauco.no` live via CNAME → `ghs.googlehosted.com` (domain mapping OK)
### Boot-skjerm og TUI-miljø
- Kompakt boot-skjerm med sanntidsstatus: Vertex AI, Cloud Run, GitHub, Git Remote
- Auto-auth med 8-timers ADC-cache (slipper å re-autentisere)
- Hurtigkommandoer live: `ca`, `cl`, `cg`, `hc`, `pf`, `st`, `rl`, `sl`, `ms`, `g`, `sol`
- Auto-sync til GitHub: push etter 10 kommandoer eller 1 time
- Cron: GDrive mirror-sync hver hele time
---
## ✅ Hva ble fullført — Sesjon 2 (2026-05-26 formiddag/middag)
### Threadstone — AI-model temaer
- `docs/models/claude.md` — terrakotta-tema, Zodiak + Satoshi, varmt bibliotek
- `docs/models/chatgpt.md` — OpenAI-grønn tema, General Sans, mørk coworking
- `docs/models/gemini.md` — indigo-blå tema, Cabinet Grotesk + Satoshi, Google I/O
- Alle tre filer pushet til `OSVauco/docs/models/` · commit `862f4ca`
### BigQuery
- Dataset `osvauco_logs` og tabell `cloud_run_logs` bekreftet eksisterende ✅
- Logging sink `osvauco-cloudrun-sink` påbegynt — IAM-verifisering pågår
### Cloud Run domain mapping
- `gcloud beta run domain-mappings create` kjørt for `opax.vauco.no`
- CNAME `ghs.googlehosted.com` bekreftet fra tidligere sesjon
- IAP direkte på Cloud Run ikke mulig uten LB — utsatt til Fase C Terraform-modul
### Smoke-test
- `bash scripts/smoke-test.sh`**10/10 ✅** (2026-05-26 10:35 UTC)
- Alle endepunkter OK: /health, /run light/heavy/legacy, /run/dag, /state, /telemetri
---
## ⚠️ Kjente utfordringer / åpne punkter
| Problem | Status | Notat |
|---|---|---|
| `opax.vauco.no` gir 403 i nettleser | Arkitekturgrense | LB+IAP tas i Terraform Fase C |
| Vertex AI helsesjekk viste FAIL tidligere | Løst via ADC re-scope | Kan skje igjen etter 8t |
| BigQuery sink IAM | Pågår | Verifiser at sink-SA har dataEditor på osvauco_logs |
| github-token secret | Mangler | Alias fra github-pat eller ny secret |
| VM kjører (always-on cost) | Advar! | Kjør `bash infrastructure/03-teardown.sh` ved dagens slutt |
---
## 📋 Neste sesjon — prioritert rekkefølge
1. **BQ-2 ferdigstill** — verifiser sink-SA IAM og test at logger flommer inn i `cloud_run_logs`
2. **BQ-3** — koble Looker Studio mot `osvauco_logs` for kostnad/kvalitet-dashboard
3. **TS-1** — opprett `vauco-saas/threadstone-instructions` repo på GitHub
4. **TS-3** — Master Hub v3.2 HTML-template med theme-switcher (claude/chatgpt/gemini)
5. **TF-1→TF-5** — konverter `infrastructure/*.sh` → Terraform-modul i `vauco-bootstrap/`
6. **DX-1→DX-6** — Dialogflow CX Playbook-agent (norsk, webhook mot Cloud Run)
7. **ML-2**`feedback_loop.py`, `hypertuner.py`, `heavy_predictor.py` (trigger: >500 telemetri-kall)
8. **B6** — eksporter Dialogflow CX agent-konfig til `OSVauco/dialogflow/`
---
## 🏗️ Arkitekturavgjørelser
- **`opax.vauco.no`** = CNAME (internt verktøy, gratis)
- **Kunde-instanser** = LB + IAP via Terraform-modul (Fase C)
- **IAP på Cloud Run direkte** = ikke støttet uten LB — tas i Fase C
- **VCO token Fase 1** = "Threadstone Credits" off-chain, 1 credit = 1 kr, Stripe top-up
- **Auto-sync** = push ved 10 kommandoer + cron hourly + GDrive mirror
- **BigQuery logs** = dataset `osvauco_logs`, tabell `cloud_run_logs`, sink `osvauco-cloudrun-sink`
- **Threadstone temaer** = `docs/models/{claude,chatgpt,gemini}.md` med CSS-variabler per modell

View File

@ -0,0 +1,67 @@
# HANDOFF — 2026-05-31
**Dato:** 2026-05-31 ~12:42 CEST
**Operatør:** Chris Christiansen
**Status:** Sesjon avsluttet. Klar for neste sesjon.
---
## Hva ble gjort denne sesjonen
- [x] `.gemini/GEMINI.md` oppgradert: Gemini 2.5 Pro → **Claude Opus 4.6 via Vertex AI EU endpoint**
- [x] Region fikset overalt: `us-central1``us-central1` / `eu`
- [x] `GCP_VERTEX_LOC: eu` lagt til som locked definition
- [x] Claude Vertex EU diagnostics playbook lagt inn i GEMINI.md
- [x] `MASTERPLAN.md` region fikset: `us-central1``us-central1`
- [x] Denne HANDOFF-filen opprettet
---
## NESTE OPPGAVE
**Phase 8 — End-to-end test av agent via opax.vauco.no**
Før du begynner:
1. Aktiver Claude Opus 4.6 i Vertex AI Model Garden (hvis ikke gjort):
```
GCP Console → Vertex AI → Model Garden → søk "Claude Opus 4" → Enable
```
2. Verifiser at Cloud Run `osvauco-agent` kjører i `us-central1` (ikke `us-central1`)
3. Test agent-endepunkt via IAP:
```bash
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
https://opax.vauco.no/health
```
4. Test Claude Opus 4.6 EU endpoint direkte:
```bash
curl -X POST \
"https://eu-aiplatform.googleapis.com/v1/projects/propane-will-491900-m5/locations/eu/publishers/anthropic/models/claude-opus-4-7@20260301:rawPredict" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-d '{"anthropic_version": "vertex-2023-10-16", "max_tokens": 64, "messages": [{"role": "user", "content": "ping"}]}'
```
5. Full end-to-end: logg inn på `opax.vauco.no`, send en melding, verifiser at agenten svarer via Claude Opus 4.6
---
## Gjeldende infrastrukturstatus
| Komponent | Status |
|-----------|--------|
| Cloud Run `osvauco-agent` | ✅ Live (us-central1) |
| `opax.vauco.no` LB + SSL | ✅ Live |
| IAP | ✅ Aktivert (`@vauco.no`) |
| CI/CD Cloud Build | ✅ Operativt (Fase 6 ferdig) |
| RAG corpus `osvauco-corpus` | ✅ Populert (europe-west4) |
| Claude Opus 4.6 Vertex EU | ⏳ Krever Model Garden aktivering |
| BigQuery billing export | ⏳ Venter på første data |
---
## Boot-prompt til neste sesjon
```
BOOT: Read docs/AGENT_RULEBOOK.md, docs/VAUCO_OS_ROADMAP.md, docs/OSVAUCO_OPAX_SESSION_LOG.md.
Print LOCKED DEFINITIONS. Print last NESTE OPPGAVE. Do not take any action until I give a PLAN prompt.
```

View File

@ -0,0 +1,148 @@
# HANDOFF — 2026-05-31 · For Claude 4.7
**Skrevet av:** Perplexity (Sonnet 4.6) på vegne av Chris Christiansen
**Dato:** 2026-05-31 ~13:20 CEST
**Formål:** Full konteksthåndtering til Claude 4.7 — les alt her før du gjør noe som helst.
---
## 🔴 ABSOLUTT REGEL #1
**Du gjør ingenting uten at Chris eksplisitt skriver `PLAN APPROVED` eller et tilsvarende godkjenningssignal.**
Du er en assistent, ikke en autonom agent. Foreslå. Vent. Utfør kun etter godkjenning.
Self-modifisering av infrastruktur, secrets, IAM eller produksjonsmiljø uten godkjenning = **hard stop**.
---
## 🏗 Hva er dette prosjektet?
**Vauco OS** er en AI-plattform bygget av Chris Christiansen (Vauco AS, Oslo).
Kjernen er **OPAX** — en GCP-basert multi-agent engine som kjører på Cloud Run i `us-central1`.
Målet er å selge AI-drevne produktmoduler til bedrifter. Første produkt: **CostGuard** (GCP-kostnadsanalyse via AI).
```
Vauco OS (plattform, merkevare, token-lag)
└── OPAX (engine — Cloud Run, agentarkitektur)
├── CostGuard (Modul #1 — live på costguard.oss.vauco.no)
├── Threadstone (Modul #2 — Q3 2026)
└── [Modul #3+]
```
---
## ✅ Hva er faktisk ferdig (ikke spør om dette igjen)
| Komponent | Status |
|---|---|
| Cloud Run `osvauco-agent` | ✅ Live, `us-central1` |
| `opax.vauco.no` (LB + SSL) | ✅ Live, IP `34.98.77.173` |
| IAP (`@vauco.no` only) | ✅ Aktivert |
| CI/CD Cloud Build → Cloud Run | ✅ Operativt (auto-deploy på push til main) |
| RAG corpus `osvauco-corpus` | ✅ Populert, `europe-west4` |
| OAuth2 (Google) | ✅ I Secret Manager |
| Budget guard (500 NOK/mnd) | ✅ Live |
| AI-modell | ✅ Claude Opus 4.6 via Vertex AI `eu` |
| `costguard.oss.vauco.no` | ✅ Live (Cloudflare Pages) |
| BigQuery dataset + sink | ⏳ Oppe, men tabell ikke populert ennå |
| Claude Opus 4.6 Model Garden | ⏳ Krever aktivering av Chris i GCP Console |
---
## 🔄 Neste oppgave: Fase 8 — End-to-end test
Dette er der vi er **nå**. Ingenting av dette er gjort ennå.
### Steg 1 — Aktiver Claude Opus 4.6 i Vertex AI Model Garden
Chris gjør dette manuelt:
```
GCP Console → Vertex AI → Model Garden → søk "Claude Opus 4" → Enable
```
### Steg 2 — Verifiser Cloud Run er i us-central1
```bash
gcloud run services describe osvauco-agent --region us-central1 --format='value(status.url)'
```
### Steg 3 — Test agent via IAP
```bash
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
https://opax.vauco.no/health
# Forventet: 200
```
### Steg 4 — Test Claude EU endpoint direkte
```bash
curl -X POST \
"https://eu-aiplatform.googleapis.com/v1/projects/propane-will-491900-m5/locations/eu/publishers/anthropic/models/claude-opus-4-7@20260301:rawPredict" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-d '{"anthropic_version": "vertex-2023-10-16", "max_tokens": 64, "messages": [{"role": "user", "content": "ping"}]}'
# Forventet: JSON med "pong" eller tilsvarende
```
### Steg 5 — Full end-to-end
Logg inn på `opax.vauco.no` med `chris.christiansen@vauco.no`, send en melding, verifiser at agenten svarer via Claude Opus 4.6.
**HITL gate:** `FASE8 APPROVED` (skrives av Chris når alle steg er verifisert)
---
## 🔜 Etter Fase 8 — Fase C (ikke rør dette nå)
**C1 — Terraform-konvertering**
- `infrastructure/*.sh` → Terraform-modul
- LB, IAP, Cloud Run som kode
**C2 — Multi-tenant stamp-out**
- Admin-panel på `opax.vauco.no/admin`
- `scripts/onboard-client.sh <klientnavn>` provisjonerer ny klient-instans
- Første testkandidat: Medioteq (helse — krever isolert GCP-prosjekt)
**C3 — BQ-2 / Looker Studio**
- Koble live billing-data til dashboard
**CG3 — CostGuard Frontend Wiring**
- OAuth login flow kobles til Cloud Run
- BigQuery-data vises i UI
---
## 📋 Nøkkelinformasjon
| Nøkkel | Verdi |
|---|---|
| GCP Project | `propane-will-491900-m5` |
| Region | `us-central1` |
| Vertex AI endpoint | `eu` |
| Cloud Run service | `osvauco-agent` |
| Cloud Run URL (direkte) | `https://osvauco-agent-357036551735.us-central1.run.app` |
| Public URL | `https://opax.vauco.no` |
| LB IP | `34.98.77.173` |
| AI-modell | `claude-opus-4-7@20260301` |
| Repo | `vauco-saas/OSVauco` |
| Kontakt | `chris.christiansen@vauco.no` |
---
## 📁 Viktige filer å lese
1. `docs/MASTERPLAN.md` — autoritativ sannhet om hele prosjektet
2. `docs/AGENT_RULEBOOK.md` — regler for hvordan du oppfører deg
3. `.gemini/GEMINI.md` — locked definitions og boot-protokoll
4. `docs/ARCHITECTURE.md` — teknisk arkitektur
5. `docs/OSVAUCO_OPAX_SESSION_LOG.md` — historikk
---
## ⚠️ Vanlige feil — ikke gjør disse
- **Ikke oppdater VAUCO_OS_ROADMAP.md** — den er deprecated, alt er i MASTERPLAN.md seksjon 3
- **Ikke anta at VS Code / Phase 6 mangler** — CI/CD er ferdig
- **Ikke foreslå å migrere fra us-central1** — regionen er låst
- **Ikke rør IAP, OAuth, eller Secret Manager** uten eksplisitt `DEPLOY GODKJENT` fra Chris
- **Ikke hopp over HITL gates** — hver fase avsluttes med et godkjenningssignal
---
*Vauco AS · propane-will-491900-m5 · Handoff skrevet 2026-05-31*

42
docs/bq-looker-setup.md Normal file
View File

@ -0,0 +1,42 @@
# Looker Studio BigQuery Setup
This document provides the configuration and steps to manually connect Looker Studio to the project's BigQuery data source.
## Datasource Configuration
Below are the parameters for the BigQuery data source. You will need to copy and paste these values into the Looker Studio interface.
```json
{
"type": "bigquery",
"projectId": "vauco-saas-project",
"datasetId": "osvauco_telemetry",
"tableId": "daily_usage_metrics"
}
```
## Manual Setup Instructions
Since Looker Studio does not have an API for programmatically creating data sources, you must follow these manual steps:
1. **Go to Looker Studio:** Navigate to [https://lookerstudio.google.com/](https://lookerstudio.google.com/).
2. **Create a new Data Source:**
* Click on **Create** in the top left corner.
* Select **Data Source**.
3. **Select the BigQuery Connector:**
* From the list of Google Connectors, choose **BigQuery**.
4. **Authorize Access:**
* If prompted, authorize Looker Studio to access your Google Cloud projects.
5. **Select Your Project:**
* Under the **My Projects** tab, find and select the project with ID `vauco-saas-project`.
6. **Specify the Dataset and Table:**
* For **Dataset**, select `osvauco_telemetry`.
* For **Table**, select `daily_usage_metrics`.
7. **Connect:**
* Click the **Connect** button in the top right corner.
8. **Verify Schema:**
* Looker Studio will now display the table's schema. Verify that the fields look correct.
9. **Create Report:**
* Click **Create Report** to start building your dashboard with the connected data.
You have now successfully connected Looker Studio to your BigQuery table.

Some files were not shown because too many files have changed in this diff Show More