pod: simplify pod tool surface

This commit is contained in:
Keisuke Hirata 2026-05-31 11:50:41 +09:00
parent 32ac0eee35
commit 5472ceca48
No known key found for this signature in database
10 changed files with 192 additions and 355 deletions

View File

@ -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"

View File

@ -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();

View File

@ -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::*;

View File

@ -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

View File

@ -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

View File

@ -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(&registry, "alive", &live_socket, tmp.path()).await;
// …the other is not.
let dead_socket = tmp.path().join("dead.sock");
register_child(&registry, "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());
}

View File

@ -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:?}"),
} }
} }

View File

@ -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;

View File

@ -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"),
])), ])),

View File

@ -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 表示だけで権限を広げない。