Skip to main content

scv_server/
lib.rs

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