Synced from monorepo

Synced from monorepo

Changes:
- Workspace task snapshots only list incomplete backgrounded tasks
- Quiet auth, LSP, and config warnings in the shell
- Fix observability attributes for warm store errors, restore setup, remote tools, and preview denials
- Fail closed when soak metrics are missing
- Run plan-mode exit last in mixed tool batches
- Allow /loop to store prompts that can terminate the loop
- Make subagent maximum nesting depth configurable
- Security: apply sandbox profile to the leader process that executes tools

Source-Revision: 1adcd1f477870e4a97bacbd6be78c8a3bfbac46d
This commit is contained in:
grokkybara[bot] 2026-07-27 17:54:34 +00:00
commit 02d9359435
96 changed files with 2346 additions and 351 deletions

View file

@ -50,7 +50,8 @@ pub use lsp::LspTool;
pub use monitor::tool::MonitorTool;
pub use read_file::ReadFileTool;
pub use scheduler::create::{
SCHEDULER_CREATE_TOOL_NAME, SchedulerCreateTool, loop_schedule_instruction, loop_usage_message,
LoopFireMode, SCHEDULER_CREATE_TOOL_NAME, SchedulerCreateTool, loop_schedule_instruction,
loop_usage_message,
};
pub use scheduler::delete::{SCHEDULER_DELETE_TOOL_NAME, SchedulerDeleteTool};
pub use scheduler::list::SchedulerListTool;

View file

