fix: use bitwarden secret backend

This commit is contained in:
thibaud-lclr 2026-05-11 11:24:56 +02:00
parent be2b7e631b
commit fe9e70b61a
6 changed files with 26 additions and 15 deletions

View file

@ -6,7 +6,7 @@ Le binaire sappuie maintenant sur [`mcp-framework`](../mcp-framework) pour :
- la gestion de profils CLI
- le stockage JSON de configuration dans `os.UserConfigDir()`
- le stockage du mot de passe dans le wallet natif de lOS
- le stockage du mot de passe dans Bitwarden via `bw`
- le manifeste `mcp.toml`
- les helpers Go générés depuis `mcp.toml` (`mcpgen/`)
- lauto-update via `email-mcp update`
@ -18,7 +18,7 @@ Le binaire sappuie maintenant sur [`mcp-framework`](../mcp-framework) pour :
- `email-mcp config test` : lance les checks de configuration/connectivité (équivalent de `doctor`)
- `email-mcp config delete` : supprime un profil local et son mot de passe stocké
- `email-mcp mcp` : lance le serveur MCP sur `stdin/stdout`
- `email-mcp doctor` : diagnostique la configuration locale, le wallet, le manifeste et laccès IMAP
- `email-mcp doctor` : diagnostique la configuration locale, Bitwarden, le manifeste et laccès IMAP
- `email-mcp update` : met à jour le binaire courant depuis la dernière release
- `email-mcp version` : affiche la version du binaire
@ -35,7 +35,7 @@ La commande `email-mcp help` (ou `-h` / `--help`) affiche laide globale.
La configuration est séparée en deux parties :
- `host` et `username` sont stockés dans `config.json`
- `password` est stocké dans le wallet système
- `password` est stocké dans Bitwarden via le CLI `bw`
Le profil actif est résolu dans cet ordre :
@ -49,7 +49,7 @@ Les credentials IMAP sont résolus ensuite via les champs `[[config.fields]]` du
1. `host` : `EMAIL_MCP_HOST` puis `config.json`
2. `username` : `EMAIL_MCP_USERNAME` puis `config.json`
3. `password` : `EMAIL_MCP_PASSWORD` puis secret wallet `imap-password/<profile>`
3. `password` : `EMAIL_MCP_PASSWORD` puis secret Bitwarden `imap-password/<profile>`
### Configurer un profil
@ -69,9 +69,9 @@ Le binaire demande ensuite :
2. le nom dutilisateur
3. le mot de passe
Si un mot de passe existe déjà dans le wallet, laisser le champ vide le conserve.
Si un mot de passe existe déjà dans Bitwarden, laisser le champ vide le conserve.
Si le backend de secrets est en lecture seule (`[secret_store].backend_policy = "env-only"`), `setup` ne peut pas persister le mot de passe dans un wallet. Dans ce cas, exporte `EMAIL_MCP_PASSWORD` avant `setup`. La commande sauvegarde alors `host`/`username` et utilise le mot de passe depuis lenvironnement.
Le backend de secrets déclaré dans `mcp.toml` est `bitwarden-cli`. Le CLI `bw` doit être installé, connecté et déverrouillé avec `BW_SESSION` disponible dans lenvironnement. `EMAIL_MCP_PASSWORD` reste accepté pour fournir le mot de passe sans lire Bitwarden.
### Lancer le serveur MCP
@ -135,7 +135,7 @@ token_env_names = ["GITEA_TOKEN"]
- la lisibilité du fichier de configuration
- le profil IMAP résolu
- la disponibilité du wallet système
- la disponibilité du backend Bitwarden
- la présence du mot de passe stocké
- la validité du manifeste `mcp.toml`
- la connectivité IMAP avec les credentials résolus

View file

