Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/apps/cli/src/peer_host/deny.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ static LOCAL_ONLY_COMMANDS: &[&str] = &[
"dispatch_install_cli_start",
"dispatch_install_cli_poll",
"dispatch_install_cli_cancel",
"dispatch_sync_model_config",
"dispatch_submit",
"dispatch_status",
"dispatch_cancel",
Expand Down Expand Up @@ -133,6 +134,7 @@ mod tests {
"dispatch_install_cli_start",
"dispatch_install_cli_poll",
"dispatch_install_cli_cancel",
"dispatch_sync_model_config",
"dispatch_submit",
"dispatch_status",
"dispatch_cancel",
Expand Down
18 changes: 13 additions & 5 deletions src/apps/cli/src/self_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -822,14 +822,22 @@ async fn download_text(client: &Client, url: &str) -> Result<String> {
/// Ed25519 (minisign) public key for official release archives, injected at
/// build time from the same `TAURI_UPDATER_PUBKEY` the Desktop updater trusts.
///
/// Absent in local and fork builds; those fall back to checksum-only, which is
/// why `signature_required` gates on it rather than assuming.
/// Forks that publish their own releases override this with their own key.
const RELEASE_PUBKEY: Option<&str> = option_env!("BITFUN_RELEASE_PUBKEY");

/// The trust root this binary was built with, if any. `Some` means an official
/// release build, and signature verification is then mandatory.
/// The official BitFun release public key (minisign key ID `50F47CBE6CC0A376`),
/// base64-wrapped the way Tauri wraps `minisign.pub`. Public data — each
/// release ships it as the `minisign.pub` asset — and the update source above
/// is pinned to the official repository, so local and fork builds verifying
/// against it is strictly stronger than their old checksum-only fallback.
const OFFICIAL_RELEASE_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDUwRjQ3Q0JFNkNDMEEzNzYKUldSMm84QnN2bnowVU9CYzNOb1RWVzA2d2RpR003cExQM0xwaUw0QTNTcDRueGtCc1dsSlJUeG4K";

/// The trust root for release archives. Always present, so signature
/// verification is mandatory on every update path.
fn release_pubkey() -> Option<&'static str> {
RELEASE_PUBKEY.filter(|key| !key.trim().is_empty())
RELEASE_PUBKEY
.filter(|key| !key.trim().is_empty())
.or(Some(OFFICIAL_RELEASE_PUBKEY))
}

/// Verify a Tauri-format `.sig` (base64 of a minisign signature file) over the
Expand Down
15 changes: 15 additions & 0 deletions src/apps/desktop/src/api/dispatch_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use bitfun_core::service::dispatch::{
get_device_dispatch_status, get_dispatch_status, list_device_dispatch_jobs, list_dispatch_jobs,
list_dispatch_targets, poll_dispatch_cli_install, probe_device_dispatch_target,
probe_dispatch_target, start_dispatch_cli_install, submit_device_dispatch, submit_dispatch,
sync_dispatch_model_config,
DeviceDispatchRpc, DispatchAnswerRequest, DispatchAppendRequest, DispatchConnectionRequest,
DispatchInstallPollRequest, DispatchInstallStartRequest, DispatchJobRequest,
DispatchListJobsRequest, DispatchListTargetsRequest, DispatchProbeTargetRequest,
Expand Down Expand Up @@ -172,6 +173,20 @@ pub async fn dispatch_install_cli_cancel(
.map_err(|error| error.to_string())
}

#[tauri::command]
pub async fn dispatch_sync_model_config(
state: State<'_, AppState>,
request: DispatchConnectionRequest,
) -> Result<(), String> {
let manager = state
.get_ssh_manager_async()
.await
.map_err(|error| error.to_string())?;
sync_dispatch_model_config(&manager, request)
.await
.map_err(|error| error.to_string())
}

#[tauri::command]
pub async fn dispatch_submit(
state: State<'_, AppState>,
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/src/api/peer_host_invoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ static LOCAL_ONLY_COMMANDS: &[&str] = &[
"dispatch_install_cli_start",
"dispatch_install_cli_poll",
"dispatch_install_cli_cancel",
"dispatch_sync_model_config",
"dispatch_submit",
"dispatch_status",
"dispatch_cancel",
Expand Down
4 changes: 4 additions & 0 deletions src/apps/desktop/src/api/remote_workspace_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] =
"dispatch_probe_target",
RemoteWorkspacePolicy::WorkspaceAgnostic,
),
(
"dispatch_sync_model_config",
RemoteWorkspacePolicy::WorkspaceAgnostic,
),
("dispatch_answer", RemoteWorkspacePolicy::WorkspaceAgnostic),
("dispatch_append", RemoteWorkspacePolicy::WorkspaceAgnostic),
("dispatch_status", RemoteWorkspacePolicy::WorkspaceAgnostic),
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1747,6 +1747,7 @@ pub async fn run() {
api::dispatch_api::dispatch_install_cli_start,
api::dispatch_api::dispatch_install_cli_poll,
api::dispatch_api::dispatch_install_cli_cancel,
api::dispatch_api::dispatch_sync_model_config,
api::dispatch_api::dispatch_submit,
api::dispatch_api::dispatch_status,
api::dispatch_api::dispatch_cancel,
Expand Down
12 changes: 11 additions & 1 deletion src/apps/server/src/routes/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use bitfun_core::external_sources::{
use bitfun_core::service::dispatch::{
answer_dispatch, append_dispatch, cancel_dispatch, cancel_dispatch_cli_install,
get_dispatch_status, list_dispatch_jobs, list_dispatch_targets, poll_dispatch_cli_install,
probe_dispatch_target, start_dispatch_cli_install, submit_dispatch, DispatchAnswerRequest,
probe_dispatch_target, start_dispatch_cli_install, submit_dispatch,
sync_dispatch_model_config, DispatchAnswerRequest,
DispatchAppendRequest, DispatchConnectionRequest, DispatchInstallPollRequest,
DispatchInstallStartRequest, DispatchJobRequest, DispatchListJobsRequest,
DispatchListTargetsRequest, DispatchProbeTargetRequest, DispatchStatusRequest,
Expand All @@ -28,6 +29,7 @@ pub(crate) fn supports(method: &str) -> bool {
| "dispatch_install_cli_start"
| "dispatch_install_cli_poll"
| "dispatch_install_cli_cancel"
| "dispatch_sync_model_config"
| "dispatch_submit"
| "dispatch_status"
| "dispatch_cancel"
Expand Down Expand Up @@ -90,6 +92,13 @@ pub(crate) async fn dispatch(
.map_err(operation_error)?;
Ok(serde_json::Value::Null)
}
"dispatch_sync_model_config" => {
let request = parse_request::<DispatchConnectionRequest>(&params)?;
sync_dispatch_model_config(&host.ssh_manager, request)
.await
.map_err(operation_error)?;
Ok(serde_json::Value::Null)
}
"dispatch_submit" => {
let request = parse_request::<DispatchSubmitRequest>(&params)?;
submit_dispatch(&host.ssh_manager, &store(host), request)
Expand Down Expand Up @@ -179,6 +188,7 @@ mod tests {
"dispatch_install_cli_start",
"dispatch_install_cli_poll",
"dispatch_install_cli_cancel",
"dispatch_sync_model_config",
"dispatch_submit",
"dispatch_status",
"dispatch_cancel",
Expand Down
42 changes: 42 additions & 0 deletions src/crates/assembly/core/src/service/dispatch/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,48 @@ pub async fn install_cli_cancel(
dispatch_ssh::install_cli_cancel(manager, request.connection_id.trim()).await
}

/// Copy this controller's model configuration (catalog, credentials, and
/// default-model selections) onto the SSH target so its CLI can resolve a
/// ready model. Explicit, credential-bearing operation: the UI must confirm
/// before calling it, mirroring CLI installation.
pub async fn sync_model_config(
manager: &SSHConnectionManager,
request: DispatchConnectionRequest,
) -> anyhow::Result<()> {
crate::service::config::initialize_global_config()
.await
.map_err(|error| anyhow::anyhow!("initialize controller configuration: {error}"))?;
let config_service = crate::service::config::get_global_config_service()
.await
.map_err(|error| anyhow::anyhow!("read controller configuration: {error}"))?;
let config: crate::service::config::GlobalConfig = config_service
.get_config(None)
.await
.map_err(|error| anyhow::anyhow!("load controller configuration: {error}"))?;
if !config.ai.models.iter().any(|model| model.enabled) {
anyhow::bail!("no enabled AI model is configured on this device to sync");
}
let ai = serde_json::to_value(&config.ai)
.map_err(|error| anyhow::anyhow!("encode controller model configuration: {error}"))?;
let mut payload = serde_json::Map::new();
for key in [
"models",
"default_models",
"agent_model_defaults",
"func_agent_models",
] {
if let Some(value) = ai.get(key) {
payload.insert(key.to_string(), value.clone());
}
}
dispatch_ssh::sync_model_config(
manager,
request.connection_id.trim(),
&Value::Object(payload),
)
.await
}

pub async fn submit(
manager: &SSHConnectionManager,
store: &OutboundDispatchStore,
Expand Down
3 changes: 2 additions & 1 deletion src/crates/assembly/core/src/service/dispatch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ pub use controller::{
install_cli_poll as poll_dispatch_cli_install, install_cli_start as start_dispatch_cli_install,
list_jobs as list_dispatch_jobs, list_targets as list_dispatch_targets,
probe_target as probe_dispatch_target, status as get_dispatch_status,
submit as submit_dispatch, DispatchAnswerRequest, DispatchAppendRequest,
submit as submit_dispatch, sync_model_config as sync_dispatch_model_config,
DispatchAnswerRequest, DispatchAppendRequest,
DispatchConnectionRequest, DispatchInstallPollRequest, DispatchInstallStartRequest,
DispatchJobRequest, DispatchListJobsRequest, DispatchListTargetsRequest,
DispatchPermissionReplyKind, DispatchProbeTargetRequest, DispatchStatusRequest,
Expand Down
Loading