mirror of
https://github.com/block/buzz.git
synced 2026-11-01 06:51:17 +00:00
fix(agents): apply live instructions on local restart
Resolve shared-instruction authorization from the current linked definition for both local and remote starts, and stamp the exact spawned list so restart-required clears after revocation. Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -50,7 +50,10 @@ pub(super) fn build_launch_block(
|
||||
effective_model: Option<&str>,
|
||||
owner_pubkey: &str,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let assigned_relay_skills = live_assigned_relay_skills(record, personas)?;
|
||||
let assigned_relay_skills =
|
||||
crate::managed_agents::effective_config::resolve_effective_assigned_relay_skills(
|
||||
record, personas,
|
||||
)?;
|
||||
Ok(serialize_launch_block(
|
||||
record,
|
||||
descriptor,
|
||||
@@ -193,20 +196,6 @@ pub(super) fn ensure_remote_provider_supported(provider: Option<&str>) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn live_assigned_relay_skills<'a>(
|
||||
record: &'a ManagedAgentRecord,
|
||||
personas: &'a [AgentDefinition],
|
||||
) -> Result<&'a [String], String> {
|
||||
match record.persona_id.as_deref() {
|
||||
Some(persona_id) => personas
|
||||
.iter()
|
||||
.find(|persona| persona.id == persona_id)
|
||||
.map(|persona| persona.assigned_relay_skills.as_slice())
|
||||
.ok_or_else(|| format!("linked agent definition {persona_id:?} is missing")),
|
||||
None => Ok(record.assigned_relay_skills.as_slice()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the standard agent JSON payload for provider deploy calls.
|
||||
pub(crate) fn build_deploy_payload(
|
||||
app: &AppHandle,
|
||||
|
||||
@@ -261,6 +261,22 @@ pub fn resolve_effective_config(
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve shared-instruction authorization from the live linked definition.
|
||||
/// Definition-less records retain their own assignments; orphaned links are refused.
|
||||
pub fn resolve_effective_assigned_relay_skills<'a>(
|
||||
record: &'a ManagedAgentRecord,
|
||||
definitions: &'a [AgentDefinition],
|
||||
) -> Result<&'a [String], String> {
|
||||
match record.persona_id.as_deref() {
|
||||
Some(persona_id) => definitions
|
||||
.iter()
|
||||
.find(|definition| definition.id == persona_id)
|
||||
.map(|definition| definition.assigned_relay_skills.as_slice())
|
||||
.ok_or_else(|| ORPHANED_INSTANCE_ERROR.to_string()),
|
||||
None => Ok(record.assigned_relay_skills.as_slice()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_effective_model_provider_pair(
|
||||
record: &ManagedAgentRecord,
|
||||
definitions: &[AgentDefinition],
|
||||
|
||||
@@ -699,6 +699,58 @@ mod tests {
|
||||
assert_eq!(env, Some(coordinate.as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_assigned_relay_skill_env_uses_live_definition() {
|
||||
let stale = format!("30023:{}:revoked", "a".repeat(64));
|
||||
let current = format!("30023:{}:kept", "b".repeat(64));
|
||||
let record: super::super::ManagedAgentRecord = serde_json::from_value(serde_json::json!({
|
||||
"pubkey": "agent",
|
||||
"name": "agent",
|
||||
"persona_id": "persona",
|
||||
"private_key_nsec": "nsec1fake",
|
||||
"relay_url": "wss://relay.example",
|
||||
"acp_command": "buzz-acp",
|
||||
"agent_command": "goose",
|
||||
"agent_args": [],
|
||||
"mcp_command": "",
|
||||
"turn_timeout_seconds": 300,
|
||||
"parallelism": 1,
|
||||
"assigned_relay_skills": [stale.clone()],
|
||||
"created_at": "now",
|
||||
"updated_at": "now"
|
||||
}))
|
||||
.unwrap();
|
||||
let definition: super::super::AgentDefinition = serde_json::from_value(serde_json::json!({
|
||||
"id": "persona",
|
||||
"display_name": "Persona",
|
||||
"system_prompt": "prompt",
|
||||
"name_pool": [],
|
||||
"is_builtin": false,
|
||||
"is_active": true,
|
||||
"assigned_relay_skills": [current.clone()],
|
||||
"created_at": "now",
|
||||
"updated_at": "now"
|
||||
}))
|
||||
.unwrap();
|
||||
let mut command = std::process::Command::new("buzz-acp");
|
||||
|
||||
let definitions = [definition];
|
||||
let assigned =
|
||||
crate::managed_agents::effective_config::resolve_effective_assigned_relay_skills(
|
||||
&record,
|
||||
&definitions,
|
||||
)
|
||||
.unwrap();
|
||||
apply_assigned_relay_skills_env(&mut command, assigned);
|
||||
|
||||
let env = command
|
||||
.get_envs()
|
||||
.find(|(key, _)| *key == ASSIGNED_RELAY_SKILLS_ENV)
|
||||
.and_then(|(_, value)| value)
|
||||
.and_then(|value| value.to_str());
|
||||
assert_eq!(env, Some(current.as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_exact_coordinate_and_deduplicates() {
|
||||
let key = "a".repeat(64);
|
||||
|
||||
@@ -694,10 +694,9 @@ pub fn spawn_agent_child(
|
||||
} else {
|
||||
command.env_remove("BUZZ_ACP_TEAM_INSTRUCTIONS");
|
||||
}
|
||||
super::relay_skills::apply_assigned_relay_skills_env(
|
||||
&mut command,
|
||||
&record.assigned_relay_skills,
|
||||
);
|
||||
let assigned_relay_skills =
|
||||
super::effective_config::resolve_effective_assigned_relay_skills(record, &personas)?;
|
||||
super::relay_skills::apply_assigned_relay_skills_env(&mut command, assigned_relay_skills);
|
||||
|
||||
// Prompt, model, and provider all come from the single `effective_cfg`
|
||||
// resolved at the top of this function — the SAME resolve the spawn-config
|
||||
@@ -864,6 +863,7 @@ pub fn spawn_agent_child(
|
||||
system_prompt: effective_prompt.as_deref(),
|
||||
model: effective_model.as_deref(),
|
||||
provider: effective_provider.as_deref(),
|
||||
assigned_relay_skills,
|
||||
enforced_owner_only: super::owner_only_access_build(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -73,6 +73,8 @@ pub(crate) struct SpawnConfigInputs<'a> {
|
||||
pub system_prompt: Option<&'a str>,
|
||||
pub model: Option<&'a str>,
|
||||
pub provider: Option<&'a str>,
|
||||
/// Exact live authorization list applied to the spawned process.
|
||||
pub assigned_relay_skills: &'a [String],
|
||||
/// Compile-time distribution capability projected at this runtime boundary.
|
||||
/// The stored record remains portable; only effective spawned access is stamped.
|
||||
pub enforced_owner_only: bool,
|
||||
@@ -167,6 +169,7 @@ impl SpawnConfigSnapshot {
|
||||
system_prompt,
|
||||
model,
|
||||
provider,
|
||||
assigned_relay_skills,
|
||||
enforced_owner_only,
|
||||
} = inputs;
|
||||
let (respond_to, respond_to_allowlist) =
|
||||
@@ -195,7 +198,7 @@ impl SpawnConfigSnapshot {
|
||||
system_prompt: system_prompt.map(str::to_string),
|
||||
model: model.map(str::to_string),
|
||||
provider: provider.map(str::to_string),
|
||||
assigned_relay_skills: record.assigned_relay_skills.clone(),
|
||||
assigned_relay_skills: assigned_relay_skills.to_vec(),
|
||||
session_title: (!descriptor.env.contains_key(SESSION_TITLE_ENV_VAR))
|
||||
.then(|| resolve_session_title(record.display_name.as_deref(), &record.name))
|
||||
.flatten(),
|
||||
@@ -301,6 +304,10 @@ pub(crate) fn prospective_spawn_config_snapshot(
|
||||
EffectiveConfigResult::OrphanedInstance { .. } => (None, None, None),
|
||||
};
|
||||
|
||||
let assigned_relay_skills =
|
||||
super::effective_config::resolve_effective_assigned_relay_skills(record, personas)
|
||||
.unwrap_or_default();
|
||||
|
||||
SpawnConfigSnapshot::from_inputs(SpawnConfigInputs {
|
||||
record,
|
||||
descriptor: &descriptor,
|
||||
@@ -311,6 +318,7 @@ pub(crate) fn prospective_spawn_config_snapshot(
|
||||
system_prompt: prompt.as_deref(),
|
||||
model: model.as_deref(),
|
||||
provider: provider.as_deref(),
|
||||
assigned_relay_skills,
|
||||
enforced_owner_only,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use super::*;
|
||||
use crate::managed_agents::types::RespondTo;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
mod assigned_relay_skills;
|
||||
|
||||
/// Canonical projection of a prospective snapshot — the exact value the drift
|
||||
/// comparison reads, so these tests assert on drift itself rather than on a
|
||||
/// proxy for it.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn restart_stamp_uses_live_shared_instructions_and_clears_diff() {
|
||||
let mut rec = record();
|
||||
rec.persona_id = Some("pers".into());
|
||||
rec.assigned_relay_skills = vec![
|
||||
format!("30023:{}:kept", "b".repeat(64)),
|
||||
format!("30023:{}:revoked", "a".repeat(64)),
|
||||
];
|
||||
let mut live = persona("pers", Some("goose"), "prompt");
|
||||
live.assigned_relay_skills = vec![format!("30023:{}:kept", "b".repeat(64))];
|
||||
let personas = [live];
|
||||
let global = GlobalAgentConfig::default();
|
||||
let descriptor =
|
||||
crate::managed_agents::resolve_effective_harness_descriptor(&rec, &personas, &global)
|
||||
.unwrap();
|
||||
let effective = resolve_effective_config(&rec, &personas, &global)
|
||||
.require_resolved()
|
||||
.unwrap();
|
||||
let assigned =
|
||||
crate::managed_agents::effective_config::resolve_effective_assigned_relay_skills(
|
||||
&rec, &personas,
|
||||
)
|
||||
.unwrap();
|
||||
let stamped = SpawnConfigSnapshot::from_inputs(SpawnConfigInputs {
|
||||
record: &rec,
|
||||
descriptor: &descriptor,
|
||||
relay_url: "wss://ws.example",
|
||||
team_instructions: None,
|
||||
system_prompt: effective.system_prompt.value.as_deref(),
|
||||
model: effective.model.value.as_deref(),
|
||||
provider: effective.provider.value.as_deref(),
|
||||
assigned_relay_skills: assigned,
|
||||
enforced_owner_only: false,
|
||||
});
|
||||
let current =
|
||||
prospective_spawn_config_snapshot(&rec, &personas, &[], "wss://ws.example", &global, false);
|
||||
|
||||
assert_eq!(
|
||||
stamped.assigned_relay_skills,
|
||||
personas[0].assigned_relay_skills
|
||||
);
|
||||
assert_eq!(stamped.canonical(), current.canonical());
|
||||
}
|
||||
Reference in New Issue
Block a user