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