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