@ -110,6 +110,53 @@ impl MonitorInput {
}
}
// Mid-turn monitor event buffer
/// A monitor event notification to be surfaced as a `<system-reminder>` mid-turn.
#[derive(Debug, Clone)]
pub struct MonitorEventNotification {
pub task_id: String,
pub event_text: String,
/// Session that owns the monitor which produced this event.
///
/// In leader mode every session shares one [`MonitorEventBuffer`], so the
/// drain sites filter on this to avoid surfacing one session's monitor
/// events inside another session's turn. `None` for legacy / non-grok-build
/// backends, which any session drains for backwards compatibility.
pub owner_session_id: Option<String>,
}
impl MonitorEventNotification {
/// Whether this buffered event should surface in the session whose owner id
/// is `my_owner`. Mirrors `task_owned_by_session`: an event surfaces only
/// when it has no recorded owner (legacy) or its owner matches the draining
/// session. Foreign events stay buffered for their own session to drain.
pub fn owned_by_session(&self, my_owner: Option<&str>) -> bool {
match (my_owner, self.owner_session_id.as_deref()) {
(Some(me), Some(owner)) => me == owner,
_ => true,
}
}
}
/// Shared buffer for mid-turn monitor event notifications: an [`EventQueue`]
/// of [`MonitorEventNotification`]. Producers `push_capped`; the turn loop
/// drains its session's events via [`drain_owned`].
///
/// [`EventQueue`]: xai_interjection_core::EventQueue
pub type MonitorEventBuffer = xai_interjection_core::EventQueue<MonitorEventNotification>;
crate::register_resource!("grok_build", "MonitorEventBuffer", MonitorEventBuffer);
/// Drain only `my_owner`'s events (the buffer is shared across sessions in
/// leader mode); owner-less legacy events drain anywhere.
pub fn drain_owned(
buffer: &MonitorEventBuffer,
my_owner: Option<&str>,
) -> Vec<MonitorEventNotification> {
buffer.drain_matching(|e| e.owned_by_session(my_owner))
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -8,7 +8,7 @@ use super::types::{ScheduledTask, SchedulerCommand, SchedulerHandle, scheduler_t
// Canonical /loop wording lives in the light API crate so other consumers can
// link it without the tools implementation crate; re-exported to keep paths stable.
pub use xai_grok_tools_api::slash_commands::{
SCHEDULER_CREATE_TOOL_NAME, loop_schedule_instruction, loop_usage_message,
LoopFireMode, SCHEDULER_CREATE_TOOL_NAME, loop_schedule_instruction, loop_usage_message,
};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
@ -475,7 +475,7 @@ mod tests {
#[test]
fn loop_schedule_instruction_holds_invariants() {
let args = "every 30 minutes do x";
let instr = loop_schedule_instruction(args);
let instr = loop_schedule_instruction(args, LoopFireMode::Detached);
assert!(
!instr.contains("10m"),
"instruction must not default: {instr}"

View file

@ -14,7 +14,6 @@ use super::types::{
};
/// Cap on retained completed-subagent entries before the oldest are evicted.
/// Public so the subagent soak test can assert the coordinator stays bounded.
pub const MAX_COMPLETED_ENTRIES: usize = 1024;
pub(super) const OUTPUT_UNAVAILABLE_PLACEHOLDER: &str = "[subagent output no longer available]";

View file

@ -9,6 +9,7 @@
//!
//! - `SubagentBackendResource` — backend for spawn/query/cancel (required)
//! - `SubagentDepthCounter` — current nesting depth (optional, defaults to 0)
//! - `MaxSubagentDepth` — max nesting (optional, defaults to [`MAX_SUBAGENT_DEPTH`])
//! - `SessionIdResource` — current session ID for parent scoping (optional)
//! - `SubagentForegroundWait` — host wait-window guard factory (optional)
//! - `TaskModelValidator` — validates explicit model slugs before spawn
@ -30,10 +31,16 @@ use crate::types::resources::SharedResources;
use crate::types::tool::{ToolKind, ToolNamespace};
use xai_tool_types::{SubagentCompletedOutput, SubagentIsolationMode, TaskToolInput};
/// Maximum nesting depth for subagents. A top-level session is depth 0;
/// the first subagent is depth 1. Subagents cannot spawn further subagents.
/// Default max nesting depth when [`MaxSubagentDepth`] is not injected.
pub const MAX_SUBAGENT_DEPTH: u32 = 1;
pub fn effective_max_subagent_depth(resources: &crate::types::resources::Resources) -> u32 {
resources
.get::<MaxSubagentDepth>()
.map(|d| d.0)
.unwrap_or(MAX_SUBAGENT_DEPTH)
}
// ───────────────────────────────────────────────────────────────────────────
// Tool implementation
// ───────────────────────────────────────────────────────────────────────────
@ -125,10 +132,19 @@ impl xai_tool_runtime::Tool for TaskTool {
.map(|cancellation| cancellation.0.clone());
// 1. Depth check
let (depth, backend, model_validator, parent_session_id, parent_prompt_id, foreground_wait) = {
let (
depth,
max_depth,
backend,
model_validator,
parent_session_id,
parent_prompt_id,
foreground_wait,
) = {
let res = resources.lock().await;
let depth = res.get::<SubagentDepthCounter>().map(|d| d.0).unwrap_or(0);
let max_depth = effective_max_subagent_depth(&res);
let backend = res
.get::<SubagentBackendResource>()
@ -155,6 +171,7 @@ impl xai_tool_runtime::Tool for TaskTool {
(
depth,
max_depth,
backend,
model_validator,
parent_session_id,
@ -163,9 +180,9 @@ impl xai_tool_runtime::Tool for TaskTool {
)
};
if depth >= MAX_SUBAGENT_DEPTH {
if depth >= max_depth {
return Err(xai_tool_runtime::ToolError::invalid_arguments(format!(
"Subagent depth limit exceeded (current depth: {depth}, max: {MAX_SUBAGENT_DEPTH}). \
"Subagent depth limit exceeded (current depth: {depth}, max: {max_depth}). \
Cannot spawn further nested subagents."
)));
}
@ -558,6 +575,41 @@ mod tests {
assert!(err.contains("depth limit exceeded"), "error: {err}");
}
#[tokio::test]
async fn raised_max_depth_allows_nested_spawn() {
let (backend, mut rx) = make_backend();
let mut resources = Resources::new();
resources.insert(backend);
resources.insert(SubagentDepthCounter(1));
resources.insert(MaxSubagentDepth(2));
resources.insert(SessionIdResource("child-session".to_string()));
resources.insert(CurrentPromptIdResource("prompt-nested".to_string()));
let result = xai_tool_runtime::Tool::run(
&TaskTool,
test_ctx(resources.into_shared()),
TaskToolInput {
description: "nested ok".into(),
prompt: "should be allowed at max_depth=2".into(),
subagent_type: "explore".into(),
run_in_background: true,
capability_mode: None,
isolation: None,
resume_from: None,
cwd: None,
model: None,
task_id: None,
},
)
.await;
assert!(
result.is_ok(),
"expected Ok at depth 1 with max 2: {result:?}"
);
let _ = rx.try_recv();
}
#[tokio::test]
async fn subagent_cannot_spawn_nested_subagent() {
let (backend, _rx) = make_backend();

View file

@ -10,7 +10,8 @@
//!
//! - `SubagentBackendResource` — wraps an `Arc<dyn SubagentBackend>` that
//! abstracts spawn/query/cancel (see [`super::backend`])
//! - `SubagentDepthCounter` — tracks nesting depth (max 1, no recursive spawning)
//! - `SubagentDepthCounter` — current nesting depth
//! - `MaxSubagentDepth` — configured max nesting depth
//! - `SessionIdResource` — carries the current session ID for parent scoping
//! - `TaskModelValidator` — validates explicit model slugs before background spawn
//!
@ -877,51 +878,6 @@ pub struct SubagentEventSender(#[educe(Debug(ignore))] pub mpsc::UnboundedSender
register_resource!("grok_build", "SubagentEventSender", SubagentEventSender);
// Mid-turn monitor event buffer
/// A monitor event notification to be surfaced as a `<system-reminder>` mid-turn.
#[derive(Debug, Clone)]
pub struct MonitorEventNotification {
pub task_id: String,
pub event_text: String,
/// Session that owns the monitor which produced this event.
///
/// In leader mode every session shares one [`MonitorEventBuffer`], so the
/// drain sites filter on this to avoid surfacing one session's monitor
/// events inside another session's turn. `None` for legacy / non-grok-build
/// backends, which any session drains for backwards compatibility.
pub owner_session_id: Option<String>,
}
impl MonitorEventNotification {
/// Whether this buffered event should surface in the session whose owner id
/// is `my_owner`. Mirrors `task_owned_by_session`: an event surfaces only
/// when it has no recorded owner (legacy) or its owner matches the draining
/// session. Foreign events stay buffered for their own session to drain.
pub fn owned_by_session(&self, my_owner: Option<&str>) -> bool {
match (my_owner, self.owner_session_id.as_deref()) {
(Some(me), Some(owner)) => me == owner,
_ => true,
}
}
}
/// Shared buffer for mid-turn monitor event notifications: an [`EventQueue`]
/// of [`MonitorEventNotification`]. Producers `push_capped`; the turn loop
/// drains its session's events via [`drain_owned`].
pub type MonitorEventBuffer = xai_interjection_core::EventQueue<MonitorEventNotification>;
register_resource!("grok_build", "MonitorEventBuffer", MonitorEventBuffer);
/// Drain only `my_owner`'s events (the buffer is shared across sessions in
/// leader mode); owner-less legacy events drain anywhere.
pub fn drain_owned(
buffer: &MonitorEventBuffer,
my_owner: Option<&str>,
) -> Vec<MonitorEventNotification> {
buffer.drain_matching(|e| e.owned_by_session(my_owner))
}
// Active subagent listing (compaction)
/// Lightweight summary of a running subagent.
@ -952,15 +908,18 @@ pub struct SubagentListActiveRequest {
pub respond_to: oneshot::Sender<Vec<ActiveSubagentSummary>>,
}
/// Tracks nesting depth. Injected into child's Resources with depth+1.
///
/// Top-level sessions start at depth 0. Each child increments by 1.
/// `TaskTool` rejects spawns when `depth >= MAX_SUBAGENT_DEPTH`.
/// Current nesting depth (top-level = 0; child = parent + 1).
#[derive(Debug, Clone)]
pub struct SubagentDepthCounter(pub u32);
register_resource!("grok_build", "SubagentDepthCounter", SubagentDepthCounter);
/// Host-injected max nesting depth; absent → [`super::MAX_SUBAGENT_DEPTH`].
#[derive(Debug, Clone, Copy)]
pub struct MaxSubagentDepth(pub u32);
register_resource!("grok_build", "MaxSubagentDepth", MaxSubagentDepth);
/// Session-scoped validator for model-facing `Task.model` arguments.
///
/// Returns an error message for an invalid slug and `None` for a valid slug.

View file

@ -917,6 +917,7 @@ pub(crate) mod test_helpers {
explicitly_killed: false,
owner_session_id: None,
description: None,
is_backgrounded: false,
}
}

View file

@ -1,7 +1,6 @@
use crate::types::requirements::{Expr, ToolRequirement};
use crate::types::tool::{ToolKind, ToolNamespace};
use super::task::MAX_SUBAGENT_DEPTH;
use super::task::types::SubagentDepthCounter;
pub use xai_grok_tools_api::slash_commands::WORKFLOW_TOOL_NAME;
@ -237,7 +236,8 @@ impl xai_tool_runtime::Tool for WorkflowTool {
(depth, sender)
};
if depth >= MAX_SUBAGENT_DEPTH {
// Workflows stay top-level-only regardless of configurable subagent depth.
if depth > 0 {
return Err(xai_tool_runtime::ToolError::custom(
"workflow_depth_exceeded",
"Workflows can only be launched from a top-level session (subagents and \

View file

@ -108,6 +108,7 @@ mod tests {
explicitly_killed: false,
owner_session_id: None,
description: None,
is_backgrounded: false,
}
}