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)
This commit is contained in:
t8y2 committed 2026-09-24 11:27:41 +08:00
1 parent a1dab2b3ec
commit d94a0fe312
3 files changed
+32 -2

No files matched your search

+5
View File
@@ -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]]
+18
View File
@@ -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?;