Skip to content

Commit edaf5fb

Browse files
committed
feat(acp): add frontend-controlled permission mode
1 parent 7bef7be commit edaf5fb

11 files changed

Lines changed: 271 additions & 32 deletions

File tree

anycode-backend/src/acp.rs

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use agent_client_protocol_schema::ProtocolVersion;
33
use serde::{Deserialize, Serialize};
44
use serde_json::Value;
55
use std::collections::HashMap;
6-
use std::sync::atomic::{AtomicBool, Ordering};
6+
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
77
use std::sync::Arc;
88
use std::path::PathBuf;
99
use tokio::process::Command;
@@ -15,6 +15,58 @@ use anyhow::{Result, anyhow};
1515
use crate::utils::relative_to_current_dir;
1616
use crate::acp_history::AcpHistoryManager;
1717

18+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19+
#[repr(u8)]
20+
pub enum AcpPermissionMode {
21+
Ask = 0,
22+
FullAccess = 1,
23+
}
24+
25+
impl AcpPermissionMode {
26+
pub fn from_env() -> Self {
27+
let raw = std::env::var("ANYCODE_ACP_PERMISSION_MODE")
28+
.unwrap_or_else(|_| "full_access".to_string());
29+
Self::from_str(&raw).unwrap_or_else(|| {
30+
error!(
31+
"Unknown ANYCODE_ACP_PERMISSION_MODE value '{}', defaulting to full_access",
32+
raw
33+
);
34+
Self::FullAccess
35+
})
36+
}
37+
38+
pub fn from_str(value: &str) -> Option<Self> {
39+
match value.to_lowercase().as_str() {
40+
"ask" => Some(Self::Ask),
41+
"full_access" | "full-access" | "fullaccess" => Some(Self::FullAccess),
42+
_ => None,
43+
}
44+
}
45+
46+
pub fn as_str(self) -> &'static str {
47+
match self {
48+
Self::Ask => "ask",
49+
Self::FullAccess => "full_access",
50+
}
51+
}
52+
53+
pub fn is_full_access(self) -> bool {
54+
matches!(self, Self::FullAccess)
55+
}
56+
57+
pub fn as_atomic(self) -> u8 {
58+
self as u8
59+
}
60+
61+
pub fn from_atomic(value: u8) -> Self {
62+
match value {
63+
0 => Self::Ask,
64+
1 => Self::FullAccess,
65+
_ => Self::FullAccess,
66+
}
67+
}
68+
}
69+
1870
#[derive(Debug, Clone, Serialize, Deserialize)]
1971
pub struct AcpUserMessage {
2072
pub content: String,
@@ -130,6 +182,7 @@ pub struct PermissionResponse {
130182

131183
struct AcpClientImpl {
132184
agent_id: String,
185+
permission_mode: Arc<AtomicU8>,
133186
message_sender: broadcast::Sender<AcpMessage>,
134187
history: Arc<tokio::sync::Mutex<Vec<AcpMessage>>>,
135188
/// Pending permission requests waiting for user response
@@ -143,6 +196,40 @@ impl Client for AcpClientImpl {
143196
args: acp::RequestPermissionRequest,
144197
) -> acp::Result<acp::RequestPermissionResponse> {
145198
info!("request_permission called for agent {}: {:?}", self.agent_id, args);
199+
200+
let permission_mode = AcpPermissionMode::from_atomic(self.permission_mode.load(Ordering::Relaxed));
201+
if permission_mode.is_full_access() {
202+
let selected_option = args.options.iter().find(|opt| {
203+
let name = opt.name.to_lowercase();
204+
name.contains("allow")
205+
|| name.contains("approve")
206+
|| name.contains("accept")
207+
|| name.contains("grant")
208+
|| name.contains("yes")
209+
|| name.contains("continue")
210+
|| name.contains("proceed")
211+
}).or_else(|| args.options.first());
212+
213+
if let Some(option) = selected_option {
214+
info!(
215+
"Auto-approving permission for agent {} in full_access mode: {}",
216+
self.agent_id,
217+
option.name
218+
);
219+
let selected_outcome = acp::SelectedPermissionOutcome::new(option.option_id.clone());
220+
return Ok(acp::RequestPermissionResponse::new(
221+
acp::RequestPermissionOutcome::Selected(selected_outcome),
222+
));
223+
}
224+
225+
error!(
226+
"Full access mode but no permission options were returned for agent {}",
227+
self.agent_id
228+
);
229+
return Ok(acp::RequestPermissionResponse::new(
230+
acp::RequestPermissionOutcome::Cancelled,
231+
));
232+
}
146233

147234
// Handle tool_call if present
148235
let tool_call_update = &args.tool_call;
@@ -729,6 +816,7 @@ pub type PendingPermissionsMap = Arc<tokio::sync::Mutex<HashMap<String, tokio::s
729816
pub struct AcpAgent {
730817
agent_id: String,
731818
agent_name: String,
819+
permission_mode: Arc<AtomicU8>,
732820
connection: Option<acp::ClientSideConnection>,
733821
session_id: Option<acp::SessionId>,
734822
ready: Arc<AtomicBool>,
@@ -744,7 +832,7 @@ pub struct AcpAgent {
744832
}
745833

746834
impl AcpAgent {
747-
pub fn new(agent_id: String, agent_name: String) -> Self {
835+
pub fn new(agent_id: String, agent_name: String, permission_mode: Arc<AtomicU8>) -> Self {
748836
// Initialize history manager with current working directory and agent ID
749837
let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
750838
let mut history_manager = AcpHistoryManager::new(&project_root, &agent_id);
@@ -755,6 +843,7 @@ impl AcpAgent {
755843
Self {
756844
agent_id,
757845
agent_name,
846+
permission_mode,
758847
connection: None,
759848
session_id: None,
760849
ready: Arc::new(AtomicBool::new(false)),
@@ -794,6 +883,7 @@ impl AcpAgent {
794883
let history_clone = self.history.clone();
795884
let message_sender_clone = history_tx.clone();
796885
let pending_permissions_clone = self.pending_permissions.clone();
886+
let permission_mode_clone = self.permission_mode.clone();
797887

798888
let local_set_handle = tokio::task::spawn_blocking(move || {
799889
tokio::runtime::Handle::current().block_on(async {
@@ -805,6 +895,7 @@ impl AcpAgent {
805895
history_clone,
806896
message_sender_clone,
807897
pending_permissions_clone,
898+
permission_mode_clone,
808899
stdin,
809900
stdout,
810901
stderr,
@@ -864,6 +955,7 @@ impl AcpAgent {
864955
history: Arc<tokio::sync::Mutex<Vec<AcpMessage>>>,
865956
message_sender: broadcast::Sender<AcpMessage>,
866957
pending_permissions: PendingPermissionsMap,
958+
permission_mode: Arc<AtomicU8>,
867959
stdin: tokio::process::ChildStdin,
868960
stdout: tokio::process::ChildStdout,
869961
stderr: tokio::process::ChildStderr,
@@ -878,6 +970,7 @@ impl AcpAgent {
878970
// Create client implementation
879971
let client_impl = AcpClientImpl {
880972
agent_id: agent_id.clone(),
973+
permission_mode,
881974
message_sender: message_sender.clone(),
882975
history,
883976
pending_permissions,
@@ -1247,15 +1340,25 @@ impl AcpAgent {
12471340

12481341
pub struct AcpManager {
12491342
agents: HashMap<String, AcpAgent>,
1343+
permission_mode: Arc<AtomicU8>,
12501344
}
12511345

12521346
impl AcpManager {
1253-
pub fn new() -> Self {
1347+
pub fn new(permission_mode: AcpPermissionMode) -> Self {
12541348
Self {
12551349
agents: HashMap::new(),
1350+
permission_mode: Arc::new(AtomicU8::new(permission_mode.as_atomic())),
12561351
}
12571352
}
12581353

1354+
pub fn get_permission_mode(&self) -> AcpPermissionMode {
1355+
AcpPermissionMode::from_atomic(self.permission_mode.load(Ordering::Relaxed))
1356+
}
1357+
1358+
pub fn set_permission_mode(&self, mode: AcpPermissionMode) {
1359+
self.permission_mode.store(mode.as_atomic(), Ordering::Relaxed);
1360+
}
1361+
12591362
/// Start agent by agent_id and agent_name. Returns an error if the agent already exists.
12601363
pub async fn start_agent(
12611364
&mut self, agent_id: String, agent_name: String, cmd: &str, args: &[String],
@@ -1264,7 +1367,7 @@ impl AcpManager {
12641367
return Err(anyhow::anyhow!("Agent {} already running", agent_id));
12651368
}
12661369

1267-
let mut agent = AcpAgent::new(agent_id.clone(), agent_name.clone());
1370+
let mut agent = AcpAgent::new(agent_id.clone(), agent_name.clone(), self.permission_mode.clone());
12681371

12691372
info!("Starting ACP agent {} with command: {} {:?}", agent_id, cmd, args);
12701373
agent.start(cmd, args).await?;

anycode-backend/src/handlers/acp_handler.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use tracing::{info, error};
55
use anyhow::anyhow;
66
use crate::app_state::AppState;
77
use crate::error_ack;
8-
use crate::acp::AcpMessage;
8+
use crate::acp::{AcpMessage, AcpPermissionMode};
99
use tokio::sync::broadcast;
1010

1111
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -199,6 +199,11 @@ pub struct AcpPermissionResponseRequest {
199199
pub option_id: String,
200200
}
201201

202+
#[derive(Debug, Serialize, Deserialize, Clone)]
203+
pub struct AcpPermissionModeRequest {
204+
pub mode: String,
205+
}
206+
202207
pub async fn handle_acp_permission_response(
203208
Data(request): Data<AcpPermissionResponseRequest>,
204209
ack: AckSender,
@@ -232,6 +237,28 @@ pub async fn handle_acp_permission_response(
232237
}
233238
}
234239

240+
pub async fn handle_acp_permission_mode(
241+
Data(request): Data<AcpPermissionModeRequest>,
242+
ack: AckSender,
243+
state: State<AppState>
244+
) {
245+
info!("handle_acp_permission_mode {:?}", request);
246+
let AcpPermissionModeRequest { mode } = request;
247+
248+
let permission_mode = match AcpPermissionMode::from_str(&mode) {
249+
Some(mode) => mode,
250+
None => {
251+
error!("Invalid ACP permission mode requested: {}", mode);
252+
error_ack!(ack, &mode, "Invalid ACP permission mode: {}", mode);
253+
}
254+
};
255+
256+
let acp_manager = state.acp_manager.lock().await;
257+
acp_manager.set_permission_mode(permission_mode);
258+
259+
ack.send(&json!({ "success": true, "mode": permission_mode.as_str() })).ok();
260+
}
261+
235262
pub async fn handle_acp_reconnect(
236263
socket: SocketRef,
237264
ack: AckSender,

anycode-backend/src/main.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ mod acp;
1919
mod acp_history;
2020
mod git;
2121
use lsp::LspManager;
22-
use acp::AcpManager;
22+
use acp::{AcpManager, AcpPermissionMode};
2323
use git::GitManager;
2424

2525
use std::sync::Arc;
@@ -81,6 +81,7 @@ async fn on_connect(socket: SocketRef, state: State<AppState>) {
8181
socket.on("acp:list", handle_acp_list);
8282
socket.on("acp:reconnect", handle_acp_reconnect);
8383
socket.on("acp:permission_response", handle_acp_permission_response);
84+
socket.on("acp:set_permission_mode", handle_acp_permission_mode);
8485
socket.on("acp:undo", handle_acp_undo);
8586

8687
socket.on("git:status", handle_git_status);
@@ -165,13 +166,14 @@ async fn on_disconnect(socket: SocketRef, state: State<AppState>) {
165166

166167
fn build_app_state() -> (AppState, Receiver<PublishDiagnosticsParams>) {
167168
let config = crate::config::get();
169+
let acp_permission_mode = AcpPermissionMode::from_env();
168170

169171
let (diagnostic_send, diagnostic_recv) = mpsc::channel::<PublishDiagnosticsParams>(1);
170172
let mut lsp_manager = LspManager::new(config.clone());
171173
lsp_manager.set_diagnostics_sender(diagnostic_send);
172174

173175
let lsp_manager = Arc::new(Mutex::new(lsp_manager));
174-
let acp_manager = Arc::new(Mutex::new(AcpManager::new()));
176+
let acp_manager = Arc::new(Mutex::new(AcpManager::new(acp_permission_mode)));
175177
let git_manager = Arc::new(Mutex::new(GitManager::new(crate::utils::current_dir())));
176178

177179
let file2code = Arc::new(Mutex::new(HashMap::new()));
@@ -235,6 +237,7 @@ fn print_help() {
235237
println!("ENVIRONMENT:");
236238
println!(" ANYCODE_PORT Port to listen on (default: 3000)");
237239
println!(" ANYCODE_HOME Path to configuration directory");
240+
println!(" ANYCODE_ACP_PERMISSION_MODE ACP permission mode: full_access (default) or ask");
238241
println!();
239242
println!("Start the anycode server. The server will be available at http://localhost:<port>");
240243
}

anycode/App.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {
2727
loadFollowEnabled,
2828
loadLeftPanelVisible,
2929
loadRightPanelVisible,
30+
loadAcpPermissionMode,
31+
saveAcpPermissionMode,
3032
saveItem,
3133
} from './storage';
3234
import { useSocket } from './hooks/useSocket';
@@ -36,6 +38,7 @@ import { useFileTree } from './hooks/useFileTree';
3638
import { useTerminals } from './hooks/useTerminals';
3739
import { useEditors } from './hooks/useEditors';
3840
import { useAgents } from './hooks/useAgents';
41+
import { type AcpPermissionMode } from './types';
3942

4043
const App: React.FC = () => {
4144
const [leftPanelVisible, setLeftPanelVisible] = useState<boolean>(loadLeftPanelVisible());
@@ -46,6 +49,7 @@ const App: React.FC = () => {
4649
const [leftPanelMode, setLeftPanelMode] = useState<'files' | 'changes' | 'search'>('files');
4750
const [diffEnabled, setDiffEnabled] = useState<boolean>(loadDiffEnabled());
4851
const [followEnabled, setFollowEnabled] = useState<boolean>(loadFollowEnabled());
52+
const [permissionMode, setPermissionMode] = useState<AcpPermissionMode>(loadAcpPermissionMode());
4953

5054
const { wsRef, isConnected } = useSocket({});
5155

@@ -179,6 +183,16 @@ const App: React.FC = () => {
179183
saveItem('followEnabled', followEnabled);
180184
}, [followEnabled]);
181185

186+
useEffect(() => {
187+
saveAcpPermissionMode(permissionMode);
188+
}, [permissionMode]);
189+
190+
useEffect(() => {
191+
if (!isConnected || !wsRef.current) return;
192+
193+
wsRef.current.emit('acp:set_permission_mode', { mode: permissionMode });
194+
}, [isConnected, permissionMode, wsRef]);
195+
182196
useEffect(() => {
183197
saveItem('terminals', terminals.terminals);
184198
}, [terminals.terminals]);
@@ -247,8 +261,9 @@ const App: React.FC = () => {
247261
setFollowEnabled((prev) => !prev);
248262
};
249263

250-
const handleSaveAgents = (agentList: AcpAgent[], defaultAgentId: string | null) => {
264+
const handleSaveAgents = (agentList: AcpAgent[], defaultAgentId: string | null, nextPermissionMode: AcpPermissionMode) => {
251265
updateAgents(agentList, defaultAgentId);
266+
setPermissionMode(nextPermissionMode);
252267
agents.setAgentsVersion((prev) => prev + 1);
253268
};
254269

@@ -371,7 +386,6 @@ const App: React.FC = () => {
371386
onClose={() => setRightPanelVisible(false)}
372387
onSendPrompt={agents.sendPrompt}
373388
onCancelPrompt={agents.cancelPrompt}
374-
onPermissionResponse={agents.sendPermissionResponse}
375389
onUndoPrompt={agents.undoPrompt}
376390
messages={currentSession?.messages || []}
377391
toolCalls={[]}
@@ -380,12 +394,14 @@ const App: React.FC = () => {
380394
showSettings={agents.isAgentSettingsOpen}
381395
settingsAgents={agents.isAgentSettingsOpen ? getAllAgents() : []}
382396
settingsDefaultAgentId={agents.isAgentSettingsOpen ? getDefaultAgentId() : null}
397+
settingsPermissionMode={permissionMode}
383398
onSaveSettings={handleSaveAgents}
384399
onCloseSettings={() => agents.setIsAgentSettingsOpen(false)}
385400
diffEnabled={diffEnabled}
386401
onToggleDiff={toggleDiffMode}
387402
followEnabled={followEnabled}
388403
onToggleFollow={toggleFollowMode}
404+
onPermissionResponse={agents.sendPermissionResponse}
389405
/>
390406
);
391407
})();

0 commit comments

Comments
 (0)