9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
185 lines
7.0 KiB
Rust
185 lines
7.0 KiB
Rust
//! Agent kind:0 profile reconciliation — split from `agents.rs` (file-size
|
|
//! guard). Owns the reconcile data carrier, the legacy-avatar backfill, and
|
|
//! the needs-sync predicate.
|
|
|
|
use tauri::AppHandle;
|
|
|
|
use crate::app_state::AppState;
|
|
use crate::managed_agents::managed_agent_avatar_url;
|
|
|
|
use super::*;
|
|
|
|
pub(crate) struct ProfileReconcileData {
|
|
pub(crate) private_key_nsec: String,
|
|
pub(crate) name: String,
|
|
pub(crate) relay_url: String,
|
|
/// Expected avatar URL for the published profile. `None` for legacy records
|
|
/// that predate the `avatar_url` field — these will be backfilled from the
|
|
/// relay's existing kind:0 profile on first reconciliation.
|
|
pub(crate) avatar_url: Option<String>,
|
|
pub(crate) auth_tag: Option<String>,
|
|
/// The agent's pubkey (hex). Needed to update the persisted record during
|
|
/// avatar backfill migration.
|
|
pub(crate) pubkey: String,
|
|
/// The agent's command (e.g. "goose"). Used as fallback when no profile
|
|
/// exists on the relay during avatar backfill.
|
|
pub(crate) agent_command: String,
|
|
/// Persona ID if this agent was created from a persona. Used during avatar
|
|
/// backfill to recover the correct avatar from the persona record when the
|
|
/// relay profile has been corrupted.
|
|
pub(crate) persona_id: Option<String>,
|
|
}
|
|
|
|
/// Resolve the avatar to backfill for a legacy agent record (pre-PR-921, no
|
|
/// stored `avatar_url`).
|
|
///
|
|
/// Priority: the persona's avatar wins, because the old reconciliation code
|
|
/// could have overwritten the relay's kind:0 `picture` with the command default
|
|
/// — making the relay an unreliable source for persona-backed agents. Only fall
|
|
/// back to the relay's `picture`, then the command icon, for agents with no
|
|
/// persona avatar to recover from.
|
|
pub(super) fn resolve_legacy_avatar(
|
|
persona_avatar: Option<String>,
|
|
relay_picture: Option<String>,
|
|
agent_command: &str,
|
|
) -> String {
|
|
persona_avatar
|
|
.or(relay_picture)
|
|
.or_else(|| managed_agent_avatar_url(agent_command))
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Reconcile an agent's kind:0 profile on the relay.
|
|
///
|
|
/// Queries the relay for the agent's existing profile and re-publishes if missing
|
|
/// or stale (display_name or picture mismatch). This is fire-and-forget — errors
|
|
/// are returned to the caller for logging but never block agent startup.
|
|
///
|
|
/// For legacy records (pre-PR-921) where `avatar_url` is `None`, this function
|
|
/// backfills via `resolve_legacy_avatar` — preferring the persona record's avatar
|
|
/// over the relay's `picture`, since the old code may have corrupted the relay
|
|
/// profile — and persists the updated record. After backfill, normal
|
|
/// reconciliation proceeds.
|
|
///
|
|
/// Query and publish target the relay returned by `effective_agent_relay_url`
|
|
/// for every agent regardless of backend: an explicit per-agent `relay_url`
|
|
/// wins, and a blank one falls back to the active workspace relay. This keeps
|
|
/// reconciliation following the session's relay for never-pinned agents while
|
|
/// honoring a deliberate pin wherever it points.
|
|
pub(crate) async fn reconcile_agent_profile(
|
|
state: &AppState,
|
|
app: &AppHandle,
|
|
agent_pubkey: &str,
|
|
data: &ProfileReconcileData,
|
|
) -> Result<(), String> {
|
|
use crate::relay::{query_agent_profile, sync_managed_agent_profile};
|
|
|
|
// An explicit per-agent relay wins; an empty one falls back to the active
|
|
// workspace relay. Resolved once and used for both the read and write-back.
|
|
let relay_url = crate::relay::effective_agent_relay_url(
|
|
&data.relay_url,
|
|
&relay_ws_url_with_override(state),
|
|
);
|
|
|
|
if !state
|
|
.managed_agent_profile_reconcile_enabled
|
|
.load(std::sync::atomic::Ordering::Acquire)
|
|
{
|
|
return Ok(());
|
|
}
|
|
|
|
// Query the relay for the agent's existing kind:0 profile.
|
|
let existing = query_agent_profile(state, &relay_url, agent_pubkey).await?;
|
|
|
|
// Resolve the expected avatar — backfilling for legacy records that have no
|
|
// stored avatar_url yet.
|
|
let expected_avatar = match data.avatar_url.as_deref() {
|
|
Some(url) => url.to_string(),
|
|
None => {
|
|
// Legacy record: the relay profile may have been corrupted by the
|
|
// old reconciliation code (it overwrote the persona avatar with the
|
|
// command default), so the persona record is the authoritative source.
|
|
let persona_avatar = data.persona_id.as_ref().and_then(|pid| {
|
|
load_personas(app)
|
|
.ok()?
|
|
.into_iter()
|
|
.find(|p| p.id == *pid)?
|
|
.avatar_url
|
|
});
|
|
|
|
let backfilled = resolve_legacy_avatar(
|
|
persona_avatar,
|
|
existing.as_ref().and_then(|info| info.picture.clone()),
|
|
&data.agent_command,
|
|
);
|
|
|
|
// Persist the backfilled avatar so this migration only runs once.
|
|
if !backfilled.is_empty() {
|
|
let _store_guard = state
|
|
.managed_agents_store_lock
|
|
.lock()
|
|
.map_err(|e| e.to_string())?;
|
|
let mut records = load_managed_agents(app)?;
|
|
if let Some(record) = records.iter_mut().find(|r| r.pubkey == data.pubkey) {
|
|
record.avatar_url = Some(backfilled.clone());
|
|
save_managed_agents(app, &records)?;
|
|
}
|
|
}
|
|
|
|
backfilled
|
|
}
|
|
};
|
|
|
|
let expected_avatar = if expected_avatar.is_empty() {
|
|
None
|
|
} else {
|
|
Some(expected_avatar)
|
|
};
|
|
|
|
if !profile_needs_sync(existing.as_ref(), &data.name, expected_avatar.as_deref()) {
|
|
return Ok(());
|
|
}
|
|
|
|
let agent_keys = Keys::parse(&data.private_key_nsec)
|
|
.map_err(|e| format!("failed to parse agent keys: {e}"))?;
|
|
|
|
if !state
|
|
.managed_agent_profile_reconcile_enabled
|
|
.load(std::sync::atomic::Ordering::Acquire)
|
|
{
|
|
return Ok(());
|
|
}
|
|
|
|
sync_managed_agent_profile(
|
|
state,
|
|
&relay_url,
|
|
&agent_keys,
|
|
&data.name,
|
|
expected_avatar.as_deref(),
|
|
data.auth_tag.as_deref(),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Decide whether a published profile is missing or stale relative to the
|
|
/// expected name and avatar. A missing profile always needs sync; a present
|
|
/// one is stale when either the display name or picture diverges.
|
|
pub(super) fn profile_needs_sync(
|
|
existing: Option<&crate::relay::AgentProfileInfo>,
|
|
expected_name: &str,
|
|
expected_avatar: Option<&str>,
|
|
) -> bool {
|
|
match existing {
|
|
None => true,
|
|
Some(info) => {
|
|
let name_matches = info.display_name.as_deref() == Some(expected_name);
|
|
let picture_matches = info.picture.as_deref() == expected_avatar;
|
|
!name_matches || !picture_matches
|
|
}
|
|
}
|
|
}
|
|
|
|
// Async so the blocking body (disk reads/writes + process termination) runs off
|
|
// the main UI thread via spawn_blocking. State is re-derived from the owned
|
|
// AppHandle inside the closure (`State<'_, _>` is borrowed, MutexGuard is !Send).
|