# FIKO-AI — PHASE 1 API

Temel adres: `http://localhost:8080` (`.env` → `HTTP_ADDR`). Tüm istekler JSON.
Kullanıcı: **üyelik kapalıyken** (`FIKO_USERS_ENABLED=false`, varsayılan) kimlik doğrulama yok; istek `X-Fiko-User` başlığıyla kullanıcı seçer. **Üyelik açıkken** kullanıcıyı sunucu oturum token'ından çözer ve bu başlık YOK SAYILIR (bkz. «Üyelik ve oturum»). Üyelik kapalıyken başlık da yoksa `CHAT_DEFAULT_USER_ID` (varsayılan `local`) kullanılır; üyelik açıkken oturumsuz istek `401` alır.
**Sahiplik denetlenir:** başka kullanıcının konuşması/akışı/yürütmesi `404` döner (varlığı bile sızdırılmaz).

## Hata gövdesi

WS `error` olayıyla aynı alan adları:

```json
{ "error": { "code": "not_found", "message": "konuşma bulunamadı" } }
```

| Durum | HTTP | `code` |
|---|---|---|
| Geçersiz gövde / sorgu | 400 | `bad_request` |
| Skill/tool alan doğrulaması | 400 | `validation_error` (+ `fields`) |
| Tool **girdi** doğrulaması | 400 | `validation_error` (+ `fields`) |
| Kimlik doğrulanamadı | 401 | `unauthorized` |
| Konuşma/skill/tool yok ya da başkasının | 404 | `not_found` |
| Tool için yetki yok | 403 | `permission_denied` |
| Çakışma (aynı ad, kapalı tool) | 409 | `conflict` |
| Tool için uygulama yok | 501 | `not_implemented` |
| Mesaj çok büyük | 413 | `too_large` |
| Akış alan doğrulaması (yapısal) | 400 | `validation_error` (+ `fields`) |
| Akış kaydedilebilir ama **çalıştırılamaz** | 409 | `validation_error` (+ `fields`, `runErrors`) |
| Akışta sunucuya bağlı olmayan düğüm türü | 501 | `not_implemented` |
| Eşzamanlı yürütme sınırı dolu | 429 | `conflict` |
| Bitmiş yürütmeyi iptal etme | 409 | `conflict` |
| Akış/yürütme yok ya da başkasının | 404 | `not_found` |
| LLM sağlayıcı / tool hatası | 502 | `llm_error` / `internal` |
| Tool zaman aşımı | 504 | `llm_error` |
| Sunucu hatası / panic | 500 | `internal` |
| LLM/skill/tool/akış yapılandırılmamış | 503 | `internal` |
---

## GET /api/health

Sunucu hazır mı. LLM'e istek atmaz.

```bash
curl http://localhost:8080/api/health
```
```json
{ "status": "ok", "env": "development", "version": "0.1.0", "uptimeSeconds": 42,
  "time": "2026-10-02T13:52:14Z", "wsPath": "/ws", "wsConnections": 0,
  "llmProvider": "ollama" }
```
`status` = `degraded` ise WS veya LLM katmanı bağlı değildir.

## GET /api/models

```bash
curl http://localhost:8080/api/models
```
```json
{ "provider": "ollama", "defaultModel": "qwen2.5:7b",
  "models": [ { "name": "qwen2.5:7b", "sizeBytes": 4683079371, "modifiedAt": "2026-10-02T12:01:46Z" } ] }
```
Ollama erişilemezse `502` + `llm_error`.

`defaultModel` **ayarlarda kayıtlı (o an gerçekte üretimde kullanılan)** modeldir; ayar servisi yoksa `.env` (`OLLAMA_MODEL`) bildirilir.

---

## GET /api/system

Sunucunun **yetenekleri ve anahtarları** (salt okunur; hiçbir ayarı değiştirmez).
Arayüzün "Sistem" ekranı bu ucu okur: kullanıcı bir düğmeye tıklayıp neden çalışmadığını
tahmin etmek zorunda kalmaz.

```json
{ "status": "ok", "env": "local", "version": "1.0.0",
  "features": [
    { "key": "stt", "title": "Konuşma tanıma (STT)", "group": "Ses",
      "enabled": true, "configured": false, "provider": "whispercpp",
      "detail": "STT_URL boş",
      "hint": "STT_URL'i motor adresiyle doldur, sonra STT_ENABLED=true" }
  ],
  "permissions": { "user": "local", "scope": "default",
    "policy": { "grants": ["execute"], "allowHighRisk": false, "allowCritical": false },
    "known": ["read","write","execute","delete","system"], "users": [] } }
```

| Alan | Anlamı |
| --- | --- |
| `enabled` | Sunucuda açık mı (bilinçli tercih) |
| `configured` | Açık olmak için gereken ayarlar tam mı |
| `hint` | Eksikse kullanıcı ne yapmalı |

`enabled` ile `configured` **ayrıdır**: "kapalı" ile "eksik ayarlı" farklı durumlardır ve
kullanıcıya farklı iş yaptırır. Gizli değerler (API anahtarları) asla gönderilmez —
yalnızca "var/yok" bilgisi verilir.

---
## Konuşmalar

### POST /api/conversations

Yeni konuşma. Gövde opsiyoneldir; verilmezse başlık `"Yeni konuşma"` olur ve ilk kullanıcı mesajından türetilir. → `201 Created`

```bash
curl -X POST http://localhost:8080/api/conversations \
  -H 'Content-Type: application/json' -d '{"title":"Haftalık plan"}'
```
```json
{ "conversation": { "id": "conv_533bca...", "title": "Haftalık plan",
  "sessionId": "sess_7dbe4...", "createdAt": "...", "updatedAt": "..." } }
```

---

## Skill'ler

Skill, yapay zekânın **ne yapabildiğini** tanımlar; gerçek işi Tool yapar (Skill ≠ Tool). Mongo'da tutulur, arayüzden yönetilir. Her yazma işlemi denetim kaydına düşer (`skill.create|update|status|duplicate|delete`, başarısızlar dahil) ve `SkillCreated/Updated/Deleted` olayı yayınlanır.

Zarf: tek skill `{ "skill": {...} }`; liste `{ "items": [...], "count", "limit", "offset" }`. `id`, `createdAt`, `updatedAt` ve `embedding` **sunucuya aittir** (gövdede verilse de yok sayılır; embedding hiçbir zaman dönmez).

| Alan | Kural |
|---|---|
| `name` | zorunlu, benzersiz; `^[a-z][a-z0-9_]{1,63}$` (snake_case) |
| `description` | zorunlu, ≤2000 karakter (vektör aramanın temeli) |
| `version` | `MAJOR.MINOR.PATCH`; boşsa `1.0.0` |
| `status` | `draft` (varsayılan) · `active` · `disabled` |
| `executorType` | `tool` (varsayılan) · `llm` |
| `executorConfig` | `active` iken zorunlu: `tool` → `{"tool":"..."}`, `llm` → istem |
| `parameters[]` | `name`, `type`, `required`; ≤20 |
| `examples[]` | `input` (zorunlu, ≤500 karakter), `arguments`; ≤20 |
| `permissions[]` | örn. `catia.write`; ≤20 |
| `tags[]` | küçük harfe çevrilir, tekilleştirilir; ≤20 |

### POST /api/skills

**201** + `{ "skill": {...} }`. Geçersizse **400** `validation_error`, ad alınmışsa **409** `conflict`.

```bash
curl -X POST http://localhost:8080/api/skills -H 'Content-Type: application/json' -d '{
  "name": "create_plane", "description": "Offset plane oluşturur.", "status": "active",
  "executorType": "tool", "executorConfig": {"tool": "catia.create_plane"},
  "parameters": [{"name": "offset", "type": "number", "required": true}],
  "tags": ["catia"] }'
```

### Diğer skill uçları

