Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion src/apps/cli/src/agent/runtime_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2693,6 +2693,7 @@ mod tests {
subagent_type: "general".to_string(),
}),
display_metadata: serde_json::Map::new(),
permission_mode: None,
},
};
project_routed_permission_event(&mut permission, "root");
Expand Down Expand Up @@ -3320,6 +3321,7 @@ mod dual_backend_behavior_tests {
},
delegation: None,
display_metadata: serde_json::Map::new(),
permission_mode: None,
}
}

Expand Down Expand Up @@ -3418,7 +3420,10 @@ mod dual_backend_behavior_tests {
.await;
let permission_seen = true;
client
.respond_permission(&request.request_id, PermissionReply::Once)
.respond_permission(
&request.request_id,
PermissionReply::Once { feedback: None },
)
.await
.expect("respond permission");
wait_until(|| {
Expand Down
1 change: 1 addition & 0 deletions src/apps/cli/src/chat_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,7 @@ mod tests {
subagent_type: "Explore".to_string(),
}),
display_metadata: serde_json::Map::new(),
permission_mode: None,
}
}

Expand Down
9 changes: 5 additions & 4 deletions src/apps/cli/src/dispatch/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2181,6 +2181,7 @@ mod tests {
},
delegation: None,
display_metadata: Map::new(),
permission_mode: None,
}
}

Expand Down Expand Up @@ -2477,14 +2478,14 @@ mod tests {
.save_permission_answer(
"job-permission",
&permission.request_id,
PermissionReply::Once,
PermissionReply::Once { feedback: None },
)
.expect("first answer"));
assert!(store
.save_permission_answer(
"job-permission",
&permission.request_id,
PermissionReply::Once,
PermissionReply::Once { feedback: None },
)
.expect("retry pending answer"));
let answer = store
Expand All @@ -2499,14 +2500,14 @@ mod tests {
.save_permission_answer(
"job-permission",
&permission.request_id,
PermissionReply::Once,
PermissionReply::Once { feedback: None },
)
.expect("retry resolved answer"));
assert!(store
.save_permission_answer(
"job-permission",
&permission.request_id,
PermissionReply::Always,
PermissionReply::Always { feedback: None },
)
.is_err());
}
Expand Down
5 changes: 4 additions & 1 deletion src/apps/cli/src/modes/exec/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,10 @@ impl ExecMode {
) -> Self {
let approval_mode = match runtime.approval_policy() {
crate::runtime::approval::CliApprovalPolicy::Auto => ExecApprovalMode::Auto,
crate::runtime::approval::CliApprovalPolicy::Ask
// Non-interactive execution cannot confirm AI-judge escalations,
// so requests that reach the user prompt are rejected.
crate::runtime::approval::CliApprovalPolicy::AiAuto
| crate::runtime::approval::CliApprovalPolicy::Ask
| crate::runtime::approval::CliApprovalPolicy::DisableAuto
| crate::runtime::approval::CliApprovalPolicy::Reject => ExecApprovalMode::Reject,
};
Expand Down
1 change: 1 addition & 0 deletions src/apps/cli/src/modes/exec/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ fn delegated_permission_request() -> PermissionRequest {
subagent_type: "Explore".to_string(),
}),
display_metadata: serde_json::Map::new(),
permission_mode: None,
}
}

Expand Down
14 changes: 12 additions & 2 deletions src/apps/cli/src/peer_host/commands/permission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ use crate::peer_host::state::PeerHostState;

