Skip to content

Commit 7003323

Browse files
ZhiXiao-LinRoyLin
andauthored
Fix #5: Add sub-agent event streaming and fine-grained permissive control (#6)
This commit addresses both issues raised in #5: ## Issue 1: Sub-agent event streaming - Added `event_tx` field to `SubAgentHandle` to enable event subscriptions - Added `events()` method to `SubAgentHandle` for filtered event streams - Forward `TextDelta` and `TurnStart` events as `SubAgentInternalEvent` in wrapper - Updated `spawn_subagent()` to pass event transmitter to handle Now users can call `handle.events()` to get a stream of all events for that specific sub-agent, including LLM thinking text, tool calls, and state changes. ## Issue 2: Fine-grained permissive control - Added `permissive_deny` field to `SubAgentConfig` and `AgentSlot` - Added `with_permissive_deny()` builder methods - Modified wrapper to build permissive policy that respects deny rules - When `permissive=true`, deny rules from both config and agent definition are enforced Now users can use `permissive=true` for broad permissions while still blocking specific tools via `permissive_deny` (e.g., ["mcp__longvt__*"]). ## SDK Updates - Updated Python SDK to expose `permissive_deny` parameter - Updated Node.js SDK to expose `permissive_deny` parameter Both SDKs now support the new fine-grained permissive control. Co-authored-by: RoyLin <roylin@RoyLindeMacBook-Pro.local>
1 parent 61bf242 commit 7003323

6 files changed

Lines changed: 95 additions & 4 deletions

File tree