- **GET /api/skills?status=&tag=&q=&limit=&offset=** — ada göre sıralı. `status` geçersizse 400. `tag` tam eşleşme. `q` ad/açıklamada **düz metin** aramadır (regex kaçırılır).
- **GET /api/skills/:id** — yoksa 404.
- **PUT /api/skills/:id** — gövdedeki alanlarla **tamamen** değiştirir (kimlik, `createdAt`, embedding korunur).
- **PUT /api/skills/:id/status** `{"status":"active"}` — tüm kurallar yeniden denetlenir; executor tanımsız taslak `active` yapılamaz (400). Boş `status` 400.
- **POST /api/skills/:id/duplicate** — `<ad>_copy`, `<ad>_copy2`, ... adıyla **taslak** kopya. **201**.
- **DELETE /api/skills/:id** — **204**; yoksa 404.
- **POST /api/skills/validate[?id=:id]** — kaydetmeden doğrular. Geçersizlik hata değil **sonuçtur**: her zaman **200**. `id` verilirse (güncelleme) o skill'in kendi adı çakışma sayılmaz.

```json
{ "valid": false, "errors": [ { "field": "name", "message": "bu ada sahip bir skill zaten var" } ] }
```
Geçerliyse `{ "valid": true, "errors": [] }` (`null` değil).

---

### POST /api/skills/search

Aktif skill'ler arasında arar (Agent'a tüm skill'ler yerine yalnızca ilgili olanları vermek için). Taslak ve kapalı skill'ler **asla** dönmez. Skor 0..1, azalan sıralı. Embedding dönmez.

```json
{ "query": "30 mm geriye plane koy", "mode": "hybrid", "limit": 5, "minScore": 0 }
---

## Tool'lar

Tool = gerçek işi yapan dış çağrı (skill "ne yapılabilir" tanımıdır, tool yapar). Mongo'da saklanır; uygulamaları (handler) çalışma zamanında Go koduyla bağlanır.

| Alan | Açıklama |
| --- | --- |
| `id`, `name` | `name` benzersiz ve noktalı: `catia.create_plane`, `fiko.echo` |
| `title`, `description`, `version` | Tanım metni (LLM tool listesinde görünür) |
| `status` | `enabled` \| `disabled` |
| `kind` | `builtin` (Go uygulaması bağlı) \| `custom` (bu fazda **yürütülemez**) |
| `inputSchema`, `outputSchema` | JSON Schema (`type: "object"`); girdi doğrulaması bununla yapılır |
| `permissions` | `read`, `write`, `execute`, `delete`, `system` (çalıştırmak için `execute` **her zaman** gerekir) |
| `risk` | `low` \| `medium` \| `high` \| `critical` |
| `timeoutMs`, `retry` | Zaman aşımı (sunucu üst sınırı `TOOL_MAX_WAIT`) ve tekrar deneme politikası |
| `config` | Uygulamaya özel ayarlar |
| `tags`, `builtin` | Gruplama; `builtin` kayıtları **silinemez** (yalnızca kapatılır) |
| `ui` | Çıktının arayüzde **hangi görünümde** sunulacağı: `auto` \| `json` \| `table` \| `file` \| `list` \| `code` \| `metrics` \| `form` |

### `ui` — tool/sistem türüne göre arayüz (Faz 5)

Her tool çıktısı bir **görünüm** ile sunulur; arayüz JSON'dan farklı davranır:

| Görünüm | Nerede kullanılır | Davranış |
| --- | --- | --- |
| `table` | `biz.*`, `memory.*`, `knowledge.*`, `fs.list`, `fs.search`, `unit.list` | Aranabilir, sıralanabilir satırlar; satır genişletme + ham JSON |
| `file` | `fs.read_text`, `fs.write_text`, `web.fetch` | Okunabilir metin, kopyalama, kısaltma uyarısı |
| `list` | `web.search` | Bağlantılı sonuç kartları + özet |
| `code` | `sys.exec` | `stdout`/`stderr` **ayrı** bloklar |
| `metrics` | `biz.summary`, `biz.vat`, `web.exchange_rate`, `date.workdays` | Sayısal kartlar |
| `form` | `fs.stat`, `calc.eval`, `text.stats`, `unit.convert`, `date.diff`, `date.add` | Küçük düz sonuçlar — anahtar–değer listesi |
| `json` | Diğer her şey | Ham çıktı (her zaman çalışır) |

**Kural:** `ui` verilmezse `auto` sayılır ve **sunucu** ad ön ekinden türetir
(`biz/memory/knowledge` → table, `fs` → file, `web` → list, `sys` → code, diğer → json).
`GET /api/tools` ve `POST /api/tools/:id/execute` yanıtında `ui` **çözülmüş** değer olarak döner —
istemci türetme yapmaz. WS `tool.result` olayı da `ui` taşır ve kalıcı kayda yazılır
(sayfa yenilenince aynı görünüm açılır).

Yerleşik tool'lar (`TOOL_FS_ROOT` altında çalışır, kök dışı yollar reddedilir):

| Tool | İzin | Risk | Açıklama |
|---|---|---|---|
| `fiko.echo` | execute | low | Girdiyi döndürür (hat sınaması) |
| `fiko.now` | execute | low | Sunucu saati (isteğe bağlı `timezone`) |
| `fs.read_text` | read | low | Kök altından metin okur |
| `fs.write_text` | write | medium | Kök altına yazar (`append` destekler) |
| `fs.delete` | delete | high | Kök altından siler |
| `fs.list` | read | low | Klasör içeriğini listeler (`ls`) — `ui: table` |
| `fs.search` | read | low | Ada veya içerikte (`contentRegex`) arar (`grep -r`) — `ui: table` |
| `fs.stat` | read | low | Boyut/tür/tarih; olmayan yolda `exists:false` — `ui: form` |
| `calc.eval` | execute | low | Aritmetik ifade hesaplar — `ui: form` |
| `text.stats` | execute | low | Kelime/cümle/okuma süresi — `ui: form` |
| `unit.convert` | execute | low | Birim dönüştürür (mm↔inç, °C↔°F, kg↔lb) — `ui: form` |
| `unit.list` | execute | low | Desteklenen birim grupları — `ui: table` |
| `date.diff` | execute | low | Tarih farkı (gün/hafta/iş günü/ay) — `ui: form` |
| `date.add` | execute | low | Tarihe gün/hafta/ay/yıl ekler — `ui: form` |
| `date.workdays` | execute | low | İş günü (hafta sonu + tatil hariç) — `ui: metrics` |
| `sys.exec` | system | critical | **Varsayılan YOK**: `TOOL_EXEC_ENABLED=true` **ve** `TOOL_EXEC_ALLOW` doluysa tanımlanır |

**Dosya adı bilmeden çalışmak:** `fs.read_text` yol ister. Yolu bilmiyorsanız önce
`fs.list` (klasörün içi) ya da `fs.search` (kökün tamamı) çağırın:

```bash
# 1) Var mı, ne var?
curl -X POST http://localhost:8080/api/tools/fs.list/execute \
  -H 'Content-Type: application/json' -d '{"input":{"path":"notlar"}}'

# 2) İçerikle ara (kökün tamamı; "path" boşsa tüm kök taranır)
curl -X POST http://localhost:8080/api/tools/fs.search/execute \
  -H 'Content-Type: application/json' -d '{"input":{"query":"fiyat","contentRegex":true}}'
```

**Not — `fs.*` izinleri:** bu tool'lar `read` izni ister. Varsayılan politikada yalnızca
`execute` açıktır; `read`'i **Tools → İzinler** ekranından açmanız gerekir (güvenlik
varsayılanıdır). Arama çıktısı sınıra takılırsa son satırda
`(sonuçlar sınırlandı, ~N girdi tarandı)` yazar — sonuç sessizce kesilmez.

**Hesaplama:** `calc.eval` **kod çalıştırmaz**. İfade kendi özyinelemeli çözümleyicisiyle
ayrıştırılır; `+ - * / ^ %`, parantez, `sqrt log ln exp sin cos tan round ceil floor
abs min max pi` ve `variables` ile adlandırılmış sayılar desteklenir. `import`, `open`,
`eval` gibi ifadeler **hata** döner (kod kaçış yolu yoktur). Derinlik 64 ile sınırlıdır.

```bash
curl -X POST http://localhost:8080/api/tools/calc.eval/execute \
  -H 'Content-Type: application/json' \
  -d '{"input":{"expression":"(1250*3)/4 + sqrt(16)","variables":{"adet":3}}}'
```

### Uzak (HTTP) tool'lar — `kind: http`

`config` alanıyla uzak servisi çağırır; uygulama **SSRF korumalı istemciyle üretilir** — ek kod gerekmez.

```json
{ "name": "catia.create_plane", "kind": "http", "risk": "medium",
  "permissions": ["execute"],
  "inputSchema": {"type": "object", "properties": {"ad": {"type": "string"}}},
  "config": { "method": "POST", "url": "http://catia-sunucu:8080/api/plane",
    "headers": {"X-Api-Key": "..."}, "bodyTemplate": "{\"ad\":\"{{ad}}\"}" } }