fn permission_reply(request: &Value) -> Result<PermissionReply, String> {
match get_string(request, "reply")?.as_str() {
"once" => Ok(PermissionReply::Once),
"always" => Ok(PermissionReply::Always),
"once" => Ok(PermissionReply::Once {
feedback: request
.get("feedback")
.and_then(Value::as_str)
.map(str::to_string),
}),
"always" => Ok(PermissionReply::Always {
feedback: request
.get("feedback")
.and_then(Value::as_str)
.map(str::to_string),
}),
"reject" => Ok(PermissionReply::Reject {
feedback: request
.get("feedback")
Expand Down
33 changes: 33 additions & 0 deletions src/apps/cli/src/peer_host/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,11 @@ pub(crate) fn peer_host_state() -> Result<&'static PeerHostState, String> {
mod tests {
use std::collections::HashSet;

use bitfun_agent_runtime::sdk::{
PermissionDelegationContext, PermissionRequest, PermissionRequestSource,
PermissionRequestSourceKind,
};

use super::{aggregate_cancellation_results, PeerTurnKey, PeerTurnTracker};

fn register_background_child(
Expand All @@ -1089,6 +1094,34 @@ mod tests {
.expect("register background child"));
}

fn permission_request(session_id: &str, parent_session_id: Option<&str>) -> PermissionRequest {
PermissionRequest {
request_id: format!("request-{session_id}"),
round_id: format!("synthetic:request-{session_id}"),
order: 0,
tool_call_id: Some("tool-call".to_string()),
project_path: None,
project_id: "project".to_string(),
session_id: session_id.to_string(),
agent_id: "Explore".to_string(),
action: "read".to_string(),
resources: vec!["README.md".to_string()],
save_resources: Vec::new(),
source: PermissionRequestSource {
kind: PermissionRequestSourceKind::ToolCall,
identity: "Read".to_string(),
},
delegation: parent_session_id.map(|parent_session_id| PermissionDelegationContext {
parent_session_id: parent_session_id.to_string(),
parent_dialog_turn_id: Some("parent-turn".to_string()),
parent_tool_call_id: "parent-task".to_string(),
subagent_type: "Explore".to_string(),
}),
display_metadata: serde_json::Map::new(),
permission_mode: None,
}
}

#[test]
fn detach_reports_any_unconfirmed_cancellation_round() {
assert!(aggregate_cancellation_results(Ok(()), Ok(())).is_ok());
Expand Down
35 changes: 31 additions & 4 deletions src/apps/cli/src/runtime/approval.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use bitfun_agent_runtime::permission::PERMISSION_MODE_CONTEXT_KEY;
use bitfun_agent_runtime::sdk::{PermissionRequest, AUTO_APPROVE_ASK_CONTEXT_KEY};
use bitfun_agent_runtime::permission::{
AI_AUTO_APPROVE_ASK_CONTEXT_KEY, AUTO_APPROVE_ASK_CONTEXT_KEY, PERMISSION_MODE_CONTEXT_KEY,
};
use bitfun_agent_runtime::sdk::PermissionRequest;
use bitfun_agent_runtime::user_questions::USER_INPUT_AVAILABLE_CONTEXT_KEY;
use bitfun_runtime_ports::PermissionMode;
use serde_json::{Map, Value};
Expand All @@ -12,6 +14,9 @@ pub(crate) enum CliApprovalPolicy {
DisableAuto,
Reject,
Auto,
/// Let the fast-model permission judge decide: safe requests auto-approve,
/// critical-risk requests are rejected, the rest escalate to the user.
AiAuto,
}

/// Build invocation-scoped approval metadata consumed by the shared Runtime.
Expand All @@ -30,7 +35,7 @@ pub(crate) fn approval_metadata(approval_policy: CliApprovalPolicy) -> Map<Strin
);
}
let auto_approve_ask = match approval_policy {
CliApprovalPolicy::Ask => None,
CliApprovalPolicy::Ask | CliApprovalPolicy::AiAuto => None,
CliApprovalPolicy::DisableAuto | CliApprovalPolicy::Reject => Some(false),
CliApprovalPolicy::Auto => Some(true),
};
Expand All @@ -55,6 +60,18 @@ pub(crate) fn approval_metadata(approval_policy: CliApprovalPolicy) -> Map<Strin
),
);
}
if matches!(approval_policy, CliApprovalPolicy::AiAuto) {
// The AI mode resolves through the same single value as every other
// surface. The legacy flag is kept so older runtimes still honor it.
metadata.insert(
PERMISSION_MODE_CONTEXT_KEY.to_string(),
Value::String(PermissionMode::AiAutoApprove.as_str().to_string()),
);
metadata.insert(
AI_AUTO_APPROVE_ASK_CONTEXT_KEY.to_string(),
Value::Bool(true),
);
}
metadata
}

