fix: use bitwarden secret backend
This commit is contained in:
parent
be2b7e631b
commit
fe9e70b61a
6 changed files with 26 additions and 15 deletions
14
README.md
14
README.md
|
|
@ -6,7 +6,7 @@ Le binaire s’appuie maintenant sur [`mcp-framework`](../mcp-framework) pour :
|
||||||
|
|
||||||
- la gestion de profils CLI
|
- la gestion de profils CLI
|
||||||
- le stockage JSON de configuration dans `os.UserConfigDir()`
|
- 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`
|
- le manifeste `mcp.toml`
|
||||||
- les helpers Go générés depuis `mcp.toml` (`mcpgen/`)
|
- les helpers Go générés depuis `mcp.toml` (`mcpgen/`)
|
||||||
- l’auto-update via `email-mcp update`
|
- 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 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 config delete` : supprime un profil local et son mot de passe stocké
|
||||||
- `email-mcp mcp` : lance le serveur MCP sur `stdin/stdout`
|
- `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 update` : met à jour le binaire courant depuis la dernière release
|
||||||
- `email-mcp version` : affiche la version du binaire
|
- `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 :
|
La configuration est séparée en deux parties :
|
||||||
|
|
||||||
- `host` et `username` sont stockés dans `config.json`
|
- `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 :
|
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`
|
1. `host` : `EMAIL_MCP_HOST` puis `config.json`
|
||||||
2. `username` : `EMAIL_MCP_USERNAME` 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
|
### Configurer un profil
|
||||||
|
|
||||||
|
|
@ -69,9 +69,9 @@ Le binaire demande ensuite :
|
||||||
2. le nom d’utilisateur
|
2. le nom d’utilisateur
|
||||||
3. le mot de passe
|
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
|
### Lancer le serveur MCP
|
||||||
|
|
||||||
|
|
@ -135,7 +135,7 @@ token_env_names = ["GITEA_TOKEN"]
|
||||||
|
|
||||||
- la lisibilité du fichier de configuration
|
- la lisibilité du fichier de configuration
|
||||||
- le profil IMAP résolu
|
- 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 présence du mot de passe stocké
|
||||||
- la validité du manifeste `mcp.toml`
|
- la validité du manifeste `mcp.toml`
|
||||||
- la connectivité IMAP avec les credentials résolus
|
- la connectivité IMAP avec les credentials résolus
|
||||||
|
|
|
||||||
|
|
@ -688,7 +688,7 @@ func mapAppError(err error) error {
|
||||||
return newUserFacingError("credentials not configured; run `email-mcp setup`", err)
|
return newUserFacingError("credentials not configured; run `email-mcp setup`", err)
|
||||||
case errors.Is(err, frameworksecretstore.ErrBackendUnavailable):
|
case errors.Is(err, frameworksecretstore.ErrBackendUnavailable):
|
||||||
return newUserFacingError(
|
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,
|
err,
|
||||||
)
|
)
|
||||||
case errors.Is(err, frameworksecretstore.ErrReadOnly):
|
case errors.Is(err, frameworksecretstore.ErrReadOnly):
|
||||||
|
|
|
||||||
|
|
@ -1197,7 +1197,7 @@ func TestMapAppErrorMapsMissingCredentialError(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMapAppErrorMapsUnavailableWalletError(t *testing.T) {
|
func TestMapAppErrorMapsUnavailableSecretBackendError(t *testing.T) {
|
||||||
err := mapAppError(&frameworksecretstore.BackendUnavailableError{
|
err := mapAppError(&frameworksecretstore.BackendUnavailableError{
|
||||||
Policy: frameworksecretstore.BackendAuto,
|
Policy: frameworksecretstore.BackendAuto,
|
||||||
Required: "any keyring backend",
|
Required: "any keyring backend",
|
||||||
|
|
@ -1205,8 +1205,8 @@ func TestMapAppErrorMapsUnavailableWalletError(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected mapped error")
|
t.Fatal("expected mapped error")
|
||||||
}
|
}
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "wallet") {
|
if !strings.Contains(strings.ToLower(err.Error()), "secret backend") {
|
||||||
t.Fatalf("expected wallet guidance, got %v", err)
|
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 {
|
if code := Execute(app, []string{"setup"}, stderr); code != 1 {
|
||||||
t.Fatalf("expected exit code 1, got %d", code)
|
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)
|
t.Fatalf("unexpected stderr: %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
mcp.toml
12
mcp.toml
|
|
@ -12,10 +12,18 @@ checksum_required = true
|
||||||
token_env_names = ["GITEA_TOKEN"]
|
token_env_names = ["GITEA_TOKEN"]
|
||||||
|
|
||||||
[environment]
|
[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]
|
[secret_store]
|
||||||
backend_policy = "auto"
|
backend_policy = "bitwarden-cli"
|
||||||
|
|
||||||
[profiles]
|
[profiles]
|
||||||
default = "default"
|
default = "default"
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ func TestGeneratedManifestFallsBackToEmbeddedRootManifest(t *testing.T) {
|
||||||
if len(manifestFile.Config.Fields) != 3 {
|
if len(manifestFile.Config.Fields) != 3 {
|
||||||
t.Fatalf("config fields = %d, want 3", len(manifestFile.Config.Fields))
|
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) {
|
func TestGeneratedConfigHelpersExposeIMAPResolutionFields(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ package mcpgen
|
||||||
|
|
||||||
import fwmanifest "forge.lclr.dev/AI/mcp-framework/manifest"
|
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) {
|
func LoadManifest(startDir string) (fwmanifest.File, string, error) {
|
||||||
return fwmanifest.LoadDefaultOrEmbedded(startDir, embeddedManifest)
|
return fwmanifest.LoadDefaultOrEmbedded(startDir, embeddedManifest)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue