mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 14:08:11 +02:00
Phases 5 & 8 : onglet Logs, durcissement Docker et documentation
Phase 5 (fin) : - PreviewPanel : onglet Logs (journal d'activité des outils de la session, compteur dynamique, statuts ✓/✕/⏸/…) Phase 8 : - .dockerignore - Dockerfile : utilisateur non-root (uid 10001), HEALTHCHECK via curl, dossiers de runtime détenus par l'utilisateur - docker-compose : healthcheck, variable SEARX_URL optionnelle - .env.example : SEARX_URL - README : sections Utilisation, Outils de l'agent, Sécurité (confinement, validation run_shell, non-root) ; feuille de route complète Validation : config docker compose OK ; build d'image non exécutable ici (démon Docker absent de l'environnement). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
1 parent
de2befe31e
commit
66be893ea6
6 files changed
+159
-14
No files matched your search
@@ -0,0 +1,26 @@
|
|||||||
|
# Contrôle de version
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
**/__pycache__
|
||||||
|
**/*.pyc
|
||||||
|
backend/.venv
|
||||||
|
backend/venv
|
||||||
|
|
||||||
|
# Node / build
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
**/.vite
|
||||||
|
frontend/tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
# Données & workspace de runtime (montés en volume)
|
||||||
|
data
|
||||||
|
workspace
|
||||||
|
|
||||||
|
# Environnement / IDE
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
**/.DS_Store
|
||||||
@@ -14,3 +14,7 @@ DATA_DIR=/data
|
|||||||
|
|
||||||
# Port exposé par l'application
|
# Port exposé par l'application
|
||||||
PORT=8080
|
PORT=8080
|
||||||
|
|
||||||
|
# Optionnel : URL d'une instance SearxNG pour l'outil web_search.
|
||||||
|
# Si vide, web_search utilise DuckDuckGo (sans clé d'API).
|
||||||
|
SEARX_URL=
|
||||||
+13
-1
@@ -16,6 +16,10 @@ ENV PYTHONUNBUFFERED=1 \
|
|||||||
DATA_DIR=/data \
|
DATA_DIR=/data \
|
||||||
PORT=8080
|
PORT=8080
|
||||||
|
|
||||||
|
# curl pour le HEALTHCHECK
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY backend/requirements.txt ./
|
COPY backend/requirements.txt ./
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
@@ -23,8 +27,16 @@ COPY backend/ ./backend/
|
|||||||
# Frontend compilé servi en statique par FastAPI
|
# Frontend compilé servi en statique par FastAPI
|
||||||
COPY --from=frontend /app/frontend/dist ./backend/static
|
COPY --from=frontend /app/frontend/dist ./backend/static
|
||||||
|
|
||||||
RUN mkdir -p /workspace /data
|
# Utilisateur non-root + dossiers de runtime lui appartenant
|
||||||
|
RUN useradd --create-home --uid 10001 loki \
|
||||||
|
&& mkdir -p /workspace /data \
|
||||||
|
&& chown -R loki:loki /workspace /data /app
|
||||||
|
USER loki
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
WORKDIR /app/backend
|
WORKDIR /app/backend
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -fsS "http://localhost:${PORT}/api/health" || exit 1
|
||||||
|
|
||||||
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT}"]
|
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT}"]
|
||||||
@@ -63,8 +63,44 @@ npm run dev # http://localhost:5173
|
|||||||
| `OLLAMA_HOST` | `http://host.docker.internal:11434` | URL de l'instance Ollama |
|
| `OLLAMA_HOST` | `http://host.docker.internal:11434` | URL de l'instance Ollama |
|
||||||
| `DEFAULT_MODEL` | `llama3.1:8b` | Modèle sélectionné au démarrage |
|
| `DEFAULT_MODEL` | `llama3.1:8b` | Modèle sélectionné au démarrage |
|
||||||
| `WORKSPACE_DIR` | `/workspace` | Dossier de travail de l'agent |
|
| `WORKSPACE_DIR` | `/workspace` | Dossier de travail de l'agent |
|
||||||
| `DATA_DIR` | `/data` | Base SQLite (sessions) |
|
| `DATA_DIR` | `/data` | Base SQLite (sessions + config) |
|
||||||
| `PORT` | `8080` | Port exposé |
|
| `PORT` | `8080` | Port exposé |
|
||||||
|
| `SEARX_URL` | *(vide)* | Instance SearxNG pour `web_search` (sinon DuckDuckGo) |
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
1. Vérifie la pastille **Ollama** (verte = connecté) en haut à droite, et choisis
|
||||||
|
un modèle qui supporte le *function calling* (ex. `llama3.1:8b`,
|
||||||
|
`qwen2.5-coder`).
|
||||||
|
2. Décris une tâche dans le tchat, p. ex. *« Crée une landing page pour un café
|
||||||
|
nommé Café Lumière, avec menu et horaires »*.
|
||||||
|
3. L'agent lit/écrit des fichiers dans le **workspace** ; chaque appel d'outil
|
||||||
|
s'affiche dans le fil, et l'aperçu HTML apparaît à droite (onglets **Aperçu /
|
||||||
|
Code / Logs**).
|
||||||
|
4. Règle le comportement dans **Configuration** (modèle, température/top-p/top-k,
|
||||||
|
jetons max, outils actifs, invite système).
|
||||||
|
|
||||||
|
## Outils de l'agent
|
||||||
|
|
||||||
|
| Outil | Rôle | Par défaut |
|
||||||
|
| ------------- | -------------------------------------- | ---------- |
|
||||||
|
| `read_file` | Lire un fichier du workspace | activé |
|
||||||
|
| `write_file` | Créer / modifier un fichier | activé |
|
||||||
|
| `list_dir` | Lister un répertoire | activé |
|
||||||
|
| `web_search` | Recherche web (DuckDuckGo / SearxNG) | désactivé |
|
||||||
|
| `run_shell` | Exécuter une commande **(sensible)** | désactivé |
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
- **Confinement** : toutes les opérations fichier (`read_file`, `write_file`,
|
||||||
|
`list_dir`) et `run_shell` sont strictement confinées au `WORKSPACE_DIR`. Toute
|
||||||
|
tentative de sortie (`../`, chemin absolu) est rejetée.
|
||||||
|
- **`run_shell`** est désactivé par défaut. Une fois activé, chaque commande
|
||||||
|
proposée par l'agent demande une **validation explicite** dans l'interface
|
||||||
|
(option *confirm_shell*, activée par défaut) avant exécution.
|
||||||
|
- Le conteneur tourne en **utilisateur non-root** et expose un **HEALTHCHECK**.
|
||||||
|
- Loki est conçu pour un usage **local** : n'expose pas le port publiquement sans
|
||||||
|
ajouter ta propre couche d'authentification.
|
||||||
|
|
||||||
## Feuille de route
|
## Feuille de route
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,20 @@ services:
|
|||||||
- WORKSPACE_DIR=/workspace
|
- WORKSPACE_DIR=/workspace
|
||||||
- DATA_DIR=/data
|
- DATA_DIR=/data
|
||||||
- PORT=8080
|
- PORT=8080
|
||||||
|
# Optionnel : instance SearxNG pour web_search (sinon DuckDuckGo)
|
||||||
|
- SEARX_URL=${SEARX_URL:-}
|
||||||
volumes:
|
volumes:
|
||||||
- ./workspace:/workspace
|
- ./workspace:/workspace
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
# Permet d'atteindre un Ollama installé sur la machine hôte
|
# Permet d'atteindre un Ollama installé sur la machine hôte
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://localhost:8080/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# ── Ollama optionnel ──────────────────────────────────────────────────
|
# ── Ollama optionnel ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useStore } from "../store/useStore";
|
import { useStore } from "../store/useStore";
|
||||||
|
import type { ToolCall } from "../api/client";
|
||||||
|
|
||||||
type TabId = "preview" | "code";
|
type TabId = "preview" | "code" | "logs";
|
||||||
|
|
||||||
/** Panneau droit : onglets Aperçu / Code + rendu du fichier sélectionné. */
|
/** Panneau droit : onglets Aperçu / Code / Logs. */
|
||||||
export function PreviewPanel() {
|
export function PreviewPanel() {
|
||||||
const { previewPath, previewContent } = useStore();
|
const { previewPath, previewContent, messages, streamTools } = useStore();
|
||||||
const [tab, setTab] = useState<TabId>("preview");
|
const [tab, setTab] = useState<TabId>("preview");
|
||||||
|
|
||||||
const isHtml = previewPath ? /\.html?$/.test(previewPath) : false;
|
const isHtml = previewPath ? /\.html?$/.test(previewPath) : false;
|
||||||
|
|
||||||
|
// Journal d'activité : tous les appels d'outils de la session + en cours.
|
||||||
|
const logs: ToolCall[] = [
|
||||||
|
...messages.flatMap((m) => m.meta?.tools ?? []),
|
||||||
|
...streamTools,
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-[452px] flex-none flex-col border-l border-line-soft bg-panel">
|
<div className="flex w-[452px] flex-none flex-col border-l border-line-soft bg-panel">
|
||||||
{/* Onglets */}
|
{/* Onglets */}
|
||||||
@@ -21,6 +28,12 @@ export function PreviewPanel() {
|
|||||||
<Tab active={tab === "code"} onClick={() => setTab("code")}>
|
<Tab active={tab === "code"} onClick={() => setTab("code")}>
|
||||||
Code
|
Code
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab active={tab === "logs"} onClick={() => setTab("logs")}>
|
||||||
|
Logs
|
||||||
|
<span className="ml-1.5 rounded-[5px] bg-line-strong px-1.5 py-px font-mono text-[10px] text-muted">
|
||||||
|
{logs.length}
|
||||||
|
</span>
|
||||||
|
</Tab>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,9 +46,54 @@ export function PreviewPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Viewport */}
|
{/* Onglet Logs (fond sombre) */}
|
||||||
<div className="scr mx-3.5 mb-3.5 flex-1 overflow-auto rounded-xl border border-line bg-[#faf6ef]">
|
{tab === "logs" ? (
|
||||||
{!previewPath ? (
|
<div className="scr mx-3.5 mb-3.5 flex-1 overflow-auto rounded-xl border border-line bg-sunken p-3 font-mono text-[11.5px]">
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<div className="py-10 text-center text-muted-3">
|
||||||
|
Aucune activité d'outil pour cette session.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
logs.map((l, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex items-center gap-2 border-b border-line-soft py-[7px] last:border-0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
l.status === "error"
|
||||||
|
? "text-warn"
|
||||||
|
: l.status === "pending"
|
||||||
|
? "text-warn"
|
||||||
|
: l.status === "running"
|
||||||
|
? "text-muted"
|
||||||
|
: "text-ok"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{l.status === "error"
|
||||||
|
? "✕"
|
||||||
|
: l.status === "pending"
|
||||||
|
? "⏸"
|
||||||
|
: l.status === "running"
|
||||||
|
? "…"
|
||||||
|
: "✓"}
|
||||||
|
</span>
|
||||||
|
<span className="text-ink-2">{l.name}</span>
|
||||||
|
<span className="flex-1 truncate text-muted-3">
|
||||||
|
{(l.args?.path as string) ??
|
||||||
|
(l.args?.query as string) ??
|
||||||
|
(l.args?.command as string) ??
|
||||||
|
""}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-3">{l.summary}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Viewport Aperçu / Code (fond clair) */
|
||||||
|
<div className="scr mx-3.5 mb-3.5 flex-1 overflow-auto rounded-xl border border-line bg-[#faf6ef]">
|
||||||
|
{!previewPath ? (
|
||||||
<Empty>
|
<Empty>
|
||||||
L'aperçu s'affichera ici dès que l'agent générera un fichier (clique
|
L'aperçu s'affichera ici dès que l'agent générera un fichier (clique
|
||||||
aussi un fichier à gauche).
|
aussi un fichier à gauche).
|
||||||
@@ -51,12 +109,13 @@ export function PreviewPanel() {
|
|||||||
className="h-full w-full border-0 bg-white"
|
className="h-full w-full border-0 bg-white"
|
||||||
sandbox="allow-same-origin"
|
sandbox="allow-same-origin"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<pre className="m-0 whitespace-pre-wrap p-4 font-mono text-[11.5px] leading-relaxed text-[#2a2018]">
|
<pre className="m-0 whitespace-pre-wrap p-4 font-mono text-[11.5px] leading-relaxed text-[#2a2018]">
|
||||||
{previewContent}
|
{previewContent}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user