Expand All @@ -75,7 +92,7 @@ mod tests {
use bitfun_agent_runtime::permission::PERMISSION_MODE_CONTEXT_KEY;
use bitfun_agent_runtime::sdk::{
PermissionDelegationContext, PermissionRequest, PermissionRequestSource,
PermissionRequestSourceKind, AUTO_APPROVE_ASK_CONTEXT_KEY,
PermissionRequestSourceKind, AI_AUTO_APPROVE_ASK_CONTEXT_KEY, AUTO_APPROVE_ASK_CONTEXT_KEY,
};
use bitfun_agent_runtime::user_questions::USER_INPUT_AVAILABLE_CONTEXT_KEY;
use serde_json::Map;
Expand Down Expand Up @@ -104,6 +121,7 @@ mod tests {
subagent_type: "Explore".to_string(),
}),
display_metadata: Map::new(),
permission_mode: None,
}
}

Expand Down Expand Up @@ -152,6 +170,15 @@ mod tests {
approval_metadata(CliApprovalPolicy::DisableAuto).get(AUTO_APPROVE_ASK_CONTEXT_KEY),
Some(&serde_json::Value::Bool(false))
);

let ai_auto = approval_metadata(CliApprovalPolicy::AiAuto);
assert_eq!(
ai_auto.get(AI_AUTO_APPROVE_ASK_CONTEXT_KEY),
Some(&serde_json::Value::Bool(true))
);
// AI judging keeps interactive escalation possible.
assert!(ai_auto.get(USER_INPUT_AVAILABLE_CONTEXT_KEY).is_none());
assert!(ai_auto.get(AUTO_APPROVE_ASK_CONTEXT_KEY).is_none());
}

#[test]
Expand Down
3 changes: 2 additions & 1 deletion src/apps/cli/src/shared_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1662,6 +1662,7 @@ mod tests {
subagent_type: "general".to_string(),
}),
display_metadata: serde_json::Map::new(),
permission_mode: None,
}
}

Expand Down Expand Up @@ -1865,7 +1866,7 @@ mod tests {
},
PermissionRequestEvent::Replied {
request_id: request.request_id,
reply: bitfun_agent_runtime::sdk::PermissionReply::Once,
reply: bitfun_agent_runtime::sdk::PermissionReply::Once { feedback: None },
source: PermissionReplySource::User,
},
];
Expand Down
7 changes: 4 additions & 3 deletions src/apps/cli/src/ui/permission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ impl PermissionPrompt {
}
KeyCode::Esc => PermissionAction::Reply(PermissionReply::Reject { feedback: None }),
KeyCode::Enter => match self.selected_option {
0 => PermissionAction::Reply(PermissionReply::Once),
1 => PermissionAction::Reply(PermissionReply::Always),
0 => PermissionAction::Reply(PermissionReply::Once { feedback: None }),
1 => PermissionAction::Reply(PermissionReply::Always { feedback: None }),
_ => {
self.editing_reject_feedback = true;
PermissionAction::None
Expand Down Expand Up @@ -310,6 +310,7 @@ mod tests {
},
delegation: None,
display_metadata: Map::new(),
permission_mode: None,
}
}

Expand All @@ -320,7 +321,7 @@ mod tests {

assert_eq!(
prompt.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
PermissionAction::Reply(PermissionReply::Always)
PermissionAction::Reply(PermissionReply::Always { feedback: None })
);
}

Expand Down
Loading