pod: simplify pod tool surface
This commit is contained in:
parent
32ac0eee35
commit
5472ceca48
|
|
@ -8,9 +8,7 @@ use pod_store::PodMetadataStore;
|
||||||
use session_store::Store;
|
use session_store::Store;
|
||||||
use tokio::sync::{broadcast, mpsc, oneshot};
|
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||||
|
|
||||||
use crate::discovery::{
|
use crate::discovery::{PodDiscovery, list_pods_tool, restore_pod_tool};
|
||||||
PodDiscovery, attach_or_restore_pod_tool, inspect_pod_tool, list_visible_pods_tool,
|
|
||||||
};
|
|
||||||
use crate::ipc::alerter::Alerter;
|
use crate::ipc::alerter::Alerter;
|
||||||
use crate::ipc::notify_buffer::NotifyBuffer;
|
use crate::ipc::notify_buffer::NotifyBuffer;
|
||||||
use crate::ipc::server::SocketServer;
|
use crate::ipc::server::SocketServer;
|
||||||
|
|
@ -18,9 +16,7 @@ use crate::pod::{Pod, PodError, PodRunResult, SystemItemCommitter};
|
||||||
use crate::runtime::dir::RuntimeDir;
|
use crate::runtime::dir::RuntimeDir;
|
||||||
use crate::segment_log_sink::SegmentLogSink;
|
use crate::segment_log_sink::SegmentLogSink;
|
||||||
use crate::shared_state::PodSharedState;
|
use crate::shared_state::PodSharedState;
|
||||||
use crate::spawn::comm_tools::{
|
use crate::spawn::comm_tools::{read_pod_output_tool, send_to_pod_tool, stop_pod_tool};
|
||||||
list_pods_tool, read_pod_output_tool, send_to_pod_tool, stop_pod_tool,
|
|
||||||
};
|
|
||||||
use crate::spawn::registry::SpawnedPodRegistry;
|
use crate::spawn::registry::SpawnedPodRegistry;
|
||||||
use crate::spawn::tool::spawn_pod_tool;
|
use crate::spawn::tool::spawn_pod_tool;
|
||||||
use protocol::{
|
use protocol::{
|
||||||
|
|
@ -563,12 +559,9 @@ where
|
||||||
worker.register_tool(send_to_pod_tool(spawned_registry.clone()));
|
worker.register_tool(send_to_pod_tool(spawned_registry.clone()));
|
||||||
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
|
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
|
||||||
worker.register_tool(stop_pod_tool(spawned_registry.clone()));
|
worker.register_tool(stop_pod_tool(spawned_registry.clone()));
|
||||||
worker.register_tool(list_pods_tool(spawned_registry.clone()));
|
|
||||||
|
|
||||||
let discovery = PodDiscovery::new(pod_store, spawner_name, runtime_base, pwd, spawned_registry);
|
let discovery = PodDiscovery::new(pod_store, spawner_name, runtime_base, pwd, spawned_registry);
|
||||||
worker.register_tool(list_visible_pods_tool(discovery.clone()));
|
worker.register_tool(list_pods_tool(discovery.clone()));
|
||||||
worker.register_tool(inspect_pod_tool(discovery.clone()));
|
worker.register_tool(restore_pod_tool(discovery));
|
||||||
worker.register_tool(attach_or_restore_pod_tool(discovery));
|
|
||||||
pod.attach_tracker(tracker);
|
pod.attach_tracker(tracker);
|
||||||
fs_for_view
|
fs_for_view
|
||||||
}
|
}
|
||||||
|
|
@ -835,10 +828,10 @@ async fn controller_loop<C, St>(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Method::ListVisiblePods => match discovery.list_visible().await {
|
Method::ListPods => match discovery.list_visible().await {
|
||||||
Ok(pods) => match serde_json::to_value(pods) {
|
Ok(pods) => match serde_json::to_value(pods) {
|
||||||
Ok(pods) => {
|
Ok(pods) => {
|
||||||
let _ = event_tx.send(Event::VisiblePods { pods });
|
let _ = event_tx.send(Event::PodsListed { pods });
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let _ = event_tx.send(Event::Error {
|
let _ = event_tx.send(Event::Error {
|
||||||
|
|
@ -855,35 +848,15 @@ async fn controller_loop<C, St>(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Method::InspectPod { name } => match discovery.inspect(&name).await {
|
Method::RestorePod { name } => match discovery.restore(&name).await {
|
||||||
Ok(pod) => match serde_json::to_value(pod) {
|
|
||||||
Ok(pod) => {
|
|
||||||
let _ = event_tx.send(Event::PodInspection { pod });
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
let _ = event_tx.send(Event::Error {
|
|
||||||
code: ErrorCode::Internal,
|
|
||||||
message: format!("serialize pod inspection: {error}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(error) => {
|
|
||||||
let _ = event_tx.send(Event::Error {
|
|
||||||
code: ErrorCode::InvalidRequest,
|
|
||||||
message: error.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
Method::AttachOrRestorePod { name } => match discovery.attach_or_restore(&name).await {
|
|
||||||
Ok(result) => match serde_json::to_value(result) {
|
Ok(result) => match serde_json::to_value(result) {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
let _ = event_tx.send(Event::PodAttachRestore { result });
|
let _ = event_tx.send(Event::PodRestored { result });
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let _ = event_tx.send(Event::Error {
|
let _ = event_tx.send(Event::Error {
|
||||||
code: ErrorCode::Internal,
|
code: ErrorCode::Internal,
|
||||||
message: format!("serialize pod attach/restore result: {error}"),
|
message: format!("serialize pod restore result: {error}"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1096,11 +1069,7 @@ where
|
||||||
notify_buffer.push_notify(message);
|
notify_buffer.push_notify(message);
|
||||||
}
|
}
|
||||||
Some(Method::ListCompletions { .. }) => {}
|
Some(Method::ListCompletions { .. }) => {}
|
||||||
Some(
|
Some(Method::ListPods | Method::RestorePod { .. }) => {
|
||||||
Method::ListVisiblePods
|
|
||||||
| Method::InspectPod { .. }
|
|
||||||
| Method::AttachOrRestorePod { .. },
|
|
||||||
) => {
|
|
||||||
let _ = event_tx.send(Event::Error {
|
let _ = event_tx.send(Event::Error {
|
||||||
code: ErrorCode::AlreadyRunning,
|
code: ErrorCode::AlreadyRunning,
|
||||||
message: "Pod discovery requests are only handled while the Pod is idle or paused"
|
message: "Pod discovery requests are only handled while the Pod is idle or paused"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! Pod-state-backed discovery and restore/attach tools.
|
//! Pod-state-backed discovery and restore tools.
|
||||||
//!
|
//!
|
||||||
//! This surface deliberately does not enumerate every Pod on the host. The
|
//! This surface deliberately does not enumerate every Pod on the host. The
|
||||||
//! listing path starts from the caller's visibility set (the caller itself and
|
//! listing path starts from the caller's visibility set (the caller itself and
|
||||||
|
|
@ -15,6 +15,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||||
|
use manifest::{Permission, ScopeRule};
|
||||||
use pod_store::{PodActiveSegmentRef, PodMetadata, PodMetadataStore};
|
use pod_store::{PodActiveSegmentRef, PodMetadata, PodMetadataStore};
|
||||||
use protocol::stream::JsonLineReader;
|
use protocol::stream::JsonLineReader;
|
||||||
use protocol::{Event, PodStatus};
|
use protocol::{Event, PodStatus};
|
||||||
|
|
@ -24,6 +25,7 @@ use session_store::{SegmentId, SessionId};
|
||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
use crate::runtime::dir::SpawnedPodRecord;
|
||||||
use crate::runtime::pod_registry;
|
use crate::runtime::pod_registry;
|
||||||
use crate::spawn::registry::SpawnedPodRegistry;
|
use crate::spawn::registry::SpawnedPodRegistry;
|
||||||
|
|
||||||
|
|
@ -97,26 +99,23 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn attach_or_restore(
|
pub async fn restore(&self, pod_name: &str) -> Result<RestoreResult, PodDiscoveryError> {
|
||||||
&self,
|
match self.plan_restore(pod_name).await? {
|
||||||
pod_name: &str,
|
RestorePlan::AlreadyLive {
|
||||||
) -> Result<AttachRestoreResult, PodDiscoveryError> {
|
|
||||||
match self.plan_attach_or_restore(pod_name).await? {
|
|
||||||
AttachRestorePlan::Attach {
|
|
||||||
pod_name,
|
pod_name,
|
||||||
socket_path,
|
socket_path,
|
||||||
status,
|
status,
|
||||||
} => Ok(AttachRestoreResult::Attached {
|
} => Ok(RestoreResult::AlreadyLive {
|
||||||
pod_name,
|
pod_name,
|
||||||
socket_path,
|
socket_path,
|
||||||
status,
|
status,
|
||||||
}),
|
}),
|
||||||
AttachRestorePlan::Restore {
|
RestorePlan::Restore {
|
||||||
pod_name,
|
pod_name,
|
||||||
socket_path,
|
socket_path,
|
||||||
} => {
|
} => {
|
||||||
self.spawn_restore_process(&pod_name, &socket_path).await?;
|
self.spawn_restore_process(&pod_name, &socket_path).await?;
|
||||||
Ok(AttachRestoreResult::Restored {
|
Ok(RestoreResult::Restored {
|
||||||
pod_name,
|
pod_name,
|
||||||
socket_path,
|
socket_path,
|
||||||
})
|
})
|
||||||
|
|
@ -124,13 +123,10 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn plan_attach_or_restore(
|
pub async fn plan_restore(&self, pod_name: &str) -> Result<RestorePlan, PodDiscoveryError> {
|
||||||
&self,
|
|
||||||
pod_name: &str,
|
|
||||||
) -> Result<AttachRestorePlan, PodDiscoveryError> {
|
|
||||||
let detail = self.inspect(pod_name).await?;
|
let detail = self.inspect(pod_name).await?;
|
||||||
if detail.live.reachable {
|
if detail.live.reachable {
|
||||||
return Ok(AttachRestorePlan::Attach {
|
return Ok(RestorePlan::AlreadyLive {
|
||||||
pod_name: pod_name.to_string(),
|
pod_name: pod_name.to_string(),
|
||||||
socket_path: detail.live.socket_path,
|
socket_path: detail.live.socket_path,
|
||||||
status: detail.live.status,
|
status: detail.live.status,
|
||||||
|
|
@ -153,7 +149,7 @@ where
|
||||||
if let Some(lock) = lookup_segment_lock(segment_id)? {
|
if let Some(lock) = lookup_segment_lock(segment_id)? {
|
||||||
let lock_live = probe_socket(&lock.socket).await;
|
let lock_live = probe_socket(&lock.socket).await;
|
||||||
return if lock_live.reachable {
|
return if lock_live.reachable {
|
||||||
Ok(AttachRestorePlan::Attach {
|
Ok(RestorePlan::AlreadyLive {
|
||||||
pod_name: lock.pod_name,
|
pod_name: lock.pod_name,
|
||||||
socket_path: lock.socket,
|
socket_path: lock.socket,
|
||||||
status: lock_live.status,
|
status: lock_live.status,
|
||||||
|
|
@ -169,7 +165,7 @@ where
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(AttachRestorePlan::Restore {
|
Ok(RestorePlan::Restore {
|
||||||
pod_name: pod_name.to_string(),
|
pod_name: pod_name.to_string(),
|
||||||
socket_path: self.default_socket_path(pod_name),
|
socket_path: self.default_socket_path(pod_name),
|
||||||
})
|
})
|
||||||
|
|
@ -178,6 +174,7 @@ where
|
||||||
async fn visibility(&self) -> Result<VisibilitySet, PodDiscoveryError> {
|
async fn visibility(&self) -> Result<VisibilitySet, PodDiscoveryError> {
|
||||||
let mut visible = BTreeMap::new();
|
let mut visible = BTreeMap::new();
|
||||||
let mut child_sockets = BTreeMap::new();
|
let mut child_sockets = BTreeMap::new();
|
||||||
|
let mut comm_registry = BTreeMap::new();
|
||||||
visible.insert(self.self_pod_name.clone(), VisibilityReason::SelfPod);
|
visible.insert(self.self_pod_name.clone(), VisibilityReason::SelfPod);
|
||||||
|
|
||||||
// Durable parent -> child state is the primary visibility source.
|
// Durable parent -> child state is the primary visibility source.
|
||||||
|
|
@ -186,7 +183,8 @@ where
|
||||||
visible
|
visible
|
||||||
.entry(child.pod_name.clone())
|
.entry(child.pod_name.clone())
|
||||||
.or_insert(VisibilityReason::SpawnedChild);
|
.or_insert(VisibilityReason::SpawnedChild);
|
||||||
child_sockets.insert(child.pod_name, child.socket_path);
|
child_sockets.insert(child.pod_name.clone(), child.socket_path.clone());
|
||||||
|
comm_registry.insert(child.pod_name.clone(), comm_info_from_spawned_child(&child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,12 +195,17 @@ where
|
||||||
visible
|
visible
|
||||||
.entry(record.pod_name.clone())
|
.entry(record.pod_name.clone())
|
||||||
.or_insert(VisibilityReason::SpawnedChild);
|
.or_insert(VisibilityReason::SpawnedChild);
|
||||||
child_sockets.insert(record.pod_name, record.socket_path);
|
child_sockets.insert(record.pod_name.clone(), record.socket_path.clone());
|
||||||
|
comm_registry.insert(
|
||||||
|
record.pod_name.clone(),
|
||||||
|
CommRegistryInfo::from_record(&record),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(VisibilitySet {
|
Ok(VisibilitySet {
|
||||||
visible,
|
visible,
|
||||||
child_sockets,
|
child_sockets,
|
||||||
|
comm_registry,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,6 +225,7 @@ where
|
||||||
active: detail.active,
|
active: detail.active,
|
||||||
live: detail.live,
|
live: detail.live,
|
||||||
restore: detail.restore,
|
restore: detail.restore,
|
||||||
|
comm_registry: detail.comm_registry,
|
||||||
spawned_children: detail.spawned_children,
|
spawned_children: detail.spawned_children,
|
||||||
error: None,
|
error: None,
|
||||||
}
|
}
|
||||||
|
|
@ -233,6 +237,7 @@ where
|
||||||
active: None,
|
active: None,
|
||||||
live: self.live_for_name(pod_name, None).await,
|
live: self.live_for_name(pod_name, None).await,
|
||||||
restore: RestoreInfo::not_possible("pod state missing"),
|
restore: RestoreInfo::not_possible("pod state missing"),
|
||||||
|
comm_registry: visibility.comm_info_for(pod_name),
|
||||||
spawned_children: SpawnedChildrenSummary::default(),
|
spawned_children: SpawnedChildrenSummary::default(),
|
||||||
error: None,
|
error: None,
|
||||||
},
|
},
|
||||||
|
|
@ -243,6 +248,7 @@ where
|
||||||
active: None,
|
active: None,
|
||||||
live: self.live_for_name(pod_name, None).await,
|
live: self.live_for_name(pod_name, None).await,
|
||||||
restore: RestoreInfo::not_possible("pod state is unreadable"),
|
restore: RestoreInfo::not_possible("pod state is unreadable"),
|
||||||
|
comm_registry: visibility.comm_info_for(pod_name),
|
||||||
spawned_children: SpawnedChildrenSummary::default(),
|
spawned_children: SpawnedChildrenSummary::default(),
|
||||||
error: Some(error.to_string()),
|
error: Some(error.to_string()),
|
||||||
},
|
},
|
||||||
|
|
@ -268,6 +274,7 @@ where
|
||||||
active: metadata.active.map(ActivePointer::from),
|
active: metadata.active.map(ActivePointer::from),
|
||||||
live,
|
live,
|
||||||
restore,
|
restore,
|
||||||
|
comm_registry: visibility.comm_info_for(&metadata.pod_name),
|
||||||
spawned_children,
|
spawned_children,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -424,6 +431,33 @@ pub struct SpawnedChildrenSummary {
|
||||||
pub unreachable: usize,
|
pub unreachable: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct CommRegistryInfo {
|
||||||
|
pub registered: bool,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub socket_path: Option<PathBuf>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub scope_delegated: Vec<ScopeRule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommRegistryInfo {
|
||||||
|
fn missing() -> Self {
|
||||||
|
Self {
|
||||||
|
registered: false,
|
||||||
|
socket_path: None,
|
||||||
|
scope_delegated: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_record(record: &SpawnedPodRecord) -> Self {
|
||||||
|
Self {
|
||||||
|
registered: true,
|
||||||
|
socket_path: Some(record.socket_path.clone()),
|
||||||
|
scope_delegated: record.scope_delegated.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct VisiblePodItem {
|
pub struct VisiblePodItem {
|
||||||
pub pod_name: String,
|
pub pod_name: String,
|
||||||
|
|
@ -433,6 +467,7 @@ pub struct VisiblePodItem {
|
||||||
pub active: Option<ActivePointer>,
|
pub active: Option<ActivePointer>,
|
||||||
pub live: LiveInfo,
|
pub live: LiveInfo,
|
||||||
pub restore: RestoreInfo,
|
pub restore: RestoreInfo,
|
||||||
|
pub comm_registry: CommRegistryInfo,
|
||||||
pub spawned_children: SpawnedChildrenSummary,
|
pub spawned_children: SpawnedChildrenSummary,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
|
|
@ -446,13 +481,14 @@ pub struct PodDetail {
|
||||||
pub active: Option<ActivePointer>,
|
pub active: Option<ActivePointer>,
|
||||||
pub live: LiveInfo,
|
pub live: LiveInfo,
|
||||||
pub restore: RestoreInfo,
|
pub restore: RestoreInfo,
|
||||||
|
pub comm_registry: CommRegistryInfo,
|
||||||
pub spawned_children: SpawnedChildrenSummary,
|
pub spawned_children: SpawnedChildrenSummary,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(tag = "action", rename_all = "snake_case")]
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
pub enum AttachRestorePlan {
|
pub enum RestorePlan {
|
||||||
Attach {
|
AlreadyLive {
|
||||||
pod_name: String,
|
pod_name: String,
|
||||||
socket_path: PathBuf,
|
socket_path: PathBuf,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -466,8 +502,8 @@ pub enum AttachRestorePlan {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(tag = "action", rename_all = "snake_case")]
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
pub enum AttachRestoreResult {
|
pub enum RestoreResult {
|
||||||
Attached {
|
AlreadyLive {
|
||||||
pod_name: String,
|
pod_name: String,
|
||||||
socket_path: PathBuf,
|
socket_path: PathBuf,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -514,6 +550,7 @@ pub enum PodDiscoveryError {
|
||||||
struct VisibilitySet {
|
struct VisibilitySet {
|
||||||
visible: BTreeMap<String, VisibilityReason>,
|
visible: BTreeMap<String, VisibilityReason>,
|
||||||
child_sockets: BTreeMap<String, PathBuf>,
|
child_sockets: BTreeMap<String, PathBuf>,
|
||||||
|
comm_registry: BTreeMap<String, CommRegistryInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VisibilitySet {
|
impl VisibilitySet {
|
||||||
|
|
@ -527,6 +564,37 @@ impl VisibilitySet {
|
||||||
fn child_socket_for(&self, pod_name: &str) -> Option<PathBuf> {
|
fn child_socket_for(&self, pod_name: &str) -> Option<PathBuf> {
|
||||||
self.child_sockets.get(pod_name).cloned()
|
self.child_sockets.get(pod_name).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn comm_info_for(&self, pod_name: &str) -> CommRegistryInfo {
|
||||||
|
self.comm_registry
|
||||||
|
.get(pod_name)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(CommRegistryInfo::missing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn comm_info_from_spawned_child(child: &pod_store::PodSpawnedChild) -> CommRegistryInfo {
|
||||||
|
let scope_delegated = child
|
||||||
|
.scope_delegated
|
||||||
|
.iter()
|
||||||
|
.filter_map(|rule| {
|
||||||
|
let permission = match rule.permission.as_str() {
|
||||||
|
"read" => Permission::Read,
|
||||||
|
"write" => Permission::Write,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(ScopeRule {
|
||||||
|
target: rule.target.clone(),
|
||||||
|
permission,
|
||||||
|
recursive: rule.recursive,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
CommRegistryInfo {
|
||||||
|
registered: true,
|
||||||
|
socket_path: Some(child.socket_path.clone()),
|
||||||
|
scope_delegated,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn summarize_spawned_children(
|
async fn summarize_spawned_children(
|
||||||
|
|
@ -604,16 +672,16 @@ fn resolve_pod_command() -> PathBuf {
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, JsonSchema)]
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
struct PodNameInput {
|
struct PodNameInput {
|
||||||
/// Pod name to inspect, attach, or restore.
|
/// Pod name to restore.
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ListVisiblePodsTool<St> {
|
struct ListPodsTool<St> {
|
||||||
discovery: PodDiscovery<St>,
|
discovery: PodDiscovery<St>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<St> Tool for ListVisiblePodsTool<St>
|
impl<St> Tool for ListPodsTool<St>
|
||||||
where
|
where
|
||||||
St: PodMetadataStore + Clone + Send + Sync + 'static,
|
St: PodMetadataStore + Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
|
|
@ -631,53 +699,28 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InspectPodTool<St> {
|
struct RestorePodTool<St> {
|
||||||
discovery: PodDiscovery<St>,
|
discovery: PodDiscovery<St>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<St> Tool for InspectPodTool<St>
|
impl<St> Tool for RestorePodTool<St>
|
||||||
where
|
where
|
||||||
St: PodMetadataStore + Clone + Send + Sync + 'static,
|
St: PodMetadataStore + Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||||
let input: PodNameInput = serde_json::from_str(input_json)
|
let input: PodNameInput = serde_json::from_str(input_json)
|
||||||
.map_err(|e| ToolError::InvalidArgument(format!("invalid InspectPod input: {e}")))?;
|
.map_err(|e| ToolError::InvalidArgument(format!("invalid RestorePod input: {e}")))?;
|
||||||
let detail = self
|
|
||||||
.discovery
|
|
||||||
.inspect(&input.name)
|
|
||||||
.await
|
|
||||||
.map_err(discovery_error_to_tool_error)?;
|
|
||||||
Ok(ToolOutput {
|
|
||||||
summary: format!("pod `{}` inspected", detail.pod_name),
|
|
||||||
content: Some(json_content(&detail)?),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AttachOrRestorePodTool<St> {
|
|
||||||
discovery: PodDiscovery<St>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<St> Tool for AttachOrRestorePodTool<St>
|
|
||||||
where
|
|
||||||
St: PodMetadataStore + Clone + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
|
||||||
let input: PodNameInput = serde_json::from_str(input_json).map_err(|e| {
|
|
||||||
ToolError::InvalidArgument(format!("invalid AttachOrRestorePod input: {e}"))
|
|
||||||
})?;
|
|
||||||
let result = self
|
let result = self
|
||||||
.discovery
|
.discovery
|
||||||
.attach_or_restore(&input.name)
|
.restore(&input.name)
|
||||||
.await
|
.await
|
||||||
.map_err(discovery_error_to_tool_error)?;
|
.map_err(discovery_error_to_tool_error)?;
|
||||||
let summary = match &result {
|
let summary = match &result {
|
||||||
AttachRestoreResult::Attached { pod_name, .. } => {
|
RestoreResult::AlreadyLive { pod_name, .. } => {
|
||||||
format!("pod `{pod_name}` is live; attached to existing socket")
|
format!("pod `{pod_name}` is already live")
|
||||||
}
|
}
|
||||||
AttachRestoreResult::Restored { pod_name, .. } => {
|
RestoreResult::Restored { pod_name, .. } => {
|
||||||
format!("pod `{pod_name}` restored from pod state")
|
format!("pod `{pod_name}` restored from pod state")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -688,55 +731,38 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_visible_pods_tool<St>(discovery: PodDiscovery<St>) -> ToolDefinition
|
pub fn list_pods_tool<St>(discovery: PodDiscovery<St>) -> ToolDefinition
|
||||||
where
|
where
|
||||||
St: PodMetadataStore + Clone + Send + Sync + 'static,
|
St: PodMetadataStore + Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
Arc::new(move || {
|
Arc::new(move || {
|
||||||
let meta = ToolMeta::new("ListVisiblePods")
|
let meta = ToolMeta::new("ListPods")
|
||||||
.description(
|
.description(
|
||||||
"List Pod state entries visible to this Pod. This is state-backed and does not expose the host-wide Pod universe.",
|
"List Pods visible to this Pod from durable Pod state and the spawned-child registry. This does not expose the host-wide Pod universe.",
|
||||||
)
|
)
|
||||||
.input_schema(serde_json::json!({
|
.input_schema(serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {},
|
"properties": {},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
}));
|
}));
|
||||||
let tool: Arc<dyn Tool> = Arc::new(ListVisiblePodsTool {
|
let tool: Arc<dyn Tool> = Arc::new(ListPodsTool {
|
||||||
discovery: discovery.clone(),
|
discovery: discovery.clone(),
|
||||||
});
|
});
|
||||||
(meta, tool)
|
(meta, tool)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inspect_pod_tool<St>(discovery: PodDiscovery<St>) -> ToolDefinition
|
pub fn restore_pod_tool<St>(discovery: PodDiscovery<St>) -> ToolDefinition
|
||||||
where
|
where
|
||||||
St: PodMetadataStore + Clone + Send + Sync + 'static,
|
St: PodMetadataStore + Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
Arc::new(move || {
|
Arc::new(move || {
|
||||||
let meta = ToolMeta::new("InspectPod")
|
let meta = ToolMeta::new("RestorePod")
|
||||||
.description(
|
.description(
|
||||||
"Inspect one visible Pod by name from durable Pod state, distinguishing missing state from not-visible Pods.",
|
"Restore a visible stopped/restorable Pod, or report that a visible Pod is already live. Missing state is an error.",
|
||||||
)
|
)
|
||||||
.input_schema(serde_json::to_value(schemars::schema_for!(PodNameInput)).unwrap());
|
.input_schema(serde_json::to_value(schemars::schema_for!(PodNameInput)).unwrap());
|
||||||
let tool: Arc<dyn Tool> = Arc::new(InspectPodTool {
|
let tool: Arc<dyn Tool> = Arc::new(RestorePodTool {
|
||||||
discovery: discovery.clone(),
|
|
||||||
});
|
|
||||||
(meta, tool)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attach_or_restore_pod_tool<St>(discovery: PodDiscovery<St>) -> ToolDefinition
|
|
||||||
where
|
|
||||||
St: PodMetadataStore + Clone + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
Arc::new(move || {
|
|
||||||
let meta = ToolMeta::new("AttachOrRestorePod")
|
|
||||||
.description(
|
|
||||||
"Attach to a visible live Pod, or restore it from Pod state when no live socket is reachable. Missing state is an error.",
|
|
||||||
)
|
|
||||||
.input_schema(serde_json::to_value(schemars::schema_for!(PodNameInput)).unwrap());
|
|
||||||
let tool: Arc<dyn Tool> = Arc::new(AttachOrRestorePodTool {
|
|
||||||
discovery: discovery.clone(),
|
discovery: discovery.clone(),
|
||||||
});
|
});
|
||||||
(meta, tool)
|
(meta, tool)
|
||||||
|
|
@ -781,7 +807,7 @@ mod tests {
|
||||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn state_backed_visibility_and_attach_restore_planning() {
|
async fn state_backed_visibility_and_restore_planning() {
|
||||||
let _env = ENV_LOCK.lock().unwrap();
|
let _env = ENV_LOCK.lock().unwrap();
|
||||||
let root = TempDir::new().unwrap();
|
let root = TempDir::new().unwrap();
|
||||||
let store_dir = root.path().join("store");
|
let store_dir = root.path().join("store");
|
||||||
|
|
@ -873,6 +899,13 @@ mod tests {
|
||||||
registry,
|
registry,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let list_tool_def = list_pods_tool(discovery.clone());
|
||||||
|
let (list_meta, _) = list_tool_def();
|
||||||
|
assert_eq!(list_meta.name, "ListPods");
|
||||||
|
let restore_tool_def = restore_pod_tool(discovery.clone());
|
||||||
|
let (restore_meta, _) = restore_tool_def();
|
||||||
|
assert_eq!(restore_meta.name, "RestorePod");
|
||||||
|
|
||||||
let list = discovery.list_visible().await.unwrap();
|
let list = discovery.list_visible().await.unwrap();
|
||||||
let names: Vec<_> = list.iter().map(|p| p.pod_name.as_str()).collect();
|
let names: Vec<_> = list.iter().map(|p| p.pod_name.as_str()).collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -912,25 +945,16 @@ mod tests {
|
||||||
missing_err,
|
missing_err,
|
||||||
PodDiscoveryError::StateMissing { .. }
|
PodDiscoveryError::StateMissing { .. }
|
||||||
));
|
));
|
||||||
let hidden_restore_err = discovery
|
let hidden_restore_err = discovery.plan_restore("hidden").await.unwrap_err();
|
||||||
.plan_attach_or_restore("hidden")
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
hidden_restore_err,
|
hidden_restore_err,
|
||||||
PodDiscoveryError::NotVisible { .. }
|
PodDiscoveryError::NotVisible { .. }
|
||||||
));
|
));
|
||||||
|
|
||||||
let attach_plan = discovery
|
let live_plan = discovery.plan_restore("child-live").await.unwrap();
|
||||||
.plan_attach_or_restore("child-live")
|
assert!(matches!(live_plan, RestorePlan::AlreadyLive { .. }));
|
||||||
.await
|
let restore_plan = discovery.plan_restore("child-stale").await.unwrap();
|
||||||
.unwrap();
|
assert!(matches!(restore_plan, RestorePlan::Restore { .. }));
|
||||||
assert!(matches!(attach_plan, AttachRestorePlan::Attach { .. }));
|
|
||||||
let restore_plan = discovery
|
|
||||||
.plan_attach_or_restore("child-stale")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(matches!(restore_plan, AttachRestorePlan::Restore { .. }));
|
|
||||||
|
|
||||||
let lock_socket = runtime_base.join("lock-owner.sock");
|
let lock_socket = runtime_base.join("lock-owner.sock");
|
||||||
let _guard = pod_registry::install_top_level(
|
let _guard = pod_registry::install_top_level(
|
||||||
|
|
@ -945,10 +969,7 @@ mod tests {
|
||||||
active_child_segment,
|
active_child_segment,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let locked_err = discovery
|
let locked_err = discovery.plan_restore("child-stale").await.unwrap_err();
|
||||||
.plan_attach_or_restore("child-stale")
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
assert!(matches!(locked_err, PodDiscoveryError::LockConflict { .. }));
|
assert!(matches!(locked_err, PodDiscoveryError::LockConflict { .. }));
|
||||||
|
|
||||||
live_listener.abort();
|
live_listener.abort();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Pod-to-Pod communication tools.
|
//! Pod-to-Pod communication tools.
|
||||||
//!
|
//!
|
||||||
//! Four tools in one module — `SendToPod`, `ReadPodOutput`, `StopPod`,
|
//! Three tools in one module: `SendToPod`, `ReadPodOutput`, `StopPod`,
|
||||||
//! `ListPods` — all built on the same `SpawnedPodRegistry` handed in by
|
//! all built on the same `SpawnedPodRegistry` handed in by
|
||||||
//! the controller. Each operation is request-response: connect to the
|
//! the controller. Each operation is request-response: connect to the
|
||||||
//! target's Unix socket, perform one method exchange, disconnect.
|
//! target's Unix socket, perform one method exchange, disconnect.
|
||||||
//!
|
//!
|
||||||
|
|
@ -23,7 +23,6 @@ use session_store::LogEntry;
|
||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
|
|
||||||
use crate::runtime::dir::SpawnedPodRecord;
|
use crate::runtime::dir::SpawnedPodRecord;
|
||||||
use crate::runtime::pod_registry::{self, LockFileGuard};
|
|
||||||
use crate::spawn::registry::SpawnedPodRegistry;
|
use crate::spawn::registry::SpawnedPodRegistry;
|
||||||
|
|
||||||
/// Timeout applied to each socket-level operation — connect, write,
|
/// Timeout applied to each socket-level operation — connect, write,
|
||||||
|
|
@ -244,76 +243,6 @@ pub fn stop_pod_tool(registry: Arc<SpawnedPodRegistry>) -> ToolDefinition {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ListPods
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const LIST_PODS_DESCRIPTION: &str = "List all Pods spawned by this Pod along with their reachability \
|
|
||||||
status (`alive` / `stopped`) and the scope each was granted.";
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
|
||||||
struct EmptyInput {}
|
|
||||||
|
|
||||||
struct ListPodsTool {
|
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Tool for ListPodsTool {
|
|
||||||
async fn execute(&self, _input_json: &str) -> Result<ToolOutput, ToolError> {
|
|
||||||
let records = self.registry.list().await;
|
|
||||||
if records.is_empty() {
|
|
||||||
return Ok(ToolOutput {
|
|
||||||
summary: "no spawned pods".into(),
|
|
||||||
content: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut lines: Vec<String> = Vec::with_capacity(records.len());
|
|
||||||
let mut stale_names: Vec<String> = Vec::new();
|
|
||||||
for record in &records {
|
|
||||||
let alive = is_reachable(&record.socket_path).await;
|
|
||||||
let status = if alive { "alive" } else { "stopped" };
|
|
||||||
let scope = summarize_scope(record);
|
|
||||||
lines.push(format!("{} [{status}] scope={scope}", record.pod_name));
|
|
||||||
if !alive {
|
|
||||||
stale_names.push(record.pod_name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger stale reclaim on unreachable pods so the lock file's
|
|
||||||
// allocation table doesn't keep growing indefinitely when
|
|
||||||
// children crash without a clean exit path.
|
|
||||||
if !stale_names.is_empty() {
|
|
||||||
if let Ok(lock_path) = pod_registry::default_registry_path()
|
|
||||||
&& let Ok(mut guard) = LockFileGuard::open(&lock_path)
|
|
||||||
{
|
|
||||||
pod_registry::reclaim_stale(&mut guard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let summary = format!("{} pod(s) known", records.len());
|
|
||||||
Ok(ToolOutput {
|
|
||||||
summary,
|
|
||||||
content: Some(lines.join("\n")),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_pods_tool(registry: Arc<SpawnedPodRegistry>) -> ToolDefinition {
|
|
||||||
Arc::new(move || {
|
|
||||||
let schema = schemars::schema_for!(EmptyInput);
|
|
||||||
let schema_value = serde_json::to_value(schema).unwrap_or(serde_json::json!({}));
|
|
||||||
let meta = ToolMeta::new("ListPods")
|
|
||||||
.description(LIST_PODS_DESCRIPTION)
|
|
||||||
.input_schema(schema_value);
|
|
||||||
let tool: Arc<dyn Tool> = Arc::new(ListPodsTool {
|
|
||||||
registry: registry.clone(),
|
|
||||||
});
|
|
||||||
(meta, tool)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -322,6 +251,29 @@ fn unknown_pod_err(name: &str) -> ToolError {
|
||||||
ToolError::InvalidArgument(format!("no spawned pod named `{name}`"))
|
ToolError::InvalidArgument(format!("no spawned pod named `{name}`"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn summarize_scope(record: &SpawnedPodRecord) -> String {
|
||||||
|
if record.scope_delegated.is_empty() {
|
||||||
|
return "(none)".into();
|
||||||
|
}
|
||||||
|
let parts: Vec<String> = record
|
||||||
|
.scope_delegated
|
||||||
|
.iter()
|
||||||
|
.map(|rule| {
|
||||||
|
let perm = match rule.permission {
|
||||||
|
manifest::Permission::Read => "read",
|
||||||
|
manifest::Permission::Write => "write",
|
||||||
|
};
|
||||||
|
let recursive = if rule.recursive {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
" [non-recursive]"
|
||||||
|
};
|
||||||
|
format!("{perm}:{}{recursive}", rule.target.display())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
parts.join(", ")
|
||||||
|
}
|
||||||
|
|
||||||
/// Connect with a timeout, drain the server's connect-time snapshot,
|
/// Connect with a timeout, drain the server's connect-time snapshot,
|
||||||
/// write one `Method` line, flush, and close.
|
/// write one `Method` line, flush, and close.
|
||||||
///
|
///
|
||||||
|
|
@ -487,14 +439,6 @@ async fn fetch_history(socket: &Path) -> std::io::Result<Vec<serde_json::Value>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Probe-connect test. Connection accepted within timeout → alive.
|
|
||||||
async fn is_reachable(socket: &Path) -> bool {
|
|
||||||
tokio::time::timeout(SOCKET_OP_TIMEOUT, UnixStream::connect(socket))
|
|
||||||
.await
|
|
||||||
.map(|r| r.is_ok())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_assistant_text(entries: &[serde_json::Value]) -> String {
|
fn extract_assistant_text(entries: &[serde_json::Value]) -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
for value in entries {
|
for value in entries {
|
||||||
|
|
@ -536,25 +480,6 @@ fn push_assistant_text(out: &mut String, logged: session_store::LoggedItem) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn summarize_scope(record: &SpawnedPodRecord) -> String {
|
|
||||||
if record.scope_delegated.is_empty() {
|
|
||||||
return "(none)".into();
|
|
||||||
}
|
|
||||||
let parts: Vec<String> = record
|
|
||||||
.scope_delegated
|
|
||||||
.iter()
|
|
||||||
.map(|r| {
|
|
||||||
let perm = match r.permission {
|
|
||||||
manifest::Permission::Read => "read",
|
|
||||||
manifest::Permission::Write => "write",
|
|
||||||
};
|
|
||||||
let tag = if r.recursive { "" } else { " [non-recursive]" };
|
|
||||||
format!("{perm}:{}{tag}", r.target.display())
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
parts.join(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
//! Shared registry of Pods spawned by this Pod.
|
//! Shared registry of Pods spawned by this Pod.
|
||||||
//!
|
//!
|
||||||
//! `SpawnPod` writes here; the pod-comm tools (`SendToPod`,
|
//! `SpawnPod` writes here; the pod-comm tools (`SendToPod`,
|
||||||
//! `ReadPodOutput`, `StopPod`, `ListPods`) read and mutate the same
|
//! `ReadPodOutput`, `StopPod`) read and mutate the same instance. Discovery
|
||||||
//! instance. Runtime write-through still materialises `spawned_pods.json`,
|
//! tools consult this registry together with durable Pod state. Runtime
|
||||||
//! but durable state lives in the spawner's Pod metadata.
|
//! write-through still materialises `spawned_pods.json`, but durable state lives
|
||||||
|
//! in the spawner's Pod metadata.
|
||||||
//!
|
//!
|
||||||
//! `ReadPodOutput` additionally owns a per-spawned-pod cursor here so
|
//! `ReadPodOutput` additionally owns a per-spawned-pod cursor here so
|
||||||
//! two consecutive reads yield only new assistant text. The cursor is
|
//! two consecutive reads yield only new assistant text. The cursor is
|
||||||
|
|
|
||||||
|
|
@ -220,8 +220,8 @@ pub struct SpawnPodTool {
|
||||||
/// override it. Defaults to the spawner's pwd — see module docs.
|
/// override it. Defaults to the spawner's pwd — see module docs.
|
||||||
spawner_pwd: PathBuf,
|
spawner_pwd: PathBuf,
|
||||||
/// Shared registry of spawned children, also used by the
|
/// Shared registry of spawned children, also used by the
|
||||||
/// pod-comm tools (`SendToPod` / `ReadPodOutput` / `StopPod` /
|
/// pod-comm tools (`SendToPod` / `ReadPodOutput` / `StopPod`) and by
|
||||||
/// `ListPods`). Writes the list to runtime and durable Pod state on
|
/// Pod discovery. Writes the list to runtime and durable Pod state on
|
||||||
/// each add.
|
/// each add.
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
registry: Arc<SpawnedPodRegistry>,
|
||||||
/// THIS Pod's own parent-callback socket, if any. After a
|
/// THIS Pod's own parent-callback socket, if any. After a
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! Integration tests for the pod-comm tools (`SendToPod`,
|
//! Integration tests for the pod-comm tools (`SendToPod`,
|
||||||
//! `ReadPodOutput`, `StopPod`, `ListPods`).
|
//! `ReadPodOutput`, `StopPod`).
|
||||||
//!
|
//!
|
||||||
//! The real child Pod binary is not started. Instead each test stands
|
//! The real child Pod binary is not started. Instead each test stands
|
||||||
//! up a mock `UnixListener` that speaks the socket protocol directly:
|
//! up a mock `UnixListener` that speaks the socket protocol directly:
|
||||||
|
|
@ -16,9 +16,7 @@ use llm_worker::tool::ToolOutput;
|
||||||
use manifest::{Permission, Scope, ScopeRule, SharedScope};
|
use manifest::{Permission, Scope, ScopeRule, SharedScope};
|
||||||
use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord};
|
use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord};
|
||||||
use pod::runtime::pod_registry::{self, LockFileGuard};
|
use pod::runtime::pod_registry::{self, LockFileGuard};
|
||||||
use pod::spawn::comm_tools::{
|
use pod::spawn::comm_tools::{read_pod_output_tool, send_to_pod_tool, stop_pod_tool};
|
||||||
list_pods_tool, read_pod_output_tool, send_to_pod_tool, stop_pod_tool,
|
|
||||||
};
|
|
||||||
use pod::spawn::registry::SpawnedPodRegistry;
|
use pod::spawn::registry::SpawnedPodRegistry;
|
||||||
use pod_store::{CombinedStore, FsPodStore, PodMetadataStore};
|
use pod_store::{CombinedStore, FsPodStore, PodMetadataStore};
|
||||||
use protocol::stream::{JsonLineReader, JsonLineWriter};
|
use protocol::stream::{JsonLineReader, JsonLineWriter};
|
||||||
|
|
@ -544,13 +542,6 @@ async fn restored_registry_uses_pod_state_without_runtime_file() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let def = list_pods_tool(restored.clone());
|
|
||||||
let (_meta, tool) = def();
|
|
||||||
let output: ToolOutput = tool.execute("{}").await.unwrap();
|
|
||||||
assert!(output.summary.contains("1 pod"), "{}", output.summary);
|
|
||||||
let body = output.content.expect("restored ListPods should list child");
|
|
||||||
assert!(body.contains("child [alive]"), "body: {body}");
|
|
||||||
|
|
||||||
let def = send_to_pod_tool(restored.clone());
|
let def = send_to_pod_tool(restored.clone());
|
||||||
let (_meta, tool) = def();
|
let (_meta, tool) = def();
|
||||||
let input = json!({ "name": "child", "message": "after restart" }).to_string();
|
let input = json!({ "name": "child", "message": "after restart" }).to_string();
|
||||||
|
|
@ -718,51 +709,3 @@ async fn load_from_pod_state_reclaims_missing_child_scope_and_records_history()
|
||||||
let runtime_records: Vec<SpawnedPodRecord> = serde_json::from_str(&runtime_contents).unwrap();
|
let runtime_records: Vec<SpawnedPodRecord> = serde_json::from_str(&runtime_contents).unwrap();
|
||||||
assert!(runtime_records.is_empty());
|
assert!(runtime_records.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ListPods
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn list_pods_reports_alive_and_stopped() {
|
|
||||||
let (tmp, registry, _rd) = setup_registry().await;
|
|
||||||
|
|
||||||
// One child is reachable…
|
|
||||||
let (live_socket, listener) = bind_mock_socket(tmp.path(), "alive").await;
|
|
||||||
// Keep the listener alive by moving it into a task that never exits.
|
|
||||||
let _accept = tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
let Ok((stream, _)) = listener.accept().await else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
drop(stream);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
register_child(®istry, "alive", &live_socket, tmp.path()).await;
|
|
||||||
|
|
||||||
// …the other is not.
|
|
||||||
let dead_socket = tmp.path().join("dead.sock");
|
|
||||||
register_child(®istry, "dead", &dead_socket, tmp.path()).await;
|
|
||||||
|
|
||||||
let def = list_pods_tool(registry);
|
|
||||||
let (_meta, tool) = def();
|
|
||||||
let output: ToolOutput = tool.execute("{}").await.unwrap();
|
|
||||||
assert!(output.summary.contains("2 pod"), "{}", output.summary);
|
|
||||||
let body = output.content.expect("list_pods should populate content");
|
|
||||||
assert!(body.contains("alive [alive]"), "body: {body}");
|
|
||||||
assert!(body.contains("dead [stopped]"), "body: {body}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn list_pods_empty_when_nothing_registered() {
|
|
||||||
let (_tmp, registry, _rd) = setup_registry().await;
|
|
||||||
let def = list_pods_tool(registry);
|
|
||||||
let (_meta, tool) = def();
|
|
||||||
let output: ToolOutput = tool.execute("{}").await.unwrap();
|
|
||||||
assert!(
|
|
||||||
output.summary.contains("no spawned pods"),
|
|
||||||
"{}",
|
|
||||||
output.summary
|
|
||||||
);
|
|
||||||
assert!(output.content.is_none());
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -56,16 +56,12 @@ pub enum Method {
|
||||||
kind: CompletionKind,
|
kind: CompletionKind,
|
||||||
prefix: String,
|
prefix: String,
|
||||||
},
|
},
|
||||||
/// List Pods visible to this Pod from durable Pod state. This is not a
|
/// List Pods visible to this Pod from durable Pod state and the spawned-child
|
||||||
/// host-wide Pod universe query.
|
/// registry. This is not a host-wide Pod universe query.
|
||||||
ListVisiblePods,
|
ListPods,
|
||||||
/// Inspect one Pod by name if its state exists and it is visible to this Pod.
|
/// Restore a visible stopped/restorable Pod, or report that it is already
|
||||||
InspectPod {
|
/// live. Missing state and not-visible state are distinct errors.
|
||||||
name: String,
|
RestorePod {
|
||||||
},
|
|
||||||
/// Attach to a visible live Pod, or restore it from durable Pod state when
|
|
||||||
/// it is not live. Missing state and not-visible state are distinct errors.
|
|
||||||
AttachOrRestorePod {
|
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -474,18 +470,14 @@ pub enum Event {
|
||||||
input: Vec<Segment>,
|
input: Vec<Segment>,
|
||||||
summary: RewindSummary,
|
summary: RewindSummary,
|
||||||
},
|
},
|
||||||
/// Reply to `Method::ListVisiblePods`. Payload is a stable JSON value so
|
/// Reply to `Method::ListPods`. Payload is a stable JSON value so the Pod
|
||||||
/// the Pod crate can evolve discovery fields without introducing a protocol
|
/// crate can evolve discovery fields without introducing a protocol
|
||||||
/// dependency on session-store.
|
/// dependency on session-store.
|
||||||
VisiblePods {
|
PodsListed {
|
||||||
pods: serde_json::Value,
|
pods: serde_json::Value,
|
||||||
},
|
},
|
||||||
/// Reply to `Method::InspectPod`.
|
/// Reply to `Method::RestorePod`.
|
||||||
PodInspection {
|
PodRestored {
|
||||||
pod: serde_json::Value,
|
|
||||||
},
|
|
||||||
/// Reply to `Method::AttachOrRestorePod`.
|
|
||||||
PodAttachRestore {
|
|
||||||
result: serde_json::Value,
|
result: serde_json::Value,
|
||||||
},
|
},
|
||||||
Alert(Alert),
|
Alert(Alert),
|
||||||
|
|
@ -1469,11 +1461,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn pod_discovery_methods_roundtrip() {
|
fn pod_discovery_methods_roundtrip() {
|
||||||
let methods = [
|
let methods = [
|
||||||
Method::ListVisiblePods,
|
Method::ListPods,
|
||||||
Method::InspectPod {
|
Method::RestorePod {
|
||||||
name: "child".into(),
|
|
||||||
},
|
|
||||||
Method::AttachOrRestorePod {
|
|
||||||
name: "child".into(),
|
name: "child".into(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -1481,9 +1470,8 @@ mod tests {
|
||||||
let json = serde_json::to_string(&method).unwrap();
|
let json = serde_json::to_string(&method).unwrap();
|
||||||
let decoded: Method = serde_json::from_str(&json).unwrap();
|
let decoded: Method = serde_json::from_str(&json).unwrap();
|
||||||
match (decoded, method) {
|
match (decoded, method) {
|
||||||
(Method::ListVisiblePods, Method::ListVisiblePods)
|
(Method::ListPods, Method::ListPods)
|
||||||
| (Method::InspectPod { .. }, Method::InspectPod { .. })
|
| (Method::RestorePod { .. }, Method::RestorePod { .. }) => {}
|
||||||
| (Method::AttachOrRestorePod { .. }, Method::AttachOrRestorePod { .. }) => {}
|
|
||||||
(decoded, expected) => panic!("decoded {decoded:?}, expected {expected:?}"),
|
(decoded, expected) => panic!("decoded {decoded:?}, expected {expected:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1492,30 +1480,23 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn pod_discovery_events_roundtrip() {
|
fn pod_discovery_events_roundtrip() {
|
||||||
let events = [
|
let events = [
|
||||||
Event::VisiblePods {
|
Event::PodsListed {
|
||||||
pods: serde_json::json!([{ "pod_name": "child" }]),
|
pods: serde_json::json!([{ "pod_name": "child" }]),
|
||||||
},
|
},
|
||||||
Event::PodInspection {
|
Event::PodRestored {
|
||||||
pod: serde_json::json!({ "pod_name": "child" }),
|
result: serde_json::json!({ "action": "already_live" }),
|
||||||
},
|
|
||||||
Event::PodAttachRestore {
|
|
||||||
result: serde_json::json!({ "action": "attach" }),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
for event in events {
|
for event in events {
|
||||||
let json = serde_json::to_string(&event).unwrap();
|
let json = serde_json::to_string(&event).unwrap();
|
||||||
let decoded: Event = serde_json::from_str(&json).unwrap();
|
let decoded: Event = serde_json::from_str(&json).unwrap();
|
||||||
match (decoded, event) {
|
match (decoded, event) {
|
||||||
(Event::VisiblePods { pods }, Event::VisiblePods { pods: expected }) => {
|
(Event::PodsListed { pods }, Event::PodsListed { pods: expected }) => {
|
||||||
assert_eq!(pods, expected)
|
assert_eq!(pods, expected)
|
||||||
}
|
}
|
||||||
(Event::PodInspection { pod }, Event::PodInspection { pod: expected }) => {
|
(Event::PodRestored { result }, Event::PodRestored { result: expected }) => {
|
||||||
assert_eq!(pod, expected)
|
assert_eq!(result, expected)
|
||||||
}
|
}
|
||||||
(
|
|
||||||
Event::PodAttachRestore { result },
|
|
||||||
Event::PodAttachRestore { result: expected },
|
|
||||||
) => assert_eq!(result, expected),
|
|
||||||
(decoded, expected) => panic!("decoded {decoded:?}, expected {expected:?}"),
|
(decoded, expected) => panic!("decoded {decoded:?}, expected {expected:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1204,9 +1204,7 @@ impl App {
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Event::VisiblePods { .. }
|
Event::PodsListed { .. } | Event::PodRestored { .. } => {}
|
||||||
| Event::PodInspection { .. }
|
|
||||||
| Event::PodAttachRestore { .. } => {}
|
|
||||||
Event::Shutdown => {
|
Event::Shutdown => {
|
||||||
self.mark_orphan_compacts_incomplete();
|
self.mark_orphan_compacts_incomplete();
|
||||||
self.quit = true;
|
self.quit = true;
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,7 @@ fn draw(f: &mut Frame<'_>, list: &PodList) {
|
||||||
Span::styled("[↑/↓]", Style::default().fg(Color::DarkGray)),
|
Span::styled("[↑/↓]", Style::default().fg(Color::DarkGray)),
|
||||||
Span::raw(" select "),
|
Span::raw(" select "),
|
||||||
Span::styled("[enter]", Style::default().fg(Color::Green)),
|
Span::styled("[enter]", Style::default().fg(Color::Green)),
|
||||||
Span::raw(" attach/restore "),
|
Span::raw(" open/restore "),
|
||||||
Span::styled("[esc]", Style::default().fg(Color::Yellow)),
|
Span::styled("[esc]", Style::default().fg(Color::Yellow)),
|
||||||
Span::raw(" cancel"),
|
Span::raw(" cancel"),
|
||||||
])),
|
])),
|
||||||
|
|
|
||||||
|
|
@ -64,11 +64,11 @@ Pod の制御・監視に使う JSONL ベースのメッセージプロトコル
|
||||||
- context/session 制御: `Compact`, `ListRewindTargets`, `RewindTo`
|
- context/session 制御: `Compact`, `ListRewindTargets`, `RewindTo`
|
||||||
- typed injection / child lifecycle: `Notify`, `PodEvent`
|
- typed injection / child lifecycle: `Notify`, `PodEvent`
|
||||||
- client 補助: `ListCompletions`
|
- client 補助: `ListCompletions`
|
||||||
- Pod visibility / attach: `ListVisiblePods`, `InspectPod`, `AttachOrRestorePod`
|
- Pod visibility / restore: `ListPods`, `RestorePod`
|
||||||
- **Pod → Client (`Event`)**
|
- **Pod → Client (`Event`)**
|
||||||
- accepted input / history seed: `Snapshot`, `UserMessage`, `SystemItem`, `SegmentRotated`
|
- accepted input / history seed: `Snapshot`, `UserMessage`, `SystemItem`, `SegmentRotated`
|
||||||
- generation stream: `TurnStart`, `TurnEnd`, `LlmCallStart`, `LlmCallEnd`, retry/continuation events, `Text*`, `Thinking*`, `ToolCall*`, `ToolResult`, `Usage`, `RunEnd`
|
- generation stream: `TurnStart`, `TurnEnd`, `LlmCallStart`, `LlmCallEnd`, retry/continuation events, `Text*`, `Thinking*`, `ToolCall*`, `ToolResult`, `Usage`, `RunEnd`
|
||||||
- control replies: completions, rewind, visible Pod / inspect / attach results
|
- control replies: completions, rewind, visible Pod list / restore results
|
||||||
- operational status: `Status`, `Alert`, `MemoryWorker`, `Compact*`, `Error`, `Shutdown`
|
- operational status: `Status`, `Alert`, `MemoryWorker`, `Compact*`, `Error`, `Shutdown`
|
||||||
- リクエストとレスポンスの紐付けを一般化した RPC にはしない。多くの状態は broadcast event と Pod status で観測する
|
- リクエストとレスポンスの紐付けを一般化した RPC にはしない。多くの状態は broadcast event と Pod status で観測する
|
||||||
- 一部の reply(例: completions)は要求 socket にだけ返る。broadcast event と request-local reply の違いは enum variant のコメントを正とする
|
- 一部の reply(例: completions)は要求 socket にだけ返る。broadcast event と request-local reply の違いは enum variant のコメントを正とする
|
||||||
|
|
@ -146,8 +146,7 @@ Pod が操作できるファイルパスの制御。
|
||||||
| File / shell | `Read`, `Write`, `Edit`, `Glob`, `Grep`, `Bash` | workspace ファイル操作と shell 実行。file tools は `ScopedFs` と read-before-edit tracker を通る。`Bash` は permission policy と出力退避で制御する |
|
| File / shell | `Read`, `Write`, `Edit`, `Glob`, `Grep`, `Bash` | workspace ファイル操作と shell 実行。file tools は `ScopedFs` と read-before-edit tracker を通る。`Bash` は permission policy と出力退避で制御する |
|
||||||
| Task | `TaskCreate`, `TaskUpdate`, `TaskList`, `TaskGet` | セッション内の短期 task 状態管理 |
|
| Task | `TaskCreate`, `TaskUpdate`, `TaskList`, `TaskGet` | セッション内の短期 task 状態管理 |
|
||||||
| Memory / Knowledge | `MemoryQuery`, `MemoryRead`, `MemoryWrite`, `MemoryEdit`, `MemoryDelete`, `KnowledgeQuery` | manifest の memory 設定が有効な時に登録される durable memory / knowledge 操作 |
|
| Memory / Knowledge | `MemoryQuery`, `MemoryRead`, `MemoryWrite`, `MemoryEdit`, `MemoryDelete`, `KnowledgeQuery` | manifest の memory 設定が有効な時に登録される durable memory / knowledge 操作 |
|
||||||
| Pod orchestration | `SpawnPod`, `SendToPod`, `ReadPodOutput`, `StopPod`, `ListPods` | child Pod の起動・通信・停止・一覧 |
|
| Pod orchestration | `SpawnPod`, `SendToPod`, `ReadPodOutput`, `StopPod`, `ListPods`, `RestorePod` | child / visible Pod の起動・通信・停止・一覧・復元 |
|
||||||
| Visible Pod state | `ListVisiblePods`, `InspectPod`, `AttachOrRestorePod` | durable Pod state と visibility に基づく Pod inspection / attach / restore |
|
|
||||||
| Web | `WebSearch`, `WebFetch` | manifest/env で明示設定された provider 経由の bounded web access |
|
| Web | `WebSearch`, `WebFetch` | manifest/env で明示設定された provider 経由の bounded web access |
|
||||||
|
|
||||||
すべての tool call は manifest tool permission と scope/policy のチェックを通る。ファイル write scope、Pod delegation、memory layout、web provider 設定はそれぞれ別の authority を持ち、UI 表示だけで権限を広げない。
|
すべての tool call は manifest tool permission と scope/policy のチェックを通る。ファイル write scope、Pod delegation、memory layout、web provider 設定はそれぞれ別の authority を持ち、UI 表示だけで権限を広げない。
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user