```

| Alan | Açıklama |
| --- | --- |
| `method` | `GET/POST/PUT/PATCH/DELETE`; boşsa `GET` |
| `url` | Zorunlu. `{{alan}}` yer tutucuları girdiyle doldurulur |
| `headers` | Sabit başlıklar; `{{alan}}` desteklenir (CRLF reddedilir) |
| `bodyTemplate` | İstek gövdesi; boşsa gövde gönderilmez |
| `result` | `text` ise gövde metin döner; varsayılanda `json` da ayrıştırılır |

Yanıt: `{"status":200,"body":"…","json":{…}}`; sınırdan büyük gövdeler `truncated: true` ile kısaltılır.
**Hata sınıflandırması:** `4xx` kalıcı (tekrar denenmez), `5xx`/bağlantı hatası geçici (retry çalışır).

**Güvenlik (SSRF):** varsayılan olarak loopback, `10/8`, `172.16/12`, `192.168/16`, `169.254.169.254` (cloud metadata), `100.64/10` ve `::1` **reddedilir**; yalnızca `http`/`https` kabul edilir; denetim **bağlanılan IP** üzerinden yapılır (DNS rebinding'e kapalı). Kapatmak için `TOOL_HTTP_ALLOW_PRIVATE=true` — **üretimde kapalı tut**.

#### Yerel SearXNG ile `web.search` (önerilen kurulum)

Bu kurulumda `web.search` **korumayı gevşetmeden** yerel SearXNG'ye bağlanır. Sağlayıcı değişkenleri:

| Değişken | Açıklama |
| --- | --- |
| `WEB_SEARCH_PROVIDER` | `duckduckgo` (varsayılan) \| `searxng` \| `brave` |
| `WEB_SEARCH_URL` | searxng örnek adresi (örn. `http://127.0.0.1:8181`) |
| `WEB_SEARCH_KEY` | yalnızca `brave` için API anahtarı |

`WEB_SEARCH_PROVIDER=searxng` iken `web.search`, **yalnızca `WEB_SEARCH_URL` host'ına** bağlanabilen sabitlenmiş (pinned) bir istemci kullanır (`tool.NewPinnedClient`). `web.fetch` ise kendi SSRF korumalı istemcisini korur: kullanıcı/model verdiği adreslere loopback'e gidemez.

> **Gerekçe:** `TOOL_HTTP_ALLOW_PRIVATE=true` tüm web tool'larına özel ağ erişimi açardı. Bunun yerine izin yalnızca operatörün yapılandırdığı tek arama adresine verilir; saldırgan model üzerinden iç ağa (`127.0.0.1`, cloud metadata) ulaşamaz.

**SearXNG tarafı (Docker) zorunlu ayarlar:**

1. `settings.yml` içinde JSON çıktısı **açık** olmalı, aksi halde `format=json` isteğine `403` döner:
   ```yaml
   search:
     formats:
       - html
       - json
   ```
2. Bu makinede `brave`, `duckduckgo`, `google` ve `startpage` motorları **engellenmiş** durumda (`CAPTCHA` / `access denied` / `too many requests`). Doğrulanan çalışan motorlar: `bing`, `yahoo`, `mojeek`, `mwmbl` — `settings.yml` içinde `disabled: false` yapılmış olmalı.
3. Port eşlemesi `host:konteyner` yazılır. SearXNG konteyner içinde `SEARXNG_PORT` (varsayılan 8181) dinler; `8181:8181` çalışır.

### GET /api/tools?status=&kind=&risk=&tag=&q=&limit=&offset=

Ada göre sıralı liste. Her kayıt, çağıran kullanıcının bakış açısından üç ek alan taşır:

```json
{ "items": [ { "name": "fs.read_text", "permissions": ["read"], "risk": "low",
               "executable": true, "allowed": false, "missing": ["read"] } ],
  "count": 5, "limit": 50, "offset": 0 }
```
`executable` = uygulaması bağlı mı, `allowed` = bu kullanıcı yetkili mi, `missing` = eksik izinler (risk kapısı kapalıysa `risk:high` gibi).

---

## İzinler

### GET /api/permissions

Çağıran kullanıcının tool yetkileri.

```json
{ "user": "local", "scope": "default",
  "policy": { "grants": ["execute"], "allowHighRisk": false, "allowCritical": false },
  "known": ["read", "write", "execute", "delete", "system"], "users": [] }
```

### PUT /api/permissions

```json
{ "grants": ["execute", "read", "write"], "allowHighRisk": false, "allowCritical": false }
```

**Tüm küme değiştirilir** (PATCH yok): `grants` içinde `execute` yoksa hiçbir tool çalışmaz. Politika kullanıcıya özeldir (`scope: "user"`); tanımsız kimlikler `TOOL_DEFAULT_PERMISSIONS` ve `TOOL_ALLOW_*` değerlerini kullanır. Bilinmeyen izin `400 bad_request`. Denetim kaydına yazılır.

**Güvenlik varsayılanı:** yalnızca `execute`; `read/write/delete/system` ve `high/critical` risk kullanıcı tarafından **açıkça** açılmalıdır (spec: "AI kendisine sınırsız yetki vermemeli").

---

## Akışlar (Agent Builder — Faz 9)

Kullanıcının **kod yazmadan** çizdiği iş akışı: `nodes` (düğümler) + `edges` (bağlantılar).
Her yürütme adım adım kaydedilir (`steps`) ve kullanıcıya aittir: başkasının akışı/yürütmesi `404`
döner (varlığı bile sızdırılmaz). Yazma işlemleri denetim kaydına düşer
(`workflow.create|update|duplicate|delete|run|cancel`).

### Kaydetme ve çalıştırma arasındaki fark (en önemli kural)

