feat(desktop): introduce destination-local keyless launch boundary

Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-09-04 17:46:29 -04:00
parent b85c53293f
commit 0f2eb99fee
4 changed files with 250 additions and 6 deletions

View File

@@ -0,0 +1,181 @@
//! 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,6 +12,7 @@ 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

@@ -452,10 +452,39 @@ pub fn spawn_agent_child(
owner_hex: Option<&str>,
replay_floor_unix: Option<u64>,
resume: Option<&super::remote_stop::ResumeTicket>,
) -> Result<crate::managed_agents::ManagedAgentProcess, String> {
spawn_agent_child_with_broker(
app,
record,
relay_url,
lazy,
owner_hex,
replay_floor_unix,
resume,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn spawn_agent_child_with_broker(
app: &AppHandle,
record: &ManagedAgentRecord,
relay_url: &str,
lazy: bool,
owner_hex: Option<&str>,
replay_floor_unix: Option<u64>,
resume: Option<&super::remote_stop::ResumeTicket>,
broker: Option<&super::broker_launch::BrokerSession>,
) -> Result<crate::managed_agents::ManagedAgentProcess, String> {
let key = ManagedAgentRuntimeKey::new(record.pubkey.clone(), relay_url)?;
super::remote_stop::check_launch(app, &key, owner_hex, resume)?;
if let Some(error) = spawn_key_refusal(record) {
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) {
return Err(error);
}
let runtime_key = ManagedAgentRuntimeKey::new(record.pubkey.clone(), relay_url)?;
@@ -830,6 +859,16 @@ pub fn spawn_agent_child(
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,
},
)?;
}
let child = spawn_with_effort_proof(&mut command, effort).map_err(|error| {
format!(
"failed to spawn `{}` for agent {}: {error}",

View File

@@ -5,10 +5,10 @@ use tauri::{AppHandle, Emitter, Manager};
use super::{
agent_readiness, current_instance_id, find_managed_agent_mut, load_global_agent_config,
load_managed_agents, load_personas, managed_agent_runtime_log_path, process_is_running,
record_agent_command, resolve_effective_agent_env, save_managed_agents, spawn_agent_child,
terminate_process, terminate_untracked_pair_runtime, write_agent_runtime_receipt,
AgentReadiness, BackendKind, ManagedAgentPairRuntime, ManagedAgentRuntimeKey,
ManagedAgentRuntimeLifecycle, ManagedAgentRuntimeReceipt, ManagedAgentRuntimeStatus,
record_agent_command, resolve_effective_agent_env, save_managed_agents, terminate_process,
terminate_untracked_pair_runtime, write_agent_runtime_receipt, AgentReadiness, BackendKind,
ManagedAgentPairRuntime, ManagedAgentRuntimeKey, ManagedAgentRuntimeLifecycle,
ManagedAgentRuntimeReceipt, ManagedAgentRuntimeStatus,
};
use crate::app_state::AppState;
@@ -261,6 +261,28 @@ fn start_pair(
.managed_agent_runtime_transition
.lock()
.map_err(|e| e.to_string())?;
start_pair_locked(
pubkey,
relay_url,
lazy,
expected_updated_at,
explicit_start,
None,
app.clone(),
)
}
// Caller owns the transition lock across admission, effect, receipt and result.
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> {
let state = app.state::<AppState>();
if state.shutdown_started.load(Ordering::Acquire) {
return Err("desktop shutdown has started".into());
}
@@ -305,7 +327,7 @@ fn start_pair(
} else {
None
};
let mut process = spawn_agent_child(
let mut process = super::spawn_agent_child_with_broker(
&app,
record,
&key.relay_url,
@@ -313,6 +335,7 @@ fn start_pair(
owner.as_deref(),
None,
resume.as_ref(),
broker,
)?;
let now = crate::util::now_iso();
let receipt = ManagedAgentRuntimeReceipt {