@ -688,7 +688,7 @@ func mapAppError(err error) error {
return newUserFacingError("credentials not configured; run `email-mcp setup`", err)
case errors.Is(err, frameworksecretstore.ErrBackendUnavailable):
return newUserFacingError(
fmt.Sprintf("%s is not available; configure a supported OS wallet and retry", frameworksecretstore.BackendName()),
fmt.Sprintf("%s is not available; configure the declared secret backend and retry", frameworksecretstore.BackendName()),
err,
)
case errors.Is(err, frameworksecretstore.ErrReadOnly):

View file

@ -1197,7 +1197,7 @@ func TestMapAppErrorMapsMissingCredentialError(t *testing.T) {
}
}
func TestMapAppErrorMapsUnavailableWalletError(t *testing.T) {
func TestMapAppErrorMapsUnavailableSecretBackendError(t *testing.T) {
err := mapAppError(&frameworksecretstore.BackendUnavailableError{
Policy: frameworksecretstore.BackendAuto,
Required: "any keyring backend",
@ -1205,8 +1205,8 @@ func TestMapAppErrorMapsUnavailableWalletError(t *testing.T) {
if err == nil {
t.Fatal("expected mapped error")
}
if !strings.Contains(strings.ToLower(err.Error()), "wallet") {
t.Fatalf("expected wallet guidance, got %v", err)
if !strings.Contains(strings.ToLower(err.Error()), "secret backend") {
t.Fatalf("expected secret backend guidance, got %v", err)
}
}
@ -1234,7 +1234,7 @@ func TestExecuteSetupWritesMappedErrorAndReturnsExitCodeOne(t *testing.T) {
if code := Execute(app, []string{"setup"}, stderr); code != 1 {
t.Fatalf("expected exit code 1, got %d", code)
}
if got := strings.ToLower(stderr.String()); !strings.Contains(got, "wallet") {
if got := strings.ToLower(stderr.String()); !strings.Contains(got, "secret backend") {
t.Fatalf("unexpected stderr: %q", got)
}
}

View file

@ -12,10 +12,18 @@ checksum_required = true
token_env_names = ["GITEA_TOKEN"]
[environment]
known = ["EMAIL_MCP_PROFILE", "EMAIL_MCP_HOST", "EMAIL_MCP_USERNAME", "EMAIL_MCP_PASSWORD"]
known = [
"EMAIL_MCP_PROFILE",
"EMAIL_MCP_HOST",
"EMAIL_MCP_USERNAME",
"EMAIL_MCP_PASSWORD",
"BW_SESSION",
"MCP_FRAMEWORK_BITWARDEN_CACHE",
"MCP_FRAMEWORK_BITWARDEN_DEBUG",
]
[secret_store]
backend_policy = "auto"
backend_policy = "bitwarden-cli"
[profiles]
default = "default"

View file

@ -35,6 +35,9 @@ func TestGeneratedManifestFallsBackToEmbeddedRootManifest(t *testing.T) {
if len(manifestFile.Config.Fields) != 3 {
t.Fatalf("config fields = %d, want 3", len(manifestFile.Config.Fields))
}
if manifestFile.SecretStore.BackendPolicy != "bitwarden-cli" {
t.Fatalf("secret store backend policy = %q, want bitwarden-cli", manifestFile.SecretStore.BackendPolicy)
}
}
func TestGeneratedConfigHelpersExposeIMAPResolutionFields(t *testing.T) {

View file

@ -4,7 +4,7 @@ package mcpgen
import fwmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
const embeddedManifest = "binary_name = \"email-mcp\"\ndocs_url = \"https://gitea.lclr.dev/AI/email-mcp\"\n\n[update]\nsource_name = \"email-mcp releases\"\ndriver = \"gitea\"\nrepository = \"AI/email-mcp\"\nbase_url = \"https://gitea.lclr.dev\"\nasset_name_template = \"{binary}-{os}-{arch}{ext}\"\nchecksum_asset_name = \"{asset}.sha256\"\nchecksum_required = true\ntoken_env_names = [\"GITEA_TOKEN\"]\n\n[environment]\nknown = [\"EMAIL_MCP_PROFILE\", \"EMAIL_MCP_HOST\", \"EMAIL_MCP_USERNAME\", \"EMAIL_MCP_PASSWORD\"]\n\n[secret_store]\nbackend_policy = \"auto\"\n\n[profiles]\ndefault = \"default\"\nknown = [\"default\"]\n\n[bootstrap]\ndescription = \"Local MCP server to read an IMAP mailbox.\"\n\n[[config.fields]]\nname = \"host\"\nenv = \"EMAIL_MCP_HOST\"\nconfig_key = \"host\"\ntype = \"string\"\nlabel = \"IMAP host\"\nrequired = true\nsources = [\"env\", \"config\"]\n\n[[config.fields]]\nname = \"username\"\nenv = \"EMAIL_MCP_USERNAME\"\nconfig_key = \"username\"\ntype = \"string\"\nlabel = \"Username\"\nrequired = true\nsources = [\"env\", \"config\"]\n\n[[config.fields]]\nname = \"password\"\nenv = \"EMAIL_MCP_PASSWORD\"\nsecret_key_template = \"imap-password/{profile}\"\ntype = \"secret\"\nlabel = \"Password\"\nrequired = true\nsources = [\"env\", \"secret\"]\n"
const embeddedManifest = "binary_name = \"email-mcp\"\ndocs_url = \"https://gitea.lclr.dev/AI/email-mcp\"\n\n[update]\nsource_name = \"email-mcp releases\"\ndriver = \"gitea\"\nrepository = \"AI/email-mcp\"\nbase_url = \"https://gitea.lclr.dev\"\nasset_name_template = \"{binary}-{os}-{arch}{ext}\"\nchecksum_asset_name = \"{asset}.sha256\"\nchecksum_required = true\ntoken_env_names = [\"GITEA_TOKEN\"]\n\n[environment]\nknown = [\n \"EMAIL_MCP_PROFILE\",\n \"EMAIL_MCP_HOST\",\n \"EMAIL_MCP_USERNAME\",\n \"EMAIL_MCP_PASSWORD\",\n \"BW_SESSION\",\n \"MCP_FRAMEWORK_BITWARDEN_CACHE\",\n \"MCP_FRAMEWORK_BITWARDEN_DEBUG\",\n]\n\n[secret_store]\nbackend_policy = \"bitwarden-cli\"\n\n[profiles]\ndefault = \"default\"\nknown = [\"default\"]\n\n[bootstrap]\ndescription = \"Local MCP server to read an IMAP mailbox.\"\n\n[[config.fields]]\nname = \"host\"\nenv = \"EMAIL_MCP_HOST\"\nconfig_key = \"host\"\ntype = \"string\"\nlabel = \"IMAP host\"\nrequired = true\nsources = [\"env\", \"config\"]\n\n[[config.fields]]\nname = \"username\"\nenv = \"EMAIL_MCP_USERNAME\"\nconfig_key = \"username\"\ntype = \"string\"\nlabel = \"Username\"\nrequired = true\nsources = [\"env\", \"config\"]\n\n[[config.fields]]\nname = \"password\"\nenv = \"EMAIL_MCP_PASSWORD\"\nsecret_key_template = \"imap-password/{profile}\"\ntype = \"secret\"\nlabel = \"Password\"\nrequired = true\nsources = [\"env\", \"secret\"]\n"
func LoadManifest(startDir string) (fwmanifest.File, string, error) {
return fwmanifest.LoadDefaultOrEmbedded(startDir, embeddedManifest)