1pub mod components;
4mod config;
5
6use std::{
7 collections::{HashMap, VecDeque},
8 io::Read as _,
9 path::{Path, PathBuf},
10 sync::{
11 Arc,
12 atomic::{AtomicU64, AtomicUsize, Ordering},
13 },
14 time::Duration,
15};
16
17use anyhow::{Context, Result, anyhow};
18use async_trait::async_trait;
19use config::Config;
20pub use config::{ApprovalPolicy, ConfigOverrides};
21pub fn init_user_config() -> anyhow::Result<std::path::PathBuf> {
22 config::Config::init_user_config()
23}
24pub fn update_index_url(workspace: &std::path::Path) -> anyhow::Result<Option<String>> {
25 Ok(config::Config::load(workspace, ConfigOverrides::default())?
26 .update
27 .index_url)
28}
29use scv_core::{
30 AgentError, AgentRuntime, ApprovalGate, ApprovalRequest, BudgetContextPolicy, CoreEvent,
31 EventSink, Message, ToolRegistry, ToolRisk,
32};
33use scv_protocol::{ClientMessage, PROTOCOL_VERSION, PeerInfo, QueueEntry, ServerEvent, Usage};
34use scv_provider_openai::OpenAiProvider;
35use scv_tools::{SkillMap, builtin_registry};
36use tokio::{
37 io::{AsyncBufRead, AsyncBufReadExt, AsyncWriteExt, BufReader},
38 net::{UnixListener, UnixStream},
39 sync::{Mutex, OwnedSemaphorePermit, Semaphore, mpsc, oneshot},
40 task::JoinHandle,
41};
42use tokio_util::sync::CancellationToken;
43use tokio_util::task::TaskTracker;
44use uuid::Uuid;
45
46const PROMPT_LIMIT_BYTES: usize = 256 * 1024;
47const OUTPUT_QUEUE_CAPACITY: usize = 256;
48const OUTPUT_QUEUE_MIN_BYTES: usize = 16 * 1024 * 1024;
49const SHUTDOWN_GRACE: Duration = Duration::from_secs(3);
50const MAX_QUEUE_ITEMS: usize = 64;
51const MAX_QUEUE_BYTES: usize = 4 * 1024 * 1024;
52
53pub async fn run_stdio(overrides: ConfigOverrides) -> Result<()> {
54 let stdin = tokio::io::stdin();
55 let stdout = tokio::io::stdout();
56 let tasks = TaskTracker::new();
57 let result = run_managed(
58 stdin,
59 stdout,
60 overrides,
61 None,
62 CancellationToken::new(),
63 tasks.clone(),
64 )
65 .await;
66 tasks.close();
67 tasks.wait().await;
68 result
69}
70
71pub fn default_socket_path() -> Result<PathBuf> {
73 scv_client::default_socket_path()
74}
75
76pub async fn run_socket(path: &Path, overrides: ConfigOverrides) -> Result<()> {
78 if let Some(parent) = path.parent() {
79 tokio::fs::create_dir_all(parent)
80 .await
81 .context("create SCV socket directory")?;
82 }
83 let _lock = SocketLock::acquire(path)?;
84 if path.exists() {
85 if UnixStream::connect(path).await.is_ok() {
86 return Err(anyhow!(
87 "SCV server is already running at {}",
88 path.display()
89 ));
90 }
91 use std::os::unix::fs::FileTypeExt;
92 if !std::fs::symlink_metadata(path)?.file_type().is_socket() {
93 return Err(anyhow!(
94 "refusing to remove a non-socket at SCV socket path"
95 ));
96 }
97 tokio::fs::remove_file(path)
98 .await
99 .with_context(|| format!("remove stale SCV socket {}", path.display()))?;
100 }
101 let listener = UnixListener::bind(path)
102 .with_context(|| format!("bind SCV server socket {}", path.display()))?;
103 #[cfg(unix)]
104 {
105 use std::os::unix::fs::PermissionsExt;
106 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
107 .context("secure SCV socket")?;
108 }
109 let components = Arc::new(Mutex::new(components::Components::new(
110 path.to_owned(),
111 std::env::current_dir()?,
112 )));
113 let cancellation = CancellationToken::new();
114 let tasks = TaskTracker::new();
115 let mut clients = tokio::task::JoinSet::new();
116 let refresh_components = components.clone();
117 let refresh_cancel = cancellation.clone();
118 let mut refresh_task = tokio::spawn(async move {
119 let mut refresh = tokio::time::interval(Duration::from_secs(2));
120 loop {
121 tokio::select! {
122 biased;
123 _ = refresh_cancel.cancelled() => break,
124 _ = refresh.tick() => {
125 tokio::select! {
126 biased;
127 _ = refresh_cancel.cancelled() => break,
128 result = async { refresh_components.lock().await.reconcile().await } => {
129 if result.is_err() { tracing::warn!("Component account discovery failed"); }
130 }
131 }
132 }
133 }
134 }
135 });
136 let _refresh_abort = AbortGuard(refresh_task.abort_handle());
137 let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
138 let result = loop {
139 tokio::select! {
140 accepted = listener.accept() => {
141 let (stream, _) = match accepted { Ok(value) => value, Err(error) => break Err(error.into()) };
142 let child_overrides = overrides.clone();
143 let components = components.clone();
144 let cancellation = cancellation.clone();
145 let tasks = tasks.clone();
146 clients.spawn(async move {
147 let (reader, writer) = stream.into_split();
148 if run_managed(reader, writer, child_overrides, Some(components), cancellation, tasks).await.is_err() {
149 tracing::warn!("SCV socket client stopped");
150 }
151 });
152 }
153 _ = clients.join_next(), if !clients.is_empty() => {},
154 _ = tokio::signal::ctrl_c() => break Ok(()),
155 _ = terminate.recv() => break Ok(()),
156 }
157 };
158 drop(listener);
159 cancellation.cancel();
160 let _ = (&mut refresh_task).await;
161 components.lock().await.shutdown().await;
162 if tokio::time::timeout(Duration::from_secs(8), async {
163 while clients.join_next().await.is_some() {}
164 })
165 .await
166 .is_err()
167 {
168 clients.abort_all();
169 while clients.join_next().await.is_some() {}
170 }
171 tasks.close();
172 tasks.wait().await;
173 let _ = tokio::fs::remove_file(path).await;
174 result
175}
176
177async fn run_managed<R, W>(
178 reader: R,
179 writer: W,
180 overrides: ConfigOverrides,
181 components: Option<Arc<Mutex<components::Components>>>,
182 cancellation: CancellationToken,
183 tasks: TaskTracker,
184) -> Result<()>
185where
186 R: tokio::io::AsyncRead + Unpin,
187 W: tokio::io::AsyncWrite + Unpin + Send + 'static,
188{
189 let initial_output_bytes =
190 output_queue_bytes(Config::default().protocol.max_server_frame_bytes)?;
191 let (output_tx, mut output_rx) = outbound_channel(initial_output_bytes);
192 let mut writer_task = tasks.spawn(async move {
193 let mut writer = writer;
194 while let Some(frame) = output_rx.recv().await {
195 writer.write_all(&frame.bytes).await?;
196 writer.write_all(b"\n").await?;
197 writer.flush().await?;
198 }
199 Ok::<(), std::io::Error>(())
200 });
201 let _writer_abort = AbortGuard(writer_task.abort_handle());
202 let (done_tx, mut done_rx) = mpsc::channel::<TurnDone>(4);
203 let approvals = Arc::new(ApprovalBroker::default());
204 let mut reader = BufReader::new(reader);
205 let mut frames = FrameBuffer::default();
206 let mut initialized = false;
207 let mut session: Option<Session> = None;
208 let mut active: Option<ActiveTurn> = None;
209 let mut fatal = false;
210 let mut writer_finished = false;
211
212 let loop_result: Result<()> = async {
213 loop {
214 let frame_limit = session.as_ref().map_or_else(
215 || Config::default().protocol.max_client_frame_bytes,
216 |value| value.config.protocol.max_client_frame_bytes,
217 );
218 tokio::select! {
219 _ = cancellation.cancelled() => break,
220 read = frames.read(&mut reader, frame_limit) => {
221 let frame = match read.context("read protocol input")? {
222 FrameRead::Eof => {
223 if let Some(active) = &active { active.cancellation.cancel(); }
224 break;
225 }
226 FrameRead::TooLarge => {
227 send_error(&output_tx, "", "invalid_request", "client frame exceeds configured limit", false, server_frame_limit(&session)).await?;
228 continue;
229 }
230 FrameRead::Frame(frame) => frame,
231 };
232 if frame.is_empty() {
233 send_error(&output_tx, "", "invalid_json", "protocol frame is empty", false, server_frame_limit(&session)).await?;
234 continue;
235 }
236 let message = match serde_json::from_slice::<ClientMessage>(&frame) {
237 Ok(message) => message,
238 Err(error) => {
239 send_error(&output_tx, "", "invalid_json", &format!("invalid protocol JSON: {error}"), false, server_frame_limit(&session)).await?;
240 continue;
241 }
242 };
243 match message {
244 ClientMessage::Initialize { request_id, protocol_version, .. } => {
245 if initialized {
246 send_error(&output_tx, &request_id, "invalid_request", "connection is already initialized", false, server_frame_limit(&session)).await?;
247 continue;
248 }
249 if protocol_version != PROTOCOL_VERSION {
250 send_error(&output_tx, &request_id, "version_mismatch", &format!("server supports protocol {PROTOCOL_VERSION}"), true, server_frame_limit(&session)).await?;
251 fatal = true;
252 break;
253 }
254 initialized = true;
255 send_event(&output_tx, ServerEvent::Initialized {
256 request_id,
257 protocol_version: PROTOCOL_VERSION,
258 server: PeerInfo { name: "scv-server".into(), version: env!("CARGO_PKG_VERSION").into() },
259 }, Config::default().protocol.max_server_frame_bytes).await?;
260 }
261 other if !initialized => {
262 send_error(&output_tx, other.request_id(), "not_initialized", "initialize must be the first message", false, server_frame_limit(&session)).await?;
263 }
264 ClientMessage::DaemonControl { request_id, command } => {
265 if let Some(components) = &components {
266 let result = tokio::select! {
267 biased;
268 _ = cancellation.cancelled() => break,
269 result = async { components.lock().await.control(command).await } => result,
270 };
271 match result {
272 Ok(status) => send_event(&output_tx, ServerEvent::DaemonStatus { request_id, status }, server_frame_limit(&session)).await?,
273 Err(_) => send_error(&output_tx, &request_id, "component_error", "Component operation failed; check account credentials, private file permissions and absolute workspace", false, server_frame_limit(&session)).await?,
274 }
275 } else {
276 send_error(&output_tx, &request_id, "unsupported", "Component management requires the daemon socket", false, server_frame_limit(&session)).await?;
277 }
278 }
279 ClientMessage::SessionStart { request_id, cwd, provider, model, base_url, no_tools } => {
280 if session.is_some() {
281 send_error(&output_tx, &request_id, "invalid_request", "this connection already has a session", false, server_frame_limit(&session)).await?;
282 continue;
283 }
284 let session_overrides = ConfigOverrides {
285 provider: provider.or_else(|| overrides.provider.clone()),
286 model: model.or_else(|| overrides.model.clone()),
287 base_url: base_url.or_else(|| overrides.base_url.clone()),
288 approval_policy: overrides.approval_policy,
289 no_tools: no_tools.unwrap_or(overrides.no_tools),
290 };
291 match build_session(&cwd, session_overrides).await {
292 Ok(new_session) => {
293 output_tx.ensure_capacity(output_queue_bytes(
294 new_session.config.protocol.max_server_frame_bytes,
295 )?)?;
296 let event = ServerEvent::SessionStarted {
297 request_id,
298 session_id: new_session.id.clone(),
299 cwd: new_session.workspace.display().to_string(),
300 model: new_session.runtime.model().to_owned(),
301 context_max_tokens: new_session.config.context.max_tokens,
302 max_server_frame_bytes: new_session.config.protocol.max_server_frame_bytes,
303 max_transcript_bytes: new_session.config.tui.max_transcript_bytes,
304 max_transcript_items: new_session.config.tui.max_transcript_items,
305 max_prompt_history_bytes: new_session.config.tui.max_prompt_history_bytes,
306 max_prompt_history_items: new_session.config.tui.max_prompt_history_items,
307 };
308 send_event(&output_tx, event, new_session.config.protocol.max_server_frame_bytes).await?;
309 send_event(&output_tx, ServerEvent::QueueSnapshot {
310 request_id: None,
311 session_id: new_session.id.clone(),
312 seq: next_seq(&new_session.seq),
313 entries: new_session.queue.lock().await.iter().cloned().collect(),
314 paused: new_session.paused.load(Ordering::Acquire),
315 }, new_session.config.protocol.max_server_frame_bytes).await?;
316 session = Some(new_session);
317 }
318 Err(error) => {
319 send_error(&output_tx, &request_id, "invalid_request", &error.to_string(), false, server_frame_limit(&session)).await?;
320 }
321 }
322 }
323 ClientMessage::SessionAttach { request_id, .. } => {
324 send_error(&output_tx, &request_id, "unsupported", "session attach requires the shared socket server", false, server_frame_limit(&session)).await?;
325 }
326 ClientMessage::TurnStart { request_id, session_id, prompt } => {
327 let Some(current) = session.as_ref() else {
328 send_error(&output_tx, &request_id, "session_not_found", "start a session first", false, server_frame_limit(&session)).await?;
329 continue;
330 };
331 if current.id != session_id {
332 send_error(&output_tx, &request_id, "session_not_found", "session id does not match", false, server_frame_limit(&session)).await?;
333 continue;
334 }
335 if prompt.trim().is_empty() || prompt.len() > PROMPT_LIMIT_BYTES {
336 send_error(&output_tx, &request_id, "invalid_request", "prompt must be non-empty and no larger than 256 KiB", false, server_frame_limit(&session)).await?;
337 continue;
338 }
339 if active.is_some() {
340 let entry = match current.enqueue(prompt, request_id.clone()).await {
341 Ok(entry) => entry,
342 Err(code) => { send_error(&output_tx, &request_id, code, "session queue limit reached", false, server_frame_limit(&session)).await?; continue; }
343 };
344 let position = current.queue.lock().await.len().saturating_sub(1);
345 send_event(&output_tx, ServerEvent::QueueEnqueued {
346 request_id, session_id: current.id.clone(), seq: next_seq(¤t.seq), entry, position,
347 }, current.config.protocol.max_server_frame_bytes).await?;
348 continue;
349 }
350 let turn_id = Uuid::new_v4().to_string();
351 let cancellation = cancellation.child_token();
352 let meta = TurnMeta {
353 request_id: request_id.clone(),
354 session_id: current.id.clone(),
355 turn_id: turn_id.clone(),
356 seq: Arc::clone(¤t.seq),
357 max_server_frame: current.config.protocol.max_server_frame_bytes,
358 };
359 send_event(&output_tx, ServerEvent::TurnStarted {
360 request_id: request_id.clone(),
361 session_id: current.id.clone(),
362 turn_id: turn_id.clone(),
363 seq: next_seq(¤t.seq),
364 }, current.config.protocol.max_server_frame_bytes).await?;
365 let runtime = Arc::clone(¤t.runtime);
366 let history = Arc::clone(¤t.history);
367 let sink: Arc<dyn EventSink> = Arc::new(ProtocolSink {
368 meta: meta.clone(),
369 output: output_tx.clone(),
370 cancellation: cancellation.clone(),
371 });
372 let gate: Arc<dyn ApprovalGate> = Arc::new(ProtocolApprovalGate {
373 policy: current.config.tools.approval_policy,
374 broker: Arc::clone(&approvals),
375 meta,
376 output: output_tx.clone(),
377 });
378 let task_cancel = cancellation.clone();
379 let task_done = done_tx.clone();
380 let task_request = request_id.clone();
381 let task_session = current.id.clone();
382 let task_turn = turn_id.clone();
383 let task = tasks.spawn(async move {
384 let mut history = history.lock().await;
385 let result = runtime.run_turn(&mut history, prompt, sink, gate, task_cancel).await;
386 let _ = task_done.send(TurnDone {
387 request_id: task_request,
388 session_id: task_session,
389 turn_id: task_turn,
390 result,
391 }).await;
392 });
393 active = Some(ActiveTurn { turn_id, cancellation, task });
394 }
395 ClientMessage::QueueUpdate { request_id, session_id, queue_id, revision, prompt } => {
396 let Some(current) = session.as_ref() else { send_error(&output_tx, &request_id, "session_not_found", "start a session first", false, server_frame_limit(&session)).await?; continue; };
397 if current.id != session_id { send_error(&output_tx, &request_id, "session_not_found", "session id does not match", false, server_frame_limit(&session)).await?; continue; }
398 if prompt.trim().is_empty() || prompt.len() > PROMPT_LIMIT_BYTES { send_error(&output_tx, &request_id, "invalid_request", "prompt must be non-empty and no larger than 256 KiB", false, server_frame_limit(&session)).await?; continue; }
399 match current.update_queue(&queue_id, revision, prompt).await {
400 Ok(entry) => send_event(&output_tx, ServerEvent::QueueUpdated { request_id, session_id: current.id.clone(), seq: next_seq(¤t.seq), entry }, current.config.protocol.max_server_frame_bytes).await?,
401 Err(code) => send_error(&output_tx, &request_id, code, "queue entry was not found or revision is stale", false, server_frame_limit(&session)).await?,
402 }
403 }
404 ClientMessage::QueueMove { request_id, session_id, queue_id, revision, before_queue_id } => {
405 let Some(current) = session.as_ref() else { send_error(&output_tx, &request_id, "session_not_found", "start a session first", false, server_frame_limit(&session)).await?; continue; };
406 match current.move_queue(&session_id, &queue_id, revision, before_queue_id).await {
407 Ok((id, rev, pos)) => send_event(&output_tx, ServerEvent::QueueMoved { request_id, session_id: current.id.clone(), seq: next_seq(¤t.seq), queue_id: id, position: pos, revision: rev }, current.config.protocol.max_server_frame_bytes).await?,
408 Err(code) => send_error(&output_tx, &request_id, code, "queue entry was not found or revision is stale", false, server_frame_limit(&session)).await?,
409 }
410 }
411 ClientMessage::QueueRemove { request_id, session_id, queue_id, revision } => {
412 let Some(current) = session.as_ref() else { send_error(&output_tx, &request_id, "session_not_found", "start a session first", false, server_frame_limit(&session)).await?; continue; };
413 match current.remove_queue(&session_id, &queue_id, revision).await {
414 Ok((id, rev)) => send_event(&output_tx, ServerEvent::QueueRemoved { request_id, session_id: current.id.clone(), seq: next_seq(¤t.seq), queue_id: id, revision: rev }, current.config.protocol.max_server_frame_bytes).await?,
415 Err(code) => send_error(&output_tx, &request_id, code, "queue entry was not found or revision is stale", false, server_frame_limit(&session)).await?,
416 }
417 }
418 ClientMessage::SessionPause { request_id, session_id, paused } => {
419 let Some(current) = session.as_ref() else { send_error(&output_tx, &request_id, "session_not_found", "start a session first", false, server_frame_limit(&session)).await?; continue; };
420 if current.id != session_id { send_error(&output_tx, &request_id, "session_not_found", "session id does not match", false, server_frame_limit(&session)).await?; continue; }
421 current.paused.store(paused, Ordering::Release);
422 send_event(&output_tx, ServerEvent::SessionPaused { request_id, session_id: current.id.clone(), seq: next_seq(¤t.seq), paused }, current.config.protocol.max_server_frame_bytes).await?;
423 }
424 ClientMessage::TurnCancel { request_id, session_id, turn_id } => {
425 match (&session, &active) {
426 (Some(current), Some(running)) if current.id == session_id && running.turn_id == turn_id => running.cancellation.cancel(),
427 _ => send_error(&output_tx, &request_id, "turn_not_found", "active turn was not found", false, server_frame_limit(&session)).await?,
428 }
429 }
430 ClientMessage::ApprovalResolve { request_id, session_id, approval_id, approved } => {
431 if session.as_ref().is_none_or(|current| current.id != session_id) {
432 send_error(&output_tx, &request_id, "session_not_found", "session id does not match", false, server_frame_limit(&session)).await?;
433 } else if !approvals.resolve(&approval_id, approved).await {
434 send_error(&output_tx, &request_id, "approval_not_found", "approval was not found or already resolved", false, server_frame_limit(&session)).await?;
435 }
436 }
437 ClientMessage::SessionClear { request_id, session_id } => {
438 let Some(current) = session.as_ref() else {
439 send_error(&output_tx, &request_id, "session_not_found", "session was not found", false, server_frame_limit(&session)).await?;
440 continue;
441 };
442 if current.id != session_id {
443 send_error(&output_tx, &request_id, "session_not_found", "session id does not match", false, server_frame_limit(&session)).await?;
444 } else if active.is_some() {
445 send_error(&output_tx, &request_id, "turn_active", "cancel the active turn before clearing", false, server_frame_limit(&session)).await?;
446 } else {
447 current.history.lock().await.clear();
448 current.queue.lock().await.clear();
449 send_event(&output_tx, ServerEvent::SessionCleared {
450 request_id,
451 session_id: current.id.clone(),
452 seq: next_seq(¤t.seq),
453 }, current.config.protocol.max_server_frame_bytes).await?;
454 send_event(&output_tx, ServerEvent::QueueSnapshot { request_id: None, session_id: current.id.clone(), seq: next_seq(¤t.seq), entries: Vec::new(), paused: current.paused.load(Ordering::Acquire) }, current.config.protocol.max_server_frame_bytes).await?;
455 }
456 }
457 }
458 }
459 writer = &mut writer_task => {
460 writer_finished = true;
461 writer.context("join protocol writer")??;
462 break;
463 }
464 done = done_rx.recv(), if active.is_some() => {
465 if let Some(done) = done {
466 if let Some(current) = session.as_ref() {
467 let seq = next_seq(¤t.seq);
468 let event = match done.result {
469 Ok(outcome) => ServerEvent::TurnCompleted {
470 request_id: done.request_id,
471 session_id: done.session_id,
472 turn_id: done.turn_id,
473 seq,
474 steps: outcome.steps,
475 usage: Usage { input_tokens: outcome.usage.input_tokens, output_tokens: outcome.usage.output_tokens },
476 },
477 Err(AgentError::Cancelled) => ServerEvent::TurnCancelled {
478 request_id: done.request_id,
479 session_id: done.session_id,
480 turn_id: done.turn_id,
481 seq,
482 },
483 Err(error) => ServerEvent::TurnFailed {
484 request_id: done.request_id,
485 session_id: done.session_id,
486 turn_id: done.turn_id,
487 seq,
488 code: error.code().into(),
489 message: error.to_string(),
490 },
491 };
492 send_event(&output_tx, event, current.config.protocol.max_server_frame_bytes).await?;
493 }
494 if let Some(mut active) = active.take() {
495 let _ = (&mut active.task).await;
496 }
497 if let Some(current) = session.as_ref()
498 && !current.paused.load(Ordering::Acquire)
499 && let Some(entry) = current.queue.lock().await.pop_front()
500 {
501 let turn_id = Uuid::new_v4().to_string();
502 let cancellation = cancellation.child_token();
503 send_event(&output_tx, ServerEvent::QueueDequeued {
504 request_id: entry.submitter.clone(),
505 session_id: current.id.clone(),
506 seq: next_seq(¤t.seq),
507 queue_id: entry.queue_id,
508 turn_id: turn_id.clone(),
509 }, current.config.protocol.max_server_frame_bytes).await?;
510 send_event(&output_tx, ServerEvent::TurnStarted {
511 request_id: entry.submitter.clone(),
512 session_id: current.id.clone(),
513 turn_id: turn_id.clone(),
514 seq: next_seq(¤t.seq),
515 }, current.config.protocol.max_server_frame_bytes).await?;
516 let meta = TurnMeta {
517 request_id: entry.submitter.clone(), session_id: current.id.clone(), turn_id: turn_id.clone(),
518 seq: Arc::clone(¤t.seq), max_server_frame: current.config.protocol.max_server_frame_bytes,
519 };
520 let sink: Arc<dyn EventSink> = Arc::new(ProtocolSink { meta: meta.clone(), output: output_tx.clone(), cancellation: cancellation.clone() });
521 let gate: Arc<dyn ApprovalGate> = Arc::new(ProtocolApprovalGate { policy: current.config.tools.approval_policy, broker: Arc::clone(&approvals), meta, output: output_tx.clone() });
522 let runtime = Arc::clone(¤t.runtime);
523 let history = Arc::clone(¤t.history);
524 let task_done = done_tx.clone();
525 let task_request = entry.submitter;
526 let task_session = current.id.clone();
527 let task_turn = turn_id.clone();
528 let task_cancel = cancellation.clone();
529 let task = tasks.spawn(async move {
530 let mut history = history.lock().await;
531 let result = runtime.run_turn(&mut history, entry.prompt, sink, gate, task_cancel).await;
532 let _ = task_done.send(TurnDone { request_id: task_request, session_id: task_session, turn_id: task_turn, result }).await;
533 });
534 active = Some(ActiveTurn { turn_id, cancellation, task });
535 }
536 }
537 }
538 }
539 }
540 Ok(())
541 }
542 .await;
543
544 if let Some(active) = active.take() {
545 shutdown_active_turn(active, SHUTDOWN_GRACE).await;
546 }
547 drop(output_tx);
548 let writer_result = if writer_finished {
549 Ok(())
550 } else {
551 shutdown_writer(writer_task, SHUTDOWN_GRACE).await
552 };
553 loop_result?;
554 writer_result?;
555 if fatal {
556 return Err(anyhow!("protocol version mismatch"));
557 }
558 Ok(())
559}
560
561enum FrameRead {
562 Eof,
563 Frame(Vec<u8>),
564 TooLarge,
565}
566
567struct OutboundFrame {
568 bytes: Vec<u8>,
569 _byte_permit: OwnedSemaphorePermit,
570}
571
572#[derive(Clone)]
573struct OutboundSender {
574 frames: mpsc::Sender<OutboundFrame>,
575 budget: Arc<Semaphore>,
576 capacity: Arc<AtomicUsize>,
577}
578
579#[derive(Debug, PartialEq, Eq)]
580enum OutboundSendError {
581 Cancelled,
582 Closed,
583 TimedOut,
584 FrameExceedsQueue { frame_bytes: usize, capacity: usize },
585}
586
587impl std::fmt::Display for OutboundSendError {
588 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
589 match self {
590 Self::Cancelled => formatter.write_str("outbound send cancelled"),
591 Self::Closed => formatter.write_str("protocol client disconnected"),
592 Self::TimedOut => formatter.write_str("outbound send timed out under backpressure"),
593 Self::FrameExceedsQueue {
594 frame_bytes,
595 capacity,
596 } => write!(
597 formatter,
598 "outbound frame uses {frame_bytes} bytes but queue capacity is {capacity} bytes"
599 ),
600 }
601 }
602}
603
604impl std::error::Error for OutboundSendError {}
605
606fn outbound_channel(capacity: usize) -> (OutboundSender, mpsc::Receiver<OutboundFrame>) {
607 let (frames, receiver) = mpsc::channel(OUTPUT_QUEUE_CAPACITY);
608 (
609 OutboundSender {
610 frames,
611 budget: Arc::new(Semaphore::new(capacity)),
612 capacity: Arc::new(AtomicUsize::new(capacity)),
613 },
614 receiver,
615 )
616}
617
618impl OutboundSender {
619 fn ensure_capacity(&self, required: usize) -> Result<()> {
620 if required > Semaphore::MAX_PERMITS {
621 return Err(anyhow!(
622 "outbound queue capacity {required} exceeds runtime limit {}",
623 Semaphore::MAX_PERMITS
624 ));
625 }
626 let current = self.capacity.load(Ordering::Acquire);
627 if required > current {
628 self.budget.add_permits(required - current);
629 self.capacity.store(required, Ordering::Release);
630 }
631 Ok(())
632 }
633
634 async fn send(
635 &self,
636 bytes: Vec<u8>,
637 cancellation: Option<&CancellationToken>,
638 ) -> std::result::Result<(), OutboundSendError> {
639 self.send_with_timeout(bytes, cancellation, SHUTDOWN_GRACE)
640 .await
641 }
642
643 async fn send_with_timeout(
644 &self,
645 bytes: Vec<u8>,
646 cancellation: Option<&CancellationToken>,
647 control_timeout: Duration,
648 ) -> std::result::Result<(), OutboundSendError> {
649 let frame_bytes =
650 bytes
651 .len()
652 .checked_add(1)
653 .ok_or(OutboundSendError::FrameExceedsQueue {
654 frame_bytes: usize::MAX,
655 capacity: self.capacity.load(Ordering::Acquire),
656 })?;
657 let capacity = self.capacity.load(Ordering::Acquire);
658 let permits =
659 u32::try_from(frame_bytes).map_err(|_| OutboundSendError::FrameExceedsQueue {
660 frame_bytes,
661 capacity,
662 })?;
663 if frame_bytes > capacity {
664 return Err(OutboundSendError::FrameExceedsQueue {
665 frame_bytes,
666 capacity,
667 });
668 }
669
670 let control_deadline = tokio::time::Instant::now() + control_timeout;
671 let acquire = Arc::clone(&self.budget).acquire_many_owned(permits);
672 let permit = if let Some(cancellation) = cancellation {
673 tokio::select! {
674 biased;
675 _ = cancellation.cancelled() => return Err(OutboundSendError::Cancelled),
676 permit = acquire => permit.map_err(|_| OutboundSendError::Closed)?,
677 }
678 } else {
679 tokio::time::timeout_at(control_deadline, acquire)
680 .await
681 .map_err(|_| OutboundSendError::TimedOut)?
682 .map_err(|_| OutboundSendError::Closed)?
683 };
684 let frame = OutboundFrame {
685 bytes,
686 _byte_permit: permit,
687 };
688 if let Some(cancellation) = cancellation {
689 tokio::select! {
690 biased;
691 _ = cancellation.cancelled() => Err(OutboundSendError::Cancelled),
692 result = self.frames.send(frame) => result.map_err(|_| OutboundSendError::Closed),
693 }
694 } else {
695 tokio::time::timeout_at(control_deadline, self.frames.send(frame))
696 .await
697 .map_err(|_| OutboundSendError::TimedOut)?
698 .map_err(|_| OutboundSendError::Closed)
699 }
700 }
701}
702
703fn output_queue_bytes(max_frame_bytes: usize) -> Result<usize> {
704 let required = max_frame_bytes
705 .checked_add(1)
706 .and_then(|bytes| bytes.checked_mul(2))
707 .ok_or_else(|| anyhow!("configured server frame limit is too large"))?
708 .max(OUTPUT_QUEUE_MIN_BYTES);
709 if required > Semaphore::MAX_PERMITS {
710 return Err(anyhow!(
711 "configured server frame limit requires an outbound queue larger than the runtime supports"
712 ));
713 }
714 Ok(required)
715}
716
717#[derive(Default)]
719struct FrameBuffer {
720 bytes: Vec<u8>,
721 oversized: bool,
722}
723
724impl FrameBuffer {
725 async fn read<R>(&mut self, reader: &mut R, max_bytes: usize) -> std::io::Result<FrameRead>
726 where
727 R: AsyncBufRead + Unpin,
728 {
729 loop {
730 let available = reader.fill_buf().await?;
731 let eof = available.is_empty();
732 let end = available.iter().position(|b| *b == b'\n');
733 let take = end.map_or(available.len(), |n| n + 1);
734 if !self.oversized {
735 if self.bytes.len().saturating_add(take) > max_bytes.saturating_add(2) {
736 self.oversized = true;
737 self.bytes.clear();
738 } else {
739 self.bytes.extend_from_slice(&available[..take]);
740 }
741 }
742 reader.consume(take);
743 if end.is_some() || eof {
744 if std::mem::take(&mut self.oversized) {
745 return Ok(FrameRead::TooLarge);
746 }
747 if eof && self.bytes.is_empty() {
748 return Ok(FrameRead::Eof);
749 }
750 let mut bytes = std::mem::take(&mut self.bytes);
751 while matches!(bytes.last(), Some(b'\n' | b'\r')) {
752 bytes.pop();
753 }
754 return Ok(if bytes.len() > max_bytes {
755 FrameRead::TooLarge
756 } else {
757 FrameRead::Frame(bytes)
758 });
759 }
760 }
761 }
762}
763
764#[cfg(test)]
765async fn read_bounded_frame<R: AsyncBufRead + Unpin>(
766 reader: &mut R,
767 max_bytes: usize,
768) -> std::io::Result<FrameRead> {
769 FrameBuffer::default().read(reader, max_bytes).await
770}
771
772struct SocketLock(std::fs::File);
774impl SocketLock {
775 fn acquire(socket: &Path) -> Result<Self> {
776 use std::os::unix::{fs::OpenOptionsExt, io::AsRawFd};
777 let file = std::fs::OpenOptions::new()
778 .read(true)
779 .write(true)
780 .create(true)
781 .truncate(false)
782 .mode(0o600)
783 .custom_flags(libc::O_NOFOLLOW)
784 .open(socket.with_extension("lock"))?;
785 if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 {
787 return Err(anyhow!("SCV daemon already owns this socket"));
788 }
789 Ok(Self(file))
790 }
791}
792impl Drop for SocketLock {
793 fn drop(&mut self) {
794 use std::os::unix::io::AsRawFd;
795 unsafe {
797 libc::flock(self.0.as_raw_fd(), libc::LOCK_UN);
798 }
799 }
800}
801
802fn server_frame_limit(session: &Option<Session>) -> usize {
803 session.as_ref().map_or_else(
804 || Config::default().protocol.max_server_frame_bytes,
805 |value| value.config.protocol.max_server_frame_bytes,
806 )
807}
808
809struct Session {
810 id: String,
811 workspace: PathBuf,
812 config: Config,
813 runtime: Arc<AgentRuntime>,
814 history: Arc<Mutex<Vec<Message>>>,
815 seq: Arc<AtomicU64>,
816 queue: Arc<Mutex<VecDeque<QueueEntry>>>,
817 paused: Arc<std::sync::atomic::AtomicBool>,
818}
819
820impl Session {
821 async fn enqueue(
822 &self,
823 prompt: String,
824 submitter: String,
825 ) -> std::result::Result<QueueEntry, &'static str> {
826 let entry = QueueEntry {
827 queue_id: Uuid::new_v4().to_string(),
828 revision: 1,
829 prompt,
830 submitter,
831 };
832 let mut queue = self.queue.lock().await;
833 let bytes: usize = queue.iter().map(|item| item.prompt.len()).sum();
834 if queue.len() >= MAX_QUEUE_ITEMS
835 || bytes.saturating_add(entry.prompt.len()) > MAX_QUEUE_BYTES
836 {
837 return Err("queue_limit");
838 }
839 queue.push_back(entry.clone());
840 Ok(entry)
841 }
842
843 async fn update_queue(
844 &self,
845 id: &str,
846 revision: u64,
847 prompt: String,
848 ) -> std::result::Result<QueueEntry, &'static str> {
849 let mut queue = self.queue.lock().await;
850 let bytes: usize = queue.iter().map(|item| item.prompt.len()).sum();
851 let entry = queue
852 .iter_mut()
853 .find(|entry| entry.queue_id == id)
854 .ok_or("queue_not_found")?;
855 if entry.revision != revision {
856 return Err("queue_conflict");
857 }
858 if bytes
859 .saturating_sub(entry.prompt.len())
860 .saturating_add(prompt.len())
861 > MAX_QUEUE_BYTES
862 {
863 return Err("queue_limit");
864 }
865 entry.prompt = prompt;
866 entry.revision += 1;
867 Ok(entry.clone())
868 }
869
870 async fn move_queue(
871 &self,
872 session_id: &str,
873 id: &str,
874 revision: u64,
875 before: Option<String>,
876 ) -> std::result::Result<(String, u64, usize), &'static str> {
877 if self.id != session_id {
878 return Err("session_not_found");
879 }
880 let mut queue = self.queue.lock().await;
881 let index = queue
882 .iter()
883 .position(|entry| entry.queue_id == id)
884 .ok_or("queue_not_found")?;
885 if queue[index].revision != revision {
886 return Err("queue_conflict");
887 }
888 let target_index = match before.as_deref() {
891 Some(target) if target == id => return Ok((id.to_string(), revision, index)),
892 Some(target) => Some(
893 queue
894 .iter()
895 .position(|item| item.queue_id == target)
896 .ok_or("queue_not_found")?,
897 ),
898 None => None,
899 };
900 let mut entry = queue.remove(index).expect("queue index exists");
901 let target = target_index.map_or(queue.len(), |target| {
902 target.saturating_sub(usize::from(target > index))
903 });
904 let pos = target.min(queue.len());
905 let id = entry.queue_id.clone();
906 let rev = entry.revision + 1;
907 entry.revision = rev;
908 queue.insert(pos, entry);
909 Ok((id, rev, pos))
910 }
911
912 async fn remove_queue(
913 &self,
914 session_id: &str,
915 id: &str,
916 revision: u64,
917 ) -> std::result::Result<(String, u64), &'static str> {
918 if self.id != session_id {
919 return Err("session_not_found");
920 }
921 let mut queue = self.queue.lock().await;
922 let index = queue
923 .iter()
924 .position(|entry| entry.queue_id == id)
925 .ok_or("queue_not_found")?;
926 if queue[index].revision != revision {
927 return Err("queue_conflict");
928 }
929 let entry = queue.remove(index).expect("queue index exists");
930 Ok((entry.queue_id, entry.revision))
931 }
932}
933
934struct ActiveTurn {
935 turn_id: String,
936 cancellation: CancellationToken,
937 task: JoinHandle<()>,
938}
939
940impl Drop for ActiveTurn {
941 fn drop(&mut self) {
942 self.cancellation.cancel();
943 self.task.abort();
944 }
945}
946
947struct AbortGuard(tokio::task::AbortHandle);
948impl Drop for AbortGuard {
949 fn drop(&mut self) {
950 self.0.abort();
951 }
952}
953
954async fn shutdown_active_turn(mut active: ActiveTurn, grace: Duration) -> bool {
955 active.cancellation.cancel();
956 if tokio::time::timeout(grace, &mut active.task).await.is_ok() {
957 true
958 } else {
959 active.task.abort();
960 let _ = (&mut active.task).await;
961 false
962 }
963}
964
965async fn shutdown_writer(
966 mut writer: JoinHandle<std::io::Result<()>>,
967 grace: Duration,
968) -> Result<()> {
969 match tokio::time::timeout(grace, &mut writer).await {
970 Ok(result) => {
971 result.context("join protocol writer")??;
972 Ok(())
973 }
974 Err(_) => {
975 writer.abort();
976 let _ = writer.await;
977 Err(anyhow!("protocol writer shutdown timed out"))
978 }
979 }
980}
981
982struct TurnDone {
983 request_id: String,
984 session_id: String,
985 turn_id: String,
986 result: Result<scv_core::TurnOutcome, AgentError>,
987}
988
989#[derive(Clone)]
990struct TurnMeta {
991 request_id: String,
992 session_id: String,
993 turn_id: String,
994 seq: Arc<AtomicU64>,
995 max_server_frame: usize,
996}
997
998fn next_seq(sequence: &AtomicU64) -> u64 {
999 sequence.fetch_add(1, Ordering::Relaxed) + 1
1000}
1001
1002async fn build_session(cwd: &str, overrides: ConfigOverrides) -> Result<Session> {
1003 let workspace = std::fs::canonicalize(cwd).with_context(|| format!("resolve cwd {cwd}"))?;
1004 if !workspace.is_dir() {
1005 return Err(anyhow!("cwd is not a directory"));
1006 }
1007 let no_tools = overrides.no_tools;
1008 let config = Config::load(&workspace, overrides)?;
1009 let provider_config = config.provider.clone();
1010 let api_key = provider_config.api_key.clone().or_else(|| {
1011 provider_config.api_key_env.as_deref().and_then(|name| std::env::var(name).ok())
1012 }).filter(|key| !key.trim().is_empty()).ok_or_else(|| anyhow!("provider credential is not configured; set provider.api_key or provider.api_key_env"))?;
1013 let (skills, skill_roots, skill_prompt) = discover_skills(&workspace, &config)?;
1014 let system_prompt = build_system_prompt(&workspace, &config, &skill_prompt)?;
1015 let provider = Arc::new(OpenAiProvider::new(
1016 provider_config.model.clone(),
1017 provider_config.base_url.clone(),
1018 api_key,
1019 Duration::from_secs(provider_config.timeout_seconds),
1020 config.provider_limits(),
1021 provider_config.headers.clone(),
1022 )?);
1023 let tools = if no_tools {
1024 Arc::new(ToolRegistry::default())
1025 } else {
1026 Arc::new(builtin_registry(
1027 config.tools(),
1028 skills,
1029 skill_roots,
1030 config.skills.max_skill_bytes,
1031 config.adapters(),
1032 )?)
1033 };
1034 let context = Arc::new(BudgetContextPolicy::new((&config.context).into())?);
1035 let runtime = Arc::new(AgentRuntime::new(
1036 provider,
1037 tools,
1038 context,
1039 config.core_agent(system_prompt),
1040 workspace.clone(),
1041 ));
1042 Ok(Session {
1043 id: Uuid::new_v4().to_string(),
1044 workspace,
1045 config,
1046 runtime,
1047 history: Arc::new(Mutex::new(Vec::new())),
1048 seq: Arc::new(AtomicU64::new(0)),
1049 queue: Arc::new(Mutex::new(VecDeque::new())),
1050 paused: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1051 })
1052}
1053
1054fn build_system_prompt(workspace: &Path, config: &Config, skills: &str) -> Result<String> {
1055 let mut prompt = config.agent.system_prompt.clone();
1056 prompt.push_str(&format!(
1057 "\nCurrent working directory: {}\n",
1058 workspace.display()
1059 ));
1060 let agents_path = workspace.join("AGENTS.md");
1061 if agents_path.is_file() {
1062 let canonical = std::fs::canonicalize(&agents_path).context("resolve project AGENTS.md")?;
1063 if !canonical.starts_with(workspace) {
1064 return Err(anyhow!("project AGENTS.md escaped workspace"));
1065 }
1066 let (bytes, truncated) = read_prefix(&canonical, config.tools.max_read_bytes)
1067 .context("read project AGENTS.md")?;
1068 let instructions = std::str::from_utf8(&bytes).context("project AGENTS.md is not UTF-8")?;
1069 prompt.push_str("\n# Project instructions\n");
1070 prompt.push_str(instructions);
1071 if truncated {
1072 prompt.push_str("\n[AGENTS.md truncated by configured read limit]\n");
1073 }
1074 }
1075 if !skills.is_empty() {
1076 prompt.push_str("\n# Available skills\n");
1077 prompt.push_str(skills);
1078 prompt.push_str("\nUse read_skill with a skill name when its workflow applies.\n");
1079 }
1080 Ok(prompt)
1081}
1082
1083fn discover_skills(workspace: &Path, config: &Config) -> Result<(SkillMap, Vec<PathBuf>, String)> {
1084 let mut skills = SkillMap::new();
1085 let mut roots = Vec::new();
1086 let project_root = workspace.join(&config.skills.project_dir);
1087 for (root, must_be_workspace) in [(&project_root, true), (&config.skills.user_dir, false)] {
1088 if !root.is_dir() {
1089 continue;
1090 }
1091 let canonical = std::fs::canonicalize(root)
1092 .with_context(|| format!("resolve skill root {}", root.display()))?;
1093 if must_be_workspace && !canonical.starts_with(workspace) {
1094 return Err(anyhow!("project skill root escaped workspace"));
1095 }
1096 roots.push(canonical.clone());
1097 let mut entries: Vec<_> = std::fs::read_dir(&canonical)
1098 .with_context(|| format!("read skill root {}", canonical.display()))?
1099 .filter_map(Result::ok)
1100 .collect();
1101 entries.sort_by_key(|entry| entry.file_name());
1102 for entry in entries {
1103 if skills.len() >= config.skills.max_skills {
1104 break;
1105 }
1106 let path = entry.path().join("SKILL.md");
1107 if !path.is_file() {
1108 continue;
1109 }
1110 let canonical_file = std::fs::canonicalize(&path)
1111 .with_context(|| format!("resolve skill {}", path.display()))?;
1112 if !canonical_file.starts_with(&canonical) {
1113 continue;
1114 }
1115 let name = entry.file_name().to_string_lossy().to_string();
1116 skills.entry(name).or_insert(canonical_file);
1117 }
1118 }
1119 let mut names: Vec<_> = skills.keys().cloned().collect();
1120 names.sort();
1121 let mut listing = String::new();
1122 for name in names {
1123 let path = &skills[&name];
1124 let bytes = read_prefix(path, config.skills.max_skill_bytes)
1125 .map(|(bytes, _)| bytes)
1126 .unwrap_or_default();
1127 let content = String::from_utf8_lossy(&bytes);
1128 let description = skill_description(&content);
1129 listing.push_str(&format!("- {name}: {description}\n"));
1130 }
1131 Ok((skills, roots, listing))
1132}
1133
1134fn read_prefix(path: &Path, max_bytes: usize) -> std::io::Result<(Vec<u8>, bool)> {
1135 let file = std::fs::File::open(path)?;
1136 let mut bytes = Vec::with_capacity(max_bytes.min(8192));
1137 file.take(
1138 u64::try_from(max_bytes)
1139 .unwrap_or(u64::MAX)
1140 .saturating_add(1),
1141 )
1142 .read_to_end(&mut bytes)?;
1143 let truncated = bytes.len() > max_bytes;
1144 bytes.truncate(max_bytes);
1145 Ok((bytes, truncated))
1146}
1147
1148fn skill_description(content: &str) -> String {
1149 if let Some(frontmatter) = content.strip_prefix("---\n")
1150 && let Some((header, _)) = frontmatter.split_once("\n---")
1151 {
1152 for line in header.lines() {
1153 if let Some(description) = line.strip_prefix("description:") {
1154 return description.trim().trim_matches('"').to_owned();
1155 }
1156 }
1157 }
1158 content
1159 .lines()
1160 .map(str::trim)
1161 .find(|line| !line.is_empty() && !line.starts_with('#'))
1162 .unwrap_or("No description provided")
1163 .chars()
1164 .take(240)
1165 .collect()
1166}
1167
1168struct ProtocolSink {
1169 meta: TurnMeta,
1170 output: OutboundSender,
1171 cancellation: CancellationToken,
1172}
1173
1174#[async_trait]
1175impl EventSink for ProtocolSink {
1176 async fn emit(&self, event: CoreEvent) -> Result<(), AgentError> {
1177 let seq = next_seq(&self.meta.seq);
1178 let event = match event {
1179 CoreEvent::AssistantDelta { content } => ServerEvent::AssistantDelta {
1180 request_id: self.meta.request_id.clone(),
1181 session_id: self.meta.session_id.clone(),
1182 turn_id: self.meta.turn_id.clone(),
1183 seq,
1184 content,
1185 },
1186 CoreEvent::AssistantCompleted { content } => ServerEvent::AssistantCompleted {
1187 request_id: self.meta.request_id.clone(),
1188 session_id: self.meta.session_id.clone(),
1189 turn_id: self.meta.turn_id.clone(),
1190 seq,
1191 content,
1192 },
1193 CoreEvent::ToolProposed {
1194 call_id,
1195 name,
1196 arguments,
1197 } => ServerEvent::ToolProposed {
1198 request_id: self.meta.request_id.clone(),
1199 session_id: self.meta.session_id.clone(),
1200 turn_id: self.meta.turn_id.clone(),
1201 seq,
1202 call_id,
1203 name,
1204 arguments,
1205 },
1206 CoreEvent::ToolStarted { call_id, name } => ServerEvent::ToolStarted {
1207 request_id: self.meta.request_id.clone(),
1208 session_id: self.meta.session_id.clone(),
1209 turn_id: self.meta.turn_id.clone(),
1210 seq,
1211 call_id,
1212 name,
1213 },
1214 CoreEvent::ToolCompleted {
1215 call_id,
1216 name,
1217 output,
1218 } => ServerEvent::ToolCompleted {
1219 request_id: self.meta.request_id.clone(),
1220 session_id: self.meta.session_id.clone(),
1221 turn_id: self.meta.turn_id.clone(),
1222 seq,
1223 call_id,
1224 name,
1225 success: !output.is_error,
1226 output: output.content,
1227 truncated: output.truncated,
1228 },
1229 CoreEvent::ContextCompacted {
1230 before_tokens,
1231 after_tokens,
1232 removed_messages,
1233 } => ServerEvent::ContextCompacted {
1234 request_id: self.meta.request_id.clone(),
1235 session_id: self.meta.session_id.clone(),
1236 turn_id: self.meta.turn_id.clone(),
1237 seq,
1238 before_tokens,
1239 after_tokens,
1240 removed_messages,
1241 },
1242 CoreEvent::SessionTrimmed {
1243 removed_messages,
1244 history_bytes,
1245 } => ServerEvent::SessionTrimmed {
1246 request_id: self.meta.request_id.clone(),
1247 session_id: self.meta.session_id.clone(),
1248 seq,
1249 removed_messages,
1250 history_bytes,
1251 },
1252 };
1253 send_turn_event(
1254 &self.output,
1255 event,
1256 self.meta.max_server_frame,
1257 &self.cancellation,
1258 )
1259 .await
1260 }
1261}
1262
1263#[derive(Default)]
1264struct ApprovalBroker {
1265 pending: Mutex<HashMap<String, oneshot::Sender<bool>>>,
1266}
1267
1268impl ApprovalBroker {
1269 async fn insert(&self, id: String, sender: oneshot::Sender<bool>) {
1270 self.pending.lock().await.insert(id, sender);
1271 }
1272
1273 async fn remove(&self, id: &str) {
1274 self.pending.lock().await.remove(id);
1275 }
1276
1277 async fn resolve(&self, id: &str, approved: bool) -> bool {
1278 let sender = self.pending.lock().await.remove(id);
1279 sender.is_some_and(|sender| sender.send(approved).is_ok())
1280 }
1281}
1282
1283struct ProtocolApprovalGate {
1284 policy: ApprovalPolicy,
1285 broker: Arc<ApprovalBroker>,
1286 meta: TurnMeta,
1287 output: OutboundSender,
1288}
1289
1290#[async_trait]
1291impl ApprovalGate for ProtocolApprovalGate {
1292 async fn approve(
1293 &self,
1294 request: ApprovalRequest,
1295 cancellation: CancellationToken,
1296 ) -> Result<bool, AgentError> {
1297 match self.policy {
1298 ApprovalPolicy::OnRisk if request.risk == ToolRisk::ReadOnly => return Ok(true),
1299 ApprovalPolicy::Never => return Ok(request.risk == ToolRisk::ReadOnly),
1300 ApprovalPolicy::Always | ApprovalPolicy::OnRisk => {}
1301 }
1302 let approval_id = Uuid::new_v4().to_string();
1303 let (sender, receiver) = oneshot::channel();
1304 self.broker.insert(approval_id.clone(), sender).await;
1305 let event = ServerEvent::ApprovalRequested {
1306 request_id: self.meta.request_id.clone(),
1307 session_id: self.meta.session_id.clone(),
1308 turn_id: self.meta.turn_id.clone(),
1309 seq: next_seq(&self.meta.seq),
1310 approval_id: approval_id.clone(),
1311 call_id: request.call_id,
1312 name: request.name,
1313 risk: request.risk.as_str().into(),
1314 cwd: request.cwd.display().to_string(),
1315 summary: request.summary,
1316 };
1317 if let Err(error) = send_turn_event(
1318 &self.output,
1319 event,
1320 self.meta.max_server_frame,
1321 &cancellation,
1322 )
1323 .await
1324 {
1325 self.broker.remove(&approval_id).await;
1326 return Err(error);
1327 }
1328 tokio::select! {
1329 result = receiver => result.map_err(|_| AgentError::Cancelled),
1330 _ = cancellation.cancelled() => {
1331 self.broker.remove(&approval_id).await;
1332 Err(AgentError::Cancelled)
1333 }
1334 }
1335 }
1336}
1337
1338async fn send_event(output: &OutboundSender, event: ServerEvent, max_bytes: usize) -> Result<()> {
1339 let bytes = encode_event(&event, max_bytes)?;
1340 output.send(bytes, None).await.map_err(anyhow::Error::new)
1341}
1342
1343async fn send_turn_event(
1344 output: &OutboundSender,
1345 event: ServerEvent,
1346 max_bytes: usize,
1347 cancellation: &CancellationToken,
1348) -> Result<(), AgentError> {
1349 let bytes = encode_event(&event, max_bytes)
1350 .map_err(|error| AgentError::ResponseLimit(error.to_string()))?;
1351 match output.send(bytes, Some(cancellation)).await {
1352 Ok(()) => Ok(()),
1353 Err(OutboundSendError::Cancelled) => Err(AgentError::Cancelled),
1354 Err(error) => Err(AgentError::Internal(error.to_string())),
1355 }
1356}
1357
1358fn encode_event(event: &ServerEvent, max_bytes: usize) -> Result<Vec<u8>> {
1359 let bytes = serde_json::to_vec(event).context("serialize protocol event")?;
1360 if bytes.len() > max_bytes {
1361 return Err(anyhow!("server event exceeds configured frame limit"));
1362 }
1363 Ok(bytes)
1364}
1365
1366async fn send_error(
1367 output: &OutboundSender,
1368 request_id: &str,
1369 code: &str,
1370 message: &str,
1371 fatal: bool,
1372 max_bytes: usize,
1373) -> Result<()> {
1374 send_event(
1375 output,
1376 ServerEvent::Error {
1377 request_id: (!request_id.is_empty()).then(|| request_id.to_owned()),
1378 code: code.into(),
1379 message: message.into(),
1380 fatal,
1381 },
1382 max_bytes,
1383 )
1384 .await
1385}
1386
1387#[cfg(test)]
1388mod tests {
1389 use std::{
1390 future::pending,
1391 io::Cursor,
1392 sync::atomic::{AtomicBool, Ordering},
1393 };
1394
1395 use super::*;
1396
1397 struct DropSignal(Arc<AtomicBool>);
1398
1399 impl Drop for DropSignal {
1400 fn drop(&mut self) {
1401 self.0.store(true, Ordering::Release);
1402 }
1403 }
1404
1405 #[tokio::test]
1406 async fn nonreading_management_client_does_not_hold_component_lock() {
1407 let (mut input, server_input) = tokio::io::duplex(65536);
1408 let (server_output, _blocked_output) = tokio::io::duplex(1);
1409 let tasks = TaskTracker::new();
1410 let components = Arc::new(Mutex::new(components::Components::new(
1411 PathBuf::from("/unused.sock"),
1412 PathBuf::from("/"),
1413 )));
1414 let cancel = CancellationToken::new();
1415 let handler = tokio::spawn(run_managed(
1416 server_input,
1417 server_output,
1418 ConfigOverrides::default(),
1419 Some(components.clone()),
1420 cancel.clone(),
1421 tasks.clone(),
1422 ));
1423 input.write_all(b"{\"type\":\"initialize\",\"request_id\":\"init\",\"protocol_version\":2,\"client\":{\"name\":\"test\",\"version\":\"0\"}}\n").await.unwrap();
1424 for _ in 0..300 {
1425 input.write_all(b"{\"type\":\"daemon.control\",\"request_id\":\"s\",\"command\":{\"action\":\"status\"}}\n").await.unwrap();
1426 }
1427 tokio::time::sleep(Duration::from_millis(50)).await;
1428 let status = tokio::time::timeout(Duration::from_millis(100), async {
1429 components.lock().await.status()
1430 })
1431 .await
1432 .unwrap();
1433 assert_eq!(status.pid, std::process::id());
1434 cancel.cancel();
1435 handler.abort();
1436 let _ = handler.await;
1437 tasks.close();
1438 tokio::time::timeout(Duration::from_secs(1), tasks.wait())
1439 .await
1440 .unwrap();
1441 }
1442
1443 #[tokio::test]
1444 async fn forced_connection_abort_drops_and_joins_writer_descendants() {
1445 let (mut input, server_input) = tokio::io::duplex(512);
1446 let (server_output, _blocked_output) = tokio::io::duplex(1);
1447 let tasks = TaskTracker::new();
1448 let handler = tokio::spawn(run_managed(
1449 server_input,
1450 server_output,
1451 ConfigOverrides::default(),
1452 None,
1453 CancellationToken::new(),
1454 tasks.clone(),
1455 ));
1456 input.write_all(b"{\"type\":\"initialize\",\"request_id\":\"init\",\"protocol_version\":2,\"client\":{\"name\":\"test\",\"version\":\"0\"}}\n").await.unwrap();
1457 tokio::time::timeout(Duration::from_secs(1), async {
1458 while tasks.is_empty() {
1459 tokio::task::yield_now().await;
1460 }
1461 })
1462 .await
1463 .unwrap();
1464 handler.abort();
1465 let _ = handler.await;
1466 tasks.close();
1467 tokio::time::timeout(Duration::from_secs(1), tasks.wait())
1468 .await
1469 .unwrap();
1470 assert!(tasks.is_empty());
1471 }
1472
1473 #[tokio::test]
1474 async fn forced_handler_abort_cancels_and_joins_active_turn() {
1475 let tasks = TaskTracker::new();
1476 let cancellation = CancellationToken::new();
1477 let child_cancel = cancellation.child_token();
1478 let observed_cancel = child_cancel.clone();
1479 let dropped = Arc::new(AtomicBool::new(false));
1480 let (ready_tx, ready_rx) = oneshot::channel();
1481 let task = tasks.spawn({
1482 let dropped = dropped.clone();
1483 async move {
1484 let _guard = DropSignal(dropped);
1485 let _ = ready_tx.send(());
1486 pending::<()>().await;
1487 }
1488 });
1489 ready_rx.await.unwrap();
1490 let (owned_tx, owned_rx) = oneshot::channel();
1491 let handler = tokio::spawn(async move {
1492 let _active = ActiveTurn {
1493 turn_id: "test".into(),
1494 cancellation: child_cancel,
1495 task,
1496 };
1497 let _ = owned_tx.send(());
1498 pending::<()>().await;
1499 });
1500 owned_rx.await.unwrap();
1501 handler.abort();
1502 let _ = handler.await;
1503 tasks.close();
1504 tokio::time::timeout(Duration::from_secs(1), tasks.wait())
1505 .await
1506 .unwrap();
1507 assert!(observed_cancel.is_cancelled());
1508 assert!(dropped.load(Ordering::Acquire));
1509 }
1510
1511 #[tokio::test]
1512 async fn frame_buffer_preserves_partial_and_discard_state_across_cancellation() {
1513 let (mut input, output) = tokio::io::duplex(64);
1514 let mut reader = BufReader::new(output);
1515 let mut frames = FrameBuffer::default();
1516 input.write_all(b"12").await.unwrap();
1517 assert!(
1518 tokio::time::timeout(Duration::from_millis(10), frames.read(&mut reader, 4))
1519 .await
1520 .is_err()
1521 );
1522 input.write_all(b"34\n").await.unwrap();
1523 assert!(
1524 matches!(frames.read(&mut reader, 4).await.unwrap(), FrameRead::Frame(value) if value == b"1234")
1525 );
1526 input.write_all(b"123456789").await.unwrap();
1527 assert!(
1528 tokio::time::timeout(Duration::from_millis(10), frames.read(&mut reader, 4))
1529 .await
1530 .is_err()
1531 );
1532 input.write_all(b"\n{}\n").await.unwrap();
1533 assert!(matches!(
1534 frames.read(&mut reader, 4).await.unwrap(),
1535 FrameRead::TooLarge
1536 ));
1537 assert!(
1538 matches!(frames.read(&mut reader, 4).await.unwrap(), FrameRead::Frame(value) if value == b"{}")
1539 );
1540 }
1541
1542 #[tokio::test]
1543 async fn bounded_reader_discards_an_oversized_line() {
1544 let input = format!("{}\n{{}}\n", "x".repeat(10));
1545 let mut reader = BufReader::new(Cursor::new(input.into_bytes()));
1546 assert!(matches!(
1547 read_bounded_frame(&mut reader, 4).await.unwrap(),
1548 FrameRead::TooLarge
1549 ));
1550 match read_bounded_frame(&mut reader, 4).await.unwrap() {
1551 FrameRead::Frame(frame) => assert_eq!(frame, b"{}"),
1552 _ => panic!("expected the frame following the oversized line"),
1553 }
1554 }
1555
1556 #[tokio::test]
1557 async fn bounded_reader_accepts_exact_crlf_limit() {
1558 let mut reader = BufReader::new(Cursor::new(b"1234\r\n".to_vec()));
1559 match read_bounded_frame(&mut reader, 4).await.unwrap() {
1560 FrameRead::Frame(frame) => assert_eq!(frame, b"1234"),
1561 _ => panic!("expected an exact-limit frame"),
1562 }
1563 }
1564
1565 #[tokio::test]
1566 async fn outbound_byte_backpressure_is_cancellation_aware() {
1567 let (output, mut receiver) = outbound_channel(5);
1568 output.send(vec![0; 4], None).await.unwrap();
1569
1570 let cancellation = CancellationToken::new();
1571 let blocked = tokio::spawn({
1572 let output = output.clone();
1573 let cancellation = cancellation.clone();
1574 async move { output.send(vec![1; 4], Some(&cancellation)).await }
1575 });
1576 tokio::task::yield_now().await;
1577 assert!(!blocked.is_finished());
1578
1579 cancellation.cancel();
1580 assert_eq!(blocked.await.unwrap(), Err(OutboundSendError::Cancelled));
1581
1582 drop(receiver.recv().await.unwrap());
1583 output.send(vec![2; 4], None).await.unwrap();
1584 }
1585
1586 #[tokio::test]
1587 async fn outbound_control_send_times_out_under_byte_backpressure() {
1588 let (output, _receiver) = outbound_channel(5);
1589 output.send(vec![0; 4], None).await.unwrap();
1590 let result = output
1591 .send_with_timeout(vec![1; 4], None, Duration::from_millis(10))
1592 .await;
1593 assert_eq!(result, Err(OutboundSendError::TimedOut));
1594 }
1595
1596 #[tokio::test]
1597 async fn active_turn_shutdown_aborts_after_grace_period() {
1598 let cancellation = CancellationToken::new();
1599 let dropped = Arc::new(AtomicBool::new(false));
1600 let (started_tx, started_rx) = oneshot::channel();
1601 let task = tokio::spawn({
1602 let dropped = Arc::clone(&dropped);
1603 async move {
1604 let _signal = DropSignal(dropped);
1605 let _ = started_tx.send(());
1606 pending::<()>().await;
1607 }
1608 });
1609 started_rx.await.unwrap();
1610
1611 let graceful = shutdown_active_turn(
1612 ActiveTurn {
1613 turn_id: "turn".into(),
1614 cancellation,
1615 task,
1616 },
1617 Duration::from_millis(10),
1618 )
1619 .await;
1620
1621 assert!(!graceful);
1622 assert!(dropped.load(Ordering::Acquire));
1623 }
1624
1625 #[tokio::test]
1626 async fn writer_shutdown_aborts_after_grace_period() {
1627 let dropped = Arc::new(AtomicBool::new(false));
1628 let (started_tx, started_rx) = oneshot::channel();
1629 let writer = tokio::spawn({
1630 let dropped = Arc::clone(&dropped);
1631 async move {
1632 let _signal = DropSignal(dropped);
1633 let _ = started_tx.send(());
1634 pending::<std::io::Result<()>>().await
1635 }
1636 });
1637 started_rx.await.unwrap();
1638
1639 let result = shutdown_writer(writer, Duration::from_millis(10)).await;
1640
1641 assert!(result.is_err());
1642 assert!(dropped.load(Ordering::Acquire));
1643 }
1644}