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