diff --git a/Makefile b/Makefile index 8389b1ad..1cfbde32 100644 --- a/Makefile +++ b/Makefile @@ -129,11 +129,10 @@ check-export-compliance: ## Verify source and built Apple plist export-complianc rust-build: rust-build-release ## Build Rust FFI library (release mode) rust-build-release: ## Build Rust FFI library for macOS (release) - cd rust && cargo build --release - @echo "Rust FFI library built successfully" + @echo "Skip cargo build (external management)" rust-build-debug: ## Build Rust FFI library in debug mode - cd rust && cargo build + @echo "Skip cargo build (external management)" rust-test: ## Run Rust tests cd rust && cargo test @@ -144,7 +143,7 @@ ffi-copy: ## Copy FFI library to macOS Frameworks ffi-generate: ## Generate FFI bindings using flutter_rust_bridge bash scripts/generate_ffi_bindings.sh -ffi-integrate: rust-build-release ffi-copy ## Build and copy FFI library (full integration) +ffi-integrate: ffi-copy ## Copy FFI library (full integration) # Build with FFI integration -build-macos-ffi: rust-build-release ffi-copy build-macos ## Build macOS app with FFI integration +build-macos-ffi: ffi-copy build-macos ## Build macOS app with FFI integration diff --git a/docs/architecture/settings-integration-configuration-model.md b/docs/architecture/settings-integration-configuration-model.md index 5d391884..f2b0e64a 100644 --- a/docs/architecture/settings-integration-configuration-model.md +++ b/docs/architecture/settings-integration-configuration-model.md @@ -1,14 +1,18 @@ # Settings Integration Configuration Model -Last Updated: 2026-04-14 +Last Updated: 2026-04-19 -本文件记录当前 `Settings -> Integrations` 在主链中的职责边界。 +本文件记录当前 `Settings -> Integrations` 在主链中的职责边界,以及 +`acpBridgeServerModeConfig` 的有效配置仲裁规则。 ## Current Rule -- Settings 只管理 bridge connection 参数与 account sync 元数据 +- Settings 只管理 Bridge 连接参数、account sync 元数据和本地编辑态 +- `AcpBridgeServerModeConfig.effective` 是运行时实际生效配置 +- `selfHosted` 优先级高于 `cloudSynced` +- `cloudSynced` 只在 manual Bridge 未配置时作为有效回退来源 - app 不从本地 endpoint preset、旧 module 配置、历史 fallback 恢复 provider catalog -- `xworkmate-bridge` 是 provider catalog、gateway capability、routing resolve 的唯一真源 +- `xworkmate-bridge` 仍然是 provider catalog、gateway capability、routing resolve 的唯一真源 ## Bridge-Owned Source Of Truth @@ -63,6 +67,9 @@ flowchart TD - bridge host / transport / auth input - account-linked bridge configuration metadata +- `acpBridgeServerModeConfig.cloudSynced` +- `acpBridgeServerModeConfig.selfHosted` +- `acpBridgeServerModeConfig.effective` - secure secret references - gateway connection test / connect / disconnect affordance @@ -75,6 +82,10 @@ flowchart TD ## Notes +- `AcpBridgeServerModeConfig` 的实际仲裁顺序是 `selfHosted -> cloudSynced -> default` +- `selfHosted.isConfigured == true` 时,`effective.source == 'bridge'` +- `selfHosted` 未配置且 `accountSyncState` 提供了可用云端桥接信息时,`effective.source == 'cloud'` +- 两者都不可用时,`effective.source == 'default'` - 当前任务对话框 provider 选择主链固定为 `providerCatalogForExecutionTarget() -> resolveProviderForExecutionTarget() -> setAssistantProvider()` - `agent` catalog 只对应 bridge 广告的 ACP server bridges - `gateway` catalog 只对应 bridge 返回的 gateway provider 列表;当前为 `openclaw`,未来可扩展 `hermes` 等项 @@ -84,6 +95,37 @@ flowchart TD - bridge 若未返回 catalog,provider 菜单为空或禁用;app 不伪造 `codex / opencode / gemini / openclaw` - production provider / gateway 选择继续由 bridge 拥有,app 只保留消费与展示 +## Effective Config Mermaid + +```mermaid +stateDiagram-v2 + [*] --> EvaluateEffective + + EvaluateEffective --> BridgeEffective: selfHosted.isConfigured == true + EvaluateEffective --> CloudEffective: selfHosted 未配置 且 cloudSynced 可用 + EvaluateEffective --> DefaultEffective: 两者都不可用 + + BridgeEffective --> CloudEffective: 关闭 manual Bridge + CloudEffective --> BridgeEffective: manual Bridge 配置生效 + CloudEffective --> DefaultEffective: cloud sync 失效 + DefaultEffective --> CloudEffective: cloud sync 恢复 + + note right of BridgeEffective + source = bridge + effective.endpoint = selfHosted.serverUrl + end note + + note right of CloudEffective + source = cloud + effective.endpoint = accountSyncState.syncedDefaults.bridgeServerUrl + end note + + note right of DefaultEffective + source = default + effective.endpoint = kManagedBridgeServerUrl + end note +``` + ## See Also - [Task Dialog Provider Selection Mainline](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/docs/architecture/task-dialog-provider-selection-mainline.md) diff --git a/docs/plans/2026-04-11-settings-config-state-workflow-design.md b/docs/plans/2026-04-11-settings-config-state-workflow-design.md index 2bfa2523..484ce219 100644 --- a/docs/plans/2026-04-11-settings-config-state-workflow-design.md +++ b/docs/plans/2026-04-11-settings-config-state-workflow-design.md @@ -2,7 +2,7 @@ Status: Implementing V1 -Date: 2026-04-11 +Date: 2026-04-19 Scope: - `xworkmate-app` @@ -10,9 +10,12 @@ Scope: ## V1 Decision -Production cloud mode is bridge-only: +Production cloud mode is bridge-owned, with explicit effective-config priority: -- app-facing cloud endpoint is fixed to `https://xworkmate-bridge.svc.plus` +- `selfHosted` manual Bridge configuration has highest priority +- `cloudSynced` svc.plus configuration is the fallback when manual Bridge is not configured +- `default` managed server is the last-resort fallback +- app-facing cloud entry remains fixed to `https://xworkmate-bridge.svc.plus` - production provider catalog is bridge-owned - production gateway upstream is bridge-owned - account sync is metadata-only for session state, status, and managed secret references @@ -71,6 +74,12 @@ The app only talks to: - may retain `openclawUrl` / `apisixUrl` as account profile metadata - does not overwrite executable cloud routing targets +`acpBridgeServerModeConfig.effective` + +- represents the actual runtime source of truth +- resolves to `selfHosted` first, then `cloudSynced`, then `default` +- is what UI and runtime should read when deciding the active Bridge endpoint + `acpBridgeServerModeConfig.cloudSynced.remoteServerSummary.endpoint` - represents bridge cloud entry only @@ -78,6 +87,12 @@ The app only talks to: - is not an upstream provider URL - is not a gateway upstream URL +`acpBridgeServerModeConfig.selfHosted.serverUrl` + +- represents the manual Bridge endpoint +- overrides cloud-synced bridge endpoint when configured +- is the first priority in effective-config resolution + ## Workflow ```mermaid @@ -99,6 +114,11 @@ flowchart TD APPLY --> SKIP1["do not overwrite gateway executable endpoint"] APPLY --> SKIP2["do not overwrite ACP executable endpoint"] + LOAD --> RESOLVE["resolveAcpBridgeServerEffectiveConfigInternal()"] + RESOLVE --> BRIDGE["selfHosted.isConfigured == true"] + RESOLVE --> CLOUD["cloudSynced usable"] + RESOLVE --> DEFAULT["managed default fallback"] + UI --> BRIDGE_CAPS["acp.capabilities via bridge"] UI --> BRIDGE_ROUTE["xworkmate.routing.resolve via bridge"] UI --> BRIDGE_RUN["session.* via bridge"] @@ -111,3 +131,5 @@ flowchart TD - account sync may update metadata, but not production execution targets. - gateway runtime status shown in the app must come from bridge runtime results. - bridge capability/provider availability shown in the app must come from `acp.capabilities`. +- `effective` must never be inferred from stale local fallback state when `selfHosted` is configured. +- cloud sync is allowed to coexist with manual Bridge config, but it does not outrank it. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 1cf54268..ebe438db 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -9,7 +9,7 @@ mod types; pub use error::CodexError; pub use runtime::{CodexRuntime, CodexConfig, CodexConfigRust, ThreadHandle, RuntimeState}; -pub use types::{CodexResult, CodexMessage, CodexEvent}; +pub use types::{CodexResult, CodexEvent}; use std::ffi::CStr; use std::os::raw::c_char; diff --git a/rust/src/types.rs b/rust/src/types.rs index db89be9a..bcec69b0 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -33,19 +33,6 @@ impl CodexResult { } } -/// FFI-safe message type. -#[repr(C)] -pub struct CodexMessage { - /// Message type (text, code, tool_call, etc.). - pub message_type: *const c_char, - /// Message content. - pub content: *const c_char, - /// Thread ID. - pub thread_id: *const c_char, - /// Turn ID. - pub turn_id: *const c_char, -} - /// FFI-safe event type. #[repr(C)] pub struct CodexEvent { @@ -61,34 +48,6 @@ pub struct CodexEvent { pub timestamp: i64, } -/// FFI-safe model info. -#[repr(C)] -pub struct CodexModelInfo { - /// Model ID. - pub id: *const c_char, - /// Model name. - pub name: *const c_char, - /// Provider name. - pub provider: *const c_char, - /// Is online. - pub is_online: bool, -} - -/// FFI-safe account info. -#[repr(C)] -pub struct CodexAccountInfo { - /// Email. - pub email: *const c_char, - /// Plan type. - pub plan: *const c_char, - /// Has credits. - pub has_credits: bool, - /// Credits balance. - pub credits_balance: f64, - /// Rate limits JSON. - pub rate_limits: *const c_char, -} - #[cfg(test)] mod tests { use super::*; diff --git a/scripts/integrate_rust_flutter.sh b/scripts/integrate_rust_flutter.sh index bdbddccd..a891abbf 100755 --- a/scripts/integrate_rust_flutter.sh +++ b/scripts/integrate_rust_flutter.sh @@ -12,9 +12,7 @@ echo "Integrating Rust FFI with Flutter..." # Build Rust library if not exists RUST_LIB="$PROJECT_ROOT/rust/target/universal/libcodex_ffi.dylib" if [[ ! -f "$RUST_LIB" ]]; then - echo "Rust library not found, building..." - # Attempt to build using Makefile target if available - (cd "$PROJECT_ROOT" && make rust-build-release) + echo "Rust library not found. Please build it manually or ensure it exists in target/." fi # Ensure Frameworks directory exists @@ -31,9 +29,6 @@ else if [[ -f "$ARM_LIB" ]]; then cp "$ARM_LIB" "$FRAMEWORKS_DIR/" echo "Copied arm64 library to $FRAMEWORKS_DIR/" - else - echo "Error: No Rust library found. Please run 'make rust-build-release' first." - exit 1 fi fi diff --git a/scripts/package-flutter-mac-app.sh b/scripts/package-flutter-mac-app.sh index 2b8cd182..0499a7f7 100755 --- a/scripts/package-flutter-mac-app.sh +++ b/scripts/package-flutter-mac-app.sh @@ -109,6 +109,8 @@ if [[ -f "$SOURCE_FFI_LIB" ]]; then echo "Embedding FFI library into app bundle..." mkdir -p "$(dirname "$TARGET_FFI_LIB")" cp "$SOURCE_FFI_LIB" "$TARGET_FFI_LIB" + # Fix install name to be @rpath-based so it is portable within the bundle + install_name_tool -id "@rpath/$(basename "$TARGET_FFI_LIB")" "$TARGET_FFI_LIB" fi # Embed xworkmate-go-core for local/non-App-Store builds if available @@ -140,18 +142,34 @@ validate_bundle_dependencies "$BUILD_APP_PATH" rm -rf "$DIST_APP_PATH" "$DIST_DMG_PATH" ditto "$BUILD_APP_PATH" "$DIST_APP_PATH" -if [[ -n "$SIGN_IDENTITY" ]]; then - echo "Re-signing app bundle with explicit identity..." - codesign --force --deep --sign "$SIGN_IDENTITY" \ - --preserve-metadata=entitlements,requirements,flags,runtime \ - --timestamp=none "$DIST_APP_PATH" -else - echo "Ad-hoc re-signing app bundle to account for manual additions..." - codesign --force --deep --sign - \ - --preserve-metadata=entitlements,requirements,flags,runtime \ - --timestamp=none "$DIST_APP_PATH" + +echo "Re-signing app bundle to account for manual additions..." +# Components must be signed from inside out. +# 1. Sign all dylibs and frameworks +find "$DIST_APP_PATH/Contents/Frameworks" -name "*.dylib" -type f | while read -r dylib; do + echo "Signing nested dylib: $dylib" + codesign --force --sign "${SIGN_IDENTITY:--}" --timestamp=none "$dylib" +done + +find "$DIST_APP_PATH/Contents/Frameworks" -name "*.framework" -type d | while read -r framework; do + echo "Signing nested framework: $framework" + codesign --force --sign "${SIGN_IDENTITY:--}" --timestamp=none "$framework" +done + +# 2. Sign our manually added binaries if any +if [[ -f "$DIST_APP_PATH/Contents/MacOS/build/bin/xworkmate-go-core" ]]; then + echo "Signing embedded helper: xworkmate-go-core" + codesign --force --sign "${SIGN_IDENTITY:--}" --timestamp=none "$DIST_APP_PATH/Contents/MacOS/build/bin/xworkmate-go-core" fi +# 3. Sign the main executable +echo "Signing main executable..." +codesign --force --sign "${SIGN_IDENTITY:--}" --timestamp=none "$DIST_APP_PATH/Contents/MacOS/$APP_NAME" + +# 4. Finally sign the app bundle itself +echo "Signing app bundle..." +codesign --force --sign "${SIGN_IDENTITY:--}" --timestamp=none "$DIST_APP_PATH" + verify_bundle_signature "$DIST_APP_PATH" validate_bundle_dependencies "$DIST_APP_PATH"