From d94a0fe312cc518636dc7ad2d15a30b912a6b20a Mon Sep 17 00:00:00 2001 From: t8y2 Date: Thu, 24 Sep 2026 11:27:41 +0800 Subject: [PATCH] fix(mcp): read the OS keychain key on native macOS/Windows package builds Desktop enables os-keyring and provisions the data-encryption key in the OS keychain during the data security upgrade, but dbx-mcp/dbx-cli package builds compiled without any keyring backend (the feature became opt-in for the Linux cross builds), so every desktop-migrated profile made the MCP fail with DATA_MIGRATION_REQUIRED forever - the error even pointed users back at a migration they had already completed. - dbx-mcp gains an os-keyring forwarding feature; the native macOS and Windows MCP and CLI legs of the packages release build with it, Linux zigbuild legs keep the managed .dbx/secret.key fallback - LocalBackend::open now separates 'key unreadable in this process' (SECRET_KEY_UNAVAILABLE with the DBX_SECRET_KEY_FILE/DBX_SECRET_KEY escape hatch) from genuinely pending legacy data, keeping the DATA_MIGRATION_REQUIRED contract for the latter (test covered) --- .github/workflows/mcp-release.yml | 11 +++++++++-- crates/dbx-mcp/Cargo.toml | 5 +++++ crates/dbx-mcp/src/backend.rs | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) 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?;