Compare commits

...
6 Commits
Author SHA1 Message Date
Spark 6972e9ec87 fix: planchers de sécurité pip-audit — brotli, filelock, opentelemetry-api, pypdf #224
CI / security (push) Successful in 2m42s
CI / lint (push) Successful in 3m22s
CI / test (push) Successful in 5m34s
CI / build (push) Successful in 2m48s
CI / e2e (push) In progress
2026-10-25 21:02:57 +00:00
Spark f29a22886a feat: URLs publiques de l'écosystème par configuration — dev/prod sans noms en dur #223
CI / lint (push) Successful in 3m11s
CI / security (push) Failing after 1m55s
CI / test (push) Successful in 5m23s
CI / build (push) Successful in 2m35s
CI / e2e (push) Successful in 18m24s
2026-10-25 18:10:59 +00:00
Spark 287afa1537 fix: CSP — frame-src autorise la console du service d'agents (mode console du panneau Spark) BUG-119
CI / lint (push) Successful in 3m5s
CI / security (push) Failing after 1m49s
CI / test (push) Successful in 5m19s
CI / build (push) Successful in 1m36s
CI / e2e (push) Successful in 18m9s
2026-10-24 03:10:34 +00:00
Spark b0ac6dfdb9 docs: #222 livré (2.77.0) — statut Roadmap
CI / lint (push) Successful in 2m58s
CI / security (push) Failing after 1m46s
CI / test (push) Successful in 5m10s
CI / build (push) Successful in 1m39s
CI / e2e (push) Successful in 18m2s
2026-10-23 04:13:51 +00:00
Spark 7f06606300 feat: panneau Spark V3 — la console en surimpression pleine page #222 2026-10-23 04:13:37 +00:00
Spark 985af3a294 docs: #221 livré (2.76.1) — statut Roadmap
CI / lint (push) Successful in 2m54s
CI / security (push) Failing after 1m40s
CI / test (push) Successful in 4m59s
CI / build (push) Failing after 11m14s
CI / e2e (push) Skipped
2026-10-22 21:21:27 +00:00
27 changed files with 1074 additions and 59 deletions
+25
View File
@@ -104,3 +104,28 @@ DEEPSEEK_MODEL=deepseek-chat
# OBSIGATE_GITEA_URL=https://git.example.net
# OBSIGATE_GITEA_TOKEN=
# OBSIGATE_GITHUB_TOKEN=
# ── Fusion / dev-prod — URL publiques de l'écosystème (#223) ──
# Aucun nom public n'est figé dans le code : ces variables portent les
# URL des autres applications. Valeurs par défaut (si absentes) = noms
# OFFICIELS de la production. L'environnement dev déclare ses noms en
# -dev dans son propre .env uniquement, par exemple :
# DRACO_TIKTAK_URL=https://tiktak-dev.dracodev.net
# DRACO_AGENT_URL=https://agent.dracodev.net
# DRACO_OBSIGATE_URL=https://og.dracodev.net
# DRACO_FLOWDECK_URL=https://flowdeck.dracodev.net
# DRACO_TIKTAK_URL=https://tiktak.dracodev.net
# DRACO_KANBANTE_URL=https://kanbante.dracodev.net
# DRACO_GITEA_URL=https://git.dracodev.net
# ── Fusion — émission d'événements vers le service d'agents (#220) ──
# URL complète de réception ; à défaut, dérivée de DRACO_AGENT_URL.
# AGENT_EVENEMENTS_URL=https://agent.dracodev.net/api/v1/evenements
# AGENT_EVENEMENTS_CLE=<clé propre à ObsiGate, jamais commitée>
# AGENT_EVENEMENTS_DOMAINES={"NomDuVault": "famille"}
# ── Fusion — émettrice SSO draco_session (#216/#217) ──
# DRACO_SESSION_SECRET=<secret partagé, jamais commité>
# DRACO_SESSION_SECRET_PREV=<ancien secret, fenêtre de rotation>
# DRACO_SESSION_COOKIE_DOMAIN=.dracodev.net
# DRACO_SSO_MACHINE_KEY=<clé machine des révocations, jamais commitée>
+7 -1
View File
@@ -180,7 +180,13 @@ jobs:
# PYSEC-2026-3910 / PYSEC-2026-3911 (pypdf, DoS de ressources sur
# l'extraction de texte et la lecture d'outlines — donc atteignables
# via backend/pdf_reader.py) sont corrigés par le plancher
# pypdf>=6.16.1 (BUG-093).
# pypdf>=6.16.1 (BUG-093), relevé à pypdf>=6.21.2 par #224 (11 CVE
# sur la 6.19.0 de la toolcache, corrigées au plus tard en 6.21.2).
# #224 ajoute les planchers brotli>=1.2.1 (PYSEC-2026-4322,
# GHSA-9v8h-qqgm-4m35), filelock>=4.2.1 (CVE-2026-112940,
# CVE-2026-112913, CVE-2026-112907) et opentelemetry-api>=1.45.1
# (CVE-2026-114603) : paquets transitifs ou préinstallés dans la
# toolcache, audités mais jamais alignés sans plancher explicite.
# CVE-2026-97687 / CVE-2026-97688 / CVE-2026-97689 (urllib3 2.7.0)
# corrigés par le plancher urllib3>=2.8.0.
# CVE-2026-104874 (multidict 6.7.x) corrigé par le plancher
+80 -1
View File
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
> **En cours de développement** : les changements à venir sont listés dans la section
> [Unreleased](#unreleased). La dernière version livrée est **2.76.1**.
> [Unreleased](#unreleased). La dernière version livrée est **2.78.1**.
---
@@ -14,6 +14,85 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.78.1] — 2026-10-25
### Corrigé
- **Sécurité des dépendances — `pip-audit` rouge sur 2.78.0 (#224)** —
le job `security` du run CI 2303 signalait 17 vulnérabilités dans
4 paquets hérités de l'environnement du runner (versions
préinstallées dans la toolcache, jamais alignées car « already
satisfied » — même piège que BUG-093). Planchers explicites dans
`backend/requirements.txt` : `pypdf>=6.21.2` (11 CVE sur la 6.19.0,
dont CVE-2026-113786 et CVE-2026-115572/115585/115580),
`brotli>=1.2.1` (PYSEC-2026-4322, GHSA-9v8h-qqgm-4m35),
`filelock>=4.2.1` (CVE-2026-112940, CVE-2026-112913,
CVE-2026-112907) et `opentelemetry-api>=1.45.1`
(CVE-2026-114603). Garde-fou `TestDependencySecurityFloors`
étendu ; aucun code applicatif touché, aucun nouvel ignore
`pip-audit`. Redéploiement Docker requis.
---
## [2.78.0] — 2026-10-25
### Modifié
- **Dev/prod sans noms publics en dur (#223, Fusion)** — toutes les URL
publiques de l'écosystème dracodev viennent désormais de la
configuration (`backend/draco_config.py` : `DRACO_AGENT_URL`,
`DRACO_OBSIGATE_URL`, `DRACO_FLOWDECK_URL`, `DRACO_TIKTAK_URL`,
`DRACO_KANBANTE_URL`, `DRACO_GITEA_URL` ; défauts = noms officiels,
l'environnement dev déclare ses noms en `-dev` dans son seul `.env`).
La CSP (`connect-src`/`frame-src`) dérive l'origine de l'agent de la
config (à défaut, de l'origine d'`AGENT_EVENEMENTS_URL`) ; le backend
injecte `window.DRACO_URLS` dans `index.html` pour le panneau Spark,
la barre d'apps et les liens inter-apps ; les références croisées
reconnaissent les hôtes officiels et configurés et leur URL
d'affichage est reconstruite depuis la config à la lecture (aucune
donnée stockée migrée). WebAuthn confirmé dérivé de la requête /
`OBSIGATE_WEBAUTHN_*` (aucun changement). Redéploiement Docker
requis ; en dev, déclarer les noms en `-dev` dans le `.env` du poste.
---
## [2.77.1] — 2026-10-24
### Corrigé
- **Panneau Spark : « mode console » bloqué par la CSP (BUG-119, Fusion)** —
la surimpression du panneau Spark V3 (#222) charge la console du service
d'agents (`https://agent.dracodev.net`) dans une iframe, mais la CSP servie
limitait `frame-src` à `'self' blob:` : le navigateur refusait le
chargement (« This content is blocked »). `frame-src` autorise désormais
aussi `https://agent.dracodev.net` — déjà présente dans `connect-src`
pour le panneau texte ; aucune autre directive n'est modifiée. Test de
non-régression dans `tests/test_security_headers.py`. Redéploiement
Docker requis pour l'activer côté ObsiGate.
---
## [2.77.0] — 2026-10-23
### Modifié
- **Panneau Spark V3 : la console en surimpression pleine page
(#222, Fusion)** — le bouton « Console ⤢ » du panneau Spark
(asset vendangé, octet-identique dans les 4 apps) n'ouvre plus
la console web du service d'agents en nouvel onglet (#221) :
elle s'ouvre **dans la même page**, en surimpression née à la
largeur du panneau et étendue à toute la fenêtre par
transition, avec le lien profond conservé (`?disc=<id>` et
`?app=obsigate`). Sa barre offre les deux sorties : « Mode
sidebar » (retour à l'agent en barre latérale) et « Fermer
l'agent ». Les styles sont injectés par le panneau lui-même ;
côté service (0.20.0), la console autorise l'encadrement par
les 4 applications. Même livraison dans TikTak 0.60.0,
Flowdeck 7.81.0 et Kanbante 0.29.0. Redéploiement Docker
requis pour l'activer côté ObsiGate.
---
## [2.76.1] — 2026-10-22
### Ajouté
+3 -3
View File
@@ -4,7 +4,7 @@
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
[![Version](https://img.shields.io/badge/Version-2.76.1-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.78.1-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
## 📝 Changelog
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.76.1).
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.78.1).
---
*Projet : ObsiGate | Version : 2.76.1 | Dernière mise à jour : Septembre 2026*
*Projet : ObsiGate | Version : 2.78.1 | Dernière mise à jour : Septembre 2026*
+3 -3
View File
@@ -2,7 +2,7 @@
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
[![Version](https://img.shields.io/badge/Version-2.76.1-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.78.1-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
## 📝 Changelog
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.76.1).
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.78.1).
---
*Project: ObsiGate | Version: 2.76.1 | Last updated: September 2026*
*Project: ObsiGate | Version: 2.78.1 | Last updated: September 2026*
+1 -1
View File
@@ -1 +1 @@
2.76.1
2.78.1
+12 -1
View File
@@ -55,6 +55,9 @@ import httpx
logger = logging.getLogger("obsigate.agent_journal")
SOURCE_APP = "obsigate"
# Défaut officiel, conservé pour référence : depuis #223, ``_url()``
# passe par la config centralisée (``backend.draco_config``) — jamais
# un nom d'environnement particulier.
URL_DEFAUT = "https://agent.dracodev.net/api/v1/evenements"
DELAI_S = 2.0
@@ -72,7 +75,15 @@ def _cle() -> str:
def _url() -> str:
return os.environ.get("AGENT_EVENEMENTS_URL", "").strip() or URL_DEFAUT
# #223 : AGENT_EVENEMENTS_URL d'abord ; à défaut, l'URL du service
# configurée (DRACO_AGENT_URL) complétée du chemin d'événements ;
# à défaut, le défaut officiel.
explicite = os.environ.get("AGENT_EVENEMENTS_URL", "").strip()
if explicite:
return explicite
from backend.draco_config import agent_url
return f"{agent_url()}/api/v1/evenements"
def actif() -> bool:
+131
View File
@@ -0,0 +1,131 @@
"""URLs publiques de l'écosystème dracodev — par configuration (#223).
Règle dev/prod (Bruno, 2026-10-25) : **aucun nom public de
l'écosystème n'est figé dans le code vivant**. Chaque URL publique
vient de l'environnement ; les valeurs par défaut sont les noms
**officiels** (sans suffixe) de la future production. L'environnement
dev déclare ses noms en ``-dev`` dans son seul ``.env`` — le code
n'en connaît aucun.
Variables (voir ``.env.example``) :
- ``DRACO_AGENT_URL`` — base publique du service d'agents ;
à défaut, l'origine est **dérivée** de ``AGENT_EVENEMENTS_URL``
(la configuration réelle de l'émission d'événements, #220), puis
le défaut officiel.
- ``DRACO_OBSIGATE_URL`` — base publique d'ObsiGate (liens profonds).
- ``DRACO_FLOWDECK_URL`` — base publique de Flowdeck.
- ``DRACO_TIKTAK_URL`` — base publique de TikTak.
- ``DRACO_KANBANTE_URL`` — base publique de Kanbante.
- ``DRACO_GITEA_URL`` — base publique de Gitea (les cartes
Kanbante *sont* des issues Gitea, contrat liens-profonds) ;
distincte de ``OBSIGATE_GITEA_URL`` (source connectée de
l'assistant, réglable depuis l'UI, pas une URL d'affichage).
Ce module ne lit l'environnement qu'à l'appel (jamais à l'import) :
les tests et un changement de ``.env`` suivi d'un redémarrage
prennent effet sans état figé.
"""
from __future__ import annotations
import os
from urllib.parse import urlparse
# Défauts officiels (production future) — jamais de nom en ``-dev`` ici.
DEFAUTS: dict[str, str] = {
"agent": "https://agent.dracodev.net",
"obsigate": "https://og.dracodev.net",
"flowdeck": "https://flowdeck.dracodev.net",
"tiktak": "https://tiktak.dracodev.net",
"kanbante": "https://kanbante.dracodev.net",
"gitea": "https://git.dracodev.net",
}
_VARIABLES: dict[str, str] = {
"agent": "DRACO_AGENT_URL",
"obsigate": "DRACO_OBSIGATE_URL",
"flowdeck": "DRACO_FLOWDECK_URL",
"tiktak": "DRACO_TIKTAK_URL",
"kanbante": "DRACO_KANBANTE_URL",
"gitea": "DRACO_GITEA_URL",
}
def _nettoyer(valeur: str) -> str:
return valeur.strip().rstrip("/")
def origine(url: str) -> str:
"""Origine ``schéma://hôte[:port]`` d'une URL (``""`` si illisible)."""
u = urlparse(url or "")
if u.scheme and u.netloc:
return f"{u.scheme}://{u.netloc}"
return ""
def agent_url() -> str:
"""Base publique du service d'agents.
Priorité : ``DRACO_AGENT_URL``, puis l'origine de
``AGENT_EVENEMENTS_URL`` (déjà la configuration réelle du poste),
puis le défaut officiel.
"""
explicite = _nettoyer(os.environ.get(_VARIABLES["agent"], ""))
if explicite:
return explicite
evenements = _nettoyer(os.environ.get("AGENT_EVENEMENTS_URL", ""))
if evenements:
derivee = origine(evenements)
if derivee:
return derivee
return DEFAUTS["agent"]
def agent_origine() -> str:
"""Origine du service d'agents, pour la CSP (``connect-src``/``frame-src``)."""
return origine(agent_url()) or DEFAUTS["agent"]
def app_url(app: str) -> str:
"""Base publique d'une application de l'écosystème (sans ``/`` final)."""
if app == "agent":
return agent_url()
variable = _VARIABLES[app]
return _nettoyer(os.environ.get(variable, "")) or DEFAUTS[app]
def app_hote(app: str) -> str:
"""Nom d'hôte (minuscules) de l'URL publique configurée de *app*."""
return (urlparse(app_url(app)).hostname or "").lower()
def hotes_reconnus() -> dict[str, str]:
"""``{hôte: app}`` pour le parseur de liens profonds.
Contient **toujours** les hôtes officiels (un lien collé depuis la
production reste reconnaissable en dev, et réciproquement) **plus**
l'hôte configuré de chaque application (les noms en ``-dev`` du
poste quand ils sont déclarés dans son ``.env``).
"""
hotes: dict[str, str] = {}
for app, defaut in DEFAUTS.items():
hote = (urlparse(defaut).hostname or "").lower()
if hote:
hotes[hote] = app
for app in DEFAUTS:
hote = app_hote(app)
if hote:
hotes[hote] = app
return hotes
def urls_publiques() -> dict[str, str]:
"""Carte des URL publiques injectée à l'hôte web (``window.DRACO_URLS``).
C'est le contrat d'injection de dracodev-commons : l'application
hôte (ici ObsiGate, qui sert ``index.html``) fournit les URL aux
assets partagés (panneau Spark, barre, liens) — les assets ne
portent que les défauts officiels en repli.
"""
return {app: app_url(app) for app in DEFAUTS}
+76 -10
View File
@@ -24,7 +24,7 @@ import uuid
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, unquote, urlparse
from urllib.parse import parse_qs, quote, unquote, urlparse
logger = logging.getLogger("obsigate.external_refs")
@@ -64,26 +64,87 @@ def _write_refs(username: str, refs: list[dict[str, Any]]) -> None:
# ------------------------------------------------------------ parseur d'URL
def build_deep_link(target_app: str, target_type: str, target_id: str) -> str | None:
"""Construit l'URL profonde d'affichage depuis la **configuration**
courante (#223), ou ``None`` si la forme est inconnue.
Les références stockées portent une URL absolue figée au moment du
collage (voir ``add_ref``) : elle peut nommer l'autre environnement
(un lien collé en prod, lu en dev). L'affichage ne l'utilise donc
jamais telle quelle — il reconstruit l'URL depuis
``(target_app, target_type, target_id)`` et les bases publiques
configurées (``backend.draco_config``), au moment de la lecture.
Aucune donnée stockée n'est migrée ni réécrite.
"""
from backend.draco_config import app_url
if target_app == "tiktak":
if target_type == "task":
return f"{app_url('tiktak')}/?task={quote(target_id, safe='')}"
if target_type == "list":
return f"{app_url('tiktak')}/?list={quote(target_id, safe='')}"
return None
if target_app == "obsigate" and target_type == "note":
return f"{app_url('obsigate')}/#file={quote(target_id, safe=':')}"
if target_app == "flowdeck":
if target_type == "page":
return f"{app_url('flowdeck')}/pages/{quote(target_id, safe='')}"
if target_type == "project":
return f"{app_url('flowdeck')}/collections/{quote(target_id, safe='')}"
return None
if target_app == "kanbante" and target_type == "card":
m = re.fullmatch(r"([^/]+)/([^/#]+)#(\d+)", target_id or "")
if m:
owner, repo, num = m.groups()
return f"{app_url('gitea')}/{owner}/{repo}/issues/{num}"
return None
return None
def _avec_url_affichee(ref: dict[str, Any]) -> dict[str, Any]:
"""Copie de la référence dont ``target_url`` est reconstruite
depuis la config courante (#223) — le stockage n'est pas touché."""
construite = build_deep_link(
str(ref.get("target_app") or ""),
str(ref.get("target_type") or ""),
str(ref.get("target_id") or ""),
)
if construite is None:
return ref
copie = dict(ref)
copie["target_url"] = construite
return copie
def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
"""Reconnaît une URL profonde des applications dracodev.
Formes figées par ``dracodev-commons/contrats/liens-profonds.md``.
Retourne ``(target_app, target_type, target_id, label_par_défaut)``
ou ``None`` si l'URL ne correspond à aucune forme du contrat.
Hôtes reconnus (#223) : les hôtes **officiels** et les hôtes
**configurés** de chaque application (``backend.draco_config``) —
un lien collé depuis l'environnement courant (noms en ``-dev``
déclarés dans son ``.env``) comme depuis l'autre environnement
est donc reconnu ; aucun nom d'environnement n'est figé ici.
"""
from backend.draco_config import hotes_reconnus
try:
u = urlparse((url or "").strip())
except ValueError:
return None
host = (u.hostname or "").lower()
if host == "tiktak.dracodev.net":
app = hotes_reconnus().get(host)
if app == "tiktak":
q = parse_qs(u.query)
if q.get("task"):
return ("tiktak", "task", q["task"][0], f"TikTak · tâche {q['task'][0]}")
if q.get("list"):
return ("tiktak", "list", q["list"][0], f"TikTak · liste {q['list'][0]}")
return None
if host == "og.dracodev.net":
if app == "obsigate":
frag = u.fragment or ""
if frag.startswith("file="):
cible = unquote(frag[5:])
@@ -91,7 +152,7 @@ def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
nom = cible.rsplit("/", 1)[-1]
return ("obsigate", "note", cible, f"ObsiGate · {nom}")
return None
if host == "flowdeck.dracodev.net":
if app == "flowdeck":
m = re.fullmatch(r"/pages/(\d+)", u.path or "")
if m:
return ("flowdeck", "page", m.group(1), f"Flowdeck · page {m.group(1)}")
@@ -99,7 +160,7 @@ def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
if m:
return ("flowdeck", "project", m.group(1), f"Flowdeck · projet {m.group(1)}")
return None
if host == "git.dracodev.net":
if app == "gitea":
m = re.fullmatch(r"/([^/]+)/([^/]+)/issues/(\d+)", u.path or "")
if m:
owner, repo, num = m.groups()
@@ -111,20 +172,25 @@ def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
# ------------------------------------------------------------ CRUD
def list_refs(username: str, vault: str, path: str) -> list[dict[str, Any]]:
"""Références posées sur une note, dans l'ordre de création."""
"""Références posées sur une note, dans l'ordre de création.
``target_url`` de chaque référence retournée est **reconstruite
depuis la config courante** (#223, ``_avec_url_affichee``) : la
valeur stockée, figée au collage, n'est jamais servie telle quelle.
"""
if not username:
return []
return [
r for r in _read_refs(username)
_avec_url_affichee(r) for r in _read_refs(username)
if r.get("owner_vault") == vault and r.get("owner_path") == path
]
def get_ref(username: str, ref_id: str) -> dict[str, Any] | None:
"""Une référence par son identifiant, ou ``None``."""
"""Une référence par son identifiant, ou ``None`` (URL reconstruite, #223)."""
for r in _read_refs(username):
if r.get("id") == ref_id:
return r
return _avec_url_affichee(r)
return None
@@ -169,4 +235,4 @@ def delete_ref(username: str, ref_id: str) -> dict[str, Any] | None:
return None
removed = next(r for r in refs if r.get("id") == ref_id)
_write_refs(username, kept)
return removed
return _avec_url_affichee(removed)
+39 -3
View File
@@ -186,15 +186,21 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
# avec un nonce frais (`backend.csp`) ou servis par 'self'/CDN
# listés s'exécutent. `style-src` garde 'unsafe-inline' (attributs
# `style=` et `el.style` omniprésents — chantier séparé).
# #223 : l'origine du service d'agents n'est pas figée — elle
# est dérivée de la configuration (DRACO_AGENT_URL, sinon
# l'origine d'AGENT_EVENEMENTS_URL, sinon le défaut officiel).
from backend.draco_config import agent_origine
agent = agent_origine()
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
f"script-src 'self' 'nonce-{nonce}' blob: https://cdnjs.cloudflare.com https://unpkg.com https://esm.sh https://cdn.jsdelivr.net https://static.cloudflareinsights.com; "
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com https://cdn.jsdelivr.net https://esm.sh; "
"img-src 'self' data: blob:; "
"connect-src 'self' blob: https://esm.sh https://unpkg.com https://cdnjs.cloudflare.com https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net https://agent.dracodev.net; "
f"connect-src 'self' blob: https://esm.sh https://unpkg.com https://cdnjs.cloudflare.com https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net {agent}; "
"font-src 'self' data: https://fonts.gstatic.com https://esm.sh; "
"worker-src 'self' blob:; "
"frame-src 'self' blob:; "
f"frame-src 'self' blob: {agent}; "
"object-src 'none'; "
"base-uri 'self'; "
"form-action 'self'; "
@@ -829,6 +835,36 @@ def _html_with_nonce(request: Request, name: str) -> str:
request.state.csp_nonce,
)
def _index_html(request: Request) -> str:
"""``index.html`` + injection des URL publiques de l'écosystème (#223).
Contrat d'injection de dracodev-commons : l'hôte (ObsiGate, qui
sert la page) fournit les URL aux assets partagés — panneau Spark,
barre d'apps, liens inter-apps lisent ``window.DRACO_URLS`` et ne
portent que les défauts officiels en repli. Les valeurs viennent
de :mod:`backend.draco_config` (environnement ; défauts officiels,
noms en ``-dev`` déclarés par le seul ``.env`` du poste). Le script
injecté passe par le même mécanisme de nonce CSP que les autres
scripts inline de la page.
"""
import json as _json
from backend.csp import inject_csp_nonce
from backend.draco_config import urls_publiques
html = (FRONTEND_DIR / "index.html").read_text(encoding="utf-8")
script = (
"<script>window.DRACO_URLS = "
+ _json.dumps(urls_publiques()).replace("<", "\\u003c")
+ ";</script>"
)
if "</head>" in html:
html = html.replace("</head>", script + "</head>", 1)
else: # repli défensif : tout en tête de document
html = script + html
return inject_csp_nonce(html, request.state.csp_nonce)
if FRONTEND_DIR.exists():
# ``Cache-Control`` for /static is set by SecurityHeadersMiddleware (no-cache).
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")
@@ -907,5 +943,5 @@ if FRONTEND_DIR.exists():
"""Serve the SPA index.html for all non-API routes."""
index_file = FRONTEND_DIR / "index.html"
if index_file.exists():
return HTMLResponse(content=_html_with_nonce(request, "index.html"), headers={"Cache-Control": "no-cache"})
return HTMLResponse(content=_index_html(request), headers={"Cache-Control": "no-cache"})
raise HTTPException(status_code=404, detail="Frontend not found")
+25 -7
View File
@@ -16,13 +16,16 @@ sortedcontainers>=2.4.0
snowballstemmer>=2.2.0
weasyprint>=70.0
httpx>=0.27.0
# Plancher de sécurité (BUG-093) : 6.16.0 est vulnérable à deux DoS de
# ressources (PYSEC-2026-3910 outlines, PYSEC-2026-3911 XForm, fix 6.16.1),
# atteignables via backend/pdf_reader.py (PDF fournis par l'utilisateur).
# Le plancher doit être >= 6.16.1 : l'image Act du runner embarque 6.16.0
# dans sa toolcache Python, donc un plancher trop bas est « already satisfied »
# et n'est jamais mis à niveau.
pypdf>=6.16.1
# Plancher de sécurité (BUG-093, relevé par #224) : 6.16.0 est vulnérable à
# deux DoS de ressources (PYSEC-2026-3910 outlines, PYSEC-2026-3911 XForm,
# fix 6.16.1), atteignables via backend/pdf_reader.py (PDF fournis par
# l'utilisateur). #224 : la version préinstallée dans la toolcache du
# runner (6.19.0) est frappée par 11 CVE supplémentaires (dont
# CVE-2026-113786, CVE-2026-115572, CVE-2026-115585, CVE-2026-115580),
# corrigées au plus tard en 6.21.2 — le plancher doit être >= 6.21.2 :
# en dessous, pip répond « already satisfied » pour la toolcache et
# n'aligne jamais (même piège que BUG-093).
pypdf>=6.21.2
pyotp>=2.10.0
segno>=1.5.0
webauthn==2.6.0
@@ -55,3 +58,18 @@ multidict>=6.9.1
# transitive (httpx/requests) — sans plancher explicite, pip-audit
# signale la version héritée de l'environnement.
certifi>=2026.10.19
# Planchers de sécurité (#224) : paquets transitifs ou préinstallés dans
# la toolcache du runner, audités par pip-audit (CI, bloquant) mais jamais
# alignés sans plancher explicite — « already satisfied » (même piège que
# BUG-093). Versions relevées par le job security du run sur 2.78.0 :
# brotli 1.2.0 (transitive fonttools[woff] via weasyprint) — PYSEC-2026-4322
# et GHSA-9v8h-qqgm-4m35, corrigées en 1.2.1.
brotli>=1.2.1
# filelock 3.32.3 (transitive de CacheControl, outil pip-audit, et
# préinstallée dans la toolcache) — CVE-2026-112940, CVE-2026-112913 et
# CVE-2026-112907, corrigées en 4.2.1.
filelock>=4.2.1
# opentelemetry-api 1.37.0 (préinstallée dans la toolcache du runner,
# aucune dépendance directe d'ObsiGate) — CVE-2026-114603, corrigée
# en 1.45.1.
opentelemetry-api>=1.45.1
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.76.1"
version = "2.78.1"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.76.1"
version = "2.78.1"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate",
"version": "2.76.1",
"version": "2.78.1",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+2
View File
@@ -225,6 +225,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| *BUG-115* | Éditeur Forge : cases à cocher collées au texte dans l'aperçu, aperçu qui flashe à la frappe, menu « dimensions du tableau » coupé hors écran sur un document court | 🟢 corrigé | P2 | 📱 frontend / éditeur | IA | `frontend/editor-poc.html` (CSS aperçu, `renderPreview`, `showTableForm`, `showLangPicker`) | Ouvrir un `.md` avec `- [ ]`, activer l'aperçu → texte collé au checkbox + flash « Rendering... » ; `/table` sur un document d'une ligne → forme partiellement visible | (1) règle `.task-list-item input[type=checkbox] { margin-right: 6px }` ajoutée au `<style>` de l'aperçu (miroir `style.css:2303`) ; (2) `renderPreview` ne remplace plus le contenu par le spinner (affiché seulement si vide) + flag `_previewDirty` pour rejouer la dernière frappe ; (3) `.inline-form`/`.lang-picker` en `position: fixed` (coordonnées viewport) avec clamp bas/droite comme le menu slash + `stopPropagation` sur les clics qi/slash (le listener document refermait la forme) | Vérifié en Playwright (instance E2E) : marginRight 6 px + `list-style: none`, MutationObserver : aucun « Rendering... » pendant 14 frappes et dernier état rendu, forme table `bottom 193 < vh 900` sur document 1 ligne + insertion OK |
| *BUG-116* | Éditeur Forge : après insertion d'un tableau (ex. 4 lignes), la textarea reste d'une seule ligne au lieu de s'agrandir | 🟢 corrigé | P2 | 📱 frontend / éditeur | IA | `frontend/editor-poc.html` (`insertAtCursor`, `wrapSelection`) | Insérer un tableau via le menu : le contenu contient bien les nouvelles lignes mais la hauteur de la textarea ne change pas | Les mutations programmatiques de `ta.value` ne déclenchent pas l'événement `input` — seul handler qui rappelait `autoHeight()`/`updateLineNumbers()` (les handlers clavier, eux, les appelaient à la main). Ajout des deux appels dans `insertAtCursor` et `wrapSelection`, les deux points de passage de toutes les insertions (tableau, code, mermaid, checklist, gras multi-ligne) | Vérifié en Playwright sur l'instance E2E (qi → Table, 4 lignes, sur doc d'une ligne) : hauteur 51 → 189 px, 8 lignes rendues, gutter aligné | ### TODOs techniques (améliorations / nouvelles tâches)
| *BUG-117* | Drag & drop des fichiers dans la sidebar : le déplacement d'un répertoire à un autre ne fait plus rien, sans message | 🟢 corrigé | P1 | 📱 frontend / sidebar | IA | `frontend/js/dragdrop.js` (`onDrop`) | Glisser un fichier sur un dossier : aucune requête `/api/move`, aucun toast | La détection `isExternalFiles` dans `onDrop` (`items.length > 0`, ajoutée par #89) comptait aussi les formats string du drag interne (`tree:vault/path/type` posés par `setData` au `dragstart`) : tout déplacement partait dans la branche upload où rien ne passe le filtre `kind === 'file'` → drop avalé silencieusement. Corrigé à la racine : `dragState` (interne) prioritaire + items réellement fichiers. Livré avec #201 : menu contextuel Déplacer/Dupliquer réutilisant `promptTargetFolder`, emplacement actuel pré-sélectionné | Vérifié Playwright instance E2E : glisser `note1.md` → `d1` = toast « Moved file to d1 » + chemin déplacé ; 4 tests `TestDuplicateApi` |
| *BUG-119* | Panneau Spark : le « mode console » (surimpression iframe de la console du service d'agents) est bloqué par le navigateur (« This content is blocked ») | 🟢 corrigé | P1 | ⚙️ backend | IA | `backend/main.py` (`SecurityHeadersMiddleware`) | Panneau Spark → « Console ⤢ » : l'iframe reste sur la page d'erreur du navigateur | `frame-src 'self' blob: https://agent.dracodev.net` dans la CSP servie ; test `tests/test_security_headers.py::test_csp_frame_src_allows_agent_console` | Cause : `frame-src` n'autorisait pas l'origine du service d'agents, pourtant déjà présente dans `connect-src` (le panneau texte fonctionnait). Aucune autre directive modifiée. Redéploiement Docker requis |
| # | Titre | Statut | Priorité | Scope | Assigné | Zone (fichier) | Cmd de repro | Correctif / Commit | Notes |
|---|---|---|---|---|---|---|---|---|---|
@@ -357,6 +358,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| 2026-10-21 | BUG-118 | Correction | `frontend/index.html`, `frontend/draco-barre.css`, `tests/test_draco_barre.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-118 — la barre d'apps Fusion était inutilisable en mobile** (retours de Bruno sur captures téléphone). Deux causes : (1) le palier ≤ 700 px de la CSS commune gardait onglets entiers et pastilles domaines — la barre débordait ; la source dracodev-commons passe en **v0.1.2** (spec des barres, § Mobile : marque, onglet de l'app courante, alertes, Spark, avatar ; rail/recherche/domaines/menu masqués au profit des contrôles natifs). (2) Le bouton ☰ de la barre cliquait `sidebar-toggle-btn` (bureau) au lieu de `hamburger-btn` (tiroir mobile) : sur téléphone il ne faisait rien, doublé du vrai hamburger d'ObsiGate juste en dessous. Rail devenu contextuel (`matchMedia` ≤ 768 px, point de bascule d'ObsiGate). Test de non-régression `tests/test_draco_barre.py` (2 tests) | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-10-15 | BUG-117 + #201 | Correction + feature | `frontend/js/dragdrop.js`, `frontend/js/ui.js`, `frontend/js/upload.js`, `frontend/locales/fr.json`, `frontend/locales/en.json`, `backend/services/mutations.py`, `backend/routers/files_write.py`, `backend/schemas.py`, `tests/test_api_main.py`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/ISSUES_TODOLIST.md` | **BUG-117 — le drag & drop interne de la sidebar était mort en silence.** `onDrop` classait tout dépôt « externe » dès que `dataTransfer.items` était non vide — or un glisser interne dépose 4 formats string (`tree:*`) : chaque déplacement partait dans la branche upload où le filtre `kind === 'file'` ne trouvait rien — aucun appel API, aucun toast. Détection corrigée (vrais fichiers seulement, `dragState` prioritaire). Livré avec **#201 : menu contextuel Déplacer/Dupliquer** (fichiers et dossiers), modal `promptTargetFolder` réutilisé (+ `opts`, emplacement actuel pré-sélectionné), endpoint `POST /api/duplicate/{vault}` (`duplicate_path`, index incrémental, SSE/webhooks, refus inside-self), i18n FR/EN, 4 tests `TestDuplicateApi` + validation Playwright E2E complète | 🟢 corrigé/livré (en attente vérif utilisateur) |
| 2026-10-09 | #194 | Correction (isolation) | `backend/auth/middleware.py`, `backend/services/recent.py`, `backend/services/search.py`, `backend/routers/search.py`, `backend/routers/config.py`, `backend/routers/conflicts.py`, `backend/routers/files_read.py`, `backend/routers/history.py`, `backend/push.py`, `backend/user_home.py`, `tests/test_user_home.py`, `tests/test_auth_api.py`, `docs/features/user-home-194.md`, `CHANGELOG.md` | **#194 — l'admin voyait le home de chaque utilisateur** (retour utilisateur) : `vaults: ["*"]` ouvrait **tous** les vaults, donc `home-<user>` compris. Correctif à la racine : `check_vault_access` exige un octroi **explicite** pour tout vault `home-*` (nouveau `is_home_vault()`), et les filtres « * » écrits en dur dans les routes ont été remplacés par cet appel unique — dashboard (`routers/config.py`), conflits, liens retour, favoris, abonnements push (`/api/search/replace` utilisait déjà `is_vault_allowed`). `/api/search` filtre désormais les bruts **avant** pagination via `search_vaults(is_allowed=…)` (sinon notes personnelles d'un autre compte dans les résultats). `_grant` n'écarte plus les comptes « * » → l'admin reçoit son propre `home-admin` (auto-réparé au démarrage). Vérifié : `test_user_home.py` 12→**14/14** (dont « * » sans homes + filtrage recherche), `test_auth_api.py` 2/2 (admin ne voit **plus** `home-alice`, alice voit le sien), suite **1705 passed / 2 skipped**, ruff backend 0, mypy 114 fichiers 0, **live Playwright instance 2020** : arborescence admin = `home-admin, TestDir, TestVault` (plus de `home-bruno`), `svg.lucide-home` rendu. Reste ouvert (fuite pré-existante, hors périmètre) : `/api/search/advanced`, `/api/tags`, `/api/tags/suggest`, `/api/tree-search`, `/api/suggest` ne filtrent pas par vault accessible | ✅ livré (en attente vérif utilisateur) |
| 2026-10-23 | BUG-119 | Correction | `backend/main.py`, `tests/test_security_headers.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md`, `docs/ROADMAP.md` | **BUG-119 — le « mode console » du panneau Spark V3 (#222) était bloqué par le navigateur** (« This content is blocked ») : la console du service d'agents s'affiche dans une iframe `https://agent.dracodev.net`, mais la CSP servie par `SecurityHeadersMiddleware` limitait `frame-src` à `'self' blob:` — seul le panneau texte passait (`connect-src` autorisait déjà l'origine). Correctif strict : `frame-src 'self' blob: https://agent.dracodev.net`, toutes les autres directives inchangées à l'identique. Test de non-régression `test_csp_frame_src_allows_agent_console` (la CSP servie est analysée et la liste exacte des sources `frame-src` verrouillée). Vérifié : 100 passed (security_headers + csp_nonce + security_hardening + draco_barre + version), ruff propre sur les fichiers touchés (51 erreurs préexistantes ailleurs, inchangées), mypy 0 (120 fichiers) | 🟢 corrigé (en attente vérif utilisateur) |
---
+64 -3
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.76.1 | **Dernière mise à jour :** 2026-10-22
> **Version :** 2.78.1 | **Dernière mise à jour :** 2026-10-25
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
> vers les fonctionnalités livrées.
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
@@ -30,9 +30,69 @@
## 🔵 En cours
### 224. Sécurité des dépendances — pip-audit vierge (brotli, filelock, opentelemetry-api, pypdf)
- **Effort :** ~0,1 jour | **Impact :** 🔴 | **Ouvert :** 2026-10-25 | **Statut :** ✅ livré le 2026-10-25 (version du hook — voir CHANGELOG ; redéploiement Docker requis)
- **Fiche :** [features/securite-dependances-pip-audit-224.md](./features/securite-dependances-pip-audit-224.md)
- **Description :** le job `security` du run CI 2303 (2.78.0) échoue
sur `pip-audit` : 17 vulnérabilités dans 4 paquets hérités de
l'environnement du runner (toolcache « already satisfied », même
piège que BUG-093). Planchers explicites dans
`backend/requirements.txt` : pypdf relevé à `>=6.21.2`, nouveaux
planchers `brotli>=1.2.1`, `filelock>=4.2.1`,
`opentelemetry-api>=1.45.1` ; garde-fou `TestDependencySecurityFloors`
étendu. Aucun code applicatif, aucun nouvel ignore pip-audit.
- **Sous-tâches :**
- [x] Planchers de sécurité dans `backend/requirements.txt` + commentaires
- [x] Garde-fou `tests/test_ci_workflow.py::TestDependencySecurityFloors` étendu (pypdf 6.21.2, brotli, filelock, opentelemetry-api)
- [x] `pip-audit` local : 0 vulnérabilité non ignorée (mêmes ignores que la CI)
- [x] Release publiée — CI verte à constater, redéploiement Docker (geste de Bruno)
### 223. Dev/prod sans noms publics en dur — URLs de l'écosystème par configuration
- **Effort :** ~0,5 jour | **Impact :** 🟡 | **Ouvert :** 2026-10-25 | **Statut :** ✅ livré le 2026-10-25 (version du hook — voir CHANGELOG ; redéploiement Docker requis, et en dev déclarer les noms en `-dev` dans le `.env` du poste)
- **Fiche :** [features/devprod-urls-config-223.md](./features/devprod-urls-config-223.md)
- **Description :** aucun nom public de l'écosystème dracodev ne doit être
figé dans le code vivant : les URL publiques (service d'agents, autres
apps) viennent de la configuration (défauts = noms **officiels** sans
suffixe ; l'environnement dev déclare ses noms en `-dev` dans son
`.env`). CSP dérivée de l'origine de l'agent configurée, panneau Spark
et liens inter-apps alimentés par l'hôte (injection `window.DRACO_URLS`
au service de `index.html`), références croisées reconstruites depuis
la config à l'affichage (aucune donnée migrée).
- **Sous-tâches :**
- [x] Module `backend/draco_config.py` + `.env.example`
- [x] CSP `connect-src`/`frame-src` dérivée de la config (tests via config)
- [x] `external_refs` : hôtes reconnus + URL d'affichage construites par config
- [x] Frontend : panneau Spark, liens TikTak, onglets des apps via `DRACO_URLS`
- [x] WebAuthn : confirmation rp_id/origines dérivés de la requête / `OBSIGATE_WEBAUTHN_*`
### 222. Fusion — panneau Spark V3 : la console en surimpression pleine page
- **Effort :** ~0,1 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-23 | **Statut :** ✅ livré le 2026-10-23 (2.77.0, redéploiement Docker en attente)
- **Fiche :** [features/fusion-panneau-console-surimpression-222.md](./features/fusion-panneau-console-surimpression-222.md)
- **Description :** le panneau Spark (asset vendangé `frontend/draco-panneau.js`,
octet-identique dans les 4 apps) passe en V3 (demande de Bruno,
2026-10-22) : le bouton « Console ⤢ » n'ouvre plus la console du
service d'agents en nouvel onglet (#221) mais en **surimpression
pleine page dans la même page** — montée à la largeur du panneau,
étendue à toute la fenêtre par transition, iframe gardant le lien
profond (`?disc=` / `?app=`), et deux sorties dans sa barre :
« Mode sidebar » (rétraction vers le panneau) et « Fermer l'agent ».
Styles injectés par le panneau ; côté service, la console autorise
l'encadrement par les 4 apps (0.20.0). Même livraison : TikTak
0.60.0, Flowdeck 7.81.0, Kanbante 0.29.0.
- **Sous-tâches :**
- [x] Surimpression + barre « Mode sidebar » / « Fermer l'agent »
dans le panneau canonique (TikTak), recopié octet-identique
dans les 4 apps
- [x] Test `tests/test_draco_barre.py::test_panneau_console_surimpression_222`
(et révision du test #221 : le nouvel onglet disparaît)
- [x] Release v2.77.0 publiée — CI à constater, redéploiement Docker (geste de Bruno)
### 221. Fusion — panneau Spark : bouton « Console ↗ » vers la console du service
- **Effort :** ~0,1 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-22 | **Statut :** 🔵 en cours
- **Effort :** ~0,1 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-22 | **Statut :** ✅ livré le 2026-10-22 (2.76.1, redéploiement Docker en attente)
- **Fiche :** [features/fusion-panneau-console-221.md](./features/fusion-panneau-console-221.md)
- **Description :** le panneau Spark (asset vendangé `frontend/draco-panneau.js`,
octet-identique dans les 4 apps) gagne un bouton « Console ↗ » dans sa barre
@@ -47,7 +107,7 @@
- [x] Bouton + `ouvrirConsole()` / `appHote()` dans le panneau canonique
(TikTak), recopié octet-identique dans les 4 apps
- [x] Test `tests/test_draco_barre.py::test_panneau_bouton_console_221`
- [ ] CI verte + redéploiement Docker (geste de Bruno)
- [x] Release v2.76.1 publiée — CI à constater, redéploiement Docker (geste de Bruno)
### 220. Fusion — émission d'événements vers le journal du service d'agents (Lot 2, vague 5)
@@ -1088,6 +1148,7 @@
| 166 | Assistant IA — détection & fusion de doublons (score déterministe, paires candidates, fusion backup + confirmation) | Unreleased | [features/agent-phase4-166-168-170.md](./features/agent-phase4-166-168-170.md) |
| 168 | Assistant IA — notifications externes Discord / Telegram / SMTP / webhook (déclencheurs, secrets hors config, SSRF-safe) | Unreleased | [features/agent-phase4-166-168-170.md](./features/agent-phase4-166-168-170.md) |
| 170 | Assistant IA — tâches planifiées type cron (create/append/notify, interval/daily/once, tick 60 s, échec notifié) | Unreleased | [features/agent-phase4-166-168-170.md](./features/agent-phase4-166-168-170.md) |
| BUG-119 | Panneau Spark — « mode console » bloqué par la CSP (`frame-src` sans l'origine du service d'agents) | 2.77.1 | [ISSUES_TODOLIST.md](./ISSUES_TODOLIST.md) |
---
+81
View File
@@ -0,0 +1,81 @@
# #223 — Dev/prod sans noms publics en dur : URLs de l'écosystème par configuration
> Ouvert le 2026-10-25 (chantier dev/prod de Bruno). Contexte : l'installation
> actuelle (`deepin.lab.home`) devient l'environnement **dev** (noms en `-dev`),
> la future installation (`jump.point.mint.home`) sera la **prod** (noms
> officiels sans suffixe), identique sauf noms et clés. Règle : aucun nom
> public de l'écosystème codé en dur dans le code vivant ; défauts = noms
> officiels ; le dev déclare ses `-dev` dans son seul `.env`.
## Configuration (nouveau module `backend/draco_config.py`)
Lecture à l'appel, jamais à l'import. Défauts officiels :
| Variable | Défaut officiel | Rôle |
|---|---|---|
| `DRACO_AGENT_URL` | `https://agent.dracodev.net` | Base du service d'agents. À défaut : origine dérivée d'`AGENT_EVENEMENTS_URL`, puis défaut. |
| `DRACO_OBSIGATE_URL` | `https://og.dracodev.net` | Liens profonds ObsiGate |
| `DRACO_FLOWDECK_URL` | `https://flowdeck.dracodev.net` | Liens profonds Flowdeck |
| `DRACO_TIKTAK_URL` | `https://tiktak.dracodev.net` | Liens profonds TikTak |
| `DRACO_KANBANTE_URL` | `https://kanbante.dracodev.net` | Onglet Kanbante de la barre |
| `DRACO_GITEA_URL` | `https://git.dracodev.net` | Cartes Kanbante (= issues Gitea). Distincte d'`OBSIGATE_GITEA_URL` (source connectée de l'assistant). |
`AGENT_EVENEMENTS_URL` (existant, #220) reste la configuration première du
journal d'événements ; `agent_journal._url()` retombe sur
`DRACO_AGENT_URL + /api/v1/evenements` quand elle est absente.
## Backend
- **CSP** (`backend/main.py`, `SecurityHeadersMiddleware`) : `connect-src` et
`frame-src` utilisent `draco_config.agent_origine()` — dérivée de la config
à chaque réponse, plus aucun littéral. Tests dans
`tests/test_security_headers.py` **via la config** (défaut officiel neutralisé,
dérivation depuis `AGENT_EVENEMENTS_URL`, priorité `DRACO_AGENT_URL`).
- **Références croisées** (`backend/external_refs.py`) :
- `parse_deep_link` reconnaît les hôtes officiels **et** configurés
(`draco_config.hotes_reconnus()`) — un lien collé depuis le dev comme
depuis la prod est reconnu.
- `build_deep_link(app, type, id)` construit l'URL d'affichage depuis la
config ; `list_refs` / `get_ref` / `delete_ref` servent des copies dont
`target_url` est **reconstruite à la lecture**. ⚠️ Les références
**stockées** (`data/external_refs/*.json`) gardent l'URL absolue collée
par l'utilisateur : aucune migration, la valeur stockée n'est simplement
plus jamais servie telle quelle.
- **WebAuthn — confirmé, aucun changement** : `resolve_relying_party()`
(`backend/auth/webauthn_mfa.py`, BUG-070) dérive `rp_id`/origines de la
requête (hôte exact, `X-Forwarded-Host`/`X-Forwarded-Proto` honorés si
`OBSIGATE_TRUST_PROXY=true`), et `OBSIGATE_WEBAUTHN_RP_ID` /
`OBSIGATE_WEBAUTHN_ORIGINS` priment quand ils sont posés. Aucun nom figé.
## Frontend (contrat d'injection de dracodev-commons)
L'hôte fournit les URL : `backend/main.py::_index_html()` injecte
`window.DRACO_URLS = {…}` (JSON issu de `draco_config.urls_publiques()`,
nonce CSP par le mécanisme existant) dans `index.html` au service.
- `frontend/draco-panneau.js` : `API = window.DRACO_URLS.agent || défaut officiel` ;
`appHote()` retire un suffixe `-dev` éventuel du premier label d'hôte
(`og-dev` → `obsigate` pour le lien profond `?app=`).
- `frontend/draco-liens.js` : « Ouvrir dans TikTak » =
`window.DRACO_URLS.tiktak || défaut officiel`.
- `frontend/index.html` : les `href` des onglets portent les défauts officiels ;
le script de configuration de la barre les réécrit depuis `DRACO_URLS`.
Le menu des apps (`draco-barre.js`) est construit depuis ces onglets : il
suit donc sans changement.
- Chemin réellement servi : `/static/draco-*.js` via le montage `StaticFiles`,
`Cache-Control: no-cache` posé par `SecurityHeadersMiddleware` sur
`/static/*`, et service worker **network-first** pour le code — aucun
cache-busting par empreinte n'existe (revalidation systématique).
## Restes assumés
- Suffixe SSO `dracodev.net` (`draco_session.validate_return_to`,
`frontend/js/auth.js`) : ce n'est pas un nom d'hôte mais le domaine parent
commun aux deux environnements (les noms `-dev` restent des sous-domaines) ;
le domaine du cookie est déjà configurable (`DRACO_SESSION_COOKIE_DOMAIN`).
- Mentions textuelles (titres/messages du panneau, contact OpenAPI et lien
« À propos » vers le dépôt Git) : infra Git hors périmètre, ou libellés.
- `OBSIGATE_SEARXNG_URL` : déjà configurable, défaut officiel conservé.
- Le panneau reste un asset partagé : la même ligne d'injection doit être
recopiée dans les 3 autres apps par leurs chantiers respectifs pour
retrouver l'octet-identique.
@@ -0,0 +1,52 @@
# #222 — Fusion : panneau Spark V3, la console en surimpression pleine page
**Contexte.** Le bouton « Console ↗ » du panneau Spark (#221) ouvrait
la console web du service d'agents (https://agent.dracodev.net/)
dans un nouvel onglet : on quittait l'application en cours pour un
autre onglet. Demande de Bruno (2026-10-22) : rester dans la même
page — la console s'ouvre par-dessus l'application, avec une
animation comme celle de la barre latérale mais qui recouvre la
page au complet, et deux sorties explicites : revenir en mode
sidebar, ou fermer l'agent.
**Changement.** Dans `frontend/draco-panneau.js` — asset **vendangé**,
maintenu en canonique dans TikTak et recopié **octet-identique** dans
les 4 applications (TikTak, Flowdeck, Kanbante, ObsiGate) :
- le bouton devient « Console ⤢ » (même emplacement, barre des
discussions) et `ouvrirConsole()` n'ouvre plus d'onglet : il
monte une **surimpression** `.draco-console-sur` ancrée à droite,
née à la largeur du panneau, qui **s'étend à 100 % de la fenêtre**
par transition CSS (double `requestAnimationFrame` pour laisser
peindre la géométrie de départ) ;
- la console est chargée dans une **iframe** dont l'URL reste
construite au premier clic par `urlConsole()` : discussion
affichée (`?disc=<id>`) et app d'origine (`?app=obsigate`, déduite
du sous-domaine `og`) — le lien profond de #221 est conservé ;
- la barre de la surimpression (« ✦ Console Spark ») porte les deux
sorties demandées : **« ◧ Mode sidebar »** (`retrecirConsole()` :
l'overlay se rétracte à la largeur du panneau puis se masque,
l'iframe reste chargée) et **« ✕ Fermer l'agent »** (`fermer()` +
rétraction : panneau et console fermés) ;
- les styles de la surimpression sont **injectés par le panneau
lui-même** (`<style id="draco-console-styles">`, variables du
thème du panneau) : aucun fichier CSS à toucher côté hôte, et la
surimpression suit le thème de l'app comme le panneau.
Côté service d'agents (0.20.0) : la console autorise désormais
l'encadrement par les 4 applications (`frame-ancestors` restreint
aux sous-domaines `dracodev.net` des apps) — sans quoi l'iframe
resterait blanche.
**Vérifications.** Syntaxe JS (`node --check`) ; test
`tests/test_draco_barre.py::test_panneau_console_surimpression_222`
(marqueurs de la surimpression, des deux sorties et de l'animation
dans l'asset) ; le test #221 est révisé en conséquence (le lien
profond `?disc=` / `?app=` subsiste, l'ouverture en nouvel onglet
disparaît) ; somme md5 commune des copies :
`93026ffa14bc8ee896349e3858e12edf`.
**Versions.** ObsiGate 2.77.0 (cette livraison) ; TikTak 0.60.0 et
Flowdeck 7.81.0 déployées automatiquement sur release ; Kanbante
0.29.0 publiée. **Activation côté ObsiGate : redéploiement Docker
par Bruno.**
@@ -0,0 +1,94 @@
# #224 — Sécurité des dépendances : pip-audit vierge (brotli, filelock, opentelemetry-api, pypdf)
> Ouvert le 2026-10-25. Déclencheur : le job `security` du run CI 2303
> (commit `f29a228`, version 2.78.0) échoue sur `pip-audit` (bloquant,
> #87) — **17 vulnérabilités connues dans 4 paquets**. Le job `lint`
> du même run est vert ; `security` est le seul job terminé en échec
> au moment du constat (`test` en file, `build`/`e2e` en attente).
## Constat (journal du job `security`, run 2303)
| Paquet | Version auditée | Vulnérabilités | Corrigée en |
|---|---|---|---|
| brotli | 1.2.0 | PYSEC-2026-4322, GHSA-9v8h-qqgm-4m35 | 1.2.1 |
| filelock | 3.32.3 | CVE-2026-112940, CVE-2026-112913, CVE-2026-112907 | 4.2.1 |
| opentelemetry-api | 1.37.0 | CVE-2026-114603 | 1.45.1 |
| pypdf | 6.19.0 | CVE-2026-113792, CVE-2026-113795, CVE-2026-113793, CVE-2026-113794, CVE-2026-113786, CVE-2026-113791, CVE-2026-113790, CVE-2026-113789, CVE-2026-115572, CVE-2026-115585, CVE-2026-115580 | 6.20.0 → 6.21.2 selon l'advisory (max 6.21.2) |
Total : 2 + 3 + 1 + 11 = **17 vulnérabilités**.
Audit local de confirmation, en deux temps : (1) un venv épinglé sur
les versions du runner (brotli 1.2.0, filelock 3.32.3,
opentelemetry-api 1.37.0, pypdf 6.19.0) reproduit **exactement** les
17 vulnérabilités et les versions corrigées du tableau ci-dessus ;
(2) dans le venv du dépôt, `pip-audit` (avec les deux ignores déjà en
place dans la CI) ne ressortait avant correctif que brotli 1.2.0
(2 advisories, fix 1.2.1) et pypdf 6.21.1 (3 advisories restants,
fix 6.21.2) — ce venv a déjà filelock 4.2.4 (corrigée) et ne contient
pas opentelemetry-api. L'écart avec la CI vient de l'environnement
audité, pas des dépendances déclarées (voir cause).
## Cause racine
Même piège que BUG-093 / BUG-095, déjà documenté dans
`backend/requirements.txt` et `.gitea/workflows/ci.yml` : `pip-audit`
audite **tout l'environnement Python du runner**, dont les paquets
préinstallés dans la toolcache de l'image `catthehacker/ubuntu:act-latest`.
`pip install -r backend/requirements.txt` répond « already satisfied »
pour toute exigence transitive déjà satisfaite par la toolcache et
n'aligne jamais la version vulnérable :
- **pypdf 6.19.0** : préinstallée ; le plancher `>=6.16.1` (BUG-093)
est satisfait, pip ne monte pas vers 6.21.2.
- **brotli 1.2.0** : transitive de `fonttools[woff]` (via weasyprint),
exigée en `>=1.0.1` seulement.
- **filelock 3.32.3** : transitive de CacheControl (outil `pip-audit`
lui-même) et préinstallée ; exigée en `>=3.8.0` seulement.
- **opentelemetry-api 1.37.0** : préinstallée dans la toolcache,
aucune dépendance d'ObsiGate ne la demande.
## Correctif
Planchers de sécurité explicites dans `backend/requirements.txt`,
selon le patron déjà en place (pyjwt, urllib3, multidict, certifi) —
aucune autre dépendance touchée, aucun code applicatif modifié :
- `pypdf>=6.16.1` → **`pypdf>=6.21.2`** (plancher existant relevé) ;
- **`brotli>=1.2.1`** (nouveau plancher) ;
- **`filelock>=4.2.1`** (nouveau plancher) ;
- **`opentelemetry-api>=1.45.1`** (nouveau plancher).
Garde-fou : `tests/test_ci_workflow.py::TestDependencySecurityFloors`
couvre désormais aussi brotli, filelock et opentelemetry-api, et le
plancher verrouillé de pypdf passe à (6, 21, 2). Le commentaire de
l'étape Pip-audit de `.gitea/workflows/ci.yml` est mis à jour en
conséquence (aucune commande CI modifiée).
## Exceptions (ignores)
**Aucun nouvel ignore.** Les deux exceptions déjà en place dans la
CI restent inchangées et ne concernent pas ces paquets :
`PYSEC-2026-1325` (ecdsa, aucun correctif upstream, chemins ECDSA
jamais exécutés — HS256 uniquement) et `CVE-2026-85394`
(python-jose, aucun correctif upstream, non atteignable — voir le
commentaire de l'étape dans `ci.yml`).
## Vérification
- `pip-audit` local (mêmes ignores que la CI) après alignement de
l'environnement sur les nouveaux planchers : **0 vulnérabilité
non ignorée**.
- Portes locales du workflow : `pytest tests/`, `ruff check backend/`,
`mypy backend/ --ignore-missing-imports`, tests frontend
(`validate-imports` + `unit`) — voir le commit #224.
- CI Gitea complète verte exigée (lint, test, security, build, e2e)
avant de considérer #224 livré.
## Livraison
- Commit conventionnel `fix: … #224` ; le hook de version porte
2.78.0 → **2.78.1** (correctif) et crée le tag `v2.78.1`, publié
au push ; release Gitea créée sur ce tag après vérification de sa
cible.
- **Redéploiement Docker requis** pour que l'image embarque les
dépendances alignées (l'hôte actuel est l'environnement dev).
+7 -1
View File
@@ -23,6 +23,12 @@
kanbante: "#0d9488",
};
/* Base TikTak fournie par l'hôte via window.DRACO_URLS (contrat
* d'injection dracodev-commons, #223) ; repli = défaut officiel. */
function baseTikTak() {
return (window.DRACO_URLS && window.DRACO_URLS.tiktak) || "https://tiktak.dracodev.net";
}
function t(key, fallback) {
try {
var v = window.t && window.t(key);
@@ -92,7 +98,7 @@
t("draco.links.projected", "Tâche TikTak projetée")));
var ouvrir = el("a", "btn-action draco-liens-ouvrir",
t("draco.links.openInTikTak", "Ouvrir dans TikTak"));
ouvrir.href = "https://tiktak.dracodev.net/?task=" + tid[1];
ouvrir.href = baseTikTak() + "/?task=" + tid[1];
ouvrir.target = "_blank";
ouvrir.rel = "noopener";
bandeau.appendChild(ouvrir);
+131 -12
View File
@@ -18,15 +18,24 @@
* Panneau v2 (Lot 1) : rendu Markdown, largeur réglable à la souris
* (persistée), thème clair/sombre aligné sur l'app hôte
* (html[data-theme], repli prefers-color-scheme, suivi en direct).
* Bouton « Console ↗ » (barre des discussions) : ouvre la console web
* du même service (agent.dracodev.net) dans un nouvel onglet, sur la
* discussion courante (?disc=) et avec l'app d'origine (?app=),
* déduite du sous-domaine de l'hôte.
* Bouton « Console » (barre des discussions) : ouvre la console web
* du même service (agent.dracodev.net) DANS la page, en surimpression
* pleine page (iframe) qui naît à la géométrie du panneau puis
* s'étend à toute la fenêtre ; l'URL profonde (?disc= discussion
* courante, ?app= app d'origine déduite du sous-domaine) est celle
* du premier chargement — l'iframe est créé paresseusement et
* conservé, la console garde son état entre ouvertures. La barre
* d'outils de la surimpression offre « Mode sidebar » (retour au
* panneau seul, animation inverse) et « Fermer l'agent » (referme
* surimpression et panneau).
* États honnêtes : service injoignable / session non reconnue / LLM non
* configuré / exécuteur non configuré sont affichés tels quels. */
(function () {
"use strict";
var API = "https://agent.dracodev.net";
/* URL du service : fournie par l'hôte via window.DRACO_URLS
* (contrat d'injection dracodev-commons, #223) — le repli est le
* défaut officiel ; aucun nom d'environnement n'est figé ici. */
var API = (window.DRACO_URLS && window.DRACO_URLS.agent) || "https://agent.dracodev.net";
var convId = null;
var convKind = null; // 'principale' | 'parallele'
var convTitre = "";
@@ -48,23 +57,126 @@
catch (e) { return null; }
}
/* ── Console web (même service, nouvel onglet) ─────────────── */
/* ── Console web (même service, surimpression pleine page) ─── */
function appHote() {
var h = (window.location.hostname || "").split(".")[0];
/* Premier label de l'hôte, suffixe d'environnement éventuel
* retiré (ex. « og-dev » → « og » en dev, #223). */
var h = (window.location.hostname || "").split(".")[0].replace(/-dev$/, "");
var correspondance = { tiktak: "tiktak", flowdeck: "flowdeck",
kanbante: "kanbante", og: "obsigate" };
return correspondance[h] || null;
}
function ouvrirConsole() {
function urlConsole() {
var params = [];
var id = convId || convStockee();
if (id) params.push("disc=" + encodeURIComponent(id));
var app = appHote();
if (app) params.push("app=" + encodeURIComponent(app));
window.open(API + "/" + (params.length ? "?" + params.join("&") : ""),
"_blank", "noopener");
return API + "/" + (params.length ? "?" + params.join("&") : "");
}
/* La surimpression vit entièrement dans ce fichier (DOM + styles
* injectés) : les apps hôtes n'ont que ce fichier à recopier. Son
* thème suit celui du panneau (data-draco-theme synchronisé par
* appliquerTheme) et sa palette reprend les mêmes valeurs que
* draco-panneau.css. L'iframe est créé au premier clic puis
* conservé : rouvrir la console ne le recharge pas. */
var surEl = null, surCadre = null, surOuverte = false;
var STYLES_CONSOLE =
".draco-console-sur{" +
"--p-fond:#fbfbfa;--p-surface:#ffffff;--p-bord:#e4e5e0;--p-texte:#242624;" +
"--p-chip:#f4f5f1;--p-chip-bord:#d6d8d1;--p-chip-survol:#e9ebe4;--p-accent:#7c5cd6;" +
"position:fixed;top:0;right:0;height:100dvh;width:388px;max-width:100vw;z-index:1000;" +
"background:var(--p-fond);color:var(--p-texte);" +
"box-shadow:-12px 0 36px rgb(0 0 0 / .22);" +
"display:flex;flex-direction:column;transition:width .3s ease;" +
"font:14px/1.5 system-ui,\"Segoe UI\",sans-serif}" +
".draco-console-sur[data-draco-theme=\"dark\"]{" +
"--p-fond:#17181a;--p-surface:#1f2023;--p-bord:#33353a;--p-texte:#eceeeb;" +
"--p-chip:#26272b;--p-chip-bord:#3a3c41;--p-chip-survol:#2e3036}" +
".draco-console-sur[hidden]{display:none}" +
".draco-console-barre{display:flex;align-items:center;gap:9px;flex:none;" +
"padding:9px 12px;border-bottom:1px solid var(--p-bord);background:var(--p-surface)}" +
".draco-console-barre .dc-titre{font-weight:700;font-size:14px;" +
"display:flex;align-items:center;gap:8px;white-space:nowrap}" +
".draco-console-barre .dc-etoile{color:var(--p-accent);font-size:17px}" +
".draco-console-barre .dc-actions{margin-left:auto;display:flex;gap:7px}" +
".draco-console-barre button{border:1px solid var(--p-chip-bord);" +
"background:var(--p-chip);color:var(--p-texte);border-radius:999px;" +
"padding:5px 12px;font:600 12.5px system-ui;cursor:pointer;white-space:nowrap}" +
".draco-console-barre button:hover{background:var(--p-chip-survol)}" +
".draco-console-cadre{flex:1;width:100%;border:0;display:block;background:var(--p-fond)}";
function assurerSurimpression() {
if (surEl) return;
if (!document.getElementById("draco-console-styles")) {
var st = document.createElement("style");
st.id = "draco-console-styles";
st.textContent = STYLES_CONSOLE;
document.head.appendChild(st);
}
surEl = document.createElement("div");
surEl.className = "draco-console-sur";
surEl.setAttribute("aria-label", "Console Spark");
surEl.hidden = true;
surEl.innerHTML =
'<div class="draco-console-barre">' +
' <span class="dc-titre"><span class="dc-etoile">✦</span> Console Spark</span>' +
' <span class="dc-actions">' +
' <button type="button" data-dc="sidebar" title="Réduire la console et revenir au panneau latéral">◧ Mode sidebar</button>' +
' <button type="button" data-dc="fermer" title="Fermer la console et le panneau Spark">✕ Fermer l\'agent</button>' +
' </span></div>' +
'<iframe class="draco-console-cadre" title="Console Spark" allow="microphone"></iframe>';
document.body.appendChild(surEl);
surCadre = surEl.querySelector("iframe");
surEl.querySelector('[data-dc="sidebar"]')
.addEventListener("click", modeSidebarConsole);
surEl.querySelector('[data-dc="fermer"]')
.addEventListener("click", fermerAgentConsole);
appliquerTheme();
}
function largeurPanneau() {
if (root) {
var w = root.getBoundingClientRect().width;
if (w > 0) return Math.round(w);
}
return 388;
}
function ouvrirConsole() {
assurerSurimpression();
if (!surCadre.getAttribute("src")) surCadre.src = urlConsole();
if (surOuverte) return;
surOuverte = true;
/* Naissance à la géométrie du panneau (ancrée à droite), puis
* extension à toute la page ; le double rAF laisse le temps de
* peindre la largeur de départ, sinon la transition ne joue pas. */
surEl.style.width = largeurPanneau() + "px";
surEl.hidden = false;
requestAnimationFrame(function () {
requestAnimationFrame(function () { surEl.style.width = "100%"; });
});
}
function retrecirConsole() {
if (!surEl || !surOuverte) return;
surOuverte = false;
var fini = false;
function terminer() {
if (fini) return;
fini = true;
surEl.removeEventListener("transitionend", surFin);
surEl.hidden = true; // l'iframe reste chargé, juste masqué
}
function surFin(ev) { if (ev.propertyName === "width") terminer(); }
surEl.addEventListener("transitionend", surFin);
setTimeout(terminer, 380); // filet si transitionend ne vient pas
surEl.style.width = largeurPanneau() + "px";
}
function modeSidebarConsole() { retrecirConsole(); }
function fermerAgentConsole() { fermer(); retrecirConsole(); }
/* ── Thème : suivre l'app hôte ─────────────────────────────── */
function themeHote() {
var t = document.documentElement.getAttribute("data-theme");
@@ -78,6 +190,7 @@
}
function appliquerTheme() {
if (root) root.setAttribute("data-draco-theme", themeHote());
if (surEl) surEl.setAttribute("data-draco-theme", themeHote());
}
function surveillerTheme() {
try {
@@ -143,7 +256,7 @@
' <span class="d-titre">Discussion principale</span>' +
' <button type="button" class="d-btn" data-disc="liste">☰ Discussions</button>' +
' <button type="button" class="d-btn" data-disc="nouvelle">+ Nouvelle</button>' +
' <button type="button" class="d-btn" data-disc="console" title="Ouvrir la console Spark (agent.dracodev.net) dans un nouvel onglet, sur cette discussion">Console ↗</button>' +
' <button type="button" class="d-btn" data-disc="console" title="Ouvrir la console Spark (agent.dracodev.net) en pleine page par-dessus l\'application, sur cette discussion">Console ⤢</button>' +
'</div>' +
'<div class="draco-discussions" hidden></div>' +
'<div class="draco-panneau-outils">' +
@@ -178,7 +291,13 @@
appliquerLargeur();
window.addEventListener("resize", appliquerLargeur);
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape" && root.classList.contains("open")) fermer();
if (ev.key !== "Escape") return;
/* Surimpression ouverte : Échap = retour au panneau seul
* (« Mode sidebar »). Quand le focus est dans l'iframe de la
* console (autre origine), l'événement ne remonte pas
* jusqu'ici : il ne peut donc pas fermer par surprise. */
if (surOuverte) { modeSidebarConsole(); return; }
if (root.classList.contains("open")) fermer();
});
}
+10
View File
@@ -397,6 +397,16 @@
localement ; la recherche focus la recherche native
d'ObsiGate ; Alertes unifiées et Spark arrivent avec la
fédération / l'agent commun (phases suivantes). */
/* #223 : les href des onglets portent les défauts officiels ;
l'hôte les réécrit depuis window.DRACO_URLS (injecté par le
backend depuis sa configuration) quand il est présent. */
(function () {
if (!window.DRACO_URLS) return;
document.querySelectorAll(".draco-apptab").forEach(function (a) {
var u = window.DRACO_URLS[a.dataset.app];
if (u) a.href = u.replace(/\/$/, "") + "/";
});
})();
window.DRACO_BAR_CONFIG = {
current: "obsigate",
domain: (function () { try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; } })(),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.76.1",
"version": "2.78.1",
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
"main": "patch.js",
"directories": {
+7 -1
View File
@@ -251,11 +251,17 @@ class TestDependencySecurityFloors:
#: (paquet, plancher minimal, advisories corrigées au-dessus)
FLOORS = {
"pypdf": (6, 16, 1), # PYSEC-2026-3910, PYSEC-2026-3911 (fix 6.16.1)
# #224 : 6.19.0 (toolcache) frappée par 11 CVE, corrigées au plus
# tard en 6.21.2 ; avant : PYSEC-2026-3910/3911 (fix 6.16.1).
"pypdf": (6, 21, 2),
# BUG-095 : 2.13.0 a son propre advisory (CVE-2026-102274, fix 2.14.0).
"pyjwt": (2, 14, 0), # PYSEC-2026-178 (2.13.0) puis CVE-2026-102274 (2.14.0)
"urllib3": (2, 8, 0), # CVE-2026-97687, CVE-2026-97688, CVE-2026-97689 (fix 2.8.0)
"multidict": (6, 9, 1), # CVE-2026-104874 (fix 6.9.1 ; 6.7.x toolcache runner)
# #224 : transitives / préinstallées toolcache, auditées par pip-audit.
"brotli": (1, 2, 1), # PYSEC-2026-4322, GHSA-9v8h-qqgm-4m35 (fix 1.2.1)
"filelock": (4, 2, 1), # CVE-2026-112940, CVE-2026-112913, CVE-2026-112907 (fix 4.2.1)
"opentelemetry-api": (1, 45, 1), # CVE-2026-114603 (fix 1.45.1)
}
def test_security_floors_are_declared(self):
+49 -8
View File
@@ -60,27 +60,68 @@ def test_panneau_spark_integre_218():
def test_panneau_bouton_console_221():
# #221 : le panneau Spark (asset vendangé, octet-identique dans
# les 4 apps) porte un bouton « Console ↗ » qui ouvre la console
# web du service dans un nouvel onglet, sur la discussion
# courante (?disc=) et avec l'app d'origine (?app= déduite du
# sous-domaine : og → obsigate).
# les 4 apps) porte un bouton « Console » qui mène à la console
# web du service, sur la discussion courante (?disc=) et avec
# l'app d'origine (?app= déduite du sous-domaine : og → obsigate).
# Révisé par #222 : l'ouverture en nouvel onglet a disparu (la
# console s'ouvre en surimpression, voir le test #222) ; le lien
# profond, lui, subsiste via urlConsole().
panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8")
assert 'data-disc="console"' in panneau
assert "Console ↗" in panneau
assert "ouvrirConsole" in panneau
assert "urlConsole" in panneau
assert '"disc=" + encodeURIComponent(id)' in panneau
assert '"app=" + encodeURIComponent(app)' in panneau
assert 'og: "obsigate"' in panneau
assert 'window.open(API + "/"' in panneau
assert '"_blank", "noopener"' in panneau
assert 'window.open(' not in panneau
def test_panneau_console_surimpression_222():
# #222 : le bouton « Console ⤢ » ouvre la console dans la même
# page, en surimpression née à la largeur du panneau et étendue
# à toute la fenêtre (transition sur la largeur), avec deux
# sorties : « Mode sidebar » et « Fermer l'agent ». L'URL de
# l'iframe garde le lien profond de #221 (urlConsole()).
panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8")
assert "Console ⤢" in panneau
assert "draco-console-sur" in panneau
assert "assurerSurimpression" in panneau
assert "draco-console-styles" in panneau
assert 'surCadre.src = urlConsole()' in panneau
assert 'surEl.style.width = "100%"' in panneau
assert 'data-dc="sidebar"' in panneau
assert "Mode sidebar" in panneau
assert "modeSidebarConsole" in panneau
assert 'data-dc="fermer"' in panneau
assert "Fermer l'agent" in panneau
assert "fermerAgentConsole" in panneau
def test_csp_ouvre_le_service_d_agents_218():
# Le panneau parle au service d'agents : la CSP doit nommer son
# hôte en connect-src, sans ouvrir de générique supplémentaire.
# #223 : l'origine n'est plus un littéral dans main.py — elle est
# dérivée de la configuration (backend.draco_config.agent_origine,
# testée via la config dans tests/test_security_headers.py).
main = (Path(__file__).resolve().parent.parent / "backend" / "main.py").read_text(encoding="utf-8")
ligne = next(l for l in main.splitlines() if "connect-src" in l)
assert "https://agent.dracodev.net" in ligne
assert "agent_origine" in main
assert "{agent}" in ligne
def test_urls_ecosysteme_via_injection_hote_223():
# #223 : panneau Spark, liens TikTak et onglets des apps lisent
# window.DRACO_URLS (injecté par l'hôte/backend depuis sa config) ;
# les littéraux restants ne sont que les défauts officiels en repli.
panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8")
assert "window.DRACO_URLS" in panneau
assert "DRACO_URLS.agent" in panneau
liens = (FRONTEND / "draco-liens.js").read_text(encoding="utf-8")
assert "window.DRACO_URLS" in liens
assert "baseTikTak()" in liens
html = _index()
assert "window.DRACO_URLS" in html
assert "DRACO_URLS[a.dataset.app]" in html
def test_rail_mobile_vers_le_vrai_hamburger():
+115
View File
@@ -0,0 +1,115 @@
"""URLs publiques de l'écosystème par configuration — #223 (dev/prod).
Règle : aucun nom public de l'écosystème n'est figé dans le code
vivant. Défauts = noms **officiels** (production future) ; le dev
déclare ses noms en ``-dev`` dans son seul ``.env``
(``DRACO_*_URL`` / ``AGENT_EVENEMENTS_URL``). On vérifie ici la
config centralisée, la dérivation de l'URL d'événements du journal,
la reconnaissance des liens profonds des deux environnements et la
reconstruction des URL d'affichage des références croisées.
"""
import pytest
from backend import draco_config
@pytest.fixture()
def env_propre(monkeypatch):
"""Aucune variable d'URL d'écosystème posée (défauts officiels)."""
for nom in (
"DRACO_AGENT_URL", "DRACO_OBSIGATE_URL", "DRACO_FLOWDECK_URL",
"DRACO_TIKTAK_URL", "DRACO_KANBANTE_URL", "DRACO_GITEA_URL",
"AGENT_EVENEMENTS_URL",
):
monkeypatch.delenv(nom, raising=False)
return monkeypatch
def test_defauts_officiels_sans_dev(env_propre):
assert draco_config.agent_url() == "https://agent.dracodev.net"
assert draco_config.app_url("tiktak") == "https://tiktak.dracodev.net"
assert draco_config.app_url("obsigate") == "https://og.dracodev.net"
assert draco_config.app_url("flowdeck") == "https://flowdeck.dracodev.net"
assert draco_config.app_url("kanbante") == "https://kanbante.dracodev.net"
assert draco_config.app_url("gitea") == "https://git.dracodev.net"
for url in draco_config.urls_publiques().values():
assert "-dev" not in url
def test_agent_derive_des_evenements(env_propre):
"""Sans DRACO_AGENT_URL, l'origine suit AGENT_EVENEMENTS_URL —
la configuration réelle du poste (dev inclus)."""
env_propre.setenv(
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
assert draco_config.agent_url() == "https://agent-dev.dracodev.net"
assert draco_config.agent_origine() == "https://agent-dev.dracodev.net"
def test_agent_explicite_prime(env_propre):
env_propre.setenv(
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
env_propre.setenv("DRACO_AGENT_URL", "https://agent.example.net/")
assert draco_config.agent_url() == "https://agent.example.net"
def test_journal_url_suit_la_config(env_propre):
from backend import agent_journal
assert agent_journal._url() == "https://agent.dracodev.net/api/v1/evenements"
env_propre.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
assert agent_journal._url() == "https://agent-dev.dracodev.net/api/v1/evenements"
env_propre.setenv(
"AGENT_EVENEMENTS_URL", "https://autre.example.net/api/v1/evenements")
assert agent_journal._url() == "https://autre.example.net/api/v1/evenements"
# ------------------------------------------------------------ liens profonds
def test_parse_hotes_configures(env_propre):
"""Un lien collé depuis l'environnement dev (hôtes déclarés dans
son .env) est reconnu, comme un lien officiel."""
from backend.external_refs import parse_deep_link
env_propre.setenv("DRACO_TIKTAK_URL", "https://tiktak-dev.dracodev.net")
env_propre.setenv("DRACO_OBSIGATE_URL", "https://og-dev.dracodev.net")
assert parse_deep_link("https://tiktak-dev.dracodev.net/?task=5") == (
"tiktak", "task", "5", "TikTak · tâche 5")
assert parse_deep_link("https://og-dev.dracodev.net/#file=v:Dossier/Note.md") == (
"obsigate", "note", "v:Dossier/Note.md", "ObsiGate · Note.md")
# L'officiel reste reconnu en parallèle (lien venu de la prod).
assert parse_deep_link("https://tiktak.dracodev.net/?task=5") is not None
def test_url_affichage_reconstruite_depuis_config(env_propre, tmp_path):
"""#223 : la référence stockée garde son URL absolue d'origine
(aucune migration), mais l'URL **servie** est reconstruite depuis
la config courante."""
from backend.external_refs import add_ref, build_deep_link, list_refs
env_propre.setenv("OBSIGATE_EXTERNAL_REFS_DIR", str(tmp_path / "refs"))
assert build_deep_link("tiktak", "task", "42") == \
"https://tiktak.dracodev.net/?task=42"
assert build_deep_link("kanbante", "card", "Projets/obsigate#7") == \
"https://git.dracodev.net/Projets/obsigate/issues/7"
assert build_deep_link("obsigate", "note", "v:Dossier/Note.md") == \
"https://og.dracodev.net/#file=v:Dossier%2FNote.md"
env_propre.setenv("DRACO_TIKTAK_URL", "https://tiktak-dev.dracodev.net")
ref = add_ref("alice", "Vault", "N.md", "tiktak", "task", "42",
"https://tiktak.dracodev.net/?task=42", "TikTak · tâche 42")
assert ref["target_url"] == "https://tiktak.dracodev.net/?task=42" # stocké tel quel
servies = list_refs("alice", "Vault", "N.md")
assert servies[0]["target_url"] == "https://tiktak-dev.dracodev.net/?task=42"
# ------------------------------------------------------------ injection hôte
def test_index_injecte_draco_urls(client, env_propre):
"""L'hôte sert index.html avec window.DRACO_URLS issu de sa config."""
env_propre.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
resp = client.get("/")
assert resp.status_code == 200
assert "window.DRACO_URLS" in resp.text
assert '"agent": "https://agent-dev.dracodev.net"' in resp.text
assert '"tiktak": "https://tiktak.dracodev.net"' in resp.text
+56
View File
@@ -72,3 +72,59 @@ def test_security_headers_present(client):
csp = resp.headers.get("content-security-policy", "")
assert "object-src 'none'" in csp
assert "frame-ancestors 'self'" in csp
def _csp_directives(csp: str) -> dict[str, list[str]]:
"""Découpe une politique CSP en ``{directive: [sources]}``."""
directives: dict[str, list[str]] = {}
for part in csp.split(";"):
tokens = part.split()
if tokens:
directives[tokens[0]] = tokens[1:]
return directives
def test_csp_frame_src_allows_agent_console(client, monkeypatch):
"""BUG-119 : le « mode console » du panneau Spark charge la console du
service d'agents (``https://agent.dracodev.net``) dans une iframe —
``frame-src`` doit autoriser cette origine, sinon le navigateur bloque
le chargement (« This content is blocked »). Les sources historiques
(``'self'`` et ``blob:``, visualiseur PDF) restent en place et aucune
autre origine n'est admise.
#223 : la configuration est neutralisée ici (défaut officiel) —
l'origine n'est plus figée dans le code, voir le test via config
ci-dessous."""
monkeypatch.delenv("DRACO_AGENT_URL", raising=False)
monkeypatch.delenv("AGENT_EVENEMENTS_URL", raising=False)
resp = client.get("/api/health")
csp = resp.headers.get("content-security-policy", "")
directives = _csp_directives(csp)
assert directives.get("frame-src") == [
"'self'",
"blob:",
"https://agent.dracodev.net",
]
assert "https://agent.dracodev.net" in directives.get("connect-src", [])
def test_csp_agent_origine_via_config(client, monkeypatch):
"""#223 : ``frame-src``/``connect-src`` suivent l'origine de l'agent
**configurée** — dérivée d'``AGENT_EVENEMENTS_URL`` (config réelle
du poste, noms en ``-dev`` en dev), ou de ``DRACO_AGENT_URL`` qui
prime. Aucun nom d'environnement n'est codé en dur."""
monkeypatch.delenv("DRACO_AGENT_URL", raising=False)
monkeypatch.setenv(
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
resp = client.get("/api/health")
directives = _csp_directives(resp.headers.get("content-security-policy", ""))
assert directives.get("frame-src") == [
"'self'", "blob:", "https://agent-dev.dracodev.net"]
assert "https://agent-dev.dracodev.net" in directives.get("connect-src", [])
assert "https://agent.dracodev.net" not in directives.get("connect-src", [])
monkeypatch.setenv("DRACO_AGENT_URL", "https://agent.example.net")
resp = client.get("/api/health")
directives = _csp_directives(resp.headers.get("content-security-policy", ""))
assert directives.get("frame-src") == [
"'self'", "blob:", "https://agent.example.net"]