Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6972e9ec87 | ||
|
|
f29a22886a | ||
|
|
287afa1537 | ||
|
|
b0ac6dfdb9 | ||
|
|
7f06606300 | ||
|
|
985af3a294 |
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](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*
|
||||
|
||||
@@ -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.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](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*
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "obsigate-desktop"
|
||||
version = "2.76.1"
|
||||
version = "2.78.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"env_logger",
|
||||
|
||||
+1
-1
@@ -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,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",
|
||||
|
||||
@@ -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
@@ -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) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user