Files
groot d18405ed2e
ci / test (services/knowledge) (push) Successful in 47s
ci / test (services/agent) (push) Successful in 55s
ci / test (platform/neuroforge) (push) Successful in 56s
ci / docker-build (push) Failing after 1m10s
ci / test (services/control) (push) Successful in 1m26s
release-tag / release-image (push) Successful in 6m43s
update-1.6.2
2026-09-09 12:55:35 +02:00

6803 lines
235 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
diff --git a/.env.example b/.env.example
index 45a8d49..d3c6f7d 100644
--- a/.env.example
+++ b/.env.example
@@ -29,7 +29,7 @@
# 01. MEGA STACK - RELEASE / HOST PORTS / PFADE
###############################################################################
# Immutable Registry-Tag der sechs Projekt-Images. "latest" ist produktiv verboten.
-IMAGE_TAG=1.6.1
+IMAGE_TAG=1.6.2
CONTROL_HOST_PORT=8070
AGENT_HOST_PORT=8080
@@ -74,6 +74,12 @@ OPENAI_API_KEY=
# qwen3:8b statt gemma3 setzen. Das Modell muss vorher in Ollama vorhanden sein.
OLLAMA_MODEL=gemma3
OLLAMA_EMBEDDING_MODEL=embeddinggemma
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
+# Optional NeuroForge GPU-worker override; falls back to OLLAMA_API_KEY.
+NEUROFORGE_WORKER_OLLAMA_API_KEY=
+# Optional NeuroForge-only override; takes precedence over OLLAMA_API_KEY.
+NEUROFORGE_OLLAMA_API_KEY=
OLLAMA_TIMEOUT=10m
OLLAMA_MAX_CONCURRENT=1
OLLAMA_NUM_PREDICT=768
diff --git a/.env.subagent.example b/.env.subagent.example
index 3276990..968db6f 100644
--- a/.env.subagent.example
+++ b/.env.subagent.example
@@ -1,5 +1,5 @@
-# Remote NeuroForge subagent host (v1.6.1)
-IMAGE_TAG=1.6.1
+# Remote NeuroForge subagent host (v1.6.2)
+IMAGE_TAG=1.6.2
NEUROFORGE_MASTER_URL=https://neuroforge.internal.example
NEUROFORGE_WORKER_TOKEN=CHANGE_ME_WORKER_TOKEN_SHARED_WITH_MASTER
NEUROFORGE_WORKER_HEARTBEAT_INTERVAL=15s
@@ -16,5 +16,10 @@ NEUROFORGE_GPU_WORKER_CONCURRENCY=1
NEUROFORGE_WORKER_OLLAMA_URL=http://host.docker.internal:11434
OLLAMA_MODEL=gemma4
OLLAMA_EMBEDDING_MODEL=embeddinggemma
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
NEUROFORGE_OLLAMA_NUM_CTX=8192
OLLAMA_KEEP_ALIVE=10m
+
+# Optional GPU-worker-specific override; falls back to OLLAMA_API_KEY.
+NEUROFORGE_WORKER_OLLAMA_API_KEY=
diff --git ab/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
new file mode 100644
index 0000000..5c5d928
--- /dev/null
+++ b/.gitea/workflows/ci.yml
@@ -0,0 +1,39 @@
+name: ci
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ module:
+ - platform/neuroforge
+ - services/agent
+ - services/knowledge
+ - services/control
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: ${{ matrix.module }}/go.mod
+ cache: false
+ - name: Test
+ working-directory: ${{ matrix.module }}
+ run: go test ./...
+ - name: Vet
+ working-directory: ${{ matrix.module }}
+ run: go vet ./...
+
+ docker-build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: docker/setup-buildx-action@v3
+ - name: Build all release images
+ run: docker buildx bake --set '*.output=type=cacheonly'
diff --git a/.gitea/workflows/release-tag.yml ba/.gitea/workflows/release-tag.yml
deleted file mode 100644
index 668bef1..0000000
--- a/.gitea/workflows/release-tag.yml
+++ /dev/null
@@ -1,116 +0,0 @@
-name: release-tag
-on:
- push:
- branches:
- - 'main'
-
-jobs:
- release-image:
- runs-on: ubuntu-latest
- env:
- DOCKER_ORG: sendnrw
- DOCKER_LATEST: latest
- RUNNER_TOOL_CACHE: /toolcache
- steps:
- - name: Checkout
- uses: actions/checkout@v3
- with:
- fetch-depth: 0
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v2
-
- - name: Set up Docker BuildX
- uses: docker/setup-buildx-action@v2
- with:
- config-inline: |
- [registry."git.send.nrw"]
- http = true
- insecure = true
-
- - name: Login to Registry
- uses: docker/login-action@v2
- with:
- registry: git.send.nrw
- username: ${{ secrets.DOCKER_USERNAME }}
- password: ${{ secrets.DOCKER_PASSWORD }}
-
- - name: Get Meta
- id: meta
- run: |
- echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
-
- - name: Build and push NeuroForge
- uses: docker/build-push-action@v4
- with:
- context: ./platform/neuroforge
- file: ./platform/neuroforge/Dockerfile
- target: server
- platforms: |
- linux/amd64
- push: true
- tags: |
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-neuroforge:${{ steps.meta.outputs.REPO_VERSION }}
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-neuroforge:${{ env.DOCKER_LATEST }}
-
- - name: Build and push NeuroForge Worker
- uses: docker/build-push-action@v4
- with:
- context: ./platform/neuroforge
- file: ./platform/neuroforge/Dockerfile
- target: worker
- platforms: |
- linux/amd64
- push: true
- tags: |
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-neuroforge-worker:${{ steps.meta.outputs.REPO_VERSION }}
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-neuroforge-worker:${{ env.DOCKER_LATEST }}
-
- - name: Build and push Agent
- uses: docker/build-push-action@v4
- with:
- context: ./services/agent
- file: ./services/agent/Dockerfile
- platforms: |
- linux/amd64
- push: true
- tags: |
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-agent:${{ steps.meta.outputs.REPO_VERSION }}
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-agent:${{ env.DOCKER_LATEST }}
-
- - name: Build and push Agent Data Init
- uses: docker/build-push-action@v4
- with:
- context: ./services/agent
- file: ./services/agent/Dockerfile
- target: data-init
- platforms: |
- linux/amd64
- push: true
- tags: |
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-agent-data-init:${{ steps.meta.outputs.REPO_VERSION }}
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-agent-data-init:${{ env.DOCKER_LATEST }}
-
- - name: Build and push Knowledgebase
- uses: docker/build-push-action@v4
- with:
- context: ./services/knowledge
- file: ./services/knowledge/Dockerfile
- platforms: |
- linux/amd64
- push: true
- tags: |
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-knowledge:${{ steps.meta.outputs.REPO_VERSION }}
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-knowledge:${{ env.DOCKER_LATEST }}
-
- - name: Build and push Control Center
- uses: docker/build-push-action@v4
- with:
- context: ./services/control
- file: ./services/control/Dockerfile
- platforms: |
- linux/amd64
- push: true
- tags: |
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-control:${{ steps.meta.outputs.REPO_VERSION }}
- git.send.nrw/${{ env.DOCKER_ORG }}/glpi-neuroforge-mega-control:${{ env.DOCKER_LATEST }}
diff --git ab/.gitea/workflows/release.yml b/.gitea/workflows/release.yml
new file mode 100644
index 0000000..d63343f
--- /dev/null
+++ b/.gitea/workflows/release.yml
@@ -0,0 +1,42 @@
+name: release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ release-images:
+ runs-on: ubuntu-latest
+ env:
+ REGISTRY: git.send.nrw/sendnrw
+ RUNNER_TOOL_CACHE: /toolcache
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: docker/setup-qemu-action@v3
+ - uses: docker/setup-buildx-action@v3
+ with:
+ config-inline: |
+ [registry."git.send.nrw"]
+ http = true
+ insecure = true
+ - uses: docker/login-action@v3
+ with:
+ registry: git.send.nrw
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+ - name: Resolve immutable release version
+ id: meta
+ shell: bash
+ run: |
+ set -euo pipefail
+ VERSION="$(git describe --tags --exact-match HEAD | sed 's/^v//')"
+ test -n "$VERSION"
+ test "$(cat VERSION)" = "$VERSION"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+ - name: Build and push all release images
+ env:
+ IMAGE_TAG: ${{ steps.meta.outputs.version }}
+ run: docker buildx bake --push
diff --git ab/BUILD-AND-RELEASE.md b/BUILD-AND-RELEASE.md
new file mode 100644
index 0000000..325daa8
--- /dev/null
+++ b/BUILD-AND-RELEASE.md
@@ -0,0 +1,43 @@
+# Build and Release
+
+## Repository image set
+
+The repository publishes six immutable project images:
+
+- `glpi-neuroforge-mega-neuroforge`
+- `glpi-neuroforge-mega-neuroforge-worker`
+- `glpi-neuroforge-mega-agent`
+- `glpi-neuroforge-mega-agent-data-init`
+- `glpi-neuroforge-mega-knowledge`
+- `glpi-neuroforge-mega-control`
+
+The default registry is `git.send.nrw/sendnrw`.
+
+## Local build
+
+From repository root:
+
+```bash
+docker buildx bake
+```
+
+Override registry/tag when required:
+
+```bash
+REGISTRY=registry.example/org IMAGE_TAG=1.6.2 docker buildx bake
+```
+
+## CI
+
+`.gitea/workflows/ci.yml` runs `go test ./...` and `go vet ./...` for NeuroForge, Agent, Knowledge and Control and then performs a Buildx Bake build of the full image set.
+
+## Release
+
+Set `VERSION` and all compose/deployment tags to the desired immutable version, then push a matching tag:
+
+```bash
+git tag v1.6.2
+git push origin v1.6.2
+```
+
+`.gitea/workflows/release.yml` verifies that the git tag matches `VERSION` and pushes all six images with tag `1.6.2`. Project production compose files do not require `latest`.
diff --git ab/DISTRIBUTED-DEPLOYMENT-BUNDLE.md b/DISTRIBUTED-DEPLOYMENT-BUNDLE.md
new file mode 100644
index 0000000..a031908
--- /dev/null
+++ b/DISTRIBUTED-DEPLOYMENT-BUNDLE.md
@@ -0,0 +1,5 @@
+# v1.6.2 Distributed Deployment Bundle
+
+This is the unchanged v1.6.2 application release plus ready-to-edit deployment kits under `deployments/`. No application code or persisted data format was changed.
+
+See `deployments/README.md`.
diff --git ab/MIGRATION-FROM-MEGA.md b/MIGRATION-FROM-MEGA.md
new file mode 100644
index 0000000..30740ad
--- /dev/null
+++ b/MIGRATION-FROM-MEGA.md
@@ -0,0 +1,33 @@
+# Migration: Mega v1.6.1 -> Agent + Knowledge Core
+
+Der Core-Modus verändert das Datenformat der Knowledge-JSONs nicht.
+
+## Variante A: Knowledge-Verzeichnis weiterverwenden
+
+Setze in `deployments/agent/.env` und `deployments/knowledge/.env` denselben `CORE_DATA_ROOT`, sodass beide auf denselben Hostpfad zeigen.
+
+## Variante B: Agent-Index übernehmen
+
+Der Agent speichert seinen persistenten lokalen Knowledge-Index unter `/app/data`. Der Core-Compose bind-mountet dafür `runtime/agent-data`.
+Um einen bestehenden Named Volume zu übernehmen, zuerst dessen Namen ermitteln und dann offline kopieren. Niemals das laufende Volume gleichzeitig von zwei Agent-Instanzen beschreiben lassen.
+
+## Funktionale Änderungen gegenüber Mega
+
+Nicht verfügbar im Core-Modus:
+
+- NeuroForge Memories/Synapses/Graph
+- autonome Research Goals
+- Staging-Synthese durch NeuroForge
+- CPU/GPU Subagent-Orchestrator
+- Control Center
+
+Weiter verfügbar:
+
+- GLPI Polling/OAuth2
+- Agent WebUI und Diagnose
+- lokale Knowledge-Indizierung und Embeddings
+- Hybrid Retrieval/RAG
+- Kategorien/Keywords/Source Policy
+- lokale Human-in-the-loop Lernbeispiele
+- optional GLPI-KB-Synchronisation
+- Knowledge Editor/Search
diff --git a/README.md b/README.md
index 8145919..1869f73 100644
--- a/README.md
+++ b/README.md
@@ -252,3 +252,15 @@ NEUROFORGE_GOAL_LEARNING_ENABLED=true
```
Damit bleibt rohes Chat-/Assistant-Lernen weiterhin deaktiviert.
+
+## v1.6.2: vollständige Deployment-Rollen und Ollama Bearer Auth
+
+Das Repository enthält neben dem vollständigen Mega-Compose eigenständige Kits unter `deployments/` für `master`, `cpu-subagent`, `gpu-subagent`, `agent`, `knowledge`, `ollama` und `combined`. Damit können der produktive GLPI-Agent/Knowledge-Core und die NeuroForge-Orchestrierung getrennt oder gemeinsam betrieben werden.
+
+Für Ollama-kompatible Endpunkte hinter einem Bearer-geschützten Gateway kann optional gesetzt werden:
+
+```env
+OLLAMA_API_KEY=
+```
+
+Agent, Knowledge, NeuroForge und modellfähige Subagents senden den Wert dann als `Authorization: Bearer ...`. NeuroForge kennt zusätzlich `NEUROFORGE_OLLAMA_API_KEY` und `NEUROFORGE_WORKER_OLLAMA_API_KEY` als rollenbezogene Overrides. Details: `docs/OLLAMA-BEARER-AUTH.md`.
diff --git ab/RELEASE-NOTES-v1.6.2.md b/RELEASE-NOTES-v1.6.2.md
new file mode 100644
index 0000000..e7e060a
--- /dev/null
+++ b/RELEASE-NOTES-v1.6.2.md
@@ -0,0 +1,59 @@
+# GLPI NeuroForge Mega v1.6.2
+
+v1.6.2 is the consolidated production release that reunifies the full Mega repository after the stripped-core packaging experiment and adds authenticated Ollama-compatible endpoints.
+
+## Complete deployment set
+
+The repository now ships all supported operating modes together:
+
+- full single-host Mega stack,
+- distributed Master,
+- remote CPU subagent,
+- remote GPU subagent with Ollama,
+- standalone GLPI Agent,
+- standalone Knowledge service,
+- standalone Ollama,
+- combined Agent + Knowledge + Ollama core,
+- Prometheus/Grafana example monitoring on the Master.
+
+The v1.6.1 recovery/OOM hardening remains included, including startup pre-compaction of legacy relink payloads, bounded durable-job payload retention, lower graph-backfill pressure, visible startup phases, stricter corrupted-state handling and HNSW checkpoint memory reductions.
+
+## Ollama Bearer authentication
+
+A shared optional variable is now supported:
+
+```env
+OLLAMA_API_KEY=
+```
+
+When non-empty, clients send:
+
+```http
+Authorization: Bearer <OLLAMA_API_KEY>
+```
+
+for Ollama-compatible `/api/tags`, `/api/chat` and `/api/embed` requests.
+
+Supported clients:
+
+- GLPI Agent, including every node in its Ollama pool,
+- Knowledge AI fallback,
+- NeuroForge provider and readiness/provider-health checks,
+- NeuroForge GPU/model subagents.
+
+NeuroForge also supports role-specific overrides:
+
+```env
+NEUROFORGE_OLLAMA_API_KEY=
+NEUROFORGE_WORKER_OLLAMA_API_KEY=
+```
+
+The role-specific value takes precedence over `OLLAMA_API_KEY` for the corresponding NeuroForge component.
+
+The key is stored as a NeuroForge secret when configured through `NEUROFORGE_OLLAMA_API_KEY`; admin APIs expose only configured/masked status unless secret reveal is explicitly enabled.
+
+Native Ollama does not itself become authenticated merely by setting this variable. The option is intended for an authenticated Ollama-compatible gateway/reverse proxy or another Ollama-compatible endpoint that validates Bearer tokens. Direct native Ollama deployments should normally leave it empty.
+
+## CI/release consolidation
+
+Gitea workflows now live only at repository root and cover all four Go modules. The release workflow builds and pushes the six immutable project images. `docker-bake.hcl` contains the same complete image set. Production compose files remain pinned through `IMAGE_TAG`; no `latest` project image is required.
diff --git a/VERSION b/VERSION
index 9c6d629..fdd3be6 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.6.1
+1.6.2
diff --git ab/deployments/README.md b/deployments/README.md
new file mode 100644
index 0000000..6ee8c5a
--- /dev/null
+++ b/deployments/README.md
@@ -0,0 +1,16 @@
+# Distributed Deployment Kits (v1.6.2)
+
+This directory contains three independent deployment kits:
+
+- `master/` - authoritative NeuroForge Master plus Agent, Knowledge, Control, optional SearXNG and optional Prometheus/Grafana.
+- `cpu-subagent/` - CPU worker for `vector.relink` / graph convergence.
+- `gpu-subagent/` - GPU worker plus local Ollama for `model.chat` and `model.embed`.
+
+The generated `NEUROFORGE_WORKER_TOKEN` is identical in all three `.env` files. Replace the RFC 5737 example IP addresses (`192.0.2.x`) with real reachable addresses before starting.
+
+Recommended order:
+1. GPU subagent: `docker compose --profile monitoring up -d`
+2. CPU subagent: `docker compose --profile monitoring up -d`
+3. Master: fill GLPI credentials, then `docker compose --profile research --profile monitoring up -d`
+
+Only the Master holds authoritative NeuroForge state. Workers are disposable execution nodes.
diff --git ab/deployments/agent/.env b/deployments/agent/.env
new file mode 100644
index 0000000..62c86d2
--- /dev/null
+++ b/deployments/agent/.env
@@ -0,0 +1,164 @@
+###############################################################################
+# GLPI AI Agent - standalone / stripped core
+###############################################################################
+IMAGE_TAG=1.6.2
+AGENT_IMAGE=git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:1.6.2
+AGENT_DATA_INIT_IMAGE=git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:1.6.2
+CORE_NETWORK=glpi-ai-core
+CORE_DATA_ROOT=../../runtime
+AGENT_BIND_IP=127.0.0.1
+AGENT_HOST_PORT=9980
+
+# Pflicht: GLPI High-Level API / OAuth2 Service Account
+GLPI_URL=https://CHANGE_ME_GLPI_HOST
+GLPI_API_VERSION=v2.3
+GLPI_CLIENT_ID=CHANGE_ME_GLPI_CLIENT_ID
+GLPI_CLIENT_SECRET=CHANGE_ME_GLPI_CLIENT_SECRET
+GLPI_USERNAME=CHANGE_ME_GLPI_USERNAME
+GLPI_PASSWORD=CHANGE_ME_GLPI_PASSWORD
+GLPI_AGENT_USER_ID=0
+GLPI_ALLOW_INSECURE_HTTP=false
+
+# Sichere Einführung
+DRY_RUN=true
+AUTO_CATEGORY=true
+AUTO_REPLY=false
+AUTO_PRIORITY=false
+AUTO_ESCALATION=false
+LOG_LEVEL=info
+
+# Agent WebUI
+WEB_USERNAME=admin
+WEB_PASSWORD=CHANGE_ME_LONG_AGENT_WEB_PASSWORD
+WEB_ALLOW_ANONYMOUS=false
+AI_CONTENT_LABEL_ENABLED=true
+WEBHOOK_SECRET=
+
+# Polling
+GLPI_ALLOWED_STATUS_IDS=1
+GLPI_POLL_INTERVAL=30s
+GLPI_POLL_LIMIT=50
+GLPI_TICKET_FILTER=status.id==1
+GLPI_TIMEOUT=20s
+
+# Ollama: entweder separater Ollama-Compose im gemeinsamen CORE_NETWORK
+# oder eine vom Agent-Container erreichbare externe URL eintragen.
+OLLAMA_URL=http://ollama:11434
+OLLAMA_URLS=http://ollama:11434
+OLLAMA_NODE_NAMES=ollama-01
+OLLAMA_NODE_WEIGHTS=1
+OLLAMA_MODEL=gemma4
+OLLAMA_EMBEDDING_MODEL=embeddinggemma
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
+OLLAMA_ROUTING_MODE=least_inflight
+OLLAMA_NODE_MAX_INFLIGHT=1
+OLLAMA_NODE_HEALTH_INTERVAL=15s
+OLLAMA_NODE_FAILURE_COOLDOWN=30s
+OLLAMA_NODE_REQUEST_TIMEOUT=10m
+OLLAMA_FAILOVER_ENABLED=true
+OLLAMA_FAILOVER_ATTEMPTS=0
+OLLAMA_REQUIRE_SAME_MODEL_DIGEST=true
+OLLAMA_REQUIRE_EMBEDDING_MODEL=true
+OLLAMA_TIMEOUT=10m
+OLLAMA_MAX_CONCURRENT=1
+OLLAMA_NUM_PREDICT=768
+OLLAMA_JSON_RETRIES=1
+OLLAMA_KEEP_ALIVE=10m
+OLLAMA_THINK=false
+
+# Lokale Knowledge/RAG-Funktion - KEIN NeuroForge erforderlich
+RAG_ENABLED=true
+KNOWLEDGE_VECTOR_BACKEND=local
+KNOWLEDGE_EMBEDDING_PROFILE=auto
+KNOWLEDGE_INDEX_MODE=incremental
+KNOWLEDGE_EMBED_BATCH_SIZE=64
+KNOWLEDGE_INDEX_SCAN_INTERVAL=5m
+KNOWLEDGE_CATEGORY_MODE=unscoped
+KNOWLEDGE_CATEGORY_MAP_FILE=/app/data/knowledge-category-map.json
+KNOWLEDGE_IGNORE_GLOBS=
+KNOWLEDGE_RETRIEVAL_FLOOR=0.30
+KNOWLEDGE_CANDIDATE_MAX_GAP=0.20
+KNOWLEDGE_TOP_K=6
+KNOWLEDGE_AUDIT_TOP_K=10
+KNOWLEDGE_MIN_SCORE=0.70
+KNOWLEDGE_EVIDENCE_WEIGHT_RETRIEVAL=0.45
+KNOWLEDGE_EVIDENCE_WEIGHT_AI=0.35
+KNOWLEDGE_EVIDENCE_WEIGHT_CATEGORY=0.20
+KNOWLEDGE_WEIGHT_SEMANTIC=0.45
+KNOWLEDGE_WEIGHT_TITLE=0.20
+KNOWLEDGE_WEIGHT_LEXICAL=0.20
+KNOWLEDGE_WEIGHT_KEYWORDS=0.075
+KNOWLEDGE_WEIGHT_CATEGORY=0.075
+KNOWLEDGE_CHUNK_WORDS=160
+KNOWLEDGE_CHUNK_OVERLAP_WORDS=30
+KNOWLEDGE_MAX_CHUNKS_PER_DOC=24
+KNOWLEDGE_MAX_QUERY_CHUNKS=64
+CATEGORY_PROMPT_LIMIT=80
+KNOWLEDGE_ALLOWED_SOURCES=internal-kb,glpi-kb,vendor-docs,vendor-docs-ms,vendor-docs-linux,vendor-docs-sec
+KNOWLEDGE_CATEGORY_SOURCES=internal-category
+KNOWLEDGE_AUTO_REPLY_SOURCES=internal-kb,glpi-kb,vendor-docs,vendor-docs-ms,vendor-docs-linux,vendor-docs-sec
+KNOWLEDGE_WEB_EDIT_ENABLED=false
+
+# GLPI-eigene KB optional zusätzlich synchronisieren
+GLPI_KB_ENABLED=false
+GLPI_KB_PATH=auto
+GLPI_KB_FILTER=
+GLPI_KB_LIMIT=500
+GLPI_KB_SYNC_INTERVAL=10m
+GLPI_KB_SOURCE=glpi-kb
+GLPI_KB_AUTO_REPLY=false
+GLPI_KB_AUTO_REPLY_CATEGORY_IDS=
+GLPI_KB_AUTO_REPLY_ALLOW_UNCATEGORIZED=false
+GLPI_KB_AUTO_REPLY_UNCATEGORIZED_ARTICLE_IDS=
+
+# Lokales Human-in-the-loop Category Learning bleibt möglich
+LEARNING_ENABLED=true
+LEARNING_MAX_EXAMPLES=500
+LEARNING_EXAMPLES_PER_CATEGORY=5
+# NeuroForge Outcome-Learning ist im stripped Core bewusst deaktiviert.
+OUTCOME_LEARNING_ENABLED=false
+OUTCOME_RETRIEVAL_ENABLED=false
+OUTCOME_LEARNING_FAIL_OPEN=true
+OUTCOME_LEARNING_MAX_OUTCOMES=2000
+OUTCOME_RETRIEVAL_SEARCH_K=6
+OUTCOME_RETRIEVAL_MIN_SIMILARITY=0.58
+OUTCOME_RETRIEVAL_FAIL_OPEN=true
+
+# Kommunikation / Policy
+COMMUNICATION_LANGUAGE=de-DE
+COMMUNICATION_STYLE=formal
+COMMUNICATION_SALUTATION=Guten Tag,
+COMMUNICATION_CLOSING=Mit freundlichen Grüßen
+COMMUNICATION_SIGNATURE=IT-Service
+CATEGORY_CONFIDENCE=0.90
+REPLY_CONFIDENCE=0.97
+
+# Optionale Kontextfunktionen im Core zunächst deaktiviert
+CONTEXT_ENABLED=false
+CONTEXT_TIMEOUT=12s
+CONTEXT_RELEVANCE_MIN_SCORE=0.20
+CONTEXT_BLOCK_AUTO_REPLY_ON_ERRORS=true
+CONTEXT_BLOCK_AUTO_REPLY_ON_INCIDENT=true
+CONTEXT_STATUS_REPLY_ENABLED=false
+CHANGE_CALENDAR_ENABLED=false
+MAJOR_INCIDENTS_ENABLED=false
+USER_DEVICE_CONTEXT_ENABLED=false
+UPTIME_KUMA_ENABLED=false
+
+# Optionale Priorisierung / Eskalation im stripped Standard aus
+PRIORITY_ENABLED=false
+PRIORITY_CONFIDENCE=0.88
+PRIORITY_ANALYSIS_TIMEOUT=45s
+PRIORITY_MAX_INCREASE=1
+ESCALATION_ENABLED=false
+ESCALATION_SCAN_INTERVAL=15m
+ESCALATION_MIN_AGE=4h
+ESCALATION_MIN_INACTIVITY=2h
+ESCALATION_ANALYSIS_TIMEOUT=45s
+ESCALATION_CONFIDENCE=0.88
+ESCALATION_MAX_LEVEL=3
+ESCALATION_ALLOWED_ACTIONS=none
+
+QUEUE_SIZE=256
+WORKERS=2
diff --git ab/deployments/agent/docker-compose.build.yml b/deployments/agent/docker-compose.build.yml
new file mode 100644
index 0000000..37248cf
--- /dev/null
+++ b/deployments/agent/docker-compose.build.yml
@@ -0,0 +1,10 @@
+services:
+ agent-data-init:
+ image: glpi-ai-agent-data-init-local:1.6.2
+ build:
+ context: ../../services/agent
+ target: data-init
+ agent:
+ image: glpi-ai-agent-local:1.6.2
+ build:
+ context: ../../services/agent
diff --git ab/deployments/agent/docker-compose.yml b/deployments/agent/docker-compose.yml
new file mode 100644
index 0000000..856b750
--- /dev/null
+++ b/deployments/agent/docker-compose.yml
@@ -0,0 +1,63 @@
+name: glpi-ai-agent-core
+services:
+ agent-data-init:
+ image: ${AGENT_DATA_INIT_IMAGE:-git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG:-1.6.2}}
+ restart: "no"
+ user: "0:0"
+ volumes:
+ - ${CORE_DATA_ROOT:-../../runtime}/agent-data:/app/data
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ cap_add:
+ - CHOWN
+ - FOWNER
+
+ agent:
+ image: ${AGENT_IMAGE:-git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:${IMAGE_TAG:-1.6.2}}
+ restart: unless-stopped
+ env_file:
+ - .env
+ environment:
+ HTTP_ADDR: :8080
+ DATA_DIR: /app/data
+ KNOWLEDGE_DIR: /app/knowledge
+ KNOWLEDGE_VECTOR_BACKEND: local
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
+ OUTCOME_LEARNING_ENABLED: "false"
+ OUTCOME_RETRIEVAL_ENABLED: "false"
+ BRAIN_ACTIVITY_URL: ""
+ BRAIN_ACTIVITY_API_KEY: ""
+ NEUROFORGE_URL: ""
+ NEUROFORGE_API_KEY: ""
+ CONTROL_READ_TOKEN: ""
+ ports:
+ - ${AGENT_BIND_IP:-127.0.0.1}:${AGENT_HOST_PORT:-9980}:8080
+ volumes:
+ - ${CORE_DATA_ROOT:-../../runtime}/agent-data:/app/data
+ - ${CORE_DATA_ROOT:-../../runtime}/knowledge:/app/knowledge:ro
+ depends_on:
+ agent-data-init:
+ condition: service_completed_successfully
+ networks:
+ - core
+ read_only: true
+ tmpfs:
+ - /tmp:size=64m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ healthcheck:
+ test: ["CMD", "/app/glpi-ai-agent", "healthcheck"]
+ interval: 15s
+ timeout: 3s
+ retries: 8
+ start_period: 10s
+ stop_grace_period: 20s
+
+networks:
+ core:
+ external: true
+ name: ${CORE_NETWORK:-glpi-ai-core}
diff --git ab/deployments/combined/.env b/deployments/combined/.env
new file mode 100644
index 0000000..5f77328
--- /dev/null
+++ b/deployments/combined/.env
@@ -0,0 +1,183 @@
+###############################################################################
+# GLPI AI Agent - standalone / stripped core
+###############################################################################
+IMAGE_TAG=1.6.2
+AGENT_IMAGE=git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:1.6.2
+AGENT_DATA_INIT_IMAGE=git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:1.6.2
+CORE_NETWORK=glpi-ai-core
+CORE_DATA_ROOT=../../runtime
+AGENT_BIND_IP=127.0.0.1
+AGENT_HOST_PORT=9980
+
+# Pflicht: GLPI High-Level API / OAuth2 Service Account
+GLPI_URL=https://CHANGE_ME_GLPI_HOST
+GLPI_API_VERSION=v2.3
+GLPI_CLIENT_ID=CHANGE_ME_GLPI_CLIENT_ID
+GLPI_CLIENT_SECRET=CHANGE_ME_GLPI_CLIENT_SECRET
+GLPI_USERNAME=CHANGE_ME_GLPI_USERNAME
+GLPI_PASSWORD=CHANGE_ME_GLPI_PASSWORD
+GLPI_AGENT_USER_ID=28
+GLPI_ALLOW_INSECURE_HTTP=false
+
+# Sichere Einführung
+DRY_RUN=true
+AUTO_CATEGORY=true
+AUTO_REPLY=false
+AUTO_PRIORITY=false
+AUTO_ESCALATION=false
+LOG_LEVEL=info
+
+# Agent WebUI
+WEB_USERNAME=admin
+WEB_PASSWORD=CHANGE_ME_LONG_AGENT_WEB_PASSWORD
+WEB_ALLOW_ANONYMOUS=false
+AI_CONTENT_LABEL_ENABLED=true
+WEBHOOK_SECRET=
+
+# Polling
+GLPI_ALLOWED_STATUS_IDS=1
+GLPI_POLL_INTERVAL=30s
+GLPI_POLL_LIMIT=50
+GLPI_TICKET_FILTER=status.id==1
+GLPI_TIMEOUT=20s
+
+# Ollama: entweder separater Ollama-Compose im gemeinsamen CORE_NETWORK
+# oder eine vom Agent-Container erreichbare externe URL eintragen.
+OLLAMA_URL=http://10.2.10.40:8080
+OLLAMA_URLS=http://10.2.10.40:8080
+OLLAMA_NODE_NAMES=ollama-gateway
+OLLAMA_NODE_WEIGHTS=1
+OLLAMA_MODEL=gemma4
+OLLAMA_EMBEDDING_MODEL=embeddinggemma
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
+OLLAMA_ROUTING_MODE=least_inflight
+OLLAMA_NODE_MAX_INFLIGHT=1
+OLLAMA_NODE_HEALTH_INTERVAL=15s
+OLLAMA_NODE_FAILURE_COOLDOWN=30s
+OLLAMA_NODE_REQUEST_TIMEOUT=10m
+OLLAMA_FAILOVER_ENABLED=true
+OLLAMA_FAILOVER_ATTEMPTS=0
+OLLAMA_REQUIRE_SAME_MODEL_DIGEST=true
+OLLAMA_REQUIRE_EMBEDDING_MODEL=true
+OLLAMA_TIMEOUT=10m
+OLLAMA_MAX_CONCURRENT=1
+OLLAMA_NUM_PREDICT=768
+OLLAMA_JSON_RETRIES=1
+OLLAMA_KEEP_ALIVE=10m
+OLLAMA_THINK=false
+
+# Lokale Knowledge/RAG-Funktion - KEIN NeuroForge erforderlich
+RAG_ENABLED=true
+KNOWLEDGE_VECTOR_BACKEND=local
+KNOWLEDGE_EMBEDDING_PROFILE=auto
+KNOWLEDGE_INDEX_MODE=incremental
+KNOWLEDGE_EMBED_BATCH_SIZE=64
+KNOWLEDGE_INDEX_SCAN_INTERVAL=5m
+KNOWLEDGE_CATEGORY_MODE=unscoped
+KNOWLEDGE_CATEGORY_MAP_FILE=/app/data/knowledge-category-map.json
+KNOWLEDGE_IGNORE_GLOBS=
+KNOWLEDGE_RETRIEVAL_FLOOR=0.30
+KNOWLEDGE_CANDIDATE_MAX_GAP=0.20
+KNOWLEDGE_TOP_K=6
+KNOWLEDGE_AUDIT_TOP_K=10
+KNOWLEDGE_MIN_SCORE=0.70
+KNOWLEDGE_EVIDENCE_WEIGHT_RETRIEVAL=0.45
+KNOWLEDGE_EVIDENCE_WEIGHT_AI=0.35
+KNOWLEDGE_EVIDENCE_WEIGHT_CATEGORY=0.20
+KNOWLEDGE_WEIGHT_SEMANTIC=0.45
+KNOWLEDGE_WEIGHT_TITLE=0.20
+KNOWLEDGE_WEIGHT_LEXICAL=0.20
+KNOWLEDGE_WEIGHT_KEYWORDS=0.075
+KNOWLEDGE_WEIGHT_CATEGORY=0.075
+KNOWLEDGE_CHUNK_WORDS=160
+KNOWLEDGE_CHUNK_OVERLAP_WORDS=30
+KNOWLEDGE_MAX_CHUNKS_PER_DOC=24
+KNOWLEDGE_MAX_QUERY_CHUNKS=64
+CATEGORY_PROMPT_LIMIT=80
+KNOWLEDGE_ALLOWED_SOURCES=internal-kb,glpi-kb,vendor-docs,vendor-docs-ms,vendor-docs-linux,vendor-docs-sec
+KNOWLEDGE_CATEGORY_SOURCES=internal-category
+KNOWLEDGE_AUTO_REPLY_SOURCES=internal-kb,glpi-kb,vendor-docs,vendor-docs-ms,vendor-docs-linux,vendor-docs-sec
+KNOWLEDGE_WEB_EDIT_ENABLED=false
+
+# GLPI-eigene KB optional zusätzlich synchronisieren
+GLPI_KB_ENABLED=false
+GLPI_KB_PATH=auto
+GLPI_KB_FILTER=
+GLPI_KB_LIMIT=500
+GLPI_KB_SYNC_INTERVAL=10m
+GLPI_KB_SOURCE=glpi-kb
+GLPI_KB_AUTO_REPLY=false
+GLPI_KB_AUTO_REPLY_CATEGORY_IDS=
+GLPI_KB_AUTO_REPLY_ALLOW_UNCATEGORIZED=false
+GLPI_KB_AUTO_REPLY_UNCATEGORIZED_ARTICLE_IDS=
+
+# Lokales Human-in-the-loop Category Learning bleibt möglich
+LEARNING_ENABLED=true
+LEARNING_MAX_EXAMPLES=500
+LEARNING_EXAMPLES_PER_CATEGORY=5
+# NeuroForge Outcome-Learning ist im stripped Core bewusst deaktiviert.
+OUTCOME_LEARNING_ENABLED=false
+OUTCOME_RETRIEVAL_ENABLED=false
+OUTCOME_LEARNING_FAIL_OPEN=true
+OUTCOME_LEARNING_MAX_OUTCOMES=2000
+OUTCOME_RETRIEVAL_SEARCH_K=6
+OUTCOME_RETRIEVAL_MIN_SIMILARITY=0.58
+OUTCOME_RETRIEVAL_FAIL_OPEN=true
+
+# Kommunikation / Policy
+COMMUNICATION_LANGUAGE=de-DE
+COMMUNICATION_STYLE=formal
+COMMUNICATION_SALUTATION=Guten Tag,
+COMMUNICATION_CLOSING=Mit freundlichen Grüßen
+COMMUNICATION_SIGNATURE=IT-Service
+CATEGORY_CONFIDENCE=0.90
+REPLY_CONFIDENCE=0.97
+
+# Optionale Kontextfunktionen im Core zunächst deaktiviert
+CONTEXT_ENABLED=false
+CONTEXT_TIMEOUT=12s
+CONTEXT_RELEVANCE_MIN_SCORE=0.20
+CONTEXT_BLOCK_AUTO_REPLY_ON_ERRORS=true
+CONTEXT_BLOCK_AUTO_REPLY_ON_INCIDENT=true
+CONTEXT_STATUS_REPLY_ENABLED=false
+CHANGE_CALENDAR_ENABLED=false
+MAJOR_INCIDENTS_ENABLED=false
+USER_DEVICE_CONTEXT_ENABLED=false
+UPTIME_KUMA_ENABLED=false
+
+# Optionale Priorisierung / Eskalation im stripped Standard aus
+PRIORITY_ENABLED=false
+PRIORITY_CONFIDENCE=0.88
+PRIORITY_ANALYSIS_TIMEOUT=45s
+PRIORITY_MAX_INCREASE=1
+ESCALATION_ENABLED=false
+ESCALATION_SCAN_INTERVAL=15m
+ESCALATION_MIN_AGE=4h
+ESCALATION_MIN_INACTIVITY=2h
+ESCALATION_ANALYSIS_TIMEOUT=45s
+ESCALATION_CONFIDENCE=0.88
+ESCALATION_MAX_LEVEL=3
+ESCALATION_ALLOWED_ACTIONS=none
+
+QUEUE_SIZE=256
+WORKERS=2
+
+# Combined stack / Ollama / Knowledge UI
+OLLAMA_IMAGE=ollama/ollama:0.33.2
+OLLAMA_BIND_IP=127.0.0.1
+OLLAMA_HOST_PORT=11434
+KNOWLEDGE_IMAGE=git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:1.6.2
+KNOWLEDGE_BIND_IP=127.0.0.1
+KNOWLEDGE_HOST_PORT=9981
+KB_APP_MODE=editor
+APP_TITLE=Knowledge Base Editor
+APP_SUBTITLE=GLPI AI Agent Core
+AUTO_RELOAD_INTERVAL=60s
+BASIC_AUTH_USER=admin
+BASIC_AUTH_PASSWORD=CHANGE_ME_LONG_KNOWLEDGE_PASSWORD
+AI_FALLBACK_ENABLED=false
+OLLAMA_BASE_URL=http://10.2.10.40:8080
+OLLAMA_STAGING_AUTO_REPLY=false
+OLLAMA_STAGING_MIN_SCORE=0.78
+KB_INTEGRATION_TOKEN=
diff --git ab/deployments/combined/docker-compose.build.yml b/deployments/combined/docker-compose.build.yml
new file mode 100644
index 0000000..2a2e3ac
--- /dev/null
+++ b/deployments/combined/docker-compose.build.yml
@@ -0,0 +1,14 @@
+services:
+ agent-data-init:
+ image: glpi-ai-agent-data-init-local:1.6.2
+ build:
+ context: ../../services/agent
+ target: data-init
+ agent:
+ image: glpi-ai-agent-local:1.6.2
+ build:
+ context: ../../services/agent
+ knowledge:
+ image: glpi-ai-knowledge-local:1.6.2
+ build:
+ context: ../../services/knowledge
diff --git ab/deployments/combined/docker-compose.yml b/deployments/combined/docker-compose.yml
new file mode 100644
index 0000000..7f4b170
--- /dev/null
+++ b/deployments/combined/docker-compose.yml
@@ -0,0 +1,100 @@
+name: glpi-ai-core
+services:
+ ollama:
+ image: ${OLLAMA_IMAGE:-ollama/ollama:0.33.2}
+ restart: unless-stopped
+ volumes:
+ - ${CORE_DATA_ROOT:-../../runtime}/ollama:/root/.ollama
+ ports:
+ - ${OLLAMA_BIND_IP:-127.0.0.1}:${OLLAMA_HOST_PORT:-11434}:11434
+ networks:
+ core:
+ aliases: [ollama]
+ security_opt:
+ - no-new-privileges:true
+
+ agent-data-init:
+ image: ${AGENT_DATA_INIT_IMAGE:-git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG:-1.6.2}}
+ restart: "no"
+ user: "0:0"
+ volumes:
+ - ${CORE_DATA_ROOT:-../../runtime}/agent-data:/app/data
+ security_opt:
+ - no-new-privileges:true
+ cap_drop: [ALL]
+ cap_add: [CHOWN, FOWNER]
+
+ agent:
+ image: ${AGENT_IMAGE:-git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:${IMAGE_TAG:-1.6.2}}
+ restart: unless-stopped
+ env_file: [.env]
+ environment:
+ HTTP_ADDR: :8080
+ DATA_DIR: /app/data
+ KNOWLEDGE_DIR: /app/knowledge
+ OLLAMA_URL: http://ollama:11434
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
+ OLLAMA_URLS: http://ollama:11434
+ KNOWLEDGE_VECTOR_BACKEND: local
+ OUTCOME_LEARNING_ENABLED: "false"
+ OUTCOME_RETRIEVAL_ENABLED: "false"
+ BRAIN_ACTIVITY_URL: ""
+ BRAIN_ACTIVITY_API_KEY: ""
+ NEUROFORGE_URL: ""
+ NEUROFORGE_API_KEY: ""
+ CONTROL_READ_TOKEN: ""
+ ports:
+ - ${AGENT_BIND_IP:-127.0.0.1}:${AGENT_HOST_PORT:-9980}:8080
+ volumes:
+ - ${CORE_DATA_ROOT:-../../runtime}/agent-data:/app/data
+ - ${CORE_DATA_ROOT:-../../runtime}/knowledge:/app/knowledge:ro
+ depends_on:
+ agent-data-init:
+ condition: service_completed_successfully
+ ollama:
+ condition: service_started
+ networks: [core]
+ read_only: true
+ tmpfs: [/tmp:size=64m,mode=1777]
+ security_opt: [no-new-privileges:true]
+ cap_drop: [ALL]
+ healthcheck:
+ test: ["CMD", "/app/glpi-ai-agent", "healthcheck"]
+ interval: 15s
+ timeout: 3s
+ retries: 8
+ start_period: 10s
+ stop_grace_period: 20s
+
+ knowledge:
+ image: ${KNOWLEDGE_IMAGE:-git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:${IMAGE_TAG:-1.6.2}}
+ restart: unless-stopped
+ env_file: [.env]
+ environment:
+ APP_MODE: ${KB_APP_MODE:-editor}
+ DATA_DIR: /data/knowledge
+ BACKUP_DIR: /data/backups
+ STAGING_DIR: /data/staging
+ LISTEN_ADDR: :8080
+ OLLAMA_BASE_URL: http://ollama:11434
+ BRAIN_ACTIVITY_URL: ""
+ BRAIN_ACTIVITY_API_KEY: ""
+ ports:
+ - ${KNOWLEDGE_BIND_IP:-127.0.0.1}:${KNOWLEDGE_HOST_PORT:-9981}:8080
+ volumes:
+ - ${CORE_DATA_ROOT:-../../runtime}/knowledge:/data/knowledge:rw
+ - ${CORE_DATA_ROOT:-../../runtime}/staging:/data/staging:rw
+ - ${CORE_DATA_ROOT:-../../runtime}/backups:/data/backups:rw
+ depends_on:
+ ollama:
+ condition: service_started
+ networks: [core]
+ read_only: true
+ tmpfs: [/tmp:size=32m,mode=1777]
+ security_opt: [no-new-privileges:true]
+ cap_drop: [ALL]
+ stop_grace_period: 35s
+
+networks:
+ core:
+ name: ${CORE_NETWORK:-glpi-ai-core}
diff --git ab/deployments/cpu-subagent/.env b/deployments/cpu-subagent/.env
new file mode 100644
index 0000000..a7a93b5
--- /dev/null
+++ b/deployments/cpu-subagent/.env
@@ -0,0 +1,17 @@
+# NeuroForge v1.6.2 CPU Subagent
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
+IMAGE_TAG=1.6.2
+
+# Reachable Master endpoint. Use TLS/reverse proxy outside a trusted LAN.
+NEUROFORGE_MASTER_URL=http://192.0.2.10:8090
+NEUROFORGE_WORKER_TOKEN=CHANGE_ME_SHARED_WORKER_TOKEN
+NEUROFORGE_CPU_WORKER_ID=cpu-node-01
+NEUROFORGE_CPU_WORKER_CAPABILITIES=cpu,vector.relink
+NEUROFORGE_CPU_WORKER_CONCURRENCY=4
+NEUROFORGE_WORKER_HEARTBEAT_INTERVAL=15s
+
+# Optional Prometheus host telemetry (start profile monitoring).
+NODE_EXPORTER_IMAGE=prom/node-exporter:v1.12.1
+EXPORTER_BIND_ADDRESS=0.0.0.0
+NODE_EXPORTER_HOST_PORT=9100
diff --git ab/deployments/cpu-subagent/docker-compose.yml b/deployments/cpu-subagent/docker-compose.yml
new file mode 100644
index 0000000..18c716a
--- /dev/null
+++ b/deployments/cpu-subagent/docker-compose.yml
@@ -0,0 +1,46 @@
+name: neuroforge-cpu-subagent
+services:
+ cpu-subagent:
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set
+ immutable IMAGE_TAG}
+ command:
+ - -server
+ - ${NEUROFORGE_MASTER_URL:?Set reachable NeuroForge master URL}
+ - -id
+ - ${NEUROFORGE_CPU_WORKER_ID:?Set CPU worker ID}
+ restart: unless-stopped
+ environment:
+ NEUROFORGE_WORKER_TOKEN: ${NEUROFORGE_WORKER_TOKEN:?Set worker token shared
+ with master}
+ NEUROFORGE_WORKER_RESOURCE_CLASS: cpu
+ NEUROFORGE_WORKER_CAPABILITIES: ${NEUROFORGE_CPU_WORKER_CAPABILITIES:-cpu,vector.relink}
+ NEUROFORGE_WORKER_MAX_CONCURRENCY: ${NEUROFORGE_CPU_WORKER_CONCURRENCY:-2}
+ NEUROFORGE_WORKER_HEARTBEAT_INTERVAL: ${NEUROFORGE_WORKER_HEARTBEAT_INTERVAL:-15s}
+ read_only: true
+ tmpfs:
+ - /tmp:size=32m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ node-exporter:
+ profiles:
+ - monitoring
+ image: ${NODE_EXPORTER_IMAGE:-prom/node-exporter:v1.12.1}
+ restart: unless-stopped
+ command:
+ - --path.procfs=/host/proc
+ - --path.sysfs=/host/sys
+ - --path.rootfs=/rootfs
+ - --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
+ ports:
+ - ${EXPORTER_BIND_ADDRESS:-0.0.0.0}:${NODE_EXPORTER_HOST_PORT:-9100}:9100
+ volumes:
+ - /proc:/host/proc:ro
+ - /sys:/host/sys:ro
+ - /:/rootfs:ro,rslave
+ read_only: true
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
diff --git ab/deployments/cpu-subagent/preflight.sh b/deployments/cpu-subagent/preflight.sh
new file mode 100755
index 0000000..435c650
--- /dev/null
+++ b/deployments/cpu-subagent/preflight.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env sh
+set -eu
+[ -f .env ] || { echo 'missing .env'; exit 1; }
+for k in IMAGE_TAG NEUROFORGE_MASTER_URL NEUROFORGE_WORKER_TOKEN NEUROFORGE_CPU_WORKER_ID; do
+ v=$(awk -F= -v key="$k" '$1==key {sub(/^[^=]*=/,""); print; exit}' .env)
+ [ -n "$v" ] || { echo "missing: $k"; exit 1; }
+done
+if grep -Eq '192\.0\.2\.|example\.invalid|CHANGE_ME|YOUR-' .env; then echo 'placeholder/example values remain in .env'; exit 1; fi
+[ "$(awk -F= '$1=="IMAGE_TAG"{print $2}' .env)" = "1.6.2" ] || { echo 'IMAGE_TAG must be 1.6.2'; exit 1; }
+docker compose --profile monitoring config >/dev/null
+echo 'cpu subagent preflight: OK'
diff --git ab/deployments/gpu-subagent/.env b/deployments/gpu-subagent/.env
new file mode 100644
index 0000000..224a325
--- /dev/null
+++ b/deployments/gpu-subagent/.env
@@ -0,0 +1,30 @@
+# NeuroForge v1.6.2 GPU Subagent + Ollama
+IMAGE_TAG=1.6.2
+
+# Reachable Master endpoint. Use TLS/reverse proxy outside a trusted LAN.
+NEUROFORGE_MASTER_URL=http://192.0.2.10:8090
+NEUROFORGE_WORKER_TOKEN=CHANGE_ME_SHARED_WORKER_TOKEN
+NEUROFORGE_GPU_WORKER_ID=gpu-node-01
+NEUROFORGE_GPU_WORKER_CAPABILITIES=gpu,model.chat,model.embed
+NEUROFORGE_GPU_WORKER_CONCURRENCY=1
+NEUROFORGE_WORKER_HEARTBEAT_INTERVAL=15s
+
+# Ollama runs locally on this GPU host and is also exposed to the Master host.
+OLLAMA_IMAGE=ollama/ollama:0.33.2
+OLLAMA_BIND_ADDRESS=0.0.0.0
+OLLAMA_HOST_PORT=11434
+OLLAMA_MODEL=gemma4
+OLLAMA_EMBEDDING_MODEL=embeddinggemma
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
+# Optional NeuroForge GPU-worker override; falls back to OLLAMA_API_KEY.
+NEUROFORGE_WORKER_OLLAMA_API_KEY=
+NEUROFORGE_OLLAMA_NUM_CTX=8192
+OLLAMA_KEEP_ALIVE=10m
+
+# Optional Prometheus host/GPU telemetry (start profile monitoring).
+NODE_EXPORTER_IMAGE=prom/node-exporter:v1.12.1
+DCGM_EXPORTER_IMAGE=nvcr.io/nvidia/k8s/dcgm-exporter:4.6.0-4.8.3-distroless
+EXPORTER_BIND_ADDRESS=0.0.0.0
+NODE_EXPORTER_HOST_PORT=9100
+DCGM_EXPORTER_HOST_PORT=9400
diff --git ab/deployments/gpu-subagent/docker-compose.yml b/deployments/gpu-subagent/docker-compose.yml
new file mode 100644
index 0000000..1787d81
--- /dev/null
+++ b/deployments/gpu-subagent/docker-compose.yml
@@ -0,0 +1,120 @@
+name: neuroforge-gpu-subagent
+services:
+ ollama:
+ image: ${OLLAMA_IMAGE:-ollama/ollama:0.33.2}
+ restart: unless-stopped
+ gpus: all
+ environment:
+ OLLAMA_HOST: 0.0.0.0:11434
+ OLLAMA_KEEP_ALIVE: ${OLLAMA_KEEP_ALIVE:-10m}
+ ports:
+ - ${OLLAMA_BIND_ADDRESS:-0.0.0.0}:${OLLAMA_HOST_PORT:-11434}:11434
+ volumes:
+ - ollama-data:/root/.ollama
+ security_opt:
+ - no-new-privileges:true
+ healthcheck:
+ test:
+ - CMD
+ - ollama
+ - list
+ interval: 15s
+ timeout: 5s
+ retries: 20
+ start_period: 15s
+ gpu-subagent:
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set
+ immutable IMAGE_TAG}
+ command:
+ - -server
+ - ${NEUROFORGE_MASTER_URL:?Set reachable NeuroForge master URL}
+ - -id
+ - ${NEUROFORGE_GPU_WORKER_ID:?Set GPU worker ID}
+ restart: unless-stopped
+ environment:
+ NEUROFORGE_WORKER_TOKEN: ${NEUROFORGE_WORKER_TOKEN:?Set worker token shared
+ with master}
+ NEUROFORGE_WORKER_RESOURCE_CLASS: gpu
+ NEUROFORGE_WORKER_CAPABILITIES: ${NEUROFORGE_GPU_WORKER_CAPABILITIES:-gpu,model.chat,model.embed}
+ NEUROFORGE_WORKER_MAX_CONCURRENCY: ${NEUROFORGE_GPU_WORKER_CONCURRENCY:-1}
+ NEUROFORGE_WORKER_HEARTBEAT_INTERVAL: ${NEUROFORGE_WORKER_HEARTBEAT_INTERVAL:-15s}
+ NEUROFORGE_WORKER_OLLAMA_URL: http://ollama:11434
+ NEUROFORGE_WORKER_OLLAMA_API_KEY: ${NEUROFORGE_WORKER_OLLAMA_API_KEY:-}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
+ NEUROFORGE_WORKER_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma4}
+ NEUROFORGE_WORKER_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
+ NEUROFORGE_WORKER_OLLAMA_NUM_CTX: ${NEUROFORGE_OLLAMA_NUM_CTX:-8192}
+ NEUROFORGE_WORKER_OLLAMA_KEEP_ALIVE: ${OLLAMA_KEEP_ALIVE:-10m}
+ depends_on:
+ ollama-model-chat-init:
+ condition: service_completed_successfully
+ ollama-model-embed-init:
+ condition: service_completed_successfully
+ read_only: true
+ tmpfs:
+ - /tmp:size=32m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ node-exporter:
+ profiles:
+ - monitoring
+ image: ${NODE_EXPORTER_IMAGE:-prom/node-exporter:v1.12.1}
+ restart: unless-stopped
+ command:
+ - --path.procfs=/host/proc
+ - --path.sysfs=/host/sys
+ - --path.rootfs=/rootfs
+ - --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
+ ports:
+ - ${EXPORTER_BIND_ADDRESS:-0.0.0.0}:${NODE_EXPORTER_HOST_PORT:-9100}:9100
+ volumes:
+ - /proc:/host/proc:ro
+ - /sys:/host/sys:ro
+ - /:/rootfs:ro,rslave
+ read_only: true
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ dcgm-exporter:
+ profiles:
+ - monitoring
+ image: ${DCGM_EXPORTER_IMAGE:-nvcr.io/nvidia/k8s/dcgm-exporter:4.6.0-4.8.3-distroless}
+ restart: unless-stopped
+ gpus: all
+ cap_add:
+ - SYS_ADMIN
+ ports:
+ - ${EXPORTER_BIND_ADDRESS:-0.0.0.0}:${DCGM_EXPORTER_HOST_PORT:-9400}:9400
+ security_opt:
+ - no-new-privileges:true
+ ollama-model-chat-init:
+ image: ${OLLAMA_IMAGE:-ollama/ollama:0.33.2}
+ restart: 'no'
+ command:
+ - pull
+ - ${OLLAMA_MODEL:-gemma4}
+ environment:
+ OLLAMA_HOST: http://ollama:11434
+ depends_on:
+ ollama:
+ condition: service_healthy
+ security_opt:
+ - no-new-privileges:true
+ ollama-model-embed-init:
+ image: ${OLLAMA_IMAGE:-ollama/ollama:0.33.2}
+ restart: 'no'
+ command:
+ - pull
+ - ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
+ environment:
+ OLLAMA_HOST: http://ollama:11434
+ depends_on:
+ ollama:
+ condition: service_healthy
+ security_opt:
+ - no-new-privileges:true
+volumes:
+ ollama-data: null
diff --git ab/deployments/gpu-subagent/preflight.sh b/deployments/gpu-subagent/preflight.sh
new file mode 100755
index 0000000..2061a06
--- /dev/null
+++ b/deployments/gpu-subagent/preflight.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env sh
+set -eu
+[ -f .env ] || { echo 'missing .env'; exit 1; }
+for k in IMAGE_TAG NEUROFORGE_MASTER_URL NEUROFORGE_WORKER_TOKEN NEUROFORGE_GPU_WORKER_ID OLLAMA_MODEL OLLAMA_EMBEDDING_MODEL; do
+ v=$(awk -F= -v key="$k" '$1==key {sub(/^[^=]*=/,""); print; exit}' .env)
+ [ -n "$v" ] || { echo "missing: $k"; exit 1; }
+done
+if grep -Eq '192\.0\.2\.|example\.invalid|CHANGE_ME|YOUR-' .env; then echo 'placeholder/example values remain in .env'; exit 1; fi
+[ "$(awk -F= '$1=="IMAGE_TAG"{print $2}' .env)" = "1.6.2" ] || { echo 'IMAGE_TAG must be 1.6.2'; exit 1; }
+docker compose --profile monitoring config >/dev/null
+echo 'gpu subagent preflight: OK'
diff --git ab/deployments/knowledge/.env b/deployments/knowledge/.env
new file mode 100644
index 0000000..ab2c3c7
--- /dev/null
+++ b/deployments/knowledge/.env
@@ -0,0 +1,33 @@
+###############################################################################
+# Knowledge Editor/Search - standalone / stripped core
+###############################################################################
+IMAGE_TAG=1.6.2
+KNOWLEDGE_IMAGE=git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:1.6.2
+CORE_NETWORK=glpi-ai-core
+CORE_DATA_ROOT=../../runtime
+KNOWLEDGE_BIND_IP=127.0.0.1
+KNOWLEDGE_HOST_PORT=9981
+
+KB_APP_MODE=editor
+APP_TITLE=Knowledge Base Editor
+APP_SUBTITLE=GLPI AI Agent Core
+AUTO_RELOAD_INTERVAL=60s
+
+# Für Produktion setzen
+BASIC_AUTH_USER=admin
+BASIC_AUTH_PASSWORD=CHANGE_ME_LONG_KNOWLEDGE_PASSWORD
+
+# Ohne KI-Fallback ist Knowledge vollständig unabhängig von Ollama.
+AI_FALLBACK_ENABLED=false
+# Nur bei AI_FALLBACK_ENABLED=true relevant:
+OLLAMA_BASE_URL=http://ollama:11434
+OLLAMA_MODEL=gemma4
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
+OLLAMA_TIMEOUT=10m
+OLLAMA_MAX_CONCURRENT=1
+OLLAMA_STAGING_AUTO_REPLY=false
+OLLAMA_STAGING_MIN_SCORE=0.78
+
+# Nur für externe Staging-API nötig; Browser-Editor funktioniert auch leer.
+KB_INTEGRATION_TOKEN=
diff --git ab/deployments/knowledge/docker-compose.build.yml b/deployments/knowledge/docker-compose.build.yml
new file mode 100644
index 0000000..82f3010
--- /dev/null
+++ b/deployments/knowledge/docker-compose.build.yml
@@ -0,0 +1,5 @@
+services:
+ knowledge:
+ image: glpi-ai-knowledge-local:1.6.2
+ build:
+ context: ../../services/knowledge
diff --git ab/deployments/knowledge/docker-compose.yml b/deployments/knowledge/docker-compose.yml
new file mode 100644
index 0000000..b0b7e05
--- /dev/null
+++ b/deployments/knowledge/docker-compose.yml
@@ -0,0 +1,37 @@
+name: glpi-ai-knowledge-core
+services:
+ knowledge:
+ image: ${KNOWLEDGE_IMAGE:-git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:${IMAGE_TAG:-1.6.2}}
+ restart: unless-stopped
+ env_file:
+ - .env
+ environment:
+ APP_MODE: ${KB_APP_MODE:-editor}
+ DATA_DIR: /data/knowledge
+ BACKUP_DIR: /data/backups
+ STAGING_DIR: /data/staging
+ LISTEN_ADDR: :8080
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
+ BRAIN_ACTIVITY_URL: ""
+ BRAIN_ACTIVITY_API_KEY: ""
+ ports:
+ - ${KNOWLEDGE_BIND_IP:-127.0.0.1}:${KNOWLEDGE_HOST_PORT:-9981}:8080
+ volumes:
+ - ${CORE_DATA_ROOT:-../../runtime}/knowledge:/data/knowledge:rw
+ - ${CORE_DATA_ROOT:-../../runtime}/staging:/data/staging:rw
+ - ${CORE_DATA_ROOT:-../../runtime}/backups:/data/backups:rw
+ networks:
+ - core
+ read_only: true
+ tmpfs:
+ - /tmp:size=32m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ stop_grace_period: 35s
+
+networks:
+ core:
+ external: true
+ name: ${CORE_NETWORK:-glpi-ai-core}
diff --git ab/deployments/master/.env b/deployments/master/.env
new file mode 100644
index 0000000..4efd6fd
--- /dev/null
+++ b/deployments/master/.env
@@ -0,0 +1,957 @@
+###############################################################################
+# GLPI NEUROFORGE MEGA v1.6.2 - VOLLSTÄNDIGE .ENV.example
+#
+# Diese Datei ist die zentrale Konfiguration für docker compose.
+# Sie enthält:
+# - GLPI AI Agent (vollständige produktive Optionen)
+# - NeuroForge / Vector Backend / Controlled Learning
+# - Knowledge Editor
+# - Control Center
+# - optional SearXNG Research
+# - optionale Codebase-Memory-UI
+#
+# SICHERER START:
+# DRY_RUN=true
+# AUTO_REPLY=false
+# AUTO_PRIORITY=false
+# AUTO_ESCALATION=false
+# NEUROFORGE_RESEARCH_ENABLED=false
+# NEUROFORGE_AUTONOMY_ENABLED=false
+#
+# WICHTIG:
+# Interne Container-Adressen/Ports wie HTTP_ADDR, DATA_DIR, KNOWLEDGE_DIR,
+# OLLAMA_URL, NEUROFORGE_URL und BRAIN_ACTIVITY_URL werden im Mega-Compose
+# fest verdrahtet. Dadurch können alte Standalone-Werte die Container-
+# Kommunikation nicht versehentlich beschädigen.
+###############################################################################
+
+###############################################################################
+# 01. MEGA STACK - RELEASE / HOST PORTS / PFADE
+###############################################################################
+# Immutable Registry-Tag der sechs Projekt-Images. "latest" ist produktiv verboten.
+IMAGE_TAG=1.6.2
+
+CONTROL_HOST_PORT=8070
+AGENT_HOST_PORT=8080
+KNOWLEDGE_HOST_PORT=8081
+NEUROFORGE_HOST_PORT=8090
+OLLAMA_HOST_PORT=11434
+SEARXNG_HOST_PORT=8888
+
+# Gemeinsame produktive Knowledge-Quelle auf dem Docker-Host.
+KB_DATA_PATH=./knowledge
+KB_DATA_MOUNT_MODE=rw
+KB_BACKUP_PATH=./backups
+KB_STAGING_PATH=./staging
+
+###############################################################################
+# 02. MEGA STACK - SECURITY / SERVICE TOKENS
+###############################################################################
+# Erzeugen z. B. mit: ./scripts/generate-secrets.sh
+NEUROFORGE_ADMIN_TOKEN=CHANGE_ME_MASTER_ADMIN_TOKEN
+NEUROFORGE_APP_API_KEY=CHANGE_ME_MASTER_APP_API_KEY
+# Nur Agent/Knowledge Integrations- und Outcome-Schreibpfade.
+NEUROFORGE_INTEGRATION_TOKEN=CHANGE_ME_MASTER_INTEGRATION_TOKEN
+# Nur NeuroForge Status/Graph-Lesezugriffe des Control Centers.
+NEUROFORGE_CONTROL_READ_TOKEN=CHANGE_ME_MASTER_CONTROL_READ_TOKEN
+NEUROFORGE_WORKER_TOKEN=CHANGE_ME_SHARED_WORKER_TOKEN
+NEUROFORGE_METRICS_TOKEN=CHANGE_ME_MASTER_METRICS_TOKEN
+# Nur bei Clusterbetrieb erforderlich.
+NEUROFORGE_CLUSTER_TOKEN=
+KB_INTEGRATION_TOKEN=CHANGE_ME_KB_INTEGRATION_TOKEN
+CONTROL_READ_TOKEN=CHANGE_ME_AGENT_CONTROL_READ_TOKEN
+# Eigene Anmeldung des Control Centers; /healthz bleibt absichtlich öffentlich.
+CONTROL_BASIC_AUTH_USER=admin
+CONTROL_BASIC_AUTH_PASSWORD=CHANGE_ME_CONTROL_PASSWORD
+# Optionaler externer OpenAI-Fallback. Leer = deaktiviert.
+OPENAI_API_KEY=
+
+###############################################################################
+# 03. SHARED OLLAMA RUNTIME
+###############################################################################
+# Dieses Modell wird von Agent, Knowledge und NeuroForge gemeinsam verwendet.
+# Wenn du dein bisheriges Verhalten beibehalten willst, kannst du hier z. B.
+# qwen3:8b statt gemma3 setzen. Das Modell muss vorher in Ollama vorhanden sein.
+OLLAMA_MODEL=gemma4
+OLLAMA_EMBEDDING_MODEL=embeddinggemma
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
+# Optional NeuroForge-only override; takes precedence over OLLAMA_API_KEY.
+NEUROFORGE_OLLAMA_API_KEY=
+OLLAMA_TIMEOUT=10m
+OLLAMA_MAX_CONCURRENT=1
+OLLAMA_NUM_PREDICT=768
+OLLAMA_JSON_RETRIES=1
+OLLAMA_KEEP_ALIVE=10m
+OLLAMA_THINK=false
+# NeuroForge verwendet für lange, quellengebundene KB-Synthesen ein eigenes
+# Call-Budget. num_predict=0 ist hier absichtlich: dadurch gilt das jeweilige
+# Staging-/Verification-Limit statt eines globalen niedrigen Ollama-Limits.
+NEUROFORGE_OLLAMA_NUM_CTX=8192
+NEUROFORGE_OLLAMA_NUM_PREDICT=0
+
+###############################################################################
+# 04. KNOWLEDGE EDITOR
+###############################################################################
+KB_APP_MODE=editor
+APP_TITLE=Knowledge Base Editor
+APP_SUBTITLE=GLPI NeuroForge Mega
+AUTO_RELOAD_INTERVAL=60s
+
+# Für Produktion setzen. Leer würde die optionale Basic-Auth deaktivieren.
+BASIC_AUTH_USER=admin
+BASIC_AUTH_PASSWORD=CHANGE_ME_KNOWLEDGE_PASSWORD
+
+AI_FALLBACK_ENABLED=true
+# Optional externer Ollama-Endpunkt. Leer/fehlend = interner Compose-Service http://ollama:11434.
+OLLAMA_BASE_URL=http://192.0.2.22:11434
+OLLAMA_STAGING_AUTO_REPLY=false
+OLLAMA_STAGING_MIN_SCORE=0.78
+
+###############################################################################
+# 05. NEUROFORGE VECTOR BACKEND / INTEGRATION
+###############################################################################
+# local | dual | neuroforge
+KNOWLEDGE_VECTOR_BACKEND=dual
+NEUROFORGE_NAMESPACE=glpi-agent
+NEUROFORGE_TIMEOUT=15s
+NEUROFORGE_SEARCH_K=128
+# true = bei NeuroForge-Ausfall lokale/lexikalische Evidenz weiterverwenden.
+NEUROFORGE_FAIL_OPEN=true
+
+###############################################################################
+# 06. CONTROLLED LEARNING / VALIDATED OUTCOMES
+###############################################################################
+# Verhindert automatisches Langzeitlernen aus rohem Chatinput/AI-Ausgaben.
+NEUROFORGE_CONTROLLED_LEARNING=true
+# Separate gate for semantic goal-cycle memories. Required for manual/autonomous
+# Goal cycles to learn while Controlled Learning remains enabled.
+NEUROFORGE_GOAL_LEARNING_ENABLED=false
+# Mega-Readiness prüft Ollama /api/tags sowie Chat- und Embedding-Modell live.
+NEUROFORGE_READINESS_OLLAMA_LIVE=true
+
+# Ticket -> KI-Vorschlag -> Techniker bestätigt/korrigiert -> Trusted Outcome.
+OUTCOME_LEARNING_ENABLED=true
+OUTCOME_LEARNING_FAIL_OPEN=false
+OUTCOME_LEARNING_MAX_OUTCOMES=2000
+
+# Menschlich validierte Erfahrungen als sekundäre Evidenz abrufen.
+OUTCOME_RETRIEVAL_ENABLED=true
+OUTCOME_RETRIEVAL_SEARCH_K=6
+OUTCOME_RETRIEVAL_MIN_SIMILARITY=0.58
+OUTCOME_RETRIEVAL_FAIL_OPEN=true
+
+###############################################################################
+# 07. OPTIONAL RESEARCH / SEARXNG / AUTONOMY
+###############################################################################
+# SearXNG-Container wird nur mit `docker compose --profile research ...` gestartet.
+SEARXNG_IMAGE=docker.io/searxng/searxng:latest
+SEARXNG_SECRET=CHANGE_ME_SEARXNG_SECRET
+
+# Research und Autonomy sind absichtlich getrennt.
+NEUROFORGE_RESEARCH_ENABLED=false
+NEUROFORGE_SEARXNG_ENABLED=false
+NEUROFORGE_SEARXNG_URL=http://searxng:8080
+NEUROFORGE_RESEARCH_GOAL_ENABLED=true
+NEUROFORGE_AUTONOMY_ENABLED=false
+NEUROFORGE_AUTONOMY_INTERVAL_MINUTES=30
+NEUROFORGE_RESEARCH_MAX_QUERIES=2
+NEUROFORGE_RESEARCH_MAX_PAGES=4
+
+# Research -> Knowledge human-review staging bridge. Never writes production KB.
+NEUROFORGE_KB_STAGING_ENABLED=true
+NEUROFORGE_KB_STAGING_MIN_EVIDENCE=4
+NEUROFORGE_KB_STAGING_MIN_SOURCES=2
+# 0 allows a human-review draft from multiple independent sources before exact
+# semantic corroboration exists. Raise to 1+ for stricter environments.
+NEUROFORGE_KB_STAGING_MIN_CORROBORATIONS=0
+NEUROFORGE_KB_STAGING_MAX_EVIDENCE=12
+# llm = echte, quellengebundene Artikelsynthese (Production default).
+# evidence = nur diagnostisches Roh-Evidence-Bundle, nicht als Artikel geeignet.
+NEUROFORGE_KB_STAGING_SYNTHESIS_MODE=llm
+# Produktions-Gate: mindestens eine belastbare Erst-/Herstellerquelle muss im
+# tatsächlich an die Synthese übergebenen Evidence-Set enthalten sein.
+NEUROFORGE_KB_STAGING_REQUIRE_AUTHORITATIVE_SOURCE=true
+NEUROFORGE_KB_STAGING_MIN_AUTHORITATIVE_SOURCES=1
+# Kommagetrennte zusätzliche First-Party-Domains. Built-ins umfassen u.a.
+# Microsoft, Fortinet, NVIDIA, Cisco, Broadcom/VMware, Red Hat, Ubuntu, Apple,
+# Google und Mozilla. Leer = nur Built-ins.
+NEUROFORGE_KB_STAGING_AUTHORITATIVE_DOMAINS=
+# Zweite LLM-Stufe prüft jede materielle Draft-Aussage gegen konkrete E*-Belege.
+NEUROFORGE_KB_STAGING_VERIFY_CLAIMS=true
+NEUROFORGE_KB_STAGING_MIN_CLAIM_COVERAGE=1.0
+# Handlungsanweisungen/Commands müssen durch eine autoritative Quelle belegt sein.
+NEUROFORGE_KB_STAGING_REQUIRE_AUTHORITATIVE_ACTIONS=true
+NEUROFORGE_KB_STAGING_MAX_VERIFICATION_STATEMENTS=32
+# Einmaliger Grounding-Repair ist erlaubt; anschließend wird vollständig neu geprüft.
+NEUROFORGE_KB_STAGING_VERIFICATION_REPAIR=true
+# Article-Depth-Gate: `text` ist der vollständige KB-Artikel; `answer` bleibt eine
+# kompakte operative Zusammenfassung für den Agenten. Die Evidence wird für den
+# Prompt fair über alle ausgewählten Belege verteilt und auf ein Context-Budget
+# begrenzt, damit bei typischen 8k-Modellkontexten genügend Output-Budget bleibt.
+NEUROFORGE_KB_STAGING_SYNTHESIS_MAX_TOKENS=2600
+NEUROFORGE_KB_STAGING_EVIDENCE_PROMPT_MAX_CHARS=14000
+NEUROFORGE_KB_STAGING_MIN_ARTICLE_CHARS=3500
+NEUROFORGE_KB_STAGING_TARGET_ARTICLE_CHARS=6500
+NEUROFORGE_KB_STAGING_MAX_ARTICLE_CHARS=10000
+NEUROFORGE_KB_STAGING_MIN_ANSWER_CHARS=160
+NEUROFORGE_KB_STAGING_MAX_ANSWER_CHARS=1200
+
+###############################################################################
+# 07B. MASTER / SUBAGENTS / DURABLE ORCHESTRATOR / KNOWLEDGE GRAPH
+###############################################################################
+# Der NeuroForge-Master hält den autoritativen Zustand. Subagents ziehen nur
+# capability-passende Jobs und arbeiten unter lease/heartbeat fencing.
+NEUROFORGE_WORKER_LEASE_SECONDS=120
+NEUROFORGE_WORKER_HEARTBEAT_SECONDS=15
+NEUROFORGE_WORKER_STALE_AFTER_SECONDS=60
+NEUROFORGE_WORKER_DEFAULT_MAX_ATTEMPTS=3
+NEUROFORGE_WORKER_RETRY_BACKOFF_SECONDS=15
+NEUROFORGE_WORKER_MAX_QUEUED_JOBS=5000
+NEUROFORGE_WORKER_MAX_QUEUED_PAYLOAD_MB=128
+# Worker-Ergebnisse, die Master-State verändern (z.B. vector.relink), werden
+# zweiphasig persistiert und bei Master-Neustart idempotent weiter angewendet.
+NEUROFORGE_WORKER_MASTER_APPLY_MAX_ATTEMPTS=5
+NEUROFORGE_WORKER_MASTER_APPLY_BACKOFF_SECONDS=5
+NEUROFORGE_WORKER_JOB_RETENTION_HOURS=24
+NEUROFORGE_WORKER_MAX_TERMINAL_JOBS=2000
+# Lokale Compose-Subagents. Zusätzliche Remote-Subagents können mit
+# docker-compose.subagent.yml auf anderen Hosts gestartet werden.
+NEUROFORGE_CPU_WORKER_CONCURRENCY=2
+NEUROFORGE_GPU_WORKER_CONCURRENCY=1
+NEUROFORGE_WORKER_HEARTBEAT_INTERVAL=15s
+# Bestehende/importierte knowledge.chunk-Memories werden kontrolliert zu einem
+# n:m-Graphen verlinkt. Kein O(N²)-Queue-Burst: ANN-Kandidaten + bounded queue.
+NEUROFORGE_GRAPH_BACKFILL_ENABLED=true
+NEUROFORGE_GRAPH_BACKFILL_INTERVAL_SECONDS=10
+NEUROFORGE_GRAPH_BACKFILL_BATCH_SIZE=16
+NEUROFORGE_GRAPH_BACKFILL_MAX_QUEUED=64
+NEUROFORGE_GRAPH_BACKFILL_MIN_DEGREE=3
+NEUROFORGE_GRAPH_CANDIDATE_MULTIPLIER=6
+NEUROFORGE_GRAPH_RETRY_AFTER_MINUTES=360
+NEUROFORGE_GRAPH_REQUIRE_WORKER=true
+# Retrieval kann echte mehrstufige Assoziationsketten verfolgen.
+NEUROFORGE_GRAPH_MAX_HOPS=3
+NEUROFORGE_GRAPH_HOP_DECAY=0.60
+NEUROFORGE_GRAPH_MAX_EXPANSION=64
+NEUROFORGE_GRAPH_MIN_EDGE_WEIGHT=0.05
+# LLM/Embedding-Aufgaben werden bevorzugt an GPU-Subagents delegiert. Bei nicht
+# verfügbarem GPU-Subagent fällt der Master auf seinen Provider-Router zurück.
+NEUROFORGE_OFFLOAD_CHAT=true
+NEUROFORGE_OFFLOAD_EMBEDDINGS=true
+NEUROFORGE_DISTRIBUTED_INFERENCE_WAIT_SECONDS=180
+
+###############################################################################
+# 08. OPTIONAL CODEBASE MEMORY MCP / ENGINEERING UI
+###############################################################################
+# Developer-only. Für den Produktivbetrieb nicht erforderlich.
+CODEBASE_MEMORY_URL=
+PUBLIC_CODEBASE_MEMORY_URL=http://localhost:9749
+
+###############################################################################
+# 09. AGENT - VOLLSTÄNDIGE KONFIGURATION
+###############################################################################
+# 09A. GLPI AI AGENT - ALLGEMEINER BETRIEB
+###############################################################################
+# true:
+# Der Agent analysiert vollständig, schreibt aber keine Änderungen nach GLPI.
+#
+# false:
+# Durch die Policy freigegebene Aktionen werden tatsächlich ausgeführt.
+#
+# Für Tests / Einführung:
+# true
+DRY_RUN=true
+# Mögliche Werte:
+# debug
+# info
+# warn
+# error
+LOG_LEVEL=info
+# HTTP-Listener INNERHALB des Agent-Containers.
+# Im Mega-Compose wird dieser Wert zusätzlich fest auf :8080 überschrieben.
+# Der veröffentlichte Host-Port wird ausschließlich über AGENT_HOST_PORT gesteuert.
+HTTP_ADDR=:8080
+# Persistentes Verzeichnis IM Container.
+#
+# Compose mountet:
+# agent-data:/app/data
+#
+# Enthält unter anderem:
+# - Knowledge-Index
+# - Audit/Run-Daten
+# - Category Learning
+# - Managed Knowledge
+# - GLPI-KB-Cache
+DATA_DIR=/app/data
+###############################################################################
+# 06. AGENT WEBUI / API / DIAGNOSE
+###############################################################################
+# Benutzer für Agent-Dashboard, Knowledge-Verwaltung und Diagnose-Cockpit.
+WEB_USERNAME=admin
+WEB_PASSWORD=CHANGE_ME_AGENT_WEB_PASSWORD
+# false:
+# Anmeldung erforderlich.
+#
+# true:
+# Weboberfläche ohne Authentifizierung erreichbar.
+#
+# In Produktion normalerweise false.
+WEB_ALLOW_ANONYMOUS=false
+# TrustedNet-Kennzeichnung vor automatisch ausgewählten Antworten.
+#
+# true:
+# TrustedNet-KI-Badge wird vor Anrede und Antwort eingefügt.
+#
+# false:
+# keine KI-Kennzeichnung.
+AI_CONTENT_LABEL_ENABLED=true
+###############################################################################
+# 07. OPTIONALER GLPI-WEBHOOK
+###############################################################################
+# Optionales Shared Secret für eingehende GLPI-Webhooks.
+#
+# Der Absender muss dasselbe Secret z. B. über:
+# X-Webhook-Secret
+# übertragen.
+#
+# Leer lassen, falls kein Webhook verwendet wird.
+WEBHOOK_SECRET=
+###############################################################################
+# 08. GLPI 11 / HIGH-LEVEL API / OAUTH2
+###############################################################################
+GLPI_URL=https://YOUR-GLPI-HOST
+# Verwendete GLPI High-Level API.
+GLPI_API_VERSION=v2.3
+# OAuth2 Service Account.
+GLPI_CLIENT_ID=CHANGE_ME_GLPI_CLIENT_ID
+GLPI_CLIENT_SECRET=CHANGE_ME_GLPI_CLIENT_SECRET
+GLPI_USERNAME=CHANGE_ME_GLPI_USERNAME
+GLPI_PASSWORD=CHANGE_ME_GLPI_PASSWORD
+# Numerische GLPI-Benutzer-ID des Service-Accounts.
+#
+# Wird unter anderem benötigt, um Agent-Followups von menschlichen
+# Followups unterscheiden zu können.
+GLPI_AGENT_USER_ID=0
+# Nur für lokale Testsysteme ohne TLS.
+#
+# Produktion:
+# false
+GLPI_ALLOW_INSECURE_HTTP=false
+###############################################################################
+# 09. GLPI TICKET-POLLING
+###############################################################################
+# Fail-closed Whitelist erlaubter GLPI-Ticketstatus.
+#
+# Beispiel:
+# 1
+# 1,2
+#
+# Status 1 entspricht typischerweise "Neu".
+GLPI_ALLOWED_STATUS_IDS=1
+# Polling-Intervall.
+GLPI_POLL_INTERVAL=30s
+# Maximale Anzahl Tickets pro Poll.
+GLPI_POLL_LIMIT=50
+# Optionale serverseitige Vorfilterung.
+#
+# Die Agent-Policy prüft GLPI_ALLOWED_STATUS_IDS anschließend trotzdem selbst.
+#
+# Änderungen der Syntax immer gegen /api.php/doc der eigenen GLPI-Instanz
+# prüfen.
+GLPI_TICKET_FILTER=status.id==1
+# HTTP-Timeout für GLPI-Aufrufe.
+GLPI_TIMEOUT=20s
+###############################################################################
+# 10. GLPI AI AGENT - OLLAMA-POOL
+###############################################################################
+# Einzelnode-Kompatibilität. Wird nur verwendet, wenn OLLAMA_URLS leer ist.
+OLLAMA_URL=http://192.0.2.22:11434
+
+# Mehrere Ollama-Instanzen, durch Komma getrennt. Alle Nodes sollten dieselbe
+# Ollama-Version, dasselbe Chat-Modell und dasselbe Embedding-Modell besitzen.
+# Beispiel für vorhandene Lenovo-Nodes:
+# OLLAMA_URLS=http://10.20.30.21:11434,http://10.20.30.22:11434,http://10.20.30.23:11434
+OLLAMA_URLS=http://192.0.2.22:11434
+
+# Optionale lesbare Namen; Anzahl muss exakt zu OLLAMA_URLS passen.
+# OLLAMA_NODE_NAMES=lenovo-01,lenovo-02,lenovo-03
+OLLAMA_NODE_NAMES=gpu-node-01
+
+# Optionale Gewichte 1..100; nur für OLLAMA_ROUTING_MODE=weighted relevant.
+# OLLAMA_NODE_WEIGHTS=1,1,1
+OLLAMA_NODE_WEIGHTS=1
+
+# Routing-Modi:
+# least_inflight = Node mit den wenigsten laufenden Requests (empfohlen)
+# round_robin = zyklische Verteilung
+# weighted = Verteilung anhand OLLAMA_NODE_WEIGHTS und Auslastung
+# fastest_recent = bevorzugt die zuletzt schnellsten Nodes
+OLLAMA_ROUTING_MODE=least_inflight
+
+# Maximale parallele Requests JE Node. Für integrierte GPUs/RAM-Sharing 1.
+OLLAMA_NODE_MAX_INFLIGHT=1
+
+# Regelmäßige Prüfung von /api/tags.
+OLLAMA_NODE_HEALTH_INTERVAL=15s
+
+# Nach einem retryfähigen Netzwerk-/HTTP-Fehler wird der Node so lange nicht
+# für neue Requests verwendet.
+OLLAMA_NODE_FAILURE_COOLDOWN=30s
+
+# Maximalzeit für einen einzelnen Request an genau einen Node. Der übergeordnete
+# Analyse-Timeout kann kürzer sein und hat dann Vorrang.
+OLLAMA_NODE_REQUEST_TIMEOUT=10m
+
+# Bei Netzwerkfehlern, HTTP 408/429/5xx oder ungültigem Response-JSON auf einen
+# anderen kompatiblen Node wechseln.
+OLLAMA_FAILOVER_ENABLED=true
+
+# Maximale Anzahl verschiedener Nodes je logischem Request. 0 bedeutet:
+# automatisch alle konfigurierten Nodes. Ein positiver Wert darf höchstens der
+# Zahl der OLLAMA_URLS-Einträge entsprechen.
+OLLAMA_FAILOVER_ATTEMPTS=0
+
+# Bei abweichenden Chat-/Embedding-Modelldigests wird der Pool vollständig
+# fail-closed. Für reproduzierbare Entscheidungen unbedingt true lassen.
+OLLAMA_REQUIRE_SAME_MODEL_DIGEST=true
+
+# true: Jeder Node muss auch OLLAMA_EMBEDDING_MODEL installiert haben.
+# Bei RAG empfohlen. false erlaubt Chat-only-Nodes; Embedding-Requests werden
+# trotzdem nur an Nodes mit erkanntem Embedding-Modell gesendet.
+OLLAMA_REQUIRE_EMBEDDING_MODEL=true
+
+# OLLAMA_MODEL ist bereits oben im gemeinsamen Compose-/Ollama-Bereich gesetzt:
+# OLLAMA_MODEL=qwen3:8b
+# Embedding-Modell für RAG.
+# OLLAMA_EMBEDDING_MODEL=embeddinggemma # already configured in SHARED OLLAMA RUNTIME above
+
+# Modellspezifisches Retrieval-Prompting.
+# auto = Modell automatisch erkennen; für embeddinggemma empfohlen.
+# plain = keine modellspezifischen Retrieval-Prompts.
+KNOWLEDGE_EMBEDDING_PROFILE=auto
+
+# Gesamtbudget für Ollama-Aufrufe und Fallback für Node-Request-Timeouts.
+# OLLAMA_TIMEOUT ist bereits oben gesetzt.
+# OLLAMA_MAX_CONCURRENT bleibt als Legacy-Alias für
+# OLLAMA_NODE_MAX_INFLIGHT erhalten, falls der neue Wert nicht gesetzt ist.
+
+# Maximale Anzahl generierter Tokens für strukturierte Antworten.
+# OLLAMA_NUM_PREDICT=768 # already configured in SHARED OLLAMA RUNTIME above
+# Wiederholungen bei semantisch/strukturell fehlerhaftem Modell-JSON.
+# Diese Wiederholungen sind von Netzwerk-Failover getrennt.
+# OLLAMA_JSON_RETRIES=1 # already configured in SHARED OLLAMA RUNTIME above
+# Ollama-Modell nach Benutzung im Speicher halten.
+# OLLAMA_KEEP_ALIVE=10m # already configured in SHARED OLLAMA RUNTIME above
+# Thinking bei unterstützten Modellen deaktivieren.
+# OLLAMA_THINK=false # already configured in SHARED OLLAMA RUNTIME above
+###############################################################################
+# 11. KNOWLEDGE BASE / RAG - BASIS
+###############################################################################
+# Knowledge-Verzeichnis IM Agent-Container.
+#
+# Compose sollte hierhin KB_DATA_PATH mounten:
+# ${KB_DATA_PATH:-./knowledge}:/app/knowledge:ro
+KNOWLEDGE_DIR=/app/knowledge
+# Gesamtes Retrieval-System aktivieren.
+RAG_ENABLED=true
+###############################################################################
+# 12. EXTERNE KNOWLEDGE-KATEGORIEN
+###############################################################################
+# Verhalten bei String-/Fremdkategorien, z. B.:
+#
+# "AI-Staging"
+# "Outlook"
+# "E-Mail"
+# "Signatur"
+#
+# Mögliche Werte:
+#
+# unscoped
+# Artikel bleibt nutzbar.
+# Fremdkategorien können als Retrieval-Metadaten dienen.
+#
+# skip
+# Artikel mit unbekannten Kategorien überspringen.
+#
+# strict
+# unbekannte Kategorie als Fehler behandeln.
+#
+# Für eine gemeinsam mit anderen Anwendungen verwendete KB:
+# unscoped
+KNOWLEDGE_CATEGORY_MODE=unscoped
+# Optionales Mapping von Fremdkategorien auf GLPI-ITIL-Kategorie-IDs.
+#
+# Beispiel knowledge-category-map.json:
+#
+# {
+# "Outlook": 12,
+# "E-Mail": 12,
+# "Active Directory": 2,
+# "Security": [20,21]
+# }
+KNOWLEDGE_CATEGORY_MAP_FILE=/app/data/knowledge-category-map.json
+# Optional bestimmte KB-Dateien ignorieren.
+#
+# Beispiele:
+# KB-SEC-ATTCK-*.json
+# legacy-*.json,external-only-*.json
+#
+# Leer:
+# keine zusätzlichen Ignore-Regeln.
+KNOWLEDGE_IGNORE_GLOBS=
+###############################################################################
+# 13. PERSISTENTER KNOWLEDGE-INDEX
+###############################################################################
+# Mögliche Werte:
+#
+# incremental
+# Persistent gespeicherten Index sofort verwenden.
+# Neue/geänderte Dateien anschließend inkrementell nachziehen.
+# Für Produktion empfohlen.
+#
+# rebuild
+# vollständigen Index neu erzeugen.
+#
+# readonly
+# nur bestehenden Index verwenden, keine Änderungen übernehmen.
+KNOWLEDGE_INDEX_MODE=incremental
+# Anzahl Texte pro Embedding-Batch.
+KNOWLEDGE_EMBED_BATCH_SIZE=64
+# Intervall für neue/geänderte/gelöschte Dateien.
+#
+# Beispiele:
+# 30s
+# 1m
+# 5m
+#
+# 0:
+# keinen automatischen Hintergrundscan durchführen.
+KNOWLEDGE_INDEX_SCAN_INTERVAL=5m
+###############################################################################
+# 14. RETRIEVAL / DYNAMISCHE KANDIDATENAUSWAHL
+###############################################################################
+# Unterhalb dieses Retrieval-Scores wird eine KB nicht als geeigneter
+# Kandidat betrachtet.
+#
+# Der Wert ist KEINE Wahrscheinlichkeit.
+KNOWLEDGE_RETRIEVAL_FLOOR=0.30
+# Maximale Differenz zum besten Treffer.
+#
+# Beispiel:
+#
+# bester Treffer 0.82
+# MAX_GAP 0.20
+# dynamischer Cutoff 0.62
+#
+# Ein Kandidat mit 0.55 würde dann nicht an die KI gesendet.
+KNOWLEDGE_CANDIDATE_MAX_GAP=0.20
+# Maximale Anzahl Knowledge-Kandidaten, die tatsächlich an Ollama gehen.
+KNOWLEDGE_TOP_K=6
+# Anzahl Kandidaten für Audit / Diagnose.
+#
+# Kann größer als KNOWLEDGE_TOP_K sein.
+KNOWLEDGE_AUDIT_TOP_K=10
+###############################################################################
+# 15. HYBRID-RETRIEVAL - RANKING-GEWICHTE
+###############################################################################
+# Die Werte beschreiben die Gewichtung beim KB-Ranking.
+#
+# Summe aktuell:
+# 1.0
+#
+# Fehlende Metadaten sollen nicht automatisch negativ bewertet werden.
+# Embedding-/Chunk-Semantik.
+KNOWLEDGE_WEIGHT_SEMANTIC=0.45
+# Ticket-Betreff gegenüber KB-Titel.
+KNOWLEDGE_WEIGHT_TITLE=0.20
+# Lexikalische / sprachliche Übereinstimmung.
+KNOWLEDGE_WEIGHT_LEXICAL=0.20
+# KB-Keywords.
+KNOWLEDGE_WEIGHT_KEYWORDS=0.075
+# Kategorie-/Lernsignal.
+KNOWLEDGE_WEIGHT_CATEGORY=0.075
+###############################################################################
+# 16. FINALE EVIDENZ FÜR AUTO-REPLY
+###############################################################################
+# Mindestwert der FINALEN Evidenz.
+#
+# WICHTIG:
+# Das ist nicht der reine Retrieval-Score.
+#
+# Die finale Evidenz kombiniert:
+# - Retrieval
+# - AI Confidence
+# - Kategorieübereinstimmung
+KNOWLEDGE_MIN_SCORE=0.70
+# Gewicht Retrieval.
+KNOWLEDGE_EVIDENCE_WEIGHT_RETRIEVAL=0.45
+# Gewicht KI-Auswahl / KI-Confidence.
+KNOWLEDGE_EVIDENCE_WEIGHT_AI=0.35
+# Gewicht Kategorieübereinstimmung.
+KNOWLEDGE_EVIDENCE_WEIGHT_CATEGORY=0.20
+###############################################################################
+# 17. KNOWLEDGE-CHUNKING
+###############################################################################
+# Ungefähre Anzahl Wörter pro Dokument-Chunk.
+KNOWLEDGE_CHUNK_WORDS=160
+# Überlappung benachbarter Chunks.
+KNOWLEDGE_CHUNK_OVERLAP_WORDS=30
+# Maximale Anzahl Chunks pro KB-Dokument.
+KNOWLEDGE_MAX_CHUNKS_PER_DOC=24
+# Maximale Anzahl Query-Chunks bei sehr langen Tickets.
+KNOWLEDGE_MAX_QUERY_CHUNKS=64
+# Maximale Anzahl Kategorien im Kategorie-Prompt.
+CATEGORY_PROMPT_LIMIT=80
+###############################################################################
+# 18. KNOWLEDGE-QUELLEN / TRUST POLICY
+###############################################################################
+# Quellen für normale Knowledge-Suche und mögliche Antwortkandidaten.
+# Indexiert wird die Vereinigung mit KNOWLEDGE_CATEGORY_SOURCES.
+#
+# Beispiele:
+# internal-kb
+# glpi-kb
+# runbook
+# vendor-docs
+KNOWLEDGE_ALLOWED_SOURCES=internal-kb,glpi-kb,vendor-docs,vendor-docs-ms,vendor-docs-linux,vendor-docs-sec
+# Quellen, die ausschließlich die Kategorieentscheidung unterstützen.
+# Ohne explizite Angabe wird aus Kompatibilitätsgründen KNOWLEDGE_ALLOWED_SOURCES verwendet.
+# Mit "none" wird Knowledge-Einfluss auf die Kategorisierung deaktiviert.
+KNOWLEDGE_CATEGORY_SOURCES=internal-category
+# Nur diese Quellen dürfen grundsätzlich automatische Antworten liefern.
+#
+# Muss eine Teilmenge von KNOWLEDGE_ALLOWED_SOURCES sein.
+#
+# Beispiel zum kompletten Abschalten:
+# KNOWLEDGE_AUTO_REPLY_SOURCES=none
+KNOWLEDGE_AUTO_REPLY_SOURCES=internal-kb,glpi-kb,vendor-docs,vendor-docs-ms,vendor-docs-linux,vendor-docs-sec
+# Webbasierte Bearbeitung von Agent-eigenen Knowledge-Artikeln.
+#
+# Diese werden unter:
+# DATA_DIR/knowledge-managed
+# gespeichert.
+#
+# Das statische KNOWLEDGE_DIR bleibt read-only.
+KNOWLEDGE_WEB_EDIT_ENABLED=false
+# Nur Mega-Compose: expliziter Legacy-Schalter für den Agent-eigenen Editor.
+AGENT_LEGACY_KNOWLEDGE_EDITOR_ENABLED=false
+###############################################################################
+# 19. GLPI KNOWLEDGE BASE CONNECTOR
+###############################################################################
+# GLPI-interne Knowledge Base synchronisieren.
+GLPI_KB_ENABLED=true
+# auto:
+# Agent ermittelt die KnowbaseItem-Route aus /api.php/doc.json.
+GLPI_KB_PATH=auto
+# Optionaler serverseitiger GLPI-Filter.
+#
+# Leer:
+# alle für den Service Account sichtbaren Artikel, begrenzt durch LIMIT.
+GLPI_KB_FILTER=
+# Maximale Anzahl GLPI-KB-Artikel.
+GLPI_KB_LIMIT=500
+# Synchronisationsintervall.
+GLPI_KB_SYNC_INTERVAL=10m
+# source-Wert importierter GLPI-KB-Artikel.
+GLPI_KB_SOURCE=glpi-kb
+# true:
+# GLPI-KB-Artikel können grundsätzlich Auto-Replies auslösen.
+#
+# Zusätzlich gelten weiterhin alle anderen Policy-Gates wie Retrieval,
+# KI-Auswahl, Evidenz, Sprache, Stil und vorhandene Antworten.
+GLPI_KB_AUTO_REPLY=false
+# Whitelist der GLPI KNOWLEDGE-BASE-Kategorie-IDs.
+#
+# WICHTIG:
+# Dies sind NICHT die ITIL-/Ticketkategorie-IDs. Ein kategorisierter Artikel
+# ist genau dann grundsätzlich für Auto-Reply freigegeben, wenn mindestens
+# eine seiner GLPI-KB-Kategorien hier enthalten ist.
+#
+# Mehrere Werte:
+# 1,2,7
+GLPI_KB_AUTO_REPLY_CATEGORY_IDS=1
+# VERALTET / WIRD IGNORIERT:
+# Ticket-/ITIL-Kategorien geben einen GLPI-Wissensartikel nicht mehr für
+# Auto-Reply frei. Die Variable bleibt nur erhalten, damit alte .env-Dateien
+# verständlich migriert werden können. Wert bitte leeren oder entfernen.
+GLPI_KB_AUTO_REPLY_ITIL_CATEGORY_IDS=
+# GLPI-KB-Artikel ohne Knowledge-Base-Kategorie bleiben standardmäßig gesperrt.
+#
+# true:
+# Solche Artikel dürfen ausschließlich dann Auto-Reply verwenden, wenn ihre
+# konkrete GLPI-KnowbaseItem-ID zusätzlich in
+# GLPI_KB_AUTO_REPLY_UNCATEGORIZED_ARTICLE_IDS steht.
+GLPI_KB_AUTO_REPLY_ALLOW_UNCATEGORIZED=false
+# Explizite GLPI-KnowbaseItem-IDs für unkategorisierte Artikel.
+# Beispiel: Das synchronisierte Dokument GLPI-KB-1 entspricht Artikel-ID 1.
+# Diese Liste ist bei ALLOW_UNCATEGORIZED=true verpflichtend.
+GLPI_KB_AUTO_REPLY_UNCATEGORIZED_ARTICLE_IDS=
+###############################################################################
+# 20. HUMAN-IN-THE-LOOP / KATEGORIE-LERNEN
+###############################################################################
+# Menschlich bestätigte/korrigierte Entscheidungen als Lernbeispiele verwenden.
+#
+# Der Agent lernt NICHT automatisch aus seinen eigenen unbestätigten
+# Entscheidungen.
+LEARNING_ENABLED=true
+# Maximale Anzahl gespeicherter Beispiele.
+LEARNING_MAX_EXAMPLES=500
+# Maximale Beispiele pro Kategorie im Prompt.
+LEARNING_EXAMPLES_PER_CATEGORY=5
+###############################################################################
+# 21. KOMMUNIKATIONSPOLICY
+###############################################################################
+# Erwartete Sprache von Auto-Reply-KBs.
+COMMUNICATION_LANGUAGE=de-DE
+# Erwarteter Kommunikationsstil.
+COMMUNICATION_STYLE=formal
+# Wird vor die Knowledge-Antwort gesetzt.
+COMMUNICATION_SALUTATION=Guten Tag,
+# Abschluss.
+COMMUNICATION_CLOSING=Mit freundlichen Grüßen
+COMMUNICATION_SIGNATURE=IT-Service
+###############################################################################
+# 22. OPERATIONAL CONTEXT - GLOBAL
+###############################################################################
+# Globaler Schalter für zusätzliche Betriebsinformationen:
+# - Changes
+# - Major Incidents
+# - Requester-Geräte
+# - Uptime Kuma
+CONTEXT_ENABLED=true
+# Timeout für Kontextabfragen.
+CONTEXT_TIMEOUT=12s
+# Mindestscore, ab dem Incident/Outage als für das Ticket relevant gilt.
+CONTEXT_RELEVANCE_MIN_SCORE=0.20
+# true:
+# Fehler einer aktivierten Kontextquelle blockieren Auto-Reply.
+#
+# Fail-closed und für Produktion empfohlen.
+CONTEXT_BLOCK_AUTO_REPLY_ON_ERRORS=true
+# true:
+# relevante zentrale Störung blockiert individuelle Standardantwort.
+CONTEXT_BLOCK_AUTO_REPLY_ON_INCIDENT=true
+###############################################################################
+# 23. GLPI CHANGE CALENDAR
+###############################################################################
+CHANGE_CALENDAR_ENABLED=true
+# API-Route.
+GLPI_CHANGE_PATH=/Assistance/Change
+# Optionaler serverseitiger GLPI-Filter.
+GLPI_CHANGE_FILTER=
+# Maximale Anzahl geladener Changes.
+GLPI_CHANGE_LIMIT=100
+# Betrachteter Zeitraum in der Vergangenheit.
+CHANGE_LOOKBACK=72h
+# Betrachteter Zeitraum in der Zukunft.
+CHANGE_LOOKAHEAD=24h
+###############################################################################
+# 24. MAJOR INCIDENTS
+###############################################################################
+# Major Incidents über GLPI-Tickets ermitteln.
+#
+# Erst aktivieren, wenn GLPI_MAJOR_INCIDENT_FILTER getestet wurde.
+MAJOR_INCIDENTS_ENABLED=false
+# Expliziter Filter für Tickets, die als Major Incident gelten.
+GLPI_MAJOR_INCIDENT_FILTER=
+GLPI_MAJOR_INCIDENT_LIMIT=20
+###############################################################################
+# 25. REQUESTER -> GERÄT / ASSET CONTEXT
+###############################################################################
+# Zusätzlich zu direkt verknüpften Ticket-Assets Geräte des Requesters suchen.
+USER_DEVICE_CONTEXT_ENABLED=true
+# Asset-Routen.
+GLPI_USER_DEVICE_PATHS=/Assets/Computer
+# {{user_id}} wird vom Agenten ersetzt.
+GLPI_USER_DEVICE_FILTER_TEMPLATE=user.id=={{user_id}}
+# Maximale Anzahl Geräte je Suche.
+GLPI_USER_DEVICE_LIMIT=20
+###############################################################################
+# 26. UPTIME KUMA
+###############################################################################
+# Globaler Schalter für Uptime-Kuma-Kontext.
+UPTIME_KUMA_ENABLED=false
+UPTIME_KUMA_URL=https://uptime.example.com
+# Mögliche Werte:
+#
+# metrics
+# authentifizierte Prometheus-Metrics.
+#
+# status_page
+# öffentliche/publizierte Statusseiten.
+UPTIME_KUMA_MODE=metrics
+# Nur in metrics erforderlich.
+UPTIME_KUMA_API_KEY=
+# Nur in status_page erforderlich.
+#
+# Mehrere Slugs:
+# it-services,network,applications
+UPTIME_KUMA_STATUS_PAGES=it-services
+UPTIME_KUMA_TIMEOUT=10s
+# Maximale Anzahl gleichzeitig berücksichtigter Probleme.
+UPTIME_KUMA_MAX_ISSUES=20
+# Maintenance ebenfalls als Kontext berücksichtigen.
+UPTIME_KUMA_INCLUDE_MAINTENANCE=true
+
+# Optional: bei eindeutig passender Uptime-Kuma-Störung oder Wartung einen
+# ausschließlich vom Betreiber vorgegebenen Text senden. Die KI erzeugt keinen
+# Antworttext; sie wählt nur einen aktiven Kandidaten und liefert eine Confidence.
+CONTEXT_STATUS_REPLY_ENABLED=false
+CONTEXT_STATUS_REPLY_MIN_RELEVANCE=0.50
+CONTEXT_STATUS_REPLY_MIN_AI_CONFIDENCE=0.80
+# Finaler Score = Relevanz × KI-Confidence.
+CONTEXT_STATUS_REPLY_MIN_FINAL_SCORE=0.45
+# Literal \n wird als Zeilenumbruch interpretiert. Verfügbare Platzhalter:
+# {{service_name}}, {{status}}, {{status_page}}, {{message}},
+# {{incident_title}}, {{incident_content}}, {{last_heartbeat}}
+CONTEXT_INCIDENT_REPLY_TEXT=Zu Ihrer Meldung liegt derzeit wahrscheinlich eine zentrale Störung bei {{service_name}} vor. Die Einschränkung kann damit zusammenhängen. Wir beobachten den Status.
+CONTEXT_MAINTENANCE_REPLY_TEXT=Für {{service_name}} läuft derzeit eine Wartung. Die von Ihnen beschriebene Einschränkung kann damit zusammenhängen. Bitte testen Sie den Dienst nach Abschluss der Wartung erneut.
+###############################################################################
+# 27. POLICY-GATES
+###############################################################################
+# Automatische Kategorisierung zulassen.
+AUTO_CATEGORY=true
+# Automatische Antworten grundsätzlich zulassen.
+#
+# DRY_RUN=true verhindert trotzdem das tatsächliche Schreiben nach GLPI.
+AUTO_REPLY=false
+# Mindestconfidence der KI für Kategorieänderungen.
+CATEGORY_CONFIDENCE=0.90
+# Mindestconfidence der KI für Antwortauswahl.
+#
+# Dies allein reicht NICHT für Auto-Reply.
+# Zusätzlich gelten unter anderem:
+#
+# - Knowledge-Evidenz
+# - Retrieval-Regeln
+# - Source Policy
+# - KB auto_reply
+# - Kommunikationspolicy
+# - Followup-Prüfung
+# - Kontext-/Incident-Regeln
+# - zweite Followup-Prüfung unmittelbar vor dem Schreiben
+REPLY_CONFIDENCE=0.97
+###############################################################################
+# 28. KI-PRIORISIERUNG
+###############################################################################
+# Separater KI-Lauf zur Empfehlung der GLPI-Priorität. Der Lauf wird im
+# Diagnose-Cockpit unabhängig von Kategorie, Status und Antwort gespeichert.
+PRIORITY_ENABLED=true
+# Standardmäßig Shadow Mode: Empfehlung und Policy-Gates werden protokolliert,
+# GLPI wird nicht verändert. Für Live-Schreibzugriffe zusätzlich DRY_RUN=false.
+AUTO_PRIORITY=false
+PRIORITY_CONFIDENCE=0.88
+# Eigener Fail-open-Timeout für diesen optionalen KI-Lauf. Kategorie und Antwort laufen danach weiter.
+PRIORITY_ANALYSIS_TIMEOUT=45s
+# Automatische Erhöhung je Ticketlauf; Herabstufungen sind grundsätzlich gesperrt.
+PRIORITY_MAX_INCREASE=1
+# Nur kontrollierte, kommaseparierte Grundcodes dürfen eine Empfehlung tragen.
+PRIORITY_ALLOWED_REASON_CODES=multiple_users_affected,site_affected,organization_affected,core_service_unavailable,security_incident_suspected,data_loss_possible,legal_or_regulatory_risk,business_deadline,no_workaround,safety_relevant,exam_or_event_critical
+###############################################################################
+# 29. ZEITGESTEUERTE KI-ESKALATION
+###############################################################################
+# Unabhängiger Scheduler. Er prüft offene Tickets auch ohne Änderung von date_mod.
+ESCALATION_ENABLED=false
+# Standardmäßig werden nur Diagnose-/Shadow-Läufe erzeugt.
+# Live-Ausführung benötigt zusätzlich DRY_RUN=false und GLPI_AGENT_USER_ID.
+AUTO_ESCALATION=false
+ESCALATION_SCAN_INTERVAL=15m
+# Mindestalter des Tickets seit date_creation, bevor es in den Eskalationsscan gelangt.
+ESCALATION_MIN_AGE=4h
+# Mindestdauer seit der letzten menschlichen Aktivität für den Grund
+# no_human_response. SLA-, Security- und Major-Incident-Gründe können unabhängig
+# davon greifen. Agent-Followups werden über GLPI_AGENT_USER_ID ausgenommen.
+ESCALATION_MIN_INACTIVITY=2h
+# Eigenes KI-Zeitbudget; blockiert die normalen Ticketläufe nicht unbegrenzt.
+ESCALATION_ANALYSIS_TIMEOUT=45s
+ESCALATION_CONFIDENCE=0.88
+ESCALATION_MAX_LEVEL=3
+# Zeitfenster vor time_to_resolve, in dem sla_at_risk deterministisch wahr wird.
+ESCALATION_SLA_RISK_WINDOW=2h
+# Aktionsspezifische Mindeststufen.
+ESCALATION_SERVICE_OWNER_MIN_LEVEL=2
+ESCALATION_MANAGER_REVIEW_MIN_LEVEL=3
+# Mindest-Relevanz eines vom Kontextkollektor gelieferten Major Incidents.
+ESCALATION_MAJOR_INCIDENT_MIN_RELEVANCE=0.50
+ESCALATION_ALLOWED_REASON_CODES=no_human_response,sla_at_risk,sla_breached,business_deadline,no_workaround,security_incident_suspected,unassigned,major_incident_candidate
+# Jede Aktion muss einzeln freigegeben werden. Sichere Einführung: zunächst nur
+# none,raise_priority; weitere Aktionen erst nach Konfiguration der Ziele aktivieren.
+# Verfügbar: none,raise_priority,assign_second_level,assign_security_team,
+# notify_service_owner,link_major_incident,request_manager_review
+ESCALATION_ALLOWED_ACTIONS=none,raise_priority
+
+# Zielgruppen/-benutzer für Zuweisungs- und Benachrichtigungsaktionen.
+# Es handelt sich um numerische GLPI-IDs.
+ESCALATION_SECOND_LEVEL_GROUP_ID=0
+ESCALATION_SECURITY_GROUP_ID=0
+ESCALATION_SERVICE_OWNER_GROUP_ID=0
+ESCALATION_SERVICE_OWNER_USER_ID=0
+ESCALATION_MANAGER_REVIEW_GROUP_ID=0
+ESCALATION_MANAGER_REVIEW_USER_ID=0
+
+# Zu jeder ausgeführten Aktion kann ein privater GLPI-Followup geschrieben werden.
+ESCALATION_ADD_PRIVATE_FOLLOWUP=true
+# Platzhalter: {{ticket_id}}, {{ticket_name}}, {{level}}, {{action}}, {{reason}},
+# {{reason_codes}}, {{major_incident_id}}, {{major_incident_name}},
+# {{major_incident_score}}.
+# Literal \n wird in Template-Werten als Zeilenumbruch interpretiert.
+ESCALATION_SECOND_LEVEL_NOTE=Automatische Eskalation Stufe {{level}}: Übergabe an den Second-Level-Support. Gründe: {{reason_codes}}. KI-Begründung: {{reason}}
+ESCALATION_SECURITY_NOTE=Automatische Eskalation Stufe {{level}}: Übergabe an das Security-Team. Gründe: {{reason_codes}}. KI-Begründung: {{reason}}
+ESCALATION_SERVICE_OWNER_NOTE=Automatische Eskalation Stufe {{level}}: Service Owner wurde zur Prüfung einbezogen. Gründe: {{reason_codes}}. KI-Begründung: {{reason}}
+ESCALATION_MAJOR_INCIDENT_NOTE="Automatische Eskalation Stufe {{level}}: Verknüpfung mit Major Incident #{{major_incident_id}} ({{major_incident_name}}). Relevanz: {{major_incident_score}}. Gründe: {{reason_codes}}."
+ESCALATION_MANAGER_REVIEW_NOTE=Automatische Eskalation Stufe {{level}}: Management-Review angefordert. Gründe: {{reason_codes}}. KI-Begründung: {{reason}}
+
+# Optionaler ausgehender Webhook für Service-Owner- und Management-Benachrichtigungen.
+# Das Token wird nie über die Status-API ausgegeben.
+ESCALATION_WEBHOOK_URL=
+ESCALATION_WEBHOOK_BEARER_TOKEN=
+ESCALATION_WEBHOOK_TIMEOUT=10s
+# Nur für isolierte Testnetze; HTTPS ist der sichere Standard.
+ESCALATION_WEBHOOK_ALLOW_INSECURE_HTTP=false
+
+# GLPI-Adapter für Zuweisungen. Die Feldnamen müssen zur OpenAPI-Beschreibung der
+# konkreten GLPI-Installation passen. Unterstützte Payload-Formen:
+# assigned_groups/assigned_users = Liste von {"id":...};
+# group/group_tech/user/user_tech = einzelnes {"id":...}.
+GLPI_ESCALATION_GROUP_PATCH_FIELD=assigned_groups
+GLPI_ESCALATION_USER_PATCH_FIELD=assigned_users
+
+# Installationsspezifischer Adapter für link_major_incident. Beide Werte sind
+# erforderlich. Platzhalter im Pfad/JSON: {{ticket_id}}, {{source_ticket_id}},
+# {{major_incident_id}}, {{target_ticket_id}}.
+GLPI_ESCALATION_ITIL_LINK_PATH=
+GLPI_ESCALATION_ITIL_LINK_BODY=
+
+# Leer = GLPI_TICKET_FILTER verwenden. Für Produktion ausdrücklich auf offene,
+# eskalierbare Status und die gewünschte Einheit beschränken.
+GLPI_ESCALATION_FILTER=
+GLPI_ESCALATION_LIMIT=100
+###############################################################################
+# 30. WORKER / PRIORITÄTSQUEUE
+###############################################################################
+# Maximale Anzahl wartender Jobs.
+QUEUE_SIZE=256
+# Parallele Ticket-Worker. Der Ollama-Pool kann nur so viele unabhängige
+# Ticketpipelines gleichzeitig verteilen, wie Worker aktiv sind. Für drei
+# gleichartige Nodes ist WORKERS=3 ein sinnvoller Lasttest; jeder Node bleibt
+# zusätzlich durch OLLAMA_NODE_MAX_INFLIGHT begrenzt.
+WORKERS=2
+
+###############################################################################
+# 31. DISTRIBUTED MASTER / MONITORING
+###############################################################################
+NEUROFORGE_BIND_ADDRESS=0.0.0.0
+PROMETHEUS_IMAGE=prom/prometheus:v3.14.0
+GRAFANA_IMAGE=grafana/grafana:13.2.0
+NODE_EXPORTER_IMAGE=prom/node-exporter:v1.12.1
+PROMETHEUS_HOST_PORT=9090
+PROMETHEUS_RETENTION=30d
+GRAFANA_HOST_PORT=3000
+GRAFANA_ADMIN_USER=admin
+GRAFANA_ADMIN_PASSWORD=CHANGE_ME_GRAFANA_ADMIN_PASSWORD
+GRAFANA_ROOT_URL=http://localhost:3000
+# Set to the real subagent host/IP. Used only by Prometheus host telemetry.
+CPU_SUBAGENT_NODE_EXPORTER_TARGET=192.0.2.21:9100
+GPU_SUBAGENT_NODE_EXPORTER_TARGET=192.0.2.22:9100
+GPU_SUBAGENT_DCGM_EXPORTER_TARGET=192.0.2.22:9400
diff --git ab/deployments/master/docker-compose.yml b/deployments/master/docker-compose.yml
new file mode 100644
index 0000000..9bd0c2b
--- /dev/null
+++ b/deployments/master/docker-compose.yml
@@ -0,0 +1,594 @@
+name: glpi-neuroforge-master
+services:
+ searxng:
+ image: ${SEARXNG_IMAGE:-docker.io/searxng/searxng:latest}
+ profiles:
+ - research
+ restart: unless-stopped
+ environment:
+ SEARXNG_SECRET: ${SEARXNG_SECRET}
+ SEARXNG_BASE_URL: http://searxng:8080/
+ FORCE_OWNERSHIP: 'false'
+ volumes:
+ - ./deploy/searxng/settings.yml:/etc/searxng/settings.yml:ro
+ - searxng-cache:/var/cache/searxng
+ ports:
+ - 127.0.0.1:${SEARXNG_HOST_PORT:-8888}:8080
+ read_only: true
+ tmpfs:
+ - /tmp:size=64m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ neuroforge:
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG:?Set IMAGE_TAG
+ to an immutable release tag, for example 1.6.2}
+ command:
+ - -data
+ - /app/data
+ - -listen
+ - :8080
+ restart: unless-stopped
+ environment:
+ NEUROFORGE_ADMIN_TOKEN: ${NEUROFORGE_ADMIN_TOKEN:?Set a unique NeuroForge admin
+ token}
+ NEUROFORGE_APP_API_KEY: ${NEUROFORGE_APP_API_KEY:?Set a unique NeuroForge app
+ API key}
+ NEUROFORGE_INTEGRATION_TOKEN: ${NEUROFORGE_INTEGRATION_TOKEN:?Set a unique NeuroForge
+ integration token}
+ NEUROFORGE_CONTROL_READ_TOKEN: ${NEUROFORGE_CONTROL_READ_TOKEN:?Set a unique
+ NeuroForge control read token}
+ NEUROFORGE_WORKER_TOKEN: ${NEUROFORGE_WORKER_TOKEN:?Set a unique NeuroForge
+ worker token}
+ NEUROFORGE_METRICS_TOKEN: ${NEUROFORGE_METRICS_TOKEN:?Set a unique NeuroForge
+ metrics token}
+ NEUROFORGE_CLUSTER_TOKEN: ${NEUROFORGE_CLUSTER_TOKEN:-}
+ OPENAI_API_KEY: ${OPENAI_API_KEY:-}
+ NEUROFORGE_OLLAMA_URL: ${OLLAMA_BASE_URL:?Set Ollama URL reachable from master,
+ normally GPU subagent host}
+ NEUROFORGE_OLLAMA_API_KEY: ${NEUROFORGE_OLLAMA_API_KEY:-}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
+ NEUROFORGE_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma3}
+ NEUROFORGE_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
+ NEUROFORGE_OLLAMA_NUM_CTX: ${NEUROFORGE_OLLAMA_NUM_CTX:-8192}
+ NEUROFORGE_OLLAMA_NUM_PREDICT: ${NEUROFORGE_OLLAMA_NUM_PREDICT:-0}
+ NEUROFORGE_WORKER_LEASE_SECONDS: ${NEUROFORGE_WORKER_LEASE_SECONDS:-120}
+ NEUROFORGE_WORKER_HEARTBEAT_SECONDS: ${NEUROFORGE_WORKER_HEARTBEAT_SECONDS:-15}
+ NEUROFORGE_WORKER_STALE_AFTER_SECONDS: ${NEUROFORGE_WORKER_STALE_AFTER_SECONDS:-60}
+ NEUROFORGE_WORKER_DEFAULT_MAX_ATTEMPTS: ${NEUROFORGE_WORKER_DEFAULT_MAX_ATTEMPTS:-3}
+ NEUROFORGE_WORKER_RETRY_BACKOFF_SECONDS: ${NEUROFORGE_WORKER_RETRY_BACKOFF_SECONDS:-15}
+ NEUROFORGE_WORKER_MAX_QUEUED_JOBS: ${NEUROFORGE_WORKER_MAX_QUEUED_JOBS:-5000}
+ NEUROFORGE_WORKER_MAX_QUEUED_PAYLOAD_MB: ${NEUROFORGE_WORKER_MAX_QUEUED_PAYLOAD_MB:-128}
+ NEUROFORGE_WORKER_MASTER_APPLY_MAX_ATTEMPTS: ${NEUROFORGE_WORKER_MASTER_APPLY_MAX_ATTEMPTS:-5}
+ NEUROFORGE_WORKER_MASTER_APPLY_BACKOFF_SECONDS: ${NEUROFORGE_WORKER_MASTER_APPLY_BACKOFF_SECONDS:-5}
+ NEUROFORGE_WORKER_JOB_RETENTION_HOURS: ${NEUROFORGE_WORKER_JOB_RETENTION_HOURS:-24}
+ NEUROFORGE_WORKER_MAX_TERMINAL_JOBS: ${NEUROFORGE_WORKER_MAX_TERMINAL_JOBS:-2000}
+ NEUROFORGE_GRAPH_BACKFILL_ENABLED: ${NEUROFORGE_GRAPH_BACKFILL_ENABLED:-true}
+ NEUROFORGE_GRAPH_BACKFILL_INTERVAL_SECONDS: ${NEUROFORGE_GRAPH_BACKFILL_INTERVAL_SECONDS:-10}
+ NEUROFORGE_GRAPH_BACKFILL_BATCH_SIZE: ${NEUROFORGE_GRAPH_BACKFILL_BATCH_SIZE:-16}
+ NEUROFORGE_GRAPH_BACKFILL_MAX_QUEUED: ${NEUROFORGE_GRAPH_BACKFILL_MAX_QUEUED:-64}
+ NEUROFORGE_GRAPH_BACKFILL_MIN_DEGREE: ${NEUROFORGE_GRAPH_BACKFILL_MIN_DEGREE:-3}
+ NEUROFORGE_GRAPH_CANDIDATE_MULTIPLIER: ${NEUROFORGE_GRAPH_CANDIDATE_MULTIPLIER:-6}
+ NEUROFORGE_GRAPH_RETRY_AFTER_MINUTES: ${NEUROFORGE_GRAPH_RETRY_AFTER_MINUTES:-360}
+ NEUROFORGE_GRAPH_REQUIRE_WORKER: ${NEUROFORGE_GRAPH_REQUIRE_WORKER:-true}
+ NEUROFORGE_GRAPH_MAX_HOPS: ${NEUROFORGE_GRAPH_MAX_HOPS:-3}
+ NEUROFORGE_GRAPH_HOP_DECAY: ${NEUROFORGE_GRAPH_HOP_DECAY:-0.60}
+ NEUROFORGE_GRAPH_MAX_EXPANSION: ${NEUROFORGE_GRAPH_MAX_EXPANSION:-64}
+ NEUROFORGE_GRAPH_MIN_EDGE_WEIGHT: ${NEUROFORGE_GRAPH_MIN_EDGE_WEIGHT:-0.05}
+ NEUROFORGE_OFFLOAD_CHAT: ${NEUROFORGE_OFFLOAD_CHAT:-true}
+ NEUROFORGE_OFFLOAD_EMBEDDINGS: ${NEUROFORGE_OFFLOAD_EMBEDDINGS:-true}
+ NEUROFORGE_DISTRIBUTED_INFERENCE_WAIT_SECONDS: ${NEUROFORGE_DISTRIBUTED_INFERENCE_WAIT_SECONDS:-180}
+ NEUROFORGE_READINESS_OLLAMA_LIVE: ${NEUROFORGE_READINESS_OLLAMA_LIVE:-true}
+ NEUROFORGE_CONTROLLED_LEARNING: ${NEUROFORGE_CONTROLLED_LEARNING:-true}
+ NEUROFORGE_GOAL_LEARNING_ENABLED: ${NEUROFORGE_GOAL_LEARNING_ENABLED:-false}
+ NEUROFORGE_RESEARCH_ENABLED: ${NEUROFORGE_RESEARCH_ENABLED:-false}
+ NEUROFORGE_SEARXNG_ENABLED: ${NEUROFORGE_SEARXNG_ENABLED:-false}
+ NEUROFORGE_SEARXNG_URL: ${NEUROFORGE_SEARXNG_URL:-http://searxng:8080}
+ NEUROFORGE_RESEARCH_GOAL_ENABLED: ${NEUROFORGE_RESEARCH_GOAL_ENABLED:-true}
+ NEUROFORGE_AUTONOMY_ENABLED: ${NEUROFORGE_AUTONOMY_ENABLED:-false}
+ NEUROFORGE_AUTONOMY_INTERVAL_MINUTES: ${NEUROFORGE_AUTONOMY_INTERVAL_MINUTES:-30}
+ NEUROFORGE_RESEARCH_MAX_QUERIES: ${NEUROFORGE_RESEARCH_MAX_QUERIES:-2}
+ NEUROFORGE_RESEARCH_MAX_PAGES: ${NEUROFORGE_RESEARCH_MAX_PAGES:-4}
+ NEUROFORGE_KB_STAGING_ENABLED: ${NEUROFORGE_KB_STAGING_ENABLED:-true}
+ NEUROFORGE_KB_STAGING_URL: http://knowledge:8080/api/integrations/staging
+ NEUROFORGE_KB_STAGING_TOKEN: ${KB_INTEGRATION_TOKEN:?Set the Knowledge integration
+ token}
+ NEUROFORGE_KB_STAGING_MIN_EVIDENCE: ${NEUROFORGE_KB_STAGING_MIN_EVIDENCE:-4}
+ NEUROFORGE_KB_STAGING_MIN_SOURCES: ${NEUROFORGE_KB_STAGING_MIN_SOURCES:-2}
+ NEUROFORGE_KB_STAGING_MIN_CORROBORATIONS: ${NEUROFORGE_KB_STAGING_MIN_CORROBORATIONS:-0}
+ NEUROFORGE_KB_STAGING_MAX_EVIDENCE: ${NEUROFORGE_KB_STAGING_MAX_EVIDENCE:-12}
+ NEUROFORGE_KB_STAGING_SYNTHESIS_MODE: ${NEUROFORGE_KB_STAGING_SYNTHESIS_MODE:-llm}
+ NEUROFORGE_KB_STAGING_REQUIRE_AUTHORITATIVE_SOURCE: ${NEUROFORGE_KB_STAGING_REQUIRE_AUTHORITATIVE_SOURCE:-true}
+ NEUROFORGE_KB_STAGING_MIN_AUTHORITATIVE_SOURCES: ${NEUROFORGE_KB_STAGING_MIN_AUTHORITATIVE_SOURCES:-1}
+ NEUROFORGE_KB_STAGING_AUTHORITATIVE_DOMAINS: ${NEUROFORGE_KB_STAGING_AUTHORITATIVE_DOMAINS:-}
+ NEUROFORGE_KB_STAGING_VERIFY_CLAIMS: ${NEUROFORGE_KB_STAGING_VERIFY_CLAIMS:-true}
+ NEUROFORGE_KB_STAGING_MIN_CLAIM_COVERAGE: ${NEUROFORGE_KB_STAGING_MIN_CLAIM_COVERAGE:-1.0}
+ NEUROFORGE_KB_STAGING_REQUIRE_AUTHORITATIVE_ACTIONS: ${NEUROFORGE_KB_STAGING_REQUIRE_AUTHORITATIVE_ACTIONS:-true}
+ NEUROFORGE_KB_STAGING_MAX_VERIFICATION_STATEMENTS: ${NEUROFORGE_KB_STAGING_MAX_VERIFICATION_STATEMENTS:-32}
+ NEUROFORGE_KB_STAGING_VERIFICATION_REPAIR: ${NEUROFORGE_KB_STAGING_VERIFICATION_REPAIR:-true}
+ NEUROFORGE_KB_STAGING_SYNTHESIS_MAX_TOKENS: ${NEUROFORGE_KB_STAGING_SYNTHESIS_MAX_TOKENS:-2600}
+ NEUROFORGE_KB_STAGING_EVIDENCE_PROMPT_MAX_CHARS: ${NEUROFORGE_KB_STAGING_EVIDENCE_PROMPT_MAX_CHARS:-14000}
+ NEUROFORGE_KB_STAGING_MIN_ARTICLE_CHARS: ${NEUROFORGE_KB_STAGING_MIN_ARTICLE_CHARS:-3500}
+ NEUROFORGE_KB_STAGING_TARGET_ARTICLE_CHARS: ${NEUROFORGE_KB_STAGING_TARGET_ARTICLE_CHARS:-6500}
+ NEUROFORGE_KB_STAGING_MAX_ARTICLE_CHARS: ${NEUROFORGE_KB_STAGING_MAX_ARTICLE_CHARS:-10000}
+ NEUROFORGE_KB_STAGING_MIN_ANSWER_CHARS: ${NEUROFORGE_KB_STAGING_MIN_ANSWER_CHARS:-160}
+ NEUROFORGE_KB_STAGING_MAX_ANSWER_CHARS: ${NEUROFORGE_KB_STAGING_MAX_ANSWER_CHARS:-1200}
+ ports:
+ - ${NEUROFORGE_BIND_ADDRESS:-0.0.0.0}:${NEUROFORGE_HOST_PORT:-8090}:8080
+ volumes:
+ - neuroforge-data:/app/data
+ read_only: true
+ tmpfs:
+ - /tmp:size=64m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ healthcheck:
+ test:
+ - CMD
+ - wget
+ - -q
+ - -O
+ - '-'
+ - http://127.0.0.1:8080/readyz
+ interval: 15s
+ timeout: 3s
+ retries: 8
+ start_period: 15s
+ stop_grace_period: 35s
+ agent-data-init:
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG:?Set
+ IMAGE_TAG to an immutable release tag, for example 1.6.2}
+ restart: 'no'
+ user: 0:0
+ volumes:
+ - agent-data:/app/data
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ cap_add:
+ - CHOWN
+ - FOWNER
+ agent:
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:${IMAGE_TAG:?Set IMAGE_TAG
+ to an immutable release tag, for example 1.6.2}
+ restart: unless-stopped
+ environment:
+ AI_CONTENT_LABEL_ENABLED: ${AI_CONTENT_LABEL_ENABLED:-}
+ AUTO_CATEGORY: ${AUTO_CATEGORY:-}
+ AUTO_ESCALATION: ${AUTO_ESCALATION:-}
+ AUTO_PRIORITY: ${AUTO_PRIORITY:-}
+ AUTO_REPLY: ${AUTO_REPLY:-}
+ BRAIN_ACTIVITY_API_KEY: ${NEUROFORGE_INTEGRATION_TOKEN:?Set the NeuroForge integration
+ token}
+ BRAIN_ACTIVITY_URL: http://neuroforge:8080/api/v1/integrations/events
+ CATEGORY_CONFIDENCE: ${CATEGORY_CONFIDENCE:-}
+ CATEGORY_PROMPT_LIMIT: ${CATEGORY_PROMPT_LIMIT:-}
+ CHANGE_CALENDAR_ENABLED: ${CHANGE_CALENDAR_ENABLED:-}
+ CHANGE_LOOKAHEAD: ${CHANGE_LOOKAHEAD:-}
+ CHANGE_LOOKBACK: ${CHANGE_LOOKBACK:-}
+ COMMUNICATION_CLOSING: ${COMMUNICATION_CLOSING:-}
+ COMMUNICATION_LANGUAGE: ${COMMUNICATION_LANGUAGE:-}
+ COMMUNICATION_SALUTATION: ${COMMUNICATION_SALUTATION:-}
+ COMMUNICATION_SIGNATURE: ${COMMUNICATION_SIGNATURE:-}
+ COMMUNICATION_STYLE: ${COMMUNICATION_STYLE:-}
+ CONTEXT_BLOCK_AUTO_REPLY_ON_ERRORS: ${CONTEXT_BLOCK_AUTO_REPLY_ON_ERRORS:-}
+ CONTEXT_BLOCK_AUTO_REPLY_ON_INCIDENT: ${CONTEXT_BLOCK_AUTO_REPLY_ON_INCIDENT:-}
+ CONTEXT_ENABLED: ${CONTEXT_ENABLED:-}
+ CONTEXT_INCIDENT_REPLY_TEXT: ${CONTEXT_INCIDENT_REPLY_TEXT:-}
+ CONTEXT_MAINTENANCE_REPLY_TEXT: ${CONTEXT_MAINTENANCE_REPLY_TEXT:-}
+ CONTEXT_RELEVANCE_MIN_SCORE: ${CONTEXT_RELEVANCE_MIN_SCORE:-}
+ CONTEXT_STATUS_REPLY_ENABLED: ${CONTEXT_STATUS_REPLY_ENABLED:-}
+ CONTEXT_STATUS_REPLY_MIN_AI_CONFIDENCE: ${CONTEXT_STATUS_REPLY_MIN_AI_CONFIDENCE:-}
+ CONTEXT_STATUS_REPLY_MIN_FINAL_SCORE: ${CONTEXT_STATUS_REPLY_MIN_FINAL_SCORE:-}
+ CONTEXT_STATUS_REPLY_MIN_RELEVANCE: ${CONTEXT_STATUS_REPLY_MIN_RELEVANCE:-}
+ CONTEXT_TIMEOUT: ${CONTEXT_TIMEOUT:-}
+ CONTROL_READ_TOKEN: ${CONTROL_READ_TOKEN:?Set the Agent control read token}
+ DATA_DIR: /app/data
+ DRY_RUN: ${DRY_RUN:-}
+ ESCALATION_ADD_PRIVATE_FOLLOWUP: ${ESCALATION_ADD_PRIVATE_FOLLOWUP:-}
+ ESCALATION_ALLOWED_ACTIONS: ${ESCALATION_ALLOWED_ACTIONS:-}
+ ESCALATION_ALLOWED_REASON_CODES: ${ESCALATION_ALLOWED_REASON_CODES:-}
+ ESCALATION_ANALYSIS_TIMEOUT: ${ESCALATION_ANALYSIS_TIMEOUT:-}
+ ESCALATION_CONFIDENCE: ${ESCALATION_CONFIDENCE:-}
+ ESCALATION_ENABLED: ${ESCALATION_ENABLED:-}
+ ESCALATION_MAJOR_INCIDENT_MIN_RELEVANCE: ${ESCALATION_MAJOR_INCIDENT_MIN_RELEVANCE:-}
+ ESCALATION_MAJOR_INCIDENT_NOTE: ${ESCALATION_MAJOR_INCIDENT_NOTE:-}
+ ESCALATION_MANAGER_REVIEW_GROUP_ID: ${ESCALATION_MANAGER_REVIEW_GROUP_ID:-}
+ ESCALATION_MANAGER_REVIEW_MIN_LEVEL: ${ESCALATION_MANAGER_REVIEW_MIN_LEVEL:-}
+ ESCALATION_MANAGER_REVIEW_NOTE: ${ESCALATION_MANAGER_REVIEW_NOTE:-}
+ ESCALATION_MANAGER_REVIEW_USER_ID: ${ESCALATION_MANAGER_REVIEW_USER_ID:-}
+ ESCALATION_MAX_LEVEL: ${ESCALATION_MAX_LEVEL:-}
+ ESCALATION_MIN_AGE: ${ESCALATION_MIN_AGE:-}
+ ESCALATION_MIN_INACTIVITY: ${ESCALATION_MIN_INACTIVITY:-}
+ ESCALATION_SCAN_INTERVAL: ${ESCALATION_SCAN_INTERVAL:-}
+ ESCALATION_SECOND_LEVEL_GROUP_ID: ${ESCALATION_SECOND_LEVEL_GROUP_ID:-}
+ ESCALATION_SECOND_LEVEL_NOTE: ${ESCALATION_SECOND_LEVEL_NOTE:-}
+ ESCALATION_SECURITY_GROUP_ID: ${ESCALATION_SECURITY_GROUP_ID:-}
+ ESCALATION_SECURITY_NOTE: ${ESCALATION_SECURITY_NOTE:-}
+ ESCALATION_SERVICE_OWNER_GROUP_ID: ${ESCALATION_SERVICE_OWNER_GROUP_ID:-}
+ ESCALATION_SERVICE_OWNER_MIN_LEVEL: ${ESCALATION_SERVICE_OWNER_MIN_LEVEL:-}
+ ESCALATION_SERVICE_OWNER_NOTE: ${ESCALATION_SERVICE_OWNER_NOTE:-}
+ ESCALATION_SERVICE_OWNER_USER_ID: ${ESCALATION_SERVICE_OWNER_USER_ID:-}
+ ESCALATION_SLA_RISK_WINDOW: ${ESCALATION_SLA_RISK_WINDOW:-}
+ ESCALATION_WEBHOOK_ALLOW_INSECURE_HTTP: ${ESCALATION_WEBHOOK_ALLOW_INSECURE_HTTP:-}
+ ESCALATION_WEBHOOK_BEARER_TOKEN: ${ESCALATION_WEBHOOK_BEARER_TOKEN:-}
+ ESCALATION_WEBHOOK_TIMEOUT: ${ESCALATION_WEBHOOK_TIMEOUT:-}
+ ESCALATION_WEBHOOK_URL: ${ESCALATION_WEBHOOK_URL:-}
+ GLPI_AGENT_USER_ID: ${GLPI_AGENT_USER_ID:-}
+ GLPI_ALLOWED_STATUS_IDS: ${GLPI_ALLOWED_STATUS_IDS:-}
+ GLPI_ALLOW_INSECURE_HTTP: ${GLPI_ALLOW_INSECURE_HTTP:-}
+ GLPI_API_VERSION: ${GLPI_API_VERSION:-}
+ GLPI_CHANGE_FILTER: ${GLPI_CHANGE_FILTER:-}
+ GLPI_CHANGE_LIMIT: ${GLPI_CHANGE_LIMIT:-}
+ GLPI_CHANGE_PATH: ${GLPI_CHANGE_PATH:-}
+ GLPI_CLIENT_ID: ${GLPI_CLIENT_ID:?Set GLPI_CLIENT_ID}
+ GLPI_CLIENT_SECRET: ${GLPI_CLIENT_SECRET:?Set GLPI_CLIENT_SECRET}
+ GLPI_ESCALATION_FILTER: ${GLPI_ESCALATION_FILTER:-}
+ GLPI_ESCALATION_GROUP_PATCH_FIELD: ${GLPI_ESCALATION_GROUP_PATCH_FIELD:-}
+ GLPI_ESCALATION_ITIL_LINK_BODY: ${GLPI_ESCALATION_ITIL_LINK_BODY:-}
+ GLPI_ESCALATION_ITIL_LINK_PATH: ${GLPI_ESCALATION_ITIL_LINK_PATH:-}
+ GLPI_ESCALATION_LIMIT: ${GLPI_ESCALATION_LIMIT:-}
+ GLPI_ESCALATION_USER_PATCH_FIELD: ${GLPI_ESCALATION_USER_PATCH_FIELD:-}
+ GLPI_KB_AUTO_REPLY: ${GLPI_KB_AUTO_REPLY:-}
+ GLPI_KB_AUTO_REPLY_ALLOW_UNCATEGORIZED: ${GLPI_KB_AUTO_REPLY_ALLOW_UNCATEGORIZED:-}
+ GLPI_KB_AUTO_REPLY_CATEGORY_IDS: ${GLPI_KB_AUTO_REPLY_CATEGORY_IDS:-}
+ GLPI_KB_AUTO_REPLY_ITIL_CATEGORY_IDS: ${GLPI_KB_AUTO_REPLY_ITIL_CATEGORY_IDS:-}
+ GLPI_KB_AUTO_REPLY_UNCATEGORIZED_ARTICLE_IDS: ${GLPI_KB_AUTO_REPLY_UNCATEGORIZED_ARTICLE_IDS:-}
+ GLPI_KB_ENABLED: ${GLPI_KB_ENABLED:-}
+ GLPI_KB_FILTER: ${GLPI_KB_FILTER:-}
+ GLPI_KB_LIMIT: ${GLPI_KB_LIMIT:-}
+ GLPI_KB_PATH: ${GLPI_KB_PATH:-}
+ GLPI_KB_SOURCE: ${GLPI_KB_SOURCE:-}
+ GLPI_KB_SYNC_INTERVAL: ${GLPI_KB_SYNC_INTERVAL:-}
+ GLPI_MAJOR_INCIDENT_FILTER: ${GLPI_MAJOR_INCIDENT_FILTER:-}
+ GLPI_MAJOR_INCIDENT_LIMIT: ${GLPI_MAJOR_INCIDENT_LIMIT:-}
+ GLPI_PASSWORD: ${GLPI_PASSWORD:?Set GLPI_PASSWORD}
+ GLPI_POLL_INTERVAL: ${GLPI_POLL_INTERVAL:-}
+ GLPI_POLL_LIMIT: ${GLPI_POLL_LIMIT:-}
+ GLPI_TICKET_FILTER: ${GLPI_TICKET_FILTER:-}
+ GLPI_TIMEOUT: ${GLPI_TIMEOUT:-}
+ GLPI_URL: ${GLPI_URL:?Set GLPI_URL}
+ GLPI_USERNAME: ${GLPI_USERNAME:?Set GLPI_USERNAME}
+ GLPI_USER_DEVICE_FILTER_TEMPLATE: ${GLPI_USER_DEVICE_FILTER_TEMPLATE:-}
+ GLPI_USER_DEVICE_LIMIT: ${GLPI_USER_DEVICE_LIMIT:-}
+ GLPI_USER_DEVICE_PATHS: ${GLPI_USER_DEVICE_PATHS:-}
+ HTTP_ADDR: :8080
+ KNOWLEDGE_ALLOWED_SOURCES: ${KNOWLEDGE_ALLOWED_SOURCES:-}
+ KNOWLEDGE_AUDIT_TOP_K: ${KNOWLEDGE_AUDIT_TOP_K:-}
+ KNOWLEDGE_AUTO_REPLY_SOURCES: ${KNOWLEDGE_AUTO_REPLY_SOURCES:-}
+ KNOWLEDGE_CANDIDATE_MAX_GAP: ${KNOWLEDGE_CANDIDATE_MAX_GAP:-}
+ KNOWLEDGE_CATEGORY_MAP_FILE: ${KNOWLEDGE_CATEGORY_MAP_FILE:-}
+ KNOWLEDGE_CATEGORY_MODE: ${KNOWLEDGE_CATEGORY_MODE:-}
+ KNOWLEDGE_CATEGORY_SOURCES: ${KNOWLEDGE_CATEGORY_SOURCES:-}
+ KNOWLEDGE_CHUNK_OVERLAP_WORDS: ${KNOWLEDGE_CHUNK_OVERLAP_WORDS:-}
+ KNOWLEDGE_CHUNK_WORDS: ${KNOWLEDGE_CHUNK_WORDS:-}
+ KNOWLEDGE_DIR: /app/knowledge
+ KNOWLEDGE_EMBEDDING_PROFILE: ${KNOWLEDGE_EMBEDDING_PROFILE:-}
+ KNOWLEDGE_EMBED_BATCH_SIZE: ${KNOWLEDGE_EMBED_BATCH_SIZE:-}
+ KNOWLEDGE_EVIDENCE_WEIGHT_AI: ${KNOWLEDGE_EVIDENCE_WEIGHT_AI:-}
+ KNOWLEDGE_EVIDENCE_WEIGHT_CATEGORY: ${KNOWLEDGE_EVIDENCE_WEIGHT_CATEGORY:-}
+ KNOWLEDGE_EVIDENCE_WEIGHT_RETRIEVAL: ${KNOWLEDGE_EVIDENCE_WEIGHT_RETRIEVAL:-}
+ KNOWLEDGE_IGNORE_GLOBS: ${KNOWLEDGE_IGNORE_GLOBS:-}
+ KNOWLEDGE_INDEX_MODE: ${KNOWLEDGE_INDEX_MODE:-}
+ KNOWLEDGE_INDEX_SCAN_INTERVAL: ${KNOWLEDGE_INDEX_SCAN_INTERVAL:-}
+ KNOWLEDGE_MAX_CHUNKS_PER_DOC: ${KNOWLEDGE_MAX_CHUNKS_PER_DOC:-}
+ KNOWLEDGE_MAX_QUERY_CHUNKS: ${KNOWLEDGE_MAX_QUERY_CHUNKS:-}
+ KNOWLEDGE_MIN_SCORE: ${KNOWLEDGE_MIN_SCORE:-}
+ KNOWLEDGE_RETRIEVAL_FLOOR: ${KNOWLEDGE_RETRIEVAL_FLOOR:-}
+ KNOWLEDGE_TOP_K: ${KNOWLEDGE_TOP_K:-}
+ KNOWLEDGE_VECTOR_BACKEND: ${KNOWLEDGE_VECTOR_BACKEND:-dual}
+ KNOWLEDGE_WEB_EDIT_ENABLED: ${AGENT_LEGACY_KNOWLEDGE_EDITOR_ENABLED:-false}
+ KNOWLEDGE_WEIGHT_CATEGORY: ${KNOWLEDGE_WEIGHT_CATEGORY:-}
+ KNOWLEDGE_WEIGHT_KEYWORDS: ${KNOWLEDGE_WEIGHT_KEYWORDS:-}
+ KNOWLEDGE_WEIGHT_LEXICAL: ${KNOWLEDGE_WEIGHT_LEXICAL:-}
+ KNOWLEDGE_WEIGHT_SEMANTIC: ${KNOWLEDGE_WEIGHT_SEMANTIC:-}
+ KNOWLEDGE_WEIGHT_TITLE: ${KNOWLEDGE_WEIGHT_TITLE:-}
+ LEARNING_ENABLED: ${LEARNING_ENABLED:-}
+ LEARNING_EXAMPLES_PER_CATEGORY: ${LEARNING_EXAMPLES_PER_CATEGORY:-}
+ LEARNING_MAX_EXAMPLES: ${LEARNING_MAX_EXAMPLES:-}
+ LOG_LEVEL: ${LOG_LEVEL:-}
+ MAJOR_INCIDENTS_ENABLED: ${MAJOR_INCIDENTS_ENABLED:-}
+ NEUROFORGE_API_KEY: ${NEUROFORGE_INTEGRATION_TOKEN:?Set the NeuroForge integration
+ token}
+ NEUROFORGE_FAIL_OPEN: ${NEUROFORGE_FAIL_OPEN:-true}
+ NEUROFORGE_NAMESPACE: ${NEUROFORGE_NAMESPACE:-glpi-agent}
+ NEUROFORGE_SEARCH_K: ${NEUROFORGE_SEARCH_K:-128}
+ NEUROFORGE_TIMEOUT: ${NEUROFORGE_TIMEOUT:-}
+ NEUROFORGE_URL: http://neuroforge:8080
+ OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-}
+ OLLAMA_FAILOVER_ATTEMPTS: ${OLLAMA_FAILOVER_ATTEMPTS:-}
+ OLLAMA_FAILOVER_ENABLED: ${OLLAMA_FAILOVER_ENABLED:-}
+ OLLAMA_JSON_RETRIES: ${OLLAMA_JSON_RETRIES:-}
+ OLLAMA_KEEP_ALIVE: ${OLLAMA_KEEP_ALIVE:-}
+ OLLAMA_MAX_CONCURRENT: ${OLLAMA_MAX_CONCURRENT:-}
+ OLLAMA_MODEL: ${OLLAMA_MODEL:-}
+ OLLAMA_NODE_FAILURE_COOLDOWN: ${OLLAMA_NODE_FAILURE_COOLDOWN:-}
+ OLLAMA_NODE_HEALTH_INTERVAL: ${OLLAMA_NODE_HEALTH_INTERVAL:-}
+ OLLAMA_NODE_MAX_INFLIGHT: ${OLLAMA_NODE_MAX_INFLIGHT:-}
+ OLLAMA_NODE_NAMES: ${OLLAMA_NODE_NAMES:-}
+ OLLAMA_NODE_REQUEST_TIMEOUT: ${OLLAMA_NODE_REQUEST_TIMEOUT:-}
+ OLLAMA_NODE_WEIGHTS: ${OLLAMA_NODE_WEIGHTS:-}
+ OLLAMA_NUM_PREDICT: ${OLLAMA_NUM_PREDICT:-}
+ OLLAMA_REQUIRE_EMBEDDING_MODEL: ${OLLAMA_REQUIRE_EMBEDDING_MODEL:-}
+ OLLAMA_REQUIRE_SAME_MODEL_DIGEST: ${OLLAMA_REQUIRE_SAME_MODEL_DIGEST:-}
+ OLLAMA_ROUTING_MODE: ${OLLAMA_ROUTING_MODE:-}
+ OLLAMA_THINK: ${OLLAMA_THINK:-}
+ OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-}
+ OLLAMA_URL: ${OLLAMA_BASE_URL:?Set Ollama URL reachable from master}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
+ OLLAMA_URLS: ${OLLAMA_URLS:?Set one or more Ollama URLs reachable from master}
+ OUTCOME_LEARNING_ENABLED: ${OUTCOME_LEARNING_ENABLED:-true}
+ OUTCOME_LEARNING_FAIL_OPEN: ${OUTCOME_LEARNING_FAIL_OPEN:-false}
+ OUTCOME_LEARNING_MAX_OUTCOMES: ${OUTCOME_LEARNING_MAX_OUTCOMES:-2000}
+ OUTCOME_RETRIEVAL_ENABLED: ${OUTCOME_RETRIEVAL_ENABLED:-true}
+ OUTCOME_RETRIEVAL_FAIL_OPEN: ${OUTCOME_RETRIEVAL_FAIL_OPEN:-true}
+ OUTCOME_RETRIEVAL_MIN_SIMILARITY: ${OUTCOME_RETRIEVAL_MIN_SIMILARITY:-0.58}
+ OUTCOME_RETRIEVAL_SEARCH_K: ${OUTCOME_RETRIEVAL_SEARCH_K:-6}
+ PRIORITY_ALLOWED_REASON_CODES: ${PRIORITY_ALLOWED_REASON_CODES:-}
+ PRIORITY_ANALYSIS_TIMEOUT: ${PRIORITY_ANALYSIS_TIMEOUT:-}
+ PRIORITY_CONFIDENCE: ${PRIORITY_CONFIDENCE:-}
+ PRIORITY_ENABLED: ${PRIORITY_ENABLED:-}
+ PRIORITY_MAX_INCREASE: ${PRIORITY_MAX_INCREASE:-}
+ QUEUE_SIZE: ${QUEUE_SIZE:-}
+ RAG_ENABLED: ${RAG_ENABLED:-}
+ REPLY_CONFIDENCE: ${REPLY_CONFIDENCE:-}
+ UPTIME_KUMA_API_KEY: ${UPTIME_KUMA_API_KEY:-}
+ UPTIME_KUMA_ENABLED: ${UPTIME_KUMA_ENABLED:-}
+ UPTIME_KUMA_INCLUDE_MAINTENANCE: ${UPTIME_KUMA_INCLUDE_MAINTENANCE:-}
+ UPTIME_KUMA_MAX_ISSUES: ${UPTIME_KUMA_MAX_ISSUES:-}
+ UPTIME_KUMA_MODE: ${UPTIME_KUMA_MODE:-}
+ UPTIME_KUMA_STATUS_PAGES: ${UPTIME_KUMA_STATUS_PAGES:-}
+ UPTIME_KUMA_TIMEOUT: ${UPTIME_KUMA_TIMEOUT:-}
+ UPTIME_KUMA_URL: ${UPTIME_KUMA_URL:-}
+ USER_DEVICE_CONTEXT_ENABLED: ${USER_DEVICE_CONTEXT_ENABLED:-}
+ WEBHOOK_SECRET: ${WEBHOOK_SECRET:-}
+ WEB_ALLOW_ANONYMOUS: ${WEB_ALLOW_ANONYMOUS:-}
+ WEB_PASSWORD: ${WEB_PASSWORD:-}
+ WEB_USERNAME: ${WEB_USERNAME:-}
+ WORKERS: ${WORKERS:-}
+ ports:
+ - 127.0.0.1:${AGENT_HOST_PORT:-8080}:8080
+ volumes:
+ - agent-data:/app/data
+ - ${KB_DATA_PATH:-./knowledge}:/app/knowledge:ro
+ depends_on:
+ agent-data-init:
+ condition: service_completed_successfully
+ read_only: true
+ tmpfs:
+ - /tmp:size=64m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ healthcheck:
+ test:
+ - CMD
+ - /app/glpi-ai-agent
+ - healthcheck
+ interval: 15s
+ timeout: 3s
+ retries: 8
+ start_period: 10s
+ stop_grace_period: 20s
+ knowledge:
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:${IMAGE_TAG:?Set IMAGE_TAG
+ to an immutable release tag, for example 1.6.2}
+ restart: unless-stopped
+ environment:
+ APP_MODE: ${KB_APP_MODE:-editor}
+ DATA_DIR: /data/knowledge
+ BACKUP_DIR: /data/backups
+ STAGING_DIR: /data/staging
+ LISTEN_ADDR: :8080
+ APP_TITLE: ${APP_TITLE:-}
+ APP_SUBTITLE: ${APP_SUBTITLE:-}
+ AUTO_RELOAD_INTERVAL: ${AUTO_RELOAD_INTERVAL:-}
+ BASIC_AUTH_USER: ${BASIC_AUTH_USER:?Set the Knowledge web user}
+ BASIC_AUTH_PASSWORD: ${BASIC_AUTH_PASSWORD:?Set the Knowledge web password}
+ AI_FALLBACK_ENABLED: ${AI_FALLBACK_ENABLED:-false}
+ OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:?Set Ollama URL reachable from master}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
+ OLLAMA_MODEL: ${OLLAMA_MODEL:-gemma3}
+ OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-10m}
+ OLLAMA_MAX_CONCURRENT: ${OLLAMA_MAX_CONCURRENT:-2}
+ OLLAMA_STAGING_AUTO_REPLY: ${OLLAMA_STAGING_AUTO_REPLY:-false}
+ OLLAMA_STAGING_MIN_SCORE: ${OLLAMA_STAGING_MIN_SCORE:-0.72}
+ BRAIN_ACTIVITY_URL: http://neuroforge:8080/api/v1/integrations/events
+ BRAIN_ACTIVITY_API_KEY: ${NEUROFORGE_INTEGRATION_TOKEN:?Set the NeuroForge integration
+ token}
+ KB_INTEGRATION_TOKEN: ${KB_INTEGRATION_TOKEN:?Set the Knowledge integration
+ token}
+ ports:
+ - 127.0.0.1:${KNOWLEDGE_HOST_PORT:-8081}:8080
+ volumes:
+ - ${KB_DATA_PATH:-./knowledge}:/data/knowledge:${KB_DATA_MOUNT_MODE:-rw}
+ - ${KB_STAGING_PATH:-./staging}:/data/staging:rw
+ - ${KB_BACKUP_PATH:-./backups}:/data/backups:rw
+ read_only: true
+ tmpfs:
+ - /tmp:size=32m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ stop_grace_period: 35s
+ control:
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-control:${IMAGE_TAG:?Set IMAGE_TAG
+ to an immutable release tag, for example 1.6.2}
+ restart: unless-stopped
+ environment:
+ CONTROL_ADDR: :8070
+ CONTROL_BASIC_AUTH_USER: ${CONTROL_BASIC_AUTH_USER:?Set the Control Center user}
+ CONTROL_BASIC_AUTH_PASSWORD: ${CONTROL_BASIC_AUTH_PASSWORD:?Set the Control
+ Center password}
+ AGENT_URL: http://agent:8080
+ KNOWLEDGE_URL: http://knowledge:8080
+ NEUROFORGE_URL: http://neuroforge:8080
+ NEUROFORGE_API_KEY: ${NEUROFORGE_CONTROL_READ_TOKEN:?Set the NeuroForge control
+ read token}
+ CONTROL_READ_TOKEN: ${CONTROL_READ_TOKEN:?Set the Agent control read token}
+ CODEBASE_MEMORY_URL: ${CODEBASE_MEMORY_URL:-}
+ PUBLIC_CODEBASE_MEMORY_URL: ${PUBLIC_CODEBASE_MEMORY_URL:-}
+ KNOWLEDGE_VECTOR_BACKEND: ${KNOWLEDGE_VECTOR_BACKEND:-dual}
+ NEUROFORGE_SEARCH_K: ${NEUROFORGE_SEARCH_K:-128}
+ NEUROFORGE_FAIL_OPEN: ${NEUROFORGE_FAIL_OPEN:-true}
+ NEUROFORGE_CONTROLLED_LEARNING: ${NEUROFORGE_CONTROLLED_LEARNING:-true}
+ NEUROFORGE_GOAL_LEARNING_ENABLED: ${NEUROFORGE_GOAL_LEARNING_ENABLED:-false}
+ OUTCOME_LEARNING_ENABLED: ${OUTCOME_LEARNING_ENABLED:-true}
+ OUTCOME_RETRIEVAL_ENABLED: ${OUTCOME_RETRIEVAL_ENABLED:-true}
+ OUTCOME_RETRIEVAL_SEARCH_K: ${OUTCOME_RETRIEVAL_SEARCH_K:-6}
+ OUTCOME_RETRIEVAL_MIN_SIMILARITY: ${OUTCOME_RETRIEVAL_MIN_SIMILARITY:-0.58}
+ OUTCOME_RETRIEVAL_FAIL_OPEN: ${OUTCOME_RETRIEVAL_FAIL_OPEN:-true}
+ NEUROFORGE_RESEARCH_ENABLED: ${NEUROFORGE_RESEARCH_ENABLED:-false}
+ NEUROFORGE_SEARXNG_ENABLED: ${NEUROFORGE_SEARXNG_ENABLED:-false}
+ NEUROFORGE_KB_STAGING_ENABLED: ${NEUROFORGE_KB_STAGING_ENABLED:-true}
+ NEUROFORGE_AUTONOMY_ENABLED: ${NEUROFORGE_AUTONOMY_ENABLED:-false}
+ PUBLIC_AGENT_URL: http://localhost:${AGENT_HOST_PORT:-8080}
+ PUBLIC_KNOWLEDGE_URL: http://localhost:${KNOWLEDGE_HOST_PORT:-8081}
+ PUBLIC_NEUROFORGE_URL: http://localhost:${NEUROFORGE_HOST_PORT:-8090}/admin
+ ports:
+ - 127.0.0.1:${CONTROL_HOST_PORT:-8070}:8070
+ read_only: true
+ tmpfs:
+ - /tmp:size=16m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ extra_hosts:
+ - host.docker.internal:host-gateway
+ stop_grace_period: 25s
+ prometheus-secrets-init:
+ profiles:
+ - monitoring
+ image: alpine:3.22.1
+ restart: 'no'
+ environment:
+ NEUROFORGE_METRICS_TOKEN: ${NEUROFORGE_METRICS_TOKEN:?Set NeuroForge metrics
+ token}
+ command:
+ - /bin/sh
+ - -ec
+ - printf "%s" "$${NEUROFORGE_METRICS_TOKEN}" > /secrets/neuroforge_metrics.token;
+ chown 65534:65534 /secrets/neuroforge_metrics.token; chmod 0400 /secrets/neuroforge_metrics.token
+ volumes:
+ - prometheus-secrets:/secrets
+ read_only: true
+ tmpfs:
+ - /tmp:size=8m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ prometheus-config-init:
+ profiles:
+ - monitoring
+ image: alpine:3.22.1
+ restart: 'no'
+ environment:
+ CPU_TARGET: ${CPU_SUBAGENT_NODE_EXPORTER_TARGET:-cpu-subagent.example.invalid:9100}
+ GPU_TARGET: ${GPU_SUBAGENT_NODE_EXPORTER_TARGET:-gpu-subagent.example.invalid:9100}
+ GPU_DCGM_TARGET: ${GPU_SUBAGENT_DCGM_EXPORTER_TARGET:-gpu-subagent.example.invalid:9400}
+ command:
+ - /bin/sh
+ - -ec
+ - sed -e "s|__CPU_TARGET__|$${CPU_TARGET}|g" -e "s|__GPU_TARGET__|$${GPU_TARGET}|g"
+ -e "s|__GPU_DCGM_TARGET__|$${GPU_DCGM_TARGET}|g" /template/prometheus.yml.template
+ > /rendered/prometheus.yml
+ volumes:
+ - ./monitoring/prometheus:/template:ro
+ - prometheus-config:/rendered
+ read_only: true
+ tmpfs:
+ - /tmp:size=8m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ node-exporter:
+ profiles:
+ - monitoring
+ image: ${NODE_EXPORTER_IMAGE:-prom/node-exporter:v1.12.1}
+ restart: unless-stopped
+ command:
+ - --path.procfs=/host/proc
+ - --path.sysfs=/host/sys
+ - --path.rootfs=/rootfs
+ - --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
+ volumes:
+ - /proc:/host/proc:ro
+ - /sys:/host/sys:ro
+ - /:/rootfs:ro,rslave
+ read_only: true
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ prometheus:
+ profiles:
+ - monitoring
+ image: ${PROMETHEUS_IMAGE:-prom/prometheus:v3.14.0}
+ restart: unless-stopped
+ command:
+ - --config.file=/etc/prometheus/prometheus.yml
+ - --storage.tsdb.path=/prometheus
+ - --storage.tsdb.retention.time=${PROMETHEUS_RETENTION:-30d}
+ - --web.enable-lifecycle
+ ports:
+ - 127.0.0.1:${PROMETHEUS_HOST_PORT:-9090}:9090
+ volumes:
+ - prometheus-config:/etc/prometheus:ro
+ - ./monitoring/prometheus/alerts.yml:/etc/prometheus-alerts/alerts.yml:ro
+ - prometheus-data:/prometheus
+ - prometheus-secrets:/etc/prometheus-secrets:ro
+ depends_on:
+ neuroforge:
+ condition: service_started
+ prometheus-secrets-init:
+ condition: service_completed_successfully
+ prometheus-config-init:
+ condition: service_completed_successfully
+ read_only: true
+ tmpfs:
+ - /tmp:size=32m,mode=1777
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ grafana:
+ profiles:
+ - monitoring
+ image: ${GRAFANA_IMAGE:-grafana/grafana:13.2.0}
+ restart: unless-stopped
+ environment:
+ GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
+ GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?Set Grafana admin password}
+ GF_USERS_ALLOW_SIGN_UP: 'false'
+ GF_AUTH_ANONYMOUS_ENABLED: 'false'
+ GF_SERVER_ROOT_URL: ${GRAFANA_ROOT_URL:-http://localhost:3000}
+ ports:
+ - 127.0.0.1:${GRAFANA_HOST_PORT:-3000}:3000
+ volumes:
+ - grafana-data:/var/lib/grafana
+ - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
+ - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
+ depends_on:
+ prometheus:
+ condition: service_started
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+volumes:
+ neuroforge-data: null
+ agent-data: null
+ searxng-cache: null
+ prometheus-data: null
+ prometheus-secrets: null
+ prometheus-config: null
+ grafana-data: null
diff --git ab/deployments/master/monitoring/grafana/dashboards/neuroforge-master-subagents.json b/deployments/master/monitoring/grafana/dashboards/neuroforge-master-subagents.json
new file mode 100644
index 0000000..4b48074
--- /dev/null
+++ b/deployments/master/monitoring/grafana/dashboards/neuroforge-master-subagents.json
@@ -0,0 +1,1487 @@
+{
+ "id": null,
+ "uid": "neuroforge-master-subagents",
+ "title": "NeuroForge Master / Subagents / Knowledge Graph",
+ "tags": [
+ "neuroforge",
+ "orchestrator",
+ "knowledge-graph"
+ ],
+ "timezone": "browser",
+ "schemaVersion": 41,
+ "version": 2,
+ "refresh": "15s",
+ "time": {
+ "from": "now-6h",
+ "to": "now"
+ },
+ "timepicker": {},
+ "templating": {
+ "list": []
+ },
+ "annotations": {
+ "list": []
+ },
+ "panels": [
+ {
+ "id": 1,
+ "type": "stat",
+ "title": "Master Up",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_up",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 2,
+ "type": "stat",
+ "title": "Memories",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 4,
+ "y": 0,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_memories",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 3,
+ "type": "stat",
+ "title": "Synapses",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 8,
+ "y": 0,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_synapses",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 4,
+ "type": "stat",
+ "title": "Online CPU Workers",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 12,
+ "y": 0,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "sum(neuroforge_workers{resource=\"cpu\",status=\"online\"})",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 5,
+ "type": "stat",
+ "title": "Online GPU Workers",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 16,
+ "y": 0,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "sum(neuroforge_workers{resource=\"gpu\",status=\"online\"})",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 6,
+ "type": "stat",
+ "title": "Queued Jobs",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 20,
+ "y": 0,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_jobs{status=\"queued\"}",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 7,
+ "type": "stat",
+ "title": "Linked Memories",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 0,
+ "y": 4,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_graph_memories{state=\"linked\"}",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 8,
+ "type": "stat",
+ "title": "Isolated Memories",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 4,
+ "y": 4,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_graph_memories{state=\"isolated\"}",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 9,
+ "type": "stat",
+ "title": "Multi-linked",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 8,
+ "y": 4,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_graph_memories{state=\"multi_linked\"}",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 10,
+ "type": "stat",
+ "title": "Average Degree",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 12,
+ "y": 4,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_graph_average_degree",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 11,
+ "type": "stat",
+ "title": "Connected Components",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 16,
+ "y": 4,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_graph_connected_components",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 12,
+ "type": "stat",
+ "title": "Largest Component",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 20,
+ "y": 4,
+ "w": 4,
+ "h": 4
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_graph_largest_component",
+ "instant": true,
+ "range": false
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "values": false,
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": ""
+ },
+ "orientation": "auto",
+ "textMode": "auto",
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto"
+ }
+ },
+ {
+ "id": 13,
+ "type": "timeseries",
+ "title": "Orchestrator Jobs by Status",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 0,
+ "y": 8,
+ "w": 12,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_jobs",
+ "legendFormat": "{{status}}",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 14,
+ "type": "timeseries",
+ "title": "Worker Inflight",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 12,
+ "y": 8,
+ "w": 12,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_worker_inflight",
+ "legendFormat": "{{worker}} / {{resource}}",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 15,
+ "type": "timeseries",
+ "title": "Worker Capacity",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 0,
+ "y": 16,
+ "w": 12,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_worker_capacity",
+ "legendFormat": "{{worker}} / {{resource}}",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 16,
+ "type": "timeseries",
+ "title": "Graph Linkage",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 12,
+ "y": 16,
+ "w": 12,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_graph_memories",
+ "legendFormat": "{{state}}",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 17,
+ "type": "timeseries",
+ "title": "Vector Index Population",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 0,
+ "y": 24,
+ "w": 12,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_hnsw_nodes",
+ "legendFormat": "HNSW",
+ "range": true
+ },
+ {
+ "refId": "B",
+ "expr": "neuroforge_disk_pq_items",
+ "legendFormat": "Disk PQ",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 18,
+ "type": "timeseries",
+ "title": "HTTP Request Rate",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 12,
+ "y": 24,
+ "w": 12,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "sum by (code) (rate(neuroforge_http_requests_total[5m]))",
+ "legendFormat": "HTTP {{code}}",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "reqps",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 19,
+ "type": "timeseries",
+ "title": "HTTP P95 Latency",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 0,
+ "y": 32,
+ "w": 12,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "histogram_quantile(0.95, sum by (le) (rate(neuroforge_http_request_duration_seconds_bucket[5m])))",
+ "legendFormat": "p95",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "s",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 20,
+ "type": "timeseries",
+ "title": "Master Heap",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 12,
+ "y": 32,
+ "w": 12,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "neuroforge_runtime_heap_alloc_bytes",
+ "legendFormat": "heap alloc",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "bytes",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 21,
+ "type": "timeseries",
+ "title": "Master / CPU / GPU Host CPU",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 0,
+ "y": 40,
+ "w": 12,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "100 - (avg by (job) (rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
+ "legendFormat": "{{job}}",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "percent",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 22,
+ "type": "timeseries",
+ "title": "Host Memory Usage",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 12,
+ "y": 40,
+ "w": 12,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "100 * (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes))",
+ "legendFormat": "{{job}}",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "percent",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 23,
+ "type": "timeseries",
+ "title": "GPU Utilization",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 0,
+ "y": 48,
+ "w": 8,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "avg by (gpu) (DCGM_FI_DEV_GPU_UTIL)",
+ "legendFormat": "GPU {{gpu}}",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "percent",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 24,
+ "type": "timeseries",
+ "title": "GPU Framebuffer Used",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 8,
+ "y": 48,
+ "w": 8,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "DCGM_FI_DEV_FB_USED",
+ "legendFormat": "GPU {{gpu}}",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "MiB",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 25,
+ "type": "timeseries",
+ "title": "GPU Power",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 16,
+ "y": 48,
+ "w": 8,
+ "h": 8
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "DCGM_FI_DEV_POWER_USAGE",
+ "legendFormat": "GPU {{gpu}}",
+ "range": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "watt",
+ "custom": {
+ "drawStyle": "line",
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "fillOpacity": 8,
+ "showPoints": "auto",
+ "spanNulls": false
+ },
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "calcs": [
+ "lastNotNull"
+ ]
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 26,
+ "title": "Durable Job Payload Bytes",
+ "type": "timeseries",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "gridPos": {
+ "x": 0,
+ "y": 56,
+ "w": 24,
+ "h": 8
+ },
+ "fieldConfig": {
+ "defaults": {
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "expr": "neuroforge_job_payload_bytes",
+ "legendFormat": "{{state}}",
+ "refId": "A"
+ }
+ ]
+ }
+ ]
+}
diff --git ab/deployments/master/monitoring/grafana/provisioning/dashboards/neuroforge.yml b/deployments/master/monitoring/grafana/provisioning/dashboards/neuroforge.yml
new file mode 100644
index 0000000..707ffdd
--- /dev/null
+++ b/deployments/master/monitoring/grafana/provisioning/dashboards/neuroforge.yml
@@ -0,0 +1,12 @@
+apiVersion: 1
+
+providers:
+ - name: NeuroForge
+ orgId: 1
+ folder: NeuroForge
+ type: file
+ disableDeletion: true
+ updateIntervalSeconds: 30
+ allowUiUpdates: false
+ options:
+ path: /var/lib/grafana/dashboards
diff --git ab/deployments/master/monitoring/grafana/provisioning/datasources/prometheus.yml b/deployments/master/monitoring/grafana/provisioning/datasources/prometheus.yml
new file mode 100644
index 0000000..00f9915
--- /dev/null
+++ b/deployments/master/monitoring/grafana/provisioning/datasources/prometheus.yml
@@ -0,0 +1,10 @@
+apiVersion: 1
+
+datasources:
+ - name: Prometheus
+ uid: prometheus
+ type: prometheus
+ access: proxy
+ url: http://prometheus:9090
+ isDefault: true
+ editable: false
diff --git ab/deployments/master/monitoring/prometheus/alerts.yml b/deployments/master/monitoring/prometheus/alerts.yml
new file mode 100644
index 0000000..af8bc9d
--- /dev/null
+++ b/deployments/master/monitoring/prometheus/alerts.yml
@@ -0,0 +1,74 @@
+groups:
+ - name: neuroforge-production
+ rules:
+ - alert: NeuroForgeMasterDown
+ expr: up{job="neuroforge"} == 0
+ for: 1m
+ labels: {severity: critical}
+ annotations:
+ summary: "NeuroForge Master is down"
+ description: "Prometheus cannot scrape the NeuroForge Master for more than 1 minute."
+
+ - alert: NeuroForgeCPUWorkerMissing
+ expr: sum(neuroforge_workers{resource="cpu",status="online"}) < 1
+ for: 2m
+ labels: {severity: critical}
+ annotations:
+ summary: "No online CPU subagent"
+ description: "Graph relink/backfill jobs cannot converge without a CPU worker."
+
+ - alert: NeuroForgeGPUWorkerMissing
+ expr: sum(neuroforge_workers{resource="gpu",status="online"}) < 1
+ for: 2m
+ labels: {severity: warning}
+ annotations:
+ summary: "No online GPU subagent"
+ description: "Distributed chat/embed jobs have no online GPU worker."
+
+ - alert: NeuroForgeFailedJobs
+ expr: neuroforge_jobs{status="failed"} > 0
+ for: 5m
+ labels: {severity: warning}
+ annotations:
+ summary: "NeuroForge has failed durable jobs"
+ description: "At least one orchestrator job has remained failed for 5 minutes."
+
+ - alert: NeuroForgeApplyWaitStuck
+ expr: neuroforge_jobs{status="apply_wait"} > 0
+ for: 10m
+ labels: {severity: critical}
+ annotations:
+ summary: "Master apply phase appears stuck"
+ description: "A durable job has remained in apply_wait for more than 10 minutes."
+
+ - alert: NeuroForgeQueueHigh
+ expr: neuroforge_jobs{status="queued"} > 1000
+ for: 10m
+ labels: {severity: warning}
+ annotations:
+ summary: "NeuroForge queue is high"
+ description: "More than 1000 jobs have remained queued for 10 minutes."
+
+ - alert: NeuroForgeGraphHighlyIsolated
+ expr: (neuroforge_graph_memories{state="isolated"} / clamp_min(neuroforge_memories, 1)) > 0.80 and on() neuroforge_memories > 1000
+ for: 30m
+ labels: {severity: warning}
+ annotations:
+ summary: "Knowledge graph remains highly isolated"
+ description: "More than 80% of memories are still isolated after 30 minutes."
+
+ - alert: NeuroForgePendingPayloadHigh
+ expr: neuroforge_job_payload_bytes{state="pending"} > 104857600
+ for: 5m
+ labels: {severity: warning}
+ annotations:
+ summary: "NeuroForge pending job payload memory is high"
+ description: "Pending durable job payload/result bytes exceed 100 MiB for 5 minutes; backfill should remain below the v1.6.1 safety budget."
+
+ - alert: NeuroForgeTerminalPayloadHigh
+ expr: neuroforge_job_payload_bytes{state="terminal"} > 67108864
+ for: 10m
+ labels: {severity: warning}
+ annotations:
+ summary: "NeuroForge terminal job payload retention is high"
+ description: "Terminal job payload/result bytes exceed 64 MiB. Completed vector.relink jobs should be compacted automatically in v1.6.1."
diff --git ab/deployments/master/monitoring/prometheus/prometheus.yml.template b/deployments/master/monitoring/prometheus/prometheus.yml.template
new file mode 100644
index 0000000..5e5899a
--- /dev/null
+++ b/deployments/master/monitoring/prometheus/prometheus.yml.template
@@ -0,0 +1,31 @@
+global:
+ scrape_interval: 15s
+ evaluation_interval: 15s
+
+rule_files:
+ - /etc/prometheus-alerts/alerts.yml
+
+scrape_configs:
+ - job_name: neuroforge
+ metrics_path: /metrics
+ authorization:
+ type: Bearer
+ credentials_file: /etc/prometheus-secrets/neuroforge_metrics.token
+ static_configs:
+ - targets: ["neuroforge:8080"]
+
+ - job_name: master_node
+ static_configs:
+ - targets: ["node-exporter:9100"]
+
+ - job_name: cpu_subagent_node
+ static_configs:
+ - targets: ["__CPU_TARGET__"]
+
+ - job_name: gpu_subagent_node
+ static_configs:
+ - targets: ["__GPU_TARGET__"]
+
+ - job_name: gpu_dcgm
+ static_configs:
+ - targets: ["__GPU_DCGM_TARGET__"]
diff --git ab/deployments/master/preflight.sh b/deployments/master/preflight.sh
new file mode 100755
index 0000000..cb00405
--- /dev/null
+++ b/deployments/master/preflight.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env sh
+set -eu
+[ -f .env ] || { echo 'missing .env'; exit 1; }
+required='IMAGE_TAG NEUROFORGE_ADMIN_TOKEN NEUROFORGE_APP_API_KEY NEUROFORGE_INTEGRATION_TOKEN NEUROFORGE_CONTROL_READ_TOKEN NEUROFORGE_WORKER_TOKEN NEUROFORGE_METRICS_TOKEN KB_INTEGRATION_TOKEN CONTROL_READ_TOKEN GLPI_URL GLPI_CLIENT_ID GLPI_CLIENT_SECRET GLPI_USERNAME GLPI_PASSWORD GRAFANA_ADMIN_PASSWORD'
+for k in $required; do
+ v=$(awk -F= -v key="$k" '$1==key {sub(/^[^=]*=/,""); print; exit}' .env)
+ [ -n "$v" ] || { echo "missing: $k"; exit 1; }
+done
+if grep -Eq '(^|=)(https?://)?192\.0\.2\.|example\.invalid|CHANGE_ME|YOUR-' .env; then
+ echo 'placeholder/example values remain in .env'; exit 1
+fi
+[ "$(awk -F= '$1=="IMAGE_TAG"{print $2}' .env)" = "1.6.2" ] || { echo 'IMAGE_TAG must be 1.6.2'; exit 1; }
+docker compose --profile research --profile monitoring config >/dev/null
+echo 'master preflight: OK'
diff --git ab/deployments/ollama/.env b/deployments/ollama/.env
new file mode 100644
index 0000000..9020ebc
--- /dev/null
+++ b/deployments/ollama/.env
@@ -0,0 +1,12 @@
+###############################################################################
+# Ollama standalone
+###############################################################################
+CORE_NETWORK=glpi-ai-core
+CORE_DATA_ROOT=../../runtime
+OLLAMA_IMAGE=ollama/ollama:0.33.2
+OLLAMA_BIND_IP=127.0.0.1
+OLLAMA_HOST_PORT=11434
+OLLAMA_MODEL=gemma4
+OLLAMA_EMBEDDING_MODEL=embeddinggemma
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
diff --git ab/deployments/ollama/docker-compose.yml b/deployments/ollama/docker-compose.yml
new file mode 100644
index 0000000..735731f
--- /dev/null
+++ b/deployments/ollama/docker-compose.yml
@@ -0,0 +1,20 @@
+name: glpi-ai-ollama-core
+services:
+ ollama:
+ image: ${OLLAMA_IMAGE:-ollama/ollama:0.33.2}
+ restart: unless-stopped
+ ports:
+ - ${OLLAMA_BIND_IP:-127.0.0.1}:${OLLAMA_HOST_PORT:-11434}:11434
+ volumes:
+ - ${CORE_DATA_ROOT:-../../runtime}/ollama:/root/.ollama
+ networks:
+ core:
+ aliases:
+ - ollama
+ security_opt:
+ - no-new-privileges:true
+
+networks:
+ core:
+ external: true
+ name: ${CORE_NETWORK:-glpi-ai-core}
diff --git ab/deployments/ollama/pull-models.sh b/deployments/ollama/pull-models.sh
new file mode 100755
index 0000000..097e592
--- /dev/null
+++ b/deployments/ollama/pull-models.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env sh
+set -eu
+[ -f .env ] && set -a && . ./.env && set +a
+: "${OLLAMA_MODEL:=gemma4}"
+: "${OLLAMA_EMBEDDING_MODEL:=embeddinggemma}"
+docker compose exec ollama ollama pull "$OLLAMA_MODEL"
+docker compose exec ollama ollama pull "$OLLAMA_EMBEDDING_MODEL"
+echo "Modelle vorhanden:"
+docker compose exec ollama ollama list
diff --git ab/docker-bake.hcl b/docker-bake.hcl
new file mode 100644
index 0000000..4a29258
--- /dev/null
+++ b/docker-bake.hcl
@@ -0,0 +1,56 @@
+variable "IMAGE_TAG" {
+ default = "1.6.2"
+}
+
+variable "REGISTRY" {
+ default = "git.send.nrw/sendnrw"
+}
+
+group "default" {
+ targets = ["neuroforge", "neuroforge-worker", "agent", "agent-data-init", "knowledge", "control"]
+}
+
+target "neuroforge" {
+ context = "./platform/neuroforge"
+ dockerfile = "Dockerfile"
+ target = "server"
+ tags = ["${REGISTRY}/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG}"]
+ platforms = ["linux/amd64"]
+}
+
+target "neuroforge-worker" {
+ context = "./platform/neuroforge"
+ dockerfile = "Dockerfile"
+ target = "worker"
+ tags = ["${REGISTRY}/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG}"]
+ platforms = ["linux/amd64"]
+}
+
+target "agent" {
+ context = "./services/agent"
+ dockerfile = "Dockerfile"
+ tags = ["${REGISTRY}/glpi-neuroforge-mega-agent:${IMAGE_TAG}"]
+ platforms = ["linux/amd64"]
+}
+
+target "agent-data-init" {
+ context = "./services/agent"
+ dockerfile = "Dockerfile"
+ target = "data-init"
+ tags = ["${REGISTRY}/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG}"]
+ platforms = ["linux/amd64"]
+}
+
+target "knowledge" {
+ context = "./services/knowledge"
+ dockerfile = "Dockerfile"
+ tags = ["${REGISTRY}/glpi-neuroforge-mega-knowledge:${IMAGE_TAG}"]
+ platforms = ["linux/amd64"]
+}
+
+target "control" {
+ context = "./services/control"
+ dockerfile = "Dockerfile"
+ tags = ["${REGISTRY}/glpi-neuroforge-mega-control:${IMAGE_TAG}"]
+ platforms = ["linux/amd64"]
+}
diff --git a/docker-compose.subagent.yml b/docker-compose.subagent.yml
index 5f923e8..30228ac 100644
--- a/docker-compose.subagent.yml
+++ b/docker-compose.subagent.yml
@@ -7,7 +7,7 @@ name: neuroforge-subagents
services:
cpu-subagent:
profiles: [cpu]
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG, e.g. 1.6.1}
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG, e.g. 1.6.2}
command:
- -server
- ${NEUROFORGE_MASTER_URL:?Set the reachable NeuroForge master URL}
@@ -29,7 +29,7 @@ services:
gpu-subagent:
profiles: [gpu]
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG, e.g. 1.6.1}
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG, e.g. 1.6.2}
command:
- -server
- ${NEUROFORGE_MASTER_URL:?Set the reachable NeuroForge master URL}
@@ -43,6 +43,8 @@ services:
NEUROFORGE_WORKER_MAX_CONCURRENCY: ${NEUROFORGE_GPU_WORKER_CONCURRENCY:-1}
NEUROFORGE_WORKER_HEARTBEAT_INTERVAL: ${NEUROFORGE_WORKER_HEARTBEAT_INTERVAL:-15s}
NEUROFORGE_WORKER_OLLAMA_URL: ${NEUROFORGE_WORKER_OLLAMA_URL:?Set Ollama URL reachable from the GPU subagent}
+ NEUROFORGE_WORKER_OLLAMA_API_KEY: ${NEUROFORGE_WORKER_OLLAMA_API_KEY:-}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
NEUROFORGE_WORKER_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma3}
NEUROFORGE_WORKER_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
NEUROFORGE_WORKER_OLLAMA_NUM_CTX: ${NEUROFORGE_OLLAMA_NUM_CTX:-8192}
diff --git a/docker-compose.yml b/docker-compose.yml
index 61900b1..9e3936b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -33,7 +33,7 @@ services:
cap_drop:
- ALL
neuroforge:
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}
command:
- -data
- /app/data
@@ -50,6 +50,8 @@ services:
NEUROFORGE_CLUSTER_TOKEN: ${NEUROFORGE_CLUSTER_TOKEN:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
NEUROFORGE_OLLAMA_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
+ NEUROFORGE_OLLAMA_API_KEY: ${NEUROFORGE_OLLAMA_API_KEY:-}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
NEUROFORGE_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma3}
NEUROFORGE_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
NEUROFORGE_OLLAMA_NUM_CTX: ${NEUROFORGE_OLLAMA_NUM_CTX:-8192}
@@ -143,7 +145,7 @@ services:
start_period: 15s
stop_grace_period: 35s
neuroforge-worker-cpu:
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}
command:
- -server
- http://neuroforge:8080
@@ -168,7 +170,7 @@ services:
- ALL
neuroforge-worker-gpu:
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}
command:
- -server
- http://neuroforge:8080
@@ -182,6 +184,8 @@ services:
NEUROFORGE_WORKER_MAX_CONCURRENCY: ${NEUROFORGE_GPU_WORKER_CONCURRENCY:-1}
NEUROFORGE_WORKER_HEARTBEAT_INTERVAL: ${NEUROFORGE_WORKER_HEARTBEAT_INTERVAL:-15s}
NEUROFORGE_WORKER_OLLAMA_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
+ NEUROFORGE_WORKER_OLLAMA_API_KEY: ${NEUROFORGE_WORKER_OLLAMA_API_KEY:-}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
NEUROFORGE_WORKER_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma3}
NEUROFORGE_WORKER_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
NEUROFORGE_WORKER_OLLAMA_NUM_CTX: ${NEUROFORGE_OLLAMA_NUM_CTX:-8192}
@@ -200,7 +204,7 @@ services:
- ALL
agent-data-init:
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}
restart: 'no'
user: 0:0
volumes:
@@ -213,7 +217,7 @@ services:
- CHOWN
- FOWNER
agent:
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}
restart: unless-stopped
environment:
AI_CONTENT_LABEL_ENABLED: ${AI_CONTENT_LABEL_ENABLED:-}
@@ -376,6 +380,7 @@ services:
OLLAMA_THINK: ${OLLAMA_THINK:-}
OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-}
OLLAMA_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_URLS: ${OLLAMA_URLS:-http://ollama:11434}
OUTCOME_LEARNING_ENABLED: ${OUTCOME_LEARNING_ENABLED:-true}
OUTCOME_LEARNING_FAIL_OPEN: ${OUTCOME_LEARNING_FAIL_OPEN:-false}
@@ -434,7 +439,7 @@ services:
start_period: 10s
stop_grace_period: 20s
knowledge:
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}
restart: unless-stopped
environment:
APP_MODE: ${KB_APP_MODE:-editor}
@@ -449,6 +454,7 @@ services:
BASIC_AUTH_PASSWORD: ${BASIC_AUTH_PASSWORD:?Set the Knowledge web password}
AI_FALLBACK_ENABLED: ${AI_FALLBACK_ENABLED:-false}
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_MODEL: ${OLLAMA_MODEL:-gemma3}
OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-10m}
OLLAMA_MAX_CONCURRENT: ${OLLAMA_MAX_CONCURRENT:-2}
@@ -475,7 +481,7 @@ services:
- ALL
stop_grace_period: 35s
control:
- image: git.send.nrw/sendnrw/glpi-neuroforge-mega-control:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}
+ image: git.send.nrw/sendnrw/glpi-neuroforge-mega-control:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}
restart: unless-stopped
environment:
CONTROL_ADDR: :8070
diff --git ab/docs/MIGRATION-v1.6.1-to-v1.6.2.md b/docs/MIGRATION-v1.6.1-to-v1.6.2.md
new file mode 100644
index 0000000..b34632c
--- /dev/null
+++ b/docs/MIGRATION-v1.6.1-to-v1.6.2.md
@@ -0,0 +1,24 @@
+# Migration v1.6.1 -> v1.6.2
+
+No data-volume migration is required. Keep `neuroforge-data`, Agent data and Knowledge directories intact.
+
+1. Set `IMAGE_TAG=1.6.2`.
+2. Copy the new optional Ollama variables into your `.env`:
+
+ ```env
+ OLLAMA_API_KEY=
+ NEUROFORGE_OLLAMA_API_KEY=
+ NEUROFORGE_WORKER_OLLAMA_API_KEY=
+ ```
+
+ Leave them empty for an unauthenticated native Ollama endpoint.
+3. If Ollama is protected by a Bearer-aware proxy, normally set only `OLLAMA_API_KEY`. Use the two NeuroForge overrides only when the Master and model worker need different credentials.
+4. Pull/recreate services without deleting volumes:
+
+ ```bash
+ docker compose --profile research pull
+ docker compose --profile research up -d --force-recreate --remove-orphans
+ ```
+5. For distributed deployments, use the complete `.env` shipped under `deployments/master`, `deployments/cpu-subagent`, and `deployments/gpu-subagent` and preserve the same `NEUROFORGE_WORKER_TOKEN` on Master and subagents.
+
+The v1.6.1 OOM/recovery fixes are part of v1.6.2 and must not be removed when merging older deployment files.
diff --git ab/docs/OLLAMA-BEARER-AUTH.md b/docs/OLLAMA-BEARER-AUTH.md
new file mode 100644
index 0000000..ff19322
--- /dev/null
+++ b/docs/OLLAMA-BEARER-AUTH.md
@@ -0,0 +1,36 @@
+# Ollama Bearer authentication
+
+v1.6.2 can call Ollama-compatible endpoints protected by an HTTP Bearer token.
+
+## Shared configuration
+
+```env
+OLLAMA_API_KEY=CHANGE_ME
+```
+
+Every supported Ollama request then carries:
+
+```http
+Authorization: Bearer CHANGE_ME
+```
+
+This applies to health/model discovery (`/api/tags`), chat (`/api/chat`) and embeddings (`/api/embed`).
+
+## NeuroForge overrides
+
+```env
+NEUROFORGE_OLLAMA_API_KEY=
+NEUROFORGE_WORKER_OLLAMA_API_KEY=
+```
+
+`NEUROFORGE_OLLAMA_API_KEY` overrides the shared key for the NeuroForge Master/provider. `NEUROFORGE_WORKER_OLLAMA_API_KEY` overrides it for model-capable NeuroForge workers. If the override is empty, workers fall back to `OLLAMA_API_KEY`; the Master also accepts `OLLAMA_API_KEY` as a shared alias when no NeuroForge-specific key is supplied.
+
+## Agent pools
+
+The GLPI Agent uses one `OLLAMA_API_KEY` for all URLs in `OLLAMA_URLS`. This is intentional for a pool behind one common authentication boundary. Deploy separate Agent instances or a common gateway if individual nodes require unrelated credentials.
+
+## Native Ollama
+
+The environment variable configures the **clients**, not the bundled native Ollama server. Native Ollama does not gain access control from this setting alone. To require authentication, place Ollama behind an authentication-capable reverse proxy/gateway and point `OLLAMA_URL`, `OLLAMA_URLS` or `OLLAMA_BASE_URL` at that endpoint.
+
+Do not put credentials into Ollama URLs. Keep URLs credential-free and use the Bearer variable.
diff --git a/mega-project.json b/mega-project.json
index e97312f..a555b73 100644
--- a/mega-project.json
+++ b/mega-project.json
@@ -20,7 +20,8 @@
"worker_to_neuroforge": "worker_token",
"research_to_kb_staging": "kb_integration_token",
"control_to_agent_graph": "control_read_token",
- "control_to_neuroforge_graph": "app_api_key_read_only_endpoints"
+ "control_to_neuroforge_graph": "app_api_key_read_only_endpoints",
+ "ollama": "optional bearer token via OLLAMA_API_KEY; role-specific NeuroForge overrides supported"
},
"knowledge_export": {
"format": "Obsidian Markdown + YAML frontmatter + Wikilinks",
@@ -28,7 +29,7 @@
"schema": "Wiki/Schema.md",
"glpi_relations": "KnowbaseItem_Item when exposed by GLPI OpenAPI"
},
- "version": "1.5.6",
+ "version": "1.6.2",
"controlled_learning": {
"raw_chat_auto_learning": false,
"validated_outcomes": [
diff --git a/platform/neuroforge/.env.example b/platform/neuroforge/.env.example
index a4f0579..4475a6a 100644
--- a/platform/neuroforge/.env.example
+++ b/platform/neuroforge/.env.example
@@ -1,4 +1,8 @@
# Generate strong random values, e.g. openssl rand -hex 32
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
+# Optional NeuroForge GPU-worker override; falls back to OLLAMA_API_KEY.
+NEUROFORGE_WORKER_OLLAMA_API_KEY=
NEUROFORGE_ADMIN_TOKEN=replace-with-random-admin-token
NEUROFORGE_APP_API_KEY=replace-with-random-app-key
NEUROFORGE_WORKER_TOKEN=replace-with-random-worker-token
@@ -7,3 +11,6 @@ NEUROFORGE_CLUSTER_TOKEN=replace-with-shared-random-cluster-token
# Optional. You can also set this through the web interface.
OPENAI_API_KEY=
+
+# Optional NeuroForge-specific override; takes precedence over OLLAMA_API_KEY.
+NEUROFORGE_OLLAMA_API_KEY=
diff --git a/platform/neuroforge/cmd/server/main.go b/platform/neuroforge/cmd/server/main.go
index c48a86a..d9b0f56 100644
--- a/platform/neuroforge/cmd/server/main.go
+++ b/platform/neuroforge/cmd/server/main.go
@@ -234,6 +234,7 @@ func run() (retErr error) {
changed := false
for name, dst := range map[string]*string{
"OPENAI_API_KEY": &sec.OpenAIAPIKey,
+ "NEUROFORGE_OLLAMA_API_KEY": &sec.OllamaAPIKey,
"NEUROFORGE_ADMIN_TOKEN": &sec.AdminToken,
"NEUROFORGE_APP_API_KEY": &sec.AppAPIKey,
"NEUROFORGE_INTEGRATION_TOKEN": &sec.IntegrationToken,
@@ -247,6 +248,14 @@ func run() (retErr error) {
changed = true
}
}
+ // OLLAMA_API_KEY is a convenient shared alias. The NeuroForge-specific
+ // variable wins when both are set.
+ if strings.TrimSpace(os.Getenv("NEUROFORGE_OLLAMA_API_KEY")) == "" {
+ if v := strings.TrimSpace(os.Getenv("OLLAMA_API_KEY")); v != "" {
+ sec.OllamaAPIKey = v
+ changed = true
+ }
+ }
if changed {
if err := s.UpdateSecrets(sec); err != nil {
return err
diff --git a/platform/neuroforge/cmd/worker/main.go b/platform/neuroforge/cmd/worker/main.go
index d189683..7ba39d1 100644
--- a/platform/neuroforge/cmd/worker/main.go
+++ b/platform/neuroforge/cmd/worker/main.go
@@ -86,6 +86,7 @@ type workerConfig struct {
MaxConcurrency int
Hostname string
OllamaURL string
+ OllamaAPIKey string
OllamaChatModel string
OllamaEmbedModel string
OllamaNumCtx int
@@ -145,6 +146,7 @@ func main() {
Heartbeat: heartbeat, ResourceClass: resource, Capabilities: caps,
MaxConcurrency: maxConcurrency, Hostname: hostname(),
OllamaURL: strings.TrimRight(firstNonEmpty(os.Getenv("NEUROFORGE_WORKER_OLLAMA_URL"), os.Getenv("OLLAMA_BASE_URL"), os.Getenv("OLLAMA_URL")), "/"),
+ OllamaAPIKey: firstNonEmpty(os.Getenv("NEUROFORGE_WORKER_OLLAMA_API_KEY"), os.Getenv("OLLAMA_API_KEY")),
OllamaChatModel: firstNonEmpty(os.Getenv("NEUROFORGE_WORKER_OLLAMA_CHAT_MODEL"), os.Getenv("OLLAMA_MODEL")),
OllamaEmbedModel: firstNonEmpty(os.Getenv("NEUROFORGE_WORKER_OLLAMA_EMBEDDING_MODEL"), os.Getenv("OLLAMA_EMBEDDING_MODEL")),
OllamaNumCtx: envInt("NEUROFORGE_WORKER_OLLAMA_NUM_CTX", 8192),
@@ -342,7 +344,7 @@ func ollamaEmbed(ctx context.Context, c *http.Client, cfg workerConfig, p modelE
Embeddings [][]float32 `json:"embeddings"`
PromptEvalCount int64 `json:"prompt_eval_count"`
}
- if err := postOllama(ctx, c, cfg.OllamaURL+"/api/embed", body, &resp); err != nil {
+ if err := postOllama(ctx, c, cfg.OllamaURL+"/api/embed", cfg.OllamaAPIKey, body, &resp); err != nil {
return modelResult{}, err
}
if len(resp.Embeddings) == 0 || len(resp.Embeddings[0]) == 0 {
@@ -385,19 +387,22 @@ func ollamaChat(ctx context.Context, c *http.Client, cfg workerConfig, p modelCh
PromptEvalCount int64 `json:"prompt_eval_count"`
EvalCount int64 `json:"eval_count"`
}
- if err := postOllama(ctx, c, cfg.OllamaURL+"/api/chat", body, &resp); err != nil {
+ if err := postOllama(ctx, c, cfg.OllamaURL+"/api/chat", cfg.OllamaAPIKey, body, &resp); err != nil {
return modelResult{}, err
}
return modelResult{Text: strings.TrimSpace(resp.Message.Content), Usage: modelUsage{InputTokens: resp.PromptEvalCount, OutputTokens: resp.EvalCount}, Provider: "ollama", Model: model, NodeID: cfg.ID}, nil
}
-func postOllama(ctx context.Context, c *http.Client, url string, body any, out any) error {
+func postOllama(ctx context.Context, c *http.Client, url, apiKey string, body any, out any) error {
raw, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
+ if token := strings.TrimSpace(apiKey); token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
resp, err := c.Do(req)
if err != nil {
return err
diff --git ab/platform/neuroforge/cmd/worker/main_test.go b/platform/neuroforge/cmd/worker/main_test.go
new file mode 100644
index 0000000..90fd243
--- /dev/null
+++ b/platform/neuroforge/cmd/worker/main_test.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestPostOllamaSendsBearerToken(t *testing.T) {
+ const token = "ollama-secret"
+ var auth string
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ auth = r.Header.Get("Authorization")
+ _ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
+ }))
+ defer srv.Close()
+ var out map[string]any
+ if err := postOllama(context.Background(), srv.Client(), srv.URL, token, map[string]any{"x": 1}, &out); err != nil {
+ t.Fatal(err)
+ }
+ if auth != "Bearer "+token {
+ t.Fatalf("authorization=%q", auth)
+ }
+}
diff --git a/platform/neuroforge/docker-compose.yml b/platform/neuroforge/docker-compose.yml
index 618d6aa..d9f9924 100644
--- a/platform/neuroforge/docker-compose.yml
+++ b/platform/neuroforge/docker-compose.yml
@@ -12,6 +12,8 @@ services:
NEUROFORGE_METRICS_TOKEN: ${NEUROFORGE_METRICS_TOKEN}
NEUROFORGE_CLUSTER_TOKEN: ${NEUROFORGE_CLUSTER_TOKEN:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
+ NEUROFORGE_OLLAMA_API_KEY: ${NEUROFORGE_OLLAMA_API_KEY:-}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
volumes:
- neuroforge-data:/app/data
restart: unless-stopped
@@ -59,6 +61,8 @@ services:
NEUROFORGE_WORKER_CAPABILITIES: gpu,model.chat,model.embed
NEUROFORGE_WORKER_MAX_CONCURRENCY: ${NEUROFORGE_GPU_WORKER_CONCURRENCY:-1}
NEUROFORGE_WORKER_OLLAMA_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
+ NEUROFORGE_WORKER_OLLAMA_API_KEY: ${NEUROFORGE_WORKER_OLLAMA_API_KEY:-}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
NEUROFORGE_WORKER_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma3}
NEUROFORGE_WORKER_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
depends_on:
diff --git a/platform/neuroforge/internal/core/types.go b/platform/neuroforge/internal/core/types.go
index c75ef25..fcc08df 100644
--- a/platform/neuroforge/internal/core/types.go
+++ b/platform/neuroforge/internal/core/types.go
@@ -419,6 +419,7 @@ type Config struct {
type Secrets struct {
OpenAIAPIKey string `json:"openai_api_key"`
+ OllamaAPIKey string `json:"ollama_api_key,omitempty"`
AppAPIKey string `json:"app_api_key"`
IntegrationToken string `json:"integration_token,omitempty"`
ControlReadToken string `json:"control_read_token,omitempty"`
diff --git a/platform/neuroforge/internal/httpapi/httpapi.go b/platform/neuroforge/internal/httpapi/httpapi.go
index 254eea6..6017263 100644
--- a/platform/neuroforge/internal/httpapi/httpapi.go
+++ b/platform/neuroforge/internal/httpapi/httpapi.go
@@ -693,7 +693,7 @@ func (s *Server) adminPutModelRouting(w http.ResponseWriter, r *http.Request) {
func (s *Server) adminSecretsStatus(w http.ResponseWriter, r *http.Request) {
sec := s.store.Secrets()
- s.json(w, 200, map[string]any{"openai_configured": sec.OpenAIAPIKey != "", "app_key_configured": sec.AppAPIKey != "", "integration_token_configured": sec.IntegrationToken != "", "control_read_token_configured": sec.ControlReadToken != "", "worker_token_configured": sec.WorkerToken != "", "metrics_token_configured": sec.MetricsToken != "", "shard_tokens": len(sec.ShardAPIToken), "cluster_token_configured": sec.ClusterToken != ""})
+ s.json(w, 200, map[string]any{"openai_configured": sec.OpenAIAPIKey != "", "ollama_api_key_configured": sec.OllamaAPIKey != "", "app_key_configured": sec.AppAPIKey != "", "integration_token_configured": sec.IntegrationToken != "", "control_read_token_configured": sec.ControlReadToken != "", "worker_token_configured": sec.WorkerToken != "", "metrics_token_configured": sec.MetricsToken != "", "shard_tokens": len(sec.ShardAPIToken), "cluster_token_configured": sec.ClusterToken != ""})
}
func maskedSecret(v string) string {
if v == "" {
@@ -708,18 +708,19 @@ func (s *Server) adminGetSecrets(w http.ResponseWriter, r *http.Request) {
sec := s.store.Secrets()
reveal := r.URL.Query().Get("reveal") == "1" && s.store.Config().Security.AllowSecretReveal
if reveal {
- s.json(w, 200, map[string]any{"revealed": true, "app_api_key": sec.AppAPIKey, "integration_token": sec.IntegrationToken, "control_read_token": sec.ControlReadToken, "worker_token": sec.WorkerToken, "metrics_token": sec.MetricsToken, "shard_api_tokens": sec.ShardAPIToken, "cluster_token": sec.ClusterToken})
+ s.json(w, 200, map[string]any{"revealed": true, "ollama_api_key": sec.OllamaAPIKey, "app_api_key": sec.AppAPIKey, "integration_token": sec.IntegrationToken, "control_read_token": sec.ControlReadToken, "worker_token": sec.WorkerToken, "metrics_token": sec.MetricsToken, "shard_api_tokens": sec.ShardAPIToken, "cluster_token": sec.ClusterToken})
return
}
maskedShards := map[string]string{}
for k, v := range sec.ShardAPIToken {
maskedShards[k] = maskedSecret(v)
}
- s.json(w, 200, map[string]any{"revealed": false, "reveal_allowed": s.store.Config().Security.AllowSecretReveal, "app_api_key": maskedSecret(sec.AppAPIKey), "integration_token": maskedSecret(sec.IntegrationToken), "control_read_token": maskedSecret(sec.ControlReadToken), "worker_token": maskedSecret(sec.WorkerToken), "metrics_token": maskedSecret(sec.MetricsToken), "shard_api_tokens": maskedShards, "cluster_token": maskedSecret(sec.ClusterToken)})
+ s.json(w, 200, map[string]any{"revealed": false, "reveal_allowed": s.store.Config().Security.AllowSecretReveal, "ollama_api_key": maskedSecret(sec.OllamaAPIKey), "app_api_key": maskedSecret(sec.AppAPIKey), "integration_token": maskedSecret(sec.IntegrationToken), "control_read_token": maskedSecret(sec.ControlReadToken), "worker_token": maskedSecret(sec.WorkerToken), "metrics_token": maskedSecret(sec.MetricsToken), "shard_api_tokens": maskedShards, "cluster_token": maskedSecret(sec.ClusterToken)})
}
func (s *Server) adminPutSecrets(w http.ResponseWriter, r *http.Request) {
var q struct {
OpenAIAPIKey string `json:"openai_api_key,omitempty"`
+ OllamaAPIKey string `json:"ollama_api_key,omitempty"`
AppAPIKey string `json:"app_api_key,omitempty"`
IntegrationToken string `json:"integration_token,omitempty"`
ControlReadToken string `json:"control_read_token,omitempty"`
@@ -737,8 +738,13 @@ func (s *Server) adminPutSecrets(w http.ResponseWriter, r *http.Request) {
_, ok := os.LookupEnv(name)
return ok && strings.TrimSpace(os.Getenv(name)) != ""
}
+ if q.OllamaAPIKey != "" && (envLocked("NEUROFORGE_OLLAMA_API_KEY") || envLocked("OLLAMA_API_KEY")) {
+ s.err(w, http.StatusConflict, fmt.Errorf("Ollama API key is environment-managed and cannot be changed through the admin API"))
+ return
+ }
for name, value := range map[string]string{
"OPENAI_API_KEY": q.OpenAIAPIKey,
+ "NEUROFORGE_OLLAMA_API_KEY": q.OllamaAPIKey,
"NEUROFORGE_APP_API_KEY": q.AppAPIKey,
"NEUROFORGE_INTEGRATION_TOKEN": q.IntegrationToken,
"NEUROFORGE_CONTROL_READ_TOKEN": q.ControlReadToken,
@@ -754,6 +760,9 @@ func (s *Server) adminPutSecrets(w http.ResponseWriter, r *http.Request) {
if q.OpenAIAPIKey != "" {
sec.OpenAIAPIKey = q.OpenAIAPIKey
}
+ if q.OllamaAPIKey != "" {
+ sec.OllamaAPIKey = q.OllamaAPIKey
+ }
if q.AppAPIKey != "" {
sec.AppAPIKey = q.AppAPIKey
}
@@ -899,7 +908,7 @@ func configuredModelAvailable(models map[string]bool, configured string) bool {
return false
}
-func checkConfiguredOllamaModels(ctx context.Context, cfg core.Config) (bool, any) {
+func checkConfiguredOllamaModels(ctx context.Context, cfg core.Config, apiKey string) (bool, any) {
type tagsResponse struct {
Models []struct {
Name string `json:"name"`
@@ -917,6 +926,9 @@ func checkConfiguredOllamaModels(ctx context.Context, cfg core.Config) (bool, an
details[node.ID] = err.Error()
continue
}
+ if token := strings.TrimSpace(apiKey); token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
resp, err := http.DefaultClient.Do(req)
if err != nil {
details[node.ID] = err.Error()
@@ -977,7 +989,7 @@ func (s *Server) readyz(w http.ResponseWriter, r *http.Request) {
if s.readinessOllamaLive {
ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second)
var detail any
- ollamaLiveReady, detail = checkConfiguredOllamaModels(ctx, cfg)
+ ollamaLiveReady, detail = checkConfiguredOllamaModels(ctx, cfg, s.store.Secrets().OllamaAPIKey)
cancel()
components["ollama_live_models"] = detail
}
diff --git a/platform/neuroforge/internal/provider/provider.go b/platform/neuroforge/internal/provider/provider.go
index 806c05a..1642e2c 100644
--- a/platform/neuroforge/internal/provider/provider.go
+++ b/platform/neuroforge/internal/provider/provider.go
@@ -307,7 +307,7 @@ func (r *Router) chatOllama(ctx context.Context, o core.OllamaServer, model, ins
}
requestCtx, cancel := optionalTimeout(ctx, o.RequestTimeoutSeconds)
defer cancel()
- if err := r.doJSON(requestCtx, "POST", cleanBase(o.BaseURL)+"/api/chat", "", body, &out); err != nil {
+ if err := r.doJSON(requestCtx, "POST", cleanBase(o.BaseURL)+"/api/chat", r.store.Secrets().OllamaAPIKey, body, &out); err != nil {
return ChatResult{}, fmt.Errorf("ollama %s: %w", o.Name, err)
}
if strings.TrimSpace(out.Message.Content) == "" {
@@ -327,7 +327,7 @@ func (r *Router) embedOllama(ctx context.Context, o core.OllamaServer, model, te
}
requestCtx, cancel := optionalTimeout(ctx, o.RequestTimeoutSeconds)
defer cancel()
- if err := r.doJSON(requestCtx, "POST", cleanBase(o.BaseURL)+"/api/embed", "", body, &out); err != nil {
+ if err := r.doJSON(requestCtx, "POST", cleanBase(o.BaseURL)+"/api/embed", r.store.Secrets().OllamaAPIKey, body, &out); err != nil {
return EmbedResult{}, fmt.Errorf("ollama %s: %w", o.Name, err)
}
if len(out.Embeddings) == 0 || len(out.Embeddings[0]) == 0 {
@@ -459,6 +459,9 @@ func (r *Router) Health(ctx context.Context) []map[string]any {
}
healthCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
req, _ := http.NewRequestWithContext(healthCtx, "GET", cleanBase(o.BaseURL)+"/api/tags", nil)
+ if token := strings.TrimSpace(r.store.Secrets().OllamaAPIKey); token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
resp, err := r.http.Do(req)
if err != nil {
cancel()
diff --git a/platform/neuroforge/internal/provider/runtime_test.go b/platform/neuroforge/internal/provider/runtime_test.go
index ee1577a..b804b95 100644
--- a/platform/neuroforge/internal/provider/runtime_test.go
+++ b/platform/neuroforge/internal/provider/runtime_test.go
@@ -111,3 +111,48 @@ func TestOllamaExplicitRequestTimeoutStillWorks(t *testing.T) {
t.Fatalf("configured timeout was not enforced promptly: %v", time.Since(start))
}
}
+
+func TestOllamaBearerTokenIsSentToChatAndHealth(t *testing.T) {
+ const token = "ollama-secret"
+ var chatAuth, tagsAuth string
+ fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/api/chat":
+ chatAuth = r.Header.Get("Authorization")
+ _ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]any{"content": "ok"}})
+ case "/api/tags":
+ tagsAuth = r.Header.Get("Authorization")
+ _ = json.NewEncoder(w).Encode(map[string]any{"models": []map[string]any{{"name": "chat"}, {"name": "embed"}}})
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+ defer fake.Close()
+ s, err := store.New(t.TempDir())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+ cfg := s.Config()
+ cfg.Ollama = []core.OllamaServer{{ID: "secure", Name: "secure", BaseURL: fake.URL, ChatModel: "chat", EmbeddingModel: "embed", Weight: 1, Enabled: true}}
+ cfg.Routing.ChatProvider = "ollama"
+ if err := s.UpdateConfig(cfg); err != nil {
+ t.Fatal(err)
+ }
+ sec := s.Secrets()
+ sec.OllamaAPIKey = token
+ if err := s.UpdateSecrets(sec); err != nil {
+ t.Fatal(err)
+ }
+ r := NewRouter(s)
+ if _, err := r.Chat(context.Background(), "ollama", "chat", "", "hello", 32); err != nil {
+ t.Fatal(err)
+ }
+ _ = r.Health(context.Background())
+ if chatAuth != "Bearer "+token {
+ t.Fatalf("chat authorization=%q", chatAuth)
+ }
+ if tagsAuth != "Bearer "+token {
+ t.Fatalf("tags authorization=%q", tagsAuth)
+ }
+}
diff --git ab/runtime/agent-data/.gitkeep b/runtime/agent-data/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git ab/runtime/backups/.gitkeep b/runtime/backups/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git ab/runtime/knowledge/.gitkeep b/runtime/knowledge/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git ab/runtime/ollama/.gitkeep b/runtime/ollama/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git ab/runtime/staging/.gitkeep b/runtime/staging/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git ab/scripts/create-network.sh b/scripts/create-network.sh
new file mode 100755
index 0000000..28d743e
--- /dev/null
+++ b/scripts/create-network.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env sh
+set -eu
+NETWORK="${CORE_NETWORK:-glpi-ai-core}"
+if docker network inspect "$NETWORK" >/dev/null 2>&1; then
+ echo "Docker-Netzwerk $NETWORK existiert bereits."
+else
+ docker network create "$NETWORK" >/dev/null
+ echo "Docker-Netzwerk $NETWORK wurde angelegt."
+fi
diff --git ab/scripts/health.sh b/scripts/health.sh
new file mode 100755
index 0000000..fefa361
--- /dev/null
+++ b/scripts/health.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env sh
+set -eu
+AGENT_URL="${AGENT_URL:-http://127.0.0.1:9980}"
+KNOWLEDGE_URL="${KNOWLEDGE_URL:-http://127.0.0.1:9981}"
+printf 'Agent /healthz: '
+curl -fsS "$AGENT_URL/healthz" || true
+echo
+printf 'Agent /readyz: '
+curl -fsS "$AGENT_URL/readyz" || true
+echo
+printf 'Knowledge /api/health: '
+curl -fsS "$KNOWLEDGE_URL/api/health" || true
+echo
diff --git ab/scripts/preflight-role.sh b/scripts/preflight-role.sh
new file mode 100755
index 0000000..ef560ff
--- /dev/null
+++ b/scripts/preflight-role.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env sh
+set -eu
+ROLE="${1:-}"
+case "$ROLE" in agent|knowledge|ollama|combined) ;; *) echo "Usage: $0 agent|knowledge|ollama|combined" >&2; exit 2;; esac
+DIR="$(CDPATH= cd -- "$(dirname -- "$0")/../deployments/$ROLE" && pwd)"
+ENVFILE="$DIR/.env"
+[ -f "$ENVFILE" ] || { echo "Fehlt: $ENVFILE" >&2; exit 1; }
+if grep -Eq '^[A-Z0-9_]+=CHANGE_ME' "$ENVFILE"; then
+ echo "Hinweis: CHANGE_ME-Werte vorhanden:"
+ grep -E '^[A-Z0-9_]+=CHANGE_ME' "$ENVFILE" | cut -d= -f1
+ if [ "$ROLE" = agent ] || [ "$ROLE" = combined ]; then exit 1; fi
+fi
+mkdir -p "$DIR/../../runtime/agent-data" "$DIR/../../runtime/knowledge" "$DIR/../../runtime/backups" "$DIR/../../runtime/staging" "$DIR/../../runtime/ollama"
+if command -v docker >/dev/null 2>&1; then
+ (cd "$DIR" && docker compose config >/dev/null)
+ echo "docker compose config: OK ($ROLE)"
+else
+ echo "Docker CLI nicht vorhanden; YAML wurde beim Release statisch geprüft."
+fi
diff --git a/scripts/secret-scan.sh b/scripts/secret-scan.sh
index f16be47..a238aed 100755
--- a/scripts/secret-scan.sh
+++ b/scripts/secret-scan.sh
@@ -23,10 +23,29 @@ fi
# Private keys and common live-token shapes must not be committed. Placeholders in
# templates/docs are intentionally allowed.
-if grep -E '(^|/)\.env$|\.pem$|\.p12$|\.pfx$|(^|/)id_rsa$|(^|/)id_ed25519$' "$FILES" >/dev/null; then
+if grep -E '(^|/)\.env$|\.pem$|\.p12$|\.pfx$|(^|/)id_rsa$|(^|/)id_ed25519$' "$FILES" \
+ | grep -Ev '^deployments/[^/]+/\.env$' >/dev/null; then
bad "private environment/key material found"
fi
+# Deployment role .env files are intentionally shipped as complete templates.
+# Reject them if a sensitive field is populated with anything other than an
+# empty value or an explicit CHANGE_ME placeholder.
+for f in deployments/*/.env; do
+ [ -f "$f" ] || continue
+ awk -F= '
+ /^[[:space:]]*#/ || NF < 2 { next }
+ $1 ~ /(PASSWORD|SECRET|TOKEN|API_KEY|CLIENT_SECRET)$/ {
+ v=$0; sub(/^[^=]*=/,"",v)
+ if (v != "" && v !~ /^CHANGE_ME/) {
+ print FILENAME ": unsafe populated secret field " $1 > "/dev/stderr"
+ bad=1
+ }
+ }
+ END { exit bad ? 1 : 0 }
+ ' "$f" || bad "deployment .env contains a populated secret: $f"
+done
+
if [ "$HAVE_GIT" = true ]; then
if git grep -nE -- '-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----' -- ':!*.example' ':!*.md' >"$KEYS" 2>/dev/null; then
cat "$KEYS" >&2; bad "private key material found"
diff --git ab/scripts/verify-package.sh b/scripts/verify-package.sh
new file mode 100755
index 0000000..8070ced
--- /dev/null
+++ b/scripts/verify-package.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env sh
+set -eu
+ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+cd "$ROOT"
+
+for f in \
+ services/agent/Dockerfile \
+ services/knowledge/Dockerfile \
+ .gitea/workflows/ci.yml \
+ .gitea/workflows/release.yml \
+ docker-bake.hcl; do
+ test -f "$f" || { echo "missing: $f" >&2; exit 1; }
+done
+
+grep -q '^FROM ' services/agent/Dockerfile
+grep -q 'AS data-init' services/agent/Dockerfile
+grep -q '^FROM ' services/knowledge/Dockerfile
+
+echo "OK: Dockerfiles and repository workflows are present."
diff --git a/services/agent/.env.example b/services/agent/.env.example
index f60b51f..4738654 100644
--- a/services/agent/.env.example
+++ b/services/agent/.env.example
@@ -290,6 +290,8 @@ OLLAMA_REQUIRE_EMBEDDING_MODEL=true
# OLLAMA_MODEL=qwen3:8b
# Embedding-Modell für RAG.
OLLAMA_EMBEDDING_MODEL=embeddinggemma
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
# Modellspezifisches Retrieval-Prompting.
# auto = Modell automatisch erkennen; für embeddinggemma empfohlen.
@@ -799,4 +801,4 @@ QUEUE_SIZE=256
# Ticketpipelines gleichzeitig verteilen, wie Worker aktiv sind. Für drei
# gleichartige Nodes ist WORKERS=3 ein sinnvoller Lasttest; jeder Node bleibt
# zusätzlich durch OLLAMA_NODE_MAX_INFLIGHT begrenzt.
-WORKERS=2
\ No newline at end of file
+WORKERS=2
diff --git a/services/agent/cmd/agent/main.go b/services/agent/cmd/agent/main.go
index d413c74..8b57d04 100644
--- a/services/agent/cmd/agent/main.go
+++ b/services/agent/cmd/agent/main.go
@@ -62,7 +62,7 @@ func main() {
nodes = append(nodes, ollama.NodeConfig{Name: name, URL: nodeURL, Weight: weight})
}
o, err := ollama.NewPool(ollama.PoolConfig{
- Nodes: nodes, RoutingMode: cfg.OllamaRoutingMode, NodeMaxInflight: cfg.OllamaNodeMaxInflight,
+ APIKey: cfg.OllamaAPIKey, Nodes: nodes, RoutingMode: cfg.OllamaRoutingMode, NodeMaxInflight: cfg.OllamaNodeMaxInflight,
HealthInterval: cfg.OllamaNodeHealthInterval, FailureCooldown: cfg.OllamaNodeFailureCooldown,
NodeRequestTimeout: cfg.OllamaNodeRequestTimeout, FailoverEnabled: cfg.OllamaFailoverEnabled,
FailoverAttempts: cfg.OllamaFailoverAttempts, RequireSameModelDigest: cfg.OllamaRequireSameDigest,
diff --git a/services/agent/docker-compose.registry.yml b/services/agent/docker-compose.registry.yml
index d8eee4c..b075921 100644
--- a/services/agent/docker-compose.registry.yml
+++ b/services/agent/docker-compose.registry.yml
@@ -8,6 +8,7 @@ services:
DATA_DIR: /app/data
KNOWLEDGE_DIR: /app/knowledge
OLLAMA_URL: ${OLLAMA_URL:-http://ollama:11434}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-10m}
OLLAMA_NUM_PREDICT: ${OLLAMA_NUM_PREDICT:-768}
OLLAMA_JSON_RETRIES: ${OLLAMA_JSON_RETRIES:-1}
diff --git a/services/agent/docker-compose.yml b/services/agent/docker-compose.yml
index d6b4492..92e9667 100644
--- a/services/agent/docker-compose.yml
+++ b/services/agent/docker-compose.yml
@@ -24,6 +24,7 @@ services:
DATA_DIR: /app/data
KNOWLEDGE_DIR: /app/knowledge
OLLAMA_URL: ${OLLAMA_URL:-http://ollama:11434}
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
# Local CPU inference can take several minutes on the first request.
OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-10m}
OLLAMA_NUM_PREDICT: ${OLLAMA_NUM_PREDICT:-768}
diff --git a/services/agent/internal/config/config.go b/services/agent/internal/config/config.go
index d660a1a..e274989 100644
--- a/services/agent/internal/config/config.go
+++ b/services/agent/internal/config/config.go
@@ -39,6 +39,7 @@ type Config struct {
GLPIAllowedStatusIDs []int64
OllamaURL string // legacy single-node value
+ OllamaAPIKey string // optional Bearer token sent to every Ollama node
OllamaURLs []string
OllamaNodeNames []string
OllamaNodeWeights []int
@@ -246,6 +247,7 @@ func Load() (Config, error) {
GLPIAllowInsecureHTTP: envBool("GLPI_ALLOW_INSECURE_HTTP", false),
GLPIAllowedStatusIDs: envInt64List("GLPI_ALLOWED_STATUS_IDS", "1"),
OllamaURL: strings.TrimRight(env("OLLAMA_URL", "http://ollama:11434"), "/"),
+ OllamaAPIKey: strings.TrimSpace(os.Getenv("OLLAMA_API_KEY")),
OllamaURLs: envStringListPreserveCase("OLLAMA_URLS", ""),
OllamaNodeNames: envStringListPreserveCase("OLLAMA_NODE_NAMES", ""),
OllamaNodeWeights: envIntListAllowEmpty("OLLAMA_NODE_WEIGHTS"),
diff --git a/services/agent/internal/ollama/pool.go b/services/agent/internal/ollama/pool.go
index 6341700..bd0579f 100644
--- a/services/agent/internal/ollama/pool.go
+++ b/services/agent/internal/ollama/pool.go
@@ -29,6 +29,7 @@ type NodeConfig struct {
}
type PoolConfig struct {
+ APIKey string
Nodes []NodeConfig
RoutingMode string
NodeMaxInflight int
@@ -390,6 +391,7 @@ func (p *Pool) checkNode(ctx context.Context, n *poolNode) (string, string, erro
if err != nil {
return "", "", err
}
+ setBearer(req, p.cfg.APIKey)
resp, err := p.http.Do(req)
if err != nil {
return "", "", err
@@ -559,6 +561,7 @@ func (p *Pool) doPost(ctx context.Context, n *poolNode, path string, body []byte
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
+ setBearer(req, p.cfg.APIKey)
resp, err := p.http.Do(req)
if err != nil {
return 0, nil, err
@@ -574,6 +577,13 @@ func (p *Pool) doPost(ctx context.Context, n *poolNode, path string, body []byte
return resp.StatusCode, raw, nil
}
+func setBearer(req *http.Request, token string) {
+ token = strings.TrimSpace(token)
+ if token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+}
+
func (p *Pool) selectNode(ctx context.Context, excluded map[string]struct{}, stage string) (*poolNode, int64, error) {
for {
nodes := p.orderedCandidates(excluded, stage)
diff --git a/services/agent/internal/ollama/pool_test.go b/services/agent/internal/ollama/pool_test.go
index ed3bd03..eacf386 100644
--- a/services/agent/internal/ollama/pool_test.go
+++ b/services/agent/internal/ollama/pool_test.go
@@ -388,3 +388,41 @@ func TestPoolFastestRecentProbesUnmeasuredNodes(t *testing.T) {
t.Fatalf("fastest_recent must measure both nodes before preferring one: a=%d b=%d", callsA.Load(), callsB.Load())
}
}
+
+func TestPoolSendsBearerToHealthAndInference(t *testing.T) {
+ const token = "ollama-secret"
+ var tagsAuth, embedAuth string
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/api/tags":
+ tagsAuth = r.Header.Get("Authorization")
+ tagsResponse(w, "chat-digest", "embed-digest")
+ case "/api/embed":
+ embedAuth = r.Header.Get("Authorization")
+ _ = json.NewEncoder(w).Encode(map[string]any{"embeddings": [][]float64{{1, 0}}})
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+ defer srv.Close()
+ c, err := NewPool(PoolConfig{
+ APIKey: token, Nodes: []NodeConfig{{Name: "secure", URL: srv.URL, Weight: 1}}, RoutingMode: "least_inflight",
+ NodeMaxInflight: 1, HealthInterval: time.Minute, FailureCooldown: time.Second, NodeRequestTimeout: time.Second,
+ FailoverEnabled: false, FailoverAttempts: 1, RequireSameModelDigest: true, RequireEmbeddingModel: true, Model: "m", EmbeddingModel: "e",
+ }, "m", "e", "de-DE", "formal", 128, time.Minute, false, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := c.Ping(context.Background()); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := c.Embed(context.Background(), []string{"x"}); err != nil {
+ t.Fatal(err)
+ }
+ if tagsAuth != "Bearer "+token {
+ t.Fatalf("tags authorization=%q", tagsAuth)
+ }
+ if embedAuth != "Bearer "+token {
+ t.Fatalf("embed authorization=%q", embedAuth)
+ }
+}
diff --git a/services/control/engineering-graph.json b/services/control/engineering-graph.json
index e7c5396..77c70d6 100644
--- a/services/control/engineering-graph.json
+++ b/services/control/engineering-graph.json
@@ -2464,7 +2464,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": true,
- "line": 1063,
+ "line": 1065,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2476,7 +2476,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": true,
- "line": 440,
+ "line": 442,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2488,7 +2488,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": true,
- "line": 224,
+ "line": 225,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2500,7 +2500,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1037,
+ "line": 1039,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2512,7 +2512,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1208,
+ "line": 1210,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2524,7 +2524,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1252,
+ "line": 1254,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2536,7 +2536,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1241,
+ "line": 1243,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2548,7 +2548,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1219,
+ "line": 1221,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2560,7 +2560,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1230,
+ "line": 1232,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2572,7 +2572,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1086,
+ "line": 1088,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2584,7 +2584,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1108,
+ "line": 1110,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2596,7 +2596,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1191,
+ "line": 1193,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2608,7 +2608,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1052,
+ "line": 1054,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2620,7 +2620,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1183,
+ "line": 1185,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2632,7 +2632,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1130,
+ "line": 1132,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2644,7 +2644,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1156,
+ "line": 1158,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2656,7 +2656,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1046,
+ "line": 1048,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2668,7 +2668,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1082,
+ "line": 1084,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2680,7 +2680,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1023,
+ "line": 1025,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2692,7 +2692,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 1018,
+ "line": 1020,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2704,7 +2704,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
- "line": 993,
+ "line": 995,
"path": "services/agent/internal/config/config.go"
}
},
@@ -5716,7 +5716,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": true,
- "line": 272,
+ "line": 273,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5728,7 +5728,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": true,
- "line": 262,
+ "line": 263,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5740,7 +5740,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": true,
- "line": 243,
+ "line": 244,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5752,7 +5752,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 667,
+ "line": 677,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5764,7 +5764,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 388,
+ "line": 389,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5776,7 +5776,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 549,
+ "line": 551,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5788,7 +5788,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 596,
+ "line": 606,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5800,7 +5800,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 453,
+ "line": 455,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5812,7 +5812,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 280,
+ "line": 281,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5824,7 +5824,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 577,
+ "line": 587,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5836,7 +5836,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 680,
+ "line": 690,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5848,7 +5848,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": true,
- "line": 774,
+ "line": 784,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5860,7 +5860,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": true,
- "line": 731,
+ "line": 741,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5872,7 +5872,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 369,
+ "line": 370,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5896,7 +5896,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 709,
+ "line": 719,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5908,7 +5908,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 693,
+ "line": 703,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5920,7 +5920,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 763,
+ "line": 773,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5932,7 +5932,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 715,
+ "line": 725,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5944,7 +5944,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 438,
+ "line": 440,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5956,7 +5956,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 161,
+ "line": 162,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5992,7 +5992,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 703,
+ "line": 713,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6004,7 +6004,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 78,
+ "line": 79,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6016,7 +6016,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 540,
+ "line": 542,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6028,7 +6028,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 92,
+ "line": 93,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6040,7 +6040,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 104,
+ "line": 105,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6052,7 +6052,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 87,
+ "line": 88,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6064,7 +6064,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 135,
+ "line": 136,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6076,7 +6076,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 745,
+ "line": 755,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6088,7 +6088,19 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 740,
+ "line": 750,
+ "path": "services/agent/internal/ollama/pool.go"
+ }
+ },
+ {
+ "id": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer",
+ "kind": "function",
+ "label": "setBearer",
+ "group": "engineering",
+ "community": "github.com/example/glpi-ai-agent/internal/ollama",
+ "meta": {
+ "exported": false,
+ "line": 580,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6112,7 +6124,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
- "line": 736,
+ "line": 746,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -7756,7 +7768,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
- "line": 292,
+ "line": 293,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7768,7 +7780,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
- "line": 304,
+ "line": 305,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7804,7 +7816,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
- "line": 345,
+ "line": 346,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7828,7 +7840,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
- "line": 311,
+ "line": 312,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7840,7 +7852,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
- "line": 284,
+ "line": 285,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7864,7 +7876,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
- "line": 337,
+ "line": 338,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7948,7 +7960,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
- "line": 40,
+ "line": 41,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -7960,7 +7972,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
- "line": 76,
+ "line": 77,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -7972,7 +7984,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
- "line": 106,
+ "line": 107,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -7984,7 +7996,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
- "line": 73,
+ "line": 74,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -7996,7 +8008,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
- "line": 74,
+ "line": 75,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -8008,7 +8020,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
- "line": 72,
+ "line": 73,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -8020,7 +8032,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": false,
- "line": 110,
+ "line": 111,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -9832,7 +9844,7 @@
"community": "neuroforge/cmd/server",
"meta": {
"exported": false,
- "line": 623,
+ "line": 632,
"path": "platform/neuroforge/cmd/server/main.go"
}
},
@@ -9916,7 +9928,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 255,
+ "line": 257,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9928,7 +9940,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 416,
+ "line": 421,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9940,7 +9952,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 454,
+ "line": 459,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9952,7 +9964,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 507,
+ "line": 512,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9964,7 +9976,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 495,
+ "line": 500,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9976,7 +9988,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 486,
+ "line": 491,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9988,7 +10000,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 468,
+ "line": 473,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10000,7 +10012,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 239,
+ "line": 241,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10012,7 +10024,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 446,
+ "line": 451,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10024,7 +10036,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 100,
+ "line": 101,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10036,7 +10048,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 105,
+ "line": 106,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10048,7 +10060,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 110,
+ "line": 111,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10060,7 +10072,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 478,
+ "line": 483,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10072,7 +10084,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 120,
+ "line": 121,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10084,7 +10096,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 354,
+ "line": 356,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10096,7 +10108,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 332,
+ "line": 334,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10108,7 +10120,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 189,
+ "line": 191,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10120,7 +10132,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 421,
+ "line": 426,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10132,7 +10144,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 394,
+ "line": 396,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10144,7 +10156,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 235,
+ "line": 237,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10156,7 +10168,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 279,
+ "line": 281,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10168,7 +10180,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 178,
+ "line": 180,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10180,7 +10192,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
- "line": 227,
+ "line": 229,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -12124,7 +12136,7 @@
"community": "neuroforge/internal/core",
"meta": {
"exported": true,
- "line": 741,
+ "line": 742,
"path": "platform/neuroforge/internal/core/types.go"
}
},
@@ -12340,7 +12352,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 827,
+ "line": 836,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12352,7 +12364,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 801,
+ "line": 810,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12388,7 +12400,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 823,
+ "line": 832,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12544,7 +12556,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 790,
+ "line": 799,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12616,7 +12628,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 785,
+ "line": 794,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12784,7 +12796,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 813,
+ "line": 822,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12808,7 +12820,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 816,
+ "line": 825,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13348,7 +13360,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 884,
+ "line": 893,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13384,7 +13396,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 949,
+ "line": 961,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13396,7 +13408,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 859,
+ "line": 868,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13456,7 +13468,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 837,
+ "line": 846,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13600,7 +13612,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 902,
+ "line": 911,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13612,7 +13624,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
- "line": 888,
+ "line": 897,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -21102,7 +21114,7 @@
"community": "compose",
"status": "configured",
"meta": {
- "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}"
+ "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-agent:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}"
}
},
{
@@ -21113,7 +21125,7 @@
"community": "compose",
"status": "configured",
"meta": {
- "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}"
+ "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}"
}
},
{
@@ -21124,7 +21136,7 @@
"community": "compose",
"status": "configured",
"meta": {
- "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-control:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}"
+ "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-control:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}"
}
},
{
@@ -21135,7 +21147,7 @@
"community": "compose",
"status": "configured",
"meta": {
- "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}"
+ "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-knowledge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}"
}
},
{
@@ -21146,7 +21158,7 @@
"community": "compose",
"status": "configured",
"meta": {
- "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}"
+ "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}"
}
},
{
@@ -21157,7 +21169,7 @@
"community": "compose",
"status": "configured",
"meta": {
- "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}"
+ "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}"
}
},
{
@@ -21168,7 +21180,7 @@
"community": "compose",
"status": "configured",
"meta": {
- "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.1}"
+ "image": "git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG to an immutable release tag, for example 1.6.2}"
}
},
{
@@ -32967,6 +32979,12 @@
"to": "function:github.com/example/glpi-ai-agent/internal/ollama:requestStage",
"kind": "defines"
},
+ {
+ "id": "file:services/agent/internal/ollama/pool.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:setBearer:defines",
+ "from": "file:services/agent/internal/ollama/pool.go",
+ "to": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer",
+ "kind": "defines"
+ },
{
"id": "file:services/agent/internal/ollama/pool.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:withStage:defines",
"from": "file:services/agent/internal/ollama/pool.go",
@@ -43734,6 +43752,12 @@
"to": "function:github.com/example/glpi-ai-agent/internal/ollama:modelNameMatches",
"kind": "calls"
},
+ {
+ "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.checkNode-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:setBearer:calls",
+ "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.checkNode",
+ "to": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer",
+ "kind": "calls"
+ },
{
"id": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.checkNode-\u003epackage:encoding/json:calls_package",
"from": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.checkNode",
@@ -43769,6 +43793,12 @@
"kind": "calls_package",
"label": "TrimSpace"
},
+ {
+ "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.doPost-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:setBearer:calls",
+ "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.doPost",
+ "to": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer",
+ "kind": "calls"
+ },
{
"id": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.doPost-\u003epackage:bytes:calls_package",
"from": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.doPost",
@@ -44018,6 +44048,13 @@
"kind": "calls_package",
"label": "Now"
},
+ {
+ "id": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer-\u003epackage:strings:calls_package",
+ "from": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer",
+ "to": "package:strings",
+ "kind": "calls_package",
+ "label": "TrimSpace"
+ },
{
"id": "function:github.com/example/glpi-ai-agent/internal/ollama:uniqueStrings-\u003epackage:strings:calls_package",
"from": "function:github.com/example/glpi-ai-agent/internal/ollama:uniqueStrings",
@@ -56620,6 +56657,13 @@
"kind": "calls_package",
"label": "NewRequestWithContext"
},
+ {
+ "id": "function:neuroforge/internal/provider:Router.Health-\u003epackage:strings:calls_package",
+ "from": "function:neuroforge/internal/provider:Router.Health",
+ "to": "package:strings",
+ "kind": "calls_package",
+ "label": "TrimSpace"
+ },
{
"id": "function:neuroforge/internal/provider:Router.chatOllama-\u003efunction:neuroforge/internal/provider:Router.doJSON:calls",
"from": "function:neuroforge/internal/provider:Router.chatOllama",
@@ -66863,10 +66907,10 @@
}
],
"meta": {
- "edges": 7273,
+ "edges": 7278,
"format_version": 1,
"generator": "go-ast+compose",
"modules": 4,
- "nodes": 1845
+ "nodes": 1846
}
}
diff --git a/services/knowledge/.env.example b/services/knowledge/.env.example
index 9289ba6..e560405 100644
--- a/services/knowledge/.env.example
+++ b/services/knowledge/.env.example
@@ -1,4 +1,6 @@
# Betriebsmodus desselben Images:
+# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
+OLLAMA_API_KEY=
# editor = vollständiger Einzel-/Masseneditor mit Schreibzugriff
# google = schreibgeschützte Helpdesk-Suchmaschine mit Artikel-Viewer
APP_MODE=editor
diff --git a/services/knowledge/cmd/server/main.go b/services/knowledge/cmd/server/main.go
index c2b3233..cf4fbb1 100644
--- a/services/knowledge/cmd/server/main.go
+++ b/services/knowledge/cmd/server/main.go
@@ -269,6 +269,7 @@ func aiServiceFromEnv(mode string, st *staging.Store) (*aifallback.Service, time
}
svc, err := aifallback.New(aifallback.Config{
BaseURL: envOr("OLLAMA_BASE_URL", "http://ollama:11434"),
+ APIKey: strings.TrimSpace(os.Getenv("OLLAMA_API_KEY")),
Model: strings.TrimSpace(os.Getenv("OLLAMA_MODEL")),
Timeout: timeout,
MaxConcurrent: maxConcurrent,
diff --git a/services/knowledge/docker-compose.dual.yml b/services/knowledge/docker-compose.dual.yml
index bdcc023..7574e60 100644
--- a/services/knowledge/docker-compose.dual.yml
+++ b/services/knowledge/docker-compose.dual.yml
@@ -49,6 +49,7 @@ services:
BASIC_AUTH_PASSWORD: "${SEARCH_AUTH_PASSWORD:-}"
AI_FALLBACK_ENABLED: "${AI_FALLBACK_ENABLED:-false}"
OLLAMA_BASE_URL: "${OLLAMA_BASE_URL:-http://ollama:11434}"
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_MODEL: "${OLLAMA_MODEL:-}"
OLLAMA_TIMEOUT: "${OLLAMA_TIMEOUT:-10m}"
OLLAMA_MAX_CONCURRENT: "${OLLAMA_MAX_CONCURRENT:-1}"
diff --git a/services/knowledge/docker-compose.yml b/services/knowledge/docker-compose.yml
index f05a373..739ec1d 100644
--- a/services/knowledge/docker-compose.yml
+++ b/services/knowledge/docker-compose.yml
@@ -18,6 +18,7 @@ services:
BASIC_AUTH_PASSWORD: "${BASIC_AUTH_PASSWORD:-}"
AI_FALLBACK_ENABLED: "${AI_FALLBACK_ENABLED:-false}"
OLLAMA_BASE_URL: "${OLLAMA_BASE_URL:-http://ollama:11434}"
+ OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_MODEL: "${OLLAMA_MODEL:-}"
OLLAMA_TIMEOUT: "${OLLAMA_TIMEOUT:-10m}"
OLLAMA_MAX_CONCURRENT: "${OLLAMA_MAX_CONCURRENT:-1}"
diff --git a/services/knowledge/internal/aifallback/ollama.go b/services/knowledge/internal/aifallback/ollama.go
index 7a0c673..b8e8806 100644
--- a/services/knowledge/internal/aifallback/ollama.go
+++ b/services/knowledge/internal/aifallback/ollama.go
@@ -18,6 +18,7 @@ import (
type Config struct {
BaseURL string
Model string
+ APIKey string
Timeout time.Duration
MaxConcurrent int
AutoReply bool
@@ -148,6 +149,9 @@ func (s *Service) askOllama(ctx context.Context, query string) (staging.Draft, e
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
+ if token := strings.TrimSpace(s.cfg.APIKey); token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
resp, err := s.client.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(ctx.Err(), context.DeadlineExceeded) {
diff --git a/services/knowledge/internal/aifallback/ollama_test.go b/services/knowledge/internal/aifallback/ollama_test.go
index 593af81..5282cdb 100644
--- a/services/knowledge/internal/aifallback/ollama_test.go
+++ b/services/knowledge/internal/aifallback/ollama_test.go
@@ -50,3 +50,27 @@ func TestGenerateUsesStructuredChatAndStoresResult(t *testing.T) {
t.Fatal(err)
}
}
+
+func TestGenerateSendsOllamaBearerToken(t *testing.T) {
+ const token = "ollama-secret"
+ var auth string
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ auth = r.Header.Get("Authorization")
+ _ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]any{"content": `{"title":"T","text":"X","answer":"A","categories":[],"keywords":[]}`}})
+ }))
+ defer server.Close()
+ st, err := staging.New(t.TempDir())
+ if err != nil {
+ t.Fatal(err)
+ }
+ svc, err := New(Config{BaseURL: server.URL, Model: "test", APIKey: token, Timeout: time.Second, MaxConcurrent: 1, MinScore: 0.78}, st)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := svc.Generate(context.Background(), "secure ollama request"); err != nil {
+ t.Fatal(err)
+ }
+ if auth != "Bearer "+token {
+ t.Fatalf("authorization=%q", auth)
+ }
+}