core/src/orchestrator/agent.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ impl AgentOrchestrator {
169169
id.clone(),
170170
config,
171171
control_tx,
172+
self.event_tx.clone(),
172173
state.clone(),
173174
activity.clone(),
174175
task_handle,

core/src/orchestrator/config.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ pub struct SubAgentConfig {
4545
#[serde(default)]
4646
pub permissive: bool,
4747

48+
/// Deny rules to enforce even in permissive mode (e.g., ["mcp__longvt__*"])
49+
#[serde(default)]
50+
pub permissive_deny: Vec<String>,
51+
4852
/// 最大执行步数
4953
pub max_steps: Option<usize>,
5054

@@ -132,6 +136,7 @@ impl SubAgentConfig {
132136
description: String::new(),
133137
prompt: prompt.into(),
134138
permissive: false,
139+
permissive_deny: Vec::new(),
135140
max_steps: None,
136141
timeout_ms: None,
137142
parent_id: None,
@@ -154,6 +159,12 @@ impl SubAgentConfig {
154159
self
155160
}
156161

162+
/// Set deny rules to enforce even in permissive mode
163+
pub fn with_permissive_deny(mut self, deny_rules: Vec<String>) -> Self {
164+
self.permissive_deny = deny_rules;
165+
self
166+
}
167+
157168
/// 设置最大步数
158169
pub fn with_max_steps(mut self, max_steps: usize) -> Self {
159170
self.max_steps = Some(max_steps);
@@ -226,6 +237,10 @@ pub struct AgentSlot {
226237
#[serde(default)]
227238
pub permissive: bool,
228239

240+
/// Deny rules to enforce even in permissive mode
241+
#[serde(default)]
242+
pub permissive_deny: Vec<String>,
243+
229244
/// Maximum execution steps
230245
pub max_steps: Option<usize>,
231246

@@ -262,6 +277,7 @@ impl AgentSlot {
262277
description: String::new(),
263278
prompt: prompt.into(),
264279
permissive: false,
280+
permissive_deny: Vec::new(),
265281
max_steps: None,
266282
timeout_ms: None,
267283
parent_id: None,
@@ -290,6 +306,12 @@ impl AgentSlot {
290306
self
291307
}
292308

309+
/// Set deny rules to enforce even in permissive mode.
310+
pub fn with_permissive_deny(mut self, deny_rules: Vec<String>) -> Self {
311+
self.permissive_deny = deny_rules;
312+
self
313+
}
314+
293315
/// Set the maximum number of execution steps.
294316
pub fn with_max_steps(mut self, max_steps: usize) -> Self {
295317
self.max_steps = Some(max_steps);
@@ -341,6 +363,7 @@ impl From<SubAgentConfig> for AgentSlot {
341363
description: c.description,
342364
prompt: c.prompt,
343365
permissive: c.permissive,
366+
permissive_deny: c.permissive_deny,
344367
max_steps: c.max_steps,
345368
timeout_ms: c.timeout_ms,
346369
parent_id: c.parent_id,
@@ -359,6 +382,7 @@ impl From<AgentSlot> for SubAgentConfig {
359382
description: s.description,
360383
prompt: s.prompt,
361384
permissive: s.permissive,
385+
permissive_deny: s.permissive_deny,
362386
max_steps: s.max_steps,
363387
timeout_ms: s.timeout_ms,
364388
parent_id: s.parent_id,

core/src/orchestrator/handle.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ pub struct SubAgentHandle {
2222
/// 控制信号发送器
2323
control_tx: tokio::sync::mpsc::Sender<ControlSignal>,
2424

25+
/// 事件广播发送器
26+
event_tx: tokio::sync::broadcast::Sender<OrchestratorEvent>,
27+
2528
/// 状态
2629
state: Arc<RwLock<SubAgentState>>,
2730

@@ -39,6 +42,7 @@ impl SubAgentHandle {
3942
id: String,
4043
config: SubAgentConfig,
4144
control_tx: tokio::sync::mpsc::Sender<ControlSignal>,
45+
event_tx: tokio::sync::broadcast::Sender<OrchestratorEvent>,
4246
state: Arc<RwLock<SubAgentState>>,
4347
activity: Arc<RwLock<SubAgentActivity>>,
4448
task_handle: tokio::task::JoinHandle<Result<String>>,
@@ -51,6 +55,7 @@ impl SubAgentHandle {
5155
.unwrap()
5256
.as_millis() as u64,
5357
control_tx,
58+
event_tx,
5459
state,
5560
activity,
5661
task_handle: Arc::new(task_handle),
@@ -169,6 +174,17 @@ impl SubAgentHandle {
169174
pub fn is_paused(&self) -> bool {
170175
self.state().is_paused()
171176
}
177+
178+
/// Subscribe to events for this SubAgent.
179+
///
180+
/// Returns a filtered event stream that only includes events for this SubAgent.
181+
pub fn events(&self) -> crate::orchestrator::SubAgentEventStream {
182+
let rx = self.event_tx.subscribe();
183+
crate::orchestrator::SubAgentEventStream {
184+
rx,
185+
filter_id: self.id.clone(),
186+
}
187+
}
172188
}
173189

174190
impl std::fmt::Debug for SubAgentHandle {

core/src/orchestrator/wrapper.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,27 @@ impl SubAgentWrapper {
115115

116116
// Build session options from SubAgentConfig fields.
117117
let mut opts = crate::SessionOptions::new();
118+
119+
// Handle permissive mode with fine-grained deny control
118120
if self.config.permissive {
119-
opts = opts.with_permissive_policy();
121+
// Build a permissive policy that still respects deny rules
122+
let mut policy = crate::permissions::PermissionPolicy::permissive();
123+
124+
// Add deny rules from permissive_deny config
125+
for rule in &self.config.permissive_deny {
126+
policy = policy.deny(rule);
127+
}
128+
129+
// If we have an agent definition, also add its deny rules
130+
if let Some(def) = registry.get(&self.config.agent_type) {
131+
for rule in &def.permissions.deny {
132+
policy = policy.deny(&rule.rule);
133+
}
134+
}
135+
136+
opts = opts.with_permission_checker(Arc::new(policy));
120137
}
138+
121139
if let Some(steps) = self.config.max_steps {
122140
opts = opts.with_max_tool_rounds(steps);
123141
}
@@ -175,9 +193,16 @@ impl SubAgentWrapper {
175193

176194
// Consume the next agent event.
177195
match rx.recv().await {
178-
Some(AgentEvent::TurnStart { .. }) => {
196+
Some(AgentEvent::TurnStart { turn }) => {
179197
*self.activity.write().await =
180198
SubAgentActivity::RequestingLlm { message_count: 0 };
199+
// Forward as internal event for observability
200+
let _ = self
201+
.event_tx
202+
.send(OrchestratorEvent::SubAgentInternalEvent {
203+
id: self.id.clone(),
204+
event: AgentEvent::TurnStart { turn },
205+
});
181206
}
182207
Some(AgentEvent::ToolStart { id, name }) => {
183208
*self.activity.write().await = SubAgentActivity::CallingTool {
@@ -220,6 +245,13 @@ impl SubAgentWrapper {
220245
}
221246
Some(AgentEvent::TextDelta { text }) => {
222247
output.push_str(&text);
248+
// Forward as internal event for streaming observability
249+
let _ = self
250+
.event_tx
251+
.send(OrchestratorEvent::SubAgentInternalEvent {
252+
id: self.id.clone(),
253+
event: AgentEvent::TextDelta { text },
254+
});
223255
}
224256
Some(AgentEvent::ExternalTaskPending {
225257
task_id,

sdk/node/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2920,6 +2920,8 @@ pub struct SubAgentConfig {
29202920
pub prompt: String,
29212921
/// Enable permissive mode (bypass HITL)
29222922
pub permissive: bool,
2923+
/// Deny rules to enforce even in permissive mode (e.g., ["mcp__longvt__*"])
2924+
pub permissive_deny: Option<Vec<String>>,
29232925
/// Maximum execution steps
29242926
pub max_steps: Option<u32>,
29252927
/// Execution timeout (milliseconds)
@@ -2942,6 +2944,9 @@ impl From<SubAgentConfig> for RustSubAgentConfig {
29422944
config = config.with_description(c.description);
29432945
}
29442946
config = config.with_permissive(c.permissive);
2947+
if let Some(deny) = c.permissive_deny {
2948+
config = config.with_permissive_deny(deny);
2949+
}
29452950
if let Some(steps) = c.max_steps {
29462951
config = config.with_max_steps(steps as usize);
29472952
}
@@ -2981,6 +2986,8 @@ pub struct AgentSlot {
29812986
pub prompt: String,
29822987
/// Enable permissive mode (bypass HITL)
29832988
pub permissive: bool,
2989+
/// Deny rules to enforce even in permissive mode (e.g., ["mcp__longvt__*"])
2990+
pub permissive_deny: Option<Vec<String>>,
29842991
/// Maximum execution steps
29852992
pub max_steps: Option<u32>,
29862993
/// Execution timeout (milliseconds)
@@ -3011,6 +3018,9 @@ impl From<AgentSlot> for RustAgentSlot {
30113018
slot = slot.with_description(s.description);
30123019
}
30133020
slot = slot.with_permissive(s.permissive);
3021+
if let Some(deny) = s.permissive_deny {
3022+
slot = slot.with_permissive_deny(deny);
3023+
}
30143024
if let Some(steps) = s.max_steps {
30153025
slot = slot.with_max_steps(steps as usize);
30163026
}

sdk/python/src/lib.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3726,12 +3726,13 @@ struct PySubAgentConfig {
37263726
#[pymethods]
37273727
impl PySubAgentConfig {
37283728
#[new]
3729-
#[pyo3(signature = (agent_type, prompt, description=None, permissive=false, max_steps=None, timeout_ms=None, parent_id=None, workspace=None, agent_dirs=None, lane_config=None))]
3729+
#[pyo3(signature = (agent_type, prompt, description=None, permissive=false, permissive_deny=None, max_steps=None, timeout_ms=None, parent_id=None, workspace=None, agent_dirs=None, lane_config=None))]
37303730
fn new(
37313731
agent_type: String,
37323732
prompt: String,
37333733
description: Option<String>,
37343734
permissive: bool,
3735+
permissive_deny: Option<Vec<String>>,
37353736
max_steps: Option<usize>,
37363737
timeout_ms: Option<u64>,
37373738
parent_id: Option<String>,
@@ -3744,6 +3745,9 @@ impl PySubAgentConfig {
37443745
config = config.with_description(desc);
37453746
}
37463747
config = config.with_permissive(permissive);
3748+
if let Some(deny) = permissive_deny {
3749+
config = config.with_permissive_deny(deny);
3750+
}
37473751
if let Some(steps) = max_steps {
37483752
config = config.with_max_steps(steps);
37493753
}
@@ -3786,14 +3790,15 @@ struct PyAgentSlot {
37863790
#[pymethods]
37873791
impl PyAgentSlot {
37883792
#[new]
3789-
#[pyo3(signature = (agent_type, prompt, role=None, description=None, permissive=false, max_steps=None, timeout_ms=None, parent_id=None, workspace=None, agent_dirs=None, lane_config=None))]
3793+
#[pyo3(signature = (agent_type, prompt, role=None, description=None, permissive=false, permissive_deny=None, max_steps=None, timeout_ms=None, parent_id=None, workspace=None, agent_dirs=None, lane_config=None))]
37903794
#[allow(clippy::too_many_arguments)]
37913795
fn new(
37923796
agent_type: String,
37933797
prompt: String,
37943798
role: Option<String>,
37953799
description: Option<String>,
37963800
permissive: bool,
3801+
permissive_deny: Option<Vec<String>>,
37973802
max_steps: Option<usize>,
37983803
timeout_ms: Option<u64>,
37993804
parent_id: Option<String>,
@@ -3815,6 +3820,9 @@ impl PyAgentSlot {
38153820
slot = slot.with_description(desc);
38163821
}
38173822
slot = slot.with_permissive(permissive);
3823+
if let Some(deny) = permissive_deny {
3824+
slot = slot.with_permissive_deny(deny);
3825+
}
38183826
if let Some(steps) = max_steps {
38193827
slot = slot.with_max_steps(steps);
38203828
}

0 commit comments

Comments
 (0)