Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
070add4e7b | ||
|
|
847b3225b2 | ||
|
|
3ae5c7efac | ||
|
|
4966c27548 | ||
|
|
fddbaec82e | ||
|
|
8e073438a3 | ||
|
|
6972e9ec87 | ||
|
|
f29a22886a | ||
|
|
287afa1537 | ||
|
|
b0ac6dfdb9 |
@@ -104,3 +104,31 @@ DEEPSEEK_MODEL=deepseek-chat
|
|||||||
# OBSIGATE_GITEA_URL=https://git.example.net
|
# OBSIGATE_GITEA_URL=https://git.example.net
|
||||||
# OBSIGATE_GITEA_TOKEN=
|
# OBSIGATE_GITEA_TOKEN=
|
||||||
# OBSIGATE_GITHUB_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. Depuis BUG-120, les URL sœurs se dérivent
|
||||||
|
# de l'hôte de l'agent (agent<SUFFIXE>.dracodev.net, lu via
|
||||||
|
# AGENT_EVENEMENTS_URL / DRACO_AGENT_URL) : en dev, rien d'autre à
|
||||||
|
# déclarer pour obtenir les noms en -dev. Une variable explicite
|
||||||
|
# reste prioritaire sur la dérivation, 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
|
# PYSEC-2026-3910 / PYSEC-2026-3911 (pypdf, DoS de ressources sur
|
||||||
# l'extraction de texte et la lecture d'outlines — donc atteignables
|
# l'extraction de texte et la lecture d'outlines — donc atteignables
|
||||||
# via backend/pdf_reader.py) sont corrigés par le plancher
|
# 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)
|
# CVE-2026-97687 / CVE-2026-97688 / CVE-2026-97689 (urllib3 2.7.0)
|
||||||
# corrigés par le plancher urllib3>=2.8.0.
|
# corrigés par le plancher urllib3>=2.8.0.
|
||||||
# CVE-2026-104874 (multidict 6.7.x) corrigé par le plancher
|
# CVE-2026-104874 (multidict 6.7.x) corrigé par le plancher
|
||||||
|
|||||||
+102
-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).
|
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
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.77.0**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.79.0**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,107 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.79.0] — 2026-10-26
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
|
||||||
|
- **Panneau Spark : version du service d'agents visible en mode
|
||||||
|
sidebar (#225)** — le pied du panneau Spark
|
||||||
|
(`frontend/draco-panneau.js`, asset partagé des 4 applications)
|
||||||
|
affiche la ligne discrète « Service d'agents vX », comme la
|
||||||
|
console pleine page. La version est lue en direct dans la santé
|
||||||
|
du service (`GET /healthz` de l'origine injectée par l'hôte), à
|
||||||
|
l'ouverture du panneau, avec cache de session ; jamais une
|
||||||
|
valeur codée en dur — service injoignable, rien d'affiché.
|
||||||
|
Demande de Bruno (2026-10-26), carte fusion #64 ; même
|
||||||
|
livraison : TikTak 0.61.3, Flowdeck 7.83.1, Kanbante 0.37.1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.78.2] — 2026-10-26
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
|
||||||
|
- **Menu inter-apps : les URL sœurs suivaient l'environnement de
|
||||||
|
l'agent (BUG-120, suite de #223)** — sur la dev
|
||||||
|
(`og-dev.dracodev.net`), le `window.DRACO_URLS` servi donnait
|
||||||
|
l'agent en `-dev` mais les applications sœurs (ObsiGate, Flowdeck,
|
||||||
|
TikTak, Kanbante) en noms **prod** : seule l'URL de l'agent avait
|
||||||
|
une dérivation de repli, et le `.env` dev ne déclare pas les quatre
|
||||||
|
`DRACO_<APP>_URL`. Résolution d'une URL sœur dans
|
||||||
|
`backend/draco_config.py`, inchangée dans son contrat : (1) la
|
||||||
|
variable explicite prime ; (2) à défaut, **dérivation
|
||||||
|
d'environnement** depuis l'hôte de l'agent tel qu'ObsiGate le
|
||||||
|
connaît déjà (`agent_url()`, qui lit `AGENT_EVENEMENTS_URL`) — si
|
||||||
|
cet hôte est de la forme `agent<SUFFIXE>.dracodev.net`, l'URL sœur
|
||||||
|
devient `https://<slug><SUFFIXE>.dracodev.net` (`og`, `flowdeck`,
|
||||||
|
`tiktak`, `kanbante`) ; hôte hors motif (autre domaine, LAN, IP,
|
||||||
|
port) → **aucune dérivation**, jamais de lien deviné ; (3) défaut
|
||||||
|
officiel. L'URL Gitea ne change pas. Aucune variable supplémentaire
|
||||||
|
à déclarer en dev. Tests `tests/test_draco_config.py` étendus
|
||||||
|
(dérivation `-dev`, priorité de l'explicite, hôtes hors motif,
|
||||||
|
non-régression sans env). Redéploiement Docker requis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [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
|
## [2.77.0] — 2026-10-23
|
||||||
|
|
||||||
### Modifié
|
### Modifié
|
||||||
|
|||||||
+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.
|
**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://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.77.0).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.79.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.77.0 | Dernière mise à jour : Septembre 2026*
|
*Projet : ObsiGate | Version : 2.79.0 | 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.
|
**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://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.77.0).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.79.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.77.0 | Last updated: September 2026*
|
*Project: ObsiGate | Version: 2.79.0 | Last updated: September 2026*
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ import httpx
|
|||||||
logger = logging.getLogger("obsigate.agent_journal")
|
logger = logging.getLogger("obsigate.agent_journal")
|
||||||
|
|
||||||
SOURCE_APP = "obsigate"
|
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"
|
URL_DEFAUT = "https://agent.dracodev.net/api/v1/evenements"
|
||||||
DELAI_S = 2.0
|
DELAI_S = 2.0
|
||||||
|
|
||||||
@@ -72,7 +75,15 @@ def _cle() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _url() -> 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:
|
def actif() -> bool:
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
"""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).
|
||||||
|
|
||||||
|
Résolution d'une URL sœur (BUG-120) : la variable explicite prime ;
|
||||||
|
à défaut, l'**environnement est dérivé de l'hôte de l'agent** tel
|
||||||
|
qu'ObsiGate le connaît déjà (:func:`agent_url`) — si cet hôte est de
|
||||||
|
la forme ``agent<SUFFIXE>.dracodev.net`` (``-dev`` en dev, vide en
|
||||||
|
prod), l'URL sœur est ``https://<slug><SUFFIXE>.dracodev.net``. Un
|
||||||
|
hôte hors motif (autre domaine, LAN, IP, port) ne dérive **rien** :
|
||||||
|
on ne fabrique jamais un lien deviné, le défaut officiel s'applique.
|
||||||
|
L'installation pointe donc les bons liens selon son environnement
|
||||||
|
sans déclarer les quatre ``DRACO_<APP>_URL``.
|
||||||
|
|
||||||
|
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
|
||||||
|
import re
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Slugs d'hôte des applications sœurs pour la dérivation
|
||||||
|
# d'environnement (BUG-120) : premier label de ``<slug>.dracodev.net``
|
||||||
|
# (et de ``<slug>-dev.dracodev.net`` en dev). Gitea est **exclue** :
|
||||||
|
# son URL ne se dérive jamais.
|
||||||
|
_SLUGS_HOTE: dict[str, str] = {
|
||||||
|
"obsigate": "og",
|
||||||
|
"flowdeck": "flowdeck",
|
||||||
|
"tiktak": "tiktak",
|
||||||
|
"kanbante": "kanbante",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Premier label de l'hôte de l'agent : ``agent`` suivi d'un suffixe
|
||||||
|
# d'environnement éventuel (``-dev`` en dev, vide en prod).
|
||||||
|
_MOTIF_AGENT = re.compile(r"^agent(-[a-z0-9-]*)?$")
|
||||||
|
|
||||||
|
|
||||||
|
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 _suffixe_environnement() -> str | None:
|
||||||
|
"""Suffixe d'environnement déduit de l'hôte de l'agent (BUG-120).
|
||||||
|
|
||||||
|
Lit l'hôte de :func:`agent_url` — l'origine de l'agent telle
|
||||||
|
qu'ObsiGate la connaît déjà (``DRACO_AGENT_URL``, à défaut
|
||||||
|
``AGENT_EVENEMENTS_URL``). Si cet hôte est exactement de la forme
|
||||||
|
``agent<SUFFIXE>.dracodev.net`` sans port, retourne ``SUFFIXE``
|
||||||
|
(``-dev`` en dev, ``""`` en prod). Sinon (autre domaine, LAN, IP,
|
||||||
|
port exotique) retourne ``None`` : aucune dérivation, on ne
|
||||||
|
fabrique jamais un lien deviné.
|
||||||
|
"""
|
||||||
|
u = urlparse(agent_url())
|
||||||
|
hote = (u.hostname or "").lower()
|
||||||
|
labels = hote.split(".")
|
||||||
|
if len(labels) != 3 or labels[1:] != ["dracodev", "net"]:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
if u.port is not None:
|
||||||
|
return None
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
motif = _MOTIF_AGENT.match(labels[0])
|
||||||
|
if motif is None:
|
||||||
|
return None
|
||||||
|
return motif.group(1) or ""
|
||||||
|
|
||||||
|
|
||||||
|
def app_url(app: str) -> str:
|
||||||
|
"""Base publique d'une application de l'écosystème (sans ``/`` final).
|
||||||
|
|
||||||
|
Résolution d'une URL sœur (BUG-120) : ``DRACO_<APP>_URL``
|
||||||
|
explicite d'abord ; à défaut, dérivation d'environnement depuis
|
||||||
|
l'hôte de l'agent (:func:`_suffixe_environnement`) ; à défaut,
|
||||||
|
le défaut officiel. L'URL Gitea ne se dérive jamais.
|
||||||
|
"""
|
||||||
|
if app == "agent":
|
||||||
|
return agent_url()
|
||||||
|
variable = _VARIABLES[app]
|
||||||
|
explicite = _nettoyer(os.environ.get(variable, ""))
|
||||||
|
if explicite:
|
||||||
|
return explicite
|
||||||
|
if app in _SLUGS_HOTE:
|
||||||
|
suffixe = _suffixe_environnement()
|
||||||
|
if suffixe is not None:
|
||||||
|
return f"https://{_SLUGS_HOTE[app]}{suffixe}.dracodev.net"
|
||||||
|
return 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 résolu de chaque application (les noms en ``-dev`` du poste,
|
||||||
|
déclarés dans son ``.env`` ou dérivés de l'hôte de l'agent).
|
||||||
|
"""
|
||||||
|
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 datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
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")
|
logger = logging.getLogger("obsigate.external_refs")
|
||||||
|
|
||||||
@@ -64,26 +64,87 @@ def _write_refs(username: str, refs: list[dict[str, Any]]) -> None:
|
|||||||
|
|
||||||
# ------------------------------------------------------------ parseur d'URL
|
# ------------------------------------------------------------ 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:
|
def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
|
||||||
"""Reconnaît une URL profonde des applications dracodev.
|
"""Reconnaît une URL profonde des applications dracodev.
|
||||||
|
|
||||||
Formes figées par ``dracodev-commons/contrats/liens-profonds.md``.
|
Formes figées par ``dracodev-commons/contrats/liens-profonds.md``.
|
||||||
Retourne ``(target_app, target_type, target_id, label_par_défaut)``
|
Retourne ``(target_app, target_type, target_id, label_par_défaut)``
|
||||||
ou ``None`` si l'URL ne correspond à aucune forme du contrat.
|
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:
|
try:
|
||||||
u = urlparse((url or "").strip())
|
u = urlparse((url or "").strip())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
host = (u.hostname or "").lower()
|
host = (u.hostname or "").lower()
|
||||||
if host == "tiktak.dracodev.net":
|
app = hotes_reconnus().get(host)
|
||||||
|
if app == "tiktak":
|
||||||
q = parse_qs(u.query)
|
q = parse_qs(u.query)
|
||||||
if q.get("task"):
|
if q.get("task"):
|
||||||
return ("tiktak", "task", q["task"][0], f"TikTak · tâche {q['task'][0]}")
|
return ("tiktak", "task", q["task"][0], f"TikTak · tâche {q['task'][0]}")
|
||||||
if q.get("list"):
|
if q.get("list"):
|
||||||
return ("tiktak", "list", q["list"][0], f"TikTak · liste {q['list'][0]}")
|
return ("tiktak", "list", q["list"][0], f"TikTak · liste {q['list'][0]}")
|
||||||
return None
|
return None
|
||||||
if host == "og.dracodev.net":
|
if app == "obsigate":
|
||||||
frag = u.fragment or ""
|
frag = u.fragment or ""
|
||||||
if frag.startswith("file="):
|
if frag.startswith("file="):
|
||||||
cible = unquote(frag[5:])
|
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]
|
nom = cible.rsplit("/", 1)[-1]
|
||||||
return ("obsigate", "note", cible, f"ObsiGate · {nom}")
|
return ("obsigate", "note", cible, f"ObsiGate · {nom}")
|
||||||
return None
|
return None
|
||||||
if host == "flowdeck.dracodev.net":
|
if app == "flowdeck":
|
||||||
m = re.fullmatch(r"/pages/(\d+)", u.path or "")
|
m = re.fullmatch(r"/pages/(\d+)", u.path or "")
|
||||||
if m:
|
if m:
|
||||||
return ("flowdeck", "page", m.group(1), f"Flowdeck · page {m.group(1)}")
|
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:
|
if m:
|
||||||
return ("flowdeck", "project", m.group(1), f"Flowdeck · projet {m.group(1)}")
|
return ("flowdeck", "project", m.group(1), f"Flowdeck · projet {m.group(1)}")
|
||||||
return None
|
return None
|
||||||
if host == "git.dracodev.net":
|
if app == "gitea":
|
||||||
m = re.fullmatch(r"/([^/]+)/([^/]+)/issues/(\d+)", u.path or "")
|
m = re.fullmatch(r"/([^/]+)/([^/]+)/issues/(\d+)", u.path or "")
|
||||||
if m:
|
if m:
|
||||||
owner, repo, num = m.groups()
|
owner, repo, num = m.groups()
|
||||||
@@ -111,20 +172,25 @@ def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
|
|||||||
# ------------------------------------------------------------ CRUD
|
# ------------------------------------------------------------ CRUD
|
||||||
|
|
||||||
def list_refs(username: str, vault: str, path: str) -> list[dict[str, Any]]:
|
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:
|
if not username:
|
||||||
return []
|
return []
|
||||||
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
|
if r.get("owner_vault") == vault and r.get("owner_path") == path
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_ref(username: str, ref_id: str) -> dict[str, Any] | None:
|
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):
|
for r in _read_refs(username):
|
||||||
if r.get("id") == ref_id:
|
if r.get("id") == ref_id:
|
||||||
return r
|
return _avec_url_affichee(r)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -169,4 +235,4 @@ def delete_ref(username: str, ref_id: str) -> dict[str, Any] | None:
|
|||||||
return None
|
return None
|
||||||
removed = next(r for r in refs if r.get("id") == ref_id)
|
removed = next(r for r in refs if r.get("id") == ref_id)
|
||||||
_write_refs(username, kept)
|
_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
|
# avec un nonce frais (`backend.csp`) ou servis par 'self'/CDN
|
||||||
# listés s'exécutent. `style-src` garde 'unsafe-inline' (attributs
|
# listés s'exécutent. `style-src` garde 'unsafe-inline' (attributs
|
||||||
# `style=` et `el.style` omniprésents — chantier séparé).
|
# `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"] = (
|
response.headers["Content-Security-Policy"] = (
|
||||||
"default-src 'self'; "
|
"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; "
|
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; "
|
"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:; "
|
"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; "
|
"font-src 'self' data: https://fonts.gstatic.com https://esm.sh; "
|
||||||
"worker-src 'self' blob:; "
|
"worker-src 'self' blob:; "
|
||||||
"frame-src 'self' blob:; "
|
f"frame-src 'self' blob: {agent}; "
|
||||||
"object-src 'none'; "
|
"object-src 'none'; "
|
||||||
"base-uri 'self'; "
|
"base-uri 'self'; "
|
||||||
"form-action 'self'; "
|
"form-action 'self'; "
|
||||||
@@ -829,6 +835,36 @@ def _html_with_nonce(request: Request, name: str) -> str:
|
|||||||
request.state.csp_nonce,
|
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():
|
if FRONTEND_DIR.exists():
|
||||||
# ``Cache-Control`` for /static is set by SecurityHeadersMiddleware (no-cache).
|
# ``Cache-Control`` for /static is set by SecurityHeadersMiddleware (no-cache).
|
||||||
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")
|
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."""
|
"""Serve the SPA index.html for all non-API routes."""
|
||||||
index_file = FRONTEND_DIR / "index.html"
|
index_file = FRONTEND_DIR / "index.html"
|
||||||
if index_file.exists():
|
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")
|
raise HTTPException(status_code=404, detail="Frontend not found")
|
||||||
|
|||||||
@@ -16,13 +16,16 @@ sortedcontainers>=2.4.0
|
|||||||
snowballstemmer>=2.2.0
|
snowballstemmer>=2.2.0
|
||||||
weasyprint>=70.0
|
weasyprint>=70.0
|
||||||
httpx>=0.27.0
|
httpx>=0.27.0
|
||||||
# Plancher de sécurité (BUG-093) : 6.16.0 est vulnérable à deux DoS de
|
# Plancher de sécurité (BUG-093, relevé par #224) : 6.16.0 est vulnérable à
|
||||||
# ressources (PYSEC-2026-3910 outlines, PYSEC-2026-3911 XForm, fix 6.16.1),
|
# deux DoS de ressources (PYSEC-2026-3910 outlines, PYSEC-2026-3911 XForm,
|
||||||
# atteignables via backend/pdf_reader.py (PDF fournis par l'utilisateur).
|
# fix 6.16.1), atteignables via backend/pdf_reader.py (PDF fournis par
|
||||||
# Le plancher doit être >= 6.16.1 : l'image Act du runner embarque 6.16.0
|
# l'utilisateur). #224 : la version préinstallée dans la toolcache du
|
||||||
# dans sa toolcache Python, donc un plancher trop bas est « already satisfied »
|
# runner (6.19.0) est frappée par 11 CVE supplémentaires (dont
|
||||||
# et n'est jamais mis à niveau.
|
# CVE-2026-113786, CVE-2026-115572, CVE-2026-115585, CVE-2026-115580),
|
||||||
pypdf>=6.16.1
|
# 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
|
pyotp>=2.10.0
|
||||||
segno>=1.5.0
|
segno>=1.5.0
|
||||||
webauthn==2.6.0
|
webauthn==2.6.0
|
||||||
@@ -55,3 +58,18 @@ multidict>=6.9.1
|
|||||||
# transitive (httpx/requests) — sans plancher explicite, pip-audit
|
# transitive (httpx/requests) — sans plancher explicite, pip-audit
|
||||||
# signale la version héritée de l'environnement.
|
# signale la version héritée de l'environnement.
|
||||||
certifi>=2026.10.19
|
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]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.77.0"
|
version = "2.79.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.77.0"
|
version = "2.79.0"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.77.0",
|
"version": "2.79.0",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
@@ -225,6 +225,8 @@ 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-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-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-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 |
|
||||||
|
| *BUG-120* | Menu inter-apps : sur l'environnement dev, les URL sœurs servies (`window.DRACO_URLS` : ObsiGate, Flowdeck, TikTak, Kanbante) restent en noms **prod** alors que l'URL de l'agent est bien en `-dev` | 🟢 corrigé | P1 | ⚙️ backend | IA | `backend/draco_config.py` (`app_url`, `_suffixe_environnement`) | Sur og-dev : `window.DRACO_URLS` donne `agent: https://agent-dev.dracodev.net` mais `tiktak: https://tiktak.dracodev.net` (prod) — le `.env` dev ne déclare pas les quatre `DRACO_<APP>_URL` | `backend/draco_config.py` : résolution sœur en 3 temps — explicite, puis **dérivation d'environnement** depuis l'hôte de `agent_url()` (`agent<SUFFIXE>.dracodev.net` → `https://<slug><SUFFIXE>.dracodev.net`, slugs `og`/`flowdeck`/`tiktak`/`kanbante` ; hors motif → rien), puis défaut officiel ; Gitea exclue. Tests `tests/test_draco_config.py` (+9 : dérivation `-dev` via `AGENT_EVENEMENTS_URL` et via `DRACO_AGENT_URL`, priorité de l'explicite, 5 hôtes hors motif, suffixe vide, injection) | Suite de #223 : seule l'URL de l'agent avait une dérivation de repli (`AGENT_EVENEMENTS_URL`) ; les sœurs retombaient sur les défauts officiels. Correctif de code demandé par Bruno : l'environnement se déduit de l'agent déjà configuré, sans variable supplémentaire. Redéploiement Docker requis |
|
||||||
|
|
||||||
| # | Titre | Statut | Priorité | Scope | Assigné | Zone (fichier) | Cmd de repro | Correctif / Commit | Notes |
|
| # | Titre | Statut | Priorité | Scope | Assigné | Zone (fichier) | Cmd de repro | Correctif / Commit | Notes |
|
||||||
|---|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
@@ -357,6 +359,8 @@ 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-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-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-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) |
|
||||||
|
| 2026-10-25 | BUG-120 (suite #223) | Correction | `backend/draco_config.py`, `tests/test_draco_config.py`, `.env.example`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md`, `docs/ROADMAP.md`, `docs/features/devprod-urls-config-223.md` | **BUG-120 — le menu inter-apps de la dev pointait la prod** : `window.DRACO_URLS` servait l'agent en `-dev` mais les quatre sœurs en noms officiels, le `.env` dev ne déclarant pas les `DRACO_<APP>_URL`. Correctif dans `draco_config.app_url()` : après l'explicite, **dérivation d'environnement** — hôte de `agent_url()` de la forme `agent<SUFFIXE>.dracodev.net` sans port → sœur en `<slug><SUFFIXE>.dracodev.net` ; hors motif (autre domaine, LAN, IP, port) aucune dérivation ; Gitea jamais dérivée. Vérifié : `test_draco_config.py` 16 passed, suite complète **1668 passed / 9 skipped**, ruff 0 et mypy 0 sur les fichiers touchés (51 erreurs ruff préexistantes ailleurs, inchangées) | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+61
-3
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.77.0 | **Dernière mise à jour :** 2026-10-23
|
> **Version :** 2.79.0 | **Dernière mise à jour :** 2026-10-26
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
@@ -30,9 +30,64 @@
|
|||||||
|
|
||||||
## 🔵 En cours
|
## 🔵 En cours
|
||||||
|
|
||||||
|
### 225. Fusion — panneau Spark : version du service d'agents visible en mode sidebar
|
||||||
|
|
||||||
|
- **Effort :** ~0,1 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-26 | **Statut :** ✅ livré le 2026-10-26 (version du hook — voir CHANGELOG ; redéploiement Docker requis)
|
||||||
|
- **Fiche :** [features/fusion-panneau-version-agent-225.md](./features/fusion-panneau-version-agent-225.md)
|
||||||
|
- **Description :** demande de Bruno (2026-10-26, carte fusion #64) : la
|
||||||
|
version « Service d'agents vX », déjà visible dans la console pleine
|
||||||
|
page, doit se lire aussi dans le **panneau natif des apps** (mode
|
||||||
|
sidebar). Le pied du panneau Spark affiche désormais cette ligne,
|
||||||
|
discrète et permanente, lue **en direct** dans la santé du service
|
||||||
|
(`GET /healthz` de l'origine injectée par l'hôte) à l'ouverture du
|
||||||
|
panneau, avec cache de session — jamais une valeur codée en dur ;
|
||||||
|
service injoignable = rien d'affiché. Même livraison : TikTak
|
||||||
|
0.61.3, Flowdeck 7.83.1, Kanbante 0.37.1.
|
||||||
|
- **Sous-tâches :**
|
||||||
|
- [x] Ligne de version dans le pied du panneau (canonique TikTak, propagée aux 4 apps)
|
||||||
|
- [x] Test `tests/test_draco_barre.py::test_panneau_version_service_affichee_225` + épreuve comportementale jsdom sur les 4 copies
|
||||||
|
- [x] Release publiée — CI verte à constater, redéploiement Docker (geste de Bruno)
|
||||||
|
|
||||||
|
### 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
|
### 222. Fusion — panneau Spark V3 : la console en surimpression pleine page
|
||||||
|
|
||||||
- **Effort :** ~0,1 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-23 | **Statut :** 🔵 en cours
|
- **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)
|
- **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`,
|
- **Description :** le panneau Spark (asset vendangé `frontend/draco-panneau.js`,
|
||||||
octet-identique dans les 4 apps) passe en V3 (demande de Bruno,
|
octet-identique dans les 4 apps) passe en V3 (demande de Bruno,
|
||||||
@@ -51,7 +106,7 @@
|
|||||||
dans les 4 apps
|
dans les 4 apps
|
||||||
- [x] Test `tests/test_draco_barre.py::test_panneau_console_surimpression_222`
|
- [x] Test `tests/test_draco_barre.py::test_panneau_console_surimpression_222`
|
||||||
(et révision du test #221 : le nouvel onglet disparaît)
|
(et révision du test #221 : le nouvel onglet disparaît)
|
||||||
- [ ] Release v2.77.0 publiée — CI à constater, redéploiement Docker (geste de Bruno)
|
- [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
|
### 221. Fusion — panneau Spark : bouton « Console ↗ » vers la console du service
|
||||||
|
|
||||||
@@ -1111,6 +1166,9 @@
|
|||||||
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
||||||
|
| BUG-120 | Menu inter-apps — les URL sœurs (`window.DRACO_URLS`) dérivent l'environnement de l'hôte de l'agent (`-dev` en dev), explicite prioritaire, Gitea inchangée (suite de #223) | 2.78.2 | [ISSUES_TODOLIST.md](./ISSUES_TODOLIST.md) |
|
||||||
|
| 225 | Panneau Spark — version du service d'agents visible en mode sidebar (pied du panneau, lue en direct via `GET /healthz`) | 2.79.0 | [features/fusion-panneau-version-agent-225.md](./features/fusion-panneau-version-agent-225.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# #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).
|
||||||
|
|
||||||
|
## Correctif BUG-120 (2026-10-25) — dérivation d'environnement des sœurs
|
||||||
|
|
||||||
|
Constat sur la dev : le `.env` du poste ne déclare que l'agent (via
|
||||||
|
`AGENT_EVENEMENTS_URL`) ; les quatre `DRACO_<APP>_URL` absentes
|
||||||
|
retombaient sur les défauts prod dans `window.DRACO_URLS` — le menu
|
||||||
|
inter-apps pointait la prod depuis la dev. La résolution d'une URL
|
||||||
|
sœur (`backend/draco_config.py::app_url`) gagne une étape
|
||||||
|
intermédiaire, entre l'explicite et le défaut officiel :
|
||||||
|
**dérivation depuis l'hôte de l'agent** (`agent_url()`) — hôte de la
|
||||||
|
forme `agent<SUFFIXE>.dracodev.net` sans port → sœur =
|
||||||
|
`https://<slug><SUFFIXE>.dracodev.net` (slugs : `og`, `flowdeck`,
|
||||||
|
`tiktak`, `kanbante`). Hôte hors motif (autre domaine, LAN, IP,
|
||||||
|
port) → aucune dérivation. Gitea exclue. Le dev n'a plus quatre
|
||||||
|
variables à déclarer ; un `.env` qui les déclare déjà est inchangé
|
||||||
|
(l'explicite prime).
|
||||||
|
|
||||||
|
## 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,40 @@
|
|||||||
|
# #225 — Fusion : panneau Spark, version du service d'agents visible en mode sidebar
|
||||||
|
|
||||||
|
**Contexte.** La version « Service d'agents vX » du service d'agents
|
||||||
|
est visible dans la console pleine page (pilule d'en-tête, rendue par
|
||||||
|
le service lui-même), mais pas dans le **panneau natif des
|
||||||
|
applications** (mode sidebar) — or c'est là que Bruno vit au
|
||||||
|
quotidien. Demande de Bruno (2026-10-26), carte fusion #64 : la
|
||||||
|
version doit se lire dans le panneau ouvert, sans ouvrir la console.
|
||||||
|
|
||||||
|
**Changement.** Dans `frontend/draco-panneau.js` — asset **vendangé**,
|
||||||
|
maintenu en canonique dans TikTak et recopié dans les 4 applications
|
||||||
|
(TikTak, Flowdeck, Kanbante, ObsiGate ; chaque copie garde son bloc
|
||||||
|
d'injection d'origine propre à l'hôte, le corps est commun) :
|
||||||
|
|
||||||
|
- le pied du panneau gagne une ligne dédiée
|
||||||
|
`.draco-panneau-version`, discrète (11,5 px, opacité 0,6,
|
||||||
|
monochrome, aucun emoji), **masquée tant qu'aucune version n'est
|
||||||
|
connue** ;
|
||||||
|
- à l'ouverture du panneau, `chargerVersion()` interroge la santé
|
||||||
|
du service — `GET /healthz` de l'origine que le panneau connaît
|
||||||
|
déjà (celle injectée par l'hôte, ici `window.DRACO_URLS.agent`) —
|
||||||
|
et affiche « Service d'agents vX » depuis le champ `version` de
|
||||||
|
la réponse (la même source que la pilule de la console) ;
|
||||||
|
- cache de session de 5 minutes, rafraîchissement au rythme des
|
||||||
|
ouvertures du panneau (comme la vérification d'identité) ;
|
||||||
|
- **jamais de valeur codée en dur** : réponse illisible ou service
|
||||||
|
injoignable = la ligne reste masquée — pas de fausse version, pas
|
||||||
|
d'erreur visible.
|
||||||
|
|
||||||
|
**Preuve.** Épreuve comportementale (jsdom) exécutée sur les 4
|
||||||
|
copies avec le contrat d'injection de chaque hôte : santé joignable
|
||||||
|
→ la ligne affiche « Service d'agents v0.38.0 » (version lue sur
|
||||||
|
l'origine de l'agent) ; santé injoignable → rien d'affiché ; hôte
|
||||||
|
sans injection → aucun appel. Test de garde :
|
||||||
|
`tests/test_draco_barre.py::test_panneau_version_service_affichee_225`.
|
||||||
|
|
||||||
|
**Même livraison** (carte fusion #64) : TikTak 0.61.3, Flowdeck
|
||||||
|
7.83.1, Kanbante 0.37.1. Côté ObsiGate, la prise d'effet attend le
|
||||||
|
**redéploiement Docker** (geste de Bruno), comme les items panneau
|
||||||
|
précédents.
|
||||||
@@ -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).
|
||||||
+4
-4
@@ -19,8 +19,8 @@
|
|||||||
var mode = localStorage.getItem("obsigate-theme-mode") || "dark";
|
var mode = localStorage.getItem("obsigate-theme-mode") || "dark";
|
||||||
root.setAttribute("data-theme", mode);
|
root.setAttribute("data-theme", mode);
|
||||||
var light = mode === "light" || mode === "sepia";
|
var light = mode === "light" || mode === "sepia";
|
||||||
root.style.setProperty("--bg-primary", light ? "#ffffff" : "#0f1117");
|
root.style.setProperty("--bg-primary", light ? "#fbfbfa" : "#17181a");
|
||||||
root.style.setProperty("--text-primary", light ? "#1a1a1a" : "#e6edf3");
|
root.style.setProperty("--text-primary", light ? "#242624" : "#eceeeb");
|
||||||
} catch (e) { root.setAttribute("data-theme", "dark"); }
|
} catch (e) { root.setAttribute("data-theme", "dark"); }
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow: auto; /* override SPA style.css: body{overflow:hidden} */
|
overflow: auto; /* override SPA style.css: body{overflow:hidden} */
|
||||||
height: auto; /* override SPA style.css: body{height:100vh} */
|
height: auto; /* override SPA style.css: body{height:100vh} */
|
||||||
background: var(--bg-primary, #0f1117);
|
background: var(--bg-primary, #17181a);
|
||||||
color: var(--text-primary, #e6edf3);
|
color: var(--text-primary, #eceeeb);
|
||||||
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
|
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,12 @@
|
|||||||
kanbante: "#0d9488",
|
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) {
|
function t(key, fallback) {
|
||||||
try {
|
try {
|
||||||
var v = window.t && window.t(key);
|
var v = window.t && window.t(key);
|
||||||
@@ -92,7 +98,7 @@
|
|||||||
t("draco.links.projected", "Tâche TikTak projetée")));
|
t("draco.links.projected", "Tâche TikTak projetée")));
|
||||||
var ouvrir = el("a", "btn-action draco-liens-ouvrir",
|
var ouvrir = el("a", "btn-action draco-liens-ouvrir",
|
||||||
t("draco.links.openInTikTak", "Ouvrir dans TikTak"));
|
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.target = "_blank";
|
||||||
ouvrir.rel = "noopener";
|
ouvrir.rel = "noopener";
|
||||||
bandeau.appendChild(ouvrir);
|
bandeau.appendChild(ouvrir);
|
||||||
|
|||||||
@@ -32,13 +32,17 @@
|
|||||||
* configuré / exécuteur non configuré sont affichés tels quels. */
|
* configuré / exécuteur non configuré sont affichés tels quels. */
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"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 convId = null;
|
||||||
var convKind = null; // 'principale' | 'parallele'
|
var convKind = null; // 'principale' | 'parallele'
|
||||||
var convTitre = "";
|
var convTitre = "";
|
||||||
var etatConnu = false;
|
var etatConnu = false;
|
||||||
var root = null, msgsEl = null, etatEl = null, inputEl = null, sendBtn = null;
|
var root = null, msgsEl = null, etatEl = null, inputEl = null, sendBtn = null;
|
||||||
var titreEl = null, tiroirEl = null;
|
var titreEl = null, tiroirEl = null;
|
||||||
|
var versionEl = null, versionConnue = "", versionMaj = 0;
|
||||||
|
|
||||||
function domaine() {
|
function domaine() {
|
||||||
try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; }
|
try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; }
|
||||||
@@ -56,7 +60,9 @@
|
|||||||
|
|
||||||
/* ── Console web (même service, surimpression pleine page) ─── */
|
/* ── Console web (même service, surimpression pleine page) ─── */
|
||||||
function appHote() {
|
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",
|
var correspondance = { tiktak: "tiktak", flowdeck: "flowdeck",
|
||||||
kanbante: "kanbante", og: "obsigate" };
|
kanbante: "kanbante", og: "obsigate" };
|
||||||
return correspondance[h] || null;
|
return correspondance[h] || null;
|
||||||
@@ -264,6 +270,7 @@
|
|||||||
' <input type="text" placeholder="Écrire à Spark…" autocomplete="off" aria-label="Message à Spark">' +
|
' <input type="text" placeholder="Écrire à Spark…" autocomplete="off" aria-label="Message à Spark">' +
|
||||||
' <button type="submit">Envoyer</button></form>' +
|
' <button type="submit">Envoyer</button></form>' +
|
||||||
' <div class="draco-panneau-note">Conversation réelle, sous-agents (J3) et missions longues (J5) vers les exécuteurs configurés ; toute écriture dans les apps passe par une approbation.</div>' +
|
' <div class="draco-panneau-note">Conversation réelle, sous-agents (J3) et missions longues (J5) vers les exécuteurs configurés ; toute écriture dans les apps passe par une approbation.</div>' +
|
||||||
|
' <div class="draco-panneau-version" style="margin-top:6px;font-size:11.5px;opacity:.6" hidden></div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
document.body.appendChild(root);
|
document.body.appendChild(root);
|
||||||
msgsEl = root.querySelector(".draco-panneau-msgs");
|
msgsEl = root.querySelector(".draco-panneau-msgs");
|
||||||
@@ -272,6 +279,7 @@
|
|||||||
sendBtn = root.querySelector("button[type=submit]");
|
sendBtn = root.querySelector("button[type=submit]");
|
||||||
titreEl = root.querySelector(".d-titre");
|
titreEl = root.querySelector(".d-titre");
|
||||||
tiroirEl = root.querySelector(".draco-discussions");
|
tiroirEl = root.querySelector(".draco-discussions");
|
||||||
|
versionEl = root.querySelector(".draco-panneau-version");
|
||||||
root.querySelector(".fermer").addEventListener("click", fermer);
|
root.querySelector(".fermer").addEventListener("click", fermer);
|
||||||
root.querySelector('[data-disc="liste"]').addEventListener("click", basculerTiroir);
|
root.querySelector('[data-disc="liste"]').addEventListener("click", basculerTiroir);
|
||||||
root.querySelector('[data-disc="nouvelle"]').addEventListener("click", nouvelleDiscussion);
|
root.querySelector('[data-disc="nouvelle"]').addEventListener("click", nouvelleDiscussion);
|
||||||
@@ -326,6 +334,35 @@
|
|||||||
titreEl.title = titreEl.textContent;
|
titreEl.title = titreEl.textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Version du service : lue en direct dans la santé de
|
||||||
|
* l'agent (GET /healthz → champ « version », la même source que
|
||||||
|
* la console) à l'origine que le panneau connaît déjà. Cache de
|
||||||
|
* session (5 min), rafraîchie à l'ouverture du panneau, au même
|
||||||
|
* rythme que la vérification d'identité. Jamais de valeur codée
|
||||||
|
* en dur : source injoignable ou réponse illisible = la ligne
|
||||||
|
* reste masquée — pas de fausse version, pas d'erreur visible. */
|
||||||
|
function afficherVersion() {
|
||||||
|
if (!versionEl) return;
|
||||||
|
if (versionConnue) {
|
||||||
|
versionEl.textContent = "Service d'agents v" + versionConnue;
|
||||||
|
versionEl.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function chargerVersion() {
|
||||||
|
if (!API || !versionEl) return;
|
||||||
|
if (versionConnue && Date.now() - versionMaj < 300000) { afficherVersion(); return; }
|
||||||
|
fetch(API + "/healthz")
|
||||||
|
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
|
||||||
|
.then(function (d) {
|
||||||
|
if (d && typeof d.version === "string" && /^[0-9]+\.[0-9]+\.[0-9]+/.test(d.version)) {
|
||||||
|
versionConnue = d.version;
|
||||||
|
versionMaj = Date.now();
|
||||||
|
afficherVersion();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () { /* service injoignable : rien d'affiché */ });
|
||||||
|
}
|
||||||
|
|
||||||
function verifierIdentite() {
|
function verifierIdentite() {
|
||||||
if (etatConnu) return;
|
if (etatConnu) return;
|
||||||
fetch(API + "/api/v1/me", { credentials: "include" })
|
fetch(API + "/api/v1/me", { credentials: "include" })
|
||||||
@@ -802,6 +839,7 @@
|
|||||||
root.classList.add("open");
|
root.classList.add("open");
|
||||||
appliquerTheme();
|
appliquerTheme();
|
||||||
verifierIdentite();
|
verifierIdentite();
|
||||||
|
chargerVersion();
|
||||||
setTimeout(function () { inputEl.focus(); }, 300);
|
setTimeout(function () { inputEl.focus(); }, 300);
|
||||||
}
|
}
|
||||||
function fermer() { if (root) root.classList.remove("open"); }
|
function fermer() { if (root) root.classList.remove("open"); }
|
||||||
|
|||||||
+14
-4
@@ -10,7 +10,7 @@
|
|||||||
name="description"
|
name="description"
|
||||||
content="Porte d'entrée web pour vos vaults Obsidian - Accédez, naviguez et recherchez dans toutes vos notes"
|
content="Porte d'entrée web pour vos vaults Obsidian - Accédez, naviguez et recherchez dans toutes vos notes"
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#2563eb" />
|
<meta name="theme-color" content="#7c5cd6" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta
|
<meta
|
||||||
@@ -54,14 +54,14 @@
|
|||||||
#boot-splash {
|
#boot-splash {
|
||||||
position: fixed; inset: 0; z-index: 2147483647;
|
position: fixed; inset: 0; z-index: 2147483647;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
background: radial-gradient(1200px 600px at 50% -10%, #1b2030 0%, #0e1117 60%);
|
background: radial-gradient(1200px 600px at 50% -10%, #26272b 0%, #17181a 60%);
|
||||||
color: #e6e8ee; font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
|
color: #eceeeb; font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||||
}
|
}
|
||||||
body.app-booted #boot-splash { display: none; }
|
body.app-booted #boot-splash { display: none; }
|
||||||
.bs-card { display: flex; flex-direction: column; align-items: center; gap: 14px; }
|
.bs-card { display: flex; flex-direction: column; align-items: center; gap: 14px; }
|
||||||
.bs-logo {
|
.bs-logo {
|
||||||
width: 72px; height: 72px; display: flex; align-items: center; justify-content: center;
|
width: 72px; height: 72px; display: flex; align-items: center; justify-content: center;
|
||||||
font-size: 38px; background: #171c26; border: 1px solid #2a3242; border-radius: 18px;
|
font-size: 38px; background: #1f2023; border: 1px solid #33353a; border-radius: 18px;
|
||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
.bs-title { font-size: 22px; font-weight: 600; letter-spacing: 0.3px; }
|
.bs-title { font-size: 22px; font-weight: 600; letter-spacing: 0.3px; }
|
||||||
@@ -397,6 +397,16 @@
|
|||||||
localement ; la recherche focus la recherche native
|
localement ; la recherche focus la recherche native
|
||||||
d'ObsiGate ; Alertes unifiées et Spark arrivent avec la
|
d'ObsiGate ; Alertes unifiées et Spark arrivent avec la
|
||||||
fédération / l'agent commun (phases suivantes). */
|
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 = {
|
window.DRACO_BAR_CONFIG = {
|
||||||
current: "obsigate",
|
current: "obsigate",
|
||||||
domain: (function () { try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; } })(),
|
domain: (function () { try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; } })(),
|
||||||
|
|||||||
+67
-2
@@ -1,5 +1,5 @@
|
|||||||
/* ObsiGate — Theme System
|
/* ObsiGate — Theme System
|
||||||
* 15 themes x 2 modes (dark/light)
|
* 16 themes x 2 modes (dark/light)
|
||||||
* Persisted in localStorage as 'obsigate-theme'
|
* Persisted in localStorage as 'obsigate-theme'
|
||||||
* Usage: import { initThemes, applyTheme, getCurrentTheme, THEMES } from './themes.js'
|
* Usage: import { initThemes, applyTheme, getCurrentTheme, THEMES } from './themes.js'
|
||||||
*/
|
*/
|
||||||
@@ -45,6 +45,71 @@ var THEMES = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Thème Spark ──────────────────────────────────────────────────────
|
||||||
|
// Palette source : console Spark (agent.dracodev.net), app/console/console.css
|
||||||
|
// (variables --p-*). Mapping Spark -> ObsiGate, identique en structure pour
|
||||||
|
// dark et light ; seules les valeurs changent (sauf accent/ok/warn/err).
|
||||||
|
// Fidèle (valeur Spark directe) :
|
||||||
|
// fond -> --bg-primary, --surface2 ; surface -> --surface ;
|
||||||
|
// chip -> --bg-secondary, --tag-bg ; chip-survol -> --bg-hover ;
|
||||||
|
// bord -> --border ; chip-bord -> --border-md, --scrollbar, --resize-handle ;
|
||||||
|
// texte -> --text-primary, --text ; doux -> --text-secondary, --text-2 ;
|
||||||
|
// faint -> --text-muted, --text-3 ; champ -> --search-bg ;
|
||||||
|
// code -> --code-bg ; accent -> --accent, --accent-card, --purple, --ai ;
|
||||||
|
// lien -> --accent-text (variante lisible de l'accent sur fond teinté) ;
|
||||||
|
// ok -> --accent-green, --success, --green ;
|
||||||
|
// danger -> --danger ; danger-fond -> --danger-bg ;
|
||||||
|
// pastilles ok (fond) -> --success-bg, --green-bg ;
|
||||||
|
// pastilles ok (texte) -> --ok ; ambre -> --warn ; rouge -> --err.
|
||||||
|
// Approximations documentées (ObsiGate a plus de variables que Spark,
|
||||||
|
// ce ne sont PAS des valeurs Spark) :
|
||||||
|
// --bg-sidebar <- fond (Spark n'a pas de token sidebar dédié) ;
|
||||||
|
// --accent-bg <- agent (surface teintée de Spark, pas de teinte lavande) ;
|
||||||
|
// --surface3 <- agent ; --purple-bg <- appro-fond (fond teinté le plus
|
||||||
|
// proche, la palette Spark n'a pas de teinte lavande) ;
|
||||||
|
// --overlay-bg <- convention ObsiGate (rgba noir 0.3 clair / 0.5 sombre,
|
||||||
|
// aucun token voile dans la palette Spark).
|
||||||
|
// Non reportés (aucune variable ObsiGate correspondante dans les 39) :
|
||||||
|
// bulle utilisateur/envoi #2f6fed, titres approbation #9a6a1c / #e8bc7a.
|
||||||
|
// Une seule entrée 'spark' : dark et light sont ses deux modes, jamais
|
||||||
|
// deux thèmes séparés. Modes high-contrast/sepia générés comme pour les
|
||||||
|
// autres thèmes (à partir de dark/light).
|
||||||
|
'spark': {
|
||||||
|
name: 'Spark', desc: 'Palette de la console Spark',
|
||||||
|
modes: {
|
||||||
|
dark: {
|
||||||
|
'--bg-primary': '#17181a', '--bg-secondary': '#26272b', '--bg-sidebar': '#17181a',
|
||||||
|
'--bg-hover': '#2e3036', '--border': '#33353a', '--text-primary': '#eceeeb',
|
||||||
|
'--text-secondary': '#a6aba0', '--text-muted': '#83887e', '--accent': '#7c5cd6',
|
||||||
|
'--accent-green': '#2ea36b', '--tag-bg': '#26272b', '--tag-text': '#7c5cd6',
|
||||||
|
'--code-bg': '#2c2e33', '--search-bg': '#1f2023', '--scrollbar': '#3a3c41',
|
||||||
|
'--resize-handle': '#3a3c41', '--overlay-bg': 'rgba(0,0,0,0.5)', '--danger': '#e8a08d',
|
||||||
|
'--danger-bg': '#40241f', '--success': '#2ea36b', '--success-bg': '#173d29',
|
||||||
|
'--accent-card': '#7c5cd6', '--accent-bg': '#26272b', '--accent-text': '#b39df5',
|
||||||
|
'--green': '#2ea36b', '--green-bg': '#173d29', '--purple': '#7c5cd6',
|
||||||
|
'--purple-bg': '#2a2118', '--surface': '#1f2023', '--surface2': '#17181a',
|
||||||
|
'--surface3': '#26272b', '--border-md': '#3a3c41', '--text': '#eceeeb',
|
||||||
|
'--text-2': '#a6aba0', '--text-3': '#83887e', '--ok': '#7fd6a8',
|
||||||
|
'--warn': '#e8a13c', '--ai': '#7c5cd6', '--err': '#cf4b32'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
'--bg-primary': '#fbfbfa', '--bg-secondary': '#f4f5f1', '--bg-sidebar': '#fbfbfa',
|
||||||
|
'--bg-hover': '#e9ebe4', '--border': '#e4e5e0', '--text-primary': '#242624',
|
||||||
|
'--text-secondary': '#6c7268', '--text-muted': '#8a8f84', '--accent': '#7c5cd6',
|
||||||
|
'--accent-green': '#2ea36b', '--tag-bg': '#f4f5f1', '--tag-text': '#7c5cd6',
|
||||||
|
'--code-bg': '#e8eae3', '--search-bg': '#ffffff', '--scrollbar': '#d6d8d1',
|
||||||
|
'--resize-handle': '#d6d8d1', '--overlay-bg': 'rgba(0,0,0,0.3)', '--danger': '#a03a28',
|
||||||
|
'--danger-bg': '#f6e3de', '--success': '#2ea36b', '--success-bg': '#dff2e7',
|
||||||
|
'--accent-card': '#7c5cd6', '--accent-bg': '#eef0ea', '--accent-text': '#5b3df0',
|
||||||
|
'--green': '#2ea36b', '--green-bg': '#dff2e7', '--purple': '#7c5cd6',
|
||||||
|
'--purple-bg': '#fdf6ec', '--surface': '#ffffff', '--surface2': '#fbfbfa',
|
||||||
|
'--surface3': '#eef0ea', '--border-md': '#d6d8d1', '--text': '#242624',
|
||||||
|
'--text-2': '#6c7268', '--text-3': '#8a8f84', '--ok': '#1d7a4c',
|
||||||
|
'--warn': '#e8a13c', '--ai': '#7c5cd6', '--err': '#cf4b32'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
'github': {
|
'github': {
|
||||||
name: 'GitHub', desc: 'Contraste doux',
|
name: 'GitHub', desc: 'Contraste doux',
|
||||||
modes: {
|
modes: {
|
||||||
@@ -523,7 +588,7 @@ var THEMES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Theme Engine ─────────────────────────────────────────────────────
|
// ── Theme Engine ─────────────────────────────────────────────────────
|
||||||
var _currentTheme = 'defaut-obsigate';
|
var _currentTheme = 'spark';
|
||||||
var _currentMode = 'dark';
|
var _currentMode = 'dark';
|
||||||
|
|
||||||
var AVAILABLE_MODES = ['dark', 'light', 'high-contrast', 'sepia'];
|
var AVAILABLE_MODES = ['dark', 'light', 'high-contrast', 'sepia'];
|
||||||
|
|||||||
+42
-42
@@ -11,58 +11,58 @@
|
|||||||
|
|
||||||
/* ── Editor save dot state colors ── */
|
/* ── Editor save dot state colors ── */
|
||||||
:root {
|
:root {
|
||||||
--ok: #34d399;
|
--ok: #7fd6a8;
|
||||||
--warn: #fbbf24;
|
--warn: #e8a13c;
|
||||||
--ai: #8b5cf6;
|
--ai: #7c5cd6;
|
||||||
--err: #f87171;
|
--err: #cf4b32;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== THEME — DARK (fallback default, no data-theme) ===== */
|
/* ===== THEME — DARK (fallback default, no data-theme) ===== */
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #0f1117;
|
--bg-primary: #17181a;
|
||||||
--bg-secondary: #161b22;
|
--bg-secondary: #26272b;
|
||||||
--bg-sidebar: #13161d;
|
--bg-sidebar: #17181a;
|
||||||
--bg-hover: #1f2430;
|
--bg-hover: #2e3036;
|
||||||
--border: #21262d;
|
--border: #33353a;
|
||||||
--text-primary: #e6edf3;
|
--text-primary: #eceeeb;
|
||||||
--text-secondary: #8b949e;
|
--text-secondary: #a6aba0;
|
||||||
--text-muted: #484f58;
|
--text-muted: #83887e;
|
||||||
--accent: #58a6ff;
|
--accent: #7c5cd6;
|
||||||
--accent-green: #3fb950;
|
--accent-green: #2ea36b;
|
||||||
--tag-bg: #1f2a3a;
|
--tag-bg: #26272b;
|
||||||
--tag-text: #79c0ff;
|
--tag-text: #7c5cd6;
|
||||||
--code-bg: #161b22;
|
--code-bg: #2c2e33;
|
||||||
--search-bg: #21262d;
|
--search-bg: #1f2023;
|
||||||
--scrollbar: #30363d;
|
--scrollbar: #3a3c41;
|
||||||
--resize-handle: #30363d;
|
--resize-handle: #3a3c41;
|
||||||
--overlay-bg: rgba(0, 0, 0, 0.5);
|
--overlay-bg: rgba(0, 0, 0, 0.5);
|
||||||
--danger: #ff7b72;
|
--danger: #e8a08d;
|
||||||
--danger-bg: #3d1a18;
|
--danger-bg: #40241f;
|
||||||
--success: #3fb950;
|
--success: #2ea36b;
|
||||||
--success-bg: #1a3d1f;
|
--success-bg: #173d29;
|
||||||
--accent-card: #58a6ff;
|
--accent-card: #7c5cd6;
|
||||||
--accent-bg: #1f2a3a;
|
--accent-bg: #26272b;
|
||||||
--accent-text: #79c0ff;
|
--accent-text: #b39df5;
|
||||||
--green: #3fb950;
|
--green: #2ea36b;
|
||||||
--green-bg: #1a3d1f;
|
--green-bg: #173d29;
|
||||||
--purple: #a371f7;
|
--purple: #7c5cd6;
|
||||||
--purple-bg: #2d1b4e;
|
--purple-bg: #2a2118;
|
||||||
--surface: #161b22;
|
--surface: #1f2023;
|
||||||
--surface2: #0f1117;
|
--surface2: #17181a;
|
||||||
--surface3: #0d0f14;
|
--surface3: #26272b;
|
||||||
--border-md: #30363d;
|
--border-md: #3a3c41;
|
||||||
--text: #e6edf3;
|
--text: #eceeeb;
|
||||||
--text-2: #8b949e;
|
--text-2: #a6aba0;
|
||||||
--text-3: #484f58;
|
--text-3: #83887e;
|
||||||
--mono: "JetBrains Mono", monospace;
|
--mono: "JetBrains Mono", monospace;
|
||||||
--radius: 6px;
|
--radius: 6px;
|
||||||
--radius-lg: 10px;
|
--radius-lg: 10px;
|
||||||
/* #154-A1 — data-grid tokens: the sheet reads slightly lighter than the
|
/* #154-A1 — data-grid tokens: the sheet reads slightly lighter than the
|
||||||
chrome (toolbar/panels), headers are clearly distinct from cells. */
|
chrome (toolbar/panels), headers are clearly distinct from cells. */
|
||||||
--grid-bg: #161b22;
|
--grid-bg: #1f2023;
|
||||||
--grid-header-bg: #1f2430;
|
--grid-header-bg: #2e3036;
|
||||||
--grid-header-text: #e6edf3;
|
--grid-header-text: #eceeeb;
|
||||||
--grid-border: #3d444d;
|
--grid-border: #33353a;
|
||||||
--grid-zebra: rgba(255, 255, 255, 0.025);
|
--grid-zebra: rgba(255, 255, 255, 0.025);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.77.0",
|
"version": "2.79.0",
|
||||||
"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.",
|
"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",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -251,11 +251,17 @@ class TestDependencySecurityFloors:
|
|||||||
|
|
||||||
#: (paquet, plancher minimal, advisories corrigées au-dessus)
|
#: (paquet, plancher minimal, advisories corrigées au-dessus)
|
||||||
FLOORS = {
|
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).
|
# 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)
|
"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)
|
"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)
|
"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):
|
def test_security_floors_are_declared(self):
|
||||||
|
|||||||
@@ -100,9 +100,28 @@ def test_panneau_console_surimpression_222():
|
|||||||
def test_csp_ouvre_le_service_d_agents_218():
|
def test_csp_ouvre_le_service_d_agents_218():
|
||||||
# Le panneau parle au service d'agents : la CSP doit nommer son
|
# 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.
|
# 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")
|
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)
|
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():
|
def test_rail_mobile_vers_le_vrai_hamburger():
|
||||||
@@ -110,3 +129,20 @@ def test_rail_mobile_vers_le_vrai_hamburger():
|
|||||||
assert 'matchMedia("(max-width: 768px)")' in html
|
assert 'matchMedia("(max-width: 768px)")' in html
|
||||||
assert '"hamburger-btn"' in html
|
assert '"hamburger-btn"' in html
|
||||||
assert '"sidebar-toggle-btn"' in html
|
assert '"sidebar-toggle-btn"' in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_panneau_version_service_affichee_225():
|
||||||
|
# #225 : le pied du panneau Spark (mode sidebar) affiche la
|
||||||
|
# version du service d'agents, comme la console pleine page.
|
||||||
|
# La version est lue en direct dans la santé du service
|
||||||
|
# (GET /healthz de l'origine injectée par l'hôte), à l'ouverture
|
||||||
|
# du panneau ; jamais un littéral de version dans l'asset, et
|
||||||
|
# source injoignable = ligne laissée masquée (rien d'affiché).
|
||||||
|
panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8")
|
||||||
|
assert "draco-panneau-version" in panneau
|
||||||
|
assert "chargerVersion" in panneau
|
||||||
|
assert 'API + "/healthz"' in panneau
|
||||||
|
assert '"Service d\'agents v" + versionConnue' in panneau
|
||||||
|
# La ligne naît masquée et n'est démasquée qu'avec une version lue.
|
||||||
|
assert 'class="draco-panneau-version"' in panneau
|
||||||
|
assert "versionEl.hidden = false" in panneau
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
"""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"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------- BUG-120 : dérivation sœurs
|
||||||
|
|
||||||
|
def test_soeurs_derivees_depuis_agent_dev(env_propre):
|
||||||
|
"""BUG-120 : avec **seul** ``AGENT_EVENEMENTS_URL`` en ``-dev``
|
||||||
|
posé (aucune ``DRACO_<APP>_URL``), les URL sœurs servies suivent
|
||||||
|
l'environnement de l'agent ; Gitea ne bouge jamais."""
|
||||||
|
env_propre.setenv(
|
||||||
|
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
|
||||||
|
urls = draco_config.urls_publiques()
|
||||||
|
assert urls["agent"] == "https://agent-dev.dracodev.net"
|
||||||
|
assert urls["obsigate"] == "https://og-dev.dracodev.net"
|
||||||
|
assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net"
|
||||||
|
assert urls["tiktak"] == "https://tiktak-dev.dracodev.net"
|
||||||
|
assert urls["kanbante"] == "https://kanbante-dev.dracodev.net"
|
||||||
|
assert urls["gitea"] == "https://git.dracodev.net"
|
||||||
|
|
||||||
|
|
||||||
|
def test_soeurs_derivees_depuis_draco_agent_url(env_propre):
|
||||||
|
"""BUG-120 : la dérivation lit l'agent via ``agent_url()`` — un
|
||||||
|
``DRACO_AGENT_URL`` en ``-dev`` suffit aussi."""
|
||||||
|
env_propre.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
|
||||||
|
assert draco_config.app_url("tiktak") == "https://tiktak-dev.dracodev.net"
|
||||||
|
assert draco_config.app_url("obsigate") == "https://og-dev.dracodev.net"
|
||||||
|
|
||||||
|
|
||||||
|
def test_soeur_explicite_prime_sur_derivation(env_propre):
|
||||||
|
"""BUG-120 : une ``DRACO_<APP>_URL`` explicite gagne toujours sur
|
||||||
|
la dérivation d'environnement ; les autres sœurs dérivent."""
|
||||||
|
env_propre.setenv(
|
||||||
|
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
|
||||||
|
env_propre.setenv("DRACO_TIKTAK_URL", "https://taches.example.net/")
|
||||||
|
assert draco_config.app_url("tiktak") == "https://taches.example.net"
|
||||||
|
assert draco_config.app_url("flowdeck") == "https://flowdeck-dev.dracodev.net"
|
||||||
|
assert draco_config.app_url("kanbante") == "https://kanbante-dev.dracodev.net"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evenements", [
|
||||||
|
"https://agent.example.net/api/v1/evenements", # autre domaine
|
||||||
|
"https://agent-dev.example.com/api/v1/evenements", # suffixe hors dracodev.net
|
||||||
|
"http://deepin.lab.home:8790/api/v1/evenements", # nom LAN + port
|
||||||
|
"http://192.168.30.101:8790/api/v1/evenements", # IP + port
|
||||||
|
"https://agent-dev.dracodev.net:8790/api/v1/evenements", # port exotique
|
||||||
|
])
|
||||||
|
def test_agent_hors_motif_aucune_derivation(env_propre, evenements):
|
||||||
|
"""BUG-120 : hôte d'agent hors motif ``agent<SUFFIXE>.dracodev.net``
|
||||||
|
→ **aucune dérivation** (jamais de lien deviné), défauts officiels."""
|
||||||
|
env_propre.setenv("AGENT_EVENEMENTS_URL", evenements)
|
||||||
|
urls = draco_config.urls_publiques()
|
||||||
|
assert urls["obsigate"] == "https://og.dracodev.net"
|
||||||
|
assert urls["flowdeck"] == "https://flowdeck.dracodev.net"
|
||||||
|
assert urls["tiktak"] == "https://tiktak.dracodev.net"
|
||||||
|
assert urls["kanbante"] == "https://kanbante.dracodev.net"
|
||||||
|
assert urls["gitea"] == "https://git.dracodev.net"
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_prod_suffixe_vide_defauts_officiels(env_propre):
|
||||||
|
"""BUG-120 : hôte d'agent prod (suffixe vide) → les sœurs dérivées
|
||||||
|
sont exactement les défauts officiels."""
|
||||||
|
env_propre.setenv(
|
||||||
|
"AGENT_EVENEMENTS_URL", "https://agent.dracodev.net/api/v1/evenements")
|
||||||
|
for app, defaut in draco_config.DEFAUTS.items():
|
||||||
|
assert draco_config.app_url(app) == defaut
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
BUG-120 : l'agent en ``-dev`` entraîne les sœurs en ``-dev``
|
||||||
|
(dérivation d'environnement) — avant, ``tiktak`` restait en prod.
|
||||||
|
"""
|
||||||
|
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-dev.dracodev.net"' in resp.text
|
||||||
|
assert '"gitea": "https://git.dracodev.net"' in resp.text
|
||||||
@@ -72,3 +72,59 @@ def test_security_headers_present(client):
|
|||||||
csp = resp.headers.get("content-security-policy", "")
|
csp = resp.headers.get("content-security-policy", "")
|
||||||
assert "object-src 'none'" in csp
|
assert "object-src 'none'" in csp
|
||||||
assert "frame-ancestors 'self'" 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