Skip to content
Draft
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
5 changes: 4 additions & 1 deletion crates/buzz-backend-kubernetes/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,10 @@ mod tests {
assert_eq!(json["ok"], true);
assert_eq!(json["protocol_version"], wire::PROTOCOL_VERSION);
assert!(json["config_schema"]["properties"]["namespace"].is_object());
assert!(json.get("ops").is_none());
assert_eq!(
json["ops"],
serde_json::json!(["info", "deploy", "stop", "recover"])
);
}

/// `stop` is gated like a preflight: no approved identity policy, no
Expand Down
15 changes: 13 additions & 2 deletions crates/buzz-backend-kubernetes/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ use std::collections::BTreeMap;
/// The wire-contract version this provider speaks (spec §Info).
pub const PROTOCOL_VERSION: u32 = 1;

/// What this provider supports, advertised in `info.ops`. Additive within
/// protocol version 1. Each entry is a request `op`, except `recover`: it
/// advertises that `deploy` accepts `provider_config.sandbox.recovery`. A
/// desktop gates its session end/recover actions on `recover`.
pub const OPS: &[&str] = &["info", "deploy", "stop", "recover"];

/// Request envelope. `op` discriminates; unknown ops are an in-band error.
///
/// `request_id` is deliberately absent from every variant. The desktop sends
Expand Down Expand Up @@ -108,6 +114,7 @@ pub struct InfoResponse {
pub name: &'static str,
pub version: &'static str,
pub protocol_version: u32,
pub ops: &'static [&'static str],
pub description: &'static str,
pub config_schema: serde_json::Value,
}
Expand Down Expand Up @@ -267,6 +274,7 @@ impl Response {
name: "kubernetes",
version: env!("CARGO_PKG_VERSION"),
protocol_version: PROTOCOL_VERSION,
ops: OPS,
description: "Runs agents as pods in a Kubernetes cluster",
config_schema: crate::config::config_schema(),
})
Expand Down Expand Up @@ -335,9 +343,12 @@ mod tests {
}

