feat: add --debug tracing for bitwarden calls
This commit is contained in:
parent
98f07f557d
commit
98bac84ab8
10 changed files with 259 additions and 3 deletions
|
|
@ -18,6 +18,8 @@ const (
|
|||
CommandVersion = "version"
|
||||
CommandDoctor = "doctor"
|
||||
|
||||
bitwardenDebugEnvName = "MCP_FRAMEWORK_BITWARDEN_DEBUG"
|
||||
|
||||
ConfigSubcommandShow = "show"
|
||||
ConfigSubcommandTest = "test"
|
||||
ConfigSubcommandDelete = "delete"
|
||||
|
|
@ -118,7 +120,13 @@ func Run(ctx context.Context, opts Options) error {
|
|||
return ErrBinaryNameRequired
|
||||
}
|
||||
|
||||
command, commandArgs, showHelp := parseArgs(expandAliases(normalized.Args, normalized.Aliases))
|
||||
resolvedArgs := expandAliases(normalized.Args, normalized.Aliases)
|
||||
resolvedArgs, debugEnabled := extractGlobalDebugFlag(resolvedArgs)
|
||||
if debugEnabled {
|
||||
_ = os.Setenv(bitwardenDebugEnvName, "1")
|
||||
}
|
||||
|
||||
command, commandArgs, showHelp := parseArgs(resolvedArgs)
|
||||
if showHelp {
|
||||
return printHelp(normalized, command, commandArgs)
|
||||
}
|
||||
|
|
@ -252,6 +260,25 @@ func parseArgs(args []string) (command string, commandArgs []string, showHelp bo
|
|||
return command, commandArgs, false
|
||||
}
|
||||
|
||||
func extractGlobalDebugFlag(args []string) ([]string, bool) {
|
||||
args = trimArgs(args)
|
||||
if len(args) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(args))
|
||||
debugEnabled := false
|
||||
for _, arg := range args {
|
||||
if strings.TrimSpace(arg) == "--debug" {
|
||||
debugEnabled = true
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, arg)
|
||||
}
|
||||
|
||||
return filtered, debugEnabled
|
||||
}
|
||||
|
||||
func expandAliases(args []string, aliases map[string][]string) []string {
|
||||
args = trimArgs(args)
|
||||
if len(args) == 0 || len(aliases) == 0 {
|
||||
|
|
@ -512,6 +539,13 @@ func printGlobalHelp(opts Options) error {
|
|||
}
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintln(opts.Stdout, "\nOptions globales:"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintln(opts.Stdout, " --debug Active le debug des appels Bitwarden."); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(opts.Stdout, "\nAide detaillee: %s help <command>\n", opts.BinaryName)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -482,3 +483,73 @@ func TestRunPrintsDoctorAliasInGlobalHelpWhenEnabled(t *testing.T) {
|
|||
t.Fatalf("global help output missing default doctor alias details: %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAcceptsGlobalDebugFlagAndRoutesCommand(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
var got Invocation
|
||||
|
||||
t.Setenv("MCP_FRAMEWORK_BITWARDEN_DEBUG", "")
|
||||
|
||||
err := Run(context.Background(), Options{
|
||||
BinaryName: "my-mcp",
|
||||
Args: []string{"--debug", "setup", "--profile", "prod"},
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
Hooks: Hooks{
|
||||
Setup: func(_ context.Context, inv Invocation) error {
|
||||
got = inv
|
||||
return nil
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run error = %v", err)
|
||||
}
|
||||
|
||||
if got.Command != CommandSetup {
|
||||
t.Fatalf("invocation command = %q, want %q", got.Command, CommandSetup)
|
||||
}
|
||||
wantArgs := []string{"--profile", "prod"}
|
||||
if !slices.Equal(got.Args, wantArgs) {
|
||||
t.Fatalf("invocation args = %v, want %v", got.Args, wantArgs)
|
||||
}
|
||||
if os.Getenv("MCP_FRAMEWORK_BITWARDEN_DEBUG") != "1" {
|
||||
t.Fatalf("MCP_FRAMEWORK_BITWARDEN_DEBUG = %q, want %q", os.Getenv("MCP_FRAMEWORK_BITWARDEN_DEBUG"), "1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAcceptsGlobalDebugFlagAfterCommand(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
var got Invocation
|
||||
|
||||
t.Setenv("MCP_FRAMEWORK_BITWARDEN_DEBUG", "")
|
||||
|
||||
err := Run(context.Background(), Options{
|
||||
BinaryName: "my-mcp",
|
||||
Args: []string{"setup", "--debug", "--profile", "prod"},
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
Hooks: Hooks{
|
||||
Setup: func(_ context.Context, inv Invocation) error {
|
||||
got = inv
|
||||
return nil
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run error = %v", err)
|
||||
}
|
||||
|
||||
if got.Command != CommandSetup {
|
||||
t.Fatalf("invocation command = %q, want %q", got.Command, CommandSetup)
|
||||
}
|
||||
wantArgs := []string{"--profile", "prod"}
|
||||
if !slices.Equal(got.Args, wantArgs) {
|
||||
t.Fatalf("invocation args = %v, want %v", got.Args, wantArgs)
|
||||
}
|
||||
if os.Getenv("MCP_FRAMEWORK_BITWARDEN_DEBUG") != "1" {
|
||||
t.Fatalf("MCP_FRAMEWORK_BITWARDEN_DEBUG = %q, want %q", os.Getenv("MCP_FRAMEWORK_BITWARDEN_DEBUG"), "1")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ type DoctorOptions struct {
|
|||
|
||||
type BitwardenDoctorOptions struct {
|
||||
Command string
|
||||
Debug bool
|
||||
Shell string
|
||||
LookupEnv func(string) (string, bool)
|
||||
}
|
||||
|
|
@ -238,6 +239,7 @@ func BitwardenReadyCheck(options BitwardenDoctorOptions) DoctorCheck {
|
|||
return func(context.Context) DoctorResult {
|
||||
err := checkBitwardenReady(secretstore.Options{
|
||||
BitwardenCommand: strings.TrimSpace(options.Command),
|
||||
BitwardenDebug: options.Debug,
|
||||
Shell: strings.TrimSpace(options.Shell),
|
||||
LookupEnv: options.LookupEnv,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -43,3 +43,5 @@ Si `Hooks.Version` n'est pas fourni, la sous-commande `version` affiche automati
|
|||
Sans arguments, `bootstrap.Run` affiche l'aide globale (pas de lancement implicite de `mcp`).
|
||||
La commande `config` impose une sous-commande (`show`, `test`, et optionnellement `delete`).
|
||||
Les alias déclarés (ou `EnableDoctorAlias`) sont affichés dans l'aide globale.
|
||||
Le flag global `--debug` est supporté et active le debug des appels Bitwarden
|
||||
(`MCP_FRAMEWORK_BITWARDEN_DEBUG=1`).
|
||||
|
|
|
|||
|
|
@ -164,6 +164,10 @@ fmt.Println(report.Remediation) // action recommandée
|
|||
|
||||
## Debug Bitwarden en 60 secondes
|
||||
|
||||
Tu peux activer les traces d'appels Bitwarden avec le flag CLI global `--debug`
|
||||
(via `bootstrap`) ou en exportant `MCP_FRAMEWORK_BITWARDEN_DEBUG=1`.
|
||||
Les commandes `bw` exécutées seront affichées (avec redaction des payloads sensibles).
|
||||
|
||||
1. Vérifier l'état de session :
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
|
@ -17,6 +18,7 @@ import (
|
|||
|
||||
const (
|
||||
defaultBitwardenCommand = "bw"
|
||||
bitwardenDebugEnvName = "MCP_FRAMEWORK_BITWARDEN_DEBUG"
|
||||
bitwardenSessionEnvName = "BW_SESSION"
|
||||
bitwardenSecretFieldName = "mcp-secret"
|
||||
bitwardenServiceFieldName = "mcp-service"
|
||||
|
|
@ -34,10 +36,12 @@ type bitwardenRunner func(command string, stdin []byte, args ...string) ([]byte,
|
|||
|
||||
var runBitwardenCLI bitwardenRunner = executeBitwardenCLI
|
||||
var bitwardenLoaderActive atomic.Bool
|
||||
var bitwardenDebugOutput io.Writer = os.Stderr
|
||||
|
||||
type bitwardenStore struct {
|
||||
command string
|
||||
serviceName string
|
||||
debug bool
|
||||
}
|
||||
|
||||
type bitwardenListItem struct {
|
||||
|
|
@ -54,10 +58,12 @@ func newBitwardenStore(options Options, policy BackendPolicy, serviceName string
|
|||
if command == "" {
|
||||
command = defaultBitwardenCommand
|
||||
}
|
||||
debugEnabled := isBitwardenDebugEnabled(options.BitwardenDebug)
|
||||
|
||||
store := &bitwardenStore{
|
||||
command: command,
|
||||
serviceName: serviceName,
|
||||
debug: debugEnabled,
|
||||
}
|
||||
|
||||
if _, err := store.execute("verify bitwarden CLI availability", nil, "--version"); err != nil {
|
||||
|
|
@ -80,6 +86,7 @@ func newBitwardenStore(options Options, policy BackendPolicy, serviceName string
|
|||
|
||||
if err := EnsureBitwardenReady(Options{
|
||||
BitwardenCommand: command,
|
||||
BitwardenDebug: debugEnabled,
|
||||
LookupEnv: options.LookupEnv,
|
||||
Shell: options.Shell,
|
||||
}); err != nil {
|
||||
|
|
@ -99,6 +106,7 @@ func EnsureBitwardenReady(options Options) error {
|
|||
if command == "" {
|
||||
command = defaultBitwardenCommand
|
||||
}
|
||||
debugEnabled := isBitwardenDebugEnabled(options.BitwardenDebug)
|
||||
unlockCommand := bitwardenUnlockRemediation(command, options.Shell)
|
||||
|
||||
lookupEnv := options.LookupEnv
|
||||
|
|
@ -106,7 +114,7 @@ func EnsureBitwardenReady(options Options) error {
|
|||
lookupEnv = os.LookupEnv
|
||||
}
|
||||
|
||||
output, err := runBitwardenCLI(command, nil, "status")
|
||||
output, err := runBitwardenCommand(command, debugEnabled, nil, "status")
|
||||
if err != nil {
|
||||
return fmt.Errorf("check bitwarden CLI status: %w", err)
|
||||
}
|
||||
|
|
@ -438,7 +446,7 @@ func (s *bitwardenStore) encodePayload(payload map[string]any) (string, error) {
|
|||
}
|
||||
|
||||
func (s *bitwardenStore) execute(operation string, stdin []byte, args ...string) ([]byte, error) {
|
||||
output, err := runBitwardenCLI(s.command, stdin, args...)
|
||||
output, err := runBitwardenCommand(s.command, s.debug, stdin, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", operation, err)
|
||||
}
|
||||
|
|
@ -521,6 +529,71 @@ func bitwardenItemMatchesMarkers(payload map[string]any, serviceName, secretName
|
|||
strings.TrimSpace(markedSecretName) == strings.TrimSpace(secretName)
|
||||
}
|
||||
|
||||
func runBitwardenCommand(command string, debug bool, stdin []byte, args ...string) ([]byte, error) {
|
||||
if debug {
|
||||
logBitwardenCommand(command, args...)
|
||||
}
|
||||
return runBitwardenCLI(command, stdin, args...)
|
||||
}
|
||||
|
||||
func isBitwardenDebugEnabled(explicit bool) bool {
|
||||
if explicit {
|
||||
return true
|
||||
}
|
||||
|
||||
raw, ok := os.LookupEnv(bitwardenDebugEnvName)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "1", "true", "yes", "y", "on":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func logBitwardenCommand(command string, args ...string) {
|
||||
writer := bitwardenDebugOutput
|
||||
if writer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
renderedArgs := sanitizeBitwardenDebugArgs(args)
|
||||
if len(renderedArgs) == 0 {
|
||||
_, _ = fmt.Fprintf(writer, "[bitwarden debug] %s\n", strings.TrimSpace(command))
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(
|
||||
writer,
|
||||
"[bitwarden debug] %s %s\n",
|
||||
strings.TrimSpace(command),
|
||||
strings.Join(renderedArgs, " "),
|
||||
)
|
||||
}
|
||||
|
||||
func sanitizeBitwardenDebugArgs(args []string) []string {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rendered := make([]string, len(args))
|
||||
for idx, arg := range args {
|
||||
rendered[idx] = strings.TrimSpace(arg)
|
||||
}
|
||||
|
||||
if len(rendered) >= 3 && rendered[0] == "create" && rendered[1] == "item" {
|
||||
rendered[2] = "<redacted>"
|
||||
}
|
||||
if len(rendered) >= 4 && rendered[0] == "edit" && rendered[1] == "item" {
|
||||
rendered[3] = "<redacted>"
|
||||
}
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
func executeBitwardenCLI(command string, stdin []byte, args ...string) ([]byte, error) {
|
||||
stopLoader := startBitwardenLoader()
|
||||
defer stopLoader()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package secretstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
|
@ -367,6 +369,59 @@ func TestExecuteBitwardenCLIClassifiesLoginError(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOpenBitwardenCLIDebugFromEnvPrintsBitwardenCalls(t *testing.T) {
|
||||
withBitwardenSession(t)
|
||||
fakeCLI := newFakeBitwardenCLI("bw")
|
||||
withBitwardenRunner(t, fakeCLI.run)
|
||||
t.Setenv("MCP_FRAMEWORK_BITWARDEN_DEBUG", "1")
|
||||
|
||||
var logs bytes.Buffer
|
||||
withBitwardenDebugOutput(t, &logs)
|
||||
|
||||
_, err := Open(Options{
|
||||
ServiceName: "email-mcp",
|
||||
BackendPolicy: BackendBitwardenCLI,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Open returned error: %v", err)
|
||||
}
|
||||
|
||||
text := logs.String()
|
||||
if !strings.Contains(text, "bw --version") {
|
||||
t.Fatalf("debug logs = %q, want command bw --version", text)
|
||||
}
|
||||
if !strings.Contains(text, "bw status") {
|
||||
t.Fatalf("debug logs = %q, want command bw status", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBitwardenDebugRedactsSensitivePayloadArguments(t *testing.T) {
|
||||
withBitwardenSession(t)
|
||||
fakeCLI := newFakeBitwardenCLI("bw")
|
||||
withBitwardenRunner(t, fakeCLI.run)
|
||||
|
||||
var logs bytes.Buffer
|
||||
withBitwardenDebugOutput(t, &logs)
|
||||
|
||||
store, err := Open(Options{
|
||||
ServiceName: "graylog-mcp",
|
||||
BackendPolicy: BackendBitwardenCLI,
|
||||
BitwardenDebug: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Open returned error: %v", err)
|
||||
}
|
||||
|
||||
if err := store.SetSecret("api-token", "API token", "secret-v1"); err != nil {
|
||||
t.Fatalf("SetSecret returned error: %v", err)
|
||||
}
|
||||
|
||||
text := logs.String()
|
||||
if !strings.Contains(text, "bw create item <redacted>") {
|
||||
t.Fatalf("debug logs = %q, want redacted create payload", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBitwardenLoaderFrameUsesSingleLineRewriteAndMessage(t *testing.T) {
|
||||
frame := bitwardenLoaderFrame(0)
|
||||
if !strings.HasPrefix(frame, "\r\033[2K") {
|
||||
|
|
@ -455,6 +510,16 @@ func withBitwardenRunner(
|
|||
})
|
||||
}
|
||||
|
||||
func withBitwardenDebugOutput(t *testing.T, writer io.Writer) {
|
||||
t.Helper()
|
||||
|
||||
previous := bitwardenDebugOutput
|
||||
bitwardenDebugOutput = writer
|
||||
t.Cleanup(func() {
|
||||
bitwardenDebugOutput = previous
|
||||
})
|
||||
}
|
||||
|
||||
type fakeBitwardenCLI struct {
|
||||
command string
|
||||
itemsByID map[string]fakeBitwardenItem
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type OpenFromManifestOptions struct {
|
|||
KWalletAppID string
|
||||
KWalletFolder string
|
||||
BitwardenCommand string
|
||||
BitwardenDebug bool
|
||||
Shell string
|
||||
ManifestLoader ManifestLoader
|
||||
ExecutableResolver ExecutableResolver
|
||||
|
|
@ -38,6 +39,7 @@ func OpenFromManifest(options OpenFromManifestOptions) (Store, error) {
|
|||
KWalletAppID: options.KWalletAppID,
|
||||
KWalletFolder: options.KWalletFolder,
|
||||
BitwardenCommand: strings.TrimSpace(options.BitwardenCommand),
|
||||
BitwardenDebug: options.BitwardenDebug,
|
||||
Shell: strings.TrimSpace(options.Shell),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ type DescribeRuntimeOptions struct {
|
|||
KWalletAppID string
|
||||
KWalletFolder string
|
||||
BitwardenCommand string
|
||||
BitwardenDebug bool
|
||||
Shell string
|
||||
ManifestLoader ManifestLoader
|
||||
ExecutableResolver ExecutableResolver
|
||||
|
|
@ -73,6 +74,7 @@ func DescribeRuntime(options DescribeRuntimeOptions) (RuntimeDescription, error)
|
|||
KWalletAppID: options.KWalletAppID,
|
||||
KWalletFolder: options.KWalletFolder,
|
||||
BitwardenCommand: options.BitwardenCommand,
|
||||
BitwardenDebug: options.BitwardenDebug,
|
||||
Shell: options.Shell,
|
||||
})
|
||||
if openErr != nil {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ type Options struct {
|
|||
KWalletAppID string
|
||||
KWalletFolder string
|
||||
BitwardenCommand string
|
||||
BitwardenDebug bool
|
||||
Shell string
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue