73 Commits

Author SHA1 Message Date
Jaakko Vanhala
b8e8a83e49 Dockerfile.prod päivitetty Astro-frontendille
Muutokset:
- Vaihe 1: Node.js buildaa Astro-frontendin (frontend/dist/)
- Vaihe 2: wasm-pack buildaa Wasm-moduulin erikseen
- Vaihe 3: Hub rakennetaan Rustilla
- Vaihe 4: Yhdistetään dist/ + pkg/ + avatars/ + templates/ + GUIDE.md

STATIC_DIR=/app/frontend/dist (ei enää /app/static)
Avatarit, templates ja GUIDE.md kopioidaan erikseen koska
Astro ei kopioi public/-tiedostoja dist/:iin buildin yhteydessä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:04:49 +03:00
Jaakko Vanhala
3d6914974d Prompti-textarea kasvaa automaattisesti sisällön mukaan
Koko prompti näkyy kerralla kun avatarin klikkaa — ei scrollausta.
Textarea saa overflow:hidden + auto-height sekä avatessa että kirjoittaessa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:03:18 +03:00
Jaakko Vanhala
9aff2ec154 uv kauttaaltaan + Tarkkailijan raportti rakenteelliseksi markdowniksi
uv-päivitykset:
- Koodarin NEVER-lista: ei requirements.txt, ei pip, käytä uv
- Template pyproject.toml: PEP 621, uv-yhteensopiva
- Raportin Quick Start: uv sync + uv run uvicorn

Tarkkailijan raportti uudessa formaatissa:
- Overview (yksi kappale)
- Files (taulukko: tiedosto + tarkoitus)
- Quick Start (uv-komennot koodiblokissa)
- Docker (build + run koodiblokissa)
- API Endpoints (taulukko: method, path, description)
- Architecture (rakenne ja päätökset)
- Risk Assessment (taulukko: severity, issue)

Malli saa taulukkopohjat valmiina → täyttää ne oikealla datalla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:59:10 +03:00
Jaakko Vanhala
ecd4525a7f Review-korjauskierrokset nostettu 2→3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:51:41 +03:00
Jaakko Vanhala
7a3e5278b9 Review-korjausluuppi: DevOps tarkistaa korjaukset, max 2 kierrosta
Pipeline:
1. DevOps review → löytää virheitä
2. Koodari korjaa → päivittää files-objektin
3. DevOps review (kierros 2) → tarkistaa korjaukset
4. Jos yhä virheitä → Koodari korjaa uudelleen
5. LGTM tai max 2 kierrosta → eteenpäin

Terminaalissa näkyy kierrosnumero: "koodikatselmointi (kierros 2)"
LGTM merkitään vihreällä ✓-merkillä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:50:43 +03:00
Jaakko Vanhala
8dcf269b42 strip_code_fences: poistetaan kaikki backtick-rivit aggressiivisesti
Ollama tuottaa \`\`\`python ... \`\`\` -blokkeja vaikka system prompt
kieltää ne. Nyt kaikki rivit jotka alkavat \`\`\` suodatetaan pois,
myös keskeltä vastausta (useita koodiblokkeja per vastaus).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:47:03 +03:00
Jaakko Vanhala
cb16f35265 Tiedostot kulkevat agentilta toiselle: korjaukset päivittyvät, QA saa korjatun koodin
Kontekstiketju nyt:
1. Data → models.py
2. Koodari → schemas.py (saa models.py kontekstina)
3. Koodari → main.py (saa models.py + schemas.py)
4. Koodari → pyproject.toml
5. DevOps → review (saa kaikki tiedostot)
6. Koodari → korjaukset → parsitaan takaisin files-objektiin (--- filename ---)
7. QA → testit (saa KORJATUT tiedostot, ei alkuperäisiä)
8. DevOps → Dockerfile (saa kaikki tiedostot + testit)
9. Tarkkailija → README (saa kaiken)

Aiemmin: korjaukset menivät terminaaliin mutta eivät päivittyneet files-objektiin.
Nyt: korjattu koodi parsitaan --- filename --- -erottimilla takaisin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:44:01 +03:00
Jaakko Vanhala
b9d340b4b4 install.sh: Debian/Ubuntu-asennusskripti
Asentaa automaattisesti:
1. Build-työkalut (build-essential, pkg-config, libssl-dev)
2. Rust (rustup)
3. Node.js 22 (nodesource)
4. Ollama
5. qwen2.5-coder:3b -malli

Käyttö: ./network-poc/install.sh && ./network-poc/local.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:42:27 +03:00
Jaakko Vanhala
dd07e536f0 Korjattu duplikaatti const tst -määrittely kpnProject:ssa
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:39:51 +03:00
Jaakko Vanhala
9af481a022 DevOps-agentin prompti laajennettu staattiseksi koodianalyysiksi
9-kohdan checklist: importit, nimeämiset, tyypit, virheenkäsittely,
resurssivuodot, tietoturva, endpointit, Pydantic v2, täydellisyys.

Aiemmin 7 kohtaa, nyt 9 — lisätty: type hints, tietoturva (raw SQL,
hardcoded secrets), Pydantic v2 (model_dump, from_attributes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:36:57 +03:00
Jaakko Vanhala
529a30a6e1 Korjattu harhaanjohtava GPU-viesti: Ollama käyttää GPU:ta automaattisesti
Kun --no-default-features (ei wgpu/nvml), viesti on nyt:
"GPU-tunnistus ei käytössä. Ollama käyttää GPU:ta automaattisesti."
eikä "GPU:ta ei havaittu — CPU-moodissa" (joka oli väärä M2:lla).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:35:00 +03:00
Jaakko Vanhala
7d842529b1 Tarkkailijan raportti: klikkaa avataria → modal + kehäväri arvosanalla
Tarkkailijan vastaus alkaa VERDICT-rivillä:
- GREEN → vihreä kehä → "OK"
- ORANGE → oranssi kehä → "HUOMIOITA"
- RED → punainen kehä → "KRIITTISTÄ"

Kehäväri ja glow jäävät näkyviin pipelinen jälkeen.
Klikkaamalla Tarkkailija-avataria avautuu raportti-modal jossa
README.md renderöidään markdown-muotoiltuna (taulukot, koodi, listat).
Modal sulkeutuu ✕-napista tai klikkaamalla taustaa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:33:11 +03:00
Jaakko Vanhala
c731c18360 DevOps generoi Dockerfilen + Tarkkailija kirjoittaa README.md-raportin
Uudet pipeline-vaiheet:

DevOps — Dockerfile:
- python:3.12-slim + uv (astral-sh)
- Oikea COPY-järjestys (pyproject.toml → sync → source)
- Expose 8000, CMD uvicorn

Tarkkailija — README.md:
- Markdown-raportti joka sisältää:
  - Tiedostolista ja kuvaukset
  - Käyttöohjeet (uv + Docker)
  - API Endpoints -taulukko
  - Arkkitehtuurihuomiot
  - Riskiarviointi
- README.md lisätään projektikorttin tiedostoihin
  → avattavissa editorissa

Pipeline nyt: Data → Koodari → DevOps review → korjaukset →
QA testit → DevOps Dockerfile → Tarkkailija README → Valmis

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:29:45 +03:00
Jaakko Vanhala
5498eb6cbb Kaikki 6 agenttia osallistuvat pipeline-projektiin
Pipeline-vaiheet:
1. Data-agentti: models.py (tietokanta-asiantuntija)
2. Koodari: schemas.py, main.py (ohjelmistokehittäjä)
3. Koodari: pyproject.toml
4. DevOps: koodikatselmointi (importit, nimeämiset, virheet)
5. Koodari: korjaukset (jos DevOps löysi ongelmia)
6. QA: pytest-testit (test_main.py lisätään projektiin)
7. Tarkkailija: riskianalyysi (arkkitehtuuri, tietoturva)

Data-agentti valitaan automaattisesti models.py/database.py -tiedostoille.
Jokainen vaihe highlightaa oikean avatarin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:26:57 +03:00
Jaakko Vanhala
43f0aebf54 Poistettu pystylayout — highlight toimii vaakarivillä pipelinen aikana
Avatarit pysyvät aina vaakarivillä. Aktiivinen agentti saa
glow-highlightin kun pipeline etenee (koodari → testaaja → koodari).
Highlight poistuu kun pipeline valmistuu.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:23:06 +03:00
Jaakko Vanhala
6413f0238f Avatarit vaihtuvat pystyyn pipelinen aikana, aktiivinen agentti highlightataan
Pipeline käynnistyessä:
- Avataririvi vaihtuu pystyyn (column) → näkee kuka tekee mitä
- Aktiivinen agentti saa glow-highlightin (coder sininen, tester sininen)
- Korjausvaiheessa koodari highlightataan uudelleen

Pipeline valmistuttua:
- Palautetaan vaakarivi (row)
- Poistetaan highlight

Poistettu manuaalinen layout-toggle-nappi — layout on automaattinen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:17:30 +03:00
Jaakko Vanhala
6b0394586e Mermaid-kaaviot oppaaseen + avatareiden vaaka/pysty-toggle
Mermaid ladataan CDN:stä (esm module). Opas-sivun renderMd tunnistaa
\`\`\`mermaid -koodiblokit ja renderöi ne SVG-kaavioiksi dark-teemalla.

toggleAgentLayout vaihtaa avatareiden suunnan (row/column),
tallennetaan localStorageen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:11:01 +03:00
Jaakko Vanhala
108094b06a Opas-sivun markdown-renderöijä laajennettu: taulukot, inline-muotoilu, listat
Lisätty tuki:
- Taulukot (| header | ... | -parsinta, thead/tbody, border-collapse)
- Inline: **bold**, *italic*, \`code\` (kaikissa elementeissä)
- Numeroidut listat (1. 2. 3.)
- Parempi tyhjien rivien käsittely (8px spacer, ei <br>)
- Otsikkojen border-bottom h1/h2:lle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:01:54 +03:00
Jaakko Vanhala
d7c974792d Agenttien päivitys kysyy käyttäjältä: OK = oletukset, Peruuta = säilytä omat
Kun AGENTS_VERSION kasvaa eikä localStorage ole tyhjä, näytetään confirm-dialogi:
"Agenttien oletuspromptit on päivitetty. Haluatko ottaa uudet käyttöön?"
- OK: ylikirjoitetaan oletuksilla
- Peruuta: käyttäjän muokkaukset säilyvät

Ensimmäisellä käyttökerralla (tyhjä localStorage) ladataan oletukset ilman kysymystä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:00:08 +03:00
Jaakko Vanhala
1987eb57a0 Agenttien oletuspromptit päivittyvät automaattisesti (AGENTS_VERSION)
Kun AGENTS_VERSION kasvaa, localStorage ylikirjoitetaan uusilla oletuksilla.
Ei tarvitse enää manuaalisesti tyhjentää localStorage.removeItem('kpn-agents').
Kasvata AGENTS_VERSION aina kun oletusprompteja muutetaan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 06:58:30 +03:00
Jaakko Vanhala
12ca87415c Poistettu native-noden kovakoodattu system prompt — agentin prompti toimii nyt
Ollaman system-kenttä yliajoi agentin konfiguroiman promptin.
Nyt system-kenttää ei lähetetä ollenkaan — agentin prompti tulee
osana prompt-kenttää (kpnRun koostaa sen frontendissä).

Tämä mahdollistaa per-agentti promptien toimimisen myös natiivilaskennalla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 06:54:18 +03:00
Jaakko Vanhala
a0e52faa44 Localhost vapautettu IP-yhteysrajasta, tuotannon raja nostettu 20:een
Kehitysympäristössä (127.0.0.1) ei enää yhteysrajaa — useita
selainikkunoita ja native-nodeja voi yhdistää vapaasti.
Tuotannossa raja 10→20 per ulkoinen IP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:14:06 +03:00
Jaakko Vanhala
f910cd8c61 Kattavat promptit + opettavat tooltipit jokaiselle parametrille
Promptit laajennettu moninkertaisiksi jokaiselle agentille:
- Manageri: RULES + EXAMPLE OUTPUT -formaatti
- Koodari: 8 CRITICAL RULES + NEVER-lista (importit, nimeäminen, Pydantic v2)
- Data: SQLAlchemy-spesifit ohjeet (String(length), connect_args, sessionmaker)
- QA: pytest-testirakenne (5 testitapausta enumeroituna)
- DevOps: 7-kohdan CHECKLIST + LGTM/ISSUE-vastausformaatti
- Tarkkailija: 4-alueen arviointi + RISK-formaatti + SHIP IT/NEEDS WORK

Tooltipit (hover 💡):
- Temperature: milloin matala/korkea, suositukset per rooli
- Max tokens: milloin nostaa/laskea, ~1 token ≈ 4 merkkiä
- Top-K: milloin muuttaa, harvoin tarpeen
- Repetition penalty: miksi liian korkea rikkoo koodin
- System prompt: hyvän promptin rakenne (rooli → säännöt → esimerkit → kiellot)

Prompt-tekstikenttä kasvatettu 4 → 8 riviä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:11:11 +03:00
Jaakko Vanhala
91dc7579bc Per-agentti sampling-parametrit: temperature, top-k, max tokens, repetition penalty
Jokainen agentti saa omat parametrit jotka näkyvät avatarin konfigurointipaneelissa:
- Temperature: Manageri 0.5 (tarkka), Koodari 0.7, Testaaja 0.3 (deterministinen)
- Max tokens: Manageri 512, Koodari 1024, Testaaja 512
- Top-K ja Repetition penalty per agentti
- Sliderit reaaliaikaisilla arvoilla

Parametrit tallentuvat localStorageen agentin mukana.
Perustelut: manageri ja testaaja hyötyvät matalasta temperaturesta
(determinismi tärkeää), koodari tarvitsee enemmän tokeneita ja
hieman korkeamman temperaturen luovempiin ratkaisuihin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:03:11 +03:00
Jaakko Vanhala
90c9a7e4fa Asetukset-välilehti: kaikki LLM-parametrit muokattavissa UI:sta
Uusi "Asetukset"-tab jossa:
- System Prompt (tekstikenttä, Courier-fontti)
- Temperature (slider 0-1.5, reaaliaikainen arvo)
- Top-K (slider 1-100)
- Repetition Penalty (slider 1.0-2.0)
- Max Tokens (slider 64-4096)
- Stop-sekvenssit (yksi per rivi)
- Mallinvalinta (dropdown: 1.5B/3B/7B Q4/7B)
- "Palauta oletukset" -nappi

Kaikki tallentuvat localStorageen (kpn-settings).
Jokainen parametri selitetty hint-tekstillä.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:44:03 +03:00
Jaakko Vanhala
1216e016c2 Template-pohjainen projektipipeline + opettavat selitykset
Uusi lähestymistapa: sen sijaan, että malli keksii rakenteen tyhjästä,
sille annetaan mallipohja (template) joka sisältää:
- Tiedostojärjestys (models → schemas → main → pyproject.toml)
- Esimerkkikoodi jokaiselle tiedostolle (few-shot)
- Yksityiskohtaiset ohjeet (importit, nimeämiskäytännöt, patternit)

Jokainen vaihe selitetään terminaalissa:
💡 models.py — "Define the SQLAlchemy model. Always include engine
   with check_same_thread=False for SQLite..."
💡 Koodikatselmointi — "Testaaja tarkistaa importit, nimeämiset..."
💡 Tulos — "Aja: uv run uvicorn main:app --reload"

templates/fastapi-crud.json sisältää täydellisen esimerkkiprojektin
jota malli adaptoi käyttäjän kuvaukseen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:32:03 +03:00
Jaakko Vanhala
d85cab4bc0 Native-noden vastausten siivous: stop-sekvenssit + selitystekstien poisto
Stop-sekvenssit laajennettu: Please note, This is, Example, ```
strip_code_fences laajennettu poistamaan:
- Selitystekstit lopusta (Please note, This is a basic, Note that, ...)
- Johdantolauseet alusta (Sure!, Here is, Certainly!)
System prompt vahvistettu: "No 'Please note' or 'Here is' text"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:04:43 +03:00
Jaakko Vanhala
4fef8824e1 Kattavat oletuspromptit kaikille agenteille
Manageri: arkkitehtuuri + tiedostojako + pyproject.toml
Koodari: importit + Pydantic/SQLAlchemy-erottelu + moderni Python
Data: normalisointi + SQLAlchemy + migration-ystävälliset patternit
QA: pytest + TestClient + edge caset + mock
DevOps: koodireview + importtien tarkistus + LGTM-protokolla
Tarkkailija: riskianalyysi + tietoturva + arkkitehtuurihuomiot

Promptit näkyvät klikkaamalla avataria → konfigurointipaneeli.
Tyhjennä localStorage (kpn-agents) jotta uudet promptit tulevat voimaan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:59:19 +03:00
Jaakko Vanhala
009bf492c8 Parannetut koodarin promptit + token-raja 512→1024
Koodarin prompti sisältää nyt:
- Import-vihjeen: "from models import ..." aiemmista tiedostoista
- Nimeämisvihjeen: Pydantic-schemat (UserCreate) vs SQLAlchemy (User)
- "Include all necessary imports. Write complete, working code."

Native-noden max_tokens nostettu 512→1024 jotta CRUD-endpointit
mahtuvat yhteen vastaukseen.

Testattu API:n kautta: 3B-malli tuottaa nyt oikeat importit,
erilliset Pydantic-schemat ja kaikki 5 CRUD-endpointtia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:56:24 +03:00
Jaakko Vanhala
f7e0e8dff8 Status-palkki tunnistaa natiivisolmun: ei enää turhaa Wasm-latausta
Sivulatauksessa tarkistetaan onko hubissa jo laskentasolmu (natiivi/Wasm).
Jos on → "Valmis (natiivi)", llmReady=true, ei Wasm-latausta.
Jos 503 → Wasm-fallback "Alusta"-napista.

Poistettu automaattinen Wasm-käynnistys (ensureNode) sivulatauksessa
koska natiivisolmu hoitaa laskennan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:48:23 +03:00
Jaakko Vanhala
eb57ee7b92 Avatarit ja tyylit palautettu main-haarasta: glassmorphism-kortit, oikeat hahmot
Avatarit: karhunpentu (Manageri), kipina (Koodari), pesukarhu (Data),
susi (QA), laiskiainen (DevOps), aikuinen_susi (Tarkkailija).

CSS: glassmorphism-taustat, hover-animaatio, active-glow,
pyöristetyt reunat, varjostukset. Sama tyyli kuin main-haarassa.

Huom: tyhjennä localStorage (kpn-agents) selainkonsolin kautta
jotta uudet oletusavatarit tulevat voimaan:
  localStorage.removeItem('kpn-agents')

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:46:21 +03:00
Jaakko Vanhala
84d13153ed Korjattu: agents ja defaultAgents siirretty scriptin alkuun (let hoisting)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:40:35 +03:00
Jaakko Vanhala
8beac57b50 Agenttien konfigurointi: promptit, mallit, järjestys, omat agentit
Klikatessa avataria avautuu konfigurointipaneeli:
- Nimen muokkaus
- Mallinvalinta (0.5B/1.5B/3B/7B)
- System promptin muokkaus
- Pipeline-järjestys (drag & drop avatarit ja pipeline-tagit)
- Agentin poisto

"+" -nappi luo uuden agentin satunnaisella avatarilla.
Konfiguraatio tallennetaan localStorageen (kpn-agents).
Pipeline (kpn project) käyttää agenttien prompteja ja malleja.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:38:30 +03:00
Jaakko Vanhala
44067efdb6 Monaco-lataus refaktoroitu: singleton Promise, parempi virheenkäsittely
initMonaco palauttaa nyt aina saman Promisen (ei moninkertaista latausta).
AMD-loader virheet ja CDN-virheet napataan ja logitetaan.
monacoLoaded-lippu korvattu Promise-pohjaisella tilanhallinnalla.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:24:08 +03:00
Jaakko Vanhala
5528be1812 Korjattu monacoLoaded: siirretty scriptin alkuun ennen switchTab-kutsua
let ei hoistu — monacoLoaded pitää olla määritelty ennen initMonaco-kutsua.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:21:22 +03:00
Jaakko Vanhala
f4cf4c73b9 Korjattu syntaksivirhe: ylimääräinen }); poistettu openInEditorista
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:18:58 +03:00
Jaakko Vanhala
e19852a509 Korjattu Monaco: window.monaco asetetaan eksplisiittisesti AMD-loaderin jälkeen
openInEditor käyttää nyt async/await initMonacoa ja window.monaco-globaalia
eikä oleta AMD-moduulin scopea. Korjaa "monaco is not defined" -virheen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:16:20 +03:00
Jaakko Vanhala
6de0df365e Korjattu projektikortin JSON-parsintavirhe: tiedostot globaaliin muuttujaan
Koodin sisältämät lainausmerkit ja erikoismerkit rikkoivat data-files
HTML-attribuutin. Nyt tiedostot tallennetaan window._projectFiles[id]:hen
ja onclick-handlerit viittaavat siihen suoraan. Ei JSON DOM:ssa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:11:11 +03:00
Jaakko Vanhala
28f620f901 kpnRun lähettää suoraan hubille, Wasm-fallback vain 503:lla
Ei enää odoteta Wasm-latausta ennen API-kutsua. Jos natiivisolmu
on hubissa, vastaus tulee suoraan. Jos hub palauttaa 503 (ei solmua),
käynnistetään Wasm-fallback ja yritetään uudelleen.

Tämä korjaa tilanteen jossa native-node on jo käynnissä mutta
selain yritti silti ladata Wasmia ensin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:09:26 +03:00
Jaakko Vanhala
3497f66db7 Agenttiavatarit palautettu: AgentBar-komponentti viidellä roolilla
Manageri (pöllö), Koodari (kameleontti), Testaaja (rukoilijasirkka),
QA (kilpikonna), Data (norsu). Klikattavat, highlight aktiiviselle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:53:35 +03:00
Jaakko Vanhala
1c7362c9b0 Native-node oletusmalli: qwen2.5-coder:3b
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:46:48 +03:00
Jaakko Vanhala
9983c80ef1 Native-noden oletusmalli vaihdettu kvantisoiduksi: qwen2.5-coder:7b-instruct-q4_K_M
Q4-kvantisointi: ~4GB (vs. 7GB), ~40 tok/s M2:lla (vs. ~25 tok/s).
Parempi nopeus/laatu-suhde.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:35:11 +03:00
Jaakko Vanhala
fc1fb33d5e local.sh: käynnistää native-noden automaattisesti jos Ollama on käynnissä
Käynnistysjärjestys: frontend build → hub → native-node (jos Ollama löytyy).
Ilman Ollamaa käytetään selaimen Wasm-laskentaa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:28:56 +03:00
Jaakko Vanhala
3bee8e8020 Kaikki pipeline-vaiheet käyttävät qwen-coder -mallia (smollm-135m poistettu)
Testaaja ja QA saivat 503 koska smollm-solmua ei ole ladattu.
Kaikki agentit käyttävät nyt samaa qwen-coder -mallia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:25:04 +03:00
Jaakko Vanhala
f8ea5ed76e Yksinkertaistettu reititys: poistettu busy-tila ja jonotus
Pipelinen peräkkäiset kpnRun-kutsut saivat 503 koska hub merkitsi
solmun busyksi eikä vapauttanut sitä ajoissa. Reititetään aina
ensimmäiselle matchaavalle solmulle. LLM_BUSY suojaa Wasm-puolella.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:22:53 +03:00
Jaakko Vanhala
6c7c2d6dd3 local.sh: npm install automaattisesti jos node_modules puuttuu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:18:47 +03:00
Jaakko Vanhala
c179b4ab7e .gitignore: node_modules, dist, .astro poistettu gitistä
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:18:16 +03:00
Jaakko Vanhala
a8c4af0975 Frontend uudelleenrakennettu: Astro-komponentit, Wasm pääsäikeessä, ei Workeria
Vanha frontend siirretty temp/. Uusi rakenne:
- StatusBar.astro, Terminal.astro, Editor.astro, Guide.astro
- global.css erillinen
- Wasm pääsäikeessä (ei Worker — yksinkertainen, debugattava)
- Tab-completion, dropdown, projektikortti, Monaco, GUIDE.md
- Ei tokenisointia eikä koodilaboratoriota

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:17:39 +03:00
Jaakko Vanhala
e3fdb91ac5 local.sh: buildaa frontendin ja käynnistää hubin yhdellä komennolla
Käyttö: ./network-poc/local.sh → http://localhost:3000

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:42:24 +03:00
Jaakko Vanhala
9925079729 Korjattu laskenta: poistettu warmup (aiheutti busy-konfliktin) + parempi solmun odotus
Ongelma: warmup-prompt käynnisti inferenssin heti yhdistymisen jälkeen,
jolloin oikea kpnRun-prompt tuli solmulle kun se oli vielä busy.
Solmu lähetti llm_error mutta UI:ssa ei ollut käsittelijää → ikuinen odotus.

Korjaukset:
1. Poistettu warmup ensureCoderNode:sta — ei tarvita koska kpnRun
   käynnistää solmun automaattisesti
2. kpnRun odottaa coderWsReady-lippua max 15s (pollaus 500ms välein)
   kiinteän 1s viiveen sijaan
3. Selkeä virheilmoitus jos solmu ei käynnisty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:37:28 +03:00
Jaakko Vanhala
6031737f83 Laskentasolmu käynnistyy automaattisesti: kpnRun + refresh-autostart
Kaksi korjausta laskentaan:
1. kpnRun kutsuu ensureCoderNode() automaattisesti jos solmu ei ole
   vielä käynnissä — käyttäjän ei tarvitse muistaa kpn load
2. localStorage-autostart: jos malli oli ladattu ennen refreshiä,
   ensureCoderNode() ajetaan automaattisesti sivulatauksessa

Tämä korjaa "Ei vapaata solmua" -virheen kpn run coder -komennoissa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:06:51 +03:00
Jaakko Vanhala
b6a8fa2671 Monaco Editor -välilehti: selainpohjainen koodieditori agenttien tuottamalle koodille
Uusi "Editor"-välilehti jossa:
- Monaco Editor (VS Coden ydin) CDN:stä, dark-teema
- Tiedostopuu vasemmalla (klikataan tiedostoa)
- Välilehdet ylhäällä (useita tiedostoja auki)
- Kielitunnistus tiedostopäätteestä (Python, Rust, JS, ...)
- "Avaa editorissa" -nappi projektikorteissa

Monaco ladataan taustalla requestIdleCallback:llä — ei hidasta
sivun käynnistymistä. Editor alustetaan vasta kun sitä tarvitaan.

Projektikortin "Avaa editorissa" -nappi:
1. Avaa Editor-välilehden
2. Luo Monaco-mallit jokaiselle tiedostolle
3. Renderöi tiedostopuun ja välilehdet
4. Avaa ensimmäisen tiedoston editoriin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:04:59 +03:00
Jaakko Vanhala
0dc53dba1c Korjattu Worker-laskentasolmun timing: odotetaan WS-yhteyden avautumista
Ongelma: start_agent_node palautui heti ennen kuin WebSocket ehti avautua.
Worker lähetti 'started' ja warmup lähti liian aikaisin → hub ei löytänyt
solmua → "Ei vapaata solmua" -virhe.

Korjaukset:
1. worker.js: kuuntelee "Yhteys Hubiin avattu" -logia Wasmista ja
   resolveaa started-Promisen vasta sen jälkeen (15s timeout)
2. worker.js: onerror + onunhandledrejection käsittelijät
3. worker.js: console.error välitetään pääsäikeelle
4. index.astro: ensureCoderNode odottaa (await) workerStarted-Promisea
   ennen warmupia ja pending-promptin lähetystä

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:55:22 +03:00
Jaakko Vanhala
857afbe111 feat: complete revolution architecture modernization
- Decoupled robust frontend into an Astro framework in `frontend/`.
- Replaced direct WebSocket broadcast with Smart Routing to distribute workload only to idle capable nodes, preventing 503 errors and duplicate responses.
- Rewrote WASM panic points (`unwrap()` handling) into panic-safe match blocks in qwen_coder.rs preventing Node Web Workers from crashing.
- Integrated robust dynamic Three.js 3D visualization.
- Resolved mermaid and THREE.js frontend hydration issues.
2026-04-09 16:38:24 +03:00
Jaakko Vanhala
84b78eb9c6 GPU-tunnistus valinnainen: cargo run --no-default-features toimii ilman nvml/wgpu
Native-node kääntyy nyt macOS:llä ja muilla koneilla ilman NVIDIA-ajureita:
  cargo run --no-default-features  ← vain Ollama, ei GPU-tunnistusta
  cargo run                        ← oletus: GPU-tunnistus mukana (nvml + wgpu)

Feature flag "gpu-detect" kontrolloi nvml-wrapper ja wgpu -riippuvuuksia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:42:35 +03:00
Jaakko Vanhala
4f18377a3b Native-node lähettää NODE_API_KEY auth-viestissä hubille
Luetaan NODE_API_KEY-ympäristömuuttuja ja lisätään api_key-kenttä
auth-viestiin. Hub tarkistaa avaimen ja hylkää solmun jos se ei täsmää.

Käyttö:
  NODE_API_KEY=kpn_sk_abc123 HUB_URL=ws://hub:3000/ws cargo run

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:39:48 +03:00
Jaakko Vanhala
7f5bb45138 API-avain -autentikaatio natiivisolmuille
Natiivisolmujen (node_type: native) auth-viesti vaatii api_key-kentän
joka vastaa hubin NODE_API_KEY-ympäristömuuttujaa. Virheellinen avain
sulkee WebSocket-yhteyden.

Selainsolmut eivät vaadi avainta (Origin-validointi suojaa niitä).
Jos NODE_API_KEY ei ole asetettu, kaikki natiivisolmut hyväksytään
(kehitysympäristö).