#[test]
fn info_remains_compatible_until_the_desktop_accepts_ops() {
fn info_advertises_stop_and_recover() {
let v = serde_json::to_value(Response::info()).unwrap();
assert!(v.get("ops").is_none());
assert_eq!(
v["ops"],
serde_json::json!(["info", "deploy", "stop", "recover"])
);
}

#[test]
Expand Down
9 changes: 6 additions & 3 deletions crates/buzz-backend-kubernetes/tests/wire_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,12 @@ fn info_response_carries_the_contract_fields() {
assert_eq!(info["ok"], true);
assert_eq!(info["protocol_version"], 1);
assert_eq!(info["name"], "kubernetes");
// `ops` is not advertised yet: a desktop that predates it must keep
// accepting `info`. A later change adds it, additive within version 1.
assert!(info.get("ops").is_none());
// Additive within protocol version 1; the desktop gates its session
// end/recover actions on `recover`.
assert_eq!(
info["ops"],
serde_json::json!(["info", "deploy", "stop", "recover"])
);
let schema = &info["config_schema"];
assert_eq!(
schema["required"],
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/commands/agent_providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub async fn probe_backend_provider(binary_path: String) -> Result<serde_json::V
});
tokio::task::spawn_blocking(move || {
invoke_provider(&canonical, &request, std::time::Duration::from_secs(10))
.map_err(String::from)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
Expand Down
97 changes: 44 additions & 53 deletions desktop/src-tauri/src/commands/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ use crate::{
save_managed_agents, start_managed_agent_process, stop_managed_agent_process,
stop_managed_agent_workspace_pair, sync_managed_agent_processes, try_regenerate_nest,
validate_provider_config, BackendKind, CreateManagedAgentRequest,
CreateManagedAgentResponse, ManagedAgentRecord, ManagedAgentSummary, RelayMeshConfig,
DEFAULT_ACP_COMMAND, DEFAULT_AGENT_PARALLELISM, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS,
CreateManagedAgentResponse, ManagedAgentRecord, ManagedAgentSummary, ProviderFailure,
RelayMeshConfig, DEFAULT_ACP_COMMAND, DEFAULT_AGENT_PARALLELISM,
DEFAULT_AGENT_TURN_TIMEOUT_SECONDS,
},
relay::relay_ws_url_with_override,
util::now_iso,
Expand Down Expand Up @@ -332,6 +333,7 @@ where
}

pub(crate) use provider_deploy::deploy_to_provider;
use provider_deploy::{InvocationScope, ProviderAction};

// Async so the blocking body (disk reads of agent/persona records, per-agent
// process-liveness syscalls, and a possible save) runs on Tauri's worker pool
Expand Down Expand Up @@ -829,8 +831,10 @@ async fn create_managed_agent_in<R: tauri::Runtime>(
&& input.backend != BackendKind::Local
&& !input.backend.is_thread_sandbox()
{
if let BackendKind::Provider { ref id, ref config } = input.backend {
let agent_json = {
if let BackendKind::Provider { .. } = input.backend {
{
// Fail create on a payload that cannot be built; deploy
// rebuilds it after its lock.
let _g = state
.managed_agents_store_lock
.lock()
Expand All @@ -840,15 +844,11 @@ async fn create_managed_agent_in<R: tauri::Runtime>(
.iter()
.find(|r| r.pubkey == pubkey)
.ok_or_else(|| "agent disappeared".to_string())?;
build_deploy_payload(&app, state, rec)?
};
match deploy_to_provider(
&app, state, &pubkey, id, config, agent_json, None, None, None, None, None,
)
.await
{
build_deploy_payload(&app, state, rec)?;
}
match deploy_to_provider(&app, state, &pubkey, &Default::default(), None).await {
Ok(()) => spawn_error,
Err(e) => Some(e),
Err(e) => Some(e.message),
}
} else {
spawn_error
Expand Down Expand Up @@ -895,7 +895,7 @@ pub async fn start_managed_agent(
session_scope: Option<provider_deploy::RemoteSessionScope>,
app: AppHandle,
state: State<'_, AppState>,
) -> Result<ManagedAgentSummary, String> {
) -> Result<ManagedAgentSummary, ProviderFailure> {
// Snapshot the workspace owner pubkey for the legacy auth_tag fallback.
// Read outside the records lock to keep lock ordering simple.
let owner_hex = workspace_owner_hex(&state)?;
Expand Down Expand Up @@ -926,11 +926,7 @@ pub async fn start_managed_agent(
)?;
enum StartTarget {
Local,
Provider {
backend: BackendKind,
cached_binary_path: Option<String>,
agent_json: serde_json::Value,
},
Provider { backend: BackendKind },
}

// Collect backend info under lock; async preflight/spawn happens below.
Expand Down Expand Up @@ -973,33 +969,31 @@ pub async fn start_managed_agent(
let target = if record.backend == BackendKind::Local {
StartTarget::Local
} else {
// Fail before the deploy lock on a payload that cannot be
// built; deploy rebuilds it after the lock.
build_deploy_payload(&app, &state, record)?;
StartTarget::Provider {
backend: record.backend.clone(),
cached_binary_path: record.provider_binary_path.clone(),
agent_json: build_deploy_payload(&app, &state, record)?,
}
};

(target, reconcile)
};

let result = match target {
StartTarget::Local => {
start_local_agent_with_preflight(
&app,
&state,
&pubkey,
false,
expected_relay_url.as_deref(),
expected_signer_pubkey.as_deref(),
replay_floor_unix,
)
.await
}
StartTarget::Local => start_local_agent_with_preflight(
&app,
&state,
&pubkey,
false,
expected_relay_url.as_deref(),
expected_signer_pubkey.as_deref(),
replay_floor_unix,
)
.await
.map_err(ProviderFailure::from),
StartTarget::Provider {
backend: BackendKind::Provider { id, config },
cached_binary_path,
agent_json,
backend: BackendKind::Provider { .. },
} => {
// The caller's captured scope is asserted INSIDE deploy_to_provider
// against the payload rebuilt after the deploy lock — the exact
Expand All @@ -1008,20 +1002,13 @@ pub async fn start_managed_agent(
// The replay floor rides along so a publish-first mention send's
// remote harness replays past the already-published message, same
// as the local spawn path.
deploy_to_provider(
&app,
&state,
&pubkey,
&id,
&config,
agent_json,
cached_binary_path.as_deref(),
expected_relay_url.as_deref(),
expected_signer_pubkey.as_deref(),
replay_floor_unix,
session_scope.as_ref(),
)
.await?;
let scope = InvocationScope {
expected_relay_url: expected_relay_url.as_deref(),
expected_signer_pubkey: expected_signer_pubkey.as_deref(),
session: session_scope.as_ref(),
action: ProviderAction::Deploy,
};
deploy_to_provider(&app, &state, &pubkey, &scope, replay_floor_unix).await?;

// Return updated summary.
let _store_guard = state
Expand All @@ -1037,11 +1024,11 @@ pub async fn start_managed_agent(
.iter()
.find(|r| r.pubkey == pubkey)
.ok_or_else(|| format!("agent {pubkey} not found"))?;
summarize_from_disk(&app, record, &runtimes)
Ok(summarize_from_disk(&app, record, &runtimes)?)
}
StartTarget::Provider { backend } => {
Err(format!("agent {pubkey} has unsupported backend kind: {backend:?}").into())
}
StartTarget::Provider { backend, .. } => Err(format!(
"agent {pubkey} has unsupported backend kind: {backend:?}"
)),
};

// ── Profile reconciliation (fire-and-forget) ────────────────────────────
Expand Down Expand Up @@ -1229,11 +1216,15 @@ pub async fn delete_managed_agent(
mod deploy;
pub(super) mod provider_access;
mod provider_deploy;
mod remote_session;
pub(super) use deploy::build_deploy_payload;
#[cfg(test)]
use deploy::{deploy_payload_json, DeployProjections};
#[cfg(test)]
use deploy::{ensure_remote_provider_supported, resolve_deploy_model_provider};
pub use remote_session::{
end_remote_agent_session, recover_remote_agent_session, remote_agent_session_supports_lifecycle,
};

#[path = "agents_profile.rs"]
mod profile;
Expand Down
52 changes: 11 additions & 41 deletions desktop/src-tauri/src/commands/agents/provider_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ pub(super) fn needs_reconciliation_with_policy(
#[derive(Debug)]
struct ProviderAccessTarget {
pubkey: String,
provider_id: String,
config: serde_json::Value,
cached_binary_path: Option<String>,
agent_json: Result<serde_json::Value, String>,
}

Expand All @@ -40,13 +37,10 @@ fn collect_targets_with(
records
.into_iter()
.filter(|record| needs_reconciliation_with_policy(record, owner_only_access))
.map(|record| match record.backend.clone() {
BackendKind::Provider { id, config } => ProviderAccessTarget {
.map(|record| match record.backend {
BackendKind::Provider { .. } => ProviderAccessTarget {
agent_json: build_payload(&record),
pubkey: record.pubkey,
provider_id: id,
config,
cached_binary_path: record.provider_binary_path,
},
BackendKind::Local => {
unreachable!("provider access reconciliation selected a local agent")
Expand Down Expand Up @@ -77,36 +71,15 @@ pub(crate) async fn reconcile_on_workspace_apply(
};

for target in targets {
let ProviderAccessTarget {
pubkey,
provider_id,
config,
cached_binary_path,
agent_json,
} = target;
let agent_json = match agent_json {
Ok(agent_json) => agent_json,
Err(error) => {
persist_failure(app, state, &pubkey, &error)?;
return Err(format!(
"provider access reconciliation failed for agent {pubkey}: {error}"
));
}
};
if let Err(error) = super::deploy_to_provider(
app,
state,
&pubkey,
&provider_id,
&config,
agent_json,
cached_binary_path.as_deref(),
None,
None,
None,
None,
)
.await
let ProviderAccessTarget { pubkey, agent_json } = target;
if let Err(error) = agent_json {
persist_failure(app, state, &pubkey, &error)?;
return Err(format!(
"provider access reconciliation failed for agent {pubkey}: {error}"
));
}
if let Err(error) =
super::deploy_to_provider(app, state, &pubkey, &Default::default(), None).await
{
return Err(format!(
"provider access reconciliation failed for agent {pubkey}: {error}"
Expand Down Expand Up @@ -198,8 +171,6 @@ mod tests {

assert_eq!(targets.len(), 1);
assert_eq!(targets[0].pubkey, "agent");
assert_eq!(targets[0].provider_id, "provider");
assert_eq!(targets[0].config["region"], "test");
assert_eq!(
targets[0].agent_json.as_ref().unwrap()["respond_to"],
"owner-only"
Expand Down Expand Up @@ -231,7 +202,6 @@ mod tests {

assert_eq!(targets.len(), 1);
assert_eq!(targets[0].pubkey, "pending-agent");
assert_eq!(targets[0].provider_id, "pending-provider");
assert_eq!(
targets[0].agent_json.as_ref().unwrap()["pubkey"],
"pending-agent"
Expand Down
Loading
Loading