| | Kaydetme | Çalıştırma |
|---|---|---|
| Zorunlu | yalnızca **yapısal** kurallar | + **çalıştırılabilirlik** (`Check`) |
| Taslak (ör. output'suz) | ✅ kaydedilir | ❌ `409` |
| Sunucuya bağlı olmayan düğüm (ör. LLM yokken `llm`) | ✅ kaydedilir | ❌ `501` |

Arayüz bu ayrımı `POST /api/workflows/validate` ile canlı gösterir: `valid` = kaydedilebilir,
`runnable` = çalıştırılabilir. Kullanıcı yarım akışını kaybetmez.

### Düğüm türleri

`input`, `memory`, `rag`, `skill`, `llm`, `tool`, `condition`, `loop`, `transform`, `output`.

`memory/rag/skill/llm/tool` **yetenek** ister (sunucudaki `Deps`). Bağlı değilse akış yine de
kaydedilir, yalnızca çalıştırılamaz. Hangi türlerin çalıştırılabilir olduğunu sunucu bildirir
(`meta.available`).

### Bağlantı kuralları (sunucu doğrulayan)

- Her `(düğüm, dal)` çıkışı **bir kez** kullanılır. `condition` → `true`/`false`, `loop` → `body`/`` (döngü sonrası).
- `input`'a bağlantı **girilemez**, `input`'tan çıkış bağlanamaz (akış `input`'tan başlar).
- **Döngü (graf döngüsü) yasaktır** — tekrar için `loop` düğümü kullanılır.
- Kenarlarda döngü bulunmazsa akış doğrusal + dallanmalıdır; yürütme tek sırada ilerler.

### Düğüm config alanları

| Düğüm | Alanlar |
|---|---|
| `llm` | `prompt`*, `system`, `model`, `temperature` |
| `transform` | `template`*, `json` |
| `output` | `template` (boşsa `{{prev}}`) |
| `condition` | `left`*, `op`*, `right` (sayısal karşılaştırmada sayı olmalı) |
| `loop` | `items`* (JSON dizi ya da satır satır metin) |
| `rag` | `query`*, `limit` (≤10) |
| `skill` | `query`* |
| `memory` | `limit` (≤50) |
| `tool` | `name`* (Tool Registry'deki ad) |

`*` = zorunlu (boşsa **kaydetme** engellenmez, **çalıştırma** engellenir).
`condition.op` değerleri: `eq`, `ne`, `contains`, `not_contains`, `starts_with`, `ends_with`,
`gt`, `gte`, `lt`, `lte`, `empty`, `not_empty` (liste `meta.ops` ile gelir).

### Şablon değişkenleri

Config metinlerinde `{{değişken}}` kullanılır:

| Değişken | Anlamı |
|---|---|
| `input` | çalıştırma girdisi |
| `prev` | son çalışan düğümün çıktısı (koşul hariç) |
| `<nodeId>` / `<nodeId>.alan` / `<nodeId>.liste.1` | o düğümün çıktısı; nesne/dizi alan erişimi |
| `item`, `index` | `loop` gövdesinde geçerli öğe ve 0 tabanlı sıra |
| `conversationId` | çalıştırmanın konuşması |

Bilinmeyen kök değişken **hatadır** (yazım hatası sessizce boş metne dönüşmez).
Henüz çalışmamış bir düğümün değişkeni ise boş metindir (hata değil).
Bu değişken adları düğüm kimliği olarak **kullanılamaz** (`meta.reserved`).

### GET /api/workflows?q=&limit=&offset=

En son güncellenen önce. `q` ad/açıklamada büyük-küçük harf duyarsız alt dizedir (düz metin;
regex özel karakterleri kaçırılır).

```json
{ "items": [ { "id": "wf_9f2a...", "userId": "local", "name": "Rapor özeti",
    "description": "", "nodes": [ … ], "edges": [ … ],
    "createdAt": "…", "updatedAt": "…" } ],
  "count": 3, "limit": 50, "offset": 0 }
```

### POST /api/workflows

`id`, `userId`, `createdAt`, `updatedAt` **sunucuya aittir** (gövdedekiler yok sayılır). `201 Created`.

```bash
curl -X POST http://localhost:8080/api/workflows \
  -H 'Content-Type: application/json' -H 'X-Fiko-User: local' \
  -d '{"name":"Rapor özeti","nodes":[{"id":"in","type":"input","position":{"x":0,"y":0}},
       {"id":"out","type":"output","position":{"x":280,"y":0},"config":{"template":"{{prev}}"}}],
       "edges":[{"id":"e1","from":"in","to":"out","branch":""}]}'
```

### Diğer akış uçları

| Uç | Not |
|---|---|
| `GET /api/workflows/:id` | Akış + düğüm/kenarlar |
| `PUT /api/workflows/:id` | **TAM** değiştirme (`id`/`userId`/`createdAt` korunur) |
| `DELETE /api/workflows/:id` | Akış **+ yürütme geçmişi** silinir; süren yürütmeler iptal edilir |
| `POST /api/workflows/:id/duplicate` | `"<ad> (kopya)"` adıyla yeni akış, `201` |
| `POST /api/workflows/validate` | **Her zaman `200`** — geçersizlik hata değil *sonuçtur* |
| `GET /api/workflows/meta` | Palet, operatörler, ayrılmış değişkenler, sunucu sınırları |

### POST /api/workflows/validate

Kaydetmeden denetler (arayüzün canlı doğrulaması). `errors`/`runErrors` **dizi** olur (`null` değil).

```json
{ "valid": true, "runnable": false,
  "errors": [],
  "runErrors": [ { "field": "nodes[2]", "message": "tam olarak bir input node'u gerekli (var: 0)" } ] }
```

### GET /api/workflows/meta

Arka plan/eksik alt sistemde sunucunun yeteneklerini bildirir; arayüz paleti buna göre kurar.

```json
{ "available": { "llm": true, "tool": true, "rag": false, "skill": true, "memory": true,
                 "input": true, "condition": true, "loop": true, "transform": true, "output": true },
  "ops": ["eq","ne","contains","gt", …],
  "reserved": ["input","prev","item","index","conversationId"],
  "limits": { "maxNodes": 100, "maxEdges": 300, "maxSteps": 200, "maxLoopIterations": 50,
              "maxInputRunes": 20000, "runTimeoutSeconds": 300, "maxConcurrent": 4 } }
```

`available[t] = false` → o türdeki düğüm **kaydedilebilir ama çalıştırılamaz**.
`limits` sunucu sınırlarıdır; `WORKFLOW_MAX_CONCURRENT` / `WORKFLOW_RUN_TIMEOUT` ile ayarlanır.

### POST /api/workflows/:id/run

Akışı **arka planda** başlatır, `202 Accepted` + `"running"` kaydı döner (istek kilitlenmez).
İlerleme `GET /api/workflows/runs/:id` ile izlenir.

```bash
curl -X POST http://localhost:8080/api/workflows/wf_9f2a/run \
  -H 'Content-Type: application/json' -H 'X-Fiko-User: local' \
  -d '{"input":"bu çeyreğin özetini çıkar","conversationId":"conv_533b..."}'
```

**Yürütme başarısızlıkları HTTP'de görünür, yürütme İÇİ hatalar kayıttadır:** bir `llm` düğümü
LLM'den boş yanıt alırsa istek `202` ile döner, hata `execution.error` ve `steps[].error`
alanlarına yazılır. `409` = çalıştırılamaz (yapısal/yetenek eksikliği değil), `429` = eşzamanlılık
sınırı dolu, `501` = sunucuda gerekli yetenek bağlı değil.

### Yürütme kaydı

```json
{ "execution": { "id": "exec_1a2b...", "userId": "local",
    "workflowId": "wf_9f2a...", "workflowName": "Rapor özeti",
    "conversationId": "conv_533b...", "input": "bu çeyreğin özetini çıkar",
    "output": "… nihai çıktı …", "status": "succeeded",
    "steps": [ { "index": 1, "nodeId": "in", "nodeType": "input", "status": "ok",
                 "output": "…", "startedAt": "…", "finishedAt": "…", "durationMs": 2 },
               { "index": 2, "nodeId": "out", "nodeType": "output", "status": "ok",
                 "durationMs": 1, "iteration": 0 } ],
    "toolCalls": 1, "promptTokens": 412, "completionTokens": 96,
    "startedAt": "…", "finishedAt": "…", "latencyMs": 1840 } }
```

`status`: `running` → `succeeded` | `failed` | `cancelled`.
`steps[].status`: `running` | `ok` | `error`. `iteration` döngü gövdesinde 1'den başlar
(döngü dışında `0`). `step.input`/`output` önizlemedir (kaydı şişirmemek için kırpılır).

| Uç | Not |
|---|---|
| `GET /api/workflows/runs?workflowId=&status=&limit=&offset=` | En yeni önce; akış silinse de geçmiş okunur |
| `GET /api/workflows/runs/:id` | Tek yürütme + adımlar |
| `POST /api/workflows/runs/:id/cancel` | **Asenkron**: dönen kayıt hâlâ `running` olabilir, kesin durum `GET` ile okunur |

**İptal** bitmiş yürütmede `409` döner. **Sunucu çökmesi:** açılışta `running` kalmış yürütmeler
`failed` yapılır ve yarım kalan adımlar kapatılır (`sunucu yeniden başlatıldı; yürütme yarım kaldı`) —
arayüzde sonsuza dek "çalışıyor" görünmez.

### Güvenlik

Akış, ajanın aşamayacağı bir kapıyı **aşamaz**: `tool` düğümü `tool.Executor`'a gider (yetki, risk
kapıları, şema doğrulaması, denetim kaydı) ve yetki çağıran kullanıcı kimliğine göre denetlenir.


## Yetkilendirme

| Durum | Davranış |
|---|---|
| `FIKO_API_KEY` **boş** | Doğrulama yapılmaz. Sistem **açık** kabul edilir; `X-Fiko-User` taklit edilebilir. `GET /api/system` → `auth.enabled=false` + uyarı. |
| `FIKO_API_KEY` **dolu** | Korumalı uçlar `X-Fiko-Auth` başlığını zorunlu tutar; eksik/yanlış → **401**. |

```bash
# korumalı istek
curl -H "X-Fiko-Auth: $FIKO_API_KEY" http://localhost:8080/api/conversations
```

**Korumalı olanlar:** `/conversations`, `/settings`, `/skills`, `/tools`, `/permissions`,
`/memories`, `/knowledge`, `/workflows`, `/audit` ve `WS /ws`.

**Korumasız olanlar:** `/api/health`, `/api/models`, `/api/system` — yalnızca yapılandırma
ve sürüm bilgisi döner, kullanıcı verisi içermez; sağlık izleme (health probe) anahtarsız
çalışabilmelidir.

**`X-Fiko-User` tek başına kimlik SAYILMAZ.** Anahtar tanımlıysa yalnızca anahtarı bilen
istemci kullanıcı adı seçebilir. Anahtar boşken bu başlık yalnızca çok kullanıcılı
deneme içindir ve güvenlik sınırı DEĞİLDİR.

**WebSocket:** tarayıcı özel başlık gönderemediği için anahtar `?key=` ile taşınır
(`ws://localhost:8080/ws?key=...`). Anahtar URL'de görünür ve sunucu erişim günlüğüne
sızabilir — bu bir zorunluluktur; anahtar yoksa parametre hiç eklenmez.

**Kapsam:** bu bölüm yalnızca **paylaşılan anahtar** modunu anlatır (üyelik kapalı). Parolayla giriş, oturum, rol ve üye bazlı yetki için bir sonraki bölüme bak: **Üyelik ve oturum**.

## Üyelik ve oturum

`FIKO_USERS_ENABLED=true` iken parolayla gerçek giriş vardır: sunucu kimliği **kendisi doğrular** ve
token verir. `X-Fiko-User` başlığı artık yalnızca bir istemci iddiasıdır ve **yok sayılır** (taklit edilemez).
Kapalıyken `/api/auth/*` uçları `404` döner ve eski akış aynen çalışır.

### Kimlik çözümleme sırası

| Sıra | Kaynak | Not |
|---|---|---|
| 1 | Oturum token'ı | **Gerçek kimlik.** Kullanıcı adını sunucu belirler |
| 2 | `X-Fiko-Auth` (`FIKO_API_KEY`) | İşletmecinin paylaşılan anahtarı → **yönetici** sayılır |
| 3 | `X-Fiko-User` | YALNIZCA üyelik **kapalıyken** |

Token şu yerlerden okunur (ilk bulunan kazanır): `Authorization: Bearer <token>` → `fiko_session` çerezi
(HttpOnly, SameSite=Lax) → **yalnızca `/ws` yolunda** `?token=`. REST'te URL'ye token koymak erişim
günlüklerine sızacağı için orada KABUL EDİLMEZ.

- Geçerli oturum token'ı olan üyeden ayrıca `X-Fiko-Auth` istenmez.
- Token var ama geçersiz/süresi dolmuş → korumalı uçta **`401`** (`400` değil); doğru API anahtarı bunu kurtarmaz.
- Oturumsuz istek → `401` «giriş gerekli».
- **Kimlik istemeyen uçlar:** `OPTIONS` (CORS ön kontrolü), `GET /api/health`, `/api/models`, `/api/system`,
  `/api/auth/*`. Arayüz giriş ekranını açıp açmayacağını `/api/system` → `auth.users` ile öğrenir.

### POST /api/auth/register · POST /api/auth/login

```bash
curl -X POST http://localhost:8080/api/auth/register -H 'Content-Type: application/json' \
  -d '{"username":"ayse","password":"en-az-bir-parola","displayName":"Ayşe"}'
```
```json
{ "token": "…", "expiresAt": "2026-11-03T10:00:00Z",
  "user": { "id": "usr_…", "username": "ayse", "role": "user", "disabled": false,
            "displayName": "Ayşe", "createdAt": "2026-10-04T10:00:00Z", "lastLoginAt": "2026-10-04T10:00:00Z" } }
```

- **Kurallar:** kullanıcı adı 3–32 karakter, yalnızca `a-z 0-9 _ . -` (kırpılır, küçük harfe çevrilir); parola 8–128 karakter.
- Kayıt **otomatik oturum açar** (aynı yanıt biçimi). Token yalnızca burada düz metin verilir; sunucu **özetini** saklar.
- **İlk hesap yönetici olur:** hiç hesap yokken açılan ilk kayıt `admin` rolü alır, sonrakiler `user`.
- Kullanıcı adı büyük/küçük harfe duyarsızdır (aynı hesap). Parola bcrypt ile saklanır; yanıtta asla yoktur.
- Çerez de ayarlanır (`fiko_session`); script'ler `token` alanını `Authorization: Bearer` ile kullanabilir.

| Durum | HTTP |
|---|---|
| Kullanıcı adı/parola kuralı ihlali, bozuk gövde | `400` |
| Kullanıcı adı alınmış | `409` |
| Giriş hatası (kullanıcı yok **ya da** parola yanlış — **tek mesaj**, hesap sızmaz) | `401` |
| Hesap devre dışı | `403` |
| Üyelik kapalı (`FIKO_USERS_ENABLED=false`) | `404` |
| Hesap servisi kurulamadı (`Deps.Users` yok) | `503` |

### GET /api/auth/me · POST /api/auth/logout

`me` açılışta çağrılır: «geçerli oturum var mı?» sorusunun tek yanıtı.

```json
{ "user": { "id": "usr_…", "username": "ayse", "role": "user", "disabled": false, "…": "…" }, "role": "user" }
```

Oturum yoksa/geçersizse `401`. `logout` token'ı sunucuda geçersiz kılar ve çerezi siler → `{ "ok": true }`.

> **İşletme notu:** oturumlar **bellekte** tutulur (`user.Service.sessions`). Sunucu yeniden başlayınca herkes
> yeniden giriş yapar. Oturum ömrü `FIKO_SESSION_TTL` ile ayarlanır (`720h`, `24h` gibi Go süresi; boş/geçersiz → 30 gün, geçersizse uyarı loglanır).
> `FIKO_BOOTSTRAP_PASSWORD` doluysa açılışta `FIKO_BOOTSTRAP_USER` (varsayılan `admin`) adıyla yönetici hesabı oluşturulur;
> hesap zaten varsa dokunulmaz. Parola boşsa hiçbir hesap oluşturulmaz (rastgele parola kullanıcıyı kilitlerdi) ve «ilk kayıt yönetici olur» kuralı geçerlidir.

### Roller ve üye bazlı araç yetkisi

| Rol | Kim | Yapabildiği |
|---|---|---|
| `admin` | İlk kayıt, ya da `FIKO_API_KEY` sahibi; **üyelik kapalıyken herkes** | Yetkileri görür ve değiştirir; üyeleri yönetir |
| `user` | Diğer üyeler | Kendi yetkilerini **yalnızca okur** |

Yeni üyenin varsayılan politikası yalnızca `execute`'tür (güvenli başlangıç).

- `GET /api/permissions` yanıtına **`admin`** bayrağı eklenir; arayüz düzenleme düğmelerini buna göre açar/kapatır.
  Özel politikalı üye kimlik listesi (`users`) **yalnızca yöneticiye** gelir, üyede boştur.
- `PUT /api/permissions` üyelik açıkken **yalnızca yönetici**; üyeye `404` (uç görünmez). Aksi hâlde herhangi bir üye
  kendine `delete`/`system` verebilirdi.

#### GET /api/admin/users

Yalnızca yönetici (üyeye `404`). Her satır hesap bilgisi + araç politikasıdır; `custom=false` varsayılan politikanın geçerli olduğunu söyler.

```json
{ "users": [ { "id": "usr_…", "username": "ayse", "role": "user", "disabled": false, "displayName": "Ayşe",
               "policy": { "grants": ["execute"], "allowHighRisk": false, "allowCritical": false },
               "custom": false } ],
  "known": ["read","write","execute","delete","system"] }
```

#### PUT /api/admin/users/:id/permissions

Gövde `PUT /api/permissions` ile aynıdır (`grants`, `allowHighRisk`, `allowCritical`; **tüm küme** değiştirilir).
Politika **yalnızca o üyeye** yazılır; yöneticinin kendisininki değişmez. Yanıt `{ "user": { …satır… } }`.
Bilinmeyen üye `404`, bilinmeyen izin adı `400`. Denetim kaydına `permissions.update` (`target=<kullanıcı adı>`) düşer.

Hesap ya da izin sistemi yapılandırılmamışsa her iki yönetici ucu da `503` döner.

### WebSocket ve oturum

Tarayıcı WS'e özel başlık koyamaz. Aynı origin'de `fiko_session` çerezi otomatik gider; çerez taşınmazsa
(`ng serve` proxy'si, farklı origin) `ws://localhost:8080/ws?token=<token>` kullanılır. Geçerli oturum token'ı `?key=`
anahtarının yerini tutar.

`GET /api/system` yanıtındaki `auth` alanı:

```json
{ "auth": { "enabled": false, "users": true, "header": "X-Fiko-Auth" } }
```

`users=true` → arayüz kullanıcı adı + parola (Giriş/Kayıt) ekranını gösterir. İzin özeti (`permissions`) yalnızca
oturumu olana verilir; anonim istek için yoktur.


### Arayüzde giriş

Giriş ekranı **yalnızca sunucu anahtar istiyorsa** açılır:

| Sunucu | Arayüz |
|---|---|
| `auth.enabled=false` | Giriş ekranı **hiç görünmez** — kullanıcı hiçbir şey yapmadan uygulamayı kullanır |
| `auth.enabled=true`, anahtar yok | Tam ekran giriş kutusu açılır (anahtar + kullanıcı adı) |
| Anahtar girilip doğrulandı | Uygulama açılır; anahtar `sessionStorage`'da tutulur (sekme kapanınca unutulur) |

`window.__FIKO_CONFIG__.authKey` ile anahtar derleme zamanında da verilebilir;
giriş ekranı yine yalnızca sunucu anahtar istiyorsa görünür.
## Bellek / Hafıza (Faz 10)

Asistanın kullanıcı hakkında **kalıcı** olarak hatırladıkları. Anılar kullanıcıya aittir
(`X-Fiko-User`); başkasının anısı her uçta `404`'tür (varlığı sızdırılmaz). `MEMORY_ENABLED=false` ise
tüm `/api/memories` uçları `503` döner.

```json
{ "id": "mem_3f9a...", "userId": "local", "kind": "long_term",
  "content": "Angular kullanıyor.", "tags": ["angular"], "origin": "auto",
  "conversationId": "conv_...", "importance": 0.6, "useCount": 2,
  "lastUsedAt": "2026-10-03T18:00:00Z",
  "createdAt": "2026-10-03T17:40:00Z", "updatedAt": "2026-10-03T17:40:00Z" }
```

| Alan | Anlam |
|---|---|
| `kind` | `long_term` (kişisel gerçek/tercih, varsayılan) · `semantic` (alan bilgisi/kural) · `episodic` (olay) |
| `origin` | `user` (elle yazıldı/düzenlendi) · `auto` (sohbetten öğrenildi) |
| `importance` | 0..1. Elle eklemede varsayılan 0.8, otomatikte 0.6 |
| `useCount`, `lastUsedAt` | Kaç turda isteme girdi; arama ekranı bunu **artırmaz** |

Embedding ve içerik özeti istemciye **gönderilmez**. İçerik 4-400 karakter (art arda boşluklar tek boşluğa
indirilir), en fazla 8 etiket (her biri ≤ 32 karakter, küçük harfe çevrilir, tekrarlar atılır).

**Sınır:** kullanıcı başına `MEMORY_MAX_PER_USER` (varsayılan 500) anı; aşılırsa `409 conflict`.

### GET /api/memories?kind=&tag=&q=&limit=&offset=

En son güncellenen önce. `q` içerikte büyük/küçük harf duyarsız düz metin aramadır (anlam araması için
`/search`). Geçersiz `kind` → `400 validation_error`.

```json
{ "items": [ { "id": "mem_3f9a...", "…": "…" } ],
  "count": 1, "total": 12, "limit": 50, "offset": 0 }
```

`total` **süzgeçsiz** toplamdır (kullanıcının tüm anıları); `count` bu sayfadaki satır sayısıdır.

### GET /api/memories/:id

`{ "memory": { … } }`; yoksa `404`.

### POST /api/memories

Kullanıcının elle eklediği anı (`origin: user`). `kind` boşsa `long_term`, `importance` 0/boşsa 0.8.

```bash
curl -X POST http://localhost:8080/api/memories \
  -H 'Content-Type: application/json' -H 'X-Fiko-User: local' \
  -d '{"content":"Angular ve Go kullanıyorum","tags":["angular","go"]}'
```

Yanıt `{ "memory": {…}, "outcome": "created" }`. `outcome` üç değer alır:

| `outcome` | HTTP | Ne oldu |
|---|---|---|
| `created` | `201` | Yeni anı eklendi |
| `updated` | `200` | Aynı konuda eski bir anı vardı (embedding benzerliği ≥ 0.93); yenisi **onun yerini aldı** |
| `unchanged` | `200` | Aynı bilgi zaten kayıtlıydı (büyük/küçük harf, Türkçe aksan ve boşluk farkı yok sayılır); tekrar eklenmedi |

Hata: `400 validation_error` (`fields` alan bazlı), `409 conflict` (anı sınırı).

### PUT /api/memories/:id

**Kısmi** güncelleme (skill/tool `PUT`'larının aksine): gönderilmeyen alan değişmez. En az biri gerekli,
aksi hâlde `400`. Düzenlenen anı `origin: user` olur — **otomatik çıkarım elle yazılan anıyı ezmez**.
İçerik değişirse embedding yeniden üretilir; üretilemezse eski vektör atılır (bayat vektör yanıltmasın).

```json
{ "content": "Angular ve Go kullanıyorum", "tags": ["angular"], "kind": "long_term", "importance": 0.9 }
```

`tags: []` etiketleri temizler (`null`/eksik = dokunma).

### DELETE /api/memories/:id

`204 No Content`; yoksa `404`.

### DELETE /api/memories?confirm=true — "beni unut"

Kullanıcının **tüm** anılarını siler; geri alınamaz. `confirm=true` olmadan `400`. Yanıt `{ "deleted": 12 }`.

### POST /api/memories/search

Hibrit arama: embedding varsa %80 kosinüs + %20 anahtar kelime, yoksa yalnızca anahtar kelime
(sağlayıcı çökse de çalışır). Okuma işlemidir: `useCount` artırmaz.

```json
{ "query": "hangi framework?", "limit": 5, "minScore": 0.35 }
```

`limit` boşsa 5, en fazla 50. `minScore` 0..1. Yanıt skora göre azalan:

```json
{ "items": [ { "id": "mem_3f9a...", "content": "Angular kullanıyor.", "score": 0.72, "…": "…" } ], "count": 1 }
```

### POST /api/memories/reindex[?force=true]

Vektörü olmayan ya da başka embedding modeliyle üretilmiş anıların vektörünü **arka planda** üretir
(`force`: hepsi). Embedding modeli değişince çalıştırılır. `202 Accepted`:

```json
{ "total": 12, "queued": 3 }
```

Embedding sağlayıcısı yoksa `503 llm_error` (`OLLAMA_EMBED_MODEL` ayarla).

### Ajan nasıl kullanır?

- **Getirme (her tur):** kullanıcı mesajıyla `MEMORY_RECALL_MIN_SCORE` (varsayılan 0.35) üzerindeki anılar
  sistem istemine "bilinenler" olarak girer — kesin gerçek değil **veri** olarak çerçevelenir; çelişkide
  kullanıcının son sözü kazanır. Bilgi tabanıyla **paralel** alınır. Hiç anı yoksa sorgu embedding'i üretilmez.
- **Öğrenme (tur sonu, arka plan):** yalnızca `complete` biten turun **kullanıcı mesajından** kalıcı bilgi
  çıkarılır (asistan cevabından değil: model kendi uydurmasını "kullanıcı gerçeği" diye kaydetmesin).
  "Bugün bunu 30 mm yap" gibi geçici istekler anı değil bağlamdır. `MEMORY_AUTO_LEARN=false` ile kapanır;
  `MEMORY_LEARN_MODEL` ile daha küçük bir model seçilebilir (çıkarım ek bir `Generate` çağrısıdır).

### Gizlilik

Anı **içeriği** denetim kaydına ve loglara yazılmaz; yalnızca kimlik ve sayılar (`memory.create`,
`memory.update`, `memory.delete`, `memory.delete_all`, `memory.reindex`).

## Ayarlar

### GET /api/settings

```bash
curl http://localhost:8080/api/settings
```
```json
{ "model": "muse-glimmer:30b", "temperature": 0, "topP": 0, "maxTokens": 0, "contextSize": 0 }
```
`0` değerleri **"sağlayıcı varsayılanı"** demektir (Ollama'ya hiç gönderilmez).

**`model` bu ayardan gelir ve üretimde o model kullanılır.** İstek (`text.message`) payload'ında model gönderilmez; istemci bunu bilinçli olarak yapmaz ve ayar ekranının tek kaynağı burasıdır. Bir sonraki üretimde modelin çözülme sırası:

1. İstekte **açıkça** verilmiş `model` (payload) — istemci tek seferlik geçersiz kılabilir.
2. **Bu ayardaki kayıtlı model** (`GET/PUT /api/settings` → `model`).
3. Boş → sağlayıcının kendi varsayılanı (`OLLAMA_MODEL`).

> `.env` (`OLLAMA_MODEL`) yalnızca **sunucu açılışındaki** başlangıç değeridir; çalışma sırasında ayar ekranından değiştirilirse `.env` yeniden okunmaz.

### PUT /api/settings

Kısmi güncelleme: yalnızca gönderilen alanlar değişir. Denetim kaydı alınır.

```bash
curl -X PUT http://localhost:8080/api/settings \
  -H 'Content-Type: application/json' -d '{"temperature": 0.85}'
```
```json
{ "model": "muse-glimmer:30b", "temperature": 0.85, "topP": 0, "maxTokens": 0, "contextSize": 0 }
```

| Alan | Aralık | Not |
|---|---|---|
| `model` | Ollama'da yüklü olmalı | Boş bırakılırsa değişmez |
| `temperature` | 0..2 | 0 = deterministik |
| `topP` | 0..1 | |
| `maxTokens` / `contextSize` | 0..1_000_000 | 0 = sağlayıcı varsayılanı |

Geçersiz değer → `400` (değerler değiştirilmez). Değişiklik `SettingsChanged` olayı olarak yayınlanır; **bir sonraki mesaj** yeni değerleri kullanır.

---

## Denetim kaydı

Her kullanıcı işlemi JSON satırı olarak yazılır (`AUDIT_LOG_PATH`; boşsa stdout).

```json
{"id":"aud_5cf3...","at":"2026-10-02T17:53:25Z","actor":"user","userId":"local",
 "action":"settings.update","result":"success","detail":"model=denetim_testi,temperature"}
```

| Alan | Anlamı |
|---|---|
| `actor` | `user` \| `agent` \| `tool` \| `system` |
| `result` | `success` \| `error` \| `denied` |
| `action` | `settings.update`, `conversation.create`, `message.send`, `tool.exec`… |
| `detail` | Özet (tüm mesaj metni **yazılmaz** — geçmişte zaten durur) |

> Denetim kaydı üretim akışını **durdurmaz**: dosya hatası olursa loglanır, işlem devam eder. Yazma sessizce yutulmaz; dosya modunda **0600** (Unix) ve append modundadır.

---

## WebSocket: /ws

REST'te mesaj gönderilmez; akış WS üzerinden yürür. `ws://localhost:8080/ws` (upgrade başlığı olmadan → `426`).

Zarf: `{ "type": "...", "id": "...", "conversationId": "...", "payload": { ... } }`
Sunucu olayları: `assistant.thinking` → (`tool.start` → `tool.progress`* → `tool.result`)* → `assistant.text.delta`* → `assistant.text.final` → `conversation.state`; kesintide `assistant.interrupted`, hatada `error`.
Ayrıntı: `PROJE.MD` ve `backend/internal/protocol/protocol.go`.
---

### Ses (voice) olayları — Faz 6

Konuşma → metin → ajan akışı. `STT_URL` boşken bu olaylar `not_implemented` ile reddedilir (mikrofon düğmesi pasif görünür; hata sessizce yutulmaz).

```json
// istemci → sunucu
{"type":"voice.start","id":"v1","conversationId":"conv_…","payload":{"mime":"audio/webm"}}
{"type":"voice.audio","id":"v2","conversationId":"conv_…","payload":{"data":"<base64>"}}
{"type":"voice.stop","id":"v3","conversationId":"conv_…","payload":{}}

// sunucu → istemci (tanınan metin; boşsa text yoktur ve mesaj GÖNDERİLMEZ)
{"type":"voice.detected","payload":{"text":"bugün hava nasıl","durationMs":3200,"sent":true}}
```

- Ses **base64** taşınır (kare okuyucu JSON tabanlı; ~%33 ek yük).
- Tanınan metin **otomatik olarak kullanıcı mesajı gibi gönderilir** (`sent: true`); boş dönerse boş mesaj gönderilmez.
- **Barge-in:** `voice.start` geldiğinde sunucu süren seslendirmeyi keser (`tts.done` + `stopped`).

### Sesli çıkış (TTS) olayları — Faz 7

Metin → ses akışı. `TTS_URL` boşken `tts.mode` istekleri `not_implemented` ile reddedilir (hoparlör düğmesi pasif); metin akışı bundan etkilenmez.

```json
// istemci → sunucu: sesli modu aç/kapat (oturum bazlı)
{"type":"tts.mode","id":"t1","conversationId":"conv_…","payload":{"enabled":true}}
// istemci → sunucu: konuşmayı kes (barge-in)
{"type":"tts.stop","id":"t2","conversationId":"conv_…","payload":{}}
// sunucu → istemci: HER CÜMLE için ayrı olay (cümleler sırayla üretilir)
{"type":"tts.audio","payload":{"conversationId":"conv_…","messageId":"msg_1","index":0,"mime":"audio/mpeg","data":"<base64>"}}
{"type":"tts.done","payload":{"conversationId":"conv_…","messageId":"msg_1","sentences":3,"bytes":3012,"durationMs":22,"stopped":false}}
```

- **Neden cümle cümle?** Motorlar uzun metni ya reddeder ya da çok yavaş konuşur; cümle bölme ilk sesi hızlı getirir ve aradaki cümleyi `tts.stop` ile kesmeyi mümkün kılar. Cümle **metin akışı sırasında** gönderilir — `text.final`'ı beklemez.
- Markdown **sunucuda temizlenir**: `**kalın**`, `` `kod` ``, liste/başlık işaretleri atılır, `[metin](url)` → `metin` olur, **kod blokları atlanır** (yalnız kod geldiyse ses üretilmez — hata değildir).
- `stopped: true` bir **hata değildir**: kullanıcı konuşmayı kesti.
- Motor hatası sessizce yutulmaz: `error` olayı (`llm_error`, `retryable: true`) gider.
- Hatalar: kayıt başlatılmamış / boş ses / süre sınırı → `bad_request`/`too_large`; motor kapalı veya zaman aşımı → `llm_error` (**retry** edilebilir).
- Kayıt bağlantıya bağlıdır; bağlantı kapanınca yarım kayıt **düşer**.

## Çalıştırma

```bash
cd backend
cp .env.example .env         # en az OLLAMA_MODEL ayarlanmalı
go run ./cmd/server
```

```jsonc
// 1) konuşma oluştur (REST)
POST /api/conversations            -> { "conversation": { "id": "conv_..." } }
// 2) bağlan
ws://localhost:8080/ws
// 3) mesaj gönder
{"type":"text.message","id":"r1","conversationId":"conv_...","payload":{"text":"Merhaba"}}
// 4) yanıtları dinle
{"type":"assistant.text.delta","payload":{"messageId":"msg_...","delta":"Mer"}}
{"type":"assistant.text.final","payload":{"messageId":"msg_...","content":"Merhaba!","status":"complete"}}
// 5) iptal
{"type":"conversation.cancel","conversationId":"conv_...","payload":{"reason":"user"}}
```

### Tool olayları (Faz 5)

Model bir tool çağırdığında üç olay gelir; hepsi `replyTo` ile ilgili isteğe bağlanır.

```json
{"type":"tool.start","conversationId":"conv_...","payload":{"callId":"tc_1","name":"fiko.echo","input":{"message":"merhaba"}}}
{"type":"tool.progress","payload":{"callId":"tc_1","step":"başlıyor","percent":40}}
{"type":"tool.result","payload":{"callId":"tc_1","name":"fiko.echo","status":"success","output":{"message":"merhaba"},"durationMs":12}}
// yetki/çalıştırma hatası:
{"type":"tool.result","payload":{"callId":"tc_1","name":"fs.delete","status":"error","error":"yetki yok","durationMs":1}}
```

- `status` yalnızca `success`/`error`; yetki reddi de `error`tur, `error` alanında belirtilir.
- `tool.result` HER çağrı için gelir (başarılı, hatalı veya **tekrarlanan**).
- Modele yalnızca **yetkili ve etkin** tool'lar sunulur; yetkisiz tool çağrılamaz bile.

### Tool kayıtları (Faz 5b — execution tracking)

Tool çağrıları **asistan mesajının `toolRuns` alanında saklanır**; sayfa yenilendiğinde `GET /api/conversations/:id/messages` ile geri gelir, arayüzde rozet olarak görünür.

```json
{"type":"assistant.text.final","payload":{"messageId":"msg_1","content":"Echo sonucu: merhaba","status":"complete"}}
{ "id": "msg_1", "role": "assistant", "content": "Echo sonucu: merhaba", "status": "complete",
  "toolRuns": [ { "callId": "tc_fe3aec2f", "name": "fiko.echo", "status": "success",
    "durationMs": 1, "input": {"message":"merhaba"}, "output": {"message":"merhaba"} } ] }
```

- `toolRuns` yalnızca tool çalışan mesajlarda bulunur; `omitempty` olduğu için eski mesajların şeması bozulmaz.
- `input`/`output` **görüntü amaçlı ve kısaltılır**: JSON 2 kB'yi aşarsa `{"_truncated":true,"_size":…,"_preview":"…"}`.
- Tek cevapta en çok **16** çalıştırma kaydedilir.
- Tekrar eden çağrı **yeniden çalıştırılmaz**: `tool.result` yine gider (önceki çıktıyla), ama `toolRuns`'a yeni giriş eklenmez.
### CRUD · POST /api/tools · PUT /api/tools/:id · DELETE /api/tools/:id

Gövde `Tool` alanlarıdır (kimlik ve zaman damgaları sunucuya aittir). `builtin` kayıtlar `409 conflict` ile **silinmez**; `custom` kayıtlar **yürütülemez** (`501`).

### PUT /api/tools/:id/status `{"status":"disabled"}`

Tool'u kapatır/açar. Kapatılan tool `409 conflict` ile çalıştırılamaz. Sunucu yeniden başlatıldığında **kullanıcının kapatma kararı korunur** (açılışta otomatik açılmaz).

### POST /api/tools/validate[?id=:id]

Kaydetmeden doğrular; her zaman **200** (`{"valid":…, "errors":[…]}`).

### POST /api/tools/:id/execute `{"input": { … }}`

Zincir **sırası**: çözümle → kapalı mı → **girdi doğrulaması** → **yetki** → süre sınırı → çalıştır (gerekirse tekrar dene) → denetim kaydı.

```json
{ "toolId": "tool_…", "toolName": "fiko.echo", "output": { "message": "merhaba" },
  "attempts": 1, "durationMs": 2 }
```

| Durum | Sonuç |
|---|---|
| Girdi şemaya uymuyor | `400 validation_error` + `fields` |
| İzin eksik / risk kapısı kapalı | `403 permission_denied` |
| Tool kapalı | `409 conflict` |
| Uygulaması yok (custom) | `501 not_implemented` |
| Tool'un kendi hatası | `502 internal` |
| Zaman aşımı / çağıran iptali | `504` / `499` |

Her çalıştırma (başarılı, hatalı veya **reddedilmiş**) denetim kaydına girer: `action=tool.execute`, `result=success|error|denied`, `meta={tool, risk, attempts, elapsedMs}`.
```

| Alan | Açıklama |
| --- | --- |
| `query` | Zorunlu. Boşsa `400 validation_error` (`fields[0].field = "query"`). |
| `mode` | `hybrid` (varsayılan), `semantic`, `keyword`. |
| `limit` | Varsayılan 5, en fazla 50. |
| `minScore` | 0..1; altındakiler elenir. |

Modlar: `keyword` embedding gerektirmez (sorgu sözcüklerinin skill metninde geçme oranı). `semantic` yalnızca vektör benzerliği; embedding üretilemezse **hata döner** (sessizce düşmez): sağlayıcı yok → `503 llm_error`, sağlayıcı hatası → `502 llm_error`. `hybrid` önce semantik (%80) + anahtar kelime (%20); embedding üretilemezse **anahtar kelimeye düşer ve 200 döner**. Embedding'i olmayan ya da boyutu farklı (model değişmiş) skill'ler `hybrid`'de yalnızca anahtar kelimeyle bulunur, `semantic`'te aranmaz.

Yanıt: `{"items":[{"skill":{...},"score":0.83}],"count":1,"mode":"hybrid"}`

### POST /api/skills/reindex[?force=true]

Embedding'i eksik skill'lerin vektörünü üretip kaydeder (taslaklar dahil). `force=true` hepsini yeniden üretir; **embedding modeli değiştiğinde gerekir** (eski/yeni vektörler karşılaştırılamaz). Tarama sırasında kullanıcı bir skill'i düzenlerse o atlanır (düzenleme ezilmez). Sağlayıcı yoksa `503 llm_error`, geçersiz `force` → `400`.

Yanıt: `{"total":12,"embedded":12,"skipped":0,"failed":0}`. `failed>0` ise sağlayıcı bazı çağrılarda hata vermiştir (logda ayrıntı); işlem tekrarlanabilir. Her çağrı denetim kaydına yazılır (`skill.reindex`).

Embedding `OLLAMA_EMBED_MODEL` verilirse etkindir. Kayıt/güncellemede otomatik üretilir; **üretilemezse kayıt yine de başarılı olur** (`reindex` ile doldurulur). Sunucu açılışında eksikler arka planda doldurulur. Giren metin: ad, açıklama, etiketler, örnek cümleler — yalnızca bunlar değişince yeniden üretilir.

### POST /api/skills/:id/execute

Skill'i `executorConfig.tool` üzerinden **çalıştırır**. Gövde: `{ "arguments": { "offset": 30 } }`
Yanıt: `{"skillId":"...","skillName":"create_plane","tool":"fiko.echo","output":{"message":"ok"},"durationMs":2,"at":"..."}`

| Durum | Sonuç |
|---|---|
| Skill yok | `404 not_found` |
| Taslak/kapalı | `409 conflict` |
| Zorunlu parametre eksik | `400 validation_error` (`fields`: `parameters[0]`) |
| `executorType = llm` | `501 not_implemented` |
| Tool yok/kapalı/yetkisiz | `404` / `409` / `403` |

Skill'in `executorConfig.tool` adı Tool Registry'de yoksa skill kaydedilemez/etkinleştirilemez (400, `fields[0].field = "executorConfig"`). Tool sistemi yapılandırılmamışsa `503`.
### GET /api/conversations?limit=&offset=

Kullanıcının konuşmaları, yeniden eskiye. `limit`: boş/0 → 50, üst sınır 200. `offset` negatif veya sayı değilse `400`.

```bash
curl 'http://localhost:8080/api/conversations?limit=20&offset=0'
```
```json
{ "items": [ /* konuşmalar */ ], "count": 1, "limit": 20, "offset": 0 }
```

### GET /api/conversations/:id

Tek konuşma → `{ "conversation": { ... } }`. Yoksa `404`.

### PUT /api/conversations/:id

Başlığı değiştirir. Gövde: `{ "title": "Yeni ad" }` → `{ "conversation": { ... } }`. Başlık kırpılır; boş ya da 120 karakterden uzunsa `400`. Yabancı/olmayan konuşma `404` (başlık değişmez). `ConversationRenamed` olayı yayınlanır.

### DELETE /api/conversations/:id

Konuşmayı ve mesajlarını siler → `204`. Yoksa `404`.

### GET /api/conversations/:id/messages?limit=

Son N mesaj (eskiden yeniye); `hasMore` ile "daha fazla var" bilgisi verilir. `items` her zaman **son** N mesajdır (sunucu `limit+1` ister, fazlalığı atar).

```bash
curl 'http://localhost:8080/api/conversations/conv_533bca.../messages?limit=50'
```
```json
{ "items": [ { "id": "msg_...", "conversationId": "conv_...", "role": "user",
  "content": "Merhaba", "status": "complete", "createdAt": "..." } ],
  "count": 1, "limit": 50, "hasMore": false }
```

`validation_error` alan bazlı hataları da taşır (arayüz hepsini bir seferde işaretleyebilsin):

```json
{ "error": { "code": "validation_error", "message": "skill geçersiz",
  "fields": [ { "field": "name", "message": "küçük harfle başlamalı; ..." },
              { "field": "description", "message": "boş olamaz ..." } ] } }
```