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