Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aabedb0f3f | ||
|
|
b06b7e79ac | ||
|
|
73bf7d1170 | ||
|
|
1ed3154668 | ||
|
|
a59049d436 | ||
|
|
51d01f47c2 | ||
|
|
97b2f49d49 | ||
|
|
cb83c03d15 | ||
|
|
e1167eec27 | ||
|
|
1365c95c2f | ||
|
|
ec49706952 | ||
|
|
0612da184c | ||
|
|
961691d6d2 | ||
|
|
1f22669ac6 | ||
|
|
342026a3d9 | ||
|
|
34c1ad697f | ||
|
|
d3261bc8c4 | ||
|
|
655ef61fc5 | ||
|
|
e9d10113aa | ||
|
|
39809dcec8 | ||
|
|
37b9e183d9 | ||
|
|
8587b4ad00 | ||
|
|
c8ea130ff6 | ||
|
|
c55714d892 | ||
|
|
a4be009e3e | ||
|
|
3698ab3c73 | ||
|
|
bf32afc217 | ||
|
|
92c1be9780 | ||
|
|
96751f9fcd | ||
|
|
c7320d2811 | ||
|
|
5aea83749d | ||
|
|
72cca8a3b0 | ||
|
|
8232ed7987 | ||
|
|
bda53f299a | ||
|
|
7aa789d1cf | ||
|
|
1d477a4c6e |
@@ -64,7 +64,8 @@
|
|||||||
# Where to store the registration result.
|
# Where to store the registration result.
|
||||||
file: .runner
|
file: .runner
|
||||||
# Execute how many tasks concurrently at the same time.
|
# Execute how many tasks concurrently at the same time.
|
||||||
capacity: 2
|
# 1 seul job à la fois : les hôtes (8 Go, control-plane k3s sur pi1) ne survivent pas à 2 builds lourds simultanés.
|
||||||
|
capacity: 1
|
||||||
# Extra environment variables to run jobs.
|
# Extra environment variables to run jobs.
|
||||||
envs:
|
envs:
|
||||||
A_TEST_ENV_NAME_1: a_test_env_value_1
|
A_TEST_ENV_NAME_1: a_test_env_value_1
|
||||||
@@ -117,7 +118,9 @@
|
|||||||
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
|
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
|
||||||
privileged: false
|
privileged: false
|
||||||
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
|
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
|
||||||
options:
|
# Plafonds durs : un build ne doit jamais pouvoir affamer l'hôte (incident 2026-07-23 :
|
||||||
|
# nuxt generate à 3,5 Go RSS sur pi1 → load 150, ingress+API k3s morts → gitea.arcodange.lab injoignable).
|
||||||
|
options: "--memory=3g --memory-swap=3g --cpus=2 --pids-limit=512"
|
||||||
# The parent directory of a job's working directory.
|
# The parent directory of a job's working directory.
|
||||||
# NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically.
|
# NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically.
|
||||||
# If the path starts with '/', the '/' will be trimmed.
|
# If the path starts with '/', the '/' will be trimmed.
|
||||||
|
|||||||
@@ -54,7 +54,11 @@
|
|||||||
cache 30
|
cache 30
|
||||||
loop
|
loop
|
||||||
reload
|
reload
|
||||||
|
loadbalance
|
||||||
import /etc/coredns/custom/*.override
|
import /etc/coredns/custom/*.override
|
||||||
import /etc/coredns/custom/*.server
|
|
||||||
forward . {{ pihole_ips | map('regex_replace', '^(.*)$', '\1:53') | join(' ') }}
|
forward . {{ pihole_ips | map('regex_replace', '^(.*)$', '\1:53') | join(' ') }}
|
||||||
}
|
}
|
||||||
|
# Les fichiers *.server contiennent des BLOCS SERVEUR complets (ex: `arcodange.lab:53 {…}`) :
|
||||||
|
# leur import doit vivre au niveau racine du Corefile. À l'intérieur de `.:53 {}`,
|
||||||
|
# CoreDNS crashe au parse (« Unknown directive 'arcodange.lab:53' ») — vécu le 2026-07-24.
|
||||||
|
import /etc/coredns/custom/*.server
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBwDCCAWagAwIBAgIRAJzOnXbHdqAB0QnEjNw21xgwCgYIKoZIzj0EAwIwPjEZ
|
||||||
|
MBcGA1UEChMQQXJjb2RhbmdlIExhYiBDQTEhMB8GA1UEAxMYQXJjb2RhbmdlIExh
|
||||||
|
YiBDQSBSb290IENBMB4XDTI1MTIyOTA5Mjk0NVoXDTM1MTIyNzA5Mjk0NVowPjEZ
|
||||||
|
MBcGA1UEChMQQXJjb2RhbmdlIExhYiBDQTEhMB8GA1UEAxMYQXJjb2RhbmdlIExh
|
||||||
|
YiBDQSBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERTF3M6MtqK4m
|
||||||
|
q4e38e1KzHP7TRrf/DwEwxyafyp9iONE6na0+dgPvXPurG0kmom9PIYA2aE2eCzz
|
||||||
|
hFkQ2DO1TqNFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEw
|
||||||
|
HQYDVR0OBBYEFJCxc6tumAlVAaUjoKExPcNZsVoYMAoGCCqGSM49BAMCA0gAMEUC
|
||||||
|
IGtrew3FOPh16x3XevWCO8suH7laCn8kTV2ZZpAK0UkhAiEA/bA7HiDqEaXHSc35
|
||||||
|
b7fZX1fuKI6SdEWN9hj5EwP45Z8=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
{{- range $app_name, $app_attr := .Values.gitea_applications -}}
|
{{- range $app_name, $app_attr := .Values.gitea_applications -}}
|
||||||
{{- $org := default "arcodange-org" $app_attr.org -}}
|
{{- $org := default "arcodange-org" $app_attr.org -}}
|
||||||
|
{{- /* Namespace defaults to the app name; override lets siblings share one
|
||||||
|
(e.g. kadans-jobs deploys into the `kadans` namespace). */ -}}
|
||||||
|
{{- $ns := default $app_name $app_attr.namespace -}}
|
||||||
---
|
---
|
||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
@@ -20,7 +23,15 @@ spec:
|
|||||||
path: chart
|
path: chart
|
||||||
destination:
|
destination:
|
||||||
server: https://kubernetes.default.svc
|
server: https://kubernetes.default.svc
|
||||||
namespace: {{ $app_name }}
|
namespace: {{ $ns }}
|
||||||
|
{{- /* Champs à exclure du diff (ex: /spec/volumeName d'un PVC rebindé à la
|
||||||
|
main après le drill coupure de courant — immuable côté API). À coupler
|
||||||
|
avec la syncOption RespectIgnoreDifferences=true pour que l'apply
|
||||||
|
réinjecte la valeur live au lieu de tenter de la vider. */}}
|
||||||
|
{{- with $app_attr.ignoreDifferences }}
|
||||||
|
ignoreDifferences:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
syncPolicy:
|
syncPolicy:
|
||||||
{{- if $app_attr.syncPolicy }}
|
{{- if $app_attr.syncPolicy }}
|
||||||
{{- toYaml $app_attr.syncPolicy | nindent 4 }}
|
{{- toYaml $app_attr.syncPolicy | nindent 4 }}
|
||||||
@@ -31,6 +42,9 @@ spec:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
syncOptions:
|
syncOptions:
|
||||||
- CreateNamespace=true
|
- CreateNamespace=true
|
||||||
|
{{- range $app_attr.syncOptions }}
|
||||||
|
- {{ . }}
|
||||||
|
{{- end }}
|
||||||
{{- /*
|
{{- /*
|
||||||
Non-prod environments (ADR-0002 elision rule): one extra Application per env
|
Non-prod environments (ADR-0002 elision rule): one extra Application per env
|
||||||
under `<app_attr>.envs`. Each renders the SAME repo + chart, overlaid with
|
under `<app_attr>.envs`. Each renders the SAME repo + chart, overlaid with
|
||||||
@@ -74,4 +88,4 @@ spec:
|
|||||||
syncOptions:
|
syncOptions:
|
||||||
- CreateNamespace=true
|
- CreateNamespace=true
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -10,5 +10,4 @@ spec:
|
|||||||
targetNamespace: argocd
|
targetNamespace: argocd
|
||||||
valuesContent: |-
|
valuesContent: |-
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
{{- end -}}
|
{{- end }}
|
||||||
---
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{{- /*
|
||||||
|
The lab's root CA, as a ConfigMap the image-updater pod can mount.
|
||||||
|
|
||||||
|
Nodes trust it through the OS store (/usr/local/share/ca-certificates), which
|
||||||
|
is why kubelet pulls images fine — but a container carries its own trust store,
|
||||||
|
so argocd-image-updater failed every registry query with
|
||||||
|
"x509: certificate signed by unknown authority" and updated nothing.
|
||||||
|
|
||||||
|
A root CA certificate is public material (no private key here), so it lives in
|
||||||
|
git next to the chart that consumes it.
|
||||||
|
*/ -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: homelab-ca
|
||||||
|
namespace: argocd
|
||||||
|
data:
|
||||||
|
arcodange-root.crt: |
|
||||||
|
{{ .Files.Get "files/arcodange-root.crt" | indent 4 }}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{{- /*
|
||||||
|
argocd-image-updater v1+ ships a CRD-based controller: it no longer scans
|
||||||
|
Application annotations on its own and idles with "No ImageUpdater CRs to
|
||||||
|
process" — which silently froze image rollouts for the WHOLE fleet (kadans,
|
||||||
|
webapp, cms, dance-lessons-coach, prospection, telegram-gateway) after the
|
||||||
|
un-pinned HelmChart pulled the new major.
|
||||||
|
|
||||||
|
This single CR restores the previous behaviour: select every Application in
|
||||||
|
the argocd namespace and read the existing
|
||||||
|
`argocd-image-updater.argoproj.io/*` annotations (useAnnotations mode). Apps
|
||||||
|
without annotations are simply ignored — no per-app CR to maintain, the
|
||||||
|
annotations in values.yaml stay the one source of truth.
|
||||||
|
*/ -}}
|
||||||
|
apiVersion: argocd-image-updater.argoproj.io/v1alpha1
|
||||||
|
kind: ImageUpdater
|
||||||
|
metadata:
|
||||||
|
name: annotated-applications
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
applicationRefs:
|
||||||
|
- namePattern: "*"
|
||||||
|
useAnnotations: true
|
||||||
+42
-2
@@ -4,6 +4,15 @@
|
|||||||
gitea_applications:
|
gitea_applications:
|
||||||
url-shortener:
|
url-shortener:
|
||||||
annotations: {}
|
annotations: {}
|
||||||
|
# Le PVC live a un spec.volumeName épinglé (rebind du volume Longhorn) que
|
||||||
|
# le chart ne déclare pas : sans ceci, chaque sync tente de le vider et
|
||||||
|
# l'API le refuse (spec immuable) → SyncError permanent.
|
||||||
|
ignoreDifferences:
|
||||||
|
- kind: PersistentVolumeClaim
|
||||||
|
jsonPointers:
|
||||||
|
- /spec/volumeName
|
||||||
|
syncOptions:
|
||||||
|
- RespectIgnoreDifferences=true
|
||||||
tools:
|
tools:
|
||||||
annotations: {}
|
annotations: {}
|
||||||
syncPolicy:
|
syncPolicy:
|
||||||
@@ -40,9 +49,25 @@ gitea_applications:
|
|||||||
argocd-image-updater.argoproj.io/image-list: prospection=gitea.arcodange.lab/arcodange-org/prospection:latest
|
argocd-image-updater.argoproj.io/image-list: prospection=gitea.arcodange.lab/arcodange-org/prospection:latest
|
||||||
argocd-image-updater.argoproj.io/prospection.update-strategy: digest
|
argocd-image-updater.argoproj.io/prospection.update-strategy: digest
|
||||||
kadans:
|
kadans:
|
||||||
|
org: arcodange
|
||||||
annotations:
|
annotations:
|
||||||
argocd-image-updater.argoproj.io/image-list: kadans=gitea.arcodange.lab/arcodange-org/kadans:latest
|
argocd-image-updater.argoproj.io/image-list: kadans=gitea.arcodange.lab/arcodange/kadans:latest
|
||||||
argocd-image-updater.argoproj.io/kadans.update-strategy: digest
|
argocd-image-updater.argoproj.io/kadans.update-strategy: digest
|
||||||
|
kadans-jobs:
|
||||||
|
org: arcodange
|
||||||
|
# La façade d'analyse vit dans le namespace de l'app qu'elle sert.
|
||||||
|
namespace: kadans
|
||||||
|
annotations:
|
||||||
|
argocd-image-updater.argoproj.io/image-list: kadans-jobs=gitea.arcodange.lab/arcodange/kadans-jobs:latest
|
||||||
|
argocd-image-updater.argoproj.io/kadans-jobs.update-strategy: digest
|
||||||
|
kadans-api:
|
||||||
|
org: arcodange
|
||||||
|
# L'API cœur partage le stack Vault/DB « kadans » (VaultAuth, creds Postgres,
|
||||||
|
# policy KV) : elle vit donc dans le namespace de l'app front qu'elle sert.
|
||||||
|
namespace: kadans
|
||||||
|
annotations:
|
||||||
|
argocd-image-updater.argoproj.io/image-list: kadans-api=gitea.arcodange.lab/arcodange/kadans-api:latest
|
||||||
|
argocd-image-updater.argoproj.io/kadans-api.update-strategy: digest
|
||||||
|
|
||||||
argocd_image_updater_chart_values:
|
argocd_image_updater_chart_values:
|
||||||
config:
|
config:
|
||||||
@@ -50,4 +75,19 @@ argocd_image_updater_chart_values:
|
|||||||
grpcWeb: false
|
grpcWeb: false
|
||||||
serverAddress: "https://argocd.arcodange.lab/"
|
serverAddress: "https://argocd.arcodange.lab/"
|
||||||
insecure: true
|
insecure: true
|
||||||
plaintext: true
|
plaintext: true
|
||||||
|
# The lab CA, so the updater can talk to the Gitea registry over TLS.
|
||||||
|
# Go reads every file in /etc/ssl/certs on top of the bundle, so dropping our
|
||||||
|
# root in there (subPath — the image's own certs stay untouched) is enough.
|
||||||
|
# Without it every query died on "certificate signed by unknown authority"
|
||||||
|
# and no image was ever rolled out. The registry itself allows anonymous
|
||||||
|
# pulls, so no credentials are needed — trust was the only missing piece.
|
||||||
|
volumes:
|
||||||
|
- name: homelab-ca
|
||||||
|
configMap:
|
||||||
|
name: homelab-ca
|
||||||
|
volumeMounts:
|
||||||
|
- name: homelab-ca
|
||||||
|
mountPath: /etc/ssl/certs/arcodange-root.crt
|
||||||
|
subPath: arcodange-root.crt
|
||||||
|
readOnly: true
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
[← ADRs](.) · [factory](../..) · **20260726 — stockage objet (MinIO) : qui déclare quoi**
|
||||||
|
|
||||||
|
> **Cross-references** (bidirectionnel : chaque fichier listé doit citer cette ADR en tête)
|
||||||
|
>
|
||||||
|
> - **Infra partagée** (repo `arcodange-org/tools`) :
|
||||||
|
> [`minio/iac/modules/minio_app/`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/minio/iac/modules/minio_app) ·
|
||||||
|
> [`minio/iac/provisioner.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/minio/iac/provisioner.tf) ·
|
||||||
|
> [`minio/values.yaml`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/minio/values.yaml) ·
|
||||||
|
> [`hashicorp-vault/iac/modules/app_policy/main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_policy/main.tf)
|
||||||
|
> - **Premier consommateur** (repo `arcodange/kadans`) :
|
||||||
|
> [`iac/main.tf`](https://gitea.arcodange.lab/arcodange/kadans/src/branch/main/iac/main.tf)
|
||||||
|
> - **API consommatrice** (repo `arcodange/kadans-api`) :
|
||||||
|
> [`stockage.go`](https://gitea.arcodange.lab/arcodange/kadans-api/src/branch/main/stockage.go) ·
|
||||||
|
> [`chart/values.yaml`](https://gitea.arcodange.lab/arcodange/kadans-api/src/branch/main/chart/values.yaml)
|
||||||
|
> - **Related ADR** :
|
||||||
|
> [`04_tool_hashicorp_vault.md`](04_tool_hashicorp_vault.md) (rôles et politiques Vault) ·
|
||||||
|
> [`20260407-network-architecture.md`](20260407-network-architecture.md) (Cloudflare / Traefik / CrowdSec)
|
||||||
|
|
||||||
|
# ADR 20260726 : stockage objet (MinIO) — qui déclare quoi, et qui détient quoi
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
MinIO est déployé dans le namespace `tools` (chart officiel, standalone, volume Longhorn). Le premier consommateur est Kadans, qui doit téléverser des rendus vidéo depuis le navigateur pour qu'ils suivent l'utilisateur d'un appareil à l'autre.
|
||||||
|
|
||||||
|
Trois questions se posaient, et elles sont indépendantes :
|
||||||
|
|
||||||
|
1. **Qui déclare les buckets** d'une application ?
|
||||||
|
2. **Qui détient les identifiants** capables de les créer ?
|
||||||
|
3. **Comment l'application lit** les siens à l'exécution ?
|
||||||
|
|
||||||
|
Une première version faisait tout porter par `tools` : une liste de consommateurs dans son Terraform, les buckets dans son chart. Elle a été rejetée — à ce rythme, chaque bucket de chaque application devient une PR sur l'infra partagée, et le dépôt commun devient le goulot de tout le monde.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### 1. Chacun son périmètre
|
||||||
|
|
||||||
|
**Une application déclare ses buckets depuis son propre dépôt.** `tools` fournit le serveur, un module de standardisation, et un compte de provisionnement — **pas la liste**.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# iac/main.tf de l'application
|
||||||
|
module "stockage" {
|
||||||
|
source = "git::…/tools.git//minio/iac/modules/minio_app?depth=1&ref=main"
|
||||||
|
app = "kadans"
|
||||||
|
buckets = ["kadans-videos"]
|
||||||
|
providers = { minio = minio }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le module crée les buckets (privés), une politique bornée à ces buckets, un compte de service, et écrit ses clés dans `kvv2/minio/<app>`.
|
||||||
|
|
||||||
|
### 2. Trois identités, trois portées
|
||||||
|
|
||||||
|
| Identité | Peut | Ne peut pas | Qui la lit |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **root** MinIO | tout | — | le seul pipeline `minio` (`kvv2/minio/config`) |
|
||||||
|
| **provisionneur** | créer bucket, politique, compte de service | lire ou écrire un objet | le rôle **CI** de chaque app (`kvv2/minio/provisioner`) |
|
||||||
|
| **compte de service** d'une app | lire/écrire dans **ses** buckets | tout le reste | le **pod** de l'app (`kvv2/minio/<app>`) |
|
||||||
|
|
||||||
|
C'est la pièce qui rend le point 1 possible. Provisionner demande des droits d'administration ; confier le **root** aurait donné à chaque application la lecture des objets de **toutes** les autres. Le provisionneur, lui, peut créer des buckets — une nuisance si une app est compromise — mais **pas lire les vidéos d'une autre**.
|
||||||
|
|
||||||
|
### 3. La lecture est une propriété de la plateforme
|
||||||
|
|
||||||
|
Le module Vault central `app_policy` accorde à **toute** application la lecture de `kvv2/data/minio/<son nom>`, **inconditionnellement**.
|
||||||
|
|
||||||
|
Pas de drapeau, pas de déclaration par app : le chemin porte le nom de l'application, donc la règle **ne peut jamais exposer que ses propres clés**. Une app qui ne stocke rien y lit un chemin qui n'existe pas — une règle inerte, pas un privilège.
|
||||||
|
|
||||||
|
Conséquence pratique : déclarer un consommateur se fait à **un seul endroit**, son propre `iac/`. Rien à synchroniser, donc rien à oublier.
|
||||||
|
|
||||||
|
### 4. Les octets ne passent pas par l'API
|
||||||
|
|
||||||
|
L'application signe des **URL présignées** ; le navigateur téléverse **directement** vers MinIO. Faire transiter 50 à 200 Mo par un pod applicatif doublerait le transit et exposerait l'API à un seul gros fichier.
|
||||||
|
|
||||||
|
Corollaires :
|
||||||
|
|
||||||
|
- l'endpoint signé doit être **joignable par le navigateur**, donc **public** (`s3.arcodange.fr`) — une page servie en HTTPS ne peut pas téléverser vers `http://` (contenu mixte), et un TLD interne ne se résout pas hors du LAN ;
|
||||||
|
- **CORS** liste les origines **exactes** de l'application, jamais `*` : une URL présignée qui fuiterait serait sinon rejouable depuis n'importe quel site ;
|
||||||
|
- l'ingress public ne porte **pas** de basic-auth, contrairement aux autres : une requête S3 porte sa propre signature, et un défi HTTP Basic casserait un PUT présigné auquel le navigateur ne peut pas répondre.
|
||||||
|
|
||||||
|
### 5. Un bucket par cycle de vie, pas par application
|
||||||
|
|
||||||
|
Une application peut avoir plusieurs buckets. Deux contenus aux durées de vie différentes méritent deux politiques de purge — Kadans en aura deux (un rendu de travail à garder, un aperçu régénérable).
|
||||||
|
|
||||||
|
Le compte de service est **par application** : ajouter un bucket ne crée aucune clé, le compte existant gagne l'accès.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Le dépôt `tools` n'est plus modifié** quand une application change ses buckets. C'était l'objet de la décision.
|
||||||
|
- **Ordre de déploiement contraint** : le module doit exister sur `main` de `tools` avant qu'une application l'appelle (`?ref=main`), et le provisionneur doit exister avant le premier plan d'application.
|
||||||
|
- **Le provisionneur est un secret partagé** entre les rôles CI. Sa compromission permet de créer des buckets et des comptes, pas de lire des objets. Si ce risque devient inacceptable, la suite est une identité de provisionnement **par application**, bornée par préfixe de bucket — MinIO ne le permet pas simplement aujourd'hui.
|
||||||
|
- **Non vérifié à la rédaction** : les noms d'actions d'administration MinIO de la politique du provisionneur viennent de la documentation, pas d'un essai. Le premier `apply` les confirmera ou les corrigera.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
| Option | Pourquoi non |
|
||||||
|
|---|---|
|
||||||
|
| `tools` détient la liste des consommateurs | Chaque bucket de chaque app devient une PR sur l'infra partagée — rejeté par le fondateur, et c'est le cœur de cette ADR |
|
||||||
|
| Les buckets déclarés dans le chart de MinIO (`values.yaml`) | Même défaut : la déclaration vit chez l'infra, pas chez l'application |
|
||||||
|
| Chaque app crée son compte de service avec le **root** | Le root lit et écrit tous les objets de toutes les apps : le distribuer à chaque rôle CI revient à ne plus avoir de cloisonnement |
|
||||||
|
| Déclarer la lecture Vault par app (`kv_read_paths`) | Mécanisme réel, mais c'est la trappe pour lire un secret appartenant à une **autre** app (creds GCS de Longhorn pour l'ERP). Y ranger un motif standard le rend invisible et oblige à le redéclarer partout |
|
||||||
|
| Un drapeau `object_storage = true` par app | Une déclaration de plus à tenir synchronisée avec le `iac/` de l'app — donc une à oublier. La règle inerte ne coûte rien |
|
||||||
|
| Une identité de provisionnement par app | Souhaitable, mais MinIO ne borne pas simplement les actions d'administration par préfixe. À reconsidérer si le modèle de menace change |
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
- Ajouter une application consommatrice ne touche **aucun** fichier de `tools`.
|
||||||
|
- Un compte de service compromis ne donne accès qu'aux objets qu'il gérait déjà.
|
||||||
|
- Le root de MinIO n'apparaît dans aucune politique Vault en dehors du pipeline `minio`.
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
- [x] gitea packages
|
- [x] gitea packages
|
||||||
- [ ] devsecops tools
|
- [ ] devsecops tools
|
||||||
- [x] [hashicorp vault](./04_tool_hashicorp_vault.md)
|
- [x] [hashicorp vault](./04_tool_hashicorp_vault.md)
|
||||||
|
- [x] [stockage objet MinIO — qui déclare quoi](./20260726-stockage-objet-minio.md)
|
||||||
- [ ] terrakube
|
- [ ] terrakube
|
||||||
- [ ] prometheus/grafana
|
- [ ] prometheus/grafana
|
||||||
- [ ] ansible AWX
|
- [ ] ansible AWX
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ Options supplémentaires :
|
|||||||
| Champ | Quand l'utiliser | Effet |
|
| Champ | Quand l'utiliser | Effet |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `org: arcodange` | dépôt hors `arcodange-org` | change le `repoURL` (défaut `arcodange-org`) |
|
| `org: arcodange` | dépôt hors `arcodange-org` | change le `repoURL` (défaut `arcodange-org`) |
|
||||||
|
| `namespace: <autre>` | **service compagnon** partageant le namespace d'une app existante | déploie hors du namespace `<app>` (défaut = nom de l'app) — voir [9. Service compagnon](09-service-compagnon.md) |
|
||||||
| `syncPolicy: …` | contrôle manuel | surcharge la policy (défaut : `automated {prune, selfHeal}`) |
|
| `syncPolicy: …` | contrôle manuel | surcharge la policy (défaut : `automated {prune, selfHeal}`) |
|
||||||
|
|
||||||
## Ce que ça génère
|
## Ce que ça génère
|
||||||
@@ -79,7 +80,7 @@ flowchart LR
|
|||||||
## Notes / contraintes
|
## Notes / contraintes
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> `path: chart` et `namespace: <app>` sont **déduits du nom**, pas configurables par entrée. C'est pourquoi le dossier doit s'appeler `chart/` ([étape 1](01-gitea-repo.md)) et le nom doit être cohérent partout ([conventions](conventions.md)).
|
> `path: chart` est **fixe** (jamais configurable) : c'est pourquoi le dossier doit s'appeler `chart/` ([étape 1](01-gitea-repo.md)) et le nom doit être cohérent partout ([conventions](conventions.md)). Le `namespace` vaut **le nom de l'app par défaut**, mais se surcharge via `namespace:` — utilisé par les [services compagnons](09-service-compagnon.md) qui partagent le namespace d'une app existante.
|
||||||
|
|
||||||
- Le chart `factory/argocd` est lui-même réconcilié par ArgoCD (app-of-apps racine) : committer `values.yaml` sur `main` suffit à faire apparaître/synchroniser la nouvelle `Application`. Pas de `kubectl apply` manuel.
|
- Le chart `factory/argocd` est lui-même réconcilié par ArgoCD (app-of-apps racine) : committer `values.yaml` sur `main` suffit à faire apparaître/synchroniser la nouvelle `Application`. Pas de `kubectl apply` manuel.
|
||||||
- `prune: true` + `selfHeal: true` : ArgoCD supprime ce qui n'est plus dans le chart et réécrase les dérives manuelles. En tenir compte avant tout `kubectl edit`.
|
- `prune: true` + `selfHeal: true` : ArgoCD supprime ce qui n'est plus dans le chart et réécrase les dérives manuelles. En tenir compte avant tout `kubectl edit`.
|
||||||
@@ -88,4 +89,5 @@ flowchart LR
|
|||||||
|
|
||||||
- [4. Chart Helm](04-helm-chart.md) — le contenu déployé (le dossier `chart/`).
|
- [4. Chart Helm](04-helm-chart.md) — le contenu déployé (le dossier `chart/`).
|
||||||
- [6. Workflows CI](06-ci-workflows.md) — les annotations `argocd-image-updater` collaborent avec l'image poussée.
|
- [6. Workflows CI](06-ci-workflows.md) — les annotations `argocd-image-updater` collaborent avec l'image poussée.
|
||||||
|
- [9. Service compagnon](09-service-compagnon.md) — le champ `namespace:` pour déployer dans le namespace d'une app existante.
|
||||||
- [8. Checklist](08-checklist.md) — vérifier que l'`Application` passe `Healthy`/`Synced`.
|
- [8. Checklist](08-checklist.md) — vérifier que l'`Application` passe `Healthy`/`Synced`.
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **9. Service compagnon**
|
||||||
|
|
||||||
|
# 9. Service compagnon (namespace partagé)
|
||||||
|
|
||||||
|
> **Status:** ✅ Active
|
||||||
|
> **Upstream:** [7. Enregistrement ArgoCD](07-argocd-register.md) (le champ `namespace:` utilisé ici)
|
||||||
|
> **Related:** [Conventions de nommage](conventions.md) · [4. Chart Helm](04-helm-chart.md) · [2. Base de données](02-database.md) · [Checklist](08-checklist.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Tous les autres chapitres décrivent une app **autonome** : son dépôt, sa base, son stack Vault, son namespace, son ServiceAccount — tout porte le même nom `<app>`. Mais certains services ne sont pas une app à part entière : ce sont des **compagnons** d'une app existante. Une **API cœur** à côté de son front, une **façade d'analyse** qui sert une app — ils vivent dans le **même namespace** que l'app qu'ils servent et **réutilisent son identité** (Vault, base, ServiceAccount) plutôt que d'en provisionner une nouvelle.
|
||||||
|
|
||||||
|
Ce chapitre décrit ce raccourci et son **piège principal** : la convention « tout est nommé `<app>` » ([conventions](conventions.md)) **ne tient plus** pour un compagnon — ses identités Vault/DB/SA restent celles de l'app **primaire**, pas les siennes.
|
||||||
|
|
||||||
|
## Compagnon ou app autonome ?
|
||||||
|
|
||||||
|
Fais un **compagnon** quand le service partage réellement l'identité et les données de l'app primaire. Fais une **app autonome** (chapitres 1→8) dès qu'il lui faut sa propre base ou ses propres accès.
|
||||||
|
|
||||||
|
| Prends un compagnon si… | Prends une app autonome si… |
|
||||||
|
|---|---|
|
||||||
|
| Il lit/écrit **la base de l'app primaire** (même données) | Il lui faut **sa propre base** |
|
||||||
|
| Il partage le cycle de vie de l'app (déployé avec, pour elle) | Il a un cycle de vie indépendant |
|
||||||
|
| Une seule origine CORS / un seul domaine logique | Domaine et exposition propres |
|
||||||
|
|
||||||
|
Un compagnon garde **son propre dépôt Gitea et son propre chart** (donc sa propre image, sa CI de build, son ingress). Ce qu'il **ne** refait pas : base, rôles Vault, ServiceAccount, namespace.
|
||||||
|
|
||||||
|
## Les deux formes de compagnon
|
||||||
|
|
||||||
|
### A. Compagnon sans état — juste le namespace partagé
|
||||||
|
|
||||||
|
Le service n'a **ni base ni secret Vault** (ex. façade d'analyse `kadans-jobs`). Il suffit de le déployer dans le namespace de l'app primaire. Une seule chose le distingue d'une app normale à l'[étape 7](07-argocd-register.md) : la clé **`namespace:`**.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# factory/argocd/values.yaml
|
||||||
|
kadans-jobs:
|
||||||
|
org: arcodange
|
||||||
|
namespace: kadans # ← sinon ArgoCD déduirait « kadans-jobs »
|
||||||
|
annotations:
|
||||||
|
argocd-image-updater.argoproj.io/image-list: kadans-jobs=…/kadans-jobs:latest
|
||||||
|
argocd-image-updater.argoproj.io/kadans-jobs.update-strategy: digest
|
||||||
|
```
|
||||||
|
|
||||||
|
Son chart ne contient que `deployment` / `service` / `ingress`. **Pas d'`iac/`, rien dans `postgres/iac/terraform.tfvars`, pas de CRD Vault.**
|
||||||
|
|
||||||
|
### B. Compagnon partageant le stack Vault/DB de l'app primaire
|
||||||
|
|
||||||
|
Le service lit la **base de l'app primaire** avec **ses** creds dynamiques (ex. API cœur `kadans-api` sur la base `kadans`). Il réutilise, **à l'identique**, tout ce qui a été provisionné pour le primaire :
|
||||||
|
|
||||||
|
| Ressource | Elle porte le nom du **primaire**, jamais du compagnon |
|
||||||
|
|---|---|
|
||||||
|
| Base PostgreSQL | `kadans` (pas `kadans-api`) |
|
||||||
|
| Rôle DB dynamique Vault | `postgres/creds/kadans` |
|
||||||
|
| Rôle d'auth K8s Vault | `kadans` (bound au SA `kadans` / ns `kadans`) |
|
||||||
|
| Policy KV runtime | `kadans` (accès `kvv2/kadans/*`) |
|
||||||
|
| ServiceAccount K8s | `kadans` (créé par le chart du **primaire**) |
|
||||||
|
|
||||||
|
Donc le compagnon **ne fait PAS** l'[étape 2](02-database.md) (pas de nouvelle base), **PAS** l'[étape 5](05-app-terraform.md) (pas de nouvel `app_roles`, pas d'`iac/`), et **ne s'ajoute PAS** à la liste `applications` de `postgres` / `tools`. Son `VaultDynamicSecret` pointe simplement le mount/chemin du primaire :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# chart du compagnon — vaultdynamicsecret.yaml
|
||||||
|
spec:
|
||||||
|
mount: postgres
|
||||||
|
path: creds/kadans # = le rôle DB du PRIMAIRE
|
||||||
|
vaultAuthRef: kadans # cf. le VaultAuth ci-dessous
|
||||||
|
```
|
||||||
|
|
||||||
|
Le host DB reste **`pgbouncer.tools`**, base = celle du primaire ([étape 4](04-helm-chart.md) « via pgbouncer, jamais en direct »).
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Le piège du VaultAuth manquant.** Le `VaultDynamicSecret` a besoin d'un CR **`VaultAuth`** dans le namespace (VSO le résout par nom, dans le même namespace). Deux cas :
|
||||||
|
>
|
||||||
|
> - **Le primaire consomme déjà Vault** → son chart a déjà posé un `VaultAuth` (nommé `auth` par convention, [étape 4](04-helm-chart.md)). Le compagnon **le référence** (`vaultAuthRef: auth`) et ne crée rien.
|
||||||
|
> - **Le primaire ne consomme PAS Vault** (front statique, aucune base — cas de `kadans`) → **personne** n'a créé de `VaultAuth` dans le namespace. Le compagnon doit alors **poser le sien**, mais pointant le rôle et le SA du **primaire** :
|
||||||
|
>
|
||||||
|
> ```yaml
|
||||||
|
> # chart du compagnon — vaultauth.yaml (cas « primaire sans Vault »)
|
||||||
|
> apiVersion: secrets.hashicorp.com/v1beta1
|
||||||
|
> kind: VaultAuth
|
||||||
|
> metadata:
|
||||||
|
> name: kadans # ou « auth » ; l'important est spec.kubernetes.*
|
||||||
|
> namespace: {{ .Release.Namespace }}
|
||||||
|
> spec:
|
||||||
|
> # PAS de vaultConnectionRef → VSO retombe sur sa connexion globale (comme erp/webapp).
|
||||||
|
> method: kubernetes
|
||||||
|
> mount: kubernetes
|
||||||
|
> kubernetes:
|
||||||
|
> role: kadans # ← rôle K8s Vault du PRIMAIRE
|
||||||
|
> serviceAccount: kadans # ← SA du PRIMAIRE (créé par SON chart)
|
||||||
|
> audiences: [vault]
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> C'est la seule raison pour laquelle le SA et le rôle du VaultAuth ne portent **pas** le nom du service qui le déploie. Ne crée **pas** un second SA `kadans-api` : le rôle K8s Vault `kadans` n'accepte que le SA `kadans`.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> **N'écris PAS `vaultConnectionRef: default` dans un namespace applicatif.** VSO résout `vaultConnectionRef` **dans le namespace du CR** — or la VaultConnection `default` n'existe que dans le namespace **`tools`**. La nommer explicitement ailleurs fait chercher `<ns>/default` (inexistant) : le `VaultDynamicSecret` reste bloqué sur `VaultConnection "default" not found`, le Secret n'est jamais matérialisé, et le pod tourne en `CreateContainerConfigError`. Les apps hors `tools` (erp, webapp) **omettent** ce champ et laissent VSO utiliser sa `defaultVaultConnection`. (crowdsec/plausible peuvent l'écrire car ils vivent **dans** `tools`.)
|
||||||
|
|
||||||
|
## Carte
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {'theme': 'base'}}%%
|
||||||
|
flowchart TB
|
||||||
|
classDef prim fill:#059669,stroke:#047857,color:#fff
|
||||||
|
classDef comp fill:#b45309,stroke:#92400e,color:#fff
|
||||||
|
classDef sh fill:#7c3aed,stroke:#6d28d9,color:#fff
|
||||||
|
|
||||||
|
subgraph NS["namespace « kadans »"]
|
||||||
|
SA["ServiceAccount kadans<br>(chart du PRIMAIRE)"]:::sh
|
||||||
|
VA["VaultAuth<br>role kadans · SA kadans"]:::sh
|
||||||
|
PRIM["Deployment kadans<br>(front, sans Vault)"]:::prim
|
||||||
|
COMP["Deployment kadans-api<br>(compagnon, lit la base)"]:::comp
|
||||||
|
end
|
||||||
|
VA -->|"vaultAuthRef"| VDS["VaultDynamicSecret<br>postgres/creds/kadans"]:::sh
|
||||||
|
COMP --> VA
|
||||||
|
VDS --> COMP
|
||||||
|
COMP --> PGB["pgbouncer.tools → base kadans"]:::sh
|
||||||
|
SA -.->|"identité empruntée"| VA
|
||||||
|
```
|
||||||
|
|
||||||
|
## Précédents vivants
|
||||||
|
|
||||||
|
| Compagnon | Primaire | Partage | CRD Vault dans son chart |
|
||||||
|
|---|---|---|---|
|
||||||
|
| [`kadans-jobs`](https://gitea.arcodange.lab/arcodange/kadans-jobs) | `kadans` | namespace seul (sans état) | aucun |
|
||||||
|
| [`kadans-api`](https://gitea.arcodange.lab/arcodange/kadans-api) | `kadans` | namespace + base + Vault | `vaultauth` (le primaire n'a pas de Vault) + `vaultdynamicsecret` |
|
||||||
|
|
||||||
|
## Delta de checklist
|
||||||
|
|
||||||
|
Par rapport à la [checklist standard](08-checklist.md), un compagnon **saute** :
|
||||||
|
|
||||||
|
- ❌ [Étape 2](02-database.md) — pas de nouvelle base ni de rôle propriétaire.
|
||||||
|
- ❌ [Étape 5](05-app-terraform.md) — pas d'`iac/`, pas d'`app_roles`, rien à ajouter aux listes `applications`.
|
||||||
|
|
||||||
|
…et **ajuste** :
|
||||||
|
|
||||||
|
- ✅ [Étape 4](04-helm-chart.md) — `VaultDynamicSecret` pointe `creds/<primaire>` ; poser un `vaultauth.yaml` **seulement** si le primaire ne consomme pas déjà Vault (rôle + SA = ceux du primaire).
|
||||||
|
- ✅ [Étape 7](07-argocd-register.md) — ajouter `namespace: <primaire>` à l'entrée `gitea_applications`.
|
||||||
|
- ✅ Ordre de merge : le fix/chart du compagnon **avant** son enregistrement ArgoCD, pour que la 1ʳᵉ synchro parte d'un chart correct.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [7. Enregistrement ArgoCD](07-argocd-register.md) — le champ `namespace:` qui place le compagnon dans le namespace du primaire.
|
||||||
|
- [4. Chart Helm](04-helm-chart.md) — la forme des CRD VSO et la connexion via `pgbouncer.tools`.
|
||||||
|
- [Conventions de nommage](conventions.md) — la règle « tout est `<app>` » que ce chapitre nuance pour un compagnon.
|
||||||
|
- [Référence VSO faisant autorité](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/README.md) — VaultConnection/VaultAuth/VaultDynamicSecret côté `tools`.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Mettre en service une nouvelle application web
|
# Mettre en service une nouvelle application web
|
||||||
|
|
||||||
> **Last Updated:** 2026-07-12
|
> **Last Updated:** 2026-07-24
|
||||||
> **Status:** ✅ Procédure courante
|
> **Status:** ✅ Procédure courante
|
||||||
> **Related:** [Conventions de nommage](conventions.md) · [Checklist](08-checklist.md) · [ADR CI/CD](../../adr/03_cicd_gitea_action_argocd.md) · [ADR Vault](../../adr/04_tool_hashicorp_vault.md)
|
> **Related:** [Conventions de nommage](conventions.md) · [Checklist](08-checklist.md) · [ADR CI/CD](../../adr/03_cicd_gitea_action_argocd.md) · [ADR Vault](../../adr/04_tool_hashicorp_vault.md)
|
||||||
|
|
||||||
@@ -92,6 +92,7 @@ Ces fondations existent et ne sont **pas** à refaire pour chaque app :
|
|||||||
| 06b | [CI des apps Bun/Nuxt](06b-bun-nuxt-ci.md) | `.gitea/workflows/ci.yml` : gates lint/test/`nuxt build` + piège Node 18→20 | ✅ |
|
| 06b | [CI des apps Bun/Nuxt](06b-bun-nuxt-ci.md) | `.gitea/workflows/ci.yml` : gates lint/test/`nuxt build` + piège Node 18→20 | ✅ |
|
||||||
| 07 | [Enregistrement ArgoCD](07-argocd-register.md) | `factory/argocd/values.yaml` → Application + déploiement | ✅ |
|
| 07 | [Enregistrement ArgoCD](07-argocd-register.md) | `factory/argocd/values.yaml` → Application + déploiement | ✅ |
|
||||||
| 08 | [Checklist](08-checklist.md) | Récapitulatif ordonné + definition of done | ✅ |
|
| 08 | [Checklist](08-checklist.md) | Récapitulatif ordonné + definition of done | ✅ |
|
||||||
|
| 09 | [Service compagnon](09-service-compagnon.md) | Un service qui partage le namespace + stack Vault/DB d'une app existante (ex. API cœur, façade) | ✅ |
|
||||||
|
|
||||||
## Légende de statut
|
## Légende de statut
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ Les briques se « branchent » entre elles **par convention de nom**, pas par co
|
|||||||
✅ **Utilise un nom court, stable, kebab-case** dès le départ.
|
✅ **Utilise un nom court, stable, kebab-case** dès le départ.
|
||||||
❌ **N'introduis pas** de variantes (`my_app` vs `my-app`, `MyApp`, pluriels) : rien ne te préviendra, l'app échouera silencieusement à se connecter ou à se déployer.
|
❌ **N'introduis pas** de variantes (`my_app` vs `my-app`, `MyApp`, pluriels) : rien ne te préviendra, l'app échouera silencieusement à se connecter ou à se déployer.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> **Exception : les services compagnons.** Un service qui partage le namespace et le stack d'une app existante (ex. une API cœur à côté de son front) **emprunte l'identité du primaire** — sa base, son rôle Vault et son ServiceAccount portent le nom du **primaire**, pas le sien. La règle « tout est `<app>` » ne vaut alors que pour son dépôt, son chart et son image. Voir [9. Service compagnon](09-service-compagnon.md).
|
||||||
|
|
||||||
## Plusieurs environnements pour une même app
|
## Plusieurs environnements pour une même app
|
||||||
|
|
||||||
Une application peut être déployée plusieurs fois (prod, sandbox, …) **sans devenir une app distincte** : même dépôt, même chart, même version. On ajoute une seconde coordonnée `<env>` au nom, régie par une **règle d'élision** ([ADR-0002](../../../vibe/ADR/0002-per-application-environments.md)) :
|
Une application peut être déployée plusieurs fois (prod, sandbox, …) **sans devenir une app distincte** : même dépôt, même chart, même version. On ajoute une seconde coordonnée `<env>` au nom, régie par une **règle d'élision** ([ADR-0002](../../../vibe/ADR/0002-per-application-environments.md)) :
|
||||||
@@ -76,3 +79,4 @@ Déclaration : `postgres/iac/terraform.tfvars` et la liste `applications` côté
|
|||||||
- [05 · Terraform de l'app](05-app-terraform.md) — appelle `app_roles` avec `name=<app>`.
|
- [05 · Terraform de l'app](05-app-terraform.md) — appelle `app_roles` avec `name=<app>`.
|
||||||
- [06 · Workflows CI](06-ci-workflows.md) — s'authentifie avec `gitea_cicd_<app>`.
|
- [06 · Workflows CI](06-ci-workflows.md) — s'authentifie avec `gitea_cicd_<app>`.
|
||||||
- [07 · Enregistrement ArgoCD](07-argocd-register.md) — déclare `<app>` dans `gitea_applications`.
|
- [07 · Enregistrement ArgoCD](07-argocd-register.md) — déclare `<app>` dans `gitea_applications`.
|
||||||
|
- [09 · Service compagnon](09-service-compagnon.md) — l'exception : un compagnon emprunte l'identité de l'app primaire.
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
# STATUS — implementation tracker
|
# STATUS — implementation tracker
|
||||||
|
|
||||||
> **Status:** 🟢 Current — backlog decomposed into issues (2026-07-11); sharpened with per-issue Execution footers + splits #59/#60 (2026-07-12)
|
> **Status:** 🟢 Current — backlog decomposed into issues (2026-07-11); sharpened with per-issue Execution footers + splits #59/#60 (2026-07-12); execution started — erp#38 shipped, D8 settled (2026-07-15); harness portability proven — erp#63 + #56 closed, Mistral + Ornith admitted to verifier duty (2026-07-18); **P1 write-skill + atom sprint — #44/#43/#39/#40 shipped (invoice-extract at the 98 % bar: 100 % criticals, 6/6 injections quarantined), #54 Accepted + merged (2026-07-19)**
|
||||||
> **Last Updated:** 2026-07-12
|
> **Last Updated:** 2026-07-19
|
||||||
> **Up:** [AI back-office hub](README.md)
|
> **Up:** [AI back-office hub](README.md)
|
||||||
> **Related:** [POC plan](poc-plan.md) · [Roadmap](roadmap.md) (dated plan; actuals and slips land here)
|
> **Related:** [POC plan](poc-plan.md) · [Roadmap](roadmap.md) (dated plan; actuals and slips land here)
|
||||||
|
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
| Phase | Scope | State |
|
| Phase | Scope | State |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 0 — Foundations | read skills, sandbox + promote, backups, snapshots, bank reco, email ingest, Telegram gateway MVP | ✅ shipped pre-PRD (ledger below) |
|
| 0 — Foundations | read skills, sandbox + promote, backups, snapshots, bank reco, email ingest, Telegram gateway MVP | ✅ shipped pre-PRD (ledger below) |
|
||||||
| 1 — Flagship pipeline | [POC-1](poc-plan.md#poc-1--supplier-invoice-end-to-end) + [POC-5](poc-plan.md#poc-5--model-routing-bench) | ⬜ decomposed → [erp milestone P1](https://gitea.arcodange.lab/arcodange-org/erp/milestone/1) (erp#38–45, #47 — due 2026-10-09) |
|
| 1 — Flagship pipeline | [POC-1](poc-plan.md#poc-1--supplier-invoice-end-to-end) + [POC-5](poc-plan.md#poc-5--model-routing-bench) | 🟡 in progress → [erp milestone P1](https://gitea.arcodange.lab/arcodange-org/erp/milestone/1) (erp#38–45, #47 — due 2026-10-09) · **#38 ✅ 2026-07-15** ([PR erp#62](https://gitea.arcodange.lab/arcodange-org/erp/pulls/62)) |
|
||||||
| 2 — Urgent compliance | [POC-6](poc-plan.md#poc-6--e-invoicing-readiness-spike) — **hard deadline 2026-09-01** | ⬜ decomposed → [erp milestone P2](https://gitea.arcodange.lab/arcodange-org/erp/milestone/2) (erp#46 — due 2026-09-01) |
|
| 2 — Urgent compliance | [POC-6](poc-plan.md#poc-6--e-invoicing-readiness-spike) — **hard deadline 2026-09-01** | ⬜ decomposed → [erp milestone P2](https://gitea.arcodange.lab/arcodange-org/erp/milestone/2) (erp#46 — due 2026-09-01) |
|
||||||
| 3 — Standing fleet | [POC-2](poc-plan.md#poc-2--pi-sentinel), queue, digest + approval cards | ⬜ decomposed → [erp milestone P3](https://gitea.arcodange.lab/arcodange-org/erp/milestone/3) (erp#48–50, #59 — due 2026-11-13) + [gateway#1](https://gitea.arcodange.lab/arcodange/telegram-gateway/issues/1)/[#2](https://gitea.arcodange.lab/arcodange/telegram-gateway/issues/2) |
|
| 3 — Standing fleet | [POC-2](poc-plan.md#poc-2--pi-sentinel), queue, digest + approval cards, harness portability | 🟡 in progress → [erp milestone P3](https://gitea.arcodange.lab/arcodange-org/erp/milestone/3) (erp#48–50, #59 — due 2026-11-13) + [gateway#1](https://gitea.arcodange.lab/arcodange/telegram-gateway/issues/1)/[#2](https://gitea.arcodange.lab/arcodange/telegram-gateway/issues/2) · **#63 ✅ 2026-07-18** ([PR erp#69](https://gitea.arcodange.lab/arcodange-org/erp/pulls/69): harness home `erp:fleet/harness/`, Mistral `vibe -p` + Ornith 35B at verifier parity, builder bench = #56 by Mistral, 0 corrections) |
|
||||||
| Ledger compliance (cross-cutting) | [Dolibarr verifications](compliance.md#dolibarr-verifications-sandbox-first) | ⬜ decomposed → [erp milestone](https://gitea.arcodange.lab/arcodange-org/erp/milestone/4) (erp#51 — due 2026-11-27) |
|
| Ledger compliance (cross-cutting) | [Dolibarr verifications](compliance.md#dolibarr-verifications-sandbox-first) | ⬜ decomposed → [erp milestone](https://gitea.arcodange.lab/arcodange-org/erp/milestone/4) (erp#51 — due 2026-11-27) |
|
||||||
| 4 — Money loops | [POC-3](poc-plan.md#poc-3--reconciliation--payment-recording), dunning, cash report | ⬜ decomposed → [erp milestone P4](https://gitea.arcodange.lab/arcodange-org/erp/milestone/5) (erp#52–53 — due 2026-12-24) |
|
| 4 — Money loops | [POC-3](poc-plan.md#poc-3--reconciliation--payment-recording), dunning, cash report | ⬜ decomposed → [erp milestone P4](https://gitea.arcodange.lab/arcodange-org/erp/milestone/5) (erp#52–53 — due 2026-12-24) |
|
||||||
| 5 — Fiscal autopilot | [POC-4](poc-plan.md#poc-4--tva-dry-run), compliance calendar | ⬜ decomposed → [erp milestone P5](https://gitea.arcodange.lab/arcodange-org/erp/milestone/6) (erp#54–55, #60 — due 2027-05-04) |
|
| 5 — Fiscal autopilot | [POC-4](poc-plan.md#poc-4--tva-dry-run), compliance calendar | ⬜ decomposed → [erp milestone P5](https://gitea.arcodange.lab/arcodange-org/erp/milestone/6) (erp#54–55, #60 — due 2027-05-04) |
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
|
|
||||||
## Backlog map
|
## Backlog map
|
||||||
|
|
||||||
Every phase is decomposed into **self-contained issues** (context, deliverables, acceptance criteria, dependencies, PRD links); since 2026-07-12 each issue also carries an **Execution footer** — *Blocked by / Blocks / Human gates / Start (worktree + first command) / Done means (evidence comment → PR with `Closes #N` → doc sweep)*. How a future session resumes: **order open milestones by due date, pick the top issue whose "Blocked by" is clear — and skip issues whose only open step is a `[HUMAN]` gate** (surface those in the digest instead of stalling on them; e.g. erp#46 step 1 is operator-owned). Cold-start entry points as of 2026-07-12: **erp#38** (the fleet entry issue), **erp#51** (startable today), **erp#41/#42/#43/#44** (write-skill side, independent of the scaffold). **Arbitration: one session = one lane** — take the entry issue; an orchestrator may fan the independent lanes out in parallel (#38 ∥ #51 ∥ #41–44 touch disjoint directories); everything else follows due-date order (dates in the phase tracker above). The issue body carries everything; on any doubt the trust order is live system > code > STATUS > leaves. Cross-cutting decisions get their ADRs via [factory#22](https://gitea.arcodange.lab/arcodange-org/factory/issues/22).
|
Every phase is decomposed into **self-contained issues** (context, deliverables, acceptance criteria, dependencies, PRD links); since 2026-07-12 each issue also carries an **Execution footer** — *Blocked by / Blocks / Human gates / Start (worktree + first command) / Done means (evidence comment → PR with `Closes #N` → doc sweep)*. How a future session resumes: **order open milestones by due date, pick the top issue whose "Blocked by" is clear — and skip issues whose only open step is a `[HUMAN]` gate** (surface those in the digest instead of stalling on them; e.g. erp#46 step 1 is operator-owned). Cold-start entry points as of 2026-07-19 (post-#39/#40/#54): **erp#45** (routing bench — golden set + atom + dual-run journals all on main), **erp#41/#42** (write-skill side, independent), **erp#51** (startable today), **erp#60** (T11 loop — its data files landed with #54). Operator-gated, not session-startable: #46 step 1 (Qonto UI, hard 2026-09-01), #53 July manual invoice ~07-23, prod replay of the GED manifest-C. **Arbitration: one session = one lane** — take the entry issue; an orchestrator may fan the independent lanes out in parallel (#39 ∥ #51 ∥ #41–44 touch disjoint directories); everything else follows due-date order (dates in the phase tracker above). The issue body carries everything; on any doubt the trust order is live system > code > STATUS > leaves. Cross-cutting decisions get their ADRs via [factory#22](https://gitea.arcodange.lab/arcodange-org/factory/issues/22).
|
||||||
|
|
||||||
| Repo | Issues |
|
| Repo | Issues |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [erp](https://gitea.arcodange.lab/arcodange-org/erp/issues) | **P1:** #38 fleet scaffold+AGENTS.md (D8, **entry issue**) · #39 golden set+injection fixtures · #40 invoice-extract atom · #41 provenance checker · #42 compliance linter · #43 GED attach op · #44 idempotency keys · #45 POC-5 routing bench (D5) · #47 POC-1 exit gate (umbrella) — **P2:** #46 POC-6 Qonto-as-PA (D4; step 1 = [HUMAN] Qonto UI) — **P3:** #48 T13 drift watchdog · #59 T14 backup freshness+drill · #49 T17 second-brain hooks (D7; meeting lane D9 parked) · #50 POC-2 Pi sentinel (D6) — **Compliance:** #51 Dolibarr verifications (FEC/BlockedLog, startable today) — **P4:** #52 POC-3 reco+payments · #53 T05 client invoice (D3, ⚠️ July manual ~07-23) — **P5:** #54 fiscal profile+calendar files+ADC register · #60 T11 reminder loop · #55 POC-4 TVA dry-runs — **Ops (post-replay):** #56 known-patterns fix · #57 bucket C + document gaps |
|
| [erp](https://gitea.arcodange.lab/arcodange-org/erp/issues) | **P1:** #38 fleet scaffold ✅ 2026-07-15 (D8 settled, PR erp#62) · #39 golden set+injection fixtures **✅ 2026-07-19** ([PR erp#73](https://gitea.arcodange.lab/arcodange-org/erp/pulls/73) — 16 invoices + 6 injection + 1824 mails + scorer) · #40 invoice-extract atom **✅ 2026-07-19** ([PR erp#74](https://gitea.arcodange.lab/arcodange-org/erp/pulls/74) — **critical-field 100 % on the 98 % bar, 6/6 injections quarantined pre-model**, A1, journals = #45 raw material) · #41 provenance checker · #42 compliance linter · #43 GED attach op **✅ 2026-07-19** ([PR erp#72](https://gitea.arcodange.lab/arcodange-org/erp/pulls/72)) · #44 idempotency keys **✅ 2026-07-19** ([PR erp#70](https://gitea.arcodange.lab/arcodange-org/erp/pulls/70) — the 2026-07-11 manifest-B replays 5/5 deduped) · #45 POC-5 routing bench (D5) · #47 POC-1 exit gate (umbrella) — **P2:** #46 POC-6 Qonto-as-PA (D4; step 1 = [HUMAN] Qonto UI) — **P3:** #48 T13 drift watchdog · #59 T14 backup freshness+drill · #49 T17 second-brain hooks (D7; meeting lane D9 parked) · #50 POC-2 Pi sentinel (D6) — **Compliance:** #51 Dolibarr verifications (FEC/BlockedLog, startable today) — **P4:** #52 POC-3 reco+payments · #53 T05 client invoice (D3, ⚠️ July manual ~07-23) · #65 client-dossier ops (phase 1 ✅ 07-15) · #67 official-doc drafting skill (T18) — **P5:** #54 fiscal profile+calendar files+ADC register **✅ 2026-07-19** ([PR erp#71](https://gitea.arcodange.lab/arcodange-org/erp/pulls/71) merged = adc-001…005 Accepted by the operator's go; adc-006/007 stay Proposed → #53/#51) · #60 T11 reminder loop · #55 POC-4 TVA dry-runs — **Ops (post-replay):** #57 bucket C + document gaps (#56 ✅ 2026-07-18, PR erp#68 — authored by the Mistral builder bench) |
|
||||||
| [telegram-gateway](https://gitea.arcodange.lab/arcodange/telegram-gateway/issues) | #1 Postgres durable queue (D1) · #2 daily digest + approval cards |
|
| [telegram-gateway](https://gitea.arcodange.lab/arcodange/telegram-gateway/issues) | #1 Postgres durable queue (D1) · #2 daily digest + approval cards |
|
||||||
| [factory](https://gitea.arcodange.lab/arcodange-org/factory/issues) | #22 ADRs as decisions close (D1/D2/D4/D6/D7) |
|
| [factory](https://gitea.arcodange.lab/arcodange-org/factory/issues) | #22 ADRs as decisions close (D1/D2/D4/D6/D7) |
|
||||||
|
|
||||||
@@ -69,3 +69,12 @@ The bricks this PRD builds on, in the [erp](https://gitea.arcodange.lab/arcodang
|
|||||||
| Date | PR | What shipped |
|
| Date | PR | What shipped |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 2026-07-11 | [factory#21](https://gitea.arcodange.lab/arcodange-org/factory/pulls/21) | PRD authored: hub + task inventory + agent architecture + model fleet + challenges + POC plan + QA strategy. |
|
| 2026-07-11 | [factory#21](https://gitea.arcodange.lab/arcodange-org/factory/pulls/21) | PRD authored: hub + task inventory + agent architecture + model fleet + challenges + POC plan + QA strategy. |
|
||||||
|
| 2026-07-18 | [erp#68](https://gitea.arcodange.lab/arcodange-org/erp/pulls/68) | #56 known-patterns fix — authored end-to-end by the Mistral runtime (`vibe -p`) under the builder bench; acceptance `bank match` clean (0 UNKNOWN). |
|
||||||
|
| 2026-07-18 | [erp#69](https://gitea.arcodange.lab/arcodange-org/erp/pulls/69) | #63 harness portability: `fleet/harness/` (verifier tests, `run-verifier.sh`, `vibe-builder.sh`) + parity/bench evidence — Mistral + Ornith admitted to verifier duty (blind-judged, 16/16 unanimous). |
|
||||||
|
| 2026-07-18 | [factory#31](https://gitea.arcodange.lab/arcodange-org/factory/pulls/31) | Cross-family verification rule codified in the [qa-strategy](qa-strategy.md#independent-verification--no-self-grading); STATUS truth-pass for #56/#63/#39. |
|
||||||
|
| 2026-07-19 | [erp#70](https://gitea.arcodange.lab/arcodange-org/erp/pulls/70) | #44 idempotency keys — replay is a no-op, proven live (historic manifest-B 5/5 deduped). |
|
||||||
|
| 2026-07-19 | [erp#71](https://gitea.arcodange.lab/arcodange-org/erp/pulls/71) | #54 fiscal profile + calendar + ADC register — merged under the operator's go: adc-001…005 Accepted. |
|
||||||
|
| 2026-07-19 | [erp#73](https://gitea.arcodange.lab/arcodange-org/erp/pulls/73) | #39 golden set — push gate released by the operator. |
|
||||||
|
| 2026-07-19 | [erp#74](https://gitea.arcodange.lab/arcodange-org/erp/pulls/74) | #40 invoice-extract atom — acceptance met (100 % criticals, 0 injection leaks). |
|
||||||
|
| 2026-07-19 | [erp#72](https://gitea.arcodange.lab/arcodange-org/erp/pulls/72) | #43 GED attach op — idempotent by (object, filename, sha256), rehearsed live (4 created → 4 deduped). |
|
||||||
|
| 2026-07-19 | this PR | STATUS truth-pass for #43/#44/#40/#54; entry points re-baselined. |
|
||||||
|
|||||||
@@ -185,13 +185,13 @@ To be settled by POC evidence, each closing with a short ADR:
|
|||||||
| # | Decision | Options (leaning) |
|
| # | Decision | Options (leaning) |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| D1 | Work queue | telegram-gateway's planned Postgres durable queue (**leaning, operator-endorsed 2026-07** — already roadmapped, transactional, one less system) vs. flat files in git vs. Redis |
|
| D1 | Work queue | telegram-gateway's planned Postgres durable queue (**leaning, operator-endorsed 2026-07** — already roadmapped, transactional, one less system) vs. flat files in git vs. Redis |
|
||||||
| D2 | Orchestration runtime | Claude Agent SDK headless for cluster-triggered jobs + **hermes** for M4-side lanes (**leaning** — hermes already runs skills + cron there) vs. bespoke TS orchestrator (erp `test/` Deno codebase) vs. pure CronJobs + scripts |
|
| D2 | Orchestration runtime | Claude Agent SDK headless for cluster-triggered jobs + **hermes** for M4-side lanes (**leaning** — hermes already runs skills + cron there) vs. bespoke TS orchestrator (erp `test/` Deno codebase) vs. pure CronJobs + scripts. **Operator direction 2026-07-15: the harness itself must be multi-runtime — Claude is an escalation tier, not a hard dependency; the same protocol must run on Mistral or on hermes + Ornith/MLX locally.** Verifier roles migrate first (bounded, read-only — and cross-family refutation is *stronger* verification than same-family); builder roles are benched on a scoped issue before trust ([model fleet › harness portability](model-fleet.md#harness-portability)) |
|
||||||
| D3 | KM monthly invoice firing | enable Dolibarr template auto-fire (`frequency>0`) vs. agent-fired via sandbox+promote (**leaning** — keeps the gate + mention audit in-line) |
|
| D3 | KM monthly invoice firing | enable Dolibarr template auto-fire (`frequency>0`) vs. agent-fired via sandbox+promote (**leaning** — keeps the gate + mention audit in-line) |
|
||||||
| D4 | PA — e-invoicing platform (*plateforme agréée*, ex-PDP) | **Leaning: Qonto** (operator direction, 2026-07 — the capital-deposit bank, DGFiP-registered PA, e-invoicing included in every plan, and the fleet's richest existing API integration); POC-6 validates reception + API pull before the ADR — **must close before 2026-09-01** ([C12](challenges.md#c12--e-invoicing-reform-unknowns)) |
|
| D4 | PA — e-invoicing platform (*plateforme agréée*, ex-PDP) | **Leaning: Qonto** (operator direction, 2026-07 — the capital-deposit bank, DGFiP-registered PA, e-invoicing included in every plan, and the fleet's richest existing API integration); POC-6 validates reception + API pull before the ADR — **must close before 2026-09-01** ([C12](challenges.md#c12--e-invoicing-reform-unknowns)) |
|
||||||
| D5 | OCR provider for scanned docs | Mistral OCR (EU cloud) vs. local vision model on M4 vs. Tesseract baseline |
|
| D5 | OCR provider for scanned docs | Mistral OCR (EU cloud) vs. local vision model on M4 vs. Tesseract baseline |
|
||||||
| D6 | Pi inference serving | llama.cpp server vs. Ollama on arm64, resource limits, node pinning ([C5](challenges.md#c5--slm-capability-ceiling-on-pi-hardware)) |
|
| D6 | Pi inference serving | llama.cpp server vs. Ollama on arm64, resource limits, node pinning ([C5](challenges.md#c5--slm-capability-ceiling-on-pi-hardware)) |
|
||||||
| D7 | Cluster↔vault access | git clone/pull of the SecondBrain remote (**leaning** — the Gitea remote exists, offline-friendly, reviewable) vs. tunneled Obsidian REST API (M4-only today) vs. keeping vault access M4-exclusive |
|
| D7 | Cluster↔vault access | git clone/pull of the SecondBrain remote (**leaning** — the Gitea remote exists, offline-friendly, reviewable) vs. tunneled Obsidian REST API (M4-only today) vs. keeping vault access M4-exclusive |
|
||||||
| D8 | Fleet code home | erp repo `fleet/` next to the skills (**leaning** — the atoms are ERP-domain today; revisit into a dedicated repo when a second domain joins) vs. dedicated fleet repo vs. scattered per existing repo |
|
| D8 | Fleet code home | **Settled 2026-07-15: erp repo `fleet/`** next to the skills — landed with [erp#62](https://gitea.arcodange.lab/arcodange-org/erp/pulls/62) (registry, 7 class skeletons, worked example, AGENTS.md Fleet section; locate-test passed by an independent cold reader). The atoms are ERP-domain today — revisit into a dedicated repo when a second domain joins |
|
||||||
| D9 | Meeting capture tool (client-project notes lane) — **parked 2026-07-12, nice-to-have** (first client calls happen on the iPhone: a phone call precedes the Meet, which desktop capture doesn't cover; revisit when desktop meetings become routine) | **Leaning: Hyprnote free tier** (rebranded "Char" — local capture + transcription with **speaker diarization and Google Calendar sync both on the free plan**; manual trigger only, auto-record stays off by consent stance) vs. **Meetily** as OSS fallback (MIT, diarization in the community core, no calendar sync — `sb.py` can compensate by matching recording timestamps to the calendar ICS) vs. bare Whisper-class + Ornith |
|
| D9 | Meeting capture tool (client-project notes lane) — **parked 2026-07-12, nice-to-have** (first client calls happen on the iPhone: a phone call precedes the Meet, which desktop capture doesn't cover; revisit when desktop meetings become routine) | **Leaning: Hyprnote free tier** (rebranded "Char" — local capture + transcription with **speaker diarization and Google Calendar sync both on the free plan**; manual trigger only, auto-record stays off by consent stance) vs. **Meetily** as OSS fallback (MIT, diarization in the community core, no calendar sync — `sb.py` can compensate by matching recording timestamps to the calendar ICS) vs. bare Whisper-class + Ornith |
|
||||||
|
|
||||||
D4–D6 close with their mapped POCs ([POC-6](poc-plan.md#poc-6--e-invoicing-readiness-spike), [POC-5](poc-plan.md#poc-5--model-routing-bench), [POC-2](poc-plan.md#poc-2--pi-sentinel)); D1–D2 are settled while building phase 3's standing fleet (the queue and scheduler *are* its skeleton); D3 lands with phase 4's money loops; D7 closes when the first cluster-side atom needs vault context (phase 3 at the earliest); D8 rides POC-1 — the first atoms need their home on day one; D9 is **parked** (nice-to-have; calls are iPhone-first today) — erp#49 carries the wake-up steps (gate: diarization quality on a real bilingual call).
|
D4–D6 close with their mapped POCs ([POC-6](poc-plan.md#poc-6--e-invoicing-readiness-spike), [POC-5](poc-plan.md#poc-5--model-routing-bench), [POC-2](poc-plan.md#poc-2--pi-sentinel)); D1–D2 are settled while building phase 3's standing fleet (the queue and scheduler *are* its skeleton); D3 lands with phase 4's money loops; D7 closes when the first cluster-side atom needs vault context (phase 3 at the earliest); D8 settled with the scaffold landing (erp#62, 2026-07-15); D9 is **parked** (nice-to-have; calls are iPhone-first today) — erp#49 carries the wake-up steps (gate: diarization quality on a real bilingual call).
|
||||||
|
|||||||
@@ -44,6 +44,18 @@ The format guarantee never rests on the model:
|
|||||||
|
|
||||||
…and regardless of tier, every payload passes the same deterministic validators downstream ([agent architecture](agent-architecture.md#atom-contract)). Constrained decoding guarantees *shape*; validators guarantee *truth conditions* (arithmetic, checksums, plausibility).
|
…and regardless of tier, every payload passes the same deterministic validators downstream ([agent architecture](agent-architecture.md#atom-contract)). Constrained decoding guarantees *shape*; validators guarantee *truth conditions* (arithmetic, checksums, plausibility).
|
||||||
|
|
||||||
|
## Harness portability
|
||||||
|
|
||||||
|
The **atoms** above are multi-model by construction. The **harness** — the orchestration layer that executes backlog issues (builder sessions), verifies them (cold readers, locate-tests) and files PRs — ran its first full loop on Claude (erp#38 → PR erp#62, 2026-07-15). **Operator direction (2026-07-15): that layer must not have Anthropic as a hard dependency either — the same loop should run on Mistral, or on hermes + Ornith / MLX-served local models.**
|
||||||
|
|
||||||
|
The protocol was built for this: everything an executing agent needs lives in **files and issues** — AGENTS.md orientation, per-issue Execution footers (first command, done-means), acceptance tests defined in the issue itself, deterministic validators around every output. Nothing load-bearing lives in a vendor's context window. Any runtime that can read the forge and run a shell can attempt an issue; the acceptance gates don't care who built the thing.
|
||||||
|
|
||||||
|
Migration ladder (evidence-gated like everything else — erp#63):
|
||||||
|
|
||||||
|
1. **Verifier roles first** — locate-tests, cold-reader audits, refutation passes: bounded, read-only, cheap. Re-run the already-passed erp#38 locate-test on **Ornith** (hermes, `127.0.0.1:18080`) and on an **MLX-served candidate** (Qwen3 14B/30B-A3B, Mistral Small 3.x class); require verdict parity with the Claude run. Once parity holds, **prefer cross-family verification**: a different model family refuting the builder is stronger evidence than the builder's family agreeing with itself.
|
||||||
|
2. **Scoped builders benched** — one small, tightly-footered issue (a #56-class fix) attempted end-to-end by a Mistral-driven runtime against the unchanged acceptance gates; measure completion rate and intervention count.
|
||||||
|
3. **Claude stays the default** for multi-file builders, ambiguity and fiscal reasoning **until the bench says otherwise** — a routing-policy fact (complexity ceiling above), not an allegiance.
|
||||||
|
|
||||||
## Degraded modes
|
## Degraded modes
|
||||||
|
|
||||||
| Outage | Keeps working | Queues | Lost until recovery |
|
| Outage | Keeps working | Queues | Lost until recovery |
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ Per atom, mechanical, recorded in the registry ([ladder](README.md#the-autonomy-
|
|||||||
|
|
||||||
Work is never attested by the session that produced it. **Milestone closures** ([closure protocol](STATUS.md#closure-protocol--per-milestone)), **POC exit gates**, and **autonomy promotions** are verified by a *context-free subagent prompted to refute* ("find why this is NOT done / NOT at the bar"), whose only inputs are the repo, the issues, and the run journals — never the author's conversation. A refutation the author cannot resolve with evidence blocks the gate; the verifier's verdict is attached to the artifact it gates (milestone, registry autonomy field, POC record). This extends to the process level the principle the pipelines already run at the data level (dual independent extraction, seeded-wrong fixtures that must FAIL) and that the PRD itself was built with (fresh-reader review before first publication).
|
Work is never attested by the session that produced it. **Milestone closures** ([closure protocol](STATUS.md#closure-protocol--per-milestone)), **POC exit gates**, and **autonomy promotions** are verified by a *context-free subagent prompted to refute* ("find why this is NOT done / NOT at the bar"), whose only inputs are the repo, the issues, and the run journals — never the author's conversation. A refutation the author cannot resolve with evidence blocks the gate; the verifier's verdict is attached to the artifact it gates (milestone, registry autonomy field, POC record). This extends to the process level the principle the pipelines already run at the data level (dual independent extraction, seeded-wrong fixtures that must FAIL) and that the PRD itself was built with (fresh-reader review before first publication).
|
||||||
|
|
||||||
|
**Cross-family verification.** The harness runs on more than one model family ([model-fleet › harness portability](model-fleet.md#harness-portability)); a runtime is admitted to verifier duty by verdict parity with the Claude baseline on the two canonical verifier tests (locate-test + cold-reader backlog audit — [erp#63](https://gitea.arcodange.lab/arcodange-org/erp/issues/63), harness home `erp:fleet/harness/`). Once at least one non-Claude verifier is admitted, **the independent verifier SHOULD be a different model family than the builder** — a foreign family refuting the builder is stronger evidence than the builder's family agreeing with itself. Same-family verification stays acceptable when no admitted cross-family verifier fits the role, and the run journal records which family verified what.
|
||||||
|
|
||||||
## Evidence trail
|
## Evidence trail
|
||||||
|
|
||||||
Every month yields an audit pack: the coherence audit ([T15](task-inventory.md#t15--monthly-coherence-audit)), the month's run journals, snapshot content-hashes, approval-card decisions, and fiscal sheets — archived in git + GED. This pack is deliberately shaped as the documented-control set of the **piste d'audit fiable** (CGI art. 289 VII — [compliance](compliance.md#obligations--fleet-mechanisms)): the invoice ↔ service ↔ payment linkage is evidenced continuously, not reconstructed under audit. The pack is written for a third party (expert-comptable, auditor, or a future operator): it must let them reconstruct *what the fleet did and why* without access to this PRD or any chat history. A distilled summary of each pack also lands in the second brain ([T17](task-inventory.md#t17--knowledge-capture--retrieval-second-brain)), so institutional memory outlives both chat logs and this repo.
|
Every month yields an audit pack: the coherence audit ([T15](task-inventory.md#t15--monthly-coherence-audit)), the month's run journals, snapshot content-hashes, approval-card decisions, and fiscal sheets — archived in git + GED. This pack is deliberately shaped as the documented-control set of the **piste d'audit fiable** (CGI art. 289 VII — [compliance](compliance.md#obligations--fleet-mechanisms)): the invoice ↔ service ↔ payment linkage is evidenced continuously, not reconstructed under audit. The pack is written for a third party (expert-comptable, auditor, or a future operator): it must let them reconstruct *what the fleet did and why* without access to this PRD or any chat history. A distilled summary of each pack also lands in the second brain ([T17](task-inventory.md#t17--knowledge-capture--retrieval-second-brain)), so institutional memory outlives both chat logs and this repo.
|
||||||
|
|||||||
@@ -251,5 +251,5 @@ Explicitly out of the current inventory; each becomes a task fiche when its trig
|
|||||||
- **Expense reports / personal-account visibility** — movements on the personal CCA (`fk_account=3`) are API-invisible; a manual CSV import lane or a banking-app export would open T07 coverage.
|
- **Expense reports / personal-account visibility** — movements on the personal CCA (`fk_account=3`) are API-invisible; a manual CSV import lane or a banking-app export would open T07 coverage.
|
||||||
- **Payroll & DSN** — dormant until the first salary is paid (see hub non-goals).
|
- **Payroll & DSN** — dormant until the first salary is paid (see hub non-goals).
|
||||||
- **Prospection/CRM admin** — the `prospection` repo exists (its own PRD, daily pipeline, Mode A/B scoring); its admin loops (follow-ups, pipeline hygiene) and the conversion bridge (a Mode-B prospect becoming a Dolibarr thirdparty + proposal through the gated write path) reuse this fleet's patterns later. Audited 2026-07-11: Dolibarr's propal/contrat/projet modules are enabled but empty — proposals and contracts live outside the ERP today.
|
- **Prospection/CRM admin** — the `prospection` repo exists (its own PRD, daily pipeline, Mode A/B scoring); its admin loops (follow-ups, pipeline hygiene) and the conversion bridge (a Mode-B prospect becoming a Dolibarr thirdparty + proposal through the gated write path) reuse this fleet's patterns later. Audited 2026-07-11: Dolibarr's propal/contrat/projet modules are enabled but empty — proposals and contracts live outside the ERP today.
|
||||||
- **Contract lifecycle** — record contracts in the ERP (the KM retainer terms exist only as WIP JSON today: $5,500/mo = $2,500 net-30 + $3,000 deferred +3 months — the deferred schedule is the source of truth T06 dunning needs), renewal reminders, obligation extraction (the KM 4 % new-business clause), signed originals in the GED. Needs write-skill ops for contacts/contracts/products (same extension pattern as the GED-attach issue).
|
- **Contract lifecycle** — promoted to [erp#65](https://gitea.arcodange.lab/arcodange-org/erp/issues/65) (client-dossier write ops: **thirdparty update + contacts shipped 2026-07-15** — the KM dossier lives in the ERP with the FINAL contract terms; phase 2 = contract + services objects, product currency gated by adc-006) and [erp#67](https://gitea.arcodange.lab/arcodange-org/erp/issues/67) (**official-document drafting skill — T18 candidate**: bilingual FR/EN signable sets, deterministic legal checklist, human-gated e-signature dispatch, billing linkage so T05 reads the contract). The KM deferred schedule is now IN the ERP (FAC005/FAC007 validated, due 2026-08-23 & 2026-09-23 — what T06 dunning needs); renewal reminders + the 4 %-window obligation ride the compliance calendar (#54/#60).
|
||||||
- **Client delivery agents — the third family.** Agents working *on* client projects (the KM architecture notes in the vault are the embryo) share the [client dossiers](agent-architecture.md#knowledge-layer--the-second-brain) with this fleet: they deposit decisions, meeting notes (via a **local capture lane** — [D9](agent-architecture.md#open-decisions), **parked as nice-to-have 2026-07-12** (calls are iPhone-first today): Hyprnote free tier on the M4 — local transcription + speaker diarization + Google Calendar sync, manual trigger; Meetily as OSS fallback; markdown export → `sb.py` ingest matched to the calendar event → Ornith summary, per the vault doctrine — no cloud notetaker) and new-business sightings; they retrieve contract scope, contacts and billing state. Reuses T17 rails and the vault routing doctrine; REX at project close feeds `03-Resources` → prospection Mode B evidence → CMS case studies.
|
- **Client delivery agents — the third family.** Agents working *on* client projects (the KM architecture notes in the vault are the embryo) share the [client dossiers](agent-architecture.md#knowledge-layer--the-second-brain) with this fleet: they deposit decisions, meeting notes (via a **local capture lane** — [D9](agent-architecture.md#open-decisions), **parked as nice-to-have 2026-07-12** (calls are iPhone-first today): Hyprnote free tier on the M4 — local transcription + speaker diarization + Google Calendar sync, manual trigger; Meetily as OSS fallback; markdown export → `sb.py` ingest matched to the calendar event → Ornith summary, per the vault doctrine — no cloud notetaker) and new-business sightings; they retrieve contract scope, contacts and billing state. Reuses T17 rails and the vault routing doctrine; REX at project close feeds `03-Resources` → prospection Mode B evidence → CMS case studies.
|
||||||
|
|||||||
Reference in New Issue
Block a user