fix(desktop): use destination-local credentials for remote launch

Route signed lifecycle requests through ordinary async preflight and shared captured launch admission. Require an already provisioned matching local agent key, revalidate after awaits and teardown, and retain independent Stop.

Persist named launch lifecycle metadata without migrating or restoring captured credentials. Add isolated signed receiver and actual teardown regressions for revoked keys, missing executables, request expiry, and fresh same-host Start.

Native fixtures require Unix --no-default-features and remain unexecuted locally; dependency-complete CI owns compilation and runtime validation.

Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-09-07 12:50:12 -04:00
parent 12f75703b7
commit b8d72f55da
20 changed files with 811 additions and 312 deletions

View File

@@ -50,7 +50,8 @@ pub enum Outcome {
Running,
/// Actual status confirms no managed process at this target.
Stopped,
/// Destination-local broker session issuance is not available.
/// Legacy receiver refusal, retained for wire compatibility. New receivers
/// use destination-local credentials and report Ineligible when unavailable.
ProvisioningUnavailable,
/// Runtime/readiness/ownership rejected the request.
Failed,

View File

@@ -165,7 +165,6 @@ where
true,
None,
None,
None,
&plan,
true,
false,

View File

@@ -5,7 +5,7 @@ use super::{
};
use crate::{
app_state::AppState,
managed_agents::{self, broker_launch, placement, retention::open_retention_db},
managed_agents::{self, placement, retention::open_retention_db},
};
use buzz_core_pkg::{
desktop_lifecycle::{Action, Outcome, Request, ResultMessage, RuntimeConfigurationRef},
@@ -132,8 +132,8 @@ pub fn read_desktop_placement(
placement::latest_start(&open_retention_db(&scope.db_path)?, &agent)
}
fn generation(
app: &AppHandle,
fn generation<R: tauri::Runtime>(
app: &AppHandle<R>,
state: &AppState,
agent: &str,
community: &str,
@@ -158,7 +158,7 @@ fn generation(
.managed_agents_store_lock
.lock()
.map_err(|e| e.to_string())?;
let records = managed_agents::load_managed_agents(app)?;
let records = managed_agents::storage::load_agent_store(app)?;
let legacy = records
.iter()
.find(|r| r.pubkey == agent)
@@ -181,8 +181,8 @@ fn generation(
Ok(None)
}
fn status(
app: &AppHandle,
fn status<R: tauri::Runtime>(
app: &AppHandle<R>,
conn: &rusqlite::Connection,
state: &AppState,
event: &Event,
@@ -201,8 +201,8 @@ fn status(
conn.execute("DELETE FROM desktop_status_generation WHERE rowid NOT IN (SELECT rowid FROM desktop_status_generation ORDER BY rowid DESC LIMIT 256)",[]).map_err(|e|e.to_string())?;
Ok(Outcome::Running)
}
fn current_observation(
app: &AppHandle,
fn current_observation<R: tauri::Runtime>(
app: &AppHandle<R>,
conn: &rusqlite::Connection,
state: &AppState,
request: &Request,
@@ -236,6 +236,37 @@ pub async fn receive_desktop_lifecycle(
community: String,
event: Event,
) -> Result<Option<Event>, String> {
let preflight_app = app.clone();
receive_desktop_lifecycle_with(app, owner, community, event, move |model, allow| {
let app = preflight_app.clone();
async move {
#[cfg(feature = "mesh-llm")]
{
crate::commands::ensure_relay_mesh_for_record(&app, model.as_deref(), allow).await
}
#[cfg(not(feature = "mesh-llm"))]
{
let _ = (app, model, allow);
Ok(())
}
}
})
.await
}
// Production receiver with only ordinary provider I/O replaceable in isolated tests.
pub(crate) async fn receive_desktop_lifecycle_with<R, F, Fut>(
app: AppHandle<R>,
owner: String,
community: String,
event: Event,
preflight: F,
) -> Result<Option<Event>, String>
where
R: tauri::Runtime,
F: Fn(Option<String>, bool) -> Fut,
Fut: std::future::Future<Output = Result<(), String>>,
{
// Authenticate and fence the destination before reading any configuration or
// doing ordinary async preflight. Never hold the transition lock across await.
let request = {
@@ -259,18 +290,23 @@ pub async fn receive_desktop_lifecycle(
&request.target.agent,
)?;
let fresh = nostr::Timestamp::now().as_secs() < event.created_at.as_secs().saturating_add(30);
let prepared = if owned
let mut prepared = if owned
&& fresh
&& matches!(
request.action,
Action::Start | Action::Restart | Action::Preflight
) {
configurations::prepare(&app, &owner, &request).await
configurations::prepare(&app, &owner, &request, &preflight).await
} else {
Err(Outcome::Ineligible)
};
if let Ok(captured) = &mut prepared {
captured
.plan
.expire_at(event.created_at.as_secs().saturating_add(30));
}
let catalog = if owned && fresh && request.action == Action::Catalog {
configurations::catalog(&app, &owner, &request).await
configurations::catalog(&app, &owner, &request, &preflight).await
} else {
Err("Catalog is unavailable".into())
};
@@ -333,8 +369,8 @@ pub async fn receive_desktop_lifecycle(
.map_err(|e| format!("Desktop lifecycle task failed: {e}"))?
}
fn execute(
app: &AppHandle,
fn execute<R: tauri::Runtime>(
app: &AppHandle<R>,
state: &AppState,
conn: &rusqlite::Connection,
owner: &str,
@@ -367,19 +403,6 @@ fn execute(
if configurations::revalidate(app, owner, request, plan).is_err() {
return Ok(Outcome::Ineligible);
}
// Provision before destructive Restart Stop. No supported session: leave the
// existing child running and return the precise non-secret missing capability.
let broker = match broker_launch::provision(
broker_launch::LaunchScope {
owner,
community: &target.community,
agent: &target.agent,
},
plan.plan.record(),
) {
Ok(b) => b,
Err(outcome) => return Ok(outcome),
};
// Shared admission validates the captured Stop fence and generation BEFORE
// destructive Restart. The transition lock spans that Stop, launch and receipt.
if placement::blocked(conn, &target.agent, &target.desktop)? {
@@ -391,7 +414,6 @@ fn execute(
true,
None,
plan.resume.as_ref(),
Some(&broker),
&plan.plan,
false, // Explicit wire reference, not the destination's next selection.
request.action == Action::Restart,

View File

@@ -10,23 +10,32 @@ pub(super) struct CapturedLaunch {
pub(super) generation: Option<String>,
}
fn record(app: &AppHandle, agent: &str) -> Result<managed_agents::ManagedAgentRecord, String> {
fn record<R: tauri::Runtime>(
app: &AppHandle<R>,
agent: &str,
) -> Result<managed_agents::ManagedAgentRecord, String> {
let state = app.state::<AppState>();
let _store = state
.managed_agents_store_lock
.lock()
.map_err(|e| e.to_string())?;
managed_agents::load_managed_agents(app)?
managed_agents::storage::load_managed_agents_for_launch(app)?
.into_iter()
.find(|r| r.pubkey == agent)
.ok_or_else(|| "Agent is not provisioned on this Desktop".into())
}
pub(super) async fn prepare(
app: &AppHandle,
pub(super) async fn prepare<R, F, Fut>(
app: &AppHandle<R>,
owner: &str,
request: &Request,
) -> Result<CapturedLaunch, Outcome> {
preflight: &F,
) -> Result<CapturedLaunch, Outcome>
where
R: tauri::Runtime,
F: Fn(Option<String>, bool) -> Fut,
Fut: std::future::Future<Output = Result<(), String>>,
{
let reference = request.configuration.as_ref().ok_or(Outcome::Ineligible)?;
let mut captured = (|| -> Result<CapturedLaunch, String> {
let state = app.state::<AppState>();
@@ -72,39 +81,20 @@ pub(super) async fn prepare(
})
})()
.map_err(|_| Outcome::Ineligible)?;
// Do not advertise an eligible Switch that is known to fail after source Stop.
broker_launch::provision(
broker_launch::LaunchScope {
owner,
community: &request.target.community,
agent: &request.target.agent,
},
captured.plan.record(),
)?;
configurations::preflight_with(
&mut captured.plan,
owner,
&request.target.community,
false,
|model, allow| async move {
#[cfg(feature = "mesh-llm")]
{
crate::commands::ensure_relay_mesh_for_record(app, model.as_deref(), allow).await
}
#[cfg(not(feature = "mesh-llm"))]
{
let _ = (app, model, allow);
Ok(())
}
},
preflight,
)
.await
.map_err(|_| Outcome::Ineligible)?;
Ok(captured)
}
pub(super) fn revalidate(
app: &AppHandle,
pub(super) fn revalidate<R: tauri::Runtime>(
app: &AppHandle<R>,
owner: &str,
request: &Request,
captured: &CapturedLaunch,
@@ -122,11 +112,17 @@ pub(super) fn revalidate(
)
}
pub(super) async fn catalog(
app: &AppHandle,
pub(super) async fn catalog<R, F, Fut>(
app: &AppHandle<R>,
owner: &str,
request: &Request,
) -> Result<CatalogPage, String> {
preflight: &F,
) -> Result<CatalogPage, String>
where
R: tauri::Runtime,
F: Fn(Option<String>, bool) -> Fut,
Fut: std::future::Future<Output = Result<(), String>>,
{
let record = record(app, &request.target.agent)?;
let mut entries =
configurations::catalog_for_app(app, &record, owner, &request.target.community)?
@@ -177,7 +173,18 @@ pub(super) async fn catalog(
cursor: None,
..request.clone()
};
entry.eligible = prepare(app, owner, &probe).await.is_ok();
entry.eligible = match prepare(app, owner, &probe, preflight).await {
Ok(captured) => {
let state = app.state::<AppState>();
let _transition = state
.managed_agent_runtime_transition
.lock()
.map_err(|e| e.to_string())?;
scope(app, &state, owner, &request.target.community)?;
revalidate(app, owner, &probe, &captured).is_ok()
}
Err(_) => false,
};
}
}
Ok(CatalogPage { entry, next })

View File

@@ -8,8 +8,8 @@ use tauri::{AppHandle, State};
use crate::app_state::AppState;
use crate::managed_agents::retention::{active_retention_scope, open_retention_db, RetentionScope};
pub(super) fn scope(
app: &AppHandle,
pub(super) fn scope<R: tauri::Runtime>(
app: &AppHandle<R>,
state: &AppState,
owner: &str,
community: &str,

View File

@@ -61,6 +61,15 @@ pub async fn receive_desktop_stop(
owner: String,
community: String,
event: Event,
) -> Result<Option<Event>, String> {
receive_desktop_stop_for_app(app, owner, community, event).await
}
pub(crate) async fn receive_desktop_stop_for_app<R: tauri::Runtime>(
app: AppHandle<R>,
owner: String,
community: String,
event: Event,
) -> Result<Option<Event>, String> {
tokio::task::spawn_blocking(move || {
let state = app.state::<AppState>();
@@ -144,8 +153,8 @@ pub fn read_desktop_stop_results(
}
/// Local possession/profile alone never establishes owner authority.
pub(super) fn owned_local(
app: &AppHandle,
pub(super) fn owned_local<R: tauri::Runtime>(
app: &AppHandle<R>,
state: &AppState,
owner: &str,
agent: &str,
@@ -154,7 +163,7 @@ pub(super) fn owned_local(
.managed_agents_store_lock
.lock()
.map_err(|e| e.to_string())?;
let records = managed_agents::load_managed_agents(app)?;
let records = managed_agents::storage::load_agent_store(app)?;
Ok(records.iter().find(|r| r.pubkey == agent).is_some_and(|r| {
r.backend == managed_agents::BackendKind::Local
&& r.auth_tag

View File

@@ -1,181 +0,0 @@
//! Destination-local keyless launch boundary. Credentials never cross IPC/relay.
//! Automatic issuance is not in the merged broker contract. Keep the production
//! provider fail-closed until that host-owned adapter is supplied; do not mint a
//! competing bearer token or fall back to exporting the agent key.
use super::ManagedAgentRecord;
use buzz_core_pkg::desktop_lifecycle::Outcome;
use std::process::Command;
pub(crate) struct LaunchScope<'a> {
pub owner: &'a str,
pub community: &'a str,
pub agent: &'a str,
}
/// A host-issued session, bound to this destination's independently resolved
/// inputs. No Debug/Serialize: the bearer credential is not diagnostic data.
pub(crate) struct BrokerSession {
owner: String,
community: String,
agent: String,
endpoint: String,
credential: zeroize::Zeroizing<String>,
channels: Vec<String>,
expires_at: u64,
}
impl BrokerSession {
/// Future host provisioning adapter calls this with its authenticated reply,
/// never values from an incoming lifecycle command or user environment.
#[allow(dead_code)] // consumed by the pending automatic-issuance host adapter
pub(crate) fn from_host(
scope: LaunchScope<'_>,
endpoint: String,
credential: String,
channels: Vec<String>,
expires_at: u64,
) -> Result<Self, String> {
let url = url::Url::parse(&endpoint).map_err(|_| "Invalid broker endpoint")?;
if !matches!(url.scheme(), "https" | "http")
|| url.host_str().is_none()
|| (url.scheme() == "http"
&& !matches!(url.host_str(), Some("127.0.0.1" | "localhost" | "[::1]")))
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
|| credential.trim().is_empty()
|| channels.is_empty()
|| channels.len() > 256
|| channels.iter().any(|c| uuid::Uuid::parse_str(c).is_err())
{
return Err("Invalid broker provisioning".into());
}
let session = Self {
owner: scope.owner.into(),
community: scope.community.into(),
agent: scope.agent.into(),
endpoint,
credential: zeroize::Zeroizing::new(credential),
channels,
expires_at,
};
session.validate(scope)?;
Ok(session)
}
pub(crate) fn validate(&self, scope: LaunchScope<'_>) -> Result<(), String> {
if self.owner != scope.owner
|| self.community != scope.community
|| self.agent != scope.agent
|| self.expires_at <= nostr::Timestamp::now().as_secs().saturating_add(30)
{
return Err("Broker session expired or scope changed".into());
}
Ok(())
}
/// Applied last, after local user/provider configuration. Never let saved
/// environment turn a keyless launch into keyful authentication.
pub(crate) fn apply(
&self,
command: &mut Command,
scope: LaunchScope<'_>,
) -> Result<(), String> {
self.validate(scope)?;
for key in [
"BUZZ_PRIVATE_KEY",
"NOSTR_PRIVATE_KEY",
"BUZZ_AUTH_TAG",
"BUZZ_API_TOKEN",
"BUZZ_ACP_PRIVATE_KEY",
"BUZZ_ACP_API_TOKEN",
"BUZZ_RELAY_URL",
"BUZZ_ACP_RELAY_URL",
"BUZZ_ACP_RESPOND_TO_ALLOWLIST",
"GIT_CONFIG_COUNT",
] {
command.env_remove(key);
}
command
.env("BUZZ_AGENT_MODE", "broker")
.env("BUZZ_BROKER_URL", &self.endpoint)
.env("BUZZ_BROKER_CREDENTIAL", self.credential.as_str())
.env("BUZZ_BROKER_RELAY_URL", &self.community)
.env("BUZZ_ACP_AGENT_OWNER", &self.owner)
.env("BUZZ_ACP_CHANNELS", self.channels.join(","))
.env("BUZZ_ACP_RESPOND_TO", "owner-only")
.env("BUZZ_ACP_ALLOWED_RESPOND_TO", "owner-only");
Ok(())
}
}
/// Explicit external integration gate, not a fabricated successful launch.
pub(crate) fn provision(
_scope: LaunchScope<'_>,
_record: &ManagedAgentRecord,
) -> Result<BrokerSession, Outcome> {
Err(Outcome::ProvisioningUnavailable)
}
#[cfg(test)]
mod tests {
use super::*;
fn scope() -> LaunchScope<'static> {
LaunchScope {
owner: "owner",
community: "wss://one.example",
agent: "agent",
}
}
#[test]
fn scope_expiry_and_final_environment_are_enforced() {
let session = BrokerSession::from_host(
scope(),
"https://broker.example".into(),
"secret".into(),
vec![uuid::Uuid::new_v4().to_string()],
nostr::Timestamp::now().as_secs() + 300,
)
.unwrap();
assert!(session
.validate(LaunchScope {
community: "wss://other.example",
..scope()
})
.is_err());
let mut command = Command::new("unused");
command
.env("BUZZ_PRIVATE_KEY", "must-not-escape")
.env("NOSTR_PRIVATE_KEY", "must-not-escape")
.env("BUZZ_AGENT_MODE", "local");
session.apply(&mut command, scope()).unwrap();
let env: std::collections::BTreeMap<_, _> = command.get_envs().collect();
assert_eq!(
env.get(std::ffi::OsStr::new("BUZZ_PRIVATE_KEY")),
Some(&None)
);
assert_eq!(
env.get(std::ffi::OsStr::new("NOSTR_PRIVATE_KEY")),
Some(&None)
);
assert_eq!(
env.get(std::ffi::OsStr::new("BUZZ_AGENT_MODE")),
Some(&Some(std::ffi::OsStr::new("broker")))
);
assert!(BrokerSession::from_host(
scope(),
"http://remote.example".into(),
"secret".into(),
vec![uuid::Uuid::new_v4().to_string()],
nostr::Timestamp::now().as_secs() + 300
)
.is_err());
assert!(BrokerSession::from_host(
scope(),
"https://broker.example".into(),
"secret".into(),
vec![uuid::Uuid::new_v4().to_string()],
0
)
.is_err());
}
}

View File

@@ -12,7 +12,6 @@ mod agent_description;
pub(crate) use agent_description::{effective_agent_description, record_effective_description};
mod backend;
pub(crate) mod bestie_assignment;
pub(crate) mod broker_launch;
pub(crate) mod claude_config;
pub(crate) mod config_bridge;
pub(crate) mod custom_harnesses;

View File

@@ -396,7 +396,7 @@ where
None,
)?;
super::terminate_untracked_pair_runtime(app, &key)?;
super::runtime::spawn_agent_child_with_broker(
super::runtime::spawn_agent_child_prepared(
app,
current,
&relay_url,
@@ -404,7 +404,6 @@ where
owner_hex_ref,
None,
None,
None,
Some(prepared),
)
})();

View File

@@ -473,7 +473,7 @@ pub fn spawn_agent_child<R: tauri::Runtime>(
replay_floor_unix: Option<u64>,
resume: Option<&super::remote_stop::ResumeTicket>,
) -> Result<crate::managed_agents::ManagedAgentProcess, String> {
spawn_agent_child_with_broker(
spawn_agent_child_prepared(
app,
record,
relay_url,
@@ -482,12 +482,11 @@ pub fn spawn_agent_child<R: tauri::Runtime>(
replay_floor_unix,
resume,
None,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn spawn_agent_child_with_broker<R: tauri::Runtime>(
pub(crate) fn spawn_agent_child_prepared<R: tauri::Runtime>(
app: &AppHandle<R>,
record: &ManagedAgentRecord,
relay_url: &str,
@@ -495,18 +494,11 @@ pub(crate) fn spawn_agent_child_with_broker<R: tauri::Runtime>(
owner_hex: Option<&str>,
replay_floor_unix: Option<u64>,
resume: Option<&super::remote_stop::ResumeTicket>,
broker: Option<&super::broker_launch::BrokerSession>,
prepared: Option<&super::runtime_configurations::PreparedLaunch>,
) -> Result<crate::managed_agents::ManagedAgentProcess, String> {
let key = ManagedAgentRuntimeKey::new(record.pubkey.clone(), relay_url)?;
super::remote_stop::check_launch(app, &key, relay_url, owner_hex, resume)?;
if let Some(session) = broker {
session.validate(super::broker_launch::LaunchScope {
owner: owner_hex.ok_or("Desktop owner unavailable")?,
community: relay_url,
agent: &record.pubkey,
})?;
} else if let Some(error) = spawn_key_refusal(record) {
if let Some(error) = spawn_key_refusal(record) {
return Err(error);
}
let runtime_key = ManagedAgentRuntimeKey::new(record.pubkey.clone(), relay_url)?;
@@ -535,7 +527,11 @@ pub(crate) fn spawn_agent_child_with_broker<R: tauri::Runtime>(
plan.require_preflight()?;
plan.check_scope(owner_hex, relay_url)?;
plan.revalidate(record, &personas, &global)?;
let record = &plan.record;
// Keep projected settings immutable, but deliver only the current credential
// that just passed revalidation, never the captured plan's copy.
let mut launch_record = plan.record.clone();
launch_record.private_key_nsec = record.private_key_nsec.clone();
let record = &launch_record;
let effective_cfg = plan.effective.clone();
let descriptor = &plan.descriptor;
let effective_command = &descriptor.command;
@@ -876,16 +872,6 @@ pub(crate) fn spawn_agent_child_with_broker<R: tauri::Runtime>(
command.creation_flags(CREATE_NO_WINDOW);
}
if let Some(session) = broker {
session.apply(
&mut command,
super::broker_launch::LaunchScope {
owner: owner_hex.ok_or("Desktop owner unavailable")?,
community: relay_url,
agent: &record.pubkey,
},
)?;
}
// Applied last so inherited environment cannot weaken explicit model selection.
let configuration = super::runtime_configurations::selected(record)?;
let required_model = configuration
@@ -1024,7 +1010,7 @@ pub(crate) fn start_managed_agent_process_prepared<R: tauri::Runtime>(
// replace. Selection enforces host-preserving authority provenance and
// uses the ordinary process-tree termination contract.
terminate_untracked_pair_runtime(app, &key)?;
let mut process = spawn_agent_child_with_broker(
let mut process = spawn_agent_child_prepared(
app,
record,
workspace_relay.as_str(),
@@ -1032,7 +1018,6 @@ pub(crate) fn start_managed_agent_process_prepared<R: tauri::Runtime>(
owner_hex,
replay_floor_unix,
resume,
None,
prepared,
)?;
let now = now_iso();

View File

@@ -356,7 +356,6 @@ where
true,
None,
resume.as_ref(),
None,
&plan,
true,
restart,
@@ -368,21 +367,6 @@ where
.map_err(|e| format!("runtime admission task failed: {e}"))?
}
/// The keyless lifecycle adapter remains provisioning-unavailable. A synchronous
/// caller cannot manufacture async launch authority when that adapter is added;
/// it must carry a preflighted plan into start_pair_captured_locked instead.
pub(crate) fn start_pair_locked(
_pubkey: String,
_relay_url: String,
_lazy: bool,
_expected_updated_at: Option<&str>,
_explicit_start: bool,
_broker: Option<&super::broker_launch::BrokerSession>,
_app: AppHandle,
) -> Result<ManagedAgentRuntimeStatus, String> {
Err("Captured preflighted runtime launch required".into())
}
/// Captured automatic starts additionally fence the durable next-launch selection.
#[allow(clippy::too_many_arguments)]
pub(crate) fn start_pair_captured_locked<R: tauri::Runtime>(
@@ -391,7 +375,6 @@ pub(crate) fn start_pair_captured_locked<R: tauri::Runtime>(
lazy: bool,
expected_updated_at: Option<&str>,
resume: Option<&super::remote_stop::ResumeTicket>,
broker: Option<&super::broker_launch::BrokerSession>,
plan: &super::runtime_configurations::PreparedLaunch,
check_selection: bool,
restart: bool,
@@ -406,7 +389,11 @@ pub(crate) fn start_pair_captured_locked<R: tauri::Runtime>(
.managed_agents_store_lock
.lock()
.map_err(|e| e.to_string())?;
let mut records = load_managed_agents(&app)?;
let mut records = if plan.configuration().is_some() {
super::storage::load_managed_agents_for_launch(&app)?
} else {
load_managed_agents(&app)?
};
let record = find_managed_agent_mut(&mut records, &pubkey)?;
if record.backend != BackendKind::Local {
return Err("managed runtime pairs require a local agent".into());
@@ -480,7 +467,23 @@ pub(crate) fn start_pair_captured_locked<R: tauri::Runtime>(
state.clear_agent_session_cache(&key);
}
let process_result = (|| {
let mut process = super::spawn_agent_child_with_broker(
if plan.configuration().is_some() {
// Stop may take time. A credential revoked during teardown must not
// be resurrected from the captured plan or the pre-Stop record.
record.private_key_nsec.clear();
let fresh = super::storage::load_managed_agents_for_launch(&app)?;
let current = fresh
.iter()
.find(|r| r.pubkey == record.pubkey)
.ok_or("Agent removed during launch")?;
plan.revalidate(
current,
&load_personas(&app)?,
&load_global_agent_config(&app)?,
)?;
record.private_key_nsec = current.private_key_nsec.clone();
}
let mut process = super::spawn_agent_child_prepared(
&app,
record,
&relay_url,
@@ -488,7 +491,6 @@ pub(crate) fn start_pair_captured_locked<R: tauri::Runtime>(
Some(&owner),
None,
resume,
broker,
Some(plan),
)?;
let mut receipt = ManagedAgentRuntimeReceipt::new(
@@ -518,7 +520,11 @@ pub(crate) fn start_pair_captured_locked<R: tauri::Runtime>(
status.lifecycle = ManagedAgentRuntimeLifecycle::Failed;
status.error = Some(error);
drop(runtimes);
save_managed_agents(&app, &records)?;
if plan.configuration().is_some() {
super::storage::save_runtime_metadata(&app, record)?;
} else {
save_managed_agents(&app, &records)?;
}
emit_status(&app, &status);
return Ok(status);
}
@@ -534,7 +540,11 @@ pub(crate) fn start_pair_captured_locked<R: tauri::Runtime>(
super::remote_stop::finish_resume(&app, &key, &relay_url, Some(&owner), resume)?;
let status = status_for(&app, record, &key, runtimes.get(&key), None);
drop(runtimes);
save_managed_agents(&app, &records)?;
if plan.configuration().is_some() {
super::storage::save_runtime_metadata(&app, record)?;
} else {
save_managed_agents(&app, &records)?;
}
emit_status(&app, &status);
Ok(status)
}

View File

@@ -43,6 +43,7 @@ pub(crate) struct PreparedLaunch {
pub(super) effective: super::effective_config::EffectiveAgentConfig,
legacy_default: bool,
preflight_complete: bool,
expires_at: Option<u64>,
host: String,
scope: (String, String),
// Catalog-only preparation has no app context and cannot be executed.
@@ -60,9 +61,20 @@ impl PreparedLaunch {
Ok(())
}
/// Bound a remote request through destructive Stop as well as async preflight.
pub(crate) fn expire_at(&mut self, deadline: u64) {
self.expires_at = Some(self.expires_at.map_or(deadline, |old| old.min(deadline)));
}
/// Preparation is not launch authority. Only the successful ordinary async
/// provider boundary can authorize these exact inputs for shared spawn.
pub(crate) fn require_preflight(&self) -> Result<(), String> {
if self
.expires_at
.is_some_and(|deadline| nostr::Timestamp::now().as_secs() >= deadline)
{
return Err("Runtime launch request expired".into());
}
if !self.preflight_complete {
return Err("Captured runtime launch has not completed provider preflight".into());
}
@@ -160,6 +172,7 @@ pub(crate) fn prepare(
effective,
legacy_default: false,
preflight_complete: false,
expires_at: None,
host: host.into(),
scope: (owner.into(), community.into()),
app_inputs: None,
@@ -184,6 +197,7 @@ fn prepare_default(
record,
legacy_default: true,
preflight_complete: false,
expires_at: None,
host: String::new(),
scope: (owner.into(), community.into()),
app_inputs: None,
@@ -353,10 +367,14 @@ pub(crate) fn preflight(
if keys.public_key().to_hex() != record.pubkey {
return Err("Local identity does not match this agent".into());
}
if super::resolve_command(&record.acp_command).is_none() {
if !super::resolve_command(&record.acp_command)
.is_some_and(|path| super::discovery::is_executable_file(&path))
{
return Err("ACP runtime is unavailable".into());
}
if super::resolve_command(&descriptor.command).is_none() {
if !super::resolve_command(&descriptor.command)
.is_some_and(|path| super::discovery::is_executable_file(&path))
{
return Err("Selected runtime is unavailable on this Desktop".into());
}
if config.is_some() {

View File

@@ -325,7 +325,7 @@ async fn shared_spawn_registers_exact_plan_and_rejects_edits_or_lost_identity()
)
.unwrap();
assert!(plan.require_preflight().is_err());
let unpreflighted = agents::spawn_agent_child_with_broker(
let unpreflighted = agents::spawn_agent_child_prepared(
app.handle(),
&record,
community,
@@ -333,7 +333,6 @@ async fn shared_spawn_registers_exact_plan_and_rejects_edits_or_lost_identity()
Some(&owner),
None,
None,
None,
Some(&plan),
)
.unwrap_err();
@@ -503,3 +502,6 @@ fn ordinary_selection_fence_includes_default() {
#[cfg(unix)]
#[path = "orchestration_tests.rs"]
mod orchestration;
#[cfg(all(unix, not(feature = "system-keyring")))]
mod remote_credentials;

View File

@@ -0,0 +1,533 @@
//! Signed production receiver → ordinary provider preflight → exact-plan child
//! registration. Only destination-local synthetic identities and bounded fake
//! children; run with --no-default-features (never a real OS credential store).
use super::*;
use buzz_core_pkg::{
desktop_lifecycle::{Action, Outcome, Request, ResultMessage},
desktop_stop::{StopOutcome, StopResult, StopTarget},
};
use nostr::{Event, EventBuilder, Timestamp};
use std::{cell::Cell, os::unix::fs::PermissionsExt};
use tauri::Manager;
const COMMUNITY: &str = "wss://remote-credential-fixture.example";
struct Fixture {
app: tauri::App<tauri::test::MockRuntime>,
record: ManagedAgentRecord,
owner: nostr::Keys,
named: RuntimeConfiguration,
stamp: Cell<u64>,
env: Vec<(&'static str, Option<std::ffi::OsString>)>,
temp: tempfile::TempDir,
}
impl Fixture {
fn new() -> Self {
let temp = tempfile::tempdir().unwrap();
let env = ["HOME", "XDG_DATA_HOME", "PATH", "BUZZ_PRIVATE_KEY"]
.map(|key| (key, std::env::var_os(key)))
.to_vec();
let owner = nostr::Keys::generate();
std::env::set_var("HOME", temp.path());
std::env::set_var("XDG_DATA_HOME", temp.path());
std::env::set_var("PATH", format!("{}:/usr/bin:/bin", temp.path().display()));
std::env::set_var("BUZZ_PRIVATE_KEY", owner.secret_key().to_secret_hex());
agents::clear_resolve_cache();
let mut record = record();
attest(&mut record, &owner);
// This key exists only in this isolated fixture. The child checks local
// credential delivery without writing a secret to output or diagnostics.
let child = format!(
"#!/bin/sh\n[ \"$BUZZ_PRIVATE_KEY\" = '{}' ] || exit 12\nprintf '%s|%s\\n' \"$BUZZ_ACP_REQUIRED_MODEL\" \"$BUZZ_ACP_MODEL\" >> '{}'\nexec /bin/sleep 20\n",
record.private_key_nsec, temp.path().join("launches").display()
);
for (name, script) in [
("buzz-acp", child.as_str()),
("buzz-agent", "#!/bin/sh\nexit 0\n"),
("buzz-dev-mcp", "#!/bin/sh\nexit 0\n"),
] {
let path = temp.path().join(name);
std::fs::write(&path, script).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
}
let app = tauri::test::mock_builder()
.manage(crate::app_state::build_app_state())
.build(tauri::test::mock_context(tauri::test::noop_assets()))
.unwrap();
*app.state::<crate::app_state::AppState>()
.relay_url_override
.lock()
.unwrap() = Some(COMMUNITY.into());
assert_eq!(
app.state::<crate::app_state::AppState>()
.signing_keys()
.unwrap()
.public_key(),
owner.public_key()
);
record.acp_command = temp.path().join("buzz-acp").display().to_string();
record.runtime = Some("buzz-agent".into());
record.agent_command = "buzz-agent".into();
record
.env_vars
.insert("OPENAI_COMPAT_API_KEY".into(), "fixture-only".into());
let host = local_host(app.handle(), &owner.public_key().to_hex(), COMMUNITY).unwrap();
let mut named = config(&host);
named.workspace = Some(temp.path().display().to_string());
let named = save(&mut record, &owner.public_key().to_hex(), COMMUNITY, named);
let fixture = Self {
app,
record,
owner,
named,
stamp: Cell::new(Timestamp::now().as_secs().saturating_sub(10)),
env,
temp,
};
fixture.persist();
fixture
}
fn persist(&self) {
agents::save_managed_agents(self.app.handle(), &[self.record.clone()]).unwrap();
}
fn target(&self) -> StopTarget {
StopTarget {
v: 1,
community: COMMUNITY.into(),
desktop: self.named.host.clone(),
agent: self.record.pubkey.clone(),
}
}
fn ordered(&self, event: Event) -> Event {
let stamp = self.stamp.get() + 1;
self.stamp.set(stamp);
EventBuilder::new(event.kind, event.content)
.tags(event.tags.iter().cloned())
.custom_created_at(Timestamp::from_secs(stamp))
.sign_with_keys(&self.owner)
.unwrap()
}
fn request(&self, action: Action, observed: Option<String>) -> Event {
self.ordered(
Request {
target: self.target(),
action,
observed,
configuration: (!matches!(action, Action::Catalog | Action::Status))
.then(|| self.named.reference()),
cursor: None,
}
.sign(&self.owner)
.unwrap(),
)
}
async fn receive<F, Fut>(&self, event: Event, preflight: F) -> ResultMessage
where
F: Fn(Option<String>, bool) -> Fut,
Fut: std::future::Future<Output = Result<(), String>>,
{
let response = crate::commands::receive_desktop_lifecycle_with(
self.app.handle().clone(),
self.owner.public_key().to_hex(),
COMMUNITY.into(),
event.clone(),
preflight,
)
.await
.unwrap()
.unwrap();
let result = ResultMessage::read(&response, &self.owner, &event, COMMUNITY).unwrap();
let safe = serde_json::to_string(&result).unwrap();
assert!(
!safe.contains(&self.record.private_key_nsec)
|| self.record.private_key_nsec.is_empty()
);
assert!(!safe.contains("fixture-only"));
assert!(!safe.contains(self.temp.path().to_str().unwrap()));
result
}
async fn action(&self, action: Action, observed: Option<String>) -> ResultMessage {
self.receive(self.request(action, observed), |model, allow| async move {
assert!(model.is_none());
assert!(!allow);
Ok(())
})
.await
}
fn running(&self) -> Option<(u32, Option<RuntimeConfigurationRef>)> {
let state = self.app.state::<crate::app_state::AppState>();
let mut runtimes = state.managed_agent_processes.lock().unwrap();
let key = agents::ManagedAgentRuntimeKey::new(&self.record.pubkey, COMMUNITY).unwrap();
runtimes.get_mut(&key).map(|runtime| {
assert!(runtime.child.try_wait().unwrap().is_none());
(
runtime.child.id(),
runtime.spawn_config.runtime_configuration.clone(),
)
})
}
fn launched(&self, expected: &str) {
for _ in 0..100 {
if std::fs::read_to_string(self.temp.path().join("launches"))
.unwrap_or_default()
.contains(expected)
{
return;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
panic!("fixture child did not confirm exact model and destination-local credential");
}
async fn stop(&self) -> StopOutcome {
let event = self.ordered(self.target().sign(&self.owner).unwrap());
let response = crate::commands::desktop_stop::receive_desktop_stop_for_app(
self.app.handle().clone(),
self.owner.public_key().to_hex(),
COMMUNITY.into(),
event.clone(),
)
.await
.unwrap()
.unwrap();
StopResult::read(&response, &self.owner, &event, COMMUNITY)
.unwrap()
.outcome
}
}
impl Drop for Fixture {
fn drop(&mut self) {
for (_, mut runtime) in self
.app
.state::<crate::app_state::AppState>()
.managed_agent_processes
.lock()
.unwrap()
.drain()
{
let _ = agents::terminate_process(runtime.child.id());
let _ = runtime.child.wait();
}
for (key, value) in self.env.drain(..) {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
agents::clear_resolve_cache();
}
}
#[tokio::test]
async fn provisioned_destination_catalog_start_and_same_host_switch_use_shared_launch() {
let _guard = agents::lock_path_mutex();
let mut fixture = Fixture::new();
let catalog = fixture.action(Action::Catalog, None).await;
assert_eq!(catalog.outcome, Outcome::Ready);
assert!(
catalog
.observation
.unwrap()
.catalog
.unwrap()
.entry
.unwrap()
.eligible
);
assert_eq!(
fixture.action(Action::Preflight, None).await.outcome,
Outcome::Ready
);
let started = fixture.action(Action::Start, None).await;
assert_eq!(started.outcome, Outcome::Running);
assert_eq!(
started.observation.unwrap().running_configuration,
Some(fixture.named.reference())
);
fixture.launched("fixture-model|fixture-model");
let old_pid = fixture.running().unwrap().0;
let status = fixture.action(Action::Status, None).await;
let mut changed = fixture.named.clone();
changed.model = "switched-model".into();
fixture.named = save(
&mut fixture.record,
&fixture.owner.public_key().to_hex(),
COMMUNITY,
changed,
);
fixture.persist();
let switched = fixture.action(Action::Restart, Some(status.id)).await;
assert_eq!(switched.outcome, Outcome::Running);
assert_ne!(fixture.running().unwrap().0, old_pid);
assert_eq!(
fixture.running().unwrap().1,
Some(fixture.named.reference())
);
fixture.launched("switched-model|switched-model");
assert_eq!(fixture.stop().await, StopOutcome::Stopped);
assert!(fixture.running().is_none());
// Deliberate same-host Stop → fresh Start consumes the new Stop fence,
// unlike a stale Restart continuation. Keep the exact switched revision.
assert_eq!(
fixture.action(Action::Preflight, None).await.outcome,
Outcome::Ready
);
assert_eq!(
fixture.action(Action::Start, None).await.outcome,
Outcome::Running
);
assert_eq!(
fixture.running().unwrap().1,
Some(fixture.named.reference())
);
assert_eq!(fixture.stop().await, StopOutcome::Stopped);
}
#[tokio::test]
async fn unavailable_identity_or_runtime_excludes_catalog_and_refuses_start() {
let _guard = agents::lock_path_mutex();
for missing in ["absent", "wrong", "runtime"] {
let mut fixture = Fixture::new();
match missing {
"absent" => fixture.record.private_key_nsec.clear(),
"wrong" => {
fixture.record.private_key_nsec =
nostr::Keys::generate().secret_key().to_secret_hex()
}
_ => {
// Cache was warmed by catalog readiness. Revocation must still stat.
assert!(agents::resolve_command(&fixture.record.acp_command).is_some());
std::fs::remove_file(&fixture.record.acp_command).unwrap();
}
}
fixture.persist();
let catalog = fixture.action(Action::Catalog, None).await;
assert_eq!(catalog.outcome, Outcome::Ready);
assert!(
!catalog
.observation
.unwrap()
.catalog
.unwrap()
.entry
.unwrap()
.eligible
);
assert_eq!(
fixture.action(Action::Start, None).await.outcome,
Outcome::Ineligible
);
assert!(fixture.running().is_none());
assert!(!fixture.temp.path().join("launches").exists());
}
}
#[tokio::test]
async fn revoked_key_keeps_existing_process_visible_and_restart_refuses_without_teardown() {
let _guard = agents::lock_path_mutex();
let mut fixture = Fixture::new();
assert_eq!(
fixture.action(Action::Start, None).await.outcome,
Outcome::Running
);
fixture.launched("fixture-model|fixture-model");
let pid = fixture.running().unwrap().0;
fixture.record.private_key_nsec.clear();
fixture.persist();
let status = fixture.action(Action::Status, None).await;
assert_eq!(status.outcome, Outcome::Running);
assert_eq!(
fixture
.action(Action::Restart, Some(status.id))
.await
.outcome,
Outcome::Ineligible
);
assert_eq!(fixture.running().unwrap().0, pid);
assert_eq!(fixture.stop().await, StopOutcome::Stopped);
assert!(fixture.running().is_none());
}
#[tokio::test]
async fn key_loss_during_preflight_is_rechecked_for_start_and_catalog() {
let _guard = agents::lock_path_mutex();
for action in [Action::Start, Action::Catalog] {
let fixture = Fixture::new();
let result = fixture
.receive(fixture.request(action, None), |_, _| {
let mut revoked = fixture.record.clone();
revoked.private_key_nsec.clear();
agents::save_managed_agents(fixture.app.handle(), &[revoked]).unwrap();
async { Ok(()) }
})
.await;
if action == Action::Start {
assert_eq!(result.outcome, Outcome::Ineligible);
} else {
assert!(
!result
.observation
.unwrap()
.catalog
.unwrap()
.entry
.unwrap()
.eligible
);
}
assert!(fixture.running().is_none());
assert!(!fixture.temp.path().join("launches").exists());
}
}
#[tokio::test]
async fn failed_destination_preflight_never_tears_down_existing_source() {
let _guard = agents::lock_path_mutex();
let fixture = Fixture::new();
assert_eq!(
fixture.action(Action::Start, None).await.outcome,
Outcome::Running
);
fixture.launched("fixture-model|fixture-model");
let pid = fixture.running().unwrap().0;
let result = fixture
.receive(fixture.request(Action::Preflight, None), |_, _| async {
Err("isolated provider unavailable".into())
})
.await;
assert_eq!(result.outcome, Outcome::Ineligible);
assert_eq!(fixture.running().unwrap().0, pid);
}
// The fake child changes only its temporary fixture files when ordinary Stop
// signals it. This binds revocation to the actual teardown boundary, after both
// async and locked admission have already accepted the old key/executable.
fn on_stop(fixture: &Fixture, effect: &str) {
let script = std::fs::read_to_string(&fixture.record.acp_command).unwrap();
let script = script
.replacen(
"#!/bin/sh\n",
&format!("#!/bin/sh\ntrap '{}' TERM\n", effect),
1,
)
.replace("exec /bin/sleep 20", "/bin/sleep 20 &\nwait");
std::fs::write(&fixture.record.acp_command, script).unwrap();
}
fn assert_failed_without_child(fixture: &Fixture) -> ManagedAgentRecord {
assert!(fixture.running().is_none());
let saved = agents::storage::load_agent_store(fixture.app.handle())
.unwrap()
.into_iter()
.find(|r| r.pubkey == fixture.record.pubkey)
.unwrap();
assert!(saved.runtime_pid.is_none());
assert!(saved.last_stopped_at.is_some());
assert!(saved.last_error.is_some());
assert_eq!(
std::fs::read_to_string(fixture.temp.path().join("launches"))
.unwrap()
.lines()
.count(),
1
);
saved
}
#[tokio::test]
async fn post_stop_key_loss_or_executable_loss_persists_failed_without_restoring_credentials() {
let _guard = agents::lock_path_mutex();
for revoke_key in [true, false] {
let fixture = Fixture::new();
let store_path = agents::storage::managed_agents_store_path(fixture.app.handle()).unwrap();
let revoked_path = fixture.temp.path().join("revoked.json");
let mut revoked = fixture.record.clone();
revoked.private_key_nsec.clear();
std::fs::write(&revoked_path, serde_json::to_vec(&[revoked]).unwrap()).unwrap();
let effect = if revoke_key {
format!(
"/bin/cp \"{}\" \"{}\"; exit 0",
revoked_path.display(),
store_path.display()
)
} else {
format!("/bin/rm \"{}\"; exit 0", fixture.record.acp_command)
};
on_stop(&fixture, &effect);
assert_eq!(
fixture.action(Action::Start, None).await.outcome,
Outcome::Running
);
fixture.launched("fixture-model|fixture-model");
let status = fixture.action(Action::Status, None).await;
let result = fixture.action(Action::Restart, Some(status.id)).await;
assert_eq!(result.outcome, Outcome::Failed);
let saved = assert_failed_without_child(&fixture);
if revoke_key {
assert!(saved.private_key_nsec.is_empty());
assert!(!std::fs::read_to_string(store_path)
.unwrap()
.contains(&fixture.record.private_key_nsec));
assert_eq!(
fixture.action(Action::Start, None).await.outcome,
Outcome::Ineligible
);
} else {
assert_eq!(saved.private_key_nsec, fixture.record.private_key_nsec);
}
}
}
#[tokio::test]
async fn post_stop_expiry_persists_truthful_failed_without_second_spawn() {
let _guard = agents::lock_path_mutex();
let fixture = Fixture::new();
// TERM is ignored only by this bounded synthetic process; ordinary Stop
// takes its one-second grace then SIGKILL, crossing the captured deadline.
on_stop(&fixture, "");
assert_eq!(
fixture.action(Action::Start, None).await.outcome,
Outcome::Running
);
fixture.launched("fixture-model|fixture-model");
let mut plan = prepare_for_app(
fixture.app.handle(),
&fixture.record,
Some(&fixture.named.reference()),
&fixture.owner.public_key().to_hex(),
COMMUNITY,
)
.unwrap();
preflight_with(
&mut plan,
&fixture.owner.public_key().to_hex(),
COMMUNITY,
false,
|_, _| async { Ok(()) },
)
.await
.unwrap();
let state = fixture.app.state::<crate::app_state::AppState>();
let _transition = state.managed_agent_runtime_transition.lock().unwrap();
plan.expire_at(Timestamp::now().as_secs() + 1);
let status = agents::start_pair_captured_locked(
fixture.record.pubkey.clone(),
COMMUNITY.into(),
true,
None,
None,
&plan,
false,
true,
None,
fixture.app.handle().clone(),
)
.unwrap();
assert_eq!(
status.lifecycle,
agents::ManagedAgentRuntimeLifecycle::Failed
);
assert!(status.error.unwrap().contains("expired"));
let saved = assert_failed_without_child(&fixture);
assert_eq!(saved.private_key_nsec, fixture.record.private_key_nsec);
}

View File

@@ -242,7 +242,7 @@ pub(crate) fn spawn_key_refusal(record: &ManagedAgentRecord) -> Option<String> {
/// Read the raw unified store — keyed instances AND key-less definitions —
/// with fail-loud parse handling. Internal seam; public readers filter.
fn load_agent_store<R: tauri::Runtime>(
pub(crate) fn load_agent_store<R: tauri::Runtime>(
app: &AppHandle<R>,
) -> Result<Vec<ManagedAgentRecord>, String> {
let path = managed_agents_store_path(app)?;
@@ -276,6 +276,28 @@ pub fn load_managed_agents<R: tauri::Runtime>(
Ok(records)
}
/// Read launch inputs without migrating keys or trusting a warm keyring cache.
/// Missing/unreadable credentials remain unavailable, but do not hide the record
/// from Status/Stop. Inline keys are the existing destination-local file fallback.
pub(crate) fn load_managed_agents_for_launch<R: tauri::Runtime>(
app: &AppHandle<R>,
) -> Result<Vec<ManagedAgentRecord>, String> {
let mut records = load_agent_store(app)?;
records.retain(|record| !record.pubkey.is_empty());
if let Some(store) = agent_secret_store() {
for record in &mut records {
if record.private_key_nsec.is_empty() {
record.private_key_nsec = store
.load_fresh_readonly(&agent_keyring_name(&record.pubkey))
.ok()
.flatten()
.unwrap_or_default();
}
}
}
Ok(records)
}
/// Load the key-less agent *definitions* (former personas) from the unified
/// store. The persona compatibility shim (`load_personas`) presents these in
/// the legacy shape via `to_definition_view`.
@@ -396,6 +418,28 @@ pub fn save_managed_agents<R: tauri::Runtime>(
write_agent_store(app, definitions, sorted)
}
/// Persist only lifecycle bookkeeping from a named launch. Caller holds the
/// store lock. Never feed captured/hydrated keys into the migration save path:
/// the fresh raw store owns inline credentials, and the keyring is not written.
/// Re-read even on failure so a revoked key or removed record stays removed.
pub(crate) fn save_runtime_metadata<R: tauri::Runtime>(
app: &AppHandle<R>,
record: &ManagedAgentRecord,
) -> Result<(), String> {
let mut current = load_agent_store(app)?;
if let Some(saved) = current.iter_mut().find(|r| r.pubkey == record.pubkey) {
saved.runtime_pid = record.runtime_pid;
saved.updated_at = record.updated_at.clone();
saved.last_started_at = record.last_started_at.clone();
saved.last_stopped_at = record.last_stopped_at.clone();
saved.last_exit_code = record.last_exit_code;
saved.last_error = record.last_error.clone();
saved.last_error_code = record.last_error_code;
}
let (definitions, instances) = current.into_iter().partition(|r| r.pubkey.is_empty());
write_agent_store(app, definitions, instances)
}
/// Save the key-less agent *definitions*, preserving the keyed instances —
/// the definition-side mirror of [`save_managed_agents`].
pub(crate) fn save_agent_definitions<R: tauri::Runtime>(

View File

@@ -593,6 +593,25 @@ impl SecretStore {
}
}
/// Read one already-provisioned secret from the backend, bypassing the cache.
/// Launch admission must observe revocation and outages; never migrate or write.
pub(crate) fn load_fresh_readonly(&self, key: &str) -> Result<Option<String>, String> {
#[cfg(feature = "system-keyring")]
{
let Some(raw) = self.read_blob_raw()? else {
return Ok(None);
};
let mut map: HashMap<String, String> =
serde_json::from_slice(&raw).map_err(|_| "Invalid secret store".to_string())?;
Ok(map.remove(key))
}
#[cfg(not(feature = "system-keyring"))]
{
let _ = key;
Err("system-keyring feature disabled".into())
}
}
/// Insert all entries from `entries` into the blob in a single mutation.
///
/// Entries that already exist in the blob are overwritten; entries not

View File

@@ -468,9 +468,12 @@ resume ticket before preflight; probes and Restart never receive that authority.
Shared admission checks the captured runtime generation before destructive Restart,
and a post-Stop Failed status must never become a Running response. No second spawn
path or launch authority is allowed. Missing/edited targets fail rather than
substitute another configuration. Remote
keyless provisioning remains its existing independent fail-closed gate; a known
provisioning failure must exclude Switch before source Stop.
substitute another configuration. Remote Start uses ordinary destination-local
credentials: a matching agent key must already be independently provisioned on the
host. Catalog/preflight and execution recheck that local access; missing, unreadable
or wrong identity excludes launch choices before source Stop. No key transfer,
broker issuer, enrollment or provisioning wizard is part of this flow. Readiness
never hides an existing running process or its ordinary Stop control.
Running identity comes only from the actual live process snapshot, never the next
selection. A different or unknown running configuration cannot satisfy an explicit

View File

@@ -88,9 +88,7 @@ test("mounted picker expires without refresh, reports actual ref, and preserves
return (
probeResult(args.request) ?? {
outcome:
args.request.action === "status"
? "running"
: "provisioning_unavailable",
args.request.action === "status" ? "running" : "ineligible",
observation: {
running_configuration: {
id: "actual-config",
@@ -155,7 +153,7 @@ test("mounted picker expires without refresh, reports actual ref, and preserves
await click("Start on destination");
assert.match(
document.body.textContent,
/keyless launch provisioning is unavailable/,
/exact configuration is missing, changed, or unavailable/,
);
assert.equal(
prepared.find((r) => r.action === "start").desktop,

View File

@@ -59,7 +59,7 @@ function message(outcome: LifecycleOutcome) {
case "running":
return "Desktop confirmed a running local process. This does not prove model readiness.";
case "provisioning_unavailable":
return "Destination keyless launch provisioning is unavailable. No new process was started.";
return "Destination cannot launch this agent. No new process was started.";
case "stopped":
return "Desktop reports the agent stopped.";
case "ineligible":

View File

@@ -60,9 +60,35 @@ sets `BUZZ_ACP_REQUIRED_MODEL` to the exact prepared wire ID after inherited,
harness and mesh writes; non-Claude `BUZZ_ACP_MODEL` is pinned too. Default removes
both strict variables. Claude retains A1 (`ANTHROPIC_MODEL`, no `BUZZ_ACP_MODEL`).
ACP requires matching fresh session/current-model or verified switch evidence
before any prompt, not merely catalog membership or a successful child spawn. Remote lifecycle policy
is independent: this feature does not remove the existing keyless broker gate,
transfer keys, or prove cross-host Move.
before any prompt, not merely catalog membership or a successful child spawn.
## Remote Start credentials
Remote-initiated Start uses the same destination-local credential behavior as
ordinary local Start. The owner must independently provision the agent's matching
identity key on that host. Buzz does not transfer/distribute keys, issue a broker
session, or enroll a host through lifecycle messages. The native shared launcher
may pass that already-local key to its child just as ordinary Start does; lifecycle
requests/results never carry keys, resolved credentials, or local paths.
Catalog and preflight require the exact owner/community/agent/host configuration
ID and revision, matching local identity, executable harness/tools, workspace and
runtime/provider prerequisites. Launch reads bypass a warm keyring cache and never
migrate a missing key into existence. Missing, unreadable or wrong identity means
ineligible, not an eligible inventory host. Catalog eligibility is revalidated
after async preflight. It is advisory and expires, never launch authority.
Execution repeats those checks under the shared admission lock before destructive
Restart and after teardown, so a revoked key cannot be recovered from a captured
plan. Named launch success/failure saves only lifecycle metadata into a fresh raw
store; it never persists captured/hydrated credentials or migrates keys. Ownership
and generation reads likewise do not hydrate or migrate keys. The request deadline
also survives through Stop into shared spawn. Failure after Stop is Failed, not
Running or automatic resurrection. A missing key does
not hide a still-running process or block ordinary owner-authorized Stop. Move
preflights before source Stop and rechecks expiry and exact revision afterward;
same-host switches use these same fences. General host inventory is retained;
selection and running configuration are separate. No new provisioning UX is implied.
## Regression evidence
@@ -70,4 +96,10 @@ transfer keys, or prove cross-host Move.
foreign refs/hosts, plan revalidation and shared child spawn. The child is a shell
fixture, not a real model. `RuntimeConfigurations.test.mjs` mounts the editor with
mock IPC and checks exact revision, next-launch-only writes, failure and unmount
fences. Neither test suite constitutes native/model or two-Desktop acceptance.
fences. `runtime_configurations/tests/remote_credentials.rs` exercises the signed
receiver and shared launch with synthetic identities and bounded child processes,
including actual Stop-time credential/executable loss and post-Stop expiry. Run it
on Unix with `cargo test --manifest-path desktop/src-tauri/Cargo.toml
--no-default-features managed_agents::runtime_configurations::tests::remote_credentials
-- --test-threads=1` (seven tests; default system-keyring builds exclude the fixture).
These fixture suites do not constitute real-model or two-Desktop acceptance.