Implement transactional scripted prim builds (#131)
This commit is contained in:
1323
crates/metacrate-grid-agent/src/build.rs
Normal file
1323
crates/metacrate-grid-agent/src/build.rs
Normal file
File diff suppressed because it is too large
Load Diff
363
crates/metacrate-grid-agent/src/build_tests.rs
Normal file
363
crates/metacrate-grid-agent/src/build_tests.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
use crate::build::*;
|
||||
use crate::{
|
||||
ActionOrigin, MemoryPolicyAudit, PolicyAuditSink, PolicyGateway, PolicyLimits,
|
||||
PolicyRequestContext,
|
||||
};
|
||||
use libremetaverse_types::{
|
||||
UUID,
|
||||
compat::{CancellationToken, CancellationTokenSource},
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeGrid {
|
||||
calls: Mutex<Vec<String>>,
|
||||
fail: Mutex<Vec<String>>,
|
||||
deny_land: Mutex<bool>,
|
||||
owned: Mutex<bool>,
|
||||
next: Mutex<u32>,
|
||||
hang_create: Mutex<bool>,
|
||||
}
|
||||
impl FakeGrid {
|
||||
fn calls(&self) -> Vec<String> {
|
||||
self.calls.lock().unwrap().clone()
|
||||
}
|
||||
fn record(&self, value: &str) -> Result<(), BuildError> {
|
||||
self.calls.lock().unwrap().push(value.to_owned());
|
||||
if self
|
||||
.fail
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|failed| failed == value)
|
||||
{
|
||||
Err(BuildError::GridOperation)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
impl BuildGrid for FakeGrid {
|
||||
fn validate_land(&self, _: &str, _: &[[i32; 3]], _: CancellationToken) -> BuildFuture<'_, ()> {
|
||||
Box::pin(async move {
|
||||
self.record("land")?;
|
||||
if *self.deny_land.lock().unwrap() {
|
||||
Err(BuildError::LandDenied)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
fn texture_is_owned(&self, _: UUID, _: CancellationToken) -> BuildFuture<'_, bool> {
|
||||
Box::pin(async move {
|
||||
self.record("texture")?;
|
||||
Ok(*self.owned.lock().unwrap())
|
||||
})
|
||||
}
|
||||
fn create_prim(
|
||||
&self,
|
||||
_: &str,
|
||||
prim: &BuildPrim,
|
||||
_: CancellationToken,
|
||||
) -> BuildFuture<'_, PrimReceipt> {
|
||||
let id = prim.id.clone();
|
||||
Box::pin(async move {
|
||||
self.record(&format!("create:{id}"))?;
|
||||
if *self.hang_create.lock().unwrap() {
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
let mut next = self.next.lock().unwrap();
|
||||
*next += 1;
|
||||
Ok(PrimReceipt {
|
||||
plan_id: id,
|
||||
object_id: UUID::new_with_string(format!("00000000-0000-0000-0000-{:012}", *next))
|
||||
.unwrap(),
|
||||
local_id: *next,
|
||||
})
|
||||
})
|
||||
}
|
||||
fn configure_prim(
|
||||
&self,
|
||||
_: &PrimReceipt,
|
||||
prim: &BuildPrim,
|
||||
_: CancellationToken,
|
||||
) -> BuildFuture<'_, ()> {
|
||||
let id = prim.id.clone();
|
||||
Box::pin(async move { self.record(&format!("configure:{id}")) })
|
||||
}
|
||||
fn link_prims(
|
||||
&self,
|
||||
_: &PrimReceipt,
|
||||
child: &PrimReceipt,
|
||||
_: CancellationToken,
|
||||
) -> BuildFuture<'_, ()> {
|
||||
let id = child.plan_id.clone();
|
||||
Box::pin(async move { self.record(&format!("link:{id}")) })
|
||||
}
|
||||
fn insert_script(
|
||||
&self,
|
||||
receipt: &PrimReceipt,
|
||||
_: &str,
|
||||
_: CancellationToken,
|
||||
) -> BuildFuture<'_, ()> {
|
||||
let id = receipt.plan_id.clone();
|
||||
Box::pin(async move { self.record(&format!("script:{id}")) })
|
||||
}
|
||||
fn confirm_prim(&self, receipt: &PrimReceipt, _: CancellationToken) -> BuildFuture<'_, ()> {
|
||||
let id = receipt.plan_id.clone();
|
||||
Box::pin(async move { self.record(&format!("confirm:{id}")) })
|
||||
}
|
||||
fn delete_owned_prim(
|
||||
&self,
|
||||
receipt: &PrimReceipt,
|
||||
_: CancellationToken,
|
||||
) -> BuildFuture<'_, ()> {
|
||||
let id = receipt.plan_id.clone();
|
||||
Box::pin(async move { self.record(&format!("delete:{id}")) })
|
||||
}
|
||||
}
|
||||
|
||||
fn prim(id: &str, parent: Option<&str>) -> BuildPrim {
|
||||
BuildPrim {
|
||||
id: id.into(),
|
||||
parent: parent.map(str::to_owned),
|
||||
shape: BuildShape::Box,
|
||||
position_millimeters: [1_000, 2_000, 3_000],
|
||||
scale_millimeters: [500, 500, 500],
|
||||
rotation_degrees: [0.0, 0.0, 0.0],
|
||||
color_rgba: [1.0, 0.5, 0.0, 1.0],
|
||||
material: "wood".into(),
|
||||
texture_inventory_id: None,
|
||||
name: id.into(),
|
||||
description: "safe".into(),
|
||||
script: None,
|
||||
}
|
||||
}
|
||||
fn plan() -> BuildPlan {
|
||||
BuildPlan {
|
||||
version: 1,
|
||||
region_id: "region-a".into(),
|
||||
prims: vec![prim("root", None), prim("child", Some("root"))],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_adversarial_and_unbounded_plans() {
|
||||
let grid = Arc::new(FakeGrid::default());
|
||||
let service = BuildService::new(grid, BuildLimits::default()).unwrap();
|
||||
let mut cases = Vec::new();
|
||||
let mut p = plan();
|
||||
p.version = 2;
|
||||
cases.push(p);
|
||||
let mut p = plan();
|
||||
p.prims[1].parent = Some("child".into());
|
||||
cases.push(p);
|
||||
let mut p = plan();
|
||||
p.prims[0].scale_millimeters[0] = 0;
|
||||
cases.push(p);
|
||||
let mut p = plan();
|
||||
p.prims[0].rotation_degrees[0] = f64::NAN;
|
||||
cases.push(p);
|
||||
let mut p = plan();
|
||||
p.prims[0].position_millimeters[0] = 99_000;
|
||||
cases.push(p);
|
||||
let mut p = plan();
|
||||
p.prims[0].material = "money".into();
|
||||
cases.push(p);
|
||||
let mut p = plan();
|
||||
p.prims[0].texture_inventory_id = Some("https://evil.invalid/a".into());
|
||||
cases.push(p);
|
||||
let mut p = plan();
|
||||
p.prims[0].script = Some(
|
||||
"default { state_entry() { llHTTPRequest(\"https://evil.invalid\", [], \"\"); } }".into(),
|
||||
);
|
||||
cases.push(p);
|
||||
assert!(cases.iter().all(|p| service.validate(p).is_err()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dry_run_checks_rights_and_owned_assets_without_mutation() {
|
||||
let grid = Arc::new(FakeGrid::default());
|
||||
*grid.owned.lock().unwrap() = true;
|
||||
let service = BuildService::new(grid.clone(), BuildLimits::default()).unwrap();
|
||||
let mut p = plan();
|
||||
p.prims[0].texture_inventory_id = Some("00000000-0000-0000-0000-000000000099".into());
|
||||
let report = service
|
||||
.dry_run(&p, CancellationToken::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(report.prim_count, 2);
|
||||
assert_eq!(report.link_count, 1);
|
||||
assert_eq!(grid.calls(), vec!["land", "texture"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn land_denial_and_cancellation_stop_before_mutation() {
|
||||
let grid = Arc::new(FakeGrid::default());
|
||||
*grid.deny_land.lock().unwrap() = true;
|
||||
let service = BuildService::new(grid.clone(), BuildLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
service
|
||||
.dry_run(&plan(), CancellationToken::default())
|
||||
.await
|
||||
.unwrap_err(),
|
||||
BuildError::LandDenied
|
||||
);
|
||||
assert!(!grid.calls().iter().any(|call| call.starts_with("create:")));
|
||||
let cancelled = CancellationTokenSource::new();
|
||||
cancelled.cancel();
|
||||
assert_eq!(
|
||||
service
|
||||
.execute("tx-cancel", "call", plan(), cancelled.token())
|
||||
.await
|
||||
.unwrap_err(),
|
||||
BuildError::Cancelled
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn partial_create_reply_times_out_without_guessing_an_object_identity() {
|
||||
let grid = Arc::new(FakeGrid::default());
|
||||
*grid.hang_create.lock().unwrap() = true;
|
||||
let limits = BuildLimits {
|
||||
step_timeout: std::time::Duration::from_millis(1),
|
||||
..BuildLimits::default()
|
||||
};
|
||||
let service = BuildService::new(grid, limits).unwrap();
|
||||
assert_eq!(
|
||||
service
|
||||
.execute("tx-timeout", "call", plan(), CancellationToken::default())
|
||||
.await
|
||||
.unwrap_err(),
|
||||
BuildError::TimedOut
|
||||
);
|
||||
assert!(service.orphan_ids("tx-timeout").is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn representative_build_confirms_configures_scripts_links_and_identity() {
|
||||
let grid = Arc::new(FakeGrid::default());
|
||||
let service = BuildService::new(grid.clone(), BuildLimits::default()).unwrap();
|
||||
let mut p = plan();
|
||||
p.prims[1].script = Some("default { state_entry() { llOwnerSay(\"ready\"); } }".into());
|
||||
let receipt = service
|
||||
.execute("tx-1", "call-1", p, CancellationToken::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(receipt.object_ids.len(), 2);
|
||||
assert_eq!(
|
||||
receipt.root_object_id,
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
);
|
||||
assert_eq!(
|
||||
grid.calls(),
|
||||
vec![
|
||||
"land",
|
||||
"create:root",
|
||||
"confirm:root",
|
||||
"configure:root",
|
||||
"confirm:root",
|
||||
"create:child",
|
||||
"confirm:child",
|
||||
"configure:child",
|
||||
"confirm:child",
|
||||
"script:child",
|
||||
"confirm:child",
|
||||
"link:child",
|
||||
"confirm:child"
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
service.observations().last().unwrap().kind,
|
||||
BuildObservationKind::Completed
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn failure_cleans_only_objects_created_by_transaction_in_reverse_order() {
|
||||
let grid = Arc::new(FakeGrid::default());
|
||||
*grid.fail.lock().unwrap() = vec!["link:child".into()];
|
||||
let service = BuildService::new(grid.clone(), BuildLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
service
|
||||
.execute("tx-2", "call-2", plan(), CancellationToken::default())
|
||||
.await
|
||||
.unwrap_err(),
|
||||
BuildError::GridOperation
|
||||
);
|
||||
let calls = grid.calls();
|
||||
assert_eq!(&calls[calls.len() - 2..], ["delete:child", "delete:root"]);
|
||||
assert!(service.orphan_ids("tx-2").is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cleanup_failure_records_recoverable_orphan_identity() {
|
||||
let grid = Arc::new(FakeGrid::default());
|
||||
*grid.fail.lock().unwrap() = vec!["configure:root".into(), "delete:root".into()];
|
||||
let service = BuildService::new(grid.clone(), BuildLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
service
|
||||
.execute("tx-3", "call-3", plan(), CancellationToken::default())
|
||||
.await
|
||||
.unwrap_err(),
|
||||
BuildError::CleanupIncomplete
|
||||
);
|
||||
assert_eq!(service.orphan_ids("tx-3").len(), 1);
|
||||
assert!(
|
||||
service
|
||||
.observations()
|
||||
.iter()
|
||||
.any(|o| o.kind == BuildObservationKind::Orphaned)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_requires_bound_operator_approval_only_above_small_limit() {
|
||||
let tools = build_policy_tools(BuildLimits::default()).unwrap();
|
||||
assert_eq!(tools.len(), 2);
|
||||
assert_eq!(tools[0].definition.name.as_str(), BUILD_DRY_RUN_TOOL);
|
||||
assert!(!tools[0].definition.mutating);
|
||||
assert_eq!(tools[1].definition.name.as_str(), BUILD_EXECUTE_TOOL);
|
||||
assert!(tools[1].definition.mutating);
|
||||
assert!(matches!(
|
||||
tools[1].approval,
|
||||
crate::ApprovalRule::WhenExceeds(_)
|
||||
));
|
||||
let service = BuildService::new(Arc::new(FakeGrid::default()), BuildLimits::default()).unwrap();
|
||||
assert!(!service.validate(&plan()).unwrap().approval_required);
|
||||
let mut larger = plan();
|
||||
for index in 2..5 {
|
||||
larger
|
||||
.prims
|
||||
.push(prim(&format!("child-{index}"), Some("root")));
|
||||
}
|
||||
assert!(service.validate(&larger).unwrap().approval_required);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tools_are_invisible_to_public_and_unauthorized_origins() {
|
||||
let authorized = UUID::new_with_string("11111111-1111-4111-8111-111111111111".into()).unwrap();
|
||||
let stranger = UUID::new_with_string("22222222-2222-4222-8222-222222222222".into()).unwrap();
|
||||
let audit = Arc::new(MemoryPolicyAudit::new(32).unwrap());
|
||||
let sink: Arc<dyn PolicyAuditSink> = audit;
|
||||
let gateway = PolicyGateway::new(
|
||||
std::collections::BTreeSet::from([authorized]),
|
||||
build_policy_tools(BuildLimits::default()).unwrap(),
|
||||
PolicyLimits::default(),
|
||||
sink,
|
||||
)
|
||||
.unwrap();
|
||||
let public =
|
||||
PolicyRequestContext::new(ActionOrigin::public_chat(authorized), "public", "call").unwrap();
|
||||
let unauthorized =
|
||||
PolicyRequestContext::new(ActionOrigin::instant_message(stranger), "im", "call").unwrap();
|
||||
let authorized_im = PolicyRequestContext::new(
|
||||
ActionOrigin::instant_message(authorized),
|
||||
"authorized",
|
||||
"call",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(gateway.tools_for(&public, 1).is_empty());
|
||||
assert!(gateway.tools_for(&unauthorized, 1).is_empty());
|
||||
assert_eq!(gateway.tools_for(&authorized_im, 1).len(), 2);
|
||||
}
|
||||
@@ -199,6 +199,9 @@ pub struct RuntimeView {
|
||||
pub control_queue_capacity: usize,
|
||||
pub budget_tool_calls_used: u64,
|
||||
pub budget_movement_millimeters_used: u64,
|
||||
pub active_build_transaction: Option<String>,
|
||||
pub build_progress: Option<String>,
|
||||
pub build_orphan_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -53,6 +53,9 @@ impl ControlTarget for FakeTarget {
|
||||
control_queue_capacity: 64,
|
||||
budget_tool_calls_used: 3,
|
||||
budget_movement_millimeters_used: 2_000,
|
||||
active_build_transaction: None,
|
||||
build_progress: None,
|
||||
build_orphan_ids: Vec::new(),
|
||||
})),
|
||||
ControlRequest::ListSessions { page } => Ok(ControlPayload::Sessions(page_values(
|
||||
&page,
|
||||
|
||||
@@ -84,6 +84,7 @@ pub struct AgentControlTarget {
|
||||
policy: Arc<PolicyGateway>,
|
||||
audit: Arc<MemoryPolicyAudit>,
|
||||
observability: Mutex<Option<Arc<Observability>>>,
|
||||
build: Mutex<Option<Arc<dyn crate::build::BuildControl>>>,
|
||||
behavior: BehaviorIngress,
|
||||
commands: mpsc::Sender<RuntimeControlCommand>,
|
||||
command_capacity: usize,
|
||||
@@ -127,6 +128,7 @@ impl AgentControlTarget {
|
||||
policy,
|
||||
audit,
|
||||
observability: Mutex::new(None),
|
||||
build: Mutex::new(None),
|
||||
behavior,
|
||||
commands,
|
||||
command_capacity,
|
||||
@@ -142,6 +144,11 @@ impl AgentControlTarget {
|
||||
*lock(&self.observability) = Some(observability);
|
||||
}
|
||||
|
||||
/// Attaches transactional build cancellation to the operator action API.
|
||||
pub fn attach_build_control(&self, build: Arc<dyn crate::build::BuildControl>) {
|
||||
*lock(&self.build) = Some(build);
|
||||
}
|
||||
|
||||
pub fn update_session(&self, session: SessionStatus) {
|
||||
let mut state = lock(&self.state);
|
||||
state.session = session;
|
||||
@@ -237,6 +244,7 @@ impl AgentControlTarget {
|
||||
ControlRequest::Runtime => {
|
||||
let state = lock(&self.state).clone();
|
||||
let usage = self.policy.global_budget_usage();
|
||||
let build = lock(&self.build);
|
||||
Ok(ControlPayload::Runtime(RuntimeView {
|
||||
grid_state: state.session.state.as_str().to_owned(),
|
||||
generation: state.session.generation,
|
||||
@@ -252,6 +260,11 @@ impl AgentControlTarget {
|
||||
control_queue_capacity: self.command_capacity,
|
||||
budget_tool_calls_used: usage.tool_calls,
|
||||
budget_movement_millimeters_used: usage.movement_millimeters,
|
||||
active_build_transaction: build.as_ref().and_then(|build| build.active_build()),
|
||||
build_progress: build.as_ref().and_then(|build| build.build_progress()),
|
||||
build_orphan_ids: build
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |build| build.build_orphans()),
|
||||
}))
|
||||
}
|
||||
ControlRequest::ListSessions { page } => {
|
||||
@@ -340,10 +353,13 @@ impl AgentControlTarget {
|
||||
Ok(response)
|
||||
}
|
||||
ControlRequest::CancelAction { action_id } => {
|
||||
let build_cancelled = lock(&self.build)
|
||||
.as_ref()
|
||||
.is_some_and(|build| build.cancel_build(&action_id));
|
||||
let cancelled = self.behavior.cancel_action(&action_id).map_err(|_| {
|
||||
control_error(ControlErrorCode::InvalidRequest, "invalid action ID", false)
|
||||
})?;
|
||||
if !cancelled {
|
||||
if !cancelled && !build_cancelled {
|
||||
return Err(control_error(
|
||||
ControlErrorCode::NotFound,
|
||||
"active behavior action not found",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
pub mod backend;
|
||||
pub mod behavior;
|
||||
pub mod build;
|
||||
pub mod config;
|
||||
pub mod control_plane;
|
||||
pub mod control_runtime;
|
||||
@@ -26,6 +27,8 @@ pub mod types;
|
||||
#[cfg(test)]
|
||||
mod behavior_tests;
|
||||
#[cfg(test)]
|
||||
mod build_tests;
|
||||
#[cfg(test)]
|
||||
mod control_plane_tests;
|
||||
#[cfg(test)]
|
||||
mod control_runtime_tests;
|
||||
@@ -64,6 +67,13 @@ pub use behavior::{
|
||||
EmbodimentSink, FACE_AVATAR_TOOL, FACE_POINT_TOOL, LOOK_AROUND_TOOL, SIT_TOOL, STAND_TOOL,
|
||||
STOP_TOOL, WALK_SHORT_TOOL, behavior_policy_tools,
|
||||
};
|
||||
#[cfg(feature = "live-grid")]
|
||||
pub use build::LibremetaverseBuildGrid;
|
||||
pub use build::{
|
||||
BUILD_DRY_RUN_TOOL, BUILD_EXECUTE_TOOL, BuildControl, BuildError, BuildFuture, BuildGrid,
|
||||
BuildLimits, BuildObservation, BuildObservationKind, BuildPlan, BuildPrim, BuildReceipt,
|
||||
BuildService, BuildShape, BuildToolBackend, BuildValidation, PrimReceipt, build_policy_tools,
|
||||
};
|
||||
pub use config::{
|
||||
AgentConfig, BehaviorSettings, ConfigError, ConfigLoader, ControlSettings,
|
||||
ConversationSettings, EndpointUrl, Environment, GridConnection, Limits, LlmConnection,
|
||||
|
||||
@@ -291,6 +291,7 @@ async fn run_live(
|
||||
config.limits.control_queue,
|
||||
)?;
|
||||
control_target.attach_observability(live.observability.clone());
|
||||
control_target.attach_build_control(live.build_control.clone());
|
||||
control_target.update_session(handle.status());
|
||||
let erased_target: Arc<dyn ControlTarget> = control_target.clone();
|
||||
let (control_plane, integrated_client, control_server) = match config.mode {
|
||||
@@ -490,6 +491,7 @@ struct LiveInteractions {
|
||||
observability: Arc<metacrate_grid_agent::Observability>,
|
||||
_landmark_intake: metacrate_grid_agent::LibremetaverseLandmarkIntake,
|
||||
landmark_roaming: metacrate_grid_agent::LandmarkRoamingHandle,
|
||||
build_control: Arc<dyn metacrate_grid_agent::BuildControl>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "live-grid")]
|
||||
@@ -500,13 +502,14 @@ fn start_live_interactions(
|
||||
) -> Result<LiveInteractions, Box<dyn Error>> {
|
||||
use metacrate_grid_agent::{
|
||||
AuthorizedBackendRouter, AuthorizedToolBackend, BehaviorBackend, BehaviorController,
|
||||
ConversationStore, InteractionCoordinator, LandmarkLimits, LandmarkService,
|
||||
LandmarkToolBackend, LibremetaverseLandmarkGrid, LibremetaverseLandmarkIntake,
|
||||
LibremetaverseScriptInventory, LlmClient, LlmTransportLimits, MemoryPolicyAudit,
|
||||
Observability, ObservabilityLimits, PerceptionBackend, PolicyAuditSink, PolicyGateway,
|
||||
PolicyLimits, PolicyLlmResponder, ScriptDeliveryBackend, ScriptDeliverySettings,
|
||||
SystemRoamingRandom, ToolLoopLimits, UnifiedPolicyAudit, behavior_policy_tools,
|
||||
landmark_policy_tools, perception_policy_tools, script_delivery_policy_tool,
|
||||
BuildLimits, BuildService, BuildToolBackend, ConversationStore, InteractionCoordinator,
|
||||
LandmarkLimits, LandmarkService, LandmarkToolBackend, LibremetaverseBuildGrid,
|
||||
LibremetaverseLandmarkGrid, LibremetaverseLandmarkIntake, LibremetaverseScriptInventory,
|
||||
LlmClient, LlmTransportLimits, MemoryPolicyAudit, Observability, ObservabilityLimits,
|
||||
PerceptionBackend, PolicyAuditSink, PolicyGateway, PolicyLimits, PolicyLlmResponder,
|
||||
ScriptDeliveryBackend, ScriptDeliverySettings, SystemRoamingRandom, ToolLoopLimits,
|
||||
UnifiedPolicyAudit, behavior_policy_tools, build_policy_tools, landmark_policy_tools,
|
||||
perception_policy_tools, script_delivery_policy_tool,
|
||||
};
|
||||
|
||||
let transport_limits = LlmTransportLimits {
|
||||
@@ -558,6 +561,14 @@ fn start_live_interactions(
|
||||
tools.push(script_delivery_policy_tool(
|
||||
ScriptDeliverySettings::default(),
|
||||
)?);
|
||||
let build_service = Arc::new(BuildService::new(
|
||||
Arc::new(LibremetaverseBuildGrid::new(owner)),
|
||||
BuildLimits::default(),
|
||||
)?);
|
||||
let build_control: Arc<dyn metacrate_grid_agent::BuildControl> = build_service.clone();
|
||||
let build_backend: Arc<dyn AuthorizedToolBackend> =
|
||||
Arc::new(BuildToolBackend::new(build_service));
|
||||
tools.extend(build_policy_tools(BuildLimits::default())?);
|
||||
let landmark_service = Arc::new(LandmarkService::new(
|
||||
Arc::new(LibremetaverseLandmarkGrid::new(owner)),
|
||||
Arc::new(SystemRoamingRandom::default()),
|
||||
@@ -582,8 +593,9 @@ fn start_live_interactions(
|
||||
.iter()
|
||||
.map(|tool| tool.definition.name.as_str().to_owned())
|
||||
.map(|name| {
|
||||
let backend: Arc<dyn AuthorizedToolBackend> =
|
||||
if name == metacrate_grid_agent::SCRIPT_DELIVERY_TOOL {
|
||||
let backend: Arc<dyn AuthorizedToolBackend> = if name.starts_with("build_object_") {
|
||||
Arc::clone(&build_backend)
|
||||
} else if name == metacrate_grid_agent::SCRIPT_DELIVERY_TOOL {
|
||||
Arc::clone(&script_backend)
|
||||
} else if name.starts_with("landmark_") {
|
||||
Arc::clone(&landmark_backend)
|
||||
@@ -649,6 +661,7 @@ fn start_live_interactions(
|
||||
observability,
|
||||
_landmark_intake: landmark_intake,
|
||||
landmark_roaming,
|
||||
build_control,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -709,6 +709,14 @@ fn render_overview(s: &OperatorSnapshot, out: &mut Vec<String>) {
|
||||
v.position,
|
||||
v.behavior_mode
|
||||
));
|
||||
if v.active_build_transaction.is_some() || !v.build_orphan_ids.is_empty() {
|
||||
out.push(format!(
|
||||
"build={} progress={} recoverable-orphans={}",
|
||||
v.active_build_transaction.as_deref().unwrap_or("idle"),
|
||||
v.build_progress.as_deref().unwrap_or("none"),
|
||||
v.build_orphan_ids.len()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn render_queues(s: &OperatorSnapshot, out: &mut Vec<String>) {
|
||||
|
||||
@@ -43,6 +43,9 @@ impl TuiTransport for FakeTransport {
|
||||
control_queue_capacity: 8,
|
||||
budget_tool_calls_used: 2,
|
||||
budget_movement_millimeters_used: 3,
|
||||
active_build_transaction: Some("build-7".into()),
|
||||
build_progress: Some("configured".into()),
|
||||
build_orphan_ids: Vec::new(),
|
||||
}),
|
||||
ControlRequest::ListSessions { .. } => ControlPayload::Sessions(crate::Page {
|
||||
items: Vec::new(),
|
||||
|
||||
@@ -48,10 +48,10 @@ fn package_has_only_reviewed_rust_dependencies_and_no_build_script() {
|
||||
#[test]
|
||||
fn runtime_source_has_no_subprocess_or_native_abi_escape_hatch() {
|
||||
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut files = Vec::with_capacity(32);
|
||||
let mut files = Vec::with_capacity(34);
|
||||
collect_rust_files(&source, &mut files);
|
||||
assert!(
|
||||
files.len() <= 32,
|
||||
files.len() <= 34,
|
||||
"source-file count needs a reviewed bound update"
|
||||
);
|
||||
for path in files {
|
||||
|
||||
@@ -11,5 +11,9 @@ description = "Native MetaCrate APIs outside the LibreMetaverse compatibility su
|
||||
metacrate-grid-agent = { version = "0.0.1", path = "../metacrate-grid-agent" }
|
||||
metacrate-lsl-tools = { version = "0.0.1", path = "../metacrate-lsl-tools" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
live-grid = ["metacrate-grid-agent/live-grid"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
Reference in New Issue
Block a user