diff --git a/.github/workflows/mcp-release.yml b/.github/workflows/mcp-release.yml index b3c2074ee8..6fe8247b20 100644 --- a/.github/workflows/mcp-release.yml +++ b/.github/workflows/mcp-release.yml @@ -426,7 +426,9 @@ jobs: if [[ "${{ runner.os }}" == "Linux" ]]; then cargo zigbuild --release -p dbx-cli --no-default-features --target "${{ matrix.target }}.2.31" else - cargo build --release -p dbx-cli --no-default-features --target "${{ matrix.target }}" + # Same keychain rationale as the MCP native legs; the feature is + # forwarded through the dbx-mcp dependency. + cargo build --release -p dbx-cli --no-default-features --features dbx-mcp/os-keyring --target "${{ matrix.target }}" fi - name: Stage platform package @@ -660,9 +662,14 @@ jobs: shell: bash run: | if [[ "${{ runner.os }}" == "Linux" ]]; then + # Linux zigbuild cannot link the dbus Secret Service backend; the + # managed .dbx/secret.key file remains the key material source. cargo zigbuild --release -p dbx-mcp --target "${{ matrix.target }}.2.31" else - cargo build --release -p dbx-mcp --target "${{ matrix.target }}" + # Native macOS/Windows builds read the OS keychain key that DBX + # Desktop provisions during the data security upgrade, so the MCP + # works against desktop-migrated profiles without env overrides. + cargo build --release -p dbx-mcp --features os-keyring --target "${{ matrix.target }}" fi - name: Stage platform package diff --git a/crates/dbx-mcp/Cargo.toml b/crates/dbx-mcp/Cargo.toml index cf64d0441d..c5724ddc6a 100644 --- a/crates/dbx-mcp/Cargo.toml +++ b/crates/dbx-mcp/Cargo.toml @@ -9,6 +9,11 @@ default = ["duckdb-sidecar", "mq-admin", "sqlite-bundled"] duckdb-sidecar = ["dbx-core/duckdb-sidecar"] mq-admin = ["dbx-core/mq-admin"] sqlite-bundled = ["dbx-core/sqlite-bundled"] +# Native macOS/Windows builds enable this so CLI/MCP can read the OS +# keychain key Desktop provisioned during the data security upgrade; the +# Linux zigbuild legs cannot link the dbus Secret Service backend and keep +# falling back to the managed .dbx/secret.key file. +os-keyring = ["dbx-core/os-keyring"] transaction-test-hooks = [] [[bin]] diff --git a/crates/dbx-mcp/src/backend.rs b/crates/dbx-mcp/src/backend.rs index 6854f14f59..2eefb17d0e 100644 --- a/crates/dbx-mcp/src/backend.rs +++ b/crates/dbx-mcp/src/backend.rs @@ -697,6 +697,24 @@ impl LocalBackend { let storage = Storage::open_unmigrated(path).await?.with_secret_key_creation(false); let migration = storage.inspect_data_migration().await?; if !migration.is_ready() { + // The migration wizard may have already completed on Desktop with + // the key provisioned in the OS keychain, which a keyring-less + // CLI/MCP build cannot read. Pointing those users back at the + // wizard loops forever, so separate the two failure shapes. + let migration_data_remaining = migration.database_plaintext_count > 0 + || migration.sync_credential_count > 0 + || migration.legacy_json_files.iter().any(|file| file.exists); + if !migration_data_remaining && !migration.key_provider_available { + return Err(format!( + "SECRET_KEY_UNAVAILABLE: this process cannot read the DBX data encryption key ({}). \ + If the data security upgrade was completed in DBX Desktop, its key may live in the OS \ + keychain: use an MCP/CLI build with OS keychain support, or expose the key to headless \ + tools via the DBX_SECRET_KEY_FILE / DBX_SECRET_KEY environment variables. Otherwise open \ + DBX Desktop or Web to complete the data security upgrade first.", + migration.error_code.as_deref().unwrap_or("KEY_PROVIDER_UNAVAILABLE") + ) + .into()); + } return Err("DATA_MIGRATION_REQUIRED: open DBX Desktop or Web to complete the data security upgrade".into()); } let configs = storage.load_connections().await?;