diff --git a/README.md b/README.md index 79c5f8e..af10751 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Le binaire s’appuie 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 l’OS +- 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/`) - l’auto-update via `email-mcp update` @@ -18,7 +18,7 @@ Le binaire s’appuie 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 l’accès IMAP +- `email-mcp doctor` : diagnostique la configuration locale, Bitwarden, le manifeste et l’accè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 l’aide 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/` +3. `password` : `EMAIL_MCP_PASSWORD` puis secret Bitwarden `imap-password/` ### Configurer un profil @@ -69,9 +69,9 @@ Le binaire demande ensuite : 2. le nom d’utilisateur 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 l’environnement. +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 l’environnement. `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 diff --git a/internal/cli/app.go b/internal/cli/app.go index 7ebb2b6..4566a41 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -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): diff --git a/internal/cli/app_test.go b/internal/cli/app_test.go index d90297b..95d7a54 100644 --- a/internal/cli/app_test.go +++ b/internal/cli/app_test.go @@ -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) } } diff --git a/mcp.toml b/mcp.toml index c40c491..aafd042 100644 --- a/mcp.toml +++ b/mcp.toml @@ -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" diff --git a/mcpgen/generated_test.go b/mcpgen/generated_test.go index 25d38b4..b6e4500 100644 --- a/mcpgen/generated_test.go +++ b/mcpgen/generated_test.go @@ -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) { diff --git a/mcpgen/manifest.go b/mcpgen/manifest.go index bb41a34..65ca329 100644 --- a/mcpgen/manifest.go +++ b/mcpgen/manifest.go @@ -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)