mirror of
https://github.com/openai/codex.git
synced 2026-11-10 00:42:33 +00:00
## Why Parent-owned Multi-Agent V2 subagents must remain controlled by their parent, but several app-server endpoints could still mutate them directly. ## What changed - Centralize the direct-input ownership check and apply it to turn injection, MCP calls, reviews, compaction, rollback and revert, shell and Guardian actions, realtime requests, and goal mutations. - Enforce the same policy for goal mutations on unloaded threads while keeping goal reads and turn interruption available. - Document the affected endpoint behavior and continue exposing `canAcceptDirectInput: false` for parent-owned subagents. ## Testing - Cover direct-input rejection for both legacy and paginated thread history. - Cover goal mutation ownership checks for unloaded V2 subagents, V1 subagents, and non-subagent V2 threads. GitOrigin-RevId: 791b7af98490aed5eab4e672b90661acad11ec23
80 lines
2.9 KiB
Rust
80 lines
2.9 KiB
Rust
use super::thread_input::can_accept_direct_input;
|
|
use crate::thread_status::ThreadWatchManager;
|
|
use crate::thread_status::resolve_thread_status;
|
|
use codex_app_server_protocol::SessionSource;
|
|
use codex_app_server_protocol::Thread;
|
|
use codex_app_server_protocol::ThreadStatus;
|
|
use codex_core::ThreadManager;
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::protocol::AgentStatus;
|
|
use codex_protocol::protocol::SubAgentSource;
|
|
|
|
pub(super) async fn enrich_loaded_threads<T>(
|
|
thread_manager: &ThreadManager,
|
|
thread_watch_manager: &ThreadWatchManager,
|
|
threads: &mut [T],
|
|
mut as_thread: impl FnMut(&mut T) -> &mut Thread,
|
|
) {
|
|
let statuses = thread_watch_manager
|
|
.loaded_statuses_for_threads(
|
|
threads
|
|
.iter_mut()
|
|
.map(&mut as_thread)
|
|
.map(|thread| thread.id.clone()),
|
|
)
|
|
.await;
|
|
|
|
futures::future::join_all(threads.iter_mut().map(as_thread).map(|thread| {
|
|
let statuses = &statuses;
|
|
async move {
|
|
let watched_status = statuses.get(&thread.id);
|
|
if let Some(status) = watched_status {
|
|
thread.status = status.clone();
|
|
}
|
|
|
|
if !matches!(
|
|
&thread.source,
|
|
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
|
|
) || matches!(watched_status, Some(ThreadStatus::NotLoaded))
|
|
{
|
|
return;
|
|
}
|
|
|
|
let Ok(thread_id) = ThreadId::from_string(&thread.id) else {
|
|
return;
|
|
};
|
|
let Ok(loaded_thread) = thread_manager.get_thread(thread_id).await else {
|
|
return;
|
|
};
|
|
match loaded_thread.agent_status().await {
|
|
AgentStatus::Running => {
|
|
if watched_status.is_none() {
|
|
thread.status = resolve_thread_status(
|
|
ThreadStatus::Idle,
|
|
/*has_in_progress_turn*/ true,
|
|
);
|
|
}
|
|
}
|
|
AgentStatus::PendingInit | AgentStatus::Interrupted | AgentStatus::Completed(_) => {
|
|
if watched_status.is_none() {
|
|
thread.status = ThreadStatus::Idle;
|
|
}
|
|
}
|
|
AgentStatus::Errored(_) => {
|
|
thread.status = ThreadStatus::SystemError;
|
|
}
|
|
AgentStatus::Shutdown | AgentStatus::NotFound => {
|
|
thread.status = ThreadStatus::NotLoaded;
|
|
return;
|
|
}
|
|
}
|
|
let config_snapshot = loaded_thread.config_snapshot().await;
|
|
thread.can_accept_direct_input = Some(can_accept_direct_input(
|
|
loaded_thread.multi_agent_version(),
|
|
&config_snapshot.session_source,
|
|
));
|
|
}
|
|
}))
|
|
.await;
|
|
}
|