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
5 changes: 3 additions & 2 deletions desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ const overrides = new Map([
["src-tauri/src/commands/agents.rs", 881], // remote agent lifecycle routing (local + provider branches) + scope enforcement + persona pack metadata wiring + mcp_toolsets field + NIP-OA auth_tag in deploy payload
["src-tauri/src/commands/messages.rs", 510], // feed multi-query + NIP-50 search + forum thread resolution + thread ref + reactions via REQ
["src-tauri/src/nostr_convert.rs", 870], // 12 Nostr event→model converters (channels, profiles, members, notes, search, agents, relay members) + 20 unit tests
["src-tauri/src/managed_agents/runtime.rs", 780], // KNOWN_AGENT_BINARIES const + process_belongs_to_us FFI (macOS proc_name + Linux /proc/comm) + terminate_process + start/stop/sync lifecycle + pack persona live-read + login shell PATH augmentation + observer endpoint wiring + git credential helper env injection + sprout-agent mcp_hooks wiring + tests
["src-tauri/src/managed_agents/runtime.rs", 990], // ... + respond-to gate env (SPROUT_ACP_RESPOND_TO[_ALLOWLIST]) + per-mode env builder + tests
["src-tauri/src/managed_agents/types.rs", 700], // ManagedAgentRecord/Summary + Create/Update request structs + RespondTo enum + validate_respond_to_allowlist + tests
["src-tauri/src/managed_agents/backend.rs", 530], // provider IPC, validation, discovery, binary resolution + tests
["src/features/huddle/HuddleContext.tsx", 650], // huddle lifecycle context + joinHuddle + connectAndSetupMedia shared helper + activeSpeakers/isReconnecting state + PTT (reusable AudioContext) + TTS subscription + mic level analyser (10fps throttle) + agent pubkey refresh
["src/features/agents/hooks.ts", 540], // agent query/mutation surface now includes built-in persona library activation + useUpdateManagedAgentMutation
Expand All @@ -62,7 +63,7 @@ const overrides = new Map([
["src/features/agents/ui/CreateAgentDialog.tsx", 685], // provider selector + config form + schema-typed config coercion + required field validation + locked scopes
["src/features/channels/ui/AddChannelBotDialog.tsx", 640], // provider mode: Run on selector, trust warning, probe effect, single-agent enforcement, provider warnings display
["src/features/settings/ui/ChannelTemplatesSettingsCard.tsx", 850], // template CRUD card + TemplateFormDialog (persona/team chip selectors + provider assignments + canvas template) + TemplateTeamSelector + ProviderAssignments + ProviderRow
["src/shared/api/types.ts", 580], // persona provider/model fields + forum types + workflow type re-exports + ephemeral channel TTL fields + mcpToolsets + sourcePack + UpdateManagedAgentInput edit fields + UserStatus/UserStatusLookup (NIP-38) + RelayMember/RelayMemberRole types + ChannelTemplate/TemplateAgentEntry/TemplateTeamEntry
["src/shared/api/types.ts", 620], // ... + RespondToMode + respondTo/respondToAllowlist on ManagedAgent/Create/Update inputs
["src-tauri/src/events.rs", 610], // event builders + build_huddle_guidelines (kind:48106) + post_event_raw transport helper + participant p-tag on join/leave + NIP-43 relay admin builders (add/remove/change-role) + check_relay_role + DM/presence/workflow command builders
["src-tauri/src/huddle/kokoro.rs", 980], // Kokoro ONNX TTS engine + three-tier G2P + ARPAbet→IPA + CoreML + synth_chunk() public API + style validation + hyphenated compound splitting + 23 unit tests
["src-tauri/src/huddle/mod.rs", 1020], // huddle state machine + Tauri commands + sync protocol doc; state/relay/pipeline extracted + emit_huddle_state_changed wiring
Expand Down
24 changes: 24 additions & 0 deletions desktop/src-tauri/src/commands/agent_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,30 @@ pub async fn update_managed_agent(
if let Some(mcp_command) = input.mcp_command {
record.mcp_command = mcp_command;
}

// Inbound author gate: merge patch onto current values, then validate
// the merged state. This lets a single update switch to Allowlist AND
// supply pubkeys atomically.
let prospective_mode = input.respond_to.unwrap_or(record.respond_to);
let prospective_allowlist = match input.respond_to_allowlist.as_ref() {
Some(list) => crate::managed_agents::validate_respond_to_allowlist(list)?,
None => record.respond_to_allowlist.clone(),
};
if prospective_mode == crate::managed_agents::RespondTo::Allowlist
&& prospective_allowlist.is_empty()
{
return Err(
"respond-to mode 'allowlist' requires at least one pubkey in the allowlist"
.to_string(),
);
}
record.respond_to = prospective_mode;
// Preserve the persisted allowlist across mode toggles — only replace
// when the caller explicitly supplied a new list.
if input.respond_to_allowlist.is_some() {
record.respond_to_allowlist = prospective_allowlist;
}

record.updated_at = now_iso();

save_managed_agents(&app, &records)?;
Expand Down
41 changes: 39 additions & 2 deletions desktop/src-tauri/src/commands/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ use crate::{
util::now_iso,
};

/// Read the workspace owner's pubkey hex from app state without holding the
/// lock for longer than necessary. Used to populate `SPROUT_ACP_AGENT_OWNER`
/// as a fallback for legacy agent records that have no NIP-OA `auth_tag`.
fn workspace_owner_hex(state: &AppState) -> Result<String, String> {
let keys = state.keys.lock().map_err(|e| e.to_string())?;
Ok(keys.public_key().to_hex())
}

/// Build the standard agent JSON payload for provider deploy calls.
fn build_deploy_payload(record: &ManagedAgentRecord) -> serde_json::Value {
serde_json::json!({
Expand All @@ -34,6 +42,10 @@ fn build_deploy_payload(record: &ManagedAgentRecord) -> serde_json::Value {
"idle_timeout_seconds": record.idle_timeout_seconds,
"max_turn_duration_seconds": record.max_turn_duration_seconds,
"parallelism": record.parallelism,
// Inbound author gate. Providers that don't yet read these fall back
// to the harness default (`owner-only`) — no protocol break.
"respond_to": record.respond_to,
"respond_to_allowlist": &record.respond_to_allowlist,
})
}

Expand Down Expand Up @@ -151,6 +163,24 @@ pub async fn create_managed_agent(
}
}

// Validate & normalize the respond-to allowlist BEFORE any side effects.
// The harness has its own validator (sprout-acp/src/config.rs) but we want
// to catch malformed input at the boundary so the agent never tries to
// start with a list that will crash it on launch.
let respond_to_allowlist =
crate::managed_agents::validate_respond_to_allowlist(&input.respond_to_allowlist)?;
if input.respond_to == crate::managed_agents::RespondTo::Allowlist
&& respond_to_allowlist.is_empty()
{
return Err(
"respond-to mode 'allowlist' requires at least one pubkey in the allowlist".to_string(),
);
}

// Snapshot the workspace owner pubkey for the legacy-record auth_tag
// fallback. Computed outside the records lock to keep lock ordering simple.
let owner_hex = workspace_owner_hex(&state)?;

// ── Phase 1: generate keys (sync lock) ────────────────────────────────────
let (agent_keys, private_key_nsec, pubkey, resolved_relay_url, input) = {
let _store_guard = state
Expand Down Expand Up @@ -350,14 +380,18 @@ pub async fn create_managed_agent(
last_stopped_at: None,
last_exit_code: None,
last_error: None,
respond_to: input.respond_to,
respond_to_allowlist: respond_to_allowlist.clone(),
};

records.push(record);

let mut spawn_error = None;
if input.spawn_after_create && input.backend == BackendKind::Local {
let record = find_managed_agent_mut(&mut records, &pubkey)?;
if let Err(error) = start_managed_agent_process(&app, record, &mut runtimes) {
if let Err(error) =
start_managed_agent_process(&app, record, &mut runtimes, Some(&owner_hex))
{
record.updated_at = now_iso();
record.last_error = Some(error.clone());
spawn_error = Some(error);
Expand Down Expand Up @@ -455,6 +489,9 @@ pub async fn start_managed_agent(
app: AppHandle,
state: State<'_, AppState>,
) -> Result<ManagedAgentSummary, String> {
// 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)?;
// Collect backend info and handle local vs provider under lock.
let (backend, cached_binary_path, agent_json) = {
let _store_guard = state
Expand All @@ -475,7 +512,7 @@ pub async fn start_managed_agent(

if record.backend == BackendKind::Local {
// Local: spawn in-process and return immediately.
start_managed_agent_process(&app, record, &mut runtimes)?;
start_managed_agent_process(&app, record, &mut runtimes, Some(&owner_hex))?;
save_managed_agents(&app, &records)?;
let record = records
.iter()
Expand Down
13 changes: 12 additions & 1 deletion desktop/src-tauri/src/managed_agents/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,29 @@ pub fn restore_managed_agents_on_launch(
return Ok(());
}

// Snapshot the workspace owner pubkey once for the legacy auth_tag fallback.
// Read outside the per-agent spawn loop so all parallel spawns see the same
// value and we don't lock `state.keys` repeatedly.
let owner_hex: Option<String> = state
.keys
.lock()
.map_err(|e| e.to_string())
.ok()
.map(|k| k.public_key().to_hex());

// ── Phase B (no locks): resolve commands and spawn processes in parallel ──
let spawn_results: Vec<(
String,
Result<(std::process::Child, std::path::PathBuf), String>,
)> = std::thread::scope(|scope| {
let owner_hex_ref = owner_hex.as_deref();
let handles: Vec<_> = agents_to_start
.iter()
.filter(|_| !shutdown_started.load(Ordering::SeqCst))
.map(|record| {
let pubkey = record.pubkey.clone();
let handle = scope.spawn(move || {
let result = spawn_agent_child(app, record);
let result = spawn_agent_child(app, record, owner_hex_ref);
(pubkey, result)
});
handle
Expand Down
Loading
Loading