diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 330714e804..0fb4a4c376 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -301,8 +301,13 @@ jobs: run: cargo check --manifest-path BitFun-Installer/src-tauri/Cargo.toml - name: Run core library tests + if: runner.os != 'Linux' run: cargo test --locked -p bitfun-core --lib + - name: Run full core library tests on Linux + if: runner.os == 'Linux' + run: cargo test --locked -p bitfun-core --features product-full --lib + - name: Run desktop library tests if: runner.os != 'Windows' run: cargo test --locked -p bitfun-desktop --lib diff --git a/Cargo.lock b/Cargo.lock index a14c48cd7c..7f4f1e8f1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -567,29 +567,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "aws-lc-rs" -version = "1.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", -] - [[package]] name = "axum" version = "0.8.9" @@ -1274,6 +1251,7 @@ dependencies = [ "axum", "base64 0.22.1", "bitfun-product-domains", + "bitfun-services-core", "chrono", "hex", "image 0.25.10", @@ -1555,6 +1533,7 @@ dependencies = [ "log", "regex", "rusqlite", + "rustls", "serde", "serde_json", "serde_yaml", @@ -1665,6 +1644,7 @@ dependencies = [ "axum", "base64 0.22.1", "bitfun-product-domains", + "bitfun-services-core", "chrono", "hex", "hmac", @@ -2247,15 +2227,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "cobs" version = "0.3.0" @@ -3752,12 +3723,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -5689,12 +5654,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "lzma-sys" version = "0.1.20" @@ -7816,63 +7775,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quinn" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" -dependencies = [ - "bytes", - "cfg_aliases 0.2.2", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.3", - "rustls", - "socket2", - "thiserror 2.0.19", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.4.3", - "lru-slab", - "rand 0.10.2", - "rand_pcg", - "ring", - "rustc-hash 2.1.3", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.19", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" -dependencies = [ - "cfg_aliases 0.2.2", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.61.2", -] - [[package]] name = "quote" version = "1.0.47" @@ -7970,15 +7872,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" -[[package]] -name = "rand_pcg" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core 0.10.1", -] - [[package]] name = "rangemap" version = "1.7.1" @@ -8166,7 +8059,6 @@ dependencies = [ "mime_guess", "percent-encoding", "pin-project-lite", - "quinn", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -8607,7 +8499,6 @@ version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ - "aws-lc-rs", "log", "once_cell", "ring", @@ -8635,7 +8526,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ - "web-time", "zeroize", ] @@ -8672,7 +8562,6 @@ version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", diff --git a/Cargo.toml b/Cargo.toml index c06e612c66..2b069164a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -266,13 +266,14 @@ tokio-tungstenite = "0.29" sherpa-onnx = "1.13.4" # MCP and remote runtimes +# Keep RMCP's Reqwest TLS provider-neutral; services-core owns ring selection. rmcp = { version = "1.7", default-features = false, features = [ "auth", "base64", "client", "elicitation", "macros", - "reqwest", + "reqwest-tls-no-provider", "schemars", "server", ] } @@ -282,7 +283,7 @@ russh-sftp = "2.1" russh-keys = "0.45" shellexpand = "3" ssh_config = "0.1" -rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } +rustls = { version = "0.23", default-features = false } [patch.crates-io] # Tauri 2.11 loses keyboard focus for child webviews after Windows reactivates diff --git a/docs/architecture/rust-build-dependency-boundaries.md b/docs/architecture/rust-build-dependency-boundaries.md index 2392668862..ee29a0d96e 100644 --- a/docs/architecture/rust-build-dependency-boundaries.md +++ b/docs/architecture/rust-build-dependency-boundaries.md @@ -97,10 +97,11 @@ Plugin Source 和完整 domain feature 集合一起带回 Agent Runtime。产品 ### 3.4 Reqwest 能力由客户端 owner 选择 - workspace 级 `reqwest` 只统一版本并关闭默认 feature,不替任何客户端选择 HTTP/2、序列化、表单、流、代理或 TLS 能力; -- 真正创建 client 的 app、service 或 adapter 必须在自身依赖声明中显式选择实际使用的 Reqwest feature 和 `reqwest/rustls`;只使用 `reqwest::Url` 的 contract/assembly 路径不加载传输能力; -- capability crate 的每个 Reqwest owner feature 必须独立带齐自己的数据/传输 feature 与 `reqwest/rustls`,不能依赖 `product-full` 或其他 feature 的 Cargo feature-union 偶然补齐; -- 边界检查以 Cargo metadata 的解码结果看护全部直接 consumer,并检查 resolved Reqwest feature union,防止传递依赖重新激活 Native TLS; -- 不并列启用 native-tls 兼容栈。只有真实产品场景无法由 Rustls 平台证书验证承载时,才以明确行为证据评审替换方案,而不是重新叠加第二后端。 +- 真正创建 client 的 app、service 或 adapter 必须在自身依赖声明中显式选择实际使用的 Reqwest feature 和 provider-neutral 的 `reqwest/rustls-no-provider`;只使用 `reqwest::Url` 的 contract/assembly 路径不加载传输能力; +- capability crate 的每个 Reqwest owner feature 必须独立带齐自己的数据/传输 feature、`reqwest/rustls-no-provider` 和进程级 TLS provider owner,不能依赖 `product-full` 或其他 feature 的 Cargo feature-union 偶然补齐; +- workspace 级 `rustls` 只统一兼容版本并关闭默认 feature;`services-core/tls-provider` 是内置 crypto provider 的唯一 owner,精确选择并安装 `ring`、`std` 和 `tls12`。产品进程入口或集中 client helper 必须在构造 TLS client 前确保该 provider 已安装; +- 边界检查以 Cargo metadata 的解码结果看护全部直接 consumer,并检查 resolved Reqwest/Rustls feature union,拒绝缺失 provider、同时选择多个 provider、传递依赖重新激活 AWS-LC 或 Native TLS,以及绕过集中 helper 的 Reqwest client 构造; +- 不并列启用 Native TLS 或 AWS-LC 兼容栈。只有真实产品场景无法由当前 Ring/Rustls 平台证书验证承载时,才以明确行为证据评审替换方案;替换时由同一 owner 切换 provider,不能在同一产品闭包叠加第二后端。 ### 3.5 稳定契约 crate 按消费能力切片 diff --git a/scripts/check-core-boundaries.test.mjs b/scripts/check-core-boundaries.test.mjs index 4fef34316e..15c84b6ede 100644 --- a/scripts/check-core-boundaries.test.mjs +++ b/scripts/check-core-boundaries.test.mjs @@ -188,6 +188,71 @@ test('Agent Runtime leaf capabilities have one managed feature and source contra } }); +test('Server canonical Agent Runtime ownership is protected by source boundary rules', async () => { + const bootstrapPath = 'src/apps/server/src/bootstrap.rs'; + const mainPath = 'src/apps/server/src/main.rs'; + const bootstrapRequired = requiredContentRules.find((rule) => rule.path === bootstrapPath); + const bootstrapForbidden = forbiddenContentRules.find((rule) => rule.path === bootstrapPath); + const mainRequired = requiredContentRules.find((rule) => rule.path === mainPath); + const mainForbidden = forbiddenContentRules.find((rule) => rule.path === mainPath); + + assert.ok(bootstrapRequired, 'Server bootstrap must have a required ownership contract'); + assert.ok(bootstrapForbidden, 'Server bootstrap must reject duplicate Runtime assembly'); + assert.ok(mainRequired, 'Server main must retain the product event owner'); + assert.ok(mainForbidden, 'Server main must reject an unowned event source'); + + const [bootstrapSource, mainSource] = await Promise.all([ + readFile(new URL('../src/apps/server/src/bootstrap.rs', import.meta.url), 'utf8'), + readFile(new URL('../src/apps/server/src/main.rs', import.meta.url), 'utf8'), + ]); + for (const pattern of bootstrapRequired.patterns) { + assert.match(bootstrapSource, pattern.regex, pattern.message); + } + for (const pattern of mainRequired.patterns) { + assert.match(mainSource, pattern.regex, pattern.message); + } + assert.match( + 'let queue = EventQueue::new(Default::default());', + bootstrapForbidden.patterns[0].regex, + ); + assert.match( + 'let source = AgentEventSource::new(event_queue);', + mainForbidden.patterns[0].regex, + ); +}); + +test('TLS source boundaries reject bypasses of the centralized provider owner', () => { + const integrationsRule = forbiddenContentUnderRules.find((rule) => + rule.path === 'src/crates/services/services-integrations/src' + && rule.reason.includes('provider-initializing Reqwest constructors')); + const providerRule = forbiddenContentUnderRules.find((rule) => + rule.path === 'src' + && rule.reason.includes('only owner allowed to install')); + assert.ok(integrationsRule, 'integration Reqwest constructors must be guarded'); + assert.ok(providerRule, 'direct Rustls provider installation must be guarded'); + + assert.ok(integrationsRule.patterns.some((pattern) => pattern.regex.test( + 'use reqwest::Client;\nfn client() { let _ = Client::builder(); }', + ))); + assert.ok(integrationsRule.patterns.some((pattern) => pattern.regex.test( + 'use reqwest::{Client as HttpClient, Url};\nfn client() { let _ = HttpClient::new(); }', + ))); + assert.ok(integrationsRule.patterns.some((pattern) => pattern.regex.test( + 'fn client() { let _ = reqwest::ClientBuilder::new(); }', + ))); + assert.ok(integrationsRule.patterns.some((pattern) => pattern.regex.test( + 'use reqwest as http;\nfn client() { let _ = http::Client::new(); }', + ))); + assert.match( + 'rustls::crypto::ring::default_provider().install_default();', + providerRule.patterns[0].regex, + ); + assert.doesNotMatch( + 'unrelated_component.install_default();', + providerRule.patterns[0].regex, + ); +}); + test('Core and ACP defaults preserve their explicit assembly contracts', async () => { const [coreManifest, acpManifest] = await Promise.all([ readFile(new URL('../src/crates/assembly/core/Cargo.toml', import.meta.url), 'utf8'), @@ -2640,11 +2705,11 @@ test('services integrations Reqwest policy uses Cargo-decoded feature references const pkg = servicesIntegrationsPackage(` [features] reqwest = ["dep:reqwest"] -announcement = ["reqwest", "reqwest/rustls"] +announcement = ["reqwest", "reqwest/rustls-no-provider"] file-watch = ["reqwest?/__native-tls"] -mcp = ["reqwest", "reqwest/rustls", "reqwest/json"] -models-dev = ["reqwest", "reqwest/rustls", "reqwest/system-proxy"] -speech = ["reqwest", "reqwest/rustls", "reqwest/http3"] +mcp = ["reqwest", "reqwest/rustls-no-provider", "reqwest/json"] +models-dev = ["reqwest", "reqwest/rustls-no-provider", "reqwest/system-proxy"] +speech = ["reqwest", "reqwest/rustls-no-provider", "reqwest/http3"] `); const messages = findServicesIntegrationsReqwestFeatureViolations(pkg) @@ -2655,6 +2720,26 @@ speech = ["reqwest", "reqwest/rustls", "reqwest/http3"] assert.match(messages, /mcp.*missing Reqwest feature reference reqwest\/stream/); assert.doesNotMatch(messages, /models-dev.*system-proxy/); assert.match(messages, /speech.*unreviewed Reqwest feature reference reqwest\/http3/); + assert.doesNotMatch(messages, /missing reqwest\/rustls\b/); + assert.doesNotMatch(messages, /unreviewed.*reqwest\/rustls-no-provider/); +}); + +test('every services integrations Reqwest owner activates the reviewed TLS provider', async () => { + const manifest = await readFile( + new URL('../src/crates/services/services-integrations/Cargo.toml', import.meta.url), + 'utf8', + ); + const mutated = removeFeatureValue( + manifest, + 'review-platform', + 'bitfun-services-core/tls-provider', + ); + assert.notEqual(mutated, manifest, 'review-platform must own the TLS provider in the fixture'); + + const messages = findServicesIntegrationsReqwestFeatureViolations( + servicesIntegrationsPackage(mutated), + ).map((violation) => violation.message).join('\n'); + assert.match(messages, /review-platform.*missing bitfun-services-core\/tls-provider/); }); test('direct Reqwest clients reject extra decoded dependency and package features', () => { @@ -2667,7 +2752,7 @@ test('direct Reqwest clients reject extra decoded dependency and package feature features: [ 'http2', 'stream', - 'rustls', + 'rustls-no-provider', '__native-tls', ], }]), @@ -2691,26 +2776,42 @@ test('direct Reqwest clients reject extra decoded dependency and package feature test('AI adapters Reqwest profile owns the supported SOCKS transport', () => { const baseFeatures = ['http2', 'json', 'stream']; const valid = { - ...packageAt('bitfun-ai-adapters', 'src/crates/adapters/ai-adapters/Cargo.toml', [{ - name: 'reqwest', - kind: null, - optional: false, - uses_default_features: false, - features: [...baseFeatures, 'rustls', 'socks'], - }]), + ...packageAt('bitfun-ai-adapters', 'src/crates/adapters/ai-adapters/Cargo.toml', [ + { + name: 'reqwest', + kind: null, + optional: false, + uses_default_features: false, + features: [...baseFeatures, 'rustls-no-provider', 'socks'], + }, + { + name: 'bitfun-services-core', + kind: null, + optional: false, + features: ['tls-provider'], + }, + ]), features: { 'subscription-auth': ['reqwest/form'] }, }; const missingSocks = { ...packageAt( 'bitfun-ai-adapters', 'src/crates/adapters/ai-adapters/Cargo.toml', - [{ - name: 'reqwest', - kind: null, - optional: false, - uses_default_features: false, - features: [...baseFeatures, 'rustls'], - }], + [ + { + name: 'reqwest', + kind: null, + optional: false, + uses_default_features: false, + features: [...baseFeatures, 'rustls-no-provider'], + }, + { + name: 'bitfun-services-core', + kind: null, + optional: false, + features: ['tls-provider'], + }, + ], ), features: { 'subscription-auth': ['reqwest/form'] }, }; @@ -2783,8 +2884,11 @@ test('Reqwest consumers inherit the workspace version without duplicating featur assert.equal(rules.length, 7); for (const rule of rules) { const pattern = rule.patterns[0].regex; - assert.match('reqwest = { workspace = true, features = ["rustls"] }', pattern); - assert.doesNotMatch('reqwest = { version = "99", features = ["rustls"] }', pattern); + assert.match('reqwest = { workspace = true, features = ["rustls-no-provider"] }', pattern); + assert.doesNotMatch( + 'reqwest = { version = "99", features = ["rustls-no-provider"] }', + pattern, + ); } }); @@ -2977,6 +3081,11 @@ test('resolved Reqwest feature union rejects every native TLS backend alias', () version: '0.12.28', features: ['rustls-tls', 'default-tls'], }, + { + name: 'rustls', + version: '0.23.42', + features: ['ring', 'std'], + }, ], { root: TEST_ROOT }, ); @@ -2987,6 +3096,132 @@ test('resolved Reqwest feature union rejects every native TLS backend alias', () assert.match(messages, /reqwest 0\.12\.28.*default-tls/); }); +test('resolved Reqwest feature union rejects the AWS-LC selecting Rustls alias', () => { + const violations = findResolvedReqwestNativeTlsViolations( + [{ + name: 'reqwest', + version: '0.13.4', + features: ['rustls', 'rustls-no-provider'], + }, { + name: 'rustls', + version: '0.23.42', + features: ['ring', 'std'], + }], + { root: TEST_ROOT }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /rustls.*AWS-LC/); +}); + +test('resolved Rustls feature union rejects multiple crypto providers', () => { + const violations = findResolvedReqwestNativeTlsViolations( + [ + { + name: 'reqwest', + version: '0.13.4', + features: ['rustls-no-provider'], + }, + { + name: 'rustls', + version: '0.23.42', + features: ['aws_lc_rs', 'ring', 'std'], + }, + ], + { root: TEST_ROOT }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /only the ring crypto provider.*aws_lc_rs, ring/); +}); + +test('resolved Rustls feature union normalizes and rejects AWS-LC aliases once', () => { + const violations = findResolvedReqwestNativeTlsViolations( + [ + { + name: 'reqwest', + version: '0.13.4', + features: ['rustls-no-provider'], + }, + { + name: 'rustls', + version: '0.23.42', + features: ['aws-lc-rs', 'aws_lc_rs', 'std'], + }, + ], + { root: TEST_ROOT }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /selects aws_lc_rs$/); +}); + +test('resolved Rustls feature union rejects providers split across Rustls versions', () => { + const violations = findResolvedReqwestNativeTlsViolations( + [ + { + name: 'reqwest', + version: '0.13.4', + features: ['rustls-no-provider'], + }, + { + name: 'rustls', + version: '0.23.42', + features: ['ring', 'std'], + }, + { + name: 'rustls', + version: '0.22.4', + features: ['aws_lc_rs', 'std'], + }, + ], + { root: TEST_ROOT }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /only the ring crypto provider.*aws_lc_rs, ring/); +}); + +test('resolved Reqwest Rustls closure rejects a missing crypto provider', () => { + const violations = findResolvedReqwestNativeTlsViolations( + [{ + name: 'reqwest', + version: '0.13.4', + features: ['rustls-no-provider'], + }], + { root: TEST_ROOT }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /selects \(no provider\)/); +}); + +test('resolved Reqwest Rustls closure rejects linked AWS-LC packages', () => { + const violations = findResolvedReqwestNativeTlsViolations( + [ + { + name: 'reqwest', + version: '0.13.4', + features: ['rustls-no-provider'], + }, + { + name: 'rustls', + version: '0.23.42', + features: ['ring', 'std'], + }, + { + name: 'aws-lc-rs', + version: '1.13.3', + features: [], + }, + ], + { root: TEST_ROOT }, + ); + + assert.equal(violations.length, 1); + assert.match(violations[0].message, /aws_lc_rs, ring/); +}); + test('Cargo metadata Tokio policy catches table-style and renamed full dependencies', () => { const pkg = packageAt('table-style', 'src/crates/services/table-style/Cargo.toml', [{ name: 'tokio', diff --git a/scripts/check-github-config.test.mjs b/scripts/check-github-config.test.mjs index 4ead104878..ffab49fcef 100644 --- a/scripts/check-github-config.test.mjs +++ b/scripts/check-github-config.test.mjs @@ -250,6 +250,11 @@ test('keeps Rust CI independent, restore-only on PRs, and target-focused', () => 'build-impact', 'Rust validation must not wait for the frontend build', ); + assert.deepEqual( + rustJob.strategy.matrix.os, + ['ubuntu-latest', 'macos-15', 'windows-latest'], + 'Rust validation must retain the reviewed Linux, macOS, and Windows matrix', + ); assert.equal( rustJob.steps.some((step) => step.uses?.startsWith('actions/download-artifact@')), false, @@ -376,6 +381,12 @@ test('keeps Rust CI independent, restore-only on PRs, and target-focused', () => const coreLibraryTests = rustJob.steps.find( (step) => step.name === 'Run core library tests', ); + const linuxFullCoreLibraryTests = rustJob.steps.find( + (step) => step.name === 'Run full core library tests on Linux', + ); + const coreLibraryTestSteps = rustJob.steps.filter( + (step) => /^cargo test --locked -p bitfun-core\b.*\s--lib$/.test(step.run ?? ''), + ); const desktopLibraryTests = rustJob.steps.find( (step) => step.name === 'Run desktop library tests', ); @@ -385,10 +396,36 @@ test('keeps Rust CI independent, restore-only on PRs, and target-focused', () => const productControlContracts = rustJob.steps.find( (step) => step.name === 'Run product-control domain and delivery-profile contracts', ); + assert.equal( + coreLibraryTests?.if, + "runner.os != 'Linux'", + ); assert.equal( coreLibraryTests?.run, 'cargo test --locked -p bitfun-core --lib', ); + assert.equal( + linuxFullCoreLibraryTests?.if, + "runner.os == 'Linux'", + ); + assert.equal( + linuxFullCoreLibraryTests?.run, + 'cargo test --locked -p bitfun-core --features product-full --lib', + ); + assert.deepEqual( + coreLibraryTestSteps.map((step) => ({ if: step.if, run: step.run })), + [ + { + if: "runner.os != 'Linux'", + run: 'cargo test --locked -p bitfun-core --lib', + }, + { + if: "runner.os == 'Linux'", + run: 'cargo test --locked -p bitfun-core --features product-full --lib', + }, + ], + 'Core library validation must contain exactly the reviewed complementary steps', + ); assert.equal(desktopLibraryTests?.if, "runner.os != 'Windows'"); assert.equal( desktopLibraryTests?.run, diff --git a/scripts/core-boundaries/cargo-dependency-boundaries.mjs b/scripts/core-boundaries/cargo-dependency-boundaries.mjs index 8a4f7c94f2..665732809e 100644 --- a/scripts/core-boundaries/cargo-dependency-boundaries.mjs +++ b/scripts/core-boundaries/cargo-dependency-boundaries.mjs @@ -303,28 +303,32 @@ const REQWEST_PACKAGE_PROFILES = new Map([ servicesOwners: true, }], ['bitfun-ai-adapters', { - dependencyFeatures: ['http2', 'json', 'rustls', 'socks', 'stream'], + dependencyFeatures: ['http2', 'json', 'rustls-no-provider', 'socks', 'stream'], optional: false, allowedPackageFeatureRefs: new Set(['reqwest/form']), requiredPackageFeatureRefs: new Map([ ['subscription-auth', new Set(['reqwest/form'])], ]), + tlsProviderDependency: 'bitfun-services-core', }], ['bitfun-cli', { - dependencyFeatures: ['http2', 'rustls', 'stream'], + dependencyFeatures: ['http2', 'rustls-no-provider', 'stream'], optional: false, + tlsProviderDependency: 'bitfun-services-core', }], ['bitfun-desktop', { - dependencyFeatures: ['http2', 'json', 'query', 'rustls', 'stream'], + dependencyFeatures: ['http2', 'json', 'query', 'rustls-no-provider', 'stream'], optional: false, }], ['bitfun-miniapp-market-service', { - dependencyFeatures: ['form', 'http2', 'json', 'rustls'], + dependencyFeatures: ['form', 'http2', 'json', 'rustls-no-provider'], optional: false, + tlsProviderDependency: 'bitfun-services-core', }], ['bitfun-skin-market-service', { - dependencyFeatures: ['http2', 'json', 'rustls'], + dependencyFeatures: ['http2', 'json', 'rustls-no-provider'], optional: false, + tlsProviderDependency: 'bitfun-services-core', }], ]); @@ -423,6 +427,26 @@ function findReqwestPackageProfileViolations(pkg, profile) { } } + if (profile.tlsProviderDependency) { + const providerDependencies = (pkg.dependencies ?? []).filter( + (candidate) => candidate.name === profile.tlsProviderDependency + && (candidate.kind ?? null) === null + && (candidate.target ?? null) === null, + ); + if ( + providerDependencies.length !== 1 + || !(providerDependencies[0].features ?? []).includes('tls-provider') + ) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: + `${pkg.name} must select ${profile.tlsProviderDependency}/tls-provider ` + + 'for its provider-neutral Reqwest client', + }); + } + } + return violations; } @@ -930,7 +954,7 @@ export function findResolvedReqwestNativeTlsViolations(records, { root }) { }]; } - return reqwestRecords.flatMap((record) => { + const reqwestViolations = reqwestRecords.flatMap((record) => { const nativeTlsFeatures = (record.features ?? []).filter( (feature) => feature === 'default-tls' @@ -939,7 +963,12 @@ export function findResolvedReqwestNativeTlsViolations(records, { root }) { || feature === 'native-tls' || feature.startsWith('native-tls-'), ); - if (nativeTlsFeatures.length === 0) { + const providerSelectingFeatures = record.version.startsWith('0.13.') + && (record.features ?? []).includes('rustls') + ? ['rustls (selects AWS-LC)'] + : []; + const unreviewedTlsFeatures = [...nativeTlsFeatures, ...providerSelectingFeatures]; + if (unreviewedTlsFeatures.length === 0) { return []; } return [{ @@ -947,9 +976,35 @@ export function findResolvedReqwestNativeTlsViolations(records, { root }) { line: 1, message: `resolved reqwest ${record.version} feature union enables an unreviewed TLS backend: ` - + nativeTlsFeatures.join(', '), + + unreviewedTlsFeatures.join(', '), }]; }); + + const rustlsRecords = records.filter((record) => record.name === 'rustls'); + const selectedRustlsProviders = new Set(rustlsRecords + .flatMap((record) => (record.features ?? []) + .filter((feature) => feature === 'ring' || feature === 'aws_lc_rs' || feature === 'aws-lc-rs') + .map((feature) => feature === 'ring' ? feature : 'aws_lc_rs'))); + if (records.some((record) => record.name === 'aws-lc-rs' || record.name === 'aws-lc-sys')) { + selectedRustlsProviders.add('aws_lc_rs'); + } + const sortedRustlsProviders = [...selectedRustlsProviders].sort(); + const rustlsVersions = [...new Set(rustlsRecords.map((record) => record.version))].sort(); + const reqwestUsesRustls = reqwestRecords.some((record) => + (record.features ?? []).some((feature) => feature.includes('rustls'))); + const rustlsProviderViolations = !reqwestUsesRustls + || (sortedRustlsProviders.length === 1 && sortedRustlsProviders[0] === 'ring') + ? [] + : [{ + path: join(root, 'Cargo.toml'), + line: 1, + message: + `resolved Reqwest/Rustls closure must select only the ring crypto provider; ` + + `rustls ${rustlsVersions.join(', ') || '(missing)'} selects ` + + (sortedRustlsProviders.join(', ') || '(no provider)'), + }]; + + return [...reqwestViolations, ...rustlsProviderViolations]; } export function findServicesIntegrationsReqwestFeatureViolations(pkg) { @@ -987,11 +1042,18 @@ export function findServicesIntegrationsReqwestFeatureViolations(pkg) { message: `${pkg.name}:${featureName} must explicitly enable reqwest`, }); } - if (!references.includes('reqwest/rustls')) { + if (!references.includes('reqwest/rustls-no-provider')) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: `${pkg.name}:${featureName} is missing reqwest/rustls-no-provider`, + }); + } + if (!references.includes('bitfun-services-core/tls-provider')) { violations.push({ path: pkg.manifest_path, line: 1, - message: `${pkg.name}:${featureName} is missing reqwest/rustls`, + message: `${pkg.name}:${featureName} is missing bitfun-services-core/tls-provider`, }); } for (const reference of ownerFeatureReferences.get(featureName) ?? []) { @@ -1026,7 +1088,7 @@ export function findServicesIntegrationsReqwestFeatureViolations(pkg) { const allowedReferences = new Set([ 'reqwest', 'dep:reqwest', - 'reqwest/rustls', + 'reqwest/rustls-no-provider', ...(ownerFeatureReferences.get(featureName) ?? []), ]); for (const reference of reqwestReferences) { diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index ed75390271..292ce6802f 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -66,6 +66,7 @@ export const optionalDependencyFeatureOwnerRules = [ ], }, { depName: 'rusqlite', ownerFeatures: ['permission', 'session-search'] }, + { depName: 'rustls', ownerFeatures: ['tls-provider'] }, { depName: 'serde_yaml', ownerFeatures: ['markdown', 'workspace-instructions'] }, { depName: 'similar', ownerFeatures: ['diff', 'local-storage'] }, { @@ -266,7 +267,24 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'bitfun-runtime-ports', ownerFeatures: ['deep-research', 'git', 'remote-connect', 'remote-ssh', 'remote-ssh-concrete', 'script-tool-runtime'] }, { depName: 'bitfun-services-core', - ownerFeatures: ['browser-control', 'git', 'hook-import', 'mcp', 'miniapp-runtime', 'process-tree', 'remote-connect', 'remote-ssh', 'review-platform', 'workspace-search'], + ownerFeatures: [ + 'announcement', + 'browser-control', + 'git', + 'hook-import', + 'mcp', + 'miniapp-market', + 'miniapp-runtime', + 'models-dev', + 'process-tree', + 'remote-connect', + 'remote-ssh', + 'remote-ssh-concrete', + 'review-platform', + 'speech', + 'web-tools', + 'workspace-search', + ], }, { depName: 'bzip2', ownerFeatures: ['speech'] }, { depName: 'chrono', ownerFeatures: ['git', 'miniapp-market', 'remote-connect', 'remote-ssh-concrete', 'review-platform', 'speech'] }, @@ -1321,6 +1339,13 @@ export const coreClosedFeatureProfileRules = [ exact: true, reason: 'services-core default profile must stay empty so consumers select capabilities explicitly', }, + { + manifestPath: 'src/crates/services/services-core/Cargo.toml', + featureName: 'tls-provider', + requiredFeatureRefs: ['dep:rustls'], + exact: true, + reason: 'services-core TLS provider owner must select only the workspace Rustls implementation', + }, { manifestPath: 'src/crates/services/services-core/Cargo.toml', featureName: 'diagnostics', diff --git a/scripts/core-boundaries/rules/source/forbidden-rules.mjs b/scripts/core-boundaries/rules/source/forbidden-rules.mjs index 9a4caf3e15..21882de3c1 100644 --- a/scripts/core-boundaries/rules/source/forbidden-rules.mjs +++ b/scripts/core-boundaries/rules/source/forbidden-rules.mjs @@ -8,6 +8,17 @@ const agentRuntimeRootUnexpectedLine = new RegExp( ); export const forbiddenContentRules = [ + { + path: 'Cargo.toml', + reason: + 'workspace Rustls owns only the compatible version; concrete provider features belong to services-core', + patterns: [ + { + regex: /^rustls\s*=\s*\{[^\n}]*,\s*features\s*=/m, + message: 'root workspace Rustls dependency must not select crypto-provider capabilities', + }, + ], + }, { path: 'src/crates/execution/agent-runtime/src/lib.rs', reason: @@ -3967,6 +3978,39 @@ export const forbiddenContentRules = [ }, ], }, + { + path: 'src/apps/server/src/bootstrap.rs', + reason: + 'Server must not assemble a second Agent Runtime beside the canonical Core owner', + patterns: [ + { + regex: /\b(?:EventQueue|EventRouter|SessionManager|ToolPipeline|ExecutionEngine|ConversationCoordinator)::new\s*\(/, + message: 'Server bootstrap must not directly construct canonical Agent Runtime components', + }, + ], + }, + { + path: 'src/apps/server/src/main.rs', + reason: + 'Server must retain the product event-queue owner instead of bypassing legacy queue draining', + patterns: [ + { + regex: /\bAgentEventSource::new\s*\(/, + message: 'Server must not construct an unowned Agent event source', + }, + ], + }, + { + path: 'src/apps/desktop/src/api/app_state.rs', + reason: + 'token usage publication belongs to the canonical Core Agent Runtime initializer for every embedded host', + patterns: [ + { + regex: /\bset_global_token_usage_service\s*\(/, + message: 'Desktop AppState must not re-own canonical token usage publication', + }, + ], + }, ]; export const rustWebUiSourceBoundaryRule = { @@ -4375,4 +4419,36 @@ export const forbiddenContentUnderRules = [ }, ], }, + { + path: 'src/crates/services/services-integrations/src', + reason: + 'integration modules must use the crate-level provider-initializing Reqwest constructors', + patterns: [ + { + regex: /\breqwest::(?:get|Client::(?:new|builder)|ClientBuilder::new)\s*\(/, + allowPaths: ['src/crates/services/services-integrations/src/lib.rs'], + message: 'use crate::reqwest_client or crate::reqwest_client_builder', + }, + { + regex: /use\s+reqwest(?:::(?:Client|ClientBuilder)|::\{[^}]*\b(?:Client|ClientBuilder)\b[^}]*\})[^;]*;/, + message: 'keep Reqwest client types fully qualified and construct them through crate-level TLS helpers', + }, + { + regex: /use\s+reqwest\s+as\s+\w+\s*;/, + message: 'do not alias Reqwest around the centralized client-construction guard', + }, + ], + }, + { + path: 'src', + reason: + 'services-core is the only owner allowed to install the process-wide Rustls provider', + patterns: [ + { + regex: /\brustls::crypto::(?:ring|aws_lc_rs)\b/, + allowPaths: ['src/crates/services/services-core/src/tls_provider.rs'], + message: 'delegate built-in Rustls provider selection to services-core::tls_provider', + }, + ], + }, ]; diff --git a/scripts/core-boundaries/rules/source/required-rules.mjs b/scripts/core-boundaries/rules/source/required-rules.mjs index 900dcbae49..9963951f42 100644 --- a/scripts/core-boundaries/rules/source/required-rules.mjs +++ b/scripts/core-boundaries/rules/source/required-rules.mjs @@ -10090,4 +10090,119 @@ export const requiredContentRules = [ }, ], }, + { + path: 'src/apps/server/src/bootstrap.rs', + reason: + 'Server must remain on the canonical Core Agent Runtime and product event-queue owners', + patterns: [ + { + regex: /init_agentic_system_for_profile_with_runtime_ownership\s*\(/, + message: 'Server bootstrap must use the canonical Core Agent Runtime initializer', + }, + { + regex: /CoreProductEventQueueOwner::new\s*\(/, + message: 'Server bootstrap must retain the product event-queue owner', + }, + ], + }, + { + path: 'src/apps/server/src/main.rs', + reason: + 'Server App Server event projection must consume the product-owned runtime source', + patterns: [ + { + regex: /agent_event_queue_owner\.runtime_source\s*\(\s*\)/, + message: 'Server must consume events through CoreProductEventQueueOwner', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/system.rs', + reason: + 'the canonical Core Agent Runtime initializer must publish its token usage service for every embedded host', + patterns: [ + { + regex: /set_global_token_usage_service\s*\(\s*token_usage_service\.clone\s*\(\s*\)\s*\)/, + message: 'canonical Agent Runtime initialization must publish token usage globally', + }, + ], + }, + { + path: 'src/crates/services/services-core/src/tls_provider.rs', + reason: + 'provider-neutral TLS clients must share the services-owned process-level ring selection', + patterns: [ + { + regex: /rustls::crypto::ring::default_provider\s*\(\s*\)\.install_default\s*\(\s*\)/, + message: 'services-core TLS owner must install the workspace-owned ring provider', + }, + ], + }, + { + path: 'src/crates/services/services-core/Cargo.toml', + reason: + 'services-core must own the exact Rustls provider and protocol feature selection', + patterns: [ + { + regex: /rustls = \{ workspace = true, features = \["ring", "std", "tls12"\], optional = true \}/, + message: 'services-core TLS owner must select only ring, std, and TLS 1.2 support', + }, + ], + }, + { + path: 'src/crates/services/services-integrations/src/lib.rs', + reason: + 'integration clients must initialize the reviewed TLS provider through centralized constructors', + patterns: [ + { + regex: /fn reqwest_client_builder[\s\S]*?ensure_ring_crypto_provider\s*\(\s*\)/, + message: 'integration Reqwest builder must initialize the ring provider', + }, + { + regex: /fn reqwest_client\s*\([^)]*\)[\s\S]*?ensure_ring_crypto_provider\s*\(\s*\)/, + message: 'integration default Reqwest client must initialize the ring provider', + }, + ], + }, + { + path: 'src/crates/adapters/ai-adapters/src/client/http.rs', + reason: 'AI transport clients must initialize the reviewed TLS provider before Reqwest', + patterns: [ + { + regex: /fn create_http_client[\s\S]*?ensure_ring_crypto_provider\s*\(\s*\)[\s\S]*?Client::builder\s*\(/, + message: 'AI client construction must initialize the ring provider first', + }, + ], + }, + { + path: 'src/crates/adapters/ai-adapters/src/subscription_auth/mod.rs', + reason: + 'subscription authentication clients must initialize the same reviewed TLS provider as normal AI transport', + patterns: [ + { + regex: /fn build_http_client[\s\S]*?ensure_ring_crypto_provider\s*\(\s*\)[\s\S]*?reqwest::Client::builder\s*\(/, + message: 'subscription HTTP client construction must initialize the ring provider first', + }, + ], + }, + { + path: 'src/crates/services/miniapp-market-service/src/auth.rs', + reason: 'MiniApp market HTTP clients must initialize the reviewed TLS provider', + patterns: [ + { + regex: /fn new\s*\([^)]*MarketConfig[\s\S]*?ensure_ring_crypto_provider\s*\(\s*\)[\s\S]*?reqwest::Client::builder\s*\(/, + message: 'MiniApp market AuthService must initialize the ring provider first', + }, + ], + }, + { + path: 'src/crates/services/skin-market-service/src/auth.rs', + reason: 'appearance market HTTP clients must initialize the reviewed TLS provider', + patterns: [ + { + regex: /fn new\s*\([^)]*Url[\s\S]*?ensure_ring_crypto_provider\s*\(\s*\)[\s\S]*?Client::builder\s*\(/, + message: 'appearance market IdentityVerifier must initialize the ring provider first', + }, + ], + }, ]; diff --git a/src/apps/cli/Cargo.toml b/src/apps/cli/Cargo.toml index 36aa5262af..aa78508ce2 100644 --- a/src/apps/cli/Cargo.toml +++ b/src/apps/cli/Cargo.toml @@ -60,7 +60,7 @@ bitfun-agent-runtime-ipc = { path = "../../crates/adapters/agent-runtime-ipc" } bitfun-app-server = { path = "../../crates/interfaces/app-server" } bitfun-runtime-ports = { path = "../../crates/contracts/runtime-ports", features = ["agent-api", "git-port", "permission", "plugin-runtime", "workspace-ports"] } bitfun-runtime-services = { path = "../../crates/execution/runtime-services" } -bitfun-services-core = { path = "../../crates/services/services-core", features = ["dispatch-workspace", "local-storage", "process-runtime", "runtime-ownership"] } +bitfun-services-core = { path = "../../crates/services/services-core", features = ["dispatch-workspace", "local-storage", "process-runtime", "runtime-ownership", "tls-provider"] } bitfun-agent-tools = { path = "../../crates/execution/tool-contracts" } bitfun-product-domains = { path = "../../crates/contracts/product-domains", features = ["external-sources"] } @@ -120,7 +120,7 @@ fs2 = { workspace = true } base64 = { workspace = true } image = { workspace = true, features = ["gif", "jpeg", "png", "webp"] } minisign-verify = "0.2" -reqwest = { workspace = true, features = ["http2", "rustls", "stream"] } +reqwest = { workspace = true, features = ["http2", "rustls-no-provider", "stream"] } sha2 = { workspace = true } tar = { workspace = true } tempfile = "3" diff --git a/src/apps/cli/src/main.rs b/src/apps/cli/src/main.rs index baca196035..7eb1f00a6d 100644 --- a/src/apps/cli/src/main.rs +++ b/src/apps/cli/src/main.rs @@ -1677,9 +1677,9 @@ async fn run_interactive_with_session( } fn main() { - // Install rustls CryptoProvider before any TLS-capable work (relay WS, - // reqwest rustls paths, Feishu wss). Required when both ring and aws-lc-rs - // are linked: rustls cannot auto-select a provider. + // Install the workspace-owned ring CryptoProvider before any TLS-capable + // work (relay WS, reqwest rustls paths, Feishu wss) so every client uses + // the same explicit process-level provider. bitfun_core::service::remote_connect::ensure_rustls_crypto_provider(); let worker = std::thread::Builder::new() diff --git a/src/apps/cli/src/self_update.rs b/src/apps/cli/src/self_update.rs index 7f17be4526..cdec225042 100644 --- a/src/apps/cli/src/self_update.rs +++ b/src/apps/cli/src/self_update.rs @@ -671,6 +671,7 @@ fn platform_asset<'a>( } fn build_client() -> Result { + bitfun_services_core::tls_provider::ensure_ring_crypto_provider(); Client::builder() .connect_timeout(Duration::from_secs(8)) // Deliberately no `.timeout()`: a whole-request ceiling turns "slow" into diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index 6fc396bd70..6b44ddf8f3 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -65,7 +65,7 @@ dark-light = { workspace = true } similar = { workspace = true } ignore = { workspace = true } urlencoding = { workspace = true } -reqwest = { workspace = true, features = ["http2", "json", "query", "rustls", "stream"] } +reqwest = { workspace = true, features = ["http2", "json", "query", "rustls-no-provider", "stream"] } semver = { workspace = true } zip = { workspace = true } tar = { workspace = true } diff --git a/src/apps/desktop/src/api/app_state.rs b/src/apps/desktop/src/api/app_state.rs index 838d07c9f8..43359e5b55 100644 --- a/src/apps/desktop/src/api/app_state.rs +++ b/src/apps/desktop/src/api/app_state.rs @@ -104,11 +104,6 @@ impl AppState { ) -> BitFunResult { let start_time = std::time::Instant::now(); - // Make the usage store reachable from tools that call the model outside - // the round executor (e.g. `analyze_image`), so their consumption is - // included in the usage statistics. - token_usage::set_global_token_usage_service(token_usage_service.clone()); - let config_service = config::get_global_config_service().await.map_err(|e| { BitFunError::config(format!("Failed to get global config service: {}", e)) })?; diff --git a/src/apps/desktop/src/api/relay_deploy_api.rs b/src/apps/desktop/src/api/relay_deploy_api.rs index 207c06d61d..0150dc83df 100644 --- a/src/apps/desktop/src/api/relay_deploy_api.rs +++ b/src/apps/desktop/src/api/relay_deploy_api.rs @@ -158,6 +158,7 @@ pub async fn relay_deploy_verify(relay_url: String) -> Result Result Vec { + crate::ensure_rustls_crypto_provider(); let client = match reqwest::Client::builder() .connect_timeout(std::time::Duration::from_secs(5)) .read_timeout(PROBE_WINDOW) diff --git a/src/apps/desktop/src/embedded_relay_host.rs b/src/apps/desktop/src/embedded_relay_host.rs index 72fd793762..e663084eee 100644 --- a/src/apps/desktop/src/embedded_relay_host.rs +++ b/src/apps/desktop/src/embedded_relay_host.rs @@ -294,6 +294,7 @@ mod tests { let host = DesktopEmbeddedRelayHost::default(); let port = start_on_free_port(&host, Some(static_dir.to_string_lossy().into_owned())).await; + crate::ensure_rustls_crypto_provider(); let client = reqwest::Client::new(); let index = client .get(format!("http://127.0.0.1:{port}/")) diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 1f7c767b60..2f21d62ff7 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -94,6 +94,10 @@ use startup_trace::{DesktopStartupTrace, DesktopStartupTraceSnapshot}; pub(crate) const PLUGIN_HOST_LAUNCH_POLICY: bitfun_core::plugin_host::PluginHostLaunchPolicy = bitfun_core::plugin_host::PluginHostLaunchPolicy::Disabled; +pub(crate) fn ensure_rustls_crypto_provider() { + bitfun_core::service::remote_connect::ensure_rustls_crypto_provider(); +} + /// Agentic Coordinator state #[derive(Clone)] pub struct CoordinatorState { @@ -589,7 +593,7 @@ pub async fn run() { // Install the rustls ring CryptoProvider as the process-level default early, // so that all subsequent TLS operations (relay_client, reqwest, tokio-tungstenite) // reuse the same provider instead of each attempting their own install_default(). - bitfun_core::service::remote_connect::ensure_rustls_crypto_provider(); + ensure_rustls_crypto_provider(); eprintln!("=== BitFun Desktop Starting ==="); diff --git a/src/apps/sdk-host/Cargo.toml b/src/apps/sdk-host/Cargo.toml index afa6bddf81..3a483fe6e6 100644 --- a/src/apps/sdk-host/Cargo.toml +++ b/src/apps/sdk-host/Cargo.toml @@ -30,10 +30,10 @@ bitfun-core = { path = "../../crates/assembly/core", features = [ bitfun-sdk-host = { path = "../../crates/interfaces/sdk-host" } bitfun-services-core = { path = "../../crates/services/services-core", features = [ "process-runtime", + "tls-provider", ] } bitfun-transport = { path = "../../crates/adapters/transport" } futures-util = { workspace = true } -rustls = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["io-std", "io-util", "macros", "rt-multi-thread", "sync", "time"] } tokio-util = { workspace = true, features = ["codec"] } @@ -42,6 +42,7 @@ tracing-subscriber = { workspace = true } url = { workspace = true } [dev-dependencies] +rustls = { workspace = true } tempfile = "3" tokio = { workspace = true, features = ["process"] } diff --git a/src/apps/sdk-host/src/lib.rs b/src/apps/sdk-host/src/lib.rs index 062de67bb9..1c133d690c 100644 --- a/src/apps/sdk-host/src/lib.rs +++ b/src/apps/sdk-host/src/lib.rs @@ -11,7 +11,7 @@ pub const SDK_HOST_WORKER_STACK_BYTES: usize = 16 * 1024 * 1024; /// Installs process-global prerequisites before any service or descendant starts. pub fn initialize_process_runtime() -> std::io::Result<()> { bitfun_services_core::process_manager::contain_current_process_tree()?; - let _ = rustls::crypto::ring::default_provider().install_default(); + bitfun_services_core::tls_provider::ensure_ring_crypto_provider(); Ok(()) } diff --git a/src/apps/server/src/bootstrap.rs b/src/apps/server/src/bootstrap.rs index 17dd01c35e..f836c503f8 100644 --- a/src/apps/server/src/bootstrap.rs +++ b/src/apps/server/src/bootstrap.rs @@ -2,9 +2,13 @@ //! //! Mirrors the Desktop app's init sequence without any Tauri dependency. -use bitfun_core::agentic::*; +use bitfun_core::agentic::{agents, coordination, system, tools}; use bitfun_core::infrastructure::ai::AIClientFactory; use bitfun_core::infrastructure::try_get_path_manager_arc; +use bitfun_core::product_runtime::{ + ensure_product_dialog_scheduler, CoreProductEventQueueOwner, CoreRuntimeServicesProvider, +}; +use bitfun_core::runtime_ownership::CoreRuntimeOwnership; use bitfun_core::service::{config, filesystem, mcp, token_usage, workspace}; use std::sync::Arc; use tokio::sync::RwLock; @@ -26,8 +30,7 @@ pub(crate) struct ServerAppState { pub token_usage_service: Arc, pub coordinator: Arc, pub scheduler: Arc, - pub event_queue: Arc, - pub event_router: Arc, + pub agent_event_queue_owner: CoreProductEventQueueOwner, pub tool_registry_snapshot: Arc>>, pub start_time: std::time::Instant, } @@ -38,9 +41,7 @@ pub(crate) struct ServerAppState { pub(crate) async fn initialize(workspace: Option) -> anyhow::Result> { log::info!("Initializing BitFun server core services"); - bitfun_core::agentic::system::select_agentic_system_profile( - bitfun_core::agentic::system::DeliveryProfile::ProductFull, - )?; + system::select_agentic_system_profile(system::DeliveryProfile::ProductFull)?; // 1. Global config config::initialize_global_config().await?; @@ -64,87 +65,27 @@ pub(crate) async fn initialize(workspace: Option) -> anyhow::Result) -> anyhow::Result) -> anyhow::Result Some(Arc::new(service)), + Ok(service) => { + let service = Arc::new(service); + mcp::set_global_mcp_service(service.clone()); + Some(service) + } Err(e) => { log::warn!("Failed to initialize MCP service: {}", e); None @@ -188,7 +134,7 @@ pub(crate) async fn initialize(workspace: Option) -> anyhow::Result) -> anyhow::Result { - log::info!( - "Workspace opened: name={}, path={}", - info.name, - info.root_path.display() - ); - Some(info.root_path) - } - Err(e) => { - log::error!("Failed to open workspace '{}': {}", ws_path, e); - None - } - } + .map_err(|error| { + anyhow::anyhow!("Failed to open workspace '{}': {}", ws_path, error) + })?; + log::info!( + "Workspace opened: name={}, path={}", + info.name, + info.root_path.display() + ); + Some(info.root_path) } else { // Try to restore last workspace workspace_service @@ -230,8 +171,7 @@ pub(crate) async fn initialize(workspace: Option) -> anyhow::Result, @@ -90,6 +90,7 @@ async fn main() -> Result<()> { .init(); tracing::info!("BitFun Server v{}", env!("CARGO_PKG_VERSION")); + bitfun_core::service::remote_connect::ensure_rustls_crypto_provider(); let args = ServerArgs::parse(); let external_workspace_root = args @@ -128,11 +129,9 @@ async fn main() -> Result<()> { server_state.token_usage_service.clone(), ) .map_err(|error| anyhow::anyhow!("Failed to build agent runtime: {error}"))?; - // The event source wraps the same `EventQueue` the coordinator publishes to; - // each connection's `serve` main loop subscribes independently and projects - // runtime events to the frontend shape before pushing them to the browser. - let event_source = - bitfun_agent_runtime::sdk::AgentEventSource::new(server_state.event_queue.clone()); + // The product owner keeps the legacy queue drained while each connection's + // `serve` loop independently subscribes to and projects Runtime events. + let event_source = server_state.agent_event_queue_owner.runtime_source(); let product_search = Arc::new( bitfun_core::product_runtime::CoreAgentRuntimeCompatibility::build( server_state.coordinator.clone(), @@ -165,10 +164,9 @@ async fn main() -> Result<()> { }) .collect::>>()?; - // This is a narrow controller/observer capability. It deliberately does - // not initialize the Server Host's dormant Agent Runtime: authoritative - // sessions and execution stay inside the target-side `bitfun dispatch` - // worker. + // Detached dispatch remains a narrow controller/observer capability beside + // the host-owned Agent Runtime. It keeps its SSH/process state separate and + // does not construct another Agent Runtime or widen the App Server scope. let path_manager = Arc::new(bitfun_core::infrastructure::PathManager::new()?); let ssh_data_dir = dirs::data_local_dir() .ok_or_else(|| anyhow::anyhow!("Could not resolve the local data directory"))? @@ -271,39 +269,4 @@ mod tests { assert!(normalize_browser_origin(invalid).is_err(), "{invalid}"); } } - - #[test] - fn agent_bootstrap_reuses_core_ownership_without_activating_the_http_shell() { - let bootstrap = include_str!("bootstrap.rs"); - assert!(bootstrap.contains("CoreRuntimeOwnership::embedded")); - let coordinator = bootstrap - .split("ConversationCoordinator::new") - .nth(1) - .and_then(|source| source.split(");").next()) - .expect("Server agent bootstrap Coordinator assembly"); - assert!(coordinator.contains("runtime_ownership")); - assert!(bootstrap.contains("open_workspace_with_runtime_ownership")); - assert!(!bootstrap.contains("initialize_snapshot_manager_for_workspace")); - - let rpc = include_str!("rpc_dispatcher.rs"); - let delete = rpc - .split("\"delete_session\" =>") - .nth(1) - .and_then(|source| source.split("\"start_dialog_turn\" =>").next()) - .expect("Server delete RPC"); - assert!(delete.contains("ensure_workspace_runtime_ownership")); - - let main_source = include_str!("main.rs") - .split("#[cfg(test)]") - .next() - .expect("Server production entrypoint"); - assert!( - !main_source.contains("bootstrap::initialize"), - "the current read-only HTTP shell must not silently start an Agent Runtime" - ); - assert!( - main_source.contains("DispatchHostState"), - "the lightweight Server Host should expose dispatch without booting an Agent Runtime" - ); - } } diff --git a/src/apps/server/src/routes/dispatch.rs b/src/apps/server/src/routes/dispatch.rs index 86329e95fc..6244ae9ced 100644 --- a/src/apps/server/src/routes/dispatch.rs +++ b/src/apps/server/src/routes/dispatch.rs @@ -1,4 +1,4 @@ -//! Narrow detached-dispatch capability for the lightweight Server Host. +//! Narrow detached-dispatch capability for the loopback Server Host. //! //! This route owns no Agent Runtime and no target session. It only exposes the //! same platform-neutral controller used by Desktop, backed by saved SSH diff --git a/src/crates/adapters/ai-adapters/Cargo.toml b/src/crates/adapters/ai-adapters/Cargo.toml index dd676b6a14..38b25324e6 100644 --- a/src/crates/adapters/ai-adapters/Cargo.toml +++ b/src/crates/adapters/ai-adapters/Cargo.toml @@ -24,7 +24,7 @@ async-trait = { workspace = true } base64 = { workspace = true, optional = true } bitfun-agent-stream = { path = "../../execution/agent-stream" } bitfun-core-types = { path = "../../contracts/core-types" } -bitfun-services-core = { path = "../../services/services-core", optional = true } +bitfun-services-core = { path = "../../services/services-core", features = ["tls-provider"] } chrono = { workspace = true } dirs = { workspace = true, optional = true } keyring-core = { workspace = true, optional = true } @@ -33,7 +33,7 @@ futures = { workspace = true } fs2 = { workspace = true, optional = true } libc = { workspace = true, optional = true } log = { workspace = true } -reqwest = { workspace = true, features = ["http2", "json", "rustls", "socks", "stream"] } +reqwest = { workspace = true, features = ["http2", "json", "rustls-no-provider", "socks", "stream"] } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } diff --git a/src/crates/adapters/ai-adapters/src/client/http.rs b/src/crates/adapters/ai-adapters/src/client/http.rs index 3477831804..35e5b6a6d7 100644 --- a/src/crates/adapters/ai-adapters/src/client/http.rs +++ b/src/crates/adapters/ai-adapters/src/client/http.rs @@ -8,6 +8,7 @@ pub(crate) fn create_http_client( proxy_config: Option, skip_ssl_verify: bool, ) -> Client { + bitfun_services_core::tls_provider::ensure_ring_crypto_provider(); let mut builder = Client::builder() .tls_backend_rustls() .connect_timeout(std::time::Duration::from_secs( diff --git a/src/crates/adapters/ai-adapters/src/client/sse.rs b/src/crates/adapters/ai-adapters/src/client/sse.rs index 560f1da90e..4c8c29ff5c 100644 --- a/src/crates/adapters/ai-adapters/src/client/sse.rs +++ b/src/crates/adapters/ai-adapters/src/client/sse.rs @@ -585,7 +585,7 @@ mod tests { .expect("retry fixture should run"); }); let url = format!("http://{address}/chat/completions"); - let client = reqwest::Client::new(); + let client = crate::client::http::create_http_client(None, false); let request_body = serde_json::json!({"model": "configured-model"}); let result = execute_sse_request( @@ -623,7 +623,7 @@ mod tests { .expect("retry-after fixture should run"); }); let url = format!("http://{address}/chat/completions"); - let client = reqwest::Client::new(); + let client = crate::client::http::create_http_client(None, false); let request_body = serde_json::json!({"model": "configured-model"}); let result = execute_sse_request( diff --git a/src/crates/adapters/ai-adapters/src/subscription_auth/mod.rs b/src/crates/adapters/ai-adapters/src/subscription_auth/mod.rs index 5d68cc68de..90724cfbec 100644 --- a/src/crates/adapters/ai-adapters/src/subscription_auth/mod.rs +++ b/src/crates/adapters/ai-adapters/src/subscription_auth/mod.rs @@ -337,6 +337,7 @@ pub(crate) fn build_http_client( options: &SubscriptionHttpOptions, provider: &str, ) -> Result { + bitfun_services_core::tls_provider::ensure_ring_crypto_provider(); let mut builder = reqwest::Client::builder() .tls_backend_rustls() .timeout(Duration::from_secs(30)) diff --git a/src/crates/adapters/ai-adapters/tests/common/stream_test_harness.rs b/src/crates/adapters/ai-adapters/tests/common/stream_test_harness.rs index 025f963f2d..7ea5ab1768 100644 --- a/src/crates/adapters/ai-adapters/tests/common/stream_test_harness.rs +++ b/src/crates/adapters/ai-adapters/tests/common/stream_test_harness.rs @@ -98,6 +98,7 @@ pub(crate) async fn run_stream_fixture_with_options( ) -> StreamFixtureRunOutput { let fixture_bytes = load_fixture_bytes(fixture_relative_path); let fixture_server = FixtureSseServer::spawn(fixture_bytes, options.server_options).await; + bitfun_services_core::tls_provider::ensure_ring_crypto_provider(); let response = tokio::time::timeout( options.request_timeout, diff --git a/src/crates/assembly/core/src/agentic/system.rs b/src/crates/assembly/core/src/agentic/system.rs index 92351371d7..7d92477ff4 100644 --- a/src/crates/assembly/core/src/agentic/system.rs +++ b/src/crates/assembly/core/src/agentic/system.rs @@ -16,7 +16,9 @@ use crate::agentic::tools; use crate::infrastructure::ai::AIClientFactory; use crate::infrastructure::try_get_path_manager_arc; use crate::runtime_ownership::CoreRuntimeOwnership; -use crate::service::token_usage::{TokenUsageService, TokenUsageSubscriber}; +use crate::service::token_usage::{ + set_global_token_usage_service, TokenUsageService, TokenUsageSubscriber, +}; pub use bitfun_product_capabilities::DeliveryProfile; fn session_manager_config_for_profile( @@ -92,6 +94,7 @@ pub async fn init_agentic_system_for_profile_with_runtime_ownership( let path_manager = try_get_path_manager_arc()?; let persistence_manager = Arc::new(persistence::PersistenceManager::new(path_manager.clone())?); let token_usage_service = Arc::new(TokenUsageService::new(path_manager.clone()).await?); + set_global_token_usage_service(token_usage_service.clone()); let context_store = Arc::new(session::SessionContextStore::new()); let context_compressor = Arc::new(session::ContextCompressor::new(Default::default())); diff --git a/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs b/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs index fd23016227..bbd589c5b5 100644 --- a/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs +++ b/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs @@ -31,7 +31,6 @@ const PRODUCT_TOOL_REGISTRATION_ORDER: &[&str] = &[ "ExecControl", "GetTime", "ListModels", - "BitFunControl", "Task", "AgentSpawn", "AgentSendInput", @@ -50,6 +49,7 @@ const PRODUCT_TOOL_REGISTRATION_ORDER: &[&str] = &[ "submit_code_review", "GetToolSpec", "CallDeferredTool", + "BitFunControl", "GetFileDiff", "CreateCanvas", "ReadCanvas", diff --git a/src/crates/assembly/core/src/service/token_usage/service.rs b/src/crates/assembly/core/src/service/token_usage/service.rs index bb6938af8d..3e96ed04a8 100644 --- a/src/crates/assembly/core/src/service/token_usage/service.rs +++ b/src/crates/assembly/core/src/service/token_usage/service.rs @@ -189,10 +189,10 @@ impl TokenUsageService { static GLOBAL_TOKEN_USAGE_SERVICE: std::sync::OnceLock> = std::sync::OnceLock::new(); -/// Install the process-wide token usage service. Called once by the desktop -/// runtime after the service is constructed; tools that call the model outside -/// the round executor (e.g. `analyze_image`) use it to persist usage that would -/// otherwise never reach the token usage store. +/// Install the process-wide token usage service. The canonical Agent Runtime +/// initializer calls this after constructing the service; tools that call the +/// model outside the round executor (e.g. `analyze_image`) use it to persist +/// usage that would otherwise never reach the token usage store. pub fn set_global_token_usage_service(service: Arc) { match GLOBAL_TOKEN_USAGE_SERVICE.set(service) { Ok(_) => log::info!("Global token usage service set"), diff --git a/src/crates/services/AGENTS.md b/src/crates/services/AGENTS.md index a161899b77..3fc3b90439 100644 --- a/src/crates/services/AGENTS.md +++ b/src/crates/services/AGENTS.md @@ -11,7 +11,7 @@ OS/network capabilities. | Crate | Responsibility | Local doc | |---|---|---| -| `services-core` | Reusable local service primitives, managed process-tree lifecycle, filesystem helpers, session storage layout/indexing/deletion, metadata store CRUD/index rebuild, metadata construction/counter/index/field mutation/lineage rules, and JSON file IO without product assembly decisions | [AGENTS.md](services-core/AGENTS.md) | +| `services-core` | Reusable local service primitives, process-wide TLS provider selection, managed process-tree lifecycle, filesystem helpers, session storage layout/indexing/deletion, metadata store CRUD/index rebuild, metadata construction/counter/index/field mutation/lineage rules, and JSON file IO without product assembly decisions | [AGENTS.md](services-core/AGENTS.md) | | `services-integrations` | Concrete MCP, git, remote, file-watch, MiniApp runtime, review-platform provider service, product-domain port implementations, and platform-neutral Remote Connect primitives | [AGENTS.md](services-integrations/AGENTS.md) | | `miniapp-market-service` | Concrete SQLite, artifact storage, GitHub OAuth, package validation, and HTTP behavior for the MiniApp market | [README.md](miniapp-market-service/README.md) | | `skin-market-service` | Concrete SQLite, artifact storage, Appearance package validation, review, and HTTP behavior for the Skin market | [README.md](skin-market-service/README.md) | @@ -21,7 +21,7 @@ OS/network capabilities. ## Placement Rules -- Put concrete OS, process, filesystem, git, terminal, MCP, remote SSH, +- Put concrete OS, process-wide TLS provider, process, filesystem, git, terminal, MCP, remote SSH, file-watch, MiniApp runtime IO, and network service implementations here. - Implement `contracts`, `execution`, or `contracts/product-domains` ports here when the implementation needs concrete dependencies. diff --git a/src/crates/services/miniapp-market-service/Cargo.toml b/src/crates/services/miniapp-market-service/Cargo.toml index eb939c9c47..634be7f06a 100644 --- a/src/crates/services/miniapp-market-service/Cargo.toml +++ b/src/crates/services/miniapp-market-service/Cargo.toml @@ -13,11 +13,12 @@ anyhow = { workspace = true } axum = { workspace = true } base64 = { workspace = true } bitfun-product-domains = { path = "../../contracts/product-domains", features = ["miniapp"] } +bitfun-services-core = { path = "../services-core", features = ["tls-provider"] } chrono = { workspace = true } hex = { workspace = true } image = { workspace = true, features = ["jpeg", "png", "webp"] } rand = { workspace = true } -reqwest = { workspace = true, features = ["form", "http2", "json", "rustls"] } +reqwest = { workspace = true, features = ["form", "http2", "json", "rustls-no-provider"] } serde = { workspace = true } serde_json = { workspace = true } semver = { workspace = true } diff --git a/src/crates/services/miniapp-market-service/src/auth.rs b/src/crates/services/miniapp-market-service/src/auth.rs index 2bc852f17c..67da6667e9 100644 --- a/src/crates/services/miniapp-market-service/src/auth.rs +++ b/src/crates/services/miniapp-market-service/src/auth.rs @@ -147,6 +147,7 @@ struct OAuthFlowRecord { impl AuthService { pub(crate) fn new(config: MarketConfig, db: Database) -> MarketResult { + bitfun_services_core::tls_provider::ensure_ring_crypto_provider(); let client = reqwest::Client::builder() .user_agent("BitFun-MiniApp-Market/1") .redirect(reqwest::redirect::Policy::none()) diff --git a/src/crates/services/services-core/AGENTS.md b/src/crates/services/services-core/AGENTS.md index 13d0f39129..7f92c8657b 100644 --- a/src/crates/services/services-core/AGENTS.md +++ b/src/crates/services/services-core/AGENTS.md @@ -7,7 +7,8 @@ without the full product runtime. This includes generic filesystem/search/JSON IO helpers, bounded local Instruction file reads, session metadata storage helpers, and local OS action primitives such as command lookup, clipboard, file/url opening, script execution, workspace runtime FS/shell -providers, managed process-tree lifecycle, process-level Agent Runtime ownership locks, and system facts. Product crates may layer routing, policy, +providers, process-wide TLS provider selection, managed process-tree lifecycle, +process-level Agent Runtime ownership locks, and system facts. Product crates may layer routing, policy, capability selection, event emission, or legacy error mapping outside this crate. @@ -24,7 +25,7 @@ crate. (JSON/session/usage persistence), `process-runtime` (command lookup and supervised child lifecycle), and `workspace-instructions` (declarative instruction discovery). Consumers enable those or the narrower - `workspace-runtime`, `workspace-identity`, `runtime-ownership`, + `workspace-runtime`, `workspace-identity`, `runtime-ownership`, `tls-provider`, `permission`, `dispatch-workspace`, `markdown`, `session-git`, and `workspace-text-runtime` extensions only for behavior they use. Products needing IANA time-zone ranges and dashboard aggregation additionally select @@ -34,6 +35,10 @@ crate. carries no Tokio dependency, `workspace-runtime` explicitly composes `process-runtime`, and Windows storage/process bindings must not be enabled from one shared dependency feature union. +- `tls-provider` is the single owner of the process-wide Rustls provider. It + selects only `ring`, `std`, and `tls12`; provider-neutral Reqwest consumers + call `tls_provider::ensure_ring_crypto_provider` before client construction. + Do not install a Rustls provider from another crate. - Runtime call sites that touch agent execution, scheduler state, workspace managers, filesystem orchestration, or product behavior stay outside this crate. `workspace-runtime` may implement local `bitfun-runtime-ports` @@ -75,6 +80,7 @@ cargo test -p bitfun-services-core --no-default-features --features local-storag cargo test -p bitfun-services-core --no-default-features --features local-storage --test session_write_lock_contracts cargo test -p bitfun-services-core --no-default-features --features token-usage-statistics --lib token_usage:: cargo test -p bitfun-services-core --no-default-features --features process-runtime --test process_runtime_contracts +cargo test --locked -p bitfun-services-core --no-default-features --features tls-provider --lib tls_provider::tests pnpm run check:core-boundaries ``` diff --git a/src/crates/services/services-core/Cargo.toml b/src/crates/services/services-core/Cargo.toml index d313b55bcc..63e401987d 100644 --- a/src/crates/services/services-core/Cargo.toml +++ b/src/crates/services/services-core/Cargo.toml @@ -36,6 +36,7 @@ which = { workspace = true, optional = true } similar = { workspace = true, optional = true } regex = { workspace = true, optional = true } rusqlite = { version = "0.32", features = ["bundled"], optional = true } +rustls = { workspace = true, features = ["ring", "std", "tls12"], optional = true } [target.'cfg(windows)'.dependencies] win32job = { workspace = true, optional = true } @@ -46,6 +47,7 @@ libc = { workspace = true, optional = true } [features] default = [] +tls-provider = ["dep:rustls"] diagnostics = ["dep:regex"] diff = ["dep:similar", "dep:tokio", "tokio/rt", "tokio/time"] filesystem = [ diff --git a/src/crates/services/services-core/src/lib.rs b/src/crates/services/services-core/src/lib.rs index d388b83fa0..6c55cec1f6 100644 --- a/src/crates/services/services-core/src/lib.rs +++ b/src/crates/services/services-core/src/lib.rs @@ -47,6 +47,8 @@ pub mod session_usage; pub mod storage_cleanup; #[cfg(feature = "process-runtime")] pub mod system; +#[cfg(feature = "tls-provider")] +pub mod tls_provider; #[cfg(feature = "local-storage")] pub mod token_usage; #[cfg(feature = "workspace-runtime")] diff --git a/src/crates/services/services-core/src/tls_provider.rs b/src/crates/services/services-core/src/tls_provider.rs new file mode 100644 index 0000000000..0136b2992f --- /dev/null +++ b/src/crates/services/services-core/src/tls_provider.rs @@ -0,0 +1,19 @@ +//! Process-wide TLS crypto-provider selection for BitFun product clients. + +/// Installs the workspace-owned ring provider before a provider-neutral TLS +/// client is constructed. +/// +/// Repeated calls are safe: rustls keeps the first process-wide provider and +/// every reviewed BitFun build selects ring as its only provider. +pub fn ensure_ring_crypto_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); +} + +#[cfg(test)] +mod tests { + #[test] + fn installs_the_workspace_owned_provider() { + super::ensure_ring_crypto_provider(); + assert!(rustls::crypto::CryptoProvider::get_default().is_some()); + } +} diff --git a/src/crates/services/services-integrations/Cargo.toml b/src/crates/services/services-integrations/Cargo.toml index b3cf31adc5..43f4750270 100644 --- a/src/crates/services/services-integrations/Cargo.toml +++ b/src/crates/services/services-integrations/Cargo.toml @@ -97,10 +97,11 @@ default = [] # feature group (e.g. `git`) only derive `TS` when that group is also enabled; # app-server's `ts` feature enables `product-full` which pulls `git`. ts = ["dep:ts-rs"] -announcement = ["reqwest", "reqwest/json", "reqwest/rustls", "tokio/fs", "tokio/sync"] +announcement = ["bitfun-services-core/tls-provider", "reqwest", "reqwest/json", "reqwest/rustls-no-provider", "tokio/fs", "tokio/sync"] models-dev = [ + "bitfun-services-core/tls-provider", "reqwest", - "reqwest/rustls", + "reqwest/rustls-no-provider", "reqwest/system-proxy", "sha2", "tokio/fs", @@ -108,7 +109,7 @@ models-dev = [ "tokio/time", "windows", ] -browser-control = ["anyhow", "bitfun-services-core/process-runtime", "dirs", "reqwest", "reqwest/json", "reqwest/rustls", "thiserror", "tokio/time"] +browser-control = ["anyhow", "bitfun-services-core/process-runtime", "bitfun-services-core/tls-provider", "dirs", "reqwest", "reqwest/json", "reqwest/rustls-no-provider", "thiserror", "tokio/time"] canvas-runtime = [ "dep:bitfun-product-domains", "oxc", @@ -149,12 +150,13 @@ mcp = [ "base64", "bitfun-agent-tools/mcp-bridge", "bitfun-services-core/process-runtime", + "bitfun-services-core/tls-provider", "futures", "hex", "rand", "reqwest", "reqwest/json", - "reqwest/rustls", + "reqwest/rustls-no-provider", "reqwest/stream", "rmcp", "rmcp/transport-streamable-http-client-reqwest", @@ -174,10 +176,11 @@ miniapp-runtime = [ "base64", "bitfun-product-domains/miniapp", "bitfun-services-core/process-runtime", + "bitfun-services-core/tls-provider", "dep:bitfun-product-domains", "dirs", "reqwest", - "reqwest/rustls", + "reqwest/rustls-no-provider", "reqwest/stream", "tokio/fs", "tokio/io-util", @@ -190,6 +193,7 @@ miniapp-runtime = [ "which", ] miniapp-market = [ + "bitfun-services-core/tls-provider", "bitfun-product-domains/appearance-market", "chrono", "dep:apple-native-keyring-store", @@ -206,7 +210,7 @@ miniapp-market = [ "reqwest", "reqwest/json", "reqwest/query", - "reqwest/rustls", + "reqwest/rustls-no-provider", "reqwest/stream", "semver", "sha2", @@ -254,6 +258,7 @@ remote-connect = [ "dep:bitfun-core-types", "bitfun-services-core/local-storage", "bitfun-services-core/process-runtime", + "bitfun-services-core/tls-provider", "bitfun-runtime-ports/agent-api", "bitfun-runtime-ports/remote-workspace-ports", "chrono", @@ -276,7 +281,7 @@ remote-connect = [ "reqwest/json", "reqwest/multipart", "reqwest/query", - "reqwest/rustls", + "reqwest/rustls-no-provider", "rustls", "rustls-native-certs", "schannel", @@ -324,13 +329,14 @@ remote-ssh-concrete = [ "anyhow", "async-trait", "base64", + "bitfun-services-core/tls-provider", "dep:bitfun-runtime-ports", "chrono", "dirs", "minisign-verify", "rand", "reqwest", - "reqwest/rustls", + "reqwest/rustls-no-provider", "reqwest/stream", "russh", "russh-sftp", @@ -345,12 +351,13 @@ remote-ssh-concrete = [ review-platform = [ "async-trait", "bitfun-services-core/process-runtime", + "bitfun-services-core/tls-provider", "chrono", "futures", "reqwest", "reqwest/json", "reqwest/query", - "reqwest/rustls", + "reqwest/rustls-no-provider", "reqwest/stream", "sha2", "thiserror", @@ -363,12 +370,13 @@ review-platform = [ speech = [ "async-trait", "base64", + "bitfun-services-core/tls-provider", "bzip2", "chrono", "dep:bitfun-core-types", "futures-util", "reqwest", - "reqwest/rustls", + "reqwest/rustls-no-provider", "reqwest/stream", "sha2", "sherpa-onnx", @@ -406,7 +414,7 @@ script-tool-runtime = [ "tokio/time", "which", ] -web-tools = ["reqwest", "reqwest/json", "reqwest/rustls", "thiserror"] +web-tools = ["bitfun-services-core/tls-provider", "reqwest", "reqwest/json", "reqwest/rustls-no-provider", "thiserror"] product-full = [ "announcement", "models-dev", diff --git a/src/crates/services/services-integrations/src/announcement/remote.rs b/src/crates/services/services-integrations/src/announcement/remote.rs index 80d04486b9..7c4183fbc3 100644 --- a/src/crates/services/services-integrations/src/announcement/remote.rs +++ b/src/crates/services/services-integrations/src/announcement/remote.rs @@ -76,7 +76,7 @@ impl RemoteAnnouncementFetcher { request.endpoint_url ); - match reqwest::get(&request_url).await { + match crate::reqwest_client().get(&request_url).send().await { Ok(resp) if resp.status().is_success() => { match resp.json::>().await { Ok(cards) => { diff --git a/src/crates/services/services-integrations/src/appearance_market/client.rs b/src/crates/services/services-integrations/src/appearance_market/client.rs index 7746e83a97..36f842965a 100644 --- a/src/crates/services/services-integrations/src/appearance_market/client.rs +++ b/src/crates/services/services-integrations/src/appearance_market/client.rs @@ -57,7 +57,7 @@ impl AppearanceMarketClient { ) -> Result { let base_url = base_url.into().trim_end_matches('/').to_string(); validate_market_url(&base_url)?; - let client = reqwest::Client::builder() + let client = crate::reqwest_client_builder() .user_agent(format!("BitFun-Desktop/{}", env!("CARGO_PKG_VERSION"))) .redirect(reqwest::redirect::Policy::none()) .build() diff --git a/src/crates/services/services-integrations/src/browser_control/cdp.rs b/src/crates/services/services-integrations/src/browser_control/cdp.rs index f8bb9454b0..9ac4cde965 100644 --- a/src/crates/services/services-integrations/src/browser_control/cdp.rs +++ b/src/crates/services/services-integrations/src/browser_control/cdp.rs @@ -52,7 +52,9 @@ impl CdpEndpointProvider { /// Discover browser version on the given debug port. pub async fn get_version(port: u16) -> Result { let url = format!("http://127.0.0.1:{}/json/version", port); - let resp = reqwest::get(&url) + let resp = crate::reqwest_client() + .get(&url) + .send() .await .map_err(|source| CdpEndpointError::VersionRequest { port, @@ -66,13 +68,14 @@ impl CdpEndpointProvider { /// List all pages/tabs on the given debug port. pub async fn list_pages(port: u16) -> Result, CdpEndpointError> { let url = format!("http://127.0.0.1:{}/json", port); - let resp = - reqwest::get(&url) - .await - .map_err(|source| CdpEndpointError::ListPagesRequest { - port, - message: source.to_string(), - })?; + let resp = crate::reqwest_client() + .get(&url) + .send() + .await + .map_err(|source| CdpEndpointError::ListPagesRequest { + port, + message: source.to_string(), + })?; resp.json() .await .map_err(|source| CdpEndpointError::ListPagesResponse(source.to_string())) @@ -89,7 +92,7 @@ impl CdpEndpointProvider { } else { format!("http://127.0.0.1:{}/json/new", port) }; - let resp = reqwest::Client::new() + let resp = crate::reqwest_client() .put(&endpoint) .send() .await diff --git a/src/crates/services/services-integrations/src/browser_control/launcher.rs b/src/crates/services/services-integrations/src/browser_control/launcher.rs index 554865db1c..289202429c 100644 --- a/src/crates/services/services-integrations/src/browser_control/launcher.rs +++ b/src/crates/services/services-integrations/src/browser_control/launcher.rs @@ -85,7 +85,7 @@ impl BrowserLauncher { /// Check if a CDP debug port is already listening. pub async fn is_cdp_available(port: u16) -> bool { let url = format!("http://127.0.0.1:{}/json/version", port); - reqwest::Client::new() + crate::reqwest_client() .get(&url) .timeout(std::time::Duration::from_secs(2)) .send() diff --git a/src/crates/services/services-integrations/src/lib.rs b/src/crates/services/services-integrations/src/lib.rs index 066ea77ca9..715906502d 100644 --- a/src/crates/services/services-integrations/src/lib.rs +++ b/src/crates/services/services-integrations/src/lib.rs @@ -3,6 +3,33 @@ //! Heavy external integrations live here behind feature groups so local checks //! can opt into only the integration family they need. +#[cfg(any( + feature = "mcp", + feature = "miniapp-market", + feature = "miniapp-runtime", + feature = "models-dev", + feature = "remote-connect", + feature = "remote-ssh-concrete", + feature = "review-platform", + feature = "speech", + feature = "web-tools", +))] +pub(crate) fn reqwest_client_builder() -> reqwest::ClientBuilder { + bitfun_services_core::tls_provider::ensure_ring_crypto_provider(); + reqwest::Client::builder() +} + +#[cfg(any( + feature = "announcement", + feature = "browser-control", + feature = "mcp", + feature = "remote-connect", +))] +pub(crate) fn reqwest_client() -> reqwest::Client { + bitfun_services_core::tls_provider::ensure_ring_crypto_provider(); + reqwest::Client::new() +} + #[cfg(feature = "announcement")] pub mod announcement; diff --git a/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs b/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs index bdc0629517..63f6b1d075 100644 --- a/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs +++ b/src/crates/services/services-integrations/src/mcp/protocol/transport_remote.rs @@ -420,7 +420,7 @@ impl RemoteMCPTransport { None }; - let http_client = reqwest::Client::builder() + let http_client = crate::reqwest_client_builder() .connect_timeout(Duration::from_secs(10)) .danger_accept_invalid_certs(false) .tls_backend_rustls() @@ -428,7 +428,7 @@ impl RemoteMCPTransport { .build() .unwrap_or_else(|e| { warn!("Failed to create HTTP client, using default config: {}", e); - reqwest::Client::new() + crate::reqwest_client() }); let transport = StreamableHttpClientTransport::with_client( diff --git a/src/crates/services/services-integrations/src/miniapp/host_dispatch.rs b/src/crates/services/services-integrations/src/miniapp/host_dispatch.rs index b05152b9b6..535ca051fe 100644 --- a/src/crates/services/services-integrations/src/miniapp/host_dispatch.rs +++ b/src/crates/services/services-integrations/src/miniapp/host_dispatch.rs @@ -547,7 +547,7 @@ async fn dispatch_net( .host_str() .ok_or_else(|| MiniAppHostDispatchError::parse("URL has no host"))?; let pinned_address = resolve_public_address(¤t_url).await?; - let client = reqwest::Client::builder() + let client = crate::reqwest_client_builder() .redirect(reqwest::redirect::Policy::none()) .resolve(host, pinned_address) .build() diff --git a/src/crates/services/services-integrations/src/miniapp_market/client.rs b/src/crates/services/services-integrations/src/miniapp_market/client.rs index deb17b9257..a23cbfba23 100644 --- a/src/crates/services/services-integrations/src/miniapp_market/client.rs +++ b/src/crates/services/services-integrations/src/miniapp_market/client.rs @@ -140,7 +140,7 @@ impl MarketClient { "The MiniApp market API must use HTTPS.", )); } - let client = reqwest::Client::builder() + let client = crate::reqwest_client_builder() .user_agent(format!("BitFun-Desktop/{}", env!("CARGO_PKG_VERSION"))) .redirect(reqwest::redirect::Policy::none()) .build() diff --git a/src/crates/services/services-integrations/src/models_dev.rs b/src/crates/services/services-integrations/src/models_dev.rs index b86f8dd5da..c20ef227a5 100644 --- a/src/crates/services/services-integrations/src/models_dev.rs +++ b/src/crates/services/services-integrations/src/models_dev.rs @@ -225,7 +225,7 @@ impl ModelsDevCatalogService { refresh_state.last_attempt = Some(now); drop(refresh_state); - let client = match reqwest::Client::builder() + let client = match crate::reqwest_client_builder() .timeout(Duration::from_secs(10)) .build() { diff --git a/src/crates/services/services-integrations/src/remote_connect/bot/feishu.rs b/src/crates/services/services-integrations/src/remote_connect/bot/feishu.rs index 58dea5ffb6..5ccda09748 100644 --- a/src/crates/services/services-integrations/src/remote_connect/bot/feishu.rs +++ b/src/crates/services/services-integrations/src/remote_connect/bot/feishu.rs @@ -76,7 +76,7 @@ impl FeishuBotApi { } } - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let resp = client .post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal") .json(&serde_json::json!({ @@ -112,7 +112,7 @@ impl FeishuBotApi { pub async fn send_message(&self, chat_id: &str, content: &str) -> Result<()> { let token = self.get_access_token().await?; let card = build_markdown_card(content); - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let resp = client .post("https://open.feishu.cn/open-apis/im/v1/messages") .query(&[("receive_id_type", "chat_id")]) @@ -156,7 +156,7 @@ impl FeishuBotApi { actions: &[BotAction], ) -> Result<()> { let token = self.get_access_token().await?; - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let card = build_action_card(chat_id, language, content, actions); let resp = client .post("https://open.feishu.cn/open-apis/im/v1/messages") @@ -185,7 +185,7 @@ impl FeishuBotApi { file_key: &str, ) -> Result { let token = self.get_access_token().await?; - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let url = format!( "https://open.feishu.cn/open-apis/im/v1/messages/{}/resources/{}?type=image", message_id, file_key @@ -224,7 +224,7 @@ impl FeishuBotApi { let file_key = self.upload_file(file_path).await?; let token = self.get_access_token().await?; - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let resp = client .post("https://open.feishu.cn/open-apis/im/v1/messages") .query(&[("receive_id_type", "chat_id")]) @@ -272,7 +272,7 @@ impl FeishuBotApi { .text("file_name", content.name) .part("file", part); - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let resp = client .post("https://open.feishu.cn/open-apis/im/v1/files") .bearer_auth(&token) @@ -294,7 +294,7 @@ impl FeishuBotApi { /// Obtain a WebSocket URL from Feishu for long-connection event delivery. pub async fn get_ws_endpoint(&self) -> Result { - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let resp = client .post("https://open.feishu.cn/callback/ws/endpoint") .json(&serde_json::json!({ diff --git a/src/crates/services/services-integrations/src/remote_connect/bot/telegram.rs b/src/crates/services/services-integrations/src/remote_connect/bot/telegram.rs index 0535a6bcd8..0e7ae4aa64 100644 --- a/src/crates/services/services-integrations/src/remote_connect/bot/telegram.rs +++ b/src/crates/services/services-integrations/src/remote_connect/bot/telegram.rs @@ -73,7 +73,7 @@ impl TelegramBotApi { } pub async fn send_message(&self, chat_id: i64, text: &str) -> Result<()> { - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); for chunk in chunk_text_for_telegram(text) { let resp = client .post(self.api_url("sendMessage")) @@ -110,7 +110,7 @@ impl TelegramBotApi { }) .collect(); - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let resp = client .post(self.api_url("sendMessage")) .json(&serde_json::json!({ @@ -143,7 +143,7 @@ impl TelegramBotApi { .text("chat_id", chat_id.to_string()) .part("document", part); - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let resp = client .post(self.api_url("sendDocument")) .multipart(form) @@ -160,7 +160,7 @@ impl TelegramBotApi { /// Acknowledge a callback query so Telegram removes the button loading state. pub async fn answer_callback_query(&self, callback_query_id: &str) { - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let _ = client .post(self.api_url("answerCallbackQuery")) .json(&serde_json::json!({ "callback_query_id": callback_query_id })) @@ -170,7 +170,7 @@ impl TelegramBotApi { /// Register the bot command menu visible in Telegram's "/" menu. pub async fn set_bot_commands(&self) -> Result<()> { - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let commands = serde_json::json!({ "commands": [ { "command": "menu", "description": "Show the main menu" }, @@ -199,7 +199,7 @@ impl TelegramBotApi { /// Download a Telegram photo by file_id and return it as an ImageAttachment. async fn download_photo(&self, file_id: &str) -> Result { - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let resp = client .post(self.api_url("getFile")) @@ -251,7 +251,7 @@ impl TelegramBotApi { &self, last_update_id: i64, ) -> Result<(i64, Vec)> { - let client = reqwest::Client::builder() + let client = crate::reqwest_client_builder() .timeout(std::time::Duration::from_secs(35)) .build()?; diff --git a/src/crates/services/services-integrations/src/remote_connect/bot/weixin.rs b/src/crates/services/services-integrations/src/remote_connect/bot/weixin.rs index eae2d2f731..4126b44540 100644 --- a/src/crates/services/services-integrations/src/remote_connect/bot/weixin.rs +++ b/src/crates/services/services-integrations/src/remote_connect/bot/weixin.rs @@ -254,7 +254,7 @@ impl WeixinProviderClient { async fn post_ilink(&self, endpoint: &str, body: Value, timeout: Duration) -> Result { let url = format!("{}{}", self.base_url(), endpoint.trim_start_matches('/')); let body_str = serde_json::to_string(&body)?; - let client = reqwest::Client::builder().timeout(timeout).build()?; + let client = crate::reqwest_client_builder().timeout(timeout).build()?; let resp = client .post(&url) .headers(self.build_auth_headers()) @@ -660,7 +660,7 @@ impl WeixinProviderClient { Some(url) => url.to_string(), None => build_cdn_download_url(DEFAULT_CDN_BASE_URL, encrypted_query_param), }; - let client = reqwest::Client::builder() + let client = crate::reqwest_client_builder() .timeout(Duration::from_secs(120)) .build()?; let resp = client.get(&url).send().await?; @@ -804,7 +804,7 @@ impl WeixinProviderClient { } async fn post_weixin_cdn_upload(&self, cdn_url: &str, ciphertext: &[u8]) -> Result { - let client = reqwest::Client::builder() + let client = crate::reqwest_client_builder() .timeout(Duration::from_secs(120)) .build()?; let mut last_err: Option = None; @@ -945,7 +945,7 @@ async fn poll_found_qr_session( url.push_str(&urlencoding::encode(code)); } - let client = reqwest::Client::builder() + let client = crate::reqwest_client_builder() .timeout(Duration::from_secs(QR_POLL_TIMEOUT_SECS)) .build()?; @@ -1508,7 +1508,7 @@ async fn fetch_qr_code(base: &str, local_token_list: &[String]) -> Result() ); - let client = reqwest::Client::new(); + let client = crate::reqwest_client(); let relay_base = relay_url.trim_end_matches('/'); let manifest = mobile_web_upload_manifest(&all_files); diff --git a/src/crates/services/services-integrations/src/remote_connect/relay_client.rs b/src/crates/services/services-integrations/src/remote_connect/relay_client.rs index 4dd59c9030..2527d0d075 100644 --- a/src/crates/services/services-integrations/src/remote_connect/relay_client.rs +++ b/src/crates/services/services-integrations/src/remote_connect/relay_client.rs @@ -25,11 +25,10 @@ use tokio_tungstenite::{tungstenite::client::IntoClientRequest, Connector}; /// `install_default()` returns `Err` only when a provider is already installed, /// which is harmless — we silently ignore it. /// -/// This is safe to call multiple times and from any thread. Required on all -/// platforms: rustls 0.23 does not auto-select a provider when multiple TLS -/// stacks are linked into the process. +/// This is safe to call multiple times and from any thread. Installing it +/// explicitly keeps provider choice deterministic for every product client. pub fn ensure_rustls_crypto_provider() { - let _ = rustls::crypto::ring::default_provider().install_default(); + bitfun_services_core::tls_provider::ensure_ring_crypto_provider(); } type WsStream = @@ -696,11 +695,7 @@ mod tests { #[cfg(windows)] fn build_windows_rustls_connector() -> Result { - // Install the ring CryptoProvider as the process-level default. - // Required by rustls 0.23+ when `default-features = false`. - // `install_default()` returns Err only when a provider is already installed, - // which is fine — subsequent reconnects reuse the same process-level provider. - let _ = rustls::crypto::ring::default_provider().install_default(); + bitfun_services_core::tls_provider::ensure_ring_crypto_provider(); let mut root_store = rustls::RootCertStore::empty(); diff --git a/src/crates/services/services-integrations/src/remote_connect/relay_http.rs b/src/crates/services/services-integrations/src/remote_connect/relay_http.rs index 48ff10e1c7..f13c4471e5 100644 --- a/src/crates/services/services-integrations/src/remote_connect/relay_http.rs +++ b/src/crates/services/services-integrations/src/remote_connect/relay_http.rs @@ -75,7 +75,7 @@ impl BufferedRelayResponse { pub(crate) fn relay_http_client() -> reqwest::Client { RELAY_HTTP_CLIENT .get_or_init(|| { - reqwest::Client::builder() + crate::reqwest_client_builder() .timeout(RELAY_HTTP_TIMEOUT) .connect_timeout(RELAY_HTTP_CONNECT_TIMEOUT) .read_timeout(RELAY_HTTP_READ_TIMEOUT) @@ -85,7 +85,7 @@ pub(crate) fn relay_http_client() -> reqwest::Client { warn!( "Failed to build shared relay HTTP client; using reqwest defaults: {error}" ); - reqwest::Client::new() + crate::reqwest_client() }) }) .clone() @@ -318,7 +318,7 @@ mod tests { stream.write_all(response.as_bytes()).await.unwrap(); } }); - let client = reqwest::Client::builder().no_proxy().build().unwrap(); + let client = crate::reqwest_client_builder().no_proxy().build().unwrap(); let response = send_with_retry( "test-safe-read", @@ -353,7 +353,7 @@ mod tests { stream.write_all(response.as_bytes()).await.unwrap(); } }); - let client = reqwest::Client::builder().no_proxy().build().unwrap(); + let client = crate::reqwest_client_builder().no_proxy().build().unwrap(); let response = send_with_retry( "test-truncated-body", @@ -386,7 +386,7 @@ mod tests { .await .unwrap(); }); - let client = reqwest::Client::builder().no_proxy().build().unwrap(); + let client = crate::reqwest_client_builder().no_proxy().build().unwrap(); let response = send_with_retry( "test-truncated-error-body", diff --git a/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs b/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs index 14caf5d576..843526510d 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs @@ -2371,7 +2371,7 @@ fn release_target(os: &str, arch: &str) -> Result<&'static str> { } fn release_http_client() -> Result { - reqwest::Client::builder() + crate::reqwest_client_builder() .connect_timeout(Duration::from_secs(10)) // Per-read rather than whole-request: a genuine slow link may take // longer than an arbitrary archive deadline, but a stalled source must diff --git a/src/crates/services/services-integrations/src/remote_ssh/relay_deploy.rs b/src/crates/services/services-integrations/src/remote_ssh/relay_deploy.rs index 6ab81b05e1..329c2b3319 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/relay_deploy.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/relay_deploy.rs @@ -1400,7 +1400,7 @@ async fn verified_latest_relay_image_descriptor() -> Result Result { - let inner = reqwest::Client::builder() + let inner = crate::reqwest_client_builder() .tls_backend_rustls() .redirect(review_redirect_policy()) .timeout(Duration::from_secs(REVIEW_PLATFORM_TIMEOUT_SECS)) diff --git a/src/crates/services/services-integrations/src/speech/downloader.rs b/src/crates/services/services-integrations/src/speech/downloader.rs index e93f8890c7..57f5c3617f 100644 --- a/src/crates/services/services-integrations/src/speech/downloader.rs +++ b/src/crates/services/services-integrations/src/speech/downloader.rs @@ -25,7 +25,7 @@ pub(super) async fn download_and_install_model( where F: Fn(SpeechModelProgress) + Send + Sync, { - let client = reqwest::Client::builder() + let client = crate::reqwest_client_builder() .connect_timeout(Duration::from_secs(15)) .timeout(Duration::from_secs(30 * 60)) .build() diff --git a/src/crates/services/services-integrations/src/web_tools.rs b/src/crates/services/services-integrations/src/web_tools.rs index 971b513425..0c75ba5bc4 100644 --- a/src/crates/services/services-integrations/src/web_tools.rs +++ b/src/crates/services/services-integrations/src/web_tools.rs @@ -64,7 +64,7 @@ pub struct WebToolNetworkProvider; impl WebToolNetworkProvider { pub async fn fetch_text(url: &str) -> Result { - let client = reqwest::Client::builder() + let client = crate::reqwest_client_builder() .user_agent(USER_AGENT_VALUE) .timeout(Duration::from_secs(WEB_FETCH_TIMEOUT_SECS)) .build() @@ -105,7 +105,7 @@ impl WebToolNetworkProvider { } pub async fn search_exa(request: ExaSearchRequest<'_>) -> Result { - let client = reqwest::Client::builder() + let client = crate::reqwest_client_builder() .timeout(Duration::from_secs(EXA_TIMEOUT_SECS)) .build() .map_err(|error| WebToolNetworkError::BuildClient(error.to_string()))?; diff --git a/src/crates/services/skin-market-service/Cargo.toml b/src/crates/services/skin-market-service/Cargo.toml index de0c5f3e35..b89a512bd3 100644 --- a/src/crates/services/skin-market-service/Cargo.toml +++ b/src/crates/services/skin-market-service/Cargo.toml @@ -13,11 +13,12 @@ anyhow = { workspace = true } axum = { workspace = true } base64 = { workspace = true } bitfun-product-domains = { path = "../../contracts/product-domains", features = ["appearance-market"] } +bitfun-services-core = { path = "../services-core", features = ["tls-provider"] } chrono = { workspace = true } hex = { workspace = true } image = { workspace = true, features = ["gif", "jpeg", "png", "webp"] } hmac = { workspace = true } -reqwest = { workspace = true, features = ["http2", "json", "rustls"] } +reqwest = { workspace = true, features = ["http2", "json", "rustls-no-provider"] } semver = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/src/crates/services/skin-market-service/src/auth.rs b/src/crates/services/skin-market-service/src/auth.rs index 7c402a90b5..0fc3050f75 100644 --- a/src/crates/services/skin-market-service/src/auth.rs +++ b/src/crates/services/skin-market-service/src/auth.rs @@ -31,6 +31,7 @@ struct IdentityResponse { impl IdentityVerifier { pub(crate) fn new(me_url: Url) -> anyhow::Result { + bitfun_services_core::tls_provider::ensure_ring_crypto_provider(); Ok(Self { client: Client::builder() .connect_timeout(Duration::from_secs(3))