Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
78042a3
fix(channels): gate /sh and privileged tools behind a per-account ope…
claude Jul 26, 2026
74f574f
chore(gateway): delete orphaned channel_events/helpers.rs
claude Jul 26, 2026
311a23e
fix(chat): apply request `_tool_policy` on the async send path
claude Jul 26, 2026
f66124a
style(e2e): apply biome formatting to the channels-operators spec
penso Jul 27, 2026
02d1959
test(e2e): fix the operators tag assertion so the spec can pass
penso Jul 27, 2026
0818099
fix(chat): stop Collect queue replay from merging senders of differen…
penso Jul 27, 2026
8b243eb
refactor(chat): extract the queued-message drain into one shared module
penso Jul 27, 2026
2952bb2
fix(channels): close remaining operator security bypasses
penso Jul 28, 2026
684712a
Merge remote-tracking branch 'origin/main' into claude/moltis-sh-secu…
penso Jul 28, 2026
befe8a0
fix(scripts): normalize targeted test package names
penso Jul 28, 2026
6d90079
fix(chat): detect explicit shell commands with the gateway's own pred…
penso Jul 28, 2026
566b683
feat(channels): scope command privilege to reach, and explain refusals
penso Jul 29, 2026
b0f68c3
feat(channels): make operator an explicit choice when approving a sender
penso Jul 29, 2026
ee8a51b
fix(channels): restrict privileged access to operator DMs
penso Jul 29, 2026
365eebe
fix(channels): keep session-local commands public
penso Jul 29, 2026
7ccd84c
fix(channels): close review gaps in the operator privilege gate
penso Jul 30, 2026
a87a2fd
refactor(gateway): split channel-binding tests out of session/tests.rs
penso Jul 30, 2026
e932b8f
fix(chat): repair tool-call pairing in both directions when filtering…
penso Jul 30, 2026
a616d48
fix(scripts): only treat crate-root tests/ files as integration targets
penso Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions crates/agents/src/lazy_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ use std::sync::Arc;

use {anyhow::Result, async_trait::async_trait, tracing::debug};

use crate::tool_registry::{ActivatedTools, AgentTool, ToolEntry, ToolRegistry, ToolSource};
use crate::tool_registry::{
ActivatedTools, AgentTool, ToolAudience, ToolEntry, ToolRegistry, ToolSource,
};

/// Maximum number of results returned by a keyword search.
const MAX_SEARCH_RESULTS: usize = 15;
Expand Down Expand Up @@ -73,13 +75,21 @@ impl ToolSearchTool {
.full_registry
.get_source(name)
.unwrap_or(ToolSource::Builtin);
let audience = self
.full_registry
.get_audience(name)
.unwrap_or(ToolAudience::Trusted);

let schema = tool.parameters_schema();
let description = tool.description().to_string();

// Insert into activated map, preserving the original source metadata.
let mut activated = self.activated.lock().unwrap_or_else(|e| e.into_inner());
activated.insert(name.to_string(), ToolEntry { tool, source });
activated.insert(name.to_string(), ToolEntry {
tool,
source,
audience,
});

debug!(tool = name, "tool activated via tool_search");

Expand Down Expand Up @@ -192,9 +202,13 @@ impl AgentTool for ToolSearchTool {
/// Wrap a full tool registry for lazy mode.
///
/// Returns a new registry containing only `tool_search`. The model discovers
/// and activates tools from `full` via that meta-tool. Activated tools
/// appear in `list_schemas()` on the next runner iteration.
/// and activates tools from `full` via that meta-tool. Activated tools appear
/// in `list_schemas()` on the next runner iteration. An empty input stays empty
/// so a deny-all request cannot regain a meta-tool after policy filtering.
pub fn wrap_registry_lazy(full: ToolRegistry) -> ToolRegistry {
if full.is_empty() {
return full;
}
let full = Arc::new(full);
let mut lazy_registry = ToolRegistry::new();

Expand All @@ -205,7 +219,9 @@ pub fn wrap_registry_lazy(full: ToolRegistry) -> ToolRegistry {
full_registry: full,
activated,
};
lazy_registry.register(Box::new(search_tool));
// The full registry has already been filtered for this run. Searching it
// cannot discover or activate a tool above the caller's audience ceiling.
lazy_registry.register_public(Box::new(search_tool));
lazy_registry
}

Expand Down Expand Up @@ -300,6 +316,12 @@ mod tests {
assert!(names.contains(&"tool_search".to_string()));
}

#[test]
fn wrap_registry_lazy_keeps_deny_all_registry_empty() {
let lazy = wrap_registry_lazy(ToolRegistry::new());
assert!(lazy.list_names().is_empty());
}

#[tokio::test]
async fn keyword_search_returns_matching_tools() {
let full = build_full_registry();
Expand Down
13 changes: 12 additions & 1 deletion crates/agents/src/runner/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,9 +426,20 @@ pub(crate) fn explicit_shell_command_from_user_content(
user_content: &UserContent,
) -> Option<String> {
let text = match user_content {
UserContent::Text(text) => text.trim(),
UserContent::Text(text) => text,
UserContent::Multimodal(_) => return None,
};
explicit_shell_command(text)
}

/// Detect an explicit `/sh ...` shell request in raw message text.
///
/// Callers that gate shell access (e.g. the gateway's channel dispatch) must
/// use this exact predicate so authorization and execution agree on what
/// counts as a shell request.
#[must_use]
pub fn explicit_shell_command(text: &str) -> Option<String> {
let text = text.trim();

if text.is_empty() || text.len() > 4096 || text.contains('\n') || text.contains('\r') {
return None;
Expand Down
5 changes: 4 additions & 1 deletion crates/agents/src/runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ mod tests_legacy;
// ── Re-exports (preserve public API) ────────────────────────────────────

pub use {
helpers::{AgentLoopLimits, AgentRunError, AgentRunResult, OnEvent, RunnerEvent},
helpers::{
AgentLoopLimits, AgentRunError, AgentRunResult, OnEvent, RunnerEvent,
explicit_shell_command,
},
non_streaming::{
run_agent, run_agent_loop, run_agent_loop_with_context,
run_agent_loop_with_context_and_limits,
Expand Down
Loading