Käyttö:
  Hub:  NODE_API_KEY=kpn_sk_abc123 cargo run
  Node: NODE_API_KEY=kpn_sk_abc123 cargo run

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:39:05 +03:00
973d7a69c7 Galleria: hahmokuvaukset ja ehdotetut roolit kaikille 21 avatarille
Jokaiselle avatarille lisätty:
- Ehdotettu rooli (Arkkitehti, CI/CD, UI/UX, Tech Lead, SRE jne.)
- Persoonakuvaus joka kuvaa hahmon luonnetta ja erikoisosaamista

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:57:21 +03:00
aebc64e76e Tofuist poistettu Playgroundilta (jää Agent Builder -esimerkiksi)
Poistettu: avatar-kortti, gallery-head, agentPrompts-entry,
avatarMap, värimapit. Tofuist on edelleen Agent Builderin
esimerkkiagentti ja docs/tofu-cheatsheet.md säilytetään.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:43:15 +03:00
48c832c61b Module import absoluuttiseksi: ./pkg/node.js → /pkg/node.js
Suhteellinen polku rikkoi sivun kun navigoitiin suoraan
/avatars/ tai muuhun alihakemistoon (selain yritti ladata
/avatars/pkg/node.js jota ei ollut).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:37:47 +03:00
8435bd32a9 Hahmogalleria: #gallery -tabi kaikilla 21 avatarilla
- Uusi välilehti: Galleria (navigointi #gallery)
- Grid-näkymä: avatar, nimi, rooli, tiedostonimi
- Klikkaa korttia → kopioi polku leikepöydälle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:33:25 +03:00
ece41dd622 Export CrewAI: generoi ZIP-projektin agenttitiimistä
Export-nappi Agent Builderissa generoi kipina-crew.zip sisältäen:
- crew.py: CrewAI agentit, tehtävät ja sequential pipeline
- Dockerfile: Python 3.12 + uv
- docker-compose.yml: Ollama + healthcheck + mallilataus + crew
- pyproject.toml: crewai[tools] riippuvuus
- .env: Ollama-asetukset
- README.md: käynnistysohjeet

Käynnistys: docker compose run crew uv run python crew.py "projekti"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:26:27 +03:00
c7f3b0d79f Agent Builder: tooltip-ohjeistukset kaikille kentille
- System Prompt: miten kirjoittaa hyvä prompti, esimerkit hyvä/huono
- Temperature: mitä arvot tarkoittavat (0.0–1.0+)
- Top-k: tokenivalinnan laajuus
- Max tokens: vastauksen pituus
- Malli: Ollama-malliesimerkit
- Docs: referenssidokumentin käyttö
- CSS .builder-tip ::after tooltip (white-space: pre-wrap)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:20:28 +03:00
8905b50f41 Builder-funktiot window-scopeen (onclick vaatii globaalin)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:54:01 +03:00
43b0612004 Agent Builder 2026-04-08 10:51:35 +03:00
599ac2d2d9 Agent Builder: kaikki 21 avataria valittavissa
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:50:49 +03:00
d1975bd55c Uudet Gemini-generoidut avatar-kuvat (13 kpl)
bear, beaver, chameleon, elephant, gecko, lion, mantis, owl,
penguin, serpent, spider, tortoise, walrus

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:50:21 +03:00
24a8139d3e Agent Builder UI: #builder -tabi, lomake, CRUD-integraatio
- Uusi välilehti: Agent Builder (navigointi #builder)
- Agenttilista: ladataan /api/v1/agents, näytetään kortteina
- Lomake: avatar-valitsin, rooli-template, malli, väri, docs, prompt, parametrit
- Tallenna → POST /api/v1/agents, Poista → DELETE /api/v1/agents/:id
- Avatar-grid: 8 valmista hahmoa valittavissa

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:45:31 +03:00
21aac49a52 Agent Builder: SQLite-taulu + REST API (GET/POST/DELETE)
- DB skeemaversio 3: agents-taulu (id, name, avatar, role, model, color,
  docs, prompt, temperature, top_k, max_tokens, repetition_penalty)
- CRUD: upsert_agent, get_agents, delete_agent
- API: GET/POST /api/v1/agents, DELETE /api/v1/agents/:id
- Oletusagentteja (is_default=1) ei voi poistaa

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:43:22 +03:00
8a5f1b753c AGENTBUILDER.md: Agent Builder -suunnitelma ja building blockit
Kuvaa hahmolomakkeen arkkitehtuurin: agenttiskeema, rooli-templatet,
malli-valitsin, avatar-grid, docs-kenttä, localStorage-tallennus,
export/import ja 4-vaiheinen toteutussuunnitelma.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:39:25 +03:00
1b0b5eb198 Eksaktit mallinimet agenteille: qwen-coder → qwen2.5-coder:7b
- Kaikki agentPrompts.model vaihdettu 'qwen-coder' → 'qwen2.5-coder:7b'
- Native-node selected_task: 'qwen2.5-coder:7b'
- Hub-reititys: qwen-perhe matchaa keskenään (selain qwen-coder-05b,
  natiivi qwen2.5-coder:7b) taaksepäin yhteensopivuuden vuoksi

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:33:43 +03:00
44c8a189b6 Tofuist malli qwen2.5-coder:7b, hub-reititys laajennettu
- Tofuist-agentin model vaihdettu qwen-coder → qwen2.5-coder:7b
- Hub: qwen2.5-coder:* matchaa nyt qwen-coder*-solmuille ja päinvastoin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:31:53 +03:00
1a58324689 Tofuist-agentti: OpenTofu/IaC-asiantuntija gecko-avatarilla
- Uusi agentti: Tofuist (gecko-avatar, oranssinkulta #e3a336)
- System prompt: HCL-koodi, moduulit, lifecycle, state encryption
- docs-kenttä: lataa automaattisesti /docs/tofu-cheatsheet.md referenssiksi
- kpnRun: tukee nyt agentin docs-kenttää (haetaan kerran, cachetetaan)
- OpenTofu-dokumentaatio haettu GitHubista + tiivistetty cheatsheet
- Avatar, gallery-head, värimapit ja pipeline-tuet lisätty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:29:42 +03:00
302 changed files with 74799 additions and 183 deletions

215
network-poc/AGENTBUILDER.md Normal file
View File

@@ -0,0 +1,215 @@
# Kipinä Agent Builder — Suunnitelma
Käyttäjä voi rakentaa omia agentteja "hahmolomakkeella": valitsee avatarin, roolin, kielimallin ja muokkaa prompteja. Agentit tallentuvat localStorageen ja ovat käytettävissä pipelineissa.
## Nykytila
```js
// Kovakoodattu agentPrompts-objekti
const agentPrompts = {
manager: { name: 'Manageri', model: 'qwen2.5-coder:7b', default: '...' },
coder: { name: 'Koodari', model: 'qwen2.5-coder:7b', default: '...' },
tofuist: { name: 'Tofuist', model: 'qwen2.5-coder:7b', docs: '/docs/tofu-cheatsheet.md', default: '...' },
// ...
};
```
**Ongelma:** Uuden agentin lisääminen vaatii koodimuutoksen index.html:ään.
## Tavoite
```
┌─────────────────────────────────────────────────────┐
│ Agent Builder -lomake │
│ │
│ ┌─────────┐ Nimi: [Tofuist ] │
│ │ 🦎 │ Rooli: [IaC / Infra ▼] │
│ │ avatar │ Malli: [qwen2.5-coder:7b ▼] │
│ └─────────┘ Docs: [/docs/tofu-cheatsheet.md] │
│ │
│ System Prompt: │
│ ┌─────────────────────────────────────────────┐ │
│ │ You are an OpenTofu/Terraform IaC specialist│ │
│ │ ... │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ LLM-parametrit: │
│ Temperature: [0.7] Top-k: [40] Max tokens: [512]│
│ │
│ [💾 Tallenna] [🗑️ Poista] [📤 Export JSON] │
└─────────────────────────────────────────────────────┘
```
## Building Blocks
### 1. Agenttiskeema
```js
{
id: 'tofuist', // uniikki tunniste
name: 'Tofuist', // näyttönimi
avatar: '/avatars/gecko_notext.png', // avatar-kuvan polku
role: 'iac', // rooli-template
model: 'qwen2.5-coder:7b', // eksakti Ollama-mallinimi
color: '#e3a336', // teemaväri UI:ssa
docs: '/docs/tofu-cheatsheet.md', // valinnainen referenssidokumentti
prompt: 'You are an OpenTofu...', // system prompt
params: { // LLM-parametrit
temperature: 0.7,
top_k: 40,
max_tokens: 512,
repetition_penalty: 1.15
}
}
```
### 2. Rooli-templatet (alasvetovalikko)
Valmiit pohjat jotka tuovat oletuspromptit ja parametrit:
| Rooli | Oletusprompt | Parametrit |
|-------|-------------|------------|
| Koodari | "Kirjoita selkeää, testattavaa koodia" | temp 0.7, max 512 |
| QA / Testaus | "Kirjoita testejä, etsi virheitä" | temp 0.4, max 512 |
| DevOps | "Dockerfile, Compose, CI/CD" | temp 0.5, max 512 |
| DevSecOps | "Tietoturva-auditointi, OWASP" | temp 0.3, max 512 |
| Arkkitehti | "Järjestelmäsuunnittelu, rajapinnat" | temp 0.6, max 512 |
| IaC / Infra | "OpenTofu/Terraform HCL-koodi" | temp 0.5, max 512 |
| Data | "Tietokannat, SQL, datamallit" | temp 0.5, max 512 |
| Manageri | "Tehtävien jako ja koordinointi" | temp 0.8, max 200 |
| Kirjoittaja | "Dokumentaatio, README, ohjeet" | temp 0.8, max 512 |
| Vapaa | (tyhjä, käyttäjä kirjoittaa) | temp 0.7, max 512 |
### 3. Malli-valitsin
Lista saatavilla olevista malleista — haetaan dynaamisesti:
```
Hub-kysely: GET /api/models → palauttaa yhdistettyjen solmujen mallit
Tai staattinen lista:
- qwen2.5-coder:7b (oletus, natiivi GPU)
- qwen2.5-coder:1.5b (kevyt)
- qwen2.5-coder:0.5b (selain Wasm)
- deepseek-r1 (reasoning)
- llama3.2:3b (yleiskäyttö)
```
Pitkän aikavälin tavoite: hub ilmoittaa WebSocketin kautta mitkä mallit ovat saatavilla.
### 4. Avatar-valitsin
Valmiit avatarit + mahdollisuus ladata oma:
| Hahmo | Tiedosto | Eläin |
|-------|----------|-------|
| Asiakas | kettu_notext.png | Kettu |
| Manageri | karhunpentu.png | Karhunpentu |
| Koodari | kipina_notext.png | Salamanteri |
| Data | pesukarhu_notext.png | Pesukarhu |
| QA | susi_notext.png | Pikkususi |
| DevOps | laiskiainen_notext.png | Laiskiainen |
| Tarkkailija | aikuinen_susi.png | Aikuinen susi |
| Tofuist | gecko_notext.png | Gecko/Lisko |
| Arkkitehti | ??? | (tulossa) |
| DevSecOps | ??? | (tulossa) |
### 5. Docs-kenttä (referenssidokumentti)
Agentti voi viitata ulkoiseen dokumenttiin joka ladataan promptiin:
```
docs: '/docs/tofu-cheatsheet.md' → haetaan fetch():llä, cachetetaan _docsCache-kenttään
```
**Toiminta:**
1. Ensimmäisellä `kpnRun`-kutsulla ladataan docs-URL
2. Sisältö cachetetaan `agent._docsCache`-kenttään
3. Liitetään promptiin: `"Reference:\n" + docsContent`
4. Ei ladata uudelleen saman session aikana
**Rajoitukset:**
- Max ~3000 tokenia (~10 KB) — pidempi docs tiivistetään
- Vain tekstitiedostot (.md, .txt)
### 6. Tallennus (localStorage)
```js
// Tallennusavain
'kpn-custom-agents' JSON.stringify([ agentSkeema1, agentSkeema2, ... ])
// Ladattaessa
const customAgents = JSON.parse(localStorage.getItem('kpn-custom-agents') || '[]');
const defaultAgents = { manager: {...}, coder: {...}, ... };
const agentPrompts = { ...defaultAgents };
for (const agent of customAgents) {
agentPrompts[agent.id] = agent;
}
```
**Oletusagentit** (manager, coder, tester, qa, data) ovat aina mukana — niitä ei voi poistaa, mutta prompteja voi muokata.
**Käyttäjäagentit** (tofuist, arkkitehti, devsecops, ...) tallentuvat localStorageen ja latautuvat käynnistyksessä.
### 7. Export / Import
```js
// Export — JSON-tiedosto
const blob = new Blob([JSON.stringify(agent, null, 2)], { type: 'application/json' });
// → agent-tofuist.json
// Import — tiedoston valinta tai drag & drop
// Validoidaan skeema, lisätään agentPrompts-objektiin
```
Mahdollistaa agenttien jakamisen tiimin kesken.
## Toteutusvaiheet
### Vaihe 1: Hahmolomake UI
- Avatar-grid valitsin
- Rooli-template alasvetovalikko (täyttää oletuspromptit)
- Malli-valitsin
- System prompt -tekstikenttä
- LLM-parametrit (temperature, top-k, max_tokens)
- Tallenna/Poista-napit
### Vaihe 2: Dynaaminen agenttirekisteri
- `agentPrompts` ladataan localStoragesta
- Oletusagentit + käyttäjän agentit yhdistetään
- Avatar-kortit renderöidään dynaamisesti (ei HTML:ssä)
- Värimapit generoidaan agenttiskeemasta
### Vaihe 3: Pipeline käyttää dynaamisia agentteja
- Pipeline-vaiheet viittaavat agentin id:hen (ei kovakoodattuun nimeen)
- Käyttäjä voi valita mitkä agentit osallistuvat pipelineen
- Tofuist voi korvata DevOpsin IaC-projekteissa
### Vaihe 4: Mallirekisteri (hub-integraatio)
- Hub tarjoaa `/api/models`-endpointin
- Saatavilla olevat mallit näkyvät valitsimessa reaaliajassa
- Solmun liittyessä/poistuessa mallit päivittyvät
## Arkkitehtuurikaavio
```
┌──────────────────────────────────────────────────┐
│ Agent Builder UI │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Avatar │ │ Rooli │ │ Malli-valitsin │ │
│ │ Grid │ │ Template │ │ (hub/staattinen) │ │
│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
│ └─────────────┼───────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Agent Schema { id, name, avatar, model, │ │
│ │ role, color, docs, prompt, │ │
│ │ params } │ │
│ └──────────────────┬───────────────────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ localStorage Org Chart Pipeline │
│ (persist) (render) (execute) │
└──────────────────────────────────────────────────┘
```

View File

@@ -1,47 +1,57 @@
# syntax=docker/dockerfile:1
FROM rust:slim AS builder
RUN apt-get update && apt-get install -y \
curl pkg-config libssl-dev g++ \
&& rm -rf /var/lib/apt/lists/*
# --- Vaihe 1: Frontend (Astro) ---
FROM node:22-slim AS frontend
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json* ./
RUN npm install --silent
COPY frontend/ .
COPY frontend/public/pkg public/pkg
RUN npm run build
# --- Vaihe 2: Wasm (wasm-pack) ---
FROM rust:slim AS wasm-builder
RUN apt-get update && apt-get install -y curl pkg-config libssl-dev g++ && rm -rf /var/lib/apt/lists/*
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
WORKDIR /app
COPY Cargo.toml Cargo.lock* ./
COPY node/Cargo.toml node/Cargo.toml
COPY node/src node/src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cd node && wasm-pack build --target web --out-dir /app/wasm-pkg
# Kopioi kaikki Cargo-tiedostot
COPY Cargo.toml ./
COPY Cargo.lock* ./
# --- Vaihe 3: Hub (Rust) ---
FROM rust:slim AS hub-builder
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Cargo.toml Cargo.lock* ./
COPY hub/Cargo.toml hub/Cargo.toml
COPY hub/src hub/src
# Tarvitaan dummy-cratet jotta workspace kompiloi
COPY node/Cargo.toml node/Cargo.toml
COPY native-node/Cargo.toml native-node/Cargo.toml
COPY cli/Cargo.toml cli/Cargo.toml
# Kopioi lähdekoodi
COPY hub/src hub/src
COPY node/src node/src
COPY native-node/src native-node/src
COPY cli/src cli/src
COPY static static
# Rakenna Wasm — cache mount pitää Cargo-rekisterin ja target-kansion buildien välillä
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cd node && wasm-pack build --target web --out-dir ../static/pkg
# Rakenna Hub
RUN mkdir -p node/src native-node/src cli/src && touch node/src/lib.rs native-node/src/main.rs cli/src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release -p hub \
&& cp /app/target/release/hub /usr/local/bin/hub
# --- Vaihe 4: Tuotantoimage ---
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/bin/hub /usr/local/bin/hub
COPY --from=builder /app/static /app/static
COPY --from=hub-builder /usr/local/bin/hub /usr/local/bin/hub
COPY --from=frontend /app/frontend/dist /app/frontend/dist
COPY --from=wasm-builder /app/wasm-pkg /app/frontend/dist/pkg
# Kopioidaan GUIDE.md ja templates
COPY frontend/public/GUIDE.md /app/frontend/dist/GUIDE.md
COPY frontend/public/templates /app/frontend/dist/templates
COPY frontend/public/avatars /app/frontend/dist/avatars
WORKDIR /app
ENV STATIC_DIR=/app/static
ENV STATIC_DIR=/app/frontend/dist
EXPOSE 3000
CMD ["hub"]

3
network-poc/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
dist/
.astro/

View File

@@ -0,0 +1,2 @@
import { defineConfig } from 'astro/config';
export default defineConfig({});

4721
network-poc/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"name": "kipina-frontend",
"type": "module",
"version": "0.1.0",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"astro": "^6.1.5"
}
}

View File

@@ -0,0 +1,413 @@
# Kipinä Agentic Studio — Opas
Hajautettu AI-laskentaverkko jossa kielimallit ajavat koodia suoraan selaimessa.
Tämä opas selittää miten kielimallit toimivat, miten niitä ohjataan, ja miten
tuloksia voi parantaa.
---
## Kielimallit ja niiden koot
Kielimalli on neuroverkko joka ennustaa seuraavan sanan (tokenin) edellisten
perusteella. Mallin "koko" tarkoittaa parametrien (painojen) määrää:
| Malli | Parametrit | Koko levyllä | Nopeus selaimessa | Koodinlaatu |
|-------|-----------|-------------|-------------------|-------------|
| SmolLM 135M | 135 miljoonaa | ~270 MB | ~5 tok/s | Yksinkertainen teksti |
| Qwen2.5-Coder:0.5B | 500 miljoonaa | ~990 MB | ~3-6 tok/s | Pienet funktiot |
| Qwen2.5-Coder:3B | 3 miljardia | ~6.2 GB | ~0.4 tok/s | Kokonaiset tiedostot |
| GPT-4 (vertailu) | ~1800 miljardia | ~3.6 TB | pilvipalvelu | Kokonaiset projektit |
**Parametrien vaikutus:** Jokainen parametri on yksi liukuluku (float16 = 2 tavua)
joka tallentaa opittua tietoa. 0.5B-malli tietää perusrakenteet mutta tekee
loogisia virheitä. 3B-malli ymmärtää kontekstin paremmin. Ero on kuin sanakirjan
ja oppikirjan välillä.
**Miksi selaimessa?** Malli ajetaan käyttäjän omalla laitteella WebAssemblyn
kautta. Data ei lähde koneelta, eikä tarvita pilvipalvelua. Haittapuoli on
hitaus — GPU-palvelimella sama 0.5B-malli tuottaa ~100 tok/s.
---
## Tokenit — kielimallin "sanat"
Malli ei näe tekstiä kirjaimina vaan **tokeneina**. Tokeni on yleensä
sanan osa, kokonainen sana tai välilyönti. Tokenisaatio tehdään
BPE-algoritmilla (Byte Pair Encoding) joka oppii yleisimmät
merkkijonot harjoitusdatasta.
### Esimerkki: suomi vs. englanti
Alla oikea tokenisointitulos Qwen2.5-Coder-tokenisaattorilla. Jokainen
värikoodattu lohko on yksi tokeni — huomaa miten suomi vaatii enemmän
tokeneita saman merkityksen välittämiseen:
![Tokenisointivertailu EN/FI](/images/tokenization-example.png)
**Huomaa miten:**
- Englannin yleiset sanat (`the`, `in`, `a`, `function`) ovat kokonaisia tokeneita
- Suomen sanat pilkotaan pienempiin osiin (`Hajautettu` → 4 tokenia, `Distributed` → 2)
- Suomi vaatii **30-50% enemmän tokeneita** saman merkityksen välittämiseen
- Koodiavainsanat (`function`, `list`, `sort`) ovat tehokkaita molemmilla kielillä
### Miksi tämä merkitsee?
**Jokainen tokeni = yksi laskentakierros.** Jos suomi vaatii 50% enemmän tokeneita:
1. **Hitaampi vastaus:** 100 tokenin englanninkielinen vastaus ≈ 150 tokenia suomeksi
→ 50% pidempi odotusaika
2. **Pienempi konteksti:** Sama merkityssisältö vie enemmän tilaa konteksti-ikkunasta
3. **Huonompi ymmärrys:** Pitkät sanat pilkotaan osiin jotka malli ei välttämättä
tunnista → hallusinaatiot lisääntyvät
**Siksi tekniset promptit ovat englanniksi** — malli saa enemmän informaatiota
samassa token-budjetissa ja ymmärtää ohjeet paremmin.
**Token-budjetti tässä järjestelmässä:**
| Osa | Tokeneita | Osuus |
|-----|-----------|-------|
| System prompt | ~30 | kiinteä |
| Agent prompt | ~25 | kiinteä |
| Konteksti (aiemmat tiedostot) | 0-300 | kasvaa |
| Käyttäjän prompti | ~20-50 | vaihtelee |
| **Syöte yhteensä** | **~75-400** | |
| Generoitu vastaus (max) | 512 | raja |
| **Yhteensä** | **~600-900** | /32 768 |
Konteksti-ikkuna on reilusti riittävä. Pullonkaula ei ole ikkunan koko
vaan **mallin kyky ymmärtää pitkää kontekstia** — 0.5B-malli alkaa
"unohtaa" ohjeet kun konteksti kasvaa yli ~200 tokenin.
---
## Promptit — miten mallia ohjataan
### Kolmitasoinen prompttirakenne
```mermaid
flowchart TD
S["System prompt<br/><i>You are a coding assistant. Respond with ONLY code.</i><br/>🔒 Kiinteä, kovakoodattu — malli priorisoi tämän"]
A["Agent prompt<br/><i>Olet kokenut ohjelmistokehittäjä...</i><br/>✏️ Käyttäjän muokattavissa UI:ssa"]
U["User prompt<br/><i>Write ONLY the file main.py...</i><br/>📋 Vaihtelee joka kutsussa, sisältää kontekstin"]
P["Prefill: ``` <br/>🎯 Pakottaa mallin aloittamaan koodilla"]
S --> A --> U --> P
P -->|malli jatkaa| R["Generoitu koodi"]
style S fill:#1a1e2e,stroke:#f85149,color:#c9d1d9
style A fill:#1a1e2e,stroke:#d29922,color:#c9d1d9
style U fill:#1a1e2e,stroke:#3fb950,color:#c9d1d9
style P fill:#1a1e2e,stroke:#a371f7,color:#c9d1d9
style R fill:#0d1117,stroke:#58a6ff,color:#58a6ff
```
### Miksi promptit ovat englanniksi?
Qwen2.5-Coder on harjoitettu pääosin englanninkielisellä koodilla ja
dokumentaatiolla. Suomenkielinen ohje kuluttaa enemmän tokeneita JA
malli ymmärtää sen huonommin. Agenttien nimet ja käyttöliittymä ovat
suomeksi, mutta tekniset ohjeet mallille englanniksi.
Poikkeus: agenttipromptit ovat suomeksi koska ne menevät user-blokkiin
(ei system-blokkiin) ja niiden tarkoitus on enemmän "persoonallisuus"
kuin tekninen ohje.
---
## Prefill-tekniikka
Normaalisti malli päättää vapaasti miten vastaa:
```
Ilman prefilliä:
Malli: "Sure! Here is a Python program that prints Hello World:\n```python\nprint('Hello')\n```"
→ 25 tokenia, joista 15 turhia
Prefillin kanssa:
Me syötämme: ```
Malli jatkaa: python\nprint('Hello')\n```
→ 5 tokenia, kaikki hyödyllisiä
```
Prefill on kuin aloittaisit lauseen toisen puolesta — malli jatkaa
siitä mihin jäit sen sijaan, että aloittaisi kohteliaalla johdannolla.
**Sivuvaikutus:** Malli tuottaa kielitunnisteen (`python`, `rust`) ja
sulkevan ` ``` `:n. Nämä siivotaan jälkikäteen `strip_markdown_wrapper`-funktiolla.
---
## Sampling — miten malli valitsee seuraavan tokenin
Malli ei "tiedä" oikeaa vastausta. Se laskee jokaiselle mahdolliselle
seuraavalle tokenille todennäköisyyden ja valitsee yhden. Valintaa
ohjataan kolmella parametrilla:
### Temperature (0.7)
Kontrolloi "luovuutta" vs. "varmuutta":
```
Temperature 0.0 (greedy): Aina todennäköisin tokeni → "def fibonacci(n):"
Temperature 0.7 (oletus): Painottaa todennäköisiä mutta sallii vaihtelua
Temperature 1.5 (luova): Lähes satunnainen → "async lambda fib = ..."
```
0.7 on kompromissi: tarpeeksi determinististä tuottamaan toimivaa koodia,
mutta tarpeeksi vaihtelevaa välttämään toistoa.
### Top-k (40)
Rajaa valinnan 40 todennäköisimpään tokeniin. Estää mallia valitsemasta
täysin absurdeja vaihtoehtoja:
```
Ilman top-k: 150 936 vaihtoehtoa → voi valita minkä tahansa
Top-k 40: 40 vaihtoehtoa → järkevät vaihtoehdot
Top-k 1: 1 vaihtoehto → greedy (aina sama vastaus)
```
### Repetition penalty (1.15)
Vähentää jo tuotettujen tokenien todennäköisyyttä. Estää mallia
juuttumasta luuppiin:
```
Ilman rangaistusta: "print print print print print..."
Penalty 1.15: "print('Hello')\nprint('World')"
```
1.15 on lievä rangaistus — estää pahimman toiston mutta sallii
saman avainsanan (esim. `return`) esiintymisen useasti.
---
## Stop-sekvenssit — milloin generointi loppuu
Malli generoi tokeneita kunnes jokin näistä tapahtuu:
1. **EOS-tokeni** (151645): Mallin oma "loppu"-merkki
2. **Max tokens** (512): Kovakoodattu raja
3. **Stop-sekvenssi**: Malli alkaa tuottaa selitystä
```
fn fibonacci(n: usize) -> usize {
if n <= 1 { return n; }
fibonacci(n-1) + fibonacci(n-2)
}
← Tähän asti koodia, ok
// Example usage: ← Stop! Tämä ei ole enää vastausta
let result = fibonacci(10); ← Ei generoida
```
Tunnistetut stop-sekvenssit: `### `, `Explanation`, `Note:`, `Output:`,
`// Example`, `# Example`. Generointi katkaistaan ja teksti trimmataan
stop-kohtaan.
---
## Projekti-pipeline — miten agenttitiimi toimii
```mermaid
flowchart TD
U["Käyttäjä: FastAPI + SQLite REST API for users"] --> M
M["🟡 Manageri: Pilko tiedostoiksi"] -->|tiedostolista| C1
C1["🟢 Koodari: models.py"] -->|"konteksti: models.py"| C2
C2["🟢 Koodari: main.py"] -->|"konteksti: models + main"| C3
C3["🟢 Koodari: pyproject.toml"] -->|kaikki tiedostot| T1
T1["🔵 Testaaja: Review"] -->|bugeja löytyi| C4
T1 -->|LGTM| Done["✅ Projekti valmis"]
C4["🟡 Koodari: Korjaukset"] --> T2
T2["🔵 Testaaja: Uudelleenarviointi"] --> Done
```
**Kontekstin ketjutus** on kriittistä: kun koodari kirjoittaa `main.py`:tä,
se saa `models.py`:n sisällön promptissa. Ilman tätä se ei tietäisi
mitä luokkia importata.
**Riippuvuusjärjestys:** Manageria pyydetään listaamaan riippuvuudet ensin
(models.py ennen main.py) jotta kontekstiketju toimii oikeaan suuntaan.
---
## Laadun parantaminen
### 1. Isompi malli (suurin vaikutus)
| | 0.5B | 3B | Pilvi-API |
|---|---|---|---|
| Fibonacci | Joskus virheitä | Yleensä oikein | Aina oikein |
| FastAPI CRUD | Voi käyttää Flaskia | Oikea kirjasto | Täydellinen |
| Monimutkainen logiikka | Hallusinoi | Osaa perusasiat | Syvä ymmärrys |
| Nopeus (selain) | ~5 tok/s | ~0.4 tok/s | — |
| Latauksen koko | 990 MB | 6.2 GB | 0 (API) |
**Käytännössä:** `kpn load 2` lataa 3B-mallin. Hitaampi mutta huomattavasti
parempi koodinlaatu. Suositus monimutkaisiin projekteihin.
### 2. Paremmat promptit (ilmaista)
**Huono:** `"tee fibonacci"`
- Malli ei tiedä kieltä, formaattia tai kontekstia
**Hyvä:** `"Write a fibonacci function in Rust that returns Vec<u64>"`
- Kieli, palautustyyppi ja rakenne määritelty
**Promptin säännöt:**
- Englanniksi (tehokkaampi tokenisointi, parempi ymmärrys)
- Konkreettinen (mainitse kieli, kirjastot, palautustyyppi)
- Lyhyt (jokainen sana kuluttaa tokenin konteksti-ikkunasta)
- Positiivinen ("Write X" ei "Don't write Y")
### 3. Kontekstin hallinta (pipeline-taso)
**Ongelma:** 0.5B-malli "unohtaa" promptin alun kun konteksti kasvaa.
**Ratkaisu:** Pienet, kohdennetut promptit:
- Yksi tiedosto kerrallaan (ei "kirjoita koko projekti")
- Vain relevantit aiemmat tiedostot kontekstina
- Max 4 tiedostoa per projekti
### 4. Iterointi (review-luuppi)
Yksi generointikierros tuottaa harvoin virheetöntä koodia.
Pipeline-arkkitehtuuri mahdollistaa:
1. **Generointi** — ensimmäinen versio
2. **Review** — testaaja löytää ongelmat
3. **Korjaus** — koodari saa palautteen ja korjaa
4. **Uusi review** — tarkistetaan korjaukset
Nykyinen järjestelmä tekee max 1 korjauskierroksen. Useampi
iteraatio parantaisi laatua mutta kasvattaisi laskenta-aikaa.
### 5. Erikoistetut system promptit
Oletuspromptit ovat yleiskäyttöisiä. Projektikohtaiset promptit
parantavat laatua merkittävästi:
```
Oletus: "Olet kokenut ohjelmistokehittäjä."
Parempi: "You are a Python backend developer specializing in FastAPI.
Always use Pydantic models for request/response schemas.
Always use dependency injection for database sessions.
Follow the repository pattern."
```
Agenttikohtaiset promptit voi muokata suoraan UI:ssa.
### 6. Few-shot esimerkit
Malli oppii parhaiten esimerkeistä. Sen sijaan, että sanot "kirjoita
FastAPI endpoint", näytä miltä haluat tuloksen näyttävän:
```
Write a GET endpoint like this example:
@app.get("/items")
def list_items():
db = SessionLocal()
return db.query(Item).all()
Now write a similar endpoint for /users.
```
0.5B-malli jäljittelee rakennetta tehokkaasti — se on parempi kopioimaan
kuin keksimään. Nykyinen pyproject.toml-esimerkki promptissa on tätä tekniikkaa.
### 7. Temperature-säätö tehtävän mukaan
Nykyinen temperature 0.7 on kompromissi. Eri tehtävät hyötyisivät eri arvoista:
| Tehtävä | Paras temperature | Miksi |
|---------|-------------------|-------|
| Tarkka koodi (CRUD, boilerplate) | 0.2-0.4 | Determinismi tärkeää |
| Luova koodi (algoritmit, arkkitehtuuri) | 0.6-0.8 | Vaihtelu löytää ratkaisuja |
| Vapaa teksti (kommentit, dokumentaatio) | 0.8-1.0 | Luonnollisempi kieli |
Järjestelmä voisi valita temperaturen automaattisesti tehtävätyypin perusteella.
### 8. Ensemble — sama prompti usealle mallille
Lähetetään sama tehtävä kahdelle solmulle ja valitaan parempi vastaus.
Nykyinen Proof of Compute -arkkitehtuuri tukee tätä periaatteessa:
hub voisi reitittää saman task_id:n kahdelle solmulle ja verrata tuloksia.
Käytännössä tämä kaksinkertaistaa laskenta-ajan mutta parantaa laatua
merkittävästi — virheellinen vastaus harvoin on sama kahdella ajolla
koska sampling on stokastinen.
### 9. Post-processing (nykyinen)
Mallin raakavastaus siivotaan:
1. Kielitunniste poistetaan (`python`, `rust`, ...)
2. Sulkeva ` ``` ` poistetaan
3. Johdantolauseet poistetaan ("Sure!", "Here is...")
4. Selityskommentit poistetaan ("# This is a simple...")
5. Stop-sekvenssit katkaisevat generoinnin
Tämä ei paranna mallin ajattelua mutta poistaa turhan roskan.
### 10. Mallin hienosäätö (fine-tuning)
Qwen2.5-Coder on yleiskäyttöinen koodimalli. Jos sitä hienosäätäisi
omalla koodiaineistolla (esim. yrityksen koodikanta, tietty framework),
se tuottaisi huomattavasti parempaa koodia juuri siihen kontekstiin.
LoRA-hienosäätö 0.5B-mallille vaatii ~4 GB GPU-muistia ja muutaman
tunnin harjoittelua. Tulos on erikoistunut malli joka osaa tuottaa
esimerkiksi juuri FastAPI + SQLAlchemy -koodia luotettavasti.
---
## Välimuistiarkkitehtuuri — miksi toinen lataus on nopea
```
Ensimmäinen lataus (hidas):
Verkko (HuggingFace CDN) → IndexedDB → RAM → Mallin rakennus
~990 MB lataus, ~30-60s
Toinen lataus samalla sivulatauksella (nopea):
RAM-cache → Mallia ei rakenneta uusiksi, vain KV-cache nollataan
~0ms
Refresh jälkeen (keskitaso):
IndexedDB → RAM → Mallin rakennus
~0 MB lataus, ~2-5s rakennus
Uusi selain/laite (hidas):
Verkko → IndexedDB → RAM → Mallin rakennus
Kuten ensimmäinen lataus
```
**KV-cache:** Mallin sisäinen muisti joka tallentaa aiempien tokenien
laskenta tulokset. Nollataan (`clear_kv_cache()`) jokaisen promptin
välillä jotta edellinen vastaus ei vuoda seuraavaan.
---
## Lukuja käytännöstä
**Yksittäinen funktio** (esim. fibonacci):
- Input: ~80 tokenia
- Output: ~50-100 tokenia
- Aika: ~10-20s (0.5B, selain)
- Laatu: Yleensä toimiva, joskus loogisia virheitä
**3 tiedoston projekti** (esim. FastAPI CRUD):
- Manageri: ~30 tok out
- Koodari (3x): ~100-150 tok out per tiedosto
- Testeri: ~50 tok out
- Korjaukset: ~100 tok out (jos tarpeen)
- **Yhteensä: ~500-700 tokenia, ~3-5 min**
- Laatu: Rakenne oikein, yksittäisiä bugeja
**Token-kustannus vs. pilvipalvelu:**
- Tässä järjestelmässä: 0 euroa (laskenta omalla koneella)
- GPT-4 API: ~700 tokenia x $0.03/1K = ~$0.02 per projekti
- Claude API: ~700 tokenia x $0.015/1K = ~$0.01 per projekti
Selaimessa ajettava malli on ilmainen mutta huomattavasti hitaampi
ja heikompilaatuinen kuin pilvi-API. Sopii oppimiseen, prototypointiin
ja tilanteisiin joissa data ei saa lähteä omalta koneelta.

View File

@@ -0,0 +1,34 @@
# Kipinä Agentic Playground - Animaatioiden käyttöönotto
Koska Kipinä-verkon agenttien avatarit tällä erää ovat staattisia PNG-kuvatiedostoja, käyttöliittymä hyödyntää CSS-pohjaista pomppimisilmiötä (sekä pulppuavaa 💬 puhekuplaa) "puhumisen" merkkinä. Olemme kuitenkin koodanneet taustalle piilotetun tuen aivioiduille videoloopeille myöhempää käyttöä varten!
Näin saat UI:n tukemaan oikeasti animoituja kasvoja/videoita.
## 1. Luo Animoidut GIF-tiedostot
Valitse mikä tahansa ulkoinen AI-työkalu (kuten HeyGen, Pika v1.0, tai Midjourney+Runway yhdistelmä) ja muunna avatar-kuvat (esim. `kettu_notext.png`) 3-5 sekunnin kestäviksi GIF-loopeiksi. Hahmon leuka tulisi pyöriä tai naama vääntyillä puhuessaan.
## 2. Nimeä Tiedostot Oikein ja Lisää Ne Kansioon
Siirrä uudet GIF-animaatiot samaan kansioon alkuperäisten kuvien kanssa. Muuta niiden nimi siten, että se päättyy tunnisteeseen `_puhuva.gif`.
Esimerkkejä:
- Koodari `kipina_notext.png``kipina_notext_puhuva.gif`
- Manageri `karhunpentu.png``karhunpentu_puhuva.gif`
- Asiakas `kettu_notext.png``kettu_notext_puhuva.gif`
## 3. Aktivoi Koodi
Käännä Kipinä Playground -ohjaimen JavaScript-koodista piilotettu ominaisuus päälle.
Etsi tiedostosta `../index.html` (noin riviltä 1084, `updatePromptEditor`-funktiosta):
```javascript
// Piilotettu ominaisuus: Puhuvien videoiden / gif-animaatioiden kytkentä
window.USE_ANIMATED_GIFS = false;
```
Muuta tuo `false` arvoon `true`:
```javascript
window.USE_ANIMATED_GIFS = true;
```
**Mitä logiikka tekee?**
Aina kun valitset agentin kaaviosta, koodi korvaa aktiivisen kuvakkeen lopussa olevan `.png` -päätteen sanalla `_puhuva.gif` lennosta! Jos poistut agentin valinnasta tai valitset jonkun toisen, koodi vaihtaa kuvan välittömästi takaisin staattiseen `.png`-versioon ja sulkee ilmentymän suun.
Näin saat kaikkien asiantuntijoiden face-track looppeja hallittua yhdellä kädenkäänteellä.

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 KiB

View File

@@ -0,0 +1,63 @@
/* tslint:disable */
/* eslint-disable */
export function set_auto_tasks(enabled: boolean): void;
export function set_gpu_load(load: number): void;
export function start_agent_node(hub_url: string, has_webgpu: boolean, device_info_json: string, task_id: number): Promise<void>;
/**
* JS-exportti: tokenisoi tekstin ja palauttaa JSON-merkkijonon
* Tokenizer ladataan IndexedDB:stä (täytyy olla ladattu aiemmin)
*/
export function tokenize_js(text: string): Promise<string>;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly set_auto_tasks: (a: number) => void;
readonly set_gpu_load: (a: number) => void;
readonly start_agent_node: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
readonly tokenize_js: (a: number, b: number) => any;
readonly wasm_bindgen__convert__closures_____invoke__h6ec112f0342d232e: (a: number, b: number, c: any) => [number, number];
readonly wasm_bindgen__convert__closures_____invoke__h737e63bacb96714d: (a: number, b: number, c: any, d: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h9cacd8a9a6ca46c2: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4_3: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a_5: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h698aa4c8c2e7db1b: (a: number, b: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_externrefs: WebAssembly.Table;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_destroy_closure: (a: number, b: number) => void;
readonly __externref_table_dealloc: (a: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,24 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const set_auto_tasks: (a: number) => void;
export const set_gpu_load: (a: number) => void;
export const start_agent_node: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
export const tokenize_js: (a: number, b: number) => any;
export const wasm_bindgen__convert__closures_____invoke__h6ec112f0342d232e: (a: number, b: number, c: any) => [number, number];
export const wasm_bindgen__convert__closures_____invoke__h737e63bacb96714d: (a: number, b: number, c: any, d: any) => void;
export const wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h9cacd8a9a6ca46c2: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__ha390eb51fa5285b4_3: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h0afc19def95e993a_5: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h698aa4c8c2e7db1b: (a: number, b: number) => void;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_exn_store: (a: number) => void;
export const __externref_table_alloc: () => number;
export const __wbindgen_externrefs: WebAssembly.Table;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __wbindgen_destroy_closure: (a: number, b: number) => void;
export const __externref_table_dealloc: (a: number) => void;
export const __wbindgen_start: () => void;

View File

@@ -0,0 +1,15 @@
{
"name": "node",
"type": "module",
"version": "0.1.0",
"files": [
"node_bg.wasm",
"node.js",
"node.d.ts"
],
"main": "node.js",
"types": "node.d.ts",
"sideEffects": [
"./snippets/*"
]
}

View File

@@ -0,0 +1,27 @@
{
"name": "FastAPI CRUD",
"description": "REST API with SQLite database",
"files": {
"models.py": {
"description": "SQLAlchemy models, engine, and session",
"example": "from sqlalchemy import create_engine, Column, Integer, String\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker\n\nDATABASE_URL = \"sqlite:///./app.db\"\nengine = create_engine(DATABASE_URL, connect_args={\"check_same_thread\": False})\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase = declarative_base()\n\nclass Item(Base):\n __tablename__ = \"items\"\n id = Column(Integer, primary_key=True, index=True)\n name = Column(String(100), nullable=False)\n description = Column(String(500))",
"instructions": "Define the SQLAlchemy model based on the project description. Always include:\n- engine with check_same_thread=False for SQLite\n- SessionLocal with autocommit=False\n- Base = declarative_base()\n- Model class with __tablename__, primary key, and fields"
},
"schemas.py": {
"description": "Pydantic request/response schemas",
"example": "from pydantic import BaseModel\n\nclass ItemCreate(BaseModel):\n name: str\n description: str | None = None\n\nclass ItemResponse(ItemCreate):\n id: int\n\n class Config:\n from_attributes = True",
"instructions": "Create Pydantic schemas that match the SQLAlchemy model:\n- Create schema: fields without id (user provides these)\n- Response schema: inherits from Create, adds id\n- Add class Config with from_attributes = True (required for SQLAlchemy ORM)"
},
"main.py": {
"description": "FastAPI app with CRUD endpoints",
"example": "from fastapi import FastAPI, Depends, HTTPException\nfrom sqlalchemy.orm import Session\nfrom models import Base, engine, SessionLocal, Item\nfrom schemas import ItemCreate, ItemResponse\n\nBase.metadata.create_all(bind=engine)\napp = FastAPI()\n\ndef get_db():\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n@app.post(\"/items/\", response_model=ItemResponse, status_code=201)\ndef create_item(item: ItemCreate, db: Session = Depends(get_db)):\n db_item = Item(**item.model_dump())\n db.add(db_item)\n db.commit()\n db.refresh(db_item)\n return db_item\n\n@app.get(\"/items/\", response_model=list[ItemResponse])\ndef list_items(db: Session = Depends(get_db)):\n return db.query(Item).all()\n\n@app.get(\"/items/{item_id}\", response_model=ItemResponse)\ndef get_item(item_id: int, db: Session = Depends(get_db)):\n item = db.query(Item).filter(Item.id == item_id).first()\n if not item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n return item\n\n@app.put(\"/items/{item_id}\", response_model=ItemResponse)\ndef update_item(item_id: int, item: ItemCreate, db: Session = Depends(get_db)):\n db_item = db.query(Item).filter(Item.id == item_id).first()\n if not db_item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n for key, value in item.model_dump().items():\n setattr(db_item, key, value)\n db.commit()\n db.refresh(db_item)\n return db_item\n\n@app.delete(\"/items/{item_id}\", status_code=204)\ndef delete_item(item_id: int, db: Session = Depends(get_db)):\n db_item = db.query(Item).filter(Item.id == item_id).first()\n if not db_item:\n raise HTTPException(status_code=404, detail=\"Not found\")\n db.delete(db_item)\n db.commit()",
"instructions": "Create the FastAPI app with all CRUD endpoints:\n- Import from models.py and schemas.py (use exact class names)\n- create_all(bind=engine) at module level\n- get_db dependency with yield pattern\n- POST (201), GET list, GET by id, PUT, DELETE (204)\n- Use response_model for type safety\n- Use model_dump() not dict() (Pydantic v2)"
},
"pyproject.toml": {
"description": "Project dependencies",
"example": "[project]\nname = \"myapp\"\nversion = \"0.1.0\"\nrequires-python = \">=3.11\"\ndependencies = [\n \"fastapi\",\n \"uvicorn[standard]\",\n \"sqlalchemy\",\n]\n\n[project.scripts]\ndev = \"uvicorn main:app --reload\"",
"instructions": "Use [project] format (PEP 621, compatible with uv). List dependencies under [project.dependencies]. Add [project.scripts] with dev command. Never use requirements.txt or Poetry format. Run with: uv run uvicorn main:app --reload"
}
},
"order": ["models.py", "schemas.py", "main.py", "pyproject.toml"]
}

View File

@@ -0,0 +1,79 @@
<!-- Agenttigalleria + konfigurointipaneeli -->
<div style="display:flex;gap:16px;padding:10px 0;align-items:flex-start">
<!-- Agenttilista (drag & drop) -->
<div id="agent-bar" style="display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap">
<!-- Renderöidään JS:stä -->
</div>
<!-- + Lisää agentti -->
<div id="add-agent-btn" class="agent-avatar" onclick="addCustomAgent()" title="Lisää oma agentti" style="opacity:0.4">
<div style="width:48px;height:48px;border-radius:50%;border:2px dashed var(--border);display:flex;align-items:center;justify-content:center;font-size:24px;color:var(--border)">+</div>
<span style="font-size:10px;color:#8b949e;text-align:center;display:block">Lisää</span>
</div>
</div>
<!-- Agentin konfigurointipaneeli (avautuu klikkaamalla avataria) -->
<div id="agent-config" style="display:none;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:16px;margin-bottom:10px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:10px">
<img id="config-avatar" src="" style="width:40px;height:40px;border-radius:50%">
<div>
<input id="config-name" style="background:transparent;border:none;color:var(--text);font-size:16px;font-weight:600;outline:none;width:200px" placeholder="Agentin nimi">
<div id="config-role" style="font-size:11px;color:#8b949e"></div>
</div>
</div>
<div style="display:flex;gap:6px">
<button class="btn btn-red" onclick="deleteAgent()" title="Poista agentti">Poista</button>
<button class="btn btn-muted" onclick="closeAgentConfig()">Sulje</button>
</div>
</div>
<!-- Malli -->
<div style="margin-bottom:10px">
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Kielimalli</label>
<select id="config-model" style="background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:6px 10px;font-size:13px;width:100%">
<option value="qwen-coder">Qwen2.5-Coder:0.5B (selain)</option>
<option value="qwen-coder-3b">Qwen2.5-Coder:3B (Ollama)</option>
<option value="qwen2.5-coder:7b">Qwen2.5-Coder:7B (Ollama)</option>
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (Ollama)</option>
</select>
</div>
<!-- System prompt -->
<div style="margin-bottom:10px" title="Agentin perusohje joka lähetetään kielimallille jokaisessa pyynnössä.&#10;&#10;Hyvän promptin rakenne:&#10;1. Rooli: 'You are an expert...'&#10;2. Säännöt: RULES/CRITICAL RULES listana&#10;3. Esimerkit: EXAMPLE OUTPUT&#10;4. Kiellot: NEVER-lista&#10;&#10;Vinkki: käytä englantia — malli ymmärtää sen paremmin ja se kuluttaa vähemmän tokeneita.">
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px;cursor:help">System prompt 💡</label>
<textarea id="config-prompt" style="width:100%;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:8px;font-size:13px;font-family:'Courier New',monospace;resize:vertical;overflow:hidden;min-height:60px" placeholder="Kuvaa agentin rooli ja käyttäytyminen..."></textarea>
</div>
<!-- Sampling-parametrit -->
<div style="margin-bottom:10px">
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:8px">Sampling-parametrit</label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div title="Kontrolloi 'luovuutta'. Matala arvo (0.2-0.4) tuottaa ennustettavaa, toistettavaa koodia — hyvä testaajille ja reviewereille. Keskiarvo (0.6-0.8) on paras koodin generointiin. Korkea arvo (1.0+) lisää vaihtelua mutta myös virheitä.&#10;&#10;Suositus:&#10;• Manageri: 0.5 (tarkat tiedostolistat)&#10;• Koodari: 0.7 (toimiva koodi + vaihtelu)&#10;• Testaaja: 0.3 (deterministinen arviointi)">
<label style="font-size:11px;color:#8b949e;cursor:help">Temperature 💡 <span id="config-temp-val" style="color:var(--accent);float:right">0.7</span></label>
<input type="range" id="config-temperature" min="0" max="1.5" step="0.1" value="0.7" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">0=tarkka · 0.7=oletus · 1.5=luova</div>
</div>
<div title="Vastauksen maksimipituus tokeneina (~1 token ≈ 4 merkkiä).&#10;&#10;Suositus:&#10;• Manageri: 256-512 (lyhyet tiedostolistat)&#10;• Koodari: 1024-2048 (täydet tiedostot, CRUD-endpointit)&#10;• Testaaja: 256-512 (lyhyet arvioinnit)&#10;&#10;Jos koodi katkeaa kesken, nosta tätä. Jos malli tuottaa turhaa toistoa, laske.">
<label style="font-size:11px;color:#8b949e;cursor:help">Max tokens 💡 <span id="config-maxtok-val" style="color:var(--accent);float:right">1024</span></label>
<input type="range" id="config-maxtokens" min="64" max="4096" step="64" value="1024" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">Vastauksen maksimipituus</div>
</div>
<div title="Montako todennäköisintä tokenia huomioidaan valinnassa. Pieni arvo (1-10) tekee vastauksesta deterministisen. Suuri arvo (50-100) sallii harvinaisempia sanoja.&#10;&#10;Suositus:&#10;• Boilerplate-koodi: 20-30 (tutut patternit)&#10;• Yleiskoodi: 40 (hyvä oletus)&#10;• Luova teksti: 60-80&#10;&#10;Yleensä ei tarvitse muuttaa oletuksesta.">
<label style="font-size:11px;color:#8b949e;cursor:help">Top-K 💡 <span id="config-topk-val" style="color:var(--accent);float:right">40</span></label>
<input type="range" id="config-topk" min="1" max="100" step="1" value="40" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">1=greedy · 40=oletus · 100=laaja</div>
</div>
<div title="Vähentää jo tuotettujen sanojen todennäköisyyttä. Estää mallia toistamasta samaa lausetta. Liian korkea arvo (>1.5) voi rikkoa koodin koska samat avainsanat (return, if, def) ovat tarpeellisia.&#10;&#10;Suositus:&#10;• Koodi: 1.1-1.2 (lievä, sallii toiston)&#10;• Teksti: 1.15-1.3 (vahvempi)&#10;• Review: 1.0-1.1 (ei rangaistusta, lyhyet vastaukset)">
<label style="font-size:11px;color:#8b949e;cursor:help">Repetition penalty 💡 <span id="config-rep-val" style="color:var(--accent);float:right">1.15</span></label>
<input type="range" id="config-repeat" min="1.0" max="2.0" step="0.05" value="1.15" style="width:100%;accent-color:var(--accent)">
<div style="font-size:10px;color:#30363d">1.0=ei · 1.15=oletus · 2.0=vahva</div>
</div>
</div>
</div>
<!-- Pipeline-järjestys -->
<div>
<label style="font-size:12px;color:#8b949e;display:block;margin-bottom:4px">Pipeline-järjestys <span style="color:var(--border)">(vedä järjestääksesi)</span></label>
<div id="config-pipeline" style="display:flex;gap:4px;flex-wrap:wrap"></div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<!-- Monaco Editor paneeli -->
<div id="panel-editor" class="panel">
<div style="display:flex;height:calc(100vh - 200px);gap:0;border:1px solid var(--border);border-radius:6px;overflow:hidden">
<div id="editor-filetree" style="width:200px;min-width:150px;background:var(--bg);border-right:1px solid var(--border);overflow-y:auto;font-family:'Courier New',monospace;font-size:13px">
<div style="padding:10px 12px;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;border-bottom:1px solid var(--border)">Tiedostot</div>
<div id="editor-file-list" style="padding:4px 0">
<div style="padding:8px 16px;color:#8b949e;font-size:12px">Generoi projekti:<br><code style="color:var(--accent)">kpn project "..."</code></div>
</div>
</div>
<div style="flex:1;display:flex;flex-direction:column">
<div id="editor-tabs" style="display:flex;background:var(--bg);border-bottom:1px solid var(--border);min-height:35px;align-items:flex-end;padding:0 8px;gap:2px;overflow-x:auto"></div>
<div id="monaco-container" style="flex:1"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,6 @@
<!-- Opas-paneeli: ladataan GUIDE.md fetchillä -->
<div id="panel-guide" class="panel">
<div id="guide-content" style="max-width:800px;margin:0 auto;padding:20px;line-height:1.7;font-size:15px">
<p style="color:#8b949e">Ladataan opasta...</p>
</div>
</div>

View File

@@ -0,0 +1,67 @@
<!-- Asetukset-paneeli: kaikki LLM-parametrit muokattavissa -->
<div id="panel-settings" class="panel">
<div style="max-width:800px;margin:0 auto;padding:20px">
<h2 style="color:#e6edf3;margin-bottom:16px">Asetukset</h2>
<p style="color:#8b949e;margin-bottom:20px;font-size:14px">Kaikki kielimallin toimintaan vaikuttavat parametrit. Muutokset tallentuvat automaattisesti.</p>
<!-- System prompt -->
<div class="settings-section">
<h3 class="settings-title">System Prompt</h3>
<p class="settings-desc">Kielimallin perusohje joka lähetetään jokaisessa pyynnössä. Määrittää mallin käyttäytymisen.</p>
<textarea id="set-system-prompt" class="settings-textarea" rows="4"></textarea>
</div>
<!-- Sampling -->
<div class="settings-section">
<h3 class="settings-title">Sampling-parametrit</h3>
<p class="settings-desc">Kontrolloi miten malli valitsee seuraavan tokenin. <a href="#guide" onclick="switchTab('guide')" style="color:var(--accent)">Lue lisää oppaasta.</a></p>
<div class="settings-grid">
<div>
<label class="settings-label">Temperature <span id="set-temp-val" class="settings-val">0.7</span></label>
<input type="range" id="set-temperature" min="0" max="1.5" step="0.1" value="0.7" class="settings-slider">
<div class="settings-hint">0 = deterministic, 0.7 = balanced, 1.5 = creative</div>
</div>
<div>
<label class="settings-label">Top-K <span id="set-topk-val" class="settings-val">40</span></label>
<input type="range" id="set-topk" min="1" max="100" step="1" value="40" class="settings-slider">
<div class="settings-hint">Montako tokenia huomioidaan. 1 = greedy, 40 = oletus</div>
</div>
<div>
<label class="settings-label">Repetition Penalty <span id="set-rep-val" class="settings-val">1.15</span></label>
<input type="range" id="set-repeat" min="1.0" max="2.0" step="0.05" value="1.15" class="settings-slider">
<div class="settings-hint">Estää toistoa. 1.0 = ei rangaistusta, 1.15 = oletus</div>
</div>
<div>
<label class="settings-label">Max Tokens <span id="set-maxtok-val" class="settings-val">1024</span></label>
<input type="range" id="set-maxtokens" min="64" max="4096" step="64" value="1024" class="settings-slider">
<div class="settings-hint">Vastauksen maksimipituus tokeneina</div>
</div>
</div>
</div>
<!-- Stop-sekvenssit -->
<div class="settings-section">
<h3 class="settings-title">Stop-sekvenssit</h3>
<p class="settings-desc">Generointi katkeaa kun malli tuottaa jonkin näistä. Yksi per rivi.</p>
<textarea id="set-stop-sequences" class="settings-textarea" rows="4"></textarea>
</div>
<!-- Malli -->
<div class="settings-section">
<h3 class="settings-title">Malli (Ollama)</h3>
<p class="settings-desc">Natiivisolmun käyttämä kielimalli. Muutos vaatii native-noden uudelleenkäynnistyksen.</p>
<select id="set-model" class="settings-select">
<option value="qwen2.5-coder:1.5b">Qwen2.5-Coder:1.5B (~80 tok/s, ~1GB)</option>
<option value="qwen2.5-coder:3b">Qwen2.5-Coder:3B (~50 tok/s, ~2GB)</option>
<option value="qwen2.5-coder:7b-instruct-q4_K_M">Qwen2.5-Coder:7B Q4 (~30 tok/s, ~4GB)</option>
<option value="qwen2.5-coder:7b">Qwen2.5-Coder:7B (~20 tok/s, ~7GB)</option>
</select>
</div>
<!-- Reset -->
<div style="margin-top:24px;padding-top:16px;border-top:1px solid var(--border)">
<button class="btn btn-red" onclick="resetSettings()" style="padding:6px 16px">Palauta oletukset</button>
<span style="color:#8b949e;font-size:12px;margin-left:8px">Palauttaa kaikki parametrit oletusarvoihin</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<!-- Hub-yhteys + laskentasolmun tila -->
<div class="status-bar">
<span class="status-group" title="Hub-yhteyden tila">
<span id="hub-dot" class="status-dot" style="background:#d29922"></span>
<span style="color:#8b949e">Hub:</span>
<span id="hub-label" style="color:#d29922">Yhdistetään...</span>
</span>
<span class="status-separator">│</span>
<span class="status-group">
<span id="compute-dot" class="status-dot" style="background:#30363d"></span>
<span style="color:#8b949e">Laskenta:</span>
<span id="compute-label" style="color:#8b949e">—</span>
<button id="compute-btn" class="btn btn-accent" title="Käynnistä kielimalli">Alusta</button>
</span>
</div>

View File

@@ -0,0 +1,10 @@
<!-- Pipeline-palkki + Terminaali + Input -->
<div id="pipeline-bar" class="pipeline-bar"></div>
<div id="terminal" class="terminal"></div>
<div class="terminal-input-row">
<span class="terminal-prompt">$</span>
<input id="term-input" class="terminal-input" type="text"
placeholder='kpn run coder "hello world in python"'
spellcheck="false" autocomplete="off">
<div id="term-dropdown" class="terminal-dropdown"></div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
:root {
--bg: #0d1117;
--panel: #161b22;
--text: #c9d1d9;
--accent: #58a6ff;
--green: #3fb950;
--yellow: #d29922;
--red: #f85149;
--purple: #a371f7;
--border: #30363d;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
/* Tabs */
.tabs { display: flex; gap: 4px; margin-bottom: 16px; }
.tab {
padding: 8px 16px; border-radius: 6px 6px 0 0; cursor: pointer;
border: 1px solid var(--border); border-bottom: none;
background: var(--bg); color: #8b949e; font-size: 14px;
}
.tab.active { background: var(--panel); color: var(--accent); border-color: var(--border); }
/* Panels */
.panel { display: none; }
.panel.active { display: block; }
/* Status bar */
.status-bar {
display: flex; align-items: center; gap: 12px;
padding: 8px 14px; background: var(--bg);
border: 1px solid var(--border); border-radius: 6px 6px 0 0;
font-family: 'Courier New', monospace; font-size: 13px;
}
.status-dot {
width: 8px; height: 8px; border-radius: 50%; display: inline-block;
}
.status-group { display: flex; align-items: center; gap: 6px; }
.status-separator { color: var(--border); }
/* Terminal */
.terminal {
background: #010409; border: 1px solid var(--border); border-top: none;
font-family: 'Courier New', monospace; font-size: 14px;
min-height: 300px; max-height: 60vh; overflow-y: auto;
padding: 8px 12px;
}
.terminal-line { padding: 1px 0; white-space: pre-wrap; word-break: break-word; }
.terminal-prompt { color: var(--yellow); margin-right: 8px; }
.terminal-input-row {
display: flex; align-items: center; position: relative;
background: #010409; border: 1px solid var(--border); border-top: none;
border-radius: 0 0 6px 6px; padding: 8px 12px;
font-family: 'Courier New', monospace; font-size: 14px;
}
.terminal-input {
flex: 1; background: transparent; border: none; outline: none;
color: var(--green); font-family: inherit; font-size: inherit;
}
.terminal-dropdown {
display: none; position: absolute; bottom: 100%; left: 30px;
background: var(--panel); border: 1px solid var(--border);
border-radius: 6px; max-height: 200px; overflow-y: auto;
font-size: 13px; min-width: 200px; z-index: 100;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.dd-item {
padding: 6px 12px; cursor: pointer; color: var(--text);
white-space: nowrap; border-bottom: 1px solid #21262d;
}
.dd-item:hover, .dd-item.active { background: var(--border); color: var(--accent); }
/* Pipeline progress */
.pipeline-bar {
display: none; padding: 8px 14px; background: var(--bg);
border: 1px solid var(--border); border-top: none;
font-family: 'Courier New', monospace; font-size: 12px;
overflow-x: auto; white-space: nowrap;
}
/* Project card */
.project-card {
margin: 8px 0; border: 1px solid var(--border);
border-radius: 6px; background: var(--panel); overflow: hidden;
}
.project-header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 12px; background: var(--bg); border-bottom: 1px solid var(--border);
}
.project-tabs { display: flex; gap: 2px; padding: 6px 8px 0; background: var(--bg); }
.project-tab {
padding: 4px 10px; cursor: pointer; border-radius: 4px 4px 0 0;
font-size: 12px; color: #8b949e;
}
.project-tab.active { background: var(--panel); color: var(--accent); border: 1px solid var(--border); border-bottom: none; }
/* Buttons */
.btn {
padding: 2px 10px; border-radius: 4px;
border: 1px solid var(--border); background: var(--panel);
font-size: 12px; font-family: inherit; cursor: pointer;
}
.btn-accent { color: var(--accent); }
.btn-green { color: var(--green); border-color: var(--green); }
.btn-red { color: var(--red); border-color: var(--red); }
.btn-muted { color: #8b949e; background: none; }
/* Code display */
.code-block {
font-family: 'Courier New', monospace; background: #010409;
border: 1px solid var(--border); border-radius: 6px;
padding: 14px; font-size: 13px; line-height: 1.6;
white-space: pre-wrap; overflow-x: auto; max-height: 400px; overflow-y: auto;
}
.code-block .hljs { background: transparent; padding: 0; }
/* Agent avatars */
.agent-avatar {
background: linear-gradient(145deg, rgba(33,38,45,0.4) 0%, rgba(13,17,23,0.8) 100%);
backdrop-filter: blur(12px);
border: 1px solid rgba(240,246,252,0.1);
border-radius: 12px;
padding: 6px 6px 4px;
text-align: center;
width: 72px;
opacity: 0.8;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.agent-avatar:hover {
opacity: 0.85;
transform: translateY(-2px) scale(1.02);
border-color: rgba(240,246,252,0.3);
box-shadow: 0 8px 14px rgba(0,0,0,0.4);
}
.agent-avatar img {
width: 50px; height: 50px; border-radius: 12px;
margin-bottom: 4px; border: 2px solid rgba(240,246,252,0.1);
transition: all 0.4s ease; object-fit: cover;
}
.agent-avatar .avatar-name {
font-size: 10px; color: #8b949e; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
}
.agent-avatar.active {
opacity: 1;
transform: translateY(-8px) scale(1.05);
border-color: var(--accent);
background: linear-gradient(145deg, rgba(88,166,255,0.15) 0%, rgba(13,17,23,0.9) 100%);
box-shadow: 0 16px 24px rgba(0,0,0,0.5), 0 0 20px rgba(88,166,255,0.3);
z-index: 2;
}
.agent-avatar.active img {
border-color: var(--accent);
box-shadow: 0 0 25px rgba(88,166,255,0.8);
}
/* Settings */
.settings-section {
margin-bottom: 24px; padding: 16px; background: var(--panel);
border: 1px solid var(--border); border-radius: 6px;
}
.settings-title { color: #e6edf3; font-size: 15px; margin-bottom: 4px; }
.settings-desc { color: #8b949e; font-size: 13px; margin-bottom: 12px; }
.settings-label { color: var(--text); font-size: 13px; display: block; margin-bottom: 4px; }
.settings-val { color: var(--accent); font-weight: 600; float: right; }
.settings-hint { color: #8b949e; font-size: 11px; margin-top: 2px; }
.settings-textarea {
width: 100%; background: var(--bg); color: var(--text);
border: 1px solid var(--border); border-radius: 4px;
padding: 8px; font-size: 13px; font-family: 'Courier New', monospace;
resize: vertical;
}
.settings-select {
width: 100%; background: var(--bg); color: var(--text);
border: 1px solid var(--border); border-radius: 4px;
padding: 8px; font-size: 13px;
}
.settings-slider {
width: 100%; accent-color: var(--accent);
}
.settings-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
}
/* Animations */
@keyframes blink { 0%,100% { opacity:1 } 50% { opacity:0 } }
@keyframes spin { to { transform: rotate(360deg) } }

View File

@@ -0,0 +1 @@
{ "extends": "astro/tsconfigs/strict" }

View File

@@ -1,6 +1,6 @@
[package]
name = "hub"
version = "0.2.4"
version = "0.3.0"
edition = "2024"
[dependencies]

View File

@@ -26,6 +26,29 @@ impl NodeDb {
INSERT INTO _schema_version VALUES (2);
");
}
if version < 3 {
let _ = conn.execute_batch("
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
avatar TEXT NOT NULL DEFAULT '/avatars/kipina_notext.png',
role TEXT NOT NULL DEFAULT 'coder',
model TEXT NOT NULL DEFAULT 'qwen2.5-coder:7b',
color TEXT NOT NULL DEFAULT '#3fb950',
docs TEXT,
prompt TEXT NOT NULL DEFAULT '',
temperature REAL DEFAULT 0.7,
top_k INTEGER DEFAULT 40,
max_tokens INTEGER DEFAULT 512,
repetition_penalty REAL DEFAULT 1.15,
is_default BOOLEAN DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
DELETE FROM _schema_version;
INSERT INTO _schema_version VALUES (3);
");
}
conn.execute_batch("
CREATE TABLE IF NOT EXISTS node_sessions (
@@ -279,6 +302,82 @@ impl NodeDb {
})
}
// ── Agents CRUD ──
pub fn upsert_agent(&self, agent: &serde_json::Value) -> Result<(), String> {
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
let now = chrono::Utc::now().to_rfc3339();
let id = agent.get("id").and_then(|v| v.as_str()).ok_or("id puuttuu")?;
let name = agent.get("name").and_then(|v| v.as_str()).ok_or("name puuttuu")?;
conn.execute(
"INSERT INTO agents (id, name, avatar, role, model, color, docs, prompt,
temperature, top_k, max_tokens, repetition_penalty, is_default, created_at, updated_at)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?14)
ON CONFLICT(id) DO UPDATE SET
name=?2, avatar=?3, role=?4, model=?5, color=?6, docs=?7, prompt=?8,
temperature=?9, top_k=?10, max_tokens=?11, repetition_penalty=?12, updated_at=?14",
params![
id, name,
agent.get("avatar").and_then(|v| v.as_str()).unwrap_or("/avatars/kipina_notext.png"),
agent.get("role").and_then(|v| v.as_str()).unwrap_or("coder"),
agent.get("model").and_then(|v| v.as_str()).unwrap_or("qwen2.5-coder:7b"),
agent.get("color").and_then(|v| v.as_str()).unwrap_or("#3fb950"),
agent.get("docs").and_then(|v| v.as_str()),
agent.get("prompt").and_then(|v| v.as_str()).unwrap_or(""),
agent.get("temperature").and_then(|v| v.as_f64()).unwrap_or(0.7),
agent.get("top_k").and_then(|v| v.as_u64()).unwrap_or(40) as i64,
agent.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as i64,
agent.get("repetition_penalty").and_then(|v| v.as_f64()).unwrap_or(1.15),
agent.get("is_default").and_then(|v| v.as_bool()).unwrap_or(false),
now,
],
).map_err(|e| format!("Agent upsert: {}", e))?;
Ok(())
}
pub fn get_agents(&self) -> Vec<serde_json::Value> {
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
let mut stmt = conn.prepare(
"SELECT id, name, avatar, role, model, color, docs, prompt,
temperature, top_k, max_tokens, repetition_penalty, is_default,
created_at, updated_at
FROM agents ORDER BY is_default DESC, name"
).unwrap();
stmt.query_map([], |row| {
Ok(serde_json::json!({
"id": row.get::<_, String>(0)?,
"name": row.get::<_, String>(1)?,
"avatar": row.get::<_, String>(2)?,
"role": row.get::<_, String>(3)?,
"model": row.get::<_, String>(4)?,
"color": row.get::<_, String>(5)?,
"docs": row.get::<_, Option<String>>(6)?,
"prompt": row.get::<_, String>(7)?,
"temperature": row.get::<_, f64>(8)?,
"top_k": row.get::<_, i64>(9)?,
"max_tokens": row.get::<_, i64>(10)?,
"repetition_penalty": row.get::<_, f64>(11)?,
"is_default": row.get::<_, bool>(12)?,
"created_at": row.get::<_, String>(13)?,
"updated_at": row.get::<_, String>(14)?,
}))
}).unwrap().filter_map(|r| r.ok()).collect()
}
pub fn delete_agent(&self, id: &str) -> Result<(), String> {
let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
let deleted = conn.execute(
"DELETE FROM agents WHERE id = ?1 AND is_default = 0",
params![id],
).map_err(|e| format!("Agent delete: {}", e))?;
if deleted == 0 {
Err("Agenttia ei löydy tai se on oletusagentti".to_string())
} else {
Ok(())
}
}
pub fn insert_pair_result(
&self,
node_id: u64,

View File

@@ -330,15 +330,6 @@ async fn main() {
let idx = (rng_state as usize) % pairs.len();
let (en, fi) = pairs[idx];
// Tokenisointiparit
let pair_msg = serde_json::json!({
"type": "pair_task",
"en": en,
"fi": fi,
});
let _ = state_for_task.stats_tx.send(pair_msg.to_string());
// LLM-promptit
let llm_prompts = vec![
"Tell me a short joke.",
"What is WebGPU in one sentence?",
@@ -348,33 +339,39 @@ async fn main() {
];
let llm_idx = (rng_state as usize / 7) % llm_prompts.len();
// SmolLM-prompt
let smollm_msg = serde_json::json!({
"type": "llm_prompt",
"prompt": llm_prompts[llm_idx],
"model": "smollm-135m",
});
let _ = state_for_task.stats_tx.send(smollm_msg.to_string());
// Smart Routing: Lähetetään vain niille, jotka valittuna ja idle
let mut sends = Vec::new();
{
let channels = state_for_task.node_channels.read().await;
let tasks = state_for_task.node_tasks.lock().unwrap();
let mut busy = state_for_task.node_busy.lock().unwrap();
// Qwen-prompt (sama prompti, eri malli-tagi)
let qwen_msg = serde_json::json!({
"type": "llm_prompt",
"prompt": llm_prompts[llm_idx],
"model": "qwen-05b",
});
let _ = state_for_task.stats_tx.send(qwen_msg.to_string());
for (node_id, task) in tasks.iter() {
if !busy.contains(node_id) {
// Vapaa node -> lähetetään oikea tehtävä
let msg = match task.as_str() {
"tokenize" => Some(serde_json::json!({ "type": "pair_task", "en": en, "fi": fi })),
"smollm-135m" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "smollm-135m" })),
"qwen-05b" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "qwen-05b" })),
"phi3-mini" => Some(serde_json::json!({ "type": "llm_prompt", "prompt": llm_prompts[llm_idx], "model": "phi3-mini" })),
_ => None, // Coder ja viewer ei saa auto-tehtäviä
};
// Phi-3 prompt
let phi3_msg = serde_json::json!({
"type": "llm_prompt",
"prompt": llm_prompts[llm_idx],
"model": "phi3-mini",
});
let _ = state_for_task.stats_tx.send(phi3_msg.to_string());
if let Some(payload) = msg {
if let Some(ch) = channels.get(node_id) {
sends.push((ch.clone(), payload.to_string()));
busy.insert(*node_id);
}
}
}
}
}
// Coder ei saa automaattisia tehtäviä — vain käyttäjän user_text
for (ch, msg_str) in sends {
let _ = ch.send(msg_str);
}
tracing::debug!("Tehtävät lähetetty: pair + smollm + qwen + phi3");
// tracing::debug!("Tehtävät lähetetty reititetysti idle-nodeille");
}
});
@@ -387,9 +384,11 @@ async fn main() {
.route("/api/v1/model", axum::routing::post(api_change_model))
.route("/api/v1/hardware", get(api_hardware))
.route("/api/v1/ollama/tags", get(api_ollama_tags))
.route("/api/v1/agents", get(api_get_agents).post(api_upsert_agent))
.route("/api/v1/agents/:id", axum::routing::delete(api_delete_agent))
.route("/admin", get(admin_page))
.nest_service("/", {
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../static".to_string());
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../frontend/dist".to_string());
ServeDir::new(&static_dir).fallback(ServeFile::new(format!("{}/index.html", static_dir)))
})
.with_state(state);
@@ -462,6 +461,34 @@ fn admin_unauthorized() -> axum::response::Response {
.unwrap()
}
// ── Agents API ──
async fn api_get_agents(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
) -> axum::response::Response {
axum::Json(state.db.get_agents()).into_response()
}
async fn api_upsert_agent(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
axum::Json(payload): axum::Json<serde_json::Value>,
) -> axum::response::Response {
match state.db.upsert_agent(&payload) {
Ok(()) => axum::Json(serde_json::json!({"ok": true})).into_response(),
Err(e) => (axum::http::StatusCode::BAD_REQUEST, e).into_response(),
}
}
async fn api_delete_agent(
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
axum::extract::Path(id): axum::extract::Path<String>,
) -> axum::response::Response {
match state.db.delete_agent(&id) {
Ok(()) => axum::Json(serde_json::json!({"ok": true})).into_response(),
Err(e) => (axum::http::StatusCode::BAD_REQUEST, e).into_response(),
}
}
async fn admin_page(headers: axum::http::HeaderMap) -> axum::response::Response {
if !check_admin_auth(&headers) { return admin_unauthorized(); }
axum::response::Html(ADMIN_HTML).into_response()
@@ -491,16 +518,19 @@ async fn ws_handler(
.and_then(|s| s.trim().parse::<IpAddr>().ok())
.unwrap_or_else(|| addr.ip());
// Max yhteyttä per IP: jokainen selain tarvitsee 2 (UI + coder-node)
// Max yhteyttä per IP (ei rajoiteta localhost/127.0.0.1)
{
let conns = state.ip_connections.lock().unwrap();
let count = conns.get(&ip).copied().unwrap_or(0);
if count >= 10 {
tracing::warn!("IP {} ylitti yhteysrajan ({}/10) — estetty", ip, count);
return (
axum::http::StatusCode::TOO_MANY_REQUESTS,
"Max 10 yhteyttä per IP",
).into_response();
let is_local = ip.is_loopback();
if !is_local {
let conns = state.ip_connections.lock().unwrap();
let count = conns.get(&ip).copied().unwrap_or(0);
if count >= 20 {
tracing::warn!("IP {} ylitti yhteysrajan ({}/20) — estetty", ip, count);
return (
axum::http::StatusCode::TOO_MANY_REQUESTS,
"Max 20 yhteyttä per IP",
).into_response();
}
}
}
@@ -661,6 +691,18 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
let allocated = json.get("allocated_gb").and_then(|v| v.as_u64()).unwrap_or(4) as u32;
let node_type = json.get("node_type").and_then(|v| v.as_str()).unwrap_or("browser");
// API-avain vaaditaan natiivisolmuilta (ei selaimilta)
if node_type == "native" {
let required_key = std::env::var("NODE_API_KEY").unwrap_or_default();
if !required_key.is_empty() {
let provided_key = json.get("api_key").and_then(|v| v.as_str()).unwrap_or("");
if provided_key != required_key {
tracing::warn!("Solmu {} ({}) hylätty: virheellinen API-avain", node_id, ip);
break; // Suljetaan WebSocket
}
}
}
{
let mut map = state.nodes_vram.lock().unwrap();
map.insert(node_id, allocated);
@@ -741,6 +783,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
}
broadcast_stats(&state).await;
} else if msg_type == "pair_done" {
state.node_busy.lock().unwrap().remove(&node_id);
{
let mut json = json; // Siirretään omistajuus muokkausta varten
if let Some(obj) = json.as_object_mut() {
@@ -1054,93 +1097,50 @@ async fn api_chat_completions(
}
// Etsitään vapaa solmu — priorisoidaan natiivisolmut (GPU) selaimen edelle
let (target_node_free, target_node_any, total_matching) = {
let (target_node, _total_matching) = {
let tasks = state.node_tasks.lock().unwrap();
let busy = state.node_busy.lock().unwrap();
let node_types = state.node_types.lock().unwrap();
let matching: Vec<u64> = tasks.iter().filter(|(_, task)| {
if payload.model == "qwen-coder" {
task.starts_with("qwen-coder")
// Eksakti match tai qwen-perheen yhteensopivuus (selain: qwen-coder-05b, natiivi: qwen2.5-coder:7b)
let req_model = payload.model.to_lowercase();
let node_task = task.to_lowercase();
if req_model.starts_with("qwen") {
node_task.starts_with("qwen")
} else if req_model.starts_with("phi") {
node_task.starts_with("phi")
} else {
**task == payload.model
}
}).map(|(k, _)| *k).collect();
// Vapaat solmut: natiivi ensin, sitten selain
let free_native = matching.iter().find(|id| {
!busy.contains(id) && node_types.get(id).map(|t| t == "native").unwrap_or(false)
// Etsitään mikä tahansa matchaava solmu (natiivi priorisoidaan)
let native = matching.iter().find(|id| {
node_types.get(id).map(|t| t == "native").unwrap_or(false)
}).copied();
let free_any = matching.iter().find(|id| !busy.contains(id)).copied();
let free = free_native.or(free_any);
let any = matching.first().copied();
(free, any, matching.len())
let any = native.or_else(|| matching.first().copied());
(any, matching.len())
};
// Broadcastataan reititystila UI:lle
let task_id = payload.task_id.clone();
if target_node_any.is_none() {
// Ei yhtään solmua tälle mallille
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
}
let target_node_id = match target_node {
Some(id) => id,
None => {
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Ei solmua tälle mallille (käynnistä malli selaimessa)").into_response();
}
};
let target_node_id;
if let Some(free_id) = target_node_free {
// Vapaa solmu löytyi — reititetään suoraan
target_node_id = free_id;
let node_type = if state.node_tasks.lock().unwrap().get(&free_id).map(|t| t.contains("native")).unwrap_or(false) { "natiivi" } else { "selain" };
// Reititystila UI:lle
{
let routing_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"node_id": free_id,
"node_type": node_type,
"node_id": target_node_id,
"status": "routed",
"message": format!("Reititetty solmulle #{}", free_id),
"message": format!("Reititetty solmulle #{}", target_node_id),
});
let _ = state.stats_tx.send(routing_msg.to_string());
} else {
// Kaikki solmut varattuja — odotetaan vapautumista (max 30s)
let queue_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"status": "queued",
"message": format!("Kaikki {} solmua varattuja — odotetaan vapautumista...", total_matching),
});
let _ = state.stats_tx.send(queue_msg.to_string());
// Pollaa busy-tilaa 500ms välein, max 30s
let mut waited = 0u32;
loop {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
waited += 500;
let free = {
let tasks = state.node_tasks.lock().unwrap();
let busy = state.node_busy.lock().unwrap();
tasks.iter().find(|(node_id, task)| {
let model_match = if payload.model == "qwen-coder" {
*task == "qwen-coder-05b" || *task == "qwen-coder"
} else {
**task == payload.model
};
model_match && !busy.contains(node_id)
}).map(|(k, _)| *k)
};
if let Some(id) = free {
target_node_id = id;
let routing_msg = serde_json::json!({
"type": "task_routed",
"task_id": task_id,
"node_id": id,
"status": "routed",
"message": format!("Solmu #{} vapautui — reititetään ({:.1}s jonossa)", id, waited as f64 / 1000.0),
});
let _ = state.stats_tx.send(routing_msg.to_string());
break;
}
if waited >= 30000 {
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "Aikakatkaisu: kaikki solmut varattuja 30s ajan").into_response();
}
}
};
}
// Merkitään solmu varatuksi ja task_id jaetuksi
state.node_busy.lock().unwrap().insert(target_node_id);

59
network-poc/install.sh Executable file
View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Kipinä Agentic Studio — asennusskripti (Debian/Ubuntu)
set -e
echo "=== Kipinä Agentic Studio — Asennus ==="
echo ""
# Tarkistetaan käyttöjärjestelmä
if [ ! -f /etc/debian_version ]; then
echo "⚠ Tämä skripti on suunniteltu Debian/Ubuntu-järjestelmille."
echo " Muilla jakeluilla voit asentaa riippuvuudet manuaalisesti."
read -p " Jatketaanko? (k/e) " -n 1 -r; echo
[[ $REPLY =~ ^[Kk]$ ]] || exit 1
fi
echo "[1/6] Päivitetään pakettilistaus..."
sudo apt-get update -qq
echo "[2/6] Asennetaan peruspaketteja..."
sudo apt-get install -y -qq curl git build-essential pkg-config libssl-dev
# Rust
if command -v rustc &>/dev/null; then
echo "[3/6] Rust löytyi: $(rustc --version)"
else
echo "[3/6] Asennetaan Rust..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
fi
# Node.js (Astro-frontend vaatii)
if command -v node &>/dev/null; then
echo "[4/6] Node.js löytyi: $(node --version)"
else
echo "[4/6] Asennetaan Node.js 22..."
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y -qq nodejs
fi
# Ollama
if command -v ollama &>/dev/null; then
echo "[5/6] Ollama löytyi"
else
echo "[5/6] Asennetaan Ollama..."
curl -fsSL https://ollama.ai/install.sh | sh
fi
# Malli
echo "[6/6] Ladataan kielimalli (qwen2.5-coder:3b)..."
ollama pull qwen2.5-coder:3b
echo ""
echo "=== Asennus valmis! ==="
echo ""
echo "Käynnistä:"
echo " cd $(pwd)"
echo " ./network-poc/local.sh"
echo ""
echo "Avaa selaimessa: http://localhost:3000"

37
network-poc/local.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Kipinä Studio Local Development ==="
# Frontend
echo "[1/3] Rakennetaan frontend..."
cd "$SCRIPT_DIR/frontend"
[ -d node_modules ] || npm install --silent
npm run build --silent 2>&1 | tail -1
# Hub
echo "[2/3] Käynnistetään hub..."
cd "$SCRIPT_DIR/hub"
cargo run &
HUB_PID=$!
sleep 3
# Native-node (jos Ollama on käynnissä)
if curl -s http://localhost:11434/api/tags >/dev/null 2>&1; then
echo "[3/3] Ollama löytyi — käynnistetään native-node..."
cd "$SCRIPT_DIR/native-node"
HUB_URL=ws://localhost:3000/ws cargo run --no-default-features &
NODE_PID=$!
echo " Native-node PID: $NODE_PID"
else
echo "[3/3] Ollama ei käynnissä — käytetään selaimen Wasm-laskentaa"
echo " Nopeampi: ollama serve & ollama pull qwen2.5-coder:7b && ./local.sh"
fi
echo ""
echo "=== http://localhost:3000 ==="
echo " Ctrl+C pysäyttää"
# Odotetaan hub-prosessia
wait $HUB_PID

View File

@@ -3,6 +3,10 @@ name = "native-node"
version = "0.2.2"
edition = "2024"
[features]
default = ["gpu-detect"]
gpu-detect = ["nvml-wrapper", "wgpu"]
[dependencies]
tokio = { version = "1.36", features = ["full"] }
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
@@ -10,8 +14,8 @@ futures-util = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sysinfo = "0.30"
nvml-wrapper = "0.10"
wgpu = "24"
nvml-wrapper = { version = "0.10", optional = true }
wgpu = { version = "24", optional = true }
reqwest = { version = "0.12", features = ["json"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@@ -9,7 +9,7 @@ pub struct LlmEngine {
impl LlmEngine {
pub async fn load() -> Result<Self, String> {
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:7b".to_string());
let model = std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen2.5-coder:3b".to_string());
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(600))
@@ -79,7 +79,8 @@ impl LlmEngine {
}
pub async fn generate(&self, prompt: &str, max_tokens: usize) -> Result<GenerateResult, String> {
let system = "You are a coding assistant. Respond with ONLY code. Use proper newlines and indentation. No explanations, no markdown fences, no comments unless asked.";
// System prompt tulee agentin konfiguraatiosta (frontend lähettää sen osana promptia).
// Tässä ei yliajeta sitä — Ollama saa vain prompt-kentän.
let model = self.model.borrow().clone();
let start = Instant::now();
@@ -87,14 +88,13 @@ impl LlmEngine {
.json(&serde_json::json!({
"model": model,
"prompt": prompt,
"system": system,
"stream": false,
"options": {
"num_predict": max_tokens,
"temperature": 0.7,
"top_k": 40,
"repeat_penalty": 1.15,
"stop": ["<|im_end|>", "\n###", "\nExplanation", "\nNote:"]
"stop": ["<|im_end|>", "\n###", "\nExplanation", "\nNote:", "\nPlease note", "\nThis is", "\n```\n\n", "\n// Example", "\n# Example"]
}
}))
.send()
@@ -109,7 +109,7 @@ impl LlmEngine {
.map_err(|e| format!("Ollama JSON: {}", e))?;
let text = body["response"].as_str().unwrap_or("").to_string();
let total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
let _total_duration_ns = body["total_duration"].as_u64().unwrap_or(0);
let eval_count = body["eval_count"].as_u64().unwrap_or(0) as usize;
let eval_duration_ns = body["eval_duration"].as_u64().unwrap_or(1);
@@ -127,27 +127,40 @@ impl LlmEngine {
}
}
/// Siivoa mahdolliset markdown-koodiblokki-merkit
/// Siivoa markdown-koodiblokki-merkit ja selitystekstit
fn strip_code_fences(text: &str) -> String {
let mut result = text.trim().to_string();
// Poistetaan kaikki ```-rivit ja kielitunnisteet (```python, ```rust jne.)
let lines: Vec<&str> = text.lines().collect();
let filtered: Vec<&str> = lines.into_iter().filter(|line| {
let trimmed = line.trim();
// Poista rivit jotka ovat pelkkiä ``` tai ```kielitunniste
if trimmed.starts_with("```") {
return false;
}
true
}).collect();
let mut result = filtered.join("\n").trim().to_string();
// Poista aloittava ```lang
if result.starts_with("```") {
if let Some(nl) = result.find('\n') {
result = result[nl + 1..].to_string();
// Poista selitysteksti lopusta (kaikki rivin "\nPlease note" jälkeen jne.)
let lower = result.to_lowercase();
for stop in &["\nplease note", "\nthis is a basic", "\nthis code", "\nnote that", "\nremember to", "\nyou can", "\nto run"] {
if let Some(pos) = lower.find(stop) {
result = result[..pos].trim_end().to_string();
}
}
// Poista sulkeva ```
let trimmed = result.trim_end();
if trimmed.ends_with("```") {
let before = &trimmed[..trimmed.len() - 3];
if before.is_empty() || before.ends_with('\n') {
result = before.trim_end().to_string();
// Poista johdantolauseet alusta
let lower = result.to_lowercase();
for prefix in &["sure!", "here is", "here's", "certainly!", "below is"] {
if lower.starts_with(prefix) {
if let Some(nl) = result.find('\n') {
result = result[nl + 1..].to_string();
}
break;
}
}
result
result.trim().to_string()
}
pub struct GenerateResult {

View File

@@ -33,6 +33,7 @@ impl GpuInfo {
}
}
#[cfg(feature = "gpu-detect")]
/// Tunnistaa kaikki GPU:t wgpu:lla (NVIDIA/AMD/Apple/Intel)
fn collect_gpus_wgpu() -> Vec<GpuInfo> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
@@ -84,6 +85,7 @@ fn collect_gpus_wgpu() -> Vec<GpuInfo> {
gpus
}
#[cfg(feature = "gpu-detect")]
/// Täydentää NVIDIA-GPU:iden tiedot NVML:llä (VRAM, lämpötila, kuormitus)
fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
let Ok(nvml) = nvml_wrapper::Nvml::init() else { return };
@@ -109,6 +111,7 @@ fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
}
}
#[cfg(feature = "gpu-detect")]
/// AMD GPU-tiedot Linuxin sysfs:stä (/sys/class/drm/)
fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return };
@@ -150,10 +153,12 @@ fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
}
}
#[cfg(feature = "gpu-detect")]
fn read_sysfs_u64(path: &std::path::Path) -> Option<u64> {
std::fs::read_to_string(path).ok()?.trim().parse().ok()
}
#[cfg(feature = "gpu-detect")]
fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
let hwmon_dir = device_path.join("hwmon");
let entries = std::fs::read_dir(&hwmon_dir).ok()?;
@@ -166,8 +171,8 @@ fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
None
}
#[cfg(feature = "gpu-detect")]
/// Apple GPU-tiedot — wgpu/Metal antaa nimen, tarkempaa dataa ei saa ilman IOKit:ia
/// mutta Metal adapter_info sisältää jo olennaiset tiedot
fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
// Apple Silicon -koneiden unified memory: koko RAM on GPU:n käytettävissä
// Arvioidaan system RAM:sta
@@ -187,13 +192,18 @@ fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
/// Kerää kaikki GPU:t ja täydentää valmistajakohtaiset tiedot
fn collect_all_gpus() -> Vec<GpuInfo> {
let mut gpus = collect_gpus_wgpu();
enrich_nvidia_gpus(&mut gpus);
enrich_amd_gpus(&mut gpus);
enrich_apple_gpus(&mut gpus);
gpus
#[cfg(feature = "gpu-detect")]
{
let mut gpus = collect_gpus_wgpu();
enrich_nvidia_gpus(&mut gpus);
enrich_amd_gpus(&mut gpus);
enrich_apple_gpus(&mut gpus);
return gpus;
}
#[cfg(not(feature = "gpu-detect"))]
{
Vec::new()
}
}
/// Kerää järjestelmätiedot (CPU, RAM, OS)
@@ -222,15 +232,21 @@ fn build_auth_message(allocated_gb: u32) -> String {
v
}).collect();
let api_key = std::env::var("NODE_API_KEY").unwrap_or_default();
let mut msg = json!({
"type": "auth",
"status": "agent_ready",
"node_type": "native",
"allocated_gb": allocated_gb,
"selected_task": "qwen-coder-05b",
"selected_task": "qwen2.5-coder:7b",
"system": sys,
});
if !api_key.is_empty() {
msg.as_object_mut().unwrap().insert("api_key".to_string(), json!(api_key));
}
if !gpu_json.is_empty() {
msg.as_object_mut().unwrap().insert("gpus".to_string(), json!(gpu_json));
}
@@ -269,6 +285,9 @@ async fn main() {
let gpus = collect_all_gpus();
if gpus.is_empty() {
#[cfg(not(feature = "gpu-detect"))]
tracing::info!("GPU-tunnistus ei käytössä (--no-default-features). Ollama käyttää GPU:ta automaattisesti jos saatavilla.");
#[cfg(feature = "gpu-detect")]
tracing::info!("GPU:ta ei havaittu — toimitaan CPU-moodissa");
} else {
for (i, gpu) in gpus.iter().enumerate() {
@@ -315,22 +334,19 @@ async fn main() {
continue;
}
let mut busy = false;
while let Some(Ok(msg)) = read.next().await {
if let Message::Text(text) = msg {
// LLM-promptit
if text.contains("llm_prompt") && !busy {
if text.contains("llm_prompt") {
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&text) {
let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or("");
let task_id = task.get("task_id").and_then(|v| v.as_str()).unwrap_or("?");
let msg_model = task.get("model").and_then(|v| v.as_str()).unwrap_or("");
if !prompt.is_empty() && msg_model.starts_with("qwen-coder") {
if !prompt.is_empty() && (msg_model.starts_with("qwen-coder") || msg_model.starts_with("qwen2.5-coder")) {
if let Some(ref engine) = llm {
busy = true;
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(512) as usize;
let max_tokens = task.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(1024) as usize;
tracing::info!("Generoidaan (task_id: {}, max_tokens: {}): \"{}\"", task_id, max_tokens, &prompt[..prompt.len().min(100)]);
let model_name = engine.model_name();
@@ -361,7 +377,6 @@ async fn main() {
tracing::error!("Inferenssivirhe: {}", e);
}
}
busy = false;
}
}
}

View File

@@ -320,7 +320,11 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
if let Ok(text) = cached.tokenizer.decode(&[next_token], true) {
generated_text.push_str(&text);
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); }
if let Some(ref tid) = task_id {
if let Some(obj) = chunk.as_object_mut() {
obj.insert("task_id".to_string(), serde_json::json!(tid));
}
}
let _ = ws.borrow().send_with_str(&chunk.to_string());
}
all_generated.push(next_token);
@@ -362,7 +366,11 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
}
let mut chunk = serde_json::json!({ "type": "llm_chunk", "token": text, "prompt": prompt, "model": "Qwen2.5-Coder" });
if let Some(ref tid) = task_id { chunk.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid)); }
if let Some(ref tid) = task_id {
if let Some(obj) = chunk.as_object_mut() {
obj.insert("task_id".to_string(), serde_json::json!(tid));
}
}
let _ = ws.borrow().send_with_str(&chunk.to_string());
}
all_generated.push(next_token);
@@ -391,7 +399,9 @@ pub async fn run_coder_inference(prompt: String, ws: Rc<RefCell<WebSocket>>, use
"load_time_ms": (load_time * 100.0).round() / 100.0,
});
if let Some(tid) = task_id {
done.as_object_mut().unwrap().insert("task_id".to_string(), serde_json::json!(tid));
if let Some(obj) = done.as_object_mut() {
obj.insert("task_id".to_string(), serde_json::json!(tid));
}
}
let _ = ws.borrow().send_with_str(&done.to_string());
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 KiB

View File

@@ -0,0 +1,43 @@
# OpenTofu Core Codebase Documentation
This directory contains some documentation about the OpenTofu Core codebase,
aimed at readers who are interested in making code contributions.
If you're looking for information on _using_ OpenTofu, please instead refer
to [the main OpenTofu CLI documentation](https://opentofu.org/docs/cli/index.html).
## OpenTofu Core Architecture Documents
* [OpenTofu Core Architecture Summary](./architecture.md): an overview of the
main components of OpenTofu Core and how they interact. This is the best
starting point if you are diving in to this codebase for the first time.
* [Resource Instance Change Lifecycle](./resource-instance-change-lifecycle.md):
a description of the steps in validating, planning, and applying a change
to a resource instance, from the perspective of the provider plugin RPC
operations. This may be useful for understanding the various expectations
OpenTofu enforces about provider behavior, either if you intend to make
changes to those behaviors or if you are implementing a new OpenTofu plugin
SDK and so wish to conform to them.
(If you are planning to write a new provider using the _official_ SDK then
please refer to [the Extend documentation](https://github.com/hashicorp/terraform-docs-common)
instead; it presents similar information from the perspective of the SDK
API, rather than the plugin wire protocol.)
* [Diagnostics](./diagnostics): how we report errors and warnings to end-users
in OpenTofu.
* [Plugin Protocol](./plugin-protocol/): gRPC/protobuf definitions for the
plugin wire protocol and information about its versioning strategy.
This documentation is for SDK developers, and is not necessary reading for
those implementing a provider using the official SDK.
* [How OpenTofu Uses Unicode](./unicode.md): an overview of the various
features of OpenTofu that rely on Unicode and how to change those features
to adopt new versions of Unicode.
## Contribution Guides
* [Contributing to OpenTofu](../CONTRIBUTING.md): a complete guideline for those who want to contribute to this project.

View File

@@ -0,0 +1,374 @@
# OpenTofu Core Architecture Summary
This document is a summary of the main components of OpenTofu Core and how
data and requests flow between these components. It's intended as a primer
to help navigate the codebase to dig into more details.
We assume some familiarity with user-facing OpenTofu concepts like
configuration, state, CLI workflow, etc. The OpenTofu website has
documentation on these ideas.
## OpenTofu Request Flow
The following diagram shows an approximation of how a user command is
executed in OpenTofu:
![OpenTofu Architecture Diagram, described in text below](./images/architecture-overview.png)
Each of the different subsystems (solid boxes) in this diagram is described
in more detail in a corresponding section below.
## CLI (`command` package)
Each time a user runs the `tofu` program, aside from some initial
bootstrapping in the root package (not shown in the diagram) execution
transfers immediately into one of the "command" implementations in
[the `command` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/command).
The mapping between the user-facing command names and
their corresponding `command` package types can be found in the `commands.go`
file under the `cmd/tofu` directory (package `main`).
The full flow illustrated above does not actually apply to _all_ commands,
but it applies to the main OpenTofu workflow commands `tofu plan` and
`tofu apply`, along with a few others.
For these commands, the role of the command implementation is to read and parse
any command line arguments, command line options, and environment variables
that are needed for the given command and use them to produce a
[`backend.Operation`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend#Operation)
object that describes an action to be taken.
An _operation_ consists of:
* The action to be taken (e.g. "plan", "apply").
* The name of the [workspace](https://opentofu.org/docs/language/state/workspaces)
where the action will be taken.
* Root module input variables to use for the action.
* For the "plan" operation, a path to the directory containing the configuration's root module.
* For the "apply" operation, the plan to apply.
* Various other less-common options/settings such as `-target` addresses, the
"force" flag, etc.
The operation is then passed to the currently-selected
[backend](https://opentofu.org/docs/language/settings/backends/configuration). Each backend name
corresponds to an implementation of
[`backend.Backend`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend#Backend), using a
mapping table in
[the `backend/init` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend/init).
Backends that are able to execute operations additionally implement
[`backend.Enhanced`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend#Enhanced);
the command-handling code calls `Operation` with the operation it has
constructed, and then the backend is responsible for executing that action.
Backends that execute operations, however, do so as an architectural implementation detail and not a
general feature of backends. That is, the term 'backend' as a OpenTofu feature is used to refer to
a plugin that determines where OpenTofu stores its state snapshots - only the default `local`, `remote` and `cloud` backends perform operations.
Thus, most backends do _not_ implement this interface, and so the `command` package wraps these
backends in an instance of
[`local.Local`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/backend/local#Local),
causing the operation to be executed locally within the `tofu` process itself.
## Backends
A _backend_ determines where OpenTofu should store its state snapshots.
As described above, the `local` backend also executes operations on behalf of most other
backends. It uses a _state manager_
(either
[`statemgr.Filesystem`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr#Filesystem) if the
local backend is being used directly, or an implementation provided by whatever
backend is being wrapped) to retrieve the current state for the workspace
specified in the operation, then uses the _config loader_ to load and do
initial processing/validation of the configuration specified in the
operation. It then uses these, along with the other settings given in the
operation, to construct a
[`tofu.Context`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context),
which is the main object that actually performs OpenTofu operations.
The `local` backend finally calls an appropriate method on that context to
begin execution of the relevant command, such as
[`Plan`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context.Plan)
or
[`Apply`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context.Apply), which in turn constructs a graph using a _graph builder_,
described in a later section.
## Configuration Loader
The top-level configuration structure is represented by model types in
[package `configs`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs).
A whole configuration (the root module plus all of its descendent modules)
is represented by
[`configs.Config`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs#Config).
The `configs` package contains some low-level functionality for constructing
configuration objects, but the main entry point is in the sub-package
[`configload`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs/configload]),
via
[`configload.Loader`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs/configload#Loader).
A loader deals with all of the details of installing child modules
(during `tofu init`) and then locating those modules again when a
configuration is loaded by a backend. It takes the path to a root module
and recursively loads all of the child modules to produce a single
[`configs.Config`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/configs#Config)
representing the entire configuration.
OpenTofu expects configuration files written in the OpenTofu language, which
is a DSL built on top of
[HCL](https://github.com/hashicorp/hcl). Some parts of the configuration
cannot be interpreted until we build and walk the graph, since they depend
on the outcome of other parts of the configuration, and so these parts of
the configuration remain represented as the low-level HCL types
[`hcl.Body`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Body)
and
[`hcl.Expression`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Expression),
allowing OpenTofu to interpret them at a more appropriate time.
## State Manager
A _state manager_ is responsible for storing and retrieving snapshots of the
[OpenTofu state](https://opentofu.org/docs/language/state/index.html)
for a particular workspace. Each manager is an implementation of
some combination of interfaces in
[the `statemgr` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr),
with most practical managers implementing the full set of operations
described by
[`statemgr.Full`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr#Full)
provided by a _backend_. The smaller interfaces exist primarily for use in
other function signatures to be explicit about what actions the function might
take on the state manager; there is little reason to write a state manager
that does not implement all of `statemgr.Full`.
The implementation
[`statemgr.Filesystem`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states/statemgr#Filesystem) is used
by default (by the `local` backend) and is responsible for the familiar
`terraform.tfstate` local file that most OpenTofu users start with, before
they switch to [remote state](https://opentofu.org/docs/language/state/remote).
Other implementations of `statemgr.Full` are used to implement remote state.
Each of these saves and retrieves state via a remote network service
appropriate to the backend that creates it.
A state manager accepts and returns a state snapshot as a
[`states.State`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states#State)
object. The state manager is responsible for exactly how that object is
serialized and stored, but all state managers at the time of writing use
the same JSON serialization format, storing the resulting JSON bytes in some
kind of arbitrary blob store.
## Graph Builder
A _graph builder_ is called by a
[`tofu.Context`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Context)
method (e.g. `Plan` or `Apply`) to produce the graph that will be used
to represent the necessary steps for that operation and the dependency
relationships between them.
In most cases, the
[vertices](https://en.wikipedia.org/wiki/Vertex_(graph_theory)) of OpenTofu's
graphs each represent a specific object in the configuration, or something
derived from those configuration objects. For example, each `resource` block
in the configuration has one corresponding
[`GraphNodeConfigResource`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeConfigResource)
vertex representing it in the "plan" graph. (OpenTofu Core uses terminology
inconsistently, describing graph _vertices_ also as graph _nodes_ in various
places. These both describe the same concept.)
The [edges](https://en.wikipedia.org/wiki/Glossary_of_graph_theory_terms#edge)
in the graph represent "must happen after" relationships. These define the
order in which the vertices are evaluated, ensuring that e.g. one resource is
created before another resource that depends on it.
Each operation has its own graph builder, because the graph building process
is different for each. For example, a "plan" operation needs a graph built
directly from the configuration, but an "apply" operation instead builds its
graph from the set of changes described in the plan that is being applied.
The graph builders all work in terms of a sequence of _transforms_, which
are implementations of
[`tofu.GraphTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphTransformer).
Implementations of this interface just take a graph and mutate it in any
way needed, and so the set of available transforms is quite varied. Some
important examples include:
* [`ConfigTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ConfigTransformer),
which creates a graph vertex for each `resource` block in the configuration.
* [`StateTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#StateTransformer),
which creates a graph vertex for each resource instance currently tracked
in the state.
* [`ReferenceTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ReferenceTransformer),
which analyses the configuration to find dependencies between resources and
other objects and creates any necessary "happens after" edges for these.
* [`ProviderTransformer`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ProviderTransformer),
which associates each resource or resource instance with exactly one
provider configuration (implementing
[the inheritance rules](https://opentofu.org/docs/language/providers/))
and then creates "happens after" edges to ensure that the providers are
initialized before taking any actions with the resources that belong to
them.
There are many more different graph transforms, which can be discovered
by reading the source code for the different graph builders. Each graph
builder uses a different subset of these depending on the needs of the
operation that is being performed.
The result of graph building is a
[`tofu.Graph`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#Graph), which
can then be processed using a _graph walker_.
## Graph Walk
The process of walking the graph visits each vertex of that graph in a way
which respects the "happens after" edges in the graph. The walk algorithm
itself is implemented in
[the low-level `dag` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/dag#AcyclicGraph.Walk)
(where "DAG" is short for [_Directed Acyclic Graph_](https://en.wikipedia.org/wiki/Directed_acyclic_graph)), in
[`AcyclicGraph.Walk`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/dag#AcyclicGraph.Walk).
However, the "interesting" OpenTofu walk functionality is implemented in
[`tofu.ContextGraphWalker`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#ContextGraphWalker),
which implements a small set of higher-level operations that are performed
during the graph walk:
* `EnterPath` is called once for each module in the configuration, taking a
module address and returning a
[`tofu.EvalContext`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#EvalContext)
that tracks objects within that module. `tofu.Context` is the _global_
context for the entire operation, while `tofu.EvalContext` is a
context for processing within a single module, and is the primary means
by which the namespaces in each module are kept separate.
Each vertex in the graph is evaluated, in an order that guarantees that the
"happens after" edges will be respected. If possible, the graph walk algorithm
will evaluate multiple vertices concurrently. Vertex evaluation code must
therefore make careful use of concurrency primitives such as mutexes in order
to coordinate access to shared objects such as the `states.State` object.
In most cases, we use the helper wrapper
[`states.SyncState`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/states#SyncState)
to safely implement concurrent reads and writes from the shared state.
## Vertex Evaluation
The action taken for each vertex during the graph walk is called
_execution_. Execution runs a sequence of arbitrary actions that make sense
for a particular vertex type.
For example, evaluation of a vertex representing a resource instance during
a plan operation would include the following high-level steps:
* Retrieve the resource's associated provider from the `EvalContext`. This
should already be initialized earlier by the provider's own graph vertex,
due to the "happens after" edge between the resource node and the provider
node.
* Retrieve from the state the portion relevant to the specific resource
instance being evaluated.
* Evaluate the attribute expressions given for the resource in configuration.
This often involves retrieving the state of _other_ resource instances so
that their values can be copied or transformed into the current instance's
attributes, which is coordinated by the `EvalContext`.
* Pass the current instance state and the resource configuration to the
provider, asking the provider to produce an _instance diff_ representing the
differences between the state and the configuration.
* Save the instance diff as part of the plan that is being constructed by
this operation.
Each execution step for a vertex is an implementation of
[`tofu.Execute`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeExecutable.Execute).
As with graph transforms, the behavior of these implementations varies widely:
whereas graph transforms can take any action against the graph, an `Execute`
implementation can take any action against the `EvalContext`.
The implementation of `tofu.EvalContext` used in real processing
(as opposed to testing) is
[`tofu.BuiltinEvalContext`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#BuiltinEvalContext).
It provides coordinated access to plugins, the current state, and the current
plan via the `EvalContext` interface methods.
In order to be executed, a vertex must implement
[`tofu.GraphNodeExecutable`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeExecutable),
which has a single `Execute` method that handles. There are numerous `Execute`
implementations with different behaviors, but some prominent examples are:
* [NodePlannableResource.Execute](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#NodePlannableResourceInstance.Execute), which handles the `plan` operation.
* [`NodeApplyableResourceInstance.Execute`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#NodeApplyableResourceInstance.Execute), which handles the main `apply` operation.
* [`NodeDestroyResourceInstance.Execute`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#EvalWriteState), which handles the main `destroy` operation.
A vertex must complete successfully before the graph walk will begin evaluation
for other vertices that have "happens after" edges. Evaluation can fail with one
or more errors, in which case the graph walk is halted and the errors are
returned to the user.
### Expression Evaluation
An important part of vertex evaluation for most vertex types is evaluating
any expressions in the configuration block associated with the vertex. This
completes the processing of the portions of the configuration that were not
processed by the configuration loader.
The high-level process for expression evaluation is:
1. Analyze the configuration expressions to see which other objects they refer
to. For example, the expression `aws_instance.example[1]` refers to one of
the instances created by a `resource "aws_instance" "example"` block in
configuration. This analysis is performed by
[`lang.References`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#References),
or more often one of the helper wrappers around it:
[`lang.ReferencesInBlock`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#ReferencesInBlock)
or
[`lang.ReferencesInExpr`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#ReferencesInExpr)
1. Retrieve from the state the data for the objects that are referred to and
create a lookup table of the values from these objects that the
HCL evaluation code can refer to.
1. Prepare the table of built-in functions so that HCL evaluation can refer to
them.
1. Ask HCL to evaluate each attribute's expression (a
[`hcl.Expression`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Expression)
object) against the data and function lookup tables.
In practice, steps 2 through 4 are usually run all together using one
of the methods on [`lang.Scope`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#Scope);
most commonly,
[`lang.EvalBlock`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#Scope.EvalBlock)
or
[`lang.EvalExpr`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/lang#Scope.EvalExpr).
Expression evaluation produces a dynamic value represented as a
[`cty.Value`](https://pkg.go.dev/github.com/zclconf/go-cty/cty#Value).
This Go type represents values from the OpenTofu language and such values
are eventually passed to provider plugins.
### Sub-graphs
Some vertices have a special additional behavior that happens after their
evaluation steps are complete, where the vertex implementation is given
the opportunity to build another separate graph which will be walked as part
of the evaluation of the vertex.
The main example of this is when a `resource` block has the `count` argument
set. In that case, the plan graph initially contains one vertex for each
`resource` block, but that graph then _dynamically expands_ to have a sub-graph
containing one vertex for each instance requested by the count. That is, the
sub-graph of `aws_instance.example` might contain vertices for
`aws_instance.example[0]`, `aws_instance.example[1]`, etc. This is necessary
because the `count` argument may refer to other objects whose values are not
known when the main graph is constructed, but become known while evaluating
other vertices in the main graph.
This special behavior applies to vertex objects that implement
[`tofu.GraphNodeDynamicExpandable`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tofu#GraphNodeDynamicExpandable).
Such vertices have their own nested _graph builder_, _graph walk_,
and _vertex evaluation_ steps, with the same behaviors as described in these
sections for the main graph. The difference is in which graph transforms
are used to construct the graph and in which evaluation steps apply to the
nodes in that sub-graph.

View File

@@ -0,0 +1,361 @@
# OpenTofu Core Resource Destruction Notes
This document intends to describe some of the details and complications
involved in the destruction of resources. It covers the ordering defined for
related create and destroy operations, as well as changes to the lifecycle
ordering imposed by `create_before_destroy`. It is not intended to enumerate
all possible combinations of dependency ordering, only to outline the basics
and document some of the more complicated aspects of resource destruction.
The graph diagrams here will continue to use the inverted graph structure used
internally by OpenTofu, where edges represent dependencies rather than order
of operations.
## Simple Resource Creation
In order to describe resource destruction, we first need to create the
resources and define their order. The order of creation is that which fulfills
the dependencies for each resource. In this example, `A` has no dependencies,
`B` depends on `A`, and `C` depends on `B`, and transitively depends on `A`.
![Simple Resource Creation](./images/simple_create.png)
<!--
digraph create {
subgraph nodes {
rank=same;
a [label="A create"];
b [label="B create"];
c [label="C create"];
b -> c [dir=back];
a -> b [dir=back];
}
}
-->
Order of operations:
1. `A` is created
1. `B` is created
1. `C` is created
## Resource Updates
An existing resource may be updated with references to a newly created
resource. The ordering here is exactly the same as one would expect for
creation.
![Simple Resource Updates](./images/simple_update.png)
<!--
digraph update {
subgraph nodes {
rank=same;
a [label="A create"];
b [label="B update"];
c [label="C update"];
b -> c [dir=back];
a -> b [dir=back];
}
}
-->
Order of operations:
1. `A` is created
1. `B` is created
1. `C` is created
## Simple Resource Destruction
The order for destroying resource is exactly the inverse used to create them.
This example shows the graph for the destruction of the same nodes defined
above. While destroy nodes will not contain attribute references, we will
continue to use the inverted edges showing dependencies for destroy, so the
operational ordering is still opposite the flow of the arrows.
![Simple Resource Destruction](./images/simple_destroy.png)
<!--
digraph destroy {
subgraph nodes {
rank=same;
a [label="A destroy"];
b [label="B destroy"];
c [label="C destroy"];
a -> b;
b -> c;
}
}
-->
Order of operations:
1. `C` is destroyed
1. `B` is destroyed
1. `A` is Destroyed
## Resource Replacement
Resource replacement is the logical combination of the above scenarios. Here we
will show the replacement steps involved when `B` depends on `A`.
In this first example, we simultaneously replace both `A` and `B`. Here `B` is
destroyed before `A`, then `A` is recreated before `B`.
![Replace All](./images/replace_all.png)
<!--
digraph replacement {
subgraph create {
rank=same;
a [label="A create"];
b [label="B create"];
a -> b [dir=back];
}
subgraph destroy {
rank=same;
a_d [label="A destroy"];
b_d [label="B destroy"];
a_d -> b_d;
}
a -> a_d;
a -> b_d [style=dotted];
b -> a_d [style=dotted];
b -> b_d;
}
-->
Order of operations:
1. `B` is destroyed
1. `A` is destroyed
1. `A` is created
1. `B` is created
This second example replaces only `A`, while updating `B`. Resource `B` is only
updated once `A` has been destroyed and recreated.
![Replace Dependency](./images/replace_one.png)
<!--
digraph replacement {
subgraph create {
rank=same;
a [label="A create"];
b [label="B update"];
a -> b [dir=back];
}
subgraph destroy {
rank=same;
a_d [label="A destroy"];
}
a -> a_d;
b -> a_d [style=dotted];
}
-->
Order of operations:
1. `A` is destroyed
1. `A` is created
1. `B` is updated
While the dependency edge from `B update` to `A destroy` isn't necessary in
these examples, it is shown here as an implementation detail which will be
mentioned later on.
A final example based on the replacement graph; starting with the above
configuration where `B` depends on `A`. The graph is reduced to an update of
`A` while only destroying `B`. The interesting feature here is the remaining
dependency of `A update` on `B destroy`. We can derive this ordering of
operations from the full replacement example above, by replacing `A create`
with `A update` and removing the unused nodes.
![Replace All](./images/destroy_then_update.png)
<!--
digraph destroy_then_update {
subgraph update {
rank=same;
a [label="A update"];
}
subgraph destroy {
rank=same;
b_d [label="B destroy"];
}
a -> b_d;
}
-->
## Create Before Destroy
Currently, the only user-controllable method for changing the ordering of
create and destroy operations is with the `create_before_destroy` resource
`lifecycle` attribute. This has the obvious effect of causing a resource to be
created before it is destroyed when replacement is required, but has a couple
of other effects we will detail here.
Taking the previous replacement examples, we can change the behavior of `A` to
be that of `create_before_destroy`.
![Replace all, dependency is create_before_destroy](./images/replace_all_cbd_dep.png)
<!--
digraph replacement {
subgraph create {
rank=same;
a [label="A create"];
b [label="B create"];
a -> b [dir=back];
}
subgraph destroy {
rank=same;
a_d [label="A destroy"];
b_d [label="B destroy"];
a_d -> b_d;
}
a -> a_d [dir=back];
a -> b_d;
b -> a_d [dir=back];
b -> b_d;
}
-->
Order of operations:
1. `B` is destroyed
2. `A` is created
1. `B` is created
1. `A` is destroyed
Note that in this first example, the creation of `B` is inserted in between the
creation of `A` and the destruction of `A`. This becomes more important in the
update example below.
![Replace dependency, dependency is create_before_destroy](./images/replace_dep_cbd_dep.png)
<!--
digraph replacement {
subgraph create {
rank=same;
a [label="A create"];
b [label="B update"];
a -> b [dir=back];
}
subgraph destroy {
rank=same;
a_d [label="A destroy"];
}
a -> a_d [dir=back, style=dotted];
b -> a_d [dir=back];
}
-->
Order of operations:
1. `A` is created
1. `B` is updated
1. `A` is destroyed
Here we can see clearly how `B` is updated after the creation of `A` and before
the destruction of the _deposed_ resource `A`. (The prior resource `A` is
sometimes referred to as "deposed" before it is destroyed, to disambiguate it
from the newly created `A`.) This ordering is important for resource that
"register" other resources, and require updating before the dependent resource
can be destroyed.
The transformation used to create these graphs is also where we use the extra
edges mentioned above connecting `B` to `A destroy`. The algorithm to change a
resource from the default ordering to `create_before_destroy` simply inverts
any incoming edges from other resources, which automatically creates the
necessary dependency ordering for dependent updates. This also ensures that
reduced versions of this example still adhere to the same ordering rules, such
as when the dependency is only being removed:
![Update a destroyed create_before_destroy dependency](./images/update_destroy_cbd.png)
<!--
digraph update {
subgraph create {
rank=same;
b [label="B update"];
}
subgraph destroy {
rank=same;
a_d [label="A destroy"];
}
b -> a_d [dir=back];
}
-->
Order of operations:
1. `B` is updated
1. `A` is destroyed
### Forced Create Before Destroy
In the previous examples, only resource `A` was being used as is it were
`create_before_destroy`. The minimal graphs used show that it works in
isolation, but that is only when the `create_before_destroy` resource has no
dependencies of it own. When a `create_before_resource` depends on another
resource, that dependency is "infected" by the `create_before_destroy`
lifecycle attribute.
This example demonstrates why forcing `create_before_destroy` is necessary. `B`
has `create_before_destroy` while `A` does not. If we only invert the ordering
for `B`, we can see that results in a cycle.
![Incorrect create_before_destroy replacement](./images/replace_cbd_incorrect.png)
<!--
digraph replacement {
subgraph create {
rank=same;
a [label="A create"];
b [label="B create"];
a -> b [dir=back];
}
subgraph destroy {
rank=same;
a_d [label="A destroy"];
b_d [label="B destroy"];
a_d -> b_d;
}
a -> a_d;
a -> b_d [style=dotted];
b -> a_d [style=dotted];
b -> b_d [dir=back];
}
-->
In order to resolve these cycles, all resources that precede a resource
with `create_before_destroy` must in turn be handled in the same manner.
Reversing the incoming edges to `A destroy` resolves the problem:
![Correct create_before_destroy replacement](./images/replace_all_cbd.png)
<!--
digraph replacement {
subgraph create {
rank=same;
a [label="A create"];
b [label="B create"];
a -> b [dir=back];
}
subgraph destroy {
rank=same;
a_d [label="A destroy"];
b_d [label="B destroy"];
a_d -> b_d;
}
a -> a_d [dir=back];
a -> b_d [dir=back, style=dotted];
b -> a_d [dir=back, style=dotted];
b -> b_d [dir=back];
}
-->
Order of operations:
1. `A` is created
1. `B` is created
1. `B` is destroyed
1. `A` is destroyed
This also demonstrates why `create_before_destroy` cannot be overridden when
it is inherited; changing the behavior here isn't possible without removing
the initial reason for `create_before_destroy`; otherwise cycles are always
introduced into the graph.

View File

@@ -0,0 +1,495 @@
# OpenTofu Diagnostics Guide
"Diagnostics" is the general term we use to describe the error and warning
messages that OpenTofu returns when there are problems with the configuration,
or when interactions with external systems fail.
This document is an overview of how we typically use diagnostics in OpenTofu.
It includes both some technical information about how we represent diagnostics
in code, and some more subjective information about the writing style we most
often use in diagnostic messages.
## Diagnostics in Code
Diagnostics are modelled using the types from
[the `tfdiags` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags).
In particular:
- [`tfdiags.Diagnostics`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics)
represents a set of zero or more diagnostics.
A total lack of diagnostics is usually represented by a `nil` value of this
type.
When constructing sets of diagnostics to return we typically don't worry
about the order they are returned in, even though we return them using a
slice type. The UI-layer code uses
[`tfdiags.Diagnostics.Sort`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.Sort)
to place all of the collected diagnostics into a predictable order before
rendering them, and so that function effectively turns the set of
diagnostics into an ordered list of diagnostics _just in time_.
- [`tfdiags.Diagnostic`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostic)
is an interface type that all diagnostic values implement.
In practice values of this type are often created automatically as an
implementation detail of [`Diagnostics.Append`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.Append),
which accepts various types that _don't_ directly implement
`Diagnostic` and then automatically wraps them in a type that does.
In particular:
- We often use [`hcl.Diagnostic`](https://pkg.go.dev/github.com/hashicorp/hcl/v2#Diagnostic)
to describe problems related to the configuration or operations that are
strongly related to parts of the configuration, because it is the most
fully-fledged type of diagnostic we allow including support for source
ranges and relevant expressions as described later.
It's also acceptable to append a whole `hcl.Diagnostics` (the HCL
equivalent of `tfdiags.Diagnostics`) in which case each diagnostic
will be wrapped and appended in turn. This is common when calling
HCL's own functions and passing on its diagnostics verbatim.
- Normal `error` values can be appended to a `tfdiags.Diagnostics`, but
that's mainly for historical reasons -- adapting code that was present
before the diagnostic models were added -- and should not be used in new
code because it typically results in low-quality diagnostics that don't
meet the style guidelines later in this document.
One exception is for "should never happen" cases: we sometimes use
`error` directly in that case to avoid overwhelming the surrounding
code with the construction of a full diagnostic.
Package `tfdiags` also includes some functions for constructing other kinds
of diagnostics, including:
- [`tfdiags.Sourceless`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Sourceless)
is good for diagnostics that don't relate to any part of the configuration,
such as when reporting incorrect usage of a command line argument.
- [`tfdiags.AttributeValue`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#AttributeValue) and
[`tfdiags.WholeContainingBody`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#WholeContainingBody)
produce special "contextual diagnostics" that must be transformed by
calling [`InConfigBody`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.InConfigBody)
on the resulting `Diagnostics` value. This is a special mechanism used
when the subsystem generating the diagnostic does not have direct access
to the configuration itself, such as when a provider returns a diagnostic
via the provider wire protocol.
- [`tfdiags.Severity`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Severity)
(and its HCL equivalent [`hcl.DiagnosticSeverity`](https://pkg.go.dev/github.com/hashicorp/hcl/v2#DiagnosticSeverity))
are how we distinguish between "error" and "warning" diagnostics.
The `tfdiags.Diagnostics.HasErrors` method returns true if the diagnostics
contains at least one with the severity `tfdiags.Error`.
The most common pattern for handling diagnostics in code is:
1. Declare `var diags tfdiags.Diagnostics` at the very start of a function.
2. During the function's body, whenever calling another function that might
produce its own diagnostics, capture them into a separate variable
(often called `moreDiags`, or `hclDiags` if the return type is
`hcl.Diagnostics`) and then immediately append them to the main `diags`
using `tfdiags.Diagnostics.Append`.
If subsequent code depends on the success of the call, check
`moreDiags.HasErrors()` (or similar) and return early if it returns `true`.
3. If the function generates any diagnostics of its own, append them directly
to `diags`.
4. At all exit points of the function, return `diags` regardless of whether
it has been assigned to or whether it contains errors. This ensures that
we always return any warnings that might have been produced and avoids
the risk of missing certain return paths under future maintenance if we
introduce additional diagnostics later.
Here's a code-example version of the above advice:
```go
func Example() (anything, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
somethingElse, moreDiags := otherFunction()
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
// NOTE: it isn't _always_ necessary to return immediately when there
// are errors, as long as the callee clearly documents what it
// guarantees about an errored result and the caller is able to
// work within those limitations. Collecting multiple errors to
// return together is often desirable.
//
// If the caller cannot continue at all though, or if continuing is
// likely to cause redundant errors that just restate the same problem
// in more confusing terms, then...
return nil, diags
}
if isProblematic(somethingElse) {
// A function might need to generate its own diagnostics if it detects
// a problem directly.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
// ...
})
return nil, diags
}
// ...
// The final return statement should include diags even if no errors
// were detected along the way, because it might contain warnings.
return something, diags
}
```
Some functions diverge from this pattern for special reasons, such as capturing
multiple sets of child function diagnostics and then using some logic to decide
which ones to append, or processing multiple items in a loop and appending
new diagnostics for each iteration. The above is just a general example of the
most common case, not a fixed template to follow in all cases.
## Information in a Diagnostic
The general model of `tfdiags.Diagnostic` has the following parts, though not
all implementations of the interface make use of all of them:
- Severity: either `tfdiags.Error` or `tfdiags.Warning`.
- Description: the main human-readable text describing the problem. This
has the following fields:
- Summary: A short, terse description of the general type of problem
that has occurred.
- Detail: A longer description of the problem, sometimes including multiple
paragraphs of information.
- Address: The address of some object that the error relates to, which
is most often a resource instance address.
OpenTofu does not currently have a localized UI, so built-in diagnostics
always have their summary and detail written in US English. There's more
subjective guidance about the content of these fields in sections below.
- Source location information: optional references to parts of the configuration
that the problem relates to. This has the following fields:
- Subject: source range for the part of the configuration that caused the
problem or that the problem is directly about.
- Context: optional source range of a larger section of configuration that
might make the cause of the problem easier to quickly understand if
included in the diagnostic message. The Context source range must always
contain the Subject source range within it.
The UI uses the context and subject together to display a source code
snippet. The lines of code included in the snippet cover both the context
and the subject, and then the subject itself is rendered with an underline
if we're rendering into a terminal that supports that style.
We don't use "context" very often, but it can be useful if the problem
we're describing is that just one part of a larger source element is
problematic. For example, if one of the operands to the `+` operator
isn't a number then that operand would be the "subject" but the entire
addition operation could be returned as "context", so that both of the
operands and the `+` symbol will definitely be included in the rendered
diagnostic too.
- Expression-related information: optional information about an expression whose
evaluation cause the problem. This has the following fields:
- Expression: The `hcl.Expression` representing the expression itself.
- EvalContext: The `hcl.EvalContext` that the expression was being evaluated
in.
The diagnostic renderer for the UI uses this information, when available,
to offer some extra hints about the values of any symbols that were used
in the expression, because it's often the dynamic values that cause a
problem, rather than the syntax used to obtain them.
- Extra info: this is a rather underspecified collection of assorted other
information that's only relevant in very specific contexts. Refer to the
`tfdiags` package documentation for more information.
There's _some_ guidance on this later in this document, but it's focused
only on a few main cases.
## Diagnostic Description Writing Style
Although there is some variation in diagnostic writing style, particularly in
parts of the system like state storage backends which were originally written by
third-parties, most of the _built-in_ diagnostics follow a relatively consistent
writing style that is in turn based on the writing style used by HCL itself in
its own diagnostics, because HCL and OpenTofu diagnostics often mix together
in the same set of problems.
The "summary" should typically be a very short and concise description of
what was wrong and what was wrong about it. Our summaries typically don't
include any user-chosen information such as symbol names, because that means a
particular kind of problem is always described using the same text and so
readers can become familiar enough with the summaries of problems they see
frequently to skip reading the rest of the diagnostic when skimming.
The following are some real examples of summaries currently used across both
HCL and OpenTofu:
- Unsupported operator
- Duplicate argument
- Invalid index
- Unexpected end of template
- Invalid template interpolation value
- Invalid default value for variable
- Required variable not set
- Invalid "count" attribute
The "detail" text is where we tend to put most of the information, and so
there's a lot more variation here but ideally a good diagnostic detail
should mention the following information, usually in the following order:
- What was wrong and what was wrong about it: similar to the summary but this
time including information about specifically what was wrong, such as the
name of the input variable whose default value was invalid.
- Why the situation is problematic, if knowing that relies on some
characteristic of OpenTofu's design that might not be obvious to a newcomer.
- What should be done to fix it, or (if it's unclear what the author's intention
was) a question-sentence that implies a _possible_ solution, often starting
with the words "Did you mean" and ending with a question mark.
While the summary message is often terse and uses only minimal punctuation,
the detail message should always be written in full sentences including
end-of-sentence punctuation (`.`, `?`). If "what was wrong about it" is
coming from the string representation of an `error` value, we typically
present it with a prefix ending with a colon and then append a period `.`
after the error string, and format the error itself using `tfdiags.FormatError`,
like this:
```go
Detail: fmt.Sprintf("Unsuitable value for thingy: %s.", tfdiags.FormatError(err))
```
If the second and third items in the above take more than a few words, it's
helpful to split them into their own paragraphs for easier scanning. When
writing multiple paragraphs in a detail message they should be separated by
`\n\n` -- two newline characters.
In many cases our diagnostics only include a subset of this information because
either the reason why it's problematic is relatively clear or because we don't
have any specific suggestion for how to solve the problem, but the following
is an example of a real diagnostic message from OpenTofu at the time of writing
this documentation which includes all of these parts:
```
Error: Invalid for_each argument
The "for_each" map includes keys derived from resource attributes that cannot
be determined until apply, and so OpenTofu cannot determine the full set of keys
that will identify the instances of this resource.
When working with unknown values in for_each, it's better to define the map keys
statically in your configuration and place apply-time results only in the map
values.
Alternatively, you could use the planning option -exclude=aws_instance.example
to first apply without this object, and then apply normally to converge.
```
The text immediately after "Error:" above is the summary for this diagnostic.
The paragraphs that follow are all a single "detail" string.
That was a particularly extreme diagnostic message with lots of information to
communicate. Most diagnostics are not so complicated; the following is an
example with less information to communicate:
```
Error: Invalid value for input variable
The given value is not suitable for var.example declared
at example.tf:12,1: a string is required.
```
This example also illustrates a situation where there are two different source
locations that could be relevant: the input variable's declaration or the
expression that's used to define its value. Because this message is talking
about a problem with the _value_, the diagnostic should have the source
"Subject" set to the expression that defined it, but it also mentions the
location of the declaration as part of the detail text as some additional
context.
Some other notes about some other specific situations that arise sometimes:
- If a diagnostic message includes a suggestion for a shell command to run
or a URL to visit for more information, use a paragraph that ends with a
colon, followed by a single newline, four spaces for indentation, and then the
command or URL:
```
To view the root module output values, run:
tofu output
```
The goal of this formatting is to make it very clear what part of the
message is intended to be copied and used elsewhere, by placing it on a
line of its own without any surrounding punctuation. The indented text
should ideally be formatted so that the user can copy it _verbatim_ into
whatever place it will be used.
The diagnostic renderer also has a special case where it will not try to
word-wrap a line that begins with spaces, and so this layout has the
useful side-effect of avoiding introducing extra newline characters into
a command line that is intended to be copied.
- There are some terminology choices we use to refer to some OpenTofu-specific
ideas and concepts that disagree slightly with terminology used in the code.
These differences are the result of learning from feedback from folks who
had been confused by the original terminology, even though the code still
often uses the original terminology:
- Instead of referring to "unknown values" or "computed values" we say that
values are "known after apply" or "cannot be determined until apply".
- In HCL the word "variable" means anything that's available to refer to
in the current evaluation context, which is confusing because OpenTofu
itself uses that word to refer only to input variables.
Sometimes messages are generated by HCL itself and so it's unavoidably
confusing, but when we're generating messages _inside OpenTofu_ we
use the two words "input variable" to refer to an input variable,
and "symbol" or "object" (depending on whether we're talking about
the name itself or what the name refers to) as the general word for
something you can refer to in an expression.
- For consistency with our use of "input variable" to distinguish from
HCL's more general meaning of "variable", we also tend to write
"local value" and "output value" when referring to those concepts, rather
than using the shorthands "locals" and "outputs".
- HCL distinguishes between "attributes" meaning the named keys inside an
object type, and "arguments" meaning the names used for individual
settings inside a configuration block.
OpenTofu itself uses those words a little more interchangeably because
in _many_ cases the configuration arguments in a block directly
correspond to the attributes of an object created by evaluating that
block.
However, if a particular error message is talking about a configuration
setting inside a block it's better to use "argument" rather than
"attribute" because that's then consistent with error messages that
HCL itself might generate.
Go uses the term "field" to describe an element of a struct type, and
JavaScript and JSON use the word "property" to describe an element of
an object type. We don't use either of those words in OpenTofu: the
elements of an object are its _attributes_, and the settings available
in a configuration block are its _arguments_. The string values that
identify elements of a map are called "keys".
- The `cty` terminology "marks" or "value marks" refers to an implementation
detail that should never be mentioned directly in an error message.
Instead, we use specific terminology related to what each mark type
is representing: "sensitive values", "ephemeral values", etc.
- `aws_instance` is an example of a "resource _type_", not of a "resource",
even though the provider protocol uses the single noun "resource" to refer
to both ideas.
A "resource" is what's declared by a `resource`, `data`, or `ephemeral`
block. A "resource _instance_" is what such a block can declare zero
or more of, when using the `count`, `for_each`, or `enabled` arguments.
- Although there are certainly some historical diagnostic messages that
predate this adjustment of terminology, new error messages should use
"managed resource" to refer to the kind of resource that's declared
using a `resource` block, "data resource" for `data` blocks, and
"ephemeral resource" for an `ephemeral` block.
In the code we refer to these three as "resource _modes_", but that is
internal terminology that should never appear in a diagnostic message.
- When a file or directory path appears as part of a diagnostic message, it
should typically be presented relative to the current working directory and
should use the syntax conventions of the platform where OpenTofu is running.
In particular, we return paths using backslashes as the separator when we
are running on Windows, but normal slashes otherwise. Using the Go
`filepath` package is a good way to get this right, though you might need
to add some complexity to your tests to make them pass on all platforms.
- If an error message is describing a "should never happen" case, we typically
end the detail string with the sentence "This is a bug in OpenTofu.". This
hopefully prompts the reader that this wasn't directly caused by something
they did, and so they should probably open a bug report in the
OpenTofu repository instead of just trying to solve it themselves.
For this kind of error message we often relax our preference against
mentioning implementation details in the error message, because the most
likely next step is for the user to copy-paste the entire message into their
bug report text and so the final reader of the message is OpenTofu
maintainers rather than OpenTofu users.
For example, it can be okay to use internal terminology like "cty marks" and
use the `GoString` representations of values in a "This is a bug in
OpenTofu" detail message, if that's the most concise way to capture the
information the OpenTofu maintainers would need to debug the problem.
## Diagnostics caused by unknown or sensitive values
When a diagnostic has expression information associated with it, the diagnostic
renderer for the UI includes some additional information about the values
that were in scope, like this:
```
var.greeting is "Hello"
var.items is list of string with 5 elements
```
By default, this renderer will not mention any symbol which refers to an unknown
or sensitive value. That was not historically true: originally, this could
say something like "var.example is a string, known only after apply".
Those who are less familiar with these concepts often misunderstood the
"known only after apply" part of the message as being _the problem itself_,
rather than just context to help diagnose the problem, and so the UI no longer
mentions "unknown-ness" or "sensitive-ness" in most cases.
However, there are some diagnostics messages that _are_ directly caused by the
presence of an unknown or sensitive value, in which case it's helpful to
mention that in the summary of values that were in scope.
To allow for this, we set the "extra info" field of a diagnostic to contain
an implementation of one of the following interfaces:
- [`tfdiags.DiagnosticExtraBecauseUnknown`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#DiagnosticExtraBecauseUnknown)
for a problem that's caused by an unknown value.
(Remember that the _text_ of the error message should refer to this as "known
only after apply", or similar.)
- [`tfdiags.DiagnosticExtraBecauseSensitive`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#DiagnosticExtraBecauseSensitive)
for situations where a sensitive value was used in a location that OpenTofu
cannot permit it, such as in the instance key of a resource instance.
These extra markers should be used only when mentioning the unknown or sensitive
values in the diagnostic message is likely to help with debugging a problem.
If the problem is not directly caused by unknown or sensitive values then
neither of these should be used, to avoid creating a distracting
[red herring](https://en.wikipedia.org/wiki/Red_herring) for the reader.
## Consolidation of Diagnostics
The UI layer has some special rules for finding sets of similar diagnostics
and showing them as just a single diagnostic referring to the first example
of a problem, with a short extra note about how many other similar diagnostics
there are.
```
(and 2 similar warnings elsewhere)
```
The main implementation of this behavior is in
[`tfdiags.Diagnostics.Consolidate`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.Consolidate),
but we allow end-users to customize (using command line options) whether this
consolidation applies to errors or warnings separately. By default, we
consolidate only warnings.
For a severity that is subject to consolidation, the main behavior is to group
together diagnostics that have the same "summary" text, and this is part of
why we tend to use terse, fixed strings in the summary field.
There are two extra mechanisms for customizing this behavior for specific
diagnostic messages:
- If the "extra info" of a diagnostic contains an implementation of
[`tfdiags.DiagnosticExtraDoNotConsolidate`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#DiagnosticExtraDoNotConsolidate)
then that diagnostic is not eligible for consolidation at all, regardless
of how similar it might be to other diagnostics in the same set.
- If the "extra info" of a diagnostic contains an implementation of
[`tfdiags.Keyable`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Keyable)
then the string returned by its `ExtraInfoKey` method is used _in addition to_
the summary text for deciding what to consolidate.
For example, if there were three warnings with the same summary text but
two of them have the same `ExtraInfoKey` and the third has a different
one then only the first two would be able to consolidate.
The `ExtraInfoKey` is an internal key used for comparison only and is never
exposed in the UI, so it can be set to whatever makes sense to define
separate consolidation groups for diagnostics with a specific summary.

View File

@@ -0,0 +1,78 @@
# OpenTofu glossary
This document is intended for anyone who wants to gain more knowledge about the terms and vocabulary used to talk about different concepts in OpenTofu.
> [!NOTE]
> This was created with the intent of gathering more knowledge over time.
> The state that you find this in right now could be incomplete.
> Once you discover and learn about a new concept that would benefit others,
> feel free to open a PR to update this.
> [!NOTE]
> When adding new content to this document, try to place it in such a way to match the alphabetical order of the already existing content.
>
> Optionally, also add a reference link if possible (GitHub conversation, issue, other docs, etc).
## OpenTofu
### Attribute/argument/field
* Attribute - a named key inside an object type.
* Argument - a name used for an individual setting inside a configuration block.
It is recommended to avoid using popular programming language terms such as "field" or "property" to describe an element of an object in OpenTofu.
Reference: [link](./diagnostics.md#diagnostic-description-writing-style)
### Data sources/data resource
* Data source - the remote thing that the data resource reads from.
* Data resource - refers to a block of type `data`, and the associated object it declares.
* Data resource type - is what is represented by the first label in a `data` block header, and the associated declarations and code for it in the provider plugin.
Reference: [link](https://github.com/opentofu/opentofu/pull/3389#discussion_r2440264786)
### Diagnostic
"Diagnostic" is the general term we use to describe the error or warning
message that OpenTofu returns when there are problems with the configuration,
or when interactions with external systems fail.
Reference: [link](./diagnostics.md)
### Mark/value mark
OpenTofu uses cty.Value to represent the result of expressions (and other data).
Occasionally, we will want to annotate that data with additional properties, without actually modifying the underlying value.
Marks are used for that purpose and are the preferred method of doing so with go-cty.
### Resource/resource instance/resource type
* Resource - is what is declared by a `resource`, `data` or `ephemeral` block.
* Resource instance - is what such a block can declare zero or more of, when using the `count`, `for_each`, or `enabled` arguments.
* Resource type - the type of a "resource". E.g., `aws_instance` is a "resource type".
It is recommended to use the following terms when discussing about "resource" blocks:
* managed resource - a block declared as `resource "type" "name" {}`
* data resource - a block declared as `data "type" "name" {}`
* ephemeral resource - a block declared as `ephemeral "type" "name" {}`
Reference: [link](./diagnostics.md#diagnostic-description-writing-style)
### Unknown value/Computed value
* Unknown value - Unknown values are the result of expressions that have unknown inputs. E.g.: a value that will not be known until a resource is created.
Right now the main source of this type of values is resources, but we are considering adding others like unknown inputs.
Another place where an unknown value can be encountered is from using some of the built-in functions like `timestamp`, `bcrypt` and `uuid`.
* Computed value - Computed is more of a resource specific concept that a provider can specify in its resource schema.
When set to true, the provider does not expect a value and may instead produce one that may or may not be unknown.
With other flags, the actual functionality is a bit more subtle.
References: [link](https://github.com/opentofu/opentofu/blob/490762343322eff42c0586f7a4c267b579fe80ef/internal/configs/configschema/schema.go#L65), [link](https://github.com/opentofu/opentofu/blob/490762343322eff42c0586f7a4c267b579fe80ef/internal/lang/functions.go#L22)
## HCL
### Evaluation context (HCL)
A set of already known functions, input values, local values, resources, etc. that is used to evaluate an expression that can reference any of the concepts listed above.
The list of concepts above, in the context of HCL evaluation, are called [variables](#variable-hcl).
### Expression
An expression is any right hand side of an assignment that will be evaluated to generate the value that will be associated with key on the left hand side of the assignment.
The simplest expressions are just literal values, like "hello" or 5, but the OpenTofu language also allows more complex
expressions such as references to data exported by resources, arithmetic, conditional evaluation, and a number of built-in and provider-defined functions.
Reference: [link](https://opentofu.org/docs/language/expressions/)
### Variable (HCL)
Anything that's available to refer to in the current evaluation context.

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,294 @@
# Planning Behaviors
A key design tenet for OpenTofu is that any actions with externally-visible
side-effects should be carried out via the standard process of creating a
plan and then applying it. Any new features should typically fit within this
model.
There are also some historical exceptions to this rule, which we hope to
supplement with plan-and-apply-based equivalents over time.
This document describes the default planning behavior of OpenTofu in the
absence of any special instructions, and also describes the three main
design approaches we can choose from when modelling non-default behaviors that
require additional information from outside OpenTofu Core.
This document focuses primarily on actions relating to _resource instances_,
because that is OpenTofu's main concern. However, these design principles can
potentially generalize to other externally-visible objects, if we can describe
their behaviors in a way comparable to the resource instance behaviors.
This is developer-oriented documentation rather than user-oriented
documentation. See
[the main OpenTofu documentation](https://opentofu.org/docs) for
information on existing planning behaviors and other behaviors as viewed from
an end-user perspective.
## Default Planning Behavior
When given no explicit information to the contrary, OpenTofu Core will
automatically propose taking the following actions in the appropriate
situations:
- **Create**, if either of the following are true:
- There is a `resource` block in the configuration that has no corresponding
managed resource in the prior state.
- There is a `resource` block in the configuration that is recorded in the
prior state but whose `count` or `for_each` argument (or lack thereof)
describes an instance key that is not tracked in the prior state.
- **Delete**, if either of the following are true:
- There is a managed resource tracked in the prior state which has no
corresponding `resource` block in the configuration.
- There is a managed resource tracked in the prior state which has a
corresponding `resource` block in the configuration _but_ its `count`
or `for_each` argument (or lack thereof) lacks an instance key that is
tracked in the prior state.
- **Update**, if there is a corresponding resource instance both declared in the
configuration (in a `resource` block) and recorded in the prior state
(unless it's marked as "tainted") but there are differences between the prior
state and the configuration which the corresponding provider doesn't
explicitly classify as just being normalization.
- **Replace**, if there is a corresponding resource instance both declared in
the configuration (in a `resource` block) and recorded in the prior state
_marked as "tainted"_. The special "tainted" status means that the process
of creating the object failed partway through and so the existing object does
not necessarily match the configuration, so OpenTofu plans to replace it
in order to ensure that the resulting object is complete.
- **Read**, if there is a `data` block in the configuration.
- If possible, OpenTofu will eagerly perform this action during the planning
phase, rather than waiting until the apply phase.
- If the configuration contains at least one unknown value, or if the
data resource directly depends on a managed resource that has any change
proposed elsewhere in the plan, OpenTofu will instead delay this action
to the apply phase so that it can react to the completion of modification
actions on other objects.
- **No-op**, to explicitly represent that OpenTofu considered a particular
resource instance but concluded that no action was required.
The **Replace** action described above is really a sort of "meta-action", which
OpenTofu expands into separate **Create** and **Delete** operations. There are
two possible orderings, and the first one is the default planning behavior
unless overridden by a special planning behavior as described later. The
two possible lowerings of **Replace** are:
1. **Delete** then **Create**: first delete the existing object bound to an
instance, and then create a new object at the same address based on the
current configuration.
2. **Create** then **Delete**: mark the existing object bound to an instance as
"deposed" (still exists but not current), create a new current object at the
same address based on the current configuration, and then delete the deposed
object.
## Special Planning Behaviors
For the sake of this document, a "special" planning behavior is one where
OpenTofu Core will select a different action than the defaults above,
based on explicit instructions given either by a module author, an operator,
or a provider.
There are broadly three different design patterns for special planning
behaviors, and so each "special" use-case will typically be met by one or more
of the following depending on which stakeholder is activating the behavior:
- [Configuration-driven Behaviors](#configuration-driven-behaviors) are
activated by additional annotations given in the source code of a module.
This design pattern is good for situations where the behavior relates to
a particular module and so should be activated for anyone using that
module. These behaviors are therefore specified by the module author, such
that any caller of the module will automatically benefit with no additional
work.
- [Provider-driven Behaviors](#provider-driven-behaviors) are activated by
optional fields in a provider's response when asked to help plan one of the
default actions given above.
This design pattern is good for situations where the behavior relates to
the behavior of the remote system that a provider is wrapping, and so from
the perspective of a user of the provider the behavior should appear
"automatic".
Because these special behaviors are activated by values in the provider's
response to the planning request from OpenTofu Core, behaviors of this
sort will typically represent "tweaks" to or variants of the default
planning behaviors, rather than entirely different behaviors.
- [Single-run Behaviors](#single-run-behaviors) are activated by explicitly
setting additional "plan options" when calling OpenTofu Core's plan
operation.
This design pattern is good for situations where the direct operator of
OpenTofu needs to do something exceptional or one-off, such as when the
configuration is correct but the real system has become degraded or damaged
in a way that OpenTofu cannot automatically understand.
However, this design pattern has the disadvantage that each new single-run
behavior type requires custom work in every wrapping UI or automaton around
OpenTofu Core, in order provide the user of that wrapper some way
to directly activate the special option, or to offer an "escape hatch" to
use OpenTofu CLI directly and bypass the wrapping automation for a
particular change.
We've also encountered use-cases that seem to call for a hybrid between these
different patterns. For example, a configuration construct might cause OpenTofu
Core to _invite_ a provider to activate a special behavior, but let the
provider make the final call about whether to do it. Or conversely, a provider
might advertise the possibility of a special behavior but require the user to
specify something in the configuration to activate it. The above are just
broad categories to help us think through potential designs; some problems
will require more creative combinations of these patterns than others.
### Configuration-driven Behaviors
Within the space of configuration-driven behaviors, we've encountered two
main sub-categories:
- Resource-specific behaviors, whose effect is scoped to a particular resource.
The configuration for these often lives inside the `resource` or `data`
block that declares the resource.
- Global behaviors, whose effect can span across more than one resource and
sometimes between resources in different modules. The configuration for
these often lives in a separate location in a module, such as a separate
top-level block which refers to other resources using the typical address
syntax.
The following is a non-exhaustive list of existing examples of
configuration-driven behaviors, selected to illustrate some different variations
that might be useful inspiration for new designs:
- The `ignore_changes` argument inside `resource` block `lifecycle` blocks
tells OpenTofu that if there is an existing object bound to a particular
resource instance address then OpenTofu should ignore the configured value
for a particular argument and use the corresponding value from the prior
state instead.
This can therefore potentially cause what would've been an **Update** to be
a **No-op** instead.
- The `replace_triggered_by` argument inside `resource` block `lifecycle`
blocks can use a proposed change elsewhere in a module to force OpenTofu
to propose one of the two **Replace** variants for a particular resource.
- The `create_before_destroy` argument inside `resource` block `lifecycle`
blocks only takes effect if a particular resource instance has a proposed
**Replace** action. If not set or set to `false`, OpenTofu will decompose
it to **Destroy** then **Create**, but if set to `true` OpenTofu will use
the inverted ordering.
Because OpenTofu Core will never select a **Replace** action automatically
by itself, this is an example of a hybrid design where the config-driven
`create_before_destroy` combines with any other behavior (config-driven or
otherwise) that might cause **Replace** to customize exactly what that
**Replace** will mean.
- Top-level `moved` blocks in a module activate a special behavior during the
planning phase, where OpenTofu will first try to change the bindings of
existing objects in the prior state to attach to new addresses before running
the normal planning process. This therefore allows a module author to
document certain kinds of refactoring so that OpenTofu can update the
state automatically once users upgrade to a new version of the module.
This special behavior is interesting because it doesn't _directly_ change
what actions OpenTofu will propose, but instead it adds an extra
preparation step before the typical planning process which changes the
addresses that the planning process will consider. It can therefore
_indirectly_ cause different proposed actions for affected resource
instances, such as transforming what by default might've been a **Delete**
of one instance and a **Create** of another into just a **No-op** or
**Update** of the second instance.
This one is an example of a "global behavior", because at minimum it
affects two resource instance addresses and, if working with whole resource
or whole module addresses, can potentially affect a large number of resource
instances all at once.
### Provider-driven Behaviors
Providers get an opportunity to activate some special behaviors for a particular
resource instance when they respond to the `PlanResourceChange` function of
the provider plugin protocol.
When OpenTofu Core executes this RPC, it has already selected between
**Create**, **Delete**, or **Update** actions for the particular resource
instance, and so the special behaviors a provider may activate will typically
serve as modifiers or tweaks to that base action, and will not allow
the provider to select another base action altogether. The provider wire
protocol does not talk about the action types explicitly, and instead only
implies them via other content of the request and response, with OpenTofu Core
making the final decision about how to react to that information.
The following is a non-exhaustive list of existing examples of
provider-driven behaviors, selected to illustrate some different variations
that might be useful inspiration for new designs:
- When the base action is **Update**, a provider may optionally return one or
more paths to attributes which have changes that the provider cannot
implement as an in-place update due to limitations of the remote system.
In that case, OpenTofu Core will replace the **Update** action with one of
the two **Replace** variants, which means that from the provider's
perspective the apply phase will really be two separate calls for the
decomposed **Create** and **Delete** actions (in either order), rather
than **Update** directly.
- When the base action is **Update**, a provider may optionally return a
proposed new object where one or more of the arguments has its value set
to what was in the prior state rather than what was set in the configuration.
This represents any situation where a remote system supports multiple
different serializations of the same value that are all equivalent, and
so changing from one to another doesn't represent a real change in the
remote system.
If all of those taken together causes the new object to match the prior
state, OpenTofu Core will treat the update as a **No-op** instead.
Of the three genres of special behaviors, provider-driven behaviors is the one
we've made the least use of historically but one that seems to have a lot of
opportunities for future exploration. Provider-driven behaviors can often be
ideal because their effects appear as if they are built in to OpenTofu so
that "it just works", with OpenTofu automatically deciding and explaining what
needs to happen and why, without any special effort on the user's part.
### Single-run Behaviors
OpenTofu Core's "plan" operation takes a set of arguments that we collectively
call "plan options", that can modify OpenTofu's planning behavior on a per-run
basis without any configuration changes or special provider behaviors.
As noted above, this particular genre of designs is the most burdensome to
implement because any wrapping software that can ask OpenTofu Core to create
a plan must ideally offer some way to set all of the available planning options,
or else some part of OpenTofu's functionality won't be available to anyone
using that wrapper.
However, we've seen various situations where single-run behaviors really are the
most appropriate way to handle a particular use-case, because the need for the
behavior originates in some process happening outside of the scope of any
particular OpenTofu module or provider.
The following is a non-exhaustive list of existing examples of
single-run behaviors, selected to illustrate some different variations
that might be useful inspiration for new designs:
- The "replace" planning option specifies zero or more resource instance
addresses.
For any resource instance specified, OpenTofu Core will transform any
**Update** or **No-op** action for that instance into one of the
**Replace** actions, thereby allowing an operator to respond to something
having become degraded in a way that OpenTofu and providers cannot
automatically detect and force OpenTofu to replace that object with
a new one that will hopefully function correctly.
- The "refresh only" planning mode ("planning mode" is a single planning option
that selects between a few mutually-exclusive behaviors) forces OpenTofu
to treat every resource instance as **No-op**, regardless of what is bound
to that address in state or present in the configuration.
## Legacy Operations
Some of the legacy operations OpenTofu CLI offers that _aren't_ integrated
with the plan and apply flow could be thought of as various degenerate kinds
of single-run behaviors. Most don't offer any opportunity to preview an effect
before applying it, but do meet a similar set of use-cases where an operator
needs to take some action to respond to changes to the context OpenTofu is
in rather than to the OpenTofu configuration itself.
Most of these legacy operations could therefore most readily be translated to
single-run behaviors, but before doing so it's worth researching whether people
are using them as a workaround for missing configuration-driven and/or
provider-driven behaviors. A particular legacy operation might be better
replaced with a different sort of special behavior, or potentially by multiple
different special behaviors of different genres if it's currently serving as
a workaround for many different unmet needs.

View File

@@ -0,0 +1,213 @@
# OpenTofu Plugin Protocol
This directory contains documentation about the physical wire protocol that
OpenTofu Core uses to communicate with provider plugins.
Most providers are not written directly against this protocol. Instead, prefer
to use an SDK that implements this protocol and write the provider against
the SDK's API.
----
**If you want to write a plugin for OpenTofu, please refer to
[Extending OpenTofu](https://github.com/hashicorp/terraform-docs-common) instead.**
This documentation is for those who are developing _OpenTofu SDKs_, rather
than those implementing plugins.
----
From OpenTofu v0.12.0 onwards, OpenTofu's plugin protocol is built on
[gRPC](https://grpc.io/). This directory contains `.proto` definitions of
different versions of OpenTofu's protocol.
Only `.proto` files published as part of OpenTofu release tags are actually
official protocol versions. If you are reading this directory on the `main`
branch or any other development branch then it may contain protocol definitions
that are not yet finalized and that may change before final release.
## RPC Plugin Model
OpenTofu plugins are normal executable programs that, when launched, expose
gRPC services on a server accessed via the loopback interface. OpenTofu Core
discovers and launches plugins, waits for a handshake to be printed on the
plugin's `stdout`, and then connects to the indicated port number as a
gRPC client.
For this reason, we commonly refer to OpenTofu Core itself as the plugin
"client" and the plugin program itself as the plugin "server". Both of these
processes run locally, with the server process appearing as a child process
of the client. OpenTofu Core controls the lifecycle of these server processes
and will terminate them when they are no longer required.
The startup and handshake protocol is not currently documented. We hope to
document it here or to link to external documentation on it in future.
## Versioning Strategy
The Plugin Protocol uses a versioning strategy that aims to allow gradual
enhancements to the protocol while retaining compatibility, but also to allow
more significant breaking changes from time to time while allowing old and
new plugins to be used together for some period.
The versioning strategy described below was introduced with protocol version
5.0 in OpenTofu v0.12. Prior versions of OpenTofu and prior protocol versions
do not follow this strategy.
The authoritative definition for each protocol version is in this directory
as a Protocol Buffers (protobuf) service definition. The files follow the
naming pattern `tfpluginX.Y.proto`, where X is the major version and Y
is the minor version.
### Major and minor versioning
The minor version increases for each change introducing optional new
functionality that can be ignored by implementations of prior versions. For
example, if a new field were added to an response message, it could be a minor
release as long as OpenTofu Core can provide some default behavior when that
field is not populated.
The major version increases for any significant change to the protocol where
compatibility is broken. However, OpenTofu Core and an SDK may both choose
to support multiple major versions at once: the plugin handshake includes a
negotiation step where client and server can work together to select a
mutually-supported major version.
The major version number is encoded into the protobuf package name: major
version 5 uses the package name `tfplugin5`, and one day major version 6
will switch to `tfplugin6`. This change of name allows a plugin server to
implement multiple major versions at once, by exporting multiple gRPC services.
Minor version differences rely instead on feature-detection mechanisms, so they
are not represented directly on the wire and exist primarily as a human
communication tool to help us easily talk about which software supports which
features.
## Version compatibility for Core, SDK, and Providers
A particular version of OpenTofu Core has both a minimum minor version it
requires and a maximum major version that it supports. A particular version of
OpenTofu Core may also be able to optionally use a newer minor version when
available, but fall back on older behavior when that functionality is not
available.
Likewise, each provider plugin release is compatible with a set of versions.
The compatible versions for a provider are a list of major and minor version
pairs, such as "4.0", "5.2", which indicates that the provider supports the
baseline features of major version 4 and supports major version 5 including
the enhancements from both minor versions 1 and 2. This provider would
therefore be compatible with a OpenTofu Core release that supports only
protocol version 5.0, since major version 5 is supported and the optional
5.1 and 5.2 enhancements will be ignored.
If OpenTofu Core and the plugin do not have at least one mutually-supported
major version, OpenTofu Core will return an error from `tofu init`
during plugin installation:
```
Provider "aws" v1.0.0 is not compatible with OpenTofu v0.12.0.
Provider version v2.0.0 is the earliest compatible version.
Select it with the following version constraint:
version = "~> 2.0.0"
```
```
Provider "aws" v3.0.0 is not compatible with OpenTofu v0.12.0.
Provider version v2.34.0 is the latest compatible version. Select
it with the following constraint:
version = "~> 2.34.0"
Alternatively, upgrade to the latest version of OpenTofu for compatibility with newer provider releases.
```
The above messages are for plugins installed via `tofu init` from a
OpenTofu registry, where the registry API allows OpenTofu Core to recognize
the protocol compatibility for each provider release. For plugins that are
installed manually to a local plugin directory, OpenTofu Core has no way to
suggest specific versions to upgrade or downgrade to, and so the error message
is more generic:
```
The installed version of provider "example" is not compatible with OpenTofu v0.12.0.
This provider was loaded from:
/usr/local/bin/terraform-provider-example_v0.1.0
```
## Adding/removing major version support in SDK and Providers
The set of supported major versions is decided by the SDK used by the plugin.
Over time, SDKs will add support for new major versions and phase out support
for older major versions.
In doing so, the SDK developer passes those capabilities and constraints on to
any provider using their SDK, and that will in turn affect the compatibility
of the plugin in ways that affect its semver-based version numbering:
- If an SDK upgrade adds support for a new provider protocol, that will usually
be considered a new feature and thus warrant a new minor version.
- If an SDK upgrade removes support for an old provider protocol, that is
always a breaking change and thus requires a major release of the provider.
For this reason, SDK developers must be clear in their release notes about
the addition and removal of support for major versions.
OpenTofu Core also makes an assumption about major version support when
it produces actionable error messages for users about incompatibilities:
a particular protocol major version is supported for a single consecutive
range of provider releases, with no "gaps".
## Using the protobuf specifications in an SDK
If you wish to build an SDK for OpenTofu plugins, an early step will be to
copy one or more `.proto` files from this directory into your own repository
(depending on which protocol versions you intend to support) and use the
`protoc` protocol buffers compiler (with gRPC extensions) to generate suitable
RPC stubs and types for your target language.
For example, if you happen to be targeting Python, you might generate the
stubs using a command like this:
```
protoc --python_out=. --grpc_python_out=. tfplugin5.1.proto
```
You can find out more about the tool usage for each target language in
[the gRPC Quick Start guides](https://grpc.io/docs/quickstart/).
The protobuf specification for a version is immutable after it has been
included in at least one OpenTofu release. Any changes will be documented in
a new `.proto` file establishing a new protocol version.
The protocol buffer compiler will produce some sort of library object appropriate
for the target language, which depending on the language might be called a
module, or a package, or something else. We recommend to include the protocol
major version in your module or package name so that you can potentially
support multiple versions concurrently in future. For example, if you are
targeting major version 5 you might call your package or module `tfplugin5`.
To upgrade to a newer minor protocol version, copy the new `.proto` file
from this directory into the same location as your previous version, delete
the previous version, and then run the protocol buffers compiler again
against the new `.proto` file. Because minor releases are backward-compatible,
you can simply update your previous stubs in-place rather than creating a
new set alongside.
To support a new _major_ protocol version, create a new package or module
and copy the relevant `.proto` file into it, creating a separate set of stubs
that can in principle allow your SDK to support both major versions at the
same time. We recommend supporting both the previous and current major versions
together for a while across a major version upgrade so that users can avoid
having to upgrade both OpenTofu Core and all of their providers at the same
time, but you can delete the previous major version stubs once you remove
support for that version.
**Note:** Some of the `.proto` files contain statements about being updated
in-place for minor versions. This reflects an earlier version management
strategy which is no longer followed. The current process is to create a
new file in this directory for each new minor version and consider all
previously-tagged definitions as immutable. The outdated comments in those
files are retained in order to keep the promise of immutability, even though
it is now incorrect.

View File

@@ -0,0 +1,267 @@
# Wire Format for OpenTofu Objects and Associated Values
The provider wire protocol (as of major version 5) includes a protobuf message
type `DynamicValue` which OpenTofu uses to represent values from the OpenTofu
Language type system, which result from evaluating the content of `resource`,
`data`, and `provider` blocks, based on a schema defined by the corresponding
provider.
Because the structure of these values is determined at runtime, `DynamicValue`
uses one of two possible dynamic serialization formats for the values
themselves: MessagePack or JSON. OpenTofu most commonly uses MessagePack,
because it offers a compact binary representation of a value. However, a server
implementation of the provider protocol should fall back to JSON if the
MessagePack field is not populated, in order to support both formats.
The remainder of this document describes how OpenTofu translates from its own
type system into the type system of the two supported serialization formats.
A server implementation of the OpenTofu provider protocol can use this
information to decode `DynamicValue` values from incoming messages into
whatever representation is convenient for the provider implementation.
A server implementation must also be able to _produce_ `DynamicValue` messages
as part of various response messages. When doing so, servers should always
use MessagePack encoding, because OpenTofu does not consistently support
JSON responses across all request types and all OpenTofu versions.
Both the MessagePack and JSON serializations are driven by information the
provider previously returned in a `Schema` message. OpenTofu will encode each
value depending on the type constraint given for it in the corresponding schema,
using the closest possible MessagePack or JSON type to the OpenTofu language
type. Therefore a server implementation can decode a serialized value using a
standard MessagePack or JSON library and assume it will conform to the
serialization rules described below.
## MessagePack Serialization Rules
The MessagePack types referenced in this section are those defined in
[The MessagePack type system specification](https://github.com/msgpack/msgpack/blob/master/spec.md#type-system).
Note that MessagePack defines several possible serialization formats for each
type, and OpenTofu may choose any of the formats of a specified type.
The exact serialization chosen for a given value may vary between OpenTofu
versions, but the types given here are contractual.
Conversely, server implementations that are _producing_ MessagePack-encoded
values are free to use any of the valid serialization formats for a particular
type. However, we recommend choosing the most compact format that can represent
the value without a loss of range.
### `Schema.Block` Mapping Rules for MessagePack
To represent the content of a block as MessagePack, OpenTofu constructs a
MessagePack map that contains one key-value pair per attribute and one
key-value pair per distinct nested block described in the `Schema.Block` message.
The key-value pairs representing attributes have values based on
[the `Schema.Attribute` mapping rules](#Schema.Attribute-mapping-rules-for-messagepack).
The key-value pairs representing nested block types have values based on
[the `Schema.NestedBlock` mapping rules](#Schema.NestedBlock-mapping-rules-for-messagepack).
### `Schema.Attribute` Mapping Rules for MessagePack
The MessagePack serialization of an attribute value depends on the value of the
`type` field of the corresponding `Schema.Attribute` message. The `type` field is
a compact JSON serialization of a
[OpenTofu type constraint](https://opentofu.org/docs/language/expressions/type-constraints/),
which consists either of a single
string value (for primitive types) or a two-element array giving a type kind
and a type argument.
The following table describes the type-specific mapping rules. Along with those
type-specific rules there are two special rules that override the mappings
in the table below, regardless of type:
* A null value is represented as a MessagePack nil value.
* An unknown value (that is, a placeholder for a value that will be decided
only during the apply operation) is represented as a
[MessagePack extension](https://github.com/msgpack/msgpack/blob/master/spec.md#extension-types)
value, described in more detail below.
| `type` Pattern | MessagePack Representation |
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `"string"` | A MessagePack string containing the Unicode characters from the string value serialized as normalized UTF-8. |
| `"number"` | Either MessagePack integer, MessagePack float, or MessagePack string representing the number. If a number is represented as a string then the string contains a decimal representation of the number which may have a larger mantissa than can be represented by a 64-bit float. |
| `"bool"` | A MessagePack boolean value corresponding to the value. |
| `["list",T]` | A MessagePack array with the same number of elements as the list value, each of which is represented by the result of applying these same mapping rules to the nested type `T`. |
| `["set",T]` | Identical in representation to `["list",T]`, but the order of elements is undefined because OpenTofu sets are unordered. |
| `["map",T]` | A MessagePack map with one key-value pair per element of the map value, where the element key is serialized as the map key (always a MessagePack string) and the element value is represented by a value constructed by applying these same mapping rules to the nested type `T`. |
| `["object",ATTRS]` | A MessagePack map with one key-value pair per attribute defined in the `ATTRS` object. The attribute name is serialized as the map key (always a MessagePack string) and the attribute value is represented by a value constructed by applying these same mapping rules to each attribute's own type. |
| `["tuple",TYPES]` | A MessagePack array with one element per element described by the `TYPES` array. The element values are constructed by applying these same mapping rules to the corresponding element of `TYPES`. |
| `"dynamic"` | A MessagePack array with exactly two elements. The first element is a MessagePack binary value containing a JSON-serialized type constraint in the same format described in this table. The second element is the result of applying these same mapping rules to the value with the type given in the first element. This special type constraint represents values whose types will be decided only at runtime. |
Unknown values have two possible representations, both using
[MessagePack extension](https://github.com/msgpack/msgpack/blob/master/spec.md#extension-types)
values.
The older encoding is for unrefined unknown values and uses an extension
code of zero, with the extension value payload completely ignored.
Newer OpenTofu versions can produce "refined" unknown values which carry some
additional information that constrains the possible range of the final value/
Refined unknown values have extension code 12 and then the extension object's
payload is a MessagePack-encoded map using integer keys to represent different
kinds of refinement:
* `1` represents "nullness", and the value of that key will be a boolean
value that is true if the value is definitely null or false if it is
definitely not null. If this key isn't present at all then the value may or
may not be null. It's not actually useful to encode that an unknown value
is null; use a known null value instead in that case, because there is only
one null value of each type.
* `2` represents string prefix, and the value is a string that the final
value is known to begin with. This is valid only for unknown values of string
type.
* `3` and `4` represent the lower and upper bounds respectively of a number
value, and the value of both is a two-element msgpack array whose
first element is a valid encoding of a number (as in the table above)
and whose second element is a boolean value that is true for an inclusive
bound and false for an exclusive bound. This is valid only for unknown values
of number type.
* `5` and `6` represent the lower and upper bounds respectively of the length
of a collection value. The value of both is an integer representing an
inclusive bound. This is valid only for unknown values of the three kinds of
collection types: list, set, and map.
Unknown value refinements are an optional way to reduce the range of possible
values for situations where that makes it possible to produce a known result
for unknown inputs or where it allows detecting an error during the planning
phase that would otherwise be detected only during the apply phase. It's always
safe to ignore refinements and just treat an unknown value as wholly unknown,
but considering refinements may allow a more precise answer. A provider that
produces refined values in its planned new state (from `PlanResourceChange`)
_must_ honor those refinements in the final state (from `ApplyResourceChange`).
Unmarshalling code should ignore refinement map keys that they don't know about,
because future versions of the protocol might define additional refinements.
When encoding an unknown value without any refinements, always use the older
format with extension code zero instead of using extension code 12 with an
empty refinement map. Any refined unknown value _must_ have at least one
refinement map entry. This rule ensures backward compatibility with older
implementations that predate the value refinements concept.
A server implementation of the protocol should treat _any_ MessagePack extension
code as representing an unknown value, but should ignore the payload of that
extension value entirely unless the extension code is 12 to indicate that
the body represents refinements. Future versions of this protocol may define
specific formats for other extension codes, but they will always represent
unknown values.
### `Schema.NestedBlock` Mapping Rules for MessagePack
The MessagePack serialization of a collection of blocks of a particular type
depends on the `nesting` field of the corresponding `Schema.NestedBlock` message.
The `nesting` field is a value from the `Schema.NestingBlock.NestingMode`
enumeration.
All `nesting` values cause the individual blocks of a type to be represented
by applying
[the `Schema.Block` mapping rules](#Schema.Block-mapping-rules-for-messagepack)
to the block's contents based on the `block` field, producing what we'll call
a _block value_ in the table below.
The `nesting` value then in turn defines how OpenTofu will collect all of the
individual block values together to produce a single property value representing
the nested block type. For all `nesting` values other than `MAP`, blocks may
not have any labels. For the `nesting` value `MAP`, blocks must have exactly
one label, which is a string we'll call a _block label_ in the table below.
| `nesting` Value | MessagePack Representation |
|---|---|
| `SINGLE` | The block value of the single block of this type, or nil if there is no block of that type. |
| `LIST` | A MessagePack array of all of the block values, preserving the order of definition of the blocks in the configuration. |
| `SET` | A MessagePack array of all of the block values in no particular order. |
| `MAP` | A MessagePack map with one key-value pair per block value, where the key is the block label and the value is the block value. |
| `GROUP` | The same as with `SINGLE`, except that if there is no block of that type OpenTofu will synthesize a block value by pretending that all of the declared attributes are null and that there are zero blocks of each declared block type. |
For the `LIST` and `SET` nesting modes, OpenTofu guarantees that the
MessagePack array will have a number of elements between the `min_items` and
`max_items` values given in the schema, _unless_ any of the block values contain
nested unknown values. When unknown values are present, OpenTofu considers
the value to be potentially incomplete and so OpenTofu defers validation of
the number of blocks. For example, if the configuration includes a `dynamic`
block whose `for_each` argument is unknown then the final number of blocks is
not predictable until the apply phase.
## JSON Serialization Rules
The JSON serialization is a secondary representation for `DynamicValue`, with
MessagePack preferred due to its ability to represent unknown values via an
extension.
The JSON encoding described in this section is also used for the `json` field
of the `RawValue` message that forms part of an `UpgradeResourceState` request.
However, in that case the data is serialized per the schema of the provider
version that created it, which won't necessarily match the schema of the
_current_ version of that provider.
### `Schema.Block` Mapping Rules for JSON
To represent the content of a block as JSON, OpenTofu constructs a
JSON object that contains one property per attribute and one property per
distinct nested block described in the `Schema.Block` message.
The properties representing attributes have property values based on
[the `Schema.Attribute` mapping rules](#Schema.Attribute-mapping-rules-for-json).
The properties representing nested block types have property values based on
[the `Schema.NestedBlock` mapping rules](#Schema.NestedBlock-mapping-rules-for-json).
### `Schema.Attribute` Mapping Rules for JSON
The JSON serialization of an attribute value depends on the value of the `type`
field of the corresponding `Schema.Attribute` message. The `type` field is
a compact JSON serialization of a
[OpenTofu type constraint](https://opentofu.org/docs/language/expressions/type-constraints/),
which consists either of a single
string value (for primitive types) or a two-element array giving a type kind
and a type argument.
The following table describes the type-specific mapping rules. Along with those
type-specific rules there is one special rule that overrides the rules in the
table regardless of type:
* A null value is always represented as JSON `null`.
| `type` Pattern | JSON Representation |
|---|---|
| `"string"` | A JSON string containing the Unicode characters from the string value. |
| `"number"` | A JSON number representing the number value. OpenTofu numbers are arbitrary-precision floating point, so the value may have a larger mantissa than can be represented by a 64-bit float. |
| `"bool"` | Either JSON `true` or JSON `false`, depending on the boolean value. |
| `["list",T]` | A JSON array with the same number of elements as the list value, each of which is represented by the result of applying these same mapping rules to the nested type `T`. |
| `["set",T]` | Identical in representation to `["list",T]`, but the order of elements is undefined because OpenTofu sets are unordered. |
| `["map",T]` | A JSON object with one property per element of the map value, where the element key is serialized as the property name string and the element value is represented by a property value constructed by applying these same mapping rules to the nested type `T`. |
| `["object",ATTRS]` | A JSON object with one property per attribute defined in the `ATTRS` object. The attribute name is serialized as the property name string and the attribute value is represented by a property value constructed by applying these same mapping rules to each attribute's own type. |
| `["tuple",TYPES]` | A JSON array with one element per element described by the `TYPES` array. The element values are constructed by applying these same mapping rules to the corresponding element of `TYPES`. |
| `"dynamic"` | A JSON object with two properties: `"type"` specifying one of the `type` patterns described in this table in-band, giving the exact runtime type of the value, and `"value"` specifying the result of applying these same mapping rules to the table for the specified runtime type. This special type constraint represents values whose types will be decided only at runtime. |
### `Schema.NestedBlock` Mapping Rules for JSON
The JSON serialization of a collection of blocks of a particular type depends
on the `nesting` field of the corresponding `Schema.NestedBlock` message.
The `nesting` field is a value from the `Schema.NestingBlock.NestingMode`
enumeration.
All `nesting` values cause the individual blocks of a type to be represented
by applying
[the `Schema.Block` mapping rules](#Schema.Block-mapping-rules-for-json)
to the block's contents based on the `block` field, producing what we'll call
a _block value_ in the table below.
The `nesting` value then in turn defines how OpenTofu will collect all of the
individual block values together to produce a single property value representing
the nested block type. For all `nesting` values other than `MAP`, blocks may
not have any labels. For the `nesting` value `MAP`, blocks must have exactly
one label, which is a string we'll call a _block label_ in the table below.
| `nesting` Value | JSON Representation |
|---|---|
| `SINGLE` | The block value of the single block of this type, or `null` if there is no block of that type. |
| `LIST` | A JSON array of all of the block values, preserving the order of definition of the blocks in the configuration. |
| `SET` | A JSON array of all of the block values in no particular order. |
| `MAP` | A JSON object with one property per block value, where the property name is the block label and the value is the block value. |
| `GROUP` | The same as with `SINGLE`, except that if there is no block of that type OpenTofu will synthesize a block value by pretending that all of the declared attributes are null and that there are zero blocks of each declared block type. |
For the `LIST` and `SET` nesting modes, OpenTofu guarantees that the JSON
array will have a number of elements between the `min_items` and `max_items`
values given in the schema.

View File

@@ -0,0 +1,53 @@
# Releasing a New Version of the Protocol
OpenTofu's plugin protocol is the contract between OpenTofu's plugins and
OpenTofu, and as such releasing a new version requires some coordination
between those pieces. This document is intended to be a checklist to consult
when adding a new major version of the protocol (X in X.Y) to ensure that
everything that needs to be is aware of it.
## New Protobuf File
The protocol is defined in protobuf files that live in the opentofu/opentofu
repository. Adding a new version of the protocol involves creating a new
`.proto` file in that directory. It is recommended that you copy the latest
protocol file, and modify it accordingly.
## New terraform-plugin-go Package
The
[hashicorp/terraform-plugin-go](https://github.com/hashicorp/terraform-plugin-go)
repository serves as the foundation for OpenTofu's plugin ecosystem. It needs
to know about the new major protocol version. Either open an issue in that repo
to have the Plugin SDK team add the new package, or if you would like to
contribute it yourself, open a PR. It is recommended that you copy the package
for the latest protocol version and modify it accordingly.
## Update the Registry's List of Allowed Versions
The OpenTofu Registry validates the protocol versions a provider advertises
support for when ingesting providers. Providers will not be able to advertise
support for the new protocol version until it is added to that list.
## Update OpenTofu's Version Constraints
OpenTofu only downloads providers that speak protocol versions it is
compatible with from the Registry during `tofu init`. When adding support
for a new protocol, you need to tell OpenTofu it knows that protocol version.
Modify the `SupportedPluginProtocols` variable in opentofu/opentofu's
`internal/getproviders/registry_client.go` file to include the new protocol.
## Test Running a Provider With the Test Framework
Use the provider test framework to test a provider written with the new
protocol. This end-to-end test ensures that providers written with the new
protocol work correctly with the test framework, especially in communicating
the protocol version between the test framework and OpenTofu.
## Test Retrieving and Running a Provider From the Registry
Publish a provider, either to the public registry or to the staging registry,
and test running `tofu init` and `tofu apply`, along with exercising
any of the new functionality the protocol version introduces. This end-to-end
test ensures that all the pieces needing to be updated before practitioners can
use providers built with the new protocol have been updated.

View File

@@ -0,0 +1,358 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Terraform Plugin RPC protocol version 5.0
//
// This file defines version 5.0 of the RPC protocol. To implement a plugin
// against this protocol, copy this definition into your own codebase and
// use protoc to generate stubs for your target language.
//
// This file will be updated in-place in the source Terraform repository for
// any minor versions of protocol 5, but later minor versions will always be
// backwards compatible. Breaking changes, if any are required, will come
// in a subsequent major version with its own separate proto definition.
//
// Note that only the proto files included in a release tag of Terraform are
// official protocol releases. Proto files taken from other commits may include
// incomplete changes or features that did not make it into a final release.
// In all reasonable cases, plugin developers should take the proto file from
// the tag of the most recent release of Terraform, and not from the main
// branch or any other development branch.
//
syntax = "proto3";
package tfplugin5;
// DynamicValue is an opaque encoding of terraform data, with the field name
// indicating the encoding scheme used.
message DynamicValue {
bytes msgpack = 1;
bytes json = 2;
}
message Diagnostic {
enum Severity {
INVALID = 0;
ERROR = 1;
WARNING = 2;
}
Severity severity = 1;
string summary = 2;
string detail = 3;
AttributePath attribute = 4;
}
message AttributePath {
message Step {
oneof selector {
// Set "attribute_name" to represent looking up an attribute
// in the current object value.
string attribute_name = 1;
// Set "element_key_*" to represent looking up an element in
// an indexable collection type.
string element_key_string = 2;
int64 element_key_int = 3;
}
}
repeated Step steps = 1;
}
message Stop {
message Request {
}
message Response {
string Error = 1;
}
}
// RawState holds the stored state for a resource to be upgraded by the
// provider. It can be in one of two formats, the current json encoded format
// in bytes, or the legacy flatmap format as a map of strings.
message RawState {
bytes json = 1;
map<string, string> flatmap = 2;
}
// Schema is the configuration schema for a Resource, Provider, or Provisioner.
message Schema {
message Block {
int64 version = 1;
repeated Attribute attributes = 2;
repeated NestedBlock block_types = 3;
}
message Attribute {
string name = 1;
bytes type = 2;
string description = 3;
bool required = 4;
bool optional = 5;
bool computed = 6;
bool sensitive = 7;
}
message NestedBlock {
enum NestingMode {
INVALID = 0;
SINGLE = 1;
LIST = 2;
SET = 3;
MAP = 4;
GROUP = 5;
}
string type_name = 1;
Block block = 2;
NestingMode nesting = 3;
int64 min_items = 4;
int64 max_items = 5;
}
// The version of the schema.
// Schemas are versioned, so that providers can upgrade a saved resource
// state when the schema is changed.
int64 version = 1;
// Block is the top level configuration block for this schema.
Block block = 2;
}
service Provider {
//////// Information about what a provider supports/expects
rpc GetSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response);
rpc PrepareProviderConfig(PrepareProviderConfig.Request) returns (PrepareProviderConfig.Response);
rpc ValidateResourceTypeConfig(ValidateResourceTypeConfig.Request) returns (ValidateResourceTypeConfig.Response);
rpc ValidateDataSourceConfig(ValidateDataSourceConfig.Request) returns (ValidateDataSourceConfig.Response);
rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response);
//////// One-time initialization, called before other functions below
rpc Configure(Configure.Request) returns (Configure.Response);
//////// Managed Resource Lifecycle
rpc ReadResource(ReadResource.Request) returns (ReadResource.Response);
rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response);
rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response);
rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response);
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
//////// Graceful Shutdown
rpc Stop(Stop.Request) returns (Stop.Response);
}
message GetProviderSchema {
message Request {
}
message Response {
Schema provider = 1;
map<string, Schema> resource_schemas = 2;
map<string, Schema> data_source_schemas = 3;
repeated Diagnostic diagnostics = 4;
}
}
message PrepareProviderConfig {
message Request {
DynamicValue config = 1;
}
message Response {
DynamicValue prepared_config = 1;
repeated Diagnostic diagnostics = 2;
}
}
message UpgradeResourceState {
message Request {
string type_name = 1;
// version is the schema_version number recorded in the state file
int64 version = 2;
// raw_state is the raw states as stored for the resource. Core does
// not have access to the schema of prior_version, so it's the
// provider's responsibility to interpret this value using the
// appropriate older schema. The raw_state will be the json encoded
// state, or a legacy flat-mapped format.
RawState raw_state = 3;
}
message Response {
// new_state is a msgpack-encoded data structure that, when interpreted with
// the _current_ schema for this resource type, is functionally equivalent to
// that which was given in prior_state_raw.
DynamicValue upgraded_state = 1;
// diagnostics describes any errors encountered during migration that could not
// be safely resolved, and warnings about any possibly-risky assumptions made
// in the upgrade process.
repeated Diagnostic diagnostics = 2;
}
}
message ValidateResourceTypeConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ValidateDataSourceConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message Configure {
message Request {
string terraform_version = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ReadResource {
message Request {
string type_name = 1;
DynamicValue current_state = 2;
bytes private = 3;
}
message Response {
DynamicValue new_state = 1;
repeated Diagnostic diagnostics = 2;
bytes private = 3;
}
}
message PlanResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue proposed_new_state = 3;
DynamicValue config = 4;
bytes prior_private = 5;
}
message Response {
DynamicValue planned_state = 1;
repeated AttributePath requires_replace = 2;
bytes planned_private = 3;
repeated Diagnostic diagnostics = 4;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 5;
}
}
message ApplyResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue planned_state = 3;
DynamicValue config = 4;
bytes planned_private = 5;
}
message Response {
DynamicValue new_state = 1;
bytes private = 2;
repeated Diagnostic diagnostics = 3;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 4;
}
}
message ImportResourceState {
message Request {
string type_name = 1;
string id = 2;
}
message ImportedResource {
string type_name = 1;
DynamicValue state = 2;
bytes private = 3;
}
message Response {
repeated ImportedResource imported_resources = 1;
repeated Diagnostic diagnostics = 2;
}
}
message ReadDataSource {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
DynamicValue state = 1;
repeated Diagnostic diagnostics = 2;
}
}
service Provisioner {
rpc GetSchema(GetProvisionerSchema.Request) returns (GetProvisionerSchema.Response);
rpc ValidateProvisionerConfig(ValidateProvisionerConfig.Request) returns (ValidateProvisionerConfig.Response);
rpc ProvisionResource(ProvisionResource.Request) returns (stream ProvisionResource.Response);
rpc Stop(Stop.Request) returns (Stop.Response);
}
message GetProvisionerSchema {
message Request {
}
message Response {
Schema provisioner = 1;
repeated Diagnostic diagnostics = 2;
}
}
message ValidateProvisionerConfig {
message Request {
DynamicValue config = 1;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ProvisionResource {
message Request {
DynamicValue config = 1;
DynamicValue connection = 2;
}
message Response {
string output = 1;
repeated Diagnostic diagnostics = 2;
}
}

View File

@@ -0,0 +1,358 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Terraform Plugin RPC protocol version 5.1
//
// This file defines version 5.1 of the RPC protocol. To implement a plugin
// against this protocol, copy this definition into your own codebase and
// use protoc to generate stubs for your target language.
//
// This file will be updated in-place in the source Terraform repository for
// any minor versions of protocol 5, but later minor versions will always be
// backwards compatible. Breaking changes, if any are required, will come
// in a subsequent major version with its own separate proto definition.
//
// Note that only the proto files included in a release tag of Terraform are
// official protocol releases. Proto files taken from other commits may include
// incomplete changes or features that did not make it into a final release.
// In all reasonable cases, plugin developers should take the proto file from
// the tag of the most recent release of Terraform, and not from the main
// branch or any other development branch.
//
syntax = "proto3";
package tfplugin5;
// DynamicValue is an opaque encoding of terraform data, with the field name
// indicating the encoding scheme used.
message DynamicValue {
bytes msgpack = 1;
bytes json = 2;
}
message Diagnostic {
enum Severity {
INVALID = 0;
ERROR = 1;
WARNING = 2;
}
Severity severity = 1;
string summary = 2;
string detail = 3;
AttributePath attribute = 4;
}
message AttributePath {
message Step {
oneof selector {
// Set "attribute_name" to represent looking up an attribute
// in the current object value.
string attribute_name = 1;
// Set "element_key_*" to represent looking up an element in
// an indexable collection type.
string element_key_string = 2;
int64 element_key_int = 3;
}
}
repeated Step steps = 1;
}
message Stop {
message Request {
}
message Response {
string Error = 1;
}
}
// RawState holds the stored state for a resource to be upgraded by the
// provider. It can be in one of two formats, the current json encoded format
// in bytes, or the legacy flatmap format as a map of strings.
message RawState {
bytes json = 1;
map<string, string> flatmap = 2;
}
// Schema is the configuration schema for a Resource, Provider, or Provisioner.
message Schema {
message Block {
int64 version = 1;
repeated Attribute attributes = 2;
repeated NestedBlock block_types = 3;
}
message Attribute {
string name = 1;
bytes type = 2;
string description = 3;
bool required = 4;
bool optional = 5;
bool computed = 6;
bool sensitive = 7;
}
message NestedBlock {
enum NestingMode {
INVALID = 0;
SINGLE = 1;
LIST = 2;
SET = 3;
MAP = 4;
GROUP = 5;
}
string type_name = 1;
Block block = 2;
NestingMode nesting = 3;
int64 min_items = 4;
int64 max_items = 5;
}
// The version of the schema.
// Schemas are versioned, so that providers can upgrade a saved resource
// state when the schema is changed.
int64 version = 1;
// Block is the top level configuration block for this schema.
Block block = 2;
}
service Provider {
//////// Information about what a provider supports/expects
rpc GetSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response);
rpc PrepareProviderConfig(PrepareProviderConfig.Request) returns (PrepareProviderConfig.Response);
rpc ValidateResourceTypeConfig(ValidateResourceTypeConfig.Request) returns (ValidateResourceTypeConfig.Response);
rpc ValidateDataSourceConfig(ValidateDataSourceConfig.Request) returns (ValidateDataSourceConfig.Response);
rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response);
//////// One-time initialization, called before other functions below
rpc Configure(Configure.Request) returns (Configure.Response);
//////// Managed Resource Lifecycle
rpc ReadResource(ReadResource.Request) returns (ReadResource.Response);
rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response);
rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response);
rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response);
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
//////// Graceful Shutdown
rpc Stop(Stop.Request) returns (Stop.Response);
}
message GetProviderSchema {
message Request {
}
message Response {
Schema provider = 1;
map<string, Schema> resource_schemas = 2;
map<string, Schema> data_source_schemas = 3;
repeated Diagnostic diagnostics = 4;
}
}
message PrepareProviderConfig {
message Request {
DynamicValue config = 1;
}
message Response {
DynamicValue prepared_config = 1;
repeated Diagnostic diagnostics = 2;
}
}
message UpgradeResourceState {
message Request {
string type_name = 1;
// version is the schema_version number recorded in the state file
int64 version = 2;
// raw_state is the raw states as stored for the resource. Core does
// not have access to the schema of prior_version, so it's the
// provider's responsibility to interpret this value using the
// appropriate older schema. The raw_state will be the json encoded
// state, or a legacy flat-mapped format.
RawState raw_state = 3;
}
message Response {
// new_state is a msgpack-encoded data structure that, when interpreted with
// the _current_ schema for this resource type, is functionally equivalent to
// that which was given in prior_state_raw.
DynamicValue upgraded_state = 1;
// diagnostics describes any errors encountered during migration that could not
// be safely resolved, and warnings about any possibly-risky assumptions made
// in the upgrade process.
repeated Diagnostic diagnostics = 2;
}
}
message ValidateResourceTypeConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ValidateDataSourceConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message Configure {
message Request {
string terraform_version = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ReadResource {
message Request {
string type_name = 1;
DynamicValue current_state = 2;
bytes private = 3;
}
message Response {
DynamicValue new_state = 1;
repeated Diagnostic diagnostics = 2;
bytes private = 3;
}
}
message PlanResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue proposed_new_state = 3;
DynamicValue config = 4;
bytes prior_private = 5;
}
message Response {
DynamicValue planned_state = 1;
repeated AttributePath requires_replace = 2;
bytes planned_private = 3;
repeated Diagnostic diagnostics = 4;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 5;
}
}
message ApplyResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue planned_state = 3;
DynamicValue config = 4;
bytes planned_private = 5;
}
message Response {
DynamicValue new_state = 1;
bytes private = 2;
repeated Diagnostic diagnostics = 3;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 4;
}
}
message ImportResourceState {
message Request {
string type_name = 1;
string id = 2;
}
message ImportedResource {
string type_name = 1;
DynamicValue state = 2;
bytes private = 3;
}
message Response {
repeated ImportedResource imported_resources = 1;
repeated Diagnostic diagnostics = 2;
}
}
message ReadDataSource {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
DynamicValue state = 1;
repeated Diagnostic diagnostics = 2;
}
}
service Provisioner {
rpc GetSchema(GetProvisionerSchema.Request) returns (GetProvisionerSchema.Response);
rpc ValidateProvisionerConfig(ValidateProvisionerConfig.Request) returns (ValidateProvisionerConfig.Response);
rpc ProvisionResource(ProvisionResource.Request) returns (stream ProvisionResource.Response);
rpc Stop(Stop.Request) returns (Stop.Response);
}
message GetProvisionerSchema {
message Request {
}
message Response {
Schema provisioner = 1;
repeated Diagnostic diagnostics = 2;
}
}
message ValidateProvisionerConfig {
message Request {
DynamicValue config = 1;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ProvisionResource {
message Request {
DynamicValue config = 1;
DynamicValue connection = 2;
}
message Response {
string output = 1;
repeated Diagnostic diagnostics = 2;
}
}

View File

@@ -0,0 +1,953 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: MPL-2.0
// Terraform Plugin RPC protocol version 5.10
//
// This file defines version 5.10 of the RPC protocol. To implement a plugin
// against this protocol, copy this definition into your own codebase and
// use protoc to generate stubs for your target language.
//
// Any minor versions of protocol 5 to follow should modify this file while
// maintaining backwards compatibility. Breaking changes, if any are required,
// will come in a subsequent major version with its own separate proto definition.
//
// Note that only the proto files included in a release tag of Terraform are
// official protocol releases. Proto files taken from other commits may include
// incomplete changes or features that did not make it into a final release.
// In all reasonable cases, plugin developers should take the proto file from
// the tag of the most recent release of Terraform, and not from the main
// branch or any other development branch.
//
syntax = "proto3";
option go_package = "github.com/opentofu/opentofu/internal/tfplugin5";
import "google/protobuf/timestamp.proto";
package tfplugin5;
// DynamicValue is an opaque encoding of terraform data, with the field name
// indicating the encoding scheme used.
message DynamicValue {
bytes msgpack = 1;
bytes json = 2;
}
message Diagnostic {
enum Severity {
INVALID = 0;
ERROR = 1;
WARNING = 2;
}
Severity severity = 1;
string summary = 2;
string detail = 3;
AttributePath attribute = 4;
}
message FunctionError {
string text = 1;
// The optional function_argument records the index position of the
// argument which caused the error.
optional int64 function_argument = 2;
}
message AttributePath {
message Step {
oneof selector {
// Set "attribute_name" to represent looking up an attribute
// in the current object value.
string attribute_name = 1;
// Set "element_key_*" to represent looking up an element in
// an indexable collection type.
string element_key_string = 2;
int64 element_key_int = 3;
}
}
repeated Step steps = 1;
}
message Stop {
message Request {
}
message Response {
string Error = 1;
}
}
// RawState holds the stored state for a resource to be upgraded by the
// provider. It can be in one of two formats, the current json encoded format
// in bytes, or the legacy flatmap format as a map of strings.
message RawState {
bytes json = 1;
map<string, string> flatmap = 2;
}
enum StringKind {
PLAIN = 0;
MARKDOWN = 1;
}
// ResourceIdentitySchema represents the structure and types of data used to identify
// a managed resource type. Effectively, resource identity is a versioned object
// that can be used to compare resources, whether already managed and/or being
// discovered.
message ResourceIdentitySchema {
// IdentityAttribute represents one value of data within resource identity. These
// are always used in resource identity comparisons.
message IdentityAttribute {
// name is the identity attribute name
string name = 1;
// type is the identity attribute type
bytes type = 2;
// required_for_import when enabled signifies that this attribute must be
// defined for ImportResourceState to complete successfully
bool required_for_import = 3;
// optional_for_import when enabled signifies that this attribute is not
// required for ImportResourceState, because it can be supplied by the
// provider. It is still possible to supply this attribute during import.
bool optional_for_import = 4;
// description is a human-readable description of the attribute in Markdown
string description = 5;
}
// version is the identity version and separate from the Schema version.
// Any time the structure or format of identity_attributes changes, this version
// should be incremented. Versioning implicitly starts at 0 and by convention
// should be incremented by 1 each change.
//
// When comparing identity_attributes data, differing versions should always be treated
// as inequal.
int64 version = 1;
// identity_attributes are the individual value definitions which define identity data
// for a managed resource type. This information is used to decode DynamicValue of
// identity data.
//
// These attributes are intended for permanent identity data and must be wholly
// representative of all data necessary to compare two managed resource instances
// with no other data. This generally should include account, endpoint, location,
// and automatically generated identifiers. For some resources, this may include
// configuration-based data, such as a required name which must be unique.
repeated IdentityAttribute identity_attributes = 2;
}
message ResourceIdentityData {
// identity_data is the resource identity data for the given definition. It should
// be decoded using the identity schema.
//
// This data is considered permanent for the identity version and suitable for
// longer-term storage.
DynamicValue identity_data = 1;
}
// ActionSchema defines the schema for an action that can be invoked by Terraform.
message ActionSchema {
Schema schema = 1; // of the action itself
}
// Schema is the configuration schema for a Resource, Provider, or Provisioner.
message Schema {
message Block {
int64 version = 1;
repeated Attribute attributes = 2;
repeated NestedBlock block_types = 3;
string description = 4;
StringKind description_kind = 5;
bool deprecated = 6;
string deprecation_message = 7;
}
message Attribute {
string name = 1;
bytes type = 2;
string description = 3;
bool required = 4;
bool optional = 5;
bool computed = 6;
bool sensitive = 7;
StringKind description_kind = 8;
bool deprecated = 9;
bool write_only = 10;
string deprecation_message = 11;
}
message NestedBlock {
enum NestingMode {
INVALID = 0;
SINGLE = 1;
LIST = 2;
SET = 3;
MAP = 4;
GROUP = 5;
}
string type_name = 1;
Block block = 2;
NestingMode nesting = 3;
int64 min_items = 4;
int64 max_items = 5;
}
// The version of the schema.
// Schemas are versioned, so that providers can upgrade a saved resource
// state when the schema is changed.
int64 version = 1;
// Block is the top level configuration block for this schema.
Block block = 2;
}
message Function {
// parameters is the ordered list of positional function parameters.
repeated Parameter parameters = 1;
// variadic_parameter is an optional final parameter which accepts
// zero or more argument values, in which Terraform will send an
// ordered list of the parameter type.
Parameter variadic_parameter = 2;
// Return is the function return parameter.
Return return = 3;
// summary is the human-readable shortened documentation for the function.
string summary = 4;
// description is human-readable documentation for the function.
string description = 5;
// description_kind is the formatting of the description.
StringKind description_kind = 6;
// deprecation_message is human-readable documentation if the
// function is deprecated.
string deprecation_message = 7;
message Parameter {
// name is the human-readable display name for the parameter.
string name = 1;
// type is the type constraint for the parameter.
bytes type = 2;
// allow_null_value when enabled denotes that a null argument value can
// be passed to the provider. When disabled, Terraform returns an error
// if the argument value is null.
bool allow_null_value = 3;
// allow_unknown_values when enabled denotes that only wholly known
// argument values will be passed to the provider. When disabled,
// Terraform skips the function call entirely and assumes an unknown
// value result from the function.
bool allow_unknown_values = 4;
// description is human-readable documentation for the parameter.
string description = 5;
// description_kind is the formatting of the description.
StringKind description_kind = 6;
}
message Return {
// type is the type constraint for the function result.
bytes type = 1;
}
}
// ServerCapabilities allows providers to communicate extra information
// regarding supported protocol features. This is used to indicate
// availability of certain forward-compatible changes which may be optional
// in a major protocol version, but cannot be tested for directly.
message ServerCapabilities {
// The plan_destroy capability signals that a provider expects a call
// to PlanResourceChange when a resource is going to be destroyed.
bool plan_destroy = 1;
// The get_provider_schema_optional capability indicates that this
// provider does not require calling GetProviderSchema to operate
// normally, and the caller can used a cached copy of the provider's
// schema.
bool get_provider_schema_optional = 2;
// The move_resource_state capability signals that a provider supports the
// MoveResourceState RPC.
bool move_resource_state = 3;
// The generate_resource_config capability signals that a provider supports
// GenerateResourceConfig.
bool generate_resource_config = 4;
}
// ClientCapabilities allows Terraform to publish information regarding
// supported protocol features. This is used to indicate availability of
// certain forward-compatible changes which may be optional in a major
// protocol version, but cannot be tested for directly.
message ClientCapabilities {
// The deferral_allowed capability signals that the client is able to
// handle deferred responses from the provider.
bool deferral_allowed = 1;
// The write_only_attributes_allowed capability signals that the client
// is able to handle write_only attributes for managed resources.
bool write_only_attributes_allowed = 2;
}
// Deferred is a message that indicates that change is deferred for a reason.
message Deferred {
// Reason is the reason for deferring the change.
enum Reason {
// UNKNOWN is the default value, and should not be used.
UNKNOWN = 0;
// RESOURCE_CONFIG_UNKNOWN is used when the config is partially unknown and the real
// values need to be known before the change can be planned.
RESOURCE_CONFIG_UNKNOWN = 1;
// PROVIDER_CONFIG_UNKNOWN is used when parts of the provider configuration
// are unknown, e.g. the provider configuration is only known after the apply is done.
PROVIDER_CONFIG_UNKNOWN = 2;
// ABSENT_PREREQ is used when a hard dependency has not been satisfied.
ABSENT_PREREQ = 3;
}
// reason is the reason for deferring the change.
Reason reason = 1;
}
service Provider {
//////// Information about what a provider supports/expects
// GetMetadata returns upfront information about server capabilities and
// supported resource types without requiring the server to instantiate all
// schema information, which may be memory intensive.
// This method is CURRENTLY UNUSED and it serves mostly for convenience
// of code generation inside of terraform-plugin-mux.
rpc GetMetadata(GetMetadata.Request) returns (GetMetadata.Response);
// GetSchema returns schema information for the provider, data resources,
// and managed resources.
rpc GetSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response);
rpc PrepareProviderConfig(PrepareProviderConfig.Request) returns (PrepareProviderConfig.Response);
rpc ValidateResourceTypeConfig(ValidateResourceTypeConfig.Request) returns (ValidateResourceTypeConfig.Response);
rpc ValidateDataSourceConfig(ValidateDataSourceConfig.Request) returns (ValidateDataSourceConfig.Response);
rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response);
// GetResourceIdentitySchemas returns the identity schemas for all managed
// resources.
rpc GetResourceIdentitySchemas(GetResourceIdentitySchemas.Request) returns (GetResourceIdentitySchemas.Response);
// UpgradeResourceIdentityData should return the upgraded resource identity
// data for a managed resource type.
rpc UpgradeResourceIdentity(UpgradeResourceIdentity.Request) returns (UpgradeResourceIdentity.Response);
//////// One-time initialization, called before other functions below
rpc Configure(Configure.Request) returns (Configure.Response);
//////// Managed Resource Lifecycle
rpc ReadResource(ReadResource.Request) returns (ReadResource.Response);
rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response);
rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response);
rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response);
rpc MoveResourceState(MoveResourceState.Request) returns (MoveResourceState.Response);
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
rpc GenerateResourceConfig(GenerateResourceConfig.Request) returns (GenerateResourceConfig.Response);
//////// Ephemeral Resource Lifecycle
rpc ValidateEphemeralResourceConfig(ValidateEphemeralResourceConfig.Request) returns (ValidateEphemeralResourceConfig.Response);
rpc OpenEphemeralResource(OpenEphemeralResource.Request) returns (OpenEphemeralResource.Response);
rpc RenewEphemeralResource(RenewEphemeralResource.Request) returns (RenewEphemeralResource.Response);
rpc CloseEphemeralResource(CloseEphemeralResource.Request) returns (CloseEphemeralResource.Response);
/////// List
rpc ListResource(ListResource.Request) returns (stream ListResource.Event);
rpc ValidateListResourceConfig(ValidateListResourceConfig.Request) returns (ValidateListResourceConfig.Response);
// GetFunctions returns the definitions of all functions.
rpc GetFunctions(GetFunctions.Request) returns (GetFunctions.Response);
//////// Provider-contributed Functions
rpc CallFunction(CallFunction.Request) returns (CallFunction.Response);
//////// Actions
rpc PlanAction(PlanAction.Request) returns (PlanAction.Response);
rpc InvokeAction(InvokeAction.Request) returns (stream InvokeAction.Event);
rpc ValidateActionConfig(ValidateActionConfig.Request) returns (ValidateActionConfig.Response);
//////// Graceful Shutdown
rpc Stop(Stop.Request) returns (Stop.Response);
}
message GetMetadata {
message Request {
}
message Response {
ServerCapabilities server_capabilities = 1;
repeated Diagnostic diagnostics = 2;
repeated DataSourceMetadata data_sources = 3;
repeated ResourceMetadata resources = 4;
// functions returns metadata for any functions.
repeated FunctionMetadata functions = 5;
repeated EphemeralMetadata ephemeral_resources = 6;
repeated ListResourceMetadata list_resources = 7;
repeated ActionMetadata actions = 8;
}
message EphemeralMetadata {
string type_name = 1;
}
message FunctionMetadata {
// name is the function name.
string name = 1;
}
message DataSourceMetadata {
string type_name = 1;
}
message ResourceMetadata {
string type_name = 1;
}
message ListResourceMetadata {
string type_name = 1;
}
message ActionMetadata {
string type_name = 1;
}
}
message GetProviderSchema {
message Request {
}
message Response {
Schema provider = 1;
map<string, Schema> resource_schemas = 2;
map<string, Schema> data_source_schemas = 3;
map<string, Function> functions = 7;
map<string, Schema> ephemeral_resource_schemas = 8;
map<string, Schema> list_resource_schemas = 9;
reserved 10; // Field number 10 is used by state stores in version 6
map<string, ActionSchema> action_schemas = 11;
repeated Diagnostic diagnostics = 4;
Schema provider_meta = 5;
ServerCapabilities server_capabilities = 6;
}
}
message PrepareProviderConfig {
message Request {
DynamicValue config = 1;
}
message Response {
DynamicValue prepared_config = 1;
repeated Diagnostic diagnostics = 2;
}
}
message UpgradeResourceState {
// Request is the message that is sent to the provider during the
// UpgradeResourceState RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to exist (in the case of resource destruction), be wholly
// known, nor match the given prior state, which could lead to unexpected
// provider behaviors for practitioners.
message Request {
string type_name = 1;
// version is the schema_version number recorded in the state file
int64 version = 2;
// raw_state is the raw states as stored for the resource. Core does
// not have access to the schema of prior_version, so it's the
// provider's responsibility to interpret this value using the
// appropriate older schema. The raw_state will be the json encoded
// state, or a legacy flat-mapped format.
RawState raw_state = 3;
}
message Response {
// new_state is a msgpack-encoded data structure that, when interpreted with
// the _current_ schema for this resource type, is functionally equivalent to
// that which was given in prior_state_raw.
DynamicValue upgraded_state = 1;
// diagnostics describes any errors encountered during migration that could not
// be safely resolved, and warnings about any possibly-risky assumptions made
// in the upgrade process.
repeated Diagnostic diagnostics = 2;
}
}
message GetResourceIdentitySchemas {
message Request {
}
message Response {
// identity_schemas is a mapping of resource type names to their identity schemas.
map<string, ResourceIdentitySchema> identity_schemas = 1;
// diagnostics is the collection of warning and error diagnostics for this request.
repeated Diagnostic diagnostics = 2;
}
}
message UpgradeResourceIdentity {
message Request {
// type_name is the managed resource type name
string type_name = 1;
// version is the version of the resource identity data to upgrade
int64 version = 2;
// raw_identity is the raw identity as stored for the resource. Core does
// not have access to the identity schema of prior_version, so it's the
// provider's responsibility to interpret this value using the
// appropriate older schema. The raw_identity will be json encoded.
RawState raw_identity = 3;
}
message Response {
// upgraded_identity returns the upgraded resource identity data
ResourceIdentityData upgraded_identity = 1;
// diagnostics is the collection of warning and error diagnostics for this request
repeated Diagnostic diagnostics = 2;
}
}
message ValidateResourceTypeConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
ClientCapabilities client_capabilities = 3;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ValidateDataSourceConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ValidateEphemeralResourceConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message Configure {
message Request {
string terraform_version = 1;
DynamicValue config = 2;
ClientCapabilities client_capabilities = 3;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ReadResource {
// Request is the message that is sent to the provider during the
// ReadResource RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to be wholly known nor match the given prior state, which
// could lead to unexpected provider behaviors for practitioners.
message Request {
string type_name = 1;
DynamicValue current_state = 2;
bytes private = 3;
DynamicValue provider_meta = 4;
ClientCapabilities client_capabilities = 5;
ResourceIdentityData current_identity = 6;
}
message Response {
DynamicValue new_state = 1;
repeated Diagnostic diagnostics = 2;
bytes private = 3;
// deferred is set if the provider is deferring the change. If set the caller
// needs to handle the deferral.
Deferred deferred = 4;
ResourceIdentityData new_identity = 5;
}
}
message PlanResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue proposed_new_state = 3;
DynamicValue config = 4;
bytes prior_private = 5;
DynamicValue provider_meta = 6;
ClientCapabilities client_capabilities = 7;
ResourceIdentityData prior_identity = 8;
}
message Response {
DynamicValue planned_state = 1;
repeated AttributePath requires_replace = 2;
bytes planned_private = 3;
repeated Diagnostic diagnostics = 4;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 5;
// deferred is set if the provider is deferring the change. If set the caller
// needs to handle the deferral.
Deferred deferred = 6;
ResourceIdentityData planned_identity = 7;
}
}
message ApplyResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue planned_state = 3;
DynamicValue config = 4;
bytes planned_private = 5;
DynamicValue provider_meta = 6;
ResourceIdentityData planned_identity = 7;
}
message Response {
DynamicValue new_state = 1;
bytes private = 2;
repeated Diagnostic diagnostics = 3;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 4;
ResourceIdentityData new_identity = 5;
}
}
message ImportResourceState {
message Request {
string type_name = 1;
string id = 2;
ClientCapabilities client_capabilities = 3;
ResourceIdentityData identity = 4;
}
message ImportedResource {
string type_name = 1;
DynamicValue state = 2;
bytes private = 3;
ResourceIdentityData identity = 4;
}
message Response {
repeated ImportedResource imported_resources = 1;
repeated Diagnostic diagnostics = 2;
// deferred is set if the provider is deferring the change. If set the caller
// needs to handle the deferral.
Deferred deferred = 3;
}
}
message GenerateResourceConfig {
message Request {
string type_name = 1;
DynamicValue state = 2;
}
message Response {
// config is the provided state modified such that it represents a valid resource configuration value.
DynamicValue config = 1;
repeated Diagnostic diagnostics = 2;
}
}
message MoveResourceState {
message Request {
// The address of the provider the resource is being moved from.
string source_provider_address = 1;
// The resource type that the resource is being moved from.
string source_type_name = 2;
// The schema version of the resource type that the resource is being
// moved from.
int64 source_schema_version = 3;
// The raw state of the resource being moved. Only the json field is
// populated, as there should be no legacy providers using the flatmap
// format that support newly introduced RPCs.
RawState source_state = 4;
// The resource type that the resource is being moved to.
string target_type_name = 5;
// The private state of the resource being moved.
bytes source_private = 6;
// The raw identity of the resource being moved. Only the json field is
// populated, as there should be no legacy providers using the flatmap
// format that support newly introduced RPCs.
RawState source_identity = 7;
// The identity schema version of the resource type that the resource
// is being moved from.
int64 source_identity_schema_version = 8;
}
message Response {
// The state of the resource after it has been moved.
DynamicValue target_state = 1;
// Any diagnostics that occurred during the move.
repeated Diagnostic diagnostics = 2;
// The private state of the resource after it has been moved.
bytes target_private = 3;
ResourceIdentityData target_identity = 4;
}
}
message ReadDataSource {
message Request {
string type_name = 1;
DynamicValue config = 2;
DynamicValue provider_meta = 3;
ClientCapabilities client_capabilities = 4;
}
message Response {
DynamicValue state = 1;
repeated Diagnostic diagnostics = 2;
// deferred is set if the provider is deferring the change. If set the caller
// needs to handle the deferral.
Deferred deferred = 3;
}
}
service Provisioner {
rpc GetSchema(GetProvisionerSchema.Request) returns (GetProvisionerSchema.Response);
rpc ValidateProvisionerConfig(ValidateProvisionerConfig.Request) returns (ValidateProvisionerConfig.Response);
rpc ProvisionResource(ProvisionResource.Request) returns (stream ProvisionResource.Response);
rpc Stop(Stop.Request) returns (Stop.Response);
}
message GetProvisionerSchema {
message Request {
}
message Response {
Schema provisioner = 1;
repeated Diagnostic diagnostics = 2;
}
}
message ValidateProvisionerConfig {
message Request {
DynamicValue config = 1;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ProvisionResource {
message Request {
DynamicValue config = 1;
DynamicValue connection = 2;
}
message Response {
string output = 1;
repeated Diagnostic diagnostics = 2;
}
}
message OpenEphemeralResource {
message Request {
string type_name = 1;
DynamicValue config = 2;
ClientCapabilities client_capabilities = 3;
}
message Response {
repeated Diagnostic diagnostics = 1;
optional google.protobuf.Timestamp renew_at = 2;
DynamicValue result = 3;
optional bytes private = 4;
Deferred deferred = 5;
}
}
message RenewEphemeralResource {
message Request {
string type_name = 1;
optional bytes private = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
optional google.protobuf.Timestamp renew_at = 2;
optional bytes private = 3;
}
}
message CloseEphemeralResource {
message Request {
string type_name = 1;
optional bytes private = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message GetFunctions {
message Request {}
message Response {
// functions is a mapping of function names to definitions.
map<string, Function> functions = 1;
// diagnostics is any warnings or errors.
repeated Diagnostic diagnostics = 2;
}
}
message CallFunction {
message Request {
string name = 1;
repeated DynamicValue arguments = 2;
}
message Response {
DynamicValue result = 1;
FunctionError error = 2;
}
}
message ListResource {
message Request {
// type_name is the list resource type name.
string type_name = 1;
// configuration is the list ConfigSchema-based configuration data.
DynamicValue config = 2;
// when include_resource_object is set to true, the provider should
// include the full resource object for each result
bool include_resource_object = 3;
// The maximum number of results that Terraform is expecting.
// The stream will stop, once this limit is reached.
int64 limit = 4;
}
message Event {
// identity is the resource identity data of the resource instance.
ResourceIdentityData identity = 1;
// display_name can be displayed in a UI to make it easier for humans to identify a resource
string display_name = 2;
// optional resource object which can be useful when combining list blocks in configuration
optional DynamicValue resource_object = 3;
// A warning or error diagnostics for this event
repeated Diagnostic diagnostic = 4;
}
}
message ValidateListResourceConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
DynamicValue include_resource_object = 3;
DynamicValue limit = 4;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message PlanAction {
message Request {
string action_type = 1;
// config of the action, based on the schema of the actual action
DynamicValue config = 2;
// metadata
ClientCapabilities client_capabilities = 3;
}
message Response {
repeated Diagnostic diagnostics = 1;
// metadata
Deferred deferred = 2;
}
}
message InvokeAction {
message Request {
string action_type = 1;
// response from the plan
DynamicValue config = 2;
// metadata
ClientCapabilities client_capabilities = 3;
}
message Event {
message Progress {
// message to be printed in the console / HCPT
string message = 1;
}
message Completed {
repeated Diagnostic diagnostics = 1;
}
oneof type {
Progress progress = 1;
Completed completed = 2;
}
}
}
message ValidateActionConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}

View File

@@ -0,0 +1,374 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Terraform Plugin RPC protocol version 5.2
//
// This file defines version 5.2 of the RPC protocol. To implement a plugin
// against this protocol, copy this definition into your own codebase and
// use protoc to generate stubs for your target language.
//
// This file will not be updated. Any minor versions of protocol 5 to follow
// should copy this file and modify the copy while maintaining backwards
// compatibility. Breaking changes, if any are required, will come
// in a subsequent major version with its own separate proto definition.
//
// Note that only the proto files included in a release tag of Terraform are
// official protocol releases. Proto files taken from other commits may include
// incomplete changes or features that did not make it into a final release.
// In all reasonable cases, plugin developers should take the proto file from
// the tag of the most recent release of Terraform, and not from the main
// branch or any other development branch.
//
syntax = "proto3";
option go_package = "github.com/opentofu/opentofu/internal/tfplugin5";
package tfplugin5;
// DynamicValue is an opaque encoding of terraform data, with the field name
// indicating the encoding scheme used.
message DynamicValue {
bytes msgpack = 1;
bytes json = 2;
}
message Diagnostic {
enum Severity {
INVALID = 0;
ERROR = 1;
WARNING = 2;
}
Severity severity = 1;
string summary = 2;
string detail = 3;
AttributePath attribute = 4;
}
message AttributePath {
message Step {
oneof selector {
// Set "attribute_name" to represent looking up an attribute
// in the current object value.
string attribute_name = 1;
// Set "element_key_*" to represent looking up an element in
// an indexable collection type.
string element_key_string = 2;
int64 element_key_int = 3;
}
}
repeated Step steps = 1;
}
message Stop {
message Request {
}
message Response {
string Error = 1;
}
}
// RawState holds the stored state for a resource to be upgraded by the
// provider. It can be in one of two formats, the current json encoded format
// in bytes, or the legacy flatmap format as a map of strings.
message RawState {
bytes json = 1;
map<string, string> flatmap = 2;
}
enum StringKind {
PLAIN = 0;
MARKDOWN = 1;
}
// Schema is the configuration schema for a Resource, Provider, or Provisioner.
message Schema {
message Block {
int64 version = 1;
repeated Attribute attributes = 2;
repeated NestedBlock block_types = 3;
string description = 4;
StringKind description_kind = 5;
bool deprecated = 6;
}
message Attribute {
string name = 1;
bytes type = 2;
string description = 3;
bool required = 4;
bool optional = 5;
bool computed = 6;
bool sensitive = 7;
StringKind description_kind = 8;
bool deprecated = 9;
}
message NestedBlock {
enum NestingMode {
INVALID = 0;
SINGLE = 1;
LIST = 2;
SET = 3;
MAP = 4;
GROUP = 5;
}
string type_name = 1;
Block block = 2;
NestingMode nesting = 3;
int64 min_items = 4;
int64 max_items = 5;
}
// The version of the schema.
// Schemas are versioned, so that providers can upgrade a saved resource
// state when the schema is changed.
int64 version = 1;
// Block is the top level configuration block for this schema.
Block block = 2;
}
service Provider {
//////// Information about what a provider supports/expects
rpc GetSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response);
rpc PrepareProviderConfig(PrepareProviderConfig.Request) returns (PrepareProviderConfig.Response);
rpc ValidateResourceTypeConfig(ValidateResourceTypeConfig.Request) returns (ValidateResourceTypeConfig.Response);
rpc ValidateDataSourceConfig(ValidateDataSourceConfig.Request) returns (ValidateDataSourceConfig.Response);
rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response);
//////// One-time initialization, called before other functions below
rpc Configure(Configure.Request) returns (Configure.Response);
//////// Managed Resource Lifecycle
rpc ReadResource(ReadResource.Request) returns (ReadResource.Response);
rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response);
rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response);
rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response);
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
//////// Graceful Shutdown
rpc Stop(Stop.Request) returns (Stop.Response);
}
message GetProviderSchema {
message Request {
}
message Response {
Schema provider = 1;
map<string, Schema> resource_schemas = 2;
map<string, Schema> data_source_schemas = 3;
repeated Diagnostic diagnostics = 4;
Schema provider_meta = 5;
}
}
message PrepareProviderConfig {
message Request {
DynamicValue config = 1;
}
message Response {
DynamicValue prepared_config = 1;
repeated Diagnostic diagnostics = 2;
}
}
message UpgradeResourceState {
message Request {
string type_name = 1;
// version is the schema_version number recorded in the state file
int64 version = 2;
// raw_state is the raw states as stored for the resource. Core does
// not have access to the schema of prior_version, so it's the
// provider's responsibility to interpret this value using the
// appropriate older schema. The raw_state will be the json encoded
// state, or a legacy flat-mapped format.
RawState raw_state = 3;
}
message Response {
// new_state is a msgpack-encoded data structure that, when interpreted with
// the _current_ schema for this resource type, is functionally equivalent to
// that which was given in prior_state_raw.
DynamicValue upgraded_state = 1;
// diagnostics describes any errors encountered during migration that could not
// be safely resolved, and warnings about any possibly-risky assumptions made
// in the upgrade process.
repeated Diagnostic diagnostics = 2;
}
}
message ValidateResourceTypeConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ValidateDataSourceConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message Configure {
message Request {
string terraform_version = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ReadResource {
message Request {
string type_name = 1;
DynamicValue current_state = 2;
bytes private = 3;
DynamicValue provider_meta = 4;
}
message Response {
DynamicValue new_state = 1;
repeated Diagnostic diagnostics = 2;
bytes private = 3;
}
}
message PlanResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue proposed_new_state = 3;
DynamicValue config = 4;
bytes prior_private = 5;
DynamicValue provider_meta = 6;
}
message Response {
DynamicValue planned_state = 1;
repeated AttributePath requires_replace = 2;
bytes planned_private = 3;
repeated Diagnostic diagnostics = 4;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 5;
}
}
message ApplyResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue planned_state = 3;
DynamicValue config = 4;
bytes planned_private = 5;
DynamicValue provider_meta = 6;
}
message Response {
DynamicValue new_state = 1;
bytes private = 2;
repeated Diagnostic diagnostics = 3;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 4;
}
}
message ImportResourceState {
message Request {
string type_name = 1;
string id = 2;
}
message ImportedResource {
string type_name = 1;
DynamicValue state = 2;
bytes private = 3;
}
message Response {
repeated ImportedResource imported_resources = 1;
repeated Diagnostic diagnostics = 2;
}
}
message ReadDataSource {
message Request {
string type_name = 1;
DynamicValue config = 2;
DynamicValue provider_meta = 3;
}
message Response {
DynamicValue state = 1;
repeated Diagnostic diagnostics = 2;
}
}
service Provisioner {
rpc GetSchema(GetProvisionerSchema.Request) returns (GetProvisionerSchema.Response);
rpc ValidateProvisionerConfig(ValidateProvisionerConfig.Request) returns (ValidateProvisionerConfig.Response);
rpc ProvisionResource(ProvisionResource.Request) returns (stream ProvisionResource.Response);
rpc Stop(Stop.Request) returns (Stop.Response);
}
message GetProvisionerSchema {
message Request {
}
message Response {
Schema provisioner = 1;
repeated Diagnostic diagnostics = 2;
}
}
message ValidateProvisionerConfig {
message Request {
DynamicValue config = 1;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ProvisionResource {
message Request {
DynamicValue config = 1;
DynamicValue connection = 2;
}
message Response {
string output = 1;
repeated Diagnostic diagnostics = 2;
}
}

View File

@@ -0,0 +1,403 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Terraform Plugin RPC protocol version 5.3
//
// This file defines version 5.3 of the RPC protocol. To implement a plugin
// against this protocol, copy this definition into your own codebase and
// use protoc to generate stubs for your target language.
//
// This file will not be updated. Any minor versions of protocol 5 to follow
// should copy this file and modify the copy while maintaining backwards
// compatibility. Breaking changes, if any are required, will come
// in a subsequent major version with its own separate proto definition.
//
// Note that only the proto files included in a release tag of Terraform are
// official protocol releases. Proto files taken from other commits may include
// incomplete changes or features that did not make it into a final release.
// In all reasonable cases, plugin developers should take the proto file from
// the tag of the most recent release of Terraform, and not from the main
// branch or any other development branch.
//
syntax = "proto3";
option go_package = "github.com/opentofu/opentofu/internal/tfplugin5";
package tfplugin5;
// DynamicValue is an opaque encoding of terraform data, with the field name
// indicating the encoding scheme used.
message DynamicValue {
bytes msgpack = 1;
bytes json = 2;
}
message Diagnostic {
enum Severity {
INVALID = 0;
ERROR = 1;
WARNING = 2;
}
Severity severity = 1;
string summary = 2;
string detail = 3;
AttributePath attribute = 4;
}
message AttributePath {
message Step {
oneof selector {
// Set "attribute_name" to represent looking up an attribute
// in the current object value.
string attribute_name = 1;
// Set "element_key_*" to represent looking up an element in
// an indexable collection type.
string element_key_string = 2;
int64 element_key_int = 3;
}
}
repeated Step steps = 1;
}
message Stop {
message Request {
}
message Response {
string Error = 1;
}
}
// RawState holds the stored state for a resource to be upgraded by the
// provider. It can be in one of two formats, the current json encoded format
// in bytes, or the legacy flatmap format as a map of strings.
message RawState {
bytes json = 1;
map<string, string> flatmap = 2;
}
enum StringKind {
PLAIN = 0;
MARKDOWN = 1;
}
// Schema is the configuration schema for a Resource, Provider, or Provisioner.
message Schema {
message Block {
int64 version = 1;
repeated Attribute attributes = 2;
repeated NestedBlock block_types = 3;
string description = 4;
StringKind description_kind = 5;
bool deprecated = 6;
}
message Attribute {
string name = 1;
bytes type = 2;
string description = 3;
bool required = 4;
bool optional = 5;
bool computed = 6;
bool sensitive = 7;
StringKind description_kind = 8;
bool deprecated = 9;
}
message NestedBlock {
enum NestingMode {
INVALID = 0;
SINGLE = 1;
LIST = 2;
SET = 3;
MAP = 4;
GROUP = 5;
}
string type_name = 1;
Block block = 2;
NestingMode nesting = 3;
int64 min_items = 4;
int64 max_items = 5;
}
// The version of the schema.
// Schemas are versioned, so that providers can upgrade a saved resource
// state when the schema is changed.
int64 version = 1;
// Block is the top level configuration block for this schema.
Block block = 2;
}
service Provider {
//////// Information about what a provider supports/expects
rpc GetSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response);
rpc PrepareProviderConfig(PrepareProviderConfig.Request) returns (PrepareProviderConfig.Response);
rpc ValidateResourceTypeConfig(ValidateResourceTypeConfig.Request) returns (ValidateResourceTypeConfig.Response);
rpc ValidateDataSourceConfig(ValidateDataSourceConfig.Request) returns (ValidateDataSourceConfig.Response);
rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response);
//////// One-time initialization, called before other functions below
rpc Configure(Configure.Request) returns (Configure.Response);
//////// Managed Resource Lifecycle
rpc ReadResource(ReadResource.Request) returns (ReadResource.Response);
rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response);
rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response);
rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response);
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
//////// Graceful Shutdown
rpc Stop(Stop.Request) returns (Stop.Response);
}
message GetProviderSchema {
message Request {
}
message Response {
Schema provider = 1;
map<string, Schema> resource_schemas = 2;
map<string, Schema> data_source_schemas = 3;
repeated Diagnostic diagnostics = 4;
Schema provider_meta = 5;
ServerCapabilities server_capabilities = 6;
}
// ServerCapabilities allows providers to communicate extra information
// regarding supported protocol features. This is used to indicate
// availability of certain forward-compatible changes which may be optional
// in a major protocol version, but cannot be tested for directly.
message ServerCapabilities {
// The plan_destroy capability signals that a provider expects a call
// to PlanResourceChange when a resource is going to be destroyed.
bool plan_destroy = 1;
}
}
message PrepareProviderConfig {
message Request {
DynamicValue config = 1;
}
message Response {
DynamicValue prepared_config = 1;
repeated Diagnostic diagnostics = 2;
}
}
message UpgradeResourceState {
// Request is the message that is sent to the provider during the
// UpgradeResourceState RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to exist (in the case of resource destruction), be wholly
// known, nor match the given prior state, which could lead to unexpected
// provider behaviors for practitioners.
message Request {
string type_name = 1;
// version is the schema_version number recorded in the state file
int64 version = 2;
// raw_state is the raw states as stored for the resource. Core does
// not have access to the schema of prior_version, so it's the
// provider's responsibility to interpret this value using the
// appropriate older schema. The raw_state will be the json encoded
// state, or a legacy flat-mapped format.
RawState raw_state = 3;
}
message Response {
// new_state is a msgpack-encoded data structure that, when interpreted with
// the _current_ schema for this resource type, is functionally equivalent to
// that which was given in prior_state_raw.
DynamicValue upgraded_state = 1;
// diagnostics describes any errors encountered during migration that could not
// be safely resolved, and warnings about any possibly-risky assumptions made
// in the upgrade process.
repeated Diagnostic diagnostics = 2;
}
}
message ValidateResourceTypeConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ValidateDataSourceConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message Configure {
message Request {
string terraform_version = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ReadResource {
// Request is the message that is sent to the provider during the
// ReadResource RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to be wholly known nor match the given prior state, which
// could lead to unexpected provider behaviors for practitioners.
message Request {
string type_name = 1;
DynamicValue current_state = 2;
bytes private = 3;
DynamicValue provider_meta = 4;
}
message Response {
DynamicValue new_state = 1;
repeated Diagnostic diagnostics = 2;
bytes private = 3;
}
}
message PlanResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue proposed_new_state = 3;
DynamicValue config = 4;
bytes prior_private = 5;
DynamicValue provider_meta = 6;
}
message Response {
DynamicValue planned_state = 1;
repeated AttributePath requires_replace = 2;
bytes planned_private = 3;
repeated Diagnostic diagnostics = 4;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 5;
}
}
message ApplyResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue planned_state = 3;
DynamicValue config = 4;
bytes planned_private = 5;
DynamicValue provider_meta = 6;
}
message Response {
DynamicValue new_state = 1;
bytes private = 2;
repeated Diagnostic diagnostics = 3;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 4;
}
}
message ImportResourceState {
message Request {
string type_name = 1;
string id = 2;
}
message ImportedResource {
string type_name = 1;
DynamicValue state = 2;
bytes private = 3;
}
message Response {
repeated ImportedResource imported_resources = 1;
repeated Diagnostic diagnostics = 2;
}
}
message ReadDataSource {
message Request {
string type_name = 1;
DynamicValue config = 2;
DynamicValue provider_meta = 3;
}
message Response {
DynamicValue state = 1;
repeated Diagnostic diagnostics = 2;
}
}
service Provisioner {
rpc GetSchema(GetProvisionerSchema.Request) returns (GetProvisionerSchema.Response);
rpc ValidateProvisionerConfig(ValidateProvisionerConfig.Request) returns (ValidateProvisionerConfig.Response);
rpc ProvisionResource(ProvisionResource.Request) returns (stream ProvisionResource.Response);
rpc Stop(Stop.Request) returns (Stop.Response);
}
message GetProvisionerSchema {
message Request {
}
message Response {
Schema provisioner = 1;
repeated Diagnostic diagnostics = 2;
}
}
message ValidateProvisionerConfig {
message Request {
DynamicValue config = 1;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ProvisionResource {
message Request {
DynamicValue config = 1;
DynamicValue connection = 2;
}
message Response {
string output = 1;
repeated Diagnostic diagnostics = 2;
}
}

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