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